From 540cbf19e55c4a6a1eb927dbc0de3424415781ef Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 16 Aug 2026 20:40:51 +0200 Subject: [PATCH] Record why module bounds cannot be fixed by rewriting the receiver The item proposed receiver rewriting during expansion or type parameters on ModuleInstanciation. The first was tried and does not work, for a reason worth keeping rather than rediscovering. Expansion 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 rather than a type, so the replacement never reaches it. Renaming it to the using class's parameter is what the note meant, and the rename works - with it the error moves from the rejection to "Could not find variable K" at the dispatch. That is NameResolution.nextScope sending a ModuleInstanciation to attrModuleOrigin() instead of to the class using it, so a module body cannot see the names of whoever uses it. Deliberate, and it rules the approach out. What is left is the other half: the instantiation declaring the parameter itself, so the body keeps saying T and T resolves. That is a grammar change. The test is kept as a pinned rejection carrying that reason, and an argument which is a concrete type is recorded as a second case, since a requirement dispatches on a type parameter and int.show(x) is not a dispatch at all. --- BACKLOG.md | 34 ++++++++++++- .../wurstscript/tests/TypeClassTests.java | 48 +++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/BACKLOG.md b/BACKLOG.md index 86165e337..b9b90e6d5 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -77,8 +77,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 031583379..cfc526a46 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 @@ -393,6 +393,54 @@ public void superCallToAGenericMethodOfABoundedGenericClass() { testAssertOkLines(true, SUPER_CALL_TO_A_GENERIC_METHOD); } + /** + * 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 an expression rather + * than a type, so it has to be carried across by that substitution too. + */ + 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() {