# 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;` 形式用于定义简单类型别名,但目前仅支持聚合类型的别名定义。 ### 聚合类型拓展与match ``` type Expr = enum { Int: i32, Add: struct { left: *Expr, right: *Expr }, fn eval(self: *Expr) i32 { match self { .Int(val) => ret val, .Add(left, right) => ret eval(left) + eval(right), } ret 0; } } type Point = struct { x: i32, y: i32, type Test = enum { TestEnum0, TestEnum1 } fn init(x: i32, y: i32) Point { ret Point { .x = x, .y = y }; } fn dump(self: *Point) void { vm_printf("Point: %d %d", self.x, self.y); } } ``` 这里面包括聚合类型声明除了成员以外任何东西,即整个编译模块也属于聚合类型,将类型抽象化。 其中这里面还包括调用时支持第一个参数self匹配类型时自动填充。 暂时默认全部pub即全部暴露没有不暴露的。 复合enum支持match等语法解包 ``` match self { .Int(val) => ret val, .Add(left, right) => ret eval(left) + eval(right), } ``` --- ## 声明 ### 函数 ``` 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 循环。支持三种形式: 1. **数值区间** `for begin..end as i`:`i` 从 `begin` 到 `end-1`,步长 1 2. **切片遍历** `for slice as val`:遍历切片的每个元素 3. **带索引的切片遍历** `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 中尚未实现。)