diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index dcdefefc3b..ab34c2473b 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -21944,6 +21944,25 @@ fn parse_int_value(s: &str) -> Option { /// The argument must be a bare identifier naming a slice parameter of the /// caller. Anything else -- a literal, an index, a call -- is not a parameter /// being threaded through and is left alone rather than guessed at. +/// Every struct field name this file declares, at any depth. +/// +/// `bool_fields` is filled DURING emission, from inside `gen_struct`, so it is +/// only complete once the structs have been written. A guard consulted from an +/// expression must not depend on emission order, so this is a pre-pass like its +/// neighbours. +fn collect_field_names(node: &Node, out: &mut std::collections::HashSet) { + if node.kind == NodeKind::StructDecl { + for f in &node.children { + if f.kind == NodeKind::ExprIdentifier && !f.name.is_empty() { + out.insert(f.name.clone()); + } + } + } + for c in &node.children { + collect_field_names(c, out); + } +} + /// Each function's parameter names, in order. A call site knows argument /// POSITIONS; `written_slice_params` is keyed by parameter NAME, and this is /// the map between them. @@ -23800,6 +23819,10 @@ pub struct RustCodegen { /// being threaded into a callee that writes them. Emitted as `&mut [T]`. written_slice_params: std::collections::HashMap>, + /// Every struct field name this file declares. Guards the `.len` lowering: + /// 6 corpus specs give a struct a field actually named `len`, and rewriting + /// that field access into a method call would be wrong there. + field_names: std::collections::HashSet, /// Parameter names by position, per function. Pairs with the map above so a /// call site can ask "is argument 2 a `&mut [T]` slot?". param_names: std::collections::HashMap>, @@ -23877,6 +23900,7 @@ impl RustCodegen { in_const_init: false, written_slice_params: std::collections::HashMap::new(), param_names: std::collections::HashMap::new(), + field_names: std::collections::HashSet::new(), current_mut_slice_params: std::collections::HashSet::new(), bool_vars: std::collections::HashSet::new(), bool_module_vars: std::collections::HashSet::new(), @@ -24001,6 +24025,8 @@ impl RustCodegen { self.written_slice_params = collect_written_slice_params(ast); self.param_names.clear(); collect_param_names(ast, &mut self.param_names); + self.field_names.clear(); + collect_field_names(ast, &mut self.field_names); // Same pre-pass, for the integer widths used by `infer_int_type`: // callee return types and module-level constants are both visible from @@ -25567,6 +25593,24 @@ impl RustCodegen { // reborrowing and must not get a second `&mut`; that is what // `current_mut_slice_params` is for. It is also why this cannot // be done by looking at the argument text alone. + // The FREE-call spelling of the same thing. Zig exposes a + // slice length as a field, so specs write both `x.len` and + // `len(x)`; the second reached rustc as a call to a function + // that does not exist -- 38 of the corpus's E0425 diagnostics. + // Two guards, and the code checks both: `declared_fns` because + // 3 specs declare their own `fn len`, and `field_names` because + // 6 declare a struct field named `len`. The second is stricter + // than this form strictly needs -- a free `len(x)` is a length + // call even in a file that also has a `len` field -- and it is + // kept deliberately so both spellings answer to one condition + // rather than drifting apart. + if node.name == "len" + && args.len() == 1 + && !self.declared_fns.contains("len") + && !self.field_names.contains("len") + { + return format!("({}).len()", args[0]); + } if let Some(callee_params) = self.param_names.get(&node.name) { if let Some(callee_written) = self.written_slice_params.get(&node.name) { for (i, a) in args.iter_mut().enumerate() { @@ -25755,6 +25799,19 @@ impl RustCodegen { // an `if x != null`. if node.name == "?" { format!("{}.unwrap()", base) + } else if node.name == "len" && !self.field_names.contains("len") { + // Zig exposes a slice length as the FIELD `.len` and + // specs are written in that shape, so `data.len` + // reached rustc verbatim: "attempted to take value + // of method `len`" (E0615, 26 diagnostics). Rust + // spells it as a method. The Zig backend documents + // the mirror image of this at its own `.len` site. + // + // Guarded by the whole-file field census: 6 corpus + // specs declare a struct field genuinely named + // `len`, and in those files the access is a field + // and must stay one. + format!("{}.len()", base) } else { format!("{}.{}", base, rust_ident(&node.name)) } diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index 989743a854..1baa82a730 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -ff93a9bf0dc2ba784e27d8154ebc90666dac834653236caa5e39fa159221811c bootstrap/src/compiler.rs +5857419820d4ed4ee5dcaa44df8662d6bfaa8a7a3c4129e5f82df3e4a9389091 bootstrap/src/compiler.rs diff --git a/bootstrap/tests/backend_behaviour.rs b/bootstrap/tests/backend_behaviour.rs index 39182134d8..3fe8e27e47 100644 --- a/bootstrap/tests/backend_behaviour.rs +++ b/bootstrap/tests/backend_behaviour.rs @@ -500,3 +500,54 @@ fn a_local_array_out_parameter_round_trips_at_runtime() { }; assert_eq!(out, "3", "the local array must actually receive the write"); } + +/// Zig exposes a slice length as the FIELD `.len`, so specs are written that +/// way and both spellings reached rustc unlowered: `data.len` as E0615 +/// ("attempted to take value of method"), `len(data)` as E0425. +const SPEC_LEN_BOTH_SPELLINGS: &str = r#" +module lens { + fn field_form(a: []i32) -> usize { + return a.len; + } + fn call_form(a: []i32) -> usize { + return len(a); + } +} +"#; + +/// The guard. A struct may have a field genuinely named `len` -- 6 corpus specs +/// do -- and there the access is a field and must stay one. +const SPEC_LEN_IS_A_REAL_FIELD: &str = r#" +module owns_len { + struct Buf { + len: u32, + cap: u32, + } + fn size_of(b: Buf) -> u32 { + return b.len; + } +} +"#; + +#[test] +fn both_spellings_of_length_become_a_method_call() { + let Some(text) = rust_text(SPEC_LEN_BOTH_SPELLINGS, "rust-len-both") else { + return; + }; + assert!(text.contains("a.len()"), "field form, got:\n{text}"); + assert!(text.contains("(a).len()"), "free-call form, got:\n{text}"); +} + +#[test] +fn a_struct_field_named_len_stays_a_field() { + // Rewriting this to `b.len()` is a wrong translation: `Buf` has no such + // method, and in a struct that did have one it would read the wrong thing. + let Some(out) = rust_says( + SPEC_LEN_IS_A_REAL_FIELD, + "fn main(){ println!(\"{}\", size_of(Buf{ len: 7, cap: 9 })); }\n", + "rust-len-real-field", + ) else { + return; + }; + assert_eq!(out, "7", "the declared field must win over the method"); +} diff --git a/docs/now/2026-09-08-a-length-that-resolved-to-nothing.md b/docs/now/2026-09-08-a-length-that-resolved-to-nothing.md new file mode 100644 index 0000000000..d9b506e843 --- /dev/null +++ b/docs/now/2026-09-08-a-length-that-resolved-to-nothing.md @@ -0,0 +1,11 @@ +# NOW -- A length that resolved to nothing (2026-09-08) + +## A length that resolved to nothing (Closes #3406) + +- Zig exposes a slice length as the FIELD `.len`, so specs write it that way and both spellings reached rustc unlowered: `data.len` as **26** E0615 ("attempted to take value of method"), `len(data)` as **38** of the corpus's E0425. Both now lower to Rust's method call. +- Two guards, each with a measured population. `declared_fns`: 3 specs declare their own `fn len`. `field_names`: 6 declare a struct field genuinely named `len`, and there the access is a field. The existing `bool_fields` could NOT serve as the second guard -- it is filled DURING emission from inside `gen_struct`, so a guard consulted from an expression would depend on emission order. `collect_field_names` is a pre-pass like its neighbours. +- **First fix in this series with a positive acceptance delta.** rustc accepts **430 -> 433 of 651**, and the three are real: `isa/ternary_pattern_matching` (5 functions), `isa/ternary_search` (6), `isa/ternary_sorting` (6), each going from N errors to zero. Coded diagnostics **3096 -> 2962**. E0615 **26 -> 0**, E0425 **770 -> 732**. +- The cost, stated: **9 new E0308**. `path.len() > MAX_PATH_LENGTH` is `usize` against a `u32` constant; `analysis.total_tasks = tasks.len()` assigns `usize` to a `u32` field. These are true statements about the specs, previously hidden behind a name that did not resolve at all. +- **`pow` was declined, not missed.** It is the second-largest unresolved name at 48 diagnostics, but 10 of the 29 specs calling it declare their own `fn pow`, and Rust spells it three ways by type (`powf`, `powi`, `pow`). The corpus writes both `pow(3, k)` and `pow(E, (k as f64))`, so a choice here would be a wrong translation that COMPILES -- worse than the unresolved name. Filed in #3406. +- Caught while measuring: I compared 3745 against 2994 and read a drop of 751. The first counts `^error` LINES (including "aborting due to N previous errors"), the second counts `error[E....]` CODES. Same directory, same binary, two different questions. The honest figure is 3096 -> 2962. +- Mutation-checked twice, one test each: removing the field guard fails only `a_struct_field_named_len_stays_a_field`; disabling the free-call form fails only `both_spellings_of_length_become_a_method_call`.