From c695eff45436b9c5efb4657539c4ec1bf7d2085a Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 1 Apr 2026 10:36:42 +0200 Subject: [PATCH 1/3] fix donach error --- .../wurstscript/types/FunctionSignature.java | 1 - .../tests/wurstscript/tests/GenericsTests.java | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/FunctionSignature.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/FunctionSignature.java index 62677a02a..f94a96c2a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/FunctionSignature.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/FunctionSignature.java @@ -234,7 +234,6 @@ public WurstType getVarargType() { for (int i = 0; i < argTypes.size(); i++) { WurstType pt = getParamType(i); WurstType at = argTypes.get(i); - mapping = at.matchAgainstSupertype(pt, location, mapping, VariablePosition.RIGHT); VariableBinding before = mapping; VariableBinding after = at.matchAgainstSupertype(pt, location, mapping, VariablePosition.RIGHT); WLogger.trace(() -> "[IMPLCONV] vb " + System.identityHashCode(before) diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsTests.java index 181097068..1c71d5868 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsTests.java @@ -1212,4 +1212,21 @@ public void nestedList2() { ); } + @Test + public void conflictingTypeArgsMemberMethod() { + // Regression test: calling a generic method via dot notation with conflicting type arguments + // used to cause a NullPointerException instead of a proper type error. + // The two arguments infer conflicting types for T (int vs string), which should produce + // a "Wrong parameter type" compile error, not a compiler crash. + testAssertErrorsLines(false, "Wrong parameter type", + "package test", + "class C", + " function combine(T x, T y) returns T", + " return x", + "init", + " let c = new C()", + " c.combine(1, \"hello\")" + ); + } + } From 87171e7f954ed52f181d5b3fc5b33f4e65db2938 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 1 Apr 2026 10:47:33 +0200 Subject: [PATCH 2/3] fixes & regression test --- .../attributes/names/FuncLink.java | 17 ++++- .../wurstscript/types/FunctionSignature.java | 10 ++- .../tests/GenericsModuleTests.java | 70 +++++++++++++++++++ 3 files changed, 93 insertions(+), 4 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/FuncLink.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/FuncLink.java index 55618981d..5622ed2f9 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/FuncLink.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/FuncLink.java @@ -69,8 +69,11 @@ public static FuncLink create(FunctionDefinition func, WScope definedIn) { WurstType returnType = func.attrReturnTyp(); WurstType receiverType = calcReceiverType(definedIn, func); - // Seed mapping with ALL visible type vars (not just the function's) - VariableBinding mapping = VariableBinding.emptyMapping().withTypeVariables(typeParams); + // Only the function's OWN type params need inference via argument matching. + // Enclosing structure type params (class/module) are resolved through + // receiver type matching in matchDefLinkReceiver, not through inference. + List ownTypeParams = typeParams(func).collect(Collectors.toList()); + VariableBinding mapping = VariableBinding.emptyMapping().withTypeVariables(ownTypeParams); return new FuncLink(visibility, definedIn, typeParams, receiverType, func, paramNames, paramTypes, returnType, mapping); } @@ -182,6 +185,16 @@ public FuncLink withTypeArgBinding(Element context, VariableBinding binding) { } WurstType newReceiverType = adjustType(context, getReceiverType(), binding); changed = changed || newReceiverType != getReceiverType(); + // Also check if any type params are being bound (even if types don't + // change structurally — e.g., T bound to itself within a generic module) + if (!changed) { + for (TypeParamDef tp : getTypeParams()) { + if (binding.contains(tp)) { + changed = true; + break; + } + } + } if (changed) { // remove type parameters that are now bound: List newTypeParams = new ArrayList<>(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/FunctionSignature.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/FunctionSignature.java index f94a96c2a..2f8bea2d5 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/FunctionSignature.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/FunctionSignature.java @@ -118,8 +118,14 @@ public static List getParamNames(WParameters parameters) { public static FunctionSignature fromNameLink(FuncLink f) { VariableBinding mapping = f.getVariableBinding(); - mapping = mapping.withTypeVariables(f.getTypeParams()); - return new FunctionSignature(f.getDef(), mapping, f.getReceiverType(), f.getName(), f.getParameterTypes(), getParamNames(f.getDef().getParameters()), f.getReturnType()); + // Only add the function definition's own type parameters as type variables + // for inference. Enclosing structure type params (from class/module) are + // resolved through receiver type matching, not argument inference. + FunctionDefinition def = f.getDef(); + if (def instanceof AstElementWithTypeParameters) { + mapping = mapping.withTypeVariables(((AstElementWithTypeParameters) def).getTypeParameters()); + } + return new FunctionSignature(def, mapping, f.getReceiverType(), f.getName(), f.getParameterTypes(), getParamNames(def.getParameters()), f.getReturnType()); } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsModuleTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsModuleTests.java index 3fa1fde84..cbd62b05d 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsModuleTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsModuleTests.java @@ -47,6 +47,76 @@ public void biggerModule() { ); } + // Regression: FuncLink.create() was seeding the mapping with ALL visible type params + // (including enclosing module/class type params), causing them to appear as unbound + // inference variables on generic function calls. Only the function's own type params + // should be seeded; module type params are resolved via receiver type matching. + @Test + public void genericModuleFunctionWithOwnTypeParam() { + testAssertOkLines(false, + "package test", + " native testSuccess()", + " module M", + " T value", + " function map(S s) returns S", + " return s", + " class C", + " use M", + " init", + " C c = new C", + " string r = c.map(\"hello\")", + " if r == \"hello\"", + " testSuccess()", + "endpackage" + ); + } + + // Regression: FuncLink.withTypeArgBinding() only detected structural type changes, + // missing the case where a type param is bound to itself (e.g. T→T in a generic class + // using a generic module). The FuncLink was not updated, leaving stale type params. + @Test + public void genericModuleInGenericClassGet() { + testAssertOkLines(false, + "package test", + " native testSuccess()", + " module M", + " T value", + " function get() returns T", + " return value", + " class C", + " use M", + " init", + " C c = new C", + " c.value = 42", + " int v = c.get()", + " if v == 42", + " testSuccess()", + "endpackage" + ); + } + + // Regression: static generic function in a generic module caused NPE due to + // duplicate matchAgainstSupertype call passing potentially-null mapping to the + // second call. FunctionSignature.fromNameLink also incorrectly added enclosing + // module type params as inference variables. + @Test + public void staticGenericFunctionInGenericModule() { + testAssertOkLines(false, + "package test", + " native testSuccess()", + " module M", + " static function wrap(S s) returns S", + " return s", + " class C", + " use M", + " init", + " int x = C.wrap(99)", + " if x == 99", + " testSuccess()", + "endpackage" + ); + } + @Test public void genericInception() { testAssertOkLines(false, From fc08063696b743262b8f65a9e98a71c14214d26b Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 1 Apr 2026 11:57:46 +0200 Subject: [PATCH 3/3] fix broken tests --- .github/workflows/build.yml | 8 ++++++++ .../peeeq/wurstscript/types/FunctionSignature.java | 14 ++++++++++---- .../wurstscript/tests/GenericsModuleTests.java | 4 ++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 43c805ccb..f4a50660d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -96,6 +96,14 @@ jobs: shell: bash run: ./gradlew test --no-daemon --stacktrace + - name: Report test results + if: always() + uses: mikepenz/action-junit-report@v4 + with: + report_paths: de.peeeq.wurstscript/build/test-results/**/*.xml + check_name: Test Results (${{ matrix.os }}) + fail_on_failure: false + - name: Upload packaged artifact (per-OS) uses: actions/upload-artifact@v4 with: diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/FunctionSignature.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/FunctionSignature.java index 2f8bea2d5..a8410b852 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/FunctionSignature.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/FunctionSignature.java @@ -118,12 +118,18 @@ public static List getParamNames(WParameters parameters) { public static FunctionSignature fromNameLink(FuncLink f) { VariableBinding mapping = f.getVariableBinding(); - // Only add the function definition's own type parameters as type variables - // for inference. Enclosing structure type params (from class/module) are - // resolved through receiver type matching, not argument inference. FunctionDefinition def = f.getDef(); + // Only add the function's own type params that are still unbound (i.e., still in + // f.getTypeParams() — withTypeArgBinding removes them as they get resolved). + // We must NOT add enclosing structure (module/class) type params, which would + // appear as spurious unbound inference variables. if (def instanceof AstElementWithTypeParameters) { - mapping = mapping.withTypeVariables(((AstElementWithTypeParameters) def).getTypeParameters()); + java.util.Set ownParams = new java.util.HashSet<>( + ((AstElementWithTypeParameters) def).getTypeParameters()); + List unboundOwn = f.getTypeParams().stream() + .filter(ownParams::contains) + .collect(Collectors.toList()); + mapping = mapping.withTypeVariables(unboundOwn); } return new FunctionSignature(def, mapping, f.getReceiverType(), f.getName(), f.getParameterTypes(), getParamNames(def.getParameters()), f.getReturnType()); } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsModuleTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsModuleTests.java index cbd62b05d..218d716a2 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsModuleTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsModuleTests.java @@ -64,8 +64,8 @@ public void genericModuleFunctionWithOwnTypeParam() { " use M", " init", " C c = new C", - " string r = c.map(\"hello\")", - " if r == \"hello\"", + " int r = c.map(42)", + " if r == 42", " testSuccess()", "endpackage" );