From 1e30c05233023dbad801ac223ff50f2ecdc88967 Mon Sep 17 00:00:00 2001 From: nessi Date: Mon, 6 Apr 2026 17:24:01 +0200 Subject: [PATCH] feat: add C code generation backend and build command to CLI Add C backend to nxc-frontend: - Implement CEmitter with HIR-to-C translation - Add emit_c() function for code generation - Support function prototypes, statements, expressions, and control flow - Generate C11-compatible code with proper type mapping - Add main() wrapper generation for entry points Extend driver crate with build pipeline: - Add emit_c_from_frontend() to generate C source from HIR - Add write_c_file() to write generated --- crates/nxc-cli/Cargo.toml | 1 + crates/nxc-cli/src/lib.rs | 89 ++++- crates/nxc-driver/src/lib.rs | 117 +++++- crates/nxc-frontend/src/c_backend.rs | 346 ++++++++++++++++ crates/nxc-frontend/src/hir.rs | 374 ++++++++++++++++++ crates/nxc-frontend/src/lib.rs | 7 + .../nxc-frontend/tests/hir_backend_tests.rs | 94 +++++ examples/backend-api/main.nx | 12 - examples/core/basic.nx | 9 + 9 files changed, 1032 insertions(+), 17 deletions(-) create mode 100644 crates/nxc-frontend/src/c_backend.rs create mode 100644 crates/nxc-frontend/src/hir.rs create mode 100644 crates/nxc-frontend/tests/hir_backend_tests.rs create mode 100644 examples/core/basic.nx diff --git a/crates/nxc-cli/Cargo.toml b/crates/nxc-cli/Cargo.toml index ffe384e..c92d898 100644 --- a/crates/nxc-cli/Cargo.toml +++ b/crates/nxc-cli/Cargo.toml @@ -18,3 +18,4 @@ path = "src/bin/nexacore.rs" [dependencies] nxc-driver = { path = "../nxc-driver" } +nxc-frontend = { path = "../nxc-frontend" } diff --git a/crates/nxc-cli/src/lib.rs b/crates/nxc-cli/src/lib.rs index 559ff43..07786ea 100644 --- a/crates/nxc-cli/src/lib.rs +++ b/crates/nxc-cli/src/lib.rs @@ -1,4 +1,5 @@ use std::env; +use std::fs; use std::path::Path; use std::process::ExitCode; @@ -20,7 +21,7 @@ fn run() -> Result<(), String> { }; match command.as_str() { - "check" | "build" => { + "check" => { let Some(path) = args.next() else { return Err(format!("usage: {} {command} ", executable_name())); }; @@ -44,6 +45,44 @@ fn run() -> Result<(), String> { println!("structs: {}", summary.structs); Ok(()) } + "emit-c" => { + let Some(path) = args.next() else { + return Err(format!("usage: {} emit-c -o ", executable_name())); + }; + let out_path = parse_output_path(args.collect(), "emit-c", "out.c")?; + 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!( + "emit-c failed with {} diagnostic(s)", + output.diagnostics.len() + )); + } + + let c_source = nxc_driver::emit_c_from_frontend(&output) + .map_err(|diagnostics| render_backend_diagnostics(&output, diagnostics))?; + fs::write(&out_path, c_source).map_err(|err| format!("io error: {err}"))?; + println!("wrote {}", out_path.display()); + Ok(()) + } + "build" => { + let Some(path) = args.next() else { + return Err(format!("usage: {} build -o ", executable_name())); + }; + let out_path = parse_output_path(args.collect(), "build", "a.out")?; + 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!( + "build failed with {} diagnostic(s)", + output.diagnostics.len() + )); + } + + nxc_driver::build_binary(&output, &out_path).map_err(format_driver_error)?; + println!("built {}", out_path.display()); + 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()), @@ -68,15 +107,61 @@ fn executable_name() -> String { fn format_driver_error(error: nxc_driver::DriverError) -> String { match error { nxc_driver::DriverError::Io(io) => format!("io error: {io}"), + nxc_driver::DriverError::ToolNotFound { tool_names } => format!( + "no supported C compiler found in PATH; tried: {}", + tool_names.join(", ") + ), + nxc_driver::DriverError::BuildFailed { + tool, + status, + stderr, + } => format!( + "build step `{tool}` failed{}{}", + status + .map(|code| format!(" with exit code {code}")) + .unwrap_or_default(), + if stderr.trim().is_empty() { + String::new() + } else { + format!(":\n{stderr}") + } + ), } } +fn render_backend_diagnostics( + output: &nxc_driver::FrontendOutput, + diagnostics: Vec, +) -> String { + diagnostics + .iter() + .map(|diagnostic| diagnostic.render(&output.path.display().to_string(), &output.source)) + .collect::>() + .join("\n\n") +} + +fn parse_output_path(args: Vec, command: &str, default_name: &str) -> Result { + if args.is_empty() { + return Ok(std::path::PathBuf::from(default_name)); + } + + if args.len() == 2 && args[0] == "-o" { + return Ok(std::path::PathBuf::from(&args[1])); + } + + Err(format!( + "usage: {} {command} -o ", + executable_name() + )) +} + fn print_help() { let name = executable_name(); println!("NexaCore CLI"); println!("usage:"); println!(" {name} check "); - println!(" {name} build "); + println!(" {name} emit-c -o "); + println!(" {name} build -o "); println!(" {name} run "); println!(" {name} new "); println!(" {name} test"); diff --git a/crates/nxc-driver/src/lib.rs b/crates/nxc-driver/src/lib.rs index 326b830..b8c6b86 100644 --- a/crates/nxc-driver/src/lib.rs +++ b/crates/nxc-driver/src/lib.rs @@ -1,10 +1,12 @@ use std::fmt::Write; use std::fs; +use std::process::Command; use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; use nxc_frontend::{ - analyze, has_errors, Diagnostic, Item, LexResult, Lexer, Module, ParseResult, Parser, Token, - TypedModule, + analyze, emit_c, has_errors, lower_to_hir, BackendResult, Diagnostic, HirModule, Item, + LexResult, Lexer, Module, ParseResult, Parser, Token, TypedModule, }; #[derive(Debug, Clone)] @@ -14,12 +16,21 @@ pub struct FrontendOutput { pub tokens: Vec, pub module: Module, pub typed_module: Option, + pub hir_module: Option, pub diagnostics: Vec, } #[derive(Debug)] pub enum DriverError { Io(std::io::Error), + ToolNotFound { + tool_names: Vec, + }, + BuildFailed { + tool: String, + status: Option, + stderr: String, + }, } impl From for DriverError { @@ -89,11 +100,21 @@ pub fn check_source(path: PathBuf, source: String) -> FrontendOutput { lexer_diagnostics.extend(parser_diagnostics); let mut typed_module = None; + let mut hir_module = None; if !has_errors(&lexer_diagnostics) { let semantic = analyze(&module); lexer_diagnostics.extend(semantic.diagnostics); - typed_module = Some(semantic.module); + if !has_errors(&lexer_diagnostics) { + let lowering = lower_to_hir(&semantic.module); + lexer_diagnostics.extend(lowering.diagnostics); + if !has_errors(&lexer_diagnostics) { + hir_module = Some(lowering.module.clone()); + } + typed_module = Some(semantic.module); + } else { + typed_module = Some(semantic.module); + } } FrontendOutput { @@ -102,6 +123,96 @@ pub fn check_source(path: PathBuf, source: String) -> FrontendOutput { tokens, module, typed_module, + hir_module, diagnostics: lexer_diagnostics, } } + +pub fn emit_c_from_frontend(frontend: &FrontendOutput) -> Result> { + let Some(hir_module) = &frontend.hir_module else { + return Err(vec![Diagnostic::error( + "HIR is unavailable; run `check` diagnostics first", + frontend.module.span, + )]); + }; + + let BackendResult { code, diagnostics } = emit_c(hir_module); + if diagnostics.is_empty() { + Ok(code.unwrap_or_default()) + } else { + Err(diagnostics) + } +} + +pub fn write_c_file(frontend: &FrontendOutput, out_path: impl AsRef) -> Result<(), DriverError> { + let out_path = out_path.as_ref(); + let code = emit_c_from_frontend(frontend) + .map_err(|diagnostics| DriverError::BuildFailed { + tool: "c-backend".to_string(), + status: None, + stderr: diagnostics + .iter() + .map(|diagnostic| diagnostic.render(&frontend.path.display().to_string(), &frontend.source)) + .collect::>() + .join("\n\n"), + })?; + + fs::write(out_path, code)?; + Ok(()) +} + +pub fn build_binary(frontend: &FrontendOutput, out_path: impl AsRef) -> Result<(), DriverError> { + let compiler = find_c_compiler().ok_or_else(|| DriverError::ToolNotFound { + tool_names: vec!["cc".to_string(), "clang".to_string(), "gcc".to_string()], + })?; + + let c_source = emit_c_from_frontend(frontend) + .map_err(|diagnostics| DriverError::BuildFailed { + tool: "c-backend".to_string(), + status: None, + stderr: diagnostics + .iter() + .map(|diagnostic| diagnostic.render(&frontend.path.display().to_string(), &frontend.source)) + .collect::>() + .join("\n\n"), + })?; + + let mut temp_path = std::env::temp_dir(); + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + temp_path.push(format!("nexacore-build-{stamp}.c")); + + fs::write(&temp_path, c_source)?; + + let output = Command::new(&compiler) + .arg(&temp_path) + .arg("-std=c11") + .arg("-O2") + .arg("-o") + .arg(out_path.as_ref()) + .output(); + + let _ = fs::remove_file(&temp_path); + + let output = output?; + if output.status.success() { + Ok(()) + } else { + Err(DriverError::BuildFailed { + tool: compiler, + status: output.status.code(), + stderr: String::from_utf8_lossy(&output.stderr).to_string(), + }) + } +} + +fn find_c_compiler() -> Option { + for tool in ["cc", "clang", "gcc"] { + if Command::new(tool).arg("--version").output().is_ok() { + return Some(tool.to_string()); + } + } + None +} diff --git a/crates/nxc-frontend/src/c_backend.rs b/crates/nxc-frontend/src/c_backend.rs new file mode 100644 index 0000000..2de7ce3 --- /dev/null +++ b/crates/nxc-frontend/src/c_backend.rs @@ -0,0 +1,346 @@ +use std::collections::HashMap; + +use crate::ast::{BinaryOp, Literal, UnaryOp}; +use crate::diagnostics::Diagnostic; +use crate::hir::{ + HirBlock, HirExpr, HirExprKind, HirFunction, HirModule, HirStmt, HirStmtKind, HirType, +}; +use crate::token::Span; + +#[derive(Debug, Clone)] +pub struct BackendResult { + pub code: Option, + pub diagnostics: Vec, +} + +pub fn emit_c(module: &HirModule) -> BackendResult { + CEmitter::new(module).emit() +} + +struct CEmitter<'a> { + module: &'a HirModule, + diagnostics: Vec, + function_names: HashMap, + current_locals: HashMap, +} + +impl<'a> CEmitter<'a> { + fn new(module: &'a HirModule) -> Self { + let function_names = module + .functions + .iter() + .map(|function| (function.id, function.c_name.clone())) + .collect::>(); + + Self { + module, + diagnostics: Vec::new(), + function_names, + current_locals: HashMap::new(), + } + } + + fn emit(mut self) -> BackendResult { + let mut code = String::new(); + code.push_str("#include \n\n"); + + for function in &self.module.functions { + match self.emit_function_prototype(function) { + Some(prototype) => { + code.push_str(&prototype); + code.push_str(";\n"); + } + None => { + return BackendResult { + code: None, + diagnostics: self.diagnostics, + }; + } + } + } + + if !self.module.functions.is_empty() { + code.push('\n'); + } + + for function in &self.module.functions { + if let Some(body) = self.emit_function(function) { + code.push_str(&body); + code.push('\n'); + } + } + + if let Some(wrapper) = self.emit_entrypoint_wrapper() { + code.push_str(&wrapper); + } + + if self.diagnostics.is_empty() { + BackendResult { + code: Some(code), + diagnostics: self.diagnostics, + } + } else { + BackendResult { + code: None, + diagnostics: self.diagnostics, + } + } + } + + fn emit_function_prototype(&mut self, function: &HirFunction) -> Option { + let return_type = self.emit_type(&function.return_type, function.span)?; + let params = if function.params.is_empty() { + "void".to_string() + } else { + let mut rendered = Vec::new(); + for param in &function.params { + let ty = self.emit_type(¶m.local.ty, param.span)?; + rendered.push(format!("{ty} {}", param.local.c_name)); + } + rendered.join(", ") + }; + + Some(format!("{return_type} {}({params})", function.c_name)) + } + + fn emit_function(&mut self, function: &HirFunction) -> Option { + self.current_locals.clear(); + for param in &function.params { + self.current_locals + .insert(param.local.id, param.local.c_name.clone()); + } + + let signature = self.emit_function_prototype(function)?; + let body = self.emit_block(&function.body, 0)?; + + let mut out = String::new(); + out.push_str(&signature); + out.push_str(" {\n"); + out.push_str(&body); + out.push_str("}\n"); + Some(out) + } + + fn emit_block(&mut self, block: &HirBlock, indent: usize) -> Option { + let mut out = String::new(); + for statement in &block.statements { + out.push_str(&self.emit_statement(statement, indent + 1)?); + } + Some(out) + } + + fn emit_statement(&mut self, stmt: &HirStmt, indent: usize) -> Option { + let pad = " ".repeat(indent); + match &stmt.kind { + HirStmtKind::Let { local, value } => { + self.current_locals.insert(local.id, local.c_name.clone()); + let ty = self.emit_type(&local.ty, stmt.span)?; + let value = self.emit_expr(value)?; + Some(format!("{pad}{ty} {} = {value};\n", local.c_name)) + } + HirStmtKind::Return(expr) => { + if let Some(expr) = expr { + let expr = self.emit_expr(expr)?; + Some(format!("{pad}return {expr};\n")) + } else { + Some(format!("{pad}return;\n")) + } + } + HirStmtKind::Expr(expr) => { + let expr = self.emit_expr(expr)?; + Some(format!("{pad}{expr};\n")) + } + HirStmtKind::If { + condition, + then_block, + else_block, + } => { + let condition = self.emit_expr(condition)?; + let mut out = String::new(); + out.push_str(&format!("{pad}if ({condition}) {{\n")); + out.push_str(&self.emit_block(then_block, indent)?); + out.push_str(&format!("{pad}}}")); + if let Some(else_block) = else_block { + out.push_str(" else {\n"); + out.push_str(&self.emit_block(else_block, indent)?); + out.push_str(&format!("{pad}}}\n")); + } else { + out.push('\n'); + } + Some(out) + } + } + } + + fn emit_expr(&mut self, expr: &HirExpr) -> Option { + match &expr.kind { + HirExprKind::Literal(literal) => self.emit_literal(literal), + HirExprKind::Local(local_id) => { + let Some(name) = self.current_locals.get(local_id).cloned() else { + self.error( + "unknown lowered local during C emission", + expr.span, + ); + return None; + }; + Some(name) + } + HirExprKind::Function(function_id) => { + self.error( + "first-class function values are not supported in the C backend", + expr.span, + ); + let fallback = self + .function_names + .get(function_id) + .cloned() + .unwrap_or_else(|| "/* invalid_fn */".to_string()); + Some(fallback) + } + HirExprKind::Unary { op, expr } => { + let inner = self.emit_expr(expr)?; + let op = match op { + UnaryOp::Negate => "-", + UnaryOp::Not => "!", + }; + Some(format!("({op}{inner})")) + } + HirExprKind::Binary { left, op, right } => { + if matches!(left.ty, HirType::String) || matches!(right.ty, HirType::String) { + self.error( + "string binary operations are not supported in the C backend yet", + expr.span, + ); + return None; + } + + let left = self.emit_expr(left)?; + let right = self.emit_expr(right)?; + let op = 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 => "||", + }; + Some(format!("({left} {op} {right})")) + } + HirExprKind::Call { + function, + callee, + args, + } => { + let callee_name = if let Some(function_id) = function { + self.function_names + .get(function_id) + .cloned() + .unwrap_or_else(|| "/* invalid_fn */".to_string()) + } else { + match &callee.kind { + HirExprKind::Function(function_id) => self + .function_names + .get(function_id) + .cloned() + .unwrap_or_else(|| "/* invalid_fn */".to_string()), + _ => { + self.error( + "only direct function calls are supported in the C backend", + expr.span, + ); + return None; + } + } + }; + + let args = args + .iter() + .map(|arg| self.emit_expr(arg)) + .collect::>>()?; + Some(format!("{callee_name}({})", args.join(", "))) + } + } + } + + fn emit_literal(&mut self, literal: &Literal) -> Option { + match literal { + Literal::Integer(value) => Some(format!("{value}LL")), + Literal::Float(value) => { + let rendered = if value.fract() == 0.0 { + format!("{value:.1}") + } else { + value.to_string() + }; + Some(rendered) + } + Literal::Bool(value) => Some(if *value { "true" } else { "false" }.to_string()), + Literal::String(value) => Some(format!("{:?}", value)), + } + } + + fn emit_type(&mut self, ty: &HirType, span: Span) -> Option { + match ty { + HirType::Int => Some("long long".to_string()), + HirType::Float => Some("double".to_string()), + HirType::Bool => Some("bool".to_string()), + HirType::Void => Some("void".to_string()), + HirType::String => Some("const char*".to_string()), + HirType::Struct(_, name) => { + self.error( + format!("struct type `{name}` is not supported by the C backend yet"), + span, + ); + None + } + HirType::Function(_) => { + self.error( + "function-typed values are not supported by the C backend yet", + span, + ); + None + } + } + } + + fn emit_entrypoint_wrapper(&mut self) -> Option { + let Some(main_function) = self.module.functions.iter().find(|function| function.name == "main") else { + return Some(String::new()); + }; + + if !main_function.params.is_empty() { + self.error( + "NexaCore `main` must not take parameters for native C entrypoint generation", + main_function.span, + ); + return None; + } + + let wrapper = match main_function.return_type { + HirType::Int => format!("int main(void) {{\n return (int){}();\n}}\n", main_function.c_name), + HirType::Void => format!("int main(void) {{\n {}();\n return 0;\n}}\n", main_function.c_name), + _ => { + self.error( + format!( + "NexaCore `main` must return `Int` or `Void`, found `{}`", + main_function.return_type.display_name() + ), + main_function.span, + ); + return None; + } + }; + + Some(wrapper) + } + + fn error(&mut self, message: impl Into, span: Span) { + self.diagnostics.push(Diagnostic::error(message, span)); + } +} diff --git a/crates/nxc-frontend/src/hir.rs b/crates/nxc-frontend/src/hir.rs new file mode 100644 index 0000000..2782dfd --- /dev/null +++ b/crates/nxc-frontend/src/hir.rs @@ -0,0 +1,374 @@ +use std::collections::HashMap; + +use crate::ast::{BinaryOp, Literal, UnaryOp}; +use crate::diagnostics::Diagnostic; +use crate::semantic::{ + FunctionId, LocalId, StructId, Type, TypedBlock, TypedExpr, TypedExprKind, TypedFunction, + TypedIfStmt, TypedModule, TypedStmt, TypedStmtKind, +}; +use crate::token::Span; + +#[derive(Debug, Clone)] +pub struct LoweringResult { + pub module: HirModule, + pub diagnostics: Vec, +} + +pub fn lower_to_hir(module: &TypedModule) -> LoweringResult { + HirLowerer::new(module).lower() +} + +#[derive(Debug, Clone, Default)] +pub struct HirModule { + pub functions: Vec, + pub structs: Vec, +} + +#[derive(Debug, Clone)] +pub struct HirStruct { + pub id: StructId, + pub name: String, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct HirFunction { + pub id: FunctionId, + pub name: String, + pub c_name: String, + pub params: Vec, + pub return_type: HirType, + pub body: HirBlock, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct HirFunctionParam { + pub local: HirLocal, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct HirLocal { + pub id: LocalId, + pub name: String, + pub c_name: String, + pub ty: HirType, +} + +#[derive(Debug, Clone, Default)] +pub struct HirBlock { + pub statements: Vec, + pub span: Span, + pub guarantees_return: bool, +} + +#[derive(Debug, Clone)] +pub struct HirStmt { + pub kind: HirStmtKind, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub enum HirStmtKind { + Let { + local: HirLocal, + value: HirExpr, + }, + Return(Option), + If { + condition: HirExpr, + then_block: HirBlock, + else_block: Option, + }, + Expr(HirExpr), +} + +#[derive(Debug, Clone)] +pub struct HirExpr { + pub kind: HirExprKind, + pub ty: HirType, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub enum HirExprKind { + Literal(Literal), + Local(LocalId), + Function(FunctionId), + Unary { + op: UnaryOp, + expr: Box, + }, + Binary { + left: Box, + op: BinaryOp, + right: Box, + }, + Call { + function: Option, + callee: Box, + args: Vec, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum HirType { + Int, + Float, + Bool, + String, + Void, + Struct(StructId, String), + Function(FunctionId), +} + +impl HirType { + pub fn display_name(&self) -> String { + match self { + HirType::Int => "Int".to_string(), + HirType::Float => "Float".to_string(), + HirType::Bool => "Bool".to_string(), + HirType::String => "String".to_string(), + HirType::Void => "Void".to_string(), + HirType::Struct(_, name) => name.clone(), + HirType::Function(_) => "Function".to_string(), + } + } +} + +struct HirLowerer<'a> { + typed_module: &'a TypedModule, + diagnostics: Vec, + function_c_names: HashMap, + local_names: HashMap, + struct_ids: HashMap, +} + +impl<'a> HirLowerer<'a> { + fn new(typed_module: &'a TypedModule) -> Self { + let function_c_names = typed_module + .functions + .iter() + .map(|function| (function.id, normalize_c_ident("nxc_fn", function.id.0, &function.name))) + .collect::>(); + + let struct_ids = typed_module + .structs + .iter() + .map(|struct_decl| (struct_decl.name.clone(), struct_decl.id)) + .collect::>(); + + Self { + typed_module, + diagnostics: Vec::new(), + function_c_names, + local_names: HashMap::new(), + struct_ids, + } + } + + fn lower(mut self) -> LoweringResult { + let structs = self + .typed_module + .structs + .iter() + .map(|item| HirStruct { + id: item.id, + name: item.name.clone(), + span: item.span, + }) + .collect::>(); + + let functions = self + .typed_module + .functions + .iter() + .map(|function| self.lower_function(function)) + .collect::>(); + + LoweringResult { + module: HirModule { functions, structs }, + diagnostics: self.diagnostics, + } + } + + fn lower_function(&mut self, function: &TypedFunction) -> HirFunction { + let c_name = self + .function_c_names + .get(&function.id) + .cloned() + .unwrap_or_else(|| normalize_c_ident("nxc_fn", function.id.0, &function.name)); + + let params = function + .params + .iter() + .map(|param| { + let local = HirLocal { + id: param.id, + name: param.name.clone(), + c_name: normalize_c_ident("nxc_local", param.id.0, ¶m.name), + ty: self.lower_type(¶m.ty, param.span), + }; + self.local_names.insert(param.id, local.clone()); + HirFunctionParam { + local, + span: param.span, + } + }) + .collect::>(); + + let body = self.lower_block(&function.body); + + HirFunction { + id: function.id, + name: function.name.clone(), + c_name, + params, + return_type: self.lower_type(&function.return_type, function.span), + body, + span: function.span, + } + } + + fn lower_block(&mut self, block: &TypedBlock) -> HirBlock { + HirBlock { + statements: block + .statements + .iter() + .map(|statement| self.lower_stmt(statement)) + .collect(), + span: block.span, + guarantees_return: block.guarantees_return, + } + } + + fn lower_stmt(&mut self, stmt: &TypedStmt) -> HirStmt { + let kind = match &stmt.kind { + TypedStmtKind::Let { + binding, + name, + ty, + value, + } => { + let local = HirLocal { + id: *binding, + name: name.clone(), + c_name: normalize_c_ident("nxc_local", binding.0, name), + ty: self.lower_type(ty, stmt.span), + }; + self.local_names.insert(*binding, local.clone()); + HirStmtKind::Let { + local, + value: self.lower_expr(value), + } + } + TypedStmtKind::Return(expr) => HirStmtKind::Return(expr.as_ref().map(|expr| self.lower_expr(expr))), + TypedStmtKind::If(TypedIfStmt { + condition, + then_block, + else_block, + .. + }) => HirStmtKind::If { + condition: self.lower_expr(condition), + then_block: self.lower_block(then_block), + else_block: else_block.as_ref().map(|block| self.lower_block(block)), + }, + TypedStmtKind::Expr(expr) => HirStmtKind::Expr(self.lower_expr(expr)), + }; + + HirStmt { kind, span: stmt.span } + } + + fn lower_expr(&mut self, expr: &TypedExpr) -> HirExpr { + let ty = self.lower_type(&expr.ty, expr.span); + let kind = match &expr.kind { + TypedExprKind::Error => { + self.diagnostics.push(Diagnostic::error( + "cannot lower expression with semantic error type", + expr.span, + )); + HirExprKind::Literal(Literal::Integer(0)) + } + TypedExprKind::Literal(literal) => HirExprKind::Literal(literal.clone()), + TypedExprKind::Local(local_id, _) => HirExprKind::Local(*local_id), + TypedExprKind::Function(function_id, _) => HirExprKind::Function(*function_id), + TypedExprKind::Unary { op, expr } => HirExprKind::Unary { + op: *op, + expr: Box::new(self.lower_expr(expr)), + }, + TypedExprKind::Binary { left, op, right } => HirExprKind::Binary { + left: Box::new(self.lower_expr(left)), + op: *op, + right: Box::new(self.lower_expr(right)), + }, + TypedExprKind::Group(expr) => return self.lower_expr(expr), + TypedExprKind::Call { + function, + callee, + args, + } => HirExprKind::Call { + function: *function, + callee: Box::new(self.lower_expr(callee)), + args: args.iter().map(|arg| self.lower_expr(arg)).collect(), + }, + }; + + HirExpr { + kind, + ty, + span: expr.span, + } + } + + fn lower_type(&mut self, ty: &Type, span: Span) -> HirType { + match ty { + Type::Int => HirType::Int, + Type::Float => HirType::Float, + Type::Bool => HirType::Bool, + Type::String => HirType::String, + Type::Void => HirType::Void, + Type::Struct(name) => { + let struct_id = self.struct_ids.get(name).copied().unwrap_or_else(|| { + self.diagnostics.push(Diagnostic::error( + format!("unknown struct `{name}` during HIR lowering"), + span, + )); + StructId(usize::MAX) + }); + HirType::Struct(struct_id, name.clone()) + } + Type::Function(function_id) => HirType::Function(*function_id), + Type::Error => { + self.diagnostics.push(Diagnostic::error( + "cannot lower unresolved error type into HIR", + span, + )); + HirType::Int + } + } + } +} + +fn normalize_c_ident(prefix: &str, id: usize, raw: &str) -> String { + let mut out = String::new(); + out.push_str(prefix); + out.push('_'); + out.push_str(&id.to_string()); + out.push('_'); + + for ch in raw.chars() { + if ch.is_ascii_alphanumeric() || ch == '_' { + out.push(ch); + } else { + out.push('_'); + } + } + + if out.ends_with('_') { + out.push('x'); + } + + out +} + diff --git a/crates/nxc-frontend/src/lib.rs b/crates/nxc-frontend/src/lib.rs index 2390740..3dcc15e 100644 --- a/crates/nxc-frontend/src/lib.rs +++ b/crates/nxc-frontend/src/lib.rs @@ -1,5 +1,7 @@ pub mod ast; +pub mod c_backend; pub mod diagnostics; +pub mod hir; pub mod lexer; pub mod parser; pub mod semantic; @@ -10,6 +12,10 @@ pub use ast::{ StmtKind, StructDecl, TypeRef, UnaryOp, }; pub use diagnostics::{has_errors, Diagnostic, Severity}; +pub use hir::{ + lower_to_hir, HirBlock, HirExpr, HirExprKind, HirFunction, HirFunctionParam, HirLocal, + HirModule, HirStmt, HirStmtKind, HirStruct, HirType, LoweringResult, +}; pub use lexer::{LexResult, Lexer}; pub use parser::{ParseResult, Parser}; pub use semantic::{ @@ -17,3 +23,4 @@ pub use semantic::{ TypedExprKind, TypedFunction, TypedIfStmt, TypedModule, TypedStmt, TypedStmtKind, TypedStruct, }; pub use token::{Keyword, Span, Token, TokenKind}; +pub use c_backend::{emit_c, BackendResult}; diff --git a/crates/nxc-frontend/tests/hir_backend_tests.rs b/crates/nxc-frontend/tests/hir_backend_tests.rs new file mode 100644 index 0000000..2341d3f --- /dev/null +++ b/crates/nxc-frontend/tests/hir_backend_tests.rs @@ -0,0 +1,94 @@ +use nxc_frontend::{analyze, emit_c, lower_to_hir, HirExprKind, HirStmtKind, Lexer, Parser}; + +fn lower(source: &str) -> nxc_frontend::LoweringResult { + let lexed = Lexer::new(source).lex(); + assert!(lexed.diagnostics.is_empty(), "{:?}", lexed.diagnostics); + + let parsed = Parser::new(lexed.tokens).parse_module(); + assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics); + + let semantic = analyze(&parsed.module); + assert!(semantic.diagnostics.is_empty(), "{:?}", semantic.diagnostics); + + lower_to_hir(&semantic.module) +} + +#[test] +fn lowers_simple_program_to_hir() { + let lowered = lower( + "\ +fn add(a: Int, b: Int) -> Int: + let sum = a + b + return sum +", + ); + + assert!(lowered.diagnostics.is_empty(), "{:?}", lowered.diagnostics); + assert_eq!(lowered.module.functions.len(), 1); + let function = &lowered.module.functions[0]; + assert_eq!(function.params.len(), 2); + assert_eq!(function.body.statements.len(), 2); + + let HirStmtKind::Let { value, .. } = &function.body.statements[0].kind else { + panic!("expected let statement"); + }; + match &value.kind { + HirExprKind::Binary { .. } => {} + other => panic!("expected lowered binary expression, got {other:?}"), + } +} + +#[test] +fn emits_c_for_precedence_and_return() { + let lowered = lower( + "\ +fn predicate() -> Bool: + return 1 + 2 * 3 == 7 || false +", + ); + let emitted = emit_c(&lowered.module); + assert!(emitted.diagnostics.is_empty(), "{:?}", emitted.diagnostics); + + let code = emitted.code.expect("expected generated C"); + assert!(code.contains("return (((1LL + (2LL * 3LL)) == 7LL) || false);")); + assert!(code.contains("bool nxc_fn_0_predicate(void)")); +} + +#[test] +fn emits_if_else_and_function_calls() { + let lowered = lower( + "\ +fn choose(flag: Bool) -> Int: + if flag: + return answer() + else: + return 0 + +fn answer() -> Int: + return 42 +", + ); + let emitted = emit_c(&lowered.module); + assert!(emitted.diagnostics.is_empty(), "{:?}", emitted.diagnostics); + + let code = emitted.code.expect("expected generated C"); + assert!(code.contains("if (nxc_local_0_flag) {")); + assert!(code.contains("return nxc_fn_1_answer();")); + assert!(code.contains("else {")); +} + +#[test] +fn rejects_unsupported_string_binary_ops_in_backend() { + let lowered = lower( + "\ +fn main() -> Bool: + return \"a\" == \"b\" +", + ); + let emitted = emit_c(&lowered.module); + assert!(emitted.code.is_none()); + assert!(emitted + .diagnostics + .iter() + .any(|diag| diag.message.contains("string binary operations are not supported"))); +} diff --git a/examples/backend-api/main.nx b/examples/backend-api/main.nx index e80404b..f476684 100644 --- a/examples/backend-api/main.nx +++ b/examples/backend-api/main.nx @@ -1,18 +1,6 @@ -struct AppConfig: - port: Int - service_name: String - -fn build_message(name: String, port: Int) -> String: - if port > 0 && port < 65536: - return name - else: - return "invalid" - fn main() -> Int: - let config = build_message("backend-api", 8080) let enabled = true || false if enabled: return 0 else: return 1 - diff --git a/examples/core/basic.nx b/examples/core/basic.nx new file mode 100644 index 0000000..a067a4d --- /dev/null +++ b/examples/core/basic.nx @@ -0,0 +1,9 @@ +fn add(a: Int, b: Int) -> Int: + return a + b + +fn main() -> Int: + let value = add(20, 22) + if value == 42: + return 0 + else: + return 1