Skip to content

Commit 323a994

Browse files
timfennisclaude
andauthored
✨ Widen variable types on reassignment via LUB (#121)
## Summary - The analyser now tracks type changes on reassignment by computing the LUB of the old and new types and updating the binding - This fixes false "not declared" errors when destructuring a variable that was initialized as `()` and later reassigned to a tuple (e.g. `let pos = (); pos = (1, 2); let a, b = pos;`) - Documents reassignment type widening in the manual ## Test plan - [x] All 268 tests pass - [x] Regression test `bug0015` verifies the original bug is fixed - [x] `cargo clippy` and `cargo fmt` clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 <[email protected]>
1 parent 4cdadc7 commit 323a994

3 files changed

Lines changed: 35 additions & 2 deletions

File tree

manual/src/reference/variables-and-scopes.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,24 @@ let x = {
4040
print(x); // 3
4141
```
4242

43+
## Reassignment
44+
45+
The `=` operator can be used to reassign a value to an existing variable. When you reassign a variable to a value of a different type, the variable's type is widened to the least upper bound (LUB) of the old and new types.
46+
47+
```ndc
48+
let x = 1; // type is Int
49+
x = 2; // type is still Int
50+
x = 3.14; // type widens to Number (LUB of Int and Float)
51+
```
52+
53+
```ndc
54+
let pos = (); // type is ()
55+
pos = (1, 2); // type widens to Sequence<Any>
56+
pos = ("a", "b"); // type is still Sequence<Any>
57+
```
58+
59+
> **Tip:** For the best type inference, initialize variables with a value that matches the intended type. For example, use `let pos = (0, 0);` instead of `let pos = ();` if you intend to store a 2-tuple of numbers.
60+
4361
## Destructuring
4462

4563
Destructuring is more similar to how it works in python and cares mostly about where commas are and not so much about the delimiters (`[]`, `()`) used.

ndc_analyser/src/analyser.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,22 @@ impl Analyser {
7878
Ok(StaticType::unit())
7979
}
8080
Expression::Assignment { l_value, r_value } => {
81-
self.resolve_lvalue(l_value, *span)?;
82-
self.analyse(r_value)?;
81+
let old_type = self.resolve_lvalue(l_value, *span)?;
82+
let new_type = self.analyse(r_value)?;
83+
84+
// Widen the binding's type to the LUB so subsequent uses
85+
// see the broader type.
86+
if let Lvalue::Identifier {
87+
resolved: Some(target),
88+
..
89+
} = l_value
90+
{
91+
let widened = old_type.lub(&new_type);
92+
if widened != old_type {
93+
self.scope_tree.update_binding_type(*target, widened);
94+
}
95+
}
96+
8397
Ok(StaticType::unit())
8498
}
8599
Expression::OpAssignment {

tests/programs/900_bugs/bug0015_unpack_mismatched_tuple_arity.ndc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Destructuring let where the variable was declared as () but later
22
// reassigned to a 2-tuple should not cause a false "not declared" error.
3+
// The analyser widens the type via LUB on reassignment.
34
// expect-output: 4
45

56
let pos = ();

0 commit comments

Comments
 (0)