Compare commits

...

2 Commits

Author SHA1 Message Date
f836f279de Feat: add -S flag to emit LLVM IR
`-S` stops the pipeline after IR emission and writes the `.ll` file
directly to the output path (default `<stem>.ll`). It implies `-c`
(no main required). Combined with `-o`, the IR goes to the specified
path.

Pipeline summary:
  (none)  →  emit → opt → llc → cc  →  executable
  -c      →  emit → opt → llc       →  <stem>.o
  -S      →  emit                   →  <stem>.ll

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 20:28:24 +01:00
c2fc83b74b 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>
2026-03-11 20:21:34 +01:00
7 changed files with 1616 additions and 12 deletions

View File

@@ -42,14 +42,25 @@ pub enum Ty {
Char, Char,
Unit, Unit,
// Pointer types // Pointer types
Ptr { mutable: bool, pointee: Box<Ty> }, Ptr {
OpaquePtr { mutable: bool }, mutable: bool,
pointee: Box<Ty>,
},
OpaquePtr {
mutable: bool,
},
// Array type // Array type
Array { elem: Box<Ty>, size: u64 }, Array {
elem: Box<Ty>,
size: u64,
},
// User-defined struct // User-defined struct
Struct(String), Struct(String),
// Internal function signature (not user-facing) // 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. /// Unresolved integer type from a literal or an unannotated let-binding.
/// Compatible with every concrete integer type; defaults to `i32` in /// Compatible with every concrete integer type; defaults to `i32` in
/// error messages when no concrete type can be inferred. /// error messages when no concrete type can be inferred.

View File

@@ -10,6 +10,17 @@ use crate::diagnostics::{Diagnostic, Label};
use crate::token::Span; use crate::token::Span;
use env::{FieldEntry, FuncTable, ParamEntry, StructTable}; 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 ──────────────────────────────────────────────────────────────────── // ── Checker ────────────────────────────────────────────────────────────────────
pub struct Checker { pub struct Checker {
@@ -166,7 +177,7 @@ fn value_struct_name(ty: &Ty) -> Option<&str> {
// ── Entry point ──────────────────────────────────────────────────────────────── // ── 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(); let mut checker = Checker::new();
// ── Pass 1: collect struct names + function signatures ──────────────────── // ── 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,
}
} }

View File

@@ -20,6 +20,7 @@ mod tests {
parser.errors parser.errors
); );
checker::check(&program, false) checker::check(&program, false)
.errors
.into_iter() .into_iter()
.map(|d| d.message) .map(|d| d.message)
.collect() .collect()

View File

@@ -24,9 +24,18 @@ pub fn print_help() {
"--version".bold(), "--version".bold(),
); );
println!( println!(
" {} Compile without requiring a `main` entry point", " {} Compile to object file (no `main` required, no linking)",
"-c".bold(), "-c".bold(),
); );
println!(
" {} Emit LLVM IR and stop (implies `-c`)",
"-S".bold(),
);
println!(
" {} {} Write output to <file>",
"-o".bold(),
"<file>".bold(),
);
println!(); println!();
println!("{}", "ARGS:".bold().yellow()); println!("{}", "ARGS:".bold().yellow());
println!( println!(
@@ -61,15 +70,22 @@ pub fn io_error(path: &str, err: std::io::Error) -> ! {
pub struct Opts { pub struct Opts {
pub files: Vec<String>, 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, pub no_main: bool,
/// `-S`: emit LLVM IR text and stop (implies `-c`).
pub emit_ir: bool,
/// `-o <file>`: write final output to this path.
pub output: Option<String>,
} }
pub fn parse_args() -> Opts { pub fn parse_args() -> Opts {
let mut files = Vec::new(); let mut files = Vec::new();
let mut no_main = false; let mut no_main = false;
let mut emit_ir = 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() { match arg.as_str() {
"-h" | "--help" => { "-h" | "--help" => {
print_help(); print_help();
@@ -80,6 +96,11 @@ pub fn parse_args() -> Opts {
process::exit(0); process::exit(0);
} }
"-c" => no_main = true, "-c" => no_main = true,
"-S" => { emit_ir = true; no_main = true; }
"-o" => match args.next() {
Some(path) => output = Some(path),
None => fatal("option `-o` requires an argument"),
},
flag if flag.starts_with('-') => { flag if flag.starts_with('-') => {
fatal(&format!("unknown option `{flag}`")); fatal(&format!("unknown option `{flag}`"));
} }
@@ -91,5 +112,5 @@ pub fn parse_args() -> Opts {
fatal("no input files — at least one source file is required"); fatal("no input files — at least one source file is required");
} }
Opts { files, no_main } Opts { files, no_main, emit_ir, output }
} }

1418
fluxc/src/codegen/emit.rs Normal file

File diff suppressed because it is too large Load Diff

120
fluxc/src/codegen/mod.rs Normal file
View File

@@ -0,0 +1,120 @@
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.
///
/// Mode is controlled by `opts`:
///
/// | flags | output | pipeline |
/// |----------|---------------------|-----------------------|
/// | (none) | `<stem>` executable | emit → opt → llc → cc |
/// | `-c` | `<stem>.o` | emit → opt → llc |
/// | `-S` | `<stem>.ll` | emit only |
///
/// Tool overrides via environment variables:
/// `FLUXC_OPT` (default `opt`), `FLUXC_LLC` (default `llc`),
/// `FLUXC_CC` (default `cc`).
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.emit_ir {
format!("{stem}.ll")
} else if opts.no_main {
format!("{stem}.o")
} else {
stem.clone()
}
});
// ── Step 1: emit LLVM IR ──────────────────────────────────────────────────
let ir = emit::emit_program(program, &result.sigma, &result.phi);
// `-S`: write IR directly to final output and stop.
if opts.emit_ir {
return fs::write(&final_output, &ir)
.map_err(|e| format!("cannot write {final_output}: {e}"));
}
// ── Temp paths (only used when compiling beyond IR) ───────────────────────
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"));
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 (skipped in `-c` mode) ───────────────────────────────────
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(())
}

View File

@@ -5,6 +5,7 @@ use crate::parser::Parser;
pub mod ast; pub mod ast;
pub mod checker; pub mod checker;
pub mod cli; pub mod cli;
pub mod codegen;
pub mod diagnostics; pub mod diagnostics;
pub mod lexer; pub mod lexer;
pub mod parser; pub mod parser;
@@ -14,6 +15,10 @@ fn main() {
let opts = cli::parse_args(); let opts = cli::parse_args();
let mut had_errors = false; 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 { for path in &opts.files {
let content = fs::read_to_string(path).unwrap_or_else(|e| cli::io_error(path, e)); 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() { if parser.errors.is_empty() {
let sema_errors = checker::check(&program, opts.no_main); let result = checker::check(&program, opts.no_main);
for diag in &sema_errors { for diag in &result.errors {
eprint!("{}", diag.render(&content, path)); eprint!("{}", diag.render(&content, path));
had_errors = true; 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;
} }
} }