Skip to content

Instantly share code, notes, and snippets.

@tung
Created August 6, 2025 10:11
Show Gist options
  • Select an option

  • Save tung/3040d3f3dc67304ca55695bbea66062e to your computer and use it in GitHub Desktop.

Select an option

Save tung/3040d3f3dc67304ca55695bbea66062e to your computer and use it in GitHub Desktop.
Math expression calculator in C, using precedence climbing for operator precedence.
// Math expression calculator in C, using precedence climbing for operator precedence.
#include <ctype.h>
#include <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
// Build tests that can be run with "--test".
// Needs "utest.h" in the same dir: https://github.com/sheredom/utest.h
//#define TEST
#ifdef TEST
#include <float.h>
#include "utest.h"
UTEST_STATE();
#endif
typedef struct {
int bytes_read;
const char* rest;
} Parser;
#define DEFINE_OPTION(ty) \
typedef struct { \
bool is_some; \
ty inner; \
} Option##ty;
DEFINE_OPTION(double);
typedef struct {
int count;
int value;
} Digits;
DEFINE_OPTION(Digits);
typedef enum {
OpAdd,
OpSub,
OpMul,
OpDiv,
OpExp,
} Operator;
DEFINE_OPTION(Operator);
typedef struct {
int precedence;
bool left_associative;
} OperatorData;
typedef enum {
ExpectedOpenParen,
ExpectedCloseParen,
ExpectedExpression,
} Err;
typedef struct {
bool is_ok;
union {
double inner;
Err err;
};
} Result;
OperatorData OPERATORS[] = {
[OpAdd] = { 1, true },
[OpSub] = { 1, true },
[OpMul] = { 2, true },
[OpDiv] = { 2, true },
[OpExp] = { 3, false },
};
#define OP_MAX_PRECEDENCE 4
void whitespace(Parser* p) {
while (p->rest[0] == ' ') {
p->bytes_read++;
p->rest++;
}
}
bool literal(Parser* p, const char* s) {
whitespace(p);
size_t s_len = strlen(s);
if (strncmp(p->rest, s, s_len)) return false;
if (isalpha(s[0]) && isalpha(p->rest[s_len])) return false;
p->bytes_read += s_len;
p->rest += s_len;
return true;
}
OptionDigits digits(Parser* p) {
whitespace(p);
OptionDigits opt_digits = (OptionDigits){ false, {0} };
opt_digits.is_some = isdigit(p->rest[0]);
while (isdigit(p->rest[0])) {
opt_digits.inner.count++;
opt_digits.inner.value *= 10;
opt_digits.inner.value += p->rest[0] - '0';
p->bytes_read++;
p->rest++;
}
return opt_digits;
}
Optiondouble number(Parser* p) {
Optiondouble opt_double = (Optiondouble){ false, 0.0 };
OptionDigits opt_whole_digits = digits(p);
if (!opt_whole_digits.is_some) return opt_double;
Parser frac_parser = *p;
if (!literal(&frac_parser, ".")) {
opt_double.is_some = true;
opt_double.inner = (double)opt_whole_digits.inner.value;
return opt_double;
}
OptionDigits opt_frac_digits = digits(&frac_parser);
if (!opt_frac_digits.is_some) {
opt_double.is_some = true;
opt_double.inner = (double)opt_whole_digits.inner.value;
return opt_double;
}
opt_double.is_some = true;
opt_double.inner = (double)opt_frac_digits.inner.value;
for (int i = 0; i < opt_frac_digits.inner.count; i++) opt_double.inner *= 0.1;
opt_double.inner += (double)opt_whole_digits.inner.value;
p->bytes_read = frac_parser.bytes_read;
p->rest = frac_parser.rest;
return opt_double;
}
OptionOperator operator(Parser* p, int min_precedence) {
Parser op_parser = *p;
Operator op;
if (literal(&op_parser, "+")) op = OpAdd;
else if (literal(&op_parser, "-")) op = OpSub;
else if (literal(&op_parser, "*")) op = OpMul;
else if (literal(&op_parser, "/")) op = OpDiv;
else if (literal(&op_parser, "^")) op = OpExp;
else return (OptionOperator){ false, 0 };
if (OPERATORS[op].precedence < min_precedence) {
return (OptionOperator){ false, 0 };
}
p->bytes_read = op_parser.bytes_read;
p->rest = op_parser.rest;
return (OptionOperator){ true, op };
}
double compute(Operator op, double lhs, double rhs) {
switch (op) {
case OpAdd: return lhs + rhs;
case OpSub: return lhs - rhs;
case OpMul: return lhs * rhs;
case OpDiv: return lhs / rhs;
case OpExp: return pow(lhs, rhs);
}
return 0.0;
}
Result expr(Parser* p, int min_precedence);
Result atom(Parser* p) {
if (literal(p, "(")) {
Result x = expr(p, 0);
if (!x.is_ok) return x;
if (!literal(p, ")")) return (Result){ false, { .err = ExpectedExpression } };
return (Result){ true, { x.inner } };
}
if (literal(p, "-")) {
Result x = expr(p, OP_MAX_PRECEDENCE + 1);
if (x.is_ok) x.inner *= -1.0;
return x;
}
if (literal(p, "sqrt")) {
if (!literal(p, "(")) return (Result){ false, { .err = ExpectedOpenParen } };
Result x = expr(p, 0);
if (!x.is_ok) return x;
if (!literal(p, ")")) return (Result){ false, { .err = ExpectedCloseParen } };
x.inner = sqrt(x.inner);
return x;
}
Optiondouble opt_double = number(p);
if (opt_double.is_some) return (Result){ true, { .inner = opt_double.inner } };
return (Result){ false, { .err = ExpectedExpression } };
}
Result expr(Parser* p, int min_precedence) {
Result res_atom = atom(p);
if (!res_atom.is_ok) return res_atom;
double num = res_atom.inner;
for (;;) {
OptionOperator opt_op = operator(p, min_precedence);
if (!opt_op.is_some) break;
int next_min_precedence = OPERATORS[opt_op.inner].precedence;
if (OPERATORS[opt_op.inner].left_associative) next_min_precedence++;
Result res_rhs = expr(p, next_min_precedence);
if (!res_rhs.is_ok) return res_rhs;
num = compute(opt_op.inner, num, res_rhs.inner);
}
return (Result){ true, { .inner = num } };
}
int main(int argc, const char* argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s [expression]\n", argc > 0 ? argv[0] : "calc");
return 1;
}
#ifdef TEST
if (argc >= 2 && !strcmp(argv[1], "--test")) {
return utest_main(argc - 1, argv + 1);
}
#endif
if (argc > 2) {
fprintf(stderr, "Usage: %s [expression]\n", argc > 0 ? argv[0] : "calc");
return 1;
}
Parser p = (Parser){ 0, argv[1] };
Result res = expr(&p, 0);
if (res.is_ok) whitespace(&p);
if (!p.rest[0]) {
printf("%g\n", res.inner);
} else {
fprintf(stderr, "%s\n", argv[1]);
for (int i = 0; i < (&p.rest[0] - argv[1]); i++) fputc(' ', stderr);
fputs("^ Error: ", stderr);
if (res.is_ok) {
fputs("trailing input\n", stderr);
} else {
switch (res.err) {
case ExpectedOpenParen:
fputs("expected open parenthesis\n", stderr);
break;
case ExpectedCloseParen:
fputs("expected close parenthesis\n", stderr);
break;
case ExpectedExpression:
fputs("expected expression\n", stderr);
break;
}
}
return 2;
}
return 0;
}
#ifdef TEST
#define TEST_WHITESPACE(br, rst, input) \
do { \
p = (Parser){ 0, input }; \
whitespace(&p); \
EXPECT_EQ(br, p.bytes_read); \
EXPECT_STREQ(rst, p.rest); \
} while (0)
UTEST(parser, whitespace) {
Parser p;
TEST_WHITESPACE(0, "", "");
TEST_WHITESPACE(0, "1", "1");
TEST_WHITESPACE(1, "", " ");
TEST_WHITESPACE(1, "1", " 1");
TEST_WHITESPACE(2, "", " ");
}
#define TEST_LITERAL(br, rst, res, lit, input) \
do { \
p = (Parser){ 0, input }; \
EXPECT_EQ(res, literal(&p, lit)); \
EXPECT_EQ(br, p.bytes_read); \
EXPECT_STREQ(rst, p.rest); \
} while (0)
UTEST(parser, literal) {
Parser p;
TEST_LITERAL(0, "", true, "", "");
TEST_LITERAL(1, "", true, "(", "(");
TEST_LITERAL(1, "(", true, "(", "((");
TEST_LITERAL(3, "", true, "aaa", "aaa");
TEST_LITERAL(0, "aaaa", false, "aaa", "aaaa");
TEST_LITERAL(4, "", true, "aaa", " aaa");
TEST_LITERAL(0, "aaa", false, "bbb", "aaa");
TEST_LITERAL(3, " bbb", true, "aaa", "aaa bbb");
TEST_LITERAL(0, "aaabbb", false, "aaa", "aaabbb");
}
#define TEST_NO_DIGITS(br, rst, input) \
do { \
p = (Parser){ 0, input }; \
res = digits(&p); \
EXPECT_FALSE(res.is_some); \
EXPECT_EQ(br, p.bytes_read); \
EXPECT_STREQ(rst, p.rest); \
} while (0)
#define TEST_DIGITS(br, rst, cnt, val, input) \
do { \
p = (Parser){ 0, input }; \
res = digits(&p); \
ASSERT_TRUE(res.is_some); \
EXPECT_EQ(cnt, res.inner.count); \
EXPECT_EQ(val, res.inner.value); \
EXPECT_EQ(br, p.bytes_read); \
EXPECT_STREQ(rst, p.rest); \
} while (0)
UTEST(parser, digits) {
Parser p;
OptionDigits res;
TEST_NO_DIGITS(0, "", "");
TEST_NO_DIGITS(0, "aaa", "aaa");
TEST_DIGITS(3, "", 3, 123, "123");
TEST_DIGITS(4, "", 3, 123, " 123");
}
#define TEST_NO_NUMBER(br, rst, input) \
do { \
p = (Parser){ 0, input }; \
res = number(&p); \
EXPECT_FALSE(res.is_some); \
EXPECT_EQ(br, p.bytes_read); \
EXPECT_STREQ(rst, p.rest); \
} while (0)
#define TEST_NUMBER(br, rst, val, input) \
do { \
p = (Parser){ 0, input }; \
res = number(&p); \
ASSERT_TRUE(res.is_some); \
EXPECT_NEAR(val, res.inner, DBL_EPSILON); \
EXPECT_EQ(br, p.bytes_read); \
EXPECT_STREQ(rst, p.rest); \
} while(0)
UTEST(parser, number) {
Parser p;
Optiondouble res;
TEST_NO_NUMBER(0, "", "");
TEST_NUMBER(3, "", 123.0, "123");
TEST_NUMBER(3, ".", 123.0, "123.");
TEST_NUMBER(7, "", 123.456, "123.456");
TEST_NUMBER(3, ".abc", 123.0, "123.abc");
}
#define TEST_ATOM_ERR(br, rst, e, input) \
do { \
p = (Parser){ 0, input }; \
res = atom(&p); \
ASSERT_FALSE(res.is_ok); \
EXPECT_EQ(e, (int)res.err); \
EXPECT_EQ(br, p.bytes_read); \
EXPECT_STREQ(rst, p.rest); \
} while (0)
#define TEST_ATOM_OK(br, rst, val, input) \
do { \
p = (Parser){ 0, input }; \
res = atom(&p); \
ASSERT_TRUE(res.is_ok); \
EXPECT_NEAR(val, res.inner, DBL_EPSILON); \
EXPECT_EQ(br, p.bytes_read); \
EXPECT_STREQ(rst, p.rest); \
} while (0)
UTEST(parser, atom) {
Parser p;
Result res;
TEST_ATOM_ERR(0, "", ExpectedExpression, "");
TEST_ATOM_OK(7, "", 123.456, "123.456");
TEST_ATOM_OK(9, "", 123.456, "(123.456)");
}
UTEST(expr, empty) {
Parser p = (Parser){ 0, "" };
Result res = expr(&p, 0);
ASSERT_FALSE(res.is_ok);
EXPECT_EQ(ExpectedExpression, (int)res.err);
}
#define TEST_EXPR(val, input) \
do { \
p = (Parser){ 0, input }; \
res = expr(&p, 0); \
ASSERT_TRUE(res.is_ok); \
EXPECT_NEAR(val, res.inner, DBL_EPSILON); \
} while (0)
UTEST(expr, solo) {
Parser p;
Result res;
TEST_EXPR(1.0, "1");
TEST_EXPR(0.2, "0.2");
}
UTEST(expr, ops) {
Parser p;
Result res;
TEST_EXPR(15.0, "12+3");
TEST_EXPR(9.0, "12-3");
TEST_EXPR(36.0, "12*3");
TEST_EXPR(4.0, "12/3");
TEST_EXPR(1728.0, "12^3");
}
UTEST(expr, precedence) {
Parser p;
Result res;
TEST_EXPR(10.0, "5+4*3/2-1");
TEST_EXPR(5.0, "4+3*6/9-1");
TEST_EXPR(1.0, "5+2*6/4-7");
TEST_EXPR(81.0, "-9^2");
}
UTEST(expr, paren) {
Parser p;
Result res;
TEST_EXPR(1.0, "(1)");
TEST_EXPR(1.0, "(((1)))");
TEST_EXPR(60.0, "(2+3)*(5+7)");
}
UTEST(expr, prefix) {
Parser p;
Result res;
TEST_EXPR(-1.0, "-1");
TEST_EXPR(1.0, "--1");
TEST_EXPR(-1.0, "---1");
TEST_EXPR(-1.0, "- 1");
TEST_EXPR(-1.0, "(-1)");
TEST_EXPR(-1.0, "-(1)");
TEST_EXPR(1.0, "-(-1)");
TEST_EXPR(2.0, "1--1");
TEST_EXPR(0.0, "1---1");
}
UTEST(expr, pow) {
Parser p;
Result res;
TEST_EXPR(1.0, "1 ^ 1");
TEST_EXPR(4.0, "2^2");
TEST_EXPR(8.0, "2^3");
TEST_EXPR(9.0, "3^2");
TEST_EXPR(27.0, "3^3");
TEST_EXPR(1.0, "4^0");
}
UTEST(expr, sqrt) {
Parser p;
Result res;
TEST_EXPR(1.0, "sqrt(1)");
TEST_EXPR(2.0, "sqrt(4)");
TEST_EXPR(3.0, "sqrt(9)");
TEST_EXPR(4.0, "sqrt(16)");
TEST_EXPR(5.0, "sqrt(25)");
TEST_EXPR(1.0, "sqrt (1)");
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment