Iter 18c.1: Term::Clone schema (no inc emission yet)
Adds (clone X) as a new Term variant. Form-A:
(let p (expensive_fn x)
(do consume_a (clone p)) ; explicit RC inc — needs 18c.3
(do consume_b p))
Schema floor for the LLM-aware RC design (Decision 10): explicit
clone is the author-visible alternative to implicit sharing. In
18c.1 the variant is purely additive — typechecker treats it as
identity (same type as inner), codegen lowers it to the inner
term's IR with no extra calls. Iter 18c.3 will replace the
codegen identity-arm with a single call:
Term::Clone { value } =>
let v = self.lower_term(value, ...)?;
self.emit("call void @ailang_rc_inc(ptr {v})");
Ok(v)
Match-arm count: ~10 sites across 6 files, all mechanical
structural recursion through `value`. The codegen `lower_term`
arm carries an explicit comment marking the 18c.3 emission seam.
Hash invariant: `(clone X)` uses a new serde tag "clone" that no
pre-18c.1 fixture contains. Every existing fixture's canonical
JSON is bit-identical (git diff examples/ empty).
New fixture examples/clone_demo.{ailx,ail.json}:
(let x 42 (do io/print_int (clone x)))
Stdout 42 under both --alloc=gc and --alloc=rc.
Tail-position propagates through clone (verify_tail_positions
treats `(clone tail-call)` as itself a tail call), per the
identity-of-clone semantics in 18c.1.
Verified:
- cargo build --workspace clean
- cargo test --workspace green: 52 e2e (+1), 14 surface parse
(+2 for clone tests), all other buckets unchanged
- ail render round-trip exact
- clone_demo --alloc=gc → 42; --alloc=rc → 42
This commit is contained in:
@@ -969,6 +969,11 @@ fn walk_term(
|
||||
scope.remove(name);
|
||||
}
|
||||
}
|
||||
Term::Clone { value } => {
|
||||
// Iter 18c.1: clone is identity for dependency-walk
|
||||
// purposes. The wrapper introduces no new symbols.
|
||||
walk_term(value, out, builtins, scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1319,6 +1319,18 @@ fn alloc_rc_produces_same_stdout_as_gc() {
|
||||
assert_eq!(stdout_rc.trim(), "42");
|
||||
}
|
||||
|
||||
/// Iter 18c.1: `Term::Clone` is a pure schema addition. In 18c.1 the
|
||||
/// wrapper is identity for both typechecker (same type as inner) and
|
||||
/// codegen (same SSA reg, no extra IR) — programs that contain
|
||||
/// `(clone X)` produce the same stdout they would have produced
|
||||
/// without the wrapper. Iter 18c.3 will replace the codegen
|
||||
/// passthrough with `call void @ailang_rc_inc` under `--alloc=rc`.
|
||||
#[test]
|
||||
fn clone_demo_is_identity_in_18c1() {
|
||||
let stdout = build_and_run("clone_demo.ail.json");
|
||||
assert_eq!(stdout.trim(), "42");
|
||||
}
|
||||
|
||||
/// Iter 18b: extends `alloc_rc_produces_same_stdout_as_gc` to a larger
|
||||
/// fixture (`std_list_demo`) so more allocation sites — folds, maps,
|
||||
/// cross-module ctors — are exercised under `--alloc=rc`. Same
|
||||
|
||||
@@ -1125,6 +1125,11 @@ pub fn verify_tail_positions(t: &Term, is_tail: bool) -> Result<()> {
|
||||
verify_tail_positions(body, false)?;
|
||||
verify_tail_positions(in_term, is_tail)
|
||||
}
|
||||
Term::Clone { value } => {
|
||||
// Iter 18c.1: clone is identity. Tail-position propagates
|
||||
// through unchanged — `(clone tail-call)` is a tail call.
|
||||
verify_tail_positions(value, is_tail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1626,6 +1631,13 @@ pub(crate) fn synth(
|
||||
}
|
||||
in_ty
|
||||
}
|
||||
Term::Clone { value } => {
|
||||
// Iter 18c.1: `(clone X)` typechecks identically to `X`.
|
||||
// No constraint generated, no environment change. The
|
||||
// wrapper records author intent for the future RC inc/dec
|
||||
// emission pass (18c.3); typing is pure passthrough.
|
||||
synth(value, env, locals, effects, in_def, subst, counter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -357,6 +357,10 @@ impl<'a> Lifter<'a> {
|
||||
lhs: Box::new(self.lift_in_term(lhs, locals, in_def)?),
|
||||
rhs: Box::new(self.lift_in_term(rhs, locals, in_def)?),
|
||||
}),
|
||||
Term::Clone { value } => Ok(Term::Clone {
|
||||
// Iter 18c.1: structural recursion through the wrapper.
|
||||
value: Box::new(self.lift_in_term(value, locals, in_def)?),
|
||||
}),
|
||||
Term::LetRec { name, ty, params, body, in_term } => {
|
||||
// Iter 16b.3: post-order traversal — lift any inner
|
||||
// LetRecs first. Within the body's scope, `name` and
|
||||
@@ -691,6 +695,8 @@ fn contains_any_letrec(m: &Module) -> bool {
|
||||
Term::Lam { body, .. } => term_has_letrec(body),
|
||||
Term::Seq { lhs, rhs } => term_has_letrec(lhs) || term_has_letrec(rhs),
|
||||
Term::LetRec { .. } => true,
|
||||
// Iter 18c.1: identity passthrough for the letrec-detection scan.
|
||||
Term::Clone { value } => term_has_letrec(value),
|
||||
}
|
||||
}
|
||||
for def in &m.defs {
|
||||
|
||||
@@ -179,6 +179,11 @@ fn walk(t: &Term, out: &mut NonEscapeSet) {
|
||||
walk(body, out);
|
||||
walk(in_term, out);
|
||||
}
|
||||
Term::Clone { value } => {
|
||||
// Iter 18c.1: clone is identity at IR level — only walk
|
||||
// sub-terms looking for Let-allocation candidates.
|
||||
walk(value, out);
|
||||
}
|
||||
Term::Lit { .. } | Term::Var { .. } => {}
|
||||
}
|
||||
}
|
||||
@@ -338,6 +343,12 @@ fn escapes(t: &Term, tainted: &BTreeSet<String>, in_tail: bool) -> bool {
|
||||
// declare escape.
|
||||
true
|
||||
}
|
||||
Term::Clone { value } => {
|
||||
// Iter 18c.1: clone is identity. The wrapper's escape
|
||||
// verdict is exactly that of its inner term, threaded
|
||||
// through with the same `in_tail` context.
|
||||
escapes(value, tainted, in_tail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,6 +444,10 @@ fn collect_free_vars(t: &Term, bound: &mut BTreeSet<String>, out: &mut BTreeSet<
|
||||
collect_free_vars(body, bound, out);
|
||||
collect_free_vars(in_term, bound, out);
|
||||
}
|
||||
Term::Clone { value } => {
|
||||
// Iter 18c.1: clone is identity for free-var collection.
|
||||
collect_free_vars(value, bound, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1154,6 +1154,11 @@ impl<'a> Emitter<'a> {
|
||||
// here is a bug.
|
||||
unreachable!("Term::LetRec eliminated by desugar")
|
||||
}
|
||||
Term::Clone { value } => {
|
||||
// Iter 18c.1: clone is identity. Iter 18c.3 will emit
|
||||
// `call void @ailang_rc_inc` here under --alloc=rc.
|
||||
self.lower_term(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2308,6 +2313,11 @@ impl<'a> Emitter<'a> {
|
||||
// Iter 16b.1: eliminated by desugar before codegen.
|
||||
unreachable!("Term::LetRec eliminated by desugar")
|
||||
}
|
||||
Term::Clone { value } => {
|
||||
// Iter 18c.1: clone is identity for capture analysis —
|
||||
// free vars of `(clone X)` are exactly the free vars of `X`.
|
||||
Self::collect_captures(value, bound, captures, captures_set, builtins, top_level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2814,6 +2824,10 @@ impl<'a> Emitter<'a> {
|
||||
// Iter 16b.1: eliminated by desugar before codegen.
|
||||
unreachable!("Term::LetRec eliminated by desugar")
|
||||
}
|
||||
Term::Clone { value } => {
|
||||
// Iter 18c.1: clone is identity — same type as inner.
|
||||
self.synth_with_extras(value, extras)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3203,6 +3217,10 @@ fn apply_subst_to_term(t: &Term, subst: &BTreeMap<String, Type>) -> Term {
|
||||
// monomorphisation pass runs.
|
||||
unreachable!("Term::LetRec eliminated by desugar")
|
||||
}
|
||||
Term::Clone { value } => Term::Clone {
|
||||
// Iter 18c.1: structural recursion through the wrapper.
|
||||
value: Box::new(apply_subst_to_term(value, subst)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -291,6 +291,15 @@ pub enum Term {
|
||||
lhs: Box<Term>,
|
||||
rhs: Box<Term>,
|
||||
},
|
||||
/// Iter 18c.1: explicit RC clone. Lowers identically to its inner
|
||||
/// term in 18c.1; in 18c.3 the codegen will emit
|
||||
/// `call void @ailang_rc_inc(ptr %v)` before returning `%v` under
|
||||
/// `--alloc=rc`. The variant is additive: `(clone X)` round-trips
|
||||
/// through every pre-18c.1 fixture without their hashes changing
|
||||
/// because none of them use the new tag.
|
||||
Clone {
|
||||
value: Box<Term>,
|
||||
},
|
||||
}
|
||||
|
||||
/// One arm of a [`Term::Match`].
|
||||
|
||||
@@ -320,6 +320,10 @@ fn collect_used_in_term(t: &Term, used: &mut BTreeSet<String>) {
|
||||
collect_used_in_term(body, used);
|
||||
collect_used_in_term(in_term, used);
|
||||
}
|
||||
Term::Clone { value } => {
|
||||
// Iter 18c.1: identity for the used-name walk.
|
||||
collect_used_in_term(value, used);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,6 +506,11 @@ impl Desugarer {
|
||||
lhs: Box::new(self.desugar_term(lhs, scope)),
|
||||
rhs: Box::new(self.desugar_term(rhs, scope)),
|
||||
},
|
||||
Term::Clone { value } => Term::Clone {
|
||||
// Iter 18c.1: pure structural recursion through the
|
||||
// wrapper. Same pattern as `Term::Let`'s value branch.
|
||||
value: Box::new(self.desugar_term(value, scope)),
|
||||
},
|
||||
Term::LetRec { name, ty, params, body, in_term } => {
|
||||
// Iter 16b.1: lift to a synthetic top-level fn (no-capture).
|
||||
// Iter 16b.2: extend the lift to the path-1 safe subset —
|
||||
@@ -1109,6 +1118,10 @@ pub fn free_vars_in_term(t: &Term, bound: &BTreeSet<String>, out: &mut BTreeSet<
|
||||
b_in.insert(name.clone());
|
||||
free_vars_in_term(in_term, &b_in, out);
|
||||
}
|
||||
Term::Clone { value } => {
|
||||
// Iter 18c.1: `(clone X)` has the same free vars as `X`.
|
||||
free_vars_in_term(value, bound, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1244,6 +1257,10 @@ pub fn subst_var(t: &Term, from: &str, to: &str) -> Term {
|
||||
in_term: Box::new(in_rw),
|
||||
}
|
||||
}
|
||||
Term::Clone { value } => Term::Clone {
|
||||
// Iter 18c.1: structural recursion through the wrapper.
|
||||
value: Box::new(subst_var(value, from, to)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1356,6 +1373,10 @@ pub fn subst_call_with_extras(t: &Term, name: &str, lifted: &str, extras: &[Stri
|
||||
body: Box::new(subst_call_with_extras(body, name, lifted, extras)),
|
||||
in_term: Box::new(subst_call_with_extras(in_term, name, lifted, extras)),
|
||||
},
|
||||
Term::Clone { value } => Term::Clone {
|
||||
// Iter 18c.1: structural recursion through the wrapper.
|
||||
value: Box::new(subst_call_with_extras(value, name, lifted, extras)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1404,6 +1425,8 @@ pub fn find_non_callee_use(t: &Term, name: &str) -> Option<Term> {
|
||||
}
|
||||
Term::LetRec { body, in_term, .. } => find_non_callee_use(body, name)
|
||||
.or_else(|| find_non_callee_use(in_term, name)),
|
||||
// Iter 18c.1: clone is identity for the non-callee scan.
|
||||
Term::Clone { value } => find_non_callee_use(value, name),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1444,6 +1467,7 @@ mod tests {
|
||||
Term::LetRec { body, in_term, .. } => {
|
||||
any_nested_ctor(body) || any_nested_ctor(in_term)
|
||||
}
|
||||
Term::Clone { value } => any_nested_ctor(value),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1467,6 +1491,7 @@ mod tests {
|
||||
Term::Lam { body, .. } => any_let_rec(body),
|
||||
Term::Seq { lhs, rhs } => any_let_rec(lhs) || any_let_rec(rhs),
|
||||
Term::LetRec { .. } => true,
|
||||
Term::Clone { value } => any_let_rec(value),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2566,6 +2591,7 @@ mod tests {
|
||||
Term::LetRec { body, in_term, .. } => {
|
||||
any_lit_pattern(body) || any_lit_pattern(in_term)
|
||||
}
|
||||
Term::Clone { value } => any_lit_pattern(value),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
//! term ::= var-ref | int-lit | str-lit | bool-lit | unit-lit
|
||||
//! | app-term | tail-app-term | match-term | ctor-term
|
||||
//! | do-term | tail-do-term | seq-term | lam-term | if-term
|
||||
//! | let-term | let-rec-term
|
||||
//! | let-term | let-rec-term | clone-term
|
||||
//! var-ref ::= ident ; reserved: true/false → bool-lit
|
||||
//! int-lit ::= integer ; numeric atom
|
||||
//! str-lit ::= string ; string atom
|
||||
@@ -62,6 +62,7 @@
|
||||
//! type-attr
|
||||
//! body-attr
|
||||
//! "(" "in" term ")" ")"
|
||||
//! clone-term ::= "(" "clone" term ")" ; Iter 18c.1
|
||||
//!
|
||||
//! pattern ::= pat-var | pat-ctor | pat-lit | pat-wild
|
||||
//! pat-var ::= ident
|
||||
@@ -752,6 +753,7 @@ impl<'a> Parser<'a> {
|
||||
"if" => self.parse_if(),
|
||||
"let" => self.parse_let(),
|
||||
"let-rec" => self.parse_let_rec(),
|
||||
"clone" => self.parse_clone(),
|
||||
other => {
|
||||
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
||||
Err(ParseError::Production {
|
||||
@@ -759,7 +761,7 @@ impl<'a> Parser<'a> {
|
||||
message: format!(
|
||||
"unknown term head `{other}`; expected one of \
|
||||
`app`, `tail-app`, `lam`, `let`, `let-rec`, `if`, `match`, `do`, \
|
||||
`tail-do`, `seq`, `term-ctor`, `lit-unit`"
|
||||
`tail-do`, `seq`, `term-ctor`, `clone`, `lit-unit`"
|
||||
),
|
||||
pos,
|
||||
})
|
||||
@@ -1033,6 +1035,37 @@ impl<'a> Parser<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Iter 18c.1: `(clone TERM)` — explicit RC clone wrapper. In 18c.1
|
||||
/// the wrapper is identity for typechecker and codegen; in 18c.3
|
||||
/// the codegen will emit `call void @ailang_rc_inc` before yielding
|
||||
/// the inner value's SSA reg under `--alloc=rc`.
|
||||
fn parse_clone(&mut self) -> Result<Term, ParseError> {
|
||||
self.expect_lparen("clone-term")?;
|
||||
self.expect_keyword("clone")?;
|
||||
// Exactly one inner term, then `)`.
|
||||
if matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
|
||||
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
||||
return Err(ParseError::Production {
|
||||
production: "clone-term",
|
||||
message: "clone expects exactly one term argument".into(),
|
||||
pos,
|
||||
});
|
||||
}
|
||||
let inner = self.parse_term()?;
|
||||
if !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
|
||||
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
||||
return Err(ParseError::Production {
|
||||
production: "clone-term",
|
||||
message: "clone expects exactly one term argument".into(),
|
||||
pos,
|
||||
});
|
||||
}
|
||||
self.expect_rparen("clone-term")?;
|
||||
Ok(Term::Clone {
|
||||
value: Box::new(inner),
|
||||
})
|
||||
}
|
||||
|
||||
// ---- patterns -------------------------------------------------------
|
||||
|
||||
fn parse_pattern(&mut self) -> Result<Pattern, ParseError> {
|
||||
@@ -1231,6 +1264,54 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 18c.1: `(clone X)` parses to `Term::Clone { value: Var "x" }`.
|
||||
#[test]
|
||||
fn parses_clone_wraps_inner_term() {
|
||||
let m = parse(
|
||||
r#"
|
||||
(module m
|
||||
(fn id
|
||||
(type (fn-type (params (con Int)) (ret (con Int))))
|
||||
(params x)
|
||||
(body (clone x))))
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let body = match &m.defs[0] {
|
||||
Def::Fn(fd) => &fd.body,
|
||||
_ => panic!("expected fn"),
|
||||
};
|
||||
match body {
|
||||
Term::Clone { value } => match value.as_ref() {
|
||||
Term::Var { name } => assert_eq!(name, "x"),
|
||||
other => panic!("expected Var inside Clone, got {other:?}"),
|
||||
},
|
||||
other => panic!("expected Clone, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter 18c.1: `(clone)` with no inner term is rejected with a
|
||||
/// clear message.
|
||||
#[test]
|
||||
fn rejects_clone_without_argument() {
|
||||
let err = parse(
|
||||
r#"
|
||||
(module m
|
||||
(fn id
|
||||
(type (fn-type (params (con Int)) (ret (con Int))))
|
||||
(params x)
|
||||
(body (clone))))
|
||||
"#,
|
||||
)
|
||||
.err()
|
||||
.expect("parse should fail");
|
||||
let msg = format!("{err}");
|
||||
assert!(
|
||||
msg.contains("clone expects exactly one term argument"),
|
||||
"diagnostic should explain clone's arity, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 16b.1: minimal `(let-rec ...)` round-trips through the
|
||||
/// parser into a `Term::LetRec` whose `name`, `params`, `body` and
|
||||
/// `in_term` line up with the source.
|
||||
|
||||
@@ -362,6 +362,14 @@ fn write_term(out: &mut String, t: &Term, level: usize) {
|
||||
write_term(out, rhs, level);
|
||||
out.push(')');
|
||||
}
|
||||
Term::Clone { value } => {
|
||||
// Iter 18c.1: print as `(clone <inner>)`. The wrapper is
|
||||
// identity at typecheck/codegen in 18c.1; only authored
|
||||
// intent is recorded for the future inc/dec emission pass.
|
||||
out.push_str("(clone ");
|
||||
write_term(out, value, level);
|
||||
out.push(')');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"defs":[{"body":{"body":{"args":[{"t":"clone","value":{"name":"x","t":"var"}}],"op":"io/print_int","t":"do"},"name":"x","t":"let","value":{"lit":{"kind":"int","value":42},"t":"lit"}},"doc":"Use `(clone x)` on a let-bound Int; print it.","kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"clone_demo","schema":"ailang/v0"}
|
||||
@@ -0,0 +1,26 @@
|
||||
; Iter 18c.1 — `(clone X)` schema floor.
|
||||
;
|
||||
; The `clone` form is an additive `Term` variant. In 18c.1 it is
|
||||
; identity for both the typechecker and the codegen — `(clone X)`
|
||||
; has the same type as `X` and lowers to the same SSA reg with no
|
||||
; extra IR. Iter 18c.3 will replace the codegen passthrough with
|
||||
; `call void @ailang_rc_inc(ptr %v)` under `--alloc=rc`; for now
|
||||
; the wrapper's only role is to record author intent.
|
||||
;
|
||||
; This fixture wraps a let-bound `Int` in `(clone x)` at its single
|
||||
; use site. The inner term is a primitive value (Int) so RC inc is
|
||||
; meaningless even in 18c.3 — the point is purely that the schema
|
||||
; round-trips and the binary still produces the expected `42`.
|
||||
;
|
||||
; Expected stdout:
|
||||
; 42
|
||||
|
||||
(module clone_demo
|
||||
|
||||
(fn main
|
||||
(doc "Use `(clone x)` on a let-bound Int; print it.")
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
(params)
|
||||
(body
|
||||
(let x 42
|
||||
(do io/print_int (clone x))))))
|
||||
Reference in New Issue
Block a user