You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Build a migration assistant: a tool that reads .res files, recognises common patterns, and emits .affine skeletons. Not a transliterator — the human picks the decomposition; the tool emits the AffineScript syntax.
Capabilities
Read .res AST (via ReScript's bs-tools or similar)
Map known patterns:
let _ = X.constructor → call out "explicit registration needed"
Promise.catch(e => ...) → "Promise umbrella, propose Result + sum"
Named-arg lambda blob → "row-poly handler record candidate"
4+ tuple match → "Validation chain candidate"
Surface patterns without a clean translation (effect handlers, exceptions, mutable globals) for human decision
Output .affine skeletons + a comment-block listing the migration considerations
Why this is also the metaparser story
This tool is a parser for .res that produces an AST and emits .affine syntax — a metaparser, in the architectural-recommendation sense. Once it exists, the same machinery can drive other migrations (.ts→.affine, .js→.affine, etc.). It's the first concrete user of the "tree-sitter as canonical grammar source" architecture.
Why this matters for idaptik
idaptik has 542 .res + ~80 .ts files to migrate. By-hand is impractical; a per-pattern transliterator misses the point of re-decomposition. The migration assistant is the middle ground.
Filed from idaptik Wave 3 pilot. See LESSONS.md and PILOT.md. Idaptik commit hyperpolymath/idaptik@aef38db.
✅ merged 2026-05-27/28 (was tracked here as 🟡 open)
3
partial translation
(in flight)
🟡 open
So the only remaining phase is Phase 3 — partial translation, and this is its first slice.
PR #477 — Phase 3, slice 1 (Refs #57): a new --translate flag renders the two most mechanical, fully-structural forms into compilable AffineScript, leaving everything else a TODO island:
type userId = int -> type UserId = Int
type color = Red | Green -> type Color = | Red | Green
type shape = Circle(float) -> type Shape = | Circle(Float)
Conservative by construction — generics, qualified paths, record bodies, non-primitive payloads, GADT returns and variant spreads are skipped, never guessed, so output is always compilable. Built on the Phase-2c tree-sitter AST (walker-only).
Note on the #228 gate. Epic #406 lists this migration as blocked on #228 (module-qualified type paths). That grammar gap appears to have closed: lib/parser.mly:515 now wires qualified_type_name into type_expr_primary, so Mod.Type / Mod::Type parse in type position (citing "ADR-014 (#228)"). #228 itself is still open — presumably pending the ADR write-up / formal closure — but the parse blocker that gated estate porting is gone. Slice 1 here is #228-independent by design regardless.
Deferred to later Phase-3 slices: record types, generic decls, module-qualified references (now that the grammar supports them), let→const for literal bindings, and the switch→match expression rewrite (needs body translation).
res-to-affine --translate now turns the fully-structural type declarations into compiler-verified AffineScript. Two slices merged to main, both with the generated output validated by the compiler itself (main.exe check → Type checking passed):
primitive type aliases (type userId = int → type UserId = Int), simple sum types (type color = Red | Green → type Color = | Red | Green, payloads mapped)
SessionStart hook provisioning the OCaml toolchain + tree-sitter so web sessions can dune build/dune runtest; STATE.a2ml note
Conservative by construction: a declaration translates only when every part is representable; qualified-path RHS, non-primitive refs, nested generics (array<int>), object types, GADT returns, variant spreads, and mutable/optional-? records are skipped (kept in the marker block + quoted original), never mis-translated.
Remaining Phase-3 work — this issue stays open:
let-to-const for literal bindings
switch→match (needs arm-body translation, not just declaration shapes)
Per the repo's ISSUE-CLOSURE rule I'm not self-closing. If you consider the type-declaration coverage sufficient to call the assistant "done", close at your discretion (or say the word and I'll keep going on the next slice).
res-to-affine --translate now translates every self-contained top-level declaration that can be lowered to compiler-verified AffineScript. Three slices landed; each generated form is confirmed by the compiler itself (main.exe check → Type checking passed):
SessionStart hook provisioning the OCaml toolchain so web sessions build/test out of the box
Conservative throughout — qualified paths, non-primitive refs, nested generics, GADT returns, variant spreads, mutable/optional records, and non-literal/ref/destructuring lets are skipped (kept in the marker block + quoted original), never mis-translated.
Scope boundary — the two items that remain
These are out of the "every emitted form type-checks standalone" model by construction, not just unimplemented:
switch→match — a match is an expression, only meaningful inside a function body. Producing a type-checkable result means translating the whole enclosing function, but ReScript bindings are usually un-annotated (let f = x => …) while AffineScript fn requires param/return types — so the output wouldn't type-check. This belongs to a future partial-port mode (translate-with-TODO-holes) that drops the standalone-type-check guarantee. Different model, worth its own issue.
The declaration-translation milestone is complete within the type-checks-guarantee scope. The remaining work (partial-port mode for expressions/functions + module mapping) is a distinct effort. Per the repo's ISSUE-CLOSURE rule I'm not self-closing — your call whether to close this and track the partial-port mode as a fresh issue, or keep it open as the umbrella for those two items.
The goal of this issue — the .res→.affine migration assistant (parser + metaparser + skeleton emission, through to translating self-contained top-level declarations) — is delivered:
The remaining work — switch→match / function translation (partial-port mode) and module-qualified reference resolution — is a distinct model (it deliberately drops the standalone-type-check guarantee) and is now tracked in the successor #488.
Closed as completed; further work continues under #488.
affinescript#463 unblocked non-ASCII string emit in Deno-ESM (Unicode escapes, not octal).
affinescript#421 put stdlib/json on the Zig-FFI rails (hpm-json-rsr).
What this means for the gap tracked in this ticket:
The codegen pattern is now a known repeatable shape: declare in stdlib/*.affine → wire in deno_builtins table + prelude → smoke-test in tests/codegen-deno/*.
This ticket remains open as a tracked gap; no new mass-port is being kicked off from this post.
Surfacing status so the broader picture stays accurate as more sub-tickets / dependent campaigns reference it.
Activity
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
feat(res-to-affine): walker parity for raw-js, untyped-exception, mut…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
salvage: WIP Ephapax port + assail recipe from ambientops/session-sen…
feat(res-to-affine): Phase 2c — walker covers all 6 anti-patterns + d…
fix(res-to-affine): walker — explicit record-literal pattern matches …
hyperpolymath commentedon May 27, 2026
Now tracked under estate migration epic: affinescript#406.
ci(drift): detect package-essentials drift vs standalone repos (#9)
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
hyperpolymath commentedon May 30, 2026
Status reconciliation + Phase 3 kickoff (2026-05-30)
Re-checked against
main. The 2026-05-25 table above is stale for Phase 2c — it landed two days later:--engine=walkeris the defaultSo the only remaining phase is Phase 3 — partial translation, and this is its first slice.
PR #477 — Phase 3, slice 1 (
Refs #57): a new--translateflag renders the two most mechanical, fully-structural forms into compilable AffineScript, leaving everything else a TODO island:Conservative by construction — generics, qualified paths, record bodies, non-primitive payloads, GADT returns and variant spreads are skipped, never guessed, so output is always compilable. Built on the Phase-2c tree-sitter AST (walker-only).
Note on the #228 gate. Epic #406 lists this migration as blocked on #228 (module-qualified type paths). That grammar gap appears to have closed:
lib/parser.mly:515now wiresqualified_type_nameintotype_expr_primary, soMod.Type/Mod::Typeparse in type position (citing "ADR-014 (#228)"). #228 itself is still open — presumably pending the ADR write-up / formal closure — but the parse blocker that gated estate porting is gone. Slice 1 here is #228-independent by design regardless.Deferred to later Phase-3 slices: record types, generic decls, module-qualified references (now that the grammar supports them),
let→constfor literal bindings, and theswitch→matchexpression rewrite (needs body translation).Generated by Claude Code
feat(res-to-affine): Phase 3 slice 1 — structural type-decl translati…
feat(res-to-affine): Phase 3 slice 2 — record + generic type-decl tra…
chore(config): SessionStart toolchain hook + Phase-3 state note (Refs #…
hyperpolymath commentedon May 30, 2026
Phase 3 state update (2026-05-30) — slices 1+2 landed, tooling provisioned
res-to-affine --translatenow turns the fully-structural type declarations into compiler-verified AffineScript. Two slices merged tomain, both with the generated output validated by the compiler itself (main.exe check→ Type checking passed):2763909type userId = int→type UserId = Int), simple sum types (type color = Red | Green→type Color = | Red | Green, payloads mapped)3484010struct(type point = {x: int}→struct Point { x: Int }), generics ('a→[A], threaded through aliases / sums / records)138baacdune build/dune runtest; STATE.a2ml noteConservative by construction: a declaration translates only when every part is representable; qualified-path RHS, non-primitive refs, nested generics (
array<int>), object types, GADT returns, variant spreads, andmutable/optional-?records are skipped (kept in the marker block + quoted original), never mis-translated.Remaining Phase-3 work — this issue stays open:
let-to-constfor literal bindingsswitch→match(needs arm-body translation, not just declaration shapes)Belt.Map.tnow parses (the LANG: type/effect grammar has no module-qualified path —Pkg.Type/Pkg.Effectunrepresentable (estate-wide port blocker; ADR-014) #228 grammar gap closed) butBelt::Map::Twon't resolve against a non-existent target module, so it's gated on a module-mapping story rather than emitted blindPer the repo's ISSUE-CLOSURE rule I'm not self-closing. If you consider the type-declaration coverage sufficient to call the assistant "done", close at your discretion (or say the word and I'll keep going on the next slice).
Generated by Claude Code
feat(res-to-affine): Phase 3 slice 3 — literal let→const translation …
hyperpolymath commentedon May 31, 2026
Phase 3 — declaration translation complete (2026-05-31)
res-to-affine --translatenow translates every self-contained top-level declaration that can be lowered to compiler-verified AffineScript. Three slices landed; each generated form is confirmed by the compiler itself (main.exe check→ Type checking passed):27639093484010struct; generics ('a→[A]across aliases/sums/records)ea8bef5let→ typedconst(let answer = 42→const answer: Int = 42;) for int/float/string/bool138baacConservative throughout — qualified paths, non-primitive refs, nested generics, GADT returns, variant spreads,
mutable/optional records, and non-literal/ref/destructuring lets are skipped (kept in the marker block + quoted original), never mis-translated.Scope boundary — the two items that remain
These are out of the "every emitted form type-checks standalone" model by construction, not just unimplemented:
switch→match— amatchis an expression, only meaningful inside a function body. Producing a type-checkable result means translating the whole enclosing function, but ReScript bindings are usually un-annotated (let f = x => …) while AffineScriptfnrequires param/return types — so the output wouldn't type-check. This belongs to a future partial-port mode (translate-with-TODO-holes) that drops the standalone-type-check guarantee. Different model, worth its own issue.Belt.Map.tnow parses (the LANG: type/effect grammar has no module-qualified path —Pkg.Type/Pkg.Effectunrepresentable (estate-wide port blocker; ADR-014) #228 grammar gap closed) but won't resolve against a target module that doesn't exist yet; gated on a module-mapping story.Closure
The declaration-translation milestone is complete within the type-checks-guarantee scope. The remaining work (partial-port mode for expressions/functions + module mapping) is a distinct effort. Per the repo's ISSUE-CLOSURE rule I'm not self-closing — your call whether to close this and track the partial-port mode as a fresh issue, or keep it open as the umbrella for those two items.
Generated by Claude Code
hyperpolymath commentedon May 31, 2026
Closing per owner direction (2026-05-31).
The goal of this issue — the
.res→.affinemigration assistant (parser + metaparser + skeleton emission, through to translating self-contained top-level declarations) — is delivered:2763909), records →struct+ generics (feat(res-to-affine): Phase 3 slice 2 — record + generic type-decl translation (Refs #57) #4813484010), literallet→ typedconst(feat(res-to-affine): Phase 3 slice 3 — literal let→const translation (Refs #57) #484ea8bef5) — every emitted form compiler-verified (main.exe check→ Type checking passed).138baac).The remaining work —
switch→match/ function translation (partial-port mode) and module-qualified reference resolution — is a distinct model (it deliberately drops the standalone-type-check guarantee) and is now tracked in the successor #488.Closed as completed; further work continues under #488.
Generated by Claude Code
chore(docs,config): slice-3 + #57-closure state note; git-workflow pe…
docs: point migration trackers at #488 (successor to closed #57) (#493)
hyperpolymath commentedon Jun 1, 2026
Status update — AS stdlib codegen pattern now established (2026-06-01) 📌
For ticket coordinators / anyone tracking this gap:
lib/codegen_deno.ml. Tests live intests/codegen-deno/*.What this means for the gap tracked in this ticket:
stdlib/*.affine→ wire indeno_builtinstable + prelude → smoke-test intests/codegen-deno/*.🤖 Cross-estate AS-status sweep on 2026-06-01.
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re… (
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re… (
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re… (
Claude/safedom res stale sweep (#60)
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re… (
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…