Repository navigation
refactor(type-engine): preserve scalar literal types in expression resolution - #304
Merged
AJenbo merged 2 commits intoAug 1, 2026
Merged
Conversation
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…solution The expression resolver widened every scalar literal to its base type at the point of resolution, so compound expressions such as ternaries, match arms, and null-coalescing lost literal precision before any consumer saw them. The diagnostic path compensated with `narrow_literal_type()`, a second walk that re-derived literals for argument checking by enumerating every compound expression form it knew about. Precision was therefore available only where somebody had remembered to reconstruct it, and only for the expression shapes that function already listed. Resolution now returns literal string, int, and float types directly and keeps them through parentheses, unary signs, ternaries, match arms, coalescing, branch merges, guards, by-reference updates, and return inference. Widening happens where PHP itself stops distinguishing the value: entries stored into mutable arrays, lists, and shapes, values crossing a native type-hint boundary, and operations that change the value rather than observe it. Array keys follow PHP's own coercion rules across literal and broad scalar domains. Because the resolver is now precise on its own, `narrow_literal_type()` and the closure, assignment, and by-reference workarounds that existed to recover the same information are removed rather than reimplemented. Closes #293
AJenbo
force-pushed
the
refactor/preserve-literal-types
branch
from
August 1, 2026 08:38
46f11e3 to
b8680a1
Compare
Contributor
|
Thanks, this starts down a wobbly road of getting the type engine to be even more strict, but now is the right time and this was a good first step. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #293.
What this changes
Scalar literals now survive ordinary expression flow instead of being widened to
string/int/floatat resolution time.Given:
hovering
$scalarused to show$scalar = string. It now shows$scalar = 'draft', and$choiceshows'asc'|'desc'.$collectionstill showslist<string>, because storing into a mutable collection is where the distinction stops being meaningful.Why
resolve_rhs_expressionwidened literals immediately, so every consumer that wanted precision had to rebuild it. The diagnostic path did that withnarrow_literal_type(), which walked the expression tree a second time and handled a fixed list of compound forms — ternary, null-coalesce, match, and parenthesised wrappers. Anything outside that list, and anything reached by hover, completion, or inferred PHPDoc return types, got no literal precision at all.As #293 puts it, the narrowing pass is architecturally fragile because it enumerates expression forms rather than getting precision for free from the resolver.
How
literal_string_raw/literal_int/literal_floatfor scalar literals and preserves them through parenthesised and signed expressions, ternaries, match arms, coalescing, branch merges, guards, increments and decrements, by-reference updates, and return inference.int|floata real runtime union while letting broad scalar domains absorb literal subsets.Numberclass.numeric, the lowercase pseudo-typenumber, the classNumber, andrealare corrected.narrow_literal_type()and the closure, assignment, and by-reference collection workarounds are removed, since the type engine now carries the information they existed to recover.Tests
New coverage spans literals, structured unions, malformed and overflow numerics, PHP numeric strings, class metadata, branch joins, nullable and coalesce flow, keyed and appended collections, array-key coercion, iterator conversion, typed and variadic by-reference parameters, and large literal return unions.
hover_preserves_scalar_literals_but_widens_collectionsfails on an unmodified checkout of the base commit with$scalar = string, and passes with this change.Validation
Run on Windows against base
88ba719:cargo fmt --all -- --checkcargo clippy --all-targets --all-features -- -D warningscargo nextest run --no-fail-fastgit diff --checkcargo bench --bench completionThe same suite on the unmodified base commit, measured in the same session with no local changes at all, gives 11,463 run, 11,426 passed, 37 failed, 2 skipped. The two failure sets are identical by name — nothing introduced, nothing fixed — and are 9 library, 27 integration, and 1 unit case, all Windows path, symlink, and URI-escaping behaviour unrelated to this change.
Performance
Because performance matters here, base and candidate were compared by building both benchmark harnesses first and then running them alternately for three rounds each, so background load affects both sides equally. Medians:
completion_with_narrowinghover_method_callupdate_ast_parse_time/100_linesRepeating an identical build varied by ±4% to ±12% between rounds, so none of these is resolvable as a real difference, including on
update_ast_parse_time, which this change cannot affect and which is included as a control. No regression is detectable at this machine's resolution, and no improvement is claimed.Not verified locally
php -l examples/demo.php,php -d zend.assertions=1 examples/demo.php, andphp -l examples/laravel/app/Demo.phpneed a PHP CLI that was not available on the machine used, and coverage viacargo llvm-covand the Linux-musl target were likewise unavailable. Those are left to CI.Checklist
If applicable:
CHANGELOG.mdREADME.md,docs/,examples/)config-schema.json)how it works, how it's organized), including any code drafted by an LLM.
an eye towards deleting anything that is irrelevant, clarifying anything
that is confusing, and adding details that are relevant. This includes,
for example, commit descriptions, PR descriptions, and code comments.