11 KiB
SPL — 语法与语义
概述
类 C 语法的系统编程语言,受 Rust/Zig 启发。编译为 SIR(SPL 中间表示),一种基于栈的字节码。多阶段引导:
C → splc0(C) → splc1(SPL) → splc2(SPL) → ...
词法结构
注释
// 行注释
/* 块注释 */
标识符
[a-zA-Z_][a-zA-Z0-9_]*
关键字
as asm bool break catch comptime const continue
defer else enum errdefer false fn for
if loop match null ret struct
test true try type union var void
while _
字面量
| 种类 | 示例 | 类型 |
|---|---|---|
| 整数 | 42 0xFF 0b1010 0o77 |
i32 |
| 字符 | 'A' '\n' '\\' |
i32 |
| 字符串 | "hello" "line\n" |
i8* |
| 布尔 | true false |
bool |
| 空 | null |
null/0/undefinded |
运算符
| 类别 | 符号 |
|---|---|
| 算术 | + - * / % |
| 位运算 | & | ^ ~ << >> |
| 比较 | == != < <= > >= |
| 逻辑 | && || ! |
| 赋值 | = += -= *= /= %= &= |= ^= <<= >>= |
| 其他 | &(取地址) *(解引用) . -> <- .. ... : := ? |
类型系统
基本类型
| 名称 | SIR 类型 | 大小(字节) |
|---|---|---|
| void | SPL_VOID | 0 |
| bool | SPL_I32 | 4 |
| i8 | SPL_I8 | 1 |
| u8 | SPL_U8 | 1 |
| i16 | SPL_I16 | 2 |
| u16 | SPL_U16 | 2 |
| i32 | SPL_I32 | 4 |
| u32 | SPL_U32 | 4 |
| i64 | SPL_I64 | 8 |
| u64 | SPL_U64 | 8 |
| isize | SPL_ISIZE | sizeof(ptr) |
| usize | SPL_USIZE | sizeof(ptr) |
| f32 | SPL_F32 | 4 |
| f64 | SPL_F64 | 8 |
| ptr | SPL_PTR | 8 |
| _ | SPL_ ... | (推断) |
_ 是通配符类型——用作类型推断的占位符。在大多数声明上下文中无效。
指针类型
写作 *T。示例:*i32、*u8、*void。
同类型指针会去重。
数组类型
写作 [N]T。示例:[10]i32。数组是值类型(存在于栈槽或全局数据中)。T 可以是任意类型。
数组/复合类型 在VM堆上申请内存 N * sizeof(T) 。
切片类型
写作 []T。示例:[]i32、[]u8。切片是数组的一段连续视图,底层实现为胖指针:
[]T = struct { ptr: *T, len: i32 }
栈上占 2 个槽位(指针 + 长度)。通过切片表达式从数组创建:
var arr: [10]i32 = ...;
var slice: []i32 = arr[3..7]; // 取 arr[3..7) 视图
var full: []i32 = arr[0..]; // 省略结束值 = 到末尾
此外,切片也可以从另一个切片再切片得到,长度限制为原切片的 len。
聚合类型
type Name = struct { field: Type, ... };
type Name = union { field: Type, ... };
type Name = enum { A, B, C, ... };
- struct:字段按顺序排列(默认由 SPL 定义布局,可通过
#[extern("vm")]覆盖) - union:所有字段共享同一偏移(sizeof = 最大字段大小)
- enum:无字段,值为从 0 开始的整数常量
聚合类型在堆上分配,如果需要栈分配那么栈上占用 size 个槽位(struct 为各字段大小之和,union 为最大字段大小,enum 为 1)。
type Name = ExistingType; 形式用于定义简单类型别名,但目前仅支持聚合类型的别名定义。
声明
函数
fn name(param: Type, ...) ReturnType {
body...
}
fn name(param: Type, ...) ReturnType; // 前向声明一般来说不需要
函数参数在栈上按声明顺序从左到右排列。参数通过 LADDR fp+idx 访问。
原生函数(VM 互操作)
#[extern("vm")]
fn vm_function(arg: Type, ...) ReturnType;
声明一个可通过 NCALL 调用的外部 VM 函数。运行时须链接或通过 spl_syscall_register() 注册实现。
类型别名
type Name = struct/union/enum { ... };
type Name = ExistingType;
变量
var name: Type = expr;
变量在当前函数栈帧上分配空间。声明时若带 = expr 则将表达式值存入栈槽。
常量
const name: Type = expr;
在 splc0 中常量也被分配栈空间(行为与 var 相同)。编译时可求值的常量会被记录到 consts 映射表,允许在局部作用域中引用。
短声明
var name := expr; // 类型推断为 expr 的 type
const name := const_expr; // 类型推断为 expr 的 type
语法糖:声明变量/常量并立即赋初始值,类型从表达式推断。
内置指令(@ 前缀)
以 @ 开头的标识符用于编译器内置操作:
@import
@import("path")
在编译时导入另一个 SPL 源文件。路径相对于当前源文件目录。用于模块化编译。
@sizeof
@sizeof(T)
返回类型 T 的大小(字节),编译期常量。可用于分配内存、I/O 缓冲区。splc0 中暂未实现。
@offsetof
@offsetof(T, field)
返回结构体 T 中字段 field 的字节偏移。实现泛型/运行时反射时有用。splc0 中暂未实现。
@panic
@panic("message")
编译期中断,输出错误信息并终止编译。splc0 中暂未实现。
@assert
@assert(expr)
编译期断言——若 expr 为假则中断编译。splc0 中暂未实现。
@embed
@embed("file")
在编译时将文件内容作为字节数组嵌入程序。splc0 中暂未实现。
语句
块
{ statement; statement; ... }
创建新作用域——局部声明的变量在退出时被丢弃(符号表弹出),且defer也是基于块作用域的, 块可以返回值,只需要最后一个表达式没有分号。
表达式语句
expr;
赋值
name = expr;
name += expr;
name.field = expr;
name[expr] = expr;
支持 =、+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>=。
返回
ret expr;
ret; // void 返回(仅限 void 函数)
RET 指令会根据函数返回类型携带或不带返回值。
If / Else
if expr statement
if expr statement else statement
expr 必须求值为 bool(i32),或者成功语义。statement 可以是块 { }。
实现:BZ 跳转到 else 分支,JMP 跳过 else 分支。
While
while expr { ... }
实现:在循环顶部求值 expr,BZ 跳转到循环结束,循环体末尾 JMP 跳回顶部。
Loop
loop { ... }
无限循环。使用 break 退出,continue 重新开始。
Break / Continue
break;
continue;
break 通过链表记录所有跳出位置,在循环结束后统一回填目标地址。
Defer
defer { ... }
defer statement;
作用域退出时延迟执行
For Range
for 0..N as i { body }
for slice as val { body }
for slice, 0.. as val, idx { body }
Range for 循环。支持三种形式:
- 数值区间
for begin..end as i:i从begin到end-1,步长 1 - 切片遍历
for slice as val:遍历切片的每个元素 - 带索引的切片遍历
for slice, 0.. as val, idx:同时获得元素值和索引
展开实现:
// for 0..N as i { body }
var i: i32 = 0;
while i < N {
// body...
i = i + 1;
}
// for slice, 0.. as val, idx { body }
var idx: i32 = 0;
var _len: i32 = slice.len;
var _ptr: *T = slice.ptr;
while idx < _len {
var val: T = _ptr[idx];
// body...
idx = idx + 1;
}
表达式(优先级爬升)
运算符优先级表
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 1 | || |
左结合 |
| 2 | && |
左结合 |
| 3 | | |
左结合 |
| 4 | ^ |
左结合 |
| 5 | & |
左结合 |
| 6 | == != |
左结合 |
| 7 | < <= > >= |
左结合 |
| 8 | << >> |
左结合 |
| 9 | + - |
左结合 |
| 10 | * / % |
左结合 |
| 前缀 | - ! ~ & * |
右结合 |
| 后缀 | .field [expr] |
左结合 |
|| 和 && 的短路求值:|| 用 BNZ 左侧为真时跳过右侧;&& 用 BZ 左侧为假时跳过右侧。
主要表达式
字面量 → PUSH 立即数
标识符 → LADDR 局部变量地址(可选 LD64 取值)
标识符(args) → 函数调用
(expr) → 分组
- 标识符引用局部变量时:数组/聚合类型压入地址,其他类型压入值(LADDR + LD64)
- 函数调用时 push 参数(从左到右),push 函数地址,CALL nargs
后缀表达式
expr.field → 结构体成员访问(计算字节偏移)(可以单层解引用即 推断c语言的 -> 但是只能单层)
expr[expr] → 数组/指针索引
expr[begin..end] → 切片表达式(从数组/切片创建视图)
expr.* → 解引用(LD64)
.field:根据类型布局计算字节偏移。结构体:地址 + 偏移,嵌套结构体保留地址。指针指向的结构体:先 LD64 解引用获取堆地址,再加字段偏移。[expr]:指针索引用元素字节大小做乘法,数组索引用槽位大小做乘法。最后 ADD 得到元素地址,LD* 加载值。
前缀表达式
-expr → 算术取反(NEG)
!expr → 逻辑非(expr == 0 → EQ + PUSH 0)
~expr → 按位取反(NOT)
&expr → 取地址(LADDR,仅限标识符)
调用约定
见 ../stage0/spl_ir.h
SIR 指令集
见 ../stage0/spl_ir.h
内存模型
- 栈:向上增长。每个槽位为
spl_val_t(uintptr_t,8 字节)。 - 帧指针(fp):当前函数参数的基址。
- 栈指针(sp):栈顶。
- 金丝雀:存储在
data[fp-1](若 fp > 0)——对编译后的代码不可见。 - 全局数据(gdata):按索引引用的字节块。字符串、静态数据等。
编译期执行(Comptime)
comptime { ... }
在编译时执行代码,通过将编译后的 .sir 文件加载到子 VM 中。通过以下系统调用实现:
vm_new() → ptr // 创建子 VM
vm_drop(vm) // 销毁子 VM
vm_load(vm, path) → ptr // 加载 .sir,返回 prog
vm_push(vm, val) // 将参数压入子 VM 栈
vm_call(vm, name, nargs) // 调用函数
vm_run(vm) → i32 // 运行子 VM,返回退出码
(splc0 中尚未实现。)