@@ -200,3 +200,294 @@ consequences = """
200200 place that did this, and nobody has filed a bug, so blast radius is
201201 expected to be zero.
202202"""
203+
204+ [[adr]]
205+ id = "ADR-007"
206+ status = "accepted"
207+ date = "2026-04-10"
208+ accepted = "2026-04-10"
209+ title = "Surface syntax for quantity annotations: @linear/@erased/@unrestricted (primary) + :1/:0/:ω (sugar)"
210+ context = """
211+ The Track A audit on 2026-04-10 found that affine enforcement is wired
212+ through the CLI (Quantity.check_program_quantities runs from
213+ lib/typecheck.ml:1206 on every check/compile/eval invocation) but
214+ **unreachable** from user programs because the surface syntax for declaring
215+ quantity annotations on let-binders does not exist. Specifically:
216+
217+ - lib/ast.ml ExprLet and StmtLet have no quantity field. They carry
218+ el_mut/sl_mut, el_pat/sl_pat, el_ty/sl_ty, el_value/sl_value, el_body
219+ — no el_quantity. Only function `param` and `type_param` carry
220+ `p_quantity`/`tp_quantity`.
221+ - lib/lexer.ml never emits ZERO/ONE tokens. Source `0` and `1` are lexed
222+ as `INT 0`/`INT 1`. The only quantity literal that round-trips through
223+ source today is `omega`/`ω` (lexer.ml:53 and lexer.ml:153 respectively).
224+ - lib/parser.mly's `quantity` rule (lines 180-183) takes ZERO|ONE|OMEGA
225+ but only OMEGA is reachable from source. Even on function parameters,
226+ `:1` and `:0` annotations cannot be written.
227+ - ADR-002 specifies the QTT-orthodox scaled Let rule (q·Γ₁ + Γ₂), which
228+ requires the binder to carry a quantity. Without surface syntax to
229+ attach one, the rule has no input to scale by, BUG-001 cannot be
230+ closed, and the headline affine-types feature remains theatre.
231+
232+ Surface syntax is a language-design decision, not an implementation
233+ detail. The decision needs author input before any of items 1-6 in
234+ [track-a-manhattan] proceed.
235+ """
236+ options = """
237+ Three candidate surface syntaxes are listed. None has been chosen.
238+
239+ # Option A — caret-quantity suffix on `let` keyword
240+
241+ let^1 x = linear_resource() in use_x_once(x)
242+ let^0 _proof = expensive_term() in body
243+ let^ω y = pure_int() in use_y_many(y)
244+
245+ Pros:
246+ - Visually parallel to T-Let's `let x :^q A` notation in QTT papers.
247+ - The `^` is currently unused in the lexer, so no disambiguation cost.
248+ - The annotation lives on the keyword, not the binder, which mirrors
249+ the type-theory framing where `q` parameterises the rule, not the
250+ variable.
251+ - Easy parser rule: LET (CARET quantity)? pat (COLON ty)? EQ value.
252+
253+ Cons:
254+ - `^` is unfamiliar surface syntax for users coming from Rust/Idris2.
255+ - The `^` may collide with future bitwise-XOR or pow operators
256+ (currently OpBitXor exists in ast.ml but the lexer surface form is
257+ unclear).
258+ - Requires users to type a non-letter character on every annotation.
259+
260+ # Option B — colon-quantity prefix on the type ascription
261+
262+ let x :1 :Int = linear_resource()
263+ let x :0 :Int = expensive_term()
264+ let x :ω :Int = pure_int()
265+ let x :1 = linear_resource() # type inferred
266+
267+ Pros:
268+ - Reuses the existing COLON token; no new operators needed.
269+ - Mirrors the function parameter syntax, which already accepts
270+ `qty? own? name COLON ty` (parser.mly:186).
271+ - Familiar to readers of Idris2 (`x : 1 A`) and Quantitative Type
272+ Theory papers.
273+
274+ Cons:
275+ - Two consecutive colons looks awkward and may confuse readers
276+ expecting `::` (the path separator COLONCOLON token).
277+ - When the type is inferred (`let x :1 = e`), the parser needs
278+ one-token lookahead to distinguish `:1` (quantity) from `:Int`
279+ (type). This is a minor parser challenge but non-zero.
280+ - The lexer must additionally route `INT 0`/`INT 1` to ZERO/ONE in
281+ quantity position, which is context-sensitive lexing — best handled
282+ by accepting `INT 0|INT 1` directly in the parser's quantity rule
283+ instead, but that introduces a small grammar wart.
284+
285+ # Option C — annotation-style attribute before `let`
286+
287+ @linear let x = linear_resource() in use_x_once(x)
288+ @erased let _proof = expensive_term() in body
289+ @unrestricted let y = pure_int() in use_y_many(y)
290+ let z = pure_int() in use_z(z) # default = unrestricted
291+
292+ Pros:
293+ - Reads naturally; `@linear` and `@erased` are self-documenting and
294+ require no quantity-theory background.
295+ - Trivially extensible to future modal annotations (e.g. `@borrowed`,
296+ `@owned`) without grammar changes.
297+ - Parser change is local: AT (ident) LET pat (COLON ty)? EQ value.
298+ No COLON disambiguation, no new operator tokens.
299+ - Aligns with the language-policy preference for descriptive names
300+ over symbolic ones (CLAUDE.md: "Always use descriptive variable
301+ names"; the same instinct applied to annotations).
302+
303+ Cons:
304+ - Verbose. `@linear let x = ...` is six tokens vs `let^1 x = ...`'s
305+ four.
306+ - Diverges from the QTT mathematical notation, making it harder for
307+ paper-trained readers to map source to spec. Mitigation: spec.md
308+ can show both forms in parallel.
309+ - The `@` token is currently unused, so it's free, but introducing
310+ it for one feature creates pressure to use it for others (which
311+ may or may not be desirable depending on language direction).
312+ - Requires a small mapping table from attribute name to quantity
313+ value (`linear → QOne`, `erased → QZero`, `unrestricted → QOmega`).
314+ """
315+ decision = """
316+ Hybrid: Option C accepted as primary, Option B accepted as sugar.
317+
318+ Both surface syntaxes parse to the same internal representation.
319+ Tutorials, error messages, IDE tooling, and the spec's prose all use
320+ Option C. Option B remains legal for users porting from QTT papers or
321+ who prefer the compact form. Option A is rejected outright.
322+
323+ # Primary surface form (Option C — what tutorials and error messages use)
324+
325+ @linear — used exactly once (QOne)
326+ @erased — must not be used at runtime (QZero)
327+ @unrestricted — any number of uses (QOmega), the default
328+
329+ Examples:
330+ @linear let x = linear_resource() in use_x_once(x)
331+ @erased let _proof = expensive_term() in body_not_using_proof
332+ @unrestricted let y = pure_int() in use_y_many(y)
333+ let z = pure_int() in use_z(z) # default: @unrestricted
334+
335+ fn consume(@linear x: Resource) -> Unit = release(x)
336+ fn weigh(@erased _proof: Even(n), n: Nat) -> Nat = n / 2
337+
338+ # Sugar surface form (Option B — accepted but not promoted)
339+
340+ let x :1 :Resource = linear_resource() in use_x_once(x)
341+ let _proof :0 :Even(n) = expensive_term() in body
342+ let y :ω :Int = pure_int() in use_y_many(y)
343+ let z :Int = pure_int() in use_z(z) # no quantity = @unrestricted
344+
345+ fn consume(x :1: Resource) -> Unit = release(x)
346+ fn weigh(_proof :0: Even(n), n: Nat) -> Nat = n / 2
347+
348+ # Equivalence
349+
350+ @linear ≡ :1
351+ @erased ≡ :0
352+ @unrestricted ≡ :ω (also: omitted entirely)
353+
354+ Both forms parse to the same `el_quantity = Some QOne | QZero | QOmega`
355+ in lib/ast.ml. The compiler chooses the canonical form (Option C) when
356+ emitting diagnostics, formatter output, and pretty-printed source. The
357+ formatter rewrites Option B to Option C on `affinescript fmt`, with an
358+ opt-out flag `--keep-quantity-sugar` for users who deliberately prefer
359+ the compact form.
360+
361+ Rationale (decided per the standing priority order
362+ dependability > security > interop > usability/accessibility/marketability
363+ > performance > versatility > functional extension):
364+
365+ - Usability/accessibility/marketability beats consistency-with-papers
366+ in the priority order, so Option C must be the primary form a new
367+ user encounters in tutorials and error messages. A new user reading
368+ `@linear let x = e` can form a working hypothesis without consulting
369+ the spec. The same user looking at `let x :1 = e` has nothing to
370+ Google.
371+ - However, the priority order does not require *exclusivity*. Accepting
372+ Option B as sugar costs ~15 LOC of parser surface area and one
373+ paragraph in the spec, and in return it:
374+ * Preserves consistency with the existing function-parameter syntax
375+ (which uses `qty? own? name COLON ty`), so the param-quantity gap
376+ closes with the *same* grammar production rather than a parallel
377+ one. Users learning the param syntax already see `:1` in the
378+ grammar; the sugar makes that knowledge transfer to let-binders.
379+ * Preserves citation isomorphism with QTT papers — anyone porting an
380+ Idris2 example or a Granule example can paste the numeric form
381+ directly without rewriting it.
382+ * Gives advanced users a denser form for code where annotations are
383+ frequent and English keywords would create visual noise.
384+ - Compiler errors and source code share the C vocabulary regardless of
385+ which form the user wrote: "`@linear` binding 'x' used 2 times" reads
386+ cleanly without mental translation, even if the source was written
387+ as `:1`. The diagnostic always speaks the canonical form.
388+ - Tutorials can be example-driven from the first runnable program with
389+ Option C; the QTT semiring and Option B sugar become optional
390+ advanced reading rather than a prerequisite for "hello world with
391+ affine types."
392+ - The `@` namespace is now committed to AffineScript and may grow to
393+ cover additional modal annotations (e.g. `@total`, `@pure`,
394+ `@borrowed`, `@owned`) in future ADRs. This is an intentional
395+ reservation, not a side effect.
396+ - The Rust/Linear-Haskell precedent is real: Rust ships affine
397+ semantics under approachable keywords and reaches a vastly larger
398+ audience than Linear Haskell, which exposes the type-theory
399+ machinery directly. AffineScript chooses Rust's marketing instinct
400+ for the primary form, while keeping Linear Haskell's notation
401+ available for users who already think in it.
402+
403+ # Style guide commitment
404+
405+ The spec commits explicitly to keeping both forms supported. Future
406+ contributors proposing to drop Option B sugar must amend this ADR
407+ rather than removing it silently. The recommended convention in
408+ published AffineScript code is:
409+
410+ - New code: Option C (`@linear let x = e`).
411+ - Code accompanying a paper: either form, author's choice.
412+ - Auto-formatted code: Option C (formatter rewrites unless
413+ `--keep-quantity-sugar` is set).
414+ - Generated code (compiler diagnostics, IDE quick-fixes, refactoring
415+ tools): Option C exclusively.
416+
417+ Rejected alternatives:
418+
419+ - Option A (`let^q x = e`): rejected. Lowest implementation cost but
420+ worst on the priority order — symbolic, opaque to non-TT readers,
421+ forecloses future use of `^` for bitwise XOR or exponentiation.
422+ - Option B exclusively (without C): rejected for the same
423+ accessibility/adoption reasons that make C the primary form.
424+ - Option C exclusively (without B sugar): considered and rejected. The
425+ marginal cost of accepting B is low (~15 LOC parser, one paragraph
426+ spec), and the benefit — closing the param-quantity gap with one
427+ grammar production and preserving paper-citation isomorphism — is
428+ worth the cost given the standing priority order's high weighting
429+ of interop alongside accessibility.
430+ """
431+ consequences = """
432+ - lib/ast.ml gains el_quantity / sl_quantity fields on ExprLet and
433+ StmtLet. ~10 modules pattern-matching on Let must be updated
434+ mechanically (typecheck, interp, codegen, codegen_gc, julia_codegen,
435+ formatter, sexpr_dump, json_output, linter, opt, desugar_traits).
436+ - lib/lexer.ml gains an AT token for `@`. ~2 LOC.
437+ - lib/token.ml gains the AT token kind. ~2 LOC.
438+ - lib/parser.mly gains:
439+ * AT ident — a quantity_attr rule that maps `@linear`/`@erased`/
440+ `@unrestricted` to QOne/QZero/QOmega and rejects unknown attribute
441+ names with a parse error pointing at the documented set.
442+ * Optional quantity_attr prefix on let_decl, stmt_let, param, and
443+ lambda_param productions. ~15 LOC.
444+ * The existing numeric quantity rule (ZERO|ONE|OMEGA) gains INT 0
445+ and INT 1 as accepted lexemes (since the lexer emits INT not
446+ ZERO/ONE for `0` and `1`). This makes Option B sugar reachable
447+ from source on let-binders and params, in addition to type_params
448+ where it already worked. ~5 LOC.
449+ * Both productions resolve to the same el_quantity / sl_quantity /
450+ p_quantity slot — they are alternative concrete syntaxes for the
451+ identical abstract syntax.
452+ - lib/quantity.ml ExprLet/StmtLet cases gain context scaling per
453+ ADR-002. ~25 LOC including a `scale_usage_by` helper.
454+ - Error messages in lib/quantity.ml format_quantity_error are updated
455+ to use `@linear` / `@erased` / `@unrestricted` vocabulary instead of
456+ the show_quantity numeric forms.
457+ - Regression fixtures land covering BOTH surface forms:
458+ * test/e2e/fixtures/bug_001_omega_let_smuggles_linear.affine — must
459+ be rejected (Option C form: `@unrestricted let x = ...`).
460+ * test/e2e/fixtures/bug_001_sugar_form.affine — must be rejected
461+ (Option B form: `let x :ω = ...`). Proves both surface forms are
462+ enforced through the same code path.
463+ * test/e2e/fixtures/affine_let_valid.affine — must pass (Option C).
464+ * test/e2e/fixtures/affine_let_valid_sugar.affine — must pass
465+ (Option B).
466+ - docs/spec.md T-Let rule is rewritten to show the formal QTT notation
467+ alongside both surface forms (Option C primary, Option B sugar) so
468+ paper-trained readers and pedagogy-first readers both see their
469+ preferred presentation.
470+ - docs/spec.md gains a "Surface syntax for quantity annotations"
471+ section documenting the equivalence table and the style-guide
472+ commitment from this ADR.
473+ - BUG-002 (`:0` lets do not erase their RHS) closes in the same change,
474+ per ADR-002's consequence list.
475+ - The affine-types feature flag in STATE.a2ml [features] flips from
476+ `wired-but-unreachable` to `wired-and-reachable` once the change
477+ lands.
478+ - Future modal annotations (`@total`, `@pure`, `@borrowed`, `@owned`,
479+ etc.) inherit the same `@`-attribute parsing infrastructure for free.
480+ - The formatter (lib/formatter.ml) gains a `--keep-quantity-sugar` flag
481+ that defaults to OFF (so default `affinescript fmt` rewrites Option
482+ B sugar to Option C primary). Users opting in retain their chosen
483+ form across format passes.
484+ """
485+ references = [
486+ ".machine_readable/6a2/STATE.a2ml [track-a-manhattan]",
487+ ".machine_readable/6a2/STATE.a2ml [[open-bug]] BUG-001",
488+ ".machine_readable/6a2/STATE.a2ml [[open-bug]] BUG-002",
489+ ".machine_readable/6a2/META.a2ml [[adr]] ADR-002 (scaled Let rule)",
490+ "lib/ast.ml ExprLet (lines 105-111), StmtLet (lines 170-176)",
491+ "lib/lexer.ml (ZERO/ONE never emitted)",
492+ "lib/parser.mly quantity rule (lines 180-183)",
493+ ]
0 commit comments