Skip to content

Commit 50b8413

Browse files
committed
Keep the receiver's class binding, and reject two unsupported shapes clearly
A method with its own type parameters was binding the owning class's variables positionally from the method call's type arguments, overwriting the mapping taken from the receiver. Calling describe<string> on a Box<int> therefore dispatched the class's bound through string, which is wrong wherever that instance exists and silently so. The owning class is now bound only when the function has no type parameters of its own, which is the constructor case that needed it, and the receiver's mapping is never overwritten. Two shapes that cannot work yet reported misleading errors rather than saying so: A bound on a module type parameter failed with an unknown name. Using a module copies its body into the class and substitutes type parameters in type positions, but a requirement is called on the parameter as an expression, so it survives the copy and no longer resolves. Supporting it needs the copy to rewrite those receivers, or type parameters on the instantiation, which it has none of. A requirement carrying its own type parameters was compared using the interface's and the implementation's distinct definitions, so an identical signature was rejected with a mismatch between a name and itself. Matching those would mean pairing them first, which this version does not do.
1 parent 0db9fa0 commit 50b8413

4 files changed

Lines changed: 105 additions & 8 deletions

File tree

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,21 @@ private static void bindTypeArguments(ProgramState globalState, LocalState local
7373
if (typeArgs != null) {
7474
List<ImTypeVar> typeVars = f.getTypeVariables();
7575
for (int i = 0; i < Math.min(typeVars.size(), typeArgs.size()); i++) {
76-
binding.put(typeVars.get(i), inheritIfStillAbstract(globalState, typeArgs.get(i)));
76+
binding.putIfAbsent(typeVars.get(i), inheritIfStillAbstract(globalState, typeArgs.get(i)));
7777
}
7878
// A generic class holds its type variables on the class, not on its functions, so a
79-
// constructor or method is called with arguments it has no variable of its own to bind.
80-
// Bind the owning class's variables too, mirroring how type substitutions are built.
81-
ImClass owner = owningClass(f);
82-
if (owner != null) {
83-
ImTypeVars classVars = owner.getTypeVariables();
84-
for (int i = 0; i < Math.min(classVars.size(), typeArgs.size()); i++) {
85-
binding.put(classVars.get(i), inheritIfStillAbstract(globalState, typeArgs.get(i)));
79+
// constructor is called with arguments it has no variable of its own to bind. Only do
80+
// this when the function has no type parameters, because then the arguments are the
81+
// class's; a method with its own parameters is called with those instead, and the
82+
// class's mapping already came from the receiver, which must not be overwritten.
83+
if (typeVars.isEmpty()) {
84+
ImClass owner = owningClass(f);
85+
if (owner != null) {
86+
ImTypeVars classVars = owner.getTypeVariables();
87+
for (int i = 0; i < Math.min(classVars.size(), typeArgs.size()); i++) {
88+
binding.putIfAbsent(classVars.get(i),
89+
inheritIfStillAbstract(globalState, typeArgs.get(i)));
90+
}
8691
}
8792
}
8893
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,24 @@ public static boolean hasBounds(TypeParamDef tp) {
9393
return i.getName() + " extends another interface, which is not supported for bounds:"
9494
+ " the requirements of a bound are the interface's own functions.";
9595
}
96+
FuncDef generic = firstGenericMethod(i);
97+
if (generic != null) {
98+
// Matching such a requirement means pairing the interface's method type parameters with
99+
// the implementation's, which this version does not do. Say so, rather than comparing
100+
// parameters that only look identical and reporting a mismatch between a name and itself.
101+
return i.getName() + "." + generic.getName() + " has its own type parameters, which is not"
102+
+ " supported for a bound: a requirement may only use the interface's type parameter.";
103+
}
104+
return null;
105+
}
106+
107+
/** The first requirement declaring type parameters of its own, or null when none does. */
108+
public static @Nullable FuncDef firstGenericMethod(InterfaceDef iface) {
109+
for (FuncDef method : iface.getMethods()) {
110+
if (!method.getTypeParameters().isEmpty()) {
111+
return method;
112+
}
113+
}
96114
return null;
97115
}
98116

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2643,6 +2643,15 @@ private void checkBoundsSatisfied(Element location, TypeParamDef tp, WurstType t
26432643

26442644
/** Every bound written on a type parameter must be usable as a type class. */
26452645
private void checkTypeParamBounds(TypeParamDef tp) {
2646+
if (TypeClassConstraints.hasBounds(tp) && tp.attrNearestStructureDef() instanceof ModuleDef) {
2647+
// Using a module copies its body into the class, replacing the module's type parameters
2648+
// in type positions. A requirement is called on the parameter itself, which is an
2649+
// expression, so it survives the copy and no longer resolves. Reject that here rather
2650+
// than let it fail later as an unknown name.
2651+
tp.addError("Type class bounds are not supported on a module type parameter."
2652+
+ "\nMove the bounded generic into a class, or use the module without a bound.");
2653+
return;
2654+
}
26462655
for (TypeExpr boundExpr : TypeClassConstraints.boundExprs(tp)) {
26472656
String reason = TypeClassConstraints.invalidBoundReason(boundExpr);
26482657
if (reason != null) {
@@ -2679,6 +2688,13 @@ private void checkInstanceDecl(InstanceDecl decl) {
26792688
+ " for type classes: the requirements of a bound are the interface's own functions.");
26802689
return;
26812690
}
2691+
FuncDef genericRequirement = TypeClassConstraints.firstGenericMethod(iface);
2692+
if (genericRequirement != null) {
2693+
decl.addError(iface.getName() + "." + genericRequirement.getName() + " has its own type parameters,"
2694+
+ " which is not supported for a type class: a requirement may only use the interface's"
2695+
+ " type parameter.");
2696+
return;
2697+
}
26822698

26832699
checkInstanceIsNotOrphan(decl, iface, instanceType);
26842700
checkInstanceIsUnique(decl, iface, instanceType);

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,64 @@ public void boundForwardedThroughTwoLevels() {
742742
);
743743
}
744744

745+
/** A bound on a module type parameter is rejected: using a module copies its body out of scope. */
746+
@Test
747+
public void boundOnGenericModule() {
748+
testAssertErrorsLines(false, "not supported on a module type parameter",
749+
"package test",
750+
"native testSuccess()",
751+
"interface Show<T:>",
752+
" function show(T x) returns string",
753+
"implements Show<int>",
754+
" function show(int x) returns string",
755+
" return \"i\"",
756+
"module M<T: Show>",
757+
" function render(T x) returns string",
758+
" return T.show(x)",
759+
"class C",
760+
" use M<int>",
761+
"init",
762+
" if new C().render(1) == \"i\"",
763+
" testSuccess()"
764+
);
765+
}
766+
767+
/** A method with its own type parameters must not disturb the class binding taken from the receiver. */
768+
@Test
769+
public void classBoundWithIndependentMethodTypeParam() {
770+
testAssertOkLines(true,
771+
"package test",
772+
"native testSuccess()",
773+
"interface Show<T:>",
774+
" function show(T x) returns string",
775+
"implements Show<int>",
776+
" function show(int x) returns string",
777+
" return \"int\"",
778+
"implements Show<string>",
779+
" function show(string x) returns string",
780+
" return \"string\"",
781+
"class Box<T: Show>",
782+
" function describe<U:>(T x, U other) returns string",
783+
" return T.show(x)",
784+
"init",
785+
" if new Box<int>().describe<string>(1, \"a\") == \"int\"",
786+
" testSuccess()"
787+
);
788+
}
789+
790+
/** A requirement may only use the interface's type parameter, and says so plainly. */
791+
@Test
792+
public void genericRequirement() {
793+
testAssertErrorsLines(false, "has its own type parameters",
794+
"package test",
795+
"interface Pairing<T:>",
796+
" function pair<U:>(T x, U y) returns U",
797+
"implements Pairing<int>",
798+
" function pair<U:>(int x, U y) returns U",
799+
" return y"
800+
);
801+
}
802+
745803
/** A type parameter is not a value, so it may only appear as the receiver of a requirement. */
746804
@Test
747805
public void typeParameterIsNotAValue() {

0 commit comments

Comments
 (0)