Files
endorphin-aster-vm/crates/frontend/tests/ast.rs
T

240 lines
8.5 KiB
Rust

use frontend::ast::visitor::{Visitor, walk_expr, walk_function};
use frontend::ast::{
AST_SCHEMA_ID, AstDocument, AstError, AstRoot, Expr, ExprKind, Function, Spanned, StmtKind,
};
use frontend::{EndorReader, ParseMode, Span, parse_ast, parse_source_ast};
#[test]
fn function_bodies_accept_method_statements_and_optional_trailing_return() {
let root = parse_ast(
"function click(): Unit { title.setText(\"changed\") card.setColor(\"#FFEEDD\") }",
ParseMode::Module,
)
.unwrap();
let AstRoot::Module(program) = root else {
panic!("expected module")
};
let function = program.functions().next().unwrap();
assert_eq!(function.body.statements.len(), 2);
assert!(
function
.body
.statements
.iter()
.all(|s| matches!(s.kind, StmtKind::Expression(_)))
);
assert!(parse_ast("function empty(): Unit {}", ParseMode::Module).is_ok());
assert!(
parse_ast(
"function invalid(): Unit { return value other() }",
ParseMode::Module
)
.is_err()
);
}
#[test]
fn parse_modes_produce_distinct_roots_and_preserve_source_diagnostics() {
let source = "function echo(value: String): String { let copy = value return copy }";
let root = parse_ast(source, ParseMode::Module).unwrap();
let AstRoot::Module(program) = &root else {
panic!("expected module")
};
let function = program.functions().next().unwrap();
assert_eq!(function.body.statements.len(), 2);
assert!(matches!(
function.body.statements[0].kind,
StmtKind::Binding(_)
));
assert!(matches!(
function.body.statements[1].kind,
StmtKind::Return(_)
));
AstDocument::new(root)
.unwrap()
.validate_source(source)
.unwrap();
let root = parse_ast("echo(\"日\")", ParseMode::Expression).unwrap();
assert!(matches!(root, AstRoot::Expression(_)));
for text in ["let value = \"日\"", "echo(value)", source] {
let root = parse_ast(text, ParseMode::Interactive).unwrap();
let AstRoot::Interactive(input) = &root else {
panic!("expected interactive")
};
assert_eq!(input.statements.len(), 1);
AstDocument::new(root)
.unwrap()
.validate_source(text)
.unwrap();
}
assert!(parse_ast("let x = 1 let y = 2", ParseMode::Interactive).is_err());
assert!(parse_ast("", ParseMode::Interactive).is_err());
let source = EndorReader::from_text("<session:1>", "\"日\" + )");
let error = parse_source_ast(&source, ParseMode::Expression).unwrap_err();
assert_eq!(error.location.line, 1);
assert_eq!(error.location.column, 7);
assert!(error.to_string().starts_with("<session:1>:1:7:"));
}
#[test]
fn generated_visitors_reach_names_and_unwind_node_hooks_on_error() {
#[derive(Default)]
struct Names {
functions: Vec<String>,
references: Vec<String>,
depth: usize,
stop: bool,
}
impl Visitor for Names {
type Error = &'static str;
fn enter_node(&mut self, _span: Span) -> Result<(), Self::Error> {
self.depth += 1;
Ok(())
}
fn leave_node(&mut self) {
self.depth -= 1;
}
fn visit_function(&mut self, node: &Function) -> Result<(), Self::Error> {
self.functions.push(node.name.text.clone());
walk_function(self, node)
}
fn visit_expr(&mut self, node: &Expr) -> Result<(), Self::Error> {
if let ExprKind::Name(name) = &node.kind {
if self.stop {
return Err("stopped at reference");
}
self.references.push(name.clone());
}
walk_expr(self, node)
}
}
let root = parse_ast(
"function echo(value: String): String { let copy = value return copy }",
ParseMode::Module,
)
.unwrap();
let mut names = Names::default();
names.visit_ast_root(&root).unwrap();
assert_eq!(names.functions, ["echo"]);
assert_eq!(names.references, ["value", "copy"]);
assert_eq!(names.depth, 0);
let mut stopped = Names {
stop: true,
..Names::default()
};
assert_eq!(stopped.visit_ast_root(&root), Err("stopped at reference"));
assert_eq!(stopped.depth, 0);
}
#[test]
fn spans_share_unicode_locations_and_safe_original_source_access() {
let source = " \r\n(値)";
let root = parse_ast(source, ParseMode::Expression).unwrap();
assert_eq!(root.source_text(source), Some("(値)"));
let location = root.location(source).unwrap();
assert_eq!((location.line, location.column), (2, 1));
assert_eq!(Span::new(1, 3).slice("値"), None);
assert_eq!(Span::new(2, 1).slice("値"), None);
assert_eq!(Span::new(0, 4).location("値"), None);
}
#[test]
fn versioned_json_roundtrips_all_roots_and_preserves_number_spelling() {
for (source, mode) in [
(
"function f(): Number { return 00012345678901234567890.00100 }",
ParseMode::Module,
),
("f(.名前, \"日\\n\")", ParseMode::Expression),
("let value = 1", ParseMode::Interactive),
] {
let document = AstDocument::new(parse_ast(source, mode).unwrap()).unwrap();
let json = document.to_json().unwrap();
let restored = AstDocument::from_json(&json).unwrap();
assert_eq!(restored, document);
restored.validate_source(source).unwrap();
assert!(json.contains(AST_SCHEMA_ID));
if mode == ParseMode::Module {
assert!(json.contains("00012345678901234567890.00100"));
}
}
let schema: serde_json::Value =
serde_json::from_str(include_str!("../editor/ast.schema.json")).unwrap();
assert_eq!(schema["$id"], AST_SCHEMA_ID);
assert_eq!(
schema["$defs"]["AstDocument"]["properties"]["schema"]["const"],
AST_SCHEMA_ID
);
assert!(include_str!("../editor/ast.ts").contains(AST_SCHEMA_ID));
}
#[test]
fn json_interfaces_reject_unknown_fields_schema_changes_and_invalid_ranges() {
let source = "(f(値))";
let document = AstDocument::new(parse_ast(source, ParseMode::Expression).unwrap()).unwrap();
let mut value = serde_json::to_value(&document).unwrap();
value["schema"] = "urn:endor:ast:unknown".into();
assert!(matches!(
AstDocument::from_json(&value.to_string()),
Err(AstError::SchemaMismatch { .. })
));
value = serde_json::to_value(&document).unwrap();
value["root"]["data"]["unexpected"] = true.into();
assert!(matches!(
AstDocument::from_json(&value.to_string()),
Err(AstError::Json(_))
));
value = serde_json::to_value(&document).unwrap();
value["root"]["data"]["span"]["end"] = 1.into();
assert!(matches!(
AstDocument::from_json(&value.to_string()),
Err(AstError::ChildOutsideParent { .. })
));
let AstRoot::Expression(mut expression) = document.root else {
unreachable!()
};
expression.span = Span::new(5, 4);
assert!(matches!(
AstDocument::new(AstRoot::Expression(expression)),
Err(AstError::InvalidSpan(_))
));
let root = AstRoot::Expression(Expr {
kind: ExprKind::Name("値".into()),
span: Span::new(1, 3),
});
assert!(matches!(
AstDocument::new(root).unwrap().validate_source("値"),
Err(AstError::InvalidSpan(_))
));
}
#[test]
fn bare_assignment_has_its_own_ast_and_unicode_source_ranges() {
let source = "标题 = ui.text(\"你好\")";
let root = parse_ast(source, ParseMode::Interactive).unwrap();
let AstRoot::Interactive(input) = &root else {
panic!("interactive input")
};
let StmtKind::Assignment(assignment) = &input.statements[0].kind else {
panic!("assignment")
};
assert_eq!(assignment.name.text, "标题");
assert_eq!(assignment.name.source_text(source), Some("标题"));
assert_eq!(assignment.source_text(source), Some(source));
let document = AstDocument::new(root).unwrap();
document.validate_source(source).unwrap();
let json = document.to_json().unwrap();
assert_eq!(AstDocument::from_json(&json).unwrap(), document);
assert!(parse_ast("value = 1 = 2", ParseMode::Interactive).is_err());
let member = parse_ast("object.field = 1", ParseMode::Interactive).unwrap();
let AstRoot::Interactive(member) = member else {
panic!("interactive")
};
assert!(matches!(
&member.statements[0].kind,
StmtKind::MemberAssignment(_)
));
assert!(parse_ast("object() = 1", ParseMode::Interactive).is_err());
}