From d9e21003480fbb9148ae6952245639bc407764e5 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 8 Aug 2026 09:51:03 +0200 Subject: [PATCH 01/12] Add compile-time field iteration --- .../de/peeeq/wurstscript/SyntacticSugar.java | 105 +++++++++++++++++ .../tests/FieldIterationTests.java | 107 ++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java index 32b3c98b7..29c0f3523 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java @@ -20,10 +20,115 @@ public void removeSyntacticSugar(CompilationUnit root, boolean hasCommonJ) { } rewriteNegatedInts(root); addDefaultConstructors(root); + expandFieldIterations(root); addEndFunctionStatements(root); replaceTypeIdUse(root); } + /** + * Expands field-wise operations before name and overload resolution. This gives serializers a + * reflection-like API while keeping the generated program equivalent to handwritten direct + * field accesses. + * + *
+     * forFields((name, value) -> writer.write(name, value))
+     * mapFields((name, value) -> reader.read(name, value))
+     * 
+ */ + private void expandFieldIterations(CompilationUnit root) { + List calls = new ArrayList<>(); + root.accept(new WurstModel.DefaultVisitor() { + @Override + public void visit(ExprFunctionCall call) { + super.visit(call); + String name = call.getFuncName(); + if ("forFields".equals(name) || "mapFields".equals(name)) { + calls.add(call); + } + } + }); + + for (ExprFunctionCall call : calls) { + expandFieldIteration(call, "mapFields".equals(call.getFuncName())); + } + } + + private void expandFieldIteration(ExprFunctionCall call, boolean assignsResult) { + if (!(call.getParent() instanceof WStatements statements)) { + call.addError(call.getFuncName() + " can only be used as a statement."); + return; + } + ClassDef classDef = call.attrNearestClassDef(); + if (classDef == null || !call.attrIsDynamicContext()) { + call.addError(call.getFuncName() + " can only be used in an instance method or constructor."); + return; + } + if (call.getArgs().size() != 1 || !(call.getArgs().get(0) instanceof ExprClosure closure) + || closure.getShortParameters().size() != 2) { + call.addError(call.getFuncName() + " expects a closure with (fieldName, fieldValue) parameters."); + return; + } + + String nameParameter = closure.getShortParameters().get(0).getName(); + String valueParameter = closure.getShortParameters().get(1).getName(); + if (!assignsResult && !(closure.getImplementation() instanceof WStatement)) { + call.addError("forFields closure must produce a statement expression."); + return; + } + int statementIndex = statements.indexOf(call); + statements.remove(statementIndex); + + for (GlobalVarDef field : classDef.getVars()) { + if (field.attrIsStatic()) { + continue; + } + Expr fieldAccess = fieldAccess(call.getSource(), field.getName()); + Expr implementation = substituteFieldParameters( + closure.getImplementation().copy(), nameParameter, valueParameter, field.getName()); + WStatement expanded; + if (assignsResult) { + expanded = Ast.StmtSet(call.getSource(), (LExpr) fieldAccess, implementation); + } else { + expanded = (WStatement) implementation; + } + statements.add(statementIndex++, expanded); + } + } + + private Expr substituteFieldParameters(Expr expression, String nameParameter, + String valueParameter, String fieldName) { + if (expression instanceof ExprVarAccess access) { + if (access.getVarName().equals(nameParameter)) { + return Ast.ExprStringVal(access.getSource(), fieldName); + } + if (access.getVarName().equals(valueParameter)) { + return fieldAccess(access.getSource(), fieldName); + } + } + + List accesses = new ArrayList<>(); + expression.accept(new WurstModel.DefaultVisitor() { + @Override + public void visit(ExprVarAccess access) { + super.visit(access); + if (access.getVarName().equals(nameParameter) || access.getVarName().equals(valueParameter)) { + accesses.add(access); + } + } + }); + for (ExprVarAccess access : accesses) { + Expr replacement = access.getVarName().equals(nameParameter) + ? Ast.ExprStringVal(access.getSource(), fieldName) + : fieldAccess(access.getSource(), fieldName); + access.replaceBy(replacement); + } + return expression; + } + + private ExprMemberVarDot fieldAccess(WPos source, String fieldName) { + return Ast.ExprMemberVarDot(source, Ast.ExprThis(source), Ast.Identifier(source, fieldName)); + } + private void replaceTypeIdUse(CompilationUnit root) { final Map replacements = Maps.newLinkedHashMap(); root.accept(new WurstModel.DefaultVisitor() { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java new file mode 100644 index 000000000..a008a96a6 --- /dev/null +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -0,0 +1,107 @@ +package tests.wurstscript.tests; + +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class FieldIterationTests extends WurstScriptTest { + + @Test + public void serializesAndDeserializesFieldsWithoutRuntimeReflection() throws IOException { + test() + .testLua(true) + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + "", + " class Codec", + " int writtenInt", + " string writtenString", + " string writtenIntName", + " string writtenStringName", + "", + " function write(string fieldName, int value)", + " writtenInt = value", + " writtenIntName = fieldName", + "", + " function write(string fieldName, string value)", + " writtenString = value", + " writtenStringName = fieldName", + "", + " function read(string fieldName, int oldValue) returns int", + " if fieldName == \"score\"", + " return 42", + " return -1", + "", + " function read(string fieldName, string oldValue) returns string", + " if fieldName == \"name\"", + " return \"loaded\"", + " return \"wrong field\"", + "", + " class Data", + " int score = 7", + " string name = \"initial\"", + " static int schemaVersion = 1", + "", + " function save(Codec codec)", + " forFields((fieldName, value) -> codec.write(fieldName, value))", + "", + " function load(Codec codec)", + " mapFields((fieldName, value) -> codec.read(fieldName, value))", + "", + " init", + " let codec = new Codec", + " let data = new Data", + " data.save(codec)", + " if codec.writtenInt == 7 and codec.writtenString == \"initial\" and codec.writtenIntName == \"score\" and codec.writtenStringName == \"name\"", + " data.load(codec)", + " if data.score == 42 and data.name == \"loaded\"", + " testSuccess()", + "endpackage" + ); + + String lua = Files.readString(new File(TEST_OUTPUT_PATH + + "lua/FieldIterationTests_serializesAndDeserializesFieldsWithoutRuntimeReflection.lua").toPath()); + assertFalse(lua.contains("forFields")); + assertFalse(lua.contains("mapFields")); + assertTrue(lua.contains("Codec_Codec_write(codec, \"score\", this")); + assertTrue(lua.contains("Codec_Codec_write1(codec, \"name\", this")); + assertTrue(lua.contains("Data_score = Codec_Codec_read(codec1, \"score\"")); + assertTrue(lua.contains("Data_name = Codec_Codec_read1(codec1, \"name\"")); + } + + @Test + public void rejectsFieldIterationOutsideInstanceContext() { + test() + .expectError("can only be used in an instance method or constructor") + .lines( + "package FieldIterationTest", + " function consume(string name, int value)", + "", + " init", + " forFields((name, value) -> consume(name, value))", + "endpackage" + ); + } + + @Test + public void rejectsInvalidFieldIterationClosure() { + test() + .expectError("expects a closure with (fieldName, fieldValue) parameters") + .lines( + "package FieldIterationTest", + " class Data", + " int value", + "", + " function save()", + " forFields(value -> value)", + "endpackage" + ); + } +} From 6d78b456b0fcbb7c5e4fa86dc753a3bbe7b086c4 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 8 Aug 2026 11:23:02 +0200 Subject: [PATCH 02/12] Address field iteration review feedback --- .../de/peeeq/wurstscript/SyntacticSugar.java | 28 ++++++-- .../tests/FieldIterationTests.java | 67 +++++++++++++++++-- 2 files changed, 84 insertions(+), 11 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java index 29c0f3523..9419c5948 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java @@ -14,6 +14,10 @@ */ public class SyntacticSugar { + /** Compiler-reserved spelling keeps ordinary user functions named forFields/mapFields valid. */ + private static final String FOR_FIELDS = "__wurst_forFields"; + private static final String MAP_FIELDS = "__wurst_mapFields"; + public void removeSyntacticSugar(CompilationUnit root, boolean hasCommonJ) { if (hasCommonJ) { addDefaultImports(root); @@ -31,8 +35,8 @@ public void removeSyntacticSugar(CompilationUnit root, boolean hasCommonJ) { * field accesses. * *
-     * forFields((name, value) -> writer.write(name, value))
-     * mapFields((name, value) -> reader.read(name, value))
+     * __wurst_forFields((name, value) -> writer.write(name, value))
+     * __wurst_mapFields((name, value) -> reader.read(name, value))
      * 
*/ private void expandFieldIterations(CompilationUnit root) { @@ -42,14 +46,14 @@ private void expandFieldIterations(CompilationUnit root) { public void visit(ExprFunctionCall call) { super.visit(call); String name = call.getFuncName(); - if ("forFields".equals(name) || "mapFields".equals(name)) { + if (FOR_FIELDS.equals(name) || MAP_FIELDS.equals(name)) { calls.add(call); } } }); for (ExprFunctionCall call : calls) { - expandFieldIteration(call, "mapFields".equals(call.getFuncName())); + expandFieldIteration(call, MAP_FIELDS.equals(call.getFuncName())); } } @@ -108,10 +112,24 @@ private Expr substituteFieldParameters(Expr expression, String nameParameter, List accesses = new ArrayList<>(); expression.accept(new WurstModel.DefaultVisitor() { + private final Set shadowed = new HashSet<>(); + + @Override + public void visit(ExprClosure nestedClosure) { + Set previous = new HashSet<>(shadowed); + for (WShortParameter parameter : nestedClosure.getShortParameters()) { + shadowed.add(parameter.getName()); + } + super.visit(nestedClosure); + shadowed.clear(); + shadowed.addAll(previous); + } + @Override public void visit(ExprVarAccess access) { super.visit(access); - if (access.getVarName().equals(nameParameter) || access.getVarName().equals(valueParameter)) { + if (!shadowed.contains(access.getVarName()) + && (access.getVarName().equals(nameParameter) || access.getVarName().equals(valueParameter))) { accesses.add(access); } } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index a008a96a6..71a2386cc 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -15,6 +15,7 @@ public class FieldIterationTests extends WurstScriptTest { public void serializesAndDeserializesFieldsWithoutRuntimeReflection() throws IOException { test() .testLua(true) + .luaOnly(false) .executeProg() .lines( "package FieldIterationTest", @@ -50,10 +51,10 @@ public void serializesAndDeserializesFieldsWithoutRuntimeReflection() throws IOE " static int schemaVersion = 1", "", " function save(Codec codec)", - " forFields((fieldName, value) -> codec.write(fieldName, value))", + " __wurst_forFields((fieldName, value) -> codec.write(fieldName, value))", "", " function load(Codec codec)", - " mapFields((fieldName, value) -> codec.read(fieldName, value))", + " __wurst_mapFields((fieldName, value) -> codec.read(fieldName, value))", "", " init", " let codec = new Codec", @@ -68,8 +69,8 @@ public void serializesAndDeserializesFieldsWithoutRuntimeReflection() throws IOE String lua = Files.readString(new File(TEST_OUTPUT_PATH + "lua/FieldIterationTests_serializesAndDeserializesFieldsWithoutRuntimeReflection.lua").toPath()); - assertFalse(lua.contains("forFields")); - assertFalse(lua.contains("mapFields")); + assertFalse(lua.contains("__wurst_forFields")); + assertFalse(lua.contains("__wurst_mapFields")); assertTrue(lua.contains("Codec_Codec_write(codec, \"score\", this")); assertTrue(lua.contains("Codec_Codec_write1(codec, \"name\", this")); assertTrue(lua.contains("Data_score = Codec_Codec_read(codec1, \"score\"")); @@ -85,7 +86,7 @@ public void rejectsFieldIterationOutsideInstanceContext() { " function consume(string name, int value)", "", " init", - " forFields((name, value) -> consume(name, value))", + " __wurst_forFields((name, value) -> consume(name, value))", "endpackage" ); } @@ -100,7 +101,61 @@ public void rejectsInvalidFieldIterationClosure() { " int value", "", " function save()", - " forFields(value -> value)", + " __wurst_forFields(value -> value)", + "endpackage" + ); + } + + @Test + public void ordinaryFieldHelperNamesRemainUserFunctions() { + test() + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + "", + " function forFields(int value) returns int", + " return value + 1", + "", + " init", + " if forFields(1) == 2", + " testSuccess()", + "endpackage" + ); + } + + @Test + public void nestedClosureParametersAreNotSubstituted() { + test() + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " interface IntFunc", + " function apply(int value) returns int", + "", + " function consume(IntFunc callback) returns int", + " return callback.apply(5)", + "", + " class Codec", + " int total", + "", + " function write(string fieldName, int value)", + " total = total + value", + "", + " class Data", + " int first = 1", + " int second = 2", + "", + " function save(Codec codec)", + " __wurst_forFields((fieldName, value) -> codec.write(fieldName, consume((int value) -> value)))", + "", + " init", + " let codec = new Codec", + " let data = new Data", + " data.save(codec)", + " if codec.total == 10", + " testSuccess()", "endpackage" ); } From 6304748a09be38e1a3bf317d5cbb7814599bf19c Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 8 Aug 2026 11:46:56 +0200 Subject: [PATCH 03/12] Validate field iteration parameters --- .../java/de/peeeq/wurstscript/SyntacticSugar.java | 6 ++++++ .../wurstscript/tests/FieldIterationTests.java | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java index 9419c5948..1e19a9c75 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java @@ -72,6 +72,12 @@ private void expandFieldIteration(ExprFunctionCall call, boolean assignsResult) call.addError(call.getFuncName() + " expects a closure with (fieldName, fieldValue) parameters."); return; } + for (WShortParameter parameter : closure.getShortParameters()) { + if (!(parameter.getTypOpt() instanceof NoTypeExpr)) { + parameter.addError("Field iteration closure parameters must use inferred types."); + return; + } + } String nameParameter = closure.getShortParameters().get(0).getName(); String valueParameter = closure.getShortParameters().get(1).getName(); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index 71a2386cc..983521b1b 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -159,4 +159,19 @@ public void nestedClosureParametersAreNotSubstituted() { "endpackage" ); } + + @Test + public void rejectsExplicitFieldIterationParameterTypes() { + test() + .expectError("must use inferred types") + .lines( + "package FieldIterationTest", + " class Data", + " int value", + "", + " function save()", + " __wurst_forFields((NoSuch name, NoSuch value) -> value)", + "endpackage" + ); + } } From b92f5d0fa32922c6a0ea35c47451ca7571f20d70 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 8 Aug 2026 12:06:17 +0200 Subject: [PATCH 04/12] Validate field iteration placeholders --- .../de/peeeq/wurstscript/SyntacticSugar.java | 20 ++++++++++--- .../tests/FieldIterationTests.java | 30 +++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java index 1e19a9c75..2bd399c1e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java @@ -81,17 +81,29 @@ private void expandFieldIteration(ExprFunctionCall call, boolean assignsResult) String nameParameter = closure.getShortParameters().get(0).getName(); String valueParameter = closure.getShortParameters().get(1).getName(); + if (nameParameter.equals(valueParameter)) { + closure.getShortParameters().get(1).addError( + "Field iteration closure parameters must have distinct names."); + return; + } if (!assignsResult && !(closure.getImplementation() instanceof WStatement)) { call.addError("forFields closure must produce a statement expression."); return; } + List fields = new ArrayList<>(); + for (GlobalVarDef field : classDef.getVars()) { + if (!field.attrIsStatic()) { + fields.add(field); + } + } + if (fields.isEmpty()) { + call.addError(call.getFuncName() + " requires at least one instance field."); + return; + } int statementIndex = statements.indexOf(call); statements.remove(statementIndex); - for (GlobalVarDef field : classDef.getVars()) { - if (field.attrIsStatic()) { - continue; - } + for (GlobalVarDef field : fields) { Expr fieldAccess = fieldAccess(call.getSource(), field.getName()); Expr implementation = substituteFieldParameters( closure.getImplementation().copy(), nameParameter, valueParameter, field.getName()); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index 983521b1b..6366022c8 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -174,4 +174,34 @@ public void rejectsExplicitFieldIterationParameterTypes() { "endpackage" ); } + + @Test + public void rejectsFieldIterationWithoutInstanceFields() { + test() + .expectError("requires at least one instance field") + .lines( + "package FieldIterationTest", + " class Data", + " static int schemaVersion = 1", + "", + " function save()", + " __wurst_forFields((name, value) -> noSuchFunction(name, value))", + "endpackage" + ); + } + + @Test + public void rejectsDuplicateFieldIterationParameterNames() { + test() + .expectError("must have distinct names") + .lines( + "package FieldIterationTest", + " class Data", + " int value", + "", + " function save()", + " __wurst_mapFields((value, value) -> 42)", + "endpackage" + ); + } } From c84d81a148d74851f91255d2b762a72c2cf2e013 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 8 Aug 2026 12:14:54 +0200 Subject: [PATCH 05/12] Respect local callback bindings --- .../de/peeeq/wurstscript/SyntacticSugar.java | 31 ++++++++++++++++--- .../tests/FieldIterationTests.java | 26 ++++++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java index 2bd399c1e..873d04587 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java @@ -130,23 +130,44 @@ private Expr substituteFieldParameters(Expr expression, String nameParameter, List accesses = new ArrayList<>(); expression.accept(new WurstModel.DefaultVisitor() { - private final Set shadowed = new HashSet<>(); + private final Deque> shadowedScopes = new ArrayDeque<>(); + + private boolean isShadowed(String name) { + return shadowedScopes.stream().anyMatch(scope -> scope.contains(name)); + } + + @Override + public void visit(WStatements statements) { + shadowedScopes.push(new HashSet<>()); + super.visit(statements); + shadowedScopes.pop(); + } @Override public void visit(ExprClosure nestedClosure) { - Set previous = new HashSet<>(shadowed); + Set shadowed = new HashSet<>(); for (WShortParameter parameter : nestedClosure.getShortParameters()) { shadowed.add(parameter.getName()); } + shadowedScopes.push(shadowed); super.visit(nestedClosure); - shadowed.clear(); - shadowed.addAll(previous); + shadowedScopes.pop(); + } + + @Override + public void visit(LocalVarDef localVarDef) { + super.visit(localVarDef); + if (!shadowedScopes.isEmpty() + && (localVarDef.getName().equals(nameParameter) + || localVarDef.getName().equals(valueParameter))) { + shadowedScopes.peek().add(localVarDef.getName()); + } } @Override public void visit(ExprVarAccess access) { super.visit(access); - if (!shadowed.contains(access.getVarName()) + if (!isShadowed(access.getVarName()) && (access.getVarName().equals(nameParameter) || access.getVarName().equals(valueParameter))) { accesses.add(access); } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index 6366022c8..723dddd4b 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -204,4 +204,30 @@ public void rejectsDuplicateFieldIterationParameterNames() { "endpackage" ); } + + @Test + public void preservesLocalBindingsInBlockCallbacks() { + test() + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " class Data", + " int first = 1", + " int second = 2", + "", + " function load()", + " __wurst_mapFields((name, value) -> begin", + " let value = 42", + " return value", + " end)", + "", + " init", + " let data = new Data", + " data.load()", + " if data.first == 42 and data.second == 42", + " testSuccess()", + "endpackage" + ); + } } From 73b90e988e1cd37bcb5935be18e3764e7d9b7a19 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 8 Aug 2026 12:58:47 +0200 Subject: [PATCH 06/12] Scope field iteration loop bindings --- .../de/peeeq/wurstscript/SyntacticSugar.java | 42 +++++++++++++++++++ .../tests/FieldIterationTests.java | 27 ++++++++++++ 2 files changed, 69 insertions(+) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java index 873d04587..a90ed7c77 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java @@ -143,6 +143,48 @@ public void visit(WStatements statements) { shadowedScopes.pop(); } + private void visitLoopVariable(LocalVarDef loopVariable) { + loopVariable.getModifiers().accept(this); + loopVariable.getOptTyp().accept(this); + loopVariable.getInitialExpr().accept(this); + } + + private void visitLoopBody(LocalVarDef loopVariable, WStatements body) { + shadowedScopes.push(Set.of(loopVariable.getName())); + body.accept(this); + shadowedScopes.pop(); + } + + @Override + public void visit(StmtForRangeUp loop) { + visitLoopVariable(loop.getLoopVar()); + loop.getTo().accept(this); + loop.getStep().accept(this); + visitLoopBody(loop.getLoopVar(), loop.getBody()); + } + + @Override + public void visit(StmtForRangeDown loop) { + visitLoopVariable(loop.getLoopVar()); + loop.getTo().accept(this); + loop.getStep().accept(this); + visitLoopBody(loop.getLoopVar(), loop.getBody()); + } + + @Override + public void visit(StmtForIn loop) { + visitLoopVariable(loop.getLoopVar()); + loop.getIn().accept(this); + visitLoopBody(loop.getLoopVar(), loop.getBody()); + } + + @Override + public void visit(StmtForFrom loop) { + visitLoopVariable(loop.getLoopVar()); + loop.getIn().accept(this); + visitLoopBody(loop.getLoopVar(), loop.getBody()); + } + @Override public void visit(ExprClosure nestedClosure) { Set shadowed = new HashSet<>(); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index 723dddd4b..a722400a2 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -230,4 +230,31 @@ public void preservesLocalBindingsInBlockCallbacks() { "endpackage" ); } + + @Test + public void keepsLoopBindingsInsideLoopBody() { + test() + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " class Data", + " int first = 1", + " int second = 2", + "", + " function load()", + " __wurst_mapFields((name, value) -> begin", + " for int value = 0 to 1", + " continue", + " return 42 + value - value", + " end)", + "", + " init", + " let data = new Data", + " data.load()", + " if data.first == 42 and data.second == 42", + " testSuccess()", + "endpackage" + ); + } } From eec5b0b868b75312260f9c3cb9adc0ae1a622f11 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 8 Aug 2026 13:44:22 +0200 Subject: [PATCH 07/12] Reject placeholder shadowing in field callbacks --- .../de/peeeq/wurstscript/SyntacticSugar.java | 34 ++++++++++++++++++- .../tests/FieldIterationTests.java | 25 ++++++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java index a90ed7c77..eae315254 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java @@ -86,6 +86,11 @@ private void expandFieldIteration(ExprFunctionCall call, boolean assignsResult) "Field iteration closure parameters must have distinct names."); return; } + if (hasShadowingLocal(closure, nameParameter, valueParameter)) { + call.addError("Field iteration callbacks cannot declare locals or loop variables named " + + nameParameter + " or " + valueParameter + "."); + return; + } if (!assignsResult && !(closure.getImplementation() instanceof WStatement)) { call.addError("forFields closure must produce a statement expression."); return; @@ -117,6 +122,25 @@ private void expandFieldIteration(ExprFunctionCall call, boolean assignsResult) } } + private boolean hasShadowingLocal(ExprClosure closure, String nameParameter, String valueParameter) { + final boolean[] result = {false}; + closure.getImplementation().accept(new WurstModel.DefaultVisitor() { + @Override + public void visit(ExprClosure nestedClosure) { + // Nested closures have independent parameter scopes. + } + + @Override + public void visit(LocalVarDef localVarDef) { + super.visit(localVarDef); + if (localVarDef.getName().equals(nameParameter) || localVarDef.getName().equals(valueParameter)) { + result[0] = true; + } + } + }); + return result[0]; + } + private Expr substituteFieldParameters(Expr expression, String nameParameter, String valueParameter, String fieldName) { if (expression instanceof ExprVarAccess access) { @@ -138,7 +162,15 @@ private boolean isShadowed(String name) { @Override public void visit(WStatements statements) { - shadowedScopes.push(new HashSet<>()); + Set blockBindings = new HashSet<>(); + for (WStatement statement : statements) { + if (statement instanceof LocalVarDef localVarDef + && (localVarDef.getName().equals(nameParameter) + || localVarDef.getName().equals(valueParameter))) { + blockBindings.add(localVarDef.getName()); + } + } + shadowedScopes.push(blockBindings); super.visit(statements); shadowedScopes.pop(); } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index a722400a2..f63703435 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -218,8 +218,8 @@ public void preservesLocalBindingsInBlockCallbacks() { "", " function load()", " __wurst_mapFields((name, value) -> begin", - " let value = 42", - " return value", + " let temporary = 42", + " return temporary", " end)", "", " init", @@ -244,7 +244,7 @@ public void keepsLoopBindingsInsideLoopBody() { "", " function load()", " __wurst_mapFields((name, value) -> begin", - " for int value = 0 to 1", + " for int index = 0 to 1", " continue", " return 42 + value - value", " end)", @@ -257,4 +257,23 @@ public void keepsLoopBindingsInsideLoopBody() { "endpackage" ); } + + @Test + public void rejectsBlockLocalShadowingBeforeExpansion() { + test() + .expectError("cannot declare locals or loop variables") + .lines( + "package FieldIterationTest", + " class Data", + " int value", + "", + " function load()", + " __wurst_mapFields((name, value) -> begin", + " let old = value", + " let value = 42", + " return old", + " end)", + "endpackage" + ); + } } From c924a3e4f5a49e46cc6af4d5c4075dbfdc37acd4 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 8 Aug 2026 14:11:33 +0200 Subject: [PATCH 08/12] Expand field iteration after module instantiation --- .../de/peeeq/wurstscript/SyntacticSugar.java | 63 ++++++++++++++++--- .../de/peeeq/wurstscript/WurstChecker.java | 4 ++ .../tests/FieldIterationTests.java | 60 ++++++++++++++++++ 3 files changed, 118 insertions(+), 9 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java index eae315254..4f5dbae56 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java @@ -3,6 +3,7 @@ import com.google.common.collect.Maps; import de.peeeq.wurstscript.ast.*; import de.peeeq.wurstscript.parser.WPos; +import de.peeeq.wurstscript.types.WurstTypeClass; import java.util.*; @@ -24,11 +25,19 @@ public void removeSyntacticSugar(CompilationUnit root, boolean hasCommonJ) { } rewriteNegatedInts(root); addDefaultConstructors(root); - expandFieldIterations(root); addEndFunctionStatements(root); replaceTypeIdUse(root); } + /** + * Expands field iteration after module methods have been copied into their consuming classes. + * This must run after {@link ModuleExpander#expandModules(CompilationUnit)} so a module callback can + * see all fields of the concrete class using it. + */ + public void expandFieldIterations(CompilationUnit root) { + expandFieldIterationsInTree(root); + } + /** * Expands field-wise operations before name and overload resolution. This gives serializers a * reflection-like API while keeping the generated program equivalent to handwritten direct @@ -39,7 +48,7 @@ public void removeSyntacticSugar(CompilationUnit root, boolean hasCommonJ) { * __wurst_mapFields((name, value) -> reader.read(name, value)) * */ - private void expandFieldIterations(CompilationUnit root) { + private void expandFieldIterationsInTree(CompilationUnit root) { List calls = new ArrayList<>(); root.accept(new WurstModel.DefaultVisitor() { @Override @@ -63,7 +72,18 @@ private void expandFieldIteration(ExprFunctionCall call, boolean assignsResult) return; } ClassDef classDef = call.attrNearestClassDef(); - if (classDef == null || !call.attrIsDynamicContext()) { + ClassOrModule owner = call.attrNearestClassOrModule(); + if (!call.attrIsDynamicContext()) { + call.addError(call.getFuncName() + " can only be used in an instance method or constructor."); + return; + } + if (owner instanceof ModuleDef) { + // Module bodies are templates. Their copies were made by ModuleExpander; validate and + // expand those concrete copies instead of type-checking this uninstantiated template. + statements.remove(call); + return; + } + if (classDef == null) { call.addError(call.getFuncName() + " can only be used in an instance method or constructor."); return; } @@ -95,12 +115,7 @@ private void expandFieldIteration(ExprFunctionCall call, boolean assignsResult) call.addError("forFields closure must produce a statement expression."); return; } - List fields = new ArrayList<>(); - for (GlobalVarDef field : classDef.getVars()) { - if (!field.attrIsStatic()) { - fields.add(field); - } - } + List fields = collectInstanceFields(classDef, owner); if (fields.isEmpty()) { call.addError(call.getFuncName() + " requires at least one instance field."); return; @@ -122,6 +137,36 @@ private void expandFieldIteration(ExprFunctionCall call, boolean assignsResult) } } + private List collectInstanceFields(ClassDef classDef, ClassOrModule owner) { + List fields = new ArrayList<>(); + if (classDef != null) { + collectInheritedFields(classDef.attrTypC(), fields, Collections.newSetFromMap(new IdentityHashMap<>())); + } else if (owner instanceof ModuleDef moduleDef) { + addInstanceFields(moduleDef.getVars(), fields); + } + return fields; + } + + private void collectInheritedFields(WurstTypeClass type, + List fields, Set visited) { + if (!visited.add(type.getClassDef())) { + return; + } + WurstTypeClass superType = type.extendedClass(); + if (superType != null) { + collectInheritedFields(superType, fields, visited); + } + addInstanceFields(type.getClassDef().getVars(), fields); + } + + private void addInstanceFields(Iterable declarations, List fields) { + for (GlobalVarDef field : declarations) { + if (!field.attrIsStatic()) { + fields.add(field); + } + } + } + private boolean hasShadowingLocal(ExprClosure closure, String nameParameter, String valueParameter) { final boolean[] result = {false}; closure.getImplementation().accept(new WurstModel.DefaultVisitor() { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstChecker.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstChecker.java index f5d50c488..41be15bca 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstChecker.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstChecker.java @@ -47,6 +47,10 @@ public void checkProg(WurstModel root, Collection toCheck) { if (errorHandler.getErrorCount() > 0) return; + for (CompilationUnit cu : toCheck) { + new SyntacticSugar().expandFieldIterations(cu); + } + // compute the flow attributes for (CompilationUnit cu : toCheck) { WurstValidator.computeFlowAttributes(cu); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index f63703435..3f1a215de 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -276,4 +276,64 @@ public void rejectsBlockLocalShadowingBeforeExpansion() { "endpackage" ); } + + @Test + public void includesInheritedInstanceFields() { + test() + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " class Acc", + " int total = 0", + " function add(int value)", + " total = total + value", + " class Base", + " int inherited = 1", + " class Data extends Base", + " int local = 2", + "", + " function save(Acc acc)", + " __wurst_forFields((name, value) -> acc.add(value))", + "", + " init", + " let acc = new Acc", + " let data = new Data", + " data.save(acc)", + " if acc.total == 3", + " testSuccess()", + "endpackage" + ); + } + + @Test + public void expandsFieldIterationInModuleMethodsAfterInstantiation() { + test() + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " class Acc", + " int total = 0", + " function add(int value)", + " total = total + value", + " module Serializer", + " function save(Acc acc)", + " __wurst_forFields((name, value) -> acc.add(value))", + "", + " class Data", + " use Serializer", + " int inherited = 1", + " int local = 2", + " int count = 0", + "", + " init", + " let acc = new Acc", + " let data = new Data", + " data.save(acc)", + " if acc.total == 3", + " testSuccess()", + "endpackage" + ); + } } From 514b0d7ebf0581b3d72e0827c88e3ddc2d995d06 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 8 Aug 2026 14:42:19 +0200 Subject: [PATCH 09/12] Preserve module templates during field expansion --- .../de/peeeq/wurstscript/SyntacticSugar.java | 77 +++++++++++++++++-- .../de/peeeq/wurstscript/WurstChecker.java | 27 ++++--- .../tests/FieldIterationTests.java | 33 ++++++++ 3 files changed, 120 insertions(+), 17 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java index 4f5dbae56..58111e145 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java @@ -19,6 +19,28 @@ public class SyntacticSugar { private static final String FOR_FIELDS = "__wurst_forFields"; private static final String MAP_FIELDS = "__wurst_mapFields"; + public static final class DeferredModuleCall { + private final WStatements statements; + private final int index; + private final ExprFunctionCall call; + + private DeferredModuleCall(WStatements statements, int index, ExprFunctionCall call) { + this.statements = statements; + this.index = index; + this.call = call; + } + } + + public static boolean isFieldIterationIntrinsic(ExprFunctionCall call) { + return FOR_FIELDS.equals(call.getFuncName()) || MAP_FIELDS.equals(call.getFuncName()); + } + + public static boolean isUninstantiatedModuleFieldIteration(ExprFunctionCall call) { + return isFieldIterationIntrinsic(call) + && call.attrNearestClassDef() == null + && call.attrNearestClassOrModule() instanceof ModuleDef; + } + public void removeSyntacticSugar(CompilationUnit root, boolean hasCommonJ) { if (hasCommonJ) { addDefaultImports(root); @@ -38,6 +60,32 @@ public void expandFieldIterations(CompilationUnit root) { expandFieldIterationsInTree(root); } + /** Temporarily removes template intrinsics while validation runs; callers must restore them. */ + public List detachModuleTemplateFieldIterations(CompilationUnit root) { + List detached = new ArrayList<>(); + root.accept(new WurstModel.DefaultVisitor() { + @Override + public void visit(ExprFunctionCall call) { + super.visit(call); + if (isUninstantiatedModuleFieldIteration(call) + && call.getParent() instanceof WStatements statements) { + int index = statements.indexOf(call); + statements.remove(index); + detached.add(new DeferredModuleCall(statements, index, call)); + } + } + }); + return detached; + } + + public void restoreModuleTemplateFieldIterations(List detached) { + for (int i = detached.size() - 1; i >= 0; i--) { + DeferredModuleCall state = detached.get(i); + int index = Math.min(state.index, state.statements.size()); + state.statements.add(index, state.call); + } + } + /** * Expands field-wise operations before name and overload resolution. This gives serializers a * reflection-like API while keeping the generated program equivalent to handwritten direct @@ -54,8 +102,7 @@ private void expandFieldIterationsInTree(CompilationUnit root) { @Override public void visit(ExprFunctionCall call) { super.visit(call); - String name = call.getFuncName(); - if (FOR_FIELDS.equals(name) || MAP_FIELDS.equals(name)) { + if (isFieldIterationIntrinsic(call)) { calls.add(call); } } @@ -80,7 +127,6 @@ private void expandFieldIteration(ExprFunctionCall call, boolean assignsResult) if (owner instanceof ModuleDef) { // Module bodies are templates. Their copies were made by ModuleExpander; validate and // expand those concrete copies instead of type-checking this uninstantiated template. - statements.remove(call); return; } if (classDef == null) { @@ -140,7 +186,9 @@ private void expandFieldIteration(ExprFunctionCall call, boolean assignsResult) private List collectInstanceFields(ClassDef classDef, ClassOrModule owner) { List fields = new ArrayList<>(); if (classDef != null) { - collectInheritedFields(classDef.attrTypC(), fields, Collections.newSetFromMap(new IdentityHashMap<>())); + collectInheritedFields(classDef.attrTypC(), fields, + Collections.newSetFromMap(new IdentityHashMap<>()), + Collections.newSetFromMap(new IdentityHashMap<>())); } else if (owner instanceof ModuleDef moduleDef) { addInstanceFields(moduleDef.getVars(), fields); } @@ -148,17 +196,32 @@ private List collectInstanceFields(ClassDef classDef, ClassOrModul } private void collectInheritedFields(WurstTypeClass type, - List fields, Set visited) { - if (!visited.add(type.getClassDef())) { + List fields, + Set visitedClasses, + Set visitedModules) { + if (!visitedClasses.add(type.getClassDef())) { return; } WurstTypeClass superType = type.extendedClass(); if (superType != null) { - collectInheritedFields(superType, fields, visited); + collectInheritedFields(superType, fields, visitedClasses, visitedModules); } + addModuleFields(type.getClassDef().getModuleInstanciations(), fields, visitedModules); addInstanceFields(type.getClassDef().getVars(), fields); } + private void addModuleFields(Iterable modules, + List fields, + Set visited) { + for (ModuleInstanciation module : modules) { + if (!visited.add(module)) { + continue; + } + addModuleFields(module.getModuleInstanciations(), fields, visited); + addInstanceFields(module.getVars(), fields); + } + } + private void addInstanceFields(Iterable declarations, List fields) { for (GlobalVarDef field : declarations) { if (!field.attrIsStatic()) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstChecker.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstChecker.java index 41be15bca..c8fe3a40a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstChecker.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstChecker.java @@ -10,7 +10,9 @@ import de.peeeq.wurstscript.validation.TRVEHelper; import de.peeeq.wurstscript.validation.WurstValidator; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; public class WurstChecker { @@ -47,19 +49,24 @@ public void checkProg(WurstModel root, Collection toCheck) { if (errorHandler.getErrorCount() > 0) return; + SyntacticSugar syntacticSugar = new SyntacticSugar(); + List detachedTemplates = new ArrayList<>(); for (CompilationUnit cu : toCheck) { - new SyntacticSugar().expandFieldIterations(cu); + syntacticSugar.expandFieldIterations(cu); + detachedTemplates.addAll(syntacticSugar.detachModuleTemplateFieldIterations(cu)); } - - // compute the flow attributes - for (CompilationUnit cu : toCheck) { - WurstValidator.computeFlowAttributes(cu); + try { + // compute the flow attributes + for (CompilationUnit cu : toCheck) { + WurstValidator.computeFlowAttributes(cu); + } + + // validate the resource: + WurstValidator validator = new WurstValidator(root, legacyJassTypeChecks); + validator.validate(toCheck); + } finally { + syntacticSugar.restoreModuleTemplateFieldIterations(detachedTemplates); } - - - // validate the resource: - WurstValidator validator = new WurstValidator(root, legacyJassTypeChecks); - validator.validate(toCheck); } private void clearGlobalCaches(WurstModel root, Collection toCheck) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index 3f1a215de..8a37c9a9e 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -336,4 +336,37 @@ public void expandsFieldIterationInModuleMethodsAfterInstantiation() { "endpackage" ); } + + @Test + public void includesInstanceFieldsFromModules() { + test() + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " class Acc", + " int total = 0", + " function add(int value)", + " total = total + value", + " module BaseState", + " int baseValue = 2", + " module State", + " use BaseState", + " int moduleValue = 4", + " class Data", + " use State", + " int local = 5", + "", + " function save(Acc acc)", + " __wurst_forFields((name, value) -> acc.add(value))", + "", + " init", + " let acc = new Acc", + " let data = new Data", + " data.save(acc)", + " if acc.total == 11", + " testSuccess()", + "endpackage" + ); + } } From c57b3edb32d4bb6a11afdc31f6bea0bce23898c3 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 8 Aug 2026 15:33:16 +0200 Subject: [PATCH 10/12] Harden module field iteration expansion --- .../de/peeeq/wurstscript/SyntacticSugar.java | 30 ++++++++++++------- .../tests/FieldIterationTests.java | 4 ++- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java index 58111e145..889a32ebe 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java @@ -70,11 +70,14 @@ public void visit(ExprFunctionCall call) { if (isUninstantiatedModuleFieldIteration(call) && call.getParent() instanceof WStatements statements) { int index = statements.indexOf(call); - statements.remove(index); detached.add(new DeferredModuleCall(statements, index, call)); } } }); + for (int i = detached.size() - 1; i >= 0; i--) { + DeferredModuleCall state = detached.get(i); + state.statements.remove(state.index); + } return detached; } @@ -186,17 +189,18 @@ private void expandFieldIteration(ExprFunctionCall call, boolean assignsResult) private List collectInstanceFields(ClassDef classDef, ClassOrModule owner) { List fields = new ArrayList<>(); if (classDef != null) { - collectInheritedFields(classDef.attrTypC(), fields, + collectInheritedFields(classDef.attrTypC(), fields, classDef, Collections.newSetFromMap(new IdentityHashMap<>()), Collections.newSetFromMap(new IdentityHashMap<>())); } else if (owner instanceof ModuleDef moduleDef) { - addInstanceFields(moduleDef.getVars(), fields); + addInstanceFields(moduleDef.getVars(), fields, null); } return fields; } private void collectInheritedFields(WurstTypeClass type, List fields, + ClassDef concreteClass, Set visitedClasses, Set visitedModules) { if (!visitedClasses.add(type.getClassDef())) { @@ -204,27 +208,33 @@ private void collectInheritedFields(WurstTypeClass type, } WurstTypeClass superType = type.extendedClass(); if (superType != null) { - collectInheritedFields(superType, fields, visitedClasses, visitedModules); + collectInheritedFields(superType, fields, concreteClass, visitedClasses, visitedModules); } - addModuleFields(type.getClassDef().getModuleInstanciations(), fields, visitedModules); - addInstanceFields(type.getClassDef().getVars(), fields); + addModuleFields(type.getClassDef().getModuleInstanciations(), fields, concreteClass, visitedModules); + addInstanceFields(type.getClassDef().getVars(), fields, concreteClass); } private void addModuleFields(Iterable modules, List fields, + ClassDef concreteClass, Set visited) { for (ModuleInstanciation module : modules) { if (!visited.add(module)) { continue; } - addModuleFields(module.getModuleInstanciations(), fields, visited); - addInstanceFields(module.getVars(), fields); + addModuleFields(module.getModuleInstanciations(), fields, concreteClass, visited); + addInstanceFields(module.getVars(), fields, concreteClass); } } - private void addInstanceFields(Iterable declarations, List fields) { + private void addInstanceFields(Iterable declarations, + List fields, + ClassDef concreteClass) { for (GlobalVarDef field : declarations) { - if (!field.attrIsStatic()) { + boolean privateFromAnotherClass = field.attrIsPrivate() + && concreteClass != null + && field.attrNearestClassDef() != concreteClass; + if (!field.attrIsStatic() && !privateFromAnotherClass) { fields.add(field); } } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index 8a37c9a9e..2be28c573 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -289,6 +289,7 @@ public void includesInheritedInstanceFields() { " function add(int value)", " total = total + value", " class Base", + " private int hidden = 100", " int inherited = 1", " class Data extends Base", " int local = 2", @@ -320,6 +321,7 @@ public void expandsFieldIterationInModuleMethodsAfterInstantiation() { " module Serializer", " function save(Acc acc)", " __wurst_forFields((name, value) -> acc.add(value))", + " __wurst_forFields((name, value) -> acc.add(value))", "", " class Data", " use Serializer", @@ -331,7 +333,7 @@ public void expandsFieldIterationInModuleMethodsAfterInstantiation() { " let acc = new Acc", " let data = new Data", " data.save(acc)", - " if acc.total == 3", + " if acc.total == 6", " testSuccess()", "endpackage" ); From 82a0a735b57f64d9f0a9b7b2ae9938c7bfc3f414 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 8 Aug 2026 16:04:54 +0200 Subject: [PATCH 11/12] Harden field iteration rechecks and module paths --- .../languageserver/ModelManagerImpl.java | 4 + .../de/peeeq/wurstscript/SyntacticSugar.java | 126 ++++++++++++++---- .../tests/FieldIterationTests.java | 36 +++++ .../wurstscript/tests/ModelManagerTests.java | 60 +++++++++ 4 files changed, 200 insertions(+), 26 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java index c7b21f278..da7d3cbd1 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java @@ -7,6 +7,7 @@ import de.peeeq.wurstio.WurstCompilerJassImpl; import de.peeeq.wurstio.utils.FileUtils; import de.peeeq.wurstscript.RunArgs; +import de.peeeq.wurstscript.SyntacticSugar; import de.peeeq.wurstscript.WLogger; import de.peeeq.wurstscript.ast.*; import de.peeeq.wurstscript.attributes.CompileError; @@ -97,6 +98,7 @@ public Changes removeCompilationUnit(WFile resource) { } } GlobalCaches.clearLookupCacheFor(toRemove); + toRemove.forEach(SyntacticSugar::restoreDirectFieldIterations); model2.removeAll(toRemove); } @@ -117,6 +119,7 @@ public Changes removeCompilationUnit(WFile resource) { @Override public void clean() { + SyntacticSugar.clearDirectFieldIterations(); fileHashcodes.clear(); parseErrors.clear(); model = null; @@ -267,6 +270,7 @@ private void clearCompilationUnits(Collection toCheck) { } private void clearCompilationUnit(CompilationUnit cu) { + SyntacticSugar.restoreDirectFieldIterations(cu); cu.clearAttributes(); // clear module instantiations for (WPackage p : cu.getPackages()) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java index 889a32ebe..1eecee0d8 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java @@ -18,16 +18,42 @@ public class SyntacticSugar { /** Compiler-reserved spelling keeps ordinary user functions named forFields/mapFields valid. */ private static final String FOR_FIELDS = "__wurst_forFields"; private static final String MAP_FIELDS = "__wurst_mapFields"; + private static final Map> DETACHED_DIRECT_CALLS = + Collections.synchronizedMap(new WeakHashMap<>()); public static final class DeferredModuleCall { private final WStatements statements; private final int index; private final ExprFunctionCall call; + private final List generatedStatements; private DeferredModuleCall(WStatements statements, int index, ExprFunctionCall call) { + this(statements, index, call, List.of()); + } + + private DeferredModuleCall(WStatements statements, int index, ExprFunctionCall call, + List generatedStatements) { this.statements = statements; this.index = index; this.call = call; + this.generatedStatements = generatedStatements; + } + } + + private static final class FieldInfo { + private final GlobalVarDef declaration; + private final List modulePath; + + private FieldInfo(GlobalVarDef declaration, List modulePath) { + this.declaration = declaration; + this.modulePath = List.copyOf(modulePath); + } + + private String key() { + if (modulePath.isEmpty()) { + return declaration.getName(); + } + return String.join(".", modulePath) + "." + declaration.getName(); } } @@ -57,7 +83,23 @@ public void removeSyntacticSugar(CompilationUnit root, boolean hasCommonJ) { * see all fields of the concrete class using it. */ public void expandFieldIterations(CompilationUnit root) { - expandFieldIterationsInTree(root); + List detached = expandFieldIterationsInTree(root); + if (!detached.isEmpty()) { + DETACHED_DIRECT_CALLS.put(root, detached); + } + } + + /** Restores source intrinsics before an incremental compilation-unit recheck. */ + public static void restoreDirectFieldIterations(CompilationUnit root) { + List detached = DETACHED_DIRECT_CALLS.remove(root); + if (detached != null) { + new SyntacticSugar().restoreModuleTemplateFieldIterations(detached); + } + } + + /** Drops saved source intrinsics when the whole language-server model is discarded. */ + public static void clearDirectFieldIterations() { + DETACHED_DIRECT_CALLS.clear(); } /** Temporarily removes template intrinsics while validation runs; callers must restore them. */ @@ -84,6 +126,9 @@ public void visit(ExprFunctionCall call) { public void restoreModuleTemplateFieldIterations(List detached) { for (int i = detached.size() - 1; i >= 0; i--) { DeferredModuleCall state = detached.get(i); + for (WStatement generated : state.generatedStatements) { + state.statements.remove(generated); + } int index = Math.min(state.index, state.statements.size()); state.statements.add(index, state.call); } @@ -99,7 +144,7 @@ public void restoreModuleTemplateFieldIterations(List detach * __wurst_mapFields((name, value) -> reader.read(name, value)) * */ - private void expandFieldIterationsInTree(CompilationUnit root) { + private List expandFieldIterationsInTree(CompilationUnit root) { List calls = new ArrayList<>(); root.accept(new WurstModel.DefaultVisitor() { @Override @@ -111,12 +156,16 @@ public void visit(ExprFunctionCall call) { } }); + List detached = new ArrayList<>(); for (ExprFunctionCall call : calls) { - expandFieldIteration(call, MAP_FIELDS.equals(call.getFuncName())); + expandFieldIteration(call, MAP_FIELDS.equals(call.getFuncName()), detached); } + return detached; } - private void expandFieldIteration(ExprFunctionCall call, boolean assignsResult) { + private void expandFieldIteration(ExprFunctionCall call, + boolean assignsResult, + List detached) { if (!(call.getParent() instanceof WStatements statements)) { call.addError(call.getFuncName() + " can only be used as a statement."); return; @@ -164,42 +213,48 @@ private void expandFieldIteration(ExprFunctionCall call, boolean assignsResult) call.addError("forFields closure must produce a statement expression."); return; } - List fields = collectInstanceFields(classDef, owner); + List fields = collectInstanceFields(classDef, owner); if (fields.isEmpty()) { call.addError(call.getFuncName() + " requires at least one instance field."); return; } int statementIndex = statements.indexOf(call); + detached.add(new DeferredModuleCall(statements, statementIndex, call)); statements.remove(statementIndex); - for (GlobalVarDef field : fields) { - Expr fieldAccess = fieldAccess(call.getSource(), field.getName()); + List generatedStatements = new ArrayList<>(fields.size()); + for (FieldInfo field : fields) { + String fieldKey = field.key(); + Expr fieldAccess = fieldAccess(call.getSource(), field); Expr implementation = substituteFieldParameters( - closure.getImplementation().copy(), nameParameter, valueParameter, field.getName()); + closure.getImplementation().copy(), nameParameter, valueParameter, fieldKey, field); WStatement expanded; if (assignsResult) { expanded = Ast.StmtSet(call.getSource(), (LExpr) fieldAccess, implementation); } else { expanded = (WStatement) implementation; } + generatedStatements.add(expanded); statements.add(statementIndex++, expanded); } + detached.set(detached.size() - 1, + new DeferredModuleCall(statements, statementIndex - fields.size(), call, generatedStatements)); } - private List collectInstanceFields(ClassDef classDef, ClassOrModule owner) { - List fields = new ArrayList<>(); + private List collectInstanceFields(ClassDef classDef, ClassOrModule owner) { + List fields = new ArrayList<>(); if (classDef != null) { collectInheritedFields(classDef.attrTypC(), fields, classDef, Collections.newSetFromMap(new IdentityHashMap<>()), Collections.newSetFromMap(new IdentityHashMap<>())); } else if (owner instanceof ModuleDef moduleDef) { - addInstanceFields(moduleDef.getVars(), fields, null); + addInstanceFields(moduleDef.getVars(), fields, null, List.of()); } return fields; } private void collectInheritedFields(WurstTypeClass type, - List fields, + List fields, ClassDef concreteClass, Set visitedClasses, Set visitedModules) { @@ -210,32 +265,40 @@ private void collectInheritedFields(WurstTypeClass type, if (superType != null) { collectInheritedFields(superType, fields, concreteClass, visitedClasses, visitedModules); } - addModuleFields(type.getClassDef().getModuleInstanciations(), fields, concreteClass, visitedModules); - addInstanceFields(type.getClassDef().getVars(), fields, concreteClass); + addModuleFields(type.getClassDef().getModuleInstanciations(), fields, concreteClass, + visitedModules, List.of()); + addInstanceFields(type.getClassDef().getVars(), fields, concreteClass, List.of()); } private void addModuleFields(Iterable modules, - List fields, + List fields, ClassDef concreteClass, - Set visited) { + Set visited, + List parentPath) { for (ModuleInstanciation module : modules) { if (!visited.add(module)) { continue; } - addModuleFields(module.getModuleInstanciations(), fields, concreteClass, visited); - addInstanceFields(module.getVars(), fields, concreteClass); + // A nested module's fields are exposed through the outer module instance (the + // inner instance is not a member receiver in the consuming class). + List modulePath = parentPath.isEmpty() + ? List.of(module.getName()) + : parentPath; + addModuleFields(module.getModuleInstanciations(), fields, concreteClass, visited, modulePath); + addInstanceFields(module.getVars(), fields, concreteClass, modulePath); } } private void addInstanceFields(Iterable declarations, - List fields, - ClassDef concreteClass) { + List fields, + ClassDef concreteClass, + List modulePath) { for (GlobalVarDef field : declarations) { boolean privateFromAnotherClass = field.attrIsPrivate() && concreteClass != null && field.attrNearestClassDef() != concreteClass; if (!field.attrIsStatic() && !privateFromAnotherClass) { - fields.add(field); + fields.add(new FieldInfo(field, modulePath)); } } } @@ -260,13 +323,13 @@ public void visit(LocalVarDef localVarDef) { } private Expr substituteFieldParameters(Expr expression, String nameParameter, - String valueParameter, String fieldName) { + String valueParameter, String fieldName, FieldInfo field) { if (expression instanceof ExprVarAccess access) { if (access.getVarName().equals(nameParameter)) { return Ast.ExprStringVal(access.getSource(), fieldName); } if (access.getVarName().equals(valueParameter)) { - return fieldAccess(access.getSource(), fieldName); + return fieldAccess(access.getSource(), field); } } @@ -368,14 +431,25 @@ public void visit(ExprVarAccess access) { for (ExprVarAccess access : accesses) { Expr replacement = access.getVarName().equals(nameParameter) ? Ast.ExprStringVal(access.getSource(), fieldName) - : fieldAccess(access.getSource(), fieldName); + : fieldAccess(access.getSource(), field); access.replaceBy(replacement); } return expression; } - private ExprMemberVarDot fieldAccess(WPos source, String fieldName) { - return Ast.ExprMemberVarDot(source, Ast.ExprThis(source), Ast.Identifier(source, fieldName)); + private ExprMemberVarDot fieldAccess(WPos source, FieldInfo field) { + Expr left; + if (field.modulePath.isEmpty()) { + left = Ast.ExprThis(source); + } else { + left = Ast.ExprVarAccess(source, Ast.Identifier(source, field.modulePath.get(0))); + for (int i = 1; i < field.modulePath.size(); i++) { + left = Ast.ExprMemberVarDot(source, left, + Ast.Identifier(source, field.modulePath.get(i))); + } + } + return Ast.ExprMemberVarDot(source, left, + Ast.Identifier(source, field.declaration.getName())); } private void replaceTypeIdUse(CompilationUnit root) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index 2be28c573..efed46107 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -371,4 +371,40 @@ public void includesInstanceFieldsFromModules() { "endpackage" ); } + + @Test + public void qualifiesFieldsFromSiblingModules() { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " class Acc", + " int left", + " int right", + " function add(string name, int value)", + " if name == \"Left.x\"", + " left = value", + " if name == \"Right.x\"", + " right = value", + " module Left", + " int x = 1", + " module Right", + " int x = 2", + " class Data", + " use Left", + " use Right", + " function save(Acc acc)", + " __wurst_forFields((name, value) -> acc.add(name, value))", + " init", + " let acc = new Acc", + " let data = new Data", + " data.save(acc)", + " if acc.left == 1 and acc.right == 2", + " testSuccess()", + "endpackage" + ); + } } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java index 6e67ab622..97418849f 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java @@ -124,6 +124,66 @@ public void test() throws IOException, InterruptedException { } + @Test + public void incrementalRecheckRestoresFieldIterationIntrinsic() throws IOException { + File projectFolder = new File("./temp/testProject_field_iteration_incremental/"); + File wurstFolder = new File(projectFolder, "wurst"); + newCleanFolder(wurstFolder); + + WFile fileBase = WFile.create(new File(wurstFolder, "Base.wurst")); + WFile fileData = WFile.create(new File(wurstFolder, "Data.wurst")); + WFile fileWurst = WFile.create(new File(wurstFolder, "Wurst.wurst")); + writeFile(fileBase, string( + "package Base", + "public class Base", + " int oldValue = 1" + )); + writeFile(fileData, string( + "package Data", + "import Base", + "native consume(string name, int value)", + "class Data extends Base", + " function save()", + " __wurst_forFields((name, value) -> consume(name, value))" + )); + writeFile(fileWurst, "package Wurst\n"); + + ModelManagerImpl manager = new ModelManagerImpl(projectFolder, new BufferManager()); + manager.buildProject(); + + ClassDef data = findClass(manager.getCompilationUnit(fileData), "Data"); + FuncDef save = findFunction(data, "save"); + int initialBodySize = save.getBody().size(); + + manager.reconcile(manager.syncCompilationUnitContent(fileBase, string( + "package Base", + "public class Base", + " int oldValue = 1", + " int newValue = 2" + ))); + + data = findClass(manager.getCompilationUnit(fileData), "Data"); + save = findFunction(data, "save"); + assertEquals(save.getBody().size(), initialBodySize + 1); + } + + private ClassDef findClass(CompilationUnit cu, String name) { + return cu.getPackages().stream() + .flatMap(p -> p.getElements().stream()) + .filter(e -> e instanceof ClassDef && name.equals(((ClassDef) e).getName())) + .map(e -> (ClassDef) e) + .findFirst() + .orElseThrow(); + } + + private FuncDef findFunction(ClassDef clazz, String name) { + return clazz.getMethods().stream() + .filter(f -> name.equals(f.getName())) + .map(f -> (FuncDef) f) + .findFirst() + .orElseThrow(); + } + @Test public void movingFiles() throws IOException { // #712 File projectFolder = new File("./temp/testProject2/"); From 70ddd1b0317c151f9e468496bc5aeac5a0e8cca2 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 8 Aug 2026 16:37:23 +0200 Subject: [PATCH 12/12] Validate module templates and private fields --- .../languageserver/ModelManagerImpl.java | 1 - .../de/peeeq/wurstscript/SyntacticSugar.java | 67 ++++++++++--------- .../attributes/CompilationUnitInfo.java | 10 +++ .../tests/FieldIterationTests.java | 41 ++++++++++++ 4 files changed, 87 insertions(+), 32 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java index da7d3cbd1..b3dec05b1 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java @@ -119,7 +119,6 @@ public Changes removeCompilationUnit(WFile resource) { @Override public void clean() { - SyntacticSugar.clearDirectFieldIterations(); fileHashcodes.clear(); parseErrors.clear(); model = null; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java index 1eecee0d8..63289239c 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java @@ -18,9 +18,6 @@ public class SyntacticSugar { /** Compiler-reserved spelling keeps ordinary user functions named forFields/mapFields valid. */ private static final String FOR_FIELDS = "__wurst_forFields"; private static final String MAP_FIELDS = "__wurst_mapFields"; - private static final Map> DETACHED_DIRECT_CALLS = - Collections.synchronizedMap(new WeakHashMap<>()); - public static final class DeferredModuleCall { private final WStatements statements; private final int index; @@ -40,6 +37,14 @@ private DeferredModuleCall(WStatements statements, int index, ExprFunctionCall c } } + public static final class DirectFieldIterationState { + private final List detached; + + private DirectFieldIterationState(List detached) { + this.detached = detached; + } + } + private static final class FieldInfo { private final GlobalVarDef declaration; private final List modulePath; @@ -84,24 +89,22 @@ public void removeSyntacticSugar(CompilationUnit root, boolean hasCommonJ) { */ public void expandFieldIterations(CompilationUnit root) { List detached = expandFieldIterationsInTree(root); - if (!detached.isEmpty()) { - DETACHED_DIRECT_CALLS.put(root, detached); + if (!detached.isEmpty() && root.getCuInfo() != null) { + root.getCuInfo().setDirectFieldIterationState(new DirectFieldIterationState(detached)); } } /** Restores source intrinsics before an incremental compilation-unit recheck. */ public static void restoreDirectFieldIterations(CompilationUnit root) { - List detached = DETACHED_DIRECT_CALLS.remove(root); - if (detached != null) { - new SyntacticSugar().restoreModuleTemplateFieldIterations(detached); + if (root.getCuInfo() != null) { + DirectFieldIterationState state = root.getCuInfo().getDirectFieldIterationState(); + root.getCuInfo().setDirectFieldIterationState(null); + if (state != null) { + new SyntacticSugar().restoreModuleTemplateFieldIterations(state.detached); + } } } - /** Drops saved source intrinsics when the whole language-server model is discarded. */ - public static void clearDirectFieldIterations() { - DETACHED_DIRECT_CALLS.clear(); - } - /** Temporarily removes template intrinsics while validation runs; callers must restore them. */ public List detachModuleTemplateFieldIterations(CompilationUnit root) { List detached = new ArrayList<>(); @@ -172,19 +175,6 @@ private void expandFieldIteration(ExprFunctionCall call, } ClassDef classDef = call.attrNearestClassDef(); ClassOrModule owner = call.attrNearestClassOrModule(); - if (!call.attrIsDynamicContext()) { - call.addError(call.getFuncName() + " can only be used in an instance method or constructor."); - return; - } - if (owner instanceof ModuleDef) { - // Module bodies are templates. Their copies were made by ModuleExpander; validate and - // expand those concrete copies instead of type-checking this uninstantiated template. - return; - } - if (classDef == null) { - call.addError(call.getFuncName() + " can only be used in an instance method or constructor."); - return; - } if (call.getArgs().size() != 1 || !(call.getArgs().get(0) instanceof ExprClosure closure) || closure.getShortParameters().size() != 2) { call.addError(call.getFuncName() + " expects a closure with (fieldName, fieldValue) parameters."); @@ -213,6 +203,19 @@ private void expandFieldIteration(ExprFunctionCall call, call.addError("forFields closure must produce a statement expression."); return; } + if (!call.attrIsDynamicContext()) { + call.addError(call.getFuncName() + " can only be used in an instance method or constructor."); + return; + } + if (owner instanceof ModuleDef) { + // Module bodies are templates. Their copies were made by ModuleExpander; validate and + // expand those concrete copies instead of type-checking this uninstantiated template. + return; + } + if (classDef == null) { + call.addError(call.getFuncName() + " can only be used in an instance method or constructor."); + return; + } List fields = collectInstanceFields(classDef, owner); if (fields.isEmpty()) { call.addError(call.getFuncName() + " requires at least one instance field."); @@ -248,7 +251,7 @@ private List collectInstanceFields(ClassDef classDef, ClassOrModule o Collections.newSetFromMap(new IdentityHashMap<>()), Collections.newSetFromMap(new IdentityHashMap<>())); } else if (owner instanceof ModuleDef moduleDef) { - addInstanceFields(moduleDef.getVars(), fields, null, List.of()); + addInstanceFields(moduleDef.getVars(), fields, null, List.of(), moduleDef); } return fields; } @@ -267,7 +270,7 @@ private void collectInheritedFields(WurstTypeClass type, } addModuleFields(type.getClassDef().getModuleInstanciations(), fields, concreteClass, visitedModules, List.of()); - addInstanceFields(type.getClassDef().getVars(), fields, concreteClass, List.of()); + addInstanceFields(type.getClassDef().getVars(), fields, concreteClass, List.of(), null); } private void addModuleFields(Iterable modules, @@ -285,19 +288,21 @@ private void addModuleFields(Iterable modules, ? List.of(module.getName()) : parentPath; addModuleFields(module.getModuleInstanciations(), fields, concreteClass, visited, modulePath); - addInstanceFields(module.getVars(), fields, concreteClass, modulePath); + addInstanceFields(module.getVars(), fields, concreteClass, modulePath, module.attrModuleOrigin()); } } private void addInstanceFields(Iterable declarations, List fields, ClassDef concreteClass, - List modulePath) { + List modulePath, + ModuleDef declaringModule) { for (GlobalVarDef field : declarations) { boolean privateFromAnotherClass = field.attrIsPrivate() && concreteClass != null && field.attrNearestClassDef() != concreteClass; - if (!field.attrIsStatic() && !privateFromAnotherClass) { + boolean privateFromAnotherModule = field.attrIsPrivate() && declaringModule != null; + if (!field.attrIsStatic() && !privateFromAnotherClass && !privateFromAnotherModule) { fields.add(new FieldInfo(field, modulePath)); } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/CompilationUnitInfo.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/CompilationUnitInfo.java index 303b4ee8a..c52ab449b 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/CompilationUnitInfo.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/CompilationUnitInfo.java @@ -1,5 +1,6 @@ package de.peeeq.wurstscript.attributes; +import de.peeeq.wurstscript.SyntacticSugar; import de.peeeq.wurstscript.parser.TriviaIndex; import de.peeeq.wurstscript.utils.Utils; @@ -12,6 +13,7 @@ public class CompilationUnitInfo { private IndentationMode indentationMode = IndentationMode.spaces(4); private TriviaIndex triviaIndex = TriviaIndex.empty(); private boolean library; + private SyntacticSugar.DirectFieldIterationState directFieldIterationState; public CompilationUnitInfo(ErrorHandler cuErrorHandler) { this.cuErrorHandler = cuErrorHandler; @@ -57,6 +59,14 @@ public void setLibrary(boolean library) { this.library = library; } + public SyntacticSugar.DirectFieldIterationState getDirectFieldIterationState() { + return directFieldIterationState; + } + + public void setDirectFieldIterationState(SyntacticSugar.DirectFieldIterationState state) { + this.directFieldIterationState = state; + } + public interface IndentationMode { static IndentationMode tabs() { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index efed46107..bebe3e181 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -407,4 +407,45 @@ public void qualifiesFieldsFromSiblingModules() { "endpackage" ); } + + @Test + public void excludesPrivateModuleFields() { + test() + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " class Acc", + " int total", + " function add(int value)", + " total = total + value", + " module State", + " private int hidden = 100", + " int visible = 2", + " class Data", + " use State", + " function save(Acc acc)", + " __wurst_forFields((name, value) -> acc.add(value))", + " init", + " let acc = new Acc", + " let data = new Data", + " data.save(acc)", + " if acc.total == 2", + " testSuccess()", + "endpackage" + ); + } + + @Test + public void validatesUnusedModuleFieldIteration() { + test() + .expectError("expects a closure with (fieldName, fieldValue) parameters") + .lines( + "package FieldIterationTest", + " module Unused", + " function save()", + " __wurst_forFields(value -> value)", + "endpackage" + ); + } }