← 返回信息流
AI 资讯Hacker News·2 天前

类Rust编译器流水线解析Matlab语言语义

原标题:Rust-like compiler pipeline to resolve Matlab language semantics

速览

该研究提出了一种类似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] = ... 请求两个输出,该输出计数可以通过 nargoutvarargout 影响被调用者的行为。
  • 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 的语义解析问题。
查看原文 →runmat.com