Skip to content

Commit be05086

Browse files
committed
Optimize length lookup
1 parent 68cd5a3 commit be05086

2 files changed

Lines changed: 27 additions & 31 deletions

File tree

src/Compiler.php

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ private function MustacheStatement(MustacheStatement $mustache): string
489489
$cvArgs = '$in, ' . self::quote($helperName) . ($this->options->strict ? ', true' : '');
490490
return self::getRuntimeFunc($fn, self::getRuntimeFunc('lookupValue', $cvArgs));
491491
}
492-
$expression = $this->PathExpression($path);
492+
return self::getRuntimeFunc($fn, $this->PathExpression($path, true));
493493
} else {
494494
// Literal in simple position: same lambda resolution as PathExpression above.
495495
$literalKey = $this->getLiteralKeyName($path);
@@ -511,7 +511,7 @@ private function SubExpression(SubExpression $expression): string
511511
return $this->compileHelperCall($helperName, $path, $expression->params, $expression->hash);
512512
}
513513

514-
private function PathExpression(PathExpression $path): string
514+
private function PathExpression(PathExpression $path, bool $wrapLambda = false): string
515515
{
516516
$data = $path->data;
517517
$depth = $path->depth;
@@ -528,10 +528,6 @@ private function PathExpression(PathExpression $path): string
528528
$stringParts = $path->parts;
529529
}
530530

531-
if (!$stringParts) {
532-
return $base;
533-
}
534-
535531
$isLength = end($stringParts) === 'length';
536532
$isCurrentContextPath = !$hasSubExprHead && !$data && $depth === 0;
537533
$scoped = $isCurrentContextPath && self::scopedId($path);
@@ -541,25 +537,26 @@ private function PathExpression(PathExpression $path): string
541537
$bp = $this->lookupBlockParam($path->head);
542538
if ($bp !== null) {
543539
[$bpDepth, $bpIndex] = $bp;
544-
$bpBase = "\$blockParams[$bpDepth][$bpIndex]";
540+
$base = "\$blockParams[$bpDepth][$bpIndex]";
545541
// Skip the block param name since it has been resolved to a $blockParams index.
546-
$keys = $isLength ? array_slice($path->tail, 0, -1) : $path->tail;
547-
$lookup = $this->compileModeAwareLookup($bpBase, $keys);
548-
return $isLength ? $this->buildLookupLength($lookup) : $lookup;
542+
$stringParts = $path->tail;
549543
}
550544
}
551545

552-
// Handle .length: compile parent path through the normal mode-aware logic, then wrap in
553-
// lookupLength() at runtime. This mirrors HBS.js, where .length is a normal property
554-
// access with no compile-time special casing.
555546
if ($isLength) {
556-
$partsExceptLength = array_slice($stringParts, 0, -1);
557-
return $this->buildLookupLength(
558-
$this->compileModeAwareLookup($base, $partsExceptLength, $scoped),
559-
);
547+
array_pop($stringParts);
560548
}
549+
$result = $this->compileModeAwareLookup($base, $stringParts, $scoped);
561550

562-
return $this->compileModeAwareLookup($base, $stringParts, $scoped);
551+
if ($isLength) {
552+
// Handle .length: compile parent path through the normal mode-aware logic, then wrap in
553+
// lookupLength() at runtime. This mirrors HBS.js, where .length is a normal property
554+
// access with no compile-time special casing.
555+
$strict = $this->options->strict || $this->options->assumeObjects;
556+
$result = self::getRuntimeFunc('lookupLength', $strict ? "$result, true" : $result);
557+
}
558+
559+
return ($wrapLambda && !$isLength) ? self::getRuntimeFunc('lambda', $result) : $result;
563560
}
564561

565562
/**
@@ -805,12 +802,6 @@ private function compileProgramOrEmpty(?Program $program): string
805802
return $this->compileProgram($program);
806803
}
807804

808-
private function buildLookupLength(string $parent): string
809-
{
810-
$strict = $this->options->strict || $this->options->assumeObjects;
811-
return self::getRuntimeFunc('lookupLength', $strict ? "$parent, true" : $parent);
812-
}
813-
814805
/**
815806
* Compile a mode-aware path access expression for the given base and parts.
816807
* @param string[] $parts

src/Runtime.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -143,18 +143,23 @@ public static function nullCheck(mixed $base, string $key): mixed
143143
}
144144

145145
/**
146-
* Terminal .length lookup: returns an explicit 'length' key if present, and otherwise
147-
* count() for arrays and strlen() for strings, since PHP doesn't have native .length properties.
146+
* Terminal .length lookup: returns an explicit 'length' key if present
147+
* (invoking with no args if it's a Closure), and otherwise count() for arrays
148+
* and strlen() for strings, since these don't have native .length properties in PHP.
148149
*/
149150
public static function lookupLength(mixed $base, bool $strict = false): mixed
150151
{
151152
if (is_array($base)) {
152-
return array_key_exists('length', $base) ? $base['length'] : count($base);
153-
}
154-
if (is_string($base)) {
153+
if (!array_key_exists('length', $base)) {
154+
return count($base);
155+
}
156+
$v = $base['length'];
157+
} elseif (is_string($base)) {
155158
return strlen($base);
159+
} else {
160+
$v = $strict ? self::strictLookup($base, 'length') : null;
156161
}
157-
return $strict ? self::strictLookup($base, 'length') : null;
162+
return $v instanceof Closure ? $v() : $v;
158163
}
159164

160165
/**
@@ -209,7 +214,7 @@ public static function lambda(mixed $v): mixed
209214
* (PHP equivalent of JS fn.call(context), where context binds as `this` with no positional args).
210215
* When $strict is true, throws for missing keys.
211216
*/
212-
public static function lookupValue(mixed &$_this, string $name, bool $strict = false): mixed
217+
public static function lookupValue(mixed $_this, string $name, bool $strict = false): mixed
213218
{
214219
$v = $strict ? self::strictLookup($_this, $name) : ($_this[$name] ?? null);
215220
return $v instanceof Closure ? $v($_this) : $v;

0 commit comments

Comments
 (0)