Vox 语言参考手册
Vox 语言参考手册
版本:0.7.0
设计哲学:显式、正交、无魔法。无必要不增实体。自由优先,安全可选。
编译目标:转译至 C(原型阶段),最终目标为自举并直接生成机器码。
目录
- 设计原则
- 词法结构
- 基本语法
- 变量与声明
- 类型系统
- 函数
- 控制流
- 结构体与方法
- 枚举与模式匹配
- 引用与借用
- 所有权与转移
- 指针与内存管理
- 数组与切片
- 输入输出
- 模块系统
- 错误处理
- 与 C 的互操作
- 测试
- 语法糖与脱糖规则
- Arena 区域分配
- 附录
1. 设计原则
- 显式优先,可控可免:所有行为优先在代码字面上可见。完全可控、可预测的隐式(如关键字引导的默认行为)不在禁止之列。不存在隐式类型转换、隐式构造函数、隐式
self。 - 正交优于合并:每个特性只做一件事,且彼此独立。你可以自由组合它们,而不是被迫接受一个”全家桶”。
- 无魔法:语言不提供任何需要”背后特殊处理”的语法。所有高级语法(如
try、方法调用)都可通过明确的脱糖规则还原为基础语法。 - 控制权归程序员:内存何时分配、何时释放、数据如何布局,都由你决定。语言提供工具(如
defer),但绝不替你作主。 - 无必要不增实体:不会因为”别的语言有”就加一个特性。每个特性必须解决真实痛点,且不能被已有特性轻易替代。
- 性能可预测:不存在”看似轻量,实则触发隐式分配”的操作。每一行代码的时间与空间成本,都应能从字面上合理推断。
2. 词法结构
2.1 关键字
以下为保留关键字,不可用作标识符。
1
2
3
4
5
6
const var fn struct enum match
if else while for return yield break continue
defer import pub extern
as parse null true false
and or not
test macro comptime asm global move
2.2 标识符
由字母、数字、下划线组成,必须以字母或下划线开头。不允许数字开头。
2.3 字面量
- 整数字面量:十进制
42,十六进制0x2A,八进制0o52,二进制0b101010,可加下划线1_000_000。 - 浮点字面量:
3.14,1.0e-9。 - 字符串字面量:用双引号括起,支持转义
\n、\t、\\、\",类型为[]const u8。 - 布尔字面量:
true、false。 - 空指针字面量:
null,类型可赋值给任意*T/*mut T以及&T/&mut T。 - 单元字面量:
(),类型unit。
2.4 运算符与分隔符
运算符及其优先级见附录。
逻辑运算符(Python 风格):and or not(关键字)
位运算符:& | ^ ~
引用运算符:& &mut
分隔符:{ } ( ) [ ] ; , . => : ..
2.5 注释
- 单行注释:
//至行尾。 - 多行注释:
/* ... */,可嵌套。
3. 基本语法
3.1 语句与表达式
- 语句语境:独立语句、循环体、
if作为语句时等,不产生值。 - 求值语境:赋值右值、函数实参、
return右值、match臂等,必须用yield(块内)或return(函数)提供值。
3.2 分号
语句必须以分号结尾。表达式语句、变量声明、赋值、return、yield、defer、global、move 均需分号。块 {} 之后通常不加分号,除非块作为表达式语句的一部分。
4. 变量与声明
4.1 不可变变量与可变变量
1
2
3
4
5
6
7
8
// 不可变变量(默认)
const x: i32 = 10;
const y = 20; // 类型推导
// 可变变量
var counter: i32 = 0;
counter = 1; // 允许重新赋值
x = 5; // 错误:不可变变量不可赋值
const声明必须初始化,且后续不可修改绑定。var声明允许重新赋值,但类型必须匹配。
4.2 变量遮蔽
Vox 支持跨作用域遮蔽(内层作用域可以声明与外层同名的变量),但禁止同作用域重复声明。
1
2
3
4
5
6
7
8
9
var x: i32 = 5;
// 跨作用域遮蔽:允许
if cond {
var x: f64 = 3.14; // 合法:新作用域,新变量
}
// 同作用域重复声明:禁止
var x: i32 = 10; // 错误:标识符 'x' 已在当前作用域声明
设计理由:同作用域遮蔽会使旧变量隐式”消失”,与显式原则相悖。若需要重新绑定,请使用新名字或块表达式隔离中间变量。
4.3 全局变量
- 模块顶层的
const若无循环依赖,应在编译期求值,值存入只读数据段。 - 模块顶层的
var在进入main前初始化,初始化顺序按依赖拓扑排序。跨模块的可变全局状态由程序员自行保证同步安全。
4.3.1 全局变量的函数内访问 (global)
默认情况下,函数内部无法直接读写模块顶层的变量——它们必须被显式引入。
使用 global 语句可以解除这一限制:
1
2
3
4
5
6
7
8
9
10
11
12
// 模块顶层
var counter: i32 = 0;
fn increment() {
global counter; // 声明我要用外部的 counter
counter = counter + 1;
}
fn read_counter() -> i32 {
global counter;
return counter;
}
批量声明:可以在一个 global 语句中声明多个变量,用逗号分隔。
1
global counter, MAX, app_name;
规则:
global只能用于当前模块顶层已声明的const或var。- 不能通过
global修改const变量。 - 从其他模块导入的变量(如
math.PI)使用模块路径访问,不需要global。 - 若函数内未使用
global声明,直接对与全局变量同名的标识符赋值,将视为对未定义变量的错误。 global声明本身不分配内存,不产生任何运行时开销——它是纯粹的编译期名字绑定指令,翻译后消失。
4.4 move 赋值
Vox 通过 move 关键字提供显式的所有权转移。赋值默认是复制(仅对纯数据安全),当需要将资源的所有权从一个变量转移到另一个变量时,使用 move:
1
2
3
var a = BigData { ... };
var b = move a; // ✅ 显式所有权转移
print(a); // ❌ 编译错误:a 的所有权已被 move
move 是一个表达式操作符,产生被移变量的值。详细语义见第 11 节。
5. 类型系统
5.1 基础类型
| 类型 | 描述 |
| :— | :— |
| i8、i16、i32、i64 | 有符号整数 |
| u8、u16、u32、u64 | 无符号整数 |
| f32、f64 | 浮点类型 |
| bool | 布尔型 (true, false) |
| unit | 单元类型,字面量 (),仅一个值 |
5.2 复合类型
- 定长数组:
[T; N],详见第 13 节。 - 切片:
[]T和[]const T,详见第 13 节。 - 引用:
&T(不可变引用)、&mut T(可变引用)。详见第 10 节。 - 指针:
*T(指向不可变数据)、*mut T(指向可变数据)。另见第 12 节。 - 结构体:
struct { ... },详见第 8 节。 - 枚举:
enum Name { Variant1, Variant2(负载类型), ... },详见第 9 节。
5.3 可选类型
标准库定义 Option<T> 枚举:
1
2
3
4
enum Option<T> {
Some(T),
None,
}
任何可能缺失的值应使用 Option<T> 表达。
5.4 类型转换
as操作符:用于安全的位模式转换或数值宽化/窄化。"123" as i32是编译错误。parse函数:将字符串解析为数值,返回Result<T, ParseError>。
5.5 类型推导
允许在初始化时省略类型标注,推导范围限定为局部变量和函数返回值。函数参数必须显式标注。
6. 函数
1
2
3
fn add(a: i32, b: i32) -> i32 {
return a + b;
}
- 函数体是求值语境,必须显式使用
return提供返回值。 - 所有执行路径都必须遇到
return,否则编译错误。 - 不支持函数重载。Vox 鼓励用明确的函数名或结构体封装参数代替重载。
- 函数参数默认不可变,如需可变需在参数名前加
var(例如fn change(var x: i32))。
6.1 返回类型推导
可从第一个 return 语句推导返回类型,但推荐显式标注。
6.2 参数过多问题的推荐解法
当函数参数超过 3–4 个时,推荐将相关参数组织进一个结构体:
1
2
3
4
5
6
7
8
9
struct Config {
host: []const u8,
port: u16,
timeout: u32,
}
fn connect(cfg: Config) -> Result<Connection, Error> {
// ...
}
这种方式显式、自文档化,且不引入任何额外运行时开销。
7. 控制流
7.1 if 表达式 / 语句
if 可作为语句或表达式,取决于所处语境。
- 作为语句:不要求所有分支产生值,不要求必有
else。 - 作为表达式(求值语境):
if必须包含else,且每个分支必须用yield提供值。
1
2
3
4
5
6
7
8
9
10
11
// if 作为表达式
var status = if cond {
yield 1;
} else {
yield 0;
};
// if 作为语句
if flag {
do_work();
}
7.2 while 循环(语句)
while 是语句,不返回值。
1
2
3
4
5
6
7
8
9
while i < 10 {
i = i + 1;
if i == 5 {
continue;
}
if i == 8 {
break;
}
}
7.3 for 循环(语句)
for 也是语句,不返回值。
1
2
3
4
5
6
7
for item in collection {
print(item);
}
for var idx in 0..len {
arr[idx] = idx;
}
迭代变量默认 const,如需可变用 for var。break 和 continue 可用标签。
7.4 标签化的 break / continue
1
2
3
4
5
6
7
'outer: for i in 0..10 {
for j in 0..10 {
if matrix[i][j] == target {
break 'outer;
}
}
}
7.5 yield 表达式
用在求值语境下的内嵌块(if/match/独立块)中,提供块的值并退出该块。
- 如果在语句语境中出现
yield,编译错误。
7.6 独立块表达式
1
2
3
4
var x = {
var temp = compute();
yield temp + 1;
};
该块必须在所有出口用 yield 提供值。
8. 结构体与方法
1
2
3
4
5
6
7
8
9
struct Point {
x: f64,
y: f64,
}
// 方法定义:第一个参数显式为 self
fn Point.length(self: &Point) -> f64 {
return sqrt((self.x * self.x + self.y * self.y) as f64);
}
- 方法归属 struct 命名空间,通过
Type.method()调用。 - 无函数重载,无隐式
self。 self参数可以是&T(不可变方法)、&mut T(可变方法)或*T/*mut T(底层指针方法)。- 调用语法糖:
p.length()脱糖为Point.length(&p)。 - 若方法需要修改结构体,使用
self: &mut T:1 2 3
fn Point.set_x(self: &mut Point, val: f64) { self.x = val; }
9. 枚举与模式匹配
9.1 枚举声明
1
2
3
4
enum OptionInt {
Some(i32),
None,
}
变体可携带负载类型。
9.2 match 表达式
match是表达式,必须位于求值语境或作为语句都可以(语句时各分支可无yield)。- 在求值语境下,每个分支的块必须以
yield提供值。 - 穷尽性:编译器强制匹配所有变体,否则编译错误。
1
2
3
4
5
6
7
8
var result = match opt {
Some(v) => {
yield v * 2;
}
None => {
yield 0;
}
};
10. 引用与借用
10.1 引用类型
Vox 提供两种引用类型,与裸指针并列但自带编译器安全检查(借用检查):
| 类型 | 含义 | 可读 | 可写 |
|---|---|---|---|
&T |
不可变引用 | ✅ | ❌ |
&mut T |
可变引用 | ✅ | ✅ |
*T |
不可变裸指针 | ✅(不检查) | ❌ |
*mut T |
可变裸指针 | ✅(不检查) | ✅(不检查) |
引用的核心契约:”借用”——你暂时借走访问权,但不拥有数据,用完后必须归还。
10.2 创建与解引用
1
2
3
4
5
6
7
8
9
10
const data: i32 = 42;
// 创建引用
const r: &i32 = &data; // 不可变引用
var val: i32 = 10;
var rm: &mut i32 = &mut val; // 可变引用
// 解引用
var x: i32 = *r; // 通过引用读取
*rm = 99; // 通过可变引用写入
&expr创建不可变引用,&mut expr创建可变引用。- 解引用操作符
*用于通过引用访问其所指向的值。 - 切片引用同理:
&arr[0..3]的类型是&[]i32。
10.3 借用规则
编译器强制以下规则,确保引用不会导致数据竞争或悬空访问:
规则 1(读写分离):任意时刻,对于一个值,要么存在任意多个 &T 引用,要么存在唯一一个 &mut T 引用,不能同时存在。
1
2
3
4
5
6
7
8
var data: i32 = 42;
const a: &i32 = &data; // ✅ 不可变引用 1
const b: &i32 = &data; // ✅ 不可变引用 2(与 a 共存)
print(*a, *b); // ✅ 多个只读读者,安全
var c: &mut i32 = &mut data; // ❌ 编译错误:已有不可变引用活跃
*c = 99;
1
2
3
4
5
6
var data: i32 = 42;
var rw: &mut i32 = &mut data; // ✅ 唯一可变引用
*rw = 99;
const r: &i32 = &data; // ✅ 此时 rw 已不再使用,可创建新的不可变引用
规则 2(原值冻结):当 &mut T 引用存在且活跃时,原值被冻结——不可通过原值路径读或写。
1
2
3
4
5
var data: i32 = 42;
var rw: &mut i32 = &mut data; // rw 借走了 data 的可变访问权
print(data); // ❌ 编译错误:data 已被可变借用冻结
*rw = 99; // ✅ 只能通过借用者访问
规则 3(存活期不超过被借者):引用的生命周期不能超过被引用变量的生命周期。
1
2
3
4
5
6
7
8
var r: &i32; // 声明一个引用变量
{
var local: i32 = 10;
r = &local; // ❌ 编译错误:local 离开作用域后 r 将悬空
}
print(*r);
10.4 引用与裸指针的选择
| 场景 | 推荐使用 | 理由 |
|---|---|---|
| 想要编译器保障安全 | &T / &mut T |
借用检查器防止悬空和数据竞争 |
| 需要指针运算 | *T / *mut T |
引用不提供算术运算 |
| 与 C 代码交互 | *T / *mut T |
直接映射 C 指针 |
| 实现自定义分配器 | *mut T |
底层内存操作需要完全控制 |
| 对 null 语义的需求 | *T / *mut T |
引用不可为 null |
引用和裸指针可以自由互转——这种转换是显式的,编译器不做隐藏的转换:
1
2
3
4
const data: i32 = 42;
const r: &i32 = &data;
const p: *i32 = r as *i32; // 引用 → 裸指针:显式转换
const r2: &i32 = &*p; // 裸指针 → 引用:需确保此时 p 仍有效
10.5 函数中的引用
引用可以作为函数参数和返回值类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 读取值(不可变借用)
fn get_value(data: &i32) -> i32 {
return *data;
}
// 修改值(可变借用)
fn increment(data: &mut i32) {
*data = *data + 1;
}
// 返回引用
fn first(arr: &[i32]) -> &i32 {
return &arr[0];
}
多个引用参数时的生命周期省略规则(由编译器自动推断,无需手动标注):
- 如果只有一个引用参数,其生命周期自动关联到返回值的引用。
- 如果有多个引用参数,且返回值是引用类型,编译器要求显式标注以消除歧义(未来版本可能引入简化标注语法)。
10.6 方法中的 self 引用
方法定义时,self 参数的类型决定了该方法是否允许修改结构体:
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Counter {
value: i32,
}
// 不可变方法:只能读取
fn Counter.read(self: &Counter) -> i32 {
return self.value;
}
// 可变方法:可以修改
fn Counter.add(self: &mut Counter, delta: i32) {
self.value = self.value + delta;
}
10.7 引用与 defer 的关系
引用和 defer 是两个独立的机制,各司其职:
- 引用(
&T/&mut T):管理”访问权”——谁能读、谁能写、能借用多久。 defer:管理”资源”——文件、堆内存、锁等资源何时释放。
两者互补,互不替代:
1
2
3
4
5
6
7
8
9
fn process_with_resource() {
var buf: *mut u8 = alloc.allocate(1024);
defer alloc.deallocate(buf); // defer 管释放
var view: &[]u8 = &buf[0..512]; // 引用管访问安全
process(view);
// view 超出作用域,借用结束
// 但 buf 直到函数退出才由 defer 释放
}
10.8 引用的转译策略(到 C)
引用在转译到 C 时,直接映射为同类型的指针,零运行时开销:
| Vox | C |
|---|---|
&T |
T*(const 限定,若原类型不可变) |
&mut T |
T* |
&expr |
&expr(C 的取地址) |
*r(解引用) |
*r(C 的解引用) |
借用检查是纯编译期机制,转译后完全消失,不影响生成的 C 代码。
11. 所有权与转移
Vox 引入显式所有权转移(move 语义),与引用借用规则(第 10 节)共同构成完整的所有权体系:
1
2
3
引用(&T / &mut T) → 借用:暂时借走访问权,用完归还
move 转移 → 转让:永久转让所有权,原变量失效
赋值(=) → 复制:仅对安全类型(纯数据)允许
move 关键字是 Vox 设计哲学的延续——所有所有权操作在代码字面上显式可见。
11.1 move 表达式
move 是一个一元表达式操作符,将其操作数变量的所有权转移到新变量:
1
2
3
4
var data: [i32; 5] = [1, 2, 3, 4, 5];
var transferred = move data; // data 的所有权转移到 transferred
print(data[0]); // ❌ 编译错误:data 已被 move,不可再使用
print(transferred[0]); // ✅ 转移后的变量正常可用
move 的可见效果:
- 源变量在 move 后被冻结——编译器禁止任何对源变量的读写访问。
- 目标变量获得数据的所有权,其生命周期结束时负责资源的释放(若有)。
move是一个纯编译期概念,不产生任何运行时操作——它只改变编译器对变量活跃状态的跟踪。
11.2 为什么不是隐式 move?
var b = a 不隐含 move 语义。这与 Rust 不同,是设计上的主动选择:
1
2
3
var a = SomeStruct { x: 10 };
var b = a; // ✅ 当前:复制语义(a 仍有效)
var c = move a; // ✅ move 语义(a 失效)
理由:如果 var b = a 这行字面上没有说 a 会失效,那么 a 就不应该失效。”显式优于隐式”原则要求:所有权转移必须通过 move 关键字明确表达。
11.3 隐式复制规则(哪些类型可以安全复制)
并非所有类型都允许隐式复制。编译器根据类型是否包含资源自动判断:
| 类型类别 | 示例 | 隐式复制 var b = a |
|---|---|---|
| 原始类型 | i32, f64, bool, unit |
✅ 允许 |
| 纯数据 struct(无指针、无资源) | struct Point { x: f32, y: f32 } |
✅ 允许 |
| 定长数组(元素为可复制类型) | [i32; 10] |
✅ 允许 |
| 带指针字段的 struct | struct Node { data: *mut u8 } |
❌ 禁止(编译器报错) |
带 defer 资源的类型 |
函数内持有 defer free(p) 的变量 |
❌ 禁止(编译器报错) |
| 引用类型 | &T, &mut T |
✅ 允许(引用是 Copy 的) |
| 切片 | []T, []const T |
❌ 禁止(胖指针涉及数据引用) |
当编译器禁止隐式复制时,程序员必须使用 move 或显式 .clone():
1
2
3
4
5
var buf: *mut u8 = alloc.allocate(1024);
defer alloc.deallocate(buf); // 资源类型
var other = buf; // ❌ 编译错误:不能隐式复制资源类型
var other = move buf; // ✅ 所有权转移
11.4 move 在函数参数传递中
move 也可用于函数调用时显式转移所有权:
1
2
3
4
5
6
7
8
9
fn consume(data: *mut u8) {
// 函数获得所有权
defer alloc.deallocate(data);
process(data);
}
var buf: *mut u8 = alloc.allocate(1024);
consume(move buf); // ✅ 显式转移所有权给函数
print(buf); // ❌ 编译错误:buf 已被 move
函数参数默认不转移所有权——传参默认是复制(对允许复制的类型)或编译错误(对资源类型)。需要转移时必须显式使用 move。
11.5 move 在返回值中
从函数返回变量时,若变量是资源类型,编译器要求:
1
2
3
4
5
fn create_buffer() -> *mut u8 {
var buf = alloc.allocate(256);
defer alloc.deallocate(buf); // 不,defer 会在 return 时执行!
return buf; // ❌ 编译错误:defer 会在 return 前释放 buf
}
正确写法:资源类型的返回必须通过 move 解除 defer 绑定:
1
2
3
4
fn create_buffer() -> *mut u8 {
var buf = alloc.allocate(256);
return buf; // 无 defer:返回所有权,由调用者负责释放
}
编译器规则:若变量有活跃的 defer 资源释放,则不可被返回或 move 到函数参数中。
11.6 move 与引用的交互
一个变量在被借用期间不能 move:
1
2
3
var data: i32 = 42;
var r: &i32 = &data; // 借用 data
var moved = move data; // ❌ 编译错误:data 已被借用(引用 r 活跃)
当借用结束时(引用不再使用),可以 move:
1
2
3
4
5
6
var data: i32 = 42;
{
var r: &i32 = &data; // 借用开始
print(*r);
} // 借用结束,r 超出作用域
var moved = move data; // ✅ 此时可以 move
11.7 move 的转译策略(到 C)
move 是纯编译期概念,在转译到 C 时完全不产生额外代码。它只影响编译器的变量活跃分析:
| Vox | C 效果 |
|---|---|
var b = move a; |
编译器不再允许后续对 a 的访问;生成的 C 代码中 b 接管了 a 的所有引用 |
consume(move buf); |
生成的 C 代码中,buf 变量在函数调用后不再被引用 |
return buf; |
生成的 C 代码等同于普通返回 |
引用检查(借用规则)和所有权跟踪(move)都是纯编译期机制,转译后完全消失,不影响生成的 C 代码。
11.8 move 与 lambda 捕获
move 在 lambda 捕获中也有对应语义——见第 19.4.5 节。
12. 指针与内存管理
12.1 裸指针
*T:指向不可变数据的指针。*mut T:指向可变数据的指针。- 指针运算:
+、-(步长为指向类型大小),*解引用。 - 指针可为
null,用null字面量产生空指针。
12.2 堆分配
标准库提供分配器对象 std_heap,底层为 malloc/free 映射。
1
2
3
var alloc = std_heap;
var mem: *mut u8 = alloc.allocate(1024);
defer alloc.deallocate(mem); // 作用域出口自动释放
12.3 defer 语句
defer 后跟函数调用或块,在所在作用域退出前按后进先出顺序执行。纯编译期语法糖,零运行开销。
1
2
3
4
5
6
7
8
fn process() {
var f = open("data.txt");
defer close(f);
var mem = alloc.allocate(256);
defer alloc.deallocate(mem);
// 无论函数如何退出,两个 defer 都会逆序执行
}
12.4 内存安全哲学
Vox 提供渐进式安全,程序员可以自由选择安全级别:
| 级别 | 使用方式 | 保护程度 | 适用场景 |
|---|---|---|---|
| 安全 | &T / &mut T + defer |
高(编译器检查) | 业务逻辑 |
| 中等 | &T / &mut T + 手动管理 |
中(只防悬空引用) | 性能敏感区 |
| 自由 | *T / *mut T + defer |
低(程序员保证) | 底层系统编程 |
| 裸 | *T / *mut T |
无(完全手动) | 自定义分配器、FFI |
语言本身不强制任何安全级别,程序员通过选择类型来选定保护程度。未来可通过编译选项全局收紧安全检查。
13. 数组与切片
13.1 定长数组 [T; N]
1
2
3
var arr: [i32; 5]; // 声明,未初始化
var arr: [i32; 5] = [1, 2, 3, 4, 5]; // 声明并初始化
const arr = [1, 2, 3]; // 类型推导为 [i32; 3]
- 定长数组是值类型,赋值会复制整个数组。
- 索引越界是未定义行为,由程序员自行保证。
- 数组字面量必须提供与声明大小匹配的元素数量。
访问:
1
2
arr[0] = 10;
var x = arr[1];
13.2 切片 []T 和 []const T
1
2
var slice: []i32 = arr[0..3]; // 可变切片,可修改元素
var view: []const i32 = arr[0..3]; // 不可变切片,只读
- 切片是胖指针,包含数据指针和长度。
- 字符串字面量
"hello"的类型为[]const u8。 []T的元素可修改,[]const T的元素只读。- 切片操作
arr[start..end]返回一个切片,左闭右开。
13.3 切片引用
可通过引用来借用切片的访问权,遵循借用规则:
1
2
3
4
5
var data: [i32; 5] = [1, 2, 3, 4, 5];
const full: &[i32] = &data; // 对整个数组的不可变引用
const part: &[i32] = &data[1..4]; // 对切片子集的不可变引用
var mut_part: &mut [i32] = &mut data[0..2]; // 可变引用
14. 输入输出
Vox 提供两个内置符号用于基本的控制台交互。它们不是关键字,而是编译器自动导入的函数。
14.1 输出
1
2
3
print("Hello, ");
print("World!\n");
print("The answer is ", 42, "\n");
print接受任意数量的参数,按顺序输出到标准输出。- 参数类型必须为
[]const u8、整型、浮点型或布尔型。 - 不自动追加换行符。换行由程序员通过
\n显式控制。 - 不提供格式化占位符。需要格式化时,使用字符串拼接或未来标准库中的格式化模块。
14.2 输入
1
var line = input();
input()从标准输入读取一行,返回[]const u8。- 返回的字符串不包含末尾的换行符。
- 若需要将输入解析为数值,使用
parse函数:
1
var n = parse(input()) as i32;
14.3 实现说明
print 和 input 在转译到 C 时,直接映射到 C 标准库的 printf / puts 和 fgets,无额外运行时开销。
15. 模块系统
文件即模块:每个 .vox 文件就是一个模块。
pub关键字:声明公有符号(函数、类型、变量)。import语句:导入其他模块。
示例:
1
2
3
4
5
6
7
8
9
// math.vox
pub const PI = 3.14159;
pub fn surface(r: f64) -> f64 {
return PI * r * r;
}
// main.vox
import math;
print(math.surface(10.0));
- 导入模板:
import module_name,之后使用module_name.member访问。 - 模块路径对应文件系统:
import math.geometry查找math/geometry.vox。 - 模块通过文件路径隐式形成命名空间,无需手动声明。
16. 错误处理
标准库定义 Result<T, E> 枚举:
1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}
- 推荐函数在可能失败时返回
Result。 - 提供
try操作符(语法糖),若表达式结果为Err则立即从当前函数返回该错误:
1
2
3
4
fn read_config() -> Result<Config, Error> {
var data = try read_file("config.json");
return parse_config(data);
}
17. 与 C 的互操作
- 声明外部函数:
1 2 3 4
extern "C" { fn printf(format: *const u8, ...) -> i32; fn malloc(size: usize) -> *mut u8; }
- 结构布局:默认 C 布局,字段不重排。
- 指针映射:
*T直接对应 C 指针。&T也映射为 C 指针(纯编译期抽象,零开销)。
18. 测试
使用 test 块编写单元测试:
1
2
3
4
5
6
7
test "add positive" {
const result = add(2, 3);
if result != 5 {
print("test failed");
return;
}
}
- 编译时指定
--test标志将运行所有test块。 test块能访问模块私有函数。
19. 语法糖与脱糖规则
19.1 方法调用
obj.method(args) 脱糖为 Type.method(&obj, args)(若 self: &T 或 self: *T)或 Type.method(&mut obj, args)(若 self: &mut T 或 self: *mut T)。
19.2 索引操作
a[b] 脱糖为 a.at(b)(若用于读取)或 a.at_mut(b)(若用于赋值)。因此自定义类型只需实现 at / at_mut 方法即可支持下标语法。
19.3 try 表达式
try expr 脱糖为:
1
2
3
4
match expr {
Ok(val) => val,
Err(e) => return Err(e),
}
19.4 Lambda
Lambda 是纯编译期语法糖,转译到 C 后有明确的映射规则。现代 C 编译器优化能力极强,闭包结构体和函数指针调用通常可被完全内联优化,最终二进制等价于手写 C。
19.4.1 Lambda 语法
Lambda 使用 fn 关键字定义,可选捕获列表声明对外部变量的捕获方式。
捕获列表 [...] 位于参数列表之前,可包含默认捕获关键字和变量捕获项:
| 写法 | 含义 |
|---|---|
[copy] |
默认值捕获——lambda 体内所有用到的外部变量以副本形式捕获 |
[ref] |
默认引用捕获——lambda 体内所有用到的外部变量以引用形式捕获 |
[move x] |
指定 x 以所有权转移方式捕获(x 的所有权移入闭包) |
[base] |
指定 base 以值捕获 |
[&config] |
指定 config 以不可变引用捕获 |
[&mut counter] |
指定 counter 以可变引用捕获 |
[] 或无捕获列表 |
不捕获任何外部变量(纯函数) |
默认捕获关键字和变量捕获项可混合使用,变量项覆盖默认值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 无捕获列表 — 纯函数
var f = fn (x: i32) -> i32 { return x * 2; };
// 指定捕获
var g = fn [base] (x: i32) -> i32 { return x + base; }; // 值捕获
var h = fn [&config] () -> i32 { return config.value; }; // 引用捕获
var i = fn [&mut counter] () -> i32 { counter = counter + 1; }; // 可变引用捕获
// 所有权转移捕获
var resource: *mut u8 = alloc.allocate(1024);
var k = fn [move resource] () -> void { // resource 所有权移入闭包
defer alloc.deallocate(resource);
};
// 默认捕获关键字(编译器推断捕获哪些变量)
var j = fn [copy] (x: i32) -> i32 { return x + base + counter; }; // 默认全部值捕获
var l = fn [ref] () -> i32 { return config.value + counter; }; // 默认全部引用捕获
// 混合:默认 + 个别覆盖
var m = fn [ref, timeout] () -> i32 { // 默认引用,但 timeout 值捕获
return config.value + timeout;
};
var n = fn [copy, &mut counter] () -> i32 { // 默认值拷贝,但 counter 可变引用捕获
counter = counter + config.step;
return counter;
};
var o = fn [copy, move resource] () -> void { // 默认值拷贝,但 resource 所有权转移捕获
process(resource);
alloc.deallocate(resource);
};
规则:
- 每个捕获项为:默认模式关键字(
copy/ref)、name(值捕获)、&name(不可变引用捕获)、&mut name(可变引用捕获)或move name(所有权转移捕获)。 - 捕获列表中最多出现一个默认捕获关键字(
copy或ref),且必须位于列表首位。 - 无捕获列表时(即无
[...]或[]),lambda 体内只能使用参数和局部变量,不能引用外部变量。 - 值捕获存储变量在闭包创建时的快照(副本),引用捕获存储指向外部变量的引用,move 捕获将所有权完全转入闭包(原变量失效)。
- 默认捕获关键字(
[copy]/[ref])是一种可控的隐式:捕获的变量列表由编译器根据 lambda 体自动推断,但捕获方式由程序员显式指定。 - 引用捕获和 move 捕获的 lambda 必须在其捕获变量的生命周期内使用,编译器在借用检查阶段验证这一点。
19.4.2 无捕获 lambda → 函数指针
当 lambda 没有捕获列表(不捕获任何外部变量)时,转译为独立的 C 函数,类型映射为函数指针。
1
2
var f = fn (x: i32) -> i32 { return x * 2; };
var result = f(21);
1
2
3
int32_t lambda__1(int32_t x) { return x * 2; }
int32_t (*f)(int32_t) = &lambda__1;
int32_t result = f(21);
零运行时开销——调用方式与直接函数调用完全一致。
19.4.3 值捕获 lambda → 闭包结构体
当 lambda 按值捕获外部变量时,编译器生成:
- 一个闭包结构体(
struct Closure),捕获变量的副本作为其字段; - 一个独立的调用函数,接受
Closure*作为首个参数(环境指针)。
1
2
3
var base = 10;
var f = fn [base] (x: i32) -> i32 { return x + base; };
var result = f(5);
1
2
3
4
5
6
7
8
9
10
11
12
// 闭包结构体
struct Closure__1 { int32_t base; };
// 调用函数(首参数为环境指针)
int32_t lambda__1(struct Closure__1* env, int32_t x) {
return x + env->base;
}
// Vox 脱糖
int32_t base = 10;
struct Closure__1 __env_1 = { .base = base };
int32_t result = lambda__1(&__env_1, 5);
19.4.4 引用捕获 lambda
当 lambda 通过引用捕获外部变量时,闭包结构体中存储指向外部变量的引用(&T 或 &mut T)而非值副本。
1
2
3
4
5
var counter = 0;
var f = fn [&mut counter] () -> i32 {
counter = counter + 1;
return counter;
};
1
2
3
4
5
6
struct Closure__1 { int32_t* counter; }; // &mut T → T*
int32_t lambda__1(struct Closure__1* env) {
*(env->counter) = *(env->counter) + 1;
return *(env->counter);
}
引用捕获的 lambda 必须在其捕获变量的生命周期内使用,编译器在借用检查阶段验证这一点。
19.4.5 Move 捕获 lambda
当 lambda 通过 move name 捕获外部变量时,变量的所有权从原作用域转移至闭包结构体。原变量在 move 后失效,闭包结构体成为该值的新所有者。
1
2
3
4
5
6
7
var resource: *mut u8 = alloc.allocate(1024);
defer alloc.deallocate(resource); // ❌ 编译错误:resource 被 move 到闭包,不能有 defer
var resource: *mut u8 = alloc.allocate(1024);
var cleanup = fn [move resource] () -> void {
defer alloc.deallocate(resource); // ✅ defer 在闭包内部,资源由闭包管理
};
1
2
3
4
5
6
7
8
9
10
11
12
struct Closure__1 { uint8_t* resource; };
void lambda__1(struct Closure__1* env) {
// defer 展开:函数退出时释放
// ... 函数体逻辑 ...
free(env->resource);
}
// Vox 脱糖
uint8_t* resource = malloc(1024);
struct Closure__1 __env_1 = { .resource = resource };
// resource 此后不可用
规则:
move捕获的变量,在 lambda 创建后原变量失效,不得再使用。- 若原变量有
defer绑定的资源释放,编译器检测到变量被move后,自动将defer从原作用域剥离(交由闭包管理)。 - 编译器在借用检查阶段验证 move 捕获的合法性(是否在借用期内被 move、是否重复 move 等)。
20. Arena 区域分配
Arena(区域分配器)是 Vox 内置的内存管理方案,提供零手动的批量分配与释放。Arena 的核心思想是:在同一作用域内分配的所有对象,在作用域退出时自动一次性释放。
20.1 设计动机
- Rust 的所有权太麻烦:生命周期标注、borrow checker、
Rc/Arc/Box的选择焦虑。 - C/C++ 的手动 free/delete 太烦:容易忘、容易漏、异常路径难以处理。
- Arena 的承诺:
- 不需要写任何
free/delete/defer - 释放是 O(1) 的(一次
free调用) - 与 Vox 的”显式”哲学一致——
arena关键字就是显式声明”这段内存我托管”
- 不需要写任何
20.2 Arena 块(主力方案)
使用 arena 关键字声明一个 Arena 作用域:
1
2
3
4
5
6
arena {
var a = new MyStruct { x: 1, y: 2 };
var b = new MyStruct { x: 3, y: 4 };
process(a, b);
}
// a, b 在此自动释放,不需要写任何 defer 或 free
arena { ... }是一个语句。new T { ... }在当前的最近外层 Arena 中分配一个T类型的对象,返回指向该对象的指针(*T)。- Arena 块退出时,块内所有
new分配的内存被一次性释放。 - Arena 块可以嵌套,内层 Arena 在内层退出时释放,不影响外层。
20.3 new 表达式
new 是一个一元前缀表达式,在当前最近的外层 Arena 中分配内存:
1
2
3
4
5
6
7
8
9
10
// 分配结构体
var p = new Point { x: 1.0, y: 2.0 };
// 分配数组
var arr = new [i32; 10]; // 分配 10 个 i32 的数组,未初始化
var arr = new [i32; 10] { 0 }; // 分配并初始化全部为 0
// new 返回的是指针类型 *T
// 用 * 解引用访问
*p.x = 10;
20.4 Arena 函数(函数级 Arena)
当函数体最外层用 arena 包裹时,整个函数内所有 new 分配在函数退出时统一释放:
1
2
3
4
5
6
7
8
fn process() {
arena {
var temp = new Temp { ... };
var buf = new Buffer[256];
use(temp, buf);
}
// 函数返回时,temp 和 buf 自动释放
}
20.5 返回值跨越 Arena 边界
当需要从 Arena 中返回一个对象到外部时,使用 return 或 yield:
1
2
3
4
5
6
7
fn create() -> *MyStruct {
arena {
var obj = new MyStruct { x: 42 };
return obj; // obj 的所有权转移到调用者
// arena 内其他分配仍自动释放
}
}
编译器规则:
- 通过
return或yield从 Arena 中逃逸的对象,不被 Arena 释放。 - Arena 内其他分配(未逃逸的)照常在块退出时释放。
- 逃逸的对象必须由调用者负责释放(或放入调用者的 Arena)。
20.6 Arena 与现有 Vox 特性的关系
与 defer:独立且互补
1
2
3
4
5
6
7
arena {
var f = open("test.txt"); // 不是 new 分配的,用 defer
defer close(f);
var buf = new [u8; 1024]; // arena 管理,不需要 defer
read(f, buf);
}
与 &/&mut:完全兼容
1
2
3
4
5
arena {
var obj = new MyStruct { x: 1 };
var r: &MyStruct = &*obj; // 对 arena 中的对象创建引用
// 引用在 arena 结束前有效
}
与裸指针:自然对应
new 返回 *T,直接对应 C 的指针,零开销。
20.7 Arena 的转译策略(到 C)
Arena 在转译到 C 时,使用线性分配器模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Vox:
// arena {
// var a = new MyStruct { x: 1, y: 2 };
// var b = new MyStruct { x: 3, y: 4 };
// process(a, b);
// }
// 生成的 C:
{
void* __arena_begin = malloc(4096);
size_t __arena_pos = 0;
struct MyStruct* a = (struct MyStruct*)((char*)__arena_begin + __arena_pos);
__arena_pos += sizeof(struct MyStruct);
a->x = 1; a->y = 2;
struct MyStruct* b = (struct MyStruct*)((char*)__arena_begin + __arena_pos);
__arena_pos += sizeof(struct MyStruct);
b->x = 3; b->y = 4;
process(a, b);
free(__arena_begin); // O(1) 一次性释放
}
优化:对于总分配大小较小的 Arena(例如 < 4096 字节),编译器可以自动使用 alloca 在栈上分配,零堆分配、零释放:
1
2
3
4
5
6
7
{
// 对于小 Arena,使用 alloca 在栈上分配
void* __arena_begin = alloca(512);
size_t __arena_pos = 0;
// ... 同上 ...
// 不需要 free,函数返回时栈帧自动回收
}
20.8 与 GC 的关系
Arena 是 Vox 首选的内存管理方案,覆盖了 90% 以上的场景:
| 场景 | Arena 能不能解决 |
|---|---|
| 临时对象(函数内) | ✅ Arena 块完美解决 |
| 请求级别的生命周期 | ✅ 函数级 Arena 解决 |
| 对象池 | ✅ Arena 是天然的池 |
| 复杂图结构(链表、树、图) | ❌ 节点生命周期不同,需配合手动管理 |
| 长期存活的对象 | ✅ 全局 Arena |
对于 Arena 无法优雅处理的场景(如交叉引用的图结构),未来版本可能引入可选的 GC,但 Arena 始终是 Vox 的内存管理基石。
20.9 设计决策记录
- 为何引入 Arena 而非 GC:Arena 的释放是 O(1) 的,零运行时开销,且与 Vox 的”显式”哲学一致。GC 会引入不可预测的暂停和隐式的释放行为。
- 为何用
new关键字而非函数调用:new在视觉上明确表示”这是堆分配”,与 C++ 的new语义对齐但更简单(不涉及构造函数)。 - 为何
new返回指针而非引用:指针是最低开销的抽象,且与 C 的交互自然。需要安全访问时可以通过&*ptr转换为引用。 - 为何不支持
new在 Arena 块之外:这强制了 Arena 的显式性——如果你需要在 Arena 之外分配,说明你没有声明 Arena,编译器会报错。这是设计上的主动约束,防止忘记声明 Arena。 - 为何不自动使用栈分配:编译器会基于总大小自动选择栈(
alloca)或堆(malloc),程序员不需要关心这一实现细节。 - 为何
arena是一个语句而非表达式:Arena 管理的是副作用(内存分配与释放),作为语句更符合直觉,且与 Vox 的求值/语句语境划分一致。
21. 附录
21.1 运算符优先级
(从高到低)
| 优先级 | 运算符 |
|---|---|
| 1(最高) | () 调用,[] 索引,. 字段访问 |
| 2 | -(一元负),~(按位取反),*(解引用),move |
| 3 | * / % |
| 4 | + - |
| 5 | << >> |
| 6 | & \| ^ |
| 7 | < <= > >= |
| 8 | == != |
| 9 | not(关键字) |
| 10 | and(关键字) |
| 11(最低) | or(关键字) |
赋值运算符 = 优先级低于 or,右结合。
范围运算符 .. 优先级低于赋值运算符。
一元取引用运算符 & / &mut 优先级同解引用(优先级 2)。
21.2 设计决策记录
- 为何引入
move关键字而不是隐式 move:保持”显式优于隐式”原则的一致性。var b = a如果不含有move关键字,则不应改变a的有效性。隐式 move(Rust 风格)虽然简洁,但使所有权转移变得不可见,与 Vox 的核心设计哲学冲突。 - 为何没有 lambda:lambda 需要闭包捕获机制,会引入隐式堆分配和复杂的生命周期管理,与”显式、无魔法”原则冲突。参数过多时,推荐使用结构体封装。(注:引用和 move 引入后,lambda 的设计已有坚实基础,见第 19.4 节。)
- 为何没有函数重载:函数重载会增加符号查找的复杂性,且容易导致隐式行为。Vox 鼓励使用不同函数名或结构体参数。
- 为何文件即模块:参考 Zig 的设计哲学。文件路径自动成为命名空间,无需手动管理
namespace块,一个实体只出现一次。 - 为何需要显式
return:保证求值语境的明确性。每条执行路径的意图都清晰可见。 - 为何引入
global关键字:强制程序员在函数内显式声明对模块级变量的访问,避免不小心遮蔽或无意修改全局状态。该关键字是纯编译期指令,零运行时开销。 - 为何禁止同作用域遮蔽:防止旧变量隐式”消失”,保持作用域内名字绑定的单一性和可预测性。
- 为何选择转译至 C 作为第一步:C 的语义与 Vox 高度匹配,且可直接利用成熟的 C 编译器获得原生性能与平台覆盖,是最快的原型验证路径。
- 为何不默认初始化变量:默认初始化为 0 是一个隐式行为,会带来隐性运行时开销。Vox 强制程序员显式初始化所有变量,或在使用前显式赋值。
- 为何 I/O 极简:
print不自动换行、不提供格式化占位符,所有行为由程序员显式控制。这符合”无魔法”原则。 - 为何使用
&T/&mut T而非其他引用语法:Rust 风格的引用语法是最清晰的选择——&符号在视觉上即表示”借取地址”,与解引用*形成对称。&T与&mut T的区别在类型层面显式表达,一眼可读。 - 为何引入所有权转移(move):所有权转移是引用借用规则的逻辑补充。借用管理”临时访问”,move 管理”永久转让”。两者结合形成完整的资源管控体系,使 lambda 高级捕获(move 捕获)等场景的语义安全可表达。
- 为何引用必须遵守借用规则(原值冻结):
&mut T是对值的独占写入权——借出期间原值被冻结是引用的核心语义。”引用就真的是引用”,不是换了个名字的别名。参见第 10 节。 - 为何引入引用:引用是现代语言编程的基石。对 Vox 而言,引用是必须而非选择——它为生命周期管理提供了安全可控的基础,也使 lambda 等高级特性的加入成为可能。与裸指针(
*T/*mut T)并列,让程序员在安全和自由之间自由选择。 - 为何引入 lambda:lambda 已成为现代语言的一等公民,在许多场景中具有不可替代的表达力。引用和 move 机制的引入使得 Vox 可以设计安全且多样的 lambda 捕获方式——捕获关系在类型层面清晰可见,生命周期由借用检查器保障。
-
为何逻辑运算符选用
and/or/not而非&&/||/!:消除”单 & 是位与还是逻辑与”的语义混淆。Python 方案将逻辑运算和位运算在语法层面彻底分离——逻辑运算用关键字andornot,位运算用符号&|^~——程序员无需依赖优先级记忆,代码扫读时语义一目了然。此设计借鉴自 Python,也是”显式优于隐式”原则的自然延伸。v0.5.0 起生效。
本手册随语言版本持续更新,当前版本为 v0.7.0。