Repository navigation
feat: fold function_exists() polyfill guards, use imports in yield/clone/new $c, enum interface members - #1922
Conversation
…one/new $c, enum interface members
- In top-level code (in include order), a literal function_exists('f') for a
user function is decided at compile time when every nested declaration of
f is a clean polyfill (`if (!function_exists('f')) { function f() {} }`):
the true branch is flattened and f joins the inventory, so a repeated
guard folds to false. Function bodies are not folded; include-discovered
functions and externs stay runtime queries, and with a top-level eval()
only a function already known to exist folds.
- The name resolver resolves the operands of yield, yield from, clone,
new $c(...) and new-dynamic-object, so imported `use` names are qualified.
- ->name / ->value read through UnitEnum, BackedEnum and interfaces extending
them: the checker types name as string and value as mixed, ir_lower boxes
the interface receiver and codegen sends enum-interface unions to the
runtime mixed property dispatch.
[Medium impact] The PR changes how several PHP code patterns compile and run. Fix the two polyfill declaration-order bugs and satisfy the documentation and test-layout requirements before merging.
|
| let stmt = fold_stmt(stmt); | ||
| if active_fold_target().is_some() && target_dependent_if { | ||
| let kept = prune_target_boolean_if(stmt); | ||
| // A folded top-level polyfill guard flattens its declaration into this block. | ||
| crate::optimize::user_guards::record_materialized(&kept); |
There was a problem hiding this comment.
Declared helper still appears missing
record_materialized runs after the whole guard body has been folded. For if (!function_exists('f')) { function f() {} var_dump(function_exists('f')); }, the inner probe becomes false, although PHP returns true. Code inside the guard can therefore take the wrong branch after declaring its helper.
Update the known-function set as declarations are reached in the retained branch, before folding later statements.
Knowledge Base Used: IR lowering and optimization
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/optimize/control/fold.rs
Line: 410-414
Comment:
**Declared helper still appears missing**
`record_materialized` runs after the whole guard body has been folded. For `if (!function_exists('f')) { function f() {} var_dump(function_exists('f')); }`, the inner probe becomes `false`, although PHP returns `true`. Code inside the guard can therefore take the wrong branch after declaring its helper.
Update the known-function set as declarations are reached in the retained branch, before folding later statements.
**Knowledge Base Used:** [IR lowering and optimization](https://app.greptile.com/illegal-studio/-/custom-context/knowledge-base/illegalstudio/elephc/-/docs/ir-lowering-and-optimization.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| let kept = prune_target_boolean_if(stmt); | ||
| // A folded top-level polyfill guard flattens its declaration into this block. | ||
| crate::optimize::user_guards::record_materialized(&kept); | ||
| kept |
There was a problem hiding this comment.
Helper appears before it loads
Flattening a polyfill into an ordinary declaration makes runtime probes see it before its guard executes. For function probe() { return function_exists('f'); } var_dump(probe()); if (!function_exists('f')) { function f() {} } var_dump(probe());, both calls return true; PHP returns false then true.
Callable bodies keep their probes, but lower_function_exists answers from the final function list. Preserve a runtime declaration flag for these helpers and use it for probes that cannot be folded.
Knowledge Base Used: IR lowering and optimization
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/optimize/control/fold.rs
Line: 412-415
Comment:
**Helper appears before it loads**
Flattening a polyfill into an ordinary declaration makes runtime probes see it before its guard executes. For `function probe() { return function_exists('f'); } var_dump(probe()); if (!function_exists('f')) { function f() {} } var_dump(probe());`, both calls return `true`; PHP returns `false` then `true`.
Callable bodies keep their probes, but `lower_function_exists` answers from the final function list. Preserve a runtime declaration flag for these helpers and use it for probes that cannot be folded.
**Knowledge Base Used:** [IR lowering and optimization](https://app.greptile.com/illegal-studio/-/custom-context/knowledge-base/illegalstudio/elephc/-/docs/ir-lowering-and-optimization.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| } | ||
| } | ||
|
|
||
| fn classify_stmt(stmt: &Stmt, top: bool, opaque: &mut HashSet<String>) { |
There was a problem hiding this comment.
classify_stmt has no docblock. AGENTS.md requires a concise /// comment on every explicit Rust function, including private helpers. Before merging, describe how this helper marks declarations that must keep runtime probes.
| fn classify_stmt(stmt: &Stmt, top: bool, opaque: &mut HashSet<String>) { | |
| /// Marks declarations that require runtime probes and scans nested statement bodies. | |
| fn classify_stmt(stmt: &Stmt, top: bool, opaque: &mut HashSet<String>) { |
Context Used: AGENTS.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/optimize/user_guards.rs
Line: 212
Comment:
**Helper explanation is missing**
`classify_stmt` has no docblock. `AGENTS.md` requires a concise `///` comment on every explicit Rust function, including private helpers. Before merging, describe how this helper marks declarations that must keep runtime probes.
```suggestion
/// Marks declarations that require runtime probes and scans nested statement bodies.
fn classify_stmt(stmt: &Stmt, top: bool, opaque: &mut HashSet<String>) {
```
**Context Used:** AGENTS.md ([source](https://github.com/illegalstudio/elephc/blob/main/AGENTS.md))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
|
||
| /// Verifies a guarded user function is declared, and that `function_exists` follows program order. | ||
| #[test] |
There was a problem hiding this comment.
The folding regression tests live only in cond_decl.rs. AGENTS.md requires optimizer regression tests under tests/codegen/optimizer/ when folding or branch pruning can observe a change. Move or add the polyfill-folding cases there before merging to satisfy that requirement.
Context Used: AGENTS.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/codegen/cond_decl.rs
Line: 13-15
Comment:
**Folding tests are misplaced**
The folding regression tests live only in `cond_decl.rs`. `AGENTS.md` requires optimizer regression tests under `tests/codegen/optimizer/` when folding or branch pruning can observe a change. Move or add the polyfill-folding cases there before merging to satisfy that requirement.
**Context Used:** AGENTS.md ([source](https://github.com/illegalstudio/elephc/blob/main/AGENTS.md))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Summary
Three fixes for patterns that framework and vendor code uses everywhere.
function_exists()polyfill folding (Register functions declared inside function_exists guards #842).if (!function_exists('f')) { function f() {} }left the declaration inside an
ifthe checker does not register (only top-leveldeclarations are), so every call reported
Undefined function: f. In top-level code (ininclude order), a literal
function_exists('f')for a user function is now decided atcompile time when every nested declaration of
fis a clean polyfill (directly in thethenbranch of a top-levelif (!function_exists('f'))with noelseif/else). Thetrue branch is flattened and
fjoins the inventory, so a later repetition of the sameguard (Support vs Foundation helpers) folds to
falseand does not redeclare.Function-like bodies are never folded; any other conditional declaration of the name, an
include-discovered function (already tracked at run time in load order) or an
externkeeps the runtime query.
Limitation: any top-level
eval()disables the folding.eval()can declare afunction at run time, so when top-level code calls it a probe only folds when the
function is already known to exist, and a polyfill for a missing function is not
materialized. (Such a program currently fails on main too, with a separate
eval call missing eval context locallowering error.) Aneval()inside a functionbody does not count. Documented in
docs/php/functions.md.useimports insideyield,yield from,cloneandnew $c(...).resolve_exprfell into its catch-all arm for these, so their operands were never resolved and an
imported name stayed unqualified (
Undefined class: Pforyield new Pindoctrine/inflector,
clone new P,new $c(new P)). TheAssignmentarm of the originalchange is not included: fix(parser): align compound append diagnostics with PHP #1700 already added it.
->name/->valuethroughUnitEnum/BackedEnum(AccessingBackedEnum::$valueafterinstanceof BackedEnumreports an undefined class #832). The interfaces wereregistered (instanceof, type hints), but reading
$e->valueor$e->nameon a receivertyped as the interface, or narrowed to it by
instanceof, reportedUndefined class: BackedEnum. The interface has no slot layout: the checker typesnameas
stringandvalueasmixed;ir_lowerboxes theObject(interface)receiver, andcodegen sends unions (
?BackedEnum,BackedEnum|string) to the runtime mixed propertydispatch. Interfaces extending
BackedEnumare covered too.Closes #842
Closes #832
The three fixes land as one commit; happy to split it into one PR per fix if you prefer.
Left out
implements UnitEnumcheck of the original branch: main has its own sincefeat(core): complete PHP 8.5 classes and constants #1870 (
class C implements UnitEnumreports "Non-enum class C cannot implement interfaceUnitEnum" once on this branch).
$e::cases()/$e::from()on an interface-typed receiver needs the dynamic class accessPR (
$obj::m()); that part of the original test was removed.match (true) { $v instanceof BackedEnum => $v->value, ... }on an untyped value also needsthe
match (true)narrowing PR (match (true)arms withinstanceofconditions are not narrowed and silently return wrong values #1898).Tests
Based on main df57a98. The tests below were re-run against that SHA with this branch's test files on top of main's sources.
tests/codegen/cond_decl.rs(new), expected output from php 8.5:test_function_exists_guard_declares_user_function,..._name_normalization,..._skips_existing_functions(main printsbool(true)for aprobe before the guarded declaration),
..._across_included_helpers(main:Undefined function: tapx),..._leaves_include_variants_to_runtime(main:Undefined function: poly_cd),test_use_imports_resolve_inside_yield_and_composite_expressions(main:Undefined class: P),test_unit_and_backed_enum_interfaces_as_types(main:Undefined class: BackedEnum),test_backed_enum_value_after_instanceof_narrowing(the AccessingBackedEnum::$valueafterinstanceof BackedEnumreports an undefined class #832 repro).test_function_exists_probe_after_top_level_eval_stays_runtime(pins the eval rule above)and
test_function_exists_guard_inside_loop_is_not_hoisted(a non-clean guard stays anUndefined functionerror).Local runs:
cargo build;error_tests,parser_tests,lexer_tests,function_exists_tests; codegencond_decl:: enum function_exists namespaces generators:: optimizer:: eval_undefined_functions.Refs #1886
AI assistance
This change was written with substantial help from Claude Code (Anthropic). I reviewed it,
checked the expected outputs against php 8.5, and remain responsible for it.
🤖 Generated with Claude Code