Skip to content

feat(res-to-affine): Phase 3 slice 1 — structural type-decl translation (Refs #57)#477

Merged
hyperpolymath merged 1 commit into
mainfrom
claude/pensive-fermi-JVCzu
May 30, 2026
Merged

feat(res-to-affine): Phase 3 slice 1 — structural type-decl translation (Refs #57)#477
hyperpolymath merged 1 commit into
mainfrom
claude/pensive-fermi-JVCzu

Conversation

@hyperpolymath
Copy link
Copy Markdown
Owner

What & why

Issue #57 (the .res.affine migration assistant) is multi-phase. Reconciling the issue against main: Phases 1, 1-precision, 2a, 2b, and 2c are all merged (the issue's 2026-05-25 status comment is stale — #357/#385/#428 landed 2c: the walker now covers all six anti-patterns and is the default engine). The only remaining phase is Phase 3 — partial translation.

This PR lands Phase 3, slice 1: the emitter stops only marking and starts translating the two most mechanical, fully-structural forms into compilable AffineScript via a new --translate flag.

ReScript AffineScript
type userId = int type UserId = Int
type color = Red | Green | Blue type Color =
| Red
| Green
| Blue
type shape = Circle(float) | Rect(int, int) type Shape =
| Circle(Float)
| Rect(Int, Int)

Everything else stays a TODO island (markers + quoted original), so nothing is lost.

Why this slice, and the #228 relationship

Epic #406 notes the estate migration was blocked on #228 (module-qualified type paths). While preparing this I found #228's grammar fix has in fact landedlib/parser.mly:515 now wires qualified_type_name into type_expr_primary, so Mod.Type/Mod::Type parse (the issue is still open pending the ADR doc/closure). Regardless, this slice is deliberately #228-independent: it translates only primitive aliases and simple sums, so it's correct whether or not #228 is formally closed. Qualified-path translation is left for a later slice now that the grammar supports it.

Conservative by construction

A declaration that uses type parameters/generics, a qualified path (Belt.Map.t), a record body, non-primitive references, a GADT return annotation, or a variant spread is skipped — never guessed — so translated output is always compilable. Detection keys on node type (not field labels), making it robust to the grammar's optional alias/body field ambiguity; any node-name drift degrades to "skip", never a bad rewrite. Lower-case ReScript type names are capitalised because lib/parser.mly reads a lower-case name in type position as a type variable (TyVar), only an upper-case name as a TyCon.

Changes

  • walker.mltranslate_* helpers + collect_translations + Walker.translate (node names pinned to rescript-lang/tree-sitter-rescript@990214a); a shared parse_file that scan now reuses (behaviour unchanged).
  • emitter.mlEmitter.emit_translation (proper module Name; header with the required semicolon; the existing emit/skeleton path is untouched, so the sample.affine snapshot is stable).
  • main.ml--translate flag, walker-only wiring (no-op under --engine=scanner), refreshed docstring.
  • testsfixtures/phase3.res + 5 gated walker tests (translation count, each translated shape, and the generic/qualified/non-type forms it must skip). Gated like the existing walker tests; they execute in the build job, which installs the tree-sitter CLI and builds the pinned grammar.
  • README + CHANGELOG — document the slice and what's explicitly deferred (records, generics, qualified refs, letconst, switchmatch).

Test plan

  • dune build (CI — OCaml toolchain isn't available in this remote env; CI is the build oracle)
  • dune runtest — the 5 gated walker-phase3-translate tests run against the pinned grammar in the build job
  • sample.affine emitter snapshot unchanged (default path untouched)

Issue status

Refs #57 (not Closes) — consistent with every prior phase PR and the repo's ISSUE-CLOSURE rule. Phase 3 continues in later slices; I've posted a status comment on #57 correcting the stale 2c tracking and recording this slice.

https://claude.ai/code/session_017T8SzHr2yXav8hm4Ho76Uw


Generated by Claude Code

…on (Refs #57)

Phases 1/2 of the .res→.affine migration assistant only *mark* anti-patterns.
This begins Phase 3 (partial translation): a new `--translate` flag renders the
two most mechanical, module-qualified-path-independent forms into compilable
AffineScript, inline in the skeleton:

  type userId = int            -> type UserId = Int
  type color  = Red | ...      -> type Color = | Red | Green | Blue
  type shape  = Circle(float)  -> type Shape = | Circle(Float) | Rect(Int, Int)

Everything else stays a TODO island (markers + quoted original), so nothing is
lost. The translator is conservative by construction — type parameters/generics,
qualified paths, record bodies, non-primitive references, GADT return
annotations, and variant spreads are skipped rather than guessed, so output is
always compilable. Lower-case ReScript type names are capitalised because in
type position lib/parser.mly reads a lower_ident as a type *variable* (TyVar),
only an upper_ident as a TyCon.

Implementation walks the existing Phase-2c tree-sitter AST (node names pinned to
rescript-lang/tree-sitter-rescript@990214a), so it is walker-only; with
--engine=scanner the flag is a no-op. Detection keys on node type rather than
field labels, so it is robust to the grammar's optional alias/body field
ambiguity, and any node-name drift degrades to "skip", never a bad rewrite.

- walker.ml: translate_* + collect_translations + Walker.translate (shared
  parse_file; scan refactored onto it, behaviour unchanged)
- emitter.ml: Emitter.emit_translation (proper `module Name;` header with the
  required semicolon; emit/skeleton path untouched, snapshot stable)
- main.ml: --translate flag, walker-only wiring, refreshed docstring
- test: fixtures/phase3.res + 5 gated walker tests (count + each shape + the
  generic/qualified/non-type forms it must skip)
- README + CHANGELOG: document the slice and what is deferred

Refs #57

https://claude.ai/code/session_017T8SzHr2yXav8hm4Ho76Uw
@hyperpolymath hyperpolymath marked this pull request as ready for review May 30, 2026 22:52
@hyperpolymath hyperpolymath merged commit 2763909 into main May 30, 2026
0 of 24 checks passed
@hyperpolymath hyperpolymath deleted the claude/pensive-fermi-JVCzu branch May 30, 2026 22:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants