Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
483 changes: 391 additions & 92 deletions ndc_analyser/src/analyser.rs

Large diffs are not rendered by default.

37 changes: 22 additions & 15 deletions ndc_analyser/src/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ pub(crate) struct Scope {
creates_environment: bool, // Only true for function scopes and for-loop iterations
base_offset: usize,
function_scope_idx: usize,
/// Source-local high-water mark for this function frame. Only meaningful
/// on the function scope itself; child scopes update their owning frame.
local_count: usize,
identifiers: Vec<ScopeBinding>,
upvalues: Vec<(String, CaptureSource)>,
}
Expand All @@ -145,6 +148,7 @@ impl Scope {
creates_environment: true,
base_offset: 0,
function_scope_idx,
local_count: 0,
identifiers: Vec::default(),
upvalues: Vec::default(),
}
Expand All @@ -160,6 +164,7 @@ impl Scope {
creates_environment: false,
base_offset,
function_scope_idx,
local_count: 0,
identifiers: Vec::default(),
upvalues: Vec::default(),
}
Expand Down Expand Up @@ -479,6 +484,18 @@ impl ScopeTree {
.collect()
}

/// Highest source-local slot used by the current function frame, plus one.
/// Child scopes update this value as bindings are allocated, so destroyed
/// block and iteration scopes remain represented in the high-water mark.
pub(crate) fn current_function_local_count(&self) -> usize {
let function_scope_idx = self.scopes[self.current_scope_idx].function_scope_idx;
self.scopes[function_scope_idx].local_count
}

pub(crate) fn current_function_is_top_level(&self) -> bool {
self.scopes[self.current_scope_idx].function_scope_idx == 0
}

// When the Analyser encounters an identifier as the rhs of an expression during resolution it
// will use this method to lookup if that identifier has already been seen.
pub(crate) fn get_binding_any(&mut self, ident: &str) -> Option<ResolvedVar> {
Expand Down Expand Up @@ -857,9 +874,11 @@ impl ScopeTree {
ident: String,
binding: TypeBinding,
) -> ResolvedVar {
ResolvedVar::Local {
slot: self.scopes[self.current_scope_idx].allocate(ident, binding),
}
let function_scope_idx = self.scopes[self.current_scope_idx].function_scope_idx;
let slot = self.scopes[self.current_scope_idx].allocate(ident, binding);
self.scopes[function_scope_idx].local_count =
self.scopes[function_scope_idx].local_count.max(slot + 1);
ResolvedVar::Local { slot }
}

/// Check whether the current scope already has a `fn` declaration with
Expand All @@ -868,18 +887,6 @@ impl ScopeTree {
self.scopes[self.current_scope_idx].has_function_with_arity(name, arity)
}

/// Reserve a slot in the current scope without creating a named binding.
/// Used to allocate the list/map accumulator before analysing the body of a
/// for-comprehension, so that any nested comprehensions receive strictly
/// higher slot numbers and cannot collide with this accumulator.
///
/// Uses `"\x00"` as a sentinel name that can never collide with user identifiers
/// since the lexer never produces null bytes.
pub(crate) fn reserve_anonymous_slot(&mut self) -> usize {
self.scopes[self.current_scope_idx]
.allocate("\x00".to_string(), TypeBinding::Inferred(StaticType::Any))
}

/// Try to update a binding's type. Returns `Err` with the annotated type
/// if the binding has an explicit type annotation and cannot be widened.
pub(crate) fn update_binding_type(
Expand Down
4 changes: 4 additions & 0 deletions ndc_core/src/static_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,10 @@ impl StaticType {
Self::MinHeap(_) => Self::MinHeap(Box::new(new_elem)),
Self::MaxHeap(_) => Self::MaxHeap(Box::new(new_elem)),
Self::Deque(_) => Self::Deque(Box::new(new_elem)),
Self::Map { key, .. } => Self::Map {
key: key.clone(),
value: Box::new(new_elem),
},
_ => self.clone(),
}
}
Expand Down
32 changes: 21 additions & 11 deletions ndc_interpreter/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use ndc_analyser::{Analyser, ScopeTree};
use ndc_core::FunctionRegistry;
use ndc_lexer::{Lexer, SourceDb, SourceId, TokenLocation};
use ndc_parser::ExpressionLocation;
use ndc_parser::{ExpressionLocation, SourceLocalCounts};
use ndc_vm::compiler::Compiler;
use ndc_vm::value::CompiledFunction;
use ndc_vm::{OutputSink, Vm};
Expand Down Expand Up @@ -121,8 +121,11 @@ impl Interpreter {

pub fn compile_str(&mut self, input: &str) -> Result<CompiledFunction, InterpreterError> {
let source_id = self.source_db.add("<input>", input);
let (expressions, _) = self.parse_and_analyse(input, source_id)?;
Ok(Compiler::compile(expressions.into_iter())?)
let (expressions, analysis, _) = self.parse_and_analyse(input, source_id)?;
Ok(Compiler::compile(
expressions.into_iter(),
analysis.source_local_counts,
)?)
}

/// Like [`Self::compile_str`] but skips the peephole optimizer.
Expand All @@ -132,8 +135,11 @@ impl Interpreter {
input: &str,
) -> Result<CompiledFunction, InterpreterError> {
let source_id = self.source_db.add("<input>", input);
let (expressions, _) = self.parse_and_analyse(input, source_id)?;
Ok(Compiler::compile_unoptimized(expressions.into_iter())?)
let (expressions, analysis, _) = self.parse_and_analyse(input, source_id)?;
Ok(Compiler::compile_unoptimized(
expressions.into_iter(),
analysis.source_local_counts,
)?)
}

pub fn disassemble_str(&mut self, input: &str) -> Result<String, InterpreterError> {
Expand Down Expand Up @@ -171,8 +177,9 @@ impl Interpreter {
// fails — otherwise the analyser would remember declarations from a line
// that never actually ran.
let analyser_checkpoint = self.analyser.checkpoint();
let (expressions, mut timings) = self.parse_and_analyse(input, source_id)?;
let vm_result = self.interpret_vm(input, expressions.into_iter());
let (expressions, analysis, mut timings) = self.parse_and_analyse(input, source_id)?;
let vm_result =
self.interpret_vm(input, expressions.into_iter(), analysis.source_local_counts);
if vm_result.is_err() {
self.analyser.restore(analyser_checkpoint);
}
Expand All @@ -186,7 +193,7 @@ impl Interpreter {
&mut self,
input: &str,
source_id: SourceId,
) -> Result<(Vec<ExpressionLocation>, ExecutionTimings), InterpreterError> {
) -> Result<(Vec<ExpressionLocation>, AnalysisResult, ExecutionTimings), InterpreterError> {
let mut timings = ExecutionTimings::default();

let tokens = measure(&mut timings, Phase::Lexing, || {
Expand All @@ -203,6 +210,7 @@ impl Interpreter {
// Hard errors (structural issues) still abort immediately.
if let Err(e) = self.analyser.analyse(e) {
self.analyser.restore(checkpoint.clone());
self.analyser.take_result();
return Err(InterpreterError::Resolver { causes: vec![e] });
}
}
Expand All @@ -218,14 +226,16 @@ impl Interpreter {
});
}

Ok((expressions, timings))
let analysis = self.analyser.take_result();
Ok((expressions, analysis, timings))
}

fn interpret_vm(
&mut self,
#[cfg(feature = "trace")] input: &str,
#[cfg(not(feature = "trace"))] _input: &str,
expressions: impl Iterator<Item = ExpressionLocation>,
source_local_counts: SourceLocalCounts,
) -> Result<(Value, ExecutionTimings), InterpreterError> {
use ndc_vm::{Function as VmFunction, Object as VmObject, Value as VmValue};

Expand All @@ -248,7 +258,7 @@ impl Interpreter {
OutputSink::Stdout
};
let (code, checkpoint) = measure(&mut timings, Phase::Compiling, || {
Compiler::compile_resumable(expressions)
Compiler::compile_resumable(expressions, source_local_counts)
})?;
let mut vm = Vm::new(code, globals).with_output(output);
#[cfg(feature = "trace")]
Expand All @@ -271,7 +281,7 @@ impl Interpreter {
// find their locals on the stack.
let old_checkpoint = checkpoint.clone();
let (code, new_checkpoint) = measure(&mut timings, Phase::Compiling, || {
checkpoint.resume(expressions)
checkpoint.resume(expressions, source_local_counts)
})?;
vm.resume_from_halt(code, globals, resume_ip, prev_num_locals);
#[cfg(feature = "trace")]
Expand Down
34 changes: 30 additions & 4 deletions ndc_parser/src/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use ndc_core::{StaticType, TypeSignature};
use ndc_lexer::Span;
use num::BigInt;
use num::complex::Complex64;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU32, Ordering};

/// Unique identity for an AST node. Used as a key in side tables (e.g. the
Expand All @@ -19,13 +20,41 @@ impl NodeId {
}
}

/// Analyser-assigned source-local high-water marks for each compilation frame.
///
/// The top-level count remains monotonic across REPL analysis batches. Nested
/// functions use independent slot namespaces and are keyed by their declaration
/// node so the compiler can reserve their source slots before hidden temporaries.
///
/// This is transitional metadata while the compiler consumes the resolved AST
/// directly. When HIR is introduced, lowering should represent source locals
/// and generated temporaries as logical local IDs. A frame-layout pass can then
/// assign slots and store `num_locals` directly on each HIR module or function,
/// replacing this top-level count and `NodeId` side table.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SourceLocalCounts {
pub top_level: usize,
pub functions: HashMap<NodeId, usize>,
}

#[derive(Debug, Eq, PartialEq, Clone)]
pub enum Binding {
None,
Resolved(Candidate),
Dynamic(Vec<Candidate>), // figure it out at runtime
}

/// The operation selected by the analyser for an augmented assignment.
///
/// Every selected operation returns the updated left value internally. The
/// compiler writes that value back through the assignment target, while the
/// augmented-assignment expression itself evaluates to unit.
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum AugmentedAssignmentPlan {
Unresolved,
Resolved(Binding),
}
Comment on lines +53 to +56

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this just Option<Binding> with extra steps? There is precedent for using Option<Binding>? correct?


#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum ResolvedVar {
Local { slot: usize },
Expand Down Expand Up @@ -110,8 +139,7 @@ pub enum Expression {
l_value: Lvalue,
r_value: Box<ExpressionLocation>,
operation: String,
resolved_assign_operation: Binding,
resolved_operation: Binding,
plan: AugmentedAssignmentPlan,
},
FunctionDeclaration {
name: Option<String>,
Expand Down Expand Up @@ -192,13 +220,11 @@ pub enum ForBody {
Block(ExpressionLocation),
List {
expr: ExpressionLocation,
accumulator_slot: Option<usize>,
},
Map {
key: ExpressionLocation,
value: Option<ExpressionLocation>,
default: Option<Box<ExpressionLocation>>,
accumulator_slot: Option<usize>,
},
}

Expand Down
4 changes: 2 additions & 2 deletions ndc_parser/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ mod operator;
mod parser;

pub use expression::{
Binding, Candidate, CaptureSource, Expression, ExpressionLocation, ForBody, ForIteration,
FunctionParameter, Lvalue, NodeId, ResolvedVar,
AugmentedAssignmentPlan, Binding, Candidate, CaptureSource, Expression, ExpressionLocation,
ForBody, ForIteration, FunctionParameter, Lvalue, NodeId, ResolvedVar, SourceLocalCounts,
};
pub use operator::{BinaryOperator, LogicalOperator, UnaryOperator};
pub use parser::Error;
Expand Down
8 changes: 3 additions & 5 deletions ndc_parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use std::fmt::Write;

use crate::expression::Expression;
use crate::expression::{
Binding, ExpressionLocation, ForBody, ForIteration, FunctionParameter, Lvalue, NodeId,
AugmentedAssignmentPlan, Binding, ExpressionLocation, ForBody, ForIteration, FunctionParameter,
Lvalue, NodeId,
};
use crate::operator::{BinaryOperator, LogicalOperator, UnaryOperator};
use ndc_core::{Parameter, StaticType, TypeSignature};
Expand Down Expand Up @@ -376,8 +377,7 @@ impl Parser {
.expect("guaranteed to produce an lvalue"),
r_value: Box::new(expression),
operation: operation_identifier,
resolved_assign_operation: Binding::None,
resolved_operation: Binding::None,
plan: AugmentedAssignmentPlan::Unresolved,
};

Ok(op_assign.to_location(start.merge(end)))
Expand Down Expand Up @@ -850,7 +850,6 @@ impl Parser {
Some(Token::For) => {
let result = ForBody::List {
expr: expr.simplify(),
accumulator_slot: None,
};
self.for_comprehension(left_square_bracket_span, result, &Token::RightSquareBracket)
}
Expand Down Expand Up @@ -1321,7 +1320,6 @@ impl Parser {
key: key_expr,
value: value_expr,
default,
accumulator_slot: None,
},
&Token::RightCurlyBracket,
);
Expand Down
2 changes: 1 addition & 1 deletion ndc_stdlib/src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ pub mod ops {
StaticType::List(Box::new(StaticType::Any)),
StaticType::List(Box::new(StaticType::Any)),
]),
return_type: Box::new(StaticType::Tuple(vec![])),
return_type: Box::new(StaticType::List(Box::new(StaticType::Any))),
},
func: NativeFunc::Simple(Box::new(|args| {
let [left, right] = args else {
Expand Down
2 changes: 1 addition & 1 deletion ndc_stdlib/src/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ mod inner {
}

/// Appends the right string to the left string in place.
#[function(name = "++=")]
#[function(name = "++=", return_type = String)]
pub fn op_list_concat(left: &mut StringRepr, right: &mut StringRepr) -> Value {
if Rc::ptr_eq(left, right) {
let new = right.borrow().repeat(2).clone();
Expand Down
Loading
Loading