diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java index b39229cb9..65f5b0b94 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java @@ -667,6 +667,8 @@ public ImTranslator getImTranslator() { // translate wurst to intermediate lang: imTranslator = new ImTranslator(root, errorHandler.isUnitTestMode(), runArgs); imProg = getImTranslator().translateProg(); + // Before any compile-time function runs: the library asks whether this compiler lowers CodeList. + CodeListSupport.markSupported(imProg); int stage = 1; printDebugImProg("./test-output/im " + stage++ + ".im"); timeTaker.endPhase(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CodeListSupport.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CodeListSupport.java new file mode 100644 index 000000000..f086cd12b --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CodeListSupport.java @@ -0,0 +1,52 @@ +package de.peeeq.wurstscript.translation.imtranslation; + +import de.peeeq.wurstscript.ast.FuncDef; +import de.peeeq.wurstscript.jassIm.Element; +import de.peeeq.wurstscript.jassIm.ImFunction; +import de.peeeq.wurstscript.jassIm.ImFunctionCall; +import de.peeeq.wurstscript.jassIm.ImProg; +import de.peeeq.wurstscript.jassIm.JassIm; +import de.peeeq.wurstscript.types.TypesHelper; + +/** + * Answers the library's compile-time check that the compiler knows the {@code CodeList} lowering. + * + *
A library that lists code values through the {@code codeList} intrinsics runs without the Lua + * lowering too, through a trigger per list, only slowly and without saying so. The library declares + * {@code codeListLowered() returns boolean} as an intrinsic whose source body answers false and + * checks it in a compile-time function, which fails the build with {@code compileError}. This + * compiler replaces every call of that declaration with true, on both targets and before any + * compile-time function runs, so the check passes only here. An older compiler runs the source body + * and stops the build. + * + *
Matching is by declaration, as for the list operations: the annotation, the name, no + * parameters and a boolean result, of a plain package function (see {@link IntrinsicDeclarations}). + */ +public final class CodeListSupport { + + private static final String PROBE = "codeListLowered"; + + private CodeListSupport() { + } + + /** Replaces the calls of the support probe with true. */ + public static void markSupported(ImProg prog) { + prog.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImFunctionCall call) { + super.visit(call); + if (isProbe(call.getFunc())) { + call.replaceBy(JassIm.ImBoolVal(true)); + } + } + }); + } + + static boolean isProbe(ImFunction f) { + FuncDef fd = IntrinsicDeclarations.plainIntrinsic(f); + return fd != null + && PROBE.equals(fd.getName()) + && f.getParameters().isEmpty() + && TypesHelper.isBoolType(f.getReturnType()); + } +} diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/IntrinsicDeclarations.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/IntrinsicDeclarations.java new file mode 100644 index 000000000..a4cfa4df4 --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/IntrinsicDeclarations.java @@ -0,0 +1,35 @@ +package de.peeeq.wurstscript.translation.imtranslation; + +import de.peeeq.wurstscript.CompilerIntrinsics; +import de.peeeq.wurstscript.ast.FuncDef; +import de.peeeq.wurstscript.ast.WPackage; +import de.peeeq.wurstscript.jassIm.ImFunction; + +/** + * What an intrinsic declaration has to be before a lowering may take it over by name: an annotated + * function of a package, without type parameters and without a vararg parameter. + * + *
A lowering that matches on the annotation, the name and the parameter types replaces the + * declaration's behaviour with its own. Anything else that keeps those parameter types would be + * replaced too, and its calls redirected to a stub built for the plain shape: a generic function + * loses its specialisation, a vararg function is called with the wrong arguments, a static class + * function of the same name is taken for the library's. One place decides, so a shape is excluded + * for every lowering that asks and not one report at a time. + */ +final class IntrinsicDeclarations { + + private IntrinsicDeclarations() { + } + + /** The source declaration of {@code f} if it is a plain intrinsic package function, else null. */ + static FuncDef plainIntrinsic(ImFunction f) { + if (f.attrTrace() instanceof FuncDef fd + && fd.attrHasAnnotation(CompilerIntrinsics.ANNOTATION) + && fd.getTypeParameters().isEmpty() + && fd.attrNearestNamedScope() instanceof WPackage + && !f.hasFlag(FunctionFlagEnum.IS_VARARG)) { + return fd; + } + return null; + } +} diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaCodeList.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaCodeList.java new file mode 100644 index 000000000..42ee3fe26 --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaCodeList.java @@ -0,0 +1,71 @@ +package de.peeeq.wurstscript.translation.imtranslation; + +import de.peeeq.wurstscript.ast.FuncDef; +import de.peeeq.wurstscript.jassIm.ImFunction; +import de.peeeq.wurstscript.jassIm.ImSimpleType; +import de.peeeq.wurstscript.jassIm.ImType; +import de.peeeq.wurstscript.jassIm.ImVoid; +import de.peeeq.wurstscript.types.TypesHelper; + +/** + * Recognises the compiler-owned {@code CodeList} operations: an ordered list of {@code code} + * values which is run right here, in the current thread, the way a trigger's conditions are. + * + *
Jass cannot keep a {@code code} value in an array or a field, and can only run one through a + * trigger or {@code ExecuteFunc}. The library's declarations do exactly that: a list is a trigger, + * adding is {@code TriggerAddCondition} and running is {@code TriggerEvaluate}. On Lua a + * {@code code} value is a function, a list is a table of them and running is one call each; a + * trigger round trip through the engine costs about thirty times that, which is what an event with + * a few listeners pays on every dispatch. The list is one table per list, and running walks its + * array part, so the order is the order of adding and does not depend on any hash layout. A value + * added while the list runs is reached in the same run, and the result of a value is ignored: both as + * for a trigger's conditions, measured in the game for the second (it runs every condition after one + * that returned false). The list is not a trigger a value can observe: GetTriggeringTrigger and the + * trigger counters are not supported inside one. + * + *
Matching is by declaration, as for the keyed maps: the function has to be annotated + * {@code @compilerintrinsic} and have the exact signature the stub assumes. The values run must + * not wait, as a condition must not; run as direct calls they make no promise about what a wait + * does. An error in one is reported by the callback adapter around every function reference, so + * the next one still runs, as it does behind a trigger. + */ +public final class LuaCodeList { + + private static final String CREATE = "codeListCreate"; + private static final String ADD = "codeListAdd"; + private static final String RUN = "codeListRun"; + + /** Stub names whose Lua bodies live in {@code LuaNatives}. */ + public static final String NATIVE_CREATE = "__wurst_codeListCreate"; + public static final String NATIVE_ADD = "__wurst_codeListAdd"; + public static final String NATIVE_RUN = "__wurst_codeListRun"; + + private LuaCodeList() { + } + + /** The {@code __wurst_} stub {@code f} is lowered to on Lua, or null if it is not a list operation. */ + public static String nativeStubFor(ImFunction f) { + FuncDef fd = IntrinsicDeclarations.plainIntrinsic(f); + if (fd == null) { + return null; + } + int params = f.getParameters().size(); + return switch (fd.getName()) { + case CREATE -> params == 0 && TypesHelper.isIntType(f.getReturnType()) ? NATIVE_CREATE : null; + case ADD -> params == 2 && isListHandle(f, 0) && isCode(f.getParameters().get(1).getType()) + && f.getReturnType() instanceof ImVoid ? NATIVE_ADD : null; + case RUN -> params == 1 && isListHandle(f, 0) && f.getReturnType() instanceof ImVoid + ? NATIVE_RUN : null; + default -> null; + }; + } + + /** A list is an {@code int} in source: on Lua {@code castTo int} of a table is the table itself. */ + private static boolean isListHandle(ImFunction f, int index) { + return TypesHelper.isIntType(f.getParameters().get(index).getType()); + } + + private static boolean isCode(ImType t) { + return t instanceof ImSimpleType simple && "code".equals(simple.getTypename()); + } +} diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java index f3a8d0a30..d2fbdbfaf 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java @@ -111,7 +111,8 @@ private LuaNativeLowering() {} * functions, most of which are unreachable in any given program). */ /** - * Replaces the KeyedTable operations with their Lua stubs, and empties the destroy operation. + * Replaces the KeyedTable, KeyedMap and CodeList operations with their Lua stubs, and empties + * the destroy operations. * *
Separate from {@link #transform} so it can run before stack-trace injection. That
* pass appends a parameter to every affected function, and on Lua every non-native function is
@@ -158,6 +159,9 @@ public void visit(ImFunctionCall call) {
if (stubName == null) {
stubName = LuaKeyedMap.nativeStubFor(f);
}
+ if (stubName == null) {
+ stubName = LuaCodeList.nativeStubFor(f);
+ }
if (stubName == null) {
return;
}
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java
index 7601ce5c9..63262152b 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java
@@ -344,6 +344,19 @@ public class LuaNatives {
f.getParams().add(LuaAst.LuaVariable("k", LuaAst.LuaNoExpr()));
f.getBody().add(LuaAst.LuaLiteral("return t[k] ~= nil"));
});
+ addNative("__wurst_codeListCreate", f ->
+ f.getBody().add(LuaAst.LuaLiteral("return {}")));
+ addNative("__wurst_codeListAdd", f -> {
+ f.getParams().add(LuaAst.LuaVariable("t", LuaAst.LuaNoExpr()));
+ f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr()));
+ f.getBody().add(LuaAst.LuaLiteral("t[#t + 1] = c"));
+ });
+ addNative("__wurst_codeListRun", f -> {
+ f.getParams().add(LuaAst.LuaVariable("t", LuaAst.LuaNoExpr()));
+ // Re-reads the next slot each time, so a value added while the list runs is reached in the same run,
+ // as it is behind a trigger.
+ f.getBody().add(LuaAst.LuaLiteral("local i = 1 local c = t[1] while c do c() i = i + 1 c = t[i] end"));
+ });
addNative("__wurst_keyedMapRemove", f -> {
f.getParams().add(LuaAst.LuaVariable("t", LuaAst.LuaNoExpr()));
f.getParams().add(LuaAst.LuaVariable("k", LuaAst.LuaNoExpr()));
diff --git a/de.peeeq.wurstscript/src/main/resources/agent-docs/WURST_LANGUAGE.md b/de.peeeq.wurstscript/src/main/resources/agent-docs/WURST_LANGUAGE.md
index 2dd682c18..546e38c5c 100644
--- a/de.peeeq.wurstscript/src/main/resources/agent-docs/WURST_LANGUAGE.md
+++ b/de.peeeq.wurstscript/src/main/resources/agent-docs/WURST_LANGUAGE.md
@@ -198,6 +198,10 @@ The compiler-owned `keyedMapPut`/`keyedMapGet` intrinsics have a map handle stor
On Lua, the recognized operations lower to a native table keyed by the handle object itself: put is one `t[k] = v` store and get is one `t[k]` read (with typed defaults for primitive results). Do not introduce handle-id conversion or an index registry on this path. The source/Jass fallback still uses the hashtable-backed `Table` API and `GetHandleId`, because Jass has no Lua table identity keys. Jass supports only `int` and class-reference values through the existing int representation; other specializations are rejected rather than boxed. Generic reference values must be non-null; use `remove` for absence. Destroy releases the Jass `Table` and clears the Lua table in place, since aliases to a destroyed Lua object may still retain its store. `LuaKeyedMapTests` covers intrinsic recognition, direct Lua table access, native handle identity, generic integer/class values, Jass fallback, and retained-alias destruction; extend it when changing this lowering contract.
+### CodeList intrinsic representation
+
+A `code` value cannot be stored in an array or a class field (a Jass restriction, checked on both targets), so a library that wants to run a list of callbacks cannot hold them itself. The compiler-owned `codeListCreate() returns int`, `codeListAdd(int list, code c)` and `codeListRun(int list)` intrinsics hold them instead. On Lua a list is one table, add appends, and run calls each function in the order added, in the current thread, so the callbacks must not wait; a callback added while the list runs is reached in the same run, and a result is ignored, which is what a trigger's conditions do (the game runs every condition after one that returned false); the list is not a trigger a callback can observe, so `GetTriggeringTrigger` and the trigger counters are not supported inside one; each function reference carries its own error boundary, so a failing callback is reported and the next still runs. The source/Jass fallback is a trigger per list (`TriggerAddCondition`, `TriggerEvaluate`), which is also what any compiler without the lowering compiles on both targets, and is correct in source. Keep the declarations annotated `@compilerintrinsic` with exactly these signatures; `LuaCodeList` recognises them by declaration and `LuaCodeListTests` covers the lowering, both backends and the shapes that must be left alone. A library that needs the lowering declares `@compilerintrinsic function codeListLowered() returns boolean` with a body that returns false and calls it from a `@compiletime` function that ends in `compileError` when it answers false: this compiler replaces every call of that declaration with true (`CodeListSupport`, before compile-time functions run, on both targets), so only an older compiler stops the build.
+
Instances are unique and must be declared next to what they relate. An instance of `I` for type `X` may only live in the package declaring `I` or the package declaring `X`, and there may be only one. This makes `I` for `X` mean the same thing everywhere, independent of imports. Instances have no type parameters of their own in this version, so there is no way to write "every `List