Math Evaluator — Zig source
Evaluate math expressions - arithmetic, functions (sqrt, sin, log), comparisons, and constants (pi, e) - safely, in real time. Input is constrained to math-safe characters, fully client-side.
This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.
// math-evaluator — Zig port: safe arithmetic expression evaluator (no eval).
// Guards with the TS lib's allowlist + keyword blacklist + MAX_LEN 200, then
// runs the Go twin's recursive-descent grammar: comparison < > → + - → * / %
// → ^ (right-assoc) → unary - → postfix ! → primary (number, parens, pi/e,
// one-arg calls). Unsafe or invalid input yields null, never panics.
const std = @import("std");
// The TS pattern ^[0-9+\-*/().,\s a-zA-Z%^!<>]+$ and its keyword blacklist,
// matched by hand — Zig's std has no regex. safeChar covers one byte; badWord
// scans whole-word occurrences of each forbidden keyword (the \b semantics).
fn safeChar(c: u8) bool {
return switch (c) {
'0'...'9', 'a'...'z', 'A'...'Z' => true,
'+', '-', '*', '/', '(', ')', '.', ',', ' ', '\t', '\n', '\r', '%', '^', '!', '<', '>' => true,
else => false,
};
}
fn isAlnum(c: u8) bool {
return (c >= '0' and c <= '9') or (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z');
}
fn badWord(s: []const u8) bool {
const words = [_][]const u8{ "import", "require", "eval", "function", "while", "for", "process", "global", "this", "window", "document", "constructor" };
for (words) |w| {
var start: usize = 0;
while (std.mem.indexOfPos(u8, s, start, w)) |hit| {
const after = hit + w.len;
const boundary = (hit == 0 or !isAlnum(s[hit - 1])) and (after >= s.len or !isAlnum(s[after]));
if (boundary) return true;
start = hit + 1;
}
}
return false;
}
fn isFn(name: []const u8) bool {
const names = [_][]const u8{ "sqrt", "sin", "cos", "tan", "log", "abs", "exp" };
for (names) |n| if (std.mem.eql(u8, name, n)) return true;
return false;
}
fn callFn(name: []const u8, x: f64) f64 {
if (std.mem.eql(u8, name, "sqrt")) return @sqrt(x);
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, "log")) return @log(x); // natural log / ln
if (std.mem.eql(u8, name, "abs")) return @abs(x);
return @exp(x);
}
const Parser = struct {
s: []const u8,
i: usize = 0,
err: bool = false,
is_bool: bool = false,
// 0 stands in for end-of-input.
fn peek(p: *Parser) u8 {
while (p.i < p.s.len and (p.s[p.i] == ' ' or p.s[p.i] == '\t')) p.i += 1;
return if (p.i < p.s.len) p.s[p.i] else 0;
}
fn eat(p: *Parser, c: u8) bool {
if (p.peek() == c and c != 0) {
p.i += 1;
return true;
}
return false;
}
fn factorial(p: *Parser, n: f64) f64 {
if (n < 0 or n != @floor(n)) {
p.err = true;
return 0;
}
var r: f64 = 1;
var k: f64 = 2;
while (k <= n) : (k += 1) r *= k;
return r;
}
fn primary(p: *Parser) f64 {
const c = p.peek();
if (c == '(') {
p.i += 1;
const v = p.cmp();
if (!p.eat(')')) p.err = true;
return v;
}
if ((c >= '0' and c <= '9') or c == '.') {
const start = p.i;
while (p.i < p.s.len and ((p.s[p.i] >= '0' and p.s[p.i] <= '9') or p.s[p.i] == '.')) p.i += 1;
return std.fmt.parseFloat(f64, p.s[start..p.i]) catch {
p.err = true;
return 0;
};
}
if ((c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z')) {
const start = p.i;
while (p.i < p.s.len and ((p.s[p.i] >= 'a' and p.s[p.i] <= 'z') or (p.s[p.i] >= 'A' and p.s[p.i] <= 'Z'))) p.i += 1;
var name: [16]u8 = undefined;
const n = p.i - start;
if (n > name.len) {
p.err = true;
return 0;
}
for (p.s[start..p.i], 0..) |ch, k| name[k] = std.ascii.toLower(ch);
const lower = name[0..n];
if (isFn(lower)) { // one-arg call, e.g. sqrt(2)
if (!p.eat('(')) {
p.err = true;
return 0;
}
const v = p.cmp();
if (!p.eat(')')) p.err = true;
return callFn(lower, v);
}
if (std.mem.eql(u8, lower, "pi")) return std.math.pi;
if (std.mem.eql(u8, lower, "e")) return std.math.e;
p.err = true; // bare function reference or unknown symbol → rejected
return 0;
}
p.err = true;
return 0;
}
fn post(p: *Parser) f64 {
var v = p.primary();
while (p.eat('!')) v = p.factorial(v);
return v;
}
fn unary(p: *Parser) f64 {
if (p.eat('-')) return -p.unary();
if (p.eat('+')) return p.unary();
return p.post();
}
fn powr(p: *Parser) f64 {
const l = p.unary();
return if (p.eat('^')) std.math.pow(f64, l, p.powr()) else l;
}
fn mul(p: *Parser) f64 {
var v = p.powr();
while (p.eat('*') or p.eat('/') or p.eat('%')) {
const op = p.s[p.i - 1];
const r = p.powr();
v = if (op == '*') v * r else if (op == '/') v / r else @mod(v, r);
}
return v;
}
fn add(p: *Parser) f64 {
var v = p.mul();
while (p.eat('+') or p.eat('-'))
v = if (p.s[p.i - 1] == '+') v + p.mul() else v - p.mul();
return v;
}
// Lowest precedence; parens recurse back here. Comparisons yield 1/0,
// remembered in is_bool so the caller can print "true"/"false".
fn cmp(p: *Parser) f64 {
var v = p.add();
while (p.eat('<') or p.eat('>')) {
const lt = p.s[p.i - 1] == '<';
const r = p.add();
v = if (lt) (if (v < r) 1 else 0) else (if (v > r) 1 else 0);
p.is_bool = true;
}
return v;
}
};
// Mirrors evaluateExpression in src/lib/math-evaluator.ts: formatted result,
// or null for empty/over-long/unsafe input or any parse failure. Integer-
// valued doubles drop the ".0", matching JS String(number).
var out_buf: [32]u8 = undefined;
fn evaluate(expr: []const u8) ?[]const u8 {
const t = std.mem.trim(u8, expr, " \t\r\n");
if (t.len == 0 or t.len > 200) return null;
for (t) |ch| if (!safeChar(ch)) return null;
if (badWord(t)) return null;
var p = Parser{ .s = t };
const v = p.cmp();
_ = p.peek(); // skip trailing whitespace before the leftover check
if (p.err or p.i < t.len) return null; // parse error or leftover tokens
if (p.is_bool) return if (v != 0) "true" else "false";
if (v == @floor(v) and @abs(v) < 1e15) {
return std.fmt.bufPrint(&out_buf, "{d}", .{@as(i64, @intFromFloat(v))}) catch "0";
}
return std.fmt.bufPrint(&out_buf, "{d}", .{v}) catch "0";
}
pub fn main() !void {
const w = std.io.getStdOut().writer();
const cases = [_][]const u8{ "1 + 2 * 3", "2^10", "5! + sqrt(16)", "sin(pi)", "3 > 2", "sqrt" };
for (cases) |c| {
const r = evaluate(c) orelse "(rejected)";
try w.print("{s:<14} = {s}\n", .{ c, r });
}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →