Iter 18d.1: Term::ReuseAs schema + linearity check
Schema floor for explicit reuse hints. Adds Term::ReuseAs
{ source: Box<Term>, body: Box<Term> } with serde tag "reuse-as",
form-A (reuse-as <source> <body>). Schema choice (wrapper, not
modifier-on-Ctor) and rationale recorded in DESIGN.md.
Codegen is identity under all --alloc strategies — lower body,
drop source on the floor. Iter 18d.2 will lower this as the
in-place rewrite under --alloc=rc.
User-facing diagnostics:
- typecheck reuse-as-non-allocating-body: body must be a
Term::Ctor or Term::Lam, the two AST shapes that allocate.
Other shapes (literal, var, app, ...) emit this with a
suggested_rewrite that drops the wrapper.
- linearity reuse-as-source-not-bare-var: source must be a
Term::Var referring to an in-scope binder. Anything else
(literal, nested expression) is rejected here. Suggested
rewrite drops the wrapper.
- linearity use-after-consume at a reuse-as site: source must
not have been consumed earlier in the body. Suggested rewrite
drops the wrapper.
- Visiting reuse-as marks source consumed for the rest of the
body, so a subsequent use of the same binder flags
use-after-consume against it.
Uniqueness inference treats source as Consume — the consume
shows up in the side table for the codegen consumer.
Tests:
- 4 surface parse-tests (round-trip, no-args, one-arg, full
parse_term/term_to_form_a round-trip).
- 1 typecheck unit test (non-allocating body).
- 2 linearity unit tests (non-var source, after-consume).
- 1 integration test (happy-path map_inc in all-explicit mode
is linearity-clean).
- 1 E2E test running the canonical map_inc-via-reuse-as fixture
under --alloc=gc, asserting stdout 9.
- New fixture examples/reuse_as_demo.{ailx,ail.json}.
Test deltas: E2E 55 -> 56, ailang-check unit 43 -> 46,
ailang-check workspace 8 -> 9, surface 14 -> 18. cargo test
--workspace green. No existing fixture's canonical JSON changed.
This commit is contained in:
@@ -974,6 +974,12 @@ fn walk_term(
|
||||
// purposes. The wrapper introduces no new symbols.
|
||||
walk_term(value, out, builtins, scope);
|
||||
}
|
||||
Term::ReuseAs { source, body } => {
|
||||
// Iter 18d.1: walk both children; the wrapper introduces no
|
||||
// new bindings of its own.
|
||||
walk_term(source, out, builtins, scope);
|
||||
walk_term(body, out, builtins, scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1331,6 +1331,33 @@ fn clone_demo_is_identity_in_18c1() {
|
||||
assert_eq!(stdout.trim(), "42");
|
||||
}
|
||||
|
||||
/// Iter 18d.1: `Term::ReuseAs` is a schema-floor addition. In 18d.1 the
|
||||
/// wrapper is identity at codegen — the body is lowered, the source is
|
||||
/// dropped on the floor — so a program containing `(reuse-as <var>
|
||||
/// <ctor>)` produces the same stdout under `--alloc=gc` it would have
|
||||
/// produced without the wrapper. Iter 18d.2 will replace the codegen
|
||||
/// passthrough with an in-place rewrite under `--alloc=rc`.
|
||||
///
|
||||
/// Properties guarded:
|
||||
/// (1) The fixture's canonical JSON contains `"t":"reuse-as"` — the
|
||||
/// schema for the new variant did not regress.
|
||||
/// (2) `--alloc=gc` produces `9` (1+1 + 2+1 + 3+1) — the typechecker,
|
||||
/// parser/printer round-trip, and codegen identity all agree.
|
||||
#[test]
|
||||
fn reuse_as_demo_is_identity_in_18d1() {
|
||||
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
||||
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
|
||||
let json_path = workspace.join("examples").join("reuse_as_demo.ail.json");
|
||||
let json = std::fs::read_to_string(&json_path).expect("read reuse_as_demo.ail.json");
|
||||
assert!(
|
||||
json.contains("\"t\":\"reuse-as\""),
|
||||
"expected `\"t\":\"reuse-as\"` in canonical JSON; the schema for `(reuse-as ...)` regressed"
|
||||
);
|
||||
|
||||
let stdout = build_and_run_with_alloc("reuse_as_demo.ail.json", "gc");
|
||||
assert_eq!(stdout.trim(), "9");
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -426,6 +426,18 @@ pub enum CheckError {
|
||||
ctor: String,
|
||||
candidates: Vec<String>,
|
||||
},
|
||||
|
||||
/// Iter 18d.1: a `Term::ReuseAs { body, .. }` was found whose `body`
|
||||
/// is not an allocating Term variant (i.e. not `Term::Ctor` and not
|
||||
/// `Term::Lam`). `(reuse-as ...)` only makes sense when the body
|
||||
/// produces a fresh allocation that can take over the source's
|
||||
/// memory slot; otherwise the wrapper is meaningless and is
|
||||
/// rejected at typecheck. Code: `reuse-as-non-allocating-body`.
|
||||
/// `ctx`: `{"got": "<term-tag>"}`. The `body_form_a` field carries
|
||||
/// the form-A spelling of the inner body so [`Self::to_diagnostic`]
|
||||
/// can attach a `SuggestedRewrite` that drops the wrapper.
|
||||
#[error("reuse-as body must be an allocating term (Term::Ctor or Term::Lam), got {got}")]
|
||||
ReuseAsNonAllocatingBody { got: String, body_form_a: String },
|
||||
}
|
||||
|
||||
pub(crate) type Result<T> = std::result::Result<T, CheckError>;
|
||||
@@ -463,6 +475,7 @@ impl CheckError {
|
||||
CheckError::InvalidDefName { .. } => "invalid-def-name",
|
||||
CheckError::TailCallNotInTailPosition => "tail-call-not-in-tail-position",
|
||||
CheckError::AmbiguousCtor { .. } => "ambiguous-ctor",
|
||||
CheckError::ReuseAsNonAllocatingBody { .. } => "reuse-as-non-allocating-body",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,6 +513,9 @@ impl CheckError {
|
||||
CheckError::AmbiguousCtor { ctor, candidates } => {
|
||||
serde_json::json!({"ctor": ctor, "candidates": candidates})
|
||||
}
|
||||
CheckError::ReuseAsNonAllocatingBody { got, .. } => {
|
||||
serde_json::json!({"got": got})
|
||||
}
|
||||
_ => serde_json::Value::Object(serde_json::Map::new()),
|
||||
}
|
||||
}
|
||||
@@ -535,6 +551,16 @@ impl CheckError {
|
||||
if let Some(name) = self.def() {
|
||||
d = d.with_def(name);
|
||||
}
|
||||
// Iter 18d.1: typecheck-side suggested_rewrites for the
|
||||
// reuse-as-non-allocating-body diagnostic. The replacement is
|
||||
// simply the body without the `(reuse-as ...)` wrapper — the
|
||||
// hint is meaningless here, so drop it.
|
||||
if let CheckError::ReuseAsNonAllocatingBody { body_form_a, .. } = self.inner() {
|
||||
d = d.with_suggested_rewrite(
|
||||
"drop the meaningless reuse-as wrapper around the non-allocating body",
|
||||
body_form_a.clone(),
|
||||
);
|
||||
}
|
||||
d
|
||||
}
|
||||
}
|
||||
@@ -1145,6 +1171,14 @@ pub fn verify_tail_positions(t: &Term, is_tail: bool) -> Result<()> {
|
||||
// through unchanged — `(clone tail-call)` is a tail call.
|
||||
verify_tail_positions(value, is_tail)
|
||||
}
|
||||
Term::ReuseAs { source, body } => {
|
||||
// Iter 18d.1: reuse-as is identity at codegen. The `source`
|
||||
// is a side-effect-free Var in shipping fixtures, so it is
|
||||
// not a tail call; the `body` is the value of the whole
|
||||
// expression and inherits the enclosing tail position.
|
||||
verify_tail_positions(source, false)?;
|
||||
verify_tail_positions(body, is_tail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1653,6 +1687,46 @@ pub(crate) fn synth(
|
||||
// emission pass (18c.3); typing is pure passthrough.
|
||||
synth(value, env, locals, effects, in_def, subst, counter)
|
||||
}
|
||||
Term::ReuseAs { source, body } => {
|
||||
// Iter 18d.1: `(reuse-as SRC NEW-CTOR)` requires `body` to
|
||||
// be an allocating term — `Term::Ctor` or `Term::Lam`. Any
|
||||
// other shape is a structural error: the wrapper has
|
||||
// nothing to rewrite. The `source` term has no body-shape
|
||||
// constraint here — linearity (which runs only on
|
||||
// all-explicit-mode fns) enforces "source must be a bare
|
||||
// Var referring to an in-scope owned binder". Source-type
|
||||
// and body-type need not match — that's a future
|
||||
// shape-compatibility check (18d.2 will add a
|
||||
// `reuse-as-shape-mismatch` diagnostic when codegen has
|
||||
// the actual size info).
|
||||
let _ = synth(source, env, locals, effects, in_def, subst, counter)?;
|
||||
match body.as_ref() {
|
||||
Term::Ctor { .. } | Term::Lam { .. } => {}
|
||||
other => {
|
||||
let tag = match other {
|
||||
Term::Lit { .. } => "lit",
|
||||
Term::Var { .. } => "var",
|
||||
Term::App { .. } => "app",
|
||||
Term::Let { .. } => "let",
|
||||
Term::LetRec { .. } => "let-rec",
|
||||
Term::If { .. } => "if",
|
||||
Term::Do { .. } => "do",
|
||||
Term::Match { .. } => "match",
|
||||
Term::Seq { .. } => "seq",
|
||||
Term::Clone { .. } => "clone",
|
||||
Term::ReuseAs { .. } => "reuse-as",
|
||||
Term::Ctor { .. } | Term::Lam { .. } => unreachable!(),
|
||||
};
|
||||
return Err(CheckError::ReuseAsNonAllocatingBody {
|
||||
got: tag.to_string(),
|
||||
body_form_a: ailang_surface::print::term_to_form_a(other),
|
||||
});
|
||||
}
|
||||
}
|
||||
// Body's type is the result type of the whole reuse-as
|
||||
// expression.
|
||||
synth(body, env, locals, effects, in_def, subst, counter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3767,4 +3841,54 @@ mod tests {
|
||||
"(== 1 true) must fail to typecheck; got no diagnostics"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 18d.1: a `(reuse-as xs 42)` where the body is a literal
|
||||
/// (not a `Term::Ctor` and not `Term::Lam`) must emit
|
||||
/// `reuse-as-non-allocating-body` with `ctx.got = "lit"`. The
|
||||
/// suggested rewrite drops the wrapper; the replacement parses
|
||||
/// via `parse_term`.
|
||||
#[test]
|
||||
fn reuse_as_with_non_allocating_body_is_reported() {
|
||||
// Body: (reuse-as x 42) — `x` is the param, body is a lit.
|
||||
let body = Term::ReuseAs {
|
||||
source: Box::new(Term::Var { name: "x".into() }),
|
||||
body: Box::new(Term::Lit { lit: Literal::Int { value: 42 } }),
|
||||
};
|
||||
let m = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "t".into(),
|
||||
imports: vec![],
|
||||
defs: vec![fn_def(
|
||||
"f",
|
||||
Type::Fn {
|
||||
params: vec![Type::int()],
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
},
|
||||
vec!["x"],
|
||||
body,
|
||||
)],
|
||||
};
|
||||
let diags = check_module(&m);
|
||||
let bad: Vec<&Diagnostic> = diags
|
||||
.iter()
|
||||
.filter(|d| d.code == "reuse-as-non-allocating-body")
|
||||
.collect();
|
||||
assert_eq!(
|
||||
bad.len(),
|
||||
1,
|
||||
"want exactly one reuse-as-non-allocating-body; got: {diags:#?}"
|
||||
);
|
||||
let d = bad[0];
|
||||
assert_eq!(d.severity, Severity::Error);
|
||||
assert_eq!(d.def.as_deref(), Some("f"));
|
||||
assert_eq!(d.ctx.get("got").and_then(|v| v.as_str()), Some("lit"));
|
||||
assert_eq!(d.suggested_rewrites.len(), 1);
|
||||
// Replacement is the body without the wrapper — must parse.
|
||||
let rep = &d.suggested_rewrites[0].replacement;
|
||||
ailang_surface::parse_term(rep)
|
||||
.unwrap_or_else(|e| panic!("suggested rewrite must parse: {rep} ({e})"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,6 +361,11 @@ impl<'a> Lifter<'a> {
|
||||
// Iter 18c.1: structural recursion through the wrapper.
|
||||
value: Box::new(self.lift_in_term(value, locals, in_def)?),
|
||||
}),
|
||||
Term::ReuseAs { source, body } => Ok(Term::ReuseAs {
|
||||
// Iter 18d.1: structural recursion through both children.
|
||||
source: Box::new(self.lift_in_term(source, locals, in_def)?),
|
||||
body: Box::new(self.lift_in_term(body, 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
|
||||
@@ -697,6 +702,8 @@ fn contains_any_letrec(m: &Module) -> bool {
|
||||
Term::LetRec { .. } => true,
|
||||
// Iter 18c.1: identity passthrough for the letrec-detection scan.
|
||||
Term::Clone { value } => term_has_letrec(value),
|
||||
// Iter 18d.1: structural recursion through both children.
|
||||
Term::ReuseAs { source, body } => term_has_letrec(source) || term_has_letrec(body),
|
||||
}
|
||||
}
|
||||
for def in &m.defs {
|
||||
|
||||
@@ -349,6 +349,46 @@ impl<'a> Checker<'a> {
|
||||
// only borrowed. So `(clone xs)` does NOT consume `xs`.
|
||||
self.walk(value, Position::Borrow);
|
||||
}
|
||||
Term::ReuseAs { source, body } => {
|
||||
// Iter 18d.1: linearity for `(reuse-as SRC BODY)`:
|
||||
// - `SRC` must be a bare `Var { name }` of an
|
||||
// in-scope binder (else `reuse-as-source-not-bare-var`).
|
||||
// - The binder must currently have `consumed == false`
|
||||
// (else `use-after-consume` at this site).
|
||||
// - Visiting reuse-as marks the binder consumed (the
|
||||
// reuse semantics is "this is where the source is
|
||||
// freed/overwritten").
|
||||
// - `BODY` walks normally — its sub-uses are subject
|
||||
// to the usual position rules; e.g. a Consume of
|
||||
// the same binder inside `BODY` will fire
|
||||
// use-after-consume because reuse-as already ate it.
|
||||
match source.as_ref() {
|
||||
Term::Var { name } if self.binders.contains_key(name) => {
|
||||
let already_consumed = self
|
||||
.binders
|
||||
.get(name)
|
||||
.map(|s| s.consumed)
|
||||
.unwrap_or(false);
|
||||
if already_consumed {
|
||||
self.diags
|
||||
.push(make_use_after_consume_at_reuse_as(self.def_name, name, body));
|
||||
} else if let Some(s) = self.binders.get_mut(name) {
|
||||
// Mark the source consumed at the reuse-as site.
|
||||
s.consumed = true;
|
||||
}
|
||||
}
|
||||
other => {
|
||||
self.diags.push(make_reuse_as_source_not_bare_var(
|
||||
self.def_name,
|
||||
other,
|
||||
body,
|
||||
));
|
||||
}
|
||||
}
|
||||
// Walk the body normally; its sub-uses go through the
|
||||
// standard position rules.
|
||||
self.walk(body, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,6 +553,63 @@ fn make_use_after_consume(def: &str, binder: &str) -> Diagnostic {
|
||||
)
|
||||
}
|
||||
|
||||
/// Iter 18d.1: build a `reuse-as-source-not-bare-var` diagnostic.
|
||||
/// Fires when a `Term::ReuseAs.source` is anything other than a bare
|
||||
/// `Term::Var { name }` referring to an in-scope binder. The
|
||||
/// suggested rewrite drops the meaningless wrapper and keeps the
|
||||
/// `body` alone.
|
||||
fn make_reuse_as_source_not_bare_var(def: &str, source: &Term, body: &Term) -> Diagnostic {
|
||||
let got = match source {
|
||||
Term::Lit { .. } => "lit",
|
||||
Term::Var { .. } => "var",
|
||||
Term::App { .. } => "app",
|
||||
Term::Let { .. } => "let",
|
||||
Term::LetRec { .. } => "let-rec",
|
||||
Term::If { .. } => "if",
|
||||
Term::Do { .. } => "do",
|
||||
Term::Ctor { .. } => "ctor",
|
||||
Term::Match { .. } => "match",
|
||||
Term::Lam { .. } => "lam",
|
||||
Term::Seq { .. } => "seq",
|
||||
Term::Clone { .. } => "clone",
|
||||
Term::ReuseAs { .. } => "reuse-as",
|
||||
};
|
||||
let replacement = term_to_form_a(body);
|
||||
Diagnostic::error(
|
||||
"reuse-as-source-not-bare-var",
|
||||
format!(
|
||||
"reuse-as source must be a bare variable referring to an in-scope binder, got {got}"
|
||||
),
|
||||
)
|
||||
.with_def(def)
|
||||
.with_ctx(serde_json::json!({"got": got}))
|
||||
.with_suggested_rewrite(
|
||||
"drop the malformed reuse-as wrapper; keep the body alone",
|
||||
replacement,
|
||||
)
|
||||
}
|
||||
|
||||
/// Iter 18d.1: `use-after-consume` raised at a `(reuse-as <var> <body>)`
|
||||
/// site whose `<var>` binder has already been consumed elsewhere. Same
|
||||
/// code as the regular use-after-consume; the suggested rewrite is to
|
||||
/// drop the (now unsatisfiable) reuse hint and keep the body — the
|
||||
/// allocation will happen via the normal allocator.
|
||||
fn make_use_after_consume_at_reuse_as(def: &str, binder: &str, body: &Term) -> Diagnostic {
|
||||
let replacement = term_to_form_a(body);
|
||||
Diagnostic::error(
|
||||
"use-after-consume",
|
||||
format!("`{binder}` is used after it was already consumed"),
|
||||
)
|
||||
.with_def(def)
|
||||
.with_ctx(serde_json::json!({"binder": binder}))
|
||||
.with_suggested_rewrite(
|
||||
format!(
|
||||
"drop the reuse-as hint on `{binder}`; the binder is already consumed so reuse cannot fire"
|
||||
),
|
||||
replacement,
|
||||
)
|
||||
}
|
||||
|
||||
/// Build a `consume-while-borrowed` diagnostic for `binder` in `def`.
|
||||
/// Suggested rewrite: clone here so the original retains the borrow.
|
||||
fn make_consume_while_borrowed(def: &str, binder: &str) -> Diagnostic {
|
||||
@@ -606,4 +703,73 @@ mod tests {
|
||||
};
|
||||
assert!(check_module(&m).is_empty());
|
||||
}
|
||||
|
||||
/// Iter 18d.1: a `(reuse-as 42 (Cons ...))` whose source is a literal
|
||||
/// (not a bare `Var`) is flagged with `reuse-as-source-not-bare-var`.
|
||||
/// The suggested rewrite drops the wrapper and keeps the body.
|
||||
#[test]
|
||||
fn reuse_as_with_non_var_source_is_reported() {
|
||||
// Body shape: (reuse-as 42 (term-ctor Wrap Wrap p0))
|
||||
// The source `42` is a literal — invalid for reuse-as.
|
||||
let body = Term::ReuseAs {
|
||||
source: Box::new(Term::Lit { lit: Literal::Int { value: 42 } }),
|
||||
body: Box::new(Term::Ctor {
|
||||
type_name: "Wrap".into(),
|
||||
ctor: "Wrap".into(),
|
||||
args: vec![Term::Var { name: "p0".into() }],
|
||||
}),
|
||||
};
|
||||
let m = Module {
|
||||
schema: ailang_core::SCHEMA.into(),
|
||||
name: "t".into(),
|
||||
imports: vec![],
|
||||
defs: vec![fn_with_modes("f", vec![ParamMode::Own], body)],
|
||||
};
|
||||
let diags = check_module(&m);
|
||||
assert_eq!(diags.len(), 1, "expected exactly one diagnostic, got {diags:?}");
|
||||
let d = &diags[0];
|
||||
assert_eq!(d.code, "reuse-as-source-not-bare-var");
|
||||
assert_eq!(d.def.as_deref(), Some("f"));
|
||||
assert_eq!(d.ctx, serde_json::json!({"got": "lit"}));
|
||||
assert!(
|
||||
!d.suggested_rewrites.is_empty(),
|
||||
"diagnostic should carry a suggested rewrite"
|
||||
);
|
||||
// The replacement must round-trip through parse_term.
|
||||
let rep = &d.suggested_rewrites[0].replacement;
|
||||
ailang_surface::parse_term(rep)
|
||||
.unwrap_or_else(|e| panic!("suggested rewrite must parse: {rep} ({e})"));
|
||||
}
|
||||
|
||||
/// Iter 18d.1: a `(reuse-as p0 ...)` where `p0` was already
|
||||
/// consumed by an earlier sibling fires `use-after-consume` at the
|
||||
/// reuse-as site (not `reuse-as-source-not-bare-var`).
|
||||
#[test]
|
||||
fn reuse_as_after_consume_is_use_after_consume() {
|
||||
// Body shape: (seq p0 (reuse-as p0 (term-ctor Wrap Wrap p0)))
|
||||
// The first `p0` (in `seq.lhs`) consumes the binder; the
|
||||
// reuse-as in the rhs then sees an already-consumed binder.
|
||||
let body = Term::Seq {
|
||||
lhs: Box::new(Term::Var { name: "p0".into() }),
|
||||
rhs: Box::new(Term::ReuseAs {
|
||||
source: Box::new(Term::Var { name: "p0".into() }),
|
||||
body: Box::new(Term::Ctor {
|
||||
type_name: "Wrap".into(),
|
||||
ctor: "Wrap".into(),
|
||||
args: vec![Term::Lit { lit: Literal::Int { value: 0 } }],
|
||||
}),
|
||||
}),
|
||||
};
|
||||
let m = Module {
|
||||
schema: ailang_core::SCHEMA.into(),
|
||||
name: "t".into(),
|
||||
imports: vec![],
|
||||
defs: vec![fn_with_modes("f", vec![ParamMode::Own], body)],
|
||||
};
|
||||
let diags = check_module(&m);
|
||||
assert_eq!(diags.len(), 1, "expected exactly one diagnostic, got {diags:?}");
|
||||
let d = &diags[0];
|
||||
assert_eq!(d.code, "use-after-consume");
|
||||
assert_eq!(d.ctx, serde_json::json!({"binder": "p0"}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +304,16 @@ impl<'a> Walker<'a> {
|
||||
Term::Clone { value } => {
|
||||
self.walk(value, Position::Borrow);
|
||||
}
|
||||
Term::ReuseAs { source, body } => {
|
||||
// Iter 18d.1: `(reuse-as SRC BODY)` is a Consume of
|
||||
// `SRC` (the binder's slot is being taken over) and
|
||||
// a normal walk of `BODY`. So a bare-Var SRC bumps
|
||||
// its `consume_count` by one. Linearity is the
|
||||
// user-visible enforcement; this pass only feeds the
|
||||
// RC codegen its bookkeeping.
|
||||
self.walk(source, Position::Consume);
|
||||
self.walk(body, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -406,6 +406,126 @@ fn consume_while_borrowed_in_sibling_arg_is_reported() {
|
||||
assert_suggested_rewrites_well_formed(d);
|
||||
}
|
||||
|
||||
/// Iter 18d.1: happy-path `(reuse-as xs (term-ctor List Cons ...))`
|
||||
/// inside an all-explicit-mode `map_inc`-style fn must produce NO
|
||||
/// diagnostics — neither `reuse-as-non-allocating-body` (body is a
|
||||
/// Ctor) nor `reuse-as-source-not-bare-var` (source is `xs`) nor
|
||||
/// `use-after-consume` (xs is matched then consumed once via
|
||||
/// reuse-as in the Cons arm; the merge across arms is consistent).
|
||||
///
|
||||
/// Body:
|
||||
/// `(match xs
|
||||
/// (case Nil (term-ctor List Nil))
|
||||
/// (case (Cons h t)
|
||||
/// (reuse-as xs (term-ctor List Cons (+ h 1) (app map_inc t)))))`
|
||||
#[test]
|
||||
fn reuse_as_happy_path_in_map_inc_is_linearity_clean() {
|
||||
use ailang_core::ast::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
let list_adt = Def::Type(TypeDef {
|
||||
name: "List".into(),
|
||||
vars: vec![],
|
||||
ctors: vec![
|
||||
Ctor { name: "Nil".into(), fields: vec![] },
|
||||
Ctor {
|
||||
name: "Cons".into(),
|
||||
fields: vec![
|
||||
Type::Con { name: "Int".into(), args: vec![] },
|
||||
Type::Con { name: "List".into(), args: vec![] },
|
||||
],
|
||||
},
|
||||
],
|
||||
doc: None,
|
||||
});
|
||||
|
||||
let map_inc_ty = Type::Fn {
|
||||
params: vec![Type::Con { name: "List".into(), args: vec![] }],
|
||||
param_modes: vec![ParamMode::Own],
|
||||
ret: Box::new(Type::Con { name: "List".into(), args: vec![] }),
|
||||
ret_mode: ParamMode::Own,
|
||||
effects: vec![],
|
||||
};
|
||||
|
||||
// Cons arm body:
|
||||
// (reuse-as xs
|
||||
// (term-ctor List Cons (app + h 1) (app map_inc t)))
|
||||
let cons_arm_body = Term::ReuseAs {
|
||||
source: Box::new(Term::Var { name: "xs".into() }),
|
||||
body: Box::new(Term::Ctor {
|
||||
type_name: "List".into(),
|
||||
ctor: "Cons".into(),
|
||||
args: vec![
|
||||
Term::App {
|
||||
callee: Box::new(Term::Var { name: "+".into() }),
|
||||
args: vec![
|
||||
Term::Var { name: "h".into() },
|
||||
Term::Lit { lit: Literal::Int { value: 1 } },
|
||||
],
|
||||
tail: false,
|
||||
},
|
||||
Term::App {
|
||||
callee: Box::new(Term::Var { name: "map_inc".into() }),
|
||||
args: vec![Term::Var { name: "t".into() }],
|
||||
tail: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
let map_inc_body = Term::Match {
|
||||
scrutinee: Box::new(Term::Var { name: "xs".into() }),
|
||||
arms: vec![
|
||||
Arm {
|
||||
pat: Pattern::Ctor { ctor: "Nil".into(), fields: vec![] },
|
||||
body: Term::Ctor {
|
||||
type_name: "List".into(),
|
||||
ctor: "Nil".into(),
|
||||
args: vec![],
|
||||
},
|
||||
},
|
||||
Arm {
|
||||
pat: Pattern::Ctor {
|
||||
ctor: "Cons".into(),
|
||||
fields: vec![
|
||||
Pattern::Var { name: "h".into() },
|
||||
Pattern::Var { name: "t".into() },
|
||||
],
|
||||
},
|
||||
body: cons_arm_body,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let map_inc = Def::Fn(FnDef {
|
||||
name: "map_inc".into(),
|
||||
ty: map_inc_ty,
|
||||
params: vec!["xs".into()],
|
||||
body: map_inc_body,
|
||||
doc: None,
|
||||
});
|
||||
|
||||
let m = Module {
|
||||
schema: ailang_core::SCHEMA.into(),
|
||||
name: "reuse_as_happy".into(),
|
||||
imports: vec![],
|
||||
defs: vec![list_adt, map_inc],
|
||||
};
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert(m.name.clone(), m.clone());
|
||||
let ws = ailang_core::Workspace {
|
||||
entry: m.name.clone(),
|
||||
modules,
|
||||
root_dir: std::path::PathBuf::from("."),
|
||||
};
|
||||
let diags = check_workspace(&ws);
|
||||
assert!(
|
||||
diags.is_empty(),
|
||||
"happy-path reuse-as must check clean; got: {:#?}",
|
||||
diags
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 18c.2: positive control. The ON-DISK `borrow_own_demo` fixture
|
||||
/// (the only currently-shipping all-explicit-mode program) must remain
|
||||
/// linearity-clean. If this regresses, the check has become incorrect:
|
||||
|
||||
@@ -184,6 +184,12 @@ fn walk(t: &Term, out: &mut NonEscapeSet) {
|
||||
// sub-terms looking for Let-allocation candidates.
|
||||
walk(value, out);
|
||||
}
|
||||
Term::ReuseAs { source, body } => {
|
||||
// Iter 18d.1: identity at IR level (codegen lowers `body`,
|
||||
// ignores `source`). Walk both children for completeness.
|
||||
walk(source, out);
|
||||
walk(body, out);
|
||||
}
|
||||
Term::Lit { .. } | Term::Var { .. } => {}
|
||||
}
|
||||
}
|
||||
@@ -349,6 +355,18 @@ fn escapes(t: &Term, tainted: &BTreeSet<String>, in_tail: bool) -> bool {
|
||||
// through with the same `in_tail` context.
|
||||
escapes(value, tainted, in_tail)
|
||||
}
|
||||
Term::ReuseAs { source, body } => {
|
||||
// Iter 18d.1: identity at IR level. The whole expression's
|
||||
// value is `body`, so its escape verdict is the body's
|
||||
// (threaded through `in_tail`). The `source` is evaluated
|
||||
// but its value is discarded; it cannot escape via the
|
||||
// tail position, but a tainted name reachable inside it
|
||||
// could still escape — walk it in non-tail mode.
|
||||
if escapes(source, tainted, false) {
|
||||
return true;
|
||||
}
|
||||
escapes(body, tainted, in_tail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,6 +466,11 @@ fn collect_free_vars(t: &Term, bound: &mut BTreeSet<String>, out: &mut BTreeSet<
|
||||
// Iter 18c.1: clone is identity for free-var collection.
|
||||
collect_free_vars(value, bound, out);
|
||||
}
|
||||
Term::ReuseAs { source, body } => {
|
||||
// Iter 18d.1: free vars are the union of source and body.
|
||||
collect_free_vars(source, bound, out);
|
||||
collect_free_vars(body, bound, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1512,6 +1512,18 @@ impl<'a> Emitter<'a> {
|
||||
}
|
||||
Ok((val_ssa, val_ty))
|
||||
}
|
||||
Term::ReuseAs { source: _, body } => {
|
||||
// Iter 18d.1: identity. Iter 18d.2 will lower this as
|
||||
// in-place rewrite under --alloc=rc. For now we simply
|
||||
// lower `body` and return its `(ssa, ty)`. The `source`
|
||||
// is dropped on the floor — in shipping fixtures it is
|
||||
// always a `Term::Var` (no IR side effects), so the
|
||||
// identity lowering is observably equivalent to lowering
|
||||
// `body` alone. The linearity check (18c.2) and
|
||||
// typecheck (18d.1) reject ill-formed shapes before we
|
||||
// get here.
|
||||
self.lower_term(body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2774,6 +2786,11 @@ impl<'a> Emitter<'a> {
|
||||
// free vars of `(clone X)` are exactly the free vars of `X`.
|
||||
Self::collect_captures(value, bound, captures, captures_set, builtins, top_level);
|
||||
}
|
||||
Term::ReuseAs { source, body } => {
|
||||
// Iter 18d.1: free vars are the union of source and body.
|
||||
Self::collect_captures(source, bound, captures, captures_set, builtins, top_level);
|
||||
Self::collect_captures(body, bound, captures, captures_set, builtins, top_level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3284,6 +3301,11 @@ impl<'a> Emitter<'a> {
|
||||
// Iter 18c.1: clone is identity — same type as inner.
|
||||
self.synth_with_extras(value, extras)
|
||||
}
|
||||
Term::ReuseAs { body, .. } => {
|
||||
// Iter 18d.1: identity — the result type is the body's
|
||||
// type. The source is dropped at codegen.
|
||||
self.synth_with_extras(body, extras)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3677,6 +3699,11 @@ fn apply_subst_to_term(t: &Term, subst: &BTreeMap<String, Type>) -> Term {
|
||||
// Iter 18c.1: structural recursion through the wrapper.
|
||||
value: Box::new(apply_subst_to_term(value, subst)),
|
||||
},
|
||||
Term::ReuseAs { source, body } => Term::ReuseAs {
|
||||
// Iter 18d.1: structural recursion through both children.
|
||||
source: Box::new(apply_subst_to_term(source, subst)),
|
||||
body: Box::new(apply_subst_to_term(body, subst)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -300,6 +300,23 @@ pub enum Term {
|
||||
Clone {
|
||||
value: Box<Term>,
|
||||
},
|
||||
/// Iter 18d.1: explicit reuse-as hint. Wraps an allocating `body`
|
||||
/// (typically `Term::Ctor`, also `Term::Lam`) and names a `source`
|
||||
/// term whose memory slot the body's allocation should reuse.
|
||||
/// Conventionally `source` is a `Term::Var { name }` — the
|
||||
/// linearity check rejects anything else with
|
||||
/// `reuse-as-source-not-bare-var`. The body must be allocating;
|
||||
/// non-allocating bodies are rejected at typecheck with
|
||||
/// `reuse-as-non-allocating-body`. Lowers as identity in 18d.1
|
||||
/// (returns `body`'s `(ssa, ty)`, ignores `source`); 18d.2 will
|
||||
/// lower this as in-place rewrite under `--alloc=rc`. The variant
|
||||
/// is additive — pre-18d fixtures keep their canonical-JSON hash
|
||||
/// because none of them use the new tag.
|
||||
#[serde(rename = "reuse-as")]
|
||||
ReuseAs {
|
||||
source: Box<Term>,
|
||||
body: Box<Term>,
|
||||
},
|
||||
}
|
||||
|
||||
/// One arm of a [`Term::Match`].
|
||||
|
||||
@@ -324,6 +324,11 @@ fn collect_used_in_term(t: &Term, used: &mut BTreeSet<String>) {
|
||||
// Iter 18c.1: identity for the used-name walk.
|
||||
collect_used_in_term(value, used);
|
||||
}
|
||||
Term::ReuseAs { source, body } => {
|
||||
// Iter 18d.1: structural recursion through both children.
|
||||
collect_used_in_term(source, used);
|
||||
collect_used_in_term(body, used);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,6 +516,12 @@ impl Desugarer {
|
||||
// wrapper. Same pattern as `Term::Let`'s value branch.
|
||||
value: Box::new(self.desugar_term(value, scope)),
|
||||
},
|
||||
Term::ReuseAs { source, body } => Term::ReuseAs {
|
||||
// Iter 18d.1: pure structural recursion through both
|
||||
// children. Same pattern as `Term::Clone`.
|
||||
source: Box::new(self.desugar_term(source, scope)),
|
||||
body: Box::new(self.desugar_term(body, 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 —
|
||||
@@ -1122,6 +1133,11 @@ pub fn free_vars_in_term(t: &Term, bound: &BTreeSet<String>, out: &mut BTreeSet<
|
||||
// Iter 18c.1: `(clone X)` has the same free vars as `X`.
|
||||
free_vars_in_term(value, bound, out);
|
||||
}
|
||||
Term::ReuseAs { source, body } => {
|
||||
// Iter 18d.1: free vars are the union of source and body.
|
||||
free_vars_in_term(source, bound, out);
|
||||
free_vars_in_term(body, bound, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1261,6 +1277,11 @@ pub fn subst_var(t: &Term, from: &str, to: &str) -> Term {
|
||||
// Iter 18c.1: structural recursion through the wrapper.
|
||||
value: Box::new(subst_var(value, from, to)),
|
||||
},
|
||||
Term::ReuseAs { source, body } => Term::ReuseAs {
|
||||
// Iter 18d.1: structural recursion through both children.
|
||||
source: Box::new(subst_var(source, from, to)),
|
||||
body: Box::new(subst_var(body, from, to)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1377,6 +1398,11 @@ pub fn subst_call_with_extras(t: &Term, name: &str, lifted: &str, extras: &[Stri
|
||||
// Iter 18c.1: structural recursion through the wrapper.
|
||||
value: Box::new(subst_call_with_extras(value, name, lifted, extras)),
|
||||
},
|
||||
Term::ReuseAs { source, body } => Term::ReuseAs {
|
||||
// Iter 18d.1: structural recursion through both children.
|
||||
source: Box::new(subst_call_with_extras(source, name, lifted, extras)),
|
||||
body: Box::new(subst_call_with_extras(body, name, lifted, extras)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1427,6 +1453,10 @@ pub fn find_non_callee_use(t: &Term, name: &str) -> Option<Term> {
|
||||
.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),
|
||||
// Iter 18d.1: scan both source and body.
|
||||
Term::ReuseAs { source, body } => {
|
||||
find_non_callee_use(source, name).or_else(|| find_non_callee_use(body, name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1468,6 +1498,7 @@ mod tests {
|
||||
any_nested_ctor(body) || any_nested_ctor(in_term)
|
||||
}
|
||||
Term::Clone { value } => any_nested_ctor(value),
|
||||
Term::ReuseAs { source, body } => any_nested_ctor(source) || any_nested_ctor(body),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1492,6 +1523,7 @@ mod tests {
|
||||
Term::Seq { lhs, rhs } => any_let_rec(lhs) || any_let_rec(rhs),
|
||||
Term::LetRec { .. } => true,
|
||||
Term::Clone { value } => any_let_rec(value),
|
||||
Term::ReuseAs { source, body } => any_let_rec(source) || any_let_rec(body),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2592,6 +2624,7 @@ mod tests {
|
||||
any_lit_pattern(body) || any_lit_pattern(in_term)
|
||||
}
|
||||
Term::Clone { value } => any_lit_pattern(value),
|
||||
Term::ReuseAs { source, body } => any_lit_pattern(source) || any_lit_pattern(body),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
//! body-attr
|
||||
//! "(" "in" term ")" ")"
|
||||
//! clone-term ::= "(" "clone" term ")" ; Iter 18c.1
|
||||
//! reuse-as-term ::= "(" "reuse-as" term term ")" ; Iter 18d.1
|
||||
//!
|
||||
//! pattern ::= pat-var | pat-ctor | pat-lit | pat-wild
|
||||
//! pat-var ::= ident
|
||||
@@ -777,6 +778,7 @@ impl<'a> Parser<'a> {
|
||||
"let" => self.parse_let(),
|
||||
"let-rec" => self.parse_let_rec(),
|
||||
"clone" => self.parse_clone(),
|
||||
"reuse-as" => self.parse_reuse_as(),
|
||||
other => {
|
||||
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
||||
Err(ParseError::Production {
|
||||
@@ -784,7 +786,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`, `clone`, `lit-unit`"
|
||||
`tail-do`, `seq`, `term-ctor`, `clone`, `reuse-as`, `lit-unit`"
|
||||
),
|
||||
pos,
|
||||
})
|
||||
@@ -1089,6 +1091,49 @@ impl<'a> Parser<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Iter 18d.1: `(reuse-as SOURCE BODY)` — explicit reuse-as wrapper.
|
||||
/// In 18d.1 the wrapper is identity for codegen (lowers `body` and
|
||||
/// drops `source`); 18d.2 will lower this as in-place rewrite under
|
||||
/// `--alloc=rc`. The parser accepts any term in either slot — the
|
||||
/// "source must be a bare Var" rule and the "body must be allocating"
|
||||
/// rule are enforced at typecheck/linearity time, not the parser.
|
||||
fn parse_reuse_as(&mut self) -> Result<Term, ParseError> {
|
||||
self.expect_lparen("reuse-as-term")?;
|
||||
self.expect_keyword("reuse-as")?;
|
||||
// Exactly two inner terms, 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: "reuse-as-term",
|
||||
message: "reuse-as expects exactly two term arguments".into(),
|
||||
pos,
|
||||
});
|
||||
}
|
||||
let source = 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: "reuse-as-term",
|
||||
message: "reuse-as expects exactly two term arguments".into(),
|
||||
pos,
|
||||
});
|
||||
}
|
||||
let body = 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: "reuse-as-term",
|
||||
message: "reuse-as expects exactly two term arguments".into(),
|
||||
pos,
|
||||
});
|
||||
}
|
||||
self.expect_rparen("reuse-as-term")?;
|
||||
Ok(Term::ReuseAs {
|
||||
source: Box::new(source),
|
||||
body: Box::new(body),
|
||||
})
|
||||
}
|
||||
|
||||
// ---- patterns -------------------------------------------------------
|
||||
|
||||
fn parse_pattern(&mut self) -> Result<Pattern, ParseError> {
|
||||
@@ -1335,6 +1380,109 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 18d.1: `(reuse-as xs (term-ctor List Cons (...)))` parses to
|
||||
/// `Term::ReuseAs { source: Var "xs", body: Ctor "Cons" ... }`.
|
||||
#[test]
|
||||
fn parses_reuse_as_wraps_source_and_body() {
|
||||
let m = parse(
|
||||
r#"
|
||||
(module m
|
||||
(data List (vars a)
|
||||
(ctor Nil)
|
||||
(ctor Cons a (con List a)))
|
||||
(fn f
|
||||
(type (fn-type (params (own (con List (con Int)))) (ret (own (con List (con Int))))))
|
||||
(params xs)
|
||||
(body (reuse-as xs (term-ctor List Cons 1 xs)))))
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let body = match &m.defs[1] {
|
||||
Def::Fn(fd) => &fd.body,
|
||||
_ => panic!("expected fn"),
|
||||
};
|
||||
match body {
|
||||
Term::ReuseAs { source, body } => {
|
||||
assert!(matches!(source.as_ref(), Term::Var { name } if name == "xs"));
|
||||
match body.as_ref() {
|
||||
Term::Ctor { type_name, ctor, args } => {
|
||||
assert_eq!(type_name, "List");
|
||||
assert_eq!(ctor, "Cons");
|
||||
assert_eq!(args.len(), 2);
|
||||
}
|
||||
other => panic!("expected Ctor inside ReuseAs body, got {other:?}"),
|
||||
}
|
||||
}
|
||||
other => panic!("expected ReuseAs, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter 18d.1: `(reuse-as)` with no args is rejected.
|
||||
#[test]
|
||||
fn rejects_reuse_as_with_no_arguments() {
|
||||
let err = parse(
|
||||
r#"
|
||||
(module m
|
||||
(fn f
|
||||
(type (fn-type (params (con Int)) (ret (con Int))))
|
||||
(params x)
|
||||
(body (reuse-as))))
|
||||
"#,
|
||||
)
|
||||
.err()
|
||||
.expect("parse should fail");
|
||||
let msg = format!("{err}");
|
||||
assert!(
|
||||
msg.contains("reuse-as expects exactly two term arguments"),
|
||||
"diagnostic should explain reuse-as's arity, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 18d.1: `(reuse-as x)` with a single arg is rejected.
|
||||
#[test]
|
||||
fn rejects_reuse_as_with_one_argument() {
|
||||
let err = parse(
|
||||
r#"
|
||||
(module m
|
||||
(fn f
|
||||
(type (fn-type (params (con Int)) (ret (con Int))))
|
||||
(params x)
|
||||
(body (reuse-as x))))
|
||||
"#,
|
||||
)
|
||||
.err()
|
||||
.expect("parse should fail");
|
||||
let msg = format!("{err}");
|
||||
assert!(
|
||||
msg.contains("reuse-as expects exactly two term arguments"),
|
||||
"diagnostic should explain reuse-as's arity, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 18d.1: round-trip via `parse_term` / `term_to_form_a` —
|
||||
/// `(reuse-as xs (term-ctor List Cons 1 xs))` survives a print/parse
|
||||
/// cycle as the same `Term::ReuseAs`.
|
||||
#[test]
|
||||
fn parse_term_round_trip_reuse_as() {
|
||||
use crate::print::term_to_form_a;
|
||||
let original = Term::ReuseAs {
|
||||
source: Box::new(Term::Var { name: "xs".into() }),
|
||||
body: Box::new(Term::Ctor {
|
||||
type_name: "List".into(),
|
||||
ctor: "Cons".into(),
|
||||
args: vec![
|
||||
Term::Lit { lit: Literal::Int { value: 1 } },
|
||||
Term::Var { name: "xs".into() },
|
||||
],
|
||||
}),
|
||||
};
|
||||
let printed = term_to_form_a(&original);
|
||||
let parsed = parse_term(&printed).expect("parse_term should succeed");
|
||||
// Compare by canonical bytes (Term has no PartialEq) — easiest
|
||||
// structural equality is via the printer.
|
||||
assert_eq!(term_to_form_a(&parsed), printed);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
@@ -382,6 +382,17 @@ fn write_term(out: &mut String, t: &Term, level: usize) {
|
||||
write_term(out, value, level);
|
||||
out.push(')');
|
||||
}
|
||||
Term::ReuseAs { source, body } => {
|
||||
// Iter 18d.1: print as `(reuse-as <source> <body>)`. The
|
||||
// wrapper is identity at codegen in 18d.1 (the `body` is
|
||||
// lowered, the `source` is dropped); 18d.2 will lower this
|
||||
// as in-place rewrite under `--alloc=rc`.
|
||||
out.push_str("(reuse-as ");
|
||||
write_term(out, source, level);
|
||||
out.push(' ');
|
||||
write_term(out, body, level);
|
||||
out.push(')');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"defs":[{"ctors":[{"fields":[],"name":"Nil"},{"fields":[{"k":"con","name":"Int"},{"k":"con","name":"List"}],"name":"Cons"}],"doc":"Monomorphic singly-linked Int list — boxed, recursive.","kind":"type","name":"List"},{"body":{"arms":[{"body":{"args":[],"ctor":"Nil","t":"ctor","type":"List"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"body":{"args":[{"args":[{"name":"h","t":"var"},{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"+","t":"var"},"t":"app"},{"args":[{"name":"t","t":"var"}],"fn":{"name":"map_inc","t":"var"},"t":"app"}],"ctor":"Cons","t":"ctor","type":"List"},"source":{"name":"xs","t":"var"},"t":"reuse-as"},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Increment each Int in xs. The Cons arm uses (reuse-as xs ...) — author asserts that the freshly-allocated Cons cell should reuse xs's slot. In 18d.1 codegen treats this as identity.","kind":"fn","name":"map_inc","params":["xs"],"type":{"effects":[],"k":"fn","param_modes":["own"],"params":[{"k":"con","name":"List"}],"ret":{"k":"con","name":"List"},"ret_mode":"own"}},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"name":"h","t":"var"},{"args":[{"name":"t","t":"var"}],"fn":{"name":"sum_list","t":"var"},"t":"app"}],"fn":{"name":"+","t":"var"},"t":"app"},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Consume xs, sum its elements.","kind":"fn","name":"sum_list","params":["xs"],"type":{"effects":[],"k":"fn","param_modes":["own"],"params":[{"k":"con","name":"List"}],"ret":{"k":"con","name":"Int"}}},{"body":{"body":{"args":[{"args":[{"args":[{"name":"xs","t":"var"}],"fn":{"name":"map_inc","t":"var"},"t":"app"}],"fn":{"name":"sum_list","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"name":"xs","t":"let","value":{"args":[{"lit":{"kind":"int","value":1},"t":"lit"},{"args":[{"lit":{"kind":"int","value":2},"t":"lit"},{"args":[{"lit":{"kind":"int","value":3},"t":"lit"},{"args":[],"ctor":"Nil","t":"ctor","type":"List"}],"ctor":"Cons","t":"ctor","type":"List"}],"ctor":"Cons","t":"ctor","type":"List"}],"ctor":"Cons","t":"ctor","type":"List"}},"doc":"Build [1,2,3]; map_inc → [2,3,4]; sum_list → 9. Print 9.","kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"reuse_as_demo","schema":"ailang/v0"}
|
||||
@@ -0,0 +1,64 @@
|
||||
; Iter 18d.1 — `(reuse-as xs (term-ctor List Cons ...))` schema floor
|
||||
; demo. The `(reuse-as ...)` wrapper is identity at codegen in 18d.1
|
||||
; (the `body` is lowered, the `source` is dropped); 18d.2 will lower
|
||||
; this as in-place rewrite under `--alloc=rc`.
|
||||
;
|
||||
; The fixture exercises:
|
||||
; - parser + printer round-trip for `Term::ReuseAs`
|
||||
; - typecheck of an all-explicit-mode fn whose body returns a
|
||||
; `(reuse-as <var> <ctor>)` form (body is allocating: Term::Ctor)
|
||||
; - linearity check: `xs` is consumed exactly once via reuse-as in
|
||||
; the Cons arm; the Nil arm doesn't consume `xs`. Merge across
|
||||
; arms is consistent.
|
||||
; - codegen identity under --alloc=gc: program prints `9`
|
||||
; (1+1 + 2+1 + 3+1 = 9), the same value the non-reuse-as
|
||||
; `map_inc` would print.
|
||||
;
|
||||
; Expected stdout under --alloc=gc:
|
||||
; 9
|
||||
|
||||
(module reuse_as_demo
|
||||
|
||||
(data List
|
||||
(doc "Monomorphic singly-linked Int list — boxed, recursive.")
|
||||
(ctor Nil)
|
||||
(ctor Cons (con Int) (con List)))
|
||||
|
||||
(fn map_inc
|
||||
(doc "Increment each Int in xs. The Cons arm uses (reuse-as xs ...) — author asserts that the freshly-allocated Cons cell should reuse xs's slot. In 18d.1 codegen treats this as identity.")
|
||||
(type
|
||||
(fn-type
|
||||
(params (own (con List)))
|
||||
(ret (own (con List)))))
|
||||
(params xs)
|
||||
(body
|
||||
(match xs
|
||||
(case (pat-ctor Nil) (term-ctor List Nil))
|
||||
(case (pat-ctor Cons h t)
|
||||
(reuse-as xs
|
||||
(term-ctor List Cons (app + h 1) (app map_inc t)))))))
|
||||
|
||||
(fn sum_list
|
||||
(doc "Consume xs, sum its elements.")
|
||||
(type
|
||||
(fn-type
|
||||
(params (own (con List)))
|
||||
(ret (con Int))))
|
||||
(params xs)
|
||||
(body
|
||||
(match xs
|
||||
(case (pat-ctor Nil) 0)
|
||||
(case (pat-ctor Cons h t)
|
||||
(app + h (app sum_list t))))))
|
||||
|
||||
(fn main
|
||||
(doc "Build [1,2,3]; map_inc → [2,3,4]; sum_list → 9. Print 9.")
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
(params)
|
||||
(body
|
||||
(let xs
|
||||
(term-ctor List Cons 1
|
||||
(term-ctor List Cons 2
|
||||
(term-ctor List Cons 3
|
||||
(term-ctor List Nil))))
|
||||
(do io/print_int (app sum_list (app map_inc xs)))))))
|
||||
Reference in New Issue
Block a user