Skip to content

Commit 9d8be1b

Browse files
hyperpolymathclaude
andcommitted
feat(ast/parser): add quantity slot to Let + ADR-007 hybrid surface syntax
Adds el_quantity to ExprLet and sl_quantity to StmtLet, then threads two surface forms through the parser per ADR-007 (META.a2ml): - Option C primary: @linear / @Erased / @unrestricted attribute prefix. Wired into let_decl, stmt_let, lambda_param, and param. - Option B sugar: :1 / :0 / :ω numeric annotation after the binder. Wired into let_decl and stmt_let only. Sugar form on params is deferred — would require repositioning the existing qty? slot. Both surface forms parse to the same internal slot. Unknown @-attribute names and integer literals outside {0, 1} are rejected at parse time via Parser_errors.Parse_action_error, a new shared exceptions module that bridges the parser.mly prologue (where Menhir doesn't expose user exceptions through .mli) to parse_driver without circular dependency. Adds parser_errors to the dune modules list. Mechanical pattern-update sweep across julia_codegen, desugar_traits, typecheck, and sexpr_dump for the new record fields. Build clean, test baseline unchanged at this point (this commit is semantically no-op for enforcement; the scaling logic and fixtures land in the next commit). Refs ADR-007, ADR-002. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 78de347 commit 9d8be1b

9 files changed

Lines changed: 139 additions & 15 deletions

File tree

lib/ast.ml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ type expr =
104104
| ExprVar of ident
105105
| ExprLet of {
106106
el_mut : bool;
107+
el_quantity : quantity option;
108+
(** QTT binder quantity, per ADR-002 / ADR-007.
109+
None means: defaults to QOmega (unrestricted), the
110+
unannotated case. The quantity scales the value context
111+
in the typing rule q·Γ₁ + Γ₂ ⊢ let x :^q = e1 in e2.
112+
Surface syntaxes that populate this:
113+
- @linear / @erased / @unrestricted (Option C, primary)
114+
- :1 / :0 / :ω (Option B, sugar) *)
107115
el_pat : pattern;
108116
el_ty : type_expr option;
109117
el_value : expr;
@@ -170,6 +178,10 @@ and block = {
170178
and stmt =
171179
| StmtLet of {
172180
sl_mut : bool;
181+
sl_quantity : quantity option;
182+
(** QTT binder quantity for statement-position let, per
183+
ADR-002 / ADR-007. None defaults to QOmega. Same surface
184+
syntaxes as ExprLet's el_quantity. *)
173185
sl_pat : pattern;
174186
sl_ty : type_expr option;
175187
sl_value : expr;

lib/desugar_traits.ml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,10 @@ let rec desugar_expr (ctx : context) (expr : expr) : expr =
112112
elam_body = desugar_expr ctx elam_body;
113113
}
114114

115-
| ExprLet { el_mut; el_pat; el_ty; el_value; el_body } ->
115+
| ExprLet { el_mut; el_quantity; el_pat; el_ty; el_value; el_body } ->
116116
ExprLet {
117117
el_mut;
118+
el_quantity;
118119
el_pat;
119120
el_ty;
120121
el_value = desugar_expr ctx el_value;
@@ -178,9 +179,10 @@ and desugar_block (ctx : context) (blk : block) : block =
178179

179180
and desugar_stmt (ctx : context) (stmt : stmt) : stmt =
180181
match stmt with
181-
| StmtLet { sl_mut; sl_pat; sl_ty; sl_value } ->
182+
| StmtLet { sl_mut; sl_quantity; sl_pat; sl_ty; sl_value } ->
182183
StmtLet {
183184
sl_mut;
185+
sl_quantity;
184186
sl_pat;
185187
sl_ty;
186188
sl_value = desugar_expr ctx sl_value;

lib/dune

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
(name affinescript)
33
(public_name affinescript)
44
(modes byte native)
5-
(modules ast codegen codegen_gc desugar_traits effect error error_collector error_formatter formatter interp julia_codegen json_output lexer linter module_loader opt parse_driver parse parser quantity resolve span symbol token trait typecheck types unify value wasm wasm_encode wasm_gc wasm_gc_encode wasi_runtime)
5+
(modules ast codegen codegen_gc desugar_traits effect error error_collector error_formatter formatter interp julia_codegen json_output lexer linter module_loader opt parse_driver parse parser parser_errors quantity resolve span symbol token trait typecheck types unify value wasm wasm_encode wasm_gc wasm_gc_encode wasi_runtime)
66
(libraries str unix sedlex fmt menhirLib yojson)
77
(preprocess
88
(pps ppx_deriving.show ppx_deriving.eq ppx_deriving.ord sedlex.ppx)))

lib/julia_codegen.ml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ let rec gen_expr ctx (expr : expr) : string =
132132
"(if " ^ cond_str ^ "; " ^ then_str ^ "; else " ^ else_str ^ "; end)"
133133
| None ->
134134
"(if " ^ cond_str ^ "; " ^ then_str ^ "; end)")
135-
| ExprLet { el_pat; el_value; el_body; el_mut = _; el_ty = _ } ->
135+
| ExprLet { el_pat; el_value; el_body; el_mut = _; el_quantity = _; el_ty = _ } ->
136136
(* Let binding: 'local x = val; body' *)
137137
let pat_str = gen_pattern ctx el_pat in
138138
let val_str = gen_expr ctx el_value in
@@ -227,7 +227,7 @@ and gen_pattern_cond _ctx scrutinee pat =
227227
and gen_stmt ctx (stmt : stmt) : string =
228228
(* Statements (for blocks) *)
229229
match stmt with
230-
| StmtLet { sl_pat; sl_value; sl_mut = _; sl_ty = _ } ->
230+
| StmtLet { sl_pat; sl_value; sl_mut = _; sl_quantity = _; sl_ty = _ } ->
231231
let pat_str = gen_pattern ctx sl_pat in
232232
let val_str = gen_expr ctx sl_value in
233233
pat_str ^ " = " ^ val_str

lib/parse_driver.ml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,17 @@ let parse_string ~file content =
165165
offset = pos.pos_cnum }
166166
in
167167
raise (Parse_error ("Syntax error", span))
168+
| Parser_errors.Parse_action_error (msg, startpos, endpos) ->
169+
let span = Span.make
170+
~file
171+
~start_pos:{ Span.line = startpos.Lexing.pos_lnum;
172+
col = startpos.pos_cnum - startpos.pos_bol + 1;
173+
offset = startpos.pos_cnum }
174+
~end_pos:{ Span.line = endpos.Lexing.pos_lnum;
175+
col = endpos.pos_cnum - endpos.pos_bol + 1;
176+
offset = endpos.pos_cnum }
177+
in
178+
raise (Parse_error (msg, span))
168179

169180
(** Parse a program from a file *)
170181
let parse_file filename =
@@ -196,3 +207,14 @@ let parse_expr ~file content =
196207
offset = pos.pos_cnum }
197208
in
198209
raise (Parse_error ("Syntax error", span))
210+
| Parser_errors.Parse_action_error (msg, startpos, endpos) ->
211+
let span = Span.make
212+
~file
213+
~start_pos:{ Span.line = startpos.Lexing.pos_lnum;
214+
col = startpos.pos_cnum - startpos.pos_bol + 1;
215+
offset = startpos.pos_cnum }
216+
~end_pos:{ Span.line = endpos.Lexing.pos_lnum;
217+
col = endpos.pos_cnum - endpos.pos_bol + 1;
218+
offset = endpos.pos_cnum }
219+
in
220+
raise (Parse_error (msg, span))

lib/parser.mly

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,44 @@ quantity:
182182
| ONE { QOne }
183183
| OMEGA { QOmega }
184184

185+
(* ADR-007 Option C — primary attribute form for quantity annotations.
186+
`@linear` ≡ QOne, `@erased` ≡ QZero, `@unrestricted` ≡ QOmega.
187+
Rejects unknown attribute names with a parse error. *)
188+
quantity_attr:
189+
| AT name = lower_ident
190+
{ match name with
191+
| "linear" -> QOne
192+
| "erased" -> QZero
193+
| "unrestricted" -> QOmega
194+
| other ->
195+
let msg = Printf.sprintf
196+
"unknown quantity attribute '@%s'; expected @linear, @erased, or @unrestricted"
197+
other in
198+
raise (Parser_errors.Parse_action_error (msg, $startpos, $endpos)) }
199+
200+
(* ADR-007 Option B — sugar form for quantity annotations on let/stmt_let.
201+
Reads as `:1`, `:0`, or `:ω` immediately after the pattern. The lexer
202+
emits INT for `0` and `1`, so we accept INT here and validate the value
203+
at parse time, rejecting any integer outside {0, 1}. OMEGA is the
204+
`omega` keyword or `ω` codepoint. *)
205+
quantity_b_sugar:
206+
| COLON n = INT
207+
{ match n with
208+
| 0 -> QZero
209+
| 1 -> QOne
210+
| other ->
211+
let msg = Printf.sprintf
212+
"invalid quantity literal '%d'; expected 0, 1, or ω (omega)"
213+
other in
214+
raise (Parser_errors.Parse_action_error (msg, $startpos, $endpos)) }
215+
| COLON OMEGA { QOmega }
216+
185217
param:
186218
| qty = quantity? own = ownership? name = ident COLON ty = type_expr
187219
{ { p_quantity = qty; p_ownership = own; p_name = name; p_ty = ty } }
220+
(* ADR-007 Option C: @linear x: Int *)
221+
| qty_attr = quantity_attr own = ownership? name = ident COLON ty = type_expr
222+
{ { p_quantity = Some qty_attr; p_ownership = own; p_name = name; p_ty = ty } }
188223

189224
ownership:
190225
| OWN { Own }
@@ -383,7 +418,8 @@ expr:
383418

384419
expr_assign:
385420
| lhs = expr_or EQ rhs = expr_assign
386-
{ ExprLet { el_mut = false; el_pat = PatVar (mk_ident "_" $startpos(lhs) $endpos(lhs));
421+
{ ExprLet { el_mut = false; el_quantity = None;
422+
el_pat = PatVar (mk_ident "_" $startpos(lhs) $endpos(lhs));
387423
el_ty = None; el_value = lhs; el_body = Some rhs } }
388424
| e = expr_or { e }
389425

@@ -499,9 +535,19 @@ expr_primary:
499535
| MATCH scrutinee = expr LBRACE arms = list(match_arm) RBRACE
500536
{ ExprMatch { em_scrutinee = scrutinee; em_arms = arms } }
501537

502-
/* Let expressions */
538+
/* Let expressions — ADR-007 hybrid surface syntax for quantities.
539+
Four production paths cover the cross product of {C-attr, B-sugar, neither}
540+
× {with type, without type}. The C-attribute form (`@linear let x = e`)
541+
and the B-sugar form (`let x :1 = e`) cannot both appear on the same let
542+
binder; they are alternative spellings, not stackable annotations. */
503543
| LET mut_ = MUT? pat = pattern ty = type_annotation? EQ value = expr
504-
{ ExprLet { el_mut = Option.is_some mut_; el_pat = pat;
544+
{ ExprLet { el_mut = Option.is_some mut_; el_quantity = None; el_pat = pat;
545+
el_ty = ty; el_value = value; el_body = None } }
546+
| qty_attr = quantity_attr LET mut_ = MUT? pat = pattern ty = type_annotation? EQ value = expr
547+
{ ExprLet { el_mut = Option.is_some mut_; el_quantity = Some qty_attr; el_pat = pat;
548+
el_ty = ty; el_value = value; el_body = None } }
549+
| LET mut_ = MUT? pat = pattern qty = quantity_b_sugar ty = type_annotation? EQ value = expr
550+
{ ExprLet { el_mut = Option.is_some mut_; el_quantity = Some qty; el_pat = pat;
505551
el_ty = ty; el_value = value; el_body = None } }
506552

507553
/* Lambda */
@@ -548,6 +594,11 @@ lambda_param:
548594
p_ty = TyHole } }
549595
| name = ident COLON ty = type_expr
550596
{ { p_quantity = None; p_ownership = None; p_name = name; p_ty = ty } }
597+
/* ADR-007 Option C: @linear x or @linear x: Type */
598+
| qty_attr = quantity_attr name = ident
599+
{ { p_quantity = Some qty_attr; p_ownership = None; p_name = name; p_ty = TyHole } }
600+
| qty_attr = quantity_attr name = ident COLON ty = type_expr
601+
{ { p_quantity = Some qty_attr; p_ownership = None; p_name = name; p_ty = ty } }
551602

552603
match_arm:
553604
| pat = pattern guard = match_guard? FAT_ARROW body = expr COMMA?
@@ -622,7 +673,14 @@ stmt_list_nonempty_trailing_expr:
622673

623674
stmt:
624675
| LET mut_ = MUT? pat = pattern ty = type_annotation? EQ value = expr SEMICOLON
625-
{ StmtLet { sl_mut = Option.is_some mut_; sl_pat = pat; sl_ty = ty; sl_value = value } }
676+
{ StmtLet { sl_mut = Option.is_some mut_; sl_quantity = None;
677+
sl_pat = pat; sl_ty = ty; sl_value = value } }
678+
| qty_attr = quantity_attr LET mut_ = MUT? pat = pattern ty = type_annotation? EQ value = expr SEMICOLON
679+
{ StmtLet { sl_mut = Option.is_some mut_; sl_quantity = Some qty_attr;
680+
sl_pat = pat; sl_ty = ty; sl_value = value } }
681+
| LET mut_ = MUT? pat = pattern qty = quantity_b_sugar ty = type_annotation? EQ value = expr SEMICOLON
682+
{ StmtLet { sl_mut = Option.is_some mut_; sl_quantity = Some qty;
683+
sl_pat = pat; sl_ty = ty; sl_value = value } }
626684
| e = expr SEMICOLON { StmtExpr e }
627685
| IF cond = expr then_blk = block else_part = else_part?
628686
{ StmtExpr (ExprIf { ei_cond = cond; ei_then = ExprBlock then_blk; ei_else = else_part }) }

lib/parser_errors.ml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
(* SPDX-License-Identifier: PMPL-1.0-or-later *)
2+
(* SPDX-FileCopyrightText: 2024-2026 Jonathan D.A. Jewell (hyperpolymath) *)
3+
4+
(** Shared exceptions for parser semantic actions.
5+
6+
This module exists because exceptions defined in the [%{ %}] prologue
7+
of [parser.mly] are not exported through [parser.mli], so consumers
8+
(notably [parse_driver.ml]) cannot pattern-match on them. Defining the
9+
exception here lets both [parser.mly] and [parse_driver.ml] reference
10+
it without a circular dependency. *)
11+
12+
(** Raised by a parser semantic action when a syntactically valid form
13+
carries a value the action rejects (e.g. a quantity literal that is
14+
neither 0 nor 1, an unknown [@]-attribute name).
15+
16+
Caught by [Parse_driver] and translated to its [Parse_error]
17+
exception with proper [Span.t] resolution. *)
18+
exception Parse_action_error of string * Lexing.position * Lexing.position

lib/sexpr_dump.ml

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,14 @@ and literal_to_sexpr = function
171171
let rec expr_to_sexpr d = function
172172
| ExprLit lit -> literal_to_sexpr lit
173173
| ExprVar id -> id.name
174-
| ExprLet { el_mut; el_pat; el_ty; el_value; el_body } ->
174+
| ExprLet { el_mut; el_quantity; el_pat; el_ty; el_value; el_body } ->
175175
let tag = if el_mut then "let-mut" else "let" in
176+
let q_str = match el_quantity with
177+
| None -> ""
178+
| Some QZero -> " #@erased"
179+
| Some QOne -> " #@linear"
180+
| Some QOmega -> " #@unrestricted"
181+
in
176182
let ty_str = match el_ty with
177183
| None -> ""
178184
| Some t -> Printf.sprintf " : %s" (type_expr_to_sexpr t)
@@ -181,7 +187,7 @@ let rec expr_to_sexpr d = function
181187
| None -> ""
182188
| Some b -> Printf.sprintf "\n%s%s" (indent (d + 2)) (expr_to_sexpr (d + 2) b)
183189
in
184-
Printf.sprintf "(%s %s%s %s%s)" tag
190+
Printf.sprintf "(%s%s %s%s %s%s)" tag q_str
185191
(pattern_to_sexpr el_pat) ty_str (expr_to_sexpr (d + 2) el_value) body_str
186192
| ExprIf { ei_cond; ei_then; ei_else } ->
187193
let else_str = match ei_else with
@@ -298,13 +304,19 @@ and block_to_sexpr d blk =
298304

299305
(** Convert a statement to S-expression form. *)
300306
and stmt_to_sexpr d = function
301-
| StmtLet { sl_mut; sl_pat; sl_ty; sl_value } ->
307+
| StmtLet { sl_mut; sl_quantity; sl_pat; sl_ty; sl_value } ->
302308
let tag = if sl_mut then "let-mut" else "let" in
309+
let q_str = match sl_quantity with
310+
| None -> ""
311+
| Some QZero -> " #@erased"
312+
| Some QOne -> " #@linear"
313+
| Some QOmega -> " #@unrestricted"
314+
in
303315
let ty_str = match sl_ty with
304316
| None -> ""
305317
| Some t -> Printf.sprintf " : %s" (type_expr_to_sexpr t)
306318
in
307-
Printf.sprintf "(%s %s%s %s)" tag
319+
Printf.sprintf "(%s%s %s%s %s)" tag q_str
308320
(pattern_to_sexpr sl_pat) ty_str (expr_to_sexpr (d + 2) sl_value)
309321
| StmtExpr e -> expr_to_sexpr d e
310322
| StmtAssign (lhs, op, rhs) ->

lib/typecheck.ml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,7 @@ let rec synth (ctx : context) (expr : expr) : ty result =
573573
lookup_var ctx name
574574

575575
(* Let bindings: let pat = e1 in e2 *)
576-
| ExprLet { el_pat; el_ty; el_value; el_body; el_mut = _ } ->
576+
| ExprLet { el_pat; el_ty; el_value; el_body; el_mut = _; el_quantity = _ } ->
577577
(* Synthesize or check the value *)
578578
enter_level ctx;
579579
let* val_ty = begin match el_ty with
@@ -905,7 +905,7 @@ and synth_block (ctx : context) (blk : block) : ty result =
905905

906906
and check_stmt (ctx : context) (stmt : stmt) : unit result =
907907
match stmt with
908-
| StmtLet { sl_pat; sl_ty; sl_value; sl_mut = _ } ->
908+
| StmtLet { sl_pat; sl_ty; sl_value; sl_mut = _; sl_quantity = _ } ->
909909
enter_level ctx;
910910
let* val_ty = begin match sl_ty with
911911
| Some te ->

0 commit comments

Comments
 (0)