Skip to content

feat: Binary runtime helper. #255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 23, 2019
102 changes: 102 additions & 0 deletions packages/@css-blocks/runtime/src/constructor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
export type whatever = string | number | boolean | symbol | object | null | undefined | void;

export const enum OP_CODE {
OPEN = "000",
VAL = "001",
NOT = "010",
OR = "011",
AND = "100",
EQUAL = "101",
CLOSE = "110",
CONCAT = "111",
}

// The maximum safe number of opcodes we're able to encode in a single base 36 string.
// The maximum number of binary digits we're allowed in a number in Javascript is 53:
// Number.MAX_SAFE_INTEGER.toString(2).length === 53.
// However! When converting from base36 and back again, leading zeros are stripped. To
// fix this, we prefix every shape with a throwaway bit at the beginning, so the actual
// safe number is 52.
const MAX_SAFE_OPCODES = 52;

export class Expression {
public ops: string[] = [];
private classes: string[] = [];
private exprs: whatever[] = [];
private exprIdx: Map<whatever, number> = new Map();

// The bit size of value encodings is dependent on the number of value
// indices we need to keep track of.
private valSize() { return ~~Math.log2(this.exprs.length - 1) + 1; }

// Track a value, referenced by index.
val(expr: whatever) {
this.ops.push(OP_CODE.VAL);
let idx = this.exprIdx.get(expr);
if (idx === undefined) {
idx = this.exprs.length;
this.exprIdx.set(expr, idx);
this.exprs.push(expr);
}
this.ops.push(idx.toString(2));
return this;
}

// `when()` is a proxy for `val()` for better english-language-like constructor chains.
when(expr: whatever) { return this.val(expr); }

// Track an inverse value.
not(expr: whatever) {
this.ops.push(OP_CODE.NOT);
return this.val(expr);
}

// The start of an expression. Apply this class when the following
// expression evaluates to true for a given input.
apply(klass: string) {
this.classes.push(klass);
if (this.ops.length) { this.ops.push(OP_CODE.CONCAT); }
return this;
}

// Operands
get open() { this.ops.push(OP_CODE.OPEN); return this; }
get or() { this.ops.push(OP_CODE.OR); return this; }
get and() { this.ops.push(OP_CODE.AND); return this; }
get equals() { this.ops.push(OP_CODE.EQUAL); return this; }
get close() { this.ops.push(OP_CODE.CLOSE); return this; }

// Introspection methods.
getExprs(): whatever[] { return this.exprs.slice(0); }
getClasses(): string[] { return this.classes.slice(0); }
getOps(): string[] { return this.ops.slice(); }

// Calculate the binary string encoding of this expression's logic shape.
getShape(): string[] {
const OUT = [];
let working = "";
let isVal = false;
for (let op of this.ops) {

// If this opcode is a VAL (preceded by VAL opcode) ensure its binary
// value is padded to be the valSize length.
if (isVal) {
op = "0".repeat(Math.max(this.valSize() - op.length , 0)) + op;
isVal = false;
}

// If this opcode is a VAL, consume the next op as a value.
isVal = op === OP_CODE.VAL;

// If this opcode would overflow the MAX_SAVE_INTEGER when converted to
// an integer, strike a new base36 string.
if (working.length + op.length >= MAX_SAFE_OPCODES) {
OUT.push(parseInt(`1${working.split("").reverse().join("")}`, 2).toString(36));
working = "";
}
working += op;
}
if (working) { OUT.push(parseInt(`1${working.split("").reverse().join("")}`, 2).toString(36)); }
return OUT;
}
}
96 changes: 96 additions & 0 deletions packages/@css-blocks/runtime/src/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
export type whatever = string | number | boolean | symbol | object | null | undefined | void;

export function runtime(shape: string[], classes: string[], exprs: whatever[]): string {

// We dynamically determine the variable window size based on the number of
// expressions it is possible to reference. It is the compiler's responsibility
// to guarantee the expression shape matches at build time.
const VAR_SIZE = ~~Math.log2(exprs.length - 1) + 1;

let out = ""; // Output class list.
let klass = 0; // Current class we are determining presence for.
let val = null; // Working boolean expression value.
let step = 0; // Character count used for opcode discovery.
let current = null; // Current discovered opcode to evaluate.
let next = 0; // This is a single lookahead parser – next opcode will be stored here.
let op = null; // Operation to evaluate on next val discovered.
let invert = false; // Should we invert the next discovered value.
// let stack = []; // Stack for nested boolean expressions

// For each 32 bit integer passed to us as a base 36 string
for ( let segment of shape ) {

// Convert binary string segment to a base 10 integer
let integer = parseInt(segment, 36);

// Process each bit in this 32 bit integer.
// Note: `while` loop is faster than a `for` loop here.
let iters = 32;
while (iters--) {

// Construct our lookahead opcode and "pop" a bit off the end
// of our integer's binary representation.
let size = (current === 1 ? VAR_SIZE : 3);
next += integer % 2 * (2 * (size - 1 - step) || 1); // tslint:disable-line
integer = integer >>> 1; // tslint:disable-line

// When we have discovered the next opcode, process.
if (!(step = ++step % size)) {

// Each opcode type requires implementation
switch (current) {

// If no current op-code, move on.
case null: break;

// OPEN: `000`
case 0: break;

// VAL: `001`
case 1:
let tmp = invert ? !exprs[next] : !!exprs[next];
switch (op) {
case 3: val = val || tmp; break;
case 4: val = val && tmp; break;
case 5: val = val === tmp; break;
default: val = tmp;
}
op = null;
invert = false;
break;

// NOT: `010`
case 2: invert = true; break;

// OR: `011`
case 3: op = 3; break;

// AND: `100`
case 4: op = 4; break;

// EQUAL: `101`
case 5: op = 5; break;

// CLOSE: `110`
case 6: break;

// CONCAT: `111`
case 7:
out += val ? (out ? " " : "") + classes[klass] : "";
klass++;
break;

// If op-code is unrecognized, throw.
default: throw new Error("Unknown CSS Blocks op-code.");
}

// Begin construction of next opcode. Skip `val` indices.
current = (current === 1) ? null : next;
next = 0;
}
}
out += val ? (out ? " " : "") + classes[klass] : "";
}

return out;
}
34 changes: 32 additions & 2 deletions packages/@css-blocks/runtime/test/expression-test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,39 @@
import { assert } from "chai";
import { suite, test } from "mocha-typescript";

import { Expression, OP_CODE } from "../src/constructor";
import { runtime } from "../src/runtime";

@suite("Expression")
export class ExpressionTests {
@test "runs"() {
assert.equal(1, 1);

@test "simple equality expression"() {
let expr = new Expression();
expr.apply("fubar").when(true).equals.val(false);
assert.deepEqual(expr.getClasses(), ["fubar"]);
assert.deepEqual(expr.getExprs(), [true, false]);
assert.deepEqual(expr.getOps(), [OP_CODE.VAL, "0", OP_CODE.EQUAL, OP_CODE.VAL, "1"]);
assert.deepEqual([parseInt(`1${expr.getOps().join("").split("").reverse().join("")}`, 2).toString(36)], expr.getShape());
assert.equal(runtime(expr.getShape(), expr.getClasses(), [true, false]), "");
assert.equal(runtime(expr.getShape(), expr.getClasses(), [true, true]), "fubar");
}

@test "simple not expression"() {
let expr = new Expression();
expr.apply("fubar").when(true).equals.not(false);
assert.deepEqual(expr.getClasses(), ["fubar"]);
assert.deepEqual(expr.getExprs(), [true, false]);
assert.deepEqual(expr.getOps(), [OP_CODE.VAL, "0", OP_CODE.EQUAL, OP_CODE.NOT, OP_CODE.VAL, "1"]);
assert.equal(runtime(expr.getShape(), expr.getClasses(), [true, false]), "fubar");
assert.equal(runtime(expr.getShape(), expr.getClasses(), [true, true]), "");
}

@test "multiple classes"() {
let expr = new Expression();
expr.apply("fubar").when("arg0").equals.not("arg1");
expr.apply("bizbaz").when("arg0").equals.val("arg2");
assert.equal(runtime(expr.getShape(), expr.getClasses(), [true, false, false]), "fubar");
assert.equal(runtime(expr.getShape(), expr.getClasses(), [true, false, true]), "fubar bizbaz");
assert.equal(runtime(expr.getShape(), expr.getClasses(), [false, false, true]), "");
}
}