@@ -66,6 +66,22 @@ final class Compiler
6666 */
6767 private bool $ compilingHelperArgs = false ;
6868
69+ private int $ nextProgramId = 0 ;
70+ /** @var string[] */
71+ private array $ programDefs = [];
72+ /**
73+ * Stack of dep arrays — one per active compilation level. Each entry tracks
74+ * the $pN variables directly referenced at that level (direct children only;
75+ * transitive deps are captured via the use() chains of inner closures).
76+ * @var string[][]
77+ */
78+ private array $ programDepStack = [];
79+ /**
80+ * Deps for the top-level render closure — set by compile() after compileProgram() returns.
81+ * @var string[]
82+ */
83+ private array $ renderDeps = [];
84+
6985 public function __construct (
7086 private readonly Parser $ parser ,
7187 ) {}
@@ -76,7 +92,12 @@ public function compile(Program $program, Context $context): string
7692 $ this ->blockParamValues = [];
7793 $ this ->bpRefStack = [];
7894 $ this ->lastCompileProgramHadDirectBpRef = false ;
79- return $ this ->compileProgram ($ program );
95+ $ this ->nextProgramId = 0 ;
96+ $ this ->programDefs = [];
97+ $ this ->programDepStack = [[]];
98+ $ code = $ this ->compileProgram ($ program );
99+ $ this ->renderDeps = array_pop ($ this ->programDepStack );
100+ return $ code ;
80101 }
81102
82103 /**
@@ -87,8 +108,10 @@ public function compile(Program $program, Context $context): string
87108 public function composePHPRender (string $ code ): string
88109 {
89110 $ partials = implode (", \n" , $ this ->context ->partialCode );
90- $ closure = self ::templateClosure ($ code , $ partials , "\n \$in = & \$cx->data['root']; " );
91- return "use " . Runtime::class . " as LR; \nreturn $ closure; " ;
111+ $ useVars = implode (', ' , $ this ->renderDeps );
112+ $ closure = self ::templateClosure ($ code , $ partials , "\n \$in = & \$cx->data['root']; " , $ useVars );
113+ $ defs = $ this ->programDefs ? implode ("\n" , $ this ->programDefs ) . "\n" : '' ;
114+ return "use " . Runtime::class . " as LR; \n{$ defs }return $ closure; " ;
92115 }
93116
94117 /**
@@ -197,8 +220,9 @@ private function BlockStatement(BlockStatement $block): string
197220 }
198221 }
199222
200- $ nameArg = !$ this ->context ->options ->knownHelpersOnly && (!$ inverted || $ type === SexprType::Ambiguous) ? ", $ escapedName " : '' ;
201- return self ::getRuntimeFunc ('sec ' , "\$cx, $ var, \$in, $ fn, $ else$ nameArg " );
223+ $ nameArg = !$ this ->context ->options ->knownHelpersOnly && (!$ inverted || $ type === SexprType::Ambiguous) ? ", $ escapedName " : ', null ' ;
224+ $ outerBpArg = $ this ->blockParamValues ? ', $blockParams ' : '' ;
225+ return self ::getRuntimeFunc ('sec ' , "\$cx, $ var, \$in, $ fn, $ else$ nameArg$ outerBpArg " );
202226 }
203227
204228 private function isKnownHelper (string $ helperName ): bool
@@ -226,41 +250,62 @@ private function classifySexpr(?string $simpleName, array $params, ?Hash $hash):
226250 return ($ params || $ hash ) ? SexprType::Helper : SexprType::Simple;
227251 }
228252
229- /** Returns '$blockParams' when inside a block-param scope (for use() capture), '' otherwise. */
230- private function blockParamsUseVars (): string
253+ /**
254+ * Build the use() clause string for an inline partial body closure.
255+ * Prepends $blockParams when inside a block-param scope (marking bpRefStack),
256+ * then appends any hoisted program deps added since $depsBefore.
257+ */
258+ private function buildInlineUseClause (int $ depsBefore ): string
231259 {
232- return $ this ->blockParamValues ? '$blockParams ' : '' ;
260+ $ bodyDeps = array_slice ($ this ->programDepStack [array_key_last ($ this ->programDepStack )], $ depsBefore );
261+ $ vars = [];
262+ if ($ this ->blockParamValues ) {
263+ if ($ this ->bpRefStack ) {
264+ $ this ->bpRefStack [array_key_last ($ this ->bpRefStack )] = true ;
265+ }
266+ $ vars [] = '$blockParams ' ;
267+ }
268+ return implode (', ' , array_merge ($ vars , $ bodyDeps ));
233269 }
234270
235271 /**
236272 * Compile a block program, pushing/popping its block params around the compilation.
237- * Returns a PHP closure string: the signature varies based on whether the program declares or
238- * inherits block params, and a $sc preamble is added when depths are accessed multiple times .
273+ * Returns a $pN variable name referencing a hoisted closure. The signature uses
274+ * array $blockParams = [] when the program declares or inherits block params .
239275 */
240276 private function compileProgramWithBlockParams (Program $ program ): string
241277 {
242278 $ bp = $ program ->blockParams ;
243279 if ($ bp ) {
244280 array_unshift ($ this ->blockParamValues , $ bp );
245281 }
282+ $ this ->programDepStack [] = [];
246283 $ body = $ this ->compileProgram ($ program );
247284 if ($ bp ) {
248285 array_shift ($ this ->blockParamValues );
249286 }
250287
251- $ declaresBp = (bool ) $ bp ;
252- $ inheritsBp = $ this ->lastCompileProgramHadDirectBpRef ;
288+ $ usesBp = $ bp || $ this ->lastCompileProgramHadDirectBpRef ;
253289 $ preamble = '' ;
254290 if (str_contains ($ body , '$cx->depths[count($cx->depths)- ' )) {
255291 $ preamble = '$sc=count($cx->depths); ' ;
256292 $ body = str_replace ('$cx->depths[count($cx->depths)- ' , '$cx->depths[$sc- ' , $ body );
257293 }
258- $ sig = match (true ) {
259- $ declaresBp => "function( \$cx, \$in, array \$blockParams = []) " ,
260- $ inheritsBp => "function( \$cx, \$in) use ( \$blockParams) " ,
261- default => "function( \$cx, \$in) " ,
262- };
263- return "$ sig { {$ preamble }return $ body;} " ;
294+ $ sig = $ usesBp
295+ ? "function( \$cx, \$in, array \$blockParams = []) "
296+ : "function( \$cx, \$in) " ;
297+
298+ $ deps = array_pop ($ this ->programDepStack );
299+ if ($ deps ) {
300+ $ sig .= ' use ( ' . implode (', ' , $ deps ) . ') ' ;
301+ }
302+
303+ $ id = $ this ->nextProgramId ++;
304+ $ var = "\$p {$ id }" ;
305+ $ this ->programDefs [] = "{$ var } = {$ sig } { {$ preamble }return {$ body };}; " ;
306+ // Propagate this var to the parent dep level so callers can capture it.
307+ $ this ->programDepStack [array_key_last ($ this ->programDepStack )][] = $ var ;
308+ return $ var ;
264309 }
265310
266311 private function compileBlockHelper (BlockStatement $ block , string $ name ): string
@@ -366,14 +411,15 @@ private function DecoratorBlock(BlockStatement $block): string
366411 $ partialName = $ this ->getLiteralKeyName ($ firstArg );
367412 }
368413
414+ $ depsBefore = count ($ this ->programDepStack [array_key_last ($ this ->programDepStack )]);
369415 $ body = $ this ->compileProgramOrEmpty ($ block ->program );
370416
371417 // Register in usedPartial so {{> partialName}} can compile without error.
372418 // Do NOT add to partialCode - `in()` handles runtime registration, keeping inline partials block-scoped.
373419 $ this ->context ->usedPartial [$ partialName ] = '' ;
374420
375- // Capture $blockParams if we're inside a block-param scope so the inline partial body can access them.
376- $ useVars = $ this ->blockParamsUseVars ( );
421+ // Capture $blockParams and any hoisted program vars so the inline partial body can access them.
422+ $ useVars = $ this ->buildInlineUseClause ( $ depsBefore );
377423 $ escapedName = self ::quote ($ partialName );
378424 return self ::getRuntimeFunc ('in ' , "\$cx, $ escapedName, " . self ::templateClosure ($ body , useVars: $ useVars ));
379425 }
@@ -424,6 +470,7 @@ private function PartialBlockStatement(PartialBlockStatement $statement): string
424470 }
425471
426472 $ name = $ statement ->name ;
473+ $ depsBefore = count ($ this ->programDepStack [array_key_last ($ this ->programDepStack )]);
427474 $ body = $ this ->compileProgram ($ statement ->program );
428475 $ partialName = null ;
429476 $ found = false ;
@@ -450,8 +497,8 @@ private function PartialBlockStatement(PartialBlockStatement $statement): string
450497
451498 $ vars = $ this ->compilePartialParams ($ statement ->params , $ statement ->hash );
452499
453- // Capture $blockParams if we're inside a block-param scope so the partial block body can access them.
454- $ useVars = $ this ->blockParamsUseVars ( );
500+ // Capture $blockParams and any hoisted program vars so the partial block body can access them.
501+ $ useVars = $ this ->buildInlineUseClause ( $ depsBefore );
455502 $ bodyClosure = self ::templateClosure ($ body , useVars: $ useVars );
456503
457504 if ($ partialName !== null && !$ found ) {
@@ -673,9 +720,16 @@ private function compilePartialTemplate(string $name, string $template): void
673720 }
674721
675722 $ program = $ this ->parser ->parse ($ template , $ this ->context ->options ->ignoreStandalone );
676- $ code = (new Compiler ($ this ->parser ))->compile ($ program , $ this ->context );
723+ $ partialCompiler = new self ($ this ->parser );
724+ $ code = $ partialCompiler ->compile ($ program , $ this ->context );
725+ $ closureExpr = self ::templateClosure ($ code , useVars: implode (', ' , $ partialCompiler ->renderDeps ));
726+
727+ if ($ partialCompiler ->programDefs ) {
728+ $ defs = implode ("\n" , $ partialCompiler ->programDefs ) . "\n" ;
729+ $ closureExpr = "(static function() { \n{$ defs }return {$ closureExpr }; \n})() " ;
730+ }
677731
678- $ this ->context ->partialCode [$ name ] = self ::quote ($ name ) . ' => ' . self :: templateClosure ( $ code ) ;
732+ $ this ->context ->partialCode [$ name ] = self ::quote ($ name ) . ' => ' . $ closureExpr ;
679733 }
680734
681735 public function handleDynamicPartials (): void
@@ -814,7 +868,7 @@ private static function buildKeyAccess(array $parts): string
814868
815869 private function buildBlockHelperCall (string $ helperExpr , string $ escapedName , BlockStatement $ block , string $ fn , string $ else ): string
816870 {
817- // Mark the enclosing bp-declaring closure as needing to capture $blockParams via use() .
871+ // Mark the enclosing closure as needing $blockParams in its signature .
818872 if ($ this ->blockParamValues && $ this ->bpRefStack ) {
819873 $ this ->bpRefStack [array_key_last ($ this ->bpRefStack )] = true ;
820874 }
@@ -823,7 +877,7 @@ private function buildBlockHelperCall(string $helperExpr, string $escapedName, B
823877 $ params = $ this ->compileParams ($ block ->params , $ block ->hash );
824878
825879 // omit trailing bpCount/outerBp args when both are zero/empty
826- $ trailingArgs = ($ bpCount > 0 || $ outerBp !== '[] ' ) ? ", $ bpCount , $ outerBp " : '' ;
880+ $ trailingArgs = ($ bpCount > 0 || $ outerBp !== '[] ' ) ? ", $ outerBp , $ bpCount " : '' ;
827881 $ args = "\$cx, $ helperExpr, $ escapedName, $ params, \$in, $ fn, $ else " ;
828882 return self ::getRuntimeFunc ('hbbch ' , $ args . $ trailingArgs );
829883 }
0 commit comments