@tikoci/centrs 0.1.9-next.23 → 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 +14 -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 +24 -5
- package/commands/explain/README.md +452 -61
- package/commands/explain/examples.md +191 -41
- 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 +14 -9
- package/docs/MATRIX.md +34 -21
- 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 +2 -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 +115 -10
- package/src/explain/group-faults.ts +162 -29
- package/src/explain/operators.ts +2 -1
- package/src/explain/scope-brace.ts +226 -11
- package/src/explain/segment.ts +136 -1
- package/src/explain/symbols.ts +146 -6
- package/src/explain/values.ts +55 -4
- 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 +395 -43
- package/src/index.ts +6 -0
- package/src/protocols/adapter.ts +88 -10
- 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/segment.ts
CHANGED
|
@@ -91,6 +91,7 @@ import {
|
|
|
91
91
|
hashStartsHardError,
|
|
92
92
|
isIndexedStatementStart,
|
|
93
93
|
matchingBraceEnd,
|
|
94
|
+
menuBlockPath,
|
|
94
95
|
scopeNameFromMasked,
|
|
95
96
|
startsArrayCommentLine,
|
|
96
97
|
} from "./scope-brace.ts";
|
|
@@ -624,6 +625,137 @@ export function segmentScopeBody(
|
|
|
624
625
|
);
|
|
625
626
|
}
|
|
626
627
|
|
|
628
|
+
/** Nesting cap for dispatch-statement counting (#549): the 16× guard (#322). */
|
|
629
|
+
const MAX_DISPATCH_STATEMENT_DEPTH = 16;
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Executable statements in `text`, at any nesting depth (#549).
|
|
633
|
+
*
|
|
634
|
+
* A dispatched script can carry statements inside executed control scopes
|
|
635
|
+
* (`:if … do={…}`, `:for`/`:foreach`/`:while`, `:do`, `:retry`, `:onerror`,
|
|
636
|
+
* `once do={…}`), and those run before a later refusal exactly like
|
|
637
|
+
* top-level ones do — `:if (true) do={ /system/note/set …; /user/add … }`
|
|
638
|
+
* applies the note before the refused add on CHR 7.25. Stored bodies
|
|
639
|
+
* (`source={…}`, `on-event={…}`, any other `name={`) never execute at
|
|
640
|
+
* dispatch, so only `scopeNameFromMasked` scopes recurse — except a `do={…}`
|
|
641
|
+
* under a `:global`/`:local`/`:set` head, which stores a function value.
|
|
642
|
+
* Command substitutions (`[…]`) run at dispatch too — `:put [:put "p5-first";
|
|
643
|
+
* /system/identity/set …]` prints before the refused set on CHR 7.25 — so
|
|
644
|
+
* they recurse unless they sit inside a stored or array `{…}`. An `else={…}`
|
|
645
|
+
* paired with its `do={…}` counts the larger side, not the sum, because the
|
|
646
|
+
* branches exclude each other.
|
|
647
|
+
*
|
|
648
|
+
* A segment with executed blocks or substitutions counts their inner
|
|
649
|
+
* statements, not itself (a control head is not an effect); a `/menu { … }`
|
|
650
|
+
* prefix counts nothing (navigation is not an effect); any other segment
|
|
651
|
+
* counts one. So the commonest write shape, `/… remove [find …]`, stays one
|
|
652
|
+
* statement — at the cost of a lone substitution that itself writes before a
|
|
653
|
+
* refused enclosing statement, which also counts one. Past the depth cap the
|
|
654
|
+
* inner text counts its top-level siblings only. Unbalanced input counts what
|
|
655
|
+
* precedes the break and never throws.
|
|
656
|
+
*/
|
|
657
|
+
export function countExecutableStatements(text: string): number {
|
|
658
|
+
return countExecutableStatementsWithin(text, 0);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function countExecutableStatementsWithin(text: string, depth: number): number {
|
|
662
|
+
const { segments, ascii } = segmentStatements(text);
|
|
663
|
+
if (depth >= MAX_DISPATCH_STATEMENT_DEPTH) return segments.length;
|
|
664
|
+
let count = 0;
|
|
665
|
+
for (const segment of segments) {
|
|
666
|
+
// A `/menu { … }` prefix only navigates; it is not an effect. The
|
|
667
|
+
// `ascii` guard is load-bearing: segment offsets are analyzed-byte
|
|
668
|
+
// offsets, which are string indexes only for pure-ASCII input.
|
|
669
|
+
if (ascii && segment.menuOnly && /^\s*\{/.test(text.slice(segment.end)))
|
|
670
|
+
continue;
|
|
671
|
+
const inner = countExecutedBlockStatements(segment.text, depth);
|
|
672
|
+
count += inner > 0 ? inner : 1;
|
|
673
|
+
}
|
|
674
|
+
return count;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** Executable statements in the segment's executed `{…}` scopes and `[…]`. */
|
|
678
|
+
function countExecutedBlockStatements(
|
|
679
|
+
segmentText: string,
|
|
680
|
+
depth: number,
|
|
681
|
+
): number {
|
|
682
|
+
const masked = maskComments(segmentText);
|
|
683
|
+
const storedDoHead = /^:(?:global|local|set)\b/i.test(masked);
|
|
684
|
+
let total = 0;
|
|
685
|
+
let i = 0;
|
|
686
|
+
const stack: string[] = [];
|
|
687
|
+
// The last executed scope closed at this level, for `do`/`else` pairing.
|
|
688
|
+
let lastScope: { name: string; count: number; end: number } | null = null;
|
|
689
|
+
// The outermost open `[` that executes at dispatch, and its stack depth.
|
|
690
|
+
let substitution: { start: number; depth: number } | null = null;
|
|
691
|
+
while (i < masked.length) {
|
|
692
|
+
const c = masked[i];
|
|
693
|
+
if (c === '"') {
|
|
694
|
+
i = scanQuotedString(masked, i).end;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
if (c === "[" || c === "(") {
|
|
698
|
+
if (c === "[" && !stack.includes("[") && !stack.includes("{")) {
|
|
699
|
+
substitution = { start: i, depth: stack.length };
|
|
700
|
+
}
|
|
701
|
+
stack.push(c);
|
|
702
|
+
i++;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (c === "]" || c === ")") {
|
|
706
|
+
const want = c === "]" ? "[" : "(";
|
|
707
|
+
if (stack.at(-1) === want) {
|
|
708
|
+
stack.pop();
|
|
709
|
+
if (c === "]" && stack.length === substitution?.depth) {
|
|
710
|
+
total += countExecutableStatementsWithin(
|
|
711
|
+
segmentText.slice(substitution.start + 1, i),
|
|
712
|
+
depth + 1,
|
|
713
|
+
);
|
|
714
|
+
substitution = null;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
i++;
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (c === "{") {
|
|
721
|
+
const scope = stack.length === 0 ? scopeNameFromMasked(masked, i) : null;
|
|
722
|
+
if (
|
|
723
|
+
scope !== null &&
|
|
724
|
+
!(storedDoHead && (scope === "do" || scope === "command"))
|
|
725
|
+
) {
|
|
726
|
+
const end = matchingBraceEnd(masked, i);
|
|
727
|
+
if (end === undefined) return total;
|
|
728
|
+
const count = countExecutableStatementsWithin(
|
|
729
|
+
segmentText.slice(i + 1, end),
|
|
730
|
+
depth + 1,
|
|
731
|
+
);
|
|
732
|
+
if (
|
|
733
|
+
scope === "else" &&
|
|
734
|
+
lastScope?.name === "do" &&
|
|
735
|
+
/^\s*else\s*=?\s*$/.test(masked.slice(lastScope.end + 1, i))
|
|
736
|
+
) {
|
|
737
|
+
total += Math.max(lastScope.count, count) - lastScope.count;
|
|
738
|
+
} else {
|
|
739
|
+
total += count;
|
|
740
|
+
}
|
|
741
|
+
lastScope = { name: scope, count, end };
|
|
742
|
+
i = end + 1;
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
stack.push("{");
|
|
746
|
+
i++;
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (c === "}") {
|
|
750
|
+
if (stack.at(-1) === "{") stack.pop();
|
|
751
|
+
i++;
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
i++;
|
|
755
|
+
}
|
|
756
|
+
return total;
|
|
757
|
+
}
|
|
758
|
+
|
|
627
759
|
function segmentStatementsInternal(
|
|
628
760
|
original: string,
|
|
629
761
|
range: MaskedRange | undefined,
|
|
@@ -1073,7 +1205,10 @@ function scanAscii(
|
|
|
1073
1205
|
if (
|
|
1074
1206
|
c === "{" &&
|
|
1075
1207
|
atContainerLevel() &&
|
|
1076
|
-
validPrefix(f, f.stmtStart >= 0 ? f.stmtStart : i, i)
|
|
1208
|
+
(validPrefix(f, f.stmtStart >= 0 ? f.stmtStart : i, i) ||
|
|
1209
|
+
// A relative submenu inside a container is one too (#351).
|
|
1210
|
+
(frames.length > 1 &&
|
|
1211
|
+
menuBlockPath(indexed, floor + i, floor) !== null))
|
|
1077
1212
|
) {
|
|
1078
1213
|
if (frames.length - 1 < MAX_CONTAINER_DEPTH) {
|
|
1079
1214
|
openContainer(i);
|
package/src/explain/symbols.ts
CHANGED
|
@@ -419,7 +419,10 @@ interface BracketFrame {
|
|
|
419
419
|
pendingSetTarget: boolean;
|
|
420
420
|
pendingErrVar: boolean;
|
|
421
421
|
pendingDecl: SymbolClass | null;
|
|
422
|
+
pendingDeclNameValue: boolean;
|
|
422
423
|
pendingLoopVars: boolean;
|
|
424
|
+
loopCounterOpen: boolean;
|
|
425
|
+
pendingLoopCounterValue: boolean;
|
|
423
426
|
declaredHere: boolean;
|
|
424
427
|
headSigil: ":" | "/" | "";
|
|
425
428
|
cont: Continuation;
|
|
@@ -511,6 +514,20 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
511
514
|
char: "{" | "[" | "(" | '"';
|
|
512
515
|
at: number;
|
|
513
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;
|
|
514
531
|
}[] = [];
|
|
515
532
|
/** bracket/brace nesting, strings excluded (the S8 filter region uses it). */
|
|
516
533
|
let depth = 0;
|
|
@@ -525,7 +542,23 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
525
542
|
/** depth at which a filter word opened its region, or null. */
|
|
526
543
|
let filterDepth: number | null = null;
|
|
527
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;
|
|
528
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;
|
|
529
562
|
/** inside a space-separated menu path run, where bare words are `dir` (S16). */
|
|
530
563
|
let inMenuPath = false;
|
|
531
564
|
/**
|
|
@@ -593,7 +626,10 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
593
626
|
pendingSetTarget = false;
|
|
594
627
|
pendingErrVar = false;
|
|
595
628
|
pendingDecl = null;
|
|
629
|
+
pendingDeclNameValue = false;
|
|
596
630
|
pendingLoopVars = false;
|
|
631
|
+
loopCounterOpen = false;
|
|
632
|
+
pendingLoopCounterValue = false;
|
|
597
633
|
declaredHere = false;
|
|
598
634
|
headSigil = "";
|
|
599
635
|
cont = "none";
|
|
@@ -666,7 +702,10 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
666
702
|
pendingSetTarget,
|
|
667
703
|
pendingErrVar,
|
|
668
704
|
pendingDecl,
|
|
705
|
+
pendingDeclNameValue,
|
|
669
706
|
pendingLoopVars,
|
|
707
|
+
loopCounterOpen,
|
|
708
|
+
pendingLoopCounterValue,
|
|
670
709
|
declaredHere,
|
|
671
710
|
headSigil,
|
|
672
711
|
cont,
|
|
@@ -719,7 +758,10 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
719
758
|
pendingSetTarget = frame.pendingSetTarget;
|
|
720
759
|
pendingErrVar = frame.pendingErrVar;
|
|
721
760
|
pendingDecl = frame.pendingDecl;
|
|
761
|
+
pendingDeclNameValue = frame.pendingDeclNameValue;
|
|
722
762
|
pendingLoopVars = frame.pendingLoopVars;
|
|
763
|
+
loopCounterOpen = frame.loopCounterOpen;
|
|
764
|
+
pendingLoopCounterValue = frame.pendingLoopCounterValue;
|
|
723
765
|
declaredHere = frame.declaredHere;
|
|
724
766
|
headSigil = frame.headSigil;
|
|
725
767
|
cont = frame.cont;
|
|
@@ -807,7 +849,12 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
807
849
|
// string. A scan that swallowed this as string bytes would flip
|
|
808
850
|
// its quote phase on the first nested string and lose every
|
|
809
851
|
// binding after it.
|
|
810
|
-
frames.push({
|
|
852
|
+
frames.push({
|
|
853
|
+
char: next,
|
|
854
|
+
at: i + 1,
|
|
855
|
+
statements: next === "[",
|
|
856
|
+
loopCounterOpen,
|
|
857
|
+
});
|
|
811
858
|
depth++;
|
|
812
859
|
if (next === "[") {
|
|
813
860
|
// F6 — and it is a statement context like any other `[`. A `$[`
|
|
@@ -990,7 +1037,28 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
990
1037
|
// `set-dns`, and the console carries the class across the quotes, so the
|
|
991
1038
|
// span includes them. Without this the string is skipped and the next
|
|
992
1039
|
// bare word (`do`) is declared instead.
|
|
993
|
-
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
|
+
}
|
|
994
1062
|
const close = text.indexOf('"', i + 1);
|
|
995
1063
|
if (close > i) {
|
|
996
1064
|
const name = text.slice(i + 1, close);
|
|
@@ -1014,6 +1082,7 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1014
1082
|
const binding = bind(name, cls, i + 1);
|
|
1015
1083
|
if (binding !== null) bindingIds.push(binding.id);
|
|
1016
1084
|
pendingDecl = null;
|
|
1085
|
+
pendingDeclNameValue = false;
|
|
1017
1086
|
declaredHere = true;
|
|
1018
1087
|
} else {
|
|
1019
1088
|
// F7 — statement scope, and for `:onerror` the enclosing claim
|
|
@@ -1039,7 +1108,12 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1039
1108
|
continue;
|
|
1040
1109
|
}
|
|
1041
1110
|
}
|
|
1042
|
-
frames.push({
|
|
1111
|
+
frames.push({
|
|
1112
|
+
char: '"',
|
|
1113
|
+
at: i,
|
|
1114
|
+
statements: false,
|
|
1115
|
+
loopCounterOpen,
|
|
1116
|
+
});
|
|
1043
1117
|
continue;
|
|
1044
1118
|
}
|
|
1045
1119
|
|
|
@@ -1052,6 +1126,9 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1052
1126
|
(c === "{" &&
|
|
1053
1127
|
(frames.at(-1)?.statements ?? true) &&
|
|
1054
1128
|
braceStartsStatements(text, i)),
|
|
1129
|
+
loopCounterOpen,
|
|
1130
|
+
pendingDecl:
|
|
1131
|
+
c === "{" && prevNonSpace(text, i) === "=" ? pendingDecl : null,
|
|
1055
1132
|
});
|
|
1056
1133
|
depth++;
|
|
1057
1134
|
if (c === "[") openBracket(leadBefore, i);
|
|
@@ -1087,7 +1164,7 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1087
1164
|
defects.push(defectAt("unbalanced-close", i, c));
|
|
1088
1165
|
continue;
|
|
1089
1166
|
}
|
|
1090
|
-
frames.pop();
|
|
1167
|
+
const popped = frames.pop();
|
|
1091
1168
|
if (filterDepth !== null && depth <= filterDepth) filterDepth = null;
|
|
1092
1169
|
// F6 — restore the statement the `[` interrupted, and drop the bracket's
|
|
1093
1170
|
// scope. Ordered after the `filterDepth` clear on purpose: the saved
|
|
@@ -1098,6 +1175,11 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1098
1175
|
if (suppressedScopes > 0) suppressedScopes--;
|
|
1099
1176
|
else if (scopes.length > 1) scopes.pop();
|
|
1100
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
|
+
}
|
|
1101
1183
|
}
|
|
1102
1184
|
if (depth > 0) depth--;
|
|
1103
1185
|
continue;
|
|
@@ -1220,8 +1302,10 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1220
1302
|
pathOpen = true;
|
|
1221
1303
|
}
|
|
1222
1304
|
if (DECL[lower] !== undefined) pendingDecl = DECL[lower] as SymbolClass;
|
|
1223
|
-
else if (LOOP_HEADS.has(lower))
|
|
1224
|
-
|
|
1305
|
+
else if (LOOP_HEADS.has(lower)) {
|
|
1306
|
+
pendingLoopVars = true;
|
|
1307
|
+
loopCounterOpen = true;
|
|
1308
|
+
} else if (lower === "set") pendingSetTarget = true;
|
|
1225
1309
|
else if (ERRVAR_HEADS.has(lower)) pendingErrVar = true;
|
|
1226
1310
|
// S8 — `find`/`where` open a filter region from HEAD position too.
|
|
1227
1311
|
// `[find comment=$tag]` is the commonest shape of the whole corpus and
|
|
@@ -1315,6 +1399,17 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1315
1399
|
|
|
1316
1400
|
// S1/S2 — this word is the declared name.
|
|
1317
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;
|
|
1318
1413
|
const binding = bind(word, pendingDecl, wordStart);
|
|
1319
1414
|
record(
|
|
1320
1415
|
wordStart,
|
|
@@ -1327,6 +1422,7 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1327
1422
|
binding === null ? [] : [binding.id],
|
|
1328
1423
|
);
|
|
1329
1424
|
pendingDecl = null;
|
|
1425
|
+
pendingDeclNameValue = false;
|
|
1330
1426
|
declaredHere = true;
|
|
1331
1427
|
continue;
|
|
1332
1428
|
}
|
|
@@ -1334,6 +1430,13 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1334
1430
|
// S3 — `:foreach k,v in=…`: every word before `in=`/`from=` is a loop
|
|
1335
1431
|
// variable.
|
|
1336
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
|
+
}
|
|
1337
1440
|
if (lower === "in" || lower === "from") {
|
|
1338
1441
|
pendingLoopVars = false;
|
|
1339
1442
|
continue;
|
|
@@ -1355,6 +1458,43 @@ export function resolveSymbols(original: string): SymbolAnalysis {
|
|
|
1355
1458
|
continue;
|
|
1356
1459
|
}
|
|
1357
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
|
+
|
|
1358
1498
|
if (FILTER_WORDS.has(lower)) {
|
|
1359
1499
|
filterDepth = depth;
|
|
1360
1500
|
continue;
|
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
|
*
|