@hviana/sema 0.1.9 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/example/demo.d.ts +1 -0
- package/dist/example/demo.js +39 -0
- package/dist/example/train_base.d.ts +87 -0
- package/dist/example/train_base.js +2252 -0
- package/dist/src/alphabet.d.ts +7 -0
- package/dist/src/alphabet.js +33 -0
- package/dist/src/alu/src/alu.d.ts +185 -0
- package/dist/src/alu/src/alu.js +440 -0
- package/dist/src/alu/src/expr.d.ts +61 -0
- package/dist/src/alu/src/expr.js +318 -0
- package/dist/src/alu/src/index.d.ts +11 -0
- package/dist/src/alu/src/index.js +19 -0
- package/dist/src/alu/src/kernel-arith.d.ts +16 -0
- package/dist/src/alu/src/kernel-arith.js +264 -0
- package/dist/src/alu/src/kernel-bits.d.ts +19 -0
- package/dist/src/alu/src/kernel-bits.js +152 -0
- package/dist/src/alu/src/kernel-logic.d.ts +4 -0
- package/dist/src/alu/src/kernel-logic.js +60 -0
- package/dist/src/alu/src/kernel-nd.d.ts +3 -0
- package/dist/src/alu/src/kernel-nd.js +208 -0
- package/dist/src/alu/src/kernel-numeric.d.ts +54 -0
- package/dist/src/alu/src/kernel-numeric.js +366 -0
- package/dist/src/alu/src/operation.d.ts +168 -0
- package/dist/src/alu/src/operation.js +189 -0
- package/dist/src/alu/src/parser.d.ts +221 -0
- package/dist/src/alu/src/parser.js +577 -0
- package/dist/src/alu/src/resonance.d.ts +55 -0
- package/dist/src/alu/src/resonance.js +126 -0
- package/dist/src/alu/src/text.d.ts +31 -0
- package/dist/src/alu/src/text.js +73 -0
- package/dist/src/alu/src/value.d.ts +109 -0
- package/dist/src/alu/src/value.js +300 -0
- package/dist/src/alu/test/alu.test.d.ts +1 -0
- package/dist/src/alu/test/alu.test.js +764 -0
- package/dist/src/bytes.d.ts +14 -0
- package/dist/src/bytes.js +59 -0
- package/dist/src/canon.d.ts +26 -0
- package/dist/src/canon.js +57 -0
- package/dist/src/config.d.ts +111 -0
- package/dist/src/config.js +91 -0
- package/dist/src/derive/src/deduction.d.ts +125 -0
- package/dist/src/derive/src/deduction.js +155 -0
- package/dist/src/derive/src/index.d.ts +7 -0
- package/dist/src/derive/src/index.js +11 -0
- package/dist/src/derive/src/priority-queue.d.ts +20 -0
- package/dist/src/derive/src/priority-queue.js +73 -0
- package/dist/src/derive/src/rewrite.d.ts +56 -0
- package/dist/src/derive/src/rewrite.js +100 -0
- package/dist/src/derive/src/trie.d.ts +90 -0
- package/dist/src/derive/src/trie.js +217 -0
- package/dist/src/derive/test/derive.test.d.ts +1 -0
- package/dist/src/derive/test/derive.test.js +122 -0
- package/dist/src/extension.d.ts +37 -0
- package/dist/src/extension.js +7 -0
- package/dist/src/geometry.d.ts +148 -0
- package/dist/src/geometry.js +511 -0
- package/dist/src/index.d.ts +16 -0
- package/dist/src/index.js +18 -0
- package/dist/src/ingest-cache.d.ts +41 -0
- package/dist/src/ingest-cache.js +161 -0
- package/dist/src/mind/articulation.d.ts +6 -0
- package/dist/src/mind/articulation.js +99 -0
- package/dist/src/mind/attention.d.ts +72 -0
- package/dist/src/mind/attention.js +898 -0
- package/dist/src/mind/canonical.d.ts +29 -0
- package/dist/src/mind/canonical.js +88 -0
- package/dist/src/mind/graph-search.d.ts +270 -0
- package/dist/src/mind/graph-search.js +927 -0
- package/dist/src/mind/index.d.ts +5 -0
- package/dist/src/mind/index.js +5 -0
- package/dist/src/mind/junction.d.ts +95 -0
- package/dist/src/mind/junction.js +262 -0
- package/dist/src/mind/learning.d.ts +47 -0
- package/dist/src/mind/learning.js +233 -0
- package/dist/src/mind/match.d.ts +113 -0
- package/dist/src/mind/match.js +446 -0
- package/dist/src/mind/mechanisms/alu.d.ts +4 -0
- package/dist/src/mind/mechanisms/alu.js +29 -0
- package/dist/src/mind/mechanisms/cast.d.ts +35 -0
- package/dist/src/mind/mechanisms/cast.js +461 -0
- package/dist/src/mind/mechanisms/confluence.d.ts +24 -0
- package/dist/src/mind/mechanisms/confluence.js +225 -0
- package/dist/src/mind/mechanisms/cover.d.ts +6 -0
- package/dist/src/mind/mechanisms/cover.js +179 -0
- package/dist/src/mind/mechanisms/extraction.d.ts +67 -0
- package/dist/src/mind/mechanisms/extraction.js +342 -0
- package/dist/src/mind/mechanisms/recall.d.ts +13 -0
- package/dist/src/mind/mechanisms/recall.js +235 -0
- package/dist/src/mind/mind.d.ts +273 -0
- package/dist/src/mind/mind.js +598 -0
- package/dist/src/mind/pipeline-mechanism.d.ts +142 -0
- package/dist/src/mind/pipeline-mechanism.js +213 -0
- package/dist/src/mind/pipeline.d.ts +20 -0
- package/dist/src/mind/pipeline.js +185 -0
- package/dist/src/mind/primitives.d.ts +63 -0
- package/dist/src/mind/primitives.js +251 -0
- package/dist/src/mind/rationale.d.ts +134 -0
- package/dist/src/mind/rationale.js +162 -0
- package/dist/src/mind/reasoning.d.ts +15 -0
- package/dist/src/mind/reasoning.js +162 -0
- package/dist/src/mind/recognition.d.ts +20 -0
- package/dist/src/mind/recognition.js +246 -0
- package/dist/src/mind/resonance.d.ts +23 -0
- package/dist/src/mind/resonance.js +0 -0
- package/dist/src/mind/trace.d.ts +15 -0
- package/dist/src/mind/trace.js +73 -0
- package/dist/src/mind/traverse.d.ts +113 -0
- package/dist/src/mind/traverse.js +488 -0
- package/dist/src/mind/types.d.ts +186 -0
- package/dist/src/mind/types.js +84 -0
- package/dist/src/rabitq-ivf/src/database.d.ts +113 -0
- package/dist/src/rabitq-ivf/src/database.js +201 -0
- package/dist/src/rabitq-ivf/src/index.d.ts +7 -0
- package/dist/src/rabitq-ivf/src/index.js +4 -0
- package/dist/src/rabitq-ivf/src/ivf.d.ts +200 -0
- package/dist/src/rabitq-ivf/src/ivf.js +1165 -0
- package/dist/src/rabitq-ivf/src/prng.d.ts +19 -0
- package/dist/src/rabitq-ivf/src/prng.js +36 -0
- package/dist/src/rabitq-ivf/src/rabitq.d.ts +95 -0
- package/dist/src/rabitq-ivf/src/rabitq.js +283 -0
- package/dist/src/sema.d.ts +31 -0
- package/dist/src/sema.js +63 -0
- package/dist/src/store-sqlite.d.ts +184 -0
- package/dist/src/store-sqlite.js +942 -0
- package/dist/src/store.d.ts +654 -0
- package/dist/src/store.js +1612 -0
- package/dist/src/vec.d.ts +31 -0
- package/dist/src/vec.js +109 -0
- package/package.json +1 -1
- package/src/mind/graph-search.ts +128 -46
- package/src/mind/mind.ts +37 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary min-heap keyed by a numeric priority, carrying an arbitrary payload.
|
|
3
|
+
*
|
|
4
|
+
* It is the agenda of the lightest-derivation search, where the priority is the
|
|
5
|
+
* estimate f = g + h. Stale entries are tolerated by the consumer (lazy
|
|
6
|
+
* deletion), so there is no decrease-key: when an item's cost improves it is
|
|
7
|
+
* simply pushed again, and the older, higher-priority copy is recognised as
|
|
8
|
+
* stale and discarded when it surfaces. Parallel arrays (rather than an array
|
|
9
|
+
* of objects) keep the hot path allocation-free.
|
|
10
|
+
*/
|
|
11
|
+
export declare class MinHeap<T> {
|
|
12
|
+
private readonly keys;
|
|
13
|
+
private readonly vals;
|
|
14
|
+
get size(): number;
|
|
15
|
+
push(priority: number, value: T): void;
|
|
16
|
+
pop(): {
|
|
17
|
+
priority: number;
|
|
18
|
+
value: T;
|
|
19
|
+
} | undefined;
|
|
20
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary min-heap keyed by a numeric priority, carrying an arbitrary payload.
|
|
3
|
+
*
|
|
4
|
+
* It is the agenda of the lightest-derivation search, where the priority is the
|
|
5
|
+
* estimate f = g + h. Stale entries are tolerated by the consumer (lazy
|
|
6
|
+
* deletion), so there is no decrease-key: when an item's cost improves it is
|
|
7
|
+
* simply pushed again, and the older, higher-priority copy is recognised as
|
|
8
|
+
* stale and discarded when it surfaces. Parallel arrays (rather than an array
|
|
9
|
+
* of objects) keep the hot path allocation-free.
|
|
10
|
+
*/
|
|
11
|
+
export class MinHeap {
|
|
12
|
+
keys = [];
|
|
13
|
+
vals = [];
|
|
14
|
+
get size() {
|
|
15
|
+
return this.keys.length;
|
|
16
|
+
}
|
|
17
|
+
push(priority, value) {
|
|
18
|
+
const keys = this.keys;
|
|
19
|
+
const vals = this.vals;
|
|
20
|
+
let i = keys.length;
|
|
21
|
+
keys.push(priority);
|
|
22
|
+
vals.push(value);
|
|
23
|
+
while (i > 0) {
|
|
24
|
+
const parent = (i - 1) >> 1;
|
|
25
|
+
if (keys[i] < keys[parent]) {
|
|
26
|
+
const tk = keys[i];
|
|
27
|
+
keys[i] = keys[parent];
|
|
28
|
+
keys[parent] = tk;
|
|
29
|
+
const tv = vals[i];
|
|
30
|
+
vals[i] = vals[parent];
|
|
31
|
+
vals[parent] = tv;
|
|
32
|
+
i = parent;
|
|
33
|
+
}
|
|
34
|
+
else
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
pop() {
|
|
39
|
+
const keys = this.keys;
|
|
40
|
+
const vals = this.vals;
|
|
41
|
+
const n = keys.length;
|
|
42
|
+
if (n === 0)
|
|
43
|
+
return undefined;
|
|
44
|
+
const top = { priority: keys[0], value: vals[0] };
|
|
45
|
+
const lastKey = keys.pop();
|
|
46
|
+
const lastVal = vals.pop();
|
|
47
|
+
const m = keys.length;
|
|
48
|
+
if (m > 0) {
|
|
49
|
+
keys[0] = lastKey;
|
|
50
|
+
vals[0] = lastVal;
|
|
51
|
+
let i = 0;
|
|
52
|
+
for (;;) {
|
|
53
|
+
const left = 2 * i + 1;
|
|
54
|
+
const right = left + 1;
|
|
55
|
+
let best = i;
|
|
56
|
+
if (left < m && keys[left] < keys[best])
|
|
57
|
+
best = left;
|
|
58
|
+
if (right < m && keys[right] < keys[best])
|
|
59
|
+
best = right;
|
|
60
|
+
if (best === i)
|
|
61
|
+
break;
|
|
62
|
+
const tk = keys[i];
|
|
63
|
+
keys[i] = keys[best];
|
|
64
|
+
keys[best] = tk;
|
|
65
|
+
const tv = vals[i];
|
|
66
|
+
vals[i] = vals[best];
|
|
67
|
+
vals[best] = tv;
|
|
68
|
+
i = best;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return top;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sequence segmentation on the lightest-derivation engine.
|
|
3
|
+
*
|
|
4
|
+
* The only structure here is the **frontier**: the items are the positions
|
|
5
|
+
* `0…length` of a sequence, and a derivation is a path through them. That is
|
|
6
|
+
* what keeps the search linear in the input rather than quadratic — there is no
|
|
7
|
+
* enumeration of span *pairs* and no chart of O(n²) sub-sequences.
|
|
8
|
+
*
|
|
9
|
+
* Candidate spans are produced **on demand**: when the search finalises a
|
|
10
|
+
* position it asks a {@link Trie} which learned forms begin there. The trie walk
|
|
11
|
+
* stops exactly where the data's patterns stop — a position with nothing learned
|
|
12
|
+
* dead-ends immediately, a position inside a long form walks precisely that far.
|
|
13
|
+
* Nothing is scanned ahead of demand and no length bound is imposed; the
|
|
14
|
+
* structure of what was learned is the bound.
|
|
15
|
+
*
|
|
16
|
+
* {@link coverSequence} is the segmentation primitive: the lightest set of
|
|
17
|
+
* non-overlapping spans covering a sequence. It is the principled,
|
|
18
|
+
* corpus-independent replacement for "scan the whole stream with an automaton,
|
|
19
|
+
* then greedily keep the longest non-overlapping matches" — same linear cost,
|
|
20
|
+
* but the *optimal* cover and only the work the goal demands.
|
|
21
|
+
*/
|
|
22
|
+
/** A candidate span over a sequence, carrying caller payload. */
|
|
23
|
+
export interface CandidateSpan<P> {
|
|
24
|
+
/** Start offset (inclusive). */
|
|
25
|
+
start: number;
|
|
26
|
+
/** End offset (exclusive); must be > start. */
|
|
27
|
+
end: number;
|
|
28
|
+
/** Relative cost of using this span (default 1). Lower wins ties. */
|
|
29
|
+
weight?: number;
|
|
30
|
+
/** Caller data, returned on the chosen spans. */
|
|
31
|
+
payload: P;
|
|
32
|
+
}
|
|
33
|
+
export interface Cover<P> {
|
|
34
|
+
/** Chosen non-overlapping spans, left to right. */
|
|
35
|
+
spans: Array<CandidateSpan<P>>;
|
|
36
|
+
/** Symbols covered by the chosen spans. */
|
|
37
|
+
covered: number;
|
|
38
|
+
/** Symbols left uncovered. */
|
|
39
|
+
uncovered: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The lightest non-overlapping cover of `[0, length)` drawn from `candidates`.
|
|
43
|
+
*
|
|
44
|
+
* Primary objective: cover the most symbols (fewest left uncovered). Secondary:
|
|
45
|
+
* least total span weight — with the default unit weight this prefers fewer,
|
|
46
|
+
* longer spans, the optimal analogue of greedy longest-match, but it can return
|
|
47
|
+
* a pair of shorter spans when together they cover more than one long one (which
|
|
48
|
+
* greedy cannot). Modelled as a shortest path over frontier positions: from a
|
|
49
|
+
* position you either leave one symbol uncovered (costly) or take a candidate
|
|
50
|
+
* that starts there (cheap), reaching `length`.
|
|
51
|
+
*
|
|
52
|
+
* Cost: O((length + |candidates|) · log length) — the frontier has `length + 1`
|
|
53
|
+
* items, each finalised once, with one "skip" edge plus the candidates that
|
|
54
|
+
* start there. No quadratic span structure.
|
|
55
|
+
*/
|
|
56
|
+
export declare function coverSequence<P>(length: number, candidates: ReadonlyArray<CandidateSpan<P>>): Cover<P>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sequence segmentation on the lightest-derivation engine.
|
|
3
|
+
*
|
|
4
|
+
* The only structure here is the **frontier**: the items are the positions
|
|
5
|
+
* `0…length` of a sequence, and a derivation is a path through them. That is
|
|
6
|
+
* what keeps the search linear in the input rather than quadratic — there is no
|
|
7
|
+
* enumeration of span *pairs* and no chart of O(n²) sub-sequences.
|
|
8
|
+
*
|
|
9
|
+
* Candidate spans are produced **on demand**: when the search finalises a
|
|
10
|
+
* position it asks a {@link Trie} which learned forms begin there. The trie walk
|
|
11
|
+
* stops exactly where the data's patterns stop — a position with nothing learned
|
|
12
|
+
* dead-ends immediately, a position inside a long form walks precisely that far.
|
|
13
|
+
* Nothing is scanned ahead of demand and no length bound is imposed; the
|
|
14
|
+
* structure of what was learned is the bound.
|
|
15
|
+
*
|
|
16
|
+
* {@link coverSequence} is the segmentation primitive: the lightest set of
|
|
17
|
+
* non-overlapping spans covering a sequence. It is the principled,
|
|
18
|
+
* corpus-independent replacement for "scan the whole stream with an automaton,
|
|
19
|
+
* then greedily keep the longest non-overlapping matches" — same linear cost,
|
|
20
|
+
* but the *optimal* cover and only the work the goal demands.
|
|
21
|
+
*/
|
|
22
|
+
import { lightestDerivation } from "./deduction.js";
|
|
23
|
+
/**
|
|
24
|
+
* The lightest non-overlapping cover of `[0, length)` drawn from `candidates`.
|
|
25
|
+
*
|
|
26
|
+
* Primary objective: cover the most symbols (fewest left uncovered). Secondary:
|
|
27
|
+
* least total span weight — with the default unit weight this prefers fewer,
|
|
28
|
+
* longer spans, the optimal analogue of greedy longest-match, but it can return
|
|
29
|
+
* a pair of shorter spans when together they cover more than one long one (which
|
|
30
|
+
* greedy cannot). Modelled as a shortest path over frontier positions: from a
|
|
31
|
+
* position you either leave one symbol uncovered (costly) or take a candidate
|
|
32
|
+
* that starts there (cheap), reaching `length`.
|
|
33
|
+
*
|
|
34
|
+
* Cost: O((length + |candidates|) · log length) — the frontier has `length + 1`
|
|
35
|
+
* items, each finalised once, with one "skip" edge plus the candidates that
|
|
36
|
+
* start there. No quadratic span structure.
|
|
37
|
+
*/
|
|
38
|
+
export function coverSequence(length, candidates) {
|
|
39
|
+
if (length <= 0)
|
|
40
|
+
return { spans: [], covered: 0, uncovered: 0 };
|
|
41
|
+
// Candidates indexed by where they start, and by their exact (start,end) edge
|
|
42
|
+
// for reconstruction. Both are O(|candidates|) to build and to read.
|
|
43
|
+
const byStart = Array.from({ length }, () => []);
|
|
44
|
+
const byEdge = new Map();
|
|
45
|
+
let maxWeight = 1;
|
|
46
|
+
for (const c of candidates) {
|
|
47
|
+
if (c.end <= c.start || c.start < 0 || c.end > length)
|
|
48
|
+
continue;
|
|
49
|
+
const w = c.weight ?? 1;
|
|
50
|
+
if (w > maxWeight)
|
|
51
|
+
maxWeight = w;
|
|
52
|
+
byStart[c.start].push(c);
|
|
53
|
+
const edge = c.start * (length + 1) + c.end;
|
|
54
|
+
const prev = byEdge.get(edge);
|
|
55
|
+
if (!prev || w < (prev.weight ?? 1))
|
|
56
|
+
byEdge.set(edge, c);
|
|
57
|
+
}
|
|
58
|
+
// One uncovered symbol must outweigh any sum of span weights, so coverage is
|
|
59
|
+
// strictly the primary objective and weight only breaks ties.
|
|
60
|
+
const skipCost = maxWeight * length + 1;
|
|
61
|
+
const system = {
|
|
62
|
+
key: (p) => "" + p,
|
|
63
|
+
axioms: () => [{ item: 0, cost: 0 }],
|
|
64
|
+
isGoal: (p) => p === length,
|
|
65
|
+
// No nonzero admissible bound is available here: a single candidate can
|
|
66
|
+
// cover the whole remainder for unit cost, so any per-symbol estimate would
|
|
67
|
+
// overestimate. With h = 0 this is exact Knuth/Dijkstra over the frontier —
|
|
68
|
+
// optimal, and linear in the positions. (The A* outside bound is for systems
|
|
69
|
+
// whose remaining cost can be genuinely lower-bounded; see the engine.)
|
|
70
|
+
*rules(p) {
|
|
71
|
+
if (p >= length)
|
|
72
|
+
return;
|
|
73
|
+
yield { premises: [p], conclusion: p + 1, cost: skipCost }; // leave uncovered
|
|
74
|
+
for (const c of byStart[p]) {
|
|
75
|
+
yield { premises: [p], conclusion: c.end, cost: c.weight ?? 1 };
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
const best = lightestDerivation(system);
|
|
80
|
+
if (!best)
|
|
81
|
+
return { spans: [], covered: 0, uncovered: length };
|
|
82
|
+
// Walk the frontier chain back to the axiom; an edge p→q is a chosen span iff
|
|
83
|
+
// a candidate spans exactly [p, q) (a covering edge is always cheaper than the
|
|
84
|
+
// skips it replaces, so it appears on the optimal path wherever it is used).
|
|
85
|
+
const spans = [];
|
|
86
|
+
let node = best;
|
|
87
|
+
while (node && node.rule) {
|
|
88
|
+
const to = node.item;
|
|
89
|
+
const from = node.premises[0].item;
|
|
90
|
+
const span = byEdge.get(from * (length + 1) + to);
|
|
91
|
+
if (span)
|
|
92
|
+
spans.push(span);
|
|
93
|
+
node = node.premises[0];
|
|
94
|
+
}
|
|
95
|
+
spans.reverse();
|
|
96
|
+
let covered = 0;
|
|
97
|
+
for (const s of spans)
|
|
98
|
+
covered += s.end - s.start;
|
|
99
|
+
return { spans, covered, uncovered: length - covered };
|
|
100
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forward prefix trie over integer symbols — the lazy site matcher.
|
|
3
|
+
*
|
|
4
|
+
* This is the matching primitive the lightest-derivation search consults *on
|
|
5
|
+
* demand*: given a position in a sequence, {@link Trie.matchesAt} walks forward
|
|
6
|
+
* from the root and reports every stored pattern that begins there, in
|
|
7
|
+
* O(longest matching pattern). Nothing is scanned that the search never asks
|
|
8
|
+
* about — there is no global automaton, no failure links, no precomputed match
|
|
9
|
+
* table. That is the whole point: rewrite sites are *materialised only when
|
|
10
|
+
* demanded* (the "lazy hyperedge generation" of the rewrite search), so the
|
|
11
|
+
* matcher itself stays trivial and the search decides what to look at.
|
|
12
|
+
*
|
|
13
|
+
* It is fully generic and self-contained: symbols are non-negative integers
|
|
14
|
+
* (bytes 0–255, Unicode code points, opcodes, …); patterns are any
|
|
15
|
+
* `ArrayLike<number>` (`Uint8Array` or `number[]`); each pattern carries an
|
|
16
|
+
* arbitrary `payload` returned on every match. The trie also exposes a tiny
|
|
17
|
+
* cursor API ({@link Trie.root}, {@link Trie.step}, {@link Trie.terminal}) so a
|
|
18
|
+
* caller can extend a partial match symbol-by-symbol — e.g. to ask "could this
|
|
19
|
+
* span still grow into a known form?" while composing.
|
|
20
|
+
*
|
|
21
|
+
* ## Memory
|
|
22
|
+
*
|
|
23
|
+
* Most states (99.7 % in typical use) have exactly one outgoing transition.
|
|
24
|
+
* Storing a full `Map` per state would cost ~88 bytes each. Instead, a singleton
|
|
25
|
+
* transition is packed inline into two typed arrays — `_nxt` (Int32Array, 4
|
|
26
|
+
* bytes) and `_sym` (Uint8Array, 1 byte) — for ~5 bytes per state, a 17×
|
|
27
|
+
* reduction. Only the rare multi-transition state (~0.3 %) allocates a `Map`,
|
|
28
|
+
* kept in a sparse `_multi` table keyed by state number.
|
|
29
|
+
*/
|
|
30
|
+
/** One pattern occurrence found by the trie. */
|
|
31
|
+
export interface Match<P> {
|
|
32
|
+
/** Start offset in the searched sequence (inclusive). */
|
|
33
|
+
start: number;
|
|
34
|
+
/** End offset (exclusive); `end - start === length`. */
|
|
35
|
+
end: number;
|
|
36
|
+
/** Length of the matched pattern. */
|
|
37
|
+
length: number;
|
|
38
|
+
/** Pattern id, assigned in insertion order (0-based). */
|
|
39
|
+
id: number;
|
|
40
|
+
/** Payload registered with the pattern. */
|
|
41
|
+
payload: P;
|
|
42
|
+
}
|
|
43
|
+
export declare class Trie<P = undefined> {
|
|
44
|
+
private _nxt;
|
|
45
|
+
private _sym;
|
|
46
|
+
private _multi;
|
|
47
|
+
private _end;
|
|
48
|
+
private readonly _lens;
|
|
49
|
+
private readonly _vals;
|
|
50
|
+
private _len;
|
|
51
|
+
constructor();
|
|
52
|
+
/** The root state, for cursor walks. */
|
|
53
|
+
get root(): number;
|
|
54
|
+
/** Number of distinct patterns stored. */
|
|
55
|
+
get size(): number;
|
|
56
|
+
/** Allocate a fresh state, growing the typed arrays by a fixed increment
|
|
57
|
+
* when full. No power-of-2 doubling — the transient double-memory spike
|
|
58
|
+
* during growth is bounded to the increment. */
|
|
59
|
+
private _state;
|
|
60
|
+
/** Follow symbol `c` from state `s`. Returns the next state, or -1. */
|
|
61
|
+
private _follow;
|
|
62
|
+
/** Follow one symbol from `state`; returns the next state or -1 if none. */
|
|
63
|
+
step(state: number, symbol: number): number;
|
|
64
|
+
/** The pattern ending exactly at `state`, or null. */
|
|
65
|
+
terminal(state: number): {
|
|
66
|
+
id: number;
|
|
67
|
+
payload: P;
|
|
68
|
+
} | null;
|
|
69
|
+
/** Length of the pattern with this id. */
|
|
70
|
+
lengthOf(id: number): number;
|
|
71
|
+
/**
|
|
72
|
+
* Insert a pattern, returning its id. Inserting the same symbol-sequence
|
|
73
|
+
* twice returns the first id and keeps the first payload (patterns are keyed
|
|
74
|
+
* by content). Empty patterns are ignored and return -1.
|
|
75
|
+
*/
|
|
76
|
+
insert(pattern: ArrayLike<number>, payload: P): number;
|
|
77
|
+
/**
|
|
78
|
+
* Every stored pattern that begins exactly at `pos` in `seq`, shortest first.
|
|
79
|
+
* Walks forward from the root in O(longest match); reports nothing about any
|
|
80
|
+
* other position. This is the on-demand probe the search uses.
|
|
81
|
+
*/
|
|
82
|
+
matchesAt(seq: ArrayLike<number>, pos: number): Array<Match<P>>;
|
|
83
|
+
/**
|
|
84
|
+
* Every occurrence of every pattern anywhere in `seq` (eager form, the union
|
|
85
|
+
* of {@link matchesAt} over all start positions). O(seq · longest pattern),
|
|
86
|
+
* independent of how many patterns are stored. Use {@link matchesAt} when the
|
|
87
|
+
* search only needs the sites at a particular position.
|
|
88
|
+
*/
|
|
89
|
+
scan(seq: ArrayLike<number>): Array<Match<P>>;
|
|
90
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forward prefix trie over integer symbols — the lazy site matcher.
|
|
3
|
+
*
|
|
4
|
+
* This is the matching primitive the lightest-derivation search consults *on
|
|
5
|
+
* demand*: given a position in a sequence, {@link Trie.matchesAt} walks forward
|
|
6
|
+
* from the root and reports every stored pattern that begins there, in
|
|
7
|
+
* O(longest matching pattern). Nothing is scanned that the search never asks
|
|
8
|
+
* about — there is no global automaton, no failure links, no precomputed match
|
|
9
|
+
* table. That is the whole point: rewrite sites are *materialised only when
|
|
10
|
+
* demanded* (the "lazy hyperedge generation" of the rewrite search), so the
|
|
11
|
+
* matcher itself stays trivial and the search decides what to look at.
|
|
12
|
+
*
|
|
13
|
+
* It is fully generic and self-contained: symbols are non-negative integers
|
|
14
|
+
* (bytes 0–255, Unicode code points, opcodes, …); patterns are any
|
|
15
|
+
* `ArrayLike<number>` (`Uint8Array` or `number[]`); each pattern carries an
|
|
16
|
+
* arbitrary `payload` returned on every match. The trie also exposes a tiny
|
|
17
|
+
* cursor API ({@link Trie.root}, {@link Trie.step}, {@link Trie.terminal}) so a
|
|
18
|
+
* caller can extend a partial match symbol-by-symbol — e.g. to ask "could this
|
|
19
|
+
* span still grow into a known form?" while composing.
|
|
20
|
+
*
|
|
21
|
+
* ## Memory
|
|
22
|
+
*
|
|
23
|
+
* Most states (99.7 % in typical use) have exactly one outgoing transition.
|
|
24
|
+
* Storing a full `Map` per state would cost ~88 bytes each. Instead, a singleton
|
|
25
|
+
* transition is packed inline into two typed arrays — `_nxt` (Int32Array, 4
|
|
26
|
+
* bytes) and `_sym` (Uint8Array, 1 byte) — for ~5 bytes per state, a 17×
|
|
27
|
+
* reduction. Only the rare multi-transition state (~0.3 %) allocates a `Map`,
|
|
28
|
+
* kept in a sparse `_multi` table keyed by state number.
|
|
29
|
+
*/
|
|
30
|
+
const ROOT = 0;
|
|
31
|
+
export class Trie {
|
|
32
|
+
// ── compact transition storage ──────────────────────────────────────────
|
|
33
|
+
// _nxt[s] — -1 terminal (no outgoing transition)
|
|
34
|
+
// -2 multi-transition → _multi.get(s)
|
|
35
|
+
// >=0 singleton target; the symbol is _sym[s]
|
|
36
|
+
_nxt;
|
|
37
|
+
_sym;
|
|
38
|
+
_multi = null;
|
|
39
|
+
// _end[s] is the pattern id ending exactly at s, or -1.
|
|
40
|
+
_end;
|
|
41
|
+
// Per-pattern data, indexed by pattern id.
|
|
42
|
+
_lens = [];
|
|
43
|
+
_vals = [];
|
|
44
|
+
_len = 1; // next free state (state 0 = root)
|
|
45
|
+
constructor() {
|
|
46
|
+
const c = 256;
|
|
47
|
+
this._nxt = new Int32Array(c);
|
|
48
|
+
this._sym = new Uint8Array(c);
|
|
49
|
+
this._end = new Int32Array(c);
|
|
50
|
+
this._nxt[ROOT] = -1;
|
|
51
|
+
this._end[ROOT] = -1;
|
|
52
|
+
}
|
|
53
|
+
/** The root state, for cursor walks. */
|
|
54
|
+
get root() {
|
|
55
|
+
return ROOT;
|
|
56
|
+
}
|
|
57
|
+
/** Number of distinct patterns stored. */
|
|
58
|
+
get size() {
|
|
59
|
+
return this._lens.length;
|
|
60
|
+
}
|
|
61
|
+
// ── internal helpers ────────────────────────────────────────────────────
|
|
62
|
+
/** Allocate a fresh state, growing the typed arrays by a fixed increment
|
|
63
|
+
* when full. No power-of-2 doubling — the transient double-memory spike
|
|
64
|
+
* during growth is bounded to the increment. */
|
|
65
|
+
_state() {
|
|
66
|
+
const s = this._len++;
|
|
67
|
+
if (s >= this._nxt.length) {
|
|
68
|
+
const c = s + 4096;
|
|
69
|
+
const nn = new Int32Array(c);
|
|
70
|
+
nn.set(this._nxt);
|
|
71
|
+
this._nxt = nn;
|
|
72
|
+
const ns = new Uint8Array(c);
|
|
73
|
+
ns.set(this._sym);
|
|
74
|
+
this._sym = ns;
|
|
75
|
+
const ne = new Int32Array(c);
|
|
76
|
+
ne.set(this._end);
|
|
77
|
+
this._end = ne;
|
|
78
|
+
}
|
|
79
|
+
this._nxt[s] = -1;
|
|
80
|
+
this._end[s] = -1;
|
|
81
|
+
return s;
|
|
82
|
+
}
|
|
83
|
+
/** Follow symbol `c` from state `s`. Returns the next state, or -1. */
|
|
84
|
+
_follow(s, c) {
|
|
85
|
+
const n = this._nxt[s];
|
|
86
|
+
if (n >= 0)
|
|
87
|
+
return this._sym[s] === c ? n : -1;
|
|
88
|
+
if (n === -2)
|
|
89
|
+
return this._multi.get(s).get(c) ?? -1;
|
|
90
|
+
return -1; // n === -1
|
|
91
|
+
}
|
|
92
|
+
// ── cursor API (allocation-free) ─────────────────────────────────────────
|
|
93
|
+
/** Follow one symbol from `state`; returns the next state or -1 if none. */
|
|
94
|
+
step(state, symbol) {
|
|
95
|
+
return this._follow(state, symbol);
|
|
96
|
+
}
|
|
97
|
+
/** The pattern ending exactly at `state`, or null. */
|
|
98
|
+
terminal(state) {
|
|
99
|
+
const id = this._end[state];
|
|
100
|
+
return id === -1 ? null : { id, payload: this._vals[id] };
|
|
101
|
+
}
|
|
102
|
+
/** Length of the pattern with this id. */
|
|
103
|
+
lengthOf(id) {
|
|
104
|
+
return this._lens[id];
|
|
105
|
+
}
|
|
106
|
+
// ── build ───────────────────────────────────────────────────────────────
|
|
107
|
+
/**
|
|
108
|
+
* Insert a pattern, returning its id. Inserting the same symbol-sequence
|
|
109
|
+
* twice returns the first id and keeps the first payload (patterns are keyed
|
|
110
|
+
* by content). Empty patterns are ignored and return -1.
|
|
111
|
+
*/
|
|
112
|
+
insert(pattern, payload) {
|
|
113
|
+
const n = pattern.length;
|
|
114
|
+
if (n === 0)
|
|
115
|
+
return -1;
|
|
116
|
+
let s = ROOT;
|
|
117
|
+
for (let i = 0; i < n; i++) {
|
|
118
|
+
const c = pattern[i];
|
|
119
|
+
const t = this._nxt[s];
|
|
120
|
+
if (t === -1) {
|
|
121
|
+
// Terminal — place first transition as a singleton.
|
|
122
|
+
const ns = this._state();
|
|
123
|
+
this._nxt[s] = ns;
|
|
124
|
+
this._sym[s] = c;
|
|
125
|
+
s = ns;
|
|
126
|
+
}
|
|
127
|
+
else if (t >= 0) {
|
|
128
|
+
// Singleton — either advance on match, or expand to multi.
|
|
129
|
+
if (this._sym[s] === c) {
|
|
130
|
+
s = t;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const map = new Map();
|
|
134
|
+
map.set(this._sym[s], t);
|
|
135
|
+
const ns = this._state();
|
|
136
|
+
map.set(c, ns);
|
|
137
|
+
if (!this._multi)
|
|
138
|
+
this._multi = new Map();
|
|
139
|
+
this._multi.set(s, map);
|
|
140
|
+
this._nxt[s] = -2;
|
|
141
|
+
s = ns;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// Multi (t === -2) — extend the existing Map.
|
|
146
|
+
const map = this._multi.get(s);
|
|
147
|
+
let ns = map.get(c);
|
|
148
|
+
if (ns === undefined) {
|
|
149
|
+
ns = this._state();
|
|
150
|
+
map.set(c, ns);
|
|
151
|
+
}
|
|
152
|
+
s = ns;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (this._end[s] !== -1)
|
|
156
|
+
return this._end[s]; // already present
|
|
157
|
+
const id = this._lens.length;
|
|
158
|
+
this._lens.push(n);
|
|
159
|
+
this._vals.push(payload);
|
|
160
|
+
this._end[s] = id;
|
|
161
|
+
return id;
|
|
162
|
+
}
|
|
163
|
+
// ── matching ───────────────────────────────────────────────────────────
|
|
164
|
+
/**
|
|
165
|
+
* Every stored pattern that begins exactly at `pos` in `seq`, shortest first.
|
|
166
|
+
* Walks forward from the root in O(longest match); reports nothing about any
|
|
167
|
+
* other position. This is the on-demand probe the search uses.
|
|
168
|
+
*/
|
|
169
|
+
matchesAt(seq, pos) {
|
|
170
|
+
const out = [];
|
|
171
|
+
let s = ROOT;
|
|
172
|
+
for (let i = pos, n = seq.length; i < n; i++) {
|
|
173
|
+
s = this._follow(s, seq[i]);
|
|
174
|
+
if (s === -1)
|
|
175
|
+
break;
|
|
176
|
+
const id = this._end[s];
|
|
177
|
+
if (id !== -1) {
|
|
178
|
+
out.push({
|
|
179
|
+
start: pos,
|
|
180
|
+
end: i + 1,
|
|
181
|
+
length: this._lens[id],
|
|
182
|
+
id,
|
|
183
|
+
payload: this._vals[id],
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Every occurrence of every pattern anywhere in `seq` (eager form, the union
|
|
191
|
+
* of {@link matchesAt} over all start positions). O(seq · longest pattern),
|
|
192
|
+
* independent of how many patterns are stored. Use {@link matchesAt} when the
|
|
193
|
+
* search only needs the sites at a particular position.
|
|
194
|
+
*/
|
|
195
|
+
scan(seq) {
|
|
196
|
+
const out = [];
|
|
197
|
+
for (let pos = 0, n = seq.length; pos < n; pos++) {
|
|
198
|
+
let s = ROOT;
|
|
199
|
+
for (let i = pos; i < n; i++) {
|
|
200
|
+
s = this._follow(s, seq[i]);
|
|
201
|
+
if (s === -1)
|
|
202
|
+
break;
|
|
203
|
+
const id = this._end[s];
|
|
204
|
+
if (id !== -1) {
|
|
205
|
+
out.push({
|
|
206
|
+
start: pos,
|
|
207
|
+
end: i + 1,
|
|
208
|
+
length: this._lens[id],
|
|
209
|
+
id,
|
|
210
|
+
payload: this._vals[id],
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Self-contained tests for the `derive` library: the lightest-derivation
|
|
2
|
+
// engine (including a multi-premise *bridge* rule), the on-demand trie matcher,
|
|
3
|
+
// and the optimal cover. Uses node:test; no dependency on sema.
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { coverSequence, lightestDerivation, Trie, } from "../src/index.js";
|
|
7
|
+
// ── trie: on-demand matching, no length bound ──────────────────────────────
|
|
8
|
+
test("trie matchesAt reports forms beginning exactly at a position", () => {
|
|
9
|
+
const t = new Trie();
|
|
10
|
+
t.insert([1, 2], "ab");
|
|
11
|
+
t.insert([1, 2, 3], "abc");
|
|
12
|
+
t.insert([2, 3], "bc");
|
|
13
|
+
const at0 = t.matchesAt([1, 2, 3, 4], 0).map((m) => m.payload);
|
|
14
|
+
assert.deepEqual(at0, ["ab", "abc"]); // both forms starting at 0, shortest first
|
|
15
|
+
const at1 = t.matchesAt([1, 2, 3, 4], 1).map((m) => m.payload);
|
|
16
|
+
assert.deepEqual(at1, ["bc"]);
|
|
17
|
+
const at3 = t.matchesAt([1, 2, 3, 4], 3).map((m) => m.payload);
|
|
18
|
+
assert.deepEqual(at3, []); // nothing learned here → dead-ends immediately
|
|
19
|
+
});
|
|
20
|
+
test("trie scan finds every occurrence; duplicate inserts share an id", () => {
|
|
21
|
+
const t = new Trie();
|
|
22
|
+
const id1 = t.insert([7, 7], 1);
|
|
23
|
+
const id2 = t.insert([7, 7], 2); // same content
|
|
24
|
+
assert.equal(id1, id2);
|
|
25
|
+
assert.equal(t.size, 1);
|
|
26
|
+
const hits = t.scan([7, 7, 7]);
|
|
27
|
+
assert.equal(hits.length, 2); // [0,2) and [1,3)
|
|
28
|
+
});
|
|
29
|
+
// ── engine: Dijkstra/Knuth core with a bridge (multi-premise) rule ─────────
|
|
30
|
+
test("lightestDerivation solves a hypergraph with a bridge premise", () => {
|
|
31
|
+
// Items: "A", "B", "AB". Axioms A (3) and B (4). A binary bridge A ∧ B → AB
|
|
32
|
+
// at cost 1. The only derivation of AB costs 3 + 4 + 1 = 8.
|
|
33
|
+
const A = "A", B = "B", AB = "AB";
|
|
34
|
+
const system = {
|
|
35
|
+
key: (s) => s,
|
|
36
|
+
axioms: () => [{ item: A, cost: 3 }, { item: B, cost: 4 }],
|
|
37
|
+
isGoal: (s) => s === AB,
|
|
38
|
+
*rules(item) {
|
|
39
|
+
// Bridge fires from either premise; the engine waits until both are known.
|
|
40
|
+
if (item === A)
|
|
41
|
+
yield { premises: [A, B], conclusion: AB, cost: 1 };
|
|
42
|
+
if (item === B)
|
|
43
|
+
yield { premises: [A, B], conclusion: AB, cost: 1 };
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
const d = lightestDerivation(system);
|
|
47
|
+
assert.ok(d);
|
|
48
|
+
assert.equal(d.item, AB);
|
|
49
|
+
assert.equal(d.cost, 8);
|
|
50
|
+
assert.equal(d.premises.length, 2); // it really used the bridge
|
|
51
|
+
});
|
|
52
|
+
test("lightestDerivation picks the cheaper of competing derivations", () => {
|
|
53
|
+
// Two ways to reach the goal G from axiom S: S→G cost 10, or S→M→G cost 3+3.
|
|
54
|
+
const system = {
|
|
55
|
+
key: (s) => s,
|
|
56
|
+
axioms: () => [{ item: "S", cost: 0 }],
|
|
57
|
+
isGoal: (s) => s === "G",
|
|
58
|
+
*rules(item) {
|
|
59
|
+
if (item === "S") {
|
|
60
|
+
yield { premises: ["S"], conclusion: "G", cost: 10 };
|
|
61
|
+
yield { premises: ["S"], conclusion: "M", cost: 3 };
|
|
62
|
+
}
|
|
63
|
+
if (item === "M")
|
|
64
|
+
yield { premises: ["M"], conclusion: "G", cost: 3 };
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
const d = lightestDerivation(system);
|
|
68
|
+
assert.ok(d);
|
|
69
|
+
assert.equal(d.cost, 6); // via M, not the direct 10
|
|
70
|
+
assert.equal(d.rule?.premises[0], "M");
|
|
71
|
+
});
|
|
72
|
+
test("lightestDerivation returns null when the goal is unreachable", () => {
|
|
73
|
+
const system = {
|
|
74
|
+
key: (n) => "" + n,
|
|
75
|
+
axioms: () => [{ item: 0, cost: 0 }],
|
|
76
|
+
isGoal: (n) => n === 99,
|
|
77
|
+
*rules(n) {
|
|
78
|
+
if (n < 3)
|
|
79
|
+
yield { premises: [n], conclusion: n + 1, cost: 1 };
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
assert.equal(lightestDerivation(system), null);
|
|
83
|
+
});
|
|
84
|
+
// ── cover: optimal, not greedy ─────────────────────────────────────────────
|
|
85
|
+
test("coverSequence maximises coverage, beating greedy longest-match", () => {
|
|
86
|
+
// Over [0,4): one long span [1,4) (len 3) vs two short [0,2),[2,4) (cover 4).
|
|
87
|
+
// Greedy-longest takes [1,4) and covers 3; the optimal cover takes the pair.
|
|
88
|
+
const cover = coverSequence(4, [
|
|
89
|
+
{ start: 1, end: 4, payload: "long" },
|
|
90
|
+
{ start: 0, end: 2, payload: "left" },
|
|
91
|
+
{ start: 2, end: 4, payload: "right" },
|
|
92
|
+
]);
|
|
93
|
+
assert.equal(cover.covered, 4);
|
|
94
|
+
assert.equal(cover.uncovered, 0);
|
|
95
|
+
assert.deepEqual(cover.spans.map((s) => s.payload), ["left", "right"]);
|
|
96
|
+
});
|
|
97
|
+
test("coverSequence prefers fewer, longer spans on ties (unit weight)", () => {
|
|
98
|
+
// [0,3) covers the same as [0,1)+[1,3) but in one span → preferred.
|
|
99
|
+
const cover = coverSequence(3, [
|
|
100
|
+
{ start: 0, end: 3, payload: "whole" },
|
|
101
|
+
{ start: 0, end: 1, payload: "a" },
|
|
102
|
+
{ start: 1, end: 3, payload: "b" },
|
|
103
|
+
]);
|
|
104
|
+
assert.deepEqual(cover.spans.map((s) => s.payload), ["whole"]);
|
|
105
|
+
});
|
|
106
|
+
test("coverSequence leaves genuinely uncovered gaps uncovered", () => {
|
|
107
|
+
const cover = coverSequence(10, [
|
|
108
|
+
{ start: 0, end: 3, payload: "x" },
|
|
109
|
+
{ start: 6, end: 9, payload: "y" },
|
|
110
|
+
]);
|
|
111
|
+
assert.equal(cover.covered, 6);
|
|
112
|
+
assert.equal(cover.uncovered, 4);
|
|
113
|
+
assert.deepEqual(cover.spans.map((s) => s.payload), ["x", "y"]);
|
|
114
|
+
});
|
|
115
|
+
test("coverSequence honours weights when coverage ties", () => {
|
|
116
|
+
// Both single-span covers cover all 2 symbols; the lighter weight wins.
|
|
117
|
+
const cover = coverSequence(2, [
|
|
118
|
+
{ start: 0, end: 2, weight: 5, payload: "heavy" },
|
|
119
|
+
{ start: 0, end: 2, weight: 1, payload: "light" },
|
|
120
|
+
]);
|
|
121
|
+
assert.deepEqual(cover.spans.map((s) => s.payload), ["light"]);
|
|
122
|
+
});
|