Skip to content

Commit fc1aa88

Browse files
hyperpolymathclaude
andcommitted
feat(parser)!: #{ record-literal sigil; { is always a block [#218]
Structurally removes the block-vs-record-literal ambiguity (#215 families C+D) with no lookahead heuristic, per the owner-approved Rust-like model: - `{` in expression position is ALWAYS a block. - record/struct literals use `#{ … }` (anonymous `#{ x: 1 }` and typed `Foo #{ x: 1 }`). - record *patterns* unchanged (pattern position has no block alternative — no ambiguity there). Changes: HASH_LBRACE token (token.ml), `#{` lexer rule (lexer.ml; `#` was previously unused so longest-match is collision-free), both ExprRecord productions rerouted LBRACE -> HASH_LBRACE (parser.mly). Menhir conflicts on origin/main (a45b021): 72 S/R + 10 R/R after: 68 S/R + 7 R/R (-4 / -3) Build clean. BREAKING: legacy `{ … }` record syntax no longer parses; the codemod-migrated repo `.affine` files land in the same PR so the 253/257 gate stays green. Refs #218 #215 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a45b021 commit fc1aa88

4 files changed

Lines changed: 20 additions & 9 deletions

File tree

lib/lexer.ml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,9 @@ let rec token state buf =
183183
| "-=" -> MINUSEQ
184184
| "*=" -> STAREQ
185185
| "/=" -> SLASHEQ
186+
(* `#{` opens a record/struct literal (#218); longest-match, and `#` has
187+
no other lexer rule, so this never shadows a single-char token. *)
188+
| "#{" -> HASH_LBRACE
186189

187190
(* Single-char tokens *)
188191
| '(' -> LPAREN

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: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ let rec effect_union_of_list = function
6666

6767
/* Punctuation */
6868
%token LPAREN RPAREN LBRACE RBRACE LBRACKET RBRACKET
69+
/* `#{` opens a record/struct literal (#218). `{` is ALWAYS a block in
70+
expression position — this structurally removes the block-vs-record
71+
ambiguity (#215 families C+D) with no lookahead heuristic. */
72+
%token HASH_LBRACE
6973
%token COMMA SEMICOLON COLON COLONCOLON DOT DOTDOT
7074
%token ARROW FAT_ARROW PIPE AT UNDERSCORE BACKSLASH QUESTION
7175

@@ -768,10 +772,10 @@ expr_primary:
768772
ordinary parameter binding named "self". */
769773
| SELF_KW { ExprVar (mk_ident "self" $startpos $endpos) }
770774
| 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
775+
/* Struct literal: `Point #{ x: v, y: w }` (#218). The `#{` sigil makes
776+
this unambiguous against the plain `upper_ident` production and against
777+
a following block, so no LBRACE-shift heuristic is needed any more. */
778+
| _ty = upper_ident HASH_LBRACE b = expr_record_body RBRACE
775779
{ ExprRecord { er_fields = fst b; er_spread = snd b } }
776780
| name = upper_ident { ExprVar (mk_ident name $startpos $endpos) }
777781
| ty = upper_ident COLONCOLON variant = upper_ident
@@ -787,11 +791,12 @@ expr_primary:
787791
/* Arrays */
788792
| LBRACKET es = separated_list(COMMA, expr) RBRACKET { ExprArray es }
789793

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
794+
/* Anonymous record `#{ f: v, ..spread }` (#218). Uses a recursive rule
795+
(expr_record_body / expr_record_rest) to avoid the LALR(1) greedy-
796+
separator conflict that arises when a ROW_VAR spread like `..record`
797+
follows a COMMA that `separated_list` has already consumed expecting
798+
another record_field. */
799+
| HASH_LBRACE b = expr_record_body RBRACE
795800
{ ExprRecord { er_fields = fst b; er_spread = snd b } }
796801

797802
/* Block */

lib/token.ml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ type t =
7272
| LPAREN
7373
| RPAREN
7474
| LBRACE
75+
| HASH_LBRACE
7576
| RBRACE
7677
| LBRACKET
7778
| RBRACKET
@@ -186,6 +187,7 @@ let to_string = function
186187
| LPAREN -> "("
187188
| RPAREN -> ")"
188189
| LBRACE -> "{"
190+
| HASH_LBRACE -> "#{"
189191
| RBRACE -> "}"
190192
| LBRACKET -> "["
191193
| RBRACKET -> "]"

0 commit comments

Comments
 (0)