@@ -534,6 +534,11 @@ private function MustacheStatement(MustacheStatement $mustache): string
534534 return self ::getRuntimeFunc ($ fn , self ::getRuntimeFunc ('dv ' , $ varPath ));
535535 }
536536
537+ // SubExpression path: {{(path args)}} — compile and render the sub-expression result
538+ if ($ path instanceof SubExpression) {
539+ return self ::getRuntimeFunc ($ fn , $ this ->SubExpression ($ path ));
540+ }
541+
537542 // Literal path — treat as named context lookup or helper call
538543 $ literalKey = $ this ->getLiteralKeyName ($ path );
539544
@@ -574,6 +579,12 @@ private function SubExpression(SubExpression $expression): string
574579 };
575580
576581 if ($ helperName === null ) {
582+ // Dynamic callable: path rooted at a sub-expression, e.g. ((helper).prop args)
583+ if ($ path instanceof PathExpression) {
584+ $ varPath = $ this ->PathExpression ($ path );
585+ $ args = array_map (fn ($ p ) => $ this ->compileExpression ($ p ), $ expression ->params );
586+ return self ::getRuntimeFunc ('dv ' , implode (', ' , [$ varPath , ...$ args ]));
587+ }
577588 throw new \Exception ('Sub-expression must be a helper call ' );
578589 }
579590
@@ -586,10 +597,16 @@ private function PathExpression(PathExpression $expression): string
586597 $ depth = $ expression ->depth ;
587598 $ parts = $ expression ->parts ;
588599
589- $ base = $ this ->buildBasePath ($ data , $ depth );
590-
591- // Filter out SubExpression parts for string-only operations
592- $ stringParts = self ::stringPartsOf ($ parts );
600+ // When the path head is a SubExpression (e.g. (helper).foo.bar), compile the
601+ // sub-expression as the base and use the string tail as the remaining key accesses.
602+ $ hasSubExprHead = $ expression ->head instanceof SubExpression;
603+ if ($ hasSubExprHead ) {
604+ $ base = '( ' . $ this ->SubExpression ($ expression ->head ) . ') ' ;
605+ $ stringParts = $ expression ->tail ;
606+ } else {
607+ $ base = $ this ->buildBasePath ($ data , $ depth );
608+ $ stringParts = self ::stringPartsOf ($ parts );
609+ }
593610
594611 // `this` with no parts or empty parts
595612 if (($ expression ->this_ && !$ parts ) || !$ stringParts ) {
@@ -603,8 +620,8 @@ private function PathExpression(PathExpression $expression): string
603620 return "isset( \$cx->partials['@partial-block' . \$cx->partialId]) ? true : null " ;
604621 }
605622
606- // Check block params (depth-0, non-data, non-scoped paths only)
607- if (!$ data && $ depth === 0 && !self ::scopedId ($ expression )) {
623+ // Check block params (depth-0, non-data, non-scoped paths only, not SubExpression-headed )
624+ if (!$ hasSubExprHead && ! $ data && $ depth === 0 && !self ::scopedId ($ expression )) {
608625 $ bp = $ this ->lookupBlockParam ($ stringParts [0 ]);
609626 if ($ bp !== null ) {
610627 [$ bpDepth , $ bpIndex ] = $ bp ;
@@ -631,7 +648,7 @@ private function PathExpression(PathExpression $expression): string
631648 if ($ depth > 0 ) {
632649 $ checks [] = "isset( $ base) " ;
633650 }
634- if ($ p !== '' && $ depth === 0 ) {
651+ if ($ p !== '' && $ depth === 0 && ! $ hasSubExprHead ) {
635652 $ checks [] = "isset( $ base$ p) " ;
636653 }
637654 $ baseP = "$ base$ p " ;
0 commit comments