From abcdd059916056eb2f3275b93efea4db8434a8ba Mon Sep 17 00:00:00 2001 From: Brummel Date: Thu, 14 May 2026 00:48:17 +0200 Subject: [PATCH] iter clippy-sweep: clear all 61 cargo clippy warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hygiene sweep across the workspace under `cargo clippy --workspace --all-targets`. Before: 61 warnings. After: zero. Tests stay 563 green, `cargo doc` stays at zero warnings, and all three bench scripts exit 0 against existing baselines. Twelve lint classes, three fix shapes: - Documentation hygiene (~32 hits): `doc_lazy_continuation` resolved by adding blank lines before continuation paragraphs or rephrasing the offending line; plus four `empty_line_after_doc_comments` hits in workspace.rs where eight `///` blocks left orphan by form-a.1 T5 test relocation were converted to plain `//`. - Idiomatic refactors (~16 hits): `.err().expect()` → `.expect_err()`, redundant `.into_iter()` and `.into()` removed, `|f| g(f)` → `g`, `.trim().split_whitespace()` → `.split_whitespace()`, `push_str("x")` → `push('x')`, two manual `impl Default` flipped to `#[derive(Default)]` + `#[default]`, two nested `if let Some(_) = …` collapsed. - Block extraction (1 hit): a 13-line block inside an `else if` condition in synth's Var-arm extracted to a new helper `is_class_method_dispatch(name, env)` next to `qualifier_is_class_shape`. Three `#[allow]`s with inline rationale where clippy's suggestion would lose meaning: `only_used_in_recursion` on walk_pattern (the parameter preserves the five-fn walker-framework signature uniformity), 2× `if_same_then_else` on qualify_local_types shapes (two branches encode semantically distinct disqualification reasons), `too_many_arguments` on `Emitter::new` (8 workspace-flat tables; bundling would just rename boilerplate). --- crates/ail/tests/e2e.rs | 12 +- crates/ail/tests/eq_ord_e2e.rs | 2 +- crates/ail/tests/typeclass_22b3.rs | 4 +- crates/ailang-check/src/lib.rs | 43 +++--- crates/ailang-check/src/lift.rs | 2 +- crates/ailang-check/src/linearity.rs | 2 +- crates/ailang-check/src/mono.rs | 38 ++--- crates/ailang-codegen/src/drop.rs | 37 +++-- crates/ailang-codegen/src/lib.rs | 14 +- crates/ailang-codegen/src/subst.rs | 5 + crates/ailang-core/src/ast.rs | 9 +- crates/ailang-core/src/desugar.rs | 2 +- crates/ailang-core/src/workspace.rs | 138 +++++++++--------- crates/ailang-prose/src/lib.rs | 3 +- crates/ailang-surface/src/lex.rs | 1 + crates/ailang-surface/src/parse.rs | 15 +- crates/ailang-surface/tests/loader.rs | 2 +- docs/journals/2026-05-14-iter-clippy-sweep.md | 134 +++++++++++++++++ docs/journals/INDEX.md | 1 + 19 files changed, 302 insertions(+), 162 deletions(-) create mode 100644 docs/journals/2026-05-14-iter-clippy-sweep.md diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index 8dbfa31..bb743c5 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -913,11 +913,12 @@ fn compare_primitives_smoke_prints_1_2_3_thrice() { } /// Iter 23.2: end-to-end coverage for the auto-loaded `Eq` class -/// + three primitive instances. The fixture calls `eq` on Int / -/// Bool / Str with both an equal and an unequal pair each; the -/// monomorphiser synthesises `eq__Int` / `eq__Bool` / `eq__Str` -/// in the prelude module; the binary prints six lines, one per -/// call. Equal-call lines are `1`, unequal-call lines are `0`. +/// and its three primitive instances. The fixture calls `eq` on +/// Int / Bool / Str with both an equal and an unequal pair each; +/// the monomorphiser synthesises `eq__Int` / `eq__Bool` / +/// `eq__Str` in the prelude module; the binary prints six lines, +/// one per call. Equal-call lines are `1`, unequal-call lines are +/// `0`. #[test] fn eq_primitives_smoke_compiles_and_runs() { let stdout = build_and_run("eq_primitives_smoke.ail"); @@ -1610,6 +1611,7 @@ fn alloc_rc_recursive_list_sum() { /// `@drop_rc_list_drop_borrow_IntList(ptr %t)` — recursively /// cascades through the 4-cell tail, freeing each cell. /// 5. Branches to the join. +/// /// And at `xs`'s let-close, the inlined partial drop skips slot 1 /// (already freed by the arm) and dec's the outer Cons cell. /// diff --git a/crates/ail/tests/eq_ord_e2e.rs b/crates/ail/tests/eq_ord_e2e.rs index b64a569..8a70659 100644 --- a/crates/ail/tests/eq_ord_e2e.rs +++ b/crates/ail/tests/eq_ord_e2e.rs @@ -10,7 +10,7 @@ //! calls `gt`/`not` is monomorphised correctly at Int / Bool / Str. //! 2. User-ADT integration with the unified mono pass //! (`eq_ord_user_adt_runs_end_to_end`). Property: `instance Eq T` -//! + `instance Ord T` for a user-defined `T` produces working +//! plus `instance Ord T` for a user-defined `T` produces working //! mono symbols when invoked through the prelude. //! 3. Mono-symbol body-hash stability for `eq__IntBox` //! (`eq_ord_user_adt_eq_intbox_hash_stable`). Property: the user diff --git a/crates/ail/tests/typeclass_22b3.rs b/crates/ail/tests/typeclass_22b3.rs index 0d3f6a7..999cedb 100644 --- a/crates/ail/tests/typeclass_22b3.rs +++ b/crates/ail/tests/typeclass_22b3.rs @@ -704,8 +704,8 @@ fn rewrite_replaces_class_method_var_with_mono_symbol_same_module() { } /// Helper: build & run a workspace via `ail run` and capture stdout -/// + stderr. The 22b.3 e2e tests below all funnel through this so the -/// failure-mode reporting is uniform. +/// and stderr. The 22b.3 e2e tests below all funnel through this so +/// the failure-mode reporting is uniform. fn ail_run(fixture_name: &str) -> (std::process::Output, String) { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let ail_bin = std::env::var("CARGO_BIN_EXE_ail") diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 5de24c1..3fee303 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -2363,6 +2363,7 @@ pub(crate) fn parse_method_qualifier(name: &str) -> (&str, Option) { /// - bare-class qualifier, e.g. `"Show.show"` — class names are /// PascalCase per repo convention (first character uppercase). /// - module-qualified class qualifier, e.g. `"prelude.Show.show"`. +/// /// Rejects qualifier shapes that look like cross-module-fn calls /// (`"std_list.length"`): qualifier starts with lowercase, no /// inner dot — falls through to the qualified-fn arm. @@ -2382,6 +2383,19 @@ pub(crate) fn qualifier_is_class_shape(qualifier_opt: &Option) -> bool { } } +/// mq.2 dispatch entry predicate: the synth Var-arm runs this to +/// decide whether `name` should be routed through the class-method +/// dispatch helper. Matches a bare method name (e.g. `"show"`) or a +/// class-shaped qualifier (`"Show.show"`, `"prelude.Show.show"`). +/// A 1-dot lowercase name (`"std_list.length"`) is structurally a +/// cross-module-fn call per mq.1's canonical-form rule and falls +/// through to the qualified-fn arm. +fn is_class_method_dispatch(name: &str, env: &Env) -> bool { + let (method_name, qualifier_opt) = parse_method_qualifier(name); + qualifier_is_class_shape(&qualifier_opt) + && env.method_to_candidate_classes.contains_key(method_name) +} + /// mq.tidy: predicate for the `class-method-shadowed-by-fn` /// warning emission. The full spec rule (§"Class-fn collisions" /// 161-162) requires a class candidate WITH AN INSTANCE FOR THE @@ -2750,20 +2764,7 @@ pub(crate) fn synth( // Bare name resolves through implicit import; the // unqualified name in the owner module is the same `name`. (qualified, Some((owner_module, name.clone()))) - } else if { - // mq.2 dispatch entry: the Var arm matches a class - // method either bare (`"show"`) or qualified - // (`"prelude.Show.show"`). `parse_method_qualifier` - // splits at the last dot; a `Some(qualifier)` with no - // inner dot is a 1-dot name (`std_list.length`) — - // structurally a cross-module-fn call per mq.1's - // canonical-form rule (class qualifiers are always - // `.`), so we fall through to the - // qualified-fn arm below in that case. - let (method_name, qualifier_opt) = parse_method_qualifier(name); - qualifier_is_class_shape(&qualifier_opt) - && env.method_to_candidate_classes.contains_key(method_name) - } { + } else if is_class_method_dispatch(name, env) { // mq.2: type-driven dispatch entry. The Var arm runs // before App-arm unification, so the arg type is not // available here. The dispatch helper resolves on @@ -3456,6 +3457,11 @@ fn maybe_instantiate(t: Type, counter: &mut u32) -> Type { fn qualify_local_types(t: &Type, owner_module: &str, local_types: &IndexMap) -> Type { match t { Type::Con { name, args } => { + // The first two branches share the body `name.clone()` but + // express semantically distinct reasons (already qualified; + // primitive needs no qualification). Combining them with + // `||` would obscure why each disqualifies the name. + #[allow(clippy::if_same_then_else)] let qualified_name = if name.contains('.') { name.clone() } else if ailang_core::primitives::is_primitive_name(name) { @@ -3726,9 +3732,10 @@ pub struct Env { /// mq.2: workspace-flat inverse of [`Self::class_methods`] — for /// each method name, the set of qualified class names that declare /// it. Pre-mq.3, the `MethodNameCollision` invariant guarantees - /// each set is singleton; mq.3 lifts the invariant and cardinality - /// > 1 becomes legal. Synth's `Term::Var` arm consults this index - /// for type-driven dispatch (spec §Architecture's 5-step rule). + /// each set is singleton; mq.3 lifts the invariant and any + /// cardinality above one becomes legal. Synth's `Term::Var` arm + /// consults this index for type-driven dispatch (spec + /// §Architecture's 5-step rule). pub method_to_candidate_classes: BTreeMap>, /// mq.3: post-superclass-expansion declared constraints of the /// active fn body being checked. Populated by `check_fn` before @@ -5512,7 +5519,7 @@ mod tests { ret_mode: ParamMode::Implicit, }), }, - vec!["x".into()], + vec!["x"], Term::Let { name: "z".into(), value: Box::new(Term::Lit { lit: Literal::Int { value: 7 } }), diff --git a/crates/ailang-check/src/lift.rs b/crates/ailang-check/src/lift.rs index 9b4c648..e35505a 100644 --- a/crates/ailang-check/src/lift.rs +++ b/crates/ailang-check/src/lift.rs @@ -198,7 +198,7 @@ pub fn lift_letrecs(m: &Module) -> Result { } } - out.defs.extend(lifter.lifted.into_iter()); + out.defs.extend(lifter.lifted); Ok(out) } diff --git a/crates/ailang-check/src/linearity.rs b/crates/ailang-check/src/linearity.rs index 3e17866..472ee96 100644 --- a/crates/ailang-check/src/linearity.rs +++ b/crates/ailang-check/src/linearity.rs @@ -714,7 +714,7 @@ fn collect_pattern_binders(p: &Pattern) -> Vec { Pattern::Var { name } => vec![name.clone()], Pattern::Ctor { fields, .. } => fields .iter() - .flat_map(|f| collect_pattern_binders(f)) + .flat_map(collect_pattern_binders) .collect(), } } diff --git a/crates/ailang-check/src/mono.rs b/crates/ailang-check/src/mono.rs index b657954..7cb38b3 100644 --- a/crates/ailang-check/src/mono.rs +++ b/crates/ailang-check/src/mono.rs @@ -1025,25 +1025,25 @@ fn rewrite_mono_calls( let is_poly_free_fn = !is_class_method && poly_free_fns.contains(name); let should_advance = (is_class_method || is_poly_free_fn) && !locals.contains(name); if should_advance { - if let Some(slot) = ordered_targets.get(*cursor) { - if let Some(t) = slot { - let (sym, defining_module) = match t { - MonoTarget::ClassMethod { method, type_, defining_module, .. } => { - (mono_symbol(method, type_), defining_module.as_str()) - } - MonoTarget::FreeFn { name: fn_name, type_args, defining_module } => { - (mono_symbol_n(fn_name, type_args.as_slice()), defining_module.as_str()) - } - }; - let new_name = if defining_module == caller_module { - sym - } else { - format!("{}.{}", defining_module, sym) - }; - *name = new_name; - } - // else: residual / observation was non-concrete → - // leave name unchanged. + // The outer `Some` means the cursor slot exists; the inner + // `Some` means the observation at that slot is concrete. + // A `Some(None)` slot is a residual / non-concrete + // observation and leaves the name unchanged. + if let Some(Some(t)) = ordered_targets.get(*cursor) { + let (sym, defining_module) = match t { + MonoTarget::ClassMethod { method, type_, defining_module, .. } => { + (mono_symbol(method, type_), defining_module.as_str()) + } + MonoTarget::FreeFn { name: fn_name, type_args, defining_module } => { + (mono_symbol_n(fn_name, type_args.as_slice()), defining_module.as_str()) + } + }; + let new_name = if defining_module == caller_module { + sym + } else { + format!("{}.{}", defining_module, sym) + }; + *name = new_name; } *cursor += 1; } diff --git a/crates/ailang-codegen/src/drop.rs b/crates/ailang-codegen/src/drop.rs index 57c5568..2bd839b 100644 --- a/crates/ailang-codegen/src/drop.rs +++ b/crates/ailang-codegen/src/drop.rs @@ -532,26 +532,24 @@ impl<'a> Emitter<'a> { // var on an as-yet-unmonomorphised polymorphic call — // the monomorphised copies will resolve correctly). Term::App { .. } => { - if let Ok(ret_ty) = self.synth_arg_type(value) { - if let Type::Con { name, .. } = ret_ty { - // Symmetric to `field_drop_call`'s Str arm: Str is a - // built-in pointer type with no per-type drop fn. Both - // heap-Str (rc_header at payload-8) and static-Str - // (codegen-elision keeps static pointers out of this - // path) consume via `ailang_rc_dec`. - if name == "Str" { - return "ailang_rc_dec".to_string(); - } - if name.matches('.').count() == 1 { - let (prefix, suffix) = - name.split_once('.').expect("checked"); - if let Some(target) = self.import_map.get(prefix) { - return format!("drop_{target}_{suffix}"); - } - return format!("drop_{prefix}_{suffix}"); - } - return format!("drop_{m}_{name}", m = self.module_name); + if let Ok(Type::Con { name, .. }) = self.synth_arg_type(value) { + // Symmetric to `field_drop_call`'s Str arm: Str is a + // built-in pointer type with no per-type drop fn. Both + // heap-Str (rc_header at payload-8) and static-Str + // (codegen-elision keeps static pointers out of this + // path) consume via `ailang_rc_dec`. + if name == "Str" { + return "ailang_rc_dec".to_string(); } + if name.matches('.').count() == 1 { + let (prefix, suffix) = + name.split_once('.').expect("checked"); + if let Some(target) = self.import_map.get(prefix) { + return format!("drop_{target}_{suffix}"); + } + return format!("drop_{prefix}_{suffix}"); + } + return format!("drop_{m}_{name}", m = self.module_name); } "ailang_rc_dec".to_string() } @@ -578,6 +576,7 @@ impl<'a> Emitter<'a> { /// match that moved out some fields; /// 3. [`Self::emit_inlined_partial_drop`]'s non-Ctor branch /// (let-binder whose value is `Term::App`). + /// /// All three share the same shape: a binder whose runtime ctor /// tag is dynamic (not statically known from a `Term::Ctor`) /// and whose `moved_slots` is a strict subset of its ptr fields. diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index b3d2bb7..6f14156 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -154,19 +154,14 @@ type Result = std::result::Result; /// Iter 18c once uniqueness inference is wired up. /// The IR is otherwise byte-identical between the three strategies /// modulo the allocator symbol name. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum AllocStrategy { + #[default] Gc, Bump, Rc, } -impl Default for AllocStrategy { - fn default() -> Self { - AllocStrategy::Gc - } -} - impl AllocStrategy { /// LLVM IR-level name of the allocator fn (without leading `@`). fn fn_name(self) -> &'static str { @@ -759,6 +754,11 @@ struct FnSig { } impl<'a> Emitter<'a> { + // The eight parameters are all the workspace-flat tables the + // emitter needs at construction time; bundling them into a + // context struct would just rename the boilerplate without + // reducing it. Keep the explicit-arg shape. + #[allow(clippy::too_many_arguments)] fn new( module: &'a Module, module_name: &'a str, diff --git a/crates/ailang-codegen/src/subst.rs b/crates/ailang-codegen/src/subst.rs index 4b5509c..de4c20b 100644 --- a/crates/ailang-codegen/src/subst.rs +++ b/crates/ailang-codegen/src/subst.rs @@ -163,6 +163,11 @@ pub(crate) fn qualify_local_types_codegen( ) -> Type { match t { Type::Con { name, args } => { + // The first two branches share the body `name.clone()` but + // express semantically distinct reasons (already qualified; + // primitive needs no qualification). Combining them with + // `||` would obscure why each disqualifies the name. + #[allow(clippy::if_same_then_else)] let qualified = if name.contains('.') { name.clone() } else if ailang_core::primitives::is_primitive_name(name) { diff --git a/crates/ailang-core/src/ast.rs b/crates/ailang-core/src/ast.rs index 2299518..c60983f 100644 --- a/crates/ailang-core/src/ast.rs +++ b/crates/ailang-core/src/ast.rs @@ -717,10 +717,11 @@ impl Type { /// /// `Own` and `Borrow` are author-asserted: the surface form /// `(own T)` / `(borrow T)` round-trips through this enum. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum ParamMode { /// Pre-18a / unannotated. Treated as `Own` by the typechecker. + #[default] Implicit, /// `(own T)` — caller transfers ownership; callee consumes. Own, @@ -728,12 +729,6 @@ pub enum ParamMode { Borrow, } -impl Default for ParamMode { - fn default() -> Self { - ParamMode::Implicit - } -} - impl ParamMode { /// Used by the `skip_serializing_if` predicate on /// [`Type::Fn::ret_mode`]. diff --git a/crates/ailang-core/src/desugar.rs b/crates/ailang-core/src/desugar.rs index 7dc8d54..c1645ba 100644 --- a/crates/ailang-core/src/desugar.rs +++ b/crates/ailang-core/src/desugar.rs @@ -272,7 +272,7 @@ pub fn desugar_module(m: &Module) -> Module { // produced by the bottom-up walk. Typecheck/codegen are order- // insensitive at the def list level (they index by name), so this // is purely cosmetic. - out.defs.extend(d.lifted.into_iter()); + out.defs.extend(d.lifted); out } diff --git a/crates/ailang-core/src/workspace.rs b/crates/ailang-core/src/workspace.rs index d3b13f5..59b0826 100644 --- a/crates/ailang-core/src/workspace.rs +++ b/crates/ailang-core/src/workspace.rs @@ -1395,7 +1395,9 @@ where /// ct.1: Pattern::Ctor carries a `ctor` name (matches against a /// scrutinee's TypeDef) but NOT a type_name field — the type is /// inferred from the scrutinee. So Pattern walking only recurses; -/// no canonical-form check fires here. +/// no canonical-form check fires here. `f` is kept in the signature +/// to match the sibling `walk_*` framework (uniform plumbing). +#[allow(clippy::only_used_in_recursion)] fn walk_pattern(p: &crate::ast::Pattern, f: &mut F) -> Result<(), WorkspaceLoadError> where F: FnMut(&str) -> Result<(), WorkspaceLoadError>, @@ -1670,13 +1672,13 @@ mod tests { } } - /// Iter 22b.1: a workspace whose own modules contain no - /// `Def::Instance` defs produces no non-prelude registry entries. - /// This is the happy-path baseline for the registry-build pass — - /// every pre-22b workspace falls into this case. With the - /// auto-loaded prelude (iter 23.2) the registry is no longer - /// strictly empty, so the invariant is now "no entries whose - /// `defining_module` is anything other than `prelude`". + // Iter 22b.1: a workspace whose own modules contain no + // `Def::Instance` defs produces no non-prelude registry entries. + // This is the happy-path baseline for the registry-build pass — + // every pre-22b workspace falls into this case. With the + // auto-loaded prelude (iter 23.2) the registry is no longer + // strictly empty, so the invariant is now "no entries whose + // `defining_module` is anything other than `prelude`". // `iter22b1_workspace_with_no_classes_has_empty_registry` relocated // to `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5. @@ -1690,50 +1692,50 @@ mod tests { .join("examples") } - /// Iter 22b.1: 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) - /// the registry also contains the prelude's own entries; the - /// invariant is filtered to fixture-only entries. + // Iter 22b.1: 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) + // the registry also contains the prelude's own entries; the + // invariant is filtered to fixture-only entries. // `iter22b1_instance_in_class_module_loads_clean` relocated to // `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5. - /// Iter 22b.1: an instance declared in a module that is neither - /// the class's module nor the type's module fires `OrphanInstance`. - /// The fixture imports `test_22b1_orphan_third_classmod` (which - /// owns `class TShow`) and the entry module declares `instance - /// TShow Int` itself — but the entry is not the class's module - /// and `Int` is primitive, so neither leg of coherence is - /// satisfied. (24.2: class renamed `Show` → `TShow`.) + // Iter 22b.1: an instance declared in a module that is neither + // the class's module nor the type's module fires `OrphanInstance`. + // The fixture imports `test_22b1_orphan_third_classmod` (which + // owns `class TShow`) and the entry module declares `instance + // TShow Int` itself — but the entry is not the class's module + // and `Int` is primitive, so neither leg of coherence is + // satisfied. (24.2: class renamed `Show` → `TShow`.) // `iter22b1_orphan_instance_fires_diagnostic` relocated to // `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5. - /// Iter 22b.1 / mq.1: two instances of the same `(class, type)` - /// pair collide on the registry's uniqueness check. - /// - /// Pre-mq.1 the test used a two-module fixture - /// (`test_22b1_dup_a` declared the class + first instance, - /// `test_22b1_dup_b` declared the type + second instance) which - /// relied on bare cross-module class refs. The mq.1 - /// canonical-form rule makes that shape structurally - /// unrepresentable (any second instance in a module that owns - /// neither the class nor the type fires `OrphanInstance` first). - /// Post-mq.1 the only way to land two instances on the same - /// canonical key is to have them in the same module (the class's - /// or the type's module). The fixture now declares both - /// instances in `test_22b1_dup_same_module` — both class-leg - /// coherent, both collide on `(test_22b1_dup_same_module.TShow, - /// type_hash(Int))`. (24.2: class renamed `Show` → `TShow`.) + // Iter 22b.1 / mq.1: two instances of the same `(class, type)` + // pair collide on the registry's uniqueness check. + // + // Pre-mq.1 the test used a two-module fixture + // (`test_22b1_dup_a` declared the class + first instance, + // `test_22b1_dup_b` declared the type + second instance) which + // relied on bare cross-module class refs. The mq.1 + // canonical-form rule makes that shape structurally + // unrepresentable (any second instance in a module that owns + // neither the class nor the type fires `OrphanInstance` first). + // Post-mq.1 the only way to land two instances on the same + // canonical key is to have them in the same module (the class's + // or the type's module). The fixture now declares both + // instances in `test_22b1_dup_same_module` — both class-leg + // coherent, both collide on `(test_22b1_dup_same_module.TShow, + // type_hash(Int))`. (24.2: class renamed `Show` → `TShow`.) // `iter22b1_duplicate_instance_fires_diagnostic` relocated to // `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5. - /// Iter 22b.1: an instance that omits a required (non-default) - /// method of its class fires `MissingMethod`. The fixture's - /// `class TEq` declares `teq` and `tne` as both non-default; the - /// instance only specifies `tne`, leaving `teq` missing. - /// (Class is `TEq` (and method `tne`) rather than `Eq` / `ne` to - /// avoid colliding with the auto-loaded prelude's `class Eq` and - /// — since iter 23.5 — the prelude's polymorphic free fn `ne`.) + // Iter 22b.1: an instance that omits a required (non-default) + // method of its class fires `MissingMethod`. The fixture's + // `class TEq` declares `teq` and `tne` as both non-default; the + // instance only specifies `tne`, leaving `teq` missing. + // (Class is `TEq` (and method `tne`) rather than `Eq` / `ne` to + // avoid colliding with the auto-loaded prelude's `class Eq` and + // — since iter 23.5 — the prelude's polymorphic free fn `ne`.) // `iter22b1_missing_method_fires_diagnostic` relocated to // `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5. @@ -1780,16 +1782,16 @@ mod tests { } } - /// Iter 22b.2: an instance that specifies a body for a method - /// name the class never declared must fire - /// `OverridingNonExistentMethod`. Symmetric counterpart to - /// `MissingMethod`: the latter fires when the class declares a - /// non-default method that the instance omits; this one fires - /// when the instance provides a body the class did not ask for. - /// Decision 11 forbids ad-hoc additions to a class's method set - /// at the instance site. - /// (Class is `TEq` rather than `Eq` to avoid colliding with the - /// auto-loaded prelude's `class Eq`.) + // Iter 22b.2: an instance that specifies a body for a method + // name the class never declared must fire + // `OverridingNonExistentMethod`. Symmetric counterpart to + // `MissingMethod`: the latter fires when the class declares a + // non-default method that the instance omits; this one fires + // when the instance provides a body the class did not ask for. + // Decision 11 forbids ad-hoc additions to a class's method set + // at the instance site. + // (Class is `TEq` rather than `Eq` to avoid colliding with the + // auto-loaded prelude's `class Eq`.) // `instance_overriding_nonexistent_method_fires` relocated to // `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5. @@ -1827,15 +1829,15 @@ mod tests { // observations move to `env.method_to_candidate_classes` // (multi-entry set) plus the call-site warning. - /// Iter 22b.2: an instance `C T` whose class `C` declares a - /// superclass `S` requires that `instance S T` also exist in the - /// workspace. The fixture declares `class TEq a`, `class TOrd a - /// extends TEq a`, and `instance TOrd Int` — but no `instance TEq - /// Int` — so registry build must fire - /// `MissingSuperclassInstance`. Decision 11 single-superclass - /// model requires `instance S T` whenever `instance C T` exists. - /// (Classes are `TEq` / `TOrd` rather than `Eq` / `Ord` to avoid - /// colliding with the auto-loaded prelude's `class Eq`.) + // Iter 22b.2: an instance `C T` whose class `C` declares a + // superclass `S` requires that `instance S T` also exist in the + // workspace. The fixture declares `class TEq a`, `class TOrd a + // extends TEq a`, and `instance TOrd Int` — but no `instance TEq + // Int` — so registry build must fire + // `MissingSuperclassInstance`. Decision 11 single-superclass + // model requires `instance S T` whenever `instance C T` exists. + // (Classes are `TEq` / `TOrd` rather than `Eq` / `Ord` to avoid + // colliding with the auto-loaded prelude's `class Eq`.) // `instance_without_superclass_instance_fires` relocated to // `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5. @@ -2494,12 +2496,12 @@ mod tests { } } - /// mq.1: on-disk fixture pin — a workspace where the consumer's - /// `Constraint.class` references a class in an imported module via - /// the qualified form loads cleanly. Symmetric to ct.1's positive - /// cross-module type-ref fixture (`ct1_validator_accepts_qualified_xmod_ref` - /// in-test sibling). Guards the full load → validator → registry - /// path on a real on-disk pair. + // mq.1: on-disk fixture pin — a workspace where the consumer's + // `Constraint.class` references a class in an imported module via + // the qualified form loads cleanly. Symmetric to ct.1's positive + // cross-module type-ref fixture (`ct1_validator_accepts_qualified_xmod_ref` + // in-test sibling). Guards the full load → validator → registry + // path on a real on-disk pair. // `mq1_xmod_constraint_class_fixture_loads` relocated to // `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5. diff --git a/crates/ailang-prose/src/lib.rs b/crates/ailang-prose/src/lib.rs index 47eed72..963dfca 100644 --- a/crates/ailang-prose/src/lib.rs +++ b/crates/ailang-prose/src/lib.rs @@ -380,7 +380,7 @@ fn write_class_method(out: &mut String, m: &ClassMethod, level: usize, owning_mo // as `x`, `x1`, `x2`, … to keep the projection unambiguous // without inventing names that could collide with the body. if i == 0 { - out.push_str("x"); + out.push('x'); } else { out.push_str(&format!("x{i}")); } @@ -2100,7 +2100,6 @@ mod tests { let last = doc_lines.last().unwrap(); let last_word_count = last .trim_start_matches("///") - .trim() .split_whitespace() .count(); assert!( diff --git a/crates/ailang-surface/src/lex.rs b/crates/ailang-surface/src/lex.rs index 1bfd013..3d8f7be 100644 --- a/crates/ailang-surface/src/lex.rs +++ b/crates/ailang-surface/src/lex.rs @@ -237,6 +237,7 @@ pub fn tokenize(input: &str) -> Result, LexError> { /// - `[-]?.` /// - `[-]?.(e|E)[+-]?` /// - `[-]?(e|E)[+-]?` +/// /// and contains at least one of `.`, `e`, `E` (otherwise the caller /// would dispatch to the integer path). Rejects bare leading/trailing /// dots (`5.`, `5.e10`), missing exponent digits (`1.5e`), and any diff --git a/crates/ailang-surface/src/parse.rs b/crates/ailang-surface/src/parse.rs index 1b5753b..131a08e 100644 --- a/crates/ailang-surface/src/parse.rs +++ b/crates/ailang-surface/src/parse.rs @@ -1758,8 +1758,7 @@ mod tests { (body 0))) "#, ) - .err() - .expect("parse should fail"); + .expect_err("parse should fail"); let msg = format!("{err}"); assert!( msg.contains("borrow"), @@ -1806,8 +1805,7 @@ mod tests { (body (clone)))) "#, ) - .err() - .expect("parse should fail"); + .expect_err("parse should fail"); let msg = format!("{err}"); assert!( msg.contains("clone expects exactly one term argument"), @@ -1864,8 +1862,7 @@ mod tests { (body (reuse-as)))) "#, ) - .err() - .expect("parse should fail"); + .expect_err("parse should fail"); let msg = format!("{err}"); assert!( msg.contains("reuse-as expects exactly two term arguments"), @@ -1885,8 +1882,7 @@ mod tests { (body (reuse-as x)))) "#, ) - .err() - .expect("parse should fail"); + .expect_err("parse should fail"); let msg = format!("{err}"); assert!( msg.contains("reuse-as expects exactly two term arguments"), @@ -1986,8 +1982,7 @@ mod tests { (drop-iterative oops))) "#, ) - .err() - .expect("parse should fail"); + .expect_err("parse should fail"); let msg = format!("{err}"); assert!( msg.contains("drop-iterative takes no arguments"), diff --git a/crates/ailang-surface/tests/loader.rs b/crates/ailang-surface/tests/loader.rs index b750c28..28f9026 100644 --- a/crates/ailang-surface/tests/loader.rs +++ b/crates/ailang-surface/tests/loader.rs @@ -105,7 +105,7 @@ fn load_workspace_prefers_ail_when_both_extensions_exist() { let names: Vec<&str> = leaf .defs .iter() - .map(|d| ailang_core::def_name(d)) + .map(ailang_core::def_name) .collect(); assert!( names.contains(&"winner"), diff --git a/docs/journals/2026-05-14-iter-clippy-sweep.md b/docs/journals/2026-05-14-iter-clippy-sweep.md new file mode 100644 index 0000000..71e23bf --- /dev/null +++ b/docs/journals/2026-05-14-iter-clippy-sweep.md @@ -0,0 +1,134 @@ +# iter clippy-sweep — clear all 61 `cargo clippy` warnings + +**Date:** 2026-05-14 +**Started from:** 04258c5 (post-WhatsNew of rustdoc-sweep + drift-test-narrowing) +**Status:** DONE + +## Summary + +Hygiene sweep against `cargo clippy --workspace --all-targets`. Before: +61 warnings across all eight crates. After: zero. Tests stay 563 green; +`cargo doc --workspace --no-deps` stays at zero warnings (the +rustdoc-sweep baseline); all three bench scripts (`bench/check.py`, +`bench/compile_check.py`, `bench/cross_lang.py`) all exit 0 against +the existing baselines — a free Stabilitätsbeweis that the sweep +touched no semantics. + +Twelve warning classes, fixed with three different shapes: + +- **Pure documentation fixes** (~32 warnings). `doc_lazy_continuation` + on multi-line `///` and `//!` comments where clippy parsed the + second line as a list continuation; resolved by either adding a + blank line, escaping the marker, or rephrasing the offending line. + Affected: `lex.rs:235-243` (3-line bullet list), `lib.rs:2360-2368` + in `ailang-check` (3-line bullet list), `lib.rs:3736` in + `ailang-check` (a `> 1` numeric comparison parsed as a quote + marker; rephrased to "any cardinality above one"), `drop.rs:570-585` + (numbered list + paragraph mix), `e2e.rs:915-920` (a `+ three + primitive instances` line where `+` was parsed as bullet marker), + `e2e.rs:1607-1614`, `typeclass_22b3.rs:706-708`, `eq_ord_e2e.rs:13`. + Plus four `empty_line_after_doc_comments` hits in + `workspace.rs:1675-2506` — these were `///` doc-comments left + orphaned by form-a.1 Task 5 test relocation; converted to plain + `//` comments (eight blocks total) since they no longer document + any function. + +- **Idiomatic refactors** (8 warnings). `err_expect` (5 hits in + `parse.rs`): `.err().expect("…")` → `.expect_err("…")`. + `useless_conversion` (3 hits): `.extend(x.into_iter())` → `.extend(x)` + in `desugar.rs:275` + `lift.rs:201`; `vec!["x".into()]` → + `vec!["x"]` in `lib.rs:5522` (where the call-site `fn_def` + parameter is `Vec<&str>` so the conversion was identity). + `redundant_closure` (2 hits): `|f| collect_pattern_binders(f)` → + `collect_pattern_binders` in `linearity.rs:717`; same pattern in + `loader.rs:108`. `trim_split_whitespace`: `.trim().split_whitespace()` + → `.split_whitespace()` in `prose/src/lib.rs:2103` (since + `split_whitespace` already ignores leading/trailing whitespace). + `single_char_add_str`: `out.push_str("x")` → `out.push('x')` in + `prose/src/lib.rs:383`. `collapsible_match` (2 hits): nested + `if let Some(_) = …` collapsed to `if let Some(Some(_)) = …` in + `mono.rs:1028` + `drop.rs:535-536`. + +- **`derivable_impls`** (2 hits). `impl Default for ParamMode { + fn default() -> Self { ParamMode::Implicit } }` in `ast.rs:731` + → `#[derive(Default)]` + `#[default] Implicit`. Same shape for + `AllocStrategy` in `codegen/src/lib.rs:164`. The derive form is + strictly more explicit (the `#[default]` attribute marks which + variant in-source rather than the impl body referencing it by + name) so the change is also a small clarity gain. + +- **`blocks_in_conditions`** (1 hit). The synth `Term::Var` arm in + `check/src/lib.rs` had a 13-line block inside an `else if` + condition (the mq.2 dispatch entry: `else if { let (m, q) = + parse_method_qualifier(name); qualifier_is_class_shape(&q) && + env.method_to_candidate_classes.contains_key(m) }`). Extracted + to a new helper `fn is_class_method_dispatch(name, env) -> bool` + next to `qualifier_is_class_shape`. The helper inherits the + block's full doc-comment so the why-each-branch reasoning is + preserved at the call site. + +- **`only_used_in_recursion`** (1 hit). `walk_pattern(p, f)` in + `workspace.rs:1399` carries an `f: &mut F` parameter that + Pattern walking never invokes (ct.1 left this seam unused: no + Pattern variant carries a cross-module reference). Rather than + delete `f` (which would break the symmetry with the five other + `walk_*` framework functions in this file that all share the + same `(_, f: &mut F) -> Result<…>` signature), added + `#[allow(clippy::only_used_in_recursion)]` with an inline + comment naming the framework-uniformity rationale. + +- **`if_same_then_else`** (2 hits). Both in `qualify_local_types` + shapes (`check/src/lib.rs:3457` + `codegen/src/subst.rs:165`): + the if-chain has two branches returning `name.clone()` for + semantically distinct reasons (`name.contains('.')` = already + qualified; `is_primitive_name(name)` = primitive needs no + qualification). Combining the branches with `||` would obscure + the reasoning. Added `#[allow(clippy::if_same_then_else)]` with + an inline comment naming why each branch disqualifies the name. + +- **`too_many_arguments`** (1 hit). `Emitter::new` takes 8 of the + workspace-flat tables the emitter needs at construction time. + Bundling them into a context struct would just rename the + boilerplate without reducing it. Added + `#[allow(clippy::too_many_arguments)]` with the rationale + inline. + +## Files touched + +- `crates/ailang-core/src/ast.rs` (1 derive flip) +- `crates/ailang-core/src/desugar.rs` (1 .extend) +- `crates/ailang-core/src/workspace.rs` (1 walk_pattern allow + 8 `///`→`//` blocks) +- `crates/ailang-surface/src/lex.rs` (1 doc-list blank line) +- `crates/ailang-surface/src/parse.rs` (5 `.expect_err`) +- `crates/ailang-surface/tests/loader.rs` (1 redundant closure) +- `crates/ailang-prose/src/lib.rs` (push_str + trim removal) +- `crates/ailang-check/src/lib.rs` (helper extraction + 1 allow + 1 doc rephrase + 1 doc-list blank line + 1 vec literal) +- `crates/ailang-check/src/lift.rs` (1 .extend) +- `crates/ailang-check/src/linearity.rs` (1 redundant closure) +- `crates/ailang-codegen/src/lib.rs` (1 derive flip + 1 too_many_args allow) +- `crates/ailang-codegen/src/subst.rs` (1 if_same_then_else allow) +- `crates/ailang-codegen/src/drop.rs` (1 doc-list blank line + 1 collapsible_match) +- `crates/ailang-check/src/mono.rs` (1 collapsible_match) +- `crates/ail/tests/e2e.rs` (2 doc rephrasings) +- `crates/ail/tests/typeclass_22b3.rs` (1 doc rephrasing) +- `crates/ail/tests/eq_ord_e2e.rs` (1 doc rephrasing) + +## Verification + +- `cargo clippy --workspace --all-targets 2>&1 | grep -c '^warning:'` + → `0` (was 61). +- `cargo test --workspace` → 563 passed + 3 ignored, 0 failed + (baseline holds). +- `cargo doc --workspace --no-deps 2>&1 | grep -c '^warning:'` → + `0` (rustdoc-sweep baseline holds — no regression). +- `bench/check.py` → exit 0. +- `bench/compile_check.py` → exit 0. +- `bench/cross_lang.py` → exit 0. + +## Concerns + +- (none) + +## Known debt + +- (none) diff --git a/docs/journals/INDEX.md b/docs/journals/INDEX.md index 4cc2829..1785252 100644 --- a/docs/journals/INDEX.md +++ b/docs/journals/INDEX.md @@ -60,3 +60,4 @@ - 2026-05-13 — iter str-concat: `str_concat : (borrow Str, borrow Str) -> Str` heap-Str concatenation primitive shipped in four-site lockstep, closing fieldtest-form-a friction finding #4. T1 RED-pin `crates/ail/tests/str_concat_e2e.rs` + new corpus fixture `examples/show_user_adt_with_label.ail` (Show user-ADT body using `(app str_concat "Item " (app int_to_str n))` through prelude `print`; ok 23/2). T2 `runtime/str.c` C helper `ailang_str_concat(a, b)` appended after `ailang_str_clone`: reads `len` headers from both source payloads, `str_alloc(la+lb)`, memcpys both bytes, NUL-terminates. T3 `crates/ailang-check/src/builtins.rs` install entry (Type::Fn arity-2 Borrow-Borrow params, Own return, effects empty) + list() row + `install_str_concat_signature` unit test reaching into `env.globals` directly (deeper than synth_in_builtins_env-based siblings because first builtin with arity-2 Borrow params). T4 `crates/ailang-codegen/src/lib.rs` four-site lockstep: extern `declare ptr @ailang_str_concat(ptr, ptr)` after str_clone declare + lower_app arm after str_clone arm (arity-2 check + two `lower_term` + `fresh_ssa` + body push) + `is_builtin_callable` match-list extended with `"str_concat"` + IR-pin unit test `str_concat_emits_call_to_ailang_str_concat` asserting both extern declaration AND call instruction in emitted IR. Five IR snapshots regenerated (`hello.ll`, `list.ll`, `max3.ll`, `sum.ll`, `ws_main.ll`) to absorb the new unconditional declare line in IR header — same upkeep pattern as hs.4 (sole diff verified at IR line 17 across all snapshots). T5 lockstep-collision repair: `examples/bug_unbound_in_instance_method.ail` had used `str_concat` as the literal UNBOUND name (because that was the fieldtester's LLM-natural repro); renamed to `format_label` (LLM-author-realistic helper name that will never become a builtin) and updated `crates/ail/tests/unbound_in_instance_method_pin.rs` (`replace_all` on `str_concat` → `format_label`, including one test name flipped to `check_fires_unbound_var_for_format_label_in_instance_method_body`), preserving the regression guard's intent (instance-method-body walked through unbound-var check) while substituting a name with stable unbound semantics. T6 `docs/DESIGN.md` new §"Heap-Str primitives" subsection (line 1994) between the milestone-24 Show-backer enumeration and the existing `Primitive output goes through ...` paragraph, cataloguing all five heap-Str primitives (`int_to_str`, `bool_to_str`, `float_to_str`, `str_clone`, `str_concat`) with type signatures, iter origins, and the user-visible-vs-prelude-internal distinction; Show-backer block unchanged. T7 `docs/roadmap.md` struck `[x] [feature] str_concat` line at end of P2 cluster. Two minor plan-vs-actual discrepancies recorded in journal Concerns: (a) T3 Step 5 test-count prediction `passed: 560 failed: 1` was actual `passed: 558 failed: 3` because the unbound_in_instance_method_pin tests turn RED at checker registration not at codegen (assertion is on the `[unbound-var]` diagnostic which depends only on the checker registry) — implementation correct, T5 fully repairs; (b) IR snapshot regen for T4 was not explicitly scripted in the plan but follows hs.4 precedent. Final iter state: 562/562 green (559 baseline + 3 new pins), zero re-loops across all 7 tasks. Bench impact none (no new code paths touched by bench corpus; bench fixtures use `int_to_str` / `bool_to_str` only). Closes fieldtest-form-a queue: bug (closed bugfix-instance-body-unbound-var), spec_gaps 2+3 (closed form-a.tidy), friction (closed here). Remaining open: spec_gap 1 dropped per recon (audit-form-a "plan two sites" claim did not match HEAD) → 2026-05-13-iter-str-concat.md - 2026-05-13 — iter rustdoc-sweep: cleared all 23 `cargo doc --workspace --no-deps` warnings (17 in `ailang-check` + 4 in `ailang-core` + 2 in `ailang-surface`). Two warning classes: (a) `pub` doc-comments that linked-via-brackets to `pub(crate)`/private items (15 hits) — replaced ``[`fn`]`` with plain backtick-code-span ``` `fn` ``` so the identifier stays readable but rustdoc no longer tries to resolve the link; (b) unresolved/ambiguous links (8 hits) — `mono.rs` `[`Registry`]`/`[`Registry::entries`]` ×3 fully-qualified to `[`ailang_core::workspace::Registry…`]`; `loader.rs` `[`crate::parse`]` ×2 (ambiguous between fn + mod) disambiguated to `[`crate::parse()`]`; `lib.rs` `metas[i]` (parsed as a link) escaped to `metas\[i\]`. Tests 562 → 562, no behaviour change → 2026-05-13-iter-rustdoc-sweep.md - 2026-05-13 — iter drift-test-narrowing: `crates/ailang-core/tests/design_schema_drift.rs` now scans §"Data model" only, not the whole DESIGN.md. New helper `data_model_section()` slices `DESIGN_MD` from `## Data model` to the next top-level `## ` header; all 7 anchor-presence tests switched from `DESIGN_MD.contains(anchor)` to `data_model_section().contains(anchor)`. Closes the audit-form-a-precursor `[high]` "anchors-elsewhere-pass-silently" failure mode (an anchor present only in §"Decision 11" or another discussion section was previously treated as documented). All 38 anchors verified pre-edit to live in §"Data model" so the tightening doesn't regress to red. New pin `data_model_section_is_bounded` (starts-with `## Data model`, no bleed past `\n## Pipeline`, > 1 KB) guards the extractor itself against silent regression to whole-document scanning. Tests 562 → 563 (+1) → 2026-05-13-iter-drift-test-narrowing.md +- 2026-05-14 — iter clippy-sweep: cleared all 61 `cargo clippy --workspace --all-targets` warnings across 12 lint classes. Bulk: 32× `doc_lazy_continuation` + 4× `empty_line_after_doc_comments` (8 orphan `///` blocks in `workspace.rs` left by form-a.1 T5 relocation converted to plain `//`) — pure documentation hygiene. Idiomatic refactors: 5× `err_expect` (`.err().expect()` → `.expect_err()`), 3× `useless_conversion`, 2× `redundant_closure`, 2× `collapsible_match`, 1× `single_char_add_str`, 1× `trim_split_whitespace`. Two `derivable_impls` (manual `Default` for `ParamMode` + `AllocStrategy` flipped to `#[derive(Default)]` + `#[default]`). One `blocks_in_conditions` extracted to a new helper `is_class_method_dispatch(name, env)` next to `qualifier_is_class_shape` in `check/src/lib.rs`. Three `#[allow]`s with inline rationale: `only_used_in_recursion` on `walk_pattern` (preserves the 5-fn walker-framework signature uniformity), 2× `if_same_then_else` on `qualify_local_types` shapes (`check/src/lib.rs:3457` + `codegen/src/subst.rs:165` — the two `name.clone()` branches encode semantically distinct disqualification reasons), `too_many_arguments` on `Emitter::new` (8 workspace-flat tables; bundling would just rename boilerplate). Tests 563 → 563 (no behavior change); `cargo doc` stays at 0 warnings (rustdoc-sweep baseline holds); all 3 bench scripts exit 0 against existing baselines (free stability check that the sweep touched no semantics) → 2026-05-14-iter-clippy-sweep.md