@turing-machine-js/machine 7.0.0-alpha.1 → 7.0.0-alpha.2
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 +50 -0
- package/README.md +29 -23
- package/dist/index.cjs +515 -310
- package/dist/index.mjs +515 -310
- package/dist/utilities/graph.d.ts +3 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -349,7 +349,7 @@ var __classPrivateFieldGet$2 = (undefined && undefined.__classPrivateFieldGet) |
|
|
|
349
349
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
350
350
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
351
351
|
};
|
|
352
|
-
var _a, _TapeBlock_symbolToPatternListMap, _TapeBlock_lock, _TapeBlock_tapes, _TapeBlock_generateSymbolHint, _TapeBlock_buildPatternList, _TapeBlock_getSymbolForPatternList, _TapeBlock_symbol;
|
|
352
|
+
var _a$1, _TapeBlock_symbolToPatternListMap, _TapeBlock_lock, _TapeBlock_tapes, _TapeBlock_generateSymbolHint, _TapeBlock_buildPatternList, _TapeBlock_getSymbolForPatternList, _TapeBlock_symbol;
|
|
353
353
|
const symbolToPatternListMapSymbol = Symbol('symbol for symbolToPatternListMap setter');
|
|
354
354
|
const lockSymbol = Symbol('capture symbol');
|
|
355
355
|
class TapeBlock {
|
|
@@ -388,7 +388,7 @@ class TapeBlock {
|
|
|
388
388
|
symbol = storedPatternListSymbol;
|
|
389
389
|
}
|
|
390
390
|
else {
|
|
391
|
-
symbol = Symbol(__classPrivateFieldGet$2(_a, _a, "f", _TapeBlock_generateSymbolHint).call(_a, patternList));
|
|
391
|
+
symbol = Symbol(__classPrivateFieldGet$2(_a$1, _a$1, "f", _TapeBlock_generateSymbolHint).call(_a$1, patternList));
|
|
392
392
|
__classPrivateFieldGet$2(this, _TapeBlock_symbolToPatternListMap, "f").set(symbol, patternList);
|
|
393
393
|
}
|
|
394
394
|
return symbol;
|
|
@@ -488,10 +488,10 @@ class TapeBlock {
|
|
|
488
488
|
clone(cloneTapes = false) {
|
|
489
489
|
let tapeBlock;
|
|
490
490
|
if (cloneTapes) {
|
|
491
|
-
tapeBlock = _a.fromTapes(this.tapes.map((tape) => new Tape(tape)));
|
|
491
|
+
tapeBlock = _a$1.fromTapes(this.tapes.map((tape) => new Tape(tape)));
|
|
492
492
|
}
|
|
493
493
|
else {
|
|
494
|
-
tapeBlock = _a.fromAlphabets(this.alphabets);
|
|
494
|
+
tapeBlock = _a$1.fromAlphabets(this.alphabets);
|
|
495
495
|
}
|
|
496
496
|
tapeBlock[symbolToPatternListMapSymbol] = __classPrivateFieldGet$2(this, _TapeBlock_symbolToPatternListMap, "f");
|
|
497
497
|
return tapeBlock;
|
|
@@ -520,12 +520,12 @@ class TapeBlock {
|
|
|
520
520
|
}
|
|
521
521
|
}
|
|
522
522
|
}
|
|
523
|
-
_a = TapeBlock;
|
|
523
|
+
_a$1 = TapeBlock;
|
|
524
524
|
TapeBlock.fromAlphabets = (alphabets) => {
|
|
525
|
-
return new _a({ alphabets });
|
|
525
|
+
return new _a$1({ alphabets });
|
|
526
526
|
};
|
|
527
527
|
TapeBlock.fromTapes = (tapes) => {
|
|
528
|
-
return new _a({ tapes });
|
|
528
|
+
return new _a$1({ tapes });
|
|
529
529
|
};
|
|
530
530
|
_TapeBlock_generateSymbolHint = { value: (patternList) => JSON.stringify(patternList
|
|
531
531
|
.map((pattern) => pattern
|
|
@@ -689,7 +689,7 @@ var __classPrivateFieldGet$1 = (undefined && undefined.__classPrivateFieldGet) |
|
|
|
689
689
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
690
690
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
691
691
|
};
|
|
692
|
-
var _DebugConfig_ownerState, _DebugConfig_before, _DebugConfig_after, _State_id, _State_name, _State_overriddenHaltState, _State_bareState, _State_symbolToDataMap, _State_debugRef;
|
|
692
|
+
var _DebugConfig_ownerState, _DebugConfig_before, _DebugConfig_after, _a, _State_wrapperCache, _State_id, _State_name, _State_overriddenHaltState, _State_bareState, _State_symbolToDataMap, _State_debugRef;
|
|
693
693
|
const ifOtherSymbol = Symbol('other symbol');
|
|
694
694
|
// Module-private symbol used by DebugConfig setters to call State's validator
|
|
695
695
|
// without exposing the validator on the public surface.
|
|
@@ -762,7 +762,7 @@ class State {
|
|
|
762
762
|
symbols.forEach((symbol) => {
|
|
763
763
|
const { nextState } = stateDefinition[symbol];
|
|
764
764
|
const nextStateLocal = nextState ?? this;
|
|
765
|
-
if (!(nextStateLocal instanceof
|
|
765
|
+
if (!(nextStateLocal instanceof _a) && !(nextStateLocal instanceof Reference)) {
|
|
766
766
|
throw new Error('invalid nextState');
|
|
767
767
|
}
|
|
768
768
|
let { command } = stateDefinition[symbol];
|
|
@@ -882,16 +882,40 @@ class State {
|
|
|
882
882
|
throw new Error(`No nextState for symbol at state named ${__classPrivateFieldGet$1(this, _State_id, "f")}`);
|
|
883
883
|
}
|
|
884
884
|
withOverriddenHaltState(overriddenHaltState) {
|
|
885
|
-
//
|
|
886
|
-
//
|
|
887
|
-
//
|
|
888
|
-
//
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
|
|
885
|
+
// Unwrap `this` if it's itself a wrapper — the chain's inner overrides
|
|
886
|
+
// are dead at runtime anyway (only the outermost `.wohs()`'s override is
|
|
887
|
+
// pushed onto the halt-stack on entry; verified empirically). Composite
|
|
888
|
+
// name reflects runtime behavior, not construction history. See #176.
|
|
889
|
+
const bare = __classPrivateFieldGet$1(this, _State_bareState, "f") ?? this;
|
|
890
|
+
// Memoize by (bare, override) so identical args return the same instance
|
|
891
|
+
// (#175). The cache uses WeakMaps + WeakRefs so cached wrappers can be
|
|
892
|
+
// GC'd when nothing else holds them. Compounds with the chain-collapse
|
|
893
|
+
// above: `A.wohs(t1).wohs(t2)` keys as (A, t2) after the unwrap, hitting
|
|
894
|
+
// the same cache slot as a direct `A.wohs(t2)`.
|
|
895
|
+
let innerCache = __classPrivateFieldGet$1(_a, _a, "f", _State_wrapperCache).get(bare);
|
|
896
|
+
if (innerCache !== undefined) {
|
|
897
|
+
const ref = innerCache.get(overriddenHaltState);
|
|
898
|
+
if (ref !== undefined) {
|
|
899
|
+
const cached = ref.deref();
|
|
900
|
+
if (cached !== undefined) {
|
|
901
|
+
return cached;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
else {
|
|
906
|
+
innerCache = new WeakMap();
|
|
907
|
+
__classPrivateFieldGet$1(_a, _a, "f", _State_wrapperCache).set(bare, innerCache);
|
|
908
|
+
}
|
|
909
|
+
// Cache miss — construct with no name, then overwrite #name directly
|
|
910
|
+
// (composed names contain `(` and `)` which the constructor's user-facing
|
|
911
|
+
// validation would reject; private-field access bypasses that).
|
|
912
|
+
const state = new _a();
|
|
913
|
+
__classPrivateFieldSet$1(state, _State_name, `${bare.name}(${overriddenHaltState.name})`, "f");
|
|
914
|
+
__classPrivateFieldSet$1(state, _State_symbolToDataMap, __classPrivateFieldGet$1(bare, _State_symbolToDataMap, "f"), "f");
|
|
892
915
|
__classPrivateFieldSet$1(state, _State_overriddenHaltState, overriddenHaltState, "f");
|
|
893
|
-
__classPrivateFieldSet$1(state, _State_debugRef, __classPrivateFieldGet$1(
|
|
894
|
-
__classPrivateFieldSet$1(state, _State_bareState,
|
|
916
|
+
__classPrivateFieldSet$1(state, _State_debugRef, __classPrivateFieldGet$1(bare, _State_debugRef, "f"), "f");
|
|
917
|
+
__classPrivateFieldSet$1(state, _State_bareState, bare, "f");
|
|
918
|
+
innerCache.set(overriddenHaltState, new WeakRef(state));
|
|
895
919
|
return state;
|
|
896
920
|
}
|
|
897
921
|
// Single-state introspection — no traversal, no tapeBlock required.
|
|
@@ -904,7 +928,7 @@ class State {
|
|
|
904
928
|
for (const [sym, { command, nextState }] of __classPrivateFieldGet$1(state, _State_symbolToDataMap, "f")) {
|
|
905
929
|
let target = null;
|
|
906
930
|
try {
|
|
907
|
-
target = nextState instanceof
|
|
931
|
+
target = nextState instanceof _a ? nextState : nextState.ref;
|
|
908
932
|
}
|
|
909
933
|
catch {
|
|
910
934
|
target = null; // unbound Reference
|
|
@@ -928,153 +952,86 @@ class State {
|
|
|
928
952
|
transitions,
|
|
929
953
|
};
|
|
930
954
|
}
|
|
931
|
-
// Walks the State graph and emits a `Graph` data structure. v7
|
|
932
|
-
//
|
|
933
|
-
// onto their bare's representation in the graph, with the wrapper's own `#id`
|
|
934
|
-
// used as the graph node id, `isWrapped: true`, and `overriddenHaltStateId`
|
|
935
|
-
// set to the override's collapsed id. A per-wrapper "halt marker" graph node
|
|
936
|
-
// (id = negative-of-the-wrapper-id, `isHalt: true, isHaltMarker: true`) is
|
|
937
|
-
// synthesized; the bare's halt-bound transitions are rewritten to target the
|
|
938
|
-
// halt marker instead of the real one.
|
|
955
|
+
// Walks the State graph and emits a `Graph` data structure. v7 callable-
|
|
956
|
+
// subtree emit shape (#174):
|
|
939
957
|
//
|
|
940
|
-
//
|
|
941
|
-
//
|
|
942
|
-
//
|
|
958
|
+
// Each `withOverriddenHaltState` wrapper produces TWO graph nodes:
|
|
959
|
+
// - A wrapper node (`isWrapper: true`, `[[composite-name]]` shape) — the
|
|
960
|
+
// call site. No transitions of its own. `bareStateId` points to the
|
|
961
|
+
// bare's GraphNode; `overriddenHaltStateId` points to the override
|
|
962
|
+
// target's GraphNode.
|
|
963
|
+
// - A bare node (`isWrapper: false`, regular shape) — the callable body.
|
|
964
|
+
// Has the bare's transitions. Shared across all wrappers that wrap
|
|
965
|
+
// this bare (no per-context duplication).
|
|
966
|
+
//
|
|
967
|
+
// Frames are computed via union-find on bare reachability: two bares whose
|
|
968
|
+
// forward-reachable sets overlap merge into one frame. Each frame contains
|
|
969
|
+
// its bares + body states + a single halt marker (id = `-frameId`). The
|
|
970
|
+
// canonical `frameId` is the smallest bare-id in the component.
|
|
971
|
+
//
|
|
972
|
+
// Halt-bound transitions of any in-frame state are retargeted to the
|
|
973
|
+
// frame's halt marker. The frame's `subtree -. return .-> wrapper` and
|
|
974
|
+
// `subtree -. halt .-> s0` arrows are demand-emitted by `toMermaid` from
|
|
975
|
+
// the frame structure; they're not stored as graph edges.
|
|
943
976
|
static toGraph(initialState, tapeBlock) {
|
|
944
977
|
const nodes = {};
|
|
945
978
|
const alphabets = tapeBlock.alphabets.map((alphabet) => alphabet.symbols);
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
|
|
949
|
-
const
|
|
950
|
-
const
|
|
951
|
-
// The `initialId` is the user-passed start. If it's a wrapper, the
|
|
952
|
-
// collapsed graph node uses its `#id`; otherwise its own `#id`.
|
|
953
|
-
const initialId = __classPrivateFieldGet$1(initialState, _State_id, "f");
|
|
954
|
-
const queue = [];
|
|
955
|
-
// Decide how to enqueue the start: if it's a wrapper, enqueue its bare with
|
|
956
|
-
// the wrapper as context; otherwise enqueue the state itself.
|
|
957
|
-
if (__classPrivateFieldGet$1(initialState, _State_overriddenHaltState, "f") && __classPrivateFieldGet$1(initialState, _State_bareState, "f")) {
|
|
958
|
-
queue.push({ state: __classPrivateFieldGet$1(initialState, _State_bareState, "f"), wrapperContext: initialState });
|
|
959
|
-
}
|
|
960
|
-
else {
|
|
961
|
-
queue.push({ state: initialState, wrapperContext: null });
|
|
962
|
-
}
|
|
979
|
+
// Pass 1: BFS-discover all reachable States; emit one GraphNode per State
|
|
980
|
+
// (wrapper or bare/regular). Wrappers and bares are separate nodes.
|
|
981
|
+
const visited = new Set();
|
|
982
|
+
const queue = [initialState];
|
|
983
|
+
const bareIds = new Set(); // ids referenced as a wrapper's bareStateId
|
|
963
984
|
while (queue.length > 0) {
|
|
964
|
-
const
|
|
985
|
+
const state = queue.shift();
|
|
986
|
+
if (visited.has(__classPrivateFieldGet$1(state, _State_id, "f"))) {
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
visited.add(__classPrivateFieldGet$1(state, _State_id, "f"));
|
|
965
990
|
if (state.isHalt) {
|
|
966
|
-
// Real halt — always id 0, single node.
|
|
967
991
|
if (!(0 in nodes)) {
|
|
968
992
|
nodes[0] = {
|
|
969
993
|
id: 0,
|
|
970
994
|
name: __classPrivateFieldGet$1(state, _State_name, "f"),
|
|
971
995
|
isHalt: true,
|
|
972
996
|
isHaltMarker: false,
|
|
973
|
-
|
|
997
|
+
isWrapper: false,
|
|
998
|
+
bareStateId: null,
|
|
999
|
+
frameId: null,
|
|
974
1000
|
transitions: [],
|
|
975
1001
|
overriddenHaltStateId: null,
|
|
976
1002
|
};
|
|
977
1003
|
}
|
|
978
1004
|
continue;
|
|
979
1005
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
const haltMarkerId = haltMarkerIdFor(wrapperContext);
|
|
988
|
-
const overrideTarget = __classPrivateFieldGet$1(wrapperContext, _State_overriddenHaltState, "f");
|
|
989
|
-
// The override target's collapsed id: if the override is itself a
|
|
990
|
-
// wrapper, its graph node id is `overrideTarget.#id` (its own wrapper
|
|
991
|
-
// id); otherwise its own bare id.
|
|
992
|
-
const overrideGraphId = __classPrivateFieldGet$1(overrideTarget, _State_overriddenHaltState, "f")
|
|
993
|
-
? wrapperGraphId(overrideTarget)
|
|
994
|
-
: __classPrivateFieldGet$1(overrideTarget, _State_id, "f");
|
|
995
|
-
// Emit the halt-marker node if not already present (one per wrapper).
|
|
996
|
-
if (!(haltMarkerId in nodes)) {
|
|
997
|
-
nodes[haltMarkerId] = {
|
|
998
|
-
id: haltMarkerId,
|
|
999
|
-
name: 'halt',
|
|
1000
|
-
isHalt: true,
|
|
1001
|
-
isHaltMarker: true,
|
|
1002
|
-
isWrapped: false,
|
|
1003
|
-
transitions: [],
|
|
1004
|
-
overriddenHaltStateId: null,
|
|
1005
|
-
};
|
|
1006
|
-
}
|
|
1007
|
-
// Build the collapsed node.
|
|
1008
|
-
const collapsedNode = {
|
|
1009
|
-
id: collapsedId,
|
|
1010
|
-
name: __classPrivateFieldGet$1(state, _State_name, "f"),
|
|
1006
|
+
// Wrapper? Emit wrapper node + queue bare and override target.
|
|
1007
|
+
if (__classPrivateFieldGet$1(state, _State_overriddenHaltState, "f") !== null && __classPrivateFieldGet$1(state, _State_bareState, "f") !== null) {
|
|
1008
|
+
const bareState = __classPrivateFieldGet$1(state, _State_bareState, "f");
|
|
1009
|
+
const overrideTarget = __classPrivateFieldGet$1(state, _State_overriddenHaltState, "f");
|
|
1010
|
+
nodes[__classPrivateFieldGet$1(state, _State_id, "f")] = {
|
|
1011
|
+
id: __classPrivateFieldGet$1(state, _State_id, "f"),
|
|
1012
|
+
name: __classPrivateFieldGet$1(state, _State_name, "f"), // composite name like "A(target)"
|
|
1011
1013
|
isHalt: false,
|
|
1012
1014
|
isHaltMarker: false,
|
|
1013
|
-
|
|
1015
|
+
isWrapper: true,
|
|
1016
|
+
bareStateId: __classPrivateFieldGet$1(bareState, _State_id, "f"),
|
|
1017
|
+
frameId: null,
|
|
1014
1018
|
transitions: [],
|
|
1015
|
-
overriddenHaltStateId:
|
|
1019
|
+
overriddenHaltStateId: __classPrivateFieldGet$1(overrideTarget, _State_id, "f"),
|
|
1016
1020
|
};
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
let target;
|
|
1021
|
-
try {
|
|
1022
|
-
target = nextState instanceof State ? nextState : nextState.ref;
|
|
1023
|
-
}
|
|
1024
|
-
catch {
|
|
1025
|
-
patternIx += 1;
|
|
1026
|
-
continue;
|
|
1027
|
-
}
|
|
1028
|
-
// Retarget transitions per Variant X conventions:
|
|
1029
|
-
// - target == haltState → halt marker (stays inside the subgraph)
|
|
1030
|
-
// - target == bare (self-loop) → the collapsed wrapper id
|
|
1031
|
-
// - target is itself a wrapper → that wrapper's collapsed id
|
|
1032
|
-
// - else → target's own id
|
|
1033
|
-
let nextStateId;
|
|
1034
|
-
if (target.isHalt) {
|
|
1035
|
-
nextStateId = haltMarkerId;
|
|
1036
|
-
}
|
|
1037
|
-
else if (target === state) {
|
|
1038
|
-
nextStateId = collapsedId;
|
|
1039
|
-
}
|
|
1040
|
-
else if (__classPrivateFieldGet$1(target, _State_overriddenHaltState, "f") && __classPrivateFieldGet$1(target, _State_bareState, "f")) {
|
|
1041
|
-
nextStateId = wrapperGraphId(target);
|
|
1042
|
-
queue.push({ state: __classPrivateFieldGet$1(target, _State_bareState, "f"), wrapperContext: target });
|
|
1043
|
-
}
|
|
1044
|
-
else {
|
|
1045
|
-
nextStateId = __classPrivateFieldGet$1(target, _State_id, "f");
|
|
1046
|
-
queue.push({ state: target, wrapperContext: null });
|
|
1047
|
-
}
|
|
1048
|
-
collapsedNode.transitions.push({
|
|
1049
|
-
pattern: decodePatternDescription(sym.description, alphabets),
|
|
1050
|
-
command: command.tapesCommands.map((tc) => ({
|
|
1051
|
-
symbol: decodeWriteSymbol(tc.symbol),
|
|
1052
|
-
movement: decodeMovement(tc.movement.description),
|
|
1053
|
-
})),
|
|
1054
|
-
nextStateId,
|
|
1055
|
-
id: `${collapsedId}-${patternIx}`,
|
|
1056
|
-
});
|
|
1057
|
-
patternIx += 1;
|
|
1058
|
-
}
|
|
1059
|
-
// Enqueue the override target so its own node is emitted.
|
|
1060
|
-
if (__classPrivateFieldGet$1(overrideTarget, _State_overriddenHaltState, "f") && __classPrivateFieldGet$1(overrideTarget, _State_bareState, "f")) {
|
|
1061
|
-
queue.push({ state: __classPrivateFieldGet$1(overrideTarget, _State_bareState, "f"), wrapperContext: overrideTarget });
|
|
1062
|
-
}
|
|
1063
|
-
else {
|
|
1064
|
-
queue.push({ state: overrideTarget, wrapperContext: null });
|
|
1065
|
-
}
|
|
1066
|
-
continue;
|
|
1067
|
-
}
|
|
1068
|
-
// Non-wrapper context: emit `state` as a regular node.
|
|
1069
|
-
if (__classPrivateFieldGet$1(state, _State_id, "f") in nodes) {
|
|
1021
|
+
bareIds.add(__classPrivateFieldGet$1(bareState, _State_id, "f"));
|
|
1022
|
+
queue.push(bareState);
|
|
1023
|
+
queue.push(overrideTarget);
|
|
1070
1024
|
continue;
|
|
1071
1025
|
}
|
|
1026
|
+
// Regular (or bare) state — build node with transitions.
|
|
1072
1027
|
const node = {
|
|
1073
1028
|
id: __classPrivateFieldGet$1(state, _State_id, "f"),
|
|
1074
1029
|
name: __classPrivateFieldGet$1(state, _State_name, "f"),
|
|
1075
1030
|
isHalt: false,
|
|
1076
1031
|
isHaltMarker: false,
|
|
1077
|
-
|
|
1032
|
+
isWrapper: false,
|
|
1033
|
+
bareStateId: null,
|
|
1034
|
+
frameId: null,
|
|
1078
1035
|
transitions: [],
|
|
1079
1036
|
overriddenHaltStateId: null,
|
|
1080
1037
|
};
|
|
@@ -1083,49 +1040,185 @@ class State {
|
|
|
1083
1040
|
for (const [sym, { command, nextState }] of __classPrivateFieldGet$1(state, _State_symbolToDataMap, "f")) {
|
|
1084
1041
|
let target;
|
|
1085
1042
|
try {
|
|
1086
|
-
target = nextState instanceof
|
|
1043
|
+
target = nextState instanceof _a ? nextState : nextState.ref;
|
|
1087
1044
|
}
|
|
1088
1045
|
catch {
|
|
1089
1046
|
patternIx += 1;
|
|
1090
1047
|
continue;
|
|
1091
1048
|
}
|
|
1092
|
-
let nextStateId;
|
|
1093
|
-
if (__classPrivateFieldGet$1(target, _State_overriddenHaltState, "f") && __classPrivateFieldGet$1(target, _State_bareState, "f")) {
|
|
1094
|
-
// Transition into a wrapper — use its collapsed id.
|
|
1095
|
-
nextStateId = wrapperGraphId(target);
|
|
1096
|
-
queue.push({ state: __classPrivateFieldGet$1(target, _State_bareState, "f"), wrapperContext: target });
|
|
1097
|
-
}
|
|
1098
|
-
else {
|
|
1099
|
-
nextStateId = __classPrivateFieldGet$1(target, _State_id, "f");
|
|
1100
|
-
queue.push({ state: target, wrapperContext: null });
|
|
1101
|
-
}
|
|
1102
1049
|
node.transitions.push({
|
|
1103
1050
|
pattern: decodePatternDescription(sym.description, alphabets),
|
|
1104
1051
|
command: command.tapesCommands.map((tc) => ({
|
|
1105
1052
|
symbol: decodeWriteSymbol(tc.symbol),
|
|
1106
1053
|
movement: decodeMovement(tc.movement.description),
|
|
1107
1054
|
})),
|
|
1108
|
-
nextStateId,
|
|
1055
|
+
nextStateId: __classPrivateFieldGet$1(target, _State_id, "f"),
|
|
1109
1056
|
id: `${__classPrivateFieldGet$1(state, _State_id, "f")}-${patternIx}`,
|
|
1110
1057
|
});
|
|
1058
|
+
queue.push(target);
|
|
1111
1059
|
patternIx += 1;
|
|
1112
1060
|
}
|
|
1113
1061
|
}
|
|
1114
|
-
|
|
1062
|
+
// Always emit real halt as a sentinel, even if no transition targets it.
|
|
1063
|
+
// It anchors the `subtree -. halt .-> s0` frame-level arrow whenever a
|
|
1064
|
+
// frame demand-emits one, and it's the canonical machine-halt singleton.
|
|
1065
|
+
if (!(0 in nodes)) {
|
|
1066
|
+
nodes[0] = {
|
|
1067
|
+
id: 0,
|
|
1068
|
+
name: 'halt',
|
|
1069
|
+
isHalt: true,
|
|
1070
|
+
isHaltMarker: false,
|
|
1071
|
+
isWrapper: false,
|
|
1072
|
+
bareStateId: null,
|
|
1073
|
+
frameId: null,
|
|
1074
|
+
transitions: [],
|
|
1075
|
+
overriddenHaltStateId: null,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
// Pass 2: For each bare, compute its forward-reachable set (following
|
|
1079
|
+
// transitions; stopping at halt and at wrappers — both are frame
|
|
1080
|
+
// boundaries).
|
|
1081
|
+
const computeReach = (startId) => {
|
|
1082
|
+
const reach = new Set();
|
|
1083
|
+
const stack = [startId];
|
|
1084
|
+
while (stack.length > 0) {
|
|
1085
|
+
const id = stack.pop();
|
|
1086
|
+
if (reach.has(id)) {
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
const node = nodes[id];
|
|
1090
|
+
if (!node || node.isHalt || node.isWrapper) {
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
reach.add(id);
|
|
1094
|
+
for (const t of node.transitions) {
|
|
1095
|
+
const target = nodes[t.nextStateId];
|
|
1096
|
+
if (!target || target.isHalt || target.isWrapper) {
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
stack.push(t.nextStateId);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return reach;
|
|
1103
|
+
};
|
|
1104
|
+
const reachByBare = new Map();
|
|
1105
|
+
for (const bareId of bareIds) {
|
|
1106
|
+
reachByBare.set(bareId, computeReach(bareId));
|
|
1107
|
+
}
|
|
1108
|
+
// Pass 3: Union-find on bare overlaps. Two bares merge if their reach
|
|
1109
|
+
// sets share any state. Canonical representative = smallest bare-id in
|
|
1110
|
+
// the component.
|
|
1111
|
+
const ufParent = new Map();
|
|
1112
|
+
const ufFind = (id) => {
|
|
1113
|
+
if (!ufParent.has(id)) {
|
|
1114
|
+
ufParent.set(id, id);
|
|
1115
|
+
}
|
|
1116
|
+
let root = id;
|
|
1117
|
+
while (ufParent.get(root) !== root) {
|
|
1118
|
+
root = ufParent.get(root);
|
|
1119
|
+
}
|
|
1120
|
+
// Path compression
|
|
1121
|
+
let cur = id;
|
|
1122
|
+
while (ufParent.get(cur) !== root) {
|
|
1123
|
+
const next = ufParent.get(cur);
|
|
1124
|
+
ufParent.set(cur, root);
|
|
1125
|
+
cur = next;
|
|
1126
|
+
}
|
|
1127
|
+
return root;
|
|
1128
|
+
};
|
|
1129
|
+
const ufUnion = (a, b) => {
|
|
1130
|
+
const ra = ufFind(a);
|
|
1131
|
+
const rb = ufFind(b);
|
|
1132
|
+
if (ra === rb)
|
|
1133
|
+
return;
|
|
1134
|
+
if (ra < rb) {
|
|
1135
|
+
ufParent.set(rb, ra);
|
|
1136
|
+
}
|
|
1137
|
+
else {
|
|
1138
|
+
ufParent.set(ra, rb);
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
for (const bareId of bareIds) {
|
|
1142
|
+
ufFind(bareId);
|
|
1143
|
+
}
|
|
1144
|
+
// For each state, collect the bares that reach it; union all bares that
|
|
1145
|
+
// share a state.
|
|
1146
|
+
const stateToReachingBares = new Map();
|
|
1147
|
+
for (const [bareId, reachSet] of reachByBare) {
|
|
1148
|
+
for (const stateId of reachSet) {
|
|
1149
|
+
let bares = stateToReachingBares.get(stateId);
|
|
1150
|
+
if (!bares) {
|
|
1151
|
+
bares = [];
|
|
1152
|
+
stateToReachingBares.set(stateId, bares);
|
|
1153
|
+
}
|
|
1154
|
+
bares.push(bareId);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
for (const bares of stateToReachingBares.values()) {
|
|
1158
|
+
for (let i = 1; i < bares.length; i += 1) {
|
|
1159
|
+
ufUnion(bares[0], bares[i]);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
// Assign frameId to each in-reach state.
|
|
1163
|
+
const frameIds = new Set();
|
|
1164
|
+
for (const [stateId, bares] of stateToReachingBares) {
|
|
1165
|
+
const frameId = ufFind(bares[0]);
|
|
1166
|
+
nodes[stateId].frameId = frameId;
|
|
1167
|
+
frameIds.add(frameId);
|
|
1168
|
+
}
|
|
1169
|
+
// Pass 4: Retarget halt-bound transitions for in-frame states to the
|
|
1170
|
+
// frame's halt marker. Out-of-frame states (top-level dispatcher, override
|
|
1171
|
+
// targets, etc.) keep their halt-bound transitions pointing at real halt.
|
|
1172
|
+
for (const node of Object.values(nodes)) {
|
|
1173
|
+
if (node.frameId === null) {
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
const haltMarkerId = -node.frameId;
|
|
1177
|
+
for (const t of node.transitions) {
|
|
1178
|
+
const target = nodes[t.nextStateId];
|
|
1179
|
+
if (target && target.isHalt && !target.isHaltMarker) {
|
|
1180
|
+
t.nextStateId = haltMarkerId;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
// Pass 5: Emit one halt marker per frame.
|
|
1185
|
+
for (const frameId of frameIds) {
|
|
1186
|
+
const haltMarkerId = -frameId;
|
|
1187
|
+
nodes[haltMarkerId] = {
|
|
1188
|
+
id: haltMarkerId,
|
|
1189
|
+
name: 'halt',
|
|
1190
|
+
isHalt: true,
|
|
1191
|
+
isHaltMarker: true,
|
|
1192
|
+
isWrapper: false,
|
|
1193
|
+
bareStateId: null,
|
|
1194
|
+
frameId,
|
|
1195
|
+
transitions: [],
|
|
1196
|
+
overriddenHaltStateId: null,
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
return { initialId: __classPrivateFieldGet$1(initialState, _State_id, "f"), alphabets, nodes };
|
|
1115
1200
|
}
|
|
1116
1201
|
// Inverse of toGraph: rebuilds a State graph (and a fresh TapeBlock with the
|
|
1117
1202
|
// graph's alphabets) from a serialized Graph. Round-trips with toGraph in
|
|
1118
1203
|
// the sense that running the rebuilt machine on the same input gives the
|
|
1119
1204
|
// same output, but the rebuilt State instances have *new* internal IDs.
|
|
1205
|
+
//
|
|
1206
|
+
// Under the v7 callable-subtree model (#174), graph nodes split into:
|
|
1207
|
+
// - Wrapper nodes (`isWrapper: true`, no transitions) — reconstructed via
|
|
1208
|
+
// `bareStates[bareStateId].withOverriddenHaltState(finalStates[overriddenHaltStateId])`.
|
|
1209
|
+
// - Bare/regular nodes — constructed as normal States with transitions.
|
|
1210
|
+
// - Halt + halt-marker nodes — collapse to the singleton `haltState`.
|
|
1120
1211
|
static fromGraph(graph) {
|
|
1121
1212
|
const alphabetObjs = graph.alphabets.map((syms) => new Alphabet(syms));
|
|
1122
1213
|
const tapeBlock = TapeBlock.fromAlphabets(alphabetObjs);
|
|
1123
1214
|
const ids = Object.keys(graph.nodes).map(Number);
|
|
1124
|
-
// Pass 1: pre-create a Reference for each non-halt node
|
|
1125
|
-
//
|
|
1215
|
+
// Pass 1: pre-create a Reference for each non-halt non-halt-marker node
|
|
1216
|
+
// (both wrappers and regulars). Halt and halt-marker nodes collapse to the
|
|
1217
|
+
// singleton `haltState` and need no ref.
|
|
1126
1218
|
const refs = {};
|
|
1127
1219
|
for (const nodeId of ids) {
|
|
1128
|
-
|
|
1220
|
+
const node = graph.nodes[nodeId];
|
|
1221
|
+
if (!node.isHalt) {
|
|
1129
1222
|
refs[nodeId] = new Reference();
|
|
1130
1223
|
}
|
|
1131
1224
|
}
|
|
@@ -1142,19 +1235,22 @@ class State {
|
|
|
1142
1235
|
}
|
|
1143
1236
|
return tapeBlock.symbol(flat);
|
|
1144
1237
|
};
|
|
1145
|
-
// Pass 2: build a
|
|
1146
|
-
//
|
|
1238
|
+
// Pass 2: build a State for each non-wrapper non-halt non-halt-marker
|
|
1239
|
+
// node. Transitions point at refs so cycles work; haltState (and halt
|
|
1240
|
+
// markers, which collapse to haltState) are used directly.
|
|
1147
1241
|
const bareStates = {};
|
|
1148
1242
|
for (const nodeId of ids) {
|
|
1149
1243
|
const node = graph.nodes[nodeId];
|
|
1150
|
-
if (node.isHalt) {
|
|
1244
|
+
if (node.isHalt || node.isWrapper) {
|
|
1151
1245
|
continue;
|
|
1152
1246
|
}
|
|
1153
1247
|
const stateDefinition = {};
|
|
1154
1248
|
for (const t of node.transitions) {
|
|
1155
1249
|
const key = patternToKey(parsePatternString(t.pattern, graph.alphabets));
|
|
1156
1250
|
const target = graph.nodes[t.nextStateId];
|
|
1157
|
-
const nextState = target
|
|
1251
|
+
const nextState = !target || target.isHalt
|
|
1252
|
+
? haltState
|
|
1253
|
+
: refs[t.nextStateId];
|
|
1158
1254
|
stateDefinition[key] = {
|
|
1159
1255
|
command: t.command.map((c) => ({
|
|
1160
1256
|
symbol: parseWriteSymbolLabel(c.symbol),
|
|
@@ -1163,14 +1259,17 @@ class State {
|
|
|
1163
1259
|
nextState,
|
|
1164
1260
|
};
|
|
1165
1261
|
}
|
|
1166
|
-
// Graph-sourced names may contain `(` and `)` (composite wrapper names
|
|
1167
|
-
//
|
|
1168
|
-
//
|
|
1169
|
-
|
|
1262
|
+
// Graph-sourced names may contain `(` and `)` (composite wrapper names —
|
|
1263
|
+
// although wrappers go through a separate path below, defensive
|
|
1264
|
+
// construction here keeps the bypass uniform). Construct without a name
|
|
1265
|
+
// and assign `#name` directly to skip user-facing name validation.
|
|
1266
|
+
const bare = new _a(stateDefinition);
|
|
1170
1267
|
__classPrivateFieldSet$1(bare, _State_name, node.name, "f");
|
|
1171
1268
|
bareStates[nodeId] = bare;
|
|
1172
1269
|
}
|
|
1173
|
-
// Pass 3:
|
|
1270
|
+
// Pass 3: resolve every node to its final State (memoized + cycle-safe).
|
|
1271
|
+
// Wrappers compose lazily via `withOverriddenHaltState` once their bare
|
|
1272
|
+
// and override are resolved.
|
|
1174
1273
|
const finalStates = {};
|
|
1175
1274
|
const inProgress = new Set();
|
|
1176
1275
|
const getFinal = (nodeId) => {
|
|
@@ -1178,7 +1277,7 @@ class State {
|
|
|
1178
1277
|
return finalStates[nodeId];
|
|
1179
1278
|
}
|
|
1180
1279
|
const node = graph.nodes[nodeId];
|
|
1181
|
-
if (node.isHalt) {
|
|
1280
|
+
if (!node || node.isHalt) {
|
|
1182
1281
|
finalStates[nodeId] = haltState;
|
|
1183
1282
|
return haltState;
|
|
1184
1283
|
}
|
|
@@ -1186,9 +1285,14 @@ class State {
|
|
|
1186
1285
|
throw new Error(`override-halt cycle at state #${nodeId}`);
|
|
1187
1286
|
}
|
|
1188
1287
|
inProgress.add(nodeId);
|
|
1189
|
-
let state
|
|
1190
|
-
if (node.
|
|
1191
|
-
|
|
1288
|
+
let state;
|
|
1289
|
+
if (node.isWrapper) {
|
|
1290
|
+
const bare = getFinal(node.bareStateId);
|
|
1291
|
+
const override = getFinal(node.overriddenHaltStateId);
|
|
1292
|
+
state = bare.withOverriddenHaltState(override);
|
|
1293
|
+
}
|
|
1294
|
+
else {
|
|
1295
|
+
state = bareStates[nodeId];
|
|
1192
1296
|
}
|
|
1193
1297
|
inProgress.delete(nodeId);
|
|
1194
1298
|
finalStates[nodeId] = state;
|
|
@@ -1197,8 +1301,8 @@ class State {
|
|
|
1197
1301
|
for (const nodeId of ids) {
|
|
1198
1302
|
getFinal(nodeId);
|
|
1199
1303
|
}
|
|
1200
|
-
// Pass 4: bind each ref to the
|
|
1201
|
-
//
|
|
1304
|
+
// Pass 4: bind each ref to the resolved final State so cross-node
|
|
1305
|
+
// transitions land on the right instance.
|
|
1202
1306
|
for (const nodeId of ids) {
|
|
1203
1307
|
if (!graph.nodes[nodeId].isHalt) {
|
|
1204
1308
|
refs[nodeId].bind(finalStates[nodeId]);
|
|
@@ -1211,6 +1315,13 @@ class State {
|
|
|
1211
1315
|
};
|
|
1212
1316
|
}
|
|
1213
1317
|
}
|
|
1318
|
+
_a = State;
|
|
1319
|
+
// Memoization cache for `withOverriddenHaltState`. Keyed by
|
|
1320
|
+
// (bare, override) — same args return the same wrapper instance (#175).
|
|
1321
|
+
// Two-level WeakMap so the outer entry is GC'd when the bare is collected;
|
|
1322
|
+
// WeakRef values let wrappers themselves be GC'd when nothing else holds
|
|
1323
|
+
// them, with cache misses simply reconstructing fresh wrappers.
|
|
1324
|
+
_State_wrapperCache = { value: new WeakMap() };
|
|
1214
1325
|
const haltState = new State(null);
|
|
1215
1326
|
|
|
1216
1327
|
var __classPrivateFieldSet = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
@@ -1354,19 +1465,25 @@ _TuringMachine_tapeBlock = new WeakMap(), _TuringMachine_stack = new WeakMap();
|
|
|
1354
1465
|
// Currently only Mermaid flowchart syntax is supported. Future formats
|
|
1355
1466
|
// (Graphviz, JSON-LD, custom DSL) belong here too.
|
|
1356
1467
|
//
|
|
1357
|
-
// v7 emit
|
|
1358
|
-
// - Each wrapper
|
|
1359
|
-
//
|
|
1360
|
-
// (
|
|
1361
|
-
//
|
|
1362
|
-
//
|
|
1363
|
-
//
|
|
1364
|
-
//
|
|
1365
|
-
//
|
|
1366
|
-
// -
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1369
|
-
//
|
|
1468
|
+
// v7 callable-subtree emit (#174):
|
|
1469
|
+
// - Each `withOverriddenHaltState` wrapper produces TWO graph nodes — a
|
|
1470
|
+
// wrapper node (`[[composite-name]]`, OUTSIDE any subgraph) and a bare
|
|
1471
|
+
// node (regular shape, INSIDE its callable subtree subgraph).
|
|
1472
|
+
// - Subgraphs (one per frame): `subgraph w_${frameId}["callable subtree
|
|
1473
|
+
// of NAME"]` (single bare) or `["callable scope: A ∪ B"]` (union).
|
|
1474
|
+
// - Each frame has exactly one halt marker `c${frameId}(((halt)))` inside
|
|
1475
|
+
// its subgraph; halt-bound transitions from in-frame states retarget to
|
|
1476
|
+
// it. Always emitted (orphan signals dead wrapper).
|
|
1477
|
+
// - Arrow conventions:
|
|
1478
|
+
// solid `-->` regular transitions, including wrapper-to-override.
|
|
1479
|
+
// bold `==>` RESERVED for the wrapper-to-bare `call` arrow.
|
|
1480
|
+
// `&` ribbon collapses multi-wrapper-shares-bare.
|
|
1481
|
+
// dotted `-.->` frame-level dispatch (`return`, `halt`, `enter`).
|
|
1482
|
+
// - The `return` arrow (subgraph → wrapper) is demand-emitted iff the
|
|
1483
|
+
// frame's halt marker has at least one incoming edge AND the wrapper
|
|
1484
|
+
// calls into the frame. The `halt` arrow (subgraph → s0) is emitted
|
|
1485
|
+
// iff the halt marker has incoming AND there's at least one non-wrapper
|
|
1486
|
+
// entry into the frame (cross-subgraph solid arrow from outside).
|
|
1370
1487
|
// Maps a graph node id to its Mermaid id.
|
|
1371
1488
|
// - non-negative id N → "sN"
|
|
1372
1489
|
// - negative id -N (halt marker) → "cN"
|
|
@@ -1380,108 +1497,182 @@ function parseMermaidId(s) {
|
|
|
1380
1497
|
}
|
|
1381
1498
|
return Number(s.slice(1));
|
|
1382
1499
|
}
|
|
1500
|
+
function frameSubgraphId(frameId) {
|
|
1501
|
+
return `w_${frameId}`;
|
|
1502
|
+
}
|
|
1383
1503
|
function toMermaid(graph) {
|
|
1384
1504
|
const lines = [
|
|
1385
1505
|
'flowchart TD',
|
|
1386
1506
|
`%% alphabets: ${JSON.stringify(graph.alphabets)}`,
|
|
1387
1507
|
];
|
|
1388
|
-
// Sort nodes by id
|
|
1389
|
-
//
|
|
1390
|
-
//
|
|
1508
|
+
// Sort nodes by id ascending — real halt (0) first, then regulars by their
|
|
1509
|
+
// ids, then halt markers (negative) at the end. Deterministic emit lets
|
|
1510
|
+
// toMermaid → fromMermaid → toMermaid round-trip stably (#139).
|
|
1391
1511
|
const nodes = Object.values(graph.nodes).slice().sort((a, b) => a.id - b.id);
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
const
|
|
1395
|
-
//
|
|
1396
|
-
const
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
haltMarkerIds.add(haltMarkerId);
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
// Emit non-subgraph nodes first: real halt + regular non-wrapped nodes.
|
|
1404
|
-
// No special round-shape `((…))` for the initial — the `idle -. enter .->`
|
|
1405
|
-
// arrow emitted below is the sole "start here" signal.
|
|
1512
|
+
// Bucket nodes for emit order.
|
|
1513
|
+
const topLevelNodes = nodes.filter((n) => n.frameId === null && !n.isWrapper);
|
|
1514
|
+
const wrapperNodes = nodes.filter((n) => n.isWrapper);
|
|
1515
|
+
// Bares-and-bodies inside frames, grouped by frameId.
|
|
1516
|
+
const nodesByFrame = new Map();
|
|
1517
|
+
// Halt-marker per frame (kept separate so it always emits LAST inside the
|
|
1518
|
+
// subgraph for deterministic shape).
|
|
1519
|
+
const haltMarkerByFrame = new Map();
|
|
1406
1520
|
for (const node of nodes) {
|
|
1407
|
-
if (node.
|
|
1521
|
+
if (node.frameId === null || node.isWrapper)
|
|
1408
1522
|
continue;
|
|
1523
|
+
if (node.isHaltMarker) {
|
|
1524
|
+
haltMarkerByFrame.set(node.frameId, node);
|
|
1525
|
+
}
|
|
1526
|
+
else {
|
|
1527
|
+
let bucket = nodesByFrame.get(node.frameId);
|
|
1528
|
+
if (!bucket) {
|
|
1529
|
+
bucket = [];
|
|
1530
|
+
nodesByFrame.set(node.frameId, bucket);
|
|
1531
|
+
}
|
|
1532
|
+
bucket.push(node);
|
|
1409
1533
|
}
|
|
1410
|
-
|
|
1534
|
+
}
|
|
1535
|
+
// 1. Emit top-level nodes (real halt, non-wrapper regulars outside any frame).
|
|
1536
|
+
for (const node of topLevelNodes) {
|
|
1537
|
+
const mid = mermaidIdFor(node.id);
|
|
1411
1538
|
if (node.isHalt) {
|
|
1412
|
-
lines.push(` ${
|
|
1539
|
+
lines.push(` ${mid}(((halt)))`);
|
|
1413
1540
|
}
|
|
1414
1541
|
else {
|
|
1415
|
-
lines.push(` ${
|
|
1542
|
+
lines.push(` ${mid}["${node.name}"]`);
|
|
1416
1543
|
}
|
|
1417
1544
|
}
|
|
1418
|
-
//
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1545
|
+
// 2. Emit wrappers at top level.
|
|
1546
|
+
for (const wrapper of wrapperNodes) {
|
|
1547
|
+
lines.push(` ${mermaidIdFor(wrapper.id)}[["${wrapper.name}"]]`);
|
|
1548
|
+
}
|
|
1549
|
+
// 3. `idle` sentinel.
|
|
1422
1550
|
lines.push(' idle([idle])');
|
|
1423
|
-
//
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
const
|
|
1427
|
-
const
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1551
|
+
// 4. Subgraph per frame.
|
|
1552
|
+
const frameIds = [...nodesByFrame.keys()].sort((a, b) => a - b);
|
|
1553
|
+
for (const frameId of frameIds) {
|
|
1554
|
+
const frameBares = (nodesByFrame.get(frameId) ?? []).filter((n) => isFrameBare(n, graph));
|
|
1555
|
+
const frameBareNames = frameBares
|
|
1556
|
+
.slice()
|
|
1557
|
+
.sort((a, b) => a.id - b.id)
|
|
1558
|
+
.map((n) => n.name);
|
|
1559
|
+
const label = frameBareNames.length > 1
|
|
1560
|
+
? `callable scope: ${frameBareNames.join(' ∪ ')}`
|
|
1561
|
+
: `callable subtree of ${frameBareNames[0] ?? frameId}`;
|
|
1562
|
+
lines.push(` subgraph ${frameSubgraphId(frameId)}["${label}"]`);
|
|
1563
|
+
// Inner nodes — sort by id for determinism.
|
|
1564
|
+
for (const node of (nodesByFrame.get(frameId) ?? []).slice().sort((a, b) => a.id - b.id)) {
|
|
1565
|
+
lines.push(` ${mermaidIdFor(node.id)}["${node.name}"]`);
|
|
1566
|
+
}
|
|
1567
|
+
const haltMarker = haltMarkerByFrame.get(frameId);
|
|
1568
|
+
if (haltMarker) {
|
|
1569
|
+
lines.push(` ${mermaidIdFor(haltMarker.id)}(((halt)))`);
|
|
1432
1570
|
}
|
|
1433
1571
|
lines.push(' end');
|
|
1434
1572
|
}
|
|
1435
|
-
// Enter arrow
|
|
1436
|
-
// node (whether plain `[…]` or wrapped `[[…]]` inside a subgraph).
|
|
1573
|
+
// 5. Enter arrow.
|
|
1437
1574
|
lines.push(` idle -. enter .-> ${mermaidIdFor(graph.initialId)}`);
|
|
1438
|
-
//
|
|
1439
|
-
//
|
|
1440
|
-
|
|
1575
|
+
// 6. `call` arrows — grouped by bare (multi-wrapper-shares-bare collapses
|
|
1576
|
+
// into a single `&` ribbon).
|
|
1577
|
+
const wrappersByBare = new Map();
|
|
1578
|
+
for (const wrapper of wrapperNodes) {
|
|
1579
|
+
if (wrapper.bareStateId === null)
|
|
1580
|
+
continue;
|
|
1581
|
+
let group = wrappersByBare.get(wrapper.bareStateId);
|
|
1582
|
+
if (!group) {
|
|
1583
|
+
group = [];
|
|
1584
|
+
wrappersByBare.set(wrapper.bareStateId, group);
|
|
1585
|
+
}
|
|
1586
|
+
group.push(wrapper);
|
|
1587
|
+
}
|
|
1588
|
+
const sortedBares = [...wrappersByBare.keys()].sort((a, b) => a - b);
|
|
1589
|
+
for (const bareId of sortedBares) {
|
|
1590
|
+
const wrappers = wrappersByBare.get(bareId).slice().sort((a, b) => a.id - b.id);
|
|
1591
|
+
const sources = wrappers.map((w) => mermaidIdFor(w.id)).join(' & ');
|
|
1592
|
+
lines.push(` ${sources} == "call" ==> ${mermaidIdFor(bareId)}`);
|
|
1593
|
+
}
|
|
1594
|
+
// 7. Demand-emit `return` and `halt` arrows per frame.
|
|
1595
|
+
// For each frame: check if its halt marker has incoming transitions.
|
|
1596
|
+
const haltMarkerHasIncoming = new Map();
|
|
1441
1597
|
for (const node of nodes) {
|
|
1442
|
-
|
|
1598
|
+
for (const t of node.transitions) {
|
|
1599
|
+
const target = graph.nodes[t.nextStateId];
|
|
1600
|
+
if (target && target.isHaltMarker && target.frameId !== null) {
|
|
1601
|
+
haltMarkerHasIncoming.set(target.frameId, true);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
// For each frame: check if there's at least one non-wrapper entry (a solid
|
|
1606
|
+
// `-->` from OUTSIDE the frame into any node INSIDE).
|
|
1607
|
+
const hasNonWrapperEntry = new Map();
|
|
1608
|
+
for (const node of nodes) {
|
|
1609
|
+
if (node.isWrapper)
|
|
1443
1610
|
continue;
|
|
1611
|
+
for (const t of node.transitions) {
|
|
1612
|
+
const target = graph.nodes[t.nextStateId];
|
|
1613
|
+
if (target
|
|
1614
|
+
&& target.frameId !== null
|
|
1615
|
+
&& node.frameId !== target.frameId) {
|
|
1616
|
+
hasNonWrapperEntry.set(target.frameId, true);
|
|
1617
|
+
}
|
|
1444
1618
|
}
|
|
1619
|
+
}
|
|
1620
|
+
for (const frameId of frameIds) {
|
|
1621
|
+
if (!haltMarkerHasIncoming.get(frameId))
|
|
1622
|
+
continue;
|
|
1623
|
+
// Return arrow — collapsed `&` ribbon over all wrappers calling this frame.
|
|
1624
|
+
const callingWrappers = wrapperNodes.filter((w) => {
|
|
1625
|
+
if (w.bareStateId === null)
|
|
1626
|
+
return false;
|
|
1627
|
+
const bare = graph.nodes[w.bareStateId];
|
|
1628
|
+
return !!bare && bare.frameId === frameId;
|
|
1629
|
+
});
|
|
1630
|
+
if (callingWrappers.length > 0) {
|
|
1631
|
+
const targets = callingWrappers
|
|
1632
|
+
.slice()
|
|
1633
|
+
.sort((a, b) => a.id - b.id)
|
|
1634
|
+
.map((w) => mermaidIdFor(w.id))
|
|
1635
|
+
.join(' & ');
|
|
1636
|
+
lines.push(` ${frameSubgraphId(frameId)} -. "return" .-> ${targets}`);
|
|
1637
|
+
}
|
|
1638
|
+
if (hasNonWrapperEntry.get(frameId)) {
|
|
1639
|
+
lines.push(` ${frameSubgraphId(frameId)} -. "halt" .-> s0`);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
// 8. Wrapper-to-override arrows (regular solid).
|
|
1643
|
+
for (const wrapper of wrapperNodes) {
|
|
1644
|
+
if (wrapper.overriddenHaltStateId === null)
|
|
1645
|
+
continue;
|
|
1646
|
+
lines.push(` ${mermaidIdFor(wrapper.id)} --> ${mermaidIdFor(wrapper.overriddenHaltStateId)}`);
|
|
1647
|
+
}
|
|
1648
|
+
// 9. Regular transitions for non-wrapper non-halt-marker non-halt nodes.
|
|
1649
|
+
for (const node of nodes) {
|
|
1650
|
+
if (node.isHalt || node.isHaltMarker || node.isWrapper)
|
|
1651
|
+
continue;
|
|
1445
1652
|
for (const t of node.transitions) {
|
|
1446
|
-
// Bracketed-tape-block format (v7): each role-list — read alternatives,
|
|
1447
|
-
// writes, movements — wraps in `[…]` to mark "this is a tape-block
|
|
1448
|
-
// reading". Brackets stay even for single-tape machines; the `[…]` is
|
|
1449
|
-
// the tape-block concept indicator.
|
|
1450
|
-
//
|
|
1451
|
-
// Single-tape: ['X'] → [K]/[R]
|
|
1452
|
-
// Single-tape + alternation: ['^']|['1']|['0'] → [K]/[S]
|
|
1453
|
-
// Two-tape: ['0','a'] → [K,'1']/[R,S]
|
|
1454
|
-
// Two-tape + alternation: ['0','a']|['1','b'] → [K,K]/[R,L]
|
|
1455
|
-
//
|
|
1456
|
-
// Alternation is ALWAYS per-pattern-bracket — one full bracketed list
|
|
1457
|
-
// per alternative — regardless of tape count. Pedagogically each
|
|
1458
|
-
// alternative is its own drawn transition; a compact in-bracket form
|
|
1459
|
-
// (`['^'|'1']`) would read as cross-product semantics in multi-tape
|
|
1460
|
-
// (`['0'|'1','a'|'b']` = 4 combos, not 2 paired alternatives), so we
|
|
1461
|
-
// avoid introducing it for the single-tape case too.
|
|
1462
1653
|
const alternatives = t.pattern.split('|');
|
|
1463
1654
|
const reads = alternatives.map((alt) => `[${alt}]`).join('|');
|
|
1464
1655
|
const writes = `[${t.command.map((c) => c.symbol).join(',')}]`;
|
|
1465
1656
|
const moves = `[${t.command.map((c) => c.movement).join(',')}]`;
|
|
1466
1657
|
const label = `${reads} → ${writes}/${moves}`;
|
|
1467
|
-
|
|
1468
|
-
// signals "this transition pushes that wrapper's override onto the
|
|
1469
|
-
// runtime stack" (per `TuringMachine.run` line ~220's
|
|
1470
|
-
// `if (state !== nextState && nextState.overriddenHaltState) push(...)`).
|
|
1471
|
-
// Self-loops (state === nextState) don't push at runtime — keep the
|
|
1472
|
-
// regular `-->` for those even when the target is wrapped.
|
|
1473
|
-
const targetNode = graph.nodes[t.nextStateId];
|
|
1474
|
-
const isEnteringWrapper = targetNode && targetNode.isWrapped && t.nextStateId !== node.id;
|
|
1475
|
-
const lineSegment = isEnteringWrapper ? '==' : '--';
|
|
1476
|
-
const arrowTip = isEnteringWrapper ? '==>' : '-->';
|
|
1477
|
-
lines.push(` ${mermaidIdFor(node.id)} ${lineSegment} "${label}" ${arrowTip} ${mermaidIdFor(t.nextStateId)}`);
|
|
1478
|
-
}
|
|
1479
|
-
if (node.overriddenHaltStateId !== null) {
|
|
1480
|
-
lines.push(` ${mermaidIdFor(node.id)} -. onHalt .-> ${mermaidIdFor(node.overriddenHaltStateId)}`);
|
|
1658
|
+
lines.push(` ${mermaidIdFor(node.id)} -- "${label}" --> ${mermaidIdFor(t.nextStateId)}`);
|
|
1481
1659
|
}
|
|
1482
1660
|
}
|
|
1483
1661
|
return lines.join('\n');
|
|
1484
1662
|
}
|
|
1663
|
+
// Helper: identify "the bare states" that anchor a frame's name. A bare is a
|
|
1664
|
+
// node referenced as some wrapper's `bareStateId`. Body states (also in-frame
|
|
1665
|
+
// but not bare) are excluded from the frame label.
|
|
1666
|
+
function isFrameBare(node, graph) {
|
|
1667
|
+
if (node.isWrapper || node.isHalt)
|
|
1668
|
+
return false;
|
|
1669
|
+
for (const other of Object.values(graph.nodes)) {
|
|
1670
|
+
if (other.isWrapper && other.bareStateId === node.id) {
|
|
1671
|
+
return true;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
return false;
|
|
1675
|
+
}
|
|
1485
1676
|
// Inverse of toMermaid: parses the Mermaid output produced by toMermaid back
|
|
1486
1677
|
// into a Graph. The parser is strict to the dialect toMermaid emits — it
|
|
1487
1678
|
// recognises the specific node/edge shapes and the leading
|
|
@@ -1496,13 +1687,23 @@ function toMermaid(graph) {
|
|
|
1496
1687
|
const haltNodeRegex = /^([sc]\d+)\(\(\(halt\)\)\)$/;
|
|
1497
1688
|
const regularNodeRegex = /^(s\d+)\["([^"]*)"\]$/;
|
|
1498
1689
|
const wrappedNodeRegex = /^(s\d+)\[\["([^"]*)"\]\]$/;
|
|
1499
|
-
const subgraphStartRegex = /^subgraph\s+w_\d
|
|
1690
|
+
const subgraphStartRegex = /^subgraph\s+w_(\d+)\["([^"]*)"\]$/;
|
|
1500
1691
|
const subgraphEndRegex = /^end$/;
|
|
1501
1692
|
const idleNodeRegex = /^idle\(\[idle\]\)$/;
|
|
1502
1693
|
const enterArrowRegex = /^idle\s+-\.\s+enter\s+\.->\s+(s\d+)$/;
|
|
1503
|
-
|
|
1504
|
-
const
|
|
1505
|
-
|
|
1694
|
+
// Regular labeled transition (solid `-->`).
|
|
1695
|
+
const labeledTransitionRegex = /^([sc]\d+)\s+--\s+"(.*)"\s+-->\s+([sc]\d+)$/;
|
|
1696
|
+
// Wrapper → override (unlabeled solid `-->`).
|
|
1697
|
+
const wrapperOverrideRegex = /^(s\d+)\s+-->\s+([sc]\d+)$/;
|
|
1698
|
+
// Call arrow (bold `==>`), with optional `&`-joined source ribbon.
|
|
1699
|
+
// Ribbon separator is fixed at " & " (single spaces around &) — toMermaid
|
|
1700
|
+
// emits exactly that form, so the parser is strict to it. The literal-space
|
|
1701
|
+
// form avoids CodeQL's polynomial-ReDoS flag on a `\s+&\s+` shape.
|
|
1702
|
+
const callArrowRegex = /^(s\d+(?: & s\d+)*)\s+==\s+"call"\s+==>\s+(s\d+)$/;
|
|
1703
|
+
// Return arrow (`w_N -. return .-> s_W` with optional `&` target ribbon).
|
|
1704
|
+
const returnArrowRegex = /^w_(\d+)\s+-\.\s+"return"\s+\.->\s+(s\d+(?: & s\d+)*)$/;
|
|
1705
|
+
// Halt arrow (`w_N -. halt .-> s0`).
|
|
1706
|
+
const haltArrowRegex = /^w_(\d+)\s+-\.\s+"halt"\s+\.->\s+s0$/;
|
|
1506
1707
|
// First capture char anchored as \S to avoid polynomial backtracking between
|
|
1507
1708
|
// the preceding \s* and a permissive (.+); see CodeQL js/polynomial-redos.
|
|
1508
1709
|
const alphabetsRegex = /^%%\s*alphabets:\s*(\S.*)$/;
|
|
@@ -1511,11 +1712,7 @@ function fromMermaid(text) {
|
|
|
1511
1712
|
let alphabets = [];
|
|
1512
1713
|
let initialId = null;
|
|
1513
1714
|
const nodes = {};
|
|
1514
|
-
|
|
1515
|
-
// marked `isHaltMarker: true` even though they share the `(((halt)))` shape
|
|
1516
|
-
// with the real halt at the top level.
|
|
1517
|
-
const haltMarkerIds = new Set();
|
|
1518
|
-
let inSubgraph = false;
|
|
1715
|
+
let currentFrameId = null;
|
|
1519
1716
|
const ensureNode = (id, opts = {}) => {
|
|
1520
1717
|
if (!nodes[id]) {
|
|
1521
1718
|
nodes[id] = {
|
|
@@ -1523,7 +1720,9 @@ function fromMermaid(text) {
|
|
|
1523
1720
|
name: opts.name ?? mermaidIdFor(id),
|
|
1524
1721
|
isHalt: opts.isHalt ?? false,
|
|
1525
1722
|
isHaltMarker: opts.isHaltMarker ?? false,
|
|
1526
|
-
|
|
1723
|
+
isWrapper: opts.isWrapper ?? false,
|
|
1724
|
+
bareStateId: opts.bareStateId ?? null,
|
|
1725
|
+
frameId: opts.frameId ?? null,
|
|
1527
1726
|
transitions: [],
|
|
1528
1727
|
overriddenHaltStateId: null,
|
|
1529
1728
|
};
|
|
@@ -1535,72 +1734,100 @@ function fromMermaid(text) {
|
|
|
1535
1734
|
nodes[id].isHalt = opts.isHalt;
|
|
1536
1735
|
if (opts.isHaltMarker !== undefined)
|
|
1537
1736
|
nodes[id].isHaltMarker = opts.isHaltMarker;
|
|
1538
|
-
if (opts.
|
|
1539
|
-
nodes[id].
|
|
1737
|
+
if (opts.isWrapper !== undefined)
|
|
1738
|
+
nodes[id].isWrapper = opts.isWrapper;
|
|
1739
|
+
if (opts.bareStateId !== undefined)
|
|
1740
|
+
nodes[id].bareStateId = opts.bareStateId;
|
|
1741
|
+
if (opts.frameId !== undefined)
|
|
1742
|
+
nodes[id].frameId = opts.frameId;
|
|
1540
1743
|
}
|
|
1541
1744
|
return nodes[id];
|
|
1542
1745
|
};
|
|
1543
|
-
// First pass:
|
|
1746
|
+
// First pass: nodes + alphabets (track subgraph context for frameId).
|
|
1544
1747
|
for (const line of lines) {
|
|
1545
|
-
if (line === 'flowchart TD')
|
|
1748
|
+
if (line === 'flowchart TD')
|
|
1546
1749
|
continue;
|
|
1547
|
-
}
|
|
1548
1750
|
const am = line.match(alphabetsRegex);
|
|
1549
1751
|
if (am) {
|
|
1550
1752
|
alphabets = JSON.parse(am[1]);
|
|
1551
1753
|
continue;
|
|
1552
1754
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1755
|
+
const sgStart = line.match(subgraphStartRegex);
|
|
1756
|
+
if (sgStart) {
|
|
1757
|
+
currentFrameId = Number(sgStart[1]);
|
|
1555
1758
|
continue;
|
|
1556
1759
|
}
|
|
1557
1760
|
if (subgraphEndRegex.test(line)) {
|
|
1558
|
-
|
|
1761
|
+
currentFrameId = null;
|
|
1559
1762
|
continue;
|
|
1560
1763
|
}
|
|
1561
|
-
|
|
1562
|
-
// node — skip declaration, parse the `idle -. enter .-> sN` arrow in the
|
|
1563
|
-
// edge pass to set initialId.
|
|
1564
|
-
if (idleNodeRegex.test(line)) {
|
|
1764
|
+
if (idleNodeRegex.test(line))
|
|
1565
1765
|
continue;
|
|
1566
|
-
}
|
|
1567
1766
|
const hm = line.match(haltNodeRegex);
|
|
1568
1767
|
if (hm) {
|
|
1569
1768
|
const id = parseMermaidId(hm[1]);
|
|
1570
|
-
const isHaltMarker =
|
|
1571
|
-
ensureNode(id, {
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1769
|
+
const isHaltMarker = currentFrameId !== null;
|
|
1770
|
+
ensureNode(id, {
|
|
1771
|
+
name: 'halt',
|
|
1772
|
+
isHalt: true,
|
|
1773
|
+
isHaltMarker,
|
|
1774
|
+
frameId: isHaltMarker ? currentFrameId : null,
|
|
1775
|
+
});
|
|
1575
1776
|
continue;
|
|
1576
1777
|
}
|
|
1577
1778
|
const wm = line.match(wrappedNodeRegex);
|
|
1578
1779
|
if (wm) {
|
|
1579
|
-
ensureNode(parseMermaidId(wm[1]), {
|
|
1780
|
+
ensureNode(parseMermaidId(wm[1]), {
|
|
1781
|
+
name: wm[2],
|
|
1782
|
+
isWrapper: true,
|
|
1783
|
+
});
|
|
1580
1784
|
continue;
|
|
1581
1785
|
}
|
|
1582
1786
|
const rm = line.match(regularNodeRegex);
|
|
1583
1787
|
if (rm) {
|
|
1584
|
-
ensureNode(parseMermaidId(rm[1]), {
|
|
1788
|
+
ensureNode(parseMermaidId(rm[1]), {
|
|
1789
|
+
name: rm[2],
|
|
1790
|
+
frameId: currentFrameId,
|
|
1791
|
+
});
|
|
1585
1792
|
continue;
|
|
1586
1793
|
}
|
|
1587
1794
|
}
|
|
1588
1795
|
// Second pass: edges.
|
|
1589
1796
|
for (const line of lines) {
|
|
1590
|
-
// `idle -. enter .-> sN`: the sole source of initialId.
|
|
1591
1797
|
const em = line.match(enterArrowRegex);
|
|
1592
1798
|
if (em) {
|
|
1593
1799
|
initialId = parseMermaidId(em[1]);
|
|
1594
1800
|
continue;
|
|
1595
1801
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1802
|
+
// Return/halt arrows are derivable from frame structure at the next
|
|
1803
|
+
// toMermaid emit; consume but don't persist as graph data.
|
|
1804
|
+
if (returnArrowRegex.test(line) || haltArrowRegex.test(line)) {
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
// `call` arrow — sets bareStateId on each source wrapper.
|
|
1808
|
+
const cm = line.match(callArrowRegex);
|
|
1809
|
+
if (cm) {
|
|
1810
|
+
const sources = cm[1].split(' & ');
|
|
1811
|
+
const bareId = parseMermaidId(cm[2]);
|
|
1812
|
+
for (const src of sources) {
|
|
1813
|
+
ensureNode(parseMermaidId(src), { isWrapper: true, bareStateId: bareId });
|
|
1814
|
+
}
|
|
1599
1815
|
continue;
|
|
1600
1816
|
}
|
|
1601
|
-
//
|
|
1602
|
-
//
|
|
1603
|
-
const
|
|
1817
|
+
// Wrapper → override (unlabeled solid `-->`). Only fires if the source
|
|
1818
|
+
// node is a known wrapper (declared as `[[…]]`).
|
|
1819
|
+
const wo = line.match(wrapperOverrideRegex);
|
|
1820
|
+
if (wo) {
|
|
1821
|
+
const fromId = parseMermaidId(wo[1]);
|
|
1822
|
+
const toId = parseMermaidId(wo[2]);
|
|
1823
|
+
if (nodes[fromId] && nodes[fromId].isWrapper) {
|
|
1824
|
+
nodes[fromId].overriddenHaltStateId = toId;
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
// Fall through — unlabeled solid from a non-wrapper is unexpected;
|
|
1828
|
+
// treated as a malformed line and ignored by the labeled-regex below.
|
|
1829
|
+
}
|
|
1830
|
+
const tm = line.match(labeledTransitionRegex);
|
|
1604
1831
|
if (tm) {
|
|
1605
1832
|
const fromId = parseMermaidId(tm[1]);
|
|
1606
1833
|
const label = tm[2];
|
|
@@ -1609,31 +1836,13 @@ function fromMermaid(text) {
|
|
|
1609
1836
|
if (arrowIx === -1) {
|
|
1610
1837
|
throw new Error(`fromMermaid: malformed edge label: "${label}"`);
|
|
1611
1838
|
}
|
|
1612
|
-
// Bracketed-tape-block format (v7):
|
|
1613
|
-
// [<read-cells>]|[<read-cells>]... → [<write-cells>]/[<move-cells>]
|
|
1614
|
-
// Each bracketed list is a tape-block reading; the outer `|` separates
|
|
1615
|
-
// alternative read patterns. For single-tape machines with alternation,
|
|
1616
|
-
// the compact form `[<alt1>|<alt2>|...]` (one bracket, alternatives
|
|
1617
|
-
// inside) is also accepted; both forms decode to the same pattern
|
|
1618
|
-
// string.
|
|
1619
1839
|
const readLabel = label.slice(0, arrowIx);
|
|
1620
1840
|
const cmdLabel = label.slice(arrowIx + ' → '.length);
|
|
1621
|
-
// Strict per-pattern bracket form: `|` only between bracketed lists,
|
|
1622
|
-
// never inside. The compact `['^'|'1']` form is rejected by design —
|
|
1623
|
-
// every alternative must be its own bracketed pattern (`['^']|['1']`).
|
|
1624
|
-
// Pedagogically: each transition is drawn explicitly; the compact form
|
|
1625
|
-
// would read as cross-product semantics in multi-tape and confuse
|
|
1626
|
-
// readers (`['0'|'1','a'|'b']` could mean 4 combos, not 2 paired alts).
|
|
1627
|
-
// The rule applies to all bracketed lists — read alternatives, writes,
|
|
1628
|
-
// and movements — because commands and movements have no alternation
|
|
1629
|
-
// semantic either.
|
|
1630
1841
|
const stripBrackets = (s) => {
|
|
1631
1842
|
if (!s.startsWith('[') || !s.endsWith(']')) {
|
|
1632
1843
|
throw new Error(`fromMermaid: malformed bracketed list: "${s}"`);
|
|
1633
1844
|
}
|
|
1634
1845
|
const inner = s.slice(1, -1);
|
|
1635
|
-
// Walk the inner content; backslash escapes the next char (so `\|`
|
|
1636
|
-
// inside a cell is a literal pipe, not the alternation separator).
|
|
1637
1846
|
let i = 0;
|
|
1638
1847
|
while (i < inner.length) {
|
|
1639
1848
|
if (inner[i] === '\\' && i + 1 < inner.length) {
|
|
@@ -1648,10 +1857,6 @@ function fromMermaid(text) {
|
|
|
1648
1857
|
}
|
|
1649
1858
|
return inner;
|
|
1650
1859
|
};
|
|
1651
|
-
// Match `[…]` blocks in the read label. Inner content is a tape-block
|
|
1652
|
-
// reading (possibly with `|` for compact single-tape alternation).
|
|
1653
|
-
// `[^\]]*` is the simple non-greedy match — works because cell content
|
|
1654
|
-
// doesn't typically contain literal `]`.
|
|
1655
1860
|
const blockMatches = readLabel.match(/\[[^\]]*\]/g);
|
|
1656
1861
|
if (!blockMatches || blockMatches.length === 0) {
|
|
1657
1862
|
throw new Error(`fromMermaid: no bracketed read-list in label: "${label}"`);
|