Formulae 🧮

Since eval() is (properly) not available at Turtletoy I implemented a simple (probably naive) formula parser (tokenizer) and solver.

It supports all basic arithmetic and binary operators and Math functions and constants.

It also allows the use of custom variables and assigning values (including functions) to them when solving.

Fibonacci sequence: Formulae 🧮 (variation)
08066.repeat: Formulae 🧮 (variation)

Log in to post a comment.

// Formula parser and solver - Created by Jurgen Westerhof 2024
// MIT license
// https://turtletoy.net/turtle/187a81ec7d

const formulaText = 'Math.abs(a*x**2 + b*x + c)'; //type=string
const a = 3; //min=-30 max=30 step=.5
const b = -16; //min=-30 max=30 step=.5
const c = 5; //min=-30 max=30 step=.5

const fn = new Formula(formulaText);
console.info(formulaText);
console.info('Parsed', fn.getParsed());
console.info('Variabes', fn.getVariables());

const results = Array.from({length: 26},
    (e, x) => `f(${x}) = ${fn.solve({x: x, a: a, b: (vars) => b, c: c})}` // 'b: (vars) => b' could be 'b: b', but I added it this way as an example that you can feed custom functions as values. The 'vars' variable is a handle to an object containing all values passed to solve()
);

new Text({"name":"HersheySans1","unitsPerEm":1000,"ascent":800,"descent":-200,"capHeight":500,"xHeight":300,"glyphs":"IADEDgDAIQBODE4MJOZODGj3AEBODIr9GAvF\/k4MAACEDcX+TgyK\/QBAAMAiALATmAgk5pgIxu4AQHASJOZwEsbuAEAAwCMA3Bk6ETjhmAiYCABAnBg44QQQmAgAQJgIPPHcGTzxAEBiB574nBie+ABAAMAkAJwYhA044YQN7AQAQHASOOFwEuwEAECcGNrpJhZk53ASJOaEDSTm2Alk52IH2uliB1DsmAjG7tgJ\/O9ODDzxsBOy8yYW6PRcFyj2nBie+JwYT\/wmFsX+cBIAAIQNAADYCcX+YgdP\/ABAAMAlAIgdiB0k5mIHAAAAQIQNJOYEEKToBBAQ68QOkO1ODMbu2AnG7mIHUOxiB9rpmAhk5xgLJOaEDSTmBBBk57ATpOhcF6ToEhtk54gdJOYAQJwYaPcmFp748BQU+\/AUiv1cFwAA3BkAAEgcxf6IHU\/8iB3U+RIbaPecGGj3AEAAwCYA\/h\/+Hzzx\/h\/878gexu6IHcbuSBz87xIbfPKcGJ74JhZP\/LATxf46EQAATgwAANgJxf6YCIr9YgcU+2IHnviYCCj22Ano9HAS\/O+wE8bu8BRQ7PAU2umwE2TnOhEk5sQOZOeEDdrphA1Q7MQO\/O86EbLzXBdP\/NwZxf5IHAAAyB4AAP4fxf7+H4r9AEAAwCcATgzYCaTomAhk59gJJOYYC2TnGAva6dgJUOyYCJDtAEAAwCgAOhE6ETjhxA64404MZOfYCVDsmAh88pgIaPfYCYr9Tgx2AsQOLAY6EZgIAEAAwCkAOhFiBzjh2Am4404MZOfEDlDsBBB88gQQaPfEDor9Tgx2AtgJLAZiB5gIAEAAwCoAsBOEDZDthA1P\/ABAYgc88bATnvgAQLATPPFiB574AEAAwCsA\/h+wE9rpsBMAAABAmAjo9Mge6PQAQADALADYCdgJFPuYCE\/8YgcU+5gI1PnYCRT72AmK\/WIHAAAAQADALQD+H5gI6PTIHuj0AEAAwC4A2AmYCNT5YgcU+5gIT\/zYCRT7mAjU+QBAAMAvABIbSBw44SwGmAgAQADAMACcGMQOJOYYC2TnmAgQ62IHPPFiB+j0mAgU+xgLxf7EDgAAOhEAAPAUxf5cFxT7nBjo9JwYPPFcFxDr8BRk5zoRJObEDiTmAEAAwDEAnBgYCxDrhA3a6ToRJOY6EQAAAEAAwDIAnBiYCFDsmAgQ69gJpOgYC2TnhA0k5nASJObwFGTnJhak6FwXEOtcF5DtJhb877ATsvNiBwAAnBgAAABAAMAzAJwY2Akk5lwXJOYEEPzvsBP87yYWPPFcF3zynBgo9pwYnvhcF0\/88BTF\/joRAACEDQAA2AnF\/pgIiv1iBxT7AEAAwDQAnBiwEyTmYgdo99wZaPcAQLATJOawEwAAAEAAwDUAnBgmFiTm2Akk5pgIPPHYCfzvhA3G7joRxu7wFPzvXBd88pwYKPacGJ74XBdP\/PAUxf46EQAAhA0AANgJxf6YCIr9YgcU+wBAAMA2AJwYXBfa6SYWZOdwEiTmBBAk5k4MZOfYCRDrmAg88ZgIaPfYCU\/8TgzF\/gQQAAA6EQAA8BTF\/lwXT\/ycGJ74nBho91wXsvPwFDzxOhH87wQQ\/O9ODDzx2Amy85gIaPcAQADANwCcGJwYJOZODAAAAEBiByTmnBgk5gBAAMA4AJwYhA0k5tgJZOeYCNrpmAhQ7NgJxu5ODPzvOhE88fAUfPJcF+j0nBho95wYFPtcF4r9JhbF\/nASAACEDQAA2AnF\/pgIiv1iBxT7Ygdo95gI6PQYC3zyxA488bAT\/O8mFsbuXBdQ7FwX2ukmFmTncBIk5oQNJOYAQADAOQCcGFwXxu4mFnzysBPo9AQQKPbEDij2GAvo9JgIfPJiB8buYgeQ7ZgI2ukYC2TnxA4k5gQQJOawE2TnJhba6VwXxu5cF+j0JhYU+7ATxf4EEAAAhA0AANgJxf6YCE\/8AEAAwDoA2AmYCDzxYgd88pgIsvPYCXzymAg88QBAmAjU+WIHFPuYCE\/82AkU+5gI1PkAQADAOwDYCZgIPPFiB3zymAiy89gJfPKYCDzxAEDYCRT7mAhP\/GIHFPuYCNT52AkU+9gJiv1iBwAAAEAAwDwAiB1IHNrpmAjo9EgcAAAAQADAPQD+H5gIPPHIHjzxAECYCJ74yB6e+ABAAMA+AIgdmAja6Ugc6PSYCAAAAEAAwD8AJhZiB1DsYgcQ65gIpOjYCWTnTgwk5joRJOawE2Tn8BSk6CYWEOsmFpDt8BT877ATPPHEDrLzxA5o9wBAxA6K\/YQNxf7EDgAABBDF\/sQOiv0AQADAQAA0IdwZ\/O+cGJDtJhZQ7HASUOwEEJDtxA7G7oQNfPKEDSj2xA6e+DoR1PnwFNT5XBee+JwYKPYAQHASUOwEEMbuxA588sQOKPYEEJ74OhHU+QBA3BlQ7JwYKPacGJ74EhvU+Ygd1Pn+H2j3NCGy8zQhPPH+H5DtyB4Q60gcpOjcGWTnJhYk5nASJObEDmTnTgyk6NgJEOuYCJDtYgc88WIH6PSYCJ742AkU+04Miv3EDsX+cBIAACYWAADcGcX+SByK\/YgdT\/wAQBIbUOzcGSj23Bme+BIb1PkAQADAQQAmFsQOJObsBAAAAEDEDiTmnBgAAABAmAho9\/AUaPcAQADAQgDcGZgIJOaYCAAAAECYCCTmsBMk5lwXZOecGKTo3BkQ69wZkO2cGPzvXBc88bATfPIAQJgIfPKwE3zyXBey85wY6PTcGWj33BkU+5wYiv1cF8X+sBMAAJgIAAAAQADAQwDcGdwZUOycGNrpJhZk57ATJObEDiTmTgxk59gJ2umYCFDsYgf872IHKPaYCNT52AlP\/E4Mxf7EDgAAsBMAACYWxf6cGE\/83BnU+QBAAMBEANwZmAgk5pgIAAAAQJgIJOY6ESTm8BRk51wX2umcGFDs3Bn879wZKPacGNT5XBdP\/PAUxf46EQAAmAgAAABAAMBFAFwXmAgk5pgIAAAAQJgIJOacGCTmAECYCHzycBJ88gBAmAgAAJwYAAAAQADARgAmFpgIJOaYCAAAAECYCCTmnBgk5gBAmAh88nASfPIAQADARwDcGdwZUOycGNrpJhZk57ATJObEDiTmTgxk59gJ2umYCFDsYgf872IHKPaYCNT52AlP\/E4Mxf7EDgAAsBMAACYWxf6cGE\/83BnU+dwZKPYAQLATKPbcGSj2AEAAwEgAEhuYCCTmmAgAAABA3Bkk5twZAAAAQJgIfPLcGXzyAEAAwEkA2AmYCCTmmAgAAABAAMBKALATcBIk5nAS1Pk6EYr9BBDF\/oQNAAAYCwAAmAjF\/mIHiv0sBtT5LAZo9wBAAMBLANwZmAgk5pgIAAAAQNwZJOaYCGj3AEDEDjzx3BkAAABAAMBMAPAUmAgk5pgIAAAAQJgIAABcFwAAAEAAwE0AiB2YCCTmmAgAAABAmAgk5nASAAAAQEgcJOZwEgAAAEBIHCTmSBwAAABAAMBOABIbmAgk5pgIAAAAQJgIJObcGQAAAEDcGSTm3BkAAABAAMBPABIbxA4k5k4MZOfYCdrpmAhQ7GIH\/O9iByj2mAjU+dgJT\/xODMX+xA4AALATAAAmFsX+nBhP\/NwZ1PkSGyj2Ehv879wZUOycGNrpJhZk57ATJObEDiTmAEAAwFAA3BmYCCTmmAgAAABAmAgk5rATJOZcF2TnnBik6NwZEOvcGcbunBg88VwXfPKwE7LzmAiy8wBAAMBRABIbxA4k5k4MZOfYCdrpmAhQ7GIH\/O9iByj2mAjU+dgJT\/xODMX+xA4AALATAAAmFsX+nBhP\/NwZ1PkSGyj2Ehv879wZUOycGNrpJhZk57ATJObEDiTmAEBwEhT73Bl2AgBAAMBSANwZmAgk5pgIAAAAQJgIJOawEyTmXBdk55wYpOjcGRDr3BmQ7ZwY\/O9cFzzxsBN88pgIfPIAQDoRfPLcGQAAAEAAwFMAnBicGNrpJhZk53ASJOaEDSTm2Alk52IH2uliB1DsmAjG7tgJ\/O9ODDzxsBOy8yYW6PRcFyj2nBie+JwYT\/wmFsX+cBIAAIQNAADYCcX+YgdP\/ABAAMBUALAThA0k5oQNAAAAQOwEJOYmFiTmAEAAwFUAEhuYCCTmmAie+NgJT\/xODMX+BBAAAHASAAAmFsX+nBhP\/NwZnvjcGSTmAEAAwFYAJhbsBCTmxA4AAABAnBgk5sQOAAAAQADAVwCIHSwGJOZODAAAAEBwEiTmTgwAAABAcBIk5pwYAAAAQMgeJOacGAAAAEAAwFgAnBhiByTmnBgAAABAnBgk5mIHAAAAQADAWQAmFuwEJObEDnzyxA4AAABAnBgk5sQOfPIAQADAWgCcGJwYJOZiBwAAAEBiByTmnBgk5gBAYgcAAJwYAAAAQADAWwA6EZgIOOGYCJgIAEDYCTjh2AmYCABAmAg44ToROOEAQJgImAg6EZgIAEAAwFwAOhGxAyTm8BSxAwBAAMBdADoRxA444cQOmAgAQAQQOOEEEJgIAEBiBzjhBBA44QBAYgeYCAQQmAgAQADAXgCwE4QNuOOxA+j0AECEDbjjXBfo9ABAAMBfACYWsQOYCNwZmAgAQADAYADYCdgJUOxiB8buYgc88ZgIfPLYCTzxmAj872IHPPEAQADAYQBcFyYWxu4mFgAAAEAmFnzysBP87zoRxu6EDcbuGAv875gIfPJiByj2Ygee+JgIT\/wYC8X+hA0AADoRAACwE8X+JhZP\/ABAAMBiAFwXmAgk5pgIAAAAQJgIfPIYC\/zvhA3G7joRxu6wE\/zvJhZ88lwXKPZcF574JhZP\/LATxf46EQAAhA0AABgLxf6YCE\/8AEAAwGMAJhYmFnzysBP87zoRxu6EDcbuGAv875gIfPJiByj2Ygee+JgIT\/wYC8X+hA0AADoRAACwE8X+JhZP\/ABAAMBkAFwXJhYk5iYWAAAAQCYWfPKwE\/zvOhHG7oQNxu4YC\/zvmAh88mIHKPZiB574mAhP\/BgLxf6EDQAAOhEAALATxf4mFk\/8AEAAwGUAJhZiByj2JhYo9iYWsvPwFDzxsBP87zoRxu6EDcbuGAv875gIfPJiByj2Ygee+JgIT\/wYC8X+hA0AADoRAACwE8X+JhZP\/ABAAMBmAMQOBBAk5oQNJOYYC2Tn2AkQ69gJAAAAQCwGxu7EDsbuAEAAwGcAXBcmFsbuJhZ2AvAULAawE2IHOhGYCIQNmAgYC2IHAEAmFnzysBP87zoRxu6EDcbuGAv875gIfPJiByj2Ygee+JgIT\/wYC8X+hA0AADoRAACwE8X+JhZP\/ABAAMBoAFwXmAgk5pgIAAAAQJgIsvNODPzvxA7G7nASxu7wFPzvJhay8yYWAAAAQADAaQDYCWIHJOaYCGTn2Akk5pgI7uRiByTmAECYCMbumAgAAABAAMBqAE4M2Akk5hgLZOdODCTmGAvu5NgJJOYAQBgLxu4YC7ED2AliB2IHmAjsBJgIAEAAwGsA8BSYCCTmmAgAAABA8BTG7pgIFPsAQIQNKPYmFgAAAEAAwGwA2AmYCCTmmAgAAABAAMBtAOokmAjG7pgIAAAAQJgIsvNODPzvxA7G7nASxu7wFPzvJhay8yYWAAAAQCYWsvPcGfzvSBzG7v4fxu50IvzvtCOy87QjAAAAQADAbgBcF5gIxu6YCAAAAECYCLLzTgz878QOxu5wEsbu8BT87yYWsvMmFgAAAEAAwG8AXBeEDcbuGAv875gIfPJiByj2Ygee+JgIT\/wYC8X+hA0AADoRAACwE8X+JhZP\/FwXnvhcFyj2JhZ88rAT\/O86EcbuhA3G7gBAAMBwAFwXmAjG7pgImAgAQJgIfPIYC\/zvhA3G7joRxu6wE\/zvJhZ88lwXKPZcF574JhZP\/LATxf46EQAAhA0AABgLxf6YCE\/8AEAAwHEAXBcmFsbuJhaYCABAJhZ88rAT\/O86EcbuhA3G7hgL\/O+YCHzyYgco9mIHnviYCE\/8GAvF\/oQNAAA6EQAAsBPF\/iYWT\/wAQADAcgAEEJgIxu6YCAAAAECYCCj22Al88k4M\/O\/EDsbucBLG7gBAAMBzAPAU8BR88rAT\/O8EEMbuTgzG7pgI\/O9iB3zymAjo9BgLKPY6EWj3sBOe+PAUFPvwFE\/8sBPF\/gQQAABODAAAmAjF\/mIHT\/wAQADAdADEDtgJJObYCRT7GAvF\/oQNAAAEEAAAAEAsBsbuxA7G7gBAAMB1AFwXmAjG7pgIFPvYCcX+TgwAAAQQAABwEsX+JhYU+wBAJhbG7iYWAAAAQADAdgCwEywGxu6EDQAAAEDwFMbuhA0AAABAAMB3ABIbYgfG7k4MAAAAQDoRxu5ODAAAAEA6EcbuJhYAAABAEhvG7iYWAAAAQADAeADwFGIHxu7wFAAAAEDwFMbuYgcAAABAAMB5ALATLAbG7oQNAAAAQPAUxu6EDQAAGAvsBJgIYgcsBpgI7ASYCABAAMB6APAU8BTG7mIHAAAAQGIHxu7wFMbuAEBiBwAA8BQAAABAAMB7ADoRxA444U4MeOIYC7jj2Akk5tgJpOgYCxDrTgxQ7IQNxu6EDTzxGAuy8wBATgx44hgL7uQYC2TnTgza6YQNEOvEDpDtxA7874QNfPKYCOj0hA1o98QO1PnEDk\/8hA3F\/k4MAAAYC3YCGAvsBE4MYgcAQBgLKPaEDZ74hA0U+04Miv0YC8X+2Ak7AdgJsQMYCywGTgxiB8QOmAgAQADAfADYCZgIOOGYCJgIAEAAwH0AOhHYCTjhTgx44oQNuOPEDiTmxA6k6IQNEOtODFDsGAvG7hgLPPGEDbLzAEBODHjihA3u5IQNZOdODNrpGAsQ69gJkO3YCfzvGAt88gQQ6PQYC2j32AnU+dgJT\/wYC8X+TgwAAIQNdgKEDewETgxiBwBAhA0o9hgLnvgYCxT7TgyK\/YQNxf7EDjsBxA6xA4QNLAZODGIH2AmYCABAAMB+AIgdYgee+GIHKPaYCHzyGAs88YQNPPEEEHzy8BQo9lwXaPfcGWj3SBwo9ogdsvMAQGIHKPaYCLLzGAt88oQNfPIEELLz8BRo91wXnvjcGZ74SBxo94gdsvOIHTzxAEAAwA=="}).print(
    new Turtle(-97, -93),
    `f(x) = ${formulaText}
a=${a}, b=${b}, c=${c}
${results.join("\n")}`,
    .5
);

/* Usage:

// You can use 'normal' javascript notation
let e = new Formula('2 * (3 + 4) - 1/2**2');
let o = e.solve());

// including the Math namespace
let f = new Formula('Math.sin(2 * Math.PI * Math.random())');
o = f.solve();

// You can also use custom variables
let g = new Formula('2 * Math.PI * i / max');
o = g.solve({i: 2, max: 5});

// Which might be usefull in a loop:
array.map((e,i,a) => g.solve({i: i, max: a.length - 1}));

// You can also solve using a function as value for a custom variable.
// On solving the function will get an hashmap as the first (and only)
//  parameter with a key in it for every custom variable in the solve() call:
g.solve({i: (varMap) => varMap.max - 1, max: 5}); // i = 5 - 1 = 4, max = 5

*/

/////////////////////////////////////////////////////////////////
// Formula parser and solver - Created by Jurgen Westerhof 2024
// https://turtletoy.net/turtle/187a81ec7d
/////////////////////////////////////////////////////////////////
function Formula(string) {
    const types = {
        'Function': 'Function',
        'Literal': 'Literal',
        'Variable': 'Variable',
        'Arithmetic': 'Arithmetic',
        'Unary': 'Unary',
    };
    const operators = [ //ordered by operator precedence (PEMDAS)
        ['**', (a, b) => a**b],
        ['*', (a, b) => a*b],
        ['/', (a, b) => a/b],
        ['%', (a, b) => a%b],
        ['+', (a, b) => a+b],
        ['-', (a, b) => a-b],
        ['<<', (a, b) => a<<b],
        ['>>', (a, b) => a>>b],
        ['|', (a, b) => a|b],
        ['^', (a, b) => a^b],
        ['&', (a, b) => a&b],
    ];
    class Formula {
        #variables;
        #parsed;
        #raw;
        constructor(string) {
            this.#raw = string;
            this.#variables = [];
            this.#parsed = this.tokenize(string);
        }
        getVariables() {
            return this.#variables.map(e => e);
        }
        getParsed() {
            const clone = (v) => (typeof v == 'object')? v.map(vv => clone(vv)): v;
            return this.#parsed.map(v => clone(v));
        }
        tokenize(str) {
            const tokens = [];
            let m;

            nextToken: for(let i = 0; i < str.length; i++) {
                //Skip whitespace
                if(/\s/.test(str[i])) continue;
                //Parse Math namespace
                if(str.substr(i, 5) == 'Math.') {
                    i += 5;
                    m = new RegExp(`^.{${i}}(?<payload>(?<const>[A-Z][A-Z0-9]*)|(?<fn>[a-z][a-z0-9]*)).*?`).exec(str);
                    if(Math[m.groups.payload] === undefined) {
                        console.error(`Math.${m.groups.payload} is undefined`);
                    }
                    if(m.groups.const) {
                        tokens.push([types.Literal, Math[m.groups.payload]]);
                    } else {
                        tokens.push([types.Function, m.groups.payload]);
                    }
                    i+=m.groups.payload.length-1;
                    continue nextToken;
                }
                //Parse variable
                // taking a shortcut here: unicode in variable names not accepted: https://stackoverflow.com/questions/1661197/what-characters-are-valid-for-javascript-variable-names
                m = new RegExp('^' + '\.'.repeat(i) + '(?<payload>[a-zA-Z_$][0-9a-zA-Z_$]*).*').exec(str);
                if(m !== null) {
                    tokens.push([types.Variable, m.groups.payload]);
                    if(!this.#variables.includes(m.groups.payload)) {
                        this.#variables.push(m.groups.payload);
                    }
                    i+= m.groups.payload.length - 1;
                    continue nextToken;
                }
                //Parse unary
                if((tokens.length == 0 || tokens[tokens.length - 1][0] == types.Arithmetic) && (str[i] == '-' || str[i] == '+' || str[i] == '~')) {
                    tokens.push([types.Unary, str[i]]);
                    continue nextToken;
                }
                //Parse (group) (including function parameters)
                if(str[i] == '(') {
                    const isFunction = (tokens.length > 0 && tokens[tokens.length - 1][0] == types.Function);
                    let cnt = 1;
                    let k = i + 1;
                    let j = 0;
                    let fnArgs = [];
                    for(; 0 < cnt && k+j < str.length; j++) {
                        if(str[k+j] == '(') cnt++;
                        if(str[k+j] == ')') cnt--;
                        if(str[k+j] == ',' && cnt == 1) {
                            fnArgs.push(this.tokenize(str.substr(i+1, j)));
                            i += j+1;
                            k += j+1;
                            j=0;
                        }
                    }
                    if(cnt == 0) {
                        if(isFunction) {
                            fnArgs.push(this.tokenize(str.substr(i+1, j-1)));
                            tokens[tokens.length - 1].push(fnArgs)
                        } else {
                            tokens.push(this.tokenize(str.substr(i+1, j-1)));
                        }
                        i += j;
                        continue nextToken;
                    }
                    console.error(`Opened bracket at character ${i} not closed: ${str.substr(i)}`);
                    throw new Error(`Opened bracket at character ${i} not closed: ${str.substr(i)}`);
                }
                //Parse literal
                m = new RegExp(`^.{${i}}(?<payload>\\d+(\\.\\d+)?|\\.\\d+).*?`).exec(str);
                if(m !== null) {
                    tokens.push([types.Literal, +m.groups.payload]);
                    i+=m.groups.payload.length-1;
                    continue nextToken;
                }
                //Parse operator
                m = new RegExp(`^.{${i}}(?<payload>\\${operators.map(o => o[0].split('').join('\\')).join('|\\')})`).exec(str);
                if(m !== null) {
                    tokens.push([types.Arithmetic, m.groups.payload]);
                    i+=m.groups.payload.length-1;
                    continue nextToken;
                }
                //Something I didn't think of occured
                console.error(`Unable to parse '${str}' because a character at ${i}: ${str[i]}`);
                throw new Error(`Unable to parse '${str}' because a character at ${i}: ${str[i]}`);
            }
            return tokens;
        }
        solve(variableMap = {}) {
            return this.solveInt(
                this.#parsed,
                //Map required variables to values assuming 0 if not set
                this.#variables.reduce((a, c) => {
                    let val = 0;
                    if(variableMap[c] === undefined) {
                        console.warn(`Variable ${c} not set in argument to solve() of ${this.#raw}.`);
                        throw new Error(`Variable ${c} not set in argument to solve() of ${this.#raw}.`);
                    } else {
                        val = variableMap[c];
                    }
                    return {...a, [c]: val};
                }, {})
            );
        }
        solveInt(tokenss, variableMap) {
            if(tokenss.length == 0) return 0;
            
            const clone = (v) => (typeof v == 'object')? v.map(vv => clone(vv)): v;
            const tokens = tokenss.map(v => clone(v));
            
            //Resolve functions
            for(let i = 0; i < tokens.length; i++) {
                if(tokens[i][0] == types.Function) {
                    const literals = tokens[i][2].map(v => this.solveInt(v, variableMap));
                    tokens[i] = [types.Literal, Math[tokens[i][1]].apply(null, literals)];
                }
            }
            //Resolve (group)s
            for(let i = 0; i < tokens.length; i++) {
                if(typeof tokens[i][0] == 'object') {
                    tokens[i] = [types.Literal, this.solveInt(tokens[i], variableMap)];
                }
                if(tokens[i][0] == types.Variable) {
                    tokens[i] = [types.Literal, typeof variableMap[tokens[i][1]] == 'function'? variableMap[tokens[i][1]](variableMap): variableMap[tokens[i][1]]];
                }
            }
            //Resolve unary
            for(let i = 0; i < tokens.length; i++) {
                if(tokens[i][0] == types.Unary) {
                    switch(tokens[i][1]) {
                        case '-':
                        case '+':
                            tokens[i+1][1] = tokens[i+1][1]*(tokens[i][1]=='-'?-1:1);
                            break;
                        case '~':
                            tokens[i+1][1] = ~tokens[i+1][1];
                    }
                    tokens.splice(i, 1);
                    i--;
                }
            }
            //Resolve operators
            operators.forEach(op => {
                for(let i = 0; i < tokens.length; i++) {
                    if(tokens[i][0] == types.Arithmetic && tokens[i][1] == op[0]) {
                        tokens[i-1][1] = op[1](tokens[i-1][1], tokens[i+1][1]);
                        tokens.splice(i, 2);
                        i-=2;
                    }
                }
            });
            //Get solution
            if(tokens.length == 1 && tokens[0][0] == types.Literal) {
                return tokens[0][1];
            }
            //Something I didn't think of occured
            console.error('Something went wrong solving token ' + i, tokens);
            throw new Error('Something went wrong solving token ' + i);
        }
    }
    return new Formula(string);
}

////////////////////////////////////////////////////////////////
// Text utility code. Created by Reinder Nijhoff 2024
// https://turtletoy.net/turtle/0f84fd3ae4
////////////////////////////////////////////////////////////////

function Text(fontData) {
    const decode = (data) => {
        const b = atob(data), a = new Int16Array(b.length / 2), g = {};
        for (let i = 0; i < b.length; i+=2) {
            a[i / 2 | 0] = b.charCodeAt(i) | (b.charCodeAt(i + 1) << 8);
        }
        for (let i = 0;i < a.length; i++) {
            let u = String.fromCharCode(a[i++]), x = a[i++], d = [], p = [];
            for (;a[i] !== -16384; i++) a[i] === 16384 ? (d.push(p), p = []) : p.push(a[i]);
            g[u] = { x, d };
        }
        return g;
    }
    const rotAdd = (a, b, h) => [Math.cos(h)*a[0] - Math.sin(h)*a[1] + b[0],
                                 Math.cos(h)*a[1] + Math.sin(h)*a[0] + b[1]];

    const data = Object.fromEntries(Object.entries(fontData).map(([key, value]) =>
        [key, key === 'glyphs' ? decode(value) : value]));

    class Text {
        print (t, str, scale = 1) {
            let pos = t ? [t.x(), t.y()] : [0,0], h = t ? t.h() * Math.PI * 2 / t.fullCircle() : 0, 
                      o = pos, s = scale / data.unitsPerEm;
            str.split('').map(c => {
                if (c == '\n') {
                    pos = o = rotAdd([0, 14 * scale], o, h);
                    return;
                }
                const glyph = (data.glyphs[c] || data.glyphs[' ']), d = glyph.d;
                d.forEach( (p, k) => {
                    t && t.up();
                    for (let i=0; i<p.length; i+=2) {
                        t && t.goto(rotAdd([p[i]*s, p[i+1]*s], pos, h));
                        t && t.down();
                    }
                });
                pos = rotAdd([glyph.x*s, 0], pos, h);
            });
            return rotAdd([0, 10*scale], pos, h);
        }
    }

    return new Text();
}