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>
This commit is contained in:
2026-03-11 20:28:24 +01:00
parent c2fc83b74b
commit f836f279de
2 changed files with 42 additions and 34 deletions

View File

@@ -27,6 +27,10 @@ pub fn print_help() {
" {} 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(),
@@ -68,6 +72,8 @@ pub struct Opts {
pub files: Vec<String>,
/// `-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>,
}
@@ -75,6 +81,7 @@ pub struct Opts {
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();
@@ -89,6 +96,7 @@ 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"),
@@ -104,9 +112,5 @@ pub fn parse_args() -> Opts {
fatal("no input files — at least one source file is required");
}
Opts {
files,
no_main,
output,
}
Opts { files, no_main, emit_ir, output }
}

View File

@@ -10,15 +10,19 @@ 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).
/// Compile a parsed + type-checked program.
///
/// 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)
/// 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>,
@@ -32,51 +36,51 @@ pub fn compile(
.unwrap_or_else(|| "out".to_string());
let final_output = opts.output.clone().unwrap_or_else(|| {
if opts.no_main {
if opts.emit_ir {
format!("{stem}.ll")
} else if opts.no_main {
format!("{stem}.o")
} else {
stem.clone()
}
});
// ── Temp paths ────────────────────────────────────────────────────────────
// ── 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"));
// ── 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()))?;
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(),
],
&["-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(),
],
&[opt_ll.to_str().unwrap(), "-filetype=obj", "-o", obj.to_str().unwrap()],
)?;
// ── Step 4: link (or copy object as final output) ─────────────────────────
// ── 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}"))?;
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])?;