371 lines
13 KiB
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);
|
|
}
|