Repository navigation
[Repo] res-to-affine partial-port mode: switch→match / function translation + module-qualified references #488
Description
Proposal
Successor to #57 (now closed — its goal, the .res→.affine declaration translator, is delivered). This issue tracks the next translation model for tools/res-to-affine/: a partial-port mode that translates expression- and function-level constructs, plus module-qualified references.
Why a separate issue / model
--translate today guarantees that every emitted form type-checks standalone (main.exe check → Type checking passed), which is exactly why it's bounded to self-contained top-level declarations: type aliases, sums, structs, generics, and literal let→const (slices delivered in #477 / #481 / #484). Two remaining forms cannot meet that guarantee by construction and need a different model:
1. switch→match (+ function translation) — partial-port mode
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 parameter and return types, so the output won't compile.
The model this needs is translate-with-TODO-holes: render the structural skeleton of a function (signature stub + match arms with translated patterns) and leave un-inferable bodies / types as explicit // TODO islands. This drops the standalone-type-check guarantee — output is a partial port a human finishes, matching the README's original "~60–80% translation" vision. Scope to design:
- function signature stubs from un-annotated params (TODO-typed params, inferred-where-possible)
switch e { | P => body, … }→match e { P => body, … }(pattern translation;=>kept; leading|dropped; commas added)- a small expression translator for the common, safe cases (literals, identifiers, constructor application, simple calls/operators), TODO for the rest
2. Module-qualified references
Belt.Map.t now parses (the #228 grammar gap closed — parser.mly:515 wires qualified_type_name into type_expr_primary) but Belt::Map::T won't resolve against a target module that doesn't exist yet. Needs a module-mapping story (ReScript stdlib module → AffineScript stdlib module, or an explicit "unresolved import" marker) before it can be emitted without breaking a build.
Acceptance / starting points
- Decide the partial-port output contract (it is explicitly not required to type-check; document that distinction from the declaration translator).
- Reuse the existing walker AST (
tools/res-to-affine/walker.ml) and the conservative skip-don't-guess discipline. - Gate tests like the existing
walker-phase3*suites (skip without the tree-sitter grammar).
Context
- Predecessor: [Repo] Migration assistant tool: parser + metaparser for .res→.affine skeletons #57 (declaration translation — closed completed). Child of estate migration epic affinescript: estate-wide ReScript→AffineScript migration tracker [meta] #406.
- Grammar dep for qualified refs: LANG: type/effect grammar has no module-qualified path —
Pkg.Type/Pkg.Effectunrepresentable (estate-wide port blocker; ADR-014) #228. - Scope boundary write-up:
tools/res-to-affine/README.md§"Scope boundary — what--translatewill not do".
Filed as the successor to #57 at the owner's request.
Activity
chore(docs,config): slice-3 + #57-closure state note; git-workflow pe…
docs: point migration trackers at #488 (successor to closed #57) (#493)
feat(res-to-affine): partial-port mode — --partial fn skeletons + swi…
feat(res-to-affine): #488 partial-port slice 2 — pipe desugaring + if…
feat(res-to-affine): #488 partial-port slice 3 — array + record liter…
hyperpolymath commentedon May 31, 2026
Partial-port mode — slices 1–3 landed (2026-05-31)
The
--partialflag (the partial-port model from this issue, distinct from--translate) now ports module-top-level functions into AffineScriptfnskeletons. Output deliberately does not type-check (un-annotated ReScript functions can't yield a compilablefn) but parses — un-inferable types are_holes, un-translatable forms are() /* TODO *//_ /* TODO */islands. Each slice was built/tested/parse-verified locally and admin-merged past the backlogged CI:c157a0ffnskeletons;switch→match(variant/tuple/literal patterns); calls; binary ops (float-op + identity-equality normalisation);++; member/qualified access; ternary78906f9->desugaring (incl. left-nested chainsx->f->g(2)→g(f(x), 2));if/else; blocks withletstatements4d4d1d4[a, b]); record literals ({x, y}→Rec #{ x: x, y: y }, nominal placeholder type + field punning)32 walker tests;
main.exe checkconfirms the skeletons reach resolution/type-checking with no parse error.Remaining (this issue stays open)
--partialwith--translate— emit declarations and function skeletons in one pass (the "port this whole file" experience)Belt.Map.tparses but won't resolve; needs a ReScript→AffineScript module-mapping policy (deliberately not built without your scoping — it's a design decision, not just code)Happy to pick any of these up — the module-mapping one I'd want to settle the policy with you first.
Generated by Claude Code
chore(docs,config): #488 partial-port state note; allow compiler orac…