核心特性
编译期证明
五类未定义行为(越界 / 溢出 / 除零 / 读未初始化 / 空指针)必须在当前函数内被证明安全,失败即编译错误。
原子类型
atomic T 是并发部分唯一的新关键字,读写与复合赋值编译为原子指令,不加锁。
零 GC
栈数组加显式 IAllocator;结构体离开作用域自动执行 drop,没有回收器线程。
32 个关键字
src/lexer.uya 的 is_keyword 全表共 32 个保留字,全部可在规范里一次读完。
统一结构体布局
Uya struct 一律是 C 内存布局,可直接传给 C;同时可以有方法、drop、实现接口。没有 extern struct 语法。
C99 后端自举
编译器由 Uya 自身实现,输出 C99。默认多文件到 .uyacache,--no-split-c 时输出单文件。
代码量与验证规模
下列数字取自仓库当前提交,不含估算。
| 项 | 数量 | 位置 |
|---|---|---|
| 编译器源码 | 82 个文件 / 129,234 行 | src/**/*.uya |
| 标准库与运行时 | 215 个文件 / 79,264 行 | lib/**/*.uya |
| 标准库模块 | 21 个目录 + 13 个顶层模块 | lib/std/ |
| 测试文件 | 904 正例 + 204 负例 + 72 其他 | tests/*.uya |
| 校验脚本 | 186 个,其中 169 个 verify_* | tests/*.sh |
| 文档 | 232 篇 | docs/**/*.md |
| 最近一次全量验证 | 1153/1153 通过 | v0.10.4 发布记录 |
构建时间
实测环境:44 线程 / gcc 12.3 / Intel,数据见 docs/build_speed.md。
| 场景 | 提速前 | 当前 |
|---|---|---|
make uya 全量 | 22.0 s | 14.0 s |
make uya 无改动 | 22.4 s | 0.4 s |
make b 自举验证 | 43.1 s | 26.6 s |
| 前端 parse | 2.25 s | 0.54 s |
| 前端 codegen | 6.70 s | 2.62 s |
提速来自前端按文件 fork 子进程并行(--jobs/-j,默认 CPU 数,上限 32)与 C 阶段 make -j 加增量指纹。
安全模型
编译器在当前函数内验证,证明失败就是编译错误
Uya 不把未定义行为推迟到运行时。越界、溢出、除零、读未初始化内存、空指针解引用,每一项都必须在编译期给出证明;给不出就报错,不降级为运行时检查,也不插入运行时锁。
五类未定义行为的要求
| 未定义行为 | 编译器要求 |
|---|---|
| 数组越界 | 常量索引越界 → 编译错误;变量索引 → 必须证明 i >= 0 && i < N |
| 整数溢出 | 默认开启(--strict-overflow):有符号 + - * 必须可证明不溢出 |
| 除零 | 常量除零 → 编译错误;变量 → 必须证明除数非零 |
| 读未初始化内存 | 必须证明首次使用前已赋值 |
| 空指针解引用 | 必须证明指针非空,或前序已有判空 |
证明失败时的实际诊断
n1.uya:(2:16): 错误: 数组索引安全证明失败
变量: i, 数组大小: 10
建议: 添加边界检查 (如 if i >= 0 && i < 10)
so.uya:(1:39): 错误: i32 溢出无法证明(--strict-overflow):
改用 try a + b、(a + b) catch、加区间守卫,或用饱和/包装运算符
溢出由调用方选语义
| 写法 | 溢出时 |
|---|---|
a + b |
必须能证明不溢出,否则编译错误 |
a +| b |
饱和到 @max / @min(-| *| 同理) |
a +% b |
回绕(-% *% 同理) |
try a + b |
越界返回 error.Overflow,除零返回 error.DivisionByZero,结果为 !i32 |
fn add_checked(a: i32, b: i32) !i32 {
return try a + b; // 越界 -> error.Overflow
}
fn main() i32 {
const p: i32 = add_checked(1, 2) catch |e| { return 1; };
printf("in range: %d\n", p);
const q: i32 = add_checked(2000000000, 2000000000) catch |e| {
printf("overflow: caught\n");
return 0;
};
printf("unreachable %d\n", q);
return 0;
}
// 输出:
// in range: 3
// overflow: caught
未证明的溢出不会落入 C 未定义行为:构建脚本与默认 CFLAGS 统一带 -fwrapv -fno-strict-overflow,退化为定义良好的回绕。
一句话:程序员给出检查,编译器在当前函数内验证,验证通过才生成代码;验证不通过就是编译错误。
工具链
命令
uya build <file> [-o out] [--c99] 编译为可执行文件或 C 代码
uya check <file> 只做词法 / 语法 / 类型检查
uya run <file> [-- args...] 编译并运行
uya test <file> 运行 test 块
uya fmt <file> 格式化
uya upm <sub> 包管理
常用选项
| 选项 | 作用 |
|---|---|
--c99 | 使用 C99 后端生成 C 代码 |
--no-strict-overflow | 关闭溢出闸门(默认开) |
--jobs N / -jN | 前端进程级并行度,默认 CPU 数,上限 32 |
--split-c-dir [dir] | 多文件 C 输出目录,默认 .uyacache |
--no-split-c | 关闭多文件 C,输出单文件 |
--opt=0..3 / -O0..-O3 | 优化级别,默认 1 |
--nostdlib | 完全独立模式,走 syscall |
--stack-size KB | 栈大小,默认 65536(64 MB) |
构建与验证
| 命令 | 作用 |
|---|---|
make from-c / make from-c-native | 冷启动(Linux / macOS),从 backup/ 的 C99 种子构建 |
make uya / make uya-hosted | 构建自举编译器(nostdlib / hosted) |
make b / make b-hosted | 自举验证:编译器编译自身,比对输出 |
make tests / make tests-uya | 回归套件 |
make check | 全量验证(自举 + 测试 + 各闸门) |
make microapp-check | microapp 聚合回归 |
make fmt / make check-fmt | 格式化 / 格式检查 |
环境变量
| 变量 | 作用 |
|---|---|
UYA_ROOT | 标准库位置,未设置时取编译器所在目录上一级的 lib |
UYA_JOBS | 并行度(--jobs 优先) |
UYA_GCC_JOBS | C 阶段 make -j 并行度 |
UYA_FORCE_BUILD=1 | 跳过增量指纹,强制重建 |
TARGET_OS / TARGET_ARCH | 交叉目标:linux / macos / windows,x86_64 / arm64 / riscv64 |
类型系统
无隐式转换。指针与 FFI 指针分离:&T 用于 Uya 内部,*T 仅用于 C 互操作,转换写 &T as *T。
| 类型 | x86_64 大小 | 说明 |
|---|---|---|
i8 i16 i32 i64 |
1 / 2 / 4 / 8 B | 对齐 = 类型大小;@max / @min 取极值 |
u8 u16 u32 u64 |
1 / 2 / 4 / 8 B | 无符号;溢出是良定义模运算 |
usize isize |
8 B | 指针宽度;isize 当前归一为 i64 |
f32 f64 |
4 / 8 B | 对齐 = 类型大小 |
bool byte |
1 B | byte 与 C char 兼容 |
void |
0 B | 仅作返回类型 |
&T / *T |
8 B | 普通指针 / FFI 指针 |
&[T] / &[T: N] |
16 B | 切片(动态长度 / 编译期已知长度),指针 + 长度 |
[T: N] |
N·sizeof(T) | 定长数组,N 为编译期正整数 |
atomic T |
sizeof(T) | 读写与复合赋值编译为原子指令 |
struct S { } |
字段顺序布局 | 统一 C 内存布局,可直接传给 C;也可有方法、drop、实现接口 |
interface I { } |
16 B | 生成 C 结构 { void *vtable; void *data; };鸭子类型,零注册 |
enum E { } |
sizeof(底层类型) | 默认底层 i32 |
(T1, T2) |
字段顺序布局 | 元组 |
!T |
4 + 对齐 + sizeof(T) | 生成 C 结构 { uint32_t error_id; T value; },!i64 实测 16 B |
@vector(T, N) |
N·sizeof(T) | SIMD 向量;@mask(N) 为比较结果类型 |
上表大小取自生成的 C 结构,不是 @size_of 的输出,因为这两个内置函数在这几类类型上当前不可靠:
@size_of(!T):作为顶层常量时返回1(!i64实际 16 B);写成内联表达式时生成sizeof((!i64)),C 层无法编译。@size_of(IFoo):生成sizeof(IFoo),C 层报IFoo undeclared。@align_of:i8i16i64u16返回1(应为 1/2/8/2);i32/f64/ 指针正确。
以上均已实测,登记在 buglist.md。
结构体方法
方法属于类型命名空间,编译期静态绑定,最终展开为普通函数。有两种等价写法,区别只在排版;fn 首参是不是 receiver 决定它是实例方法还是静态方法。
| 判定 | 条件 | 调用写法 |
|---|---|---|
| 实例方法 | 首参是 &Self / &StructName(联合体为 &UnionName);参数名不限,self 只是惯例 |
obj.method(...),或显式传 receiver 的 Type.method(obj, ...) |
| 静态方法 | 首参不是 receiver(或无参数) | Type.method(...) |
drop |
唯一按值接收的特例:fn drop(self: T) void |
只能由编译器在离开作用域时插入 |
两种写法与静态方法
extern "libc" fn printf(fmt: *byte, ...) i32;
// 写法一:方法直接写在结构体体内,字段与方法同一个花括号
struct Point {
x: i32,
y: i32,
fn norm2(self: &Self) i32 {
return self.x *| self.x +| self.y *| self.y;
}
}
// 写法二:字段单独声明,方法写在同名的 Type { ... } 方法块里
struct Rect {
w: i32,
h: i32
}
Rect {
// 实例方法:首参是 receiver(&Self),用 obj.method() 调用
fn area(self: &Self) i32 {
return self.w *| self.h;
}
// 静态方法:首参不是 receiver,用 Type.method() 调用
fn square(n: i32) Rect {
return Rect{ w: n, h: n };
}
}
fn main() i32 {
const p: Point = Point{ x: 3, y: 4 };
printf("Point.norm2 = %d\n", p.norm2());
const r: Rect = Rect{ w: 3, h: 4 };
printf("Rect.area = %d\n", r.area());
const s: Rect = Rect.square(5);
printf("square(5) = %d\n", s.area());
// 静态方法返回的值可以继续链式调用
printf("chain = %d\n", Rect.square(6).area());
return 0;
}
// 输出:
// Point.norm2 = 25
// Rect.area = 12
// square(5) = 25
// chain = 36
规则
- 方法里可用
Self,静态方法里没有self值:Self是类型占位符(返回值、参数、局部标注都能写),self是实例方法才有的值级 receiver。返回类型写成Self与写成结构体名等价,链式调用都成立。 - 同一类型内不允许重名:静态方法与实例方法同名、两个静态方法同名、两个实例方法同名都报「方法名重复」。Uya 没有重载。
- 接口只描述实例行为:
interface I { fn make() Self; }非法;静态方法不进 vtable,也不参与接口实现完备性检查。 - 联合体静态方法不得与变体同名:
UnionName.variant(...)已占用变体构造语法。 - 两种写法可以混用:字段在结构体体内、方法在外置方法块,或者反过来;也可一部分方法写在体内、一部分写在外置块。
误用时的诊断
| 写法 | 编译结果 |
|---|---|
f.new()(实例调静态方法) |
错误: 结构体上不存在该方法 |
F.get()(类型调实例方法但没给 receiver) |
错误: Type.method(...) 调用实例方法时缺少 receiver 或实参个数不匹配 |
fn new() F 与 fn new(self: &Self) F 并存 |
错误: 类型 'F' 中方法名重复:'new'(Uya 不支持同名静态/实例方法或重载) |
本轮修复的两个缺陷:临时值直接作 receiver(Box.wrap(7).get())此前在 -O1 及以上静默读到 0;方法返回类型写 Self 时链式调用发射 unknown()。两者均已修复并有回归 tests/test_method_temp_receiver.uya,详见 buglist.md。
完整示例
以下每段都用 bin/uya run 实跑通过,输出即注释所示。
Hello, world
extern "libc" fn printf(fmt: *byte, ...) i32;
fn main() i32 {
printf("hello, uya\n");
return 0;
}
// 输出:hello, uya
边界检查 → 编译期证明
extern "libc" fn printf(fmt: *byte, ...) i32;
fn safe_get(arr: &[i32: 10], i: i32) !i32 {
if i < 0 || i >= 10 {
return error.OutOfBounds;
}
return arr[i]; // 到这里编译器已知 0 <= i < 10,直接访问
}
fn main() i32 {
var arr: [i32: 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const v: i32 = safe_get(&arr[0:10], 3) catch |e| {
printf("out of bounds\n");
return 1;
};
printf("v = %d\n", v);
return 0;
}
// 输出:v = 3
结构体默认值
struct Conf {
port: i32 = 8080,
debug: bool = false
}
fn main() i32 {
const def: Conf = Conf{};
const custom: Conf = Conf{ port: 9000 };
printf("def.port = %d, custom.port = %d\n", def.port, custom.port);
return 0;
}
// 输出:def.port = 8080, custom.port = 9000
原子类型
struct Counter {
value: atomic i32
}
Counter {
fn bump(self: &Self) i32 {
self.value += 1; // 编译为原子 fetch_add
const v: i32 = self.value; // 原子 load
return v;
}
}
fn main() i32 {
var c: Counter = Counter{ value: 0 };
_ = c.bump();
const v: i32 = c.bump();
printf("counter = %d\n", v);
return 0;
}
// 输出:counter = 2
接口与 RAII
interface IShape {
fn area(self: &Self) i32;
}
struct Rect : IShape {
w: i32,
h: i32,
fn area(self: &Self) i32 {
return self.w *| self.h; // 饱和乘法,不需要证明
}
}
fn total(s: IShape) i32 {
return s.area();
}
fn main() i32 {
var r: Rect = Rect{ w: 3, h: 4 };
printf("%d\n", total(r)); // 经接口调用
printf("%d\n", r.area()); // 直接调用
return 0;
}
// 输出:12 / 12
切片与循环
fn main() i32 {
var arr: [i32: 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const s: &[i32] = &arr[2:5]; // 从下标 2 起取 5 个元素
printf("len = %d\n", @len(s));
for 0..@len(s) |i| { // 切片不能直接 for,走下标
printf("%d ", s[i]);
}
printf("\n");
for arr |&v| { // 引用迭代,可修改原数组
*v = *v *| 2;
}
printf("arr[0] = %d\n", arr[0]);
return 0;
}
// 输出:
// len = 5
// 2 3 4 5 6
// arr[0] = 0
错误处理与 drop
extern "libc" fn malloc(n: usize) *void;
extern "libc" fn free(p: *void) void;
struct Buffer {
p: *void,
fn drop(self: Buffer) void {
if self.p != null {
free(self.p);
}
}
}
fn buffer_new(n: usize) !Buffer {
const p: *void = malloc(n);
if p == null {
return error.OutOfMemory;
}
return Buffer{ p: p };
}
fn main() i32 {
const b: Buffer = buffer_new(64) catch |e| {
printf("alloc failed\n");
return 1;
};
printf("allocated\n");
return 0; // b 离开作用域,drop 自动 free
}
// 输出:allocated
drop 的约束:只能由编译器在离开作用域时插入,手写 drop(x) 或 x.drop() 在类型检查阶段报错;self 必须按值接收,写成 &Self 会报错。
标准库
入口在 lib/std/(21 个目录 + 13 个顶层模块),lib/ 顶层另有 libc syscall osal mem kernel tls tflm。
| 模块 | 主要内容 |
|---|---|
std.collections |
Vec<T> / HashMap / RingQueue / StringBuf,创建时传入 IAllocator,drop 释放缓冲区 |
std.mem |
IAllocator、arena bump 分配、基于 osal mmap 的堆分配器,不依赖 malloc |
std.crypto |
纯 Uya 实现:MD5、SHA-1、SHA-256、SHA-512、BLAKE2b/2s、BLAKE3、HMAC、CRC32、4096 位 bigint + Montgomery、X25519、Ed25519 |
std.image |
内存位图与基线 JPEG 编解码(Huffman + 8×8 整数 DCT),零 libm、零 C 依赖 |
std.http |
解析 / 路由 / 服务端原语、HTTP/1.1 chunked、HTTP/2 帧与流、HPACK、WebSocket、JWT,以及异步框架 uyagin |
std.async* |
Waker / Future<T> / Poll<T> / Task<T>、帧池、调度器、epoll 事件循环、通道 |
std.process |
Pipeline 与类型化管道 |>:cmd_argv / cwd / env / capture_into |
| 序列化 | json / yaml / protobuf / mqtt / sql(通用抽象层,尚无 SQLite / MySQL 驱动) |
| IO 与网络 | io.file / io.stream / reader / writer;net.addr / net.socket / net.dns |
| 工具 | fmt(uya fmt 后端)、testing、path / env / time / thread / platform、encoding.base64 |
内置函数
规范第 16 章列出 36 个,全部以 @ 开头,无需导入。常用的几个:
| 函数 | 签名 / 语法 | 说明 |
|---|---|---|
@len |
fn @len(a: [T: N]) i32 |
返回数组元素个数 N(编译期常量);对空数组字面量 [] 返回声明时的大小而非 0 |
@size_of |
fn @size_of(T) i32 |
类型字节大小(编译期常量);错误联合与接口上结果不正确或无法编译,见类型系统一节的说明 |
@align_of |
fn @align_of(T) i32 |
类型对齐字节数(编译期常量);i8/i16/i64/u16 目前返回 1 |
@max / @min |
类型由上下文推断 | 整数极值常量,如 const n: u32 = @max; → 4294967295 |
@error_id / @error_name |
fn @error_id(err: error) u32 |
取错误的数值 ID / 语言级名字;error_id = hash(error_name),同一名字在任意编译中稳定 |
@embed / @embed_dir |
fn @embed("path") &[const byte] |
编译期嵌入单文件 / 递归嵌入目录 |
@print / @println |
fn @print(expr) i32 |
按实参 AST 形态选格式串展开为 printf;注意 i32 负值直打有已知缺陷,改用字符串插值 |
@vector / @mask |
@vector(T, N) / @mask(N) |
SIMD 向量与掩码类型,配套 @vector.splat/load/store/select/reduce_* |
| 声明属性 | @async_fn / @naked_fn / @align(N) |
写在 fn 声明前的属性,不是可调用的函数表达式 |
完整清单与逐条说明见 docs/builtin_functions.md;语法层面见 docs/grammar_quick.md。
已知边界
以下都是当前实现的实测行为,不是推测。每条都在本仓库对应的最小文件上复现过。
| 项 | 当前行为 |
|---|---|
| 负索引切片 | &arr[-3:3] 不做 len + start 换算,报「切片起始位置越界」 |
| 切片的 for 迭代 | 切片不能作为 for 受体,需写 for 0..@len(s) |i| { s[i] } |
| 函数指针类型 | type Op = fn(i32) i32; 与 fn(...) 作参数类型都解析失败 |
| 泛型方法块 | Pair<T> { ... } 独立方法块不被接受,方法需写在结构体体内 |
| 指针算术直接返回 | return p + n;(函数返回 &i32)报类型不匹配,需先赋给局部变量 |
@align_of |
i8 i16 i64 u16 返回 1 |
@size_of |
错误联合在顶层常量下返回 1(!i64 实际 16 B),内联时生成无法编译的 sizeof((!i64));接口生成无法编译的 sizeof(IFoo) |
带 *byte 形参的普通函数 |
报「普通函数不能使用 FFI 指针类型作为参数」,改用 &byte 并在调用处 as *byte |
| 枚举 match | 必须带 else 或通配符分支 |
printf 声明 |
写 extern "libc" fn printf(fmt: *byte, ...) i32;;省略 "libc" 会在 C 层与 stdio.h 冲突 |
| 引用迭代 | for arr |&v| 只能用于 var 数组,const 数组报错 |
完整的问题清单、优先级与修复状态见 buglist.md。
文档
| 文档 | 内容 |
|---|---|
| docs/uya.md | 完整语言规范 0.49.55,含每版变更说明 |
| docs/grammar_formal.md | 完整 BNF 语法 |
| docs/grammar_quick.md | 语法速查 |
| docs/builtin_functions.md | 36 个 @ 内置函数逐条说明 |
| docs/usage_guide.md | 编译器使用指南 |
| docs/build_speed.md | 并行与增量构建的实测数据、全部开关 |
| docs/DEVELOPMENT.md | 开发说明 |
| docs/TESTING.md | 回归测试说明 |
| buglist.md | 已知问题与优先级 |
| CHANGELOG.md | 编译器 changelog |
| readme.md | 项目说明与标准库模块一览 |