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

+10 -10
View File
@@ -1,13 +1,12 @@
import test from "node:test";
import assert from "node:assert";
import initSqlJs from "sql.js/dist/sql-asm.js";
import { parse, stringify } from "../lib/common/expression/parser.ts";
import {
bookmarkToExpression,
paginate,
toBookmark,
} from "../lib/common/expression/pagination.ts";
import { Expression } from "../lib/types.ts";
import Expression from "../lib/common/expression.ts";
import { covers, minimize } from "../lib/common/expression/synth.ts";
const VALUES = [null, -1, false, "a"];
@@ -79,15 +78,15 @@ async function testPaginate(
.map(([k, v]) => `${k} ${v > 0 ? "ASC" : "DESC"}`)
.join(", ");
const allMatches = await query(`${stringify(q2)} ORDER BY ${orderBy}`);
const allMatches = await query(`${q2.toString()} ORDER BY ${orderBy}`);
const [fulfilled, diff] = paginate(q1, q2, sort);
assert.ok(covers(q1, fulfilled));
const fulfilledMatches = await query(
`${stringify(fulfilled)} ORDER BY ${orderBy}`,
`${fulfilled.toString()} ORDER BY ${orderBy}`,
);
const diffMatches = await query(`${stringify(diff)} ORDER BY ${orderBy}`);
const diffMatches = await query(`${diff.toString()} ORDER BY ${orderBy}`);
assert.deepStrictEqual(allMatches, [...fulfilledMatches, ...diffMatches]);
if (allMatches.length === fulfilledMatches.length) return;
@@ -98,14 +97,14 @@ async function testPaginate(
);
const bookmark = toBookmark(sort, nextMatches[nextMatches.length - 1]);
const bookmarkFilter = bookmarkToExpression(bookmark, sort);
const capped = ["AND", q2, bookmarkFilter];
const capped = Expression.and(q2, bookmarkFilter);
assert.ok(!covers(q1, capped));
const cappedMatches = await query(
`(${stringify(capped)}) ORDER BY ${orderBy}`,
`(${capped.toString()}) ORDER BY ${orderBy}`,
);
assert.deepStrictEqual(cappedMatches, [...fulfilledMatches, ...nextMatches]);
const min = minimize(["OR", q1, capped]);
const min = minimize(Expression.or(q1, capped));
await testPaginate(min, q2, sort);
}
@@ -115,6 +114,7 @@ void test("paginate", async () => {
const sortOrders = getAllSortOrders(params);
for (const [q1, q2] of cases)
for (const sort of sortOrders)
await testPaginate(parse(q1), parse(q2), sort);
for (const sort of sortOrders) {
await testPaginate(Expression.parse(q1), Expression.parse(q2), sort);
}
});