Something went wrong. Try again.
A general purpose .. programming language written in zig ijadux2.tngl.sh/Rinnescript
zig language
Something went wrong. Try again.
21 kB · 535 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535//! Resolves module-level `@include` directives between parsing and compilation.//!//! `@include = .{ "./a.rg", .vec = "./vec.rg" }` stitches local RinneScript//! files into one program (Nix `import` / Zig `@import` inspiration)://!//! - bare paths flatten a file's `pub fn` + `pub const` into the importing//! scope (callable directly);//! - `.alias = "path"` binds the same declarations to a namespace//! (`alias.fn(...)`, `alias.CONST`);//! - nested `@include` inside an included file is resolved transitively,//! every distinct file exactly once (idempotent — Python `import` caching),//! and paths resolve relative to the file that declares them.//!//! Include targets are pure: only declarations (functions and constants).//! Top-level runtime statements are rejected (Nix-style purity; no side//! effects on import). Every reachable declaration is spliced flat under its//! real name — mirroring the `@fetch` loader — and the include namespace is an//! additional alias so name collisions across files are caught loudly.//!//! Memory contract: spliced declarations reference their file's parsed AST and//! source, so parsed include programs are intentionally kept alive. Callers//! must pass an arena-backed allocator (as vm.runSource / main.runFile do).
const std = @import("std");const parser_mod = @import("parser.zig");const compiler_mod = @import("compiler.zig");const vm_mod = @import("vm.zig");const Program = parser_mod.Program;const IncludeEntry = parser_mod.IncludeEntry;const IncludeExport = parser_mod.IncludeExport;const ConstDecl = parser_mod.ConstDecl;const FnDecl = parser_mod.FnDecl;
pub const Error = error{ OutOfMemory, NoIo, IncludeMissing, InvalidInclude,};
fn setDetail(a: std.mem.Allocator, detail: *?[]u8, comptime fmt: []const u8, args: anytype) void { detail.* = std.fmt.allocPrint(a, fmt, args) catch null;}
/// A discovered include target, kept alive for its spliced declarations.const FileEntry = struct { canonical: []const u8, // normalized absolute path (dedupe key) dir: []const u8, // directory containing this file ("" when unknown) source: []const u8, program: parser_mod.Program,};
/// Normalizes a path into an absolute, `.`/`..`-collapsed key. Purely textual:/// good enough for dedupe keys and for reading via the filesystem.fn normJoin(a: std.mem.Allocator, base: []const u8, rel: []const u8) Error![]const u8 { var combined: std.ArrayList(u8) = .empty; defer combined.deinit(a); if (rel.len > 0 and rel[0] == '/') { try combined.appendSlice(a, rel); } else { if (base.len > 0) { try combined.appendSlice(a, base); if (combined.items.len > 0 and combined.items[combined.items.len - 1] != '/') try combined.append(a, '/'); } try combined.appendSlice(a, rel); }
var parts: std.ArrayList([]const u8) = .empty; defer parts.deinit(a); var it = std.mem.splitScalar(u8, combined.items, '/'); while (it.next()) |seg| { if (seg.len == 0 or std.mem.eql(u8, seg, ".")) continue; if (std.mem.eql(u8, seg, "..")) { if (parts.items.len > 0) { _ = parts.pop(); } continue; } try parts.append(a, seg); }
var out: std.ArrayList(u8) = .empty; defer out.deinit(a); try out.append(a, '/'); for (parts.items, 0..) |seg, i| { if (i > 0) try out.append(a, '/'); try out.appendSlice(a, seg); } return out.toOwnedSlice(a);}
fn dirnameOf(a: std.mem.Allocator, path: []const u8) Error![]const u8 { const idx = std.mem.lastIndexOfScalar(u8, path, '/') orelse return a.dupe(u8, ""); return a.dupe(u8, path[0..idx]);}
fn isReservedNamespace(ns: []const u8) bool { const reserved = [_][]const u8{ "std", "ai", "llm", "net", "c", "runic", "rinne" }; for (reserved) |r| { if (std.mem.eql(u8, ns, r)) return true; } return false;}
/// Collects include entries from a parsed program (used for the host program/// and for nested includes inside listed files).fn collectEntries(program: *const Program, out: *std.ArrayList(IncludeEntry), a: std.mem.Allocator) Error!void { for (program.directives) |d| { if (d != .include_directive) continue; for (d.include_directive) |e| { const path = a.dupe(u8, e.path) catch return error.OutOfMemory; const ns = if (e.ns) |n| a.dupe(u8, n) catch return error.OutOfMemory else null; try out.append(a, .{ .path = path, .ns = ns }); } }}
const LoadState = struct { a: std.mem.Allocator, io: std.Io, files: std.ArrayList(FileEntry) = .empty, seen: std.StringHashMapUnmanaged(void) = .empty, detail: *?[]u8, chain: std.ArrayList([]const u8) = .empty,
fn readAndParse(self: *LoadState, path: []const u8) Error!parser_mod.Program { const source = std.Io.Dir.cwd().readFileAlloc(self.io, path, self.a, .limited(64 * 1024 * 1024)) catch |e| { setDetail(self.a, self.detail, "include '{s}': {s}", .{ path, @errorName(e) }); return Error.IncludeMissing; }; var pp = parser_mod.Parser.init(self.a, source); return pp.parseProgram() catch { setDetail(self.a, self.detail, "include '{s}' does not parse as RinneScript source", .{path}); return Error.InvalidInclude; }; }
/// Depth-first discovery: a file and everything it recursively includes, /// each visited exactly once. `base` is the directory of the file that /// declared this path ("" = cwd / host program). fn load(self: *LoadState, path: []const u8, base: []const u8) Error!void { const canonical = try normJoin(self.a, base, path); if (self.seen.contains(canonical)) return; // idempotent dedupe try self.seen.put(self.a, canonical, {}); try self.chain.append(self.a, canonical);
const program = try self.readAndParse(canonical);
// Validate purity: only declarations (functions/constants) allowed // plus @import (ignored) and nested @include; no runtime statements. for (program.directives) |d| { if (d == .import_directive or d == .include_directive) continue; if (d == .generic and (std.mem.eql(u8, d.generic.name, "fetch") or std.mem.eql(u8, d.generic.name, "fetch_pkg"))) { setDetail(self.a, self.detail, "include '{s}' may not use '@{s}'; declare fetched packages only in the host program", .{ canonical, d.generic.name }); return Error.InvalidInclude; } setDetail(self.a, self.detail, "include '{s}' may only contain functions, constants and nested @include (found a directive)", .{canonical}); return Error.InvalidInclude; } for (program.global_stmts.stmts) |*stmt| { if (stmt.* != .var_decl or !stmt.var_decl.is_const) { setDetail(self.a, self.detail, "include '{s}' may only contain declarations (found a top-level statement)", .{canonical}); return Error.InvalidInclude; } }
const dir = try dirnameOf(self.a, canonical); try self.files.append(self.a, .{ .canonical = canonical, .dir = dir, .source = dir, .program = program });
// Nested includes resolve relative to this file's directory. var nested: std.ArrayList(IncludeEntry) = .empty; defer nested.deinit(self.a); try collectEntries(&program, &nested, self.a); for (nested.items) |e| { try self.load(e.path, dir); } _ = self.chain.pop(); }
/// Free only the bookkeeping: file/program contents are kept alive /// intentionally (spliced declarations reference them), matching the /// loader's memory contract for arena-backed callers. fn deinit(self: *LoadState) void { self.files.deinit(self.a); self.seen.deinit(self.a); self.chain.deinit(self.a); }};
fn spliceFile( a: std.mem.Allocator, file: *FileEntry, fn_names: *std.StringHashMapUnmanaged(void), const_names: *std.StringHashMapUnmanaged(void), exports: *std.ArrayList(IncludeExport), program: *Program, err_detail: *?[]u8,) Error!void { _ = exports; const proj = &file.program;
// Collision checks: host decls + already-spliced files. for (proj.fn_decls) |fd| { if (fn_names.contains(fd.name) or const_names.contains(fd.name)) { setDetail(a, err_detail, "include '{s}' declares '{s}', which conflicts with an existing declaration", .{ file.canonical, fd.name }); return Error.InvalidInclude; } } for (proj.const_decls) |cd| { if (const_names.contains(cd.name) or fn_names.contains(cd.name)) { setDetail(a, err_detail, "include '{s}' declares '{s}', which conflicts with an existing declaration", .{ file.canonical, cd.name }); return Error.InvalidInclude; } } for (proj.global_stmts.stmts) |*stmt| { if (stmt.* != .var_decl or !stmt.var_decl.is_const) continue; if (const_names.contains(stmt.var_decl.name) or fn_names.contains(stmt.var_decl.name)) { setDetail(a, err_detail, "include '{s}' declares '{s}', which conflicts with an existing declaration", .{ file.canonical, stmt.var_decl.name }); return Error.InvalidInclude; } }
// Register every name that now belongs to the merged module. for (proj.fn_decls) |fd| try fn_names.put(a, fd.name, {}); for (proj.const_decls) |cd| try const_names.put(a, cd.name, {}); for (proj.struct_decls) |sd| try const_names.put(a, sd.name, {}); for (proj.enum_decls) |ed| try const_names.put(a, ed.name, {}); for (proj.global_stmts.stmts) |*stmt| { if (stmt.* != .var_decl or !stmt.var_decl.is_const) continue; try const_names.put(a, stmt.var_decl.name, {}); }
// Move function declarations (shallow; memory is owned by `file`, which // the arena keeps alive). const fstart = program.fn_decls.len; program.fn_decls = try a.realloc(program.fn_decls, fstart + proj.fn_decls.len); @memcpy(program.fn_decls[fstart..], proj.fn_decls); proj.fn_decls = &.{};
// Move `pub const` declarations. const pstart = program.const_decls.len; program.const_decls = try a.realloc(program.const_decls, pstart + proj.const_decls.len); @memcpy(program.const_decls[pstart..], proj.const_decls); proj.const_decls = &.{};
// Move struct declarations. const sstart = program.struct_decls.len; program.struct_decls = try a.realloc(program.struct_decls, sstart + proj.struct_decls.len); @memcpy(program.struct_decls[sstart..], proj.struct_decls); proj.struct_decls = &.{};
// Move enum declarations. const estart = program.enum_decls.len; program.enum_decls = try a.realloc(program.enum_decls, estart + proj.enum_decls.len); @memcpy(program.enum_decls[estart..], proj.enum_decls); proj.enum_decls = &.{};
// Promote included `const Name = expr;` statements to ConstDecls. The // original statements are neutralized so neither copy frees them twice. for (proj.global_stmts.stmts) |*stmt| { if (stmt.* != .var_decl or !stmt.var_decl.is_const) continue; const cd: ConstDecl = .{ .name = stmt.var_decl.name, .is_pub = false, .initializer = stmt.var_decl.initializer, }; const cur = program.const_decls.len; program.const_decls = try a.realloc(program.const_decls, cur + 1); program.const_decls[cur] = cd; stmt.* = .{ .expr_stmt = .null_lit }; }}
pub fn resolveIncludes( allocator: std.mem.Allocator, io: ?std.Io, program: *Program, base_dir: ?[]const u8, err_detail: *?[]u8,) Error!void { err_detail.* = null;
var top: std.ArrayList(IncludeEntry) = .empty; defer top.deinit(allocator); try collectEntries(program, &top, allocator); if (top.items.len == 0) return;
const real_io = io orelse { setDetail(allocator, err_detail, "include error: @include requires an Io environment (none available)", .{}); return Error.NoIo; };
var st = LoadState{ .a = allocator, .io = real_io, .detail = err_detail, }; // Discover and validate the whole include graph. const host_base = base_dir orelse ""; for (top.items) |e| { try st.load(e.path, host_base); }
// Splice declarations and build exports. var fn_names: std.StringHashMapUnmanaged(void) = .empty; defer fn_names.deinit(allocator); var const_names: std.StringHashMapUnmanaged(void) = .empty; defer const_names.deinit(allocator); for (program.fn_decls) |fd| { try fn_names.put(allocator, fd.name, {}); } for (program.const_decls) |cd| { try const_names.put(allocator, cd.name, {}); }
var exports: std.ArrayList(IncludeExport) = .empty; defer exports.deinit(allocator); for (top.items) |e| { const canonical = try normJoin(allocator, host_base, e.path); const file = for (st.files.items) |*f| { if (std.mem.eql(u8, f.canonical, canonical)) break f; } else null;
// Distinct duplicate path spellings collapse to the same canonical // file; re-export under each alias in the directive. if (file == null) { setDetail(allocator, err_detail, "include '{s}' was not discovered before splicing", .{e.path}); return Error.InvalidInclude; }
if (e.ns) |ns| { if (isReservedNamespace(ns)) { setDetail(allocator, err_detail, "include namespace '{s}' collides with a reserved module name", .{ns}); return Error.InvalidInclude; } } else if (!std.mem.eql(u8, e.path, canonical)) { // keep flat }
// Exported symbols: pub fns + pub consts from the file. const fprog = &file.?.program; for (fprog.fn_decls) |fd| { if (!fd.is_pub) continue; try exports.append(allocator, .{ .ns = if (e.ns) |ns| ns else "", .local = fd.name, .global = fd.name, .is_const = false, }); } for (fprog.const_decls) |cd| { if (!cd.is_pub) continue; try exports.append(allocator, .{ .ns = if (e.ns) |ns| ns else "", .local = cd.name, .global = cd.name, .is_const = true, }); } }
// Splice each discovered file exactly once (DSF order). var spliced: std.StringHashMapUnmanaged(void) = .empty; defer spliced.deinit(allocator); for (st.files.items) |*f| { if (spliced.contains(f.canonical)) continue; try spliced.put(allocator, f.canonical, {}); try spliceFile(allocator, f, &fn_names, &const_names, &exports, program, err_detail); }
program.include_exports = try exports.toOwnedSlice(allocator);
st.deinit();
// Strip consumed @include directives. var kept: std.ArrayList(parser_mod.Directive) = .empty; defer kept.deinit(allocator); for (program.directives) |d| { if (d == .include_directive) continue; kept.append(allocator, d) catch return Error.OutOfMemory; } const old = program.directives; const new_dirs = allocator.alloc(parser_mod.Directive, kept.items.len) catch return Error.OutOfMemory; @memcpy(new_dirs, kept.items); program.directives = new_dirs; allocator.free(old);}
test "resolveIncludes splices fns and consts (flatten + namespace) and runs" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const a = arena_state.allocator();
var td = std.testing.tmpDir(.{}); defer td.cleanup();
{ var f = try td.dir.createFile(std.testing.io, "vec.rg", .{}); defer f.close(std.testing.io); try std.Io.File.writePositionalAll(f, std.testing.io, \\pub fn dot(a: int, b: int) int { \\ return a * 10 + b; \\} \\fn private_helper(v: int) int { \\ return v * 2; \\} \\pub const EPS: str = "0.0001"; \\ , 0); } { var f = try td.dir.createFile(std.testing.io, "util.rg", .{}); defer f.close(std.testing.io); try std.Io.File.writePositionalAll(f, std.testing.io, \\pub fn triple(v: int) int { \\ return v * 3; \\} \\ , 0); }
var cwd_buf: [4096]u8 = undefined; const cwd_len = try std.process.currentPath(std.testing.io, &cwd_buf); const base_dir = try std.fmt.allocPrint(a, "{s}/.zig-cache/tmp/{s}", .{ cwd_buf[0..cwd_len], &td.sub_path });
const source = \\@include = .{ "./vec.rg", .util = "./util.rg" }; \\@stdout("flat dot: {}\n", .{dot(2, 5)}); \\@stdout("ns triple: {}\n", .{util.triple(4)}); \\@stdout("eps: {}\n", .{EPS}); ; var parser = parser_mod.Parser.init(a, source); var program = try parser.parseProgram();
var detail: ?[]u8 = null; try resolveIncludes(a, std.testing.io, &program, base_dir, &detail);
// Exports: `dot` (flat), `EPS` (flat), `triple` (ns "util"). const exp = program.include_exports; try std.testing.expectEqual(@as(usize, 3), exp.len); try std.testing.expectEqualStrings("", exp[0].ns); try std.testing.expectEqualStrings("dot", exp[0].local); try std.testing.expectEqual(false, exp[0].is_const); try std.testing.expectEqualStrings("", exp[1].ns); try std.testing.expectEqualStrings("EPS", exp[1].local); try std.testing.expectEqual(true, exp[1].is_const); try std.testing.expectEqualStrings("util", exp[2].ns); try std.testing.expectEqualStrings("triple", exp[2].local); try std.testing.expectEqual(false, exp[2].is_const);
// The @include directives were consumed. for (program.directives) |d| { try std.testing.expect(d != .include_directive); }
// Splice + compile + execute the merged module. var out = std.Io.Writer.Allocating.init(a); var errout = std.Io.Writer.Allocating.init(a); var vm = vm_mod.Vm.init(a, &out.writer, &errout.writer); var compiler = compiler_mod.Compiler.init(a); var module = try compiler.compileProgram(&program); defer module.deinit(); const code = try vm.runModule(&module, &.{}); try std.testing.expectEqual(@as(u8, 0), code); try std.testing.expectEqualStrings("flat dot: 25\nns triple: 12\neps: 0.0001\n", out.written());}
test "resolveIncludes rejects non-declarations in included files" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const a = arena_state.allocator();
var td = std.testing.tmpDir(.{}); defer td.cleanup(); { var f = try td.dir.createFile(std.testing.io, "imp.rg", .{}); defer f.close(std.testing.io); try std.Io.File.writePositionalAll(f, std.testing.io, \\pub fn fine(v: int) int { \\ return v; \\} \\@stdout("side effect\n", .{}); \\ , 0); }
var cwd_buf: [4096]u8 = undefined; const cwd_len = try std.process.currentPath(std.testing.io, &cwd_buf); const base_dir = try std.fmt.allocPrint(a, "{s}/.zig-cache/tmp/{s}", .{ cwd_buf[0..cwd_len], &td.sub_path });
const source = "@include = .{ \"./imp.rg\" };\n"; var parser = parser_mod.Parser.init(a, source); var program = try parser.parseProgram();
var detail: ?[]u8 = null; try std.testing.expectError(Error.InvalidInclude, resolveIncludes(a, std.testing.io, &program, base_dir, &detail)); try std.testing.expect(detail != null);}
test "resolveIncludes rejects name collisions with host declarations" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const a = arena_state.allocator();
var td = std.testing.tmpDir(.{}); defer td.cleanup(); { var f = try td.dir.createFile(std.testing.io, "c1.rg", .{}); defer f.close(std.testing.io); try std.Io.File.writePositionalAll(f, std.testing.io, \\pub fn collide(v: int) int { \\ return v; \\} \\ , 0); }
var cwd_buf: [4096]u8 = undefined; const cwd_len = try std.process.currentPath(std.testing.io, &cwd_buf); const base_dir = try std.fmt.allocPrint(a, "{s}/.zig-cache/tmp/{s}", .{ cwd_buf[0..cwd_len], &td.sub_path });
const source = \\@include = .{ "./c1.rg" }; \\fn collide(v: int) int { \\ return v; \\} ; var parser = parser_mod.Parser.init(a, source); var program = try parser.parseProgram();
var detail: ?[]u8 = null; try std.testing.expectError(Error.InvalidInclude, resolveIncludes(a, std.testing.io, &program, base_dir, &detail)); try std.testing.expect(detail != null);}