Skip to content

Commit

Permalink
The API is starting to coagulate
Browse files Browse the repository at this point in the history
  • Loading branch information
Patrick McElhaney committed Aug 3, 2019
1 parent 31df459 commit 35d0d25
Show file tree
Hide file tree
Showing 7 changed files with 315 additions and 228 deletions.
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"workbench.colorCustomizations": {}
"workbench.colorCustomizations": {},
"cSpell.words": [
"Boolio"
]
}
59 changes: 59 additions & 0 deletions Boolio.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const ESParser = require("./ESParser");

const parser = new ESParser();

module.exports = class Boolio {
constructor(expression) {
this.ast = this.parse(expression);
}

parse(expression) {
return parser.parse(expression);
}

evaluate(values) {
return this.evaluateNode(this.ast, values);
}

evaluateNode(node, values) {
if (node.type === "atom") {
return values[node.name];
}

if (node.type === "and") {
return (
this.evaluateNode(node.left, values) &&
this.evaluateNode(node.right, values)
);
}

if (node.type === "or") {
return (
this.evaluateNode(node.left, values) ||
this.evaluateNode(node.right, values)
);
}

if (node.type === "not") {
return !this.evaluateNode(node.argument, values);
}

throw new Error(`Cannot evaluate type: ${node.type}`);
}

atoms() {
function findAtoms(node) {
if (node.type === "atom") {
return [node.name];
}
if (node.type === "or" || node.type === "and") {
return findAtoms(node.left).concat(findAtoms(node.right));
}
if (node.type === "not") {
return findAtoms(node.argument);
}
}

return new Set(findAtoms(this.ast));
}
};
97 changes: 97 additions & 0 deletions Boolio.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
function api() {
const boolio = new Boolio("foo && bar");

boolio.atoms(); // ['foo', 'bar']
boolio.evaluate({ foo: true, bar: true }); // true

boolio.truthTable(); /*
{
atoms: ['foo', 'bar'],
rows: [
[true, true, true],
[true, false, true],
[false, true, true],
[false, false, false]
]
}
*/
}

const Boolio = require("./Boolio");

function evaluate(expression, values) {
return new Boolio(expression).evaluate(values);
}

function evaluateNode(ast, values) {
return new Boolio().evaluateNode(ast, values);
}

const T = true;
const F = false;

it.each([[T, T, T], [T, F, F], [F, T, F], [F, F, F]])(
"given a = %s, b = %s: a and b === %s",
(a, b, result) => {
const ast = {
type: "and",
left: { type: "atom", name: "a" },
right: { type: "atom", name: "b" }
};
expect(evaluateNode(ast, { a, b })).toEqual(result);
}
);

it.each([[T, T, T], [T, F, T], [F, T, T], [F, F, F]])(
"given a = %s, b = %s: a or b === %s",
(a, b, result) => {
const ast = {
type: "or",
left: { type: "atom", name: "a" },
right: { type: "atom", name: "b" }
};
expect(evaluateNode(ast, { a, b })).toEqual(result);
}
);

it.each([[T, F], [F, T]])("given a = %s, not a === %s", (a, result) => {
const ast = {
type: "not",
argument: { type: "atom", name: "a" }
};
expect(evaluateNode(ast, { a })).toEqual(result);
});

it.each([
[T, T, T, T],
[T, T, F, T],
[T, F, F, F],
[T, F, T, T],
[F, F, T, T],
[F, T, T, T],
[F, T, F, F],
[F, F, F, F]
])("given a = %s, b = %s, c = %s: a and b or c === %s", (a, b, c, result) => {
const ast = {
type: "or",
left: {
type: "and",
left: { type: "atom", name: "a" },
right: { type: "atom", name: "b" }
},
right: { type: "atom", name: "c" }
};
expect(evaluateNode(ast, { a, b, c })).toEqual(result);
});

it.each([[T, T, T], [T, F, F], [F, T, F], [F, F, F]])(
"given a = %s, b = %s: a && b === %s",
(a, b, result) => {
expect(evaluate("a && b", { a, b })).toEqual(result);
}
);

it("finds the unique atoms in an expression", () => {
const boolio = new Boolio("foo && bar(1) || (bar(2) && !foo)");
expect(boolio.atoms()).toEqual(new Set(["foo", "bar(1)", "bar(2)"]));
});
68 changes: 68 additions & 0 deletions ESParser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const acorn = require("acorn");

function atomFromAcorn(node) {
if (node.raw) {
return node.raw;
}
if (node.type === "Identifier") {
return node.name;
}
if (node.type === "CallExpression") {
return `${node.callee.name}(${node.arguments.map(atomFromAcorn)})`;
}

if (node.type === "LogicalExpression") {
return `${atomFromAcorn(node.left)} ${node.operator} ${atomFromAcorn(
node.right
)}`;
}

return `?${node.type}?`;
}

module.exports = class AcornTransformer {
parse(expression) {
return this.fromAcorn(acorn.parse(expression));
}

fromAcorn(node) {
if (node.type === "Program") {
return this.fromAcorn(node.body[0]);
}

if (node.type === "ExpressionStatement") {
return this.fromAcorn(node.expression);
}

if (node.type === "LogicalExpression") {
return {
type: node.operator === "||" ? "or" : "and",
left: this.fromAcorn(node.left),
right: this.fromAcorn(node.right)
};
}

if (node.type === "UnaryExpression" && node.operator === "!") {
return {
type: "not",
argument: this.fromAcorn(node.argument)
};
}

if (node.type === "Identifier") {
return {
type: "atom",
name: node.name
};
}

if (node.type === "CallExpression") {
return {
type: "atom",
name: atomFromAcorn(node)
};
}

throw new Error(`Unrecognized node type: ${node.type}`);
}
};
87 changes: 87 additions & 0 deletions ESParser.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const ESParser = require("./ESParser");

const parser = new ESParser();

function parse(expression) {
return parser.parse(expression);
}

describe("builds a boolio tree from a JS expression", () => {
it("simple or expression", () => {
expect(parse("a || b")).toEqual({
type: "or",
left: { type: "atom", name: "a" },
right: { type: "atom", name: "b" }
});
});

it("simple and expression", () => {
expect(parse("a && b")).toEqual({
type: "and",
left: { type: "atom", name: "a" },
right: { type: "atom", name: "b" }
});
});

it("simple not expression", () => {
expect(parse("!a")).toEqual({
type: "not",
argument: { type: "atom", name: "a" }
});
});

it("multiple operators", () => {
expect(parse("a || b && c")).toEqual({
type: "or",
left: { type: "atom", name: "a" },
right: {
type: "and",
left: { type: "atom", name: "b" },
right: { type: "atom", name: "c" }
}
});
});

it("grouping", () => {
expect(parse("(a || b)")).toEqual({
type: "or",
left: { type: "atom", name: "a" },
right: { type: "atom", name: "b" }
});
});

it("grouping with multiple operators", () => {
expect(parse("(a || b) && c")).toEqual({
type: "and",
left: {
type: "or",
left: { type: "atom", name: "a" },
right: { type: "atom", name: "b" }
},
right: { type: "atom", name: "c" }
});
});

it("call expression", () => {
expect(parse("foo() && bar(1,2,x)")).toEqual({
type: "and",
left: { type: "atom", name: "foo()" },
right: { type: "atom", name: "bar(1,2,x)" }
});
});

it("nested call expression", () => {
expect(parse("foo(bar(1)) && x")).toEqual({
type: "and",
left: { type: "atom", name: "foo(bar(1))" },
right: { type: "atom", name: "x" }
});
});

it("call with operator", () => {
expect(parse("foo(a && b)")).toEqual({
type: "atom",
name: "foo(a && b)"
});
});
});
Loading

0 comments on commit 35d0d25

Please sign in to comment.