Something went wrong. Try again.
A general purpose .. programming language written in zig ijadux2.tngl.sh/Rinnescript
zig language
Something went wrong. Try again.
73 kB · 1664 lines
Zig
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665const std = @import("std");const lexer_mod = @import("lexer.zig");const TokenType = lexer_mod.TokenType;const parser_mod = @import("parser.zig");const Program = parser_mod.Program;const Stmt = parser_mod.Stmt;const Expr = parser_mod.Expr;const FnDecl = parser_mod.FnDecl;const value_mod = @import("value.zig");pub const Value = value_mod.Value;
/// Bytecode operations. Each instruction is one opcode byte followed by a/// fixed operand encoding (u32 little-endian and/or u8), keeping decode O(1).pub const Op = enum(u8) { constant, // +u32 const_idx null_const, true_const, false_const, pop,
get_local, // +u32 slot set_local, // +u32 slot (pops value)
def_global, // +u32 name_idx (pops value) get_global, // +u32 name_idx set_global, // +u32 name_idx (pops value)
array_lit, // +u32 count get_index, // pops idx, obj -> pushes obj[idx] set_index, // pops val, idx, obj
add, sub, mul, div, mod_op, neg, not_op,
eq, neq, lt, le, gt, ge, and_op, or_op,
jump, // +u32 abs target jump_if_false, // +u32 abs target (pops condition) loop, // +u32 abs target (backward)
call_fn, // +u32 fn_idx +u8 argc call_builtin, // +u32 name_idx +u8 argc call_method, // +u32 name_idx +u8 argc (receiver under args) tensor_simd, // +u32 kernel_fn_idx (receiver under nothing) push_defer, // +u32 fn_idx (deferred zero-arg fn) try_call, // +u32 fn_idx (guarded call; VM recovers runtime errors into [ok, result_or_msg])
ret, // returns top-of-stack (or null)};
pub const Chunk = struct { code: std.ArrayList(u8) = .empty, constants: std.ArrayList(Value) = .empty, /// One source line per code byte, for runtime diagnostics. lines: std.ArrayList(u32) = .empty,
pub fn deinit(self: *Chunk, allocator: std.mem.Allocator) void { self.code.deinit(allocator); self.constants.deinit(allocator); self.lines.deinit(allocator); }
pub fn lineAt(self: *const Chunk, ip: usize) u32 { if (self.lines.items.len == 0) return 0; const i = @min(ip, self.lines.items.len - 1); return self.lines.items[i]; }};
pub const FnProto = struct { name: []const u8, arity: usize, /// High-water mark of local slots; VM reserves this many stack slots per frame. locals_count: usize, chunk: Chunk, /// SIMD lane hint for apply_simd kernels (`@vector(N, T)` param). /// 0 = unspecified; the VM executes scalar regardless (interpreted /// fallback — real vectorization is the native backend's job). vector_width: usize = 0,};
/// A compiled RinneScript program: one implicit global chunk plus one chunk/// per function declaration. Ready for `vm.Vm.run`.pub const Module = struct { allocator: std.mem.Allocator, fns: []FnProto, main_idx: ?usize = null,
pub fn deinit(self: *Module) void { for (self.fns) |*fp| fp.chunk.deinit(self.allocator); self.allocator.free(self.fns); }};
pub const CompileError = error{ OutOfMemory, UnexpectedToken, DuplicateFunction, UnknownFunction, UnknownDirective, UnknownModule, NotImplementedInPhase, InvalidAssignmentTarget, BreakOutsideLoop, ContinueOutsideLoop, ArityMismatch, TooManyArguments, InvalidFormatCall, InvalidTensorSpec, InvalidSimdKernel, PackageFetch, InvalidTryTarget,};
/// Every builtin and its canonical, fully-qualified path. This is the single/// source of truth for the standard library surface: the compiler routes/// namespace calls against it, and it is exposed for the LSP (completions +/// snippets) and for introspection (docs). The runtime still dispatches by/// leaf name, so the VM's `callBuiltin` switch is unchanged.////// Builtins owned by a non-std module (c/ai/llm/net) must be called through/// that module's namespace (c.load_lib, net.tcp_listen, ...). Everything else/// lives under `std` and is grouped into a sub-namespace (std.io, std.vec,/// std.json, std.strings, std.collections, std.sha).pub const builtin_paths = [_]struct { name: []const u8, path: []const u8 }{ // std core .{ .name = "print", .path = "std.print" }, .{ .name = "log", .path = "std.log" }, .{ .name = "stderr", .path = "std.stderr" }, .{ .name = "args", .path = "std.args" }, .{ .name = "env", .path = "std.env" }, .{ .name = "clock", .path = "std.clock" }, // int/float/str/as are type-cast builtins. Because their names lex as // reserved type keywords (int, float, str), they cannot be written as // `std.<name>(...)` — they are reached only through their directive forms // (@int, @float, @str, @as). path[] is empty (no reachable call path). .{ .name = "int", .path = "@int" }, .{ .name = "float", .path = "@float" }, .{ .name = "str", .path = "@str" }, .{ .name = "as", .path = "@as" }, .{ .name = "assert", .path = "std.assert" }, .{ .name = "assert_eq", .path = "std.assert_eq" }, .{ .name = "panic", .path = "std.panic" }, .{ .name = "log_write", .path = "std.log_write" }, // std.io (stdout is a directive most of the time, but callable) .{ .name = "stdout", .path = "std.io.stdout" }, .{ .name = "read_file", .path = "std.io.read_file" }, .{ .name = "write_file", .path = "std.io.write_file" }, .{ .name = "file_exists", .path = "std.io.file_exists" }, .{ .name = "make_dir", .path = "std.io.make_dir" }, .{ .name = "make_dir_all", .path = "std.io.make_dir_all" }, .{ .name = "remove_file", .path = "std.io.remove_file" }, .{ .name = "remove_dir", .path = "std.io.remove_dir" }, .{ .name = "rename_file", .path = "std.io.rename_file" }, .{ .name = "append_file", .path = "std.io.append_file" }, .{ .name = "list_dir", .path = "std.io.list_dir" }, // std.strings .{ .name = "len", .path = "std.strings.len" }, .{ .name = "slice", .path = "std.strings.slice" }, .{ .name = "split", .path = "std.strings.split" }, .{ .name = "join", .path = "std.strings.join" }, .{ .name = "trim", .path = "std.strings.trim" }, .{ .name = "replace", .path = "std.strings.replace" }, .{ .name = "contains", .path = "std.strings.contains" }, .{ .name = "starts_with", .path = "std.strings.starts_with" }, .{ .name = "ends_with", .path = "std.strings.ends_with" }, .{ .name = "to_lower", .path = "std.strings.to_lower" }, .{ .name = "to_upper", .path = "std.strings.to_upper" }, .{ .name = "repeat", .path = "std.strings.repeat" }, .{ .name = "chars", .path = "std.strings.chars" }, .{ .name = "reverse", .path = "std.strings.reverse" }, .{ .name = "pad_start", .path = "std.strings.pad_start" }, .{ .name = "pad_end", .path = "std.strings.pad_end" }, .{ .name = "capitalize", .path = "std.strings.capitalize" }, .{ .name = "count", .path = "std.strings.count" }, .{ .name = "index_of", .path = "std.strings.index_of" }, .{ .name = "last_index_of", .path = "std.strings.last_index_of" }, .{ .name = "lines", .path = "std.strings.lines" }, // std.json / data formats .{ .name = "json_parse", .path = "std.json.json_parse" }, .{ .name = "json_stringify", .path = "std.json.json_stringify" }, .{ .name = "csv_parse", .path = "std.json.csv_parse" }, .{ .name = "csv_format", .path = "std.json.csv_format" }, .{ .name = "json_pretty", .path = "std.json.json_pretty" }, .{ .name = "base64_encode", .path = "std.json.base64_encode" }, .{ .name = "base64_decode", .path = "std.json.base64_decode" }, // std.vec (SIMD-style vector builtins; scalar loops on the VM) .{ .name = "vector", .path = "std.vec.vector" }, .{ .name = "splat", .path = "std.vec.splat" }, .{ .name = "reduce", .path = "std.vec.reduce" }, .{ .name = "select", .path = "std.vec.select" }, // std.collections .{ .name = "push", .path = "std.collections.push" }, .{ .name = "map_new", .path = "std.collections.map_new" }, .{ .name = "contains_val", .path = "std.collections.contains_val" }, .{ .name = "sort", .path = "std.collections.sort" }, .{ .name = "unique", .path = "std.collections.unique" }, .{ .name = "sum", .path = "std.collections.sum" }, .{ .name = "mean", .path = "std.collections.mean" }, .{ .name = "min_of", .path = "std.collections.min_of" }, .{ .name = "max_of", .path = "std.collections.max_of" }, .{ .name = "first", .path = "std.collections.first" }, .{ .name = "last", .path = "std.collections.last" }, .{ .name = "empty", .path = "std.collections.empty" }, // std.sha (hash digests; all return lowercase hex strings) .{ .name = "sha256", .path = "std.sha.sha256" }, .{ .name = "sha1", .path = "std.sha.sha1" }, .{ .name = "md5", .path = "std.sha.md5" }, .{ .name = "sha384", .path = "std.sha.sha384" }, .{ .name = "sha512", .path = "std.sha.sha512" }, // std.crypto (keyed/auth helpers + binary encoding) .{ .name = "hmac_sha256", .path = "std.crypto.hmac_sha256" }, .{ .name = "hex_encode", .path = "std.crypto.hex_encode" }, .{ .name = "hex_decode", .path = "std.crypto.hex_decode" }, // std.math (numeric helpers; all take numeric scalars) .{ .name = "abs", .path = "std.math.abs" }, .{ .name = "floor", .path = "std.math.floor" }, .{ .name = "ceil", .path = "std.math.ceil" }, .{ .name = "round", .path = "std.math.round" }, .{ .name = "sqrt", .path = "std.math.sqrt" }, .{ .name = "pow", .path = "std.math.pow" }, .{ .name = "min", .path = "std.math.min" }, .{ .name = "max", .path = "std.math.max" }, .{ .name = "clamp", .path = "std.math.clamp" }, .{ .name = "sign", .path = "std.math.sign" }, .{ .name = "trunc", .path = "std.math.trunc" }, .{ .name = "fract", .path = "std.math.fract" }, .{ .name = "exp", .path = "std.math.exp" }, .{ .name = "ln", .path = "std.math.ln" }, .{ .name = "log2", .path = "std.math.log2" }, .{ .name = "log10", .path = "std.math.log10" }, .{ .name = "deg_to_rad", .path = "std.math.deg_to_rad" }, .{ .name = "rad_to_deg", .path = "std.math.rad_to_deg" }, .{ .name = "is_even", .path = "std.math.is_even" }, .{ .name = "is_odd", .path = "std.math.is_odd" }, // std.os (interact with the host OS / time) .{ .name = "sleep", .path = "std.os.sleep" }, .{ .name = "wall_time_ms", .path = "std.os.wall_time_ms" }, .{ .name = "current_dir", .path = "std.os.current_dir" }, .{ .name = "home_dir", .path = "std.os.home_dir" }, .{ .name = "cpu_count", .path = "std.os.cpu_count" }, // std.random (entropy via the OS CSPRNG) .{ .name = "random_int", .path = "std.random.random_int" }, .{ .name = "random_float", .path = "std.random.random_float" }, .{ .name = "random_chance", .path = "std.random.random_chance" }, .{ .name = "random_pick", .path = "std.random.random_pick" }, .{ .name = "shuffle", .path = "std.random.shuffle" }, // std.test (assertion helpers used by the `rinne test` runner) .{ .name = "test_eq", .path = "std.test.test_eq" }, .{ .name = "test_ne", .path = "std.test.test_ne" }, .{ .name = "test_true", .path = "std.test.test_true" }, .{ .name = "test_false", .path = "std.test.test_false" }, // non-std module owners .{ .name = "load_safetensors", .path = "ai.load_safetensors" }, .{ .name = "load_gguf", .path = "ai.load_gguf" }, .{ .name = "llm_load", .path = "llm.llm_load" }, .{ .name = "tcp_listen", .path = "net.tcp_listen" }, .{ .name = "tcp_connect", .path = "net.tcp_connect" }, .{ .name = "load_lib", .path = "c.load_lib" },};
/// Library modules selectable via `@import = .{ std, ai, llm, net, c }`.pub const modules = [_][]const u8{ "std", "ai", "llm", "net", "c" };
pub fn moduleIndex(name: []const u8) ?usize { for (modules, 0..) |m, i| { if (std.mem.eql(u8, m, name)) return i; } return null;}
pub fn isBuiltin(name: []const u8) bool { for (builtin_paths) |b| { if (std.mem.eql(u8, b.name, name)) return true; } return false;}
/// The canonical fully-qualified path for a builtin (e.g. "std.io.read_file")./// Returns null when `name` is not a builtin.pub fn builtinPath(name: []const u8) ?[]const u8 { for (builtin_paths) |b| { if (std.mem.eql(u8, b.name, name)) return b.path; } return null;}
/// The top-level module that owns a builtin ("std", "c", "ai", "llm", "net")./// Directive-only cast builtins (@int/@float/@str/@as) count as std-owned so/// the module gates treat them like std: they need no @import.pub fn builtinOwner(name: []const u8) []const u8 { const p = builtinPath(name) orelse return "std"; const dot = std.mem.indexOfScalar(u8, p, '.') orelse return "std"; return p[0..dot];}
/// Builtins with the Zig-style signature `@name("fmt {}", .{args})`.pub fn isFmtBuiltin(name: []const u8) bool { const fmt_builtins = [_][]const u8{ "stdout", "stderr", "log", "print" }; for (fmt_builtins) |b| { if (std.mem.eql(u8, b, name)) return true; } return false;}
/// True when argument position `arg_i` of builtin `name` accepts a bare/// identifier as a compile-time token (type names, reduce ops).fn isComptimeArgBuiltin(name: []const u8, arg_i: usize) bool { if (std.mem.eql(u8, name, "vector")) return arg_i == 1; if (std.mem.eql(u8, name, "reduce")) return arg_i == 0; if (std.mem.eql(u8, name, "as")) return arg_i == 0; return false;}
/// Directives reserved by the language spec but implemented in later phases/// (see PIVOT.md). Reported as a friendly compile-time error.fn futurePhaseOf(directive: []const u8) ?u8 { // @vector/@splat/@reduce run on the VM (scalar fallback); real SIMD // codegen lands in Phase 3. @tensor and @fetch are implemented as of v1. const phase3 = [_][]const u8{ "import_c", "comptime", "build" }; for (phase3) |d| { if (std.mem.eql(u8, d, directive)) return 3; } return null;}
/// True for spellings that were renamed in the v1 ecosystem (@fetch_pkg -> @fetch).pub fn isRenamedFetchDirective(directive: []const u8) bool { return std.mem.eql(u8, directive, "fetch_pkg");}
const Local = struct { name: []const u8, depth: u32,};
const LoopCtx = struct { /// Byte offset of the loop condition (continue target). start: usize, /// Offsets of break-jump operands to patch once the loop ends. breaks: std.ArrayList(usize) = .empty, /// Offsets of continue jumps recorded for for-loops; patched to /// `cont_target` so `continue` still advances the iteration. continues: std.ArrayList(usize) = .empty, /// Increment address for for-loops; null for while loops (continue /// falls back to `start`). cont_target: ?usize = null,};
/// Per-function compilation state: local slots, scope nesting, open loops.const FnCtx = struct { compiler: *Compiler, fn_idx: usize, is_global: bool, locals: std.ArrayList(Local) = .empty, scope_depth: u32 = 0, loops: std.ArrayList(*LoopCtx) = .empty, /// Source line of the statement currently being compiled. current_line: u32 = 0,
fn chunk(fx: *FnCtx) *Chunk { return &fx.compiler.fns.items[fx.fn_idx].chunk; }
fn setLine(fx: *FnCtx, line: u32, col: u32) void { fx.current_line = line; fx.compiler.cur_line = line; fx.compiler.cur_col = col; }
// ---- Emission helpers ----
fn emitByte(fx: *FnCtx, byte: u8) CompileError!void { try fx.chunk().code.append(fx.compiler.allocator, byte); try fx.chunk().lines.append(fx.compiler.allocator, fx.current_line); }
fn emitOp(fx: *FnCtx, op: Op) void { // Bytecode chunks are tiny; treat allocation failure as fatal here to // keep every call site clean. fx.emitByte(@intFromEnum(op)) catch @panic("out of memory emitting bytecode"); }
fn emitU32(fx: *FnCtx, v: u32) CompileError!void { var buf: [4]u8 = undefined; std.mem.writeInt(u32, &buf, v, .little); for (buf) |b| try fx.emitByte(b); }
fn emitU8(fx: *FnCtx, v: u8) CompileError!void { try fx.emitByte(v); }
fn emitJump(fx: *FnCtx, op: Op) CompileError!usize { fx.emitOp(op); const operand_at = fx.chunk().code.items.len; try fx.emitU32(0); return operand_at; }
/// Points a previously emitted jump at the instruction that follows next. fn patchJump(fx: *FnCtx, operand_at: usize) void { const target: u32 = @intCast(fx.chunk().code.items.len); fx.patchJumpTo(operand_at, target); }
/// Points a previously emitted jump at an explicit byte offset. fn patchJumpTo(fx: *FnCtx, operand_at: usize, target: u32) void { const bytes = fx.chunk().code.items[operand_at..][0..4]; std.mem.writeInt(u32, bytes, target, .little); }
fn addConstant(fx: *FnCtx, v: Value) CompileError!u32 { const ch = fx.chunk(); try ch.constants.append(fx.compiler.allocator, v); return @intCast(ch.constants.items.len - 1); }
fn constIdxStr(fx: *FnCtx, s: []const u8) CompileError!u32 { return fx.addConstant(.{ .str_val = s }); }
fn emitConstant(fx: *FnCtx, v: Value) CompileError!void { const idx = try fx.addConstant(v); fx.emitOp(.constant); try fx.emitU32(idx); }
// ---- Scope handling ----
fn beginScope(fx: *FnCtx) void { fx.scope_depth += 1; }
fn endScope(fx: *FnCtx) void { fx.scope_depth -= 1; while (fx.locals.items.len > 0 and fx.locals.items[fx.locals.items.len - 1].depth > fx.scope_depth) { _ = fx.locals.pop(); } }
fn resolveLocal(fx: *FnCtx, name: []const u8) ?u32 { var i = fx.locals.items.len; while (i > 0) { i -= 1; if (std.mem.eql(u8, fx.locals.items[i].name, name)) { return @intCast(i); } } return null; }};
pub const Compiler = struct { allocator: std.mem.Allocator, fns: std.ArrayList(FnProto) = .empty, fn_names: std.StringHashMapUnmanaged(usize) = .empty, defer_counter: usize = 0, for_counter: usize = 0, err_msg: ?[]u8 = null, /// Source line of the statement being compiled (for diagnostics). cur_line: u32 = 0, /// Source column of the statement being compiled (for diagnostics). cur_col: u32 = 0, /// Diagnostics position (mirrors parser.zig's err_line/err_col). err_line: usize = 0, err_col: usize = 0, /// Which library modules the program imported (indexed by moduleIndex). imports: [modules.len]bool = .{false} ** modules.len,
pub fn init(allocator: std.mem.Allocator) Compiler { var c = Compiler{ .allocator = allocator }; // std is the default standard library: always available. Only the // non-std modules (c/ai/llm/net) require an explicit @import. if (moduleIndex("std")) |i| c.imports[i] = true; return c; }
pub fn deinit(self: *Compiler) void { for (self.fns.items) |*fp| fp.chunk.deinit(self.allocator); self.fns.deinit(self.allocator); self.fn_names.deinit(self.allocator); if (self.err_msg) |m| self.allocator.free(m); }
fn fail(self: *Compiler, e: CompileError, comptime fmt: []const u8, args: anytype) CompileError { // Structured position for tooling (LSP); mirrors parser.zig err_line/err_col. self.err_line = self.cur_line; self.err_col = self.cur_col; if (self.err_msg == null) { if (self.cur_line != 0) { self.err_msg = std.fmt.allocPrint(self.allocator, fmt ++ " (at line {d})", args ++ .{self.cur_line}) catch null; } else { self.err_msg = std.fmt.allocPrint(self.allocator, fmt, args) catch null; } } return e; }
/// Compiles a parsed program into an executable Module. /// Module memory is tied to the compiler's allocator; release everything /// by calling `compiler.deinit()`. pub fn compileProgram(self: *Compiler, program: *const Program) CompileError!Module { // fns[0] is the implicit global initialization chunk. try self.fns.append(self.allocator, .{ .name = "<global>", .arity = 0, .locals_count = 0, .chunk = .{} });
// Pass 1: pre-register every top-level fn so forward calls resolve. // Arity is recorded HERE, not when bodies compile, so callers declared // before their callees arity-check correctly (mutual recursion). for (program.fn_decls) |*fd| { const idx = try self.registerFn(fd.name); self.fns.items[idx].arity = fd.params.len; } // Module-level directives: future-phase features fail loudly here. // Imports name runtime namespaces (not a module linker): each entry // must be a known module, then `std.print(...)`-style calls route to // builtins via the existing field-access mechanism. for (program.directives) |*d| { if (d.* == .generic) { // Renamed directive guidance (see PIVOT.md ecosystem notes). if (isRenamedFetchDirective(d.generic.name)) { return self.fail(error.UnknownDirective, "'@fetch_pkg' was renamed to '@fetch'", .{}); } // `@fetch` at module level is consumed by the loader between // parse and compile; reaching here means no loader ran. if (std.mem.eql(u8, d.generic.name, "fetch")) continue; if (futurePhaseOf(d.generic.name)) |phase| { return self.fail(error.NotImplementedInPhase, "@{s} is planned for Phase {d} (see PIVOT.md)", .{ d.generic.name, phase }); } } if (d.* == .import_directive) { const mi = moduleIndex(d.import_directive) orelse return self.fail(error.UnknownModule, "unknown module '{s}' in @import (known modules: std, ai, llm, net, c)", .{d.import_directive}); self.imports[mi] = true; } } // Pass 2: compile bodies. for (program.fn_decls) |*fd| { const idx = self.fn_names.get(fd.name).?; try self.compileFnBody(idx, fd); } // Global statements. var gtx = FnCtx{ .compiler = self, .fn_idx = 0, .is_global = true }; defer { gtx.locals.deinit(self.allocator); gtx.loops.deinit(self.allocator); } for (program.global_stmts.stmts, program.global_stmts.lines, program.global_stmts.cols) |*stmt, line, col| { gtx.setLine(line, col); try self.compileStmt(>x, stmt); } gtx.emitOp(.ret);
var main_idx: ?usize = null; if (self.fn_names.get("main")) |mi| main_idx = mi;
return .{ .allocator = self.allocator, .fns = try self.fns.toOwnedSlice(self.allocator), .main_idx = main_idx, }; }
fn registerFn(self: *Compiler, name: []const u8) CompileError!usize { if (self.fn_names.get(name)) |_| { return self.fail(error.DuplicateFunction, "duplicate function '{s}'", .{name}); } const idx = self.fns.items.len; try self.fns.append(self.allocator, .{ .name = name, .arity = 0, .locals_count = 0, .chunk = .{} }); try self.fn_names.put(self.allocator, name, idx); return idx; }
fn compileFnBody(self: *Compiler, fn_idx: usize, fd: *const FnDecl) CompileError!void { var ftx = FnCtx{ .compiler = self, .fn_idx = fn_idx, .is_global = false }; defer { ftx.locals.deinit(self.allocator); ftx.loops.deinit(self.allocator); } self.fns.items[fn_idx].arity = fd.params.len; for (fd.params) |param| { // Params occupy slots 0..arity-1; the VM places arguments there. try ftx.locals.append(self.allocator, .{ .name = param.name, .depth = 0 }); } self.fns.items[fn_idx].locals_count = ftx.locals.items.len;
for (fd.body.stmts, fd.body.lines, fd.body.cols) |*stmt, line, col| { ftx.setLine(line, col); try self.compileStmt(&ftx, stmt); } ftx.emitOp(.ret); }
// ---- Statements ----
fn compileStmt(self: *Compiler, fx: *FnCtx, stmt: *const Stmt) CompileError!void { switch (stmt.*) { .var_decl => |decl| { try self.compileExpr(fx, &decl.initializer); if (fx.is_global) { const name_idx = try fx.constIdxStr(decl.name); fx.emitOp(.def_global); try fx.emitU32(name_idx); } else { // Slots are pre-reserved by the VM at frame entry // (nulled); store the initializer into the newly claimed // slot rather than relying on stack position. const slot: u32 = @intCast(fx.locals.items.len); fx.emitOp(.set_local); try fx.emitU32(slot); try fx.locals.append(self.allocator, .{ .name = decl.name, .depth = fx.scope_depth }); self.fns.items[fx.fn_idx].locals_count = @max(self.fns.items[fx.fn_idx].locals_count, fx.locals.items.len); } }, .assignment => |assign| { switch (assign.target) { .identifier => |name| { try self.compileExpr(fx, &assign.value); if (!fx.is_global) { if (fx.resolveLocal(name)) |slot| { fx.emitOp(.set_local); try fx.emitU32(slot); return; } } const name_idx = try fx.constIdxStr(name); fx.emitOp(.set_global); try fx.emitU32(name_idx); }, .index_access => |ia| { try self.compileExpr(fx, ia.object); try self.compileExpr(fx, ia.index); try self.compileExpr(fx, &assign.value); fx.emitOp(.set_index); }, else => return error.InvalidAssignmentTarget, } }, .expr_stmt => |*expr| { try self.compileExpr(fx, expr); fx.emitOp(.pop); }, .if_stmt => |*if_s| { try self.compileExpr(fx, &if_s.condition); const jelse = try fx.emitJump(.jump_if_false); fx.beginScope(); for (if_s.then_branch.stmts, if_s.then_branch.lines, if_s.then_branch.cols) |*s, line, col| { fx.setLine(line, col); try self.compileStmt(fx, s); } fx.endScope(); if (if_s.else_branch) |eb| { const jend = try fx.emitJump(.jump); fx.patchJump(jelse); fx.beginScope(); for (eb.stmts, eb.lines, eb.cols) |*s, line, col| { fx.setLine(line, col); try self.compileStmt(fx, s); } fx.endScope(); fx.patchJump(jend); } else { fx.patchJump(jelse); } }, .while_stmt => |*w| { var ctx = LoopCtx{ .start = fx.chunk().code.items.len }; try self.compileExpr(fx, &w.condition); const jexit = try fx.emitJump(.jump_if_false);
fx.beginScope(); try fx.loops.append(self.allocator, &ctx); for (w.body.stmts, w.body.lines, w.body.cols) |*s, line, col| { fx.setLine(line, col); try self.compileStmt(fx, s); } _ = fx.loops.pop(); fx.endScope();
fx.emitOp(.loop); try fx.emitU32(@intCast(ctx.start)); fx.patchJump(jexit); for (ctx.breaks.items) |b| fx.patchJump(b); for (ctx.continues.items) |c| fx.patchJumpTo(c, @intCast(ctx.cont_target orelse ctx.start)); ctx.breaks.deinit(self.allocator); ctx.continues.deinit(self.allocator); }, .for_stmt => |*f| { // Desugar: arr/i/n hidden storage, element copy into the user // variable each iteration; `continue` jumps to the increment. // Global scope uses globals (the global chunk has no locals); // function scope uses locals. if (!fx.is_global) { try self.compileExpr(fx, &f.iter); const arr_slot: u32 = @intCast(fx.locals.items.len); try fx.locals.append(self.allocator, .{ .name = "__for_arr", .depth = fx.scope_depth }); fx.emitOp(.set_local); try fx.emitU32(arr_slot);
try fx.emitConstant(.{ .int_val = 0 }); const i_slot: u32 = @intCast(fx.locals.items.len); try fx.locals.append(self.allocator, .{ .name = "__for_i", .depth = fx.scope_depth }); fx.emitOp(.set_local); try fx.emitU32(i_slot);
fx.emitOp(.get_local); try fx.emitU32(arr_slot); const len_name = try fx.constIdxStr("len"); fx.emitOp(.call_builtin); try fx.emitU32(len_name); try fx.emitU8(1); const n_slot: u32 = @intCast(fx.locals.items.len); try fx.locals.append(self.allocator, .{ .name = "__for_n", .depth = fx.scope_depth }); fx.emitOp(.set_local); try fx.emitU32(n_slot);
if (fx.locals.items.len > self.fns.items[fx.fn_idx].locals_count) { self.fns.items[fx.fn_idx].locals_count = fx.locals.items.len; }
const loop_start = fx.chunk().code.items.len; fx.emitOp(.get_local); try fx.emitU32(i_slot); fx.emitOp(.get_local); try fx.emitU32(n_slot); fx.emitOp(.lt); const jexit = try fx.emitJump(.jump_if_false);
fx.emitOp(.get_local); try fx.emitU32(arr_slot); fx.emitOp(.get_local); try fx.emitU32(i_slot); fx.emitOp(.get_index); const x_slot: u32 = @intCast(fx.locals.items.len); try fx.locals.append(self.allocator, .{ .name = f.var_name, .depth = fx.scope_depth }); fx.emitOp(.set_local); try fx.emitU32(x_slot);
var ctx = LoopCtx{ .start = loop_start }; // Non-null marks this as a for-loop for `continue` handling; // the real increment address is patched in below. ctx.cont_target = 0; fx.beginScope(); try fx.loops.append(self.allocator, &ctx); for (f.body.stmts, f.body.lines, f.body.cols) |*s2, line, col| { fx.setLine(line, col); try self.compileStmt(fx, s2); } _ = fx.loops.pop(); fx.endScope();
// Increment; continue lands here. ctx.cont_target = fx.chunk().code.items.len; fx.emitOp(.get_local); try fx.emitU32(i_slot); try fx.emitConstant(.{ .int_val = 1 }); fx.emitOp(.add); fx.emitOp(.set_local); try fx.emitU32(i_slot);
fx.emitOp(.loop); try fx.emitU32(@intCast(loop_start)); fx.patchJump(jexit); for (ctx.breaks.items) |b| fx.patchJump(b); for (ctx.continues.items) |c| fx.patchJumpTo(c, @intCast(ctx.cont_target orelse ctx.start)); ctx.breaks.deinit(self.allocator); ctx.continues.deinit(self.allocator);
if (fx.locals.items.len > self.fns.items[fx.fn_idx].locals_count) { self.fns.items[fx.fn_idx].locals_count = fx.locals.items.len; } } else { self.for_counter += 1; const fc = self.for_counter; const arr_g = try fx.constIdxStr(try std.fmt.allocPrint(self.allocator, "__for_{d}_arr", .{fc})); const i_g = try fx.constIdxStr(try std.fmt.allocPrint(self.allocator, "__for_{d}_i", .{fc})); const n_g = try fx.constIdxStr(try std.fmt.allocPrint(self.allocator, "__for_{d}_n", .{fc})); const x_g = try fx.constIdxStr(f.var_name);
try self.compileExpr(fx, &f.iter); fx.emitOp(.def_global); try fx.emitU32(arr_g);
try fx.emitConstant(.{ .int_val = 0 }); fx.emitOp(.def_global); try fx.emitU32(i_g);
fx.emitOp(.get_global); try fx.emitU32(arr_g); const len_name = try fx.constIdxStr("len"); fx.emitOp(.call_builtin); try fx.emitU32(len_name); try fx.emitU8(1); fx.emitOp(.def_global); try fx.emitU32(n_g);
const loop_start = fx.chunk().code.items.len; fx.emitOp(.get_global); try fx.emitU32(i_g); fx.emitOp(.get_global); try fx.emitU32(n_g); fx.emitOp(.lt); const jexit = try fx.emitJump(.jump_if_false);
fx.emitOp(.get_global); try fx.emitU32(arr_g); fx.emitOp(.get_global); try fx.emitU32(i_g); fx.emitOp(.get_index); fx.emitOp(.def_global); try fx.emitU32(x_g);
var gctx = LoopCtx{ .start = loop_start }; gctx.cont_target = 0; try fx.loops.append(self.allocator, &gctx); for (f.body.stmts, f.body.lines, f.body.cols) |*s2, line, col| { fx.setLine(line, col); try self.compileStmt(fx, s2); } _ = fx.loops.pop();
gctx.cont_target = fx.chunk().code.items.len; fx.emitOp(.get_global); try fx.emitU32(i_g); try fx.emitConstant(.{ .int_val = 1 }); fx.emitOp(.add); fx.emitOp(.def_global); try fx.emitU32(i_g);
fx.emitOp(.loop); try fx.emitU32(@intCast(loop_start)); fx.patchJump(jexit); for (gctx.breaks.items) |b| fx.patchJump(b); for (gctx.continues.items) |c| fx.patchJumpTo(c, @intCast(gctx.cont_target orelse gctx.start)); gctx.breaks.deinit(self.allocator); gctx.continues.deinit(self.allocator); } }, .range_for => |*r| { // Desugar `for i in lo..hi` / `..=hi` into a counting loop: // i = lo; while (i < limit) { body; i = i + 1 } // where limit = hi (exclusive) or hi + 1 (inclusive). if (!fx.is_global) { try self.compileExpr(fx, &r.lo); const i_slot: u32 = @intCast(fx.locals.items.len); try fx.locals.append(self.allocator, .{ .name = r.var_name, .depth = fx.scope_depth }); fx.emitOp(.set_local); try fx.emitU32(i_slot);
try self.compileExpr(fx, &r.hi); if (r.hi_inclusive) { try fx.emitConstant(.{ .int_val = 1 }); fx.emitOp(.add); } const hi_slot: u32 = @intCast(fx.locals.items.len); try fx.locals.append(self.allocator, .{ .name = "__range_hi", .depth = fx.scope_depth }); fx.emitOp(.set_local); try fx.emitU32(hi_slot);
if (fx.locals.items.len > self.fns.items[fx.fn_idx].locals_count) { self.fns.items[fx.fn_idx].locals_count = fx.locals.items.len; }
const loop_start = fx.chunk().code.items.len; fx.emitOp(.get_local); try fx.emitU32(i_slot); fx.emitOp(.get_local); try fx.emitU32(hi_slot); fx.emitOp(.lt); const jexit = try fx.emitJump(.jump_if_false);
var ctx = LoopCtx{ .start = loop_start, .cont_target = 0 }; fx.beginScope(); try fx.loops.append(self.allocator, &ctx); for (r.body.stmts, r.body.lines, r.body.cols) |*s2, line, col| { fx.setLine(line, col); try self.compileStmt(fx, s2); } _ = fx.loops.pop(); fx.endScope();
// Increment; continue lands here. ctx.cont_target = fx.chunk().code.items.len; fx.emitOp(.get_local); try fx.emitU32(i_slot); try fx.emitConstant(.{ .int_val = 1 }); fx.emitOp(.add); fx.emitOp(.set_local); try fx.emitU32(i_slot);
fx.emitOp(.loop); try fx.emitU32(@intCast(loop_start)); fx.patchJump(jexit); for (ctx.breaks.items) |b| fx.patchJump(b); for (ctx.continues.items) |c| fx.patchJumpTo(c, @intCast(ctx.cont_target orelse ctx.start)); ctx.breaks.deinit(self.allocator); ctx.continues.deinit(self.allocator);
if (fx.locals.items.len > self.fns.items[fx.fn_idx].locals_count) { self.fns.items[fx.fn_idx].locals_count = fx.locals.items.len; } } else { const i_g = try fx.constIdxStr(r.var_name); const hi_g = try fx.constIdxStr(try std.fmt.allocPrint(self.allocator, "__rng_{d}_hi", .{self.for_counter}));
try self.compileExpr(fx, &r.lo); fx.emitOp(.def_global); try fx.emitU32(i_g);
try self.compileExpr(fx, &r.hi); if (r.hi_inclusive) { try fx.emitConstant(.{ .int_val = 1 }); fx.emitOp(.add); } fx.emitOp(.def_global); try fx.emitU32(hi_g);
const loop_start = fx.chunk().code.items.len; fx.emitOp(.get_global); try fx.emitU32(i_g); fx.emitOp(.get_global); try fx.emitU32(hi_g); fx.emitOp(.lt); const jexit = try fx.emitJump(.jump_if_false);
var gctx = LoopCtx{ .start = loop_start, .cont_target = 0 }; try fx.loops.append(self.allocator, &gctx); for (r.body.stmts, r.body.lines, r.body.cols) |*s2, line, col| { fx.setLine(line, col); try self.compileStmt(fx, s2); } _ = fx.loops.pop();
gctx.cont_target = fx.chunk().code.items.len; fx.emitOp(.get_global); try fx.emitU32(i_g); try fx.emitConstant(.{ .int_val = 1 }); fx.emitOp(.add); fx.emitOp(.def_global); try fx.emitU32(i_g);
fx.emitOp(.loop); try fx.emitU32(@intCast(loop_start)); fx.patchJump(jexit); for (gctx.breaks.items) |b| fx.patchJump(b); for (gctx.continues.items) |c| fx.patchJumpTo(c, @intCast(gctx.cont_target orelse gctx.start)); gctx.breaks.deinit(self.allocator); gctx.continues.deinit(self.allocator); } }, .break_stmt => { const ctx = fx.loops.getLastOrNull() orelse return error.BreakOutsideLoop; const j = try fx.emitJump(.jump); ctx.breaks.append(self.allocator, j) catch return error.OutOfMemory; }, .continue_stmt => { const ctx = fx.loops.getLastOrNull() orelse return error.ContinueOutsideLoop; if (ctx.cont_target != null) { // for-loop: jump to the increment so iteration advances. const j = try fx.emitJump(.jump); ctx.continues.append(self.allocator, j) catch return error.OutOfMemory; } else { fx.emitOp(.loop); try fx.emitU32(@intCast(ctx.start)); } }, .return_stmt => |maybe_expr| { if (maybe_expr) |*expr| { try self.compileExpr(fx, expr); } else { fx.emitOp(.null_const); } fx.emitOp(.ret); }, .defer_stmt => |*expr| { // Compile the deferred expression as a hidden zero-argument // function; the VM runs pending defers LIFO at frame return. const hidden_name = std.fmt.allocPrint(self.allocator, "<defer.{d}>", .{self.defer_counter}) catch return error.OutOfMemory; self.defer_counter += 1; const idx = self.fns.items.len; try self.fns.append(self.allocator, .{ .name = hidden_name, .arity = 0, .locals_count = 0, .chunk = .{} });
var dtx = FnCtx{ .compiler = self, .fn_idx = idx, .is_global = false }; defer { dtx.locals.deinit(self.allocator); dtx.loops.deinit(self.allocator); } try self.compileExpr(&dtx, expr); dtx.emitOp(.pop); dtx.emitOp(.ret);
fx.emitOp(.push_defer); try fx.emitU32(@intCast(idx)); }, .fn_decl => |*fd| { // Hoisted: compiled into its own chunk immediately. const idx = try self.registerFn(fd.name); try self.compileFnBody(idx, fd); }, } }
// ---- Expressions ----
fn compileExpr(self: *Compiler, fx: *FnCtx, expr: *const Expr) CompileError!void { switch (expr.*) { .int_lit => |v| try fx.emitConstant(.{ .int_val = v }), .float_lit => |v| try fx.emitConstant(.{ .float_val = v }), .string_lit => |s| try fx.emitConstant(.{ .str_val = s }), .bool_lit => |v| fx.emitOp(if (v) Op.true_const else Op.false_const), .null_lit => fx.emitOp(.null_const), .identifier => |name| { if (!fx.is_global) { if (fx.resolveLocal(name)) |slot| { fx.emitOp(.get_local); try fx.emitU32(slot); return; } } const name_idx = try fx.constIdxStr(name); fx.emitOp(.get_global); try fx.emitU32(name_idx); }, .array_lit => |items| { for (items) |*item| try self.compileExpr(fx, item); fx.emitOp(.array_lit); try fx.emitU32(@intCast(items.len)); }, .struct_lit => |fields| { // Empty `.{}` is the classic empty tuple (format-arg lists). if (fields.len == 0) { fx.emitOp(.array_lit); try fx.emitU32(0); return; } // `.{ .key = value, ... }` builds a string-keyed map. The // colon form `{key: value}` carries the key in `key_expr`; // the dot form carries it in `name`. for (fields) |*f| { if (f.key_expr) |*key| { try self.compileExpr(fx, key); } else { const kidx = try fx.constIdxStr(f.name); fx.emitOp(.constant); try fx.emitU32(kidx); } try self.compileExpr(fx, &f.value); } fx.emitOp(.call_builtin); const nidx = try fx.constIdxStr("map_new"); try fx.emitU32(nidx); try fx.emitU8(@intCast(fields.len * 2)); }, .binary => |bin| { try self.compileExpr(fx, bin.lhs); try self.compileExpr(fx, bin.rhs); const op: Op = switch (bin.op) { .plus => .add, .minus => .sub, .star => .mul, .slash => .div, .percent => .mod_op, .equal_equal => .eq, .not_equal => .neq, .less => .lt, .less_equal => .le, .greater => .gt, .greater_equal => .ge, .kw_and => .and_op, .kw_or => .or_op, else => return self.fail(error.UnexpectedToken, "unsupported binary operator", .{}), }; fx.emitOp(op); }, .unary => |un| { try self.compileExpr(fx, un.operand); fx.emitOp(switch (un.op) { .minus => .neg, else => .not_op, }); }, .index_access => |ia| { try self.compileExpr(fx, ia.object); try self.compileExpr(fx, ia.index); fx.emitOp(.get_index); }, .field_access => |fa| { // Record fields arrive with struct support (Phase 2). try self.compileExpr(fx, fa.object); fx.emitOp(.pop); fx.emitOp(.null_const); }, .call => |c| { // `try(fn(args))`: the callee name is not a keyword, so the // parser produces a plain call; intercept it here so the // inner call is NOT evaluated eagerly. if (c.callee.* == .identifier and std.mem.eql(u8, c.callee.identifier, "try")) { // compileTry validates the [call] shape and raises // InvalidTryTarget for malformed uses like try(42). return self.compileTry(fx, c.args); } try self.compileCall(fx, c.callee, c.args); }, .call_directive => |cd| { // try(CALL): compile the call into a hidden chunk and run it // under the VM's error-recovery boundary. if (std.mem.eql(u8, cd.name, "try")) { return self.compileTry(fx, cd.args); } if (std.mem.eql(u8, cd.name, "tensor")) { try self.compileTensorNew(fx, cd.name, cd.args); return; } if (isBuiltin(cd.name)) { try self.requireBuiltinModule(cd.name); if (isFmtBuiltin(cd.name)) { const ok = cd.args.len == 2 and cd.args[0] == .string_lit and cd.args[1] == .array_lit; if (!ok) { return self.fail(error.InvalidFormatCall, "@{s} expects a format string and tuple: @{s}(\"x = {{}}\\n\", .{{x}})", .{ cd.name, cd.name }); } } const name_idx = try fx.constIdxStr(cd.name); for (cd.args, 0..) |*arg, arg_i| { // Comptime-style arguments arrive as bare identifiers // that name types or ops: `@vector(16, f32)`, // `@reduce(add, xs)`. Pass them through as strings. if (arg.* == .identifier and isComptimeArgBuiltin(cd.name, arg_i)) { const idx = try fx.constIdxStr(arg.*.identifier); fx.emitOp(.constant); try fx.emitU32(idx); continue; } try self.compileExpr(fx, arg); } if (cd.args.len > 255) return error.TooManyArguments; fx.emitOp(.call_builtin); try fx.emitU32(name_idx); try fx.emitU8(@intCast(cd.args.len)); return; } if (isRenamedFetchDirective(cd.name)) { return self.fail(error.UnknownDirective, "'@fetch_pkg' was renamed to '@fetch'", .{}); } if (std.mem.eql(u8, cd.name, "fetch")) { return self.fail(error.UnknownDirective, "'@fetch' may only be used at module level (top of file)", .{}); } if (futurePhaseOf(cd.name)) |phase| { return self.fail(error.NotImplementedInPhase, "@{s} is planned for Phase {d} (see PIVOT.md)", .{ cd.name, phase }); } return self.fail(error.UnknownDirective, "unknown directive '@{s}'", .{cd.name}); }, .enum_lit => |n| { return self.fail(error.UnknownDirective, "enum literal '.{s}' is only valid inside @tensor option structs", .{n}); }, .fn_lit => { return self.fail(error.InvalidSimdKernel, "anonymous fn literals are only allowed as the kernel of apply_simd: t.apply_simd(fn(x: f32) f32 {{ ... }})", .{}); }, } }
/// Lowers `@tensor(dtype, shape, options?)` into a positional /// `tensor_new` builtin call: /// tensor_new(dtype_str, shape_arr [, device_str] [, fill_value]) /// Options are a struct literal: `.{ .device = .cpu, .fill = 0 }`. fn compileTensorNew(self: *Compiler, fx: *FnCtx, name: []const u8, args: []const Expr) CompileError!void { if (args.len < 2 or args.len > 3) { return self.fail(error.InvalidTensorSpec, "@{s} expects (dtype, shape, options?), e.g. @tensor(f32, [2, 3], .{{.device = .cpu}})", .{name}); } // dtype: bare type-name identifier (f32, i32, ...) passed as string. const dtype_name = switch (args[0]) { .identifier => |id| id, else => return self.fail(error.InvalidTensorSpec, "@{s} dtype must be a bare type name (f32, f64, i32, i64, u8, bool)", .{name}), }; var argc: u8 = 2; var device_idx: ?u32 = null; var has_fill = false; if (args.len == 3) { switch (args[2]) { .struct_lit => |fields| { for (fields) |*field| { if (std.mem.eql(u8, field.name, "device")) { switch (field.value) { .enum_lit => |n| device_idx = try fx.constIdxStr(n), .identifier => |id| device_idx = try fx.constIdxStr(id), else => return self.fail(error.InvalidTensorSpec, "@{s} .device must be an enum literal like .cpu", .{name}), } } else if (std.mem.eql(u8, field.name, "fill")) { has_fill = true; } else { return self.fail(error.InvalidTensorSpec, "@{s} option '.{s}' is unknown (known: .device, .fill)", .{ name, field.name }); } } if (device_idx != null) argc += 1; if (has_fill) { if (device_idx == null) { // fill without device: keep ordering by emitting a // default device first. The injected device also // occupies a stack slot. device_idx = try fx.constIdxStr("cpu"); argc += 1; } argc += 1; } }, else => return self.fail(error.InvalidTensorSpec, "@{s} options must be a struct literal: .{{.device = .cpu, .fill = 0}}", .{name}), } }
// Emit: dtype_str, shape, [device_str], [fill]. const name_idx = try fx.constIdxStr("tensor_new"); const dtype_idx = try fx.constIdxStr(dtype_name); fx.emitOp(.constant); try fx.emitU32(dtype_idx); try self.compileExpr(fx, &args[1]); if (device_idx) |di| { fx.emitOp(.constant); try fx.emitU32(di); } if (has_fill) { for (args[2].struct_lit) |*field| { if (std.mem.eql(u8, field.name, "fill")) { try self.compileExpr(fx, &field.value); } } } fx.emitOp(.call_builtin); try fx.emitU32(name_idx); try fx.emitU8(argc); }
/// Module namespaces route `ns.name(...)` to the builtin `name` — the /// pre-existing rinne.log back-compat mechanism, now also used for /// std.print/std.ai-style surfaces. const namespaces = [_][]const u8{ "rinne", "std", "ai", "llm", "net", "c" };
/// Bare calls/dlures of non-std builtins require their module to be /// imported: load_lib needs c, llm_load needs llm, etc. (`std` builtins /// are always importable via std, which is the default namespace.) fn requireBuiltinModule(self: *Compiler, name: []const u8) CompileError!void { const owner = builtinOwner(name); if (std.mem.eql(u8, owner, "std")) return; const mi = moduleIndex(owner) orelse return; if (!self.imports[mi]) { return self.fail(error.UnknownFunction, "builtin '{s}' requires the '{s}' module — add '@import = .{{ {s} }};' at the top of the file", .{ name, owner, owner }); } }
fn isNamespace(name: []const u8) bool { for (namespaces) |ns| { if (std.mem.eql(u8, ns, name)) return true; } return false; }
/// Resolves a namespaced builtin call like `std.io.read_file(...)` or /// `c.load_lib(...)` to the runtime builtin's leaf name. Returns null when /// the receiver is a runtime object (not a static namespace) — the caller /// then falls through to method dispatch. On a static namespace that does /// not match the builtin's canonical path, this fails with a precise hint. fn resolveBuiltinCall(self: *Compiler, fa_object: *const Expr, leaf: []const u8, args: []const Expr) CompileError!?[]const u8 { _ = args; if (!isBuiltin(leaf)) return null;
// Collect the dotted receiver segments: std.io.read_file -> std, io. var segs: [4][]const u8 = undefined; var n: usize = 0; var cur: ?*const Expr = fa_object; var seen_non_flat = false; while (cur) |node| { switch (node.*) { .identifier => |ident| { if (n >= segs.len) return null; segs[n] = ident; n += 1; break; }, .field_access => |inner| { if (n >= segs.len) return null; segs[n] = inner.field; n += 1; cur = inner.object; }, else => { seen_non_flat = true; break; }, } } if (seen_non_flat or n == 0 or n > segs.len) return null;
// Reverse: we collected leaf-wards first (io, std), so flip to // root-wards (std, io) for comparison against the canonical path. std.mem.reverse([]const u8, segs[0..n]);
// A receiver whose root is not a static namespace (libc.call_int, m.keys) // is a runtime object, not a namespace — method dispatch. if (!isNamespace(segs[0])) return null;
const owner = builtinOwner(leaf); const canon = builtinPath(leaf) orelse unreachable; const canon_ns = canon[0 .. std.mem.lastIndexOfScalar(u8, canon, '.') orelse 0];
// Build the used prefix we can compare: e.g. ["std","io"] -> "std.io", // ["std"] -> "std". const used_ns = joinDots(self.allocator, segs[0..n]) catch return error.OutOfMemory;
// rinne.log(...) back-compat: always route. if (std.mem.eql(u8, segs[0], "rinne")) { try self.requireBuiltinModule(leaf); return leaf; }
// Accept when the used receiver is the builtin's canonical namespace // (std.io) or its owning module (std), so both std.io.read_file and // std.read_file resolve. if (std.mem.eql(u8, used_ns, canon_ns) or std.mem.eql(u8, used_ns, owner)) { // Non-std owners must be imported (std is implicit). if (!std.mem.eql(u8, owner, "std")) { const mi = moduleIndex(owner) orelse unreachable; if (!self.imports[mi]) { return self.fail(error.UnknownFunction, "module '{s}' is not imported (add '@import = .{{ {s} }};' at the top of the file)", .{ owner, owner }); } } return leaf; }
// A static std/owner namespace that carries this builtin's name but is // not the canonical path (std.load_lib, std.io.json_parse) — hint. if (std.mem.eql(u8, segs[0], owner) or std.mem.eql(u8, segs[0], "std")) { return self.fail(error.UnknownFunction, "builtin '{s}' is '{s}' — use {s}(...)", .{ leaf, canon, canon }); }
// Some other namespace guessed at this builtin's name. return self.fail(error.UnknownFunction, "builtin '{s}' is '{s}', not '{s}.{s}'", .{ leaf, canon, used_ns, leaf }); }
fn joinDots(alloc: std.mem.Allocator, segs: []const []const u8) ![]const u8 { var out: std.ArrayList(u8) = .empty; defer out.deinit(alloc); for (segs, 0..) |s, i| { if (i > 0) try out.append(alloc, '.'); try out.appendSlice(alloc, s); } return out.toOwnedSlice(alloc); }
/// Compiles `obj.method(arg1, ...)`: object first, then args, then a /// `call_method` opcode carrying the method-name constant. fn compileMethodCall(self: *Compiler, fx: *FnCtx, obj: *const Expr, method_name: []const u8, args: []const Expr) CompileError!void { if (args.len > 255) return error.TooManyArguments; if (std.mem.eql(u8, method_name, "apply_simd")) { return self.compileApplySimd(fx, obj, args); } try self.compileExpr(fx, obj); for (args) |*arg| try self.compileExpr(fx, arg); const name_idx = try fx.constIdxStr(method_name); fx.emitOp(.call_method); try fx.emitU32(name_idx); try fx.emitU8(@intCast(args.len)); }
/// Lowers `t.apply_simd(fn(x) { ... })` into a hidden one-parameter /// kernel chunk plus `tensor_simd kernel_idx`. The fn literal is NOT a /// runtime value — it never touches the operand stack. fn compileApplySimd(self: *Compiler, fx: *FnCtx, obj: *const Expr, args: []const Expr) CompileError!void { if (args.len != 1 or args[0] != .fn_lit) { return self.fail(error.InvalidSimdKernel, "apply_simd expects exactly one inline fn literal kernel: t.apply_simd(fn(x: f32) f32 {{ return x * x; }})", .{}); } const fl = args[0].fn_lit; if (fl.params.len != 1) { return self.fail(error.InvalidSimdKernel, "apply_simd kernel must take exactly one element parameter, got {d}", .{fl.params.len}); }
const hidden_name = std.fmt.allocPrint(self.allocator, "<simd.{d}>", .{self.defer_counter}) catch return error.OutOfMemory; self.defer_counter += 1; const idx = self.fns.items.len; try self.fns.append(self.allocator, .{ .name = hidden_name, .arity = 1, .locals_count = 0, .chunk = .{} });
// Reuse the named-fn body compiler via a synthetic declaration. const tmp_decl = FnDecl{ .is_pub = false, .name = hidden_name, .params = fl.params, .return_type = .{ .primitive = .inferred }, .decorators = &.{}, .body = fl.body, }; try self.compileFnBody(idx, &tmp_decl); self.fns.items[idx].vector_width = @intCast(fl.vector_width);
try self.compileExpr(fx, obj); fx.emitOp(.tensor_simd); try fx.emitU32(@intCast(idx)); }
/// try(CALL_EXPR): lowers to a hidden zero-arg chunk evaluating the call, /// executed by Op.try_call under runtime-error recovery. The result is an /// array [ok: bool, result_or_message]. fn compileTry(self: *Compiler, fx: *FnCtx, args: []const Expr) CompileError!void { if (args.len != 1 or args[0] != .call) { return self.fail(error.InvalidTryTarget, "try expects exactly one function call: try(fn(args))", .{}); } const hidden_name = std.fmt.allocPrint(self.allocator, "<try.{d}>", .{self.defer_counter}) catch return error.OutOfMemory; self.defer_counter += 1; const idx = self.fns.items.len; try self.fns.append(self.allocator, .{ .name = hidden_name, .arity = 0, .locals_count = 0, .chunk = .{} });
var ttx = FnCtx{ .compiler = self, .fn_idx = idx, .is_global = false }; defer { ttx.locals.deinit(self.allocator); ttx.loops.deinit(self.allocator); } // Route through compileExpr so NESTED try(...) calls re-enter this // special form instead of resolving 'try' as an unknown function. try self.compileExpr(&ttx, &args[0]); ttx.emitOp(.ret);
fx.emitOp(.try_call); try fx.emitU32(@intCast(idx)); }
fn compileCall(self: *Compiler, fx: *FnCtx, callee: *const Expr, args: []const Expr) CompileError!void { if (args.len > 255) return error.TooManyArguments;
// Resolve a simple name or namespace-style callee to a concrete // compile-time target. var target_name: ?[]const u8 = null; var is_user_fn = false;
switch (callee.*) { .identifier => |name| { // A declared user function shadows a same-named builtin; // otherwise a bare builtin must be reached via its canonical // path (std.io.read_file, c.load_lib, ...) after Phase 2. if (self.fn_names.get(name) != null) { target_name = name; is_user_fn = true; } else if (isBuiltin(name)) { const canon = builtinPath(name) orelse unreachable; if (canon.len > 0 and canon[0] == '@') { return self.fail(error.UnknownFunction, "builtin '{s}' is a directive — use {s}(...)", .{ name, canon }); } return self.fail(error.UnknownFunction, "builtin '{s}' must be called through its namespace — use {s}(...)", .{ name, canon }); } else { target_name = name; is_user_fn = true; } }, .field_access => |fa| { const def = try self.resolveBuiltinCall(fa.object, fa.field, args); if (def) |d| { target_name = d; is_user_fn = false; } else { // Not a static-namespace builtin call: runtime method // dispatch (tensor.apply_simd, libc.call_int, m.keys()). return self.compileMethodCall(fx, fa.object, fa.field, args); } }, else => return self.fail(error.UnknownFunction, "only named functions are callable in Phase 1", .{}), }
const name = target_name.?;
if (is_user_fn) { if (!fx.is_global and fx.resolveLocal(name) != null) { return self.fail(error.UnknownFunction, "'{s}' is shadowed by a local variable and not callable", .{name}); } if (self.fn_names.get(name)) |fn_idx| { const arity = self.fns.items[fn_idx].arity; if (arity != args.len) { return self.fail(error.ArityMismatch, "'{s}' expects {d} argument(s), got {d}", .{ name, arity, args.len }); } for (args) |*arg| try self.compileExpr(fx, arg); fx.emitOp(.call_fn); try fx.emitU32(@intCast(fn_idx)); try fx.emitU8(@intCast(args.len)); return; } }
if (isBuiltin(name)) { try self.requireBuiltinModule(name); const name_idx = try fx.constIdxStr(name); for (args, 0..) |*arg, arg_i| { if (arg.* == .identifier and isComptimeArgBuiltin(name, arg_i)) { const idx = try fx.constIdxStr(arg.*.identifier); fx.emitOp(.constant); try fx.emitU32(idx); continue; } try self.compileExpr(fx, arg); } fx.emitOp(.call_builtin); try fx.emitU32(name_idx); try fx.emitU8(@intCast(args.len)); return; }
return self.fail(error.UnknownFunction, "unknown function '{s}'", .{name}); }};
// ---- Tests ----
test "compile fibonacci program" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator();
var p = parser_mod.Parser.init(arena, \\fn fib(n: int) int { \\ if n < 2 { return n; } \\ return fib(n - 1) + fib(n - 2); \\} \\ \\pub fn main(args: []str) int { \\ @stdout("{}\n", .{fib(20)}); \\ return 0; \\} ); const prog = try p.parseProgram();
var compiler = Compiler.init(arena); const module = try compiler.compileProgram(&prog);
try std.testing.expect(module.main_idx != null); try std.testing.expect(module.fns.len >= 3);}
test "compile rejects arity mismatch" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator();
var p = parser_mod.Parser.init(arena, \\fn add(a: int, b: int) int { return a + b; } \\add(1); ); const prog = try p.parseProgram();
var compiler = Compiler.init(arena); try std.testing.expectError(error.ArityMismatch, compiler.compileProgram(&prog));}
test "compiler records structured error position" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator();
// `return add(1)` is on line 6; arity check fails while compiling it, // so the recorded statement position must be line 6, column 5 (1-based). var p = parser_mod.Parser.init(arena, \\fn add(a: int, b: int) int { \\ return a + b \\} \\ \\pub fn main() int { \\ return add(1) \\} ); const prog = try p.parseProgram();
// Statement position plumbing: the block records line AND column arrays. try std.testing.expectEqual(@as(u32, 6), prog.fn_decls[1].body.lines[0]); try std.testing.expectEqual(@as(u32, 5), prog.fn_decls[1].body.cols[0]);
var compiler = Compiler.init(arena); try std.testing.expectError(error.ArityMismatch, compiler.compileProgram(&prog)); try std.testing.expectEqual(@as(usize, 6), compiler.err_line); try std.testing.expectEqual(@as(usize, 5), compiler.err_col); // CLI message keeps the human-readable suffix. try std.testing.expect(compiler.err_msg != null); try std.testing.expect(std.mem.indexOf(u8, compiler.err_msg.?, "(at line 6)") != null);}
test "compile rejects future-phase directive with helpful error" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator();
var p = parser_mod.Parser.init(arena, \\fn f() { @comptime(); } ); const prog = try p.parseProgram();
var compiler = Compiler.init(arena); try std.testing.expectError(error.NotImplementedInPhase, compiler.compileProgram(&prog)); try std.testing.expect(compiler.err_msg != null); try std.testing.expect(std.mem.indexOf(u8, compiler.err_msg.?, "Phase 3") != null);}
test "body-level @fetch is rejected with module-level guidance" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator();
var p = parser_mod.Parser.init(arena, \\fn f() { @fetch("https://example.com/lib.rg"); } ); const prog = try p.parseProgram();
var compiler = Compiler.init(arena); try std.testing.expectError(error.UnknownDirective, compiler.compileProgram(&prog)); try std.testing.expect(std.mem.indexOf(u8, compiler.err_msg.?, "module level") != null);}
test "@fetch_pkg rename guidance" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator();
var p = parser_mod.Parser.init(arena, \\@fetch_pkg("https://example.com/lib.rg", "1.0.0"); \\fn main() int { return 0; } ); const prog = try p.parseProgram();
var compiler = Compiler.init(arena); try std.testing.expectError(error.UnknownDirective, compiler.compileProgram(&prog)); try std.testing.expect(std.mem.indexOf(u8, compiler.err_msg.?, "renamed to '@fetch'") != null);}
test "builtin must be called through its owning module's namespace" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator();
// c.load_lib(...) compiles once its owner module is imported. var ok_p = parser_mod.Parser.init(arena, \\@import = .{ c }; \\fn main() int { const x = c.load_lib("libc.so.6"); _ = x; return 0; } ); const ok_prog = try ok_p.parseProgram(); var ok_c = Compiler.init(arena); _ = ok_c.compileProgram(&ok_prog) catch |e| return e; try std.testing.expect(ok_c.err_msg == null);
// std.load_lib(...) is rejected even when c is imported: load_lib is c's. var bad_p = parser_mod.Parser.init(arena, \\@import = .{ std, c }; \\fn main() int { const x = std.load_lib("libc.so.6"); _ = x; return 0; } ); const bad_prog = try bad_p.parseProgram(); var bad_c = Compiler.init(arena); try std.testing.expectError(error.UnknownFunction, bad_c.compileProgram(&bad_prog)); try std.testing.expect(std.mem.indexOf(u8, bad_c.err_msg.?, "is 'c.load_lib'") != null); try std.testing.expect(std.mem.indexOf(u8, bad_c.err_msg.?, "c.load_lib") != null);}