AlmidePRE-1.0

Almide · 一门编程语言 · pre-1.0

我正在开发一门语言,让 AI 编写的代码更安全。

AI 智能体写出的代码,已经多到我读不过来了。所以,我希望由编译器来提供安全保障。而编译器自己的工作,也不能由它自己来打分。

ALMIDE编程语言 / 开源
654条定理和引理,在 Rocq 中完成证明并由其内核检查。没有跳过证明,也没有添加公理
100%的测试集在两个目标平台上都附有经机器检查的证据:Wasm 上 1,050 个程序,Rust 上 1,055 个程序
1,324个标准库函数,分布在 51 个模块中,全部用 Almide 自身编写
325 B的 Hello world WebAssembly 文件。直接生成,不经过 LLVM

数据来自公开仓库的提交 79cdadd(2026-11-10;契约数量来自标注为 2026-11-08 的记录)。325 字节对应 0.69.0 版本,未使用 wasm-opt。这些数字还会变化。

01 — 为什么要做

我已经读不完智能体写的所有代码了。

现在,我的很多代码都由 AI 智能体编写。我想,很快会有很多人也处于这种状态,而代码审查跟不上这样的规模。

所以,我想要一门语言,让安全更多地依靠编译器,而不是依靠某个人仔细阅读。对于不是自己写的代码,我也需要一些可以依赖的保障。

我觉得,Rust 是你会希望 AI 使用的语言。但坦白说,我并不擅长 Rust,也一直没能习惯所有权。Almide 是我的一次尝试:保留那样的安全性,却不让任何人或智能体手写所有权相关的部分。

它采用静态类型,通过 Rust 编译为原生代码,也能直接生成 WebAssembly。它提供 Option 和 Result、穷尽模式匹配,以及显式的副作用声明。

02 — 不必手写,就少一处出错

所有权不用你写,由编译器推断。

type Order = { id: Int, items: List[String], total: Int }

fn large(orders: List[Order]) -> List[Order] =
  orders |> list.filter((o) => o.total > 500)

fn main() -> Unit = {
  let orders: List[Order] = [
    { id: 1, items: ["pen", "ink"], total: 260 },
    { id: 2, items: ["desk"], total: 900 },
  ]
  let big = large(orders)
  println("${list.len(orders)} orders, ${list.len(big)} large")
  for o in orders {
    println("${o.id}: ${list.join(o.items, ", ")}")
  }
}
$ almide run orders.almd --target wasm
2 orders, 1 large
1: pen, ink
2: desk

orders 被传给 large,之后又在循环中使用。源码没有标明移动、借用或复制,没有生命周期,也没有 free。

编译器通过 Perceus 风格的推断,确定每个堆上的值在何处创建、共享和消耗。两个目标平台采用不同方式执行这些操作:原生目标输出 Rust,由 Rust 自身的 drop 负责释放;Wasm 使用引用计数。

代价。复制和引用计数操作仍然会发生,只是你无法从源码中看出发生在哪里。在 Rust 中,你能看到 .clone();在这里却看不到。对此,我还没有好的答案。

03 — 我最在意的部分

编译器不能给自己的工作打分。

我不信任自己写的编译器。它是我借助 AI 智能体开发的,我凭什么信任它?

每次构建 Wasm 时,编译器都必须记录自己做过什么,并给出证据:每一处值的共享或释放,每一次数组索引检查。另一个独立程序读取这些证据。如果证据不成立,就不生成输出文件。

被拒绝不是你的代码有 bug,而是编译器出了问题。构建信息会明确说明:

error[certificate-rejected]: app.almd: almide-verify judged this build's
certificate and did not accept it (exit 1)
  app.wasm was not written: the build's own certificate does not hold,
  which is a compiler bug — please report it

已节选。验证器自己的报告接在第一行之后。

04 — TRUST SPINE

检查器也会有 bug,所以一共有四个。

只有一个检查器,只是把信任转移到了别处。这四个检查器以不同方式构建,同一个错误同时出现在四者中的可能性因此较低。项目把它们与所检查的证据统称为 Trust Spine。

◆

整体流程

一份源码,两个目标,各自由谁检查

一个编译器,两个目标。上方是 WebAssembly,下方是 Rust。两条路径之间是它们共用的部分:规则本身、证明规则正确的 Proofcore,以及将规则应用于两条路径证据的 Proofcast。

两条路径的区别在 Wasm 路径中,Buildgate 把关文件写入,Byteanchor 将证据与字节关联。在 Rust 路径中,这个位置由受信任的 rustc 占据。Buildgate 在项目验证中判断证据,而不是在你的构建中;证据目前也还没有与编译后的二进制文件绑定。覆盖率已不再是两者的区别:跨目标测试集中的每个程序,如今都在两条路径上经过认证流程,Wasm 为 1,050 个,Rust 为 1,055 个。超出 Rust 渲染器处理范围的程序,仍会回退到标准代码生成器,不附带证据。

1

Proofcore

Rocq 证明内核

在 CI 中运行

具体来看

(* proofs/MemorySafe.v *)
Definition safe_ev (h : Heap) (e : Ev) : Prop :=
  spatial_ev h e /\ temporal_ev h e /\ init_ev h e /\ type_ev h e.

Definition MemorySafe (t : list Ev) : Prop := holds safe_ev empty_heap t.

Theorem rc_check_gives_temporal : forall t,
  holds fresh_alloc_ev empty_heap t ->
  (forall b, check (gproj b t) = true) ->
  Temporal t.

这就是实际的定义与定理。内存安全由针对每个内存事件的四个条件构成。定理表明:如果引用计数检查接受所有块,就不会访问已释放的内存。Rocq 检查这份证明,无需仅凭信任接受它。

检查器在 Rocq 证明助手中定义并得到证明。证明内核本身就能计算其判定结果。

防范的问题检查器自身的逻辑错误。被证明的是规则本身:若接受证据,则相应性质成立。

2

Proofcast

从证明中生成的检查器

在 CI 中运行

具体来看

witness ownership 8 large
ibd

im

witness bounds 5 (program)
e0 a0

witness init 53 (program)
h12 w4 n2 r1 s0 s1;h12 w4 n2 r0 s0 s1;h12 w4 n1 r0 s0

witness call-modes 25 (program)
1;1 1;1 1|2 1 1;0 1;1 1 1

这就是前面订单示例产生的证据。它是纯文本:对于所有权,每个值占一行,对其进行的每个操作用一个字母表示。检查器只读取这些内容,不读取源码,也不读取编译器内部状态。

将同一个已证明的检查器从 Rocq 提取为独立程序,使它能以足够快的速度运行整个测试集。

防范的问题证明仅停留在纸面上。这里让已证明的规则真正运行在测试集中的每一个程序上。

3

Buildgate

用 Rust 独立编写的第二种实现

在你的电脑上运行

具体来看

$ almide verify orders.almd --artifact orders.wasm
ARTIFACT  sha256 8d407d7b…273e9356  orders.wasm: MATCH
ACCEPT  ownership        large
ACCEPT  ownership        main
ACCEPT  call-modes       (program)
ACCEPT  bounds           (program)
ACCEPT  init             (program)
ACCEPT  fields           (program)
ACCEPT  names            (module:funcs)
ACCEPT  caps             main
ACCEPT  caps-transitive  (program)
  … 35 more lines
almide-verify 0.2.0: 44 witness(es), 44 accepted, 0 rejected, 0 uncertified -> CERTIFIED

# the same check after flipping one bit of orders.wasm
ARTIFACT  sha256 99a2b283…e4078e57  orders.wasm: MISMATCH (the bundle names 8d407d7b…)
almide-verify 0.2.0: 44 witness(es), 44 accepted, 0 rejected, 0 uncertified -> REJECTED

这是 16 行订单示例的实际输出,已节选。编译器记录了 44 条证据,全部被接受。将文件中的一个比特翻转,同样的证据就会被拒绝,因为它记录的是另一个文件。

almide-verify。独立编写的代码,未经形式化证明。它随编译器一起分发,负责在必要时阻止构建。

防范的问题有问题的构建结果写入磁盘。它还通过哈希拒绝针对其他文件记录的证据。

4

Byteanchor

打开最终文件进行检查的工具

在 CI 中运行

具体来看

;; a recorded bounds check must be exactly this, in the file
i64.ge_u          ;; index >= length ?
if
  i32.const …     ;; the evidence is anchored at this instruction
  …
  unreachable     ;; abort
end               ;; and the if has no else

;; a recorded allocation must be exactly this
i32.const N*W
call $alloc
local.set B       ;; every store that follows goes through local.get B

摘自工具自身的源码。它解码最终文件,要求每个记录的事件都对应这样的指令模式。如果记录了一次检查,却没有生成相应指令,就无从对应。

它解码最终的 .wasm 文件,将记录的每个边界、初始化和字段事件与其指向的指令核对。不存在的代码所对应的证据无法通过。

防范的问题针对不存在代码的证据。记录了却没有实际生成的边界检查,会在这里被发现。

在 CI 中,Proofcore、Proofcast 和 Buildgate 必须对测试集每个程序的每一条证据给出一致的判定。这套机制的一次拒绝,已经在我的编译器中发现过真实的错误编译。

仓库中的证明由 46 个 Rocq 文件构成,约 21,700 行,包含 654 条定理和引理。证明针对检查器及其模型,而不是编译器。

证据证明什么

证据按性质分类,每种性质对应一类证据。每类都有自己的规则,四个检查器应用的就是这些规则。

类别防范的 bug接受的必要条件
ownership
防范的 bug重复释放、内存泄漏、释放后使用
接受的必要条件每个对象的引用计数始终不出错,最终归零
bounds
防范的 bug读写超出列表末尾的位置
接受的必要条件在每条执行路径上,每次访问之前都检查了相应位置
init
防范的 bug读取从未写入过的内存
接受的必要条件每次构造都写入所属块的每个位置,且不越界
fields
防范的 bug将值当成错误的结构来处理
接受的必要条件每次记录、元组和变体的存储,以及每次字段访问,都符合其布局
call-modes
防范的 bug调用方与被调用方对参数所有权的理解不一致
接受的必要条件每个调用点都按照被调用函数签名规定的方式传递参数
names
防范的 bug引用不存在的对象
接受的必要条件每个使用的名称都有定义
caps
防范的 bug代码访问文件或网络却没有声明
接受的必要条件函数直接或通过调用可触及的所有能力,都在其声明范围内

这些性质适用于证据覆盖的代码。Byteanchor 将边界、初始化和字段事件与字节重新核对,并把记录的引用计数事件与文件中的调用匹配。无法与指令对应的事件会被计为未定位,而不会悄悄通过。

05 — 代码出错时

错误信息会告诉智能体该改哪里。

智能体会犯错,重要的是接下来怎么办。学过其他语言的智能体可能会写出 items.length:

error[E013]: no field 'length' on list
  --> app.almd:3:14
  in field access .length
  here: println("${items.length}")
  hint: Almide values have no fields — use the `list` stdlib module.
  try: (machine-applicable — `almide fix` applies it)
      list.len(items)
$ almide fix app.almd
  Applied 1 machine-applicable fix-it(s) from diagnostics: E013 at line 3

这种修复是机械性的,因此工具会直接应用。函数名拼写错误则不同:

error[E002]: undefined function 'string.lenn'
  hint: Did you mean `string.len`?

$ almide fix app.almd
1 diagnostic(s) have `try:` snippets that need manual application:
  [E002] app.almd:3:14  undefined function 'string.lenn' (maybe-incorrect)

0.69.0 版本的编译器输出,已节选。

这涉及对作者意图的猜测,所以只保留为建议。我希望工具能清楚区分机械性修复与对意图的判断。

06 — 不让错误进入成品

遗漏分支会导致编译错误,忽略失败也是。

我最常见到的智能体修改错误是:给类型增加一个变体,却漏掉某个必须处理它的位置。

type Shape =
  | Circle(Float)
  | Rect(Float, Float)
  | Triangle(Float, Float)      // added

fn area(s: Shape) -> Float = match s {
  Circle(r) => 3.14159 * r * r,
  Rect(w, h) => w * h,
}                               // not updated
error[E010]: non-exhaustive match: missing Triangle(_, _)
  --> shape.almd:6:36
  hint: add arms for Triangle(_, _):
  Triangle(arg1, arg2) => _

失败也是如此。可能失败的调用会返回 Result,你必须说明如何处理它。直接丢弃它是错误,不是警告。

effect fn load(path: String) -> String = {
  let text = fs.read_text(path)     // can fail, and nothing says so
  text
}
error[E041]: implicit propagation of this binding's value was removed —
this value is a Result
  --> load.almd:4:14
  hint: Write `expr!` to propagate, or consume the Result as a value
  (`??`, `?`, match ok/err).

0.69.0 版本的编译器输出,已节选。

加一个字符就能修复:fs.read_text(path)!。关键在于,作者必须作出决定,而且这个决定在源码中清晰可见。

07 — 一份源码,两个目标

同一份源码生成原生程序和 WebAssembly,要求输出一致。

原生构建通过 Rust 完成。WebAssembly 由编译器直接生成,中间不经过 LLVM。Hello world 生成的是一个 325 字节的 .wasm 文件。

两个后端可能产生不同结果。因此,每一项可观察行为的承诺都记录为契约,目前共有 454 项,每项都关联可执行的证据。CI 在两个目标上运行 1,055 个程序,比较 stdout、stderr 和退出码。第三个执行器也参与核对:它是一个在生成任何代码之前就运行程序的解释器。

比较在文档规定的条件下成立。随机性、时钟和平台细节各有明确限定范围的契约;解释器对其无法运行的 17 个程序不参与判定。

08 — 还有什么

这是一门完整的语言,不是演示。

标准库

51 个模块中的 1,324 个函数,全部用 Almide 自身编写。这是目前最大的 Almide 程序。

一个二进制文件,完整工具链

almide test、fmt、check、fix 和 explain。测试直接写在代码旁边的 test 块中。

显式副作用

effect fn 标记与外部世界交互的代码。传播失败始终需要显式写出 !。

结构化并发

fan 在原生目标上通过线程执行任务,在 Wasm 上保持相同的输出顺序。

可验证的发布包

每个发布资产都有 Sigstore 证明。安装器会核对校验和,缺少证明时拒绝安装。

已经用于实际程序

编程智能体、解析器、2D/3D 渲染器、终端模拟器。2026 年 9 月,144 个公开仓库中包含 Almide 源码。查看列表。

09 — 尚未完成的部分

它还没完成,我想把这一点说清楚。

  • 编译器整体未经形式化证明。证明覆盖检查器及其模型。
  • 原生构建不会运行验证器。它们通过经过认证的渲染器构建,如今整个测试集也都走这条路径。但证据在项目验证中判断,不在你的构建中判断;渲染器无法处理的程序仍会回退到标准代码生成器。
  • 四个检查器中,只有一个在你的电脑上运行。另外三个在 CI 中针对测试集运行。如果没有安装验证器,Wasm 构建会未经检查而完成。
  • 部分证据并不完整。只覆盖程序一部分的证书也会放行。Component 输出不采用这种方式检查。
  • 并非每个记录的事件都已与指令关联。Byteanchor 将无法定位的事件报告为未定位。这个数字并非处处为零。
  • 程序可以通过所有检查,却仍然算出错误的结果。这些检查都不判断逻辑是否正确。
  • 我无法证明智能体写 Almide 比写 Rust 或 TypeScript 更好。我第一次测量时,强模型在每种语言上都接近 100%,所以结果无法说明问题。
  • 目前仍是 pre-1.0。存在已知 bug,也还会发生变化。

哪些已经证明、哪些只是被信任,完整说明在仓库中:已证明与受信任的部分。

10 — 如何开发

我用 AI 智能体开发了它。这正是重点。

编译器用 Rust 编写,开发时我大量使用了 AI 智能体。我先把这件事说清楚,因为有些人不想看以这种方式开发的项目,这可以理解。

但这也解释了项目为何设计成这样。如果编译器由智能体编写,我就不能靠“我读过它”来担保它。因此它必须生成证据,再由其他工具检查证据。每个使用智能体的人,都即将在自己的代码上遇到同样的问题。Almide 是我公开探索这个问题的过程。

11 — 试一试

看看它会在哪里出问题。

在线试用环境直接在浏览器中运行,无需安装。在 macOS 或 Linux 上安装编译器和验证器:

curl --proto '=https' --tlsv1.2 -fsSL \
  https://raw.githubusercontent.com/almide/almide/main/tools/install.sh | sh

安装器需要 GitHub CLI(gh)来验证发布证明。Windows 安装及源码构建说明请参见 README。