stage2 迁移ast解析器 stage1 防止错误死循环

This commit is contained in:
zzy committed 2026-09-18 22:32:57 +08:00
1 parent bb9d0a9f18
commit b5a7a811b3
7 files changed
+3000 -71

No files matched your search

+5 -4
View File
@@ -401,8 +401,8 @@ static bool test_once_diff(const char *fname, void *user_data) {
} }
Nob_Cmd s_cmd = {0}; Nob_Cmd s_cmd = {0};
nob_cmd_append(&s_cmd, exe_path("spl_cli"), BUILD_FOLDER "splc1.sir", "--dump", nob_cmd_append(&s_cmd, exe_path("spl_cli"), BUILD_FOLDER "splc1.sir", "--dump", (char *)mode,
(char *)mode, (char *)fname); (char *)fname);
Nob_Cmd_Result s_res = {0}; Nob_Cmd_Result s_res = {0};
if (!run_capture(&s_cmd, &s_res)) { if (!run_capture(&s_cmd, &s_res)) {
nob_cmd_result_free(c_res); nob_cmd_result_free(c_res);
@@ -414,8 +414,8 @@ static bool test_once_diff(const char *fname, void *user_data) {
Nob_String_View s_out = nob_sb_to_sv(s_res.out); Nob_String_View s_out = nob_sb_to_sv(s_res.out);
bool same = nob_sv_eq(c_out, s_out); bool same = nob_sv_eq(c_out, s_out);
if (!same) { if (!same) {
nob_log(NOB_ERROR, "diff mismatch: %s (c=%zu bytes, spl=%zu bytes)", fname, nob_log(NOB_ERROR, "diff mismatch: %s (c=%zu bytes, spl=%zu bytes)", fname, c_out.count,
c_out.count, s_out.count); s_out.count);
// 打印首个不相同行(按行比较)。 // 打印首个不相同行(按行比较)。
Nob_String_View ca = c_out; Nob_String_View ca = c_out;
Nob_String_View sa = s_out; Nob_String_View sa = s_out;
@@ -618,6 +618,7 @@ int main(int argc, char **argv) {
return run_test(stage, &files, tfn, (void *)mode_name) ? 0 : 1; return run_test(stage, &files, tfn, (void *)mode_name) ? 0 : 1;
} }
if (strcmp(command_name, "clean") == 0) { if (strcmp(command_name, "clean") == 0) {
if (nob_file_exists(BUILD_FOLDER)) { if (nob_file_exists(BUILD_FOLDER)) {
if (!delete_directory_recursively(BUILD_FOLDER)) if (!delete_directory_recursively(BUILD_FOLDER))
+1
View File
@@ -849,6 +849,7 @@ static spl_ast_node_ref_t parse_container_declaration(spl_parser_t *p) {
} }
default: default:
PARSE_ERROR(p, SPL_DIAG_KIND_PARSE_UNRECOGNIZED, t, "unrecognized declaration"); PARSE_ERROR(p, SPL_DIAG_KIND_PARSE_UNRECOGNIZED, t, "unrecognized declaration");
advance(p); /* 消费该 token,避免调用方在错误恢复时原地死循环 */
vec_free(tag_list); vec_free(tag_list);
return 0; return 0;
} }
+642
View File
@@ -0,0 +1,642 @@
// Expect retcode = 0
// AST:用 ADT(enum 带载荷)表达节点,节点引用是 arena 序号 usize。
// stage1 的 C 结构只是「手写 union 模拟 ADT」;这里直接用 SPL 的 enum 载荷,
// 不照抄 union 布局,只要求最终 dump / 语义与 stage1 一致。
// 索引 0 为 NONE 哨兵(与 stage1 一致)。
type .{ .Error } = @import("utils/error.spl");
type .{ .Vec } = @import("utils/vec.spl");
type .{ .ByteView } = @import("utils/byte_view.spl");
type .{ .ByteBuffer } = @import("utils/byte_buffer.spl");
type .{ .append_usize } = @import("utils/fmt.spl");
type .{ .Punct } = @import("token.spl");
type BigInt = @import("utils/mp.spl");
type .{ .Span, .span_none, .span_make, .span_is_none } = @import("./diag.spl");
type .{ .Sources, .Source, .set_at } = @import("utils/source.spl");
// 算子不另立 enum:AST 直接存词法 Punct。一元/二元由节点 kind 区分
// (一元:Sub=负 / Not=! / BitNot=~ / And=& / Mul=.*;二元即对应 Punct)。
// ---- 节点体(ADT;variant 名 = stage1 的 kind 名;载荷用内联匿名 struct)----
type Body = enum {
None,
Root: struct { members: usize },
ContainerMembers: struct { external_tags: Vec, declarations: Vec },
ExternalTagItem: struct { is_at: bool, is_external: bool, name: ByteView, args: Vec },
TagItem: struct { is_at: bool, is_external: bool, name: ByteView, args: Vec },
ComptimeDecl: struct { body: usize },
TopLevelBlock: struct { tag_list: Vec, block: usize },
FnDecl: struct {
tag_list: Vec,
is_comptime: bool,
name: ByteView,
params: Vec,
ret_type: usize,
body: usize,
},
VarDecl: struct { attr_list: Vec, pattern: usize, type_expr: usize, init_expr: usize },
ConstDecl: struct { attr_list: Vec, pattern: usize, type_expr: usize, init_expr: usize },
TypeDecl: struct { attr_list: Vec, pattern: usize, type_expr: usize },
MemberDecl: struct {
attr_list: Vec,
name: ByteView,
type_expr: usize,
init_expr: usize,
},
ParamDecl: struct { attr_list: Vec, name: ByteView, type_expr: usize },
StatementRet: struct { expr: usize },
StatementBreak: struct { label: usize, expr: usize },
StatementContinue: struct { label: usize },
StatementDefer: struct { body: usize },
StatementComptime: struct { body: usize },
StatementDecl: struct { decl: usize },
StatementExpr: struct { expr: usize },
AggregateMember: struct {
attr_list: Vec,
name: ByteView,
type_expr: usize,
init_expr: usize,
},
StructInitItem: struct { name: ByteView, expr: usize },
MatchArm: struct { pattern: usize, body: usize },
Label: struct { name: ByteView },
ExprIntLit: struct { value: BigInt }, // 任意宽度(mp),解析后由 Ast 拥有
ExprFloatLit: struct { text: ByteView },
ExprCharLit: struct { value: u8 },
ExprStringLit: struct { data: ByteView },
ExprBoolLit: struct { value: bool },
ExprNullLit,
ExprUndefinedLit,
ExprIdent: struct { name: ByteView },
ExprUnary: struct { op: Punct, operand: usize },
ExprBinary: struct { op: Punct, left: usize, right: usize },
ExprCall: struct { callee: usize, args: Vec },
ExprField: struct { obj: usize, name: ByteView },
ExprIndex: struct { obj: usize, index: usize },
ExprSlice: struct { obj: usize, begin: usize, end: usize },
ExprCast: struct { expr: usize, type_expr: usize },
ExprTuple: struct { items: Vec },
ExprArrayLit: struct {
has_len: bool,
len: usize,
is_underscore_len: bool,
elem_type: usize,
elems: Vec,
},
ExprAggregateLit: struct { name: ByteView, fields: Vec },
ExprComptimeCall: struct { name: ByteView, args: Vec },
ExprBlock: struct { stmts: Vec, tail_expr: usize },
ExprIf: struct { cond: usize, then_block: usize, else_branch: usize },
ExprIfvar: struct { var_decl: usize, then_block: usize, else_branch: usize },
ExprWhile: struct { label: usize, cond: usize, body: usize, else_branch: usize },
ExprLoop: struct { label: usize, body: usize },
ExprFor: struct {
label: usize,
iter_exprs: Vec,
pattern: usize,
body: usize,
else_branch: usize,
},
ExprMatch: struct { matched_expr: usize, arms: Vec },
ExprFnType: struct { is_comptime: bool, params: Vec, ret_type: usize },
TypePath: struct { name: ByteView, generic_args: Vec },
TypePointer: struct { pointee: usize },
TypeArray: struct {
has_len: bool,
len: isize,
is_underscore_len: bool,
elem_type: usize,
},
TypeSlice: struct { elem_type: usize },
TypeFn: struct { is_comptime: bool, params: Vec, ret_type: usize },
TypeParen: struct { types: Vec },
TypeAggregate: struct { kind_name: ByteView, members: Vec },
TypeComptimeCall: struct { name: ByteView, args: Vec },
PatternIdent: struct { name: ByteView },
PatternUnderscore,
PatternDot: struct { name: ByteView, sub_pattern: usize },
PatternTuple: struct { items: Vec },
PatternList: struct { items: Vec },
PatternAggregate: struct { items: Vec },
PatternField: struct { name: ByteView, pattern: usize },
}
fn kind_name(body: Body): []u8 {
match body {
.None => { ret "none"; },
.Root(_) => { ret "root"; },
.ContainerMembers(_) => { ret "container_members"; },
.ExternalTagItem(_) => { ret "external_tag_item"; },
.TagItem(_) => { ret "tag_item"; },
.ComptimeDecl(_) => { ret "comptime_decl"; },
.TopLevelBlock(_) => { ret "top_level_block"; },
.FnDecl(_) => { ret "fn_decl"; },
.VarDecl(_) => { ret "var_decl"; },
.ConstDecl(_) => { ret "const_decl"; },
.TypeDecl(_) => { ret "type_decl"; },
.MemberDecl(_) => { ret "member_decl"; },
.ParamDecl(_) => { ret "param_decl"; },
.StatementRet(_) => { ret "statement_ret"; },
.StatementBreak(_) => { ret "statement_break"; },
.StatementContinue(_) => { ret "statement_continue"; },
.StatementDefer(_) => { ret "statement_defer"; },
.StatementComptime(_) => { ret "statement_comptime"; },
.StatementDecl(_) => { ret "statement_decl"; },
.StatementExpr(_) => { ret "statement_expr"; },
.AggregateMember(_) => { ret "aggregate_member"; },
.StructInitItem(_) => { ret "struct_init_item"; },
.MatchArm(_) => { ret "match_arm"; },
.Label(_) => { ret "label"; },
.ExprIntLit(_) => { ret "int_lit"; },
.ExprFloatLit(_) => { ret "float_lit"; },
.ExprCharLit(_) => { ret "char_lit"; },
.ExprStringLit(_) => { ret "string_lit"; },
.ExprBoolLit(_) => { ret "bool_lit"; },
.ExprNullLit => { ret "null_lit"; },
.ExprUndefinedLit => { ret "undefined_lit"; },
.ExprIdent(_) => { ret "ident"; },
.ExprUnary(_) => { ret "unary"; },
.ExprBinary(_) => { ret "binary"; },
.ExprCall(_) => { ret "call"; },
.ExprField(_) => { ret "field"; },
.ExprIndex(_) => { ret "index"; },
.ExprSlice(_) => { ret "slice"; },
.ExprCast(_) => { ret "cast"; },
.ExprTuple(_) => { ret "tuple"; },
.ExprArrayLit(_) => { ret "array_literal"; },
.ExprAggregateLit(_) => { ret "aggregate_literal"; },
.ExprComptimeCall(_) => { ret "comptime_call"; },
.ExprBlock(_) => { ret "block"; },
.ExprIf(_) => { ret "if_expr"; },
.ExprIfvar(_) => { ret "ifvar_expr"; },
.ExprWhile(_) => { ret "while_expr"; },
.ExprLoop(_) => { ret "loop_expr"; },
.ExprFor(_) => { ret "for_expr"; },
.ExprMatch(_) => { ret "match_expr"; },
.ExprFnType(_) => { ret "fn_type"; },
.TypePath(_) => { ret "type_path"; },
.TypePointer(_) => { ret "type_pointer"; },
.TypeArray(_) => { ret "type_array"; },
.TypeSlice(_) => { ret "type_slice"; },
.TypeFn(_) => { ret "type_fn"; },
.TypeParen(_) => { ret "type_paren"; },
.TypeAggregate(_) => { ret "type_aggregate"; },
.TypeComptimeCall(_) => { ret "type_comptime_call"; },
.PatternIdent(_) => { ret "pattern_ident"; },
.PatternUnderscore => { ret "pattern_underscore"; },
.PatternDot(_) => { ret "pattern_dot"; },
.PatternTuple(_) => { ret "pattern_tuple"; },
.PatternList(_) => { ret "pattern_list"; },
.PatternAggregate(_) => { ret "pattern_aggregate"; },
.PatternField(_) => { ret "pattern_field"; },
}
ret "?";
}
type Node = struct {
span: Span,
resolved_def_id: usize,
resolved_lang_tid: usize,
expected_lang_tid: usize,
attrs: Vec,
body: Body,
}
type Ast = @self;
nodes: Vec, // Node
root: usize,
sources: *Sources, // 借用;可为 null(dump 时不解析行列)
type AstResult = enum {
Ok: Ast,
Err: Error,
}
fn new_node(body: Body, span: Span): Node {
ret Node {
.span = span,
.resolved_def_id = 0,
.resolved_lang_tid = 0,
.expected_lang_tid = 0,
.attrs = Vec.blank(@sizeof(usize), @alignof(usize)),
.body = body,
};
}
fn new(sources: *Sources): AstResult {
var ast = Ast {
.nodes = Vec.blank(@sizeof(Node), @alignof(Node)),
.root = 0,
.sources = sources,
};
var sentinel = new_node(.None, span_none());
match Vec.push(&ast.nodes, &sentinel as *u8) {
.Ok => {},
.Err(error) => {
Vec.drop(&ast.nodes);
ret .Err(error);
},
}
ret .Ok(ast);
}
fn drop(self: *Ast) {
var i: usize = 0;
while i < self.nodes.len {
var n = node(self, i);
if n != null {
Vec.drop(&n.attrs);
match n.body {
.ExprIntLit(x) => { BigInt.drop(&x.value); },
_ => {},
}
}
i = i + 1;
}
Vec.drop(&self.nodes);
self.root = 0;
}
fn node(self: *Ast, ref: usize): *Node {
match Vec.get(&self.nodes, ref) {
.Ok(ptr) => { ret ptr as *Node; },
.Err(error) => { ret null; },
}
ret null;
}
// 追加节点,返回其引用(失败返回 0)。
fn alloc(self: *Ast, body: Body, span: Span): usize {
var n = new_node(body, span);
match Vec.push(&self.nodes, &n as *u8) {
.Ok => {},
.Err(error) => {
Vec.drop(&n.attrs);
ret 0;
},
}
ret self.nodes.len - 1;
}
fn count(self: *Ast): usize {
ret self.nodes.len;
}
fn ref_vec(): Vec {
ret Vec.blank(@sizeof(usize), @alignof(usize));
}
// ---- dump(镜像 stage1 spl_ast_dump)----
//
// AST:
// <indent>`-/|-kind #ref [line:col]
// indent:栈上每层 last 为真输出 " ",否则 "| "。
fn push(buf: *ByteBuffer, bytes: []u8): bool {
match ByteBuffer.append(buf, bytes) {
.Ok => { ret true; },
.Err(error) => { ret false; },
}
ret false;
}
fn push_byte(buf: *ByteBuffer, byte: u8): bool {
match ByteBuffer.append_byte(buf, byte) {
.Ok => { ret true; },
.Err(error) => { ret false; },
}
ret false;
}
fn push_usize(buf: *ByteBuffer, value: usize): bool {
match append_usize(buf, value, 10, false) {
.Ok => { ret true; },
.Err(error) => { ret false; },
}
ret false;
}
fn ref_at(list: Vec, index: usize): usize {
match Vec.get(&list, index) {
.Ok(ptr) => { ret (ptr as *usize).*; },
.Err(error) => { ret 0; },
}
ret 0;
}
fn dump_opt(ast: *Ast, buf: *ByteBuffer, stack: *Vec, ref: usize, last: bool): bool {
if ref == 0 { ret true; }
ret dump_node(ast, buf, stack, ref, last);
}
fn dump_list(ast: *Ast, buf: *ByteBuffer, stack: *Vec, list: Vec): bool {
var i: usize = 0;
while i < list.len {
var last = (i + 1 == list.len);
if !dump_node(ast, buf, stack, ref_at(list, i), last) { ret false; }
i = i + 1;
}
ret true;
}
fn dump_node(ast: *Ast, buf: *ByteBuffer, stack: *Vec, ref: usize, last: bool): bool {
var n = node(ast, ref);
if n == null { ret false; }
var i: usize = 0;
while i < stack.len {
var flag: u8 = 0;
match Vec.get(stack, i) {
.Ok(ptr) => { flag = ptr.*; },
.Err(error) => { ret false; },
}
if !push(buf, if flag != 0 { " " } else { "| " }) { ret false; }
i = i + 1;
}
if !push(buf, if last { "`-" } else { "|-" }) { ret false; }
if !push(buf, kind_name(n.body)) { ret false; }
if !push(buf, " #") { ret false; }
if !push_usize(buf, ref) { ret false; }
if !span_is_none(n.span) && ast.sources != null {
var src = set_at(ast.sources, n.span.file);
if src != null {
var pos = Source.line_col(src.*, n.span.start);
if !push(buf, " [") { ret false; }
if !push_usize(buf, pos.line) { ret false; }
if !push_byte(buf, ':') { ret false; }
if !push_usize(buf, pos.col) { ret false; }
if !push_byte(buf, ']') { ret false; }
}
}
if !push_byte(buf, '\n') { ret false; }
var l: u8 = 0;
if last { l = 1; }
match Vec.push(stack, &l as *u8) {
.Ok => {},
.Err(error) => { ret false; },
}
var ok = true;
match n.body {
.None => {},
.Root(r) => { ok = dump_opt(ast, buf, stack, r.members, true); },
.ContainerMembers(c) => {
ok = dump_list(ast, buf, stack, c.external_tags) &&
dump_list(ast, buf, stack, c.declarations);
},
.ExternalTagItem(t) => { ok = dump_list(ast, buf, stack, t.args); },
.TagItem(t) => { ok = dump_list(ast, buf, stack, t.args); },
.ComptimeDecl(x) => { ok = dump_opt(ast, buf, stack, x.body, true); },
.TopLevelBlock(t) => {
ok = dump_list(ast, buf, stack, t.tag_list) &&
dump_opt(ast, buf, stack, t.block, true);
},
.FnDecl(f) => {
ok = dump_list(ast, buf, stack, f.tag_list) &&
dump_list(ast, buf, stack, f.params) &&
dump_opt(ast, buf, stack, f.ret_type, false) &&
dump_opt(ast, buf, stack, f.body, true);
},
.VarDecl(d) => {
ok = dump_list(ast, buf, stack, d.attr_list) &&
dump_opt(ast, buf, stack, d.pattern, false) &&
dump_opt(ast, buf, stack, d.type_expr, false) &&
dump_opt(ast, buf, stack, d.init_expr, true);
},
.ConstDecl(d) => {
ok = dump_list(ast, buf, stack, d.attr_list) &&
dump_opt(ast, buf, stack, d.pattern, false) &&
dump_opt(ast, buf, stack, d.type_expr, false) &&
dump_opt(ast, buf, stack, d.init_expr, true);
},
.TypeDecl(d) => {
ok = dump_list(ast, buf, stack, d.attr_list) &&
dump_opt(ast, buf, stack, d.pattern, false) &&
dump_opt(ast, buf, stack, d.type_expr, true);
},
.MemberDecl(d) => {
ok = dump_list(ast, buf, stack, d.attr_list) &&
dump_opt(ast, buf, stack, d.type_expr, false) &&
dump_opt(ast, buf, stack, d.init_expr, true);
},
.ParamDecl(p) => {
ok = dump_list(ast, buf, stack, p.attr_list) &&
dump_opt(ast, buf, stack, p.type_expr, true);
},
.StatementRet(e) => { ok = dump_opt(ast, buf, stack, e.expr, true); },
.StatementBreak(b) => {
ok = dump_opt(ast, buf, stack, b.label, false) &&
dump_opt(ast, buf, stack, b.expr, true);
},
.StatementContinue(x) => { ok = dump_opt(ast, buf, stack, x.label, true); },
.StatementDefer(x) => { ok = dump_opt(ast, buf, stack, x.body, true); },
.StatementComptime(x) => { ok = dump_opt(ast, buf, stack, x.body, true); },
.StatementDecl(d) => { ok = dump_opt(ast, buf, stack, d.decl, true); },
.StatementExpr(e) => { ok = dump_opt(ast, buf, stack, e.expr, true); },
.AggregateMember(d) => {
ok = dump_list(ast, buf, stack, d.attr_list) &&
dump_opt(ast, buf, stack, d.type_expr, false) &&
dump_opt(ast, buf, stack, d.init_expr, true);
},
.StructInitItem(s) => { ok = dump_opt(ast, buf, stack, s.expr, true); },
.MatchArm(m) => {
ok = dump_opt(ast, buf, stack, m.pattern, false) &&
dump_opt(ast, buf, stack, m.body, true);
},
.Label(x) => {},
.ExprIntLit(x) => {},
.ExprFloatLit(x) => {},
.ExprCharLit(x) => {},
.ExprStringLit(x) => {},
.ExprBoolLit(x) => {},
.ExprNullLit => {},
.ExprUndefinedLit => {},
.ExprIdent(x) => {},
.ExprUnary(u) => { ok = dump_opt(ast, buf, stack, u.operand, true); },
.ExprBinary(b) => {
ok = dump_opt(ast, buf, stack, b.left, false) &&
dump_opt(ast, buf, stack, b.right, true);
},
.ExprCall(c) => {
ok = dump_opt(ast, buf, stack, c.callee, false) &&
dump_list(ast, buf, stack, c.args);
},
.ExprField(f) => { ok = dump_opt(ast, buf, stack, f.obj, true); },
.ExprIndex(x) => {
ok = dump_opt(ast, buf, stack, x.obj, false) &&
dump_opt(ast, buf, stack, x.index, true);
},
.ExprSlice(s) => {
ok = dump_opt(ast, buf, stack, s.obj, false) &&
dump_opt(ast, buf, stack, s.begin, false) &&
dump_opt(ast, buf, stack, s.end, true);
},
.ExprCast(c) => {
ok = dump_opt(ast, buf, stack, c.expr, false) &&
dump_opt(ast, buf, stack, c.type_expr, true);
},
.ExprTuple(v) => { ok = dump_list(ast, buf, stack, v.items); },
.ExprArrayLit(a) => {
ok = dump_opt(ast, buf, stack, a.elem_type, false) &&
dump_list(ast, buf, stack, a.elems);
},
.ExprAggregateLit(a) => { ok = dump_list(ast, buf, stack, a.fields); },
.ExprComptimeCall(c) => { ok = dump_list(ast, buf, stack, c.args); },
.ExprBlock(b) => {
ok = dump_list(ast, buf, stack, b.stmts) &&
dump_opt(ast, buf, stack, b.tail_expr, true);
},
.ExprIf(x) => {
ok = dump_opt(ast, buf, stack, x.cond, false) &&
dump_opt(ast, buf, stack, x.then_block, false) &&
dump_opt(ast, buf, stack, x.else_branch, true);
},
.ExprIfvar(x) => {
ok = dump_opt(ast, buf, stack, x.var_decl, false) &&
dump_opt(ast, buf, stack, x.then_block, false) &&
dump_opt(ast, buf, stack, x.else_branch, true);
},
.ExprWhile(w) => {
ok = dump_opt(ast, buf, stack, w.label, false) &&
dump_opt(ast, buf, stack, w.cond, false) &&
dump_opt(ast, buf, stack, w.body, false) &&
dump_opt(ast, buf, stack, w.else_branch, true);
},
.ExprLoop(x) => {
ok = dump_opt(ast, buf, stack, x.label, false) &&
dump_opt(ast, buf, stack, x.body, true);
},
.ExprFor(f) => {
ok = dump_opt(ast, buf, stack, f.label, false) &&
dump_list(ast, buf, stack, f.iter_exprs) &&
dump_opt(ast, buf, stack, f.pattern, false) &&
dump_opt(ast, buf, stack, f.body, false) &&
dump_opt(ast, buf, stack, f.else_branch, true);
},
.ExprMatch(m) => {
ok = dump_opt(ast, buf, stack, m.matched_expr, false) &&
dump_list(ast, buf, stack, m.arms);
},
.ExprFnType(f) => {
ok = dump_list(ast, buf, stack, f.params) &&
dump_opt(ast, buf, stack, f.ret_type, true);
},
.TypePath(p) => {},
.TypePointer(p) => { ok = dump_opt(ast, buf, stack, p.pointee, true); },
.TypeArray(a) => { ok = dump_opt(ast, buf, stack, a.elem_type, true); },
.TypeSlice(s) => { ok = dump_opt(ast, buf, stack, s.elem_type, true); },
.TypeFn(f) => {
ok = dump_list(ast, buf, stack, f.params) &&
dump_opt(ast, buf, stack, f.ret_type, true);
},
.TypeParen(p) => { ok = dump_list(ast, buf, stack, p.types); },
.TypeAggregate(a) => { ok = dump_list(ast, buf, stack, a.members); },
.TypeComptimeCall(c) => { ok = dump_list(ast, buf, stack, c.args); },
.PatternIdent(x) => {},
.PatternUnderscore => {},
.PatternDot(p) => { ok = dump_opt(ast, buf, stack, p.sub_pattern, true); },
.PatternTuple(v) => { ok = dump_list(ast, buf, stack, v.items); },
.PatternList(v) => { ok = dump_list(ast, buf, stack, v.items); },
.PatternAggregate(v) => { ok = dump_list(ast, buf, stack, v.items); },
.PatternField(p) => { ok = dump_opt(ast, buf, stack, p.pattern, true); },
}
stack.len = stack.len - 1;
ret ok;
}
// 把整棵树 dump 到 out(追加,不 clear)。
fn dump(ast: *Ast, out: *ByteBuffer): bool {
if !push(out, "AST:\n") { ret false; }
var stack = Vec.blank(1, 1);
defer { Vec.drop(&stack); }
ret dump_node(ast, out, &stack, ast.root, true);
}
// ---- 测试 ----
fn test_kind_names() {
@assert(ByteView.eq(ByteView.from_slice(kind_name(.None)), ByteView.from_slice("none")),
"none name");
@assert(ByteView.eq(ByteView.from_slice(kind_name(.ExprBinary(.{
.op = .Add,
.left = 0,
.right = 0,
}))),
ByteView.from_slice("binary")),
"binary name");
}
fn test_alloc_and_payload() {
var ast: Ast = undefined;
match new(null) {
.Ok(v) => { ast = v; },
.Err(error) => { @assert(false, "ast new failed"); },
}
defer { drop(&ast); }
@assert(count(&ast) == 1, "sentinel only");
var r = alloc(&ast, .ExprIdent(.{ .name = ByteView.from_slice("main") }), span_make(0, 5, 9));
@assert(r == 1, "first alloc is 1");
var n = node(&ast, r);
n.resolved_def_id = 99;
@assert(n.span.file == 0 && n.span.start == 5 && n.span.end == 9, "span stored");
@assert(n.resolved_def_id == 99, "header field");
match n.body {
.ExprIdent(i) => {
@assert(ByteView.eq(i.name, ByteView.from_slice("main")), "payload name");
},
_ => { @assert(false, "expected ident"); },
}
}
fn expect_bytes(buf: *ByteBuffer, expected: []u8) {
var actual = ByteBuffer.as_slice(buf.*);
@assert(actual.len == expected.len, "dump length");
var i: usize = 0;
while i < expected.len {
@assert(actual.ptr[i] == expected.ptr[i], "dump byte");
i = i + 1;
}
}
fn test_dump() {
var ast: Ast = undefined;
match new(null) {
.Ok(v) => { ast = v; },
.Err(error) => { @assert(false, "ast new failed"); },
}
defer { drop(&ast); }
var id_ref = alloc(&ast, .ExprIdent(.{ .name = ByteView.from_slice("main") }), span_none()); // #1
var decls = ref_vec();
match Vec.push(&decls, &id_ref as *u8) {
.Ok => {},
.Err(error) => { @assert(false, "push decl"); },
}
var cm_ref = alloc(&ast, .ContainerMembers(.{
.external_tags = ref_vec(),
.declarations = decls,
}), span_none()); // #2
var root_ref = alloc(&ast, .Root(.{ .members = cm_ref }), span_none()); // #3
ast.root = root_ref;
var buf = ByteBuffer.new();
defer { ByteBuffer.drop(&buf); }
@assert(dump(&ast, &buf), "dump ok");
// sources=null → 不解析行列;无 span 时也不打印。
expect_bytes(&buf, "AST:\n`-root #3\n `-container_members #2\n `-ident #1\n");
}
fn main() {
test_kind_names();
test_alloc_and_payload();
test_dump();
}
+65 -39
View File
@@ -9,7 +9,7 @@ type .{ .Vec, .VecResult } = @import("utils/vec.spl");
type .{ .ByteView } = @import("utils/byte_view.spl"); type .{ .ByteView } = @import("utils/byte_view.spl");
type .{ .is_digit, .is_alpha, .is_alnum, .is_hex_digit, .digit_value } = @import("utils/ascii.spl"); type .{ .is_digit, .is_alpha, .is_alnum, .is_hex_digit, .digit_value } = @import("utils/ascii.spl");
type .{ type .{
.Token, .TokenKind, .Radix, .Keyword, .Punct, .int_value, .keyword_of .Token, .TokenKind, .Radix, .Keyword, .Punct, .keyword_of
} = @import("token.spl"); } = @import("token.spl");
type .{ .Span, .span_make } = @import("./diag.spl"); type .{ .Span, .span_make } = @import("./diag.spl");
type Diag = @import("./diag.spl"); type Diag = @import("./diag.spl");
@@ -45,23 +45,6 @@ fn hex_val(c: u8): u8 {
ret 0; ret 0;
} }
// 跳过下划线,按 radix 折叠;非法字符忽略(前缀 x/b/o 自然被忽略)。
fn decode_digits(text: ByteView, radix: u8): u64 {
var acc: u64 = 0;
var i: usize = 0;
while i < text.len {
var b = text.ptr[i];
if b != '_' {
match digit_value(b, radix) {
.Ok(v) => { acc = acc * (radix as u64) + (v as u64); },
.Err(error) => {},
}
}
i = i + 1;
}
ret acc;
}
// 文本形如 'a' / '\n' / '\x41';返回解码后的字节。 // 文本形如 'a' / '\n' / '\x41';返回解码后的字节。
fn decode_char(text: ByteView): u8 { fn decode_char(text: ByteView): u8 {
var start: usize = 1; var start: usize = 1;
@@ -119,7 +102,7 @@ fn sub(self: *Lexer, start: usize, end: usize): ByteView {
// 产出 token 并把 pos/col 推进到 end。Vec.push 失败视为致命(同 vec.make_vec 风格)。 // 产出 token 并把 pos/col 推进到 end。Vec.push 失败视为致命(同 vec.make_vec 风格)。
fn emit_to(self: *Lexer, out: *Vec, kind: TokenKind, start: usize, end: usize, line: usize, fn emit_to(self: *Lexer, out: *Vec, kind: TokenKind, start: usize, end: usize, line: usize,
col: usize) { col: usize) {
var t = Token.make(kind, sub(self, start, end), line, col); var t = Token.make(kind, sub(self, start, end), line, col, start);
match Vec.push(out, &t as *u8) { match Vec.push(out, &t as *u8) {
.Ok => {}, .Ok => {},
.Err(error) => { @assert(false, "lexer: Vec.push failed"); }, .Err(error) => { @assert(false, "lexer: Vec.push failed"); },
@@ -151,8 +134,7 @@ fn scan_int(self: *Lexer, out: *Vec, start: usize, line: usize, col: usize) {
while p < self.src.len && (is_hex_digit(self.src.ptr[p]) || self.src.ptr[p] == '_') { while p < self.src.len && (is_hex_digit(self.src.ptr[p]) || self.src.ptr[p] == '_') {
p = p + 1; p = p + 1;
} }
emit_to(self, out, .Int(int_value(decode_digits(sub(self, start, p), 16), .Hex)), start, emit_to(self, out, .Int(.Hex), start, p, line, col);
p, line, col);
ret; ret;
} }
if nc == 'b' || nc == 'B' { if nc == 'b' || nc == 'B' {
@@ -161,8 +143,7 @@ fn scan_int(self: *Lexer, out: *Vec, start: usize, line: usize, col: usize) {
self.src.ptr[p] == '_') { self.src.ptr[p] == '_') {
p = p + 1; p = p + 1;
} }
emit_to(self, out, .Int(int_value(decode_digits(sub(self, start, p), 2), .Bin)), start, emit_to(self, out, .Int(.Bin), start, p, line, col);
p, line, col);
ret; ret;
} }
if nc == 'o' || nc == 'O' { if nc == 'o' || nc == 'O' {
@@ -172,8 +153,7 @@ fn scan_int(self: *Lexer, out: *Vec, start: usize, line: usize, col: usize) {
self.src.ptr[p] == '_') { self.src.ptr[p] == '_') {
p = p + 1; p = p + 1;
} }
emit_to(self, out, .Int(int_value(decode_digits(sub(self, start, p), 8), .Oct)), start, emit_to(self, out, .Int(.Oct), start, p, line, col);
p, line, col);
ret; ret;
} }
} }
@@ -181,12 +161,25 @@ fn scan_int(self: *Lexer, out: *Vec, start: usize, line: usize, col: usize) {
while p < self.src.len && (is_digit(self.src.ptr[p]) || self.src.ptr[p] == '_') { p = p + 1; } while p < self.src.len && (is_digit(self.src.ptr[p]) || self.src.ptr[p] == '_') { p = p + 1; }
if p + 1 < self.src.len && self.src.ptr[p] == '.' && is_digit(self.src.ptr[p + 1]) { if p + 1 < self.src.len && self.src.ptr[p] == '.' && is_digit(self.src.ptr[p + 1]) {
p = p + 1; p = p + 1;
while p < self.src.len && is_digit(self.src.ptr[p]) { p = p + 1; } // 小数部分允许下划线分隔(Rust 风格)。
while p < self.src.len && (is_digit(self.src.ptr[p]) || self.src.ptr[p] == '_') {
p = p + 1;
}
// 可选指数:e/E [+-] 数字(数字允许下划线);仅当确有数字时纳入。
if p < self.src.len && (self.src.ptr[p] == 'e' || self.src.ptr[p] == 'E') {
var q = p + 1;
if q < self.src.len && (self.src.ptr[q] == '+' || self.src.ptr[q] == '-') { q = q + 1; }
if q < self.src.len && is_digit(self.src.ptr[q]) {
p = q + 1;
while p < self.src.len && (is_digit(self.src.ptr[p]) || self.src.ptr[p] == '_') {
p = p + 1;
}
}
}
emit_to(self, out, .Float, start, p, line, col); emit_to(self, out, .Float, start, p, line, col);
ret; ret;
} }
emit_to(self, out, .Int(int_value(decode_digits(sub(self, start, p), 10), .Dec)), start, p, line, emit_to(self, out, .Int(.Dec), start, p, line, col);
col);
} }
fn scan_char(self: *Lexer, out: *Vec, start: usize, line: usize, col: usize) { fn scan_char(self: *Lexer, out: *Vec, start: usize, line: usize, col: usize) {
@@ -481,9 +474,9 @@ fn lex_ok(src: []u8): Vec {
fn at(v: *Vec, index: usize): Token { fn at(v: *Vec, index: usize): Token {
match Vec.get(v, index) { match Vec.get(v, index) {
.Ok(ptr) => { ret (ptr as *Token).*; }, .Ok(ptr) => { ret (ptr as *Token).*; },
.Err(error) => { ret Token.make(.Unknown(0), ByteView.empty(), 0, 0); }, .Err(error) => { ret Token.make(.Unknown(0), ByteView.empty(), 0, 0, 0); },
} }
ret Token.make(.Unknown(0), ByteView.empty(), 0, 0); ret Token.make(.Unknown(0), ByteView.empty(), 0, 0, 0);
} }
fn expect_punct(tok: Token, expected: Punct) { fn expect_punct(tok: Token, expected: Punct) {
@@ -507,11 +500,10 @@ fn expect_ident(tok: Token) {
} }
} }
fn expect_int(tok: Token, value: u64, radix: Radix) { fn expect_int(tok: Token, radix: Radix) {
match tok.kind { match tok.kind {
.Int(iv) => { .Int(r) => {
@assert(iv.value == value, "int value"); @assert(@enum2int(r) == @enum2int(radix), "int radix");
@assert(@enum2int(iv.radix) == @enum2int(radix), "int radix");
}, },
_ => { @assert(false, "expected int"); }, _ => { @assert(false, "expected int"); },
} }
@@ -563,7 +555,7 @@ fn test_basic() {
expect_endline(at(&v, 5)); expect_endline(at(&v, 5));
expect_keyword(at(&v, 6), .Ret); expect_keyword(at(&v, 6), .Ret);
expect_pos(at(&v, 6), 2, 5); expect_pos(at(&v, 6), 2, 5);
expect_int(at(&v, 7), 0, .Dec); expect_int(at(&v, 7), .Dec);
expect_punct(at(&v, 8), .Semicolon); expect_punct(at(&v, 8), .Semicolon);
expect_endline(at(&v, 9)); expect_endline(at(&v, 9));
expect_punct(at(&v, 10), .RBrace); expect_punct(at(&v, 10), .RBrace);
@@ -652,10 +644,10 @@ fn test_comments_and_literals() {
} }
expect_text(s, "\"hi\\n\""); expect_text(s, "\"hi\\n\"");
expect_int(at(&v, 7), 255, .Hex); expect_int(at(&v, 7), .Hex);
expect_int(at(&v, 8), 10, .Bin); expect_int(at(&v, 8), .Bin);
expect_int(at(&v, 9), 15, .Oct); expect_int(at(&v, 9), .Oct);
expect_int(at(&v, 10), 1000, .Dec); expect_int(at(&v, 10), .Dec);
var fl = at(&v, 11); var fl = at(&v, 11);
match fl.kind { match fl.kind {
@@ -666,6 +658,39 @@ fn test_comments_and_literals() {
} }
// 非法输入走 diag:仍产出 token 以便继续收集错误,但 diag 必须记录。 // 非法输入走 diag:仍产出 token 以便继续收集错误,但 diag 必须记录。
// Rust 风格数字字面量:分隔符下划线 + 多进制 + 小数/指数字面量。
fn test_number_forms() {
var v = lex_ok("1_000_000 0xFF_FF 0x_ff 0b_1010 0o_7 1_000.5 1.5_0 1_000.500 1.5e-3 1_0.5_0E2");
defer { Vec.drop(&v); }
@assert(v.len == 11, "number token count");
expect_int(at(&v, 0), .Dec);
expect_text(at(&v, 0), "1_000_000");
expect_int(at(&v, 1), .Hex);
expect_text(at(&v, 1), "0xFF_FF");
expect_int(at(&v, 2), .Hex);
expect_text(at(&v, 2), "0x_ff");
expect_int(at(&v, 3), .Bin);
expect_text(at(&v, 3), "0b_1010");
expect_int(at(&v, 4), .Oct);
expect_text(at(&v, 4), "0o_7");
var i: usize = 5;
while i < 10 {
match at(&v, i).kind {
.Float => {},
_ => { @assert(false, "expected float"); },
}
i = i + 1;
}
expect_text(at(&v, 5), "1_000.5");
expect_text(at(&v, 6), "1.5_0");
expect_text(at(&v, 7), "1_000.500");
expect_text(at(&v, 8), "1.5e-3");
expect_text(at(&v, 9), "1_0.5_0E2");
expect_eof(at(&v, 10));
}
fn test_diag_errors() { fn test_diag_errors() {
var diag = Diag.new(null); var diag = Diag.new(null);
defer { Diag.drop(&diag); } defer { Diag.drop(&diag); }
@@ -689,5 +714,6 @@ fn main() {
test_basic(); test_basic();
test_operators(); test_operators();
test_comments_and_literals(); test_comments_and_literals();
test_number_forms();
test_diag_errors(); test_diag_errors();
} }
+2192
View File
File diff suppressed because it is too large. Load diff
+84 -8
View File
@@ -9,6 +9,8 @@ type .{ .ByteBuffer } = @import("./utils/byte_buffer.spl");
type .{ .Vec } = @import("./utils/vec.spl"); type .{ .Vec } = @import("./utils/vec.spl");
type .{ .Token, .kind_name } = @import("./token.spl"); type .{ .Token, .kind_name } = @import("./token.spl");
type .{ .Lexer } = @import("./lexer.spl"); type .{ .Lexer } = @import("./lexer.spl");
type Ast = @import("./ast.spl");
type Parser = @import("./parser.spl");
type .{ .append_usize } = @import("./utils/fmt.spl"); type .{ .append_usize } = @import("./utils/fmt.spl");
type .{ .read_all, .print, .eprint } = @import("./utils/io.spl"); type .{ .read_all, .print, .eprint } = @import("./utils/io.spl");
type .{ .set_new, .set_add, .set_drop } = @import("./utils/source.spl"); type .{ .set_new, .set_add, .set_drop } = @import("./utils/source.spl");
@@ -18,9 +20,9 @@ type Logger = @import("./utils/log.spl");
fn token_at(tokens: *Vec, index: usize): Token { fn token_at(tokens: *Vec, index: usize): Token {
match Vec.get(tokens, index) { match Vec.get(tokens, index) {
.Ok(ptr) => { ret (ptr as *Token).*; }, .Ok(ptr) => { ret (ptr as *Token).*; },
.Err(error) => { ret Token.make(.Unknown(0), ByteView.empty(), 0, 0); }, .Err(error) => { ret Token.make(.Unknown(0), ByteView.empty(), 0, 0, 0); },
} }
ret Token.make(.Unknown(0), ByteView.empty(), 0, 0); ret Token.make(.Unknown(0), ByteView.empty(), 0, 0, 0);
} }
// 追加成功返回 true;失败(仅 OOM)返回 false。 // 追加成功返回 true;失败(仅 OOM)返回 false。
@@ -136,6 +138,77 @@ fn dump_lex(log: *Logger, path: ByteView): i32 {
ret 0; ret 0;
} }
fn dump_ast(log: *Logger, path: ByteView): i32 {
var content = ByteBuffer.new();
defer { ByteBuffer.drop(&content); }
match read_all(path, &content) {
.Ok => {},
.Err(error) => {
Logger.error(log, "cannot read input file");
ret 1;
},
}
var text = ByteView.from_slice(ByteBuffer.as_slice(content));
var sources = set_new();
defer { set_drop(&sources); }
match set_add(&sources, path, text) {
.Ok(index) => {},
.Err(error) => {
Logger.error(log, "cannot register source");
ret 1;
},
}
var diag = Diag.new(&sources);
defer { Diag.drop(&diag); }
var tokens = Vec.blank(@sizeof(Token), @alignof(Token));
match Lexer.lex(text, &diag, 0) {
.Ok(v) => { tokens = v; },
.Err(error) => {
Logger.error(log, "lex failed");
ret 1;
},
}
defer { Vec.drop(&tokens); }
var ast: Ast = undefined;
match Ast.new(&sources) {
.Ok(v) => { ast = v; },
.Err(error) => {
Logger.error(log, "ast init failed");
ret 1;
},
}
defer { Ast.drop(&ast); }
var p = Parser.new(&ast, tokens, &diag);
Parser.parse(&p);
if Diag.has_error(diag) || p.failed {
var rendered = ByteBuffer.new();
defer { ByteBuffer.drop(&rendered); }
match Diag.render_human(&diag, &rendered) {
.Ok => {},
.Err(error) => {},
}
eprint(ByteBuffer.as_slice(rendered));
ret 1;
}
var out = ByteBuffer.new();
defer { ByteBuffer.drop(&out); }
if !Ast.dump(&ast, &out) {
Logger.fatal(log, "out of memory while dumping ast");
ret 1;
}
match print(ByteBuffer.as_slice(out)) {
.Ok => {},
.Err(error) => { ret 1; },
}
ret 0;
}
fn main(argc: i32, argv: **u8): i32 { fn main(argc: i32, argv: **u8): i32 {
// 无参数:自测运行(`nob test stage2`)视为成功。 // 无参数:自测运行(`nob test stage2`)视为成功。
if argc <= 1 { ret 0; } if argc <= 1 { ret 0; }
@@ -143,7 +216,7 @@ fn main(argc: i32, argv: **u8): i32 {
var log = Logger.new(ByteView.from_slice("splc1")); var log = Logger.new(ByteView.from_slice("splc1"));
if argc < 4 { if argc < 4 {
Logger.fatal(&log, "usage: splc1 --dump <lex> <file> [out]"); Logger.fatal(&log, "usage: splc1 --dump <lex|ast> <file> [out]");
ret 1; ret 1;
} }
@@ -152,12 +225,15 @@ fn main(argc: i32, argv: **u8): i32 {
var path = ByteView.from_cstr(argv[3]); var path = ByteView.from_cstr(argv[3]);
if !ByteView.eq(arg1, ByteView.from_slice("--dump")) { if !ByteView.eq(arg1, ByteView.from_slice("--dump")) {
Logger.fatal(&log, "usage: splc1 --dump <lex> <file> [out]"); Logger.fatal(&log, "usage: splc1 --dump <lex|ast> <file> [out]");
ret 1; ret 1;
} }
if !ByteView.eq(mode, ByteView.from_slice("lex")) { if ByteView.eq(mode, ByteView.from_slice("lex")) {
Logger.fatal(&log, "dump mode not implemented"); ret dump_lex(&log, path);
ret 1;
} }
ret dump_lex(&log, path); if ByteView.eq(mode, ByteView.from_slice("ast")) {
ret dump_ast(&log, path);
}
Logger.fatal(&log, "dump mode not implemented");
ret 1;
} }
+11 -20
View File
@@ -32,16 +32,11 @@ type Radix = enum {
Hex, Hex,
} }
type IntValue = struct {
value: u64,
radix: Radix,
}
type TokenKind = enum { type TokenKind = enum {
Keyword: Keyword, Keyword: Keyword,
Punct: Punct, Punct: Punct,
Ident, Ident,
Int: IntValue, Int: Radix, // 仅记进制;数值由 parser 按任意宽度(mp)解析
Float, Float,
Char: u8, Char: u8,
Str, Str,
@@ -57,13 +52,10 @@ kind: TokenKind,
text: ByteView, text: ByteView,
line: usize, line: usize,
col: usize, col: usize,
offset: usize, // 源文件字节偏移(供 AST Span 使用)
fn make(kind: TokenKind, text: ByteView, line: usize, col: usize): Token { fn make(kind: TokenKind, text: ByteView, line: usize, col: usize, offset: usize): Token {
ret Token { .kind = kind, .text = text, .line = line, .col = col }; ret Token { .kind = kind, .text = text, .line = line, .col = col, .offset = offset };
}
fn int_value(value: u64, radix: Radix): IntValue {
ret IntValue { .value = value, .radix = radix };
} }
fn is_trivia(kind: TokenKind): bool { fn is_trivia(kind: TokenKind): bool {
@@ -254,24 +246,23 @@ fn test_keywords() {
} }
fn test_adt_payloads() { fn test_adt_payloads() {
var t = make(.Int(int_value(255, .Hex)), view("0xFF"), 3, 5); var t = make(.Int(.Hex), view("0xFF"), 3, 5, 0);
@assert(t.line == 3 && t.col == 5, "position"); @assert(t.line == 3 && t.col == 5, "position");
match t.kind { match t.kind {
.Int(iv) => { .Int(radix) => {
@assert(iv.value == 255, "int value");
var hex: Radix = .Hex; var hex: Radix = .Hex;
@assert(@enum2int(iv.radix) == @enum2int(hex), "int radix"); @assert(@enum2int(radix) == @enum2int(hex), "int radix");
}, },
_ => { @assert(false, "expected int"); }, _ => { @assert(false, "expected int"); },
} }
var c = make(.Char('a'), view("'a'"), 1, 1); var c = make(.Char('a'), view("'a'"), 1, 1, 0);
match c.kind { match c.kind {
.Char(value) => { @assert(value == 'a', "char value"); }, .Char(value) => { @assert(value == 'a', "char value"); },
_ => { @assert(false, "expected char"); }, _ => { @assert(false, "expected char"); },
} }
var p = make(.Punct(.ArrowBoth), view("<->"), 1, 1); var p = make(.Punct(.ArrowBoth), view("<->"), 1, 1, 0);
match p.kind { match p.kind {
.Punct(punct) => { .Punct(punct) => {
var both: Punct = .ArrowBoth; var both: Punct = .ArrowBoth;
@@ -280,7 +271,7 @@ fn test_adt_payloads() {
_ => { @assert(false, "expected punct"); }, _ => { @assert(false, "expected punct"); },
} }
var k = make(.Keyword(.Ret), view("ret"), 1, 1); var k = make(.Keyword(.Ret), view("ret"), 1, 1, 0);
match k.kind { match k.kind {
.Keyword(kw) => { .Keyword(kw) => {
var ret_kw: Keyword = .Ret; var ret_kw: Keyword = .Ret;
@@ -390,7 +381,7 @@ fn test_kind_names() {
expect_kind_name(.Punct(.Dollar), "TOK_DOLLAR"); expect_kind_name(.Punct(.Dollar), "TOK_DOLLAR");
expect_kind_name(.Ident, "TOK_IDENT"); expect_kind_name(.Ident, "TOK_IDENT");
expect_kind_name(.Int(int_value(0, .Dec)), "TOK_INT_LITERAL"); expect_kind_name(.Int(.Dec), "TOK_INT_LITERAL");
expect_kind_name(.Float, "TOK_FLOAT_LITERAL"); expect_kind_name(.Float, "TOK_FLOAT_LITERAL");
expect_kind_name(.Char('a'), "TOK_CHAR_LITERAL"); expect_kind_name(.Char('a'), "TOK_CHAR_LITERAL");
expect_kind_name(.Str, "TOK_STRING_LITERAL"); expect_kind_name(.Str, "TOK_STRING_LITERAL");