From e3eabc6fc6a858d9c9c6b3a06dea64c9ec3ddb0d Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 21 Aug 2026 14:05:49 +0200 Subject: [PATCH 1/3] Make scalar compiletime migration opt-in per global --- .../wurstio/CompiletimeFunctionRunner.java | 11 +++++++- .../validation/WurstValidator.java | 4 --- .../wurstscript/tests/CompiletimeTests.java | 16 +++++------ .../tests/LuaBackendAuditTests.java | 28 +++++++++++++++---- 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java index b27e6189b..90b08e35e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java @@ -13,6 +13,7 @@ import de.peeeq.wurstio.mpq.MpqEditor; import de.peeeq.wurstscript.WLogger; import de.peeeq.wurstscript.ast.Element; +import de.peeeq.wurstscript.ast.GlobalVarDef; import de.peeeq.wurstscript.attributes.CompileError; import de.peeeq.wurstscript.attributes.ErrorHandler; import de.peeeq.wurstscript.gui.WurstGui; @@ -750,7 +751,8 @@ private void emitCompiletimeState() { .thenComparing(ImVar::getName); modifiedScalars.sort(stableGlobalOrder); for (ImVar var : modifiedScalars) { - if (!imProg.getGlobals().contains(var) || var.getType() instanceof ImArrayLikeType) { + if (!imProg.getGlobals().contains(var) || var.getType() instanceof ImArrayLikeType + || !isScalarStateMigrationTarget(var)) { continue; } StateReplayLocation replayLocation = findReplayTarget(var); @@ -811,6 +813,13 @@ private void emitCompiletimeState() { } } + private boolean isScalarStateMigrationTarget(ImVar var) { + // Scalar compiletime state is deliberately opt-in per global. This keeps + // compiletime-only scratch values and incidental writes out of runtime init. + return var.getTrace() instanceof GlobalVarDef + && ((GlobalVarDef) var.getTrace()).hasAnnotation("@compiletime"); + } + private String sourceDiagnostic(ImVar var) { return var.getTrace().attrSource().printShort(); } 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 85c9fdf47..0c7d89b1f 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 @@ -3000,10 +3000,6 @@ public void case_GlobalVarDef(GlobalVarDef g) { } else { check(VisibilityPublic.class, ModConstant.class, ModReadonly.class, Annotation.class); } - if (g.hasAnnotation("@compiletime")) { - g.getAnnotation("@compiletime") - .addWarning("The annotation '@compiletime' has no effect on variables."); - } } @Override diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java index dfba3b6e8..bc863df3a 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java @@ -103,7 +103,7 @@ public void testUnsupportedCompiletimeArrayWarningIsAggregatedAndReadable() { public void testCompiletimePackageScalarState() { test().testLua(true).luaOnly(false).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) .lines("package A", - "public int source = 1", + "@compiletime public int source = 1", "@compiletime function fill()", " source = 42", "endpackage", @@ -123,8 +123,8 @@ public void testCompiletimeObjectAndNullScalarState() { "native testSuccess()", "class A", " int value", - "A source", - "string cleared = \"value\"", + "@compiletime A source", + "@compiletime string cleared = \"value\"", "@compiletime function fill()", " source = new A", " source.value = 42", @@ -146,7 +146,7 @@ public void testCompiletimeScalarReplayOnlyWrittenValues() { "import A", "native testSuccess()", "int observed = seed", - "int migrated = 0", + "@compiletime int migrated = 0", "@compiletime function fill()", " let snapshot = observed", " migrated = snapshot + 41", @@ -165,7 +165,7 @@ public void testLazyScalarInitializerSideEffectsAreNotReplayed() { " counter++", " return counter", "int observed = bump()", - "int migrated", + "@compiletime int migrated", "@compiletime function fill()", " let _snapshot = observed", " migrated = 42", @@ -179,7 +179,7 @@ public void testCompiletimeScalarRuntimeWriteRemainsAuthoritative() { test().testLua(true).luaOnly(false).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) .lines("package Test", "native testSuccess()", - "int source = 1", + "@compiletime int source = 1", "@compiletime function fill()", " source = 42", "init", @@ -194,7 +194,7 @@ public void testCompiletimeClassStaticScalarState() { .lines("package Test", "native testSuccess()", "class Counter", - " static int value = 1", + " @compiletime static int value = 1", " static function setValue(int newValue)", " value = newValue", " static function getValue() returns int", @@ -213,7 +213,7 @@ public void testCompiletimeGenericClassStaticScalarState() { .lines("package Test", "native testSuccess()", "class Counter", - " static T value", + " @compiletime static T value", " static function setValue(T newValue)", " value = newValue", " static function getValue() returns T", diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java index adb2fbdce..ba60d9695 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java @@ -111,10 +111,10 @@ public void compiletimeScalarReplaySplittingIsDeterministicAcrossPackages() { RunArgs runArgs = new RunArgs().with( "-lua", "-runcompiletimefunctions", "-functionSplitLimit", "1"); String[] source = { - "package A", "public int a", "@compiletime function fillA()", " a = 10", "endpackage", - "package B", "public int b", "@compiletime function fillB()", " b = 20", "endpackage", - "package C", "public int c", "@compiletime function fillC()", " c = 30", "endpackage", - "package D", "public int d", "@compiletime function fillD()", " d = 40", "endpackage", + "package A", "@compiletime public int a", "@compiletime function fillA()", " a = 10", "endpackage", + "package B", "@compiletime public int b", "@compiletime function fillB()", " b = 20", "endpackage", + "package C", "@compiletime public int c", "@compiletime function fillC()", " c = 30", "endpackage", + "package D", "@compiletime public int d", "@compiletime function fillD()", " d = 40", "endpackage", "package Test", "import A", "import B", "import C", "import D", "native testSuccess()", "init", " if a + b + c + d == 100", " testSuccess()" }; @@ -139,6 +139,24 @@ public void compiletimeScalarReplaySplittingIsDeterministicAcrossPackages() { assertEquals("all compiletime scalar values must still be emitted", 4, persistedAssignments); } + @Test + public void compiletimeScalarMigrationIsDisabledByDefault() { + String compiled = compileLuaWithRunArgs( + "compiletimeScalarMigrationIsDisabledByDefault", + new RunArgs().with("-lua", "-runcompiletimefunctions"), + "package Test", + "int source = 1", + "@compiletime function fill()", + " source = 42", + "native testSuccess()", + "init", + " if source == 1", + " testSuccess()" + ); + + assertFalse("scalar compiletime state must not be emitted without the opt-in flag", compiled.contains("initCompiletimeScalarState")); + } + @Test public void compiletimeInterpreterSeesLuaTarget() { String compiled = compileLuaWithRunArgs( @@ -149,7 +167,7 @@ public void compiletimeInterpreterSeesLuaTarget() { "endpackage", "package Test", "import MagicFunctions", - "int observedBackend", + "@compiletime int observedBackend", "@compiletime function detectBackend()", " if isLua", " observedBackend = 1", From e92a214364b1deb84d9a057337937a7cf763b137 Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 21 Aug 2026 14:24:01 +0200 Subject: [PATCH 2/3] Make array compiletime migration opt-in too --- .../wurstio/CompiletimeFunctionRunner.java | 8 ++-- .../wurstscript/tests/CompiletimeTests.java | 41 ++++++++++--------- .../tests/LuaBackendAuditTests.java | 28 ++++++++++--- 3 files changed, 49 insertions(+), 28 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java index 90b08e35e..e9eab1d40 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java @@ -752,7 +752,7 @@ private void emitCompiletimeState() { modifiedScalars.sort(stableGlobalOrder); for (ImVar var : modifiedScalars) { if (!imProg.getGlobals().contains(var) || var.getType() instanceof ImArrayLikeType - || !isScalarStateMigrationTarget(var)) { + || !isCompiletimeStateMigrationTarget(var)) { continue; } StateReplayLocation replayLocation = findReplayTarget(var); @@ -781,7 +781,7 @@ private void emitCompiletimeState() { } modifiedArrays.sort(stableGlobalOrder); for (ImVar var : modifiedArrays) { - if (!imProg.getGlobals().contains(var)) { + if (!imProg.getGlobals().contains(var) || !isCompiletimeStateMigrationTarget(var)) { continue; } if (!(var.getType() instanceof ImArrayLikeType)) { @@ -813,8 +813,8 @@ private void emitCompiletimeState() { } } - private boolean isScalarStateMigrationTarget(ImVar var) { - // Scalar compiletime state is deliberately opt-in per global. This keeps + private boolean isCompiletimeStateMigrationTarget(ImVar var) { + // Compiletime state is deliberately opt-in per global. This keeps // compiletime-only scratch values and incidental writes out of runtime init. return var.getTrace() instanceof GlobalVarDef && ((GlobalVarDef) var.getTrace()).hasAnnotation("@compiletime"); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java index bc863df3a..c846501f9 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java @@ -81,8 +81,11 @@ public void testUnsupportedCompiletimeArrayWarningIsAggregatedAndReadable() { try { test().withStdLib().testLua(true).luaOnly(true).runCompiletimeFunctions(true) .lines("package Test", + "@compiletime player array source", + "@compiletime function fill()", + " source[0] = Player(0)", "init", - " let _firstPlayer = players[0]"); + " source[0] = Player(0)"); } finally { logger.detachAppender(appender); appender.stop(); @@ -90,12 +93,12 @@ public void testUnsupportedCompiletimeArrayWarningIsAggregatedAndReadable() { List playerWarnings = appender.list.stream() .map(ILoggingEvent::getFormattedMessage) - .filter(message -> message.contains("Player_players")) + .filter(message -> message.contains("Test_source")) .toList(); assertEquals(playerWarnings.size(), 1, "expected one warning for the entire array"); String warning = playerWarnings.get(0); - assertTrue(warning.contains("28 unsupported compiletime entries"), warning); - assertTrue(warning.contains("Player, line"), warning); + assertTrue(warning.contains("1 unsupported compiletime entries"), warning); + assertTrue(warning.contains("IlConstHandle"), warning); assertFalse(warning.contains("GlobalVarDef"), warning); } @@ -230,7 +233,7 @@ public void testCompiletimeArrayState() { test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) .lines("package Test", "native testSuccess()", - "int array source", + "@compiletime int array source", "@compiletime function fill()", " source[0] = 42", "init", @@ -243,7 +246,7 @@ public void testCompiletimeArrayStateAfterSourceInitializer() { test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) .lines("package Test", "native testSuccess()", - "int array source = [1]", + "@compiletime int array source = [1]", "@compiletime function fill()", " source[0] = 42", "init", @@ -256,7 +259,7 @@ public void testCompiletimeArrayStateLua() { test().testLua(true).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) .lines("package Test", "native testSuccess()", - "int array source = [1]", + "@compiletime int array source = [1]", "@compiletime function fill()", " source[0] = 42", "init", @@ -270,7 +273,7 @@ public void testCompiletimeGenericArrayState() { .lines("package Test", "native testSuccess()", "class Box", - " static T array store", + " @compiletime static T array store", " static function set(int index, T value)", " store[index] = value", " static function get(int index) returns T", @@ -288,7 +291,7 @@ public void testCompiletimeGenericArrayStateLua() { .lines("package Test", "native testSuccess()", "class Box", - " static T array store", + " @compiletime static T array store", " static function set(int index, T value)", " store[index] = value", " static function get(int index) returns T", @@ -307,7 +310,7 @@ public void testCompiletimeObjectArrayState() { "native testSuccess()", "class A", " int value", - "A array source", + "@compiletime A array source", "@compiletime function fill()", " source[0] = new A", " source[0].value = 42", @@ -326,7 +329,7 @@ public void testCompiletimeHashtableArrayState() { "@extern native InitHashtable() returns hashtable", "@extern native LoadInteger(hashtable h, int p, int c) returns int", "@extern native SaveInteger(hashtable h, int p, int c, int i)", - "hashtable array source", + "@compiletime hashtable array source", "@compiletime function fill()", " source[0] = InitHashtable()", " SaveInteger(source[0], 2, 3, 42)", @@ -340,7 +343,7 @@ public void testCompiletimeNullArrayState() { test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) .lines("package Test", "native testSuccess()", - "string array source = [\"value\"]", + "@compiletime string array source = [\"value\"]", "@compiletime function clear()", " source[0] = null", "init", @@ -354,7 +357,7 @@ public void testCompiletimeTupleArrayState() { .lines("package Test", "native testSuccess()", "tuple pair(int left, int right)", - "pair array source", + "@compiletime pair array source", "@compiletime function fill()", " source[0] = pair(42, 7)", "init", @@ -366,7 +369,7 @@ public void testCompiletimeTupleArrayState() { public void testCompiletimeArrayStateAcrossPackages() { test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) .lines("package A", - "public int array source = [1]", + "@compiletime public int array source = [1]", "@compiletime function fillA()", " source[0] = 42", "init", @@ -384,7 +387,7 @@ public void testCompiletimeArrayStateAcrossPackages() { public void testCompiletimeArrayStateAcrossPackagesWithTwoInitializers() { test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) .lines("package A", - "public int array source = [1]", + "@compiletime public int array source = [1]", "@compiletime function fillA()", " source[0] = 42", "init", @@ -392,7 +395,7 @@ public void testCompiletimeArrayStateAcrossPackagesWithTwoInitializers() { "endpackage", "package B", "import A", - "int array other = [2]", + "@compiletime int array other = [2]", "@compiletime function fillB()", " other[0] = 9", "native testSuccess()", @@ -407,9 +410,9 @@ public void testCompiletimeArrayReplayPrecedesDependentInitializer() { test().testLua(true).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) .lines("package Test", "native testSuccess()", - "int array first = [1]", + "@compiletime int array first = [1]", "int observed = first[0]", - "int array second = [2]", + "@compiletime int array second = [2]", "@compiletime function fill()", " first[0] = 42", " second[0] = 9", @@ -429,7 +432,7 @@ public void testCompiletimeArrayReplayOnlyWrittenEntries() { "package B", "import A", "native testSuccess()", - "int array source = [seed, 0]", + "@compiletime int array source = [seed, 0]", "@compiletime function fill()", " source[1] = 42", "init", diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java index ba60d9695..dbfd02e61 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java @@ -60,7 +60,7 @@ public void compiletimeGenericArrayReplayLeavesAreSplit() { new RunArgs().with("-lua", "-runcompiletimefunctions", "-functionSplitLimit", "1"), "package Test", "class Box", - " static T array store", + " @compiletime static T array store", " static function set(int index, T value)", " store[index] = value", " static function get(int index) returns T", @@ -93,10 +93,10 @@ public void compiletimeArrayReplaySplittingIsDeterministicAcrossPackages() { RunArgs runArgs = new RunArgs().with( "-lua", "-runcompiletimefunctions", "-functionSplitLimit", "1"); String[] source = { - "package A", "public int array a = [1]", "@compiletime function fillA()", " a[0] = 10", "endpackage", - "package B", "public int array b = [1]", "@compiletime function fillB()", " b[0] = 20", "endpackage", - "package C", "public int array c = [1]", "@compiletime function fillC()", " c[0] = 30", "endpackage", - "package D", "public int array d = [1]", "@compiletime function fillD()", " d[0] = 40", "endpackage", + "package A", "@compiletime public int array a = [1]", "@compiletime function fillA()", " a[0] = 10", "endpackage", + "package B", "@compiletime public int array b = [1]", "@compiletime function fillB()", " b[0] = 20", "endpackage", + "package C", "@compiletime public int array c = [1]", "@compiletime function fillC()", " c[0] = 30", "endpackage", + "package D", "@compiletime public int array d = [1]", "@compiletime function fillD()", " d[0] = 40", "endpackage", "package Test", "import A", "import B", "import C", "import D", "native testSuccess()", "init", " if a[0] + b[0] + c[0] + d[0] == 100", " testSuccess()" }; @@ -157,6 +157,24 @@ public void compiletimeScalarMigrationIsDisabledByDefault() { assertFalse("scalar compiletime state must not be emitted without the opt-in flag", compiled.contains("initCompiletimeScalarState")); } + @Test + public void compiletimeArrayMigrationIsDisabledByDefault() { + String compiled = compileLuaWithRunArgs( + "compiletimeArrayMigrationIsDisabledByDefault", + new RunArgs().with("-lua", "-runcompiletimefunctions"), + "package Test", + "int array source = [1]", + "@compiletime function fill()", + " source[0] = 42", + "native testSuccess()", + "init", + " if source[0] == 1", + " testSuccess()" + ); + + assertFalse("array compiletime state must not be emitted without the opt-in annotation", compiled.contains("initCompiletimeArrayState")); + } + @Test public void compiletimeInterpreterSeesLuaTarget() { String compiled = compileLuaWithRunArgs( From 3d5664cf434fbbcb7f6c7473c69e150b8e425179 Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 21 Aug 2026 14:38:03 +0200 Subject: [PATCH 3/3] Warn on compiletime annotations on instance fields --- .../de/peeeq/wurstscript/validation/WurstValidator.java | 4 ++++ .../java/tests/wurstscript/tests/CompiletimeTests.java | 7 +++++++ 2 files changed, 11 insertions(+) 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 0c7d89b1f..816b8de84 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 @@ -3000,6 +3000,10 @@ public void case_GlobalVarDef(GlobalVarDef g) { } else { check(VisibilityPublic.class, ModConstant.class, ModReadonly.class, Annotation.class); } + if (g.attrIsDynamicClassMember() && g.hasAnnotation("@compiletime")) { + g.getAnnotation("@compiletime") + .addWarning("The annotation '@compiletime' has no effect on instance fields."); + } } @Override diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java index c846501f9..34bed365c 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java @@ -14,6 +14,13 @@ public class CompiletimeTests extends WurstScriptTest { + @Test + public void compiletimeAnnotationWarnsOnInstanceFields() { + test().expectWarning("has no effect on instance fields") + .lines("package Test", + "class C", + " @compiletime int value"); + } @Test public void testSimpleCompiletime() {