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