Skip to content

Commit 78906f9

Browse files
feat(res-to-affine): #488 partial-port slice 2 — pipe desugaring + if/else + blocks (Refs #488) (#495)
#488 partial-port 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. walker gains translate_pipe / translate_if / translate_as_block / translate_block(_inner) / translate_block_let; partial_function unwraps block bodies. Array/record/object literals still become () /* TODO */ holes. Parses (no parse error); 30 walker tests. Refs #488.
1 parent c157a0f commit 78906f9

5 files changed

Lines changed: 149 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ this project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0
2020

2121
### Added
2222

23+
- 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)
2324
- 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)
2425
- 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)
2526
- 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)

tools/res-to-affine/README.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -222,16 +222,22 @@ AffineScript `fn` skeleton:
222222
| `let area = (w, h) => w *. h` | `fn area(w: _, h: _) -> _ { w * h }` |
223223
| `let classify = x => switch x { \| Some(n) => n + 1 \| None => 0 }` | `fn classify(x: _) -> _ { match x { Some(n) => n + 1, None => 0, } }` |
224224
| `let greet = name => "hi " ++ name` | `fn greet(name: _) -> _ { "hi " ++ name }` |
225+
| `let piped = x => x->doStuff(1)` | `fn piped(x: _) -> _ { doStuff(x, 1) }` |
226+
| `let chain = x => x->f->g(2)` | `fn chain(x: _) -> _ { g(f(x), 2) }` |
227+
| `let clamp = x => if x > 0 { x } else { 0 }` | `fn clamp(x: _) -> _ { if x > 0 { x } else { 0 } }` |
228+
| `let scaled = x => { let y = x + 1; y * 2 }` | `fn scaled(x: _) -> _ { let y = x + 1; y * 2 }` |
225229

226230
It translates literals, identifiers, calls, binary operators (normalising
227231
ReScript's float ops `+.`/`*.``+`/`*` and `===`/`!==``==`/`!=`), string
228-
concat `++`, member/qualified access, ternaries, and `switch``match` with
229-
variant/tuple/literal patterns. Anything else (pipe-first `->`, records, etc.)
230-
becomes a `() /* TODO */` hole. The output is a partial port to finish by
231-
hand: it **parses** but is not expected to type-check (verified — the
232-
generated skeletons reach resolution/type-checking without a parse error).
233-
First slice; the next steps (pipe desugaring `a->f(b)``f(a, b)`, `if`/block
234-
bodies, combining with `--translate`) continue under #488.
232+
concat `++`, member/qualified access, ternaries, **`if`/`else`**, **blocks
233+
with `let` statements**, **pipe-first `->`** (`a->f(b)``f(a, b)`, chained
234+
left-to-right), and `switch``match` with variant/tuple/literal patterns.
235+
Anything else (records, arrays, objects, …) becomes a `() /* TODO */` hole.
236+
The output is a partial port to finish by hand: it **parses** but is not
237+
expected to type-check (verified — the generated skeletons reach
238+
resolution/type-checking without a parse error). Continuing under #488:
239+
record/array/object literals, labelled args, combining `--partial` with
240+
`--translate`, and module-qualified-reference *resolution*.
235241

236242
## Corpus run
237243

tools/res-to-affine/test/fixtures/partial1.res

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// SPDX-License-Identifier: MIT
22
// #488 partial-port fixture: module-top-level functions -> `fn` skeletons
3-
// with switch->match and best-effort expression translation. Output is NOT
4-
// expected to type-check; it must parse, with un-translatable forms as
5-
// `() /* TODO */` holes.
3+
// with switch->match, pipe desugaring, if/else, blocks, and best-effort
4+
// expression translation. Output is NOT expected to type-check; it must
5+
// parse, with un-translatable forms (e.g. an array literal) as TODO holes.
66

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

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

18-
// pipe-first has no AffineScript equivalent -> must become a TODO hole.
18+
// pipe-first desugars: x->doStuff(1) -> doStuff(x, 1)
1919
let piped = x => x->doStuff(1)
20+
21+
// pipe chain desugars left-to-right: x->f->g(2) -> g(f(x), 2)
22+
let chain = x => x->f->g(2)
23+
24+
// if/else
25+
let clamp = x => if x > 0 { x } else { 0 }
26+
27+
// block with a let statement
28+
let scaled = x => { let y = x + 1; y * 2 }
29+
30+
// array literal has no handler yet -> must become a TODO hole.
31+
let pair = x => [x, x]

tools/res-to-affine/test/test_walker.ml

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -401,8 +401,26 @@ let partial1_blob () =
401401
let test_partial_count () =
402402
skip_unless_ready ();
403403
Alcotest.(check int)
404-
"five module-top-level functions -> fn skeletons"
405-
5 (List.length (translate_partial1 ()))
404+
"nine module-top-level functions -> fn skeletons"
405+
9 (List.length (translate_partial1 ()))
406+
407+
let test_partial_pipe () =
408+
skip_unless_ready ();
409+
let blob = partial1_blob () in
410+
(* x->doStuff(1) -> doStuff(x, 1); chain x->f->g(2) -> g(f(x), 2) *)
411+
Alcotest.(check bool) "pipe-first desugars, including left-nested chains"
412+
true (contains blob "doStuff(x, 1)" && contains blob "g(f(x), 2)")
413+
414+
let test_partial_if () =
415+
skip_unless_ready ();
416+
Alcotest.(check bool) "if/else translated"
417+
true (contains (partial1_blob ()) "if x > 0 { x } else { 0 }")
418+
419+
let test_partial_block () =
420+
skip_unless_ready ();
421+
let blob = partial1_blob () in
422+
Alcotest.(check bool) "block body with a let statement translated"
423+
true (contains blob "let y = x + 1" && contains blob "y * 2")
406424

407425
let test_partial_switch_to_match () =
408426
skip_unless_ready ();
@@ -503,7 +521,7 @@ let () =
503521
] );
504522
( "walker-488-partial",
505523
[
506-
Alcotest.test_case "five functions -> fn skeletons"
524+
Alcotest.test_case "nine functions -> fn skeletons"
507525
`Quick test_partial_count;
508526
Alcotest.test_case "switch -> match + patterns + arm bodies"
509527
`Quick test_partial_switch_to_match;
@@ -513,5 +531,11 @@ let () =
513531
`Quick test_partial_concat_and_call;
514532
Alcotest.test_case "untranslatable form -> TODO hole"
515533
`Quick test_partial_todo_hole;
534+
Alcotest.test_case "pipe-first desugars (incl. chains)"
535+
`Quick test_partial_pipe;
536+
Alcotest.test_case "if/else translated"
537+
`Quick test_partial_if;
538+
Alcotest.test_case "block with let statement translated"
539+
`Quick test_partial_block;
516540
] );
517541
]

tools/res-to-affine/walker.ml

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -994,13 +994,14 @@ let rec translate_expr ~source n =
994994
match List.filter (fun c -> c.ntype <> "comment") n.children with
995995
| [ inner ] -> Printf.sprintf "(%s)" (translate_expr ~source inner)
996996
| _ -> todo_expr ~source n)
997-
| "sequence_expression" | "block" -> (
997+
| "pipe_expression" -> translate_pipe ~source n
998+
| "if_expression" -> translate_if ~source n
999+
| "block" -> translate_block ~source n
1000+
| "sequence_expression" -> (
9981001
match List.filter (fun c -> c.ntype <> "comment") n.children with
9991002
| [] -> "()"
10001003
| [ single ] -> translate_expr ~source single
1001-
| many ->
1002-
Printf.sprintf "{ %s }"
1003-
(String.concat "; " (List.map (translate_expr ~source) many)))
1004+
| _ -> translate_block ~source n)
10041005
| "ternary_expression" -> (
10051006
match
10061007
( child_with_field "condition" n,
@@ -1089,6 +1090,91 @@ and translate_switch ~source sw =
10891090
in
10901091
Printf.sprintf "match %s {\n%s\n}" scrutinee (String.concat "\n" arms)
10911092

1093+
(* ReScript pipe-first: `a -> f(b)` desugars to `f(a, b)`, `a -> f` to `f(a)`.
1094+
Pipes are left-nested so chains fall out of the recursion on [left]. *)
1095+
and translate_pipe ~source n =
1096+
match List.filter (fun c -> c.ntype <> "comment") n.children with
1097+
| [ left; right ] -> (
1098+
let lhs = translate_expr ~source left in
1099+
match right.ntype with
1100+
| "call_expression" ->
1101+
let fn =
1102+
match child_with_field "function" right with
1103+
| Some f -> translate_expr ~source f
1104+
| None -> "todo_fn"
1105+
in
1106+
let rest =
1107+
match child_with_field "arguments" right with
1108+
| None -> []
1109+
| Some a ->
1110+
List.filter_map
1111+
(fun c ->
1112+
match c.ntype with
1113+
| "type_annotation" -> None
1114+
| "labeled_argument" ->
1115+
Some (translate_labeled_arg ~source c)
1116+
| _ -> Some (translate_expr ~source c))
1117+
a.children
1118+
in
1119+
Printf.sprintf "%s(%s)" fn (String.concat ", " (lhs :: rest))
1120+
| _ -> Printf.sprintf "%s(%s)" (translate_expr ~source right) lhs)
1121+
| _ -> todo_expr ~source n
1122+
1123+
and translate_if ~source n =
1124+
(* positional children: condition, then-block, optional else_clause. *)
1125+
match List.filter (fun c -> c.ntype <> "comment") n.children with
1126+
| cond :: then_blk :: rest ->
1127+
let else_part =
1128+
match rest with
1129+
| ec :: _ when ec.ntype = "else_clause" -> (
1130+
match List.filter (fun c -> c.ntype <> "comment") ec.children with
1131+
| [ b ] -> " else " ^ translate_as_block ~source b
1132+
| _ -> "")
1133+
| _ -> ""
1134+
in
1135+
Printf.sprintf "if %s %s%s" (translate_expr ~source cond)
1136+
(translate_as_block ~source then_blk) else_part
1137+
| _ -> todo_expr ~source n
1138+
1139+
(* Render a node as an AffineScript braced block (if/else branches require it). *)
1140+
and translate_as_block ~source n =
1141+
match n.ntype with
1142+
| "block" -> translate_block ~source n
1143+
| "if_expression" -> translate_if ~source n (* else-if chain *)
1144+
| _ -> Printf.sprintf "{ %s }" (translate_expr ~source n)
1145+
1146+
and translate_block ~source n =
1147+
Printf.sprintf "{ %s }" (translate_block_inner ~source n)
1148+
1149+
(* The statements of a block, `;`-joined, WITHOUT the surrounding braces (so a
1150+
function body can place them directly inside the `fn { … }`). *)
1151+
and translate_block_inner ~source n =
1152+
String.concat "; "
1153+
(List.filter_map
1154+
(fun c ->
1155+
match c.ntype with
1156+
| "comment" -> None
1157+
| "let_declaration" -> Some (translate_block_let ~source c)
1158+
| _ -> Some (translate_expr ~source c))
1159+
n.children)
1160+
1161+
and translate_block_let ~source ld =
1162+
let one lb =
1163+
let name =
1164+
match child_with_field "pattern" lb with
1165+
| Some p -> translate_pattern ~source p
1166+
| None -> "_"
1167+
in
1168+
let v =
1169+
match child_with_field "body" lb with
1170+
| Some b -> translate_expr ~source b
1171+
| None -> "()"
1172+
in
1173+
Printf.sprintf "let %s = %s" name v
1174+
in
1175+
String.concat "; "
1176+
(List.map one (List.filter (fun c -> c.ntype = "let_binding") ld.children))
1177+
10921178
let translate_param ~source p =
10931179
match p.ntype with
10941180
| "value_identifier" -> node_text ~source p ^ ": _"
@@ -1119,6 +1205,8 @@ let partial_function ~source ~name fn =
11191205
in
11201206
let body =
11211207
match child_with_field "body" fn with
1208+
(* a block body's statements go directly inside the fn braces (no nesting) *)
1209+
| Some b when b.ntype = "block" -> translate_block_inner ~source b
11221210
| Some b -> translate_expr ~source b
11231211
| None -> "()"
11241212
in

0 commit comments

Comments
 (0)