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.
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:7 modules in Effect 3.21.2 trigger this:
effect/src/Schema.tsRefinementeffect/src/JSONSchema.tsRefinementeffect/src/Pretty.tsRefinementeffect/src/internal/fiberRuntime.tsClasseffect/src/internal/mailbox.tsClasseffect/src/internal/core.tsSingleShotGeneffect/src/internal/stm/core.tsSingleShotGenWhat 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
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 ofRefinementinSchema.tsbecomes@perry_class_keys_..._Refinement_2etc. The discriminant must be deterministic (lexical-order scan), so successive compiles produce the same mangled name and downstream module-init / dispatch references resolve.Environment
Discovered as the largest residual cause of post-#314/#315/#316/#317/#318/#319/#320 failures. Tracked under #321.