const std = @import("std"); const engine = @import("engine"); const tui = @import("tui.zig"); /// Result of CLI evaluation - pure data, no I/O. pub const CliResult = struct { output: []const u8, is_error: bool, }; /// Parse CLI args and determine the expression and mode. /// Returns the joined expression and mode, or an error/help output. pub const ParsedArgs = union(enum) { expression: struct { text: []const u8, mode: engine.Mode, }, output: struct { text: []const u8, is_error: bool, }, }; pub fn parseArgs(allocator: std.mem.Allocator, args: []const []const u8) ParsedArgs { var mode: engine.Mode = .standard; var expr_parts = std.ArrayList([]const u8).empty; defer expr_parts.deinit(allocator); for (args) |arg| { if (std.mem.eql(u8, arg, "-p") or std.mem.eql(u8, arg, "--programmer")) { mode = .programmer; } else if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) { return .{ .output = .{ .text = help_text, .is_error = false, } }; } else if (std.mem.eql(u8, arg, "--version")) { return .{ .output = .{ .text = "tally 0.1.0\n", .is_error = false } }; } else { expr_parts.append(allocator, arg) catch { return .{ .output = .{ .text = "error: out of memory\n", .is_error = true } }; }; } } if (expr_parts.items.len == 0) { return .{ .output = .{ .text = "error: no expression provided\n", .is_error = true } }; } const expression = std.mem.join(allocator, " ", expr_parts.items) catch { return .{ .output = .{ .text = "error: out of memory\n", .is_error = true } }; }; return .{ .expression = .{ .text = expression, .mode = mode } }; } /// Evaluate an expression and format the result as a string. pub fn evaluate(allocator: std.mem.Allocator, expression: []const u8, mode: engine.Mode, buf: []u8) CliResult { if (mode == .programmer) { const config = engine.types.ProgrammerConfig{}; const result = engine.evalProgrammerString(allocator, expression, config) catch |err| { return .{ .output = errorMessage(err), .is_error = true }; }; return formatProgrammerResult(buf, result, config); } var env = engine.Environment.init(allocator, .standard); defer env.deinit(); const info = engine.evalStringInfo(&env, allocator, expression) catch |err| { return .{ .output = errorMessage(err), .is_error = true }; }; const formatted = engine.formatter.formatFloat(buf, info.value); // Enrich with multi-base view when the expression used non-decimal // literals and the result is a non-negative integer. if (info.has_nondecimal_literal and isDisplayableInt(info.value)) { return formatStandardMultiBase(buf, formatted.display, info.value); } return .{ .output = formatted.display, .is_error = false }; } /// True if the f64 is a non-negative integer within u128 range. fn isDisplayableInt(value: f64) bool { return value >= 0 and value == @trunc(value) and value < 340282366920938463463374607431768211456.0; } /// Format a standard-mode result with an inline multi-base breakdown. /// The decimal display is already computed; append hex/oct/bin. fn formatStandardMultiBase(buf: []u8, dec_display: []const u8, value: f64) CliResult { const int_val: u128 = @intFromFloat(value); const bw = engine.types.BitWidth.smallestFor(int_val); var hex_buf: [256]u8 = undefined; var oct_buf: [256]u8 = undefined; var bin_buf: [512]u8 = undefined; const hex = engine.formatter.formatHex(&hex_buf, int_val, bw, .big); const oct = engine.formatter.formatOctal(&oct_buf, int_val, bw); const bin = engine.formatter.formatBinary(&bin_buf, int_val, bw); // dec_display points into buf, so copy it out before we overwrite buf. var dec_copy: [128]u8 = undefined; const dec_len = @min(dec_display.len, dec_copy.len); @memcpy(dec_copy[0..dec_len], dec_display[0..dec_len]); const output = std.fmt.bufPrint(buf, \\{s} \\ hex: {s} \\ oct: {s} \\ bin: {s} , .{ dec_copy[0..dec_len], hex.display, oct.display, bin.display }) catch { return .{ .output = "error: buffer overflow\n", .is_error = true }; }; return .{ .output = output, .is_error = false }; } fn formatProgrammerResult(buf: []u8, result: engine.types.Integer, config: engine.types.ProgrammerConfig) CliResult { const value = result.unsignedValue(); const signed = result.signedValue(); var hex_buf: [256]u8 = undefined; var dec_buf: [256]u8 = undefined; var sdec_buf: [256]u8 = undefined; var oct_buf: [256]u8 = undefined; var bin_buf: [512]u8 = undefined; const hex = engine.formatter.formatHex(&hex_buf, value, config.bit_width, config.display_endian); const dec = engine.formatter.formatDecimalUnsigned(&dec_buf, value); const sdec = engine.formatter.formatDecimalSigned(&sdec_buf, signed); const oct = engine.formatter.formatOctal(&oct_buf, value, config.bit_width); const bin = engine.formatter.formatBinary(&bin_buf, value, config.bit_width); const output = std.fmt.bufPrint(buf, \\ dec(signed): {s} \\ dec(unsigned): {s} \\ hex: {s} \\ oct: {s} \\ bin: {s} \\ , .{ sdec.display, dec.display, hex.display, oct.display, bin.display }) catch { return .{ .output = "error: buffer overflow\n", .is_error = true }; }; return .{ .output = output, .is_error = false }; } fn errorMessage(err: engine.CalcError) []const u8 { return switch (err) { engine.CalcError.DivisionByZero => "error: division by zero\n", engine.CalcError.UnknownFunction => "error: unknown function\n", engine.CalcError.UnknownVariable => "error: unknown variable\n", engine.CalcError.UnmatchedParen => "error: unmatched parenthesis\n", engine.CalcError.UnexpectedToken => "error: unexpected token\n", engine.CalcError.UnexpectedEnd => "error: unexpected end of expression\n", engine.CalcError.InvalidNumber => "error: invalid number\n", engine.CalcError.InvalidExpression => "error: invalid expression\n", engine.CalcError.DomainError => "error: domain error\n", engine.CalcError.Overflow => "error: overflow\n", else => "error: evaluation error\n", }; } const help_text = \\tally - a cross-platform calculator \\ \\Usage: tally [OPTIONS] \\ \\Options: \\ -p, --programmer Programmer mode (^ = XOR, ** = power) \\ -h, --help Show this help \\ --version Show version \\ ; // -- Entry point: thin I/O shell over pure logic -- pub fn main(init: std.process.Init) u8 { const allocator = init.arena.allocator(); const io = init.io; const args = init.minimal.args.toSlice(allocator) catch { write(io, std.Io.File.stderr(), "error: failed to read arguments\n"); return 1; }; if (args.len <= 1) { tui.run(allocator, io, init.environ_map) catch |err| { write(io, std.Io.File.stderr(), "error: failed to start TUI: "); const err_name = @errorName(err); write(io, std.Io.File.stderr(), err_name); write(io, std.Io.File.stderr(), "\n"); return 1; }; return 0; } const parsed = parseArgs(allocator, args[1..]); switch (parsed) { .output => |out| { const file = if (out.is_error) std.Io.File.stderr() else std.Io.File.stdout(); write(io, file, out.text); return if (out.is_error) @as(u8, 1) else 0; }, .expression => |expr| { var buf: [4096]u8 = undefined; const result = evaluate(allocator, expr.text, expr.mode, &buf); const file = if (result.is_error) std.Io.File.stderr() else std.Io.File.stdout(); write(io, file, result.output); if (!result.is_error) write(io, std.Io.File.stdout(), "\n"); return if (result.is_error) @as(u8, 1) else 0; }, } } fn write(io: std.Io, file: std.Io.File, msg: []const u8) void { var buf: [4096]u8 = undefined; var writer = file.writer(io, &buf); writer.interface.writeAll(msg) catch return; writer.interface.flush() catch return; } // -- Unit tests -- const testing = std.testing; test "parseArgs: simple expression" { const parsed = parseArgs(testing.allocator, &.{"2+2"}); switch (parsed) { .expression => |e| { try testing.expectEqualStrings("2+2", e.text); try testing.expectEqual(engine.Mode.standard, e.mode); testing.allocator.free(e.text); }, .output => unreachable, } } test "parseArgs: multi-arg expression joins with spaces" { const parsed = parseArgs(testing.allocator, &.{ "2", "+", "2" }); switch (parsed) { .expression => |e| { try testing.expectEqualStrings("2 + 2", e.text); testing.allocator.free(e.text); }, .output => unreachable, } } test "parseArgs: programmer flag" { const parsed = parseArgs(testing.allocator, &.{ "-p", "0xFF" }); switch (parsed) { .expression => |e| { try testing.expectEqual(engine.Mode.programmer, e.mode); try testing.expectEqualStrings("0xFF", e.text); testing.allocator.free(e.text); }, .output => unreachable, } } test "parseArgs: --programmer long flag" { const parsed = parseArgs(testing.allocator, &.{ "--programmer", "0xF0", "|", "0x0F" }); switch (parsed) { .expression => |e| { try testing.expectEqual(engine.Mode.programmer, e.mode); try testing.expectEqualStrings("0xF0 | 0x0F", e.text); testing.allocator.free(e.text); }, .output => unreachable, } } test "parseArgs: --help" { const parsed = parseArgs(testing.allocator, &.{"--help"}); switch (parsed) { .output => |out| { try testing.expect(!out.is_error); try testing.expect(std.mem.startsWith(u8, out.text, "tally")); }, .expression => unreachable, } } test "parseArgs: --version" { const parsed = parseArgs(testing.allocator, &.{"--version"}); switch (parsed) { .output => |out| { try testing.expect(!out.is_error); try testing.expectEqualStrings("tally 0.1.0\n", out.text); }, .expression => unreachable, } } test "parseArgs: no expression" { const parsed = parseArgs(testing.allocator, &.{"-p"}); switch (parsed) { .output => |out| { try testing.expect(out.is_error); try testing.expectEqualStrings("error: no expression provided\n", out.text); }, .expression => unreachable, } } test "evaluate: standard arithmetic" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluate(arena.allocator(), "2 + 2", .standard, &buf); try testing.expect(!result.is_error); try testing.expectEqualStrings("4", result.output); } test "evaluate: large number has commas" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluate(arena.allocator(), "2^32 - 1", .standard, &buf); try testing.expect(!result.is_error); try testing.expectEqualStrings("4,294,967,295", result.output); } test "evaluate: function" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluate(arena.allocator(), "sin(pi/2) + 1", .standard, &buf); try testing.expect(!result.is_error); try testing.expectEqualStrings("2", result.output); } test "evaluate: explicit mul" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluate(arena.allocator(), "3*(4+5)", .standard, &buf); try testing.expect(!result.is_error); try testing.expectEqualStrings("27", result.output); } test "evaluate: programmer mode" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluate(arena.allocator(), "0xFF & 0x0F", .programmer, &buf); try testing.expect(!result.is_error); try testing.expect(std.mem.indexOf(u8, result.output, "15") != null); try testing.expect(std.mem.indexOf(u8, result.output, "00 00 00 00 00 00 00 0F") != null); } test "evaluate: division by zero error" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluate(arena.allocator(), "1/0", .standard, &buf); try testing.expect(result.is_error); try testing.expectEqualStrings("error: division by zero\n", result.output); } test "evaluate: unknown variable error" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluate(arena.allocator(), "xyz + 1", .standard, &buf); try testing.expect(result.is_error); try testing.expectEqualStrings("error: unknown variable\n", result.output); }