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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Character-level scanning of one source line.//!//! `as65` is line-oriented: nothing but a macro body ever spans a line, so a//! scanner that never looks past the end of the string it was handed is enough//! for the whole language. Everything above this — the expression parser, the//! operand parser, the pseudo-instructions — drives a `Scanner` forward and//! asks it what comes next.
const std = @import("std");
const Scanner = @This();
src: []const u8,pos: usize = 0,
pub fn init(src: []const u8) Scanner { return .{ .src = src };}
/// True for the characters `as65` accepts inside a label.////// Probing as65 shows it takes letters, digits, `_`, `.` and `?`, and that a/// label may begin with any of them; `f$` and `h#` are rejected as mnemonics/// because the scanner simply stops at the punctuation.pub fn isIdentChar(c: u8) bool { return std.ascii.isAlphanumeric(c) or c == '_' or c == '.' or c == '?';}
pub fn peek(self: Scanner) ?u8 { return if (self.pos < self.src.len) self.src[self.pos] else null;}
pub fn peekAt(self: Scanner, offset: usize) ?u8 { const i = self.pos + offset; return if (i < self.src.len) self.src[i] else null;}
pub fn advance(self: *Scanner) void { if (self.pos < self.src.len) self.pos += 1;}
pub fn skipSpace(self: *Scanner) void { while (self.peek()) |c| { // Anything below a space is white space to as65, not just the tab: a // stray control character in the middle of a line is stepped over as // if it were not there. if (c <= ' ') self.advance() else break; }}
/// True once nothing but a comment or blanks remain.pub fn atEnd(self: *Scanner) bool { self.skipSpace(); const c = self.peek() orelse return true; return c == ';';}
/// Consume `text` if it is next, and report whether it was.pub fn accept(self: *Scanner, text: []const u8) bool { if (self.pos + text.len > self.src.len) return false; if (!std.mem.eql(u8, self.src[self.pos..][0..text.len], text)) return false; self.pos += text.len; return true;}
/// Consume `text` if it is next and is not glued to an identifier character,/// so that the `hi` in `hi_score` is not mistaken for the `hi` operator.pub fn acceptWord(self: *Scanner, text: []const u8) bool { if (self.pos + text.len > self.src.len) return false; if (!std.mem.eql(u8, self.src[self.pos..][0..text.len], text)) return false; if (self.pos + text.len < self.src.len and isIdentChar(self.src[self.pos + text.len])) return false; self.pos += text.len; return true;}
/// Consume a single character if it is next.pub fn acceptChar(self: *Scanner, c: u8) bool { if (self.peek() == c) { self.advance(); return true; } return false;}
/// Read an identifier, or return null and stay put if one is not next.pub fn readIdentifier(self: *Scanner) ?[]const u8 { const start = self.pos; while (self.peek()) |c| { if (!isIdentChar(c)) break; self.advance(); } return if (self.pos == start) null else self.src[start..self.pos];}
/// What remains of the line, comment included.pub fn rest(self: Scanner) []const u8 { return self.src[self.pos..];}
test "identifier scanning stops at punctuation" { var s = Scanner.init("a.b?_1$rest"); try std.testing.expectEqualStrings("a.b?_1", s.readIdentifier().?); try std.testing.expectEqualStrings("$rest", s.rest());}
test "acceptWord will not split an identifier" { var s = Scanner.init("hi_score"); try std.testing.expect(!s.acceptWord("hi")); var t = Scanner.init("hi 5"); try std.testing.expect(t.acceptWord("hi"));}
test "atEnd treats a comment as the end of the line" { var s = Scanner.init(" ; a comment"); try std.testing.expect(s.atEnd()); var t = Scanner.init(" x"); try std.testing.expect(!t.atEnd());}