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 fe18491d1..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,22 +7,26 @@ /** 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"; 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); } @@ -32,10 +36,18 @@ 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) { return call.getArgs().stream().anyMatch(arg -> arg instanceof ExprClosure); } + + 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 ace3e1c3b..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 @@ -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); } @@ -144,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) { @@ -203,14 +204,16 @@ 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; + ClassDef classDef = null; + TupleDef tupleDef = null; ClassOrModule owner; Expr target = null; String targetName = null; LocalVarDef targetVariable = null; + LExpr tupleWriteBackTarget = null; int originalStatementIndex = statements.indexOf(call); if (explicitTarget) { target = call.getArgs().get(0); @@ -229,15 +232,29 @@ 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(call.getFuncName() + + " tuple target must be a variable so updates can be written back exactly once."); + return; + } + tupleWriteBackTarget = (LExpr) targetAccess.copy(); + } + } 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 +272,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); @@ -269,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); } @@ -287,10 +306,24 @@ 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)); } + private List collectTupleFields(TupleDef tupleDef) { + List fields = new ArrayList<>(); + for (WParameter parameter : tupleDef.getParameters()) { + fields.add(new FieldInfo(parameter, List.of())); + } + return fields; + } + private List collectInstanceFields(ClassDef classDef, ClassOrModule owner, Element accessSite, boolean explicitTarget, boolean requireMutable) { @@ -426,13 +459,16 @@ private boolean isShadowed(String name) { return shadowedScopes.stream().anyMatch(scope -> scope.contains(name)); } + private boolean isCallbackParameter(String name) { + return 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()); } } @@ -498,8 +534,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/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java index 4509b56e3..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; @@ -519,6 +535,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; @@ -541,6 +563,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 f5e6faac5..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 @@ -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 wurstForFields(DocumentedFieldCallback callback)", + " init", + " wurstForFi|", + "endpackage" + ); + + testCompletions(testData, "wurstForFields"); + } + @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..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 @@ -968,4 +968,254 @@ 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 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", + "", + "package FieldIterationTest", + " import MagicFunctions", + " native testSuccess()", + " int total = 0", + " function add(int value)", + " total += value", + " class State", + " int value = 4", + " function save()", + " wurstForFields((name, fieldValue) -> add(fieldValue))", + " function load()", + " wurstMapFields((name, fieldValue) -> fieldValue + 1)", + " init", + " State result = wurstNewInstance()", + " result.load()", + " result.save()", + " if total == 5", + " 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("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() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package FieldIterationTest", + " 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", + " int injected = 2", + " class Base", + " int inherited = 1", + " class State extends Base", + " use Injected", + " 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(string name, int value)", + " mask += value", + " function record(string name, Child value)", + " if value == null", + " mask += 1", + " else", + " 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, (name, value) -> probe.record(name, value))", + " if probe.mask == 26", + " testSuccess()", + "endpackage" + ); + + String lua = Files.readString(new File(TEST_OUTPUT_PATH + + "lua/FieldIterationTests_fieldIterationComposesAcrossSerializableFieldKinds.lua").toPath()); + String jass = Files.readString(new File(TEST_OUTPUT_PATH + + "FieldIterationTests_fieldIterationComposesAcrossSerializableFieldKinds_opt.j").toPath()); + for (String generated : new String[]{lua, jass}) { + assertFalse(generated.contains("forFields")); + assertFalse(generated.contains("reflection")); + assertTrue(generated.contains("inherited")); + } + } + + @Test + public void tupleTargetsUseDirectComponentAccesses() throws IOException { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " 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(string name, boolean value) returns boolean", + " return not value", + " init", + " var payload = Payload(4, \"x\", false)", + " 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_tupleTargetsUseDirectComponentAccesses.lua").toPath()); + String jass = Files.readString(new File(TEST_OUTPUT_PATH + + "FieldIterationTests_tupleTargetsUseDirectComponentAccesses_opt.j").toPath()); + for (String generated : new String[]{lua, jass}) { + assertFalse(generated.contains("mapFields")); + assertFalse(generated.contains("reflection")); + } + } + + @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() + .expectError("expects a closure with (fieldName, fieldValue) parameters") + .lines( + "package FieldIterationTest", + " @annotation function annotation()", + " @annotation function saveField(int id)", + " class State", + " @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(string name, int value) returns int", + " return value", + " init", + " mapFields(Pair(1, 2), (name, value) -> map(name, 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..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 @@ -128,6 +128,43 @@ 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 wurstForFields(DocumentedFieldCallback callback)", + " class State", + " int value", + " function save()", + " wurstForF|ields((name, fieldValue) -> skip)", + "endpackage" + ); + + 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 aacf3b85c..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 @@ -51,6 +51,50 @@ 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 wurstForFields(DocumentedFieldCallback callback)", + " class State", + " int value", + " function save()", + " 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 wurstForFields(DocumentedFieldCallback callback)")), + "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(