Skip to content

[Repo] Migration assistant tool: parser + metaparser for .res→.affine skeletons #57

Closed

Description

@hyperpolymath

Proposal

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.

Activity

  1. added 20 commits that reference this issue on May 26, 2026
  2. added 20 commits that reference this issue on May 27, 2026
  3. added 20 commits that reference this issue on May 27, 2026
  4. hyperpolymath commented on May 27, 2026

    @hyperpolymath
    OwnerAuthor

    Now tracked under estate migration epic: affinescript#406.

  5. added 20 commits that reference this issue on May 27, 2026
  6. hyperpolymath commented on May 30, 2026

    @hyperpolymath
    OwnerAuthor

    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:

    Phase Scope PR State
    1 / 1-precision / 2a / 2b scanner → tree-sitter pipeline + first AST detector #314 #319 #321 #322 ✅ merged
    2c walker covers all six anti-patterns; --engine=walker is the default #385 #357 #428 ✅ 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).


    Generated by Claude Code

  7. hyperpolymath commented on May 30, 2026

    @hyperpolymath
    OwnerAuthor

    Phase 3 state update (2026-05-30) — slices 1+2 landed, tooling provisioned

    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):

    Slice PR / commit Covers
    3.1 #477 / 2763909 primitive type aliases (type userId = int → type UserId = Int), simple sum types (type color = Red | Green → type Color = | Red | Green, payloads mapped)
    3.2 #481 / 3484010 record types → struct (type point = {x: int} → struct Point { x: Int }), generics ('a → [A], threaded through aliases / sums / records)
    tooling #482 / 138baac 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:

    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).


    Generated by Claude Code

  8. hyperpolymath commented on May 31, 2026

    @hyperpolymath
    OwnerAuthor

    Phase 3 — declaration translation complete (2026-05-31)

    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):

    Slice PR / commit Adds
    3.1 #477 / 2763909 primitive type aliases; simple sum types (payloads mapped)
    3.2 #481 / 3484010 record types → struct; generics ('a → [A] across aliases/sums/records)
    3.3 #484 / ea8bef5 literal let → typed const (let answer = 42 → const answer: Int = 42;) for int/float/string/bool
    tooling #482 / 138baac 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.
    • module-qualified references — Belt.Map.t now parses (the LANG: type/effect grammar has no module-qualified path — Pkg.Type/Pkg.Effect unrepresentable (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

  9. hyperpolymath commented on May 31, 2026

    @hyperpolymath
    OwnerAuthor

    Closing per owner direction (2026-05-31).

    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.


    Generated by Claude Code

  10. added 2 commits that reference this issue on May 31, 2026
  11. hyperpolymath commented on Jun 1, 2026

    @hyperpolymath
    OwnerAuthor

    Status update — AS stdlib codegen pattern now established (2026-06-01) 📌

    For ticket coordinators / anyone tracking this gap:

    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.

    🤖 Cross-estate AS-status sweep on 2026-06-01.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions