use clap::{Parser, Subcommand};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use phenotyper::diagnostic::{self, Diagnostic};
#[derive(Parser)]
#[command(name = "phenotyper", about = "Phenotyper v1 compiler", version)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Check {
#[arg(value_name = "FILE")]
file: PathBuf,
#[arg(long)]
json: bool,
},
Build {
#[arg(value_name = "FILE")]
file: PathBuf,
#[arg(long, short, value_name = "DIR")]
out: PathBuf,
#[arg(long)]
json: bool,
},
DumpAst {
#[arg(value_name = "FILE")]
file: PathBuf,
},
DumpIr {
#[arg(value_name = "FILE")]
file: PathBuf,
},
}
const EXIT_SUCCESS: u8 = 0;
const EXIT_FAILURE: u8 = 1;
fn main() -> ExitCode {
let cli = Cli::parse();
match cli.command {
Commands::Check { file, json } => cmd_check(&file, json),
Commands::Build { file, out, json } => cmd_build(&file, &out, json),
Commands::DumpAst { file } => cmd_dump_ast(&file),
Commands::DumpIr { file } => cmd_dump_ir(&file),
}
}
fn cmd_check(file: &Path, json: bool) -> ExitCode {
let file_str = file.display().to_string();
let source = match read_source(file) {
Ok(s) => s,
Err(e) => {
emit_single_error(&file_str, &e, json);
return ExitCode::from(EXIT_FAILURE);
}
};
match phenotyper::compile_source(&source, &file_str, None) {
Ok(output) => {
emit_diagnostics(&output.warnings, json);
if !json {
eprintln!(
"✓ {} — {}",
file_str,
diagnostic::format_summary(&output.warnings)
);
}
ExitCode::from(EXIT_SUCCESS)
}
Err(errors) => {
emit_diagnostics(&errors, json);
ExitCode::from(EXIT_FAILURE)
}
}
}
fn cmd_build(file: &Path, out: &Path, json: bool) -> ExitCode {
let file_str = file.display().to_string();
let source = match read_source(file) {
Ok(s) => s,
Err(e) => {
emit_single_error(&file_str, &e, json);
return ExitCode::from(EXIT_FAILURE);
}
};
match phenotyper::compile_source(&source, &file_str, Some(out)) {
Ok(output) => {
emit_diagnostics(&output.warnings, json);
if !json {
eprintln!(
"✓ {} → {} — {}",
file_str,
out.display(),
diagnostic::format_summary(&output.warnings)
);
}
ExitCode::from(EXIT_SUCCESS)
}
Err(errors) => {
emit_diagnostics(&errors, json);
ExitCode::from(EXIT_FAILURE)
}
}
}
fn cmd_dump_ast(file: &Path) -> ExitCode {
let file_str = file.display().to_string();
let source = match read_source(file) {
Ok(s) => s,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::from(EXIT_FAILURE);
}
};
match phenotyper::parser::parse_pht(&source, &file_str) {
Ok(ast) => {
println!("{ast:#?}");
ExitCode::from(EXIT_SUCCESS)
}
Err(diags) => {
for d in &diags {
eprintln!("{}", diagnostic::format_human(d));
}
ExitCode::from(EXIT_FAILURE)
}
}
}
fn cmd_dump_ir(file: &Path) -> ExitCode {
let file_str = file.display().to_string();
let source = match read_source(file) {
Ok(s) => s,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::from(EXIT_FAILURE);
}
};
let ast = match phenotyper::parser::parse_pht(&source, &file_str) {
Ok(ast) => ast,
Err(diags) => {
for d in &diags {
eprintln!("{}", diagnostic::format_human(d));
}
return ExitCode::from(EXIT_FAILURE);
}
};
let (table, sym_diags) = phenotyper::symbol::build(&ast, &file_str);
if diagnostic::has_errors(&sym_diags) {
for d in &sym_diags {
eprintln!("{}", diagnostic::format_human(d));
}
return ExitCode::from(EXIT_FAILURE);
}
let (module, ir_diags) = phenotyper::ir::lower(&ast, &table, &file_str);
if diagnostic::has_errors(&ir_diags) {
for d in &ir_diags {
eprintln!("{}", diagnostic::format_human(d));
}
return ExitCode::from(EXIT_FAILURE);
}
println!("{module:#?}");
ExitCode::from(EXIT_SUCCESS)
}
fn read_source(file: &Path) -> Result<String, String> {
let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
match ext {
"pht" | "md" => {}
_ => {
return Err(format!(
"unsupported file extension '.{ext}' — expected .pht or .md"
));
}
}
std::fs::read_to_string(file).map_err(|e| format!("cannot read '{}': {e}", file.display()))
}
fn emit_diagnostics(diags: &[Diagnostic], json: bool) {
if diags.is_empty() {
return;
}
if json {
print!("{}", diagnostic::format_json_all(diags));
} else {
for d in diags {
eprint!("{}", diagnostic::format_human(d));
}
}
}
fn emit_single_error(file: &str, message: &str, json: bool) {
let diag = Diagnostic::error(file, message);
emit_diagnostics(&[diag], json);
}