// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // 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()); }