类Rust编译器流水线解析Matlab语言语义
速览
该研究提出了一种类似Rust的编译器流水线,旨在解决Matlab语言语义的解析问题。这一方法可能为Matlab代码的静态分析和优化提供新的技术路径。
AI 深度解读
Rust 风格的编译器流水线:解决 MATLAB 语言语义难题
背景
如果你只是从外部观察 MATLAB,很容易将其视为一种面向矩阵的脚本语言:数组、绘图、数值例程,以及一个让工程师快速尝试想法的提示符。这些描述都是准确的,这也正是 MATLAB 依然保持相关性的原因:它是工程师和科学家在学习应用数学、仿真、控制论、信号处理、优化和数值计算时,被教导要“用其思维”的语言之一。许多团队继续使用它,因为他们的模型、工具、习惯和领域知识已经建立在其中。
然而,MATLAB 不仅仅是一个简单的脚本环境,它是一个庞大的科学和工程程序语言及运行时环境。真实的 MATLAB 代码通常分散在多个文件中,使用包文件夹、类文件夹、私有辅助文件夹、函数句柄、动态解析调用、工作区状态、基于 classdef 的面向对象类、多返回值、重载索引以及交互式执行。大量的长期工程代码都依赖于这些语义特性。
RunMat 是一个用于此类 MATLAB 家族代码的 Rust 运行时和编译器。因此,它不仅仅试图评估像 A + B 这样简单的表达式,而是需要执行更像以下这种真实世界的程序,同时保留原始程序所依赖的语义:
model = SignalModel(samples);
[filtered, stats] = filters.lowpass(model.samples, 30);
model.history{end + 1} = stats;
plot(filtered);
为了正确运行上述代码,RunMat 在编译和执行期间使几个语言问题显式化:
SignalModel是变量、函数、类构造函数,还是未解析的外部名称?如果是变量,这是否是一个尝试进行的数组/对象调用而非构造?filters.lowpass是指包函数、静态方法,还是字段访问后跟随调用?- 调用者从
lowpass请求了多少个输出,被调用者能否通过nargout观察到这一点? model.samples是存储的属性、依赖属性,还是重载的成员访问?- 在
model.history{end + 1}中,end意味着什么? plot(filtered)应该返回值、更新图形状态,还是抑制普通输出?
这些问题不是解析问题,而是语义问题。对于一个试图运行现有工程代码的团队来说,它们也是兼容性问题。如果运行时回答得太晚,或者解释器、语言服务器、JIT 和 GPU 规划器以不同的方式回答,系统将变得难以推理。
核心内容
RunMat 通过分阶段的编译器流水线来解决这些决策问题:
source -> AST -> semantic HIR -> MIR -> MIR analysis -> VM layout + bytecode -> runtime/providers
这种分阶段处理是与 Rust 的有用连接:源代码在可执行之前被逐步降低为更明确的编译器产物。名称、函数、类、绑定、效果、输出计数、索引上下文和运行时布局成为后续阶段可以重用的事实。
为什么 MATLAB 需要语义解析
MATLAB 的语法紧凑,因为大量的意义由上下文提供。这也是为什么该语言在数值工作中富有生产力,同时也为什么 MATLAB 难以正确执行(理想情况下是静态执行):相同的表面语法可能意味着几种不同的含义,而意图取决于上下文。
回到开头的例子:
SignalModel(samples)可能是类构造函数、函数调用、被索引的变量,或是一个未解析的外部名称。filters.lowpass可能是包函数、静态方法,或字段访问后跟随调用。[filtered, stats] = ...请求两个输出,该输出计数可以通过nargout或varargout影响被调用者的行为。model.samples可能是存储的属性、依赖属性,或重载的成员访问。model.history{end + 1}不仅是索引;它是一个赋值目标,其end取决于history的当前值。plot(filtered)是一个具有图形和显示效果的调用。
因此,相同的表面语法可能意味着几种不同的含义。RunMat 的编译器记录了程序解析到的角色,以便后续运行时路径可以共享相同的答案。
这就是为什么我们选择在流水线中放置一个语义阶段。直接 AST 到字节码的运行时是可能的(这也是我们以前的方法),但这往往使这些决定分散在解释器、运行时调度、编辑器工具、JIT 和加速规划器中。RunMat 自 0.5+ 版本起,现在首先将源代码解析为共享的语言事实。这为系统的其余部分提供了一个地方来询问程序的含义,并让这些事实与现有 MATLAB 用户期望其代码执行的方式相匹配。
流水线概览
当前的流水线如下:
MATLAB source -> parser AST -> semantic HIR assembly -> MIR assembly -> MIR analysis store -> VM assembly layout -> bytecode -> interpreter/JIT/runtime providers
每个阶段都有不同的工作:
- AST 保留语法结构。
- 语义 HIR 说明源代码作为 MATLAB 程序的含义:哪些函数存在,哪些名称绑定到哪些语言实体,定义了哪些类,哪些调用请求哪些输出,以及哪些绑定在工作区中可见。
- MIR 使控制流、位置、调用和效果更易于分析和编译。
- 分析 附加事实。
- VM 布局 将语义绑定映射到可执行帧槽。
- 字节码 是解释器和 JIT 消耗的内容。
- 运行时提供者 处理具体的执行服务,如内置调度、绘图、文件系统访问、工作区实例化和加速。
这种分离的好处在于语言决策变得可重用。解释器可以执行它们,语言服务器可以解释它们,JIT 可以通过它们进行编译,加速规划器可以将它们用作输入。编译器不必从 VM 堆栈形状重新发现调用是两个输出的调用、索引是删除目标,或函数是嵌套捕获。
字节码产品也变得更加有用。它是 VM 字节码,而不是特定于平台的机器码,因此它可以被解释器、JIT、快照和工具消耗。这也是使未来的提前编译(AOT)或静态二进制打包变得合理的路径:源代码可以在后续目标决定如何执行或打包之前,降低为稳定的语义和字节码产品。静态二进制文件仍然需要携带或链接相关的 RunMat 运行时服务,高度动态的 MATLAB 功能仍然需要明确的政策,但架构不再将执行绑定到重新解释源文本。
语义 HIR:语言产物
RunMat 的语义 HIR 是编译器在源布局和名称解析后,记录 MATLAB 程序含义的方式。它表示为程序集(assembly)。
一个程序集拥有以下表格:
- 模块 (modules)
- 入口点 (entrypoints)
- 函数 (functions)
- 类 (classes)
- 绑定 (bindings)
这些是语义 ID,而不是 VM 槽。这种区别对于 MATLAB 很重要,因为用户思考的是变量、函数、文件、类、包和工作区,而不是存储偏移量。嵌套函数中的绑定 total 在 VM 决定它位于帧中的位置之前,具有语言身份。类方法属于类,在运行时决定如何调度它之前。函数调用可以引用内置函数、绑定函数、导入路径、动态名称或外部边界,在发出字节码之前。
HIR 函数携带其 MATLAB ABI:
- 固定输入
varargin - 固定输出
varargout - 隐式
nargin - 隐式
nargout - 捕获 (captures)
- 父函数
- 封闭类(如果有)
这使得 RunMat 能够将开头的示例表示为语言结构,而不是 VM 堆栈形状:SignalModel 可以解析为构造函数,filters.lowpass 可以解析为包函数等。
关键要点
- MATLAB 的复杂性:MATLAB 并非简单的脚本语言,其语义高度依赖上下文(如变量与函数的歧义、动态绑定、多返回值机制),导致静态分析和正确执行极具挑战。
- RunMat 的定位:RunMat 是一个基于 Rust 构建的 MATLAB 运行时和编译器,旨在通过现代编译器技术解决 MATLAB 的语义解析问题。
