Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,19 +302,20 @@ of save formats, `ChunkedString`, hashes, or `Serializable`.

### Source-level contract

* The public Wurst names are `forFields`, `mapFields`, and `newInstance<T>()`; do not introduce underscore-prefixed
alternatives. Internal markers must never survive backend lowering.
* The public Wurst names are `wurstForFields`, `wurstMapFields`, and `wurstNewInstance<T>()`; 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<T>()` must invoke the normal accessible zero-argument constructor of a concrete, non-abstract class.
* `wurstNewInstance<T>()` 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
Expand All @@ -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<T>()` from the constructor of a generic class. Construct the simple state object in the
* Do not call `wurstNewInstance<T>()` 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<T>()` is a runtime Jass/Lua construction surface and is not supported inside `compiletime(...)`
* `wurstNewInstance<T>()` 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
Comment on lines +46 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expose construction intrinsic declarations to language tooling

When an IDE resolves an annotated wurstNewInstance<T>() call, this declaration is excluded from ordinary-function detection, after which AttrFuncDef.calculate(ExprFunctionCall) returns null immediately because CompilerIntrinsics.isNew(node) is true. Consequently get-definition and hover return no result for the documented construction intrinsic, even though the new declaration mechanism works for the field-iteration intrinsics; preserve the declaration link for tooling while keeping compiler lowering intact.

Useful? React with 👍 / 👎.

}

private static boolean hasName(ExprFunctionCall call, String name, String legacyName) {
return name.equals(call.getFuncName()) || legacyName.equals(call.getFuncName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -46,10 +47,10 @@ private DirectFieldIterationState(List<DeferredModuleCall> detached) {
}

private static final class FieldInfo {
private final GlobalVarDef declaration;
private final VarDef declaration;
private final List<String> modulePath;

private FieldInfo(GlobalVarDef declaration, List<String> modulePath) {
private FieldInfo(VarDef declaration, List<String> modulePath) {
this.declaration = declaration;
this.modulePath = List.copyOf(modulePath);
}
Expand Down Expand Up @@ -144,8 +145,8 @@ public void restoreModuleTemplateFieldIterations(List<DeferredModuleCall> detach
* field accesses.
*
* <pre>
* 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))
* </pre>
*/
private List<DeferredModuleCall> expandFieldIterationsInTree(CompilationUnit root) {
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -255,7 +272,9 @@ private void expandFieldIteration(ExprFunctionCall call,
return;
}
}
List<FieldInfo> fields = collectInstanceFields(classDef, owner, call, explicitTarget, assignsResult);
List<FieldInfo> fields = tupleDef == null
? collectInstanceFields(classDef, owner, call, explicitTarget, assignsResult)
: collectTupleFields(tupleDef);
if (fields.isEmpty()) {
if (targetVariable != null) {
statements.remove(targetVariable);
Expand All @@ -269,7 +288,7 @@ private void expandFieldIteration(ExprFunctionCall call,
detached.add(new DeferredModuleCall(statements, originalStatementIndex, call));
statements.remove(statementIndex);

List<WStatement> generatedStatements = new ArrayList<>(fields.size() + (explicitTarget ? 1 : 0));
List<WStatement> generatedStatements = new ArrayList<>(fields.size() + (explicitTarget ? 2 : 0));
if (targetVariable != null) {
generatedStatements.add(targetVariable);
}
Expand All @@ -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<FieldInfo> collectTupleFields(TupleDef tupleDef) {
List<FieldInfo> fields = new ArrayList<>();
for (WParameter parameter : tupleDef.getParameters()) {
fields.add(new FieldInfo(parameter, List.of()));
}
return fields;
}

private List<FieldInfo> collectInstanceFields(ClassDef classDef, ClassOrModule owner,
Element accessSite, boolean explicitTarget,
boolean requireMutable) {
Expand Down Expand Up @@ -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<String> 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());
}
}
Expand Down Expand Up @@ -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());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand All @@ -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;
Expand Down Expand Up @@ -519,6 +535,12 @@ public static boolean hasApplicableUserFunction(ExprFunctionCall node) {
}
List<WurstType> 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;
Comment on lines +541 to +542

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude intrinsic declarations from ordinary overload sets

When an imported @compilerintrinsic declaration and an applicable ordinary closure overload are both visible, this skip makes hasApplicableUserFunction disable intrinsic lowering, but normal overload resolution still retains the annotated declaration. I reproduced this with matching imported forFields(IntCallback) declarations: the call reports both as ambiguous instead of selecting the ordinary function. Filter the intrinsic contract from subsequent resolution whenever an ordinary candidate wins; the same issue applies to mapFields and newInstance.

AGENTS.md reference: AGENTS.md:L310-L311

Useful? React with 👍 / 👎.

}
if (candidate.getVisibility() == Visibility.PRIVATE_OTHER
|| candidate.getVisibility() == Visibility.PROTECTED_OTHER) {
continue;
Expand All @@ -541,6 +563,15 @@ private static FuncLink searchFunction(String funcName, @Nullable FuncRef node,
return null;
}
ImmutableCollection<FuncLink> funcs1 = node.lookupFuncs(funcName);
if (node instanceof ExprFunctionCall
&& hasApplicableUserFunction((ExprFunctionCall) node)) {
ImmutableList<FuncLink> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading
Loading