diff --git a/crates/ail/src/main.rs b/crates/ail/src/main.rs index 6294809..4e72145 100644 --- a/crates/ail/src/main.rs +++ b/crates/ail/src/main.rs @@ -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); + } } } diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index 54fe007..b0de699 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -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 diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 2026b5e..495fbc6 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -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) + } } } diff --git a/crates/ailang-check/src/lift.rs b/crates/ailang-check/src/lift.rs index 85980d4..c8e3dbe 100644 --- a/crates/ailang-check/src/lift.rs +++ b/crates/ailang-check/src/lift.rs @@ -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 { diff --git a/crates/ailang-codegen/src/escape.rs b/crates/ailang-codegen/src/escape.rs index fdd1b90..278c060 100644 --- a/crates/ailang-codegen/src/escape.rs +++ b/crates/ailang-codegen/src/escape.rs @@ -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, 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, 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); + } } } diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 85d5cef..8bf3d4b 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -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) -> 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)), + }, } } diff --git a/crates/ailang-core/src/ast.rs b/crates/ailang-core/src/ast.rs index ad408bd..36feacf 100644 --- a/crates/ailang-core/src/ast.rs +++ b/crates/ailang-core/src/ast.rs @@ -291,6 +291,15 @@ pub enum Term { lhs: Box, rhs: Box, }, + /// 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, + }, } /// One arm of a [`Term::Match`]. diff --git a/crates/ailang-core/src/desugar.rs b/crates/ailang-core/src/desugar.rs index bdc97ff..db7c00b 100644 --- a/crates/ailang-core/src/desugar.rs +++ b/crates/ailang-core/src/desugar.rs @@ -320,6 +320,10 @@ fn collect_used_in_term(t: &Term, used: &mut BTreeSet) { 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, 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::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), } } diff --git a/crates/ailang-surface/src/parse.rs b/crates/ailang-surface/src/parse.rs index 9750dc9..d8b2365 100644 --- a/crates/ailang-surface/src/parse.rs +++ b/crates/ailang-surface/src/parse.rs @@ -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 { + 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 { @@ -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. diff --git a/crates/ailang-surface/src/print.rs b/crates/ailang-surface/src/print.rs index 7369fc3..e7e0088 100644 --- a/crates/ailang-surface/src/print.rs +++ b/crates/ailang-surface/src/print.rs @@ -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 )`. 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(')'); + } } } diff --git a/examples/clone_demo.ail.json b/examples/clone_demo.ail.json new file mode 100644 index 0000000..ae8437e --- /dev/null +++ b/examples/clone_demo.ail.json @@ -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"} \ No newline at end of file diff --git a/examples/clone_demo.ailx b/examples/clone_demo.ailx new file mode 100644 index 0000000..b72b003 --- /dev/null +++ b/examples/clone_demo.ailx @@ -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))))))