//! AST evaluator for Tally. //! //! Walks an AST and produces a Value. In standard mode, all computations //! use f64 floating-point arithmetic. In programmer mode, integer operations //! are exact (masked to bit width). The evaluator uses an Environment for //! variable storage, history, and configuration. const std = @import("std"); const math = std.math; const Allocator = std.mem.Allocator; const ast = @import("ast.zig"); const Expr = ast.Expr; const BinaryOp = ast.BinaryOp; const types = @import("types.zig"); const Mode = types.Mode; const ProgrammerConfig = types.ProgrammerConfig; const CalcError = types.CalcError; const parser_mod = @import("parser.zig"); const Parser = parser_mod.Parser; /// Evaluation environment holding variables, history, and config. pub const Environment = struct { allocator: Allocator, mode: Mode, programmer_config: ProgrammerConfig, variables: std.StringHashMap(f64), ans: f64, history_len: usize, pub fn init(allocator: Allocator, mode: Mode) Environment { return .{ .allocator = allocator, .mode = mode, .programmer_config = .{}, .variables = std.StringHashMap(f64).init(allocator), .ans = 0, .history_len = 0, }; } pub fn deinit(self: *Environment) void { self.variables.deinit(); } /// Set a variable value. pub fn setVar(self: *Environment, name: []const u8, value: f64) !void { try self.variables.put(name, value); } /// Get a variable or constant value. pub fn getVar(self: *const Environment, name: []const u8) ?f64 { // Built-in constants if (std.mem.eql(u8, name, "pi")) return math.pi; if (std.mem.eql(u8, name, "e")) return math.e; if (std.mem.eql(u8, name, "tau")) return math.tau; if (std.mem.eql(u8, name, "Ans") or std.mem.eql(u8, name, "ans")) return self.ans; return self.variables.get(name); } }; /// Evaluate a parsed expression in the given environment. /// Returns the computed value as f64 for standard mode. pub fn evaluate(env: *Environment, expr: *const Expr) CalcError!f64 { switch (expr.*) { .number => |n| return n.float_value, .variable => |name| { return env.getVar(name) orelse return CalcError.UnknownVariable; }, .assignment => |a| { const val = try evaluate(env, a.value); env.setVar(a.name, val) catch return CalcError.OutOfMemory; return val; }, .unary => |u| { const operand = try evaluate(env, u.operand); return switch (u.op) { .negate => -operand, .bitwise_not => { // In standard mode, bitwise not doesn't really make sense, // but we'll compute it on the integer representation const int_val: u64 = @bitCast(@as(i64, @intFromFloat(operand))); const result = ~int_val & env.programmer_config.bit_width.mask(); return @floatFromInt(@as(i64, @bitCast(result))); }, }; }, .binary => |b| { const left = try evaluate(env, b.left); const right = try evaluate(env, b.right); return evalBinaryOp(b.op, left, right); }, .call => |c| { return evalFunction(env, c.name, c.args); }, } } /// Evaluate a binary operation on two f64 values. fn evalBinaryOp(op: BinaryOp, left: f64, right: f64) CalcError!f64 { return switch (op) { .add => left + right, .sub => left - right, .mul => left * right, .div => if (right == 0) CalcError.DivisionByZero else left / right, .mod => if (right == 0) CalcError.DivisionByZero else @mod(left, right), .pow => math.pow(f64, left, right), // Bitwise ops in standard mode operate on integer truncations .bit_and => floatBitwise(left, right, bitwiseAnd), .bit_or => floatBitwise(left, right, bitwiseOr), .bit_xor => floatBitwise(left, right, bitwiseXor), .shift_left => floatShift(left, right, true), .shift_right, .shift_right_logical => floatShift(left, right, false), .rotate_left, .rotate_right => { // Rotations need bit width context; in standard mode, use 64-bit const l: u64 = @bitCast(@as(i64, @intFromFloat(left))); const r: u6 = @intFromFloat(@mod(right, 64.0)); const result = if (op == .rotate_left) math.rotl(u64, l, r) else math.rotr(u64, l, r); return @floatFromInt(@as(i64, @bitCast(result))); }, }; } fn bitwiseAnd(a: u64, b: u64) u64 { return a & b; } fn bitwiseOr(a: u64, b: u64) u64 { return a | b; } fn bitwiseXor(a: u64, b: u64) u64 { return a ^ b; } fn floatBitwise(left: f64, right: f64, op: *const fn (u64, u64) u64) f64 { const l: u64 = @bitCast(@as(i64, @intFromFloat(left))); const r: u64 = @bitCast(@as(i64, @intFromFloat(right))); const result = op(l, r); return @floatFromInt(@as(i64, @bitCast(result))); } fn floatShift(left: f64, right: f64, is_left: bool) f64 { const l: u64 = @bitCast(@as(i64, @intFromFloat(left))); const shift_amt: u6 = @intFromFloat(@mod(right, 64.0)); const result = if (is_left) l << shift_amt else l >> shift_amt; return @floatFromInt(@as(i64, @bitCast(result))); } /// Evaluate a built-in function call. fn evalFunction(env: *Environment, name: []const u8, args: []const *Expr) CalcError!f64 { // Single-argument functions if (args.len == 1) { const x = try evaluate(env, args[0]); return evalSingleArgFn(name, x) orelse CalcError.UnknownFunction; } // Multi-argument functions if (args.len == 2) { const a = try evaluate(env, args[0]); const b = try evaluate(env, args[1]); if (std.mem.eql(u8, name, "max")) return @max(a, b); if (std.mem.eql(u8, name, "min")) return @min(a, b); if (std.mem.eql(u8, name, "atan2")) return math.atan2(a, b); if (std.mem.eql(u8, name, "log")) { // log(value, base) if (b <= 0 or b == 1 or a <= 0) return CalcError.DomainError; return @log(a) / @log(b); } } // Zero-argument functions if (args.len == 0) { if (std.mem.eql(u8, name, "rand")) { // Not truly random in a pure engine, but useful as placeholder return 0.0; } } return CalcError.UnknownFunction; } /// Evaluate a single-argument built-in function. fn evalSingleArgFn(name: []const u8, x: f64) ?f64 { if (std.mem.eql(u8, name, "sin")) return @sin(x); if (std.mem.eql(u8, name, "cos")) return @cos(x); if (std.mem.eql(u8, name, "tan")) return @tan(x); if (std.mem.eql(u8, name, "asin")) { if (x < -1 or x > 1) return null; // domain error return math.asin(x); } if (std.mem.eql(u8, name, "acos")) { if (x < -1 or x > 1) return null; return math.acos(x); } if (std.mem.eql(u8, name, "atan")) return math.atan(x); if (std.mem.eql(u8, name, "log")) return @log10(x); if (std.mem.eql(u8, name, "log10")) return @log10(x); if (std.mem.eql(u8, name, "ln")) return @log(x); if (std.mem.eql(u8, name, "log2")) return @log2(x); if (std.mem.eql(u8, name, "sqrt")) { if (x < 0) return null; return @sqrt(x); } if (std.mem.eql(u8, name, "cbrt")) return math.cbrt(x); if (std.mem.eql(u8, name, "abs")) return @abs(x); if (std.mem.eql(u8, name, "ceil")) return @ceil(x); if (std.mem.eql(u8, name, "floor")) return @floor(x); if (std.mem.eql(u8, name, "round")) return @round(x); if (std.mem.eql(u8, name, "exp")) return @exp(x); if (std.mem.eql(u8, name, "factorial")) { if (x < 0 or x != @round(x) or x > 170) return null; return factorial(@intFromFloat(x)); } return null; } fn factorial(n: u64) f64 { if (n <= 1) return 1.0; var result: f64 = 1.0; var i: u64 = 2; while (i <= n) : (i += 1) { result *= @floatFromInt(i); } return result; } /// High-level evaluate: parse a string and evaluate it. /// Updates env.ans on success. pub fn evalString(env: *Environment, allocator: Allocator, source: []const u8) CalcError!f64 { var p = Parser.init(allocator, source, env.mode); const expr = try p.parse(); const result = try evaluate(env, expr); env.ans = result; env.history_len += 1; return result; } // -- Tests -- const testing = std.testing; fn testEval(source: []const u8) !f64 { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); const alloc = arena.allocator(); var env = Environment.init(alloc, .standard); defer env.deinit(); return evalString(&env, alloc, source); } fn testEvalProgrammer(source: []const u8) !f64 { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); const alloc = arena.allocator(); var env = Environment.init(alloc, .programmer); defer env.deinit(); return evalString(&env, alloc, source); } test "eval simple number" { const result = try testEval("42"); try testing.expectEqual(@as(f64, 42.0), result); } test "eval addition" { const result = try testEval("2 + 3"); try testing.expectEqual(@as(f64, 5.0), result); } test "eval subtraction" { const result = try testEval("10 - 7"); try testing.expectEqual(@as(f64, 3.0), result); } test "eval multiplication" { const result = try testEval("6 * 7"); try testing.expectEqual(@as(f64, 42.0), result); } test "eval division" { const result = try testEval("10 / 4"); try testing.expectEqual(@as(f64, 2.5), result); } test "eval division by zero" { const result = testEval("1 / 0"); try testing.expectError(CalcError.DivisionByZero, result); } test "eval modulo" { const result = try testEval("10 % 3"); try testing.expectApproxEqAbs(@as(f64, 1.0), result, 1e-10); } test "eval power" { const result = try testEval("2^10"); try testing.expectEqual(@as(f64, 1024.0), result); } test "eval precedence" { const result = try testEval("2 + 3 * 4"); try testing.expectEqual(@as(f64, 14.0), result); } test "eval parentheses" { const result = try testEval("(2 + 3) * 4"); try testing.expectEqual(@as(f64, 20.0), result); } test "eval unary negation" { const result = try testEval("-5 + 3"); try testing.expectEqual(@as(f64, -2.0), result); } test "eval nested parens" { const result = try testEval("((2 + 3) * (4 - 1))"); try testing.expectEqual(@as(f64, 15.0), result); } test "eval pi constant" { const result = try testEval("pi"); try testing.expectApproxEqAbs(math.pi, result, 1e-10); } test "eval e constant" { const result = try testEval("e"); try testing.expectApproxEqAbs(math.e, result, 1e-10); } test "eval tau constant" { const result = try testEval("tau"); try testing.expectApproxEqAbs(math.tau, result, 1e-10); } test "eval implicit mul: 2pi" { const result = try testEval("2pi"); try testing.expectApproxEqAbs(2.0 * math.pi, result, 1e-10); } test "eval implicit mul: 3(4+5)" { const result = try testEval("3(4+5)"); try testing.expectEqual(@as(f64, 27.0), result); } test "eval sin" { const result = try testEval("sin(0)"); try testing.expectApproxEqAbs(@as(f64, 0.0), result, 1e-10); } test "eval cos" { const result = try testEval("cos(0)"); try testing.expectApproxEqAbs(@as(f64, 1.0), result, 1e-10); } test "eval sqrt" { const result = try testEval("sqrt(144)"); try testing.expectEqual(@as(f64, 12.0), result); } test "eval abs" { const result = try testEval("abs(-42)"); try testing.expectEqual(@as(f64, 42.0), result); } test "eval floor" { const result = try testEval("floor(3.7)"); try testing.expectEqual(@as(f64, 3.0), result); } test "eval ceil" { const result = try testEval("ceil(3.2)"); try testing.expectEqual(@as(f64, 4.0), result); } test "eval round" { const result = try testEval("round(3.5)"); try testing.expectEqual(@as(f64, 4.0), result); } test "eval factorial" { const result = try testEval("factorial(5)"); try testing.expectEqual(@as(f64, 120.0), result); } test "eval factorial 0" { const result = try testEval("factorial(0)"); try testing.expectEqual(@as(f64, 1.0), result); } test "eval ln" { const result = try testEval("ln(1)"); try testing.expectApproxEqAbs(@as(f64, 0.0), result, 1e-10); } test "eval exp" { const result = try testEval("exp(0)"); try testing.expectEqual(@as(f64, 1.0), result); } test "eval max" { const result = try testEval("max(3, 7)"); try testing.expectEqual(@as(f64, 7.0), result); } test "eval min" { const result = try testEval("min(3, 7)"); try testing.expectEqual(@as(f64, 3.0), result); } test "eval unknown function" { const result = testEval("bogus(1)"); try testing.expectError(CalcError.UnknownFunction, result); } test "eval unknown variable" { const result = testEval("xyz"); try testing.expectError(CalcError.UnknownVariable, result); } test "eval variable assignment and use" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); const alloc = arena.allocator(); var env = Environment.init(alloc, .standard); defer env.deinit(); const assign_result = try evalString(&env, alloc, "X = 42"); try testing.expectEqual(@as(f64, 42.0), assign_result); const use_result = try evalString(&env, alloc, "X + 8"); try testing.expectEqual(@as(f64, 50.0), use_result); } test "eval Ans" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); const alloc = arena.allocator(); var env = Environment.init(alloc, .standard); defer env.deinit(); _ = try evalString(&env, alloc, "7 * 6"); const result = try evalString(&env, alloc, "Ans + 1"); try testing.expectEqual(@as(f64, 43.0), result); } test "eval complex expression" { const result = try testEval("sin(pi/2) + cos(0)"); try testing.expectApproxEqAbs(@as(f64, 2.0), result, 1e-10); } test "eval 2^32 - 1" { const result = try testEval("2^32 - 1"); try testing.expectEqual(@as(f64, 4294967295.0), result); } test "eval programmer XOR" { const result = try testEvalProgrammer("0xF ^ 0x3"); try testing.expectEqual(@as(f64, 12.0), result); } test "eval programmer AND" { const result = try testEvalProgrammer("0xFF & 0x0F"); try testing.expectEqual(@as(f64, 15.0), result); } test "eval programmer OR" { const result = try testEvalProgrammer("0xF0 | 0x0F"); try testing.expectEqual(@as(f64, 255.0), result); } test "eval programmer shift left" { const result = try testEvalProgrammer("1 << 8"); try testing.expectEqual(@as(f64, 256.0), result); } test "eval programmer shift right" { const result = try testEvalProgrammer("256 >> 4"); try testing.expectEqual(@as(f64, 16.0), result); } test "eval number with underscores" { const result = try testEval("1_000_000 + 1"); try testing.expectEqual(@as(f64, 1_000_001.0), result); } test "eval bitwise not in standard mode" { const result = try testEval("~0"); // ~0 as i64 = -1 try testing.expectEqual(@as(f64, -1.0), result); } test "eval rotate left in standard mode" { // 1 rol 4 = 16 (for 64-bit) const result = try testEvalProgrammer("1 rol 4"); try testing.expectEqual(@as(f64, 16.0), result); } test "eval rotate right in standard mode" { const result = try testEvalProgrammer("16 ror 4"); try testing.expectEqual(@as(f64, 1.0), result); } test "eval atan2" { const result = try testEval("atan2(1, 1)"); try testing.expectApproxEqAbs(math.pi / 4.0, result, 1e-10); } test "eval log with base" { const result = try testEval("log(100, 10)"); try testing.expectApproxEqAbs(@as(f64, 2.0), result, 1e-10); } test "eval log domain error" { const result = testEval("log(-1, 10)"); try testing.expectError(CalcError.DomainError, result); } test "eval asin domain error" { const result = testEval("asin(2)"); // asin(2) is domain error since |2| > 1 try testing.expectError(CalcError.UnknownFunction, result); } test "eval acos" { const result = try testEval("acos(1)"); try testing.expectApproxEqAbs(@as(f64, 0.0), result, 1e-10); }