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]
|
[dependencies]
|
||||||
nxc-driver = { path = "../nxc-driver" }
|
nxc-driver = { path = "../nxc-driver" }
|
||||||
|
nxc-frontend = { path = "../nxc-frontend" }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ fn run() -> Result<(), String> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match command.as_str() {
|
match command.as_str() {
|
||||||
"check" | "build" => {
|
"check" => {
|
||||||
let Some(path) = args.next() else {
|
let Some(path) = args.next() else {
|
||||||
return Err(format!("usage: {} {command} <file.nx>", executable_name()));
|
return Err(format!("usage: {} {command} <file.nx>", executable_name()));
|
||||||
};
|
};
|
||||||
@@ -44,6 +45,44 @@ fn run() -> Result<(), String> {
|
|||||||
println!("structs: {}", summary.structs);
|
println!("structs: {}", summary.structs);
|
||||||
Ok(())
|
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()),
|
"run" => Err("runtime execution is not implemented yet".to_string()),
|
||||||
"new" => Err("project scaffolding 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()),
|
"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 {
|
fn format_driver_error(error: nxc_driver::DriverError) -> String {
|
||||||
match error {
|
match error {
|
||||||
nxc_driver::DriverError::Io(io) => format!("io error: {io}"),
|
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() {
|
fn print_help() {
|
||||||
let name = executable_name();
|
let name = executable_name();
|
||||||
println!("NexaCore CLI");
|
println!("NexaCore CLI");
|
||||||
println!("usage:");
|
println!("usage:");
|
||||||
println!(" {name} check <file.nx>");
|
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} run <file.nx>");
|
||||||
println!(" {name} new <name>");
|
println!(" {name} new <name>");
|
||||||
println!(" {name} test");
|
println!(" {name} test");
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::process::Command;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use nxc_frontend::{
|
use nxc_frontend::{
|
||||||
analyze, has_errors, Diagnostic, Item, LexResult, Lexer, Module, ParseResult, Parser, Token,
|
analyze, emit_c, has_errors, lower_to_hir, BackendResult, Diagnostic, HirModule, Item,
|
||||||
TypedModule,
|
LexResult, Lexer, Module, ParseResult, Parser, Token, TypedModule,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -14,12 +16,21 @@ pub struct FrontendOutput {
|
|||||||
pub tokens: Vec<Token>,
|
pub tokens: Vec<Token>,
|
||||||
pub module: Module,
|
pub module: Module,
|
||||||
pub typed_module: Option<TypedModule>,
|
pub typed_module: Option<TypedModule>,
|
||||||
|
pub hir_module: Option<HirModule>,
|
||||||
pub diagnostics: Vec<Diagnostic>,
|
pub diagnostics: Vec<Diagnostic>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum DriverError {
|
pub enum DriverError {
|
||||||
Io(std::io::Error),
|
Io(std::io::Error),
|
||||||
|
ToolNotFound {
|
||||||
|
tool_names: Vec<String>,
|
||||||
|
},
|
||||||
|
BuildFailed {
|
||||||
|
tool: String,
|
||||||
|
status: Option<i32>,
|
||||||
|
stderr: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<std::io::Error> for DriverError {
|
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);
|
lexer_diagnostics.extend(parser_diagnostics);
|
||||||
let mut typed_module = None;
|
let mut typed_module = None;
|
||||||
|
let mut hir_module = None;
|
||||||
|
|
||||||
if !has_errors(&lexer_diagnostics) {
|
if !has_errors(&lexer_diagnostics) {
|
||||||
let semantic = analyze(&module);
|
let semantic = analyze(&module);
|
||||||
lexer_diagnostics.extend(semantic.diagnostics);
|
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 {
|
FrontendOutput {
|
||||||
@@ -102,6 +123,96 @@ pub fn check_source(path: PathBuf, source: String) -> FrontendOutput {
|
|||||||
tokens,
|
tokens,
|
||||||
module,
|
module,
|
||||||
typed_module,
|
typed_module,
|
||||||
|
hir_module,
|
||||||
diagnostics: lexer_diagnostics,
|
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 ast;
|
||||||
|
pub mod c_backend;
|
||||||
pub mod diagnostics;
|
pub mod diagnostics;
|
||||||
|
pub mod hir;
|
||||||
pub mod lexer;
|
pub mod lexer;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
pub mod semantic;
|
pub mod semantic;
|
||||||
@@ -10,6 +12,10 @@ pub use ast::{
|
|||||||
StmtKind, StructDecl, TypeRef, UnaryOp,
|
StmtKind, StructDecl, TypeRef, UnaryOp,
|
||||||
};
|
};
|
||||||
pub use diagnostics::{has_errors, Diagnostic, Severity};
|
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 lexer::{LexResult, Lexer};
|
||||||
pub use parser::{ParseResult, Parser};
|
pub use parser::{ParseResult, Parser};
|
||||||
pub use semantic::{
|
pub use semantic::{
|
||||||
@@ -17,3 +23,4 @@ pub use semantic::{
|
|||||||
TypedExprKind, TypedFunction, TypedIfStmt, TypedModule, TypedStmt, TypedStmtKind, TypedStruct,
|
TypedExprKind, TypedFunction, TypedIfStmt, TypedModule, TypedStmt, TypedStmtKind, TypedStruct,
|
||||||
};
|
};
|
||||||
pub use token::{Keyword, Span, Token, TokenKind};
|
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")));
|
||||||
|
}
|
||||||
@@ -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:
|
fn main() -> Int:
|
||||||
let config = build_message("backend-api", 8080)
|
|
||||||
let enabled = true || false
|
let enabled = true || false
|
||||||
if enabled:
|
if enabled:
|
||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|||||||
9
examples/core/basic.nx
Normal file
9
examples/core/basic.nx
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user