diff --git a/BACKLOG.md b/BACKLOG.md index f1c6fc26f..ae96dcf0f 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -65,8 +65,38 @@ itself, and one gap in what the suite can see. Do not start this autonomously. -7. **Module bounds.** `module M` is rejected with a clear message today. Needs - receiver rewriting during expansion, or type parameters on `ModuleInstanciation`. +7. **Module bounds.** `module M` is rejected with a clear message today, and + `TypeClassTests.boundOnModuleTypeParameterIsRejected` pins that. Tried and reverted; what follows + is why, because the earlier note here suggested a fix which cannot work. + + Expansion copies the module body into the user and replaces the module's type parameters + **in type positions**. A requirement is called on the parameter itself — `T.show(x)` — and that + receiver is a name, resolved by `lookupBoundedTypeParam` through `lookupType`, so the replacement + never touches it. + + Renaming the receiver to the using class's parameter, which is what "receiver rewriting during + expansion" meant, does not work. `NameResolution.nextScope` sends a `ModuleInstanciation` to + `attrModuleOrigin()` rather than to the class using it: + + if (currentScope instanceof ModuleInstanciation) { + return nextScope(moduleInstanciation.attrModuleOrigin()); + } + + That is deliberate — a module body resolves in the module's own scope so it cannot capture the + names of whoever uses it — so the renamed receiver names something that scope cannot see. The + rename itself works: with it, the error moves from the rejection to `Could not find variable K` + at the dispatch, which is this scope rule and not a mistake in the rename. + + That leaves the other half of the original note: **type parameters on `ModuleInstanciation`**. The + instantiation declares the parameter itself, bound to the argument, so the copied body keeps + saying `T` and `T` resolves without any rename. It needs `ModuleInstanciation` to carry type + parameters in the grammar, so it is a change to `wurstscript.parseq` and everything reading that + node, not a patch to the expander. + + Worth knowing before starting: an argument which is a concrete type (`use Shower`) is a + second case even then. A requirement is dispatched on a type parameter, so `int.show(x)` is not a + dispatch at all — that one has to resolve to the instance during expansion rather than resolve by + name. 9. **Keep `WURST_LANGUAGE.md` and `CHANGELOG.md` current** as items land — a standing practice rather than a task to finish. `WURST_LANGUAGE.md` is tracked, at diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TypeClassTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TypeClassTests.java index 4f6def0f2..df0b45af4 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TypeClassTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TypeClassTests.java @@ -451,6 +451,54 @@ public void aBoundedMethodParameterInAGenericClassIsRejectedForLua() { .lines(SAME_NAMED_BOUNDED_PARAMETERS); } + /** + * A module may carry a bounded type parameter, and a class using it supplies the argument. Using + * a module copies its body into the class, substituting the module's type parameters — and a + * requirement is called on the parameter itself, {@code T.show(x)}, which is a name rather than a + * type, so the substitution never reaches it. + */ + private static final String[] BOUND_ON_MODULE = { + "package test", + "native testSuccess()", + "interface Show", + " function show(T x) returns int", + "implements Show", + " function show(int x) returns int", + " return x * 2", + "module Shower", + " T held", + " function shown() returns int", + " return T.show(held)", + "class Holder", + " use Shower", + " construct(K k)", + " held = k", + "init", + " let h = new Holder(21)", + " if h.shown() == 42", + " testSuccess()", + }; + + /** + * Rejected, and this pins that it is rejected clearly rather than mistranslated. + *

+ * A module body resolves names in the module's own scope by design — {@code nextScope} sends a + * {@code ModuleInstanciation} to {@code attrModuleOrigin()} rather than to the class using it, so + * a module cannot capture the names of whoever uses it. The type replacement during expansion + * therefore reaches every {@code T} used as a type, and cannot reach the one in {@code T.show(x)} + * which is a name: renaming it to the using class's parameter produces a name that scope + * deliberately cannot see. + *

+ * Making it work means the instantiation declaring the parameter itself, so the body keeps saying + * {@code T} and {@code T} resolves — type parameters on {@code ModuleInstanciation}, which is a + * grammar change. Backlog item 7. + */ + @Test + public void boundOnModuleTypeParameterIsRejected() { + testAssertErrorsLines(false, "Type class bounds are not supported on a module type parameter", + BOUND_ON_MODULE); + } + /** Each type argument picks its own instance, so one generic serves several types. */ @Test public void twoInstancesOfOneClass() {