use axon_frontend::ir_generator::IRGenerator;
use axon_frontend::ir_nodes::IRProgram;
use axon_frontend::lexer::Lexer;
use axon_frontend::parser::Parser;
use axon_frontend::type_checker::TypeChecker;
use serde::Serialize;
use serde_json::Value;
#[derive(Debug, Clone, Serialize)]
pub struct Diagnostic {
pub severity: &'static str,
pub stage: Stage,
pub message: String,
pub line: u32,
pub column: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Stage {
Lex,
Parse,
TypeCheck,
IrGenerate,
}
impl Stage {
fn as_str(self) -> &'static str {
match self {
Stage::Lex => "lex",
Stage::Parse => "parse",
Stage::TypeCheck => "type_check",
Stage::IrGenerate => "ir_generate",
}
}
}
#[derive(Debug)]
pub enum Outcome {
Ok {
ir: IRProgram,
warnings: Vec<Diagnostic>,
},
Err {
stage: Stage,
errors: Vec<Diagnostic>,
warnings: Vec<Diagnostic>,
},
}
pub fn run(source: &str, filename: &str) -> Outcome {
let tokens = match Lexer::new(source, filename).tokenize() {
Ok(t) => t,
Err(e) => {
return Outcome::Err {
stage: Stage::Lex,
errors: vec![Diagnostic {
severity: "error",
stage: Stage::Lex,
message: e.message,
line: e.line,
column: e.column,
}],
warnings: Vec::new(),
};
}
};
let program = match Parser::new(tokens).parse() {
Ok(p) => p,
Err(e) => {
return Outcome::Err {
stage: Stage::Parse,
errors: vec![Diagnostic {
severity: "error",
stage: Stage::Parse,
message: e.message,
line: e.line,
column: e.column,
}],
warnings: Vec::new(),
};
}
};
let (type_errors, type_warnings) = TypeChecker::new(&program).check_with_warnings();
let warnings: Vec<Diagnostic> = type_warnings
.into_iter()
.map(|w| Diagnostic {
severity: "warning",
stage: Stage::TypeCheck,
message: w.message,
line: w.line,
column: w.column,
})
.collect();
if !type_errors.is_empty() {
let errors = type_errors
.into_iter()
.map(|e| Diagnostic {
severity: "error",
stage: Stage::TypeCheck,
message: e.message,
line: e.line,
column: e.column,
})
.collect();
return Outcome::Err { stage: Stage::TypeCheck, errors, warnings };
}
let ir = IRGenerator::new().generate(&program);
Outcome::Ok { ir, warnings }
}
pub fn outcome_to_check_payload(outcome: &Outcome) -> Value {
match outcome {
Outcome::Ok { warnings, .. } => serde_json::json!({
"ok": true,
"stage": Stage::TypeCheck.as_str(),
"errors": Vec::<Diagnostic>::new(),
"warnings": warnings,
"summary": summary_for(true, 0, warnings.len()),
}),
Outcome::Err { stage, errors, warnings } => serde_json::json!({
"ok": false,
"stage": stage.as_str(),
"errors": errors,
"warnings": warnings,
"summary": summary_for(false, errors.len(), warnings.len()),
}),
}
}
pub fn outcome_to_parse_payload(outcome: Outcome) -> Value {
match outcome {
Outcome::Ok { ir, warnings } => {
let ir_json = serde_json::to_value(&ir)
.unwrap_or_else(|_| serde_json::json!({ "error": "ir serialisation failed" }));
serde_json::json!({
"ok": true,
"stage": Stage::IrGenerate.as_str(),
"ir": ir_json,
"warnings": warnings,
"summary": summary_for(true, 0, warnings.len()),
})
}
Outcome::Err { stage, errors, warnings } => serde_json::json!({
"ok": false,
"stage": stage.as_str(),
"errors": errors,
"warnings": warnings,
"summary": summary_for(false, errors.len(), warnings.len()),
}),
}
}
fn summary_for(ok: bool, err_count: usize, warn_count: usize) -> String {
if ok && warn_count == 0 {
"program is well-formed".to_string()
} else if ok {
format!("program is well-formed ({warn_count} warning(s))")
} else {
let suffix = if warn_count > 0 {
format!(", {warn_count} warning(s)")
} else {
String::new()
};
format!("{err_count} error(s){suffix}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_source_parses_to_an_empty_program() {
let out = run("", "<t>");
match out {
Outcome::Ok { ir, warnings } => {
assert!(warnings.is_empty());
let v = serde_json::to_value(&ir).unwrap();
assert_eq!(v["node_type"], "program");
}
other => panic!("empty source should parse OK, got: {other:?}"),
}
}
#[test]
fn syntactic_garbage_fails_at_lex_or_parse() {
let out = run("@@@", "<t>");
match out {
Outcome::Err { stage, errors, .. } => {
assert!(matches!(stage, Stage::Lex | Stage::Parse));
assert_eq!(errors.len(), 1);
assert!(errors[0].line >= 1);
}
other => panic!("garbage should fail, got: {other:?}"),
}
}
#[test]
fn well_formed_program_with_no_diagnostics_returns_ok() {
let src = r#"persona Tester { domain: ["test"] tone: precise }"#;
let out = run(src, "<t>");
match out {
Outcome::Ok { warnings, .. } => assert!(warnings.is_empty()),
other => panic!("well-formed program should pass, got: {other:?}"),
}
}
#[test]
fn check_payload_has_uniform_shape_on_ok() {
let out = run("", "<t>");
let payload = outcome_to_check_payload(&out);
assert_eq!(payload["ok"], true);
assert_eq!(payload["stage"], "type_check");
assert!(payload["errors"].is_array());
assert!(payload["warnings"].is_array());
assert_eq!(payload["summary"], "program is well-formed");
}
#[test]
fn check_payload_has_uniform_shape_on_err() {
let out = run("@@@", "<t>");
let payload = outcome_to_check_payload(&out);
assert_eq!(payload["ok"], false);
assert!(payload["errors"].as_array().unwrap().len() >= 1);
let summary = payload["summary"].as_str().unwrap();
assert!(summary.contains("error"));
}
#[test]
fn parse_payload_includes_ir_on_success() {
let out = run("", "<t>");
let payload = outcome_to_parse_payload(out);
assert_eq!(payload["ok"], true);
assert_eq!(payload["stage"], "ir_generate");
assert_eq!(payload["ir"]["node_type"], "program");
}
#[test]
fn parse_payload_omits_ir_on_failure() {
let out = run("@@@", "<t>");
let payload = outcome_to_parse_payload(out);
assert_eq!(payload["ok"], false);
assert!(payload["ir"].is_null());
}
#[test]
fn summary_phrasing_is_stable_across_branches() {
assert_eq!(summary_for(true, 0, 0), "program is well-formed");
assert_eq!(
summary_for(true, 0, 2),
"program is well-formed (2 warning(s))"
);
assert_eq!(summary_for(false, 3, 0), "3 error(s)");
assert_eq!(summary_for(false, 3, 1), "3 error(s), 1 warning(s)");
}
}