语言规范 0.49.55 · 编译器 v0.10.4

编译期证明内存安全 · 零 GC · 零 lifetime 符号 · C99 后端自举

编译期证明
零 GC
零 lifetime 符号
atomic T 单关键字
C99 后端
源码自举

核心特性

编译期证明

五类未定义行为(越界 / 溢出 / 除零 / 读未初始化 / 空指针)必须在当前函数内被证明安全,失败即编译错误。

原子类型

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 s14.0 s
make uya 无改动22.4 s0.4 s
make b 自举验证43.1 s26.6 s
前端 parse2.25 s0.54 s
前端 codegen6.70 s2.62 s

提速来自前端按文件 fork 子进程并行(--jobs/-j,默认 CPU 数,上限 32)与 C 阶段 make -j 加增量指纹。

安全模型

编译器在当前函数内验证,证明失败就是编译错误

Uya 不把未定义行为推迟到运行时。越界、溢出、除零、读未初始化内存、空指针解引用,每一项都必须在编译期给出证明;给不出就报错,不降级为运行时检查,也不插入运行时锁。

五类未定义行为的要求

未定义行为 编译器要求
数组越界 常量索引越界 → 编译错误;变量索引 → 必须证明 i >= 0 && i < N
整数溢出 默认开启(--strict-overflow):有符号 + - * 必须可证明不溢出
除零 常量除零 → 编译错误;变量 → 必须证明除数非零
读未初始化内存 必须证明首次使用前已赋值
空指针解引用 必须证明指针非空,或前序已有判空

证明失败时的实际诊断

uya check 输出
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
checked 算术:两个分支都实跑过
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 子命令
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-checkmicroapp 聚合回归
make fmt / make check-fmt格式化 / 格式检查

环境变量

变量作用
UYA_ROOT标准库位置,未设置时取编译器所在目录上一级的 lib
UYA_JOBS并行度(--jobs 优先)
UYA_GCC_JOBSC 阶段 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:i8 i16 i64 u16 返回 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 只能由编译器在离开作用域时插入

两种写法与静态方法

methods.uya
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

hello.uya
extern "libc" fn printf(fmt: *byte, ...) i32;

fn main() i32 {
    printf("hello, uya\n");
    return 0;
}

// 输出:hello, uya

边界检查 → 编译期证明

bounds.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

结构体默认值

conf.uya
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

原子类型

counter.uya
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

shape.uya
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

切片与循环

slice.uya
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

buffer.uya
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.md36 个 @ 内置函数逐条说明
docs/usage_guide.md编译器使用指南
docs/build_speed.md并行与增量构建的实测数据、全部开关
docs/DEVELOPMENT.md开发说明
docs/TESTING.md回归测试说明
buglist.md已知问题与优先级
CHANGELOG.md编译器 changelog
readme.md项目说明与标准库模块一览