Skip to content

Commit 2d8a387

Browse files
committed
Let a module's type parameter carry a type class bound
Using a module copies its body into the class and replaces the module's type parameters wherever they are used as types. A requirement of a bound is called on the parameter itself, T.show(x), where T is a name and not a type, so the replacement never reaches it. Renaming it to the using class's parameter is not open either: a module body resolves names in the module's own scope by design, so that name is one the scope deliberately cannot see. The bound was rejected outright rather than mistranslated. The instantiation now declares the module's parameters and records the arguments chosen for them. The body keeps saying T, that name resolves to the instantiation's own declaration, and the argument is right there to say what it stands for. The arguments are recorded resolved rather than copied, because an argument names something only the user's scope can see. Declaring them is all it does. A module instantiation is not an AstElementWithTypeParameters: these are names to look up, not variables for a call to infer, and making them inferable made every method of a generic module's instantiation ask a caller to infer a parameter its signature never mentions. Generic modules keep resolving their parameters by matching the receiver type. A receiver written on such a parameter denotes the argument bound to it. The requirements it offers are the ones the parameter declared, so a module cannot reach a bound it did not ask for, while their parameter and return types are the argument's, which is what the copied body speaks in. Dispatch follows the argument: on the using class's type variable when the argument is itself a parameter, and straight to the instance otherwise, since a module used with a concrete argument leaves no variable for generic elimination to substitute. The bound is checked at the use, which is the only place that sees both the parameter and the argument chosen for it.
1 parent 7851155 commit 2d8a387

11 files changed

Lines changed: 340 additions & 111 deletions

File tree

de.peeeq.wurstscript/parserspec/wurstscript.parseq

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,13 @@ ClassSlot =
8282
ConstructorDef(@ignoreForEquality de.peeeq.wurstscript.parser.WPos source, Modifiers modifiers, WParameters parameters, SuperConstructorCall superConstructorCall, WStatements body)
8383
| OnDestroyDef(@ignoreForEquality de.peeeq.wurstscript.parser.WPos source, WStatements body)
8484
| ModuleUse(@ignoreForEquality de.peeeq.wurstscript.parser.WPos source, Identifier moduleNameId, TypeExprList typeArgs)
85+
// Carries the module's type parameters, and the arguments chosen for them, so a name inside the
86+
// copied body still resolves: a requirement of a bound is called on the parameter itself, which
87+
// is a name rather than a type, and a module body resolves names in the module's own scope
88+
// rather than the user's. They are declarations, not parameters left to infer, which is why the
89+
// instantiation is not an AstElementWithTypeParameters.
8590
| ModuleInstanciation(@ignoreForEquality de.peeeq.wurstscript.parser.WPos source, Modifiers modifiers, Identifier nameId,
91+
TypeParamDefs typeParameters, TypeExprList typeArgs,
8692
ClassDefs innerClasses, FuncDefs methods, GlobalVarDefs vars, ConstructorDefs constructors,
8793
ModuleInstanciations p_moduleInstanciations, ModuleUses moduleUses, OnDestroyDef onDestroy)
8894
| ClassMember

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/ModuleExpander.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,25 @@ private static ModuleInstanciations expandModules(ClassOrModule m, List<ClassOrM
7575

7676
WPos source = moduleUse.getSource().artificial();
7777
WPos idSource = moduleUse.getModuleNameId().getSource().artificial();
78+
// The instantiation declares the module's type parameters, so a name inside the copied
79+
// body still resolves to something. Types have already been replaced by the arguments;
80+
// what is left needing a name is a requirement called on the parameter itself.
81+
TypeParamDefs instanciationTypeParams = Ast.TypeParamDefs();
82+
for (TypeParamDef moduleParam : usedModule.getTypeParameters()) {
83+
instanciationTypeParams.add(moduleParam.copy());
84+
}
85+
// Resolved rather than copied: an argument names something in the user's scope, and a
86+
// module instantiation resolves names in the module's own. Resolving here keeps the type
87+
// and needs no scope afterwards, which is what TypeExprResolved is for.
88+
TypeExprList instanciationTypeArgs = Ast.TypeExprList();
89+
for (int i = 0; i < numTypeArgs; i++) {
90+
TypeExpr arg = moduleUse.getTypeArgs().get(i);
91+
instanciationTypeArgs.add(Ast.TypeExprResolved(arg.getSource().artificial(), arg.attrTyp()));
92+
}
7893
ModuleInstanciation mi = Ast.ModuleInstanciation(source, Ast.Modifiers(),
7994
Ast.Identifier(idSource, usedModule.getName()),
95+
instanciationTypeParams,
96+
instanciationTypeArgs,
8097
smartCopy(usedModule.getInnerClasses(), typeReplacements),
8198
smartCopy(usedModule.getMethods(), typeReplacements),
8299
smartCopy(usedModule.getVars(), typeReplacements),

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrImplicitParameter.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import de.peeeq.wurstscript.types.WurstType;
88
import de.peeeq.wurstscript.types.WurstTypeTypeParam;
99
import org.eclipse.jdt.annotation.Nullable;
10+
import de.peeeq.wurstscript.types.WurstTypeBoundTypeParam;
1011

1112
public class AttrImplicitParameter {
1213

@@ -97,9 +98,17 @@ public static boolean isTypeClassDispatch(Element e) {
9798
return false;
9899
}
99100
Expr left = hasReceiver.getLeft();
100-
return left != null
101-
&& left.attrTyp() instanceof WurstTypeTypeParam tp
102-
&& tp.isStaticRef();
101+
if (left == null) {
102+
return false;
103+
}
104+
WurstType leftType = left.attrTyp();
105+
if (leftType instanceof WurstTypeTypeParam tp) {
106+
return tp.isStaticRef();
107+
}
108+
// A parameter of a module instantiation denotes the argument bound to it, so the receiver is
109+
// a binding rather than the parameter itself. It still names a type parameter, which is what
110+
// makes this a dispatch.
111+
return leftType instanceof WurstTypeBoundTypeParam bound && bound.isStaticRef();
103112
}
104113

105114
static OptExpr getFunctionCallImplicitParameter(FunctionCall e, FuncLink calledFunc, boolean showError) {

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrNameDef.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import de.peeeq.wurstscript.types.WurstTypeEnum;
1616
import de.peeeq.wurstscript.types.WurstTypeModule;
1717
import org.eclipse.jdt.annotation.Nullable;
18+
import de.peeeq.wurstscript.types.WurstTypeBoundTypeParam;
1819

1920
/**
2021
* this attribute find the variable definition for every variable reference
@@ -136,7 +137,7 @@ private static boolean isMethodCallReceiver(NameRef node) {
136137
if (!(typeDef instanceof TypeParamDef tp) || !TypeClassConstraints.hasBounds(tp)) {
137138
return null;
138139
}
139-
WurstTypeTypeParam typ = new WurstTypeTypeParam(tp).asStaticRef();
140+
WurstType typ = staticRefTypeFor(tp, node);
140141
return new OtherLink(Visibility.LOCAL, varName, typ) {
141142
@Override
142143
public de.peeeq.wurstscript.jassIm.ImExpr translate(NameRef e, ImTranslator t, ImFunction f) {
@@ -146,6 +147,34 @@ public de.peeeq.wurstscript.jassIm.ImExpr translate(NameRef e, ImTranslator t, I
146147
};
147148
}
148149

150+
/**
151+
* The type a bounded parameter's name denotes in receiver position.
152+
* <p>
153+
* A parameter declared on a module instantiation stands for the argument the user supplied, so it
154+
* denotes that type bound to this parameter: the requirement's own parameter types then substitute
155+
* to the argument rather than to a name only the module can see. Everywhere else the parameter
156+
* stands for itself.
157+
*/
158+
private static WurstType staticRefTypeFor(TypeParamDef tp, NameRef node) {
159+
WurstType argument = moduleInstanciationArgument(tp);
160+
if (argument != null) {
161+
return new WurstTypeBoundTypeParam(tp, argument, node).asStaticRef();
162+
}
163+
return new WurstTypeTypeParam(tp).asStaticRef();
164+
}
165+
166+
/** The argument a module instantiation supplied for this parameter, or null if it is not one. */
167+
private static @Nullable WurstType moduleInstanciationArgument(TypeParamDef tp) {
168+
if (!(tp.getParent() != null && tp.getParent().getParent() instanceof ModuleInstanciation mi)) {
169+
return null;
170+
}
171+
int index = mi.getTypeParameters().indexOf(tp);
172+
if (index < 0 || index >= mi.getTypeArgs().size()) {
173+
return null;
174+
}
175+
return mi.getTypeArgs().get(index).attrTyp();
176+
}
177+
149178
private static @Nullable NameLink lookupImplicitClosureSelf(NameRef node, boolean showErrors) {
150179
ExprClosure closure = node.attrNearestExprClosure();
151180
if (closure == null) {

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/TypeNameLinks.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import com.google.common.collect.ImmutableSetMultimap;
55
import de.peeeq.wurstscript.ast.*;
66

7+
import java.util.Collections;
8+
import java.util.List;
9+
710
public class TypeNameLinks {
811

912
public static ImmutableMultimap<String, TypeLink> calculate(ClassOrModuleOrModuleInstanciation c) {
@@ -94,13 +97,27 @@ public static ImmutableMultimap<String, TypeLink> calculate(WStatements statemen
9497
}
9598

9699
private static void addTypeParametersIfAny(ImmutableMultimap.Builder<String, TypeLink> result, WScope c) {
97-
if (c instanceof AstElementWithTypeParameters) {
98-
AstElementWithTypeParameters wtp = (AstElementWithTypeParameters) c;
99-
for (TypeParamDef i : wtp.getTypeParameters()) {
100-
result.put(i.getName(), TypeLink.create(i, c));
101-
}
100+
for (TypeParamDef i : declaredTypeParameters(c)) {
101+
result.put(i.getName(), TypeLink.create(i, c));
102102
}
103+
}
103104

105+
/**
106+
* The type parameter names a scope introduces.
107+
* <p>
108+
* A module instantiation declares the module's parameters so that a receiver written on one
109+
* still resolves once the body has been copied out of the module's scope. It is not an
110+
* {@link AstElementWithTypeParameters}: the arguments are recorded on the instantiation, so
111+
* these are names to look up rather than variables for a call to infer.
112+
*/
113+
private static List<TypeParamDef> declaredTypeParameters(WScope c) {
114+
if (c instanceof AstElementWithTypeParameters wtp) {
115+
return wtp.getTypeParameters();
116+
}
117+
if (c instanceof ModuleInstanciation mi) {
118+
return mi.getTypeParameters();
119+
}
120+
return Collections.emptyList();
104121
}
105122

106123
private static void addJassTypes(ImmutableMultimap.Builder<String, TypeLink> result, CompilationUnit cu) {

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ public static ImExpr translateIntern(FunctionCall e, ImTranslator t, ImFunction
521521
* function supplied by the instance chosen for the substituted type.
522522
*/
523523
private static ImExpr translateTypeClassDispatch(FunctionCall e, ImTranslator t, ImFunction f) {
524-
WurstTypeTypeParam receiver = (WurstTypeTypeParam) ((HasReceiver) e).getLeft().attrTyp();
524+
WurstType receiver = ((HasReceiver) e).getLeft().attrTyp();
525525
FunctionDefinition called = e.attrFuncDef();
526526
if (!(called instanceof FuncDef method)) {
527527
throw new CompileError(e.attrSource(),
@@ -531,7 +531,36 @@ private static ImExpr translateTypeClassDispatch(FunctionCall e, ImTranslator t,
531531
for (Expr arg : e.getArgs()) {
532532
args.add(arg.imTranslateExpr(t, f));
533533
}
534-
return JassIm.ImTypeVarDispatch(e, t.getTypeClassFunc(method), args, t.getTypeVar(receiver.getDef()));
534+
ImTypeClassFunc requirement = t.getTypeClassFunc(method);
535+
if (receiver instanceof WurstTypeBoundTypeParam bound) {
536+
return translateModuleParamDispatch(e, bound, requirement, args, t);
537+
}
538+
return JassIm.ImTypeVarDispatch(e, requirement, args,
539+
t.getTypeVar(((WurstTypeTypeParam) receiver).getDef()));
540+
}
541+
542+
/**
543+
* Dispatches a requirement called on a module instantiation's type parameter, which stands for
544+
* the argument the using class supplied rather than for a variable of its own.
545+
* <p>
546+
* When that argument is itself a type parameter the dispatch is on the using class's variable,
547+
* exactly as if the call had been written there. Otherwise the instance is already determined:
548+
* a module used with a concrete argument leaves no variable for generic elimination to
549+
* substitute, so the implementation is chosen here.
550+
*/
551+
private static ImExpr translateModuleParamDispatch(FunctionCall e, WurstTypeBoundTypeParam bound,
552+
ImTypeClassFunc requirement, ImExprs args, ImTranslator t) {
553+
WurstType argument = bound.getBaseType().normalize();
554+
if (argument instanceof WurstTypeTypeParam tp) {
555+
return JassIm.ImTypeVarDispatch(e, requirement, args, t.getTypeVar(tp.getDef()));
556+
}
557+
Either<ImMethod, ImFunction> impl = bound.imTypeClassBinding(t).get(requirement);
558+
if (impl == null) {
559+
throw new CompileError(e.attrSource(),
560+
"No type class instance supplies " + e.getFuncName() + " for " + argument + ".");
561+
}
562+
ImFunction target = impl.isRight() ? impl.get() : impl.getLeft().getImplementation();
563+
return ImFunctionCall(e, target, ImTypeArguments(), args, false, CallType.NORMAL);
535564
}
536565

537566
private static ImExpr translateFunctionCall(FunctionCall e, ImTranslator t, ImFunction f, boolean returnReveiver, boolean nullSafe) {

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/TypeClassConstraints.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package de.peeeq.wurstscript.types;
22

33
import de.peeeq.wurstscript.ast.*;
4+
import de.peeeq.wurstscript.ast.Element;
5+
import de.peeeq.wurstscript.attributes.names.FuncLink;
46
import org.eclipse.jdt.annotation.Nullable;
57

68
import java.util.ArrayList;
79
import java.util.Collections;
810
import java.util.List;
11+
import java.util.stream.Stream;
912

1013
/**
1114
* Reads the type class bounds written on a new-style type parameter.
@@ -121,6 +124,81 @@ public static boolean hasBounds(TypeParamDef tp) {
121124
return boundExpr.lookupType(simple.getTypeName(), false) instanceof InterfaceDef i ? i : null;
122125
}
123126

127+
/**
128+
* Surfaces the methods required by a type parameter's bounds as members of a receiver which
129+
* stands for that parameter, so that {@code T.f(args)} resolves.
130+
* <p>
131+
* Bounds are ordered and an earlier one wins, but only over the same signature: two bounds may
132+
* require the very same operation, and offering both would make every call ambiguous.
133+
* Differently shaped overloads are not in competition, so later bounds still contribute them and
134+
* overload resolution picks between them as usual.
135+
*
136+
* @param standsFor what the interface's own type parameter is bound to, which is what the
137+
* requirement's parameter and return types substitute to.
138+
* @param receiver the type the call is written on.
139+
*/
140+
public static void addRequirementMethods(TypeParamDef def, WurstType standsFor, WurstType receiver,
141+
Element node, String name, List<FuncLink> result) {
142+
List<FuncLink> supplied = new ArrayList<>();
143+
for (InterfaceDef bound : boundInterfaces(def)) {
144+
for (FuncDef method : bound.getMethods()) {
145+
if (!method.getName().equals(name)) {
146+
continue;
147+
}
148+
FuncLink candidate = requirementLink(bound, method, standsFor, receiver, node);
149+
if (!alreadySupplied(supplied, candidate, node)) {
150+
supplied.add(candidate);
151+
}
152+
}
153+
}
154+
result.addAll(supplied);
155+
}
156+
157+
/** Every requirement of the bounds, for callers which want the whole set rather than one name. */
158+
public static Stream<FuncLink> requirementMethods(TypeParamDef def, WurstType standsFor,
159+
WurstType receiver, Element node) {
160+
return boundInterfaces(def).stream()
161+
.flatMap(bound -> bound.getMethods().stream()
162+
.map(method -> requirementLink(bound, method, standsFor, receiver, node)));
163+
}
164+
165+
/**
166+
* Exposes one interface method as a requirement: the interface's own type parameter is
167+
* substituted by what the receiver stands for, so the call reads {@code T.f(args)} with the
168+
* arguments exactly as declared.
169+
*/
170+
private static FuncLink requirementLink(InterfaceDef bound, FuncDef method, WurstType standsFor,
171+
WurstType receiver, Element node) {
172+
TypeParamDef ifaceParam = bound.getTypeParameters().get(0);
173+
VariableBinding binding = VariableBinding.emptyMapping()
174+
.set(ifaceParam, new WurstTypeBoundTypeParam(ifaceParam, standsFor, node));
175+
return FuncLink.create(method, bound)
176+
.withTypeArgBinding(node, binding)
177+
.withReceiverType(receiver);
178+
}
179+
180+
/** True when an earlier bound already supplied a requirement of the same shape. */
181+
private static boolean alreadySupplied(List<FuncLink> supplied, FuncLink candidate, Element node) {
182+
for (FuncLink existing : supplied) {
183+
List<WurstType> a = existing.getParameterTypes();
184+
List<WurstType> b = candidate.getParameterTypes();
185+
if (a.size() != b.size()) {
186+
continue;
187+
}
188+
boolean same = true;
189+
for (int i = 0; i < a.size(); i++) {
190+
if (!a.get(i).equalsType(b.get(i), node)) {
191+
same = false;
192+
break;
193+
}
194+
}
195+
if (same) {
196+
return true;
197+
}
198+
}
199+
return false;
200+
}
201+
124202
/**
125203
* Looks up a method required by any bound of the given type parameter.
126204
* Earlier bounds win, matching the left-to-right order the bounds were written in.

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/WurstTypeBoundTypeParam.java

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,36 @@ public class WurstTypeBoundTypeParam extends WurstType {
2727
private final @Nullable Map<FuncDef, FuncLink> typeConstraintFunctions;
2828
private boolean indexInitialized = false;
2929
private final Element context;
30+
/**
31+
* True when this stands for the type parameter itself rather than a value of it, as in the
32+
* receiver of {@code T.toIndex(x)}. Only this form exposes the methods required by the bounds.
33+
*/
34+
private final boolean staticRef;
3035

3136
public WurstTypeBoundTypeParam(TypeParamDef def, WurstType baseType, Element context) {
37+
this(def, baseType, context, false);
38+
}
39+
40+
private WurstTypeBoundTypeParam(TypeParamDef def, WurstType baseType, Element context, boolean staticRef) {
3241
if (baseType instanceof WurstTypeIntLiteral) {
3342
baseType = WurstTypeInt.instance();
3443
}
3544
this.typeParamDef = def;
3645
this.baseType = baseType;
3746
this.context = context;
47+
this.staticRef = staticRef;
3848
if (def.getTypeParamConstraints() instanceof NoTypeParamConstraints) {
3949
this.typeConstraintFunctions = null;
4050
} else {
4151
this.typeConstraintFunctions = new HashMap<>();
4252
}
4353
}
4454

55+
/** The same binding, seen as the type parameter itself rather than as a value of it. */
56+
public WurstTypeBoundTypeParam asStaticRef() {
57+
return staticRef ? this : new WurstTypeBoundTypeParam(typeParamDef, baseType, context, true);
58+
}
59+
4560
@Override
4661
VariableBinding matchAgainstSupertypeIntern(WurstType other, @Nullable Element location, VariableBinding mapping, VariablePosition variablePosition) {
4762
return baseType.matchAgainstSupertypeIntern(other, location, mapping, NONE);
@@ -92,17 +107,27 @@ public boolean allowsDynamicDispatch() {
92107
@Override
93108
public void addMemberMethods(Element node, String name,
94109
List<FuncLink> result) {
110+
if (staticRef) {
111+
// The requirements are the parameter's own, so a module cannot reach a bound its
112+
// parameter did not declare; their types come from the argument it is bound to, which
113+
// is what the copied body was rewritten to speak in.
114+
TypeClassConstraints.addRequirementMethods(typeParamDef, baseType, this, node, name, result);
115+
return;
116+
}
95117
baseType.addMemberMethods(node, name, result);
96118
}
97119

98120
@Override
99121
public Stream<FuncLink> getMemberMethods(Element node) {
122+
if (staticRef) {
123+
return TypeClassConstraints.requirementMethods(typeParamDef, baseType, this, node);
124+
}
100125
return baseType.getMemberMethods(node);
101126
}
102127

103128
@Override
104129
public boolean isStaticRef() {
105-
return baseType.isStaticRef();
130+
return staticRef || baseType.isStaticRef();
106131
}
107132

108133
@Override
@@ -113,7 +138,10 @@ public boolean isCastableToInt() {
113138

114139
@Override
115140
public WurstType normalize() {
116-
return baseType.normalize();
141+
// A static reference denotes the parameter, not a value of what it is bound to. Normalising
142+
// it away would leave the argument type, which knows nothing of the bounds the requirements
143+
// are read from.
144+
return staticRef ? this : baseType.normalize();
117145
}
118146

119147
public FuncDef getFromIndex() {
@@ -178,7 +206,7 @@ private WurstTypeBoundTypeParam withBaseType(WurstType t) {
178206
if (t == baseType) {
179207
return this;
180208
}
181-
return new WurstTypeBoundTypeParam(typeParamDef, t, context);
209+
return new WurstTypeBoundTypeParam(typeParamDef, t, context, staticRef);
182210
}
183211

184212
public TypeParamDef getTypeParamDef() {

0 commit comments

Comments
 (0)