// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! The expression language. //! //! `as65` evaluates in 32-bit two's complement throughout, so every value here //! is an `i32` and every operator wraps rather than trapping. The precedence //! ladder below is the one the manual gives, read from the bottom of this file //! upwards: `hi` and `lo` bind loosest of all, which is why `hi $12ff+2` is //! `hi ($1301)` and not `($12)+2`. const std = @import("std"); const Scanner = @import("Scanner.zig"); pub const Value = i32; pub const Error = error{ SyntaxError, InvalidFormat, DivisionByZero, ValueOutOfRange, }; /// Read a backslash escape, having already consumed the backslash. /// /// `as65` knows the usual C escapes and maps `\0` through `\9` to the digit's /// own value rather than treating them as the start of an octal run; `\x` is /// zero, since the `\x` form the manual disclaims was never wired up. /// Anything else stands for itself. pub fn escapeChar(c: u8) u8 { return switch (c) { 'a' => 0x07, 'b' => 0x08, 'e' => 0x1b, 'f' => 0x0c, 'n' => 0x0a, 'r' => 0x0d, 't' => 0x09, 'v' => 0x0b, 'x' => 0x00, '0'...'9' => c - '0', else => c, }; } /// Read a quoted string, resolving escapes into `buf`. /// /// The opening quote has not been consumed yet. Returns the decoded bytes, /// which alias `buf`. pub fn readString(scanner: *Scanner, buf: []u8) Error![]const u8 { const quote = scanner.peek() orelse return error.SyntaxError; if (quote != '"' and quote != '\'') return error.SyntaxError; scanner.advance(); var len: usize = 0; while (true) { const c = scanner.peek() orelse return error.InvalidFormat; scanner.advance(); if (c == quote) break; if (len == buf.len) return error.InvalidFormat; if (c == '\\') { const esc = scanner.peek() orelse return error.InvalidFormat; scanner.advance(); buf[len] = escapeChar(esc); } else { buf[len] = c; } len += 1; } return buf[0..len]; } /// Pack a string used as a number, most significant byte first. /// /// One, two and four characters become a byte, a word and a long. Three is not /// a size the assembler has, and neither is five or more, so both are refused /// the way as65 refuses them. pub fn packString(bytes: []const u8) Error!Value { if (bytes.len != 1 and bytes.len != 2 and bytes.len != 4) return error.ValueOutOfRange; var v: u32 = 0; for (bytes) |b| v = (v << 8) | b; return @bitCast(v); } fn digitValue(c: u8) ?u32 { return switch (c) { '0'...'9' => c - '0', 'a'...'z' => c - 'a' + 10, 'A'...'Z' => c - 'A' + 10, else => null, }; } fn readDigits(scanner: *Scanner, base: u32) Error!Value { var v: u32 = 0; var any = false; while (scanner.peek()) |c| { const d = digitValue(c) orelse break; if (d >= base) break; v = v *% base +% d; any = true; scanner.advance(); } if (!any) return error.SyntaxError; return @bitCast(v); } fn hasDigit(scanner: Scanner, base: u32) bool { const c = scanner.peekAt(1) orelse return false; const d = digitValue(c) orelse return false; return d < base; } /// Parse and evaluate one expression. /// /// `ctx` supplies the two things an expression can reach outside itself: the /// location counter, through `ctx.locationCounter()`, and label values, through /// `ctx.lookupSymbol(name)`. A `null` from the latter means the label is not /// known yet; the expression evaluates as if it were zero, and it is the /// caller's business to decide whether that is a forward reference or a /// mistake. pub fn parse(scanner: *Scanner, ctx: anytype) Error!Value { const value = try parseHiLo(scanner, ctx); // One of those characters where the expression has finished is as65's // "Invalid format" rather than a plain syntax error, wherever it turns up: // `[1]`, `1]` and `$[1` all say the same thing. var after = scanner.*; after.skipSpace(); if (after.peek()) |c| if (refusedOutright(c)) return error.InvalidFormat; return value; } /// The characters as65 will not have in an expression at all: the four /// brackets, and anything from `del` up. fn refusedOutright(c: u8) bool { return c == '[' or c == ']' or c == '{' or c == '}' or c >= 0x7f; } fn parseHiLo(scanner: *Scanner, ctx: anytype) Error!Value { scanner.skipSpace(); if (scanner.acceptWord("hi")) { const v = try parseLogicalOr(scanner, ctx); return (v >> 8) & 0xff; } if (scanner.acceptWord("lo")) { const v = try parseLogicalOr(scanner, ctx); return v & 0xff; } return parseLogicalOr(scanner, ctx); } fn parseLogicalOr(scanner: *Scanner, ctx: anytype) Error!Value { var lhs = try parseLogicalAnd(scanner, ctx); while (true) { scanner.skipSpace(); if (!scanner.accept("||")) return lhs; const rhs = try parseLogicalAnd(scanner, ctx); lhs = @intFromBool(lhs != 0 or rhs != 0); } } fn parseLogicalAnd(scanner: *Scanner, ctx: anytype) Error!Value { var lhs = try parseBitOr(scanner, ctx); while (true) { scanner.skipSpace(); if (!scanner.accept("&&")) return lhs; const rhs = try parseBitOr(scanner, ctx); lhs = @intFromBool(lhs != 0 and rhs != 0); } } fn parseBitOr(scanner: *Scanner, ctx: anytype) Error!Value { var lhs = try parseBitXor(scanner, ctx); while (true) { scanner.skipSpace(); if (scanner.peek() != '|' or scanner.peekAt(1) == '|') return lhs; scanner.advance(); lhs |= try parseBitXor(scanner, ctx); } } fn parseBitXor(scanner: *Scanner, ctx: anytype) Error!Value { var lhs = try parseBitAnd(scanner, ctx); while (true) { scanner.skipSpace(); if (!scanner.acceptChar('^')) return lhs; lhs ^= try parseBitAnd(scanner, ctx); } } fn parseBitAnd(scanner: *Scanner, ctx: anytype) Error!Value { var lhs = try parseEquality(scanner, ctx); while (true) { scanner.skipSpace(); if (scanner.peek() != '&' or scanner.peekAt(1) == '&') return lhs; scanner.advance(); lhs &= try parseEquality(scanner, ctx); } } fn parseEquality(scanner: *Scanner, ctx: anytype) Error!Value { var lhs = try parseRelational(scanner, ctx); while (true) { scanner.skipSpace(); if (scanner.accept("!=")) { lhs = @intFromBool(lhs != try parseRelational(scanner, ctx)); } else if (scanner.peek() == '=' and scanner.peekAt(1) != '=') { scanner.advance(); lhs = @intFromBool(lhs == try parseRelational(scanner, ctx)); } else return lhs; } } fn parseRelational(scanner: *Scanner, ctx: anytype) Error!Value { var lhs = try parseShift(scanner, ctx); while (true) { scanner.skipSpace(); if (scanner.accept("<=")) { lhs = @intFromBool(lhs <= try parseShift(scanner, ctx)); } else if (scanner.accept(">=")) { lhs = @intFromBool(lhs >= try parseShift(scanner, ctx)); } else if (scanner.peek() == '<' and scanner.peekAt(1) != '<') { scanner.advance(); lhs = @intFromBool(lhs < try parseShift(scanner, ctx)); } else if (scanner.peek() == '>' and scanner.peekAt(1) != '>') { scanner.advance(); lhs = @intFromBool(lhs > try parseShift(scanner, ctx)); } else return lhs; } } fn parseShift(scanner: *Scanner, ctx: anytype) Error!Value { var lhs = try parseAdditive(scanner, ctx); while (true) { scanner.skipSpace(); if (scanner.accept("<<")) { lhs = shiftLeft(lhs, try parseAdditive(scanner, ctx)); } else if (scanner.accept(">>")) { lhs = shiftRight(lhs, try parseAdditive(scanner, ctx)); } else return lhs; } } fn shiftLeft(lhs: Value, rhs: Value) Value { if (rhs < 0 or rhs > 31) return 0; return lhs << @intCast(rhs); } /// An arithmetic shift: `-1 >> 1` stays `-1`, as it does in as65. fn shiftRight(lhs: Value, rhs: Value) Value { if (rhs < 0) return 0; if (rhs > 31) return lhs >> 31; return lhs >> @intCast(rhs); } fn parseAdditive(scanner: *Scanner, ctx: anytype) Error!Value { var lhs = try parseMultiplicative(scanner, ctx); while (true) { scanner.skipSpace(); if (scanner.acceptChar('+')) { lhs +%= try parseMultiplicative(scanner, ctx); } else if (scanner.acceptChar('-')) { lhs -%= try parseMultiplicative(scanner, ctx); } else return lhs; } } fn parseMultiplicative(scanner: *Scanner, ctx: anytype) Error!Value { var lhs = try parseUnary(scanner, ctx); while (true) { scanner.skipSpace(); if (scanner.acceptChar('*')) { lhs *%= try parseUnary(scanner, ctx); } else if (scanner.acceptChar('/')) { const rhs = try parseUnary(scanner, ctx); if (rhs == 0) return error.DivisionByZero; lhs = std.math.divTrunc(Value, lhs, rhs) catch lhs; } else if (scanner.peek() == '%') { // A `%` here is the modulo operator; the binary-literal prefix of // the same spelling can only appear where an operand is expected, // which is a place this loop never looks. scanner.advance(); const rhs = try parseUnary(scanner, ctx); if (rhs == 0) return error.DivisionByZero; lhs = std.math.rem(Value, lhs, rhs) catch lhs; } else return lhs; } } fn parseUnary(scanner: *Scanner, ctx: anytype) Error!Value { scanner.skipSpace(); if (scanner.acceptChar('-')) return -%try parseUnary(scanner, ctx); if (scanner.acceptChar('+')) return parseUnary(scanner, ctx); if (scanner.acceptChar('~')) return ~try parseUnary(scanner, ctx); if (scanner.peek() == '!' and scanner.peekAt(1) != '=') { scanner.advance(); return @intFromBool(try parseUnary(scanner, ctx) == 0); } return parsePrimary(scanner, ctx); } fn parsePrimary(scanner: *Scanner, ctx: anytype) Error!Value { scanner.skipSpace(); const c = scanner.peek() orelse return error.SyntaxError; switch (c) { '(' => { scanner.advance(); const v = try parseHiLo(scanner, ctx); scanner.skipSpace(); if (!scanner.acceptChar(')')) return error.SyntaxError; return v; }, // Both spellings of the location counter. `$` is only the counter when // no hex digit follows it, and `*` is only the counter in the operand // position this function occupies. '*' => { scanner.advance(); return ctx.locationCounter(); }, '$' => { if (hasDigit(scanner.*, 16)) { scanner.advance(); return readDigits(scanner, 16); } scanner.advance(); return ctx.locationCounter(); }, '%' => { if (!hasDigit(scanner.*, 2)) return error.SyntaxError; scanner.advance(); return readDigits(scanner, 2); }, '@' => { if (!hasDigit(scanner.*, 8)) return error.SyntaxError; scanner.advance(); return readDigits(scanner, 8); }, '"', '\'' => { var buf: [256]u8 = undefined; return packString(try readString(scanner, &buf)); }, '0'...'9' => return parseNumber(scanner), else => { if (refusedOutright(c)) return error.InvalidFormat; const name = scanner.readIdentifier() orelse return error.SyntaxError; // `a`, `x` and `y` name the registers, and a register is not a // value: as65 calls this a syntax error rather than looking for a // label. Only the small letters, and only on their own -- `Y` and // `y1` are labels like any other, whatever `-i` says. if (name.len == 1 and (name[0] == 'a' or name[0] == 'x' or name[0] == 'y')) { return error.SyntaxError; } // Nor are the two word operators values, so `hi lo $1234` is a // syntax error rather than `hi` of a label called `lo`. if (std.mem.eql(u8, name, "hi") or std.mem.eql(u8, name, "lo")) { return error.SyntaxError; } return ctx.lookupSymbol(name) orelse 0; }, } } /// Parse a number that begins with a decimal digit. /// /// The digit run is either a value in some base — `0x`, `0b`, or plain decimal /// — or the base itself, when a `#` follows: `36#zz` is 1305. fn parseNumber(scanner: *Scanner) Error!Value { if (scanner.peek() == '0') { const next = scanner.peekAt(1); if (next == 'x' or next == 'X') { scanner.advance(); scanner.advance(); return readDigits(scanner, 16); } if (next == 'b' or next == 'B') { // `0b1000` is binary, but `0b` must be followed by a binary digit // or it is just a zero with a label bumping into it. const after = scanner.peekAt(2) orelse 0; if (after == '0' or after == '1') { scanner.advance(); scanner.advance(); return readDigits(scanner, 2); } } } const start = scanner.pos; const decimal = try readDigits(scanner, 10); if (scanner.peek() == '#') { scanner.advance(); const base: u32 = @bitCast(decimal); _ = start; // `#` only knows bases two through thirty-six. if (base < 2 or base > 36) return error.InvalidFormat; return readDigits(scanner, base); } return decimal; } const TestCtx = struct { pc: Value = 0, pub fn locationCounter(self: *const TestCtx) Value { return self.pc; } pub fn lookupSymbol(_: *const TestCtx, _: []const u8) ?Value { return null; } }; fn eval(text: []const u8, pc: Value) !Value { var scanner = Scanner.init(text); const ctx = TestCtx{ .pc = pc }; return parse(&scanner, &ctx); } test "precedence follows the manual" { try std.testing.expectEqual(@as(Value, 11), try eval("3|24&8", 0)); try std.testing.expectEqual(@as(Value, 8), try eval("(3|24)&8", 0)); try std.testing.expectEqual(@as(Value, 0), try eval("4*(3>5)", 0)); try std.testing.expectEqual(@as(Value, 1), try eval("4*!3<5", 0)); try std.testing.expectEqual(@as(Value, 240), try eval("15<<4", 0)); } test "hi and lo bind loosest of all" { try std.testing.expectEqual(@as(Value, 0x13), try eval("hi 0x12ff+2", 0)); try std.testing.expectEqual(@as(Value, 0x01), try eval("lo 0x12ff+2", 0)); try std.testing.expectEqual(@as(Value, 0x14), try eval("(hi 0x12ff)+2", 0)); } test "the location counter is spelled two ways" { try std.testing.expectEqual(@as(Value, 0x50f0), try eval("5**", 0x1030)); try std.testing.expectEqual(@as(Value, 0x81c), try eval("2+*/2", 0x1034)); try std.testing.expectEqual(@as(Value, 0x102c), try eval("$", 0x102c)); try std.testing.expectEqual(@as(Value, 0x1000), try eval("$1000", 0)); } test "modulo and the binary prefix share a spelling" { try std.testing.expectEqual(@as(Value, 10), try eval("1234%12", 0)); try std.testing.expectEqual(@as(Value, 0x2a5), try eval("%1010100101", 0)); try std.testing.expectEqual(@as(Value, 46), try eval("1010101%%1010101", 0)); } test "arithmetic is signed, 32-bit and truncating" { try std.testing.expectEqual(@as(Value, -3), try eval("-10/3", 0)); try std.testing.expectEqual(@as(Value, -1), try eval("-10%3", 0)); try std.testing.expectEqual(@as(Value, -1), try eval("-1>>1", 0)); try std.testing.expectEqual(@as(Value, 0), try eval("-1/2", 0)); try std.testing.expectEqual(@as(Value, -1), try eval("~0", 0)); try std.testing.expectEqual(@as(Value, -1), try eval("$ffffffff", 0)); } // The manual's own worked examples for these two are miscalculated — it gives // 36#1000 as 444528 and 36#zz as 1305, where 36 cubed is 46656 and 35*36+35 is // 1295. as65 computes them correctly; the values below are what it prints. test "a base outside two through thirty-six is refused" { try std.testing.expectError(error.InvalidFormat, eval("37#1", 0)); try std.testing.expectError(error.InvalidFormat, eval("1#1", 0)); try std.testing.expectError(error.InvalidFormat, eval("0#1", 0)); } test "number bases" { try std.testing.expectEqual(@as(Value, 8), try eval("%1000", 0)); try std.testing.expectEqual(@as(Value, 512), try eval("@1000", 0)); try std.testing.expectEqual(@as(Value, 4096), try eval("$1000", 0)); try std.testing.expectEqual(@as(Value, 8), try eval("0b1000", 0)); try std.testing.expectEqual(@as(Value, 4096), try eval("0x1000", 0)); try std.testing.expectEqual(@as(Value, 64), try eval("4#1000", 0)); try std.testing.expectEqual(@as(Value, 46656), try eval("36#1000", 0)); try std.testing.expectEqual(@as(Value, 120), try eval("11#aa", 0)); try std.testing.expectEqual(@as(Value, 1295), try eval("36#zz", 0)); } test "strings are numbers of one, two or four characters" { try std.testing.expectEqual(@as(Value, 0x61), try eval("\"a\"", 0)); try std.testing.expectEqual(@as(Value, 0x7879), try eval("\"xy\"", 0)); try std.testing.expectEqual(@as(Value, 0x71756164), try eval("\"quad\"", 0)); try std.testing.expectError(error.ValueOutOfRange, eval("\"abc\"", 0)); try std.testing.expectError(error.ValueOutOfRange, eval("\"abcde\"", 0)); } test "division by zero is refused" { try std.testing.expectError(error.DivisionByZero, eval("8/0", 0)); try std.testing.expectError(error.DivisionByZero, eval("8%0", 0)); } test "comparison chains associate to the left" { try std.testing.expectEqual(@as(Value, 1), try eval("1<2<3", 0)); try std.testing.expectEqual(@as(Value, 1), try eval("1=1", 0)); } test "a register name is not a value" { try std.testing.expectError(error.SyntaxError, eval("a", 0)); try std.testing.expectError(error.SyntaxError, eval("y+1", 0)); try std.testing.expectError(error.SyntaxError, eval("1+x", 0)); try std.testing.expectError(error.SyntaxError, eval("(y)", 0)); // Only the small letters, and only on their own. try std.testing.expectEqual(@as(Value, 0), try eval("Y", 0)); try std.testing.expectEqual(@as(Value, 0), try eval("y1", 0)); } test "brackets and high characters are an invalid format" { try std.testing.expectError(error.InvalidFormat, eval("[1+2]*3", 0)); try std.testing.expectError(error.InvalidFormat, eval("]1", 0)); try std.testing.expectError(error.InvalidFormat, eval("1]", 0)); try std.testing.expectError(error.InvalidFormat, eval("{1}", 0)); try std.testing.expectError(error.InvalidFormat, eval("1\x7f", 0)); try std.testing.expectError(error.InvalidFormat, eval("\xe9", 0)); // A control character is white space, not a refusal. try std.testing.expectEqual(@as(Value, 1), try eval("\x01 1\x02", 0)); }