Skip to content

Commit ae68d6a

Browse files
committed
Compile if/unless to native ternary when possible
This can double runtime performance for complex templates with conditions in nested loops. To override the built-in `if` or `unless` helper, set the corresponding value to false via the `knownHelpers` option.
1 parent d4cbe21 commit ae68d6a

2 files changed

Lines changed: 55 additions & 0 deletions

File tree

src/Compiler.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 ?? [];

tests/RegressionTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,8 @@ public static function helperProvider(): array
556556
[
557557
'desc' => 'LNC#233 - overload if helper',
558558
'template' => '{{#if foo}}FOO{{else}}BAR{{/if}}',
559+
// Opt out of compile-time inlining so the custom runtime helper is dispatched
560+
'options' => new Options(knownHelpers: ['if' => false]),
559561
'helpers' => [
560562
'if' => fn($arg, HelperOptions $options) => $options->fn(),
561563
],

0 commit comments

Comments
 (0)