From 52d895ef711fef06140ece2f64e277ea7886dda0 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 1 Dec 2025 15:57:26 +0100 Subject: [PATCH 1/3] Implement capturing outer this in closures --- .../interpreter/EvaluateExpr.java | 16 +++- .../interpreter/ProgramState.java | 10 +++ .../imtranslation/ClosureTranslator.java | 78 ++++++++++++++++++- .../tests/GenericsWithTypeclassesTests.java | 48 ++++++++++++ 4 files changed, 145 insertions(+), 7 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java index 9aa489c72..926d3f650 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java @@ -401,20 +401,30 @@ public ILconst get() { public static ILaddress evaluateLvalue(ImMemberAccess va, ProgramState globalState, LocalState localState) { ImVar v = va.getVar(); - ILconstObject receiver = globalState.toObject(va.getReceiver().evaluate(globalState, localState)); + ILconst receiverVal = va.getReceiver().evaluate(globalState, localState); + ILconstObject receiver = globalState.toObject(receiverVal); + if (receiver == null && receiverVal instanceof ILconstInt && va.getReceiver().attrTyp() instanceof ImClassType) { + receiver = globalState.ensureObject((ImClassType) va.getReceiver().attrTyp(), + ((ILconstInt) receiverVal).getVal(), va.attrTrace()); + } + if (receiver == null) { + throw new InterpreterException(va.attrTrace(), "Null pointer dereference"); + } + ILconstObject receiverFinal = receiver; List indexes = va.getIndexes().stream() .map(ie -> ((ILconstInt) ie.evaluate(globalState, localState)).getVal()) .collect(Collectors.toList()); + List indexesFinal = indexes; return new ILaddress() { @Override public void set(ILconst value) { - receiver.set(v, indexes, value); + receiverFinal.set(v, indexesFinal, value); } @Override public ILconst get() { - return receiver.get(v, indexes) + return receiverFinal.get(v, indexesFinal) .orElseGet(() -> va.attrTyp().defaultValue()); } }; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java index 8c7bdfd45..b579dff02 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java @@ -351,6 +351,16 @@ public ILconstObject toObject(ILconst val) { throw new InterpreterException(this, "Value " + val + " (" + val.getClass().getSimpleName() + ") cannot be cast to object."); } + public ILconstObject ensureObject(ImClassType clazz, int objectId, Element trace) { + ILconstObject existing = indexToObject.get(objectId); + if (existing != null) { + return existing; + } + ILconstObject res = new ILconstObject(clazz, objectId, trace); + indexToObject.put(objectId, res); + return res; + } + public static class StackTrace { private final List stackFrames; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java index 1ee74a06e..cb4e1134e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java @@ -244,23 +244,93 @@ public ImType case_ImTypeVarRef(ImTypeVarRef t) { * class variables instead */ private void transformTranslated(ImExpr t) { - final List vas = Lists.newArrayList(); + final List capturedLocals = Lists.newArrayList(); + final List capturedFields = Lists.newArrayList(); + t.accept(new ImExpr.DefaultVisitor() { @Override public void visit(ImVarAccess va) { super.visit(va); if (isLocalToOtherFunc(va.getVar())) { - vas.add(va); + capturedLocals.add(va); + } + } + + @Override + public void visit(ImMemberAccess ma) { + super.visit(ma); + if (isCapturedOuterField(ma)) { + capturedFields.add(ma); } } + private boolean isCapturedOuterField(ImMemberAccess ma) { + // receiver must be *this* of the closure + if (!(ma.getReceiver() instanceof ImVarAccess)) { + return false; + } + ImVarAccess recv = (ImVarAccess) ma.getReceiver(); + if (recv.getVar() != tr.getThisVar(e)) { + return false; + } + // field must belong to some class + var pp = ma.getVar().getParent(); + if (pp == null || !(pp.getParent() instanceof ImClass)) { + return false; + } + + ImClass owner = (ImClass) pp.getParent(); + // and it must not be a field of the closure class itself + return owner != c; + } }); - for (ImVarAccess va : vas) { + // Existing behaviour: capture locals from outer functions + for (ImVarAccess va : capturedLocals) { ImVar v = getClosureVarFor(va.getVar()); - va.replaceBy(JassIm.ImMemberAccess(e, closureThis(), JassIm.ImTypeArguments(), v, JassIm.ImExprs())); + va.replaceBy(JassIm.ImMemberAccess(e, closureThis(), + JassIm.ImTypeArguments(), v, JassIm.ImExprs())); + } + + // New behaviour: capture outer "this" for fields of the enclosing class + for (ImMemberAccess ma : capturedFields) { + ImVar field = ma.getVar(); + ImClass owner = (ImClass) field.getParent().getParent(); + + ImVar outerThis = findOuterThisVar(owner); + if (outerThis == null) { + // no obvious owning instance – leave it, fail later with a clear error if needed + continue; + } + + // Create/get the closure field to store outer "this" + ImVar capturedOuterThisField = getClosureVarFor(outerThis); + + // Rewrite receiver: this.capturedOuterThisField + ImExpr newReceiver = JassIm.ImMemberAccess( + e, + closureThis(), + JassIm.ImTypeArguments(), + capturedOuterThisField, + JassIm.ImExprs() + ); + ma.setReceiver(newReceiver); + } + } + + private ImVar findOuterThisVar(ImClass owner) { + // in instance methods, the first parameter is typically "this" + for (ImVar p : f.getParameters()) { + ImType t = p.getType(); + if (t instanceof ImClassType) { + ImClassType ct = (ImClassType) t; + if (ct.getClassDef() == owner) { + return p; + } + } } + return null; } private ImVarAccess closureThis() { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java index 58879e31d..65a62a8f1 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java @@ -1531,5 +1531,53 @@ public void mixingLegacyOwner_newType_insideGenericClassMethod() { ); } + @Test + public void arrayListCapacity_lazyClosure_repro() { + testAssertOkLines(true, + "package test", + "", + "native testSuccess()", + "", + "constant int JASS_MAX_ARRAY_SIZE = 8190", + "", + "public function lazy(Lazy l) returns Lazy", + " return l", + "", + "public abstract class Lazy", + " T val = null", + " boolean wasRetrieved = false", + "", + " abstract function retrieve() returns T", + "", + " function get() returns T", + " if not wasRetrieved", + " val = retrieve()", + " wasRetrieved = true", + " return val", + "", + "public class ArrayList", + "", + "public class CFBuilding", + " ArrayList upgrades = null", + " Lazy hasAAUpgrade = lazy(() -> begin", + " var result = false", + " if upgrades != null", + " result = true", + " return result", + " end)", + "", + "", + "init", + " let a = new CFBuilding()", + " let b = new CFBuilding()", + " // ensure upgrades is non-null so closure does some work", + " b.upgrades = new ArrayList()", + " // invoke lazy attribute so its closure is actually referenced", + " if b.hasAAUpgrade.get() and not a.hasAAUpgrade.get()", + " testSuccess()" + ); + } + + } From 57aeb2de50d57f413d9c0ba8500e9dac05b169f9 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 1 Dec 2025 17:18:43 +0100 Subject: [PATCH 2/3] fix and test for inherited fields --- .../interpreter/ProgramState.java | 18 ++-- .../imtranslation/ClosureTranslator.java | 97 +++++++++---------- .../tests/GenericsWithTypeclassesTests.java | 35 +++++++ 3 files changed, 92 insertions(+), 58 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java index b579dff02..aa4cd1c68 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java @@ -342,15 +342,6 @@ public ILconst getHandleByIndex(int val) { return handleMap.get(val); } - public ILconstObject toObject(ILconst val) { - if (val instanceof ILconstObject) { - return (ILconstObject) val; - } else if (val instanceof ILconstInt) { - return indexToObject.get(((ILconstInt) val).getVal()); - } - throw new InterpreterException(this, "Value " + val + " (" + val.getClass().getSimpleName() + ") cannot be cast to object."); - } - public ILconstObject ensureObject(ImClassType clazz, int objectId, Element trace) { ILconstObject existing = indexToObject.get(objectId); if (existing != null) { @@ -361,6 +352,15 @@ public ILconstObject ensureObject(ImClassType clazz, int objectId, Element trace return res; } + public ILconstObject toObject(ILconst val) { + if (val instanceof ILconstObject) { + return (ILconstObject) val; + } else if (val instanceof ILconstInt) { + return indexToObject.get(((ILconstInt) val).getVal()); + } + throw new InterpreterException(this, "Value " + val + " (" + val.getClass().getSimpleName() + ") cannot be cast to object."); + } + public static class StackTrace { private final List stackFrames; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java index cb4e1134e..9838ebd59 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java @@ -30,6 +30,7 @@ public class ClosureTranslator { private Map typeVars; private ImFunction impl; private ImClass c; + private ImVar capturedThisField; public ClosureTranslator(ExprClosure e, ImTranslator tr, ImFunction f) { super(); @@ -167,6 +168,7 @@ private ImClass createClass() { } else { impl.getBody().add(JassIm.ImReturn(e, translated)); } + captureEnclosingThis(); transformTranslated(translated); typeVars = rewriteTypeVars(c); @@ -244,79 +246,76 @@ public ImType case_ImTypeVarRef(ImTypeVarRef t) { * class variables instead */ private void transformTranslated(ImExpr t) { - final List capturedLocals = Lists.newArrayList(); - final List capturedFields = Lists.newArrayList(); + final List vas = Lists.newArrayList(); + final List receiversToRewrite = Lists.newArrayList(); + ImVar closureThisVar = tr.getThisVar(e); t.accept(new ImExpr.DefaultVisitor() { @Override public void visit(ImVarAccess va) { super.visit(va); if (isLocalToOtherFunc(va.getVar())) { - capturedLocals.add(va); + vas.add(va); } } @Override public void visit(ImMemberAccess ma) { super.visit(ma); - if (isCapturedOuterField(ma)) { - capturedFields.add(ma); + if (capturedThisField != null && ma.getReceiver() instanceof ImVarAccess) { + ImVar recvVar = ((ImVarAccess) ma.getReceiver()).getVar(); + if (recvVar == closureThisVar && capturesFromEnclosingThis(owningClass(ma.getVar()))) { + receiversToRewrite.add(ma); + } } } - private boolean isCapturedOuterField(ImMemberAccess ma) { - // receiver must be *this* of the closure - if (!(ma.getReceiver() instanceof ImVarAccess)) { - return false; - } - ImVarAccess recv = (ImVarAccess) ma.getReceiver(); - if (recv.getVar() != tr.getThisVar(e)) { - return false; - } - - // field must belong to some class - var pp = ma.getVar().getParent(); - if (pp == null || !(pp.getParent() instanceof ImClass)) { - return false; - } - - ImClass owner = (ImClass) pp.getParent(); - // and it must not be a field of the closure class itself - return owner != c; - } }); - // Existing behaviour: capture locals from outer functions - for (ImVarAccess va : capturedLocals) { + for (ImVarAccess va : vas) { ImVar v = getClosureVarFor(va.getVar()); - va.replaceBy(JassIm.ImMemberAccess(e, closureThis(), - JassIm.ImTypeArguments(), v, JassIm.ImExprs())); + va.replaceBy(JassIm.ImMemberAccess(e, closureThis(), JassIm.ImTypeArguments(), v, JassIm.ImExprs())); } - // New behaviour: capture outer "this" for fields of the enclosing class - for (ImMemberAccess ma : capturedFields) { - ImVar field = ma.getVar(); - ImClass owner = (ImClass) field.getParent().getParent(); + for (ImMemberAccess ma : receiversToRewrite) { + ma.setReceiver(JassIm.ImMemberAccess(e, closureThis(), JassIm.ImTypeArguments(), capturedThisField, JassIm.ImExprs())); + } + } + + private void captureEnclosingThis() { + ImVar outerThis = getEnclosingThisVar(); + if (outerThis != null) { + capturedThisField = getClosureVarFor(outerThis); + } + } - ImVar outerThis = findOuterThisVar(owner); - if (outerThis == null) { - // no obvious owning instance – leave it, fail later with a clear error if needed - continue; + private ImVar getEnclosingThisVar() { + if (f != null && !f.getParameters().isEmpty()) { + ImVar param = f.getParameters().get(0); + if ("this".equals(param.getName()) && param.getType() instanceof ImClassType) { + return param; } + } + return null; + } - // Create/get the closure field to store outer "this" - ImVar capturedOuterThisField = getClosureVarFor(outerThis); - - // Rewrite receiver: this.capturedOuterThisField - ImExpr newReceiver = JassIm.ImMemberAccess( - e, - closureThis(), - JassIm.ImTypeArguments(), - capturedOuterThisField, - JassIm.ImExprs() - ); - ma.setReceiver(newReceiver); + private ImClass owningClass(ImVar var) { + if (var.getParent() != null && var.getParent().getParent() instanceof ImClass) { + return (ImClass) var.getParent().getParent(); } + return null; + } + + private boolean capturesFromEnclosingThis(ImClass owner) { + if (owner == null || capturedThisField == null) { + return false; + } + if (!(capturedThisField.getType() instanceof ImClassType)) { + return false; + } + + ImClass capturedClass = ((ImClassType) capturedThisField.getType()).getClassDef(); + return capturedClass == owner || capturedClass.isSubclassOf(owner); } private ImVar findOuterThisVar(ImClass owner) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java index 65a62a8f1..b8a8cfa8e 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java @@ -1578,6 +1578,41 @@ public void arrayListCapacity_lazyClosure_repro() { ); } + @Test + public void inheritedField_lazyClosure_uses_enclosing_receiver() { + testAssertOkLines(true, + "package test", + "", + "native testSuccess()", + "", + "public abstract class Lazy", + " T val = null", + " boolean wasRetrieved = false", + "", + " abstract function retrieve() returns T", + "", + " function get() returns T", + " if not wasRetrieved", + " val = retrieve()", + " wasRetrieved = true", + " return val", + "", + "public function lazy(Lazy l) returns Lazy", + " return l", + "", + "public class BaseBuilding", + " boolean hasDetector = true", + "", + "public class AdvancedBuilding extends BaseBuilding", + " Lazy detectorAvailable = lazy(() -> hasDetector)", + "", + "init", + " let b = new AdvancedBuilding()", + " b.hasDetector = true", + " if b.detectorAvailable.get()", + " testSuccess()" + ); + } } From 6d2d9c52e879164f58abb2e6bfc2123f38706e81 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 1 Dec 2025 20:25:19 +0100 Subject: [PATCH 3/3] Update EvaluateExpr.java --- .../intermediatelang/interpreter/EvaluateExpr.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java index 926d3f650..85ce3f9eb 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java @@ -404,8 +404,11 @@ public static ILaddress evaluateLvalue(ImMemberAccess va, ProgramState globalSta ILconst receiverVal = va.getReceiver().evaluate(globalState, localState); ILconstObject receiver = globalState.toObject(receiverVal); if (receiver == null && receiverVal instanceof ILconstInt && va.getReceiver().attrTyp() instanceof ImClassType) { - receiver = globalState.ensureObject((ImClassType) va.getReceiver().attrTyp(), - ((ILconstInt) receiverVal).getVal(), va.attrTrace()); + int objectId = ((ILconstInt) receiverVal).getVal(); + if (objectId != 0) { + receiver = globalState.ensureObject((ImClassType) va.getReceiver().attrTyp(), + objectId, va.attrTrace()); + } } if (receiver == null) { throw new InterpreterException(va.attrTrace(), "Null pointer dereference");