450 lines
16 KiB
Zig
450 lines
16 KiB
Zig
//! Programmer mode evaluator for Tally.
|
|
//!
|
|
//! All operations use exact integer arithmetic (u128 storage), with results
|
|
//! masked to the configured bit width. No floating-point involved.
|
|
//! Produces Integer values with signed/unsigned interpretation.
|
|
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const ast = @import("ast.zig");
|
|
const Expr = ast.Expr;
|
|
const BinaryOp = ast.BinaryOp;
|
|
const types = @import("types.zig");
|
|
const Integer = types.Integer;
|
|
const ProgrammerConfig = types.ProgrammerConfig;
|
|
const CalcError = types.CalcError;
|
|
const parser_mod = @import("parser.zig");
|
|
const Parser = parser_mod.Parser;
|
|
|
|
/// Evaluate an AST in programmer mode, producing an exact integer result.
|
|
pub fn evalProgrammer(config: ProgrammerConfig, expr: *const Expr) CalcError!Integer {
|
|
const raw = try evalExpr(config, expr);
|
|
return .{
|
|
.raw = raw & config.bit_width.mask(),
|
|
.bit_width = config.bit_width,
|
|
.signedness = config.signedness,
|
|
};
|
|
}
|
|
|
|
/// Recursively evaluate an expression to a raw u128.
|
|
fn evalExpr(config: ProgrammerConfig, expr: *const Expr) CalcError!u128 {
|
|
switch (expr.*) {
|
|
.number => |n| {
|
|
if (n.int_value) |int_val| {
|
|
return int_val & config.bit_width.mask();
|
|
}
|
|
// Float literal in programmer mode: truncate to integer.
|
|
// (Number literals are always non-negative; unary minus is a
|
|
// separate operator handled below.)
|
|
const val: u128 = @intFromFloat(n.float_value);
|
|
return val & config.bit_width.mask();
|
|
},
|
|
.string_literal => |text| {
|
|
// Pack ASCII bytes into integer.
|
|
// Big-endian packing: first char -> most significant used byte.
|
|
const max_bytes = @as(usize, config.bit_width.bits()) / 8;
|
|
if (text.len > max_bytes) return CalcError.Overflow;
|
|
var result: u128 = 0;
|
|
for (text) |byte| {
|
|
if (byte > 0x7F) return CalcError.InvalidNumber;
|
|
result = (result << 8) | byte;
|
|
}
|
|
return result & config.bit_width.mask();
|
|
},
|
|
.variable => {
|
|
return CalcError.UnknownVariable;
|
|
},
|
|
.assignment => {
|
|
return CalcError.InvalidOperandType;
|
|
},
|
|
.unary => |u| {
|
|
const operand = try evalExpr(config, u.operand);
|
|
return switch (u.op) {
|
|
.negate => blk: {
|
|
// Two's complement negation: ~x + 1, masked to width
|
|
const negated = (~operand +% 1) & config.bit_width.mask();
|
|
break :blk negated;
|
|
},
|
|
.bitwise_not => (~operand) & config.bit_width.mask(),
|
|
};
|
|
},
|
|
.binary => |b| {
|
|
const left = try evalExpr(config, b.left);
|
|
const right = try evalExpr(config, b.right);
|
|
return evalBinaryOp(config, b.op, left, right);
|
|
},
|
|
.call => {
|
|
// No function calls in programmer mode
|
|
return CalcError.UnknownFunction;
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Evaluate a binary operation on two u128 values, masked to bit width.
|
|
fn evalBinaryOp(config: ProgrammerConfig, op: BinaryOp, left: u128, right: u128) CalcError!u128 {
|
|
const mask = config.bit_width.mask();
|
|
const width = config.bit_width.bits();
|
|
|
|
const result: u128 = switch (op) {
|
|
.add => (left +% right) & mask,
|
|
.sub => (left -% right) & mask,
|
|
.mul => (left *% right) & mask,
|
|
.div => blk: {
|
|
if (right == 0) return CalcError.DivisionByZero;
|
|
break :blk (left / right) & mask;
|
|
},
|
|
.mod => blk: {
|
|
if (right == 0) return CalcError.DivisionByZero;
|
|
break :blk (left % right) & mask;
|
|
},
|
|
.pow => blk: {
|
|
// Integer exponentiation
|
|
var base = left;
|
|
var exp = right;
|
|
var acc: u128 = 1;
|
|
while (exp > 0) : (exp >>= 1) {
|
|
if (exp & 1 != 0) acc = (acc *% base) & mask;
|
|
base = (base *% base) & mask;
|
|
}
|
|
break :blk acc;
|
|
},
|
|
.bit_and => left & right,
|
|
.bit_or => left | right,
|
|
.bit_xor => left ^ right,
|
|
.shift_left => blk: {
|
|
const shift_amt: u7 = if (right >= width)
|
|
@intCast(width - 1)
|
|
else
|
|
@intCast(right);
|
|
break :blk (left << shift_amt) & mask;
|
|
},
|
|
.shift_right => blk: {
|
|
// Arithmetic right shift: preserves sign bit
|
|
const shift_amt: u7 = if (right >= width)
|
|
@intCast(width - 1)
|
|
else
|
|
@intCast(right);
|
|
// Sign-extend, shift, then mask
|
|
const sign_bit: u128 = @as(u128, 1) << @intCast(width - 1);
|
|
if (left & sign_bit != 0) {
|
|
// Negative: fill with 1s from the top
|
|
const extended = left | ~mask;
|
|
const shifted: u128 = @bitCast(@as(i128, @bitCast(extended)) >> shift_amt);
|
|
break :blk shifted & mask;
|
|
}
|
|
break :blk (left >> shift_amt) & mask;
|
|
},
|
|
.shift_right_logical => blk: {
|
|
// Logical right shift: always fills with 0s
|
|
const shift_amt: u7 = if (right >= width)
|
|
@intCast(width - 1)
|
|
else
|
|
@intCast(right);
|
|
break :blk (left >> shift_amt) & mask;
|
|
},
|
|
.rotate_left => blk: {
|
|
const amt: u7 = @intCast(@mod(right, width));
|
|
if (amt == 0) break :blk left;
|
|
const anti: u7 = @intCast(width - amt);
|
|
break :blk ((left << amt) | (left >> anti)) & mask;
|
|
},
|
|
.rotate_right => blk: {
|
|
const amt: u7 = @intCast(@mod(right, width));
|
|
if (amt == 0) break :blk left;
|
|
const anti: u7 = @intCast(width - amt);
|
|
break :blk ((left >> amt) | (left << anti)) & mask;
|
|
},
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
/// High-level: parse and evaluate a string in programmer mode.
|
|
pub fn evalProgrammerString(allocator: Allocator, source: []const u8, config: ProgrammerConfig) CalcError!Integer {
|
|
var p = Parser.init(allocator, source, .programmer);
|
|
const expr = try p.parse();
|
|
return evalProgrammer(config, expr);
|
|
}
|
|
|
|
// -- Tests --
|
|
|
|
const testing = std.testing;
|
|
|
|
fn testProg(source: []const u8) !Integer {
|
|
return testProgWith(source, .{});
|
|
}
|
|
|
|
fn testProgWith(source: []const u8, config: ProgrammerConfig) !Integer {
|
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|
defer _ = arena.deinit();
|
|
return evalProgrammerString(arena.allocator(), source, config);
|
|
}
|
|
|
|
test "prog: simple number" {
|
|
const result = try testProg("42");
|
|
try testing.expectEqual(@as(u128, 42), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: hex number" {
|
|
const result = try testProg("0xFF");
|
|
try testing.expectEqual(@as(u128, 255), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: binary number" {
|
|
const result = try testProg("0b1010");
|
|
try testing.expectEqual(@as(u128, 10), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: addition" {
|
|
const result = try testProg("10 + 20");
|
|
try testing.expectEqual(@as(u128, 30), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: subtraction wrapping" {
|
|
// 5 - 10 in 8-bit unsigned wraps
|
|
const result = try testProgWith("5 - 10", .{ .bit_width = .bits8 });
|
|
try testing.expectEqual(@as(u128, 251), result.unsignedValue()); // 256 - 5
|
|
try testing.expectEqual(@as(i128, -5), result.signedValue());
|
|
}
|
|
|
|
test "prog: multiplication" {
|
|
const result = try testProg("6 * 7");
|
|
try testing.expectEqual(@as(u128, 42), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: multiplication overflow 8-bit" {
|
|
const result = try testProgWith("200 * 2", .{ .bit_width = .bits8 });
|
|
// 400 & 0xFF = 144
|
|
try testing.expectEqual(@as(u128, 144), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: division" {
|
|
const result = try testProg("100 / 4");
|
|
try testing.expectEqual(@as(u128, 25), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: division by zero" {
|
|
const result = testProg("10 / 0");
|
|
try testing.expectError(CalcError.DivisionByZero, result);
|
|
}
|
|
|
|
test "prog: modulo" {
|
|
const result = try testProg("10 % 3");
|
|
try testing.expectEqual(@as(u128, 1), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: power" {
|
|
const result = try testProg("2 ** 10");
|
|
try testing.expectEqual(@as(u128, 1024), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: bitwise AND" {
|
|
const result = try testProg("0xFF & 0x0F");
|
|
try testing.expectEqual(@as(u128, 0x0F), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: bitwise OR" {
|
|
const result = try testProg("0xF0 | 0x0F");
|
|
try testing.expectEqual(@as(u128, 0xFF), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: bitwise XOR" {
|
|
const result = try testProg("0xFF xor 0x0F");
|
|
try testing.expectEqual(@as(u128, 0xF0), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: caret is power not XOR" {
|
|
// 0x2 ^ 0x3 = 2^3 = 8 (power), NOT 1 (XOR)
|
|
const result = try testProg("0x2 ^ 0x3");
|
|
try testing.expectEqual(@as(u128, 8), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: and/or/not keywords" {
|
|
const a = try testProg("0xFF and 0x0F");
|
|
try testing.expectEqual(@as(u128, 0x0F), a.unsignedValue());
|
|
const o = try testProg("0xF0 or 0x0F");
|
|
try testing.expectEqual(@as(u128, 0xFF), o.unsignedValue());
|
|
const n = try testProgWith("not 0x0F", .{ .bit_width = .bits8 });
|
|
try testing.expectEqual(@as(u128, 0xF0), n.unsignedValue());
|
|
}
|
|
|
|
test "prog: bitwise NOT 8-bit" {
|
|
const result = try testProgWith("~0x0F", .{ .bit_width = .bits8 });
|
|
try testing.expectEqual(@as(u128, 0xF0), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: bitwise NOT 16-bit" {
|
|
const result = try testProgWith("~0x00FF", .{ .bit_width = .bits16 });
|
|
try testing.expectEqual(@as(u128, 0xFF00), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: bitwise NOT 32-bit" {
|
|
const result = try testProgWith("~0", .{ .bit_width = .bits32 });
|
|
try testing.expectEqual(@as(u128, 0xFFFF_FFFF), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: shift left" {
|
|
const result = try testProg("1 << 8");
|
|
try testing.expectEqual(@as(u128, 256), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: shift left overflow 8-bit" {
|
|
const result = try testProgWith("1 << 8", .{ .bit_width = .bits8 });
|
|
// Shifting by width or more in 8-bit: shift_amt clamped to 7
|
|
try testing.expectEqual(@as(u128, 128), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: logical shift right" {
|
|
const result = try testProgWith("0x80 >>> 4", .{ .bit_width = .bits8 });
|
|
try testing.expectEqual(@as(u128, 0x08), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: arithmetic shift right (sign bit preserved)" {
|
|
// 0x80 in 8-bit is -128; >> 1 should give 0xC0 (-64)
|
|
const result = try testProgWith("0x80 >> 1", .{ .bit_width = .bits8 });
|
|
try testing.expectEqual(@as(u128, 0xC0), result.unsignedValue());
|
|
try testing.expectEqual(@as(i128, -64), result.signedValue());
|
|
}
|
|
|
|
test "prog: arithmetic shift right (positive)" {
|
|
// 0x40 in 8-bit is positive; >> 1 should give 0x20
|
|
const result = try testProgWith("0x40 >> 1", .{ .bit_width = .bits8 });
|
|
try testing.expectEqual(@as(u128, 0x20), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: rotate left 8-bit" {
|
|
// 0x81 rol 1 in 8-bit: bit 7 wraps to bit 0 -> 0x03
|
|
const result = try testProgWith("0x81 rol 1", .{ .bit_width = .bits8 });
|
|
try testing.expectEqual(@as(u128, 0x03), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: rotate right 8-bit" {
|
|
// 0x81 ror 1 in 8-bit: bit 0 wraps to bit 7 -> 0xC0
|
|
const result = try testProgWith("0x81 ror 1", .{ .bit_width = .bits8 });
|
|
try testing.expectEqual(@as(u128, 0xC0), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: negation two's complement" {
|
|
const result = try testProgWith("-1", .{ .bit_width = .bits8 });
|
|
try testing.expectEqual(@as(u128, 0xFF), result.unsignedValue());
|
|
try testing.expectEqual(@as(i128, -1), result.signedValue());
|
|
}
|
|
|
|
test "prog: negation 16-bit" {
|
|
const result = try testProgWith("-42", .{ .bit_width = .bits16 });
|
|
try testing.expectEqual(@as(i128, -42), result.signedValue());
|
|
}
|
|
|
|
test "prog: complex expression" {
|
|
// (0xFF & 0x0F) | (1 << 4) = 0x0F | 0x10 = 0x1F
|
|
const result = try testProg("(0xFF & 0x0F) | (1 << 4)");
|
|
try testing.expectEqual(@as(u128, 0x1F), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: precedence AND before OR" {
|
|
// 0xF0 | 0xFF & 0x0F = 0xF0 | (0xFF & 0x0F) = 0xF0 | 0x0F = 0xFF
|
|
const result = try testProg("0xF0 | 0xFF & 0x0F");
|
|
try testing.expectEqual(@as(u128, 0xFF), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: chained shifts" {
|
|
const result = try testProg("1 << 4 << 2");
|
|
// Left-associative: (1 << 4) << 2 = 16 << 2 = 64
|
|
try testing.expectEqual(@as(u128, 64), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: mask applied to input" {
|
|
// 0x1FF in 8-bit mode should be masked to 0xFF
|
|
const result = try testProgWith("0x1FF", .{ .bit_width = .bits8 });
|
|
try testing.expectEqual(@as(u128, 0xFF), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: 32-bit operations" {
|
|
const result = try testProgWith("0xFFFF_FFFF + 1", .{ .bit_width = .bits32 });
|
|
try testing.expectEqual(@as(u128, 0), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: 64-bit max" {
|
|
const result = try testProgWith("~0", .{ .bit_width = .bits64 });
|
|
try testing.expectEqual(@as(u128, 0xFFFF_FFFF_FFFF_FFFF), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: negative float input" {
|
|
// -5.0 as a float in programmer mode should become two's complement
|
|
const result = try testProgWith("-5", .{ .bit_width = .bits8 });
|
|
try testing.expectEqual(@as(i128, -5), result.signedValue());
|
|
}
|
|
|
|
test "prog: variable reference errors" {
|
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|
defer _ = arena.deinit();
|
|
const result = evalProgrammerString(arena.allocator(), "x", .{});
|
|
try testing.expectError(CalcError.UnknownVariable, result);
|
|
}
|
|
|
|
test "prog: assignment errors" {
|
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|
defer _ = arena.deinit();
|
|
const result = evalProgrammerString(arena.allocator(), "X = 5", .{});
|
|
try testing.expectError(CalcError.InvalidOperandType, result);
|
|
}
|
|
|
|
test "prog: function call errors" {
|
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|
defer _ = arena.deinit();
|
|
const result = evalProgrammerString(arena.allocator(), "sin(1)", .{});
|
|
try testing.expectError(CalcError.UnknownFunction, result);
|
|
}
|
|
|
|
test "prog: ASCII literal single char" {
|
|
const result = try testProg("'A'");
|
|
try testing.expectEqual(@as(u128, 0x41), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: ASCII literal multi char" {
|
|
const result = try testProg("'ELF'");
|
|
try testing.expectEqual(@as(u128, 0x454C46), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: ASCII literal full word" {
|
|
const result = try testProg("'ascii'");
|
|
try testing.expectEqual(@as(u128, 0x6173636969), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: ASCII literal in expression" {
|
|
const result = try testProg("'A' | 0x20");
|
|
// 0x41 | 0x20 = 0x61 = 'a'
|
|
try testing.expectEqual(@as(u128, 0x61), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: ASCII literal overflow 8-bit" {
|
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|
defer _ = arena.deinit();
|
|
const result = evalProgrammerString(arena.allocator(), "'AB'", .{ .bit_width = .bits8 });
|
|
try testing.expectError(CalcError.Overflow, result);
|
|
}
|
|
|
|
test "prog: float literal truncates to integer" {
|
|
// 3.14 has no int_value, so the float branch truncates to 3
|
|
const result = try testProg("3.14");
|
|
try testing.expectEqual(@as(u128, 3), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: arithmetic shift right amount >= width clamps" {
|
|
// 0xFF in 8-bit is negative; >> 20 clamps shift to width-1 (7),
|
|
// arithmetic shift fills with sign bit -> stays 0xFF
|
|
const result = try testProgWith("0xFF >> 20", .{ .bit_width = .bits8 });
|
|
try testing.expectEqual(@as(u128, 0xFF), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: logical shift right amount >= width clamps" {
|
|
// 0xFF in 8-bit; >>> 20 clamps shift to width-1 (7) -> 0x01
|
|
const result = try testProgWith("0xFF >>> 20", .{ .bit_width = .bits8 });
|
|
try testing.expectEqual(@as(u128, 0x01), result.unsignedValue());
|
|
}
|
|
|
|
test "prog: arithmetic shift right amount >= width positive value" {
|
|
// 0x40 in 8-bit is positive; >> 20 clamps to 7 -> 0
|
|
const result = try testProgWith("0x40 >> 20", .{ .bit_width = .bits8 });
|
|
try testing.expectEqual(@as(u128, 0), result.unsignedValue());
|
|
}
|