Something went wrong. Try again.
A general purpose .. programming language written in zig ijadux2.tngl.sh/Rinnescript
zig language
Something went wrong. Try again.
9.7 kB · 238 lines
Zig
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239//! Resolves module-level `@fetch` directives between parsing and compilation.//!//! The loader scans `program.directives` for `.generic` entries named "fetch",//! downloads/caches each package via `pkg.PackageManager`, parses the package//! source, validates it (functions only, no nested fetches), checks for name//! collisions, and splices the package functions ahead of the program's own.//!//! Memory contract: spliced function declarations reference their package//! source text, so downloaded sources are intentionally kept alive. Callers//! must pass an arena-backed allocator (as vm.runSource / main.runFile do).//!//! Error contract: on failure `err_detail` (if non-null) receives an allocated//! human-readable message owned by the caller; the returned error is always a//! member of `pkg.Error`.const std = @import("std");const parser_mod = @import("parser.zig");const Program = parser_mod.Program;const Directive = parser_mod.Directive;const pkg = @import("pkg.zig");
pub const Options = struct { /// Overrides the package cache root entirely (tests, RINNE_CACHE_HOME). cache_dir_override: ?[]const u8 = null, /// $HOME used when no cache_dir_override is given. home_dir: ?[]const u8 = null, offline: bool = false, /// Injectable transport (tests); null uses the real HTTP client. fetch_seam: ?pkg.FetchFn = null,};
pub const Error = pkg.Error;
const Work = struct { url: []u8, version: ?[]u8, sha256: ?[]u8,};
fn dupOpt(a: std.mem.Allocator, s: ?[]const u8) !?[]u8 { if (s) |v| return try a.dupe(u8, v); return null;}
fn setDetail(a: std.mem.Allocator, detail: *?[]u8, comptime fmt: []const u8, args: anytype) void { detail.* = std.fmt.allocPrint(a, fmt, args) catch null;}
/// Extracts parameters from every module-level `@fetch(...)` directive.fn collectWork( a: std.mem.Allocator, program: *const Program, works: *std.ArrayList(Work), detail: *?[]u8,) Error!void { for (program.directives) |d| { if (d != .generic) continue; if (!std.mem.eql(u8, d.generic.name, "fetch")) continue; const args = d.generic.args; if (args.len == 0 or args.len > 2 or args[0] != .string_lit) { setDetail(a, detail, "compile error: @fetch expects @fetch(\"<url>\") or @fetch(\"<url>\", .{{ .version = \"v1.2.0\", .sha256 = \"...\" }})", .{}); return Error.InvalidPackage; } var version: ?[]const u8 = null; var sha256_hex: ?[]const u8 = null; if (args.len == 2) { if (args[1] != .struct_lit) { setDetail(a, detail, "compile error: @fetch second argument must be an options struct: .{{ .version = \"...\", .sha256 = \"...\" }}", .{}); return Error.InvalidPackage; } for (args[1].struct_lit) |f| { if (f.value != .string_lit) { setDetail(a, detail, "compile error: @fetch option '.{s}' must be a string literal", .{f.name}); return Error.InvalidPackage; } if (std.mem.eql(u8, f.name, "version")) { version = f.value.string_lit; } else if (std.mem.eql(u8, f.name, "sha256")) { sha256_hex = f.value.string_lit; } else { setDetail(a, detail, "compile error: unknown @fetch option '.{s}' (known: .version, .sha256)", .{f.name}); return Error.InvalidPackage; } } } works.append(a, .{ .url = a.dupe(u8, args[0].string_lit) catch return Error.OutOfMemory, .version = dupOpt(a, version) catch return Error.OutOfMemory, .sha256 = dupOpt(a, sha256_hex) catch return Error.OutOfMemory, }) catch return Error.OutOfMemory; }}
fn deinitMovedFns(a: std.mem.Allocator, fns: *std.ArrayList(parser_mod.FnDecl)) void { for (fns.items) |*fd| fd.deinitFields(a); fns.deinit(a);}
/// Processes every `@fetch` directive in the program: downloads, validates,/// splices package functions ahead of the program's own declarations, and/// strips the consumed directives. A program without any `@fetch` is untouched.pub fn resolveFetches( allocator: std.mem.Allocator, io: ?std.Io, program: *Program, opts: Options, err_detail: *?[]u8,) Error!void { err_detail.* = null; var works: std.ArrayList(Work) = .empty; defer { for (works.items) |w| { allocator.free(w.url); if (w.version) |v| allocator.free(v); if (w.sha256) |s| allocator.free(s); } works.deinit(allocator); }
try collectWork(allocator, program, &works, err_detail); if (works.items.len == 0) return;
const real_io = io orelse { setDetail(allocator, err_detail, "package error: @fetch requires an Io environment (none available)", .{}); return Error.DownloadFailed; };
var pm = blk: { if (opts.cache_dir_override) |c| { const dir = allocator.dupe(u8, c) catch return Error.OutOfMemory; break :blk pkg.PackageManager{ .allocator = allocator, .io = real_io, .cache_dir = dir, .offline = opts.offline, .fetch_impl = opts.fetch_seam orelse pkg.defaultFetch, }; } break :blk pkg.PackageManager.init( allocator, real_io, null, opts.home_dir, if (opts.offline) "1" else null, ) catch |e| { if (e == Error.CacheUnavailable) { setDetail(allocator, err_detail, "package error: cannot determine cache directory (set HOME or RINNE_CACHE_HOME)", .{}); } return e; }; }; defer pm.deinit();
var pkg_fns: std.ArrayList(parser_mod.FnDecl) = .empty; errdefer deinitMovedFns(allocator, &pkg_fns);
for (works.items) |w| { const src = pm.resolve(w.url, w.version, w.sha256) catch |e| { if (pm.last_error) |m| { setDetail(allocator, err_detail, "package '{s}': {s}", .{ w.url, m }); } else { setDetail(allocator, err_detail, "package '{s}': {s}", .{ w.url, @errorName(e) }); } return e; }; // src is intentionally NOT freed: spliced declarations reference it // (callers use arena allocators; see file header).
var pp = parser_mod.Parser.init(allocator, src); var pprog = pp.parseProgram() catch { setDetail(allocator, err_detail, "package '{s}' does not parse as RinneScript source", .{w.url}); return Error.InvalidPackage; }; defer pprog.deinit(allocator);
if (pprog.global_stmts.stmts.len != 0) { setDetail(allocator, err_detail, "package '{s}' may only contain function declarations (found top-level statements)", .{w.url}); return Error.InvalidPackage; } if (pprog.directives.len != 0) { setDetail(allocator, err_detail, "package '{s}' contains directives, which are not supported in v1 packages", .{w.url}); return Error.InvalidPackage; }
// Name-collision checks against the host program and prior packages. for (pprog.fn_decls) |fd| { for (program.fn_decls) |existing| { if (std.mem.eql(u8, existing.name, fd.name)) { setDetail(allocator, err_detail, "package '{s}' declares '{s}', which conflicts with an existing declaration", .{ w.url, fd.name }); return Error.InvalidPackage; } } for (pkg_fns.items) |loaded| { if (std.mem.eql(u8, loaded.name, fd.name)) { setDetail(allocator, err_detail, "package '{s}' declares '{s}', which conflicts with another fetched package", .{ w.url, fd.name }); return Error.InvalidPackage; } } }
// Move ownership of the package's functions into our accumulation // list, then neutralize pprog's slice so its deinit skips them. for (pprog.fn_decls) |fd| { pkg_fns.append(allocator, fd) catch return Error.OutOfMemory; } pprog.fn_decls = &.{}; }
// Splice: package functions first, then the program's own. if (pkg_fns.items.len != 0) { const old = program.fn_decls; const combined = allocator.alloc(parser_mod.FnDecl, pkg_fns.items.len + old.len) catch return Error.OutOfMemory; @memcpy(combined[0..pkg_fns.items.len], pkg_fns.items); @memcpy(combined[pkg_fns.items.len..], old); program.fn_decls = combined; allocator.free(old); } pkg_fns.deinit(allocator); // items were copied out; shell only
// Strip consumed @fetch directives (deinit their argument expressions). var kept: std.ArrayList(Directive) = .empty; defer kept.deinit(allocator); var removed_any = false; for (program.directives) |d| { if (d == .generic and std.mem.eql(u8, d.generic.name, "fetch")) { d.generic.deinit(allocator); removed_any = true; continue; } kept.append(allocator, d) catch return Error.OutOfMemory; } if (removed_any) { const old_dirs = program.directives; const new_dirs = allocator.alloc(Directive, kept.items.len) catch return Error.OutOfMemory; @memcpy(new_dirs, kept.items); program.directives = new_dirs; allocator.free(old_dirs); }}