feat(parser): support PHP's (object) cast - #988
Merged
Merged
Conversation
Closes #836. `(object)` was rejected during parsing, so `(object) ['name' => 'Laravel']` failed with `Expected ']'` before the cast was ever considered. The conversion is implemented as an elephc-PHP prelude rather than a runtime helper: PHP's rules are a key walk plus dynamic property writes, both of which already exist as ordinary elephc-PHP, so every supported target gets the cast with no per-target assembly — the same reason `var_export` is a prelude. PHP's semantics, reproduced exactly: - an array becomes a stdClass whose property names are the array's keys rendered as strings, so `(object) ['x', 'y']` has the properties `0` and `1`; - `null` becomes an empty stdClass; - every other non-object value becomes a stdClass carrying it under the literal `scalar` property; - an object is returned UNCHANGED — `(object) $o === $o` holds and no copy is made. Two helpers keep the cast's static type precise. `__elephc_cast_object` is declared `: stdClass` and is what `lower_cast` calls when the source cannot be an object, so `(object) ['a' => 1]` types as `stdClass` and property reads stay on the nominal path; only a `mixed`/union source needs the `: mixed` `__elephc_cast_object_dynamic`, whose identity arm can return any class. An object source is lowered to the value itself, with no call at all. The source is lowered ONCE into a synthetic local and the helper call then names that local — the rewrite `ref_place_args` already uses — so the ordinary user-call path owns argument lowering, the return type, and owned-temporary release, and a source with a side effect cannot run twice. The cast has no call syntax, so the reachability walk records both helpers when it sees an object cast; without that the prelude was pruned and the lowered call resolved to nothing. Detection of the cast itself rides on `opcache_prelude::detect`'s single exhaustive walk (`SymbolKind::ObjectCast`) so no second traversal can drift out of step with the AST, and the prelude stays pay-for-use. Claude-Session: https://claude.ai/code/session_01KSAAWPyNBq6dP2b5puN3wr
Greptile SummaryThis PR adds PHP-compatible
Confidence Score: 5/5The PR appears safe to merge; no outstanding correctness, security, ownership, or repository-rule issue remains. The post-review changes fully address the prior autoload-helper failure, and the previously requested lexer, parser, error, example, and ownership coverage is present. All previous threads are resolved, and no new changes exist beyond the reviewed head.
|
| Filename | Overview |
|---|---|
| src/parser/expr/calls.rs | Recognizes the case-insensitive object cast spelling through the existing cast parser. |
| src/object_cast_prelude.rs | Defines and conditionally injects the static and dynamic PHP helpers implementing object-cast semantics. |
| src/ir_lower/expr/ternary_cast.rs | Preserves object identity and lowers non-object sources once through the appropriate helper. |
| src/pipeline.rs | Injects object-cast helpers after autoload expansion so casts in loaded classes are covered. |
| src/optimize/reachability/usage/expressions.rs | Keeps the synthetically called cast helpers reachable during prelude pruning. |
| tests/codegen/casts_and_constants/object_cast.rs | Covers PHP conversion semantics, identity, evaluation count, helper reachability, and autoloaded casts. |
| tests/codegen/runtime_gc/object_cast.rs | Exercises ownership and cleanup for arrays, strings, mixed values, objects, loops, and dropped results. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[PHP object cast] --> B[Parser: CastType::Object]
B --> C[Type inference]
C --> D{Source statically object?}
D -- Yes --> E[Return source unchanged]
D -- No --> F{Mixed or union?}
F -- Yes --> G[Dynamic cast helper]
F -- No --> H[Static cast helper]
G --> I[Object identity or conversion]
H --> J[stdClass conversion]
I --> K[EIR and native codegen]
J --> K
Reviews (3): Last reviewed commit: "fix(object-cast): inject past autoload, ..." | Re-trigger Greptile
…er ownership Review follow-up on #988. Autoloaded casts missed the helpers. The prelude sat with every other one, before name resolution — but the cast is detected SYNTACTICALLY and an autoloaded class file is not part of the AST until `autoload::run`. A program whose only `(object)` cast lived in a PSR-4 class therefore got no injection, and codegen failed with `unsupported EIR backend feature: language construct __elephc_cast_object`. Injection moved past `autoload::run`, with the declarations name-resolved on the way in exactly as `autoload::run` resolves the files it splices. The three test harnesses that mirror the pipeline moved with it. The helper names are now reserved. `ir_lower` lowers every `(object)` cast to a call on them, so a user function of the same name would not merely shadow the prelude — it would BECOME the cast's semantics. A program that declares either one and also spells a cast is rejected by name; declaring one WITHOUT a cast is still fine, since the prelude is pay-for-use. `detect::first_declaration` is added alongside `program_declares` so the diagnostic can point at the offending declaration. The dynamic helper corrupted an object payload's refcount. Its `is_object()` arm returned the parameter early, and a `mixed` function with a CONDITIONAL `return $param;` alongside another return miscompiles today — heap debug reports `bad refcount`. The body now converts in place and returns `$value` once, the spelling that is clean. This is a PRE-EXISTING return-alias hole reachable from ordinary user PHP (`function f(mixed $v): mixed { if (is_object($v)) { return $v; } return $v; }` reproduces it with no cast involved); the helper is written to stay out of it rather than to work around it, and the hole is reported separately. Tests: the required lexer and error coverage the repository asks of a new language construct (the cast's token window and case handling; a missing operand, an unterminated window, and the reserved-name diagnostic), an `examples/object-cast/` program whose output was diffed against reference PHP, six ownership fixtures under `tests/codegen/runtime_gc/` asserting a clean heap for a temporary array, a repeated cast, an owned string, each arm of a Mixed source, an object source that must be neither copied nor released, and a result dropped at the end of a function, and a multi-file fixture pinning the autoload case. Claude-Session: https://claude.ai/code/session_01KSAAWPyNBq6dP2b5puN3wr
Guikingone
added a commit
that referenced
this pull request
Sep 13, 2026
…er ownership Review follow-up on #988. Autoloaded casts missed the helpers. The prelude sat with every other one, before name resolution — but the cast is detected SYNTACTICALLY and an autoloaded class file is not part of the AST until `autoload::run`. A program whose only `(object)` cast lived in a PSR-4 class therefore got no injection, and codegen failed with `unsupported EIR backend feature: language construct __elephc_cast_object`. Injection moved past `autoload::run`, with the declarations name-resolved on the way in exactly as `autoload::run` resolves the files it splices. The three test harnesses that mirror the pipeline moved with it. The helper names are now reserved. `ir_lower` lowers every `(object)` cast to a call on them, so a user function of the same name would not merely shadow the prelude — it would BECOME the cast's semantics. A program that declares either one and also spells a cast is rejected by name; declaring one WITHOUT a cast is still fine, since the prelude is pay-for-use. `detect::first_declaration` is added alongside `program_declares` so the diagnostic can point at the offending declaration. The dynamic helper corrupted an object payload's refcount. Its `is_object()` arm returned the parameter early, and a `mixed` function with a CONDITIONAL `return $param;` alongside another return miscompiles today — heap debug reports `bad refcount`. The body now converts in place and returns `$value` once, the spelling that is clean. This is a PRE-EXISTING return-alias hole reachable from ordinary user PHP (`function f(mixed $v): mixed { if (is_object($v)) { return $v; } return $v; }` reproduces it with no cast involved); the helper is written to stay out of it rather than to work around it, and the hole is reported separately. Tests: the required lexer and error coverage the repository asks of a new language construct (the cast's token window and case handling; a missing operand, an unterminated window, and the reserved-name diagnostic), an `examples/object-cast/` program whose output was diffed against reference PHP, six ownership fixtures under `tests/codegen/runtime_gc/` asserting a clean heap for a temporary array, a repeated cast, an owned string, each arm of a Mixed source, an object source that must be neither copied nor released, and a result dropped at the end of a function, and a multi-file fixture pinning the autoload case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KSAAWPyNBq6dP2b5puN3wr
Guikingone
force-pushed
the
fix/836-object-cast
branch
from
September 13, 2026 19:39
6871fa1 to
442588b
Compare
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 #836.
What was wrong
(object)was rejected during parsing, so the issue's reproduction failed before the cast was ever considered:Approach
The conversion is an elephc-PHP prelude, not a runtime helper. PHP's rules are a key walk plus dynamic property writes, both of which already exist as ordinary elephc-PHP, so every supported target gets the cast with no per-target assembly — the same reason
var_exportis a prelude.PHP's semantics, reproduced exactly:
stdClasswhose property names are the array's keys rendered as strings, so(object) ['x', 'y']has the properties0and1;nullbecomes an emptystdClass;stdClasscarrying it under the literalscalarproperty;(object) $o === $oholds and no copy is made.Two helpers keep the cast's static type precise.
__elephc_cast_objectis declared: stdClassand is whatlower_castcalls when the source cannot be an object, so(object) ['a' => 1]types asstdClassand property reads stay on the nominal path; only amixed/union source needs the: mixed__elephc_cast_object_dynamic, whose identity arm can return any class. An object source is lowered to the value itself, with no call at all.The source is lowered once into a synthetic local and the helper call then names that local — the rewrite
ref_place_argsalready uses — so the ordinary user-call path owns argument lowering, the return type, and owned-temporary release, and a source with a side effect cannot run twice.The cast has no call syntax, so the reachability walk records both helpers when it sees an object cast; without that the prelude was pruned and the lowered call resolved to nothing. Detection of the cast itself rides on
opcache_prelude::detect's single exhaustive walk (SymbolKind::ObjectCast) so no second traversal can drift out of step with the AST, and the prelude stays pay-for-use.Verification
Output was diffed against reference PHP 8.5 for a fixture covering every arm (array literal, array variable, integer keys, each scalar type,
null, object identity,instanceof/get_class, and the(array)round trip). It matches byte for byte, including object handle numbering.tests/codegen/casts_and_constants/object_cast.rsCast { target: Object }node: stdClasssignature)cargo test --lib(1679),--test parser_tests(397),--test error_tests(1518),--test codegen_tests casts_and_constants(117) anddead_strip(9) all pass;cargo buildis warning-freeOut of scope
Two pre-existing gaps were confirmed on
mainand are untouched by this change:var_dump()of astdClasscarrying dynamic properties prints(0) { }. This reproduces with a plainnew stdClass(); $o->x = 1;and no cast at all, so it is a separatevar_dumpbug rather than a cast one.eval()bridge parses only scalar casts —(array)already fails there withParse error: eval() fragment is invalid, and(object)does the same. Adding one without the other would be asymmetric; both belong in a single follow-up.