docs(frontend): explain reader and syntax regression scenarios
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
// frontend/tests/syntax.rs 解析此模块并检查两个声明;这里只演示 AST 输入,不自动调用函数或执行数值运算。
|
||||
// Parsing produces an AST; it does not execute these functions.
|
||||
// 供语法测试解析的恒等函数,显式 String 参数与返回注解均保存在 AST 中。
|
||||
function echo(text: String): String {
|
||||
// let 绑定把参数读取存为独立 Binding 节点,供前端检查语句结构。
|
||||
let result = text
|
||||
// 末尾 return 引用 result,解析后保留名称及原文范围。
|
||||
return result
|
||||
}
|
||||
|
||||
// 供语法测试解析的双参数函数;参数和返回类型名仅作为注解文本,不在前端解析其身份。
|
||||
function discountedPrice(price: Number, discount: Number): Number {
|
||||
// 减法在 AST 中是包含两个名称读取的 Binary 节点,此示例解析过程不计算结果。
|
||||
return price - discount
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//! Rust 测试框架验证 Reader 适配器:完整 UTF-8 快照、来源/版本身份、错误阶段和宿主 URI 授权边界。
|
||||
use std::cell::Cell;
|
||||
use std::error::Error;
|
||||
use std::io::{self, Cursor, Read};
|
||||
@@ -9,19 +10,26 @@ use frontend::{
|
||||
SourceReadErrorKind, lex_source, parse_source, parse_source_expression,
|
||||
};
|
||||
|
||||
/// 流测试包装器,让 read_to_end 每次最多取得一个字节,强制 UTF-8 字符跨多次底层读取。
|
||||
struct OneByte<R>(R);
|
||||
|
||||
impl<R: Read> Read for OneByte<R> {
|
||||
/// Reader 的 read_to_end 调用此适配器,把一次底层读取限制为至多一个字节。
|
||||
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
|
||||
// 空输出缓冲保持零长度,其余读取都限制为一个字节,不能按单次读取独立解码。
|
||||
let count = buffer.len().min(1);
|
||||
self.0.read(&mut buffer[..count])
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// 由 Rust 测试框架调用:文本与单字节流保留相同 CRLF/Unicode 原文并解析成相同 AST/Token;空流可读取但空模块无效,BOM 保留到词法阶段拒绝。
|
||||
fn strings_and_split_utf8_streams_produce_the_same_ast_and_exact_text() {
|
||||
// 保留注释、CRLF、日文和四字节 emoji,使原文精确性及跨读取解码同时受检。
|
||||
let text = "// 日本語\r\nfunction 表示(): String { return \"こんにちは🌸\" }\r\n";
|
||||
// String 文本已是 UTF-8,Reader 只包装拥有快照和显示名。
|
||||
let memory = EndorReader::from_text("<editor>", text);
|
||||
// 同一字节序列经 OneByte 拆分读取,Reader 应先完整聚合再验证 UTF-8。
|
||||
let stream = EndorReader::from_stream("<pipe>", OneByte(Cursor::new(text.as_bytes()))).unwrap();
|
||||
assert_eq!(memory.text(), text);
|
||||
assert_eq!(stream.text(), text);
|
||||
@@ -42,22 +50,28 @@ fn strings_and_split_utf8_streams_produce_the_same_ast_and_exact_text() {
|
||||
}
|
||||
);
|
||||
assert_eq!(lex_source(&memory).unwrap(), lex_source(&stream).unwrap());
|
||||
// 空字节流是合法 UTF-8 快照,是否为有效模块由后续语法阶段决定。
|
||||
let empty = EndorReader::from_stream("<empty>", Cursor::new(b"")).unwrap();
|
||||
assert!(empty.text().is_empty());
|
||||
assert_eq!(
|
||||
parse_source(&empty).unwrap_err().diagnostic.kind,
|
||||
DiagnosticKind::Syntax
|
||||
);
|
||||
// Reader 不剥除 BOM,词法器看到原始开头字符后才决定是否接受。
|
||||
let bom = EndorReader::from_stream("<bom>", Cursor::new("\u{feff}1".as_bytes())).unwrap();
|
||||
assert_eq!(bom.text(), "\u{feff}1");
|
||||
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
|
||||
let error = lex_source(&bom).unwrap_err();
|
||||
assert_eq!(error.diagnostic.kind, DiagnosticKind::Lexical);
|
||||
assert_eq!(error.diagnostic.span.end, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// 由 Rust 测试框架调用:语法与词法错误保留原始字节跨度、会话名及 Unicode 行列,源快照销毁后诊断仍可显示并提供错误链。
|
||||
fn source_diagnostics_keep_session_names_unicode_locations_and_original_offsets() {
|
||||
// 建立带来源名的原文快照,后续错误必须定位这份文本而非临时转换后的内容。
|
||||
let source = EndorReader::from_text("<session:one/input:2>", "\"日\" + )");
|
||||
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
|
||||
let error = parse_source_expression(&source).unwrap_err();
|
||||
assert_eq!(error.location.line, 1);
|
||||
assert_eq!(error.location.column, 7);
|
||||
@@ -66,11 +80,14 @@ fn source_diagnostics_keep_session_names_unicode_locations_and_original_offsets(
|
||||
")"
|
||||
);
|
||||
assert_eq!(error.origin, source.origin().clone());
|
||||
// 丢弃文本快照后仍应能显示已固化的来源和行列,不依赖借用的源码缓冲。
|
||||
drop(source);
|
||||
assert!(error.to_string().starts_with("<session:one/input:2>:1:7:"));
|
||||
assert!(error.source().is_some());
|
||||
|
||||
// 建立带来源名的原文快照,后续错误必须定位这份文本而非临时转换后的内容。
|
||||
let source = EndorReader::from_text("<editor>", "\r\n\"日\\q\"");
|
||||
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
|
||||
let error = lex_source(&source).unwrap_err();
|
||||
assert_eq!(error.diagnostic.kind, DiagnosticKind::Lexical);
|
||||
assert_eq!(error.location.line, 2);
|
||||
@@ -79,8 +96,11 @@ fn source_diagnostics_keep_session_names_unicode_locations_and_original_offsets(
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// 由 Rust 测试框架调用:非法字节、截断多字节字符及合法前缀后非法字节都在读取解码阶段拒绝,不作有损替换。
|
||||
fn incomplete_or_invalid_utf8_is_rejected_without_replacement_characters() {
|
||||
// 覆盖无效起始字节、不完整 UTF-8 序列,以及一个合法 ASCII 前缀后的错误。
|
||||
for bytes in [vec![0xff], vec![0xe6, 0x97], vec![b'a', 0xff]] {
|
||||
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
|
||||
let error = EndorReader::from_stream("<bytes>", OneByte(Cursor::new(bytes))).unwrap_err();
|
||||
assert!(matches!(error.kind, SourceReadErrorKind::InvalidUtf8(_)));
|
||||
assert!(error.to_string().contains("<bytes>: invalid UTF-8 at byte"));
|
||||
@@ -88,13 +108,17 @@ fn incomplete_or_invalid_utf8_is_rejected_without_replacement_characters() {
|
||||
}
|
||||
}
|
||||
|
||||
/// 模拟中途失败的流;布尔值记录是否已返回过第一个合法字节。
|
||||
struct FailingStream(bool);
|
||||
|
||||
impl Read for FailingStream {
|
||||
/// Reader 的 read_to_end 调用;先成功一次再失败,检验中途错误不会变成部分成功。
|
||||
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
|
||||
// 空缓冲不消耗模拟状态,符合 Read 的零长度读取约定。
|
||||
if buffer.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
// 第一次非空读取给出 x,后续非空读取才返回 IO 错误。
|
||||
if !self.0 {
|
||||
self.0 = true;
|
||||
buffer[0] = b'x';
|
||||
@@ -105,18 +129,24 @@ impl Read for FailingStream {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// 由 Rust 测试框架调用:读到一个有效字节后发生 IO 失败时仍整体失败,不向解析器提供半份快照。
|
||||
fn a_failed_stream_never_returns_partially_read_source() {
|
||||
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
|
||||
let error = EndorReader::from_stream("<failed>", FailingStream(false)).unwrap_err();
|
||||
assert!(matches!(error.kind, SourceReadErrorKind::Io(_)));
|
||||
assert!(error.to_string().contains("stream failure"));
|
||||
}
|
||||
|
||||
// 并行文件测试使用进程内递增编号避免重名;这里只需要原子唯一性,不发布共享数据。
|
||||
static NEXT_FILE: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// 本文件测试用的临时文件拥有者;构造写入字节,Drop 尽力删除,不留测试文件。
|
||||
struct SourceFile(PathBuf);
|
||||
|
||||
impl SourceFile {
|
||||
/// 文件测试创建临时输入;路径组合进程 ID 和原子编号,写入失败直接使测试失败。
|
||||
fn new(bytes: &[u8]) -> Self {
|
||||
// 放在系统临时目录,不依赖当前仓库布局,同一进程的不同测试实例用不同编号。
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"aster-reader-{}-{}.endor",
|
||||
std::process::id(),
|
||||
@@ -128,14 +158,18 @@ impl SourceFile {
|
||||
}
|
||||
|
||||
impl Drop for SourceFile {
|
||||
/// 退出文件测试作用域时清理文件,忽略已不存在等删除错误,避免 Drop 再次失败。
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_file(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// 由 Rust 测试框架调用:文件保留原路径及 CRLF,坏编码与文件不存在分别产生读取编码错误和 NotFound IO 错误。
|
||||
fn file_sources_preserve_the_path_and_read_errors_are_not_lexical_errors() {
|
||||
// 建立合法模块临时文件,用作成功读取及路径来源的基线。
|
||||
let file = SourceFile::new(b"function f(): Number { return 1 }\r\n");
|
||||
// 建立带来源名的原文快照,后续错误必须定位这份文本而非临时转换后的内容。
|
||||
let source = EndorReader::from_file(&file.0).unwrap();
|
||||
assert_eq!(
|
||||
source.origin(),
|
||||
@@ -145,25 +179,34 @@ fn file_sources_preserve_the_path_and_read_errors_are_not_lexical_errors() {
|
||||
);
|
||||
assert!(source.text().ends_with("\r\n"));
|
||||
assert!(parse_source(&source).is_ok());
|
||||
// 单字节 0xff 不是 UTF-8,打开文件成功后应在解码阶段拒绝。
|
||||
let invalid = SourceFile::new(&[0xff]);
|
||||
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
|
||||
let error = EndorReader::from_file(&invalid.0).unwrap_err();
|
||||
assert!(matches!(error.kind, SourceReadErrorKind::InvalidUtf8(_)));
|
||||
assert!(error.to_string().contains(&invalid.0.display().to_string()));
|
||||
// 从基线路径换一个未创建的扩展名,验证文件不存在作为 IO 错误暴露。
|
||||
let missing = file.0.with_extension("missing");
|
||||
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
|
||||
let error = EndorReader::from_file(&missing).unwrap_err();
|
||||
// 必须能解构出原始 IO 错误,之后检查 NotFound 或 PermissionDenied 的具体种类。
|
||||
let SourceReadErrorKind::Io(error) = error.kind else {
|
||||
panic!("expected I/O error");
|
||||
};
|
||||
assert_eq!(error.kind(), io::ErrorKind::NotFound);
|
||||
}
|
||||
|
||||
/// URI 测试的可计数宿主,每次 load 返回不同版本和源码,用来检测隐式重复解析。
|
||||
struct Provider {
|
||||
// &self 的 load 通过 Cell 累计调用次数,不要求多线程共享。
|
||||
calls: Cell<usize>,
|
||||
}
|
||||
|
||||
impl EndorSourceProvider for Provider {
|
||||
/// from_endor 调用,检查定位符并累计解析次数,返回新版本拥有字节。
|
||||
fn load(&self, uri: &str) -> io::Result<ResolvedEndorSource> {
|
||||
assert_eq!(uri, "endor:workspace/namespace/function");
|
||||
// 每次解析选择新的版本编号,同时反映在标签与源码返回值中。
|
||||
let version = self.calls.get() + 1;
|
||||
self.calls.set(version);
|
||||
Ok(ResolvedEndorSource {
|
||||
@@ -174,12 +217,17 @@ impl EndorSourceProvider for Provider {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// 由 Rust 测试框架调用:同一 URI 每次读取只调用一次宿主,后续解析不再解析 URI,旧快照不被新版本改写。
|
||||
fn each_uri_read_resolves_once_and_old_snapshots_keep_their_original_revision() {
|
||||
// 为本场景新建计数宿主,不与其他 URI 测试共享解析次数。
|
||||
let provider = Provider {
|
||||
calls: Cell::new(0),
|
||||
};
|
||||
// URI 作为宿主可解释的定位符,Reader 不把 namespace/function 猜成磁盘路径。
|
||||
let uri = "endor:workspace/namespace/function";
|
||||
// 第一份快照保存第一次解析的原文及 revision-1。
|
||||
let first = EndorReader::from_endor(uri, &provider).unwrap();
|
||||
// 同一 URI 再读取得第二份独立快照,不能回写第一份文本或来源。
|
||||
let second = EndorReader::from_endor(uri, &provider).unwrap();
|
||||
assert_eq!(provider.calls.get(), 2);
|
||||
assert!(first.text().contains("return 1"));
|
||||
@@ -203,12 +251,16 @@ fn each_uri_read_resolves_once_and_old_snapshots_keep_their_original_revision()
|
||||
assert_eq!(provider.calls.get(), 2); // Parsing never resolves the URI again.
|
||||
}
|
||||
|
||||
/// 错误测试的固定宿主,按指定版本标签和字节返回,Reader 仍须自行校验协议及编码。
|
||||
struct StaticProvider {
|
||||
// 借用测试静态版本标签,load 时复制为拥有字符串。
|
||||
revision: &'static str,
|
||||
// 借用测试静态字节,load 时复制给 Reader 解码。
|
||||
bytes: &'static [u8],
|
||||
}
|
||||
|
||||
impl EndorSourceProvider for StaticProvider {
|
||||
/// from_endor 调用,把测试指定标签与字节复制到结果,不提前替 Reader 做编码/版本验证。
|
||||
fn load(&self, _uri: &str) -> io::Result<ResolvedEndorSource> {
|
||||
Ok(ResolvedEndorSource {
|
||||
revision: self.revision.into(),
|
||||
@@ -218,16 +270,22 @@ impl EndorSourceProvider for StaticProvider {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// 由 Rust 测试框架调用:URI 结果必须有非空版本身份;语法错误与编码错误都显示实际解析版本,空白标签被拒绝。
|
||||
fn uri_snapshots_require_a_revision_and_preserve_it_in_source_diagnostics() {
|
||||
// URI 作为宿主可解释的定位符,Reader 不把 namespace/function 猜成磁盘路径。
|
||||
let uri = "endor:workspace/namespace/function";
|
||||
// 使用合法版本身份但语法错误的源码,错误显示应继续包含该版本。
|
||||
let provider = StaticProvider {
|
||||
revision: "rev-one",
|
||||
bytes: b"function f(): Number { return ) }",
|
||||
};
|
||||
// 建立带来源名的原文快照,后续错误必须定位这份文本而非临时转换后的内容。
|
||||
let source = EndorReader::from_endor(uri, &provider).unwrap();
|
||||
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
|
||||
let error = parse_source(&source).unwrap_err();
|
||||
assert_eq!(error.origin, source.origin().clone());
|
||||
assert!(error.to_string().contains("[revision rev-one]"));
|
||||
// 版本全为空白,即使字节是有效源码也必须在创建 URI 来源前拒绝。
|
||||
let missing = StaticProvider {
|
||||
revision: " \t",
|
||||
bytes: b"1",
|
||||
@@ -236,18 +294,23 @@ fn uri_snapshots_require_a_revision_and_preserve_it_in_source_diagnostics() {
|
||||
EndorReader::from_endor(uri, &missing).unwrap_err().kind,
|
||||
SourceReadErrorKind::MissingRevision
|
||||
));
|
||||
// 版本有效但字节非法,编码失败说明中也应保留已解析版本身份。
|
||||
let invalid = StaticProvider {
|
||||
revision: "rev-two",
|
||||
// 借用测试静态字节,load 时复制给 Reader 解码。
|
||||
bytes: &[0xff],
|
||||
};
|
||||
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
|
||||
let error = EndorReader::from_endor(uri, &invalid).unwrap_err();
|
||||
assert!(matches!(error.kind, SourceReadErrorKind::InvalidUtf8(_)));
|
||||
assert!(error.to_string().contains("[revision rev-two]"));
|
||||
}
|
||||
|
||||
/// 宿主拒绝授权的模拟,不提供任何替代源码或路径。
|
||||
struct DeniedProvider;
|
||||
|
||||
impl EndorSourceProvider for DeniedProvider {
|
||||
/// from_endor 调用,始终返回宿主 PermissionDenied,禁止任何本地文件回退。
|
||||
fn load(&self, _uri: &str) -> io::Result<ResolvedEndorSource> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
@@ -257,10 +320,13 @@ impl EndorSourceProvider for DeniedProvider {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// 由 Rust 测试框架调用:基础非法 URI 不触发宿主,合法 URI 被宿主拒绝时保留 PermissionDenied,不改走文件读取。
|
||||
fn invalid_uris_do_not_call_the_provider_and_denials_do_not_fall_back_to_files() {
|
||||
// 为本场景新建计数宿主,不与其他 URI 测试共享解析次数。
|
||||
let provider = Provider {
|
||||
calls: Cell::new(0),
|
||||
};
|
||||
// 覆盖空定位符、空地址、错误 scheme、空白和控制字符,均应在访问 provider 前被拒绝。
|
||||
for uri in [
|
||||
"",
|
||||
"endor:",
|
||||
@@ -275,7 +341,9 @@ fn invalid_uris_do_not_call_the_provider_and_denials_do_not_fall_back_to_files()
|
||||
));
|
||||
}
|
||||
assert_eq!(provider.calls.get(), 0);
|
||||
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
|
||||
let error = EndorReader::from_endor("endor:anywhere", &DeniedProvider).unwrap_err();
|
||||
// 必须能解构出原始 IO 错误,之后检查 NotFound 或 PermissionDenied 的具体种类。
|
||||
let SourceReadErrorKind::Io(error) = error.kind else {
|
||||
panic!("expected provider error");
|
||||
};
|
||||
|
||||
@@ -1,30 +1,39 @@
|
||||
//! 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,
|
||||
@@ -37,13 +46,17 @@ fn binary(expression: &Expr, expected: BinaryOp) -> (&Expr, &Expr) {
|
||||
(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);
|
||||
@@ -51,6 +64,7 @@ fn unicode_tokens_refer_to_exact_original_byte_ranges() {
|
||||
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));
|
||||
@@ -58,8 +72,11 @@ fn unicode_tokens_refer_to_exact_original_byte_ranges() {
|
||||
}
|
||||
|
||||
#[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<_>>(),
|
||||
@@ -74,8 +91,11 @@ fn decimal_spelling_and_minus_tokens_are_preserved_without_float_conversion() {
|
||||
&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,
|
||||
@@ -88,7 +108,9 @@ fn decimal_spelling_and_minus_tokens_are_preserved_without_float_conversion() {
|
||||
}
|
||||
|
||||
#[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,
|
||||
@@ -101,7 +123,9 @@ fn strings_decode_only_the_supported_escapes_and_keep_comments_inside_strings()
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// 由 Rust 测试框架调用:坏字符串、未支持数字后缀/格式和未知字符在词法阶段失败,诊断端点仍是合法原文字节边界。
|
||||
fn malformed_strings_numbers_and_unknown_characters_report_lexical_errors() {
|
||||
// 枚举非法输入;每例必须整体失败,不能只成功消费一个前缀就返回 AST。
|
||||
for source in [
|
||||
"\"unfinished",
|
||||
"\"bad\\q\"",
|
||||
@@ -117,6 +141,7 @@ fn malformed_strings_numbers_and_unknown_characters_report_lexical_errors() {
|
||||
"🌸",
|
||||
"\0",
|
||||
] {
|
||||
// 要求失败并检查阶段/范围/位置,确保不是被其他解析路径误接收。
|
||||
let error = lex(source).unwrap_err();
|
||||
assert_eq!(error.kind, DiagnosticKind::Lexical, "{source:?}");
|
||||
assert!(error.span.start <= error.span.end);
|
||||
@@ -124,31 +149,44 @@ fn malformed_strings_numbers_and_unknown_characters_report_lexical_errors() {
|
||||
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");
|
||||
};
|
||||
@@ -162,8 +200,11 @@ fn arithmetic_precedence_and_left_associativity_form_the_expected_tree() {
|
||||
}
|
||||
|
||||
#[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,
|
||||
@@ -171,22 +212,27 @@ fn member_and_call_chains_bind_more_tightly_than_unary_minus() {
|
||||
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");
|
||||
};
|
||||
@@ -195,9 +241,12 @@ fn member_and_call_chains_bind_more_tightly_than_unary_minus() {
|
||||
}
|
||||
|
||||
#[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 {
|
||||
@@ -206,8 +255,10 @@ fn parses_typed_functions_bindings_and_multiple_top_level_definitions() {
|
||||
}
|
||||
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);
|
||||
@@ -230,7 +281,9 @@ fn parses_typed_functions_bindings_and_multiple_top_level_definitions() {
|
||||
"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()
|
||||
};
|
||||
@@ -238,13 +291,16 @@ fn parses_typed_functions_bindings_and_multiple_top_level_definitions() {
|
||||
}
|
||||
|
||||
#[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!(
|
||||
@@ -254,7 +310,9 @@ fn syntax_words_are_contextual_and_block_layout_does_not_depend_on_newlines() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// 由 Rust 测试框架调用:模块与表达式都拒绝空输入、剩余 Token、缺少必需语法及当前未支持形式。
|
||||
fn parser_requires_complete_input_and_rejects_unimplemented_or_malformed_syntax() {
|
||||
// 枚举非法输入;每例必须整体失败,不能只成功消费一个前缀就返回 AST。
|
||||
for source in [
|
||||
"",
|
||||
"// empty",
|
||||
@@ -270,6 +328,7 @@ fn parser_requires_complete_input_and_rejects_unimplemented_or_malformed_syntax(
|
||||
] {
|
||||
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",
|
||||
@@ -279,20 +338,27 @@ fn parser_requires_complete_input_and_rejects_unimplemented_or_malformed_syntax(
|
||||
}
|
||||
|
||||
#[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);
|
||||
|
||||
Reference in New Issue
Block a user