tally/src/main.zig

371 lines
13 KiB
Zig

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] <expression>
\\
\\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);
}