refactor: collapse duplicated helpers and drop dead code across check/core/codegen

Pure simplification pass, no behaviour change — hashes, canonical
forms, diagnostic codes, and emitted IR are byte-identical (hash-pin
and IR-pin tests unchanged and green).

ailang-check:
- strip_forall and collect_pattern_binders were triplicated verbatim
  across linearity.rs / reuse_shape.rs / uniqueness.rs; hoisted each to
  a single pub(crate) fn in lib.rs.
- mono.rs::pattern_binders was a fourth copy of collect_pattern_binders
  (iterative style, same result for every Pattern shape); deleted, now
  calls the shared fn.
- maybe_instantiate and expect_eq were single-call wrappers; inlined
  at their sole call sites and removed.

ailang-core:
- collect_used_in_pattern was a verbatim duplicate of pattern_binds;
  deleted, call site repointed. pattern_binds made private (no external
  callers; the doc-comment's lift_letrecs claim was stale).
- is_false serde helper replaced by std::ops::Not::not at the four
  skip_serializing_if sites (all plain-bool fields); helper removed.
- test-only any_nested_ctor / any_let_rec folded into one generic
  any_term(t, &pred) walker.
- removed dead test helpers tmp_dir / examples_dir, orphaned when their
  tests relocated to tests/workspace_pin.rs.

ailang-codegen:
- removed the write-only Emitter::types field, its populating loop, and
  the now-unused CtorInfo struct it fed.
- adt_drop_symbol / adt_partial_drop_symbol differed only in a prefix
  literal; folded into one adt_symbol(prefix, ...). Symbol strings
  unchanged.

Net -180 LOC.
This commit is contained in:
2026-06-02 01:39:35 +02:00
parent e25580e3a3
commit ac9171ebf0
10 changed files with 112 additions and 293 deletions
+4 -15
View File
@@ -41,7 +41,7 @@ pub struct Module {
/// from canonical JSON when `false`, so every pre-existing
/// fixture's hash is bit-stable. See prep.3 of the
/// kernel-extension-mechanics milestone.
#[serde(default, skip_serializing_if = "is_false")]
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub kernel: bool,
/// Imports of other modules. Resolved by the workspace loader as
/// `<root_dir>/<module>.ail.json`.
@@ -174,7 +174,7 @@ pub struct TypeDef {
#[serde(
default,
rename = "drop-iterative",
skip_serializing_if = "is_false"
skip_serializing_if = "std::ops::Not::not"
)]
pub drop_iterative: bool,
/// Closed-set restriction on type-parameter instantiation. Maps
@@ -452,7 +452,7 @@ pub enum Term {
callee: Box<Term>,
#[serde(default)]
args: Vec<Term>,
#[serde(default, skip_serializing_if = "is_false")]
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
tail: bool,
},
/// Let-binding: `value` is evaluated and bound to `name` in `body`.
@@ -494,7 +494,7 @@ pub enum Term {
Do {
op: String,
args: Vec<Term>,
#[serde(default, skip_serializing_if = "is_false")]
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
tail: bool,
},
/// Constructor application. `type_name` binds the ADT, `ctor` the
@@ -903,17 +903,6 @@ impl PartialEq for Type {
}
impl Eq for Type {}
/// Serde helper for `#[serde(skip_serializing_if = "is_false")]`.
///
/// Used by [`Term::App::tail`] and [`Term::Do::tail`] so the `tail`
/// flag is omitted from the canonical JSON whenever it is false,
/// preserving bit-identical hashes for every fixture that does not
/// carry the flag.
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_false(b: &bool) -> bool {
!*b
}
#[cfg(test)]
mod tests {
use super::*;
+46 -93
View File
@@ -384,7 +384,7 @@ fn collect_used_in_term(t: &Term, used: &mut BTreeSet<String>) {
Term::Match { scrutinee, arms } => {
collect_used_in_term(scrutinee, used);
for arm in arms {
collect_used_in_pattern(&arm.pat, used);
pattern_binds(&arm.pat, used);
collect_used_in_term(&arm.body, used);
}
}
@@ -439,22 +439,6 @@ fn collect_used_in_term(t: &Term, used: &mut BTreeSet<String>) {
}
}
/// Walks a pattern and inserts every [`Pattern::Var.name`] into `used`.
fn collect_used_in_pattern(p: &Pattern, used: &mut BTreeSet<String>) {
match p {
Pattern::Wild => {}
Pattern::Var { name } => {
used.insert(name.clone());
}
Pattern::Lit { .. } => {}
Pattern::Ctor { fields, .. } => {
for sub in fields {
collect_used_in_pattern(sub, used);
}
}
}
}
/// State carried across a module-wide desugar pass.
///
/// `counter` is incremented for every fresh-name request; `used`
@@ -1540,11 +1524,7 @@ fn rename_pattern_binders(pat: &Pattern, renamed: &BTreeMap<String, String>) ->
/// collect every name a pattern binds into `out`. Mirrors
/// `Pattern::pattern_bound_names` in `ailang-codegen`; duplicated here
/// because `ailang-core` cannot depend on the codegen crate.
///
/// made `pub` so callers (e.g. `ailang-check::lift_letrecs`)
/// can reproduce the same shadowing semantics during their own
/// scope-aware walks.
pub fn pattern_binds(p: &Pattern, out: &mut BTreeSet<String>) {
fn pattern_binds(p: &Pattern, out: &mut BTreeSet<String>) {
match p {
Pattern::Wild | Pattern::Lit { .. } => {}
Pattern::Var { name } => {
@@ -1949,85 +1929,58 @@ pub fn find_non_callee_use(t: &Term, name: &str) -> Option<Term> {
mod tests {
use super::*;
/// Generic test-only tree walk: returns true if `pred` holds for
/// `t` or any of its subterms. The structural recursion visits the
/// same subterm set every node-specific walker needs (callee/args,
/// let value+body, if branches, match scrutinee + arm bodies, etc.).
fn any_term(t: &Term, pred: &impl Fn(&Term) -> bool) -> bool {
if pred(t) {
return true;
}
match t {
Term::Lit { .. } | Term::Var { .. } | Term::Intrinsic => false,
Term::App { callee, args, .. } => {
any_term(callee, pred) || args.iter().any(|a| any_term(a, pred))
}
Term::Let { value, body, .. } => any_term(value, pred) || any_term(body, pred),
Term::If { cond, then, else_ } => {
any_term(cond, pred) || any_term(then, pred) || any_term(else_, pred)
}
Term::Do { args, .. } => args.iter().any(|a| any_term(a, pred)),
Term::Ctor { args, .. } => args.iter().any(|a| any_term(a, pred)),
Term::Match { scrutinee, arms } => {
any_term(scrutinee, pred) || arms.iter().any(|a| any_term(&a.body, pred))
}
Term::Lam { body, .. } => any_term(body, pred),
Term::Seq { lhs, rhs } => any_term(lhs, pred) || any_term(rhs, pred),
Term::LetRec { body, in_term, .. } => {
any_term(body, pred) || any_term(in_term, pred)
}
Term::Clone { value } => any_term(value, pred),
Term::ReuseAs { source, body } => any_term(source, pred) || any_term(body, pred),
Term::Loop { binders, body } => {
binders.iter().any(|b| any_term(&b.init, pred)) || any_term(body, pred)
}
Term::Recur { args } => args.iter().any(|a| any_term(a, pred)),
Term::New { args, .. } => args.iter().any(|arg| match arg {
NewArg::Value(v) => any_term(v, pred),
NewArg::Type(_) => false,
}),
}
}
/// Helper: walk a term, return true iff any [`Pattern::Ctor`] in
/// any reachable [`Term::Match`] has a non-flat field.
fn any_nested_ctor(t: &Term) -> bool {
match t {
Term::Lit { .. } | Term::Var { .. } => false,
Term::App { callee, args, .. } => {
any_nested_ctor(callee) || args.iter().any(any_nested_ctor)
}
Term::Let { value, body, .. } => any_nested_ctor(value) || any_nested_ctor(body),
Term::If { cond, then, else_ } => {
any_nested_ctor(cond) || any_nested_ctor(then) || any_nested_ctor(else_)
}
Term::Do { args, .. } => args.iter().any(any_nested_ctor),
Term::Ctor { args, .. } => args.iter().any(any_nested_ctor),
Term::Match { scrutinee, arms } => {
if any_nested_ctor(scrutinee) {
return true;
}
for a in arms {
if !is_flat(&a.pat) {
return true;
}
if any_nested_ctor(&a.body) {
return true;
}
}
false
}
Term::Lam { body, .. } => any_nested_ctor(body),
Term::Seq { lhs, rhs } => any_nested_ctor(lhs) || any_nested_ctor(rhs),
Term::LetRec { body, in_term, .. } => {
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),
Term::Loop { binders, body } => {
binders.iter().any(|b| any_nested_ctor(&b.init)) || any_nested_ctor(body)
}
Term::Recur { args } => args.iter().any(any_nested_ctor),
Term::New { args, .. } => args.iter().any(|arg| match arg {
NewArg::Value(v) => any_nested_ctor(v),
NewArg::Type(_) => false,
}),
Term::Intrinsic => false,
}
any_term(t, &|x| {
matches!(x, Term::Match { arms, .. } if arms.iter().any(|a| !is_flat(&a.pat)))
})
}
/// Helper: walk a term, return true iff any [`Term::LetRec`] is
/// reachable. After 16b.1 desugaring this should be `false`.
fn any_let_rec(t: &Term) -> bool {
match t {
Term::Lit { .. } | Term::Var { .. } => false,
Term::App { callee, args, .. } => {
any_let_rec(callee) || args.iter().any(any_let_rec)
}
Term::Let { value, body, .. } => any_let_rec(value) || any_let_rec(body),
Term::If { cond, then, else_ } => {
any_let_rec(cond) || any_let_rec(then) || any_let_rec(else_)
}
Term::Do { args, .. } => args.iter().any(any_let_rec),
Term::Ctor { args, .. } => args.iter().any(any_let_rec),
Term::Match { scrutinee, arms } => {
any_let_rec(scrutinee) || arms.iter().any(|a| any_let_rec(&a.body))
}
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),
Term::ReuseAs { source, body } => any_let_rec(source) || any_let_rec(body),
Term::Loop { binders, body } => {
binders.iter().any(|b| any_let_rec(&b.init)) || any_let_rec(body)
}
Term::Recur { args } => args.iter().any(any_let_rec),
Term::New { args, .. } => args.iter().any(|arg| match arg {
NewArg::Value(v) => any_let_rec(v),
NewArg::Type(_) => false,
}),
Term::Intrinsic => false,
}
any_term(t, &|x| matches!(x, Term::LetRec { .. }))
}
/// a `match` containing `(Cons a (Cons b _))` desugars to
-20
View File
@@ -1659,16 +1659,6 @@ mod tests {
path
}
fn tmp_dir(tag: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!(
"ailang_workspace_test_{tag}_{}",
std::process::id()
));
let _ = fs::remove_dir_all(&d);
fs::create_dir_all(&d).unwrap();
d
}
// `loads_example_workspace_happy_path` + `loads_workspace_auto_injects_prelude`
// relocated to `crates/ailang-core/tests/workspace_pin.rs` in iter
// form-a.1 Task 5 (use `ailang_surface::load_workspace` on `.ail`
@@ -1692,16 +1682,6 @@ mod tests {
// `iter22b1_workspace_with_no_classes_has_empty_registry` relocated
// to `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
fn examples_dir() -> PathBuf {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
Path::new(manifest_dir)
.parent()
.unwrap()
.parent()
.unwrap()
.join("examples")
}
// a coherent instance (in the class's module)
// loads cleanly and produces one registry entry from the
// fixture itself. With the auto-loaded prelude (iter 23.2)