Skip to content

Commit e333207

Browse files
committed
Improve closure environment handling and alignment
Enhances closure support by properly aligning captured local offsets, caching the closure environment pointer in a local to prevent overwrites from indirect calls, and updating environment size calculations. Also adds comprehensive AST node coverage for captured variable analysis and updates related tests to reflect the new closure environment management.
1 parent 03f4b7e commit e333207

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+38175
-10839
lines changed

src/compiler.ts

Lines changed: 153 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1795,12 +1795,17 @@ export class Compiler extends DiagnosticEmitter {
17951795
instance.capturedLocals = new Map();
17961796
}
17971797
if (!instance.capturedLocals.has(local)) {
1798-
// Calculate proper byte offset
1798+
// Calculate proper byte offset with alignment
17991799
let currentOffset = 0;
18001800
for (let _keys = Map_keys(instance.capturedLocals), j = 0, m = _keys.length; j < m; ++j) {
18011801
let existingLocal = _keys[j];
1802-
currentOffset += existingLocal.type.byteSize;
1802+
let endOfSlot = existingLocal.envSlotIndex + existingLocal.type.byteSize;
1803+
if (endOfSlot > currentOffset) currentOffset = endOfSlot;
18031804
}
1805+
// Align to the type's natural alignment
1806+
let typeSize = local.type.byteSize;
1807+
let align = typeSize;
1808+
currentOffset = (currentOffset + align - 1) & ~(align - 1);
18041809
local.envSlotIndex = currentOffset;
18051810
instance.capturedLocals.set(local, local.envSlotIndex);
18061811
}
@@ -1813,6 +1818,14 @@ export class Compiler extends DiagnosticEmitter {
18131818
}
18141819
}
18151820

1821+
// For closures (functions that capture from outer scope), create a local to cache
1822+
// the environment pointer. This is needed because indirect calls to other closures
1823+
// can overwrite the global $$~lib/__closure_env.
1824+
if (instance.outerFunction && !instance.closureEnvLocal) {
1825+
let closureEnvLocal = flow.addScopedLocal("$closureEnv", this.options.usizeType);
1826+
instance.closureEnvLocal = closureEnvLocal;
1827+
}
1828+
18161829
// compile statements
18171830
if (bodyNode.kind == NodeKind.Block) {
18181831
stmts = this.compileStatements((<BlockStatement>bodyNode).statements, stmts);
@@ -1851,6 +1864,27 @@ export class Compiler extends DiagnosticEmitter {
18511864
stmts[bodyStartIndex] = envAlloc;
18521865
}
18531866

1867+
// For closures (functions that capture from outer scope), emit code to cache the
1868+
// environment pointer in a local at function entry. This is needed because indirect
1869+
// calls to other closures can overwrite the global $$~lib/__closure_env.
1870+
if (instance.closureEnvLocal) {
1871+
let closureEnvLocal = instance.closureEnvLocal;
1872+
1873+
let closureEnvGlobal = this.ensureClosureEnvironmentGlobal();
1874+
let sizeTypeRef = this.options.sizeTypeRef;
1875+
let cacheEnv = module.local_set(
1876+
closureEnvLocal.index,
1877+
module.global_get(closureEnvGlobal, sizeTypeRef),
1878+
false // not a reference type
1879+
);
1880+
1881+
// Insert at the beginning of the body
1882+
for (let i = stmts.length - 1; i >= bodyStartIndex; --i) {
1883+
stmts[i + 1] = stmts[i];
1884+
}
1885+
stmts[bodyStartIndex] = cacheEnv;
1886+
}
1887+
18541888
// Make constructors return their instance pointer, and prepend a conditional
18551889
// allocation if any code path accesses `this`.
18561890
if (instance.is(CommonFlags.Constructor)) {
@@ -3271,12 +3305,17 @@ export class Compiler extends DiagnosticEmitter {
32713305
sourceFunc.capturedLocals = new Map();
32723306
}
32733307
if (!sourceFunc.capturedLocals.has(local)) {
3274-
// Calculate proper byte offset based on current environment size
3308+
// Calculate proper byte offset based on current environment size with alignment
32753309
let currentOffset = 0;
32763310
for (let _keys = Map_keys(sourceFunc.capturedLocals), i = 0, k = _keys.length; i < k; ++i) {
32773311
let existingLocal = _keys[i];
3278-
currentOffset += existingLocal.type.byteSize;
3312+
let endOfSlot = existingLocal.envSlotIndex + existingLocal.type.byteSize;
3313+
if (endOfSlot > currentOffset) currentOffset = endOfSlot;
32793314
}
3315+
// Align to the type's natural alignment
3316+
let typeSize = local.type.byteSize;
3317+
let align = typeSize;
3318+
currentOffset = (currentOffset + align - 1) & ~(align - 1);
32803319
local.envSlotIndex = currentOffset;
32813320
sourceFunc.capturedLocals.set(local, local.envSlotIndex);
32823321
}
@@ -6094,6 +6133,16 @@ export class Compiler extends DiagnosticEmitter {
60946133
// In the declaring function, we need to check if environment is set up
60956134
// In a closure, we always use the environment
60966135
if (!isInDeclaringFunction || sourceFunc.envLocal) {
6136+
// Mark the local as initialized for flow analysis
6137+
flow.setLocalFlag(localIndex, LocalFlags.Initialized);
6138+
if (type.isNullableReference) {
6139+
if (!valueType.isNullableReference || flow.isNonnull(valueExpr, type)) flow.setLocalFlag(localIndex, LocalFlags.NonNull);
6140+
else flow.unsetLocalFlag(localIndex, LocalFlags.NonNull);
6141+
}
6142+
if (type.isShortIntegerValue) {
6143+
if (!flow.canOverflow(valueExpr, type)) flow.setLocalFlag(localIndex, LocalFlags.Wrapped);
6144+
else flow.unsetLocalFlag(localIndex, LocalFlags.Wrapped);
6145+
}
60976146
let storeExpr = this.compileClosureStore(local, valueExpr, valueType);
60986147
if (tee) {
60996148
// For tee, we need to return the stored value
@@ -7541,14 +7590,26 @@ export class Compiler extends DiagnosticEmitter {
75417590
}
75427591
if (local && !captures.has(local)) {
75437592
local.isCaptured = true;
7544-
// Calculate proper byte offset based on existing captures
7545-
let currentOffset = 0;
7546-
for (let _keys = Map_keys(captures), idx = 0, cnt = _keys.length; idx < cnt; ++idx) {
7547-
let existingLocal = _keys[idx];
7548-
currentOffset += existingLocal.type.byteSize;
7593+
// If envSlotIndex is already set (from variable declaration), use it
7594+
if (local.envSlotIndex >= 0) {
7595+
captures.set(local, local.envSlotIndex);
7596+
} else {
7597+
// Calculate proper byte offset based on existing captures with alignment
7598+
// We need to compute the end of the last capture (including its size)
7599+
let currentOffset = 0;
7600+
for (let _keys = Map_keys(captures), idx = 0, cnt = _keys.length; idx < cnt; ++idx) {
7601+
let existingLocal = _keys[idx];
7602+
// The slot index already accounts for alignment, add the size to get next free offset
7603+
let endOfSlot = existingLocal.envSlotIndex + existingLocal.type.byteSize;
7604+
if (endOfSlot > currentOffset) currentOffset = endOfSlot;
7605+
}
7606+
// Align to the type's natural alignment
7607+
let typeSize = local.type.byteSize;
7608+
let align = typeSize;
7609+
currentOffset = (currentOffset + align - 1) & ~(align - 1);
7610+
local.envSlotIndex = currentOffset;
7611+
captures.set(local, local.envSlotIndex);
75497612
}
7550-
local.envSlotIndex = currentOffset;
7551-
captures.set(local, local.envSlotIndex);
75527613
}
75537614
break;
75547615
}
@@ -8102,6 +8163,64 @@ export class Compiler extends DiagnosticEmitter {
81028163
}
81038164
break;
81048165
}
8166+
case NodeKind.ElementAccess: {
8167+
let elemAccess = <ElementAccessExpression>node;
8168+
this.collectCapturedNames(elemAccess.expression, innerFunctionNames, outerFlow, declaredVars, capturedNames);
8169+
this.collectCapturedNames(elemAccess.elementExpression, innerFunctionNames, outerFlow, declaredVars, capturedNames);
8170+
break;
8171+
}
8172+
case NodeKind.PropertyAccess: {
8173+
let propAccess = <PropertyAccessExpression>node;
8174+
this.collectCapturedNames(propAccess.expression, innerFunctionNames, outerFlow, declaredVars, capturedNames);
8175+
break;
8176+
}
8177+
case NodeKind.Parenthesized: {
8178+
let paren = <ParenthesizedExpression>node;
8179+
this.collectCapturedNames(paren.expression, innerFunctionNames, outerFlow, declaredVars, capturedNames);
8180+
break;
8181+
}
8182+
case NodeKind.UnaryPrefix: {
8183+
let unary = <UnaryPrefixExpression>node;
8184+
this.collectCapturedNames(unary.operand, innerFunctionNames, outerFlow, declaredVars, capturedNames);
8185+
break;
8186+
}
8187+
case NodeKind.UnaryPostfix: {
8188+
let unary = <UnaryPostfixExpression>node;
8189+
this.collectCapturedNames(unary.operand, innerFunctionNames, outerFlow, declaredVars, capturedNames);
8190+
break;
8191+
}
8192+
case NodeKind.Ternary: {
8193+
let ternary = <TernaryExpression>node;
8194+
this.collectCapturedNames(ternary.condition, innerFunctionNames, outerFlow, declaredVars, capturedNames);
8195+
this.collectCapturedNames(ternary.ifThen, innerFunctionNames, outerFlow, declaredVars, capturedNames);
8196+
this.collectCapturedNames(ternary.ifElse, innerFunctionNames, outerFlow, declaredVars, capturedNames);
8197+
break;
8198+
}
8199+
case NodeKind.Assertion: {
8200+
let assertion = <AssertionExpression>node;
8201+
this.collectCapturedNames(assertion.expression, innerFunctionNames, outerFlow, declaredVars, capturedNames);
8202+
break;
8203+
}
8204+
case NodeKind.New: {
8205+
let newExpr = <NewExpression>node;
8206+
let args = newExpr.args;
8207+
for (let i = 0, k = args.length; i < k; i++) {
8208+
this.collectCapturedNames(args[i], innerFunctionNames, outerFlow, declaredVars, capturedNames);
8209+
}
8210+
break;
8211+
}
8212+
case NodeKind.InstanceOf: {
8213+
let instanceOf = <InstanceOfExpression>node;
8214+
this.collectCapturedNames(instanceOf.expression, innerFunctionNames, outerFlow, declaredVars, capturedNames);
8215+
break;
8216+
}
8217+
case NodeKind.Comma: {
8218+
let comma = <CommaExpression>node;
8219+
for (let i = 0, k = comma.expressions.length; i < k; i++) {
8220+
this.collectCapturedNames(comma.expressions[i], innerFunctionNames, outerFlow, declaredVars, capturedNames);
8221+
}
8222+
break;
8223+
}
81058224
// Add more cases as needed for complete coverage
81068225
default: {
81078226
// For other nodes, recursively scan children
@@ -8112,19 +8231,17 @@ export class Compiler extends DiagnosticEmitter {
81128231

81138232
/** Computes the total size needed for a closure environment. */
81148233
private computeEnvironmentSize(captures: Map<Local, i32>): i32 {
8115-
let size = 0;
8116-
let usizeSize = this.options.usizeType.byteSize;
8234+
// Calculate the total size based on already-assigned slot indices
8235+
// The envSlotIndex values were already assigned during capture analysis
8236+
let maxEnd = 0;
81178237
for (let _keys = Map_keys(captures), i = 0, k = _keys.length; i < k; i++) {
81188238
let local = _keys[i];
8119-
// Align each slot to the type's natural alignment (at least pointer size)
8120-
let typeSize = local.type.byteSize;
8121-
let align = typeSize < usizeSize ? usizeSize : typeSize;
8122-
size = (size + align - 1) & ~(align - 1);
8123-
local.envSlotIndex = size; // Store the byte offset as slot index
8124-
size += typeSize;
8239+
let endOfSlot = local.envSlotIndex + local.type.byteSize;
8240+
if (endOfSlot > maxEnd) maxEnd = endOfSlot;
81258241
}
81268242
// Ensure total size is aligned to pointer size
8127-
size = (size + usizeSize - 1) & ~(usizeSize - 1);
8243+
let usizeSize = this.options.usizeType.byteSize;
8244+
let size = (maxEnd + usizeSize - 1) & ~(usizeSize - 1);
81288245
return size;
81298246
}
81308247

@@ -8258,9 +8375,15 @@ export class Compiler extends DiagnosticEmitter {
82588375
return module.local_get(currentFunc.envLocal.index, sizeTypeRef);
82598376
}
82608377

8261-
// Case 2: We're in a closure - load environment from the global
8378+
// Case 2: We're in a closure - use the cached local if available
82628379
// The environment was passed via the closure's _env field and stored to global
8263-
// before the indirect call
8380+
// before the indirect call. We cache it in a local at function entry because
8381+
// nested indirect calls can overwrite the global.
8382+
if (currentFunc.closureEnvLocal) {
8383+
return module.local_get(currentFunc.closureEnvLocal.index, sizeTypeRef);
8384+
}
8385+
8386+
// Case 3: Fallback to global (shouldn't normally happen for closures)
82648387
let closureEnvGlobal = this.ensureClosureEnvironmentGlobal();
82658388
return module.global_get(closureEnvGlobal, sizeTypeRef);
82668389
}
@@ -8544,7 +8667,7 @@ export class Compiler extends DiagnosticEmitter {
85448667
// Handle closure access BEFORE initialization check
85458668
// Captured variables are stored in the environment, not in flow locals
85468669
if (!local.declaredByFlow(flow)) {
8547-
// Closure: load from environment
8670+
// Closure: load from environment (from inner function)
85488671
if (local.isCaptured && local.envSlotIndex >= 0) {
85498672
this.currentType = localType;
85508673
return this.compileClosureLoad(local, expression);
@@ -8557,6 +8680,13 @@ export class Compiler extends DiagnosticEmitter {
85578680
);
85588681
return module.unreachable();
85598682
}
8683+
// Also handle captured locals in the declaring function
8684+
// When a local is captured, its value lives in the environment
8685+
let sourceFunc = flow.sourceFunction;
8686+
if (local.isCaptured && local.envSlotIndex >= 0 && sourceFunc.envLocal) {
8687+
this.currentType = localType;
8688+
return this.compileClosureLoad(local, expression);
8689+
}
85608690

85618691
if (!flow.isLocalFlag(localIndex, LocalFlags.Initialized)) {
85628692
this.error(

src/program.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3805,6 +3805,10 @@ export class Function extends TypedElement {
38053805
/** The outer function whose environment this closure accesses. */
38063806
outerFunction: Function | null = null;
38073807

3808+
/** Local variable in a closure function that caches the environment pointer from the global.
3809+
* This is needed because indirect calls can overwrite the global. */
3810+
closureEnvLocal: Local | null = null;
3811+
38083812
/** Pre-scanned names of captured variables (set before compilation, used to mark locals). */
38093813
preCapturedNames: Set<string> | null = null;
38103814

0 commit comments

Comments
 (0)