@hviana/sema 0.1.0

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.
Files changed (110) hide show
  1. package/AGENTS.md +470 -0
  2. package/AUTHORS.md +12 -0
  3. package/COMMERCIAL-LICENSE.md +19 -0
  4. package/CONTRIBUTING.md +15 -0
  5. package/HOW_IT_WORKS.md +4210 -0
  6. package/LICENSE.md +117 -0
  7. package/README.md +290 -0
  8. package/TRADEMARKS.md +19 -0
  9. package/example/demo.ts +46 -0
  10. package/example/train_base.ts +2675 -0
  11. package/index.html +893 -0
  12. package/package.json +25 -0
  13. package/src/alphabet.ts +34 -0
  14. package/src/alu/README.md +332 -0
  15. package/src/alu/src/alu.ts +541 -0
  16. package/src/alu/src/expr.ts +339 -0
  17. package/src/alu/src/index.ts +115 -0
  18. package/src/alu/src/kernel-arith.ts +377 -0
  19. package/src/alu/src/kernel-bits.ts +199 -0
  20. package/src/alu/src/kernel-logic.ts +102 -0
  21. package/src/alu/src/kernel-nd.ts +235 -0
  22. package/src/alu/src/kernel-numeric.ts +466 -0
  23. package/src/alu/src/operation.ts +344 -0
  24. package/src/alu/src/parser.ts +574 -0
  25. package/src/alu/src/resonance.ts +161 -0
  26. package/src/alu/src/text.ts +83 -0
  27. package/src/alu/src/value.ts +327 -0
  28. package/src/alu/test/alu.test.ts +1004 -0
  29. package/src/bytes.ts +62 -0
  30. package/src/config.ts +227 -0
  31. package/src/derive/README.md +295 -0
  32. package/src/derive/src/deduction.ts +263 -0
  33. package/src/derive/src/index.ts +24 -0
  34. package/src/derive/src/priority-queue.ts +70 -0
  35. package/src/derive/src/rewrite.ts +127 -0
  36. package/src/derive/src/trie.ts +242 -0
  37. package/src/derive/test/derive.test.ts +137 -0
  38. package/src/extension.ts +42 -0
  39. package/src/geometry.ts +511 -0
  40. package/src/index.ts +38 -0
  41. package/src/ingest-cache.ts +224 -0
  42. package/src/mind/articulation.ts +137 -0
  43. package/src/mind/attention.ts +1061 -0
  44. package/src/mind/canonical.ts +107 -0
  45. package/src/mind/graph-search.ts +1100 -0
  46. package/src/mind/index.ts +18 -0
  47. package/src/mind/junction.ts +347 -0
  48. package/src/mind/learning.ts +246 -0
  49. package/src/mind/match.ts +507 -0
  50. package/src/mind/mechanisms/alu.ts +33 -0
  51. package/src/mind/mechanisms/cast.ts +568 -0
  52. package/src/mind/mechanisms/confluence.ts +268 -0
  53. package/src/mind/mechanisms/cover.ts +248 -0
  54. package/src/mind/mechanisms/extraction.ts +422 -0
  55. package/src/mind/mechanisms/recall.ts +241 -0
  56. package/src/mind/mind.ts +483 -0
  57. package/src/mind/pipeline-mechanism.ts +326 -0
  58. package/src/mind/pipeline.ts +300 -0
  59. package/src/mind/primitives.ts +201 -0
  60. package/src/mind/rationale.ts +275 -0
  61. package/src/mind/reasoning.ts +198 -0
  62. package/src/mind/recognition.ts +234 -0
  63. package/src/mind/resonance.ts +0 -0
  64. package/src/mind/trace.ts +108 -0
  65. package/src/mind/traverse.ts +556 -0
  66. package/src/mind/types.ts +281 -0
  67. package/src/rabitq-hnsw/README.md +303 -0
  68. package/src/rabitq-hnsw/src/database.ts +492 -0
  69. package/src/rabitq-hnsw/src/heap.ts +90 -0
  70. package/src/rabitq-hnsw/src/hnsw.ts +514 -0
  71. package/src/rabitq-hnsw/src/index.ts +15 -0
  72. package/src/rabitq-hnsw/src/prng.ts +39 -0
  73. package/src/rabitq-hnsw/src/rabitq.ts +308 -0
  74. package/src/rabitq-hnsw/src/store.ts +994 -0
  75. package/src/rabitq-hnsw/test/hnsw.test.ts +1213 -0
  76. package/src/store-sqlite.ts +928 -0
  77. package/src/store.ts +2148 -0
  78. package/src/vec.ts +124 -0
  79. package/test/00-extract.test.mjs +151 -0
  80. package/test/01-floor.test.mjs +20 -0
  81. package/test/02-roundtrip.test.mjs +83 -0
  82. package/test/03-recall.test.mjs +98 -0
  83. package/test/04-think.test.mjs +627 -0
  84. package/test/05-concepts.test.mjs +84 -0
  85. package/test/08-storage.test.mjs +948 -0
  86. package/test/09-edges.test.mjs +65 -0
  87. package/test/11-universality.test.mjs +180 -0
  88. package/test/13-conversation.test.mjs +228 -0
  89. package/test/14-scaling.test.mjs +503 -0
  90. package/test/15-decomposition-gap.test.mjs +0 -0
  91. package/test/16-bridge.test.mjs +250 -0
  92. package/test/17-intelligence.test.mjs +240 -0
  93. package/test/18-alu.test.mjs +209 -0
  94. package/test/19-nd.test.mjs +254 -0
  95. package/test/20-stability.test.mjs +489 -0
  96. package/test/21-partial.test.mjs +158 -0
  97. package/test/22-multihop.test.mjs +130 -0
  98. package/test/23-rationale.test.mjs +316 -0
  99. package/test/24-generalization.test.mjs +416 -0
  100. package/test/25-embedding.test.mjs +75 -0
  101. package/test/26-cooperative.test.mjs +89 -0
  102. package/test/27-saturation-drop.test.mjs +637 -0
  103. package/test/28-unknowable.test.mjs +88 -0
  104. package/test/29-counterfactual.test.mjs +476 -0
  105. package/test/30-conflict-resolution.test.mjs +166 -0
  106. package/test/31-audit.test.mjs +288 -0
  107. package/test/32-confluence.test.mjs +173 -0
  108. package/test/33-multi-candidate.test.mjs +222 -0
  109. package/test/34-cross-region.test.mjs +252 -0
  110. package/tsconfig.json +16 -0
@@ -0,0 +1,161 @@
1
+ // resonance.ts — ALU's window onto meaning, and how the host pre-resolves it.
2
+ //
3
+ // ALU computes on bytes of any modality, but WHICH operation a byte span invokes
4
+ // — and what the opposite of a symbol is — are questions of MEANING, and only
5
+ // resonance can read meaning from an opaque span. Two behaviours need it, and
6
+ // BOTH are generic (any operation, any modality), not special cases:
7
+ //
8
+ // • operation recognition — the operation a query asks for is, in general, NOT
9
+ // literally spelled. "2+2" carries a "+", but "the integral of …", "the
10
+ // limit as …", "the derivative of …", or the analogous request expressed as
11
+ // an image / audio gesture carry NO operator symbol at all. The operation
12
+ // must be recognised from the span's GIST resonating with the operation's
13
+ // CONCEPT. This is the primary, generic recognition path; the literal
14
+ // symbol scan (alu.ts `scan`) is merely the easy special case layered under
15
+ // it. Recognition is by gist, so it is modality-agnostic: a "+" drawn, a
16
+ // "plus" spoken, and the byte "+" all resonate toward the same `add`
17
+ // concept.
18
+ //
19
+ // • the polymorphic inverse — the opposite of a SYMBOL (the bytes of a learnt
20
+ // form, any modality) is its RESONANT opposite, found in the resonance
21
+ // space, not by arithmetic. The inverse of a number is its negation; the
22
+ // inverse of a form is its antonym/complement.
23
+ //
24
+ // Both are ASYNC in SEMA (they hit the gist / halo index), but the
25
+ // lightest-derivation search is SYNCHRONOUS. So — exactly as src/mind/pipeline.ts hoists
26
+ // concept hops and connectors into Maps before the search and reads them
27
+ // synchronously inside the rules — ALU splits each capability in two:
28
+ //
29
+ // AluResonance (async) — what the HOST implements over its resonance index.
30
+ // Called only during pre-resolution.
31
+ // ResonanceSync (sync) — the pre-resolved snapshot the op callbacks / rules
32
+ // read (ResonanceSync is declared in operation.ts).
33
+ //
34
+ // {@link prefetchOpposites} and {@link prefetchRecognisedOps} turn the async side
35
+ // into synchronous lookups for a known set of spans; the host calls them during
36
+ // its pre-resolution pass. This file is the ENTIRE coupling surface to SEMA —
37
+ // ALU imports nothing from the mind or store; the host supplies a AluResonance
38
+ // built over its own perception + resonance (gist nearness to the operation
39
+ // vocabulary ALU exposes via `conceptForms`, and halo resonance for the
40
+ // opposite).
41
+
42
+ import type { ResonanceSync } from "./operation.js";
43
+
44
+ /** A labelled concept form: the canonical operation name and one of its
45
+ * surface forms, as bytes. The host resonates opaque spans against these to
46
+ * answer "which operation does this span MEAN?" (see parser.ts AluHost). */
47
+ export interface ConceptAnchor {
48
+ name: string;
49
+ form: Uint8Array;
50
+ }
51
+ import { latin1 } from "../../bytes.js";
52
+
53
+ /** The async resonance capability the host provides. Both methods may return
54
+ * null when resonance finds nothing above the host's own threshold — ALU then
55
+ * declines to guess (operation unrecognised, inverse left unchanged). This
56
+ * keeps synthesis graph-evidenced: ALU never invents meaning resonance did not
57
+ * supply. */
58
+ export interface AluResonance {
59
+ /** The canonical operation a span's MEANING names, found by resonance over the
60
+ * operation concepts — the GENERIC, modality-agnostic recognition path. This
61
+ * is how an operation that is NOT literally spelled (no "∫" for an integral,
62
+ * no "+" for a sum drawn or spoken) is still recognised: the span's gist
63
+ * resonates with the operation's concept. Returns the canonical op name, or
64
+ * null when nothing resonates above threshold. */
65
+ recogniseOp(bytes: Uint8Array): Promise<string | null>;
66
+ /** The resonant opposite of a symbol's bytes (its antonym / inverse in the
67
+ * resonance space, any modality), or null. The host finds it by halo
68
+ * resonance over the negated concept. */
69
+ opposite(bytes: Uint8Array): Promise<Uint8Array | null>;
70
+ }
71
+
72
+ /** A AluResonance that knows nothing — the default when the host wires none in,
73
+ * so the kernel runs fully decoupled (operations resolve by literal forms only,
74
+ * and a symbol inverse is left unchanged). */
75
+ export const NO_ALU_RESONANCE: AluResonance = {
76
+ recogniseOp: async () => null,
77
+ opposite: async () => null,
78
+ };
79
+
80
+ /** Pre-resolve the resonant opposite of each symbol in `symbols`, awaiting the
81
+ * host's async resonance once per distinct span, and return a SYNCHRONOUS
82
+ * snapshot the op callbacks read during the search. Keyed by the latin1 view
83
+ * of the bytes (a lossless, stable string key — the same device the search's
84
+ * chart uses). This is the async→sync bridge for the polymorphic inverse,
85
+ * mirroring how a concept hop's target is pre-resolved and read synchronously. */
86
+ export async function prefetchOpposites(
87
+ resonance: AluResonance,
88
+ symbols: Iterable<Uint8Array>,
89
+ ): Promise<ResonanceSync> {
90
+ const table = new Map<string, Uint8Array>();
91
+ const seen = new Set<string>();
92
+ for (const bytes of symbols) {
93
+ const key = latin1(bytes);
94
+ if (seen.has(key)) continue;
95
+ seen.add(key);
96
+ const opp = await resonance.opposite(bytes);
97
+ if (opp) table.set(key, opp);
98
+ }
99
+ return {
100
+ opposite: (bytes: Uint8Array) => table.get(latin1(bytes)) ?? null,
101
+ // Opposites-only prefetch carries no op recognition; a higher-order nd op
102
+ // resolving through this falls back to literal forms / canonical names.
103
+ recogniseOp: () => null,
104
+ };
105
+ }
106
+
107
+ /** Pre-resolve BOTH capabilities a computation may need synchronously — the
108
+ * resonant opposite of a symbol (for the polymorphic inverse) AND the operation
109
+ * a symbol's MEANING names (for a higher-order nd op's function argument) — over
110
+ * one set of candidate spans, and return a single {@link ResonanceSync}. Each
111
+ * distinct span is awaited once per capability. This is the unified async→sync
112
+ * bridge the host uses for an nd computation, where the same spans may serve as
113
+ * inverse operands and as a reduce/map/filter operator. Either lookup yields
114
+ * null for a span that resonates with nothing, so callbacks decline rather than
115
+ * guess. */
116
+ export async function prefetchResonance(
117
+ resonance: AluResonance,
118
+ spans: Iterable<Uint8Array>,
119
+ ): Promise<ResonanceSync> {
120
+ const opposites = new Map<string, Uint8Array>();
121
+ const ops = new Map<string, string>();
122
+ const seen = new Set<string>();
123
+ for (const bytes of spans) {
124
+ const key = latin1(bytes);
125
+ if (seen.has(key)) continue;
126
+ seen.add(key);
127
+ const [opp, op] = await Promise.all([
128
+ resonance.opposite(bytes),
129
+ resonance.recogniseOp(bytes),
130
+ ]);
131
+ if (opp) opposites.set(key, opp);
132
+ if (op) ops.set(key, op);
133
+ }
134
+ return {
135
+ opposite: (bytes: Uint8Array) => opposites.get(latin1(bytes)) ?? null,
136
+ recogniseOp: (bytes: Uint8Array) => ops.get(latin1(bytes)) ?? null,
137
+ };
138
+ }
139
+
140
+ /** Pre-resolve, for each candidate span, the operation its MEANING names —
141
+ * awaiting the host's async gist resonance once per distinct span — and return
142
+ * a synchronous map keyed by the span's latin1 bytes → canonical op name. This
143
+ * is the async→sync bridge for the GENERIC operation-recognition path: a span
144
+ * the literal scan could not classify (no operator symbol) but whose gist
145
+ * resonates with an operation's concept. Spans that resonate with nothing are
146
+ * omitted, so the caller treats a miss as "not an operation". */
147
+ export async function prefetchRecognisedOps(
148
+ resonance: AluResonance,
149
+ spans: Iterable<Uint8Array>,
150
+ ): Promise<Map<string, string>> {
151
+ const table = new Map<string, string>();
152
+ const seen = new Set<string>();
153
+ for (const bytes of spans) {
154
+ const key = latin1(bytes);
155
+ if (seen.has(key)) continue;
156
+ seen.add(key);
157
+ const op = await resonance.recogniseOp(bytes);
158
+ if (op) table.set(key, op);
159
+ }
160
+ return table;
161
+ }
@@ -0,0 +1,83 @@
1
+ // text.ts — the byte-class floor the ALU's lexing rests on.
2
+ //
3
+ // SEMA is byte-native and multimodal; the ALU keeps its knowledge of raw text
4
+ // as small as possible. What remains is the IRREDUCIBLE floor — the decimal
5
+ // numeral grammar (digits ground a quantity exactly as the byte alphabet
6
+ // grounds a symbol), whitespace as the universal spacing byte, and the list
7
+ // grammar's delimiters — collected HERE, once, so no other module hardcodes a
8
+ // character class of its own. Everything above this floor (which spans name
9
+ // operations, where terms begin and end, what an expression means) is resolved
10
+ // from the operation registry, the host's geometric segmentation, or resonance.
11
+
12
+ /** An ASCII decimal digit. */
13
+ export const isDigitByte = (c: number): boolean => c >= 0x30 && c <= 0x39;
14
+
15
+ /** The decimal point of a numeral. */
16
+ export const DOT = 0x2e; // "."
17
+
18
+ /** The explicit list container's delimiters — the canonical nd spelling the
19
+ * codec emits, so a computed list feeds straight back in as an operand. */
20
+ export const OPEN = 0x5b; // "["
21
+ export const CLOSE = 0x5d; // "]"
22
+
23
+ /** ASCII whitespace (space, tab, newline, carriage return) — the one spacing
24
+ * convention bytes of every textual modality share. */
25
+ export const isSpaceByte = (c: number): boolean =>
26
+ c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d;
27
+
28
+ /** A byte that DELIMITS list elements inside an explicit container: whitespace,
29
+ * a comma, or a semicolon. Deliberately excludes anything that can sit inside
30
+ * a numeral or a symbol token (letters, digits, sign/decimal `+ - .`) and the
31
+ * brackets themselves, so an element's own bytes are never split. */
32
+ export const isSepByte = (c: number): boolean =>
33
+ isSpaceByte(c) || c === 0x2c /* , */ || c === 0x3b /* ; */;
34
+
35
+ /** The index of the first non-whitespace byte (or the length, if all space). */
36
+ export function trimStart(b: Uint8Array): number {
37
+ let i = 0;
38
+ while (i < b.length && isSpaceByte(b[i])) i++;
39
+ return i;
40
+ }
41
+
42
+ /** The index one past the last non-whitespace byte (or 0, if all space). */
43
+ export function trimEnd(b: Uint8Array): number {
44
+ let j = b.length;
45
+ const lo = trimStart(b);
46
+ while (j > lo && isSpaceByte(b[j - 1])) j--;
47
+ return j;
48
+ }
49
+
50
+ /** The index of the `]` matching the `[` at `open`, bracket-depth aware, or -1
51
+ * when it is unbalanced — used to test whether a span is one explicit list
52
+ * CONTAINER (its opening bracket closing exactly at its end). */
53
+ export function matchBracket(b: Uint8Array, open: number): number {
54
+ let depth = 0;
55
+ for (let i = open; i < b.length; i++) {
56
+ if (b[i] === OPEN) depth++;
57
+ else if (b[i] === CLOSE && --depth === 0) return i;
58
+ }
59
+ return -1;
60
+ }
61
+
62
+ /** The maximal non-whitespace runs of `bytes` within [from, to) — the spacing
63
+ * floor's reading of "the tokens", shared by the structural host's segmenter
64
+ * and the parser's term lexing so neither re-implements the walk. */
65
+ export function nonSpaceRuns(
66
+ bytes: Uint8Array,
67
+ from = 0,
68
+ to = bytes.length,
69
+ ): Array<{ i: number; j: number }> {
70
+ const out: Array<{ i: number; j: number }> = [];
71
+ let i = from;
72
+ while (i < to) {
73
+ if (isSpaceByte(bytes[i])) {
74
+ i++;
75
+ continue;
76
+ }
77
+ let j = i;
78
+ while (j < to && !isSpaceByte(bytes[j])) j++;
79
+ out.push({ i, j });
80
+ i = j;
81
+ }
82
+ return out;
83
+ }
@@ -0,0 +1,327 @@
1
+ // value.ts — the ALU value model.
2
+ //
3
+ // One tagged union carries every kind of quantity the ALU computes on, so a
4
+ // single Operation registry can dispatch on the tag rather than maintaining
5
+ // disjoint kernels. The four domains are exactly the strata the kernel needs:
6
+ //
7
+ // • bit — a logic value 0|1. The completeness layer (nand and the gates
8
+ // derived from it) lives here.
9
+ // • int — an EXACT integer (JS bigint). The "everything derives from nand"
10
+ // bootstrap runs here: add is a ripple of full_adders, multiply is
11
+ // shift-add, and so on. bigint keeps the proof exact past 2^53.
12
+ // • real — an IEEE double. The limit layer (converge, exp, sin, …) is
13
+ // intrinsically continuous, so it runs on the host's native reals
14
+ // rather than a fixed-point reimplementation atop bits.
15
+ // • symbol — a raw BYTE SPAN that is not a number. SEMA is byte-native and
16
+ // multimodal: a symbol's bytes may name a written form, a region of
17
+ // an image, a fragment of audio — any learned form, in any
18
+ // modality. ALU never interprets those bytes; it treats a symbol
19
+ // as an opaque token. This is the carrier for the POLYMORPHIC
20
+ // inverse: the inverse of a number is its negation, but the inverse
21
+ // of a symbol is its RESONANT OPPOSITE — found in the resonance
22
+ // space, not by arithmetic (e.g. the bytes of "large" resolve to
23
+ // "small", but the mechanism is modality-agnostic — see
24
+ // resonance.ts). Resonance is the only thing that can read meaning
25
+ // from a symbol, and ALU reaches it only through an injected
26
+ // capability, never directly.
27
+ //
28
+ // • nd — an N-DIMENSIONAL value: an ordered list of elements, each of which
29
+ // is ITSELF a Value of ANY domain — a scalar (bit/int/real/symbol)
30
+ // OR another nd. So nd is recursive (a tensor is an nd of nds), it
31
+ // is RAGGED (rows need not be equal length — it is a list, not a
32
+ // rectangular array), and it is HETEROGENEOUS (a row may mix a
33
+ // number, a symbol, and a sub-list). This one recursive case is the
34
+ // whole generalisation: there is no separate "vector" vs "matrix"
35
+ // type, only nd nesting, and the rank is read off the nesting depth.
36
+ // Every scalar operation lifts over it automatically (broadcast, see
37
+ // operation.ts), and the structural operations (the nd kernel) build
38
+ // the list-processing layer — map/reduce/filter/find/… — on a tiny
39
+ // core of nd/length/at, exactly as logic builds on nand.
40
+ //
41
+ // The module is pure: it knows nothing of SEMA, the store, or the search. The
42
+ // only host coupling is the injected ValueCodec, whose default lives here.
43
+
44
+ /** Which stratum a {@link Value} belongs to. Four scalar domains + the
45
+ * recursive container `nd`. */
46
+ export type Domain = "bit" | "int" | "real" | "symbol" | "nd";
47
+
48
+ /** A quantity the ALU computes on — a tagged union over the scalar domains plus
49
+ * the recursive n-dimensional container. An `nd`'s `items` are themselves
50
+ * Values of any domain, so nesting (a matrix = an nd of nd) and heterogeneity
51
+ * (a list mixing numbers, symbols, sub-lists) are the same case. */
52
+ export type Value =
53
+ | { domain: "bit"; b: 0 | 1 }
54
+ | { domain: "int"; n: bigint }
55
+ | { domain: "real"; x: number }
56
+ | { domain: "symbol"; bytes: Uint8Array }
57
+ | { domain: "nd"; items: Value[] };
58
+
59
+ // ── constructors ──────────────────────────────────────────────────────────
60
+
61
+ export const bit = (b: 0 | 1): Value => ({ domain: "bit", b });
62
+ export const int = (n: bigint): Value => ({ domain: "int", n });
63
+ export const real = (x: number): Value => ({ domain: "real", x });
64
+ /** A symbolic value: an opaque byte span of any modality (see module note). */
65
+ export const symbol = (bytes: Uint8Array): Value => ({
66
+ domain: "symbol",
67
+ bytes,
68
+ });
69
+ /** An n-dimensional value: an ordered list of element values (each any domain,
70
+ * including a nested nd). See the module note on nd. */
71
+ export const nd = (items: Value[]): Value => ({ domain: "nd", items });
72
+
73
+ /** The domain tag of a value. */
74
+ export const tagOf = (v: Value): Domain => v.domain;
75
+
76
+ /** Whether a value is a numeric SCALAR (bit, int, or real) — an nd is not, even
77
+ * if every element is numeric (it is a container; reduce it to a scalar). */
78
+ export function isNumeric(v: Value): boolean {
79
+ return v.domain === "bit" || v.domain === "int" || v.domain === "real";
80
+ }
81
+
82
+ /** Whether a value is the n-dimensional container. */
83
+ export function isNd(v: Value): v is { domain: "nd"; items: Value[] } {
84
+ return v.domain === "nd";
85
+ }
86
+
87
+ /** Collect, into `out`, every SYMBOL byte span reachable inside a value — itself
88
+ * when it is a symbol, or each element recursively when it is an nd. These are
89
+ * the spans whose MEANING (a resonant opposite, the operation a higher-order op
90
+ * argument names) the host pre-resolves before a computation; numbers carry no
91
+ * meaning to resolve, so they are skipped. */
92
+ export function symbolSpans(v: Value, out: Uint8Array[]): void {
93
+ if (v.domain === "symbol") out.push(v.bytes);
94
+ else if (isNd(v)) { for (const e of v.items) symbolSpans(e, out); }
95
+ }
96
+
97
+ // ── coercions (the functor's object map between domains) ────────────────────
98
+
99
+ /** A value as a JS number — bit→0|1, int→Number, real as-is. Throws on a
100
+ * symbol, because arithmetic on an opaque form is a programming error the
101
+ * caller (or the graph adapter's try/catch) should surface, never silently
102
+ * coerce to NaN. */
103
+ export function asReal(v: Value): number {
104
+ switch (v.domain) {
105
+ case "bit":
106
+ return v.b;
107
+ case "int":
108
+ return Number(v.n);
109
+ case "real":
110
+ return v.x;
111
+ case "symbol":
112
+ throw new TypeError("asReal: a symbol value has no numeric reading");
113
+ case "nd":
114
+ // An nd has no scalar reading: a scalar primitive never sees one, because
115
+ // broadcast (operation.ts) lifts it element-wise first. Reaching here
116
+ // means a structural op was mis-called on a list as if it were a scalar.
117
+ throw new TypeError("asReal: an nd value has no scalar numeric reading");
118
+ }
119
+ }
120
+
121
+ /** A value as an exact bigint — real is truncated toward zero (a real that is
122
+ * not integral loses its fraction; callers wanting exactness keep it int). */
123
+ export function asInt(v: Value): bigint {
124
+ switch (v.domain) {
125
+ case "bit":
126
+ return BigInt(v.b);
127
+ case "int":
128
+ return v.n;
129
+ case "real":
130
+ return BigInt(Math.trunc(v.x));
131
+ case "symbol":
132
+ throw new TypeError("asInt: a symbol value has no integer reading");
133
+ case "nd":
134
+ throw new TypeError("asInt: an nd value has no scalar integer reading");
135
+ }
136
+ }
137
+
138
+ /** A value as a logic bit — nonzero numbers read as 1, zero as 0. Throws on a
139
+ * symbol (an opaque form is not a truth value). */
140
+ export function asBit(v: Value): 0 | 1 {
141
+ switch (v.domain) {
142
+ case "bit":
143
+ return v.b;
144
+ case "int":
145
+ return v.n === 0n ? 0 : 1;
146
+ case "real":
147
+ return v.x === 0 ? 0 : 1;
148
+ case "symbol":
149
+ throw new TypeError("asBit: a symbol value is not a truth value");
150
+ case "nd":
151
+ throw new TypeError("asBit: an nd value is not a scalar truth value");
152
+ }
153
+ }
154
+
155
+ /** Promote a value into a target numeric domain (bit ⊂ int ⊂ real). Used when
156
+ * a mixed-domain expression must agree on one domain before a primitive runs —
157
+ * the rule is the natural lattice: any real operand pulls the whole expression
158
+ * to real, otherwise any int pulls it to int, otherwise bit. */
159
+ export function coerce(v: Value, domain: "bit" | "int" | "real"): Value {
160
+ if (v.domain === domain) return v;
161
+ switch (domain) {
162
+ case "bit":
163
+ return bit(asBit(v));
164
+ case "int":
165
+ return int(asInt(v));
166
+ case "real":
167
+ return real(asReal(v));
168
+ }
169
+ }
170
+
171
+ /** The common numeric domain of a list of operands, by the bit ⊂ int ⊂ real
172
+ * lattice: real if any operand is real, else int if any is int, else bit.
173
+ * Throws if any operand is a symbol. This is how a polymorphic arithmetic op
174
+ * decides whether to take the exact bigint path or the native-double path. */
175
+ export function joinDomain(args: Value[]): "bit" | "int" | "real" {
176
+ let d: "bit" | "int" | "real" = "bit";
177
+ for (const a of args) {
178
+ if (a.domain === "symbol") {
179
+ throw new TypeError("joinDomain: cannot join a symbol value numerically");
180
+ }
181
+ if (a.domain === "nd") {
182
+ throw new TypeError("joinDomain: cannot join an nd value numerically");
183
+ }
184
+ if (a.domain === "real") return "real";
185
+ if (a.domain === "int" && d === "bit") d = "int";
186
+ }
187
+ return d;
188
+ }
189
+
190
+ // ── the byte ⇄ value codec ──────────────────────────────────────────────────
191
+
192
+ /** The host's bridge between byte spans and values. ALU emits and consumes
193
+ * bytes only through this, so the surrounding system owns how a number is
194
+ * spelled. A default ({@link decimalCodec}) is provided. */
195
+ export interface ValueCodec {
196
+ encode(v: Value): Uint8Array;
197
+ decode(bytes: Uint8Array): Value | null;
198
+ }
199
+
200
+ const ASCII = {
201
+ decode: (b: Uint8Array): string => {
202
+ let s = "";
203
+ for (let i = 0; i < b.length; i++) s += String.fromCharCode(b[i]);
204
+ return s;
205
+ },
206
+ encode: (s: string): Uint8Array => {
207
+ const out = new Uint8Array(s.length);
208
+ for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 0xff;
209
+ return out;
210
+ },
211
+ };
212
+
213
+ /** A decimal numeral, sign, optional fraction, optional exponent — the syntax
214
+ * {@link parseScalar} accepts as a number. Anything else is a symbol. */
215
+ const NUMERAL = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
216
+
217
+ /** Concatenate byte arrays — used to build an nd literal's encoding without
218
+ * routing a (possibly non-ASCII, multimodal) symbol's bytes through a string. */
219
+ function concatBytes(arrs: Uint8Array[]): Uint8Array {
220
+ let len = 0;
221
+ for (const a of arrs) len += a.length;
222
+ const out = new Uint8Array(len);
223
+ let o = 0;
224
+ for (const a of arrs) {
225
+ out.set(a, o);
226
+ o += a.length;
227
+ }
228
+ return out;
229
+ }
230
+
231
+ /** Parse a SCALAR span: a clean integer numeral → exact int, a fractional/
232
+ * exponent numeral → real, anything else → a symbol carrying `bytes` verbatim.
233
+ * Strict, so it never hijacks a form that merely begins with a digit. */
234
+ function parseScalar(bytes: Uint8Array, s: string): Value {
235
+ if (s.length > 0 && NUMERAL.test(s)) {
236
+ if (/[.eE]/.test(s)) {
237
+ const x = Number(s);
238
+ if (Number.isFinite(x)) return real(x);
239
+ } else {
240
+ // A clean integer: strip a leading '+' that BigInt rejects.
241
+ try {
242
+ return int(BigInt(s[0] === "+" ? s.slice(1) : s));
243
+ } catch {
244
+ /* fall through to symbol */
245
+ }
246
+ }
247
+ }
248
+ return symbol(bytes);
249
+ }
250
+
251
+ /** Parse a byte span into a SCALAR value: a clean integer numeral → exact int, a
252
+ * fractional/exponent numeral → real, anything else → a symbol carrying its
253
+ * bytes verbatim for the polymorphic inverse.
254
+ *
255
+ * This is the IRREDUCIBLE FLOOR — and the ONLY structure read at the value
256
+ * model's base: a number-form's digits ground a quantity, the exact analogue of
257
+ * the byte alphabet grounding a symbol. It does NOT read n-dimensional
258
+ * STRUCTURE (no bracket/comma grammar); recognising a list — a run of element
259
+ * values joined by a consistent separator — is layered ONE level up, in the
260
+ * facade's {@link "./alu.js".Alu.recogniseValue} (over the operand scanner),
261
+ * which calls this for each element. So the codec's `decode` (which delegates
262
+ * here) is scalar-only: a bracket literal decodes to an opaque symbol.
263
+ *
264
+ * Numerals are ASCII by construction; for any other byte content the bytes are
265
+ * preserved opaquely, so a symbol of any modality round-trips untouched. */
266
+ export function parseValue(bytes: Uint8Array): Value {
267
+ return parseScalar(bytes, ASCII.decode(bytes));
268
+ }
269
+
270
+ /** Canonically format a real as a decimal string: round to `precision` places,
271
+ * then trim trailing zeros (and a bare trailing point), normalising -0 to 0.
272
+ * Determinism here is load-bearing — the search keys an output span by its
273
+ * bytes, so two derivations of the same number MUST spell it identically. */
274
+ export function formatReal(x: number, precision: number): string {
275
+ if (!Number.isFinite(x)) return String(x); // "Infinity" / "NaN" / "-Infinity"
276
+ let s = x.toFixed(precision);
277
+ if (s.indexOf(".") >= 0) s = s.replace(/\.?0+$/, "");
278
+ if (s === "-0" || s === "") s = "0";
279
+ return s;
280
+ }
281
+
282
+ /** The default codec: integers as exact decimals, reals canonically formatted,
283
+ * bits as "0"/"1", symbols as their own bytes (verbatim, any modality), and an
284
+ * nd as a bracket literal `[e0,e1,…]` of its element encodings (recursive, so a
285
+ * nested list nests its brackets and a symbol element keeps its raw bytes).
286
+ * `precision` controls real rounding (see {@link formatReal}).
287
+ *
288
+ * The bracket layout is the CANONICAL OUTPUT spelling of an `nd` — one
289
+ * deterministic form, the analogue of decimal for a number, so the search's
290
+ * chart memoises identical results identically. It is an ENCODE-only grammar:
291
+ * {@link parseValue} (hence `decode`) reads only scalars, never structure — a
292
+ * bracket literal decodes back to an opaque symbol. Reading list STRUCTURE from
293
+ * bytes is layered one level up, in {@link "./alu.js".Alu.recogniseValue}. */
294
+ export function decimalCodec(precision: number): ValueCodec {
295
+ const enc = (v: Value): Uint8Array => {
296
+ switch (v.domain) {
297
+ case "bit":
298
+ return ASCII.encode(String(v.b));
299
+ case "int":
300
+ return ASCII.encode(v.n.toString());
301
+ case "real":
302
+ return ASCII.encode(formatReal(v.x, precision));
303
+ case "symbol":
304
+ return v.bytes;
305
+ case "nd": {
306
+ // [e0,e1,…] at the byte level, so a symbol element's arbitrary
307
+ // (possibly non-ASCII / multimodal) bytes pass through untouched.
308
+ const OPEN = ASCII.encode("[");
309
+ const CLOSE = ASCII.encode("]");
310
+ const COMMA = ASCII.encode(",");
311
+ const parts: Uint8Array[] = [OPEN];
312
+ for (let i = 0; i < v.items.length; i++) {
313
+ if (i > 0) parts.push(COMMA);
314
+ parts.push(enc(v.items[i]));
315
+ }
316
+ parts.push(CLOSE);
317
+ return concatBytes(parts);
318
+ }
319
+ }
320
+ };
321
+ return {
322
+ encode: enc,
323
+ decode(bytes: Uint8Array): Value | null {
324
+ return parseValue(bytes);
325
+ },
326
+ };
327
+ }