From ade849b89af8e2a4347a51c4772519965a54a6f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 07:01:16 +0000 Subject: [PATCH] =?UTF-8?q?feat(res-to-affine):=20#488=20partial-port=20sl?= =?UTF-8?q?ice=203=20=E2=80=94=20array=20+=20record=20literals=20(Refs=20#?= =?UTF-8?q?488)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends --partial expression coverage: - array literals: ReScript `[a, b]` -> AffineScript `[a, b]` (1:1). - record literals: ReScript `{x: x, y: y}` -> `Rec #{ x: x, y: y }`. AffineScript records are NOMINAL (`Type #{ .. }`), so an anonymous ReScript record gets the placeholder type `Rec` for the human to rename; field punning `{x}` expands to `x: x`; field values are translated. JS objects, interpolated template strings, try/catch etc. still become `() /* TODO */` holes. Verified locally: full dune build exit 0; 32 res-to-affine walker tests green (2 new); `main.exe check` on the skeletons parses (resolution/type errors only, no parse error). Refs #488 https://claude.ai/code/session_017T8SzHr2yXav8hm4Ho76Uw --- CHANGELOG.md | 1 + tools/res-to-affine/README.md | 18 +++++++----- .../res-to-affine/test/fixtures/partial1.res | 15 +++++++--- tools/res-to-affine/test/test_walker.ml | 21 ++++++++++++-- tools/res-to-affine/walker.ml | 29 +++++++++++++++++++ 5 files changed, 70 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 539be27..3ec39bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ this project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- feat(res-to-affine): partial-port mode #488 slice 3 — `--partial` now translates array literals (`[a, b]`) and record literals (`{x, y}` → `Rec #{ x: x, y: y }`, with a nominal placeholder type + field-punning expansion) (Refs #488) - feat(res-to-affine): partial-port mode #488 slice 2 — `--partial` now desugars ReScript pipe-first `->` (`a->f(b)` → `f(a, b)`, chained left-to-right), and translates `if`/`else` and blocks with `let` statements (Refs #488) - feat(res-to-affine): partial-port mode (#488) — new `--partial` flag renders module-top-level functions as AffineScript `fn` skeletons with `switch`→`match` and best-effort expression translation (literals / idents / calls / binary ops with float-op + identity-equality normalisation / `++` / member + qualified access / ternary / variant + tuple + literal patterns); un-translatable forms become `() /* TODO */` / `_ /* TODO */` holes. Output deliberately does NOT type-check but parses (verified). Distinct model from `--translate` (Refs #488) - feat(res-to-affine): Phase 3 slice 3 — `--translate` now also lowers module-level `let = ` (int/float/string/bool) to a typed `const name: T = value;`; call / `ref(...)` / destructuring bindings are skipped (not compile-time constants); every emitted form verified compilable via `main.exe check`. `switch`→`match` and qualified-path resolution remain out of the standalone-type-check scope (Refs #57) diff --git a/tools/res-to-affine/README.md b/tools/res-to-affine/README.md index a23e990..7da6414 100644 --- a/tools/res-to-affine/README.md +++ b/tools/res-to-affine/README.md @@ -231,13 +231,17 @@ It translates literals, identifiers, calls, binary operators (normalising ReScript's float ops `+.`/`*.` → `+`/`*` and `===`/`!==` → `==`/`!=`), string concat `++`, member/qualified access, ternaries, **`if`/`else`**, **blocks with `let` statements**, **pipe-first `->`** (`a->f(b)` → `f(a, b)`, chained -left-to-right), and `switch`→`match` with variant/tuple/literal patterns. -Anything else (records, arrays, objects, …) becomes a `() /* TODO */` hole. -The output is a partial port to finish by hand: it **parses** but is not -expected to type-check (verified — the generated skeletons reach -resolution/type-checking without a parse error). Continuing under #488: -record/array/object literals, labelled args, combining `--partial` with -`--translate`, and module-qualified-reference *resolution*. +left-to-right), **array literals** (`[a, b]`), **record literals** (`{x, y}` → +`Rec #{ x: x, y: y }` — AffineScript records are *nominal*, so an anonymous +ReScript record gets the placeholder type `Rec` for the human to rename; +field punning `{x}` expands to `x: x`), and `switch`→`match` with +variant/tuple/literal patterns. Anything else (JS objects, interpolated +template strings, `try`/`catch`, …) becomes a `() /* TODO */` hole. The output +is a partial port to finish by hand: it **parses** but is not expected to +type-check (verified — the generated skeletons reach resolution/type-checking +without a parse error). Continuing under #488: JS objects / template strings, +labelled-arg refinement, combining `--partial` with `--translate`, and +module-qualified-reference *resolution* (a module-mapping policy decision). ## Corpus run diff --git a/tools/res-to-affine/test/fixtures/partial1.res b/tools/res-to-affine/test/fixtures/partial1.res index 33463d7..ff1832b 100644 --- a/tools/res-to-affine/test/fixtures/partial1.res +++ b/tools/res-to-affine/test/fixtures/partial1.res @@ -1,8 +1,9 @@ // SPDX-License-Identifier: MIT // #488 partial-port fixture: module-top-level functions -> `fn` skeletons -// with switch->match, pipe desugaring, if/else, blocks, and best-effort -// expression translation. Output is NOT expected to type-check; it must -// parse, with un-translatable forms (e.g. an array literal) as TODO holes. +// with switch->match, pipe desugaring, if/else, blocks, array/record literals, +// and best-effort expression translation. Output is NOT expected to +// type-check; it must parse, with un-translatable forms (e.g. an +// interpolated template string) as TODO holes. let classify = x => switch x { | Some(n) => n + 1 @@ -27,5 +28,11 @@ let clamp = x => if x > 0 { x } else { 0 } // block with a let statement let scaled = x => { let y = x + 1; y * 2 } -// array literal has no handler yet -> must become a TODO hole. +// array literal -> [x, x] let pair = x => [x, x] + +// record literal -> Rec #{ x: x, y: y } (nominal placeholder type) +let mkpt = (x, y) => {x: x, y: y} + +// interpolated template string has no handler yet -> must become a TODO hole. +let tmpl = x => `val=${x}` diff --git a/tools/res-to-affine/test/test_walker.ml b/tools/res-to-affine/test/test_walker.ml index 50b8f41..1af76f8 100644 --- a/tools/res-to-affine/test/test_walker.ml +++ b/tools/res-to-affine/test/test_walker.ml @@ -401,8 +401,19 @@ let partial1_blob () = let test_partial_count () = skip_unless_ready (); Alcotest.(check int) - "nine module-top-level functions -> fn skeletons" - 9 (List.length (translate_partial1 ())) + "eleven module-top-level functions -> fn skeletons" + 11 (List.length (translate_partial1 ())) + +let test_partial_array () = + skip_unless_ready (); + Alcotest.(check bool) "array literal translated" + true (contains (partial1_blob ()) "[x, x]") + +let test_partial_record () = + skip_unless_ready (); + (* nominal placeholder type `Rec`; field punning {x} -> x: x *) + Alcotest.(check bool) "record literal -> Rec #{ ... }" + true (contains (partial1_blob ()) "Rec #{ x: x, y: y }") let test_partial_pipe () = skip_unless_ready (); @@ -521,8 +532,12 @@ let () = ] ); ( "walker-488-partial", [ - Alcotest.test_case "nine functions -> fn skeletons" + Alcotest.test_case "eleven functions -> fn skeletons" `Quick test_partial_count; + Alcotest.test_case "array literal translated" + `Quick test_partial_array; + Alcotest.test_case "record literal -> Rec #{ ... }" + `Quick test_partial_record; Alcotest.test_case "switch -> match + patterns + arm bodies" `Quick test_partial_switch_to_match; Alcotest.test_case "float op normalised + multi-param skeleton" diff --git a/tools/res-to-affine/walker.ml b/tools/res-to-affine/walker.ml index d79058c..fec0afc 100644 --- a/tools/res-to-affine/walker.ml +++ b/tools/res-to-affine/walker.ml @@ -1044,6 +1044,35 @@ let rec translate_expr ~source n = Printf.sprintf "%s.%s" (translate_expr ~source r) (node_text ~source p) | _ -> todo_expr ~source n) + | "array" -> + Printf.sprintf "[%s]" + (String.concat ", " + (List.filter_map + (fun c -> + if c.ntype = "comment" then None + else Some (translate_expr ~source c)) + n.children)) + (* AffineScript records are nominal (`Type #{ … }`), so an anonymous ReScript + record gets a placeholder type `Rec` for the human to rename; the field + values are translated. Field punning `{x}` expands to `x: x`. *) + | "record" -> + let fields = + List.filter_map + (fun c -> + if c.ntype <> "record_field" then None + else + match List.filter (fun x -> x.ntype <> "comment") c.children with + | [ name ] -> + let nm = node_text ~source name in + Some (Printf.sprintf "%s: %s" nm nm) + | name :: value :: _ -> + Some + (Printf.sprintf "%s: %s" (node_text ~source name) + (translate_expr ~source value)) + | [] -> None) + n.children + in + Printf.sprintf "Rec #{ %s }" (String.concat ", " fields) | _ -> todo_expr ~source n and translate_labeled_arg ~source n =