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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user