Something went wrong. Try again.
An assembler for the 6502 and 65SC02 written in Zig, in the dialect of Frank Kingswood's as65.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// 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"));}