Skip to content

Commit d22be56

Browse files
hyperpolymathclaude
andcommitted
feat(grammar)!: Rust-like record syntax — bare { = block, records use #{ }
BREAKING — do NOT merge standalone; lands atomically with the codemod migration of all .affine record literals (epic affinescript#218). `{` in expression position is now ALWAYS a block. Record/struct literals use the `#{ … }` sigil — both anonymous (`#{ x: 1 }`) and typed (`Foo #{ x: 1 }`). Eliminates the block-vs-record-literal ambiguity (#215 families C+D) structurally, plus the Rust struct-literal-in-if/match-scrutinee hazard. Record patterns unaffected (no block alternative in pattern position). HASH_LBRACE token (token.ml) + `#{` lexer rule (lexer.ml, sedlex multi-char, ordered) + both ExprRecord productions rerouted (parser.mly). Conflicts 72→68 S/R, 10→7 R/R; build clean. Refs #218 #215 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a45b021 commit d22be56

3 files changed

Lines changed: 17 additions & 9 deletions

File tree

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

0 commit comments

Comments
 (0)