Skip to content

perry-codegen: @perry_class_keys_<module>__<ClassName> symbol-name collisions when the same class name appears twice in one module #336

Description

@proggeramlug

What happened

When a single TypeScript module declares two (or more) classes that share a name in different scopes (top-level + inside a function, or inside two different functions), Perry's codegen emits the global @perry_class_keys_<module>__<ClassName> array of property keys once per class — but with the same symbol name. clang then rejects the IR with:

error: redefinition of global '@perry_class_keys_node_modules_effect_src_<module>__<ClassName>'

7 modules in Effect 3.21.2 trigger this:

Module Colliding class name
effect/src/Schema.ts Refinement
effect/src/JSONSchema.ts Refinement
effect/src/Pretty.ts Refinement
effect/src/internal/fiberRuntime.ts Class
effect/src/internal/mailbox.ts Class
effect/src/internal/core.ts SingleShotGen
effect/src/internal/stm/core.ts SingleShotGen

What you expected

Perry's class-keys symbol naming should disambiguate same-named classes within a module — e.g. by appending a counter / scope hash, or by including the lexical scope path in the mangled name (<module>__<ScopePath>__<ClassName>). Today it only uses <module>__<ClassName>.

Minimal reproduction

function makeOuter() {
  class Refinement { kind = "outer"; }
  return new Refinement();
}
function makeInner() {
  class Refinement { kind = "inner"; }
  return new Refinement();
}
console.log(makeOuter().kind, makeInner().kind);
perry compile repro.ts -o /tmp/out
# fails with: error: redefinition of global '@perry_class_keys_repro_ts__Refinement'

The same shape applies to two top-level classes that happen to share a name across if (...) { class C {} } branches, but the two-functions form is the cleanest repro.

Likely fix site

Search crates/perry-codegen/src/codegen.rs (or wherever class-keys globals are emitted) for @perry_class_keys_. The mangled name needs a scope discriminant. Same pattern would also affect @perry_class_methods_, @perry_class_field_init_, etc. — worth a sweep across all per-class globals to make sure they all carry the discriminant.

A minimal fix is a per-module BTreeMap<ClassName, usize> counter; second occurrence of Refinement in Schema.ts becomes @perry_class_keys_..._Refinement_2 etc. The discriminant must be deterministic (lexical-order scan), so successive compiles produce the same mangled name and downstream module-init / dispatch references resolve.

Environment

  • Perry version: 0.5.424
  • Host OS: macOS 26.4 (arm64)
  • Target: native
  • Effect version: 3.21.2 (where this surfaced)

Discovered as the largest residual cause of post-#314/#315/#316/#317/#318/#319/#320 failures. Tracked under #321.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions