@bcts/envelope-pattern 1.0.0-alpha.23 → 1.0.0-beta.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 (59) hide show
  1. package/dist/index.cjs +1302 -766
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +101 -59
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.mts +102 -60
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.iife.js +1299 -763
  8. package/dist/index.iife.js.map +1 -1
  9. package/dist/index.mjs +1299 -766
  10. package/dist/index.mjs.map +1 -1
  11. package/package.json +8 -6
  12. package/src/format.ts +19 -31
  13. package/src/parse/index.ts +16 -1009
  14. package/src/parse/leaf/array-parser.ts +36 -0
  15. package/src/parse/leaf/cbor-parser.ts +43 -0
  16. package/src/parse/leaf/date-parser.ts +81 -0
  17. package/src/parse/leaf/known-value-parser.ts +73 -0
  18. package/src/parse/leaf/null-parser.ts +16 -0
  19. package/src/parse/leaf/number-parser.ts +90 -0
  20. package/src/parse/leaf/tag-parser.ts +160 -0
  21. package/src/parse/meta/and-parser.ts +40 -0
  22. package/src/parse/meta/capture-parser.ts +50 -0
  23. package/src/parse/meta/group-parser.ts +77 -0
  24. package/src/parse/meta/not-parser.ts +30 -0
  25. package/src/parse/meta/or-parser.ts +36 -0
  26. package/src/parse/meta/primary-parser.ts +234 -0
  27. package/src/parse/meta/search-parser.ts +41 -0
  28. package/src/parse/meta/traverse-parser.ts +42 -0
  29. package/src/parse/structure/assertion-obj-parser.ts +44 -0
  30. package/src/parse/structure/assertion-parser.ts +22 -0
  31. package/src/parse/structure/assertion-pred-parser.ts +45 -0
  32. package/src/parse/structure/compressed-parser.ts +17 -0
  33. package/src/parse/structure/digest-parser.ts +132 -0
  34. package/src/parse/structure/elided-parser.ts +17 -0
  35. package/src/parse/structure/encrypted-parser.ts +17 -0
  36. package/src/parse/structure/node-parser.ts +54 -0
  37. package/src/parse/structure/object-parser.ts +32 -0
  38. package/src/parse/structure/obscured-parser.ts +17 -0
  39. package/src/parse/structure/predicate-parser.ts +32 -0
  40. package/src/parse/structure/subject-parser.ts +32 -0
  41. package/src/parse/structure/wrapped-parser.ts +36 -0
  42. package/src/pattern/dcbor-integration.ts +40 -8
  43. package/src/pattern/index.ts +29 -0
  44. package/src/pattern/leaf/array-pattern.ts +67 -169
  45. package/src/pattern/leaf/cbor-pattern.ts +37 -23
  46. package/src/pattern/leaf/index.ts +1 -1
  47. package/src/pattern/leaf/map-pattern.ts +21 -2
  48. package/src/pattern/leaf/tagged-pattern.ts +6 -1
  49. package/src/pattern/meta/search-pattern.ts +13 -38
  50. package/src/pattern/meta/traverse-pattern.ts +2 -2
  51. package/src/pattern/structure/assertions-pattern.ts +19 -53
  52. package/src/pattern/structure/digest-pattern.ts +18 -22
  53. package/src/pattern/structure/index.ts +3 -0
  54. package/src/pattern/structure/node-pattern.ts +10 -29
  55. package/src/pattern/structure/object-pattern.ts +2 -2
  56. package/src/pattern/structure/predicate-pattern.ts +2 -2
  57. package/src/pattern/structure/subject-pattern.ts +31 -4
  58. package/src/pattern/structure/wrapped-pattern.ts +28 -9
  59. package/src/pattern/vm.ts +4 -4
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { Envelope } from "@bcts/envelope";
14
14
  import type { Cbor } from "@bcts/dcbor";
15
- import { cbor as toCbor, type CborInput } from "@bcts/dcbor";
15
+ import { cbor as toCbor, cborData, cborEquals, type CborInput } from "@bcts/dcbor";
16
16
  import {
17
17
  type Pattern as DCBORPattern,
18
18
  patternPathsWithCaptures as dcborPatternPathsWithCaptures,
@@ -110,6 +110,13 @@ export class CBORPattern implements Matcher {
110
110
 
111
111
  /**
112
112
  * Convert a single dcbor path to an envelope path.
113
+ *
114
+ * Uses canonical CBOR-byte equality (`cborEquals`) for the "skip the
115
+ * dcbor root if it duplicates our base envelope" check, mirroring
116
+ * Rust's `dcbor_path.first().map(|first| first == &base_cbor)`. The
117
+ * earlier port compared diagnostic strings, which collapses values
118
+ * that share a textual representation but differ structurally
119
+ * (e.g. NaN payloads).
113
120
  */
114
121
  private _convertDcborPathToEnvelopePath(
115
122
  dcborPath: Cbor[],
@@ -118,14 +125,12 @@ export class CBORPattern implements Matcher {
118
125
  ): Envelope[] {
119
126
  const envelopePath: Envelope[] = [baseEnvelope];
120
127
 
121
- // Skip first element if it matches the base envelope's CBOR content (compare by diagnostic)
122
- const skipFirst =
123
- dcborPath.length > 0 && dcborPath[0]?.toDiagnostic() === baseCbor.toDiagnostic();
128
+ const first = dcborPath[0];
129
+ const skipFirst = first !== undefined && cborEquals(first, baseCbor);
124
130
 
125
131
  const elementsToAdd = skipFirst ? dcborPath.slice(1) : dcborPath;
126
132
 
127
133
  for (const cborElement of elementsToAdd) {
128
- // Use newLeaf to create envelope from CBOR value
129
134
  envelopePath.push(Envelope.newLeaf(cborElement));
130
135
  }
131
136
 
@@ -162,7 +167,10 @@ export class CBORPattern implements Matcher {
162
167
  pathsWithCaptures(haystack: Envelope): [Path[], Map<string, Path[]>] {
163
168
  const envCase = haystack.subject().case();
164
169
 
165
- // Special case for KnownValue envelope
170
+ // Special case for KnownValue envelope. Rust uses `known_value.to_cbor()`,
171
+ // and the `From<KnownValue> for CBOR` impl returns
172
+ // `tagged_cbor()` — i.e. the same tagged form `taggedCbor()` produces
173
+ // here.
166
174
  if (envCase.type === "knownValue") {
167
175
  const knownValue = envCase.value;
168
176
  const knownValueCbor = knownValue.taggedCbor();
@@ -171,8 +179,8 @@ export class CBORPattern implements Matcher {
171
179
  case "Any":
172
180
  return [[[haystack]], new Map<string, Path[]>()];
173
181
  case "Value": {
174
- // Compare using diagnostic representation
175
- if (knownValueCbor.toDiagnostic() === this._pattern.cbor.toDiagnostic()) {
182
+ // Use canonical CBOR equality (mirrors Rust `==`).
183
+ if (cborEquals(knownValueCbor, this._pattern.cbor)) {
176
184
  return [[[haystack]], new Map<string, Path[]>()];
177
185
  }
178
186
  return [[], new Map<string, Path[]>()];
@@ -220,8 +228,8 @@ export class CBORPattern implements Matcher {
220
228
  return [[[haystack]], new Map<string, Path[]>()];
221
229
 
222
230
  case "Value":
223
- // Compare using diagnostic representation
224
- if (leafCbor.toDiagnostic() === this._pattern.cbor.toDiagnostic()) {
231
+ // Canonical CBOR-byte equality, mirroring Rust `==`.
232
+ if (cborEquals(leafCbor, this._pattern.cbor)) {
225
233
  return [[[haystack]], new Map<string, Path[]>()];
226
234
  }
227
235
  return [[], new Map<string, Path[]>()];
@@ -237,9 +245,8 @@ export class CBORPattern implements Matcher {
237
245
 
238
246
  const envelopePaths: Path[] = dcborPaths.map((dcborPath: Cbor[]) => {
239
247
  const extendedPath = [...basePath];
240
- // Skip the first element only if it exactly matches our root CBOR
241
- const skipFirst =
242
- dcborPath.length > 0 && dcborPath[0]?.toDiagnostic() === leafCbor.toDiagnostic();
248
+ const first = dcborPath[0];
249
+ const skipFirst = first !== undefined && cborEquals(first, leafCbor);
243
250
 
244
251
  const elementsToAdd = skipFirst ? dcborPath.slice(1) : dcborPath;
245
252
 
@@ -303,7 +310,10 @@ export class CBORPattern implements Matcher {
303
310
  }
304
311
 
305
312
  /**
306
- * Equality comparison.
313
+ * Equality comparison. `Value` variants compare by canonical CBOR
314
+ * byte sequence (mirrors Rust `==` on `CBOR`); `Pattern` variants fall
315
+ * back to display-string compare since `DCBORPattern` doesn't expose
316
+ * structural equality outside the crate.
307
317
  */
308
318
  equals(other: CBORPattern): boolean {
309
319
  if (this._pattern.type !== other._pattern.type) {
@@ -313,13 +323,11 @@ export class CBORPattern implements Matcher {
313
323
  case "Any":
314
324
  return true;
315
325
  case "Value":
316
- // Compare using diagnostic representation
317
- return (
318
- this._pattern.cbor.toDiagnostic() ===
319
- (other._pattern as { type: "Value"; cbor: Cbor }).cbor.toDiagnostic()
326
+ return cborEquals(
327
+ this._pattern.cbor,
328
+ (other._pattern as { type: "Value"; cbor: Cbor }).cbor,
320
329
  );
321
330
  case "Pattern":
322
- // Compare using display representation
323
331
  return (
324
332
  dcborPatternDisplay(this._pattern.pattern) ===
325
333
  dcborPatternDisplay(
@@ -336,11 +344,17 @@ export class CBORPattern implements Matcher {
336
344
  switch (this._pattern.type) {
337
345
  case "Any":
338
346
  return 0;
339
- case "Value":
340
- // Simple hash based on diagnostic string
341
- return simpleStringHash(this._pattern.cbor.toDiagnostic());
347
+ case "Value": {
348
+ // Hash the canonical CBOR-byte representation so two values
349
+ // that compare equal under `cborEquals` always hash the same.
350
+ const bytes = cborData(this._pattern.cbor);
351
+ let hash = 0;
352
+ for (const byte of bytes) {
353
+ hash = (hash * 31 + byte) | 0;
354
+ }
355
+ return hash;
356
+ }
342
357
  case "Pattern":
343
- // Simple hash based on display string
344
358
  return simpleStringHash(dcborPatternDisplay(this._pattern.pattern));
345
359
  }
346
360
  }
@@ -22,7 +22,7 @@ export { NumberPattern, registerNumberPatternFactory } from "./number-pattern";
22
22
  export { TextPattern, registerTextPatternFactory } from "./text-pattern";
23
23
  export { ByteStringPattern, registerByteStringPatternFactory } from "./byte-string-pattern";
24
24
  export { DatePattern, registerDatePatternFactory } from "./date-pattern";
25
- export { ArrayPattern, type ArrayPatternType, registerArrayPatternFactory } from "./array-pattern";
25
+ export { ArrayPattern, registerArrayPatternFactory } from "./array-pattern";
26
26
  export { MapPattern, type MapPatternType, registerMapPatternFactory } from "./map-pattern";
27
27
  export { KnownValuePattern, registerKnownValuePatternFactory } from "./known-value-pattern";
28
28
  export { TaggedPattern, registerTaggedPatternFactory } from "./tagged-pattern";
@@ -62,6 +62,17 @@ export class MapPattern implements Matcher {
62
62
  return new MapPattern({ type: "Interval", interval });
63
63
  }
64
64
 
65
+ /**
66
+ * Creates a new MapPattern from a length Interval.
67
+ *
68
+ * Mirrors Rust `MapPattern::from_interval`. Used by the
69
+ * dcbor-pattern → envelope-pattern bridge to preserve `{{n,m}}`
70
+ * length info.
71
+ */
72
+ static fromInterval(interval: Interval): MapPattern {
73
+ return new MapPattern({ type: "Interval", interval });
74
+ }
75
+
65
76
  /**
66
77
  * Gets the pattern type.
67
78
  */
@@ -118,9 +129,17 @@ export class MapPattern implements Matcher {
118
129
  toString(): string {
119
130
  switch (this._pattern.type) {
120
131
  case "Any":
121
- return "{*}";
132
+ return "map";
122
133
  case "Interval":
123
- return `{{${this._pattern.interval.toString()}}}`;
134
+ // Mirror Rust `bc-dcbor-pattern-rust/src/pattern/structure/
135
+ // map_pattern.rs` `MapPattern::Length` Display:
136
+ // `write!(f, "{{{}}}", interval)` → `{<interval>}`.
137
+ // The interval's own `Display` already produces `{n}` /
138
+ // `{n,m}` / `{n,}`, so the final output is `{{n}}` (double
139
+ // brace from the outer literal `{` + interval's `{n}` + `}`).
140
+ // Earlier this port wrapped with `{{...}}` and produced
141
+ // `{{{n}}}` (triple brace), breaking parser parity.
142
+ return `{${this._pattern.interval.toString()}}`;
124
143
  }
125
144
  }
126
145
 
@@ -159,7 +159,12 @@ export class TaggedPattern implements Matcher {
159
159
  }
160
160
 
161
161
  toString(): string {
162
- return taggedPatternDisplay(this._inner, patternDisplay);
162
+ // Mirror Rust `bc-envelope-pattern-rust/src/pattern/leaf/tagged_pattern.rs`'s
163
+ // `Display`: delegate to the underlying dcbor-pattern `Display`, then
164
+ // normalize the regex-variant's `, ` (comma + two spaces — see
165
+ // `bc-dcbor-pattern-rust/src/pattern/structure/tagged_pattern.rs:239`)
166
+ // back to `, ` so envelope-pattern's surface API stays consistent.
167
+ return taggedPatternDisplay(this._inner, patternDisplay).replace(", ", ", ");
163
168
  }
164
169
 
165
170
  /**
@@ -94,50 +94,25 @@ export class SearchPattern implements Matcher {
94
94
  }
95
95
 
96
96
  /**
97
- * Walk the envelope tree recursively.
97
+ * Walk the envelope tree using the canonical `Envelope.walk` traversal.
98
+ *
99
+ * Mirrors Rust `bc_envelope::Envelope::walk(false, vec![], visitor)`
100
+ * which is what `SearchPattern::paths_with_captures` uses. The earlier
101
+ * port hand-rolled a recursion that double-recursed assertions and
102
+ * stepped through wrapped subjects manually, producing a different
103
+ * path order (and extra duplicates that the digest-set deduplication
104
+ * would partially mask).
98
105
  */
99
106
  private _walkEnvelope(
100
107
  envelope: Envelope,
101
108
  pathToCurrent: Envelope[],
102
109
  visitor: (envelope: Envelope, path: Envelope[]) => void,
103
110
  ): void {
104
- // Visit this node
105
- visitor(envelope, pathToCurrent);
106
-
107
- // Get the subject
108
- const subject = envelope.subject();
109
- const newPath = [...pathToCurrent, envelope];
110
-
111
- // Walk subject if it's different from this envelope
112
- if (!subject.digest().equals(envelope.digest())) {
113
- this._walkEnvelope(subject, newPath, visitor);
114
- }
115
-
116
- // Walk assertions
117
- for (const assertion of envelope.assertions()) {
118
- this._walkEnvelope(assertion, newPath, visitor);
119
-
120
- // Walk predicate and object if available
121
- const predicate = assertion.asPredicate?.();
122
- if (predicate !== undefined) {
123
- const assertionPath = [...newPath, assertion];
124
- this._walkEnvelope(predicate, assertionPath, visitor);
125
- }
126
-
127
- const object = assertion.asObject?.();
128
- if (object !== undefined) {
129
- const assertionPath = [...newPath, assertion];
130
- this._walkEnvelope(object, assertionPath, visitor);
131
- }
132
- }
133
-
134
- // Walk wrapped content if present
135
- if (subject.isWrapped()) {
136
- const unwrapped = subject.tryUnwrap?.();
137
- if (unwrapped !== undefined) {
138
- this._walkEnvelope(unwrapped, newPath, visitor);
139
- }
140
- }
111
+ envelope.walk<Envelope[]>(false, pathToCurrent, (current, _level, _edge, state) => {
112
+ visitor(current, state);
113
+ // Children inherit a path that includes the current node.
114
+ return [[...state, current], false];
115
+ });
141
116
  }
142
117
 
143
118
  paths(haystack: Envelope): Path[] {
@@ -12,7 +12,7 @@
12
12
 
13
13
  import type { Envelope } from "@bcts/envelope";
14
14
  import type { Path } from "../../format";
15
- import type { Matcher } from "../matcher";
15
+ import { dispatchPatternToString, type Matcher } from "../matcher";
16
16
  import type { Instr } from "../vm";
17
17
  import type { Pattern } from "../index";
18
18
 
@@ -144,7 +144,7 @@ export class TraversePattern implements Matcher {
144
144
 
145
145
  toString(): string {
146
146
  return this.patterns()
147
- .map((p) => (p as unknown as { toString(): string }).toString())
147
+ .map((p) => dispatchPatternToString(p))
148
148
  .join(" -> ");
149
149
  }
150
150
 
@@ -18,6 +18,7 @@ import type { Pattern } from "../index";
18
18
 
19
19
  // Forward declaration for Pattern factory
20
20
  let createStructureAssertionsPattern: ((pattern: AssertionsPattern) => Pattern) | undefined;
21
+ let dispatchPatternToString: ((pattern: Pattern) => string) | undefined;
21
22
 
22
23
  export function registerAssertionsPatternFactory(
23
24
  factory: (pattern: AssertionsPattern) => Pattern,
@@ -25,20 +26,22 @@ export function registerAssertionsPatternFactory(
25
26
  createStructureAssertionsPattern = factory;
26
27
  }
27
28
 
29
+ export function registerAssertionsPatternToStringDispatch(fn: (pattern: Pattern) => string): void {
30
+ dispatchPatternToString = fn;
31
+ }
32
+
28
33
  /**
29
34
  * Pattern type for assertions pattern matching.
30
35
  *
31
- * Corresponds to the Rust `AssertionsPattern` enum in assertions_pattern.rs
36
+ * Corresponds to the Rust `AssertionsPattern` enum in assertions_pattern.rs:
37
+ * - `Any` matches any assertion.
38
+ * - `WithPredicate` matches assertions whose predicate matches a sub-pattern.
39
+ * - `WithObject` matches assertions whose object matches a sub-pattern.
32
40
  */
33
41
  export type AssertionsPatternType =
34
42
  | { readonly type: "Any" }
35
43
  | { readonly type: "WithPredicate"; readonly pattern: Pattern }
36
- | { readonly type: "WithObject"; readonly pattern: Pattern }
37
- | {
38
- readonly type: "WithBoth";
39
- readonly predicatePattern: Pattern;
40
- readonly objectPattern: Pattern;
41
- };
44
+ | { readonly type: "WithObject"; readonly pattern: Pattern };
42
45
 
43
46
  /**
44
47
  * Pattern for matching assertions in envelopes.
@@ -75,14 +78,6 @@ export class AssertionsPattern implements Matcher {
75
78
  return new AssertionsPattern({ type: "WithObject", pattern });
76
79
  }
77
80
 
78
- /**
79
- * Creates a new AssertionsPattern that matches assertions with both
80
- * predicate and object patterns.
81
- */
82
- static withBoth(predicatePattern: Pattern, objectPattern: Pattern): AssertionsPattern {
83
- return new AssertionsPattern({ type: "WithBoth", predicatePattern, objectPattern });
84
- }
85
-
86
81
  /**
87
82
  * Gets the pattern type.
88
83
  */
@@ -97,9 +92,6 @@ export class AssertionsPattern implements Matcher {
97
92
  if (this._pattern.type === "WithPredicate") {
98
93
  return this._pattern.pattern;
99
94
  }
100
- if (this._pattern.type === "WithBoth") {
101
- return this._pattern.predicatePattern;
102
- }
103
95
  return undefined;
104
96
  }
105
97
 
@@ -110,9 +102,6 @@ export class AssertionsPattern implements Matcher {
110
102
  if (this._pattern.type === "WithObject") {
111
103
  return this._pattern.pattern;
112
104
  }
113
- if (this._pattern.type === "WithBoth") {
114
- return this._pattern.objectPattern;
115
- }
116
105
  return undefined;
117
106
  }
118
107
 
@@ -142,19 +131,6 @@ export class AssertionsPattern implements Matcher {
142
131
  }
143
132
  break;
144
133
  }
145
- case "WithBoth": {
146
- const predicate = assertion.asPredicate?.();
147
- const object = assertion.asObject?.();
148
- if (predicate !== undefined && object !== undefined) {
149
- if (
150
- matchPattern(this._pattern.predicatePattern, predicate) &&
151
- matchPattern(this._pattern.objectPattern, object)
152
- ) {
153
- paths.push([assertion]);
154
- }
155
- }
156
- break;
157
- }
158
134
  }
159
135
  }
160
136
 
@@ -183,15 +159,18 @@ export class AssertionsPattern implements Matcher {
183
159
  }
184
160
 
185
161
  toString(): string {
162
+ const fmt = dispatchPatternToString;
186
163
  switch (this._pattern.type) {
187
164
  case "Any":
188
165
  return "assert";
189
- case "WithPredicate":
190
- return `assertpred(${(this._pattern.pattern as unknown as { toString(): string }).toString()})`;
191
- case "WithObject":
192
- return `assertobj(${(this._pattern.pattern as unknown as { toString(): string }).toString()})`;
193
- case "WithBoth":
194
- return `assert(${(this._pattern.predicatePattern as unknown as { toString(): string }).toString()}, ${(this._pattern.objectPattern as unknown as { toString(): string }).toString()})`;
166
+ case "WithPredicate": {
167
+ const inner = fmt !== undefined ? fmt(this._pattern.pattern) : "?";
168
+ return `assertpred(${inner})`;
169
+ }
170
+ case "WithObject": {
171
+ const inner = fmt !== undefined ? fmt(this._pattern.pattern) : "?";
172
+ return `assertobj(${inner})`;
173
+ }
195
174
  }
196
175
  }
197
176
 
@@ -215,17 +194,6 @@ export class AssertionsPattern implements Matcher {
215
194
  ).pattern;
216
195
  return thisPattern === otherPattern;
217
196
  }
218
- case "WithBoth": {
219
- const otherBoth = other._pattern as {
220
- type: "WithBoth";
221
- predicatePattern: Pattern;
222
- objectPattern: Pattern;
223
- };
224
- return (
225
- this._pattern.predicatePattern === otherBoth.predicatePattern &&
226
- this._pattern.objectPattern === otherBoth.objectPattern
227
- );
228
- }
229
197
  }
230
198
  }
231
199
 
@@ -240,8 +208,6 @@ export class AssertionsPattern implements Matcher {
240
208
  return 1;
241
209
  case "WithObject":
242
210
  return 2;
243
- case "WithBoth":
244
- return 3;
245
211
  }
246
212
  }
247
213
  }
@@ -39,10 +39,11 @@ function bytesToLatin1(bytes: Uint8Array): string {
39
39
  /**
40
40
  * Pattern type for digest pattern matching.
41
41
  *
42
- * Corresponds to the Rust `DigestPattern` enum in digest_pattern.rs
42
+ * Corresponds to the Rust `DigestPattern` enum in digest_pattern.rs.
43
+ * Rust has only `Digest`, `Prefix`, and `BinaryRegex` variants — there is
44
+ * no `Any` here. To match "any digest", use the meta `any()` pattern.
43
45
  */
44
46
  export type DigestPatternType =
45
- | { readonly type: "Any" }
46
47
  | { readonly type: "Digest"; readonly digest: Digest }
47
48
  | { readonly type: "Prefix"; readonly prefix: Uint8Array }
48
49
  | { readonly type: "BinaryRegex"; readonly regex: RegExp };
@@ -59,13 +60,6 @@ export class DigestPattern implements Matcher {
59
60
  this._pattern = pattern;
60
61
  }
61
62
 
62
- /**
63
- * Creates a new DigestPattern that matches any digest.
64
- */
65
- static any(): DigestPattern {
66
- return new DigestPattern({ type: "Any" });
67
- }
68
-
69
63
  /**
70
64
  * Creates a new DigestPattern that matches the exact digest.
71
65
  */
@@ -100,10 +94,6 @@ export class DigestPattern implements Matcher {
100
94
  let isHit = false;
101
95
 
102
96
  switch (this._pattern.type) {
103
- case "Any":
104
- // Any digest matches - every envelope has a digest
105
- isHit = true;
106
- break;
107
97
  case "Digest":
108
98
  isHit = digest.equals(this._pattern.digest);
109
99
  break;
@@ -152,8 +142,6 @@ export class DigestPattern implements Matcher {
152
142
 
153
143
  toString(): string {
154
144
  switch (this._pattern.type) {
155
- case "Any":
156
- return "digest";
157
145
  case "Digest":
158
146
  return `digest(${this._pattern.digest.hex()})`;
159
147
  case "Prefix":
@@ -165,14 +153,17 @@ export class DigestPattern implements Matcher {
165
153
 
166
154
  /**
167
155
  * Equality comparison.
156
+ *
157
+ * `Prefix` comparison is case-insensitive on the *hex representation* to
158
+ * mirror Rust's `eq_ignore_ascii_case` (which compares the underlying
159
+ * `Vec<u8>` of hex bytes byte-for-byte modulo ASCII case). For raw byte
160
+ * prefixes that happen to be ASCII, this is an ordinary byte compare.
168
161
  */
169
162
  equals(other: DigestPattern): boolean {
170
163
  if (this._pattern.type !== other._pattern.type) {
171
164
  return false;
172
165
  }
173
166
  switch (this._pattern.type) {
174
- case "Any":
175
- return true;
176
167
  case "Digest":
177
168
  return this._pattern.digest.equals(
178
169
  (other._pattern as { type: "Digest"; digest: Digest }).digest,
@@ -182,7 +173,13 @@ export class DigestPattern implements Matcher {
182
173
  const otherPrefix = (other._pattern as { type: "Prefix"; prefix: Uint8Array }).prefix;
183
174
  if (thisPrefix.length !== otherPrefix.length) return false;
184
175
  for (let i = 0; i < thisPrefix.length; i++) {
185
- if (thisPrefix[i] !== otherPrefix[i]) return false;
176
+ const a = thisPrefix[i];
177
+ const b = otherPrefix[i];
178
+ if (a === b) continue;
179
+ // ASCII case-insensitive compare ('A'..='Z' ↔ 'a'..='z')
180
+ const aLower = a >= 0x41 && a <= 0x5a ? a + 0x20 : a;
181
+ const bLower = b >= 0x41 && b <= 0x5a ? b + 0x20 : b;
182
+ if (aLower !== bLower) return false;
186
183
  }
187
184
  return true;
188
185
  }
@@ -199,10 +196,7 @@ export class DigestPattern implements Matcher {
199
196
  */
200
197
  hashCode(): number {
201
198
  switch (this._pattern.type) {
202
- case "Any":
203
- return 0;
204
199
  case "Digest": {
205
- // Hash based on first few bytes of digest
206
200
  const data = this._pattern.digest.data().slice(0, 8);
207
201
  let hash = 0;
208
202
  for (const byte of data) {
@@ -213,7 +207,9 @@ export class DigestPattern implements Matcher {
213
207
  case "Prefix": {
214
208
  let hash = 0;
215
209
  for (const byte of this._pattern.prefix) {
216
- hash = (hash * 31 + byte) | 0;
210
+ // Fold ASCII case to match equality semantics.
211
+ const folded = byte >= 0x41 && byte <= 0x5a ? byte + 0x20 : byte;
212
+ hash = (hash * 31 + folded) | 0;
217
213
  }
218
214
  return hash;
219
215
  }
@@ -24,6 +24,7 @@ export {
24
24
  SubjectPattern,
25
25
  type SubjectPatternType,
26
26
  registerSubjectPatternFactory,
27
+ registerSubjectPatternDispatch,
27
28
  } from "./subject-pattern";
28
29
  export {
29
30
  PredicatePattern,
@@ -39,6 +40,7 @@ export {
39
40
  AssertionsPattern,
40
41
  type AssertionsPatternType,
41
42
  registerAssertionsPatternFactory,
43
+ registerAssertionsPatternToStringDispatch,
42
44
  } from "./assertions-pattern";
43
45
  export {
44
46
  DigestPattern,
@@ -56,6 +58,7 @@ export {
56
58
  type WrappedPatternType,
57
59
  registerWrappedPatternFactory,
58
60
  registerWrappedPatternDispatch,
61
+ registerWrappedPatternAny,
59
62
  } from "./wrapped-pattern";
60
63
 
61
64
  // Import concrete types for use in StructurePattern
@@ -28,12 +28,14 @@ export function registerNodePatternFactory(factory: (pattern: NodePattern) => Pa
28
28
  /**
29
29
  * Pattern type for node pattern matching.
30
30
  *
31
- * Corresponds to the Rust `NodePattern` enum in node_pattern.rs
31
+ * Corresponds to the Rust `NodePattern` enum in node_pattern.rs:
32
+ * - `Any` matches any node.
33
+ * - `AssertionsInterval` matches a node whose number of assertions falls in
34
+ * the given interval (e.g., `node({2,5})`).
32
35
  */
33
36
  export type NodePatternType =
34
37
  | { readonly type: "Any" }
35
- | { readonly type: "AssertionsInterval"; readonly interval: Interval }
36
- | { readonly type: "WithSubject"; readonly subjectPattern: Pattern };
38
+ | { readonly type: "AssertionsInterval"; readonly interval: Interval };
37
39
 
38
40
  /**
39
41
  * Pattern for matching node envelopes.
@@ -69,13 +71,6 @@ export class NodePattern implements Matcher {
69
71
  return new NodePattern({ type: "AssertionsInterval", interval });
70
72
  }
71
73
 
72
- /**
73
- * Creates a new NodePattern with a subject pattern constraint.
74
- */
75
- static withSubject(subjectPattern: Pattern): NodePattern {
76
- return new NodePattern({ type: "WithSubject", subjectPattern });
77
- }
78
-
79
74
  /**
80
75
  * Gets the pattern type.
81
76
  */
@@ -84,17 +79,18 @@ export class NodePattern implements Matcher {
84
79
  }
85
80
 
86
81
  /**
87
- * Gets the subject pattern if this is a WithSubject type, undefined otherwise.
82
+ * Returns the subject pattern, if any. Rust's `NodePattern` does not carry
83
+ * subject patterns, so this always returns `undefined`.
88
84
  */
89
85
  subjectPattern(): Pattern | undefined {
90
- return this._pattern.type === "WithSubject" ? this._pattern.subjectPattern : undefined;
86
+ return undefined;
91
87
  }
92
88
 
93
89
  /**
94
- * Gets the assertion patterns (empty array if none).
90
+ * Returns the assertion patterns. Rust's `NodePattern` does not carry
91
+ * assertion sub-patterns, so this always returns an empty array.
95
92
  */
96
93
  assertionPatterns(): Pattern[] {
97
- // NodePattern doesn't support assertion patterns directly; return empty array
98
94
  return [];
99
95
  }
100
96
 
@@ -112,10 +108,6 @@ export class NodePattern implements Matcher {
112
108
  case "AssertionsInterval":
113
109
  isHit = this._pattern.interval.contains(haystack.assertions().length);
114
110
  break;
115
- case "WithSubject":
116
- // For WithSubject, we match if the node exists (subject pattern matching done at higher level)
117
- isHit = true;
118
- break;
119
111
  }
120
112
 
121
113
  const paths = isHit ? [[haystack]] : [];
@@ -147,8 +139,6 @@ export class NodePattern implements Matcher {
147
139
  return "node";
148
140
  case "AssertionsInterval":
149
141
  return `node(${this._pattern.interval.toString()})`;
150
- case "WithSubject":
151
- return `node(${(this._pattern.subjectPattern as unknown as { toString(): string }).toString()})`;
152
142
  }
153
143
  }
154
144
 
@@ -166,12 +156,6 @@ export class NodePattern implements Matcher {
166
156
  return this._pattern.interval.equals(
167
157
  (other._pattern as { type: "AssertionsInterval"; interval: Interval }).interval,
168
158
  );
169
- case "WithSubject":
170
- // Simple reference equality for pattern (could be improved with deep equality)
171
- return (
172
- this._pattern.subjectPattern ===
173
- (other._pattern as { type: "WithSubject"; subjectPattern: Pattern }).subjectPattern
174
- );
175
159
  }
176
160
  }
177
161
 
@@ -183,10 +167,7 @@ export class NodePattern implements Matcher {
183
167
  case "Any":
184
168
  return 0;
185
169
  case "AssertionsInterval":
186
- // Simple hash based on interval min/max
187
170
  return this._pattern.interval.min() * 31 + (this._pattern.interval.max() ?? 0);
188
- case "WithSubject":
189
- return 1;
190
171
  }
191
172
  }
192
173
  }
@@ -12,7 +12,7 @@
12
12
 
13
13
  import type { Envelope } from "@bcts/envelope";
14
14
  import type { Path } from "../../format";
15
- import { matchPattern, type Matcher } from "../matcher";
15
+ import { dispatchPatternToString, matchPattern, type Matcher } from "../matcher";
16
16
  import type { Instr } from "../vm";
17
17
  import type { Pattern } from "../index";
18
18
 
@@ -124,7 +124,7 @@ export class ObjectPattern implements Matcher {
124
124
  case "Any":
125
125
  return "obj";
126
126
  case "Pattern":
127
- return `obj(${(this._pattern.pattern as unknown as { toString(): string }).toString()})`;
127
+ return `obj(${dispatchPatternToString(this._pattern.pattern)})`;
128
128
  }
129
129
  }
130
130