refactor: replace array-based expressions with typed class hierarchy

Replace the old Lisp-like array expression representation
(["AND", ["PARAM", ...], ...]) with a class hierarchy rooted in
an abstract Expression class (Literal, Parameter, Binary, Unary,
FunctionCall, Conditional). This gives compile-time type safety
to expression construction, evaluation, and pattern matching.

Key changes:
- New lib/common/expression.ts defines the class hierarchy with
  map/evaluate/toString methods
- Rewrite parser as hand-rolled recursive descent, removing the
  parsimmon dependency
- Extend Path with a colon separator to unify parameter identity
  and attributes into a single address space (e.g.
  "Device.WiFi.SSID.1.Name:writable"). The colon field tracks
  how many trailing segments are attribute qualifiers. PathSet
  rewritten with separate param/attr segment indexes
- Flatten device attribute representation in the UI to
  colon-delimited keys mirroring the new path syntax
This commit is contained in:
Zaid Humaimidi committed 2026-03-12 14:58:20 -07:00
1 parent 75908e624f
commit 6f3ce6f7c8
71 files changed
+4045 -3449

No files matched your search

+30 -30
View File
@@ -1,5 +1,5 @@
import { PermissionSet, Expression } from "../types.ts";
import { evaluate, or } from "./expression/util.ts";
import { PermissionSet } from "../types.ts";
import Expression from "./expression.ts";
export default class Authorizer {
declare private permissionSets: PermissionSet[];
@@ -43,12 +43,12 @@ export default class Authorizer {
if (this.getFilterCache.has(cacheKey))
return this.getFilterCache.get(cacheKey);
let filter: Expression = null;
let filter: Expression = new Expression.Literal(false);
for (const permissionSet of this.permissionSets) {
for (const perm of permissionSet) {
if (perm[resourceType]) {
if (perm[resourceType].access >= access)
filter = or(filter, perm[resourceType].filter);
filter = Expression.or(filter, perm[resourceType].filter);
}
}
}
@@ -64,7 +64,7 @@ export default class Authorizer {
if (this.validatorCache.has(resource))
return this.validatorCache.get(resource);
const validators: Expression[] = [];
let validators: Expression = new Expression.Literal(false);
for (const permissionSet of this.permissionSets) {
for (const perm of permissionSet) {
@@ -73,7 +73,7 @@ export default class Authorizer {
perm[resourceType].access >= 3 &&
perm[resourceType].validate
)
validators.push(perm[resourceType].validate);
validators = Expression.or(validators, perm[resourceType].validate);
}
}
@@ -82,8 +82,6 @@ export default class Authorizer {
mutation: any,
any: any,
): boolean => {
if (!validators.length) return false;
const object = {
mutationType,
mutation,
@@ -92,31 +90,33 @@ export default class Authorizer {
options: any,
};
const valueFunction = (paramName): any => {
const entry = paramName.split(".", 1)[0];
paramName = paramName.slice(entry.length + 1);
let value = null;
if (["mutation", "options"].includes(entry)) {
value = object[entry];
for (const seg of paramName.split(".")) {
if (value == null) break;
if (typeof value !== "object") value = null;
else value = value[seg];
const now = Date.now();
const res = validators.evaluate((exp) => {
if (exp instanceof Expression.Literal) return exp;
if (exp instanceof Expression.Parameter) {
if (exp.path.colon) return new Expression.Literal(null);
const entry = exp.path.segments[0] as string;
const paramName = exp.path.slice(1);
let value = null;
if (["mutation", "options"].includes(entry)) {
value = object[entry];
for (const seg of paramName.segments) {
if (value == null) break;
if (typeof value !== "object") value = null;
else value = value[seg as string];
}
} else if (object[entry]) {
if (paramName.length) value = object[entry][paramName.toString()];
else value = object[entry];
}
} else if (object[entry]) {
if (paramName) value = object[entry][paramName];
else value = object[entry];
return new Expression.Literal(value);
} else if (exp instanceof Expression.FunctionCall) {
if (exp.name === "NOW") return new Expression.Literal(now);
}
return new Expression.Literal(null);
}).value;
return value;
};
const res = evaluate(
validators.length > 1 ? ["OR", validators] : validators[0],
valueFunction,
Date.now(),
);
return !Array.isArray(res) && !!res;
return !!res;
};
this.validatorCache.set(resource, validator);
+291
View File
@@ -0,0 +1,291 @@
import {
Cursor,
parseExpression,
stringifyExpression,
} from "./expression/parser.ts";
import Path from "./path.ts";
import { reduce } from "./expression/evaluate.ts";
export type Value = string | number | boolean | null;
export abstract class Expression {
private _string: string;
abstract map(fn: (e: Expression, i: number) => Expression): Expression;
abstract mapAsync(
fn: (e: Expression, i: number) => Promise<Expression>,
): Promise<Expression>;
toString(): string {
if (!this._string) this._string = stringifyExpression(this);
return this._string;
}
evaluate<T extends Expression>(fn: (e: Expression) => T = (e) => e as T): T {
return fn(reduce(this.map((e) => e.evaluate(fn))));
}
async evaluateAsync<T extends Expression>(
fn: (e: Expression) => Promise<T>,
): Promise<T> {
return await fn(
reduce(await this.mapAsync(async (e) => await e.evaluateAsync(fn))),
);
}
static parse(input: string): Expression {
const cursor = new Cursor(input);
const exp = parseExpression(cursor);
if (cursor.charCode) throw new Error("Unexpected character");
return exp;
}
static and(left: Expression, right: Expression): Expression {
// Flatten same-operator tree into operands
const operands: Expression[] = [];
const stack: Expression[] = [right, left];
while (stack.length) {
const e = stack.pop()!;
if (e instanceof Expression.Binary && e.operator === "AND")
stack.push(e.right, e.left);
else operands.push(e);
}
// Fold literals using three-valued AND logic
let folded: boolean | null = true;
let i = 0;
while (i < operands.length) {
const e = operands[i];
if (e instanceof Expression.Literal) {
operands.splice(i, 1);
if (e.value == null) folded = folded === false ? false : null;
else if (!e.value) return new Expression.Literal(false);
// truthy is identity for AND; discard
} else {
i++;
}
}
// Rebuild: folded value + remaining non-literal operands
if (!operands.length) return new Expression.Literal(folded);
let result = operands.reduce((a, b) => new Expression.Binary("AND", a, b));
if (folded === null)
result = new Expression.Binary(
"AND",
new Expression.Literal(null),
result,
);
return result;
}
static or(left: Expression, right: Expression): Expression {
// Flatten same-operator tree into operands
const operands: Expression[] = [];
const stack: Expression[] = [right, left];
while (stack.length) {
const e = stack.pop()!;
if (e instanceof Expression.Binary && e.operator === "OR")
stack.push(e.right, e.left);
else operands.push(e);
}
// Fold literals using three-valued OR logic
let folded: boolean | null = false;
let i = 0;
while (i < operands.length) {
const e = operands[i];
if (e instanceof Expression.Literal) {
operands.splice(i, 1);
if (e.value == null) folded = folded === true ? true : null;
else if (e.value) return new Expression.Literal(true);
// falsy is identity for OR; discard
} else {
i++;
}
}
// Rebuild: folded value + remaining non-literal operands
if (!operands.length) return new Expression.Literal(folded);
let result = operands.reduce((a, b) => new Expression.Binary("OR", a, b));
if (folded === null)
result = new Expression.Binary(
"OR",
new Expression.Literal(null),
result,
);
return result;
}
}
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Expression {
export class Literal extends Expression {
constructor(public readonly value: Value) {
super();
}
map(): Literal {
return this;
}
async mapAsync(): Promise<Literal> {
return this;
}
}
export class Parameter extends Expression {
constructor(public readonly path: Path) {
super();
}
map(): Parameter {
return this;
}
async mapAsync(): Promise<Parameter> {
return this;
}
toString(): string {
return this.path.toString();
}
}
export class Unary extends Expression {
constructor(
public readonly operator: string,
public readonly operand: Expression,
) {
super();
}
map(fn: (e: Expression, i: number) => Expression): Unary {
const operand = fn(this.operand, 0);
if (operand === this.operand) return this;
return Reflect.construct(this.constructor, [this.operator, operand]);
}
async mapAsync(
fn: (e: Expression, i: number) => Promise<Expression>,
): Promise<Unary> {
const operand = await fn(this.operand, 0);
if (operand === this.operand) return this;
return Reflect.construct(this.constructor, [this.operator, operand]);
}
}
export class Binary extends Expression {
constructor(
public readonly operator: string,
public readonly left: Expression,
public readonly right: Expression,
) {
super();
}
map(fn: (e: Expression, i: number) => Expression): Binary {
const left = fn(this.left, 0);
const right = fn(this.right, 1);
if (left === this.left && right === this.right) return this;
return Reflect.construct(this.constructor, [this.operator, left, right]);
}
async mapAsync(
fn: (e: Expression, i: number) => Promise<Expression>,
): Promise<Binary> {
const left = await fn(this.left, 0);
const right = await fn(this.right, 1);
if (left === this.left && right === this.right) return this;
return Reflect.construct(this.constructor, [this.operator, left, right]);
}
}
export class FunctionCall extends Expression {
constructor(
public readonly name: string,
public readonly args: Expression[],
) {
super();
}
map(fn: (e: Expression, i: number) => Expression): FunctionCall {
const args = this.args.map(fn);
if (args.every((arg, i) => arg === this.args[i])) return this;
return new FunctionCall(this.name, args);
}
async mapAsync(
fn: (e: Expression, i: number) => Promise<Expression>,
): Promise<FunctionCall> {
const args = await Promise.all(this.args.map(fn));
if (args.every((arg, i) => arg === this.args[i])) return this;
return new FunctionCall(this.name, args);
}
}
export class Conditional extends Expression {
constructor(
public readonly condition: Expression,
public readonly then: Expression,
public readonly otherwise: Expression,
) {
super();
}
map(fn: (e: Expression, i: number) => Expression): Conditional {
const condition = fn(this.condition, 0);
const then = fn(this.then, 1);
const otherwise = fn(this.otherwise, 2);
if (
condition === this.condition &&
then === this.then &&
otherwise === this.otherwise
)
return this;
return new Conditional(condition, then, otherwise);
}
async mapAsync(
fn: (e: Expression, i: number) => Promise<Expression>,
): Promise<Conditional> {
const condition = await fn(this.condition, 0);
const then = await fn(this.then, 1);
const otherwise = await fn(this.otherwise, 2);
if (
condition === this.condition &&
then === this.then &&
otherwise === this.otherwise
)
return this;
return new Conditional(condition, then, otherwise);
}
}
}
export function extractPaths(exp: Expression): Path[] {
if (exp instanceof Expression.Parameter) return [exp.path];
const paths: Path[] = [];
exp.map((e) => {
if (e instanceof Expression.Parameter) paths.push(e.path);
else paths.push(...extractPaths(e));
return e;
});
return paths;
}
export function parseList(input: string): Expression[] {
const CHAR_COMMA = 44;
const res: Expression[] = [];
const cursor = new Cursor(input);
cursor.skipwhitespace();
if (!cursor.charCode) return res;
res.push(parseExpression(cursor));
while (cursor.charCode === CHAR_COMMA) {
cursor.step();
res.push(parseExpression(cursor));
}
if (cursor.charCode) throw new Error("Unexpected character");
return res;
}
export default Expression;
-87
View File
@@ -1,87 +0,0 @@
export function and(a: bigint, b: bigint): bigint {
return a & b;
}
export function or(a: bigint, b: bigint): bigint {
return a | b;
}
export function xor(a: bigint, b: bigint): bigint {
return a ^ b;
}
export function not(a: bigint): bigint {
return ~a;
}
export function lshift(a: bigint, b: bigint): bigint {
return a << b;
}
export function rshift(a: bigint, b: bigint): bigint {
return a >> b;
}
export function add(a: bigint, b: bigint): bigint {
return a + b;
}
export function sub(a: bigint, b: bigint): bigint {
return a - b;
}
export function mul(a: bigint, b: bigint): bigint {
return a * b;
}
export function div(a: bigint, b: bigint): bigint {
return a / b;
}
export function exp(a: bigint, b: bigint): bigint {
return a ** b;
}
export function rem(a: bigint, b: bigint): bigint {
return a % b;
}
export function toNumber(a: bigint): number {
return Number(a);
}
export function eq(a: bigint, b: bigint): boolean {
return a === b;
}
export function ne(a: bigint, b: bigint): boolean {
return a !== b;
}
export function lt(a: bigint, b: bigint): boolean {
return a < b;
}
export function lte(a: bigint, b: bigint): boolean {
return a <= b;
}
export function gt(a: bigint, b: bigint): boolean {
return a > b;
}
export function gte(a: bigint, b: bigint): boolean {
return a >= b;
}
export function asUintN(a: number, b: bigint): bigint {
return BigInt.asUintN(a, b);
}
export function asIntN(a: number, b: bigint): bigint {
return BigInt.asIntN(a, b);
}
const _BigInt = BigInt;
type _bigint = bigint;
export { _BigInt as BigInt, type _bigint as bigint };
+218
View File
@@ -0,0 +1,218 @@
import Expression from "../expression.ts";
import { likePatternToRegExp } from "./parser.ts";
function compare(
a: boolean | number | string,
b: boolean | number | string,
): number {
if (typeof a === "boolean") a = +a;
if (typeof b === "boolean") b = +b;
if (typeof a !== typeof b) return typeof a === "string" ? 1 : -1;
return a > b ? 1 : a < b ? -1 : 0;
}
function toNumber(a: boolean | number | string): number {
switch (typeof a) {
case "number":
return a;
case "boolean":
return +a;
case "string":
return parseFloat(a) || 0;
}
}
function toString(a: boolean | number | string): string {
switch (typeof a) {
case "string":
return a;
case "number":
return a.toString();
case "boolean":
return (+a).toString();
}
}
const regExpCache: WeakMap<Expression.Literal, RegExp> = new WeakMap();
export function reduce(exp: Expression): Expression {
if (exp instanceof Expression.Literal) return exp;
if (exp instanceof Expression.Unary) {
if (exp.operator === "NOT") {
if (exp.operand instanceof Expression.Literal) {
if (exp.operand.value == null) return exp.operand;
return new Expression.Literal(!exp.operand.value);
} else if (exp.operand instanceof Expression.Unary) {
if (exp.operand.operator === "NOT") return exp.operand.operand;
}
} else if (exp.operator === "IS NULL") {
if (exp.operand instanceof Expression.Literal) {
return new Expression.Literal(exp.operand.value == null);
}
} else if (exp.operator === "IS NOT NULL") {
if (exp.operand instanceof Expression.Literal) {
return new Expression.Literal(exp.operand.value != null);
}
}
} else if (exp instanceof Expression.Binary) {
if (exp.operator === "AND") {
return Expression.and(exp.left, exp.right);
} else if (exp.operator === "OR") {
return Expression.or(exp.left, exp.right);
} else if (["=", ">", "<", "<>", ">=", "<="].includes(exp.operator)) {
if (exp.left instanceof Expression.Literal && exp.left.value == null)
return exp.left;
if (exp.right instanceof Expression.Literal && exp.right.value == null)
return exp.right;
if (
exp.left instanceof Expression.Literal &&
exp.right instanceof Expression.Literal
) {
const c = compare(exp.left.value, exp.right.value);
switch (exp.operator) {
case "=":
return new Expression.Literal(c === 0);
case ">":
return new Expression.Literal(c > 0);
case "<":
return new Expression.Literal(c < 0);
case "<>":
return new Expression.Literal(c !== 0);
case ">=":
return new Expression.Literal(c >= 0);
case "<=":
return new Expression.Literal(c <= 0);
}
}
} else if (["+", "-", "*", "/"].includes(exp.operator)) {
if (exp.left instanceof Expression.Literal && exp.left.value == null)
return exp.left;
if (exp.right instanceof Expression.Literal && exp.right.value == null)
return exp.right;
if (
exp.left instanceof Expression.Literal &&
exp.right instanceof Expression.Literal
) {
const a = toNumber(exp.left.value);
const b = toNumber(exp.right.value);
switch (exp.operator) {
case "+":
return new Expression.Literal(a + b);
case "-":
return new Expression.Literal(a - b);
case "*":
return new Expression.Literal(a * b);
case "/":
return new Expression.Literal(a / b);
}
}
} else if (exp.operator === "%") {
if (exp.left instanceof Expression.Literal && exp.left.value == null)
return exp.left;
if (exp.right instanceof Expression.Literal && exp.right.value == null)
return exp.right;
if (
exp.left instanceof Expression.Literal &&
exp.right instanceof Expression.Literal
) {
const a = toNumber(exp.left.value);
const b = Math.trunc(toNumber(exp.right.value));
if (b === 0) return new Expression.Literal(null);
return new Expression.Literal(a % b);
}
} else if (exp.operator === "||") {
if (exp.left instanceof Expression.Literal && exp.left.value == null)
return exp.left;
if (exp.right instanceof Expression.Literal && exp.right.value == null)
return exp.right;
if (
exp.left instanceof Expression.Literal &&
exp.right instanceof Expression.Literal
) {
const a = toString(exp.left.value);
const b = toString(exp.right.value);
return new Expression.Literal(a + b);
}
} else if (exp.operator === "LIKE") {
if (exp.left instanceof Expression.Literal && exp.left.value == null)
return exp.left;
if (exp.right instanceof Expression.Literal && exp.right.value == null)
return exp.right;
if (
exp.left instanceof Expression.Literal &&
exp.right instanceof Expression.Literal
) {
const s = toString(exp.left.value);
let r = regExpCache.get(exp.right);
if (!r) {
r = likePatternToRegExp(toString(exp.right.value));
regExpCache.set(exp.right, r);
}
return new Expression.Literal(r.test(s));
}
} else if (exp.operator === "NOT LIKE") {
if (exp.left instanceof Expression.Literal && exp.left.value == null)
return exp.left;
if (exp.right instanceof Expression.Literal && exp.right.value == null)
return exp.right;
if (
exp.left instanceof Expression.Literal &&
exp.right instanceof Expression.Literal
) {
const s = toString(exp.left.value);
let r = regExpCache.get(exp.right);
if (!r) {
r = likePatternToRegExp(toString(exp.right.value));
regExpCache.set(exp.right, r);
}
return new Expression.Literal(!r.test(s));
}
}
} else if (exp instanceof Expression.FunctionCall) {
if (exp.name === "COALESCE") {
const args = exp.args.filter(
(arg) => !(arg instanceof Expression.Literal && arg.value == null),
);
if (!args.length) return new Expression.Literal(null);
if (args.length === 1) return args[0];
if (args[0] instanceof Expression.Literal) return args[0];
if (args.length !== exp.args.length)
return new Expression.FunctionCall("COALESCE", args);
} else if (exp.name === "UPPER") {
if (exp.args[0] instanceof Expression.Literal) {
if (exp.args[0].value == null) return exp.args[0];
const a = toString(exp.args[0].value);
return new Expression.Literal(a.toUpperCase());
}
} else if (exp.name === "LOWER") {
if (exp.args[0] instanceof Expression.Literal) {
if (exp.args[0].value == null) return exp.args[0];
const a = toString(exp.args[0].value);
return new Expression.Literal(a.toLowerCase());
}
} else if (exp.name === "ROUND") {
let p = 0;
if (exp.args.length > 1) {
if (exp.args[1] instanceof Expression.Literal) {
if (exp.args[1].value == null) return exp.args[1];
p = Math.trunc(toNumber(exp.args[1].value));
}
}
if (exp.args[0] instanceof Expression.Literal) {
if (exp.args[0].value == null) return exp.args[0];
const n = toNumber(exp.args[0].value);
const d = 10 ** p;
const m = n * d * (1 + Number.EPSILON);
return new Expression.Literal(Math.round(m) / d);
}
}
} else if (exp instanceof Expression.Conditional) {
if (exp.condition instanceof Expression.Literal) {
if (exp.condition.value) return exp.then;
return exp.otherwise;
}
}
return exp;
}
+229 -231
View File
@@ -1,22 +1,15 @@
import * as BI from "./bigint.ts";
import { Expression } from "../../types.ts";
import { map } from "./parser.ts";
import { and, evaluateCallback } from "./util.ts";
const ZERO = BI.BigInt(0);
const ONE = BI.BigInt(1);
const TWO = BI.BigInt(2);
const NEGATIVE_ONE = BI.BigInt(-1);
import Expression from "../expression.ts";
import { reduce } from "./evaluate.ts";
class Indeterminates {
declare public map: Map<string, number>;
declare public sortedKeys: string[];
declare public map: Map<Expression, number>;
declare public sortedKeys: Expression[];
public constructor(str?: string) {
public constructor(exp?: Expression) {
this.map = new Map();
if (str) {
this.map.set(str, 1);
this.sortedKeys = [str];
if (exp) {
this.map.set(exp, 1);
this.sortedKeys = [exp];
} else {
this.sortedKeys = [];
}
@@ -37,27 +30,31 @@ class Indeterminates {
const res = new Indeterminates();
res.sortedKeys = indeterminates1.sortedKeys.slice();
res.map = new Map(indeterminates1.map);
const strMap: Map<string, Expression> = new Map();
for (const k of res.map.keys()) strMap.set(k.toString(), k);
for (const [key, val] of indeterminates2.map) {
const v = res.map.get(key);
if (!v) {
const k = strMap.get(key.toString());
if (!k) {
res.map.set(key, val);
res.sortedKeys.push(key);
} else {
const v2 = val + v;
const v2 = val + res.map.get(k);
if (!v2) {
res.map.delete(key);
res.sortedKeys = res.sortedKeys.filter((s) => s !== key);
res.map.delete(k);
res.sortedKeys = res.sortedKeys.filter((s) => s !== k);
} else {
res.map.set(key, v2);
res.map.set(k, v2);
}
}
}
res.sortedKeys.sort((a, b) => {
if (a.length !== b.length) return b.length - a.length;
else if (a > b) return 1;
else if (a < b) return -1;
const str1 = a.toString();
const str2 = b.toString();
if (str1.length !== str2.length) return str2.length - str1.length;
else if (str1 > str2) return 1;
else if (str1 < str2) return -1;
return 0;
});
@@ -73,10 +70,10 @@ class Indeterminates {
const k2 = b.sortedKeys[i];
const w2 = b.map.get(k2);
if (w1 !== w2) return w2 - w1;
if (k1.length > k2.length) return -1;
else if (k1.length < k2.length) return 1;
else if (k1 > k2) return 1;
else if (k1 < k2) return -1;
if (k1.toString().length > k2.toString().length) return -1;
else if (k1.toString().length < k2.toString().length) return 1;
else if (k1.toString() > k2.toString()) return 1;
else if (k1.toString() < k2.toString()) return -1;
}
return 0;
}
@@ -84,26 +81,35 @@ class Indeterminates {
interface Term {
indeterminates: Indeterminates;
coefficientNumerator: BI.bigint;
coefficientDenominator: BI.bigint;
coefficientNumerator: bigint;
coefficientDenominator: bigint;
}
function findGcd(a: BI.bigint, b: BI.bigint): BI.bigint {
while (BI.ne(b, ZERO)) {
function findGcd(a: bigint, b: bigint): bigint {
while (b !== 0n) {
const t = b;
b = BI.rem(a, b);
b = a % b;
a = t;
}
return a;
}
class Polynomial {
class Polynomial extends Expression {
declare public terms: Term[];
public constructor(terms: Term[]) {
super();
this.terms = terms;
}
map(): Polynomial {
return this;
}
async mapAsync(): Promise<Polynomial> {
return this;
}
public static simplifyTerms(terms: Term[]): Term[] {
const ts = terms
.slice()
@@ -115,40 +121,37 @@ class Polynomial {
const t1 = ts[i - 1];
const t2 = ts[i];
if (Indeterminates.compare(t1.indeterminates, t2.indeterminates) === 0) {
const numerator = BI.add(
BI.mul(t1.coefficientNumerator, t2.coefficientDenominator),
BI.mul(t2.coefficientNumerator, t1.coefficientDenominator),
);
const numerator =
t1.coefficientNumerator * t2.coefficientDenominator +
t2.coefficientNumerator * t1.coefficientDenominator;
const denominator = BI.mul(
t1.coefficientDenominator,
t2.coefficientDenominator,
);
const denominator =
t1.coefficientDenominator * t2.coefficientDenominator;
const gcd = findGcd(numerator, denominator);
ts[i] = {
indeterminates: t2.indeterminates,
coefficientNumerator: BI.div(numerator, gcd),
coefficientDenominator: BI.div(denominator, gcd),
coefficientNumerator: numerator / gcd,
coefficientDenominator: denominator / gcd,
};
ts[i - 1] = {
indeterminates: t1.indeterminates,
coefficientNumerator: ZERO,
coefficientNumerator: 0n,
coefficientDenominator: t1.coefficientDenominator,
};
}
}
return ts.filter((v) => BI.ne(v.coefficientNumerator, ZERO));
return ts.filter((v) => v.coefficientNumerator !== 0n);
}
public static fromIndeterminate(indeterminate: Expression): Polynomial {
const indeterminates = new Indeterminates(JSON.stringify(indeterminate));
const indeterminates = new Indeterminates(indeterminate);
const terms = [
{
indeterminates: indeterminates,
coefficientNumerator: ONE,
coefficientDenominator: ONE,
coefficientNumerator: 1n,
coefficientDenominator: 1n,
},
];
return new Polynomial(terms);
@@ -156,15 +159,12 @@ class Polynomial {
public static fromConstant(constant: number): Polynomial {
const [int, frac] = Math.abs(constant).toString(2).split(".", 2);
let numerator = BI.BigInt("0b" + int);
if (constant < 0) numerator = BI.mul(numerator, NEGATIVE_ONE);
let denominator = ONE;
let numerator = BigInt("0b" + int);
if (constant < 0) numerator = numerator / -1n;
let denominator = 1n;
if (frac) {
denominator = BI.exp(TWO, BI.BigInt(frac.length));
numerator = BI.add(
BI.mul(numerator, denominator),
BI.BigInt("0b" + frac),
);
denominator = 2n ** BigInt(frac.length);
numerator = numerator * denominator + BigInt("0b" + frac);
}
const terms = [
@@ -181,7 +181,7 @@ class Polynomial {
public negation(): Polynomial {
const terms = this.terms.map((t) => ({
indeterminates: t.indeterminates,
coefficientNumerator: BI.mul(t.coefficientNumerator, NEGATIVE_ONE),
coefficientNumerator: t.coefficientNumerator * -1n,
coefficientDenominator: t.coefficientDenominator,
}));
@@ -217,14 +217,9 @@ class Polynomial {
for (const t1 of this.terms) {
for (const t2 of rhs.terms) {
const numerator = BI.mul(
t1.coefficientNumerator,
t2.coefficientNumerator,
);
const denominator = BI.mul(
t1.coefficientDenominator,
t2.coefficientDenominator,
);
const numerator = t1.coefficientNumerator * t2.coefficientNumerator;
const denominator =
t1.coefficientDenominator * t2.coefficientDenominator;
const gcd = findGcd(numerator, denominator);
terms.push({
@@ -232,8 +227,8 @@ class Polynomial {
t1.indeterminates,
t2.indeterminates,
),
coefficientNumerator: BI.div(numerator, gcd),
coefficientDenominator: BI.div(denominator, gcd),
coefficientNumerator: numerator / gcd,
coefficientDenominator: denominator / gcd,
});
}
}
@@ -245,41 +240,49 @@ class Polynomial {
return this.multiply(rhs.reciprocal());
}
public toString(): string {
const add: string[] = [];
public toExpression(): Expression {
const add: Expression[] = [];
for (const t of this.terms) {
const coefficient =
BI.toNumber(t.coefficientNumerator) /
BI.toNumber(t.coefficientDenominator);
Number(t.coefficientNumerator) / Number(t.coefficientDenominator);
const mul: string[] = [];
const mul: Expression[] = [];
if (t.indeterminates.sortedKeys.length) {
for (const k of t.indeterminates.sortedKeys) {
const w = t.indeterminates.map.get(k);
for (let i = Math.abs(w); i > 0; --i) {
if (w > 0) mul.push(k);
else mul.push(`["/",1,${k}]`);
else
mul.push(
new Expression.Binary("/", new Expression.Literal(1), k),
);
}
}
if (coefficient !== 1) mul.push(coefficient.toString());
if (coefficient !== 1) mul.push(new Expression.Literal(coefficient));
if (mul.length > 1) add.push(`["*",${mul.join(",")}]`);
else add.push(mul["0"]);
while (mul.length > 1) {
const r = mul.pop();
const l = mul.pop();
mul.push(new Expression.Binary("*", l, r));
}
add.push(mul[0]);
} else {
add.push(coefficient.toString());
add.push(new Expression.Literal(coefficient));
}
}
if (!add.length) return "0";
else if (add.length === 1) return add[0];
else return `["+",${add.join(",")}]`;
while (add.length > 1) {
const r = add.pop();
const l = add.pop();
add.push(new Expression.Binary("+", l, r));
}
if (!add.length) return new Expression.Literal(0);
return add[0];
}
}
const ADDITIVE_IDENTITY = Polynomial.fromConstant(0);
const MULTIPLICATIVE_IDENTITY = Polynomial.fromConstant(1);
const SWAPPED_OPS = {
"=": "=",
"<>": "<>",
@@ -289,156 +292,163 @@ const SWAPPED_OPS = {
"<=": ">=",
};
function cartesianProduct<T>(arrays: T[][]): T[][] {
return arrays.reduce<T[][]>(
(acc, cur) => acc.flatMap((a) => cur.map((item) => [...a, item])),
[[]],
);
}
function toPolynomial(e: Expression): Polynomial {
if (e instanceof Polynomial) return e;
if (e instanceof Expression.Literal) {
if (e.value == null) return null;
if (typeof e.value === "number") return Polynomial.fromConstant(e.value);
if (typeof e.value === "string")
return Polynomial.fromConstant(parseFloat(e.value) || 0);
if (typeof e.value === "boolean") return Polynomial.fromConstant(+e.value);
}
return Polynomial.fromIndeterminate(e);
}
function fromPolynomial(e: Expression): Expression {
if (e instanceof Polynomial) return e.toExpression();
if (e instanceof Expression.Conditional) return e.map(fromPolynomial);
return e;
}
function normalizeCallback(exp: Expression): Expression {
if (!Array.isArray(exp)) return exp;
const op = exp[0];
if (op === "FUNC" && exp[1] === "COALESCE") {
const res: Expression[] = ["CASE"];
for (let i = 2; i < exp.length; ++i)
res.push(normalizeCallback(["IS NOT NULL", exp[i]]), exp[i]);
return normalizeCallback(res);
}
if (op === "CASE") {
const res = [] as [Expression, Expression][];
for (let i = 1; i < exp.length; i += 2) {
let w = exp[i];
if (w instanceof Polynomial) w = JSON.parse(w.toString());
if (!Array.isArray(w) && !w) continue;
const t = exp[i + 1];
if (!Array.isArray(t) || t[0] !== "CASE") {
res.push([w, t]);
continue;
}
for (let j = 1; j < t.length; j += 2) res.push([and(w, t[j]), t[j + 1]]);
res.push([w, null]);
if (!Array.isArray(w) && w) break;
}
while (res[res.length - 1][1] == null) res.pop();
return ["CASE", ...res.flat()];
}
const permutations: Map<number, [Expression, Expression][]> = new Map();
for (const [i, e] of exp.entries()) {
if (!Array.isArray(e)) continue;
if (e[0] !== "CASE") continue;
const perms: [Expression, Expression][] = [];
for (let j = 1; j < e.length; j += 2) perms.push([e[j], e[j + 1]]);
perms.push([true, null]);
permutations.set(i, perms);
}
if (permutations.size) {
let res: [Expression, Expression][] = [[true, exp]];
for (const [i, perms] of permutations) {
const res2: [Expression, Expression][] = [];
for (const [w, t] of perms) {
res2.push(
...res.map((r) => {
const e = (r[1] as Expression[]).slice();
e[i] = t;
return [and(w, r[0]), e] as [Expression, Expression];
}),
if (exp instanceof Expression.FunctionCall) {
if (exp.name === "COALESCE") {
let e: Expression = new Expression.Literal(null);
for (let i = exp.args.length - 1; i >= 0; --i) {
e = new Expression.Conditional(
normalizeCallback(new Expression.Unary("IS NOT NULL", exp.args[i])),
exp.args[i],
e,
);
if (!Array.isArray(w) && w) break;
}
res = res2;
return normalizeCallback(e);
}
for (const r of res) r[1] = normalizeCallback(r[1]);
if (res[0][0] === true) return res[0][1];
while (res[res.length - 1][1] == null) res.pop();
return ["CASE", ...res.flat()];
}
function toPolynomial(e: Expression): Polynomial {
if (e == null) return null;
if (e instanceof Polynomial) return e;
if (typeof e === "number") return Polynomial.fromConstant(e);
if (typeof e === "string")
return Polynomial.fromConstant(parseFloat(e) || 0);
if (typeof e === "boolean") return Polynomial.fromConstant(+e);
return Polynomial.fromIndeterminate(e);
if (exp instanceof Expression.Conditional) {
let e = exp;
if (e.condition instanceof Polynomial)
e = new Expression.Conditional(
e.condition.toExpression(),
e.then,
e.otherwise,
);
if (e.then instanceof Expression.Conditional) {
e = new Expression.Conditional(
Expression.and(e.condition, e.then.condition),
e.then.then,
new Expression.Conditional(e.condition, e.then.otherwise, e.otherwise),
);
}
return e;
}
if (op === "+") {
const args: Polynomial[] = [];
for (let i = 1; i < exp.length; ++i) {
const p = toPolynomial(exp[i]);
if (p == null) return null;
args.push(p);
const combs: [Expression, Expression][][] = [];
const callback: (e: Expression) => Expression = (e) => {
if (e instanceof Expression.Conditional) {
combs[combs.length - 1].push([e.condition, e.then]);
callback(e.otherwise);
} else {
combs[combs.length - 1].push([new Expression.Literal(true), e]);
}
return args.reduce(
(previousValue, currentValue) => previousValue.add(currentValue),
ADDITIVE_IDENTITY,
) as unknown as Expression;
} else if (op === "*") {
const args: Polynomial[] = [];
for (let i = 1; i < exp.length; ++i) {
const p = toPolynomial(exp[i]);
if (p == null) return null;
args.push(p);
return e;
};
exp.map((e) => {
combs.push([]);
callback(e);
return e;
});
if (combs.some((a) => a.length > 1)) {
let res: Expression = new Expression.Literal(null);
for (const p of cartesianProduct(combs).reverse()) {
let condition: Expression = new Expression.Literal(true);
const e = reduce(
normalizeCallback(
exp.map((_, i) => {
condition = Expression.and(condition, p[i][0]);
return p[i][1];
}),
),
);
if (!(condition instanceof Expression.Literal))
res = new Expression.Conditional(condition, e, res);
else if (condition.value) res = e;
}
return args.reduce(
(previousValue, currentValue) => previousValue.multiply(currentValue),
MULTIPLICATIVE_IDENTITY,
) as unknown as Expression;
} else if (op === "-") {
const args: Polynomial[] = [];
for (let i = 1; i < exp.length; ++i) {
const p = toPolynomial(exp[i]);
if (p == null) return null;
args.push(p);
}
return args.reduce((previousValue, currentValue) =>
previousValue.subtract(currentValue),
) as unknown as Expression;
} else if (op === "/") {
const args: Polynomial[] = [];
for (let i = 1; i < exp.length; ++i) {
const p = toPolynomial(exp[i]);
if (p == null) return null;
args.push(p);
}
return args.reduce((previousValue, currentValue) =>
previousValue.divide(currentValue),
) as unknown as Expression;
} else if (["=", "<>", ">", ">=", "<", "<="].includes(op)) {
if (exp[1] == null || exp[2] == null) return null;
let lhs: Polynomial, rhs: Polynomial;
if (exp[1] instanceof Polynomial) lhs = exp[1];
else if (typeof exp[1] === "number") lhs = Polynomial.fromConstant(exp[1]);
return res;
}
if (exp[2] instanceof Polynomial) rhs = exp[2];
else if (typeof exp[2] === "number") rhs = Polynomial.fromConstant(exp[2]);
if (exp instanceof Expression.Binary) {
if (["+", "-", "*", "/"].includes(exp.operator)) {
const lhs = toPolynomial(exp.left);
const rhs = toPolynomial(exp.right);
if (lhs == null || rhs == null) return new Expression.Literal(null);
if (exp.operator === "+") return lhs.add(rhs);
if (exp.operator === "-") return lhs.subtract(rhs);
if (exp.operator === "*") return lhs.multiply(rhs);
if (exp.operator === "/") return lhs.divide(rhs);
} else if ([">", ">=", "<", "<=", "=", "<>"].includes(exp.operator)) {
let lhs: Polynomial, rhs: Polynomial;
if (lhs || rhs) {
if (!lhs) lhs = Polynomial.fromIndeterminate(exp[1]);
if (!rhs) rhs = Polynomial.fromIndeterminate(exp[2]);
if (exp.left instanceof Polynomial) lhs = exp.left;
else if (exp.left instanceof Expression.Literal) {
if (exp.left.value == null) return exp.left;
else if (typeof exp.left.value === "number")
lhs = Polynomial.fromConstant(exp.left.value);
}
lhs = lhs.subtract(rhs);
rhs = lhs.constant().negation();
lhs = lhs.add(rhs);
if (exp.right instanceof Polynomial) rhs = exp.right;
else if (exp.right instanceof Expression.Literal) {
if (exp.right.value == null) return exp.right;
else if (typeof exp.right.value === "number")
rhs = Polynomial.fromConstant(exp.right.value);
}
if (!lhs.terms.length) {
exp = [op, JSON.parse(lhs.toString()), JSON.parse(rhs.toString())];
if (lhs || rhs) {
if (!lhs) lhs = Polynomial.fromIndeterminate(exp.left);
if (!rhs) rhs = Polynomial.fromIndeterminate(exp.right);
lhs = lhs.subtract(rhs);
rhs = lhs.constant().negation();
lhs = lhs.add(rhs);
if (!lhs.terms.length) {
const l = lhs.toExpression() as Expression.Literal;
const r = rhs.toExpression() as Expression.Literal;
if (exp.operator === "=")
return new Expression.Literal(l.value === r.value);
else if (exp.operator === "<>")
return new Expression.Literal(l.value !== r.value);
else if (exp.operator === ">")
return new Expression.Literal(l.value > r.value);
else if (exp.operator === ">=")
return new Expression.Literal(l.value >= r.value);
else if (exp.operator === "<")
return new Expression.Literal(l.value < r.value);
else if (exp.operator === "<=")
return new Expression.Literal(l.value <= r.value);
else throw new Error("Invalid operator");
}
if (op === "=") return exp[1] === exp[2];
else if (op === "<>") return exp[1] !== exp[2];
else if (op === ">") return exp[1] > exp[2];
else if (op === ">=") return exp[1] >= exp[2];
else if (op === "<") return exp[1] < exp[2];
else if (op === "<=") return exp[1] <= exp[2];
} else {
let flipOp = 1;
const n = lhs.terms[0].coefficientNumerator;
const d = lhs.terms[0].coefficientDenominator;
if (BI.lt(n, ZERO) || BI.lt(d, ZERO)) flipOp *= -1;
if (n < 0n || d < 0n) flipOp *= -1;
const reciprocal = new Polynomial([
{
@@ -463,30 +473,18 @@ function normalizeCallback(exp: Expression): Expression {
rhs = rhs.reciprocal();
}
if (flipOp < 0) exp = [SWAPPED_OPS[op], lhs, rhs];
else exp = [op, lhs, rhs];
if (flipOp > 0) exp = new Expression.Binary(exp.operator, lhs, rhs);
else exp = new Expression.Binary(SWAPPED_OPS[exp.operator], lhs, rhs);
}
}
}
// Restore polynomial expressions
exp = exp.map((e) =>
e instanceof Polynomial ? JSON.parse(e.toString()) : e,
);
exp = evaluateCallback(exp);
exp = exp.map(fromPolynomial);
return exp;
}
export default function normalize(expr: Expression): Expression {
expr = map(expr, normalizeCallback);
if (expr instanceof Polynomial) {
expr = JSON.parse(expr.toString());
} else if (Array.isArray(expr) && expr[0] === "CASE") {
expr = expr.map((e) =>
e instanceof Polynomial ? JSON.parse(e.toString()) : e,
);
}
return expr;
export default function normalize(exp: Expression): Expression {
return fromPolynomial(exp.evaluate(normalizeCallback));
}
+42 -30
View File
@@ -1,8 +1,8 @@
import { complement } from "espresso-iisojs";
import { Expression } from "../../types.ts";
import Expression from "../expression.ts";
import normalize from "./normalize.ts";
import { Clause, SynthContext } from "./synth.ts";
import { and, or } from "./util.ts";
import Path from "../path.ts";
type Bookmark = Record<string, null | boolean | number | string>;
@@ -27,25 +27,35 @@ export function bookmarkToExpression(
): Expression {
return Object.entries(sort)
.reverse()
.reduce((cur, kv) => {
const [param, asc] = kv;
if (asc < 0) {
if (bookmark[param] == null) {
return or(
["IS NOT NULL", ["PARAM", param]],
and(["IS NULL", ["PARAM", param]], cur),
.reduce(
(cur, kv) => {
const [param, asc] = kv;
const p = new Expression.Parameter(Path.parse(param));
const b = new Expression.Literal(bookmark[param]);
if (asc < 0) {
if (bookmark[param] == null) {
return Expression.or(
new Expression.Unary("IS NOT NULL", p),
Expression.and(new Expression.Unary("IS NULL", p), cur),
);
}
return new Expression.Binary(
"OR",
new Expression.Binary(">", p, b),
new Expression.Binary("AND", new Expression.Binary("=", p, b), cur),
);
} else {
let f: Expression = new Expression.Unary("IS NULL", p);
if (bookmark[param] == null) return Expression.and(f, cur);
f = Expression.or(f, new Expression.Binary("<", p, b));
return Expression.or(
f,
Expression.and(new Expression.Binary("=", p, b), cur),
);
}
let f = null;
f = or(f, [">", ["PARAM", param], bookmark[param]]);
return or(f, and(["=", ["PARAM", param], bookmark[param]], cur));
} else {
let f: Expression = ["IS NULL", ["PARAM", param]];
if (bookmark[param] == null) return and(f, cur);
f = or(f, ["<", ["PARAM", param], bookmark[param]]);
return or(f, and(["=", ["PARAM", param], bookmark[param]], cur));
}
}, true as Expression);
},
new Expression.Literal(true) as Expression,
);
}
function getCover(
@@ -60,7 +70,7 @@ function getCover(
const nextCov: Minterm = [];
if (sort > 0) {
const lhs = new Clause.Exp(["PARAM", param]);
const lhs = new Clause.Exp(new Expression.Parameter(Path.parse(param)));
const isNull = context.getVar(new Clause.IsNull(lhs));
cov.push((isNull << 2) ^ 2);
}
@@ -68,15 +78,15 @@ function getCover(
for (const m of minterm) {
const clause = context.getClause(m >>> 2);
if (clause instanceof Clause.IsNull) {
const lhs = clause.operand.expression();
if (!Array.isArray(lhs) || lhs[0] !== "PARAM") continue;
if (lhs[1] !== param) continue;
if (!(clause.operand instanceof Clause.Exp)) continue;
if (!(clause.operand.exp instanceof Expression.Parameter)) continue;
if (clause.operand.exp.path.toString() !== param) continue;
nextCov.push(m);
if (sort < 0 && m & 1) cov.push(m, m ^ 1);
} else if (clause instanceof Clause.Compare) {
const lhs = clause.lhs.expression();
if (!Array.isArray(lhs) || lhs[0] !== "PARAM") continue;
if (lhs[1] !== param) continue;
if (!(clause.lhs instanceof Clause.Exp)) continue;
if (!(clause.lhs.exp instanceof Expression.Parameter)) continue;
if (clause.lhs.exp.path.toString() !== param) continue;
if (!(m & 1) && sort > 0) continue;
nextCov.push(m);
@@ -117,18 +127,20 @@ export function paginate(
sort: Record<string, number>,
): [Expression, Expression] {
fetched = normalize(fetched);
if (!fetched) return [false, toFetch];
if (fetched instanceof Expression.Literal && !fetched.value)
return [new Expression.Literal(false), toFetch];
toFetch = normalize(toFetch);
if (!toFetch) return [false, toFetch];
if (toFetch instanceof Expression.Literal && !toFetch.value)
return [new Expression.Literal(false), toFetch];
const synth1 = Clause.fromExpression(fetched);
const synth2 = Clause.fromExpression(toFetch);
const context = new SynthContext();
const expr1Minterms = synth1.true(context);
const expr2MintermsC = complement(synth2.true(context));
const expr1Minterms = synth1.getMinterms(context, 0b100);
const expr2MintermsC = synth2.getMinterms(context, 0b011);
const gaps = context.sanitizeMinterms(
complement([
+496 -398
View File
@@ -1,5 +1,166 @@
import parsimmon from "parsimmon";
import { Expression } from "../../types.ts";
import Path from "../path.ts";
import Expression from "../expression.ts";
export class Cursor {
input: string;
pos: number;
boundryStack: number[][];
boundry: number[];
charCode: number;
constructor(input: string) {
this.input = input;
this.pos = 0;
this.boundryStack = [];
this.boundry = [];
this.charCode = input.charCodeAt(0) || 0;
}
fork(): Cursor {
const cursor = new Cursor(this.input);
cursor.pos = this.pos;
cursor.boundryStack = this.boundryStack.slice();
cursor.boundry = this.boundry;
cursor.charCode = this.charCode;
return cursor;
}
sync(cursor: Cursor): void {
this.pos = cursor.pos;
this.boundryStack = cursor.boundryStack.slice();
this.boundry = cursor.boundry;
this.charCode = cursor.charCode;
}
read(cur: Cursor): string {
return this.input.slice(this.pos, cur.pos);
}
step(): Cursor {
if (this.charCode === 0) return this;
++this.pos;
this.charCode = this.input.charCodeAt(this.pos) || 0;
if (this.boundry.includes(this.charCode)) this.charCode = 0;
return this;
}
walk(callback: (charCode: number) => boolean): Cursor {
while (this.charCode && callback(this.charCode)) this.step();
return this;
}
descend(chars: number[], override = true): void {
this.boundryStack.push(this.boundry);
if (override) this.boundry = chars;
else this.boundry = [...this.boundry, ...chars];
this.charCode = this.input.charCodeAt(this.pos) || 0;
if (this.boundry.includes(this.charCode)) this.charCode = 0;
}
ascend(): void {
if (!this.boundryStack.length) throw new Error("Unmatched boundry");
this.boundry = this.boundryStack.pop();
this.charCode = this.input.charCodeAt(this.pos) || 0;
}
skipwhitespace(): Cursor {
return this.walk((c) => c <= 32);
}
}
const CHAR_SINGLE_QUOTE = 39;
const CHAR_DOUBLE_QUOTE = 34;
const CHAR_OPEN_PAREN = 40;
const CHAR_CLOSE_PAREN = 41;
const CHAR_COMMA = 44;
const CHAR_PERIOD = 46;
const CHAR_COLON = 58;
const CHAR_OPEN_BRACKET = 91;
const CHAR_BACKSLASH = 92;
const CHAR_CLOSE_BRACKET = 93;
const BINARY_OPERATORS = [
">=",
"<=",
"<>",
"=",
">",
"<",
"LIKE",
"NOT LIKE",
"AND",
"OR",
"*",
"/",
"%",
"||",
"-",
"+",
];
const PRECEDENCE = {
OR: 10,
AND: 11,
NOT: 12,
"=": 20,
"<>": 20,
">": 20,
">=": 20,
"<": 20,
"<=": 20,
LIKE: 20,
"NOT LIKE": 20,
"IS NULL": 20,
"IS NOT NULL": 20,
"||": 30,
"+": 31,
"-": 31,
"*": 32,
"/": 32,
"%": 32,
};
function* range(s: number, e: number): Generator<number> {
for (let i = s; i < e; i++) yield i;
}
const PATH_CHARS = new Set<number>([
...range(65, 91),
...range(97, 123),
...range(48, 58),
95,
45,
42,
123,
125,
]);
function findOperator(cursor: Cursor): string {
cursor.skipwhitespace();
let found = "";
let foundCursor = cursor;
let operators = [...BINARY_OPERATORS, "IS NULL", "IS NOT NULL"];
for (let i = 0; operators.length && cursor.charCode; ++i) {
let c = cursor.charCode;
cursor.step();
if (c >= 97 && c <= 122) c -= 32;
if (c <= 32) {
cursor.skipwhitespace();
c = 32;
}
operators = operators.filter((o) => {
if (o.charCodeAt(i) !== c) return false;
if (o.length === i + 1) {
found = o;
foundCursor = cursor.fork();
}
return true;
});
}
if (found) cursor.sync(foundCursor);
return found;
}
// Turn escaped characters into real ones (e.g. "\\n" becomes "\n").
function interpretEscapes(str): string {
@@ -21,423 +182,330 @@ function interpretEscapes(str): string {
});
}
export function map<T>(exp: Expression, callback: (e: Expression) => T): T {
if (!Array.isArray(exp)) return callback(exp);
export function parseExpression(cursor: Cursor, presedence = 0): Expression {
cursor.skipwhitespace();
const char = cursor.charCode;
let lhs: Expression;
if (char === CHAR_OPEN_PAREN) {
cursor.step();
cursor.descend([CHAR_CLOSE_PAREN]);
lhs = parseExpression(cursor, 0);
cursor.ascend();
let clone;
for (let i = 1; i < exp.length; ++i) {
const sub = map(exp[i], callback);
if (sub !== exp[i]) {
clone = clone || exp.slice();
clone[i] = sub;
cursor.skipwhitespace();
if (cursor.charCode !== CHAR_CLOSE_PAREN)
throw new Error("Expected ')'" + cursor.pos + " " + cursor.charCode);
cursor.step();
} else if (char === CHAR_DOUBLE_QUOTE) {
cursor.step();
const cursor2 = cursor.fork();
for (
cursor2.descend([]);
cursor2.charCode !== CHAR_DOUBLE_QUOTE;
cursor2.step()
) {
if (!cursor2.charCode) throw new Error("Unterminated string");
if (cursor2.charCode === CHAR_BACKSLASH) cursor2.step();
}
cursor2.ascend();
const str = cursor.read(cursor2);
cursor.sync(cursor2);
cursor.step();
return new Expression.Literal(interpretEscapes(str));
} else if (char === CHAR_SINGLE_QUOTE) {
cursor.step();
const cursor2 = cursor.fork();
for (
cursor2.descend([]);
cursor2.charCode !== CHAR_SINGLE_QUOTE;
cursor2.step()
) {
if (!cursor2.charCode) throw new Error("Unterminated string");
if (cursor2.charCode === CHAR_BACKSLASH) cursor2.step();
}
cursor2.ascend();
const str = cursor.read(cursor2);
cursor.sync(cursor2);
cursor.step();
return new Expression.Literal(str.replaceAll("''", "'"));
} else {
const cursor2 = cursor.fork();
cursor.walk(
(c) => c > 32 && c !== CHAR_OPEN_PAREN && c !== CHAR_OPEN_BRACKET,
);
const token = cursor2.read(cursor);
if (!token) throw new Error("Invalid expression");
if (/^true$/i.test(token)) lhs = new Expression.Literal(true);
else if (/^false$/i.test(token)) lhs = new Expression.Literal(false);
else if (/^null$/i.test(token)) lhs = new Expression.Literal(null);
else if (/^-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?$/.test(token))
lhs = new Expression.Literal(Number(token));
else if (/^not$/i.test(token)) {
lhs = new Expression.Unary(
"NOT",
parseExpression(cursor, PRECEDENCE["NOT"]),
);
} else if (/^case$/i.test(token)) {
const pairs: [Expression, Expression][] = [];
for (;;) {
cursor.skipwhitespace();
const whenStr = cursor.fork().read(cursor.walk((c) => c > 32));
if (/^else$/i.test(whenStr)) {
lhs = parseExpression(cursor);
continue;
}
if (/^end$/i.test(whenStr)) break;
else if (lhs) throw new Error("Expected END");
if (!/^when$/i.test(whenStr)) throw new Error("Expected WHEN");
const condition = parseExpression(cursor);
cursor.skipwhitespace();
const thenStr = cursor.fork().read(cursor.walk((c) => c > 32));
if (!/^then$/i.test(thenStr)) throw new Error("Expected THEN");
const then = parseExpression(cursor);
pairs.push([condition, then]);
}
if (!lhs) lhs = new Expression.Literal(null);
while (pairs.length) {
const [condition, then] = pairs.pop();
lhs = new Expression.Conditional(condition, then, lhs);
}
} else if (cursor.charCode === CHAR_OPEN_PAREN) {
cursor.step();
cursor.descend([CHAR_CLOSE_PAREN]);
cursor.skipwhitespace();
const args = [] as Expression[];
while (cursor.charCode) {
cursor.descend([CHAR_COMMA], false);
const e = parseExpression(cursor);
args.push(e);
cursor.ascend();
cursor.skipwhitespace();
if ((cursor.charCode as number) !== CHAR_COMMA) break;
cursor.step();
}
cursor.ascend();
cursor.step();
lhs = new Expression.FunctionCall(token, args);
} else {
const p = parsePath(cursor2);
lhs = new Expression.Parameter(p);
cursor.sync(cursor2);
}
}
return callback(clone || exp);
}
export async function mapAsync(
exp: Expression,
callback: (e: Expression) => Promise<Expression>,
): Promise<Expression> {
if (!Array.isArray(exp)) return callback(exp);
let clone;
for (let i = 1; i < exp.length; ++i) {
const sub = await mapAsync(exp[i], callback);
if (sub !== exp[i]) {
clone = clone || exp.slice();
clone[i] = sub;
}
for (;;) {
cursor.skipwhitespace();
const cursor2 = cursor.fork();
const op = findOperator(cursor2);
const p = PRECEDENCE[op];
if (p <= presedence) return lhs;
if (op === "IS NULL") {
lhs = new Expression.Unary("IS NULL", lhs);
} else if (op === "IS NOT NULL") {
lhs = new Expression.Unary("IS NOT NULL", lhs);
} else if (BINARY_OPERATORS.includes(op)) {
lhs = new Expression.Binary(op, lhs, parseExpression(cursor2, p));
} else if (op) throw new Error("Unrecognized operator: " + op);
else break;
cursor.sync(cursor2);
}
return callback(clone || exp);
return lhs;
}
function binaryLeft(
operatorsParser: parsimmon.Parser<string>,
nextParser: parsimmon.Parser<Expression>,
): parsimmon.Parser<Expression> {
return parsimmon.seqMap(
nextParser,
parsimmon.seq(operatorsParser, nextParser).many(),
(first, rest) =>
rest.reduce((acc, ch) => {
const [op, another] = ch;
if (Array.isArray(acc) && op === acc[0]) return acc.concat([another]);
return [op, acc, another];
}, first),
);
// Parse old-format alias value: unquoted, double-quoted, or single-quoted.
// Returns the parsed string value. Cursor is advanced past the value.
function parseOldAliasValue(cursor: Cursor): string {
cursor.walk((c) => c <= 32); // skip leading whitespace
const c = cursor.charCode;
if (c === CHAR_DOUBLE_QUOTE) {
// Double-quoted: use JSON.parse semantics (handles \", \n, \uXXXX etc.)
// Descend with no boundaries so commas/brackets inside quotes are not
// treated as terminators.
const start = cursor.pos;
cursor.step();
cursor.descend([]);
while (cursor.charCode) {
if (cursor.charCode === CHAR_BACKSLASH) {
cursor.step();
if (!cursor.charCode) break;
} else if (cursor.charCode === CHAR_DOUBLE_QUOTE) {
cursor.ascend();
cursor.step();
return JSON.parse(cursor.input.slice(start, cursor.pos)) as string;
}
cursor.step();
}
throw new Error("Unterminated string");
}
if (c === CHAR_SINGLE_QUOTE) {
// Single-quoted: strip quotes, no escape processing
cursor.step();
cursor.descend([]);
const start = cursor.pos;
while (cursor.charCode && cursor.charCode !== CHAR_SINGLE_QUOTE)
cursor.step();
if (!cursor.charCode) throw new Error("Unterminated string");
const value = cursor.input.slice(start, cursor.pos);
cursor.ascend();
cursor.step();
return value;
}
// Unquoted: read until , or ] (respecting boundary), trim result
const start = cursor.pos;
cursor.walk(() => true);
return cursor.input.slice(start, cursor.pos).trim();
}
const lang = parsimmon.createLanguage({
ComparisonOperator: function () {
return parsimmon
.alt(
parsimmon.string(">="),
parsimmon.string("<>"),
parsimmon.string("<="),
parsimmon.string("="),
parsimmon.string(">"),
parsimmon.string("<"),
)
.skip(parsimmon.optWhitespace);
},
LikeOperator: function () {
return parsimmon
.alt(
parsimmon.regexp(/like/i).result("LIKE").desc("LIKE"),
parsimmon
.regexp(/not\s+like/i)
.result("NOT LIKE")
.desc("NOT LIKE"),
)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace);
},
IsNullOperator: function () {
return parsimmon
.alt(
parsimmon
.regexp(/is\s+null/i)
.result("IS NULL")
.desc("IS NULL"),
parsimmon
.regexp(/is\s+not\s+null/i)
.result("IS NOT NULL")
.desc("IS NOT NULL"),
)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace);
},
NotOperator: function () {
return parsimmon
.regexp(/not/i)
.result("NOT")
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("NOT");
},
AndOperator: function () {
return parsimmon
.regexp(/and/i)
.result("AND")
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("AND");
},
OrOperator: function () {
return parsimmon
.regexp(/or/i)
.result("OR")
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("OR");
},
Parameter: function (r) {
return parsimmon
.alt(
parsimmon.regexp(/[a-zA-Z0-9_.*-]+/),
r.Expression.wrap(
parsimmon.string("{").skip(parsimmon.optWhitespace),
parsimmon.string("}"),
),
)
.atLeast(1)
.map((x) => ["PARAM", x.length > 1 ? ["||"].concat(x) : x[0]])
.skip(parsimmon.optWhitespace)
.desc("parameter");
},
StringValueSql: function () {
return parsimmon
.regexp(/'([^']*)'/, 1)
.atLeast(1)
.skip(parsimmon.optWhitespace)
.map((s) => s.join("'"))
.desc("string");
},
StringValueJs: function () {
return parsimmon
.regexp(/"((?:\\.|.)*?)"/, 1)
.skip(parsimmon.optWhitespace)
.map(interpretEscapes)
.desc("string");
},
NumberValue: function () {
return parsimmon
.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.map(Number)
.desc("number");
},
BooleanValue: function () {
return parsimmon
.alt(
parsimmon.regexp(/true/i).result(true).desc("TRUE"),
parsimmon.regexp(/false/i).result(false).desc("FALSE"),
)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace);
},
NullValue: function () {
return parsimmon
.regexp(/null/i)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.result(null)
.desc("NULL");
},
FuncValue: function (r) {
return parsimmon.seqMap(
parsimmon
.regexp(/([a-zA-Z0-9_]+)/, 1)
.skip(parsimmon.optWhitespace)
.desc("function"),
r.ExpressionList.wrap(
parsimmon.string("(").skip(parsimmon.optWhitespace),
parsimmon.string(")").skip(parsimmon.optWhitespace),
),
(f, args) => ["FUNC", f.toUpperCase()].concat(args),
// Parse old-format alias content: key:value,key:value,...
// Cursor should be positioned at the start of bracket content
// (after '[', with ']' set as boundary).
// Returns an Expression: Binary("AND", ...) chain of Binary("=", param, literal).
// Empty content returns Literal(true).
function parseOldAlias(cursor: Cursor): Expression {
cursor.skipwhitespace();
if (!cursor.charCode) return new Expression.Literal(true);
let result: Expression | null = null;
for (;;) {
cursor.skipwhitespace();
// Parse key as a path (supports nested aliases via parsePath).
// Add colon and comma as boundaries so the path stops there.
cursor.descend([CHAR_COLON, CHAR_COMMA], false);
const keyPath = parsePath(cursor);
cursor.ascend();
// After ascend, check for the colon separator
cursor.skipwhitespace();
if ((cursor.charCode as number) !== CHAR_COLON)
throw new Error("Expected ':'");
cursor.step();
// Parse value (reads until boundary , or ])
cursor.descend([CHAR_COMMA], false);
const value = parseOldAliasValue(cursor);
cursor.ascend();
const pair = new Expression.Binary(
"=",
new Expression.Parameter(keyPath),
new Expression.Literal(value),
);
},
WhenPair: function (r) {
return parsimmon.seq(
parsimmon
.regexp(/when/i)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("WHEN")
.then(r.Expression),
parsimmon
.regexp(/then/i)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("THEN")
.then(r.Expression),
);
},
CaseStatement: function (r) {
return parsimmon.seqMap(
parsimmon
.regexp(/case/i)
.result("CASE")
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("CASE"),
r.WhenPair.many(),
parsimmon
.regexp(/else/i)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("ELSE")
.then(r.Expression)
.map((e) => [[true, e]])
.fallback(null)
.skip(
parsimmon
.regex(/end/i)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)),
)
.skip(parsimmon.optWhitespace),
(...arr) => arr.flat(2),
);
},
Value: function (r) {
return parsimmon.alt(
r.NullValue,
r.BooleanValue,
r.NumberValue,
r.StringValueSql,
r.StringValueJs,
r.FuncValue,
r.CaseStatement,
);
},
ValueExpression: function (r) {
return binaryLeft(
parsimmon.string("||").skip(parsimmon.optWhitespace),
binaryLeft(
parsimmon
.alt(parsimmon.string("+"), parsimmon.string("-"))
.skip(parsimmon.optWhitespace),
binaryLeft(
parsimmon
.alt(
parsimmon.string("*"),
parsimmon.string("/"),
parsimmon.string("%"),
)
.skip(parsimmon.optWhitespace),
parsimmon.alt(
r.Value,
r.Parameter,
r.Expression.wrap(
parsimmon.string("(").skip(parsimmon.optWhitespace),
parsimmon.string(")").skip(parsimmon.optWhitespace),
),
),
),
),
);
},
Comparison: function (r) {
return parsimmon.alt(
parsimmon.seqMap(r.ValueExpression, r.IsNullOperator, (p, o) => [o, p]),
parsimmon.seqMap(
r.ValueExpression,
r.ComparisonOperator,
r.ValueExpression,
(p, o, v) => [o, p, v],
),
parsimmon.seqMap(
r.ValueExpression,
r.LikeOperator,
r.ValueExpression.skip(
parsimmon
.regexp(/escape/i)
.result("ESCAPE")
.skip(parsimmon.whitespace)
.desc("ESCAPE"),
),
r.ValueExpression,
(a, b, c, d) => [b, a, c, d],
),
parsimmon.seqMap(
r.ValueExpression,
r.LikeOperator,
r.ValueExpression,
(a, b, c) => [b, a, c],
),
);
},
ExpressionList: function (r) {
return r.Expression.sepBy(
parsimmon.string(",").skip(parsimmon.optWhitespace),
);
},
Expression: function (r) {
function unary(
operatorsParser: parsimmon.Parser<string>,
nextParser: parsimmon.Parser<Expression>,
): parsimmon.Parser<Expression> {
return parsimmon.seq(operatorsParser, nextParser).or(nextParser);
result = result ? new Expression.Binary("AND", result, pair) : pair;
cursor.skipwhitespace();
if ((cursor.charCode as number) !== CHAR_COMMA) break;
cursor.step();
}
return result;
}
export function parsePath(cur: Cursor): Path {
const segments: (string | Expression)[] = [];
let colon = 0;
const cur2 = cur.fork();
for (;;) {
let char = cur2.charCode;
let exp: Expression;
if (char === CHAR_OPEN_BRACKET) {
if (cur2.pos !== cur.pos) throw new Error("Invalid path");
cur2.step();
cur2.descend([CHAR_CLOSE_BRACKET]);
// Try parsing as expression first
const cur3 = cur2.fork();
let err: unknown;
try {
exp = parseExpression(cur3);
} catch (e) {
err = e;
}
// Fall back to old alias format if not a valid expression or produced
// a bare Parameter (old format like [a:1] mis-parsed as a path).
if (err || exp instanceof Expression.Parameter) {
exp = parseOldAlias(cur2);
} else {
cur2.sync(cur3);
}
cur2.ascend();
char = cur2.charCode;
if (char !== CHAR_CLOSE_BRACKET) throw new Error("Expected ']'");
char = cur2.step().charCode;
}
return binaryLeft(
r.OrOperator,
binaryLeft(
r.AndOperator,
unary(r.NotOperator, r.Comparison.or(r.ValueExpression)),
),
).trim(parsimmon.optWhitespace);
},
});
if (!PATH_CHARS.has(char)) {
if (cur2.pos <= cur.pos) throw new Error("Invalid path");
export function parse(str: string): Expression {
if (!str) return null;
return lang.Expression.tryParse(str);
if (char === CHAR_COLON) {
if (colon) throw new Error("Multiple colons");
colon = segments.length + 1;
}
if (exp) segments.push(exp);
else segments.push(cur.read(cur2));
if (char !== CHAR_PERIOD && char !== CHAR_COLON) break;
cur2.step();
cur.sync(cur2);
continue;
}
if (exp) throw new Error("Invalid path");
cur2.step();
}
cur.sync(cur2);
if (colon) colon = segments.length - colon;
return new Path(segments, colon);
}
export function parseList(str: string): Expression[] {
if (!str) return [];
return lang.ExpressionList.tryParse(str);
}
export function stringify(exp: Expression, level = 0): string {
if (!Array.isArray(exp)) return JSON.stringify(exp);
const opLevels = {
OR: 10,
AND: 11,
NOT: 12,
"=": 20,
"<>": 20,
">": 20,
">=": 20,
"<": 20,
"<=": 20,
LIKE: 20,
"NOT LIKE": 20,
"IS NULL": 20,
"IS NOT NULL": 20,
"||": 30,
"+": 31,
"-": 31,
"*": 32,
"/": 32,
"%": 32,
};
const op = exp[0].toUpperCase();
function wrap(e): string {
if (opLevels[op] <= level) return `(${e})`;
export function stringifyExpression(exp: Expression, level = 0): string {
function wrap(e: string, op: string): string {
if (PRECEDENCE[op] <= level) return `(${e})`;
else return e;
}
if (op === "FUNC") {
if (exp instanceof Expression.Literal) {
if (exp.value == null) return "NULL";
if (exp.value === true) return "TRUE";
if (exp.value === false) return "FALSE";
return JSON.stringify(exp.value);
} else if (exp instanceof Expression.Unary) {
if (exp.operator === "NOT") {
return wrap(
`NOT ${stringifyExpression(exp.operand, PRECEDENCE[exp.operator])}`,
"NOT",
);
} else if (exp.operator === "IS NULL" || exp.operator === "IS NOT NULL") {
return wrap(
`${stringifyExpression(exp.operand, PRECEDENCE[exp.operator])} ${exp.operator}`,
exp.operator,
);
}
} else if (exp instanceof Expression.Binary) {
const op = exp.operator;
if (!(op in PRECEDENCE)) throw new Error("Invalid operator");
return wrap(
`${exp[1]}(${exp
.slice(2)
.map((e) => stringify(e))
.join(", ")})`,
`${stringifyExpression(exp.left, PRECEDENCE[op] - 1)} ${exp.operator} ${stringifyExpression(exp.right, PRECEDENCE[op])}`,
op,
);
} else if (op === "PARAM") {
if (typeof exp[1] === "string") {
return wrap(exp[1]);
} else if (Array.isArray(exp[1]) && exp[1][0] === "||") {
return wrap(
exp[1]
.slice(1)
.map((p) => {
if (typeof p === "string") return p;
else return `{${stringify(p)}}`;
})
.join(""),
);
} else {
return wrap(`{${stringify(exp[1])}}`);
}
} else if (op === "IS NULL" || op === "IS NOT NULL") {
return wrap(`${stringify(exp[1], opLevels[op])} ${op}`);
} else if (op === "LIKE" || op === "NOT LIKE") {
if (exp[3]) {
return wrap(
`${stringify(exp[1], opLevels[op])} ${op} ${stringify(
exp[2],
opLevels[op],
)} ESCAPE ${stringify(exp[3], opLevels[op])}`,
);
} else {
return wrap(
`${stringify(exp[1], opLevels[op])} ${op} ${stringify(
exp[2],
opLevels[op],
)}`,
);
}
} else if (op === "CASE") {
const parts: string[] = ["CASE"];
for (let i = 1; i < exp.length - 1; i += 2) {
if (!Array.isArray(exp[i]) && exp[i]) {
if (exp[i + 1] != null) parts.push("ELSE", stringify(exp[i + 1]));
break;
}
parts.push("WHEN", stringify(exp[i]), "THEN", stringify(exp[i + 1]));
}
parts.push("END");
return parts.join(" ");
} else if (op in opLevels) {
const parts = exp.slice(1).map((e, i) => {
return stringify(e, opLevels[exp[0]] + Math.min(i - 1, 0));
});
if (op === "NOT") return wrap(`${op} ${parts[0]}`);
else return wrap(parts.join(` ${op} `));
} else {
throw new Error(`Unrecognized operator ${exp[0]}`);
} else if (exp instanceof Expression.Parameter) {
return exp.path.toString();
} else if (exp instanceof Expression.FunctionCall) {
return `${exp.name}(${exp.args.map((a) => stringifyExpression(a)).join(", ")})`;
} else if (exp instanceof Expression.Conditional) {
let str = `CASE WHEN ${stringifyExpression(exp.condition)} THEN ${stringifyExpression(exp.then)}`;
if (
exp.otherwise instanceof Expression.Literal &&
exp.otherwise.value == null
)
str += " END";
else if (exp.otherwise instanceof Expression.Conditional)
str += stringifyExpression(exp.otherwise).slice(4);
else str += ` ELSE ${stringifyExpression(exp.otherwise)} END`;
return str;
}
throw new Error("Invalid expression");
}
export function parseLikePattern(pat: string, esc: string): string[] {
@@ -457,3 +525,33 @@ export function parseLikePattern(pat: string, esc: string): string[] {
}
return chars.filter((c) => c);
}
export function likePatternToRegExp(pat: string, esc = "", flags = ""): RegExp {
const convChars = {
"-": "\\-",
"/": "\\/",
"\\": "\\/",
"^": "\\^",
$: "\\$",
"*": "\\*",
"+": "\\+",
"?": "\\?",
".": "\\.",
"(": "\\(",
")": "\\)",
"|": "\\|",
"[": "\\[",
"]": "\\]",
"{": "\\{",
"}": "\\}",
"\\%": ".*",
"\\_": ".",
};
let chars = parseLikePattern(pat, esc);
if (!chars.length) return new RegExp("^$", flags);
chars = chars.map((c) => convChars[c] || c);
chars[0] = chars[0] === ".*" ? "" : "^" + chars[0];
const l = chars.length - 1;
chars[l] = [".*", ""].includes(chars[l]) ? "" : chars[l] + "$";
return new RegExp(chars.join(""), flags);
}
+295 -374
View File
@@ -1,7 +1,7 @@
import { espresso, complement, tautology } from "espresso-iisojs";
import { Expression } from "../../types.ts";
import Expression from "../expression.ts";
import { parseLikePattern } from "./parser.ts";
import normalize from "./normalize.ts";
import { map, parseLikePattern } from "./parser.ts";
type Minterm = number[];
@@ -27,7 +27,7 @@ export abstract class SynthContextBase<
return this.clauses.get(v);
}
abstract getMinterms(exp: T, res: true | false | null): Minterm[];
abstract getMinterms(exp: T, res: number): Minterm[];
abstract getDcSet(minterms: Minterm[]): Minterm[];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
canRaise(idx: number, set: Set<number>): boolean {
@@ -67,36 +67,34 @@ export abstract class SynthContextBase<
}
function* findIsNullDeps(exp: Expression): IterableIterator<Expression> {
if (!Array.isArray(exp)) return;
const op = exp[0];
if (op === "IS NULL" || op === "IS NOT NULL") return;
if (op === "FUNC") {
if (exp[1] === "NOW") {
return;
} else if (exp[1] === "LOWER" || exp[1] === "UPPER") {
yield* findIsNullDeps(exp[2]);
} else if (exp[1] === "ROUND") {
for (const e of exp.slice(2, 4)) yield* findIsNullDeps(e);
return;
if (exp instanceof Expression.Literal) return;
else if (exp instanceof Expression.Unary) {
if (exp.operator === "IS NULL" || exp.operator === "IS NOT NULL") return;
yield* findIsNullDeps(exp.operand);
} else if (exp instanceof Expression.Binary) {
yield* findIsNullDeps(exp.left);
yield* findIsNullDeps(exp.right);
} else if (exp instanceof Expression.FunctionCall) {
if (exp.name === "NOW") return;
else if (exp.name === "LOWER" || exp.name === "UPPER")
yield* findIsNullDeps(exp.args[0]);
else if (exp.name === "ROUND") {
for (const e of exp.args.slice(0, 2)) yield* findIsNullDeps(e);
}
} else if (op !== "PARAM") {
for (const e of exp.slice(1)) yield* findIsNullDeps(e);
return;
}
yield exp;
} else yield exp;
}
export abstract class Clause {
private _isNullable: Set<string>;
protected _expression: Expression;
abstract true(context: SynthContextBase<Clause>): Minterm[];
abstract false(context: SynthContextBase<Clause>): Minterm[];
abstract null(context: SynthContextBase<Clause>): Minterm[];
abstract getMinterms(
context: SynthContextBase<Clause>,
res: number,
): Minterm[];
expression(): Expression {
if (this._expression !== undefined) return this._expression;
const context = createSynthContext();
const minterms = this.true(context);
const minterms = this.getMinterms(context, 0b100);
const minimized = context.minimize(minterms);
this._expression = context.toExpression(minimized) as Expression;
return this._expression;
@@ -120,7 +118,7 @@ export abstract class Clause {
}
}
toString(): string {
return JSON.stringify(this.expression());
return this.expression().toString();
}
}
@@ -130,68 +128,11 @@ export namespace Clause {
constructor(public operand: Clause) {
super();
}
true(context: SynthContextBase<Clause>): Minterm[] {
return this.operand.false(context);
}
false(context: SynthContextBase<Clause>): Minterm[] {
return this.operand.true(context);
}
null(context: SynthContextBase<Clause>): Minterm[] {
return this.operand.null(context);
}
}
export class Or extends Clause {
constructor(public operands: Clause[]) {
super();
}
true(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[] = [];
for (const s of this.operands) {
const m = s.true(context);
if (m.length === 1 && !m[0].length) return [[]];
minterms.push(...m);
}
return minterms;
}
false(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[][] = [];
const operands = [...this.operands];
for (const e of operands) {
if (e instanceof Or) {
operands.push(...e.operands);
continue;
}
const m = e.false(context);
if (!m.length) return [];
minterms.push(m);
}
if (!minterms.length) return [[]];
if (minterms.length === 1) return minterms[0];
return complement(minterms.map((m) => complement(m)).flat());
}
null(context: SynthContextBase<Clause>): Minterm[] {
const trueMinterms: Minterm[] = [];
const operands = [...this.operands];
for (const s of operands) {
if (s instanceof Or) {
operands.push(...s.operands);
continue;
}
const t = s.true(context);
if (t.length === 1 && !t[0].length) return [];
trueMinterms.push(...t);
}
const nullMinterms: Minterm[] = [];
for (const s of operands) {
const n = s.null(context);
if (n.length === 1 && !n[0].length && !trueMinterms.length) return [[]];
nullMinterms.push(...n);
}
if (!trueMinterms.length) return nullMinterms;
return complement([...complement(nullMinterms), ...trueMinterms]);
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
let r = res & 0b001;
if (res & 0b010) r |= 0b100;
if (res & 0b100) r |= 0b010;
return this.operand.getMinterms(context, r);
}
}
@@ -199,175 +140,43 @@ export namespace Clause {
constructor(public operands: Clause[]) {
super();
}
true(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[][] = [];
const operands = [...this.operands];
for (const s of operands) {
if (s instanceof And) {
operands.push(...s.operands);
continue;
}
const m = s.true(context);
if (!m.length) return [];
if (m.length === 1 && !m[0].length) continue;
minterms.push(m);
}
if (!minterms.length) return [[]];
if (minterms.length === 1) return minterms[0];
return complement(minterms.map((m) => complement(m)).flat());
}
false(context: SynthContextBase<Clause>): Minterm[] {
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
res = res & 0b111;
if (!res) return [];
if (res === 0b111) return [[]];
if (res === 0b110)
return [
...this.getMinterms(context, 0b010),
...this.getMinterms(context, 0b100),
];
if (!(res & 0b010)) return complement(this.getMinterms(context, ~res));
const minterms: Minterm[] = [];
for (const s of this.operands) {
const m = s.false(context);
for (const o of this.operands) {
const m = o.getMinterms(context, res);
if (m.length === 1 && !m[0].length) return [[]];
minterms.push(...m);
}
return minterms;
}
null(context: SynthContextBase<Clause>): Minterm[] {
const falseMinterms: Minterm[] = [];
const operands = [...this.operands];
for (const s of operands) {
if (s instanceof And) {
operands.push(...s.operands);
continue;
}
const f = s.false(context);
if (f.length === 1 && !f[0].length) return [];
falseMinterms.push(...f);
}
const nullMinterms: Minterm[] = [];
for (const s of operands) {
const n = s.null(context);
if (n.length === 1 && !n[0].length && !falseMinterms.length)
return [[]];
nullMinterms.push(...n);
}
if (!falseMinterms.length) return nullMinterms;
return complement([...complement(nullMinterms), ...falseMinterms]);
}
}
export class Case extends Clause {
constructor(public clauses: Clause[]) {
super();
}
true(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[] = [];
const cumulative: Minterm[] = [];
for (let i = 0; i < this.clauses.length; i += 2) {
const w = this.clauses[i].true(context);
const t = this.clauses[i + 1].true(context);
minterms.push(
...complement([...cumulative, ...complement(w), ...complement(t)]),
);
if (i < this.clauses.length - 2) {
cumulative.push(
...complement([
...this.clauses[i].false(context),
...this.clauses[i].null(context),
]),
);
}
}
return minterms;
}
false(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[] = [];
const cumulative: Minterm[] = [];
for (let i = 0; i < this.clauses.length; i += 2) {
const w = this.clauses[i].true(context);
const t = this.clauses[i + 1].false(context);
minterms.push(
...complement([...cumulative, ...complement(w), ...complement(t)]),
);
if (i < this.clauses.length - 2) {
cumulative.push(
...complement([
...this.clauses[i].false(context),
...this.clauses[i].null(context),
]),
);
}
}
return minterms;
}
null(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[] = [];
const cumulative: Minterm[] = [];
for (let i = 0; i < this.clauses.length; i += 2) {
const w = this.clauses[i].true(context);
const t = this.clauses[i + 1].null(context);
minterms.push(
...complement([...cumulative, ...complement(w), ...complement(t)]),
);
cumulative.push(
...complement([
...this.clauses[i].false(context),
...this.clauses[i].null(context),
]),
);
}
minterms.push(...complement([...cumulative]));
return minterms;
}
expression(): Expression {
if (this._expression != null) return this._expression;
if (this.isBoolean()) {
this._expression = new Clause.Not(new Clause.Not(this)).expression();
return this._expression;
}
const context = createSynthContext();
const cases: { when: Minterm[]; then: Expression }[] = [];
for (let i = 0; i < this.clauses.length; i += 2) {
let minterms = this.clauses[i].true(context);
const then = this.clauses[i + 1].expression();
if (
cases.length &&
JSON.stringify(then) === JSON.stringify(cases[cases.length - 1].then)
)
minterms.push(...cases.pop().when);
minterms = context.minimize(
minterms,
cases.flatMap((c) => c.when),
);
if (!minterms.length) continue;
cases.push({ when: minterms, then });
if (minterms.length === 1 && !minterms[0].length) break;
}
while (cases[cases.length - 1].then == null) cases.pop();
if (!cases.length) return null;
this._expression = [
"CASE",
...cases.flatMap((c) => [context.toExpression(c.when), c.then]),
];
return this._expression;
}
isBoolean(): boolean {
for (let i = 1; i < this.clauses.length; i += 2)
if (!this.clauses[i].isBoolean()) return false;
return true;
}
}
export class IsNull extends Clause {
private _boolean: Clause;
constructor(public operand: Clause) {
super();
this._boolean = new Not(new Not(this));
}
true(context: SynthContextBase<Clause>): Minterm[] {
return this.operand.null(context);
}
false(context: SynthContextBase<Clause>): Minterm[] {
return complement(this.true(context));
}
null(): Minterm[] {
return [];
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
res = res & 0b111;
const minterms: Minterm[] = [];
if ((res & 0b110) === 0b110) return [[]];
if (res & 0b100)
minterms.push(...this.operand.getMinterms(context, 0b001));
if (res & 0b010)
minterms.push(...this.operand.getMinterms(context, 0b110));
return minterms;
}
isBoolean(): boolean {
return true;
@@ -378,7 +187,7 @@ export namespace Clause {
expression(): Expression {
const nullables = [...this.operand.getNullables()];
if (nullables.length === 1 && nullables[0].operand === this.operand)
return ["IS NULL", this.operand.expression()];
return new Expression.Unary("IS NULL", this.operand.expression());
return super.expression();
}
}
@@ -387,26 +196,26 @@ export namespace Clause {
constructor(public exp: Expression) {
super();
}
true(context: SynthContextBase<Clause>): Minterm[] {
if (Array.isArray(this.exp)) return context.getMinterms(this, true);
if (this.exp) return [[]];
return [];
}
false(context: SynthContextBase<Clause>): Minterm[] {
if (Array.isArray(this.exp)) return context.getMinterms(this, false);
if (!this.exp && this.exp != null) return [[]];
return [];
}
null(context: SynthContextBase<Clause>): Minterm[] {
if (Array.isArray(this.exp)) return context.getMinterms(this, null);
if (this.exp == null) return [[]];
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
if (!(this.exp instanceof Expression.Literal))
return context.getMinterms(this, res);
if (this.exp.value == null) return res & 0b001 ? [[]] : [];
if (this.exp.value && res & 0b100) return [[]];
if (!this.exp.value && res & 0b010) return [[]];
return [];
}
expression(): Expression {
return this.exp;
}
isBoolean(): boolean {
return this.exp === true || this.exp === false || this.exp == null;
if (this.exp instanceof Expression.Literal) {
return (
this.exp.value === true ||
this.exp.value === false ||
this.exp.value == null
);
}
return false;
}
}
@@ -418,14 +227,13 @@ export namespace Clause {
) {
super();
}
true(context: SynthContextBase<Clause>): Minterm[] {
return context.getMinterms(this, true);
}
false(context: SynthContextBase<Clause>): Minterm[] {
return context.getMinterms(this, false);
}
null(context: SynthContextBase<Clause>): Minterm[] {
return this.lhs.null(context);
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
res = res & 0b111;
if (!res) return [];
if (res === 0b111) return [[]];
if (res === 0b001 || res === 0b110)
return this.lhs.getMinterms(context, res);
return context.getMinterms(this, res);
}
*getNullables(): IterableIterator<IsNull> {
yield* this.lhs.getNullables();
@@ -434,7 +242,11 @@ export namespace Clause {
return true;
}
expression(): Expression {
return [this.op, this.lhs.expression(), this.rhs];
return new Expression.Binary(
this.op,
this.lhs.expression(),
new Expression.Literal(this.rhs),
);
}
}
@@ -453,15 +265,13 @@ export namespace Clause {
const exp = lhs.expression();
let caseSensitive = true;
let contradiction = false;
if (Array.isArray(exp)) {
if (exp[0] === "FUNC") {
const func = exp[1];
if (func === "UPPER" || func === "LOWER") {
const p = func === "UPPER" ? rhs.toUpperCase() : rhs.toLowerCase();
if (p === rhs) caseSensitive = false;
else contradiction = true;
lhs = new Exp(exp[2]);
}
if (exp instanceof Expression.FunctionCall) {
if (exp.name === "UPPER" || exp.name === "LOWER") {
const p =
exp.name === "UPPER" ? rhs.toUpperCase() : rhs.toLowerCase();
if (p === rhs) caseSensitive = false;
else contradiction = true;
lhs = new Exp(exp.args[0]);
}
}
this.lhs = lhs;
@@ -469,14 +279,13 @@ export namespace Clause {
this.caseSensitive = caseSensitive;
this.contradiction = contradiction;
}
true(context: SynthContextBase<Clause>): Minterm[] {
return context.getMinterms(this, true);
}
false(context: SynthContextBase<Clause>): Minterm[] {
return context.getMinterms(this, false);
}
null(context: SynthContextBase<Clause>): Minterm[] {
return this.lhs.null(context);
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
res = res & 0b111;
if (!res) return [];
if (res === 0b111) return [[]];
if (res === 0b001 || res === 0b110)
return this.lhs.getMinterms(context, res);
return context.getMinterms(this, res);
}
isBoolean(): boolean {
return true;
@@ -491,67 +300,157 @@ export namespace Clause {
let lhs = this.lhs.expression();
if (this.contradiction) {
if (this.rhs === this.rhs.toLocaleUpperCase())
lhs = ["FUNC", "LOWER", lhs];
else lhs = ["FUNC", "UPPER", lhs];
lhs = new Expression.FunctionCall("LOWER", [lhs]);
else lhs = new Expression.FunctionCall("UPPER", [lhs]);
} else if (!this.caseSensitive) {
if (this.rhs === this.rhs.toLocaleUpperCase())
lhs = ["FUNC", "UPPER", lhs];
else lhs = ["FUNC", "LOWER", lhs];
lhs = new Expression.FunctionCall("UPPER", [lhs]);
else lhs = new Expression.FunctionCall("LOWER", [lhs]);
}
return ["LIKE", lhs, this.rhs, ...(this.esc ? [this.esc] : [])];
return new Expression.Binary(
"LIKE",
lhs,
new Expression.Literal(this.rhs),
);
}
}
export class Conditional extends Clause {
constructor(
public condition: Clause,
public then: Clause,
public otherwise: Clause,
) {
super();
}
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
const condition = this.condition.getMinterms(context, 0b011);
if (!condition.length) return this.then.getMinterms(context, res);
return [
...complement([...condition, ...this.then.getMinterms(context, ~res)]),
...complement([
...complement(condition),
...this.otherwise.getMinterms(context, ~res),
]),
];
}
expression(): Expression {
if (this._expression != null) return this._expression;
if (this.isBoolean()) {
this._expression = new Clause.Not(new Clause.Not(this)).expression();
return this._expression;
}
const context = createSynthContext();
const cases: { when: Minterm[]; then: Expression }[] = [];
let clause = this as Conditional;
for (;;) {
let minterms = clause.condition.getMinterms(context, 0b100);
const then = clause.then.expression();
if (
cases.length &&
then.toString() === cases[cases.length - 1].then.toString()
)
minterms.push(...cases.pop().when);
minterms = context.minimize(
minterms,
cases.flatMap((c) => c.when),
);
if (!minterms.length) continue;
cases.push({ when: minterms, then });
if (minterms.length === 1 && !minterms[0].length) break;
if (clause.otherwise instanceof Conditional) clause = clause.otherwise;
else
clause = new Conditional(
new Exp(new Expression.Literal(true)),
clause.otherwise,
new Exp(new Expression.Literal(null)),
);
}
while (
cases[cases.length - 1].then instanceof Expression.Literal &&
(cases[cases.length - 1].then as Expression.Literal).value == null
)
cases.pop();
let res: Expression = new Expression.Literal(null);
while (cases.length) {
const c = cases.pop();
if (c.when.length === 1 && !c.when[0].length) {
res = c.then;
} else {
res = new Expression.Conditional(
context.toExpression(c.when),
c.then,
res,
);
}
}
this._expression = res;
return this._expression;
}
isBoolean(): boolean {
return this.then.isBoolean() && this.otherwise.isBoolean();
}
}
export function fromExpression(exp: Expression): Clause {
const res = map(exp, (e) => {
if (!Array.isArray(e)) return new Clause.Exp(e);
let op = e[0];
let negate = true;
let res: Clause;
let negate = false;
if (exp instanceof Expression.Unary) {
let op = exp.operator;
negate = true;
if (op === "IS NOT NULL") op = "IS NULL";
else negate = false;
if (op === "NOT") res = new Clause.Not(fromExpression(exp.operand));
else if (op === "IS NULL") res = new IsNull(fromExpression(exp.operand));
} else if (exp instanceof Expression.Binary) {
let op = exp.operator;
negate = true;
if (op === "NOT LIKE") op = "LIKE";
else if (op === "IS NOT NULL") op = "IS NULL";
else if (op === "<>") op = "=";
else if (op === ">=") op = "<";
else if (op === "<=") op = ">";
else negate = false;
let clause: Clause;
if (op === "IS NULL") {
clause = new Clause.IsNull(e[1]);
} else if (op === "NOT") {
clause = new Clause.Not(e[1]);
if (op === "AND") {
res = new Clause.And([
fromExpression(exp.left),
fromExpression(exp.right),
]);
} else if (op === "OR") {
clause = new Clause.Or(e.slice(1));
} else if (op === "AND") {
clause = new Clause.And(e.slice(1));
} else if (op === "CASE") {
clause = new Clause.Case(e.slice(1));
} else if (op === "LIKE") {
const rhs = e[2] instanceof Exp ? e[2].expression() : null;
const esc = e[3] instanceof Exp ? e[3].expression() : null;
if (typeof rhs === "string" && typeof esc === "string")
clause = new Clause.Like(e[1], rhs, esc);
else if (typeof rhs === "string" && esc == null)
clause = new Clause.Like(e[1], rhs);
} else if (op === ">" || op === "<" || op === "=") {
const rhs = e[2] instanceof Exp ? e[2].expression() : null;
if (["boolean", "number", "string"].includes(typeof rhs)) {
clause = new Clause.Compare(
e[1],
op,
rhs as boolean | number | string,
);
negate = true;
res = new Clause.And([
new Clause.Not(fromExpression(exp.left)),
new Clause.Not(fromExpression(exp.right)),
]);
} else if (exp.right instanceof Expression.Literal) {
if (["=", ">", "<"].includes(op)) {
if (
["boolean", "number", "string"].includes(typeof exp.right.value)
) {
res = new Compare(
fromExpression(exp.left),
op as ">" | "<" | "=",
exp.right.value,
);
}
} else if (op === "LIKE") {
if (typeof exp.right.value === "string")
res = new Like(fromExpression(exp.left), exp.right.value);
}
}
} else if (exp instanceof Expression.Conditional) {
res = new Conditional(
fromExpression(exp.condition),
fromExpression(exp.then),
fromExpression(exp.otherwise),
);
}
if (!clause) {
const args = e.slice(1).map((a) => (a as Clause).expression());
clause = new Clause.Exp([op, ...args]);
}
if (negate) clause = new Clause.Not(clause);
return clause;
});
if (!res) res = new Exp(exp);
if (negate) res = new Not(res);
return res;
}
}
@@ -576,11 +475,24 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
super();
}
getMinterms(clause: Clause, res: true | false | null): number[][] {
getMinterms(clause: Clause, res: number): number[][] {
const v = this.getVar(clause);
if (res === true) return [[(v << 2) ^ 3]];
else if (res === false) return [[(v << 2) ^ 1]];
else return [[v << 2, (v << 2) ^ 2]];
switch (res & 0b111) {
case 0b100:
return [[(v << 2) ^ 3]];
case 0b010:
return [[(v << 2) ^ 1]];
case 0b001:
return [[v << 2, (v << 2) ^ 2]];
case 0b110:
return [[(v << 2) ^ 3], [(v << 2) ^ 1]];
case 0b101:
return [[(v << 2) ^ 3], [v << 2, (v << 2) ^ 2]];
case 0b011:
return [[(v << 2) ^ 1], [v << 2, (v << 2) ^ 2]];
default:
throw new Error("Invalid minterms");
}
}
getDcSet(minterms: Minterm[]): number[][] {
@@ -594,9 +506,7 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
) as Clause.Compare[];
// Comparisons
for (const [, clauses] of groupBy(comparisons, (c) =>
JSON.stringify(c.lhs),
)) {
for (const [, clauses] of groupBy(comparisons, (c) => c.lhs.toString())) {
const lhs = clauses[0].lhs;
const values = new Set(clauses.map((c) => c.rhs));
const valuesSorted = [...values].sort((a, b) => {
@@ -657,7 +567,7 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
(c) => c instanceof Clause.Like,
) as Clause.Like[];
for (const [, clauses] of groupBy(likes, (c) => JSON.stringify(c.lhs))) {
for (const [, clauses] of groupBy(likes, (c) => c.lhs.toString())) {
for (let i1 = 0; i1 < clauses.length; ++i1) {
const l1 = clauses[i1];
if (l1.contradiction) {
@@ -724,9 +634,13 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
}
if (clause instanceof Clause.IsNull) {
const str = clause.operand.toString();
if (str === '["PARAM","DeviceID.ID"]' || str === '["PARAM","_id"]')
dcSet.push([(v << 2) ^ 3]);
if (clause.operand instanceof Clause.Exp) {
if (clause.operand.exp instanceof Expression.Parameter) {
const str = clause.operand.exp.path.toString();
if (str === "DeviceID.ID" || str === "_id")
dcSet.push([(v << 2) ^ 3]);
}
}
}
}
return dcSet.filter((m) => m.every((v) => whitelist.has(v >> 2)));
@@ -808,11 +722,9 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
}
toExpression(sop: number[][]): Expression {
if (!sop.length) return false;
const res: Expression[] = [];
let res: Expression = new Expression.Literal(false);
for (const s of sop) {
if (!s.length) return true;
const conjs: Expression[] = [];
let conjs: Expression = new Expression.Literal(true);
for (const i of s) {
const clause = this.getClause(i >>> 2);
if (!clause) throw new Error("Invalid literal");
@@ -823,7 +735,8 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
const isNullVars = [...clause.getNullables()].map((c) =>
this.getVar(c),
);
conjs.push(
conjs = Expression.and(
conjs,
this.toExpression([
[i ^ 3],
...isNullVars.map((n) => [(n << 2) ^ 3]),
@@ -832,31 +745,38 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
continue;
}
let expr = clause.expression() as Expression;
if (!(i & 1) !== !(i & 2)) expr = ["NOT", expr];
if (
Array.isArray(expr) &&
expr[0] === "NOT" &&
Array.isArray(expr[1])
) {
const e: Expression[] = expr[1];
if (e[0] === "IS NULL") expr = ["IS NOT NULL", ...e.slice(1)];
else if (e[0] === "LIKE") expr = ["NOT LIKE", ...e.slice(1)];
else if (e[0] === "=") expr = ["<>", ...e.slice(1)];
else if (e[0] === "<>") expr = ["=", ...e.slice(1)];
else if (e[0] === ">") expr = ["<=", ...e.slice(1)];
else if (e[0] === ">=") expr = ["<", ...e.slice(1)];
else if (e[0] === "<") expr = [">=", ...e.slice(1)];
else if (e[0] === "<=") expr = [">", ...e.slice(1)];
else if (e[0] === "NOT") expr = e[1];
let expr = clause.expression();
if (!(i & 1) !== !(i & 2)) expr = new Expression.Unary("NOT", expr);
if (expr instanceof Expression.Unary && expr.operator === "NOT") {
const e = expr.operand;
if (e instanceof Expression.Unary) {
if (e.operator === "IS NULL")
expr = new Expression.Unary("IS NOT NULL", e.operand);
else if (e.operator === "IS NOT NULL")
expr = new Expression.Unary("IS NULL", e.operand);
else if (e.operator === "NOT") expr = e.operand;
} else if (e instanceof Expression.Binary) {
if (e.operator === "LIKE")
expr = new Expression.Binary("NOT LIKE", e.left, e.right);
else if (e.operator === "=")
expr = new Expression.Binary("<>", e.left, e.right);
else if (e.operator === "<>")
expr = new Expression.Binary("=", e.left, e.right);
else if (e.operator === ">")
expr = new Expression.Binary("<=", e.left, e.right);
else if (e.operator === ">=")
expr = new Expression.Binary("<", e.left, e.right);
else if (e.operator === "<")
expr = new Expression.Binary(">=", e.left, e.right);
else if (e.operator === "<=")
expr = new Expression.Binary(">", e.left, e.right);
}
}
conjs.push(expr);
conjs = Expression.and(conjs, expr);
}
if (conjs.length > 1) res.push(["AND", ...conjs]);
else res.push(conjs[0]);
res = Expression.or(res, conjs);
}
if (res.length > 1) return ["OR", ...res];
return res[0];
return res;
}
}
@@ -925,11 +845,12 @@ export function unionDiff(
): [Expression, Expression] {
expr2 = normalize(expr2);
if (!expr2) return [expr1, false];
if (expr2 instanceof Expression.Literal && !expr2.value)
return [expr1, new Expression.Literal(false)];
const synth2 = Clause.fromExpression(expr2);
if (!expr1) {
if (expr1 instanceof Expression.Literal && !expr1.value) {
const e = synth2.expression();
return [e, e];
}
@@ -939,8 +860,8 @@ export function unionDiff(
const context = new SynthContext();
const expr2Minterms = synth2.true(context);
const expr1Minterms = synth1.true(context);
const expr2Minterms = synth2.getMinterms(context, 0b100);
const expr1Minterms = synth1.getMinterms(context, 0b100);
const union = context.minimize([...expr1Minterms, ...expr2Minterms]);
@@ -953,16 +874,16 @@ export function unionDiff(
export function covers(expr1: Expression, expr2: Expression): boolean {
expr2 = normalize(expr2);
if (!expr2) return true;
if (expr2 instanceof Expression.Literal && !expr2.value) return true;
expr1 = normalize(expr1);
if (!Array.isArray(expr1) && expr1) return true;
if (expr1 instanceof Expression.Literal && expr1.value) return true;
const synt1 = Clause.fromExpression(expr1);
const synt2 = Clause.fromExpression(expr2);
const context = new SynthContext();
const expr1Minterms = synt1.true(context);
const expr2Minterms = synt2.true(context);
const expr1Minterms = synt1.getMinterms(context, 0b100);
const expr2Minterms = synt2.getMinterms(context, 0b100);
return tautology([
...context.sanitizeMinterms(complement(expr2Minterms)),
-393
View File
@@ -1,393 +0,0 @@
import { map, mapAsync, parseLikePattern } from "./parser.ts";
import { Expression } from "../../types.ts";
const isArray = Array.isArray;
const regExpCache = new WeakMap<any, RegExp>();
const REDUCE_SKIP = {};
function reduce(exp, callback): Expression {
let loop = true;
while (loop) {
loop = false;
for (let i = 2; i < exp.length; ++i) {
const res = callback(exp[i - 1], exp[i], i - 2);
if (res !== REDUCE_SKIP) {
loop = true;
exp = exp.slice();
exp.splice(i - 1, 2, res);
}
}
}
if (exp.length === 2) return exp[1];
return exp;
}
export function likePatternToRegExp(pat: string, esc = "", flags = ""): RegExp {
const convChars = {
"-": "\\-",
"/": "\\/",
"\\": "\\/",
"^": "\\^",
$: "\\$",
"*": "\\*",
"+": "\\+",
"?": "\\?",
".": "\\.",
"(": "\\(",
")": "\\)",
"|": "\\|",
"[": "\\[",
"]": "\\]",
"{": "\\{",
"}": "\\}",
"\\%": ".*",
"\\_": ".",
};
let chars = parseLikePattern(pat, esc);
if (!chars.length) return new RegExp("^$", flags);
chars = chars.map((c) => convChars[c] || c);
chars[0] = chars[0] === ".*" ? "" : "^" + chars[0];
const l = chars.length - 1;
chars[l] = [".*", ""].includes(chars[l]) ? "" : chars[l] + "$";
return new RegExp(chars.join(""), flags);
}
function compare(
a: boolean | number | string,
b: boolean | number | string,
): number {
if (typeof a === "boolean") a = +a;
if (typeof b === "boolean") b = +b;
if (typeof a !== typeof b) return typeof a === "string" ? 1 : -1;
return a > b ? 1 : a < b ? -1 : 0;
}
function toNumber(a: boolean | number | string): number {
switch (typeof a) {
case "number":
return a;
case "boolean":
return +a;
case "string":
return parseFloat(a) || 0;
}
}
function toString(a: boolean | number | string): string {
switch (typeof a) {
case "string":
return a;
case "number":
return a.toString();
case "boolean":
return (+a).toString();
}
}
export function evaluateCallback(exp: Expression): Expression {
if (!Array.isArray(exp)) return exp;
if (exp[0] === "CASE") {
for (let i = 1; i < exp.length; i += 2) {
if (Array.isArray(exp[i])) return exp;
if (exp[i]) return exp[i + 1];
}
return null;
} else if (exp[0] === "FUNC") {
if (exp[1] === "COALESCE") {
const args: Expression[] = [];
for (let i = 2; i < exp.length; ++i) {
const e = exp[i];
if (e == null) continue;
args.push(e);
if (!Array.isArray(e)) break;
}
if (!args.length) return null;
if (args.length === 1) return args[0];
return ["FUNC", "COALESCE", ...args];
} else if (exp[1] === "UPPER") {
if (exp[2] == null) return null;
if (!isArray(exp[2])) return toString(exp[2]).toUpperCase();
} else if (exp[1] === "LOWER") {
if (exp[2] == null) return null;
if (!isArray(exp[2])) return toString(exp[2]).toLowerCase();
} else if (exp[1] === "ROUND") {
const n = exp[2];
const p = exp.length > 3 ? exp[3] : 0;
if (n == null || p == null) return null;
if (!isArray(n) && !isArray(p)) {
const d = 10 ** p;
const m = n * d * (1 + Number.EPSILON);
return Math.round(m) / d;
}
}
} else if (exp[0] === "PARAM") {
if (exp[1] == null) return null;
} else if (exp[0] === "AND") {
for (let i = 1; i < exp.length; ++i)
if (!Array.isArray(exp[i]) && exp[i] != null && !exp[i]) return false;
const args: Expression[] = [];
for (let i = 1; i < exp.length; ++i) {
const ee = exp[i];
if (ee == null) return null;
if (Array.isArray(ee)) {
if (ee[0] === "AND") args.push(...ee.slice(1));
else args.push(ee);
}
}
if (!args.length) return true;
if (args.length === 1) args.push(true);
return ["AND", ...args];
} else if (exp[0] === "OR") {
const args: Expression[] = [];
for (let i = 1; i < exp.length; ++i) {
const ee = exp[i];
if (Array.isArray(ee)) {
if (ee[0] === "OR") args.push(...ee.slice(1));
else args.push(ee);
} else if (ee) {
return true;
}
}
if (!args.length) return exp.some((ee) => ee == null) ? null : false;
if (args.length === 1) args.push(false);
return ["OR", ...args];
} else if (exp[0] === "NOT") {
if (exp[1] == null) return null;
if (!isArray(exp[1])) return !exp[1];
else if (exp[1][0] === "NOT") return exp[1][1];
} else if (exp[0] === "IS NULL") {
if (isArray(exp[1])) return exp;
else if (exp[1] == null) return true;
else return false;
} else if (exp[0] === "IS NOT NULL") {
if (isArray(exp[1])) return exp;
else if (exp[1] != null) return true;
else return false;
} else if (exp[0] === "LIKE") {
if (isArray(exp[1]) || isArray(exp[2]) || isArray(exp[3])) return exp;
else if (
exp[1] == null ||
exp[2] == null ||
((exp as any[]).length >= 4 && exp[3] == null)
)
return null;
let r = regExpCache.get(exp as any[]);
if (!r) {
r = likePatternToRegExp(exp[2], exp[3]);
regExpCache.set(exp as any[], r);
}
return r.test(exp[1]);
} else if (exp[0] === "NOT LIKE") {
if (isArray(exp[1]) || isArray(exp[2]) || isArray(exp[3])) return exp;
else if (
exp[1] == null ||
exp[2] == null ||
((exp as any[]).length >= 4 && exp[3] == null)
)
return null;
let r = regExpCache.get(exp as any[]);
if (!r) {
r = likePatternToRegExp(exp[2], exp[3]);
regExpCache.set(exp as any[], r);
}
return !r.test(exp[1]);
} else if (exp[0] === "=") {
if (exp[1] == null || exp[2] == null) return null;
if (isArray(exp[1]) || isArray(exp[2])) return exp;
return compare(exp[1], exp[2]) === 0;
} else if (exp[0] === "<>") {
if (exp[1] == null || exp[2] == null) return null;
if (isArray(exp[1]) || isArray(exp[2])) return exp;
return compare(exp[1], exp[2]) !== 0;
} else if (exp[0] === ">") {
if (exp[1] == null || exp[2] == null) return null;
if (isArray(exp[1]) || isArray(exp[2])) return exp;
return compare(exp[1], exp[2]) > 0;
} else if (exp[0] === ">=") {
if (exp[1] == null || exp[2] == null) return null;
if (isArray(exp[1]) || isArray(exp[2])) return exp;
return compare(exp[1], exp[2]) >= 0;
} else if (exp[0] === "<") {
if (exp[1] == null || exp[2] == null) return null;
if (isArray(exp[1]) || isArray(exp[2])) return exp;
return compare(exp[1], exp[2]) < 0;
} else if (exp[0] === "<=") {
if (exp[1] == null || exp[2] == null) return null;
if (isArray(exp[1]) || isArray(exp[2])) return exp;
return compare(exp[1], exp[2]) <= 0;
} else if (exp[0] === "*") {
return reduce(exp, (a, b) => {
if (a == null || b == null) return null;
if (!isArray(a) && !isArray(b)) return toNumber(a) * toNumber(b);
return REDUCE_SKIP;
});
} else if (exp[0] === "/") {
return reduce(exp, (a, b, i) => {
if (a == null || b == null) return null;
if (isArray(a) || isArray(b)) return REDUCE_SKIP;
const n1 = toNumber(a);
const n2 = toNumber(b);
if (i !== 0) return n1 * n2;
if (n2 === 0) return null;
return n1 / n2;
});
} else if (exp[0] === "+") {
return reduce(exp, (a, b) => {
if (a == null || b == null) return null;
if (!isArray(a) && !isArray(b)) return toNumber(a) + toNumber(b);
return REDUCE_SKIP;
});
} else if (exp[0] === "-") {
return reduce(exp, (a, b, i) => {
if (a == null || b == null) return null;
if (!isArray(a) && !isArray(b))
return i === 0 ? toNumber(a) - toNumber(b) : toNumber(a) + toNumber(b);
return REDUCE_SKIP;
});
} else if (exp[0] === "%") {
return reduce(exp, (a, b, i) => {
if (a == null || b == null) return null;
if (isArray(a) || isArray(b) || i !== 0) return REDUCE_SKIP;
const n1 = toNumber(a);
const n2 = Math.trunc(toNumber(b));
if (n2 === 0) return null;
return n1 % n2;
});
} else if (exp[0] === "||") {
return reduce(exp, (a, b) => {
if (a == null || b == null) return null;
if (!isArray(a) && !isArray(b)) return toString(a) + toString(b);
return REDUCE_SKIP;
});
}
return exp;
}
export function evaluate(
exp: Expression,
obj: Record<string, unknown> | ((e: string) => Expression),
now: number,
cb?: (e: Expression) => Expression,
): string | number | boolean | null;
export function evaluate(
exp: Expression,
obj?: Record<string, unknown> | ((e: string) => Expression),
now?: number,
cb?: (e: Expression) => Expression,
): Expression;
export function evaluate(
exp: Expression,
obj?: Record<string, unknown> | ((e: string) => Expression),
now?: number,
cb?: (e: Expression) => Expression,
): Expression {
return map(exp, (e) => {
if (cb) e = cb(e);
if (!isArray(e)) return e;
if (e[0] === "FUNC" && e[1] === "NOW") {
if (now) return now;
} else if (e[0] === "PARAM") {
if (e[1] == null) return null;
if (obj && !isArray(e[1])) {
let v;
if (typeof obj === "function") v = obj(e[1]);
else v = obj[e[1]];
if (v == null) return null;
if (typeof v === "object") v = v.value ? v.value[0] : null;
return v;
}
}
return evaluateCallback(e);
});
}
export async function evaluateAsync(
exp: Expression,
obj: Record<string, unknown>,
now: number,
cb?: (e: Expression) => Promise<Expression>,
): Promise<string | number | boolean | null>;
export async function evaluateAsync(
exp: Expression,
obj?: Record<string, unknown>,
now?: number,
cb?: (e: Expression) => Promise<Expression>,
): Promise<Expression>;
export async function evaluateAsync(
exp: Expression,
obj?: Record<string, unknown>,
now?: number,
cb?: (e: Expression) => Promise<Expression>,
): Promise<Expression> {
return mapAsync(exp, async (e) => {
if (cb) e = await cb(e);
if (!isArray(e)) return e;
if (e[0] === "FUNC") {
if (e[1] === "NOW") if (now) return now;
} else if (e[0] === "PARAM") {
if (e[1] == null) return null;
if (obj && !isArray(e[1])) {
let v = obj[e[1]];
if (v == null) return null;
if (typeof v === "object") v = v["value"] ? v["value"][0] : null;
return v as Expression;
}
}
return evaluateCallback(e);
});
}
export function and(exp1: Expression, exp2: Expression): Expression {
if (exp1 != null && !exp1) return false;
if (exp2 != null && !exp2) return false;
if (!Array.isArray(exp1) && !Array.isArray(exp2)) {
if (exp1 == null || exp2 == null) return null;
return true;
}
if (!Array.isArray(exp2) && exp1[0] === "AND") return exp1;
if (!Array.isArray(exp1) && exp2[0] === "AND") return exp2;
const res: Expression = ["AND"];
if (Array.isArray(exp1) && exp1[0] === "AND") res.push(...exp1.slice(1));
else res.push(exp1);
if (Array.isArray(exp2) && exp2[0] === "AND") res.push(...exp2.slice(1));
else res.push(exp2);
return res;
}
export function or(exp1: Expression, exp2: Expression): Expression {
if (!Array.isArray(exp1) && exp1) return true;
if (!Array.isArray(exp2) && exp2) return true;
if (!Array.isArray(exp1) && !Array.isArray(exp2)) {
if (exp1 == null || exp2 == null) return null;
return false;
}
if (!Array.isArray(exp2) && exp1[0] === "OR") return exp1;
if (!Array.isArray(exp1) && exp2[0] === "OR") return exp2;
const res: Expression = ["OR"];
if (Array.isArray(exp1) && exp1[0] === "OR") res.push(...exp1.slice(1));
else res.push(exp1);
if (Array.isArray(exp2) && exp2[0] === "OR") res.push(...exp2.slice(1));
else res.push(exp2);
return res;
}
export function extractParams(exp: Expression): Expression[] {
const params = new Set<Expression>();
map(exp, (e) => {
if (isArray(e) && e[0] === "PARAM") params.add(e[1]);
return e;
});
return Array.from(params);
}
+69 -65
View File
@@ -1,40 +1,32 @@
import Path from "./path.ts";
export default class PathSet {
declare private lengthIndex: Set<Path>[];
declare private fragmentIndex: Map<string, Set<Path>>[];
declare private stringIndex: Map<string, Path>;
private paramSegmentIndex: Map<string, Set<Path>>[] = [];
private attrSegmentIndex: Map<string, Set<Path>>[] = [];
private stringIndex: Map<string, Path> = new Map();
public constructor() {
this.lengthIndex = [];
this.fragmentIndex = [];
this.stringIndex = new Map();
}
public constructor() {}
public get depth(): number {
return this.lengthIndex.length;
}
public add(path: Path): Path {
public add(pathStr: string): Path {
let path: Path = this.get(pathStr);
if (path) return path;
path = Path.parse(pathStr);
if (path.alias) throw new Error("PathSet does not support aliased paths");
const p = this.get(path);
if (p) return p;
this.stringIndex.set(path.toString(), path);
while (this.lengthIndex.length <= path.length) {
this.lengthIndex.push(new Set());
// fragmentIndex is one less than lengthIndex
if (this.lengthIndex.length > 1) this.fragmentIndex.push(new Map());
}
while (this.paramSegmentIndex.length < path.paramLength)
this.paramSegmentIndex.push(new Map());
const lengthIndex = this.lengthIndex[path.length];
lengthIndex.add(path);
while (this.attrSegmentIndex.length < path.attrLength)
this.attrSegmentIndex.push(new Map());
for (let i = 0; i < path.length; ++i) {
const fragment = path.segments[i] as string;
const fragmentIndex = this.fragmentIndex[i];
const fragmentIndex =
i < path.paramLength
? this.paramSegmentIndex[i]
: this.attrSegmentIndex[i - path.paramLength];
let fragmentIndexSet = fragmentIndex.get(fragment);
if (!fragmentIndexSet) {
@@ -48,64 +40,76 @@ export default class PathSet {
return path;
}
public get(path: Path): Path {
return this.stringIndex.get(path.toString()) || null;
public get(path: string): Path {
return this.stringIndex.get(path);
}
public find(
public findCompat(
path: Path,
superset = false,
subset = false,
depth: number = path.length,
depth = path.length,
): Path[] {
if (path.attrLength)
throw new Error("findCompat() does not support attribute paths");
depth = Math.min(31, depth);
const paramMask = (1 << path.paramLength) - 1;
let mask = ((0b10 << depth) - 1) & ~paramMask;
if (superset) mask |= ~path.wildcard & paramMask;
if (subset) mask |= path.wildcard;
return this.find(path, mask, 1);
}
public find(path: Path, paramMask: number, attrMask: number): Path[] {
if (path.alias) throw new Error("PathSet does not support aliased paths");
if (path.paramLength > this.paramSegmentIndex.length) return [];
if (path.attrLength > this.attrSegmentIndex.length) return [];
const len = path.length;
const emptySet: Set<Path> = new Set();
if (!superset && depth === len && (!subset || !path.wildcard)) {
const p = this.get(path);
return p ? [p] : [];
const indexes: [Set<Path>, Set<Path>][] = [];
const paramLengthMask = (1 << path.paramLength) - 1;
const attrLengthMask = (1 << path.attrLength) - 1;
const mask = (paramMask & paramLengthMask) | (attrMask << path.paramLength);
for (const [i, s] of path.segments.entries()) {
const b = 1 << i;
const m = mask & b;
const w = b & path.wildcard;
if (w && m) continue;
const idxSet =
i < path.paramLength
? this.paramSegmentIndex[i]
: this.attrSegmentIndex[i - path.paramLength];
const idx1 = idxSet.get(s as string) || emptySet;
let idx2 = emptySet;
if (m) idx2 = idxSet.get("*") || emptySet;
indexes.push([idx1, idx2]);
}
const lengthIndex = this.lengthIndex.slice(len, depth + 1);
if (!lengthIndex.length) return [];
indexes.sort((a, b) => a[0].size + a[1].size - (b[0].size + b[1].size));
let res;
for (let i = len - 1; i >= 0; --i) {
let fragmentIndexSet2;
const fragmentIndex = this.fragmentIndex[i];
let res: Path[];
if ((path.wildcard >> i) & 1) {
if (subset) continue;
} else if (superset) {
fragmentIndexSet2 = fragmentIndex.get("*");
}
if (!indexes.length) res = [...this.stringIndex.values()];
else res = [...indexes[0][0], ...indexes[0][1]];
const fragment = path.segments[i] as string;
const fragmentIndexSet1 = fragmentIndex.get(fragment);
const paramCover = ~paramLengthMask & paramMask;
const attrCover = ~attrLengthMask & attrMask;
if (!fragmentIndexSet1) {
if (!fragmentIndexSet2) return [];
if (!res) res = [...fragmentIndexSet2];
else res = res.filter((r) => fragmentIndexSet2.has(r));
} else if (!fragmentIndexSet2) {
if (!res) res = [...fragmentIndexSet1];
else res = res.filter((r) => fragmentIndexSet1.has(r));
} else {
if (!res) {
res = [...fragmentIndexSet1, ...fragmentIndexSet2];
} else {
res = res.filter(
(r) => fragmentIndexSet1.has(r) || fragmentIndexSet2.has(r),
);
}
}
if (!res.length) return res;
res = res.filter(
(p) =>
(1 << p.paramLength) & paramCover && (1 << p.attrLength) & attrCover,
);
for (let i = 1; i < indexes.length; ++i) {
const [idx1, idx2] = indexes[i];
res = res.filter((p) => idx1.has(p) || idx2.has(p));
}
if (!res) res = [].concat(...lengthIndex.map((a) => [...a]));
else res = res.filter((r) => lengthIndex.some((a) => a.has(r)));
return res;
}
}
+75 -170
View File
@@ -1,162 +1,31 @@
type Segments = (string | Alias)[];
type Alias = [Path, string][];
import Expression from "./expression.ts";
import { Cursor, parsePath } from "./expression/parser.ts";
type Segments = (string | Expression)[];
let cache1 = new Map<string, Path>();
let cache2 = new Map<string, Path>();
const CHAR_SPACE = 32;
const CHAR_BACKSLASH = 92;
const CHAR_COLON = 58;
const CHAR_QUOTE = 34;
const CHAR_OPEN_BRACKET = 91;
const CHAR_CLOSE_BRACKET = 93;
const CHAR_ASTERISK = 42;
const CHAR_COMMA = 44;
const CHAR_DOT = 46;
function charCodeAt(str: string, index: number): number {
if (index >= str.length) return 0;
return str.charCodeAt(index);
}
function legalChar(c: number): boolean {
return (
(c >= 97 && c <= 122) ||
(c >= 65 && c <= 90) ||
(c >= 48 && c <= 57) ||
c === 95 ||
c === 45
);
}
export default class Path {
declare public readonly segments: Segments;
declare public readonly colon: number;
declare public readonly wildcard: number;
declare public readonly alias: number;
declare protected _string: string;
declare protected _stringIndex: number[];
protected static parseAliasValue(
pattern: string,
index: number,
): { index: number; value: string } {
let i = index;
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
if (charCodeAt(pattern, i) === CHAR_QUOTE) {
for (let j = i + 1; j < pattern.length; ++j) {
if (
pattern.charCodeAt(j) === CHAR_QUOTE &&
pattern.charCodeAt(j - 1) !== CHAR_BACKSLASH
) {
try {
++j;
const v = JSON.parse(pattern.slice(i, j));
return { index: j, value: v };
} catch {
return { index, value: null };
}
}
}
return { index, value: null };
}
for (; i < pattern.length; ++i) {
const c = pattern.charCodeAt(i);
if (c === CHAR_CLOSE_BRACKET || c === CHAR_COMMA) break;
}
return { index: i, value: pattern.slice(index, i).trim() };
}
protected static parseAliasPath(
pattern: string,
index: number,
): { index: number; path: Path } {
let i = index;
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
const { index: idx, segments } = Path.parsePath(pattern, i);
if (idx === i) return { index, path: null };
i = idx;
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
if (charCodeAt(pattern, i) !== CHAR_COLON) return { index, path: null };
return { index: i + 1, path: new Path(segments) };
}
protected static parseAlias(
pattern: string,
index: number,
): { index: number; alias: Alias } {
const alias: Alias = [];
let i = index;
for (;;) {
let path: Path;
let value: string;
({ index: i, path } = Path.parseAliasPath(pattern, i));
if (!path) break;
({ index: i, value } = Path.parseAliasValue(pattern, i));
if (value == null) break;
alias.push([path, value]);
index = i;
if (charCodeAt(pattern, index) !== CHAR_COMMA) break;
++i;
}
// Ensure identical expressions have idential string representation
alias.sort((a, b) => {
if (a[0].toString() > b[0].toString()) return 1;
else if (a[0].toString() < b[0].toString()) return -1;
else if (a[1] > b[1]) return 1;
else if (a[1] < b[1]) return -1;
else return 0;
});
Object.freeze(alias);
return { index, alias };
}
protected static parsePath(
pattern: string,
index: number,
): { index: number; segments: Segments } {
const segments = [];
let segStart = index;
let wildcard = -1;
for (let i = index; i <= pattern.length; ++i) {
const c = charCodeAt(pattern, i);
if (c === CHAR_OPEN_BRACKET && i === segStart) {
const { index: idx, alias } = Path.parseAlias(pattern, i + 1);
if (charCodeAt(pattern, idx) !== CHAR_CLOSE_BRACKET) break;
segments.push(alias);
index = i = idx + 1;
segStart = i + 1;
} else if (c === CHAR_ASTERISK && i === segStart) {
wildcard = i + 1;
} else if (!legalChar(c) || i === wildcard) {
if (i === segStart) break;
const s = pattern.slice(segStart, i);
segments.push(s);
index = i;
if (c !== CHAR_DOT) break;
segStart = i + 1;
}
}
constructor(segments: Segments, colon: number) {
if (!(colon <= segments.length)) throw new Error("Invalid path");
if (segments.length > 32) throw new Error("Path too long");
Object.freeze(segments);
return { index, segments };
}
protected constructor(segments: Segments) {
let alias = 0;
let wildcard = 0;
const arr = segments.map((s, i) => {
if (Array.isArray(s)) {
if (s instanceof Expression) {
alias |= 1 << i;
const parts = s.map(
(al) => `${al[0].toString()}:${JSON.stringify(al[1])}`,
);
return `[${parts.join(",")}]`;
return `[${s.toString()}]`;
} else if (s === "*") {
wildcard |= 1 << i;
}
@@ -167,23 +36,27 @@ export default class Path {
const stringIndex = arr.map((s, i) => (offset += s.length) + i);
this.segments = segments;
this.colon = colon;
this.wildcard = wildcard;
this.alias = alias;
this._string = arr.join(".");
if (!colon) this._string = arr.join(".");
else
this._string =
arr.slice(0, -colon).join(".") + ":" + arr.slice(-colon).join(".");
this._stringIndex = stringIndex;
}
public static parse(str: string): Path {
let path = cache1.get(str);
public static parse(input: string): Path {
let path = cache1.get(input);
if (!path) {
path = cache2.get(str);
path = cache2.get(input);
if (!path) {
const { index, segments } = Path.parsePath(str, 0);
if (index < str.length) throw new Error("Invalid parameter path");
path = new Path(segments);
if (path.toString() !== str) cache1.set(path.toString(), path);
const cursor = new Cursor(input);
path = parsePath(cursor);
if (cursor.charCode) throw new Error("Unexpected character");
if (path.toString() !== input) cache1.set(path.toString(), path);
}
cache1.set(str, path);
cache1.set(input, path);
}
return path;
}
@@ -192,6 +65,14 @@ export default class Path {
return this.segments.length;
}
public get paramLength(): number {
return this.segments.length - this.colon;
}
public get attrLength(): number {
return this.colon;
}
public toString(): string {
return this._string;
}
@@ -200,25 +81,27 @@ export default class Path {
if (start < 0) start = Math.max(0, this.segments.length + start);
if (end < 0) end = Math.max(0, this.segments.length + end);
let str;
if (start >= end) {
str = "";
} else {
const i1 = start > 0 ? this._stringIndex[start - 1] + 1 : 0;
const i2 =
end <= this.segments.length
? this._stringIndex[end - 1]
: this._string.length;
str = this._string.slice(i1, i2);
}
if (start >= end) return Path.root;
let i1 = start > 0 ? this._stringIndex[start - 1] + 1 : 0;
// Include the ":" when slicing exactly at the colon boundary
if (this.colon && start === this.segments.length - this.colon) --i1;
const i2 =
end <= this.segments.length
? this._stringIndex[end - 1]
: this._string.length;
const str = this._string.slice(i1, i2);
let path = cache1.get(str);
if (!path) {
path = cache2.get(str);
if (!path) {
const segments = this.segments.slice(start, end);
Object.freeze(segments);
path = new Path(segments);
const colon =
start <= this.segments.length - this.colon
? Math.max(0, this.colon - this.segments.length + end)
: 0;
path = new Path(segments, colon);
}
cache1.set(str, path);
}
@@ -230,15 +113,28 @@ export default class Path {
if (!path2._string) return this;
else if (!this._string) return path2;
const str = `${this._string}.${path2._string}`;
if (this.colon && path2.colon && path2.colon < path2.segments.length)
throw new Error("Invalid path");
const colon = this.colon ? this.colon + path2.segments.length : path2.colon;
let str;
if (this.colon && path2.colon === path2.segments.length) {
// Right is all-colon; strip its ":" prefix and join with "."
str = `${this._string}.${path2._string.slice(1)}`;
} else if (path2.colon === path2.segments.length) {
// Left has no colon; right is all-colon; concatenate directly
str = `${this._string}${path2._string}`;
} else {
str = `${this._string}.${path2._string}`;
}
let path = cache1.get(str);
if (!path) {
path = cache2.get(str);
if (!path) {
const segments = this.segments.concat(path2.segments);
Object.freeze(segments);
path = new Path(segments);
path = new Path(segments, colon);
}
cache1.set(str, path);
}
@@ -248,21 +144,30 @@ export default class Path {
public stripAlias(): Path {
if (!this.alias) return this;
const segments = this.segments.map((s) => (Array.isArray(s) ? "*" : s));
const str = segments.join(".");
const segments = this.segments.map((s) =>
s instanceof Expression ? "*" : s,
);
let str: string;
if (!this.colon) str = segments.join(".");
else
str =
segments.slice(0, -this.colon).join(".") +
":" +
segments.slice(-this.colon).join(".");
let path = cache1.get(str);
if (!path) {
path = cache2.get(str);
if (!path) {
Object.freeze(segments);
path = new Path(segments);
path = new Path(segments, this.colon);
}
cache1.set(str, path);
}
return path;
}
static root = new Path([], 0);
}
const interval = setInterval(() => {