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:
- Mutation:
let mut x = 1; x = x + 1;— forcesmuton a conceptually immutable pipeline - Renaming:
let x2 = x + 1;— proliferates meaningless names (x2,x3, ...) - 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 the bind phase. 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:5: error: cannot redeclare 'x' in the same scope (first defined at 2:5)
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:5: error: cannot redeclare 'x' in the same scope (first defined at 2:5)
hint: shadowing is allowed when the new value is derived from the old one (e.g., `let x = x + 1`)
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.
