Metalogos 项目深度解析:~30500 行 Rust 实现栈式 VM,44 条指令,两个执行后端交叉校验,将 XSS 和 SQL 注入变成编译时错误。
大多数编程语言将安全视为外部问题——依赖 linter、SAST 工具、代码审查。Metalogos 走了另一条路:安全约束由类型系统强制执行,在执行前被擦除。本文深入探讨这一思想背后的 Rust 实现,重点关注那些可以超越这门语言复用的架构决策。
该代码库约 30,500 行 Rust 代码、91 条架构决策记录(ADR),以及两个必须在每个程序输出上达成一致的执行后端。它不是玩具。不是 DSL。它是一个编译器、一个字节码虚拟机、一个 HTTP 服务器、一个语义记忆引擎、一个安全审计工具——全部汇聚在一个工作空间中。
解析器使用 Pest 2.7 PEG 语法:384 行、约 180 条规则。这是构建的第一个组件,在接下来 18 个月里保持稳定,而其他一切都在它之下发生变化。
为什么选 PEG 而不是 LALR 或手写递归下降解析器?两个原因:
操作符优先级是声明式的。在 Pest 中,~(序列)和 |(有序选择)使优先级显式化,无需左因式分解的样板代码。Metalogos 为流体类型(Float[42.0][0.9] 或 String["answer"][0.1])和可学习模式(learnable pattern Classify(msg: String) -> String)提供了自定义语法。PEG 可以在没有独立词法分析阶段的情况下处理这些。
语法即契约。每个改变语法的 ADR 都从一条语法规则开始。如果语法无法解析它,这个特性就不存在。这消除了一整类"解析器漂移"bug——即 AST 与解析器不一致的 bug。
AST 有 27 种声明变体、14 种表达式类型、12 种语句类型和 4 种匹配分支模式。所有节点都使用 arena 友好的结构——主要是 Box 和 Vec——通过 Rc 实现共享所有权,因为语义分析器需要从多个上下文引用同一个节点。
Metalogos 有两个语义相同执行后端:
树遍历解释器(约 4,400 行代码):对 AST 的递归求值。支持全部特性,包括所有 246+ 个内置函数。
字节码虚拟机(44 条指令,约 2,100 行代码):对编译后字节码的基于栈的执行。
每个程序都经过两个后端运行。如果输出不同,测试失败。这不是基于属性的测试。这是一种结构性保证:编译器和解释器实现了相同的语义。
最近,一次系统性审计(Наряд №72)发现了 13 个隐藏的分歧点,介于解释器和 VM 之间。这些不是崩溃——而是微妙的语义差异:
规则优先级排序:解释器使用稳定排序,VM 按声明顺序评估规则。
集合标记传播:编译器跟踪了 collections_loaded 但从未将其传递给 VM 的 Program 结构,因此 map/filter/reduce 在 VM 模式下悄然不可用。
宽松测试:VM 不匹配被报告为"跳过"而非失败,掩盖了漂移。
13 个问题全部修复。ADR-0075 记录了交叉检查方法论:现在 58/58 个测试程序通过,零 both-error 情况——每个匹配都是真正的 both-success。
解释器是参考实现。它很慢但显然正确。VM 是生产目标。它更快(约 3-5 倍)但复杂。交叉检查确保这种复杂性不会引入语义 bug。
这个模式可以复用:任何有参考实现和优化实现的系统都可以使用交叉检查验证。代价是增加约 40% 的实现时间。收益是单元测试无法提供的信心。
VM 被刻意设计得极简。每个操作码对应一个动作。所有非原语都是内置函数调用。
常量和变量:Const、LoadGlobal、LoadGlobalByName、StoreGlobal、LoadLocal、StoreLocal。
调用:CallBuiltin(arity, index)、CallPattern(arity, index)、Return。
算术和比较:Add、Sub、Mul、Div、CmpGt、CmpLt、CmpGe、CmpLe、CmpEq、CmpNe。
结构和列表:MakeStruct、GetField、IndexAccess、MakeList、ListLen。
控制流:Jump、JumpIfNot、JumpIfLow(threshold, offset)——最后一个是 Metalogos 特有的:它根据流体类型的置信度分数进行分支。
Metalogos 特有:Collapse(流体类型解析)、Memorize、Recall、Forget、LlmCall、Adapt、Relate、Mutate。
元指令:ExecuteRules、Halt。
编译器是两遍的:
第一遍:收集全局变量、分配槽索引、递归解析导入。
第二遍:生成指令。表达式编译是上下文感知的:模式参数变成 LoadLocal,全局变量变成 LoadGlobal。
流声明编译成一条 FlowPipeline 宏指令。管道的步骤在运行时解析,但步骤名称在编译时验证。
VM 使用 CallFrame 结构体组成的栈:返回地址 + 基指针。这是标准做法,但实现使用 Rust 的 Vec 作为值栈,用 usize 索引作为帧边界——栈操作不涉及 unsafe 代码。
每个编译后的模式都带有一个 is_pure 标记,由编译器的纯度分析设置。如果一个模式没有 LLM 调用、没有副作用、没有全局访问,则它是纯的。这个标记目前用于文档,但为通过 Cranelift 进行 JIT 编译留有脚手架(ADR-0073)。JIT 尚未投入生产,但基础设施已经就位。
Metalogos 有五种不透明类型:Html、Secret、Encrypted、Hash、SqlQuery。这些不是运行时包装器。它们是在语义分析期间强制执行的编译时约束,在代码生成之前被擦除。
语义分析器(semantic.rs)在解析之后、编译之前遍历 AST。它检查:
模板返回类型:只有 Html 是有效的。template Page() -> Secret 是编译时错误。
查询中的字符串插值:query("SELECT * WHERE id = " + id) 失败,因为 String ≠ SqlQuery。
Secret 泄露:print(apiKey) 其中 apiKey: Secret 失败,因为 Secret 在语言层面没有 Display 实现。
HTML 自动转义:类型为 String 的值无法在不显式转换为 Html 的情况下嵌入模板。
在 VM 层面,Secret 就是 String。Html 就是 String。安全保证在编译时强制执行一次,之后不再检查。这与 Rust 的借用检查器原理相同:编译时的不变式,运行时的零成本。
Наряд №74 为 SVG 和 HTML 内置函数(svg_text、html_response、escape_html)添加了静态 lint。它在编译时检查字符串字面量中是否有 javascript: 和 data:text/html 前缀。这不是类型系统的替代品——它是类型系统无法推理字符串内容时的第二层防护。
所有内置函数都在一个 const 数组中声明:
pub const BUILTIN_REGISTRY: &[BuiltinSpec] = &[
spec!("upper", 1, "string"),
spec!("lower", 1, "string"),
// ... 246+ entries
];
顺序决定了字节码索引。这是单一事实来源:添加一行 spec!,在 Builtins::new() 中添加一个处理器,内置函数就同时存在于解释器和 VM 中。
分发系统在解释器端使用 HashMap<String, BuiltinFn>(基于名称),在 VM 端使用直接索引访问(CallBuiltin(arity, index))。注册表保证两条路径解析到相同的实现。
14 个模块:string、math、collections、crypto、llm、http、json、io、memory、cron、server、core、std、tests。
Rust 层面的错误使用 thiserror::Error:
#[derive(thiserror::Error, Debug)]
pub enum RuntimeError {
#[error("parse error: {0}")]
Parse(String),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("llm error: {0}")]
Llm(String),
#[error("sandbox violation")]
Sandbox,
}
语言层面的错误(类型不匹配、未定义变量)是软失败:它们返回 Value::Unit 或 Result<Value, String>。解释器不会 panic。VM 不会崩溃。这对于可能运行 AI 生成代码的语言至关重要——每次类型不匹配都硬失败将使该语言无法用于 LLM 输出。
一个自定义宏替换了 .lock().unwrap():
macro_rules! lock_or_err {
($lock_expr:expr) => {
$lock_expr
.lock()
.map_err(|e| $crate::error::RuntimeError::Lock(e.to_string()))
};
}
没有中毒的 mutex 会让运行时崩溃。
ADR-0023 描述了一个用 Metalogos 编写的自托管词法分析器。它没有功能。测试是 #[ignore]。ADR 中描述的 5 个内置函数从未实现——只注册了操作码索引。该决策(2026 年 8 月重新衡量):将自托管推迟到 v1.0 之后。当前的优先级是 VM 稳定化和将自托管词法分析器/解析器作为垫脚石,而非完整的自举。
Cranelift 集成作为脚手架存在(ADR-0073)。is_pure 标记已设置。JIT 模块未连接到主管道。这是刻意的——正确性优先于性能。
独自构建一门语言是可能的。独自构建一门正确的语言需要纪律:双后端建立信心、trait 实现可扩展性、不透明类型保障安全、ADR 维护理智。
本文中的模式——交叉检查验证、零成本类型擦除、常量注册表、软失败模式——并非 Metalogos 特有。它们适用于任何需要运行时零开销的正确性保证的 Rust 项目。
仓库:https://github.com/ShkodnikAI/Metalogos
许可证:MIT/Apache-2.0
ADR 集合:91 条决策,全部记录在 docs/adr/ 中