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