diff --git a/conformance/valid/011_rows.affine b/conformance/valid/011_rows.affine index e4904db4..322af7d9 100644 --- a/conformance/valid/011_rows.affine +++ b/conformance/valid/011_rows.affine @@ -9,7 +9,7 @@ fn get_name[..r](entity: {name: String, ..r}) -> String { } fn with_id[..r](record: {..r}, id: Int) -> {id: Int, ..r} { - { id: id, ..record } + #{ id: id, ..record } } type HasPosition[..r] = {x: Int, y: Int, ..r} diff --git a/examples/comprehensive_test.affine b/examples/comprehensive_test.affine index 99556c84..80448d04 100644 --- a/examples/comprehensive_test.affine +++ b/examples/comprehensive_test.affine @@ -12,7 +12,7 @@ enum Shape { } fn vec2_add(a: Vec2, b: Vec2) -> Vec2 { - { x: a.x + b.x, y: a.y + b.y } + #{ x: a.x + b.x, y: a.y + b.y } } fn area(s: Shape) -> Int { @@ -30,7 +30,7 @@ fn larger_area(s1: Shape, s2: Shape) -> Int = max(area(s1), area(s2)); fn main() -> () { let circle = Circle(5); - let rect = Rect({ x: 3, y: 4 }); + let rect = Rect(#{ x: 3, y: 4 }); let result = larger_area(circle, rect); (); } diff --git a/examples/rows.affine b/examples/rows.affine index 54ac370e..2f76f3d1 100644 --- a/examples/rows.affine +++ b/examples/rows.affine @@ -15,5 +15,5 @@ struct Point3D { fn getX(p: Point2D) -> Int = p.x; fn mk_point(x: Int, y: Int) -> Point2D { - { x: x, y: y } + #{ x: x, y: y } } diff --git a/examples/typecheck_complete_test.affine b/examples/typecheck_complete_test.affine index 2b163163..7e141091 100644 --- a/examples/typecheck_complete_test.affine +++ b/examples/typecheck_complete_test.affine @@ -15,11 +15,11 @@ enum Color { } fn make_point(x: Int, y: Int) -> Point { - { x: x, y: y } + #{ x: x, y: y } } fn origin() -> Point { - { x: 0, y: 0 } + #{ x: 0, y: 0 } } fn mutate_counter() -> Int { diff --git a/lib/lexer.ml b/lib/lexer.ml index cc4f1207..654aa8de 100644 --- a/lib/lexer.ml +++ b/lib/lexer.ml @@ -165,6 +165,9 @@ let rec token state buf = | "->" -> ARROW | "=>" -> FAT_ARROW | "::" -> COLONCOLON + (* Record-literal opener (affinescript#215): `#{` is the unambiguous + record/struct-literal sigil; bare `{` is always a block. *) + | "#{" -> HASH_LBRACE (* Row variable "..name" — must come before ".." so sedlex prefers the longer match *) | "..", lower_ident -> let s = Sedlexing.Utf8.lexeme buf in diff --git a/lib/parse.ml b/lib/parse.ml index f327389f..9ecd93b1 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -105,6 +105,7 @@ let next_token state () = | Token.LPAREN -> Parser.LPAREN | Token.RPAREN -> Parser.RPAREN | Token.LBRACE -> Parser.LBRACE + | Token.HASH_LBRACE -> Parser.HASH_LBRACE | Token.RBRACE -> Parser.RBRACE | Token.LBRACKET -> Parser.LBRACKET | Token.RBRACKET -> Parser.RBRACKET diff --git a/lib/parse_driver.ml b/lib/parse_driver.ml index 806d35b1..7cad1247 100644 --- a/lib/parse_driver.ml +++ b/lib/parse_driver.ml @@ -76,6 +76,7 @@ let to_menhir_token (tok : Token.t) : Parser.token = | Token.LPAREN -> Parser.LPAREN | Token.RPAREN -> Parser.RPAREN | Token.LBRACE -> Parser.LBRACE + | Token.HASH_LBRACE -> Parser.HASH_LBRACE | Token.RBRACE -> Parser.RBRACE | Token.LBRACKET -> Parser.LBRACKET | Token.RBRACKET -> Parser.RBRACKET diff --git a/lib/parser.mly b/lib/parser.mly index f020126c..5017755e 100644 --- a/lib/parser.mly +++ b/lib/parser.mly @@ -66,6 +66,7 @@ let rec effect_union_of_list = function /* Punctuation */ %token LPAREN RPAREN LBRACE RBRACE LBRACKET RBRACKET +%token HASH_LBRACE /* `#{` record-literal opener (affinescript#215) */ %token COMMA SEMICOLON COLON COLONCOLON DOT DOTDOT %token ARROW FAT_ARROW PIPE AT UNDERSCORE BACKSLASH QUESTION @@ -768,10 +769,11 @@ expr_primary: ordinary parameter binding named "self". */ | SELF_KW { ExprVar (mk_ident "self" $startpos $endpos) } | name = lower_ident { ExprVar (mk_ident name $startpos $endpos) } - /* Struct literal: `Point { x: v, y: w }`. Must come before the plain - upper_ident production so Menhir shifts LBRACE rather than reducing - upper_ident to ExprVar when the next token is LBRACE. */ - | _ty = upper_ident LBRACE b = expr_record_body RBRACE + /* Struct literal: `Point #{ x: v, y: w }` (affinescript#215). The `#{` + sigil makes this unambiguous against a bare block and removes the + Rust-style struct-literal-in-`if`/`match`-scrutinee hazard entirely; + no production-ordering hack needed any more. */ + | _ty = upper_ident HASH_LBRACE b = expr_record_body RBRACE { ExprRecord { er_fields = fst b; er_spread = snd b } } | name = upper_ident { ExprVar (mk_ident name $startpos $endpos) } | ty = upper_ident COLONCOLON variant = upper_ident @@ -787,11 +789,12 @@ expr_primary: /* Arrays */ | LBRACKET es = separated_list(COMMA, expr) RBRACKET { ExprArray es } - /* Records — use a recursive rule (expr_record_body / expr_record_rest) to - avoid the LALR(1) greedy-separator conflict that arises when a ROW_VAR - spread like `..record` follows a COMMA that `separated_list` has already - consumed expecting another record_field. */ - | LBRACE b = expr_record_body RBRACE + /* Anonymous record `#{ f: v, ..spread }` (affinescript#215). The `#{` + sigil removes the entire block-vs-record-literal ambiguity (family + C+D) by construction — bare `{` is now unconditionally a block. + expr_record_body / expr_record_rest stay recursive to avoid the + ROW_VAR greedy-separator conflict on `..spread` after a COMMA. */ + | HASH_LBRACE b = expr_record_body RBRACE { ExprRecord { er_fields = fst b; er_spread = snd b } } /* Block */ diff --git a/lib/token.ml b/lib/token.ml index 993e69b7..a8b71f21 100644 --- a/lib/token.ml +++ b/lib/token.ml @@ -73,6 +73,7 @@ type t = | RPAREN | LBRACE | RBRACE + | HASH_LBRACE (** #{ — record-literal opener (affinescript#215) *) | LBRACKET | RBRACKET | COMMA @@ -187,6 +188,7 @@ let to_string = function | RPAREN -> ")" | LBRACE -> "{" | RBRACE -> "}" + | HASH_LBRACE -> "#{" | LBRACKET -> "[" | RBRACKET -> "]" | COMMA -> "," diff --git a/stdlib/testing.affine b/stdlib/testing.affine index 30b8b5ec..4621cb97 100644 --- a/stdlib/testing.affine +++ b/stdlib/testing.affine @@ -303,7 +303,7 @@ fn bench(f: () -> (), iterations: Int) -> BenchResult { } let tot = time_now() - start; - { + #{ iterations: iterations, total_time: tot, avg_time: tot / float(iterations) diff --git a/test/e2e/fixtures/counter.affine b/test/e2e/fixtures/counter.affine index e6f7219b..26a3160f 100644 --- a/test/e2e/fixtures/counter.affine +++ b/test/e2e/fixtures/counter.affine @@ -68,7 +68,7 @@ fn counter_subs(model: Int) -> String { // Wire up the TEA runtime and hand off to the interpreter loop. fn main() -> () { - tea_run({ + tea_run(#{ init: counter_init, update: counter_update, view: counter_view, diff --git a/test/e2e/fixtures/full_pipeline.affine b/test/e2e/fixtures/full_pipeline.affine index 118608ba..592772ab 100644 --- a/test/e2e/fixtures/full_pipeline.affine +++ b/test/e2e/fixtures/full_pipeline.affine @@ -13,7 +13,7 @@ enum Shape { } fn vec2_add(a: Vec2, b: Vec2) -> Vec2 { - { x: a.x + b.x, y: a.y + b.y } + #{ x: a.x + b.x, y: a.y + b.y } } fn area(s: Shape) -> Int { @@ -31,7 +31,7 @@ fn larger_area(s1: Shape, s2: Shape) -> Int = max(area(s1), area(s2)); fn main() -> () { let circle = Circle(5); - let rect = Rect({ x: 3, y: 4 }); + let rect = Rect(#{ x: 3, y: 4 }); let result = larger_area(circle, rect); (); } diff --git a/test/e2e/fixtures/row_polymorphism.affine b/test/e2e/fixtures/row_polymorphism.affine index 6a2b2bff..c84209e6 100644 --- a/test/e2e/fixtures/row_polymorphism.affine +++ b/test/e2e/fixtures/row_polymorphism.affine @@ -16,5 +16,5 @@ struct Point3D { fn getX(p: Point2D) -> Int = p.x; fn mk_point(x: Int, y: Int) -> Point2D { - { x: x, y: y } + #{ x: x, y: y } } diff --git a/test/e2e/fixtures/titlescreen.affine b/test/e2e/fixtures/titlescreen.affine index 66483fcc..873a6e33 100644 --- a/test/e2e/fixtures/titlescreen.affine +++ b/test/e2e/fixtures/titlescreen.affine @@ -44,7 +44,7 @@ struct TitleModel { // ── init ───────────────────────────────────────────────────────────────────── fn title_init() -> TitleModel { - { + #{ screen_w: 1280, screen_h: 720, bgm_playing: 0, @@ -58,10 +58,10 @@ fn title_init() -> TitleModel { fn title_update(msg: TitleMsg, model: TitleModel) -> TitleModel { match msg { - NewGame => { screen_w: model.screen_w, screen_h: model.screen_h, bgm_playing: model.bgm_playing, selected: "new_game" }, - LoadGame => { screen_w: model.screen_w, screen_h: model.screen_h, bgm_playing: model.bgm_playing, selected: "load_game" }, - Settings => { screen_w: model.screen_w, screen_h: model.screen_h, bgm_playing: model.bgm_playing, selected: "settings" }, - Credits => { screen_w: model.screen_w, screen_h: model.screen_h, bgm_playing: model.bgm_playing, selected: "credits" } + NewGame => #{ screen_w: model.screen_w, screen_h: model.screen_h, bgm_playing: model.bgm_playing, selected: "new_game" }, + LoadGame => #{ screen_w: model.screen_w, screen_h: model.screen_h, bgm_playing: model.bgm_playing, selected: "load_game" }, + Settings => #{ screen_w: model.screen_w, screen_h: model.screen_h, bgm_playing: model.bgm_playing, selected: "settings" }, + Credits => #{ screen_w: model.screen_w, screen_h: model.screen_h, bgm_playing: model.bgm_playing, selected: "credits" } } } @@ -84,7 +84,7 @@ fn title_subs(model: TitleModel) -> String { // ── main ────────────────────────────────────────────────────────────────────── fn main() -> () { - tea_run({ + tea_run(#{ init: title_init, update: title_update, view: title_view, diff --git a/test/e2e/fixtures/type_decls.affine b/test/e2e/fixtures/type_decls.affine index 4f65ed90..6c8e1a9d 100644 --- a/test/e2e/fixtures/type_decls.affine +++ b/test/e2e/fixtures/type_decls.affine @@ -27,9 +27,9 @@ enum Result[T, E] { } fn make_point(x: Int, y: Int) -> Point { - { x: x, y: y } + #{ x: x, y: y } } fn origin() -> Point { - { x: 0, y: 0 } + #{ x: 0, y: 0 } } diff --git a/test/golden/rows.affine b/test/golden/rows.affine index bacbcaba..f22574f1 100644 --- a/test/golden/rows.affine +++ b/test/golden/rows.affine @@ -4,5 +4,5 @@ fn getX[..r](p: {x: Int, ..r}) -> Int { } fn addY[..r](p: {..r}) -> {y: Int, ..r} { - {y: 0, ..p} + #{y: 0, ..p} }