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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Command-line options, parsed the way `as65` parses them.//!//! Letters may be run together in one argument, but an option that takes a//! string — `-d`, `-l`, `-o`, `-I` — swallows the whole rest of the argument,//! so nothing can follow it. An option that takes a number does not: `-h80t`//! is eighty lines to a page *and* a symbol table.
const std = @import("std");const Allocator = std.mem.Allocator;const isa = @import("isa.zig");
const Options = @This();
pub const OutputFormat = enum { binary, s_records, intel_hex };
pub const ParseError = error{ UnknownOption, MultipleInputFiles, NoInputFile, HelpRequested, OutOfMemory,};
/// What went wrong, ready to print. Set alongside a `ParseError`.pub const Diagnostic = struct { message: [64]u8 = undefined, len: usize = 0, /// `-h` with no number after it. as65 says so and carries on with the page /// length it already had, so this is a warning the caller prints rather /// than a reason to stop. page_length_missing: bool = false,
pub fn text(self: *const Diagnostic) []const u8 { return self.message[0..self.len]; }
fn set(self: *Diagnostic, comptime fmt: []const u8, args: anytype) void { const written = std.fmt.bufPrint(&self.message, fmt, args) catch self.message[0..]; self.len = written.len; }};
/// Show a cycle count for each instruction in the listing.cycles: bool = false,/// Write a source-level debug information file.debug_info: bool = false,/// Lines to a printed page; zero means the listing is never broken up.page_length: u32 = 60,/// Accept mnemonics in any case.ignore_case: bool = false,/// Produce a pass 2 listing.list_pass2: bool = false,/// Produce a pass 1 listing.list_pass1: bool = false,/// Show macro expansions in the listing, prefixed with `>`.list_macros: bool = false,/// Print a symbol table between the passes.symbol_table: bool = false,/// Honour `opt`; `-n` turns optimization off for good.optimize: bool = true,/// Suppress the banner and the running line counter.quiet: bool = false,/// Report each step on standard output.verbose: bool = false,/// The processor being assembled for: `-x1` adds the undocumented NMOS/// opcodes, `-x2` (or plain `-x`) the 65SC02 extensions.cpu: isa.Cpu = .mos6502,/// Fill unused bytes with zero rather than `$ff`.zero_fill: bool = false,/// Add a map of which parts of the address space were assembled into.usage_map: bool = false,/// Columns available to the listing.width: u32 = 79,format: OutputFormat = .binary,
input: []const u8 = "",output_path: ?[]const u8 = null,listing_path: ?[]const u8 = null,/// Labels to define as 1 before the first source line is read.defines: std.ArrayList([]const u8) = .empty,/// Prefixes tried in turn in front of an `include` file name.include_paths: std.ArrayList([]const u8) = .empty,
pub fn deinit(self: *Options, gpa: Allocator) void { self.defines.deinit(gpa); self.include_paths.deinit(gpa);}
/// Parse `args`, which must not include the program name.////// `include_env` is the value of the `INCLUDE` environment variable, which/// as65 searches before anything named with `-I`; pass null when it is/// unset. Reading the environment is left to the caller so that this stays a/// pure function of its arguments.pub fn parse( gpa: Allocator, args: []const []const u8, include_env: ?[]const u8, diag: *Diagnostic,) ParseError!Options { var self = Options{}; errdefer self.deinit(gpa);
if (include_env) |dir| try self.include_paths.append(gpa, dir);
var saw_listing_path = false; for (args) |arg| { if (arg.len == 0) continue; if (arg[0] != '-' or arg.len == 1) { if (self.input.len != 0) { diag.set("Only one input file can be specified.", .{}); return error.MultipleInputFiles; } self.input = arg; continue; } var i: usize = 1; while (i < arg.len) { const c = arg[i]; i += 1; switch (c) { 'c' => self.cycles = true, 'd', 'D' => { const name = arg[i..]; try self.defines.append(gpa, if (name.len == 0) "DEBUG" else name); i = arg.len; }, 'g' => self.debug_info = true, 'h' => { if (readNumber(arg, &i)) |digits| { self.page_length = digits; } else { diag.page_length_missing = true; } }, 'i' => self.ignore_case = true, 'I' => { try self.include_paths.append(gpa, arg[i..]); i = arg.len; }, 'l' => { if (i < arg.len) { self.listing_path = arg[i..]; saw_listing_path = true; i = arg.len; } else { self.list_pass2 = true; } }, 'm' => self.list_macros = true, 'n' => self.optimize = false, 'o' => { // `-o` with nothing after it is ignored, leaving the // object file with the name the source gives it. if (i < arg.len) self.output_path = arg[i..]; i = arg.len; }, 'p' => self.list_pass1 = true, 'q' => self.quiet = true, 's' => { self.format = if (readNumber(arg, &i)) |n| (if (n == 2) .intel_hex else .s_records) else .s_records; }, 't' => self.symbol_table = true, 'u' => self.usage_map = true, 'v' => self.verbose = true, 'w' => self.width = readNumber(arg, &i) orelse 131, 'x' => { const which = readNumber(arg, &i) orelse 2; self.cpu = if (which == 1) .mos6502x else .g65sc02; }, 'z' => self.zero_fill = true, '?' => return error.HelpRequested, else => { diag.set("Unknown option -{c}.", .{c}); return error.UnknownOption; }, } } }
if (self.input.len == 0) return error.NoInputFile;
// Naming a listing file is enough on its own to ask for a pass 2 listing, // unless a pass 1 listing or a symbol table was also asked for, in which // case a second `-l` is needed to get one. if (saw_listing_path and !self.list_pass1 and !self.symbol_table) { self.list_pass2 = true; } // A page shorter than twenty lines is treated as twenty; only `-h0`, // meaning no paging at all, escapes the floor. if (self.page_length != 0 and self.page_length < 20) self.page_length = 20; if (self.width < 60) self.width = 60; if (self.width > 200) self.width = 200; return self;}
fn readNumber(arg: []const u8, i: *usize) ?u32 { var value: u32 = 0; var any = false; while (i.* < arg.len and std.ascii.isDigit(arg[i.*])) { value = value *| 10 +| (arg[i.*] - '0'); i.* += 1; any = true; } return if (any) value else null;}
const testing = std.testing;
fn parseTest(args: []const []const u8) !Options { var diag = Diagnostic{}; return parse(testing.allocator, args, null, &diag);}
test "letters may be run together" { var o = try parseTest(&.{ "-xcq", "in.a65" }); defer o.deinit(testing.allocator); try testing.expectEqual(isa.Cpu.g65sc02, o.cpu); try testing.expect(o.cycles and o.quiet); try testing.expectEqualStrings("in.a65", o.input);}
test "-x1 and -x2 name different processors, and the last one wins" { var a = try parseTest(&.{ "-x1", "in.a65" }); defer a.deinit(testing.allocator); try testing.expectEqual(isa.Cpu.mos6502x, a.cpu); var b = try parseTest(&.{ "-x1", "-x2", "in.a65" }); defer b.deinit(testing.allocator); try testing.expectEqual(isa.Cpu.g65sc02, b.cpu);}
test "a numeric option may be followed by more options" { var o = try parseTest(&.{ "-h80t", "in.a65" }); defer o.deinit(testing.allocator); try testing.expectEqual(@as(u32, 80), o.page_length); try testing.expect(o.symbol_table);}
test "a string option swallows the rest of the argument" { var o = try parseTest(&.{ "-tlfile", "in.a65" }); defer o.deinit(testing.allocator); try testing.expect(o.symbol_table); try testing.expectEqualStrings("file", o.listing_path.?); // -t was asked for, so the named listing file does not by itself imply a // pass 2 listing. try testing.expect(!o.list_pass2);}
test "a listing file on its own implies a pass 2 listing" { var o = try parseTest(&.{ "-lout.lst", "in.a65" }); defer o.deinit(testing.allocator); try testing.expect(o.list_pass2);}
test "-d with no name defines DEBUG" { var o = try parseTest(&.{ "-d", "in.a65" }); defer o.deinit(testing.allocator); try testing.expectEqualStrings("DEBUG", o.defines.items[o.defines.items.len - 1]);}
test "a page shorter than twenty lines is raised to twenty" { var a = try parseTest(&.{ "-h12", "in.a65" }); defer a.deinit(testing.allocator); try testing.expectEqual(@as(u32, 20), a.page_length); var b = try parseTest(&.{ "-h0", "in.a65" }); defer b.deinit(testing.allocator); try testing.expectEqual(@as(u32, 0), b.page_length);}
test "-h with no number says so and changes nothing" { var diag = Diagnostic{}; var o = try parse(testing.allocator, &.{ "-h30", "-h", "in.a65" }, null, &diag); defer o.deinit(testing.allocator); try testing.expect(diag.page_length_missing); try testing.expectEqual(@as(u32, 30), o.page_length);}
test "-o with no name leaves the object file its default name" { var o = try parseTest(&.{ "-o", "in.a65" }); defer o.deinit(testing.allocator); try testing.expectEqual(@as(?[]const u8, null), o.output_path);}
test "-s2 selects intel-hex, -s alone selects s-records" { var a = try parseTest(&.{ "-s2", "in.a65" }); defer a.deinit(testing.allocator); try testing.expectEqual(OutputFormat.intel_hex, a.format); var b = try parseTest(&.{ "-s", "in.a65" }); defer b.deinit(testing.allocator); try testing.expectEqual(OutputFormat.s_records, b.format);}
test "an unknown option is named in the diagnostic" { var diag = Diagnostic{}; try testing.expectError( error.UnknownOption, parse(testing.allocator, &.{ "-j", "in.a65" }, null, &diag), ); try testing.expectEqualStrings("Unknown option -j.", diag.text());}