@@ -169,31 +169,33 @@ public static function lookupLength(mixed $base, bool $strict = false): mixed
169169 public static function createContext (mixed $ context , array $ options , array $ compiledPartials ): RuntimeContext
170170 {
171171 $ parentCx = self ::$ partialContext ;
172- $ root = ['root ' => $ context ];
173172
174173 if ($ parentCx !== null ) {
175174 // Partial context: reuse the parent's already-merged helpers and partials directly.
176- // PHP copy-on-write ensures inlinePartials is only copied if in() registers a new inline partial.
177- // Inherit the parent's current frame so @index, @key, etc. remain accessible inside partials.
178- // templateClosure will update frame['root'] to reference this partial's own data['root'].
175+ // PHP copy-on-write ensures inlinePartials is only copied if fn() registers a new inline partial.
176+ // Inherit the parent's current data so @index, @key, etc. remain accessible inside partials.
177+ // Unset 'root' first to break the reference established by `$in = &$cx->data['root']` in the
178+ // calling template; a direct assignment would write through it and corrupt the caller's $in.
179+ $ data = $ parentCx ->data ;
180+ unset($ data ['root ' ]);
181+ $ data ['root ' ] = $ context ;
179182 return new RuntimeContext (
180183 helpers: $ parentCx ->helpers ,
181184 partials: $ parentCx ->partials ,
182185 inlinePartials: $ parentCx ->inlinePartials ,
183186 depths: $ parentCx ->depths ,
184- data: $ root ,
185- frame: $ parentCx ->frame ,
187+ data: $ data ,
186188 partialBlock: $ parentCx ->partialBlock ,
187189 );
188190 }
189191
190192 $ data = $ options ['data ' ] ?? [];
193+ $ data ['root ' ] = $ data ['root ' ] ?? $ context ;
191194 $ extraHelpers = $ options ['helpers ' ] ?? [];
192195 return new RuntimeContext (
193196 helpers: $ extraHelpers ? array_replace (Runtime::defaultHelpers (), $ extraHelpers ) : Runtime::defaultHelpers (),
194197 partials: array_replace ($ compiledPartials , $ options ['partials ' ] ?? []),
195- data: ['root ' => $ data ['root ' ] ?? $ context ],
196- frame: $ data ,
198+ data: $ data ,
197199 );
198200 }
199201
@@ -316,7 +318,7 @@ public static function sec(RuntimeContext $cx, mixed $value, mixed $in, ?\Closur
316318 // with no arguments, mirroring HBS.js which does not treat them as helper calls.
317319 if ($ value instanceof \Closure) {
318320 $ result = $ helperName !== null
319- ? $ value (new HelperOptions (scope: $ in , data: $ cx ->frame , cx: $ cx , cb: $ cb , inv: $ else ))
321+ ? $ value (new HelperOptions (scope: $ in , data: $ cx ->data , cx: $ cx , cb: $ cb , inv: $ else ))
320322 : $ value ();
321323 return static ::resolveBlockResult ($ cx , $ result , $ in , $ cb , $ else );
322324 }
@@ -473,7 +475,7 @@ public static function hbch(RuntimeContext $cx, \Closure $helper, string $name,
473475 if ($ numParams === 0 || $ numParams > count ($ positional )) {
474476 $ positional [] = new HelperOptions (
475477 scope: $ _this ,
476- data: $ cx ->frame ,
478+ data: $ cx ->data ,
477479 cx: $ cx ,
478480 name: $ name ,
479481 hash: $ hash ,
@@ -497,7 +499,7 @@ public static function hbbch(RuntimeContext $cx, \Closure $helper, string $name,
497499 {
498500 $ positional [] = new HelperOptions (
499501 scope: $ _this ,
500- data: $ cx ->frame ,
502+ data: $ cx ->data ,
501503 cx: $ cx ,
502504 name: $ name ,
503505 hash: $ hash ,
0 commit comments