Add Program node kind

The `Program` node kind is introduced to represent top-level code
blocks, distinguishing them from `Block` nodes which introduce new
scopes. This commit updates various compiler passes to handle the
`Program` node, ensuring correct AST traversal and processing.

The `Program` node is similar to `Block`, but its definitions propagate
to the enclosing scope, unlike `Block` which creates a new, isolated
scope. This distinction is important for how variables and functions are
resolved within the compiled code.
This commit is contained in:
2026-03-31 13:53:51 +02:00
parent 35f5ea0db3
commit 02ea2f0d80
15 changed files with 337 additions and 209 deletions
+12 -2
View File
@@ -53,7 +53,7 @@ impl<'a> Analyzer<'a> {
}
self.collect_globals(value);
}
NodeKind::Block { exprs } => {
NodeKind::Block { exprs } | NodeKind::Program { exprs } => {
for e in exprs {
self.collect_globals(e);
}
@@ -241,6 +241,16 @@ impl<'a> Analyzer<'a> {
}
(NodeKind::Block { exprs: new_exprs }, p)
}
NodeKind::Program { exprs } => {
let mut new_exprs = Vec::with_capacity(exprs.len());
let mut p = Purity::Pure;
for e in exprs {
let em = self.visit(e.clone());
p = p.min(em.ty.purity);
new_exprs.push(Rc::new(em));
}
(NodeKind::Program { exprs: new_exprs }, p)
}
NodeKind::Tuple { elements } => {
let mut new_elements = Vec::with_capacity(elements.len());
@@ -350,7 +360,7 @@ impl NodeExt for NodeKind<TypedPhase> {
f(callee);
f(args);
}
NodeKind::Block { exprs } => {
NodeKind::Block { exprs } | NodeKind::Program { exprs } => {
for e in exprs {
f(e);
}
+98 -47
View File
@@ -1,10 +1,11 @@
use crate::ast::nodes::{
Address, AssignBinding, BoundPhase, DeclarationKind, DefBinding,
IdentifierBinding, LambdaBinding, Node, NodeKind, SyntaxKind, SyntaxNode, Symbol,
UpvalueIdx, VirtualId,
};
use crate::ast::diagnostics::Diagnostics;
use crate::ast::types::{Identity, NodeIdentity, Purity, RecordLayout, SourceLocation, StaticType, Value};
use crate::ast::nodes::{
Address, AssignBinding, BoundPhase, DeclarationKind, DefBinding, GlobalIdx, IdentifierBinding,
LambdaBinding, Node, NodeKind, Symbol, SyntaxKind, SyntaxNode, UpvalueIdx, VirtualId,
};
use crate::ast::types::{
Identity, NodeIdentity, Purity, RecordLayout, SourceLocation, StaticType, Value,
};
use std::collections::HashMap;
use std::rc::Rc;
@@ -43,7 +44,11 @@ struct FunctionCompiler {
}
impl FunctionCompiler {
fn new(identity: Identity, initial_scopes: Vec<CompilerScope>, initial_slot_count: u32) -> Self {
fn new(
identity: Identity,
initial_scopes: Vec<CompilerScope>,
initial_slot_count: u32,
) -> Self {
Self {
identity,
scopes: if initial_scopes.is_empty() {
@@ -66,7 +71,12 @@ impl FunctionCompiler {
}
}
fn define_variable(&mut self, name: &Symbol, identity: Identity) -> Result<Address<VirtualId>, String> {
fn define_variable(
&mut self,
name: &Symbol,
identity: Identity,
as_global: bool,
) -> Result<Address<VirtualId>, String> {
let current_scope = self.scopes.last_mut().unwrap();
if current_scope.locals.contains_key(name) {
return Err(format!(
@@ -76,17 +86,22 @@ impl FunctionCompiler {
}
let slot = VirtualId(self.slot_count);
let addr = if as_global {
Address::Global(GlobalIdx(slot.0))
} else {
Address::Local(slot)
};
current_scope.locals.insert(
name.clone(),
LocalInfo {
addr: Address::Local(slot),
addr,
identity,
_ty: StaticType::Any,
purity: Purity::Impure,
},
);
self.slot_count += 1;
Ok(Address::Local(slot))
Ok(addr)
}
fn resolve_local(&self, sym: &Symbol) -> Option<(LocalInfo, usize)> {
@@ -127,19 +142,13 @@ pub struct Binder {
}
impl Binder {
pub fn new(
initial_scopes: Vec<CompilerScope>,
initial_slot_count: u32,
) -> Self {
pub fn new(initial_scopes: Vec<CompilerScope>, initial_slot_count: u32) -> Self {
let mut binder = Self {
functions: Vec::new(),
capture_map: HashMap::new(),
};
binder.functions.push(FunctionCompiler::new(
NodeIdentity::new(SourceLocation {
line: 0,
col: 0,
}),
NodeIdentity::new(SourceLocation { line: 0, col: 0 }),
initial_scopes,
initial_slot_count,
));
@@ -177,8 +186,9 @@ impl Binder {
_kind: DeclarationKind,
diag: &mut Diagnostics,
) -> Option<Address<VirtualId>> {
let as_global = self.functions.len() == 1;
let current_fn = self.functions.last_mut().unwrap();
match current_fn.define_variable(name, identity) {
match current_fn.define_variable(name, identity, as_global) {
Ok(addr) => Some(addr),
Err(e) => {
diag.push_error(e, None);
@@ -245,14 +255,15 @@ impl Binder {
)
}
SyntaxKind::Def { pattern: target, value, .. } => {
if ctx == ExprContext::Expression {
diag.push_error(
"Statement 'def' cannot be used as an expression.",
Some(node.identity.clone()),
);
}
if let SyntaxKind::Identifier { symbol: ref name, .. } = target.kind {
SyntaxKind::Def {
pattern: target,
value,
..
} => {
if let SyntaxKind::Identifier {
symbol: ref name, ..
} = target.kind
{
let addr_opt = self.declare_variable(
name,
node.identity.clone(),
@@ -287,7 +298,8 @@ impl Binder {
}
} else {
let val_node = self.bind(value, ExprContext::Expression, diag);
let target_node = self.bind_pattern(target.as_ref(), DeclarationKind::Variable, diag);
let target_node =
self.bind_pattern(target.as_ref(), DeclarationKind::Variable, diag);
self.make_node(
node.identity.clone(),
@@ -343,17 +355,19 @@ impl Binder {
self.functions
.push(FunctionCompiler::new(identity.clone(), vec![], 0));
let params_bound = self.bind_pattern(params.as_ref(), DeclarationKind::Parameter, diag);
let params_bound =
self.bind_pattern(params.as_ref(), DeclarationKind::Parameter, diag);
let body_bound = self.bind(body.as_ref(), ExprContext::Expression, diag);
let compiled_fn = self.functions.pop().unwrap();
fn count_params(node: &Node<BoundPhase>) -> Option<u32> {
match &node.kind {
NodeKind::Identifier {
binding: IdentifierBinding::Declaration {
kind: DeclarationKind::Parameter,
..
},
binding:
IdentifierBinding::Declaration {
kind: DeclarationKind::Parameter,
..
},
..
} => Some(1),
NodeKind::Tuple { elements } => {
@@ -431,6 +445,22 @@ impl Binder {
)
}
SyntaxKind::Program { exprs } => {
let mut bound_exprs = Vec::new();
for (i, expr) in exprs.iter().enumerate() {
let expr_ctx = if i == exprs.len() - 1 {
ctx
} else {
ExprContext::Statement
};
bound_exprs.push(Rc::new(self.bind(expr, expr_ctx, diag)));
}
self.make_node(
node.identity.clone(),
NodeKind::Program { exprs: bound_exprs },
)
}
SyntaxKind::Tuple { elements } => {
let mut bound_elems = Vec::new();
for e in elements {
@@ -452,9 +482,7 @@ impl Binder {
let key_node = self.bind(k, ExprContext::Expression, diag);
let val_node = self.bind(v, ExprContext::Expression, diag);
if let NodeKind::Constant(Value::Keyword(kw)) =
&key_node.kind
{
if let NodeKind::Constant(Value::Keyword(kw)) = &key_node.kind {
layout_fields.push((*kw, StaticType::Any));
} else {
diag.push_error(
@@ -479,7 +507,10 @@ impl Binder {
)
}
SyntaxKind::Expansion { original_call, expanded } => {
SyntaxKind::Expansion {
original_call,
expanded,
} => {
let bound_expanded = self.bind(expanded.as_ref(), ctx, diag);
self.make_node(
node.identity.clone(),
@@ -495,10 +526,16 @@ impl Binder {
| SyntaxKind::Splice(_)
| SyntaxKind::MacroDecl { .. } => {
diag.push_error(
format!(
"Macro construct {:?} found in Binder. Macros must be expanded before binding.",
node.kind
),
{
let label = match &node.kind {
SyntaxKind::MacroDecl { name, .. } => format!("macro '{}'", name.name),
SyntaxKind::Template(_) => "template".to_string(),
SyntaxKind::Placeholder(_) => "placeholder".to_string(),
SyntaxKind::Splice(_) => "splice".to_string(),
_ => "unknown".to_string(),
};
format!("Unexpanded {label} found during binding. Macros must be expanded before binding.")
},
Some(node.identity.clone()),
);
self.make_node(node.identity.clone(), NodeKind::Error)
@@ -691,13 +728,18 @@ mod tests {
let syntax = parser.parse_expression();
let mut diagnostics = Diagnostics::new();
let (bound, captures, _scopes, _slots) = Binder::bind_root(vec![], 0, &syntax, &mut diagnostics).unwrap();
let (bound, captures, _scopes, _slots) =
Binder::bind_root(vec![], 0, &syntax, &mut diagnostics).unwrap();
if let NodeKind::Lambda { body, .. } = &bound.kind {
if let NodeKind::Block { exprs } = &body.kind {
let x_decl = &exprs[0];
if let NodeKind::Def { pattern, .. } = &x_decl.kind {
if let NodeKind::Identifier { binding: IdentifierBinding::Declaration { addr, .. }, .. } = &pattern.kind {
if let NodeKind::Identifier {
binding: IdentifierBinding::Declaration { addr, .. },
..
} = &pattern.kind
{
assert!(matches!(addr, Address::Local(_)));
assert!(
captures.contains_key(&x_decl.identity),
@@ -724,13 +766,18 @@ mod tests {
let syntax = parser.parse_expression();
let mut diagnostics = Diagnostics::new();
let (bound, captures, _scopes, _slots) = Binder::bind_root(vec![], 0, &syntax, &mut diagnostics).unwrap();
let (bound, captures, _scopes, _slots) =
Binder::bind_root(vec![], 0, &syntax, &mut diagnostics).unwrap();
if let NodeKind::Lambda { body, .. } = &bound.kind {
if let NodeKind::Block { exprs } = &body.kind {
let x_decl = &exprs[0];
if let NodeKind::Def { pattern, .. } = &x_decl.kind {
if let NodeKind::Identifier { binding: IdentifierBinding::Declaration { addr, .. }, .. } = &pattern.kind {
if let NodeKind::Identifier {
binding: IdentifierBinding::Declaration { addr, .. },
..
} = &pattern.kind
{
assert!(matches!(addr, Address::Local(_)));
assert!(
!captures.contains_key(&x_decl.identity),
@@ -760,7 +807,11 @@ mod tests {
let _ = Binder::bind_root(vec![], 0, &syntax, &mut diagnostics);
assert!(diagnostics.has_errors());
assert!(diagnostics.items.iter().any(|i| i.message.contains("already defined")));
assert!(
diagnostics
.items
.iter()
.any(|i| i.message.contains("already defined"))
);
}
}
+8
View File
@@ -190,6 +190,14 @@ impl Dumper {
}
self.indent -= 1;
}
NodeKind::Program { exprs } => {
self.log("Program", node);
self.indent += 1;
for expr in exprs {
self.visit(expr);
}
self.indent -= 1;
}
NodeKind::Tuple { elements } => {
self.log("Tuple", node);
+9
View File
@@ -148,6 +148,15 @@ fn emit_kind(node: &SyntaxNode, indent: usize, out: &mut String) {
out.push(')');
}
NodeKind::Program { exprs } => {
out.push_str("(do");
for expr in exprs {
out.push('\n');
emit_block(expr, indent + 1, out);
}
out.push(')');
}
NodeKind::Tuple { elements } => {
out.push('[');
for (i, el) in elements.iter().enumerate() {
+1 -1
View File
@@ -22,7 +22,7 @@ where
fn visit(&mut self, node: &Node<P>) {
match &node.kind {
NodeKind::Block { exprs } => {
NodeKind::Block { exprs } | NodeKind::Program { exprs } => {
for expr in exprs {
self.visit(expr);
}
+26
View File
@@ -161,6 +161,32 @@ impl Lowering {
}
}
NodeKind::Program { exprs } => {
if exprs.is_empty() {
NodeKind::Program { exprs: vec![] }
} else {
let scope_locals = Self::collect_scope_locals(exprs);
let last_idx = exprs.len() - 1;
let mut new_exprs = Vec::with_capacity(exprs.len());
for (i, expr) in exprs.iter().enumerate() {
let is_last = i == last_idx;
new_exprs.push(Rc::new(Self::transform(
expr.clone(),
is_tail_position && is_last,
allocator,
)));
}
for vid in scope_locals {
allocator.free_slot(vid);
}
NodeKind::Program { exprs: new_exprs }
}
}
NodeKind::Lambda {
params,
body,
+30 -1
View File
@@ -288,7 +288,36 @@ impl<E: MacroEvaluator> MacroExpander<E> {
})
}
_ => Ok(node),
SyntaxKind::Program { exprs } => {
let mut expanded_exprs = Vec::new();
for expr in exprs {
expanded_exprs.push(Rc::new(self.expand_recursive((*expr).clone())?));
}
Ok(SyntaxNode {
comments: node.comments.clone(),
identity: node.identity,
kind: SyntaxKind::Program {
exprs: expanded_exprs,
},
ty: (),
})
}
// Leaf nodes — no children to expand.
SyntaxKind::Identifier { .. }
| SyntaxKind::Constant(_)
| SyntaxKind::FieldAccessor(_)
| SyntaxKind::Nop
| SyntaxKind::Error => Ok(node),
// Quoting forms — preserved as-is (expanded in expand_template).
SyntaxKind::Template(_)
| SyntaxKind::Placeholder(_)
| SyntaxKind::Splice(_) => Ok(node),
// Post-binding constructs — should not appear in syntax phase.
SyntaxKind::GetField { .. }
| SyntaxKind::Extension(_) => Ok(node),
}
}
+1 -1
View File
@@ -122,7 +122,7 @@ impl UsageInfo {
}
}
}
NodeKind::Block { exprs } => {
NodeKind::Block { exprs } | NodeKind::Program { exprs } => {
for e in exprs {
self.collect(e);
}
+12
View File
@@ -497,6 +497,18 @@ impl TypeChecker {
(NodeKind::Block { exprs: typed_exprs }, last_ty)
}
NodeKind::Program { exprs } => {
let mut typed_exprs = Vec::new();
let mut last_ty = StaticType::Void;
for e in exprs {
let t = self.check_node(e, ctx, diag);
last_ty = t.ty.clone();
typed_exprs.push(Rc::new(t));
}
(NodeKind::Program { exprs: typed_exprs }, last_ty)
}
NodeKind::Lambda {
params,
@@ -90,6 +90,12 @@ impl TypeChecker {
.map(|e| Rc::new(self.finalize_node((*e).clone(), subst)))
.collect(),
},
NodeKind::Program { exprs } => NodeKind::Program {
exprs: exprs
.into_iter()
.map(|e| Rc::new(self.finalize_node((*e).clone(), subst)))
.collect(),
},
NodeKind::Tuple { elements } => NodeKind::Tuple {
elements: elements
.into_iter()
+78 -112
View File
@@ -2,10 +2,10 @@ use crate::ast::compiler::analyzer::Analyzer;
use crate::ast::compiler::binder::{Binder, CompilerScope, LocalInfo};
use crate::ast::compiler::call_hooks::RtlCompilerHook;
use crate::ast::compiler::{CapturePass, TypeChecker, TypedNode};
use crate::ast::nodes::{AnalyzedPhase, Symbol, SyntaxKind, SyntaxNode};
use crate::ast::nodes::{AnalyzedPhase, BoundPhase, Symbol, SyntaxKind, SyntaxNode};
use crate::ast::parser::Parser;
use crate::ast::closure::Closure;
use crate::ast::vm::{GlobalStore, TracingObserver, VM};
use crate::ast::vm::{self, GlobalStore, TracingObserver, VM};
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
@@ -334,21 +334,23 @@ impl Environment {
/// Used to pre-load all dependencies of a script before compiling it.
pub fn preload_dependencies(&self, source: &str, file_path: Option<&Path>) -> Result<(), String> {
let base_path = file_path.and_then(|p| p.parent()).unwrap_or_else(|| Path::new("."));
let mut files: Vec<(PathBuf, SyntaxNode)> = Vec::new();
let mut all_forms: Vec<Rc<SyntaxNode>> = Vec::new();
// 1. Always load the embedded system library first as a virtual module
let system_path = PathBuf::from(SYSTEM_LIB_PATH);
if !self.loaded_modules.borrow().contains(&system_path) {
self.loaded_modules.borrow_mut().insert(system_path.clone());
let mut parser = Parser::new(SYSTEM_LIB_SOURCE);
let syntax_ast = parser.parse_expression();
let forms = parser.parse_program();
if parser.diagnostics.has_errors() {
return Err(format!(
"Failed to parse embedded system library:\n{}",
parser.diagnostics.items[0].message
));
}
files.push((system_path, syntax_ast));
for form in forms {
all_forms.push(Rc::new(form));
}
}
// 2. Collect user dependencies (file I/O + parse, topological order)
@@ -356,109 +358,68 @@ impl Environment {
Rc::clone(&self.search_paths),
Rc::clone(&self.loaded_modules),
);
files.extend(loader.collect_dependency_files(source, base_path)?);
// Pass 1: Discovery (globals and macros)
for (_, syntax_ast) in &files {
self.discover_globals(syntax_ast);
for (_, syntax_ast) in loader.collect_dependency_files(source, base_path)? {
all_forms.push(Rc::new(syntax_ast));
}
// Pass 2: Compilation and initialization
for (path, syntax_ast) in files {
let typed_ast = self.compile_syntax(syntax_ast).map_err(|e: String| {
format!("Compilation error in {}:\n{}", path.display(), e)
})?;
self.run_script_compiled(typed_ast).map_err(|e: String| {
format!("Initialization error in {}:\n{}", path.display(), e)
})?;
// Compile and initialize all forms as a single program (no scope boundary)
if !all_forms.is_empty() {
let block = SyntaxNode {
identity: NodeIdentity::anonymous(),
kind: SyntaxKind::Program { exprs: all_forms },
ty: (),
comments: Rc::from([]),
};
let mut diagnostics = Diagnostics::new();
// 1. Expand macros
let expanded = self.expand(block, &mut diagnostics)
.ok_or_else(|| diagnostics.format_errors())?;
// 2. Bind names and update root scopes
let bound = self.bind_and_update(&expanded, &mut diagnostics)
.ok_or_else(|| diagnostics.format_errors())?;
// 3. Type-check (without lambda wrapping)
let typed = self.type_check(&bound, &mut diagnostics);
if diagnostics.has_errors() {
return Err(diagnostics.format_errors());
}
// 4. Link and execute
let linked = self.link(typed);
let mut vm = self.create_vm();
let stack = vec![Value::Void; linked.ty.stack_size as usize];
let result = vm.eval_in_frame(&mut vm::NoOpObserver, &linked, stack)?;
vm.resolve_tail_calls(&mut vm::NoOpObserver, Ok(result))?;
}
Ok(())
}
fn discover_globals(&self, node: &SyntaxNode) {
match &node.kind {
SyntaxKind::Def { pattern, .. } => {
let mut root_scopes = self.root_scopes.borrow_mut();
let last_idx = root_scopes.len() - 1;
let current_scope = &mut root_scopes[last_idx];
fn register_pattern(
pattern: &SyntaxNode,
scope: &mut CompilerScope,
slot_count: &mut u32,
identity: &crate::ast::types::NodeIdentity,
) {
match &pattern.kind {
SyntaxKind::Identifier { symbol: sym, .. } => {
if !scope.locals.contains_key(sym) {
let slot = VirtualId(*slot_count);
scope.locals.insert(
sym.clone(),
LocalInfo {
addr: Address::Local(slot),
identity: Rc::new(identity.clone()),
_ty: StaticType::Any,
purity: Purity::Impure,
},
);
*slot_count += 1;
}
}
SyntaxKind::Tuple { elements } => {
for el in elements {
register_pattern(el, scope, slot_count, identity);
}
}
_ => {}
}
}
let mut slot_count = self.root_slot_count.borrow_mut();
register_pattern(pattern, current_scope, &mut slot_count, &node.identity);
/// Expands macros in a syntax AST.
fn expand(&self, syntax_ast: SyntaxNode, diagnostics: &mut Diagnostics) -> Option<SyntaxNode> {
match self.get_expander().expand(syntax_ast) {
Ok(ast) => Some(ast),
Err(e) => {
diagnostics.push_error(e, None);
None
}
SyntaxKind::MacroDecl { name, params, body } => {
let mut registry = self.macro_registry.borrow_mut();
fn extract_names(node: &SyntaxNode) -> Vec<Rc<str>> {
match &node.kind {
SyntaxKind::Identifier { symbol: sym, .. } => vec![sym.name.clone()],
SyntaxKind::Tuple { elements } => {
elements.iter().flat_map(|e| extract_names(e)).collect()
}
_ => vec![],
}
}
let p_names = extract_names(params);
registry.define(name.name.clone(), p_names, body.as_ref().clone());
}
SyntaxKind::Block { exprs } => {
for expr in exprs {
self.discover_globals(expr);
}
}
SyntaxKind::Expansion { expanded, .. } => {
self.discover_globals(expanded);
}
_ => {}
}
}
fn compile_pipeline(&self, syntax_ast: SyntaxNode, diagnostics: &mut Diagnostics) -> Option<TypedNode> {
let expanded_ast = match self.get_expander().expand(syntax_ast) {
Ok(ast) => ast,
Err(e) => {
diagnostics.push_error(e, None);
return None;
}
};
/// Binds names, applies captures, updates root scopes and pre-allocates global slots.
fn bind_and_update(
&self,
syntax_ast: &SyntaxNode,
diagnostics: &mut Diagnostics,
) -> Option<Node<BoundPhase>> {
let initial_scopes = self.root_scopes.borrow().clone();
let initial_slot_count = *self.root_slot_count.borrow();
let (bound_ast, captures, final_scopes, final_slot_count) =
match Binder::bind_root(initial_scopes, initial_slot_count, &expanded_ast, diagnostics) {
match Binder::bind_root(initial_scopes, initial_slot_count, syntax_ast, diagnostics) {
Ok(res) => res,
Err(e) => {
diagnostics.push_error(e, None);
@@ -466,14 +427,13 @@ impl Environment {
}
};
// Update environment state with new bindings from this script
// Update environment state with new bindings
*self.root_scopes.borrow_mut() = final_scopes;
*self.root_slot_count.borrow_mut() = final_slot_count;
let bound_ast = CapturePass::apply(bound_ast, &captures);
// Pre-allocate user global slots to prevent out-of-bounds during specialization/optimization.
// root_slot_count includes RTL slots; subtract to get the number of user-only slots.
{
let mut user = self.user_values.borrow_mut();
let count = *self.root_slot_count.borrow() as usize;
@@ -485,20 +445,24 @@ impl Environment {
LambdaCollector::collect(&bound_ast, &mut self.function_registry.borrow_mut());
let checker = TypeChecker::new(self.root_types.clone(), Rc::clone(&self.rtl.compiler_hooks));
let wrapped_ast = if let NodeKind::Lambda { .. } = bound_ast.kind {
Some(bound_ast)
}
/// Wraps a bound AST in a parameterless lambda (unless it already is one).
fn wrap_as_lambda(&self, bound_ast: Node<BoundPhase>) -> Node<BoundPhase> {
if let NodeKind::Lambda { .. } = bound_ast.kind {
bound_ast
} else {
Node {
identity: bound_ast.identity.clone(),
kind: NodeKind::Lambda {
params: std::rc::Rc::new(Node {
params: Rc::new(Node {
identity: bound_ast.identity.clone(),
kind: NodeKind::Tuple { elements: vec![] },
ty: (),
comments: Rc::from([]),
}),
body: std::rc::Rc::new(bound_ast),
body: Rc::new(bound_ast),
info: LambdaBinding {
upvalues: vec![],
positional_count: Some(0),
@@ -507,22 +471,24 @@ impl Environment {
ty: (),
comments: Rc::from([]),
}
};
let typed = checker.check(&wrapped_ast, &[], diagnostics);
self.collect_doc_comments(&typed);
Some(typed)
}
}
fn compile_syntax(&self, syntax_ast: SyntaxNode) -> Result<TypedNode, String> {
let mut diagnostics = Diagnostics::new();
/// Type-checks a bound AST and collects doc comments.
fn type_check(&self, bound_ast: &Node<BoundPhase>, diagnostics: &mut Diagnostics) -> TypedNode {
let checker = TypeChecker::new(self.root_types.clone(), Rc::clone(&self.rtl.compiler_hooks));
let typed = checker.check(bound_ast, &[], diagnostics);
self.collect_doc_comments(&typed);
typed
}
let typed_ast_opt = self.compile_pipeline(syntax_ast, &mut diagnostics);
if diagnostics.has_errors() || typed_ast_opt.is_none() {
return Err(diagnostics.format_errors());
}
Ok(typed_ast_opt.unwrap())
/// Full compilation pipeline: expand → bind → wrap → type-check.
fn compile_pipeline(&self, syntax_ast: SyntaxNode, diagnostics: &mut Diagnostics) -> Option<TypedNode> {
let expanded = self.expand(syntax_ast, diagnostics)?;
let bound = self.bind_and_update(&expanded, diagnostics)?;
let wrapped = self.wrap_as_lambda(bound);
let typed = self.type_check(&wrapped, diagnostics);
Some(typed)
}
/// Allocates a new global slot and registers a value in the fixed RTL scope (scope index 0).
+9 -1
View File
@@ -445,6 +445,11 @@ pub enum NodeKind<P: CompilerPhase = SyntaxPhase> {
Block {
exprs: Vec<Rc<Node<P>>>,
},
/// Top-level program node. Like Block, but does not introduce a scope —
/// definitions propagate to the enclosing scope.
Program {
exprs: Vec<Rc<Node<P>>>,
},
Tuple {
elements: Vec<Rc<Node<P>>>,
},
@@ -514,6 +519,7 @@ impl<P: CompilerPhase> Clone for NodeKind<P> {
},
NodeKind::Again { args } => NodeKind::Again { args: args.clone() },
NodeKind::Block { exprs } => NodeKind::Block { exprs: exprs.clone() },
NodeKind::Program { exprs } => NodeKind::Program { exprs: exprs.clone() },
NodeKind::Tuple { elements } => NodeKind::Tuple { elements: elements.clone() },
NodeKind::Record { fields, layout } => NodeKind::Record {
fields: fields.clone(),
@@ -570,7 +576,8 @@ impl<P: CompilerPhase> PartialEq for NodeKind<P> {
Rc::ptr_eq(ca, cb) && Rc::ptr_eq(aa, ab)
}
(NodeKind::Again { args: aa }, NodeKind::Again { args: ab }) => Rc::ptr_eq(aa, ab),
(NodeKind::Block { exprs: ea }, NodeKind::Block { exprs: eb }) => {
(NodeKind::Block { exprs: ea }, NodeKind::Block { exprs: eb })
| (NodeKind::Program { exprs: ea }, NodeKind::Program { exprs: eb }) => {
ea.len() == eb.len() && ea.iter().zip(eb.iter()).all(|(a, b)| Rc::ptr_eq(a, b))
}
(NodeKind::Tuple { elements: ea }, NodeKind::Tuple { elements: eb }) => {
@@ -613,6 +620,7 @@ impl<P: CompilerPhase> NodeKind<P> {
NodeKind::Call { .. } => "CALL".to_string(),
NodeKind::Again { .. } => "AGAIN".to_string(),
NodeKind::Block { .. } => "BLOCK".to_string(),
NodeKind::Program { .. } => "PROGRAM".to_string(),
NodeKind::Tuple { elements } => format!("TUPLE({})", elements.len()),
NodeKind::Record { fields, .. } => format!("RECORD({})", fields.len()),
NodeKind::GetField { field, .. } => format!("GET_FIELD(.{})", field.name()),
+31 -33
View File
@@ -1,33 +1,31 @@
;; Myc System Library
;; Standard macros and core utilities.
(do
(def func1 (fn [] (print "Hello World")))
(macro while [cond body]
`((fn [] (if ~cond
(do ~body (again))
)))
)
(macro repeat [var limit body]
`((fn [~var __limit]
(if (< ~var __limit)
(do
~body
(again (+ ~var 1) __limit)
)
))
0 ~limit)
)
;; Creates a stateful cache (Series) from a stateless stream.
;; The element type is inferred from the stream's item type.
(macro cache [lookback src]
`(do
(def data (series ~lookback))
(pipe [~src] (fn [s] (do (push data s))))
data
)
)
)
;; Myc System Library
;; Standard macros and core utilities.
(def func1 (fn [] (print "Hello World")))
(macro while [cond body]
`((fn [] (if ~cond
(do ~body (again))
)))
)
(macro repeat [var limit body]
`((fn [~var __limit]
(if (< ~var __limit)
(do
~body
(again (+ ~var 1) __limit)
)
))
0 ~limit)
)
;; Creates a stateful cache (Series) from a stateless stream.
;; The element type is inferred from the stream's item type.
(macro cache [lookback src]
`(do
(def data (series ~lookback))
(pipe [~src] (fn [s] (do (push data s))))
data
)
)
+15 -10
View File
@@ -266,22 +266,27 @@ impl VM {
self.resolve_tail_calls(observer, result)
}
pub fn run_with_observer<O: VMObserver>(
&mut self,
observer: &mut O,
root: &ExecNode,
) -> Result<Value, String> {
self.stack.clear();
pub fn eval_in_frame<O: VMObserver>(&mut self, observer: &mut O, root: &ExecNode, stack: Vec<Value>) -> Result<Value, String> {
self.stack = stack;
self.frames.clear();
self.stack.resize(root.ty.stack_size as usize, Value::Void);
self.frames.push(CallFrame {
stack_base: 0,
closure: None,
});
let result = self.eval_internal(observer, root);
self.frames.pop();
result
}
pub fn run_with_observer<O: VMObserver>(
&mut self,
observer: &mut O,
root: &ExecNode,
) -> Result<Value, String> {
let mut stack = Vec::new();
stack.resize(root.ty.stack_size as usize, Value::Void);
let result = self.eval_in_frame(observer, root, stack);
self.resolve_tail_calls(observer, result)
}
@@ -438,7 +443,7 @@ impl VM {
Ok(Value::Void)
}
}
NodeKind::Block { exprs } => {
NodeKind::Block { exprs } | NodeKind::Program { exprs } => {
let mut last = Value::Void;
for e in exprs {
last = self.eval_internal(obs, e)?;
+1 -1
View File
@@ -21,7 +21,7 @@ fn test_destructuring_scalar_error() {
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
"Statement 'def' cannot be used as an expression.\nCannot destructure type int as a tuple/vector"
"Cannot destructure type int as a tuple/vector"
);
}