raw-buf.1 (intercept registry) shipped (140a0c0). The intrinsic-bodies
milestone (#9) then landed and pulled the foundation out from under the
original raw-buf.2/.3 split, so this revises the spec.
Two foundation changes from intrinsic-bodies:
1. The placeholder-body convention the original spec leaned on
((body x) round-trip stubs) is gone. prelude.ail now uses
(intrinsic) markers, and check_fn (lib.rs:2173) typechecks every
non-intrinsic body including kernel-tier ones. RawBuf.get with a
placeholder body (declared (ret a), body constructs a RawBuf) is now
a hard type error — get/new/set/size MUST be (intrinsic).
2. An (intrinsic) marker now requires a matching INTERCEPTS entry as a
lockstep pair (intercepts_bijection_with_intrinsic_markers). The
"manifest visible / codegen deferred" intermediate state the original
raw-buf.2 was designed to ship is forbidden by that invariant — the
marker and its codegen entry must land in the same iteration.
Re-carve (3 remaining iterations, retirement isolated):
raw-buf.2 — kernel family-crate rename ailang-kernel-stub →
ailang-kernel. Pure refactor, zero behavioural change.
raw-buf.3 — RawBuf end-to-end: raw_buf module ((intrinsic) markers)
+ 12 INTERCEPTS entries + Term::New desugar + drop +
ailang-surface wiring + E2E. Manifest+codegen land
together. Workspace count 4 → 5.
raw-buf.4 — kernel_stub retirement: RawBuf subsumes every
ratification role (Term::New, param-in, kernel-tier
auto-import, intrinsic); answer entry + stub removed,
count 5 → 4.
Preserved unchanged: Goal, slab layout, feature-acceptance argument.
The raw_buf module Form-A source in § Concrete code shapes is
gate-verified: ail check → ok (35 symbols across 3 modules); the four
(intrinsic) markers are legal because the module is (kernel).
Grounding-check: 8/9 load-bearing current-behaviour assumptions
ratified by named green tests. The single override is the Term::New
codegen-deferral arm (lib.rs:2094 + sibling at lib.rs:3296, the latter
surfaced by the grounding agent) — a transient prep.2 marker that
raw-buf.3 deletes, not a relied-upon invariant; forward-ratified by
raw-buf.3's build-and-run E2E. Override logged, not a discard.
24 KiB
raw-buf — Design Spec
Date: 2026-05-29 (revised 2026-05-29) Status: Draft — awaiting user spec review Authors: orchestrator + Claude
Revision note. raw-buf.1 (intercept registry) shipped (
140a0c0). This spec was then re-carved because theintrinsic-bodiesmilestone (#9) landed afterwards and removed the foundation the original raw-buf.2/.3 split stood on:
- The placeholder-body convention the original spec relied on (
(body x)round-trip stubs, "the existing convention fromexamples/prelude.ail") no longer exists.prelude.ailnow uses(intrinsic)markers, and the checker (check_fn,crates/ailang-check/src/lib.rs:2173) typechecks every non-intrinsic body — including kernel-tier bodies. A placeholder body forRawBuf.get(declared(ret a), body constructing aRawBuf) is now a hard type error.get(andnew/set/size) MUST be(intrinsic).- An
(intrinsic)marker now requires a matchingINTERCEPTScodegen entry as a lockstep pair, enforced byintercepts::tests::intercepts_bijection_with_intrinsic_markers. The "manifest visible, codegen deferred" intermediate state the original raw-buf.2 was designed to ship is therefore forbidden by an invariant. The marker and its codegen entry must land in the same iteration.Consequence: the kernel-manifest and the codegen now land together in one iteration. The remaining work is re-carved into three iterations (raw-buf.2/.3/.4) below. The Goal, slab layout, and feature-acceptance argument are unchanged.
Goal
Ship RawBuf a — the first kernel-tier base extension — as a
mutable, indexed, bounded-size flat buffer of primitive elements
({Int, Float, Bool}). RawBuf is the storage primitive that
unblocks two downstream needs both already captured in the
backlog:
- Series (milestone #8) — a bounded ring buffer with
financial-style indexing, implemented as a library extension
(pure AILang) wrapping
RawBuf aplus bookkeeping fields. - Embedding-ABI batch FFI (subsumes closed #2) — the M5 friction-harvest measured per-tick FFI at ~206 ns/tick on real EURUSD volume (~658 ms / 3.19M ticks). A contiguous primitive slice amortises that per-tick cost via a batch crossing. RawBuf is that primitive.
The milestone also fulfils the architectural commitment from the
kernel-extensions whitepaper § "Plugin contract": the migration
of the hardcoded primitive-instance intercepts into one registry,
triggered by the first real base extension shipping. raw-buf.1
already discharged that migration (the registry is live and the
intrinsic-bodies bijection pin locks marker ↔ entry); RawBuf is
now the first real-payload consumer of that registry.
This makes raw-buf both a feature ship (a new type LLM authors can reach for) and the proof that the registry path carries a genuine domain type, not just the migrated primitive intercepts.
Architecture
Four iterations. raw-buf.1 is done; .2/.3/.4 remain.
-
Intercept registry (raw-buf.1) — DONE (
140a0c0). Lifted the hard-codedtry_emit_primitive_instance_bodymatch into the registry tableINTERCEPTSincrates/ailang-codegen/src/intercepts.rs; the dispatch site is aintercepts::lookupcall. Pure refactor, zero behavioural change. (The subsequentintrinsic-bodiesmilestone added the(intrinsic)marker, theTerm::Intrinsicleaf, and the marker ↔ entry bijection pin on top of this registry.) -
Kernel family-crate rename (raw-buf.2) — Pure refactor, zero behavioural change. Rename
crates/ailang-kernel-stub/→crates/ailang-kernel/and reshape it as a family-crate: one Cargo crate hosting all kernel-tier modules as sibling Rust submodules undersrc/, one submodule per.ailsource. The renamed crate carriessrc/kernel_stub/{mod.rs, source.ail}(the existing stub, verbatim including its currentanswerintrinsic fn — the source has grown since the original spec was written).src/lib.rsre-exports each submodule'sSOURCEconst under the stable name every consumer already binds (pub use kernel_stub::SOURCE as STUB_AIL;). The acyclic chainailang-surface → ailang-kernel → ailang-coreis preserved; the parse hopparse_kernel_stubstays inailang-surface. Noraw_bufsubmodule yet — this iteration is only the rename, so its failure mode is "existing tests break", the cleanest bisection target. Ratified by the existing suite green. -
RawBuf end-to-end (raw-buf.3) — The feature ship, landing the manifest and the codegen together (the lockstep pair the bijection invariant requires). Adds:
- The
raw_bufsubmodule (src/raw_buf/{mod.rs, source.ail}) with theRawBufTypeDef (param-in (a Int Float Bool)) and the four operationsnew/get/set/size, each an(intrinsic)marker (NOT a placeholder body). - The 12 element-specialised
INTERCEPTSentries (4 ops × 3 element types) lowering each via@ailang_rc_alloc+ getelementptr + load/store — no new C code. The entrynameof each MUST equal the mono-mangled symbol of its(intrinsic)marker; the bijection test is the pin that forces this and surfaces the exact expected strings. - A desugar pass
(new T <types…> <values…>)→ an instantiated(app T.new <values…>)that consumes theNewArg::Typeelement type to drive monomorphisation tonew__RawBuf__<T>, soTerm::Newis eliminated before codegen.Term::Newbecomes a checker-side-only construct (the codegen completion the prep.2 deferral pointed at). raw-buf.3 removes bothTerm::Newdeferral arms codegen currently carries — thelower_termarm (crates/ailang-codegen/src/lib.rs:2094, "milestone raw-buf" internal error) and the sibling synth-path arm (lib.rs:3296). These arms are the deferral marker prep.2 left; raw-buf.3 discharges them. They are not a relied-upon current invariant — the milestone's correctness depends on removing them, not on their persisting. - Drop for primitive-element RawBuf: a single
@ailang_rc_releaseon the slab pointer (no element-walking; elements are primitives). ailang-surfacewiring:parse_raw_buf+ the workspace injection that makesraw_bufa kernel-tier auto-imported module (workspace module count 4 → 5). The worked consumer program runs end-to-end and prints60.kernel_stubstays alongside this iteration (count 5).
- The
-
kernel_stub retirement (raw-buf.4) — Focused removal. RawBuf now subsumes every ratification role kernel_stub held:
Term::Newend-to-end (the(new RawBuf …)consumer),param-inreject (the Str-element reject fixture), kernel-tier auto-import (RawBuf visible without(import raw_buf)), and the(intrinsic)mechanism (RawBuf's four ops + their registry entries). The stub is therefore pure redundancy. Deletesrc/kernel_stub/+ its re-export, dropparse_kernel_stuband its surface injection, remove theanswerINTERCEPTSentry +emit_answer(its(intrinsic)marker leaves with the stub — the bijection lockstep holds), delete thekernel_stub_module_round_tripstest, re-baselineworkspace_pinto 4 (raw_buf in, kernel_stub out — net unchanged), and updatedesign/INDEX.md+design/models/0007-kernel-extensions.md. Theailang-kernelcrate itself stays as the family-crate home for future kernel-tier modules (series, matrix, …). Isolating the retirement keeps each bijection-registry change (12 entries added in .3, theanswerentry removed in .4) in its own diff.
The ordering rationale: .2 is the only zero-behavioural-change iteration (rename), the cleanest bisection target; .3 lands the feature on a registry that has already absorbed every legacy intercept (raw-buf.1) and on the renamed family-crate (raw-buf.2), so the RawBuf manifest+codegen do not co-mingle with a registry move or a crate move; .4 removes the now-redundant stub without touching the new RawBuf code.
Concrete code shapes
Primary — the raw_buf kernel-tier module (raw-buf.3, gate-verified)
This is the exact Form-A source the raw_buf submodule embeds. It
parses and type-checks against the current tool (ail check →
ok); the four (intrinsic) markers are legal because the module
is (kernel). The element type is restricted to {Int, Float, Bool} via param-in. RawBuf.get returns the bare element type
a; set is linear (own in, own out).
(module raw_buf
(kernel)
(data RawBuf (vars a)
(doc "Mutable, indexed, fixed-size flat buffer of primitive elements. Opaque single-ctor handle; codegen intercepts emit raw alloc/load/store over an @ailang_rc_alloc slab. Element type restricted to {Int, Float, Bool} via param-in.")
(ctor B a)
(param-in (a Int Float Bool)))
(fn new
(doc "Allocate an uninitialised RawBuf of capacity n. Compiler-supplied (intrinsic) body; codegen intercept new__RawBuf__<T> emits @ailang_rc_alloc plus the i64 size header. Element bytes are uninitialised; caller must set before get.")
(type (forall (vars a) (fn-type (params (con Int)) (ret (own (con RawBuf a))))))
(params n)
(intrinsic))
(fn get
(doc "Indexed read. UB if i >= (size b); caller checks bounds via size. Compiler-supplied (intrinsic) body; codegen intercept emits getelementptr plus load.")
(type (forall (vars a) (fn-type (params (borrow (con RawBuf a)) (con Int)) (ret a))))
(params b i)
(intrinsic))
(fn set
(doc "Indexed write. Linear: own in, own out. Under uniqueness in-place; under shared copy-on-write (Issue #22). Compiler-supplied (intrinsic) body; codegen intercept emits getelementptr plus store.")
(type (forall (vars a) (fn-type (params (own (con RawBuf a)) (con Int) a) (ret (own (con RawBuf a))))))
(params b i v)
(intrinsic))
(fn size
(doc "Element count. Compiler-supplied (intrinsic) body; codegen intercept emits a single i64 load from the slab header.")
(type (forall (vars a) (fn-type (params (borrow (con RawBuf a))) (ret (con Int)))))
(params b)
(intrinsic)))
The (ctor B a) is a nominal fiction for the type system: it
gives RawBuf an identity and keeps a non-phantom. Real code
never constructs it — every operation is codegen-intercepted, and
(new RawBuf …) desugars to (app RawBuf.new …), not to a
(term-ctor RawBuf B …). The four (intrinsic) markers carry no
runnable body; codegen supplies the implementation via the
registry (raw-buf.3).
Primary — the worked consumer program (raw-buf.3 acceptance)
This is the program an LLM author naturally writes for "I need a
small mutable indexed buffer to score N values and read them
back." It is the feature-acceptance criterion's empirical
evidence. It references the raw_buf module, which is only
injected once raw-buf.3 wires it through ailang-surface — so it
does not check against the current tool (RawBuf is not yet a
workspace module). Left unlabeled deliberately: it describes
post-raw-buf.3 surface, not current behaviour.
(module raw_buf_demo
(fn main
(type (fn-type (params) (ret (con Int))))
(params)
(body
(let buf (new RawBuf (con Int) 3)
(let buf (app RawBuf.set buf 0 10)
(let buf (app RawBuf.set buf 1 20)
(let buf (app RawBuf.set buf 2 30)
(app + (app RawBuf.get buf 0)
(app + (app RawBuf.get buf 1)
(app RawBuf.get buf 2))))))))))
; ail check && ail build && ail run → prints 60
No (import raw_buf) needed: RawBuf is visible via the
kernel-tier auto-import path (prep.3). RawBuf.set / .get
resolve via type-scoped namespacing (prep.1). (new RawBuf (con Int) 3) is a Term::New whose NewArg::Type is (con Int); the
raw-buf.3 desugar threads that into new__RawBuf__Int.
Primary — the param-in reject fixture (raw-buf.3 must-fail)
(module raw_buf_reject_str
(fn cant_str
(type (fn-type (params (borrow (con RawBuf (con Str)))) (ret (con Str))))
(params buf)
(body (app RawBuf.get buf 0))))
; ail check → param-not-in-restricted-set: type Str not in {Int, Float, Bool}
Must fail at the checker with the ParamNotInRestrictedSet
diagnostic shipped in prep.3 (fieldtest F8). It is the must-fail
evidence that the param-in restriction is alive on a
real-payload extension, not just the prep.3 StubT fixture. Like
the worked program, it depends on raw_buf being injected, so it
is not gate-checkable against the current tool (unlabeled).
Secondary — INTERCEPTS entry shape (raw-buf.3, implementation)
The real registry struct (raw-buf.1) is Intercept { name, expected_params, expected_ret, wants_alwaysinline, emit }. The 12
RawBuf entries follow the existing rows' shape. One representative
(the exact symbol strings are derived by the implementer from the
bijection test, which mangles each (intrinsic) marker and
reports the expected name):
// crates/ailang-codegen/src/intercepts.rs — appended to INTERCEPTS
Intercept {
name: "new__RawBuf__Int", // == mono-mangled name of raw_buf.new @ a=Int
expected_params: &["i64"], // capacity n
expected_ret: "ptr", // slab base pointer
wants_alwaysinline: false,
emit: emit_new_raw_buf_int, // @ailang_rc_alloc(8 + n*8); store i64 n at offset 0
},
// + new__RawBuf__{Float,Bool}, RawBuf_get__{Int,Float,Bool},
// RawBuf_set__{Int,Float,Bool}, RawBuf_size__{Int,Float,Bool}
Secondary — Term::New desugar (raw-buf.3, implementation)
Term::New { type_name, args } where args: Vec<NewArg>
(NewArg::Type | NewArg::Value, both already parsed + round-trip
ratified). The new desugar pass rewrites it to an application of
the type's new fn, instantiated at the element type carried by
the leading NewArg::Type:
(new RawBuf (con Int) 3) ; Term::New { type_name: "RawBuf",
; args: [Type(con Int), Value(3)] }
──desugar──▶
(app RawBuf.new 3) @ a = Int ; instantiation pins mono → new__RawBuf__Int
The desugar consumes the NewArg::Type for the instantiation and
passes only the NewArg::Value args as call arguments. After this
pass no Term::New survives into codegen; both deferral arms
(lib.rs:2094 in lower_term, "milestone raw-buf" internal
error, and the sibling at lib.rs:3296 in the synth path) become
unreachable and are removed. Whether
the prep.2 checker already accepts the NewArg::Type-bearing
Term::New semantically, or needs a small extension, is a
raw-buf.3 task to verify first (the form parses + round-trips
today; the lift pass passes NewArg::Type through untouched).
Secondary — slab layout (raw-buf.3)
[ size : i64 ][ elem_0 ][ elem_1 ] ... [ elem_{size-1} ]
^ ^ ^
offset 0 offset 8 offset 8 + size*sizeof(T)
Element widths: Int = 8, Float = 8, Bool = 1. Header is
i64, 8-byte aligned; element payload follows directly.
@ailang_rc_alloc is called with 8 + size * sizeof(T) and
returns the slab base ptr. The size header is self-describing,
so drop and RawBuf.size need no side-table. Drop is a single
@ailang_rc_release on the slab pointer — primitive elements
carry no recursive drops. The per-TypeDef drop generation gets a
"primitive-element-RawBuf" arm dispatched via the same registry.
Components
| Component | Iter | Files |
|---|---|---|
| Intercept registry | raw-buf.1 (DONE) | crates/ailang-codegen/src/intercepts.rs + dispatch site in lib.rs |
| Kernel family-crate rename | raw-buf.2 | git mv crates/ailang-kernel-stub crates/ailang-kernel; reshape to src/lib.rs + src/kernel_stub/{mod.rs, source.ail} (existing stub source incl. answer re-housed verbatim); Cargo.toml package + workspace member + dep-alias renames; crates/ailang-surface/Cargo.toml dep rename + loader.rs use-path renames; design/INDEX.md + CLAUDE.md code-layout row |
| RawBuf module + codegen + desugar + drop | raw-buf.3 | new crates/ailang-kernel/src/raw_buf/{mod.rs, source.ail} + lib.rs re-export; parse_raw_buf + workspace injection in crates/ailang-surface/src/loader.rs + re-export in lib.rs; 12 entries + emit fns in crates/ailang-codegen/src/intercepts.rs; Term::New desugar in crates/ailang-core/src/desugar.rs (and removal of the codegen Term::New error arm in crates/ailang-codegen/src/lib.rs); primitive-element-RawBuf drop arm; workspace_pin 4 → 5; round-trip + E2E tests |
| kernel_stub retirement | raw-buf.4 | delete crates/ailang-kernel/src/kernel_stub/ + its lib.rs re-export; drop parse_kernel_stub + injection in ailang-surface; remove answer entry + emit_answer in intercepts.rs; delete kernel_stub_module_round_trips test; workspace_pin 5 → 4; design/INDEX.md + design/models/0007-kernel-extensions.md updates |
Data flow
Build-time (raw-buf.3 onwards):
- CLI loads workspace →
ailang-surfaceinjectsraw_bufas a kernel-tier module (kernel: true) just likeprelude. checkwalks consumer code;(con RawBuf (con Int))resolves toraw_buf.RawBufvia the kernel-tier auto-import path (prep.3).param-inon RawBuf'sarejects non-{Int,Float,Bool} instantiations at the consumer typecheck site, using the existingParamNotInRestrictedSetdiagnostic.
Codegen-time (raw-buf.3 onwards):
- The desugar pass replaces every
Term::New { type_name, args }with an instantiated(app <type_name>.new <values>), pinning the element type from the leadingNewArg::Type. - For each fn, codegen reaches the registry dispatch site. If the
fn's mono name resolves via
intercepts::lookup, the entry'semitruns; the(intrinsic)marker carries no body to fall back to. Otherwise the normal AILang lowering runs. - For RawBuf consumers,
new__RawBuf__Intemits the alloc;RawBuf_set__Intastore i64;RawBuf_get__Intaload i64;RawBuf_size__Inta single header load.
Bijection interaction (raw-buf.3 / .4):
Each RawBuf (intrinsic) marker reachable from the workspace is
collected by workspace_intrinsic_markers() and must resolve to
an INTERCEPTS entry (and vice versa, minus the
OPTIMISATION_ONLY allowlist). raw-buf.3 adds 12 markers + 12
entries together. raw-buf.4 removes the answer marker (with the
stub) and the answer entry together. A half-landed change on
either side is a hard unit-test failure
(intercepts_bijection_with_intrinsic_markers).
Workspace module count: prelude + kernel_stub + 2 test
fixtures = 4 today. raw-buf.3 adds raw_buf → 5. raw-buf.4 retires
kernel_stub → 4. The workspace_pin test pins each transition.
Error handling
Two consumer-facing diagnostics; both already shipped, no new arms needed:
param-not-in-restricted-set(prep.3, fieldtest F8). Fires when a consumer writes(con RawBuf (con Str))or any non-{Int,Float,Bool} element. Names the type, the variable, the TypeDef, and the restricted set.new-type-not-constructible(prep.3, fieldtest F5). Fires when a consumer writes(new SomeType …)andSomeTypehas no(fn new …)in its home module. raw_buf ships(fn new …), so the worked program passes; the diagnostic still protects future authors who write(new Counter 0)against anew-less type.
The codegen Term::New deferral arms (lib.rs:2094 in
lower_term, message "… milestone raw-buf"; and the sibling at
lib.rs:3296 in the synth path) are removed in raw-buf.3 once
the desugar eliminates Term::New before codegen. Neither is a
user-facing diagnostic — both are the deferral marker prep.2 left,
which raw-buf.3 discharges. They have no test pinning them today
(they are an internal CodegenError::Internal, not an asserted
behaviour); raw-buf.3's end-to-end build-and-run E2E (the worked
program printing 60) is the forward ratification — it exercises
the Term::New → desugar → codegen path that replaces them.
Bounds checks are not in scope (whitepaper § "RawBuf — the
base extension"): RawBuf.get/.set are UB if i >= size(b);
caller checks via RawBuf.size. Bounds-check insertion is a
future axis, no backlog item yet.
Testing strategy
raw-buf.2 (rename):
- All existing workspace tests pass unchanged — the rename is a
pure refactor. Zero test-count change. The crate-rename's
failure mode is "the workspace does not build / existing tests
break", caught by
cargo build --workspace+cargo test --workspace.
raw-buf.3 (RawBuf end-to-end):
- Round-trip:
raw_buf_module_round_trips(mirror ofkernel_stub_module_round_tripsindesign_schema_drift.rs) — parse → serialise → round-trip; assertkernel == true, nameraw_buf, andRawBufTypeDefparam_inforacontains all of Int/Float/Bool. - E2E
raw_buf_int_e2e: the worked consumer program —ail check && ail build && ail run→ prints60. - E2E
raw_buf_float_e2e/raw_buf_bool_e2e: Float and Bool element variants (Bool catches any i1/i8 packing question). - E2E
raw_buf_param_in_reject_e2e: the Str-element reject fixture —ail checkexits non-zero withparam-not-in-restricted-setin stderr. - Drop ratification: a consumer that allocates a RawBuf and lets it go out of scope without a leak — re-uses the existing leak-check harness, no new infra.
- Bijection:
intercepts_bijection_with_intrinsic_markersstays green with the 12 new markers + 12 new entries (the test forces the exact mangled-name match). workspace_pincount assertion 4 → 5 (+contains_key("raw_buf")).
raw-buf.4 (retirement):
workspace_pinre-baselines 5 → 4 (raw_buf in, kernel_stub out);contains_key("kernel_stub")assertion removed,contains_key("raw_buf")retained.kernel_stub_module_round_tripsdeleted (itsparse_kernel_stubcallee is gone;raw_buf_module_round_tripscarries the round-trip invariant forward).- Bijection stays green after the
answermarker + entry leave together. - The full suite stays green; the diff is a removal, ratified by "nothing references the stub and everything still builds".
Acceptance criteria
The milestone ships when:
- The worked consumer program runs end-to-end:
ail check && ail build && ail runprints60(raw-buf.3). - The param-in reject fixture exits at
ail checkwith the prep.3param-not-in-restricted-setdiagnostic (raw-buf.3). - RawBuf's four operations are
(intrinsic)markers, each paired with anINTERCEPTSentry;intercepts_bijection_with_intrinsic_markersis green (raw-buf.3). Term::Newno longer reaches codegen — the desugar eliminates it and both deferral arms (lib.rs:2094+lib.rs:3296) are removed (raw-buf.3).ailang-kernel-stubis renamed to theailang-kernelfamily-crate (raw-buf.2);kernel_stubis retired andraw_buftakes over its ratification roles (raw-buf.4).- Workspace test suite green at each iteration boundary, plus the new tests enumerated in § Testing strategy.
Feature-acceptance argument
Applied per design/contracts/0004-feature-acceptance.md:
-
LLM author reaches for it naturally. RawBuf is the only path to mutable indexed storage in AILang — the language has no other "fill N slots and read them back" primitive. The worked consumer program above is what an LLM author writes for that need; there is no alternative shape to reach for.
-
Measurable improvement. Embedding-ABI M5 measured per-tick FFI at ~206 ns/tick (~658 ms / 3.19M EURUSD ticks). RawBuf as the batch-FFI primitive amortises that per-tick cost across one crossing. The Series milestone (#8) is also blocked on RawBuf; shipping raw-buf removes that block.
-
No reintroduced bug class. Mutation discipline stays mode-tracked (whitepaper § "RawBuf — the base extension"):
RawBuf.setisown → own, not effect-tracked. NoRawBufeffect is declared. Uniqueness inference rewrites in-place; shared ownership falls back to copy-on-write (Issue #22). The pure-core invariant survives: code without a declared effect is pure. Local reasoning survives: a fn takingborrow (RawBuf Int)and returningIntis pure from the caller's view, because the buffer is not mutated.