//! 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(\"\") or @fetch(\"\", .{{ .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); } }