Skip to content

Commit 56e438d

Browse files
authored
Make type substitution carry what is known about a type (#1229)
1 parent 0d2bb0f commit 56e438d

8 files changed

Lines changed: 475 additions & 69 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@
2828
An instance of `I` for type `X` may only be declared in the package declaring `I` or the one declaring
2929
`X`, and only once, so `I` for `X` means the same thing throughout a program regardless of imports.
3030

31+
- A type class bound is now usable from inside a closure, so a bounded generic can hand work to one:
32+
33+
interface Producer
34+
function produce() returns int
35+
36+
function indexLater<T: Indexable>(T x) returns Producer
37+
return () -> T.toIndex(x)
38+
39+
Substituting a type variable now carries the instance chosen for it along with the type, rather than the
40+
type alone, so lifting a body into a class of its own no longer loses it. Jass only for now: Lua reaches
41+
such a class through its interface and still reports the bound as unresolvable there.
42+
3143
- Added new pseudo-natives for debugging memory leaks:
3244

3345
// returns the maximum type id, can be usd to

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -461,59 +461,6 @@ public ImType case_ImTupleType(ImTupleType tt) {
461461
}
462462

463463

464-
// Helper method to substitute type variables
465-
private ImType substituteTypeVars(ImType type, Map<ImTypeVar, ImType> substitutions) {
466-
return type.match(new ImType.Matcher<ImType>() {
467-
@Override
468-
public ImType case_ImTypeVarRef(ImTypeVarRef typeVarRef) {
469-
ImType concrete = substitutions.get(typeVarRef.getTypeVariable());
470-
return concrete != null ? concrete : typeVarRef;
471-
}
472-
473-
@Override
474-
public ImType case_ImClassType(ImClassType classType) {
475-
// Recursively substitute in type arguments
476-
ImTypeArguments newArgs = JassIm.ImTypeArguments();
477-
for (ImTypeArgument arg : classType.getTypeArguments()) {
478-
ImType substituted = substituteTypeVars(arg.getType(), substitutions);
479-
newArgs.add(JassIm.ImTypeArgument(substituted, typeClassBindingFor(arg)));
480-
}
481-
return JassIm.ImClassType(classType.getClassDef(), newArgs);
482-
}
483-
484-
// For other types, return as-is
485-
@Override
486-
public ImType case_ImSimpleType(ImSimpleType t) {
487-
return t;
488-
}
489-
490-
@Override
491-
public ImType case_ImArrayType(ImArrayType t) {
492-
return t;
493-
}
494-
495-
@Override
496-
public ImType case_ImTupleType(ImTupleType t) {
497-
return t;
498-
}
499-
500-
@Override
501-
public ImType case_ImVoid(ImVoid t) {
502-
return t;
503-
}
504-
505-
@Override
506-
public ImType case_ImAnyType(ImAnyType t) {
507-
return t;
508-
}
509-
510-
@Override
511-
public ImType case_ImArrayTypeMulti(ImArrayTypeMulti t) {
512-
return t;
513-
}
514-
});
515-
}
516-
517464
public void pushStackframe(ImCompiletimeExpr f, WPos trace) {
518465
WLogger.trace(() -> "pushStackframe compiletime expr " + f);
519466
stackFrames.push(new ILStackFrame(f, trace));

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/ImAttrType.java

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,7 @@ public static ImType getType(ImFunctionCall e) {
3535
}
3636

3737
public static ImType substituteType(ImType type, List<ImTypeArgument> generics, List<ImTypeVar> typeVars) {
38-
return type.match(new TypeRewriteMatcher() {
39-
40-
@Override
41-
public ImType case_ImTypeVarRef(ImTypeVarRef t) {
42-
int index = typeVars.indexOf(t.getTypeVariable());
43-
if (index < 0) {
44-
return t;
45-
} else if (index >= generics.size()) {
46-
throw new RuntimeException("Could not find replacement for " + t + " when replacing " + typeVars + " with " + generics);
47-
}
48-
return generics.get(index).getType();
49-
}
50-
51-
});
38+
return TypeSubst.of(typeVars, generics).apply(type);
5239
}
5340

5441

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/TypeRewriteMatcher.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,17 @@ public ImType case_ImArrayType(ImArrayType t) {
5757
public ImType case_ImClassType(ImClassType t) {
5858
ImTypeArguments args = JassIm.ImTypeArguments();
5959
for (ImTypeArgument ta : t.getTypeArguments()) {
60-
ImTypeArgument imTypeArgument = JassIm.ImTypeArgument(ta.getType().match(this), ta.getTypeClassBinding());
61-
args.add(imTypeArgument);
60+
args.add(rewriteTypeArgument(ta));
6261
}
6362
return JassIm.ImClassType(t.getClassDef(), args);
6463
}
64+
65+
/**
66+
* Rewrites one type argument. Only the type is rewritten by default, since a plain rewrite has
67+
* nothing to say about what is known of the argument. Subclasses which do -- substitution, where
68+
* the replacing argument carries its own binding -- override this.
69+
*/
70+
protected ImTypeArgument rewriteTypeArgument(ImTypeArgument ta) {
71+
return JassIm.ImTypeArgument(ta.getType().match(this), ta.getTypeClassBinding());
72+
}
6573
}

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/TypeRewriter.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ private static void rewrite(Element e, Function<ImType, ImType> rewriteFunc) {
3636
case ImTypeIdOfObj t -> t.setClazz((ImClassType) rewriteFunc.apply(t.getClazz()));
3737
case ImTypeIdOfClass t -> t.setClazz((ImClassType) rewriteFunc.apply(t.getClazz()));
3838
case ImMethod m -> m.setMethodClass((ImClassType) rewriteFunc.apply(m.getMethodClass()));
39+
case ImTypeVarDispatch d -> {
40+
// The dispatched type variable is a reference to a variable like any other, but it
41+
// is held directly instead of as a type, so a walk over types alone passes it by.
42+
// A closure capturing the type parameter it dispatches on has to rewrite it too,
43+
// otherwise the lifted body still names a variable of the function it left.
44+
ImType rewritten = rewriteFunc.apply(JassIm.ImTypeVarRef(d.getTypeVariable()));
45+
if (rewritten instanceof ImTypeVarRef ref) {
46+
d.setTypeVariable(ref.getTypeVariable());
47+
}
48+
}
3949
case ImClass c -> {
4050
List<ImClassType> newSuperClasses = new ArrayList<>();
4151
for (ImClassType tt : c.getSuperClasses()) {
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package de.peeeq.wurstscript.translation.imtojass;
2+
3+
import de.peeeq.wurstscript.jassIm.ImFunction;
4+
import de.peeeq.wurstscript.jassIm.ImMethod;
5+
import de.peeeq.wurstscript.jassIm.ImType;
6+
import de.peeeq.wurstscript.jassIm.ImTypeArgument;
7+
import de.peeeq.wurstscript.jassIm.ImTypeClassFunc;
8+
import de.peeeq.wurstscript.jassIm.ImTypeVar;
9+
import de.peeeq.wurstscript.jassIm.ImTypeVarRef;
10+
import de.peeeq.wurstscript.jassIm.JassIm;
11+
import io.vavr.control.Either;
12+
import org.eclipse.jdt.annotation.Nullable;
13+
14+
import java.util.ArrayList;
15+
import java.util.Collections;
16+
import java.util.LinkedHashMap;
17+
import java.util.List;
18+
import java.util.Map;
19+
20+
/**
21+
* Replaces type variables by the type arguments bound to them.
22+
* <p>
23+
* Substitution used to be expressed as two parallel lists -- the type variables of a function or
24+
* class next to the type arguments given at a use of it -- paired up positionally at each point of
25+
* use. That form lost information: a {@link ImTypeArgument} is a type <i>plus</i> what is known
26+
* about it, but pairing the lists unwrapped the argument and returned the bare {@link ImType}, so
27+
* the type class binding never survived. Callers which needed it had to re-attach it by hand, and
28+
* the ones which did not know to do so silently produced an argument with no binding.
29+
* <p>
30+
* Keeping the argument whole fixes that: substituting into an argument position yields the argument
31+
* that was bound, so its binding travels with the type it belongs to.
32+
* <p>
33+
* Lookup is by identity. IM nodes do not override {@code equals}, and a type variable stands for one
34+
* particular declaration, so two variables which merely share a name are different variables.
35+
*/
36+
public final class TypeSubst {
37+
38+
private static final TypeSubst EMPTY =
39+
new TypeSubst(Collections.emptyMap(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
40+
41+
private final Map<ImTypeVar, ImTypeArgument> bindings;
42+
/** Variables this substitution covers but for which no argument was supplied. */
43+
private final List<ImTypeVar> unbound;
44+
/** Kept so that a missing argument is reported the same way it was before. */
45+
private final List<ImTypeVar> typeVars;
46+
private final List<ImTypeArgument> typeArguments;
47+
48+
private TypeSubst(Map<ImTypeVar, ImTypeArgument> bindings, List<ImTypeVar> unbound,
49+
List<ImTypeVar> typeVars, List<ImTypeArgument> typeArguments) {
50+
this.bindings = bindings;
51+
this.unbound = unbound;
52+
this.typeVars = typeVars;
53+
this.typeArguments = typeArguments;
54+
}
55+
56+
public static TypeSubst empty() {
57+
return EMPTY;
58+
}
59+
60+
/**
61+
* Binds {@code typeVars} to {@code typeArguments} by position.
62+
* <p>
63+
* Fewer arguments than variables is allowed: the surplus variables stay unbound, and only a type
64+
* which actually mentions one of them fails. Callers rely on that, because a use may legitimately
65+
* leave the arguments off when it does not name the variable.
66+
*/
67+
public static TypeSubst of(List<ImTypeVar> typeVars, List<ImTypeArgument> typeArguments) {
68+
if (typeVars.isEmpty()) {
69+
return EMPTY;
70+
}
71+
// LinkedHashMap rather than IdentityHashMap: the keys already compare by identity, and this
72+
// keeps iteration order stable, which matters wherever substitution feeds emitted names.
73+
Map<ImTypeVar, ImTypeArgument> bindings = new LinkedHashMap<>();
74+
List<ImTypeVar> unbound = new ArrayList<>();
75+
for (int i = 0; i < typeVars.size(); i++) {
76+
ImTypeVar typeVar = typeVars.get(i);
77+
if (i < typeArguments.size()) {
78+
// A variable repeated in the list keeps its first argument, as positional lookup did.
79+
bindings.putIfAbsent(typeVar, typeArguments.get(i));
80+
} else if (!bindings.containsKey(typeVar)) {
81+
unbound.add(typeVar);
82+
}
83+
}
84+
return new TypeSubst(bindings, unbound, typeVars, typeArguments);
85+
}
86+
87+
public boolean isEmpty() {
88+
return bindings.isEmpty() && unbound.isEmpty();
89+
}
90+
91+
/** Applies this substitution to a type. Any binding on the replacing argument is not part of a type. */
92+
public ImType apply(ImType type) {
93+
if (isEmpty()) {
94+
// Nothing to replace, so hand back the type itself. Worth doing: this runs for the
95+
// return type of every call, and rebuilding a class or tuple type only to get an equal
96+
// one back was pure allocation.
97+
return type;
98+
}
99+
return type.match(new TypeRewriteMatcher() {
100+
@Override
101+
public ImType case_ImTypeVarRef(ImTypeVarRef t) {
102+
ImTypeArgument replacement = resolve(t);
103+
return replacement == null ? t : replacement.getType();
104+
}
105+
106+
@Override
107+
protected ImTypeArgument rewriteTypeArgument(ImTypeArgument argument) {
108+
return TypeSubst.this.apply(argument);
109+
}
110+
});
111+
}
112+
113+
/**
114+
* Applies this substitution to a type argument.
115+
* <p>
116+
* When the argument is exactly a type variable this substitution binds, the bound argument
117+
* replaces it whole, so the binding recorded at the use site reaches the body being substituted
118+
* into. A binding already present on the argument is more specific and is kept.
119+
*/
120+
public ImTypeArgument apply(ImTypeArgument argument) {
121+
if (isEmpty()) {
122+
return argument;
123+
}
124+
if (argument.getType() instanceof ImTypeVarRef ref) {
125+
ImTypeArgument replacement = resolve(ref);
126+
if (replacement != null) {
127+
return JassIm.ImTypeArgument(replacement.getType(), binding(argument, replacement));
128+
}
129+
}
130+
return JassIm.ImTypeArgument(apply(argument.getType()), argument.getTypeClassBinding());
131+
}
132+
133+
/**
134+
* Shares the map rather than copying it. A binding is only ever replaced wholesale through
135+
* {@code setTypeClassBinding}, never added to in place, so two arguments naming the same
136+
* instances can name the same map.
137+
*/
138+
private static Map<ImTypeClassFunc, Either<ImMethod, ImFunction>> binding(ImTypeArgument argument,
139+
ImTypeArgument replacement) {
140+
return argument.getTypeClassBinding().isEmpty()
141+
? replacement.getTypeClassBinding()
142+
: argument.getTypeClassBinding();
143+
}
144+
145+
private @Nullable ImTypeArgument resolve(ImTypeVarRef ref) {
146+
ImTypeVar typeVar = ref.getTypeVariable();
147+
ImTypeArgument bound = bindings.get(typeVar);
148+
if (bound != null) {
149+
return bound;
150+
}
151+
if (unbound.contains(typeVar)) {
152+
throw new RuntimeException("Could not find replacement for " + ref
153+
+ " when replacing " + typeVars + " with " + typeArguments);
154+
}
155+
return null;
156+
}
157+
158+
@Override
159+
public String toString() {
160+
StringBuilder sb = new StringBuilder("[");
161+
bindings.forEach((typeVar, argument) -> {
162+
if (sb.length() > 1) {
163+
sb.append(", ");
164+
}
165+
sb.append(typeVar.getName()).append(" -> ").append(argument.getType());
166+
});
167+
return sb.append("]").toString();
168+
}
169+
}

de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TypeClassTests.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,60 @@ public void dispatchRuntime() {
6767
);
6868
}
6969

70+
/**
71+
* A closure lifts its body into a class of its own, capturing the enclosing type variables. The
72+
* requirement is dispatched from inside that class, so the binding has to survive being carried
73+
* across into it.
74+
*/
75+
@Test
76+
public void dispatchInsideClosure() {
77+
testAssertOkLines(true,
78+
"package test",
79+
"native testSuccess()",
80+
"interface ToIndex<T:>",
81+
" function toIndex(T x) returns int",
82+
"implements ToIndex<int>",
83+
" function toIndex(int x) returns int",
84+
" return x * 2",
85+
"interface Producer",
86+
" function produce() returns int",
87+
"function foo<Q: ToIndex>(Q x) returns int",
88+
" Producer p = () -> Q.toIndex(x)",
89+
" return p.produce()",
90+
"init",
91+
" if foo(21) == 42",
92+
" testSuccess()"
93+
);
94+
}
95+
96+
/**
97+
* The same closure is still rejected for Lua, and this pins that it is rejected clearly rather
98+
* than mistranslated. Lua keeps generics erased and specialises only what it can reach through a
99+
* concrete type; a closure is reached through its interface, so the specialised class exists but
100+
* nothing calls it. Making that work is a change to Lua's erasure, not to substitution. Should
101+
* it be made, this test fails and becomes the success case above.
102+
*/
103+
@Test
104+
public void dispatchInsideClosureIsRejectedForLua() {
105+
test().testLua(true).executeProg().expectError("could not be resolved for the Lua target").lines(
106+
"package test",
107+
"native testSuccess()",
108+
"interface ToIndex<T:>",
109+
" function toIndex(T x) returns int",
110+
"implements ToIndex<int>",
111+
" function toIndex(int x) returns int",
112+
" return x * 2",
113+
"interface Producer",
114+
" function produce() returns int",
115+
"function foo<Q: ToIndex>(Q x) returns int",
116+
" Producer p = () -> Q.toIndex(x)",
117+
" return p.produce()",
118+
"init",
119+
" if foo(21) == 42",
120+
" testSuccess()"
121+
);
122+
}
123+
70124
/** Each type argument picks its own instance, so one generic serves several types. */
71125
@Test
72126
public void twoInstancesOfOneClass() {

0 commit comments

Comments
 (0)