Something went wrong. Try again.
A general purpose .. programming language written in zig ijadux2.tngl.sh/Rinnescript
zig language
Something went wrong. Try again.
47 kB · 1141 lines
Zig
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142const std = @import("std");const builtin = @import("builtin");const Io = std.Io;const rinne = @import("rinne");const lsp_mod = @import("lsp.zig");const rinne_mod = @import("rinne");
const version = "0.1.0-phase1";
const ColorMode = enum { auto, always, never };var g_color: ColorMode = .auto;var g_quiet = false;/// Auto mode colors only when TERM suggests a capable terminal (non-empty and/// not "dumb"), so piped/logged output stays clean without a syscall.var g_auto_color = false;
/// Emit `body`, colored with `code` (SGR) only when color is enabled./// Enabled on `always`, suppressed on `never`, and on `auto` per `g_auto_color`.fn ansi(w: *Io.Writer, body: []const u8, code: []const u8) void { if (colorEnabled()) { w.writeAll(code) catch {}; w.writeAll(body) catch {}; w.writeAll("\x1b[0m") catch {}; } else { w.writeAll(body) catch {}; }}
fn colorEnabled() bool { return switch (g_color) { .always => true, .never => false, .auto => g_auto_color, };}
/// Flushes both writers before terminating; `std.process.exit` skips defers,/// so buffered diagnostics would otherwise be lost.fn flushExit(out: *Io.Writer, errw: *Io.Writer, code: u8) noreturn { out.flush() catch {}; errw.flush() catch {}; std.process.exit(code);}
pub fn main(init: std.process.Init) !void { var stdout_buffer: [4096]u8 = undefined; var stdout_file_writer: Io.File.Writer = .init(.stdout(), init.io, &stdout_buffer); const out = &stdout_file_writer.interface;
var stderr_buffer: [1024]u8 = undefined; var stderr_file_writer: Io.File.Writer = .init(.stderr(), init.io, &stderr_buffer); const errw = &stderr_file_writer.interface;
const arena = init.arena.allocator(); const raw_args = try init.minimal.args.toSlice(arena);
if (raw_args.len < 2) { usageTop(out); flushExit(out, errw, 0); }
if (init.environ_map.get("TERM")) |t| { g_auto_color = t.len != 0 and !std.mem.eql(u8, t, "dumb"); }
// Fold the global `--quiet`/`-q` and `--color <mode>` options into module // state and strip them from the argument list so `args[1]` is always the // command and `args[2..]` are its operands, regardless of where the global // options appear. var args_list: std.ArrayList([]const u8) = .empty; const args = blk: { args_list.append(arena, raw_args[0]) catch return error.OutOfMemory; var i: usize = 1; while (i < raw_args.len) : (i += 1) { const a = raw_args[i]; if (std.mem.eql(u8, a, "--quiet") or std.mem.eql(u8, a, "-q")) { g_quiet = true; continue; } if (std.mem.eql(u8, a, "--color")) { if (i + 1 < raw_args.len) { const m = raw_args[i + 1]; i += 1; if (std.mem.eql(u8, m, "always")) { g_color = .always; } else if (std.mem.eql(u8, m, "never")) { g_color = .never; } else { g_color = .auto; } } continue; } args_list.append(arena, a) catch return error.OutOfMemory; } break :blk args_list.items; }; const cmd = args[1];
// ---- help: `rinne help`, `rinne --help`, and per-topic `rinne help <cmd>` ---- if (std.mem.eql(u8, cmd, "help") or std.mem.eql(u8, cmd, "--help") or std.mem.eql(u8, cmd, "-h")) { if (args.len >= 3) { const topic = args[2]; if (std.mem.eql(u8, topic, "run")) { usageRun(out); } else if (std.mem.eql(u8, topic, "build")) { usageBuild(out); } else if (std.mem.eql(u8, topic, "test")) { usageTest(out); } else if (std.mem.eql(u8, topic, "version")) { usageVersion(out); } else if (std.mem.eql(u8, topic, "help")) { usageTop(out); } else { errw.print("error: unknown help topic '{s}'\n\nTopics: run, build, test, fmt, lint, lsp, version, help\n\n", .{topic}) catch {}; usageTop(errw); flushExit(out, errw, 64); } } else { usageTop(out); } flushExit(out, errw, 0); }
// ---- version ---- if (std.mem.eql(u8, cmd, "version") or std.mem.eql(u8, cmd, "--version") or std.mem.eql(u8, cmd, "-V")) { printVersion(out); flushExit(out, errw, 0); }
// ---- lsp: language server over stdio (takes over both streams) ---- if (std.mem.eql(u8, cmd, "lsp")) { try lsp_mod.run(arena); flushExit(out, errw, 0); }
if (std.mem.eql(u8, cmd, "fmt")) { var check = false; var path: ?[]const u8 = null; for (args[2..]) |a| { if (std.mem.eql(u8, a, "--check") or std.mem.eql(u8, a, "-c")) { check = true; } else if (!std.mem.startsWith(u8, a, "-")) { path = a; } } const p = path orelse { errw.print("error: fmt needs a file: rinne fmt [--check] FILE\n", .{}) catch {}; flushExit(errw, out, 64); }; const rc = try rinne_mod.fmt.run(arena, init.io, errw, check, p); flushExit(out, errw, rc); }
if (std.mem.eql(u8, cmd, "lint")) { if (args.len < 3) { errw.print("error: lint needs at least one file: rinne lint FILE...\n", .{}) catch {}; flushExit(errw, out, 64); } const rc = try rinne_mod.lint.run(arena, init.io, errw, args[2..]); flushExit(out, errw, rc); }
// ---- check: parse + compile a file without running it ---- if (std.mem.eql(u8, cmd, "check")) { if (args.len < 3) { errw.print("error: check needs at least one file: rinne check FILE...\n", .{}) catch {}; flushExit(errw, out, 64); } var quiet = false; var paths: std.ArrayList([]const u8) = .empty; defer paths.deinit(arena); for (args[2..]) |a| { if (std.mem.eql(u8, a, "-q") or std.mem.eql(u8, a, "--quiet")) { quiet = true; } else { paths.append(arena, a) catch return error.OutOfMemory; } } var failed = false; for (paths.items) |p| { if (checkFile(init, errw, arena, p)) |_| { if (!quiet and !g_quiet) out.print("OK {s}\n", .{p}) catch {}; } else |e| { if (e != error.CompileFailed) reportFailure(e, errw); failed = true; } } flushExit(out, errw, if (failed) 70 else 0); }
// ---- count: lightweight source-metrics for one or more files ---- if (std.mem.eql(u8, cmd, "count")) { if (args.len < 3) { errw.print("error: count needs at least one file: rinne count FILE...\n", .{}) catch {}; flushExit(errw, out, 64); } var total_lines: usize = 0; var total_code: usize = 0; var total_comment: usize = 0; var total_fns: usize = 0; var any_failed = false; for (args[2..]) |p| { const m = countFile(init, arena, p) catch |e| { reportFailure(e, errw); any_failed = true; continue; }; out.print("{d:>6} lines, {d:>6} code, {d:>6} comments, {d:>3} fns {s}\n", .{ m.lines, m.code_lines, m.comment_lines, m.fns, p, }) catch {}; total_lines += m.lines; total_code += m.code_lines; total_comment += m.comment_lines; total_fns += m.fns; } if (args.len > 3 and !any_failed) { out.print("{d:>6} lines, {d:>6} code, {d:>6} comments, {d:>3} fns TOTAL\n", .{ total_lines, total_code, total_comment, total_fns, }) catch {}; } flushExit(out, errw, if (any_failed) 70 else 0); }
// ---- completions: shell completion script ---- if (std.mem.eql(u8, cmd, "completions") or std.mem.eql(u8, cmd, "completion")) { var shell: []const u8 = "bash"; if (args.len >= 3) shell = args[2]; printCompletions(out, shell); flushExit(out, errw, 0); }
// ---- init: scaffold a new project ---- if (std.mem.eql(u8, cmd, "init")) { if (args.len < 3) { errw.print("error: init needs a project name: rinne init <name>\n", .{}) catch {}; flushExit(errw, out, 64); } const rc = initProject(out, errw, init.io, arena, args[2]) catch |e| { reportFailure(e, errw); flushExit(out, errw, 70); }; flushExit(out, errw, rc); }
// ---- repl: interactive read-eval-print loop ---- if (std.mem.eql(u8, cmd, "repl")) { const rc = repl(init, out, errw, args[2..]); flushExit(out, errw, rc); }
if (std.mem.eql(u8, cmd, "run")) { if (args.len >= 3 and (std.mem.eql(u8, args[2], "-h") or std.mem.eql(u8, args[2], "--help"))) { usageRun(out); flushExit(out, errw, 0); } if (args.len < 3) { errw.writeAll("error: 'rinne run' requires a script path\n\n") catch {}; usageRun(errw); flushExit(out, errw, 64); } const exit_code = runFile(init, out, errw, args[2], args[3..]) catch |e| { // Stage-level diagnostics were already reported; only add a // generic line for unexpected failures. if (e != error.CompileFailed and e != error.RuntimeFailed) { reportFailure(e, errw); } flushExit(out, errw, 70); }; flushExit(out, errw, exit_code); } else if (std.mem.eql(u8, cmd, "build")) { if (args.len >= 3 and (std.mem.eql(u8, args[2], "-h") or std.mem.eql(u8, args[2], "--help"))) { usageBuild(out); flushExit(out, errw, 0); } if (args.len < 3) { errw.writeAll("error: 'rinne build' requires a script path\n\n") catch {}; usageBuild(errw); flushExit(out, errw, 64); } const exit_code = buildFile(init, out, errw, args[2..]) catch |e| { if (e != error.CompileFailed and e != error.BuildFailed) reportFailure(e, errw); flushExit(out, errw, 70); }; flushExit(out, errw, exit_code); }
// ---- test: run `tests/**/*.rg` (or a single file) with std.test asserts ---- if (std.mem.eql(u8, cmd, "test")) { if (args.len >= 3 and (std.mem.eql(u8, args[2], "-h") or std.mem.eql(u8, args[2], "--help"))) { usageTest(out); flushExit(out, errw, 0); } const target = if (args.len >= 3) args[2] else "tests"; const exit_code = testFiles(init, out, errw, target) catch |e| { reportFailure(e, errw); flushExit(out, errw, 70); }; flushExit(out, errw, exit_code); }
errw.print( "error: unknown command '{s}'\n\nTry 'rinne --help' for usage.\n", .{cmd}, ) catch {}; flushExit(out, errw, 64);}
// ---- help screens ----
/// Top-level help menu.fn usageTop(w: *Io.Writer) void { w.print( \\RinneScript {s} — general-purpose systems & AI language \\ \\Usage: \\ rinne <command> [options] <file.rg> [args...] \\ \\Commands: \\ run Execute a script on the bytecode VM (<10 ms startup) \\ build Compile a script to a native binary via the C11 backend \\ test Run the test suite (tests/**/*.rg) and report results \\ check Parse + compile a file without running it (CI-friendly) \\ count Print source metrics (lines / code / comments / fns) \\ init Scaffold a new project directory \\ fmt Format a script in place \\ lint Static analysis of a script \\ repl Interactive read-eval-print loop \\ completions Emit a shell completion script (bash|zsh|fish) \\ version Print version information \\ help Show this help, or detailed help for one command \\ \\Global options: \\ -h, --help Show help (also per command: `rinne run --help`) \\ -V, --version Print version information \\ --quiet Suppress non-essential output \\ --color <mode> Color output: auto|always|never (default auto) \\ \\Examples: \\ rinne run examples/hello.rg Prototype instantly on the VM \\ rinne run script.rg arg1 arg2 Extra args reach main(args) \\ rinne build app.rg Emit C11, invoke cc -> ./app \\ rinne build app.rg -O3 -o app-fast Tuned native build \\ rinne check app.rg Validate without executing \\ rinne init myapp && rinne run myapp/main.rg \\ rinne completions bash > /etc/bash_completion.d/rinne \\ \\Exit codes: \\ 0 success (or main()'s int return when a script runs) \\ 64 usage error \\ 70 parse / compile / runtime / build failure \\ \\Docs: \\ README.md Language tour, builtins, roadmap \\ docs/man/rinne.1 Man page (`man docs/man/rinne.1`) \\ rinne run --help Runtime details & builtin list \\ rinne build --help Native-build subset & flags \\ , .{version}) catch {};}
/// `rinne run --help`.fn usageRun(w: *Io.Writer) void { w.writeAll( \\rinne run — execute a script on the bytecode VM \\ \\Usage: \\ rinne run <script.rg> [args...] \\ \\Behavior: \\ * Parse -> bytecode compile -> execute in-process, instantly. \\ * Everything after the script path is passed through to \\ main(args: []str) untouched — including leading dashes. \\ * The process exits with main()'s int return value \\ (bool/null-returning mains use 0/1 conventions). \\ \\Entry point: \\ pub fn main(args: []str) int // conventional, optional \\ \\Runtime builtins (`std` is implicit; `c`/`ai`/`llm`/`net` need importing): \\ strings std.strings.{len, slice, split, join, trim, replace, contains, \\ starts_with, ends_with, to_lower, to_upper, repeat, chars, reverse} \\ math std.math.{abs, floor, ceil, round, sqrt, pow, min, max, clamp} \\ io std.io.{stdout, read_file, write_file, append_file, file_exists, \\ make_dir, make_dir_all, remove_file, remove_dir, rename_file, list_dir} \\ json std.json.{json_parse, json_stringify, json_pretty, csv_parse, \\ csv_format, base64_encode, base64_decode} \\ collections/std.vec push, map_new, contains_val; vector, splat, reduce, select \\ system std.env(name), std.clock(), std.args \\ crypto std.sha.sha256(text) \\ type casts (directives only) @int @float @str @as "@int(v)" \\ output @stdout / std.print / std.log / std.stderr ("fmt {}", .{vals}) \\ \\Format strings use {} placeholders consumed left-to-right from the \\tuple; {{ }} escape literal braces. Mismatches are runtime errors; \\malformed shapes are rejected at compile time. \\ \\See README.md for the language tour and examples/ for runnable code. \\ ) catch {};}
/// `rinne test --help`.fn usageTest(w: *Io.Writer) void { w.writeAll( \\rinne test — run the test suite \\ \\Usage: \\ rinne test [path] \\ \\Behavior: \\ * With no argument, every *.rg file under tests/ (recursively) is run. \\ * A file argument runs just that script; a directory argument acts \\ like the default but rooted there. \\ * Assertions use the std.test builtins; failures never abort the \\ script, so one file keeps going and reports every failing check. \\ * Any parse / compile / runtime error, failing assertion, or \\ non-zero main() return marks the file as failed. \\ \\Examples: \\ rinne test Run tests/ \\ rinne test tests/math_test.rg Run a single file \\ rinne test --help This screen \\ \\Test file: \\ pub fn main() int { return value ignored; see below \\ std.test.test_eq(2 + 2, 4); pass…… record + continue \\ std.test.test_true(3 > 2); \\ std.test.test_ne(1, 2); \\ std.test.test_false(false); \\ return 0; \\ } \\ \\Exit codes: \\ 0 every file passed (all assertions held) \\ 70 any file failed (assertion, error, or non-zero return) \\ ) catch {};}
/// `rinne build --help`.fn usageBuild(w: *Io.Writer) void { w.writeAll( \\rinne build — native compilation \\ \\Usage: \\ rinne build <entry.rg> [-o <path>] [-O0|-O1|-O2|-O3] \\ [--native [--no-libc]] [-l <lib>] [-L <dir>] [--shared] \\ \\Options: \\ -o <path> Output binary path (default: <name>.rgx) \\ -O0..-O3 Optimization level forwarded to cc (default -O2) \\ --native Emit x86-64 for a Rinne function directly, bypassing the \\ C11 pipeline. Assembles + links via the system linker. \\ --no-libc (with --native) emit a freestanding static ELF using raw \\ syscalls; no libc and no linker needed. \\ -l <lib> Link a library (passed through to the linker, e.g. -lm). \\ -L <dir> Add a library search path. \\ --shared Build a shared object (with --native). \\ \\Behavior: \\ * Default emits dependency-free C11 to <name>.c next to the input \\ (kept for inspection), then invokes the system cc driver to link it. \\ * --native (linked) links against libc and any -l/-L libs; the result \\ is a dynamic executable (satisfies "link with libc and other libs"). \\ * cc's own diagnostics stream straight to your terminal. \\ \\Supported native subset today: \\ fn main() int with return <int expr> and @stdout("literal", .{}). \\ \\Native entry point must be parameterless: fn main() int \\ \\Requires cc on PATH (gcc or clang). If absent, the .s/.c file is kept \\and the manual compile command is printed. \\ ) catch {};}
/// `rinne help version` / `rinne version --help`.fn usageVersion(w: *Io.Writer) void { w.writeAll( \\rinne version — print version information \\ \\Usage: \\ rinne version \\ \\Prints the RinneScript release string plus the target architecture, \\operating system, and Zig compiler version used for this binary. \\ \\Flags -V and --version are accepted anywhere as aliases. \\ ) catch {};}
/// One-line version banner with target triple info.fn printVersion(w: *Io.Writer) void { w.print( "rinne {s} ({s}-{s}, zig {d}.{d}.{d})\n", .{ version, @tagName(builtin.cpu.arch), @tagName(builtin.os.tag), builtin.zig_version.major, builtin.zig_version.minor, builtin.zig_version.patch, }, ) catch {};}
fn readFile(init: std.process.Init, alloc: std.mem.Allocator, path: []const u8) ![]u8 { return Io.Dir.cwd().readFileAlloc(init.io, path, alloc, .limited(256 * 1024 * 1024));}
/// Full scripting-mode pipeline for one file: parse -> compile -> execute.fn runFile(init: std.process.Init, out: *Io.Writer, errw: *Io.Writer, path: []const u8, cli_args: []const []const u8) !u8 { const arena = init.arena.allocator(); const source = try readFile(init, arena, path); return runSource(init, out, errw, arena, source, cli_args);}
fn reportStage(stage: []const u8, e: anyerror, detail: ?[]const u8, errw: *Io.Writer) void { errw.print("{s} error: ", .{stage}) catch {}; ansi(errw, detail orelse @errorName(e), "\x1b[31;1m"); errw.writeAll("\n") catch {};}
fn reportFailure(e: anyerror, errw: *Io.Writer) void { errw.print("error: ", .{}) catch {}; ansi(errw, @errorName(e), "\x1b[31;1m"); errw.writeAll("\n") catch {};}
/// Native-build pipeline: parse -> emit C11 -> invoke system cc.////// Flags: `-o <path>` overrides the output binary; `-O0..-O3` are forwarded/// to cc (default -O2). The intermediate `.c` file is written next to the/// input and removed after a successful build; it is kept only on failure/// (or if the system `cc` is absent) for manual compilation/inspection.fn buildFile(init: std.process.Init, out: *Io.Writer, errw: *Io.Writer, argv: []const []const u8) !u8 { const arena = init.arena.allocator();
var src_path: ?[]const u8 = null; var out_path: ?[]const u8 = null; var opt_level: []const u8 = "2"; var native = false; var no_libc = false; var link_flags: std.ArrayList([]const u8) = .empty; defer link_flags.deinit(arena); var i: usize = 0; while (i < argv.len) : (i += 1) { const arg = argv[i]; if (std.mem.eql(u8, arg, "-o")) { // `-o` consumes the next token as the output path. if (i + 1 < argv.len) { out_path = argv[i + 1]; i += 1; } } else if (std.mem.eql(u8, arg, "-l") or std.mem.eql(u8, arg, "-L")) { // Two-token form: `-l foo` / `-L /path`. Join onto the flag. if (i + 1 < argv.len) { const joined = std.fmt.allocPrint(arena, "{s}{s}", .{ arg, argv[i + 1] }) catch return error.OutOfMemory; link_flags.append(arena, joined) catch return error.OutOfMemory; i += 1; } } else if (std.mem.eql(u8, arg, "--native")) { native = true; } else if (std.mem.eql(u8, arg, "--no-libc")) { no_libc = true; } else if (std.mem.eql(u8, arg, "-O0") or std.mem.eql(u8, arg, "-O1") or std.mem.eql(u8, arg, "-O2") or std.mem.eql(u8, arg, "-O3")) { opt_level = arg[2..]; } else { // Linker pass-through single-token form: -lm, -lfoo, -L/path, // -Wl,..., --shared. const is_link = std.mem.startsWith(u8, arg, "-l") or std.mem.startsWith(u8, arg, "-L") or std.mem.startsWith(u8, arg, "-Wl") or std.mem.eql(u8, arg, "--shared"); if (is_link) { link_flags.append(arena, arg) catch return error.OutOfMemory; } else if (src_path == null) { src_path = arg; } else if (out_path == null) { out_path = arg; } else { errw.print("error: unexpected argument '{s}'\n", .{arg}) catch {}; return error.BuildFailed; } } } const path = src_path orelse { errw.writeAll("error: 'rinne build' requires a script path\n\n") catch {}; usageBuild(errw); return error.BuildFailed; };
const source = try readFile(init, arena, path);
var parser = rinne.parser.Parser.init(arena, source); defer parser.deinit(); var program = parser.parseProgram() catch |e| { reportStage("parse", e, parser.err_msg, errw); return error.CompileFailed; };
// Resolve module-level @fetch directives before compiling/building. { var pkg_opts = rinne.loader.Options{}; { const m = init.environ_map; pkg_opts.home_dir = m.get("HOME"); if (m.get("RINNE_CACHE_HOME")) |c| pkg_opts.cache_dir_override = c; if (m.get("RINNE_OFFLINE")) |o| pkg_opts.offline = std.mem.eql(u8, o, "1"); } var pkg_detail: ?[]u8 = null; rinne.loader.resolveFetches(arena, init.io, &program, pkg_opts, &pkg_detail) catch |e| { reportStage("package", e, pkg_detail orelse @errorName(e), errw); return error.CompileFailed; }; }
// Native (no-libc ELF) mode: try the `.rgx` backend directly. It rejects// code outside its subset with a clear message; fall back to C11 otherwise.const stem0 = if (std.mem.endsWith(u8, path, ".rg")) path[0 .. path.len - 3] else path;const default_bin = try std.fmt.allocPrint(arena, "{s}.rgx", .{stem0});const bin_path0 = out_path orelse default_bin;
if (native) { var rgx_err: std.ArrayList(u8) = .empty; defer rgx_err.deinit(arena); const eb = rinne.rgx.ErrBuf{ .alloc = arena, .out = &rgx_err };
if (no_libc) { // Freestanding static ELF: raw syscalls, no libc, no linker. const blob = rinne.rgx.lower(arena, &program, eb) catch |e| switch (e) { error.UnsupportedRinne => null, else => { errw.print("error: native build failed ({s})\n", .{@errorName(e)}) catch {}; return error.BuildFailed; }, }; if (blob) |b| { Io.Dir.cwd().writeFile(init.io, .{ .sub_path = bin_path0, .data = b }) catch |e| { errw.print("error: cannot write {s}: {s}\n", .{ bin_path0, @errorName(e) }) catch {}; return error.BuildFailed; }; const z = arena.dupeZ(u8, bin_path0) catch return error.BuildFailed; _ = std.os.linux.chmod(z.ptr, 0o755); out.print("built {s} (native, --no-libc)\n", .{bin_path0}) catch {}; return 0; } errw.writeAll("native build: ") catch {}; errw.writeAll(rgx_err.items) catch {}; errw.writeAll("falling back to the C11 pipeline\n") catch {}; } else { // Linked native build: emit assembly, assemble+link against libc and // the user's `-l`/`-L` libs via the system linker. Falls back to the // freestanding ELF, then to C11, if the source leaves the subset. const asm_src = rinne.rgx.lowerAssembly(arena, &program, eb) catch |e| switch (e) { error.UnsupportedRinne => null, else => { errw.print("error: native build failed ({s})\n", .{@errorName(e)}) catch {}; return error.BuildFailed; }, }; if (asm_src) |a| { const s_path = try std.fmt.allocPrint(arena, "{s}.s", .{stem0}); Io.Dir.cwd().writeFile(init.io, .{ .sub_path = s_path, .data = a }) catch |e| { errw.print("error: cannot write {s}: {s}\n", .{ s_path, @errorName(e) }) catch {}; return error.BuildFailed; }; var lk_argv: std.ArrayList([]const u8) = .empty; defer lk_argv.deinit(arena); lk_argv.appendSlice(arena, &.{ "cc", "-nostartfiles", try std.fmt.allocPrint(arena, "-O{s}", .{opt_level}), s_path, "-o", bin_path0 }) catch return error.OutOfMemory; for (link_flags.items) |f| lk_argv.append(arena, f) catch return error.OutOfMemory; var child = std.process.spawn(init.io, .{ .argv = lk_argv.items, .stdout = .inherit, .stderr = .inherit, }) catch { out.print("wrote {s}; system 'cc' not found — assemble manually:\n cc -nostartfiles {s} -o {s}\n", .{ s_path, s_path, bin_path0 }) catch {}; return 0; }; const term = child.wait(init.io) catch |e| { errw.print("error: cc failed: {s}\n", .{@errorName(e)}) catch {}; return error.BuildFailed; }; switch (term) { .exited => |code| { if (code == 0) { removeIntermediate(init.io, s_path); out.print("built {s} (from {s}, cc -O{s}, linked)\n", .{ bin_path0, s_path, opt_level }) catch {}; return 0; } }, else => {}, } return error.BuildFailed; } // Assembly route unsupported: fall back to freestanding ELF, then C11. const blob = rinne.rgx.lower(arena, &program, eb) catch |e| switch (e) { error.UnsupportedRinne => null, else => { errw.print("error: native build failed ({s})\n", .{@errorName(e)}) catch {}; return error.BuildFailed; }, }; if (blob) |b| { Io.Dir.cwd().writeFile(init.io, .{ .sub_path = bin_path0, .data = b }) catch |e| { errw.print("error: cannot write {s}: {s}\n", .{ bin_path0, @errorName(e) }) catch {}; return error.BuildFailed; }; const z = arena.dupeZ(u8, bin_path0) catch return error.BuildFailed; _ = std.os.linux.chmod(z.ptr, 0o755); out.print("built {s} (native, no libc)\n", .{bin_path0}) catch {}; return 0; } errw.writeAll("native build: ") catch {}; errw.writeAll(rgx_err.items) catch {}; errw.writeAll("falling back to the C11 pipeline\n") catch {}; }}
// Derive default paths from the input name: foo.rg -> foo.c + foo (or// foo.exe semantics left to cc).const stem = stem0;const c_path = try std.fmt.allocPrint(arena, "{s}.c", .{stem});const bin_path = out_path orelse default_bin;
var c_writer: std.Io.Writer.Allocating = .init(arena); defer c_writer.deinit();
var emitter = rinne.c_emitter.Emitter.init(arena, &c_writer.writer); emitter.emitProgram(&program) catch |e| switch (e) { error.UnsupportedConstruct => { errw.print("build error: {s}\n", .{emitter.err_msg}) catch {}; return error.CompileFailed; }, else => return e, }; const c_source = c_writer.written();
Io.Dir.cwd().writeFile(init.io, .{ .sub_path = c_path, .data = c_source, }) catch |e| { errw.print("error: cannot write {s}: {s}\n", .{ c_path, @errorName(e) }) catch {}; return error.BuildFailed; };
// Invoke the system C compiler; inherit stdio so cc diagnostics flow // straight to the user's terminal. var child = std.process.spawn(init.io, .{ .argv = &.{ "cc", "-std=c11", try std.fmt.allocPrint(arena, "-O{s}", .{opt_level}), c_path, "-o", bin_path }, .stdout = .inherit, .stderr = .inherit, }) catch { out.print("wrote {s}; system 'cc' not found — compile manually:\n cc -std=c11 -O{s} {s} -o {s}\n", .{ c_path, opt_level, c_path, bin_path }) catch {}; return 0; }; const term = child.wait(init.io) catch |e| { errw.print("error: cc failed: {s}\n", .{@errorName(e)}) catch {}; return error.BuildFailed; }; switch (term) { .exited => |code| { if (code == 0) { removeIntermediate(init.io, c_path); out.print("built {s} (from {s}, cc -O{s})\n", .{ bin_path, c_path, opt_level }) catch {}; return 0; } return error.BuildFailed; }, else => return error.BuildFailed, }}
/// Best-effort removal of an intermediate source file (`.c`/`.s`) after a/// successful build. Failures are ignored so a stale file never fails the/// build; callers need the file to be gone, but a leftover is only noise.fn removeIntermediate(io: std.Io, path: []const u8) void { _ = Io.Dir.cwd().deleteFile(io, path) catch {}; return;}
// ---------------------------------------------------------------------------// `rinne check` — parse + compile a file without executing it.// ---------------------------------------------------------------------------
fn checkFile(init: std.process.Init, errw: *Io.Writer, arena: std.mem.Allocator, path: []const u8) !void { const source = readFile(init, arena, path) catch |e| { errw.print("error: cannot read {s}: {s}\n", .{ path, @errorName(e) }) catch {}; return error.CompileFailed; }; var parser = rinne.parser.Parser.init(arena, source); defer parser.deinit(); var program = parser.parseProgram() catch |e| { reportStage("parse", e, parser.err_msg, errw); return error.CompileFailed; }; resolveFetches(init, arena, &program, errw) catch return error.CompileFailed; var compiler = rinne.compiler.Compiler.init(arena); var module = compiler.compileProgram(&program) catch |e| { reportStage("compile", e, compiler.err_msg, errw); return error.CompileFailed; }; module.deinit();}
// ---------------------------------------------------------------------------// `rinne count` — lightweight source metrics.// ---------------------------------------------------------------------------
const FileCount = struct { lines: usize, code_lines: usize, comment_lines: usize, fns: usize,};
fn countFile(init: std.process.Init, arena: std.mem.Allocator, path: []const u8) !FileCount { const source = try readFile(init, arena, path); var it = std.mem.splitScalar(u8, source, '\n'); var c = FileCount{ .lines = 0, .code_lines = 0, .comment_lines = 0, .fns = 0 }; while (it.next()) |raw| { c.lines += 1; const t = std.mem.trim(u8, raw, " \t\r"); if (t.len == 0) continue; if (std.mem.startsWith(u8, t, "//")) { c.comment_lines += 1; } else { c.code_lines += 1; } if (std.mem.indexOf(u8, t, "fn ") != null or std.mem.startsWith(u8, t, "fn ")) { c.fns += 1; } } return c;}
// ---------------------------------------------------------------------------// `rinne completions [bash|zsh|fish]` — emit a shell completion script.// ---------------------------------------------------------------------------
fn printCompletions(w: *Io.Writer, shell: []const u8) void { const subs = "run build check lint count fmt init repl completions version help"; const flags = "--native --no-libc --shared --check --quiet -o -O0 -O1 -O2 -O3 -l -L"; if (std.mem.eql(u8, shell, "zsh")) { w.print( \\#compdef rinne \\_rinne() {{ \\ local -a cmds \\ cmds=({s}) \\ if (( CURRENT == 2 )); then \\ compadd -- $cmds \\ elif [[ $words[2] == "run" || $words[2] == "build" || $words[2] == "check" || $words[2] == "lint" || $words[2] == "count" || $words[2] == "init" ]]; then \\ compadd -- ${{words[2]}}/* \\ fi \\}} \\compdef _rinne rinne \\ , .{subs}) catch {}; return; } if (std.mem.eql(u8, shell, "fish")) { w.print( \\complete -c rinne -f -n '__fish_use_subcommand' -a '{s}' \\complete -c rinne -f -l native -d 'Native x86-64 build' \\complete -c rinne -f -l no-libc -d 'Freestanding static ELF' \\complete -c rinne -f -n '__fish_seen_subcommand_from run build' -a '(__fish_complete_suffix .rg)' \\ , .{subs}) catch {}; return; } // bash (default) w.print( \\_rinne() {{ \\ local cur prev \\ COMPREPLY=() \\ cur=${{COMP_WORDS[COMP_CWORD]}} \\ prev=${{COMP_WORDS[COMP_CWORD-1]}} \\ local cmds="{s}" \\ local flags="{s}" \\ if [ $COMP_CWORD -eq 1 ]; then \\ COMPREPLY=( $(compgen -W "$cmds" -- "$cur") ) \\ return 0 \\ fi \\ case $prev in \\ -o) COMPREPLY=( $(compgen -f -- "$cur") ); return 0 ;; \\ run|build|check|lint|count) \\ COMPREPLY=( $(compgen -f -X '!*.rg' -- "$cur") ); return 0 ;; \\ esac \\ COMPREPLY=( $(compgen -W "$flags" -- "$cur") ) \\ return 0 \\}} \\complete -F _rinne rinne \\ , .{ subs, flags }) catch {};}
// ---------------------------------------------------------------------------// `rinne init <name>` — scaffold a new project directory.// ---------------------------------------------------------------------------
fn initProject(out: *Io.Writer, errw: *Io.Writer, io: std.Io, arena: std.mem.Allocator, name: []const u8) !u8 { const cwd = Io.Dir.cwd(); cwd.createDir(io, name, .default_dir) catch |e| { errw.print("error: cannot create {s}: {s}\n", .{ name, @errorName(e) }) catch {}; return 70; }; const main_src = \\// main.rg — generate a fresh project. \\// Run the scaffold: rinne run __NAME__/main.rg \\ \\@import = .{ std }; \\ \\pub fn main(args: []str) int { \\ std.print("Hello from __NAME__!\n", .{}); \\ std.print("received {} argument(s)\n", .{std.strings.len(args)}); \\ if std.strings.len(args) > 0 { \\ std.print("first arg: {}\n", .{args[0]}); \\ } \\ return 0; \\} \\ ; const generated = std.mem.replaceOwned(u8, arena, main_src, "__NAME__", name) catch return error.OutOfMemory; const main_path = try std.fmt.allocPrint(arena, "{s}/main.rg", .{name}); cwd.writeFile(io, .{ .sub_path = main_path, .data = generated }) catch |e| { errw.print("error: cannot write {s}: {s}\n", .{ main_path, @errorName(e) }) catch {}; return 70; }; const gitignore_path = try std.fmt.allocPrint(arena, "{s}/.gitignore", .{name}); cwd.writeFile(io, .{ .sub_path = gitignore_path, .data = "zig-out/\n*.rgx\n*.c\n*.s\n" }) catch |e| { errw.print("error: cannot write {s}: {s}\n", .{ gitignore_path, @errorName(e) }) catch {}; return 70; }; out.print("scaffolded project in {s}/\n {s}/main.rg\n {s}/.gitignore\n\nTry: rinne run {s}/main.rg\n", .{ name, name, name, name }) catch {}; return 0;}
// ---------------------------------------------------------------------------// `rinne repl` — line-oriented interactive loop. Each complete snippet is// parsed, compiled and run independently (no variable state persists between// entries), which is ideal for exercising expressions and the std library.// ---------------------------------------------------------------------------
var g_repl_in: [4096]u8 = undefined;
fn replReadLine() ?[]const u8 { var i: usize = 0; while (i < g_repl_in.len) { var b: [1]u8 = undefined; const n = std.posix.read(0, &b) catch return null; if (n == 0) return if (i == 0) null else g_repl_in[0..i]; // EOF if (b[0] == '\n') return g_repl_in[0..i]; if (b[0] != '\r') { g_repl_in[i] = b[0]; i += 1; } } return g_repl_in[0..i];}
fn repl(init: std.process.Init, out: *Io.Writer, errw: *Io.Writer, _: []const []const u8) u8 { const arena = init.arena.allocator(); if (!g_quiet) { out.writeAll("RinneScript REPL — type expressions/statements; Ctrl-D to exit\n") catch {}; out.writeAll("Each snippet is compiled and run on its own (no shared state).\n") catch {}; } var all_ok = true; while (true) { out.writeAll("rinne> ") catch {}; out.flush() catch {}; const first = replReadLine() orelse break; if (std.mem.trim(u8, first, " \t\r").len == 0) continue; // Gather continuation lines while braces are unbalanced. var buf: std.ArrayList(u8) = .empty; buf.appendSlice(arena, first) catch return 70; defer buf.deinit(arena); var depth: isize = braceDepth(first); while (depth > 0) { out.writeAll(" ... ") catch {}; out.flush() catch {}; const more = replReadLine() orelse break; buf.append(arena, '\n') catch return 70; buf.appendSlice(arena, more) catch return 70; depth += braceDepth(more); } if (std.mem.trim(u8, buf.items, " \t\r").len == 0) continue; if (std.mem.eql(u8, std.mem.trim(u8, buf.items, " \t\r"), "exit") or std.mem.eql(u8, std.mem.trim(u8, buf.items, " \t\r"), "quit")) { break; } if ((runSource(init, out, errw, arena, buf.items, &.{}) catch 70) != 0) all_ok = false; } out.writeAll("\n") catch {}; return if (all_ok) 0 else 70;}
fn braceDepth(line: []const u8) isize { var d: isize = 0; var in_str = false; for (line) |c| { if (c == '"') in_str = !in_str; if (!in_str) { if (c == '{') d += 1; if (c == '}') d -= 1; } } return d;}
/// Runs an already-read source string through the full pipeline/// (parse -> fetch -> compile -> execute). Shared by `run` and `repl`.fn runSource(init: std.process.Init, out: *Io.Writer, errw: *Io.Writer, arena: std.mem.Allocator, source: []const u8, cli_args: []const []const u8) !u8 { const stats = try executeSource(init, out, errw, arena, source, cli_args); return stats.code;}
const RunStats = struct { code: u8, assertions: u64, failures: u64,};
/// Full pipeline for one source unit: parse -> fetch -> compile -> execute./// Throws the same stage errors as the old monolith; tests map them onto a/// failed result after collecting whatever the VM already reported.fn executeSource(init: std.process.Init, out: *Io.Writer, errw: *Io.Writer, arena: std.mem.Allocator, source: []const u8, cli_args: []const []const u8) !RunStats { var parser = rinne.parser.Parser.init(arena, source); defer parser.deinit(); var program = parser.parseProgram() catch |e| { reportStage("parse", e, parser.err_msg, errw); return error.CompileFailed; }; resolveFetches(init, arena, &program, errw) catch return error.CompileFailed; var compiler = rinne.compiler.Compiler.init(arena); var module = compiler.compileProgram(&program) catch |e| { reportStage("compile", e, compiler.err_msg, errw); return error.CompileFailed; }; defer module.deinit(); var vm = rinne.vm.Vm.init(arena, out, errw); vm.environ = init.environ_map; vm.io = init.io; const code = vm.runModule(&module, cli_args) catch |e| { reportStage("runtime", e, vm.err_msg, errw); out.flush() catch {}; return error.RuntimeFailed; }; out.flush() catch {}; return .{ .code = code, .assertions = vm.test_assertions, .failures = vm.test_failures };}
/// `rinne test [path]`: run every `*.rg` file under a directory (recursively,/// default `tests/`) or a single file argument, then print a summary. Returns/// 0 when every file passed, 70 otherwise.fn testFiles(init: std.process.Init, out: *Io.Writer, errw: *Io.Writer, target: []const u8) !u8 { const arena = init.arena.allocator(); var paths: std.ArrayList([]const u8) = .empty; defer paths.deinit(arena);
const io = init.io; const cwd = std.Io.Dir.cwd(); if (cwd.openDir(io, target, .{ .iterate = true })) |dir_val| { var dir = dir_val; defer dir.close(io); var walker = try std.Io.Dir.walkSelectively(dir, arena); defer walker.deinit(); while (try walker.next(io)) |entry| { if (entry.kind == .file and std.mem.endsWith(u8, entry.basename, ".rg")) { const p = try std.fmt.allocPrint(arena, "{s}/{s}", .{ target, entry.path }); paths.append(arena, p) catch return error.OutOfMemory; } } } else |_| { // Not a directory: try it as a single script (checked below). paths.append(arena, target) catch return error.OutOfMemory; }
if (paths.items.len == 0) { errw.print("error: 'rinne test' found no *.rg files under '{s}'\n", .{target}) catch {}; return 64; }
var files_failed: usize = 0; var total_assertions: u64 = 0; var total_failures: u64 = 0; for (paths.items) |p| { const source = readFile(init, arena, p) catch |e| { reportFailure(e, errw); files_failed += 1; out.print("FAIL {s}\n", .{p}) catch {}; continue; }; const stats = executeSource(init, out, errw, arena, source, &.{}) catch RunStats{ .code = 70, .assertions = 0, .failures = 1 }; total_assertions += stats.assertions; total_failures += stats.failures; const ok = stats.code == 0 and stats.failures == 0; if (!ok) files_failed += 1; if (ok) { if (stats.assertions == 0) { out.print("ok {s} (no assertions)\n", .{p}) catch {}; } else { out.print("ok {s} ({d} assertion{s})\n", .{ p, stats.assertions, if (stats.assertions == 1) "" else "s" }) catch {}; } } else { out.print("FAIL {s} ({d} assertion{s}, {d} failure{s})\n", .{ p, stats.assertions, if (stats.assertions == 1) "" else "s", stats.failures, if (stats.failures == 1) "" else "s" }) catch {}; } }
if (files_failed == 0) { out.print("\nAll {d} test file{s} passed ({d} assertion{s}).\n", .{ paths.items.len, if (paths.items.len == 1) "" else "s", total_assertions, if (total_assertions == 1) "" else "s" }) catch {}; return 0; } out.print("\n{d} of {d} test file{s} failed ({d} assertion{s}, {d} failure{s}).\n", .{ files_failed, paths.items.len, if (paths.items.len == 1) "" else "s", total_assertions, if (total_assertions == 1) "" else "s", total_failures, if (total_failures == 1) "" else "s" }) catch {}; return 70;}
fn resolveFetches(init: std.process.Init, arena: std.mem.Allocator, program: *rinne.parser.Program, errw: *Io.Writer) !void { var pkg_opts = rinne.loader.Options{}; { const m = init.environ_map; pkg_opts.home_dir = m.get("HOME"); if (m.get("RINNE_CACHE_HOME")) |c| pkg_opts.cache_dir_override = c; if (m.get("RINNE_OFFLINE")) |o| pkg_opts.offline = std.mem.eql(u8, o, "1"); } var pkg_detail: ?[]u8 = null; rinne.loader.resolveFetches(arena, init.io, program, pkg_opts, &pkg_detail) catch |e| { reportStage("package", e, pkg_detail orelse @errorName(e), errw); return error.CompileFailed; };}
test { // Pull lsp.zig into the test graph so its test blocks run with `zig build test`. _ = lsp_mod;}