Skip to content

Math Evaluator — C 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 C implementation — the same logic the interactive tool runs, in a shareable, citable form.

/* math-evaluator — C port: safe arithmetic expression evaluator (no eval).
 * The TS lib leans on mathjs; C has no equivalent, so this ports the Go
 * twin's recursive-descent parser. Guard first (allowlist + keyword
 * blacklist + MAX_LEN 200), then parse; anything unsafe yields NULL. */
#include <ctype.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_LEN 200
/* The TS allowlist pattern checked by hand (no regex lib): OK is its
 * non-letter half, isalpha covers a-zA-Z. */
static const char OK[] = "0123456789+-*/()., \t\n\r\v\f%^!<>";
static const char *const BAD[] = { "import", "require", "eval", "function", "while", "for",
    "process", "global", "this", "window", "document", "constructor" };
static const struct { const char *n; double (*f)(double); } FN[] = {
    { "sqrt", sqrt }, { "sin", sin }, { "cos", cos }, { "tan", tan },
    { "log", log }, { "abs", fabs }, { "exp", exp },
};

static int safe_chars(const char *s) {
    for (const char *p = s; *p; p++)
        if (!strchr(OK, *p) && !isalpha((unsigned char)*p))
            return 0;
    return 1;
}

/* The TS \b blacklist by hand: whole-word substring match. */
static int bad_word(const char *s) {
    for (size_t i = 0; i < sizeof BAD / sizeof *BAD; i++) {
        size_t n = strlen(BAD[i]);
        for (const char *p = strstr(s, BAD[i]); p; p = strstr(p + 1, BAD[i]))
            if ((p == s || !isalnum((unsigned char)p[-1])) && !isalnum((unsigned char)p[n]))
                return 1;
    }
    return 0;
}

static const char *src;
static int pos, err, is_bool;
static int peekch(void) { while (src[pos] == ' ' || src[pos] == '\t') pos++; return src[pos]; }
static int eat(int c) { if (peekch() == c && c) { pos++; return 1; } return 0; }
static double factorial(double n) {
    if (n < 0 || n != floor(n)) { err = 1; return 0; }
    double r = 1;
    for (double i = 2; i <= n; i++) r *= i;
    return r;
}
static double parse_cmp(void);
static double parse_primary(void) {
    int c = peekch(), n = 0;
    char name[16];
    if (c == '(') { pos++; double v = parse_cmp(); if (!eat(')')) err = 1; return v; }
    if (isdigit(c) || c == '.') { char *end; double v = strtod(src + pos, &end); pos = end - src; return v; }
    if (isalpha(c)) {
        while (isalpha((unsigned char)src[pos]) && n < 15)
            name[n++] = tolower((unsigned char)src[pos++]);
        name[n] = 0;
        for (size_t k = 0; k < sizeof FN / sizeof *FN; k++)
            if (!strcmp(name, FN[k].n)) { /* one-arg call, e.g. sqrt(2) */
                if (!eat('(')) { err = 1; return 0; }
                double v = parse_cmp();
                if (!eat(')')) err = 1;
                return FN[k].f(v);
            }
        if (!strcmp(name, "pi")) return M_PI;
        if (!strcmp(name, "e")) return M_E;
    }
    err = 1; /* bare function reference or unknown symbol → rejected */
    return 0;
}
/* Precedence low → high: cmp < > → + - → * / % → ^ (right-assoc) → unary -
 * → postfix ! → primary. Comparisons yield 1/0, remembered in is_bool. */
static double parse_post(void) { double v = parse_primary(); while (eat('!')) v = factorial(v); return v; }
static double parse_unary(void) { if (eat('-')) return -parse_unary(); if (eat('+')) return parse_unary(); return parse_post(); }
static double parse_pow(void) { double l = parse_unary(); if (eat('^')) return pow(l, parse_pow()); return l; }
static double parse_mul(void) {
    double v = parse_pow();
    while (eat('*') || eat('/') || eat('%')) {
        int op = src[pos - 1];
        double r = parse_pow();
        v = op == '*' ? v * r : op == '/' ? v / r : fmod(v, r);
    }
    return v;
}
static double parse_add(void) {
    double v = parse_mul();
    while (eat('+') || eat('-')) v = src[pos - 1] == '+' ? v + parse_mul() : v - parse_mul();
    return v;
}
static double parse_cmp(void) {
    double v = parse_add();
    while (eat('<') || eat('>')) {
        int lt = src[pos - 1] == '<';
        double r = parse_add();
        v = lt ? v < r : v > r;
        is_bool = 1;
    }
    return v;
}

/* Mirrors evaluateExpression in src/lib/math-evaluator.ts: formatted result,
 * or NULL for empty/over-long/unsafe input or any parse failure. */
static const char *evaluate(const char *expr) {
    static char t[MAX_LEN + 2], out[32];
    snprintf(t, sizeof t, "%s", expr);
    for (char *e = t + strlen(t) - 1; e >= t && isspace((unsigned char)*e); e--) *e = 0;
    char *b = t;
    while (isspace((unsigned char)*b)) b++;
    if (!*b || strlen(b) > MAX_LEN || !safe_chars(b) || bad_word(b)) return NULL;
    src = b;
    pos = err = is_bool = 0;
    double v = parse_cmp();
    peekch();
    if (err || src[pos]) return NULL; /* parse error or leftover tokens */
    if (is_bool) return v ? "true" : "false";
    snprintf(out, sizeof out, "%g", v);
    return out;
}

int main(void) {
    const char *cases[] = { "1 + 2 * 3", "2^10", "5! + sqrt(16)", "sin(pi)", "3 > 2", "sqrt" };
    for (size_t i = 0; i < sizeof cases / sizeof *cases; i++) {
        const char *r = evaluate(cases[i]);
        printf("%-14s = %s\n", cases[i], r ? r : "(rejected)");
    }
}

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 →