use clap::{Parser, Subcommand};
use crisp_diagnostics::{
Severity, format_diagnostic_at, format_parse_error, format_unresolved_name,
};
use crisp_errors::ErrorPass;
use crisp_ownership::OwnershipPass;
use crisp_parser::{ParseError, Parser as CrispParser};
use crisp_regions::RegionPass;
use crisp_resolve::module::load_module_graph;
use crisp_resolve::{ResolveError, Resolver, find_crate_root};
use crisp_rust_emit::{
PipelineError, TestHarnessError, build_emitted, emit_to_target, resolve_rustc_fallbacks,
run_emitted, run_tests, verify_sealed_api,
};
use crisp_typeck::{TypeChecker, TypeError};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Parser)]
#[command(
name = "crisp",
version,
about = "Crisp language toolchain — .crp to Rust to native"
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Parse { file: PathBuf },
Resolve {
#[arg(default_value = ".")]
path: PathBuf,
},
Build {
#[arg(default_value = ".")]
path: String,
},
Run {
#[arg(default_value = ".")]
path: String,
},
Test {
#[arg(default_value = ".")]
paths: Vec<String>,
},
Check {
#[arg(default_value = ".")]
path: String,
},
Emit {
#[arg(default_value = ".")]
path: String,
},
}
fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
let cli = Cli::parse();
match cli.command {
Commands::Parse { file } => {
let src = fs::read_to_string(&file)?;
match CrispParser::new(&src).and_then(|mut p| p.parse_file()) {
Ok(module) => {
println!("{module:#?}");
Ok(())
}
Err(e) => {
print_parse_error(&file, &src, &e);
Err(e.into())
}
}
}
Commands::Resolve { path } => {
let root = find_crate_root(&path).unwrap_or(path);
match Resolver::resolve_crate(&root) {
Ok(resolved) => {
print_resolve_warnings(&resolved.warnings);
println!("{resolved:#?}");
Ok(())
}
Err(e) => {
print_resolve_diagnostic(&root, &e);
Err(e.into())
}
}
}
Commands::Check { path } => {
let root = find_crate_root(PathBuf::from(&path).as_path())
.unwrap_or_else(|| PathBuf::from(&path));
match Resolver::resolve_crate(&root) {
Err(e) => {
print_resolve_diagnostic(&root, &e);
return Err(e.into());
}
Ok(resolved) => print_resolve_warnings(&resolved.warnings),
}
match TypeChecker::check_crate(&root) {
Err(e) => {
print_type_diagnostic(&root, &e);
return Err(e.into());
}
Ok(typed) => print_type_warnings(&root, &typed.warnings),
}
match resolve_rustc_fallbacks(&root) {
Ok(_) => {}
Err(crisp_rust_emit::FallbackResolveError::RustcUnavailable) => {
OwnershipPass::analyze_crate(&root)?;
}
Err(e) => return Err(e.into()),
}
RegionPass::assign_crate(&root)?;
ErrorPass::analyze_crate(&root)?;
verify_sealed_api(&root)?;
eprintln!("crisp check: ok ({})", root.display());
Ok(())
}
Commands::Build { path } => {
let root = find_crate_root(PathBuf::from(&path).as_path())
.unwrap_or_else(|| PathBuf::from(&path));
match build_emitted(&root) {
Ok(out_dir) => {
eprintln!("crisp build: ok ({})", out_dir.display());
Ok(())
}
Err(PipelineError::ToolchainUnavailable) => {
eprintln!("crisp build: emitted to target/rust/ (cargo not on PATH)");
emit_to_target(&root)?;
std::process::exit(1);
}
Err(e) => Err(e.into()),
}
}
Commands::Run { path } => {
let root = find_crate_root(PathBuf::from(&path).as_path())
.unwrap_or_else(|| PathBuf::from(&path));
match run_emitted(&root) {
Ok(stdout) => {
print!("{stdout}");
Ok(())
}
Err(PipelineError::ToolchainUnavailable) => {
eprintln!("crisp run: cargo not on PATH");
std::process::exit(1);
}
Err(e) => Err(e.into()),
}
}
Commands::Test { paths } => {
for path in normalize_crate_paths(paths) {
let root = find_crate_root(PathBuf::from(&path).as_path())
.unwrap_or_else(|| PathBuf::from(&path));
match run_tests(&root) {
Ok(report) => {
eprintln!(
"crisp test: ok {} ({} runtime, {} compile-fail)",
root.display(),
report.runtime_passed,
report.compile_fail_passed
);
}
Err(TestHarnessError::Other(e))
if e.to_string().contains("cargo not on PATH") =>
{
eprintln!("crisp test: cargo not on PATH");
std::process::exit(1);
}
Err(e) => return Err(e.into()),
}
}
Ok(())
}
Commands::Emit { path } => {
let root = find_crate_root(PathBuf::from(&path).as_path())
.unwrap_or_else(|| PathBuf::from(&path));
let out = emit_to_target(&root)?;
eprintln!("crisp emit: ok ({})", out.out_dir.display());
Ok(())
}
}
}
fn print_parse_error(file: &Path, source: &str, err: &ParseError) {
let mut extras = Vec::new();
if let Some(h) = err.help() {
extras.push(format!("help: {h}"));
}
let name = file.display().to_string();
let rendered = format_parse_error(
&name,
source,
err.diagnostic_code(),
&err.primary_message(),
err.byte_pos(),
&extras,
)
.rendered;
eprintln!("{rendered}");
}
fn print_resolve_warnings(warnings: &[crisp_resolve::ResolveWarning]) {
for w in warnings {
eprintln!("warning: {w}");
}
}
fn print_type_warnings(root: &Path, warnings: &[crisp_typeck::TypeWarning]) {
for w in warnings {
if let Some((file, source)) = source_for_span(root, w.span()) {
let rendered = format_diagnostic_at(
&file,
&source,
w.code(),
&w.to_string().replacen(&format!("[{}] ", w.code()), "", 1),
w.span(),
Severity::Warning,
&[],
)
.rendered;
eprintln!("{rendered}");
} else {
eprintln!("warning: {w}");
}
}
}
fn print_resolve_diagnostic(root: &Path, err: &ResolveError) {
match err {
ResolveError::Parse { path, message, pos } => {
if let Ok(source) = fs::read_to_string(path) {
let rel = Path::new(path)
.strip_prefix(root)
.unwrap_or(Path::new(path))
.display()
.to_string();
let code = if message.starts_with("lex error:") {
"E0011"
} else {
"E0010"
};
let rendered = format_parse_error(&rel, &source, code, message, *pos, &[]).rendered;
eprintln!("{rendered}");
return;
}
}
ResolveError::UnresolvedName {
name, span, hint, ..
} => {
if let Some((file, source)) = source_for_span(root, *span) {
let rendered =
format_unresolved_name(&file, &source, name, *span, hint.as_deref()).rendered;
eprintln!("{rendered}");
return;
}
}
ResolveError::ShapesUnsupported { name, span } => {
if let Some((file, source)) = source_for_span(root, *span) {
let rendered = format_diagnostic_at(
&file,
&source,
"E0039",
&format!("shapes are not yet supported (`{name}`)"),
*span,
Severity::Error,
&["help: remove the `shape` definition or bound".into()],
)
.rendered;
eprintln!("{rendered}");
return;
}
}
_ => {}
}
eprintln!("{err}");
}
fn print_type_diagnostic(root: &Path, err: &TypeError) {
match err {
TypeError::UnknownName { name, span } | TypeError::UnknownType { name, span } => {
if let Some((file, source)) = source_for_span(root, *span) {
let code = if matches!(err, TypeError::UnknownType { .. }) {
"E0040"
} else {
"E0041"
};
let rendered = format_diagnostic_at(
&file,
&source,
code,
&err.to_string().replacen(&format!("[{code}] "), "", 1),
*span,
Severity::Error,
&[],
)
.rendered;
eprintln!("{rendered}");
let _ = name;
return;
}
}
TypeError::AmbiguousField {
field,
candidates,
span,
} => {
if let Some((file, source)) = source_for_span(root, *span) {
let rendered = format_diagnostic_at(
&file,
&source,
"E0043",
&format!(
"ambiguous field `{field}` on unresolved type; annotate the parameter (candidates: {candidates})"
),
*span,
Severity::Error,
&["help: write `param: StructName` on the function parameter".into()],
)
.rendered;
eprintln!("{rendered}");
return;
}
}
TypeError::Resolve(inner) => {
print_resolve_diagnostic(root, inner);
return;
}
TypeError::UnifyAt { message, span } => {
if let Some((file, source)) = source_for_span(root, *span) {
let rendered = format_diagnostic_at(
&file,
&source,
"E0041",
message,
*span,
Severity::Error,
&[],
)
.rendered;
eprintln!("{rendered}");
return;
}
}
TypeError::InvalidCast { span, .. } => {
if let Some((file, source)) = source_for_span(root, *span) {
let rendered = format_diagnostic_at(
&file,
&source,
"E0087",
&err.to_string().replacen("[E0087] ", "", 1),
*span,
Severity::Error,
&[],
)
.rendered;
eprintln!("{rendered}");
return;
}
}
TypeError::UndeclaredRustImport { span, .. } => {
if let Some((file, source)) = source_for_span(root, *span) {
let rendered = format_diagnostic_at(
&file,
&source,
"E0089",
&err.to_string().replacen("[E0089] ", "", 1),
*span,
Severity::Error,
&["help: declare it in `extern rust <crate> { item(…) -> … }` or a `.crpi` sidecar".into()],
)
.rendered;
eprintln!("{rendered}");
return;
}
}
TypeError::InvalidExternRustTy { span, .. } => {
if let Some((file, source)) = source_for_span(root, *span) {
let rendered = format_diagnostic_at(
&file,
&source,
"E0090",
&err.to_string().replacen("[E0090] ", "", 1),
*span,
Severity::Error,
&[],
)
.rendered;
eprintln!("{rendered}");
return;
}
}
_ => {}
}
eprintln!("{err}");
}
fn normalize_crate_paths(raw: Vec<String>) -> Vec<String> {
const GLUE: &[&str] = &[
"and", "&&", "crisp", "test", "check", "run", "build", "emit",
];
let out: Vec<String> = raw
.into_iter()
.filter(|p| !GLUE.contains(&p.as_str()) || Path::new(p).exists())
.collect();
if out.is_empty() {
vec![".".into()]
} else {
out
}
}
fn source_for_span(root: &Path, span: crisp_ast::Span) -> Option<(String, String)> {
let graph = load_module_graph(root).ok()?;
let mut best: Option<(String, String)> = None;
for node in graph.modules.values() {
let Ok(source) = fs::read_to_string(&node.path) else {
continue;
};
if (source.len() as u32) < span.end {
continue;
}
let rel = node
.path
.strip_prefix(root)
.unwrap_or(node.path.as_path())
.display()
.to_string();
best = Some((rel, source));
if node.module_path == "main" {
break;
}
}
best
}