use std::path::PathBuf;
use std::sync::Arc;
pub mod viz;
use clap::Subcommand;
use cljrs_ir::IrBundle;
use cljrs_runtime::tiered::{Env, GlobalEnv};
use cljrs_value::{CljxFn, Value};
#[derive(Subcommand)]
pub enum IrCommands {
Build {
#[arg(short, long)]
ns: Vec<String>,
#[arg(short, long, default_value = "ir_bundle.bin")]
output: PathBuf,
#[arg(long = "src-path", value_name = "DIR")]
src_paths: Vec<PathBuf>,
#[arg(short, long)]
verbose: bool,
},
Dump {
input: PathBuf,
},
Viz {
file: PathBuf,
#[arg(short, long)]
out: Option<PathBuf>,
#[arg(long = "src-path", value_name = "DIR")]
src_paths: Vec<PathBuf>,
#[arg(long)]
quiet: bool,
},
}
pub fn run(command: IrCommands) -> miette::Result<i32> {
match command {
IrCommands::Build {
ns,
output,
src_paths,
verbose,
} => {
let namespaces = if ns.is_empty() {
vec!["clojure.core".to_string()]
} else {
ns
};
let stats = run_prebuild(&namespaces, &output, &src_paths, verbose)
.map_err(|e| miette::miette!("{e}"))?;
eprintln!(
"Wrote {} functions ({} unsupported) to {}",
stats.lowered,
stats.unsupported,
stats.output.display()
);
Ok(0)
}
IrCommands::Dump { input } => {
let bytes =
std::fs::read(&input).map_err(|e| miette::miette!("{}: {}", input.display(), e))?;
let bundle = cljrs_ir::deserialize_bundle(&bytes)
.map_err(|e| miette::miette!("failed to deserialize {}: {e}", input.display()))?;
println!("{}", bundle);
Ok(0)
}
IrCommands::Viz {
file,
out,
src_paths,
quiet,
} => run_viz(file, out, src_paths, quiet),
}
}
fn run_viz(
file: PathBuf,
out: Option<PathBuf>,
src_paths: Vec<PathBuf>,
quiet: bool,
) -> miette::Result<i32> {
let (source, ir) = cljrs_compiler::aot::lower_file_to_ir(&file, &src_paths, quiet)
.map_err(|e| miette::miette!("{e}"))?;
let title = format!("IR — {}", file.display());
let html = viz::render_html(
&ir,
Some(&source),
&viz::RenderOptions { title: Some(title) },
);
let out_path = out.unwrap_or_else(|| {
let mut p = file.clone();
let new_name = format!(
"{}.ir.html",
file.file_name()
.and_then(|n| n.to_str())
.unwrap_or("output")
);
p.set_file_name(new_name);
p
});
std::fs::write(&out_path, html)
.map_err(|e| miette::miette!("writing {}: {e}", out_path.display()))?;
if !quiet {
eprintln!("[ir viz] wrote {}", out_path.display());
}
Ok(0)
}
struct PrebuildStats {
lowered: usize,
unsupported: usize,
output: PathBuf,
}
fn run_prebuild(
namespaces: &[String],
output: &PathBuf,
src_paths: &[PathBuf],
verbose: bool,
) -> Result<PrebuildStats, String> {
let runtime = cljrs_runtime::Runtime::builder()
.execution_mode(cljrs_runtime::ExecutionMode::Tiered)
.source_paths(src_paths.to_vec())
.build()
.map_err(|e| format!("{e}"))?;
if !runtime.tier_state().ir_enabled() {
return Err("IR lowering is disabled (CLJRS_NO_IR is set)".to_string());
}
let globals = runtime.globals().clone();
let mut env = Env::new(globals.clone(), "user");
for ns_name in namespaces {
if ns_name != "clojure.core" {
load_namespace(&globals, &mut env, ns_name, verbose)?;
}
}
let mut bundle = IrBundle::new();
let mut lowered = 0usize;
let mut unsupported = 0usize;
for ns_name in namespaces {
if verbose {
eprintln!("Lowering namespace: {ns_name}");
}
let (ns_lowered, ns_unsupported) =
lower_namespace(&globals, &mut env, ns_name, &mut bundle, verbose)?;
lowered += ns_lowered;
unsupported += ns_unsupported;
}
if verbose {
eprintln!("Lowering complete: {lowered} functions lowered, {unsupported} unsupported.");
}
let bytes =
cljrs_ir::serialize_bundle(&bundle).map_err(|e| format!("serialization failed: {e}"))?;
std::fs::write(output, &bytes)
.map_err(|e| format!("failed to write {}: {e}", output.display()))?;
if verbose {
eprintln!("Wrote {} bytes to {}", bytes.len(), output.display());
}
Ok(PrebuildStats {
lowered,
unsupported,
output: output.clone(),
})
}
fn load_namespace(
globals: &Arc<GlobalEnv>,
env: &mut Env,
ns_name: &str,
verbose: bool,
) -> Result<(), String> {
if verbose {
eprintln!("Loading namespace: {ns_name}");
}
let span = cljrs_types::span::Span::new(Arc::new("<prebuild>".to_string()), 0, 0, 1, 1);
let require_form = cljrs_reader::Form::new(
cljrs_reader::form::FormKind::List(vec![
cljrs_reader::Form::new(
cljrs_reader::form::FormKind::Symbol("require".into()),
span.clone(),
),
cljrs_reader::Form::new(
cljrs_reader::form::FormKind::Quote(Box::new(cljrs_reader::Form::new(
cljrs_reader::form::FormKind::Symbol(ns_name.into()),
span.clone(),
))),
span,
),
]),
cljrs_types::span::Span::new(Arc::new("<prebuild>".to_string()), 0, 0, 1, 1),
);
cljrs_runtime::tiered::eval(&require_form, env)
.map_err(|e| format!("failed to load namespace {ns_name}: {e:?}"))?;
if !globals.is_loaded(ns_name) {
return Err(format!(
"namespace {ns_name} was not marked as loaded after require"
));
}
Ok(())
}
fn lower_namespace(
globals: &Arc<GlobalEnv>,
env: &mut Env,
ns_name: &str,
bundle: &mut IrBundle,
verbose: bool,
) -> Result<(usize, usize), String> {
let var_entries: Vec<(Arc<str>, Value)> = {
let ns_map = globals.namespaces.read().unwrap();
let ns = ns_map
.get(ns_name)
.ok_or_else(|| format!("namespace {ns_name} not found"))?;
let interns = ns.get().interns.lock().unwrap();
interns
.iter()
.map(|(name, var)| {
let val = var.get().deref().unwrap_or(Value::Nil);
(name.clone(), val)
})
.collect()
};
let mut lowered = 0usize;
let mut unsupported = 0usize;
for (var_name, val) in &var_entries {
let f = match val {
Value::Fn(gc_fn) => gc_fn.get().clone(),
_ => continue,
};
if f.is_macro {
continue;
}
let fn_lowered = lower_function(ns_name, var_name, &f, env, bundle, verbose);
lowered += fn_lowered.0;
unsupported += fn_lowered.1;
}
if verbose {
eprintln!(" {ns_name}: {lowered} lowered, {unsupported} unsupported");
}
Ok((lowered, unsupported))
}
fn lower_function(
ns_name: &str,
var_name: &str,
f: &CljxFn,
env: &mut Env,
bundle: &mut IrBundle,
verbose: bool,
) -> (usize, usize) {
let mut lowered = 0;
let mut unsupported = 0;
for arity in &f.arities {
let param_count = arity.params.len();
let is_variadic = arity.rest_param.is_some();
let key = if is_variadic {
format!("{ns_name}/{var_name}:{param_count}+")
} else {
format!("{ns_name}/{var_name}:{param_count}")
};
let ns_arc: Arc<str> = Arc::from(ns_name);
match cljrs_runtime::tiered::lower::lower_arity(
f.name.as_deref(),
&arity.params,
arity.rest_param.as_ref(),
&arity.destructure_params,
arity.destructure_rest.as_ref(),
&arity.body,
&ns_arc,
env,
f.is_async,
) {
Ok(ir_func) => {
if verbose {
eprintln!(" lowered {key} ({} blocks)", ir_func.blocks.len());
}
bundle.insert(key, ir_func);
lowered += 1;
}
Err(e) => {
if verbose {
eprintln!(" unsupported {key}: {e}");
}
unsupported += 1;
}
}
}
(lowered, unsupported)
}