Skip to content

Commit 7544b74

Browse files
committed
compat fixes
1 parent 2bccbe4 commit 7544b74

2 files changed

Lines changed: 57 additions & 32 deletions

File tree

src/Compiler.php

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -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.

tests/RegressionTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2135,6 +2135,42 @@ public static function contextProvider(): array
21352135
'data' => ['outer' => ['inner' => 'Hello']],
21362136
'expected' => 'Hello',
21372137
],
2138+
'compat+strict: items array passed as {{#each}} arg from root context' => [
2139+
'template' => '{{#each items}}{{.}}{{/each}}',
2140+
'options' => new Options(compat: true, strict: true),
2141+
'data' => ['items' => ['a', 'b']],
2142+
'expected' => 'ab',
2143+
],
2144+
'compat+strict: {{#with}} arg from root context, inner body accesses parent via depths' => [
2145+
'template' => '{{#with inner}}{{foo}}{{/with}}',
2146+
'options' => new Options(compat: true, strict: true),
2147+
'data' => ['inner' => ['x' => 1], 'foo' => 'bar'],
2148+
'expected' => 'bar',
2149+
],
2150+
'compat+strict: multi-part path as {{#each}} arg still resolves correctly' => [
2151+
'template' => '{{#each foo.items}}{{.}}{{/each}}',
2152+
'options' => new Options(compat: true, strict: true),
2153+
'data' => ['foo' => ['items' => ['x', 'y']]],
2154+
'expected' => 'xy',
2155+
],
2156+
'compat+strict: multi-part path inside block body resolves first segment from parent' => [
2157+
'template' => '{{#each foo.list}}{{bar.baz}}{{/each}}',
2158+
'options' => new Options(compat: true, strict: true),
2159+
'data' => ['bar' => ['baz' => 'found'], 'foo' => ['list' => [[]]]],
2160+
'expected' => 'found',
2161+
],
2162+
'compat: scoped path (./name) uses current context, not depths' => [
2163+
'template' => '{{#each items}}{{./name}}{{/each}}',
2164+
'options' => new Options(compat: true),
2165+
'data' => ['name' => 'parent', 'items' => [['x' => 1]]],
2166+
'expected' => '',
2167+
],
2168+
'compat: scoped path (./name) finds key when it exists in current context' => [
2169+
'template' => '{{#each items}}{{./name}}{{/each}}',
2170+
'options' => new Options(compat: true),
2171+
'data' => ['name' => 'parent', 'items' => [['name' => 'child']]],
2172+
'expected' => 'child',
2173+
],
21382174
];
21392175
}
21402176

0 commit comments

Comments
 (0)