Files
spl/stage1/spl.md
2026-07-05 21:45:43 +08:00

12 KiB
Raw Permalink Blame History

SPL — 语法与语义

概述

类 C 语法的系统编程语言,受 Rust/Zig 启发。编译为 SIRSPL 中间表示),一种基于栈的字节码。多阶段引导:

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 必须求值为 booli32或者成功语义。statement 可以是块 { }

实现:BZ 跳转到 else 分支,JMP 跳过 else 分支。

While

while expr { ... }

实现:在循环顶部求值 exprBZ 跳转到循环结束,循环体末尾 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 iibeginend-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_tuintptr_t8 字节)。
  • 帧指针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 中尚未实现。)