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
This commit is contained in:
2026-04-06 17:24:01 +02:00
parent 9304b6bcaa
commit 1e30c05233
9 changed files with 1032 additions and 17 deletions

View File

@@ -18,3 +18,4 @@ path = "src/bin/nexacore.rs"
[dependencies]
nxc-driver = { path = "../nxc-driver" }
nxc-frontend = { path = "../nxc-frontend" }

View File

@@ -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} <file.nx>", 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 <file.nx> -o <out.c>", 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 <file.nx> -o <binary>", 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<nxc_frontend::Diagnostic>,
) -> String {
diagnostics
.iter()
.map(|diagnostic| diagnostic.render(&output.path.display().to_string(), &output.source))
.collect::<Vec<_>>()
.join("\n\n")
}
fn parse_output_path(args: Vec<String>, command: &str, default_name: &str) -> Result<std::path::PathBuf, String> {
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} <file.nx> -o <output>",
executable_name()
))
}
fn print_help() {
let name = executable_name();
println!("NexaCore CLI");
println!("usage:");
println!(" {name} check <file.nx>");
println!(" {name} build <file.nx>");
println!(" {name} emit-c <file.nx> -o <out.c>");
println!(" {name} build <file.nx> -o <binary>");
println!(" {name} run <file.nx>");
println!(" {name} new <name>");
println!(" {name} test");

View File

@@ -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<Token>,
pub module: Module,
pub typed_module: Option<TypedModule>,
pub hir_module: Option<HirModule>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Debug)]
pub enum DriverError {
Io(std::io::Error),
ToolNotFound {
tool_names: Vec<String>,
},
BuildFailed {
tool: String,
status: Option<i32>,
stderr: String,
},
}
impl From<std::io::Error> 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<String, Vec<Diagnostic>> {
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<Path>) -> 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::<Vec<_>>()
.join("\n\n"),
})?;
fs::write(out_path, code)?;
Ok(())
}
pub fn build_binary(frontend: &FrontendOutput, out_path: impl AsRef<Path>) -> 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::<Vec<_>>()
.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<String> {
for tool in ["cc", "clang", "gcc"] {
if Command::new(tool).arg("--version").output().is_ok() {
return Some(tool.to_string());
}
}
None
}

View File

@@ -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<String>,
pub diagnostics: Vec<Diagnostic>,
}
pub fn emit_c(module: &HirModule) -> BackendResult {
CEmitter::new(module).emit()
}
struct CEmitter<'a> {
module: &'a HirModule,
diagnostics: Vec<Diagnostic>,
function_names: HashMap<crate::semantic::FunctionId, String>,
current_locals: HashMap<crate::semantic::LocalId, String>,
}
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::<HashMap<_, _>>();
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 <stdbool.h>\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<String> {
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(&param.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<String> {
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<String> {
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<String> {
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<String> {
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::<Option<Vec<_>>>()?;
Some(format!("{callee_name}({})", args.join(", ")))
}
}
}
fn emit_literal(&mut self, literal: &Literal) -> Option<String> {
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<String> {
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<String> {
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<String>, span: Span) {
self.diagnostics.push(Diagnostic::error(message, span));
}
}

View File

@@ -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<Diagnostic>,
}
pub fn lower_to_hir(module: &TypedModule) -> LoweringResult {
HirLowerer::new(module).lower()
}
#[derive(Debug, Clone, Default)]
pub struct HirModule {
pub functions: Vec<HirFunction>,
pub structs: Vec<HirStruct>,
}
#[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<HirFunctionParam>,
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<HirStmt>,
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<HirExpr>),
If {
condition: HirExpr,
then_block: HirBlock,
else_block: Option<HirBlock>,
},
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<HirExpr>,
},
Binary {
left: Box<HirExpr>,
op: BinaryOp,
right: Box<HirExpr>,
},
Call {
function: Option<FunctionId>,
callee: Box<HirExpr>,
args: Vec<HirExpr>,
},
}
#[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<Diagnostic>,
function_c_names: HashMap<FunctionId, String>,
local_names: HashMap<LocalId, HirLocal>,
struct_ids: HashMap<String, StructId>,
}
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::<HashMap<_, _>>();
let struct_ids = typed_module
.structs
.iter()
.map(|struct_decl| (struct_decl.name.clone(), struct_decl.id))
.collect::<HashMap<_, _>>();
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::<Vec<_>>();
let functions = self
.typed_module
.functions
.iter()
.map(|function| self.lower_function(function))
.collect::<Vec<_>>();
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, &param.name),
ty: self.lower_type(&param.ty, param.span),
};
self.local_names.insert(param.id, local.clone());
HirFunctionParam {
local,
span: param.span,
}
})
.collect::<Vec<_>>();
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
}

View File

@@ -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};

View File

@@ -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")));
}