Skip to content

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 →