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

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