@@ -51,13 +51,6 @@ final class Compiler
5151 */
5252 private bool $ compilingHelperArgs = false ;
5353
54- /**
55- * Tracks the current block-program nesting depth. 0 = root program, >0 = inside a block body.
56- * Mirrors HBS.js's lastContext counter, which controls whether lookupOnContext() uses
57- * depthedLookup() (at root, lastContext=0) or pushContext() (inside blocks, lastContext>0).
58- */
59- private int $ compileProgramDepth = 0 ;
60-
6154 private int $ nextProgramId = 0 ;
6255 /** @var string[] */
6356 private array $ programDefs = [];
@@ -82,7 +75,6 @@ public function compile(Program $program, Context $context): string
8275 {
8376 $ this ->context = $ context ;
8477 $ this ->blockParamValues = [];
85- $ this ->compileProgramDepth = 0 ;
8678 $ this ->nextProgramId = 0 ;
8779 $ this ->programDefs = [];
8880 $ this ->programDepStack = [[]];
@@ -265,9 +257,7 @@ private function compileProgramWithBlockParams(Program $program): string
265257 array_unshift ($ this ->blockParamValues , $ bp );
266258 }
267259 $ this ->programDepStack [] = [];
268- $ this ->compileProgramDepth ++;
269260 $ body = $ this ->compileProgram ($ program );
270- $ this ->compileProgramDepth --;
271261 if ($ bp ) {
272262 array_shift ($ this ->blockParamValues );
273263 }
@@ -552,7 +542,7 @@ private function MustacheStatement(MustacheStatement $mustache): string
552542 // For all other simple paths (multi-segment, scoped, depth, data): use dv() with zero args,
553543 // matching HBS.js container.lambda which also passes no positional arguments.
554544 if ($ path instanceof PathExpression) {
555- if ($ helperName !== null && !$ path ->data && $ this ->context ->options ->knownHelpersOnly ) {
545+ if ($ helperName !== null && !$ path ->data && $ this ->context ->options ->knownHelpersOnly && ! $ this -> context -> options -> compat ) {
556546 $ cvArgs = '$in, ' . self ::quote ($ helperName ) . ($ this ->context ->options ->strict ? ', true ' : '' );
557547 return self ::getRuntimeFunc ($ fn , self ::getRuntimeFunc ('cv ' , $ cvArgs ));
558548 }
@@ -605,9 +595,11 @@ private function PathExpression(PathExpression $path): string
605595 }
606596
607597 $ isLength = end ($ stringParts ) === 'length ' ;
598+ $ isCurrentContextPath = !$ hasSubExprHead && !$ data && $ depth === 0 ;
599+ $ scoped = $ isCurrentContextPath && self ::scopedId ($ path );
608600
609601 // Check block params (depth-0, non-data, non-scoped paths only, not SubExpression-headed)
610- if (! $ hasSubExprHead && !$ data && $ depth === 0 && ! self :: scopedId ( $ path ) ) {
602+ if ($ isCurrentContextPath && !$ scoped ) {
611603 $ bp = $ this ->lookupBlockParam ($ path ->head );
612604 if ($ bp !== null ) {
613605 [$ bpDepth , $ bpIndex ] = $ bp ;
@@ -625,11 +617,11 @@ private function PathExpression(PathExpression $path): string
625617 if ($ isLength ) {
626618 $ partsExceptLength = array_slice ($ stringParts , 0 , -1 );
627619 return $ this ->buildLookupLength (
628- $ this ->compileModeAwareLookup ($ base , $ partsExceptLength , $ path ->original ),
620+ $ this ->compileModeAwareLookup ($ base , $ partsExceptLength , $ path ->original , $ scoped ),
629621 );
630622 }
631623
632- return $ this ->compileModeAwareLookup ($ base , $ stringParts , $ path ->original );
624+ return $ this ->compileModeAwareLookup ($ base , $ stringParts , $ path ->original , $ scoped );
633625 }
634626
635627 /**
@@ -954,32 +946,29 @@ private function buildLookupLength(string $parent): string
954946 * Compile a mode-aware path access expression for the given base and parts.
955947 * @param string[] $parts
956948 */
957- private function compileModeAwareLookup (string $ base , array $ parts , string $ original ): string
949+ private function compileModeAwareLookup (string $ base , array $ parts , string $ original, bool $ scoped = false ): string
958950 {
959951 if (!$ parts ) {
960952 return $ base ;
961953 }
962- // Compat: only applies to $in (not ../, @data, or block-param bases).
963- // Mirrors HBS.js lookupOnContext(), which calls depthedLookup(parts[0]) at root (lastContext=0).
964- if ($ this ->context ->options ->compat && $ base === '$in ' ) {
954+ // Compat depths-walk: resolve parts[0] via compatLookup() rather than $in directly.
955+ // Mirrors HBS.js lookupOnContext() calling depthedLookup(parts[0]) when !scoped.
956+ if ($ this ->context ->options ->compat && $ base === '$in ' && !$ scoped ) {
957+ $ compatBase = self ::getRuntimeFunc ('compatLookup ' , '$cx, $in, ' . self ::quote ($ parts [0 ]));
958+ $ remaining = array_slice ($ parts , 1 );
965959 if (!$ this ->context ->options ->strict ) {
966- // Non-strict: compatLookup resolves the root key via depths-walk; remaining parts
967- // are accessed inline, matching HBS.js where only lookup() uses depths, and
968- // subsequent nameLookup() calls in compiled code handle the rest of the path.
969- $ compatBase = self ::getRuntimeFunc ('compatLookup ' , '$cx, $in, ' . self ::quote ($ parts [0 ]));
970- $ remaining = array_slice ($ parts , 1 );
960+ // Only parts[0] is depth-walked; remaining parts are accessed inline.
971961 return $ remaining ? $ compatBase . self ::buildKeyAccess ($ remaining ) . ' ?? null ' : $ compatBase ;
972962 }
973- // Strict at root: depths-walk parts[0] to get the base, then fall through to strict
974- // resolution below. HBS.js emits strict(depthedLookup(parts[0]), parts[last], loc),
975- // which fails when the resolved value doesn't contain the key.
976- if ($ this ->compileProgramDepth === 0 ) {
977- $ base = self ::getRuntimeFunc ('compatLookup ' , '$cx, $in, ' . self ::quote ($ parts [0 ]));
978- // Consume parts[0] (already used for the compat base); for single-part paths,
979- // reuse it as the strict terminal — parts[last] == parts[0] in HBS.js's indexing.
980- $ parts = array_slice ($ parts , 1 ) ?: $ parts ;
963+ // In strict+compat, HBS.js sets path.strict=false for helper params, so a single-part
964+ // param returns the compat result directly with no strict terminal check.
965+ if (!$ remaining && $ this ->compilingHelperArgs ) {
966+ return $ compatBase ;
981967 }
982- // At non-root: fall through to strict handling with $in as the base.
968+ // Fall through: use compatLookup as the new base for strict resolution.
969+ // For single-part paths, retain parts[0] as the strict terminal key.
970+ $ base = $ compatBase ;
971+ $ parts = $ remaining ?: $ parts ;
983972 }
984973 if ($ this ->context ->options ->assumeObjects || ($ this ->context ->options ->strict && $ this ->compilingHelperArgs )) {
985974 // Use nullCheck chain for assumeObjects and helper arguments in strict mode.
0 commit comments