Skip to content

Commit 3be4c8f

Browse files
committed
Capture type variables from adapted OpCandidate
1 parent 94404b6 commit 3be4c8f

File tree

2 files changed

+146
-46
lines changed

2 files changed

+146
-46
lines changed

scijava-ops-engine/src/main/java/org/scijava/ops/engine/matcher/adapt/AdaptationMatchingRoutine.java

Lines changed: 53 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import java.util.HashMap;
3939
import java.util.List;
4040
import java.util.Map;
41+
import java.util.function.Consumer;
4142
import java.util.function.Function;
4243
import java.util.stream.Collectors;
4344

@@ -61,6 +62,7 @@
6162
import org.scijava.priority.Priority;
6263
import org.scijava.struct.FunctionalMethodType;
6364
import org.scijava.struct.ItemIO;
65+
import org.scijava.types.Any;
6466
import org.scijava.types.Nil;
6567
import org.scijava.types.Types;
6668
import org.scijava.types.inference.GenericAssignability;
@@ -117,63 +119,33 @@ public OpCandidate findMatch(MatchingConditions conditions, OpMatcher matcher,
117119
}
118120

119121
try {
120-
// resolve adaptor dependencies
121-
final Map<TypeVariable<?>, Type> adaptorBounds = new HashMap<>();
122-
final Map<TypeVariable<?>, Type> dependencyBounds = new HashMap<>();
122+
// grab the first type parameter from the OpInfo and search for
123+
// an Op that will then be adapted (this will be the only input of the
124+
// adaptor since we know it is a Function)
125+
Type adaptFrom = adaptor.inputTypes().get(0);
126+
Type srcOpType = Types.substituteTypeVariables(adaptFrom, map);
127+
final OpRequest srcOpRequest = inferOpRequest(srcOpType, conditions
128+
.request().getName(), map);
129+
final OpCandidate srcCandidate = matcher.match(MatchingConditions.from(
130+
srcOpRequest, adaptationHints), env);
131+
// Then, once we've matched an Op, use the bounds of that match
132+
// to refine the bounds on the adaptor (for dependency matching)
133+
captureTypeVarsFromCandidate(adaptFrom, srcCandidate, map);
134+
// Finally, resolve the adaptor's dependencies
123135
List<InfoTree> depTrees = Infos.dependencies(adaptor).stream() //
124136
.map(d -> {
125137
OpRequest request = inferOpRequest(d, map);
126138
Nil<?> type = Nil.of(request.getType());
127139
Nil<?>[] args = Arrays.stream(request.getArgs()).map(Nil::of)
128140
.toArray(Nil[]::new);
129141
Nil<?> outType = Nil.of(request.getOutType());
130-
InfoTree tree = env.infoTree(request.getName(), type, args, outType,
142+
return env.infoTree(request.getName(), type, args, outType,
131143
adaptationHints);
132-
// Check if the bounds of the dependency can inform the type of the
133-
// adapted Op
134-
final Type matchedOpType = tree.info().opType();
135-
// Find adaptor type variable bounds fulfilled by matched Op
136-
GenericAssignability.inferTypeVariables( //
137-
new Type[] { d.getType() }, //
138-
new Type[] { matchedOpType }, //
139-
dependencyBounds //
140-
);
141-
for (TypeVariable<?> typeVar : map.keySet()) {
142-
// Ignore TypeVariables not present in this particular dependency
143-
if (!dependencyBounds.containsKey(typeVar)) continue;
144-
Type matchedType = dependencyBounds.get(typeVar);
145-
// Resolve any type variables from the dependency request that we
146-
// can
147-
GenericAssignability.inferTypeVariables( //
148-
new Type[] { request.getType() }, //
149-
new Type[] { matchedOpType }, //
150-
adaptorBounds //
151-
);
152-
Type mapped = Types.mapVarToTypes(matchedType, adaptorBounds);
153-
// If the type variable is more specific now, update it
154-
if (mapped != null && Types.isAssignable(mapped, map.get(
155-
typeVar)))
156-
{
157-
map.put(typeVar, mapped);
158-
}
159-
}
160-
dependencyBounds.clear();
161-
return tree;
162144
}).collect(Collectors.toList());
163-
InfoTree adaptorChain = new InfoTree(adaptor, depTrees);
164-
165-
// grab the first type parameter from the OpInfo and search for
166-
// an Op that will then be adapted (this will be the only input of the
167-
// adaptor since we know it is a Function)
168-
Type srcOpType = Types.substituteTypeVariables(adaptor.inputs().get(0)
169-
.getType(), map);
170-
final OpRequest srcOpRequest = inferOpRequest(srcOpType, conditions
171-
.request().getName(), map);
172-
final OpCandidate srcCandidate = matcher.match(MatchingConditions.from(
173-
srcOpRequest, adaptationHints), env);
174-
map.putAll(srcCandidate.typeVarAssigns());
145+
// And return the Adaptor, wrapped up into an OpCandidate
175146
Type adapterOpType = Types.substituteTypeVariables(adaptor.output()
176147
.getType(), map);
148+
InfoTree adaptorChain = new InfoTree(adaptor, depTrees);
177149
OpAdaptationInfo adaptedInfo = new OpAdaptationInfo(srcCandidate
178150
.opInfo(), adapterOpType, adaptorChain);
179151
OpCandidate adaptedCandidate = new OpCandidate(env, conditions
@@ -196,6 +168,41 @@ public OpCandidate findMatch(MatchingConditions conditions, OpMatcher matcher,
196168
throw agglomerated;
197169
}
198170

171+
/**
172+
* Helper method that captures all type variable mappings found in the search
173+
* for an Op that could satisfy an adaptor input {@code srcType}.
174+
*
175+
* @param srcType the type of the Op input to an adaptor
176+
* @param candidate the {@link OpCandidate} matched for the adaptor input
177+
* @param map the mapping
178+
*/
179+
private void captureTypeVarsFromCandidate(Type srcType, OpCandidate candidate,
180+
Map<TypeVariable<?>, Type> map)
181+
{
182+
Consumer<Map<TypeVariable<?>, Type>> typeVarConsumer = assigns -> {
183+
for (var key : assigns.keySet()) {
184+
if (map.containsKey(key)) {
185+
var existing = map.get(key);
186+
var replacement = assigns.get(key);
187+
// Ignore bounds that are weaker than current bounds.
188+
if (Types.isAssignable(existing, replacement) && !existing.equals(
189+
Any.class) && !(existing instanceof Any))
190+
{
191+
continue;
192+
}
193+
}
194+
map.put(key, assigns.get(key));
195+
}
196+
};
197+
// First, capture assignments between the Adaptor and the matched Op
198+
final Map<TypeVariable<?>, Type> srcBounds = new HashMap<>();
199+
GenericAssignability.inferTypeVariables(new Type[] { srcType }, new Type[] {
200+
candidate.getType() }, srcBounds);
201+
typeVarConsumer.accept(srcBounds);
202+
// Then, capture assignments between the original OpRef and the matched Op
203+
typeVarConsumer.accept(candidate.typeVarAssigns());
204+
}
205+
199206
private OpRequest inferOpRequest(OpDependencyMember<?> dependency,
200207
Map<TypeVariable<?>, Type> typeVarAssigns)
201208
{
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
2+
package org.scijava.ops.engine.adapt;
3+
4+
import org.junit.jupiter.api.Assertions;
5+
import org.junit.jupiter.api.BeforeAll;
6+
import org.junit.jupiter.api.Test;
7+
import org.scijava.function.Computers;
8+
import org.scijava.ops.engine.AbstractTestEnvironment;
9+
import org.scijava.ops.spi.*;
10+
import org.scijava.priority.Priority;
11+
import org.scijava.types.Any;
12+
13+
import java.util.ArrayList;
14+
import java.util.Arrays;
15+
import java.util.List;
16+
import java.util.function.Function;
17+
18+
/**
19+
* When the user requests an {@link Any} as the output, we should ensure that an
20+
* output is used that can satisfy the typing constraints of the underlying Op.
21+
* In other words, ensure that when an Op is matched for adaptation, we test
22+
* that its type variable assignments are captured and used for dependency
23+
* matching.
24+
*
25+
* @param <N>
26+
* @author Gabriel Selzer
27+
*/
28+
public class AdaptationTypeVariableCaptureTest<N extends Number> extends
29+
AbstractTestEnvironment implements OpCollection
30+
{
31+
32+
@BeforeAll
33+
public static void AddNeededOps() {
34+
ops.register(new Computer1ToFunction1ViaFunction<>());
35+
ops.register(new AdaptationTypeVariableCaptureTest<>());
36+
}
37+
38+
/** Adaptor */
39+
@OpClass(names = "engine.adapt")
40+
public static class Computer1ToFunction1ViaFunction<I, O> implements
41+
Function<Computers.Arity1<I, O>, Function<I, O>>, Op
42+
{
43+
44+
@OpDependency(name = "engine.create", adaptable = false)
45+
Function<I, O> creator;
46+
47+
/**
48+
* @param computer the Computer to adapt
49+
* @return computer, adapted into a Function
50+
*/
51+
@Override
52+
public Function<I, O> apply(Computers.Arity1<I, O> computer) {
53+
return (in) -> {
54+
O out = creator.apply(in);
55+
computer.compute(in, out);
56+
return out;
57+
};
58+
}
59+
60+
}
61+
62+
/** Op that should be adapted */
63+
@OpField(names = "test.adaptedCapture")
64+
public final Computers.Arity1<List<N>, List<Double>> original = (in, out) -> {
65+
out.clear();
66+
for (var n : in) {
67+
out.add(n.doubleValue());
68+
}
69+
};
70+
71+
/** Op that will match if type variables aren't captured */
72+
@OpField(names = "engine.create", priority = Priority.HIGH)
73+
public final Function<List<Byte>, List<Byte>> highCopier = (list) -> {
74+
throw new IllegalStateException("This Op should not be called");
75+
};
76+
77+
/** Op that should match if type variables are captured */
78+
@OpField(names = "engine.create", priority = Priority.LOW)
79+
public final Function<List<Byte>, List<Double>> lowCopier = (
80+
list) -> new ArrayList<>();
81+
82+
@Test
83+
public void testCapture() {
84+
List<Byte> in = Arrays.asList((byte) 0, (byte) 1);
85+
Object out = ops.op("test.adaptedCapture") //
86+
.arity1() //
87+
.input(in) //
88+
.apply();
89+
Assertions.assertInstanceOf(List.class, out);
90+
Assertions.assertEquals(2, in.size());
91+
}
92+
93+
}

0 commit comments

Comments
 (0)