From c7db1a62ddb4ad816ecdc6f1cdd3aebd15538ddb Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Sat, 21 Mar 2026 23:05:14 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Widen=20variable=20types=20on=20rea?= =?UTF-8?q?ssignment=20via=20LUB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The analyser now tracks type changes when a variable is reassigned 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. Co-Authored-By: Claude Opus 4.6 --- manual/src/reference/variables-and-scopes.md | 18 ++++++++++++++++++ ndc_analyser/src/analyser.rs | 18 ++++++++++++++++-- .../bug0015_unpack_mismatched_tuple_arity.ndc | 1 + 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/manual/src/reference/variables-and-scopes.md b/manual/src/reference/variables-and-scopes.md index 5b65e535..83662037 100644 --- a/manual/src/reference/variables-and-scopes.md +++ b/manual/src/reference/variables-and-scopes.md @@ -40,6 +40,24 @@ let x = { print(x); // 3 ``` +## Reassignment + +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. + +```ndc +let x = 1; // type is Int +x = 2; // type is still Int +x = 3.14; // type widens to Number (LUB of Int and Float) +``` + +```ndc +let pos = (); // type is () +pos = (1, 2); // type widens to Sequence +pos = ("a", "b"); // type is still Sequence +``` + +> **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. + ## Destructuring 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. diff --git a/ndc_analyser/src/analyser.rs b/ndc_analyser/src/analyser.rs index 87ecf2c1..f6671a34 100644 --- a/ndc_analyser/src/analyser.rs +++ b/ndc_analyser/src/analyser.rs @@ -78,8 +78,22 @@ impl Analyser { Ok(StaticType::unit()) } Expression::Assignment { l_value, r_value } => { - self.resolve_lvalue(l_value, *span)?; - self.analyse(r_value)?; + let old_type = self.resolve_lvalue(l_value, *span)?; + let new_type = self.analyse(r_value)?; + + // Widen the binding's type to the LUB so subsequent uses + // see the broader type. + if let Lvalue::Identifier { + resolved: Some(target), + .. + } = l_value + { + let widened = old_type.lub(&new_type); + if widened != old_type { + self.scope_tree.update_binding_type(*target, widened); + } + } + Ok(StaticType::unit()) } Expression::OpAssignment { diff --git a/tests/programs/900_bugs/bug0015_unpack_mismatched_tuple_arity.ndc b/tests/programs/900_bugs/bug0015_unpack_mismatched_tuple_arity.ndc index ba9120ff..cdf6a84f 100644 --- a/tests/programs/900_bugs/bug0015_unpack_mismatched_tuple_arity.ndc +++ b/tests/programs/900_bugs/bug0015_unpack_mismatched_tuple_arity.ndc @@ -1,5 +1,6 @@ // Destructuring let where the variable was declared as () but later // reassigned to a 2-tuple should not cause a false "not declared" error. +// The analyser widens the type via LUB on reassignment. // expect-output: 4 let pos = ();