From f51504ddf3dfe4cf7241540d966ad836270a8c19 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 26 May 2026 09:36:16 +0100 Subject: [PATCH] feat(parser): record-update spread at start `#{ ..base, f: v }` (Refs gitbot-fleet#148) Adds the Rust-style record-update form to `expr_record_body`: a leading `..spread` followed by COMMA and one or more fields, all inside `#{ ... }`. The existing grammar accepted: - empty record `#{}` - spread-only `#{ ..s }` - fields with optional trailing spread `#{ f: v, ..s }` But the Rust-style `Record #{ ..base, override: x }` (spread captures the source-of-defaults, subsequent fields override) was a parse error. Required by sustainabot hand-port (gitbot-fleet#148): - `Model #{ ..model, totalProcessed: n }` in Oikos.affine - `Router #{ ..router, routes: rs }` in Router.affine LR(1)-safe: after parsing `expr_record_spread`, the lookahead is either RBRACE (the existing spread-only rule reduces) or COMMA (this new rule shifts); the choice is unambiguous on one-token lookahead. Parser builds with 21 shift/reduce + 1 reduce/reduce, identical to the pre-patch baseline. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/parser.mly | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/parser.mly b/lib/parser.mly index ef1b39e6..c83d08fa 100644 --- a/lib/parser.mly +++ b/lib/parser.mly @@ -928,6 +928,18 @@ expr_record_body: (* spread-only: { ..var } or { ..expr } *) | sp = expr_record_spread { ([], Some sp) } + (* Rust-style record-update: spread first, then comma-separated fields: + `Record #{ ..base, field: x, other: y }`. Required by sustainabot + hand-port (gitbot-fleet#148) — `Model #{ ..model, totalProcessed: n }` + in Oikos.affine and `Router #{ ..router, routes: rs }` in Router.affine. + The leading spread captures the source-of-defaults; subsequent fields + override. Mirrors the existing trailing-spread form below (`{ f: v, + ..s }`) but in the opposite order. LR(1)-safe: after parsing + `expr_record_spread`, the lookahead is either RBRACE (existing + spread-only rule already reduces) or COMMA (this rule shifts); + the choice is unambiguous on one-token lookahead. Added 2026-05-26. *) + | sp = expr_record_spread COMMA field = record_field rest = expr_record_rest + { (field :: fst rest, Some sp) } (* field possibly followed by more: { f: v, ... } *) | field = record_field rest = expr_record_rest { (field :: fst rest, snd rest) }