From 46a9090ed3f1f7580095ff89e82878c8576860ad Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 29 Aug 2026 22:57:35 +0200 Subject: [PATCH 1/3] Keep lowered vararg tuple locals rooted --- .../parserspec/jass_im.parseq | 5 ++- .../interpreter/RunStatement.java | 5 ++- .../optimizer/LocalMerger.java | 10 ++++- .../optimizer/LocalPlayerContextAnalyzer.java | 6 +-- .../optimizer/SideEffectAnalyzer.java | 4 +- .../translation/imoptimizer/ImInliner.java | 6 ++- .../imtranslation/AssertProperty.java | 4 +- .../imtranslation/EliminateTuples.java | 14 +++++++ .../translation/imtranslation/Flatten.java | 10 ++++- .../translation/imtranslation/ImPrinter.java | 8 +++- .../imtranslation/ReferenceRewritingCopy.java | 6 +-- .../imtranslation/StmtTranslation.java | 3 +- .../imtranslation/UsedVariables.java | 8 ++++ .../imtranslation/VarargEliminator.java | 6 ++- .../lua/translation/StmtTranslation.java | 5 ++- .../tests/LuaBackendAuditTests.java | 40 +++++++++++++++++++ 16 files changed, 121 insertions(+), 19 deletions(-) diff --git a/de.peeeq.wurstscript/parserspec/jass_im.parseq b/de.peeeq.wurstscript/parserspec/jass_im.parseq index c8d4eadae..7891d37d1 100644 --- a/de.peeeq.wurstscript/parserspec/jass_im.parseq +++ b/de.peeeq.wurstscript/parserspec/jass_im.parseq @@ -74,6 +74,9 @@ ImMethod(@ignoreForEquality de.peeeq.wurstscript.ast.Element trace, ImStmts * ImStmt +ImVarargLoopVars * ImVarargLoopVar + +ImVarargLoopVar(ref ImVar var) ImStmt = ImIf(@ignoreForEquality de.peeeq.wurstscript.ast.Element trace, ImExpr condition, ImStmts thenBlock, ImStmts elseBlock) @@ -82,7 +85,7 @@ ImStmt = | ImReturn(@ignoreForEquality de.peeeq.wurstscript.ast.Element trace, ImExprOpt returnValue) | ImSet(@ignoreForEquality de.peeeq.wurstscript.ast.Element trace, ImLExpr left, ImExpr right) | ImExpr - | ImVarargLoop(@ignoreForEquality de.peeeq.wurstscript.ast.Element trace, ImStmts body, ref ImVar loopVar) + | ImVarargLoop(@ignoreForEquality de.peeeq.wurstscript.ast.Element trace, ImStmts body, ImVarargLoopVars loopVars) ImExprOpt = diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java index cce66cc9a..9f23664e6 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java @@ -1,5 +1,6 @@ package de.peeeq.wurstscript.intermediatelang.interpreter; +import com.google.common.base.Preconditions; import de.peeeq.wurstio.jassinterpreter.InterpreterException; import de.peeeq.wurstio.jassinterpreter.VarargArray; import de.peeeq.wurstscript.intermediatelang.ILaddress; @@ -72,11 +73,13 @@ public static void run(ImStmts stmts, ProgramState globalState, LocalState local public static void run(ImVarargLoop loop, ProgramState globalState, LocalState localState) { + Preconditions.checkState(loop.getLoopVars().size() == 1, + "Expected one vararg loop variable in the interpreter."); ImFunction func = loop.getNearestFunc(); ImVar varargParam = func.getParameters().get(func.getParameters().size() - 1); VarargArray val = (VarargArray) localState.getVal(varargParam); for (int i = 0; i < val.size(); i++) { - localState.setVal(loop.getLoopVar(), val.get(i)); + localState.setVal(loop.getLoopVars().get(0).getVar(), val.get(i)); loop.getBody().runStatements(globalState, localState); } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalMerger.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalMerger.java index f2e02943e..c701803c3 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalMerger.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalMerger.java @@ -110,8 +110,10 @@ private static void applyMerges(ImFunction func, Map merges) { } @Override public void visit(ImVarargLoop varargLoop) { super.visit(varargLoop); - ImVar m = merges.get(varargLoop.getLoopVar()); - if (m != null) varargLoop.setLoopVar(m); + for (ImVarargLoopVar loopVar : varargLoop.getLoopVars()) { + ImVar m = merges.get(loopVar.getVar()); + if (m != null) loopVar.setVar(m); + } } }); } @@ -123,6 +125,10 @@ private static int removeUnusedLocals(ImFunction f) { @Override public void visit(ImVarAccess va) { super.visit(va); used.add(va.getVar()); } @Override public void visit(ImMemberAccess ma) { super.visit(ma); used.add(ma.getVar()); } @Override public void visit(ImVarArrayAccess vaa) { super.visit(vaa); used.add(vaa.getVar()); } + @Override public void visit(ImVarargLoop loop) { + super.visit(loop); + loop.getLoopVars().forEach(v -> used.add(v.getVar())); + } }); List locals = new ArrayList<>(f.getLocals()); int before = locals.size(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java index fcb1055f0..365fdf496 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java @@ -261,9 +261,9 @@ private void indexElementAfterChildren(Element element, ImFunction owner, Object } else if (element instanceof ImVarargLoop) { ImVar varargParameter = varargParameter(owner); if (varargParameter != null) { - addDependency( - variableFact(varargParameter), - variableFact(((ImVarargLoop) element).getLoopVar())); + for (ImVarargLoopVar loopVar : ((ImVarargLoop) element).getLoopVars()) { + addDependency(variableFact(varargParameter), variableFact(loopVar.getVar())); + } } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/SideEffectAnalyzer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/SideEffectAnalyzer.java index 72d6db8be..b87dd7312 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/SideEffectAnalyzer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/SideEffectAnalyzer.java @@ -316,7 +316,7 @@ public void visit(ImSet va) { @Override public void visit(ImVarargLoop va) { super.visit(va); - imVars.add(va.getLoopVar()); + va.getLoopVars().forEach(v -> imVars.add(v.getVar())); } }); @@ -405,7 +405,7 @@ public void visit(ImSet va) { @Override public void visit(ImVarargLoop va) { super.visit(va); - imVars.add(va.getLoopVar()); + va.getLoopVars().forEach(v -> imVars.add(v.getVar())); } }); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java index fde47196b..7d3617bc3 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java @@ -10,6 +10,7 @@ import de.peeeq.wurstscript.types.TypesHelper; import java.util.*; +import java.util.stream.Collectors; import static de.peeeq.wurstscript.jassIm.JassIm.ImStatementExpr; import static de.peeeq.wurstscript.jassIm.JassIm.ImStmts; @@ -278,7 +279,10 @@ private ImStmts rewriteStmtForEarlyReturn(ImStmt s, ImVar doneVar, ImVar retVar) ImStmts loopBody = JassIm.ImStmts(); loopBody.add(JassIm.ImExitwhen(l.getTrace(), JassIm.ImVarAccess(doneVar))); loopBody.addAll(rewriteForEarlyReturns(l.getBody().copy(), doneVar, retVar).removeAll()); - return JassIm.ImStmts(JassIm.ImVarargLoop(l.getTrace(), loopBody, l.getLoopVar())); + return JassIm.ImStmts(JassIm.ImVarargLoop(l.getTrace(), loopBody, + JassIm.ImVarargLoopVars(l.getLoopVars().stream() + .map(v -> JassIm.ImVarargLoopVar(v.getVar())) + .collect(Collectors.toList())))); } // Keep tree ownership valid when rewrapping statements into new blocks. return JassIm.ImStmts(s.copy()); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/AssertProperty.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/AssertProperty.java index 933e6602c..cfd5e9ad2 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/AssertProperty.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/AssertProperty.java @@ -43,7 +43,9 @@ public void check(Element e) { checkType(e, ((ImMethod) e).getMethodClass()); checkRooted(e, ((ImMethod) e).getImplementation()); } else if (e instanceof ImVarargLoop) { - checkRooted(e, ((ImVarargLoop) e).getLoopVar()); + for (ImVarargLoopVar loopVar : ((ImVarargLoop) e).getLoopVars()) { + checkRooted(e, loopVar.getVar()); + } } else if (e instanceof ImTypeVarDispatch) { checkRooted(e, ((ImTypeVarDispatch) e).getTypeClassFunc()); checkRooted(e, ((ImTypeVarDispatch) e).getTypeVariable()); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateTuples.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateTuples.java index 5e2c8f5eb..647e2e369 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateTuples.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateTuples.java @@ -427,6 +427,20 @@ public void visit(ImVarAccess va) { } } + @Override + public void visit(ImVarargLoop loop) { + super.visit(loop); + Preconditions.checkState(loop.getLoopVars().size() == 1, + "Expected one vararg loop variable before tuple elimination."); + ImVar loopVar = loop.getLoopVars().get(0).getVar(); + if (TypesHelper.typeContainsTuples(loopVar.getType())) { + loop.setLoopVars(JassIm.ImVarargLoopVars( + translator.getTupleScalarVars(loopVar).stream() + .map(JassIm::ImVarargLoopVar) + .collect(Collectors.toList()))); + } + } + @Override public void visit(ImVarArrayAccess va) { super.visit(va); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/Flatten.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/Flatten.java index 34c69dac8..5b37c05e3 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/Flatten.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/Flatten.java @@ -10,6 +10,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import static de.peeeq.wurstscript.jassIm.JassIm.*; @@ -659,7 +660,14 @@ public static Result flatten(ImGetStackTrace e, ImTranslator translator, public static Result flatten(ImVarargLoop s, ImTranslator translator, ImFunction f) { return new Result(Collections.singletonList( - JassIm.ImVarargLoop(s.getTrace(), flattenStatements(s.getBody(), translator, f), s.getLoopVar()))); + JassIm.ImVarargLoop(s.getTrace(), flattenStatements(s.getBody(), translator, f), + copyVarargLoopVars(s.getLoopVars())))); + } + + private static ImVarargLoopVars copyVarargLoopVars(ImVarargLoopVars loopVars) { + return JassIm.ImVarargLoopVars(loopVars.stream() + .map(v -> JassIm.ImVarargLoopVar(v.getVar())) + .collect(Collectors.toList())); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImPrinter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImPrinter.java index bb8b87843..1224429b3 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImPrinter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImPrinter.java @@ -507,7 +507,9 @@ public static void print(ImCompiletimeExpr e, Appendable sb, int indent) { public static void print(ImVarargLoop e, Appendable sb, int indent) { append(sb, "foreach vararg "); - e.getLoopVar().print(sb, indent); + append(sb, e.getLoopVars().stream() + .map(v -> v.getVar().getName()) + .collect(Collectors.joining(", "))); append(sb, " {\n"); e.getBody().print(sb, indent + 1); indent(sb, indent); @@ -599,6 +601,10 @@ public static String asString(ImTypeArgument s) { return s.getType() + "" + s.getTypeClassBinding(); } + public static String asString(ImVarargLoopVar s) { + return s.getVar().getName() + smallHash(s.getVar()); + } + public static void print(ImCast e, Appendable sb, int indent) { append(sb, "("); e.getExpr().print(sb, indent); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ReferenceRewritingCopy.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ReferenceRewritingCopy.java index 5bd4a6c3d..b86ee414e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ReferenceRewritingCopy.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ReferenceRewritingCopy.java @@ -50,11 +50,11 @@ public void visit(ImMethod e) { @Override - public void visit(ImVarargLoop e) { + public void visit(ImVarargLoopVar e) { super.visit(e); - Element newChild = oldToNew.get(e.getLoopVar()); + Element newChild = oldToNew.get(e.getVar()); if (newChild != null) { - e.setLoopVar((ImVar) newChild); + e.setVar((ImVar) newChild); } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java index ffe88cfa0..c59c9ce2f 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java @@ -264,7 +264,8 @@ private static ImStmt case_StmtForVararg(StmtForIn s, ImTranslator t, ImFunction List result = Lists.newArrayList(); ImVar loopVar = t.getVarFor(s.getLoopVar()); - result.add(ImVarargLoop(s, ImStmts(t.translateStatements(f, s.getBody())), loopVar)); + result.add(ImVarargLoop(s, ImStmts(t.translateStatements(f, s.getBody())), + ImVarargLoopVars(ImVarargLoopVar(loopVar)))); f.getLocals().add(loopVar); return ImHelper.statementExprVoid(ImStmts(result)); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/UsedVariables.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/UsedVariables.java index beb59d4a5..4895b705a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/UsedVariables.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/UsedVariables.java @@ -28,6 +28,8 @@ private static void collectAllVars(Element e, Set result) { result.add(((ImVarArrayAccess) e).getVar()); } else if (e instanceof ImMemberAccess) { result.add(((ImMemberAccess) e).getVar()); + } else if (e instanceof ImVarargLoop) { + ((ImVarargLoop) e).getLoopVars().forEach(v -> result.add(v.getVar())); } // Continue traversal @@ -85,6 +87,12 @@ public void visit(ImVarAccess e) { result.add(e.getVar()); } + @Override + public void visit(ImVarargLoop e) { + e.getLoopVars().forEach(v -> result.add(v.getVar())); + super.visit(e); + } + @Override public void visit(ImVarArrayAccess e) { result.add(e.getVar()); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/VarargEliminator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/VarargEliminator.java index 6a131105c..36b3fcffd 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/VarargEliminator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/VarargEliminator.java @@ -2,6 +2,7 @@ import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; +import com.google.common.base.Preconditions; import de.peeeq.wurstscript.attributes.CompileError; import de.peeeq.wurstscript.jassIm.*; import org.jetbrains.annotations.NotNull; @@ -170,6 +171,9 @@ private void redirectCall(ImFunctionCall call, ImFunction newFunc) { } private void unrollVarargLoop(ImVarargLoop imLoop, List newParams) { + Preconditions.checkState(imLoop.getLoopVars().size() == 1, + "Expected one vararg loop variable before vararg elimination."); + ImVar loopVar = imLoop.getLoopVars().get(0).getVar(); ImStatementExpr stmtExpr = ImHelper.statementExprVoid(JassIm.ImStmts()); for (int i = 0; i < newParams.size(); i++) { @@ -179,7 +183,7 @@ private void unrollVarargLoop(ImVarargLoop imLoop, List newParams) { @Override public void visit(ImVarAccess access) { super.visit(access); - if (access.getVar() == imLoop.getLoopVar()) { + if (access.getVar() == loopVar) { access.setVar(newParams.get(finalI)); } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java index cd722af06..78ab416ef 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java @@ -4,6 +4,7 @@ import de.peeeq.wurstscript.luaAst.*; import java.util.List; +import java.util.stream.Collectors; import static de.peeeq.wurstscript.translation.lua.translation.ExprTranslation.WURST_ABORT_THREAD_SENTINEL; import de.peeeq.wurstscript.jassIm.ImFunction; @@ -80,7 +81,9 @@ public static void translate(ImSet s, List res, LuaTranslator tr) public static void translate(ImVarargLoop loop, List res, LuaTranslator tr) { - List loopVars = tr.imTr.getTupleScalarVars(loop.getLoopVar()); + List loopVars = loop.getLoopVars().stream() + .map(ImVarargLoopVar::getVar) + .collect(Collectors.toList()); // The loop is built from real AST nodes (a while loop) instead of literal // 'for ... do' / 'end' lines: the printer stops printing a statement list // after a return/break (Lua forbids trailing statements), which would 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 28fbe5cdb..683595f26 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 @@ -9,6 +9,8 @@ import de.peeeq.wurstscript.jassIm.ImProg; import de.peeeq.wurstscript.jassIm.ImSet; import de.peeeq.wurstscript.jassIm.ImVar; +import de.peeeq.wurstscript.jassIm.ImVarargLoop; +import de.peeeq.wurstscript.jassIm.ImVarargLoopVar; import de.peeeq.wurstscript.jassIm.JassIm; import de.peeeq.wurstscript.luaAst.LuaCompilationUnit; import de.peeeq.wurstscript.translation.imtranslation.ImHelper; @@ -56,6 +58,21 @@ private String compileLuaWithRunArgs(String testName, RunArgs runArgs, String... compiler.translateProgToIm(model); compiler.runCompiletime(WurstProjectConfigData.empty(), false, false); LuaCompilationUnit luaCode = compiler.transformProgToLua(); + compiler.getImProg().accept(new ImVarargLoop.DefaultVisitor() { + @Override + public void visit(ImVarargLoop loop) { + de.peeeq.wurstscript.jassIm.Element owner = loop; + while (owner != null && !(owner instanceof de.peeeq.wurstscript.jassIm.ImFunction)) { + owner = owner.getParent(); + } + for (ImVarargLoopVar loopVar : loop.getLoopVars()) { + assertTrue("lowered vararg loop variable must remain attached to its declaration list: " + + loopVar.getVar() + " in " + owner, + loopVar.getVar().getParent() != null); + } + super.visit(loop); + } + }); StringBuilder result = new StringBuilder(); luaCode.print(result, 0); return result.toString(); @@ -207,6 +224,29 @@ public void optimizedTupleCommonPathIsOnlyScalarCode() { assertFalse(compiled.contains("tupleEquals")); } + @Test + public void optimizedTupleVarargLoopUsesAttachedScalarLocals() { + String compiled = compileOptimizedLua( + "optimizedTupleVarargLoopUsesAttachedScalarLocals", + "package Test", + "nativetype framehandle extends handle", + "native makeFrame() returns framehandle", + "tuple handles(framehandle first, framehandle second)", + "class Bag", + " private static T array store", + " int size = 0", + " @noinline function add(vararg T elems)", + " for elem in elems", + " store[size] = elem", + " size++", + "init", + " let bag = new Bag()", + " bag.add(handles(makeFrame(), makeFrame()))" + ); + assertTrue(compiled.contains("table.pack(...)")); + assertFalse(compiled.contains("tupleCopy")); + } + @Test public void randomizedTupleValueSemanticsStayScalar() throws IOException { Random random = new Random(0x5CA1A2L); From e749741f7165f15d83664fdb5b6080033f7a7b29 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 29 Aug 2026 23:04:00 +0200 Subject: [PATCH 2/3] Run compiler fuzz tests in the SmallCheck JUnit suite --- .../utils/SmallCheckViaJUnitCoreTestNG.java | 12 +- .../tests/CompilerFuzzTestsSC.java | 338 ++++++++++++++++++ 2 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompilerFuzzTestsSC.java diff --git a/de.peeeq.wurstscript/src/test/java/tests/utils/SmallCheckViaJUnitCoreTestNG.java b/de.peeeq.wurstscript/src/test/java/tests/utils/SmallCheckViaJUnitCoreTestNG.java index 8520aeb8c..6d84da371 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/utils/SmallCheckViaJUnitCoreTestNG.java +++ b/de.peeeq.wurstscript/src/test/java/tests/utils/SmallCheckViaJUnitCoreTestNG.java @@ -1,5 +1,6 @@ package tests.utils; +import tests.wurstscript.tests.CompilerFuzzTestsSC; import org.junit.runner.JUnitCore; import org.junit.runner.Result; import org.junit.runner.notification.Failure; @@ -11,13 +12,20 @@ public class SmallCheckViaJUnitCoreTestNG { @Test - public void runGraphInterpreterTestsSC() { + public void runSmallCheckSuite() { Result r = JUnitCore.runClasses(GraphInterpreterTestsSC.class); + assertNoFailures(r, "GraphInterpreter"); + + Result compilerFuzz = JUnitCore.runClasses(CompilerFuzzTestsSC.class); + assertNoFailures(compilerFuzz, "CompilerFuzz"); + } + + private void assertNoFailures(Result r, String suiteName) { if (!r.wasSuccessful()) { String msg = r.getFailures().stream() .map(Failure::toString) .collect(Collectors.joining("\n\n")); - Assert.fail("SmallCheck failures:\n" + msg); + Assert.fail("SmallCheck failures (" + suiteName + "):\n" + msg); } } } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompilerFuzzTestsSC.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompilerFuzzTestsSC.java new file mode 100644 index 000000000..d5effdef1 --- /dev/null +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompilerFuzzTestsSC.java @@ -0,0 +1,338 @@ +package tests.wurstscript.tests; + +import org.junit.runner.RunWith; +import org.testng.Assert; +import org.testng.annotations.Test; +import smallcheck.SmallCheckRunner; +import smallcheck.annotations.From; +import smallcheck.annotations.Property; +import smallcheck.generators.SeriesGen; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +@RunWith(SmallCheckRunner.class) +public class CompilerFuzzTestsSC extends WurstScriptTest { + + @Property(maxInvocations = 320) + public void generatedProgramsAreCrashFree(@From(RandomProgram.class) Program program) { + CompilationResult result = runProgram(program); + + Assert.assertNotNull(result); + Assert.assertNotNull(result.getGui()); + } + + @Property(maxInvocations = 180) + public void mixedNewlineStylesAreCrashFree(@From(RandomProgram.class) Program program) { + String alternateNewline = "\n".equals(program.newline) ? "\r\n" : "\n"; + CompilationResult result = runProgram(program.withNewline(alternateNewline)); + + Assert.assertNotNull(result); + Assert.assertNotNull(result.getGui()); + } + + @Property(maxInvocations = 120) + public void crossPackageProgramsAreCrashFree(@From(CrossPackageProgram.class) Program program) { + CompilationResult result = runProgram(program); + + Assert.assertNotNull(result); + Assert.assertNotNull(result.getGui()); + } + + @Test + public void deepNestedControlFlowIsCrashFree() { + CompilationResult result = runProgram(new Program( + new String[]{"deep_nested.wurst"}, + new String[]{buildDeepNestedControlFlow("\n")}, + "\n" + )); + + Assert.assertNotNull(result); + Assert.assertNotNull(result.getGui()); + } + + @Test + public void crossPackageImportChainDoesNotCrash() { + String[] names = {"chain_support.wurst", "chain_main.wurst"}; + String[] sources = { + buildChainSupportPackage("\n"), + buildChainMainPackage("\n") + }; + CompilationResult result = runProgram(new Program(names, sources, "\n")); + + Assert.assertNotNull(result); + Assert.assertNotNull(result.getGui()); + } + + private CompilationResult runProgram(Program program) { + return test() + .setStopOnFirstError(false) + .executeProg(false) + .compilationUnits(asCompilationUnits(program)) + .run(); + } + + private CU[] asCompilationUnits(Program program) { + CU[] units = new CU[program.sources.length]; + for (int i = 0; i < program.sources.length; i++) { + units[i] = new CU(program.unitNames[i], program.sources[i]); + } + return units; + } + + private static String replaceNewline(String input, String newline) { + return input.replace("\r\n", "\n").replace("\r", "\n").replace("\n", newline); + } + + private static String join(List lines, String newline) { + return String.join(newline, lines); + } + + private static String indent(int count) { + return " ".repeat(count); + } + + private static String buildDeepNestedControlFlow(String newline) { + List lines = new ArrayList<>(); + lines.add("package DeepNested"); + lines.add("init"); + lines.add(indent(4) + "int marker = 0"); + for (int depth = 0; depth < 150; depth++) { + String base = " ".repeat(4 + depth * 4); + lines.add(base + "if marker == " + depth); + lines.add(base + " marker = marker + 1"); + if (depth % 4 == 0) { + lines.add(base + " int j = " + depth); + lines.add(base + " while j > 0"); + lines.add(base + " j = j - 1"); + lines.add(base + " marker = marker + j"); + } + } + lines.add(indent(4) + "if marker >= 0"); + lines.add(indent(8) + "marker = marker"); + return join(lines, newline); + } + + private static String buildChainSupportPackage(String newline) { + List lines = Arrays.asList( + "package ChainSupport", + "public class Loader", + " int value", + " construct(int value)", + " this.value = value", + " public function withLoader(Loader loader) returns int", + " return loader.value + 1", + "public interface ChainMarker", + " function mark(int value) returns int" + ); + return String.join(newline, lines); + } + + private static String buildChainMainPackage(String newline) { + List lines = Arrays.asList( + "package ChainMain", + "import ChainSupport", + "class Adapter implements ChainMarker", + " function mark(int value) returns int", + " return value + 1", + "function callMarker(ChainMarker marker, int value) returns int", + " return marker.mark(value)", + "init", + " let loader = new Loader(3)", + " let marker = new Adapter()", + " int result = withLoader(loader)", + " if callMarker(marker, result) == 5", + " result = result + 1", + " else", + " result = result - 1" + ); + return String.join(newline, lines); + } + + private static String buildRandomSingleProgram(int seed, String newline) { + String indent = ((seed & 1) == 0) ? " " : " "; + String pkg = "FuzzPkg_" + Math.abs(seed % 997); + List lines = new ArrayList<>(); + lines.add("package " + pkg); + lines.add("int base = " + (seed % 41 + 2)); + + boolean includeTuple = (seed & 2) != 0; + boolean includeInterface = (seed & 4) != 0; + boolean includeModule = (seed & 8) != 0; + boolean includeLoop = (seed & 16) != 0; + boolean includeCallback = (seed & 32) != 0; + + if (includeTuple) { + lines.add("tuple Pair(int left, int right)"); + } + + lines.add("function plus(int a, int b) returns int"); + lines.add(indent + "return a + b"); + + if (includeInterface) { + lines.add("interface IHandler"); + lines.add(indent + "function handle(int value) returns int"); + } + + lines.add("class Counter"); + lines.add(indent + "int value"); + lines.add(indent + "construct(int start)"); + lines.add(indent + indent + "value = start"); + lines.add(indent + "function inc() returns int"); + lines.add(indent + indent + "value += 1"); + lines.add(indent + indent + "return value"); + + if (includeModule) { + lines.add("module Shared"); + lines.add(indent + "int sharedValue = base"); + lines.add(indent + "function bump(int value) returns int"); + lines.add(indent + indent + "return value + sharedValue"); + } + + if (includeInterface) { + lines.add("class Sink implements IHandler"); + lines.add(indent + "function handle(int value) returns int"); + lines.add(indent + indent + "return value + base"); + } + + if (includeCallback) { + lines.add("interface IntCallback"); + lines.add(indent + "function apply(int value) returns int"); + lines.add("function invoke(IntCallback callback, int value) returns int"); + lines.add(indent + "return callback.apply(value)"); + } + + if (includeModule) { + lines.add("class SharedUser"); + lines.add(indent + "use Shared"); + lines.add(indent + "function shifted(int value) returns int"); + lines.add(indent + indent + "return bump(value)"); + } + + lines.add("init"); + lines.add(indent + "let counter = new Counter(base)"); + lines.add(indent + "int total = plus(base, counter.inc())"); + + if (includeCallback) { + lines.add(indent + "IntCallback cb = value -> value + 1"); + lines.add(indent + "total = invoke(cb, total)"); + } + + if (includeInterface) { + lines.add(indent + "IHandler handler = new Sink()"); + lines.add(indent + "total = handler.handle(total)"); + } + + if (includeTuple) { + lines.add(indent + "let pair = Pair(base, total)"); + lines.add(indent + "total = plus(pair.left, pair.right)"); + } + + if (includeModule) { + lines.add(indent + "let user = new SharedUser()"); + lines.add(indent + "total = user.shifted(total)"); + } + + if (includeLoop) { + lines.add(indent + "int sum = 0"); + lines.add(indent + "for int i = 0 to 4"); + lines.add(indent + indent + "sum = sum + i"); + lines.add(indent + "while sum < 4"); + lines.add(indent + indent + "sum = sum + 1"); + lines.add(indent + "total = total + sum"); + } + + lines.add(indent + "if total > 0"); + lines.add(indent + indent + "total = total"); + lines.add(indent + "else"); + lines.add(indent + indent + "total = 0"); + return join(lines, newline); + } + + private static String buildRandomSupportPackage(int seed, String newline) { + String pkg = "FuzzLib_" + Math.abs(seed % 997); + String indent = " "; + List lines = new ArrayList<>(); + lines.add("package " + pkg); + lines.add("public class Value"); + lines.add(indent + "int data"); + lines.add(indent + "construct(int data)"); + lines.add(indent + indent + "this.data = data"); + lines.add(indent + "public function valuePlus(int amount) returns int"); + lines.add(indent + indent + "return data + amount"); + lines.add(indent + "public function data() returns int"); + lines.add(indent + indent + "return data"); + return join(lines, newline); + } + + private static String buildRandomMainPackage(int seed, String newline, String supportPackage) { + String pkg = "FuzzMain_" + Math.abs(seed % 997); + String indent = " "; + List lines = new ArrayList<>(); + lines.add("package " + pkg); + lines.add("import " + supportPackage); + lines.add("interface Visitor"); + lines.add(indent + "function visit(int value) returns int"); + lines.add("function run(Visitor visitor, int value) returns int"); + lines.add(indent + "return visitor.visit(value)"); + lines.add("class Delegate"); + lines.add(indent + "public function visit(int value) returns int"); + lines.add(indent + indent + "return value + 1"); + lines.add("init"); + lines.add(indent + "let value = new Value(" + (seed % 17 + 1) + ")"); + lines.add(indent + "let delegate = new Delegate()"); + lines.add(indent + "int result = run(delegate, value.value())"); + lines.add(indent + "let pair = value.valuePlus(result)"); + lines.add(indent + "if pair > 0"); + lines.add(indent + indent + "result = result + pair"); + return join(lines, newline); + } + + public static class RandomProgram extends SeriesGen { + @Override + public Stream generate(int depth) { + int seed = Math.max(0, depth) + 17_213; + String newline = ((seed & 4) == 0) ? "\n" : "\r\n"; + String source = buildRandomSingleProgram(seed, newline); + return Stream.of(new Program(new String[]{"random_fuzz.wurst"}, new String[]{source}, newline)); + } + } + + public static class CrossPackageProgram extends SeriesGen { + @Override + public Stream generate(int depth) { + int seed = Math.max(0, depth) + 17_213; + String newline = ((seed & 16) == 0) ? "\r\n" : "\n"; + String supportPkg = "ChainLib" + Math.abs(seed % 999); + String mainPkg = "ChainUse" + Math.abs(seed % 999); + String supportSource = buildRandomSupportPackage(seed, newline); + String mainSource = buildRandomMainPackage(seed, newline, supportPkg); + return Stream.of(new Program( + new String[]{"support_" + supportPkg + ".wurst", "main_" + mainPkg + ".wurst"}, + new String[]{supportSource, mainSource}, + newline + )); + } + } + + private static class Program { + final String[] unitNames; + final String[] sources; + final String newline; + + private Program(String[] unitNames, String[] sources, String newline) { + this.unitNames = unitNames; + this.sources = sources; + this.newline = newline; + } + + private Program withNewline(String newline) { + String[] replaced = Arrays.stream(sources) + .map(source -> replaceNewline(source, newline)) + .toArray(String[]::new); + return new Program(unitNames, replaced, newline); + } + } +} From 042c82c8baa9ef392ffe4596fe1268ba3682c9e4 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 29 Aug 2026 23:15:52 +0200 Subject: [PATCH 3/3] Fix compiler fuzz suite execution --- .../utils/SmallCheckViaJUnitCoreTestNG.java | 2 +- .../tests/CompilerFuzzTestsSC.java | 45 ++++++++++--------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/de.peeeq.wurstscript/src/test/java/tests/utils/SmallCheckViaJUnitCoreTestNG.java b/de.peeeq.wurstscript/src/test/java/tests/utils/SmallCheckViaJUnitCoreTestNG.java index 6d84da371..663c08eb9 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/utils/SmallCheckViaJUnitCoreTestNG.java +++ b/de.peeeq.wurstscript/src/test/java/tests/utils/SmallCheckViaJUnitCoreTestNG.java @@ -23,7 +23,7 @@ public void runSmallCheckSuite() { private void assertNoFailures(Result r, String suiteName) { if (!r.wasSuccessful()) { String msg = r.getFailures().stream() - .map(Failure::toString) + .map(Failure::getTrace) .collect(Collectors.joining("\n\n")); Assert.fail("SmallCheck failures (" + suiteName + "):\n" + msg); } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompilerFuzzTestsSC.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompilerFuzzTestsSC.java index d5effdef1..6f79bfc2b 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompilerFuzzTestsSC.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompilerFuzzTestsSC.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.IntStream; import java.util.stream.Stream; @RunWith(SmallCheckRunner.class) @@ -70,8 +71,7 @@ private CompilationResult runProgram(Program program) { return test() .setStopOnFirstError(false) .executeProg(false) - .compilationUnits(asCompilationUnits(program)) - .run(); + .compilationUnits(asCompilationUnits(program)); } private CU[] asCompilationUnits(Program program) { @@ -251,8 +251,7 @@ private static String buildRandomSingleProgram(int seed, String newline) { return join(lines, newline); } - private static String buildRandomSupportPackage(int seed, String newline) { - String pkg = "FuzzLib_" + Math.abs(seed % 997); + private static String buildRandomSupportPackage(int seed, String newline, String pkg) { String indent = " "; List lines = new ArrayList<>(); lines.add("package " + pkg); @@ -277,7 +276,7 @@ private static String buildRandomMainPackage(int seed, String newline, String su lines.add(indent + "function visit(int value) returns int"); lines.add("function run(Visitor visitor, int value) returns int"); lines.add(indent + "return visitor.visit(value)"); - lines.add("class Delegate"); + lines.add("class Delegate implements Visitor"); lines.add(indent + "public function visit(int value) returns int"); lines.add(indent + indent + "return value + 1"); lines.add("init"); @@ -293,27 +292,33 @@ private static String buildRandomMainPackage(int seed, String newline, String su public static class RandomProgram extends SeriesGen { @Override public Stream generate(int depth) { - int seed = Math.max(0, depth) + 17_213; - String newline = ((seed & 4) == 0) ? "\n" : "\r\n"; - String source = buildRandomSingleProgram(seed, newline); - return Stream.of(new Program(new String[]{"random_fuzz.wurst"}, new String[]{source}, newline)); + int firstSeed = Math.max(0, depth) * 64 + 17_213; + return IntStream.range(0, 64).mapToObj(offset -> { + int seed = firstSeed + offset; + String newline = ((seed & 4) == 0) ? "\n" : "\r\n"; + String source = buildRandomSingleProgram(seed, newline); + return new Program(new String[]{"random_fuzz.wurst"}, new String[]{source}, newline); + }); } } public static class CrossPackageProgram extends SeriesGen { @Override public Stream generate(int depth) { - int seed = Math.max(0, depth) + 17_213; - String newline = ((seed & 16) == 0) ? "\r\n" : "\n"; - String supportPkg = "ChainLib" + Math.abs(seed % 999); - String mainPkg = "ChainUse" + Math.abs(seed % 999); - String supportSource = buildRandomSupportPackage(seed, newline); - String mainSource = buildRandomMainPackage(seed, newline, supportPkg); - return Stream.of(new Program( - new String[]{"support_" + supportPkg + ".wurst", "main_" + mainPkg + ".wurst"}, - new String[]{supportSource, mainSource}, - newline - )); + int firstSeed = Math.max(0, depth) * 24 + 17_213; + return IntStream.range(0, 24).mapToObj(offset -> { + int seed = firstSeed + offset; + String newline = ((seed & 16) == 0) ? "\r\n" : "\n"; + String supportPkg = "ChainLib" + Math.abs(seed % 999); + String mainPkg = "ChainUse" + Math.abs(seed % 999); + String supportSource = buildRandomSupportPackage(seed, newline, supportPkg); + String mainSource = buildRandomMainPackage(seed, newline, supportPkg); + return new Program( + new String[]{"support_" + supportPkg + ".wurst", "main_" + mainPkg + ".wurst"}, + new String[]{supportSource, mainSource}, + newline + ); + }); } }