TinyActor 中文 · English

TinyActor (TA) Language Specification

语言概述

设计定位:Erlang 风格的 actor 模型 + ML/Rust 系语法的自托管语言。编译器自身用 TA 编写(lib/bootstrap/parser.talib/bootstrap/typecheck.talib/bootstrap/codegen.talib/bootstrap/tokenizer.ta),编译到字节码在 C VM 上运行。

编译流程

source → tokenizer.tokenize → parser.parse → typecheck.infer_program → codegen.compile → VM

运行时:基于字节码的抢占式调度 VM,多线程 worker,每个 actor 是一个轻量进程,通过消息传递通信。

类型系统:Hindley-Milner 类型推导,支持函数注解(fn f(x: int) -> int)、复合类型注解(List(int)Result(int, string))和泛型 ADT 声明(type Color { Red; Green; Blue })。类型检查器 (lib/bootstrap/typecheck.ta) 已接入编译流程,当前为宽容模式(类型错误不阻塞编译,通过 --check 标志报告)。


值类型

类型 字面量 说明
整数 42, -3, 0 48 位有符号整数(NaN-boxing int48,见「整数语义(int48)」
浮点 3.14, -0.5 IEEE 754 double(NaN-boxing 原样存储,见「浮点与严格数值塔」
字符串 "hello" 不可变,heap 分配
布尔 true, false
符号 'foo quote 构造,用于 ADT 变体和模式匹配
Nil nil 空值/空列表
Pair cons(1, 2) (car . cdr),列表是 nil 结尾的嵌套 pair
闭包 fn(x) { x }, fn { ... }
Pid spawn('worker) actor 进程标识符

NaN-boxing 设计:64 位值中,normal double 原样存储(高 16 位不等于 0xFFxx),非 double 类型用高 16 位作 tag(0xFF00=int, 0xFF01=nil, 0xFF02=true, 0xFF03=false, 0xFF04=sym, 0xFF05=pair, 0xFF06=pid, 0xFF07=closure, 0xFF08=string)。浮点数即「原样存储的 normal double」。

没有数组/向量、没有可变引用。 所有值不可变,唯一的状态变化是进程的邮箱。


语法

函数定义

// 命名函数
fn add(x, y) {
  x + y
}

// 带类型注解(可选,注解存在时 parser 额外发出 type-sig 表单)
fn add(x: int, y: int) -> int {
  x + y
}

// 公开函数(可被其他模块调用)
pub fn max(a, b) {
  if a > b { a } else { b }
}

// 匿名函数(带参数)——只能出现在表达式位置(let 值、函数参数等)
let inc = fn(x) { x + 1 };
spawn(fn(x) { loop(x) })

// 匿名函数(无参数,直接执行体)——同样只能出现在表达式位置
spawn(fn { print("hello") })

// 顶层匿名 fn 是 parse error(没有绑定目标,无用途):
//   fn(x) { x + 1 }        // parse error: anonymous fn not allowed at top level
//   fn { print("hi") }     // 同样被拒绝
// 按名称引用已有函数(传递函数值)
spawn('worker)    // 等价于 spawn(fn { worker() })

变量绑定

let x = 42          // 绑定
let y = x + 1       // 后续绑定可引用前面的

// let 在函数体中是顺序的,不支持嵌套 let 作用域语法
// 顶层只能定义函数和类型,不能有顶层 let

use 绑定(do-notation,Gleam 式)

use 是块级脱糖(参考 Gleam 的 use):把一个 monadic 计算绑定到变量,块内其余语句成为延续(lambda body)。与 Gleam 一样,RHS 自己就是延续消费者——脱糖产物是普通调用节点,语法不承诺任何名为 bind 的函数。

use x <- expr
rest...

等价于:

expr(lambda(x) { rest... })   // 即 (expr (lambda (x) rest...)),与 f(a) 同形的调用节点

规则:

示例(parser combinator,mpc 库):

import mpc

fn parse_two(s, i) {
  use k <- mpc.then(take_char('a'))
  use v <- mpc.then(take_char('b'))
  mpc.ret(cons(k, cons(v, nil)))
}

控制流

// if/else
if x > 0 {
  print("positive")
} else {
  print("non-positive")
}

// if 没有 else 时,else 分支为 nil
if done { print("done") }

块表达式

// { } 创建顺序执行块,返回最后一个表达式的值
let result = {
  let a = compute_a()
  let b = compute_b()
  a + b
}

运算符

运算符 说明 备注
+ - * / % 算术 二元。% 仅 int;+ - * / 严格同型(int~int 或 float~float,混用是 type error,见「浮点与严格数值塔」)。int 运算溢出静默回绕;除零/模零导致当前进程死亡(reason 'divzero)——详见「整数语义(int48)」
== 相等 操作数必须同型(int/float 不混用);与 nil 比较豁免。!= 同规则
< > <= >= 比较 严格同型(int~int 或 float~float),结果 bool
&& \|\| 逻辑与/或 二元,短路求值。解析为 ('and ...) / ('or ...),可用于 guard 或普通表达式
\|> 管道(pipe) 见下方「管道运算符」。解析期 desugar,最低优先级、左结合

没有逻辑非 !。tokenizer 不识别 !,parser 也没有对应规则;需要取反时用 x == false 或嵌套 if 替代。

管道运算符 |>

|> 把左侧值作为第一个参数传给右侧的调用:

5 |> double()        // => double(5)
x |> f               // => f(x)            // 裸函数名
5 |> add(3)          // => add(5, 3)
1 |> Pair(2)         // => Pair(1, 2)      // 构造函数
5 |> f() |> g()      // => g(f(5))         // 左结合链式

规则:

5 |> 42             // parse error: 字面量
5 |> 1 + 2          // parse error: 中缀表达式
99 |> { show(7) }   // parse error: 块表达式(不能把值"管道进"块)
5 |> 1.5            // parse error: float 字面量

实现上 parser 用白名单(RHS head 只能是 symbol 或 cons),新增表达式形式默认拒绝,不会静默误解析


整数语义(int48)

本章所有语义断言均经过实际运行验证(探针记录见 .pge/progress.md),并作为 tools/kernfuzz(golden 对拍 + morph 变换)的语义基准:golden 模型 tools/kernfuzz/golden/golden.pyw48 与整型算术规则以本章为准。

表示与取值范围

整数是 48 位二进制补码有符号整数,取值范围 [-2^47, 2^47 - 1],即 [-140737488355328, 140737488355327]。

运行时表示为 NaN-boxing 64 位值:tag 0xFF00,低 48 位载荷(val_payload48), 读取时从第 47 位符号扩展到 64 位(src/val.cval_int / val_get_int)。 任何时刻的 int 值都被归一化到这个区间——把 ≥ 2^47 的值装箱后再读回会变成负数 (str.to_int("140737488355328") 实测返回 -140737488355328)。

回绕语义(静默模 2^48)

+ - * 的结果溢出时静默按模 2^48 回绕(二进制补码回绕),不报错、不饱和、 不升级为浮点。VM 实现(src/vm.cOP_ADD/OP_SUB/OP_MUL)在 int64 中间结果上 运算,装箱(val_int)时截断到低 48 位——效果等价于对每个运算结果做 w48(x) = ((x + 2^47) mod 2^48) - 2^47

实测(与 golden.pyw48 测试向量一致):

// 2^46 = 70368744177664 由连乘构造后:
2^46 * 2      // => -140737488355328   (+1 溢出即回绕到 -2^47)
2^46 * 4      // => 0                  (2^48 mod 2^48)
2^47 - 1      // => 140737488355327    (-2^47 - 1 回绕到 2^47 - 1)
2^46 * 2^46   // => 0                  (2^92 mod 2^48,见下方 UB 说明)

tools/kernfuzz/golden/golden.py 的 Python 模型对每次算术运算后同样调用 w48 归一化, 与 VM 侧行为两侧一致(对拍基准)。

历史备注(issue #92 已推翻):在严格数值塔落地前,本语言曾允许任一操作数为浮点时整个运算切换到 double 语义(「浮点混合例外」,如 INT48_MIN + 1.5 得到 -1.40737e+14)。该例外让 int48 回绕语义被一个 float 字面量静默绕过,已于 issue #92 移除:混用现在是编译期 type error,见下一章。运行时 OP_ADD 等指令对动态值的数值提升行为保持不变(见「浮点与严格数值塔」末段)。

除法与取模

/% 遵循 C 语义:商向零截断余数符号跟随被除数(与 Python 的 floor 除法不同):

-7 / 2    // => -3
-7 % 2    // => -1   (不是 Python 的 1)
7 % -2    // => 1

INT48_MIN / -1 不设陷阱:2^47 装箱回绕,结果仍是 INT48_MIN (实测 -140737488355328);INT48_MIN % -10

除零 / 模零:进程死亡协议

整数 x / 0x % 0 导致当前进程死亡(reason 'divzero),遵循进程隔离协议:

浮点除零不死亡(IEEE 语义,1.0 / 0.0 得到 inf)。

C 有符号溢出(UB)说明

VM 侧算术由 C 代码执行(int64 中间结果),而 C 标准中有符号整数溢出是未定义行为

字面量的已知偏差(实现缺口,待裁定)

运行时算术严格遵循上述回绕语义;但字面量路径目前有偏差:超出 int48 的 十进制字面量在 tokenizer 阶段(str.to_int + 装箱)即被 w48 归一化(这一步正确), 随后 codegen 的 emit_i64lib/bootstrap/codegen.ta)负数分支只对 [-2^32, -1] 正确 (高 32 位硬编码 0xFF),导致 w48 归一化后为负且超出该区间的字面量被写成错误的 字节串。实测:print(140737488355328) 输出 -4294967296,而按 w48 语义应为 -140737488355328。该缺口不改 golden 基准语义,已记录 .pge/progress.md 待裁定; 运行中计算出的值不受影响。


浮点与严格数值塔(issue #92)

TA 采用 OCaml/Gleam 式严格分离的数值塔:int 和 float 是完全不同的类型, 任何运算符都不做隐式转换。

float 表示

float 是 IEEE 754 double,NaN-boxing 下「normal double 原样存储」(高 16 位不等于 0xFFxx,见「值类型」)。字面量形如 3.14 / -0.5(含小数点或指数即 float;3 是 int,3.0 也是 float)。源码层面 float 字面量的文本端到端传递, VM 侧用 strtod 解析(('float "1.5") AST 节点)。

严格同型规则(编译期)

+ - * / < <= > >= 的所有操作数必须统一到同一数值类型:

1 + 2         // ok:int~int
1.5 + 2.0     // ok:float~float
3 + 0.5       // type error:cannot unify
2.5 < 3       // type error:cannot unify

% 仅接受 int(float 取模不在范围内)。类型参数(tvar)不受约束地跟随第一个 操作数的类型绑定,泛型代码(如 fn add(a, b) { a + b })仍然可用。

== / != 同样要求操作数同型:int == float、bool == int、string == int 等 跨类型比较一律 type error(issue #65 的收紧 + issue #92 推翻 3 == 3.0 数值扩宽放行)。唯一豁免:与 nil 的比较——nil == nilx == nilnil != false 等永远合法(nil 是单例,比较不约束另一侧的类型)。

跨塔转换函数

函数 类型 语义
int.to_float(n) int -> float 精确(int48 载荷升宽为 double)
float.to_int(d) float -> int 向零截断(与 / 的商一致;float.to_int(3.9) → 3,float.to_int(-3.9) → -3)

float.to_int 的越界行为:先向零截断,再按 int48 二进制补码回绕(模 2^48)—— 与所有 int 值装箱时的归一化一致(src/num.c);NaN 截断为 0。

int.to_float(3) + 0.5        // => 3.5
float.to_int(7.0) / 2        // => 3

运行时与静态的关系

严格同型只是静态类型层的规则;运行时 OP_ADD / OP_EQ / OP_LT 等指令对 动态构造的值仍按原有数值提升语义执行(int 载荷与 boxed double 混算时升宽为 double)。这一保留是为了不动 VM 语义与 kernfuzz 对拍基准;混用程序在编译期就 被拦截,正常代码不会触达提升路径。


模式匹配

match 表达式

match scrutinee {
  pattern -> expr
  pattern when guard_expr -> expr   // guard: arm matches only if guard is true
  pattern -> expr
  _ -> default
}

Guard 表达式可以引用 pattern 中绑定的变量(n when n > 0 -> ...), 支持 && / || 组合,求值为 false 时跳过该 arm、尝试下一个。

模式语法

模式 匹配 示例
整数字面量 精确匹配 42 -> ...
符号字面量 精确匹配 'hello -> ...
nil 匹配 nil nil -> ...
true/false 匹配布尔 true -> ...
cons(a, b) 解构 pair,绑定 a/b cons(head, tail) -> ...
[a, b, c] 列表模式(语法糖) ['DOWN, r, pid, reason] -> ...
裸符号 变量绑定,匹配任何值 n -> ...
_ 通配符,匹配任何值 _ -> ...

match 编译方式

parser 将 match desugar 为嵌套 if + = 比较:

// 源码
match x {
  Red -> 1
  Green -> 2
  _ -> 3
}

// parser 生成
(let temp x
  (if (= temp 'Red) 1
    (if (= temp 'Green) 2
      3)))

列表模式 ['DOWN, r, pid, reason] desugar 为链式 cons 解构。

穷尽性检查

编译器对 ADT match 进行穷尽性检查(typecheck 层)。如果 match 缺少某个变体且没有 wildcard/binding 臂兜底,编译报错拒绝(Gleam 语义;有意部分匹配需显式加 _ -> 兜底):

[E0005] non-exhaustive match: missing Blue

ADT(代数数据类型)

声明语法

// 零参变体
type Color { Red; Green; Blue }

// 带参数变体
type Option { None; Some(value) }

// 多字段变体
type Pair { MkPair(a, b) }

// 公开类型(跨模块可见)
pub type Msg { Ping(Pid); Pong; Stop }

变体在运行时的表示

变体类型 运行时表示 示例
零参 符号值 Red'Red
带参 函数(构造器) Some(42) → 函数调用,返回包含字段的 pair 结构

parser 生成的 AST

type Color { Red; Green; Blue }
→ (type Color (quote Red) (quote Green) (quote Blue))

type Option { None; Some(value) }
→ (type Option (quote None) (Some value))

pub type Msg { Ping(Pid); Pong; Stop }
→ (type Msg (Ping (quote Pid)) (quote Pong) (quote Stop))

类型注解

函数参数和返回值注解

fn add(x: int, y: int) -> int {
  x + y
}

parser 行为

// 带注解
fn add(x: int, y: int) -> int { x + y }
→ ((type-sig add (int int) int) (define (add x y) (+ x y)))

// 无注解
fn add(x, y) { x + y }
→ ((define (add x y) (+ x y)))

支持的注解类型

基本类型:int, string, bool, pid, Pid, 自定义 ADT 名称(如 Color)。

复合类型注解(Phase 1 引入):支持泛型 ADT 应用,如 List(int)Result(int, string)Option('a)。不支持箭头类型作为注解(如 (int -> int))。


Actor 模型

进程原语

操作 语法 说明
创建进程 spawn('fn_name)spawn(fn { ... }) 返回 Pid
发送消息 send(pid, msg) 异步,消息深拷贝
接收消息 recv() 阻塞,取邮箱下一条消息
接收(带超时) recv_after(ms) 至多等 ms 毫秒,超时返回 nil(邮箱不动)
接收(选择性) receive { pattern -> body } 扫描邮箱,跳过不匹配的
自身 Pid self() 返回当前进程 Pid
监控 monitor(pid) 返回 ref,pid 死亡时收到 ['DOWN, ref, pid, reason]

spawn 语义

// 方式 1:按名称 spawn 一个无参函数
spawn('worker)

// 方式 2:spawn 一个闭包(可捕获变量)
spawn(fn { server(config) })

// 方式 3:spawn 一个匿名函数
spawn(fn(x) { loop(x) })

spawn 的函数在新进程中运行,有自己的栈和邮箱。

消息传递

// 发送任何值
send(pid, 42)
send(pid, 'hello)
send(pid, cons('data, payload))
send(pid, ['DOWN, ref, dead_pid, reason])   // 列表语法

// 邮箱是 FIFO,但 selective receive 可以跳过

recv_after:带超时的接收

let msg = recv_after(1000)   // 至多等 1 秒
match msg {
  nil -> { /* 超时:邮箱原封不动,消息不丢 */ }
  _   -> { /* 收到消息,同 recv() */ }
}

语义对齐 Erlang/Gleam:

receive vs recv

// recv() + match:严格 FIFO,取下一条消息
match recv() {
  'ping -> ...
  'pong -> ...
}
// 如果下一条消息不匹配任何分支 → 进程崩溃

// receive { }:选择性接收,扫描邮箱找匹配的
receive {
  'ping -> ...
}
// 跳过不匹配的消息(保留在邮箱中),直到找到匹配的或阻塞

actor 隔离

抢占式调度


模块系统

import

import tokenizer       // 导入 lib/bootstrap/tokenizer.ta
import parser          // 导入 lib/bootstrap/parser.ta
import msg             // 导入 lib/msg.ta
import foo as f        // 别名:本文件内写 f.member,写 foo.member 报错

模块解析路径(按序):

  1. {导入方所在目录}/{name}.ta(本地模块优先)
  2. {导入方所在目录}/helpers/{name}.ta(测试/辅助模块约定)
  3. lib/{name}.ta(标准库)

导入图按解析后的路径去重(菱形依赖只加载一次),并显式检测环: a -> b -> aerror: circular import: a -> b -> a(取代旧的深度 >16 启发式)。

pub 导出(Phase B,issue #96)

// pub fn:其他模块可通过 module.fn() 调用
pub fn tokenize(src) { ... }

// 非 pub fn:仅模块内部可见(裸名调用),外部 module.fn 引用报错
fn parse_expr(toks) { ... }

// pub type:其他模块可使用该 ADT 的变体(类型与构造子保持全局裸名,Phase C 再命名空间化)
pub type Msg { Ping(Pid); Pong; Stop }

// const:没有 pub const 语法,const 一律视为导出,可用 mod.NAME 引用
const LIMIT = 10

编译期实现(整程序单 .tabc,零 C 改动):driver 在 resolve 阶段把每个 模块单元的定义重命名——pub fn 为 mod.name,非 pub fn 为 mod$name$ 不在标识符字符集内,用户语法无法产生,因此对外不可见),const 为 mod.NAME;模块内部的裸引用同步重写(镜像 codegen.resolve_consts 的 作用域纪律:lambda 参数 / let 绑定 / match·receive 模式绑定遮蔽成员名; quoted 符号仅 const 引用被重写)。typecheck 对 dotted 调用按导出表报错:

旧的“剥掉模块前缀再查裸名”fallback 已删除:两个模块导出同名裸函数不再 互相遮蔽(见 test/module/same-name-exports.ta)。

import 别名

import tokenizer as tok

fn main() {
  tok.tokenize("hi")    // 唯一合法写法
  tokenizer.tokenize("hi")  // 编译期报错:real name 被别名占用
}

别名只作用于声明它的文件;as 之后的成员引用在编译期重写为真名。

调用导入的函数

import math

fn main() {
  print(math.abs(-42))      // module.function() 语法
}

内置函数

数据操作

函数 说明
cons(a, b) 构造 pair
car(p) pair 的 car
cdr(p) pair 的 cdr
null?(x) 是否为 nil
pair?(x) 是否为 pair
int?(x) 是否为整数
string?(x) 是否为字符串
symbol?(x) 是否为符号
print(x) 打印值

字符串(str 模块)

函数 类型签名 说明
str.length(s) string -> int 字符串长度
str.concat(a, b) string -> string -> string 拼接
str.eq(a, b) string -> string -> bool 比较
str.char_at(s, i) string -> int -> int 第 i 字符的 ASCII 码(-1 越界)
str.substr(s, start, len) string -> int -> int -> string 子串
str.to_int(s) string -> int 解析为整数(失败返回 0)
str.from_int(n) int -> string 整数转字符串
str.index_of(s, sub) string -> string -> int 查找子串(-1 未找到)
str.to_sym(s) string -> symbol 字符串转符号
str.sym_to_str(sym) symbol -> string 符号转字符串;运行时收到非 symbol(仅可能经 FFI 边界进入)以 'symtype 原因杀死当前进程,不再静默返回 nil(#101)

列表(via pair)

列表是 nil 结尾的嵌套 pair。没有内置 list 类型,用 cons + nil 构建:

let lst = cons(1, cons(2, cons(3, nil)))   // [1, 2, 3]

列表字面量的类型规则[a, b, c] 是语法糖,解析为 (list a b c) 特殊形式 (不是 cons 链),运行期由 codegen 展开回 cons(a, cons(b, cons(c, nil)))。 typecheck 要求列表字面量的所有元素类型互相统一,异构列表编译报错:

[1, 2, 3]        // OK
["a", "b"]       // OK
[]               // OK(空表,无元素约束)
[1, "a"]         // type error: cannot unify int with string
["kernfuzz", true]  // type error: cannot unify string with bool

手写 cons(h, t) 保持异构 pair 语义(a -> b -> b),不受此规则约束—— cons 是原始 pair 构造器,标准库大量依赖 ('tag . payload) 结构。


自托管编译器

标准库(lib/)

文件 职责
lib/bootstrap/tokenizer.ta 词法分析
lib/bootstrap/parser.ta 语法分析 → AST
lib/codegen.lisp 代码生成(Lisp 语法,编译到字节码)
lib/bootstrap/typecheck.ta HM 类型推导 + ADT + 注解检查
lib/bootstrap/driver.ta 编译驱动:tokenize → parse → typecheck → codegen → run
lib/math.ta 数学工具函数
lib/msg.ta actor 消息类型定义
lib/buf.ta 缓冲区
lib/file.ta 文件 I/O
lib/str.ta 字符串工具

Bootstrap

make tinyactor                     # 构建 C VM
./tinyactor lib/bootstrap/driver.ta file.ta  # 用 TA 编译器编译并运行
make bootstrap                     # 生成 bootstrap 字节码

编译器可以用自己编译自己(bootstrap)。


完整示例

基本 actor 系统

type Msg { Ping(Pid); Pong; Stop }

fn server() {
  match recv() {
    Ping(from) -> {
      send(from, Pong)
      server()
    }
    Stop -> print("done")
  }
}

fn main() {
  let pid = spawn(fn { server() })
  send(pid, Ping(self()))
  match recv() {
    Pong -> print("got-pong")
  }
  send(pid, Stop)
  print("PASS")
}

Supervisor 模式

fn worker(id) {
  let msg = recv()
  if msg == 'crash {
    1 / 0                        // 故意崩溃
  } else {
    print(msg)
    worker(id)
  }
}

fn supervisor() {
  let pid = spawn(fn { worker(0) })
  let ref = monitor(pid)
  send(pid, 'crash)
  sup_loop(pid, ref, 0)
}

fn sup_loop(pid, ref, count) {
  match recv() {
    ['DOWN, r, p, reason] -> {
      print("worker died")
      if count < 2 {
        let new_pid = spawn(fn { worker(count + 1) })
        let new_ref = monitor(new_pid)
        send(new_pid, 'crash)
        sup_loop(new_pid, new_ref, count + 1)
      } else {
        print("giving up")
      }
    }
  }
}

fn main() {
  spawn(fn { supervisor() })
  recv()
}

尾递归

// TCO 保证:500 万次迭代不爆栈
fn sum(r, i) {
  if i == 0 {
    r
  } else {
    sum(r + 1, i - 1)    // 尾调用
  }
}

fn main() {
  print(sum(0, 5000000))
}

类型检查

--check 标志

在编译命令后添加 --check 标志可启用类型错误报告:

NWORKERS=1 ./tinyactor --bootstrap source.ta '' --check

类型检查器会推导所有函数类型,验证注解,并报告不匹配错误:

typecheck: 2 type error(s) found
  in function 'bad_if':   cannot unify int with bool
  in function 'bad_call': cannot unify string with 'a

错误信息包含出错的函数名和类型冲突详情。类型错误不会阻止编译——编译器仍然生成字节码并运行程序。--check 仅提供类型安全方面的诊断信息。

注解强制执行

当函数声明了类型注解时,类型检查器会:

  1. 注册声明类型fn f(x: int) -> int 注册 int -> int 作为期望类型
  2. 推导实际类型:从函数体推导实际类型
  3. 统一检查:如果两者不匹配,报告类型错误

未注解的函数不受影响——它们照常推导但不会与声明类型比较。

内建函数类型签名

类型检查器内置了常用函数的类型签名,无需注解即可正确推导:

分类 函数 类型
算术 + - * / int -> int -> int
比较 < > <= >= int -> int -> bool
相等 == forall a. a -> a -> bool
布尔 not bool -> bool
列表 car cdr forall a. a -> a
构造 cons forall a b. a -> b -> b
谓词 null? pair? int? string? symbol? forall a. a -> bool
字符串 str.concat str.eq str.length 见上方字符串函数表
Actor spawn self forall a. a -> pid
消息 send forall a b. a -> b -> b
消息 recv forall a. a

类型系统当前能力边界

能力

能力 状态 示例
整数运算推导 fn f(x) { x + 1 }int -> int
多态推导 fn id(x) { x }'a -> 'a
高阶函数 fn app(f, x) { f(x) }('a -> 'b) -> 'a -> 'b
递归函数 fn fact(n) { ... }int -> int
ADT 变体 RedColor
泛型 ADT type List { Nil; Cons(a, List(a)) }List(int)
Actor 原语 spawn'a -> pid
函数注解验证 fn add(x: int, y: int) -> int 匹配推导
复合类型注解 fn f(xs: List(int)) -> int 验证参数类型
类型错误报告 --check 标志输出 in function 'foo': cannot unify int with string
内建函数签名 str.from_intint -> string+int -> int -> int

不支持

缺失 说明
List 类型 没有 List(a) 内置类型,nil/cons 组合只推导为 pair + tvar,无法区分空列表和空值
箭头类型注解 不能写 (int -> int) 作为参数或返回值注解
类型错误为硬错误 类型检查仍为宽容模式,类型错误不阻止编译(仅 --check 时报告)
Symbol 基础类型 类型系统没有 symbol 基础类型,str.to_sym/str.sym_to_str 使用宽松的多态类型

nil 的类型问题

nil 在类型系统中推导为 fresh tvar('b),既是空值又是空列表。HM 无法区分:

对比 Gleam:没有 nil,用 Result(a, b) / Option(a) 代替,List 有明确的 List(a) 类型。


限制与约束