From ff48f039f0fef1ab6ea5b6df4832098fcff127f4 Mon Sep 17 00:00:00 2001 From: Frotty Date: Tue, 11 Aug 2026 11:32:46 +0200 Subject: [PATCH 1/5] Add composable serialization intrinsics --- .../peeeq/wurstscript/CompilerIntrinsics.java | 5 + .../de/peeeq/wurstscript/SyntacticSugar.java | 163 +++++++++++-- .../wurstscript/attributes/AttrFuncDef.java | 6 + .../validation/WurstValidator.java | 2 +- .../wurstscript/tests/AutoCompleteTests.java | 17 ++ .../tests/FieldIterationTests.java | 224 ++++++++++++++++++ .../wurstscript/tests/GetDefinitionTests.java | 19 ++ .../tests/wurstscript/tests/HoverTests.java | 23 ++ 8 files changed, 432 insertions(+), 27 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java index fe18491d1..861c62c3e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java @@ -11,6 +11,7 @@ public final class CompilerIntrinsics { public static final String MAP_FIELDS = "mapFields"; public static final String NEW = "newInstance"; public static final String NEW_MARKER = "wurstNewMarker"; + public static final String ANNOTATION = "compilerintrinsic"; private CompilerIntrinsics() { } @@ -38,4 +39,8 @@ public static boolean isNew(ExprFunctionCall call) { private static boolean hasClosureArgument(ExprFunctionCall call) { return call.getArgs().stream().anyMatch(arg -> arg instanceof ExprClosure); } + + public static boolean isDeclaration(de.peeeq.wurstscript.ast.FunctionDefinition definition) { + return definition.attrHasAnnotation(ANNOTATION); + } } 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 ace3e1c3b..8c405d937 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 @@ -5,6 +5,7 @@ import de.peeeq.wurstscript.parser.WPos; import de.peeeq.wurstscript.types.WurstType; import de.peeeq.wurstscript.types.WurstTypeClass; +import de.peeeq.wurstscript.types.WurstTypeTuple; import de.peeeq.wurstscript.types.WurstTypeTypeParam; import java.util.*; @@ -46,10 +47,10 @@ private DirectFieldIterationState(List detached) { } private static final class FieldInfo { - private final GlobalVarDef declaration; + private final VarDef declaration; private final List modulePath; - private FieldInfo(GlobalVarDef declaration, List modulePath) { + private FieldInfo(VarDef declaration, List modulePath) { this.declaration = declaration; this.modulePath = List.copyOf(modulePath); } @@ -178,9 +179,11 @@ private void expandFieldIteration(ExprFunctionCall call, int closureIndex = explicitTarget ? 1 : 0; if ((!explicitTarget && call.getArgs().size() != 1) || !(call.getArgs().get(closureIndex) instanceof ExprClosure closure) - || closure.getShortParameters().size() != 2) { + || (closure.getShortParameters().size() != 2 + && closure.getShortParameters().size() != 3)) { call.addError(call.getFuncName() - + " expects a closure with (fieldName, fieldValue) parameters, optionally preceded by a target."); + + " expects a closure with (fieldName, fieldValue) parameters or " + + "(fieldId, fieldName, fieldValue) parameters, optionally preceded by a target."); return; } for (WShortParameter parameter : closure.getShortParameters()) { @@ -190,23 +193,28 @@ private void expandFieldIteration(ExprFunctionCall call, } } - 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."); + boolean schemaAware = closure.getShortParameters().size() == 3; + String idParameter = schemaAware ? closure.getShortParameters().get(0).getName() : null; + String nameParameter = closure.getShortParameters().get(schemaAware ? 1 : 0).getName(); + String valueParameter = closure.getShortParameters().get(schemaAware ? 2 : 1).getName(); + Set callbackParameters = new LinkedHashSet<>(); + if (idParameter != null) callbackParameters.add(idParameter); + callbackParameters.add(nameParameter); + callbackParameters.add(valueParameter); + if (callbackParameters.size() != closure.getShortParameters().size()) { + closure.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 + "."); + if (hasShadowingLocal(closure, callbackParameters)) { + call.addError("Field iteration callbacks cannot declare locals or loop variables named like callback parameters."); return; } if (!assignsResult && !(closure.getImplementation() instanceof WStatement)) { call.addError("forFields closure must produce a statement expression."); return; } - ClassDef classDef; + ClassDef classDef = null; + TupleDef tupleDef = null; ClassOrModule owner; Expr target = null; String targetName = null; @@ -229,15 +237,31 @@ private void expandFieldIteration(ExprFunctionCall call, + " is not concrete here. Move field mapping into a callback with a concrete target type."); return; } - if (!(targetType instanceof WurstTypeClass targetClass) || targetClass.isStaticRef()) { + if (targetType instanceof WurstTypeTuple targetTuple) { + tupleDef = targetTuple.getTupleDef(); + owner = tupleDef.attrNearestClassOrModule(); + if (assignsResult) { + if (!(target instanceof ExprVarAccess targetAccess)) { + statements.remove(targetVariable); + statements.clearAttributes(); + call.addError("mapFields tuple target must be a variable so updates can be written back exactly once."); + return; + } + statements.remove(targetVariable); + statements.clearAttributes(); + targetVariable = null; + targetName = targetAccess.getVarName(); + } + } else if (targetType instanceof WurstTypeClass targetClass && !targetClass.isStaticRef()) { + classDef = targetClass.getClassDef(); + owner = classDef; + } else { statements.remove(targetVariable); statements.clearAttributes(); call.addError(call.getFuncName() + " target must have a concrete class type, but found " + targetType + "."); return; } - classDef = targetClass.getClassDef(); - owner = classDef; } else { classDef = call.attrNearestClassDef(); owner = call.attrNearestClassOrModule(); @@ -255,7 +279,9 @@ private void expandFieldIteration(ExprFunctionCall call, return; } } - List fields = collectInstanceFields(classDef, owner, call, explicitTarget, assignsResult); + List fields = tupleDef == null + ? collectInstanceFields(classDef, owner, call, explicitTarget, assignsResult) + : collectTupleFields(tupleDef); if (fields.isEmpty()) { if (targetVariable != null) { statements.remove(targetVariable); @@ -265,6 +291,14 @@ private void expandFieldIteration(ExprFunctionCall call, + " requires at least one instance field; no accessible mutable instance fields were found."); return; } + Map schemaIds = schemaAware ? validateSchemaIds(call, fields) : Map.of(); + if (schemaAware && schemaIds.size() != fields.size()) { + if (targetVariable != null) { + statements.remove(targetVariable); + statements.clearAttributes(); + } + return; + } int statementIndex = statements.indexOf(call); detached.add(new DeferredModuleCall(statements, originalStatementIndex, call)); statements.remove(statementIndex); @@ -277,7 +311,8 @@ private void expandFieldIteration(ExprFunctionCall call, String fieldKey = field.key(); Expr fieldAccess = fieldAccess(call.getSource(), field, targetName); Expr implementation = substituteFieldParameters( - closure.getImplementation().copy(), nameParameter, valueParameter, fieldKey, field, targetName); + closure.getImplementation().copy(), idParameter, nameParameter, valueParameter, + schemaAware ? schemaId(field) : null, fieldKey, field, targetName); WStatement expanded; if (assignsResult) { expanded = Ast.StmtSet(call.getSource(), (LExpr) fieldAccess, implementation); @@ -291,6 +326,73 @@ private void expandFieldIteration(ExprFunctionCall call, new DeferredModuleCall(statements, originalStatementIndex, call, generatedStatements)); } + private List collectTupleFields(TupleDef tupleDef) { + List fields = new ArrayList<>(); + for (WParameter parameter : tupleDef.getParameters()) { + fields.add(new FieldInfo(parameter, List.of())); + } + return fields; + } + + private Map validateSchemaIds(ExprFunctionCall call, List fields) { + Map result = new LinkedHashMap<>(); + for (FieldInfo field : fields) { + Integer id = schemaId(field); + if (id == null) { + field.declaration.addError("Schema-aware field iteration requires @saveField(id) on field " + + field.key() + "."); + continue; + } + if (id <= 0) { + field.declaration.addError("@saveField id must be a positive integer, but found " + id + "."); + continue; + } + FieldInfo previous = result.putIfAbsent(id, field); + if (previous != null) { + call.addError("Duplicate @saveField id " + id + " on fields " + + previous.key() + " and " + field.key() + "."); + } + } + return result; + } + + private Integer schemaId(FieldInfo field) { + if (!(field.declaration instanceof GlobalVarDef global)) { + Element parent = field.declaration.getParent(); + if (parent instanceof WParameters parameters) { + int index = parameters.indexOf(field.declaration); + if (parameters.getParent() instanceof TupleDef tuple) { + Annotation annotation = tuple.getAnnotation("@saveFields"); + if (annotation != null) { + if (annotation.getArgs().size() != parameters.size()) { + tuple.addError("@saveFields must provide exactly one integer id per tuple component."); + return null; + } + Expr argument = annotation.getArgs().get(index); + if (!(argument instanceof ExprIntVal value)) { + argument.addError("@saveFields ids must be integer literals."); + return null; + } + return value.getValI(); + } + } + // Tuple order is part of its structural type. Positional ids are the compact + // default; @saveFields opts into rename/reorder-stable persisted identities. + return index + 1; + } + return null; + } + Annotation annotation = global.getAnnotation("@saveField"); + if (annotation == null || annotation.getArgs().size() != 1 + || !(annotation.getArgs().get(0) instanceof ExprIntVal value)) { + if (annotation != null) { + global.addError("@saveField expects exactly one integer literal id."); + } + return null; + } + return value.getValI(); + } + private List collectInstanceFields(ClassDef classDef, ClassOrModule owner, Element accessSite, boolean explicitTarget, boolean requireMutable) { @@ -387,7 +489,7 @@ private boolean canAccessProtectedField(GlobalVarDef field, ClassDef declaringCl && accessClass.attrTypC().isSubtypeOf(declaringClass.attrTypC(), accessSite); } - private boolean hasShadowingLocal(ExprClosure closure, String nameParameter, String valueParameter) { + private boolean hasShadowingLocal(ExprClosure closure, Set callbackParameters) { final boolean[] result = {false}; closure.getImplementation().accept(new WurstModel.DefaultVisitor() { @Override @@ -398,7 +500,7 @@ public void visit(ExprClosure nestedClosure) { @Override public void visit(LocalVarDef localVarDef) { super.visit(localVarDef); - if (localVarDef.getName().equals(nameParameter) || localVarDef.getName().equals(valueParameter)) { + if (callbackParameters.contains(localVarDef.getName())) { result[0] = true; } } @@ -406,10 +508,13 @@ public void visit(LocalVarDef localVarDef) { return result[0]; } - private Expr substituteFieldParameters(Expr expression, String nameParameter, - String valueParameter, String fieldName, FieldInfo field, + private Expr substituteFieldParameters(Expr expression, String idParameter, String nameParameter, + String valueParameter, Integer fieldId, String fieldName, FieldInfo field, String targetName) { if (expression instanceof ExprVarAccess access) { + if (idParameter != null && access.getVarName().equals(idParameter)) { + return Ast.ExprIntVal(access.getSource(), Integer.toString(Objects.requireNonNull(fieldId))); + } if (access.getVarName().equals(nameParameter)) { return Ast.ExprStringVal(access.getSource(), fieldName); } @@ -508,15 +613,21 @@ public void visit(LocalVarDef localVarDef) { public void visit(ExprVarAccess access) { super.visit(access); if (!isShadowed(access.getVarName()) - && (access.getVarName().equals(nameParameter) || access.getVarName().equals(valueParameter))) { + && ((idParameter != null && access.getVarName().equals(idParameter)) + || 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, targetName); + Expr replacement; + if (idParameter != null && access.getVarName().equals(idParameter)) { + replacement = Ast.ExprIntVal(access.getSource(), Integer.toString(Objects.requireNonNull(fieldId))); + } else if (access.getVarName().equals(nameParameter)) { + replacement = Ast.ExprStringVal(access.getSource(), fieldName); + } else { + replacement = fieldAccess(access.getSource(), field, targetName); + } access.replaceBy(replacement); } return expression; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java index 4509b56e3..949ae7123 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java @@ -519,6 +519,12 @@ public static boolean hasApplicableUserFunction(ExprFunctionCall node) { } List argumentTypes = argumentTypesPre(node); for (FuncLink candidate : candidates) { + // A @compilerintrinsic declaration is an IDE-visible contract for an operation which + // is still lowered by the compiler. It must not shadow that lowering like an ordinary + // user function with the same name does. + if (CompilerIntrinsics.isDeclaration(candidate.getDef())) { + continue; + } if (candidate.getVisibility() == Visibility.PRIVATE_OTHER || candidate.getVisibility() == Visibility.PROTECTED_OTHER) { continue; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index 6a65f1544..e0f6fd748 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -2814,7 +2814,7 @@ public void case_InterfaceDef(InterfaceDef interfaceDef) { @Override public void case_TupleDef(TupleDef tupleDef) { - check(VisibilityPublic.class); + check(VisibilityPublic.class, Annotation.class); } @Override diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/AutoCompleteTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/AutoCompleteTests.java index f5e6faac5..532282e95 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/AutoCompleteTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/AutoCompleteTests.java @@ -68,6 +68,23 @@ public void simpleExample3() { testCompletions(testData, "foo"); } + @Test + public void compilerIntrinsicDeclarationIsDiscoverable() { + CompletionTestData testData = input( + "package test", + " @annotation function annotation()", + " @annotation function compilerintrinsic()", + " interface DocumentedFieldCallback", + " function apply(string name, int value)", + " @compilerintrinsic function forFields(DocumentedFieldCallback callback)", + " init", + " forFi|", + "endpackage" + ); + + testCompletions(testData, "forFields"); + } + @Test public void testWithParentheses() { 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 4e974c49c..c36c968cb 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 @@ -968,4 +968,228 @@ public void validatesUnusedModuleFieldIteration() { "endpackage" ); } + + @Test + public void compilerIntrinsicDeclarationsRemainLowered() throws IOException { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package MagicFunctions", + " @annotation function annotation()", + " @annotation function compilerintrinsic()", + " interface DocumentedFieldCallback", + " function apply(string fieldName, int value)", + " @compilerintrinsic function forFields(DocumentedFieldCallback callback)", + " @compilerintrinsic function newInstance() returns T", + " return null", + "endpackage", + "", + "package FieldIterationTest", + " import MagicFunctions", + " native testSuccess()", + " int total = 0", + " function add(int value)", + " total += value", + " class State", + " int value = 4", + " function save()", + " forFields((name, fieldValue) -> add(fieldValue))", + " init", + " State result = newInstance()", + " result.save()", + " if total == 4", + " testSuccess()", + "endpackage" + ); + + String lua = Files.readString(new File(TEST_OUTPUT_PATH + + "lua/FieldIterationTests_compilerIntrinsicDeclarationsRemainLowered.lua").toPath()); + String jass = Files.readString(new File(TEST_OUTPUT_PATH + + "FieldIterationTests_compilerIntrinsicDeclarationsRemainLowered_opt.j").toPath()); + for (String generated : new String[]{lua, jass}) { + assertFalse(generated.contains("newInstance")); + assertFalse(generated.contains("forFields")); + } + } + + @Test + public void schemaAwareIterationComposesAcrossSerializableFieldKinds() throws IOException { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package FieldIterationTest", + " @annotation function annotation()", + " @annotation function saveField(int id)", + " native testSuccess()", + "", + " tuple Pair(int left, string right)", + " enum Mode", + " A", + " B", + " class ArrayList", + " int size = 3", + " class Child", + " int value = 9", + " function encoded() returns int", + " return value", + " module Injected", + " @saveField(12) int injected = 2", + " class Base", + " @saveField(11) int inherited = 1", + " class State extends Base", + " use Injected", + " @saveField(20) int primitive = 4", + " @saveField(30) Child nested = new Child", + " @saveField(40) Pair pair = Pair(5, \"x\")", + " @saveField(50) ArrayList values = new ArrayList", + " @saveField(60) Mode mode = Mode.B", + " @saveField(70) Child optional = null", + "", + " class Probe", + " int mask = 0", + " function record(int id, string name, int value)", + " mask += id + value", + " function record(int id, string name, Child value)", + " if value == null", + " mask += id", + " else", + " mask += id + value.encoded()", + " function record(int id, string name, Pair value)", + " mask += id + value.left", + " function record(int id, string name, ArrayList value)", + " mask += id + value.size", + " function record(int id, string name, Mode value)", + " mask += id", + "", + " init", + " let state = new State", + " let probe = new Probe", + " forFields(state, (id, name, value) -> probe.record(id, name, value))", + " if probe.mask == 317", + " testSuccess()", + "endpackage" + ); + + String lua = Files.readString(new File(TEST_OUTPUT_PATH + + "lua/FieldIterationTests_schemaAwareIterationComposesAcrossSerializableFieldKinds.lua").toPath()); + String jass = Files.readString(new File(TEST_OUTPUT_PATH + + "FieldIterationTests_schemaAwareIterationComposesAcrossSerializableFieldKinds_opt.j").toPath()); + for (String generated : new String[]{lua, jass}) { + assertFalse(generated.contains("forFields")); + assertFalse(generated.contains("saveField")); + assertFalse(generated.contains("reflection")); + assertTrue(generated.contains("11") && generated.contains("inherited")); + } + } + + @Test + public void tupleTargetsUseStableComponentIdsAndDirectAccesses() throws IOException { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " @annotation function annotation()", + " @annotation function saveFields(vararg int ids)", + " @saveFields(7, 12, 99) tuple Payload(int count, string label, boolean enabled)", + " function map(int id, string name, int value) returns int", + " return value + id", + " function map(int id, string name, string value) returns string", + " return value + name", + " function map(int id, string name, boolean value) returns boolean", + " return not value", + " init", + " var payload = Payload(4, \"x\", false)", + " mapFields(payload, (id, name, value) -> map(id, name, value))", + " if payload.count == 11 and payload.label == \"xlabel\" and payload.enabled", + " testSuccess()", + "endpackage" + ); + + String lua = Files.readString(new File(TEST_OUTPUT_PATH + + "lua/FieldIterationTests_tupleTargetsUseStableComponentIdsAndDirectAccesses.lua").toPath()); + String jass = Files.readString(new File(TEST_OUTPUT_PATH + + "FieldIterationTests_tupleTargetsUseStableComponentIdsAndDirectAccesses_opt.j").toPath()); + for (String generated : new String[]{lua, jass}) { + assertFalse(generated.contains("mapFields")); + assertFalse(generated.contains("reflection")); + } + } + + @Test + public void schemaAwareIterationDiagnosticsAreActionable() { + test() + .expectError("requires @saveField(id) on field missing") + .lines( + "package FieldIterationTest", + " @annotation function annotation()", + " @annotation function saveField(int id)", + " class State", + " int missing", + " function consume(int value)", + " init", + " let state = new State", + " forFields(state, (id, name, value) -> consume(value))", + "endpackage" + ); + test() + .expectError("Duplicate @saveField id 1") + .lines( + "package FieldIterationTest", + " @annotation function annotation()", + " @annotation function saveField(int id)", + " class State", + " @saveField(1) int first", + " @saveField(1) int second", + " function consume(int value)", + " init", + " let state = new State", + " forFields(state, (id, name, value) -> consume(value))", + "endpackage" + ); + test() + .expectError("@saveField id must be a positive integer") + .lines( + "package FieldIterationTest", + " @annotation function annotation()", + " @annotation function saveField(int id)", + " class State", + " @saveField(0) int invalid", + " function consume(int value)", + " init", + " let state = new State", + " forFields(state, (id, name, value) -> consume(value))", + "endpackage" + ); + test() + .expectError("mapFields tuple target must be a variable") + .lines( + "package FieldIterationTest", + " tuple Pair(int left, int right)", + " function map(int id, string name, int value) returns int", + " return value", + " init", + " mapFields(Pair(1, 2), (id, name, value) -> map(id, name, value))", + "endpackage" + ); + test() + .expectError("@saveFields must provide exactly one integer id per tuple component") + .lines( + "package FieldIterationTest", + " @annotation function annotation()", + " @annotation function saveFields(vararg int ids)", + " @saveFields(1) tuple Pair(int left, int right)", + " function consume(int value)", + " init", + " let pair = Pair(1, 2)", + " forFields(pair, (id, name, value) -> consume(value))", + "endpackage" + ); + } } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GetDefinitionTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GetDefinitionTests.java index 4cbcc9697..2ea4b6128 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GetDefinitionTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GetDefinitionTests.java @@ -128,6 +128,25 @@ public void indexWriteOperatorOnBracketJumpsToOverload() { testGetDef(testData, "4:17-4:31"); } + @Test + public void compilerIntrinsicCallJumpsToDocumentedDeclaration() { + CompletionTestData testData = input( + "package test", + " @annotation function annotation()", + " @annotation function compilerintrinsic()", + " interface DocumentedFieldCallback", + " function apply(string name, int value)", + " @compilerintrinsic function forFields(DocumentedFieldCallback callback)", + " class State", + " int value", + " function save()", + " forF|ields((name, fieldValue) -> skip)", + "endpackage" + ); + + testGetDef(testData, "5:32-5:41"); + } + private void testGetDef(CompletionTestData testData, String... expectedPositions) { testGetDef(testData, Arrays.asList(expectedPositions)); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HoverTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HoverTests.java index aacf3b85c..6a3664836 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HoverTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HoverTests.java @@ -51,6 +51,29 @@ public void hoverUsesHotdocComment() { assertTrue(text.stream().anyMatch(s -> s.contains("this is hover doc")), "hover text = " + text); } + @Test + public void compilerIntrinsicCallUsesDeclarationSignatureAndDocumentation() { + CompletionTestData testData = input( + "package test", + " @annotation function annotation()", + " @annotation function compilerintrinsic()", + " interface DocumentedFieldCallback", + " function apply(string name, int value)", + " /** Iterates concrete fields using direct accesses. */", + " @compilerintrinsic function forFields(DocumentedFieldCallback callback)", + " class State", + " int value", + " function save()", + " forF|ields((name, fieldValue) -> skip)", + "endpackage" + ); + + List text = testHoverText(testData); + assertTrue(text.stream().anyMatch(s -> s.contains("Iterates concrete fields")), "hover text = " + text); + assertTrue(text.stream().anyMatch(s -> s.contains("function forFields(DocumentedFieldCallback callback)")), + "hover text = " + text); + } + @Test public void hoverOnCommentShowsNothing() { CompletionTestData testData = input( From 9c5de3918be14ba7247e564a2a296469e639d6ea Mon Sep 17 00:00:00 2001 From: Frotty Date: Tue, 11 Aug 2026 11:43:28 +0200 Subject: [PATCH 2/5] Preserve nested schema ID locals --- .../de/peeeq/wurstscript/SyntacticSugar.java | 11 ++++--- .../tests/FieldIterationTests.java | 32 +++++++++++++++++++ 2 files changed, 39 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 8c405d937..09bb1c45e 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 @@ -531,13 +531,17 @@ private boolean isShadowed(String name) { return shadowedScopes.stream().anyMatch(scope -> scope.contains(name)); } + private boolean isCallbackParameter(String name) { + return (idParameter != null && name.equals(idParameter)) + || name.equals(nameParameter) || name.equals(valueParameter); + } + @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))) { + && isCallbackParameter(localVarDef.getName())) { blockBindings.add(localVarDef.getName()); } } @@ -603,8 +607,7 @@ public void visit(ExprClosure nestedClosure) { public void visit(LocalVarDef localVarDef) { super.visit(localVarDef); if (!shadowedScopes.isEmpty() - && (localVarDef.getName().equals(nameParameter) - || localVarDef.getName().equals(valueParameter))) { + && isCallbackParameter(localVarDef.getName())) { shadowedScopes.peek().add(localVarDef.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 c36c968cb..de7f74bac 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 @@ -667,6 +667,38 @@ public void preservesLocalBindingsInBlockCallbacks() { ); } + @Test + public void preservesSchemaIdNamedLocalInsideNestedClosure() { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package FieldIterationTest", + " @annotation function annotation()", + " @annotation function saveField(int id)", + " native testSuccess()", + " int total = 0", + " interface Callback", + " function run()", + " function invoke(Callback callback)", + " callback.run()", + " class Data", + " @saveField(7) int value = 1", + " function save()", + " forFields((id, name, fieldValue) -> invoke(() -> begin", + " let id = 100", + " total += id", + " end))", + " init", + " let data = new Data", + " data.save()", + " if total == 100", + " testSuccess()", + "endpackage" + ); + } + @Test public void keepsLoopBindingsInsideLoopBody() { test() From 0ae98c79cd0f1bc1053cd3aff1e3638b9774618c Mon Sep 17 00:00:00 2001 From: Frotty Date: Tue, 11 Aug 2026 12:08:23 +0200 Subject: [PATCH 3/5] Keep serialization schemas library-owned --- .../de/peeeq/wurstscript/SyntacticSugar.java | 124 +++---------- .../validation/WurstValidator.java | 2 +- .../tests/FieldIterationTests.java | 163 +++++------------- 3 files changed, 65 insertions(+), 224 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 09bb1c45e..e09d430a1 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 @@ -179,11 +179,9 @@ private void expandFieldIteration(ExprFunctionCall call, int closureIndex = explicitTarget ? 1 : 0; if ((!explicitTarget && call.getArgs().size() != 1) || !(call.getArgs().get(closureIndex) instanceof ExprClosure closure) - || (closure.getShortParameters().size() != 2 - && closure.getShortParameters().size() != 3)) { + || closure.getShortParameters().size() != 2) { call.addError(call.getFuncName() - + " expects a closure with (fieldName, fieldValue) parameters or " - + "(fieldId, fieldName, fieldValue) parameters, optionally preceded by a target."); + + " expects a closure with (fieldName, fieldValue) parameters, optionally preceded by a target."); return; } for (WShortParameter parameter : closure.getShortParameters()) { @@ -193,20 +191,16 @@ private void expandFieldIteration(ExprFunctionCall call, } } - boolean schemaAware = closure.getShortParameters().size() == 3; - String idParameter = schemaAware ? closure.getShortParameters().get(0).getName() : null; - String nameParameter = closure.getShortParameters().get(schemaAware ? 1 : 0).getName(); - String valueParameter = closure.getShortParameters().get(schemaAware ? 2 : 1).getName(); - Set callbackParameters = new LinkedHashSet<>(); - if (idParameter != null) callbackParameters.add(idParameter); - callbackParameters.add(nameParameter); - callbackParameters.add(valueParameter); - if (callbackParameters.size() != closure.getShortParameters().size()) { - closure.addError("Field iteration closure parameters must have distinct names."); + 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, callbackParameters)) { - call.addError("Field iteration callbacks cannot declare locals or loop variables named like callback parameters."); + 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)) { @@ -291,14 +285,6 @@ private void expandFieldIteration(ExprFunctionCall call, + " requires at least one instance field; no accessible mutable instance fields were found."); return; } - Map schemaIds = schemaAware ? validateSchemaIds(call, fields) : Map.of(); - if (schemaAware && schemaIds.size() != fields.size()) { - if (targetVariable != null) { - statements.remove(targetVariable); - statements.clearAttributes(); - } - return; - } int statementIndex = statements.indexOf(call); detached.add(new DeferredModuleCall(statements, originalStatementIndex, call)); statements.remove(statementIndex); @@ -311,8 +297,7 @@ private void expandFieldIteration(ExprFunctionCall call, String fieldKey = field.key(); Expr fieldAccess = fieldAccess(call.getSource(), field, targetName); Expr implementation = substituteFieldParameters( - closure.getImplementation().copy(), idParameter, nameParameter, valueParameter, - schemaAware ? schemaId(field) : null, fieldKey, field, targetName); + closure.getImplementation().copy(), nameParameter, valueParameter, fieldKey, field, targetName); WStatement expanded; if (assignsResult) { expanded = Ast.StmtSet(call.getSource(), (LExpr) fieldAccess, implementation); @@ -334,65 +319,6 @@ private List collectTupleFields(TupleDef tupleDef) { return fields; } - private Map validateSchemaIds(ExprFunctionCall call, List fields) { - Map result = new LinkedHashMap<>(); - for (FieldInfo field : fields) { - Integer id = schemaId(field); - if (id == null) { - field.declaration.addError("Schema-aware field iteration requires @saveField(id) on field " - + field.key() + "."); - continue; - } - if (id <= 0) { - field.declaration.addError("@saveField id must be a positive integer, but found " + id + "."); - continue; - } - FieldInfo previous = result.putIfAbsent(id, field); - if (previous != null) { - call.addError("Duplicate @saveField id " + id + " on fields " - + previous.key() + " and " + field.key() + "."); - } - } - return result; - } - - private Integer schemaId(FieldInfo field) { - if (!(field.declaration instanceof GlobalVarDef global)) { - Element parent = field.declaration.getParent(); - if (parent instanceof WParameters parameters) { - int index = parameters.indexOf(field.declaration); - if (parameters.getParent() instanceof TupleDef tuple) { - Annotation annotation = tuple.getAnnotation("@saveFields"); - if (annotation != null) { - if (annotation.getArgs().size() != parameters.size()) { - tuple.addError("@saveFields must provide exactly one integer id per tuple component."); - return null; - } - Expr argument = annotation.getArgs().get(index); - if (!(argument instanceof ExprIntVal value)) { - argument.addError("@saveFields ids must be integer literals."); - return null; - } - return value.getValI(); - } - } - // Tuple order is part of its structural type. Positional ids are the compact - // default; @saveFields opts into rename/reorder-stable persisted identities. - return index + 1; - } - return null; - } - Annotation annotation = global.getAnnotation("@saveField"); - if (annotation == null || annotation.getArgs().size() != 1 - || !(annotation.getArgs().get(0) instanceof ExprIntVal value)) { - if (annotation != null) { - global.addError("@saveField expects exactly one integer literal id."); - } - return null; - } - return value.getValI(); - } - private List collectInstanceFields(ClassDef classDef, ClassOrModule owner, Element accessSite, boolean explicitTarget, boolean requireMutable) { @@ -489,7 +415,7 @@ private boolean canAccessProtectedField(GlobalVarDef field, ClassDef declaringCl && accessClass.attrTypC().isSubtypeOf(declaringClass.attrTypC(), accessSite); } - private boolean hasShadowingLocal(ExprClosure closure, Set callbackParameters) { + private boolean hasShadowingLocal(ExprClosure closure, String nameParameter, String valueParameter) { final boolean[] result = {false}; closure.getImplementation().accept(new WurstModel.DefaultVisitor() { @Override @@ -500,7 +426,7 @@ public void visit(ExprClosure nestedClosure) { @Override public void visit(LocalVarDef localVarDef) { super.visit(localVarDef); - if (callbackParameters.contains(localVarDef.getName())) { + if (localVarDef.getName().equals(nameParameter) || localVarDef.getName().equals(valueParameter)) { result[0] = true; } } @@ -508,13 +434,10 @@ public void visit(LocalVarDef localVarDef) { return result[0]; } - private Expr substituteFieldParameters(Expr expression, String idParameter, String nameParameter, - String valueParameter, Integer fieldId, String fieldName, FieldInfo field, + private Expr substituteFieldParameters(Expr expression, String nameParameter, + String valueParameter, String fieldName, FieldInfo field, String targetName) { if (expression instanceof ExprVarAccess access) { - if (idParameter != null && access.getVarName().equals(idParameter)) { - return Ast.ExprIntVal(access.getSource(), Integer.toString(Objects.requireNonNull(fieldId))); - } if (access.getVarName().equals(nameParameter)) { return Ast.ExprStringVal(access.getSource(), fieldName); } @@ -532,8 +455,7 @@ private boolean isShadowed(String name) { } private boolean isCallbackParameter(String name) { - return (idParameter != null && name.equals(idParameter)) - || name.equals(nameParameter) || name.equals(valueParameter); + return name.equals(nameParameter) || name.equals(valueParameter); } @Override @@ -616,21 +538,15 @@ && isCallbackParameter(localVarDef.getName())) { public void visit(ExprVarAccess access) { super.visit(access); if (!isShadowed(access.getVarName()) - && ((idParameter != null && access.getVarName().equals(idParameter)) - || access.getVarName().equals(nameParameter) || access.getVarName().equals(valueParameter))) { + && (access.getVarName().equals(nameParameter) || access.getVarName().equals(valueParameter))) { accesses.add(access); } } }); for (ExprVarAccess access : accesses) { - Expr replacement; - if (idParameter != null && access.getVarName().equals(idParameter)) { - replacement = Ast.ExprIntVal(access.getSource(), Integer.toString(Objects.requireNonNull(fieldId))); - } else if (access.getVarName().equals(nameParameter)) { - replacement = Ast.ExprStringVal(access.getSource(), fieldName); - } else { - replacement = fieldAccess(access.getSource(), field, targetName); - } + Expr replacement = access.getVarName().equals(nameParameter) + ? Ast.ExprStringVal(access.getSource(), fieldName) + : fieldAccess(access.getSource(), field, targetName); access.replaceBy(replacement); } return expression; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index e0f6fd748..6a65f1544 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -2814,7 +2814,7 @@ public void case_InterfaceDef(InterfaceDef interfaceDef) { @Override public void case_TupleDef(TupleDef tupleDef) { - check(VisibilityPublic.class, Annotation.class); + check(VisibilityPublic.class); } @Override 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 de7f74bac..a27c46d9c 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 @@ -667,38 +667,6 @@ public void preservesLocalBindingsInBlockCallbacks() { ); } - @Test - public void preservesSchemaIdNamedLocalInsideNestedClosure() { - test() - .testLua(true) - .luaOnly(false) - .executeProg() - .lines( - "package FieldIterationTest", - " @annotation function annotation()", - " @annotation function saveField(int id)", - " native testSuccess()", - " int total = 0", - " interface Callback", - " function run()", - " function invoke(Callback callback)", - " callback.run()", - " class Data", - " @saveField(7) int value = 1", - " function save()", - " forFields((id, name, fieldValue) -> invoke(() -> begin", - " let id = 100", - " total += id", - " end))", - " init", - " let data = new Data", - " data.save()", - " if total == 100", - " testSuccess()", - "endpackage" - ); - } - @Test public void keepsLoopBindingsInsideLoopBody() { test() @@ -1047,15 +1015,13 @@ public void compilerIntrinsicDeclarationsRemainLowered() throws IOException { } @Test - public void schemaAwareIterationComposesAcrossSerializableFieldKinds() throws IOException { + public void fieldIterationComposesAcrossSerializableFieldKinds() throws IOException { test() .testLua(true) .luaOnly(false) .executeProg() .lines( "package FieldIterationTest", - " @annotation function annotation()", - " @annotation function saveField(int id)", " native testSuccess()", "", " tuple Pair(int left, string right)", @@ -1069,57 +1035,56 @@ public void schemaAwareIterationComposesAcrossSerializableFieldKinds() throws IO " function encoded() returns int", " return value", " module Injected", - " @saveField(12) int injected = 2", + " int injected = 2", " class Base", - " @saveField(11) int inherited = 1", + " int inherited = 1", " class State extends Base", " use Injected", - " @saveField(20) int primitive = 4", - " @saveField(30) Child nested = new Child", - " @saveField(40) Pair pair = Pair(5, \"x\")", - " @saveField(50) ArrayList values = new ArrayList", - " @saveField(60) Mode mode = Mode.B", - " @saveField(70) Child optional = null", + " int primitive = 4", + " Child nested = new Child", + " Pair pair = Pair(5, \"x\")", + " ArrayList values = new ArrayList", + " Mode mode = Mode.B", + " Child optional = null", "", " class Probe", " int mask = 0", - " function record(int id, string name, int value)", - " mask += id + value", - " function record(int id, string name, Child value)", + " function record(string name, int value)", + " mask += value", + " function record(string name, Child value)", " if value == null", - " mask += id", + " mask += 1", " else", - " mask += id + value.encoded()", - " function record(int id, string name, Pair value)", - " mask += id + value.left", - " function record(int id, string name, ArrayList value)", - " mask += id + value.size", - " function record(int id, string name, Mode value)", - " mask += id", + " mask += value.encoded()", + " function record(string name, Pair value)", + " mask += value.left", + " function record(string name, ArrayList value)", + " mask += value.size", + " function record(string name, Mode value)", + " mask += 1", "", " init", " let state = new State", " let probe = new Probe", - " forFields(state, (id, name, value) -> probe.record(id, name, value))", - " if probe.mask == 317", + " forFields(state, (name, value) -> probe.record(name, value))", + " if probe.mask == 26", " testSuccess()", "endpackage" ); String lua = Files.readString(new File(TEST_OUTPUT_PATH - + "lua/FieldIterationTests_schemaAwareIterationComposesAcrossSerializableFieldKinds.lua").toPath()); + + "lua/FieldIterationTests_fieldIterationComposesAcrossSerializableFieldKinds.lua").toPath()); String jass = Files.readString(new File(TEST_OUTPUT_PATH - + "FieldIterationTests_schemaAwareIterationComposesAcrossSerializableFieldKinds_opt.j").toPath()); + + "FieldIterationTests_fieldIterationComposesAcrossSerializableFieldKinds_opt.j").toPath()); for (String generated : new String[]{lua, jass}) { assertFalse(generated.contains("forFields")); - assertFalse(generated.contains("saveField")); assertFalse(generated.contains("reflection")); - assertTrue(generated.contains("11") && generated.contains("inherited")); + assertTrue(generated.contains("inherited")); } } @Test - public void tupleTargetsUseStableComponentIdsAndDirectAccesses() throws IOException { + public void tupleTargetsUseDirectComponentAccesses() throws IOException { test() .testLua(true) .luaOnly(false) @@ -1127,27 +1092,25 @@ public void tupleTargetsUseStableComponentIdsAndDirectAccesses() throws IOExcept .lines( "package FieldIterationTest", " native testSuccess()", - " @annotation function annotation()", - " @annotation function saveFields(vararg int ids)", - " @saveFields(7, 12, 99) tuple Payload(int count, string label, boolean enabled)", - " function map(int id, string name, int value) returns int", - " return value + id", - " function map(int id, string name, string value) returns string", + " tuple Payload(int count, string label, boolean enabled)", + " function map(string name, int value) returns int", + " return value + 1", + " function map(string name, string value) returns string", " return value + name", - " function map(int id, string name, boolean value) returns boolean", + " function map(string name, boolean value) returns boolean", " return not value", " init", " var payload = Payload(4, \"x\", false)", - " mapFields(payload, (id, name, value) -> map(id, name, value))", - " if payload.count == 11 and payload.label == \"xlabel\" and payload.enabled", + " mapFields(payload, (name, value) -> map(name, value))", + " if payload.count == 5 and payload.label == \"xlabel\" and payload.enabled", " testSuccess()", "endpackage" ); String lua = Files.readString(new File(TEST_OUTPUT_PATH - + "lua/FieldIterationTests_tupleTargetsUseStableComponentIdsAndDirectAccesses.lua").toPath()); + + "lua/FieldIterationTests_tupleTargetsUseDirectComponentAccesses.lua").toPath()); String jass = Files.readString(new File(TEST_OUTPUT_PATH - + "FieldIterationTests_tupleTargetsUseStableComponentIdsAndDirectAccesses_opt.j").toPath()); + + "FieldIterationTests_tupleTargetsUseDirectComponentAccesses_opt.j").toPath()); for (String generated : new String[]{lua, jass}) { assertFalse(generated.contains("mapFields")); assertFalse(generated.contains("reflection")); @@ -1155,72 +1118,34 @@ public void tupleTargetsUseStableComponentIdsAndDirectAccesses() throws IOExcept } @Test - public void schemaAwareIterationDiagnosticsAreActionable() { + public void serializationMetadataRemainsLibraryOwned() { test() - .expectError("requires @saveField(id) on field missing") - .lines( - "package FieldIterationTest", - " @annotation function annotation()", - " @annotation function saveField(int id)", - " class State", - " int missing", - " function consume(int value)", - " init", - " let state = new State", - " forFields(state, (id, name, value) -> consume(value))", - "endpackage" - ); - test() - .expectError("Duplicate @saveField id 1") - .lines( - "package FieldIterationTest", - " @annotation function annotation()", - " @annotation function saveField(int id)", - " class State", - " @saveField(1) int first", - " @saveField(1) int second", - " function consume(int value)", - " init", - " let state = new State", - " forFields(state, (id, name, value) -> consume(value))", - "endpackage" - ); - test() - .expectError("@saveField id must be a positive integer") + .expectError("expects a closure with (fieldName, fieldValue) parameters") .lines( "package FieldIterationTest", " @annotation function annotation()", " @annotation function saveField(int id)", " class State", - " @saveField(0) int invalid", + " @saveField(1) int value", " function consume(int value)", " init", " let state = new State", " forFields(state, (id, name, value) -> consume(value))", "endpackage" ); + } + + @Test + public void tupleFieldIterationDiagnosticsAreActionable() { test() .expectError("mapFields tuple target must be a variable") .lines( "package FieldIterationTest", " tuple Pair(int left, int right)", - " function map(int id, string name, int value) returns int", + " function map(string name, int value) returns int", " return value", " init", - " mapFields(Pair(1, 2), (id, name, value) -> map(id, name, value))", - "endpackage" - ); - test() - .expectError("@saveFields must provide exactly one integer id per tuple component") - .lines( - "package FieldIterationTest", - " @annotation function annotation()", - " @annotation function saveFields(vararg int ids)", - " @saveFields(1) tuple Pair(int left, int right)", - " function consume(int value)", - " init", - " let pair = Pair(1, 2)", - " forFields(pair, (id, name, value) -> consume(value))", + " mapFields(Pair(1, 2), (name, value) -> map(name, value))", "endpackage" ); } From 54131e219fd9314a2308f324d9fdda3f87e0732c Mon Sep 17 00:00:00 2001 From: Frotty Date: Tue, 11 Aug 2026 12:55:50 +0200 Subject: [PATCH 4/5] Fix intrinsic overload collisions --- AGENTS.md | 15 ++--- .../peeeq/wurstscript/CompilerIntrinsics.java | 19 ++++-- .../de/peeeq/wurstscript/SyntacticSugar.java | 9 +-- .../wurstscript/attributes/AttrFuncDef.java | 9 +++ .../imtranslation/EliminateGenerics.java | 2 +- .../wurstscript/tests/AutoCompleteTests.java | 6 +- .../tests/FieldIterationTests.java | 60 ++++++++++++++++--- .../wurstscript/tests/GetDefinitionTests.java | 6 +- .../tests/wurstscript/tests/HoverTests.java | 6 +- 9 files changed, 98 insertions(+), 34 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4aec45f1f..50c442536 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -302,19 +302,20 @@ of save formats, `ChunkedString`, hashes, or `Serializable`. ### Source-level contract -* The public Wurst names are `forFields`, `mapFields`, and `newInstance()`; do not introduce underscore-prefixed - alternatives. Internal markers must never survive backend lowering. +* The public Wurst names are `wurstForFields`, `wurstMapFields`, and `wurstNewInstance()`; the `wurst` prefix + makes the compiler-provided surface collision-resistant without underscore-prefixed names. The original + unprefixed spellings remain supported as compatibility fallbacks. Internal markers must never survive backend lowering. * Names beginning with the compiler-internal `__wurst` prefix are reserved. Generated temporaries must be fresh against user-visible enclosing declarations, but nested callback locals deliberately using that prefix are not supported. * An applicable visible ordinary function with one of these names must resolve normally. Compiler handling is only the fallback when no user-visible overload accepts the call. -* `forFields` includes accessible, non-static instance fields, including inherited, module-injected, readonly, and - constant fields. `mapFields` additionally requires each included field to be mutable. +* `wurstForFields` includes accessible, non-static instance fields, including inherited, module-injected, readonly, and + constant fields. `wurstMapFields` additionally requires each included field to be mutable. * Explicit targets are evaluated exactly once. Generated temporaries must be proven fresh in the enclosing scope. * Preserve module qualification in both field keys and generated accesses so sibling modules with equal field names remain distinct. -* `newInstance()` must invoke the normal accessible zero-argument constructor of a concrete, non-abstract class. +* `wurstNewInstance()` must invoke the normal accessible zero-argument constructor of a concrete, non-abstract class. Never replace it with uninitialized allocation or runtime type lookup. ### Lowering and backend rules @@ -333,9 +334,9 @@ of save formats, `ChunkedString`, hashes, or `Serializable`. generic receiver. Use the free generic loader shape, or bind the receiver to a typed local first. * Lua generic-construction dispatch through multi-parameter generic interfaces is outside the supported loader shape. The supported generic loader has a single construction type parameter. -* Do not call `newInstance()` from the constructor of a generic class. Construct the simple state object in the +* Do not call `wurstNewInstance()` from the constructor of a generic class. Construct the simple state object in the generic loader, then initialize any nested state explicitly after construction. -* `newInstance()` is a runtime Jass/Lua construction surface and is not supported inside `compiletime(...)` +* `wurstNewInstance()` is a runtime Jass/Lua construction surface and is not supported inside `compiletime(...)` evaluation. Do not expand interpreter behavior for compile-time construction. * Nested modules whose sibling submodules declare equal field names are outside the supported field-key model. Dedicated state classes should use direct fields, ordinary inheritance, or non-conflicting shallow module fields. diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java index 861c62c3e..81cc669c6 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java @@ -7,9 +7,12 @@ /** Source-level compiler intrinsics which must be eliminated before backend emission. */ public final class CompilerIntrinsics { - public static final String FOR_FIELDS = "forFields"; - public static final String MAP_FIELDS = "mapFields"; - public static final String NEW = "newInstance"; + public static final String FOR_FIELDS = "wurstForFields"; + public static final String MAP_FIELDS = "wurstMapFields"; + public static final String NEW = "wurstNewInstance"; + private static final String LEGACY_FOR_FIELDS = "forFields"; + private static final String LEGACY_MAP_FIELDS = "mapFields"; + private static final String LEGACY_NEW = "newInstance"; public static final String NEW_MARKER = "wurstNewMarker"; public static final String ANNOTATION = "compilerintrinsic"; @@ -17,13 +20,13 @@ private CompilerIntrinsics() { } public static boolean isForFields(ExprFunctionCall call) { - return FOR_FIELDS.equals(call.getFuncName()) + return hasName(call, FOR_FIELDS, LEGACY_FOR_FIELDS) && hasClosureArgument(call) && !AttrFuncDef.hasApplicableUserFunction(call); } public static boolean isMapFields(ExprFunctionCall call) { - return MAP_FIELDS.equals(call.getFuncName()) + return hasName(call, MAP_FIELDS, LEGACY_MAP_FIELDS) && hasClosureArgument(call) && !AttrFuncDef.hasApplicableUserFunction(call); } @@ -33,7 +36,7 @@ public static boolean isFieldIteration(ExprFunctionCall call) { } public static boolean isNew(ExprFunctionCall call) { - return NEW.equals(call.getFuncName()) && !AttrFuncDef.hasApplicableUserFunction(call); + return hasName(call, NEW, LEGACY_NEW) && !AttrFuncDef.hasApplicableUserFunction(call); } private static boolean hasClosureArgument(ExprFunctionCall call) { @@ -43,4 +46,8 @@ private static boolean hasClosureArgument(ExprFunctionCall call) { public static boolean isDeclaration(de.peeeq.wurstscript.ast.FunctionDefinition definition) { return definition.attrHasAnnotation(ANNOTATION); } + + private static boolean hasName(ExprFunctionCall call, String name, String legacyName) { + return name.equals(call.getFuncName()) || legacyName.equals(call.getFuncName()); + } } 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 e09d430a1..1d1faba12 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 @@ -145,8 +145,8 @@ public void restoreModuleTemplateFieldIterations(List detach * field accesses. * *
-     * forFields((name, value) -> writer.write(name, value))
-     * mapFields((name, value) -> reader.read(name, value))
+     * wurstForFields((name, value) -> writer.write(name, value))
+     * wurstMapFields((name, value) -> reader.read(name, value))
      * 
*/ private List expandFieldIterationsInTree(CompilationUnit root) { @@ -204,7 +204,7 @@ private void expandFieldIteration(ExprFunctionCall call, return; } if (!assignsResult && !(closure.getImplementation() instanceof WStatement)) { - call.addError("forFields closure must produce a statement expression."); + call.addError(call.getFuncName() + " closure must produce a statement expression."); return; } ClassDef classDef = null; @@ -238,7 +238,8 @@ private void expandFieldIteration(ExprFunctionCall call, if (!(target instanceof ExprVarAccess targetAccess)) { statements.remove(targetVariable); statements.clearAttributes(); - call.addError("mapFields tuple target must be a variable so updates can be written back exactly once."); + call.addError(call.getFuncName() + + " tuple target must be a variable so updates can be written back exactly once."); return; } statements.remove(targetVariable); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java index 949ae7123..eb3c8f89d 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java @@ -547,6 +547,15 @@ private static FuncLink searchFunction(String funcName, @Nullable FuncRef node, return null; } ImmutableCollection funcs1 = node.lookupFuncs(funcName); + if (node instanceof ExprFunctionCall + && hasApplicableUserFunction((ExprFunctionCall) node)) { + ImmutableList ordinaryFunctions = funcs1.stream() + .filter(f -> !CompilerIntrinsics.isDeclaration(f.getDef())) + .collect(Utils.toImmutableList()); + if (!ordinaryFunctions.isEmpty()) { + funcs1 = ordinaryFunctions; + } + } if (funcs1.size() == 0) { if (funcName.startsWith("InitTrig_")) { // ignore error diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java index 7bd10f4e7..a24fbc516 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java @@ -101,7 +101,7 @@ public void transform() { /** * Lua normally erases new generics. Generic construction is the one operation which needs the - * concrete type, so only specialize functions on paths leading to {@code newInstance}. All other + * concrete type, so only specialize functions on paths leading to {@code wurstNewInstance}. All other * generic calls and classes keep the Lua backend's normal erased representation. */ public void transformGenericNewOnly() { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/AutoCompleteTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/AutoCompleteTests.java index 532282e95..0b01edd16 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/AutoCompleteTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/AutoCompleteTests.java @@ -76,13 +76,13 @@ public void compilerIntrinsicDeclarationIsDiscoverable() { " @annotation function compilerintrinsic()", " interface DocumentedFieldCallback", " function apply(string name, int value)", - " @compilerintrinsic function forFields(DocumentedFieldCallback callback)", + " @compilerintrinsic function wurstForFields(DocumentedFieldCallback callback)", " init", - " forFi|", + " wurstForFi|", "endpackage" ); - testCompletions(testData, "forFields"); + testCompletions(testData, "wurstForFields"); } 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 a27c46d9c..587d552d8 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 @@ -981,8 +981,11 @@ public void compilerIntrinsicDeclarationsRemainLowered() throws IOException { " @annotation function compilerintrinsic()", " interface DocumentedFieldCallback", " function apply(string fieldName, int value)", - " @compilerintrinsic function forFields(DocumentedFieldCallback callback)", - " @compilerintrinsic function newInstance() returns T", + " @compilerintrinsic function wurstForFields(DocumentedFieldCallback callback)", + " interface DocumentedFieldMapper", + " function apply(string fieldName, int value) returns int", + " @compilerintrinsic function wurstMapFields(DocumentedFieldMapper callback)", + " @compilerintrinsic function wurstNewInstance() returns T", " return null", "endpackage", "", @@ -995,11 +998,14 @@ public void compilerIntrinsicDeclarationsRemainLowered() throws IOException { " class State", " int value = 4", " function save()", - " forFields((name, fieldValue) -> add(fieldValue))", + " wurstForFields((name, fieldValue) -> add(fieldValue))", + " function load()", + " wurstMapFields((name, fieldValue) -> fieldValue + 1)", " init", - " State result = newInstance()", + " State result = wurstNewInstance()", + " result.load()", " result.save()", - " if total == 4", + " if total == 5", " testSuccess()", "endpackage" ); @@ -1009,11 +1015,51 @@ public void compilerIntrinsicDeclarationsRemainLowered() throws IOException { String jass = Files.readString(new File(TEST_OUTPUT_PATH + "FieldIterationTests_compilerIntrinsicDeclarationsRemainLowered_opt.j").toPath()); for (String generated : new String[]{lua, jass}) { - assertFalse(generated.contains("newInstance")); - assertFalse(generated.contains("forFields")); + assertFalse(generated.contains("wurstNewInstance")); + assertFalse(generated.contains("wurstForFields")); + assertFalse(generated.contains("wurstMapFields")); } } + @Test + public void ordinaryOverloadsWinOverImportedIntrinsicDeclarations() { + test() + .executeProg() + .lines( + "package MagicFunctions", + " @annotation function annotation()", + " @annotation function compilerintrinsic()", + " public interface DocumentedFieldCallback", + " function apply(int value) returns int", + " @compilerintrinsic function wurstForFields(DocumentedFieldCallback callback) returns int", + " return 0", + " @compilerintrinsic function wurstMapFields(DocumentedFieldCallback callback) returns int", + " return 0", + " @compilerintrinsic function wurstNewInstance() returns T", + " return null", + "endpackage", + "", + "package UserFunctions", + " import MagicFunctions", + " public function wurstForFields(DocumentedFieldCallback callback) returns int", + " return callback.apply(2)", + " public function wurstMapFields(DocumentedFieldCallback callback) returns int", + " return callback.apply(3)", + " public function wurstNewInstance() returns int", + " return 7", + "endpackage", + "", + "package FieldIterationTest", + " import MagicFunctions", + " import UserFunctions", + " native testSuccess()", + " init", + " if wurstForFields(value -> value + 1) == 3 and wurstMapFields(value -> value + 1) == 4 and wurstNewInstance() == 7", + " testSuccess()", + "endpackage" + ); + } + @Test public void fieldIterationComposesAcrossSerializableFieldKinds() throws IOException { test() diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GetDefinitionTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GetDefinitionTests.java index 2ea4b6128..4b600e550 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GetDefinitionTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GetDefinitionTests.java @@ -136,15 +136,15 @@ public void compilerIntrinsicCallJumpsToDocumentedDeclaration() { " @annotation function compilerintrinsic()", " interface DocumentedFieldCallback", " function apply(string name, int value)", - " @compilerintrinsic function forFields(DocumentedFieldCallback callback)", + " @compilerintrinsic function wurstForFields(DocumentedFieldCallback callback)", " class State", " int value", " function save()", - " forF|ields((name, fieldValue) -> skip)", + " wurstForF|ields((name, fieldValue) -> skip)", "endpackage" ); - testGetDef(testData, "5:32-5:41"); + testGetDef(testData, "5:32-5:46"); } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HoverTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HoverTests.java index 6a3664836..f1d4740fd 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HoverTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HoverTests.java @@ -60,17 +60,17 @@ public void compilerIntrinsicCallUsesDeclarationSignatureAndDocumentation() { " interface DocumentedFieldCallback", " function apply(string name, int value)", " /** Iterates concrete fields using direct accesses. */", - " @compilerintrinsic function forFields(DocumentedFieldCallback callback)", + " @compilerintrinsic function wurstForFields(DocumentedFieldCallback callback)", " class State", " int value", " function save()", - " forF|ields((name, fieldValue) -> skip)", + " wurstForF|ields((name, fieldValue) -> skip)", "endpackage" ); List text = testHoverText(testData); assertTrue(text.stream().anyMatch(s -> s.contains("Iterates concrete fields")), "hover text = " + text); - assertTrue(text.stream().anyMatch(s -> s.contains("function forFields(DocumentedFieldCallback callback)")), + assertTrue(text.stream().anyMatch(s -> s.contains("function wurstForFields(DocumentedFieldCallback callback)")), "hover text = " + text); } From d43e3595413dc9f6fe52a7aa8287739dfea0b39f Mon Sep 17 00:00:00 2001 From: Frotty Date: Tue, 11 Aug 2026 13:24:48 +0200 Subject: [PATCH 5/5] Fix tuple capture and intrinsic tooling --- .../de/peeeq/wurstscript/SyntacticSugar.java | 14 +++++++---- .../wurstscript/attributes/AttrFuncDef.java | 18 ++++++++++++++- .../tests/FieldIterationTests.java | 23 +++++++++++++++++++ .../wurstscript/tests/GetDefinitionTests.java | 18 +++++++++++++++ .../tests/wurstscript/tests/HoverTests.java | 21 +++++++++++++++++ 5 files changed, 88 insertions(+), 6 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 1d1faba12..2142dc5ea 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 @@ -213,6 +213,7 @@ private void expandFieldIteration(ExprFunctionCall call, Expr target = null; String targetName = null; LocalVarDef targetVariable = null; + LExpr tupleWriteBackTarget = null; int originalStatementIndex = statements.indexOf(call); if (explicitTarget) { target = call.getArgs().get(0); @@ -242,10 +243,7 @@ private void expandFieldIteration(ExprFunctionCall call, + " tuple target must be a variable so updates can be written back exactly once."); return; } - statements.remove(targetVariable); - statements.clearAttributes(); - targetVariable = null; - targetName = targetAccess.getVarName(); + tupleWriteBackTarget = (LExpr) targetAccess.copy(); } } else if (targetType instanceof WurstTypeClass targetClass && !targetClass.isStaticRef()) { classDef = targetClass.getClassDef(); @@ -290,7 +288,7 @@ private void expandFieldIteration(ExprFunctionCall call, detached.add(new DeferredModuleCall(statements, originalStatementIndex, call)); statements.remove(statementIndex); - List generatedStatements = new ArrayList<>(fields.size() + (explicitTarget ? 1 : 0)); + List generatedStatements = new ArrayList<>(fields.size() + (explicitTarget ? 2 : 0)); if (targetVariable != null) { generatedStatements.add(targetVariable); } @@ -308,6 +306,12 @@ private void expandFieldIteration(ExprFunctionCall call, generatedStatements.add(expanded); statements.add(statementIndex++, expanded); } + if (tupleWriteBackTarget != null) { + WStatement writeBack = Ast.StmtSet(call.getSource(), tupleWriteBackTarget, + Ast.ExprVarAccess(call.getSource(), Ast.Identifier(call.getSource(), targetName))); + generatedStatements.add(writeBack); + statements.add(statementIndex, writeBack); + } detached.set(detached.size() - 1, new DeferredModuleCall(statements, originalStatementIndex, call, generatedStatements)); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java index eb3c8f89d..69960e244 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java @@ -294,7 +294,7 @@ private ToStringConversionResolution(@Nullable FuncLink conversion, @Nullable St return null; } if (CompilerIntrinsics.isNew(node)) { - return null; + return findIntrinsicDeclaration(node); } FuncLink result = searchFunction(node.getFuncName(), node, argumentTypes(node)); @@ -311,6 +311,22 @@ private ToStringConversionResolution(@Nullable FuncLink conversion, @Nullable St return result; } + private static @Nullable FuncLink findIntrinsicDeclaration(ExprFunctionCall node) { + for (FuncLink candidate : node.lookupFuncs(node.getFuncName())) { + if (!CompilerIntrinsics.isDeclaration(candidate.getDef()) + || candidate.getVisibility() == Visibility.PRIVATE_OTHER + || candidate.getVisibility() == Visibility.PROTECTED_OTHER) { + continue; + } + FunctionSignature signature = FunctionSignature.fromNameLink(candidate); + if (node.getTypeArgs().size() == signature.getDefinitionTypeVariables().size() + && signature.matchAgainstArgs(argumentTypesPre(node), node) != null) { + return candidate; + } + } + return null; + } + private static boolean isConstructorThisCall(ExprFunctionCall node) { if (!node.getFuncName().equals("this")) { return false; 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 587d552d8..9e6ef9af2 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 @@ -1163,6 +1163,29 @@ public void tupleTargetsUseDirectComponentAccesses() throws IOException { } } + @Test + public void tupleMapTargetIsCaptureSafeInNestedClosure() { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " tuple Pair(int left, int right)", + " interface PairCallback", + " function apply(Pair payload) returns int", + " function evaluate(PairCallback callback) returns int", + " return callback.apply(Pair(10, 20))", + " init", + " var payload = Pair(1, 2)", + " wurstMapFields(payload, (name, value) -> evaluate(payload -> value + payload.left))", + " if payload.left == 11 and payload.right == 12", + " testSuccess()", + "endpackage" + ); + } + @Test public void serializationMetadataRemainsLibraryOwned() { test() diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GetDefinitionTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GetDefinitionTests.java index 4b600e550..ba603c171 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GetDefinitionTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GetDefinitionTests.java @@ -147,6 +147,24 @@ public void compilerIntrinsicCallJumpsToDocumentedDeclaration() { testGetDef(testData, "5:32-5:46"); } + @Test + public void constructionIntrinsicCallJumpsToDocumentedDeclaration() { + CompletionTestData testData = input( + "package test", + " @annotation function annotation()", + " @annotation function compilerintrinsic()", + " /** Constructs a concrete class through its zero-argument constructor. */", + " @compilerintrinsic function wurstNewInstance() returns T", + " return null", + " class State", + " init", + " State result = wurstNewInst|ance()", + "endpackage" + ); + + testGetDef(testData, "4:32-4:48"); + } + private void testGetDef(CompletionTestData testData, String... expectedPositions) { testGetDef(testData, Arrays.asList(expectedPositions)); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HoverTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HoverTests.java index f1d4740fd..10b900b00 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HoverTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HoverTests.java @@ -74,6 +74,27 @@ public void compilerIntrinsicCallUsesDeclarationSignatureAndDocumentation() { "hover text = " + text); } + @Test + public void constructionIntrinsicCallUsesDeclarationSignatureAndDocumentation() { + CompletionTestData testData = input( + "package test", + " @annotation function annotation()", + " @annotation function compilerintrinsic()", + " /** Constructs a concrete class through its zero-argument constructor. */", + " @compilerintrinsic function wurstNewInstance() returns T", + " return null", + " class State", + " init", + " State result = wurstNewInst|ance()", + "endpackage" + ); + + List text = testHoverText(testData); + assertTrue(text.stream().anyMatch(s -> s.contains("Constructs a concrete class")), "hover text = " + text); + assertTrue(text.stream().anyMatch(s -> s.contains("function wurstNewInstance() returns T")), + "hover text = " + text); + } + @Test public void hoverOnCommentShowsNothing() { CompletionTestData testData = input(