iter eob.1: effect-op args walk as Borrow; heap-Str RC discipline closes
Language rule: Term::Do.args[*] are walked in Position::Borrow by
the uniqueness + linearity passes, symmetric to Term::Ctor.args[*]
being walked in Position::Consume. The kind itself carries the
ownership default — no per-op field on EffectOpSig. Three
analyser sites flip in lockstep: uniqueness.rs:289, linearity.rs:506,
and the shared doc-comment at linearity.rs:42.
Heap-Str-specific consequences (closing hs.4's leak):
- int_to_str / float_to_str ret_mode: Implicit → Own at four lockstep
sites (builtins.rs:200,210 + synth.rs:172,179). The codegen
trackability gate (drop.rs:464-475, iter 18g.2) now fires on these
callees, so the let-binder receives a scope-close drop.
- drop_symbol_for_binder's App arm gets a Type::Con{name:"Str"} →
"ailang_rc_dec" carve-out, symmetric to field_drop_call's existing
Str arm. Without it, the new Own-trackable App binder would emit
drop_<m>_Str (undefined).
Tests: the two pre-existing RED tests at e2e.rs (commit 592d87b)
flip to GREEN unchanged with predicted numbers (allocs=1,frees=1,
live=0 and allocs=2,frees=2,live=0). Two new positive tests pin
the rule's coverage: primitive-Int arg through io/print_int produces
zero RC traffic; heap-Str through 2× io/print_str in sequence
typechecks (no use-after-consume) and balances allocs=1,frees=1,
live=0.
DESIGN.md §Decision 10 gets the arg-position-policy table for both
AST node kinds (Ctor=Consume, Do=Borrow) plus a linking paragraph
explaining how Do=Borrow cooperates with ret_mode==Own.
heap-str-abi milestone closes here: WhatsNew entry shipped (Strings
produced at runtime now release cleanly), roadmap P1 entry checked
off, Post-22-Prelude depends-on line removed.
Workspace cargo test + cross_lang.py + compile_check.py all green.
check.py noisy latency cluster + bench_list_sum.bump_s ±12% — same
precedent as the previous two audits, not coupled to this milestone.
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"iter_id": "eob.1",
|
||||
"date": "2026-05-12",
|
||||
"mode": "standard",
|
||||
"outcome": "DONE",
|
||||
"tasks_total": 7,
|
||||
"tasks_completed": 7,
|
||||
"reloops_per_task": {
|
||||
"1": 0,
|
||||
"2": 0,
|
||||
"3": 0,
|
||||
"4": 0,
|
||||
"5": 0,
|
||||
"6": 0,
|
||||
"7": 0
|
||||
},
|
||||
"review_loops_spec": 0,
|
||||
"review_loops_quality": 0,
|
||||
"blocked_reason": null
|
||||
}
|
||||
@@ -2622,3 +2622,36 @@ fn str_field_in_adt_drops_heap_str_correctly() {
|
||||
assert_eq!(allocs, frees, "every RC slab must be freed; allocs={allocs}, frees={frees}");
|
||||
assert_eq!(live, 0, "no leaked RC slabs allowed; live={live}");
|
||||
}
|
||||
|
||||
/// Iter eob.1: primitive-Int arg passed to an effect-op. Under the
|
||||
/// new "Term::Do args = Borrow" rule, the let-binder `n` is not
|
||||
/// consumed by `io/print_int`; but Int is unboxed, so there is no
|
||||
/// pointer to RC-track. Pin: zero allocs / zero frees / zero live —
|
||||
/// the rule introduces no spurious bookkeeping on primitives.
|
||||
#[test]
|
||||
fn int_arg_to_effect_op_does_not_rc_track() {
|
||||
let (stdout, allocs, frees, live) =
|
||||
build_and_run_with_rc_stats("int_to_print_int_borrow.ail.json");
|
||||
assert_eq!(stdout, "7\n");
|
||||
assert_eq!(allocs, 0, "Int arg should not trigger any RC alloc; got allocs={allocs}");
|
||||
assert_eq!(frees, 0, "Int arg should not trigger any RC free; got frees={frees}");
|
||||
assert_eq!(live, 0, "no leaked RC slabs allowed; live={live}");
|
||||
}
|
||||
|
||||
/// Iter eob.1: heap-Str let-binder consumed by two `io/print_str`
|
||||
/// calls in sequence. Pins the linearity-side consequence of the new
|
||||
/// rule: under the old Position::Consume walk for Term::Do args, the
|
||||
/// second print would have triggered `use-after-consume`. Under the
|
||||
/// new Position::Borrow walk, both calls are borrows and the program
|
||||
/// typechecks. Under --alloc=rc the slab is allocated once by
|
||||
/// int_to_str, lives through both prints, and is freed exactly once
|
||||
/// at scope close — allocs == 1, frees == 1, live == 0.
|
||||
#[test]
|
||||
fn heap_str_repeated_print_balances_rc_stats() {
|
||||
let (stdout, allocs, frees, live) =
|
||||
build_and_run_with_rc_stats("heap_str_repeated_print_borrow.ail.json");
|
||||
assert_eq!(stdout, "42\n42\n");
|
||||
assert_eq!(allocs, 1, "expected exactly one heap-Str slab; got allocs={allocs}");
|
||||
assert_eq!(frees, 1, "expected exactly one free; got frees={frees}");
|
||||
assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}");
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ pub fn install(env: &mut crate::Env) {
|
||||
ret: Box::new(Type::str_()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ailang_core::ast::ParamMode::Implicit,
|
||||
ret_mode: ailang_core::ast::ParamMode::Own,
|
||||
},
|
||||
);
|
||||
env.globals.insert(
|
||||
@@ -207,7 +207,7 @@ pub fn install(env: &mut crate::Env) {
|
||||
ret: Box::new(Type::str_()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ailang_core::ast::ParamMode::Implicit,
|
||||
ret_mode: ailang_core::ast::ParamMode::Own,
|
||||
},
|
||||
);
|
||||
env.globals.insert(
|
||||
|
||||
@@ -39,7 +39,9 @@
|
||||
//! false-negative for "param declared own but never moved" — out of
|
||||
//! scope for 18c.2 per the assignment).
|
||||
//! - `Term::Let.value`, `Term::Seq.lhs`, `Term::If.cond` — Consume.
|
||||
//! - `Term::Ctor.args[*]`, `Term::Do.args[*]` — Consume.
|
||||
//! - `Term::Ctor.args[*]` — Consume (the new value owns them).
|
||||
//! - `Term::Do.args[*]` — Borrow (effect-op observes; ownership
|
||||
//! stays with the caller).
|
||||
//! - `Term::Lam` — captures are conservatively Consume.
|
||||
//!
|
||||
//! ## Within-call borrow lifetime (consume-while-borrowed)
|
||||
@@ -504,8 +506,11 @@ impl<'a> Checker<'a> {
|
||||
}
|
||||
}
|
||||
Term::Do { args, .. } => {
|
||||
// Effect-op args are borrowed: the op observes the value
|
||||
// but ownership stays with the caller. Symmetric to the
|
||||
// doc-comment bullet at :42-43.
|
||||
for a in args {
|
||||
self.walk(a, Position::Consume);
|
||||
self.walk(a, Position::Borrow);
|
||||
}
|
||||
}
|
||||
Term::Lam { params, body, .. } => {
|
||||
|
||||
@@ -288,7 +288,7 @@ impl<'a> Walker<'a> {
|
||||
}
|
||||
Term::Do { args, .. } => {
|
||||
for a in args {
|
||||
self.walk(a, Position::Consume);
|
||||
self.walk(a, Position::Borrow);
|
||||
}
|
||||
}
|
||||
Term::Lam { params, body, .. } => {
|
||||
|
||||
@@ -534,6 +534,14 @@ impl<'a> Emitter<'a> {
|
||||
Term::App { .. } => {
|
||||
if let Ok(ret_ty) = self.synth_arg_type(value) {
|
||||
if let Type::Con { name, .. } = ret_ty {
|
||||
// Symmetric to `field_drop_call`'s Str arm: Str is a
|
||||
// built-in pointer type with no per-type drop fn. Both
|
||||
// heap-Str (rc_header at payload-8) and static-Str
|
||||
// (codegen-elision keeps static pointers out of this
|
||||
// path) consume via `ailang_rc_dec`.
|
||||
if name == "Str" {
|
||||
return "ailang_rc_dec".to_string();
|
||||
}
|
||||
if name.matches('.').count() == 1 {
|
||||
let (prefix, suffix) =
|
||||
name.split_once('.').expect("checked");
|
||||
|
||||
@@ -169,14 +169,14 @@ pub(crate) fn builtin_ail_type(name: &str) -> Option<Type> {
|
||||
ret: Box::new(Type::str_()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
"int_to_str" => Type::Fn {
|
||||
params: vec![Type::int()],
|
||||
ret: Box::new(Type::str_()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
"is_nan" => Type::Fn {
|
||||
params: vec![Type::float()],
|
||||
|
||||
@@ -1396,6 +1396,31 @@ returns that are not `Type::Con` (e.g. unresolved type vars on
|
||||
a polymorphic call's pre-monomorphisation site; the
|
||||
monomorphised copies resolve to concrete drop fns).
|
||||
|
||||
#### Arg-position policy for compound AST nodes
|
||||
|
||||
The uniqueness and linearity passes walk arguments of compound
|
||||
nodes with a fixed `Position` policy. For ownership-bearing nodes:
|
||||
|
||||
| Node | Arg position | Reason |
|
||||
|----------------------|--------------|---------------------------------------------------------------------------------------|
|
||||
| `Term::Ctor.args[*]` | Consume | constructor packs values into the cell; the cell owns them afterwards |
|
||||
| `Term::Do.args[*]` | Borrow | effect-op observes its arguments; the caller still owns whatever pointer it passed in |
|
||||
|
||||
The two policies are language rules, not per-op annotations. They
|
||||
do not appear as fields on `EffectOpSig` or `Ctor`; the AST node
|
||||
kind itself carries the default. The walkers that read this policy
|
||||
live at `crates/ailang-check/src/uniqueness.rs` and
|
||||
`crates/ailang-check/src/linearity.rs` (matched arms in both).
|
||||
|
||||
The Do = Borrow rule pairs with the `ret_mode == Own` letbinder-
|
||||
trackability rule above: when a built-in such as `int_to_str` is
|
||||
declared `ret_mode: Own` and its result is fed into an effect-op
|
||||
(`io/print_str s`), the let-binder is RC-tracked for scope-close
|
||||
drop *and* the effect-op does not consume it — the slab is freed
|
||||
exactly once at scope close, never zero-times (RC leak under the
|
||||
old Consume rule, which silenced the scope-close drop) and never
|
||||
twice (double-free under a hypothetical Consume + scope-close).
|
||||
|
||||
**What this widening does NOT do.**
|
||||
|
||||
- Does not change the canonical hash. `param_modes` /
|
||||
|
||||
@@ -80,3 +80,9 @@ Cross-model authoring test landed. First run (Qwen3-Coder-Next, 4 tasks, two bli
|
||||
## 2026-05-12 — Cross-model authoring test: second subject added
|
||||
|
||||
Cross-model authoring test now has a second subject. Ran CodeLlama alongside Qwen on the same four tasks, fresh runs both. Both models point the same way: textual form is cheaper than structured form on tokens (61–80% of structured form's cost across the two models) and reaches working code at least as often (Qwen ties on this run; CodeLlama strictly dominates 2/4 vs 0/4). Three of four first-attempt successes across the two models are textual. A feedback-asymmetry bug surfaced during planning — the structured form was silently losing its underlying parse error on the way back to the model — and got fixed in the same milestone, so both subjects ran with the fix in place. Not a universal verdict yet (would need three or more models with repetitions), but the second data point points the same way as the first. Milestone closed; queue back to user-direction.
|
||||
|
||||
## 2026-05-12 — Strings produced at runtime now release cleanly
|
||||
|
||||
`int_to_str`, `float_to_str`, and similar built-in primitives that produce a freshly allocated string at runtime now participate in reference counting like every other heap value. A string passed into a print call is now treated as borrowed, not consumed, so it is freed exactly once when it leaves scope. Earlier programs that converted a number to a string and printed it leaked the string buffer at program exit; that no longer happens.
|
||||
|
||||
A side effect: a function parameter declared `(own T)` whose only in-body use was passing it into a print call will now correctly be flagged as over-strict — that annotation should be relaxed to `(borrow T)` (or removed). No current example programs are affected.
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# iter eob.1 — Effect-op args walked as Borrow; heap-Str RC discipline closes
|
||||
|
||||
**Date:** 2026-05-12
|
||||
**Started from:** b5b0c2d7dcd8bec1819801257de7731edc542e0f
|
||||
**Status:** DONE
|
||||
**Tasks completed:** 7 of 7
|
||||
|
||||
## Summary
|
||||
|
||||
Land the language rule that `Term::Do.args[*]` are walked in
|
||||
`Position::Borrow` by the uniqueness and linearity passes, plus the
|
||||
two heap-Str-specific shape fixes (`ret_mode: Own` for `int_to_str`
|
||||
/ `float_to_str`; Str carve-out in the App arm of
|
||||
`drop_symbol_for_binder`) that make the existing RED tests at
|
||||
`crates/ail/tests/e2e.rs` flip to GREEN under `--alloc=rc`. Both
|
||||
arg-position rules (Ctor = Consume, Do = Borrow) are anchored in
|
||||
DESIGN.md §Decision 10. The heap-str-abi milestone closes here:
|
||||
WhatsNew entry shipped, roadmap P1 entry checked off, downstream
|
||||
Post-22-Prelude `depends on:` line removed.
|
||||
|
||||
The five plan layers landed first-shot in lockstep — no review
|
||||
re-loops fired on any of the 7 tasks. The pre-existing RED tests
|
||||
(`int_to_str_drop_balances_rc_stats`,
|
||||
`str_field_in_adt_drops_heap_str_correctly`) flipped to GREEN with
|
||||
the predicted concrete numbers (`allocs == 1, frees == 1, live == 0`
|
||||
and `allocs == 2, frees == 2, live == 0`). Two new positive tests
|
||||
pin the rule's coverage at the primitive boundary (Int through
|
||||
`io/print_int`: zero RC traffic) and at the linearity boundary
|
||||
(heap-Str through two sequential `io/print_str` calls: allocs == 1,
|
||||
frees == 1, live == 0). Workspace test --workspace + cross_lang.py
|
||||
+ compile_check.py all green. check.py shows the same noisy
|
||||
latency-max + bump_s cluster the audit-cma and audit-ms journals
|
||||
both left pristine ("not coupled to milestone, baseline left
|
||||
untouched for second consecutive audit") — same precedent applies
|
||||
here; baseline left as-is.
|
||||
|
||||
## Per-task notes
|
||||
|
||||
- iter eob.1.1: Term::Do arg-walks flip Consume → Borrow in both
|
||||
uniqueness.rs:289 and linearity.rs:506; linearity.rs:42 doc
|
||||
comment split into two bullets matching the new code shape.
|
||||
Inline clarifying comment added on the linearity-side flip
|
||||
mirroring the Ctor-arm convention. cargo build + workspace
|
||||
check-crate tests green.
|
||||
- iter eob.1.2: builtins.rs `float_to_str` (:200) + `int_to_str`
|
||||
(:210) and synth.rs `float_to_str` (:172) + `int_to_str` (:179)
|
||||
all flip `ret_mode: Implicit` → `ret_mode: Own` in lockstep.
|
||||
Workspace build clean. Target test now reports the predicted
|
||||
link-error shape "undefined value @drop_int_to_str_drop_rc_Str"
|
||||
— exactly the gap Task 3 closes.
|
||||
- iter eob.1.3: drop.rs App arm of `drop_symbol_for_binder` gets
|
||||
the Str carve-out `if name == "Str" return ailang_rc_dec`,
|
||||
symmetric to `field_drop_call`'s existing Str arm. The
|
||||
comment block mirrors `field_drop_call`'s wording. cargo build
|
||||
clean.
|
||||
- iter eob.1.4 (observational): both RED tests flip to GREEN with
|
||||
the predicted concrete numbers (allocs == 1, frees == 1 / allocs
|
||||
== 2, frees == 2; live == 0 on both). Full e2e suite (79 tests)
|
||||
all green — no other fixture's RC trace shifted unexpectedly.
|
||||
- iter eob.1.5: two new fixtures
|
||||
(`examples/int_to_print_int_borrow.ail.json`,
|
||||
`examples/heap_str_repeated_print_borrow.ail.json`) + two new
|
||||
e2e tests (`int_arg_to_effect_op_does_not_rc_track`,
|
||||
`heap_str_repeated_print_balances_rc_stats`) appended near
|
||||
e2e.rs:2625. Both new tests PASS first-shot with exact predicted
|
||||
RC stats.
|
||||
- iter eob.1.6: DESIGN.md §Decision 10 gets a new subsection
|
||||
`#### Arg-position policy for compound AST nodes` after the
|
||||
`ret_mode` block. The two arg-position rules (Ctor = Consume,
|
||||
Do = Borrow) are anchored together as language rules, not
|
||||
per-op annotations. One added linking paragraph explains how
|
||||
the Do = Borrow rule cooperates with the `ret_mode == Own`
|
||||
letbinder-trackability rule above to balance heap-Str RC
|
||||
discipline — within the plan's "Boss judgement on placement"
|
||||
carve-out.
|
||||
- iter eob.1.7: WhatsNew.md entry appended at bottom (user-facing
|
||||
wording, no internals). roadmap.md heap-Str ABI entry flipped
|
||||
`[ ]` → `[x]`. Post-22 Prelude `depends on:` line removed (the
|
||||
only dep was Heap-Str, which has shipped). cargo test workspace
|
||||
+ cross_lang.py + compile_check.py all green. check.py noisy
|
||||
latency cluster left pristine per audit-cma/audit-ms precedent.
|
||||
|
||||
## Concerns
|
||||
|
||||
(none — all 7 tasks DONE without review re-loops)
|
||||
|
||||
## Known debt
|
||||
|
||||
- check.py shows recurring noisy `latency.implicit_at_rc.max_us`
|
||||
/ `latency.explicit_at_rc.max_us` tail-jitter, plus
|
||||
`throughput.bench_list_sum.bump_s` swinging +11-15% across
|
||||
back-to-back runs (with reciprocal `gc_over_bump` improvement
|
||||
on the same fixture). Neither family is reachable from this
|
||||
iter's changes (bump_s on bench_list_sum has no effect-ops; the
|
||||
list-sum hot path has no int_to_str / float_to_str calls). The
|
||||
audit-cma and audit-ms journals already document the same
|
||||
noise cluster as "not coupled to milestone, baseline left
|
||||
pristine for two consecutive audits" — same precedent applies;
|
||||
baseline untouched.
|
||||
|
||||
## Files touched
|
||||
|
||||
Code:
|
||||
- crates/ailang-check/src/uniqueness.rs (Term::Do Consume → Borrow)
|
||||
- crates/ailang-check/src/linearity.rs (Term::Do Consume → Borrow + doc-block split)
|
||||
- crates/ailang-check/src/builtins.rs (int_to_str / float_to_str ret_mode → Own)
|
||||
- crates/ailang-codegen/src/synth.rs (int_to_str / float_to_str ret_mode → Own)
|
||||
- crates/ailang-codegen/src/drop.rs (App-arm Str carve-out)
|
||||
|
||||
Tests + fixtures:
|
||||
- crates/ail/tests/e2e.rs (2 new positive tests appended)
|
||||
- examples/int_to_print_int_borrow.ail.json (new)
|
||||
- examples/heap_str_repeated_print_borrow.ail.json (new)
|
||||
|
||||
Docs:
|
||||
- docs/DESIGN.md (Decision 10: arg-position policy subsection)
|
||||
- docs/WhatsNew.md (heap-str-abi milestone close entry)
|
||||
- docs/roadmap.md (Heap-Str ABI P1 closed; Post-22 Prelude
|
||||
depends-on line removed)
|
||||
|
||||
## Stats
|
||||
|
||||
bench/orchestrator-stats/2026-05-12-iter-eob.1.json
|
||||
@@ -35,3 +35,4 @@
|
||||
- 2026-05-12 — iter hs.2: static-Str layout retrofit — drop the `UINT64_MAX` sentinel slot from packed-struct globals (`<{ i64, i64, [N x i8] }>` → `<{ i64, [N x i8] }>`); IR-`Str` pointer now lands on the `len`-field at struct-index 0 (was 1) via constexpr-GEP `i32 0, i32 0`; `+8` consumer-side GEPs at the four C-API callsites invariant across the switch; renamed `..._sentinel_and_len` test to `..._with_len`; updated 5 sites in `crates/ailang-codegen/src/lib.rs` (3 format strings + 2 doc-comments); regenerated `hello.ll` snapshot; full `cargo test --workspace`, cross_lang.py, compile_check.py, check.py all green; static-Str globals are now 8 bytes smaller per literal → 2026-05-12-iter-hs.2.md
|
||||
- 2026-05-12 — iter hs.3: heap-Str runtime additions — three new symbols appended to `runtime/str.c` (`static str_alloc(uint64_t)` slab helper, `ailang_int_to_str(int64_t)`, `ailang_float_to_str(double)`); supporting `<stdint.h>`/`<stdio.h>`/`<stdlib.h>` includes + `extern void *ailang_rc_alloc(size_t)` declaration; the extern was promoted from plain-external to `__attribute__((weak))` after the first regression sweep surfaced 11 e2e link failures under `--alloc=gc`/`bump` (public `T` symbols pull in their transitive callees regardless of upstream usage; rc.c is not linked under gc/bump in hs.3 — repair makes str.c link-safe in isolation, no-op once hs.4 lands the unconditional rc.c link); cargo test --workspace + cross_lang.py + compile_check.py + check.py all green on re-sweep; no IR-side caller wired yet (hs.4) → 2026-05-12-iter-hs.3.md
|
||||
- 2026-05-12 — iter hs.4: IR + checker + linker wiring — `int_to_str : (Int) -> Str` installed in checker + synth.rs lockstep partner; IR-header preamble unconditionally declares both `@ailang_int_to_str` / `@ailang_float_to_str`; `Emitter::lower_app` gets new `int_to_str` arm and replaces `float_to_str`'s `CodegenError::Internal` with the actual call emission; `is_static_callee` whitelist extends to `int_to_str`; `runtime/rc.c` hoisted out of `AllocStrategy::Rc` arm to the unconditional link path (the `__attribute__((weak))` on str.c's `ailang_rc_alloc` extern becomes the documented no-op); 2 new IR-shape pins + 4 new E2E (2 stdout-smoke + 2 RC-stats) + 4 `.ail.json` fixtures; `drop.rs` Str-arm comment refreshed to dual-realisation framing; 5 IR snapshots regen for the 2 new declare lines. Status: DONE_WITH_CONCERNS — heap-Str RC-discipline incomplete (slabs leak at program end) because plan's literal `ret_mode: Implicit` interacts with uniqueness analyser walking `Term::Do` args as `Position::Consume`. Test asserts weakened from `allocs == frees && live == 0` to `allocs >= 1`. Substantive fix requires spec-level effect-op arg-mode decision (deferred, bounce-back to user) → 2026-05-12-iter-hs.4.md
|
||||
- 2026-05-12 — iter eob.1: Effect-op args walked as Borrow (uniqueness.rs + linearity.rs + linearity.rs doc-comment); heap-Str RC discipline closes (int_to_str / float_to_str `ret_mode: Implicit` → `Own` at 4 lockstep sites across builtins.rs + synth.rs; `drop_symbol_for_binder` App-arm gets `Str` carve-out symmetric to `field_drop_call`'s existing one); 2 pre-existing RED tests at e2e.rs (commit 592d87b) flipped to GREEN unchanged with predicted concrete numbers (`allocs==1,frees==1,live==0` and `allocs==2,frees==2,live==0`); 2 new positive tests + fixtures (primitive-Int through `io/print_int`: zero RC traffic; heap-Str repeated borrow through 2× `io/print_str`: `allocs==1,frees==1,live==0`); DESIGN.md §Decision 10 anchors both arg-position rules together (Ctor=Consume, Do=Borrow, plus a paragraph linking Do=Borrow to ret_mode==Own letbinder-trackability); WhatsNew entry shipped, roadmap P1 `[milestone] Heap-Str ABI` checked off, Post-22-Prelude `depends on:` line removed; 7/7 tasks first-shot, zero review re-loops; lint side-effect surface (over-strict-mode on (own T) params used solely via effect-ops) was empty for the current corpus → 2026-05-12-iter-eob.1.md
|
||||
|
||||
+1
-3
@@ -41,7 +41,7 @@ context. Pick the next milestone from P1.)_
|
||||
|
||||
## P1 — Next
|
||||
|
||||
- [ ] **\[milestone\]** Heap-`Str` ABI — runtime infrastructure for
|
||||
- [x] **\[milestone\]** Heap-`Str` ABI — runtime infrastructure for
|
||||
malloc-backed, refcounted `Str` values alongside the existing
|
||||
static `@.str_*` globals. Today the `Str` path is static-only
|
||||
(DESIGN.md §"Float semantics" notes this explicitly), so any
|
||||
@@ -66,8 +66,6 @@ context. Pick the next milestone from P1.)_
|
||||
`print` through `Show.show`. Originally queued as 22b.4 in the
|
||||
typeclass milestone, kept on hold pending demand and the
|
||||
heap-`Str` prerequisite.
|
||||
- depends on: Heap-`Str` ABI (above). Without it, `Show Int`
|
||||
cannot allocate a `Str` to return.
|
||||
- context: brainstorm 2026-05-12 (iter 23.5 wrap).
|
||||
|
||||
## P2 — Medium-term
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "heap_str_repeated_print_borrow",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "main",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [],
|
||||
"ret": { "k": "con", "name": "Unit" },
|
||||
"effects": ["IO"]
|
||||
},
|
||||
"params": [],
|
||||
"doc": "Iter eob.1: heap-Str passed to io/print_str twice in sequence. Under the new rule (Term::Do args = Borrow), the second call no longer triggers use-after-consume; the linearity check accepts the program. Under --alloc=rc + AILANG_RC_STATS=1 the slab allocates once and is freed once at scope close (allocs == 1, frees == 1, live == 0).",
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "s",
|
||||
"value": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "int_to_str" },
|
||||
"args": [{ "t": "lit", "lit": { "kind": "int", "value": 42 } }]
|
||||
},
|
||||
"body": {
|
||||
"t": "seq",
|
||||
"lhs": {
|
||||
"t": "do",
|
||||
"op": "io/print_str",
|
||||
"args": [{ "t": "var", "name": "s" }]
|
||||
},
|
||||
"rhs": {
|
||||
"t": "do",
|
||||
"op": "io/print_str",
|
||||
"args": [{ "t": "var", "name": "s" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "int_to_print_int_borrow",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "main",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [],
|
||||
"ret": { "k": "con", "name": "Unit" },
|
||||
"effects": ["IO"]
|
||||
},
|
||||
"params": [],
|
||||
"doc": "Iter eob.1: primitive Int passed to io/print_int. The rule that Term::Do args are Borrow has no observable RC effect here because Int is unboxed; the test pins that no spurious bookkeeping appears (allocs == 0, frees == 0, live == 0).",
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "n",
|
||||
"value": { "t": "lit", "lit": { "kind": "int", "value": 7 } },
|
||||
"body": {
|
||||
"t": "do",
|
||||
"op": "io/print_int",
|
||||
"args": [{ "t": "var", "name": "n" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user