#!/usr/bin/env python3 """Generate checked-in AST types, walkers and editor contracts from Endor.asdl. Uses Python's standard library and rustfmt only. It never builds or runs Endor. """ from __future__ import annotations import argparse from dataclasses import dataclass import hashlib import json from pathlib import Path 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()) # 说明与 ASDL 分开维护,仅输出源码注释,不改变按 ASDL 原文计算的协议身份。 TYPE_NOTES = { "AstDocument": "宿主/编辑器传输的版本化语法文档,wire 校验后供 compiler 使用。", "AstRoot": "parse_ast 按模式构造的模块、表达式或交互根,compiler 据此选择生成入口。", "Name": "parser 复制的名称文本与位置,用于声明、类型注解或属性名。", "Program": "模块声明序列,compiler 先注册全部顶层名称再分析各函数体。", "Decl": "顶层/交互声明的种类及完整范围,compiler 按函数或类分派。", "DeclKind": "函数与类声明的判别载荷,外层 Decl 另外保存完整跨度。", "Class": "类名、可选基类、字段和方法,compiler 检查成员后生成构类指令。", "Function": "显式参数/返回注解与函数体,compiler 生成独立模板及源码签名;解析本身不调用函数。", "Parameter": "函数参数或类字段共用的名称/类型注解,实际类型身份由运行时环境解析。", "Block": "顺序语句块,compiler 检查确定初始化并按语句生成指令。", "Stmt": "语句种类及原文范围,compiler 处理副作用、控制流或返回。", "StmtKind": "语句判别载荷,parser 选择分支,compiler 按绑定、赋值、控制流等规则处理。", "Binding": "let 新绑定,compiler 检查重名并分析右值后才标记目标已初始化。", "Assignment": "普通名称赋值,compiler 按作用域写局部槽或全局环境。", "MemberAssignment": "成员写入的对象、属性名与右值,compiler 生成 StoreAttr。", "IfStatement": "条件、then 和可选 else,compiler 合并继续路径的初始化集合并生成跳转。", "WhileStatement": "循环条件与体,compiler 生成回边并处理循环可能零次执行。", "ReturnStatement": "函数返回,compiler 对缺省值补 Nil;当前源码语法要求 return 后有表达式。", "InteractiveInput": "单次交互提交容器,parser 产生一条语句,compiler 也检查数量,不保存宿主会话环境。", "Expr": "表达式载荷及完整范围,compiler 生成留下一个结果的求值序列。", "ExprKind": "名称、字面量、等待、运算和调用等表达式载荷,范围由外层 Expr 提供。", "UnaryOp": "一元运算种类,compiler 合并纯数值负号或生成运行时取负。", "BinaryOp": "二元算术/比较种类,parser 保留优先级树,compiler 映射为对应 VM 操作。", } # 字段说明以拥有类型和字段名为键,不将不同节点的同名字段混为相同用途。 FIELD_NOTES = { ("AstDocument", "schema"): "协议身份,wire 按 AST_SCHEMA_ID 核对;不是源码内容哈希。", ("AstDocument", "root"): "拥有的语法根,文档校验及编译从这里递归。", ("Name", "text"): "拥有名称文本,不依赖输入缓冲存活。", ("Program", "declarations"): "按源码顺序的声明;源码模块解析要求非空,文档结构本身不限制数量。", ("Decl", "kind"): "函数/类声明载荷,JSON 和类型采用 kind/data 标签。", ("Class", "name"): "类声明名与精确位置。", ("Class", "base"): "显式基类名,可省略;当前 compiler 对省略项使用 Object。", ("Class", "fields"): "按出现顺序的字段名称/类型注解,不含方法。", ("Class", "methods"): "按出现顺序的方法;显式 self 约束由 compiler 检查。", ("Function", "name"): "函数/方法名,代码模板沿用为诊断名称。", ("Function", "parameters"): "位置参数列表,compiler 依次分配前部局部槽。", ("Function", "return_type"): "返回类型名,生成源码签名后由运行时解析。", ("Function", "body"): "独立函数作用域的语句体,当前源码不支持嵌套声明闭包。", ("Parameter", "name"): "参数/字段名,用于布局和重复检查。", ("Parameter", "type_name"): "声明类型名,前端只保留文本与位置。", ("Block", "statements"): "有序语句列表,不引入独立块级局部作用域。", ("Stmt", "kind"): "具体语句载荷,外层另存整句范围。", ("Binding", "name"): "let 目标名,重复声明由语义分析拒绝。", ("Binding", "value"): "绑定右值,本局部不能在初始化前读取自身。", ("Assignment", "name"): "普通赋值目标,函数内首次赋值可预分配局部槽。", ("Assignment", "value"): "先求右值再更新目标。", ("MemberAssignment", "object"): "属性写入接收对象表达式,前端不求值。", ("MemberAssignment", "name"): "待写属性名,compiler 加入名字表。", ("MemberAssignment", "value"): "待写右值,当前 compiler 先于接收对象生成求值以匹配 VM 栈布局。", ("IfStatement", "condition"): "条件表达式,运行时按真值协议选择分支。", ("IfStatement", "body"): "条件为真的 then 块。", ("IfStatement", "otherwise"): "可选 else 块,缺省时存在直接继续路径。", ("WhileStatement", "condition"): "每轮重新求值的条件,假值退出。", ("WhileStatement", "body"): "循环体,可能一次也不执行。", ("ReturnStatement", "value"): "可选返回表达式,手工 AST 缺省时由 compiler 补 Nil。", ("InteractiveInput", "statements"): "当前 parse/compile 约定恰好一个完整提交的语句。", ("Expr", "kind"): "求值形状,不缓存名称解析或运行时类型。", ("ExprKind", "operation"): "运算种类,决定算术、比较或取负指令。", ("ExprKind", "operand"): "一元子表达式,Rust 的递归回边间接存储。", ("ExprKind", "left"): "二元左操作数,当前 compiler 先于右侧求值。", ("ExprKind", "right"): "二元右操作数,与左侧一起被运行时运算消费。", ("ExprKind", "object"): "成员读取接收对象。", ("ExprKind", "name"): "成员或符号名,不是已解析的运行时属性句柄。", ("ExprKind", "callee"): "调用目标,先于实参求值。", ("ExprKind", "arguments"): "有序位置实参,AST 不隐式添加接收者。", } VARIANT_NOTES = { ("AstRoot", "Module"): "模块根,包含函数/类声明。", ("AstRoot", "Expression"): "表达式根,编译后直接返回求值结果。", ("AstRoot", "Interactive"): "交互根,包含一次完整提交。", ("ExprKind", "Name"): "变量引用文本,compiler 决定局部槽或全局索引。", ("ExprKind", "Number"): "原始十进制拼写,保留全部位数、前导零及尾零,不转浮点。", ("ExprKind", "String"): "已解码受支持转义的拥有文本,原引号范围在外层 Expr。", ("ExprKind", "Symbol"): "点开头的符号名,前端可表示,当前 compiler 拒绝生成运行时值。", ("ExprKind", "Await"): "等待子表达式,VM 执行上下文决定是否允许停放。", ("ExprKind", "Unary"): "一元取负,可在纯数值链上合并符号。", ("ExprKind", "Binary"): "左右操作数及算术/比较种类,保留解析优先级树。", ("ExprKind", "Member"): "接收对象及属性名的读取。", ("ExprKind", "Call"): "被调用表达式与位置实参,由 VM 实际求值。", ("UnaryOp", "Negate"): "一元取负。", } # 操作符叶子没有子节点,仍需给各构造器明确语义;扩展项继续使用通用说明。 VARIANT_NOTES.update({("BinaryOp", name): note for name, note in { "Add": "加法。", "Subtract": "减法。", "Multiply": "乘法。", "Divide": "除法。", "Equal": "相等比较。", "NotEqual": "不等比较。", "Less": "小于比较。", "LessEqual": "小于等于比较。", "Greater": "大于比较。", "GreaterEqual": "大于等于比较。", }.items()}) # Rust/TS 类型声明生成共用,未记录的扩展节点仍能按结构正常生成。 def type_note(name): return TYPE_NOTES.get(name, f"ASDL 定义的 {name} 节点,由宿主构造并由访问器消费。") # 字段说明共用;span 的位置协议在所有节点中保持一致。 def field_note(owner, field): # 内置 span 的统一位置协议优先于拥有节点的特定字段含义。 if field.type == "span": return "原始 UTF-8 文本的半开字节范围,定位与父子范围校验共用;不是字符列。" return FIELD_NOTES.get((owner, field.name), f"{field.name} 保存 {field.type} 类型" + ("的有序列表。" if field.quantifier == "*" else "的可选值。" if field.quantifier == "?" else "的值。")) # 和类型说明共用,包装已知节点时复用其语义,否则按构造器形状提供结构说明。 def variant_note(owner, variant): # 单载荷包装已知节点时复用节点说明,未知扩展构造器仍保留通用结构说明。 if len(variant.fields) == 1 and variant.fields[0].type in TYPE_NOTES: fallback = type_note(variant.fields[0].type) else: fallback = f"{owner} 的 {variant.name} 构造器,载荷由 ASDL 字段定义。" return VARIANT_NOTES.get((owner, variant.name), fallback) @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) 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"(?" # 仅 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;", "/// wire/编辑器核对的协议身份,来自 ASDL 原文哈希;生成节点注释不参与此值。", f'pub const AST_SCHEMA_ID: &str = "{schema_id}";'] # 依稳定 schema 顺序处理每个定义,保留生成结果的可重复性。 for definition in definitions.values(): # 所有 AST 类型支持拥有副本、相等比较及 serde,简单无载荷枚举才额外 Copy。 lines.append("/// " + type_note(definition.name)) 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} {{") for field in definition.fields: # 注释与字段同序输出,重新生成时一起保留。 lines += ["/// " + field_note(definition.name, field), f"pub {field.name}: {field_type(definition.name, field)},"] else: lines.append(f"pub enum {definition.name} {{") # 每个构造器按稳定顺序生成,不合并名称相近但身份不同的分支。 for variant in definition.variants: lines.append("/// " + variant_note(definition.name, variant)) # 无载荷构造器不携带额外字段,单值和多字段载荷分别采用相应表示。 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 = ",\n".join(f"/// {field_note(definition.name, f)}\n{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} {{") lines.append("/// 原文切片、定位和 Visitor 节点钩子调用,返回存储范围或被包装节点的范围。") lines.append("fn span(&self) -> Span {") if definition.fields is not None: lines.append("self.span") else: lines.append("// 按实际构造器取得唯一可定位子节点的范围。") lines.append("match self {") lines.extend(f"Self::{v.name}(node) => node.span()," for v in definition.variants) lines.append("}") lines.append("} }") 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;", "/// wire 校验与宿主分析实现的借用节点访问协议;默认按 ASDL 字段顺序递归,不修改树。", "/// 覆盖 visit 方法后可显式调用对应 walk 继续子遍历;walk 本身不执行当前节点的进入/退出钩子。", "pub trait Visitor {", "/// 实现者选择的错误类型,任一子访问失败即向上返回。", "type Error;", "/// 默认可定位 visit 在遍历前调用;失败时本节点尚未成功进入,不再调用它的 leave_node。", "fn enter_node(&mut self, _span: Span) -> Result<(), Self::Error> { Ok(()) }", "/// 已成功进入节点的遍历结束后调用,子节点错误也需退出;默认无操作。", "fn leave_node(&mut self) {}", "/// walk 遇到显式 span 字段时调用,默认不处理;区别于围绕整个节点的 enter_node。", "fn visit_span(&mut self, _span: &Span) -> Result<(), Self::Error> { Ok(()) }"] # 按定义顺序遍历/输出每个命名类型,避免由无序集合决定源码结构。 for name in definitions: lines.append(f"/// 上层 walk 或宿主调用的 {name} 入口;默认继续对应子结构,可覆盖此方法收集/校验节点。") 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: lines.append(f"walk_{snake(name)}(self, node)") 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"// 按顺序借用 {access} 中每个 {field.type},任一访问失败立即向上传播。", f"for value in {access} {{ {method}(value)?; }}"] # 可选字段只有 Some/非空时才递归,缺省或 null 不访问。 if field.quantifier == "?": return [f"// {access} 是可选字段,仅 Some 时访问其 {field.type} 子节点。", f"if let Some(value) = {access} {{ {method}(value)?; }}"] return [f"// 访问 {access} 的 {field.type} 子结构或跨度,借用不转移节点所有权。", 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("// 按构造器只递归当前实际载荷,字符串等标量没有语法子节点。") 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("// " + variant_note(definition.name, variant)) 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"/// 默认 visit_{snake(definition.name)} 或自定义访问器调用,按字段顺序借用 {definition.name} 的子结构;本函数不进入/退出当前节点。") lines.append(f"pub fn walk_{snake(definition.name)}({visitor}: &mut V, {node}: &{definition.name}) -> Result<(), V::Error> {{") lines.extend(body) lines += ["Ok(())", "}"] 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, "// 与 Rust wire 共同核对的 ASDL 原文协议身份;生成源码注释不改变此值。", f'export const AST_SCHEMA_ID = "{schema_id}" as const;', "// 原 UTF-8 半开字节范围;number 类型本身不验证安全整数、顺序或原文边界。", "export interface Span { start: number; end: number; }"] # 依稳定 schema 顺序处理每个定义,保留生成结果的可重复性。 for definition in definitions.values(): # 节点及字段说明均输出成独立注释行,不改变 TypeScript 的类型/执行语句。 lines.append("// " + type_note(definition.name)) if definition.fields is not None: for field in definition.fields: lines.append(f"// {field.name}: {field_note(definition.name, field)}") else: for variant in definition.variants: lines.append(f"// {variant.name}: {variant_note(definition.name, variant)}") for field in variant.fields: lines.append(f"// {variant.name}.{field.name}: {field_note(definition.name, field)}") # 产品按字段顺序生成;和类型则按构造器分支选择对应形状。 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) + ";") lines += ["// 编辑器可选节点回调;walk 先回调,返回 false 只剪掉当前子树,其他值继续递归。", "export interface Visitor {", "// 跨度叶子回调没有子结构,其返回值不参与后续剪枝。", "visitSpan?(node: Span): boolean | void;"] for name in definitions: lines += [f"// walk{name} 到达 {name} 时调用,false 跳过该节点的后续子字段。", f"visit{name}?(node: {name}): boolean | void;"] 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。 function = "walkSpan" if field.type == "span" else f"walk{field.type}" # 序列优先映射到 Vec/Array/array;容器本身处理重复元素并打断 Rust 按值递归。 if field.quantifier == "*": return [f"// 按声明顺序访问 {access} 列表中的每个 {field.type} 子节点。", f"for (const value of {access}) {function}(value, visitor);"] # 可选字段只有 Some/非空时才递归,缺省或 null 不访问。 if field.quantifier == "?": return [f"// {access} 可缺省或为 null,只有存在时才访问该子节点。", f"if ({access} != null) {function}({access}, visitor);"] return [f"// 递归访问 {access} 的 {field.type} 子结构或跨度。", f"{function}({access}, visitor);"] lines.append("// 上层 walker/宿主调用的跨度叶子入口,仅通知 visitSpan,不验证输入或修改节点。") lines.append("export function walkSpan(node: Span, visitor: Visitor): void { visitor.visitSpan?.(node); }") # 依稳定 schema 顺序处理每个定义,保留生成结果的可重复性。 for definition in definitions.values(): lines.append(f"// 上层 walker 或宿主调用,先通知 {definition.name} 回调,再按 ASDL 顺序访问子结构;不修改树。") lines += [f"export function walk{definition.name}(node: {definition.name}, visitor: Visitor): void {{", "// 仅显式 false 剪枝,未提供回调或返回 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("// 判别 kind 只选择当前载荷,普通文本/数字拼写没有进一步语法子结构。") lines.append("switch (node.kind) {") # 每个构造器按稳定顺序生成,不合并名称相近但身份不同的分支。 for variant in definition.variants: lines.append("// " + variant_note(definition.name, variant)) lines.append(f"case {json.dumps(variant.name)}:") for field in variant.fields: # 单项载荷取 node.data,多字段载荷再按字段名取 node.data.。 access = "node.data" + ("." + field.name if len(variant.fields) > 1 else "") lines += children(field, access) lines.append("break;") lines.append("}") lines.append("}") 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), "crates/frontend/src/ast/visitor.rs": rust_visitor(definitions), "crates/frontend/editor/ast.ts": editor_types(definitions, schema_id), "crates/frontend/editor/ast.schema.json": json_schema(definitions, schema_id), } # 命令行 --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()