Skip to content

Commit 3ccb4d8

Browse files
hyperpolymathclaude
andcommitted
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>
1 parent 18911aa commit 3ccb4d8

11 files changed

Lines changed: 27 additions & 17 deletions

File tree

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/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 -> ","

test/e2e/fixtures/row_polymorphism.affine

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

1818
fn mk_point(x: Int, y: Int) -> Point2D {
19-
{ x: x, y: y }
19+
#{ x: x, y: y }
2020
}

test/e2e/fixtures/type_decls.affine

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ enum Result[T, E] {
2727
}
2828

2929
fn make_point(x: Int, y: Int) -> Point {
30-
{ x: x, y: y }
30+
#{ x: x, y: y }
3131
}
3232

3333
fn origin() -> Point {
34-
{ x: 0, y: 0 }
34+
#{ x: 0, y: 0 }
3535
}

0 commit comments

Comments
 (0)