@bcts/envelope-pattern 1.0.0-alpha.22 → 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.
- package/dist/index.cjs +1291 -826
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -59
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +102 -60
- package/dist/index.d.mts.map +1 -1
- package/dist/index.iife.js +1643 -1179
- package/dist/index.iife.js.map +1 -1
- package/dist/index.mjs +1315 -853
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -11
- package/src/error.ts +1 -1
- package/src/format.ts +19 -31
- package/src/parse/index.ts +17 -1010
- package/src/parse/leaf/array-parser.ts +36 -0
- package/src/parse/leaf/cbor-parser.ts +43 -0
- package/src/parse/leaf/date-parser.ts +81 -0
- package/src/parse/leaf/known-value-parser.ts +73 -0
- package/src/parse/leaf/null-parser.ts +16 -0
- package/src/parse/leaf/number-parser.ts +90 -0
- package/src/parse/leaf/tag-parser.ts +160 -0
- package/src/parse/meta/and-parser.ts +40 -0
- package/src/parse/meta/capture-parser.ts +50 -0
- package/src/parse/meta/group-parser.ts +77 -0
- package/src/parse/meta/not-parser.ts +30 -0
- package/src/parse/meta/or-parser.ts +36 -0
- package/src/parse/meta/primary-parser.ts +234 -0
- package/src/parse/meta/search-parser.ts +41 -0
- package/src/parse/meta/traverse-parser.ts +42 -0
- package/src/parse/structure/assertion-obj-parser.ts +44 -0
- package/src/parse/structure/assertion-parser.ts +22 -0
- package/src/parse/structure/assertion-pred-parser.ts +45 -0
- package/src/parse/structure/compressed-parser.ts +17 -0
- package/src/parse/structure/digest-parser.ts +132 -0
- package/src/parse/structure/elided-parser.ts +17 -0
- package/src/parse/structure/encrypted-parser.ts +17 -0
- package/src/parse/structure/node-parser.ts +54 -0
- package/src/parse/structure/object-parser.ts +32 -0
- package/src/parse/structure/obscured-parser.ts +17 -0
- package/src/parse/structure/predicate-parser.ts +32 -0
- package/src/parse/structure/subject-parser.ts +32 -0
- package/src/parse/structure/wrapped-parser.ts +36 -0
- package/src/pattern/dcbor-integration.ts +40 -8
- package/src/pattern/index.ts +29 -0
- package/src/pattern/leaf/array-pattern.ts +67 -169
- package/src/pattern/leaf/cbor-pattern.ts +37 -23
- package/src/pattern/leaf/index.ts +1 -1
- package/src/pattern/leaf/map-pattern.ts +21 -2
- package/src/pattern/leaf/tagged-pattern.ts +6 -1
- package/src/pattern/meta/search-pattern.ts +13 -38
- package/src/pattern/meta/traverse-pattern.ts +2 -2
- package/src/pattern/structure/assertions-pattern.ts +19 -53
- package/src/pattern/structure/digest-pattern.ts +18 -22
- package/src/pattern/structure/index.ts +3 -0
- package/src/pattern/structure/node-pattern.ts +10 -29
- package/src/pattern/structure/object-pattern.ts +2 -2
- package/src/pattern/structure/predicate-pattern.ts +2 -2
- package/src/pattern/structure/subject-pattern.ts +31 -4
- package/src/pattern/structure/wrapped-pattern.ts +28 -9
- package/src/pattern/vm.ts +4 -4
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2023-2026 Blockchain Commons, LLC
|
|
3
|
+
* Copyright © 2025-2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* Array parser — port of `bc-envelope-pattern-rust`
|
|
6
|
+
* `parse/leaf/array_parser.rs`.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors Rust's flow exactly: after the `[` token has been consumed,
|
|
9
|
+
* delegate to `utils::parseArrayInner` (which handles `*`, `{n}`, `{n,m}`,
|
|
10
|
+
* `{n,}` directly and otherwise wraps the body in `[...]` and re-parses
|
|
11
|
+
* via dcbor-pattern), then expect a closing `]`.
|
|
12
|
+
*
|
|
13
|
+
* @module envelope-pattern/parse/leaf/array-parser
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { type Result, err, expectedCloseBracket, ok, unexpectedToken } from "../../error";
|
|
17
|
+
import type { Pattern } from "../../pattern";
|
|
18
|
+
import type { Lexer } from "../token";
|
|
19
|
+
import { parseArrayInner } from "../utils";
|
|
20
|
+
|
|
21
|
+
export function parseArray(lexer: Lexer): Result<Pattern> {
|
|
22
|
+
const remainder = lexer.remainder();
|
|
23
|
+
const inner = parseArrayInner(remainder);
|
|
24
|
+
if (!inner.ok) return inner;
|
|
25
|
+
const [pattern, consumed] = inner.value;
|
|
26
|
+
lexer.bump(consumed);
|
|
27
|
+
|
|
28
|
+
const close = lexer.next();
|
|
29
|
+
if (close === undefined) {
|
|
30
|
+
return err(expectedCloseBracket(lexer.span()));
|
|
31
|
+
}
|
|
32
|
+
if (close.token.type !== "BracketClose") {
|
|
33
|
+
return err(unexpectedToken(close.token, close.span));
|
|
34
|
+
}
|
|
35
|
+
return ok(pattern);
|
|
36
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2023-2026 Blockchain Commons, LLC
|
|
3
|
+
* Copyright © 2025-2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* CBOR pattern parser — port of `bc-envelope-pattern-rust`
|
|
6
|
+
* `parse/leaf/cbor_parser.rs`.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors Rust's flow: lookahead for `(`. If absent, return `any_cbor()`.
|
|
9
|
+
* Otherwise consume the `(`, delegate to `parseCborInner` (handles
|
|
10
|
+
* `/regex/`, `ur:…`, and CBOR diagnostic notation), and expect a closing
|
|
11
|
+
* `)`.
|
|
12
|
+
*
|
|
13
|
+
* @module envelope-pattern/parse/leaf/cbor-parser
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { type Result, err, expectedCloseParen, ok, unexpectedToken } from "../../error";
|
|
17
|
+
import { type Pattern, anyCbor } from "../../pattern";
|
|
18
|
+
import type { Lexer } from "../token";
|
|
19
|
+
import { parseCborInner } from "../utils";
|
|
20
|
+
|
|
21
|
+
export function parseCbor(lexer: Lexer): Result<Pattern> {
|
|
22
|
+
const next = lexer.peekToken();
|
|
23
|
+
if (next?.token.type !== "ParenOpen") {
|
|
24
|
+
return ok(anyCbor());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
lexer.next(); // consume (
|
|
28
|
+
|
|
29
|
+
const remainder = lexer.remainder();
|
|
30
|
+
const innerResult = parseCborInner(remainder);
|
|
31
|
+
if (!innerResult.ok) return innerResult;
|
|
32
|
+
const [pattern, consumed] = innerResult.value;
|
|
33
|
+
lexer.bump(consumed);
|
|
34
|
+
|
|
35
|
+
const close = lexer.next();
|
|
36
|
+
if (close === undefined) {
|
|
37
|
+
return err(expectedCloseParen(lexer.span()));
|
|
38
|
+
}
|
|
39
|
+
if (close.token.type !== "ParenClose") {
|
|
40
|
+
return err(unexpectedToken(close.token, close.span));
|
|
41
|
+
}
|
|
42
|
+
return ok(pattern);
|
|
43
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2023-2026 Blockchain Commons, LLC
|
|
3
|
+
* Copyright © 2025-2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* Date content parser — port of `bc-envelope-pattern-rust`
|
|
6
|
+
* `parse/leaf/date_parser.rs`.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors Rust's `Date::from_string`, which accepts a strict ISO-8601
|
|
9
|
+
* subset, by deferring to dcbor's `CborDate.fromString`. Falls back to JS
|
|
10
|
+
* `Date.parse` only as a defensive shim — that branch is unreachable for
|
|
11
|
+
* conformant inputs.
|
|
12
|
+
*
|
|
13
|
+
* @module envelope-pattern/parse/leaf/date-parser
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { CborDate } from "@bcts/dcbor";
|
|
17
|
+
import { type Result, type Span, err, invalidDateFormat, invalidRegex, ok } from "../../error";
|
|
18
|
+
import { type Pattern, date, dateEarliest, dateLatest, dateRange, dateRegex } from "../../pattern";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse a date pattern of one of the forms accepted by Rust:
|
|
22
|
+
*
|
|
23
|
+
* - `/regex/` (regex match against ISO-8601 string)
|
|
24
|
+
* - `start...end` (inclusive range)
|
|
25
|
+
* - `start...` (earliest)
|
|
26
|
+
* - `...end` (latest)
|
|
27
|
+
* - `iso-8601` (exact)
|
|
28
|
+
*
|
|
29
|
+
* Mirrors `parse_date_content` in Rust; uses `CborDate.fromString` so the
|
|
30
|
+
* accepted formats match Rust's `bc_envelope::prelude::Date::from_string`
|
|
31
|
+
* exactly rather than the looser JS `Date.parse`.
|
|
32
|
+
*/
|
|
33
|
+
export function parseDateContent(content: string, span: Span): Result<Pattern> {
|
|
34
|
+
// Regex form: /pattern/
|
|
35
|
+
if (content.startsWith("/") && content.endsWith("/") && content.length >= 2) {
|
|
36
|
+
const regexStr = content.slice(1, -1);
|
|
37
|
+
try {
|
|
38
|
+
return ok(dateRegex(new RegExp(regexStr)));
|
|
39
|
+
} catch {
|
|
40
|
+
return err(invalidRegex(span));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ellipsisIdx = content.indexOf("...");
|
|
45
|
+
if (ellipsisIdx !== -1) {
|
|
46
|
+
const left = content.slice(0, ellipsisIdx);
|
|
47
|
+
const right = content.slice(ellipsisIdx + 3);
|
|
48
|
+
|
|
49
|
+
if (left.length === 0 && right.length > 0) {
|
|
50
|
+
const parsed = parseIsoDateStrict(right);
|
|
51
|
+
if (parsed === undefined) return err(invalidDateFormat(span));
|
|
52
|
+
return ok(dateLatest(parsed));
|
|
53
|
+
}
|
|
54
|
+
if (left.length > 0 && right.length === 0) {
|
|
55
|
+
const parsed = parseIsoDateStrict(left);
|
|
56
|
+
if (parsed === undefined) return err(invalidDateFormat(span));
|
|
57
|
+
return ok(dateEarliest(parsed));
|
|
58
|
+
}
|
|
59
|
+
if (left.length > 0 && right.length > 0) {
|
|
60
|
+
const start = parseIsoDateStrict(left);
|
|
61
|
+
const end = parseIsoDateStrict(right);
|
|
62
|
+
if (start === undefined || end === undefined) return err(invalidDateFormat(span));
|
|
63
|
+
return ok(dateRange(start, end));
|
|
64
|
+
}
|
|
65
|
+
return err(invalidDateFormat(span));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const parsed = parseIsoDateStrict(content);
|
|
69
|
+
if (parsed === undefined) {
|
|
70
|
+
return err(invalidDateFormat(span));
|
|
71
|
+
}
|
|
72
|
+
return ok(date(parsed));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseIsoDateStrict(value: string): CborDate | undefined {
|
|
76
|
+
try {
|
|
77
|
+
return CborDate.fromString(value);
|
|
78
|
+
} catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2023-2026 Blockchain Commons, LLC
|
|
3
|
+
* Copyright © 2025-2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* Helpers for parsing the body of a `'…'` (single-quoted) known-value
|
|
6
|
+
* literal. Mirrors the inline body of Rust's `Token::SingleQuotedPattern`
|
|
7
|
+
* branch in `parse_primary`:
|
|
8
|
+
*
|
|
9
|
+
* - If the contents are a valid `u64`, build `Pattern::known_value(...)`.
|
|
10
|
+
* - Otherwise, build `Pattern::known_value_named(...)`.
|
|
11
|
+
*
|
|
12
|
+
* The earlier port duck-typed a fake `KnownValue`; this version uses the
|
|
13
|
+
* real `KnownValue` constructor so all subsequent KnownValue methods work
|
|
14
|
+
* (e.g., `taggedCbor()`, `name()`, etc.).
|
|
15
|
+
*
|
|
16
|
+
* @module envelope-pattern/parse/leaf/known-value-parser
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { KnownValue } from "@bcts/known-values";
|
|
20
|
+
import { type Result, ok } from "../../error";
|
|
21
|
+
import {
|
|
22
|
+
type Pattern,
|
|
23
|
+
KnownValuePattern,
|
|
24
|
+
knownValue,
|
|
25
|
+
leafKnownValue,
|
|
26
|
+
patternLeaf,
|
|
27
|
+
} from "../../pattern";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Maximum value of a Rust `u64`. Used to reject literals that would
|
|
31
|
+
* silently wrap or lose precision when constructing a `KnownValue`.
|
|
32
|
+
*/
|
|
33
|
+
const U64_MAX = 0xffffffffffffffffn;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse the inner contents of a `'…'` known-value pattern token.
|
|
37
|
+
*
|
|
38
|
+
* Mirrors the Rust dispatch
|
|
39
|
+
* ```ignore
|
|
40
|
+
* if let Ok(value) = content.parse::<u64>() {
|
|
41
|
+
* Pattern::known_value(KnownValue::new(value))
|
|
42
|
+
* } else {
|
|
43
|
+
* Pattern::known_value_named(content)
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
* but uses BigInt parsing to preserve full `u64` range — the previous
|
|
47
|
+
* `parseInt(...)` path silently truncated above `2^53-1`.
|
|
48
|
+
*/
|
|
49
|
+
export function parseKnownValueContent(content: string): Result<Pattern> {
|
|
50
|
+
if (isU64Literal(content)) {
|
|
51
|
+
const value = BigInt(content);
|
|
52
|
+
return ok(knownValue(new KnownValue(value)));
|
|
53
|
+
}
|
|
54
|
+
return ok(patternLeaf(leafKnownValue(KnownValuePattern.named(content))));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isU64Literal(content: string): boolean {
|
|
58
|
+
if (content.length === 0) return false;
|
|
59
|
+
// Rust's `u64::from_str_radix(s, 10)` (used by `parse::<u64>`) accepts
|
|
60
|
+
// ASCII digits only — no leading sign, whitespace, or underscores —
|
|
61
|
+
// and tolerates leading zeros. Anything else falls back to a named
|
|
62
|
+
// KnownValue.
|
|
63
|
+
for (let i = 0; i < content.length; i++) {
|
|
64
|
+
const c = content.charCodeAt(i);
|
|
65
|
+
if (c < 0x30 || c > 0x39) return false;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const value = BigInt(content);
|
|
69
|
+
return value >= 0n && value <= U64_MAX;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2023-2026 Blockchain Commons, LLC
|
|
3
|
+
* Copyright © 2025-2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* Null parser — port of `bc-envelope-pattern-rust` `parse/leaf/null_parser.rs`.
|
|
6
|
+
*
|
|
7
|
+
* @module envelope-pattern/parse/leaf/null-parser
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ok, type Result } from "../../error";
|
|
11
|
+
import { type Pattern, nullPattern } from "../../pattern";
|
|
12
|
+
import type { Lexer } from "../token";
|
|
13
|
+
|
|
14
|
+
export function parseNull(_lexer: Lexer): Result<Pattern> {
|
|
15
|
+
return ok(nullPattern());
|
|
16
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2023-2026 Blockchain Commons, LLC
|
|
3
|
+
* Copyright © 2025-2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* Number parsers — port of `bc-envelope-pattern-rust` `parse/leaf/number_parser.rs`.
|
|
6
|
+
*
|
|
7
|
+
* @module envelope-pattern/parse/leaf/number-parser
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { type Result, err, ok, unexpectedEndOfInput, unexpectedToken } from "../../error";
|
|
11
|
+
import {
|
|
12
|
+
type Pattern,
|
|
13
|
+
NumberPattern,
|
|
14
|
+
number,
|
|
15
|
+
numberRange,
|
|
16
|
+
numberGreaterThan,
|
|
17
|
+
numberLessThan,
|
|
18
|
+
patternLeaf,
|
|
19
|
+
leafNumber,
|
|
20
|
+
} from "../../pattern";
|
|
21
|
+
import type { Lexer } from "../token";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parses an optional `...end` suffix following an already-consumed number,
|
|
25
|
+
* mirroring Rust `parse_number_range_or_comparison`.
|
|
26
|
+
*/
|
|
27
|
+
export function parseNumberRangeOrComparison(lexer: Lexer, firstValue: number): Result<Pattern> {
|
|
28
|
+
const next = lexer.peekToken();
|
|
29
|
+
if (next === undefined) {
|
|
30
|
+
return ok(number(firstValue));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (next.token.type === "Ellipsis") {
|
|
34
|
+
lexer.next(); // consume ...
|
|
35
|
+
const endToken = lexer.next();
|
|
36
|
+
if (endToken === undefined) {
|
|
37
|
+
return err(unexpectedEndOfInput());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let endValue: number;
|
|
41
|
+
if (
|
|
42
|
+
endToken.token.type === "UnsignedInteger" ||
|
|
43
|
+
endToken.token.type === "Integer" ||
|
|
44
|
+
endToken.token.type === "Float"
|
|
45
|
+
) {
|
|
46
|
+
if (!endToken.token.value.ok) return err(endToken.token.value.error);
|
|
47
|
+
endValue = endToken.token.value.value;
|
|
48
|
+
} else {
|
|
49
|
+
return err(unexpectedToken(endToken.token, endToken.span));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return ok(numberRange(firstValue, endValue));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return ok(number(firstValue));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parses a number following a comparison operator, mirroring Rust
|
|
60
|
+
* `parse_comparison_number`.
|
|
61
|
+
*/
|
|
62
|
+
export function parseComparisonNumber(lexer: Lexer, op: ">=" | "<=" | ">" | "<"): Result<Pattern> {
|
|
63
|
+
const numToken = lexer.next();
|
|
64
|
+
if (numToken === undefined) {
|
|
65
|
+
return err(unexpectedEndOfInput());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let value: number;
|
|
69
|
+
if (
|
|
70
|
+
numToken.token.type === "UnsignedInteger" ||
|
|
71
|
+
numToken.token.type === "Integer" ||
|
|
72
|
+
numToken.token.type === "Float"
|
|
73
|
+
) {
|
|
74
|
+
if (!numToken.token.value.ok) return err(numToken.token.value.error);
|
|
75
|
+
value = numToken.token.value.value;
|
|
76
|
+
} else {
|
|
77
|
+
return err(unexpectedToken(numToken.token, numToken.span));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
switch (op) {
|
|
81
|
+
case ">=":
|
|
82
|
+
return ok(patternLeaf(leafNumber(NumberPattern.greaterThanOrEqual(value))));
|
|
83
|
+
case "<=":
|
|
84
|
+
return ok(patternLeaf(leafNumber(NumberPattern.lessThanOrEqual(value))));
|
|
85
|
+
case ">":
|
|
86
|
+
return ok(numberGreaterThan(value));
|
|
87
|
+
case "<":
|
|
88
|
+
return ok(numberLessThan(value));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2023-2026 Blockchain Commons, LLC
|
|
3
|
+
* Copyright © 2025-2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* Tag parser — port of `bc-envelope-pattern-rust`
|
|
6
|
+
* `parse/leaf/tag_parser.rs`.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors the Rust dispatch exactly: lookahead for `(`; if absent, return
|
|
9
|
+
* the bare `any_tag()`. Otherwise build a synthetic dcbor-pattern
|
|
10
|
+
* expression `tagged(<inner>)`, parse it via `@bcts/dcbor-pattern`, and
|
|
11
|
+
* extract the resulting `TaggedPattern` to wrap as an envelope-pattern
|
|
12
|
+
* leaf. This keeps the **full** tag selector (number, name, regex)
|
|
13
|
+
* intact — the previous port discarded the tag value entirely.
|
|
14
|
+
*
|
|
15
|
+
* @module envelope-pattern/parse/leaf/tag-parser
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
parse as parseDcborPattern,
|
|
20
|
+
patternDisplay as dcborPatternDisplay,
|
|
21
|
+
} from "@bcts/dcbor-pattern";
|
|
22
|
+
import {
|
|
23
|
+
type Result,
|
|
24
|
+
err,
|
|
25
|
+
expectedCloseParen,
|
|
26
|
+
ok,
|
|
27
|
+
unexpectedEndOfInput,
|
|
28
|
+
unexpectedToken,
|
|
29
|
+
} from "../../error";
|
|
30
|
+
import { type Pattern, TaggedPattern, anyTag, patternLeaf, leafTag } from "../../pattern";
|
|
31
|
+
import type { Lexer } from "../token";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse `tagged` and `tagged(...)` patterns.
|
|
35
|
+
*/
|
|
36
|
+
export function parseTag(lexer: Lexer): Result<Pattern> {
|
|
37
|
+
const next = lexer.peekToken();
|
|
38
|
+
if (next?.token.type !== "ParenOpen") {
|
|
39
|
+
return ok(anyTag());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
lexer.next(); // consume (
|
|
43
|
+
|
|
44
|
+
const remainder = lexer.remainder();
|
|
45
|
+
const closeIdx = findMatchingCloseParen(remainder);
|
|
46
|
+
if (closeIdx === undefined) {
|
|
47
|
+
return err(expectedCloseParen(lexer.span()));
|
|
48
|
+
}
|
|
49
|
+
const innerContent = remainder.slice(0, closeIdx);
|
|
50
|
+
const taggedExpr = `tagged(${innerContent})`;
|
|
51
|
+
|
|
52
|
+
const dcborResult = parseDcborPattern(taggedExpr);
|
|
53
|
+
if (dcborResult.ok) {
|
|
54
|
+
const dcborPattern = dcborResult.value;
|
|
55
|
+
if (dcborPattern.kind === "Structure" && dcborPattern.pattern.type === "Tagged") {
|
|
56
|
+
lexer.bump(closeIdx);
|
|
57
|
+
const close = lexer.next();
|
|
58
|
+
if (close === undefined) {
|
|
59
|
+
return err(expectedCloseParen(lexer.span()));
|
|
60
|
+
}
|
|
61
|
+
if (close.token.type !== "ParenClose") {
|
|
62
|
+
return err(unexpectedToken(close.token, close.span));
|
|
63
|
+
}
|
|
64
|
+
return ok(patternLeaf(leafTag(TaggedPattern.fromDcborPattern(dcborPattern.pattern.pattern))));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Shouldn't happen for a well-formed `tagged(...)` expression, but
|
|
68
|
+
// if dcbor-pattern returned a non-Tagged structure, fall through to
|
|
69
|
+
// the simple parser below (matching Rust's UnexpectedToken arm).
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Fall back to a simple inline parser for `tagged(N)` and
|
|
73
|
+
// `tagged(name)`, mirroring Rust `parse_tag_inner`. This path is
|
|
74
|
+
// exercised only when dcbor-pattern itself can't make sense of the
|
|
75
|
+
// body — e.g. a typo. We re-emit the same error shape Rust emits
|
|
76
|
+
// (Unknown / UnexpectedToken) by giving up and letting the dcbor
|
|
77
|
+
// result propagate.
|
|
78
|
+
const fallback = parseTagInner(innerContent);
|
|
79
|
+
if (!fallback.ok) {
|
|
80
|
+
return fallback;
|
|
81
|
+
}
|
|
82
|
+
lexer.bump(closeIdx);
|
|
83
|
+
const close = lexer.next();
|
|
84
|
+
if (close === undefined) {
|
|
85
|
+
return err(expectedCloseParen(lexer.span()));
|
|
86
|
+
}
|
|
87
|
+
if (close.token.type !== "ParenClose") {
|
|
88
|
+
return err(unexpectedToken(close.token, close.span));
|
|
89
|
+
}
|
|
90
|
+
return ok(fallback.value);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Locate the index of the closing `)` matching the `(` that has already
|
|
95
|
+
* been consumed by `parseTag`. Mirrors Rust `find_matching_paren`.
|
|
96
|
+
*/
|
|
97
|
+
function findMatchingCloseParen(src: string): number | undefined {
|
|
98
|
+
let depth = 0;
|
|
99
|
+
for (let i = 0; i < src.length; i++) {
|
|
100
|
+
const ch = src.charCodeAt(i);
|
|
101
|
+
if (ch === 0x28 /* ( */) {
|
|
102
|
+
depth += 1;
|
|
103
|
+
} else if (ch === 0x29 /* ) */) {
|
|
104
|
+
if (depth === 0) return i;
|
|
105
|
+
depth -= 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Fallback for `tagged(N)` and `tagged(name)` when the full delegation
|
|
113
|
+
* to dcbor-pattern fails. Mirrors Rust `parse_tag_inner`.
|
|
114
|
+
*/
|
|
115
|
+
function parseTagInner(src: string): Result<Pattern> {
|
|
116
|
+
const trimmed = src.trim();
|
|
117
|
+
if (trimmed.length === 0) {
|
|
118
|
+
return err(unexpectedEndOfInput());
|
|
119
|
+
}
|
|
120
|
+
if (trimmed.startsWith("/")) {
|
|
121
|
+
// Rust passes this to `parse_text_regex`; with dcbor-pattern as the
|
|
122
|
+
// primary path, regex tagged forms always succeed there. If we reach
|
|
123
|
+
// this fallback, surface as InvalidPattern.
|
|
124
|
+
return err(unexpectedEndOfInput());
|
|
125
|
+
}
|
|
126
|
+
// Try u64 first.
|
|
127
|
+
if (/^\d+$/.test(trimmed)) {
|
|
128
|
+
try {
|
|
129
|
+
const expr = `tagged(${trimmed})`;
|
|
130
|
+
const dcborResult = parseDcborPattern(expr);
|
|
131
|
+
if (
|
|
132
|
+
dcborResult.ok &&
|
|
133
|
+
dcborResult.value.kind === "Structure" &&
|
|
134
|
+
dcborResult.value.pattern.type === "Tagged"
|
|
135
|
+
) {
|
|
136
|
+
return ok(
|
|
137
|
+
patternLeaf(leafTag(TaggedPattern.fromDcborPattern(dcborResult.value.pattern.pattern))),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// ignore
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Treat as a name.
|
|
145
|
+
const expr = `tagged(${trimmed})`;
|
|
146
|
+
const dcborResult = parseDcborPattern(expr);
|
|
147
|
+
if (
|
|
148
|
+
dcborResult.ok &&
|
|
149
|
+
dcborResult.value.kind === "Structure" &&
|
|
150
|
+
dcborResult.value.pattern.type === "Tagged"
|
|
151
|
+
) {
|
|
152
|
+
// Reference dcborPatternDisplay just to keep tree-shaker pinning the
|
|
153
|
+
// import path stable when used elsewhere.
|
|
154
|
+
void dcborPatternDisplay;
|
|
155
|
+
return ok(
|
|
156
|
+
patternLeaf(leafTag(TaggedPattern.fromDcborPattern(dcborResult.value.pattern.pattern))),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return err(unexpectedEndOfInput());
|
|
160
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2023-2026 Blockchain Commons, LLC
|
|
3
|
+
* Copyright © 2025-2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* And parser — port of `bc-envelope-pattern-rust`
|
|
6
|
+
* `parse/meta/and_parser.rs`.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors Rust: `parse_and` calls `parse_primary` (NOT `parse_not`); `!`
|
|
9
|
+
* is handled at a higher precedence level by `parse_not`.
|
|
10
|
+
*
|
|
11
|
+
* @module envelope-pattern/parse/meta/and-parser
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { type Result, ok } from "../../error";
|
|
15
|
+
import { type Pattern, and } from "../../pattern";
|
|
16
|
+
import type { Lexer } from "../token";
|
|
17
|
+
import { parsePrimary } from "./primary-parser";
|
|
18
|
+
|
|
19
|
+
export function parseAnd(lexer: Lexer): Result<Pattern> {
|
|
20
|
+
const patterns: Pattern[] = [];
|
|
21
|
+
const first = parsePrimary(lexer);
|
|
22
|
+
if (!first.ok) return first;
|
|
23
|
+
patterns.push(first.value);
|
|
24
|
+
|
|
25
|
+
while (true) {
|
|
26
|
+
const next = lexer.peekToken();
|
|
27
|
+
if (next?.token.type !== "And") {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
lexer.next(); // consume &
|
|
31
|
+
const nextExpr = parsePrimary(lexer);
|
|
32
|
+
if (!nextExpr.ok) return nextExpr;
|
|
33
|
+
patterns.push(nextExpr.value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (patterns.length === 1) {
|
|
37
|
+
return ok(patterns[0]);
|
|
38
|
+
}
|
|
39
|
+
return ok(and(patterns));
|
|
40
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2023-2026 Blockchain Commons, LLC
|
|
3
|
+
* Copyright © 2025-2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* Capture parser — port of `bc-envelope-pattern-rust`
|
|
6
|
+
* `parse/meta/capture_parser.rs`.
|
|
7
|
+
*
|
|
8
|
+
* The `@name(...)` form requires explicit parentheses. Mirrors Rust
|
|
9
|
+
* exactly:
|
|
10
|
+
* ```ignore
|
|
11
|
+
* @name ( expr )
|
|
12
|
+
* ```
|
|
13
|
+
* The previous TS port called `parse_group` (primary + quantifier), which
|
|
14
|
+
* wrapped the inner expression in a redundant `GroupPattern` and accepted
|
|
15
|
+
* the bare `@name p` form that Rust rejects.
|
|
16
|
+
*
|
|
17
|
+
* @module envelope-pattern/parse/meta/capture-parser
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
type Result,
|
|
22
|
+
err,
|
|
23
|
+
expectedCloseParen,
|
|
24
|
+
ok,
|
|
25
|
+
unexpectedEndOfInput,
|
|
26
|
+
unexpectedToken,
|
|
27
|
+
} from "../../error";
|
|
28
|
+
import { type Pattern, capture } from "../../pattern";
|
|
29
|
+
import type { Lexer } from "../token";
|
|
30
|
+
import { parseOr } from "./or-parser";
|
|
31
|
+
|
|
32
|
+
export function parseCapture(lexer: Lexer, name: string): Result<Pattern> {
|
|
33
|
+
const open = lexer.next();
|
|
34
|
+
if (open === undefined) {
|
|
35
|
+
return err(unexpectedEndOfInput());
|
|
36
|
+
}
|
|
37
|
+
if (open.token.type !== "ParenOpen") {
|
|
38
|
+
return err(unexpectedToken(open.token, open.span));
|
|
39
|
+
}
|
|
40
|
+
const inner = parseOr(lexer);
|
|
41
|
+
if (!inner.ok) return inner;
|
|
42
|
+
const close = lexer.next();
|
|
43
|
+
if (close === undefined) {
|
|
44
|
+
return err(expectedCloseParen(lexer.span()));
|
|
45
|
+
}
|
|
46
|
+
if (close.token.type !== "ParenClose") {
|
|
47
|
+
return err(unexpectedToken(close.token, close.span));
|
|
48
|
+
}
|
|
49
|
+
return ok(capture(name, inner.value));
|
|
50
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2023-2026 Blockchain Commons, LLC
|
|
3
|
+
* Copyright © 2025-2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* Group parser — port of `bc-envelope-pattern-rust`
|
|
6
|
+
* `parse/meta/group_parser.rs`.
|
|
7
|
+
*
|
|
8
|
+
* Called from `parse_primary` after the opening `(` has been consumed.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors Rust exactly:
|
|
11
|
+
*
|
|
12
|
+
* - Parse the inner expression with `parse_or`.
|
|
13
|
+
* - Expect `)`. If missing, surface `ExpectedCloseParen`.
|
|
14
|
+
* - Lookahead **once** for a quantifier suffix. If present, consume it
|
|
15
|
+
* and wrap as `Pattern::repeat(inner, …)`. Otherwise return the inner
|
|
16
|
+
* expression unchanged.
|
|
17
|
+
*
|
|
18
|
+
* The previous TS port wrapped every parenthesised expression in a
|
|
19
|
+
* dedicated `GroupPattern` and applied quantifiers to bare primaries —
|
|
20
|
+
* both broke `format(parse(s)) === s` round-tripping.
|
|
21
|
+
*
|
|
22
|
+
* @module envelope-pattern/parse/meta/group-parser
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { Quantifier, Reluctance } from "@bcts/dcbor-pattern";
|
|
26
|
+
import { type Result, err, expectedCloseParen, ok, unexpectedToken } from "../../error";
|
|
27
|
+
import { type Pattern, repeat } from "../../pattern";
|
|
28
|
+
import type { Lexer } from "../token";
|
|
29
|
+
import { parseOr } from "./or-parser";
|
|
30
|
+
|
|
31
|
+
export function parseGroup(lexer: Lexer): Result<Pattern> {
|
|
32
|
+
const inner = parseOr(lexer);
|
|
33
|
+
if (!inner.ok) return inner;
|
|
34
|
+
|
|
35
|
+
const close = lexer.next();
|
|
36
|
+
if (close === undefined) {
|
|
37
|
+
return err(expectedCloseParen(lexer.span()));
|
|
38
|
+
}
|
|
39
|
+
if (close.token.type !== "ParenClose") {
|
|
40
|
+
return err(unexpectedToken(close.token, close.span));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const next = lexer.peekToken();
|
|
44
|
+
if (next === undefined) {
|
|
45
|
+
return inner;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let quantifier: Quantifier | undefined;
|
|
49
|
+
const tokenType = next.token.type;
|
|
50
|
+
if (tokenType === "RepeatZeroOrMore") {
|
|
51
|
+
quantifier = Quantifier.zeroOrMore(Reluctance.Greedy);
|
|
52
|
+
} else if (tokenType === "RepeatZeroOrMoreLazy") {
|
|
53
|
+
quantifier = Quantifier.zeroOrMore(Reluctance.Lazy);
|
|
54
|
+
} else if (tokenType === "RepeatZeroOrMorePossessive") {
|
|
55
|
+
quantifier = Quantifier.zeroOrMore(Reluctance.Possessive);
|
|
56
|
+
} else if (tokenType === "RepeatOneOrMore") {
|
|
57
|
+
quantifier = Quantifier.oneOrMore(Reluctance.Greedy);
|
|
58
|
+
} else if (tokenType === "RepeatOneOrMoreLazy") {
|
|
59
|
+
quantifier = Quantifier.oneOrMore(Reluctance.Lazy);
|
|
60
|
+
} else if (tokenType === "RepeatOneOrMorePossessive") {
|
|
61
|
+
quantifier = Quantifier.oneOrMore(Reluctance.Possessive);
|
|
62
|
+
} else if (tokenType === "RepeatZeroOrOne") {
|
|
63
|
+
quantifier = Quantifier.zeroOrOne(Reluctance.Greedy);
|
|
64
|
+
} else if (tokenType === "RepeatZeroOrOneLazy") {
|
|
65
|
+
quantifier = Quantifier.zeroOrOne(Reluctance.Lazy);
|
|
66
|
+
} else if (tokenType === "RepeatZeroOrOnePossessive") {
|
|
67
|
+
quantifier = Quantifier.zeroOrOne(Reluctance.Possessive);
|
|
68
|
+
} else if (tokenType === "Range") {
|
|
69
|
+
if (!next.token.value.ok) return err(next.token.value.error);
|
|
70
|
+
quantifier = next.token.value.value;
|
|
71
|
+
} else {
|
|
72
|
+
return inner;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
lexer.next(); // consume the quantifier token
|
|
76
|
+
return ok(repeat(inner.value, quantifier.min(), quantifier.max(), quantifier.reluctance()));
|
|
77
|
+
}
|