From 3a75476bc59cdad72f6853ba80bd6a4cd6032c16 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 15 Nov 2026 18:42:54 +0800 Subject: [PATCH] docs(frontend): explain source readers and PEG grammar rules --- crates/frontend/src/grammar.peg | 63 +++++++++++++++++++++++++++++++++ crates/frontend/src/lexer.peg | 23 ++++++++++++ crates/frontend/src/reader.rs | 35 ++++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/crates/frontend/src/grammar.peg b/crates/frontend/src/grammar.peg index 914faa3..9e067d8 100644 --- a/crates/frontend/src/grammar.peg +++ b/crates/frontend/src/grammar.peg @@ -1,79 +1,110 @@ +// parser.rs 编译时引入,生成 syntax 的 Token 解析规则;source 是这些 Token 对应的原始 UTF-8 文本。 +// 规则只构造拥有文本及原文跨度的 AST,不解析名称/类型,也不执行函数或操作 VM。 peg::parser! { + // parse/parse_expression/parse_ast 的交互分支调用相应公开根,默认要求完整消费 Token 输入。 grammar syntax(source: &str) for [Token] { + // 各语法上下文按原文精确匹配 Name;词法器不把这些词全局保留为独立关键字种类。 rule keyword(word: &str) -> Span = [Token { kind: TokenKind::Name, span } if &source[span.start..span.end] == word] { span } + // function 规则的开头;quiet 隐藏内部 Token 细节,失败期望显示可读的 function。 rule function_keyword() -> Span = quiet!{keyword("function")} / expected!("'function'") + // binding 的开头,同样提供稳定可读的期望项。 rule let_keyword() -> Span = quiet!{keyword("let")} / expected!("'let'") + // return_statement 的开头及 local_statement 的负向前瞻,避免 return 被普通表达式语句消费。 rule return_keyword() -> Span = quiet!{keyword("return")} / expected!("'return'") + // 声明、类型名及表达式引用共用;从 Token 范围复制名称文本,AST 不借用原源码缓冲。 rule name() -> Name = span:quiet!{[Token { kind: TokenKind::Name, span }] { span }} { Name { text: source[span.start..span.end].into(), span } } / expected!("identifier") + // 函数参数、调用及括号表达式复用左括号匹配,返回范围供组合节点使用。 rule lparen() -> Span = quiet!{[Token { kind: TokenKind::LeftParen, span }] { span }} / expected!("'('") + // 参数/调用/括号表达式复用右括号匹配,其终点可成为完整表达式终点。 rule rparen() -> Span = quiet!{[Token { kind: TokenKind::RightParen, span }] { span }} / expected!("')'") + // 类和 block 的左边界,缺失时报告可读的左花括号期望项。 rule lbrace() -> Span = quiet!{[Token { kind: TokenKind::LeftBrace, span }] { span }} / expected!("'{'") + // 类和 block 的右边界,节点跨度包含这个闭合括号。 rule rbrace() -> Span = quiet!{[Token { kind: TokenKind::RightBrace, span }] { span }} / expected!("'}'") + // 类型注解和类继承共用冒号,不额外构造 AST 节点。 rule colon() = quiet!{[Token { kind: TokenKind::Colon, .. }]} / expected!("':'") + // 参数/实参列表分隔;列表组合器决定空列表及分隔规则。 rule comma() = quiet!{[Token { kind: TokenKind::Comma, .. }]} / expected!("','") + // 绑定和赋值共用单等号 Token,不接受相等比较 Token。 rule equal() = quiet!{[Token { kind: TokenKind::Equal, .. }]} / expected!("'='") + // 成员后缀和符号字面量共用点,返回其范围以保留符号起点。 rule dot() -> Span = quiet!{[Token { kind: TokenKind::Dot, span }] { span }} / expected!("'.'") + // 加法优先级层消费 Plus Token。 rule plus() = [Token { kind: TokenKind::Plus, .. }] + // 减法及一元取负共用 Minus,前缀负号需要它的跨度作为节点起点。 rule minus() -> Span = [Token { kind: TokenKind::Minus, span }] { span } + // 乘法优先级层消费 Star Token。 rule star() = [Token { kind: TokenKind::Star, .. }] + // 除法优先级层消费 Slash,注释已在词法 trivia 中跳过。 rule slash() = [Token { kind: TokenKind::Slash, .. }] + // parse 的模块入口;至少一个顶层函数或类声明,默认拒绝剩余 Token。 pub rule program() -> Program = declarations:declaration()+ { + // 声明列表由 + 保证非空,根范围从首声明起点延伸到末声明终点。 let span = declarations[0].span.through(declarations.last().unwrap().span); Program { declarations, span } } + // program/interactive_statement 的声明分支;函数直接包装,类则收集基类及成员。 rule declaration() -> Decl = function:function() { Decl { span: function.span, kind: DeclKind::Function(function) } } / start:keyword("class") name:name() base:(colon() base:name() { base })? lbrace() members:class_member()* end:rbrace() { + // 按字段出现顺序收集字段参数结构,类的布局稍后由编译器及 VM 解释。 let mut fields = Vec::new(); + // 按方法出现顺序收集函数结构,与字段分开存储但不改变各自顺序。 let mut methods = Vec::new(); + // 解析阶段先统一取得成员,再按中间分类分发到两份 AST 列表。 for member in members { + // 字段和方法保留各自原节点及跨度,不在这里检查成员重名或 self。 match member { ClassMember::Field(field) => fields.push(field), ClassMember::Method(method) => methods.push(method), } } + // 类范围包括 class 开头与闭合花括号,中间空白/注释仍属于原文范围。 let span = start.through(end); Decl { span, kind: DeclKind::Class(Class { name, base, fields, methods, span }) } } + // declaration 的类体调用,先尝试完整方法,再尝试名称加类型的字段。 rule class_member() -> ClassMember = function:function() { ClassMember::Method(function) } / field:parameter() { ClassMember::Field(field) } + // parse_ast 的交互入口;恰好一个完整语句包装为列表,不管理多次提交的环境。 pub rule interactive() -> InteractiveInput = statement:interactive_statement() { InteractiveInput { span: statement.span, statements: vec![statement] } } + // 交互允许声明、控制流、let、属性/普通赋值及表达式,不提供根级 return。 rule interactive_statement() -> Stmt = declaration:declaration() { Stmt { span: declaration.span, kind: StmtKind::Declaration(declaration) } @@ -86,24 +117,30 @@ peg::parser! { Stmt { span: expression.span, kind: StmtKind::Expression(expression) } } + // 顶层和类方法共用函数语法,显式参数类型、返回类型及函数体;self 合法性留给分析。 rule function() -> Function = start:function_keyword() name:name() lparen() parameters:(parameter() ** comma()) rparen() colon() return_type:name() body:block() { Function { span: start.through(body.span), name, parameters, return_type, body } } + // 函数参数或类字段共用名称/类型名节点,语法只保留注解文本而不解析类型身份。 rule parameter() -> Parameter = name:name() colon() type_name:name() { Parameter { span: name.span.through(type_name.span), name, type_name } } + // function 和控制流共用块语法,普通语句后可接一个末尾 return;return 当前要求表达式。 rule block() -> Block = start:lbrace() locals:local_statement()* returned:return_statement()? end:rbrace() { + // 先保留普通语句序列,下面仅在存在末尾 return 时追加,保持源码顺序。 let mut statements = locals; + // 可选 return 不会被当作普通表达式消费,也不能后接其他块内语句。 if let Some(returned) = returned { statements.push(returned); } Block { statements, span: start.through(end) } } + // 块内语句分派;成员赋值先于普通赋值,最后表达式分支显式排除 return 前缀。 rule local_statement() -> Stmt = control:control_statement() { control } / binding:binding() { binding_statement(binding) } @@ -113,26 +150,34 @@ peg::parser! { Stmt { span: expression.span, kind: StmtKind::Expression(expression) } } + // 交互或块内解析 if/else、while,条件和嵌套 block 都保留为语法树,尚不推断初始化。 rule control_statement() -> Stmt = start:keyword("if") condition:expression() body:block() otherwise:(keyword("else") body:block() { body })? { + // if 范围有 else 时延伸到 else 块末尾,否则只延伸到 then 块末尾。 let span=start.through(otherwise.as_ref().map_or(body.span,|b|b.span)); Stmt {span,kind:StmtKind::If(IfStatement {condition,body,otherwise,span})} } / start:keyword("while") condition:expression() body:block() { + // while 范围覆盖关键字、条件及整个循环体。 let span=start.through(body.span); Stmt {span,kind:StmtKind::While(WhileStatement {condition,body,span})} } + // 交互或局部普通赋值;允许语法上未声明目标,具体全局/局部规则由 compiler::analyze 决定。 rule assignment() -> Stmt = name:name() equal() value:expression() { + // 赋值语句的原文范围从目标名延伸到右值末尾。 let span = name.span.through(value.span); Stmt { span, kind: StmtKind::Assignment(Assignment { name, value, span }) } } + // 先解析后缀目标再检查其最外层确为 Member;错误目标使本规则失败,不执行对象写入。 rule member_assignment() -> Stmt = target:postfix_expression() equal() value:expression() {? + // 属性赋值保留整个接收对象/属性链及右值的原文范围。 let span = target.span.through(value.span); + // 只有成员访问可拆成接收对象与字段名,裸名字留给普通赋值分支识别。 match target.kind { ExprKind::Member { object, name } => Ok(Stmt { span, kind: StmtKind::MemberAssignment(MemberAssignment { object: *object, name, value, span }), @@ -141,18 +186,23 @@ peg::parser! { } } + // 交互/局部 let 绑定,范围包括 let 关键字;重复名和右值初始化检查在后续语义分析。 rule binding() -> Binding = start:let_keyword() name:name() equal() value:expression() { Binding { span: start.through(value.span), name, value } } + // block 的可选末尾返回,语法要求一个表达式,构造 ReturnStatement 的 Some 值。 rule return_statement() -> Stmt = start:return_keyword() value:expression() { + // return 范围从关键字到返回表达式末尾,包括其间空白。 let span = start.through(value.span); Stmt { span, kind: StmtKind::Return(ReturnStatement { value: Some(value), span }) } } + // parse_expression 公开入口及各子表达式共用;precedence 层由低到高组织,二元操作在同层左结合。 pub rule expression() -> Expr = precedence! { + // 比较层优先级最低;六种比较均复用 binary 包装左右节点和范围。 left:(@) [Token {kind:TokenKind::EqualEqual,..}] right:@ {binary(left,BinaryOp::Equal,right)} left:(@) [Token {kind:TokenKind::NotEqual,..}] right:@ {binary(left,BinaryOp::NotEqual,right)} left:(@) [Token {kind:TokenKind::Less,..}] right:@ {binary(left,BinaryOp::Less,right)} @@ -160,42 +210,55 @@ peg::parser! { left:(@) [Token {kind:TokenKind::Greater,..}] right:@ {binary(left,BinaryOp::Greater,right)} left:(@) [Token {kind:TokenKind::GreaterEqual,..}] right:@ {binary(left,BinaryOp::GreaterEqual,right)} -- + // 加减层高于比较、低于乘除,同层按左结合嵌套。 left:(@) plus() right:@ { binary(left, BinaryOp::Add, right) } left:(@) minus() right:@ { binary(left, BinaryOp::Subtract, right) } -- + // 乘除层先构造节点,再交给外围的加减/比较层。 left:(@) star() right:@ { binary(left, BinaryOp::Multiply, right) } left:(@) slash() right:@ { binary(left, BinaryOp::Divide, right) } -- + // 前缀 await 构造等待节点,是否允许停放由执行上下文决定。 start:keyword("await") operand:@ { Expr { span: start.through(operand.span), kind: ExprKind::Await(Box::new(operand)) } } + // 前缀取负包住当前高优先级操作数,不在语法阶段计算数值。 start:minus() operand:@ { Expr { span: start.through(operand.span), kind: ExprKind::Unary { operation: UnaryOp::Negate, operand: Box::new(operand) } } } -- + // 后缀表达式位于最高优先级层,先形成调用/成员链,再被一元或二元节点包裹。 expression:postfix_expression() { expression } } + // 优先级最高的 primary 后接零或多个成员/调用后缀,从左到右 fold 保持链式求值结构。 rule postfix_expression() -> Expr = expression:primary() suffixes:suffix()* { + // parser::postfix 每次把先前结果作为接收对象或被调用表达式,构造 a.b(c).d 一类链。 suffixes.into_iter().fold(expression, postfix) } + // postfix_expression 共用后缀;点后取 Name,调用保存参数列表及右括号范围。 rule suffix() -> Suffix = dot() name:name() { Suffix::Member(name) } / lparen() arguments:(expression() ** comma()) end:rparen() { Suffix::Call(arguments, end) } + // 原子表达式:括号、符号、数字、字符串或名称;AST 文本均从匹配 Token 原文复制/解码。 rule primary() -> Expr = start:lparen() expression:expression() end:rparen() { + // 括号不新增语义节点种类,只把子表达式范围扩大到两侧括号。 Expr { span: start.through(end), ..expression } } / start:dot() name:name() { + // 点开头的符号与成员后缀不同;前端保留它,编译器当前尚不支持生成该运行时值。 Expr { span: start.through(name.span), kind: ExprKind::Symbol(name) } } / [Token { kind: TokenKind::Number, span }] { + // 数值保存原始十进制拼写,精确解析与负号处理留给 codegen。 Expr { span, kind: ExprKind::Number(source[span.start..span.end].into()) } } / [Token { kind: TokenKind::String, span }] { + // Token 跨度含引号,去掉引号后调用已验证转义的 decode_string,AST 保存解码结果。 Expr { span, kind: ExprKind::String(decode_string(&source[span.start + 1..span.end - 1])) } } / name:name() { Expr { span: name.span, kind: ExprKind::Name(name.text) } } diff --git a/crates/frontend/src/lexer.peg b/crates/frontend/src/lexer.peg index c3240f4..4234d2a 100644 --- a/crates/frontend/src/lexer.peg +++ b/crates/frontend/src/lexer.peg @@ -1,20 +1,28 @@ +// lexer.rs 编译时引入,生成 lexical::next;输入为本轮尚未消费的 UTF-8 后缀,所有 position! 都相对这个后缀。 peg::parser! { + // Lexer::next 调用的字符串扫描规则;每轮消费 trivia 后至多一个 Token。 grammar lexical() for str { + // next 首先跳过 ASCII 空白和双斜杠行注释,不产生 Token;注释在 CR/LF 前结束。 rule trivia() = ([' ' | '\t' | '\r' | '\n'] / "//" [^'\r' | '\n']*)* + // name 首字符允许下划线或 Unicode 字母,不允许数字开头。 rule name_start() = [character if character == '_' || character.is_alphabetic()] + // name 后续字符允许下划线或 Unicode 字母/数字;number 也用它识别非法黏连后缀。 rule name_continue() = [character if character == '_' || character.is_alphanumeric()] + // kind 的名称分支;关键字也返回 Name,具体语法上下文稍后才识别。 rule name() -> Result = name_start() name_continue()* { Ok(TokenKind::Name) } + // kind 的十进制数分支;无符号整数后可接点和至少一位小数,负号留给单独操作符。 rule number() -> Result = start:position!() ['0'..='9']+ ("." ['0'..='9']+)? suffix:$(name_continue()*) end:position!() { + // 数字后没有名称字符才是完整数 Token;否则将整段作为词法错误,避免误拆成两个合法 Token。 if suffix.is_empty() { Ok(TokenKind::Number) } else { @@ -25,32 +33,42 @@ peg::parser! { } } + // string 只接受换行、制表、双引号、反斜杠四种反斜杠转义,解码在 parser 的 decode_string 中进行。 rule escape() = "\\" ['n' | 't' | '"' | '\\'] + // string 的普通字符不能是引号、反斜杠、原始换行或 NUL;其他 Unicode 字符原样消费。 rule string_character() = [^'"' | '\\' | '\r' | '\n' | '\0'] + // string 主体停止后检查闭引号或报告具体错误;start 是开引号位置,供未闭合字符串定位。 rule string_end(start: usize) -> Result<(), Diagnostic> = "\"" { Ok(()) } + // 主体已消费所有合法转义,剩余反斜杠加字符必是未支持的转义。 / position:position!() "\\" [_] end:position!() { Err(Diagnostic::lexical(Span::new(position, end), "unsupported string escape")) } + // 反斜杠后直接 EOF,按从开引号到 EOF 的未闭合范围报告。 / "\\" end:position!() ![_] { Err(Diagnostic::lexical(Span::new(start, end), "unterminated string")) } + // 原始换行/NUL 不可出现在字符串体内,定位具体违规字符。 / position:position!() ['\r' | '\n' | '\0'] end:position!() { Err(Diagnostic::lexical(Span::new(position, end), "unescaped newline or NUL in string")) } + // 普通 EOF 也表示缺少闭引号,范围从开引号覆盖到输入结尾。 / end:position!() ![_] { Err(Diagnostic::lexical(Span::new(start, end), "unterminated string")) } + // kind 的字符串分支;扫描已验证主体及终结情况,Token 跨度仍包含引号并保持未解码原文。 rule string() -> Result = start:position!() "\"" (escape() / string_character())* end:string_end(start) { + // 闭引号验证通过才确认 String 种类,错误按值继续向上传递。 end.map(|()| TokenKind::String) } + // kind 的操作符/分隔符分支;共享前缀的双字符比较必须放在单字符比较/赋值之前。 rule punctuation() -> TokenKind = "(" { TokenKind::LeftParen } / ")" { TokenKind::RightParen } @@ -71,6 +89,7 @@ peg::parser! { / "*" { TokenKind::Star } / "/" { TokenKind::Slash } + // token 按顺序尝试名称、数字、字符串、标点;未知字符作为错误值返回,避免吞掉后重试别种 Token。 rule kind() -> Result = name() / number() @@ -80,6 +99,7 @@ peg::parser! { Err(Diagnostic::lexical(Span::new(start, end), "unexpected source character")) } + // next 用此包装单词种类及相对字节范围;只在 kind 成功时生成 Token。 rule token() -> Result = start:position!() kind:kind() end:position!() { kind.map(|kind| Token { kind, span: Span::new(start, end) }) @@ -87,9 +107,12 @@ peg::parser! { // Scan only one prefix. Errors are returned as values, so a matched // malformed number/string cannot backtrack into another token kind. + // 禁用默认整段消费检查,才能逐个前缀扫描;最终 EOF 由规则内的 ![_] 判断。 #[no_eof] + // Lexer::next 的唯一公开扫描入口;允许剩余后缀尚有内容,返回本轮消费字节数和 Token/EOF/诊断。 pub rule next() -> (usize, Result, Diagnostic>) = trivia() result:(token:token() { token.map(Some) } / ![_] { Ok(None) }) end:position!() { + // end 包括前导 trivia 与当前 Token;Lexer 用它推进游标,再把结果跨度加回本轮原文起点。 (end, result) } } diff --git a/crates/frontend/src/reader.rs b/crates/frontend/src/reader.rs index 60d2c54..48c41f2 100644 --- a/crates/frontend/src/reader.rs +++ b/crates/frontend/src/reader.rs @@ -1,3 +1,4 @@ +//! 宿主及前端测试在词法处理前调用的无状态读取适配器;统一验证 UTF-8,原样保留字节解码后的文本与来源。 use std::error::Error; use std::fmt; use std::fs::File; @@ -9,26 +10,34 @@ use crate::{SourceOrigin, SourceUnit}; /// The host resolves names, authorizes access and selects a revision before /// returning its source bytes. No URI path layout or hash algorithm is assumed. +/// 宿主实现的 endor URI 解析协议;Reader 不内置仓库、网络、路径布局或版本算法。 pub trait EndorSourceProvider { + /// EndorReader::from_endor 在基础 URI 检查后调用;宿主负责授权、定位版本并返回该版本原始字节。 fn load(&self, uri: &str) -> io::Result; } #[derive(Debug, Clone, PartialEq, Eq)] +/// provider.load 的拥有型结果;Reader 验证非空版本标签及 UTF-8 后移入 SourceUnit。 pub struct ResolvedEndorSource { + // 所选版本的原始标签,前端仅检查 trim 后非空,不规范化或验证哈希。 pub revision: String, + // 该版本的源码字节,尚未验证 UTF-8,也未进行换行或编码转换。 pub bytes: Vec, } /// Stateless source adapters, before lexing. Streams are read to EOF; session /// prompting and incomplete REPL input detection belong to the interactive host. #[derive(Debug, Clone, Copy, Default)] +/// 宿主/测试共用的读取门面;每次调用独立产生快照,流读到 EOF,不保存 REPL 会话状态。 pub struct EndorReader; impl EndorReader { + /// 宿主和编译测试提供现成 UTF-8 文本时调用;String 类型已保证编码有效,只包装来源与原文。 pub fn from_text(name: impl Into, text: impl Into) -> SourceUnit { SourceUnit::new(SourceOrigin::Text { name: name.into() }, text.into()) } + /// 宿主/测试把 Read 流读成一个快照;name 仅用于来源显示,读取失败保留流来源名。 pub fn from_stream( name: impl Into, reader: impl Read, @@ -36,11 +45,15 @@ impl EndorReader { Self::read(SourceOrigin::Stream { name: name.into() }, reader) } + /// 宿主/文件测试调用,先保存提供的路径身份再打开文件;打开/读取/解码错误均带相同来源。 pub fn from_file(path: impl AsRef) -> Result { + // 借用调用方传入路径用于打开;来源另存拥有副本,不要求先规范化或转为绝对路径。 let path = path.as_ref(); + // 在打开之前保存来源,连文件不存在或权限错误也能指明请求的路径。 let origin = SourceOrigin::File { path: path.to_owned(), }; + // 打开失败属于 IO 错误;成功后与流读取共用 read 和 decode。 let file = File::open(path).map_err(|error| SourceReadError { origin: origin.to_string(), kind: SourceReadErrorKind::Io(error), @@ -48,33 +61,39 @@ impl EndorReader { Self::read(origin, file) } + /// 宿主/URI 适配测试调用;前端做基础定位符检查,宿主 provider 选择版本,成功后才解码源码。 pub fn from_endor( uri: &str, provider: &P, ) -> Result { // Only recognize the scheme and reject empty/control-filled locators. // Namespace syntax, revision selectors and percent decoding are host policy. + // 协议大小写不敏感,冒号后不能为空,整串不能含空白/控制字符;其余语义交给宿主。 let valid = uri.split_once(':').is_some_and(|(scheme, address)| { scheme.eq_ignore_ascii_case("endor") && !address.is_empty() }) && !uri .chars() .any(|character| character.is_whitespace() || character.is_control()); + // 无效定位符先拒绝,不能调用 provider 触发不必要的外部访问。 if !valid { return Err(SourceReadError { origin: uri.into(), kind: SourceReadErrorKind::InvalidEndorUri, }); } + // 基础检查通过后委托宿主,宿主解析失败统一包装为带原 URI 的 IO 错误。 let resolved = provider.load(uri).map_err(|error| SourceReadError { origin: uri.into(), kind: SourceReadErrorKind::Io(error), })?; + // 没有解析后的版本身份就不能构成稳定来源;仅检查,不把原标签改成 trim 后内容。 if resolved.revision.trim().is_empty() { return Err(SourceReadError { origin: uri.into(), kind: SourceReadErrorKind::MissingRevision, }); } + // 保存原 URI 和 provider 返回的版本标签,诊断及生成代码文件名沿用它们。 let origin = SourceOrigin::Endor { uri: uri.into(), revision: resolved.revision, @@ -82,7 +101,9 @@ impl EndorReader { Self::decode(origin, resolved.bytes) } + /// from_stream/from_file 共用;完整读到 EOF 才尝试 UTF-8 解码,中途失败不返回部分源码快照。 fn read(origin: SourceOrigin, mut reader: impl Read) -> Result { + // 收集原始字节,不过滤 BOM、换行、空白或 NUL;编码和后续语法各按自己的阶段处理。 let mut bytes = Vec::new(); reader .read_to_end(&mut bytes) @@ -93,7 +114,9 @@ impl EndorReader { Self::decode(origin, bytes) } + /// read/from_endor 共用;只接受严格 UTF-8,不做有损替换,成功原样构造拥有快照。 fn decode(origin: SourceOrigin, bytes: Vec) -> Result { + // String::from_utf8 失败保留 FromUtf8Error,来源显示中包含文件/流/已解析版本身份。 let text = String::from_utf8(bytes).map_err(|error| SourceReadError { origin: origin.to_string(), kind: SourceReadErrorKind::InvalidUtf8(error), @@ -103,22 +126,32 @@ impl EndorReader { } #[derive(Debug)] +/// Reader 的失败分类,区别外部 IO、文本编码及 URI/版本协议问题,尚未进入词法阶段。 pub enum SourceReadErrorKind { + // 文件打开、流读取或 provider.load 返回的底层 IO 原因。 Io(io::Error), + // 严格 UTF-8 解码失败,保留有效字节前缀位置及原始错误对象。 InvalidUtf8(FromUtf8Error), + // Reader 的基础 endor URI 检查不通过,没有调用 provider。 InvalidEndorUri, + // provider 成功返回字节但未提供非空版本标签,不能创建已解析来源。 MissingRevision, } #[derive(Debug)] +/// Reader 公开返回给宿主的错误,拥有来源显示文本与具体原因,可沿标准 Error 链追查底层失败。 pub struct SourceReadError { + // 错误发生时固化的来源字符串,不依赖失败的输入流或 SourceUnit 仍存在。 pub origin: String, + // 分类及可能的底层错误对象,供宿主程序化判断。 pub kind: SourceReadErrorKind, } impl fmt::Display for SourceReadError { + /// 宿主显示错误时先输出来源,再按类别显示底层说明、无效 UTF-8 字节位置或协议错误。 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!(formatter, "{}: ", self.origin)?; + // IO/编码错误保留底层原因;URI 和缺版本是 Reader 自身协议错误。 match &self.kind { SourceReadErrorKind::Io(error) => write!(formatter, "{error}"), SourceReadErrorKind::InvalidUtf8(error) => write!( @@ -137,7 +170,9 @@ impl fmt::Display for SourceReadError { } impl Error for SourceReadError { + /// 标准错误链查询入口;IO 与编码错误返回原始原因,Reader 协议错误没有下层异常。 fn source(&self) -> Option<&(dyn Error + 'static)> { + // IO/编码错误保留底层原因;URI 和缺版本是 Reader 自身协议错误。 match &self.kind { SourceReadErrorKind::Io(error) => Some(error), SourceReadErrorKind::InvalidUtf8(error) => Some(error),