diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java index 8b93faeee..8ed4dbe47 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java @@ -48,14 +48,8 @@ public static LuaExpr translate(ImBoolVal e, LuaTranslator tr) { } public static LuaExpr translate(ImDealloc e, LuaTranslator tr) { - // Deliberate no-op: Lua instances are garbage collected, so 'destroy' - // only runs onDestroy (emitted separately) and drops nothing here. - // KNOWN DIVERGENCE from the Jass backend: a destroyed object stays - // fully usable (fields, dispatch, instanceof, typeId), whereas the - // Jass backend recycles the instance id and (with runtime checks) - // errors on use-after-destroy. Use-after-destroy bugs therefore stay - // silent on Lua but crash on Jass. - return LuaAst.LuaExprNull(); + return LuaAst.LuaExprFunctionCall(tr.objectDealloc, + LuaAst.LuaExprlist(e.getObj().translateToLua(tr))); } public static LuaExpr translate(ImFuncRef e, LuaTranslator tr) { @@ -117,7 +111,17 @@ public static LuaExpr translate(ImFunctionCall e, LuaTranslator tr) { String tcFunc = tr.getTypeCastingFunctionName(e.getFunc()); if (tcFunc != null && !e.getArguments().isEmpty()) { LuaExpr arg = e.getArguments().get(0).translateToLua(tr); - if (tcFunc.equals("stringToIndex")) { + ImType argumentType = e.getArguments().get(0).attrTyp(); + ImType resultType = e.attrTyp(); + if (argumentType instanceof ImClassType && TypesHelper.isIntType(resultType)) { + return LuaAst.LuaExprFunctionCall(tr.classToIndex, LuaAst.LuaExprlist(arg)); + } else if (TypesHelper.isIntType(argumentType) && resultType instanceof ImClassType) { + return LuaAst.LuaExprFunctionCall(tr.classFromIndex, LuaAst.LuaExprlist(arg)); + } else if (tcFunc.equals("objectToIndex")) { + return LuaAst.LuaExprFunctionCall(tr.toIndexFunction, LuaAst.LuaExprlist(arg)); + } else if (tcFunc.equals("objectFromIndex")) { + return LuaAst.LuaExprFunctionCall(tr.fromIndexFunction, LuaAst.LuaExprlist(arg)); + } else if (tcFunc.equals("stringToIndex")) { return LuaAst.LuaExprFunctionCall(tr.stringToIndexFunction, LuaAst.LuaExprlist(arg)); } else if (tcFunc.equals("stringFromIndex")) { return LuaAst.LuaExprFunctionCall(tr.stringFromIndexFunction, LuaAst.LuaExprlist(arg)); @@ -181,7 +185,9 @@ public static LuaExpr translate(ImIntVal e, LuaTranslator tr) { } public static LuaExpr translate(ImMemberAccess e, LuaTranslator tr) { - LuaExpr res = LuaAst.LuaExprFieldAccess(e.getReceiver().translateToLua(tr), e.getVar().getName()); + LuaExpr res = LuaAst.LuaExprArrayAccess( + LuaAst.LuaExprVarAccess(tr.fieldStorage(e.getVar())), + LuaAst.LuaExprlist(e.getReceiver().translateToLua(tr))); if (!e.getIndexes().isEmpty()) { LuaExprlist indexes = LuaAst.LuaExprlist(); for (ImExpr index : e.getIndexes()) { @@ -204,7 +210,9 @@ public static LuaExpr translate(ImMethodCall e, LuaTranslator tr) { } return LuaAst.LuaExprFunctionCall(tr.luaFunc.getFor(method.getImplementation()), args); } - return LuaAst.LuaExprMethodCall(e.getReceiver().translateToLua(tr), tr.luaMethod.getFor(e.getMethod()), tr.translateExprList(e.getArguments())); + LuaExprlist args = LuaAst.LuaExprlist(e.getReceiver().translateToLua(tr)); + args.addAll(tr.translateExprList(e.getArguments()).removeAll()); + return LuaAst.LuaExprFunctionCall(tr.luaDispatchFunc.getFor(e.getMethod()), args); } public static LuaExpr translate(ImNull e, LuaTranslator tr) { @@ -423,7 +431,10 @@ public static LuaExpr translate(ImTypeIdOfClass e, LuaTranslator tr) { } public static LuaExpr translate(ImTypeIdOfObj e, LuaTranslator tr) { - return LuaAst.LuaExprFieldAccess(e.getObj().translateToLua(tr), TYPE_ID); + return LuaAst.LuaExprFieldAccess( + LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(tr.objectClass), + LuaAst.LuaExprlist(e.getObj().translateToLua(tr))), + TYPE_ID); } public static LuaExpr translate(ImVarAccess e, LuaTranslator tr) { @@ -476,9 +487,13 @@ public static LuaExpr translate(ImCast imCast, LuaTranslator tr) { if (TypesHelper.isStringType(imCast.getExpr().attrTyp())) { return LuaAst.LuaExprFunctionCall(tr.stringToIndexFunction, LuaAst.LuaExprlist(translated)); } + if (imCast.getExpr().attrTyp() instanceof ImClassType) { + return LuaAst.LuaExprFunctionCall(tr.classToIndex, LuaAst.LuaExprlist(translated)); + } return LuaAst.LuaExprFunctionCall(tr.toIndexFunction, LuaAst.LuaExprlist(translated)); - } else if (imCast.getToType() instanceof ImClassType - || imCast.getToType() instanceof ImAnyType) { + } else if (imCast.getToType() instanceof ImClassType) { + return LuaAst.LuaExprFunctionCall(tr.classFromIndex, LuaAst.LuaExprlist(translated)); + } else if (imCast.getToType() instanceof ImAnyType) { return LuaAst.LuaExprFunctionCall(tr.fromIndexFunction, LuaAst.LuaExprlist(translated)); } else { return translated; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaPolyfillSetup.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaPolyfillSetup.java index 31520595b..6a3a91726 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaPolyfillSetup.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaPolyfillSetup.java @@ -15,7 +15,9 @@ private LuaPolyfillSetup() {} static void createInstanceOfFunction(LuaTranslator tr) { tr.instanceOfFunction.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr())); tr.instanceOfFunction.getParams().add(LuaAst.LuaVariable("A", LuaAst.LuaNoExpr())); - tr.instanceOfFunction.getBody().add(LuaAst.LuaLiteral("return x ~= nil and x." + WURST_SUPERTYPES + "[A]")); + tr.instanceOfFunction.getBody().add(LuaAst.LuaLiteral( + "return x ~= nil and __wurst_objectClass[x] ~= nil and __wurst_objectClass[x]." + + WURST_SUPERTYPES + "[A]")); tr.luaModel.add(tr.instanceOfFunction); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java index b2c9247ae..29c065da9 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java @@ -150,10 +150,56 @@ public LuaVariable initFor(ImClass a) { } }; - GetAForB luaClassMetaTableVar = new GetAForB() { + /** + * Runtime class instances are positive integer ids. Field values live in one static Lua table + * per canonical IM field, indexed by that id; class descriptors remain static tables and are + * reached through {@link #objectClass}. Allocation therefore creates no per-instance table. + * + *

Destroy clears every field slot before putting the id on the free stack. As in the Jass + * backend, a stale reference aliases a later object after that id is recycled; before reuse its + * descriptor is absent, so virtual dispatch fails and {@code instanceof} is false. Capturing + * closures use the same representation and, like Jass closures, retain their id until destroyed. + */ + GetAForB luaFieldStorage = new GetAForB() { @Override - public LuaVariable initFor(ImClass a) { - return LuaAst.LuaVariable(uniqueName(a.getName() + "_mt"), LuaAst.LuaNoExpr()); + public LuaVariable initFor(ImVar field) { + return LuaAst.LuaVariable(uniqueName(field.getName() + "_storage"), + LuaAst.LuaTableConstructor(LuaAst.LuaTableFields())); + } + }; + + final LuaVariable objectClass = LuaAst.LuaVariable("__wurst_objectClass", + LuaAst.LuaTableConstructor(LuaAst.LuaTableFields())); + final LuaVariable objectFree = LuaAst.LuaVariable("__wurst_objectFree", + LuaAst.LuaTableConstructor(LuaAst.LuaTableFields())); + final LuaVariable objectMax = LuaAst.LuaVariable("__wurst_objectMax", LuaAst.LuaExprIntVal("0")); + final LuaVariable objectFreeCount = LuaAst.LuaVariable("__wurst_objectFreeCount", LuaAst.LuaExprIntVal("0")); + final LuaFunction objectDealloc = LuaAst.LuaFunction("__wurst_deallocObject", LuaAst.LuaParams(), LuaAst.LuaStatements()); + final LuaFunction classToIndex = LuaAst.LuaFunction("__wurst_classToIndex", LuaAst.LuaParams(), LuaAst.LuaStatements()); + final LuaFunction classFromIndex = LuaAst.LuaFunction("__wurst_classFromIndex", LuaAst.LuaParams(), LuaAst.LuaStatements()); + + GetAForB luaClassCleanup = new GetAForB() { + @Override + public LuaFunction initFor(ImClass c) { + return LuaAst.LuaFunction(uniqueName(c.getName() + "_dealloc"), LuaAst.LuaParams(), LuaAst.LuaStatements()); + } + }; + + GetAForB luaDispatchFunc = new GetAForB() { + @Override + public LuaFunction initFor(ImMethod method) { + LuaVariable receiver = LuaAst.LuaVariable("receiver", LuaAst.LuaNoExpr()); + LuaVariable dots = LuaAst.LuaVariable("...", LuaAst.LuaNoExpr()); + LuaFunction result = LuaAst.LuaFunction(uniqueName("dispatch_" + method.getName()), + LuaAst.LuaParams(receiver, dots), LuaAst.LuaStatements()); + LuaExpr descriptor = LuaAst.LuaExprArrayAccess( + LuaAst.LuaExprVarAccess(objectClass), + LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(receiver))); + LuaExpr target = LuaAst.LuaExprFieldAccess(descriptor, dispatchSlotName(method.getName())); + result.getBody().add(LuaAst.LuaReturn(LuaAst.LuaExprFunctionCallE(target, + LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(receiver), LuaAst.LuaExprVarAccess(dots))))); + luaModel.add(result); + return result; } }; @@ -226,6 +272,7 @@ public LuaCompilationUnit translate() { // NormalizeNames.normalizeNames(prog); + createObjectManagement(); createInstanceOfFunction(); createObjectIndexFunctions(); createStringIndexFunctions(); @@ -234,6 +281,16 @@ public LuaCompilationUnit translate() { translateGlobal(v); } + Set emittedFieldStorage = Collections.newSetFromMap(new IdentityHashMap<>()); + for (ImClass c : prog.getClasses()) { + for (ImVar field : c.getFields()) { + LuaVariable storage = fieldStorage(field); + if (emittedFieldStorage.add(storage)) { + luaModel.add(storage); + } + } + } + // first add class variables for (ImClass c : prog.getClasses()) { LuaVariable classVar = luaClassVar.getFor(c); @@ -499,6 +556,62 @@ private void createInstanceOfFunction() { LuaPolyfillSetup.createInstanceOfFunction(this); } + LuaVariable fieldStorage(ImVar field) { + return luaFieldStorage.getFor(imTr.canonical(field)); + } + + private void createObjectManagement() { + luaModel.add(objectClass); + luaModel.add(objectFree); + luaModel.add(objectMax); + luaModel.add(objectFreeCount); + + LuaVariable object = LuaAst.LuaVariable("object", LuaAst.LuaNoExpr()); + objectDealloc.getParams().add(object); + LuaVariable descriptor = LuaAst.LuaVariable("descriptor", + LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(objectClass), + LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(object)))); + objectDealloc.getBody().add(descriptor); + objectDealloc.getBody().add(LuaAst.LuaIf( + LuaAst.LuaExprBinary(LuaAst.LuaExprVarAccess(descriptor), LuaAst.LuaOpEquals(), LuaAst.LuaExprNull()), + LuaAst.LuaStatements(LuaAst.LuaExprFunctionCallByName("error", + LuaAst.LuaExprlist(LuaAst.LuaExprStringVal("Double free or invalid Wurst object.")))), + LuaAst.LuaStatements())); + objectDealloc.getBody().add(LuaAst.LuaExprFunctionCallE( + LuaAst.LuaExprFieldAccess(LuaAst.LuaExprVarAccess(descriptor), "__wurst_dealloc"), + LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(object)))); + objectDealloc.getBody().add(LuaAst.LuaAssignment( + LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(objectClass), + LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(object))), + LuaAst.LuaExprNull())); + objectDealloc.getBody().add(LuaAst.LuaAssignment( + LuaAst.LuaExprVarAccess(objectFreeCount), + LuaAst.LuaExprBinary(LuaAst.LuaExprVarAccess(objectFreeCount), LuaAst.LuaOpPlus(), LuaAst.LuaExprIntVal("1")))); + objectDealloc.getBody().add(LuaAst.LuaAssignment( + LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(objectFree), + LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(objectFreeCount))), + LuaAst.LuaExprVarAccess(object))); + luaModel.add(objectDealloc); + + LuaVariable toIndexObject = LuaAst.LuaVariable("object", LuaAst.LuaNoExpr()); + classToIndex.getParams().add(toIndexObject); + classToIndex.getBody().add(LuaAst.LuaIf( + LuaAst.LuaExprBinary(LuaAst.LuaExprVarAccess(toIndexObject), LuaAst.LuaOpEquals(), LuaAst.LuaExprNull()), + LuaAst.LuaStatements(LuaAst.LuaReturn(LuaAst.LuaExprIntVal("0"))), + LuaAst.LuaStatements())); + classToIndex.getBody().add(LuaAst.LuaReturn(LuaAst.LuaExprVarAccess(toIndexObject))); + luaModel.add(classToIndex); + + LuaVariable fromIndexValue = LuaAst.LuaVariable("index", LuaAst.LuaNoExpr()); + classFromIndex.getParams().add(fromIndexValue); + classFromIndex.getBody().add(LuaAst.LuaIf( + LuaAst.LuaExprBinary(LuaAst.LuaExprVarAccess(fromIndexValue), LuaAst.LuaOpEquals(), LuaAst.LuaExprIntVal("0")), + LuaAst.LuaStatements(LuaAst.LuaReturn(LuaAst.LuaExprNull())), + LuaAst.LuaStatements())); + classFromIndex.getBody().add(LuaAst.LuaReturn(LuaAst.LuaExprVarAccess(fromIndexValue))); + luaModel.add(classFromIndex); + } + private void createObjectIndexFunctions() { LuaPolyfillSetup.createObjectIndexFunctions(this); } @@ -628,6 +741,38 @@ private boolean rewriteTypeCastingCompatFunction(ImFunction f, LuaFunction lf) { ImVar firstParam = f.getParameters().get(0); LuaExpr arg = LuaAst.LuaExprVarAccess(luaVar.getFor(firstParam)); + if (firstParam.getType() instanceof ImClassType && TypesHelper.isIntType(f.getReturnType())) { + lf.getBody().clear(); + lf.getBody().add(LuaAst.LuaIf( + LuaAst.LuaExprBinary(arg.copy(), LuaAst.LuaOpEquals(), LuaAst.LuaExprNull()), + LuaAst.LuaStatements(LuaAst.LuaReturn(LuaAst.LuaExprIntVal("0"))), + LuaAst.LuaStatements())); + lf.getBody().add(LuaAst.LuaReturn(arg)); + return true; + } + if (TypesHelper.isIntType(firstParam.getType()) && f.getReturnType() instanceof ImClassType) { + lf.getBody().clear(); + lf.getBody().add(LuaAst.LuaIf( + LuaAst.LuaExprBinary(arg.copy(), LuaAst.LuaOpEquals(), LuaAst.LuaExprIntVal("0")), + LuaAst.LuaStatements(LuaAst.LuaReturn(LuaAst.LuaExprNull())), + LuaAst.LuaStatements())); + lf.getBody().add(LuaAst.LuaReturn(arg)); + return true; + } + + if ("objectToIndex".equals(tcFunc)) { + lf.getBody().clear(); + lf.getBody().add(LuaAst.LuaReturn( + LuaAst.LuaExprFunctionCall(toIndexFunction, LuaAst.LuaExprlist(arg)))); + return true; + } + if ("objectFromIndex".equals(tcFunc)) { + lf.getBody().clear(); + lf.getBody().add(LuaAst.LuaReturn( + LuaAst.LuaExprFunctionCall(fromIndexFunction, LuaAst.LuaExprlist(arg)))); + return true; + } + if ("stringToIndex".equals(tcFunc)) { lf.getBody().clear(); lf.getBody().add(LuaAst.LuaReturn(LuaAst.LuaExprFunctionCall(stringToIndexFunction, LuaAst.LuaExprlist(arg)))); @@ -817,16 +962,22 @@ private void translateClass(ImClass c) { LuaVariable classVar = luaClassVar.getFor(c); LuaMethod initMethod = luaClassInitMethod.getFor(c); - // one shared instance metatable per class — allocating a fresh - // {__index = classVar} table per instance would be pure garbage - LuaVariable metaTableVar = luaClassMetaTableVar.getFor(c); - metaTableVar.setInitialValue(LuaAst.LuaTableConstructor(LuaAst.LuaTableFields( - LuaAst.LuaTableNamedField("__index", LuaAst.LuaExprVarAccess(classVar)) - ))); - luaModel.add(metaTableVar); - luaModel.add(initMethod); + LuaFunction cleanup = luaClassCleanup.getFor(c); + LuaVariable object = LuaAst.LuaVariable("object", LuaAst.LuaNoExpr()); + cleanup.getParams().add(object); + for (ImVar field : collectFieldsForAllocation(c)) { + cleanup.getBody().add(LuaAst.LuaAssignment( + LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(fieldStorage(field)), + LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(object))), + LuaAst.LuaExprNull())); + } + luaModel.add(cleanup); + deferMainInit(LuaAst.LuaAssignment( + LuaAst.LuaExprFieldAccess(LuaAst.LuaExprVarAccess(classVar), "__wurst_dealloc"), + LuaAst.LuaExprFuncRef(cleanup))); + // translate functions for (ImFunction f : c.getFunctions()) { translateFunc(f); @@ -839,42 +990,61 @@ private void translateClass(ImClass c) { private void createClassInitFunction(ImClass c, LuaVariable classVar, LuaMethod initMethod) { // create init function: LuaStatements body = initMethod.getBody(); - // local new_inst = { ... } - LuaTableFields initialFieldValues = LuaAst.LuaTableFields(); - LuaVariable newInst = LuaAst.LuaVariable("new_inst", LuaAst.LuaTableConstructor(initialFieldValues)); + LuaVariable newInst = LuaAst.LuaVariable("new_inst", LuaAst.LuaNoExpr()); + body.add(newInst); + LuaStatements fresh = LuaAst.LuaStatements( + LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(objectMax), + LuaAst.LuaExprBinary(LuaAst.LuaExprVarAccess(objectMax), LuaAst.LuaOpPlus(), LuaAst.LuaExprIntVal("1"))), + LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(newInst), LuaAst.LuaExprVarAccess(objectMax))); + LuaStatements recycled = LuaAst.LuaStatements( + LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(newInst), + LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(objectFree), + LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(objectFreeCount)))), + LuaAst.LuaAssignment( + LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(objectFree), + LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(objectFreeCount))), + LuaAst.LuaExprNull()), + LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(objectFreeCount), + LuaAst.LuaExprBinary(LuaAst.LuaExprVarAccess(objectFreeCount), LuaAst.LuaOpMinus(), LuaAst.LuaExprIntVal("1")))); + body.add(LuaAst.LuaIf( + LuaAst.LuaExprBinary(LuaAst.LuaExprVarAccess(objectFreeCount), LuaAst.LuaOpEquals(), LuaAst.LuaExprIntVal("0")), + fresh, recycled)); + body.add(LuaAst.LuaAssignment( + LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(objectClass), + LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(newInst))), + LuaAst.LuaExprVarAccess(classVar))); for (ImVar field : collectFieldsForAllocation(c)) { - initialFieldValues.add( - LuaAst.LuaTableNamedField(field.getName(), defaultValue(field.getType())) - ); + body.add(LuaAst.LuaAssignment( + LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(fieldStorage(field)), + LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(newInst))), + defaultValue(field.getType()))); } - - - body.add(newInst); - // setmetatable(new_inst, ) - body.add(LuaAst.LuaExprFunctionCallByName("setmetatable", LuaAst.LuaExprlist( - LuaAst.LuaExprVarAccess(newInst), - LuaAst.LuaExprVarAccess(luaClassMetaTableVar.getFor(c)) - ))); body.add(LuaAst.LuaReturn(LuaAst.LuaExprVarAccess(newInst))); } private List collectFieldsForAllocation(ImClass c) { List result = new ArrayList<>(); - Set visited = new HashSet<>(); - collectFieldsForAllocation(c, result, visited); + Set visitedClasses = Collections.newSetFromMap(new IdentityHashMap<>()); + Set visitedFields = Collections.newSetFromMap(new IdentityHashMap<>()); + collectFieldsForAllocation(c, result, visitedClasses, visitedFields); return result; } - private void collectFieldsForAllocation(ImClass c, List out, Set visited) { - if (!visited.add(c)) { + private void collectFieldsForAllocation(ImClass c, List out, + Set visitedClasses, Set visitedFields) { + if (!visitedClasses.add(c)) { return; } List superClasses = new ArrayList<>(c.getSuperClasses()); superClasses.sort(Comparator.comparing(sc -> classSortKey(sc.getClassDef()))); for (ImClassType sc : superClasses) { - collectFieldsForAllocation(sc.getClassDef(), out, visited); + collectFieldsForAllocation(sc.getClassDef(), out, visitedClasses, visitedFields); + } + for (ImVar field : c.getFields()) { + if (visitedFields.add(imTr.canonical(field))) { + out.add(field); + } } - out.addAll(c.getFields()); } private void initClassTables(ImClass c) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FastHashMapTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FastHashMapTests.java index 1c0b82cf9..a4a5e8825 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FastHashMapTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FastHashMapTests.java @@ -458,16 +458,26 @@ private static void assertSpecialisedClassesAllocateTheirFields(String compiled) private static String allocatedFields(String compiled, String classPattern) { String fields = allocatedFieldsOrNull(compiled, classPattern); - if (fields == null) { + if (fields == null || fields.isEmpty()) { throw new AssertionError("expected an allocation for " + classPattern + " in:\n" + compiled); } return fields; } private static @Nullable String allocatedFieldsOrNull(String compiled, String classPattern) { - Matcher m = Pattern.compile("function " + classPattern + ":create\\d*\\(\\)\\s*\\R" - + "\\s*local new_inst = \\(\\{([^}]*)\\}\\)").matcher(compiled); - return m.find() ? m.group(1).trim() : null; + Matcher allocation = Pattern.compile("function " + classPattern + ":create\\d*\\(\\)\\s*\\R" + + "(.*?)\\R\\s*return new_inst\\s*\\Rend", Pattern.DOTALL).matcher(compiled); + if (!allocation.find()) { + return null; + } + Matcher field = Pattern.compile("(?m)^\\s*(\\w+_storage)\\[new_inst\\]\\s*=") + .matcher(allocation.group(1)); + java.util.List fields = new java.util.ArrayList<>(); + while (field.find()) { + fields.add(field.group(1)); + } + java.util.Collections.sort(fields); + return String.join(",", fields); } /** diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index c52a2fc01..420bccfd9 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -221,10 +221,10 @@ public void serializesAndDeserializesFieldsWithoutRuntimeReflection() throws IOE "lua/FieldIterationTests_serializesAndDeserializesFieldsWithoutRuntimeReflection.lua").toPath()); assertFalse(lua.contains("forFields")); assertFalse(lua.contains("mapFields")); - assertTrue(lua.contains("Codec_Codec_write(codec, \"score\", this")); - assertTrue(lua.contains("Codec_Codec_write1(codec, \"name\", this")); - assertTrue(lua.contains("Data_score = Codec_Codec_read(codec1, \"score\"")); - assertTrue(lua.contains("Data_name = Codec_Codec_read1(codec1, \"name\"")); + assertTrue(lua.contains("Codec_Codec_write(codec, \"score\", Data_score_storage[")); + assertTrue(lua.contains("Codec_Codec_write1(codec, \"name\", Data_name_storage[")); + assertTrue(lua.contains(" = Codec_Codec_read(codec1, \"score\", Data_score_storage[")); + assertTrue(lua.contains(" = Codec_Codec_read1(codec1, \"name\", Data_name_storage[")); } @Test 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 dbfd02e61..5bf33791a 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 @@ -53,6 +53,93 @@ private String compileLuaWithRunArgs(String testName, RunArgs runArgs, String... return result.toString(); } + @Test + public void classInstancesUseRecycledIntegerIdsAndStaticFieldStorage() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "int destroyed = 0", + "class Base", + " int scalar", + " Child reference", + " int array[8] values", + " function score() returns int", + " return scalar", + " ondestroy", + " destroyed++", + "class Child extends Base", + " int extra", + " override function score() returns int", + " return scalar + extra", + "init", + " let first = new Child()", + " let firstId = first castTo int", + " first.scalar = 7", + " first.reference = first", + " first.values[3] = 99", + " first.extra = 5", + " Base polymorphic = first", + " Base stale = first", + " let dispatched = polymorphic.score()", + " destroy first", + " let destroyedIsInvalid = not (stale instanceof Child)", + " let second = new Child()", + " Base secondBase = second", + " if second castTo int == firstId and dispatched == 12 and destroyed == 1", + " and second.scalar == 0 and second.reference == null", + " and second.values[3] == 0 and second.extra == 0", + " and destroyedIsInvalid and stale == second", + " and secondBase instanceof Child and second.typeId == Child.typeId", + " testSuccess()" + ); + + String compiled = compiledLua("classInstancesUseRecycledIntegerIdsAndStaticFieldStorage"); + assertTrue("integer-ID lowering must emit the live-object class map", + compiled.contains("__wurst_objectClass")); + assertTrue("integer-ID lowering must emit an explicit free-ID pool", + compiled.contains("__wurst_objectFree")); + assertFalse("class allocation must not construct a table per instance", + compiled.contains("local new_inst = {")); + assertFalse("class allocation must not attach an instance metatable", + compiled.contains("setmetatable(new_inst")); + assertTrue("class-to-int casts must use the integer object id directly", + compiled.contains("__wurst_classToIndex(first)")); + assertFalse("class casts must not allocate boxed-number identity wrappers", + compiled.contains("firstId = __wurst_objectToIndex(first)")); + assertTrue("deallocation must clear reference-bearing field slots before recycling", + compiled.contains("Base_reference_storage[object] = nil")); + } + + @Test + public void legacyGenericHandleCastsUseObjectIndexMap() throws IOException { + test().testLua(true).executeProg().lines( + "type timer extends handle", + "package Test", + "native testSuccess()", + "native CreateTimer() returns timer", + "function timerToIndex(timer value) returns int", + " return 0", + "function timerFromIndex(int value) returns timer", + " return null", + "function toIndex(T value) returns int", + " return value castTo int", + "init", + " let value = CreateTimer()", + " let first = toIndex(value)", + " let second = toIndex(value)", + " if first > 0 and first == second", + " testSuccess()" + ); + + String compiled = compiledLua("legacyGenericHandleCastsUseObjectIndexMap"); + assertTrue(java.util.regex.Pattern.compile( + "function toIndex\\((\\w+)\\)\\s*\\R\\s*return __wurst_objectToIndex\\(\\1\\)") + .matcher(compiled).find()); + assertFalse(java.util.regex.Pattern.compile( + "function toIndex\\((\\w+)\\)\\s*\\R\\s*return __wurst_classToIndex\\(\\1\\)") + .matcher(compiled).find()); + } + @Test public void compiletimeGenericArrayReplayLeavesAreSplit() { String compiled = compileLuaWithRunArgs( @@ -759,12 +846,9 @@ public void deferredInitRunsForConfigToo() throws IOException { assertTrue("config must start with the same bootstrap call", config.find()); } - /** - * Instances used to get a fresh {@code {__index = Class}} metatable per - * allocation — pure garbage. All instances of a class share one metatable. - */ + /** Class construction allocates only a scalar id; class descriptors and field storage are static. */ @Test - public void classInstancesShareOneMetatablePerClass() throws IOException { + public void classInstancesAllocateIdsWithoutInstanceTables() throws IOException { test().testLua(true).executeProg().lines( "package Test", "native testSuccess()", @@ -776,11 +860,13 @@ public void classInstancesShareOneMetatablePerClass() throws IOException { " if a.v + b.v == 2 and a != b", " testSuccess()" ); - String compiled = compiledLua("classInstancesShareOneMetatablePerClass"); - assertTrue("expected a shared per-class metatable variable", - compiled.contains("Foo_mt = ({__index=Foo, })")); - assertFalse("create must not allocate a metatable per instance", - compiled.contains("setmetatable(new_inst, ({")); + String compiled = compiledLua("classInstancesAllocateIdsWithoutInstanceTables"); + assertTrue("expected static field storage indexed by object id", + compiled.contains("Foo_v_storage[new_inst] = 0")); + assertTrue("expected each live id to point at its static class descriptor", + compiled.contains("__wurst_objectClass[new_inst] = Foo")); + assertFalse("create must not allocate an instance table", compiled.contains("local new_inst = {")); + assertFalse("create must not attach an instance metatable", compiled.contains("setmetatable(new_inst")); } /** diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java index cdb7764a7..23a8ce5f0 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java @@ -136,6 +136,13 @@ private String singleMatch(String output, String regex, int group) { return result; } + private String singleDispatchSlot(String compiled, String callerBody) { + String helper = singleMatch(callerBody, "(dispatch_[A-Za-z0-9_]+)\\(", 1); + String helperBody = getFunctionBody(compiled, helper); + return singleMatch(helperBody, + "__wurst_objectClass\\[[^\\]]+\\]\\.([A-Za-z0-9_]+)", 1); + } + private List nonBaseSubclassBindings(String output, String baseName, String slotName) { Matcher matcher = Pattern.compile("([A-Za-z0-9_]+)\\." + Pattern.quote(slotName) + "\\s*=\\s*[A-Za-z0-9_]+").matcher(output); List result = new ArrayList<>(); @@ -565,7 +572,7 @@ public void overloadedOverrideDispatchDoesNotCollapseLuaSlots() throws IOExcepti } } assertEquals("Expected three distinct Base overload dispatch slots.", 3, baseSlots.size()); - assertTrue(compiled.contains("this:Base_doThing1(a, 0)")); + assertTrue(compiled.contains("dispatch_Base_doThing1(this, a, 0)")); assertTrue(compiled.contains("Base_Base_doThing2(this1, a1, b, false)")); assertTrue(compiled.contains("Child.Base_doThing1 = Child_Child_doThing")); assertTrue(compiled.contains("Child.Base_doThing2 = Base_Base_doThing2")); @@ -599,7 +606,7 @@ public void moduleProvidedOverloadedOverrideDoesNotCollapseLuaSlots() { assertEquals("Expected exactly one overridden setup overload family from module-provided methods.", 1, overriddenSlots.size()); assertEquals("Expected three distinct setup slots on Base.", 3, baseSlots.size()); - assertTrue(compiled.contains(":Base_M_setup1(") || compiled.contains(":Base_setup1(")); + assertTrue(compiled.contains("dispatch_Base_M_setup1(") || compiled.contains("dispatch_Base_setup1(")); assertContainsRegex(compiled, "Child\\.Base(?:_M)?_setup" + Pattern.quote(overriddenSlots.get(0)) + "\\s*=\\s*Child_Child_setup"); } @@ -640,13 +647,14 @@ public void moduleProvidedInterfaceDispatchSurvivesLuaOptimizations() { " Greeter firstResult = firstObject.call(second)", " Greeter secondResult = secondObject.call(first)" ); - Matcher callMatcher = Pattern.compile("return greeter\\d*:(\\w+)\\(").matcher(compiled); - List slots = new ArrayList<>(); - while (callMatcher.find() && !slots.contains(callMatcher.group(1))) { - slots.add(callMatcher.group(1)); + Matcher callMatcher = Pattern.compile("return (dispatch_[A-Za-z0-9_]+)\\(greeter\\d*").matcher(compiled); + List helpers = new ArrayList<>(); + while (callMatcher.find() && !helpers.contains(callMatcher.group(1))) { + helpers.add(callMatcher.group(1)); } - assertEquals("Both optimized module callers must use one interface dispatch slot.", 1, slots.size()); - String slot = slots.get(0); + assertEquals("Both optimized module callers must use one interface dispatch helper.", 1, helpers.size()); + String slot = singleMatch(getFunctionBody(compiled, helpers.get(0)), + "__wurst_objectClass\\[[^\\]]+\\]\\.([A-Za-z0-9_]+)", 1); assertContainsRegex(compiled, "First\\.[^\\n]*" + Pattern.quote(slot) + "\\s*=\\s*First_[^\\n]*greet"); assertContainsRegex(compiled, "Second\\.[^\\n]*" + Pattern.quote(slot) + "\\s*=\\s*Second_[^\\n]*greet"); } @@ -676,7 +684,10 @@ public void incompatibleSameNameInterfaceReturnsDoNotAliasInLua() { " readString(new Both())" ); - assertContainsRegex(compiled, "return value:Both_IntValueImpl_value\\("); + String intSlot = singleDispatchSlot(compiled, getFunctionBody(compiled, "readInt")); + assertContainsRegex(compiled, "Both\\." + Pattern.quote(intSlot) + "\\s*="); + assertDoesNotContainRegex(compiled, + "Both\\." + Pattern.quote(intSlot) + "\\s*=\\s*StringValue_StringValue_value"); assertContainsRegex(compiled, "Both\\.StringValue_value\\s*=\\s*StringValue_StringValue_value"); assertDoesNotContainRegex(compiled, "Both\\.IntValue_value\\s*=\\s*StringValue_StringValue_value"); } @@ -901,7 +912,7 @@ public void closureInterfaceCallsitesUseSubclassBindingsForSameLuaSlot() { ); String forEachBody = getFunctionBody(compiled, "LinkedList_LinkedList_forEach"); - String slotName = singleMatch(forEachBody, ":([A-Za-z0-9_]+)\\(", 1); + String slotName = singleDispatchSlot(compiled, forEachBody); assertEquals("run", slotName); List subclassBindings = nonBaseSubclassBindings(compiled, "LLItrClosure", slotName); @@ -938,7 +949,7 @@ public void abstractCallbackFamiliesKeepCallsiteAndSubclassSlotNamesAlignedInLua ); String forEachBody = getFunctionBody(compiled, "Registry_Registry_forEachIn"); - String callbackSlot = singleMatch(forEachBody, ":([A-Za-z0-9_]+)\\(", 1); + String callbackSlot = singleDispatchSlot(compiled, forEachBody); assertEquals("callback", callbackSlot); List subclassBindings = nonBaseSubclassBindings(compiled, "ForElementCallback", callbackSlot); @@ -947,7 +958,7 @@ public void abstractCallbackFamiliesKeepCallsiteAndSubclassSlotNamesAlignedInLua assertContainsRegex(compiled, "ForElementCallback_[A-Za-z0-9_]+\\." + Pattern.quote(callbackSlot) + "\\s*="); String otherBody = getFunctionBody(compiled, "OtherRegistry_OtherRegistry_applyTo"); - String otherSlot = singleMatch(otherBody, ":([A-Za-z0-9_]+)\\(", 1); + String otherSlot = singleDispatchSlot(compiled, otherBody); assertTrue(otherSlot.startsWith("callback")); List otherSubclassBindings = nonBaseSubclassBindings(compiled, "OtherCallback", otherSlot); @@ -985,7 +996,7 @@ public void genericClosureInterfacesKeepPrefixedBaseSlotNamesInLua() { ); String forEachBody = getFunctionBody(compiled, "LinkedList_LinkedList_forEach"); - String slotName = singleMatch(forEachBody, ":([A-Za-z0-9_]+)\\(", 1); + String slotName = singleDispatchSlot(compiled, forEachBody); assertEquals("LLItrClosure_run", slotName); assertContainsRegex(compiled, "LLItrClosure_[A-Za-z0-9_]+\\." + Pattern.quote(slotName) + "\\s*="); assertDoesNotContainRegex(compiled, "LLItrClosure_[A-Za-z0-9_]+\\.LLItrClosure_run\\d+\\s*="); @@ -1015,7 +1026,7 @@ public void genericAbstractCallbacksKeepPrefixedBaseSlotNamesInLua() { ); String forEachBody = getFunctionBody(compiled, "Registry_Registry_forEachIn"); - String slotName = singleMatch(forEachBody, ":([A-Za-z0-9_]+)\\(", 1); + String slotName = singleDispatchSlot(compiled, forEachBody); assertEquals("ForElementCallback_callback", slotName); assertContainsRegex(compiled, "ForElementCallback_[A-Za-z0-9_]+\\." + Pattern.quote(slotName) + "\\s*="); assertDoesNotContainRegex(compiled, "ForElementCallback_[A-Za-z0-9_]+\\.ForElementCallback_callback\\d+\\s*="); @@ -1317,9 +1328,9 @@ public void intCasting() throws IOException { assertFunctionBodyContains(compiled, "testEnum", "zeroEnum = 0", true); assertFunctionBodyContains(compiled, "testEnum", "zeroInt = zeroEnum", true); assertFunctionBodyContains(compiled, "testEnum", "zeroEnum2 = zeroInt", true); - // classes are cast with objectToIndex and objectFromIndex in lua - assertFunctionBodyContains(compiled, "testClass", "__wurst_objectToIndex", true); - assertFunctionBodyContains(compiled, "testClass", "__wurst_objectFromIndex", true); + // Integer-ID classes use their scalar id directly, with only null/zero normalization. + assertFunctionBodyContains(compiled, "testClass", "__wurst_classToIndex", true); + assertFunctionBodyContains(compiled, "testClass", "__wurst_classFromIndex", true); assertFunctionBodyContains(compiled, "testClass", "cInt = cObj", false); assertFunctionBodyContains(compiled, "testClass", "cObj2 = cInt", false); } @@ -1417,8 +1428,8 @@ public void objectIndexFunctionsDoNotCollideWithUserFunctions() throws IOExcepti String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_objectIndexFunctionsDoNotCollideWithUserFunctions.lua"), Charsets.UTF_8); assertTrue(compiled.contains("function objectToIndex(")); assertTrue(compiled.contains("function objectFromIndex(")); - assertFunctionBodyContains(compiled, "testClass", "__wurst_objectToIndex", true); - assertFunctionBodyContains(compiled, "testClass", "__wurst_objectFromIndex", true); + assertFunctionBodyContains(compiled, "testClass", "__wurst_classToIndex", true); + assertFunctionBodyContains(compiled, "testClass", "__wurst_classFromIndex", true); } @Test @@ -1463,8 +1474,8 @@ public void oldGenericsCastingDoesNotUseGetHandleId() throws IOException { ); String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_oldGenericsCastingDoesNotUseGetHandleId.lua"), Charsets.UTF_8); assertDoesNotContainRegex(compiled, "\\bGetHandleId\\("); - assertFunctionBodyContains(compiled, "testCast", "__wurst_objectToIndex", true); - assertFunctionBodyContains(compiled, "testCast", "__wurst_objectFromIndex", true); + assertFunctionBodyContains(compiled, "testCast", "__wurst_classToIndex", true); + assertFunctionBodyContains(compiled, "testCast", "__wurst_classFromIndex", true); } @Test @@ -1506,8 +1517,8 @@ public void newGenericsStringFieldAssignmentRoundTripsInLua() throws IOException " skip" ); String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_newGenericsStringFieldAssignmentRoundTripsInLua.lua"), Charsets.UTF_8); - assertFunctionBodyContains(compiled, "testGenericStringField", "c.C_x = \"42\"", true); - assertFunctionBodyContains(compiled, "testGenericStringField", "__wurst_ensureStr(c.C_x)", true); + assertFunctionBodyContains(compiled, "testGenericStringField", "C_x_storage[c] = \"42\"", true); + assertFunctionBodyContains(compiled, "testGenericStringField", "__wurst_ensureStr(C_x_storage[c])", true); assertFunctionBodyContains(compiled, "testGenericStringField", "__wurst_stringToIndex", false); assertFunctionBodyContains(compiled, "testGenericStringField", "__wurst_stringFromIndex", false); } @@ -1537,9 +1548,8 @@ public void genericOverrideChainBindsRootSlotToMostSpecificImplInLua() throws IO test().testLua(true).compilationUnits(genericOverrideReproUnits()); String compiled = Files.toString(new File(outputFile), Charsets.UTF_8); - Matcher slotMatcher = Pattern.compile("FSM_currentState:([A-Za-z0-9_]*_update)\\(").matcher(compiled); - assertTrue("Expected FSM to dispatch through a virtual *_update slot.", slotMatcher.find()); - String dispatchedSlot = slotMatcher.group(1); + String dispatchedSlot = singleDispatchSlot(compiled, getFunctionBody(compiled, "FSM_FSM_update")); + assertTrue("Expected FSM to dispatch through a virtual *_update slot.", dispatchedSlot.endsWith("_update")); String[] states = {"FindBuilder", "PlanNextAction", "FindSpot", "BuildAtTarget", "QuickBuild", "RescueStrikeTarget"}; for (String state : states) { @@ -1761,9 +1771,8 @@ public void genericOverrideChainBindsGlobalStateSlotToMostSpecificImplInLua() th test().testLua(true).compilationUnits(genericOverrideGlobalStateReproUnits()); String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_genericOverrideChainBindsGlobalStateSlotToMostSpecificImplInLua.lua"), Charsets.UTF_8); - Matcher slotMatcher = Pattern.compile("FSM_globalState:([A-Za-z0-9_]*_update)\\(").matcher(compiled); - assertTrue("Expected FSM global state to dispatch through a virtual *_update slot.", slotMatcher.find()); - String dispatchedSlot = slotMatcher.group(1); + String dispatchedSlot = singleDispatchSlot(compiled, getFunctionBody(compiled, "FSM_FSM_update")); + assertTrue("Expected FSM global state to dispatch through a virtual *_update slot.", dispatchedSlot.endsWith("_update")); assertContainsRegex(compiled, "GlobalCheckState\\." + dispatchedSlot + "\\s*=\\s*GlobalCheckState_GlobalCheckState_update"); assertDoesNotContainRegex(compiled, "GlobalCheckState\\." + dispatchedSlot + "\\s*=\\s*NoOpState_NoOpState_update"); @@ -2606,9 +2615,9 @@ public void subclassAllocationIncludesInheritedFieldsInLua() throws IOException String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_subclassAllocationIncludesInheritedFieldsInLua.lua"), Charsets.UTF_8); assertContainsRegex(compiled, - "function\\s+[A-Za-z0-9_]+:create\\d+\\s*\\(\\)\\s*\\n\\s*local new_inst = \\(\\{[^\\n]*Window_anchorTop="); + "function\\s+[A-Za-z0-9_]+:create\\d+\\s*\\(\\)[\\s\\S]*?Window_anchorTop_storage\\[new_inst\\] = 0"); assertContainsRegex(compiled, - "function\\s+[A-Za-z0-9_]+:create\\d+\\s*\\(\\)\\s*\\n\\s*local new_inst = \\(\\{[^\\n]*Window_anchorBottom="); + "function\\s+[A-Za-z0-9_]+:create\\d+\\s*\\(\\)[\\s\\S]*?Window_anchorBottom_storage\\[new_inst\\] = 0"); } // ----- GetHandleId remapping ----- diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TypeClassTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TypeClassTests.java index 0d781a427..32d19d77f 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TypeClassTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TypeClassTests.java @@ -132,9 +132,15 @@ public void dispatchInsideClosureLua() throws IOException { "the closure should be allocated from its specialised class:\n" + compiled); String specialised = allocation.group(1); - Matcher call = Pattern.compile("\\w+:(\\w*produce\\w*)\\(").matcher(compiled); - assertTrue(call.find(), "expected a dispatched produce slot:\n" + compiled); - String slot = call.group(1); + Matcher dispatcher = Pattern.compile("function (dispatch_\\w*produce\\w*)\\(receiver, \\.\\.\\.\\)\\s*\\R" + + "\\s*return \\(__wurst_objectClass\\[receiver\\]\\.(\\w*produce\\w*)\\)" + + "\\(receiver, \\.\\.\\.\\)").matcher(compiled); + assertTrue(dispatcher.find(), "expected a dispatched produce slot:\n" + compiled); + String dispatchFunction = dispatcher.group(1); + String slot = dispatcher.group(2); + assertTrue(Pattern.compile("return\\s+" + Pattern.quote(dispatchFunction) + "\\(p\\)") + .matcher(compiled).find(), + "the closure call should use the produce dispatcher:\n" + compiled); assertTrue(Pattern.compile(Pattern.quote(specialised) + "\\." + Pattern.quote(slot) + "\\s*=\\s*" + Pattern.quote(specialised) + "\\w*").matcher(compiled).find(),