//! 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![ 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"); }; 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); }