|
|
|
@@ -35,7 +35,7 @@
|
|
|
|
|
use ailang_core::ast::*;
|
|
|
|
|
use ailang_core::Workspace;
|
|
|
|
|
use indexmap::IndexMap;
|
|
|
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
|
|
|
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
|
|
|
|
|
|
|
|
|
mod linearity;
|
|
|
|
|
mod pre_desugar_validation;
|
|
|
|
@@ -726,6 +726,16 @@ pub enum CheckError {
|
|
|
|
|
#[error("recur must be in tail position of its enclosing loop")]
|
|
|
|
|
RecurNotInTailPosition,
|
|
|
|
|
|
|
|
|
|
/// Iter it.2: a recursive call (self or mutual-group) passes a
|
|
|
|
|
/// non-structurally-smaller argument at every candidate structural
|
|
|
|
|
/// position. The author must express this iteration as an explicit
|
|
|
|
|
/// `(loop …)` / `recur` instead. Spec
|
|
|
|
|
/// `docs/specs/2026-05-15-iteration-discipline.md` (D1/D2). Display
|
|
|
|
|
/// body is bracket-`[code]`-free per the F2 convention — the CLI
|
|
|
|
|
/// formatter prepends the `[code]`.
|
|
|
|
|
#[error("recursive call to `{callee}` is not on a structurally-smaller argument (`{arg}`); express this iteration as `(loop …)` / `recur`")]
|
|
|
|
|
NonStructuralRecursion { callee: String, arg: String },
|
|
|
|
|
|
|
|
|
|
/// Iter 22b.3: an internal invariant in the typechecker / mono pass
|
|
|
|
|
/// was violated — surfaced as an error so callers can propagate
|
|
|
|
|
/// rather than abort, but in well-formed inputs (typecheck has
|
|
|
|
@@ -782,6 +792,7 @@ impl CheckError {
|
|
|
|
|
CheckError::RecurArityMismatch { .. } => "recur-arity-mismatch",
|
|
|
|
|
CheckError::RecurTypeMismatch { .. } => "recur-type-mismatch",
|
|
|
|
|
CheckError::RecurNotInTailPosition => "recur-not-in-tail-position",
|
|
|
|
|
CheckError::NonStructuralRecursion { .. } => "non-structural-recursion",
|
|
|
|
|
CheckError::Internal(_) => "internal",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -862,6 +873,9 @@ impl CheckError {
|
|
|
|
|
CheckError::RecurTypeMismatch { pos, name, got, want } => {
|
|
|
|
|
serde_json::json!({"pos": pos, "name": name, "expected": want, "actual": got})
|
|
|
|
|
}
|
|
|
|
|
CheckError::NonStructuralRecursion { callee, arg } => {
|
|
|
|
|
serde_json::json!({ "callee": callee, "arg": arg })
|
|
|
|
|
}
|
|
|
|
|
_ => serde_json::Value::Object(serde_json::Map::new()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -1704,8 +1718,20 @@ fn check_in_workspace(
|
|
|
|
|
env.imports = import_map;
|
|
|
|
|
env.current_module = m.name.clone();
|
|
|
|
|
|
|
|
|
|
// Iter it.2 (DD-3): the module's `Def::Fn`s, for the
|
|
|
|
|
// mutual-structural-group analysis. A single owned Vec built once
|
|
|
|
|
// per module; `verify_structural_recursion` reads it to decide
|
|
|
|
|
// whether a cross-call is to a same-group member.
|
|
|
|
|
let module_fns: Vec<&FnDef> = m
|
|
|
|
|
.defs
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|d| match d {
|
|
|
|
|
Def::Fn(f) => Some(f),
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
for def in &m.defs {
|
|
|
|
|
match check_def(def, &env, out_warnings) {
|
|
|
|
|
match check_def(def, &env, &module_fns, out_warnings) {
|
|
|
|
|
Ok(()) => {}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
errors.push(CheckError::Def(def.name().to_string(), Box::new(e)));
|
|
|
|
@@ -1723,10 +1749,11 @@ fn check_in_workspace(
|
|
|
|
|
fn check_def(
|
|
|
|
|
def: &Def,
|
|
|
|
|
env: &Env,
|
|
|
|
|
module_fns: &[&FnDef],
|
|
|
|
|
out_warnings: &mut Vec<Diagnostic>,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
match def {
|
|
|
|
|
Def::Fn(f) => check_fn(f, env, out_warnings),
|
|
|
|
|
Def::Fn(f) => check_fn(f, env, module_fns, out_warnings),
|
|
|
|
|
Def::Const(c) => check_const(c, env, out_warnings),
|
|
|
|
|
Def::Type(td) => check_type_def(td, env),
|
|
|
|
|
// bugfix-instance-body-unbound-var (2026-05-13): each instance
|
|
|
|
@@ -1825,7 +1852,10 @@ fn check_instance(
|
|
|
|
|
doc: None,
|
|
|
|
|
suppress: Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
check_fn(&synthetic, env, out_warnings)?;
|
|
|
|
|
// Iter it.2: a synthetic instance-method fn is not a
|
|
|
|
|
// module-level def and does not participate in the
|
|
|
|
|
// mutual-structural-group analysis; pass no siblings.
|
|
|
|
|
check_fn(&synthetic, env, &[], out_warnings)?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
@@ -1918,7 +1948,12 @@ fn check_type_well_formed(t: &Type, env: &Env) -> Result<()> {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn check_fn(f: &FnDef, env: &Env, out_warnings: &mut Vec<Diagnostic>) -> Result<()> {
|
|
|
|
|
fn check_fn(
|
|
|
|
|
f: &FnDef,
|
|
|
|
|
env: &Env,
|
|
|
|
|
module_fns: &[&FnDef],
|
|
|
|
|
out_warnings: &mut Vec<Diagnostic>,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
// Peel an outer Forall (Iter 12a). The vars become rigid in the
|
|
|
|
|
// inner env so they pass `check_type_well_formed` and unify only
|
|
|
|
|
// with themselves. An empty `vars` list (vacuously polymorphic)
|
|
|
|
@@ -2020,6 +2055,29 @@ fn check_fn(f: &FnDef, env: &Env, out_warnings: &mut Vec<Diagnostic>) -> Result<
|
|
|
|
|
// call doesn't drown out the underlying type error.
|
|
|
|
|
verify_tail_positions(&f.body, true)?;
|
|
|
|
|
|
|
|
|
|
// Iter it.2 (DD-1): structural-recursion guardedness. Sibling of
|
|
|
|
|
// `verify_tail_positions`; runs on the whole post-synth body so
|
|
|
|
|
// it can build the `smaller`-set provenance before judging any
|
|
|
|
|
// recursive call. Out of scope for synthetic Forall-peeled types
|
|
|
|
|
// — it reads `f.ty` directly via `adt_param_positions`.
|
|
|
|
|
verify_structural_recursion(f, &env, module_fns)?;
|
|
|
|
|
|
|
|
|
|
// Iter it.2 (DD-4 / D2): the first real `Diverge` effect. A fn
|
|
|
|
|
// whose body syntactically contains a `Term::Loop` raises
|
|
|
|
|
// `Diverge`, exactly as a `do print` raises `IO`. The "calls a
|
|
|
|
|
// `Diverge`-declaring callee" half needs no code here — a
|
|
|
|
|
// callee's `Type::Fn.effects` already flows into `effects`
|
|
|
|
|
// during synth (identically to `IO`). The existing
|
|
|
|
|
// declared-vs-raised reconciliation below turns an undeclared
|
|
|
|
|
// `Diverge` into the existing `UndeclaredEffect` — no new
|
|
|
|
|
// diagnostic variant. Structural recursion injects nothing (it
|
|
|
|
|
// contains no `Term::Loop`). The lam boundary is honoured by
|
|
|
|
|
// `term_contains_loop` (a loop under a `Term::Lam` is that lam's
|
|
|
|
|
// arrow effect, reconciled at the lam sub-effect site).
|
|
|
|
|
if term_contains_loop(&f.body) {
|
|
|
|
|
effects.insert("Diverge".to_string());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let declared: BTreeSet<String> = declared_effs.into_iter().collect();
|
|
|
|
|
for e in &effects {
|
|
|
|
|
if !declared.contains(e) {
|
|
|
|
@@ -2810,6 +2868,693 @@ fn verify_loop_body(t: &Term) -> Result<()> {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Iter it.2 (DD-4): true iff `t` syntactically contains a
|
|
|
|
|
/// `Term::Loop`, **not** descending into `Term::Lam` bodies. A
|
|
|
|
|
/// lambda is a value with its own arrow effect row — a loop inside
|
|
|
|
|
/// it executes on closure call, not here, so it carries `Diverge`
|
|
|
|
|
/// on the lam's arrow type (handled at the lam sub-effect reconcile
|
|
|
|
|
/// site), not on the enclosing fn. This mirrors exactly how `!IO`
|
|
|
|
|
/// inside a lam does not leak to the enclosing fn.
|
|
|
|
|
fn term_contains_loop(t: &Term) -> bool {
|
|
|
|
|
match t {
|
|
|
|
|
Term::Loop { .. } => true,
|
|
|
|
|
Term::Lam { .. } => false,
|
|
|
|
|
Term::Lit { .. } | Term::Var { .. } => false,
|
|
|
|
|
Term::Recur { args } => args.iter().any(term_contains_loop),
|
|
|
|
|
Term::App { callee, args, .. } => {
|
|
|
|
|
term_contains_loop(callee) || args.iter().any(term_contains_loop)
|
|
|
|
|
}
|
|
|
|
|
Term::Do { args, .. } => args.iter().any(term_contains_loop),
|
|
|
|
|
Term::Let { value, body, .. } => {
|
|
|
|
|
term_contains_loop(value) || term_contains_loop(body)
|
|
|
|
|
}
|
|
|
|
|
Term::LetRec { body, in_term, .. } => {
|
|
|
|
|
term_contains_loop(body) || term_contains_loop(in_term)
|
|
|
|
|
}
|
|
|
|
|
Term::If { cond, then, else_ } => {
|
|
|
|
|
term_contains_loop(cond)
|
|
|
|
|
|| term_contains_loop(then)
|
|
|
|
|
|| term_contains_loop(else_)
|
|
|
|
|
}
|
|
|
|
|
Term::Seq { lhs, rhs } => {
|
|
|
|
|
term_contains_loop(lhs) || term_contains_loop(rhs)
|
|
|
|
|
}
|
|
|
|
|
Term::Match { scrutinee, arms } => {
|
|
|
|
|
term_contains_loop(scrutinee)
|
|
|
|
|
|| arms.iter().any(|a| term_contains_loop(&a.body))
|
|
|
|
|
}
|
|
|
|
|
Term::Ctor { args, .. } => args.iter().any(term_contains_loop),
|
|
|
|
|
Term::Clone { value } => term_contains_loop(value),
|
|
|
|
|
Term::ReuseAs { source, body } => {
|
|
|
|
|
term_contains_loop(source) || term_contains_loop(body)
|
|
|
|
|
}
|
|
|
|
|
Term::Mut { vars, body } => {
|
|
|
|
|
vars.iter().any(|v| term_contains_loop(&v.init))
|
|
|
|
|
|| term_contains_loop(body)
|
|
|
|
|
}
|
|
|
|
|
Term::Assign { value, .. } => term_contains_loop(value),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Iter it.2: structural-recursion guardedness (DD-1/DD-2/DD-3) ──
|
|
|
|
|
//
|
|
|
|
|
// A recursive call (self, or to a same-ADT-family mutual-group
|
|
|
|
|
// member) must pass a structurally-smaller argument at some
|
|
|
|
|
// inferable parameter position. Accumulator positions are
|
|
|
|
|
// unconstrained (spec D1). it.2-only grandfather: a `tail:true`
|
|
|
|
|
// recursive call is not collected at all, so it never causes
|
|
|
|
|
// rejection (the 21 `tail-app` corpus fixtures stay clean through
|
|
|
|
|
// it.2; it.3 removes the `tail` field and this exemption together).
|
|
|
|
|
|
|
|
|
|
/// One collected recursive call: the callee name, its argument
|
|
|
|
|
/// terms, and the `smaller` set live at the call site. `tail:true`
|
|
|
|
|
/// recursive calls are never collected (the grandfather).
|
|
|
|
|
struct RecCall {
|
|
|
|
|
callee: String,
|
|
|
|
|
args: Vec<Term>,
|
|
|
|
|
smaller: HashSet<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Resolve a declared type to its ADT `type`-decl name, if it is a
|
|
|
|
|
/// non-primitive `Type::Con` that names a `type` decl reachable from
|
|
|
|
|
/// `env` (bare or one-dot-qualified). `Type::Fn`, `Type::Var`,
|
|
|
|
|
/// `Type::Forall`, and primitive cons return `None`.
|
|
|
|
|
fn adt_type_head(t: &Type, env: &Env) -> Option<String> {
|
|
|
|
|
let Type::Con { name, .. } = t else {
|
|
|
|
|
return None;
|
|
|
|
|
};
|
|
|
|
|
if ailang_core::primitives::is_primitive_name(name) {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
if name.matches('.').count() == 1 {
|
|
|
|
|
let (prefix, suffix) = name.split_once('.').expect("checked");
|
|
|
|
|
let target = env.imports.get(prefix)?;
|
|
|
|
|
if env
|
|
|
|
|
.module_types
|
|
|
|
|
.get(target)
|
|
|
|
|
.and_then(|tys| tys.get(suffix))
|
|
|
|
|
.is_some()
|
|
|
|
|
{
|
|
|
|
|
return Some(name.clone());
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
} else if env.types.contains_key(name) {
|
|
|
|
|
Some(name.clone())
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Candidate structural parameter positions of `f`: indices whose
|
|
|
|
|
/// declared type is a non-primitive ADT `Type::Con` (DD-2).
|
|
|
|
|
fn adt_param_positions(f: &FnDef, env: &Env) -> Vec<usize> {
|
|
|
|
|
let inner = match &f.ty {
|
|
|
|
|
Type::Forall { body, .. } => (**body).clone(),
|
|
|
|
|
other => other.clone(),
|
|
|
|
|
};
|
|
|
|
|
let Type::Fn { params, .. } = inner else {
|
|
|
|
|
return Vec::new();
|
|
|
|
|
};
|
|
|
|
|
params
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.filter_map(|(i, p)| adt_type_head(p, env).map(|_| i))
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The constructor-bound field names of a flat post-desugar
|
|
|
|
|
/// pattern. `Pattern::Ctor` fields are `Var`/`Wild` (nested ctor
|
|
|
|
|
/// patterns were rejected upstream by `NestedCtorPatternNotAllowed`).
|
|
|
|
|
fn ctor_bound_names(p: &Pattern) -> Vec<String> {
|
|
|
|
|
match p {
|
|
|
|
|
Pattern::Ctor { fields, .. } => fields
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|f| match f {
|
|
|
|
|
Pattern::Var { name } => Some(name.clone()),
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
.collect(),
|
|
|
|
|
_ => Vec::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// True iff `name` is the recursion name `f` calls itself by, or a
|
|
|
|
|
/// same-group mutual member (filled in Task 3 via `group`).
|
|
|
|
|
fn is_rec_callee(name: &str, rec_name: &str, group: &HashSet<String>) -> bool {
|
|
|
|
|
name == rec_name || group.contains(name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Single `smaller`-threaded walk. Collects every non-`tail`
|
|
|
|
|
/// recursive call together with the `smaller` set in effect at that
|
|
|
|
|
/// syntactic position. `param_smaller_seed` is the set of parameter
|
|
|
|
|
/// names that are *themselves* candidate structural roots — a
|
|
|
|
|
/// `match` on one of them (or on an already-smaller var) extends
|
|
|
|
|
/// `smaller` with that arm's constructor-bound fields. `recur` /
|
|
|
|
|
/// `loop` bodies are walked (a `recur` is not a recursive *call*);
|
|
|
|
|
/// `Term::Lam` is NOT descended into (DD-3/DD-4 lam boundary).
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
|
fn collect_rec_calls_walk(
|
|
|
|
|
t: &Term,
|
|
|
|
|
rec_name: &str,
|
|
|
|
|
group: &HashSet<String>,
|
|
|
|
|
param_roots: &HashSet<String>,
|
|
|
|
|
smaller: &HashSet<String>,
|
|
|
|
|
out: &mut Vec<RecCall>,
|
|
|
|
|
) {
|
|
|
|
|
match t {
|
|
|
|
|
Term::Lit { .. } | Term::Var { .. } => {}
|
|
|
|
|
Term::App { callee, args, tail } => {
|
|
|
|
|
if !*tail {
|
|
|
|
|
if let Term::Var { name } = &**callee {
|
|
|
|
|
if is_rec_callee(name, rec_name, group) {
|
|
|
|
|
out.push(RecCall {
|
|
|
|
|
callee: name.clone(),
|
|
|
|
|
args: args.clone(),
|
|
|
|
|
smaller: smaller.clone(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
collect_rec_calls_walk(callee, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
for a in args {
|
|
|
|
|
collect_rec_calls_walk(a, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Term::Do { args, .. } => {
|
|
|
|
|
for a in args {
|
|
|
|
|
collect_rec_calls_walk(a, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Term::Let { name, value, body } => {
|
|
|
|
|
collect_rec_calls_walk(value, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
// Alias propagation: `let v = <var>` where the bound term
|
|
|
|
|
// is a structural root (or already strictly smaller)
|
|
|
|
|
// makes `v` carry the same status in `body`. This is what
|
|
|
|
|
// makes the desugar-introduced `let $mp_N = <scrutinee>`
|
|
|
|
|
// (Iter 16a nested-pattern flattening) transparent to the
|
|
|
|
|
// guardedness walk — without it every nested-ctor-pattern
|
|
|
|
|
// recursion would be a false `NonStructuralRecursion`.
|
|
|
|
|
if let Term::Var { name: src } = &**value {
|
|
|
|
|
if smaller.contains(src) {
|
|
|
|
|
let mut s = smaller.clone();
|
|
|
|
|
s.insert(name.clone());
|
|
|
|
|
collect_rec_calls_walk(body, rec_name, group, param_roots, &s, out);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if param_roots.contains(src) {
|
|
|
|
|
let mut r = param_roots.clone();
|
|
|
|
|
r.insert(name.clone());
|
|
|
|
|
collect_rec_calls_walk(body, rec_name, group, &r, smaller, out);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
collect_rec_calls_walk(body, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
}
|
|
|
|
|
Term::LetRec { body, in_term, .. } => {
|
|
|
|
|
collect_rec_calls_walk(body, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
collect_rec_calls_walk(in_term, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
}
|
|
|
|
|
Term::If { cond, then, else_ } => {
|
|
|
|
|
collect_rec_calls_walk(cond, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
collect_rec_calls_walk(then, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
collect_rec_calls_walk(else_, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
}
|
|
|
|
|
Term::Seq { lhs, rhs } => {
|
|
|
|
|
collect_rec_calls_walk(lhs, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
collect_rec_calls_walk(rhs, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
}
|
|
|
|
|
Term::Match { scrutinee, arms } => {
|
|
|
|
|
collect_rec_calls_walk(scrutinee, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
// A match on a structural root (or an already-smaller var)
|
|
|
|
|
// makes that arm's constructor-bound fields strictly
|
|
|
|
|
// smaller. Other scrutinees do not extend `smaller`.
|
|
|
|
|
let extends = match &**scrutinee {
|
|
|
|
|
Term::Var { name } => param_roots.contains(name) || smaller.contains(name),
|
|
|
|
|
_ => false,
|
|
|
|
|
};
|
|
|
|
|
for arm in arms {
|
|
|
|
|
if extends {
|
|
|
|
|
let mut s = smaller.clone();
|
|
|
|
|
for n in ctor_bound_names(&arm.pat) {
|
|
|
|
|
s.insert(n);
|
|
|
|
|
}
|
|
|
|
|
collect_rec_calls_walk(&arm.body, rec_name, group, param_roots, &s, out);
|
|
|
|
|
} else {
|
|
|
|
|
collect_rec_calls_walk(&arm.body, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Term::Ctor { args, .. } => {
|
|
|
|
|
for a in args {
|
|
|
|
|
collect_rec_calls_walk(a, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// DD-3/DD-4: a lambda body is a separate def's territory; do
|
|
|
|
|
// not descend (consistent with the Diverge lam boundary).
|
|
|
|
|
Term::Lam { .. } => {}
|
|
|
|
|
Term::Clone { value } => {
|
|
|
|
|
collect_rec_calls_walk(value, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
}
|
|
|
|
|
Term::ReuseAs { source, body } => {
|
|
|
|
|
collect_rec_calls_walk(source, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
collect_rec_calls_walk(body, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
}
|
|
|
|
|
Term::Mut { vars, body } => {
|
|
|
|
|
for v in vars {
|
|
|
|
|
collect_rec_calls_walk(&v.init, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
}
|
|
|
|
|
collect_rec_calls_walk(body, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
}
|
|
|
|
|
Term::Assign { value, .. } => {
|
|
|
|
|
collect_rec_calls_walk(value, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
}
|
|
|
|
|
Term::Loop { binders, body } => {
|
|
|
|
|
for b in binders {
|
|
|
|
|
collect_rec_calls_walk(&b.init, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
}
|
|
|
|
|
collect_rec_calls_walk(body, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
}
|
|
|
|
|
Term::Recur { args } => {
|
|
|
|
|
for a in args {
|
|
|
|
|
collect_rec_calls_walk(a, rec_name, group, param_roots, smaller, out);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether the recursive call `c` is structurally guarded at the
|
|
|
|
|
/// candidate position `i`: its `i`-th argument is a bare
|
|
|
|
|
/// `Term::Var` whose name is in the `smaller` set at the call site.
|
|
|
|
|
fn call_guarded_at(c: &RecCall, i: usize) -> bool {
|
|
|
|
|
match c.args.get(i) {
|
|
|
|
|
Some(Term::Var { name }) => c.smaller.contains(name),
|
|
|
|
|
_ => false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Display form of a recursive call's offending argument (the arg
|
|
|
|
|
/// at the first candidate position, else the first arg), for the
|
|
|
|
|
/// diagnostic. Mirrors the short pretty form the it.1 `Recur*`
|
|
|
|
|
/// diagnostics use.
|
|
|
|
|
fn rec_call_arg_display(c: &RecCall, cand: &[usize]) -> String {
|
|
|
|
|
let idx = cand.first().copied().unwrap_or(0);
|
|
|
|
|
match c.args.get(idx).or_else(|| c.args.first()) {
|
|
|
|
|
Some(Term::Var { name }) => name.clone(),
|
|
|
|
|
Some(Term::Ctor { ctor, .. }) => format!("{ctor}(…)"),
|
|
|
|
|
Some(Term::App { callee, .. }) => format!("{}(…)", callee_name(callee)),
|
|
|
|
|
Some(Term::Lit { .. }) => "<literal>".to_string(),
|
|
|
|
|
Some(_) => "<expr>".to_string(),
|
|
|
|
|
None => "<no argument>".to_string(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// DD-1/DD-2/DD-3: structural-recursion guardedness for a single
|
|
|
|
|
/// `FnDef`. A recursive call (self, or — Task 3 — a same-family
|
|
|
|
|
/// mutual-group member) must be structurally guarded at some
|
|
|
|
|
/// parameter position that is structural at *every* such call.
|
|
|
|
|
fn verify_structural_recursion(
|
|
|
|
|
f: &FnDef,
|
|
|
|
|
env: &Env,
|
|
|
|
|
module_fns: &[&FnDef],
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
let cand = adt_param_positions(f, env);
|
|
|
|
|
let group = mutual_structural_group(f, module_fns, env);
|
|
|
|
|
|
|
|
|
|
// it.2 transitional boundary: a def with no ADT candidate
|
|
|
|
|
// position and no mutual cycle has no *structural* parameter to
|
|
|
|
|
// verify. Its recursion is integer-counter / other-shaped
|
|
|
|
|
// (`f(n) = … f(n - 1) …`) — genuinely non-structural, but its
|
|
|
|
|
// migration to `(loop …)` is the destructive it.3 corpus pass,
|
|
|
|
|
// not it.2's job. it.2 only rejects *misuse of an ADT structural
|
|
|
|
|
// position* (the spec's load-bearing case: the canonical
|
|
|
|
|
// `f(xs) = … f(xs) …` negative still fires because it HAS an ADT
|
|
|
|
|
// candidate). This keeps the corpus clean through it.2 without
|
|
|
|
|
// weakening the ADT structural check — the `tail==false`
|
|
|
|
|
// grandfather alone is insufficient because corpus fixtures like
|
|
|
|
|
// `build_tree(depth: Int)` recurse non-tail on a primitive arg.
|
|
|
|
|
// Recorded as a resolved spec/it.2-boundary clarification in the
|
|
|
|
|
// iter journal.
|
|
|
|
|
if cand.is_empty() && group.members.is_empty() {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let param_roots: HashSet<String> = cand
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|&i| f.params.get(i).cloned())
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
let mut calls: Vec<RecCall> = Vec::new();
|
|
|
|
|
collect_rec_calls_walk(
|
|
|
|
|
&f.body,
|
|
|
|
|
&f.name,
|
|
|
|
|
&group.members,
|
|
|
|
|
¶m_roots,
|
|
|
|
|
&HashSet::new(),
|
|
|
|
|
&mut calls,
|
|
|
|
|
);
|
|
|
|
|
if calls.is_empty() {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Per-call guardedness verdict:
|
|
|
|
|
// - self-call (`callee == f.name`): guarded iff some candidate
|
|
|
|
|
// position of `f` receives a structurally-smaller arg.
|
|
|
|
|
// - cross-call into a mutual-cycle member: only meaningful if the
|
|
|
|
|
// cycle is a valid same-family group (`family_valid`); then
|
|
|
|
|
// guarded iff some candidate position of the *callee* receives
|
|
|
|
|
// a var in the caller's `smaller` set (DD-3). A cross-call into
|
|
|
|
|
// a family-INVALID cycle is unconditionally unguarded — the
|
|
|
|
|
// cross-family negative.
|
|
|
|
|
let guarded = |c: &RecCall| -> bool {
|
|
|
|
|
if c.callee == f.name {
|
|
|
|
|
cand.iter().any(|&i| call_guarded_at(c, i))
|
|
|
|
|
} else if !group.family_valid {
|
|
|
|
|
false
|
|
|
|
|
} else {
|
|
|
|
|
module_fns
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|g| g.name == c.callee)
|
|
|
|
|
.map(|g| adt_param_positions(g, env))
|
|
|
|
|
.map(|gc| gc.iter().any(|&i| call_guarded_at(c, i)))
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Structural iff there is a single candidate position of `f`
|
|
|
|
|
// structural at *every* self-call (the implicit-inference rule,
|
|
|
|
|
// DD-2) AND every cross-call into the mutual cycle is guarded.
|
|
|
|
|
let self_calls: Vec<&RecCall> = calls.iter().filter(|c| c.callee == f.name).collect();
|
|
|
|
|
let self_clear = self_calls.is_empty()
|
|
|
|
|
|| cand
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|&i| self_calls.iter().all(|c| call_guarded_at(c, i)));
|
|
|
|
|
let cross_clear = calls
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|c| c.callee != f.name)
|
|
|
|
|
.all(guarded);
|
|
|
|
|
|
|
|
|
|
if self_clear && cross_clear {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Not structural — emit on the first unguarded recursive call
|
|
|
|
|
// (all collected calls are already non-`tail`; the `tail:true`
|
|
|
|
|
// grandfather filtered them out at collection time).
|
|
|
|
|
let offending = calls.iter().find(|c| !guarded(c)).unwrap_or(&calls[0]);
|
|
|
|
|
Err(CheckError::NonStructuralRecursion {
|
|
|
|
|
callee: offending.callee.clone(),
|
|
|
|
|
arg: rec_call_arg_display(offending, &cand),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Tiny `BTreeMap`-backed union-find over type names (DD-3). No
|
|
|
|
|
/// external dependency; path-halving find, union-by-insertion.
|
|
|
|
|
struct TypeUnionFind {
|
|
|
|
|
parent: BTreeMap<String, String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TypeUnionFind {
|
|
|
|
|
fn new() -> Self {
|
|
|
|
|
TypeUnionFind { parent: BTreeMap::new() }
|
|
|
|
|
}
|
|
|
|
|
fn ensure(&mut self, x: &str) {
|
|
|
|
|
if !self.parent.contains_key(x) {
|
|
|
|
|
self.parent.insert(x.to_string(), x.to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fn find(&mut self, x: &str) -> String {
|
|
|
|
|
self.ensure(x);
|
|
|
|
|
let mut cur = x.to_string();
|
|
|
|
|
while self.parent[&cur] != cur {
|
|
|
|
|
let grand = self.parent[&self.parent[&cur]].clone();
|
|
|
|
|
self.parent.insert(cur.clone(), grand.clone());
|
|
|
|
|
cur = self.parent[&cur].clone();
|
|
|
|
|
}
|
|
|
|
|
cur
|
|
|
|
|
}
|
|
|
|
|
fn union(&mut self, a: &str, b: &str) {
|
|
|
|
|
let ra = self.find(a);
|
|
|
|
|
let rb = self.find(b);
|
|
|
|
|
if ra != rb {
|
|
|
|
|
self.parent.insert(ra, rb);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fn same(&mut self, a: &str, b: &str) -> bool {
|
|
|
|
|
self.find(a) == self.find(b)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Collect the `Type::Con` head names referenced anywhere inside a
|
|
|
|
|
/// type (recursing through `Fn`/`Forall`/`Con` args), skipping
|
|
|
|
|
/// primitives and type vars.
|
|
|
|
|
fn referenced_con_names(t: &Type, out: &mut Vec<String>) {
|
|
|
|
|
match t {
|
|
|
|
|
Type::Con { name, args } => {
|
|
|
|
|
if !ailang_core::primitives::is_primitive_name(name) {
|
|
|
|
|
out.push(name.clone());
|
|
|
|
|
}
|
|
|
|
|
for a in args {
|
|
|
|
|
referenced_con_names(a, out);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Type::Fn { params, ret, .. } => {
|
|
|
|
|
for p in params {
|
|
|
|
|
referenced_con_names(p, out);
|
|
|
|
|
}
|
|
|
|
|
referenced_con_names(ret, out);
|
|
|
|
|
}
|
|
|
|
|
Type::Forall { body, .. } => referenced_con_names(body, out),
|
|
|
|
|
Type::Var { .. } => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// DD-3: connected components of the ADT type-reference graph.
|
|
|
|
|
/// Nodes are every visible `type` decl name; an undirected edge
|
|
|
|
|
/// joins `T` and any non-primitive `Con` head appearing in one of
|
|
|
|
|
/// `T`'s constructor field types. The result answers
|
|
|
|
|
/// "are these two ADT heads in one family?".
|
|
|
|
|
fn adt_families(env: &Env) -> TypeUnionFind {
|
|
|
|
|
let mut uf = TypeUnionFind::new();
|
|
|
|
|
// All visible TypeDefs: the current module's plus every module's
|
|
|
|
|
// (workspace-flat). Bare names index the current module; that is
|
|
|
|
|
// the resolution `adt_type_head` already uses for fixtures.
|
|
|
|
|
let mut all: Vec<(&String, &TypeDef)> = env.types.iter().collect();
|
|
|
|
|
for tys in env.module_types.values() {
|
|
|
|
|
all.extend(tys.iter());
|
|
|
|
|
}
|
|
|
|
|
for (name, td) in &all {
|
|
|
|
|
uf.ensure(name);
|
|
|
|
|
for ctor in &td.ctors {
|
|
|
|
|
for field in &ctor.fields {
|
|
|
|
|
let mut refs = Vec::new();
|
|
|
|
|
referenced_con_names(field, &mut refs);
|
|
|
|
|
for r in refs {
|
|
|
|
|
// Edge only between ADT decls (a referenced name
|
|
|
|
|
// that is not a known type decl, e.g. a builtin
|
|
|
|
|
// wrapper, contributes no family edge).
|
|
|
|
|
uf.union(name, &r);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
uf
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// DD-3: the mutual-recursion cycle `f` belongs to — the set of
|
|
|
|
|
/// *other* module fn names that `f` reaches and that reach `f`
|
|
|
|
|
/// through direct `Term::App` → `Term::Var` recursion (the
|
|
|
|
|
/// back-reaching connected component of the call graph restricted
|
|
|
|
|
/// to `module_fns`), plus whether that cycle is a valid same-ADT-
|
|
|
|
|
/// family structural group. Empty `members` ⇒
|
|
|
|
|
/// `verify_structural_recursion` treats `f` as self-recursive only.
|
|
|
|
|
/// Non-empty `members` with `family_valid = false` ⇒ the cycle
|
|
|
|
|
/// spans unrelated ADT families: each cross-call is an unguarded
|
|
|
|
|
/// recursive call (the cross-family negative).
|
|
|
|
|
fn mutual_structural_group(
|
|
|
|
|
f: &FnDef,
|
|
|
|
|
module_fns: &[&FnDef],
|
|
|
|
|
env: &Env,
|
|
|
|
|
) -> MutualGroup {
|
|
|
|
|
// Direct-call adjacency among module fns (callee names that are
|
|
|
|
|
// module fns; ignore builtins / cross-module).
|
|
|
|
|
let names: HashSet<&str> = module_fns.iter().map(|g| g.name.as_str()).collect();
|
|
|
|
|
let direct_callees = |g: &FnDef| -> HashSet<String> {
|
|
|
|
|
let mut cs = Vec::new();
|
|
|
|
|
collect_direct_app_var_callees(&g.body, &mut cs);
|
|
|
|
|
cs.into_iter().filter(|c| names.contains(c.as_str())).collect()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Connected component of `f` in the (undirected) call graph.
|
|
|
|
|
let by_name: BTreeMap<&str, &FnDef> =
|
|
|
|
|
module_fns.iter().map(|g| (g.name.as_str(), *g)).collect();
|
|
|
|
|
let mut adj: BTreeMap<String, HashSet<String>> = BTreeMap::new();
|
|
|
|
|
for g in module_fns {
|
|
|
|
|
let cs = direct_callees(g);
|
|
|
|
|
for c in &cs {
|
|
|
|
|
adj.entry(g.name.clone()).or_default().insert(c.clone());
|
|
|
|
|
adj.entry(c.clone()).or_default().insert(g.name.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let mut comp: HashSet<String> = HashSet::new();
|
|
|
|
|
let mut stack = vec![f.name.clone()];
|
|
|
|
|
while let Some(n) = stack.pop() {
|
|
|
|
|
if !comp.insert(n.clone()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if let Some(neigh) = adj.get(&n) {
|
|
|
|
|
for m in neigh {
|
|
|
|
|
if !comp.contains(m) {
|
|
|
|
|
stack.push(m.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
comp.remove(&f.name);
|
|
|
|
|
// Members that genuinely participate in a recursive cycle with
|
|
|
|
|
// `f`: keep only those that (transitively) call back to `f`.
|
|
|
|
|
// For the corpus shapes (tree/forest, even/odd) the component is
|
|
|
|
|
// already the mutual cycle; a one-way helper call would not put
|
|
|
|
|
// the helper into a back-reaching cycle, so it drops out here.
|
|
|
|
|
let reaches = |start: &str, target: &str| -> bool {
|
|
|
|
|
let mut seen: HashSet<String> = HashSet::new();
|
|
|
|
|
let mut st = vec![start.to_string()];
|
|
|
|
|
while let Some(n) = st.pop() {
|
|
|
|
|
if n == target && n != start {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if !seen.insert(n.clone()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if let Some(g) = by_name.get(n.as_str()) {
|
|
|
|
|
for c in direct_callees(g) {
|
|
|
|
|
if c == target {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
st.push(c);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
false
|
|
|
|
|
};
|
|
|
|
|
let members: HashSet<String> = comp
|
|
|
|
|
.into_iter()
|
|
|
|
|
.filter(|m| reaches(&f.name, m) && reaches(m, &f.name))
|
|
|
|
|
.collect();
|
|
|
|
|
if members.is_empty() {
|
|
|
|
|
return MutualGroup { members, family_valid: true };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DD-3 family test: `f` and every cycle member must have a
|
|
|
|
|
// structural ADT parameter, and all those parameter type heads
|
|
|
|
|
// must be in ONE family component. If not, the cycle is not a
|
|
|
|
|
// valid mutual structural group — `family_valid = false` makes
|
|
|
|
|
// every cross-call into it an unguarded recursive call (the
|
|
|
|
|
// cross-family negative).
|
|
|
|
|
let mut uf = adt_families(env);
|
|
|
|
|
let head_of = |g: &FnDef| -> Option<String> {
|
|
|
|
|
let inner = match &g.ty {
|
|
|
|
|
Type::Forall { body, .. } => (**body).clone(),
|
|
|
|
|
other => other.clone(),
|
|
|
|
|
};
|
|
|
|
|
let Type::Fn { params, .. } = inner else { return None };
|
|
|
|
|
params.iter().find_map(|p| adt_type_head(p, env))
|
|
|
|
|
};
|
|
|
|
|
let family_valid = match head_of(f) {
|
|
|
|
|
None => false,
|
|
|
|
|
Some(f_head) => members.iter().all(|m| {
|
|
|
|
|
by_name
|
|
|
|
|
.get(m.as_str())
|
|
|
|
|
.and_then(|mg| head_of(mg))
|
|
|
|
|
.map(|h| uf.same(&f_head, &h))
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
MutualGroup { members, family_valid }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// DD-3: the mutual-recursion cycle `f` participates in, and
|
|
|
|
|
/// whether that cycle is a valid same-ADT-family structural group.
|
|
|
|
|
/// `members` is family-agnostic (used to collect cross-calls); a
|
|
|
|
|
/// cross-call into a `members` fn is judged structurally only when
|
|
|
|
|
/// `family_valid` holds, otherwise it is an unguarded recursive
|
|
|
|
|
/// call.
|
|
|
|
|
struct MutualGroup {
|
|
|
|
|
members: HashSet<String>,
|
|
|
|
|
family_valid: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Direct `Term::App` whose callee is a `Term::Var` — the callee
|
|
|
|
|
/// names reachable by direct application (no descent into `Lam`
|
|
|
|
|
/// bodies, consistent with the guardedness walk's lam boundary).
|
|
|
|
|
fn collect_direct_app_var_callees(t: &Term, out: &mut Vec<String>) {
|
|
|
|
|
match t {
|
|
|
|
|
Term::Lit { .. } | Term::Var { .. } | Term::Recur { .. } => {}
|
|
|
|
|
Term::App { callee, args, .. } => {
|
|
|
|
|
if let Term::Var { name } = &**callee {
|
|
|
|
|
out.push(name.clone());
|
|
|
|
|
}
|
|
|
|
|
collect_direct_app_var_callees(callee, out);
|
|
|
|
|
for a in args {
|
|
|
|
|
collect_direct_app_var_callees(a, out);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Term::Do { args, .. } => {
|
|
|
|
|
for a in args {
|
|
|
|
|
collect_direct_app_var_callees(a, out);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Term::Let { value, body, .. } => {
|
|
|
|
|
collect_direct_app_var_callees(value, out);
|
|
|
|
|
collect_direct_app_var_callees(body, out);
|
|
|
|
|
}
|
|
|
|
|
Term::LetRec { body, in_term, .. } => {
|
|
|
|
|
collect_direct_app_var_callees(body, out);
|
|
|
|
|
collect_direct_app_var_callees(in_term, out);
|
|
|
|
|
}
|
|
|
|
|
Term::If { cond, then, else_ } => {
|
|
|
|
|
collect_direct_app_var_callees(cond, out);
|
|
|
|
|
collect_direct_app_var_callees(then, out);
|
|
|
|
|
collect_direct_app_var_callees(else_, out);
|
|
|
|
|
}
|
|
|
|
|
Term::Seq { lhs, rhs } => {
|
|
|
|
|
collect_direct_app_var_callees(lhs, out);
|
|
|
|
|
collect_direct_app_var_callees(rhs, out);
|
|
|
|
|
}
|
|
|
|
|
Term::Match { scrutinee, arms } => {
|
|
|
|
|
collect_direct_app_var_callees(scrutinee, out);
|
|
|
|
|
for arm in arms {
|
|
|
|
|
collect_direct_app_var_callees(&arm.body, out);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Term::Ctor { args, .. } => {
|
|
|
|
|
for a in args {
|
|
|
|
|
collect_direct_app_var_callees(a, out);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Lam boundary (DD-3/DD-4): a closure body is a separate
|
|
|
|
|
// def's territory; do not descend.
|
|
|
|
|
Term::Lam { .. } => {}
|
|
|
|
|
Term::Clone { value } => collect_direct_app_var_callees(value, out),
|
|
|
|
|
Term::ReuseAs { source, body } => {
|
|
|
|
|
collect_direct_app_var_callees(source, out);
|
|
|
|
|
collect_direct_app_var_callees(body, out);
|
|
|
|
|
}
|
|
|
|
|
Term::Mut { vars, body } => {
|
|
|
|
|
for v in vars {
|
|
|
|
|
collect_direct_app_var_callees(&v.init, out);
|
|
|
|
|
}
|
|
|
|
|
collect_direct_app_var_callees(body, out);
|
|
|
|
|
}
|
|
|
|
|
Term::Assign { value, .. } => collect_direct_app_var_callees(value, out),
|
|
|
|
|
Term::Loop { binders, body } => {
|
|
|
|
|
for b in binders {
|
|
|
|
|
collect_direct_app_var_callees(&b.init, out);
|
|
|
|
|
}
|
|
|
|
|
collect_direct_app_var_callees(body, out);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn check_const(c: &ConstDef, env: &Env, out_warnings: &mut Vec<Diagnostic>) -> Result<()> {
|
|
|
|
|
// Const types are never polymorphic — a Forall here is rejected
|
|
|
|
|
// outright. Any other type passes through to `synth` as before.
|
|
|
|
@@ -3579,6 +4324,16 @@ pub(crate) fn synth(
|
|
|
|
|
}
|
|
|
|
|
let body_ty = body_ty?;
|
|
|
|
|
unify(ret_ty, &body_ty, subst)?;
|
|
|
|
|
// Iter it.2 (DD-4): a `(loop …)` directly in this lam's
|
|
|
|
|
// body makes the lam's arrow carry `Diverge` — coherent
|
|
|
|
|
// with how `!IO` scopes across the lam edge. A loop under
|
|
|
|
|
// a *further-nested* lam is that inner lam's concern;
|
|
|
|
|
// `term_contains_loop`'s own `Term::Lam => false` stops
|
|
|
|
|
// there. Reconciled against the lam's declared arrow
|
|
|
|
|
// effects exactly like every other raised effect.
|
|
|
|
|
if term_contains_loop(body) {
|
|
|
|
|
body_effects.insert("Diverge".to_string());
|
|
|
|
|
}
|
|
|
|
|
let declared: BTreeSet<String> = lam_effects.iter().cloned().collect();
|
|
|
|
|
for e in &body_effects {
|
|
|
|
|
if !declared.contains(e) {
|
|
|
|
@@ -3670,6 +4425,13 @@ pub(crate) fn synth(
|
|
|
|
|
let body_ty = body_ty?;
|
|
|
|
|
unify(&ret_ty, &body_ty, subst)?;
|
|
|
|
|
|
|
|
|
|
// Iter it.2 (DD-4): a `Term::LetRec` clause is a
|
|
|
|
|
// fn-equivalent (same as `check_fn` / `Term::Lam`); a
|
|
|
|
|
// `(loop …)` in its body makes its arrow carry `Diverge`.
|
|
|
|
|
// Same lam-boundary rule via `term_contains_loop`.
|
|
|
|
|
if term_contains_loop(body) {
|
|
|
|
|
body_effects.insert("Diverge".to_string());
|
|
|
|
|
}
|
|
|
|
|
let declared: BTreeSet<String> = declared_effs.into_iter().collect();
|
|
|
|
|
for e in &body_effects {
|
|
|
|
|
if !declared.contains(e) {
|
|
|
|
|