Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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 <id> = <literal>` (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)
- feat(res-to-affine): Phase 3 slice 2 — `--translate` now also handles record types (→ `struct`) and generics (type params `'a` → `[A]`) across aliases / sums / records; `mutable`/optional-`?` records, qualified paths, and nested generics are still skipped (never guessed); every emitted form verified compilable via `main.exe check` (Refs #57)
Expand Down
20 changes: 13 additions & 7 deletions tools/res-to-affine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,16 +222,22 @@ AffineScript `fn` skeleton:
| `let area = (w, h) => w *. h` | `fn area(w: _, h: _) -> _ { w * h }` |
| `let classify = x => switch x { \| Some(n) => n + 1 \| None => 0 }` | `fn classify(x: _) -> _ { match x { Some(n) => n + 1, None => 0, } }` |
| `let greet = name => "hi " ++ name` | `fn greet(name: _) -> _ { "hi " ++ name }` |
| `let piped = x => x->doStuff(1)` | `fn piped(x: _) -> _ { doStuff(x, 1) }` |
| `let chain = x => x->f->g(2)` | `fn chain(x: _) -> _ { g(f(x), 2) }` |
| `let clamp = x => if x > 0 { x } else { 0 }` | `fn clamp(x: _) -> _ { if x > 0 { x } else { 0 } }` |
| `let scaled = x => { let y = x + 1; y * 2 }` | `fn scaled(x: _) -> _ { let y = x + 1; y * 2 }` |

It translates literals, identifiers, calls, binary operators (normalising
ReScript's float ops `+.`/`*.` → `+`/`*` and `===`/`!==` → `==`/`!=`), string
concat `++`, member/qualified access, ternaries, and `switch`→`match` with
variant/tuple/literal patterns. Anything else (pipe-first `->`, records, etc.)
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).
First slice; the next steps (pipe desugaring `a->f(b)` → `f(a, b)`, `if`/block
bodies, combining with `--translate`) continue under #488.
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*.

## Corpus run

Expand Down
20 changes: 16 additions & 4 deletions tools/res-to-affine/test/fixtures/partial1.res
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT
// #488 partial-port fixture: module-top-level functions -> `fn` skeletons
// with switch->match and best-effort expression translation. Output is NOT
// expected to type-check; it must parse, with un-translatable forms as
// `() /* TODO */` holes.
// 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.

let classify = x => switch x {
| Some(n) => n + 1
Expand All @@ -15,5 +15,17 @@ let greet = name => "hi " ++ name

let log2 = msg => Js.log(msg)

// pipe-first has no AffineScript equivalent -> must become a TODO hole.
// pipe-first desugars: x->doStuff(1) -> doStuff(x, 1)
let piped = x => x->doStuff(1)

// pipe chain desugars left-to-right: x->f->g(2) -> g(f(x), 2)
let chain = x => x->f->g(2)

// if/else
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.
let pair = x => [x, x]
30 changes: 27 additions & 3 deletions tools/res-to-affine/test/test_walker.ml
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,26 @@ let partial1_blob () =
let test_partial_count () =
skip_unless_ready ();
Alcotest.(check int)
"five module-top-level functions -> fn skeletons"
5 (List.length (translate_partial1 ()))
"nine module-top-level functions -> fn skeletons"
9 (List.length (translate_partial1 ()))

let test_partial_pipe () =
skip_unless_ready ();
let blob = partial1_blob () in
(* x->doStuff(1) -> doStuff(x, 1); chain x->f->g(2) -> g(f(x), 2) *)
Alcotest.(check bool) "pipe-first desugars, including left-nested chains"
true (contains blob "doStuff(x, 1)" && contains blob "g(f(x), 2)")

let test_partial_if () =
skip_unless_ready ();
Alcotest.(check bool) "if/else translated"
true (contains (partial1_blob ()) "if x > 0 { x } else { 0 }")

let test_partial_block () =
skip_unless_ready ();
let blob = partial1_blob () in
Alcotest.(check bool) "block body with a let statement translated"
true (contains blob "let y = x + 1" && contains blob "y * 2")

let test_partial_switch_to_match () =
skip_unless_ready ();
Expand Down Expand Up @@ -503,7 +521,7 @@ let () =
] );
( "walker-488-partial",
[
Alcotest.test_case "five functions -> fn skeletons"
Alcotest.test_case "nine functions -> fn skeletons"
`Quick test_partial_count;
Alcotest.test_case "switch -> match + patterns + arm bodies"
`Quick test_partial_switch_to_match;
Expand All @@ -513,5 +531,11 @@ let () =
`Quick test_partial_concat_and_call;
Alcotest.test_case "untranslatable form -> TODO hole"
`Quick test_partial_todo_hole;
Alcotest.test_case "pipe-first desugars (incl. chains)"
`Quick test_partial_pipe;
Alcotest.test_case "if/else translated"
`Quick test_partial_if;
Alcotest.test_case "block with let statement translated"
`Quick test_partial_block;
] );
]
96 changes: 92 additions & 4 deletions tools/res-to-affine/walker.ml
Original file line number Diff line number Diff line change
Expand Up @@ -994,13 +994,14 @@ let rec translate_expr ~source n =
match List.filter (fun c -> c.ntype <> "comment") n.children with
| [ inner ] -> Printf.sprintf "(%s)" (translate_expr ~source inner)
| _ -> todo_expr ~source n)
| "sequence_expression" | "block" -> (
| "pipe_expression" -> translate_pipe ~source n
| "if_expression" -> translate_if ~source n
| "block" -> translate_block ~source n
| "sequence_expression" -> (
match List.filter (fun c -> c.ntype <> "comment") n.children with
| [] -> "()"
| [ single ] -> translate_expr ~source single
| many ->
Printf.sprintf "{ %s }"
(String.concat "; " (List.map (translate_expr ~source) many)))
| _ -> translate_block ~source n)
| "ternary_expression" -> (
match
( child_with_field "condition" n,
Expand Down Expand Up @@ -1089,6 +1090,91 @@ and translate_switch ~source sw =
in
Printf.sprintf "match %s {\n%s\n}" scrutinee (String.concat "\n" arms)

(* ReScript pipe-first: `a -> f(b)` desugars to `f(a, b)`, `a -> f` to `f(a)`.
Pipes are left-nested so chains fall out of the recursion on [left]. *)
and translate_pipe ~source n =
match List.filter (fun c -> c.ntype <> "comment") n.children with
| [ left; right ] -> (
let lhs = translate_expr ~source left in
match right.ntype with
| "call_expression" ->
let fn =
match child_with_field "function" right with
| Some f -> translate_expr ~source f
| None -> "todo_fn"
in
let rest =
match child_with_field "arguments" right with
| None -> []
| Some a ->
List.filter_map
(fun c ->
match c.ntype with
| "type_annotation" -> None
| "labeled_argument" ->
Some (translate_labeled_arg ~source c)
| _ -> Some (translate_expr ~source c))
a.children
in
Printf.sprintf "%s(%s)" fn (String.concat ", " (lhs :: rest))
| _ -> Printf.sprintf "%s(%s)" (translate_expr ~source right) lhs)
| _ -> todo_expr ~source n

and translate_if ~source n =
(* positional children: condition, then-block, optional else_clause. *)
match List.filter (fun c -> c.ntype <> "comment") n.children with
| cond :: then_blk :: rest ->
let else_part =
match rest with
| ec :: _ when ec.ntype = "else_clause" -> (
match List.filter (fun c -> c.ntype <> "comment") ec.children with
| [ b ] -> " else " ^ translate_as_block ~source b
| _ -> "")
| _ -> ""
in
Printf.sprintf "if %s %s%s" (translate_expr ~source cond)
(translate_as_block ~source then_blk) else_part
| _ -> todo_expr ~source n

(* Render a node as an AffineScript braced block (if/else branches require it). *)
and translate_as_block ~source n =
match n.ntype with
| "block" -> translate_block ~source n
| "if_expression" -> translate_if ~source n (* else-if chain *)
| _ -> Printf.sprintf "{ %s }" (translate_expr ~source n)

and translate_block ~source n =
Printf.sprintf "{ %s }" (translate_block_inner ~source n)

(* The statements of a block, `;`-joined, WITHOUT the surrounding braces (so a
function body can place them directly inside the `fn { … }`). *)
and translate_block_inner ~source n =
String.concat "; "
(List.filter_map
(fun c ->
match c.ntype with
| "comment" -> None
| "let_declaration" -> Some (translate_block_let ~source c)
| _ -> Some (translate_expr ~source c))
n.children)

and translate_block_let ~source ld =
let one lb =
let name =
match child_with_field "pattern" lb with
| Some p -> translate_pattern ~source p
| None -> "_"
in
let v =
match child_with_field "body" lb with
| Some b -> translate_expr ~source b
| None -> "()"
in
Printf.sprintf "let %s = %s" name v
in
String.concat "; "
(List.map one (List.filter (fun c -> c.ntype = "let_binding") ld.children))

let translate_param ~source p =
match p.ntype with
| "value_identifier" -> node_text ~source p ^ ": _"
Expand Down Expand Up @@ -1119,6 +1205,8 @@ let partial_function ~source ~name fn =
in
let body =
match child_with_field "body" fn with
(* a block body's statements go directly inside the fn braces (no nesting) *)
| Some b when b.ntype = "block" -> translate_block_inner ~source b
| Some b -> translate_expr ~source b
| None -> "()"
in
Expand Down
Loading