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..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 @@ -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); } @@ -267,6 +269,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 32b3c98b7..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 @@ -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.*; @@ -14,6 +15,63 @@ */ 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 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; + } + } + + 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; + + 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(); + } + } + + 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); @@ -24,6 +82,381 @@ public void removeSyntacticSugar(CompilationUnit root, boolean hasCommonJ) { 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) { + List detached = expandFieldIterationsInTree(root); + 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) { + if (root.getCuInfo() != null) { + DirectFieldIterationState state = root.getCuInfo().getDirectFieldIterationState(); + root.getCuInfo().setDirectFieldIterationState(null); + if (state != null) { + new SyntacticSugar().restoreModuleTemplateFieldIterations(state.detached); + } + } + } + + /** 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); + 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; + } + + 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); + } + } + + /** + * 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. + * + *
+     * __wurst_forFields((name, value) -> writer.write(name, value))
+     * __wurst_mapFields((name, value) -> reader.read(name, value))
+     * 
+ */ + private List expandFieldIterationsInTree(CompilationUnit root) { + List calls = new ArrayList<>(); + root.accept(new WurstModel.DefaultVisitor() { + @Override + public void visit(ExprFunctionCall call) { + super.visit(call); + if (isFieldIterationIntrinsic(call)) { + calls.add(call); + } + } + }); + + List detached = new ArrayList<>(); + for (ExprFunctionCall call : calls) { + expandFieldIteration(call, MAP_FIELDS.equals(call.getFuncName()), detached); + } + return detached; + } + + 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; + } + ClassDef classDef = call.attrNearestClassDef(); + ClassOrModule owner = call.attrNearestClassOrModule(); + 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; + } + 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(); + if (nameParameter.equals(valueParameter)) { + closure.getShortParameters().get(1).addError( + "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; + } + 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."); + return; + } + int statementIndex = statements.indexOf(call); + detached.add(new DeferredModuleCall(statements, statementIndex, call)); + statements.remove(statementIndex); + + 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, 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<>(); + 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, List.of(), moduleDef); + } + return fields; + } + + private void collectInheritedFields(WurstTypeClass type, + List fields, + ClassDef concreteClass, + Set visitedClasses, + Set visitedModules) { + if (!visitedClasses.add(type.getClassDef())) { + return; + } + WurstTypeClass superType = type.extendedClass(); + if (superType != null) { + collectInheritedFields(superType, fields, concreteClass, visitedClasses, visitedModules); + } + addModuleFields(type.getClassDef().getModuleInstanciations(), fields, concreteClass, + visitedModules, List.of()); + addInstanceFields(type.getClassDef().getVars(), fields, concreteClass, List.of(), null); + } + + private void addModuleFields(Iterable modules, + List fields, + ClassDef concreteClass, + Set visited, + List parentPath) { + for (ModuleInstanciation module : modules) { + if (!visited.add(module)) { + continue; + } + // 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, module.attrModuleOrigin()); + } + } + + private void addInstanceFields(Iterable declarations, + List fields, + ClassDef concreteClass, + List modulePath, + ModuleDef declaringModule) { + for (GlobalVarDef field : declarations) { + boolean privateFromAnotherClass = field.attrIsPrivate() + && concreteClass != null + && field.attrNearestClassDef() != concreteClass; + boolean privateFromAnotherModule = field.attrIsPrivate() && declaringModule != null; + if (!field.attrIsStatic() && !privateFromAnotherClass && !privateFromAnotherModule) { + fields.add(new FieldInfo(field, modulePath)); + } + } + } + + 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, 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(), field); + } + } + + List accesses = new ArrayList<>(); + expression.accept(new WurstModel.DefaultVisitor() { + 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) { + 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(); + } + + 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<>(); + for (WShortParameter parameter : nestedClosure.getShortParameters()) { + shadowed.add(parameter.getName()); + } + shadowedScopes.push(shadowed); + super.visit(nestedClosure); + 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 (!isShadowed(access.getVarName()) + && (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(), field); + access.replaceBy(replacement); + } + return expression; + } + + 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) { final Map replacements = Maps.newLinkedHashMap(); root.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..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,15 +49,24 @@ public void checkProg(WurstModel root, Collection toCheck) { if (errorHandler.getErrorCount() > 0) return; - // compute the flow attributes + SyntacticSugar syntacticSugar = new SyntacticSugar(); + List detachedTemplates = new ArrayList<>(); for (CompilationUnit cu : toCheck) { - WurstValidator.computeFlowAttributes(cu); + syntacticSugar.expandFieldIterations(cu); + detachedTemplates.addAll(syntacticSugar.detachModuleTemplateFieldIterations(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/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 new file mode 100644 index 000000000..bebe3e181 --- /dev/null +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -0,0 +1,451 @@ +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) + .luaOnly(false) + .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)", + " __wurst_forFields((fieldName, value) -> codec.write(fieldName, value))", + "", + " function load(Codec codec)", + " __wurst_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("__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\"")); + 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", + " __wurst_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()", + " __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" + ); + } + + @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" + ); + } + + @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" + ); + } + + @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 temporary = 42", + " return temporary", + " end)", + "", + " init", + " let data = new Data", + " data.load()", + " if data.first == 42 and data.second == 42", + " testSuccess()", + "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 index = 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" + ); + } + + @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" + ); + } + + @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", + " private int hidden = 100", + " 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))", + " __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 == 6", + " testSuccess()", + "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" + ); + } + + @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" + ); + } + + @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" + ); + } +} 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/");