Something went wrong. Try again.
A general purpose .. programming language written in zig ijadux2.tngl.sh/Rinnescript
zig language
Something went wrong. Try again.
52 kB · 1334 lines
Zig
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335const std = @import("std");const Lexer = @import("lexer.zig").Lexer;const Token = @import("lexer.zig").Token;pub const TokenType = @import("lexer.zig").TokenType;
pub const DataType = union(enum) { primitive: Primitive, slice: *DataType, optional: *DataType, custom: []const u8,
pub fn deinit(self: DataType, allocator: std.mem.Allocator) void { switch (self) { .slice => |inner| { inner.deinit(allocator); allocator.destroy(inner); }, .optional => |inner| { inner.deinit(allocator); allocator.destroy(inner); }, else => {}, } }
pub const Primitive = enum { int, float, str, bool, void_type, inferred, };};
pub const Expr = union(enum) { int_lit: i64, float_lit: f64, string_lit: []const u8, bool_lit: bool, null_lit, /// Enum-literal shorthand `.name` (e.g. `.cpu`). Lexeme points into the /// source; no allocation. enum_lit: []const u8, /// Anonymous fn literal — apply_simd kernel sugar. Heap-boxed so the /// union stays small; owned by the Expr. fn_lit: *FnLit, identifier: []const u8, array_lit: []Expr, struct_lit: []StructFieldInit, binary: struct { op: TokenType, lhs: *Expr, rhs: *Expr, }, unary: struct { op: TokenType, operand: *Expr, }, call: struct { callee: *Expr, args: []Expr, }, call_directive: struct { name: []const u8, args: []Expr, }, field_access: struct { object: *Expr, field: []const u8, }, index_access: struct { object: *Expr, index: *Expr, },
pub const StructFieldInit = struct { name: []const u8, explicit_type: ?DataType, value: Expr, key_expr: ?Expr = null,
fn deinit(self: StructFieldInit, allocator: std.mem.Allocator) void { self.value.deinit(allocator); if (self.key_expr) |ke| ke.deinit(allocator); if (self.explicit_type) |t| t.deinit(allocator); } };
pub fn deinit(self: Expr, allocator: std.mem.Allocator) void { switch (self) { .string_lit => |s| allocator.free(s), .enum_lit => {}, .fn_lit => |fl| { fl.deinit(allocator); allocator.destroy(fl); }, .array_lit => |items| { for (items) |item| item.deinit(allocator); allocator.free(items); }, .struct_lit => |fields| { for (fields) |f| f.deinit(allocator); allocator.free(fields); }, .binary => |b| { b.lhs.deinit(allocator); allocator.destroy(b.lhs); b.rhs.deinit(allocator); allocator.destroy(b.rhs); }, .unary => |u| { u.operand.deinit(allocator); allocator.destroy(u.operand); }, .call => |c| { c.callee.deinit(allocator); allocator.destroy(c.callee); for (c.args) |arg| arg.deinit(allocator); allocator.free(c.args); }, .call_directive => |cd| { for (cd.args) |arg| arg.deinit(allocator); allocator.free(cd.args); }, .field_access => |fa| { fa.object.deinit(allocator); allocator.destroy(fa.object); }, .index_access => |ia| { ia.object.deinit(allocator); allocator.destroy(ia.object); ia.index.deinit(allocator); allocator.destroy(ia.index); }, else => {}, } }};
pub const Stmt = union(enum) { var_decl: struct { is_const: bool, name: []const u8, type_ann: ?DataType, initializer: Expr, }, assignment: struct { target: Expr, value: Expr, }, expr_stmt: Expr, if_stmt: struct { condition: Expr, then_branch: Block, else_branch: ?Block, }, while_stmt: struct { condition: Expr, body: Block, }, for_stmt: struct { var_name: []const u8, iter: Expr, body: Block, }, /// `for i in lo..hi { ... }` — numeric range loop; iterates i = lo..hi-1 /// (inclusive of lo, exclusive of hi). With `..=` the bound is inclusive. range_for: struct { var_name: []const u8, lo: Expr, hi: Expr, hi_inclusive: bool, body: Block, }, break_stmt, continue_stmt, return_stmt: ?Expr, defer_stmt: Expr, fn_decl: FnDecl,
pub fn deinit(self: Stmt, allocator: std.mem.Allocator) void { switch (self) { .var_decl => |d| d.initializer.deinit(allocator), .assignment => |a| { a.target.deinit(allocator); a.value.deinit(allocator); }, .expr_stmt => |e| e.deinit(allocator), .if_stmt => |i| { i.condition.deinit(allocator); i.then_branch.deinit(allocator); if (i.else_branch) |eb| eb.deinit(allocator); }, .while_stmt => |w| { w.condition.deinit(allocator); w.body.deinit(allocator); }, .for_stmt => |f| { allocator.free(f.var_name); f.iter.deinit(allocator); f.body.deinit(allocator); }, .range_for => |r| { allocator.free(r.var_name); r.lo.deinit(allocator); r.hi.deinit(allocator); r.body.deinit(allocator); }, .return_stmt => |r| if (r) |e| e.deinit(allocator), .defer_stmt => |e| e.deinit(allocator), .break_stmt, .continue_stmt => {}, .fn_decl => |fd| fd.deinitFields(allocator), } }};
/// A `{ ... }` statement block plus parallel arrays of the source line/// each statement started on and its starting column (for diagnostics).pub const Block = struct { stmts: []Stmt, lines: []u32, cols: []u32,
pub fn deinit(self: Block, allocator: std.mem.Allocator) void { for (self.stmts) |s| s.deinit(allocator); allocator.free(self.stmts); allocator.free(self.lines); allocator.free(self.cols); }};
pub const FnParam = struct { name: []const u8, param_type: DataType,};
/// An anonymous `fn(x: T) T { ... }` literal. Only legal as the kernel/// argument of a tensor `apply_simd(...)` method call (enforced by the/// compiler, which lowers the literal into a hidden function chunk).pub const FnLit = struct { params: []FnParam, body: Block, /// Lane hint captured from a `@vector(N, T)` parameter annotation. /// 0 means unspecified (the VM falls back to scalar execution; the /// native backend uses it to pick vector widths). vector_width: u64 = 0,
pub fn deinit(self: *FnLit, allocator: std.mem.Allocator) void { for (self.params) |p| p.param_type.deinit(allocator); allocator.free(self.params); self.body.deinit(allocator); }};
pub const Decorator = struct { name: []const u8, args: []Expr,
pub fn deinit(self: Decorator, allocator: std.mem.Allocator) void { for (self.args) |arg| arg.deinit(allocator); allocator.free(self.args); }};
pub const FnDecl = struct { is_pub: bool, name: []const u8, params: []FnParam, return_type: DataType, decorators: []Decorator, body: Block,
pub fn deinitFields(self: FnDecl, allocator: std.mem.Allocator) void { for (self.params) |p| p.param_type.deinit(allocator); allocator.free(self.params); self.return_type.deinit(allocator); for (self.decorators) |dec| dec.deinit(allocator); allocator.free(self.decorators); self.body.deinit(allocator); }
pub fn deinit(self: FnDecl, allocator: std.mem.Allocator) void { self.deinitFields(allocator); }};
pub const Directive = union(enum) { import_directive: []const u8, include_directive: []Expr, generic: GenericDirective,
pub const GenericDirective = struct { name: []const u8, args: []Expr,
pub fn deinit(self: GenericDirective, allocator: std.mem.Allocator) void { for (self.args) |arg| arg.deinit(allocator); allocator.free(self.args); } };};
pub const Program = struct { directives: []Directive, fn_decls: []FnDecl, global_stmts: Block,
pub fn deinit(self: Program, allocator: std.mem.Allocator) void { for (self.directives) |d| switch (d) { .include_directive => |exprs| { for (exprs) |e| e.deinit(allocator); allocator.free(exprs); }, .generic => |g| g.deinit(allocator), else => {}, }; allocator.free(self.directives); for (self.fn_decls) |fd| fd.deinitFields(allocator); allocator.free(self.fn_decls); self.global_stmts.deinit(allocator); }};
pub const Parser = struct { lexer: Lexer, allocator: std.mem.Allocator, current_token: Token, /// Diagnostics for the most recent failure (set by `fail`). err_line: usize = 0, err_col: usize = 0, err_msg: ?[]u8 = null,
pub fn init(allocator: std.mem.Allocator, source: []const u8) Parser { var lex = Lexer.init(source); const tok = lex.nextToken(); return .{ .lexer = lex, .allocator = allocator, .current_token = tok }; }
pub fn deinit(self: *Parser) void { if (self.err_msg) |m| self.allocator.free(m); self.err_msg = null; }
fn advance(self: *Parser) void { self.current_token = self.lexer.nextToken(); }
fn match(self: *Parser, tag: TokenType) bool { if (self.current_token.tag == tag) { self.advance(); return true; } return false; }
fn expect(self: *Parser, tag: TokenType) ParseError!Token { if (self.current_token.tag == tag) { const tok = self.current_token; self.advance(); return tok; } return self.fail(error.UnexpectedToken, "expected '{s}', found '{s}'", .{ @tagName(tag), @tagName(self.current_token.tag) }); }
/// Records the current token position plus a formatted message for /// diagnostics, then returns `e`. fn fail(self: *Parser, e: ParseError, comptime fmt: []const u8, args: anytype) ParseError { self.err_line = self.current_token.line; self.err_col = self.current_token.column; self.err_msg = std.fmt.allocPrint(self.allocator, fmt ++ " (at line {d}:{d})", args ++ .{ self.current_token.line, self.current_token.column }) catch null; return e; }
pub fn parseProgram(self: *Parser) ParseError!Program { var directives: std.ArrayList(Directive) = .empty; var fn_decls: std.ArrayList(FnDecl) = .empty; var global_stmts: std.ArrayList(Stmt) = .empty; var global_lines: std.ArrayList(u32) = .empty; var global_cols: std.ArrayList(u32) = .empty; var pending_decorators: std.ArrayList(Decorator) = .empty; defer pending_decorators.deinit(self.allocator);
while (self.current_token.tag != .eof) { if (self.current_token.tag == .at_directive) { const name = self.current_token.directiveName();
if (std.mem.eql(u8, name, "import")) { try self.parseImport(&directives); continue; } else if (std.mem.eql(u8, name, "include")) { try self.parseInclude(&directives); continue; }
// Generic directive or decorator: `@name(args...)` const dir_name = name; const dir_line = self.current_token.line; const dir_col: u32 = @intCast(self.current_token.column); self.advance(); var args: std.ArrayList(Expr) = .empty; if (self.match(.l_paren)) { while (self.current_token.tag != .r_paren and self.current_token.tag != .eof) { try args.append(self.allocator, try self.parseOr()); if (!self.match(.comma)) break; } _ = try self.expect(.r_paren); } if (self.current_token.tag == .kw_pub or self.current_token.tag == .kw_fn) { // Decorator directly preceding a function declaration. try pending_decorators.append(self.allocator, .{ .name = dir_name, .args = try args.toOwnedSlice(self.allocator) }); continue; } if (pending_decorators.items.len > 0) { return error.UnexpectedToken; } _ = self.match(.semicolon); if (!isModuleDirective(dir_name)) { // Executable builtin call at global scope, e.g. // `@stdout("hi")` — compile it as an expression stmt. try global_lines.append(self.allocator, @intCast(dir_line)); try global_cols.append(self.allocator, dir_col); try global_stmts.append(self.allocator, .{ .expr_stmt = .{ .call_directive = .{ .name = dir_name, .args = try args.toOwnedSlice(self.allocator) } } }); continue; } try directives.append(self.allocator, .{ .generic = .{ .name = dir_name, .args = try args.toOwnedSlice(self.allocator) } }); } else if (self.current_token.tag == .kw_pub or self.current_token.tag == .kw_fn) { var decl = try self.parseFnDecl(); decl.decorators = try mergeDecorators(self.allocator, &pending_decorators, decl.decorators); try fn_decls.append(self.allocator, decl); } else { try global_lines.append(self.allocator, @intCast(self.current_token.line)); try global_cols.append(self.allocator, @intCast(self.current_token.column)); try global_stmts.append(self.allocator, try self.parseStmt()); } }
return .{ .directives = try directives.toOwnedSlice(self.allocator), .fn_decls = try fn_decls.toOwnedSlice(self.allocator), .global_stmts = .{ .stmts = try global_stmts.toOwnedSlice(self.allocator), .lines = try global_lines.toOwnedSlice(self.allocator), .cols = try global_cols.toOwnedSlice(self.allocator), }, }; }
fn parseImport(self: *Parser, directives: *std.ArrayList(Directive)) ParseError!void { self.advance(); _ = try self.expect(.assign); if (self.match(.period)) { _ = try self.expect(.l_brace); while (self.current_token.tag != .r_brace and self.current_token.tag != .eof) { if (self.current_token.tag == .identifier) { const mod_tok = self.current_token; self.advance(); try directives.append(self.allocator, .{ .import_directive = mod_tok.lexeme }); } if (!self.match(.comma)) break; } _ = try self.expect(.r_brace); } else if (self.match(.l_brace)) { while (self.current_token.tag != .r_brace and self.current_token.tag != .eof) { if (self.current_token.tag == .identifier) { const mod_tok = self.current_token; self.advance(); try directives.append(self.allocator, .{ .import_directive = mod_tok.lexeme }); } if (!self.match(.comma)) break; } _ = try self.expect(.r_brace); } else if (self.current_token.tag == .string_literal) { const str_tok = self.current_token; self.advance(); try directives.append(self.allocator, .{ .import_directive = str_tok.lexeme }); } _ = self.match(.semicolon); }
fn parseInclude(self: *Parser, directives: *std.ArrayList(Directive)) ParseError!void { self.advance(); _ = self.match(.colon); _ = self.match(.l_bracket); _ = self.match(.r_bracket); _ = self.match(.type_str); _ = try self.expect(.assign); const list_expr = try self.parseOr(); if (list_expr == .array_lit) { try directives.append(self.allocator, .{ .include_directive = list_expr.array_lit }); } else { list_expr.deinit(self.allocator); } _ = self.match(.semicolon); }
fn mergeDecorators(allocator: std.mem.Allocator, pending: *std.ArrayList(Decorator), existing: []Decorator) ![]Decorator { if (pending.items.len == 0) return existing; var merged: std.ArrayList(Decorator) = .empty; try merged.appendSlice(allocator, pending.items); try merged.appendSlice(allocator, existing); pending.clearRetainingCapacity(); if (existing.len > 0) allocator.free(existing); return merged.toOwnedSlice(allocator); }
/// Directive names that describe the module (types, build, imports) /// rather than perform runtime work. Anything else written as a /// statement — e.g. `@stdout(...)` — is executable code. fn isModuleDirective(name: []const u8) bool { const module_level = [_][]const u8{ "import_c", "load_lib", "comptime", "build", "tensor", "fetch", "fetch_pkg", }; for (module_level) |m| { if (std.mem.eql(u8, name, m)) return true; } return false; }
fn parseFnDecl(self: *Parser) ParseError!FnDecl { var decorators: std.ArrayList(Decorator) = .empty;
const is_pub = self.match(.kw_pub); _ = try self.expect(.kw_fn); const name_tok = try self.expect(.identifier);
_ = try self.expect(.l_paren); var params: std.ArrayList(FnParam) = .empty; while (self.current_token.tag != .r_paren and self.current_token.tag != .eof) { const p_name = try self.expect(.identifier); _ = try self.expect(.colon); try params.append(self.allocator, .{ .name = p_name.lexeme, .param_type = try self.parseDataType() }); if (!self.match(.comma)) break; } _ = try self.expect(.r_paren);
// Return type: optional leading colon (`-> int` style) or bare type // token as in `pub fn main(args: []str) u32 { ... }`. var ret_type: DataType = .{ .primitive = .void_type }; if (self.match(.colon)) { ret_type = try self.parseDataType(); } else if (self.startsType()) { ret_type = try self.parseDataType(); }
const body = try self.parseBlock();
return .{ .is_pub = is_pub, .name = name_tok.lexeme, .params = try params.toOwnedSlice(self.allocator), .return_type = ret_type, .decorators = try decorators.toOwnedSlice(self.allocator), .body = body, }; }
fn startsType(self: *Parser) bool { return switch (self.current_token.tag) { .type_int, .type_float, .type_str, .type_bool, .l_bracket, .question, .identifier, .at_directive => true, else => false, }; }
/// Parameter-type parser used by anonymous fn literals. Identical to /// `parseDataType`, except that a `@vector(N, T)` annotation records its /// leading integer N into `out_width` as a SIMD lane hint. fn parseParamTypeCapturingWidth(self: *Parser, out_width: *u64) ParseError!DataType { if (self.current_token.tag == .at_directive and std.mem.eql(u8, self.current_token.directiveName(), "vector")) { const dir_tok = self.current_token; self.advance(); if (self.match(.l_paren)) { var depth: usize = 1; while (depth > 0 and self.current_token.tag != .eof) { if (out_width.* == 0 and self.current_token.tag == .int_literal) { out_width.* = std.fmt.parseInt(u64, self.current_token.lexeme, 10) catch 0; } if (self.current_token.tag == .l_paren) depth += 1; if (self.current_token.tag == .r_paren) depth -= 1; self.advance(); } } return .{ .custom = dir_tok.lexeme }; } return self.parseDataType(); }
fn parseDataType(self: *Parser) ParseError!DataType { if (self.current_token.tag == .at_directive) { // Directive types such as `@vector(4, f32)` are opaque to the // Phase 1 compiler; consume a balanced parenthesized argument // list and treat the result as a custom type. const dir_tok = self.current_token; self.advance(); if (self.match(.l_paren)) { var depth: usize = 1; while (depth > 0 and self.current_token.tag != .eof) { if (self.current_token.tag == .l_paren) depth += 1; if (self.current_token.tag == .r_paren) depth -= 1; self.advance(); } } return .{ .custom = dir_tok.lexeme }; } if (self.match(.question)) { const inner = try self.allocator.create(DataType); inner.* = try self.parseDataType(); return .{ .optional = inner }; } if (self.match(.l_bracket)) { // `[]T`, `[*]T`, `[N]T` — all treated as slices in Phase 1. while (self.current_token.tag != .r_bracket and self.current_token.tag != .eof) { self.advance(); } _ = try self.expect(.r_bracket); const inner = try self.allocator.create(DataType); inner.* = try self.parseDataType(); return .{ .slice = inner }; } if (self.match(.type_int)) return .{ .primitive = .int }; if (self.match(.type_float)) return .{ .primitive = .float }; if (self.match(.type_str)) return .{ .primitive = .str }; if (self.match(.type_bool)) return .{ .primitive = .bool }; if (self.current_token.tag == .identifier) { const name = self.current_token.lexeme; self.advance(); return .{ .custom = name }; } return .{ .primitive = .inferred }; }
fn parseBlock(self: *Parser) ParseError!Block { _ = try self.expect(.l_brace); var stmts: std.ArrayList(Stmt) = .empty; var lines: std.ArrayList(u32) = .empty; var cols: std.ArrayList(u32) = .empty; while (self.current_token.tag != .r_brace and self.current_token.tag != .eof) { try lines.append(self.allocator, @intCast(self.current_token.line)); try cols.append(self.allocator, @intCast(self.current_token.column)); try stmts.append(self.allocator, try self.parseStmt()); } _ = try self.expect(.r_brace); return .{ .stmts = try stmts.toOwnedSlice(self.allocator), .lines = try lines.toOwnedSlice(self.allocator), .cols = try cols.toOwnedSlice(self.allocator), }; }
fn parseStmt(self: *Parser) ParseError!Stmt { if (self.current_token.tag == .kw_fn) { return .{ .fn_decl = try self.parseFnDecl() }; }
if (self.current_token.tag == .kw_const or self.current_token.tag == .kw_var) { const is_const = (self.current_token.tag == .kw_const); self.advance(); const name_tok = try self.expect(.identifier); var type_ann: ?DataType = null; if (self.match(.colon)) { type_ann = try self.parseDataType(); } _ = try self.expect(.assign); const init_expr = try self.parseOr(); _ = self.match(.semicolon); return .{ .var_decl = .{ .is_const = is_const, .name = name_tok.lexeme, .type_ann = type_ann, .initializer = init_expr } }; }
// @defer — deferred expression execution (Phase 2 will wire real // resource semantics; Phase 1 executes defers at scope exit). if (self.current_token.tag == .at_directive and std.mem.eql(u8, self.current_token.directiveName(), "defer")) { self.advance(); const expr = try self.parseOr(); _ = self.match(.semicolon); return .{ .defer_stmt = expr }; } if (self.current_token.tag == .kw_defer) { self.advance(); const expr = try self.parseOr(); _ = self.match(.semicolon); return .{ .defer_stmt = expr }; }
if (self.match(.kw_if)) { return try self.parseIfTail(); }
if (self.match(.kw_while)) { const cond = try self.parseOr(); const body = try self.parseBlock(); return .{ .while_stmt = .{ .condition = cond, .body = body } }; }
// for NAME | ITER { BODY } — sugar over an index loop; `continue` // still advances the iteration. Also supports the range forms // `for i in lo..hi` and `for i in lo..=hi`. if (self.match(.kw_for)) { const name_tok = self.expect(.identifier) catch return error.UnexpectedToken; const name = self.allocator.dupe(u8, name_tok.lexeme) catch return error.OutOfMemory; if (self.match(.kw_in)) { const lo = try self.parseOr(); const inclusive = if (self.match(.dot_dot_equal)) true else if (self.match(.dot_dot)) false else return error.UnexpectedToken; const hi = try self.parseOr(); const body = try self.parseBlock(); return .{ .range_for = .{ .var_name = name, .lo = lo, .hi = hi, .hi_inclusive = inclusive, .body = body } }; } _ = self.expect(.pipe) catch return error.UnexpectedToken; const iter = try self.parseOr(); const body = try self.parseBlock(); return .{ .for_stmt = .{ .var_name = name, .iter = iter, .body = body } }; }
if (self.match(.kw_break)) { _ = self.match(.semicolon); return .break_stmt; }
if (self.match(.kw_continue)) { _ = self.match(.semicolon); return .continue_stmt; }
if (self.match(.kw_return)) { var val: ?Expr = null; if (self.current_token.tag != .semicolon and self.current_token.tag != .r_brace) { val = try self.parseOr(); } _ = self.match(.semicolon); return .{ .return_stmt = val }; }
const expr = try self.parseOr(); if (self.match(.assign)) { switch (expr) { .identifier, .index_access => {}, else => return error.InvalidAssignmentTarget, } const val = try self.parseOr(); _ = self.match(.semicolon); return .{ .assignment = .{ .target = expr, .value = val } }; } _ = self.match(.semicolon); return .{ .expr_stmt = expr }; }
fn parseIfTail(self: *Parser) ParseError!Stmt { const cond = try self.parseOr(); const then_branch = try self.parseBlock();
var else_branch: ?Block = null; if (self.match(.kw_else)) { if (self.match(.kw_if)) { // `else if` chains recurse into an inner if statement. const inner_line: u32 = @intCast(self.current_token.line); const inner_col: u32 = @intCast(self.current_token.column); const inner = try self.parseIfTail(); var one_stmts: std.ArrayList(Stmt) = .empty; var one_lines: std.ArrayList(u32) = .empty; var one_cols: std.ArrayList(u32) = .empty; try one_stmts.append(self.allocator, inner); try one_lines.append(self.allocator, inner_line); try one_cols.append(self.allocator, inner_col); else_branch = .{ .stmts = try one_stmts.toOwnedSlice(self.allocator), .lines = try one_lines.toOwnedSlice(self.allocator), .cols = try one_cols.toOwnedSlice(self.allocator), }; } else { else_branch = try self.parseBlock(); } }
return .{ .if_stmt = .{ .condition = cond, .then_branch = then_branch, .else_branch = else_branch } }; }
pub const ParseError = error{ UnexpectedToken, InvalidAssignmentTarget, OutOfMemory, Overflow, InvalidCharacter, InvalidExpression };
pub fn parseExpr(self: *Parser) ParseError!Expr { return try self.parseOr(); }
// ---- Expression precedence chain ---- // or -> and -> equality -> comparison -> additive -> multiplicative -> unary -> postfix
fn parseOr(self: *Parser) ParseError!Expr { var lhs = try self.parseAnd(); while (self.current_token.tag == .kw_or) { self.advance(); const rhs = try self.parseAnd(); lhs = try self.makeBinary(.kw_or, lhs, rhs); } return lhs; }
fn parseAnd(self: *Parser) ParseError!Expr { var lhs = try self.parseEquality(); while (self.current_token.tag == .kw_and) { self.advance(); const rhs = try self.parseEquality(); lhs = try self.makeBinary(.kw_and, lhs, rhs); } return lhs; }
fn parseEquality(self: *Parser) ParseError!Expr { var lhs = try self.parseComparison(); while (self.current_token.tag == .equal_equal or self.current_token.tag == .not_equal) { const op = self.current_token.tag; self.advance(); const rhs = try self.parseComparison(); lhs = try self.makeBinary(op, lhs, rhs); } return lhs; }
fn parseComparison(self: *Parser) ParseError!Expr { var lhs = try self.parseAdditive(); while (self.current_token.tag == .less or self.current_token.tag == .less_equal or self.current_token.tag == .greater or self.current_token.tag == .greater_equal) { const op = self.current_token.tag; self.advance(); const rhs = try self.parseAdditive(); lhs = try self.makeBinary(op, lhs, rhs); } return lhs; }
fn parseAdditive(self: *Parser) ParseError!Expr { var lhs = try self.parseMultiplicative(); while (self.current_token.tag == .plus or self.current_token.tag == .minus) { const op = self.current_token.tag; self.advance(); const rhs = try self.parseMultiplicative(); lhs = try self.makeBinary(op, lhs, rhs); } return lhs; }
fn parseMultiplicative(self: *Parser) ParseError!Expr { var lhs = try self.parseUnary(); while (self.current_token.tag == .star or self.current_token.tag == .slash or self.current_token.tag == .percent) { const op = self.current_token.tag; self.advance(); const rhs = try self.parseUnary(); lhs = try self.makeBinary(op, lhs, rhs); } return lhs; }
fn makeBinary(self: *Parser, op: TokenType, lhs: Expr, rhs: Expr) ParseError!Expr { const lhs_box = try self.allocator.create(Expr); lhs_box.* = lhs; const rhs_box = try self.allocator.create(Expr); rhs_box.* = rhs; return .{ .binary = .{ .op = op, .lhs = lhs_box, .rhs = rhs_box } }; }
fn makeUnary(self: *Parser, op: TokenType, operand: Expr) ParseError!Expr { const operand_box = try self.allocator.create(Expr); operand_box.* = operand; return .{ .unary = .{ .op = op, .operand = operand_box } }; }
fn parseUnary(self: *Parser) ParseError!Expr { // `not` keyword and `!` both compile to logical not. if (self.match(.kw_not)) { const operand = try self.parseUnary(); return try self.makeUnaryNot(operand); } if (self.match(.bang)) { const operand = try self.parseUnary(); return try self.makeUnaryNot(operand); } if (self.match(.minus)) { const operand = try self.parseUnary(); return try self.makeUnary(.minus, operand); } return try self.parsePostfix(); }
fn makeUnaryNot(self: *Parser, operand: Expr) ParseError!Expr { return try self.makeUnary(.kw_not, operand); }
fn parsePostfix(self: *Parser) ParseError!Expr { var primary = try self.parsePrimary();
while (true) { if (self.match(.period)) { const field_tok = try self.expect(.identifier); const obj_box = try self.allocator.create(Expr); obj_box.* = primary; primary = .{ .field_access = .{ .object = obj_box, .field = field_tok.lexeme } }; } else if (self.match(.l_paren)) { var args: std.ArrayList(Expr) = .empty; while (self.current_token.tag != .r_paren and self.current_token.tag != .eof) { try args.append(self.allocator, try self.parseOr()); if (!self.match(.comma)) break; } _ = try self.expect(.r_paren); const callee_box = try self.allocator.create(Expr); callee_box.* = primary; primary = .{ .call = .{ .callee = callee_box, .args = try args.toOwnedSlice(self.allocator) } }; } else if (self.match(.l_bracket)) { const idx = try self.parseOr(); _ = try self.expect(.r_bracket); const obj_box = try self.allocator.create(Expr); obj_box.* = primary; const idx_box = try self.allocator.create(Expr); idx_box.* = idx; primary = .{ .index_access = .{ .object = obj_box, .index = idx_box } }; } else { break; } }
return primary; }
fn unescapeString(self: *Parser, raw: []const u8) ParseError![]const u8 { // raw includes surrounding quotes. The result is always an owned // copy so Expr.deinit can free it uniformly. if (raw.len < 2) return raw; const body = raw[1 .. raw.len - 1]; if (std.mem.indexOfScalar(u8, body, '\\') == null) { return self.allocator.dupe(u8, body) catch return error.OutOfMemory; }
var out: std.ArrayList(u8) = .empty; var i: usize = 0; while (i < body.len) : (i += 1) { if (body[i] == '\\' and i + 1 < body.len) { i += 1; const esc: u8 = switch (body[i]) { 'n' => '\n', 't' => '\t', 'r' => '\r', '0' => 0, '"' => '"', '\\' => '\\', else => body[i], }; out.append(self.allocator, esc) catch return error.OutOfMemory; } else { out.append(self.allocator, body[i]) catch return error.OutOfMemory; } } return out.toOwnedSlice(self.allocator) catch return error.OutOfMemory; }
fn parsePrimary(self: *Parser) ParseError!Expr { var primary: Expr = undefined;
if (self.current_token.tag == .int_literal) { const val = try std.fmt.parseInt(i64, self.current_token.lexeme, 10); self.advance(); primary = .{ .int_lit = val }; } else if (self.current_token.tag == .float_literal) { const val = try std.fmt.parseFloat(f64, self.current_token.lexeme); self.advance(); primary = .{ .float_lit = val }; } else if (self.current_token.tag == .string_literal) { const lex = self.current_token.lexeme; self.advance(); primary = .{ .string_lit = try self.unescapeString(lex) }; } else if (self.current_token.tag == .kw_true) { self.advance(); primary = .{ .bool_lit = true }; } else if (self.current_token.tag == .kw_false) { self.advance(); primary = .{ .bool_lit = false }; } else if (self.current_token.tag == .kw_null) { self.advance(); primary = .null_lit; } else if (self.current_token.tag == .at_directive) { // Open-ended directive call: `@stdout("hi")`, `@env("HOME")`, // `@tensor(f32, ...)`. The directive name is resolved by the // compiler/backend, keeping the lexer O(1) and future-proof. const name = self.current_token.directiveName(); self.advance(); var args: std.ArrayList(Expr) = .empty; if (self.match(.l_paren)) { while (self.current_token.tag != .r_paren and self.current_token.tag != .eof) { try args.append(self.allocator, try self.parseOr()); if (!self.match(.comma)) break; } _ = try self.expect(.r_paren); } primary = .{ .call_directive = .{ .name = name, .args = try args.toOwnedSlice(self.allocator) } }; } else if (self.current_token.tag == .kw_fn) { // Anonymous fn literal: `fn(x: f32) f32 { ... }`. The compiler // restricts these to apply_simd kernel position. self.advance(); _ = try self.expect(.l_paren); var params: std.ArrayList(FnParam) = .empty; var vector_width: u64 = 0; errdefer { for (params.items) |*p| p.param_type.deinit(self.allocator); params.deinit(self.allocator); } while (self.current_token.tag != .r_paren and self.current_token.tag != .eof) { const p_name = try self.expect(.identifier); _ = try self.expect(.colon); const p_type = try self.parseParamTypeCapturingWidth(&vector_width); try params.append(self.allocator, .{ .name = p_name.lexeme, .param_type = p_type }); if (!self.match(.comma)) break; } _ = try self.expect(.r_paren);
// Optional return type, same rules as named fn declarations. if (self.match(.colon)) { _ = try self.parseDataType(); } else if (self.startsType()) { _ = try self.parseDataType(); }
const body = try self.parseBlock(); const fl = try self.allocator.create(FnLit); fl.* = .{ .params = try params.toOwnedSlice(self.allocator), .body = body, .vector_width = vector_width }; primary = .{ .fn_lit = fl }; } else if (self.current_token.tag == .identifier or self.current_token.tag == .type_int or self.current_token.tag == .type_float or self.current_token.tag == .type_str or self.current_token.tag == .type_bool) { // Primitive type names double as conversion builtins when they // appear in expression position, e.g. `int("42")`. const id = self.current_token.lexeme; self.advance(); primary = .{ .identifier = id }; } else if (self.match(.period)) { if (self.current_token.tag == .identifier) { // Enum-literal shorthand: `.cpu`, `.f32`. The lexer emits // float literals for `.5`-style digit starts, so there is no // ambiguity with numeric literals here. const name = self.current_token.lexeme; self.advance(); primary = .{ .enum_lit = name }; } else { // `.{...}` bundle: plain items form a tuple/array literal, // `.name = value` entries form a struct literal (option // bundles like `@tensor(..., .{ .device = .cpu })`). _ = try self.expect(.l_brace); var items: std.ArrayList(Expr) = .empty; var fields: std.ArrayList(Expr.StructFieldInit) = .empty; errdefer { for (items.items) |*it| it.deinit(self.allocator); items.deinit(self.allocator); for (fields.items) |*f| f.deinit(self.allocator); fields.deinit(self.allocator); } while (self.current_token.tag != .r_brace and self.current_token.tag != .eof) { if (self.current_token.tag == .period) { self.advance(); const f_name = try self.expect(.identifier); _ = try self.expect(.assign); try fields.append(self.allocator, .{ .name = f_name.lexeme, .explicit_type = null, .value = try self.parseOr(), .key_expr = null, }); } else { try items.append(self.allocator, try self.parseOr()); } if (!self.match(.comma)) break; } _ = try self.expect(.r_brace); if (fields.items.len > 0) { if (items.items.len > 0) return error.UnexpectedToken; primary = .{ .struct_lit = try fields.toOwnedSlice(self.allocator) }; } else { primary = .{ .array_lit = try items.toOwnedSlice(self.allocator) }; } } } else if (self.match(.l_bracket)) { var items: std.ArrayList(Expr) = .empty; while (self.current_token.tag != .r_bracket and self.current_token.tag != .eof) { try items.append(self.allocator, try self.parseOr()); if (!self.match(.comma)) break; } _ = try self.expect(.r_bracket); primary = .{ .array_lit = try items.toOwnedSlice(self.allocator) }; } else if (self.match(.l_paren)) { primary = try self.parseOr(); _ = try self.expect(.r_paren); } else if (self.match(.l_brace)) { primary = try self.parseStructLitBody(); } else { return error.InvalidExpression; }
return primary; }
fn parseStructLitBody(self: *Parser) ParseError!Expr { var fields: std.ArrayList(Expr.StructFieldInit) = .empty; while (self.current_token.tag != .r_brace and self.current_token.tag != .eof) { if (self.match(.period)) { const f_name = try self.expect(.identifier); var explicit_t: ?DataType = null; if (self.match(.colon)) { explicit_t = try self.parseDataType(); } _ = try self.expect(.assign); try fields.append(self.allocator, .{ .name = f_name.lexeme, .explicit_type = explicit_t, .value = try self.parseOr(), .key_expr = null }); } else { const key = try self.parseOr(); if (self.match(.colon)) { const val = try self.parseOr(); try fields.append(self.allocator, .{ .name = "", .explicit_type = null, .value = val, .key_expr = key }); } else { key.deinit(self.allocator); return error.UnexpectedToken; } } if (!self.match(.comma)) break; } _ = try self.expect(.r_brace); return .{ .struct_lit = try fields.toOwnedSlice(self.allocator) }; }};
// ---- Tests ----
test "Parser parses basic script" { const source = \\const tab_size: int = 4; \\pub fn main(args: []str) int { \\ @stdout(tab_size); \\} ; var p = Parser.init(std.testing.allocator, source); const prog = try p.parseProgram(); defer prog.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), prog.fn_decls.len); try std.testing.expectEqualSlices(u8, "main", prog.fn_decls[0].name); try std.testing.expect(prog.fn_decls[0].return_type == .primitive); try std.testing.expectEqual(DataType.Primitive.int, prog.fn_decls[0].return_type.primitive);}
test "Parser handles nested fn declarations" { var p = Parser.init(std.testing.allocator, \\fn helper() { \\ @stdout("toggle"); \\} \\ \\pub fn main(args: []str) { \\ helper(); \\} ); const prog = try p.parseProgram(); defer prog.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 2), prog.fn_decls.len); try std.testing.expectEqualStrings("helper", prog.fn_decls[0].name);}
test "Parser handles map literal entries" { var p = Parser.init(std.testing.allocator, \\pub fn main() { \\ bind_group("normal", { \\ key("<leader>w"): ":write", \\ key("Ctrl-h"): "window-left", \\ }); \\} ); const prog = try p.parseProgram(); defer prog.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), prog.fn_decls.len);}
test "Parser handles @import without leading dot" { var p = Parser.init(std.testing.allocator, \\@import = { std, ai }; ); const prog = try p.parseProgram(); defer prog.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 2), prog.directives.len);}
test "Parser precedence: additive vs multiplicative" { var p = Parser.init(std.testing.allocator, "1 + 2 * 3"); const e = try p.parseExpr(); defer e.deinit(std.testing.allocator); try std.testing.expect(e == .binary); try std.testing.expectEqual(TokenType.plus, e.binary.op); try std.testing.expect(e.binary.rhs.* == .binary); try std.testing.expectEqual(TokenType.star, e.binary.rhs.binary.op);}
test "Parser comparison and equality chains" { var p = Parser.init(std.testing.allocator, "a + 1 < b * 2 == true"); const e = try p.parseExpr(); defer e.deinit(std.testing.allocator); try std.testing.expect(e == .binary); try std.testing.expectEqual(TokenType.equal_equal, e.binary.op);}
test "Parser index access and assignment" { var p = Parser.init(std.testing.allocator, \\fn f() { \\ xs[0] = xs[1] + 2; \\} ); const prog = try p.parseProgram(); defer prog.deinit(std.testing.allocator); const body = prog.fn_decls[0].body.stmts; try std.testing.expect(body[0] == .assignment); try std.testing.expect(body[0].assignment.target == .index_access);}
test "Parser while loop with break and continue" { var p = Parser.init(std.testing.allocator, \\fn f() { \\ while x < 10 { \\ if x == 3 { break; } \\ continue; \\ } \\} ); const prog = try p.parseProgram(); defer prog.deinit(std.testing.allocator); const body = prog.fn_decls[0].body.stmts; try std.testing.expect(body[0] == .while_stmt); try std.testing.expect(body[0].while_stmt.body.stmts.len == 2);}
test "Parser else-if chain" { var p = Parser.init(std.testing.allocator, \\fn f(x: int) { \\ if x == 1 { @stdout("one"); } \\ else if x == 2 { @stdout("two"); } \\ else { @stdout("many"); } \\} ); const prog = try p.parseProgram(); defer prog.deinit(std.testing.allocator); const s = prog.fn_decls[0].body.stmts[0]; try std.testing.expect(s == .if_stmt); const els = s.if_stmt.else_branch.?; try std.testing.expectEqual(@as(usize, 1), els.stmts.len); try std.testing.expect(els.stmts[0] == .if_stmt);}
test "Parser generic directive statement" { var p = Parser.init(std.testing.allocator, \\@fetch_pkg("https://example.com/pkg.rg", "1.0.0"); \\fn f() {} ); const prog = try p.parseProgram(); defer prog.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), prog.directives.len); try std.testing.expect(prog.directives[0] == .generic); try std.testing.expectEqualStrings("fetch_pkg", prog.directives[0].generic.name); try std.testing.expectEqual(@as(usize, 2), prog.directives[0].generic.args.len);}
test "Parser decorator attaches to fn" { var p = Parser.init(std.testing.allocator, \\@export("fib") \\pub fn fib(n: int) int { \\ return n; \\} ); const prog = try p.parseProgram(); defer prog.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), prog.fn_decls.len); try std.testing.expectEqual(@as(usize, 1), prog.fn_decls[0].decorators.len); try std.testing.expectEqualStrings("export", prog.fn_decls[0].decorators[0].name);}
test "Parser string escapes" { var p = Parser.init(std.testing.allocator, "\"a\\nb\\tc\""); const e = try p.parseExpr(); defer e.deinit(std.testing.allocator); try std.testing.expectEqualStrings("a\nb\tc", e.string_lit);}
test "Parser parenthesized expression" { var p = Parser.init(std.testing.allocator, "(1 + 2) * 3"); const e = try p.parseExpr(); defer e.deinit(std.testing.allocator); try std.testing.expect(e == .binary); try std.testing.expectEqual(TokenType.star, e.binary.op); try std.testing.expectEqual(@as(i64, 1), e.binary.lhs.binary.lhs.int_lit);}
test "Parser diagnostics record failing position" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator();
// Line 3, col 9: `end` instead of a closing brace. var p = Parser.init(arena, \\fn ok() int { \\ return 1; \\} \\ \\fn broken() int { end ); try std.testing.expectError(error.UnexpectedToken, p.parseProgram()); try std.testing.expectEqual(@as(usize, 5), p.err_line); try std.testing.expect(p.err_msg != null); try std.testing.expect(std.mem.indexOf(u8, p.err_msg.?, "line 5:") != null); try std.testing.expect(std.mem.indexOf(u8, p.err_msg.?, "expected 'r_brace'") != null);}
test "Block records per-statement source lines" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator();
var p = Parser.init(arena, \\const a: int = 1; \\fn f() void { \\ @stdout("x\n", .{}); \\ const b: int = 2; \\} ); const prog = try p.parseProgram();
try std.testing.expectEqual(@as(u32, 1), prog.global_stmts.lines[0]); try std.testing.expectEqual(@as(usize, 2), prog.fn_decls[0].body.stmts.len); try std.testing.expectEqual(@as(u32, 3), prog.fn_decls[0].body.lines[0]); try std.testing.expectEqual(@as(u32, 4), prog.fn_decls[0].body.lines[1]);}