stage0 更改名称spl_ir -> spl_mcode spl_cli提供debug模式 提供临时语言规范

This commit is contained in:
zzy
2026-07-29 12:05:18 +08:00
parent 89d4bae8db
commit a77eade06f
10 changed files with 1224 additions and 476 deletions

1175
SPL.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,23 +4,32 @@
* Built-in syscalls are auto-registered via spl_syscall_register(). * Built-in syscalls are auto-registered via spl_syscall_register().
* *
* Usage: * Usage:
* spl_cli <file.sir> [entry_point] * spl_cli [-d] <file.sir> [entry_point]
*/ */
#include "spl_ir.h" #include "spl_mcode.h"
#include "spl_syscall.h" #include "spl_syscall.h"
#include "spl_vm.h" #include "spl_vm.h"
#include <stdio.h> #include <stdio.h>
#include <string.h>
int main(int argc, const char **argv) { int main(int argc, const char **argv) {
if (argc < 2) { int debug_mode = 0;
fprintf(stderr, "Usage: spl_cli <file.sir> [entry_point]\n"); int arg_idx = 1;
if (argc >= 2 && strcmp(argv[1], "-d") == 0) {
debug_mode = 1;
arg_idx = 2;
}
if (arg_idx >= argc) {
fprintf(stderr, "Usage: spl_cli [-d] <file.sir> [entry_point]\n");
return 1; return 1;
} }
const char *path = argv[1]; const char *path = argv[arg_idx];
const char *entry = argc >= 3 ? argv[2] : "main"; const char *entry = argc >= arg_idx + 2 ? argv[arg_idx + 1] : "main";
spl_prog_t prog; spl_prog_t prog;
if (spl_prog_load_from_file(path, &prog) != 0) { if (spl_prog_load_from_file(path, &prog) != 0) {
@@ -32,6 +41,7 @@ int main(int argc, const char **argv) {
spl_vm_t vm; spl_vm_t vm;
spl_vm_init(&vm); spl_vm_init(&vm);
if (debug_mode) spl_vm_set_debug(&vm, 1);
if (spl_vm_load_prog(&vm, &prog) != 0) { if (spl_vm_load_prog(&vm, &prog) != 0) {
fprintf(stderr, "vm: prog '%s' not found\n", entry); fprintf(stderr, "vm: prog '%s' not found\n", entry);
spl_prog_drop(&prog); spl_prog_drop(&prog);

View File

@@ -3,7 +3,7 @@
* Usage: spl_disasm <file.sir> * Usage: spl_disasm <file.sir>
*/ */
#include "spl_ir.h" #include "spl_mcode.h"
#include <stdio.h> #include <stdio.h>
int main(int argc, const char **argv) { int main(int argc, const char **argv) {

View File

@@ -1,4 +1,4 @@
/* spl_ir.c — SIR binary serialization, deserialization, and utilities /* spl_mcode.c — SPL VM machine code binary serialization, deserialization, and utilities
* *
* Binary format (all metadata fields are spl_val_t = uint64_t LE): * Binary format (all metadata fields are spl_val_t = uint64_t LE):
* [HEADER] magic(8) nfuncs(8) ninsns(8) nnatives(8) nstrs(8) ndata(8) * [HEADER] magic(8) nfuncs(8) ninsns(8) nnatives(8) nstrs(8) ndata(8)
@@ -9,7 +9,7 @@
* [STRTAB] each: slen(8) str(slen bytes, padded to 8) * [STRTAB] each: slen(8) str(slen bytes, padded to 8)
*/ */
#include "spl_ir.h" #include "spl_mcode.h"
void spl_prog_init(spl_prog_t *prog) { void spl_prog_init(spl_prog_t *prog) {
if (!prog) if (!prog)
@@ -445,12 +445,10 @@ const char *opcode_name[] = {
const char *spl_opcode_name(spl_opcode_t opcode) { return opcode_name[opcode]; } const char *spl_opcode_name(spl_opcode_t opcode) { return opcode_name[opcode]; }
const char *spl_type_tag_name(spl_type_t type) { const char *spl_type_tag_name(spl_type_t type) {
switch (type) { switch (type) {
case SPL_VOID: case SPL_VOID: return "void";
return "void"; case SPL_BOOL: return "bool";
case SPL_I8: case SPL_I8: return "i8";
return "i8"; case SPL_U8: return "u8";
case SPL_U8:
return "u8";
case SPL_I16: case SPL_I16:
return "i16"; return "i16";
case SPL_U16: case SPL_U16:

View File

@@ -1,8 +1,8 @@
/* spl_ir.h - SPL Intermediate Representation: instruction set and binary format /* spl_mcode.h - SPL VM Machine Code: instruction set and binary format
*/ */
#ifndef __SPL_IR_H__ #ifndef __SPL_MCODE_H__
#define __SPL_IR_H__ #define __SPL_MCODE_H__
#include "include/core_map.h" #include "include/core_map.h"
#include "include/core_vec.h" #include "include/core_vec.h"
@@ -14,6 +14,7 @@ typedef intptr_t isize;
typedef enum { typedef enum {
SPL_VOID, SPL_VOID,
SPL_BOOL,
SPL_I8, SPL_I8,
SPL_U8, SPL_U8,
SPL_I16, SPL_I16,
@@ -220,4 +221,4 @@ const char *spl_type_tag_name(spl_type_t type);
void spl_ins_dump(spl_ins_t *ins, spl_val_t addr); void spl_ins_dump(spl_ins_t *ins, spl_val_t addr);
#endif /* __SPL_IR_H__ */ #endif /* __SPL_MCODE_H__ */

View File

@@ -10,7 +10,7 @@
#include "spl_syscall.h" #include "spl_syscall.h"
#include "include/core_map.h" #include "include/core_map.h"
#include "include/core_vec.h" #include "include/core_vec.h"
#include "spl_ir.h" #include "spl_mcode.h"
#include "spl_vm.h" #include "spl_vm.h"
#include <stdio.h> #include <stdio.h>
@@ -128,7 +128,7 @@ static spl_val_t vm_read_file(int nargs, spl_val_t *args) {
const char *path = (const char *)(uintptr_t)args[0]; const char *path = (const char *)(uintptr_t)args[0];
if (path == nullptr) { if (path == nullptr) {
fprintf(stderr, "filepath can't be null"); fprintf(stderr, "filepath can't be null");
return 1; return 0;
} }
FILE *f = fopen(path, "rb"); FILE *f = fopen(path, "rb");
if (!f) { if (!f) {

View File

@@ -12,7 +12,7 @@
#ifndef __SPL_SYSCALL_H__ #ifndef __SPL_SYSCALL_H__
#define __SPL_SYSCALL_H__ #define __SPL_SYSCALL_H__
#include "spl_ir.h" #include "spl_mcode.h"
/* Register all known built-in syscalls into prog->natives[]. /* Register all known built-in syscalls into prog->natives[].
* Entries whose name matches a known syscall get their impl_fn set; * Entries whose name matches a known syscall get their impl_fn set;

View File

@@ -5,7 +5,7 @@
*/ */
#include "spl_vm.h" #include "spl_vm.h"
#include "spl_ir.h" #include "spl_mcode.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@@ -45,6 +45,7 @@ static int spl_type_size(spl_type_t t) {
switch (t) { switch (t) {
case SPL_VOID: case SPL_VOID:
return 0; return 0;
case SPL_BOOL:
case SPL_I8: case SPL_I8:
case SPL_U8: case SPL_U8:
return 1; return 1;
@@ -80,6 +81,16 @@ static int spl_type_size(spl_type_t t) {
return -1; \ return -1; \
} while (0) } while (0)
#define CHECK_ADDR(addr, label) do { \
if (vm->debug_addr && (uintptr_t)(addr) < 0x1000) { \
fprintf(stderr, "vm: %s at ip=%zd: LOW ADDR=%p sp=%zd fp=%zd\n", \
label, vm->ip - 1, (void*)(uintptr_t)(addr), vm->sp, vm->fp); \
spl_vm_stackdump(vm, vm->sp); \
spl_vm_backtrace(vm, vm->fp); \
vm->exit_code = 1; return -1; \
} \
} while(0)
/* ================================================================ /* ================================================================
* Stack push/pop (stacks.data is pre-allocated in init) * Stack push/pop (stacks.data is pre-allocated in init)
* ================================================================ */ * ================================================================ */
@@ -547,6 +558,7 @@ void spl_vm_init_ex(spl_vm_t *vm, int stack_size, int call_depth) {
vm->prog = NULL; vm->prog = NULL;
vm->trace = 0; vm->trace = 0;
vm->debug = 1; vm->debug = 1;
vm->debug_addr = 0;
vm->exit_code = 0; vm->exit_code = 0;
} }
@@ -584,6 +596,7 @@ void spl_vm_set_debug(spl_vm_t *vm, int enabled) {
if (!vm) if (!vm)
return; return;
vm->debug = enabled ? 1 : 0; vm->debug = enabled ? 1 : 0;
vm->debug_addr = enabled ? 1 : 0;
} }
#define STACK_CANARY(vm) (vm)->stacks.data[(vm)->fp - 1] #define STACK_CANARY(vm) (vm)->stacks.data[(vm)->fp - 1]
@@ -922,6 +935,7 @@ int spl_vm_run_once(spl_vm_t *vm) {
/* ========== Indirect Memory (load/store with types) ========== */ /* ========== Indirect Memory (load/store with types) ========== */
case SPL_LOAD: { case SPL_LOAD: {
void *_addr = (void *)POP(); void *_addr = (void *)POP();
CHECK_ADDR(_addr, "LOAD");
spl_val_t _v = 0; spl_val_t _v = 0;
usize _sz = spl_type_size(ins->type); usize _sz = spl_type_size(ins->type);
memcpy(&_v, _addr, _sz); memcpy(&_v, _addr, _sz);
@@ -936,6 +950,7 @@ int spl_vm_run_once(spl_vm_t *vm) {
case SPL_STORE: { case SPL_STORE: {
spl_val_t _v = POP(); spl_val_t _v = POP();
void *_addr = (void *)POP(); void *_addr = (void *)POP();
CHECK_ADDR(_addr, "STORE");
memcpy(_addr, &_v, spl_type_size(ins->type)); memcpy(_addr, &_v, spl_type_size(ins->type));
break; break;
} }

View File

@@ -4,7 +4,7 @@
#define __SPL_VM_H__ #define __SPL_VM_H__
#include "include/core_vec.h" #include "include/core_vec.h"
#include "spl_ir.h" #include "spl_mcode.h"
#include <stdint.h> #include <stdint.h>
#define SPL_STACK_CANARY ((spl_val_t)0xDEADBEEFCAFEBABEull) #define SPL_STACK_CANARY ((spl_val_t)0xDEADBEEFCAFEBABEull)
@@ -30,6 +30,7 @@ typedef struct {
int exit_code; int exit_code;
int trace; /* non-zero to print each instruction */ int trace; /* non-zero to print each instruction */
int debug; /* non-zero to enable canary checks */ int debug; /* non-zero to enable canary checks */
int debug_addr; /* non-zero to check for low-address memory access */
spl_prog_t *prog; spl_prog_t *prog;
char error_msg[1024]; char error_msg[1024];
struct { struct {

View File

@@ -1,452 +0,0 @@
# 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 { ... }
```
实现:在循环顶部求值 `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_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 中尚未实现。)