JOURNAL: Iter 18c.1 — Term::Clone schema

Records that 18c.1 ran zero-surprise (mechanical recursion at
~10 sites across 6 files), the future-work seam in codegen for
18c.3 is a single line (emit one rc_inc call before returning
the inner SSA reg), and 18c.2's linearity check should start
as 'green for borrow_own_demo' since that fixture's shape is
exactly what the check needs to accept.
This commit is contained in:
2026-05-08 02:27:08 +02:00
parent 7dfc88a4f3
commit 3d22dc13bf
+115
View File
@@ -5983,3 +5983,118 @@ schema), 18c.2 (linearity check + suggested_rewrites), 18c.3
(inference + codegen) sequentially. Don't attempt all three in
one pass — each builds on the previous and wants its own
verification cycle.
## Iter 18c.1 — `Term::Clone` schema (no inc emission yet)
Schema floor for explicit RC inc. Adds `Term::Clone { value }`
with serde tag `"clone"`, form-A `(clone X)`. Pure additive
schema — typechecker treats it as identity, codegen lowers it
exactly like its inner term. The runtime semantics of `(clone X)`
in 18c.1 are: nothing happens; it's an identity wrapper.
The variant is the author-visible alternative to implicit
sharing under the LLM-aware RC design (Decision 10's
mechanism (2)). When 18c.2 ships linearity enforcement, an LLM
that wants two `own`-mode uses of the same binder will be
required to spell one of them as `(clone X)`. When 18c.3 ships
codegen, the `Term::Clone` arm in `lower_term` will emit a
single `call void @ailang_rc_inc(ptr %v)` before delegating
to the inner term's lowering.
The 18c.3 emission seam is documented in the codegen arm with a
comment so the future-work site is greppable.
### Sites touched
The match-arm count was higher than the brief estimated
(~10 across 6 files) but every site was pure structural
recursion through `value`. Pretty.rs needed nothing — it has no
`match t {}` over Term. The non-obvious sites:
- `crates/ailang-check/src/lib.rs::verify_tail_positions`
records that `(clone tail-call)` is itself a tail call. Under
18c.1's identity-of-clone semantics, the inner term keeps its
tail-position. (Under 18c.3, the inserted `inc` happens before
the call returns, so the tail-position story is preserved
there too — `inc` is not a function call from the LLVM-IR
tail-position perspective; it's an inline call after the value
is computed.)
- `crates/ailang-codegen/src/escape.rs` — three sites in the
escape analysis: `walk`, `escapes`, `collect_free_vars`. The
pattern is the same as everywhere else — recurse through the
inner value.
- `crates/ail/src/main.rs::walk_term` — the deps walker that
finds cross-module references for the workspace loader. Pass-
through.
### Fixture
`examples/clone_demo.{ailx,ail.json}`:
```
(module clone_demo
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body
(let x 42
(do io/print_int (clone x))))))
```
Stdout: `42` under both `--alloc=gc` and `--alloc=rc`. The
fixture is intentionally minimal — the schema/parser/printer/
round-trip path is exercised end-to-end, but the value being
"cloned" is a primitive `Int` where RC inc is meaningless even
in 18c.3. A richer fixture wrapping a list-bound clone is a
18c.3 concern (where clone actually does something).
### Build / test
`cargo build --workspace` clean. `cargo test --workspace` green:
52 E2E (+1), 14 surface parse (+2), all other buckets unchanged.
Hash regression test in `crates/ailang-core/src/hash.rs` passes
unchanged (the new serde tag `"clone"` appears in zero existing
fixtures). `git diff examples/` is empty for pre-existing
fixtures.
### Did anything surprise
- **Zero surprises.** The 18c.1 iter ran exactly like 18a in
shape: new variant + ~10 mechanical recursion sites + parser
+ printer + fixture + test. Both iters lean on the same
underlying structure (an additive AST extension that
typechecker / codegen / desugar passes traverse without
semantic effect). When 18c.2 / 18c.3 land they will be
fundamentally different — those are real analysis passes
with semantic consequences. The schema-floor iter pattern is
rich because the project has good structural discipline
(every Term-traversal site uses pattern matching, not
reflection), and that's worth recording.
- **The future-work seam is one line.** 18c.3 only needs to
flip the codegen `Term::Clone` arm from "lower inner, return
the SSA reg" to "lower inner, emit one `inc` call, return the
SSA reg". That's the entire emission cost of explicit clone.
The hard work in 18c.3 is everywhere else (uniqueness
inference + dec instrumentation across all binder-going-out-
of-scope sites); explicit clone is comparatively cheap.
### Next
Iter 18c.2: linearity check. Walk every fn body whose params
are all explicit-mode (`Borrow` or `Own`, no `Implicit`); track
each binder's consumption state through the AST; emit
structured `use-after-consume` / `consume-while-borrowed`
diagnostics with form-A `suggested_rewrites`. Functions with any
`Implicit` param stay exempt — that's the back-compat lane
while existing fixtures stay unannotated.
The check is purely an addition to the diagnostic pipeline; it
emits no IR change. Existing fixtures (all `Implicit`) are
untouched. The new `borrow_own_demo` fixture (which has all-
explicit modes) becomes the first program subject to the check;
its current shape (borrow-then-own on `xs`) is exactly what the
check should accept, so 18c.2 starts as a "green for the
existing test" iter.