Skip to content

Commit 4bb6c79

Browse files
hyperpolymathclaude
andcommitted
docs(adr-007): accept hybrid surface syntax + close BUG-001/002/003 in STATE
ADR-007 (META.a2ml) accepted as hybrid C-primary + B-sugar: - Option C primary: @linear / @Erased / @unrestricted - Option B sugar: :1 / :0 / :ω - Option A (let^q): rejected outright Decision driven by the standing priority order dependability > security > interop > usability/accessibility/ marketability > performance > versatility > functional extension. The hybrid keeps usability/accessibility (Option C primary in tutorials, error messages, IDE tooling, formatter output) while preserving consistency-with-papers and citation isomorphism (Option B sugar legal in source for authors porting from QTT papers). Style guide commits explicitly to keeping both forms supported; future contributors proposing to drop B must amend the ADR. The `@` namespace is now formally reserved for AffineScript modal annotations (future: @ToTal, @pure, @Borrowed, @owned). docs/spec.md T-Let rule rewritten to show the QTT-orthodox scaled form q·Γ₁ + Γ₂ ⊢ let x :^q = e₁ in e₂ alongside both surface forms, with an equivalence table and worked examples in both notations. STATE.a2ml updated: - affine-types feature flipped from `wired-but-unreachable` to `wired-and-reachable`. The Track A audit corrected a stale premise: Quantity.check_program_quantities was wired into the CLI at typecheck.ml:1206 already; the actual gap was missing surface syntax, which this PR closes. - BUG-001 moved to [[closed-bug]] with verification pointer to the four new fixtures. - BUG-002 moved to [[closed-bug]] (transitively closed via the same scale_usage code path). - BUG-003 moved to [[closed-bug]] earlier in the chain (interp.ml eval_list rewritten to L-to-R per ADR-003). - [track-a-manhattan] scope rewritten to reflect the actual work shipped, with deferred items documented (B-sugar on params, formatter --keep-quantity-sugar flag, BUG-003 behavioural fixture). ANCHOR.a2ml architecture-decisions list updated: ADR-007 marked ACCEPTED with the chosen surface syntax in the title. Refs ADR-002, ADR-003, ADR-007, BUG-001, BUG-002, BUG-003. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6384826 commit 4bb6c79

4 files changed

Lines changed: 414 additions & 33 deletions

File tree

.machine_readable/6a2/META.a2ml

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)