@tikoci/centrs 0.1.9-next.22 → 0.1.9-next.24
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/CHANGELOG.md +106 -0
- package/GLOSSARY.txt +38 -0
- package/README.md +2 -2
- package/commands/api/AGENTS.md +18 -1
- package/commands/api/README.md +10 -6
- package/commands/api/examples.md +1 -1
- package/commands/execute/README.md +26 -6
- package/commands/execute/examples.md +43 -5
- package/commands/explain/README.md +873 -182
- package/commands/explain/examples.md +447 -38
- package/commands/retrieve/README.md +13 -2
- package/commands/retrieve/examples.md +30 -1
- package/commands/transfer/README.md +8 -4
- package/docs/CLI.md +19 -5
- package/docs/CONSTITUTION.md +18 -9
- package/docs/MATRIX.md +51 -26
- package/docs/errors/cmr/caller-policy-not-enforced.md +5 -3
- package/docs/errors/routeros/permission-denied.md +24 -0
- package/docs/errors/transport/edge-auth-failed.md +22 -0
- package/docs/errors/transport/write-outcome-unknown.md +3 -0
- package/docs/errors/usage/invalid-cursor.md +20 -0
- package/docs/errors/validation/unknown-path.md +9 -0
- package/package.json +5 -2
- package/src/api.ts +34 -39
- package/src/cli/explain.ts +206 -47
- package/src/core/envelope.ts +10 -3
- package/src/core/error-catalog.ts +15 -0
- package/src/core/execute-command.ts +53 -0
- package/src/core/fanout.ts +12 -2
- package/src/core/inspect.ts +4 -5
- package/src/core/routeros-errors.ts +25 -0
- package/src/execute-cmr.ts +2 -8
- package/src/execute.ts +48 -20
- package/src/explain/args.ts +181 -15
- package/src/explain/blocks.ts +3 -4
- package/src/explain/brace-slots.ts +5 -0
- package/src/explain/catalog.ts +13 -6
- package/src/explain/defects.ts +1 -0
- package/src/explain/group-faults.ts +1060 -0
- package/src/explain/highlight-projection.ts +10 -8
- package/src/explain/operators.ts +22 -20
- package/src/explain/pathresolve.ts +102 -20
- package/src/explain/quoted-string.ts +41 -6
- package/src/explain/run-tokens.ts +223 -0
- package/src/explain/scope-brace.ts +319 -7
- package/src/explain/segment.ts +177 -4
- package/src/explain/spaced-args.ts +116 -0
- package/src/explain/symbols.ts +163 -7
- package/src/explain/transport.ts +13 -0
- package/src/explain/values.ts +55 -4
- package/src/explain/verbsplit.ts +14 -188
- package/src/explain/write.ts +8 -1
- package/src/explain-live-facets.ts +739 -0
- package/src/explain-live-types.ts +490 -0
- package/src/explain-live.ts +593 -0
- package/src/explain.ts +772 -60
- package/src/index.ts +6 -0
- package/src/offline-gate.ts +30 -9
- package/src/protocols/adapter.ts +88 -10
- package/src/protocols/btest-session.ts +30 -10
- package/src/protocols/mac-telnet-console.ts +16 -0
- package/src/protocols/mac-telnet.ts +38 -10
- package/src/protocols/native-api.ts +13 -12
- package/src/retrieve-follow.ts +2 -1
- package/src/retrieve-sample.ts +2 -1
- package/src/retrieve-wait.ts +10 -6
- package/src/retrieve.ts +77 -27
- package/src/transfer.ts +38 -13
package/src/explain/symbols.ts
CHANGED
|
@@ -308,7 +308,11 @@
|
|
|
308
308
|
import { analyzeCoordinates } from "./coordinates.ts";
|
|
309
309
|
import { type Defect, defectAt } from "./defects.ts";
|
|
310
310
|
import { isMenuPath } from "./menus.ts";
|
|
311
|
-
import {
|
|
311
|
+
import {
|
|
312
|
+
braceStartsStatements,
|
|
313
|
+
hashStartsHardError,
|
|
314
|
+
startsArrayCommentLine,
|
|
315
|
+
} from "./scope-brace.ts";
|
|
312
316
|
import type { Continuation } from "./segment.ts";
|
|
313
317
|
|
|
314
318
|
/** The five variable classes the console assigns. */
|
|
@@ -415,7 +419,10 @@ interface BracketFrame {
|
|
|
415
419
|
pendingSetTarget: boolean;
|
|
416
420
|
pendingErrVar: boolean;
|
|
417
421
|
pendingDecl: SymbolClass | null;
|
|
422
|
+
pendingDeclNameValue: boolean;
|
|
418
423
|
pendingLoopVars: boolean;
|
|
424
|
+
loopCounterOpen: boolean;
|
|
425
|
+
pendingLoopCounterValue: boolean;
|
|
419
426
|
declaredHere: boolean;
|
|
420
427
|
headSigil: ":" | "/" | "";
|
|
421
428
|
cont: Continuation;
|
|
@@ -507,6 +514,20 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
507
514
|
char: "{" | "[" | "(" | '"';
|
|
508
515
|
at: number;
|
|
509
516
|
statements: boolean;
|
|
517
|
+
/**
|
|
518
|
+
* #533 reorder — a `{…}` resets the statement it interrupts, but a
|
|
519
|
+
* `:for`/`:foreach` head stays open for `counter=` past it (notably a
|
|
520
|
+
* `:foreach … in={…}` array), so the flag rides the frame and the
|
|
521
|
+
* matching `}` restores it. Only `}` restores: `[` has the
|
|
522
|
+
* `BracketFrame`, quotes and `(` never reset.
|
|
523
|
+
*/
|
|
524
|
+
loopCounterOpen: boolean;
|
|
525
|
+
/**
|
|
526
|
+
* #533 reorder — likewise a `:local`/`:global` head stays open for a
|
|
527
|
+
* later `name=` past an array VALUE (`:local value={1;2} name=foo`).
|
|
528
|
+
* Saved only on a `{` that is an argument value; `}` restores it.
|
|
529
|
+
*/
|
|
530
|
+
pendingDecl?: SymbolClass | null;
|
|
510
531
|
}[] = [];
|
|
511
532
|
/** bracket/brace nesting, strings excluded (the S8 filter region uses it). */
|
|
512
533
|
let depth = 0;
|
|
@@ -521,7 +542,23 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
521
542
|
/** depth at which a filter word opened its region, or null. */
|
|
522
543
|
let filterDepth: number | null = null;
|
|
523
544
|
let pendingDecl: SymbolClass | null = null;
|
|
545
|
+
/**
|
|
546
|
+
* #533 — the pending `:local`/`:global` declaration waits for the VALUE of
|
|
547
|
+
* a `name=` argument (`:local name=foo` declares `foo`, not `name`). Set
|
|
548
|
+
* when a `name=` arg name is read, cleared after its value declares or is
|
|
549
|
+
* skipped. Values of other args (`value=`) never declare.
|
|
550
|
+
*/
|
|
551
|
+
let pendingDeclNameValue = false;
|
|
524
552
|
let pendingLoopVars = false;
|
|
553
|
+
/**
|
|
554
|
+
* #533 reorder — a `:for`/`:foreach` head is still open for a named
|
|
555
|
+
* `counter=`: unlike `pendingLoopVars` (positional vars, ended by
|
|
556
|
+
* `in=`/`from=`/any `arg=`), this survives the whole head — CHR accepts
|
|
557
|
+
* `:for from=1 counter=i to=2` — and only `do` (the body) closes it.
|
|
558
|
+
*/
|
|
559
|
+
let loopCounterOpen = false;
|
|
560
|
+
/** The next value word is a `counter=` loop var. */
|
|
561
|
+
let pendingLoopCounterValue = false;
|
|
525
562
|
/** inside a space-separated menu path run, where bare words are `dir` (S16). */
|
|
526
563
|
let inMenuPath = false;
|
|
527
564
|
/**
|
|
@@ -589,7 +626,10 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
589
626
|
pendingSetTarget = false;
|
|
590
627
|
pendingErrVar = false;
|
|
591
628
|
pendingDecl = null;
|
|
629
|
+
pendingDeclNameValue = false;
|
|
592
630
|
pendingLoopVars = false;
|
|
631
|
+
loopCounterOpen = false;
|
|
632
|
+
pendingLoopCounterValue = false;
|
|
593
633
|
declaredHere = false;
|
|
594
634
|
headSigil = "";
|
|
595
635
|
cont = "none";
|
|
@@ -662,7 +702,10 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
662
702
|
pendingSetTarget,
|
|
663
703
|
pendingErrVar,
|
|
664
704
|
pendingDecl,
|
|
705
|
+
pendingDeclNameValue,
|
|
665
706
|
pendingLoopVars,
|
|
707
|
+
loopCounterOpen,
|
|
708
|
+
pendingLoopCounterValue,
|
|
666
709
|
declaredHere,
|
|
667
710
|
headSigil,
|
|
668
711
|
cont,
|
|
@@ -715,7 +758,10 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
715
758
|
pendingSetTarget = frame.pendingSetTarget;
|
|
716
759
|
pendingErrVar = frame.pendingErrVar;
|
|
717
760
|
pendingDecl = frame.pendingDecl;
|
|
761
|
+
pendingDeclNameValue = frame.pendingDeclNameValue;
|
|
718
762
|
pendingLoopVars = frame.pendingLoopVars;
|
|
763
|
+
loopCounterOpen = frame.loopCounterOpen;
|
|
764
|
+
pendingLoopCounterValue = frame.pendingLoopCounterValue;
|
|
719
765
|
declaredHere = frame.declaredHere;
|
|
720
766
|
headSigil = frame.headSigil;
|
|
721
767
|
cont = frame.cont;
|
|
@@ -803,7 +849,12 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
803
849
|
// string. A scan that swallowed this as string bytes would flip
|
|
804
850
|
// its quote phase on the first nested string and lose every
|
|
805
851
|
// binding after it.
|
|
806
|
-
frames.push({
|
|
852
|
+
frames.push({
|
|
853
|
+
char: next,
|
|
854
|
+
at: i + 1,
|
|
855
|
+
statements: next === "[",
|
|
856
|
+
loopCounterOpen,
|
|
857
|
+
});
|
|
807
858
|
depth++;
|
|
808
859
|
if (next === "[") {
|
|
809
860
|
// F6 — and it is a statement context like any other `[`. A `$[`
|
|
@@ -904,6 +955,18 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
904
955
|
// nested string flipping the quote phase, so every later `#` line read as
|
|
905
956
|
// string content) is now fixed in the shared scanner too, via the same
|
|
906
957
|
// frame model (#199); the two must stay in step.
|
|
958
|
+
// #503 — a column-0 `#` inside an array literal is a comment line too, but
|
|
959
|
+
// it ends nothing: the array's statement carries on past it. A continued
|
|
960
|
+
// line's comment keeps the H5 branch below, as in `maskUncached`.
|
|
961
|
+
if (
|
|
962
|
+
c === "#" &&
|
|
963
|
+
!contComment &&
|
|
964
|
+
startsArrayCommentLine(text, i, frames.at(-1))
|
|
965
|
+
) {
|
|
966
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
967
|
+
i--; // let the loop advance onto the newline (or end)
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
907
970
|
if (c === "#" && (atLead || contComment)) {
|
|
908
971
|
while (i < text.length && text[i] !== "\n") i++;
|
|
909
972
|
if (contComment) {
|
|
@@ -974,7 +1037,28 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
974
1037
|
// `set-dns`, and the console carries the class across the quotes, so the
|
|
975
1038
|
// span includes them. Without this the string is skipped and the next
|
|
976
1039
|
// bare word (`do`) is declared instead.
|
|
977
|
-
if (
|
|
1040
|
+
if (
|
|
1041
|
+
pendingDecl !== null ||
|
|
1042
|
+
pendingLoopVars ||
|
|
1043
|
+
pendingErrVar ||
|
|
1044
|
+
pendingLoopCounterValue
|
|
1045
|
+
) {
|
|
1046
|
+
// #533 — a quoted VALUE of a named arg declares only for `name=`.
|
|
1047
|
+
// `:local name="foo"` declares `foo`; `:local value="foo"` declares
|
|
1048
|
+
// nothing and stays a string (so `$` inside still classifies, S9).
|
|
1049
|
+
// Positional quotes (`:local "foo"`) declare.
|
|
1050
|
+
if (pendingDecl !== null) {
|
|
1051
|
+
const prev = prevNonSpace(text, i);
|
|
1052
|
+
if ((prev === "=" || prev === ",") && !pendingDeclNameValue) {
|
|
1053
|
+
frames.push({
|
|
1054
|
+
char: '"',
|
|
1055
|
+
at: i,
|
|
1056
|
+
statements: false,
|
|
1057
|
+
loopCounterOpen,
|
|
1058
|
+
});
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
978
1062
|
const close = text.indexOf('"', i + 1);
|
|
979
1063
|
if (close > i) {
|
|
980
1064
|
const name = text.slice(i + 1, close);
|
|
@@ -998,6 +1082,7 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
998
1082
|
const binding = bind(name, cls, i + 1);
|
|
999
1083
|
if (binding !== null) bindingIds.push(binding.id);
|
|
1000
1084
|
pendingDecl = null;
|
|
1085
|
+
pendingDeclNameValue = false;
|
|
1001
1086
|
declaredHere = true;
|
|
1002
1087
|
} else {
|
|
1003
1088
|
// F7 — statement scope, and for `:onerror` the enclosing claim
|
|
@@ -1023,7 +1108,12 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1023
1108
|
continue;
|
|
1024
1109
|
}
|
|
1025
1110
|
}
|
|
1026
|
-
frames.push({
|
|
1111
|
+
frames.push({
|
|
1112
|
+
char: '"',
|
|
1113
|
+
at: i,
|
|
1114
|
+
statements: false,
|
|
1115
|
+
loopCounterOpen,
|
|
1116
|
+
});
|
|
1027
1117
|
continue;
|
|
1028
1118
|
}
|
|
1029
1119
|
|
|
@@ -1036,6 +1126,9 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1036
1126
|
(c === "{" &&
|
|
1037
1127
|
(frames.at(-1)?.statements ?? true) &&
|
|
1038
1128
|
braceStartsStatements(text, i)),
|
|
1129
|
+
loopCounterOpen,
|
|
1130
|
+
pendingDecl:
|
|
1131
|
+
c === "{" && prevNonSpace(text, i) === "=" ? pendingDecl : null,
|
|
1039
1132
|
});
|
|
1040
1133
|
depth++;
|
|
1041
1134
|
if (c === "[") openBracket(leadBefore, i);
|
|
@@ -1071,7 +1164,7 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1071
1164
|
defects.push(defectAt("unbalanced-close", i, c));
|
|
1072
1165
|
continue;
|
|
1073
1166
|
}
|
|
1074
|
-
frames.pop();
|
|
1167
|
+
const popped = frames.pop();
|
|
1075
1168
|
if (filterDepth !== null && depth <= filterDepth) filterDepth = null;
|
|
1076
1169
|
// F6 — restore the statement the `[` interrupted, and drop the bracket's
|
|
1077
1170
|
// scope. Ordered after the `filterDepth` clear on purpose: the saved
|
|
@@ -1082,6 +1175,11 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1082
1175
|
if (suppressedScopes > 0) suppressedScopes--;
|
|
1083
1176
|
else if (scopes.length > 1) scopes.pop();
|
|
1084
1177
|
resetStatement();
|
|
1178
|
+
// #533 reorder — the head outlives its own array braces.
|
|
1179
|
+
if (popped !== undefined) {
|
|
1180
|
+
loopCounterOpen = popped.loopCounterOpen;
|
|
1181
|
+
pendingDecl = popped.pendingDecl ?? null;
|
|
1182
|
+
}
|
|
1085
1183
|
}
|
|
1086
1184
|
if (depth > 0) depth--;
|
|
1087
1185
|
continue;
|
|
@@ -1204,8 +1302,10 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1204
1302
|
pathOpen = true;
|
|
1205
1303
|
}
|
|
1206
1304
|
if (DECL[lower] !== undefined) pendingDecl = DECL[lower] as SymbolClass;
|
|
1207
|
-
else if (LOOP_HEADS.has(lower))
|
|
1208
|
-
|
|
1305
|
+
else if (LOOP_HEADS.has(lower)) {
|
|
1306
|
+
pendingLoopVars = true;
|
|
1307
|
+
loopCounterOpen = true;
|
|
1308
|
+
} else if (lower === "set") pendingSetTarget = true;
|
|
1209
1309
|
else if (ERRVAR_HEADS.has(lower)) pendingErrVar = true;
|
|
1210
1310
|
// S8 — `find`/`where` open a filter region from HEAD position too.
|
|
1211
1311
|
// `[find comment=$tag]` is the commonest shape of the whole corpus and
|
|
@@ -1299,6 +1399,17 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1299
1399
|
|
|
1300
1400
|
// S1/S2 — this word is the declared name.
|
|
1301
1401
|
if (pendingDecl !== null) {
|
|
1402
|
+
// #533 — named spelling (`:local name=foo value=1`): an argument
|
|
1403
|
+
// NAME (`word=`) is never the declaration. Only `name=` carries
|
|
1404
|
+
// it; its value declares. Other values (`value=foo`) are skipped,
|
|
1405
|
+
// and the pending declaration survives for a later `name=`
|
|
1406
|
+
// (`value=1 name=foo`). A positional word (`:local foo`, `:local
|
|
1407
|
+
// name`) declares as before.
|
|
1408
|
+
if (nextNonSpace(text, j) === "=") {
|
|
1409
|
+
pendingDeclNameValue = lower === "name";
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
if (isValue && !pendingDeclNameValue) continue;
|
|
1302
1413
|
const binding = bind(word, pendingDecl, wordStart);
|
|
1303
1414
|
record(
|
|
1304
1415
|
wordStart,
|
|
@@ -1311,6 +1422,7 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1311
1422
|
binding === null ? [] : [binding.id],
|
|
1312
1423
|
);
|
|
1313
1424
|
pendingDecl = null;
|
|
1425
|
+
pendingDeclNameValue = false;
|
|
1314
1426
|
declaredHere = true;
|
|
1315
1427
|
continue;
|
|
1316
1428
|
}
|
|
@@ -1318,6 +1430,13 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1318
1430
|
// S3 — `:foreach k,v in=…`: every word before `in=`/`from=` is a loop
|
|
1319
1431
|
// variable.
|
|
1320
1432
|
if (pendingLoopVars) {
|
|
1433
|
+
// #533 — named spelling (`:for counter=i …`): `counter=` carries
|
|
1434
|
+
// the loop var(s); the word `counter` itself is never one. Any
|
|
1435
|
+
// other `arg=` ends the var list, like `in=`/`from=` already did.
|
|
1436
|
+
if (nextNonSpace(text, j) === "=") {
|
|
1437
|
+
if (lower !== "counter") pendingLoopVars = false;
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1321
1440
|
if (lower === "in" || lower === "from") {
|
|
1322
1441
|
pendingLoopVars = false;
|
|
1323
1442
|
continue;
|
|
@@ -1339,6 +1458,43 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1339
1458
|
continue;
|
|
1340
1459
|
}
|
|
1341
1460
|
|
|
1461
|
+
// #533 reorder — a named `counter=` past `in=`/`from=` or other
|
|
1462
|
+
// args still carries the loop var: CHR accepts
|
|
1463
|
+
// `:for from=1 counter=i to=2`, and the value binds exactly like
|
|
1464
|
+
// a positional. Only `do` (the body) closes the head; a value of
|
|
1465
|
+
// another arg, or a further arg name, just ends this `counter=`.
|
|
1466
|
+
if (!isValue && lower === "do") {
|
|
1467
|
+
loopCounterOpen = false;
|
|
1468
|
+
pendingLoopCounterValue = false;
|
|
1469
|
+
}
|
|
1470
|
+
if (
|
|
1471
|
+
loopCounterOpen &&
|
|
1472
|
+
lower === "counter" &&
|
|
1473
|
+
nextNonSpace(text, j) === "="
|
|
1474
|
+
) {
|
|
1475
|
+
pendingLoopCounterValue = true;
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
if (pendingLoopCounterValue) {
|
|
1479
|
+
if (nextNonSpace(text, j) === "=" || !isValue) {
|
|
1480
|
+
pendingLoopCounterValue = false;
|
|
1481
|
+
} else {
|
|
1482
|
+
openStatementScope(wordStart);
|
|
1483
|
+
const counterBinding = bind(word, "auto", wordStart);
|
|
1484
|
+
record(
|
|
1485
|
+
wordStart,
|
|
1486
|
+
j,
|
|
1487
|
+
word,
|
|
1488
|
+
"auto",
|
|
1489
|
+
true,
|
|
1490
|
+
undefined,
|
|
1491
|
+
"binding",
|
|
1492
|
+
counterBinding === null ? [] : [counterBinding.id],
|
|
1493
|
+
);
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1342
1498
|
if (FILTER_WORDS.has(lower)) {
|
|
1343
1499
|
filterDepth = depth;
|
|
1344
1500
|
continue;
|
package/src/explain/transport.ts
CHANGED
|
@@ -111,6 +111,17 @@ export interface ExplainTransportInput {
|
|
|
111
111
|
* centrs surface should be offered for them and none is rendered.
|
|
112
112
|
*/
|
|
113
113
|
rejected?: string;
|
|
114
|
+
/**
|
|
115
|
+
* Why offline cannot pair the operands into a request, when two adjacent
|
|
116
|
+
* words may be an argument and its value with the `=` left out (#350).
|
|
117
|
+
*
|
|
118
|
+
* Also distinct from an unread argument list: the bytes are no evaluation,
|
|
119
|
+
* and nothing is wrong with them, but which word is an argument name is
|
|
120
|
+
* per-command schema. The reading before #350 classified these `unknown`
|
|
121
|
+
* through its positionals, and this keeps that fail-closed answer rather
|
|
122
|
+
* than letting the withdrawal turn it into an `execute` line.
|
|
123
|
+
*/
|
|
124
|
+
unpaired?: string;
|
|
114
125
|
}
|
|
115
126
|
|
|
116
127
|
export interface ExplainTransportOptions {
|
|
@@ -440,6 +451,8 @@ export function classifyExplainTransport(
|
|
|
440
451
|
// syntax error, which is the false confident reading in a second place.
|
|
441
452
|
if (input.rejected !== undefined)
|
|
442
453
|
return unknown(input.rejected, options.evidenceId);
|
|
454
|
+
if (input.unpaired !== undefined)
|
|
455
|
+
return unknown(input.unpaired, options.evidenceId);
|
|
443
456
|
|
|
444
457
|
// Root scripting directives have no RouterOS REST path. They always ride the
|
|
445
458
|
// raw execute surface, regardless of whether their operands are literal.
|
package/src/explain/values.ts
CHANGED
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
* them; only `:typeof` can. Grounded on CHR 7.21.5, 7.23.3 and 7.24rc4 in
|
|
18
18
|
* `test/fixtures/explain/operators.json` → `sweep.runtime` rows
|
|
19
19
|
* `typeof-deferred` / `deferred-array-typeof` and `sweep.opAxis` `defer-*`.
|
|
20
|
-
* Offline abstains on every such form
|
|
21
|
-
*
|
|
20
|
+
* Offline abstains on every such form, and so does the live `observedType`
|
|
21
|
+
* (#225): it reads only visible `:parse` conversions and never sends
|
|
22
|
+
* `:typeof`.
|
|
22
23
|
*
|
|
23
24
|
* ## The two contexts are different lexicons, not one lexicon plus a fallback
|
|
24
25
|
*
|
|
@@ -51,8 +52,8 @@ import { isIP } from "node:net";
|
|
|
51
52
|
* tests spellings, so reordering the members here must not change any result.
|
|
52
53
|
* `array` is emitted by the structured anchor reader rather than
|
|
53
54
|
* {@link valueShapeHints}, which receives decoded scalar values only.
|
|
54
|
-
* `op` is not a member: a deferred `(> …)` form abstains offline and
|
|
55
|
-
*
|
|
55
|
+
* `op` is not a member: a deferred `(> …)` form abstains offline and live
|
|
56
|
+
* (#288, #225).
|
|
56
57
|
*/
|
|
57
58
|
export const VALUE_SHAPES = [
|
|
58
59
|
"num",
|
|
@@ -70,6 +71,56 @@ export const VALUE_SHAPES = [
|
|
|
70
71
|
|
|
71
72
|
export type ValueShape = (typeof VALUE_SHAPES)[number];
|
|
72
73
|
|
|
74
|
+
/**
|
|
75
|
+
* The types a visible `:parse` conversion can name (#225 `observedType`).
|
|
76
|
+
*
|
|
77
|
+
* A subset of {@link VALUE_SHAPES}: these are exactly the scalar types whose
|
|
78
|
+
* device rendering differs from some spelling of them (`2.2` → `2.0.0.2`,
|
|
79
|
+
* `0x10` → `16`, `yes` → `true`, `200ms` → `00:00:00.200`, `1:0:0::1` →
|
|
80
|
+
* `1::1`). `str`, `id` and `mac` never convert, so they are never observed.
|
|
81
|
+
*/
|
|
82
|
+
export const OBSERVED_VALUE_TYPES = [
|
|
83
|
+
"num",
|
|
84
|
+
"bool",
|
|
85
|
+
"ip",
|
|
86
|
+
"ip-prefix",
|
|
87
|
+
"ip6",
|
|
88
|
+
"ip6-prefix",
|
|
89
|
+
"time",
|
|
90
|
+
] as const satisfies readonly ValueShape[];
|
|
91
|
+
|
|
92
|
+
export type ObservedValueType = (typeof OBSERVED_VALUE_TYPES)[number];
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The normalized terms a schema declaration maps to (#225 `schemaType`).
|
|
96
|
+
*
|
|
97
|
+
* The scalar names are shared with {@link VALUE_SHAPES} so a schema fact and a
|
|
98
|
+
* shape hint compare directly. The schema-only terms name declarations no
|
|
99
|
+
* literal spelling has: `ip-range` (`A.B.C.D[-A.B.C.D|…] (IP address range)`),
|
|
100
|
+
* `enum` (a listed word set such as `input | forward | output`, never closed:
|
|
101
|
+
* the device also lists current configuration names this way) and `name` (a
|
|
102
|
+
* reference to a configuration object). `bool` and `name` are CLI Reference
|
|
103
|
+
* spellings (`bool`, `iface_enum`) the live projection never emits:
|
|
104
|
+
* `/console/inspect` declares a boolean with a blank row, and prints the
|
|
105
|
+
* current interface names exactly like keywords.
|
|
106
|
+
*/
|
|
107
|
+
export const SCHEMA_VALUE_TYPES = [
|
|
108
|
+
"num",
|
|
109
|
+
"bool",
|
|
110
|
+
"ip",
|
|
111
|
+
"ip-prefix",
|
|
112
|
+
"ip-range",
|
|
113
|
+
"ip6",
|
|
114
|
+
"ip6-prefix",
|
|
115
|
+
"time",
|
|
116
|
+
"mac",
|
|
117
|
+
"str",
|
|
118
|
+
"enum",
|
|
119
|
+
"name",
|
|
120
|
+
] as const;
|
|
121
|
+
|
|
122
|
+
export type SchemaValueType = (typeof SCHEMA_VALUE_TYPES)[number];
|
|
123
|
+
|
|
73
124
|
/**
|
|
74
125
|
* Where the value sits, because RouterOS reads the same bytes differently.
|
|
75
126
|
*
|
package/src/explain/verbsplit.ts
CHANGED
|
@@ -88,6 +88,12 @@ import {
|
|
|
88
88
|
type Span,
|
|
89
89
|
type StatementAnalysis,
|
|
90
90
|
} from "./pathresolve.ts";
|
|
91
|
+
import {
|
|
92
|
+
asciiWordSpans,
|
|
93
|
+
isAsciiWhitespace,
|
|
94
|
+
type RunToken,
|
|
95
|
+
runTokens,
|
|
96
|
+
} from "./run-tokens.ts";
|
|
91
97
|
import {
|
|
92
98
|
documentRange,
|
|
93
99
|
type MaskedRange,
|
|
@@ -96,20 +102,6 @@ import {
|
|
|
96
102
|
} from "./segment.ts";
|
|
97
103
|
import { SUBMENU_DIRECTIVES, VERBS } from "./verbs.ts";
|
|
98
104
|
|
|
99
|
-
const BARE_WORD = /^[A-Za-z][A-Za-z0-9._-]*$/;
|
|
100
|
-
/**
|
|
101
|
-
* A word that ends the leading run: it carries `=` (V2) or opens a group /
|
|
102
|
-
* string / variable (V3). `:` is deliberately NOT here — RouterOS uses it only
|
|
103
|
-
* to start a scripting directive, not as a mid-statement opener, and a
|
|
104
|
-
* `:`-prefixed token is already rejected by `BARE_WORD` below, so it ends the
|
|
105
|
-
* run regardless.
|
|
106
|
-
*/
|
|
107
|
-
const RUN_TERMINATOR = /^[[({"$]/;
|
|
108
|
-
|
|
109
|
-
function isAsciiWhitespace(char: string | undefined): boolean {
|
|
110
|
-
return char === " " || char === "\t" || char === "\r" || char === "\n";
|
|
111
|
-
}
|
|
112
|
-
|
|
113
105
|
function trimAscii(text: string): string {
|
|
114
106
|
let start = 0;
|
|
115
107
|
let end = text.length;
|
|
@@ -125,78 +117,14 @@ function trimAsciiStart(text: string): string {
|
|
|
125
117
|
return text.slice(start);
|
|
126
118
|
}
|
|
127
119
|
|
|
128
|
-
/**
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
chars: Span[];
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* The words of a statement, WITH their offsets (#202c).
|
|
141
|
-
*
|
|
142
|
-
* The one word scanner this module has. It replaced a
|
|
143
|
-
* `replace(/\\\r?\n/g,"").split(whitespace)` pair whose output could not be
|
|
144
|
-
* mapped back to the text it came from — and a second scanner written beside it
|
|
145
|
-
* for the offsets would be free to disagree with the boundary rule, which is the
|
|
146
|
-
* failure #202b hit three times in argument handling.
|
|
147
|
-
*
|
|
148
|
-
* H5 from the Q1 segmenter: a `\<newline>` is removed before RouterOS parses the
|
|
149
|
-
* continued line, so it JOINS the words either side of it rather than separating
|
|
150
|
-
* them (`na\<nl>me` is the single word `name`) and contributes no character to
|
|
151
|
-
* the name. That is why a word's `end - start` is not its `name.length`: the span
|
|
152
|
-
* covers the continuation bytes, the name does not. A caller taking an interior
|
|
153
|
-
* offset from a name is therefore wrong on continued statements, and
|
|
154
|
-
* {@link describeStatement} only ever publishes the run's END.
|
|
155
|
-
*/
|
|
156
|
-
function* asciiWordSpans(text: string): Generator<Word> {
|
|
157
|
-
// Yielded ONE WORD AT A TIME rather than returned as a list. Both readers
|
|
158
|
-
// stop at the first word the leading run cannot take, and a statement's text
|
|
159
|
-
// carries its whole nested `do={…}` subtree — so building a word (and a span
|
|
160
|
-
// per character of it) for the rest of that subtree was work no caller ever
|
|
161
|
-
// looked at, once per enclosing level (#322). A word is complete when the
|
|
162
|
-
// next one starts, which is why the yield happens on the following boundary
|
|
163
|
-
// rather than at the point the word is opened.
|
|
164
|
-
let current: Word | null = null;
|
|
165
|
-
let i = 0;
|
|
166
|
-
while (i < text.length) {
|
|
167
|
-
const c = text[i] as string;
|
|
168
|
-
if (
|
|
169
|
-
c === "\\" &&
|
|
170
|
-
(text[i + 1] === "\n" || (text[i + 1] === "\r" && text[i + 2] === "\n"))
|
|
171
|
-
) {
|
|
172
|
-
// Inside a word this joins; between words it is whitespace that opens
|
|
173
|
-
// none — matching the removal-then-split the rule replaced.
|
|
174
|
-
i += text[i + 1] === "\r" ? 3 : 2;
|
|
175
|
-
if (current !== null) current.end = i;
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
if (isAsciiWhitespace(c)) {
|
|
179
|
-
if (current !== null) yield current;
|
|
180
|
-
current = null;
|
|
181
|
-
i++;
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
if (current === null) {
|
|
185
|
-
current = {
|
|
186
|
-
name: c,
|
|
187
|
-
start: i,
|
|
188
|
-
end: i + 1,
|
|
189
|
-
chars: [{ start: i, end: i + 1 }],
|
|
190
|
-
};
|
|
191
|
-
} else {
|
|
192
|
-
current.name += c;
|
|
193
|
-
current.end = i + 1;
|
|
194
|
-
current.chars.push({ start: i, end: i + 1 });
|
|
195
|
-
}
|
|
196
|
-
i++;
|
|
197
|
-
}
|
|
198
|
-
if (current !== null) yield current;
|
|
199
|
-
}
|
|
120
|
+
/** The run scanner lives in the leaf `run-tokens.ts` so `segment.ts` can share it (#496). */
|
|
121
|
+
export {
|
|
122
|
+
type LocatedRunToken,
|
|
123
|
+
locatedRunTokens,
|
|
124
|
+
type RunToken,
|
|
125
|
+
runTokens,
|
|
126
|
+
type Sep,
|
|
127
|
+
} from "./run-tokens.ts";
|
|
200
128
|
|
|
201
129
|
/**
|
|
202
130
|
* The frozen vocabulary lives in the leaf `verbs.ts` because `pathresolve.ts`
|
|
@@ -205,108 +133,6 @@ function* asciiWordSpans(text: string): Generator<Word> {
|
|
|
205
133
|
*/
|
|
206
134
|
export { SUBMENU_DIRECTIVES, VERBS } from "./verbs.ts";
|
|
207
135
|
|
|
208
|
-
/** Separator that precedes a run token (V1). */
|
|
209
|
-
export type Sep = "start" | "slash" | "space";
|
|
210
|
-
|
|
211
|
-
/** One token of the leading path run, with the punctuation that introduced it. */
|
|
212
|
-
export interface RunToken {
|
|
213
|
-
name: string;
|
|
214
|
-
sep: Sep;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** A run token mapped back to the statement bytes that spell it. */
|
|
218
|
-
export interface LocatedRunToken extends RunToken {
|
|
219
|
-
/** Name bytes, split where a `\<newline>` continuation interrupts them. */
|
|
220
|
-
nameSpans: Span[];
|
|
221
|
-
/** One valid `/` immediately before this token, if present. */
|
|
222
|
-
slashBefore?: Span;
|
|
223
|
-
/** One trailing `/` after this token, if present. */
|
|
224
|
-
slashAfter?: Span;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function coalesceChars(chars: readonly Span[]): Span[] {
|
|
228
|
-
const out: Span[] = [];
|
|
229
|
-
for (const char of chars) {
|
|
230
|
-
const last = out.at(-1);
|
|
231
|
-
if (last !== undefined && last.end === char.start) last.end = char.end;
|
|
232
|
-
else out.push({ start: char.start, end: char.end });
|
|
233
|
-
}
|
|
234
|
-
return out;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* The leading path-token run WITH separators preserved (V1). Same stopping
|
|
239
|
-
* rules as `pathresolve`'s run (V2/V3), so the two stay comparable: a slash-
|
|
240
|
-
* joined word expands to several `slash`-separated tokens; a space-separated
|
|
241
|
-
* word is one `space` token. Stops at the first `=`/group/string/var word, and
|
|
242
|
-
* at any word carrying a non-`BARE_WORD` segment (a variable segment truncates
|
|
243
|
-
* the run before the verb).
|
|
244
|
-
*/
|
|
245
|
-
export function locatedRunTokens(text: string): LocatedRunToken[] {
|
|
246
|
-
const out: LocatedRunToken[] = [];
|
|
247
|
-
for (const word of asciiWordSpans(text)) {
|
|
248
|
-
if (word.name.includes("=") || RUN_TERMINATOR.test(word.name)) break;
|
|
249
|
-
const parts: {
|
|
250
|
-
name: string;
|
|
251
|
-
start: number;
|
|
252
|
-
end: number;
|
|
253
|
-
slashBefore?: Span;
|
|
254
|
-
}[] = [];
|
|
255
|
-
let start = 0;
|
|
256
|
-
for (let i = 0; i <= word.name.length; i++) {
|
|
257
|
-
if (i < word.name.length && word.name[i] !== "/") continue;
|
|
258
|
-
if (start < i) {
|
|
259
|
-
const slashRunStart = start;
|
|
260
|
-
let slashCount = 0;
|
|
261
|
-
while (
|
|
262
|
-
slashRunStart - slashCount - 1 >= 0 &&
|
|
263
|
-
word.name[slashRunStart - slashCount - 1] === "/"
|
|
264
|
-
)
|
|
265
|
-
slashCount++;
|
|
266
|
-
const slashIndex = slashCount === 1 ? slashRunStart - 1 : -1;
|
|
267
|
-
parts.push({
|
|
268
|
-
name: word.name.slice(start, i),
|
|
269
|
-
start,
|
|
270
|
-
end: i,
|
|
271
|
-
...(slashIndex < 0 ? {} : { slashBefore: word.chars[slashIndex] }),
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
start = i + 1;
|
|
275
|
-
}
|
|
276
|
-
if (parts.length === 0) continue;
|
|
277
|
-
if (!parts.every((p) => BARE_WORD.test(p.name))) break;
|
|
278
|
-
for (let i = 0; i < parts.length; i++) {
|
|
279
|
-
const part = parts[i] as (typeof parts)[number];
|
|
280
|
-
const leadingSlash = word.name.startsWith("/") && i === 0;
|
|
281
|
-
const trailingSlash =
|
|
282
|
-
i === parts.length - 1 &&
|
|
283
|
-
word.name.endsWith("/") &&
|
|
284
|
-
!word.name.endsWith("//")
|
|
285
|
-
? word.chars.at(-1)
|
|
286
|
-
: undefined;
|
|
287
|
-
out.push({
|
|
288
|
-
name: part.name,
|
|
289
|
-
sep:
|
|
290
|
-
out.length === 0
|
|
291
|
-
? "start"
|
|
292
|
-
: i > 0 || leadingSlash
|
|
293
|
-
? "slash"
|
|
294
|
-
: "space",
|
|
295
|
-
nameSpans: coalesceChars(word.chars.slice(part.start, part.end)),
|
|
296
|
-
...(part.slashBefore === undefined
|
|
297
|
-
? {}
|
|
298
|
-
: { slashBefore: part.slashBefore }),
|
|
299
|
-
...(trailingSlash === undefined ? {} : { slashAfter: trailingSlash }),
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
return out;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
export function runTokens(text: string): RunToken[] {
|
|
307
|
-
return locatedRunTokens(text).map(({ name, sep }) => ({ name, sep }));
|
|
308
|
-
}
|
|
309
|
-
|
|
310
136
|
/** True when the statement is a scripting directive (`:x`, or a bare `x do={…}`). */
|
|
311
137
|
export function isDirective(text: string, range?: MaskedRange): boolean {
|
|
312
138
|
const t = trimAscii(text);
|
package/src/explain/write.ts
CHANGED
|
@@ -694,7 +694,14 @@ function collect(
|
|
|
694
694
|
});
|
|
695
695
|
continue;
|
|
696
696
|
}
|
|
697
|
-
|
|
697
|
+
// An ABSOLUTE `maybeNav` statement (#211) was `isNav` before R14, so it
|
|
698
|
+
// keeps this branch. `isConfirmedNav` cannot drop it, because its path is
|
|
699
|
+
// in neither table. A relative one never took this branch (#235), and
|
|
700
|
+
// still does not.
|
|
701
|
+
if (
|
|
702
|
+
(statement.isNav || (statement.maybeNav && t.startsWith("/"))) &&
|
|
703
|
+
!isCommandShaped(described, catalogAt)
|
|
704
|
+
) {
|
|
698
705
|
if (
|
|
699
706
|
isConfirmedNav(described) &&
|
|
700
707
|
!isDanglingBarePath(described, lastNonEmpty, index)
|