kibi-cli 2.31.1 → 2.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-register-proof.d.ts.map +1 -1
- package/dist/cli-register-proof.js +19 -7
- package/dist/cli-register-reporting.d.ts.map +1 -1
- package/dist/cli-register-reporting.js +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +5 -1
- package/dist/commands/check-generated.d.ts +5 -1
- package/dist/commands/check-generated.d.ts.map +1 -1
- package/dist/commands/check-generated.js +24 -35
- package/dist/commands/check.d.ts +1 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +14 -5
- package/dist/commands/discovery-table.d.ts.map +1 -1
- package/dist/commands/discovery-table.js +51 -4
- package/dist/commands/doctor.js +19 -4
- package/dist/commands/init-helpers.d.ts.map +1 -1
- package/dist/commands/init-helpers.js +8 -5
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +109 -5
- package/dist/commands/prove.d.ts +48 -1
- package/dist/commands/prove.d.ts.map +1 -1
- package/dist/commands/prove.js +293 -8
- package/dist/commands/report.d.ts +1 -0
- package/dist/commands/report.d.ts.map +1 -1
- package/dist/commands/report.js +70 -5
- package/dist/commands/signoff.d.ts.map +1 -1
- package/dist/commands/signoff.js +20 -1
- package/dist/commands/sync/manifest.d.ts +10 -1
- package/dist/commands/sync/manifest.d.ts.map +1 -1
- package/dist/commands/sync/manifest.js +106 -17
- package/dist/commands/sync/persistence.d.ts.map +1 -1
- package/dist/commands/sync/persistence.js +16 -5
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +19 -0
- package/dist/diagnostics.js +1 -1
- package/dist/engine.js +1 -1
- package/dist/extractors/markdown.d.ts +4 -2
- package/dist/extractors/markdown.d.ts.map +1 -1
- package/dist/extractors/markdown.js +17 -0
- package/dist/extractors/symbol-coordinates.js +2 -2
- package/dist/extractors/symbols-coordinator.d.ts +2 -4
- package/dist/extractors/symbols-coordinator.d.ts.map +1 -1
- package/dist/extractors/symbols-coordinator.js +1 -2
- package/dist/operations/bootstrap/candidate-helpers.d.ts +3 -0
- package/dist/operations/bootstrap/candidate-helpers.d.ts.map +1 -1
- package/dist/operations/bootstrap/candidate-helpers.js +4 -0
- package/dist/operations/migration/evidence-floor.d.ts +41 -0
- package/dist/operations/migration/evidence-floor.d.ts.map +1 -0
- package/dist/operations/migration/evidence-floor.js +230 -0
- package/dist/operations/migration/proof-contract-claims.d.ts +72 -0
- package/dist/operations/migration/proof-contract-claims.d.ts.map +1 -0
- package/dist/operations/migration/proof-contract-claims.js +215 -0
- package/dist/operations/migration/schema6.d.ts +6 -0
- package/dist/operations/migration/schema6.d.ts.map +1 -1
- package/dist/operations/migration/schema6.js +92 -0
- package/dist/operations/migration/symbol-coordinates-refresh.d.ts +3 -2
- package/dist/operations/migration/symbol-coordinates-refresh.d.ts.map +1 -1
- package/dist/operations/migration/symbol-coordinates-refresh.js +4 -3
- package/dist/operations/migration/untrack-symbol-coordinates.d.ts +23 -0
- package/dist/operations/migration/untrack-symbol-coordinates.d.ts.map +1 -0
- package/dist/operations/migration/untrack-symbol-coordinates.js +67 -0
- package/dist/operations/modeling/requirement-modeler.d.ts.map +1 -1
- package/dist/operations/modeling/requirement-modeler.js +48 -1
- package/dist/operations/modeling/requirement-types.d.ts +2 -1
- package/dist/operations/modeling/requirement-types.d.ts.map +1 -1
- package/dist/operations/modeling/requirement-utils.d.ts +1 -1
- package/dist/operations/modeling/requirement-utils.d.ts.map +1 -1
- package/dist/operations/modeling/requirement-utils.js +8 -1
- package/dist/operations/mutation/omitted-properties.d.ts +11 -0
- package/dist/operations/mutation/omitted-properties.d.ts.map +1 -0
- package/dist/operations/mutation/omitted-properties.js +76 -0
- package/dist/operations/mutation/serialization.d.ts.map +1 -1
- package/dist/operations/mutation/serialization.js +1 -0
- package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
- package/dist/operations/mutation/source-authoring.js +39 -20
- package/dist/operations/mutation/strict-fact.d.ts.map +1 -1
- package/dist/operations/mutation/strict-fact.js +6 -2
- package/dist/operations/mutation/symbol-granularity.d.ts.map +1 -1
- package/dist/operations/mutation/symbol-granularity.js +5 -3
- package/dist/operations/mutation/upsert.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.js +7 -1
- package/dist/operations/mutation/validation.d.ts +2 -0
- package/dist/operations/mutation/validation.d.ts.map +1 -1
- package/dist/operations/mutation/validation.js +4 -0
- package/dist/operations/proof/claim-bindings.d.ts +41 -0
- package/dist/operations/proof/claim-bindings.d.ts.map +1 -0
- package/dist/operations/proof/claim-bindings.js +364 -0
- package/dist/operations/proof/code-certificates.d.ts +155 -0
- package/dist/operations/proof/code-certificates.d.ts.map +1 -0
- package/dist/operations/proof/code-certificates.js +894 -0
- package/dist/operations/proof/code-models.d.ts +96 -0
- package/dist/operations/proof/code-models.d.ts.map +1 -0
- package/dist/operations/proof/code-models.js +601 -0
- package/dist/operations/proof/code-runner.d.ts +40 -0
- package/dist/operations/proof/code-runner.d.ts.map +1 -0
- package/dist/operations/proof/code-runner.js +164 -0
- package/dist/operations/proof/code-scope.d.ts +14 -1
- package/dist/operations/proof/code-scope.d.ts.map +1 -1
- package/dist/operations/proof/code-scope.js +44 -2
- package/dist/operations/proof/compact-receipts.d.ts.map +1 -1
- package/dist/operations/proof/compact-receipts.js +4 -3
- package/dist/operations/proof/explain.d.ts +32 -0
- package/dist/operations/proof/explain.d.ts.map +1 -1
- package/dist/operations/proof/explain.js +91 -0
- package/dist/operations/proof/ingest-proof.d.ts +9 -1
- package/dist/operations/proof/ingest-proof.d.ts.map +1 -1
- package/dist/operations/proof/ingest-proof.js +42 -2
- package/dist/operations/proof/receipt-compaction.d.ts +2 -0
- package/dist/operations/proof/receipt-compaction.d.ts.map +1 -1
- package/dist/operations/proof/receipt-compaction.js +6 -2
- package/dist/operations/proof/regular-language.d.ts +89 -0
- package/dist/operations/proof/regular-language.d.ts.map +1 -0
- package/dist/operations/proof/regular-language.js +943 -0
- package/dist/operations/proof/source-code-readers.d.ts +12 -0
- package/dist/operations/proof/source-code-readers.d.ts.map +1 -0
- package/dist/operations/proof/source-code-readers.js +699 -0
- package/dist/operations/semantic-advisor/analyze-prose.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/analyze-prose.js +2 -1
- package/dist/operations/semantic-advisor/shared.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/shared.js +4 -0
- package/dist/operations/semantic-advisor/strict-count-rules.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/strict-count-rules.js +8 -2
- package/dist/operations/semantic-advisor/strict-rules.d.ts +8 -0
- package/dist/operations/semantic-advisor/strict-rules.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/strict-rules.js +234 -4
- package/dist/operations/semantic-advisor/types.d.ts +3 -2
- package/dist/operations/semantic-advisor/types.d.ts.map +1 -1
- package/dist/plugins/approved-source-analyzers.generated.d.ts +6 -6
- package/dist/plugins/approved-source-analyzers.generated.js +6 -6
- package/dist/prolog.d.ts.map +1 -1
- package/dist/prolog.js +13 -3
- package/dist/proof/command-test-report.d.ts +4 -3
- package/dist/proof/command-test-report.d.ts.map +1 -1
- package/dist/proof/command-test-report.js +4 -10
- package/dist/proof/evaluate.d.ts.map +1 -1
- package/dist/proof/evaluate.js +18 -2
- package/dist/proof/inspect.d.ts.map +1 -1
- package/dist/proof/inspect.js +58 -0
- package/dist/proof/producers/playwright-reporter.d.ts +3 -0
- package/dist/proof/producers/playwright-reporter.d.ts.map +1 -1
- package/dist/proof/producers/playwright-reporter.js +25 -2
- package/dist/proof/step-runner.d.ts +36 -0
- package/dist/proof/step-runner.d.ts.map +1 -0
- package/dist/proof/step-runner.js +234 -0
- package/dist/public/evidence-levels.d.ts +12 -0
- package/dist/public/evidence-levels.d.ts.map +1 -0
- package/dist/public/evidence-levels.js +19 -0
- package/dist/public/impact/coverage-depth-quality.d.ts +7 -0
- package/dist/public/impact/coverage-depth-quality.d.ts.map +1 -1
- package/dist/public/impact/coverage-depth-quality.js +59 -0
- package/dist/public/impact/full-kb-quality.d.ts.map +1 -1
- package/dist/public/impact/full-kb-quality.js +3 -1
- package/dist/public/operations/catalog.d.ts +315 -11
- package/dist/public/operations/catalog.d.ts.map +1 -1
- package/dist/public/operations/check-executor.d.ts +2 -0
- package/dist/public/operations/check-executor.d.ts.map +1 -1
- package/dist/public/operations/check-executor.js +6 -0
- package/dist/public/operations/evidence-floor.d.ts +48 -0
- package/dist/public/operations/evidence-floor.d.ts.map +1 -0
- package/dist/public/operations/evidence-floor.js +143 -0
- package/dist/public/operations/migration-plan.d.ts +19 -5
- package/dist/public/operations/migration-plan.d.ts.map +1 -1
- package/dist/public/operations/migration-plan.js +22 -5
- package/dist/public/operations/node-ports.d.ts.map +1 -1
- package/dist/public/operations/node-ports.js +4 -1
- package/dist/public/operations/proof-contract-claims.d.ts +26 -0
- package/dist/public/operations/proof-contract-claims.d.ts.map +1 -0
- package/dist/public/operations/proof-contract-claims.js +71 -0
- package/dist/public/operations/specs/check-rules.generated.d.ts +1 -1
- package/dist/public/operations/specs/check-rules.generated.d.ts.map +1 -1
- package/dist/public/operations/specs/check-rules.generated.js +3 -1
- package/dist/public/operations/specs/check.d.ts +1 -1
- package/dist/public/operations/specs/composite.d.ts +16 -3
- package/dist/public/operations/specs/composite.d.ts.map +1 -1
- package/dist/public/operations/specs/modeling.d.ts +16 -3
- package/dist/public/operations/specs/modeling.d.ts.map +1 -1
- package/dist/public/operations/specs/modeling.js +22 -3
- package/dist/public/operations/specs/mutation-schemas.d.ts +289 -7
- package/dist/public/operations/specs/mutation-schemas.d.ts.map +1 -1
- package/dist/public/operations/specs/mutation-schemas.js +28 -4
- package/dist/public/operations/specs/proof.d.ts +9 -0
- package/dist/public/operations/specs/proof.d.ts.map +1 -1
- package/dist/public/operations/specs/reporting.d.ts.map +1 -1
- package/dist/public/operations/specs/reporting.js +19 -10
- package/dist/public/operations/specs/upsert.d.ts +289 -7
- package/dist/public/operations/specs/upsert.d.ts.map +1 -1
- package/dist/public/proof-certificates.d.ts +163 -0
- package/dist/public/proof-certificates.d.ts.map +1 -0
- package/dist/public/proof-certificates.js +419 -0
- package/dist/public/proof-protocol.d.ts +259 -3
- package/dist/public/proof-protocol.d.ts.map +1 -1
- package/dist/public/proof-protocol.js +524 -3
- package/dist/public/proof-receipt.d.ts +90 -6
- package/dist/public/proof-receipt.d.ts.map +1 -1
- package/dist/public/proof-receipt.js +88 -7
- package/dist/public/proof-sources.d.ts +102 -0
- package/dist/public/proof-sources.d.ts.map +1 -0
- package/dist/public/proof-sources.js +1292 -0
- package/dist/public/schemas/entity.d.ts.map +1 -1
- package/dist/public/schemas/entity.js +39 -2
- package/dist/public/skills/kibi-bootstrap/SKILL.md +2 -2
- package/dist/public/skills/kibi-commit-review/SKILL.md +8 -8
- package/dist/public/skills/kibi-commit-review/resources/staged-gate.md +12 -7
- package/dist/public/skills/kibi-decisions/SKILL.md +13 -2
- package/dist/public/skills/kibi-decisions/resources/decision-records.md +9 -1
- package/dist/public/skills/kibi-freshness/SKILL.md +5 -1
- package/dist/public/skills/kibi-traceability/SKILL.md +12 -1
- package/dist/public/skills/kibi-usage/SKILL.md +8 -4
- package/dist/public/skills/kibi-usage/resources/proof.md +221 -20
- package/dist/public/skills/kibi-usage/resources/workflows.md +4 -4
- package/dist/report/html-report.css +83 -11
- package/dist/report/html-report.d.ts +8 -0
- package/dist/report/html-report.d.ts.map +1 -1
- package/dist/report/html-report.js +214 -22
- package/dist/report/report-view.d.ts +2 -2
- package/dist/report/report-view.d.ts.map +1 -1
- package/dist/report/report-view.js +8 -0
- package/dist/schemas/entity.schema.json +404 -5
- package/dist/search-answer.d.ts.map +1 -1
- package/dist/search-answer.js +1 -0
- package/dist/signoff/html.d.ts.map +1 -1
- package/dist/signoff/html.js +3 -0
- package/dist/signoff/markdown.d.ts.map +1 -1
- package/dist/signoff/markdown.js +3 -0
- package/dist/signoff/model.d.ts +20 -0
- package/dist/signoff/model.d.ts.map +1 -1
- package/dist/signoff/model.js +20 -1
- package/dist/traceability/evidence-model.d.ts +13 -12
- package/dist/traceability/evidence-model.d.ts.map +1 -1
- package/dist/traceability/evidence-model.js +1 -1
- package/dist/traceability/impact-knowledge.d.ts.map +1 -1
- package/dist/traceability/impact-knowledge.js +1 -0
- package/dist/traceability/staged-diagnostics.d.ts.map +1 -1
- package/dist/traceability/staged-diagnostics.js +24 -20
- package/dist/traceability/staged-impact-contract.d.ts +4 -4
- package/dist/traceability/staged-impact-contract.js +9 -9
- package/dist/traceability/staged-symbols-manifest.d.ts +11 -3
- package/dist/traceability/staged-symbols-manifest.d.ts.map +1 -1
- package/dist/traceability/staged-symbols-manifest.js +47 -140
- package/dist/traceability/temp-kb.d.ts.map +1 -1
- package/dist/traceability/temp-kb.js +1 -0
- package/dist/types/entities.d.ts +5 -2
- package/dist/types/entities.d.ts.map +1 -1
- package/dist/utils/merge-driver-setup.d.ts +8 -1
- package/dist/utils/merge-driver-setup.d.ts.map +1 -1
- package/dist/utils/merge-driver-setup.js +23 -2
- package/dist/utils/rule-registry.generated.d.ts +10 -0
- package/dist/utils/rule-registry.generated.d.ts.map +1 -1
- package/dist/utils/rule-registry.generated.js +12 -0
- package/dist/utils/schema-version.d.ts +1 -1
- package/dist/utils/schema-version.d.ts.map +1 -1
- package/dist/utils/schema-version.js +7 -1
- package/dist/utils/strict-modeling.d.ts +3 -2
- package/dist/utils/strict-modeling.d.ts.map +1 -1
- package/dist/utils/strict-modeling.js +37 -4
- package/dist/workspace-cwd.d.ts +15 -0
- package/dist/workspace-cwd.d.ts.map +1 -0
- package/dist/workspace-cwd.js +86 -0
- package/package.json +6 -2
- package/schema/entities.pl +6 -0
- package/schema/validation.pl +7 -2
- package/src/public/evidence-levels.ts +23 -0
- package/src/public/impact/coverage-depth-quality.ts +73 -0
- package/src/public/impact/full-kb-quality.ts +6 -1
- package/src/public/operations/check-executor.ts +13 -0
- package/src/public/operations/evidence-floor.ts +203 -0
- package/src/public/operations/migration-plan.ts +39 -6
- package/src/public/operations/node-ports.ts +3 -1
- package/src/public/operations/proof-contract-claims.ts +103 -0
- package/src/public/operations/specs/check-rules.generated.ts +3 -1
- package/src/public/operations/specs/modeling.ts +24 -5
- package/src/public/operations/specs/mutation-schemas.ts +30 -4
- package/src/public/operations/specs/reporting.ts +20 -10
- package/src/public/proof-certificates.ts +566 -0
- package/src/public/proof-protocol-types.d.ts +69 -1
- package/src/public/proof-protocol.ts +694 -3
- package/src/public/proof-receipt.ts +133 -7
- package/src/public/proof-sources.ts +1656 -0
- package/src/public/schemas/entity.ts +51 -3
- package/src/public/skills/kibi-bootstrap/SKILL.md +2 -2
- package/src/public/skills/kibi-commit-review/SKILL.md +8 -8
- package/src/public/skills/kibi-commit-review/resources/staged-gate.md +12 -7
- package/src/public/skills/kibi-decisions/SKILL.md +13 -2
- package/src/public/skills/kibi-decisions/resources/decision-records.md +9 -1
- package/src/public/skills/kibi-freshness/SKILL.md +5 -1
- package/src/public/skills/kibi-traceability/SKILL.md +12 -1
- package/src/public/skills/kibi-usage/SKILL.md +8 -4
- package/src/public/skills/kibi-usage/resources/proof.md +221 -20
- package/src/public/skills/kibi-usage/resources/workflows.md +4 -4
- package/src/schemas/entity.schema.json +404 -5
- package/dist/commands/proof-impact.d.ts +0 -7
- package/dist/commands/proof-impact.d.ts.map +0 -1
- package/dist/commands/proof-impact.js +0 -39
- package/dist/operations/proof/baseline-diff.d.ts +0 -41
- package/dist/operations/proof/baseline-diff.d.ts.map +0 -1
- package/dist/operations/proof/baseline-diff.js +0 -153
- package/dist/operations/proof/impact.d.ts +0 -51
- package/dist/operations/proof/impact.d.ts.map +0 -1
- package/dist/operations/proof/impact.js +0 -138
- package/dist/traceability/reviewed-coordinate-refresh.d.ts +0 -10
- package/dist/traceability/reviewed-coordinate-refresh.d.ts.map +0 -1
- package/dist/traceability/reviewed-coordinate-refresh.js +0 -49
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Kibi — repo-local, per-branch, queryable long-term memory for software projects
|
|
3
|
+
Copyright (C) 2026 Piotr Franczyk
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
(at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_UNIT = 0xffff;
|
|
19
|
+
const MAX_CODE_POINT = 0x10ffff;
|
|
20
|
+
const MAX_NFA_STATES = 20_000;
|
|
21
|
+
const MAX_PRODUCT_STATES = 50_000;
|
|
22
|
+
/** A pattern Kibi refuses to model; the message says why. */
|
|
23
|
+
export class UnsupportedPattern extends Error {
|
|
24
|
+
}
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Code point sets
|
|
27
|
+
function normalize(ranges) {
|
|
28
|
+
const sorted = [...ranges]
|
|
29
|
+
.filter(([lo, hi]) => lo <= hi)
|
|
30
|
+
.sort((a, b) => a[0] - b[0]);
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const [lo, hi] of sorted) {
|
|
33
|
+
const last = out[out.length - 1];
|
|
34
|
+
if (last !== undefined && lo <= last[1] + 1)
|
|
35
|
+
out[out.length - 1] = [last[0], Math.max(last[1], hi)];
|
|
36
|
+
else
|
|
37
|
+
out.push([lo, hi]);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
function complement(ranges, max) {
|
|
42
|
+
const out = [];
|
|
43
|
+
let next = 0;
|
|
44
|
+
for (const [lo, hi] of normalize(ranges)) {
|
|
45
|
+
if (lo > next)
|
|
46
|
+
out.push([next, lo - 1]);
|
|
47
|
+
next = hi + 1;
|
|
48
|
+
}
|
|
49
|
+
if (next <= max)
|
|
50
|
+
out.push([next, max]);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function single(code) {
|
|
54
|
+
return [[code, code]];
|
|
55
|
+
}
|
|
56
|
+
const DIGIT = [[0x30, 0x39]];
|
|
57
|
+
const WORD = normalize([
|
|
58
|
+
[0x30, 0x39],
|
|
59
|
+
[0x41, 0x5a],
|
|
60
|
+
[0x5f, 0x5f],
|
|
61
|
+
[0x61, 0x7a],
|
|
62
|
+
]);
|
|
63
|
+
const JS_SPACE = normalize([
|
|
64
|
+
[0x09, 0x0d],
|
|
65
|
+
[0x20, 0x20],
|
|
66
|
+
[0xa0, 0xa0],
|
|
67
|
+
[0x1680, 0x1680],
|
|
68
|
+
[0x2000, 0x200a],
|
|
69
|
+
[0x2028, 0x2029],
|
|
70
|
+
[0x202f, 0x202f],
|
|
71
|
+
[0x205f, 0x205f],
|
|
72
|
+
[0x3000, 0x3000],
|
|
73
|
+
[0xfeff, 0xfeff],
|
|
74
|
+
]);
|
|
75
|
+
const PYTHON_ASCII_SPACE = normalize([
|
|
76
|
+
[0x09, 0x0d],
|
|
77
|
+
[0x20, 0x20],
|
|
78
|
+
]);
|
|
79
|
+
// str.isspace(): bidirectional types WS, B and S, and category Zs.
|
|
80
|
+
const PYTHON_UNICODE_SPACE = normalize([
|
|
81
|
+
[0x09, 0x0d],
|
|
82
|
+
[0x1c, 0x20],
|
|
83
|
+
[0x85, 0x85],
|
|
84
|
+
[0xa0, 0xa0],
|
|
85
|
+
[0x1680, 0x1680],
|
|
86
|
+
[0x2000, 0x200a],
|
|
87
|
+
[0x2028, 0x2029],
|
|
88
|
+
[0x202f, 0x202f],
|
|
89
|
+
[0x205f, 0x205f],
|
|
90
|
+
[0x3000, 0x3000],
|
|
91
|
+
]);
|
|
92
|
+
const JS_LINE_TERMINATORS = normalize([
|
|
93
|
+
[0x0a, 0x0a],
|
|
94
|
+
[0x0d, 0x0d],
|
|
95
|
+
[0x2028, 0x2029],
|
|
96
|
+
]);
|
|
97
|
+
/**
|
|
98
|
+
* The characters equal to an ASCII letter under case-insensitive matching,
|
|
99
|
+
* beyond its ASCII partner: JavaScript with the u flag folds U+017F to s
|
|
100
|
+
* and U+212A to k (simple case folding); Python's Unicode IGNORECASE also
|
|
101
|
+
* treats U+0130 and U+0131 as i (its documented four extra letters).
|
|
102
|
+
*/
|
|
103
|
+
function caseExtras(flavor, unicode, ascii) {
|
|
104
|
+
const extras = new Map();
|
|
105
|
+
if (!unicode || ascii)
|
|
106
|
+
return extras;
|
|
107
|
+
const add = (letter, ...codes) => {
|
|
108
|
+
for (const variant of [letter.toLowerCase(), letter.toUpperCase()])
|
|
109
|
+
extras.set(variant.charCodeAt(0), codes);
|
|
110
|
+
};
|
|
111
|
+
add("s", 0x017f);
|
|
112
|
+
add("k", 0x212a);
|
|
113
|
+
if (flavor === "python")
|
|
114
|
+
add("i", 0x0130, 0x0131);
|
|
115
|
+
return extras;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* A character with no case variants: no other character upper- or
|
|
119
|
+
* lower-cases to it, so every engine's case folding leaves it alone.
|
|
120
|
+
*/
|
|
121
|
+
function caseless(code) {
|
|
122
|
+
const char = String.fromCodePoint(code);
|
|
123
|
+
return char.toUpperCase() === char && char.toLowerCase() === char;
|
|
124
|
+
}
|
|
125
|
+
function caseClosed(ranges, syntax) {
|
|
126
|
+
if (!syntax.ignoreCase)
|
|
127
|
+
return ranges;
|
|
128
|
+
const extras = caseExtras(syntax.flavor, syntax.unicode, syntax.ascii);
|
|
129
|
+
const added = [];
|
|
130
|
+
for (const [lo, hi] of ranges) {
|
|
131
|
+
for (let code = Math.max(lo, 0x41); code <= Math.min(hi, 0x7a); code++) {
|
|
132
|
+
if (code >= 0x41 && code <= 0x5a)
|
|
133
|
+
added.push([code + 32, code + 32]);
|
|
134
|
+
else if (code >= 0x61 && code <= 0x7a)
|
|
135
|
+
added.push([code - 32, code - 32]);
|
|
136
|
+
for (const extra of extras.get(code) ?? [])
|
|
137
|
+
added.push([extra, extra]);
|
|
138
|
+
}
|
|
139
|
+
for (const [letter, codes] of extras)
|
|
140
|
+
for (const extra of codes)
|
|
141
|
+
if (extra >= lo && extra <= hi) {
|
|
142
|
+
added.push([letter, letter]);
|
|
143
|
+
for (const other of codes)
|
|
144
|
+
added.push([other, other]);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return normalize([...ranges, ...added]);
|
|
148
|
+
}
|
|
149
|
+
function syntaxOf(spec) {
|
|
150
|
+
const flags = spec.flags ?? "";
|
|
151
|
+
const allowed = spec.flavor === "js" ? "disu" : "iasu";
|
|
152
|
+
for (const flag of flags) {
|
|
153
|
+
if (!allowed.includes(flag))
|
|
154
|
+
throw new UnsupportedPattern(spec.flavor === "js"
|
|
155
|
+
? `the ${flag} flag is not supported (g and y make test() depend on lastIndex, m makes ^ and $ match at every line, v changes class syntax)`
|
|
156
|
+
: `the Python flag ${flag} is not supported (MULTILINE, VERBOSE and LOCALE change what the pattern means)`);
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
flavor: spec.flavor,
|
|
160
|
+
unicode: spec.flavor === "python" || flags.includes("u"),
|
|
161
|
+
ignoreCase: flags.includes("i"),
|
|
162
|
+
dotAll: flags.includes("s"),
|
|
163
|
+
ascii: spec.flavor === "python" && flags.includes("a"),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
class Parser {
|
|
167
|
+
source;
|
|
168
|
+
syntax;
|
|
169
|
+
index = 0;
|
|
170
|
+
constructor(source, syntax) {
|
|
171
|
+
this.source = source;
|
|
172
|
+
this.syntax = syntax;
|
|
173
|
+
}
|
|
174
|
+
parse() {
|
|
175
|
+
const node = this.alternation();
|
|
176
|
+
if (this.index < this.source.length)
|
|
177
|
+
throw new UnsupportedPattern(`unexpected ${JSON.stringify(this.source[this.index])} at offset ${this.index}`);
|
|
178
|
+
return node;
|
|
179
|
+
}
|
|
180
|
+
peek(offset = 0) {
|
|
181
|
+
return this.source[this.index + offset];
|
|
182
|
+
}
|
|
183
|
+
fail(message) {
|
|
184
|
+
throw new UnsupportedPattern(`${message} (at offset ${this.index})`);
|
|
185
|
+
}
|
|
186
|
+
alternation() {
|
|
187
|
+
const items = [this.sequence()];
|
|
188
|
+
while (this.peek() === "|") {
|
|
189
|
+
this.index++;
|
|
190
|
+
items.push(this.sequence());
|
|
191
|
+
}
|
|
192
|
+
return items.length === 1 ? items[0] : { type: "alt", items };
|
|
193
|
+
}
|
|
194
|
+
sequence() {
|
|
195
|
+
const items = [];
|
|
196
|
+
while (this.index < this.source.length) {
|
|
197
|
+
const char = this.peek();
|
|
198
|
+
if (char === "|" || char === ")")
|
|
199
|
+
break;
|
|
200
|
+
const atom = this.atom();
|
|
201
|
+
items.push(this.quantified(atom));
|
|
202
|
+
}
|
|
203
|
+
return { type: "seq", items };
|
|
204
|
+
}
|
|
205
|
+
/** A counted quantifier at `index`: its bounds and length, or null. */
|
|
206
|
+
counted() {
|
|
207
|
+
const rest = this.source.slice(this.index);
|
|
208
|
+
const match = this.syntax.flavor === "python"
|
|
209
|
+
? /^\{(\d*)(?:(,)(\d*))?\}/.exec(rest)
|
|
210
|
+
: /^\{(\d+)(?:(,)(\d*))?\}/.exec(rest);
|
|
211
|
+
if (match === null || (match[1] === "" && match[2] === undefined))
|
|
212
|
+
return null;
|
|
213
|
+
if (match[1] === "" && match[3] === "")
|
|
214
|
+
this.fail("a {,} quantifier");
|
|
215
|
+
const min = match[1] === "" ? 0 : Number(match[1]);
|
|
216
|
+
const max = match[2] === undefined
|
|
217
|
+
? min
|
|
218
|
+
: match[3] === ""
|
|
219
|
+
? Number.POSITIVE_INFINITY
|
|
220
|
+
: Number(match[3]);
|
|
221
|
+
return { min, max, length: match[0].length };
|
|
222
|
+
}
|
|
223
|
+
quantified(atom) {
|
|
224
|
+
const char = this.peek();
|
|
225
|
+
let min;
|
|
226
|
+
let max;
|
|
227
|
+
if (char === "*")
|
|
228
|
+
[min, max] = [0, Number.POSITIVE_INFINITY];
|
|
229
|
+
else if (char === "+")
|
|
230
|
+
[min, max] = [1, Number.POSITIVE_INFINITY];
|
|
231
|
+
else if (char === "?")
|
|
232
|
+
[min, max] = [0, 1];
|
|
233
|
+
else if (char === "{") {
|
|
234
|
+
const bounds = this.counted();
|
|
235
|
+
if (bounds === null) {
|
|
236
|
+
if (this.syntax.flavor === "js" && this.syntax.unicode)
|
|
237
|
+
this.fail("a { that does not start a quantifier");
|
|
238
|
+
return atom;
|
|
239
|
+
}
|
|
240
|
+
({ min, max } = bounds);
|
|
241
|
+
if (max < min)
|
|
242
|
+
this.fail("a quantifier whose maximum is below its minimum");
|
|
243
|
+
if (min > 1000 || (Number.isFinite(max) && max > 1000))
|
|
244
|
+
this.fail("a counted repeat above 1000");
|
|
245
|
+
this.index += bounds.length - 1;
|
|
246
|
+
}
|
|
247
|
+
else
|
|
248
|
+
return atom;
|
|
249
|
+
if (atom.type === "assert")
|
|
250
|
+
this.fail("a quantified assertion");
|
|
251
|
+
this.index++;
|
|
252
|
+
const lazy = this.peek();
|
|
253
|
+
if (lazy === "?")
|
|
254
|
+
this.index++;
|
|
255
|
+
else if (lazy === "+" && this.syntax.flavor === "python")
|
|
256
|
+
this.fail("a possessive quantifier");
|
|
257
|
+
const after = this.peek();
|
|
258
|
+
if (after === "*" ||
|
|
259
|
+
after === "+" ||
|
|
260
|
+
after === "?" ||
|
|
261
|
+
(after === "{" && this.counted() !== null))
|
|
262
|
+
this.fail("a quantifier on a quantifier");
|
|
263
|
+
return { type: "repeat", item: atom, min, max };
|
|
264
|
+
}
|
|
265
|
+
atom() {
|
|
266
|
+
const char = this.peek();
|
|
267
|
+
if (char === "(")
|
|
268
|
+
return this.group();
|
|
269
|
+
if (char === "[")
|
|
270
|
+
return { type: "set", ranges: this.characterClass() };
|
|
271
|
+
if (char === ".") {
|
|
272
|
+
this.index++;
|
|
273
|
+
const excluded = this.syntax.dotAll
|
|
274
|
+
? []
|
|
275
|
+
: this.syntax.flavor === "js"
|
|
276
|
+
? JS_LINE_TERMINATORS
|
|
277
|
+
: single(0x0a);
|
|
278
|
+
return { type: "set", ranges: complement(excluded, this.maxChar()) };
|
|
279
|
+
}
|
|
280
|
+
if (char === "^") {
|
|
281
|
+
this.index++;
|
|
282
|
+
return { type: "assert", kind: "bol" };
|
|
283
|
+
}
|
|
284
|
+
if (char === "$") {
|
|
285
|
+
this.index++;
|
|
286
|
+
return {
|
|
287
|
+
type: "assert",
|
|
288
|
+
kind: this.syntax.flavor === "python" ? "eolnl" : "eol",
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (char === "\\")
|
|
292
|
+
return this.escapeAtom();
|
|
293
|
+
if (char === "*" || char === "+" || char === "?")
|
|
294
|
+
this.fail("a quantifier with nothing to repeat");
|
|
295
|
+
if (char === "{" && this.counted() !== null)
|
|
296
|
+
this.fail("a quantifier with nothing to repeat");
|
|
297
|
+
if (char === "]" && this.syntax.flavor === "js" && this.syntax.unicode)
|
|
298
|
+
this.fail("a lone ]");
|
|
299
|
+
if (char === "}" && this.syntax.flavor === "js" && this.syntax.unicode)
|
|
300
|
+
this.fail("a lone }");
|
|
301
|
+
const code = this.readLiteral();
|
|
302
|
+
return { type: "set", ranges: this.literalSet(code) };
|
|
303
|
+
}
|
|
304
|
+
maxChar() {
|
|
305
|
+
return this.syntax.unicode ? MAX_CODE_POINT : MAX_UNIT;
|
|
306
|
+
}
|
|
307
|
+
readLiteral() {
|
|
308
|
+
const code = this.syntax.unicode
|
|
309
|
+
? this.source.codePointAt(this.index)
|
|
310
|
+
: this.source.charCodeAt(this.index);
|
|
311
|
+
this.index += code > 0xffff ? 2 : 1;
|
|
312
|
+
return code;
|
|
313
|
+
}
|
|
314
|
+
literalSet(code) {
|
|
315
|
+
if (this.syntax.ignoreCase && code > 0x7f && !caseless(code))
|
|
316
|
+
this.fail("a non-ASCII letter under case-insensitive matching (Kibi folds case exactly only for ASCII letters)");
|
|
317
|
+
return caseClosed(single(code), this.syntax);
|
|
318
|
+
}
|
|
319
|
+
group() {
|
|
320
|
+
this.index++;
|
|
321
|
+
if (this.peek() === "?") {
|
|
322
|
+
const rest = this.source.slice(this.index);
|
|
323
|
+
if (rest.startsWith("?:"))
|
|
324
|
+
this.index += 2;
|
|
325
|
+
else if (/^\?P?<[A-Za-z_][A-Za-z0-9_]*>/.test(rest)) {
|
|
326
|
+
if (rest.startsWith("?P") && this.syntax.flavor !== "python")
|
|
327
|
+
this.fail("a (?P<name>) group outside Python");
|
|
328
|
+
if (rest.startsWith("?<") && this.syntax.flavor === "python")
|
|
329
|
+
this.fail("a (?<name>) group in Python");
|
|
330
|
+
this.index += rest.indexOf(">") + 1;
|
|
331
|
+
}
|
|
332
|
+
else if (/^\?<?[=!]/.test(rest))
|
|
333
|
+
this.fail("lookaround (Kibi decides only regular languages)");
|
|
334
|
+
else if (rest.startsWith("?P=") || rest.startsWith("?("))
|
|
335
|
+
this.fail("a backreference or conditional group");
|
|
336
|
+
else
|
|
337
|
+
this.fail("an inline flag, comment or atomic group");
|
|
338
|
+
}
|
|
339
|
+
const inner = this.alternation();
|
|
340
|
+
if (this.peek() !== ")")
|
|
341
|
+
this.fail("an unclosed group");
|
|
342
|
+
this.index++;
|
|
343
|
+
return inner;
|
|
344
|
+
}
|
|
345
|
+
escapeAtom() {
|
|
346
|
+
const next = this.peek(1);
|
|
347
|
+
if (next === undefined)
|
|
348
|
+
this.fail("a trailing backslash");
|
|
349
|
+
if (next === "b" || next === "B")
|
|
350
|
+
this.fail("a word boundary (Kibi decides only regular languages)");
|
|
351
|
+
if (/[1-9]/.test(next) || next === "k")
|
|
352
|
+
this.fail("a backreference (Kibi decides only regular languages)");
|
|
353
|
+
if (this.syntax.flavor === "python" && (next === "A" || next === "Z")) {
|
|
354
|
+
this.index += 2;
|
|
355
|
+
return { type: "assert", kind: next === "A" ? "bol" : "eol" };
|
|
356
|
+
}
|
|
357
|
+
const escaped = this.escape(false);
|
|
358
|
+
return {
|
|
359
|
+
type: "set",
|
|
360
|
+
ranges: "code" in escaped ? this.literalSet(escaped.code) : escaped.ranges,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* One escape after the backslash at `index`, advancing past it: a single
|
|
365
|
+
* character (its code, not yet case-folded) or a class escape's set.
|
|
366
|
+
*/
|
|
367
|
+
escape(inClass) {
|
|
368
|
+
this.index++;
|
|
369
|
+
const char = this.peek();
|
|
370
|
+
this.index++;
|
|
371
|
+
const code = (value) => ({ code: value });
|
|
372
|
+
switch (char) {
|
|
373
|
+
case "d":
|
|
374
|
+
case "D":
|
|
375
|
+
case "w":
|
|
376
|
+
case "W":
|
|
377
|
+
case "s":
|
|
378
|
+
case "S":
|
|
379
|
+
return { ranges: this.classEscape(char) };
|
|
380
|
+
case "t":
|
|
381
|
+
return code(0x09);
|
|
382
|
+
case "n":
|
|
383
|
+
return code(0x0a);
|
|
384
|
+
case "r":
|
|
385
|
+
return code(0x0d);
|
|
386
|
+
case "v":
|
|
387
|
+
return code(0x0b);
|
|
388
|
+
case "f":
|
|
389
|
+
return code(0x0c);
|
|
390
|
+
case "a":
|
|
391
|
+
if (this.syntax.flavor === "python")
|
|
392
|
+
return code(0x07);
|
|
393
|
+
break;
|
|
394
|
+
case "b":
|
|
395
|
+
if (inClass)
|
|
396
|
+
return code(0x08);
|
|
397
|
+
break;
|
|
398
|
+
case "0":
|
|
399
|
+
if (!/[0-9]/.test(this.peek() ?? ""))
|
|
400
|
+
return code(0);
|
|
401
|
+
this.fail("an octal escape");
|
|
402
|
+
break;
|
|
403
|
+
case "x": {
|
|
404
|
+
const hex = /^[0-9A-Fa-f]{2}/.exec(this.source.slice(this.index));
|
|
405
|
+
if (hex === null)
|
|
406
|
+
this.fail("a malformed \\x escape");
|
|
407
|
+
this.index += 2;
|
|
408
|
+
return code(Number.parseInt(hex[0], 16));
|
|
409
|
+
}
|
|
410
|
+
case "u": {
|
|
411
|
+
const rest = this.source.slice(this.index);
|
|
412
|
+
const braced = /^\{([0-9A-Fa-f]{1,6})\}/.exec(rest);
|
|
413
|
+
if (braced !== null && this.syntax.flavor === "js" && this.syntax.unicode) {
|
|
414
|
+
this.index += braced[0].length;
|
|
415
|
+
const value = Number.parseInt(braced[1], 16);
|
|
416
|
+
if (value > MAX_CODE_POINT)
|
|
417
|
+
this.fail("a code point above U+10FFFF");
|
|
418
|
+
return code(value);
|
|
419
|
+
}
|
|
420
|
+
const hex = /^[0-9A-Fa-f]{4}/.exec(rest);
|
|
421
|
+
if (hex === null)
|
|
422
|
+
this.fail("a malformed \\u escape");
|
|
423
|
+
this.index += 4;
|
|
424
|
+
let value = Number.parseInt(hex[0], 16);
|
|
425
|
+
// In u mode a \u escape pair spells one astral code point.
|
|
426
|
+
const low = /^\\u(D[C-F][0-9A-F]{2})/i.exec(this.source.slice(this.index));
|
|
427
|
+
if (this.syntax.flavor === "js" &&
|
|
428
|
+
this.syntax.unicode &&
|
|
429
|
+
value >= 0xd800 &&
|
|
430
|
+
value <= 0xdbff &&
|
|
431
|
+
low !== null) {
|
|
432
|
+
this.index += 6;
|
|
433
|
+
value =
|
|
434
|
+
0x10000 +
|
|
435
|
+
((value - 0xd800) << 10) +
|
|
436
|
+
(Number.parseInt(low[1], 16) - 0xdc00);
|
|
437
|
+
}
|
|
438
|
+
return code(value);
|
|
439
|
+
}
|
|
440
|
+
case "U":
|
|
441
|
+
if (this.syntax.flavor === "python") {
|
|
442
|
+
const hex = /^[0-9A-Fa-f]{8}/.exec(this.source.slice(this.index));
|
|
443
|
+
if (hex === null)
|
|
444
|
+
this.fail("a malformed \\U escape");
|
|
445
|
+
this.index += 8;
|
|
446
|
+
const value = Number.parseInt(hex[0], 16);
|
|
447
|
+
if (value > MAX_CODE_POINT)
|
|
448
|
+
this.fail("a code point above U+10FFFF");
|
|
449
|
+
return code(value);
|
|
450
|
+
}
|
|
451
|
+
break;
|
|
452
|
+
case "c":
|
|
453
|
+
if (this.syntax.flavor === "js") {
|
|
454
|
+
const letter = this.peek() ?? "";
|
|
455
|
+
if (/[A-Za-z]/.test(letter)) {
|
|
456
|
+
this.index++;
|
|
457
|
+
return code(letter.charCodeAt(0) % 32);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
break;
|
|
461
|
+
case "p":
|
|
462
|
+
case "P":
|
|
463
|
+
this.fail("a Unicode property escape");
|
|
464
|
+
break;
|
|
465
|
+
case "N":
|
|
466
|
+
if (this.syntax.flavor === "python")
|
|
467
|
+
this.fail("a \\N{name} escape");
|
|
468
|
+
break;
|
|
469
|
+
default:
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
// An identity escape: a syntax character stands for itself (in Python
|
|
473
|
+
// any ASCII punctuation does); any other escape is one Kibi does not
|
|
474
|
+
// model.
|
|
475
|
+
if (/[\^$\\.*+?()[\]{}|/\-]/.test(char))
|
|
476
|
+
return code(char.charCodeAt(0));
|
|
477
|
+
if (this.syntax.flavor === "python" &&
|
|
478
|
+
/[^A-Za-z0-9]/.test(char) &&
|
|
479
|
+
char.charCodeAt(0) < 0x80)
|
|
480
|
+
return code(char.charCodeAt(0));
|
|
481
|
+
this.index--;
|
|
482
|
+
return this.fail(`the escape \\${char}`);
|
|
483
|
+
}
|
|
484
|
+
classEscape(char) {
|
|
485
|
+
const lower = char.toLowerCase();
|
|
486
|
+
let base;
|
|
487
|
+
if (this.syntax.flavor === "python" && !this.syntax.ascii) {
|
|
488
|
+
if (lower === "s")
|
|
489
|
+
base = PYTHON_UNICODE_SPACE;
|
|
490
|
+
else
|
|
491
|
+
this.fail(`\\${char} in a Python str pattern matches any Unicode ${lower === "d" ? "digit" : "word character"}; use an explicit class or re.ASCII`);
|
|
492
|
+
}
|
|
493
|
+
else if (lower === "d")
|
|
494
|
+
base = DIGIT;
|
|
495
|
+
else if (lower === "w")
|
|
496
|
+
base = WORD;
|
|
497
|
+
else
|
|
498
|
+
base = this.syntax.flavor === "python" ? PYTHON_ASCII_SPACE : JS_SPACE;
|
|
499
|
+
const closed = caseClosed(base, this.syntax);
|
|
500
|
+
return char === lower ? closed : complement(closed, this.maxChar());
|
|
501
|
+
}
|
|
502
|
+
characterClass() {
|
|
503
|
+
this.index++;
|
|
504
|
+
let negated = false;
|
|
505
|
+
if (this.peek() === "^") {
|
|
506
|
+
negated = true;
|
|
507
|
+
this.index++;
|
|
508
|
+
}
|
|
509
|
+
const ranges = [];
|
|
510
|
+
let first = true;
|
|
511
|
+
for (;;) {
|
|
512
|
+
const char = this.peek();
|
|
513
|
+
if (char === undefined)
|
|
514
|
+
this.fail("an unclosed character class");
|
|
515
|
+
if (char === "]" && !(first && this.syntax.flavor === "python")) {
|
|
516
|
+
this.index++;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
first = false;
|
|
520
|
+
if (char === "[" &&
|
|
521
|
+
this.syntax.flavor === "python" &&
|
|
522
|
+
/^\[[:=.]/.test(this.source.slice(this.index)))
|
|
523
|
+
this.fail("a POSIX class");
|
|
524
|
+
const start = this.classAtom();
|
|
525
|
+
if (this.peek() === "-" &&
|
|
526
|
+
this.peek(1) !== undefined &&
|
|
527
|
+
this.peek(1) !== "]") {
|
|
528
|
+
this.index++;
|
|
529
|
+
const end = this.classAtom();
|
|
530
|
+
if (start.single === null || end.single === null) {
|
|
531
|
+
if (this.syntax.unicode)
|
|
532
|
+
this.fail("a class escape as a range endpoint");
|
|
533
|
+
// Without u, JavaScript reads [\d-x] as \d, - and x.
|
|
534
|
+
ranges.push(...start.ranges, ...this.literalSet(0x2d), ...end.ranges);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (end.single < start.single)
|
|
538
|
+
this.fail("a range out of order");
|
|
539
|
+
if (this.syntax.ignoreCase && end.single > 0x7f)
|
|
540
|
+
this.fail("a non-ASCII range under case-insensitive matching (Kibi folds case exactly only for ASCII letters)");
|
|
541
|
+
ranges.push(...caseClosed([[start.single, end.single]], this.syntax));
|
|
542
|
+
}
|
|
543
|
+
else
|
|
544
|
+
ranges.push(...start.ranges);
|
|
545
|
+
}
|
|
546
|
+
const set = normalize(ranges);
|
|
547
|
+
return negated ? complement(set, this.maxChar()) : set;
|
|
548
|
+
}
|
|
549
|
+
classAtom() {
|
|
550
|
+
if (this.peek() === "\\") {
|
|
551
|
+
if (this.peek(1) === "B")
|
|
552
|
+
this.fail("\\B inside a class");
|
|
553
|
+
const escaped = this.escape(true);
|
|
554
|
+
return "code" in escaped
|
|
555
|
+
? { ranges: this.literalSet(escaped.code), single: escaped.code }
|
|
556
|
+
: { ranges: escaped.ranges, single: null };
|
|
557
|
+
}
|
|
558
|
+
const value = this.readLiteral();
|
|
559
|
+
return { ranges: this.literalSet(value), single: value };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
class Nfa {
|
|
563
|
+
edges = [];
|
|
564
|
+
state() {
|
|
565
|
+
if (this.edges.length >= MAX_NFA_STATES)
|
|
566
|
+
throw new UnsupportedPattern(`the pattern needs more than ${MAX_NFA_STATES} automaton states`);
|
|
567
|
+
this.edges.push([]);
|
|
568
|
+
return this.edges.length - 1;
|
|
569
|
+
}
|
|
570
|
+
add(from, edge) {
|
|
571
|
+
this.edges[from].push(edge);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
/** Code point ranges as UTF-16 code unit paths from `from` to `to`. */
|
|
575
|
+
function addCodePoints(nfa, from, to, ranges, unicode) {
|
|
576
|
+
if (!unicode) {
|
|
577
|
+
const units = normalize(ranges.map(([lo, hi]) => [lo, Math.min(hi, MAX_UNIT)]));
|
|
578
|
+
if (units.length > 0)
|
|
579
|
+
nfa.add(from, { type: "units", ranges: units, to });
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
// Lone surrogates never occur in well-formed strings, so a code point set
|
|
583
|
+
// keeps only scalar values: BMP ranges as one unit, astral ones as pairs.
|
|
584
|
+
const bmp = [];
|
|
585
|
+
const astral = [];
|
|
586
|
+
for (const [lo, hi] of ranges) {
|
|
587
|
+
if (lo <= 0xd7ff)
|
|
588
|
+
bmp.push([lo, Math.min(hi, 0xd7ff)]);
|
|
589
|
+
if (hi >= 0xe000 && lo <= 0xffff)
|
|
590
|
+
bmp.push([Math.max(lo, 0xe000), Math.min(hi, 0xffff)]);
|
|
591
|
+
if (hi >= 0x10000)
|
|
592
|
+
astral.push([Math.max(lo, 0x10000), hi]);
|
|
593
|
+
}
|
|
594
|
+
const units = normalize(bmp);
|
|
595
|
+
if (units.length > 0)
|
|
596
|
+
nfa.add(from, { type: "units", ranges: units, to });
|
|
597
|
+
for (const [lo, hi] of normalize(astral)) {
|
|
598
|
+
const highOf = (code) => 0xd800 + ((code - 0x10000) >> 10);
|
|
599
|
+
const lowOf = (code) => 0xdc00 + ((code - 0x10000) & 0x3ff);
|
|
600
|
+
const pair = (highLo, highHi, lowLo, lowHi) => {
|
|
601
|
+
const mid = nfa.state();
|
|
602
|
+
nfa.add(from, { type: "units", ranges: [[highLo, highHi]], to: mid });
|
|
603
|
+
nfa.add(mid, { type: "units", ranges: [[lowLo, lowHi]], to });
|
|
604
|
+
};
|
|
605
|
+
const firstHigh = highOf(lo);
|
|
606
|
+
const lastHigh = highOf(hi);
|
|
607
|
+
if (firstHigh === lastHigh) {
|
|
608
|
+
pair(firstHigh, firstHigh, lowOf(lo), lowOf(hi));
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
pair(firstHigh, firstHigh, lowOf(lo), 0xdfff);
|
|
612
|
+
if (lastHigh - firstHigh > 1)
|
|
613
|
+
pair(firstHigh + 1, lastHigh - 1, 0xdc00, 0xdfff);
|
|
614
|
+
pair(lastHigh, lastHigh, 0xdc00, lowOf(hi));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function build(nfa, node, from, to, unicode) {
|
|
618
|
+
switch (node.type) {
|
|
619
|
+
case "set":
|
|
620
|
+
addCodePoints(nfa, from, to, node.ranges, unicode);
|
|
621
|
+
return;
|
|
622
|
+
case "assert":
|
|
623
|
+
nfa.add(from, { type: "assert", kind: node.kind, to });
|
|
624
|
+
return;
|
|
625
|
+
case "seq": {
|
|
626
|
+
let current = from;
|
|
627
|
+
node.items.forEach((item, index) => {
|
|
628
|
+
const next = index === node.items.length - 1 ? to : nfa.state();
|
|
629
|
+
build(nfa, item, current, next, unicode);
|
|
630
|
+
current = next;
|
|
631
|
+
});
|
|
632
|
+
if (node.items.length === 0)
|
|
633
|
+
nfa.add(from, { type: "eps", to });
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
case "alt":
|
|
637
|
+
for (const item of node.items)
|
|
638
|
+
build(nfa, item, from, to, unicode);
|
|
639
|
+
return;
|
|
640
|
+
case "repeat": {
|
|
641
|
+
let current = from;
|
|
642
|
+
for (let count = 0; count < node.min; count++) {
|
|
643
|
+
const next = nfa.state();
|
|
644
|
+
build(nfa, node.item, current, next, unicode);
|
|
645
|
+
current = next;
|
|
646
|
+
}
|
|
647
|
+
if (node.max === Number.POSITIVE_INFINITY) {
|
|
648
|
+
const loop = nfa.state();
|
|
649
|
+
nfa.add(current, { type: "eps", to: loop });
|
|
650
|
+
const back = nfa.state();
|
|
651
|
+
build(nfa, node.item, loop, back, unicode);
|
|
652
|
+
nfa.add(back, { type: "eps", to: loop });
|
|
653
|
+
nfa.add(loop, { type: "eps", to });
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
for (let count = node.min; count < node.max; count++) {
|
|
657
|
+
const next = nfa.state();
|
|
658
|
+
nfa.add(current, { type: "eps", to });
|
|
659
|
+
build(nfa, node.item, current, next, unicode);
|
|
660
|
+
current = next;
|
|
661
|
+
}
|
|
662
|
+
nfa.add(current, { type: "eps", to });
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/** Compile a language to an NFA over UTF-16 code units, or refuse it. */
|
|
668
|
+
// implements REQ-kibi-non-numeric-proof-kinds
|
|
669
|
+
export function compileLanguage(spec) {
|
|
670
|
+
const nfa = new Nfa();
|
|
671
|
+
const start = nfa.state();
|
|
672
|
+
const accept = nfa.state();
|
|
673
|
+
if (spec.type === "literal") {
|
|
674
|
+
let current = start;
|
|
675
|
+
for (let index = 0; index < spec.value.length; index++) {
|
|
676
|
+
const next = index === spec.value.length - 1 ? accept : nfa.state();
|
|
677
|
+
const unit = spec.value.charCodeAt(index);
|
|
678
|
+
nfa.add(current, { type: "units", ranges: [[unit, unit]], to: next });
|
|
679
|
+
current = next;
|
|
680
|
+
}
|
|
681
|
+
if (spec.value.length === 0)
|
|
682
|
+
nfa.add(start, { type: "eps", to: accept });
|
|
683
|
+
return { nfa, start, accept, mode: "full" };
|
|
684
|
+
}
|
|
685
|
+
const syntax = syntaxOf(spec);
|
|
686
|
+
const tree = new Parser(spec.source, syntax).parse();
|
|
687
|
+
build(nfa, tree, start, accept, syntax.unicode);
|
|
688
|
+
return { nfa, start, accept, mode: spec.mode };
|
|
689
|
+
}
|
|
690
|
+
const FREE = 0;
|
|
691
|
+
const NEWLINE_OR_END = 1;
|
|
692
|
+
const END = 2;
|
|
693
|
+
const ACCEPTED = "*";
|
|
694
|
+
class Determinizer {
|
|
695
|
+
compiled;
|
|
696
|
+
ids = new Map();
|
|
697
|
+
keys = [];
|
|
698
|
+
sets = [];
|
|
699
|
+
flags = [];
|
|
700
|
+
moves = new Map();
|
|
701
|
+
constructor(compiled) {
|
|
702
|
+
this.compiled = compiled;
|
|
703
|
+
}
|
|
704
|
+
initial() {
|
|
705
|
+
return this.intern(this.closure([this.config(this.compiled.start, FREE)], true), false, false);
|
|
706
|
+
}
|
|
707
|
+
config(state, constraint) {
|
|
708
|
+
return state * 3 + constraint;
|
|
709
|
+
}
|
|
710
|
+
closure(seed, atStart) {
|
|
711
|
+
const seen = new Set(seed);
|
|
712
|
+
const stack = [...seed];
|
|
713
|
+
while (stack.length > 0) {
|
|
714
|
+
const config = stack.pop();
|
|
715
|
+
const state = Math.floor(config / 3);
|
|
716
|
+
const constraint = config % 3;
|
|
717
|
+
for (const edge of this.compiled.nfa.edges[state] ?? []) {
|
|
718
|
+
let next = null;
|
|
719
|
+
if (edge.type === "eps")
|
|
720
|
+
next = this.config(edge.to, constraint);
|
|
721
|
+
else if (edge.type === "assert") {
|
|
722
|
+
if (edge.kind === "bol")
|
|
723
|
+
next = atStart ? this.config(edge.to, constraint) : null;
|
|
724
|
+
else if (edge.kind === "eol")
|
|
725
|
+
next = this.config(edge.to, END);
|
|
726
|
+
else
|
|
727
|
+
next = this.config(edge.to, Math.max(constraint, NEWLINE_OR_END));
|
|
728
|
+
}
|
|
729
|
+
if (next !== null && !seen.has(next)) {
|
|
730
|
+
seen.add(next);
|
|
731
|
+
stack.push(next);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return [...seen].sort((a, b) => a - b);
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Intern a closed configuration set. Reaching the accept state settles the
|
|
739
|
+
* outcome: in search mode an unconstrained match accepts every extension;
|
|
740
|
+
* a constrained one (or any full match) accepts only when the rest of the
|
|
741
|
+
* input is what the constraint allows.
|
|
742
|
+
*/
|
|
743
|
+
intern(configs, atEnd, atNewline) {
|
|
744
|
+
let accepted = false;
|
|
745
|
+
let endHere = atEnd;
|
|
746
|
+
let newlineHere = atNewline;
|
|
747
|
+
const live = [];
|
|
748
|
+
for (const config of configs) {
|
|
749
|
+
const state = Math.floor(config / 3);
|
|
750
|
+
const constraint = config % 3;
|
|
751
|
+
if (state === this.compiled.accept) {
|
|
752
|
+
if (this.compiled.mode === "full" || constraint === END)
|
|
753
|
+
endHere = true;
|
|
754
|
+
else if (constraint === NEWLINE_OR_END)
|
|
755
|
+
newlineHere = true;
|
|
756
|
+
else
|
|
757
|
+
accepted = true;
|
|
758
|
+
}
|
|
759
|
+
if (constraint === END)
|
|
760
|
+
continue;
|
|
761
|
+
live.push(config);
|
|
762
|
+
}
|
|
763
|
+
const key = accepted
|
|
764
|
+
? ACCEPTED
|
|
765
|
+
: `${live.join(",")}|${endHere ? 1 : 0}${newlineHere ? 1 : 0}`;
|
|
766
|
+
const existing = this.ids.get(key);
|
|
767
|
+
if (existing !== undefined)
|
|
768
|
+
return existing;
|
|
769
|
+
const id = this.keys.length;
|
|
770
|
+
this.ids.set(key, id);
|
|
771
|
+
this.keys.push(key);
|
|
772
|
+
this.sets.push(accepted ? [] : live);
|
|
773
|
+
this.flags.push({ accepted, atEnd: endHere, atNewline: newlineHere });
|
|
774
|
+
if (id >= MAX_PRODUCT_STATES)
|
|
775
|
+
throw new UnsupportedPattern("the pattern's automaton grew too large to decide");
|
|
776
|
+
return id;
|
|
777
|
+
}
|
|
778
|
+
accepts(id) {
|
|
779
|
+
const flag = this.flags[id];
|
|
780
|
+
return flag !== undefined && (flag.accepted || flag.atEnd || flag.atNewline);
|
|
781
|
+
}
|
|
782
|
+
step(id, unit) {
|
|
783
|
+
const memo = `${id}:${unit}`;
|
|
784
|
+
const known = this.moves.get(memo);
|
|
785
|
+
if (known !== undefined)
|
|
786
|
+
return known;
|
|
787
|
+
const flag = this.flags[id];
|
|
788
|
+
let result;
|
|
789
|
+
if (flag.accepted)
|
|
790
|
+
result = id;
|
|
791
|
+
else {
|
|
792
|
+
const next = [];
|
|
793
|
+
for (const config of this.sets[id] ?? []) {
|
|
794
|
+
const state = Math.floor(config / 3);
|
|
795
|
+
const constraint = config % 3;
|
|
796
|
+
if (constraint === NEWLINE_OR_END && unit !== 0x0a)
|
|
797
|
+
continue;
|
|
798
|
+
const after = constraint === NEWLINE_OR_END ? END : constraint;
|
|
799
|
+
for (const edge of this.compiled.nfa.edges[state] ?? [])
|
|
800
|
+
if (edge.type === "units" && contains(edge.ranges, unit))
|
|
801
|
+
next.push(this.config(edge.to, after));
|
|
802
|
+
}
|
|
803
|
+
// Search mode starts a new attempt at every position.
|
|
804
|
+
if (this.compiled.mode === "search")
|
|
805
|
+
next.push(this.config(this.compiled.start, FREE));
|
|
806
|
+
const atEnd = flag.atNewline && unit === 0x0a;
|
|
807
|
+
result = this.intern(this.closure(next, false), atEnd, false);
|
|
808
|
+
}
|
|
809
|
+
this.moves.set(memo, result);
|
|
810
|
+
return result;
|
|
811
|
+
}
|
|
812
|
+
/** The unit boundaries the automaton distinguishes. */
|
|
813
|
+
boundaries(into) {
|
|
814
|
+
for (const edges of this.compiled.nfa.edges)
|
|
815
|
+
for (const edge of edges)
|
|
816
|
+
if (edge.type === "units")
|
|
817
|
+
for (const [lo, hi] of edge.ranges) {
|
|
818
|
+
into.add(lo);
|
|
819
|
+
into.add(hi + 1);
|
|
820
|
+
}
|
|
821
|
+
into.add(0x0a);
|
|
822
|
+
into.add(0x0b);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
function contains(ranges, unit) {
|
|
826
|
+
let lo = 0;
|
|
827
|
+
let hi = ranges.length - 1;
|
|
828
|
+
while (lo <= hi) {
|
|
829
|
+
const mid = (lo + hi) >> 1;
|
|
830
|
+
const [start, end] = ranges[mid];
|
|
831
|
+
if (unit < start)
|
|
832
|
+
hi = mid - 1;
|
|
833
|
+
else if (unit > end)
|
|
834
|
+
lo = mid + 1;
|
|
835
|
+
else
|
|
836
|
+
return true;
|
|
837
|
+
}
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
/** One representative unit per class of units no automaton tells apart. */
|
|
841
|
+
function minterms(automata) {
|
|
842
|
+
const cuts = new Set([0, 0xd800, 0xdc00, 0xe000, MAX_UNIT + 1]);
|
|
843
|
+
for (const automaton of automata)
|
|
844
|
+
automaton.boundaries(cuts);
|
|
845
|
+
const sorted = [...cuts].filter((cut) => cut <= MAX_UNIT + 1).sort((a, b) => a - b);
|
|
846
|
+
const representatives = [];
|
|
847
|
+
for (let index = 0; index < sorted.length - 1; index++) {
|
|
848
|
+
const lo = sorted[index];
|
|
849
|
+
const hi = sorted[index + 1] - 1;
|
|
850
|
+
// Prefer a printable character so a counterexample reads well.
|
|
851
|
+
const printable = [0x61, 0x41, 0x30, 0x20].find((code) => code >= lo && code <= hi);
|
|
852
|
+
const visible = lo <= 0x7e && hi >= 0x21 ? Math.max(lo, 0x21) : lo;
|
|
853
|
+
representatives.push(printable ?? visible);
|
|
854
|
+
}
|
|
855
|
+
// The search tries readable characters first, so among the shortest
|
|
856
|
+
// strings that tell two languages apart it reports a readable one.
|
|
857
|
+
const rank = (unit) => /[A-Za-z0-9]/.test(String.fromCharCode(unit))
|
|
858
|
+
? 0
|
|
859
|
+
: unit >= 0x20 && unit <= 0x7e
|
|
860
|
+
? 1
|
|
861
|
+
: unit >= 0xa0
|
|
862
|
+
? 2
|
|
863
|
+
: 3;
|
|
864
|
+
return representatives.sort((x, y) => rank(x) - rank(y) || x - y);
|
|
865
|
+
}
|
|
866
|
+
// Well-formed UTF-16: 0 between scalar values, 1 after a high surrogate.
|
|
867
|
+
function wellFormedStep(state, unit) {
|
|
868
|
+
const high = unit >= 0xd800 && unit <= 0xdbff;
|
|
869
|
+
const low = unit >= 0xdc00 && unit <= 0xdfff;
|
|
870
|
+
if (state === 1)
|
|
871
|
+
return low ? 0 : null;
|
|
872
|
+
if (low)
|
|
873
|
+
return null;
|
|
874
|
+
return high ? 1 : 0;
|
|
875
|
+
}
|
|
876
|
+
function decide(left, right, relation) {
|
|
877
|
+
const a = new Determinizer(compileLanguage(left));
|
|
878
|
+
const b = new Determinizer(compileLanguage(right));
|
|
879
|
+
const alphabet = minterms([a, b]);
|
|
880
|
+
const start = { a: a.initial(), b: b.initial(), wf: 0, text: [] };
|
|
881
|
+
const seen = new Set([`${start.a},${start.b},0`]);
|
|
882
|
+
const queue = [start];
|
|
883
|
+
for (let head = 0; head < queue.length; head++) {
|
|
884
|
+
const node = queue[head];
|
|
885
|
+
if (node.wf === 0) {
|
|
886
|
+
const inA = a.accepts(node.a);
|
|
887
|
+
const inB = b.accepts(node.b);
|
|
888
|
+
const witness = String.fromCharCode(...node.text);
|
|
889
|
+
if (relation === "equal" && inA !== inB)
|
|
890
|
+
return {
|
|
891
|
+
holds: false,
|
|
892
|
+
witness,
|
|
893
|
+
reason: `${JSON.stringify(witness)} is in ${inA ? "the first" : "the second"} language only`,
|
|
894
|
+
};
|
|
895
|
+
if (relation === "disjoint" && inA && inB)
|
|
896
|
+
return { holds: false, witness, reason: `${JSON.stringify(witness)} is in both languages` };
|
|
897
|
+
if (relation === "subset" && inA && !inB)
|
|
898
|
+
return {
|
|
899
|
+
holds: false,
|
|
900
|
+
witness,
|
|
901
|
+
reason: `${JSON.stringify(witness)} is in the first language but not the second`,
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
for (const unit of alphabet) {
|
|
905
|
+
const wf = wellFormedStep(node.wf, unit);
|
|
906
|
+
if (wf === null)
|
|
907
|
+
continue;
|
|
908
|
+
const next = { a: a.step(node.a, unit), b: b.step(node.b, unit), wf };
|
|
909
|
+
const key = `${next.a},${next.b},${wf}`;
|
|
910
|
+
if (seen.has(key))
|
|
911
|
+
continue;
|
|
912
|
+
if (seen.size >= MAX_PRODUCT_STATES)
|
|
913
|
+
throw new UnsupportedPattern("the languages' product automaton grew too large to decide");
|
|
914
|
+
seen.add(key);
|
|
915
|
+
queue.push({ ...next, text: [...node.text, unit] });
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return { holds: true };
|
|
919
|
+
}
|
|
920
|
+
/** Whether two languages hold exactly the same strings. */
|
|
921
|
+
// implements REQ-kibi-non-numeric-proof-kinds
|
|
922
|
+
export function languagesEqual(left, right) {
|
|
923
|
+
return decide(left, right, "equal");
|
|
924
|
+
}
|
|
925
|
+
/** Whether no string is in both languages. */
|
|
926
|
+
// implements REQ-kibi-non-numeric-proof-kinds
|
|
927
|
+
export function languagesDisjoint(left, right) {
|
|
928
|
+
return decide(left, right, "disjoint");
|
|
929
|
+
}
|
|
930
|
+
/** Whether every string of `left` is in `right`. */
|
|
931
|
+
// implements REQ-kibi-non-numeric-proof-kinds
|
|
932
|
+
export function languageIncluded(left, right) {
|
|
933
|
+
return decide(left, right, "subset");
|
|
934
|
+
}
|
|
935
|
+
/** Whether a language holds `text` (a well-formed string). */
|
|
936
|
+
// implements REQ-kibi-non-numeric-proof-kinds
|
|
937
|
+
export function languageAccepts(spec, text) {
|
|
938
|
+
const automaton = new Determinizer(compileLanguage(spec));
|
|
939
|
+
let state = automaton.initial();
|
|
940
|
+
for (let index = 0; index < text.length; index++)
|
|
941
|
+
state = automaton.step(state, text.charCodeAt(index));
|
|
942
|
+
return automaton.accepts(state);
|
|
943
|
+
}
|