Skip to content

Commit 52d895e

Browse files
committed
Implement capturing outer this in closures
1 parent 89f5a71 commit 52d895e

File tree

4 files changed

+145
-7
lines changed

4 files changed

+145
-7
lines changed

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -401,20 +401,30 @@ public ILconst get() {
401401

402402
public static ILaddress evaluateLvalue(ImMemberAccess va, ProgramState globalState, LocalState localState) {
403403
ImVar v = va.getVar();
404-
ILconstObject receiver = globalState.toObject(va.getReceiver().evaluate(globalState, localState));
404+
ILconst receiverVal = va.getReceiver().evaluate(globalState, localState);
405+
ILconstObject receiver = globalState.toObject(receiverVal);
406+
if (receiver == null && receiverVal instanceof ILconstInt && va.getReceiver().attrTyp() instanceof ImClassType) {
407+
receiver = globalState.ensureObject((ImClassType) va.getReceiver().attrTyp(),
408+
((ILconstInt) receiverVal).getVal(), va.attrTrace());
409+
}
410+
if (receiver == null) {
411+
throw new InterpreterException(va.attrTrace(), "Null pointer dereference");
412+
}
413+
ILconstObject receiverFinal = receiver;
405414
List<Integer> indexes =
406415
va.getIndexes().stream()
407416
.map(ie -> ((ILconstInt) ie.evaluate(globalState, localState)).getVal())
408417
.collect(Collectors.toList());
418+
List<Integer> indexesFinal = indexes;
409419
return new ILaddress() {
410420
@Override
411421
public void set(ILconst value) {
412-
receiver.set(v, indexes, value);
422+
receiverFinal.set(v, indexesFinal, value);
413423
}
414424

415425
@Override
416426
public ILconst get() {
417-
return receiver.get(v, indexes)
427+
return receiverFinal.get(v, indexesFinal)
418428
.orElseGet(() -> va.attrTyp().defaultValue());
419429
}
420430
};

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,16 @@ public ILconstObject toObject(ILconst val) {
351351
throw new InterpreterException(this, "Value " + val + " (" + val.getClass().getSimpleName() + ") cannot be cast to object.");
352352
}
353353

354+
public ILconstObject ensureObject(ImClassType clazz, int objectId, Element trace) {
355+
ILconstObject existing = indexToObject.get(objectId);
356+
if (existing != null) {
357+
return existing;
358+
}
359+
ILconstObject res = new ILconstObject(clazz, objectId, trace);
360+
indexToObject.put(objectId, res);
361+
return res;
362+
}
363+
354364
public static class StackTrace {
355365
private final List<ILStackFrame> stackFrames;
356366

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,23 +244,93 @@ public ImType case_ImTypeVarRef(ImTypeVarRef t) {
244244
* class variables instead
245245
*/
246246
private void transformTranslated(ImExpr t) {
247-
final List<ImVarAccess> vas = Lists.newArrayList();
247+
final List<ImVarAccess> capturedLocals = Lists.newArrayList();
248+
final List<ImMemberAccess> capturedFields = Lists.newArrayList();
249+
248250
t.accept(new ImExpr.DefaultVisitor() {
249251
@Override
250252
public void visit(ImVarAccess va) {
251253
super.visit(va);
252254
if (isLocalToOtherFunc(va.getVar())) {
253-
vas.add(va);
255+
capturedLocals.add(va);
256+
}
257+
}
258+
259+
@Override
260+
public void visit(ImMemberAccess ma) {
261+
super.visit(ma);
262+
if (isCapturedOuterField(ma)) {
263+
capturedFields.add(ma);
254264
}
255265
}
256266

267+
private boolean isCapturedOuterField(ImMemberAccess ma) {
268+
// receiver must be *this* of the closure
269+
if (!(ma.getReceiver() instanceof ImVarAccess)) {
270+
return false;
271+
}
272+
ImVarAccess recv = (ImVarAccess) ma.getReceiver();
273+
if (recv.getVar() != tr.getThisVar(e)) {
274+
return false;
275+
}
257276

277+
// field must belong to some class
278+
var pp = ma.getVar().getParent();
279+
if (pp == null || !(pp.getParent() instanceof ImClass)) {
280+
return false;
281+
}
282+
283+
ImClass owner = (ImClass) pp.getParent();
284+
// and it must not be a field of the closure class itself
285+
return owner != c;
286+
}
258287
});
259288

260-
for (ImVarAccess va : vas) {
289+
// Existing behaviour: capture locals from outer functions
290+
for (ImVarAccess va : capturedLocals) {
261291
ImVar v = getClosureVarFor(va.getVar());
262-
va.replaceBy(JassIm.ImMemberAccess(e, closureThis(), JassIm.ImTypeArguments(), v, JassIm.ImExprs()));
292+
va.replaceBy(JassIm.ImMemberAccess(e, closureThis(),
293+
JassIm.ImTypeArguments(), v, JassIm.ImExprs()));
294+
}
295+
296+
// New behaviour: capture outer "this" for fields of the enclosing class
297+
for (ImMemberAccess ma : capturedFields) {
298+
ImVar field = ma.getVar();
299+
ImClass owner = (ImClass) field.getParent().getParent();
300+
301+
ImVar outerThis = findOuterThisVar(owner);
302+
if (outerThis == null) {
303+
// no obvious owning instance – leave it, fail later with a clear error if needed
304+
continue;
305+
}
306+
307+
// Create/get the closure field to store outer "this"
308+
ImVar capturedOuterThisField = getClosureVarFor(outerThis);
309+
310+
// Rewrite receiver: this.capturedOuterThisField
311+
ImExpr newReceiver = JassIm.ImMemberAccess(
312+
e,
313+
closureThis(),
314+
JassIm.ImTypeArguments(),
315+
capturedOuterThisField,
316+
JassIm.ImExprs()
317+
);
318+
ma.setReceiver(newReceiver);
319+
}
320+
}
321+
322+
private ImVar findOuterThisVar(ImClass owner) {
323+
// in instance methods, the first parameter is typically "this"
324+
for (ImVar p : f.getParameters()) {
325+
ImType t = p.getType();
326+
if (t instanceof ImClassType) {
327+
ImClassType ct = (ImClassType) t;
328+
if (ct.getClassDef() == owner) {
329+
return p;
330+
}
331+
}
263332
}
333+
return null;
264334
}
265335

266336
private ImVarAccess closureThis() {

de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1531,5 +1531,53 @@ public void mixingLegacyOwner_newType_insideGenericClassMethod() {
15311531
);
15321532
}
15331533

1534+
@Test
1535+
public void arrayListCapacity_lazyClosure_repro() {
1536+
testAssertOkLines(true,
1537+
"package test",
1538+
"",
1539+
"native testSuccess()",
1540+
"",
1541+
"constant int JASS_MAX_ARRAY_SIZE = 8190",
1542+
"",
1543+
"public function lazy<T:>(Lazy<T> l) returns Lazy<T>",
1544+
" return l",
1545+
"",
1546+
"public abstract class Lazy<T:>",
1547+
" T val = null",
1548+
" boolean wasRetrieved = false",
1549+
"",
1550+
" abstract function retrieve() returns T",
1551+
"",
1552+
" function get() returns T",
1553+
" if not wasRetrieved",
1554+
" val = retrieve()",
1555+
" wasRetrieved = true",
1556+
" return val",
1557+
"",
1558+
"public class ArrayList<T:>",
1559+
"",
1560+
"public class CFBuilding",
1561+
" ArrayList<CFBuilding> upgrades = null",
1562+
" Lazy<boolean> hasAAUpgrade = lazy<boolean>(() -> begin",
1563+
" var result = false",
1564+
" if upgrades != null",
1565+
" result = true",
1566+
" return result",
1567+
" end)",
1568+
"",
1569+
"",
1570+
"init",
1571+
" let a = new CFBuilding()",
1572+
" let b = new CFBuilding()",
1573+
" // ensure upgrades is non-null so closure does some work",
1574+
" b.upgrades = new ArrayList<CFBuilding>()",
1575+
" // invoke lazy attribute so its closure is actually referenced",
1576+
" if b.hasAAUpgrade.get() and not a.hasAAUpgrade.get()",
1577+
" testSuccess()"
1578+
);
1579+
}
1580+
1581+
15341582

15351583
}

0 commit comments

Comments
 (0)