@turing-machine-js/machine 7.0.0-alpha.1 → 7.0.0-alpha.3
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 +90 -0
- package/README.md +65 -24
- package/dist/classes/State.d.ts +18 -0
- package/dist/index.cjs +705 -312
- package/dist/index.mjs +705 -312
- package/dist/utilities/graph.d.ts +4 -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, _State_tags;
|
|
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.
|
|
@@ -750,6 +750,14 @@ class State {
|
|
|
750
750
|
// Note: toGraph / fromGraph deliberately do not serialize debug — debug is
|
|
751
751
|
// a runtime concern, not part of the structural graph.
|
|
752
752
|
_State_debugRef.set(this, { current: null });
|
|
753
|
+
// Out-of-band tags applied to this State (#186). Tags are visualization
|
|
754
|
+
// and debugger-tooling metadata — they don't affect runtime transition
|
|
755
|
+
// lookup or `equivalentOn` comparisons. Stored as a Set for de-duplication;
|
|
756
|
+
// exposed via the `tags` getter as a frozen array snapshot. Lives on the
|
|
757
|
+
// State INSTANCE so wrappers (from `withOverriddenHaltState`) carry tags
|
|
758
|
+
// independently of their bare's tag set — see the #175 sharing test in
|
|
759
|
+
// State.spec.ts.
|
|
760
|
+
_State_tags.set(this, new Set());
|
|
753
761
|
if (stateDefinition) {
|
|
754
762
|
const keys = Object.getOwnPropertyNames(stateDefinition);
|
|
755
763
|
if (keys.length) {
|
|
@@ -762,7 +770,7 @@ class State {
|
|
|
762
770
|
symbols.forEach((symbol) => {
|
|
763
771
|
const { nextState } = stateDefinition[symbol];
|
|
764
772
|
const nextStateLocal = nextState ?? this;
|
|
765
|
-
if (!(nextStateLocal instanceof
|
|
773
|
+
if (!(nextStateLocal instanceof _a) && !(nextStateLocal instanceof Reference)) {
|
|
766
774
|
throw new Error('invalid nextState');
|
|
767
775
|
}
|
|
768
776
|
let { command } = stateDefinition[symbol];
|
|
@@ -833,8 +841,38 @@ class State {
|
|
|
833
841
|
}
|
|
834
842
|
__classPrivateFieldGet$1(this, _State_debugRef, "f").current = new DebugConfig(this, value);
|
|
835
843
|
}
|
|
844
|
+
/**
|
|
845
|
+
* Add one or more tags to this State (#186). Tags are out-of-band metadata
|
|
846
|
+
* used by visualization (`toMermaid` emits `classDef`/`class` lines) and
|
|
847
|
+
* debugger tooling — they don't affect runtime transition lookup,
|
|
848
|
+
* `equivalentOn` comparisons, or any structural identity. Chainable.
|
|
849
|
+
*/
|
|
850
|
+
tag(...tags) {
|
|
851
|
+
for (const t of tags) {
|
|
852
|
+
__classPrivateFieldGet$1(this, _State_tags, "f").add(t);
|
|
853
|
+
}
|
|
854
|
+
return this;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Remove one or more tags from this State (#186). Untagging a tag the
|
|
858
|
+
* State doesn't carry is a no-op. Chainable.
|
|
859
|
+
*/
|
|
860
|
+
untag(...tags) {
|
|
861
|
+
for (const t of tags) {
|
|
862
|
+
__classPrivateFieldGet$1(this, _State_tags, "f").delete(t);
|
|
863
|
+
}
|
|
864
|
+
return this;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Frozen snapshot of this State's current tags (#186). The returned array
|
|
868
|
+
* is `Object.freeze`d — mutating it throws in strict mode (which TS-emitted
|
|
869
|
+
* code uses). Order matches insertion order of the underlying Set.
|
|
870
|
+
*/
|
|
871
|
+
get tags() {
|
|
872
|
+
return Object.freeze([...__classPrivateFieldGet$1(this, _State_tags, "f")]);
|
|
873
|
+
}
|
|
836
874
|
/** @internal — invoked by DebugConfig setters via module-private symbol. */
|
|
837
|
-
[(_State_id = new WeakMap(), _State_name = new WeakMap(), _State_overriddenHaltState = new WeakMap(), _State_bareState = new WeakMap(), _State_symbolToDataMap = new WeakMap(), _State_debugRef = new WeakMap(), validateDebugFilter)](fieldName, filter) {
|
|
875
|
+
[(_State_id = new WeakMap(), _State_name = new WeakMap(), _State_overriddenHaltState = new WeakMap(), _State_bareState = new WeakMap(), _State_symbolToDataMap = new WeakMap(), _State_debugRef = new WeakMap(), _State_tags = new WeakMap(), validateDebugFilter)](fieldName, filter) {
|
|
838
876
|
if (filter === undefined)
|
|
839
877
|
return;
|
|
840
878
|
// #108 part 2: `.after` on haltState has no semantic anchor — halt is
|
|
@@ -882,16 +920,40 @@ class State {
|
|
|
882
920
|
throw new Error(`No nextState for symbol at state named ${__classPrivateFieldGet$1(this, _State_id, "f")}`);
|
|
883
921
|
}
|
|
884
922
|
withOverriddenHaltState(overriddenHaltState) {
|
|
885
|
-
//
|
|
886
|
-
//
|
|
887
|
-
//
|
|
888
|
-
//
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
|
|
923
|
+
// Unwrap `this` if it's itself a wrapper — the chain's inner overrides
|
|
924
|
+
// are dead at runtime anyway (only the outermost `.wohs()`'s override is
|
|
925
|
+
// pushed onto the halt-stack on entry; verified empirically). Composite
|
|
926
|
+
// name reflects runtime behavior, not construction history. See #176.
|
|
927
|
+
const bare = __classPrivateFieldGet$1(this, _State_bareState, "f") ?? this;
|
|
928
|
+
// Memoize by (bare, override) so identical args return the same instance
|
|
929
|
+
// (#175). The cache uses WeakMaps + WeakRefs so cached wrappers can be
|
|
930
|
+
// GC'd when nothing else holds them. Compounds with the chain-collapse
|
|
931
|
+
// above: `A.wohs(t1).wohs(t2)` keys as (A, t2) after the unwrap, hitting
|
|
932
|
+
// the same cache slot as a direct `A.wohs(t2)`.
|
|
933
|
+
let innerCache = __classPrivateFieldGet$1(_a, _a, "f", _State_wrapperCache).get(bare);
|
|
934
|
+
if (innerCache !== undefined) {
|
|
935
|
+
const ref = innerCache.get(overriddenHaltState);
|
|
936
|
+
if (ref !== undefined) {
|
|
937
|
+
const cached = ref.deref();
|
|
938
|
+
if (cached !== undefined) {
|
|
939
|
+
return cached;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
innerCache = new WeakMap();
|
|
945
|
+
__classPrivateFieldGet$1(_a, _a, "f", _State_wrapperCache).set(bare, innerCache);
|
|
946
|
+
}
|
|
947
|
+
// Cache miss — construct with no name, then overwrite #name directly
|
|
948
|
+
// (composed names contain `(` and `)` which the constructor's user-facing
|
|
949
|
+
// validation would reject; private-field access bypasses that).
|
|
950
|
+
const state = new _a();
|
|
951
|
+
__classPrivateFieldSet$1(state, _State_name, `${bare.name}(${overriddenHaltState.name})`, "f");
|
|
952
|
+
__classPrivateFieldSet$1(state, _State_symbolToDataMap, __classPrivateFieldGet$1(bare, _State_symbolToDataMap, "f"), "f");
|
|
892
953
|
__classPrivateFieldSet$1(state, _State_overriddenHaltState, overriddenHaltState, "f");
|
|
893
|
-
__classPrivateFieldSet$1(state, _State_debugRef, __classPrivateFieldGet$1(
|
|
894
|
-
__classPrivateFieldSet$1(state, _State_bareState,
|
|
954
|
+
__classPrivateFieldSet$1(state, _State_debugRef, __classPrivateFieldGet$1(bare, _State_debugRef, "f"), "f");
|
|
955
|
+
__classPrivateFieldSet$1(state, _State_bareState, bare, "f");
|
|
956
|
+
innerCache.set(overriddenHaltState, new WeakRef(state));
|
|
895
957
|
return state;
|
|
896
958
|
}
|
|
897
959
|
// Single-state introspection — no traversal, no tapeBlock required.
|
|
@@ -904,7 +966,7 @@ class State {
|
|
|
904
966
|
for (const [sym, { command, nextState }] of __classPrivateFieldGet$1(state, _State_symbolToDataMap, "f")) {
|
|
905
967
|
let target = null;
|
|
906
968
|
try {
|
|
907
|
-
target = nextState instanceof
|
|
969
|
+
target = nextState instanceof _a ? nextState : nextState.ref;
|
|
908
970
|
}
|
|
909
971
|
catch {
|
|
910
972
|
target = null; // unbound Reference
|
|
@@ -928,204 +990,278 @@ class State {
|
|
|
928
990
|
transitions,
|
|
929
991
|
};
|
|
930
992
|
}
|
|
931
|
-
// Walks the State graph and emits a `Graph` data structure. v7
|
|
932
|
-
//
|
|
933
|
-
//
|
|
934
|
-
//
|
|
935
|
-
//
|
|
936
|
-
//
|
|
937
|
-
//
|
|
938
|
-
//
|
|
993
|
+
// Walks the State graph and emits a `Graph` data structure. v7 callable-
|
|
994
|
+
// subtree emit shape (#174):
|
|
995
|
+
//
|
|
996
|
+
// Each `withOverriddenHaltState` wrapper produces TWO graph nodes:
|
|
997
|
+
// - A wrapper node (`isWrapper: true`, `[[composite-name]]` shape) — the
|
|
998
|
+
// call site. No transitions of its own. `bareStateId` points to the
|
|
999
|
+
// bare's GraphNode; `overriddenHaltStateId` points to the override
|
|
1000
|
+
// target's GraphNode.
|
|
1001
|
+
// - A bare node (`isWrapper: false`, regular shape) — the callable body.
|
|
1002
|
+
// Has the bare's transitions. Shared across all wrappers that wrap
|
|
1003
|
+
// this bare (no per-context duplication).
|
|
939
1004
|
//
|
|
940
|
-
//
|
|
941
|
-
//
|
|
942
|
-
// halt
|
|
1005
|
+
// Frames are computed via union-find on bare reachability: two bares whose
|
|
1006
|
+
// forward-reachable sets overlap merge into one frame. Each frame contains
|
|
1007
|
+
// its bares + body states + a single halt marker (id = `-frameId`). The
|
|
1008
|
+
// canonical `frameId` is the smallest bare-id in the component.
|
|
1009
|
+
//
|
|
1010
|
+
// Halt-bound transitions of any in-frame state are retargeted to the
|
|
1011
|
+
// frame's halt marker. The frame's `subtree -. return .-> wrapper` and
|
|
1012
|
+
// `subtree -. halt .-> s0` arrows are demand-emitted by `toMermaid` from
|
|
1013
|
+
// the frame structure; they're not stored as graph edges.
|
|
943
1014
|
static toGraph(initialState, tapeBlock) {
|
|
944
1015
|
const nodes = {};
|
|
945
1016
|
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
|
-
}
|
|
1017
|
+
// Pass 1: BFS-discover all reachable States; emit one GraphNode per State
|
|
1018
|
+
// (wrapper or bare/regular). Wrappers and bares are separate nodes.
|
|
1019
|
+
const visited = new Set();
|
|
1020
|
+
const queue = [initialState];
|
|
1021
|
+
const bareIds = new Set(); // ids referenced as a wrapper's bareStateId
|
|
963
1022
|
while (queue.length > 0) {
|
|
964
|
-
const
|
|
1023
|
+
const state = queue.shift();
|
|
1024
|
+
if (visited.has(__classPrivateFieldGet$1(state, _State_id, "f"))) {
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
visited.add(__classPrivateFieldGet$1(state, _State_id, "f"));
|
|
965
1028
|
if (state.isHalt) {
|
|
966
|
-
// Real halt — always id 0, single node.
|
|
967
1029
|
if (!(0 in nodes)) {
|
|
968
1030
|
nodes[0] = {
|
|
969
1031
|
id: 0,
|
|
970
1032
|
name: __classPrivateFieldGet$1(state, _State_name, "f"),
|
|
971
1033
|
isHalt: true,
|
|
972
1034
|
isHaltMarker: false,
|
|
973
|
-
|
|
1035
|
+
isWrapper: false,
|
|
1036
|
+
bareStateId: null,
|
|
1037
|
+
frameId: null,
|
|
974
1038
|
transitions: [],
|
|
975
1039
|
overriddenHaltStateId: null,
|
|
1040
|
+
tags: [...__classPrivateFieldGet$1(state, _State_tags, "f")],
|
|
976
1041
|
};
|
|
977
1042
|
}
|
|
978
1043
|
continue;
|
|
979
1044
|
}
|
|
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"),
|
|
1045
|
+
// Wrapper? Emit wrapper node + queue bare and override target.
|
|
1046
|
+
if (__classPrivateFieldGet$1(state, _State_overriddenHaltState, "f") !== null && __classPrivateFieldGet$1(state, _State_bareState, "f") !== null) {
|
|
1047
|
+
const bareState = __classPrivateFieldGet$1(state, _State_bareState, "f");
|
|
1048
|
+
const overrideTarget = __classPrivateFieldGet$1(state, _State_overriddenHaltState, "f");
|
|
1049
|
+
nodes[__classPrivateFieldGet$1(state, _State_id, "f")] = {
|
|
1050
|
+
id: __classPrivateFieldGet$1(state, _State_id, "f"),
|
|
1051
|
+
name: __classPrivateFieldGet$1(state, _State_name, "f"), // composite name like "A(target)"
|
|
1011
1052
|
isHalt: false,
|
|
1012
1053
|
isHaltMarker: false,
|
|
1013
|
-
|
|
1054
|
+
isWrapper: true,
|
|
1055
|
+
bareStateId: __classPrivateFieldGet$1(bareState, _State_id, "f"),
|
|
1056
|
+
frameId: null,
|
|
1014
1057
|
transitions: [],
|
|
1015
|
-
overriddenHaltStateId:
|
|
1058
|
+
overriddenHaltStateId: __classPrivateFieldGet$1(overrideTarget, _State_id, "f"),
|
|
1059
|
+
tags: [...__classPrivateFieldGet$1(state, _State_tags, "f")],
|
|
1016
1060
|
};
|
|
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) {
|
|
1061
|
+
bareIds.add(__classPrivateFieldGet$1(bareState, _State_id, "f"));
|
|
1062
|
+
queue.push(bareState);
|
|
1063
|
+
queue.push(overrideTarget);
|
|
1070
1064
|
continue;
|
|
1071
1065
|
}
|
|
1066
|
+
// Regular (or bare) state — build node with transitions.
|
|
1072
1067
|
const node = {
|
|
1073
1068
|
id: __classPrivateFieldGet$1(state, _State_id, "f"),
|
|
1074
1069
|
name: __classPrivateFieldGet$1(state, _State_name, "f"),
|
|
1075
1070
|
isHalt: false,
|
|
1076
1071
|
isHaltMarker: false,
|
|
1077
|
-
|
|
1072
|
+
isWrapper: false,
|
|
1073
|
+
bareStateId: null,
|
|
1074
|
+
frameId: null,
|
|
1078
1075
|
transitions: [],
|
|
1079
1076
|
overriddenHaltStateId: null,
|
|
1077
|
+
tags: [...__classPrivateFieldGet$1(state, _State_tags, "f")],
|
|
1080
1078
|
};
|
|
1081
1079
|
nodes[__classPrivateFieldGet$1(state, _State_id, "f")] = node;
|
|
1082
1080
|
let patternIx = 0;
|
|
1083
1081
|
for (const [sym, { command, nextState }] of __classPrivateFieldGet$1(state, _State_symbolToDataMap, "f")) {
|
|
1084
1082
|
let target;
|
|
1085
1083
|
try {
|
|
1086
|
-
target = nextState instanceof
|
|
1084
|
+
target = nextState instanceof _a ? nextState : nextState.ref;
|
|
1087
1085
|
}
|
|
1088
1086
|
catch {
|
|
1089
1087
|
patternIx += 1;
|
|
1090
1088
|
continue;
|
|
1091
1089
|
}
|
|
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
1090
|
node.transitions.push({
|
|
1103
1091
|
pattern: decodePatternDescription(sym.description, alphabets),
|
|
1104
1092
|
command: command.tapesCommands.map((tc) => ({
|
|
1105
1093
|
symbol: decodeWriteSymbol(tc.symbol),
|
|
1106
1094
|
movement: decodeMovement(tc.movement.description),
|
|
1107
1095
|
})),
|
|
1108
|
-
nextStateId,
|
|
1096
|
+
nextStateId: __classPrivateFieldGet$1(target, _State_id, "f"),
|
|
1109
1097
|
id: `${__classPrivateFieldGet$1(state, _State_id, "f")}-${patternIx}`,
|
|
1110
1098
|
});
|
|
1099
|
+
queue.push(target);
|
|
1111
1100
|
patternIx += 1;
|
|
1112
1101
|
}
|
|
1113
1102
|
}
|
|
1114
|
-
|
|
1103
|
+
// Always emit real halt as a sentinel, even if no transition targets it.
|
|
1104
|
+
// It anchors the `subtree -. halt .-> s0` frame-level arrow whenever a
|
|
1105
|
+
// frame demand-emits one, and it's the canonical machine-halt singleton.
|
|
1106
|
+
if (!(0 in nodes)) {
|
|
1107
|
+
nodes[0] = {
|
|
1108
|
+
id: 0,
|
|
1109
|
+
name: 'halt',
|
|
1110
|
+
isHalt: true,
|
|
1111
|
+
isHaltMarker: false,
|
|
1112
|
+
isWrapper: false,
|
|
1113
|
+
bareStateId: null,
|
|
1114
|
+
frameId: null,
|
|
1115
|
+
transitions: [],
|
|
1116
|
+
overriddenHaltStateId: null,
|
|
1117
|
+
tags: [...__classPrivateFieldGet$1(haltState, _State_tags, "f")],
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
// Pass 2: For each bare, compute its forward-reachable set (following
|
|
1121
|
+
// transitions; stopping at halt and at wrappers — both are frame
|
|
1122
|
+
// boundaries).
|
|
1123
|
+
const computeReach = (startId) => {
|
|
1124
|
+
const reach = new Set();
|
|
1125
|
+
const stack = [startId];
|
|
1126
|
+
while (stack.length > 0) {
|
|
1127
|
+
const id = stack.pop();
|
|
1128
|
+
if (reach.has(id)) {
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
const node = nodes[id];
|
|
1132
|
+
// `nodes[id]` is always populated for `id` that the BFS reached, so
|
|
1133
|
+
// a defensive `!node` check would be dead. `isHalt` / `isWrapper`
|
|
1134
|
+
// are real boundaries — both stop reach-set expansion.
|
|
1135
|
+
if (node.isHalt || node.isWrapper) {
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
reach.add(id);
|
|
1139
|
+
for (const t of node.transitions) {
|
|
1140
|
+
const target = nodes[t.nextStateId];
|
|
1141
|
+
if (!target || target.isHalt || target.isWrapper) {
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
stack.push(t.nextStateId);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return reach;
|
|
1148
|
+
};
|
|
1149
|
+
const reachByBare = new Map();
|
|
1150
|
+
for (const bareId of bareIds) {
|
|
1151
|
+
reachByBare.set(bareId, computeReach(bareId));
|
|
1152
|
+
}
|
|
1153
|
+
// Pass 3: Union-find on bare overlaps. Two bares merge if their reach
|
|
1154
|
+
// sets share any state. Canonical representative = smallest bare-id in
|
|
1155
|
+
// the component.
|
|
1156
|
+
const ufParent = new Map();
|
|
1157
|
+
// Note: no path compression. The union policy below ("smaller id always
|
|
1158
|
+
// becomes root") keeps the tree flat — every union targets bares[0] as
|
|
1159
|
+
// the root, so any node's parent IS the root. Walking up never exceeds
|
|
1160
|
+
// one step. Path compression would be dead code under this invariant.
|
|
1161
|
+
const ufFind = (id) => {
|
|
1162
|
+
if (!ufParent.has(id)) {
|
|
1163
|
+
ufParent.set(id, id);
|
|
1164
|
+
}
|
|
1165
|
+
let root = id;
|
|
1166
|
+
while (ufParent.get(root) !== root) {
|
|
1167
|
+
root = ufParent.get(root);
|
|
1168
|
+
}
|
|
1169
|
+
return root;
|
|
1170
|
+
};
|
|
1171
|
+
const ufUnion = (a, b) => {
|
|
1172
|
+
const ra = ufFind(a);
|
|
1173
|
+
const rb = ufFind(b);
|
|
1174
|
+
if (ra === rb)
|
|
1175
|
+
return;
|
|
1176
|
+
if (ra < rb) {
|
|
1177
|
+
ufParent.set(rb, ra);
|
|
1178
|
+
}
|
|
1179
|
+
else {
|
|
1180
|
+
ufParent.set(ra, rb);
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
for (const bareId of bareIds) {
|
|
1184
|
+
ufFind(bareId);
|
|
1185
|
+
}
|
|
1186
|
+
// For each state, collect the bares that reach it; union all bares that
|
|
1187
|
+
// share a state.
|
|
1188
|
+
const stateToReachingBares = new Map();
|
|
1189
|
+
for (const [bareId, reachSet] of reachByBare) {
|
|
1190
|
+
for (const stateId of reachSet) {
|
|
1191
|
+
let bares = stateToReachingBares.get(stateId);
|
|
1192
|
+
if (!bares) {
|
|
1193
|
+
bares = [];
|
|
1194
|
+
stateToReachingBares.set(stateId, bares);
|
|
1195
|
+
}
|
|
1196
|
+
bares.push(bareId);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
for (const bares of stateToReachingBares.values()) {
|
|
1200
|
+
for (let i = 1; i < bares.length; i += 1) {
|
|
1201
|
+
ufUnion(bares[0], bares[i]);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
// Assign frameId to each in-reach state.
|
|
1205
|
+
const frameIds = new Set();
|
|
1206
|
+
for (const [stateId, bares] of stateToReachingBares) {
|
|
1207
|
+
const frameId = ufFind(bares[0]);
|
|
1208
|
+
nodes[stateId].frameId = frameId;
|
|
1209
|
+
frameIds.add(frameId);
|
|
1210
|
+
}
|
|
1211
|
+
// Pass 4: Retarget halt-bound transitions for in-frame states to the
|
|
1212
|
+
// frame's halt marker. Out-of-frame states (top-level dispatcher, override
|
|
1213
|
+
// targets, etc.) keep their halt-bound transitions pointing at real halt.
|
|
1214
|
+
for (const node of Object.values(nodes)) {
|
|
1215
|
+
if (node.frameId === null) {
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
const haltMarkerId = -node.frameId;
|
|
1219
|
+
for (const t of node.transitions) {
|
|
1220
|
+
const target = nodes[t.nextStateId];
|
|
1221
|
+
if (target && target.isHalt && !target.isHaltMarker) {
|
|
1222
|
+
t.nextStateId = haltMarkerId;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
// Pass 5: Emit one halt marker per frame.
|
|
1227
|
+
for (const frameId of frameIds) {
|
|
1228
|
+
const haltMarkerId = -frameId;
|
|
1229
|
+
nodes[haltMarkerId] = {
|
|
1230
|
+
id: haltMarkerId,
|
|
1231
|
+
name: 'halt',
|
|
1232
|
+
isHalt: true,
|
|
1233
|
+
isHaltMarker: true,
|
|
1234
|
+
isWrapper: false,
|
|
1235
|
+
bareStateId: null,
|
|
1236
|
+
frameId,
|
|
1237
|
+
transitions: [],
|
|
1238
|
+
overriddenHaltStateId: null,
|
|
1239
|
+
tags: [],
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
return { initialId: __classPrivateFieldGet$1(initialState, _State_id, "f"), alphabets, nodes };
|
|
1115
1243
|
}
|
|
1116
1244
|
// Inverse of toGraph: rebuilds a State graph (and a fresh TapeBlock with the
|
|
1117
1245
|
// graph's alphabets) from a serialized Graph. Round-trips with toGraph in
|
|
1118
1246
|
// the sense that running the rebuilt machine on the same input gives the
|
|
1119
1247
|
// same output, but the rebuilt State instances have *new* internal IDs.
|
|
1248
|
+
//
|
|
1249
|
+
// Under the v7 callable-subtree model (#174), graph nodes split into:
|
|
1250
|
+
// - Wrapper nodes (`isWrapper: true`, no transitions) — reconstructed via
|
|
1251
|
+
// `bareStates[bareStateId].withOverriddenHaltState(finalStates[overriddenHaltStateId])`.
|
|
1252
|
+
// - Bare/regular nodes — constructed as normal States with transitions.
|
|
1253
|
+
// - Halt + halt-marker nodes — collapse to the singleton `haltState`.
|
|
1120
1254
|
static fromGraph(graph) {
|
|
1121
1255
|
const alphabetObjs = graph.alphabets.map((syms) => new Alphabet(syms));
|
|
1122
1256
|
const tapeBlock = TapeBlock.fromAlphabets(alphabetObjs);
|
|
1123
1257
|
const ids = Object.keys(graph.nodes).map(Number);
|
|
1124
|
-
// Pass 1: pre-create a Reference for each non-halt node
|
|
1125
|
-
//
|
|
1258
|
+
// Pass 1: pre-create a Reference for each non-halt non-halt-marker node
|
|
1259
|
+
// (both wrappers and regulars). Halt and halt-marker nodes collapse to the
|
|
1260
|
+
// singleton `haltState` and need no ref.
|
|
1126
1261
|
const refs = {};
|
|
1127
1262
|
for (const nodeId of ids) {
|
|
1128
|
-
|
|
1263
|
+
const node = graph.nodes[nodeId];
|
|
1264
|
+
if (!node.isHalt) {
|
|
1129
1265
|
refs[nodeId] = new Reference();
|
|
1130
1266
|
}
|
|
1131
1267
|
}
|
|
@@ -1142,19 +1278,22 @@ class State {
|
|
|
1142
1278
|
}
|
|
1143
1279
|
return tapeBlock.symbol(flat);
|
|
1144
1280
|
};
|
|
1145
|
-
// Pass 2: build a
|
|
1146
|
-
//
|
|
1281
|
+
// Pass 2: build a State for each non-wrapper non-halt non-halt-marker
|
|
1282
|
+
// node. Transitions point at refs so cycles work; haltState (and halt
|
|
1283
|
+
// markers, which collapse to haltState) are used directly.
|
|
1147
1284
|
const bareStates = {};
|
|
1148
1285
|
for (const nodeId of ids) {
|
|
1149
1286
|
const node = graph.nodes[nodeId];
|
|
1150
|
-
if (node.isHalt) {
|
|
1287
|
+
if (node.isHalt || node.isWrapper) {
|
|
1151
1288
|
continue;
|
|
1152
1289
|
}
|
|
1153
1290
|
const stateDefinition = {};
|
|
1154
1291
|
for (const t of node.transitions) {
|
|
1155
1292
|
const key = patternToKey(parsePatternString(t.pattern, graph.alphabets));
|
|
1156
1293
|
const target = graph.nodes[t.nextStateId];
|
|
1157
|
-
const nextState = target
|
|
1294
|
+
const nextState = !target || target.isHalt
|
|
1295
|
+
? haltState
|
|
1296
|
+
: refs[t.nextStateId];
|
|
1158
1297
|
stateDefinition[key] = {
|
|
1159
1298
|
command: t.command.map((c) => ({
|
|
1160
1299
|
symbol: parseWriteSymbolLabel(c.symbol),
|
|
@@ -1163,14 +1302,20 @@ class State {
|
|
|
1163
1302
|
nextState,
|
|
1164
1303
|
};
|
|
1165
1304
|
}
|
|
1166
|
-
// Graph-sourced names may contain `(` and `)` (composite wrapper names
|
|
1167
|
-
//
|
|
1168
|
-
//
|
|
1169
|
-
|
|
1305
|
+
// Graph-sourced names may contain `(` and `)` (composite wrapper names —
|
|
1306
|
+
// although wrappers go through a separate path below, defensive
|
|
1307
|
+
// construction here keeps the bypass uniform). Construct without a name
|
|
1308
|
+
// and assign `#name` directly to skip user-facing name validation.
|
|
1309
|
+
const bare = new _a(stateDefinition);
|
|
1170
1310
|
__classPrivateFieldSet$1(bare, _State_name, node.name, "f");
|
|
1311
|
+
if (node.tags.length > 0) {
|
|
1312
|
+
bare.tag(...node.tags);
|
|
1313
|
+
}
|
|
1171
1314
|
bareStates[nodeId] = bare;
|
|
1172
1315
|
}
|
|
1173
|
-
// Pass 3:
|
|
1316
|
+
// Pass 3: resolve every node to its final State (memoized + cycle-safe).
|
|
1317
|
+
// Wrappers compose lazily via `withOverriddenHaltState` once their bare
|
|
1318
|
+
// and override are resolved.
|
|
1174
1319
|
const finalStates = {};
|
|
1175
1320
|
const inProgress = new Set();
|
|
1176
1321
|
const getFinal = (nodeId) => {
|
|
@@ -1178,7 +1323,7 @@ class State {
|
|
|
1178
1323
|
return finalStates[nodeId];
|
|
1179
1324
|
}
|
|
1180
1325
|
const node = graph.nodes[nodeId];
|
|
1181
|
-
if (node.isHalt) {
|
|
1326
|
+
if (!node || node.isHalt) {
|
|
1182
1327
|
finalStates[nodeId] = haltState;
|
|
1183
1328
|
return haltState;
|
|
1184
1329
|
}
|
|
@@ -1186,9 +1331,21 @@ class State {
|
|
|
1186
1331
|
throw new Error(`override-halt cycle at state #${nodeId}`);
|
|
1187
1332
|
}
|
|
1188
1333
|
inProgress.add(nodeId);
|
|
1189
|
-
let state
|
|
1190
|
-
if (node.
|
|
1191
|
-
|
|
1334
|
+
let state;
|
|
1335
|
+
if (node.isWrapper) {
|
|
1336
|
+
const bare = getFinal(node.bareStateId);
|
|
1337
|
+
const override = getFinal(node.overriddenHaltStateId);
|
|
1338
|
+
state = bare.withOverriddenHaltState(override);
|
|
1339
|
+
// Apply wrapper-scoped tags (#186). Tags don't leak across wrappers
|
|
1340
|
+
// sharing a bare — the wrapper instance owns its own tag set, and
|
|
1341
|
+
// engine #175 memoization returns the same instance for the same
|
|
1342
|
+
// (bare, override) pair, so this is idempotent across rebuilds.
|
|
1343
|
+
if (node.tags.length > 0) {
|
|
1344
|
+
state.tag(...node.tags);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
else {
|
|
1348
|
+
state = bareStates[nodeId];
|
|
1192
1349
|
}
|
|
1193
1350
|
inProgress.delete(nodeId);
|
|
1194
1351
|
finalStates[nodeId] = state;
|
|
@@ -1197,8 +1354,8 @@ class State {
|
|
|
1197
1354
|
for (const nodeId of ids) {
|
|
1198
1355
|
getFinal(nodeId);
|
|
1199
1356
|
}
|
|
1200
|
-
// Pass 4: bind each ref to the
|
|
1201
|
-
//
|
|
1357
|
+
// Pass 4: bind each ref to the resolved final State so cross-node
|
|
1358
|
+
// transitions land on the right instance.
|
|
1202
1359
|
for (const nodeId of ids) {
|
|
1203
1360
|
if (!graph.nodes[nodeId].isHalt) {
|
|
1204
1361
|
refs[nodeId].bind(finalStates[nodeId]);
|
|
@@ -1211,6 +1368,13 @@ class State {
|
|
|
1211
1368
|
};
|
|
1212
1369
|
}
|
|
1213
1370
|
}
|
|
1371
|
+
_a = State;
|
|
1372
|
+
// Memoization cache for `withOverriddenHaltState`. Keyed by
|
|
1373
|
+
// (bare, override) — same args return the same wrapper instance (#175).
|
|
1374
|
+
// Two-level WeakMap so the outer entry is GC'd when the bare is collected;
|
|
1375
|
+
// WeakRef values let wrappers themselves be GC'd when nothing else holds
|
|
1376
|
+
// them, with cache misses simply reconstructing fresh wrappers.
|
|
1377
|
+
_State_wrapperCache = { value: new WeakMap() };
|
|
1214
1378
|
const haltState = new State(null);
|
|
1215
1379
|
|
|
1216
1380
|
var __classPrivateFieldSet = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
@@ -1354,19 +1518,25 @@ _TuringMachine_tapeBlock = new WeakMap(), _TuringMachine_stack = new WeakMap();
|
|
|
1354
1518
|
// Currently only Mermaid flowchart syntax is supported. Future formats
|
|
1355
1519
|
// (Graphviz, JSON-LD, custom DSL) belong here too.
|
|
1356
1520
|
//
|
|
1357
|
-
// v7 emit
|
|
1358
|
-
// - Each wrapper
|
|
1359
|
-
//
|
|
1360
|
-
// (
|
|
1361
|
-
//
|
|
1362
|
-
//
|
|
1363
|
-
//
|
|
1364
|
-
//
|
|
1365
|
-
//
|
|
1366
|
-
// -
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1369
|
-
//
|
|
1521
|
+
// v7 callable-subtree emit (#174):
|
|
1522
|
+
// - Each `withOverriddenHaltState` wrapper produces TWO graph nodes — a
|
|
1523
|
+
// wrapper node (`[[composite-name]]`, OUTSIDE any subgraph) and a bare
|
|
1524
|
+
// node (regular shape, INSIDE its callable subtree subgraph).
|
|
1525
|
+
// - Subgraphs (one per frame): `subgraph w_${frameId}["callable subtree
|
|
1526
|
+
// of NAME"]` (single bare) or `["callable scope: A ∪ B"]` (union).
|
|
1527
|
+
// - Each frame has exactly one halt marker `c${frameId}(((halt)))` inside
|
|
1528
|
+
// its subgraph; halt-bound transitions from in-frame states retarget to
|
|
1529
|
+
// it. Always emitted (orphan signals dead wrapper).
|
|
1530
|
+
// - Arrow conventions:
|
|
1531
|
+
// solid `-->` regular transitions, including wrapper-to-override.
|
|
1532
|
+
// bold `==>` RESERVED for the wrapper-to-bare `call` arrow.
|
|
1533
|
+
// `&` ribbon collapses multi-wrapper-shares-bare.
|
|
1534
|
+
// dotted `-.->` frame-level dispatch (`return`, `halt`, `enter`).
|
|
1535
|
+
// - The `return` arrow (subgraph → wrapper) is demand-emitted iff the
|
|
1536
|
+
// frame's halt marker has at least one incoming edge AND the wrapper
|
|
1537
|
+
// calls into the frame. The `halt` arrow (subgraph → s0) is emitted
|
|
1538
|
+
// iff the halt marker has incoming AND there's at least one non-wrapper
|
|
1539
|
+
// entry into the frame (cross-subgraph solid arrow from outside).
|
|
1370
1540
|
// Maps a graph node id to its Mermaid id.
|
|
1371
1541
|
// - non-negative id N → "sN"
|
|
1372
1542
|
// - negative id -N (halt marker) → "cN"
|
|
@@ -1380,108 +1550,256 @@ function parseMermaidId(s) {
|
|
|
1380
1550
|
}
|
|
1381
1551
|
return Number(s.slice(1));
|
|
1382
1552
|
}
|
|
1553
|
+
function frameSubgraphId(frameId) {
|
|
1554
|
+
return `w_${frameId}`;
|
|
1555
|
+
}
|
|
1383
1556
|
function toMermaid(graph) {
|
|
1384
1557
|
const lines = [
|
|
1385
1558
|
'flowchart TD',
|
|
1386
1559
|
`%% alphabets: ${JSON.stringify(graph.alphabets)}`,
|
|
1387
1560
|
];
|
|
1388
|
-
// Sort nodes by id
|
|
1389
|
-
//
|
|
1390
|
-
//
|
|
1561
|
+
// Sort nodes by id ascending — real halt (0) first, then regulars by their
|
|
1562
|
+
// ids, then halt markers (negative) at the end. Deterministic emit lets
|
|
1563
|
+
// toMermaid → fromMermaid → toMermaid round-trip stably (#139).
|
|
1391
1564
|
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.
|
|
1565
|
+
// Bucket nodes for emit order.
|
|
1566
|
+
const topLevelNodes = nodes.filter((n) => n.frameId === null && !n.isWrapper);
|
|
1567
|
+
const wrapperNodes = nodes.filter((n) => n.isWrapper);
|
|
1568
|
+
// Bares-and-bodies inside frames, grouped by frameId.
|
|
1569
|
+
const nodesByFrame = new Map();
|
|
1570
|
+
// Halt-marker per frame (kept separate so it always emits LAST inside the
|
|
1571
|
+
// subgraph for deterministic shape).
|
|
1572
|
+
const haltMarkerByFrame = new Map();
|
|
1406
1573
|
for (const node of nodes) {
|
|
1407
|
-
if (node.
|
|
1574
|
+
if (node.frameId === null || node.isWrapper)
|
|
1408
1575
|
continue;
|
|
1576
|
+
if (node.isHaltMarker) {
|
|
1577
|
+
haltMarkerByFrame.set(node.frameId, node);
|
|
1409
1578
|
}
|
|
1410
|
-
|
|
1579
|
+
else {
|
|
1580
|
+
let bucket = nodesByFrame.get(node.frameId);
|
|
1581
|
+
if (!bucket) {
|
|
1582
|
+
bucket = [];
|
|
1583
|
+
nodesByFrame.set(node.frameId, bucket);
|
|
1584
|
+
}
|
|
1585
|
+
bucket.push(node);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
// Build the visible-label string for a node — name plus, if tagged, a
|
|
1589
|
+
// `<br>tag1, tag2, ...` suffix so the rendered Mermaid shows both. Tags
|
|
1590
|
+
// are the source of truth on the GraphNode; `<br>` is the universal
|
|
1591
|
+
// Mermaid line-break that works across renderers without `classDef`-
|
|
1592
|
+
// pseudo-element hacks (#186).
|
|
1593
|
+
const labelOf = (node) => {
|
|
1594
|
+
if (node.tags.length === 0)
|
|
1595
|
+
return node.name;
|
|
1596
|
+
return `${node.name}<br>${node.tags.join(', ')}`;
|
|
1597
|
+
};
|
|
1598
|
+
// 1. Emit top-level nodes (real halt, non-wrapper regulars outside any frame).
|
|
1599
|
+
for (const node of topLevelNodes) {
|
|
1600
|
+
const mid = mermaidIdFor(node.id);
|
|
1411
1601
|
if (node.isHalt) {
|
|
1412
|
-
lines.push(` ${
|
|
1602
|
+
lines.push(` ${mid}(((halt)))`);
|
|
1413
1603
|
}
|
|
1414
1604
|
else {
|
|
1415
|
-
lines.push(` ${
|
|
1605
|
+
lines.push(` ${mid}["${labelOf(node)}"]`);
|
|
1416
1606
|
}
|
|
1417
1607
|
}
|
|
1418
|
-
//
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1608
|
+
// 2. Emit wrappers at top level.
|
|
1609
|
+
for (const wrapper of wrapperNodes) {
|
|
1610
|
+
lines.push(` ${mermaidIdFor(wrapper.id)}[["${labelOf(wrapper)}"]]`);
|
|
1611
|
+
}
|
|
1612
|
+
// 3. `idle` sentinel.
|
|
1422
1613
|
lines.push(' idle([idle])');
|
|
1423
|
-
//
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
const
|
|
1427
|
-
const
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1614
|
+
// 4. Subgraph per frame.
|
|
1615
|
+
const frameIds = [...nodesByFrame.keys()].sort((a, b) => a - b);
|
|
1616
|
+
for (const frameId of frameIds) {
|
|
1617
|
+
const frameBares = (nodesByFrame.get(frameId) ?? []).filter((n) => isFrameBare(n, graph));
|
|
1618
|
+
const frameBareNames = frameBares
|
|
1619
|
+
.slice()
|
|
1620
|
+
.sort((a, b) => a.id - b.id)
|
|
1621
|
+
.map((n) => n.name);
|
|
1622
|
+
const label = frameBareNames.length > 1
|
|
1623
|
+
? `callable scope: ${frameBareNames.join(' ∪ ')}`
|
|
1624
|
+
: `callable subtree of ${frameBareNames[0] ?? frameId}`;
|
|
1625
|
+
lines.push(` subgraph ${frameSubgraphId(frameId)}["${label}"]`);
|
|
1626
|
+
// Inner nodes — sort by id for determinism.
|
|
1627
|
+
for (const node of (nodesByFrame.get(frameId) ?? []).slice().sort((a, b) => a.id - b.id)) {
|
|
1628
|
+
lines.push(` ${mermaidIdFor(node.id)}["${labelOf(node)}"]`);
|
|
1629
|
+
}
|
|
1630
|
+
// Every frame has a halt marker — `State.toGraph`'s frame-emit pass
|
|
1631
|
+
// creates one for each frame. Non-null assertion is safe; a defensive
|
|
1632
|
+
// null check would be dead.
|
|
1633
|
+
const haltMarker = haltMarkerByFrame.get(frameId);
|
|
1634
|
+
lines.push(` ${mermaidIdFor(haltMarker.id)}(((halt)))`);
|
|
1433
1635
|
lines.push(' end');
|
|
1434
1636
|
}
|
|
1435
|
-
// Enter arrow
|
|
1436
|
-
// node (whether plain `[…]` or wrapped `[[…]]` inside a subgraph).
|
|
1637
|
+
// 5. Enter arrow.
|
|
1437
1638
|
lines.push(` idle -. enter .-> ${mermaidIdFor(graph.initialId)}`);
|
|
1438
|
-
//
|
|
1439
|
-
//
|
|
1440
|
-
|
|
1639
|
+
// 6. `call` arrows — grouped by bare (multi-wrapper-shares-bare collapses
|
|
1640
|
+
// into a single `&` ribbon).
|
|
1641
|
+
const wrappersByBare = new Map();
|
|
1642
|
+
for (const wrapper of wrapperNodes) {
|
|
1643
|
+
if (wrapper.bareStateId === null)
|
|
1644
|
+
continue;
|
|
1645
|
+
let group = wrappersByBare.get(wrapper.bareStateId);
|
|
1646
|
+
if (!group) {
|
|
1647
|
+
group = [];
|
|
1648
|
+
wrappersByBare.set(wrapper.bareStateId, group);
|
|
1649
|
+
}
|
|
1650
|
+
group.push(wrapper);
|
|
1651
|
+
}
|
|
1652
|
+
const sortedBares = [...wrappersByBare.keys()].sort((a, b) => a - b);
|
|
1653
|
+
for (const bareId of sortedBares) {
|
|
1654
|
+
const wrappers = wrappersByBare.get(bareId).slice().sort((a, b) => a.id - b.id);
|
|
1655
|
+
const sources = wrappers.map((w) => mermaidIdFor(w.id)).join(' & ');
|
|
1656
|
+
lines.push(` ${sources} == "call" ==> ${mermaidIdFor(bareId)}`);
|
|
1657
|
+
}
|
|
1658
|
+
// 7. Demand-emit `return` and `halt` arrows per frame.
|
|
1659
|
+
// For each frame: check if its halt marker has incoming transitions.
|
|
1660
|
+
const haltMarkerHasIncoming = new Map();
|
|
1441
1661
|
for (const node of nodes) {
|
|
1442
|
-
|
|
1662
|
+
for (const t of node.transitions) {
|
|
1663
|
+
const target = graph.nodes[t.nextStateId];
|
|
1664
|
+
if (target && target.isHaltMarker && target.frameId !== null) {
|
|
1665
|
+
haltMarkerHasIncoming.set(target.frameId, true);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
// For each frame: check if there's at least one non-wrapper entry (a solid
|
|
1670
|
+
// `-->` from OUTSIDE the frame into any node INSIDE).
|
|
1671
|
+
const hasNonWrapperEntry = new Map();
|
|
1672
|
+
for (const node of nodes) {
|
|
1673
|
+
if (node.isWrapper)
|
|
1674
|
+
continue;
|
|
1675
|
+
for (const t of node.transitions) {
|
|
1676
|
+
const target = graph.nodes[t.nextStateId];
|
|
1677
|
+
if (target
|
|
1678
|
+
&& target.frameId !== null
|
|
1679
|
+
&& node.frameId !== target.frameId) {
|
|
1680
|
+
hasNonWrapperEntry.set(target.frameId, true);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
for (const frameId of frameIds) {
|
|
1685
|
+
if (!haltMarkerHasIncoming.get(frameId))
|
|
1443
1686
|
continue;
|
|
1687
|
+
// Return arrow — collapsed `&` ribbon over all wrappers calling this
|
|
1688
|
+
// frame. Frames only exist because at least one wrapper's bareStateId
|
|
1689
|
+
// points to a bare in the frame, so `callingWrappers` is always
|
|
1690
|
+
// non-empty for any frame that reached this code path.
|
|
1691
|
+
const callingWrappers = wrapperNodes.filter((w) => {
|
|
1692
|
+
const bare = graph.nodes[w.bareStateId];
|
|
1693
|
+
return bare.frameId === frameId;
|
|
1694
|
+
});
|
|
1695
|
+
const targets = callingWrappers
|
|
1696
|
+
.slice()
|
|
1697
|
+
.sort((a, b) => a.id - b.id)
|
|
1698
|
+
.map((w) => mermaidIdFor(w.id))
|
|
1699
|
+
.join(' & ');
|
|
1700
|
+
lines.push(` ${frameSubgraphId(frameId)} -. "return" .-> ${targets}`);
|
|
1701
|
+
if (hasNonWrapperEntry.get(frameId)) {
|
|
1702
|
+
lines.push(` ${frameSubgraphId(frameId)} -. "halt" .-> s0`);
|
|
1444
1703
|
}
|
|
1704
|
+
}
|
|
1705
|
+
// 8. Wrapper-to-override arrows (regular solid).
|
|
1706
|
+
//
|
|
1707
|
+
// `wrapper.overriddenHaltStateId` is always non-null on wrapper nodes
|
|
1708
|
+
// (set by `State.toGraph` for every `isWrapper: true` node — it's the
|
|
1709
|
+
// wrapper's override target, which a wrapper by definition has). The
|
|
1710
|
+
// non-null assertion is safe; a defensive null check would be dead.
|
|
1711
|
+
for (const wrapper of wrapperNodes) {
|
|
1712
|
+
lines.push(` ${mermaidIdFor(wrapper.id)} --> ${mermaidIdFor(wrapper.overriddenHaltStateId)}`);
|
|
1713
|
+
}
|
|
1714
|
+
// 9. Regular transitions for non-wrapper non-halt-marker non-halt nodes.
|
|
1715
|
+
for (const node of nodes) {
|
|
1716
|
+
if (node.isHalt || node.isHaltMarker || node.isWrapper)
|
|
1717
|
+
continue;
|
|
1445
1718
|
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
1719
|
const alternatives = t.pattern.split('|');
|
|
1463
1720
|
const reads = alternatives.map((alt) => `[${alt}]`).join('|');
|
|
1464
1721
|
const writes = `[${t.command.map((c) => c.symbol).join(',')}]`;
|
|
1465
1722
|
const moves = `[${t.command.map((c) => c.movement).join(',')}]`;
|
|
1466
1723
|
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)}`);
|
|
1724
|
+
lines.push(` ${mermaidIdFor(node.id)} -- "${label}" --> ${mermaidIdFor(t.nextStateId)}`);
|
|
1481
1725
|
}
|
|
1482
1726
|
}
|
|
1727
|
+
// 10. Tags (#186) — emit one `classDef tag_<name> fill:#...` per unique
|
|
1728
|
+
// tag across all nodes, then one `class <ids> tag_<name>` line per
|
|
1729
|
+
// tag listing every node that carries it (comma-joined for compact
|
|
1730
|
+
// emit). Tag-name → CSS-class identifier sanitization replaces any
|
|
1731
|
+
// char outside `[A-Za-z0-9_-]` with `_`; tag-name uniqueness in the
|
|
1732
|
+
// emit assumes user tags are already distinct after sanitization
|
|
1733
|
+
// (collisions are user error).
|
|
1734
|
+
emitTagAnnotations(lines, nodes);
|
|
1483
1735
|
return lines.join('\n');
|
|
1484
1736
|
}
|
|
1737
|
+
// Default Mermaid `classDef` palette — 6 visually distinct fill+stroke pairs,
|
|
1738
|
+
// selected by tag-name hash so multi-tag diagrams look readable out of the
|
|
1739
|
+
// box without user configuration. Users who want different colors can edit
|
|
1740
|
+
// the emitted Mermaid before rendering or override post-emit.
|
|
1741
|
+
const TAG_PALETTE = [
|
|
1742
|
+
['#fef3c7', '#92400e'], // amber
|
|
1743
|
+
['#dbeafe', '#1e40af'], // blue
|
|
1744
|
+
['#dcfce7', '#166534'], // green
|
|
1745
|
+
['#fce7f3', '#9d174d'], // pink
|
|
1746
|
+
['#ede9fe', '#5b21b6'], // violet
|
|
1747
|
+
['#fee2e2', '#991b1b'], // red
|
|
1748
|
+
];
|
|
1749
|
+
function sanitizeTagName(tag) {
|
|
1750
|
+
return tag.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
1751
|
+
}
|
|
1752
|
+
function tagColor(tag) {
|
|
1753
|
+
// Cheap deterministic hash — sum of char codes mod palette length. Stable
|
|
1754
|
+
// across runs; same tag name always picks the same color.
|
|
1755
|
+
let h = 0;
|
|
1756
|
+
for (let i = 0; i < tag.length; i += 1) {
|
|
1757
|
+
h = (h + tag.charCodeAt(i)) % TAG_PALETTE.length;
|
|
1758
|
+
}
|
|
1759
|
+
return TAG_PALETTE[h];
|
|
1760
|
+
}
|
|
1761
|
+
function emitTagAnnotations(lines, nodes) {
|
|
1762
|
+
// Collect nodes per tag in node-id order so output is deterministic.
|
|
1763
|
+
const nodesByTag = new Map();
|
|
1764
|
+
for (const node of nodes) {
|
|
1765
|
+
for (const tag of node.tags) {
|
|
1766
|
+
let list = nodesByTag.get(tag);
|
|
1767
|
+
if (!list) {
|
|
1768
|
+
list = [];
|
|
1769
|
+
nodesByTag.set(tag, list);
|
|
1770
|
+
}
|
|
1771
|
+
list.push(node.id);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
if (nodesByTag.size === 0)
|
|
1775
|
+
return;
|
|
1776
|
+
const sortedTags = [...nodesByTag.keys()].sort();
|
|
1777
|
+
for (const tag of sortedTags) {
|
|
1778
|
+
const sanitized = sanitizeTagName(tag);
|
|
1779
|
+
const [fill, stroke] = tagColor(tag);
|
|
1780
|
+
lines.push(` classDef tag_${sanitized} fill:${fill},stroke:${stroke}`);
|
|
1781
|
+
}
|
|
1782
|
+
for (const tag of sortedTags) {
|
|
1783
|
+
const sanitized = sanitizeTagName(tag);
|
|
1784
|
+
const ids = nodesByTag.get(tag).map((id) => mermaidIdFor(id)).join(',');
|
|
1785
|
+
lines.push(` class ${ids} tag_${sanitized}`);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
// Helper: identify "the bare states" that anchor a frame's name. A bare is a
|
|
1789
|
+
// node referenced as some wrapper's `bareStateId`. Body states (also in-frame
|
|
1790
|
+
// but not bare) are excluded from the frame label.
|
|
1791
|
+
//
|
|
1792
|
+
// The caller in `toMermaid` only passes non-wrapper, non-halt-marker nodes
|
|
1793
|
+
// (wrappers go to a separate bucket; halt markers go to `haltMarkerByFrame`).
|
|
1794
|
+
// No defensive `isHalt` / `isWrapper` guards needed here.
|
|
1795
|
+
function isFrameBare(node, graph) {
|
|
1796
|
+
for (const other of Object.values(graph.nodes)) {
|
|
1797
|
+
if (other.isWrapper && other.bareStateId === node.id) {
|
|
1798
|
+
return true;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
return false;
|
|
1802
|
+
}
|
|
1485
1803
|
// Inverse of toMermaid: parses the Mermaid output produced by toMermaid back
|
|
1486
1804
|
// into a Graph. The parser is strict to the dialect toMermaid emits — it
|
|
1487
1805
|
// recognises the specific node/edge shapes and the leading
|
|
@@ -1496,26 +1814,59 @@ function toMermaid(graph) {
|
|
|
1496
1814
|
const haltNodeRegex = /^([sc]\d+)\(\(\(halt\)\)\)$/;
|
|
1497
1815
|
const regularNodeRegex = /^(s\d+)\["([^"]*)"\]$/;
|
|
1498
1816
|
const wrappedNodeRegex = /^(s\d+)\[\["([^"]*)"\]\]$/;
|
|
1499
|
-
const subgraphStartRegex = /^subgraph\s+w_\d
|
|
1817
|
+
const subgraphStartRegex = /^subgraph\s+w_(\d+)\["([^"]*)"\]$/;
|
|
1500
1818
|
const subgraphEndRegex = /^end$/;
|
|
1501
1819
|
const idleNodeRegex = /^idle\(\[idle\]\)$/;
|
|
1502
1820
|
const enterArrowRegex = /^idle\s+-\.\s+enter\s+\.->\s+(s\d+)$/;
|
|
1503
|
-
|
|
1504
|
-
const
|
|
1505
|
-
|
|
1821
|
+
// Regular labeled transition (solid `-->`).
|
|
1822
|
+
const labeledTransitionRegex = /^([sc]\d+)\s+--\s+"(.*)"\s+-->\s+([sc]\d+)$/;
|
|
1823
|
+
// Wrapper → override (unlabeled solid `-->`).
|
|
1824
|
+
const wrapperOverrideRegex = /^(s\d+)\s+-->\s+([sc]\d+)$/;
|
|
1825
|
+
// Call arrow (bold `==>`), with optional `&`-joined source ribbon.
|
|
1826
|
+
// Ribbon separator is fixed at " & " (single spaces around &) — toMermaid
|
|
1827
|
+
// emits exactly that form, so the parser is strict to it. The literal-space
|
|
1828
|
+
// form avoids CodeQL's polynomial-ReDoS flag on a `\s+&\s+` shape.
|
|
1829
|
+
const callArrowRegex = /^(s\d+(?: & s\d+)*)\s+==\s+"call"\s+==>\s+(s\d+)$/;
|
|
1830
|
+
// Return arrow (`w_N -. return .-> s_W` with optional `&` target ribbon).
|
|
1831
|
+
const returnArrowRegex = /^w_(\d+)\s+-\.\s+"return"\s+\.->\s+(s\d+(?: & s\d+)*)$/;
|
|
1832
|
+
// Halt arrow (`w_N -. halt .-> s0`).
|
|
1833
|
+
const haltArrowRegex = /^w_(\d+)\s+-\.\s+"halt"\s+\.->\s+s0$/;
|
|
1506
1834
|
// First capture char anchored as \S to avoid polynomial backtracking between
|
|
1507
1835
|
// the preceding \s* and a permissive (.+); see CodeQL js/polynomial-redos.
|
|
1508
1836
|
const alphabetsRegex = /^%%\s*alphabets:\s*(\S.*)$/;
|
|
1837
|
+
// Tag annotation lines (#186). Matches both `classDef tag_<sanitized>` and
|
|
1838
|
+
// `class <id-list> tag_<sanitized>`. ClassDef declarations are decorative
|
|
1839
|
+
// (palette) and discarded on parse — toMermaid will regenerate them from
|
|
1840
|
+
// the tag set on re-emit. `class` lines carry the actual graph-node
|
|
1841
|
+
// assignments; we strip the `tag_` prefix and assign each tag to each
|
|
1842
|
+
// listed node's `tags` array.
|
|
1843
|
+
//
|
|
1844
|
+
// Inter-token gaps are fixed at single literal spaces (matching toMermaid's
|
|
1845
|
+
// canonical emit) rather than `\s+`. This avoids the polynomial-ReDoS
|
|
1846
|
+
// pattern CodeQL flags when `\s+` surrounds a content group (see also
|
|
1847
|
+
// `callArrowRegex` / `returnArrowRegex` tightening in PR #182).
|
|
1848
|
+
const classDefTagRegex = /^classDef tag_([A-Za-z0-9_-]+) .+$/;
|
|
1849
|
+
const classAssignTagRegex = /^class ([sc]\d+(?:,[sc]\d+)*) tag_([A-Za-z0-9_-]+)$/;
|
|
1850
|
+
// Splits a node label like `"A<br>hot, sampled"` into its name and tags (#186).
|
|
1851
|
+
// Labels without `<br>` have no tags. Tags are comma-joined; trimmed of
|
|
1852
|
+
// whitespace. The `<br>` is the single source of truth for tag-name parsing —
|
|
1853
|
+
// `class` lines are decorative-only and not consulted here.
|
|
1854
|
+
function splitLabelTags(label) {
|
|
1855
|
+
const brIx = label.indexOf('<br>');
|
|
1856
|
+
if (brIx < 0) {
|
|
1857
|
+
return { name: label, tags: [] };
|
|
1858
|
+
}
|
|
1859
|
+
const name = label.slice(0, brIx);
|
|
1860
|
+
const tagsStr = label.slice(brIx + '<br>'.length);
|
|
1861
|
+
const tags = tagsStr.split(',').map((t) => t.trim()).filter((t) => t.length > 0);
|
|
1862
|
+
return { name, tags };
|
|
1863
|
+
}
|
|
1509
1864
|
function fromMermaid(text) {
|
|
1510
1865
|
const lines = text.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
1511
1866
|
let alphabets = [];
|
|
1512
1867
|
let initialId = null;
|
|
1513
1868
|
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;
|
|
1869
|
+
let currentFrameId = null;
|
|
1519
1870
|
const ensureNode = (id, opts = {}) => {
|
|
1520
1871
|
if (!nodes[id]) {
|
|
1521
1872
|
nodes[id] = {
|
|
@@ -1523,9 +1874,12 @@ function fromMermaid(text) {
|
|
|
1523
1874
|
name: opts.name ?? mermaidIdFor(id),
|
|
1524
1875
|
isHalt: opts.isHalt ?? false,
|
|
1525
1876
|
isHaltMarker: opts.isHaltMarker ?? false,
|
|
1526
|
-
|
|
1877
|
+
isWrapper: opts.isWrapper ?? false,
|
|
1878
|
+
bareStateId: opts.bareStateId ?? null,
|
|
1879
|
+
frameId: opts.frameId ?? null,
|
|
1527
1880
|
transitions: [],
|
|
1528
1881
|
overriddenHaltStateId: null,
|
|
1882
|
+
tags: opts.tags ? [...opts.tags] : [],
|
|
1529
1883
|
};
|
|
1530
1884
|
}
|
|
1531
1885
|
else {
|
|
@@ -1535,72 +1889,133 @@ function fromMermaid(text) {
|
|
|
1535
1889
|
nodes[id].isHalt = opts.isHalt;
|
|
1536
1890
|
if (opts.isHaltMarker !== undefined)
|
|
1537
1891
|
nodes[id].isHaltMarker = opts.isHaltMarker;
|
|
1538
|
-
if (opts.
|
|
1539
|
-
nodes[id].
|
|
1892
|
+
if (opts.isWrapper !== undefined)
|
|
1893
|
+
nodes[id].isWrapper = opts.isWrapper;
|
|
1894
|
+
if (opts.bareStateId !== undefined)
|
|
1895
|
+
nodes[id].bareStateId = opts.bareStateId;
|
|
1896
|
+
if (opts.frameId !== undefined)
|
|
1897
|
+
nodes[id].frameId = opts.frameId;
|
|
1898
|
+
if (opts.tags !== undefined) {
|
|
1899
|
+
for (const t of opts.tags) {
|
|
1900
|
+
if (!nodes[id].tags.includes(t))
|
|
1901
|
+
nodes[id].tags.push(t);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1540
1904
|
}
|
|
1541
1905
|
return nodes[id];
|
|
1542
1906
|
};
|
|
1543
|
-
// First pass:
|
|
1907
|
+
// First pass: nodes + alphabets (track subgraph context for frameId).
|
|
1544
1908
|
for (const line of lines) {
|
|
1545
|
-
if (line === 'flowchart TD')
|
|
1909
|
+
if (line === 'flowchart TD')
|
|
1546
1910
|
continue;
|
|
1547
|
-
}
|
|
1548
1911
|
const am = line.match(alphabetsRegex);
|
|
1549
1912
|
if (am) {
|
|
1550
1913
|
alphabets = JSON.parse(am[1]);
|
|
1551
1914
|
continue;
|
|
1552
1915
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1916
|
+
// Tag annotations (#186) — classDef lines are decorative and skipped;
|
|
1917
|
+
// `class` lines are parsed in the edge pass since they reference nodes
|
|
1918
|
+
// by id and need those nodes already created in the first pass.
|
|
1919
|
+
if (classDefTagRegex.test(line))
|
|
1920
|
+
continue;
|
|
1921
|
+
const sgStart = line.match(subgraphStartRegex);
|
|
1922
|
+
if (sgStart) {
|
|
1923
|
+
currentFrameId = Number(sgStart[1]);
|
|
1555
1924
|
continue;
|
|
1556
1925
|
}
|
|
1557
1926
|
if (subgraphEndRegex.test(line)) {
|
|
1558
|
-
|
|
1927
|
+
currentFrameId = null;
|
|
1559
1928
|
continue;
|
|
1560
1929
|
}
|
|
1561
|
-
|
|
1562
|
-
// node — skip declaration, parse the `idle -. enter .-> sN` arrow in the
|
|
1563
|
-
// edge pass to set initialId.
|
|
1564
|
-
if (idleNodeRegex.test(line)) {
|
|
1930
|
+
if (idleNodeRegex.test(line))
|
|
1565
1931
|
continue;
|
|
1566
|
-
}
|
|
1567
1932
|
const hm = line.match(haltNodeRegex);
|
|
1568
1933
|
if (hm) {
|
|
1569
1934
|
const id = parseMermaidId(hm[1]);
|
|
1570
|
-
const isHaltMarker =
|
|
1571
|
-
ensureNode(id, {
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1935
|
+
const isHaltMarker = currentFrameId !== null;
|
|
1936
|
+
ensureNode(id, {
|
|
1937
|
+
name: 'halt',
|
|
1938
|
+
isHalt: true,
|
|
1939
|
+
isHaltMarker,
|
|
1940
|
+
frameId: isHaltMarker ? currentFrameId : null,
|
|
1941
|
+
});
|
|
1575
1942
|
continue;
|
|
1576
1943
|
}
|
|
1577
1944
|
const wm = line.match(wrappedNodeRegex);
|
|
1578
1945
|
if (wm) {
|
|
1579
|
-
|
|
1946
|
+
const { name, tags } = splitLabelTags(wm[2]);
|
|
1947
|
+
ensureNode(parseMermaidId(wm[1]), {
|
|
1948
|
+
name,
|
|
1949
|
+
isWrapper: true,
|
|
1950
|
+
tags,
|
|
1951
|
+
});
|
|
1580
1952
|
continue;
|
|
1581
1953
|
}
|
|
1582
1954
|
const rm = line.match(regularNodeRegex);
|
|
1583
1955
|
if (rm) {
|
|
1584
|
-
|
|
1956
|
+
const { name, tags } = splitLabelTags(rm[2]);
|
|
1957
|
+
ensureNode(parseMermaidId(rm[1]), {
|
|
1958
|
+
name,
|
|
1959
|
+
frameId: currentFrameId,
|
|
1960
|
+
tags,
|
|
1961
|
+
});
|
|
1585
1962
|
continue;
|
|
1586
1963
|
}
|
|
1587
1964
|
}
|
|
1588
1965
|
// Second pass: edges.
|
|
1589
1966
|
for (const line of lines) {
|
|
1590
|
-
// `idle -. enter .-> sN`: the sole source of initialId.
|
|
1591
1967
|
const em = line.match(enterArrowRegex);
|
|
1592
1968
|
if (em) {
|
|
1593
1969
|
initialId = parseMermaidId(em[1]);
|
|
1594
1970
|
continue;
|
|
1595
1971
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1972
|
+
// Return/halt arrows are derivable from frame structure at the next
|
|
1973
|
+
// toMermaid emit; consume but don't persist as graph data.
|
|
1974
|
+
if (returnArrowRegex.test(line) || haltArrowRegex.test(line)) {
|
|
1599
1975
|
continue;
|
|
1600
1976
|
}
|
|
1601
|
-
//
|
|
1602
|
-
//
|
|
1603
|
-
|
|
1977
|
+
// Tag class-assignment line (#186): `class s1,s5 tag_hot` — adds
|
|
1978
|
+
// the tag to each listed node. Tag-name preserved as written
|
|
1979
|
+
// (sanitization on emit is lossy in principle; on parse we don't
|
|
1980
|
+
// un-sanitize, since the original could have any characters).
|
|
1981
|
+
const tagMatch = line.match(classAssignTagRegex);
|
|
1982
|
+
if (tagMatch) {
|
|
1983
|
+
const ids = tagMatch[1].split(',');
|
|
1984
|
+
const tagName = tagMatch[2];
|
|
1985
|
+
for (const idStr of ids) {
|
|
1986
|
+
ensureNode(parseMermaidId(idStr), { tags: [tagName] });
|
|
1987
|
+
}
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
// `call` arrow — sets bareStateId on each source wrapper.
|
|
1991
|
+
const cm = line.match(callArrowRegex);
|
|
1992
|
+
if (cm) {
|
|
1993
|
+
const sources = cm[1].split(' & ');
|
|
1994
|
+
const bareId = parseMermaidId(cm[2]);
|
|
1995
|
+
for (const src of sources) {
|
|
1996
|
+
ensureNode(parseMermaidId(src), { isWrapper: true, bareStateId: bareId });
|
|
1997
|
+
}
|
|
1998
|
+
continue;
|
|
1999
|
+
}
|
|
2000
|
+
// Wrapper → override (unlabeled solid `-->`). Only fires if the source
|
|
2001
|
+
// node is a known wrapper (declared as `[[…]]`).
|
|
2002
|
+
const wo = line.match(wrapperOverrideRegex);
|
|
2003
|
+
if (wo) {
|
|
2004
|
+
const fromId = parseMermaidId(wo[1]);
|
|
2005
|
+
const toId = parseMermaidId(wo[2]);
|
|
2006
|
+
// The wrapper-override regex only matches `sN --> sM` (unlabeled);
|
|
2007
|
+
// since `toMermaid` only emits this shape from wrappers, the source
|
|
2008
|
+
// is guaranteed to be a wrapper if `fromMermaid`'s input came from
|
|
2009
|
+
// `toMermaid`. `nodes[fromId]` is always populated (first pass emits
|
|
2010
|
+
// node declarations before any edge parsing).
|
|
2011
|
+
if (nodes[fromId].isWrapper) {
|
|
2012
|
+
nodes[fromId].overriddenHaltStateId = toId;
|
|
2013
|
+
continue;
|
|
2014
|
+
}
|
|
2015
|
+
// Fall through — unlabeled solid from a non-wrapper is unexpected;
|
|
2016
|
+
// treated as a malformed line and ignored by the labeled-regex below.
|
|
2017
|
+
}
|
|
2018
|
+
const tm = line.match(labeledTransitionRegex);
|
|
1604
2019
|
if (tm) {
|
|
1605
2020
|
const fromId = parseMermaidId(tm[1]);
|
|
1606
2021
|
const label = tm[2];
|
|
@@ -1609,31 +2024,13 @@ function fromMermaid(text) {
|
|
|
1609
2024
|
if (arrowIx === -1) {
|
|
1610
2025
|
throw new Error(`fromMermaid: malformed edge label: "${label}"`);
|
|
1611
2026
|
}
|
|
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
2027
|
const readLabel = label.slice(0, arrowIx);
|
|
1620
2028
|
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
2029
|
const stripBrackets = (s) => {
|
|
1631
2030
|
if (!s.startsWith('[') || !s.endsWith(']')) {
|
|
1632
2031
|
throw new Error(`fromMermaid: malformed bracketed list: "${s}"`);
|
|
1633
2032
|
}
|
|
1634
2033
|
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
2034
|
let i = 0;
|
|
1638
2035
|
while (i < inner.length) {
|
|
1639
2036
|
if (inner[i] === '\\' && i + 1 < inner.length) {
|
|
@@ -1648,10 +2045,6 @@ function fromMermaid(text) {
|
|
|
1648
2045
|
}
|
|
1649
2046
|
return inner;
|
|
1650
2047
|
};
|
|
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
2048
|
const blockMatches = readLabel.match(/\[[^\]]*\]/g);
|
|
1656
2049
|
if (!blockMatches || blockMatches.length === 0) {
|
|
1657
2050
|
throw new Error(`fromMermaid: no bracketed read-list in label: "${label}"`);
|