// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! The assembled memory image, and the three shapes it can be written out in. //! //! `as65` assembles into a flat 64K address space and remembers which bytes it //! has touched. A binary file spans the lowest touched address through the //! highest, with the gaps filled; s-records and intel-hex carry only the runs //! that were actually assembled. //! //! What was touched is remembered twice over. `used` is everything either pass //! wrote and is what the output is cut from, so code that shrank between the //! passes leaves its longer pass one tail behind in the file — as65 does not //! clear the image between passes, and that tail is in its output too. The //! `pass_used` map is cleared at the start of each pass, and is the one that //! decides whether a byte is being written twice or an `org` is landing on //! something, neither of which pass two would ever get past otherwise. const std = @import("std"); const Io = std.Io; const Image = @This(); pub const size = 0x10000; bytes: [size]u8 = @splat(0), used: [size]bool = @splat(false), pass_used: [size]bool = @splat(false), lowest: u32 = size, highest: u32 = 0, /// Forget what this pass wrote, keeping the image itself. pub fn beginPass(self: *Image) void { self.pass_used = @splat(false); } pub fn isEmpty(self: *const Image) bool { return self.lowest > self.highest; } /// Record one byte. Returns false if that address had already been written, /// which `as65` reports as overwriting a used location. pub fn put(self: *Image, address: u32, byte: u8) bool { const a = address & (size - 1); const fresh = !self.pass_used[a]; self.bytes[a] = byte; self.used[a] = true; self.pass_used[a] = true; if (a < self.lowest) self.lowest = a; if (a > self.highest) self.highest = a; return fresh; } /// Write the whole span from the lowest to the highest byte used, filling /// anything never assembled with `fill`. pub fn writeBinary(self: *const Image, w: *Io.Writer, fill: u8) Io.Writer.Error!void { if (self.isEmpty()) return; for (self.lowest..self.highest + 1) |a| { try w.writeByte(if (self.used[a]) self.bytes[a] else fill); } } /// The next run of assembled bytes at or after `from`, or null when there are /// no more. pub fn nextRun(self: *const Image, from: u32) ?struct { start: u32, end: u32 } { var a = from; while (a <= self.highest and !self.used[a]) a += 1; if (a > self.highest) return null; var end = a; while (end + 1 <= self.highest and self.used[end + 1]) end += 1; return .{ .start = a, .end = end }; } /// How many bytes the record starting at `start` carries. /// /// At most thirty-two, and never past the next thirty-two byte boundary: `as65` /// lines its records up on those, so a run beginning at $000a opens with a /// twenty-two byte record and only then settles into full ones. fn recordLength(start: u32, end: u32) u32 { const to_boundary = 32 - (start % 32); return @min(to_boundary, end - start + 1); } fn checksum(bytes: []const u8) u8 { var sum: u8 = 0; for (bytes) |b| sum +%= b; return sum; } /// Motorola s-records: S1 data records of at most 32 bytes, then an S5 record /// counting them, and an S9 start-address record when the program has a start /// address or `-z` asked for one regardless. pub fn writeSRecords( self: *const Image, w: *Io.Writer, start_address: ?u32, force_start_record: bool, ) Io.Writer.Error!void { var record: [32 + 3]u8 = undefined; var count: u16 = 0; if (!self.isEmpty()) { var at: u32 = self.lowest; while (self.nextRun(at)) |run| : (at = run.end + 1) { var a = run.start; while (a <= run.end) { const len = recordLength(a, run.end); record[0] = @intCast(len + 3); record[1] = @intCast((a >> 8) & 0xff); record[2] = @intCast(a & 0xff); @memcpy(record[3..][0..len], self.bytes[a..][0..len]); try w.print("S1{X:0>2}{X:0>4}", .{ record[0], a & 0xffff }); for (record[3..][0..len]) |b| try w.print("{X:0>2}", .{b}); try w.print("{X:0>2}\n", .{~checksum(record[0 .. 3 + len])}); count +%= 1; a += len; } } } const tally = [_]u8{ 3, @intCast((count >> 8) & 0xff), @intCast(count & 0xff) }; try w.print("S503{X:0>4}{X:0>2}\n", .{ count, ~checksum(&tally) }); if (start_address orelse (if (force_start_record) @as(u32, 0) else null)) |entry| { const rec = [_]u8{ 3, @intCast((entry >> 8) & 0xff), @intCast(entry & 0xff) }; try w.print("S903{X:0>4}{X:0>2}\n", .{ entry & 0xffff, ~checksum(&rec) }); } } /// Intel hex: type 00 data records of at most 32 bytes, then an end-of-file /// record carrying the start address when there is one. pub fn writeIntelHex( self: *const Image, w: *Io.Writer, start_address: ?u32, ) Io.Writer.Error!void { var record: [32 + 4]u8 = undefined; if (!self.isEmpty()) { var at: u32 = self.lowest; while (self.nextRun(at)) |run| : (at = run.end + 1) { var a = run.start; while (a <= run.end) { const len = recordLength(a, run.end); record[0] = @intCast(len); record[1] = @intCast((a >> 8) & 0xff); record[2] = @intCast(a & 0xff); record[3] = 0; @memcpy(record[4..][0..len], self.bytes[a..][0..len]); try w.print(":{X:0>2}{X:0>4}00", .{ record[0], a & 0xffff }); for (record[4..][0..len]) |b| try w.print("{X:0>2}", .{b}); try w.print("{X:0>2}\n", .{0 -% checksum(record[0 .. 4 + len])}); a += len; } } } const entry = start_address orelse 0; const rec = [_]u8{ 0, @intCast((entry >> 8) & 0xff), @intCast(entry & 0xff), 1 }; try w.print(":00{X:0>4}01{X:0>2}\n", .{ entry & 0xffff, 0 -% checksum(&rec) }); } const testing = std.testing; fn render(image: *const Image, comptime which: enum { s19, hex }) ![]u8 { var buf: [4096]u8 = undefined; var w = Io.Writer.fixed(&buf); switch (which) { .s19 => try image.writeSRecords(&w, null, false), .hex => try image.writeIntelHex(&w, null), } return testing.allocator.dupe(u8, w.buffered()); } test "s-records match what as65 writes for a single byte at $0010" { var image = Image{}; _ = image.put(0x10, 0x09); const out = try render(&image, .s19); defer testing.allocator.free(out); try testing.expectEqualStrings("S104001009E2\nS5030001FB\n", out); } test "intel-hex matches what as65 writes for a single byte at $0010" { var image = Image{}; _ = image.put(0x10, 0x09); const out = try render(&image, .hex); defer testing.allocator.free(out); try testing.expectEqualStrings(":0100100009E6\n:00000001FF\n", out); } test "records are cut at thirty-two byte boundaries" { try testing.expectEqual(@as(u32, 22), recordLength(0x0a, 0xffff)); try testing.expectEqual(@as(u32, 32), recordLength(0x20, 0xffff)); try testing.expectEqual(@as(u32, 3), recordLength(0x1d, 0x1f)); } test "a binary spans the lowest through the highest byte used" { var image = Image{}; _ = image.put(0x10, 1); _ = image.put(0x13, 2); var buf: [16]u8 = undefined; var w = Io.Writer.fixed(&buf); try image.writeBinary(&w, 0xff); try testing.expectEqualSlices(u8, &.{ 1, 0xff, 0xff, 2 }, w.buffered()); } test "writing an address twice is reported" { var image = Image{}; try testing.expect(image.put(0x20, 1)); try testing.expect(!image.put(0x20, 2)); }