initial commit: ai slop, not reviewed
This commit is contained in:
parent
8253766e0f
commit
58a94dd163
18 changed files with 3773 additions and 0 deletions
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Zig build artifacts
|
||||||
|
.zig-cache/
|
||||||
|
zig-out/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
.tmp/
|
||||||
|
|
||||||
|
# Editor files
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
coverage/
|
||||||
7
.mise.toml
Normal file
7
.mise.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
[tools]
|
||||||
|
zig = "0.16.0"
|
||||||
|
zls = "0.16.0"
|
||||||
|
"github:j178/prek" = "0.4.1"
|
||||||
|
|
||||||
|
[tools."github:DonIsaac/zlint"]
|
||||||
|
version = "0.9.0"
|
||||||
51
.pre-commit-config.yaml
Normal file
51
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# See https://pre-commit.com for more information
|
||||||
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v6.0.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: check-yaml
|
||||||
|
- id: check-added-large-files
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: forbid-ai-punctuation
|
||||||
|
name: Forbid smart punctuation (en/figure dash, minus, ellipsis, arrows, smart quotes)
|
||||||
|
language: pygrep
|
||||||
|
entry: '(–|‒|―|−|…|→|⇐|⇒|⇔|“|”|‘|’)'
|
||||||
|
files: '\.(zig|zon|md|srf|txt|toml|ya?ml)$'
|
||||||
|
exclude: '^\.pre-commit-config\.yaml$'
|
||||||
|
- id: forbid-prose-em-dash
|
||||||
|
name: Forbid prose em-dash (use ASCII hyphen); no-data sentinel glyphs are exempt
|
||||||
|
language: pygrep
|
||||||
|
entry: ' — '
|
||||||
|
files: '\.(zig|zon|md|srf|txt|toml|ya?ml)$'
|
||||||
|
- repo: https://github.com/batmac/pre-commit-zig
|
||||||
|
rev: v0.3.0
|
||||||
|
hooks:
|
||||||
|
- id: zig-fmt
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: zlint
|
||||||
|
name: Run zlint
|
||||||
|
# zlint accepts file paths only via stdin (-S); positional
|
||||||
|
# args are interpreted as directory names and silently
|
||||||
|
# produce no output. Pipe pre-commit's file list through
|
||||||
|
# bash to get the paths to zlint as stdin lines.
|
||||||
|
entry: bash -c 'printf "%s\n" "$@" | zlint --deny-warnings --fix -S' --
|
||||||
|
language: system
|
||||||
|
types: [zig]
|
||||||
|
- repo: https://github.com/batmac/pre-commit-zig
|
||||||
|
rev: v0.3.0
|
||||||
|
hooks:
|
||||||
|
- id: zig-build
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: test
|
||||||
|
name: Run zig build test
|
||||||
|
entry: zig
|
||||||
|
args: ["build", "coverage", "-Dcoverage-threshold=80"]
|
||||||
|
language: system
|
||||||
|
types: [file]
|
||||||
|
pass_filenames: false
|
||||||
97
build.zig
Normal file
97
build.zig
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const Coverage = @import("build/Coverage.zig");
|
||||||
|
|
||||||
|
pub fn build(b: *std.Build) void {
|
||||||
|
const target = b.standardTargetOptions(.{});
|
||||||
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
|
// -- Engine module (the calculation library) --
|
||||||
|
const engine_mod = b.addModule("engine", .{
|
||||||
|
.root_source_file = b.path("engine/src/engine.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
// -- Engine static library (for linking into the main binary) --
|
||||||
|
const engine_lib = b.addLibrary(.{
|
||||||
|
.linkage = .static,
|
||||||
|
.name = "tally-engine",
|
||||||
|
.root_module = engine_mod,
|
||||||
|
});
|
||||||
|
b.installArtifact(engine_lib);
|
||||||
|
|
||||||
|
// -- Engine shared library (for Android JNI) --
|
||||||
|
const engine_shared = b.addLibrary(.{
|
||||||
|
.linkage = .dynamic,
|
||||||
|
.name = "tally",
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("engine/src/c_api.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "engine", .module = engine_mod },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
b.installArtifact(engine_shared);
|
||||||
|
|
||||||
|
// -- Main binary (CLI + TUI in one) --
|
||||||
|
const exe = b.addExecutable(.{
|
||||||
|
.name = "tally",
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "engine", .module = engine_mod },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
b.installArtifact(exe);
|
||||||
|
|
||||||
|
// -- Tests --
|
||||||
|
const engine_tests = b.addTest(.{
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("engine/src/engine.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const cli_tests = b.addTest(.{
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "engine", .module = engine_mod },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const run_engine_tests = b.addRunArtifact(engine_tests);
|
||||||
|
const run_cli_tests = b.addRunArtifact(cli_tests);
|
||||||
|
const test_step = b.step("test", "Run unit tests");
|
||||||
|
test_step.dependOn(&run_engine_tests.step);
|
||||||
|
test_step.dependOn(&run_cli_tests.step);
|
||||||
|
|
||||||
|
// -- Coverage step (uses kcov, Linux x86_64/aarch64 only) --
|
||||||
|
{
|
||||||
|
var cov = Coverage.init(b);
|
||||||
|
const cov_mod = b.createModule(.{
|
||||||
|
.root_source_file = b.path("engine/src/engine.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
_ = cov.addModule(cov_mod, "tally-engine");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Run step --
|
||||||
|
const run_step = b.step("run", "Run tally");
|
||||||
|
const run_cmd = b.addRunArtifact(exe);
|
||||||
|
run_step.dependOn(&run_cmd.step);
|
||||||
|
run_cmd.step.dependOn(b.getInstallStep());
|
||||||
|
if (b.args) |args| {
|
||||||
|
run_cmd.addArgs(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
build.zig.zon
Normal file
14
build.zig.zon
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
.{
|
||||||
|
.name = .tally,
|
||||||
|
.version = "0.1.0",
|
||||||
|
.minimum_zig_version = "0.16.0",
|
||||||
|
.fingerprint = 0xe8b6fe03a23dd13b,
|
||||||
|
.paths = .{
|
||||||
|
"build.zig",
|
||||||
|
"build.zig.zon",
|
||||||
|
"build",
|
||||||
|
"engine",
|
||||||
|
"src",
|
||||||
|
},
|
||||||
|
.dependencies = .{},
|
||||||
|
}
|
||||||
215
build/Coverage.zig
Normal file
215
build/Coverage.zig
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const std = @import("std");
|
||||||
|
const Build = std.Build;
|
||||||
|
|
||||||
|
const Coverage = @This();
|
||||||
|
|
||||||
|
/// Initialize coverage infrastructure. Creates the "coverage" build step,
|
||||||
|
/// registers build options (-Dcoverage-threshold, -Dcoverage-dir),
|
||||||
|
/// and sets up the kcov download step. The kcov binary is downloaded into the
|
||||||
|
/// zig cache on first use and reused thereafter.
|
||||||
|
///
|
||||||
|
/// Use `zig build coverage --verbose` to see per-file coverage breakdown.
|
||||||
|
///
|
||||||
|
/// Call `addModule()` on the returned value to add the test module to the
|
||||||
|
/// coverage run.
|
||||||
|
///
|
||||||
|
/// Because addModule creates a new test executable from the root module provided,
|
||||||
|
/// if there are any linking steps being done to your test executable, those
|
||||||
|
/// must also be done to the test_exe returned by addModule.
|
||||||
|
pub fn init(b: *Build) Coverage {
|
||||||
|
const coverage_threshold = b.option(u7, "coverage-threshold", "Minimum coverage percentage required") orelse 0;
|
||||||
|
const coverage_dir = b.option([]const u8, "coverage-dir", "Coverage output directory") orelse
|
||||||
|
b.pathJoin(&.{ b.build_root.path orelse ".", "coverage" });
|
||||||
|
const coverage_step = b.step("coverage", "Generate test coverage report");
|
||||||
|
|
||||||
|
const arch_name = switch (builtin.cpu.arch) {
|
||||||
|
.x86_64 => "x86_64",
|
||||||
|
.aarch64 => "aarch64",
|
||||||
|
else => @tagName(builtin.cpu.arch),
|
||||||
|
};
|
||||||
|
|
||||||
|
const Algo = std.crypto.hash.sha2.Sha256;
|
||||||
|
var hasher = Algo.init(.{});
|
||||||
|
hasher.update("kcov-");
|
||||||
|
hasher.update(arch_name);
|
||||||
|
var cache_hash: [Algo.digest_length]u8 = undefined;
|
||||||
|
hasher.final(&cache_hash);
|
||||||
|
|
||||||
|
const cache_dir = b.pathJoin(&.{
|
||||||
|
b.cache_root.path.?,
|
||||||
|
"o",
|
||||||
|
b.fmt("{s}", .{std.fmt.bytesToHex(cache_hash, .lower)}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const kcov_path = b.pathJoin(&.{ cache_dir, b.fmt("kcov-{s}", .{arch_name}) });
|
||||||
|
|
||||||
|
const download_exe = b.addExecutable(.{
|
||||||
|
.name = "download-kcov",
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("build/download_kcov.zig"),
|
||||||
|
.target = b.resolveTargetQuery(.{}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const run_download = b.addRunArtifact(download_exe);
|
||||||
|
run_download.addArg(kcov_path);
|
||||||
|
run_download.addArg(arch_name);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.b = b,
|
||||||
|
.coverage_step = coverage_step,
|
||||||
|
.coverage_dir = coverage_dir,
|
||||||
|
.coverage_threshold = coverage_threshold,
|
||||||
|
.kcov_path = kcov_path,
|
||||||
|
.run_download = run_download,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a test module to the coverage run. Runs kcov on the test binary,
|
||||||
|
/// then reads the coverage JSON and prints a summary (with per-file
|
||||||
|
/// breakdown if --verbose). Fails if below -Dcoverage-threshold.
|
||||||
|
///
|
||||||
|
/// Returns the test executable so the caller can add any extra linking steps.
|
||||||
|
pub fn addModule(self: *Coverage, root_module: *Build.Module, name: []const u8) *Build.Step.Compile {
|
||||||
|
const b = self.b;
|
||||||
|
|
||||||
|
const run_coverage = b.addSystemCommand(&.{self.kcov_path});
|
||||||
|
const include_path = b.pathJoin(&.{ b.build_root.path.?, "engine", "src" });
|
||||||
|
run_coverage.addArgs(&.{ "--include-path", include_path });
|
||||||
|
const css_file = b.pathJoin(&.{ b.build_root.path.?, "build", "bcov.css" });
|
||||||
|
run_coverage.addArg(b.fmt("--configure=css-file={s}", .{css_file}));
|
||||||
|
run_coverage.addArg(self.coverage_dir);
|
||||||
|
|
||||||
|
const test_exe = b.addTest(.{
|
||||||
|
.name = name,
|
||||||
|
.root_module = root_module,
|
||||||
|
.use_llvm = true,
|
||||||
|
});
|
||||||
|
run_coverage.addArtifactArg(test_exe);
|
||||||
|
run_coverage.step.dependOn(&test_exe.step);
|
||||||
|
run_coverage.step.dependOn(&self.run_download.step);
|
||||||
|
|
||||||
|
const check = b.allocator.create(Check) catch @panic("OOM");
|
||||||
|
check.* = .{
|
||||||
|
.step = Build.Step.init(.{
|
||||||
|
.id = .custom,
|
||||||
|
.name = "check coverage",
|
||||||
|
.owner = b,
|
||||||
|
.makeFn = make,
|
||||||
|
}),
|
||||||
|
.json_path = b.fmt("{s}/{s}/coverage.json", .{ self.coverage_dir, name }),
|
||||||
|
.threshold = self.coverage_threshold,
|
||||||
|
};
|
||||||
|
check.step.dependOn(&run_coverage.step);
|
||||||
|
self.coverage_step.dependOn(&check.step);
|
||||||
|
|
||||||
|
return test_exe;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Fields --
|
||||||
|
|
||||||
|
b: *Build,
|
||||||
|
coverage_step: *Build.Step,
|
||||||
|
coverage_dir: []const u8,
|
||||||
|
coverage_threshold: u7,
|
||||||
|
kcov_path: []const u8,
|
||||||
|
run_download: *Build.Step.Run,
|
||||||
|
|
||||||
|
const Check = struct {
|
||||||
|
step: Build.Step,
|
||||||
|
json_path: []const u8,
|
||||||
|
threshold: u7,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CoverageReport = struct {
|
||||||
|
files: []const CoverageFile,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CoverageFile = struct {
|
||||||
|
file: []const u8,
|
||||||
|
covered_lines: usize,
|
||||||
|
total_lines: usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
const File = struct {
|
||||||
|
file: []const u8,
|
||||||
|
percent_covered: f64,
|
||||||
|
covered_lines: usize,
|
||||||
|
total_lines: usize,
|
||||||
|
|
||||||
|
pub fn coverageLessThanDesc(_: void, lhs: File, rhs: File) bool {
|
||||||
|
return lhs.percent_covered > rhs.percent_covered;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn make(step: *Build.Step, options: Build.Step.MakeOptions) !void {
|
||||||
|
_ = options;
|
||||||
|
const check: *Check = @fieldParentPtr("step", step);
|
||||||
|
const allocator = step.owner.allocator;
|
||||||
|
const io = step.owner.graph.io;
|
||||||
|
|
||||||
|
const file = std.Io.Dir.cwd().openFile(io, check.json_path, .{}) catch |err| {
|
||||||
|
return step.fail("Failed to open coverage report {s}: {}", .{ check.json_path, err });
|
||||||
|
};
|
||||||
|
defer file.close(io);
|
||||||
|
|
||||||
|
var file_reader = file.reader(io, &.{});
|
||||||
|
const content = try file_reader.interface.allocRemaining(allocator, .limited(10 * 1024 * 1024));
|
||||||
|
defer allocator.free(content);
|
||||||
|
|
||||||
|
const json = std.json.parseFromSlice(CoverageReport, allocator, content, .{
|
||||||
|
.ignore_unknown_fields = true,
|
||||||
|
}) catch |err| {
|
||||||
|
return step.fail("Failed to parse coverage JSON: {}", .{err});
|
||||||
|
};
|
||||||
|
defer json.deinit();
|
||||||
|
|
||||||
|
var total_covered: usize = 0;
|
||||||
|
var total_lines: usize = 0;
|
||||||
|
|
||||||
|
var file_list = std.ArrayList(File).empty;
|
||||||
|
defer file_list.deinit(allocator);
|
||||||
|
|
||||||
|
for (json.value.files) |f| {
|
||||||
|
const pct: f64 = if (f.total_lines > 0)
|
||||||
|
@as(f64, @floatFromInt(f.covered_lines)) / @as(f64, @floatFromInt(f.total_lines)) * 100.0
|
||||||
|
else
|
||||||
|
0;
|
||||||
|
try file_list.append(allocator, .{
|
||||||
|
.file = f.file,
|
||||||
|
.covered_lines = f.covered_lines,
|
||||||
|
.total_lines = f.total_lines,
|
||||||
|
.percent_covered = pct,
|
||||||
|
});
|
||||||
|
total_covered += f.covered_lines;
|
||||||
|
total_lines += f.total_lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
std.mem.sort(File, file_list.items, {}, File.coverageLessThanDesc);
|
||||||
|
|
||||||
|
var stdout_buffer: [1024]u8 = undefined;
|
||||||
|
var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer);
|
||||||
|
const stdout = &stdout_writer.interface;
|
||||||
|
if (step.owner.verbose) {
|
||||||
|
for (file_list.items) |f| {
|
||||||
|
try stdout.print(
|
||||||
|
"{d: >5.1}% {d: >5}/{d: <5}:{s}\n",
|
||||||
|
.{ f.percent_covered, f.covered_lines, f.total_lines, f.file },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const total_pct: f64 = if (total_lines > 0)
|
||||||
|
@as(f64, @floatFromInt(total_covered)) / @as(f64, @floatFromInt(total_lines)) * 100.0
|
||||||
|
else
|
||||||
|
0;
|
||||||
|
try stdout.print(
|
||||||
|
"Total test coverage: {d:.2}% ({d}/{d})\n",
|
||||||
|
.{ total_pct, total_covered, total_lines },
|
||||||
|
);
|
||||||
|
try stdout.flush();
|
||||||
|
|
||||||
|
if (@as(u7, @intFromFloat(@floor(total_pct))) < check.threshold)
|
||||||
|
return step.fail("Coverage {d:.2}% is below threshold {d}%", .{ total_pct, check.threshold });
|
||||||
|
}
|
||||||
45
build/bcov.css
Normal file
45
build/bcov.css
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
/* Based upon the lcov CSS style, style files can be reused - Dark Theme */
|
||||||
|
body { color: #e0e0e0; background-color: #1e1e1e; }
|
||||||
|
a:link { color: #6b9aff; text-decoration: underline; }
|
||||||
|
a:visited { color: #4dbb7a; text-decoration: underline; }
|
||||||
|
a:active { color: #ff6b8a; text-decoration: underline; }
|
||||||
|
td.title { text-align: center; padding-bottom: 10px; font-size: 20pt; font-weight: bold; }
|
||||||
|
td.ruler { background-color: #4a6ba8; }
|
||||||
|
td.headerItem { text-align: right; padding-right: 6px; font-family: sans-serif; font-weight: bold; }
|
||||||
|
td.headerValue { text-align: left; color: #6b9aff; font-family: sans-serif; font-weight: bold; }
|
||||||
|
td.versionInfo { text-align: center; padding-top: 2px; }
|
||||||
|
th.headerItem { text-align: right; padding-right: 6px; font-family: sans-serif; font-weight: bold; }
|
||||||
|
th.headerValue { text-align: left; color: #6b9aff; font-family: sans-serif; font-weight: bold; }
|
||||||
|
pre.source { font-family: monospace; white-space: pre; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
span.lineNum { background-color: #5a5a2a; }
|
||||||
|
span.lineNumLegend { background-color: #5a5a2a; width: 96px; font-weight: bold ;}
|
||||||
|
span.lineCov { background-color: #2d5a2d; }
|
||||||
|
span.linePartCov { background-color: #707000; }
|
||||||
|
span.lineNoCov { background-color: #762c2c; }
|
||||||
|
span.orderNum { background-color: #5a4a2a; float: right; width:5em; text-align: left; }
|
||||||
|
span.orderNumLegend { background-color: #5a4a2a; width: 96px; font-weight: bold ;}
|
||||||
|
span.coverHits { background-color: #4a4a2a; padding-left: 3px; padding-right: 1px; text-align: right; list-style-type: none; display: inline-block; width: 5em; }
|
||||||
|
span.coverHitsLegend { background-color: #4a4a2a; width: 96px; font-weight: bold; margin: 0 auto;}
|
||||||
|
td.tableHead { text-align: center; color: #e0e0e0; background-color: #4a6ba8; font-family: sans-serif; font-size: 120%; font-weight: bold; }
|
||||||
|
td.coverFile { text-align: left; padding-left: 10px; padding-right: 20px; color: #6b9aff; font-family: monospace; background-color: #3a3a3a; }
|
||||||
|
td.coverBar { padding-left: 10px; padding-right: 10px; background-color: #3a3a3a; }
|
||||||
|
td.coverBarOutline { background-color: #4a4a4a; }
|
||||||
|
td.coverPer { text-align: left; padding-left: 10px; padding-right: 10px; font-weight: bold; background-color: #3a3a3a; color: #e0e0e0; }
|
||||||
|
td.coverPerLeftMed { text-align: left; padding-left: 10px; padding-right: 10px; background-color: #5a5a00; font-weight: bold; color: #e0e0e0; }
|
||||||
|
td.coverPerLeftLo { text-align: left; padding-left: 10px; padding-right: 10px; background-color: #5a2d2d; font-weight: bold; color: #e0e0e0; }
|
||||||
|
td.coverPerLeftHi { text-align: left; padding-left: 10px; padding-right: 10px; background-color: #2d5a2d; font-weight: bold; color: #e0e0e0; }
|
||||||
|
td.coverNum { text-align: right; padding-left: 10px; padding-right: 10px; background-color: #3a3a3a; color: #e0e0e0; }
|
||||||
|
|
||||||
|
.tablesorter-blue tbody > tr:hover > td,
|
||||||
|
.tablesorter-blue tbody > tr:hover + tr.tablesorter-childRow > td,
|
||||||
|
.tablesorter-blue tbody > tr:hover + tr.tablesorter-childRow + tr.tablesorter-childRow > td,
|
||||||
|
.tablesorter-blue tbody > tr.even:hover > td,
|
||||||
|
.tablesorter-blue tbody > tr.even:hover + tr.tablesorter-childRow > td,
|
||||||
|
.tablesorter-blue tbody > tr.even:hover + tr.tablesorter-childRow + tr.tablesorter-childRow > td {
|
||||||
|
background: #4a4a4a;
|
||||||
|
}
|
||||||
|
.tablesorter-blue tbody > tr.odd:hover > td,
|
||||||
|
.tablesorter-blue tbody > tr.odd:hover + tr.tablesorter-childRow > td,
|
||||||
|
.tablesorter-blue tbody > tr.odd:hover + tr.tablesorter-childRow + tr.tablesorter-childRow > td {
|
||||||
|
background: #4a4a4a;
|
||||||
|
}
|
||||||
80
build/download_kcov.zig
Normal file
80
build/download_kcov.zig
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn main(init: std.process.Init) !void {
|
||||||
|
const allocator = init.arena.allocator();
|
||||||
|
const io = init.io;
|
||||||
|
|
||||||
|
const args = try init.minimal.args.toSlice(allocator);
|
||||||
|
|
||||||
|
if (args.len != 3) return error.InvalidArgs;
|
||||||
|
|
||||||
|
const kcov_path = args[1];
|
||||||
|
const arch_name = args[2];
|
||||||
|
|
||||||
|
// Check if file already exists
|
||||||
|
const stat = std.Io.Dir.cwd().statFile(io, kcov_path, .{}) catch |err| blk: {
|
||||||
|
if (err == error.FileNotFound) break :blk null else return err;
|
||||||
|
};
|
||||||
|
if (stat != null) return;
|
||||||
|
|
||||||
|
var stdout_buffer: [1024]u8 = undefined;
|
||||||
|
var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer);
|
||||||
|
const stdout = &stdout_writer.interface;
|
||||||
|
|
||||||
|
try stdout.writeAll("Determining latest kcov version\n");
|
||||||
|
try stdout.flush();
|
||||||
|
|
||||||
|
var client = std.http.Client{ .allocator = allocator, .io = io };
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
// Get redirect to find latest version
|
||||||
|
const list_uri = try std.Uri.parse("https://git.lerch.org/lobo/-/packages/generic/kcov/");
|
||||||
|
var req = try client.request(.GET, list_uri, .{ .redirect_behavior = .unhandled });
|
||||||
|
defer req.deinit();
|
||||||
|
|
||||||
|
try req.sendBodiless();
|
||||||
|
var redirect_buf: [1024]u8 = undefined;
|
||||||
|
const response = try req.receiveHead(&redirect_buf);
|
||||||
|
|
||||||
|
if (response.head.status != .see_other) return error.UnexpectedResponse;
|
||||||
|
|
||||||
|
const location = response.head.location orelse return error.NoLocation;
|
||||||
|
const version_start = std.mem.lastIndexOfScalar(u8, location, '/') orelse return error.InvalidLocation;
|
||||||
|
const version = location[version_start + 1 ..];
|
||||||
|
|
||||||
|
try stdout.print(
|
||||||
|
"Downloading kcov version {s} for {s} to {s}...",
|
||||||
|
.{ version, arch_name, kcov_path },
|
||||||
|
);
|
||||||
|
try stdout.flush();
|
||||||
|
|
||||||
|
const binary_url = try std.fmt.allocPrint(
|
||||||
|
allocator,
|
||||||
|
"https://git.lerch.org/api/packages/lobo/generic/kcov/{s}/kcov-{s}",
|
||||||
|
.{ version, arch_name },
|
||||||
|
);
|
||||||
|
|
||||||
|
const cache_dir = std.fs.path.dirname(kcov_path) orelse return error.InvalidPath;
|
||||||
|
std.Io.Dir.cwd().createDir(io, cache_dir, std.Io.File.Permissions.default_dir) catch |e| switch (e) {
|
||||||
|
error.PathAlreadyExists => {},
|
||||||
|
else => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
const uri = try std.Uri.parse(binary_url);
|
||||||
|
const file = try std.Io.Dir.cwd().createFile(io, kcov_path, .{});
|
||||||
|
defer file.close(io);
|
||||||
|
try file.setPermissions(io, @enumFromInt(0o755));
|
||||||
|
|
||||||
|
var buffer: [8192]u8 = undefined;
|
||||||
|
var writer = file.writer(io, &buffer);
|
||||||
|
const result = try client.fetch(.{
|
||||||
|
.location = .{ .uri = uri },
|
||||||
|
.response_writer = &writer.interface,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status != .ok) return error.DownloadFailed;
|
||||||
|
try writer.interface.flush();
|
||||||
|
|
||||||
|
try stdout.writeAll("done\n");
|
||||||
|
try stdout.flush();
|
||||||
|
}
|
||||||
69
engine/src/ast.zig
Normal file
69
engine/src/ast.zig
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
//! AST node definitions for Tally expressions.
|
||||||
|
//!
|
||||||
|
//! The AST is arena-allocated: all nodes live in a single arena and are freed
|
||||||
|
//! together when the expression is no longer needed. No per-node deallocation.
|
||||||
|
|
||||||
|
const types = @import("types.zig");
|
||||||
|
const Base = types.Base;
|
||||||
|
|
||||||
|
/// A single expression node.
|
||||||
|
pub const Expr = union(enum) {
|
||||||
|
number: Number,
|
||||||
|
unary: Unary,
|
||||||
|
binary: Binary,
|
||||||
|
call: Call,
|
||||||
|
variable: []const u8,
|
||||||
|
assignment: Assignment,
|
||||||
|
|
||||||
|
pub const Number = struct {
|
||||||
|
float_value: f64,
|
||||||
|
/// If the number is a pure integer, stores the exact value.
|
||||||
|
int_value: ?u64,
|
||||||
|
base: Base,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Unary = struct {
|
||||||
|
op: UnaryOp,
|
||||||
|
operand: *Expr,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Binary = struct {
|
||||||
|
op: BinaryOp,
|
||||||
|
left: *Expr,
|
||||||
|
right: *Expr,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Call = struct {
|
||||||
|
name: []const u8,
|
||||||
|
args: []const *Expr,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Assignment = struct {
|
||||||
|
name: []const u8,
|
||||||
|
value: *Expr,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const UnaryOp = enum {
|
||||||
|
negate, // -x
|
||||||
|
bitwise_not, // ~x
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const BinaryOp = enum {
|
||||||
|
add,
|
||||||
|
sub,
|
||||||
|
mul,
|
||||||
|
div,
|
||||||
|
mod,
|
||||||
|
pow, // ^ in standard, ** in programmer
|
||||||
|
|
||||||
|
// Bitwise (programmer mode)
|
||||||
|
bit_and,
|
||||||
|
bit_or,
|
||||||
|
bit_xor,
|
||||||
|
shift_left,
|
||||||
|
shift_right, // arithmetic >>
|
||||||
|
shift_right_logical, // >>>
|
||||||
|
rotate_left, // rol keyword
|
||||||
|
rotate_right, // ror keyword
|
||||||
|
};
|
||||||
38
engine/src/c_api.zig
Normal file
38
engine/src/c_api.zig
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
//! C ABI exports for Tally engine.
|
||||||
|
//!
|
||||||
|
//! All string inputs use (pointer, length) pairs - no null-terminated strings.
|
||||||
|
//! Callers must free results via tally_result_free().
|
||||||
|
|
||||||
|
pub const CalcResult = extern struct {
|
||||||
|
json_ptr: ?[*]u8,
|
||||||
|
json_len: usize,
|
||||||
|
error_ptr: ?[*]u8,
|
||||||
|
error_len: usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
export fn tally_eval(
|
||||||
|
expr_ptr: [*]const u8,
|
||||||
|
expr_len: usize,
|
||||||
|
mode: c_int,
|
||||||
|
config_ptr: ?[*]const u8,
|
||||||
|
config_len: usize,
|
||||||
|
) callconv(.c) CalcResult {
|
||||||
|
_ = expr_ptr;
|
||||||
|
_ = expr_len;
|
||||||
|
_ = mode;
|
||||||
|
_ = config_ptr;
|
||||||
|
_ = config_len;
|
||||||
|
// TODO: implement
|
||||||
|
return .{ .json_ptr = null, .json_len = 0, .error_ptr = null, .error_len = 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn tally_result_free(result: *CalcResult) callconv(.c) void {
|
||||||
|
_ = result;
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn tally_version(out_len: *usize) callconv(.c) [*]const u8 {
|
||||||
|
const version = "0.1.0";
|
||||||
|
out_len.* = version.len;
|
||||||
|
return version.ptr;
|
||||||
|
}
|
||||||
30
engine/src/engine.zig
Normal file
30
engine/src/engine.zig
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
//! Tally calculation engine.
|
||||||
|
//!
|
||||||
|
//! Pure computation library with no I/O. Provides expression parsing,
|
||||||
|
//! evaluation, programmer-mode bit manipulation, struct layout computation,
|
||||||
|
//! unit conversion, and financial calculations.
|
||||||
|
|
||||||
|
pub const types = @import("types.zig");
|
||||||
|
pub const tokenizer = @import("tokenizer.zig");
|
||||||
|
pub const ast = @import("ast.zig");
|
||||||
|
pub const parser = @import("parser.zig");
|
||||||
|
pub const evaluator = @import("evaluator.zig");
|
||||||
|
pub const programmer = @import("programmer.zig");
|
||||||
|
pub const formatter = @import("formatter.zig");
|
||||||
|
|
||||||
|
// Re-export primary types for convenience
|
||||||
|
pub const Value = types.Value;
|
||||||
|
pub const Mode = types.Mode;
|
||||||
|
pub const BitWidth = types.BitWidth;
|
||||||
|
pub const CalcError = types.CalcError;
|
||||||
|
pub const Parser = parser.Parser;
|
||||||
|
pub const Expr = ast.Expr;
|
||||||
|
pub const Environment = evaluator.Environment;
|
||||||
|
pub const evalString = evaluator.evalString;
|
||||||
|
pub const evalProgrammerString = programmer.evalProgrammerString;
|
||||||
|
|
||||||
|
test {
|
||||||
|
std.testing.refAllDecls(@This());
|
||||||
|
}
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
530
engine/src/evaluator.zig
Normal file
530
engine/src/evaluator.zig
Normal file
|
|
@ -0,0 +1,530 @@
|
||||||
|
//! 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);
|
||||||
|
}
|
||||||
487
engine/src/formatter.zig
Normal file
487
engine/src/formatter.zig
Normal file
|
|
@ -0,0 +1,487 @@
|
||||||
|
//! Number display formatter for Tally.
|
||||||
|
//!
|
||||||
|
//! Produces two string representations for every value:
|
||||||
|
//! - `display`: human-readable with separators (commas, underscores, spaces)
|
||||||
|
//! - `raw`: clipboard-friendly without separators (but with base prefix)
|
||||||
|
//!
|
||||||
|
//! Formatting rules per the spec:
|
||||||
|
//! - Decimal: comma-separated groups of 3 (e.g. "4,294,967,295")
|
||||||
|
//! - Hex value view: underscore per 16-bit word (e.g. "0xFFFF_FFFF")
|
||||||
|
//! - Binary: space per nibble (e.g. "1111 1111")
|
||||||
|
//! - Octal: underscore per 3-digit group (e.g. "0o37_777_777_777")
|
||||||
|
//! - Scientific notation only when |value| > 10^15 or < 10^-15 or > 15 sig digits
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const types = @import("types.zig");
|
||||||
|
const BitWidth = types.BitWidth;
|
||||||
|
|
||||||
|
/// A formatted value with both display and clipboard representations.
|
||||||
|
pub const FormattedValue = struct {
|
||||||
|
display: []const u8,
|
||||||
|
raw: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Format a floating-point value for display.
|
||||||
|
/// Uses comma grouping for integers, avoids scientific notation unless necessary.
|
||||||
|
pub fn formatFloat(buf: []u8, value: f64) FormattedValue {
|
||||||
|
// Check if value is an integer (no fractional part, within safe range)
|
||||||
|
const is_integer = value == @trunc(value) and @abs(value) < 9007199254740992.0; // 2^53
|
||||||
|
|
||||||
|
if (is_integer and @abs(value) < 1e15) {
|
||||||
|
// Format as integer with commas
|
||||||
|
const int_val: i64 = @intFromFloat(value);
|
||||||
|
const raw_len = writeSignedInt(buf, int_val);
|
||||||
|
const raw = buf[0..raw_len];
|
||||||
|
|
||||||
|
// Now write the display version (with commas) after the raw version
|
||||||
|
const display_start = raw_len;
|
||||||
|
const display_len = writeDecimalWithCommas(buf[display_start..], int_val);
|
||||||
|
const display = buf[display_start..][0..display_len];
|
||||||
|
|
||||||
|
return .{ .display = display, .raw = raw };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we should use scientific notation
|
||||||
|
const abs_val = @abs(value);
|
||||||
|
if (abs_val != 0 and (abs_val > 1e15 or abs_val < 1e-15)) {
|
||||||
|
// Scientific notation
|
||||||
|
const raw_len = (std.fmt.bufPrint(buf, "{e}", .{value}) catch return .{ .display = "ERR", .raw = "ERR" }).len;
|
||||||
|
return .{ .display = buf[0..raw_len], .raw = buf[0..raw_len] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular float formatting
|
||||||
|
const raw_len = (std.fmt.bufPrint(buf, "{d}", .{value}) catch return .{ .display = "ERR", .raw = "ERR" }).len;
|
||||||
|
return .{ .display = buf[0..raw_len], .raw = buf[0..raw_len] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format an integer for programmer mode hex display.
|
||||||
|
/// Display: "0xFFFF_FFFF" (underscore per 16-bit word)
|
||||||
|
/// Raw: "0xFFFFFFFF" (no separators)
|
||||||
|
pub fn formatHex(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue {
|
||||||
|
const width = bit_width.bits();
|
||||||
|
const hex_digits: usize = @as(usize, width) / 4;
|
||||||
|
|
||||||
|
// Write raw first: "0x" + hex digits
|
||||||
|
buf[0] = '0';
|
||||||
|
buf[1] = 'x';
|
||||||
|
var pos: usize = 2;
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < hex_digits) : (i += 1) {
|
||||||
|
const shift_amt: u6 = @intCast((hex_digits - 1 - i) * 4);
|
||||||
|
const nibble: u4 = @intCast((value >> shift_amt) & 0xF);
|
||||||
|
buf[pos] = hexDigit(nibble);
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
const raw = buf[0..pos];
|
||||||
|
|
||||||
|
// Write display after raw: "0x" + hex digits with _ every 4 digits (16-bit word boundary)
|
||||||
|
const display_start = pos;
|
||||||
|
buf[pos] = '0';
|
||||||
|
buf[pos + 1] = 'x';
|
||||||
|
pos += 2;
|
||||||
|
i = 0;
|
||||||
|
while (i < hex_digits) : (i += 1) {
|
||||||
|
if (i > 0 and i % 4 == 0) {
|
||||||
|
buf[pos] = '_';
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
const shift_amt: u6 = @intCast((hex_digits - 1 - i) * 4);
|
||||||
|
const nibble: u4 = @intCast((value >> shift_amt) & 0xF);
|
||||||
|
buf[pos] = hexDigit(nibble);
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
const display = buf[display_start..pos];
|
||||||
|
|
||||||
|
return .{ .display = display, .raw = raw };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format an integer for programmer mode binary display.
|
||||||
|
/// Display: "1111 0000 1010 1100" (space per nibble)
|
||||||
|
/// Raw: "0b1111000010101100" (no separators)
|
||||||
|
pub fn formatBinary(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue {
|
||||||
|
const width: usize = bit_width.bits();
|
||||||
|
|
||||||
|
// Write raw: "0b" + binary digits
|
||||||
|
buf[0] = '0';
|
||||||
|
buf[1] = 'b';
|
||||||
|
var pos: usize = 2;
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < width) : (i += 1) {
|
||||||
|
const shift_amt: u6 = @intCast(width - 1 - i);
|
||||||
|
const bit: u8 = @intCast((value >> shift_amt) & 1);
|
||||||
|
buf[pos] = '0' + bit;
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
const raw = buf[0..pos];
|
||||||
|
|
||||||
|
// Write display: binary digits with space per nibble
|
||||||
|
const display_start = pos;
|
||||||
|
i = 0;
|
||||||
|
while (i < width) : (i += 1) {
|
||||||
|
if (i > 0 and i % 4 == 0) {
|
||||||
|
buf[pos] = ' ';
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
const shift_amt: u6 = @intCast(width - 1 - i);
|
||||||
|
const bit: u8 = @intCast((value >> shift_amt) & 1);
|
||||||
|
buf[pos] = '0' + bit;
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
const display = buf[display_start..pos];
|
||||||
|
|
||||||
|
return .{ .display = display, .raw = raw };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format an integer for programmer mode octal display.
|
||||||
|
/// Display: "0o37_777_777_777" (underscore per 3-digit group)
|
||||||
|
/// Raw: "0o37777777777" (no separators)
|
||||||
|
pub fn formatOctal(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue {
|
||||||
|
_ = bit_width; // Octal display shows all significant digits
|
||||||
|
|
||||||
|
// Write raw octal
|
||||||
|
buf[0] = '0';
|
||||||
|
buf[1] = 'o';
|
||||||
|
var pos: usize = 2;
|
||||||
|
|
||||||
|
if (value == 0) {
|
||||||
|
buf[pos] = '0';
|
||||||
|
pos += 1;
|
||||||
|
return .{ .display = buf[0..pos], .raw = buf[0..pos] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate octal digits
|
||||||
|
var digits: [22]u8 = undefined; // max 22 octal digits for u64
|
||||||
|
var digit_count: usize = 0;
|
||||||
|
var v = value;
|
||||||
|
while (v > 0) : (v /= 8) {
|
||||||
|
digits[digit_count] = @intCast(v % 8);
|
||||||
|
digit_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write raw (most significant first)
|
||||||
|
var j: usize = digit_count;
|
||||||
|
while (j > 0) {
|
||||||
|
j -= 1;
|
||||||
|
buf[pos] = '0' + digits[j];
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
const raw = buf[0..pos];
|
||||||
|
|
||||||
|
// Write display with underscores every 3 digits from the right
|
||||||
|
const display_start = pos;
|
||||||
|
buf[pos] = '0';
|
||||||
|
buf[pos + 1] = 'o';
|
||||||
|
pos += 2;
|
||||||
|
|
||||||
|
j = digit_count;
|
||||||
|
var digits_written: usize = 0;
|
||||||
|
while (j > 0) {
|
||||||
|
j -= 1;
|
||||||
|
// Insert underscore when digits_written > 0 and j is a multiple of 3
|
||||||
|
// (j represents position from right, so j%3==0 means start of a new group)
|
||||||
|
if (digits_written > 0 and j % 3 == 2 and j + 1 < digit_count) {
|
||||||
|
// Actually let's just use digits_written from the left
|
||||||
|
}
|
||||||
|
buf[pos] = '0' + digits[j];
|
||||||
|
pos += 1;
|
||||||
|
digits_written += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// That approach is wrong. Let me rewrite: figure out how many digits are in
|
||||||
|
// the leftmost group, then insert underscore every 3 after that.
|
||||||
|
// For 11 digits: leftmost group has 2 digits (11 % 3 = 2), then groups of 3.
|
||||||
|
pos = display_start;
|
||||||
|
buf[pos] = '0';
|
||||||
|
buf[pos + 1] = 'o';
|
||||||
|
pos += 2;
|
||||||
|
|
||||||
|
const first_group = if (digit_count % 3 == 0) @as(usize, 3) else digit_count % 3;
|
||||||
|
j = digit_count;
|
||||||
|
digits_written = 0;
|
||||||
|
while (j > 0) {
|
||||||
|
j -= 1;
|
||||||
|
if (digits_written > 0 and digits_written == first_group) {
|
||||||
|
buf[pos] = '_';
|
||||||
|
pos += 1;
|
||||||
|
} else if (digits_written > first_group and (digits_written - first_group) % 3 == 0) {
|
||||||
|
buf[pos] = '_';
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
buf[pos] = '0' + digits[j];
|
||||||
|
pos += 1;
|
||||||
|
digits_written += 1;
|
||||||
|
}
|
||||||
|
const display = buf[display_start..pos];
|
||||||
|
|
||||||
|
return .{ .display = display, .raw = raw };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format an unsigned integer as decimal for programmer mode.
|
||||||
|
/// Display: "4,294,967,295" (comma-separated)
|
||||||
|
/// Raw: "4294967295" (no separators)
|
||||||
|
pub fn formatDecimalUnsigned(buf: []u8, value: u64) FormattedValue {
|
||||||
|
const raw_len = writeUnsignedInt(buf, value);
|
||||||
|
const raw = buf[0..raw_len];
|
||||||
|
|
||||||
|
const display_start = raw_len;
|
||||||
|
const display_len = writeUnsignedWithCommas(buf[display_start..], value);
|
||||||
|
const display = buf[display_start..][0..display_len];
|
||||||
|
|
||||||
|
return .{ .display = display, .raw = raw };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a signed integer as decimal for programmer mode.
|
||||||
|
/// Display: "-1" or "4,294,967,295"
|
||||||
|
/// Raw: same without commas
|
||||||
|
pub fn formatDecimalSigned(buf: []u8, value: i64) FormattedValue {
|
||||||
|
const raw_len = writeSignedInt(buf, value);
|
||||||
|
const raw = buf[0..raw_len];
|
||||||
|
|
||||||
|
const display_start = raw_len;
|
||||||
|
const display_len = writeDecimalWithCommas(buf[display_start..], value);
|
||||||
|
const display = buf[display_start..][0..display_len];
|
||||||
|
|
||||||
|
return .{ .display = display, .raw = raw };
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Internal helpers --
|
||||||
|
|
||||||
|
fn hexDigit(nibble: u4) u8 {
|
||||||
|
if (nibble < 10) return '0' + @as(u8, nibble);
|
||||||
|
return 'A' + @as(u8, nibble) - 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeUnsignedInt(buf: []u8, value: u64) usize {
|
||||||
|
if (value == 0) {
|
||||||
|
buf[0] = '0';
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
var digits: [20]u8 = undefined;
|
||||||
|
var count: usize = 0;
|
||||||
|
var v = value;
|
||||||
|
while (v > 0) : (v /= 10) {
|
||||||
|
digits[count] = @intCast(v % 10);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
var pos: usize = 0;
|
||||||
|
var i: usize = count;
|
||||||
|
while (i > 0) {
|
||||||
|
i -= 1;
|
||||||
|
buf[pos] = '0' + digits[i];
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeSignedInt(buf: []u8, value: i64) usize {
|
||||||
|
if (value < 0) {
|
||||||
|
buf[0] = '-';
|
||||||
|
const abs_val: u64 = @intCast(-value);
|
||||||
|
return 1 + writeUnsignedInt(buf[1..], abs_val);
|
||||||
|
}
|
||||||
|
return writeUnsignedInt(buf, @intCast(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeUnsignedWithCommas(buf: []u8, value: u64) usize {
|
||||||
|
if (value == 0) {
|
||||||
|
buf[0] = '0';
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
var digits: [20]u8 = undefined;
|
||||||
|
var count: usize = 0;
|
||||||
|
var v = value;
|
||||||
|
while (v > 0) : (v /= 10) {
|
||||||
|
digits[count] = @intCast(v % 10);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
// digits[0] is least significant, digits[count-1] is most significant
|
||||||
|
// Write most significant first, inserting commas every 3 from the right
|
||||||
|
var pos: usize = 0;
|
||||||
|
var i: usize = count;
|
||||||
|
while (i > 0) {
|
||||||
|
i -= 1;
|
||||||
|
buf[pos] = '0' + digits[i];
|
||||||
|
pos += 1;
|
||||||
|
// Insert comma if there are more digits and position from right is multiple of 3
|
||||||
|
if (i > 0 and i % 3 == 0) {
|
||||||
|
buf[pos] = ',';
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeDecimalWithCommas(buf: []u8, value: i64) usize {
|
||||||
|
if (value < 0) {
|
||||||
|
buf[0] = '-';
|
||||||
|
const abs_val: u64 = @intCast(-value);
|
||||||
|
return 1 + writeUnsignedWithCommas(buf[1..], abs_val);
|
||||||
|
}
|
||||||
|
return writeUnsignedWithCommas(buf, @intCast(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Tests --
|
||||||
|
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
test "formatFloat: integer value" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatFloat(&buf, 42.0);
|
||||||
|
try testing.expectEqualStrings("42", result.display);
|
||||||
|
try testing.expectEqualStrings("42", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatFloat: large integer with commas" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatFloat(&buf, 4294967295.0);
|
||||||
|
try testing.expectEqualStrings("4,294,967,295", result.display);
|
||||||
|
try testing.expectEqualStrings("4294967295", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatFloat: negative integer" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatFloat(&buf, -1234.0);
|
||||||
|
try testing.expectEqualStrings("-1,234", result.display);
|
||||||
|
try testing.expectEqualStrings("-1234", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatFloat: million" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatFloat(&buf, 1000000.0);
|
||||||
|
try testing.expectEqualStrings("1,000,000", result.display);
|
||||||
|
try testing.expectEqualStrings("1000000", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatFloat: zero" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatFloat(&buf, 0.0);
|
||||||
|
try testing.expectEqualStrings("0", result.display);
|
||||||
|
try testing.expectEqualStrings("0", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatHex: 8-bit" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatHex(&buf, 0xFF, .bits8);
|
||||||
|
try testing.expectEqualStrings("0xFF", result.display);
|
||||||
|
try testing.expectEqualStrings("0xFF", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatHex: 16-bit" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatHex(&buf, 0xABCD, .bits16);
|
||||||
|
try testing.expectEqualStrings("0xABCD", result.display);
|
||||||
|
try testing.expectEqualStrings("0xABCD", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatHex: 32-bit with grouping" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatHex(&buf, 0xDEADBEEF, .bits32);
|
||||||
|
try testing.expectEqualStrings("0xDEAD_BEEF", result.display);
|
||||||
|
try testing.expectEqualStrings("0xDEADBEEF", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatHex: 64-bit with grouping" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatHex(&buf, 0xDEAD_BEEF_CAFE_BABE, .bits64);
|
||||||
|
try testing.expectEqualStrings("0xDEAD_BEEF_CAFE_BABE", result.display);
|
||||||
|
try testing.expectEqualStrings("0xDEADBEEFCAFEBABE", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatHex: zero 32-bit" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatHex(&buf, 0, .bits32);
|
||||||
|
try testing.expectEqualStrings("0x0000_0000", result.display);
|
||||||
|
try testing.expectEqualStrings("0x00000000", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatBinary: 8-bit" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatBinary(&buf, 0xFF, .bits8);
|
||||||
|
try testing.expectEqualStrings("1111 1111", result.display);
|
||||||
|
try testing.expectEqualStrings("0b11111111", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatBinary: 8-bit mixed" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatBinary(&buf, 0xA5, .bits8);
|
||||||
|
try testing.expectEqualStrings("1010 0101", result.display);
|
||||||
|
try testing.expectEqualStrings("0b10100101", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatBinary: 16-bit" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatBinary(&buf, 0x000F, .bits16);
|
||||||
|
try testing.expectEqualStrings("0000 0000 0000 1111", result.display);
|
||||||
|
try testing.expectEqualStrings("0b0000000000001111", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatOctal: simple" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatOctal(&buf, 511, .bits16);
|
||||||
|
try testing.expectEqualStrings("0o777", result.display);
|
||||||
|
try testing.expectEqualStrings("0o777", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatOctal: large with grouping" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatOctal(&buf, 0xFFFF_FFFF, .bits32);
|
||||||
|
// 0xFFFFFFFF = 37777777777 octal (11 digits)
|
||||||
|
try testing.expectEqualStrings("0o37_777_777_777", result.display);
|
||||||
|
try testing.expectEqualStrings("0o37777777777", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatOctal: zero" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatOctal(&buf, 0, .bits8);
|
||||||
|
try testing.expectEqualStrings("0o0", result.display);
|
||||||
|
try testing.expectEqualStrings("0o0", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatDecimalUnsigned: simple" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatDecimalUnsigned(&buf, 255);
|
||||||
|
try testing.expectEqualStrings("255", result.display);
|
||||||
|
try testing.expectEqualStrings("255", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatDecimalUnsigned: large" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatDecimalUnsigned(&buf, 4294967295);
|
||||||
|
try testing.expectEqualStrings("4,294,967,295", result.display);
|
||||||
|
try testing.expectEqualStrings("4294967295", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatDecimalSigned: negative" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatDecimalSigned(&buf, -1);
|
||||||
|
try testing.expectEqualStrings("-1", result.display);
|
||||||
|
try testing.expectEqualStrings("-1", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatDecimalSigned: negative large" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatDecimalSigned(&buf, -1234567);
|
||||||
|
try testing.expectEqualStrings("-1,234,567", result.display);
|
||||||
|
try testing.expectEqualStrings("-1234567", result.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatFloat: very large number uses scientific notation" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatFloat(&buf, 1.5e16);
|
||||||
|
// Should use scientific notation for values > 1e15
|
||||||
|
try testing.expect(std.mem.indexOf(u8, result.display, "e") != null or
|
||||||
|
std.mem.indexOf(u8, result.display, "E") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatFloat: very small number uses scientific notation" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatFloat(&buf, 1.5e-16);
|
||||||
|
try testing.expect(std.mem.indexOf(u8, result.display, "e") != null or
|
||||||
|
std.mem.indexOf(u8, result.display, "E") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatFloat: regular float" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const result = formatFloat(&buf, 3.14159);
|
||||||
|
try testing.expect(std.mem.indexOf(u8, result.display, "3.14") != null);
|
||||||
|
}
|
||||||
567
engine/src/parser.zig
Normal file
567
engine/src/parser.zig
Normal file
|
|
@ -0,0 +1,567 @@
|
||||||
|
//! Pratt parser for Tally expressions.
|
||||||
|
//!
|
||||||
|
//! Uses top-down operator precedence (Pratt parsing) to convert a token
|
||||||
|
//! stream into an AST. Handles:
|
||||||
|
//! - Operator precedence and associativity
|
||||||
|
//! - Unary prefix operators (-, ~)
|
||||||
|
//! - Function calls: identifier(args...)
|
||||||
|
//! - Implicit multiplication: 2pi, 3(4+5), (2)(3)
|
||||||
|
//! - Mode-dependent ^ (power in standard, XOR in programmer)
|
||||||
|
//! - Variable assignment: X = expr
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const ast = @import("ast.zig");
|
||||||
|
const Expr = ast.Expr;
|
||||||
|
const BinaryOp = ast.BinaryOp;
|
||||||
|
const UnaryOp = ast.UnaryOp;
|
||||||
|
const tokenizer_mod = @import("tokenizer.zig");
|
||||||
|
const Tokenizer = tokenizer_mod.Tokenizer;
|
||||||
|
const TokenKind = tokenizer_mod.TokenKind;
|
||||||
|
const Token = tokenizer_mod.Token;
|
||||||
|
const parseNumber = tokenizer_mod.parseNumber;
|
||||||
|
const types = @import("types.zig");
|
||||||
|
const Mode = types.Mode;
|
||||||
|
const CalcError = types.CalcError;
|
||||||
|
|
||||||
|
/// Precedence levels (higher = tighter binding).
|
||||||
|
const Prec = enum(u8) {
|
||||||
|
none = 0,
|
||||||
|
assignment = 1, // =
|
||||||
|
bit_or = 2, // |
|
||||||
|
bit_xor = 3, // ^ (programmer mode)
|
||||||
|
bit_and = 4, // &
|
||||||
|
shift = 5, // << >> >>> rol ror
|
||||||
|
additive = 6, // + -
|
||||||
|
multiplicative = 7, // * / %
|
||||||
|
power = 8, // ^ (standard mode) or **
|
||||||
|
unary = 9, // - ~ (prefix)
|
||||||
|
call = 10, // function calls, implicit mul
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Parser = struct {
|
||||||
|
source: []const u8,
|
||||||
|
tokenizer: Tokenizer,
|
||||||
|
current: Token,
|
||||||
|
previous: Token,
|
||||||
|
mode: Mode,
|
||||||
|
allocator: Allocator,
|
||||||
|
had_error: bool,
|
||||||
|
error_pos: ?usize,
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, source: []const u8, mode: Mode) Parser {
|
||||||
|
var tok = Tokenizer.init(source, mode);
|
||||||
|
const first = tok.next();
|
||||||
|
return .{
|
||||||
|
.source = source,
|
||||||
|
.tokenizer = tok,
|
||||||
|
.current = first,
|
||||||
|
.previous = .{ .kind = .eof, .start = 0, .len = 0 },
|
||||||
|
.mode = mode,
|
||||||
|
.allocator = allocator,
|
||||||
|
.had_error = false,
|
||||||
|
.error_pos = null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a complete expression. Returns error if parsing fails.
|
||||||
|
pub fn parse(self: *Parser) CalcError!*Expr {
|
||||||
|
const expr = try self.parseExpr(.none);
|
||||||
|
if (self.current.kind != .eof) {
|
||||||
|
self.had_error = true;
|
||||||
|
self.error_pos = self.current.start;
|
||||||
|
return CalcError.UnexpectedToken;
|
||||||
|
}
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse an expression with the given minimum precedence.
|
||||||
|
fn parseExpr(self: *Parser, min_prec: Prec) CalcError!*Expr {
|
||||||
|
var left = try self.parsePrefix();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// Check for implicit multiplication first:
|
||||||
|
// If current token starts a value and isn't an operator, insert mul.
|
||||||
|
if (self.isImplicitMul()) {
|
||||||
|
const mul_prec = Prec.multiplicative;
|
||||||
|
if (@intFromEnum(mul_prec) <= @intFromEnum(min_prec)) break;
|
||||||
|
const right = try self.parseExpr(mul_prec);
|
||||||
|
left = try self.makeNode(.{ .binary = .{
|
||||||
|
.op = .mul,
|
||||||
|
.left = left,
|
||||||
|
.right = right,
|
||||||
|
} });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prec = self.infixPrecedence(self.current.kind);
|
||||||
|
if (@intFromEnum(prec) <= @intFromEnum(min_prec)) break;
|
||||||
|
|
||||||
|
left = try self.parseInfix(left, prec);
|
||||||
|
}
|
||||||
|
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a prefix expression (number, identifier, unary op, parenthesized).
|
||||||
|
fn parsePrefix(self: *Parser) CalcError!*Expr {
|
||||||
|
const tok = self.current;
|
||||||
|
switch (tok.kind) {
|
||||||
|
.number => {
|
||||||
|
self.advance();
|
||||||
|
const text = tok.text(self.source);
|
||||||
|
const num = parseNumber(text) catch return CalcError.InvalidNumber;
|
||||||
|
return self.makeNode(.{ .number = .{
|
||||||
|
.float_value = num.float,
|
||||||
|
.int_value = num.int_value,
|
||||||
|
.base = num.base,
|
||||||
|
} });
|
||||||
|
},
|
||||||
|
.identifier => {
|
||||||
|
self.advance();
|
||||||
|
const name = tok.text(self.source);
|
||||||
|
|
||||||
|
// Check for assignment: identifier = expr
|
||||||
|
if (self.current.kind == .equals) {
|
||||||
|
self.advance();
|
||||||
|
const value = try self.parseExpr(.none);
|
||||||
|
return self.makeNode(.{ .assignment = .{
|
||||||
|
.name = name,
|
||||||
|
.value = value,
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for function call: identifier(args)
|
||||||
|
if (self.current.kind == .left_paren) {
|
||||||
|
self.advance(); // consume (
|
||||||
|
var args = std.ArrayList(*Expr).empty;
|
||||||
|
defer args.deinit(self.allocator);
|
||||||
|
|
||||||
|
if (self.current.kind != .right_paren) {
|
||||||
|
const first_arg = try self.parseExpr(.none);
|
||||||
|
args.append(self.allocator, first_arg) catch return CalcError.OutOfMemory;
|
||||||
|
|
||||||
|
while (self.current.kind == .comma) {
|
||||||
|
self.advance(); // consume ,
|
||||||
|
const arg = try self.parseExpr(.none);
|
||||||
|
args.append(self.allocator, arg) catch return CalcError.OutOfMemory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.current.kind != .right_paren) {
|
||||||
|
self.had_error = true;
|
||||||
|
self.error_pos = self.current.start;
|
||||||
|
return CalcError.UnmatchedParen;
|
||||||
|
}
|
||||||
|
self.advance(); // consume )
|
||||||
|
|
||||||
|
const args_slice = self.allocator.dupe(*Expr, args.items) catch
|
||||||
|
return CalcError.OutOfMemory;
|
||||||
|
|
||||||
|
return self.makeNode(.{ .call = .{
|
||||||
|
.name = name,
|
||||||
|
.args = args_slice,
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for keyword operators (rol, ror) - these are identifiers
|
||||||
|
// that act as infix operators, handled in parseInfix via infixPrecedence
|
||||||
|
// Only reach here if it's a plain variable reference.
|
||||||
|
return self.makeNode(.{ .variable = name });
|
||||||
|
},
|
||||||
|
.left_paren => {
|
||||||
|
self.advance(); // consume (
|
||||||
|
const inner = try self.parseExpr(.none);
|
||||||
|
if (self.current.kind != .right_paren) {
|
||||||
|
self.had_error = true;
|
||||||
|
self.error_pos = self.current.start;
|
||||||
|
return CalcError.UnmatchedParen;
|
||||||
|
}
|
||||||
|
self.advance(); // consume )
|
||||||
|
return inner;
|
||||||
|
},
|
||||||
|
.minus => {
|
||||||
|
self.advance();
|
||||||
|
const operand = try self.parseExpr(.unary);
|
||||||
|
return self.makeNode(.{ .unary = .{
|
||||||
|
.op = .negate,
|
||||||
|
.operand = operand,
|
||||||
|
} });
|
||||||
|
},
|
||||||
|
.tilde => {
|
||||||
|
self.advance();
|
||||||
|
const operand = try self.parseExpr(.unary);
|
||||||
|
return self.makeNode(.{ .unary = .{
|
||||||
|
.op = .bitwise_not,
|
||||||
|
.operand = operand,
|
||||||
|
} });
|
||||||
|
},
|
||||||
|
.eof => {
|
||||||
|
self.had_error = true;
|
||||||
|
self.error_pos = tok.start;
|
||||||
|
return CalcError.UnexpectedEnd;
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
self.had_error = true;
|
||||||
|
self.error_pos = tok.start;
|
||||||
|
return CalcError.UnexpectedToken;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse an infix expression given the left-hand side and precedence.
|
||||||
|
fn parseInfix(self: *Parser, left: *Expr, prec: Prec) CalcError!*Expr {
|
||||||
|
const tok = self.current;
|
||||||
|
|
||||||
|
// Handle keyword operators (rol, ror)
|
||||||
|
if (tok.kind == .identifier) {
|
||||||
|
const name = tok.text(self.source);
|
||||||
|
if (std.mem.eql(u8, name, "rol") or std.mem.eql(u8, name, "ror")) {
|
||||||
|
self.advance();
|
||||||
|
const op: BinaryOp = if (std.mem.eql(u8, name, "rol")) .rotate_left else .rotate_right;
|
||||||
|
const right = try self.parseExpr(prec);
|
||||||
|
return self.makeNode(.{ .binary = .{
|
||||||
|
.op = op,
|
||||||
|
.left = left,
|
||||||
|
.right = right,
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.advance();
|
||||||
|
const op = self.tokenToBinaryOp(tok.kind) orelse {
|
||||||
|
self.had_error = true;
|
||||||
|
self.error_pos = tok.start;
|
||||||
|
return CalcError.UnexpectedToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Right-associative for power
|
||||||
|
const next_prec: Prec = if (op == .pow)
|
||||||
|
@enumFromInt(@intFromEnum(prec) - 1)
|
||||||
|
else
|
||||||
|
prec;
|
||||||
|
|
||||||
|
const right = try self.parseExpr(next_prec);
|
||||||
|
return self.makeNode(.{ .binary = .{
|
||||||
|
.op = op,
|
||||||
|
.left = left,
|
||||||
|
.right = right,
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine if the current position represents an implicit multiplication.
|
||||||
|
/// This happens when the current token could start a new value expression
|
||||||
|
/// and the previous token ended a value expression, with no operator between.
|
||||||
|
fn isImplicitMul(self: *Parser) bool {
|
||||||
|
// Implicit mul only happens when infixPrecedence returns .none
|
||||||
|
// (meaning the current token is NOT a recognized infix operator)
|
||||||
|
// but IS the start of a value expression.
|
||||||
|
if (self.infixPrecedence(self.current.kind) != .none) return false;
|
||||||
|
|
||||||
|
return switch (self.current.kind) {
|
||||||
|
.number, .left_paren => true,
|
||||||
|
.identifier => {
|
||||||
|
// Don't treat keyword operators as implicit mul
|
||||||
|
const name = self.current.text(self.source);
|
||||||
|
if (std.mem.eql(u8, name, "rol") or std.mem.eql(u8, name, "ror") or
|
||||||
|
std.mem.eql(u8, name, "to"))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the infix precedence of a token kind.
|
||||||
|
fn infixPrecedence(self: *Parser, kind: TokenKind) Prec {
|
||||||
|
return switch (kind) {
|
||||||
|
.pipe => .bit_or,
|
||||||
|
.caret => if (self.mode == .programmer) .bit_xor else .power,
|
||||||
|
.ampersand => .bit_and,
|
||||||
|
.shift_left, .shift_right, .shift_right_logical => .shift,
|
||||||
|
.plus, .minus => .additive,
|
||||||
|
.star, .slash, .percent => .multiplicative,
|
||||||
|
.star_star => .power,
|
||||||
|
.identifier => blk: {
|
||||||
|
// "rol" and "ror" are infix keyword operators at shift precedence
|
||||||
|
const name = self.currentText(kind);
|
||||||
|
if (name != null) {
|
||||||
|
if (std.mem.eql(u8, name.?, "rol") or std.mem.eql(u8, name.?, "ror")) {
|
||||||
|
break :blk .shift;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break :blk .none;
|
||||||
|
},
|
||||||
|
else => .none,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn currentText(self: *Parser, kind: TokenKind) ?[]const u8 {
|
||||||
|
if (kind == .identifier) {
|
||||||
|
return self.current.text(self.source);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map a token kind to a binary operator.
|
||||||
|
fn tokenToBinaryOp(self: *Parser, kind: TokenKind) ?BinaryOp {
|
||||||
|
return switch (kind) {
|
||||||
|
.plus => .add,
|
||||||
|
.minus => .sub,
|
||||||
|
.star => .mul,
|
||||||
|
.slash => .div,
|
||||||
|
.percent => .mod,
|
||||||
|
.caret => if (self.mode == .programmer) .bit_xor else .pow,
|
||||||
|
.star_star => .pow,
|
||||||
|
.ampersand => .bit_and,
|
||||||
|
.pipe => .bit_or,
|
||||||
|
.shift_left => .shift_left,
|
||||||
|
.shift_right => .shift_right,
|
||||||
|
.shift_right_logical => .shift_right_logical,
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance(self: *Parser) void {
|
||||||
|
self.previous = self.current;
|
||||||
|
self.current = self.tokenizer.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn makeNode(self: *Parser, expr: Expr) CalcError!*Expr {
|
||||||
|
const node = self.allocator.create(Expr) catch return CalcError.OutOfMemory;
|
||||||
|
node.* = expr;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// -- Tests --
|
||||||
|
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
// Use an arena for tests so error paths don't leak
|
||||||
|
var test_arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
|
|
||||||
|
fn testParse(source: []const u8, mode: Mode) !*Expr {
|
||||||
|
// Reset arena between test calls isn't needed since each test is independent
|
||||||
|
// and we use testing.allocator for successful parses (with manual free),
|
||||||
|
// but for error cases we need an arena.
|
||||||
|
var parser = Parser.init(testing.allocator, source, mode);
|
||||||
|
return parser.parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn testParseArena(source: []const u8, mode: Mode) CalcError!*Expr {
|
||||||
|
const alloc = test_arena_instance.allocator();
|
||||||
|
var p = Parser.init(alloc, source, mode);
|
||||||
|
return p.parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn freeExpr(allocator: Allocator, expr: *Expr) void {
|
||||||
|
switch (expr.*) {
|
||||||
|
.number => {},
|
||||||
|
.variable => {},
|
||||||
|
.unary => |u| freeExpr(allocator, u.operand),
|
||||||
|
.binary => |b| {
|
||||||
|
freeExpr(allocator, b.left);
|
||||||
|
freeExpr(allocator, b.right);
|
||||||
|
},
|
||||||
|
.call => |c| {
|
||||||
|
for (c.args) |arg| freeExpr(allocator, arg);
|
||||||
|
allocator.free(c.args);
|
||||||
|
},
|
||||||
|
.assignment => |a| freeExpr(allocator, a.value),
|
||||||
|
}
|
||||||
|
allocator.destroy(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse simple number" {
|
||||||
|
const expr = try testParse("42", .standard);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqual(@as(f64, 42.0), expr.number.float_value);
|
||||||
|
try testing.expectEqual(@as(?u64, 42), expr.number.int_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse hex number" {
|
||||||
|
const expr = try testParse("0xFF", .programmer);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqual(@as(?u64, 255), expr.number.int_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse addition" {
|
||||||
|
const expr = try testParse("2 + 3", .standard);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqual(BinaryOp.add, expr.binary.op);
|
||||||
|
try testing.expectEqual(@as(f64, 2.0), expr.binary.left.number.float_value);
|
||||||
|
try testing.expectEqual(@as(f64, 3.0), expr.binary.right.number.float_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse precedence: mul before add" {
|
||||||
|
// 2 + 3 * 4 should parse as 2 + (3 * 4)
|
||||||
|
const expr = try testParse("2 + 3 * 4", .standard);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqual(BinaryOp.add, expr.binary.op);
|
||||||
|
try testing.expectEqual(@as(f64, 2.0), expr.binary.left.number.float_value);
|
||||||
|
try testing.expectEqual(BinaryOp.mul, expr.binary.right.binary.op);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse precedence: power right-associative" {
|
||||||
|
// 2^3^4 should parse as 2^(3^4)
|
||||||
|
const expr = try testParse("2^3^4", .standard);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqual(BinaryOp.pow, expr.binary.op);
|
||||||
|
try testing.expectEqual(@as(f64, 2.0), expr.binary.left.number.float_value);
|
||||||
|
try testing.expectEqual(BinaryOp.pow, expr.binary.right.binary.op);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse unary negation" {
|
||||||
|
const expr = try testParse("-5", .standard);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqual(UnaryOp.negate, expr.unary.op);
|
||||||
|
try testing.expectEqual(@as(f64, 5.0), expr.unary.operand.number.float_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse negation in expression" {
|
||||||
|
// -2 + 3 should be (-2) + 3
|
||||||
|
const expr = try testParse("-2 + 3", .standard);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqual(BinaryOp.add, expr.binary.op);
|
||||||
|
try testing.expectEqual(UnaryOp.negate, expr.binary.left.unary.op);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse parentheses" {
|
||||||
|
// (2 + 3) * 4
|
||||||
|
const expr = try testParse("(2 + 3) * 4", .standard);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqual(BinaryOp.mul, expr.binary.op);
|
||||||
|
try testing.expectEqual(BinaryOp.add, expr.binary.left.binary.op);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse function call" {
|
||||||
|
const expr = try testParse("sin(3.14)", .standard);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqualStrings("sin", expr.call.name);
|
||||||
|
try testing.expectEqual(@as(usize, 1), expr.call.args.len);
|
||||||
|
try testing.expectApproxEqAbs(@as(f64, 3.14), expr.call.args[0].number.float_value, 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse multi-arg function call" {
|
||||||
|
const expr = try testParse("max(1, 2, 3)", .standard);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqualStrings("max", expr.call.name);
|
||||||
|
try testing.expectEqual(@as(usize, 3), expr.call.args.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse variable" {
|
||||||
|
const expr = try testParse("pi", .standard);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqualStrings("pi", expr.variable);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse assignment" {
|
||||||
|
const expr = try testParse("X = 42", .standard);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqualStrings("X", expr.assignment.name);
|
||||||
|
try testing.expectEqual(@as(f64, 42.0), expr.assignment.value.number.float_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse implicit mul: number identifier" {
|
||||||
|
// 2pi should parse as 2 * pi
|
||||||
|
const expr = try testParse("2pi", .standard);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqual(BinaryOp.mul, expr.binary.op);
|
||||||
|
try testing.expectEqual(@as(f64, 2.0), expr.binary.left.number.float_value);
|
||||||
|
try testing.expectEqualStrings("pi", expr.binary.right.variable);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse implicit mul: number paren" {
|
||||||
|
// 3(4+5) should parse as 3 * (4+5)
|
||||||
|
const expr = try testParse("3(4+5)", .standard);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqual(BinaryOp.mul, expr.binary.op);
|
||||||
|
try testing.expectEqual(@as(f64, 3.0), expr.binary.left.number.float_value);
|
||||||
|
try testing.expectEqual(BinaryOp.add, expr.binary.right.binary.op);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse implicit mul: paren paren" {
|
||||||
|
// (2)(3) should parse as (2) * (3)
|
||||||
|
const expr = try testParse("(2)(3)", .standard);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqual(BinaryOp.mul, expr.binary.op);
|
||||||
|
try testing.expectEqual(@as(f64, 2.0), expr.binary.left.number.float_value);
|
||||||
|
try testing.expectEqual(@as(f64, 3.0), expr.binary.right.number.float_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse caret as XOR in programmer mode" {
|
||||||
|
const expr = try testParse("0xF ^ 0x3", .programmer);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqual(BinaryOp.bit_xor, expr.binary.op);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse caret as power in standard mode" {
|
||||||
|
const expr = try testParse("2 ^ 10", .standard);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqual(BinaryOp.pow, expr.binary.op);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse ** as power in programmer mode" {
|
||||||
|
const expr = try testParse("2 ** 10", .programmer);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqual(BinaryOp.pow, expr.binary.op);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse bitwise operators" {
|
||||||
|
const expr = try testParse("0xF & 0x3 | 0x1", .programmer);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
// | has lowest precedence of these, so: (0xF & 0x3) | 0x1
|
||||||
|
try testing.expectEqual(BinaryOp.bit_or, expr.binary.op);
|
||||||
|
try testing.expectEqual(BinaryOp.bit_and, expr.binary.left.binary.op);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse shift operators" {
|
||||||
|
const expr = try testParse("1 << 4", .programmer);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqual(BinaryOp.shift_left, expr.binary.op);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse bitwise not" {
|
||||||
|
const expr = try testParse("~0xFF", .programmer);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqual(UnaryOp.bitwise_not, expr.unary.op);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse error: unmatched paren" {
|
||||||
|
defer _ = test_arena_instance.reset(.retain_capacity);
|
||||||
|
const result = testParseArena("(2 + 3", .standard);
|
||||||
|
try testing.expectError(CalcError.UnmatchedParen, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse error: unexpected token" {
|
||||||
|
defer _ = test_arena_instance.reset(.retain_capacity);
|
||||||
|
const result = testParseArena("+ +", .standard);
|
||||||
|
// + at start is not a valid prefix
|
||||||
|
try testing.expectError(CalcError.UnexpectedToken, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse error: empty expression" {
|
||||||
|
defer _ = test_arena_instance.reset(.retain_capacity);
|
||||||
|
const result = testParseArena("", .standard);
|
||||||
|
try testing.expectError(CalcError.UnexpectedEnd, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse complex expression" {
|
||||||
|
// sin(2*pi) + 1
|
||||||
|
const expr = try testParse("sin(2*pi) + 1", .standard);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqual(BinaryOp.add, expr.binary.op);
|
||||||
|
try testing.expectEqualStrings("sin", expr.binary.left.call.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse nested function calls" {
|
||||||
|
const expr = try testParse("max(sin(1), cos(2))", .standard);
|
||||||
|
defer freeExpr(testing.allocator, expr);
|
||||||
|
try testing.expectEqualStrings("max", expr.call.name);
|
||||||
|
try testing.expectEqual(@as(usize, 2), expr.call.args.len);
|
||||||
|
try testing.expectEqualStrings("sin", expr.call.args[0].call.name);
|
||||||
|
try testing.expectEqualStrings("cos", expr.call.args[1].call.name);
|
||||||
|
}
|
||||||
374
engine/src/programmer.zig
Normal file
374
engine/src/programmer.zig
Normal file
|
|
@ -0,0 +1,374 @@
|
||||||
|
//! Programmer mode evaluator for Tally.
|
||||||
|
//!
|
||||||
|
//! All operations use exact integer arithmetic (u64 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 u64.
|
||||||
|
fn evalExpr(config: ProgrammerConfig, expr: *const Expr) CalcError!u64 {
|
||||||
|
switch (expr.*) {
|
||||||
|
.number => |n| {
|
||||||
|
if (n.int_value) |int_val| {
|
||||||
|
return int_val & config.bit_width.mask();
|
||||||
|
}
|
||||||
|
// Float in programmer mode: truncate to integer
|
||||||
|
if (n.float_value < 0) {
|
||||||
|
const neg: i64 = @intFromFloat(n.float_value);
|
||||||
|
return @as(u64, @bitCast(neg)) & config.bit_width.mask();
|
||||||
|
}
|
||||||
|
const val: u64 = @intFromFloat(n.float_value);
|
||||||
|
return val & config.bit_width.mask();
|
||||||
|
},
|
||||||
|
.variable => {
|
||||||
|
// Programmer mode doesn't support named variables (yet)
|
||||||
|
// Could add register-style variables later
|
||||||
|
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 u64 values, masked to bit width.
|
||||||
|
fn evalBinaryOp(config: ProgrammerConfig, op: BinaryOp, left: u64, right: u64) CalcError!u64 {
|
||||||
|
const mask = config.bit_width.mask();
|
||||||
|
const width = config.bit_width.bits();
|
||||||
|
|
||||||
|
const result: u64 = 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: u64 = 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: u6 = 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: u6 = if (right >= width)
|
||||||
|
@intCast(width - 1)
|
||||||
|
else
|
||||||
|
@intCast(right);
|
||||||
|
// Sign-extend, shift, then mask
|
||||||
|
const sign_bit: u64 = @as(u64, 1) << @intCast(width - 1);
|
||||||
|
if (left & sign_bit != 0) {
|
||||||
|
// Negative: fill with 1s from the top
|
||||||
|
const extended = left | ~mask;
|
||||||
|
const shifted: u64 = @bitCast(@as(i64, @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: u6 = if (right >= width)
|
||||||
|
@intCast(width - 1)
|
||||||
|
else
|
||||||
|
@intCast(right);
|
||||||
|
break :blk (left >> shift_amt) & mask;
|
||||||
|
},
|
||||||
|
.rotate_left => blk: {
|
||||||
|
const amt: u6 = @intCast(@mod(right, width));
|
||||||
|
if (amt == 0) break :blk left;
|
||||||
|
const anti: u6 = @intCast(width - amt);
|
||||||
|
break :blk ((left << amt) | (left >> anti)) & mask;
|
||||||
|
},
|
||||||
|
.rotate_right => blk: {
|
||||||
|
const amt: u6 = @intCast(@mod(right, width));
|
||||||
|
if (amt == 0) break :blk left;
|
||||||
|
const anti: u6 = @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(u64, 42), result.unsignedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prog: hex number" {
|
||||||
|
const result = try testProg("0xFF");
|
||||||
|
try testing.expectEqual(@as(u64, 255), result.unsignedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prog: binary number" {
|
||||||
|
const result = try testProg("0b1010");
|
||||||
|
try testing.expectEqual(@as(u64, 10), result.unsignedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prog: addition" {
|
||||||
|
const result = try testProg("10 + 20");
|
||||||
|
try testing.expectEqual(@as(u64, 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(u64, 251), result.unsignedValue()); // 256 - 5
|
||||||
|
try testing.expectEqual(@as(i64, -5), result.signedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prog: multiplication" {
|
||||||
|
const result = try testProg("6 * 7");
|
||||||
|
try testing.expectEqual(@as(u64, 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(u64, 144), result.unsignedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prog: division" {
|
||||||
|
const result = try testProg("100 / 4");
|
||||||
|
try testing.expectEqual(@as(u64, 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(u64, 1), result.unsignedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prog: power" {
|
||||||
|
const result = try testProg("2 ** 10");
|
||||||
|
try testing.expectEqual(@as(u64, 1024), result.unsignedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prog: bitwise AND" {
|
||||||
|
const result = try testProg("0xFF & 0x0F");
|
||||||
|
try testing.expectEqual(@as(u64, 0x0F), result.unsignedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prog: bitwise OR" {
|
||||||
|
const result = try testProg("0xF0 | 0x0F");
|
||||||
|
try testing.expectEqual(@as(u64, 0xFF), result.unsignedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prog: bitwise XOR" {
|
||||||
|
const result = try testProg("0xFF ^ 0x0F");
|
||||||
|
try testing.expectEqual(@as(u64, 0xF0), result.unsignedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prog: bitwise NOT 8-bit" {
|
||||||
|
const result = try testProgWith("~0x0F", .{ .bit_width = .bits8 });
|
||||||
|
try testing.expectEqual(@as(u64, 0xF0), result.unsignedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prog: bitwise NOT 16-bit" {
|
||||||
|
const result = try testProgWith("~0x00FF", .{ .bit_width = .bits16 });
|
||||||
|
try testing.expectEqual(@as(u64, 0xFF00), result.unsignedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prog: bitwise NOT 32-bit" {
|
||||||
|
const result = try testProgWith("~0", .{ .bit_width = .bits32 });
|
||||||
|
try testing.expectEqual(@as(u64, 0xFFFF_FFFF), result.unsignedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prog: shift left" {
|
||||||
|
const result = try testProg("1 << 8");
|
||||||
|
try testing.expectEqual(@as(u64, 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(u64, 128), result.unsignedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prog: logical shift right" {
|
||||||
|
const result = try testProgWith("0x80 >>> 4", .{ .bit_width = .bits8 });
|
||||||
|
try testing.expectEqual(@as(u64, 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(u64, 0xC0), result.unsignedValue());
|
||||||
|
try testing.expectEqual(@as(i64, -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(u64, 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(u64, 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(u64, 0xC0), result.unsignedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prog: negation two's complement" {
|
||||||
|
const result = try testProgWith("-1", .{ .bit_width = .bits8 });
|
||||||
|
try testing.expectEqual(@as(u64, 0xFF), result.unsignedValue());
|
||||||
|
try testing.expectEqual(@as(i64, -1), result.signedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prog: negation 16-bit" {
|
||||||
|
const result = try testProgWith("-42", .{ .bit_width = .bits16 });
|
||||||
|
try testing.expectEqual(@as(i64, -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(u64, 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(u64, 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(u64, 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(u64, 0xFF), result.unsignedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prog: 32-bit operations" {
|
||||||
|
const result = try testProgWith("0xFFFF_FFFF + 1", .{ .bit_width = .bits32 });
|
||||||
|
try testing.expectEqual(@as(u64, 0), result.unsignedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prog: 64-bit max" {
|
||||||
|
const result = try testProgWith("~0", .{ .bit_width = .bits64 });
|
||||||
|
try testing.expectEqual(@as(u64, 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(i64, -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);
|
||||||
|
}
|
||||||
644
engine/src/tokenizer.zig
Normal file
644
engine/src/tokenizer.zig
Normal file
|
|
@ -0,0 +1,644 @@
|
||||||
|
//! Expression tokenizer for Tally.
|
||||||
|
//!
|
||||||
|
//! Converts an input string into a sequence of tokens for the parser.
|
||||||
|
//! Supports multiple number bases (decimal, hex 0x, octal 0o, binary 0b),
|
||||||
|
//! operators, identifiers (functions/variables), and implicit multiplication.
|
||||||
|
//!
|
||||||
|
//! The `TokenStream` wraps the raw `Tokenizer` and inserts synthetic `star`
|
||||||
|
//! tokens for implicit multiplication (e.g. `2pi`, `3(4+5)`, `)(` ).
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const types = @import("types.zig");
|
||||||
|
const Mode = types.Mode;
|
||||||
|
const Base = types.Base;
|
||||||
|
|
||||||
|
pub const TokenKind = enum {
|
||||||
|
// Literals
|
||||||
|
number,
|
||||||
|
// Identifiers (function names, variable names, keywords like "to", "rol", "ror")
|
||||||
|
identifier,
|
||||||
|
|
||||||
|
// Operators
|
||||||
|
plus,
|
||||||
|
minus,
|
||||||
|
star,
|
||||||
|
slash,
|
||||||
|
percent,
|
||||||
|
caret, // ^ (power in standard, XOR in programmer)
|
||||||
|
star_star, // ** (power in programmer)
|
||||||
|
ampersand, // &
|
||||||
|
pipe, // |
|
||||||
|
tilde, // ~
|
||||||
|
shift_left, // <<
|
||||||
|
shift_right, // >> (arithmetic)
|
||||||
|
shift_right_logical, // >>>
|
||||||
|
|
||||||
|
// Delimiters
|
||||||
|
left_paren,
|
||||||
|
right_paren,
|
||||||
|
comma,
|
||||||
|
semicolon,
|
||||||
|
equals, // = (assignment)
|
||||||
|
|
||||||
|
// Special
|
||||||
|
eof,
|
||||||
|
invalid,
|
||||||
|
|
||||||
|
/// Returns true if this token can be the last token before an implicit
|
||||||
|
/// multiplication (i.e. it produces a value).
|
||||||
|
pub fn isValueEnd(self: TokenKind) bool {
|
||||||
|
return switch (self) {
|
||||||
|
.number, .identifier, .right_paren => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if this token can be the first token after an implicit
|
||||||
|
/// multiplication (i.e. it starts a value).
|
||||||
|
pub fn isValueStart(self: TokenKind) bool {
|
||||||
|
return switch (self) {
|
||||||
|
.number, .identifier, .left_paren, .tilde => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Token = struct {
|
||||||
|
kind: TokenKind,
|
||||||
|
/// Byte offset into the source where this token starts.
|
||||||
|
start: usize,
|
||||||
|
/// Byte length of this token in the source.
|
||||||
|
len: usize,
|
||||||
|
|
||||||
|
/// Extract the token's text from the source.
|
||||||
|
pub fn text(self: Token, source: []const u8) []const u8 {
|
||||||
|
return source[self.start..][0..self.len];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Parsed number value from a token.
|
||||||
|
pub const NumberValue = struct {
|
||||||
|
float: f64,
|
||||||
|
/// If the number is a pure integer (no decimal point, no exponent), this
|
||||||
|
/// holds the exact integer value.
|
||||||
|
int_value: ?u64,
|
||||||
|
base: Base,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Parse a number token's text into a value.
|
||||||
|
/// Handles 0x (hex), 0o (octal), 0b (binary), decimal integers, and floats.
|
||||||
|
/// Underscores are ignored as digit separators.
|
||||||
|
pub fn parseNumber(token_text: []const u8) !NumberValue {
|
||||||
|
// Strip underscores for parsing
|
||||||
|
var buf: [128]u8 = undefined;
|
||||||
|
var buf_len: usize = 0;
|
||||||
|
for (token_text) |c| {
|
||||||
|
if (c != '_') {
|
||||||
|
if (buf_len >= buf.len) return error.InvalidNumber;
|
||||||
|
buf[buf_len] = c;
|
||||||
|
buf_len += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const clean = buf[0..buf_len];
|
||||||
|
|
||||||
|
if (clean.len == 0) return error.InvalidNumber;
|
||||||
|
|
||||||
|
// Check base prefix
|
||||||
|
if (clean.len >= 2 and clean[0] == '0') {
|
||||||
|
switch (clean[1]) {
|
||||||
|
'x', 'X' => {
|
||||||
|
const digits = clean[2..];
|
||||||
|
if (digits.len == 0) return error.InvalidNumber;
|
||||||
|
const val = std.fmt.parseInt(u64, digits, 16) catch return error.InvalidNumber;
|
||||||
|
return .{ .float = @floatFromInt(val), .int_value = val, .base = .hex };
|
||||||
|
},
|
||||||
|
'o', 'O' => {
|
||||||
|
const digits = clean[2..];
|
||||||
|
if (digits.len == 0) return error.InvalidNumber;
|
||||||
|
const val = std.fmt.parseInt(u64, digits, 8) catch return error.InvalidNumber;
|
||||||
|
return .{ .float = @floatFromInt(val), .int_value = val, .base = .octal };
|
||||||
|
},
|
||||||
|
'b', 'B' => {
|
||||||
|
const digits = clean[2..];
|
||||||
|
if (digits.len == 0) return error.InvalidNumber;
|
||||||
|
const val = std.fmt.parseInt(u64, digits, 2) catch return error.InvalidNumber;
|
||||||
|
return .{ .float = @floatFromInt(val), .int_value = val, .base = .binary };
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a pure integer (no '.', no 'e'/'E')
|
||||||
|
var is_integer = true;
|
||||||
|
for (clean) |c| {
|
||||||
|
if (c == '.' or c == 'e' or c == 'E') {
|
||||||
|
is_integer = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_integer) {
|
||||||
|
const val = std.fmt.parseInt(u64, clean, 10) catch {
|
||||||
|
// Could be too large for u64, try as float
|
||||||
|
const f = std.fmt.parseFloat(f64, clean) catch return error.InvalidNumber;
|
||||||
|
return .{ .float = f, .int_value = null, .base = .decimal };
|
||||||
|
};
|
||||||
|
return .{ .float = @floatFromInt(val), .int_value = val, .base = .decimal };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float
|
||||||
|
const f = std.fmt.parseFloat(f64, clean) catch return error.InvalidNumber;
|
||||||
|
return .{ .float = f, .int_value = null, .base = .decimal };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Token stream that inserts synthetic `star` tokens for implicit multiplication.
|
||||||
|
pub const ImplicitMulStream = struct {
|
||||||
|
tokenizer: Tokenizer,
|
||||||
|
source: []const u8,
|
||||||
|
buffered: ?Token,
|
||||||
|
prev_kind: TokenKind,
|
||||||
|
|
||||||
|
pub fn init(source: []const u8, mode: Mode) ImplicitMulStream {
|
||||||
|
return .{
|
||||||
|
.tokenizer = Tokenizer.init(source, mode),
|
||||||
|
.source = source,
|
||||||
|
.buffered = null,
|
||||||
|
.prev_kind = .eof,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(self: *ImplicitMulStream) Token {
|
||||||
|
// If we have a buffered token from a previous implicit mul detection
|
||||||
|
if (self.buffered) |buf| {
|
||||||
|
self.buffered = null;
|
||||||
|
self.prev_kind = buf.kind;
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tok = self.tokenizer.next();
|
||||||
|
|
||||||
|
// Check for implicit multiplication:
|
||||||
|
// prev is value-end AND current is value-start
|
||||||
|
if (self.prev_kind.isValueEnd() and tok.kind.isValueStart()) {
|
||||||
|
// Buffer this token for the next call, emit star now
|
||||||
|
self.buffered = tok;
|
||||||
|
self.prev_kind = .star;
|
||||||
|
return .{ .kind = .star, .start = tok.start, .len = 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
self.prev_kind = tok.kind;
|
||||||
|
return tok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Peek at the source text for a given token.
|
||||||
|
pub fn tokenText(self: *const ImplicitMulStream, tok: Token) []const u8 {
|
||||||
|
return tok.text(self.source);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// -- Raw Tokenizer --
|
||||||
|
|
||||||
|
pub const Tokenizer = struct {
|
||||||
|
source: []const u8,
|
||||||
|
pos: usize,
|
||||||
|
mode: Mode,
|
||||||
|
|
||||||
|
pub fn init(source: []const u8, mode: Mode) Tokenizer {
|
||||||
|
return .{
|
||||||
|
.source = source,
|
||||||
|
.pos = 0,
|
||||||
|
.mode = mode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(self: *Tokenizer) Token {
|
||||||
|
self.skipWhitespace();
|
||||||
|
|
||||||
|
if (self.pos >= self.source.len) {
|
||||||
|
return .{ .kind = .eof, .start = self.pos, .len = 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = self.pos;
|
||||||
|
const c = self.source[self.pos];
|
||||||
|
|
||||||
|
switch (c) {
|
||||||
|
'+' => return self.singleChar(.plus, start),
|
||||||
|
'-' => return self.singleChar(.minus, start),
|
||||||
|
'/' => return self.singleChar(.slash, start),
|
||||||
|
'%' => return self.singleChar(.percent, start),
|
||||||
|
'&' => return self.singleChar(.ampersand, start),
|
||||||
|
'|' => return self.singleChar(.pipe, start),
|
||||||
|
'~' => return self.singleChar(.tilde, start),
|
||||||
|
'(' => return self.singleChar(.left_paren, start),
|
||||||
|
')' => return self.singleChar(.right_paren, start),
|
||||||
|
',' => return self.singleChar(.comma, start),
|
||||||
|
';' => return self.singleChar(.semicolon, start),
|
||||||
|
'=' => return self.singleChar(.equals, start),
|
||||||
|
'^' => return self.singleChar(.caret, start),
|
||||||
|
'*' => {
|
||||||
|
self.pos += 1;
|
||||||
|
if (self.pos < self.source.len and self.source[self.pos] == '*') {
|
||||||
|
self.pos += 1;
|
||||||
|
return .{ .kind = .star_star, .start = start, .len = 2 };
|
||||||
|
}
|
||||||
|
return .{ .kind = .star, .start = start, .len = 1 };
|
||||||
|
},
|
||||||
|
'<' => {
|
||||||
|
self.pos += 1;
|
||||||
|
if (self.pos < self.source.len and self.source[self.pos] == '<') {
|
||||||
|
self.pos += 1;
|
||||||
|
return .{ .kind = .shift_left, .start = start, .len = 2 };
|
||||||
|
}
|
||||||
|
return .{ .kind = .invalid, .start = start, .len = 1 };
|
||||||
|
},
|
||||||
|
'>' => {
|
||||||
|
self.pos += 1;
|
||||||
|
if (self.pos < self.source.len and self.source[self.pos] == '>') {
|
||||||
|
self.pos += 1;
|
||||||
|
if (self.pos < self.source.len and self.source[self.pos] == '>') {
|
||||||
|
self.pos += 1;
|
||||||
|
return .{ .kind = .shift_right_logical, .start = start, .len = 3 };
|
||||||
|
}
|
||||||
|
return .{ .kind = .shift_right, .start = start, .len = 2 };
|
||||||
|
}
|
||||||
|
return .{ .kind = .invalid, .start = start, .len = 1 };
|
||||||
|
},
|
||||||
|
'0'...'9' => return self.readNumber(start),
|
||||||
|
'a'...'z', 'A'...'Z', '_' => return self.readIdentifier(start),
|
||||||
|
'.' => {
|
||||||
|
// Could be start of a decimal number like .5
|
||||||
|
if (self.pos + 1 < self.source.len and
|
||||||
|
self.source[self.pos + 1] >= '0' and self.source[self.pos + 1] <= '9')
|
||||||
|
{
|
||||||
|
return self.readNumber(start);
|
||||||
|
}
|
||||||
|
self.pos += 1;
|
||||||
|
return .{ .kind = .invalid, .start = start, .len = 1 };
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
self.pos += 1;
|
||||||
|
return .{ .kind = .invalid, .start = start, .len = 1 };
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn singleChar(self: *Tokenizer, kind: TokenKind, start: usize) Token {
|
||||||
|
self.pos += 1;
|
||||||
|
return .{ .kind = kind, .start = start, .len = 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skipWhitespace(self: *Tokenizer) void {
|
||||||
|
while (self.pos < self.source.len) {
|
||||||
|
switch (self.source[self.pos]) {
|
||||||
|
' ', '\t', '\r', '\n' => self.pos += 1,
|
||||||
|
else => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readNumber(self: *Tokenizer, start: usize) Token {
|
||||||
|
// Check for base prefix: 0x, 0o, 0b
|
||||||
|
if (self.source[self.pos] == '0' and self.pos + 1 < self.source.len) {
|
||||||
|
const next_ch = self.source[self.pos + 1];
|
||||||
|
switch (next_ch) {
|
||||||
|
'x', 'X' => {
|
||||||
|
self.pos += 2;
|
||||||
|
self.consumeDigits(isHexDigit);
|
||||||
|
return .{ .kind = .number, .start = start, .len = self.pos - start };
|
||||||
|
},
|
||||||
|
'o', 'O' => {
|
||||||
|
self.pos += 2;
|
||||||
|
self.consumeDigits(isOctalDigit);
|
||||||
|
return .{ .kind = .number, .start = start, .len = self.pos - start };
|
||||||
|
},
|
||||||
|
'b', 'B' => {
|
||||||
|
// Disambiguate: 0b... is binary only if followed by 0 or 1
|
||||||
|
if (self.pos + 2 < self.source.len and
|
||||||
|
(self.source[self.pos + 2] == '0' or self.source[self.pos + 2] == '1'))
|
||||||
|
{
|
||||||
|
self.pos += 2;
|
||||||
|
self.consumeDigits(isBinaryDigit);
|
||||||
|
return .{ .kind = .number, .start = start, .len = self.pos - start };
|
||||||
|
}
|
||||||
|
// Otherwise fall through to decimal
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decimal number (possibly floating point)
|
||||||
|
self.consumeDigits(isDecDigit);
|
||||||
|
|
||||||
|
// Fractional part
|
||||||
|
if (self.pos < self.source.len and self.source[self.pos] == '.') {
|
||||||
|
if (self.pos + 1 < self.source.len and
|
||||||
|
self.source[self.pos + 1] >= '0' and self.source[self.pos + 1] <= '9')
|
||||||
|
{
|
||||||
|
self.pos += 1; // consume '.'
|
||||||
|
self.consumeDigits(isDecDigit);
|
||||||
|
} else if (self.source[start] == '.') {
|
||||||
|
// Number started with '.', e.g. ".5"
|
||||||
|
self.pos += 1;
|
||||||
|
self.consumeDigits(isDecDigit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exponent part (e or E)
|
||||||
|
if (self.pos < self.source.len and
|
||||||
|
(self.source[self.pos] == 'e' or self.source[self.pos] == 'E'))
|
||||||
|
{
|
||||||
|
self.pos += 1;
|
||||||
|
if (self.pos < self.source.len and
|
||||||
|
(self.source[self.pos] == '+' or self.source[self.pos] == '-'))
|
||||||
|
{
|
||||||
|
self.pos += 1;
|
||||||
|
}
|
||||||
|
self.consumeDigits(isDecDigit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{ .kind = .number, .start = start, .len = self.pos - start };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consumeDigits(self: *Tokenizer, predicate: *const fn (u8) bool) void {
|
||||||
|
while (self.pos < self.source.len) {
|
||||||
|
const ch = self.source[self.pos];
|
||||||
|
if (predicate(ch)) {
|
||||||
|
self.pos += 1;
|
||||||
|
} else if (ch == '_') {
|
||||||
|
// Digit separator
|
||||||
|
self.pos += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readIdentifier(self: *Tokenizer, start: usize) Token {
|
||||||
|
while (self.pos < self.source.len) {
|
||||||
|
const ch = self.source[self.pos];
|
||||||
|
if ((ch >= 'a' and ch <= 'z') or
|
||||||
|
(ch >= 'A' and ch <= 'Z') or
|
||||||
|
(ch >= '0' and ch <= '9') or
|
||||||
|
ch == '_')
|
||||||
|
{
|
||||||
|
self.pos += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .{ .kind = .identifier, .start = start, .len = self.pos - start };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isHexDigit(c: u8) bool {
|
||||||
|
return (c >= '0' and c <= '9') or (c >= 'a' and c <= 'f') or (c >= 'A' and c <= 'F');
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isOctalDigit(c: u8) bool {
|
||||||
|
return c >= '0' and c <= '7';
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isBinaryDigit(c: u8) bool {
|
||||||
|
return c == '0' or c == '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isDecDigit(c: u8) bool {
|
||||||
|
return c >= '0' and c <= '9';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// -- Tests --
|
||||||
|
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
test "tokenize simple arithmetic" {
|
||||||
|
var tok = Tokenizer.init("2 + 3 * 4", .standard);
|
||||||
|
try testing.expectEqual(TokenKind.number, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.plus, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.number, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.star, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.number, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.eof, tok.next().kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tokenize hex number" {
|
||||||
|
var tok = Tokenizer.init("0xFF", .programmer);
|
||||||
|
const t = tok.next();
|
||||||
|
try testing.expectEqual(TokenKind.number, t.kind);
|
||||||
|
try testing.expectEqualStrings("0xFF", t.text("0xFF"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tokenize binary number" {
|
||||||
|
var tok = Tokenizer.init("0b1010", .programmer);
|
||||||
|
const t = tok.next();
|
||||||
|
try testing.expectEqual(TokenKind.number, t.kind);
|
||||||
|
try testing.expectEqualStrings("0b1010", t.text("0b1010"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tokenize octal number" {
|
||||||
|
var tok = Tokenizer.init("0o777", .programmer);
|
||||||
|
const t = tok.next();
|
||||||
|
try testing.expectEqual(TokenKind.number, t.kind);
|
||||||
|
try testing.expectEqualStrings("0o777", t.text("0o777"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tokenize shift operators" {
|
||||||
|
var tok = Tokenizer.init("x << 3 >> 1 >>> 2", .programmer);
|
||||||
|
try testing.expectEqual(TokenKind.identifier, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.shift_left, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.number, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.shift_right, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.number, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.shift_right_logical, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.number, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.eof, tok.next().kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tokenize star_star" {
|
||||||
|
var tok = Tokenizer.init("2**10", .programmer);
|
||||||
|
try testing.expectEqual(TokenKind.number, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.star_star, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.number, tok.next().kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tokenize number with underscores" {
|
||||||
|
var tok = Tokenizer.init("1_000_000", .standard);
|
||||||
|
const t = tok.next();
|
||||||
|
try testing.expectEqual(TokenKind.number, t.kind);
|
||||||
|
try testing.expectEqualStrings("1_000_000", t.text("1_000_000"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tokenize hex with underscores" {
|
||||||
|
var tok = Tokenizer.init("0xFF_FF", .programmer);
|
||||||
|
const t = tok.next();
|
||||||
|
try testing.expectEqual(TokenKind.number, t.kind);
|
||||||
|
try testing.expectEqualStrings("0xFF_FF", t.text("0xFF_FF"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tokenize function call" {
|
||||||
|
var tok = Tokenizer.init("sin(3.14)", .standard);
|
||||||
|
try testing.expectEqual(TokenKind.identifier, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.left_paren, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.number, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.right_paren, tok.next().kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tokenize floating point with exponent" {
|
||||||
|
var tok = Tokenizer.init("1.5e10", .standard);
|
||||||
|
const t = tok.next();
|
||||||
|
try testing.expectEqual(TokenKind.number, t.kind);
|
||||||
|
try testing.expectEqualStrings("1.5e10", t.text("1.5e10"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tokenize negative exponent" {
|
||||||
|
var tok = Tokenizer.init("2.5e-3", .standard);
|
||||||
|
const t = tok.next();
|
||||||
|
try testing.expectEqual(TokenKind.number, t.kind);
|
||||||
|
try testing.expectEqualStrings("2.5e-3", t.text("2.5e-3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tokenize number starting with dot" {
|
||||||
|
var tok = Tokenizer.init(".5", .standard);
|
||||||
|
const t = tok.next();
|
||||||
|
try testing.expectEqual(TokenKind.number, t.kind);
|
||||||
|
try testing.expectEqualStrings(".5", t.text(".5"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tokenize assignment" {
|
||||||
|
var tok = Tokenizer.init("X = 42", .standard);
|
||||||
|
try testing.expectEqual(TokenKind.identifier, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.equals, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.number, tok.next().kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tokenize all bitwise ops" {
|
||||||
|
var tok = Tokenizer.init("a & b | c ^ ~d", .programmer);
|
||||||
|
try testing.expectEqual(TokenKind.identifier, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.ampersand, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.identifier, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.pipe, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.identifier, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.caret, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.tilde, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.identifier, tok.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.eof, tok.next().kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tokenize empty string" {
|
||||||
|
var tok = Tokenizer.init("", .standard);
|
||||||
|
try testing.expectEqual(TokenKind.eof, tok.next().kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tokenize whitespace only" {
|
||||||
|
var tok = Tokenizer.init(" \t\n ", .standard);
|
||||||
|
try testing.expectEqual(TokenKind.eof, tok.next().kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- parseNumber tests --
|
||||||
|
|
||||||
|
test "parseNumber decimal integer" {
|
||||||
|
const result = try parseNumber("42");
|
||||||
|
try testing.expectEqual(@as(f64, 42.0), result.float);
|
||||||
|
try testing.expectEqual(@as(?u64, 42), result.int_value);
|
||||||
|
try testing.expectEqual(Base.decimal, result.base);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseNumber decimal with underscores" {
|
||||||
|
const result = try parseNumber("1_000_000");
|
||||||
|
try testing.expectEqual(@as(?u64, 1_000_000), result.int_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseNumber hex" {
|
||||||
|
const result = try parseNumber("0xFF");
|
||||||
|
try testing.expectEqual(@as(?u64, 255), result.int_value);
|
||||||
|
try testing.expectEqual(Base.hex, result.base);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseNumber binary" {
|
||||||
|
const result = try parseNumber("0b1010");
|
||||||
|
try testing.expectEqual(@as(?u64, 10), result.int_value);
|
||||||
|
try testing.expectEqual(Base.binary, result.base);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseNumber octal" {
|
||||||
|
const result = try parseNumber("0o777");
|
||||||
|
try testing.expectEqual(@as(?u64, 511), result.int_value);
|
||||||
|
try testing.expectEqual(Base.octal, result.base);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseNumber float" {
|
||||||
|
const result = try parseNumber("3.14");
|
||||||
|
try testing.expectApproxEqAbs(@as(f64, 3.14), result.float, 1e-10);
|
||||||
|
try testing.expectEqual(@as(?u64, null), result.int_value);
|
||||||
|
try testing.expectEqual(Base.decimal, result.base);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseNumber float with exponent" {
|
||||||
|
const result = try parseNumber("1.5e10");
|
||||||
|
try testing.expectEqual(@as(f64, 1.5e10), result.float);
|
||||||
|
try testing.expectEqual(@as(?u64, null), result.int_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseNumber hex with underscores" {
|
||||||
|
const result = try parseNumber("0xFF_FF");
|
||||||
|
try testing.expectEqual(@as(?u64, 0xFFFF), result.int_value);
|
||||||
|
try testing.expectEqual(Base.hex, result.base);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- ImplicitMulStream tests --
|
||||||
|
|
||||||
|
test "implicit mul: number followed by identifier (2pi)" {
|
||||||
|
var stream = ImplicitMulStream.init("2pi", .standard);
|
||||||
|
try testing.expectEqual(TokenKind.number, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.star, stream.next().kind); // synthetic
|
||||||
|
try testing.expectEqual(TokenKind.identifier, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.eof, stream.next().kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "implicit mul: number followed by paren 3(4+5)" {
|
||||||
|
var stream = ImplicitMulStream.init("3(4+5)", .standard);
|
||||||
|
try testing.expectEqual(TokenKind.number, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.star, stream.next().kind); // synthetic
|
||||||
|
try testing.expectEqual(TokenKind.left_paren, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.number, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.plus, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.number, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.right_paren, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.eof, stream.next().kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "implicit mul: paren followed by paren )()" {
|
||||||
|
var stream = ImplicitMulStream.init("(2)(3)", .standard);
|
||||||
|
try testing.expectEqual(TokenKind.left_paren, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.number, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.right_paren, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.star, stream.next().kind); // synthetic
|
||||||
|
try testing.expectEqual(TokenKind.left_paren, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.number, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.right_paren, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.eof, stream.next().kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "implicit mul: identifier followed by paren (function call) does NOT insert star" {
|
||||||
|
// "sin(x)" - identifier followed by ( is a function call, not implicit mul.
|
||||||
|
// However, at the tokenizer level, identifier IS a value-end and ( IS a value-start,
|
||||||
|
// so the ImplicitMulStream WILL insert a star. The PARSER is responsible for
|
||||||
|
// recognizing "identifier * (" as a function call pattern (when the star is
|
||||||
|
// synthetic/zero-length) and handling it correctly.
|
||||||
|
//
|
||||||
|
// This test documents the actual stream behavior:
|
||||||
|
var stream = ImplicitMulStream.init("sin(x)", .standard);
|
||||||
|
try testing.expectEqual(TokenKind.identifier, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.star, stream.next().kind); // synthetic
|
||||||
|
try testing.expectEqual(TokenKind.left_paren, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.identifier, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.right_paren, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.eof, stream.next().kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "no implicit mul: number + operator + number" {
|
||||||
|
var stream = ImplicitMulStream.init("2 + 3", .standard);
|
||||||
|
try testing.expectEqual(TokenKind.number, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.plus, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.number, stream.next().kind);
|
||||||
|
try testing.expectEqual(TokenKind.eof, stream.next().kind);
|
||||||
|
}
|
||||||
172
engine/src/types.zig
Normal file
172
engine/src/types.zig
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
//! Core types shared across the Tally engine.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// Calculation mode determines parsing and evaluation behavior.
|
||||||
|
pub const Mode = enum {
|
||||||
|
standard,
|
||||||
|
programmer,
|
||||||
|
financial,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Configurable integer bit width for programmer mode.
|
||||||
|
pub const BitWidth = enum(u7) {
|
||||||
|
bits8 = 8,
|
||||||
|
bits16 = 16,
|
||||||
|
bits32 = 32,
|
||||||
|
bits64 = 64,
|
||||||
|
|
||||||
|
/// Returns the mask for this bit width (all bits set within width).
|
||||||
|
pub fn mask(self: BitWidth) u64 {
|
||||||
|
return switch (self) {
|
||||||
|
.bits8 => 0xFF,
|
||||||
|
.bits16 => 0xFFFF,
|
||||||
|
.bits32 => 0xFFFF_FFFF,
|
||||||
|
.bits64 => 0xFFFF_FFFF_FFFF_FFFF,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of bits as a plain integer.
|
||||||
|
pub fn bits(self: BitWidth) u7 {
|
||||||
|
return @intFromEnum(self);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Signedness interpretation for programmer mode values.
|
||||||
|
pub const Signedness = enum {
|
||||||
|
signed,
|
||||||
|
unsigned,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Byte ordering for display purposes.
|
||||||
|
pub const Endianness = enum {
|
||||||
|
little,
|
||||||
|
big,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Number base for input/output.
|
||||||
|
pub const Base = enum {
|
||||||
|
decimal,
|
||||||
|
hex,
|
||||||
|
octal,
|
||||||
|
binary,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// An integer value in programmer mode.
|
||||||
|
/// Raw bits stored in u64; interpretation depends on bit_width and signedness.
|
||||||
|
pub const Integer = struct {
|
||||||
|
raw: u64,
|
||||||
|
bit_width: BitWidth,
|
||||||
|
signedness: Signedness,
|
||||||
|
|
||||||
|
/// Apply the bit width mask, truncating to the configured width.
|
||||||
|
pub fn masked(self: Integer) u64 {
|
||||||
|
return self.raw & self.bit_width.mask();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interpret as a signed value (sign-extend from bit_width).
|
||||||
|
pub fn signedValue(self: Integer) i64 {
|
||||||
|
const m = self.masked();
|
||||||
|
const width = self.bit_width.bits();
|
||||||
|
const sign_bit: u64 = @as(u64, 1) << @intCast(width - 1);
|
||||||
|
if (m & sign_bit != 0) {
|
||||||
|
// Sign extend: fill upper bits with 1s
|
||||||
|
const extension = ~self.bit_width.mask();
|
||||||
|
return @bitCast(m | extension);
|
||||||
|
}
|
||||||
|
return @intCast(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interpret as an unsigned value (just mask).
|
||||||
|
pub fn unsignedValue(self: Integer) u64 {
|
||||||
|
return self.masked();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Result of any calculation.
|
||||||
|
pub const Value = union(enum) {
|
||||||
|
integer: Integer,
|
||||||
|
float: f64,
|
||||||
|
boolean: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Programmer mode configuration.
|
||||||
|
pub const ProgrammerConfig = struct {
|
||||||
|
bit_width: BitWidth = .bits64,
|
||||||
|
signedness: Signedness = .signed,
|
||||||
|
display_endian: Endianness = .little,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// All possible engine errors.
|
||||||
|
pub const CalcError = error{
|
||||||
|
// Parser errors
|
||||||
|
UnexpectedToken,
|
||||||
|
UnmatchedParen,
|
||||||
|
InvalidNumber,
|
||||||
|
UnknownFunction,
|
||||||
|
UnknownVariable,
|
||||||
|
UnexpectedEnd,
|
||||||
|
InvalidExpression,
|
||||||
|
|
||||||
|
// Evaluation errors
|
||||||
|
DivisionByZero,
|
||||||
|
Overflow,
|
||||||
|
InvalidOperandType,
|
||||||
|
DomainError,
|
||||||
|
|
||||||
|
// Struct layout errors
|
||||||
|
InvalidType,
|
||||||
|
InvalidFieldName,
|
||||||
|
DuplicateFieldName,
|
||||||
|
StructTooLarge,
|
||||||
|
|
||||||
|
// Financial errors
|
||||||
|
InsufficientParameters,
|
||||||
|
ConvergenceFailure,
|
||||||
|
|
||||||
|
// Unit conversion errors
|
||||||
|
UnknownUnit,
|
||||||
|
IncompatibleUnits,
|
||||||
|
|
||||||
|
// System
|
||||||
|
OutOfMemory,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Detailed error information with source position.
|
||||||
|
pub const ErrorInfo = struct {
|
||||||
|
code: CalcError,
|
||||||
|
message: []const u8,
|
||||||
|
/// Character position in input where the error occurred (0-indexed).
|
||||||
|
position: ?usize = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
test "BitWidth.mask" {
|
||||||
|
try std.testing.expectEqual(@as(u64, 0xFF), BitWidth.bits8.mask());
|
||||||
|
try std.testing.expectEqual(@as(u64, 0xFFFF), BitWidth.bits16.mask());
|
||||||
|
try std.testing.expectEqual(@as(u64, 0xFFFF_FFFF), BitWidth.bits32.mask());
|
||||||
|
try std.testing.expectEqual(@as(u64, 0xFFFF_FFFF_FFFF_FFFF), BitWidth.bits64.mask());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Integer.signedValue" {
|
||||||
|
// 0xFF in 8-bit signed = -1
|
||||||
|
const i8_neg1 = Integer{ .raw = 0xFF, .bit_width = .bits8, .signedness = .signed };
|
||||||
|
try std.testing.expectEqual(@as(i64, -1), i8_neg1.signedValue());
|
||||||
|
|
||||||
|
// 0x7F in 8-bit signed = 127
|
||||||
|
const i8_127 = Integer{ .raw = 0x7F, .bit_width = .bits8, .signedness = .signed };
|
||||||
|
try std.testing.expectEqual(@as(i64, 127), i8_127.signedValue());
|
||||||
|
|
||||||
|
// 0x80 in 8-bit signed = -128
|
||||||
|
const i8_neg128 = Integer{ .raw = 0x80, .bit_width = .bits8, .signedness = .signed };
|
||||||
|
try std.testing.expectEqual(@as(i64, -128), i8_neg128.signedValue());
|
||||||
|
|
||||||
|
// 0xFFFF_FFFF in 32-bit signed = -1
|
||||||
|
const i32_neg1 = Integer{ .raw = 0xFFFF_FFFF, .bit_width = .bits32, .signedness = .signed };
|
||||||
|
try std.testing.expectEqual(@as(i64, -1), i32_neg1.signedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Integer.unsignedValue masks correctly" {
|
||||||
|
// Extra bits beyond width are masked off
|
||||||
|
const val = Integer{ .raw = 0x1FF, .bit_width = .bits8, .signedness = .unsigned };
|
||||||
|
try std.testing.expectEqual(@as(u64, 0xFF), val.unsignedValue());
|
||||||
|
}
|
||||||
336
src/main.zig
Normal file
336
src/main.zig
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const engine = @import("engine");
|
||||||
|
|
||||||
|
/// 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 result = engine.evalString(&env, allocator, expression) catch |err| {
|
||||||
|
return .{ .output = errorMessage(err), .is_error = true };
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatted = engine.formatter.formatFloat(buf, result);
|
||||||
|
return .{ .output = formatted.display, .is_error = false };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn formatProgrammerResult(buf: []u8, result: engine.types.Integer, config: engine.types.ProgrammerConfig) CliResult {
|
||||||
|
const value = result.unsignedValue();
|
||||||
|
const signed = result.signedValue();
|
||||||
|
|
||||||
|
// Format into sections of buf
|
||||||
|
// We'll build the output string piece by piece
|
||||||
|
var hex_buf: [256]u8 = undefined;
|
||||||
|
var dec_buf: [256]u8 = undefined;
|
||||||
|
var sdec_buf: [256]u8 = undefined;
|
||||||
|
var bin_buf: [512]u8 = undefined;
|
||||||
|
|
||||||
|
const hex = engine.formatter.formatHex(&hex_buf, value, config.bit_width);
|
||||||
|
const dec = engine.formatter.formatDecimalUnsigned(&dec_buf, value);
|
||||||
|
const sdec = engine.formatter.formatDecimalSigned(&sdec_buf, signed);
|
||||||
|
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}
|
||||||
|
\\ bin: {s}
|
||||||
|
\\
|
||||||
|
, .{ sdec.display, dec.display, hex.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
|
||||||
|
\\
|
||||||
|
;
|
||||||
|
|
||||||
|
const usage_text =
|
||||||
|
\\tally - a cross-platform calculator
|
||||||
|
\\
|
||||||
|
\\Usage:
|
||||||
|
\\ tally "<expression>" Evaluate expression
|
||||||
|
\\ tally -p "<expression>" Programmer mode
|
||||||
|
\\ tally --help Show help
|
||||||
|
\\
|
||||||
|
\\Examples:
|
||||||
|
\\ tally "2^32 - 1"
|
||||||
|
\\ tally -p "0xFF & 0x0F"
|
||||||
|
\\ tally "sin(pi/2) + 1"
|
||||||
|
\\
|
||||||
|
;
|
||||||
|
|
||||||
|
// -- 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) {
|
||||||
|
write(io, std.Io.File.stdout(), usage_text);
|
||||||
|
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: implicit 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, "0x0000_0000_0000_000F") != 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);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue