@@ -285,6 +285,17 @@ private function compileBlockHelper(BlockStatement $block, string $name): string
285285 }
286286 // For inverted blocks the fn body comes from the inverse program; for normal blocks, the program.
287287 $ fnProgram = $ inverted ? $ block ->inverse : $ block ->program ;
288+
289+ // Inline if/unless as ternary — eliminates hbbch dispatch and HelperOptions allocation.
290+ // Safe because if/unless don't change scope, so $cx and $in are already correct.
291+ // Negate for 'unless' in a normal block, or 'if' in an inverted block (swapped semantics).
292+ if ($ this ->canInlineConditional ($ block , $ name , $ fnProgram ->blockParams )) {
293+ $ cond = $ this ->compileConditionalExpr ($ block ->params [0 ], $ name === ($ inverted ? 'if ' : 'unless ' ));
294+ $ body = $ this ->compileProgram ($ fnProgram );
295+ $ elseBody = $ inverted ? "'' " : $ this ->compileProgramOrEmpty ($ block ->inverse );
296+ return "( $ cond ? $ body : $ elseBody) " ;
297+ }
298+
288299 $ blockFn = $ this ->compileProgramWithBlockParams ($ fnProgram );
289300 [$ fn , $ else ] = $ inverted
290301 ? ['null ' , $ blockFn ]
@@ -299,6 +310,48 @@ private function compileBlockHelper(BlockStatement $block, string $name): string
299310 return self ::getRuntimeFunc ('hbbch ' , "\$cx, \$cx->helpers[ $ helperName], $ helperName, $ params, \$in, $ fn, $ else$ trailingArgs " );
300311 }
301312
313+ /**
314+ * Returns true when an if/unless block can be safely inlined as a ternary expression.
315+ * Requires: no hash options (e.g. includeZero), no block params, exactly one condition param.
316+ * @param string[] $bp
317+ */
318+ private function canInlineConditional (BlockStatement $ block , string $ helperName , array $ bp ): bool
319+ {
320+ return $ this ->isKnownHelper ($ helperName )
321+ && ($ helperName === 'if ' || $ helperName === 'unless ' )
322+ && count ($ block ->params ) === 1
323+ && $ block ->hash === null
324+ && !$ bp ;
325+ }
326+
327+ /**
328+ * Compile the condition expression for an inlined if/unless ternary.
329+ * For simple single-segment paths, routes through cv() which already resolves closures,
330+ * so ifvar() suffices. For all other expressions, closures at nested paths are not
331+ * invoked (not a real-world or spec concern).
332+ * @param bool $negate true for `unless` or inverted `{{^if}}`
333+ */
334+ private function compileConditionalExpr (Expression $ condExpr , bool $ negate ): string
335+ {
336+ $ part = $ condExpr instanceof PathExpression ? ($ condExpr ->parts [0 ] ?? null ) : null ;
337+ if ($ condExpr instanceof PathExpression
338+ && !$ condExpr ->data
339+ && $ condExpr ->depth === 0
340+ && is_string ($ part )
341+ && !self ::scopedId ($ condExpr )
342+ && $ this ->lookupBlockParam ($ part ) === null
343+ ) {
344+ $ val = self ::getRuntimeFunc ('cv ' , '$in, ' . self ::quote ($ part ));
345+ } else {
346+ $ savedHelperArgs = $ this ->compilingHelperArgs ;
347+ $ this ->compilingHelperArgs = true ;
348+ $ val = $ this ->compileExpression ($ condExpr );
349+ $ this ->compilingHelperArgs = $ savedHelperArgs ;
350+ }
351+ $ cond = self ::getRuntimeFunc ('ifvar ' , $ val );
352+ return $ negate ? "! $ cond " : $ cond ;
353+ }
354+
302355 private function compileDynamicBlockHelper (BlockStatement $ block , string $ name , string $ varPath = 'null ' ): string
303356 {
304357 $ bp = $ block ->program ->blockParams ?? [];
0 commit comments