diff --git a/crates/nxc-cli/Cargo.toml b/crates/nxc-cli/Cargo.toml index d420ab0..ffe384e 100644 --- a/crates/nxc-cli/Cargo.toml +++ b/crates/nxc-cli/Cargo.toml @@ -5,14 +5,16 @@ edition.workspace = true license.workspace = true authors.workspace = true +[lib] +path = "src/lib.rs" + [[bin]] name = "nxc" path = "src/main.rs" [[bin]] name = "nexacore" -path = "src/main.rs" +path = "src/bin/nexacore.rs" [dependencies] nxc-driver = { path = "../nxc-driver" } - diff --git a/crates/nxc-cli/src/bin/nexacore.rs b/crates/nxc-cli/src/bin/nexacore.rs new file mode 100644 index 0000000..363e423 --- /dev/null +++ b/crates/nxc-cli/src/bin/nexacore.rs @@ -0,0 +1,3 @@ +fn main() -> std::process::ExitCode { + nxc_cli::main_entry() +} diff --git a/crates/nxc-cli/src/lib.rs b/crates/nxc-cli/src/lib.rs new file mode 100644 index 0000000..559ff43 --- /dev/null +++ b/crates/nxc-cli/src/lib.rs @@ -0,0 +1,86 @@ +use std::env; +use std::path::Path; +use std::process::ExitCode; + +pub fn main_entry() -> ExitCode { + match run() { + Ok(()) => ExitCode::SUCCESS, + Err(message) => { + eprintln!("{message}"); + ExitCode::FAILURE + } + } +} + +fn run() -> Result<(), String> { + let mut args = env::args().skip(1); + let Some(command) = args.next() else { + print_help(); + return Ok(()); + }; + + match command.as_str() { + "check" | "build" => { + let Some(path) = args.next() else { + return Err(format!("usage: {} {command} ", executable_name())); + }; + + let output = + nxc_driver::check_file(Path::new(&path)).map_err(format_driver_error)?; + + if output.has_errors() { + eprintln!("{}", output.render_diagnostics()); + return Err(format!( + "check failed with {} diagnostic(s)", + output.diagnostics.len() + )); + } + + let summary = output.summary(); + println!("checked {}", output.path.display()); + println!("tokens: {}", output.tokens.len()); + println!("items: {}", summary.items); + println!("functions: {}", summary.functions); + println!("structs: {}", summary.structs); + Ok(()) + } + "run" => Err("runtime execution is not implemented yet".to_string()), + "new" => Err("project scaffolding is not implemented yet".to_string()), + "test" => Err("test runner is not implemented yet".to_string()), + "fmt" => Err("formatter is not implemented yet".to_string()), + "add" => Err("package manager is not implemented yet".to_string()), + "doc" => Err("docs generator is not implemented yet".to_string()), + _ => Err(format!("unknown command: {command}")), + } +} + +fn executable_name() -> String { + env::args() + .next() + .and_then(|path| { + Path::new(&path) + .file_name() + .map(|name| name.to_string_lossy().to_string()) + }) + .unwrap_or_else(|| "nxc".to_string()) +} + +fn format_driver_error(error: nxc_driver::DriverError) -> String { + match error { + nxc_driver::DriverError::Io(io) => format!("io error: {io}"), + } +} + +fn print_help() { + let name = executable_name(); + println!("NexaCore CLI"); + println!("usage:"); + println!(" {name} check "); + println!(" {name} build "); + println!(" {name} run "); + println!(" {name} new "); + println!(" {name} test"); + println!(" {name} fmt"); + println!(" {name} add "); + println!(" {name} doc"); +} diff --git a/crates/nxc-cli/src/main.rs b/crates/nxc-cli/src/main.rs index 8382fd3..6375c86 100644 --- a/crates/nxc-cli/src/main.rs +++ b/crates/nxc-cli/src/main.rs @@ -1,87 +1,4 @@ -use std::env; -use std::path::Path; -use std::process::ExitCode; - -fn main() -> ExitCode { - match run() { - Ok(()) => ExitCode::SUCCESS, - Err(message) => { - eprintln!("{message}"); - ExitCode::FAILURE - } - } -} - -fn run() -> Result<(), String> { - let mut args = env::args().skip(1); - let Some(command) = args.next() else { - print_help(); - return Ok(()); - }; - - match command.as_str() { - "check" | "build" => { - let Some(path) = args.next() else { - return Err(format!("usage: {} {command} ", executable_name())); - }; - - let output = - nxc_driver::check_file(Path::new(&path)).map_err(format_driver_error)?; - - if output.has_errors() { - eprintln!("{}", output.render_diagnostics()); - return Err(format!( - "check failed with {} diagnostic(s)", - output.diagnostics.len() - )); - } - - let summary = output.summary(); - println!("checked {}", output.path.display()); - println!("tokens: {}", output.tokens.len()); - println!("items: {}", summary.items); - println!("functions: {}", summary.functions); - println!("structs: {}", summary.structs); - Ok(()) - } - "run" => Err("runtime execution is not implemented yet".to_string()), - "new" => Err("project scaffolding is not implemented yet".to_string()), - "test" => Err("test runner is not implemented yet".to_string()), - "fmt" => Err("formatter is not implemented yet".to_string()), - "add" => Err("package manager is not implemented yet".to_string()), - "doc" => Err("docs generator is not implemented yet".to_string()), - _ => Err(format!("unknown command: {command}")), - } -} - -fn executable_name() -> String { - env::args() - .next() - .and_then(|path| { - Path::new(&path) - .file_name() - .map(|name| name.to_string_lossy().to_string()) - }) - .unwrap_or_else(|| "nxc".to_string()) -} - -fn format_driver_error(error: nxc_driver::DriverError) -> String { - match error { - nxc_driver::DriverError::Io(io) => format!("io error: {io}"), - } -} - -fn print_help() { - let name = executable_name(); - println!("NexaCore CLI"); - println!("usage:"); - println!(" {name} check "); - println!(" {name} build "); - println!(" {name} run "); - println!(" {name} new "); - println!(" {name} test"); - println!(" {name} fmt"); - println!(" {name} add "); - println!(" {name} doc"); +fn main() -> std::process::ExitCode { + nxc_cli::main_entry() } diff --git a/crates/nxc-driver/src/lib.rs b/crates/nxc-driver/src/lib.rs index 21ceec6..326b830 100644 --- a/crates/nxc-driver/src/lib.rs +++ b/crates/nxc-driver/src/lib.rs @@ -3,7 +3,8 @@ use std::fs; use std::path::{Path, PathBuf}; use nxc_frontend::{ - has_errors, Diagnostic, Item, LexResult, Lexer, Module, ParseResult, Parser, Token, + analyze, has_errors, Diagnostic, Item, LexResult, Lexer, Module, ParseResult, Parser, Token, + TypedModule, }; #[derive(Debug, Clone)] @@ -12,6 +13,7 @@ pub struct FrontendOutput { pub source: String, pub tokens: Vec, pub module: Module, + pub typed_module: Option, pub diagnostics: Vec, } @@ -86,13 +88,20 @@ pub fn check_source(path: PathBuf, source: String) -> FrontendOutput { } = Parser::new(tokens.clone()).parse_module(); lexer_diagnostics.extend(parser_diagnostics); + let mut typed_module = None; + + if !has_errors(&lexer_diagnostics) { + let semantic = analyze(&module); + lexer_diagnostics.extend(semantic.diagnostics); + typed_module = Some(semantic.module); + } FrontendOutput { path, source, tokens, module, + typed_module, diagnostics: lexer_diagnostics, } } - diff --git a/crates/nxc-frontend/src/lib.rs b/crates/nxc-frontend/src/lib.rs index 618dd69..2390740 100644 --- a/crates/nxc-frontend/src/lib.rs +++ b/crates/nxc-frontend/src/lib.rs @@ -2,6 +2,7 @@ pub mod ast; pub mod diagnostics; pub mod lexer; pub mod parser; +pub mod semantic; pub mod token; pub use ast::{ @@ -11,5 +12,8 @@ pub use ast::{ pub use diagnostics::{has_errors, Diagnostic, Severity}; pub use lexer::{LexResult, Lexer}; pub use parser::{ParseResult, Parser}; +pub use semantic::{ + analyze, FunctionId, LocalId, SemanticResult, StructId, Type, TypedBlock, TypedExpr, + TypedExprKind, TypedFunction, TypedIfStmt, TypedModule, TypedStmt, TypedStmtKind, TypedStruct, +}; pub use token::{Keyword, Span, Token, TokenKind}; - diff --git a/crates/nxc-frontend/src/semantic.rs b/crates/nxc-frontend/src/semantic.rs new file mode 100644 index 0000000..20a72b8 --- /dev/null +++ b/crates/nxc-frontend/src/semantic.rs @@ -0,0 +1,1009 @@ +use std::collections::HashMap; + +use crate::ast::{ + BinaryOp, Block, Expr, ExprKind, FunctionDecl, IfStmt, Item, Literal, Module, Stmt, StmtKind, + StructDecl, TypeRef, UnaryOp, +}; +use crate::diagnostics::Diagnostic; +use crate::token::Span; + +#[derive(Debug, Clone)] +pub struct SemanticResult { + pub module: TypedModule, + pub diagnostics: Vec, +} + +pub fn analyze(module: &Module) -> SemanticResult { + SemanticAnalyzer::new(module).analyze() +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct FunctionId(pub usize); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct StructId(pub usize); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct LocalId(pub usize); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Type { + Int, + Float, + Bool, + String, + Void, + Struct(String), + Function(FunctionId), + Error, +} + +impl Type { + pub fn display_name(&self) -> String { + match self { + Type::Int => "Int".to_string(), + Type::Float => "Float".to_string(), + Type::Bool => "Bool".to_string(), + Type::String => "String".to_string(), + Type::Void => "Void".to_string(), + Type::Struct(name) => name.clone(), + Type::Function(_) => "Function".to_string(), + Type::Error => "".to_string(), + } + } + + fn is_numeric(&self) -> bool { + matches!(self, Type::Int | Type::Float) + } +} + +#[derive(Debug, Clone, Default)] +pub struct TypedModule { + pub functions: Vec, + pub structs: Vec, +} + +#[derive(Debug, Clone)] +pub struct TypedStruct { + pub id: StructId, + pub name: String, + pub fields: Vec, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct TypedField { + pub name: String, + pub ty: Type, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct TypedFunction { + pub id: FunctionId, + pub name: String, + pub params: Vec, + pub return_type: Type, + pub body: TypedBlock, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct TypedParam { + pub id: LocalId, + pub name: String, + pub ty: Type, + pub span: Span, +} + +#[derive(Debug, Clone, Default)] +pub struct TypedBlock { + pub statements: Vec, + pub span: Span, + pub guarantees_return: bool, +} + +#[derive(Debug, Clone)] +pub struct TypedStmt { + pub kind: TypedStmtKind, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub enum TypedStmtKind { + Let { + binding: LocalId, + name: String, + ty: Type, + value: TypedExpr, + }, + Return(Option), + If(TypedIfStmt), + Expr(TypedExpr), +} + +#[derive(Debug, Clone)] +pub struct TypedIfStmt { + pub condition: TypedExpr, + pub then_block: TypedBlock, + pub else_block: Option, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct TypedExpr { + pub kind: TypedExprKind, + pub ty: Type, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub enum TypedExprKind { + Error, + Literal(Literal), + Local(LocalId, String), + Function(FunctionId, String), + Unary { + op: UnaryOp, + expr: Box, + }, + Binary { + left: Box, + op: BinaryOp, + right: Box, + }, + Group(Box), + Call { + callee: Box, + function: Option, + args: Vec, + }, +} + +#[derive(Debug, Clone)] +struct StructInfo { + id: StructId, + decl: StructDecl, + fields: Vec, +} + +#[derive(Debug, Clone)] +struct FunctionInfo { + id: FunctionId, + decl: FunctionDecl, + params: Vec, + return_type: Type, +} + +#[derive(Debug, Clone)] +struct ResolvedParam { + name: String, + ty: Type, + span: Span, +} + +#[derive(Debug, Clone)] +struct LocalBinding { + id: LocalId, + name: String, + ty: Type, +} + +#[derive(Debug, Default)] +struct Scope { + bindings: HashMap, +} + +struct SemanticAnalyzer<'a> { + module: &'a Module, + diagnostics: Vec, + functions: Vec, + structs: Vec, + function_names: HashMap, + struct_names: HashMap, + next_local_id: usize, +} + +impl<'a> SemanticAnalyzer<'a> { + fn new(module: &'a Module) -> Self { + Self { + module, + diagnostics: Vec::new(), + functions: Vec::new(), + structs: Vec::new(), + function_names: HashMap::new(), + struct_names: HashMap::new(), + next_local_id: 0, + } + } + + fn analyze(mut self) -> SemanticResult { + self.collect_top_level_symbols(); + self.resolve_structs(); + self.resolve_function_signatures(); + + let typed_structs = self.build_typed_structs(); + let typed_functions = self.analyze_functions(); + + SemanticResult { + module: TypedModule { + functions: typed_functions, + structs: typed_structs, + }, + diagnostics: self.diagnostics, + } + } + + fn collect_top_level_symbols(&mut self) { + for item in &self.module.items { + match item { + Item::Function(function) => { + let id = FunctionId(self.functions.len()); + if let Some(existing) = self.function_names.get(&function.name) { + let _ = existing; + self.error( + format!("duplicate function `{}`", function.name), + function.span, + ); + } else { + self.function_names.insert(function.name.clone(), id); + } + + self.functions.push(FunctionInfo { + id, + decl: function.clone(), + params: Vec::new(), + return_type: Type::Error, + }); + } + Item::Struct(struct_decl) => { + let id = StructId(self.structs.len()); + if let Some(existing) = self.struct_names.get(&struct_decl.name) { + let _ = existing; + self.error( + format!("duplicate struct `{}`", struct_decl.name), + struct_decl.span, + ); + } else { + self.struct_names.insert(struct_decl.name.clone(), id); + } + + self.structs.push(StructInfo { + id, + decl: struct_decl.clone(), + fields: Vec::new(), + }); + } + } + } + } + + fn resolve_structs(&mut self) { + for index in 0..self.structs.len() { + let decl = self.structs[index].decl.clone(); + let mut fields = Vec::new(); + let mut seen = HashMap::::new(); + + for field in &decl.fields { + if let Some(existing) = seen.insert(field.name.clone(), field.span) { + let _ = existing; + self.error( + format!( + "duplicate field `{}` in struct `{}`", + field.name, decl.name + ), + field.span, + ); + } + + let ty = self.resolve_type_ref(&field.ty, TypeContext::Field); + fields.push(TypedField { + name: field.name.clone(), + ty, + span: field.span, + }); + } + + self.structs[index].fields = fields; + } + } + + fn resolve_function_signatures(&mut self) { + for index in 0..self.functions.len() { + let decl = self.functions[index].decl.clone(); + + let mut params = Vec::new(); + for param in &decl.params { + params.push(ResolvedParam { + name: param.name.clone(), + ty: self.resolve_type_ref(¶m.ty, TypeContext::Parameter), + span: param.span, + }); + } + + let return_type = match &decl.return_type { + Some(type_ref) => self.resolve_type_ref(type_ref, TypeContext::Return), + None => { + self.error( + format!("function `{}` must declare an explicit return type", decl.name), + decl.span, + ); + Type::Error + } + }; + + self.functions[index].params = params; + self.functions[index].return_type = return_type; + } + } + + fn build_typed_structs(&self) -> Vec { + self.structs + .iter() + .map(|info| TypedStruct { + id: info.id, + name: info.decl.name.clone(), + fields: info.fields.clone(), + span: info.decl.span, + }) + .collect() + } + + fn analyze_functions(&mut self) -> Vec { + let mut typed = Vec::new(); + + for index in 0..self.functions.len() { + let info = self.functions[index].clone(); + typed.push(self.analyze_function(&info)); + } + + typed + } + + fn analyze_function(&mut self, info: &FunctionInfo) -> TypedFunction { + let mut scopes = vec![Scope::default()]; + let mut typed_params = Vec::new(); + + for param in &info.params { + let local_id = self.next_local(); + let binding = LocalBinding { + id: local_id, + name: param.name.clone(), + ty: param.ty.clone(), + }; + + if !self.declare_local(&mut scopes, binding.clone()) { + self.error( + format!( + "duplicate parameter `{}` in function `{}`", + param.name, info.decl.name + ), + param.span, + ); + } + + typed_params.push(TypedParam { + id: local_id, + name: param.name.clone(), + ty: param.ty.clone(), + span: param.span, + }); + } + + let body = self.analyze_block(&info.decl.body, &mut scopes, &info.return_type, false); + if !matches!(info.return_type, Type::Void | Type::Error) && !body.guarantees_return { + self.error( + format!( + "function `{}` may exit without returning `{}`", + info.decl.name, + info.return_type.display_name() + ), + info.decl.body.span, + ); + } + + TypedFunction { + id: info.id, + name: info.decl.name.clone(), + params: typed_params, + return_type: info.return_type.clone(), + body, + span: info.decl.span, + } + } + + fn analyze_block( + &mut self, + block: &Block, + scopes: &mut Vec, + return_type: &Type, + push_scope: bool, + ) -> TypedBlock { + if push_scope { + scopes.push(Scope::default()); + } + + let mut statements = Vec::new(); + let mut guarantees_return = false; + for statement in &block.statements { + let typed = self.analyze_statement(statement, scopes, return_type); + if typed_guarantees_return(&typed) { + guarantees_return = true; + } + statements.push(typed); + } + + if push_scope { + let _ = scopes.pop(); + } + + TypedBlock { + statements, + span: block.span, + guarantees_return, + } + } + + fn analyze_statement( + &mut self, + statement: &Stmt, + scopes: &mut Vec, + return_type: &Type, + ) -> TypedStmt { + match &statement.kind { + StmtKind::Let { name, ty, value } => { + let typed_value = self.analyze_expr(value, scopes); + let binding_type = if let Some(type_ref) = ty { + let declared_type = self.resolve_type_ref(type_ref, TypeContext::Local); + if !self.assignment_compatible(&declared_type, &typed_value.ty) { + self.error( + format!( + "cannot assign `{}` to binding `{}` of type `{}`", + typed_value.ty.display_name(), + name, + declared_type.display_name() + ), + value.span, + ); + } + declared_type + } else { + typed_value.ty.clone() + }; + + let local_id = self.next_local(); + let binding = LocalBinding { + id: local_id, + name: name.clone(), + ty: binding_type.clone(), + }; + + if !self.declare_local(scopes, binding) { + self.error( + format!("duplicate binding `{}` in the same scope", name), + statement.span, + ); + } + + TypedStmt { + span: statement.span, + kind: TypedStmtKind::Let { + binding: local_id, + name: name.clone(), + ty: binding_type, + value: typed_value, + }, + } + } + StmtKind::Return(value) => { + let typed_value = value.as_ref().map(|expr| self.analyze_expr(expr, scopes)); + match (&typed_value, return_type) { + (Some(expr), Type::Void) => self.error( + "void function cannot return a value", + expr.span, + ), + (None, ty) if !matches!(ty, Type::Void | Type::Error) => self.error( + format!( + "return statement must produce `{}`", + ty.display_name() + ), + statement.span, + ), + (Some(expr), ty) + if !matches!(ty, Type::Void | Type::Error) + && !self.assignment_compatible(ty, &expr.ty) => + { + self.error( + format!( + "return type mismatch: expected `{}`, found `{}`", + ty.display_name(), + expr.ty.display_name() + ), + expr.span, + ); + } + _ => {} + } + + TypedStmt { + span: statement.span, + kind: TypedStmtKind::Return(typed_value), + } + } + StmtKind::If(if_stmt) => self.analyze_if_statement(if_stmt, statement.span, scopes, return_type), + StmtKind::Expr(expr) => TypedStmt { + span: statement.span, + kind: TypedStmtKind::Expr(self.analyze_expr(expr, scopes)), + }, + } + } + + fn analyze_if_statement( + &mut self, + if_stmt: &IfStmt, + span: Span, + scopes: &mut Vec, + return_type: &Type, + ) -> TypedStmt { + let condition = self.analyze_expr(&if_stmt.condition, scopes); + if !matches!(condition.ty, Type::Bool | Type::Error) { + self.error( + format!( + "if condition must be `Bool`, found `{}`", + condition.ty.display_name() + ), + if_stmt.condition.span, + ); + } + + let then_block = self.analyze_block(&if_stmt.then_block, scopes, return_type, true); + let else_block = if_stmt + .else_block + .as_ref() + .map(|block| self.analyze_block(block, scopes, return_type, true)); + + TypedStmt { + span, + kind: TypedStmtKind::If(TypedIfStmt { + condition, + then_block, + else_block, + span, + }), + } + } + + fn analyze_expr(&mut self, expr: &Expr, scopes: &mut Vec) -> TypedExpr { + match &expr.kind { + ExprKind::Literal(literal) => TypedExpr { + kind: TypedExprKind::Literal(literal.clone()), + ty: match literal { + Literal::Integer(_) => Type::Int, + Literal::Float(_) => Type::Float, + Literal::String(_) => Type::String, + Literal::Bool(_) => Type::Bool, + }, + span: expr.span, + }, + ExprKind::Identifier(name) => self.resolve_identifier(name, expr.span, scopes), + ExprKind::Group(inner) => { + let typed_inner = self.analyze_expr(inner, scopes); + TypedExpr { + ty: typed_inner.ty.clone(), + span: expr.span, + kind: TypedExprKind::Group(Box::new(typed_inner)), + } + } + ExprKind::Unary { op, expr: inner } => { + let typed_inner = self.analyze_expr(inner, scopes); + let result_ty = match op { + UnaryOp::Negate => { + if matches!(typed_inner.ty, Type::Int | Type::Float | Type::Error) { + typed_inner.ty.clone() + } else { + self.error( + format!( + "unary `-` expects `Int` or `Float`, found `{}`", + typed_inner.ty.display_name() + ), + inner.span, + ); + Type::Error + } + } + UnaryOp::Not => { + if matches!(typed_inner.ty, Type::Bool | Type::Error) { + Type::Bool + } else { + self.error( + format!( + "unary `!` expects `Bool`, found `{}`", + typed_inner.ty.display_name() + ), + inner.span, + ); + Type::Error + } + } + }; + + TypedExpr { + kind: TypedExprKind::Unary { + op: *op, + expr: Box::new(typed_inner), + }, + ty: result_ty, + span: expr.span, + } + } + ExprKind::Binary { left, op, right } => { + let typed_left = self.analyze_expr(left, scopes); + let typed_right = self.analyze_expr(right, scopes); + let result_ty = self.binary_result_type(*op, &typed_left.ty, &typed_right.ty, expr.span); + + TypedExpr { + kind: TypedExprKind::Binary { + left: Box::new(typed_left), + op: *op, + right: Box::new(typed_right), + }, + ty: result_ty, + span: expr.span, + } + } + ExprKind::Call { callee, args } => self.analyze_call(expr.span, callee, args, scopes), + } + } + + fn analyze_call( + &mut self, + span: Span, + callee: &Expr, + args: &[Expr], + scopes: &mut Vec, + ) -> TypedExpr { + let (typed_callee, function_id) = match &callee.kind { + ExprKind::Identifier(name) => { + if let Some(binding) = self.lookup_local(scopes, name) { + ( + TypedExpr { + kind: TypedExprKind::Local(binding.id, binding.name.clone()), + ty: binding.ty.clone(), + span: callee.span, + }, + None, + ) + } else if let Some(function_id) = self.function_names.get(name).copied() { + ( + TypedExpr { + kind: TypedExprKind::Function(function_id, name.clone()), + ty: Type::Function(function_id), + span: callee.span, + }, + Some(function_id), + ) + } else { + self.error(format!("unknown function `{name}`"), callee.span); + let typed_args = args + .iter() + .map(|arg| self.analyze_expr(arg, scopes)) + .collect::>(); + return TypedExpr { + kind: TypedExprKind::Call { + callee: Box::new(TypedExpr { + kind: TypedExprKind::Error, + ty: Type::Error, + span: callee.span, + }), + function: None, + args: typed_args, + }, + ty: Type::Error, + span, + }; + } + } + _ => { + let typed_callee = self.analyze_expr(callee, scopes); + let function_id = match typed_callee.ty { + Type::Function(function_id) => Some(function_id), + _ => None, + }; + (typed_callee, function_id) + } + }; + + let typed_args = args + .iter() + .map(|arg| self.analyze_expr(arg, scopes)) + .collect::>(); + + let result_ty = if let Some(function_id) = function_id { + let signature_name = self.functions[function_id.0].decl.name.clone(); + let signature_params = self.functions[function_id.0].params.clone(); + let signature_return = self.functions[function_id.0].return_type.clone(); + + if typed_args.len() != signature_params.len() { + self.error( + format!( + "function `{}` expects {} argument(s), found {}", + signature_name, + signature_params.len(), + typed_args.len() + ), + span, + ); + } + + for (arg, param) in typed_args.iter().zip(signature_params.iter()) { + if !self.assignment_compatible(¶m.ty, &arg.ty) { + self.error( + format!( + "argument for parameter `{}` expects `{}`, found `{}`", + param.name, + param.ty.display_name(), + arg.ty.display_name() + ), + arg.span, + ); + } + } + + signature_return + } else if matches!(typed_callee.ty, Type::Error) { + Type::Error + } else { + self.error( + format!( + "expression of type `{}` is not callable", + typed_callee.ty.display_name() + ), + callee.span, + ); + Type::Error + }; + + TypedExpr { + kind: TypedExprKind::Call { + callee: Box::new(typed_callee), + function: function_id, + args: typed_args, + }, + ty: result_ty, + span, + } + } + + fn resolve_identifier( + &mut self, + name: &str, + span: Span, + scopes: &mut Vec, + ) -> TypedExpr { + if let Some(binding) = self.lookup_local(scopes, name) { + return TypedExpr { + kind: TypedExprKind::Local(binding.id, binding.name.clone()), + ty: binding.ty.clone(), + span, + }; + } + + if let Some(function_id) = self.function_names.get(name).copied() { + return TypedExpr { + kind: TypedExprKind::Function(function_id, name.to_string()), + ty: Type::Function(function_id), + span, + }; + } + + self.error(format!("unknown identifier `{name}`"), span); + TypedExpr { + kind: TypedExprKind::Error, + ty: Type::Error, + span, + } + } + + fn binary_result_type(&mut self, op: BinaryOp, left: &Type, right: &Type, span: Span) -> Type { + if matches!(left, Type::Error) || matches!(right, Type::Error) { + return Type::Error; + } + + match op { + BinaryOp::Add + | BinaryOp::Subtract + | BinaryOp::Multiply + | BinaryOp::Divide + | BinaryOp::Remainder => { + if left.is_numeric() && right.is_numeric() { + if matches!(left, Type::Float) || matches!(right, Type::Float) { + Type::Float + } else { + Type::Int + } + } else { + self.error( + format!( + "operator `{}` expects numeric operands, found `{}` and `{}`", + binary_op_name(op), + left.display_name(), + right.display_name() + ), + span, + ); + Type::Error + } + } + BinaryOp::LogicalAnd | BinaryOp::LogicalOr => { + if matches!(left, Type::Bool) && matches!(right, Type::Bool) { + Type::Bool + } else { + self.error( + format!( + "operator `{}` expects `Bool` operands, found `{}` and `{}`", + binary_op_name(op), + left.display_name(), + right.display_name() + ), + span, + ); + Type::Error + } + } + BinaryOp::Less | BinaryOp::LessEqual | BinaryOp::Greater | BinaryOp::GreaterEqual => { + if left.is_numeric() && right.is_numeric() { + Type::Bool + } else { + self.error( + format!( + "operator `{}` expects comparable numeric operands, found `{}` and `{}`", + binary_op_name(op), + left.display_name(), + right.display_name() + ), + span, + ); + Type::Error + } + } + BinaryOp::Equal | BinaryOp::NotEqual => { + if self.equality_compatible(left, right) { + Type::Bool + } else { + self.error( + format!( + "operator `{}` expects compatible operands, found `{}` and `{}`", + binary_op_name(op), + left.display_name(), + right.display_name() + ), + span, + ); + Type::Error + } + } + } + } + + fn equality_compatible(&self, left: &Type, right: &Type) -> bool { + match (left, right) { + (Type::Int, Type::Int) + | (Type::Float, Type::Float) + | (Type::Int, Type::Float) + | (Type::Float, Type::Int) + | (Type::Bool, Type::Bool) + | (Type::String, Type::String) + | (Type::Void, Type::Void) + | (Type::Error, _) + | (_, Type::Error) => true, + (Type::Struct(left_name), Type::Struct(right_name)) => left_name == right_name, + _ => false, + } + } + + fn assignment_compatible(&self, expected: &Type, actual: &Type) -> bool { + matches!(expected, Type::Error) + || matches!(actual, Type::Error) + || expected == actual + } + + fn resolve_type_ref(&mut self, type_ref: &TypeRef, context: TypeContext) -> Type { + match type_ref.name.as_str() { + "Int" => Type::Int, + "Float" => Type::Float, + "Bool" => Type::Bool, + "String" => Type::String, + "Void" => Type::Void, + name if self.struct_names.contains_key(name) => Type::Struct(name.to_string()), + _ => { + self.error( + format!( + "unknown {} type `{}`", + context.description(), + type_ref.name + ), + type_ref.span, + ); + Type::Error + } + } + } + + fn declare_local(&mut self, scopes: &mut Vec, binding: LocalBinding) -> bool { + let current_scope = scopes.last_mut().expect("at least one scope"); + if current_scope.bindings.contains_key(&binding.name) { + false + } else { + current_scope.bindings.insert(binding.name.clone(), binding); + true + } + } + + fn lookup_local(&self, scopes: &[Scope], name: &str) -> Option { + scopes + .iter() + .rev() + .find_map(|scope| scope.bindings.get(name).cloned()) + } + + fn next_local(&mut self) -> LocalId { + let id = LocalId(self.next_local_id); + self.next_local_id += 1; + id + } + + fn error(&mut self, message: impl Into, span: Span) { + self.diagnostics.push(Diagnostic::error(message, span)); + } +} + +fn typed_guarantees_return(statement: &TypedStmt) -> bool { + match &statement.kind { + TypedStmtKind::Return(_) => true, + TypedStmtKind::If(if_stmt) => { + if_stmt.then_block.guarantees_return + && if_stmt + .else_block + .as_ref() + .is_some_and(|block| block.guarantees_return) + } + TypedStmtKind::Let { .. } | TypedStmtKind::Expr(_) => false, + } +} + +fn binary_op_name(op: BinaryOp) -> &'static str { + match op { + BinaryOp::Multiply => "*", + BinaryOp::Divide => "/", + BinaryOp::Remainder => "%", + BinaryOp::Add => "+", + BinaryOp::Subtract => "-", + BinaryOp::Equal => "==", + BinaryOp::NotEqual => "!=", + BinaryOp::Less => "<", + BinaryOp::LessEqual => "<=", + BinaryOp::Greater => ">", + BinaryOp::GreaterEqual => ">=", + BinaryOp::LogicalAnd => "&&", + BinaryOp::LogicalOr => "||", + } +} + +#[derive(Debug, Clone, Copy)] +enum TypeContext { + Parameter, + Return, + Field, + Local, +} + +impl TypeContext { + fn description(self) -> &'static str { + match self { + TypeContext::Parameter => "parameter", + TypeContext::Return => "return", + TypeContext::Field => "field", + TypeContext::Local => "binding", + } + } +} diff --git a/crates/nxc-frontend/tests/semantic_tests.rs b/crates/nxc-frontend/tests/semantic_tests.rs new file mode 100644 index 0000000..4322d30 --- /dev/null +++ b/crates/nxc-frontend/tests/semantic_tests.rs @@ -0,0 +1,223 @@ +use nxc_frontend::{analyze, Lexer, Parser}; + +fn analyze_source(source: &str) -> nxc_frontend::SemanticResult { + let lexed = Lexer::new(source).lex(); + assert!( + lexed.diagnostics.is_empty(), + "unexpected lexer diagnostics: {:?}", + lexed.diagnostics + ); + + let parsed = Parser::new(lexed.tokens).parse_module(); + assert!( + parsed.diagnostics.is_empty(), + "unexpected parser diagnostics: {:?}", + parsed.diagnostics + ); + + analyze(&parsed.module) +} + +fn messages(result: &nxc_frontend::SemanticResult) -> Vec { + result + .diagnostics + .iter() + .map(|diagnostic| diagnostic.message.clone()) + .collect() +} + +#[test] +fn rejects_duplicate_function_names() { + let result = analyze_source( + "\ +fn main() -> Int: + return 0 + +fn main() -> Int: + return 1 +", + ); + + assert!(messages(&result).iter().any(|msg| msg.contains("duplicate function `main`"))); +} + +#[test] +fn rejects_duplicate_parameter_names() { + let result = analyze_source( + "\ +fn main(value: Int, value: Int) -> Int: + return value +", + ); + + assert!(messages(&result) + .iter() + .any(|msg| msg.contains("duplicate parameter `value`"))); +} + +#[test] +fn rejects_duplicate_local_bindings_in_same_scope() { + let result = analyze_source( + "\ +fn main() -> Int: + let value = 1 + let value = 2 + return value +", + ); + + assert!(messages(&result) + .iter() + .any(|msg| msg.contains("duplicate binding `value`"))); +} + +#[test] +fn rejects_unknown_identifiers() { + let result = analyze_source( + "\ +fn main() -> Int: + return missing +", + ); + + assert!(messages(&result) + .iter() + .any(|msg| msg.contains("unknown identifier `missing`"))); +} + +#[test] +fn rejects_unknown_function_calls() { + let result = analyze_source( + "\ +fn main() -> Int: + return missing(1) +", + ); + + assert!(messages(&result) + .iter() + .any(|msg| msg.contains("unknown function `missing`"))); +} + +#[test] +fn rejects_invalid_unary_operator_types() { + let result = analyze_source( + "\ +fn main() -> Int: + return -true +", + ); + + assert!(messages(&result) + .iter() + .any(|msg| msg.contains("unary `-` expects `Int` or `Float`"))); +} + +#[test] +fn rejects_invalid_binary_operator_types() { + let result = analyze_source( + "\ +fn main() -> Int: + return true + 1 +", + ); + + assert!(messages(&result) + .iter() + .any(|msg| msg.contains("operator `+` expects numeric operands"))); +} + +#[test] +fn rejects_invalid_if_condition_types() { + let result = analyze_source( + "\ +fn main() -> Int: + if 1: + return 0 + else: + return 1 +", + ); + + assert!(messages(&result) + .iter() + .any(|msg| msg.contains("if condition must be `Bool`"))); +} + +#[test] +fn rejects_wrong_call_arity() { + let result = analyze_source( + "\ +fn add(a: Int, b: Int) -> Int: + return a + b + +fn main() -> Int: + return add(1) +", + ); + + assert!(messages(&result) + .iter() + .any(|msg| msg.contains("expects 2 argument(s), found 1"))); +} + +#[test] +fn rejects_wrong_call_argument_types() { + let result = analyze_source( + "\ +fn negate(value: Int) -> Int: + return -value + +fn main() -> Int: + return negate(true) +", + ); + + assert!(messages(&result) + .iter() + .any(|msg| msg.contains("parameter `value` expects `Int`, found `Bool`"))); +} + +#[test] +fn rejects_return_type_mismatch() { + let result = analyze_source( + "\ +fn main() -> Int: + return true +", + ); + + assert!(messages(&result) + .iter() + .any(|msg| msg.contains("return type mismatch: expected `Int`, found `Bool`"))); +} + +#[test] +fn rejects_missing_return_in_non_void_function() { + let result = analyze_source( + "\ +fn main() -> Int: + let value = 1 +", + ); + + assert!(messages(&result) + .iter() + .any(|msg| msg.contains("may exit without returning `Int`"))); +} + +#[test] +fn allows_shadowing_in_nested_scopes() { + let result = analyze_source( + "\ +fn main(value: Int) -> Int: + if true: + let value = 2 + return value + else: + return value +", + ); + + assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics); +}