Skip to content

Commit 28a09b1

Browse files
committed
Improve closure environment handling and parent pointer support
Reserve slot 0 in closure environments for the parent environment pointer, ensuring correct alignment and traversal for nested closures. Track the owning function for each captured local, update environment access logic to traverse parent chains, and initialize the parent pointer when allocating environments. This enhances support for deeply nested closures and corrects environment memory layout.
1 parent fe1a286 commit 28a09b1

File tree

2 files changed

+93
-16
lines changed

2 files changed

+93
-16
lines changed

src/compiler.ts

Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1796,7 +1796,9 @@ export class Compiler extends DiagnosticEmitter {
17961796
}
17971797
if (!instance.capturedLocals.has(local)) {
17981798
// Calculate proper byte offset with alignment
1799-
let currentOffset = 0;
1799+
// Reserve slot 0 for parent environment pointer (4 or 8 bytes depending on wasm32/64)
1800+
let ptrSize = this.options.usizeType.byteSize;
1801+
let currentOffset = ptrSize; // Start after parent pointer slot
18001802
for (let _keys = Map_keys(instance.capturedLocals), j = 0, m = _keys.length; j < m; ++j) {
18011803
let existingLocal = _keys[j];
18021804
let endOfSlot = existingLocal.envSlotIndex + existingLocal.type.byteSize;
@@ -1807,6 +1809,7 @@ export class Compiler extends DiagnosticEmitter {
18071809
let align = typeSize;
18081810
currentOffset = (currentOffset + align - 1) & ~(align - 1);
18091811
local.envSlotIndex = currentOffset;
1812+
local.envOwner = instance; // Track which function owns this capture
18101813
instance.capturedLocals.set(local, local.envSlotIndex);
18111814
}
18121815
if (!instance.envLocal) {
@@ -3306,7 +3309,9 @@ export class Compiler extends DiagnosticEmitter {
33063309
}
33073310
if (!sourceFunc.capturedLocals.has(local)) {
33083311
// Calculate proper byte offset based on current environment size with alignment
3309-
let currentOffset = 0;
3312+
// Reserve slot 0 for parent environment pointer (4 or 8 bytes depending on wasm32/64)
3313+
let ptrSize = this.options.usizeType.byteSize;
3314+
let currentOffset = ptrSize; // Start after parent pointer slot
33103315
for (let _keys = Map_keys(sourceFunc.capturedLocals), i = 0, k = _keys.length; i < k; ++i) {
33113316
let existingLocal = _keys[i];
33123317
let endOfSlot = existingLocal.envSlotIndex + existingLocal.type.byteSize;
@@ -3317,6 +3322,7 @@ export class Compiler extends DiagnosticEmitter {
33173322
let align = typeSize;
33183323
currentOffset = (currentOffset + align - 1) & ~(align - 1);
33193324
local.envSlotIndex = currentOffset;
3325+
local.envOwner = sourceFunc; // Track which function owns this capture
33203326
sourceFunc.capturedLocals.set(local, local.envSlotIndex);
33213327
}
33223328
// Ensure we have an environment local
@@ -7621,13 +7627,18 @@ export class Compiler extends DiagnosticEmitter {
76217627
}
76227628
if (local && !captures.has(local)) {
76237629
local.isCaptured = true;
7630+
// Set envOwner to track which function's environment holds this local
7631+
if (!local.envOwner) {
7632+
local.envOwner = <Function>local.parent;
7633+
}
76247634
// If envSlotIndex is already set (from variable declaration), use it
76257635
if (local.envSlotIndex >= 0) {
76267636
captures.set(local, local.envSlotIndex);
76277637
} else {
76287638
// Calculate proper byte offset based on existing captures with alignment
7629-
// We need to compute the end of the last capture (including its size)
7630-
let currentOffset = 0;
7639+
// Reserve slot 0 for parent environment pointer (4 or 8 bytes depending on wasm32/64)
7640+
let ptrSize = this.options.usizeType.byteSize;
7641+
let currentOffset = ptrSize; // Start after parent pointer slot
76317642
for (let _keys = Map_keys(captures), idx = 0, cnt = _keys.length; idx < cnt; ++idx) {
76327643
let existingLocal = _keys[idx];
76337644
// The slot index already accounts for alignment, add the size to get next free offset
@@ -8269,6 +8280,25 @@ export class Compiler extends DiagnosticEmitter {
82698280
}
82708281
break;
82718282
}
8283+
case NodeKind.Function: {
8284+
// For nested function expressions, scan their body recursively
8285+
// This is critical for deeply nested closures that capture from grandparent scopes
8286+
let funcExpr = <FunctionExpression>node;
8287+
let decl = funcExpr.declaration;
8288+
let params = decl.signature.parameters;
8289+
// Add the nested function's params to inner names
8290+
for (let i = 0, k = params.length; i < k; i++) {
8291+
innerFunctionNames.add(params[i].name.text);
8292+
}
8293+
if (decl.body) {
8294+
this.collectCapturedNames(decl.body, innerFunctionNames, outerFlow, declaredVars, capturedNames);
8295+
}
8296+
// Remove the params after scanning
8297+
for (let i = 0, k = params.length; i < k; i++) {
8298+
innerFunctionNames.delete(params[i].name.text);
8299+
}
8300+
break;
8301+
}
82728302
// Add more cases as needed for complete coverage
82738303
default: {
82748304
// For other nodes, recursively scan children
@@ -8281,14 +8311,15 @@ export class Compiler extends DiagnosticEmitter {
82818311
private computeEnvironmentSize(captures: Map<Local, i32>): i32 {
82828312
// Calculate the total size based on already-assigned slot indices
82838313
// The envSlotIndex values were already assigned during capture analysis
8284-
let maxEnd = 0;
8314+
// Slot 0 is always reserved for the parent environment pointer
8315+
let usizeSize = this.options.usizeType.byteSize;
8316+
let maxEnd = usizeSize; // Minimum size is parent pointer slot
82858317
for (let _keys = Map_keys(captures), i = 0, k = _keys.length; i < k; i++) {
82868318
let local = _keys[i];
82878319
let endOfSlot = local.envSlotIndex + local.type.byteSize;
82888320
if (endOfSlot > maxEnd) maxEnd = endOfSlot;
82898321
}
82908322
// Ensure total size is aligned to pointer size
8291-
let usizeSize = this.options.usizeType.byteSize;
82928323
let size = (maxEnd + usizeSize - 1) & ~(usizeSize - 1);
82938324
return size;
82948325
}
@@ -8417,23 +8448,45 @@ export class Compiler extends DiagnosticEmitter {
84178448
let flow = this.currentFlow;
84188449
let currentFunc = flow.sourceFunction;
84198450
let sizeTypeRef = this.options.sizeTypeRef;
8451+
let envOwner = capturedLocal.envOwner;
84208452

8421-
// Case 1: We're in the function that owns the environment
8422-
if (capturedLocal.parent == currentFunc && currentFunc.envLocal) {
8453+
// Case 1: We're in the function that owns the environment (the variable was declared here)
8454+
if (envOwner == currentFunc && currentFunc.envLocal) {
84238455
return module.local_get(currentFunc.envLocal.index, sizeTypeRef);
84248456
}
84258457

8426-
// Case 2: We're in a closure - use the cached local if available
8427-
// The environment was passed via the closure's _env field and stored to global
8428-
// before the indirect call. We cache it in a local at function entry because
8429-
// nested indirect calls can overwrite the global.
8458+
// Case 2: We're in a closure and need to access a variable from an outer scope
8459+
// Start from our closure's environment and traverse parent pointers
8460+
let envExpr: ExpressionRef;
84308461
if (currentFunc.closureEnvLocal) {
8431-
return module.local_get(currentFunc.closureEnvLocal.index, sizeTypeRef);
8462+
envExpr = module.local_get(currentFunc.closureEnvLocal.index, sizeTypeRef);
8463+
} else {
8464+
// Fallback to global (shouldn't normally happen)
8465+
let closureEnvGlobal = this.ensureClosureEnvironmentGlobal();
8466+
envExpr = module.global_get(closureEnvGlobal, sizeTypeRef);
8467+
}
8468+
8469+
// Count how many levels up we need to go
8470+
// Start from current function's outer function and walk up to find envOwner
8471+
let func: Function | null = currentFunc.outerFunction;
8472+
let depth = 0;
8473+
while (func && func != envOwner) {
8474+
depth++;
8475+
func = func.outerFunction;
8476+
}
8477+
8478+
// Traverse the parent chain: load parent pointer (at offset 0) `depth` times
8479+
for (let i = 0; i < depth; i++) {
8480+
envExpr = module.load(
8481+
this.options.usizeType.byteSize,
8482+
false, // unsigned
8483+
envExpr,
8484+
sizeTypeRef,
8485+
0 // Parent pointer is at offset 0
8486+
);
84328487
}
84338488

8434-
// Case 3: Fallback to global (shouldn't normally happen for closures)
8435-
let closureEnvGlobal = this.ensureClosureEnvironmentGlobal();
8436-
return module.global_get(closureEnvGlobal, sizeTypeRef);
8489+
return envExpr;
84378490
}
84388491

84398492
/** Compiles loading a captured variable from the closure environment. */
@@ -8514,6 +8567,27 @@ export class Compiler extends DiagnosticEmitter {
85148567
module.local_set(envLocal.index, allocExpr, false)
85158568
);
85168569

8570+
// Store parent environment pointer at slot 0
8571+
// If this is a closure (has outerFunction), use closureEnvLocal as parent
8572+
// Otherwise, parent is null (0)
8573+
let parentEnvExpr: ExpressionRef;
8574+
if (instance.closureEnvLocal) {
8575+
// This is a nested closure - use the cached closure env as parent
8576+
parentEnvExpr = module.local_get(instance.closureEnvLocal.index, sizeTypeRef);
8577+
} else {
8578+
// This is the outermost function - no parent
8579+
parentEnvExpr = options.isWasm64 ? module.i64(0) : module.i32(0);
8580+
}
8581+
stmts.push(
8582+
module.store(
8583+
usizeType.byteSize,
8584+
module.local_get(envLocal.index, sizeTypeRef),
8585+
parentEnvExpr,
8586+
sizeTypeRef,
8587+
0 // Parent pointer is at offset 0
8588+
)
8589+
);
8590+
85178591
// Initialize captured parameters in the environment
85188592
// Parameters are already initialized, so copy them now
85198593
// Local variables (var/let) will be initialized later when they're declared

src/program.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3636,6 +3636,9 @@ export class Local extends VariableLikeElement {
36363636
/** Environment slot index if captured, -1 otherwise. */
36373637
envSlotIndex: i32 = -1;
36383638

3639+
/** The function whose environment this local is stored in. Set when captured. */
3640+
envOwner: Function | null = null;
3641+
36393642
/** Constructs a new local variable. */
36403643
constructor(
36413644
/** Simple name. */

0 commit comments

Comments
 (0)