// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Where source text comes from. //! //! Nothing in this library opens a file. When the assembler wants the text of //! a name — the source it was pointed at, an `include`, or one of the names it //! tries around either — it asks here, and what a name means is the caller's //! business: a file, an entry in an archive, a string it already had. //! //! `open` hands back a reader or says there is no such source. The assembler //! reads that reader to the end and then calls `close`, and it never has two //! open at once, so one reader's worth of state is enough to implement this. const std = @import("std"); const Io = std.Io; const Allocator = std.mem.Allocator; const Source = @This(); /// Handed back to `open` and `close` untouched. context: ?*anyopaque = null, /// A reader positioned at the start of `name`, or null when there is no such /// source. A name that is not there is not a failure: that is what null says. open: *const fn (context: ?*anyopaque, name: []const u8) OpenError!?*Io.Reader, /// Say that the reader `open` handed back has been read and is finished with. close: *const fn (context: ?*anyopaque, reader: *Io.Reader) void = ignore, /// What `open` may fail with. `error.ReadFailed` is for a source that is /// there and would not open; the caller keeps whatever detail it has. pub const OpenError = Allocator.Error || error{ReadFailed}; fn ignore(_: ?*anyopaque, _: *Io.Reader) void {} /// A source with nothing in it. Every name comes back missing. pub const none: Source = .{ .open = openNothing }; fn openNothing(_: ?*anyopaque, _: []const u8) OpenError!?*Io.Reader { return null; } /// Source text held in memory, each piece under the name the source will ask /// for it by. /// /// ```zig /// var files: aigrette65.Source.Memory = .{ .files = &.{ /// .{ .name = "prog.a65", .text = " org $1000\n nop\n" }, /// } }; /// var assembler = Assembler.init(gpa, files.source(), &options, image, console); /// ``` pub const Memory = struct { files: []const File, /// Where the reader handed to the assembler lives. One is enough: the /// assembler reads a source to the end before it asks for another. reader: Io.Reader = undefined, pub const File = struct { name: []const u8, text: []const u8, }; pub fn source(self: *Memory) Source { return .{ .context = self, .open = open }; } fn open(context: ?*anyopaque, name: []const u8) OpenError!?*Io.Reader { const self: *Memory = @ptrCast(@alignCast(context.?)); for (self.files) |file| { if (std.mem.eql(u8, file.name, name)) { self.reader = .fixed(file.text); return &self.reader; } } return null; } }; const testing = std.testing; test "a memory source hands back what it was given, and nothing else" { var files: Memory = .{ .files = &.{ .{ .name = "one.a65", .text = "first" }, .{ .name = "two.a65", .text = "second" }, } }; const source = files.source(); const reader = (try source.open(source.context, "two.a65")).?; const text = try reader.allocRemaining(testing.allocator, .unlimited); defer testing.allocator.free(text); try testing.expectEqualStrings("second", text); source.close(source.context, reader); try testing.expectEqual(@as(?*Io.Reader, null), try source.open(source.context, "three.a65")); } test "the empty source has nothing in it" { try testing.expectEqual(@as(?*Io.Reader, null), try none.open(none.context, "anything")); }