Wado

WEP: Same-Scope Shadowing with Self-Reference

Decision

Allow let declarations to shadow a variable in the same scope if and only if the initializer expression references the variable being shadowed. The reference must be to the variable itself — not to a same-named parameter, field, or method.

let x = 1;
let x = x + 1;           // OK: RHS references `x`
let x = transform(x);    // OK: RHS references `x` (inside a call)
let x = 2;               // Error: RHS does not reference `x`

This preserves Wado's protection against accidental shadowing while enabling the most common and intentional pattern: transforming a value in place without mut.

Context

Current Behavior

Wado prohibits all same-scope shadowing:

let x = 1;
let x = x + 1;  // Error: cannot redeclare 'x' in the same scope

The workarounds are:

  1. Mutation: let mut x = 1; x = x + 1; — forces mut on a conceptually immutable pipeline
  2. Renaming: let x2 = x + 1; — proliferates meaningless names (x2, x3, ...)
  3. Nested scope: scope: { let x = x + 1; ... } — adds unnecessary indentation

Rust's Unrestricted Shadowing

Rust allows any same-scope shadowing without restriction:

let x = 1;
let x = "hello";  // OK in Rust — completely unrelated rebinding

This is controversial in the Rust community. Clippy provides shadow_unrelated to lint against shadowing where the old value is not used. The consensus is that self-referential shadowing (let x = x + 1) is good practice, while unrelated shadowing (let x = "hello") is a code smell.

Motivation from Real Code

Generated parser code (e.g., package-gale/) frequently transforms values in sequence. Without same-scope shadowing, these pipelines require either mut or invented names:

// Current: forced mut
let mut result = parse(input);
result = result.trim();
result = result.to_lowercase();

// With this WEP:
let result = parse(input);
let result = result.trim();
let result = result.to_lowercase();

The second form is clearer: each let signals a new immutable binding derived from the previous value.

Specification

Rule

A let declaration may shadow a same-scope binding of name N if and only if the initializer expression contains at least one variable reference to N that resolves to that same-scope binding.

What Counts as a Reference

The check is performed during name resolution (resolve.rs), the one pass that knows which bare names in a pattern bind. It began in the bind phase, which reads one module's AST alone and so took a case or a constant pattern's name for a binding. A "variable reference" is an Ident AST node that resolves to the variable being shadowed. The check walks the entire initializer expression, including nested sub-expressions:

Expression References x? Reason
let x = x + 1 Yes Direct reference
let x = transform(x) Yes Passed as argument
let x = if cond { x } else { 0 } Yes In branch
let x = match opt { Some(v) => x, None => 0 } Yes In match arm
let x = [x, 1, 2] Yes In array/tuple literal
let x = \|\| x + 1 Yes Captured in closure
let x = some_fn(\|\| x) Yes Captured and passed
let x = 2 No No reference to x
let x = y + 1 No References y, not x
let x = \|x\| x + 1 No x in body refers to the parameter, not the outer variable
let x = foo.x() No .x() is a method call, not a variable reference
let x = point.x No .x is a field access, not a variable reference

Semantics

The shadowing declaration creates a new binding that replaces the previous one in the current scope. The old binding becomes inaccessible after the declaration. The initializer is evaluated with the old binding still in scope (the new binding does not exist yet during RHS evaluation).

This is identical to how nested-scope shadowing already works:

let x = 1;
let x = x + 1;  // RHS `x` refers to the old binding (value 1), LHS creates new binding (value 2)
// `x` is now 2; the old binding (value 1) is inaccessible

Mutability

The new binding's mutability is independent of the old binding:

let x = 1;
let mut x = x + 1;  // OK: new mutable binding from old immutable one

Error Messages

When same-scope shadowing is rejected (RHS does not reference the variable):

3:9: error: cannot redeclare 'x' in the same scope (first defined at 2:9)
  hint: shadowing is allowed when the new value is derived from the old one (e.g., `let x = x + 1`)

When the user might have intended to reassign instead of redeclare:

3:9: error: cannot redeclare 'x' in the same scope (first defined at 2:9)
  hint: shadowing is allowed when the new value is derived from the old one (e.g., `let x = x + 1`)

The shadowed_name Lint

The rule above governs one shape, the same-scope let. Every other binder may take a name that already reaches a known symbol. That is legal and warns under shadowed_name: a parameter, a closure parameter, a type parameter, a pattern binding, and a local item.

The symbol may be of any kind and may sit in any namespace: a function, a global, a type, a trait, a case, an outer binding. A reader who meets the name has to work out which one it means, and that is the same work whichever namespace answers.

The exemption is the derivation this WEP sanctions, read off the binder's own source rather than off the let keyword. A binder whose source already mentions the name it binds rebuilds that binding instead of hiding it. This holds for let, if let and while let, at any scope:

let x = x + 1;                    // exempt, at the same scope or an inner one
if let Some(x) = x { ... }        // exempt: `x` comes from `x`
while let Some(x) = x { ... }     // exempt
if let Some(x) = y { ... }        // warns: `x` comes from something else

A match arm is not exempt. Its scrutinee stays in scope across every arm, so match x { Some(x) => … } gives the name one meaning in one arm and another next to it. That is the confusion the lint names.

#[allow(shadowed_name)] on the binder waives it, and #![allow(shadowed_name)] waives it for a module. A condition and a match arm take no attribute, so a rename is the only answer to a warning there.

A bare identifier pattern is exempt where the name reaches a case or a global. Such a pattern matches by value rather than binding, and the scrutinee's type decides which it does. The resolution pass has no types, so it leaves the name alone.

Alternatives Considered

Allow All Same-Scope Shadowing (Rust-style)

Simpler rule but loses protection against accidental unrelated shadowing — the exact footgun Rust's shadow_unrelated lint addresses.

Keep Current Behavior (No Same-Scope Shadowing)

Safe but forces mut or naming proliferation for common transformation patterns.