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
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 83 issues detected

Severity Count
🔴 Critical 2
🟠 High 13
🟡 Medium 68

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Action perpolymath/standards/.github/workflows/governance-reusable.yml@main\n needs attention",
    "type": "unpinned_action",
    "file": "governance.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action ons/checkout@v6\n    needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action land/setup-deno@v2\n    needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in affine-vscode-publish.yml",
    "type": "missing_timeout_minutes",
    "file": "affine-vscode-publish.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "missing_timeout_minutes",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "missing_timeout_minutes",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "missing_timeout_minutes",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "missing_timeout_minutes",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "missing_timeout_minutes",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "missing_timeout_minutes",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

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

Labels

None yet

2 participants