Public
Edited
Jul 22, 2023
Importers
Insert cell
Insert cell
api.makeFPCore({specMathJS: 'sqrt(x + 1) - sqrt(x)', ranges: [["x", [0, 1000]]], targetMathJS: 'x^2', name: 'myfun'}) // doesn't check errors
Insert cell
api.parseErrors('sqrt(x - 42 - sqrt(x)')
Insert cell
api.parseErrors('sqrt(1, 2)')
Insert cell
api.getVarnamesMathJS('sqrt(a + b)')
Insert cell
math.parse('1')
Insert cell
api.rangeErrors([undefined, 1000])
Insert cell
api.rangeErrors([NaN, Number.POSITIVE_INFINITY])
Insert cell
api.rangeErrors([1, 0])
Insert cell
api.FPCorePrecondition([["x", [0, 1000]], ["y", [-1, 1]]]) // doesn't check errors
Insert cell
api.FPCoreBody('sqrt(x + 1) - sqrt(x)') // doesn't check errors
Insert cell
api = {
const CONSTANTS = {"PI": "real", "E": "real", "TRUE": "bool", "FALSE": "bool"}

const FUNCTIONS = {}
"+ - * / pow copysign fdim fmin fmax fmod hypot remainder".split(" ").forEach(function(op) {
FUNCTIONS[op] = [["real", "real"], "real"];
});
("fabs sqrt exp log sin cos tan asin acos atan sinh cosh tanh asinh acosh atanh " +
"cbrt ceil erf erfc exp2 expm1 floor lgamma log10 log1p log2 logb rint " +
"round tgamma trunc").split(" ").forEach(function(op) {
FUNCTIONS[op] = [["real"], "real"];
});
FUNCTIONS["fma"] = [["real", "real", "real"], "real"];
"< > == != <= >=".split(" ").forEach(function(op) {
FUNCTIONS[op] = [["real", "real"], "bool"];
});
"and or".split(" ").forEach(function(op) {
FUNCTIONS[op] = [["bool", "bool"], "bool"];
});
const SECRETFUNCTIONS = {"^": "pow", "**": "pow", "abs": "fabs", "min": "fmin", "max": "fmax", "mod": "fmod"}

function tree_errors(tree, expected) /* tree -> list */ {
var messages = [];
var names = [];

var rtype = bottom_up(tree, function(node, path, parent) {
switch(node.type) {
case "ConstantNode":
if (["number", "boolean"].indexOf((node.valueType || typeof node.value)) === -1) {
messages.push("Constants that are " + (node.valueType || typeof node.value) + "s not supported.");
}
return ({"number": "real", "boolean": "bool"})[(node.valueType || typeof node.value)] || "real";
case "FunctionNode":
const name = SECRETFUNCTIONS[node.name] || node.name;
if (!FUNCTIONS[name]) {
messages.push("Function <code>" + name + "</code> unsupported.");
} else if (FUNCTIONS[name][0].length !== node.args.length) {
messages.push("Function <code>" + name + "</code> expects " +
FUNCTIONS[name][0].length + " arguments");
} else if (""+extract(node.args) !== ""+FUNCTIONS[name][0]) {
messages.push("Function <code>" + name + "</code>" +
" expects arguments of type " +
FUNCTIONS[name][0].join(", ") +
", got " + extract(node.args).join(", "));
}
return (FUNCTIONS[name] || [[], "real"])[1];
case "OperatorNode":
node.op = SECRETFUNCTIONS[node.op] || node.op;
if (!FUNCTIONS[node.op]) {
messages.push("Operator <code>" + node.op + "</code> unsupported.");
} else if (FUNCTIONS[node.op][0].length !== node.args.length &&
!(node.op === "-" && node.args.length === 1)) {
messages.push("Operator <code>" + node.op + "</code> expects " +
FUNCTIONS[node.op][0].length + " arguments");
} else if (""+extract(node.args) !== ""+FUNCTIONS[node.op][0] &&
!(node.op === "-" && ""+extract(node.args) === "real") &&
!(is_comparison(node.op) /* TODO improve */)) {
messages.push("Operator <code>" + node.op + "</code>" +
" expects arguments of type " +
FUNCTIONS[node.op][0].join(", ") +
", got " + extract(node.args).join(", "));
}
return (FUNCTIONS[node.op] || [[], "real"])[1];
case "SymbolNode":
if (!CONSTANTS[node.name]) {
names.push(node.name);
return "real";
} else {
return CONSTANTS[node.name];
}
case "ConditionalNode":
if (node.condition.res !== "bool") {
messages.push("Conditional has type " + node.condition.res + " instead of bool");
}
if (node.trueExpr.res !== node.falseExpr.res) {
messages.push("Conditional branches have different types " + node.trueExpr.res + " and " + node.falseExpr.res);
}
return node.trueExpr.res;
default:
messages.push("Unsupported syntax; found unexpected <code>" + node.type + "</code>.")
return "real";
}
}).res;

if (rtype !== expected) {
messages.push("Expected an expression of type " + expected + ", got " + rtype);
}

return messages;
}

function bottom_up(tree, cb) {
if (tree.args) {
tree.args = tree.args.map(function(node) {return bottom_up(node, cb)});
} else if (tree.condition) {
tree.condition = bottom_up(tree.condition, cb);
tree.trueExpr = bottom_up(tree.trueExpr, cb);
tree.falseExpr = bottom_up(tree.falseExpr, cb);
}
tree.res = cb(tree);
return tree;
}

function dump_fpcore(formula, ranges) { // NOTE modified get_precondition...
var tree = math.parse(formula);

var names = [];
var body = dump_tree(tree, names);
var precondition = ranges ? get_precondition_from_input_ranges(ranges) : null;

var dnames = [];
for (var i = 0; i < names.length; i++) {
if (dnames.indexOf(names[i]) === -1) dnames.push(names[i]);
}

var name = formula.replace("\\", "\\\\").replace("\"", "\\\"");
var fpcore = "(FPCore (" + dnames.join(" ") + ")\n :name \"" + name + "\"";
if (precondition) fpcore += "\n :pre " + precondition;

return fpcore + "\n " + body + ")";
}

function is_comparison(name) {
return ["==", "!=", "<", ">", "<=", ">="].indexOf(name) !== -1;
}

function flatten_comparisons(node) {
var terms = [];
(function collect_terms(node) {
if (node.type == "OperatorNode" && is_comparison(node.op)) {
collect_terms(node.args[0]);
collect_terms(node.args[1]);
} else {
terms.push(node.res);
}
})(node);
var conjuncts = [];
var iters = 0;
(function do_flatten(node) {
if (node.type == "OperatorNode" && is_comparison(node.op)) {
do_flatten(node.args[0]);
var i = iters++; // save old value and increment it
var prev = conjuncts[conjuncts.length - 1];
if (prev && prev[0] == node.op && prev[2] == terms[i]) {
prev.push(terms[i + 1]);
} else {
conjuncts.push([node.op, terms[i], terms[i+1]]);
}
do_flatten(node.args[1]);
}
})(node);
var comparisons = [];
for (var i = 0; i < conjuncts.length; i++) {
comparisons.push("(" + conjuncts[i].join(" ") + ")");
}
if (comparisons.length == 0) {
return "TRUE";
} else if (comparisons.length == 1) {
return comparisons[0];
} else {
return "(and " + comparisons.join(" ") + ")";
}
}

function extract(args) {return args.map(function(n) {return n.res});}

function dump_tree(tree, names) {
return bottom_up(tree, function(node) {
switch(node.type) {
case "ConstantNode":
return "" + node.value;
case "FunctionNode":
// NOTE changed from node.name reassignment to be compatible with mathjs 4.4.2
const name = SECRETFUNCTIONS[node.name] || node.name;
return "(" + name + " " + extract(node.args).join(" ") + ")";
case "OperatorNode":
node.op = SECRETFUNCTIONS[node.op] || node.op;
if (is_comparison(node.op)) {
return flatten_comparisons(node);
} else {
return "(" + node.op + " " + extract(node.args).join(" ") + ")";
}
case "SymbolNode":
if (!CONSTANTS[node.name])
names.push(node.name);
return node.name;
case "ConditionalNode":
return "(if " + node.condition.res +
" " + node.trueExpr.res +
" " + node.falseExpr.res + ")";
default:
throw SyntaxError("Invalid tree!");
}
}).res;
}
function get_varnames_mathjs(mathjs_text) {
const names = []
dump_tree(math.parse(mathjs_text), names)
var dnames = [];
for (var i = 0; i < names.length; i++) {
if (dnames.indexOf(names[i]) === -1) dnames.push(names[i]);
}
return dnames
}
function get_precondition_from_input_ranges(ranges) { // NOTE modified get_precondition...
// ranges should be like [["x", [-1, 1]], ["y", [0, 1000]]
// assumes ranges was already checked for validity using eg. get_input_range_errors
const exprs = ranges.map(([name, [start, end]]) => `(<= ${start} ${name} ${end})`).join(' ')
return `(and ${exprs})`
}
function get_input_range_errors([low, high] = [undefined, undefined], empty_if_missing=false) {
if ((low === undefined || low === '') || (high === undefined || high === '')) return empty_if_missing ? [] : ['input missing']
const A = []
if (!(low === undefined || low === '') && isNaN(Number(low))) {
A.push(`The start of the range (${low}) is not a number.`)
} else if (!Number.isFinite(Number(low))) {
A.push(`The start of the range (${low}) is outside the floating point range.`)
}

if (!(high === undefined || high === '') && isNaN(Number(high))) {
A.push(`The end of the range (${high}) is not a number.`)
} else if (!Number.isFinite(Number(high))) {
A.push(`The end of the range (${high}) is outside the floating point range.`)
}

if (Number(low) > Number(high)) A.push(`The start of the range is higher than the end.`)
return A
}
function FPCoreBody(mathJSExpr) {
return dump_tree(math.parse(mathJSExpr), [])
}
return {
//dumpFPCore: dump_fpcore, // Currently has no error handling!
rangeErrors: get_input_range_errors,
FPCorePrecondition: get_precondition_from_input_ranges,
getVarnamesMathJS: get_varnames_mathjs,
parseErrors: mathJSExpr => {
function mathJSErrors (mathJSExpr) {
try {math.parse(mathJSExpr)} catch (e) {return [e.message]}
return []
}
const mjserrors = mathJSErrors(mathJSExpr)
return mjserrors.length > 0 ? mjserrors : tree_errors(math.parse(mathJSExpr), 'real')
},
FPCoreBody,
makeFPCore: ({ specMathJS, ranges, targetFPCoreBody, name=specMathJS }) => {
const vars = get_varnames_mathjs(specMathJS)
const target = targetFPCoreBody ? `:herbie-target ${targetFPCoreBody}\n ` : ''
return `(FPCore (${vars.join(' ')})\n :name "${name}"\n :pre ${get_precondition_from_input_ranges(ranges)}\n ${target}${FPCoreBody(specMathJS)})`
}
}
}
Insert cell
//math = (await import('https://cdn.skypack.dev/mathjs@11.3.1')) // NOTE this is a different version from graph.html page (4.4.2) and old code (1.6.0!)...
Insert cell
math = (await import('https://cdn.skypack.dev/mathjs@1.6.0')).default // NOTE this is the old 1.6.0 version we are currently using, we should update eventually!
Insert cell
math.parse("((sqrt(1.0 + x) - sqrt(x)) <= 2e-6) ? (0.5 * pow(x, -0.5)) : (sqrt(1.0 + x) - sqrt(x))")
Insert cell
math4 = (await import('https://cdn.skypack.dev/mathjs@4')).default
Insert cell
math4.parse("((sqrt(1.0 + x) - sqrt(x)) <= 2e-6) ? (0.5 * pow(x, -0.5)) : (sqrt(1.0 + x) - sqrt(x))")
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more