docs(frontend): explain source readers and PEG grammar rules

This commit is contained in:
2026-11-15 18:42:54 +08:00
parent 6da2e54bcf
commit 3a75476bc5
3 changed files with 121 additions and 0 deletions
+35
View File
@@ -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<ResolvedEndorSource>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
/// provider.load 的拥有型结果;Reader 验证非空版本标签及 UTF-8 后移入 SourceUnit。
pub struct ResolvedEndorSource {
// 所选版本的原始标签,前端仅检查 trim 后非空,不规范化或验证哈希。
pub revision: String,
// 该版本的源码字节,尚未验证 UTF-8,也未进行换行或编码转换。
pub bytes: Vec<u8>,
}
/// 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<String>, text: impl Into<String>) -> SourceUnit {
SourceUnit::new(SourceOrigin::Text { name: name.into() }, text.into())
}
/// 宿主/测试把 Read 流读成一个快照;name 仅用于来源显示,读取失败保留流来源名。
pub fn from_stream(
name: impl Into<String>,
reader: impl Read,
@@ -36,11 +45,15 @@ impl EndorReader {
Self::read(SourceOrigin::Stream { name: name.into() }, reader)
}
/// 宿主/文件测试调用,先保存提供的路径身份再打开文件;打开/读取/解码错误均带相同来源。
pub fn from_file(path: impl AsRef<Path>) -> Result<SourceUnit, SourceReadError> {
// 借用调用方传入路径用于打开;来源另存拥有副本,不要求先规范化或转为绝对路径。
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<P: EndorSourceProvider + ?Sized>(
uri: &str,
provider: &P,
) -> Result<SourceUnit, SourceReadError> {
// 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<SourceUnit, SourceReadError> {
// 收集原始字节,不过滤 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<u8>) -> Result<SourceUnit, SourceReadError> {
// 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),