Skip to content

feat: fold function_exists() polyfill guards, use imports in yield/clone/new $c, enum interface members - #1922

Open
JGamboa wants to merge 1 commit into
illegalstudio:mainfrom
JGamboa:fix/conditional-declarations-enum-members
Open

JGamboa wants to merge 1 commit into
illegalstudio:mainfrom
JGamboa:fix/conditional-declarations-enum-members

Conversation

@JGamboa

@JGamboa JGamboa commented Oct 19, 2026

Copy link
Copy Markdown

Summary

Three fixes for patterns that framework and vendor code uses everywhere.

  1. function_exists() polyfill folding (Register functions declared inside function_exists guards #842). if (!function_exists('f')) { function f() {} }
    left the declaration inside an if the checker does not register (only top-level
    declarations are), so every call reported Undefined function: f. In top-level code (in
    include order), a literal function_exists('f') for a user function is now decided at
    compile time when every nested declaration of f is a clean polyfill (directly in the
    then branch of a top-level if (!function_exists('f')) with no elseif/else). The
    true branch is flattened and f joins the inventory, so a later repetition of the same
    guard (Support vs Foundation helpers) folds to false and 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 extern
    keeps the runtime query.

    Limitation: any top-level eval() disables the folding. eval() can declare a
    function 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 local lowering error.) An eval() inside a function
    body does not count. Documented in docs/php/functions.md.

  2. use imports inside yield, yield from, clone and new $c(...). resolve_expr
    fell into its catch-all arm for these, so their operands were never resolved and an
    imported name stayed unqualified (Undefined class: P for yield new P in
    doctrine/inflector, clone new P, new $c(new P)). The Assignment arm of the original
    change is not included: fix(parser): align compound append diagnostics with PHP #1700 already added it.

  3. ->name / ->value through UnitEnum / BackedEnum (Accessing BackedEnum::$value after instanceof BackedEnum reports an undefined class #832). The interfaces were
    registered (instanceof, type hints), but reading $e->value or $e->name on a receiver
    typed as the interface, or narrowed to it by instanceof, reported
    Undefined class: BackedEnum. The interface has no slot layout: the checker types name
    as string and value as mixed; ir_lower boxes the Object(interface) receiver, and
    codegen sends unions (?BackedEnum, BackedEnum|string) to the runtime mixed property
    dispatch. Interfaces extending BackedEnum are 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

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:

  • Fail on main: test_function_exists_guard_declares_user_function,
    ..._name_normalization, ..._skips_existing_functions (main prints bool(true) for a
    probe 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 Accessing BackedEnum::$value after instanceof BackedEnum reports an undefined class #832 repro).
  • Controls, pass on main (marked in their doc comment where they are not obvious):
    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 an
    Undefined function error).

Local runs: cargo build; error_tests, parser_tests, lexer_tests,
function_exists_tests; codegen cond_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

…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.
@github-actions github-actions Bot added area:optimizer Touches AST or EIR optimization passes. area:resolver Touches include, namespace, name, or autoload resolution. area:types Touches type checking, inference, or compatibility. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:m Medium-sized pull request. type:feature Introduces new user-visible behavior or capabilities. labels Oct 19, 2026
@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 3/5

[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.

Fix All in Claude CodeFindings

  1. P1 Declared helper still appears missing ▶
  2. P1 Helper appears before it loads ▶
  3. P2 Helper explanation is missing ▶
  4. P2 Folding tests are misplaced ▶
Fix with agent prompt
### Issue 1
src/optimize/control/fold.rs:410-414
`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.

### Issue 2
src/optimize/control/fold.rs:412-415
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.

### Issue 3
src/optimize/user_guards.rs:212
`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>) {
```

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!

### Issue 4
tests/codegen/cond_decl.rs:13-15
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.

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!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds compile-time polyfill guards, resolves imports inside several composite expressions, and supports property reads through enum interfaces.

  • Top-level function guards now make helper functions available.
  • Imported class names now resolve inside generators and construction expressions.
  • Enum interface property reads now reach the actual enum value.

Acknowledged scope from JGamboa:

  • Top-level eval() intentionally disables missing-function folding.
  • Dynamic enum class-method calls and untyped match (true) narrowing are deferred to separate work.
  • The non-enum implements UnitEnum check is already supplied by main.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Read function declarations] --> B[Fold a polyfill guard]
  B --> C[Fold the entire guard body]
  C --> D[Flatten the retained body]
  D --> E[Record newly declared functions]
  E --> F[Fold later statements]
  C -. Probe after declaration still sees missing function .-> G[Wrong false result]
  D -. Ordinary declaration enters final function list .-> H[Earlier runtime probes see function too soon]
Loading

Reviews (1) · Last reviewed commit: "feat: fold function_exists() polyfill gu..." · Reviewed by Greptile

Comment on lines +410 to +414
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Fix in Claude Code Fix in Codex Fix in Cursor

Comment on lines +412 to +415
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Fix in Claude Code Fix in Codex Fix in Cursor

}
}

fn classify_stmt(stmt: &Stmt, top: bool, opaque: &mut HashSet<String>) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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!

Fix in Claude Code Fix in Codex Fix in Cursor

Comment on lines +13 to +15

/// Verifies a guarded user function is declared, and that `function_exists` follows program order.
#[test]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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)

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!

Fix in Claude Code Fix in Codex Fix in Cursor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:optimizer Touches AST or EIR optimization passes. area:resolver Touches include, namespace, name, or autoload resolution. area:types Touches type checking, inference, or compatibility. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:m Medium-sized pull request. type:feature Introduces new user-visible behavior or capabilities.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Register functions declared inside function_exists guards Accessing BackedEnum::$value after instanceof BackedEnum reports an undefined class

1 participant