Feat: add LLVM IR backend with opt/llc pipeline
Implements a full LLVM IR text emitter and three-step toolchain:
1. Emit LLVM IR (.ll) via alloca-based codegen (mem2reg-friendly)
2. `opt -O2` → optimised IR (override with FLUXC_OPT)
3. `llc -filetype=obj` → object file (override with FLUXC_LLC)
4. `cc` → link into executable (override with FLUXC_CC)
(step 4 skipped in -c mode)
Emitter supports all Flux types, operators, control flow (if/else,
while, loop, break, continue), structs, arrays, pointer operations,
function calls, string literals, and integer literal type inference
via UnboundInt → concrete-type coercion.
Also adds -o <file> CLI flag, exposes CheckResult from the checker
(sigma + phi tables reused by codegen), and updates main.rs to run
the full parse → check → codegen pipeline.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -42,14 +42,25 @@ pub enum Ty {
|
||||
Char,
|
||||
Unit,
|
||||
// Pointer types
|
||||
Ptr { mutable: bool, pointee: Box<Ty> },
|
||||
OpaquePtr { mutable: bool },
|
||||
Ptr {
|
||||
mutable: bool,
|
||||
pointee: Box<Ty>,
|
||||
},
|
||||
OpaquePtr {
|
||||
mutable: bool,
|
||||
},
|
||||
// Array type
|
||||
Array { elem: Box<Ty>, size: u64 },
|
||||
Array {
|
||||
elem: Box<Ty>,
|
||||
size: u64,
|
||||
},
|
||||
// User-defined struct
|
||||
Struct(String),
|
||||
// Internal function signature (not user-facing)
|
||||
FnSig { params: Vec<Ty>, ret: Box<Ty> },
|
||||
FnSig {
|
||||
params: Vec<Ty>,
|
||||
ret: Box<Ty>,
|
||||
},
|
||||
/// Unresolved integer type from a literal or an unannotated let-binding.
|
||||
/// Compatible with every concrete integer type; defaults to `i32` in
|
||||
/// error messages when no concrete type can be inferred.
|
||||
|
||||
@@ -10,6 +10,17 @@ use crate::diagnostics::{Diagnostic, Label};
|
||||
use crate::token::Span;
|
||||
use env::{FieldEntry, FuncTable, ParamEntry, StructTable};
|
||||
|
||||
// ── Check result ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// The result of running the semantic checker. Carries both the diagnostics and
|
||||
/// the resolved symbol tables so that downstream passes (e.g. codegen) can
|
||||
/// reuse them without re-running the checker.
|
||||
pub struct CheckResult {
|
||||
pub errors: Vec<Diagnostic>,
|
||||
pub sigma: StructTable,
|
||||
pub phi: FuncTable,
|
||||
}
|
||||
|
||||
// ── Checker ────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub struct Checker {
|
||||
@@ -166,7 +177,7 @@ fn value_struct_name(ty: &Ty) -> Option<&str> {
|
||||
|
||||
// ── Entry point ────────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn check(program: &ast::Program<Parsed>, no_main: bool) -> Vec<Diagnostic> {
|
||||
pub fn check(program: &ast::Program<Parsed>, no_main: bool) -> CheckResult {
|
||||
let mut checker = Checker::new();
|
||||
|
||||
// ── Pass 1: collect struct names + function signatures ────────────────────
|
||||
@@ -288,5 +299,9 @@ pub fn check(program: &ast::Program<Parsed>, no_main: bool) -> Vec<Diagnostic> {
|
||||
}
|
||||
}
|
||||
|
||||
checker.errors
|
||||
CheckResult {
|
||||
errors: checker.errors,
|
||||
sigma: checker.sigma,
|
||||
phi: checker.phi,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ mod tests {
|
||||
parser.errors
|
||||
);
|
||||
checker::check(&program, false)
|
||||
.errors
|
||||
.into_iter()
|
||||
.map(|d| d.message)
|
||||
.collect()
|
||||
|
||||
@@ -24,9 +24,14 @@ pub fn print_help() {
|
||||
"--version".bold(),
|
||||
);
|
||||
println!(
|
||||
" {} Compile without requiring a `main` entry point",
|
||||
" {} Compile to object file (no `main` required, no linking)",
|
||||
"-c".bold(),
|
||||
);
|
||||
println!(
|
||||
" {} {} Write output to <file>",
|
||||
"-o".bold(),
|
||||
"<file>".bold(),
|
||||
);
|
||||
println!();
|
||||
println!("{}", "ARGS:".bold().yellow());
|
||||
println!(
|
||||
@@ -61,15 +66,19 @@ pub fn io_error(path: &str, err: std::io::Error) -> ! {
|
||||
|
||||
pub struct Opts {
|
||||
pub files: Vec<String>,
|
||||
/// `-c`: compile without requiring a `main` entry point.
|
||||
/// `-c`: compile to object file without requiring a `main` entry point.
|
||||
pub no_main: bool,
|
||||
/// `-o <file>`: write final output to this path.
|
||||
pub output: Option<String>,
|
||||
}
|
||||
|
||||
pub fn parse_args() -> Opts {
|
||||
let mut files = Vec::new();
|
||||
let mut no_main = false;
|
||||
let mut output: Option<String> = None;
|
||||
let mut args = std::env::args().skip(1).peekable();
|
||||
|
||||
for arg in std::env::args().skip(1) {
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"-h" | "--help" => {
|
||||
print_help();
|
||||
@@ -80,6 +89,10 @@ pub fn parse_args() -> Opts {
|
||||
process::exit(0);
|
||||
}
|
||||
"-c" => no_main = true,
|
||||
"-o" => match args.next() {
|
||||
Some(path) => output = Some(path),
|
||||
None => fatal("option `-o` requires an argument"),
|
||||
},
|
||||
flag if flag.starts_with('-') => {
|
||||
fatal(&format!("unknown option `{flag}`"));
|
||||
}
|
||||
@@ -91,5 +104,9 @@ pub fn parse_args() -> Opts {
|
||||
fatal("no input files — at least one source file is required");
|
||||
}
|
||||
|
||||
Opts { files, no_main }
|
||||
Opts {
|
||||
files,
|
||||
no_main,
|
||||
output,
|
||||
}
|
||||
}
|
||||
|
||||
1418
fluxc/src/codegen/emit.rs
Normal file
1418
fluxc/src/codegen/emit.rs
Normal file
File diff suppressed because it is too large
Load Diff
116
fluxc/src/codegen/mod.rs
Normal file
116
fluxc/src/codegen/mod.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
pub mod emit;
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::{env, fs};
|
||||
|
||||
use crate::ast::{self, Parsed};
|
||||
use crate::checker::CheckResult;
|
||||
use crate::cli::Opts;
|
||||
|
||||
// ── Entry point ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Compile a parsed + type-checked program to a native binary (or object file
|
||||
/// when `opts.no_main` is set).
|
||||
///
|
||||
/// Pipeline:
|
||||
/// 1. Emit LLVM IR text → temp `.ll` file
|
||||
/// 2. `opt -O2` → optimised `.ll` file (`FLUXC_OPT` overrides `opt`)
|
||||
/// 3. `llc -filetype=obj` → `.o` file (`FLUXC_LLC` overrides `llc`)
|
||||
/// 4. `cc` link → executable (`FLUXC_CC` overrides `cc`)
|
||||
/// (step 4 is skipped in `-c` mode)
|
||||
pub fn compile(
|
||||
input_path: &str,
|
||||
program: &ast::Program<Parsed>,
|
||||
result: CheckResult,
|
||||
opts: &Opts,
|
||||
) -> Result<(), String> {
|
||||
// ── Derive output path ────────────────────────────────────────────────────
|
||||
let stem = Path::new(input_path)
|
||||
.file_stem()
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "out".to_string());
|
||||
|
||||
let final_output = opts.output.clone().unwrap_or_else(|| {
|
||||
if opts.no_main {
|
||||
format!("{stem}.o")
|
||||
} else {
|
||||
stem.clone()
|
||||
}
|
||||
});
|
||||
|
||||
// ── Temp paths ────────────────────────────────────────────────────────────
|
||||
let tmp = env::temp_dir();
|
||||
let raw_ll = tmp.join(format!("fluxc_{stem}.ll"));
|
||||
let opt_ll = tmp.join(format!("fluxc_{stem}.opt.ll"));
|
||||
let obj = tmp.join(format!("fluxc_{stem}.o"));
|
||||
|
||||
// ── Step 1: emit LLVM IR ──────────────────────────────────────────────────
|
||||
let ir = emit::emit_program(program, &result.sigma, &result.phi);
|
||||
fs::write(&raw_ll, &ir).map_err(|e| format!("cannot write IR to {}: {e}", raw_ll.display()))?;
|
||||
|
||||
// ── Step 2: opt ───────────────────────────────────────────────────────────
|
||||
let opt_bin = tool_path("FLUXC_OPT", "opt");
|
||||
run(
|
||||
&opt_bin,
|
||||
&[
|
||||
"-O2",
|
||||
raw_ll.to_str().unwrap(),
|
||||
"-S",
|
||||
"-o",
|
||||
opt_ll.to_str().unwrap(),
|
||||
],
|
||||
)?;
|
||||
|
||||
// ── Step 3: llc ───────────────────────────────────────────────────────────
|
||||
let llc_bin = tool_path("FLUXC_LLC", "llc");
|
||||
run(
|
||||
&llc_bin,
|
||||
&[
|
||||
opt_ll.to_str().unwrap(),
|
||||
"-filetype=obj",
|
||||
"-o",
|
||||
obj.to_str().unwrap(),
|
||||
],
|
||||
)?;
|
||||
|
||||
// ── Step 4: link (or copy object as final output) ─────────────────────────
|
||||
if opts.no_main {
|
||||
fs::copy(&obj, &final_output).map_err(|e| format!("cannot write {final_output}: {e}"))?;
|
||||
} else {
|
||||
let cc_bin = tool_path("FLUXC_CC", "cc");
|
||||
run(&cc_bin, &[obj.to_str().unwrap(), "-o", &final_output])?;
|
||||
}
|
||||
|
||||
// ── Clean up temp files ───────────────────────────────────────────────────
|
||||
let _ = fs::remove_file(&raw_ll);
|
||||
let _ = fs::remove_file(&opt_ll);
|
||||
let _ = fs::remove_file(&obj);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
fn tool_path(env_var: &str, default: &str) -> String {
|
||||
env::var(env_var).unwrap_or_else(|_| default.to_string())
|
||||
}
|
||||
|
||||
fn run(bin: &str, args: &[&str]) -> Result<(), String> {
|
||||
let output = Command::new(bin)
|
||||
.args(args)
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.map_err(|e| format!("failed to run `{bin}`: {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!(
|
||||
"`{bin}` exited with {}\n{}",
|
||||
output.status,
|
||||
stderr.trim_end()
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use crate::parser::Parser;
|
||||
pub mod ast;
|
||||
pub mod checker;
|
||||
pub mod cli;
|
||||
pub mod codegen;
|
||||
pub mod diagnostics;
|
||||
pub mod lexer;
|
||||
pub mod parser;
|
||||
@@ -14,6 +15,10 @@ fn main() {
|
||||
let opts = cli::parse_args();
|
||||
let mut had_errors = false;
|
||||
|
||||
// Collect (path, source, program, check_result) for all input files.
|
||||
// We gate codegen on all files being error-free.
|
||||
let mut compiled = Vec::new();
|
||||
|
||||
for path in &opts.files {
|
||||
let content = fs::read_to_string(path).unwrap_or_else(|e| cli::io_error(path, e));
|
||||
|
||||
@@ -26,11 +31,24 @@ fn main() {
|
||||
}
|
||||
|
||||
if parser.errors.is_empty() {
|
||||
let sema_errors = checker::check(&program, opts.no_main);
|
||||
for diag in &sema_errors {
|
||||
let result = checker::check(&program, opts.no_main);
|
||||
for diag in &result.errors {
|
||||
eprint!("{}", diag.render(&content, path));
|
||||
had_errors = true;
|
||||
}
|
||||
compiled.push((path.clone(), program, result));
|
||||
}
|
||||
}
|
||||
|
||||
if had_errors {
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
// All files are clean — run codegen.
|
||||
for (path, program, result) in compiled {
|
||||
if let Err(e) = codegen::compile(&path, &program, result, &opts) {
|
||||
eprintln!("{}: {e}", "error".to_string());
|
||||
had_errors = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user