@@ -194,7 +194,7 @@ top_level:
194194extern_fn_decl :
195195 | vis = visibility ? EXTERN FN name = ident
196196 type_params = type_params ?
197- LPAREN params = separated_list ( COMMA , param ) RPAREN
197+ LPAREN params = param_list_trailing_comma RPAREN
198198 ret = return_type ?
199199 SEMICOLON
200200 { { fd_vis = Option. value vis ~default: Private ;
@@ -226,7 +226,7 @@ const_decl:
226226fn_decl:
227227 | vis = visibility ? total = TOTAL ? FN name = ident
228228 type_params = type_params ?
229- LPAREN params = separated_list ( COMMA , param ) RPAREN
229+ LPAREN params = param_list_trailing_comma RPAREN
230230 ret = return_type ?
231231 where_clause = where_clause ?
232232 body = fn_body
@@ -382,6 +382,36 @@ ownership:
382382 | REF { Ref }
383383 | MUT { Mut }
384384
385+ /* `param_list_trailing_comma ` — comma - separated param list that also
386+ accepts an optional trailing comma before the closing RPAREN , per the
387+ Rust - likes estate convention . Hand - rolled right - recursive form
388+ (rather than `separated_list (COMMA , param ) COMMA ?`) so the trailing
389+ COMMA is absorbed inside the recursion : after each `param `, the LR (1 )
390+ choice on COMMA is unambiguous (shift into the recursive tail , whose
391+ body may be empty when RPAREN follows ). Adds no new s / r or r / r
392+ conflicts beyond the grammar 's existing baseline . */
393+ param_list_trailing_comma:
394+ | /* empty */ { [] }
395+ | p = param { [p] }
396+ | p = param COMMA rest = param_list_trailing_comma { p :: rest }
397+
398+ /* `expr_list_trailing_comma ` — comma - separated expression list that also
399+ accepts an optional trailing comma before the closing delimiter . Used by
400+ array literals (`[a, b , c ,]`) and function - call argument lists
401+ (`f (a , b , c ,)`). Same right - recursive shape as
402+ `param_list_trailing_comma ` (above ): the COMMA - after- `expr ` is absorbed
403+ inside the recursive tail , whose body may be empty when the closing
404+ token (RBRACKET / RPAREN ) follows . Conflict - neutral: the existing rules
405+ used `separated_list (COMMA , expr )`, which already inspects COMMA in the
406+ same lookahead position ; only the post- final- COMMA branch is new and
407+ has no other production to compete with . Added 2026-05-26 (Refs
408+ hyperpolymath/ gitbot- fleet#148 : sustainabot hand- port —
409+ `json ::encode_object([(" k" , v),])` and `f (..., last_arg,)`). */
410+ expr_list_trailing_comma :
411+ | /* empty */ { [] }
412+ | e = expr { [e] }
413+ | e = expr COMMA rest = expr_list_trailing_comma { e :: rest }
414+
385415where_clause:
386416 | WHERE constraints = separated_nonempty_list (COMMA , constraint_ ) { constraints }
387417
@@ -593,7 +623,7 @@ effect_decl:
593623
594624effect_op_decl:
595625 (* Type parameters on effect operations are allowed: `fn await[T](promise: Promise[T]) -> T;` *)
596- | FN name = ident _type_params = type_params ? LPAREN params = separated_list ( COMMA , param ) RPAREN ret = return_type ? SEMICOLON
626+ | FN name = ident _type_params = type_params ? LPAREN params = param_list_trailing_comma RPAREN ret = return_type ? SEMICOLON
597627 { { eod_name = name;
598628 eod_params = params;
599629 eod_ret_ty = fst (Option. value ret ~default: (None , None )) } }
@@ -659,7 +689,7 @@ type_default:
659689fn_sig:
660690 | vis = visibility ? FN name = ident
661691 type_params = type_params ?
662- LPAREN params = separated_list ( COMMA , param ) RPAREN
692+ LPAREN params = param_list_trailing_comma RPAREN
663693 ret = return_type ?
664694 { { fs_vis = Option. value vis ~default: Private ;
665695 fs_name = name;
@@ -796,7 +826,7 @@ expr_postfix:
796826 { ExprApp (ExprVar (mk_ident " slice" $ startpos $ endpos),
797827 [e; ExprLit (LitInt (0 , mk_span $ startpos $ endpos));
798828 ExprApp (ExprVar (mk_ident " len" $ startpos $ endpos), [e])]) }
799- | e = expr_postfix LPAREN args = separated_list ( COMMA , expr ) RPAREN { ExprApp (e, args) }
829+ | e = expr_postfix LPAREN args = expr_list_trailing_comma RPAREN { ExprApp (e, args) }
800830 | e = expr_postfix BACKSLASH field = field_name { ExprRowRestrict (e, field) }
801831 | e = expr_postfix QUESTION { ExprTry { et_body = { blk_stmts = [] ; blk_expr = Some e };
802832 et_catch = None ; et_finally = None } }
@@ -849,7 +879,7 @@ expr_primary:
849879 { ExprTuple (e :: es) }
850880
851881 /* Arrays */
852- | LBRACKET es = separated_list( COMMA , expr) RBRACKET { ExprArray es }
882+ | LBRACKET es = expr_list_trailing_comma RBRACKET { ExprArray es }
853883
854884 /* Anonymous record `#{ f : v , ..spread }` (affinescript#215 ). The `#{`
855885 sigil removes the entire block- vs- record- literal ambiguity (family
@@ -899,6 +929,22 @@ expr_primary:
899929 { ExprLambda { elam_params = params; elam_ret_ty = None ; elam_body = body } }
900930 | FN LPAREN params = separated_list(COMMA , lambda_param) RPAREN ARROW ret = type_expr body = block
901931 { ExprLambda { elam_params = params; elam_ret_ty = Some ret; elam_body = ExprBlock body } }
932+ /* Effect - annotated lambda: `fn (params) - {eff}-> RetTy { body }`.
933+ Mirrors the type - position `fn (...) - {E }-> T ` form already accepted in
934+ `type_expr_primary `. ExprLambda has no effect- row slot (lambda effects
935+ are inferred from the body by the typechecker), so the parsed effect
936+ row is dropped here. The annotation is a surface affordance for the
937+ user (and the AffineScript-aware formatter ), matching the symmetric
938+ type -position syntax that is already part of the language . The MINUS
939+ LBRACE lookahead after RPAREN unambiguously distinguishes this from
940+ the unannotated `-> RetTy` form above (no other lambda production
941+ has that pair there ). Added 2026-05-26 (Refs hyperpolymath /gitbot -
942+ fleet #148 : sustainabot hand -port — `fn () -{IO} -> Json { payload } `
943+ in Oikos. affine , tea /Cmd. affine , tea /Runtime. affine ). */
944+ | FN LPAREN params = separated_list(COMMA , lambda_param) RPAREN
945+ MINUS LBRACE _eff = effect_expr RBRACE ARROW
946+ ret = type_expr body = block
947+ { ExprLambda { elam_params = params; elam_ret_ty = Some ret; elam_body = ExprBlock body } }
902948
903949 /* `return `/ `resume ` moved to expr_assign (affinescript#215 family B ) */
904950
0 commit comments