Files

367 lines
18 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Rust 测试框架验证 Token、语法树形状、优先级、拥有文本及原文诊断;不进行名称分析或 VM 执行。
use frontend::ast::{
BinaryOp, Binding, DeclKind, Expr, ExprKind, Function, Program, StmtKind, UnaryOp,
};
use frontend::{DiagnosticKind, Span, TokenKind, lex, parse, parse_expression};
/// 声明测试借用 program 指定位置的函数,错误节点种类直接使测试失败,不创建或调用 VM 函数。
fn function(program: &Program, index: usize) -> &Function {
// 借用指定声明并要求确为函数,类等其他声明不应被辅助函数静默接受。
let DeclKind::Function(function) = &program.declarations[index].kind else {
panic!("expected function")
};
function
}
/// 声明测试借用函数体指定位置的 let Binding,核对解析树语句形状与名称。
fn binding(function: &Function, index: usize) -> &Binding {
// 要求当前语句是 let 节点,普通表达式或赋值不满足此测试前提。
let StmtKind::Binding(binding) = &function.body.statements[index].kind else {
panic!("expected binding")
};
binding
}
/// 声明测试借用函数体末尾 Return 的表达式,断言本用例确实有返回节点及值。
fn result(function: &Function) -> &Expr {
// 末语句必须是 Return,下面取得其中表达式,不把普通表达式语句误当返回。
let StmtKind::Return(returned) = &function.body.statements.last().unwrap().kind else {
panic!("expected return")
};
returned.value.as_ref().unwrap()
}
/// 优先级测试拆二元节点,先核对操作种类再借用左右操作数,供逐层检查嵌套树。
fn binary(expression: &Expr, expected: BinaryOp) -> (&Expr, &Expr) {
// 拆出借用的操作符及两个子表达式,操作种类在返回前统一核对。
let ExprKind::Binary {
operation,
left,
right,
} = &expression.kind
else {
panic!("expected a binary expression: {expression:?}");
};
assert_eq!(*operation, expected);
(left, right)
}
/// 表达式测试核对原始数值拼写,前端保存 String,不在这里转换或计算数值。
fn number(expression: &Expr, expected: &str) {
assert_eq!(expression.kind, ExprKind::Number(expected.into()));
}
#[test]
/// 由 Rust 测试框架调用:Unicode 名称、全角数字及 emoji 字符串的 Token 范围精确切回原文,所有端点都在字符边界。
fn unicode_tokens_refer_to_exact_original_byte_ranges() {
// 保留当前原始文本用于字节切片与位置比较,不规范化其中的 Unicode、注释、空白或换行。
let source = "// 日本語\r\nlet 商品_2 = \"こんにちは🌸\"";
// 仅分词,后续同时检查种类与通过 span 切回的原文,不借助编译器判断数值。
let tokens = lex(source).unwrap();
assert_eq!(tokens.len(), 4);
assert_eq!(tokens[0].kind, TokenKind::Name);
assert_eq!(&source[tokens[0].span.start..tokens[0].span.end], "let");
assert_eq!(&source[tokens[1].span.start..tokens[1].span.end], "商品_2");
assert_eq!(tokens[2].kind, TokenKind::Equal);
assert_eq!(tokens[3].kind, TokenKind::String);
// 每个半开范围的两端都必须是 UTF-8 边界,不能只检查特定一个名称。
for token in tokens {
assert!(source.is_char_boundary(token.span.start));
assert!(source.is_char_boundary(token.span.end));
}
}
#[test]
/// 由 Rust 测试框架调用:负号是独立 Token 和一元节点,长十进制的前导/尾随零在解析树中完整保留。
fn decimal_spelling_and_minus_tokens_are_preserved_without_float_conversion() {
// 保留当前原始文本用于字节切片与位置比较,不规范化其中的 Unicode、注释、空白或换行。
let source = "-000123456789012345678901234567890.00100 / 7 // comment";
// 仅分词,后续同时检查种类与通过 span 切回的原文,不借助编译器判断数值。
let tokens = lex(source).unwrap();
assert_eq!(
tokens.iter().map(|token| token.kind).collect::<Vec<_>>(),
vec![
TokenKind::Minus,
TokenKind::Number,
TokenKind::Slash,
TokenKind::Number,
]
);
assert_eq!(
&source[tokens[1].span.start..tokens[1].span.end],
"000123456789012345678901234567890.00100"
);
// 解析完整表达式得到拥有型节点,下面直接核对语法结构而非运行结果。
let expression = parse_expression(source).unwrap();
// 根为 Divide;左子树应是取负,右子树保持原始数字 7。
let (negative, divisor) = binary(&expression, BinaryOp::Divide);
// 当前层必须是前缀取负节点,后面的操作数继续用于检查内部树。
let ExprKind::Unary {
operation: UnaryOp::Negate,
operand,
} = &negative.kind
else {
panic!("expected unary minus");
};
number(operand, "000123456789012345678901234567890.00100");
number(divisor, "7");
}
#[test]
/// 由 Rust 测试框架调用:四种支持转义被解码,字符串内双斜杠作为文本保留,节点范围仍覆盖带引号原文。
fn strings_decode_only_the_supported_escapes_and_keep_comments_inside_strings() {
// 解析完整表达式得到拥有型节点,下面直接核对语法结构而非运行结果。
let expression = parse_expression(r#""商品\n\t\"\\// text""#).unwrap();
assert_eq!(
expression.kind,
ExprKind::String("商品\n\t\"\\// text".into())
);
assert_eq!(
expression.span,
Span::new(0, r#""商品\n\t\"\\// text""#.len())
);
}
#[test]
/// 由 Rust 测试框架调用:坏字符串、未支持数字后缀/格式和未知字符在词法阶段失败,诊断端点仍是合法原文字节边界。
fn malformed_strings_numbers_and_unknown_characters_report_lexical_errors() {
// 枚举非法输入;每例必须整体失败,不能只成功消费一个前缀就返回 AST。
for source in [
"\"unfinished",
"\"bad\\q\"",
"\"bad\nline\"",
"\"bad\rline\"",
"\"bad\0\"",
"\"trailing\\",
"1e3",
"12name",
"1_000",
"'text'",
";",
"🌸",
"\0",
] {
// 要求失败并检查阶段/范围/位置,确保不是被其他解析路径误接收。
let error = lex(source).unwrap_err();
assert_eq!(error.kind, DiagnosticKind::Lexical, "{source:?}");
assert!(error.span.start <= error.span.end);
assert!(error.span.end <= source.len());
assert!(source.is_char_boundary(error.span.start));
assert!(source.is_char_boundary(error.span.end));
}
// 保留当前原始文本用于字节切片与位置比较,不规范化其中的 Unicode、注释、空白或换行。
let source = "\"日\\q\"";
// 要求失败并检查阶段/范围/位置,确保不是被其他解析路径误接收。
let error = lex(source).unwrap_err();
assert_eq!(&source[error.span.start..error.span.end], "\\q");
assert_eq!(error.location(source).column, 3);
}
#[test]
/// 由 Rust 测试框架调用:逐层拆 AST 验证乘除优先于加减、同层左结合、括号扩大节点范围及连续负号保持两层一元节点。
fn arithmetic_precedence_and_left_associativity_form_the_expected_tree() {
// 解析完整表达式得到拥有型节点,下面直接核对语法结构而非运行结果。
let expression = parse_expression("1 - 2 - 3 * 4 / 5 + 6").unwrap();
// 最外层是最后的加法,右侧是 6,左侧包含前面的减法链及乘除。
let (subtract, last) = binary(&expression, BinaryOp::Add);
number(last, "6");
// 第二次减法的左侧仍是第一次减法,体现同层左结合。
let (first_subtract, divide) = binary(subtract, BinaryOp::Subtract);
// 第一次减法直接连接数字 1 和 2。
let (first, second) = binary(first_subtract, BinaryOp::Subtract);
number(first, "1");
number(second, "2");
// 乘除同层左结合,因此除法左边是 3*4,右边是 5。
let (multiply, fifth) = binary(divide, BinaryOp::Divide);
number(fifth, "5");
// 乘法的两个叶子分别为数字 3 和 4。
let (third, fourth) = binary(multiply, BinaryOp::Multiply);
number(third, "3");
number(fourth, "4");
// 括号让加法作为乘法左操作数,且加法节点范围包含括号本身。
let grouped = parse_expression("(1 + 2) * 3").unwrap();
// 根为 Multiply,取左边的括号加法验证优先级被分组覆盖。
let (sum, _) = binary(&grouped, BinaryOp::Multiply);
binary(sum, BinaryOp::Add);
assert_eq!(sum.span, Span::new(0, 7));
// 两个负号在语法阶段保留两个节点,精确字面量符号合并属于之后的 codegen。
let nested = parse_expression("--1").unwrap();
// 当前层必须是前缀取负节点,后面的操作数继续用于检查内部树。
let ExprKind::Unary { operand, .. } = nested.kind else {
panic!("expected outer negate");
};
// 外层负号的操作数仍是 Negate,证明解析器保留两个前缀节点而未提前计算数值。
assert!(matches!(
operand.kind,
ExprKind::Unary {
operation: UnaryOp::Negate,
..
}
));
}
#[test]
/// 由 Rust 测试框架调用:取负包住整个 factory/成员/调用后缀链,逐层检查接收对象、符号实参、文本实参及属性名。
fn member_and_call_chains_bind_more_tightly_than_unary_minus() {
// 解析完整表达式得到拥有型节点,下面直接核对语法结构而非运行结果。
let expression = parse_expression("-factory(1).build(.title)(\"日\", 2)").unwrap();
// 当前层必须是前缀取负节点,后面的操作数继续用于检查内部树。
let ExprKind::Unary {
operation: UnaryOp::Negate,
operand,
} = expression.kind
else {
panic!("expected negate");
};
// 拆下一层调用的被调子树及实参;同名变量在逐层拆解时遮蔽上一层借用/拥有值。
let ExprKind::Call { callee, arguments } = operand.kind else {
panic!("expected last call");
};
assert_eq!(arguments.len(), 2);
assert_eq!(arguments[0].kind, ExprKind::String("日".into()));
// 拆下一层调用的被调子树及实参;同名变量在逐层拆解时遮蔽上一层借用/拥有值。
let ExprKind::Call { callee, arguments } = callee.kind else {
panic!("expected build call");
};
// build 的第一个参数是 .title 符号节点,不是普通名称读取或成员访问。
let ExprKind::Symbol(name) = &arguments[0].kind else {
panic!("expected a symbol");
};
assert_eq!(name.text, "title");
// 成员节点保留 build 名称,其接收对象应是 factory(1) 调用。
let ExprKind::Member { object, name } = callee.kind else {
panic!("expected member");
};
assert_eq!(name.text, "build");
// 拆下一层调用的被调子树及实参;同名变量在逐层拆解时遮蔽上一层借用/拥有值。
let ExprKind::Call { callee, arguments } = object.kind else {
panic!("expected factory call");
};
assert_eq!(callee.kind, ExprKind::Name("factory".into()));
number(&arguments[0], "1");
}
#[test]
/// 由 Rust 测试框架调用:解析示例和多函数源码,核对显式类型、绑定/返回、Unicode 原文范围及 AST 独立拥有文本。
fn parses_typed_functions_bindings_and_multiple_top_level_definitions() {
// 使用仓库原始示例,测试该文件仍是两个可独立解析的顶层函数声明。
let example = parse(include_str!("../examples/basic.endor")).unwrap();
assert_eq!(example.declarations.len(), 2);
// 保留当前原始文本用于字节切片与位置比较,不规范化其中的 Unicode、注释、空白或换行。
let source = r#"
// Basic draft syntax, including a Unicode name.
function discountedPrice(price: Number, discount: Number): Number {
let amount = price - discount
return amount
}
function 表示(): String { return "こんにちは" }
"#;
// 解析模块语法树,随后通过辅助函数核对声明、参数、语句及返回。
let program = parse(source).unwrap();
assert_eq!(program.declarations.len(), 2);
// 只借用第一个顶层函数,所有文本由 program 持有而非原文临时切片。
let first = function(&program, 0);
assert_eq!(first.name.text, "discountedPrice");
assert_eq!(first.parameters.len(), 2);
assert_eq!(first.parameters[0].type_name.text, "Number");
assert_eq!(first.return_type.text, "Number");
assert_eq!(binding(first, 0).name.text, "amount");
binary(&binding(first, 0).value, BinaryOp::Subtract);
assert_eq!(result(first).kind, ExprKind::Name("amount".into()));
assert_eq!(
&source[first.body.statements.last().unwrap().span.start
..first.body.statements.last().unwrap().span.end],
"return amount"
);
assert_eq!(
result(function(&program, 1)).kind,
ExprKind::String("こんにちは".into())
);
assert_eq!(
&source[function(&program, 1).span.start..function(&program, 1).span.end],
"function 表示(): String { return \"こんにちは\" }"
);
// The output owns its text and can outlive the input buffer.
// 内层 String 离开块后销毁,外层 Program 仍应拥有函数名和字面量文本。
let owned = {
// 保留当前原始文本用于字节切片与位置比较,不规范化其中的 Unicode、注释、空白或换行。
let source = String::from("function f(): String { return \"ok\" }");
parse(&source).unwrap()
};
assert_eq!(function(&owned, 0).name.text, "f");
}
#[test]
/// 由 Rust 测试框架调用:关键字形名称可在其他上下文作标识符,CRLF 可出现在参数/块边界而不改变树结构。
fn syntax_words_are_contextual_and_block_layout_does_not_depend_on_newlines() {
// 解析模块语法树,随后通过辅助函数核对声明、参数、语句及返回。
let program =
parse("function class(function: Type): Type { let agent = function return agent }")
.unwrap();
assert_eq!(function(&program, 0).name.text, "class");
assert_eq!(function(&program, 0).parameters[0].name.text, "function");
assert_eq!(binding(function(&program, 0), 0).name.text, "agent");
// 在语法分隔点插入 CRLF,验证空白布局不依赖特定换行格式。
let multiline =
parse("function f\r\n(\r\nx: Number\r\n): Number\r\n{\r\nreturn x\r\n}").unwrap();
assert_eq!(
result(function(&multiline, 0)).kind,
ExprKind::Name("x".into())
);
}
#[test]
/// 由 Rust 测试框架调用:模块与表达式都拒绝空输入、剩余 Token、缺少必需语法及当前未支持形式。
fn parser_requires_complete_input_and_rejects_unimplemented_or_malformed_syntax() {
// 枚举非法输入;每例必须整体失败,不能只成功消费一个前缀就返回 AST。
for source in [
"",
"// empty",
"let x = 1",
"use atelier:shop.data",
"agent Worker {}",
"function f() { return 1 }",
"function f(x): Number { return x }",
"function f(x: Number,): Number { return x }",
"function f(): Number { return 1 let x = 2 }",
"function f(): Number { return 1 } extra",
"function f(): Number { function g(): Number { return 1 } return 2 }",
] {
assert!(parse(source).is_err(), "{source}");
}
// 枚举非法输入;每例必须整体失败,不能只成功消费一个前缀就返回 AST。
for source in [
"", "x y", "1 +", "+1", ".5", "1.", "f(1,)", "f(,1)", "f(1 2)", "(1", "obj.", "[1]",
"1.2.3",
] {
assert!(parse_expression(source).is_err(), "{source}");
}
}
#[test]
/// 由 Rust 测试框架调用:语法 Token 索引准确转回原文违规字符或 EOF 空范围,CRLF 和 Unicode 不破坏显示行列。
fn syntax_errors_map_token_indexes_to_utf8_source_and_eof_locations() {
// 保留当前原始文本用于字节切片与位置比较,不规范化其中的 Unicode、注释、空白或换行。
let source = "function f(): String {\r\n let 商品 = \"日\"\r\n return )\r\n}";
// 要求失败并检查阶段/范围/位置,确保不是被其他解析路径误接收。
let error = parse(source).unwrap_err();
assert_eq!(error.kind, DiagnosticKind::Syntax);
assert_eq!(&source[error.span.start..error.span.end], ")");
assert_eq!(error.location(source).line, 3);
assert_eq!(error.location(source).column, 10);
assert!(!error.expected.is_empty());
// 保留当前原始文本用于字节切片与位置比较,不规范化其中的 Unicode、注释、空白或换行。
let source = "function f(): Number { return 1\r\n ";
// 要求失败并检查阶段/范围/位置,确保不是被其他解析路径误接收。
let error = parse(source).unwrap_err();
assert_eq!(error.span, Span::new(source.len(), source.len()));
assert_eq!(error.location(source).line, 2);
assert_eq!(error.location(source).column, 3);
// 保留当前原始文本用于字节切片与位置比较,不规范化其中的 Unicode、注释、空白或换行。
let source = "\r\n\r\n🌸";
// 要求失败并检查阶段/范围/位置,确保不是被其他解析路径误接收。
let error = lex(source).unwrap_err();
assert_eq!(error.location(source).line, 3);
assert_eq!(error.location(source).column, 1);
}