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:
@@ -18,3 +18,4 @@ path = "src/bin/nexacore.rs"
|
||||
|
||||
[dependencies]
|
||||
nxc-driver = { path = "../nxc-driver" }
|
||||
nxc-frontend = { path = "../nxc-frontend" }
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
346
crates/nxc-frontend/src/c_backend.rs
Normal file
346
crates/nxc-frontend/src/c_backend.rs
Normal 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(¶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<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));
|
||||
}
|
||||
}
|
||||
374
crates/nxc-frontend/src/hir.rs
Normal file
374
crates/nxc-frontend/src/hir.rs
Normal 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, ¶m.name),
|
||||
ty: self.lower_type(¶m.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
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
94
crates/nxc-frontend/tests/hir_backend_tests.rs
Normal file
94
crates/nxc-frontend/tests/hir_backend_tests.rs
Normal 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")));
|
||||
}
|
||||
Reference in New Issue
Block a user