genes-ts: preserve payload reads after erased enum matches - #104
Merged
Conversation
Haxe can prove that only one generic enum constructor can inhabit a value and erase the authored switch. TypeScript still sees the complete emitted union, so the remaining direct payload read fails strict checking. Plan an exact constructor view from the final typed AST before imports are allocated, then render one identity projection only when no emitted discriminant switch already gives TypeScript the same fact. Ordinary switches remain direct and classic JavaScript remains unchanged. Add a dependency-free focused fixture covering typed evidence, fail-closed controls, assertion-only imports, single evaluation, TypeScript 5/6/7, runtime parity, classic output, and source maps. Full acceptance and yarn test:ci pass; the pinned tink_cli pressure test removes only the Source.ts TS2339 diagnostic (16 to 15 on Haxe 4.3.7). Prepared by the GameCarry agent.
Add executable fail-closed controls for an unresolved monomorph receiver and a constructor-local generic application, matching the scope promised by the owning Bead and PR. Document the erased-match boundary in the authoritative architecture and fixture guides with Haxe, failing TypeScript, planned TypeScript, import timing, and unsupported cases. Describe declaration correlation accurately as compiler-owned module/type coordinates with the existing request-local type-parameter source-range fallback. The focused TypeScript 5/6/7, runtime, classic, and source-map gate passes after formatting, as do the agent-guide and staged pre-commit checks. Prepared by the GameCarry agent.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A Haxe program can safely match a generic enum even when TypeScript cannot reconstruct the same proof from the generated union.
This surfaced while compiling GameCarry's package-neutral
tink_cliintegration fixture. The Tink source contains the equivalent of:Reduction<Item, Safety, Quality, Result>has failure constructors that fixSafetyorQualityto an error type. In this call, both parameters are a nominalNoisetype, so those constructors cannot produce the applied enum value. Haxe proves that onlyReducedis possible, accepts the one-case match, and may erase the runtime switch.The final typed Haxe tree still contains an exact payload-read node: it names the
Reducedconstructor, payload slot0, the fully applied receiver enum, and the exact result type. Before this change, Genes emitted the remaining read directly:Strict TypeScript reports
TS2339:resultdoes not exist on the other union variants. This is not unsafe Haxe source and it is not a framework-specific type rule; it is a difference between Haxe's completed enum proof and the information visible to TypeScript after that proof has been erased.What
This PR adds one narrow TypeScript boundary decision for an exact enum payload read left behind by an erased Haxe match.
The generated TypeScript now preserves Haxe's already-proven constructor view:
Register.unsafeCast<T>(value)is a runtime identity operation. It evaluatesvalueonce and returns that same value. It does not convert, validate, repair, clone, or otherwise change the data. Its type argument records for TypeScript the exact constructor fact that Haxe already proved.When Haxe keeps an ordinary discriminant switch, Genes continues to emit idiomatic TypeScript with no assertion:
Classic JavaScript remains the same direct property read:
The focused fixture's complete classic JavaScript and declaration artifacts were byte-identical to unmodified
main. Itsindex.jsSHA-256 wasbbfe700cc37290a991365b7a764a31573386cc7234df3fbf4f5a6f6771161800in both builds. Source-map files differed only because the comparison used different output-directory paths.How
The immutable
TsBoundaryPlannow records anEnumPayloadReaddecision before dependency bindings and import aliases are allocated. The emitter only consumes that precomputed decision; it does not infer a cast while printing TypeScript.A decision is authorized only when the final typed AST proves all of the following:
TEnumParameterpayload-read node;_hx_indexswitch arm already gives TypeScript the same narrowing fact.Exact enum declarations are correlated through Haxe's compiler-owned fully-qualified module/type coordinates. When Haxe re-encodes one logical type parameter through multiple wrapper objects, the shared exact-type comparison may use its already-documented request-local module/name plus source-range fallback. This fallback stays inside one immutable module plan and is not a general source-position identity rule.
The planner does not infer authority from generated property text, unqualified names, TypeScript diagnostics, raw target strings, or downstream framework names. Every type that the assertion can print is exposed to dependency collection before import projection. This prevents late import-binding discovery.
The planner fails closed for
Dynamic, unresolved monomorphs, invalid payload slots, unrelated same-named constructors, and constructor-local generic applications. These cases receive no assertion and remain visible to strict TypeScript rather than being guessed through.Focused regression fixture
tests/enum-payload-narrowingis dependency-free and includes:Dynamic, an unresolved monomorph, an invalid slot, a constructor-local generic, and an unrelated same-named enum;Factory.read()receiver that proves single evaluation;The same Haxe → failing TypeScript → planned TypeScript example, proof boundary, limitations, and import timing are documented in
docs/ARCHITECTURE.md, and the compiler fixture guide routes future changes to this test.Run the focused task with:
Downstream evidence
Against GameCarry's pinned package-neutral
tink_clifixture and official Haxe 4.3.7, this branch changes the strict TypeScript diagnostic count from exactly 16 to 15. The only removed diagnostic is the expectedTS2339intink/io/Source.ts; the other 15 diagnostics are unchanged.The fixture was then regenerated with GameCarry's authoritative merged Genes pin, restoring the documented baseline of 16. GameCarry will not pin this branch: it will move through Lix only after this PR is independently reviewed and merged.
The two separate
Bytes.toHexdiagnostics remain intentionally visible. Their missing typed boundary belongs to the Haxe standard library and is tracked in HaxeFoundation/haxe#13003 and HaxeFoundation/haxe#13004; this PR does not add a raw-syntax inference rule.Validation
Observed locally on the implementation commit:
yarn test:enum-payload-narrowingyarn test:higher-order-enum-constructorsyarn test:classic:dtsyarn test:genes-ts:full— 350/350 assertions in the acceptance profileyarn test:compiler-server— cold/warm requests, edits, rollback, and cleanupyarn test:output-qualityyarn benchmark:dependency-planyarn test:acceptance— passed in 1,013.98 seconds, including both TodoApp Playwright profilesyarn test:ci— passed in 1,175.45 secondsyarn precommit:run— formatting and staged secret scan passedAfter independent review requested the missing negative evidence and architecture documentation, exact head
def4499also passed:yarn test:enum-payload-narrowingafter staged formatting — includes the new unresolved-monomorph and constructor-local-generic negatives across TypeScript 5/6/7, runtime, classic output, and source mapsyarn test:agent-guidesyarn precommit:runGitHub's protected CI matrix reruns on the exact new head before merge.
Report-only output performance remained in the established range: TypeScript approximately 2.0–2.1 seconds and classic approximately 1.9–2.0 seconds for the measured fixture. The dependency-plan benchmark completed at 128, 256, and 512 edge sizes; the largest/smallest ratio was 5.11× for 4× the edges.
Independent review disposition
The first independent Genes review found no implementation correctness defect, but requested three completeness changes. Exact head
def4499now:The second independent review approved exact head
def4499with no remaining actionable finding. The PR is ready for review; merge remains gated on required CI and a final conversation/thread check.Limits
This is not a blanket assertion for enum payload reads and it is not a TypeScript assignability engine. It intentionally does not authorize projections from
Dynamic,Any, unresolved monomorphs, copied constructor names, diagnostic text, raw JavaScript strings, or constructor-local generic applications whose exact parameters are absent from the receiver.Unsupported shapes continue to fail strict TypeScript so they can be modeled deliberately instead of being hidden by a broad cast.
Owning Bead:
genes-pxkv.14.Prepared by the GameCarry agent.