Enum Name Collision Across Linked Contracts
Design Log #122 — Enum Name Collision Across Linked Contracts
Written for AI agents. See Log Methodology Note below for details.
Background
In jay-html, headless contracts define enums via dataType: enum(A | B). The compiler generates a TypeScript enum name using pascalCase(tag) — e.g., tag optionRenderType → OptionRenderType. When a page transitively imports multiple contracts that define enums with the same tag name but different values, the generated imports collide.
Related Design Logs
- #93 — Client hydration
- #118 — Hydrate compilation target extraction
Problem
Reproduction
Page products/kitan/[[category]]/page.jay-html imports product-search contract (key: search). The linking chain:
product-search
├── filters.optionFilters.optionRenderType: enum(TEXT_CHOICES | SWATCH_CHOICES)
└── searchResults (link: ./product-card)
└── quickOption, secondQuickOption (link: ./product-options)
└── optionRenderType: enum(TEXT_CHOICES | COLOR_SWATCH_CHOICES)
Both enums produce OptionRenderType via pascalCase('optionRenderType') in contract-parser.ts:78.
Generated output (broken)
// page.jay-html.d.ts
import { ..., OptionRenderType, ... } from ".../product-search.jay-contract";
import { OptionRenderType, ChoiceType, OptionRenderType, ChoiceType } from ".../product-options.jay-contract";
Three bugs visible:
- Shadowing: two
import { OptionRenderType }from different modules; second shadows first at runtime - Duplicate within import:
product-cardlinksproduct-optionstwice (forquickOptionandsecondQuickOption), soOptionRenderType, ChoiceTypeappear twice in the same import - Validation false-negative: compiler validates
SWATCH_CHOICESagainst the correct enum (product-search's, via the type tree), raises no error, but at runtimeOptionRenderTypeis the product-options version which doesn't haveSWATCH_CHOICES
Root cause trace
contract-parser.ts:78—new JayEnumType(pascalCase(tag), parseEnumValues(dataType))derives name solely from tag name, no scoping by contract or pathcontract-to-view-state-and-refs.ts:92,103— enums from linked sub-contracts bubble up viaenumsToImportwithout deduplicationjay-html-parser.ts:657-701, 951-992— enum import links are built without collision detection; same enum can appear multiple times from multiply-linked contractsjay-html-compile-imports.ts:22-25—renderImportsgeneratesimport {symbols}usingsymbol.name, producing duplicate bindingsexpression-parser.pegjs:570—enumConditionuseshead.resolvedType.namefor code generation; the type tree correctly resolves to the right enum, but the generated identifier matches the wrong runtime object due to shadowing
Why validation doesn't catch it
The expression parser resolves optionRenderType via Variables.resolveAccessor, which walks the type tree: search → filters → optionFilters[] → optionRenderType. This correctly lands on the product-search JayEnumType with values [TEXT_CHOICES, SWATCH_CHOICES]. Validation passes. But the generated code references OptionRenderType.SWATCH_CHOICES, and at runtime OptionRenderType is the product-options version (due to import shadowing) which has [TEXT_CHOICES, COLOR_SWATCH_CHOICES].
Design
Approach: Import-level aliasing with JayEnumType.alias
Keep JayEnumType.name as the canonical contract-defined name (used in .d.ts generation). Add an optional alias field that the expression parser checks first. Import uses import { Name as Alias }.
Generated output (fixed)
import { ..., OptionRenderType } from './product-search.jay-contract';
import { OptionRenderType as OptionRenderType$1, ChoiceType } from './product-options.jay-contract';
Expression for search.filters.optionFilters:
→ vs1.optionRenderType === OptionRenderType.SWATCH_CHOICES ✓
Expression for search.searchResults.quickOption (if used):
→ vs1.optionRenderType === OptionRenderType$1.COLOR_SWATCH_CHOICES ✓
Implementation Plan
Step 1: Add alias field to JayEnumType
File: packages/compiler/compiler-shared/lib/jay-type.ts
Add public alias?: string to JayEnumType. This is a page-local override — set only when a collision is detected during page compilation. Contract .d.ts generation is unaffected (never sets alias).
Step 2: Use alias in expression parser
File: packages/compiler/compiler-jay-html/lib/expressions/expression-parser.pegjs
In the enumCondition rule (line 566-575), replace head.resolvedType.name with (head.resolvedType.alias || head.resolvedType.name).
Rebuild parser (yarn build regenerates expression-parser.cjs).
Step 3: Deduplicate enums within a single headless import
File: packages/compiler/compiler-jay-html/lib/jay-target/jay-html-parser.ts
In both enum collection blocks (lines ~679-701 and ~970-992), deduplicate enumsFromOtherContracts by (declaringModule, type.name) before building enumImportLinks. Fixes the {X, Y, X, Y} duplication from multiply-linked sub-contracts.
Step 4: Resolve cross-contract collisions in parseJayFile
File: packages/compiler/compiler-jay-html/lib/jay-target/jay-html-parser.ts
After allHeadlessImports is assembled (line ~1254), before parseTypes is called:
- Collect all enum-typed
JayImportNameentries from allcontractLinksacross all headless imports - Group by
name - For groups with >1 entry from different modules:
- First occurrence: unchanged
- Subsequent: set
enumType.alias = name + '$' + counter, setimportName.as = alias
- Always alias across different modules — even same values in different order would produce different numeric indices
Must happen before parseTypes because the type tree is built from the same JayEnumType instances — the alias field propagates automatically.
Verification
cd packages/compiler/compiler-jay-html && yarn vitest run— all tests pass- Golf page
.d.tshas no duplicate OptionRenderType imports - Expression output uses aliased name for the colliding enum
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.