docs(tooling): explain AST generator and validation scripts
This commit is contained in:
@@ -15,85 +15,128 @@ import re
|
||||
import subprocess
|
||||
|
||||
|
||||
# ASDL 内置标量到 Rust 类型的映射;自定义节点类型另由 definitions 解析,不在这里注册。
|
||||
BUILTINS = {"string": "String", "span": "Span", "schema_id": "String"}
|
||||
# 所有生成源码共用的来源提示;修改生成注释应改模板,不手改派生文件。
|
||||
HEADER = "// Generated by scripts/generate_ast.py from Endor.asdl. Do not edit.\n"
|
||||
# parse_schema 拒绝 Rust 保留字标识符,避免输出无法表示的字段/类型名;schema 名称必须可跨目标使用。
|
||||
RESERVED = set("as break const continue crate else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while async await dyn abstract become box do final macro override priv typeof unsized virtual yield try gen".split())
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
# 不可变字段描述,由 parse_schema 构造,Rust/TS/JSON 及遍历器生成阶段共同消费。
|
||||
class Field:
|
||||
# 字段引用的 ASDL 类型名,可能是 BUILTINS 标量或用户定义节点。
|
||||
type: str
|
||||
# 字段、构造器或定义的原始标识符,生成器不自动改写它。
|
||||
name: str
|
||||
# 空串为必选单值,星号为序列,问号为可空/可缺省字段。
|
||||
quantifier: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
# 和类型的一个构造器;fields 可空、单项或多项,对应三种不同生成形状。
|
||||
class Variant:
|
||||
# 字段、构造器或定义的原始标识符,生成器不自动改写它。
|
||||
name: str
|
||||
# 产品类型字段或构造器字段,保持 ASDL 顺序;Definition 的 None 才表示和类型。
|
||||
fields: tuple[Field, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
# 一个命名定义;fields 非 None 为产品类型,None 为带 variants 的和类型。
|
||||
class Definition:
|
||||
# 字段、构造器或定义的原始标识符,生成器不自动改写它。
|
||||
name: str
|
||||
# 产品类型字段或构造器字段,保持 ASDL 顺序;Definition 的 None 才表示和类型。
|
||||
fields: tuple[Field, ...] | None
|
||||
# 和类型构造器列表,产品类型保持为空元组。
|
||||
variants: tuple[Variant, ...] = ()
|
||||
|
||||
# 图分析与类型校验调用;产品直接取字段,和类型按构造器顺序展平所有字段。
|
||||
def all_fields(self):
|
||||
return self.fields if self.fields is not None else tuple(f for v in self.variants for f in v.fields)
|
||||
|
||||
# Rust/TS/Schema 生成共用;只有全部构造器均无载荷的和类型才可作为简单枚举。
|
||||
def simple(self):
|
||||
return self.fields is None and all(not v.fields for v in self.variants)
|
||||
|
||||
|
||||
# generate 及生成器回归测试调用;解析本项目 ASDL 子集,拒绝未知语法、重名与未定义字段类型。
|
||||
def parse_schema(text: str) -> dict[str, Definition]:
|
||||
"""Parse the project's ASDL subset; reject unknown syntax and field types."""
|
||||
# ASDL 双横线注释不影响语法解析;generate 的协议哈希仍使用未经删注释的原文。
|
||||
text = re.sub(r"--[^\n]*", "", text)
|
||||
# 保存标识符及结构标点,后面的递归小解析器只按此序列推进。
|
||||
tokens = []
|
||||
# ASDL 原始文本的 Python 字符索引,用于词法扫描错误定位片段。
|
||||
cursor = 0
|
||||
# 只接受 ASCII 标识符和已支持的 ASDL 标点,其他字符不能被静默跳过。
|
||||
pattern = re.compile(r"\s*([A-Za-z_][A-Za-z_0-9]*|[{}()=|,*?])")
|
||||
# 按输入顺序扫描 Token,纯尾部空白不生成额外 Token。
|
||||
while cursor < len(text):
|
||||
# 只有剩余部分全为空白才正常结束,非空未知内容必须进入失败路径。
|
||||
if not text[cursor:].strip():
|
||||
break
|
||||
# 从当前位置匹配一个含前导空白的 Token,禁止跳过未识别片段向后搜索。
|
||||
match = pattern.match(text, cursor)
|
||||
# 未知字符直接报错,避免生成不完整的部分 schema。
|
||||
if not match:
|
||||
raise ValueError(f"unsupported ASDL syntax near {text[cursor:cursor + 30]!r}")
|
||||
tokens.append(match[1])
|
||||
cursor = match.end()
|
||||
# 语法游标与前面的文本游标分开,指向尚未消费的 Token。
|
||||
index = 0
|
||||
|
||||
# parse_schema 内部前瞻,不前进游标;不完整输入在这里明确报告 EOF。
|
||||
def peek():
|
||||
# 无可消费/前瞻 Token 时报告输入不完整,不让 Python 索引异常泄漏为协议。
|
||||
if index == len(tokens):
|
||||
raise ValueError("unexpected end of ASDL")
|
||||
return tokens[index]
|
||||
|
||||
# 内部消费一个 Token,可同时校验固定标点;只有成功后才推进 index。
|
||||
def take(expected=None):
|
||||
# 多个内部解析函数共享外层 Token 游标,不能各自维护副本。
|
||||
nonlocal index
|
||||
# 无可消费/前瞻 Token 时报告输入不完整,不让 Python 索引异常泄漏为协议。
|
||||
if index == len(tokens):
|
||||
raise ValueError("unexpected end of ASDL")
|
||||
# 保留待返回 Token,在验证期望值后再推进游标。
|
||||
value = tokens[index]
|
||||
# 固定词/标点与当前 Token 必须相同,错误显示两者便于定位 schema 编辑问题。
|
||||
if expected is not None and value != expected:
|
||||
raise ValueError(f"expected {expected!r}, found {value!r}")
|
||||
index += 1
|
||||
return value
|
||||
|
||||
# 定义名、类型名、构造器名及字段名共用的标识符检查。
|
||||
def identifier():
|
||||
# 消费一个候选名称,然后检查允许的标识符形状与保留字约束。
|
||||
value = take()
|
||||
# 禁止未知命名形式及 Rust 保留字,以免后续模板生成不可用源码。
|
||||
if not re.fullmatch(r"[A-Za-z_][A-Za-z_0-9]*", value) or value in RESERVED:
|
||||
raise ValueError(f"invalid or reserved identifier: {value}")
|
||||
return value
|
||||
|
||||
# 产品及带载荷构造器共用括号字段列表解析,返回不可变元组保留原顺序。
|
||||
def fields():
|
||||
take("(")
|
||||
# 当前字段列表的累积结果,结束时再做字段名去重。
|
||||
result = []
|
||||
# 右括号之前逐项读取类型、可选数量修饰符及字段名。
|
||||
while peek() != ")":
|
||||
# 先读字段类型,实际是否已定义在整个 schema 读完后统一检查,允许前向类型引用。
|
||||
type_name = identifier()
|
||||
# 仅问号或星号作为数量修饰,缺省是单个必需字段。
|
||||
quantifier = take() if peek() in ("?", "*") else ""
|
||||
result.append(Field(type_name, identifier(), quantifier))
|
||||
# 没有逗号则结束列表,随后必须读到闭括号。
|
||||
if peek() != ",":
|
||||
break
|
||||
take(",")
|
||||
take(")")
|
||||
# 同一产品/构造器内不允许重复字段名。
|
||||
if len({f.name for f in result}) != len(result):
|
||||
raise ValueError("duplicate field name")
|
||||
return tuple(result)
|
||||
@@ -101,110 +144,160 @@ def parse_schema(text: str) -> dict[str, Definition]:
|
||||
take("module")
|
||||
identifier()
|
||||
take("{")
|
||||
# 按声明顺序保留定义;后续生成及 DFS 使用稳定顺序以得到可重复输出。
|
||||
definitions = {}
|
||||
# 逐个读取命名定义,模块闭括号结束后不允许残留输入。
|
||||
while peek() != "}":
|
||||
# 读取当前定义名并检查是否覆盖已定义类型或内置标量。
|
||||
name = identifier()
|
||||
# 自定义类型不能重名或覆写内置类型映射。
|
||||
if name in definitions or name in BUILTINS:
|
||||
raise ValueError(f"duplicate or builtin type: {name}")
|
||||
take("=")
|
||||
# 等号后直接括号表示产品;否则解析一个或多个和类型构造器。
|
||||
if peek() == "(":
|
||||
# 产品类型保存字段元组,区别于和类型的 fields=None。
|
||||
definition = Definition(name, fields())
|
||||
else:
|
||||
# 按顺序收集当前和类型构造器,供输出判别联合/枚举。
|
||||
variants = []
|
||||
# 持续收集由竖线分隔的构造器,遇到非竖线后交外层处理下一定义。
|
||||
while True:
|
||||
# 当前构造器名称,是否带载荷由接下来的左括号决定。
|
||||
variant = identifier()
|
||||
# 没有括号的构造器使用空元组,生成时成为无载荷分支。
|
||||
members = fields() if peek() == "(" else ()
|
||||
variants.append(Variant(variant, members))
|
||||
# 构造器列表结束;不提前消费下一定义的标识符。
|
||||
if peek() != "|":
|
||||
break
|
||||
take("|")
|
||||
# 同一和类型的构造器必须唯一,否则 JSON 判别标签会歧义。
|
||||
if len({v.name for v in variants}) != len(variants):
|
||||
raise ValueError(f"duplicate constructor in {name}")
|
||||
# 和类型以 None 区别产品的空字段元组,保留完整构造器列表。
|
||||
definition = Definition(name, None, tuple(variants))
|
||||
definitions[name] = definition
|
||||
take("}")
|
||||
# 模块闭括号之后必须 EOF,拒绝尾随文本。
|
||||
if index != len(tokens):
|
||||
raise ValueError("trailing ASDL input")
|
||||
# 依稳定 schema 顺序处理每个定义,保留生成结果的可重复性。
|
||||
for definition in definitions.values():
|
||||
# 产品及所有构造器字段都必须引用内置标量或已声明类型。
|
||||
for field in definition.all_fields():
|
||||
# 解析完所有声明再检查类型,因此支持合法前向引用但拒绝拼错的未知类型。
|
||||
if field.type not in definitions and field.type not in BUILTINS:
|
||||
raise ValueError(f"undefined field type: {field.type}")
|
||||
return definitions
|
||||
|
||||
|
||||
# Rust 访问器方法名生成调用,把类型名大写边界转成蛇形小写,不改变 schema 原类型名。
|
||||
def snake(name):
|
||||
return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
|
||||
|
||||
|
||||
# rust_types 和回归测试调用;在按值引用图上 DFS 标记回边,Vec 自带间接存储而无需 Box。
|
||||
def recursive_edges(definitions):
|
||||
"""Box DFS back edges in the by-value graph; Vec already breaks recursion."""
|
||||
# 分别记录当前递归路径、已完成节点、需要装箱的 (拥有类型, 被引用类型) 边。
|
||||
active, done, boxed = set(), set(), set()
|
||||
|
||||
# DFS 内部递归访问一个定义,进入 active 后检查其所有按值字段,结束时转到 done。
|
||||
def visit(name):
|
||||
active.add(name)
|
||||
# 检查当前类型的全部产品/构造器字段,不遗漏和类型形成的递归环。
|
||||
for field in definitions[name].all_fields():
|
||||
# 序列 Vec 已打断 Rust 按值递归,内置标量也无需进入定义图。
|
||||
if field.quantifier == "*" or field.type not in definitions:
|
||||
continue
|
||||
# 指向当前递归路径形成回边,在 Rust 字段映射时为这条边加 Box。
|
||||
if field.type in active:
|
||||
boxed.add((name, field.type))
|
||||
# 尚未遍历完成的引用类型继续 DFS,已完成类型不重复递归。
|
||||
elif field.type not in done:
|
||||
visit(field.type)
|
||||
active.remove(name)
|
||||
done.add(name)
|
||||
|
||||
# 按定义顺序遍历/输出每个命名类型,避免由无序集合决定源码结构。
|
||||
for name in definitions:
|
||||
# 每个未访问连通分量都需启动 DFS,不能只从 AstRoot 假设全部类型可达。
|
||||
if name not in done:
|
||||
visit(name)
|
||||
return boxed
|
||||
|
||||
|
||||
# rust_types/rust_visitor 共用;显式 span 产品和只包装单个可定位子节点的和类型可实现 Spanned。
|
||||
def spanned_types(definitions):
|
||||
# 起始集合仅含拥有必需 span 字段的产品类型,不把可选 span 误当作可靠位置。
|
||||
result = {d.name for d in definitions.values() if d.fields is not None and Field("span", "span") in d.fields}
|
||||
# 求可定位类型闭包,包装层可能依赖前一轮刚加入的类型。
|
||||
while True:
|
||||
# 所有构造器均恰有一个必选且已可定位的子节点,才可把该和类型加入集合。
|
||||
expanded = result | {
|
||||
d.name for d in definitions.values()
|
||||
if d.variants and all(len(v.fields) == 1 and not v.fields[0].quantifier and v.fields[0].type in result for v in d.variants)
|
||||
}
|
||||
# 集合不再增长即达到不动点,后续生成 span 及节点钩子使用同一判定。
|
||||
if expanded == result:
|
||||
return result
|
||||
result = expanded
|
||||
|
||||
|
||||
# generate 生成 Rust AST 源码,递归布局、serde 形状和 Spanned 实现都来自同一份定义。
|
||||
def rust_types(definitions, schema_id):
|
||||
# 先标出必须间接存储的回边,不把所有字段无差别装箱。
|
||||
boxed = recursive_edges(definitions)
|
||||
# 取得结构上可提供跨度的类型集合,决定 span 实现或访问器节点钩子。
|
||||
spanned = spanned_types(definitions)
|
||||
|
||||
# Rust 字段映射:内置标量/命名类型、Vec、必要 Box 和可空 Option 的组合。
|
||||
def field_type(owner, field):
|
||||
# 先映射内置名,用户节点保持原类型名。
|
||||
value = BUILTINS.get(field.type, field.type)
|
||||
# 序列优先映射到 Vec/Array/array;容器本身处理重复元素并打断 Rust 按值递归。
|
||||
if field.quantifier == "*":
|
||||
return f"Vec<{value}>"
|
||||
# 仅 DFS 回边添加 Box;可选字段之后再包 Option,以保留空值语义。
|
||||
if (owner, field.type) in boxed:
|
||||
value = f"Box<{value}>"
|
||||
return f"Option<{value}>" if field.quantifier == "?" else value
|
||||
|
||||
lines = [HEADER, "use crate::Span;", "use super::Spanned;", f'pub const AST_SCHEMA_ID: &str = "{schema_id}";']
|
||||
# 依稳定 schema 顺序处理每个定义,保留生成结果的可重复性。
|
||||
for definition in definitions.values():
|
||||
# 所有 AST 类型支持拥有副本、相等比较及 serde,简单无载荷枚举才额外 Copy。
|
||||
derives = "Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize"
|
||||
# 无载荷和类型可用简单枚举/字符串联合表示,无需 kind/data 的载荷包装。
|
||||
if definition.simple():
|
||||
derives += ", Copy"
|
||||
lines.append(f"#[derive({derives})]")
|
||||
# 产品及带载荷和类型拒绝未知字段;后者额外采用 kind/data 邻接标签。
|
||||
if not definition.simple():
|
||||
# 只有和类型才加 kind/data 标签,产品字段直接序列化成对象。
|
||||
tag = 'tag = "kind", content = "data", ' if definition.fields is None else ""
|
||||
lines.append(f"#[serde({tag}deny_unknown_fields)]")
|
||||
# 产品按字段顺序生成;和类型则按构造器分支选择对应形状。
|
||||
if definition.fields is not None:
|
||||
lines.append(f"pub struct {definition.name} {{")
|
||||
lines.extend(f"pub {f.name}: {field_type(definition.name, f)}," for f in definition.fields)
|
||||
else:
|
||||
lines.append(f"pub enum {definition.name} {{")
|
||||
# 每个构造器按稳定顺序生成,不合并名称相近但身份不同的分支。
|
||||
for variant in definition.variants:
|
||||
# 无载荷构造器不携带额外字段,单值和多字段载荷分别采用相应表示。
|
||||
if not variant.fields:
|
||||
lines.append(f"{variant.name},")
|
||||
# 单字段构造器直接包装字段类型,多字段才使用命名字段对象。
|
||||
elif len(variant.fields) == 1:
|
||||
lines.append(f"{variant.name}({field_type(definition.name, variant.fields[0])}),")
|
||||
else:
|
||||
# 多字段构造器的具名成员保留每个字段自己的类型/数量修饰。
|
||||
fields = ", ".join(f"{f.name}: {field_type(definition.name, f)}" for f in variant.fields)
|
||||
lines.append(f"{variant.name} {{ {fields} }},")
|
||||
lines.append("}")
|
||||
# 只为结构上可定位的类型生成 Spanned,不编造 ExprKind 这类混合载荷的范围。
|
||||
if definition.name in spanned:
|
||||
lines.append(f"impl Spanned for {definition.name} {{ fn span(&self) -> Span {{")
|
||||
if definition.fields is not None:
|
||||
@@ -217,15 +310,20 @@ def rust_types(definitions, schema_id):
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
# generate 生成 Rust 借用节点访问器及 walk 函数;可定位节点通过 enter/leave 支持校验状态栈。
|
||||
def rust_visitor(definitions):
|
||||
# 取得结构上可提供跨度的类型集合,决定 span 实现或访问器节点钩子。
|
||||
spanned = spanned_types(definitions)
|
||||
# 按 schema 顺序导入所有节点类型,walk 可以直接引用它们。
|
||||
names = ", ".join(definitions)
|
||||
lines = [HEADER, f"use super::{{{names}, Spanned}};", "use crate::Span;", "pub trait Visitor {", "type Error;",
|
||||
"fn enter_node(&mut self, _span: Span) -> Result<(), Self::Error> { Ok(()) }",
|
||||
"fn leave_node(&mut self) {}",
|
||||
"fn visit_span(&mut self, _span: &Span) -> Result<(), Self::Error> { Ok(()) }"]
|
||||
# 按定义顺序遍历/输出每个命名类型,避免由无序集合决定源码结构。
|
||||
for name in definitions:
|
||||
lines.append(f"fn visit_{snake(name)}(&mut self, node: &{name}) -> Result<(), Self::Error> {{")
|
||||
# 可定位节点先进入,再保存子遍历 Result,退出后才返回错误,保证成功进入祖先的钩子配平。
|
||||
if name in spanned:
|
||||
lines += ["self.enter_node(node.span())?;", f"let result = walk_{snake(name)}(self, node);", "self.leave_node();", "result"]
|
||||
else:
|
||||
@@ -233,38 +331,59 @@ def rust_visitor(definitions):
|
||||
lines.append("}")
|
||||
lines.append("}")
|
||||
|
||||
# 访问器生成共用的字段递归模板:跳过普通文本/标识,仅递归节点或显式跨度。
|
||||
def children(field, access):
|
||||
# 字符串和 schema_id 无语法子节点,不能生成不存在的 visit_string/walkstring 调用。
|
||||
if field.type not in definitions and field.type != "span":
|
||||
return []
|
||||
# Rust 字段类型名映射成 visitor.visit_*,span 走默认可覆盖的 visit_span。
|
||||
method = f"visitor.visit_{snake(field.type)}"
|
||||
# 序列优先映射到 Vec/Array/array;容器本身处理重复元素并打断 Rust 按值递归。
|
||||
if field.quantifier == "*":
|
||||
return [f"for value in {access} {{ {method}(value)?; }}"]
|
||||
# 可选字段只有 Some/非空时才递归,缺省或 null 不访问。
|
||||
if field.quantifier == "?":
|
||||
return [f"if let Some(value) = {access} {{ {method}(value)?; }}"]
|
||||
return [f"{method}({access})?;"]
|
||||
|
||||
# 依稳定 schema 顺序处理每个定义,保留生成结果的可重复性。
|
||||
for definition in definitions.values():
|
||||
# 为当前 walk 函数累积字段/构造器递归语句,不在生成器内运行访问。
|
||||
body = []
|
||||
# 产品按字段顺序生成;和类型则按构造器分支选择对应形状。
|
||||
if definition.fields is not None:
|
||||
# 产品字段依声明顺序递归,保证访问顺序与数据契约一致。
|
||||
for field in definition.fields:
|
||||
body += children(field, f"&node.{field.name}")
|
||||
# 带载荷和类型按判别分支访问,简单枚举没有子节点。
|
||||
elif not definition.simple():
|
||||
body.append("match node {")
|
||||
# 每个构造器按稳定顺序生成,不合并名称相近但身份不同的分支。
|
||||
for variant in definition.variants:
|
||||
# 只提取节点或 span 载荷;纯文本载荷匹配时忽略,避免无用 Rust 绑定。
|
||||
visited = [f for f in variant.fields if f.type in definitions or f.type == "span"]
|
||||
# 无载荷构造器不携带额外字段,单值和多字段载荷分别采用相应表示。
|
||||
if not variant.fields:
|
||||
# 按构造器载荷形状生成 Rust match 模式,仅绑定需要访问的成员。
|
||||
pattern = f"{definition.name}::{variant.name}"
|
||||
# 单字段构造器直接包装字段类型,多字段才使用命名字段对象。
|
||||
elif len(variant.fields) == 1:
|
||||
# 按构造器载荷形状生成 Rust match 模式,仅绑定需要访问的成员。
|
||||
pattern = f"{definition.name}::{variant.name}({'value' if visited else '_'})"
|
||||
else:
|
||||
# 多字段构造器只绑定需要递归的成员,其余通过 .. 忽略。
|
||||
members = ", ".join(f.name for f in visited)
|
||||
# 按构造器载荷形状生成 Rust match 模式,仅绑定需要访问的成员。
|
||||
pattern = f"{definition.name}::{variant.name} {{ " + (members + ", " if members else "") + ".. }"
|
||||
body.append(pattern + " => {")
|
||||
# 为每个被选中的载荷展开对应序列、可选或单值访问。
|
||||
for field in visited:
|
||||
body += children(field, "value" if len(variant.fields) == 1 else field.name)
|
||||
body.append("}")
|
||||
body.append("}")
|
||||
# 没有实际递归调用的 walker 使用下划线参数名,避免生成未使用变量告警。
|
||||
visitor = "visitor" if any("visitor." in line for line in body) else "_visitor"
|
||||
# 完全无子结构的 walker 也不使用 node,生成下划线名字以满足警告检查。
|
||||
node = "node" if body else "_node"
|
||||
lines.append(f"pub fn walk_{snake(definition.name)}<V: Visitor + ?Sized>({visitor}: &mut V, {node}: &{definition.name}) -> Result<(), V::Error> {{")
|
||||
lines.extend(body)
|
||||
@@ -272,32 +391,48 @@ def rust_visitor(definitions):
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
# generate 生成与 serde 形状匹配的 TypeScript 类型及可剪枝 walker,供编辑器消费 AST JSON。
|
||||
def editor_types(definitions, schema_id):
|
||||
# 编辑器字段映射:标量名、数组、可缺省/可空联合;schema_id 限定为当前常量的字面量类型。
|
||||
def field_type(field):
|
||||
# 将内置标量映射为 TypeScript 对应类型;命名节点保持 schema 名称。
|
||||
result = {"string": "string", "span": "Span", "schema_id": "typeof AST_SCHEMA_ID"}.get(field.type, field.type)
|
||||
# 序列优先映射到 Vec/Array/array;容器本身处理重复元素并打断 Rust 按值递归。
|
||||
if field.quantifier == "*":
|
||||
result = f"Array<{result}>"
|
||||
# 问号字段允许空值,目标类型/Schema 同时处理字段缺省。
|
||||
elif field.quantifier == "?":
|
||||
result += " | null"
|
||||
return result
|
||||
|
||||
# TypeScript 产品或多载荷对象形状,问号字段标为可缺省,并保留其 null 可能性。
|
||||
def object_type(fields):
|
||||
return "{ " + " ".join(f"{f.name}{'?' if f.quantifier == '?' else ''}: {field_type(f)};" for f in fields) + " }"
|
||||
|
||||
lines = [HEADER, f'export const AST_SCHEMA_ID = "{schema_id}" as const;', "export interface Span { start: number; end: number; }"]
|
||||
# 依稳定 schema 顺序处理每个定义,保留生成结果的可重复性。
|
||||
for definition in definitions.values():
|
||||
# 产品按字段顺序生成;和类型则按构造器分支选择对应形状。
|
||||
if definition.fields is not None:
|
||||
lines.append(f"export type {definition.name} = {object_type(definition.fields)};")
|
||||
else:
|
||||
# 按顺序收集当前和类型构造器,供输出判别联合/枚举。
|
||||
variants = []
|
||||
# 每个构造器按稳定顺序生成,不合并名称相近但身份不同的分支。
|
||||
for variant in definition.variants:
|
||||
if definition.simple():
|
||||
# 拼接当前构造器的 kind 字面量和数据对象声明。
|
||||
value = json.dumps(variant.name)
|
||||
# 当前分支不是无载荷简单枚举,需要构造带 kind 及可选 data 的判别对象。
|
||||
else:
|
||||
# 无载荷分支没有 data 属性;确有载荷才生成相应 data 类型。
|
||||
data = ""
|
||||
# 仅有载荷的分支附加 data,单项直接取类型,多项包具名对象。
|
||||
if variant.fields:
|
||||
# 选择直接载荷或多字段对象,保持与 Rust serde 的 data 形状一致。
|
||||
result = field_type(variant.fields[0]) if len(variant.fields) == 1 else object_type(variant.fields)
|
||||
data = f" data: {result};"
|
||||
# 拼接当前构造器的 kind 字面量和数据对象声明。
|
||||
value = '{ kind: ' + json.dumps(variant.name) + ';' + data + ' }'
|
||||
variants.append(value)
|
||||
lines.append(f"export type {definition.name} = " + " | ".join(variants) + ";")
|
||||
@@ -305,27 +440,38 @@ def editor_types(definitions, schema_id):
|
||||
lines.extend(f"visit{name}?(node: {name}): boolean | void;" for name in definitions)
|
||||
lines.append("}")
|
||||
|
||||
# 访问器生成共用的字段递归模板:跳过普通文本/标识,仅递归节点或显式跨度。
|
||||
def children(field, access):
|
||||
# 字符串和 schema_id 无语法子节点,不能生成不存在的 visit_string/walkstring 调用。
|
||||
if field.type not in definitions and field.type != "span":
|
||||
return []
|
||||
# TS span 特殊走 walkSpan,其余命名类型使用对应 walk<Type>。
|
||||
function = "walkSpan" if field.type == "span" else f"walk{field.type}"
|
||||
# 序列优先映射到 Vec/Array/array;容器本身处理重复元素并打断 Rust 按值递归。
|
||||
if field.quantifier == "*":
|
||||
return [f"for (const value of {access}) {function}(value, visitor);"]
|
||||
# 可选字段只有 Some/非空时才递归,缺省或 null 不访问。
|
||||
if field.quantifier == "?":
|
||||
return [f"if ({access} != null) {function}({access}, visitor);"]
|
||||
return [f"{function}({access}, visitor);"]
|
||||
|
||||
lines.append("export function walkSpan(node: Span, visitor: Visitor): void { visitor.visitSpan?.(node); }")
|
||||
# 依稳定 schema 顺序处理每个定义,保留生成结果的可重复性。
|
||||
for definition in definitions.values():
|
||||
lines += [f"export function walk{definition.name}(node: {definition.name}, visitor: Visitor): void {{", f"if (visitor.visit{definition.name}?.(node) === false) return;"]
|
||||
# 产品按字段顺序生成;和类型则按构造器分支选择对应形状。
|
||||
if definition.fields is not None:
|
||||
# 产品字段依声明顺序递归,保证访问顺序与数据契约一致。
|
||||
for field in definition.fields:
|
||||
lines += children(field, "node." + field.name)
|
||||
# 带载荷和类型按判别分支访问,简单枚举没有子节点。
|
||||
elif not definition.simple():
|
||||
lines.append("switch (node.kind) {")
|
||||
# 每个构造器按稳定顺序生成,不合并名称相近但身份不同的分支。
|
||||
for variant in definition.variants:
|
||||
lines.append(f"case {json.dumps(variant.name)}:")
|
||||
for field in variant.fields:
|
||||
# 单项载荷取 node.data,多字段载荷再按字段名取 node.data.<field>。
|
||||
access = "node.data" + ("." + field.name if len(variant.fields) > 1 else "")
|
||||
lines += children(field, access)
|
||||
lines.append("break;")
|
||||
@@ -334,41 +480,62 @@ def editor_types(definitions, schema_id):
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
# generate 生成编辑器 JSON Schema,结构与 Rust serde/TS 对齐;范围包含关系仍由运行时文档校验负责。
|
||||
def json_schema(definitions, schema_id):
|
||||
# 字段映射为内置约束或 $ref,并按数量修饰包数组或可空 anyOf。
|
||||
def field_schema(field):
|
||||
# 内置 string、schema_id 常量和 Span 引用特殊处理;其他字段引用对应定义。
|
||||
value = {"string": {"type": "string"}, "schema_id": {"const": schema_id}, "span": {"$ref": "#/$defs/Span"}}.get(field.type, {"$ref": f"#/$defs/{field.type}"})
|
||||
# 序列优先映射到 Vec/Array/array;容器本身处理重复元素并打断 Rust 按值递归。
|
||||
if field.quantifier == "*":
|
||||
value = {"type": "array", "items": value}
|
||||
# 问号字段允许空值,目标类型/Schema 同时处理字段缺省。
|
||||
elif field.quantifier == "?":
|
||||
value = {"anyOf": [value, {"type": "null"}]}
|
||||
return value
|
||||
|
||||
# 产品和带标签构造器共用,默认所有属性必需,可选字段调用方提供较小 required 列表。
|
||||
def object_schema(properties, required=None):
|
||||
return {"type": "object", "properties": properties, "required": list(properties) if required is None else required, "additionalProperties": False}
|
||||
|
||||
# Span 两端要求非负 JS 安全整数,但此 Schema 不表达 start<=end 或父子包含关系。
|
||||
definitions_json = {"Span": object_schema({name: {"type": "integer", "minimum": 0, "maximum": 9007199254740991} for name in ["start", "end"]})}
|
||||
# 依稳定 schema 顺序处理每个定义,保留生成结果的可重复性。
|
||||
for definition in definitions.values():
|
||||
# 产品按字段顺序生成;和类型则按构造器分支选择对应形状。
|
||||
if definition.fields is not None:
|
||||
# 产品属性以字段名为键,只有非问号字段进入 required,且拒绝额外属性。
|
||||
value = object_schema({f.name: field_schema(f) for f in definition.fields}, [f.name for f in definition.fields if f.quantifier != "?"])
|
||||
# 无载荷枚举映射成字符串 enum,与 serde 和 TS 字面量联合一致。
|
||||
elif definition.simple():
|
||||
# 每个枚举项使用 schema 中的构造器原名。
|
||||
value = {"enum": [v.name for v in definition.variants]}
|
||||
else:
|
||||
# 按顺序收集当前和类型构造器,供输出判别联合/枚举。
|
||||
variants = []
|
||||
# 每个构造器按稳定顺序生成,不合并名称相近但身份不同的分支。
|
||||
for variant in definition.variants:
|
||||
# 带载荷和类型先固定 kind 标签,有字段时再加入 data 约束。
|
||||
properties = {"kind": {"const": variant.name}}
|
||||
if variant.fields:
|
||||
properties["data"] = field_schema(variant.fields[0]) if len(variant.fields) == 1 else object_schema({f.name: field_schema(f) for f in variant.fields}, [f.name for f in variant.fields if f.quantifier != "?"])
|
||||
variants.append(object_schema(properties))
|
||||
# 联合要求恰匹配一个构造器,kind 常量把各分支区分开。
|
||||
value = {"oneOf": variants}
|
||||
definitions_json[definition.name] = value
|
||||
return json.dumps({"$schema": "https://json-schema.org/draft/2020-12/schema", "$id": schema_id, "$ref": "#/$defs/AstDocument", "$defs": definitions_json}, ensure_ascii=False, indent=2) + "\n"
|
||||
|
||||
|
||||
# main 和生成器回归测试调用的纯生成入口,返回目标相对路径到源码/Schema 文本,不写文件。
|
||||
def generate(text):
|
||||
# 先完成所有 ASDL 语法及引用校验,失败不返回部分产物。
|
||||
definitions = parse_schema(text)
|
||||
# 文档协议要求存在两个根定义,不能把任意 ASDL 片段当完整接口生成。
|
||||
for required in ("AstDocument", "AstRoot"):
|
||||
# 缺少入口类型时明确拒绝,避免输出互相引用但没有协议根的文件。
|
||||
if required not in definitions:
|
||||
raise ValueError(f"missing required schema type: {required}")
|
||||
# 协议身份对原始 ASDL 全文做 SHA-256,连注释或空白变化也会改变身份;缓存或传输文档应核对这一身份。
|
||||
schema_id = "urn:endor:ast:sha256:" + hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
return {
|
||||
"crates/frontend/src/ast/generated.rs": rust_types(definitions, schema_id),
|
||||
@@ -378,28 +545,42 @@ def generate(text):
|
||||
}
|
||||
|
||||
|
||||
# 命令行 --check/--write 入口,先生成内存文本,Rust 交给 rustfmt,再按模式比较或写入源码文件。
|
||||
def main():
|
||||
# 建立命令行参数说明,check 与 write 必须且只能选择一个。
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
# 互斥参数组阻止检查和写入混用,避免验证命令隐式修改仓库。
|
||||
mode = parser.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument("--check", action="store_true")
|
||||
mode.add_argument("--write", action="store_true")
|
||||
# 解析参数后才定位仓库、读取 schema 及生成所有目标。
|
||||
args = parser.parse_args()
|
||||
# 脚本文件所在仓库根,不依赖用户运行命令时的工作目录。
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
# 读取原始 ASDL 文本一次,generate 为四个目标共享同一 schema 身份。
|
||||
outputs = generate((root / "crates/frontend/Endor.asdl").read_text(encoding="utf-8"))
|
||||
# check 模式收集缺失或内容过期的相对路径,最终统一报告。
|
||||
stale = []
|
||||
# 每个目标独立处理;仅 Rust 文本需标准格式化,TS/JSON 保留确定性模板输出。
|
||||
for relative, content in outputs.items():
|
||||
# rustfmt 只格式化源码并输出 stdout,不运行 Cargo、编译器或 Endor 代码。
|
||||
if relative.endswith(".rs"):
|
||||
content = subprocess.run(["rustfmt", "--edition", "2024", "--emit", "stdout"], input=content, text=True, capture_output=True, check=True).stdout
|
||||
# 目标路径相对于固定仓库根,防止工作目录改变写入位置。
|
||||
path = root / relative
|
||||
# 显式写入模式才创建父目录和更新文件,供开发者将生成源码纳入提交。
|
||||
if args.write:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
# 检查模式不写入;缺失目标或任意文本差异都算过期。
|
||||
elif not path.is_file() or path.read_text(encoding="utf-8") != content:
|
||||
stale.append(relative)
|
||||
# 存在过期目标时以非零状态退出,报告如何通过 --write 显式更新。
|
||||
if stale:
|
||||
parser.exit(1, "stale generated AST files:\n" + "\n".join(stale) + "\nRun python3 scripts/generate_ast.py --write\n")
|
||||
print("AST artifacts generated." if args.write else "AST artifacts match Endor.asdl.")
|
||||
|
||||
|
||||
# 直接运行脚本才解析命令行,测试导入 generate 等函数不会触发文件写入或格式化进程。
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user