bireactive 0.3.1 → 0.3.3

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 (95) hide show
  1. package/README.md +14 -7
  2. package/dist/automerge/doc-cell.d.ts +24 -11
  3. package/dist/automerge/doc-cell.js +19 -13
  4. package/dist/automerge/index.d.ts +3 -2
  5. package/dist/automerge/index.js +6 -5
  6. package/dist/automerge/reconcile.d.ts +5 -2
  7. package/dist/automerge/reconcile.js +73 -15
  8. package/dist/core/_counts.js +5 -12
  9. package/dist/core/cell.d.ts +3 -3
  10. package/dist/core/cell.js +6 -7
  11. package/dist/core/derived-geometry.js +4 -7
  12. package/dist/core/index.d.ts +3 -1
  13. package/dist/core/index.js +3 -1
  14. package/dist/core/lenses/aggregates.d.ts +42 -52
  15. package/dist/core/lenses/aggregates.js +225 -116
  16. package/dist/core/lenses/geometry.d.ts +22 -4
  17. package/dist/core/lenses/geometry.js +59 -27
  18. package/dist/core/lenses/index.d.ts +5 -6
  19. package/dist/core/lenses/index.js +5 -6
  20. package/dist/core/lenses/memory.js +4 -17
  21. package/dist/core/lenses/numerical.d.ts +100 -0
  22. package/dist/core/lenses/{typed-factor.js → numerical.js} +136 -34
  23. package/dist/core/lenses/point-cloud.d.ts +67 -0
  24. package/dist/core/lenses/{closed-form-policies.js → point-cloud.js} +218 -81
  25. package/dist/core/lenses/snap.d.ts +1 -1
  26. package/dist/core/lenses/snap.js +3 -10
  27. package/dist/core/lenses/text.d.ts +40 -0
  28. package/dist/core/lenses/text.js +202 -0
  29. package/dist/core/lifecycle.js +3 -6
  30. package/dist/core/linalg.js +5 -11
  31. package/dist/core/optic.js +10 -15
  32. package/dist/core/optics.js +4 -8
  33. package/dist/core/store.d.ts +1 -2
  34. package/dist/core/store.js +7 -15
  35. package/dist/core/traits.d.ts +4 -7
  36. package/dist/core/traits.js +8 -12
  37. package/dist/core/values/anchor.js +0 -4
  38. package/dist/core/values/arr.d.ts +110 -0
  39. package/dist/core/values/arr.js +336 -0
  40. package/dist/core/values/audio.d.ts +8 -9
  41. package/dist/core/values/audio.js +7 -23
  42. package/dist/core/values/bool.d.ts +11 -11
  43. package/dist/core/values/bool.js +12 -22
  44. package/dist/core/values/box.d.ts +15 -20
  45. package/dist/core/values/box.js +20 -33
  46. package/dist/core/values/canvas.d.ts +18 -25
  47. package/dist/core/values/canvas.js +17 -48
  48. package/dist/core/values/color.d.ts +5 -7
  49. package/dist/core/values/color.js +5 -11
  50. package/dist/core/values/field.d.ts +6 -7
  51. package/dist/core/values/field.js +10 -35
  52. package/dist/core/values/flags.d.ts +1 -2
  53. package/dist/core/values/flags.js +1 -17
  54. package/dist/core/values/gpu.d.ts +6 -10
  55. package/dist/core/values/gpu.js +8 -22
  56. package/dist/core/values/matrix.d.ts +2 -4
  57. package/dist/core/values/matrix.js +2 -12
  58. package/dist/core/values/num.d.ts +19 -28
  59. package/dist/core/values/num.js +23 -41
  60. package/dist/core/values/pose.d.ts +2 -4
  61. package/dist/core/values/pose.js +3 -12
  62. package/dist/core/values/range.d.ts +18 -26
  63. package/dist/core/values/range.js +22 -39
  64. package/dist/core/values/reg/ambiguity.d.ts +8 -0
  65. package/dist/core/values/reg/ambiguity.js +131 -0
  66. package/dist/core/values/reg/engine.d.ts +91 -0
  67. package/dist/core/values/reg/engine.js +373 -0
  68. package/dist/core/values/reg/nfa.d.ts +42 -0
  69. package/dist/core/values/reg/nfa.js +391 -0
  70. package/dist/core/values/reg/regex.d.ts +7 -0
  71. package/dist/core/values/reg/regex.js +318 -0
  72. package/dist/core/values/reg/types.d.ts +60 -0
  73. package/dist/core/values/reg/types.js +3 -0
  74. package/dist/core/values/reg.d.ts +250 -0
  75. package/dist/core/values/reg.js +649 -0
  76. package/dist/core/values/str.d.ts +16 -60
  77. package/dist/core/values/str.js +133 -315
  78. package/dist/core/values/template.js +1 -24
  79. package/dist/core/values/transform.d.ts +3 -5
  80. package/dist/core/values/transform.js +3 -12
  81. package/dist/core/values/tri.d.ts +9 -10
  82. package/dist/core/values/tri.js +9 -15
  83. package/dist/core/values/vec.d.ts +9 -24
  84. package/dist/core/values/vec.js +9 -64
  85. package/dist/index.d.ts +0 -11
  86. package/dist/index.js +1 -11
  87. package/package.json +17 -10
  88. package/dist/coll.d.ts +0 -74
  89. package/dist/coll.js +0 -210
  90. package/dist/core/lenses/closed-form-policies.d.ts +0 -57
  91. package/dist/core/lenses/decompositions.d.ts +0 -14
  92. package/dist/core/lenses/decompositions.js +0 -224
  93. package/dist/core/lenses/domain-aggregates.d.ts +0 -42
  94. package/dist/core/lenses/domain-aggregates.js +0 -245
  95. package/dist/core/lenses/typed-factor.d.ts +0 -40
@@ -0,0 +1,649 @@
1
+ import { Cell } from "../cell.js";
2
+ import { optic } from "../optic.js";
3
+ import { Arr } from "./arr.js";
4
+ import { concatAmbiguity, intersects } from "./reg/ambiguity.js";
5
+ import { accepts, alphabetOf, altAll, CharSet, chr, EPS, language, nullable as reNullable, seq as reSeq, star as reStar, seqAll, } from "./reg/engine.js";
6
+ import { compileProgram, parseValue, recognize } from "./reg/nfa.js";
7
+ import { compileRegex, RegError } from "./reg/regex.js";
8
+ import { Str } from "./str.js";
9
+ import { numCodec } from "./template.js";
10
+ const isSilent = (n) => n.kind === "lit";
11
+ // ── memoized structural views (toRe / program) ───────────────────────
12
+ const reMemo = new WeakMap();
13
+ const progMemo = new WeakMap();
14
+ /** The compiled Thompson program for a node (built once, then cached). */
15
+ function progOf(n) {
16
+ let p = progMemo.get(n);
17
+ if (p === undefined) {
18
+ p = compileProgram(n);
19
+ progMemo.set(n, p);
20
+ }
21
+ return p;
22
+ }
23
+ function reOf(n) {
24
+ let r = reMemo.get(n);
25
+ if (r === undefined) {
26
+ r = toRe(n);
27
+ reMemo.set(n, r);
28
+ }
29
+ return r;
30
+ }
31
+ // ── parse (get): linear PikeVM, whole-string ──────────────────────────
32
+ /** Parse `s` fully; `null` if it doesn't match. */
33
+ function parseNode(n, s, spans) {
34
+ const r = parseValue(n, progOf(n), s, spans);
35
+ return r === null ? null : r.val;
36
+ }
37
+ /** Whole-string match of a leaf's language — for validating a scalar write. */
38
+ function fullLeafMatch(leaf, s) {
39
+ return accepts(leaf.engine, s);
40
+ }
41
+ /** Does `n` parse all of `s`? General write-validation. */
42
+ function fullNodeMatch(n, s) {
43
+ return recognize(progOf(n), s);
44
+ }
45
+ // ── print (put) ──────────────────────────────────────────────────────
46
+ function printNode(n, val) {
47
+ switch (n.kind) {
48
+ case "lit":
49
+ return n.text;
50
+ case "copy":
51
+ return String(val ?? "");
52
+ case "of":
53
+ return n.codec.format(val);
54
+ case "seq": {
55
+ const vals = val ?? [];
56
+ let out = "";
57
+ let vi = 0;
58
+ for (const part of n.parts) {
59
+ if (isSilent(part))
60
+ out += printNode(part, null);
61
+ else
62
+ out += printNode(part, vals[vi++] ?? defaultVal(part));
63
+ }
64
+ return out;
65
+ }
66
+ case "alt": {
67
+ const a = val ?? { branch: 0, val: defaultVal(n.branches[0]) };
68
+ const branch = n.branches[a.branch] ?? n.branches[0];
69
+ return printNode(branch, a.val);
70
+ }
71
+ case "opt":
72
+ if (val === null || val === undefined)
73
+ return "";
74
+ return printNode(n.part, isSilent(n.part) ? null : val);
75
+ case "star": {
76
+ const sv = val ?? { items: [], seps: [] };
77
+ let out = "";
78
+ for (let k = 0; k < sv.items.length; k++) {
79
+ if (k > 0)
80
+ out += sv.seps[k - 1] ?? n.joiner;
81
+ out += printNode(n.part, sv.items[k]);
82
+ }
83
+ return out;
84
+ }
85
+ }
86
+ }
87
+ // ── totalization + nullability ───────────────────────────────────────
88
+ function defaultVal(n) {
89
+ switch (n.kind) {
90
+ case "lit":
91
+ return null;
92
+ case "copy":
93
+ return "";
94
+ case "of":
95
+ return (n.codec.parse("") ?? n.codec.parse("0") ?? null);
96
+ case "seq":
97
+ return n.parts.filter(p => !isSilent(p)).map(defaultVal);
98
+ case "alt":
99
+ return { branch: 0, val: defaultVal(n.branches[0]) };
100
+ case "opt":
101
+ return null;
102
+ case "star":
103
+ return { items: [], seps: [] };
104
+ }
105
+ }
106
+ function nullable(n) {
107
+ switch (n.kind) {
108
+ case "lit":
109
+ return n.text === "";
110
+ case "copy":
111
+ case "of":
112
+ return reNullable(n.engine);
113
+ case "seq":
114
+ return n.parts.every(nullable);
115
+ case "alt":
116
+ return n.branches.some(nullable);
117
+ case "opt":
118
+ return true;
119
+ case "star":
120
+ return n.sep === undefined && n.min === 0 ? true : nullable(n.part);
121
+ }
122
+ }
123
+ // ── unambiguity checks (thrown at construction) ──────────────────────
124
+ // Each combinator validates only its own new seams (children are already valid),
125
+ // deciding on the derivative automaton (see `reg/ambiguity.ts`). Each error
126
+ // names a concrete witness string that would parse two ways.
127
+ const quote = (s) => (s === "" ? '""' : JSON.stringify(s));
128
+ function checkSeq(parts) {
129
+ for (let i = 0; i < parts.length - 1; i++) {
130
+ const a = reOf(parts[i]);
131
+ const b = seqAll(parts.slice(i + 1).map(reOf));
132
+ const w = concatAmbiguity(a, b);
133
+ if (w !== null) {
134
+ throw new RegError(`Reg.seq: the boundary after part ${i} is ambiguous — ${quote(w)} splits two ways. Insert a lit() delimiter or use disjoint character classes.`);
135
+ }
136
+ }
137
+ }
138
+ function checkAlt(branches) {
139
+ for (let i = 0; i < branches.length; i++) {
140
+ for (let j = i + 1; j < branches.length; j++) {
141
+ const w = intersects(reOf(branches[i]), reOf(branches[j]));
142
+ if (w !== null) {
143
+ throw new RegError(`Reg.alt: branches ${i} and ${j} both match ${quote(w)} — make the branches' languages disjoint.`);
144
+ }
145
+ }
146
+ }
147
+ }
148
+ function checkOpt(part) {
149
+ if (nullable(part)) {
150
+ throw new RegError("Reg.opt: the element is itself nullable — present-vs-absent is ambiguous.");
151
+ }
152
+ }
153
+ function checkStar(part, sep) {
154
+ const e = reOf(part);
155
+ if (sep === undefined) {
156
+ if (nullable(part)) {
157
+ throw new RegError('Reg.star/plus: a nullable element with no separator iterates ambiguously — add a separator (e.g. star(lit(","))).');
158
+ }
159
+ const w = concatAmbiguity(e, reStar(e)); // element · element* unambiguous?
160
+ if (w !== null) {
161
+ throw new RegError(`Reg.star/plus: element boundaries are not self-delimiting — ${quote(w)} iterates two ways. Add a separator.`);
162
+ }
163
+ return;
164
+ }
165
+ if (nullable(sep)) {
166
+ throw new RegError("Reg.star/plus: a nullable separator cannot pin element boundaries.");
167
+ }
168
+ // Pattern is `E (S E)*`; check each seam: S·E, the (S·E) repetition, and the
169
+ // leading E · (S E)*.
170
+ const s = reOf(sep);
171
+ const se = reSeq(s, e);
172
+ const tail = reStar(se);
173
+ const w = concatAmbiguity(s, e) ?? concatAmbiguity(se, tail) ?? concatAmbiguity(e, reSeq(s, tail));
174
+ if (w !== null) {
175
+ throw new RegError(`Reg.star/plus: element and separator boundaries overlap — ${quote(w)} parses two ways.`);
176
+ }
177
+ }
178
+ // ── grammar → capture-free automaton (recognition) ────────────────────
179
+ function toRe(n) {
180
+ switch (n.kind) {
181
+ case "lit": {
182
+ const parts = [];
183
+ for (let i = 0; i < n.text.length; i++)
184
+ parts.push(chr(CharSet.char(n.text.charCodeAt(i))));
185
+ return seqAll(parts);
186
+ }
187
+ case "copy":
188
+ case "of":
189
+ return n.engine;
190
+ case "seq":
191
+ return seqAll(n.parts.map(toRe));
192
+ case "alt":
193
+ return altAll(n.branches.map(toRe));
194
+ case "opt":
195
+ return altAll([EPS, toRe(n.part)]);
196
+ case "star": {
197
+ const P = toRe(n.part);
198
+ const S = n.sep !== undefined ? toRe(n.sep) : EPS;
199
+ return altAll([EPS, reSeq(P, reStar(reSeq(S, P)))]);
200
+ }
201
+ }
202
+ }
203
+ /** The separator string to emit when inserting into a star: the literal text
204
+ * for a `lit`, otherwise the *shortest* string in the separator's language.
205
+ * A separator whose language is empty has no insertable member, so the star
206
+ * is rejected at construction rather than silently writing off-language text. */
207
+ function joinerFor(sep) {
208
+ if (sep === undefined)
209
+ return "";
210
+ if (sep.kind === "lit")
211
+ return sep.text;
212
+ const re = reOf(sep);
213
+ for (const w of language(re, [...alphabetOf(re)], 1024, 1))
214
+ return w;
215
+ throw new RegError("Reg.star/plus: the separator matches no string, so nothing can be inserted between elements.");
216
+ }
217
+ function collectCaptures(n, path, acc) {
218
+ switch (n.kind) {
219
+ case "copy":
220
+ case "of":
221
+ case "star":
222
+ if (n.name !== undefined)
223
+ acc.set(n.name, { node: n, path: path.slice() });
224
+ return;
225
+ case "seq": {
226
+ let vi = 0;
227
+ for (const part of n.parts) {
228
+ if (isSilent(part))
229
+ continue;
230
+ collectCaptures(part, [...path, { seq: vi }], acc);
231
+ vi++;
232
+ }
233
+ return;
234
+ }
235
+ case "opt":
236
+ collectCaptures(n.part, path, acc);
237
+ return;
238
+ case "alt": {
239
+ for (let b = 0; b < n.branches.length; b++) {
240
+ collectCaptures(n.branches[b], [...path, { alt: b }], acc);
241
+ }
242
+ return;
243
+ }
244
+ case "lit":
245
+ return;
246
+ }
247
+ }
248
+ /** Structural value equality, for the print-validate (PutGet) write check. */
249
+ function regEqual(a, b) {
250
+ return JSON.stringify(a) === JSON.stringify(b);
251
+ }
252
+ const isAltVal = (v) => v !== null && typeof v === "object" && "branch" in v;
253
+ function getAt(val, path) {
254
+ let v = val;
255
+ for (const step of path) {
256
+ if ("seq" in step)
257
+ v = v?.[step.seq] ?? null;
258
+ else if (isAltVal(v) && v.branch === step.alt)
259
+ v = v.val;
260
+ else
261
+ return null; // inactive alt branch
262
+ }
263
+ return v;
264
+ }
265
+ function setAt(val, path, next) {
266
+ if (path.length === 0)
267
+ return next;
268
+ const [head, ...rest] = path;
269
+ if ("seq" in head) {
270
+ const arr = (val ?? []).slice();
271
+ arr[head.seq] = setAt(arr[head.seq] ?? null, rest, next);
272
+ return arr;
273
+ }
274
+ // alt step: descend only into the active branch; otherwise the write is a no-op.
275
+ if (isAltVal(val) && val.branch === head.alt) {
276
+ return { branch: val.branch, val: setAt(val.val, rest, next) };
277
+ }
278
+ return val;
279
+ }
280
+ // ── the Reg class ────────────────────────────────────────────────────
281
+ /** An immutable bidirectional string-lens description. Build with the typed
282
+ * leaf builders and combinators, then `bind`/`view` onto a `Cell<string>`.
283
+ *
284
+ * The four type parameters are phantom: `V` is the parsed value, `N` whether
285
+ * it accepts "", and `F`/`L` the character classes its match can begin/end
286
+ * with. `F`/`L`/`N` drive the compile-time ambiguity checks; they have no
287
+ * runtime presence. */
288
+ export class Reg {
289
+ /** @internal */
290
+ root;
291
+ #lastParse = null;
292
+ /** @internal — use the static builders. */
293
+ constructor(root) {
294
+ this.root = root;
295
+ }
296
+ // ── leaf builders ─────────────────────────────────────────────────
297
+ /** A fixed delimiter: matched and printed, never surfaced as a value. */
298
+ static lit(text) {
299
+ return new Reg({ kind: "lit", text });
300
+ }
301
+ /** Text up to (but not including) the delimiter `c` — i.e. `[^c]*`. Nullable
302
+ * (an empty field is allowed); the natural companion of `star(lit(c))`. */
303
+ static until(c) {
304
+ const escaped = c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
305
+ return new Reg({
306
+ kind: "copy",
307
+ re: new RegExp(`[^${escaped}]*`),
308
+ engine: compileRegex(new RegExp(`[^${escaped}]*`)),
309
+ });
310
+ }
311
+ /** One or more digits, `\d+`, as a string. */
312
+ static digits() {
313
+ return new Reg({ kind: "copy", re: /\d+/, engine: compileRegex(/\d+/) });
314
+ }
315
+ /** One or more digits, decoded as a `number` (a quotient lens — leading
316
+ * zeros are not preserved). */
317
+ static int() {
318
+ return new Reg({
319
+ kind: "of",
320
+ re: /\d+/,
321
+ engine: compileRegex(/\d+/),
322
+ codec: numCodec(true),
323
+ });
324
+ }
325
+ /** One or more ASCII letters, `[A-Za-z]+`. */
326
+ static letters() {
327
+ return new Reg({ kind: "copy", re: /[A-Za-z]+/, engine: compileRegex(/[A-Za-z]+/) });
328
+ }
329
+ /** One or more word characters, `\w+` (letters, digits, underscore). */
330
+ static word() {
331
+ return new Reg({ kind: "copy", re: /\w+/, engine: compileRegex(/\w+/) });
332
+ }
333
+ /** The escape hatch: capture the text matched by an arbitrary regular `re`.
334
+ * Non-regular constructs (anchors, lookaround, backreferences) throw. The
335
+ * boundary is opaque to the type system (`AnyBound`), so adjacency can't be
336
+ * checked at compile time — the construction-time check still applies. */
337
+ static copy(re) {
338
+ return new Reg({ kind: "copy", re, engine: compileRegex(re) });
339
+ }
340
+ /** Typed escape hatch: `re` recognizes, `codec` decodes/encodes. */
341
+ static of(re, codec) {
342
+ return new Reg({
343
+ kind: "of",
344
+ re,
345
+ engine: compileRegex(re),
346
+ codec: codec,
347
+ });
348
+ }
349
+ // ── combinators ───────────────────────────────────────────────────
350
+ /** Unambiguous concatenation; every boundary is checked here and throws on
351
+ * ambiguity. (For compile-time adjacency checking, prefer the fluent
352
+ * `a.then(b).then(c)`, which validates each link.) */
353
+ static seq(...parts) {
354
+ const flat = flattenSeq(parts);
355
+ checkSeq(flat);
356
+ return new Reg({ kind: "seq", parts: flat });
357
+ }
358
+ /** Ordered union; branches must be first-disjoint (checked). */
359
+ static alt(...branches) {
360
+ const bs = branches.map(b => b.root);
361
+ checkAlt(bs);
362
+ return new Reg({ kind: "alt", branches: bs });
363
+ }
364
+ /** Optional (`part` or nothing). `part` must be non-nullable. The value is
365
+ * the inner value when present and `null` when absent; an optional with no
366
+ * value of its own (e.g. `lit(...).optional()`) records presence as `true`. */
367
+ static opt(part) {
368
+ checkOpt(part.root);
369
+ return new Reg({ kind: "opt", part: part.root });
370
+ }
371
+ /** `seq(this, ...next)`. A provably-overlapping boundary between `this` and
372
+ * the next part is a *type* error — so `a.then(b).then(c)` statically checks
373
+ * every link. Interior boundaries of a multi-arg call, and the `copy`/`of`
374
+ * escapes, are checked at construction (throws). */
375
+ then(...next) {
376
+ const flat = flattenSeq([this, ...next]);
377
+ checkSeq(flat);
378
+ return new Reg({ kind: "seq", parts: flat });
379
+ }
380
+ /** `alt(this, other)`. */
381
+ or(other) {
382
+ return Reg.alt(this, other);
383
+ }
384
+ /** `opt(this)`. Only available when `this` is non-nullable. */
385
+ optional() {
386
+ return Reg.opt(this);
387
+ }
388
+ /** Iterate zero-or-more, optionally separated by `sep`; binds to an `Arr`.
389
+ * A separated star is a *split* (≥1 piece, like `Str.split`). Pass
390
+ * `opts.key` for resourceful alignment across reorders. */
391
+ star(sep, opts = {}) {
392
+ const part = this.root;
393
+ const sepNode = sep === undefined ? undefined : sep.root;
394
+ checkStar(part, sepNode);
395
+ return new Reg({
396
+ kind: "star",
397
+ part,
398
+ sep: sepNode,
399
+ joiner: joinerFor(sepNode),
400
+ min: 0,
401
+ key: opts.key,
402
+ });
403
+ }
404
+ /** Iterate one-or-more (forbids the empty list when unseparated). */
405
+ plus(sep, opts = {}) {
406
+ const part = this.root;
407
+ const sepNode = sep === undefined ? undefined : sep.root;
408
+ checkStar(part, sepNode);
409
+ return new Reg({
410
+ kind: "star",
411
+ part,
412
+ sep: sepNode,
413
+ joiner: joinerFor(sepNode),
414
+ min: 1,
415
+ key: opts.key,
416
+ });
417
+ }
418
+ /** Name this capture so `bind` exposes it as a handle (`copy`/`of`/`star`). */
419
+ as(name) {
420
+ const r = this.root;
421
+ if (r.kind !== "copy" && r.kind !== "of" && r.kind !== "star") {
422
+ throw new RegError(`Reg.as: can only name copy/of/star, got "${r.kind}"`);
423
+ }
424
+ return new Reg({ ...r, name });
425
+ }
426
+ /** Attach a codec to a `copy` leaf, turning it into a typed `of` capture. */
427
+ map(codec) {
428
+ const r = this.root;
429
+ if (r.kind !== "copy")
430
+ throw new RegError(`Reg.map: only on a copy leaf, got "${r.kind}"`);
431
+ return new Reg({
432
+ kind: "of",
433
+ re: r.re,
434
+ engine: r.engine,
435
+ codec: codec,
436
+ name: r.name,
437
+ });
438
+ }
439
+ // ── pure parser / printer ────────────────────────────────────────
440
+ /** Parse `s` fully (must consume to the end); `null` if it doesn't match.
441
+ * Single-pass and linear. */
442
+ match(s) {
443
+ if (this.#lastParse !== null && this.#lastParse.s === s)
444
+ return this.#lastParse.v;
445
+ const v = parseNode(this.root, s);
446
+ this.#lastParse = { s, v };
447
+ return v;
448
+ }
449
+ /** Reflective print: render a value back to source text. */
450
+ print(v) {
451
+ return printNode(this.root, v);
452
+ }
453
+ /** Does `s` fully match? Linear. */
454
+ test(s) {
455
+ return recognize(progOf(this.root), s);
456
+ }
457
+ /** Source spans of each named capture, keyed by name — the `get`/`put`
458
+ * correspondence made visible. Empty if `s` doesn't fully match. */
459
+ spans(s) {
460
+ const m = new Map();
461
+ const r = parseValue(this.root, progOf(this.root), s, m);
462
+ return r !== null ? Object.fromEntries(m) : {};
463
+ }
464
+ // ── reactive binding ─────────────────────────────────────────────
465
+ /** This grammar as a first-class, composable `Optic<string, V>`: `get`
466
+ * parses (falling back to the default value off-language), `put` reprints
467
+ * and round-trip-guards (an off-language source or a non-round-tripping
468
+ * value leaves the source untouched). Drops straight into `compose(...)`
469
+ * and `cell.through(...)`, so it chains with `atKey`/`iso` and string
470
+ * lenses like `caseFold`. */
471
+ optic() {
472
+ const def = defaultVal(this.root);
473
+ return optic((s) => (this.match(s) ?? def), (v, s) => {
474
+ if (this.match(s) === null)
475
+ return s; // source off-language: don't clobber
476
+ const next = this.print(v);
477
+ return this.match(next) === null ? s : next; // print must round-trip
478
+ });
479
+ }
480
+ /** The whole abstract value as a writable lens over `source`. */
481
+ view(source) {
482
+ return source.through(this.optic());
483
+ }
484
+ bind(source, opts = {}) {
485
+ const captures = new Map();
486
+ collectCaptures(this.root, [], captures);
487
+ if (opts.schema !== undefined) {
488
+ for (const [name, kind] of Object.entries(opts.schema)) {
489
+ const cap = captures.get(name);
490
+ if (cap === undefined)
491
+ throw new RegError(`Reg.bind: schema names "${name}", which isn't a capture`);
492
+ const isArr = cap.node.kind === "star";
493
+ if ((kind === "arr") !== isArr) {
494
+ throw new RegError(`Reg.bind: schema says "${name}" is "${kind}" but it's a ${isArr ? "star" : "scalar"} capture`);
495
+ }
496
+ }
497
+ }
498
+ const def = defaultVal(this.root);
499
+ const out = {};
500
+ for (const [name, cap] of captures) {
501
+ out[name] =
502
+ cap.node.kind === "star"
503
+ ? this.#starHandle(source, cap, def)
504
+ : this.#scalarHandle(source, cap, def);
505
+ }
506
+ return out;
507
+ }
508
+ // ── internals ────────────────────────────────────────────────────
509
+ #scalarHandle(source, cap, def) {
510
+ const leaf = cap.node;
511
+ const path = cap.path;
512
+ return Str.lens(source, (s) => {
513
+ const v = getAt(this.match(s) ?? def, path);
514
+ return leaf.kind === "of" ? leaf.codec.format(v) : String(v ?? "");
515
+ }, (target, s) => {
516
+ if (!fullLeafMatch(leaf, target))
517
+ return s;
518
+ const decoded = leaf.kind === "of" ? leaf.codec.parse(target) : target;
519
+ if (decoded === undefined)
520
+ return s;
521
+ const base = this.match(s);
522
+ if (base === null)
523
+ return s;
524
+ const next = this.print(setAt(base, path, decoded));
525
+ const back = this.match(next);
526
+ if (back === null || !regEqual(getAt(back, path), decoded))
527
+ return s;
528
+ return next;
529
+ });
530
+ }
531
+ #starHandle(source, cap, def) {
532
+ const starNode = cap.node;
533
+ if (starNode.part.kind !== "copy" && starNode.part.kind !== "of") {
534
+ throw new RegError(`Reg.bind: named star "${starNode.name}" needs a copy/of element for an Arr handle (got "${starNode.part.kind}") — use view() for structured elements`);
535
+ }
536
+ const path = cap.path;
537
+ const self = this;
538
+ const readStar = (s) => getAt(self.match(s) ?? def, path) ?? { items: [], seps: [] };
539
+ const writeStar = (s, sv) => {
540
+ const base = self.match(s);
541
+ if (base === null)
542
+ return s;
543
+ const next = self.print(setAt(base, path, sv));
544
+ return self.match(next) === null ? s : next;
545
+ };
546
+ const keyFn = starNode.key;
547
+ const slotIdsOf = (items) => {
548
+ if (keyFn === undefined)
549
+ return items.map((_, i) => String(i));
550
+ const occ = new Map();
551
+ return items.map(it => {
552
+ const k = keyFn(String(it ?? ""));
553
+ const n = occ.get(k) ?? 0;
554
+ occ.set(k, n + 1);
555
+ return `${k}#${n}`;
556
+ });
557
+ };
558
+ const indexOfId = (items, id) => keyFn === undefined
559
+ ? Number(id) < items.length
560
+ ? Number(id)
561
+ : -1
562
+ : slotIdsOf(items).indexOf(id);
563
+ const segCache = new Map();
564
+ const idOfCell = new WeakMap();
565
+ const seg = (id) => {
566
+ let c = segCache.get(id);
567
+ if (c === undefined) {
568
+ c = Str.lens(source, (s) => {
569
+ const sv = readStar(s);
570
+ const idx = indexOfId(sv.items, id);
571
+ return idx < 0 ? "" : String(sv.items[idx] ?? "");
572
+ }, (target, s) => {
573
+ const sv = readStar(s);
574
+ const idx = indexOfId(sv.items, id);
575
+ if (idx < 0)
576
+ return s;
577
+ if (!fullNodeMatch(starNode.part, target))
578
+ return s;
579
+ const items = sv.items.slice();
580
+ items[idx] = target;
581
+ return writeStar(s, { items, seps: sv.seps });
582
+ });
583
+ segCache.set(id, c);
584
+ idOfCell.set(c, id);
585
+ }
586
+ return c;
587
+ };
588
+ const write = (sv) => {
589
+ source.value = writeStar(source.peek(), sv);
590
+ };
591
+ return Arr.fromSource(source, (s) => {
592
+ const items = readStar(s).items;
593
+ return slotIdsOf(items).map(seg);
594
+ }, {
595
+ insert: (v, at) => {
596
+ const text = v instanceof Cell ? v.value : v;
597
+ const sv = readStar(source.peek());
598
+ const items = sv.items.slice();
599
+ const seps = sv.seps.slice();
600
+ const idx = at == null || at > items.length ? items.length : Math.max(0, at);
601
+ items.splice(idx, 0, text);
602
+ if (items.length > 1)
603
+ seps.splice(Math.min(idx, seps.length), 0, starNode.joiner);
604
+ write({ items, seps });
605
+ return seg(slotIdsOf(items)[idx]);
606
+ },
607
+ remove: e => {
608
+ const sv = readStar(source.peek());
609
+ const id = idOfCell.get(e);
610
+ const idx = id === undefined ? -1 : indexOfId(sv.items, id);
611
+ if (idx < 0)
612
+ return;
613
+ const items = sv.items.slice();
614
+ const seps = sv.seps.slice();
615
+ items.splice(idx, 1);
616
+ if (seps.length > 0)
617
+ seps.splice(Math.min(idx, seps.length - 1), 1);
618
+ write({ items, seps });
619
+ },
620
+ moveBefore: (e, anchor) => {
621
+ const sv = readStar(source.peek());
622
+ const fromId = idOfCell.get(e);
623
+ const from = fromId === undefined ? -1 : indexOfId(sv.items, fromId);
624
+ if (from < 0)
625
+ return;
626
+ const items = sv.items.slice();
627
+ const [moved] = items.splice(from, 1);
628
+ const anchorId = anchor == null ? undefined : idOfCell.get(anchor);
629
+ const ai = anchorId === undefined ? -1 : indexOfId(sv.items, anchorId);
630
+ const at = ai < 0 ? items.length : ai > from ? ai - 1 : ai;
631
+ items.splice(at, 0, moved);
632
+ write({ items, seps: sv.seps });
633
+ },
634
+ });
635
+ }
636
+ }
637
+ /** Flatten nested seqs so `a.then(b).then(c)` and `seq(a,b,c)` agree (a flat
638
+ * tuple of visible values), and adjacency checks see real neighbours. */
639
+ function flattenSeq(parts) {
640
+ const flat = [];
641
+ for (const p of parts) {
642
+ const r = p.root;
643
+ if (r.kind === "seq")
644
+ flat.push(...r.parts);
645
+ else
646
+ flat.push(r);
647
+ }
648
+ return flat;
649
+ }