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,
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.

View File

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

View File

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

View File

@@ -24,9 +24,18 @@ 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!(
" {} Emit LLVM IR and stop (implies `-c`)",
"-S".bold(),
);
println!(
" {} {} Write output to <file>",
"-o".bold(),
"<file>".bold(),
);
println!();
println!("{}", "ARGS:".bold().yellow());
println!(
@@ -61,15 +70,22 @@ 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,
/// `-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 {
let mut files = Vec::new();
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() {
"-h" | "--help" => {
print_help();
@@ -80,6 +96,11 @@ pub fn parse_args() -> Opts {
process::exit(0);
}
"-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('-') => {
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");
}
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 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;
}
}