Skip to content

fix(codegen): guard cyclic inheritance walks against non-termination - #4845

Merged
proggeramlug merged 1 commit into
mainfrom
guard-cyclic-inheritance-walks
Jun 9, 2026
Merged

fix(codegen): guard cyclic inheritance walks against non-termination#4845
proggeramlug merged 1 commit into
mainfrom
guard-cyclic-inheritance-walks

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

What changed

Add a visited-set + depth cap (64) to the codegen helper walks over the class extends_name inheritance chain, so they terminate on cyclic parent links instead of assuming the graph is a tree:

  • is_class_getter / is_class_setter (escape-analysis accessor dispatch)
  • class_uses_this_as_value + its parent-field-initializer walk
  • class_chain_extends_builtin_error
  • declared_field_type
  • class_field_global_index (the recursive walk and its inner parent-count loop)
  • class_field_declared_type

Plus focused regression tests for the accessor, this-as-value, and builtin-error walks under a 2-class cycle.

The three class-field helpers (declared_field_type, class_field_global_index, class_field_declared_type) move into a new sibling module type_analysis_class_fields.rs so type_analysis.rs stays under the 2000-line file-size gate (it was already at 1997). They're re-exported from type_analysis, so call sites are unchanged.

Why

Heavily-modular TypeScript packages (Effect, OpenCode) declare same-named classes across modules. When those are pulled into a single importing module's class table by name, the parent chains can form a cycle — e.g. local Base → extends "Class" and imported Class → parent "Base" resolving back to the local Base. Every chain-walking site here used an unbounded while let Some(name) = cur loop, so a single cycle would CPU-hang the counter walks or OOM the accumulating ones. This is what stalled the OpenCode native-graph codegen right after it reached visited=N/N.

The walks now bail on a repeated class name. The deeper name-collision problem (two distinct classes sharing a name across modules) is orthogonal and out of scope — this is purely the termination guard.

Provenance

Salvaged from the still-relevant core of #4261 (h/t @andrewtdiz). The rest of that branch is a stale OpenCode-migration grab-bag now 420+ commits behind main and largely superseded, so it's been closed in favor of this focused fix. The guard logic was re-applied on top of current main (the type_analysis.rs portion in particular, since #4261's copy of that file predated a lot of unrelated work). class_field_declared_type had an unguarded walk that #4261 didn't touch — guarded here too.

Validation

  • cargo test -p perry-codegen cyclic — 3 tests pass.
  • cargo test -p perry-codegen --lib — 92 pass, 0 fail.
  • cargo fmt --all -- --check clean.
  • ./scripts/check_file_size.sh — passes (type_analysis.rs back under 2000 lines).

Several codegen helper walks over the class `extends_name` chain assumed
the inheritance graph is a tree. Heavily-modular TypeScript packages
(Effect, OpenCode) declare same-named classes across modules; when those
are pulled into one importing module's class table by name, the parent
chains can form a cycle (local `Base` → extends `Class` → resolves back
to local `Base`). The unbounded `while let Some(name) = cur` walks then
either CPU-hang (counter walks) or OOM (accumulating walks), which is
what stalled the OpenCode native-graph codegen after `visited=N/N`.

Add a visited-set + depth cap (64) to the inheritance-chain walks in:
  - is_class_getter / is_class_setter (escape-analysis accessor dispatch)
  - class_uses_this_as_value + parent-field-init walk
  - class_chain_extends_builtin_error
  - declared_field_type
  - class_field_global_index (recursive walk and its inner parent-count loop)
  - class_field_declared_type

Each walk now terminates on a repeated class name instead of treating the
parent chain as a guaranteed tree. Focused regression tests cover the
accessor, this-as-value, and builtin-error walks under a 2-cycle graph.

The three class-field helpers (declared_field_type, class_field_global_index,
class_field_declared_type) move to a new sibling module
`type_analysis_class_fields.rs` so type_analysis.rs stays under the 2000-line
file-size gate; they're re-exported from `type_analysis` so call sites are
unchanged.

Salvaged from the still-relevant core of #4261 (h/t @andrewtdiz); the rest
of that branch is a stale OpenCode-migration grab-bag, superseded on main.
@proggeramlug
proggeramlug force-pushed the guard-cyclic-inheritance-walks branch from e922a9c to 2c8f6c7 Compare June 9, 2026 15:20
@proggeramlug
proggeramlug merged commit 0fa6d6d into main Jun 9, 2026
13 checks passed
@proggeramlug
proggeramlug deleted the guard-cyclic-inheritance-walks branch June 9, 2026 15:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant