@@ -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
0 commit comments