Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions bootstrap/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21944,6 +21944,25 @@ fn parse_int_value(s: &str) -> Option<i64> {
/// 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<String>) {
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.
Expand Down Expand Up @@ -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<String, std::collections::HashSet<String>>,
/// 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<String>,
/// 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<String, Vec<String>>,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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))
}
Expand Down
2 changes: 1 addition & 1 deletion bootstrap/stage0/FROZEN_HASH
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ff93a9bf0dc2ba784e27d8154ebc90666dac834653236caa5e39fa159221811c bootstrap/src/compiler.rs
5857419820d4ed4ee5dcaa44df8662d6bfaa8a7a3c4129e5f82df3e4a9389091 bootstrap/src/compiler.rs
51 changes: 51 additions & 0 deletions bootstrap/tests/backend_behaviour.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
11 changes: 11 additions & 0 deletions docs/now/2026-09-08-a-length-that-resolved-to-nothing.md
Original file line number Diff line number Diff line change
@@ -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`.
Loading