Skip to content

Instantly share code, notes, and snippets.

@munrocket
Last active May 31, 2025 04:02
Show Gist options
  • Save munrocket/28aabef7de171668f927cc2eda1ca667 to your computer and use it in GitHub Desktop.
Save munrocket/28aabef7de171668f927cc2eda1ca667 to your computer and use it in GitHub Desktop.
Minimal math expression parser
/**
* Safely evaluates a mathematical expression using `Function`.
* This function is designed to support many GLSL/WGSL functions.
* It handles float/int expressions and basic boolean operations.
* Safety is ensured by removing all numbers and operators from
* the expression and checking the remaining symbols.
*/
function calcMathExpression(expression) {
const VALID_EXPRESSIONS = /^[+\-*/%(,)]*$/;
const BOOL_FUNCTIONS = /(\^|&&|\|\||<<|>>)/g;
const INTEGER_FUNCTIONS = /(select|abs|min|max|sign)/g;
const FLOAT_FUNCTIONS =
/(asin|acos|atan|atan2|asinh|acosh|atanh|sin|cos|tan|sinh|cosh|tanh|round|floor|ceil|pow|sqrt|log|log2|exp|exp2|fract|clamp|mix|smoothstep|radians|degrees)/g;
const FLOATS = /[+-]?(?=\d*[.eE])(?=\.?\d)\d*\.?\d*(?:[eE][+-]?\d+)?/g;
const INTEGERS = /(0[xX][0-9a-fA-F]+[iu]?|[0-9]+[iu]?)/g;
let result = expression.replace(/\s+/g, '');
if (result === '') {
return '';
}
// safety check for eval vulnerabilities (https://jsfuck.com/, https://aem1k.com/five/)
let stripped = result
.replace(BOOL_FUNCTIONS, '')
.replace(FLOAT_FUNCTIONS, '')
.replace(INTEGER_FUNCTIONS, '')
.replace(FLOATS, '')
.replace(INTEGERS, '');
if (!VALID_EXPRESSIONS.test(stripped)) {
stripped = stripped.replace(/[+\-*/%(,)]+/g, '');
throw new Error(`Unsupported symbols '${stripped}' in expression ${expression}`);
}
// convert to bigint if there are no any floating point numbers or float functions
let isAbstractInteger = false;
if (
(!FLOATS.test(expression) && !FLOAT_FUNCTIONS.test(expression)) ||
BOOL_FUNCTIONS.test(expression)
) {
result = result.replace(INTEGERS, match => `${parseInt(match)}n`);
isAbstractInteger = true;
}
const mathReplacements = {
'select(': '((f,t,cond) => cond ? t : f)(',
'abs(': '((x) => Math.abs(Number(x)))(',
'min(': '((x,y) => Math.min(Number(x),Number(y)))(',
'max(': '((x,y) => Math.max(Number(x),Number(y)))(',
'sign(': '((x) => Math.sign(Number(x)))(',
'sin(': 'Math.sin(',
'cos(': 'Math.cos(',
'tan(': 'Math.tan(',
'sinh(': 'Math.sinh(',
'cosh(': 'Math.cosh(',
'tanh(': 'Math.tanh(',
'asin(': 'Math.asin(',
'acos(': 'Math.acos(',
'atan(': 'Math.atan(',
'atan2(': 'Math.atan2(',
'asinh(': 'Math.asinh(',
'acosh(': 'Math.acosh(',
'atanh(': 'Math.atanh(',
'round(': 'Math.round(',
'floor(': 'Math.floor(',
'ceil(': 'Math.ceil(',
'pow(': 'Math.pow(',
'sqrt(': 'Math.sqrt(',
'log(': 'Math.log(',
'log2(': 'Math.log2(',
'exp(': 'Math.exp(',
'exp2(': 'Math.pow(2,',
'fract(': '((x) => x - Math.floor(x))(',
'clamp(': '((x,min,max) => Math.min(Math.max(x,min),max))(',
'mix(': '((x,y,a) => x*(1-a) + y*a)(',
'smoothstep(':
'((e0,e1,x) => { let t = Math.min(Math.max((x-e0)/(e1-e0),0),1); return t*t*(3-2*t); })(',
'step(': '((edge,x) => x < edge ? 0 : 1)(',
'radians(': '((x) => x * Math.PI / 180)(',
'degrees(': '((x) => x * 180 / Math.PI)('
};
result = result.replaceAll('^', '**');
result = result.replace(/\b\w+\(/g, match => mathReplacements[match] || '');
try {
result = Function(`'use strict'; return (${result}).toString();`)();
return !isAbstractInteger && /^[-+]?\d+$/.test(result) ? `${result}.0` : result;
} catch (error) {
throw new Error(`Invalid eval '${expression}' -> '${result}' (${error})`);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment