Repository navigation
fix(enums): validate trait constant reads and case collisions - #1827
Guikingone wants to merge 7 commits into
Conversation
Confidence Score: 3/5[Medium risk] An enum can read constants imported from a used trait, and reflection lists them after the enum's own cases and constants. Trait-constant expressions that use Summary
Diagramflowchart LR
traitConst[Trait constants] --> flatten[flatten_classes merge]
enumOwn[Enum cases and constants] --> flatten
flatten --> merged[merged_constants]
enumOwn --> order[Span-sorted own order]
merged --> values[ClassInfo constants]
order --> constOrder[constant_order]
merged --> constOrder
values --> read["E::CONST"]
constOrder --> refl[ReflectionClass getConstants]
Findings
Last reviewed commit: 92c86b3 |
|
Addressed both review threads in
Validation: warning-free build; 14 trait and 55 enum codegen tests; 71 focused diagnostic tests. Four PHP 8.5.10 oracle fixtures with EIR on/off ran on macOS ARM64 and Linux ARM64/x86_64: 24 matching outputs, all heap-clean. Both iOS device/Simulator targets produced 24 assembled program/runtime objects with runtime-symbol closure; no iOS execution or full-local-suite claim. The direct/Reflection comparison changes from
|
[Medium impact] The PR changes constant behavior across enum reads and shared declaration checks. The PR still needs the referenced-constant type check fixed before merging.
|
489d45f to
8bd617c
Compare
|
@nahime0 Ready to review when you have time 🚀 |
nahime0
left a comment
There was a problem hiding this comment.
Changes requested on head 8bd617c1. Three problems, each checked against PHP 8.5.9.
1. Imported __CLASS__ / __TRAIT__ / __NAMESPACE__ constants crash the compiler
insert_enum_metadata stores every merged trait constant after resolve_lexical_enum_constant_value. That rewriter handles self:: and parent:: only, and ExprKind::MagicConstant is cloned unchanged (src/types/checker/schema/classes/constants.rs:364). Constant initializers are skipped by magic-constant lowering, and enum flattening rebinds __CLASS__ only on methods (src/types/traits.rs:308).
trait T { const N = __CLASS__; const TR = __TRAIT__; }
enum E { use T; case One; }
class C { static function read() { echo E::N, "|", E::TR, "\n"; } }
C::read();PHP prints E|T. elephc panics before codegen with entered unreachable code: MagicConstant must be lowered before type inference (src/types/checker/inference/expr/calls_objects.rs:422). A namespaced trait panics the same way (PHP prints A|B\E|A\T).
Expected: lower magic constants while the declaring trait is still known. Keep __TRAIT__, __NAMESPACE__, __FUNCTION__ and __LINE__ on the trait, and rebind __CLASS__ to each consuming enum. Please add a regression that reads them from another class.
2. parent:: in an enum constant is a compile error even when the constant is never read
resolve_lexical_enum_constant_value always calls rewrite_expr with no parent (constants.rs:40, constants.rs:447), so any parent:: receiver fails with Class 'E' has no parent class while the enum metadata is built.
PHP compiles and runs enum E { case One; const A = parent::X; } and const A = [parent::X]. It throws a catchable Error (Cannot access "parent" when current class scope has no parent) only when the constant is read.
Expected: keep parent:: in the stored expression and raise the catchable Error on evaluation, without failing the enum declaration.
3. Private and protected trait constants imported by an enum are readable from global scope
Visibility is copied onto constant_visibilities, but infer_scoped_constant_access returns info.constants without consulting it (src/types/checker/schema/enums.rs:501, src/types/checker/inference/expr/class_refs.rs:125).
trait T { private const A = 1; public function read(): int { return self::A; } }
enum E { use T; case One; }
echo E::One->read(), "\n";
try { echo E::A, "\n"; } catch (Error $e) { echo "caught:", $e->getMessage(), "\n"; }PHP prints 1, then caught:Cannot access private constant E::A. elephc prints 1 twice. The same lookup ignores protected constants.
Expected: outside the declaring enum, raise Cannot access private constant E::A / Cannot access protected constant E::A; reads inside the enum keep working.
8bd617c to
d21c825
Compare
d21c825 to
ccb9f79
Compare
|
Thanks for the detailed pass. I re-checked all three scenarios against the current head: the intermediate commit on this branch (
I added your exact shapes as AOT regressions in Could you re-review at the current head? The PR head moved twice since your review (a rebase plus the collision-message test fix), and I'd like the |
|
Follow-up on the inline findings, all re-checked on the current head ( |
Reject own/direct/nested trait constants that collide with enum case names. Keep missing parent constant accesses lazy and catchable, enforce private and protected reads before evaluating inaccessible initializers, and preserve declaration scope for public initializers using private values. Model scoped reads as MAY_THROW so unused reads retain their exceptions. Use the merged enum declaration APIs from main a8f730c without changing its magic walkers, driver/schema argument order or reference-return paths. Refs illegalstudio#1317; addresses the outstanding PR1827 review findings on its existing branch, with regressions also covering lexical and magic scope. Validation on the integrated compiler:23 focused tests per IR mode, 32 strict fresh CLI cases,50 target program/runtime assembly pairs with 102 clean native macOS/Linux executions,20 target diagnostics and the example in both IR modes. iOS evidence is assembly only. Functional receiver/Reflection controls are separate from strict heap assertions. Trail reports20 lexical matches with old failure descriptions. The old walker opt-in/signature extension is superseded by main's dedicated APIs. Current source/artifact digests and fresh target executions validate the rebase; iOS uses real function bodies. Parent errors remain lazy, visibility precedes inference, and scoped reads carry throwing effects. The separate TLS dependency removes the old roots wrapper. New immutable integration learning records these decisions without editing earlier checkpoints.
The collision reached through a trait reports PHP's own wording — 'Cannot use trait T, because T::A conflicts with enum case E::A' — while a direct enum constant reports the 'Eenum constant E::A conflicts with enum case' form. The test's fixtures are all trait-imported, so it must assert the substring both share.
… trait constants The current head already handles the three shapes from the review: - trait magic constants lower against the declaring trait and __CLASS__ rebinds to the consuming enum, readable from another class across namespaces (A\E|A\T|A); - parent:: in an enum constant keeps the declaration compiling and raises PHP's catchable Error on read, for the scalar and array forms; - private/protected imported trait constants stay readable inside the enum and raise 'Cannot access private|protected constant E::A' outside it. These regressions pin the exact shapes against the reviewed head.
A parent:: initializer is stored as a deferred Throw that raises PHP's catchable Error only
when the constant is read, but validate_constant_declaration inferred it as void and rejected
the declaration against the declared type, so 'enum E { const int P = parent::X; }' failed to
compile even when the constant was never read (PHP compiles it and only reading fails).
Skip the declared-type check for the deferred shape; a non-deferred mismatch still errors.
PHP forbids `throw` in a constant expression, but the review bypass that defers a missing `parent::` receiver skipped every `Throw`, so `const int X = throw new Exception();` was accepted (typed and untyped, in classes, interfaces and traits). The deferred lexical Error is inserted by the checker into the normalized value only, while `ClassConst::value` keeps the source expression, so the check now scans the source and rejects it before the type check.
b1de8d9 to
01bd5b6
Compare
…lizers A class-constant initializer is a constant expression PHP evaluates lazily, on first read, so `const int Z = Y;` is legal when `Y` is declared later in the file or imported with `use const`. The declared-type check ran against `checker.constants`, which the statement pass only fills when it reaches each declaration, so those initializers failed with "Undefined constant" even though PHP accepts them (and even for a fully-qualified `\A\XV` reference). Seed the program's own top-level constants for the declared-type check, from the same `collect_constants` scan codegen uses, and restore the map afterwards. A name declared nowhere still fails closed, and a declared constant holding an incompatible value keeps PHP's declared-type rejection (`Cannot use string as value for class constant ...`).
|
Both findings re-checked on the current head Finding 1 — invalid constants pass checks: fixed. Finding 2 — unused typed constants fail: fixed, and the root cause was broader. While verifying the "imported typed constants" half I found the real defect: any typed constant naming a top-level constant failed with const Y = 5; class C { const int Z = Y; } // error: Undefined constant: Y
namespace A { const XV = 5; }
namespace B { use const A\XV; class C { const int Y = XV; } } // sameThe declared-type check runs before the statement pass records top-level constants. It now seeds them from the same Verification (PHP 8.5.10, both streams): enum One remaining, separate gap (pre-existing, not introduced here): a typed constant naming a missing member ( @nahime0 could you re-review when you have a moment? |
| for (name, (_, ty)) in declared_constants { | ||
| if !checker.constants.contains_key(name) { | ||
| checker.constants.insert(name.clone(), ty.clone()); |
There was a problem hiding this comment.
The new loop trusts types from collect_constants, but that collector labels arrays and unresolved constant references as Int. For example, const DATA = ['x']; class C { const int X = DATA; } now passes the declared-type check even though DATA is an array. Before this change, the missing lookup stopped compilation.
Infer the referenced declaration's real type instead of using the collector's fallback.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/types/checker/schema/class_constants.rs
Line: 119-121
Comment:
**Wrong constant types pass**
The new loop trusts types from `collect_constants`, but that collector labels arrays and unresolved constant references as `Int`. For example, `const DATA = ['x']; class C { const int X = DATA; }` now passes the declared-type check even though `DATA` is an array. Before this change, the missing lookup stopped compilation.
Infer the referenced declaration's real type instead of using the collector's fallback.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.…t types Review of the typed-constant work found three gaps: - contains_throw only knew the shapes contains_deferred_error walked, so a throw nested in @(...), an array element, a ternary, a call argument or a match arm was accepted. It now visits every ExprKind that can carry a sub-expression; closure bodies stay exempt because their throw runs when the closure is called. - Top-level const declarations had no throw check at all. They now reject it at the same point as class constants, with PHP's 'Constant expression contains invalid operations'. - Seeding a referenced constant's type trusted the prescan's Int fallback for an initializer it cannot fold, which rejected valid array constants and accepted an array as int. Only literal and array-literal types are seeded now; anything else seeds Mixed, which the declared-type check accepts. PHP 8.5.10 differentials cover each shape; tests pin the nested-throw rejection, the array-backed typed constant and the array-into-scalar rejection.
|
Second review round addressed on 1. Seeded types no longer trust the prescan's
2. const X = @(throw new Exception());
const X = [throw new Exception()];
const X = true ? throw new Exception() : 1;
const X = 1 + (throw new Exception());
const X = boom(throw new Exception());fail with PHP's The remaining shapes that PHP rejects as non-constant expressions without a throw ( Verification (PHP 8.5.10, both streams): each shape above, plus the array-backed constant, the array-into-scalar rejection, the enum Review: glm-5.3-flash and muse-spark-open both APPROVE |
|
Checked the finding against <?php
const DATA = ['x'];
class C { const int X = DATA; }
echo "unused\n";elephc ( PHP 8.5.10 accepts the declaration ( The seeding does not trust the collector's fallback:
|
Enum constants now enforce private/protected visibility at the read, before evaluating an inaccessible initializer. An unused
parent::initializer no longer rejects the declaration: its catchableErroris raised only when evaluation reaches that access. Public initializers retain the declaring enum scope when reading private constants, and unused constant reads keep their exceptions during optimization.Direct and nested trait constants whose names exactly match a case are rejected. Member names remain case-sensitive. Regressions also protect lexical
self::, magic-constant provenance and Reflection order provided by the enum metadata now onmain.Refs #1317. Rebased on
a 8f730c5after #1674 merged; its declaration walkers, schema/driver signatures and interface/reference-return behavior are preserved. The enum feature remains one commit above the independent TLS advisory fix #1954.Validation on the final integrated compiler:
The 20 iOS library witnesses contain real probe functions. Their evidence is assembly only; no iOS execution or full local suite is claimed. Instance/closure and Reflection controls compare output separately: a 40-byte enum-case receiver retention remains identical on the reviewed baseline and final compiler, so those controls make no heap-clean claim.
Full CI must validate the new exact head. Trail's lexical matches concern recorded old failures: the final code retains main's dedicated APIs, preserves lazy errors and throwing effects, and checks visibility before initializer inference. The accompanying integration learning supersedes the temporary old-base walker design.