@@ -532,24 +532,19 @@ private function MustacheStatement(MustacheStatement $mustache): string
532532
533533 if ($ helperName !== null && $ type === SexprType::Ambiguous && !$ this ->context ->options ->strict ) {
534534 $ escapedKey = self ::quote ($ helperName );
535+ $ isData = $ path instanceof PathExpression && $ path ->data ;
535536 if ($ this ->context ->options ->knownHelpersOnly ) {
536- return self ::getRuntimeFunc ($ fn , self ::getRuntimeFunc ('cv ' , "\$in, $ escapedKey " ));
537+ $ lookup = $ isData ? "\$cx->data[ $ escapedKey] ?? null " : self ::getRuntimeFunc ('cv ' , "\$in, $ escapedKey " );
538+ return self ::getRuntimeFunc ($ fn , $ lookup );
537539 }
538- $ hvArgs = "\$cx, $ escapedKey, \$in " . ($ this ->context ->options ->assumeObjects ? ', true ' : '' );
540+ $ scope = $ isData ? '$cx->data ' : '$in ' ;
541+ $ hvArgs = "\$cx, $ escapedKey, $ scope " . ($ this ->context ->options ->assumeObjects ? ', true ' : '' );
539542 return self ::getRuntimeFunc ($ fn , self ::getRuntimeFunc ('hv ' , $ hvArgs ));
540543 }
541544
542545 // Simple: direct path lookup
543546 if ($ path instanceof PathExpression) {
544- // Single-segment @data vars use lambda() so closures receive HelperOptions;
545- // everything else (multi-segment, depth/scoped paths) uses dv().
546- $ varPath = $ this ->PathExpression ($ path );
547- if ($ path ->data && $ path ->depth === 0 && count ($ path ->parts ) === 1
548- && is_string ($ path ->parts [0 ]) && $ path ->parts [0 ] !== 'partial-block ' ) {
549- $ escapedName = self ::quote ($ path ->parts [0 ]);
550- return self ::getRuntimeFunc ($ fn , self ::getRuntimeFunc ('lambda ' , "\$cx, $ escapedName, $ varPath, \$in " ));
551- }
552- return self ::getRuntimeFunc ($ fn , self ::getRuntimeFunc ('dv ' , $ varPath ));
547+ return self ::getRuntimeFunc ($ fn , self ::getRuntimeFunc ('dv ' , $ this ->PathExpression ($ path )));
553548 }
554549
555550 // Literal in simple/fallthrough position (strict, assumeObjects, or knownHelpersOnly):
@@ -795,8 +790,7 @@ private function getSimpleHelperName(PathExpression|Literal $path): ?string
795790 if ($ path instanceof Literal) {
796791 return $ this ->getLiteralKeyName ($ path );
797792 }
798- if ($ path ->data
799- || $ path ->depth > 0
793+ if ($ path ->depth > 0
800794 || self ::scopedId ($ path )
801795 || count ($ path ->parts ) !== 1
802796 || !is_string ($ path ->parts [0 ])
@@ -866,43 +860,47 @@ private static function buildHbbchCall(string $helperExpr, string $escapedName,
866860 return self ::getRuntimeFunc ('hbbch ' , "\$cx, $ helperExpr, $ escapedName, $ params, \$in, $ blockFn, $ else$ trailingArgs " );
867861 }
868862
869- /**
870- * Build a resolveHelper + hbch call for dynamic inline helper dispatch.
871- */
872- private function buildResolvedHbchCall (string $ escapedName , string $ varPath , string $ params ): string
873- {
874- $ resolved = self ::getRuntimeFunc ('resolveHelper ' , "\$cx, $ escapedName, $ varPath " );
875- return self ::getRuntimeFunc ('hbch ' , "\$cx, $ resolved, $ escapedName, $ params, \$in " );
876- }
877-
878863 /**
879864 * Compile a helper call for a MustacheStatement (Helper type) or SubExpression.
880865 * @param Expression[] $params
881866 */
882867 private function compileHelperCall (?string $ helperName , Expression $ path , array $ params , ?Hash $ hash ): string
883868 {
869+ $ compiledParams = $ this ->compileParams ($ params , $ hash );
870+
884871 if ($ helperName !== null ) {
885- $ compiledParams = $ this ->compileParams ($ params , $ hash );
886872 $ escapedName = self ::quote ($ helperName );
873+ $ isData = $ path instanceof PathExpression && $ path ->data ;
887874 if ($ this ->isKnownHelper ($ helperName )) {
888875 return self ::getRuntimeFunc ('hbch ' , "\$cx, \$cx->helpers[ $ escapedName], $ escapedName, $ compiledParams, \$in " );
889876 }
890877 if ($ this ->context ->options ->knownHelpersOnly ) {
891878 $ this ->throwKnownHelpersOnly ($ helperName );
892879 }
893- return $ this ->buildResolvedHbchCall ($ escapedName , "\$in[ $ escapedName] ?? null " , $ compiledParams );
880+ $ fallback = $ isData ? "\$cx->data[ $ escapedName] ?? null " : "\$in[ $ escapedName] ?? null " ;
881+ $ resolved = self ::getRuntimeFunc ('resolveHelper ' , "\$cx, $ escapedName, $ fallback " );
882+ return self ::getRuntimeFunc ('hbch ' , "\$cx, $ resolved, $ escapedName, $ compiledParams, \$in " );
894883 }
884+
895885 if ($ path instanceof PathExpression) {
896886 $ varPath = $ this ->PathExpression ($ path );
897887 $ stringParts = array_filter ($ path ->parts , 'is_string ' );
898- if (!$ path ->data && $ path ->depth === 0 && !self ::scopedId ($ path )
899- && count ($ stringParts ) === count ($ path ->parts )) {
900- $ logicalName = self ::quote (implode ('. ' , $ stringParts ));
901- return $ this ->buildResolvedHbchCall ($ logicalName , $ varPath , $ this ->compileParams ($ params , $ hash ));
888+ if (count ($ stringParts ) === count ($ path ->parts )) {
889+ // All-string parts (foo.bar, ../fn, ./fn, @fn): scoped/depth/data paths resolve
890+ // from context only; normal paths check the helpers hash first via resolveHelper.
891+ $ escapedName = self ::quote ($ path ->original );
892+ $ resolverFn = (!$ path ->data && $ path ->depth === 0 && !self ::scopedId ($ path ))
893+ ? 'resolveHelper '
894+ : 'resolveContextHelper ' ;
895+ } else {
896+ // SubExpression-headed path (e.g. ((helper).prop args)): context-only resolution.
897+ $ escapedName = self ::quote (implode ('. ' , $ stringParts ));
898+ $ resolverFn = 'resolveContextHelper ' ;
902899 }
903- $ args = array_map ( fn ( $ p ) => $ this -> compileExpression ( $ p ) , $ params );
904- return self ::getRuntimeFunc ('dv ' , implode ( ' , ' , [ $ varPath , ... $ args ]) );
900+ $ resolved = self :: getRuntimeFunc ( $ resolverFn , "\$ cx, $ escapedName , $ varPath " );
901+ return self ::getRuntimeFunc ('hbch ' , "\$ cx, $ resolved , $ escapedName , $ compiledParams , \$ in " );
905902 }
903+
906904 throw new \Exception ('Sub-expression must be a helper call ' );
907905 }
908906
0 commit comments