Skip to content

Commit de966e2

Browse files
committed
Address review: check instance signatures, abstract bounds and invalid bounds
Instance methods were matched by name only, so an implementation with the wrong parameter types, parameter count or return type was accepted and emitted, leaving the error to pjass. Compare against the requirement with the interface's type parameter replaced by the instance type, and report the expected signature. A type argument which is itself a type parameter was accepted unconditionally. That let a generic without the bound pass its parameter to one that needs it: the call type checked but had no instance to dispatch to at runtime. Such an argument now has to declare the bound itself. Bounds which do not name a usable type class were silently dropped, so a bound on a class or an applied interface became a no-op. They are reported at the bound. Interfaces which extend another interface are rejected as bounds, at the bound and at the instance: the requirements of a bound are the interface's own functions, so allowing inheritance would silently omit the inherited ones.
1 parent aa9558b commit de966e2

4 files changed

Lines changed: 309 additions & 17 deletions

File tree

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

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ private TypeClassConstraints() {
2121

2222
/**
2323
* The interfaces named as bounds of the given type parameter, in source order.
24-
* Bounds which do not resolve to a single-parameter interface are skipped here; the validator
25-
* reports those separately so that a bad bound does not cascade into every use site.
24+
* Unusable bounds are skipped; {@link #invalidBoundReason} explains why, and the validator
25+
* reports it at the bound itself so a bad bound does not cascade into every use site.
2626
*/
2727
public static List<InterfaceDef> boundInterfaces(TypeParamDef tp) {
2828
List<TypeExpr> exprs = boundExprs(tp);
@@ -58,22 +58,49 @@ public static boolean hasBounds(TypeParamDef tp) {
5858
* plain reference to a single-parameter interface.
5959
*/
6060
public static @Nullable InterfaceDef resolveBound(TypeExpr boundExpr) {
61+
return invalidBoundReason(boundExpr) == null ? namedInterface(boundExpr) : null;
62+
}
63+
64+
/**
65+
* Explains why a bound cannot be used as a type class, or null when it is usable.
66+
* <p>
67+
* A bound must name an interface, unapplied, with exactly one type parameter, and that
68+
* interface must not extend another. The last restriction keeps the set of requirements equal
69+
* to the interface's own methods, so an instance cannot silently miss an inherited one.
70+
*/
71+
public static @Nullable String invalidBoundReason(TypeExpr boundExpr) {
6172
if (!(boundExpr instanceof TypeExprSimple simple)) {
62-
return null;
73+
return "A bound must name an interface.";
6374
}
6475
// A bound names the interface unapplied: writing the type argument would be redundant,
6576
// because it is always the type parameter being constrained.
6677
if (!simple.getTypeArgs().isEmpty()) {
67-
return null;
78+
return "A bound must name the interface without type arguments, because the argument is"
79+
+ " always the type parameter being constrained.";
6880
}
6981
TypeDef def = boundExpr.lookupType(simple.getTypeName(), false);
82+
if (def == null) {
83+
return "Could not find " + simple.getTypeName() + ".";
84+
}
7085
if (!(def instanceof InterfaceDef i)) {
71-
return null;
86+
return simple.getTypeName() + " is not an interface, so it cannot be used as a bound.";
7287
}
7388
if (i.getTypeParameters().size() != 1) {
89+
return i.getName() + " must have exactly one type parameter to be used as a bound, but has "
90+
+ i.getTypeParameters().size() + ".";
91+
}
92+
if (!i.getExtendsList().isEmpty()) {
93+
return i.getName() + " extends another interface, which is not supported for bounds:"
94+
+ " the requirements of a bound are the interface's own functions.";
95+
}
96+
return null;
97+
}
98+
99+
private static @Nullable InterfaceDef namedInterface(TypeExpr boundExpr) {
100+
if (!(boundExpr instanceof TypeExprSimple simple)) {
74101
return null;
75102
}
76-
return i;
103+
return boundExpr.lookupType(simple.getTypeName(), false) instanceof InterfaceDef i ? i : null;
77104
}
78105

79106
/**

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

Lines changed: 116 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,8 @@ private void check(Element e) {
405405
checkConstructorsUnique((ClassOrModule) e);
406406
if (e instanceof InstanceDecl)
407407
checkInstanceDecl((InstanceDecl) e);
408+
if (e instanceof TypeParamDef)
409+
checkTypeParamBounds((TypeParamDef) e);
408410
if (e instanceof StmtCall)
409411
checkCallBounds((StmtCall) e);
410412
if (e instanceof CompilationUnit)
@@ -2600,16 +2602,51 @@ private void checkCallBounds(StmtCall call) {
26002602
* where the type argument is chosen so the message can name both.
26012603
*/
26022604
private void checkBoundsSatisfied(Element location, TypeParamDef tp, WurstType typ) {
2603-
if (typ instanceof WurstTypeUnknown || typ instanceof WurstTypeTypeParam
2604-
|| typ instanceof WurstTypeBoundTypeParam) {
2605-
// still abstract, or already broken: checked once it is bound to something concrete
2605+
List<InterfaceDef> bounds = TypeClassConstraints.boundInterfaces(tp);
2606+
if (bounds.isEmpty() || typ instanceof WurstTypeUnknown) {
26062607
return;
26072608
}
2608-
for (InterfaceDef bound : TypeClassConstraints.boundInterfaces(tp)) {
2609-
if (TypeClassInstances.find(bound, typ) == null) {
2610-
location.addError("Type " + typ + " does not satisfy the bound " + tp.getName() + ": "
2611-
+ bound.getName() + ".\nDeclare 'implements " + bound.getName() + "<" + typ + ">' in the package of "
2612-
+ bound.getName() + " or of " + typ + ".");
2609+
WurstType normalized = typ.normalize();
2610+
TypeParamDef abstractArg = asTypeParam(normalized);
2611+
if (abstractArg != null) {
2612+
// The argument is another type parameter, so no instance exists yet. It can only supply
2613+
// the bound if it declares it itself; otherwise the call is unsatisfiable no matter what
2614+
// the outer generic is later instantiated with.
2615+
for (InterfaceDef bound : bounds) {
2616+
if (!TypeClassConstraints.boundInterfaces(abstractArg).contains(bound)) {
2617+
location.addError("Type parameter " + abstractArg.getName() + " does not satisfy the bound "
2618+
+ tp.getName() + ": " + bound.getName() + ".\nAdd the bound to " + abstractArg.getName()
2619+
+ ", as in <" + abstractArg.getName() + ": " + bound.getName() + ">.");
2620+
}
2621+
}
2622+
return;
2623+
}
2624+
for (InterfaceDef bound : bounds) {
2625+
if (TypeClassInstances.find(bound, normalized) == null) {
2626+
location.addError("Type " + normalized + " does not satisfy the bound " + tp.getName() + ": "
2627+
+ bound.getName() + ".\nDeclare 'implements " + bound.getName() + "<" + normalized
2628+
+ ">' in the package of " + bound.getName() + " or of " + normalized + ".");
2629+
}
2630+
}
2631+
}
2632+
2633+
/** The type parameter this type stands for, when it is still abstract. */
2634+
private static @Nullable TypeParamDef asTypeParam(WurstType typ) {
2635+
if (typ instanceof WurstTypeTypeParam tp) {
2636+
return tp.getDef();
2637+
}
2638+
if (typ instanceof WurstTypeBoundTypeParam bound) {
2639+
return asTypeParam(bound.getBaseType().normalize());
2640+
}
2641+
return null;
2642+
}
2643+
2644+
/** Every bound written on a type parameter must be usable as a type class. */
2645+
private void checkTypeParamBounds(TypeParamDef tp) {
2646+
for (TypeExpr boundExpr : TypeClassConstraints.boundExprs(tp)) {
2647+
String reason = TypeClassConstraints.invalidBoundReason(boundExpr);
2648+
if (reason != null) {
2649+
boundExpr.addError("Invalid bound on type parameter " + tp.getName() + ": " + reason);
26132650
}
26142651
}
26152652
}
@@ -2637,9 +2674,15 @@ private void checkInstanceDecl(InstanceDecl decl) {
26372674
return;
26382675
}
26392676

2677+
if (!iface.getExtendsList().isEmpty()) {
2678+
decl.addError("Interface " + iface.getName() + " extends another interface, which is not supported"
2679+
+ " for type classes: the requirements of a bound are the interface's own functions.");
2680+
return;
2681+
}
2682+
26402683
checkInstanceIsNotOrphan(decl, iface, instanceType);
26412684
checkInstanceIsUnique(decl, iface, instanceType);
2642-
checkInstanceIsComplete(decl, iface);
2685+
checkInstanceIsComplete(decl, iface, instanceType);
26432686
}
26442687

26452688
private void checkInstanceIsNotOrphan(InstanceDecl decl, InterfaceDef iface, WurstType instanceType) {
@@ -2673,17 +2716,24 @@ private void checkInstanceIsUnique(InstanceDecl decl, InterfaceDef iface, WurstT
26732716
}
26742717
}
26752718

2676-
private void checkInstanceIsComplete(InstanceDecl decl, InterfaceDef iface) {
2719+
private void checkInstanceIsComplete(InstanceDecl decl, InterfaceDef iface, WurstType instanceType) {
2720+
// The requirement is written in terms of the interface's type parameter, so compare against
2721+
// it with that parameter replaced by the type this instance is for.
2722+
TypeParamDef ifaceParam = iface.getTypeParameters().get(0);
2723+
VariableBinding binding = VariableBinding.emptyMapping()
2724+
.set(ifaceParam, new WurstTypeBoundTypeParam(ifaceParam, instanceType, decl));
2725+
26772726
StringBuilder missing = new StringBuilder();
26782727
for (FuncDef requirement : iface.getMethods()) {
26792728
int found = 0;
26802729
for (FuncDef provided : decl.getMethods()) {
26812730
if (provided.getName().equals(requirement.getName())) {
26822731
found++;
2732+
checkInstanceMethodSignature(provided, requirement, binding, iface);
26832733
}
26842734
}
26852735
if (found == 0) {
2686-
missing.append("\n ").append(requirement.getName());
2736+
missing.append("\n ").append(requirementTemplate(requirement, binding, decl));
26872737
} else if (found > 1) {
26882738
decl.addError("This instance defines " + requirement.getName() + " more than once.");
26892739
}
@@ -2706,6 +2756,61 @@ private void checkInstanceIsComplete(InstanceDecl decl, InterfaceDef iface) {
27062756
}
27072757
}
27082758

2759+
/**
2760+
* An instance method must match its requirement exactly once the interface's type parameter is
2761+
* replaced by the instance type. Matching on the name alone would let a wrongly typed
2762+
* implementation be selected and emitted, which the backend cannot catch.
2763+
*/
2764+
private void checkInstanceMethodSignature(FuncDef provided, FuncDef requirement,
2765+
VariableBinding binding, InterfaceDef iface) {
2766+
List<WurstType> expectedParams = new ArrayList<>();
2767+
for (WParameter p : requirement.getParameters()) {
2768+
expectedParams.add(p.attrTyp().setTypeArgs(binding));
2769+
}
2770+
if (provided.getParameters().size() != expectedParams.size()) {
2771+
provided.addError(provided.getName() + " must take " + expectedParams.size()
2772+
+ " parameter(s) to implement " + iface.getName() + "."
2773+
+ "\nExpected: " + signatureText(requirement, binding));
2774+
return;
2775+
}
2776+
for (int i = 0; i < expectedParams.size(); i++) {
2777+
WParameter actual = provided.getParameters().get(i);
2778+
WurstType expected = expectedParams.get(i);
2779+
if (!actual.attrTyp().equalsType(expected, actual)) {
2780+
actual.addError("Parameter " + actual.getName() + " should have type " + expected
2781+
+ " to implement " + iface.getName() + "." + provided.getName() + ", but has "
2782+
+ actual.attrTyp() + ".");
2783+
}
2784+
}
2785+
WurstType expectedReturn = requirement.attrReturnTyp().setTypeArgs(binding);
2786+
if (!provided.attrReturnTyp().equalsType(expectedReturn, provided)) {
2787+
provided.addError(provided.getName() + " should return " + expectedReturn + " to implement "
2788+
+ iface.getName() + ", but returns " + provided.attrReturnTyp() + ".");
2789+
}
2790+
}
2791+
2792+
private String requirementTemplate(FuncDef requirement, VariableBinding binding, Element context) {
2793+
return signatureText(requirement, binding);
2794+
}
2795+
2796+
private String signatureText(FuncDef requirement, VariableBinding binding) {
2797+
StringBuilder sb = new StringBuilder("function ").append(requirement.getName()).append("(");
2798+
boolean first = true;
2799+
for (WParameter p : requirement.getParameters()) {
2800+
if (!first) {
2801+
sb.append(", ");
2802+
}
2803+
first = false;
2804+
sb.append(p.attrTyp().setTypeArgs(binding)).append(" ").append(p.getName());
2805+
}
2806+
sb.append(")");
2807+
WurstType returnType = requirement.attrReturnTyp().setTypeArgs(binding);
2808+
if (!(returnType instanceof WurstTypeVoid)) {
2809+
sb.append(" returns ").append(returnType);
2810+
}
2811+
return sb.toString();
2812+
}
2813+
27092814
private void checkFuncRef(FuncRef ref) {
27102815
if (isConstructorThisCall(ref)) {
27112816
return;

‎de.peeeq.wurstscript/src/main/resources/agent-docs/WURST_LANGUAGE.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,18 @@ Unlike an interface used as a supertype, a bound works for `int`, `real`, `strin
184184

185185
Instances are unique and must be declared next to what they relate. An instance of `I` for type `X` may only live in the package declaring `I` or the package declaring `X`, and there may be only one. This makes `I` for `X` mean the same thing everywhere, independent of imports. Instances have no type parameters of their own in this version, so there is no way to write "every `List<T>` is `Indexable` when `T` is".
186186

187+
An instance must implement each requirement with the signature it has after the interface's type parameter is replaced by the instance type; a matching name is not enough. An interface used as a bound must not extend another interface, because the requirements of a bound are the interface's own functions.
188+
189+
A generic which passes its own type parameter to another bounded generic must declare that bound itself:
190+
191+
```wurst
192+
function inner<Q: Show>(Q x) returns string
193+
return Q.show(x)
194+
195+
function outer<R: Show>(R x) returns string // <R:> alone would not compile
196+
return inner(x)
197+
```
198+
187199
## Lua and Jass targets
188200

189201
The target is selected by the project `wurst.build` `scriptMode` field. `wc3Patch` separately selects the compatible core Jass and standard-library era.

0 commit comments

Comments
 (0)