Skip to content

Commit 8de3e0b

Browse files
feat!: Rust-like record syntax — bare { = block, records use #{ } (#218; resolves #215 C+D) (#222)
* wip(#218): Rust-like record syntax — grammar + token bridges + 6 fixtures ATOMIC PR IN PROGRESS — DO NOT MERGE (15/257 still failing). Contains: - Grammar: bare { = block, records #{ } (token.ml/lexer.ml/parser.mly, from stage-c/pc-brace-disambig). Conflicts 72->68 S/R, 10->7 R/R. - Token-bridge completeness: added HASH_LBRACE arm to BOTH lib/parse.ml AND lib/parse_driver.ml (same non-exhaustive-match class as #219 EXTERN — warning-8 demoted, Match_failure at runtime; these were the ONLY two Token->Parser bridges). - Migrated 6 regression .affine fixtures (expression-position record literals {..}->#{..}; type-position records unchanged). Progress: 21 -> 15 failures. Remaining 15 are inline AffineScript source embedded as string literals in the OCaml test suite (E2E TEA counter/titlescreen, LSP Phase B hover, full_pipeline, AOT test 17, etc.) — those test inputs still use old record syntax and need #{ migration in the test .ml fixtures, not .affine files. Refs #218 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(#218)!: complete Rust-like record migration — 257/257 green Migrated the remaining expression-position record literals to #{ } (compiler-as-oracle, iterated to green): - test/e2e/fixtures/counter.affine (tea_run arg record) - test/e2e/fixtures/full_pipeline.affine (fn-return + Rect(#{}) arg) - test/e2e/fixtures/titlescreen.affine (title_init body + 4 match-arm records + tea_run arg) - examples/comprehensive_test.affine (fn-return + Rect(#{}) arg) - stdlib/testing.affine (benchmark-result record — the ONE stdlib record literal; earlier survey missed it, parse-gated) Type-position records, struct/type declarations, blocks, and match-arm blocks left unchanged (different grammar rules — verified). Full dune gate now GREEN: 257/257, 0 failures. Conflicts 72->68 S/R, 10->7 R/R (residual families tracked on #215). Breaking syntax change — review-gated, do not auto-merge. Refs #218 #215 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 18911aa commit 8de3e0b

16 files changed

Lines changed: 39 additions & 29 deletions

conformance/valid/011_rows.affine

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ fn get_name[..r](entity: {name: String, ..r}) -> String {
99
}
1010

1111
fn with_id[..r](record: {..r}, id: Int) -> {id: Int, ..r} {
12-
{ id: id, ..record }
12+
#{ id: id, ..record }
1313
}
1414

1515
type HasPosition[..r] = {x: Int, y: Int, ..r}

examples/comprehensive_test.affine

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ enum Shape {
1212
}
1313

1414
fn vec2_add(a: Vec2, b: Vec2) -> Vec2 {
15-
{ x: a.x + b.x, y: a.y + b.y }
15+
#{ x: a.x + b.x, y: a.y + b.y }
1616
}
1717

1818
fn area(s: Shape) -> Int {
@@ -30,7 +30,7 @@ fn larger_area(s1: Shape, s2: Shape) -> Int = max(area(s1), area(s2));
3030

3131
fn main() -> () {
3232
let circle = Circle(5);
33-
let rect = Rect({ x: 3, y: 4 });
33+
let rect = Rect(#{ x: 3, y: 4 });
3434
let result = larger_area(circle, rect);
3535
();
3636
}

examples/rows.affine

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ struct Point3D {
1515
fn getX(p: Point2D) -> Int = p.x;
1616

1717
fn mk_point(x: Int, y: Int) -> Point2D {
18-
{ x: x, y: y }
18+
#{ x: x, y: y }
1919
}

examples/typecheck_complete_test.affine

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ enum Color {
1515
}
1616

1717
fn make_point(x: Int, y: Int) -> Point {
18-
{ x: x, y: y }
18+
#{ x: x, y: y }
1919
}
2020

2121
fn origin() -> Point {
22-
{ x: 0, y: 0 }
22+
#{ x: 0, y: 0 }
2323
}
2424

2525
fn mutate_counter() -> Int {

lib/lexer.ml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ let rec token state buf =
165165
| "->" -> ARROW
166166
| "=>" -> FAT_ARROW
167167
| "::" -> COLONCOLON
168+
(* Record-literal opener (affinescript#215): `#{` is the unambiguous
169+
record/struct-literal sigil; bare `{` is always a block. *)
170+
| "#{" -> HASH_LBRACE
168171
(* Row variable "..name" — must come before ".." so sedlex prefers the longer match *)
169172
| "..", lower_ident ->
170173
let s = Sedlexing.Utf8.lexeme buf in

lib/parse.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ let next_token state () =
105105
| Token.LPAREN -> Parser.LPAREN
106106
| Token.RPAREN -> Parser.RPAREN
107107
| Token.LBRACE -> Parser.LBRACE
108+
| Token.HASH_LBRACE -> Parser.HASH_LBRACE
108109
| Token.RBRACE -> Parser.RBRACE
109110
| Token.LBRACKET -> Parser.LBRACKET
110111
| Token.RBRACKET -> Parser.RBRACKET

lib/parse_driver.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ let to_menhir_token (tok : Token.t) : Parser.token =
7676
| Token.LPAREN -> Parser.LPAREN
7777
| Token.RPAREN -> Parser.RPAREN
7878
| Token.LBRACE -> Parser.LBRACE
79+
| Token.HASH_LBRACE -> Parser.HASH_LBRACE
7980
| Token.RBRACE -> Parser.RBRACE
8081
| Token.LBRACKET -> Parser.LBRACKET
8182
| Token.RBRACKET -> Parser.RBRACKET

lib/parser.mly

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ let rec effect_union_of_list = function
6666

6767
/* Punctuation */
6868
%token LPAREN RPAREN LBRACE RBRACE LBRACKET RBRACKET
69+
%token HASH_LBRACE /* `#{` record-literal opener (affinescript#215) */
6970
%token COMMA SEMICOLON COLON COLONCOLON DOT DOTDOT
7071
%token ARROW FAT_ARROW PIPE AT UNDERSCORE BACKSLASH QUESTION
7172

@@ -768,10 +769,11 @@ expr_primary:
768769
ordinary parameter binding named "self". */
769770
| SELF_KW { ExprVar (mk_ident "self" $startpos $endpos) }
770771
| name = lower_ident { ExprVar (mk_ident name $startpos $endpos) }
771-
/* Struct literal: `Point { x: v, y: w }`. Must come before the plain
772-
upper_ident production so Menhir shifts LBRACE rather than reducing
773-
upper_ident to ExprVar when the next token is LBRACE. */
774-
| _ty = upper_ident LBRACE b = expr_record_body RBRACE
772+
/* Struct literal: `Point #{ x: v, y: w }` (affinescript#215). The `#{`
773+
sigil makes this unambiguous against a bare block and removes the
774+
Rust-style struct-literal-in-`if`/`match`-scrutinee hazard entirely;
775+
no production-ordering hack needed any more. */
776+
| _ty = upper_ident HASH_LBRACE b = expr_record_body RBRACE
775777
{ ExprRecord { er_fields = fst b; er_spread = snd b } }
776778
| name = upper_ident { ExprVar (mk_ident name $startpos $endpos) }
777779
| ty = upper_ident COLONCOLON variant = upper_ident
@@ -787,11 +789,12 @@ expr_primary:
787789
/* Arrays */
788790
| LBRACKET es = separated_list(COMMA, expr) RBRACKET { ExprArray es }
789791

790-
/* Recordsuse a recursive rule (expr_record_body / expr_record_rest) to
791-
avoid the LALR(1) greedy-separator conflict that arises when a ROW_VAR
792-
spread like `..record` follows a COMMA that `separated_list` has already
793-
consumed expecting another record_field. */
794-
| LBRACE b = expr_record_body RBRACE
792+
/* Anonymous record `#{ f: v, ..spread }` (affinescript#215). The `#{`
793+
sigil removes the entire block-vs-record-literal ambiguity (family
794+
C+D) by construction — bare `{` is now unconditionally a block.
795+
expr_record_body / expr_record_rest stay recursive to avoid the
796+
ROW_VAR greedy-separator conflict on `..spread` after a COMMA. */
797+
| HASH_LBRACE b = expr_record_body RBRACE
795798
{ ExprRecord { er_fields = fst b; er_spread = snd b } }
796799

797800
/* Block */

lib/token.ml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ type t =
7373
| RPAREN
7474
| LBRACE
7575
| RBRACE
76+
| HASH_LBRACE (** #{ — record-literal opener (affinescript#215) *)
7677
| LBRACKET
7778
| RBRACKET
7879
| COMMA
@@ -187,6 +188,7 @@ let to_string = function
187188
| RPAREN -> ")"
188189
| LBRACE -> "{"
189190
| RBRACE -> "}"
191+
| HASH_LBRACE -> "#{"
190192
| LBRACKET -> "["
191193
| RBRACKET -> "]"
192194
| COMMA -> ","

stdlib/testing.affine

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ fn bench(f: () -> (), iterations: Int) -> BenchResult {
303303
}
304304

305305
let tot = time_now() - start;
306-
{
306+
#{
307307
iterations: iterations,
308308
total_time: tot,
309309
avg_time: tot / float(iterations)

0 commit comments

Comments
 (0)