Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7b0bcdf
feat: symbol table
timfennis Sep 9, 2026
8a52d1d
feat: scope stack v2
timfennis Sep 9, 2026
3b5e444
feat: name resolver v1
timfennis Sep 10, 2026
bedd336
refactor(analyser): port name resolver to binding patterns and assign…
timfennis Oct 13, 2026
8912222
feat(analyser): record binding kinds in the symbol table 🏷️
timfennis Oct 13, 2026
9601bca
feat(analyser): resolve identifiers to grouped candidates 🧺
timfennis Oct 13, 2026
3682dff
feat(analyser): declare builtins in a scope outside the program 🏛️
timfennis Oct 13, 2026
8f182f2
feat(analyser): resolve function declarations and parameters 🧮
timfennis Oct 13, 2026
5488109
fix(analyser): match the current unknown identifier message 🔤
timfennis Oct 13, 2026
6a69d63
feat(analyser): reject functions that redefine variables in the same …
timfennis Oct 13, 2026
ecee3ad
feat(analyser): record scope kinds 🗂️
timfennis Oct 13, 2026
0cc5cec
feat(analyser): record closure captures 🪤
timfennis Oct 13, 2026
5df0846
feat(analyser): resolve augmented assignment operators ➕
timfennis Oct 13, 2026
cf57e63
feat(analyser): resolve getters and setters behind member and index a…
timfennis Oct 13, 2026
14ce1aa
feat(analyser): resolve type names in annotations 🏷️
timfennis Oct 13, 2026
0087093
feat(analyser): resolve struct declarations 🧱
timfennis Oct 13, 2026
5f7036b
fix(analyser): resolve the None literal 🫥
timfennis Oct 13, 2026
6174e55
feat(analyser): put the name resolver in front of the analyser 🔗 (#230)
timfennis Oct 13, 2026
98391ca
feat(analyser): record each native's registry slot 🔢 (#231)
timfennis Oct 13, 2026
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions manual/src/reference/types/function.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,18 @@ let foo = fn(a) => "let";
foo("x"); // "let": both fn overloads are shadowed
```

The reverse order is an error within one scope: a scope holds a name either as `let` bindings or as functions. In a nested scope, the `fn` shadows the outer `let` as usual. The same rule applies to a struct's constructor.

```ndc
let foo = fn(a) => "let";
fn foo(a, b) { "fn" } // ERROR: Illegal redefinition of variable foo as a function in the same scope

let bar = 1;
{
fn bar(a) { a } // fine: a different scope
}
```

A non-function `let` binding shadows the name for value access, but function calls still resolve to the underlying function:

```ndc
Expand Down
1 change: 1 addition & 0 deletions ndc_analyser/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ ndc_core.workspace = true
ndc_lexer.workspace = true
ndc_parser.workspace = true
thiserror.workspace = true
ahash.workspace = true
68 changes: 62 additions & 6 deletions ndc_analyser/src/analyser.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::name_resolution::{NameResolutionError, NameResolver};
use crate::scope::{CallKind, ResolvedCall, ScopeTree, TypeBinding, TypeDef};
use itertools::{Itertools, izip};
use ndc_core::static_type::StaticTypeConstructionError;
Expand Down Expand Up @@ -33,12 +34,16 @@ pub struct AnalysisResult {
/// discards every binding and struct declared since.
#[derive(Debug, Clone)]
pub struct Checkpoint {
resolver: NameResolver,
scope_tree: ScopeTree,
struct_count: usize,
}

#[derive(Debug)]
pub struct Analyser {
/// Resolves names before analysis. It runs alongside the scope tree below
/// until the type pass reads names from it instead.
resolver: NameResolver,
struct_registry: Rc<RefCell<StructRegistry>>,
scope_tree: ScopeTree,
/// Stack of explicit `return` types for each enclosing function scope.
Expand All @@ -57,6 +62,7 @@ impl Analyser {
struct_registry: Rc<RefCell<StructRegistry>>,
) -> Self {
Self {
resolver: NameResolver::new(scope_tree.native_names()),
scope_tree,
struct_registry,
return_type_stack: Vec::new(),
Expand All @@ -67,12 +73,14 @@ impl Analyser {

pub fn checkpoint(&self) -> Checkpoint {
Checkpoint {
resolver: self.resolver.clone(),
scope_tree: self.scope_tree.clone(),
struct_count: self.struct_registry.borrow().len(),
}
}

pub fn restore(&mut self, checkpoint: Checkpoint) {
self.resolver = checkpoint.resolver;
self.scope_tree = checkpoint.scope_tree;
self.struct_registry
.borrow_mut()
Expand Down Expand Up @@ -152,9 +160,25 @@ impl Analyser {
!self.errors.is_empty()
}

/// Analyses a top-level statement. Names resolve first; a statement with
/// unresolved names isn't analysed further, which would only repeat its errors.
pub fn analyse(
&mut self,
expr_loc: &mut ExpressionLocation,
) -> Result<StaticType, AnalysisError> {
self.resolver.resolve_statement(expr_loc);
let name_errors = self.resolver.take_errors();
if !name_errors.is_empty() {
self.errors
.extend(name_errors.into_iter().map(AnalysisError::from));
return Ok(StaticType::Any);
}
self.analyse_expr(expr_loc)
}

fn analyse_expr(
&mut self,
expr_loc: &mut ExpressionLocation,
) -> Result<StaticType, AnalysisError> {
let typ = self.analyse_inner(expr_loc)?;
self.result.expr_types.insert(expr_loc.id, typ.clone());
Expand All @@ -164,7 +188,7 @@ impl Analyser {
/// Like [`analyse`], but on error emits the error and returns `Any`
/// so that analysis can continue.
fn analyse_or_any(&mut self, expr_loc: &mut ExpressionLocation) -> StaticType {
match self.analyse(expr_loc) {
match self.analyse_expr(expr_loc) {
Ok(t) => t,
Err(e) => {
self.emit(e);
Expand Down Expand Up @@ -298,7 +322,7 @@ impl Analyser {
self.analyse_or_any(right);
Ok(StaticType::Bool)
}
Expression::Grouping(expr) => self.analyse(expr),
Expression::Grouping(expr) => self.analyse_expr(expr),
Expression::Cast {
value,
annotation,
Expand Down Expand Up @@ -1273,6 +1297,16 @@ pub struct AnalysisError {
help_text: Option<String>,
}

impl From<NameResolutionError> for AnalysisError {
fn from(error: NameResolutionError) -> Self {
Self {
text: error.to_string(),
span: error.span(),
help_text: None,
}
}
}

impl AnalysisError {
pub fn span(&self) -> Span {
self.span
Expand Down Expand Up @@ -1476,6 +1510,19 @@ mod tests {
analyse_last_type_with_globals(source, vec![])
}

/// `[]` and `[]=`, which index reads and writes go through. Name resolution
/// rejects index access without them, as it would any other unknown function.
fn index_natives() -> Vec<(String, StaticType)> {
let any_function = StaticType::Function {
parameters: None,
return_type: Box::new(StaticType::Any),
};
vec![
("[]".to_string(), any_function.clone()),
("[]=".to_string(), any_function),
]
}

fn assert_analysis_error(source: &str, globals: Vec<(String, StaticType)>, expected: &str) {
let (_, result) = analyse_with_globals(source, globals);
assert!(
Expand All @@ -1491,23 +1538,32 @@ mod tests {
#[test]
fn inferred_list_index_assignment_widens_element_type() {
assert_eq!(
analyse_last_type("let values = [1]; values[0] = \"two\"; values"),
analyse_last_type_with_globals(
"let values = [1]; values[0] = \"two\"; values",
index_natives()
),
StaticType::List(Box::new(StaticType::Any)),
);
}

#[test]
fn inferred_list_slice_assignment_widens_element_type() {
assert_eq!(
analyse_last_type("let values = [1]; values[0..1] = [\"two\"]; values"),
analyse_last_type_with_globals(
"let values = [1]; values[0..1] = [\"two\"]; values",
index_natives()
),
StaticType::List(Box::new(StaticType::Any)),
);
}

#[test]
fn inferred_map_index_assignment_widens_value_and_preserves_key_type() {
assert_eq!(
analyse_last_type("let values = %{\"one\": 1}; values[\"two\"] = \"two\"; values"),
analyse_last_type_with_globals(
"let values = %{\"one\": 1}; values[\"two\"] = \"two\"; values",
index_natives()
),
StaticType::Map {
key: Box::new(StaticType::String),
value: Box::new(StaticType::Any),
Expand All @@ -1524,7 +1580,7 @@ mod tests {
assert_eq!(
analyse_last_type_with_globals(
"let values = [1]; values[0] += 0.5; values",
vec![("+".to_string(), add)],
[index_natives(), vec![("+".to_string(), add)]].concat(),
),
StaticType::List(Box::new(StaticType::Any)),
);
Expand Down
2 changes: 2 additions & 0 deletions ndc_analyser/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
mod analyser;
mod name_resolution;
mod scope;
mod symbols;

pub use analyser::{Analyser, AnalysisError, AnalysisResult};
pub use scope::ScopeTree;
Loading
Loading