diff --git a/doc/cfgcpuReadme.md b/doc/cfgcpuReadme.md
index 511a85b4f1..b8b72a75e4 100644
--- a/doc/cfgcpuReadme.md
+++ b/doc/cfgcpuReadme.md
@@ -314,6 +314,50 @@ An `ExecutionContext` tracks graph state within a single flow of control:
- **Variant merging** minimizes SelectorNode creation. Most self-modifying code only touches operands (non-final fields), not opcodes.
- **Null wildcards in signatures** give a uniform matching mechanism that works for both merged instructions and SelectorNode dispatch.
- **Observer-based replacement** (`InstructionReplacerRegistry`) keeps caches and graph consistent when nodes are merged, without coupling components.
+- **CfgBlock as a structural overlay** - blocks group straight-line instruction sequences without duplicating edge state. Successors/predecessors delegate by reference to the terminator/entry, so they stay in sync with the instruction-level graph automatically.
+- **O(1) block liveness** via a maintained counter rather than iterating all contained instructions on every check.
+- **Block boundary from instruction properties** - the linker determines boundaries exclusively from `IsBlockTerminator` and `IsBlockStarter` flags (set at parse time) plus static memory adjacency, never from `Kind` or `MaxSuccessorsCount` directly.
+- **Monotonic discovery** - `IsDiscoveryComplete` flips from false to true exactly once and never back, simplifying reasoning about block state.
+
+## CfgBlock
+
+A `CfgBlock` groups a contiguous sequence of instructions that always execute together: one entry, one exit, no intermediate join points. It's a structural overlay on the instruction-level CFG - the instruction-level edges remain the single source of truth.
+
+### Structure
+
+- `Entry` - first instruction in the block.
+- `Terminator` - last instruction (may be a `CfgInstruction` or a `SelectorNode`).
+- `Instructions` - ordered list from entry through terminator inclusive.
+- `IsDiscoveryComplete` - true once the linker has finalised the block.
+- `IsLive` - true if every contained instruction is live (O(1) via maintained counter).
+
+### Edge Delegation
+
+Block-level `Successors` returns `Terminator.Successors` by reference. Block-level `Predecessors` returns `Entry.Predecessors` by reference. No separate block-edge state is stored - this eliminates sync bugs.
+
+### Block Construction (NodeLinker)
+
+The linker builds blocks incrementally as instruction-level edges are added:
+
+1. **Bootstrap** - first edge from a node opens a one-node block for it.
+2. **Continuation** - if the predecessor is not a terminator, the next node is not a starter, and they're memory-adjacent, the next node is appended to the predecessor's block.
+3. **Boundary** - otherwise, the predecessor's block is closed and the next node gets its own block (new or split from an existing one).
+4. **Split** - when a new edge targets the interior of an existing block, the block is split at that point.
+
+### Self-Modifying Code and Blocks
+
+When `ReplaceInstruction` fires (variant merging), the replacement happens in-place within the block (`ReplaceInPlace`). The new instruction inherits the block position and back-pointer. Subsequent edge rewires hit the intra-block idempotency check and don't cause spurious splits.
+
+When a `SelectorNode` is injected via `CreateSelectorNodeBetween`, it either absorbed into the predecessor's block as its terminator (if the continuation rule applies) or lands in its own one-node block (if the predecessor is already a terminator).
+
+### Hot-Path Execution
+
+`CfgCpu.ExecuteNext` dispatches two ways depending on the resolved next node:
+
+- **Hot path** – if the next node is the entry of a discovered, live block (`next == block.Entry`), `ExecuteBlock` walks the block's instruction list directly without re-entering the feeder between steps. This skips feeder lookup, linker calls, and memory reconciliation for every non-terminator instruction in the block.
+- **Cold path** – in every other case (incomplete block, non-live block, or next node is an interior node of a complete live block), `ExecuteOneNode` steps exactly that one node.
+
+The interior-node cold-step case is expected and valid. It arises during incremental discovery, selector insertion, instruction replacement, fault edges, and other graph surgery – none of which should be treated as an error. External interrupts fire once per dispatch, after the last node actually executed (block or single node).
## Core Classes
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/CfgCpu.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/CfgCpu.cs
index 52f7039855..2926009715 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/CfgCpu.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/CfgCpu.cs
@@ -17,6 +17,7 @@ namespace Spice86.Core.Emulator.CPU.CfgCpu;
using Spice86.Core.Emulator.VM.Breakpoint;
using Spice86.Shared.Emulator.Memory;
using Spice86.Shared.Interfaces;
+using Spice86.Shared.Utils;
public class CfgCpu : IFunctionHandlerProvider, IClearable {
private readonly ILoggerService _loggerService;
@@ -26,15 +27,20 @@ public class CfgCpu : IFunctionHandlerProvider, IClearable {
private readonly ExecutionContextManager _executionContextManager;
private readonly InstructionReplacerRegistry _replacerRegistry = new();
private readonly CpuHeavyLogger? _cpuHeavyLogger;
+ private readonly EmulatorBreakpointsManager _emulatorBreakpointsManager;
+ private readonly IPauseHandler _pauseHandler;
public CfgCpu(IMemory memory, State state, IOPortDispatcher ioPortDispatcher, CallbackHandler callbackHandler,
DualPic dualPic, EmulatorBreakpointsManager emulatorBreakpointsManager,
+ IPauseHandler pauseHandler,
FunctionCatalogue functionCatalogue,
bool useCodeOverride, bool failOnInvalidOpcode, bool allowIvtAddress0, ILoggerService loggerService, CfgNodeExecutionCompiler executionCompiler, CpuHeavyLogger? cpuHeavyLogger = null) {
_loggerService = loggerService;
_state = state;
_dualPic = dualPic;
_cpuHeavyLogger = cpuHeavyLogger;
+ _emulatorBreakpointsManager = emulatorBreakpointsManager;
+ _pauseHandler = pauseHandler;
CfgNodeFeeder = new(memory, state, emulatorBreakpointsManager, _replacerRegistry, executionCompiler);
_executionContextManager = new(memory, state, CfgNodeFeeder, _replacerRegistry, functionCatalogue, useCodeOverride, loggerService, cpuHeavyLogger);
@@ -56,35 +62,120 @@ public CfgCpu(IMemory memory, State state, IOPortDispatcher ioPortDispatcher, Ca
public FunctionHandler FunctionHandlerInUse => ExecutionContextManager.CurrentExecutionContext.FunctionHandler;
public bool IsInitialExecutionContext => ExecutionContextManager.CurrentExecutionContext.Depth == 0;
private ExecutionContext CurrentExecutionContext => _executionContextManager.CurrentExecutionContext;
-
public ICfgNode ToExecute() {
return CfgNodeFeeder.GetLinkedCfgNodeToExecute(CurrentExecutionContext);
}
- ///
+ ///
+ /// Returns the block to execute on the hot path if the node is the entry point of a discovered,
+ /// live block, or null if the cold path should be taken.
+ /// Entering a live block via something else than the entry point happens just after block discovery is completed.
+ /// In this case, next is the terminator, appended to block, Block became complete but cold path needs to be done one last time.
+ ///
+ private CfgBlock? HotPathBlock(ICfgNode next) {
+ if (next.ContainingBlock is { IsDiscoveryComplete: true, IsLive: true } block
+ && next.Id == block.Entry.Id) {
+ return block;
+ }
+ return null;
+ }
+
+ ///
+ /// Resolves the next node via the cold-path entry edge, then dispatches hot or cold based
+ /// on whether the node belongs to a discovered, live block. Hot path runs the block walker;
+ /// cold path steps a single node. fires exactly once
+ /// at the boundary, on the last node actually executed.
+ ///
public void ExecuteNext() {
- ICfgNode toExecute = ToExecute();
+ ICfgNode next = ToExecute();
+ ICfgNode lastExecuted;
+
+ CfgBlock? hotBlock = HotPathBlock(next);
+ if (hotBlock is not null) {
+ lastExecuted = ExecuteBlock(hotBlock);
+ } else {
+ ExecuteOneNode(next);
+ lastExecuted = next;
+ }
+
+ // After execution, advance ExecutingNode to the next node so that a loop-level
+ // pause (EmulationLoop.WaitIfPaused) shows the node about to execute rather than
+ // the one that just finished.
+ ICfgNode? nextToExecute = CurrentExecutionContext.NodeToExecuteNextAccordingToGraph;
+ if (nextToExecute is not null) {
+ _executionContextManager.ExecutingNode = nextToExecute;
+ }
+
+ HandleExternalInterrupt(lastExecuted);
+ }
- // Execute the node
+ ///
+ /// Executes a single CFG node: updates CS:IP logging, runs the compiled execution,
+ /// handles any via the instruction execution helper, increments
+ /// cycles, and records last-executed / next-to-execute state. Returns false when a
+ /// CpuException was observed, signalling the caller to stop stepping.
+ ///
+ internal bool ExecuteOneNode(ICfgNode node) {
+ _executionContextManager.ExecutingNode = node;
+ if (_emulatorBreakpointsManager.HasActiveBreakpoints) {
+ _emulatorBreakpointsManager.CheckExecutionBreakPointsAt(
+ MemoryUtils.ToPhysicalAddress(node.Address.Segment, node.Address.Offset));
+ if (_state.CS != node.Address.Segment || _state.IP != node.Address.Offset) {
+ CurrentExecutionContext.NodeToExecuteNextAccordingToGraph = null;
+ return false;
+ }
+ _pauseHandler.WaitIfPaused();
+ }
+
+ bool faulted = false;
try {
- _loggerService.LoggerPropertyBag.CsIp = toExecute.Address;
- // Log instruction before execution if CPU heavy logging is enabled
- _cpuHeavyLogger?.LogInstruction(toExecute);
- toExecute.CompiledExecution(_instructionExecutionHelper);
+ _loggerService.LoggerPropertyBag.CsIp = node.Address;
+ _cpuHeavyLogger?.LogInstruction(node);
+ node.CompiledExecution(_instructionExecutionHelper);
} catch (CpuException e) {
- if(toExecute is CfgInstruction cfgInstruction) {
+ if (node is CfgInstruction cfgInstruction) {
_instructionExecutionHelper.HandleCpuException(cfgInstruction, e);
}
+ faulted = true;
}
- ICfgNode? nextToExecute = toExecute.GetNextSuccessor(_instructionExecutionHelper);
-
+ ICfgNode? nextToExecute = node.GetNextSuccessor(_instructionExecutionHelper);
+
_state.IncCycles();
- // Register what was executed and what is next node according to the graph in the execution context for next pass
- CurrentExecutionContext.LastExecuted = toExecute;
+ CurrentExecutionContext.LastExecuted = node;
CurrentExecutionContext.NodeToExecuteNextAccordingToGraph = nextToExecute;
- HandleExternalInterrupt(toExecute);
+
+ return !faulted;
+ }
+
+ ///
+ /// Hot-path block walker: iterates a discovered, live from
+ /// its entry through its terminator, calling
+ /// per instruction. Exits early if a node is non-live (memory mutated), a CpuException is
+ /// observed, or the terminator is reached. Does not fire interrupts between steps.
+ ///
+ internal ICfgNode ExecuteBlock(CfgBlock block) {
+ int count = block.Instructions.Count;
+ ICfgNode lastExecuted = block.Entry;
+
+ for (int i = 0; i < count; i++) {
+ ICfgNode node = block.Instructions[i];
+
+ if (!node.IsLive) {
+ CurrentExecutionContext.NodeToExecuteNextAccordingToGraph = null;
+ return lastExecuted;
+ }
+
+ bool ok = ExecuteOneNode(node);
+ lastExecuted = node;
+
+ if (!ok) {
+ return lastExecuted;
+ }
+ }
+
+ return lastExecuted;
}
///
@@ -141,4 +232,4 @@ public void Clear() {
CfgNodeFeeder.InstructionsFeeder.Clear();
_executionContextManager.Clear();
}
-}
\ No newline at end of file
+}
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/CfgBlock.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/CfgBlock.cs
new file mode 100644
index 0000000000..b14d1f5833
--- /dev/null
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/CfgBlock.cs
@@ -0,0 +1,207 @@
+namespace Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
+
+using Spice86.Core.Emulator.CPU.CfgCpu.Ast;
+using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Instruction;
+using Spice86.Core.Emulator.CPU.CfgCpu.Exceptions;
+using Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor;
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
+
+using System.Linq;
+
+///
+/// An that owns an ordered sequence of instruction nodes which always
+/// execute together as a straight-line path: one entry, one exit, no intermediate join points.
+/// Block-level successors/predecessors delegate by reference to the terminator/entry, so they
+/// stay in lock-step with the instruction-level graph automatically.
+///
+public sealed class CfgBlock : CfgNode {
+ private readonly List _instructions = new();
+
+ ///
+ /// Number of contained nodes whose is currently false.
+ /// is (_nonLiveCounter == 0).
+ ///
+ private int _nonLiveCounter;
+
+ private BlockNode? _cachedDisplayAst;
+ private bool _isDisplayAstStale = true;
+
+ public CfgBlock(int id, ICfgNode entry)
+ : base(id, entry.Address, maxSuccessorsCount: null) {
+ Append(entry);
+ }
+
+ public IReadOnlyList Instructions => _instructions;
+
+ public ICfgNode Entry => _instructions[0];
+
+ public ICfgNode Terminator => _instructions[_instructions.Count - 1];
+
+ ///
+ /// Whether the linker has finished discovering this block. Flipped to true once and
+ /// never back.
+ ///
+ public bool IsDiscoveryComplete { get; internal set; }
+
+ ///
+ ///
+ /// O(1). The block is live iff every contained instruction is live; computed from the
+ /// maintained without iterating.
+ ///
+ public override bool IsLive => _nonLiveCounter == 0;
+
+ ///
+ ///
+ /// Always null: a is itself the container, not contained in one.
+ /// Attempting to set this throws to surface bugs
+ /// where the linker inadvertently tries to nest a block inside another block.
+ ///
+ public override CfgBlock? ContainingBlock {
+ get => null;
+ set => throw new UnhandledCfgDiscrepancyException(
+ $"Cannot set {nameof(ContainingBlock)} on a {nameof(CfgBlock)}.");
+ }
+
+ ///
+ public override HashSet Successors => Terminator.Successors;
+
+ ///
+ public override HashSet Predecessors => Entry.Predecessors;
+
+ ///
+ public override bool CanCauseContextRestore => Terminator.CanCauseContextRestore;
+
+ ///
+ ///
+ /// Writing is not supported: a has no stored block-edge state.
+ /// Any write would corrupt the terminator's value, so this throws to surface the bug.
+ ///
+ public override int? MaxSuccessorsCount {
+ get => Terminator.MaxSuccessorsCount;
+ set => throw new NotSupportedException(
+ $"{nameof(MaxSuccessorsCount)} cannot be set on a {nameof(CfgBlock)}; " +
+ $"set it on the underlying terminator ({nameof(CfgInstruction)} or SelectorNode) instead.");
+ }
+
+ ///
+ public override bool CanHaveMoreSuccessors {
+ get => Terminator.CanHaveMoreSuccessors;
+ set => throw new NotSupportedException(
+ $"{nameof(CanHaveMoreSuccessors)} cannot be set on a {nameof(CfgBlock)}; " +
+ $"set it on the underlying terminator ({nameof(CfgInstruction)} or SelectorNode) instead.");
+ }
+
+ ///
+ public override ICfgNode? UniqueSuccessor {
+ get => Terminator.UniqueSuccessor;
+ set => throw new NotSupportedException(
+ $"{nameof(UniqueSuccessor)} cannot be set on a {nameof(CfgBlock)}; " +
+ $"set it on the underlying terminator ({nameof(CfgInstruction)} or SelectorNode) instead.");
+ }
+
+ ///
+ public override ICfgNode? GetNextSuccessor(InstructionExecutionHelper helper) =>
+ Terminator.GetNextSuccessor(helper);
+
+ ///
+ public override void UpdateSuccessorCache() =>
+ throw new NotSupportedException(
+ $"{nameof(UpdateSuccessorCache)} cannot be called on a {nameof(CfgBlock)}; " +
+ $"the terminator's cache is updated through instruction-level paths directly.");
+
+ ///
+ ///
+ /// Returns a wrapping all contained instructions' display ASTs.
+ /// Lazily regenerated when the block's structure changes.
+ ///
+ public override IVisitableAstNode DisplayAst {
+ get {
+ if (_isDisplayAstStale || _cachedDisplayAst is null) {
+ IVisitableAstNode[] statements = new IVisitableAstNode[_instructions.Count];
+ for (int i = 0; i < _instructions.Count; i++) {
+ statements[i] = _instructions[i].DisplayAst;
+ }
+ _cachedDisplayAst = new BlockNode(statements);
+ _isDisplayAstStale = false;
+ }
+ return _cachedDisplayAst;
+ }
+ }
+
+ ///
+ public override IVisitableAstNode ExecutionAst =>
+ throw new NotSupportedException(
+ $"{nameof(ExecutionAst)} is not supported on a {nameof(CfgBlock)}; " +
+ $"block-level compiled execution is not yet implemented.");
+
+ // -----------------------------------------------------------------------
+ // Internal API used by NodeLinker / CfgInstruction.SetLive only.
+ // -----------------------------------------------------------------------
+
+ ///
+ /// Appends to the end of the block, making it the new
+ /// . Updates the non-live counter from the node's live state.
+ ///
+ internal void Append(ICfgNode node) {
+ _instructions.Add(node);
+ if (!node.IsLive) {
+ _nonLiveCounter++;
+ }
+ _isDisplayAstStale = true;
+ }
+
+ ///
+ /// Replaces the node at with , preserving
+ /// order. Adjusts the non-live counter to reflect the new node's live state.
+ ///
+ internal void ReplaceInPlace(int index, ICfgNode newNode) {
+ ICfgNode old = _instructions[index];
+ _instructions[index] = newNode;
+ if (!old.IsLive) {
+ _nonLiveCounter--;
+ }
+ if (!newNode.IsLive) {
+ _nonLiveCounter++;
+ }
+ _isDisplayAstStale = true;
+ }
+
+ ///
+ /// Removes nodes from through the end and returns them as a
+ /// new list. The non-live counter is recomputed for the surviving prefix.
+ ///
+ internal List SliceFrom(int splitIndex) {
+ int tailLength = _instructions.Count - splitIndex;
+ List tail = _instructions.GetRange(splitIndex, tailLength);
+ _instructions.RemoveRange(splitIndex, tailLength);
+ RecountNonLiveFromInstructions();
+ _isDisplayAstStale = true;
+ return tail;
+ }
+
+ ///
+ /// Counter choke-point invoked by exactly once per
+ /// actual transition of a contained instruction.
+ ///
+ internal void OnContainedInstructionLiveChanged(bool nowLive) {
+ if (nowLive) {
+ _nonLiveCounter--;
+ } else {
+ _nonLiveCounter++;
+ }
+ }
+
+ ///
+ /// Recomputes the non-live counter from scratch by iterating the contained instructions.
+ /// Used after a split to re-base the counter on the new instruction set.
+ ///
+ internal void RecountNonLiveFromInstructions() {
+ _nonLiveCounter = _instructions.Count(node => !node.IsLive);
+ }
+
+ ///
+ /// Returns the index of in the contained list, or -1 if
+ /// the node is not part of this block.
+ ///
+ internal int IndexOf(ICfgNode node) => _instructions.IndexOf(node);
+}
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/CfgNode.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/CfgNode.cs
index e708d679c6..27c70e1cbf 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/CfgNode.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/CfgNode.cs
@@ -10,17 +10,20 @@ namespace Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
using System.Threading;
public abstract class CfgNode : ICfgNode {
- private static int _nextId;
- public CfgNode(SegmentedAddress address, int? maxSuccessorsCount) {
+ public CfgNode(int id, SegmentedAddress address, int? maxSuccessorsCount) {
+ Id = id;
Address = address;
- Id = _nextId++;
- MaxSuccessorsCount = maxSuccessorsCount;
+ // Assign the backing field directly: the virtual MaxSuccessorsCount setter is overridden
+ // by CfgBlock to throw NotSupportedException, so going through the property here would
+ // make CfgBlock impossible to construct. Subclasses that override the property are
+ // responsible for ignoring or surfacing this initial value as appropriate.
+ _maxSuccessorsCount = maxSuccessorsCount;
_compiledExecution = CreateUninitializedCompiledExecution(address, Id);
}
public int Id { get; }
- public HashSet Predecessors { get; } = new();
- public HashSet Successors { get; } = new();
+ public virtual HashSet Predecessors { get; } = new();
+ public virtual HashSet Successors { get; } = new();
public SegmentedAddress Address { get; }
public virtual bool CanCauseContextRestore => false;
@@ -47,18 +50,45 @@ public CfgNodeExecutionAction CompiledExecution {
///
/// Pre-built Abstract Syntax Tree representing the grammar of the assembly instruction (for display).
///
- public abstract InstructionNode DisplayAst { get; }
+ public abstract IVisitableAstNode DisplayAst { get; }
///
/// Pre-built Abstract Syntax Tree representing the execution logic of this node.
///
public abstract IVisitableAstNode ExecutionAst { get; }
- public int? MaxSuccessorsCount { get; set; }
+ private int? _maxSuccessorsCount;
+ public virtual int? MaxSuccessorsCount {
+ get => _maxSuccessorsCount;
+ set => _maxSuccessorsCount = value;
+ }
- public bool CanHaveMoreSuccessors { get; set; } = true;
+ public virtual bool CanHaveMoreSuccessors { get; set; } = true;
- public ICfgNode? UniqueSuccessor { get; set; }
+ public virtual ICfgNode? UniqueSuccessor { get; set; }
+
+ ///
+ /// Default to false. and
+ /// override this to provide their own logic.
+ ///
+ public virtual bool IsBlockTerminator => false;
+
+ ///
+ /// Default to false. overrides this to expose its
+ /// stored explicit starter flag.
+ ///
+ public virtual bool IsBlockStarter => false;
+
+ ///
+ /// Containing- back-pointer. Defaults to null.
+ /// and
+ /// override this with a settable
+ /// auto-property back-pointer maintained by .
+ /// keeps the default null because a block is itself the container,
+ /// not contained.
+ /// is the sole writer of this property.
+ ///
+ public virtual CfgBlock? ContainingBlock { get; set; }
private static CfgNodeExecutionAction CreateUninitializedCompiledExecution(
SegmentedAddress address,
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/CfgNodeIdAllocator.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/CfgNodeIdAllocator.cs
new file mode 100644
index 0000000000..3dab5865bc
--- /dev/null
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/CfgNodeIdAllocator.cs
@@ -0,0 +1,24 @@
+namespace Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
+
+using System.Threading;
+
+///
+/// Thread-safe, instance-scoped ID counter for CFG nodes.
+/// One instance per emulator run ensures IDs are deterministic and independent across runs.
+///
+public sealed class CfgNodeIdAllocator {
+ private int _nextId = -1;
+
+ ///
+ /// Allocates and returns the next available node ID.
+ ///
+ public int AllocateId() => Interlocked.Increment(ref _nextId);
+
+ ///
+ /// Sets the next ID to be allocated. Used when importing pre-existing blocks
+ /// that carry known IDs, to resume allocation beyond the highest imported ID.
+ ///
+ public int NextId {
+ set => Volatile.Write(ref _nextId, value - 1);
+ }
+}
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/ICfgNode.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/ICfgNode.cs
index 63aa5d56aa..4bb010d79c 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/ICfgNode.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/ICfgNode.cs
@@ -46,7 +46,6 @@ public interface ICfgNode {
/// Needs to be called each time a successor is added
///
void UpdateSuccessorCache();
-
///
/// Monotonically increasing counter incremented each time is recompiled.
@@ -73,8 +72,10 @@ public interface ICfgNode {
///
/// Pre-built Abstract Syntax Tree representing the grammar of the assembly instruction (for display).
+ /// For a single instruction this is an ; for a block it is a
+ /// wrapping all contained instructions' ASTs.
///
- InstructionNode DisplayAst { get; }
+ IVisitableAstNode DisplayAst { get; }
///
/// Pre-built Abstract Syntax Tree representing the execution logic of this node.
@@ -97,4 +98,28 @@ public interface ICfgNode {
/// Direct access to successor for nodes with only one successor
///
ICfgNode? UniqueSuccessor { get; set; }
+
+ ///
+ /// True when this node MUST be the last node of its .
+ /// For a this is computed from an explicit terminator flag set by the parser,
+ /// the instruction's Kind, and the current value of MaxSuccessorsCount.
+ /// For a this is always true.
+ /// is the only consumer of this for boundary decisions; no other
+ /// code SHALL inspect Kind or MaxSuccessorsCount for boundary purposes.
+ ///
+ bool IsBlockTerminator { get; }
+
+ ///
+ /// True when this node MUST be the first node of its , ending the
+ /// preceding block before it. Stored explicit flag, set by parser only. Defaults to false.
+ ///
+ bool IsBlockStarter { get; }
+
+ ///
+ /// that contains this node, or null if the node is not (yet) a
+ /// member of any block. 's own ContainingBlock returns null
+ /// by definition: a block is itself a block, not a member of one.
+ /// is the sole writer of this property.
+ ///
+ CfgBlock? ContainingBlock { get; set; }
}
\ No newline at end of file
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/NodeToString.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/NodeToString.cs
index b7a36a5f02..3bce92bd2d 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/NodeToString.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/NodeToString.cs
@@ -1,5 +1,6 @@
namespace Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
+using Spice86.Core.Emulator.CPU.CfgCpu.Ast;
using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Instruction;
using Spice86.Core.Emulator.CPU.CfgCpu.InstructionRenderer;
using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
@@ -32,7 +33,7 @@ public string ToHeaderString(ICfgNode node) {
}
public string ToAssemblyString(ICfgNode node) {
- InstructionNode ast = node.DisplayAst;
+ IVisitableAstNode ast = node.DisplayAst;
return ast.Accept(_renderer);
}
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/ExecutionContextManager.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/ExecutionContextManager.cs
index 7f0ed9b91b..459bded3ac 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/ExecutionContextManager.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/ExecutionContextManager.cs
@@ -2,6 +2,7 @@
using Serilog.Events;
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
using Spice86.Core.Emulator.CPU.CfgCpu.Feeder;
using Spice86.Core.Emulator.CPU.CfgCpu.Linker;
using Spice86.Core.Emulator.CPU.CfgCpu.Logging;
@@ -55,6 +56,14 @@ private int CurrentDepth {
public ExecutionContext CurrentExecutionContext { get; private set; }
+ ///
+ /// The node the CPU is about to execute next. Set in before
+ /// dispatch (so breakpoint pauses show the correct node) and updated after dispatch to the
+ /// next-to-execute successor (so loop-level pauses also show the next node, not the last one).
+ /// This is global across all execution contexts; only one node is ever executing at a time.
+ ///
+ public ICfgNode? ExecutingNode { get; set; }
+
public void SignalEntry() {
CurrentExecutionContext = NewExecutionContext(_state.IpSegmentedAddress);
}
@@ -137,5 +146,6 @@ public void Clear() {
ExecutionContextEntryPoints.Clear();
CurrentDepth = 0;
CurrentExecutionContext = NewExecutionContext(SegmentedAddress.ZERO);
+ ExecutingNode = null;
}
}
\ No newline at end of file
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Feeder/CfgNodeFeeder.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Feeder/CfgNodeFeeder.cs
index 8170f11801..e67b0a2dfe 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Feeder/CfgNodeFeeder.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Feeder/CfgNodeFeeder.cs
@@ -23,8 +23,9 @@ public class CfgNodeFeeder {
public CfgNodeFeeder(IMemory memory, State state, EmulatorBreakpointsManager emulatorBreakpointsManager,
InstructionReplacerRegistry replacerRegistry, CfgNodeExecutionCompiler executionCompiler) {
_state = state;
- InstructionsFeeder = new(emulatorBreakpointsManager, memory, state, replacerRegistry, executionCompiler);
- _nodeLinker = new(replacerRegistry, executionCompiler);
+ CfgNodeIdAllocator idAllocator = new();
+ InstructionsFeeder = new(emulatorBreakpointsManager, memory, state, replacerRegistry, executionCompiler, idAllocator);
+ _nodeLinker = new(replacerRegistry, executionCompiler, idAllocator);
}
public InstructionsFeeder InstructionsFeeder { get; }
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Feeder/InstructionsFeeder.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Feeder/InstructionsFeeder.cs
index 481ca0a752..f309752d25 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Feeder/InstructionsFeeder.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Feeder/InstructionsFeeder.cs
@@ -1,5 +1,6 @@
namespace Spice86.Core.Emulator.CPU.CfgCpu.Feeder;
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
using Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor.Expressions;
using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
using Spice86.Core.Emulator.CPU.CfgCpu.Parser;
@@ -22,8 +23,8 @@ public class InstructionsFeeder : IClearable {
private readonly CfgNodeExecutionCompiler _executionCompiler;
public InstructionsFeeder(EmulatorBreakpointsManager emulatorBreakpointsManager, IMemory memory, State cpuState,
- InstructionReplacerRegistry replacerRegistry, CfgNodeExecutionCompiler executionCompiler) {
- _instructionParser = new(memory, cpuState);
+ InstructionReplacerRegistry replacerRegistry, CfgNodeExecutionCompiler executionCompiler, CfgNodeIdAllocator idAllocator) {
+ _instructionParser = new(memory, cpuState, idAllocator);
_executionCompiler = executionCompiler;
CurrentInstructions = new(memory, emulatorBreakpointsManager, replacerRegistry);
PreviousInstructions = new(memory, replacerRegistry);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/InstructionExecutor/Expressions/CfgNodeExecutionCompiler.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/InstructionExecutor/Expressions/CfgNodeExecutionCompiler.cs
index b885dc732c..1f2ac797c9 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/InstructionExecutor/Expressions/CfgNodeExecutionCompiler.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/InstructionExecutor/Expressions/CfgNodeExecutionCompiler.cs
@@ -30,7 +30,7 @@ public class CfgNodeExecutionCompiler : IDisposable {
private readonly JitMode _jitMode;
private readonly BlockingCollection<(Expression> Expression,
- TaskCompletionSource> Tcs)> _compilationQueue = new();
+ ICfgNode Node, long Generation)> _compilationQueue = new();
private readonly CancellationTokenSource _cts = new();
private readonly Thread[] _backgroundThreads;
@@ -55,8 +55,7 @@ public CfgNodeExecutionCompiler(CfgNodeExecutionCompilerMonitor monitor, ILogger
for (int i = 0; i < backgroundCompilerThreadCount; i++) {
Thread thread = new Thread(() => RunBackgroundCompiler(token)) {
IsBackground = true,
- Name = $"CfgNodeBackgroundCompiler-{i}",
- Priority = ThreadPriority.BelowNormal
+ Name = $"CfgNodeBackgroundCompiler-{i}"
};
_backgroundThreads[i] = thread;
thread.Start();
@@ -70,13 +69,13 @@ public CfgNodeExecutionCompiler(CfgNodeExecutionCompilerMonitor monitor, ILogger
private void RunBackgroundCompiler(CancellationToken cancellationToken) {
try {
foreach ((Expression> Expression,
- TaskCompletionSource> Tcs) item
+ ICfgNode Node, long Generation) item
in _compilationQueue.GetConsumingEnumerable(cancellationToken)) {
try {
CfgNodeExecutionAction optimized = CompileExpression(item.Expression);
- item.Tcs.TrySetResult(optimized);
+ TrySwapCompiledExecution(item.Node, item.Generation, optimized);
} catch (InvalidOperationException ex) {
- item.Tcs.TrySetException(ex);
+ _logger.Error(ex, "CfgNodeExecutionCompiler: background compile error: {Message}", ex.Message);
} finally {
_monitor.RecordQueuePopped();
}
@@ -148,20 +147,15 @@ private void CompileInterpretedThenBackground(ICfgNode node,
long generation = CompileInterpreted(node, exprWithHelper);
_monitor.RecordInterpreted();
_monitor.RecordQueuePushed();
- TaskCompletionSource> tcs = new(
- TaskCreationOptions.RunContinuationsAsynchronously);
- _compilationQueue.TryAdd((exprWithHelper, tcs));
- Task> task = tcs.Task;
-
- // When the compiled delegate is ready, swap it onto the node only if no newer
- // Compile() call has been made in the meantime.
- task.ContinueWith(completedTask => {
- if (completedTask.IsCompletedSuccessfully
- && node.CompilationGeneration == generation) {
- AssignExecution(node, completedTask.Result);
- _monitor.RecordSwapped();
- }
- }, TaskContinuationOptions.ExecuteSynchronously);
+ _compilationQueue.TryAdd((exprWithHelper, node, generation));
+ }
+
+ private void TrySwapCompiledExecution(ICfgNode node, long generation,
+ CfgNodeExecutionAction optimized) {
+ if (node.CompilationGeneration == generation) {
+ AssignExecution(node, optimized);
+ _monitor.RecordSwapped();
+ }
}
private void AssignExecution(ICfgNode node, CfgNodeExecutionAction action) {
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/InstructionRenderer/AstRendererVisitor.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/InstructionRenderer/AstRendererVisitor.cs
index 1c32e83da3..792b537b4e 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/InstructionRenderer/AstRendererVisitor.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/InstructionRenderer/AstRendererVisitor.cs
@@ -333,7 +333,17 @@ public TOutput VisitMethodCallNode(MethodCallNode node) {
}
public TOutput VisitBlockNode(BlockNode node) {
- throw CreateUnsupportedNodeException(nameof(BlockNode));
+ if (node.Statements.Count == 0) {
+ return _outputRenderer.Empty();
+ }
+ List result = [];
+ for (int i = 0; i < node.Statements.Count; i++) {
+ if (i > 0) {
+ result.Add(_outputRenderer.Text(Environment.NewLine));
+ }
+ result.Add(node.Statements[i].Accept(this));
+ }
+ return _outputRenderer.Concat([.. result]);
}
public TOutput VisitIfElseNode(IfElseNode node) {
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Linker/NodeLinker.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Linker/NodeLinker.cs
index 0fddd7dfec..61c82469d4 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Linker/NodeLinker.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Linker/NodeLinker.cs
@@ -13,9 +13,11 @@ namespace Spice86.Core.Emulator.CPU.CfgCpu.Linker;
public class NodeLinker : InstructionReplacer {
private readonly CfgNodeExecutionCompiler _executionCompiler;
+ private readonly CfgNodeIdAllocator _idAllocator;
- public NodeLinker(InstructionReplacerRegistry replacerRegistry, CfgNodeExecutionCompiler executionCompiler) : base(replacerRegistry) {
+ public NodeLinker(InstructionReplacerRegistry replacerRegistry, CfgNodeExecutionCompiler executionCompiler, CfgNodeIdAllocator idAllocator) : base(replacerRegistry) {
_executionCompiler = executionCompiler;
+ _idAllocator = idAllocator;
}
///
@@ -46,7 +48,7 @@ public ICfgNode Link(InstructionSuccessorType linkToNextType, ICfgNode current,
private ICfgNode LinkRetInstruction(InstructionSuccessorType linkToNextType, CfgInstruction returnInstruction, ICfgNode next) {
ICfgNode resolvedForRet = LinkCfgInstructionWithType(linkToNextType, returnInstruction, next);
- // Need to link the call instruction now that ret is known
+ // Link the call instruction now that ret target is known
CfgInstruction? callInstruction = returnInstruction.CurrentCorrespondingCallInstruction;
returnInstruction.CurrentCorrespondingCallInstruction = null;
if (callInstruction == null) {
@@ -92,13 +94,31 @@ private ICfgNode ResolveSuccessorConflict(CfgInstruction current, ICfgNode next,
}
///
- /// Creates a SelectorNode at the address of the two instructions and inserts it as an intermediate predecessor of both.
+ /// Creates a at the address of the two instructions and inserts it
+ /// as an intermediate predecessor of both, dispatching between their two distinct
+ /// s based on memory bytes at runtime.
///
+ ///
+ /// The order of the two calls matters:
+ /// the first call is the only opportunity for the selector's incoming edges
+ /// (predecessor → selector) to be wired and reconciled, which gives the selector a
+ /// containing block. The second call assumes the selector already has a block.
+ /// Therefore we always process the instruction that has predecessors FIRST, guaranteeing
+ /// that at least one predecessor → selector edge is wired before the first
+ /// selector → variant edge fires.
+ ///
public SelectorNode CreateSelectorNodeBetween(CfgInstruction instruction1, CfgInstruction instruction2) {
- SelectorNode selectorNode = new SelectorNode(instruction1.Address);
+ SelectorNode selectorNode = new SelectorNode(_idAllocator.AllocateId(), instruction1.Address);
_executionCompiler.Compile(selectorNode);
- InsertIntermediatePredecessor(instruction1, selectorNode);
- InsertIntermediatePredecessor(instruction2, selectorNode);
+ // Wire the instruction that already has predecessors FIRST so the selector enters a block
+ // via the predecessor -> selector edge before any selector -> variant edge fires.
+ if (instruction1.Predecessors.Count > 0) {
+ InsertIntermediatePredecessor(instruction1, selectorNode);
+ InsertIntermediatePredecessor(instruction2, selectorNode);
+ } else {
+ InsertIntermediatePredecessor(instruction2, selectorNode);
+ InsertIntermediatePredecessor(instruction1, selectorNode);
+ }
return selectorNode;
}
@@ -146,7 +166,11 @@ private void AttachToNext(ICfgNode current, ICfgNode next) {
}
private void LinkToNext(ICfgNode current, ICfgNode next) {
- current.Successors.Add(next);
+ if (!current.Successors.Add(next)) {
+ // Edge already present — keep Link idempotent by short-circuiting before
+ // any predecessor-set, unique-successor, or block-reconciliation side effect.
+ return;
+ }
next.Predecessors.Add(current);
// Unique successor is only valid if node has 1 successor
current.UniqueSuccessor = current.MaxSuccessorsCount == 1 ? next : null;
@@ -155,10 +179,172 @@ private void LinkToNext(ICfgNode current, ICfgNode next) {
// This means that there is no need to try to link it to other nodes, it is impossible there will be new links.
current.CanHaveMoreSuccessors = false;
}
+ // Block-level reconciliation: single choke-point where block construction,
+ // extension, splitting, and discovery state are kept in sync.
+ ReconcileBlockAtEdge(current, next);
+ }
+
+ // -----------------------------------------------------------------------
+ // Block reconciliation state machine.
+ //
+ // ReconcileBlockAtEdge is invoked after every instruction-level edge addition.
+ // It is idempotent: a second call with the same (current, next) produces the
+ // same final block layout as the first.
+ //
+ // Cases:
+ // Bootstrap: current has no containing block. Open one.
+ // Idempotency: edge is intra-block and next is current's neighbor; nothing to do.
+ // Intra-block non-neighbor: split at next (new entry) and/or after current (new terminator).
+ // Rear-side split: current is interior; split off the strict tail.
+ // Continuation: append next to current's block.
+ // Boundary: Close currentBlock and place next in its own block.
+ //
+ // Block reconciliation never mutates any instruction-level edge.
+ // -----------------------------------------------------------------------
+
+ private void ReconcileBlockAtEdge(ICfgNode current, ICfgNode next) {
+ // Bootstrap: current has no containing block yet.
+ if (current.ContainingBlock is null) {
+ OpenBlock(current);
+ }
+ if (current.ContainingBlock is not CfgBlock currentBlock) {
+ return;
+ }
+
+ // Intra-block edge: both nodes already share the same block.
+ if (ReferenceEquals(next.ContainingBlock, currentBlock)) {
+ int currentIndex = currentBlock.IndexOf(current);
+ int nextIndex = currentBlock.IndexOf(next);
+ // True idempotency: next immediately follows current in the block.
+ if (currentIndex + 1 == nextIndex) {
+ return;
+ }
+ // Non-neighbor intra-block edge: split at next so it becomes a block entry.
+ if (nextIndex > 0) {
+ SplitBlock(currentBlock, nextIndex, completePrefixDiscovery: true);
+ }
+ // After the split above current may now be in a different block.
+ // If current is interior, split after it so it becomes a terminator.
+ if (current.ContainingBlock is CfgBlock currentBlockAfterSplit) {
+ int updatedCurrentIndex = currentBlockAfterSplit.IndexOf(current);
+ if (updatedCurrentIndex >= 0 && updatedCurrentIndex < currentBlockAfterSplit.Instructions.Count - 1) {
+ SplitBlock(currentBlockAfterSplit, updatedCurrentIndex + 1, completePrefixDiscovery: true);
+ }
+ }
+ return;
+ }
+
+ // Rear-side split: current is interior to its block so next must be in a different block.
+ int indexOfCurrent = currentBlock.IndexOf(current);
+ if (indexOfCurrent >= 0 && indexOfCurrent < currentBlock.Instructions.Count - 1) {
+ SplitBlock(currentBlock, indexOfCurrent + 1, completePrefixDiscovery: false);
+ }
+
+ // Continuation check: Is it a simple fallthrough?
+ // Uses IsBlockTerminator, IsBlockStarter, and static memory adjacency.
+ // SelectorNode.IsBlockTerminator is always true, so a
+ // SelectorNode current short-circuits to false.
+ bool isContinuation =
+ !current.IsBlockTerminator
+ && !next.IsBlockStarter
+ && current is CfgInstruction cfgCurrent
+ && cfgCurrent.NextInMemoryAddress32.ToSegmentedAddress() == next.Address;
+
+ // Continuation: append next to current's block.
+ if (isContinuation && next.ContainingBlock is null) {
+ AppendToBlock(currentBlock, next);
+ if (next.IsBlockTerminator) {
+ CompleteBlockDiscovery(currentBlock);
+ }
+ return;
+ }
+
+ // Boundary path: close currentBlock and place next.
+ CompleteBlockDiscovery(currentBlock);
+ AssignBlockForNext(next);
+ }
+
+ ///
+ /// Opens a fresh seeded with and
+ /// auto-marks discovery-complete when the entry is itself a block terminator.
+ ///
+ private CfgBlock OpenBlock(ICfgNode entry) {
+ CfgBlock block = new CfgBlock(_idAllocator.AllocateId(), entry);
+ entry.ContainingBlock = block;
+ if (entry.IsBlockTerminator) {
+ CompleteBlockDiscovery(block);
+ }
+ return block;
+ }
+
+ ///
+ /// Appends to and sets its containing block.
+ ///
+ private void AppendToBlock(CfgBlock block, ICfgNode node) {
+ block.Append(node);
+ node.ContainingBlock = block;
+ }
+
+ ///
+ /// Monotonic flip false → true on ;
+ /// no-op when already complete.
+ ///
+ private static void CompleteBlockDiscovery(CfgBlock block) {
+ if (!block.IsDiscoveryComplete) {
+ block.IsDiscoveryComplete = true;
+ }
+ }
+
+ ///
+ /// Splits at , moving the tail
+ /// into a new closed suffix block. Optionally closes the prefix.
+ /// Touches no instruction-level edges.
+ ///
+ private CfgBlock SplitBlock(CfgBlock block, int splitIndex, bool completePrefixDiscovery) {
+ List tail = block.SliceFrom(splitIndex);
+ ICfgNode newEntry = tail[0];
+ CfgBlock newBlock = new CfgBlock(_idAllocator.AllocateId(), newEntry);
+ newEntry.ContainingBlock = newBlock;
+ for (int i = 1; i < tail.Count; i++) {
+ ICfgNode node = tail[i];
+ newBlock.Append(node);
+ node.ContainingBlock = newBlock;
+ }
+ newBlock.IsDiscoveryComplete = true;
+ if (completePrefixDiscovery) {
+ block.IsDiscoveryComplete = true;
+ }
+ return newBlock;
+ }
+
+ ///
+ /// Places in a block: opens a fresh one if it has none,
+ /// or splits its existing block when the new edge targets an interior node.
+ /// No-op when is already the entry of its block.
+ ///
+ private void AssignBlockForNext(ICfgNode next) {
+ if (next.ContainingBlock is null) {
+ OpenBlock(next);
+ return;
+ }
+ if (!ReferenceEquals(next, next.ContainingBlock.Entry)) {
+ SplitBlock(next.ContainingBlock, next.ContainingBlock.IndexOf(next), completePrefixDiscovery: true);
+ }
}
public override void ReplaceInstruction(CfgInstruction oldInstruction, CfgInstruction newInstruction) {
- // Switch predecessors and successors of old to new
+ // In-place block fix-up: replace oldInstruction in its block and transfer the
+ // back-pointer BEFORE rewiring edges, so subsequent intra-block rewires hit
+ // Idempotency in ReconcileBlockAtEdge and avoid spurious splits.
+ CfgBlock? containingBlock = oldInstruction.ContainingBlock;
+ if (containingBlock is not null) {
+ int index = containingBlock.IndexOf(oldInstruction);
+ if (index >= 0) {
+ containingBlock.ReplaceInPlace(index, newInstruction);
+ newInstruction.ContainingBlock = containingBlock;
+ oldInstruction.ContainingBlock = null;
+ }
+ }
SwitchPredecessorsToNew(oldInstruction, newInstruction);
SwitchSuccessorsToNew(oldInstruction, newInstruction);
// Keep data from SuccessorsPerType map of old
@@ -169,10 +355,8 @@ public override void ReplaceInstruction(CfgInstruction oldInstruction, CfgInstru
}
///
- /// Inserts newPredecessor as an intermediate node between current and its own predecessors
+ /// Inserts newPredecessor as an intermediate node between current and its own predecessors.
///
- ///
- ///
public void InsertIntermediatePredecessor(ICfgNode current, ICfgNode newPredecessor) {
SwitchPredecessorsToNew(current, newPredecessor);
// Make new the only predecessor of current
@@ -183,7 +367,7 @@ public void InsertIntermediatePredecessor(ICfgNode current, ICfgNode newPredeces
private void SwitchSuccessorsToNew(ICfgNode oldNode, ICfgNode newNode) {
foreach (ICfgNode successor in oldNode.Successors) {
- // Replace current with new in the successor's predecessors (:
+ // Replace current with new in the successor's predecessors
LinkToNext(newNode, successor);
successor.Predecessors.Remove(oldNode);
// No cache update of newNode. Should be done at the end of the loop but caller already does it.
@@ -193,7 +377,7 @@ private void SwitchSuccessorsToNew(ICfgNode oldNode, ICfgNode newNode) {
private void SwitchPredecessorsToNew(ICfgNode oldNode, ICfgNode newNode) {
foreach (ICfgNode predecessor in oldNode.Predecessors) {
- // Replace current with new in the predecessors successors (:
+ // Replace current with new in the predecessors successors
LinkToNext(predecessor, newNode);
predecessor.Successors.Remove(oldNode);
predecessor.UpdateSuccessorCache();
@@ -213,4 +397,4 @@ private void ReplaceSuccessorOfCallInstruction(CfgInstruction instruction, ICfgN
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/ParsedInstruction/CfgInstruction.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/ParsedInstruction/CfgInstruction.cs
index d14f13f066..0fe38d59f9 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/ParsedInstruction/CfgInstruction.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/ParsedInstruction/CfgInstruction.cs
@@ -64,13 +64,36 @@ internal void AttachAsts(InstructionNode instructionAst, IVisitableAstNode execu
///
private bool _isLive = true;
- public CfgInstruction(SegmentedAddress address, InstructionField opcodeField, int? maxSuccessorsCount) : this(address,
- opcodeField, new List(), maxSuccessorsCount) {
- }
+ ///
+ /// Explicit block-terminator flag. Set ONLY by the parser via
+ /// . Combined with and the
+ /// live in .
+ ///
+ private bool _explicitBlockTerminator;
+
+ ///
+ /// Stored explicit block-starter flag. Set ONLY by the parser via
+ /// and surfaced by .
+ ///
+ private bool _isBlockStarter;
- public CfgInstruction(SegmentedAddress address, InstructionField opcodeField,
+ ///
+ /// Marks this instruction as forcing the end of its .
+ /// Intended to be called by parsers (e.g. for STI) immediately after parsing
+ /// the instruction. Has no effect on already-set state (the flag is monotonic).
+ ///
+ internal void MarkAsBlockTerminator() => _explicitBlockTerminator = true;
+
+ ///
+ /// Marks this instruction as forcing the start of a new .
+ /// Intended to be called by parsers (e.g. for CLI) immediately after parsing
+ /// the instruction. Has no effect on already-set state (the flag is monotonic).
+ ///
+ internal void MarkAsBlockStarter() => _isBlockStarter = true;
+
+ public CfgInstruction(int id, SegmentedAddress address, InstructionField opcodeField,
List prefixes, int? maxSuccessorsCount)
- : base(address, maxSuccessorsCount) {
+ : base(id, address, maxSuccessorsCount) {
InstructionPrefixes = prefixes;
PrefixFields = prefixes.Select(prefix => prefix.PrefixField).ToList();
foreach (InstructionPrefix prefix in prefixes) {
@@ -124,6 +147,28 @@ public override void UpdateSuccessorCache() {
public override bool IsLive => _isLive;
+ ///
+ ///
+ /// Re-evaluated on every read so that any change to
+ /// (e.g. via ) is automatically reflected without
+ /// callers needing to refresh anything. The portion
+ /// treats null (unbounded) as "not equal to 1" and therefore terminator.
+ ///
+ public override bool IsBlockTerminator =>
+ _explicitBlockTerminator
+ || (Kind & (InstructionKind.Jump | InstructionKind.Call | InstructionKind.Return)) != 0
+ || MaxSuccessorsCount != 1;
+
+ ///
+ public override bool IsBlockStarter => _isBlockStarter;
+
+ ///
+ ///
+ /// Set exclusively by when this instruction is appended to a
+ /// . Never cleared on eviction from the current-instructions cache.
+ ///
+ public override CfgBlock? ContainingBlock { get; set; }
+
public List FieldsInOrder { get; } = new();
internal void AddField(FieldWithValue fieldWithValue) {
@@ -191,8 +236,21 @@ public Signature SignatureFinal {
.ToImmutableList();
}
+ ///
+ /// Updates the live state of this instruction.
+ ///
+ ///
+ /// On an actual transition (when differs from the current value),
+ /// notifies the exactly once via
+ /// so the block's non-live
+ /// instruction counter stays in sync. No-op if the live state is unchanged.
+ ///
public void SetLive(bool isLive) {
+ if (_isLive == isLive) {
+ return;
+ }
_isLive = isLive;
+ ContainingBlock?.OnContainedInstructionLiveChanged(isLive);
}
///
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/ParsedInstruction/SelfModifying/SelectorNode.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/ParsedInstruction/SelfModifying/SelectorNode.cs
index b015e9f602..34a17df740 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/ParsedInstruction/SelfModifying/SelectorNode.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/ParsedInstruction/SelfModifying/SelectorNode.cs
@@ -13,9 +13,31 @@ namespace Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.SelfModifying;
/// Node that precedes self modifying code divergence point.
/// To decide what is next node in the graph, the only way is to compare signatures in SuccessorsPerSignature with actual memory content.
///
-public class SelectorNode(SegmentedAddress address) : CfgNode(address, null) {
+public class SelectorNode(int id, SegmentedAddress address) : CfgNode(id, address, null) {
public override bool IsLive => true;
+ ///
+ ///
+ /// A is, by definition, always a block terminator: it dispatches
+ /// between multiple instruction variants at the same address and therefore must end its
+ /// containing .
+ ///
+ public override bool IsBlockTerminator => true;
+
+ ///
+ ///
+ /// A is never a block starter (the explicit starter flag is reserved
+ /// for instructions whose parser tags them as such, e.g. CLI). The base default is also
+ /// false; the override is kept here for clarity and to mirror .
+ ///
+ public override bool IsBlockStarter => false;
+
+ ///
+ ///
+ /// Set exclusively by .
+ ///
+ public override CfgBlock? ContainingBlock { get; set; }
+
public Dictionary SuccessorsPerSignature { get; private set; } =
new();
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/InstructionParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/InstructionParser.cs
index c62638ea00..ab32b0c049 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/InstructionParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/InstructionParser.cs
@@ -6,6 +6,7 @@ namespace Spice86.Core.Emulator.CPU.CfgCpu.Parser;
using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Operations;
using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Value;
using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Visitor;
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.Prefix;
using Spice86.Core.Emulator.CPU.CfgCpu.Parser.SpecificParsers;
@@ -88,8 +89,8 @@ public class InstructionParser {
private readonly XchgRmParser _xchgRmParser;
private readonly XlatParser _xlatParser;
- public InstructionParser(IIndexable memory, State state) {
- _parsingTools = new(memory, state);
+ public InstructionParser(IIndexable memory, State state, CfgNodeIdAllocator idAllocator) {
+ _parsingTools = new(memory, state, idAllocator);
_aluOperationParser = new(_parsingTools);
_bcdAdjustParser = new(_parsingTools);
_bitScanBsfParser = new(_parsingTools, InstructionOperation.BSF, "BitScanForward");
@@ -174,7 +175,7 @@ public CfgInstruction ParseInstructionAt(SegmentedAddress address) {
ValidateLockPrefix(parsed, prefixes);
return parsed;
} catch (CpuException e) {
- CfgInstruction instruction = new(address, opcodeField, prefixes, null) {
+ CfgInstruction instruction = new(_parsingTools.IdAllocator.AllocateId(), address, opcodeField, prefixes, null) {
Kind = InstructionKind.Invalid
};
instruction.AttachAsts(
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/ParsingTools.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/ParsingTools.cs
index c96c894ae2..46d828f066 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/ParsingTools.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/ParsingTools.cs
@@ -1,6 +1,7 @@
namespace Spice86.Core.Emulator.CPU.CfgCpu.Parser;
using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Builder;
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
using Spice86.Core.Emulator.CPU.CfgCpu.Parser.FieldReader;
using Spice86.Core.Emulator.CPU.Registers;
using Spice86.Core.Emulator.Memory.Indexable;
@@ -15,13 +16,15 @@ public class ParsingTools {
public ModRmParser ModRmParser { get; }
public State State { get; }
public AstBuilder AstBuilder { get; }
+ public CfgNodeIdAllocator IdAllocator { get; }
- public ParsingTools(IIndexable memory, State state) {
+ public ParsingTools(IIndexable memory, State state, CfgNodeIdAllocator idAllocator) {
InstructionReader instructionReader = new(memory);
InstructionReader = instructionReader;
InstructionPrefixParser = new(instructionReader);
ModRmParser = new(instructionReader, state);
State = state;
AstBuilder = new AstBuilder();
+ IdAllocator = idAllocator;
}
}
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/AluOperationParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/AluOperationParser.cs
index ba1aee3a16..8a69b9c1f0 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/AluOperationParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/AluOperationParser.cs
@@ -40,7 +40,7 @@ public CfgInstruction Parse(ParsingContext context, int operationIndex) {
bool hasModRm = (opcode & ModRmMask) == 0;
BitWidth bitWidth = GetBitWidth(context.OpcodeField, context.HasOperandSize32);
DataType dataType = _astBuilder.UType(bitWidth);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode destNode;
ValueNode srcNode;
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BaseInstructionParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BaseInstructionParser.cs
index e8d163f1aa..ffe8004f00 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BaseInstructionParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BaseInstructionParser.cs
@@ -5,6 +5,7 @@ namespace Spice86.Core.Emulator.CPU.CfgCpu.Parser.SpecificParsers;
using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Instruction;
using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Operations;
using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Value;
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.ModRm;
using Spice86.Core.Emulator.CPU.CfgCpu.Parser;
@@ -23,12 +24,14 @@ public class BaseInstructionParser {
protected readonly ModRmParser _modRmParser;
protected readonly State _state;
protected readonly AstBuilder _astBuilder;
+ protected readonly CfgNodeIdAllocator _idAllocator;
protected BaseInstructionParser(ParsingTools parsingTools) {
_instructionReader = parsingTools.InstructionReader;
_modRmParser = parsingTools.ModRmParser;
_state = parsingTools.State;
_astBuilder = parsingTools.AstBuilder;
+ _idAllocator = parsingTools.IdAllocator;
}
protected bool HasOperandSize8(ushort opcode) {
@@ -64,7 +67,7 @@ protected void RegisterModRmFields(CfgInstruction instr, ModRmContext modRmConte
///
protected (CfgInstruction instr, ModRmContext modRmContext) ParseModRmBase(ParsingContext context, int maxSuccessors) {
ModRmContext modRmContext = _modRmParser.ParseNext(context);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, maxSuccessors);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, maxSuccessors);
RegisterModRmFields(instr, modRmContext);
return (instr, modRmContext);
}
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BcdAdjustParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BcdAdjustParser.cs
index d52cae10b6..d5ada451f3 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BcdAdjustParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BcdAdjustParser.cs
@@ -18,7 +18,7 @@ public BcdAdjustParser(ParsingTools parsingTools) : base(parsingTools) {
public CfgInstruction ParseDecimalAdjust(ParsingContext context, BinaryOperation adjustOp, InstructionOperation displayOp) {
bool isSubtract = adjustOp == BinaryOperation.MINUS;
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode al = _astBuilder.Register.Reg8(RegisterIndex.AxIndex);
VariableDeclarationNode initialAlDeclaration = _astBuilder.DeclareVariable(DataType.UINT8, "initialAl", al);
VariableDeclarationNode initialCfDeclaration = _astBuilder.DeclareVariable(DataType.BOOL, "initialCf", _astBuilder.Flag.Carry());
@@ -77,7 +77,7 @@ public CfgInstruction ParseDecimalAdjust(ParsingContext context, BinaryOperation
}
public CfgInstruction ParseAsciiAdjust(ParsingContext context, BinaryOperation adjustOp, InstructionOperation displayOp) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode al = _astBuilder.Register.Reg8(RegisterIndex.AxIndex);
ValueNode ax = _astBuilder.Register.Reg16(RegisterIndex.AxIndex);
VariableDeclarationNode finalAuxDeclaration = _astBuilder.DeclareVariable(DataType.BOOL, "finalAuxiliaryFlag", _astBuilder.Constant.ToNode(false));
@@ -117,7 +117,7 @@ public CfgInstruction ParseAsciiAdjust(ParsingContext context, BinaryOperation a
}
public CfgInstruction ParseAam(ParsingContext context) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
InstructionField valueField = _instructionReader.UInt8.NextField(false);
instr.AddField(valueField);
ValueNode valueNode = _astBuilder.InstructionField.ToNode(valueField);
@@ -150,7 +150,7 @@ public CfgInstruction ParseAam(ParsingContext context) {
}
public CfgInstruction ParseAad(ParsingContext context) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
InstructionField valueField = _instructionReader.UInt8.NextField(false);
instr.AddField(valueField);
ValueNode valueNode = _astBuilder.InstructionField.ToNode(valueField);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BitTestImmediateParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BitTestImmediateParser.cs
index 08b7fc7a97..77289b4fb7 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BitTestImmediateParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BitTestImmediateParser.cs
@@ -33,7 +33,7 @@ protected override CfgInstruction Parse(ParsingContext context, ModRmContext mod
InstructionField immField = _instructionReader.UInt8.NextField(false);
BitWidth bitWidth = context.DefaultWordOperandBitWidth;
DataType dataType = _astBuilder.UType(bitWidth);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
RegisterModRmFields(instr, modRmContext);
instr.AddField(immField);
ValueNode immNode = _astBuilder.InstructionField.ToNode(immField);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BswapParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BswapParser.cs
index 5b8aa894f3..b5199c3261 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BswapParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/BswapParser.cs
@@ -15,7 +15,7 @@ public BswapParser(ParsingTools parsingTools) : base(parsingTools) {
}
public CfgInstruction Parse(ParsingContext context, int regIndex) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode reg = _astBuilder.Register.Reg(DataType.UINT32, regIndex);
ValueNode swapped = _astBuilder.Bitwise.ByteSwap(reg);
InstructionNode displayAst = new InstructionNode(InstructionOperation.BSWAP, reg);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/CallParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/CallParser.cs
index 4e7a3f52e3..463277398b 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/CallParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/CallParser.cs
@@ -19,7 +19,7 @@ public CfgInstruction ParseCallFarImm(ParsingContext context) {
BitWidth operandBitWidth = context.DefaultWordOperandBitWidth;
if (context.HasOperandSize32) {
InstructionField addrField32 = _instructionReader.SegmentedAddress32.NextField(true);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, null) { Kind = InstructionKind.Call };
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, null) { Kind = InstructionKind.Call };
instr.AddField(addrField32);
SegmentedAddressNode targetAddress = _astBuilder.SegmentedAddressBuilder.ToNode(addrField32);
InstructionNode displayAst = new InstructionNode(InstructionOperation.CALL_FAR, _astBuilder.InstructionField.ToNode(addrField32));
@@ -28,7 +28,7 @@ public CfgInstruction ParseCallFarImm(ParsingContext context) {
return instr;
}
InstructionField addrField = _instructionReader.SegmentedAddress16.NextField(true);
- CfgInstruction instr16 = new(context.Address, context.OpcodeField, context.Prefixes, null) { Kind = InstructionKind.Call };
+ CfgInstruction instr16 = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, null) { Kind = InstructionKind.Call };
instr16.AddField(addrField);
SegmentedAddressNode targetAddress16 = _astBuilder.SegmentedAddressBuilder.ToNode(addrField);
InstructionNode displayAst16 = new InstructionNode(InstructionOperation.CALL_FAR, _astBuilder.InstructionField.ToNode(addrField));
@@ -40,7 +40,7 @@ public CfgInstruction ParseCallFarImm(ParsingContext context) {
public CfgInstruction ParseCallNearImm(ParsingContext context) {
BitWidth offsetWidth = context.DefaultWordOperandBitWidth;
BitWidth operandBitWidth = context.DefaultWordOperandBitWidth;
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, null) { Kind = InstructionKind.Call };
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, null) { Kind = InstructionKind.Call };
(int offsetValue, FieldWithValue offsetField) = ReadSignedOffset(offsetWidth);
instr.AddField(offsetField);
ushort targetIp = (ushort)(instr.NextInMemoryAddress32.Offset + offsetValue);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/CbwParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/CbwParser.cs
index 4259599554..5dd130ec13 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/CbwParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/CbwParser.cs
@@ -17,7 +17,7 @@ public CbwParser(ParsingTools parsingTools) : base(parsingTools) {
public CfgInstruction Parse(ParsingContext context) {
BitWidth bitWidth = GetBitWidth(false, context.HasOperandSize32);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
InstructionOperation displayOp = bitWidth == BitWidth.DWORD_32
? InstructionOperation.CBWE
: InstructionOperation.CBW;
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/CwdParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/CwdParser.cs
index 5cf7585bc9..57e19313d8 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/CwdParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/CwdParser.cs
@@ -17,7 +17,7 @@ public CwdParser(ParsingTools parsingTools) : base(parsingTools) {
public CfgInstruction Parse(ParsingContext context) {
BitWidth bitWidth = GetBitWidth(false, context.HasOperandSize32);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
DataType signedType = _astBuilder.SType(bitWidth);
DataType unsignedType = _astBuilder.UType(bitWidth);
int shiftAmount = (int)bitWidth - 1;
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/EnterParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/EnterParser.cs
index 89570f758d..fcb827d403 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/EnterParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/EnterParser.cs
@@ -18,7 +18,7 @@ public EnterParser(ParsingTools parsingTools) : base(parsingTools) {
public CfgInstruction Parse(ParsingContext context) {
InstructionField storageField = _instructionReader.UInt16.NextField(false);
InstructionField levelField = _instructionReader.UInt8.NextField(false);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
instr.AddField(storageField);
instr.AddField(levelField);
BitWidth bitWidth = GetBitWidth(false, context.HasOperandSize32);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/FlagControlParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/FlagControlParser.cs
index f858c743d7..1eb4f3eabe 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/FlagControlParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/FlagControlParser.cs
@@ -15,7 +15,7 @@ public FlagControlParser(ParsingTools parsingTools) : base(parsingTools) {
}
public CfgInstruction ParseCmc(ParsingContext context) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
CpuFlagNode flagNode = _astBuilder.Flag.Carry();
UnaryOperationNode notFlag = new UnaryOperationNode(DataType.BOOL, UnaryOperation.NOT, flagNode);
BinaryOperationNode flagAssignment = _astBuilder.Assign(DataType.BOOL, flagNode, notFlag);
@@ -26,22 +26,30 @@ public CfgInstruction ParseCmc(ParsingContext context) {
}
public CfgInstruction ParseFlagControl(ParsingContext context, CpuFlagNode flagNode, ulong value, InstructionOperation displayOp) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
BinaryOperationNode flagAssignment = _astBuilder.Assign(DataType.BOOL, flagNode, _astBuilder.Constant.ToNode(DataType.BOOL, value));
InstructionNode displayAst = new InstructionNode(displayOp);
IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, flagAssignment);
instr.AttachAsts(displayAst, execAst);
+ // CLI (opcode 0xFA) must start a new CfgBlock so external interrupt delivery
+ // happens at the boundary just before interrupts are disabled.
+ if (displayOp == InstructionOperation.CLI) {
+ instr.MarkAsBlockStarter();
+ }
return instr;
}
public CfgInstruction ParseSti(ParsingContext context) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
CpuFlagNode flagNode = _astBuilder.Flag.Interrupt();
BinaryOperationNode flagAssignment = _astBuilder.Assign(DataType.BOOL, flagNode, _astBuilder.Constant.ToNode(DataType.BOOL, 1UL));
IVisitableAstNode setInterruptShadowing = _astBuilder.Flag.SetInterruptShadowingIfInterruptDisabled();
InstructionNode displayAst = new InstructionNode(InstructionOperation.STI);
IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, setInterruptShadowing, flagAssignment);
instr.AttachAsts(displayAst, execAst);
+ // STI must terminate its CfgBlock so external interrupt delivery happens at the
+ // boundary just after interrupts are enabled (after the one-instruction shadow).
+ instr.MarkAsBlockTerminator();
return instr;
}
}
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/FlagTransferParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/FlagTransferParser.cs
index 922ca394cd..0570300cee 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/FlagTransferParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/FlagTransferParser.cs
@@ -16,7 +16,7 @@ public FlagTransferParser(ParsingTools parsingTools) : base(parsingTools) {
}
public CfgInstruction ParseSahf(ParsingContext context) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode ah = _astBuilder.Register.Reg8H(RegisterIndex.AxIndex);
ValueNode flags32 = _astBuilder.Flag.FlagsRegister(DataType.UINT32);
ValueNode preservedUpperFlags = new BinaryOperationNode(DataType.UINT32, flags32,
@@ -32,7 +32,7 @@ public CfgInstruction ParseSahf(ParsingContext context) {
}
public CfgInstruction ParseLahf(ParsingContext context) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode ah = _astBuilder.Register.Reg8H(RegisterIndex.AxIndex);
ValueNode flags = _astBuilder.Flag.FlagsRegister(DataType.UINT16);
ValueNode flagsAsByte = _astBuilder.TypeConversion.Convert(DataType.UINT8, flags);
@@ -44,7 +44,7 @@ public CfgInstruction ParseLahf(ParsingContext context) {
}
public CfgInstruction ParseSalc(ParsingContext context) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode al = _astBuilder.Register.Reg8(RegisterIndex.AxIndex);
IfElseNode ternaryAssign = _astBuilder.ControlFlow.TernaryAssign(DataType.UINT8, al,
_astBuilder.Flag.Carry(),
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/Grp1Parser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/Grp1Parser.cs
index 6b96f2207b..7ded81bb23 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/Grp1Parser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/Grp1Parser.cs
@@ -30,7 +30,7 @@ protected override CfgInstruction Parse(ParsingContext context, ModRmContext mod
bool signExtendOp2 = opCode is 0x83;
BitWidth bitWidth = GetBitWidth(context.OpcodeField, context.HasOperandSize32);
DataType dataType = _astBuilder.UType(bitWidth);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
RegisterModRmFields(instr, modRmContext);
ValueNode displayImmNode;
ValueNode execImmNode;
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/Grp2Parser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/Grp2Parser.cs
index 6a51ed2d9e..22d50caf01 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/Grp2Parser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/Grp2Parser.cs
@@ -44,7 +44,7 @@ protected override CfgInstruction Parse(ParsingContext context, ModRmContext mod
(string operation, InstructionOperation displayOp) = Operations[groupIndex];
BitWidth bitWidth = GetBitWidth(context.OpcodeField, context.HasOperandSize32);
DataType dataType = _astBuilder.UType(bitWidth);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
RegisterModRmFields(instr, modRmContext);
ValueNode countNode;
ValueNode displayCountNode;
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/Grp3Parser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/Grp3Parser.cs
index 13146c7f0f..377a8a026e 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/Grp3Parser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/Grp3Parser.cs
@@ -40,7 +40,7 @@ protected override CfgInstruction Parse(ParsingContext context, ModRmContext mod
private CfgInstruction ParseTest(ParsingContext context, ModRmContext modRmContext,
BitWidth bitWidth, DataType dataType) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
RegisterModRmFields(instr, modRmContext);
ValueNode immNode = ReadUnsignedImmediate(instr, bitWidth);
ValueNode rmNode = _astBuilder.ModRm.RmToNode(dataType, modRmContext);
@@ -53,7 +53,7 @@ private CfgInstruction ParseTest(ParsingContext context, ModRmContext modRmConte
private CfgInstruction ParseNot(ParsingContext context, ModRmContext modRmContext,
BitWidth bitWidth, DataType dataType) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
RegisterModRmFields(instr, modRmContext);
ValueNode rmNode = _astBuilder.ModRm.RmToNode(dataType, modRmContext);
UnaryOperationNode notOp = new UnaryOperationNode(dataType, UnaryOperation.BITWISE_NOT, rmNode);
@@ -66,7 +66,7 @@ private CfgInstruction ParseNot(ParsingContext context, ModRmContext modRmContex
private CfgInstruction ParseNeg(ParsingContext context, ModRmContext modRmContext,
BitWidth bitWidth, DataType dataType) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
RegisterModRmFields(instr, modRmContext);
ValueNode rmNode = _astBuilder.ModRm.RmToNode(dataType, modRmContext);
ValueNode zeroNode = _astBuilder.Constant.ToNode(dataType, 0UL);
@@ -87,7 +87,7 @@ private CfgInstruction ParseMulImul(ParsingContext context, ModRmContext modRmCo
DataType unsignedType = _astBuilder.UType(bitWidth);
DataType signedType = isSigned ? _astBuilder.SType(bitWidth) : _astBuilder.UType(bitWidth);
DataType wideType = isSigned ? _astBuilder.SType(bitWidth.Double()) : _astBuilder.UType(bitWidth.Double());
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
RegisterModRmFields(instr, modRmContext);
(ValueNode sourceReg, ValueNode additionalReg) = GetMulDivRegisterNodes(bitWidth);
ValueNode v1 = _astBuilder.TypeConversion.Convert(signedType, sourceReg);
@@ -109,7 +109,7 @@ private CfgInstruction ParseDiv(ParsingContext context, ModRmContext modRmContex
DataType unsignedType = _astBuilder.UType(bitWidth);
DataType signedType = isSigned ? _astBuilder.SType(bitWidth) : _astBuilder.UType(bitWidth);
DataType wideSignedType = isSigned ? _astBuilder.SType(bitWidth.Double()) : _astBuilder.UType(bitWidth.Double());
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
RegisterModRmFields(instr, modRmContext);
(ValueNode lowReg, ValueNode highReg) = GetMulDivRegisterNodes(bitWidth);
ValueNode v1Node;
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/Grp45Parser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/Grp45Parser.cs
index 4ce4cd27bf..7968a554dc 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/Grp45Parser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/Grp45Parser.cs
@@ -55,7 +55,7 @@ private CfgInstruction ParseGrp5(ParsingContext context, ModRmContext modRmConte
private CfgInstruction ParseIncDec(ParsingContext context, ModRmContext modRmContext,
BitWidth bitWidth, string operation, InstructionOperation displayOp) {
DataType dataType = _astBuilder.UType(bitWidth);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
RegisterModRmFields(instr, modRmContext);
ValueNode rmNode = _astBuilder.ModRm.RmToNode(dataType, modRmContext);
MethodCallValueNode aluCall = _astBuilder.AluCall(dataType, bitWidth, operation, rmNode);
@@ -68,7 +68,7 @@ private CfgInstruction ParseIncDec(ParsingContext context, ModRmContext modRmCon
private CfgInstruction ParseCallback(ParsingContext context, ModRmContext modRmContext) {
InstructionField callbackNumber = _instructionReader.UInt16.NextField(true);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, null);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, null);
RegisterModRmFields(instr, modRmContext);
instr.AddField(callbackNumber);
ValueNode callbackNode = _astBuilder.InstructionField.ToNode(callbackNumber);
@@ -83,7 +83,7 @@ private CfgInstruction ParseCallNear(ParsingContext context, ModRmContext modRmC
if (operandBitWidth == BitWidth.DWORD_32) {
dataType = DataType.UINT32;
}
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, null);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, null);
RegisterModRmFields(instr, modRmContext);
ValueNode targetIp = _astBuilder.ModRm.RmToNode(dataType, modRmContext);
if (operandBitWidth == BitWidth.DWORD_32) {
@@ -99,7 +99,7 @@ private CfgInstruction ParseCallNear(ParsingContext context, ModRmContext modRmC
private CfgInstruction ParseCallFar(ParsingContext context, ModRmContext modRmContext, BitWidth operandBitWidth) {
ModRmContext ensuredModRm = _modRmParser.EnsureNotMode3(modRmContext);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, null);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, null);
RegisterModRmFields(instr, ensuredModRm);
InstructionNode displayAst = new InstructionNode(InstructionOperation.CALL_FAR,
_astBuilder.ModRm.ToMemoryAddressNode(DataType.UINT32, ensuredModRm));
@@ -110,7 +110,7 @@ private CfgInstruction ParseCallFar(ParsingContext context, ModRmContext modRmCo
}
private CfgInstruction ParseJumpNear(ParsingContext context, ModRmContext modRmContext) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, null);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, null);
RegisterModRmFields(instr, modRmContext);
// Real mode: indirect jump target is read as 16-bit IP
ValueNode targetIp = _astBuilder.ModRm.RmToNode(DataType.UINT16, modRmContext);
@@ -122,7 +122,7 @@ private CfgInstruction ParseJumpNear(ParsingContext context, ModRmContext modRmC
private CfgInstruction ParseJumpFar(ParsingContext context, ModRmContext modRmContext) {
ModRmContext ensuredModRm = _modRmParser.EnsureNotMode3(modRmContext);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, null);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, null);
RegisterModRmFields(instr, ensuredModRm);
InstructionNode displayAst = new InstructionNode(InstructionOperation.JMP_FAR,
_astBuilder.ModRm.ToMemoryAddressNode(DataType.UINT32, ensuredModRm));
@@ -134,7 +134,7 @@ private CfgInstruction ParseJumpFar(ParsingContext context, ModRmContext modRmCo
private CfgInstruction ParsePush(ParsingContext context, ModRmContext modRmContext, BitWidth bitWidth) {
DataType dataType = _astBuilder.UType(bitWidth);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
RegisterModRmFields(instr, modRmContext);
ValueNode rmNode = _astBuilder.ModRm.RmToNode(dataType, modRmContext);
MethodCallNode pushBlock = _astBuilder.Stack.Push(dataType, rmNode);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/InterruptParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/InterruptParser.cs
index b340399319..d2a96ff47d 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/InterruptParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/InterruptParser.cs
@@ -16,7 +16,7 @@ public InterruptParser(ParsingTools parsingTools) : base(parsingTools) {
}
public CfgInstruction ParseInterrupt3(ParsingContext context) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, null) { Kind = InstructionKind.Call };
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, null) { Kind = InstructionKind.Call };
InstructionNode displayAst = new InstructionNode(InstructionOperation.INT, _astBuilder.Constant.ToNode((byte)3));
IVisitableAstNode execAst = new InterruptCallNode(instr, _astBuilder.Constant.ToNode((byte)3));
instr.AttachAsts(displayAst, execAst);
@@ -24,7 +24,7 @@ public CfgInstruction ParseInterrupt3(ParsingContext context) {
}
public CfgInstruction ParseInterruptWithVector(ParsingContext context) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, null) { Kind = InstructionKind.Call };
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, null) { Kind = InstructionKind.Call };
InstructionField vectorField = _instructionReader.UInt8.NextField(true);
instr.AddField(vectorField);
ValueNode vectorNode = _astBuilder.InstructionField.ToNode(vectorField);
@@ -35,7 +35,7 @@ public CfgInstruction ParseInterruptWithVector(ParsingContext context) {
}
public CfgInstruction ParseInterruptOverflow(ParsingContext context) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, null) { Kind = InstructionKind.Call };
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, null) { Kind = InstructionKind.Call };
ValueNode overflowFlag = _astBuilder.Flag.Overflow();
ValueNode vectorNumber = _astBuilder.Constant.ToNode((byte)4);
InstructionNode displayAst = new InstructionNode(InstructionOperation.INTO);
@@ -45,7 +45,7 @@ public CfgInstruction ParseInterruptOverflow(ParsingContext context) {
}
public CfgInstruction ParseRetInterrupt(ParsingContext context) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, null) { Kind = InstructionKind.Return };
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, null) { Kind = InstructionKind.Return };
instr.EnableCanCauseContextRestore();
InstructionNode displayAst = new InstructionNode(InstructionOperation.IRET);
IVisitableAstNode execAst = new ReturnInterruptNode(instr, context.HasOperandSize32 ? BitWidth.DWORD_32 : BitWidth.WORD_16);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/IoAccDxParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/IoAccDxParser.cs
index 9b231a0776..e3dec2722d 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/IoAccDxParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/IoAccDxParser.cs
@@ -17,7 +17,7 @@ public IoAccDxParser(ParsingTools parsingTools) : base(parsingTools) {
public CfgInstruction Parse(ParsingContext context, bool isInput) {
BitWidth bitWidth = GetBitWidth(context.OpcodeField, context.HasOperandSize32);
DataType dataType = _astBuilder.UType(bitWidth);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode dx = _astBuilder.Register.Reg16(RegisterIndex.DxIndex);
ValueNode accumulator = _astBuilder.Register.Accumulator(dataType);
(InstructionNode displayAst, IVisitableAstNode execAst) = BuildIoAsts(instr, dataType, dx, accumulator, isInput);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/IoAccImmParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/IoAccImmParser.cs
index d2756c7ce4..36e5ebd404 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/IoAccImmParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/IoAccImmParser.cs
@@ -17,7 +17,7 @@ public CfgInstruction Parse(ParsingContext context, bool isInput) {
BitWidth bitWidth = GetBitWidth(context.OpcodeField, context.HasOperandSize32);
DataType dataType = _astBuilder.UType(bitWidth);
InstructionField immField = _instructionReader.UInt8.NextField(false);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
instr.AddField(immField);
ValueNode portNode = _astBuilder.InstructionField.ToNode(immField);
ValueNode accumulator = _astBuilder.Register.Accumulator(dataType);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/IoStringParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/IoStringParser.cs
index ed5bb6d986..a5eca43d21 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/IoStringParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/IoStringParser.cs
@@ -21,7 +21,7 @@ public IoStringParser(ParsingTools parsingTools) : base(parsingTools) {
public CfgInstruction Parse(ParsingContext context, bool isInput) {
BitWidth bitWidth = GetBitWidth(context.OpcodeField, context.HasOperandSize32);
DataType dataType = _astBuilder.UType(bitWidth);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
DataType addressType = _astBuilder.AddressType(instr);
ValueNode dx = _astBuilder.Register.Reg16(RegisterIndex.DxIndex);
BlockNode coreOperation;
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/JccParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/JccParser.cs
index 36c37c5379..a89d5f33b1 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/JccParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/JccParser.cs
@@ -63,7 +63,7 @@ private CfgInstruction ParseJcc(ParsingContext context, int conditionCode, BitWi
(int offsetValue, FieldWithValue offsetField) = ReadSignedOffset(offsetWidth);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1) { Kind = InstructionKind.Jump };
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1) { Kind = InstructionKind.Jump };
instr.AddField(offsetField);
instr.MaxSuccessorsCount = 2;
ushort targetIp = (ushort)(instr.NextInMemoryAddress32.Offset + offsetValue);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/JcxzParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/JcxzParser.cs
index 2e84570853..e89724866c 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/JcxzParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/JcxzParser.cs
@@ -18,7 +18,7 @@ public JcxzParser(ParsingTools parsingTools) : base(parsingTools) {
public CfgInstruction Parse(ParsingContext context) {
(int offsetValue, FieldWithValue offsetField) = ReadSignedOffset(BitWidth.BYTE_8);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1) { Kind = InstructionKind.Jump };
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1) { Kind = InstructionKind.Jump };
instr.AddField(offsetField);
instr.MaxSuccessorsCount = 2;
ushort targetIp = (ushort)(instr.NextInMemoryAddress32.Offset + offsetValue);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/JmpParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/JmpParser.cs
index d9126aa49f..7706c7cef3 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/JmpParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/JmpParser.cs
@@ -25,7 +25,7 @@ public CfgInstruction ParseJmpNearImm8(ParsingContext context) {
}
public CfgInstruction ParseJmpFarImm(ParsingContext context) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1) { Kind = InstructionKind.Jump };
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1) { Kind = InstructionKind.Jump };
SegmentedAddress targetAddress;
if (context.HasOperandSize32) {
InstructionField addrField32 = _instructionReader.SegmentedAddress32.NextField(true);
@@ -44,7 +44,7 @@ public CfgInstruction ParseJmpFarImm(ParsingContext context) {
}
private CfgInstruction ParseJmpNear(ParsingContext context, BitWidth offsetWidth, InstructionOperation displayOp) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1) { Kind = InstructionKind.Jump };
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1) { Kind = InstructionKind.Jump };
(int offsetValue, FieldWithValue offsetField) = ReadSignedOffset(offsetWidth);
instr.AddField(offsetField);
// Real mode: jump target is truncated to 16-bit IP
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/LeaveParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/LeaveParser.cs
index f083962261..c96603c6d9 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/LeaveParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/LeaveParser.cs
@@ -18,7 +18,7 @@ public LeaveParser(ParsingTools parsingTools) : base(parsingTools) {
public CfgInstruction Parse(ParsingContext context) {
BitWidth bitWidth = GetBitWidth(false, context.HasOperandSize32);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, _astBuilder.Stack.Leave(bitWidth));
InstructionNode displayAst = new InstructionNode(bitWidth == BitWidth.DWORD_32 ? InstructionOperation.LEAVEW : InstructionOperation.LEAVE);
instr.AttachAsts(displayAst, execAst);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/LoopParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/LoopParser.cs
index 8c7688228e..fe7b7a04fd 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/LoopParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/LoopParser.cs
@@ -31,7 +31,7 @@ private CfgInstruction ParseLoop(ParsingContext context, InstructionOperation di
BitWidth addressWidth = context.AddressWidthFromPrefixes;
(int offsetValue, FieldWithValue offsetField) = ReadSignedOffset(BitWidth.BYTE_8);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1) { Kind = InstructionKind.Jump };
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1) { Kind = InstructionKind.Jump };
instr.AddField(offsetField);
instr.MaxSuccessorsCount = 2;
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MemoryStringOpParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MemoryStringOpParser.cs
index db2e693607..4320e52337 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MemoryStringOpParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MemoryStringOpParser.cs
@@ -30,7 +30,7 @@ public MemoryStringOpParser(ParsingTools parsingTools) : base(parsingTools) {
public CfgInstruction Parse(ParsingContext context, MemoryStringOpKind kind) {
BitWidth bitWidth = GetBitWidth(context.OpcodeField, context.HasOperandSize32);
DataType dataType = _astBuilder.UType(bitWidth);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
DataType addressType = _astBuilder.AddressType(instr);
bool usesEqualityRep = kind is MemoryStringOpKind.Cmps or MemoryStringOpKind.Scas;
RepPrefix? repPrefix = _astBuilder.Rep(instr.RepPrefix, usesEqualityRep);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MovMoffsParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MovMoffsParser.cs
index 428c27eceb..fc79231146 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MovMoffsParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MovMoffsParser.cs
@@ -18,7 +18,7 @@ public CfgInstruction Parse(ParsingContext context, bool isLoad) {
BitWidth bitWidth = GetBitWidth(context.OpcodeField, context.HasOperandSize32);
DataType dataType = _astBuilder.UType(bitWidth);
int segmentRegisterIndex = GetSegmentRegisterOverrideOrDs(context);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode offsetNode = ReadUnsignedImmediate(instr, context.AddressWidthFromPrefixes);
ValueNode accNode = _astBuilder.Register.Accumulator(dataType);
ValueNode memoryPointer = _astBuilder.Pointer.ToSegmentedPointer(dataType, segmentRegisterIndex, offsetNode);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MovRegImmParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MovRegImmParser.cs
index 34a052c36c..467af45232 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MovRegImmParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/MovRegImmParser.cs
@@ -16,7 +16,7 @@ public MovRegImmParser(ParsingTools parsingTools) : base(parsingTools) {
public CfgInstruction Parse(ParsingContext context, int regIndex, bool is8Bit) {
BitWidth bitWidth = GetBitWidth(is8Bit, context.HasOperandSize32);
DataType dataType = _astBuilder.UType(bitWidth);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode immNode = ReadUnsignedImmediate(instr, bitWidth);
ValueNode regNode = _astBuilder.Register.Reg(dataType, regIndex);
InstructionNode displayAst = new InstructionNode(InstructionOperation.MOV, regNode, immNode);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PopFParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PopFParser.cs
index 86b27e7db7..31c8bf1d6d 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PopFParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PopFParser.cs
@@ -17,13 +17,15 @@ public PopFParser(ParsingTools parsingTools) : base(parsingTools) {
public CfgInstruction Parse(ParsingContext context) {
BitWidth bitWidth = GetBitWidth(false, context.HasOperandSize32);
DataType dataType = _astBuilder.UType(bitWidth);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode popValue = _astBuilder.Stack.Pop(bitWidth);
ValueNode flagsRegister = _astBuilder.Flag.FlagsRegister(dataType);
BinaryOperationNode assign = new BinaryOperationNode(dataType, flagsRegister, BinaryOperation.ASSIGN, popValue);
InstructionNode displayAst = new InstructionNode(bitWidth == BitWidth.DWORD_32 ? InstructionOperation.POPFD : InstructionOperation.POPF);
IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr, assign);
instr.AttachAsts(displayAst, execAst);
+ instr.MarkAsBlockStarter();
+ instr.MarkAsBlockTerminator();
return instr;
}
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PopaParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PopaParser.cs
index fd5f0ea0cf..a08b41e461 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PopaParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PopaParser.cs
@@ -14,7 +14,7 @@ public PopaParser(ParsingTools parsingTools) : base(parsingTools) {
public CfgInstruction Parse(ParsingContext context) {
BitWidth bitWidth = GetBitWidth(false, context.HasOperandSize32);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
string methodName = bitWidth == BitWidth.DWORD_32 ? nameof(Stack.PopAll32) : nameof(Stack.PopAll16);
MethodCallNode popAll = new("Stack", methodName);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PushFParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PushFParser.cs
index c6f51554fa..7d79544929 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PushFParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PushFParser.cs
@@ -16,7 +16,7 @@ public PushFParser(ParsingTools parsingTools) : base(parsingTools) {
public CfgInstruction Parse(ParsingContext context) {
BitWidth bitWidth = GetBitWidth(false, context.HasOperandSize32);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
InstructionNode displayAst;
IVisitableAstNode execAst;
if (bitWidth == BitWidth.DWORD_32) {
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PushImmParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PushImmParser.cs
index 41da31bbf1..20356e006c 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PushImmParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PushImmParser.cs
@@ -17,7 +17,7 @@ public PushImmParser(ParsingTools parsingTools) : base(parsingTools) {
public CfgInstruction Parse(ParsingContext context, bool imm8SignExtended) {
BitWidth bitWidth = GetBitWidth(false, context.HasOperandSize32);
DataType dataType = _astBuilder.UType(bitWidth);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode displayImmNode;
ValueNode pushValueNode;
if (imm8SignExtended) {
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PushaParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PushaParser.cs
index d9ba3eb19c..b7471374b9 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PushaParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/PushaParser.cs
@@ -18,7 +18,7 @@ public CfgInstruction Parse(ParsingContext context) {
BitWidth bitWidth = GetBitWidth(false, context.HasOperandSize32);
DataType dataType = _astBuilder.UType(bitWidth);
DataType addressType = _astBuilder.UType(BitWidth.WORD_16);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
string methodName = bitWidth == BitWidth.DWORD_32 ? nameof(Stack.PushAll32) : nameof(Stack.PushAll16);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/RegisterOpParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/RegisterOpParser.cs
index 51b4433272..72d65c610b 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/RegisterOpParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/RegisterOpParser.cs
@@ -18,7 +18,7 @@ public RegisterOpParser(ParsingTools parsingTools) : base(parsingTools) {
public CfgInstruction ParseIncDecReg(ParsingContext context, int regIndex, string aluOperation, InstructionOperation displayOp) {
BitWidth bitWidth = context.DefaultWordOperandBitWidth;
DataType dataType = _astBuilder.UType(bitWidth);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode regNode = _astBuilder.Register.Reg(dataType, regIndex);
MethodCallValueNode aluCall = _astBuilder.AluCall(dataType, bitWidth, aluOperation, regNode);
InstructionNode displayAst = new InstructionNode(displayOp, regNode);
@@ -30,7 +30,7 @@ public CfgInstruction ParseIncDecReg(ParsingContext context, int regIndex, strin
public CfgInstruction ParsePushReg(ParsingContext context, int regIndex) {
BitWidth bitWidth = context.DefaultWordOperandBitWidth;
DataType dataType = _astBuilder.UType(bitWidth);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode regNode = _astBuilder.Register.Reg(dataType, regIndex);
MethodCallNode pushBlock = _astBuilder.Stack.Push(dataType, regNode);
InstructionNode displayAst = new InstructionNode(InstructionOperation.PUSH, regNode);
@@ -42,7 +42,7 @@ public CfgInstruction ParsePushReg(ParsingContext context, int regIndex) {
public CfgInstruction ParsePopReg(ParsingContext context, int regIndex) {
BitWidth bitWidth = context.DefaultWordOperandBitWidth;
DataType dataType = _astBuilder.UType(bitWidth);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode regNode = _astBuilder.Register.Reg(dataType, regIndex);
ValueNode popValue = _astBuilder.Stack.Pop(bitWidth);
BinaryOperationNode assign = new BinaryOperationNode(dataType, regNode, BinaryOperation.ASSIGN, popValue);
@@ -55,7 +55,7 @@ public CfgInstruction ParsePopReg(ParsingContext context, int regIndex) {
public CfgInstruction ParseXchgRegAcc(ParsingContext context, int regIndex) {
BitWidth bitWidth = context.DefaultWordOperandBitWidth;
DataType dataType = _astBuilder.UType(bitWidth);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode regNode = _astBuilder.Register.Reg(dataType, regIndex);
ValueNode accNode = _astBuilder.Register.Accumulator(dataType);
VariableDeclarationNode tempDecl = _astBuilder.DeclareVariable(dataType, "temp", regNode);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/ReturnParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/ReturnParser.cs
index 456f77c226..ec2a77f169 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/ReturnParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/ReturnParser.cs
@@ -28,7 +28,7 @@ public CfgInstruction ParseRetFar(ParsingContext context, bool hasImm) {
private CfgInstruction ParseRet(ParsingContext context, bool hasImm, InstructionOperation operation,
Func execNodeFactory) {
BitWidth operandBitWidth = context.DefaultWordOperandBitWidth;
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, null) { Kind = InstructionKind.Return };
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, null) { Kind = InstructionKind.Return };
ValueNode bytesToPop;
InstructionNode displayAst;
if (hasImm) {
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SegRegPushPopParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SegRegPushPopParser.cs
index 781704bced..50108659bf 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SegRegPushPopParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SegRegPushPopParser.cs
@@ -16,7 +16,7 @@ public SegRegPushPopParser(ParsingTools parsingTools) : base(parsingTools) {
}
public CfgInstruction ParsePushSReg(ParsingContext context, int segRegIndex) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode regNode = _astBuilder.Register.SReg(segRegIndex);
DataType pushType = context.HasOperandSize32 ? DataType.UINT32 : DataType.UINT16;
ValueNode pushValue = _astBuilder.TypeConversion.Convert(pushType, regNode);
@@ -28,7 +28,7 @@ public CfgInstruction ParsePushSReg(ParsingContext context, int segRegIndex) {
}
public CfgInstruction ParsePopSReg(ParsingContext context, int segRegIndex) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode regNode = _astBuilder.Register.SReg(segRegIndex);
DataType addressType = DataType.UINT16;
ushort slotSize = context.HasOperandSize32 ? (ushort)4 : (ushort)2;
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SimpleInstructionParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SimpleInstructionParser.cs
index 88e4ef86a9..6d0f6a5984 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SimpleInstructionParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/SimpleInstructionParser.cs
@@ -14,7 +14,7 @@ public SimpleInstructionParser(ParsingTools parsingTools) : base(parsingTools) {
}
public CfgInstruction ParseHlt(ParsingContext context) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 0);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 0);
InstructionNode displayAst = new InstructionNode(InstructionOperation.HLT);
IVisitableAstNode execAst = new HltNode(instr);
instr.AttachAsts(displayAst, execAst);
@@ -22,7 +22,7 @@ public CfgInstruction ParseHlt(ParsingContext context) {
}
public CfgInstruction ParseNop(ParsingContext context) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
InstructionNode displayAst = new InstructionNode(InstructionOperation.NOP);
IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr);
instr.AttachAsts(displayAst, execAst);
@@ -30,7 +30,7 @@ public CfgInstruction ParseNop(ParsingContext context) {
}
public CfgInstruction ParseFwait(ParsingContext context) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
InstructionNode displayAst = new InstructionNode(InstructionOperation.FWAIT);
IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr);
instr.AttachAsts(displayAst, execAst);
@@ -38,7 +38,7 @@ public CfgInstruction ParseFwait(ParsingContext context) {
}
public CfgInstruction ParseCpuid(ParsingContext context) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
InstructionNode displayAst = new InstructionNode(InstructionOperation.CPUID);
IVisitableAstNode execAst = new CpuidNode(instr);
instr.AttachAsts(displayAst, execAst);
@@ -50,7 +50,7 @@ public CfgInstruction ParseCpuid(ParsingContext context) {
/// Encoded as a 2-byte 0F 06 instruction.
///
public CfgInstruction ParseClts(ParsingContext context) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
InstructionNode displayAst = new InstructionNode(InstructionOperation.CLTS);
IVisitableAstNode execAst = _astBuilder.WithIpAdvancement(instr);
instr.AttachAsts(displayAst, execAst);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/TestAccImmParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/TestAccImmParser.cs
index a79c562f54..de39029483 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/TestAccImmParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/TestAccImmParser.cs
@@ -17,7 +17,7 @@ public TestAccImmParser(ParsingTools parsingTools) : base(parsingTools) {
public CfgInstruction Parse(ParsingContext context) {
BitWidth bitWidth = GetBitWidth(context.OpcodeField, context.HasOperandSize32);
DataType dataType = _astBuilder.UType(bitWidth);
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
ValueNode immNode = ReadUnsignedImmediate(instr, bitWidth);
ValueNode accNode = _astBuilder.Register.Accumulator(dataType);
MethodCallValueNode aluCall = _astBuilder.AluCall(dataType, bitWidth, "And", accNode, immNode);
diff --git a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/XlatParser.cs b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/XlatParser.cs
index 08ad283655..e139f6e733 100644
--- a/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/XlatParser.cs
+++ b/src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/XlatParser.cs
@@ -16,7 +16,7 @@ public XlatParser(ParsingTools parsingTools) : base(parsingTools) {
}
public CfgInstruction Parse(ParsingContext context) {
- CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
+ CfgInstruction instr = new(_idAllocator.AllocateId(), context.Address, context.OpcodeField, context.Prefixes, 1);
int segRegIndex = GetSegmentRegisterOverrideOrDs(context);
int defaultSegRegIndex = (int)SegmentRegisterIndex.DsIndex;
DataType addrType = _astBuilder.AddressType(instr);
diff --git a/src/Spice86.Core/Emulator/Mcp/EmulatorMcpServices.cs b/src/Spice86.Core/Emulator/Mcp/EmulatorMcpServices.cs
index 4b6fadd657..7bf334cddd 100644
--- a/src/Spice86.Core/Emulator/Mcp/EmulatorMcpServices.cs
+++ b/src/Spice86.Core/Emulator/Mcp/EmulatorMcpServices.cs
@@ -17,6 +17,7 @@ namespace Spice86.Core.Emulator.Mcp;
using Spice86.Core.Emulator.IOPorts;
using Spice86.Core.Emulator.Memory;
using Spice86.Core.Emulator.OperatingSystem;
+using Spice86.Core.Emulator.StateSerialization;
using Spice86.Core.Emulator.VM;
using Spice86.Core.Emulator.VM.Breakpoint;
using Spice86.Shared.Interfaces;
@@ -38,9 +39,12 @@ public sealed class EmulatorMcpServices(
ExpandedMemoryManager? emsManager,
ExtendedMemoryManager? xmsManager,
EmulatorBreakpointsManager breakpointsManager,
+ CfgBlocksJsonExporter cfgBlocksExporter,
ILoggerService loggerService) {
public IMemory Memory { get; } = memory;
+ public CfgBlocksJsonExporter CfgBlocksExporter { get; } = cfgBlocksExporter;
+
public State State { get; } = state;
public FunctionCatalogue FunctionCatalogue { get; } = functionCatalogue;
diff --git a/src/Spice86.Core/Emulator/Mcp/EmulatorMcpTools.cs b/src/Spice86.Core/Emulator/Mcp/EmulatorMcpTools.cs
index 70a19ac5d1..9bc1e7d37e 100644
--- a/src/Spice86.Core/Emulator/Mcp/EmulatorMcpTools.cs
+++ b/src/Spice86.Core/Emulator/Mcp/EmulatorMcpTools.cs
@@ -242,7 +242,7 @@ public CallToolResult ReadDisassembly(ushort segment, ushort offset, int instruc
throw new InvalidOperationException("instructionCount must be between 1 and 500");
}
- InstructionParser parser = new(_services.Memory, _services.State);
+ InstructionParser parser = new(_services.Memory, _services.State, new CfgNodeIdAllocator());
AstInstructionRenderer renderer = new(AsmRenderingConfig.CreateSpice86Style());
List lines = new();
SegmentedAddress current = new(segment, offset);
@@ -464,81 +464,22 @@ public CallToolResult ListFunctions(int? limit) {
});
}
- [McpServerTool(Name = "read_cfg_cpu_graph", UseStructuredContent = true), Description("Read the Control Flow Graph built by the CPU. Returns execution context depth, entry point addresses, last executed address, and graph nodes with successor/predecessor edges for each instruction. Use nodeLimit to cap BFS traversal; omit or pass null for the full graph.")]
+ [McpServerTool(Name = "read_cfg_cpu_graph", UseStructuredContent = true), Description("Read the CFG as compact basic blocks. Returns context depth, entry points as 'SSSS:OOOO' strings, last executed address/block id, and blocks reachable via BFS. Each block: {id, entry, term, pred, succ, asm} where asm is a string[] of compact entries. Each instruction entry is a single string 'BYTES|assembly' (e.g. 'B83412|mov AX,0x1234') where BYTES is the hex-encoded instruction bytes and assembly is the disassembly text. A selector entry is the single string 'selector': it marks a self-modifying code boundary where multiple instruction variants exist at the block's term address. Optional dead/incomplete flags. Use nodeLimit to cap blocks; truncated indicates cap hit.")]
public CallToolResult ReadCfgCpuGraph(int? nodeLimit) {
return ExecuteTool(() => {
lock (_services.ToolsLock) {
- ExecutionContextManager contextManager = _services.CfgCpu.ExecutionContextManager;
- ExecutionContext currentContext = contextManager.CurrentExecutionContext;
-
- SegmentedAddress[] entryPointAddresses = contextManager.ExecutionContextEntryPoints
- .Select(kvp => kvp.Key)
- .ToArray();
-
- ISet entryNodes = new HashSet();
- foreach (ISet instructions in contextManager.ExecutionContextEntryPoints.Values) {
- foreach (ICfgNode node in instructions) {
- entryNodes.Add(node);
- }
- }
- if (currentContext.LastExecuted != null) {
- entryNodes.Add(currentContext.LastExecuted);
- }
-
- int? effectiveLimit = nodeLimit.HasValue && nodeLimit.Value > 0 ? nodeLimit.Value : null;
- (CfgNodeInfo[] nodes, bool truncated) = CollectGraphNodes(entryNodes, effectiveLimit);
-
- return new CfgCpuGraphResponse {
- CurrentContextDepth = currentContext.Depth,
- CurrentContextEntryPoint = currentContext.EntryPoint,
- TotalEntryPoints = entryPointAddresses.Length,
- EntryPointAddresses = entryPointAddresses,
- LastExecutedAddress = currentContext.LastExecuted?.Address,
- Nodes = nodes,
- Truncated = truncated
- };
+ return _services.CfgBlocksExporter.BuildGraph(
+ _services.CfgCpu.ExecutionContextManager,
+ NormalizeNodeLimit(nodeLimit));
}
});
}
- private static (CfgNodeInfo[] Nodes, bool Truncated) CollectGraphNodes(ISet seeds, int? limit) {
- HashSet visited = new();
- Queue queue = new();
- List result = new();
-
- foreach (ICfgNode seed in seeds) {
- if (visited.Add(seed.Id)) {
- queue.Enqueue(seed);
- }
+ private static int? NormalizeNodeLimit(int? nodeLimit) {
+ if (nodeLimit.HasValue && nodeLimit.Value > 0) {
+ return nodeLimit.Value;
}
-
- while (queue.Count > 0) {
- if (limit.HasValue && result.Count >= limit.Value) {
- return (result.ToArray(), true);
- }
-
- ICfgNode current = queue.Dequeue();
- result.Add(new CfgNodeInfo {
- Id = current.Id,
- Address = current.Address,
- SuccessorIds = current.Successors.Select(s => s.Id).ToArray(),
- PredecessorIds = current.Predecessors.Select(p => p.Id).ToArray(),
- IsLive = current.IsLive
- });
-
- foreach (ICfgNode successor in current.Successors) {
- if (visited.Add(successor.Id)) {
- queue.Enqueue(successor);
- }
- }
- foreach (ICfgNode predecessor in current.Predecessors) {
- if (visited.Add(predecessor.Id)) {
- queue.Enqueue(predecessor);
- }
- }
- }
-
- return (result.ToArray(), false);
+ return null;
}
[McpServerTool(Name = "read_io_port", UseStructuredContent = true), Description("Read a byte from an x86 IO port (0-65535). Returns the port number and byte value.")]
@@ -1391,24 +1332,21 @@ public CallToolResult Go() {
}
[McpManualControl]
- [McpServerTool(Name = "step", UseStructuredContent = true), Description("Execute exactly one CPU instruction and then pause. Returns the updated CPU state (registers, IP, flags, cycles) after the step.")]
+ [McpServerTool(Name = "step", UseStructuredContent = true), Description("Step into: execute exactly one CPU instruction then pause. Returns immediately; call read_cpu_state to inspect post-step state.")]
public CallToolResult Step() {
return ExecuteTool(() => {
lock (_services.ToolsLock) {
if (!_services.PauseHandler.IsPaused) {
_services.PauseHandler.RequestPause("Step requested while running");
if (!WaitUntilPaused(StepCompletionTimeout)) {
- throw new InvalidOperationException("Could not pause emulator before stepping.");
+ throw new InvalidOperationException("Timed out waiting to pause before stepping.");
}
}
-
- _services.CfgCpu.ExecuteNext();
-
- return new StepResponse {
- Success = true,
- Message = "Step completed",
- CpuState = CpuStateSnapshot.FromState(_services.State)
- };
+ DebuggerStepHelper.SetupStepIntoBreakpoint(
+ _services.BreakpointsManager, _services.State,
+ () => _services.PauseHandler.RequestPause("Step completed"));
+ _services.PauseHandler.Resume();
+ return new EmulatorControlResponse { Success = true, Message = "Step initiated" };
}
});
}
@@ -1564,6 +1502,9 @@ public CallToolResult StepOver(uint nextAddress, bool isCallOrInterrupt) {
lock (_services.ToolsLock) {
if (!_services.PauseHandler.IsPaused) {
_services.PauseHandler.RequestPause("Step over requested while running");
+ if (!WaitUntilPaused(StepCompletionTimeout)) {
+ throw new InvalidOperationException("Timed out waiting to pause before stepping over.");
+ }
}
if (isCallOrInterrupt) {
DebuggerStepHelper.SetupStepOverBreakpoint(_services.BreakpointsManager, nextAddress,
diff --git a/src/Spice86.Core/Emulator/Mcp/Response/CfgCpuGraphResponse.cs b/src/Spice86.Core/Emulator/Mcp/Response/CfgCpuGraphResponse.cs
deleted file mode 100644
index c975e739d9..0000000000
--- a/src/Spice86.Core/Emulator/Mcp/Response/CfgCpuGraphResponse.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-namespace Spice86.Core.Emulator.Mcp.Response;
-
-using Spice86.Shared.Emulator.Memory;
-
-internal sealed record CfgCpuGraphResponse {
- public required int CurrentContextDepth { get; init; }
-
- public required SegmentedAddress CurrentContextEntryPoint { get; init; }
-
- public required int TotalEntryPoints { get; init; }
-
- public required SegmentedAddress[] EntryPointAddresses { get; init; }
-
- public required SegmentedAddress? LastExecutedAddress { get; init; }
-
- public required CfgNodeInfo[] Nodes { get; init; }
-
- public required bool Truncated { get; init; }
-}
diff --git a/src/Spice86.Core/Emulator/Mcp/Response/CfgNodeInfo.cs b/src/Spice86.Core/Emulator/Mcp/Response/CfgNodeInfo.cs
deleted file mode 100644
index b37f0d6b35..0000000000
--- a/src/Spice86.Core/Emulator/Mcp/Response/CfgNodeInfo.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace Spice86.Core.Emulator.Mcp.Response;
-
-using Spice86.Shared.Emulator.Memory;
-
-internal sealed record CfgNodeInfo {
- public required int Id { get; init; }
-
- public required SegmentedAddress Address { get; init; }
-
- public required int[] SuccessorIds { get; init; }
-
- public required int[] PredecessorIds { get; init; }
-
- public required bool IsLive { get; init; }
-}
diff --git a/src/Spice86.Core/Emulator/Mcp/Response/StepResponse.cs b/src/Spice86.Core/Emulator/Mcp/Response/StepResponse.cs
deleted file mode 100644
index 88e5b65b30..0000000000
--- a/src/Spice86.Core/Emulator/Mcp/Response/StepResponse.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Spice86.Core.Emulator.Mcp.Response;
-
-internal sealed record StepResponse {
- public required bool Success { get; init; }
-
- public required string Message { get; init; }
-
- public required CpuStateSnapshot CpuState { get; init; }
-}
diff --git a/src/Spice86.Core/Emulator/StateSerialization/CfgBlockInfo.cs b/src/Spice86.Core/Emulator/StateSerialization/CfgBlockInfo.cs
new file mode 100644
index 0000000000..b07fc6f8d3
--- /dev/null
+++ b/src/Spice86.Core/Emulator/StateSerialization/CfgBlockInfo.cs
@@ -0,0 +1,21 @@
+namespace Spice86.Core.Emulator.StateSerialization;
+
+using System.Text.Json.Serialization;
+
+///
+/// Compact json serialization of a single CfgBlock.
+///
+internal sealed record CfgBlockInfo {
+ [JsonPropertyName("id")] public required int Id { get; init; }
+ [JsonPropertyName("entry")] public required string Entry { get; init; }
+ // Inverted from IsLive: null (omitted) means live, true means dead. Most blocks are live,
+ // so this avoids emitting the field for the common case and saves LLM context tokens.
+ [JsonPropertyName("dead")] public bool? Dead { get; init; }
+ // Inverted from IsDiscoveryComplete: null (omitted) means complete, true means incomplete.
+ // Most blocks are fully discovered, so this avoids emitting the field for the common case.
+ [JsonPropertyName("incomplete")] public bool? Incomplete { get; init; }
+ [JsonPropertyName("term")] public required string Term { get; init; }
+ [JsonPropertyName("pred")] public required int[] Pred { get; init; }
+ [JsonPropertyName("succ")] public required int[] Succ { get; init; }
+ [JsonPropertyName("asm")] public required string[] Asm { get; init; }
+}
diff --git a/src/Spice86.Core/Emulator/StateSerialization/CfgBlocksJsonExporter.cs b/src/Spice86.Core/Emulator/StateSerialization/CfgBlocksJsonExporter.cs
new file mode 100644
index 0000000000..28f991570c
--- /dev/null
+++ b/src/Spice86.Core/Emulator/StateSerialization/CfgBlocksJsonExporter.cs
@@ -0,0 +1,128 @@
+namespace Spice86.Core.Emulator.StateSerialization;
+
+using Spice86.Core.Emulator.CPU.CfgCpu;
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
+using Spice86.Core.Emulator.CPU.CfgCpu.InstructionRenderer;
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.SelfModifying;
+using Spice86.Core.Emulator.StateSerialization.ControlFlow;
+
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+///
+/// JSON serialization adapter for the CFG block graph. Delegates traversal to
+/// and converts the exported graph into the
+/// / wire model used for
+/// on-disk dumps and MCP responses.
+///
+public class CfgBlocksJsonExporter {
+ private static readonly JsonSerializerOptions SerializerOptions = new() {
+ WriteIndented = true,
+ Converters = { new JsonStringEnumConverter() },
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ };
+
+ private readonly AstInstructionRenderer _renderer;
+ private readonly CfgBlockGraphExporter _graphExporter;
+
+ ///
+ /// Creates an exporter with a dedicated graph exporter instance.
+ ///
+ public CfgBlocksJsonExporter(CfgBlockGraphExporter graphExporter) {
+ _renderer = new AstInstructionRenderer(AsmRenderingConfig.CreateSpice86Style());
+ _graphExporter = graphExporter;
+ }
+
+ ///
+ /// Builds a from the given execution context manager.
+ /// Stops early if is reached; Truncated is set accordingly.
+ ///
+ internal CfgCpuGraph BuildGraph(ExecutionContextManager contextManager, int? nodeLimit) {
+ CfgExecutionContextGraph exported = _graphExporter.ExportFromExecutionContext(contextManager, nodeLimit);
+ CfgBlockGraph graph = exported.Graph;
+
+ HashSet includedBlockIds = new(graph.Blocks.Select(n => n.Block.Id));
+ CfgBlockInfo[] blocks = graph.Blocks
+ .Select(graphNode => BuildCfgBlockInfo(graphNode.Block, includedBlockIds))
+ .ToArray();
+
+ int? lastExecutedBlockId = null;
+ if (exported.LastExecutedBlock is CfgBlock lastBlock) {
+ lastExecutedBlockId = lastBlock.Id;
+ }
+
+ return new CfgCpuGraph {
+ CurrentContextDepth = exported.CurrentContextDepth,
+ CurrentContextEntryPoint = exported.CurrentContextEntryPoint,
+ TotalEntryPoints = exported.EntryPointAddresses.Length,
+ EntryPointAddresses = exported.EntryPointAddresses,
+ LastExecutedAddress = exported.LastExecuted?.Address.ToString(),
+ LastExecutedBlockId = lastExecutedBlockId,
+ Blocks = blocks,
+ Truncated = graph.Truncated
+ };
+ }
+
+ ///
+ /// Serializes the full block graph to a JSON string.
+ ///
+ internal string ToJson(ExecutionContextManager contextManager) {
+ CfgCpuGraph graph = BuildGraph(contextManager, null);
+ return JsonSerializer.Serialize(graph, SerializerOptions);
+ }
+
+ ///
+ /// Writes the full block graph JSON to a file.
+ ///
+ internal void Write(ExecutionContextManager contextManager, string path) {
+ string json = ToJson(contextManager);
+ File.WriteAllText(path, json);
+ }
+
+ private CfgBlockInfo BuildCfgBlockInfo(CfgBlock block, HashSet includedBlockIds) {
+ List asm = new(block.Instructions.Count);
+ foreach (ICfgNode node in block.Instructions) {
+ switch (node) {
+ case CfgInstruction instruction:
+ string assembly = instruction.DisplayAst.Accept(_renderer);
+ string sigHex = string.Concat(instruction.Signature.SignatureValue.Select(b => b.HasValue ? b.Value.ToString("X2") : "__"));
+ asm.Add($"{sigHex}|{assembly}");
+ break;
+ case SelectorNode:
+ asm.Add("selector");
+ break;
+ }
+ }
+
+ int[] predecessorBlockIds = block.Predecessors
+ .Select(node => node.ContainingBlock)
+ .OfType()
+ .Select(containingBlock => containingBlock.Id)
+ .Where(id => includedBlockIds.Contains(id))
+ .Distinct()
+ .OrderBy(id => id)
+ .ToArray();
+
+ int[] successorBlockIds = block.Successors
+ .Select(node => node.ContainingBlock)
+ .OfType()
+ .Select(containingBlock => containingBlock.Id)
+ .Where(id => includedBlockIds.Contains(id))
+ .Distinct()
+ .OrderBy(id => id)
+ .ToArray();
+
+ return new CfgBlockInfo {
+ Id = block.Id,
+ Entry = block.Entry.Address.ToString(),
+ Dead = !block.IsLive ? true : null,
+ Incomplete = !block.IsDiscoveryComplete ? true : null,
+ Term = block.Terminator.Address.ToString(),
+ Pred = predecessorBlockIds,
+ Succ = successorBlockIds,
+ Asm = asm.ToArray()
+ };
+ }
+}
diff --git a/src/Spice86.Core/Emulator/StateSerialization/CfgCpuGraph.cs b/src/Spice86.Core/Emulator/StateSerialization/CfgCpuGraph.cs
new file mode 100644
index 0000000000..96d807e935
--- /dev/null
+++ b/src/Spice86.Core/Emulator/StateSerialization/CfgCpuGraph.cs
@@ -0,0 +1,20 @@
+namespace Spice86.Core.Emulator.StateSerialization;
+
+using Spice86.Core.Emulator.Mcp.Response;
+
+using System.Text.Json.Serialization;
+
+///
+/// Universal model for the CFG graph with execution-context metadata. Used for both the
+/// on-disk dump and the MCP wire format.
+///
+internal sealed record CfgCpuGraph {
+ [JsonPropertyName("currentContextDepth")] public required int CurrentContextDepth { get; init; }
+ [JsonPropertyName("currentContextEntryPoint")] public required string CurrentContextEntryPoint { get; init; }
+ [JsonPropertyName("totalEntryPoints")] public required int TotalEntryPoints { get; init; }
+ [JsonPropertyName("entryPointAddresses")] public required string[] EntryPointAddresses { get; init; }
+ [JsonPropertyName("lastExecutedAddress")] public string? LastExecutedAddress { get; init; }
+ [JsonPropertyName("lastExecutedBlockId")] public int? LastExecutedBlockId { get; init; }
+ [JsonPropertyName("blocks")] public required CfgBlockInfo[] Blocks { get; init; }
+ [JsonPropertyName("truncated")] public required bool Truncated { get; init; }
+}
diff --git a/src/Spice86.Core/Emulator/StateSerialization/ControlFlow/CfgBlockGraph.cs b/src/Spice86.Core/Emulator/StateSerialization/ControlFlow/CfgBlockGraph.cs
new file mode 100644
index 0000000000..60b9e4b6f2
--- /dev/null
+++ b/src/Spice86.Core/Emulator/StateSerialization/ControlFlow/CfgBlockGraph.cs
@@ -0,0 +1,10 @@
+namespace Spice86.Core.Emulator.StateSerialization.ControlFlow;
+
+///
+/// Pure traversal result shared by both JSON and UI consumers. Contains no execution-context metadata.
+///
+public sealed record CfgBlockGraph {
+ public required CfgBlockGraphNode[] Blocks { get; init; }
+ public required CfgBlockGraphEdge[] Edges { get; init; }
+ public required bool Truncated { get; init; }
+}
diff --git a/src/Spice86.Core/Emulator/StateSerialization/ControlFlow/CfgBlockGraphEdge.cs b/src/Spice86.Core/Emulator/StateSerialization/ControlFlow/CfgBlockGraphEdge.cs
new file mode 100644
index 0000000000..ca508c1007
--- /dev/null
+++ b/src/Spice86.Core/Emulator/StateSerialization/ControlFlow/CfgBlockGraphEdge.cs
@@ -0,0 +1,14 @@
+namespace Spice86.Core.Emulator.StateSerialization.ControlFlow;
+
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
+
+///
+/// Represents one cross-block edge in the exported CFG graph. is the
+/// specific entry already resolved from the source block terminator's successor list during BFS
+/// traversal. UI adapters use it to derive the edge label and color without re-scanning the terminator.
+///
+public sealed record CfgBlockGraphEdge {
+ public required CfgBlock From { get; init; }
+ public required CfgBlock To { get; init; }
+ public required ICfgNode BridgeNode { get; init; }
+}
diff --git a/src/Spice86.Core/Emulator/StateSerialization/ControlFlow/CfgBlockGraphExporter.cs b/src/Spice86.Core/Emulator/StateSerialization/ControlFlow/CfgBlockGraphExporter.cs
new file mode 100644
index 0000000000..01d6ee46ea
--- /dev/null
+++ b/src/Spice86.Core/Emulator/StateSerialization/ControlFlow/CfgBlockGraphExporter.cs
@@ -0,0 +1,165 @@
+namespace Spice86.Core.Emulator.StateSerialization.ControlFlow;
+
+using Spice86.Core.Emulator.CPU.CfgCpu;
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
+using Spice86.Core.Emulator.CPU.CfgCpu.Linker;
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
+
+using System.Linq;
+
+///
+/// Shared CFG block traversal layer. Owns BFS over blocks, seed collection, and edge discovery.
+/// Both JSON serialization and UI rendering consume the exported graph without running their own traversal.
+///
+public sealed class CfgBlockGraphExporter {
+ ///
+ /// Exports a from the given execution context manager,
+ /// seeded by entry-point blocks and the last-executed block.
+ ///
+ public CfgExecutionContextGraph ExportFromExecutionContext(ExecutionContextManager contextManager, int? nodeLimit) {
+ ExecutionContext currentContext = contextManager.CurrentExecutionContext;
+
+ string[] entryPointAddresses = contextManager.ExecutionContextEntryPoints
+ .Select(kvp => kvp.Key.ToString())
+ .ToArray();
+
+ List seeds = CollectSeeds(contextManager);
+
+ ICfgNode? executingNode = contextManager.ExecutingNode;
+ CfgBlock? executingBlock = executingNode?.ContainingBlock;
+
+ CfgBlockGraph graph = TraverseFromSeeds(seeds, executingBlock, nodeLimit);
+
+ ICfgNode? lastExecuted = currentContext.LastExecuted;
+ CfgBlock? lastExecutedBlock = lastExecuted?.ContainingBlock;
+
+ return new CfgExecutionContextGraph {
+ Graph = graph,
+ CurrentContextDepth = currentContext.Depth,
+ CurrentContextEntryPoint = currentContext.EntryPoint.ToString(),
+ EntryPointAddresses = entryPointAddresses,
+ LastExecuted = lastExecuted,
+ LastExecutedBlock = lastExecutedBlock
+ };
+ }
+
+ ///
+ /// Exports a from a specific seed node, for UI navigation or search.
+ ///
+ public CfgBlockGraph ExportFromNode(ICfgNode startNode, ICfgNode? executingNode, int? nodeLimit) {
+ CfgBlock? seedBlock = startNode as CfgBlock ?? startNode.ContainingBlock;
+ if (seedBlock is null) {
+ return new CfgBlockGraph {
+ Blocks = [],
+ Edges = [],
+ Truncated = false
+ };
+ }
+
+ CfgBlock? executingBlock = executingNode?.ContainingBlock;
+ return TraverseFromSeeds([seedBlock], executingBlock, nodeLimit);
+ }
+
+ private static List CollectSeeds(ExecutionContextManager contextManager) {
+ HashSet seenBlockIds = new();
+ List seeds = new();
+
+ foreach (ISet instructions in contextManager.ExecutionContextEntryPoints.Values) {
+ foreach (CfgInstruction instruction in instructions) {
+ CfgBlock? block = instruction.ContainingBlock;
+ if (block != null && seenBlockIds.Add(block.Id)) {
+ seeds.Add(block);
+ }
+ }
+ }
+
+ if (contextManager.CurrentExecutionContext.LastExecuted is { } lastExecuted) {
+ CfgBlock? lastBlock = lastExecuted.ContainingBlock;
+ if (lastBlock != null && seenBlockIds.Add(lastBlock.Id)) {
+ seeds.Add(lastBlock);
+ }
+ }
+
+ return seeds.OrderBy(b => b.Entry.Address.Linear).ThenBy(b => b.Id).ToList();
+ }
+
+ private static CfgBlockGraph TraverseFromSeeds(List seeds, CfgBlock? executingBlock, int? nodeLimit) {
+ HashSet visitedBlockIds = new();
+ HashSet<(int, int)> existingEdgeKeys = new();
+ Queue queue = new();
+ List blocks = new();
+ List edges = new();
+ bool truncated = false;
+
+ foreach (CfgBlock seed in seeds.Where(seed => visitedBlockIds.Add(seed.Id))) {
+ queue.Enqueue(seed);
+ }
+
+ while (queue.Count > 0) {
+ if (nodeLimit.HasValue && blocks.Count >= nodeLimit.Value) {
+ truncated = true;
+ break;
+ }
+
+ CfgBlock block = queue.Dequeue();
+
+ bool isExecutingBlock = executingBlock is not null && block.Id == executingBlock.Id;
+ blocks.Add(new CfgBlockGraphNode {
+ Block = block,
+ IsExecutingBlock = isExecutingBlock
+ });
+
+ // Successor edges
+ foreach (ICfgNode successor in block.Successors) {
+ CfgBlock? successorBlock = successor.ContainingBlock;
+ if (successorBlock is null) {
+ continue;
+ }
+ (int, int) edgeKey = (block.Id, successorBlock.Id);
+ if (existingEdgeKeys.Add(edgeKey)) {
+ edges.Add(new CfgBlockGraphEdge {
+ From = block,
+ To = successorBlock,
+ BridgeNode = successor
+ });
+ }
+ if (visitedBlockIds.Add(successorBlock.Id)) {
+ queue.Enqueue(successorBlock);
+ }
+ }
+
+ // Predecessor edges
+ foreach (CfgBlock? predecessorBlock in block.Predecessors.Select(predecessor => predecessor.ContainingBlock)) {
+ if (predecessorBlock is null) {
+ continue;
+ }
+ (int, int) edgeKey = (predecessorBlock.Id, block.Id);
+ if (existingEdgeKeys.Add(edgeKey)) {
+ edges.Add(new CfgBlockGraphEdge {
+ From = predecessorBlock,
+ To = block,
+ BridgeNode = block.Entry
+ });
+ }
+ if (visitedBlockIds.Add(predecessorBlock.Id)) {
+ queue.Enqueue(predecessorBlock);
+ }
+ }
+ }
+
+ if (queue.Count > 0) {
+ truncated = true;
+ }
+
+ HashSet includedIds = new(blocks.Select(n => n.Block.Id));
+ CfgBlockGraphEdge[] closedEdges = edges
+ .Where(e => includedIds.Contains(e.From.Id) && includedIds.Contains(e.To.Id))
+ .ToArray();
+
+ return new CfgBlockGraph {
+ Blocks = blocks.ToArray(),
+ Edges = closedEdges,
+ Truncated = truncated
+ };
+ }
+}
diff --git a/src/Spice86.Core/Emulator/StateSerialization/ControlFlow/CfgBlockGraphNode.cs b/src/Spice86.Core/Emulator/StateSerialization/ControlFlow/CfgBlockGraphNode.cs
new file mode 100644
index 0000000000..dacbdbdf67
--- /dev/null
+++ b/src/Spice86.Core/Emulator/StateSerialization/ControlFlow/CfgBlockGraphNode.cs
@@ -0,0 +1,11 @@
+namespace Spice86.Core.Emulator.StateSerialization.ControlFlow;
+
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
+
+///
+/// Represents one exported CFG block without serialization or rendering concerns.
+///
+public sealed record CfgBlockGraphNode {
+ public required CfgBlock Block { get; init; }
+ public required bool IsExecutingBlock { get; init; }
+}
diff --git a/src/Spice86.Core/Emulator/StateSerialization/ControlFlow/CfgExecutionContextGraph.cs b/src/Spice86.Core/Emulator/StateSerialization/ControlFlow/CfgExecutionContextGraph.cs
new file mode 100644
index 0000000000..5598675c41
--- /dev/null
+++ b/src/Spice86.Core/Emulator/StateSerialization/ControlFlow/CfgExecutionContextGraph.cs
@@ -0,0 +1,16 @@
+namespace Spice86.Core.Emulator.StateSerialization.ControlFlow;
+
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
+
+///
+/// Wraps the traversal result with execution-context metadata. Returned only by
+/// ; the UI never sees this type directly.
+///
+public sealed record CfgExecutionContextGraph {
+ public required CfgBlockGraph Graph { get; init; }
+ public required int CurrentContextDepth { get; init; }
+ public required string CurrentContextEntryPoint { get; init; }
+ public required string[] EntryPointAddresses { get; init; }
+ public required ICfgNode? LastExecuted { get; init; }
+ public required CfgBlock? LastExecutedBlock { get; init; }
+}
diff --git a/src/Spice86.Core/Emulator/StateSerialization/EmulationStateDataIoHandler.cs b/src/Spice86.Core/Emulator/StateSerialization/EmulationStateDataIoHandler.cs
index 481127cf4d..aa8c48764a 100644
--- a/src/Spice86.Core/Emulator/StateSerialization/EmulationStateDataIoHandler.cs
+++ b/src/Spice86.Core/Emulator/StateSerialization/EmulationStateDataIoHandler.cs
@@ -55,6 +55,11 @@ protected EmulationStateDataIoHandler(EmulatorStateSerializationFolder emulatorS
///
protected string BreakpointsFile => $"{DataDirectory}/Breakpoints.json";
+ ///
+ /// Gets name of the file the CFG blocks JSON dump.
+ ///
+ protected string CfgBlocksFile => GenerateDumpFileName("CfgBlocks.json");
+
///
/// Generates a dump file name with the specified suffix.
///
diff --git a/src/Spice86.Core/Emulator/StateSerialization/EmulationStateDataWriter.cs b/src/Spice86.Core/Emulator/StateSerialization/EmulationStateDataWriter.cs
index 78e87c5ab3..b24f9c8dbf 100644
--- a/src/Spice86.Core/Emulator/StateSerialization/EmulationStateDataWriter.cs
+++ b/src/Spice86.Core/Emulator/StateSerialization/EmulationStateDataWriter.cs
@@ -3,6 +3,7 @@ namespace Spice86.Core.Emulator.StateSerialization;
using Serilog.Events;
using Spice86.Core.Emulator.CPU;
+using Spice86.Core.Emulator.CPU.CfgCpu;
using Spice86.Core.Emulator.Function;
using Spice86.Shared.Emulator.VM.Breakpoint.Serializable;
using Spice86.Shared.Interfaces;
@@ -18,6 +19,8 @@ public class EmulationStateDataWriter : EmulationStateDataIoHandler {
private readonly ExecutionAddressesExtractor _executionAddressesExtractor;
private readonly MemoryDataExporter _memoryDataExporter;
private readonly ListingExporter _listingExporter;
+ private readonly CfgBlocksJsonExporter _cfgBlocksJsonExporter;
+ private readonly ExecutionContextManager _executionContextManager;
private readonly FunctionCatalogue _functionCatalogue;
private readonly ISerializableBreakpointsSource _serializableBreakpointsSource;
@@ -27,6 +30,8 @@ public class EmulationStateDataWriter : EmulationStateDataIoHandler {
/// The class that dumps machine code execution flow.
/// The class used to dump main memory data properly.
/// The class used to dump asm listing of encountered instructions.
+ /// The class used to dump the CFG block graph as JSON.
+ /// The execution context manager for CFG graph export.
/// The list of functions encountered.
/// The CPU state.
/// Where to save the data.
@@ -36,6 +41,8 @@ public EmulationStateDataWriter(State state,
ExecutionAddressesExtractor executionAddressesExtractor,
MemoryDataExporter memoryDataExporter,
ListingExporter listingExporter,
+ CfgBlocksJsonExporter cfgBlocksJsonExporter,
+ ExecutionContextManager executionContextManager,
FunctionCatalogue functionCatalogue,
EmulatorStateSerializationFolder emulatorStateSerializationFolder,
ISerializableBreakpointsSource serializableBreakpointsSource,
@@ -44,6 +51,8 @@ public EmulationStateDataWriter(State state,
_state = state;
_memoryDataExporter = memoryDataExporter;
_listingExporter = listingExporter;
+ _cfgBlocksJsonExporter = cfgBlocksJsonExporter;
+ _executionContextManager = executionContextManager;
_functionCatalogue = functionCatalogue;
_serializableBreakpointsSource = serializableBreakpointsSource;
}
@@ -58,6 +67,7 @@ public void Write() {
File.WriteAllText(CpuRegistersFile, JsonSerializer.Serialize(_state));
_memoryDataExporter.Write(MemoryFile);
_listingExporter.Write(ListingFile);
+ _cfgBlocksJsonExporter.Write(_executionContextManager, CfgBlocksFile);
ExecutionAddresses executionAddresses = _executionAddressesExtractor.Extract();
new GhidraSymbolsExporter(LoggerService).Write(executionAddresses, _functionCatalogue, SymbolsFile);
new ExecutionAddressesExporter(LoggerService).Write(executionAddresses, ExecutionFlowFile);
diff --git a/src/Spice86.Core/Emulator/VM/Breakpoint/EmulatorBreakpointsManager.cs b/src/Spice86.Core/Emulator/VM/Breakpoint/EmulatorBreakpointsManager.cs
index 87659a4dde..45976fcd84 100644
--- a/src/Spice86.Core/Emulator/VM/Breakpoint/EmulatorBreakpointsManager.cs
+++ b/src/Spice86.Core/Emulator/VM/Breakpoint/EmulatorBreakpointsManager.cs
@@ -119,17 +119,14 @@ public void ToggleBreakPoint(BreakPoint breakPoint, bool on) {
_executionBreakPoints.HasActiveBreakpoints || _cycleBreakPoints.HasActiveBreakpoints;
///
- /// Checks the current breakpoints and triggers them if necessary.
+ /// Checks execution breakpoints only for the specified address without chasing IP changes,
+ /// plus cycle breakpoints. Used by CfgCpu per-node execution where each node independently
+ /// checks its own address.
///
- public void CheckExecutionBreakPoints() {
+ /// The physical address of the node about to execute.
+ public void CheckExecutionBreakPointsAt(uint physicalAddress) {
if (_executionBreakPoints.HasActiveBreakpoints) {
- uint address;
- // We do a loop here because if breakpoint action modifies the IP address we may miss other breakpoints.
- bool triggered;
- do {
- address = _state.IpPhysicalAddress;
- triggered = _executionBreakPoints.TriggerMatchingBreakPoints(address);
- } while (triggered && address != _state.IpPhysicalAddress);
+ _executionBreakPoints.TriggerMatchingBreakPoints(physicalAddress);
}
if (_cycleBreakPoints.HasActiveBreakpoints) {
diff --git a/src/Spice86.Core/Emulator/VM/EmulationLoop.cs b/src/Spice86.Core/Emulator/VM/EmulationLoop.cs
index 7b15d6e1bf..a886049bea 100644
--- a/src/Spice86.Core/Emulator/VM/EmulationLoop.cs
+++ b/src/Spice86.Core/Emulator/VM/EmulationLoop.cs
@@ -130,9 +130,6 @@ private void RunLoop() {
_performanceStopwatch.Start();
_cpu.SignalEntry();
while (_cpuState.IsRunning) {
- if (_emulatorBreakpointsManager.HasActiveBreakpoints) {
- _emulatorBreakpointsManager.CheckExecutionBreakPoints();
- }
_pauseHandler.WaitIfPaused();
_emulationLoopScheduler.ProcessEvents();
_cpu.ExecuteNext();
diff --git a/src/Spice86.Core/Emulator/VM/PauseHandler.cs b/src/Spice86.Core/Emulator/VM/PauseHandler.cs
index 8f68778d3b..b1883aa2c1 100644
--- a/src/Spice86.Core/Emulator/VM/PauseHandler.cs
+++ b/src/Spice86.Core/Emulator/VM/PauseHandler.cs
@@ -21,6 +21,7 @@ public class PauseHandler : IPauseHandler {
public delegate void ResumedEventHandler(object sender, EventArgs e);
private readonly ILoggerService _loggerService;
+ private readonly object _pauseLock = new();
private readonly ManualResetEvent _manualResetEvent = new(false);
private bool _disposed;
private volatile bool _pausing;
@@ -38,13 +39,15 @@ public PauseHandler(ILoggerService loggerService) {
///
public void Dispose() {
- if (_disposed) {
- return;
+ lock (_pauseLock) {
+ if (_disposed) {
+ return;
+ }
+ _disposed = true;
+ // Resume all waiting threads before teardown
+ _manualResetEvent.Set();
+ _manualResetEvent.Dispose();
}
- _disposed = true;
- // Resume all waiting threads before teardown
- _manualResetEvent.Set();
- _manualResetEvent.Dispose();
GC.SuppressFinalize(this);
}
@@ -59,25 +62,34 @@ public void Dispose() {
///
public void RequestPause(string? reason = null) {
- if (_disposed) {
- return;
+ lock (_pauseLock) {
+ if (_disposed) {
+ return;
+ }
}
_loggerService.Information("Pause requested by thread '{Thread}': {Reason}",
Thread.CurrentThread.Name ?? Environment.CurrentManagedThreadId.ToString(), reason);
Pausing?.Invoke();
- _pausing = true;
- _manualResetEvent.Reset();
+ lock (_pauseLock) {
+ if (_disposed) {
+ return;
+ }
+ _pausing = true;
+ _manualResetEvent.Reset();
+ }
Paused?.Invoke();
}
///
public void Resume() {
- if (_disposed) {
- return;
- }
_loggerService.Debug("Pause ended by thread {Thread}", Thread.CurrentThread.Name ?? Environment.CurrentManagedThreadId.ToString());
- _manualResetEvent.Set();
- _pausing = false;
+ lock (_pauseLock) {
+ if (_disposed) {
+ return;
+ }
+ _pausing = false;
+ _manualResetEvent.Set();
+ }
Resumed?.Invoke();
}
diff --git a/src/Spice86.Core/Spice86.Core.csproj b/src/Spice86.Core/Spice86.Core.csproj
index a5213473ad..4a288d3133 100644
--- a/src/Spice86.Core/Spice86.Core.csproj
+++ b/src/Spice86.Core/Spice86.Core.csproj
@@ -56,6 +56,9 @@
<_Parameter1>Spice86.Tests
+
+ <_Parameter1>Spice86.Tests.UI
+
<_Parameter1>DynamicProxyGenAssembly2
diff --git a/src/Spice86/Spice86DependencyInjection.cs b/src/Spice86/Spice86DependencyInjection.cs
index 737259204c..d5e69703eb 100644
--- a/src/Spice86/Spice86DependencyInjection.cs
+++ b/src/Spice86/Spice86DependencyInjection.cs
@@ -47,6 +47,7 @@ namespace Spice86;
using Spice86.Core.Emulator.OperatingSystem;
using Spice86.Core.Emulator.OperatingSystem.Structures;
using Spice86.Core.Emulator.StateSerialization;
+using Spice86.Core.Emulator.StateSerialization.ControlFlow;
using Spice86.Core.Emulator.VM;
using Spice86.Core.Emulator.VM.Breakpoint;
using Spice86.Core.Emulator.VM.Clock;
@@ -294,7 +295,7 @@ internal Spice86DependencyInjection(Configuration configuration, MainWindow? mai
_cfgNodeExecutionCompiler = cfgNodeExecutionCompiler;
CfgCpu cfgCpu = new(memory, state, ioPortDispatcher, callbackHandler,
- dualPic, emulatorBreakpointsManager, functionCatalogue,
+ dualPic, emulatorBreakpointsManager, pauseHandler, functionCatalogue,
configuration.UseCodeOverrideOption, configuration.FailOnInvalidOpcode, configuration.AllowIvtAddress0, loggerService, cfgNodeExecutionCompiler, cpuHeavyLogger);
if (loggerService.IsEnabled(LogEventLevel.Information)) {
@@ -432,8 +433,10 @@ internal Spice86DependencyInjection(Configuration configuration, MainWindow? mai
ListingExporter listingExporter = new(cfgCpu, loggerService, nodeToString);
ExecutionAddressesExtractor executionAddressesExtractor = new(cfgCpu, executionAddresses);
+ CfgBlockGraphExporter cfgBlockGraphExporter = new();
+ CfgBlocksJsonExporter cfgBlocksJsonExporter = new(cfgBlockGraphExporter);
EmulationStateDataWriter emulationStateDataWriter = new(state, executionAddressesExtractor, memoryDataExporter,
- listingExporter, functionCatalogue, emulatorStateSerializationFolder, emulatorBreakpointsManager,
+ listingExporter, cfgBlocksJsonExporter, cfgCpu.ExecutionContextManager, functionCatalogue, emulatorStateSerializationFolder, emulatorBreakpointsManager,
loggerService);
EmulatorStateSerializer emulatorStateSerializer = new(emulatorStateSerializationFolder,
emulationStateDataReader, emulationStateDataWriter);
@@ -452,7 +455,7 @@ internal Spice86DependencyInjection(Configuration configuration, MainWindow? mai
EmulatorMcpServices emulatorMcpServices = new(memory, state, functionCatalogue, cfgCpu,
ioPortDispatcher, vgaRenderer, pauseHandler, mcpEmsManager, xms,
- emulatorBreakpointsManager, loggerService);
+ emulatorBreakpointsManager, cfgBlocksJsonExporter, loggerService);
BiosKeyboardBuffer biosKeyboardBuffer = new BiosKeyboardBuffer(memory, biosDataArea);
KeyboardInt16Handler keyboardInt16Handler = new(
@@ -732,7 +735,8 @@ internal Spice86DependencyInjection(Configuration configuration, MainWindow? mai
cpuTabPlugin.Register(debuggerTabRegistry);
CfgCpuTabPlugin cfgCpuTabPlugin = new(uiDispatcher,
- cfgCpu.ExecutionContextManager, pauseHandler, nodeToString, asmRenderingConfig);
+ cfgCpu.ExecutionContextManager, pauseHandler, nodeToString, asmRenderingConfig,
+ cfgBlockGraphExporter);
cfgCpuTabPlugin.Register(debuggerTabRegistry);
StructureViewModelFactory structureViewModelFactory = new(configuration,
diff --git a/src/Spice86/ViewModels/CfgCpuViewModel.cs b/src/Spice86/ViewModels/CfgCpuViewModel.cs
index 20da39acb3..1d1399e019 100644
--- a/src/Spice86/ViewModels/CfgCpuViewModel.cs
+++ b/src/Spice86/ViewModels/CfgCpuViewModel.cs
@@ -18,6 +18,7 @@
using Spice86.Core.Emulator.CPU.CfgCpu.InstructionRenderer;
using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.SelfModifying;
+using Spice86.Core.Emulator.StateSerialization.ControlFlow;
using Spice86.Core.Emulator.VM;
using Spice86.Shared.Emulator.Memory;
using Spice86.ViewModels.Services;
@@ -31,6 +32,7 @@ public partial class CfgCpuViewModel : ViewModelBase {
private readonly ExecutionContextManager _executionContextManager;
private readonly NodeToString _nodeToString;
private readonly AstFormattedTextTokensRenderer _textOffsetsRenderer;
+ private readonly CfgBlockGraphExporter _graphExporter;
// Collection of searchable nodes for AutoCompleteBox
private Dictionary _searchableNodes = new();
@@ -65,11 +67,13 @@ public CfgCpuViewModel(IUIDispatcher uiDispatcher,
ExecutionContextManager executionContextManager,
IPauseHandler pauseHandler,
NodeToString nodeToString,
- AsmRenderingConfig asmRenderingConfig) {
+ AsmRenderingConfig asmRenderingConfig,
+ CfgBlockGraphExporter graphExporter) {
_nodeToString = nodeToString;
_textOffsetsRenderer = new AstFormattedTextTokensRenderer(asmRenderingConfig);
_uiDispatcher = uiDispatcher;
_executionContextManager = executionContextManager;
+ _graphExporter = graphExporter;
AutoFollow = true;
pauseHandler.Paused += () => {
@@ -145,37 +149,19 @@ private async Task NavigateToTableNode(NodeTableEntry? node) {
private async Task FindNodeByTextAsync(string searchText) {
return await Task.Run(() => {
- ICfgNode? nodeRoot = _executionContextManager.CurrentExecutionContext?.LastExecuted;
- if (nodeRoot == null) {
+ ICfgNode? nodeRoot = _executionContextManager.ExecutingNode;
+ if (nodeRoot is null) {
return null;
}
- Queue queue = new();
- queue.Enqueue(nodeRoot);
- HashSet visitedNodes = new();
-
- while (queue.Count > 0) {
- ICfgNode node = queue.Dequeue();
- if (visitedNodes.Contains(node)) {
- continue;
- }
-
- visitedNodes.Add(node);
-
- string nodeText = $"{_nodeToString.ToHeaderString(node)} {_nodeToString.ToAssemblyString(node)}";
- if (nodeText.Contains(searchText, StringComparison.OrdinalIgnoreCase)) {
- return node;
- }
-
- foreach (ICfgNode successor in node.Successors) {
- if (!visitedNodes.Contains(successor)) {
- queue.Enqueue(successor);
- }
- }
-
- foreach (ICfgNode predecessor in node.Predecessors) {
- if (!visitedNodes.Contains(predecessor)) {
- queue.Enqueue(predecessor);
+ CfgBlockGraph exportedGraph = _graphExporter.ExportFromNode(nodeRoot, null, null);
+ foreach (CfgBlockGraphNode graphNode in exportedGraph.Blocks) {
+ CfgBlock block = graphNode.Block;
+ foreach (ICfgNode instruction in block.Instructions) {
+ string nodeText =
+ $"{_nodeToString.ToHeaderString(instruction)} {_nodeToString.ToAssemblyString(instruction)}";
+ if (nodeText.Contains(searchText, StringComparison.OrdinalIgnoreCase)) {
+ return (ICfgNode)block;
}
}
}
@@ -191,61 +177,54 @@ private async Task RegenerateGraphFromNodeAsync(ICfgNode startNode) {
StatusMessage = "Generating graph...";
await Task.Run(async () => {
- long localNumberOfNodes = 0;
+ ICfgNode? executingNode = _executionContextManager.ExecutingNode;
+ CfgBlockGraph exportedGraph = _graphExporter.ExportFromNode(startNode, executingNode, MaxNodesToDisplay);
+
Graph currentGraph = new();
- Queue queue = new();
- queue.Enqueue(startNode);
- HashSet visitedNodes = new();
- HashSet<(int, int)> existingEdges = new();
+ HashSet blockIdsWithEdges = new();
Dictionary graphNodeCache = new();
Dictionary localSearchableNodes = new();
List localTableNodesList = new();
- while (queue.Count > 0 && localNumberOfNodes < MaxNodesToDisplay) {
- ICfgNode node = queue.Dequeue();
- if (visitedNodes.Contains(node)) {
- continue;
- }
-
- visitedNodes.Add(node);
-
- bool isLastExecuted =
- node.Id == _executionContextManager.CurrentExecutionContext?.LastExecuted?.Id;
- GetOrCreateGraphNode(node, isLastExecuted, graphNodeCache);
-
- string searchableText =
- $"{_nodeToString.ToHeaderString(node)} - {_nodeToString.ToAssemblyString(node)}";
- localSearchableNodes[searchableText] = node;
-
- localTableNodesList.Add(CreateTableEntry(node));
-
- foreach (ICfgNode successor in node.Successors) {
- (int, int) edgeKey = GenerateEdgeKey(node, successor);
- if (!existingEdges.Contains(edgeKey)) {
- currentGraph.Edges.Add(CreateEdge(node, successor, graphNodeCache));
- existingEdges.Add(edgeKey);
- }
-
- if (!visitedNodes.Contains(successor)) {
- queue.Enqueue(successor);
- }
+ // Build UI graph nodes from exported blocks
+ foreach (CfgBlockGraphNode exportedNode in exportedGraph.Blocks) {
+ CfgBlock block = exportedNode.Block;
+ GetOrCreateBlockGraphNode(block, exportedNode.IsExecutingBlock, graphNodeCache);
+
+ localTableNodesList.Add(CreateBlockHeaderTableEntry(block, exportedNode.IsExecutingBlock));
+ foreach (ICfgNode instruction in block.Instructions) {
+ string searchableText =
+ $"{_nodeToString.ToHeaderString(instruction)} - {_nodeToString.ToAssemblyString(instruction)}";
+ localSearchableNodes[searchableText] = instruction;
+ localTableNodesList.Add(CreateTableEntry(instruction));
}
+ }
- foreach (ICfgNode predecessor in node.Predecessors) {
- (int, int) edgeKey = GenerateEdgeKey(predecessor, node);
- if (!existingEdges.Contains(edgeKey)) {
- currentGraph.Edges.Add(CreateEdge(predecessor, node, graphNodeCache));
- existingEdges.Add(edgeKey);
- }
+ // Build UI edges from exported edges
+ foreach (CfgBlockGraphEdge exportedEdge in exportedGraph.Edges) {
+ bool isFromExecuting = graphNodeCache.TryGetValue(exportedEdge.From.Id, out CfgGraphNode? fromNode)
+ && fromNode.IsExecuting;
+ bool isToExecuting = graphNodeCache.TryGetValue(exportedEdge.To.Id, out CfgGraphNode? toNode)
+ && toNode.IsExecuting;
+ currentGraph.Edges.Add(CreateBlockEdge(
+ exportedEdge.From, exportedEdge.To, exportedEdge.BridgeNode,
+ isFromExecuting, isToExecuting, graphNodeCache));
+ blockIdsWithEdges.Add(exportedEdge.From.Id);
+ blockIdsWithEdges.Add(exportedEdge.To.Id);
+ }
- if (!visitedNodes.Contains(predecessor)) {
- queue.Enqueue(predecessor);
- }
+ // AvaloniaGraphControl only renders nodes that appear as edge endpoints.
+ // A block with no predecessors or successors (e.g. the very first block
+ // still being constructed) would be silently invisible. Add a self-loop
+ // edge for every such isolated block so the graph panel renders it.
+ foreach (KeyValuePair entry in graphNodeCache) {
+ if (!blockIdsWithEdges.Contains(entry.Key)) {
+ currentGraph.Edges.Add(new Edge(entry.Value, entry.Value,
+ new CfgGraphEdgeLabel { EdgeType = CfgEdgeType.IsolatedNodeLoop }, Edge.Symbol.None, Edge.Symbol.None));
}
-
- localNumberOfNodes++;
}
+ long localNumberOfBlocks = exportedGraph.Blocks.Length;
await _uiDispatcher.InvokeAsync(() => {
_searchableNodes = new Dictionary(localSearchableNodes);
@@ -255,8 +234,8 @@ await _uiDispatcher.InvokeAsync(() => {
Graph = currentGraph;
IsLoading = false;
- NumberOfNodes = localNumberOfNodes;
- StatusMessage = $"Graph generated with {localNumberOfNodes} nodes";
+ NumberOfNodes = localNumberOfBlocks;
+ StatusMessage = $"Graph generated with {localNumberOfBlocks} blocks";
NodeEntries.Clear();
NodeEntries.AddRange(_searchableNodes.Keys.OrderBy(k => k));
@@ -293,7 +272,7 @@ private async Task UpdateCurrentGraphAsync() {
return;
}
- ICfgNode? nodeRoot = _executionContextManager.CurrentExecutionContext?.LastExecuted;
+ ICfgNode? nodeRoot = _executionContextManager.ExecutingNode;
if (nodeRoot is null) {
return;
}
@@ -301,181 +280,201 @@ private async Task UpdateCurrentGraphAsync() {
await RegenerateGraphFromNodeAsync(nodeRoot);
}
- private Edge CreateEdge(ICfgNode node, ICfgNode successor, Dictionary graphNodeCache) {
+ private Edge CreateBlockEdge(CfgBlock fromBlock, CfgBlock toBlock, ICfgNode bridgeSuccessor,
+ bool isFromExecuting, bool isToExecuting,
+ Dictionary graphNodeCache) {
+ // Edge label and CfgEdgeType are derived from the source block's terminator.
+ // The bridge node identifies which entry of the terminator's
+ // SuccessorsPerType / SuccessorsPerSignature map produced this cross-block edge.
string labelText = string.Empty;
- CfgEdgeType edgeType = DetermineEdgeType(node);
-
- ICfgNode? lastExecutedNode = _executionContextManager.CurrentExecutionContext?.LastExecuted;
- bool isNodeLastExecuted = node.Id == lastExecutedNode?.Id;
- bool isSuccessorLastExecuted = successor.Id == lastExecutedNode?.Id;
-
- CfgGraphNode nodeGraphNode = GetOrCreateGraphNode(node, isNodeLastExecuted, graphNodeCache);
- CfgGraphNode successorGraphNode = GetOrCreateGraphNode(successor, isSuccessorLastExecuted, graphNodeCache);
-
- switch (node) {
- case CfgInstruction cfgInstruction:
- List keys = cfgInstruction
- .SuccessorsPerType
- .Where(kvp => kvp.Value.Contains(successor))
- .Select(kvp => kvp.Key)
- .ToList();
- labelText = string.Join(", ", keys);
- // Refine edge type based on successor relationship
- if (keys.Contains(InstructionSuccessorType.CallToReturn) ||
- keys.Contains(InstructionSuccessorType.CallToMisalignedReturn)) {
- edgeType = CfgEdgeType.CallToReturn;
- } else if (keys.Contains(InstructionSuccessorType.CpuFault)) {
- edgeType = CfgEdgeType.CpuFault;
- }
- break;
+ CfgEdgeType edgeType;
+
+ ICfgNode terminator = fromBlock.Terminator;
+ switch (terminator) {
case SelectorNode selectorNode: {
Signature? signature = selectorNode.SuccessorsPerSignature
- .FirstOrDefault(x => x.Value.Id == successor.Id).Key;
- labelText = signature?.ToString() ?? "";
+ .FirstOrDefault(x => x.Value.Id == bridgeSuccessor.Id).Key;
+ labelText = signature?.ToString() ?? string.Empty;
edgeType = CfgEdgeType.Selector;
break;
}
+ case CfgInstruction cfgInstruction: {
+ edgeType = DetermineEdgeType(cfgInstruction);
+ List keys = cfgInstruction
+ .SuccessorsPerType
+ .Where(kvp => kvp.Value.Contains(bridgeSuccessor))
+ .Select(kvp => kvp.Key)
+ .ToList();
+ labelText = string.Join(", ", keys);
+ if (keys.Contains(InstructionSuccessorType.CallToReturn) ||
+ keys.Contains(InstructionSuccessorType.CallToMisalignedReturn)) {
+ edgeType = CfgEdgeType.CallToReturn;
+ } else if (keys.Contains(InstructionSuccessorType.CpuFault)) {
+ edgeType = CfgEdgeType.CpuFault;
+ }
+ break;
+ }
+ default:
+ edgeType = CfgEdgeType.Normal;
+ break;
}
- CfgGraphEdgeLabel edgeLabel = new() { Text = labelText, EdgeType = edgeType };
- return new Edge(nodeGraphNode, successorGraphNode, edgeLabel);
- }
+ CfgGraphNode fromGraphNode = GetOrCreateBlockGraphNode(fromBlock, isFromExecuting, graphNodeCache);
+ CfgGraphNode toGraphNode = GetOrCreateBlockGraphNode(toBlock, isToExecuting, graphNodeCache);
- private static CfgNodeType ClassifyNodeType(ICfgNode node) {
- return node switch {
- CfgInstruction instr when instr.IsJump => CfgNodeType.Jump,
- CfgInstruction instr when instr.IsCall => CfgNodeType.Call,
- CfgInstruction instr when instr.IsReturn => CfgNodeType.Return,
- SelectorNode => CfgNodeType.Selector,
- _ => CfgNodeType.Instruction
- };
+ CfgGraphEdgeLabel edgeLabel = new() { Text = labelText, EdgeType = edgeType };
+ return new Edge(fromGraphNode, toGraphNode, edgeLabel);
}
private static CfgEdgeType DetermineEdgeType(ICfgNode node) {
- return ClassifyNodeType(node) switch {
- CfgNodeType.Jump => CfgEdgeType.Jump,
- CfgNodeType.Call => CfgEdgeType.Call,
- CfgNodeType.Return => CfgEdgeType.Return,
- CfgNodeType.Selector => CfgEdgeType.Selector,
+ return node switch {
+ CfgInstruction instr when instr.IsJump => CfgEdgeType.Jump,
+ CfgInstruction instr when instr.IsCall => CfgEdgeType.Call,
+ CfgInstruction instr when instr.IsReturn => CfgEdgeType.Return,
+ SelectorNode => CfgEdgeType.Selector,
_ => CfgEdgeType.Normal
};
}
- private CfgGraphNode GetOrCreateGraphNode(ICfgNode node, bool isLastExecuted,
+ private CfgGraphNode GetOrCreateBlockGraphNode(CfgBlock block, bool isExecuting,
Dictionary cache) {
- if (cache.TryGetValue(node.Id, out CfgGraphNode? existing)) {
+ if (cache.TryGetValue(block.Id, out CfgGraphNode? existing)) {
return existing;
}
- CfgGraphNode graphNode = CreateGraphNode(node, isLastExecuted);
- cache[node.Id] = graphNode;
+ CfgGraphNode graphNode = CreateGraphNode(block, isExecuting);
+ cache[block.Id] = graphNode;
return graphNode;
}
- private CfgGraphNode CreateGraphNode(ICfgNode node, bool isLastExecuted) {
+ private CfgGraphNode CreateGraphNode(CfgBlock block, bool isExecuting) {
List textOffsets = [];
- // Prefix line
- if (isLastExecuted) {
- textOffsets.Add(new() { Text = "🔴 last run ", Kind = FormatterTextKind.Text });
- }
+ textOffsets.Add(new() { Text = block.Address.ToString(), Kind = FormatterTextKind.FunctionAddress });
+ textOffsets.Add(new() { Text = " / ", Kind = FormatterTextKind.Punctuation });
+ textOffsets.Add(new() { Text = block.Id.ToString(), Kind = FormatterTextKind.Number });
+ textOffsets.Add(new() { Text = Environment.NewLine, Kind = FormatterTextKind.Text });
- CfgNodeType nodeType = ClassifyNodeType(node);
- switch (nodeType) {
- case CfgNodeType.Jump:
- textOffsets.Add(new() { Text = "\u2192 jump ", Kind = FormatterTextKind.Mnemonic });
- break;
- case CfgNodeType.Call:
- textOffsets.Add(new() { Text = "\u27b1 call ", Kind = FormatterTextKind.Mnemonic });
- break;
- case CfgNodeType.Return:
- textOffsets.Add(new() { Text = "\u27f0 return ", Kind = FormatterTextKind.Mnemonic });
- break;
- case CfgNodeType.Selector:
- textOffsets.Add(new() { Text = "\u2630 selector ", Kind = FormatterTextKind.Keyword });
- break;
+ // The currently executing instruction may be inside this block. When so, the
+ // per-instruction line gets a 🔴 marker so the user can pinpoint which instruction
+ // is about to execute. The block itself gets a red border (via IsExecuting) instead
+ // of an in-title dot.
+ ICfgNode? executingNode = _executionContextManager.ExecutingNode;
+ int executingIndex = -1;
+ if (executingNode is not null) {
+ for (int i = 0; i < block.Instructions.Count; i++) {
+ if (block.Instructions[i].Id == executingNode.Id) {
+ executingIndex = i;
+ break;
+ }
+ }
}
- // Header: address / id
- textOffsets.Add(new() { Text = node.Address.ToString(), Kind = FormatterTextKind.FunctionAddress });
- textOffsets.Add(new() { Text = " / ", Kind = FormatterTextKind.Punctuation });
- textOffsets.Add(new() { Text = node.Id.ToString(), Kind = FormatterTextKind.Number });
- textOffsets.Add(new() { Text = Environment.NewLine, Kind = FormatterTextKind.Text });
+ // Walk the block's instructions and render each through the AST renderer.
+ // SelectorNode's DisplayAst delegates to the active variant (or its terminator's),
+ // so we render it using the same renderer for consistency.
+ IReadOnlyList instructions = block.Instructions;
+ for (int i = 0; i < instructions.Count; i++) {
+ ICfgNode instruction = instructions[i];
+ IVisitableAstNode ast = instruction.DisplayAst;
+ textOffsets.AddRange(ast.Accept(_textOffsetsRenderer));
+ if (i == executingIndex) {
+ textOffsets.Add(new() { Text = " \U0001f534", Kind = FormatterTextKind.Text });
+ }
+ if (i < instructions.Count - 1) {
+ textOffsets.Add(new() { Text = Environment.NewLine, Kind = FormatterTextKind.Text });
+ }
+ }
- // Assembly instruction (syntax-highlighted via AST renderer)
- InstructionNode ast = node.DisplayAst;
- textOffsets.AddRange(ast.Accept(_textOffsetsRenderer));
+ // In-discovery blocks render with a trailing "..." marker so the user can see
+ // at a glance that the block's terminator is not final. The dashed outline that
+ // complements this indicator is applied by the AXAML view via the
+ // IsDiscoveryComplete flag set on the CfgGraphNode below.
+ if (!block.IsDiscoveryComplete) {
+ textOffsets.Add(new() { Text = Environment.NewLine, Kind = FormatterTextKind.Text });
+ textOffsets.Add(new() { Text = "\u2026", Kind = FormatterTextKind.Text });
+ }
return new CfgGraphNode {
- NodeId = node.Id,
+ NodeId = block.Id,
TextOffsets = textOffsets,
- IsLastExecuted = isLastExecuted,
- NodeType = nodeType
+ IsExecuting = isExecuting,
+ IsLive = block.IsLive,
+ IsDiscoveryComplete = block.IsDiscoveryComplete
};
}
- private string FormatNodeText(ICfgNode node, bool isLastExecuted) {
- string headerText = _nodeToString.ToHeaderString(node);
- string assemblyText = _nodeToString.ToAssemblyString(node);
+ private NodeTableEntry CreateTableEntry(ICfgNode node) {
+ bool isExecuting = node.Id == _executionContextManager.ExecutingNode?.Id;
- string prefix = "";
+ AvaloniaList predecessors = new(node.Predecessors.Select(predecessor => new NodeTableEntry {
+ Address = $"0x{predecessor.Address}",
+ Assembly = _nodeToString.ToAssemblyString(predecessor),
+ Node = predecessor
+ }));
- if (isLastExecuted) {
- prefix += "🔴 last run ";
- }
-
- switch (ClassifyNodeType(node)) {
- case CfgNodeType.Jump:
- prefix += "\u2192 jump ";
- break;
- case CfgNodeType.Call:
- prefix += "\u27b1 call ";
- break;
- case CfgNodeType.Return:
- prefix += "\u27f0 return ";
- break;
- case CfgNodeType.Selector:
- prefix += "\u2630 selector ";
- break;
- }
+ AvaloniaList successors = new(node.Successors.Select(successor => new NodeTableEntry {
+ Address = $"0x{successor.Address}",
+ Assembly = _nodeToString.ToAssemblyString(successor),
+ Node = successor
+ }));
- return $"{prefix}{headerText}{Environment.NewLine}{assemblyText}";
+ return new NodeTableEntry {
+ Address = $"0x{node.Address}",
+ Assembly = _nodeToString.ToAssemblyString(node),
+ Type = node.GetType().Name,
+ Predecessors = predecessors,
+ Successors = successors,
+ IsExecuting = isExecuting,
+ Node = node
+ };
}
- private static (int, int) GenerateEdgeKey(ICfgNode node, ICfgNode successor)
- => (node.Id, successor.Id);
-
- private NodeTableEntry CreateTableEntry(ICfgNode node) {
- string nodeType = ClassifyNodeType(node).ToString();
-
- bool isLastExecuted = node.Id == _executionContextManager.CurrentExecutionContext?.LastExecuted?.Id;
-
+ ///
+ /// Builds the synthetic "CfgBlock" header row inserted at the top of each block's listing
+ /// in the table view. Predecessors / successors are the addresses of the neighbouring
+ /// CfgBlocks (derived from the underlying instruction-level edges via the
+ /// ContainingBlock back-pointers). Selecting this row navigates to the block.
+ ///
+ private NodeTableEntry CreateBlockHeaderTableEntry(CfgBlock block, bool isExecutingBlock) {
AvaloniaList predecessors = new();
- foreach (ICfgNode predecessor in node.Predecessors) {
+ HashSet seenPredecessorBlockIds = new();
+ foreach (ICfgNode predecessor in block.Predecessors) {
+ CfgBlock? predecessorBlock = predecessor.ContainingBlock;
+ if (predecessorBlock is null || !seenPredecessorBlockIds.Add(predecessorBlock.Id)) {
+ continue;
+ }
predecessors.Add(new NodeTableEntry {
- Address = $"0x{predecessor.Address}",
- Assembly = _nodeToString.ToAssemblyString(predecessor),
- Node = predecessor
+ Address = $"0x{predecessorBlock.Address}",
+ Assembly = _nodeToString.ToAssemblyString(predecessorBlock),
+ Type = nameof(CfgBlock),
+ Node = predecessorBlock
});
}
AvaloniaList successors = new();
- foreach (ICfgNode successor in node.Successors) {
+ HashSet seenSuccessorBlockIds = new();
+ foreach (ICfgNode successor in block.Successors) {
+ CfgBlock? successorBlock = successor.ContainingBlock;
+ if (successorBlock is null || !seenSuccessorBlockIds.Add(successorBlock.Id)) {
+ continue;
+ }
successors.Add(new NodeTableEntry {
- Address = $"0x{successor.Address}",
- Assembly = _nodeToString.ToAssemblyString(successor),
- Node = successor
+ Address = $"0x{successorBlock.Address}",
+ Assembly = _nodeToString.ToAssemblyString(successorBlock),
+ Type = nameof(CfgBlock),
+ Node = successorBlock
});
}
return new NodeTableEntry {
- Address = $"0x{node.Address}",
- Assembly = _nodeToString.ToAssemblyString(node),
- Type = nodeType,
+ Address = $"0x{block.Address}",
+ Assembly = _nodeToString.ToAssemblyString(block),
+ Type = nameof(CfgBlock),
Predecessors = predecessors,
Successors = successors,
- IsLastExecuted = isLastExecuted,
- Node = node
+ IsExecuting = isExecutingBlock,
+ Node = block
};
}
@@ -486,7 +485,7 @@ public record NodeTableEntry {
public AvaloniaList Predecessors { get; init; } = new();
public AvaloniaList Successors { get; init; } = new();
- public bool IsLastExecuted { get; init; }
+ public bool IsExecuting { get; init; }
public ICfgNode? Node { get; init; }
public string PredecessorsText => string.Join(Environment.NewLine, Predecessors.Select(p => p.Address));
diff --git a/src/Spice86/ViewModels/CfgEdgeType.cs b/src/Spice86/ViewModels/CfgEdgeType.cs
index c3312934c1..ec7b9a82ac 100644
--- a/src/Spice86/ViewModels/CfgEdgeType.cs
+++ b/src/Spice86/ViewModels/CfgEdgeType.cs
@@ -10,5 +10,7 @@ public enum CfgEdgeType {
Return,
Selector,
CallToReturn,
- CpuFault
+ CpuFault,
+ /// Technical self-loop added to make isolated nodes visible in the graph panel. Rendered invisible.
+ IsolatedNodeLoop
}
\ No newline at end of file
diff --git a/src/Spice86/ViewModels/CfgGraphNode.cs b/src/Spice86/ViewModels/CfgGraphNode.cs
index faf4254e6f..b59a1f5230 100644
--- a/src/Spice86/ViewModels/CfgGraphNode.cs
+++ b/src/Spice86/ViewModels/CfgGraphNode.cs
@@ -20,12 +20,23 @@ public sealed class CfgGraphNode : IEquatable {
///
/// Whether this node is the last executed instruction.
///
- public bool IsLastExecuted { get; init; }
+ public bool IsExecuting { get; init; }
///
- /// The type of instruction this node represents, used for visual differentiation.
+ /// Whether the underlying CfgBlock (or instruction) is currently live.
+ /// Defaults to true. When false, the view applies "stale" styling
+ /// to visually distinguish the node from a live block.
///
- public CfgNodeType NodeType { get; init; }
+ public bool IsLive { get; init; } = true;
+
+ ///
+ /// Whether the underlying CfgBlock has finished discovery. Defaults to true
+ /// for plain instruction nodes; for CfgBlock nodes this mirrors
+ /// .
+ /// When false, the view applies an in-progress indicator (e.g. trailing "…"
+ /// on the listing and a dashed outline) to distinguish the node from a closed block.
+ ///
+ public bool IsDiscoveryComplete { get; init; } = true;
public override bool Equals(object? obj) => obj is CfgGraphNode other && NodeId == other.NodeId;
public bool Equals(CfgGraphNode? other) => other is not null && NodeId == other.NodeId;
diff --git a/src/Spice86/ViewModels/Services/CfgCpuTabPlugin.cs b/src/Spice86/ViewModels/Services/CfgCpuTabPlugin.cs
index 4d76983cc4..d739e3dcef 100644
--- a/src/Spice86/ViewModels/Services/CfgCpuTabPlugin.cs
+++ b/src/Spice86/ViewModels/Services/CfgCpuTabPlugin.cs
@@ -3,6 +3,7 @@ namespace Spice86.ViewModels.Services;
using Spice86.Core.Emulator.CPU.CfgCpu;
using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
using Spice86.Core.Emulator.CPU.CfgCpu.InstructionRenderer;
+using Spice86.Core.Emulator.StateSerialization.ControlFlow;
using Spice86.Core.Emulator.VM;
using Spice86.ViewModels;
@@ -12,18 +13,22 @@ internal sealed class CfgCpuTabPlugin : IDebuggerTabPlugin {
private readonly IPauseHandler _pauseHandler;
private readonly NodeToString _nodeToString;
private readonly AsmRenderingConfig _asmRenderingConfig;
+ private readonly CfgBlockGraphExporter _graphExporter;
public CfgCpuTabPlugin(IUIDispatcher uiDispatcher, ExecutionContextManager executionContextManager,
- IPauseHandler pauseHandler, NodeToString nodeToString, AsmRenderingConfig asmRenderingConfig) {
+ IPauseHandler pauseHandler, NodeToString nodeToString, AsmRenderingConfig asmRenderingConfig,
+ CfgBlockGraphExporter graphExporter) {
_uiDispatcher = uiDispatcher;
_executionContextManager = executionContextManager;
_pauseHandler = pauseHandler;
_nodeToString = nodeToString;
_asmRenderingConfig = asmRenderingConfig;
+ _graphExporter = graphExporter;
}
public void Register(IDebuggerTabRegistry registry) {
registry.Add(DebuggerTabId.CfgCpu,
- new CfgCpuViewModel(_uiDispatcher, _executionContextManager, _pauseHandler, _nodeToString, _asmRenderingConfig));
+ new CfgCpuViewModel(_uiDispatcher, _executionContextManager, _pauseHandler,
+ _nodeToString, _asmRenderingConfig, _graphExporter));
}
}
\ No newline at end of file
diff --git a/src/Spice86/Views/Behaviors/CfgNodeThemeBehavior.cs b/src/Spice86/Views/Behaviors/CfgNodeThemeBehavior.cs
index d66d0bc1b7..776928f19a 100644
--- a/src/Spice86/Views/Behaviors/CfgNodeThemeBehavior.cs
+++ b/src/Spice86/Views/Behaviors/CfgNodeThemeBehavior.cs
@@ -65,18 +65,44 @@ private static void ApplyTheme(Border border) {
// Apply theme-aware background and border
border.Background = HighlightingConverter.GetDefaultBackgroundBrush();
- bool isLastExecuted = border.DataContext is CfgGraphNode { IsLastExecuted: true };
- if (isLastExecuted) {
- IBrush lastExecutedBrush = ConverterUtilities.GetResourceBrush(
- "CfgNodeLastExecutedBorderBrush",
+ bool isExecuting = border.DataContext is CfgGraphNode { IsExecuting: true };
+ bool isLive = border.DataContext is not CfgGraphNode { IsLive: false };
+ bool isDiscoveryComplete = border.DataContext is not CfgGraphNode { IsDiscoveryComplete: false };
+
+ if (isExecuting) {
+ IBrush executingBrush = ConverterUtilities.GetResourceBrush(
+ "CfgNodeExecutingBorderBrush",
new SolidColorBrush(Color.FromRgb(0xCB, 0x43, 0x35)));
- border.BorderBrush = lastExecutedBrush;
+ border.BorderBrush = executingBrush;
border.BorderThickness = new Thickness(2.5);
} else {
border.BorderBrush = HighlightingConverter.GetDefaultForegroundBrush();
border.BorderThickness = new Thickness(1.5);
}
+ // Stale block styling: grey background makes invalidated nodes immediately
+ // obvious without relying on a subtle opacity change.
+ if (!isLive) {
+ IBrush staleBrush = ConverterUtilities.GetResourceBrush(
+ "CfgNodeStaleBackgroundBrush",
+ new SolidColorBrush(Color.FromRgb(0xD5, 0xD8, 0xDC)));
+ border.Background = staleBrush;
+ border.Opacity = 0.7;
+ } else {
+ border.Opacity = 1.0;
+ }
+
+ // In-discovery block styling: a green border signals the block's terminator
+ // is not yet final ("in birth"). Combined with the trailing "…" marker on
+ // the listing text, this makes in-discovery blocks clearly identifiable.
+ if (!isDiscoveryComplete && !isExecuting) {
+ IBrush inDiscoveryBrush = ConverterUtilities.GetResourceBrush(
+ "CfgNodeInDiscoveryBorderBrush",
+ new SolidColorBrush(Color.FromRgb(0x28, 0xB4, 0x63)));
+ border.BorderBrush = inDiscoveryBrush;
+ border.BorderThickness = new Thickness(2.0);
+ }
+
// Apply syntax-highlighted inlines to the child TextBlock
if (border.Child is TextBlock textBlock && border.DataContext is CfgGraphNode node) {
ApplyFormattedTextOffsets(textBlock, node.TextOffsets);
diff --git a/src/Spice86/Views/Behaviors/ConnectionThemeBehavior.cs b/src/Spice86/Views/Behaviors/ConnectionThemeBehavior.cs
index 65d0920adf..2da58f7584 100644
--- a/src/Spice86/Views/Behaviors/ConnectionThemeBehavior.cs
+++ b/src/Spice86/Views/Behaviors/ConnectionThemeBehavior.cs
@@ -70,6 +70,7 @@ private static void OnEnableThemingChanged(AvaloniaPropertyChangedEventArgs
-
+
@@ -267,7 +267,7 @@
Padding="8,4"
Margin="0,5,5,5">
-
+
diff --git a/src/Spice86/Views/Styles/CfgCpuResources.axaml b/src/Spice86/Views/Styles/CfgCpuResources.axaml
index 6b80cb1f88..8bb52378b5 100644
--- a/src/Spice86/Views/Styles/CfgCpuResources.axaml
+++ b/src/Spice86/Views/Styles/CfgCpuResources.axaml
@@ -13,8 +13,14 @@
-
-
+
+
+
+
+
+
+
+
@@ -28,8 +34,14 @@
-
-
+
+
+
+
+
+
+
+
diff --git a/tests/Spice86.Tests/CfgCpu/Blocks/BlockBasicsTest.cs b/tests/Spice86.Tests/CfgCpu/Blocks/BlockBasicsTest.cs
new file mode 100644
index 0000000000..46e7ded8f2
--- /dev/null
+++ b/tests/Spice86.Tests/CfgCpu/Blocks/BlockBasicsTest.cs
@@ -0,0 +1,117 @@
+namespace Spice86.Tests.CfgCpu.Blocks;
+
+using FluentAssertions;
+
+using Spice86.Core.Emulator.CPU.CfgCpu.Ast;
+using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Instruction;
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.Prefix;
+using Spice86.Shared.Emulator.Memory;
+
+using System.Collections.Immutable;
+
+using Xunit;
+
+///
+/// Tests basic state derived from contained instructions.
+///
+public class BlockBasicsTest {
+ /// Address used for the synthetic single-instruction block under test.
+ private static readonly SegmentedAddress TestAddress = new(0x1000, 0x0000);
+ private static readonly CfgNodeIdAllocator _allocator = new();
+
+ ///
+ /// reflects the live state of its single contained
+ /// instruction and correctly tracks round-trip transitions via .
+ ///
+ [Fact]
+ public void IsLive_PropagatesFromSingleInstruction_OnSetLiveTransitions() {
+ CfgInstruction entry = CreateMinimalInstruction();
+ CfgBlock block = new(_allocator.AllocateId(), entry);
+ entry.ContainingBlock = block;
+
+ block.IsLive.Should().BeTrue(
+ "a block is live iff every contained instruction is live");
+
+ entry.SetLive(false);
+ block.IsLive.Should().BeFalse(
+ "marking the only instruction as non-live must flip the block's IsLive to false");
+
+ entry.SetLive(true);
+ block.IsLive.Should().BeTrue(
+ "re-marking every contained instruction as live must flip the block's IsLive back to true");
+ }
+
+ ///
+ /// Verifies that correctly tracks the non-live counter
+ /// across multiple instructions: the block is live only when all contained instructions
+ /// are live.
+ ///
+ [Fact]
+ public void IsLive_TracksNonLiveCounter_AcrossMultipleInstructions() {
+ CfgInstruction instr0 = CreateMinimalInstruction(new SegmentedAddress(0x1000, 0));
+ CfgInstruction instr1 = CreateMinimalInstruction(new SegmentedAddress(0x1000, 1));
+ CfgInstruction instr2 = CreateMinimalInstruction(new SegmentedAddress(0x1000, 2));
+
+ CfgBlock block = new(_allocator.AllocateId(), instr0);
+ block.Append(instr1);
+ block.Append(instr2);
+ instr0.ContainingBlock = block;
+ instr1.ContainingBlock = block;
+ instr2.ContainingBlock = block;
+
+ block.IsLive.Should().BeTrue("all instructions are live");
+
+ instr1.SetLive(false);
+ block.IsLive.Should().BeFalse("one instruction is non-live");
+
+ instr2.SetLive(false);
+ block.IsLive.Should().BeFalse("two instructions are non-live");
+
+ instr2.SetLive(true);
+ block.IsLive.Should().BeFalse("instr1 is still non-live");
+
+ instr1.SetLive(true);
+ block.IsLive.Should().BeTrue("all instructions are live again");
+ }
+
+ ///
+ /// returns a wrapping all
+ /// contained instructions' display ASTs.
+ ///
+ [Fact]
+ public void DisplayAst_ReturnsBlockNodeWrappingInstructions() {
+ TestInstructionHelper helper = new();
+ CfgInstruction terminator = helper.WriteAndParse(TestAddress, w => w.WriteUInt8(0x90));
+ CfgBlock block = new(_allocator.AllocateId(), terminator);
+
+ // Act
+ IVisitableAstNode blockAst = block.DisplayAst;
+
+ blockAst.Should().BeOfType();
+ BlockNode blockNode = (BlockNode)blockAst;
+ blockNode.Statements.Should().HaveCount(1);
+ blockNode.Statements[0].Should().BeSameAs(terminator.DisplayAst);
+ }
+
+ ///
+ /// Builds a minimal at .
+ /// Sufficient for tests that do not access DisplayAst or ExecutionAst.
+ ///
+ private static CfgInstruction CreateMinimalInstruction() => CreateMinimalInstruction(TestAddress);
+
+ ///
+ /// Builds a minimal at the given address.
+ ///
+ private static CfgInstruction CreateMinimalInstruction(SegmentedAddress address) {
+ InstructionField opcodeField = new(
+ indexInInstruction: 0,
+ length: 1,
+ physicalAddress: address.Linear,
+ value: 0x90,
+ signatureValue: ImmutableList.Create(0x90),
+ final: true);
+ return new CfgInstruction(_allocator.AllocateId(), address, opcodeField, new List(), maxSuccessorsCount: 1);
+ }
+}
diff --git a/tests/Spice86.Tests/CfgCpu/Blocks/CfgBlockGraphExporterTest.cs b/tests/Spice86.Tests/CfgCpu/Blocks/CfgBlockGraphExporterTest.cs
new file mode 100644
index 0000000000..6811b0d1ce
--- /dev/null
+++ b/tests/Spice86.Tests/CfgCpu/Blocks/CfgBlockGraphExporterTest.cs
@@ -0,0 +1,277 @@
+namespace Spice86.Tests.CfgCpu.Blocks;
+
+using FluentAssertions;
+
+using NSubstitute;
+
+using Spice86.Core.Emulator.CPU;
+using Spice86.Core.Emulator.CPU.CfgCpu;
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
+using Spice86.Core.Emulator.CPU.CfgCpu.Feeder;
+using Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor.Expressions;
+using Spice86.Core.Emulator.CPU.CfgCpu.Linker;
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
+using Spice86.Core.Emulator.Function;
+using Spice86.Core.Emulator.Memory;
+using Spice86.Core.Emulator.StateSerialization.ControlFlow;
+using Spice86.Core.Emulator.Memory.Mmu;
+using Spice86.Core.Emulator.VM.Breakpoint;
+using Spice86.Shared.Emulator.Memory;
+using Spice86.Shared.Interfaces;
+
+using Xunit;
+
+///
+/// Tests for validating BFS traversal, edge discovery,
+/// deduplication, truncation, and execution-context metadata.
+///
+public class CfgBlockGraphExporterTest : IDisposable {
+ private const ushort Seg = 0x1000;
+ private readonly LinkerHarness _harness;
+ private readonly CfgBlockGraphExporter _exporter;
+
+ public CfgBlockGraphExporterTest() {
+ _harness = new LinkerHarness();
+ _exporter = new CfgBlockGraphExporter();
+ }
+
+ public void Dispose() {
+ _harness.Dispose();
+ }
+
+ ///
+ /// Builds a three-block linear chain: blkA (jmp) → blkB (jmp) → blkC.
+ /// Each block is a single JMP instruction (which is a block terminator by Kind).
+ ///
+ private (CfgInstruction instrA, CfgInstruction instrB, CfgInstruction instrC) BuildThreeBlockChain() {
+ CfgInstruction instrA = _harness.CreateInstruction(new SegmentedAddress(Seg, 0x0000), 0xEB, 1, InstructionKind.Jump);
+ CfgInstruction instrB = _harness.CreateInstruction(new SegmentedAddress(Seg, 0x0001), 0xEB, 1, InstructionKind.Jump);
+ CfgInstruction instrC = _harness.CreateInstruction(new SegmentedAddress(Seg, 0x0002), 0x90, 1, InstructionKind.None);
+
+ _harness.Linker.Link(InstructionSuccessorType.Normal, instrA, instrB);
+ _harness.Linker.Link(InstructionSuccessorType.Normal, instrB, instrC);
+
+ return (instrA, instrB, instrC);
+ }
+
+ ///
+ /// Exports a linear chain of three blocks from the first block. All three blocks and
+ /// two edges should be present in the exported graph.
+ ///
+ [Fact]
+ public void ExportFromNode_LinearChain_ExportsAllBlocksAndEdges() {
+ // Arrange
+ (CfgInstruction instrA, CfgInstruction instrB, CfgInstruction instrC) = BuildThreeBlockChain();
+
+ CfgBlock blockA = CfgTestHelpers.GetContainingBlock(instrA);
+ CfgBlock blockB = CfgTestHelpers.GetContainingBlock(instrB);
+ CfgBlock blockC = CfgTestHelpers.GetContainingBlock(instrC);
+
+ // Act
+ CfgBlockGraph graph = _exporter.ExportFromNode(instrA, null, null);
+
+ // Assert
+ graph.Blocks.Should().HaveCount(3);
+ graph.Blocks.Select(b => b.Block.Id).Should().Contain(blockA.Id);
+ graph.Blocks.Select(b => b.Block.Id).Should().Contain(blockB.Id);
+ graph.Blocks.Select(b => b.Block.Id).Should().Contain(blockC.Id);
+
+ graph.Edges.Should().HaveCount(2);
+ graph.Edges.Should().Contain(e => e.From.Id == blockA.Id && e.To.Id == blockB.Id);
+ graph.Edges.Should().Contain(e => e.From.Id == blockB.Id && e.To.Id == blockC.Id);
+ graph.Truncated.Should().BeFalse();
+ }
+
+ ///
+ /// Predecessor-only reachable blocks are included in the exported graph.
+ ///
+ [Fact]
+ public void ExportFromNode_IncludesPredecessorOnlyBlocks() {
+ // Arrange: A → B ← C (start from B; A and C are reachable only via predecessors)
+ CfgInstruction instrA = _harness.CreateInstruction(new SegmentedAddress(Seg, 0x0000), 0xEB, 1, InstructionKind.Jump);
+ CfgInstruction instrB = _harness.CreateInstruction(new SegmentedAddress(Seg, 0x0001), 0x90, 1, InstructionKind.None);
+ CfgInstruction instrC = _harness.CreateInstruction(new SegmentedAddress(Seg, 0x0010), 0xEB, 1, InstructionKind.Jump);
+
+ _harness.Linker.Link(InstructionSuccessorType.Normal, instrA, instrB);
+ _harness.Linker.Link(InstructionSuccessorType.Normal, instrC, instrB);
+
+ CfgBlock blockA = CfgTestHelpers.GetContainingBlock(instrA);
+ CfgBlock blockB = CfgTestHelpers.GetContainingBlock(instrB);
+ CfgBlock blockC = CfgTestHelpers.GetContainingBlock(instrC);
+
+ // Act — start from B; A and C are reachable only through predecessors
+ CfgBlockGraph graph = _exporter.ExportFromNode(instrB, null, null);
+
+ // Assert
+ graph.Blocks.Select(b => b.Block.Id).Should().Contain(blockA.Id);
+ graph.Blocks.Select(b => b.Block.Id).Should().Contain(blockB.Id);
+ graph.Blocks.Select(b => b.Block.Id).Should().Contain(blockC.Id);
+ }
+
+ ///
+ /// Edges are deduplicated by (from.Id, to.Id).
+ ///
+ [Fact]
+ public void ExportFromNode_DeduplicatesEdges() {
+ // Arrange: A → B
+ CfgInstruction instrA = _harness.CreateInstruction(new SegmentedAddress(Seg, 0x0000), 0xEB, 1, InstructionKind.Jump);
+ CfgInstruction instrB = _harness.CreateInstruction(new SegmentedAddress(Seg, 0x0001), 0x90, 1, InstructionKind.None);
+
+ _harness.Linker.Link(InstructionSuccessorType.Normal, instrA, instrB);
+
+ CfgBlock blockA = CfgTestHelpers.GetContainingBlock(instrA);
+ CfgBlock blockB = CfgTestHelpers.GetContainingBlock(instrB);
+
+ // Act
+ CfgBlockGraph graph = _exporter.ExportFromNode(instrA, null, null);
+
+ // Assert — only one A→B edge even though both successor and predecessor traversal see it
+ int edgeCount = graph.Edges.Count(e => e.From.Id == blockA.Id && e.To.Id == blockB.Id);
+ edgeCount.Should().Be(1);
+ }
+
+ ///
+ /// Truncation is reported when nodeLimit is reached.
+ ///
+ [Fact]
+ public void ExportFromNode_TruncatesWhenNodeLimitReached() {
+ // Arrange: A → B → C
+ (CfgInstruction instrA, _, _) = BuildThreeBlockChain();
+
+ // Act — limit to 2 blocks
+ CfgBlockGraph graph = _exporter.ExportFromNode(instrA, null, 2);
+
+ // Assert
+ graph.Blocks.Should().HaveCount(2);
+ graph.Truncated.Should().BeTrue();
+ }
+
+ ///
+ /// When the graph is truncated, edges must only reference blocks that are present
+ /// in the exported block list. No edge endpoint may point to an omitted block.
+ ///
+ [Fact]
+ public void ExportFromNode_TruncatedGraph_EdgesAreClosedToIncludedBlocks() {
+ // Arrange: A → B → C; limit to 1 block so only A is included
+ (CfgInstruction instrA, _, _) = BuildThreeBlockChain();
+
+ // Act
+ CfgBlockGraph graph = _exporter.ExportFromNode(instrA, null, 1);
+
+ // Assert
+ graph.Blocks.Should().HaveCount(1);
+ graph.Truncated.Should().BeTrue();
+
+ HashSet includedIds = graph.Blocks.Select(n => n.Block.Id).ToHashSet();
+ foreach (CfgBlockGraphEdge edge in graph.Edges) {
+ includedIds.Should().Contain(edge.From.Id,
+ "edge source must be an included block");
+ includedIds.Should().Contain(edge.To.Id,
+ "edge target must be an included block");
+ }
+ }
+
+ ///
+ /// Raw block IDs are preserved in nodes and edges.
+ ///
+ [Fact]
+ public void ExportFromNode_PreservesRawBlockIds() {
+ // Arrange
+ CfgInstruction instrA = _harness.CreateInstruction(new SegmentedAddress(Seg, 0x0000), 0xEB, 1, InstructionKind.Jump);
+ CfgInstruction instrB = _harness.CreateInstruction(new SegmentedAddress(Seg, 0x0001), 0x90, 1, InstructionKind.None);
+
+ _harness.Linker.Link(InstructionSuccessorType.Normal, instrA, instrB);
+
+ CfgBlock blockA = CfgTestHelpers.GetContainingBlock(instrA);
+ CfgBlock blockB = CfgTestHelpers.GetContainingBlock(instrB);
+
+ // Act
+ CfgBlockGraph graph = _exporter.ExportFromNode(instrA, null, null);
+
+ // Assert — IDs match the actual CfgBlock.Id values
+ graph.Blocks.Should().Contain(b => b.Block.Id == blockA.Id);
+ graph.Blocks.Should().Contain(b => b.Block.Id == blockB.Id);
+ graph.Edges.Should().Contain(e => e.From.Id == blockA.Id && e.To.Id == blockB.Id);
+ }
+
+ ///
+ /// A seed node with no containing block returns an empty graph.
+ ///
+ [Fact]
+ public void ExportFromNode_NoContainingBlock_ReturnsEmptyGraph() {
+ // Arrange — a raw instruction with no links has no containing block
+ CfgInstruction instrA = _harness.CreateInstruction(new SegmentedAddress(Seg, 0x0000));
+
+ // Act
+ CfgBlockGraph graph = _exporter.ExportFromNode(instrA, null, null);
+
+ // Assert
+ graph.Blocks.Should().BeEmpty();
+ graph.Edges.Should().BeEmpty();
+ graph.Truncated.Should().BeFalse();
+ }
+
+ ///
+ /// The executing block is marked in the exported graph.
+ ///
+ [Fact]
+ public void ExportFromNode_MarksExecutingBlock() {
+ // Arrange
+ CfgInstruction instrA = _harness.CreateInstruction(new SegmentedAddress(Seg, 0x0000), 0xEB, 1, InstructionKind.Jump);
+ CfgInstruction instrB = _harness.CreateInstruction(new SegmentedAddress(Seg, 0x0001), 0x90, 1, InstructionKind.None);
+
+ _harness.Linker.Link(InstructionSuccessorType.Normal, instrA, instrB);
+
+ CfgBlock blockA = CfgTestHelpers.GetContainingBlock(instrA);
+
+ // Act — instrA is the executing node, so its block should be marked
+ CfgBlockGraph graph = _exporter.ExportFromNode(instrA, instrA, null);
+
+ // Assert
+ CfgBlockGraphNode executingGraphNode = graph.Blocks.First(b => b.Block.Id == blockA.Id);
+ executingGraphNode.IsExecutingBlock.Should().BeTrue();
+
+ CfgBlockGraphNode otherGraphNode = graph.Blocks.First(b => b.Block.Id != blockA.Id);
+ otherGraphNode.IsExecutingBlock.Should().BeFalse();
+ }
+
+ ///
+ /// ExportFromExecutionContext includes the last executed block and context metadata.
+ ///
+ [Fact]
+ public void ExportFromExecutionContext_IncludesContextMetadata() {
+ // Arrange — create a minimal ExecutionContextManager
+ ILoggerService loggerService = Substitute.For();
+ AddressReadWriteBreakpoints memoryBreakpoints = new();
+ Memory memory = new(memoryBreakpoints, new Ram(0x100000), new A20Gate(), new RealModeMmu386(), false);
+ State state = new(CpuModel.INTEL_80286);
+ InstructionReplacerRegistry replacerRegistry = new();
+ using CfgNodeExecutionCompilerMonitor monitor = new(loggerService);
+ using CfgNodeExecutionCompiler compiler = new(monitor, loggerService, Spice86.Core.CLI.JitMode.InterpretedOnly);
+ using Spice86.Core.Emulator.VM.PauseHandler pauseHandler = new(loggerService);
+ CfgNodeFeeder feeder = new(memory, state, new EmulatorBreakpointsManager(
+ pauseHandler, state, memory,
+ memoryBreakpoints, new AddressReadWriteBreakpoints()), replacerRegistry, compiler);
+ ExecutionContextManager contextManager = new(memory, state, feeder, replacerRegistry,
+ new FunctionCatalogue(), false, loggerService, null);
+
+ // Build a simple chain
+ CfgInstruction instrA = _harness.CreateInstruction(new SegmentedAddress(Seg, 0x0000), 0xEB, 1, InstructionKind.Jump);
+ CfgInstruction instrB = _harness.CreateInstruction(new SegmentedAddress(Seg, 0x0001), 0x90, 1, InstructionKind.None);
+ _harness.Linker.Link(InstructionSuccessorType.Normal, instrA, instrB);
+
+ CfgBlock blockA = CfgTestHelpers.GetContainingBlock(instrA);
+ contextManager.CurrentExecutionContext.LastExecuted = instrA;
+ contextManager.ExecutingNode = instrA;
+
+ // Act
+ CfgExecutionContextGraph result = _exporter.ExportFromExecutionContext(contextManager, null);
+
+ // Assert
+ result.CurrentContextDepth.Should().Be(0);
+ result.LastExecuted.Should().Be(instrA);
+ result.LastExecutedBlock.Should().Be(blockA);
+ result.Graph.Should().NotBeNull();
+ result.Graph.Blocks.Should().Contain(b => b.Block.Id == blockA.Id);
+ }
+}
diff --git a/tests/Spice86.Tests/CfgCpu/Blocks/CfgBlockLinkerTest.cs b/tests/Spice86.Tests/CfgCpu/Blocks/CfgBlockLinkerTest.cs
new file mode 100644
index 0000000000..83ee29cef0
--- /dev/null
+++ b/tests/Spice86.Tests/CfgCpu/Blocks/CfgBlockLinkerTest.cs
@@ -0,0 +1,573 @@
+namespace Spice86.Tests.CfgCpu.Blocks;
+
+using FluentAssertions;
+
+using NSubstitute;
+
+using Spice86.Core.CLI;
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
+using Spice86.Core.Emulator.CPU.CfgCpu.Feeder;
+using Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor.Expressions;
+using Spice86.Core.Emulator.CPU.CfgCpu.Linker;
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
+using Spice86.Shared.Emulator.Memory;
+using Spice86.Shared.Interfaces;
+
+using static CfgTestHelpers;
+
+using Xunit;
+
+///
+/// Tests block construction and maintenance behavior in .
+///
+public class CfgBlockLinkerTest : IDisposable {
+ private const ushort BaseSegment = 0x1000;
+ private const ushort TargetSegment = 0x4000;
+
+ private static readonly CfgNodeIdAllocator _blockAllocator = new();
+ private readonly CfgNodeExecutionCompiler _compiler;
+ private readonly CfgNodeExecutionCompilerMonitor _monitor;
+ private readonly NodeLinker _linker;
+
+ public CfgBlockLinkerTest() {
+ ILoggerService loggerService = Substitute.For();
+ _monitor = new CfgNodeExecutionCompilerMonitor(loggerService);
+ _compiler = new CfgNodeExecutionCompiler(_monitor, loggerService, JitMode.InterpretedOnly);
+ _linker = new NodeLinker(new InstructionReplacerRegistry(), _compiler, new CfgNodeIdAllocator());
+ }
+
+ public void Dispose() {
+ _compiler.Dispose();
+ _monitor.Dispose();
+ }
+
+ [Fact]
+ public void IsLive_IgnoresRepeatedSetLiveCalls() {
+ CfgInstruction[] instructions = BuildBlock(4);
+ CfgBlock block = GetContainingBlock(instructions[0]);
+
+ instructions[1].SetLive(true);
+ block.IsLive.Should().BeTrue();
+
+ instructions[2].SetLive(false);
+ block.IsLive.Should().BeFalse();
+ instructions[2].SetLive(false);
+ block.IsLive.Should().BeFalse();
+
+ instructions[2].SetLive(true);
+ block.IsLive.Should().BeTrue();
+ }
+
+ [Fact]
+ public void IsLive_BecomesTrueOnlyAfterEveryInstructionIsLiveAgain() {
+ CfgInstruction[] instructions = BuildBlock(5);
+ CfgBlock block = GetContainingBlock(instructions[0]);
+
+ for (int i = 0; i < instructions.Length; i++) {
+ instructions[i].SetLive(false);
+ block.IsLive.Should().BeFalse();
+ }
+
+ for (int i = 0; i < instructions.Length - 1; i++) {
+ instructions[i].SetLive(true);
+ block.IsLive.Should().BeFalse($"instruction {i + 1} to {instructions.Length - 1} still non-live");
+ }
+ instructions[^1].SetLive(true);
+ block.IsLive.Should().BeTrue();
+ }
+
+ [Fact]
+ public void IsDiscoveryComplete_NeverTransitionsBackToFalse() {
+ CfgInstruction a = CreateInstruction(new SegmentedAddress(BaseSegment, 0));
+ CfgInstruction b = CreateInstruction(new SegmentedAddress(BaseSegment, 1));
+ CfgInstruction c = CreateInstruction(new SegmentedAddress(TargetSegment, 0));
+
+ _linker.Link(InstructionSuccessorType.Normal, a, b);
+ CfgBlock ab = GetContainingBlock(a);
+ ab.IsDiscoveryComplete.Should().BeFalse("block is still being built");
+
+ _linker.Link(InstructionSuccessorType.Normal, b, c);
+ ab.IsDiscoveryComplete.Should().BeTrue("boundary path finalizes the block");
+
+ CfgInstruction d = CreateInstruction(new SegmentedAddress(TargetSegment, 1));
+ _linker.Link(InstructionSuccessorType.Normal, c, d);
+ ab.IsDiscoveryComplete.Should().BeTrue("must never flip back to false");
+ }
+
+ [Theory]
+ [InlineData(InstructionSuccessorType.Normal)]
+ [InlineData(InstructionSuccessorType.CallToReturn)]
+ [InlineData(InstructionSuccessorType.CallToMisalignedReturn)]
+ [InlineData(InstructionSuccessorType.CpuFault)]
+ public void ColdPath_Continuation_AppendsBothToSameBlock(InstructionSuccessorType linkType) {
+ using LinkerHarness harness = new();
+ CfgInstruction current = harness.CreateInstruction(new SegmentedAddress(BaseSegment, 0));
+ CfgInstruction next = harness.CreateInstruction(new SegmentedAddress(BaseSegment, 1));
+
+ harness.Linker.Link(linkType, current, next);
+
+ current.ContainingBlock.Should().NotBeNull();
+ next.ContainingBlock.Should().BeSameAs(current.ContainingBlock,
+ "continuation: both must be in the same block");
+ GetContainingBlock(current).Instructions.Should().HaveCount(2);
+ }
+
+ [Fact]
+ public void ColdPath_ContinuationToTerminator_KeepsTerminatorInPredecessorBlock() {
+ using LinkerHarness harness = new();
+ CfgInstruction current = harness.CreateInstruction(new SegmentedAddress(BaseSegment, 0));
+ CfgInstruction next = harness.CreateInstruction(new SegmentedAddress(BaseSegment, 1));
+ next.MarkAsBlockTerminator();
+
+ ICfgNode returned = harness.Linker.Link(InstructionSuccessorType.Normal, current, next);
+
+ returned.Should().BeSameAs(next);
+ CfgBlock currentBlock = GetContainingBlock(current);
+ next.ContainingBlock.Should().BeSameAs(currentBlock,
+ "a returned terminator is not a graph boundary by itself");
+ currentBlock.Instructions.Should().Equal([current, next]);
+ currentBlock.Entry.Should().BeSameAs(current);
+ currentBlock.Terminator.Should().BeSameAs(next);
+ currentBlock.IsDiscoveryComplete.Should().BeTrue();
+ }
+
+ [Theory]
+ [InlineData(InstructionSuccessorType.Normal)]
+ [InlineData(InstructionSuccessorType.CallToReturn)]
+ [InlineData(InstructionSuccessorType.CallToMisalignedReturn)]
+ [InlineData(InstructionSuccessorType.CpuFault)]
+ public void ColdPath_MemoryGap_PlacesInSeparateBlocks(InstructionSuccessorType linkType) {
+ using LinkerHarness harness = new();
+ CfgInstruction current = harness.CreateInstruction(new SegmentedAddress(BaseSegment, 0));
+ CfgInstruction next = harness.CreateInstruction(new SegmentedAddress(BaseSegment, 10));
+
+ harness.Linker.Link(linkType, current, next);
+
+ current.ContainingBlock.Should().NotBeNull();
+ next.ContainingBlock.Should().NotBeNull();
+ next.ContainingBlock.Should().NotBeSameAs(current.ContainingBlock,
+ "memory gap: must be in separate blocks");
+ }
+
+ [Theory]
+ [InlineData(InstructionSuccessorType.Normal)]
+ [InlineData(InstructionSuccessorType.CallToReturn)]
+ [InlineData(InstructionSuccessorType.CallToMisalignedReturn)]
+ [InlineData(InstructionSuccessorType.CpuFault)]
+ public void ColdPath_NextIsStarter_PlacesInSeparateBlocks(InstructionSuccessorType linkType) {
+ using LinkerHarness harness = new();
+ CfgInstruction current = harness.CreateInstruction(new SegmentedAddress(BaseSegment, 0));
+ CfgInstruction next = harness.CreateInstruction(new SegmentedAddress(BaseSegment, 1));
+ next.MarkAsBlockStarter();
+
+ harness.Linker.Link(linkType, current, next);
+
+ next.ContainingBlock.Should().NotBeSameAs(current.ContainingBlock,
+ "next is a starter: must be in a separate block");
+ }
+
+ [Theory]
+ [InlineData(InstructionSuccessorType.Normal)]
+ [InlineData(InstructionSuccessorType.CallToReturn)]
+ [InlineData(InstructionSuccessorType.CallToMisalignedReturn)]
+ [InlineData(InstructionSuccessorType.CpuFault)]
+ public void ColdPath_CurrentIsTerminator_PlacesInSeparateBlocks(InstructionSuccessorType linkType) {
+ using LinkerHarness harness = new();
+ CfgInstruction current = harness.CreateInstruction(new SegmentedAddress(BaseSegment, 0));
+ current.MarkAsBlockTerminator();
+ CfgInstruction next = harness.CreateInstruction(new SegmentedAddress(BaseSegment, 1));
+
+ harness.Linker.Link(linkType, current, next);
+
+ next.ContainingBlock.Should().NotBeSameAs(current.ContainingBlock,
+ "current is a terminator: must be in a separate block");
+ GetContainingBlock(current).IsDiscoveryComplete.Should().BeTrue();
+ }
+
+ [Fact]
+ public void Split_InteriorEdge_ProducesCorrectPrefixAndSuffix() {
+ CfgInstruction[] instructions = BuildLinkedBlock(5);
+ CfgBlock alpha = GetContainingBlock(instructions[0]);
+ alpha.Instructions.Should().HaveCount(5);
+
+ CfgInstruction target = CreateInstruction(new SegmentedAddress(TargetSegment, 0));
+ _linker.Link(InstructionSuccessorType.Normal, instructions[2], target);
+
+ CfgBlock prefix = GetContainingBlock(instructions[0]);
+ prefix.Instructions.Should().HaveCount(3);
+ prefix.Entry.Should().BeSameAs(instructions[0]);
+ prefix.Terminator.Should().BeSameAs(instructions[2]);
+ prefix.IsDiscoveryComplete.Should().BeTrue();
+
+ CfgBlock suffix = GetContainingBlock(instructions[3]);
+ suffix.Should().NotBeSameAs(prefix);
+ suffix.Instructions.Should().HaveCount(2);
+ suffix.Entry.Should().BeSameAs(instructions[3]);
+ suffix.Terminator.Should().BeSameAs(instructions[4]);
+ suffix.IsDiscoveryComplete.Should().BeTrue();
+
+ for (int i = 0; i <= 2; i++) {
+ instructions[i].ContainingBlock.Should().BeSameAs(prefix);
+ }
+ for (int i = 3; i < 5; i++) {
+ instructions[i].ContainingBlock.Should().BeSameAs(suffix);
+ }
+
+ instructions[2].Successors.Should().Contain(target);
+ instructions[2].Successors.Should().Contain(instructions[3]);
+ }
+
+ [Fact]
+ public void Split_PreservesNonLiveCounterInBothHalves() {
+ CfgInstruction[] instructions = BuildLinkedBlock(6);
+ CfgBlock alpha = GetContainingBlock(instructions[0]);
+
+ instructions[1].SetLive(false);
+ instructions[4].SetLive(false);
+ alpha.IsLive.Should().BeFalse();
+
+ CfgInstruction target = CreateInstruction(new SegmentedAddress(TargetSegment, 0));
+ _linker.Link(InstructionSuccessorType.Normal, instructions[2], target);
+
+ CfgBlock prefix = GetContainingBlock(instructions[0]);
+ CfgBlock suffix = GetContainingBlock(instructions[3]);
+
+ prefix.IsLive.Should().BeFalse("prefix contains non-live instruction[1]");
+ suffix.IsLive.Should().BeFalse("suffix contains non-live instruction[4]");
+
+ instructions[1].SetLive(true);
+ prefix.IsLive.Should().BeTrue();
+
+ instructions[4].SetLive(true);
+ suffix.IsLive.Should().BeTrue();
+ }
+
+ [Fact]
+ public void ReplaceInstruction_InPlace_TransfersBackPointerAndAdjustsCounter() {
+ CfgInstruction[] instructions = BuildBlock(4);
+ CfgBlock block = GetContainingBlock(instructions[0]);
+
+ CfgInstruction oldInstr = instructions[2];
+ CfgInstruction newInstr = CreateInstruction(new SegmentedAddress(0x2000, 2));
+
+ _linker.ReplaceInstruction(oldInstr, newInstr);
+
+ block.Instructions.Should().HaveCount(4);
+ block.Instructions[2].Should().BeSameAs(newInstr);
+ block.Instructions[0].Should().BeSameAs(instructions[0]);
+ block.Instructions[1].Should().BeSameAs(instructions[1]);
+ block.Instructions[3].Should().BeSameAs(instructions[3]);
+ newInstr.ContainingBlock.Should().BeSameAs(block);
+ oldInstr.ContainingBlock.Should().BeNull();
+ }
+
+ [Fact]
+ public void ReplaceInstruction_NonLiveOldWithLiveNew_FixesCounter() {
+ CfgInstruction[] instructions = BuildBlock(3);
+ CfgBlock block = GetContainingBlock(instructions[0]);
+
+ instructions[1].SetLive(false);
+ block.IsLive.Should().BeFalse();
+
+ CfgInstruction newInstr = CreateInstruction(new SegmentedAddress(0x2000, 1));
+ _linker.ReplaceInstruction(instructions[1], newInstr);
+
+ block.IsLive.Should().BeTrue();
+ }
+
+ [Fact]
+ public void ReplaceInstruction_LiveOldWithNonLiveNew_FixesCounter() {
+ CfgInstruction[] instructions = BuildBlock(3);
+ CfgBlock block = GetContainingBlock(instructions[0]);
+ block.IsLive.Should().BeTrue();
+
+ CfgInstruction newInstr = CreateInstruction(new SegmentedAddress(0x2000, 1));
+ newInstr.SetLive(false);
+ _linker.ReplaceInstruction(instructions[1], newInstr);
+
+ block.IsLive.Should().BeFalse();
+ }
+
+ [Fact]
+ public void IntraBlockBackEdge_SplitsBlockAtTarget() {
+ // Arrange: three consecutive instructions A, B, C where C is a block terminator.
+ CfgInstruction a = CreateInstruction(new SegmentedAddress(BaseSegment, 0));
+ CfgInstruction b = CreateInstruction(new SegmentedAddress(BaseSegment, 1));
+ CfgInstruction c = CreateInstruction(new SegmentedAddress(BaseSegment, 2));
+ c.MarkAsBlockTerminator();
+
+ // Act: build block [A, B, C] via sequential linking, then create back-edge C->B.
+ _linker.Link(InstructionSuccessorType.Normal, a, b);
+ _linker.Link(InstructionSuccessorType.Normal, b, c);
+
+ CfgBlock blockBeforeBackEdge = GetContainingBlock(a);
+ blockBeforeBackEdge.Instructions.Should().HaveCount(3, "block should be [A, B, C] before back-edge");
+ blockBeforeBackEdge.IsDiscoveryComplete.Should().BeTrue("C is a terminator appended by continuation");
+
+ _linker.Link(InstructionSuccessorType.Normal, c, b);
+
+ // Assert: the back-edge C->B should split the block so that B becomes an entry point.
+ CfgBlock aBlock = GetContainingBlock(a);
+ CfgBlock bBlock = GetContainingBlock(b);
+ CfgBlock cBlock = GetContainingBlock(c);
+ bBlock.Should().NotBeSameAs(aBlock,
+ "B is a back-edge target and must be split into its own block");
+ bBlock.Entry.Should().BeSameAs(b, "B must be the entry of the new block");
+ bBlock.Instructions.Should().Equal([b, c], "target suffix block should be [B, C]");
+ bBlock.Terminator.Should().BeSameAs(c);
+ bBlock.IsDiscoveryComplete.Should().BeTrue("suffix block inherits completed discovery");
+ cBlock.Should().BeSameAs(bBlock);
+ aBlock.Instructions.Should().HaveCount(1, "prefix block should be [A]");
+ aBlock.Entry.Should().BeSameAs(a);
+ aBlock.IsDiscoveryComplete.Should().BeTrue("prefix block is completed on split");
+ }
+
+ [Fact]
+ public void IntraBlockBackEdge_ToEntry_DoesNotSplit() {
+ // Arrange: back-edge to the block entry (A) from the terminator (B) should NOT
+ // split A's block: A is already the entry.
+ CfgInstruction a = CreateInstruction(new SegmentedAddress(BaseSegment, 0));
+ CfgInstruction b = CreateInstruction(new SegmentedAddress(BaseSegment, 1));
+ b.MarkAsBlockTerminator();
+
+ // Act
+ _linker.Link(InstructionSuccessorType.Normal, a, b);
+ CfgBlock blockBefore = GetContainingBlock(a);
+ blockBefore.Instructions.Should().Equal([a, b]);
+
+ _linker.Link(InstructionSuccessorType.Normal, b, a);
+
+ // Assert: block stays [A, B] because A is already the entry.
+ CfgBlock blockAfter = GetContainingBlock(a);
+ blockAfter.Should().BeSameAs(blockBefore);
+ blockAfter.Instructions.Should().Equal([a, b]);
+ blockAfter.Entry.Should().BeSameAs(a);
+ }
+
+ [Fact]
+ public void IntraBlockNeighborEdge_ToCompletedInteriorNode_RemainsIdempotent() {
+ // Arrange: completed block [A, B, C] where C is a terminator. Re-linking A->B
+ // returns B, but B is still only an internal neighbor in the graph.
+ CfgInstruction a = CreateInstruction(new SegmentedAddress(BaseSegment, 0));
+ CfgInstruction b = CreateInstruction(new SegmentedAddress(BaseSegment, 1));
+ CfgInstruction c = CreateInstruction(new SegmentedAddress(BaseSegment, 2));
+ c.MarkAsBlockTerminator();
+
+ _linker.Link(InstructionSuccessorType.Normal, a, b);
+ _linker.Link(InstructionSuccessorType.Normal, b, c);
+ CfgBlock blockBefore = GetContainingBlock(a);
+ blockBefore.Instructions.Should().HaveCount(3);
+
+ // Act: re-link the existing neighbor edge A->B via a different link type.
+ _linker.Link(InstructionSuccessorType.CpuFault, a, b);
+
+ // Assert: the block shape is unchanged; returned-node normalization is not a graph rule.
+ CfgBlock blockAfter = GetContainingBlock(a);
+ CfgBlock bBlock = GetContainingBlock(b);
+ blockAfter.Should().BeSameAs(blockBefore);
+ blockAfter.Instructions.Should().Equal([a, b, c]);
+ blockAfter.Entry.Should().BeSameAs(a);
+ bBlock.Should().BeSameAs(blockAfter);
+ }
+
+ [Fact]
+ public void IntraBlockEdge_InteriorToEntry_SplitsAfterCurrent() {
+ // Arrange: block [A, B, C, D] where D is a terminator — a CPU fault edge from B to A.
+ // B is interior and not A's neighbor in the block for this edge direction.
+ // A is already the entry so no split there, but B must become a terminator
+ // by splitting after it.
+ CfgInstruction a = CreateInstruction(new SegmentedAddress(BaseSegment, 0));
+ CfgInstruction b = CreateInstruction(new SegmentedAddress(BaseSegment, 1));
+ CfgInstruction c = CreateInstruction(new SegmentedAddress(BaseSegment, 2));
+ CfgInstruction d = CreateInstruction(new SegmentedAddress(BaseSegment, 3));
+ d.MarkAsBlockTerminator();
+
+ _linker.Link(InstructionSuccessorType.Normal, a, b);
+ _linker.Link(InstructionSuccessorType.Normal, b, c);
+ _linker.Link(InstructionSuccessorType.Normal, c, d);
+ GetContainingBlock(a).Instructions.Should().HaveCount(4, "block should be [A, B, C, D]");
+
+ // Act: CPU fault edge from B (interior, index 1) to A (entry, index 0).
+ _linker.Link(InstructionSuccessorType.CpuFault, b, a);
+
+ // Assert: B is now a terminator; block split into [A, B] and [C, D].
+ CfgBlock abBlock = GetContainingBlock(a);
+ CfgBlock cdBlock = GetContainingBlock(c);
+ abBlock.Should().NotBeSameAs(cdBlock);
+ abBlock.Instructions.Should().HaveCount(2, "prefix block should be [A, B]");
+ abBlock.Entry.Should().BeSameAs(a);
+ abBlock.Terminator.Should().BeSameAs(b);
+ abBlock.IsDiscoveryComplete.Should().BeTrue("prefix block is completed on split");
+ cdBlock.Instructions.Should().Equal([c, d], "suffix block should be [C, D]");
+ cdBlock.Entry.Should().BeSameAs(c);
+ cdBlock.Terminator.Should().BeSameAs(d);
+ cdBlock.IsDiscoveryComplete.Should().BeTrue("suffix block is completed on split");
+ GetContainingBlock(d).Should().BeSameAs(cdBlock);
+ }
+
+ [Fact]
+ public void IntraBlockForwardSkip_SplitsAtTarget() {
+ // Arrange: block [A, B, C, D, E] where E is a terminator — a forward edge from A to D skipping B, C.
+ CfgInstruction a = CreateInstruction(new SegmentedAddress(BaseSegment, 0));
+ CfgInstruction b = CreateInstruction(new SegmentedAddress(BaseSegment, 1));
+ CfgInstruction c = CreateInstruction(new SegmentedAddress(BaseSegment, 2));
+ CfgInstruction d = CreateInstruction(new SegmentedAddress(BaseSegment, 3));
+ CfgInstruction e = CreateInstruction(new SegmentedAddress(BaseSegment, 4));
+ e.MarkAsBlockTerminator();
+
+ _linker.Link(InstructionSuccessorType.Normal, a, b);
+ _linker.Link(InstructionSuccessorType.Normal, b, c);
+ _linker.Link(InstructionSuccessorType.Normal, c, d);
+ _linker.Link(InstructionSuccessorType.Normal, d, e);
+ GetContainingBlock(a).Instructions.Should().HaveCount(5, "block should be [A, B, C, D, E]");
+
+ // Act: forward skip edge from A (index 0) to D (index 3).
+ _linker.Link(InstructionSuccessorType.CpuFault, a, d);
+
+ // Assert: split at D (new entry) and after A (new terminator).
+ // Result: [A], [B, C], [D, E].
+ CfgBlock aBlock = GetContainingBlock(a);
+ CfgBlock bcBlock = GetContainingBlock(b);
+ CfgBlock deBlock = GetContainingBlock(d);
+ aBlock.Instructions.Should().HaveCount(1, "A is now a single-instruction block");
+ aBlock.Entry.Should().BeSameAs(a);
+ bcBlock.Should().NotBeSameAs(aBlock);
+ bcBlock.Instructions.Should().HaveCount(2, "middle block should be [B, C]");
+ bcBlock.Entry.Should().BeSameAs(b);
+ deBlock.Should().NotBeSameAs(bcBlock);
+ deBlock.Instructions.Should().Equal([d, e], "target block should be [D, E]");
+ deBlock.Entry.Should().BeSameAs(d);
+ aBlock.IsDiscoveryComplete.Should().BeTrue();
+ bcBlock.IsDiscoveryComplete.Should().BeTrue();
+ deBlock.IsDiscoveryComplete.Should().BeTrue();
+ GetContainingBlock(e).Should().BeSameAs(deBlock);
+ }
+
+ [Fact]
+ public void IntraBlockForwardFault_SplitsSourceMiddleAndTarget() {
+ // Arrange: block [A, B, C, D, E] where E is a terminator.
+ CfgInstruction a = CreateInstruction(new SegmentedAddress(BaseSegment, 0));
+ CfgInstruction b = CreateInstruction(new SegmentedAddress(BaseSegment, 1));
+ CfgInstruction c = CreateInstruction(new SegmentedAddress(BaseSegment, 2));
+ CfgInstruction d = CreateInstruction(new SegmentedAddress(BaseSegment, 3));
+ CfgInstruction e = CreateInstruction(new SegmentedAddress(BaseSegment, 4));
+ e.MarkAsBlockTerminator();
+
+ _linker.Link(InstructionSuccessorType.Normal, a, b);
+ _linker.Link(InstructionSuccessorType.Normal, b, c);
+ _linker.Link(InstructionSuccessorType.Normal, c, d);
+ _linker.Link(InstructionSuccessorType.Normal, d, e);
+
+ // Act: CPU fault edge from B to D skips C and makes B a block terminator.
+ _linker.Link(InstructionSuccessorType.CpuFault, b, d);
+
+ // Assert: both endpoints are normalized, producing [A, B], [C], and [D, E].
+ CfgBlock abBlock = GetContainingBlock(a);
+ CfgBlock cBlock = GetContainingBlock(c);
+ CfgBlock deBlock = GetContainingBlock(d);
+ abBlock.Instructions.Should().Equal([a, b]);
+ abBlock.Terminator.Should().BeSameAs(b);
+ cBlock.Should().NotBeSameAs(abBlock);
+ cBlock.Instructions.Should().Equal([c]);
+ cBlock.Entry.Should().BeSameAs(c);
+ deBlock.Should().NotBeSameAs(cBlock);
+ deBlock.Instructions.Should().Equal([d, e]);
+ deBlock.Entry.Should().BeSameAs(d);
+ deBlock.Terminator.Should().BeSameAs(e);
+ abBlock.IsDiscoveryComplete.Should().BeTrue();
+ cBlock.IsDiscoveryComplete.Should().BeTrue();
+ deBlock.IsDiscoveryComplete.Should().BeTrue();
+ }
+
+ [Fact]
+ public void Link_SameTriple_IsIdempotent_Continuation() {
+ using LinkerHarness harness = new();
+ CfgInstruction current = harness.CreateInstruction(new SegmentedAddress(BaseSegment, 0));
+ CfgInstruction next = harness.CreateInstruction(new SegmentedAddress(BaseSegment, 1));
+
+ ICfgNode first = harness.Linker.Link(InstructionSuccessorType.Normal, current, next);
+ int successorsAfterFirst = current.Successors.Count;
+ int predecessorsAfterFirst = next.Predecessors.Count;
+ CfgBlock? blockAfterFirst = current.ContainingBlock;
+
+ ICfgNode second = harness.Linker.Link(InstructionSuccessorType.Normal, current, next);
+
+ second.Should().BeSameAs(first, "Link must return the same node");
+ current.Successors.Should().HaveCount(successorsAfterFirst, "no duplicate edges");
+ next.Predecessors.Should().HaveCount(predecessorsAfterFirst, "no duplicate edges");
+ current.ContainingBlock.Should().BeSameAs(blockAfterFirst, "no new block created");
+ }
+
+ [Fact]
+ public void Link_SameTriple_IsIdempotent_Boundary() {
+ using LinkerHarness harness = new();
+ CfgInstruction current = harness.CreateInstruction(new SegmentedAddress(BaseSegment, 0));
+ current.MarkAsBlockTerminator();
+ CfgInstruction next = harness.CreateInstruction(new SegmentedAddress(BaseSegment, 1));
+
+ ICfgNode first = harness.Linker.Link(InstructionSuccessorType.Normal, current, next);
+ int currentBlockSize = GetContainingBlock(current).Instructions.Count;
+ int nextBlockSize = GetContainingBlock(next).Instructions.Count;
+
+ ICfgNode second = harness.Linker.Link(InstructionSuccessorType.Normal, current, next);
+
+ second.Should().BeSameAs(first);
+ GetContainingBlock(current).Instructions.Should().HaveCount(currentBlockSize);
+ GetContainingBlock(next).Instructions.Should().HaveCount(nextBlockSize);
+ }
+
+ ///
+ /// Builds a block of N instructions with ContainingBlock back-pointers set (no linker).
+ ///
+ private static CfgInstruction[] BuildBlock(int size) {
+ CfgInstruction[] instructions = new CfgInstruction[size];
+ for (int i = 0; i < size; i++) {
+ instructions[i] = CreateInstruction(new SegmentedAddress(BaseSegment, (ushort)i));
+ }
+ CfgBlock block = new(_blockAllocator.AllocateId(), instructions[0]);
+ for (int i = 1; i < size; i++) {
+ block.Append(instructions[i]);
+ }
+ foreach (CfgInstruction instr in instructions) {
+ instr.ContainingBlock = block;
+ }
+ return instructions;
+ }
+
+ [Fact]
+ public void ColdPath_PopfBothStarterAndTerminator_FormsSingleInstructionBlock() {
+ using LinkerHarness harness = new();
+ CfgInstruction before = harness.CreateInstruction(new SegmentedAddress(BaseSegment, 0));
+ CfgInstruction popf = harness.CreateInstruction(new SegmentedAddress(BaseSegment, 1));
+ popf.MarkAsBlockStarter();
+ popf.MarkAsBlockTerminator();
+ CfgInstruction after = harness.CreateInstruction(new SegmentedAddress(BaseSegment, 2));
+
+ harness.Linker.Link(InstructionSuccessorType.Normal, before, popf);
+ harness.Linker.Link(InstructionSuccessorType.Normal, popf, after);
+
+ CfgBlock popfBlock = GetContainingBlock(popf);
+ popfBlock.Instructions.Should().HaveCount(1, "POPF is both starter and terminator so it must be alone in its block");
+ popfBlock.Entry.Should().BeSameAs(popf);
+ popfBlock.Terminator.Should().BeSameAs(popf);
+ popfBlock.IsDiscoveryComplete.Should().BeTrue();
+ popfBlock.Should().NotBeSameAs(GetContainingBlock(before), "POPF starter flag must split it from its predecessor");
+ popfBlock.Should().NotBeSameAs(GetContainingBlock(after), "POPF terminator flag must split it from its successor");
+ }
+
+ ///
+ /// Builds a block of N instructions by linking them through the NodeLinker (cold path).
+ ///
+ private CfgInstruction[] BuildLinkedBlock(int size) {
+ CfgInstruction[] instructions = new CfgInstruction[size];
+ for (int i = 0; i < size; i++) {
+ instructions[i] = CreateInstruction(new SegmentedAddress(BaseSegment, (ushort)i));
+ }
+ for (int i = 0; i < size - 1; i++) {
+ _linker.Link(InstructionSuccessorType.Normal, instructions[i], instructions[i + 1]);
+ }
+ return instructions;
+ }
+
+}
+
diff --git a/tests/Spice86.Tests/CfgCpu/Blocks/CfgCpuHotPathTest.cs b/tests/Spice86.Tests/CfgCpu/Blocks/CfgCpuHotPathTest.cs
new file mode 100644
index 0000000000..16620d1f9b
--- /dev/null
+++ b/tests/Spice86.Tests/CfgCpu/Blocks/CfgCpuHotPathTest.cs
@@ -0,0 +1,213 @@
+namespace Spice86.Tests.CfgCpu.Blocks;
+
+using FluentAssertions;
+
+using Spice86.Core.Emulator.CPU;
+using Spice86.Core.Emulator.CPU.CfgCpu;
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
+using Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor;
+using Spice86.Core.Emulator.CPU.CfgCpu.Linker;
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
+using Spice86.Core.Emulator.CPU.Exceptions;
+using Spice86.Shared.Emulator.Memory;
+using Spice86.Tests.CpuTests.SingleStepTests;
+
+using ExecutionContext = Spice86.Core.Emulator.CPU.CfgCpu.Linker.ExecutionContext;
+
+using System.Collections.Generic;
+using System.Linq;
+
+using static CfgTestHelpers;
+
+using Xunit;
+
+///
+/// Deterministic tests for the hot-path block walker in .
+/// Replaces the CsCheck-based CfgCpuHotPathPbt with explicit edge-case inputs.
+///
+public class CfgCpuHotPathTest : IDisposable {
+ private const ushort BaseSegment = 0x1000;
+ private const int BlockSize = 4;
+ private static readonly CfgNodeIdAllocator _allocator = new();
+ private readonly SingleStepTestMinimalMachine _machine = new(CpuModel.INTEL_80286);
+
+ private CfgCpu Cpu => _machine.Cpu;
+ private State State => _machine.State;
+
+ public void Dispose() {
+ _machine.Dispose();
+ }
+
+ ///
+ /// The hot path executes every instruction in a discovery-complete block exactly once
+ /// and finishes at the terminator.
+ ///
+ [Fact]
+ public void HotPath_ExecutesEntireBlock_ExactlyOnce() {
+ ExecutionContext ctx = Cpu.ExecutionContextManager.CurrentExecutionContext;
+
+ int executionCount = 0;
+ (CfgBlock block, CfgInstruction[] instructions) = BuildBlock(BlockSize, (instr, i) => {
+ instr.CompiledExecution = _ => executionCount++;
+ });
+
+ ctx.LastExecuted = null;
+ ctx.NodeToExecuteNextAccordingToGraph = instructions[0];
+
+ long cyclesBefore = State.Cycles;
+ Cpu.ExecuteNext();
+ long cyclesAfter = State.Cycles;
+
+ executionCount.Should().Be(BlockSize,
+ "every instruction must execute exactly once");
+ (cyclesAfter - cyclesBefore).Should().Be(BlockSize,
+ "each instruction increments cycles once");
+ ctx.LastExecuted.Should().BeSameAs(block.Terminator,
+ "walk must finish at the terminator");
+ }
+
+ ///
+ /// When the graph points at an interior node of a discovered block, the executor must cold-step
+ /// only that node rather than replaying the block prefix from the entry.
+ ///
+ [Fact]
+ public void ExecuteNext_ColdSteps_WhenCompletedBlockNextNodeIsInterior() {
+ ExecutionContext ctx = Cpu.ExecutionContextManager.CurrentExecutionContext;
+
+ List trace = new();
+ (CfgBlock block, CfgInstruction[] instructions) = BuildBlock(BlockSize, (instr, i) => {
+ int captured = i;
+ instr.CompiledExecution = _ => trace.Add(captured);
+ });
+
+ ctx.LastExecuted = null;
+ ctx.NodeToExecuteNextAccordingToGraph = instructions[2];
+
+ long cyclesBefore = State.Cycles;
+ Cpu.ExecuteNext();
+ long cyclesAfter = State.Cycles;
+
+ trace.Should().Equal([2],
+ "an interior entry is outside the block hot-path contract");
+ (cyclesAfter - cyclesBefore).Should().Be(1,
+ "cold stepping executes one node");
+ ctx.LastExecuted.Should().BeSameAs(instructions[2]);
+ block.Entry.Should().BeSameAs(instructions[0]);
+ }
+
+ ///
+ /// Incomplete blocks are still being discovered, so the executor must keep taking the cold path.
+ ///
+ [Fact]
+ public void ExecuteNext_ColdSteps_WhenBlockDiscoveryIsIncomplete() {
+ ExecutionContext ctx = Cpu.ExecutionContextManager.CurrentExecutionContext;
+ List trace = new();
+ (CfgBlock block, CfgInstruction[] instructions) = BuildBlock(BlockSize, (instr, i) => {
+ int captured = i;
+ instr.CompiledExecution = _ => trace.Add(captured);
+ });
+ block.IsDiscoveryComplete = false;
+
+ ctx.LastExecuted = null;
+ ctx.NodeToExecuteNextAccordingToGraph = instructions[0];
+
+ Cpu.ExecuteNext();
+
+ trace.Should().Equal([0]);
+ ctx.LastExecuted.Should().BeSameAs(instructions[0]);
+ }
+
+ ///
+ /// Non-live blocks may contain stale compiled nodes, so entry dispatch must remain cold.
+ ///
+ [Fact]
+ public void ExecuteNext_ColdSteps_WhenBlockIsNotLive() {
+ ExecutionContext ctx = Cpu.ExecutionContextManager.CurrentExecutionContext;
+ List trace = new();
+ (CfgBlock block, CfgInstruction[] instructions) = BuildBlock(BlockSize, (instr, i) => {
+ int captured = i;
+ instr.CompiledExecution = _ => trace.Add(captured);
+ });
+ instructions[1].SetLive(false);
+
+ ctx.LastExecuted = null;
+ ctx.NodeToExecuteNextAccordingToGraph = instructions[0];
+
+ Cpu.ExecuteNext();
+
+ block.IsLive.Should().BeFalse();
+ trace.Should().Equal([0]);
+ ctx.LastExecuted.Should().BeSameAs(instructions[0]);
+ }
+
+ ///
+ /// The hot path stops before executing a non-live instruction.
+ ///
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(3)]
+ public void HotPath_StopsBeforeNonLiveInstruction(int nonLiveIndex) {
+ List trace = new();
+ (CfgBlock block, CfgInstruction[] instructions) = BuildBlock(BlockSize, (instr, i) => {
+ int captured = i;
+ instr.CompiledExecution = _ => trace.Add(captured);
+ });
+
+ instructions[nonLiveIndex].SetLive(false);
+
+ Cpu.ExecuteBlock(block);
+
+ trace.Should().Equal(Enumerable.Range(0, nonLiveIndex),
+ "hot path must stop before the non-live instruction");
+ }
+
+ ///
+ /// The hot path stops after executing an instruction that throws CpuException.
+ ///
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(3)]
+ public void HotPath_StopsAfterCpuException(int faultIndex) {
+ List trace = new();
+ (CfgBlock block, CfgInstruction[] instructions) = BuildBlock(BlockSize, (instr, i) => {
+ int captured = i;
+ bool shouldThrow = captured == faultIndex;
+ instr.CompiledExecution = _ => {
+ trace.Add(captured);
+ if (shouldThrow) {
+ throw new CpuDivisionErrorException("test fault");
+ }
+ };
+ });
+
+ ICfgNode lastExecuted = Cpu.ExecuteBlock(block);
+
+ trace.Should().Equal(Enumerable.Range(0, faultIndex + 1),
+ "hot path must stop after the faulting instruction");
+ lastExecuted.Should().BeSameAs(instructions[faultIndex]);
+ }
+
+ private static (CfgBlock Block, CfgInstruction[] Instructions) BuildBlock(int size, Action configure) {
+ CfgInstruction[] instructions = new CfgInstruction[size];
+ for (int i = 0; i < size; i++) {
+ CfgInstruction instr = CreateInstruction(new SegmentedAddress(BaseSegment, (ushort)i));
+ configure(instr, i);
+ instructions[i] = instr;
+ }
+ CfgBlock block = new(_allocator.AllocateId(), instructions[0]);
+ for (int i = 1; i < size; i++) {
+ block.Append(instructions[i]);
+ }
+ foreach (CfgInstruction instr in instructions) {
+ instr.ContainingBlock = block;
+ }
+ block.IsDiscoveryComplete = true;
+ return (block, instructions);
+ }
+
+}
+
diff --git a/tests/Spice86.Tests/CfgCpu/Blocks/CfgTestHelpers.cs b/tests/Spice86.Tests/CfgCpu/Blocks/CfgTestHelpers.cs
new file mode 100644
index 0000000000..43c895d6e7
--- /dev/null
+++ b/tests/Spice86.Tests/CfgCpu/Blocks/CfgTestHelpers.cs
@@ -0,0 +1,50 @@
+namespace Spice86.Tests.CfgCpu.Blocks;
+
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.Prefix;
+using Spice86.Shared.Emulator.Memory;
+
+using System.Collections.Immutable;
+
+///
+/// Shared factory helpers for CFG block tests.
+///
+internal static class CfgTestHelpers {
+ private static readonly CfgNodeIdAllocator _allocator = new();
+
+ ///
+ /// Builds a synthetic NOP at .
+ ///
+ internal static CfgInstruction CreateInstruction(SegmentedAddress address) {
+ return CreateInstruction(address, 0x90, 1, InstructionKind.None);
+ }
+
+ ///
+ /// Builds a synthetic at with the
+ /// specified , , and .
+ ///
+ internal static CfgInstruction CreateInstruction(SegmentedAddress address, byte opcode, int length, InstructionKind kind) {
+ InstructionField opcodeField = new(
+ indexInInstruction: 0,
+ length: length,
+ physicalAddress: address.Linear,
+ value: opcode,
+ signatureValue: ImmutableList.Create(opcode),
+ final: true);
+ return new(_allocator.AllocateId(), address, opcodeField, new List(), maxSuccessorsCount: 1) {
+ Kind = kind
+ };
+ }
+
+ ///
+ /// Returns the that contains ,
+ /// or throws if the node has no containing block.
+ ///
+ internal static CfgBlock GetContainingBlock(ICfgNode node) {
+ if (node.ContainingBlock is CfgBlock block) {
+ return block;
+ }
+ throw new InvalidOperationException($"Node at {node.Address} has no containing block.");
+ }
+}
diff --git a/tests/Spice86.Tests/CfgCpu/Blocks/EmulatorMcpToolsTest.cs b/tests/Spice86.Tests/CfgCpu/Blocks/EmulatorMcpToolsTest.cs
new file mode 100644
index 0000000000..07c13b5e17
--- /dev/null
+++ b/tests/Spice86.Tests/CfgCpu/Blocks/EmulatorMcpToolsTest.cs
@@ -0,0 +1,124 @@
+namespace Spice86.Tests.CfgCpu.Blocks;
+
+using FluentAssertions;
+
+using ModelContextProtocol.Protocol;
+
+using Spice86;
+using Spice86.Core.Emulator.CPU.CfgCpu;
+using Spice86.Core.Emulator.Mcp;
+using Spice86.Core.Emulator.StateSerialization;
+using Spice86.Mcp;
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+using Xunit;
+
+///
+/// Tests concrete read_cfg_cpu_graph MCP tool scenarios.
+///
+public class EmulatorMcpToolsTest {
+ private const string TestProgramName = "add";
+
+ private static readonly JsonSerializerOptions DeserializerOptions = new() {
+ PropertyNameCaseInsensitive = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ Converters = { new JsonStringEnumConverter() }
+ };
+
+ ///
+ /// After has been invoked, the BFS that
+ /// powers read_cfg_cpu_graph has no seed blocks (no entry-point
+ /// instructions, no LastExecuted) and therefore must return an
+ /// empty Blocks array while still populating the other top-level
+ /// fields of the response.
+ ///
+ [Fact]
+ public void ReadCfgCpuGraph_AfterClear_ReturnsEmptyBlocks() {
+ // Arrange boot a real Spice86 dependency-injection graph so that
+ // EmulatorMcpTools is wired against a production-shaped CfgCpu and
+ // ExecutionContextManager. We do NOT run the program: the test
+ // operates on the pristine, then-cleared graph.
+ Spice86Creator creator = new(TestProgramName);
+ using Spice86DependencyInjection spice86 = creator.Create();
+ EmulatorMcpServices services = spice86.McpServices;
+ EmulatorMcpTools tools = new(services);
+
+ // Pre-pause so EmulatorMcpTools.ExecuteTool's auto-pause branch does
+ // not request a pause that would never resolve without an emulation
+ // loop running.
+ services.PauseHandler.RequestPause("test setup");
+
+ // Drop every seed: clears ExecutionContextEntryPoints, current
+ // execution context (so LastExecuted becomes null), and all
+ // instruction caches.
+ services.CfgCpu.Clear();
+
+ // Act
+ CallToolResult result = tools.ReadCfgCpuGraph(nodeLimit: null);
+
+ // Assert
+ result.IsError.Should().NotBe(true);
+ result.StructuredContent.Should().NotBeNull();
+ CfgCpuGraph response = DeserializeResponse(result);
+
+ response.Blocks.Should().NotBeNull();
+ response.Blocks.Should().BeEmpty(
+ "after CfgCpu.Clear() there are no entry points and no LastExecuted, " +
+ "so the block-centric BFS has no seeds");
+ response.Truncated.Should().BeFalse();
+ response.LastExecutedAddress.Should().BeNull();
+ response.LastExecutedBlockId.Should().BeNull();
+ response.TotalEntryPoints.Should().Be(0);
+ response.EntryPointAddresses.Should().BeEmpty();
+ response.CurrentContextDepth.Should().Be(0);
+ }
+
+ private static CfgCpuGraph DeserializeResponse(CallToolResult result) {
+ result.IsError.Should().NotBe(true);
+ result.StructuredContent.Should().NotBeNull();
+
+ CfgCpuGraph? response = result.StructuredContent.Value.Deserialize(DeserializerOptions);
+ if (response == null) {
+ throw new InvalidOperationException("The structured MCP response could not be deserialized.");
+ }
+ return response;
+ }
+
+ ///
+ /// When the graph is truncated by nodeLimit, every pred/succ ID in the returned
+ /// blocks must reference a block that is also present in the returned block list.
+ /// No edge endpoint may reference an omitted block.
+ ///
+ [Fact]
+ public void ReadCfgCpuGraph_TruncatedGraph_PredSuccAreClosedToIncludedBlocks() {
+ // Arrange — run the full "add" program so the CFG is populated with real blocks
+ Spice86Creator creator = new(TestProgramName);
+ using Spice86DependencyInjection spice86 = creator.Create();
+ spice86.ProgramExecutor.Run();
+ EmulatorMcpServices services = spice86.McpServices;
+ EmulatorMcpTools tools = new(services);
+ services.PauseHandler.RequestPause("test setup");
+
+ // Act — nodeLimit=1 forces truncation on any real multi-block program
+ CallToolResult result = tools.ReadCfgCpuGraph(nodeLimit: 1);
+
+ // Assert
+ result.IsError.Should().NotBe(true);
+ CfgCpuGraph response = DeserializeResponse(result);
+ response.Blocks.Should().NotBeEmpty("running the program must produce at least one block");
+
+ HashSet includedIds = response.Blocks.Select(b => b.Id).ToHashSet();
+ foreach (CfgBlockInfo block in response.Blocks) {
+ foreach (int predId in block.Pred) {
+ includedIds.Should().Contain(predId,
+ $"block {block.Id} pred references id {predId} which is not in the returned block list");
+ }
+ foreach (int succId in block.Succ) {
+ includedIds.Should().Contain(succId,
+ $"block {block.Id} succ references id {succId} which is not in the returned block list");
+ }
+ }
+ }
+}
diff --git a/tests/Spice86.Tests/CfgCpu/Blocks/FlagControlParserBlockBoundaryTest.cs b/tests/Spice86.Tests/CfgCpu/Blocks/FlagControlParserBlockBoundaryTest.cs
new file mode 100644
index 0000000000..940ec025c1
--- /dev/null
+++ b/tests/Spice86.Tests/CfgCpu/Blocks/FlagControlParserBlockBoundaryTest.cs
@@ -0,0 +1,71 @@
+namespace Spice86.Tests.CfgCpu.Blocks;
+
+using FluentAssertions;
+
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
+using Spice86.Shared.Emulator.Memory;
+
+using Xunit;
+
+///
+/// Verifies that
+/// flags STI and CLI as block boundaries at parse time.
+///
+public class FlagControlParserBlockBoundaryTest {
+ private readonly TestInstructionHelper _helper = new();
+
+ ///
+ /// STI (opcode 0xFB) must end its CfgBlock so that external interrupt delivery happens
+ /// at the boundary just after interrupts are enabled (after the one-instruction shadow).
+ ///
+ [Fact]
+ public void Sti_SetsExplicitTerminatorFlag() {
+ // Arrange
+ SegmentedAddress address = new(0x0000, 0x0000);
+
+ // Act
+ CfgInstruction instruction = _helper.WriteAndParse(address, w => w.WriteUInt8(0xFB));
+
+ // Assert
+ instruction.IsBlockTerminator.Should().BeTrue(
+ "the STI parser must mark the parsed instruction as a block terminator");
+ }
+
+ ///
+ /// CLI (opcode 0xFA) must start a new CfgBlock so that external interrupt delivery happens
+ /// at the boundary just before interrupts are disabled.
+ ///
+ [Fact]
+ public void Cli_SetsExplicitStarterFlag() {
+ // Arrange
+ SegmentedAddress address = new(0x0000, 0x0000);
+
+ // Act
+ CfgInstruction instruction = _helper.WriteAndParse(address, w => w.WriteUInt8(0xFA));
+
+ // Assert
+ instruction.IsBlockStarter.Should().BeTrue(
+ "the CLI parser must mark the parsed instruction as a block starter");
+ }
+
+ ///
+ /// POPF (opcode 0x9D) must be simultaneously a block terminator and a block starter.
+ /// It is a terminator because it restores flags (including the interrupt flag) from the stack,
+ /// and a starter so that any preceding instructions are sealed into their own block before
+ /// POPF takes effect.
+ ///
+ [Fact]
+ public void Popf_SetsBothStarterAndTerminatorFlags() {
+ // Arrange
+ SegmentedAddress address = new(0x0000, 0x0000);
+
+ // Act
+ CfgInstruction instruction = _helper.WriteAndParse(address, w => w.WriteUInt8(0x9D));
+
+ // Assert
+ instruction.IsBlockTerminator.Should().BeTrue(
+ "the POPF parser must mark the instruction as a block terminator");
+ instruction.IsBlockStarter.Should().BeTrue(
+ "the POPF parser must mark the instruction as a block starter");
+ }
+}
diff --git a/tests/Spice86.Tests/CfgCpu/Blocks/LinkerHarness.cs b/tests/Spice86.Tests/CfgCpu/Blocks/LinkerHarness.cs
new file mode 100644
index 0000000000..0f9413629f
--- /dev/null
+++ b/tests/Spice86.Tests/CfgCpu/Blocks/LinkerHarness.cs
@@ -0,0 +1,53 @@
+namespace Spice86.Tests.CfgCpu.Blocks;
+
+using NSubstitute;
+
+using Spice86.Core.CLI;
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
+using Spice86.Core.Emulator.CPU.CfgCpu.Feeder;
+using Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor.Expressions;
+using Spice86.Core.Emulator.CPU.CfgCpu.Linker;
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
+using Spice86.Shared.Emulator.Memory;
+using Spice86.Shared.Interfaces;
+
+///
+/// Disposable test harness that constructs a real with its production
+/// dependencies. Each test instantiates a fresh harness so the JIT compiler's background thread
+/// does not leak across tests.
+///
+internal sealed class LinkerHarness : IDisposable {
+ private readonly CfgNodeExecutionCompiler _compiler;
+ private readonly CfgNodeExecutionCompilerMonitor _monitor;
+ private readonly CfgNodeIdAllocator _idAllocator;
+
+ public LinkerHarness() {
+ ILoggerService loggerService = Substitute.For();
+ _monitor = new CfgNodeExecutionCompilerMonitor(loggerService);
+ _compiler = new CfgNodeExecutionCompiler(_monitor, loggerService, JitMode.InterpretedOnly);
+ _idAllocator = new CfgNodeIdAllocator();
+ Linker = new NodeLinker(new InstructionReplacerRegistry(), _compiler, _idAllocator);
+ }
+
+ public NodeLinker Linker { get; }
+
+ ///
+ /// Builds a synthetic NOP at .
+ ///
+ public CfgInstruction CreateInstruction(SegmentedAddress address) {
+ return CfgTestHelpers.CreateInstruction(address);
+ }
+
+ ///
+ /// Builds a synthetic at with the
+ /// specified , , and .
+ ///
+ public CfgInstruction CreateInstruction(SegmentedAddress address, byte opcode, int length, InstructionKind kind) {
+ return CfgTestHelpers.CreateInstruction(address, opcode, length, kind);
+ }
+
+ public void Dispose() {
+ _compiler.Dispose();
+ _monitor.Dispose();
+ }
+}
diff --git a/tests/Spice86.Tests/CfgCpu/Blocks/SelectorBlockAssignmentTest.cs b/tests/Spice86.Tests/CfgCpu/Blocks/SelectorBlockAssignmentTest.cs
new file mode 100644
index 0000000000..2c813b4c6c
--- /dev/null
+++ b/tests/Spice86.Tests/CfgCpu/Blocks/SelectorBlockAssignmentTest.cs
@@ -0,0 +1,288 @@
+namespace Spice86.Tests.CfgCpu.Blocks;
+
+using FluentAssertions;
+
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
+using Spice86.Core.Emulator.CPU.CfgCpu.Linker;
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.SelfModifying;
+using Spice86.Shared.Emulator.Memory;
+
+using static CfgTestHelpers;
+
+using Xunit;
+
+///
+/// Tests how places selector nodes into blocks.
+///
+public class SelectorBlockAssignmentTest {
+ private const ushort PredecessorSegment = 0x1000;
+ private const ushort VariantSegment = 0x2000;
+
+ [Fact]
+ public void Selector_BetweenNonTerminatorAndSameAddressVariant_RemainsInPredecessorBlock() {
+ using LinkerHarness harness = new();
+ SegmentedAddress aAddr = new(PredecessorSegment, 0x0000);
+ SegmentedAddress variantAddr = new(PredecessorSegment, 0x0001);
+ CfgInstruction a = harness.CreateInstruction(aAddr, 0x90, 1, InstructionKind.None);
+ CfgInstruction variantA = harness.CreateInstruction(variantAddr, 0x91, 1, InstructionKind.None);
+ harness.Linker.Link(InstructionSuccessorType.Normal, a, variantA);
+
+ CfgBlock alpha = GetContainingBlock(a);
+ alpha.Instructions.Should().HaveCount(2);
+ alpha.Entry.Should().BeSameAs(a);
+ alpha.Terminator.Should().BeSameAs(variantA);
+
+ CfgInstruction variantB = harness.CreateInstruction(variantAddr, 0x92, 1, InstructionKind.None);
+
+ SelectorNode selector = harness.Linker.CreateSelectorNodeBetween(variantA, variantB);
+
+ CfgBlock alphaAfter = GetContainingBlock(a);
+ CfgBlock selectorBlock = GetContainingBlock(selector);
+ alphaAfter.Instructions.Should().Equal([a, selector],
+ "an adjacent non-terminator predecessor can append the selector by normal continuation");
+ alphaAfter.Entry.Should().BeSameAs(a);
+ alphaAfter.Terminator.Should().BeSameAs(selector);
+ selectorBlock.Should().BeSameAs(alphaAfter,
+ "being returned for execution is not itself a graph boundary");
+ selectorBlock.Terminator.Should().BeSameAs(selector);
+
+ variantA.ContainingBlock.Should().NotBeSameAs(alphaAfter);
+ variantB.ContainingBlock.Should().NotBeSameAs(alphaAfter);
+ }
+
+ ///
+ /// Predecessor is a terminator (e.g. a CALL). The selector's
+ /// predecessor wiring goes through the boundary path (continuation cannot fire because the
+ /// predecessor is itself a block terminator); the selector must therefore land in its own
+ /// one-node block, not be silently dropped.
+ ///
+ ///
+ /// This is the selfmodifycall.bin regression: a call at F000:000D targets
+ /// the handler at F000:0017, and the patched return-point at F000:0010 ends up
+ /// guarded by a SelectorNode dispatching between nop and hlt. The call's
+ /// NextInMemoryAddress matches the selector's address but the call IS a block terminator,
+ /// so Case T cannot fire and AssignBlockForNext must open a block for the selector.
+ ///
+ ///
+ [Fact]
+ public void Selector_PredecessorIsCallTerminator_LandsInOwnOneNodeBlock() {
+ using LinkerHarness harness = new();
+ SegmentedAddress callAddr = new(PredecessorSegment, 0x0000);
+ SegmentedAddress variantAddr = new(PredecessorSegment, 0x0003);
+
+ // Predecessor is a call (terminator) with NextInMemoryAddress matching the variant address.
+ CfgInstruction call = harness.CreateInstruction(callAddr, 0xE8, 3, InstructionKind.Call);
+ CfgInstruction variantA = harness.CreateInstruction(variantAddr, 0x90, 1, InstructionKind.None);
+ harness.Linker.Link(InstructionSuccessorType.Normal, call, variantA);
+
+ CfgBlock callBlock = GetContainingBlock(call);
+ callBlock.Instructions.Should().HaveCount(1,
+ "the call is a block terminator, so its block contains only itself");
+ callBlock.Terminator.Should().BeSameAs(call);
+ callBlock.IsDiscoveryComplete.Should().BeTrue();
+
+ CfgInstruction variantB = harness.CreateInstruction(variantAddr, 0x91, 1, InstructionKind.None);
+
+ SelectorNode selector = harness.Linker.CreateSelectorNodeBetween(variantA, variantB);
+
+ CfgBlock selectorBlock = GetContainingBlock(selector);
+ selectorBlock.Should().NotBeSameAs(callBlock,
+ "the selector cannot be appended to a block whose terminator is a call (Case T does not fire)");
+ selectorBlock.Instructions.Should().HaveCount(1,
+ "the selector's block contains only the selector");
+ selectorBlock.Entry.Should().BeSameAs(selector);
+ selectorBlock.Terminator.Should().BeSameAs(selector);
+ selectorBlock.IsDiscoveryComplete.Should().BeTrue(
+ "a block terminator entry auto-closes its block on creation");
+
+ callBlock.Instructions.Should().HaveCount(1);
+ callBlock.Terminator.Should().BeSameAs(call);
+ }
+
+ ///
+ /// Predecessor is a RET terminator. Same expectation as a CALL: the selector lands
+ /// in its own one-node block.
+ ///
+ [Fact]
+ public void Selector_PredecessorIsReturnTerminator_LandsInOwnOneNodeBlock() {
+ using LinkerHarness harness = new();
+ SegmentedAddress retAddr = new(PredecessorSegment, 0x0000);
+ SegmentedAddress variantAddr = new(PredecessorSegment, 0x0001);
+
+ CfgInstruction ret = harness.CreateInstruction(retAddr, 0xC3, 1, InstructionKind.Return);
+ CfgInstruction variantA = harness.CreateInstruction(variantAddr, 0x90, 1, InstructionKind.None);
+ harness.Linker.Link(InstructionSuccessorType.Normal, ret, variantA);
+
+ CfgInstruction variantB = harness.CreateInstruction(variantAddr, 0x91, 1, InstructionKind.None);
+
+ SelectorNode selector = harness.Linker.CreateSelectorNodeBetween(variantA, variantB);
+
+ CfgBlock selectorBlock = GetContainingBlock(selector);
+ selectorBlock.Should().NotBeSameAs(ret.ContainingBlock,
+ "the selector cannot be appended to a ret-terminated block");
+ selectorBlock.Instructions.Should().HaveCount(1);
+ selectorBlock.Terminator.Should().BeSameAs(selector);
+ }
+
+ ///
+ /// Predecessor is an unconditional JMP terminator. Same expectation: selector lands
+ /// in its own one-node block.
+ ///
+ [Fact]
+ public void Selector_PredecessorIsJumpTerminator_LandsInOwnOneNodeBlock() {
+ using LinkerHarness harness = new();
+ SegmentedAddress jmpAddr = new(PredecessorSegment, 0x0000);
+ SegmentedAddress variantAddr = new(PredecessorSegment, 0x0002);
+
+ CfgInstruction jmp = harness.CreateInstruction(jmpAddr, 0xEB, 2, InstructionKind.Jump);
+ CfgInstruction variantA = harness.CreateInstruction(variantAddr, 0x90, 1, InstructionKind.None);
+ harness.Linker.Link(InstructionSuccessorType.Normal, jmp, variantA);
+
+ CfgInstruction variantB = harness.CreateInstruction(variantAddr, 0x91, 1, InstructionKind.None);
+
+ SelectorNode selector = harness.Linker.CreateSelectorNodeBetween(variantA, variantB);
+
+ CfgBlock selectorBlock = GetContainingBlock(selector);
+ selectorBlock.Should().NotBeSameAs(jmp.ContainingBlock);
+ selectorBlock.Instructions.Should().HaveCount(1);
+ selectorBlock.Terminator.Should().BeSameAs(selector);
+ }
+
+ ///
+ /// Predecessor is a non-terminator whose NextInMemoryAddress does NOT match
+ /// the variant's address (e.g. a far-jump landing back into the variant's address). Continuation
+ /// cannot fire because the address adjacency check fails; the selector must land in its own block.
+ ///
+ [Fact]
+ public void Selector_PredecessorIsNonAdjacent_LandsInOwnOneNodeBlock() {
+ using LinkerHarness harness = new();
+ SegmentedAddress predAddr = new(PredecessorSegment, 0x0000);
+ SegmentedAddress variantAddr = new(VariantSegment, 0x0000); // different segment, not adjacent
+
+ CfgInstruction pred = harness.CreateInstruction(predAddr, 0x90, 1, InstructionKind.None);
+ CfgInstruction variantA = harness.CreateInstruction(variantAddr, 0x91, 1, InstructionKind.None);
+ harness.Linker.Link(InstructionSuccessorType.Normal, pred, variantA);
+
+ // pred is non-terminator; its block is just [pred] in-discovery (pred's NextInMemoryAddress
+ // is PredecessorSegment:0x0001, NOT variantAddr, so cold-path continuation did not fold variantA in).
+ CfgBlock predBlock = GetContainingBlock(pred);
+ predBlock.Instructions.Should().ContainSingle().Which.Should().BeSameAs(pred,
+ "non-adjacent predecessor stays alone in its block (continuation fails on the address check)");
+
+ CfgInstruction variantB = harness.CreateInstruction(variantAddr, 0x92, 1, InstructionKind.None);
+
+ SelectorNode selector = harness.Linker.CreateSelectorNodeBetween(variantA, variantB);
+
+ CfgBlock selectorBlock = GetContainingBlock(selector);
+ selectorBlock.Should().NotBeSameAs(predBlock,
+ "non-adjacent address means continuation cannot fire, so the selector lands in its own block");
+ selectorBlock.Instructions.Should().HaveCount(1);
+ selectorBlock.Terminator.Should().BeSameAs(selector);
+ }
+
+ ///
+ /// Multiple predecessors all flowing into the same variant address. Only one of
+ /// them needs to absorb the selector via continuation; the rest land in the boundary path.
+ /// All predecessors and the selector must end up with non-null ContainingBlock.
+ ///
+ [Fact]
+ public void Selector_MultiplePredecessors_RoutesBothPredecessorsThroughSelector() {
+ using LinkerHarness harness = new();
+
+ SegmentedAddress pred1Addr = new(PredecessorSegment, 0x0000);
+ SegmentedAddress variantAddr = new(PredecessorSegment, 0x0001);
+ SegmentedAddress pred2Addr = new(VariantSegment, 0x0000);
+
+ CfgInstruction pred1 = harness.CreateInstruction(pred1Addr, 0x90, 1, InstructionKind.None);
+ CfgInstruction pred2 = harness.CreateInstruction(pred2Addr, 0x91, 1, InstructionKind.None);
+ CfgInstruction variantA = harness.CreateInstruction(variantAddr, 0xA0, 1, InstructionKind.None);
+
+ harness.Linker.Link(InstructionSuccessorType.Normal, pred1, variantA);
+ harness.Linker.Link(InstructionSuccessorType.Normal, pred2, variantA);
+
+ variantA.Predecessors.Should().Contain(new ICfgNode[] { pred1, pred2 });
+
+ CfgInstruction variantB = harness.CreateInstruction(variantAddr, 0xA1, 1, InstructionKind.None);
+
+ SelectorNode selector = harness.Linker.CreateSelectorNodeBetween(variantA, variantB);
+
+ GetContainingBlock(selector).Successors.Should().Contain(new ICfgNode[] { variantA, variantB });
+ pred1.Successors.Should().Contain(selector);
+ pred2.Successors.Should().Contain(selector);
+ }
+
+ ///
+ /// Full selfmodifycall-shape regression at the linker level: the variant's
+ /// predecessor is a CALL, and the selector dispatches between two variants. Asserts the exact
+ /// shape we want for that binary's CFG: call's block is finalised at the call, the selector
+ /// has its own one-node block, both variants live in their own discovery-complete blocks.
+ ///
+ [Fact]
+ public void Selector_BetweenCallTerminatorAndTwoVariants_ProducesValidSelfModifyCallShape() {
+ using LinkerHarness harness = new();
+ SegmentedAddress callAddr = new(PredecessorSegment, 0x0000);
+ SegmentedAddress variantAddr = new(PredecessorSegment, 0x0003);
+
+ CfgInstruction call = harness.CreateInstruction(callAddr, 0xE8, 3, InstructionKind.Call);
+ CfgInstruction nop = harness.CreateInstruction(variantAddr, 0x90, 1, InstructionKind.None); // first iteration variant
+ CfgInstruction hlt = harness.CreateInstruction(variantAddr, 0xF4, 1, InstructionKind.None); // patched second iteration variant
+
+ harness.Linker.Link(InstructionSuccessorType.Normal, call, nop);
+
+ SelectorNode selector = harness.Linker.CreateSelectorNodeBetween(nop, hlt);
+
+ CfgBlock callBlock = GetContainingBlock(call);
+ callBlock.Instructions.Should().ContainSingle().Which.Should().BeSameAs(call);
+ callBlock.IsDiscoveryComplete.Should().BeTrue();
+
+ CfgBlock selectorBlock = GetContainingBlock(selector);
+ selectorBlock.Instructions.Should().ContainSingle().Which.Should().BeSameAs(selector);
+ selectorBlock.IsDiscoveryComplete.Should().BeTrue();
+
+ nop.ContainingBlock.Should().NotBeSameAs(selectorBlock);
+ hlt.ContainingBlock.Should().NotBeSameAs(selectorBlock);
+
+ callBlock.Successors.Should().Contain(selector);
+ selectorBlock.Successors.Should().Contain(new ICfgNode[] { nop, hlt });
+ }
+
+ ///
+ /// Order independence of .
+ /// The caller may pass the two variants in either order; the resulting block layout must
+ /// be the same. This is the contract that frees downstream block-reconciliation code from
+ /// having to special-case "selector with no predecessors yet" via node-type checks.
+ ///
+ [Fact]
+ public void Selector_CreateSelectorNodeBetween_IsOrderIndependent() {
+ // Set up two identical CFGs and inject the selector with the variants in opposite order.
+ SegmentedAddress predAddr = new(PredecessorSegment, 0x0000);
+ SegmentedAddress variantAddr = new(PredecessorSegment, 0x0001);
+
+ CfgBlock RunInjection(bool variantWithPredecessorFirst) {
+ using LinkerHarness harness = new();
+ CfgInstruction pred = harness.CreateInstruction(predAddr, 0x90, 1, InstructionKind.None);
+ CfgInstruction variantInGraph = harness.CreateInstruction(variantAddr, 0xA0, 1, InstructionKind.None);
+ harness.Linker.Link(InstructionSuccessorType.Normal, pred, variantInGraph);
+
+ CfgInstruction freshVariant = harness.CreateInstruction(variantAddr, 0xA1, 1, InstructionKind.None);
+ SelectorNode selector = variantWithPredecessorFirst
+ ? harness.Linker.CreateSelectorNodeBetween(variantInGraph, freshVariant)
+ : harness.Linker.CreateSelectorNodeBetween(freshVariant, variantInGraph);
+
+ return GetContainingBlock(selector);
+ }
+
+ CfgBlock blockOrderA = RunInjection(variantWithPredecessorFirst: true);
+ CfgBlock blockOrderB = RunInjection(variantWithPredecessorFirst: false);
+
+ // The two runs are independent harness instances (so block ids differ), but the
+ // structural shape of the result must be the same: a discovery-complete block
+ // terminating at the selector.
+ blockOrderA.Instructions.Should().HaveCount(blockOrderB.Instructions.Count,
+ "the selector's containing block has the same structure regardless of argument order");
+ blockOrderA.IsDiscoveryComplete.Should().Be(blockOrderB.IsDiscoveryComplete);
+ blockOrderA.Terminator.GetType().Should().Be(blockOrderB.Terminator.GetType());
+ }
+
+}
diff --git a/tests/Spice86.Tests/CfgCpu/CfgNodeExecutionCompilerTest.cs b/tests/Spice86.Tests/CfgCpu/CfgNodeExecutionCompilerTest.cs
index 6d19a4286b..dd75272ca3 100644
--- a/tests/Spice86.Tests/CfgCpu/CfgNodeExecutionCompilerTest.cs
+++ b/tests/Spice86.Tests/CfgCpu/CfgNodeExecutionCompilerTest.cs
@@ -14,6 +14,9 @@ namespace Spice86.Tests.CfgCpu;
using Spice86.Shared.Emulator.Memory;
using Spice86.Shared.Interfaces;
+using System.Linq.Expressions;
+using System.Threading;
+
using Xunit;
///
@@ -32,10 +35,11 @@ private static CfgNodeExecutionCompiler CreateCompiler() {
/// The AST is a simple which compiles to a no-op like expression.
///
private sealed class FixedAstNode : CfgNode {
+ private static readonly CfgNodeIdAllocator _allocator = new();
private IVisitableAstNode _ast;
public FixedAstNode(SegmentedAddress address, IVisitableAstNode ast)
- : base(address, 1) {
+ : base(_allocator.AllocateId(), address, 1) {
_ast = ast;
}
@@ -50,6 +54,21 @@ public override void UpdateSuccessorCache() { }
public override IVisitableAstNode ExecutionAst => _ast;
}
+ private sealed class ExpressionAstNode : IVisitableAstNode {
+ private readonly Expression _expression;
+
+ public ExpressionAstNode(Expression expression) {
+ _expression = expression;
+ }
+
+ public T Accept(IAstVisitor astVisitor) {
+ if (astVisitor is AstExpressionBuilder) {
+ return (T)(object)_expression;
+ }
+ throw new InvalidOperationException($"Unsupported visitor {astVisitor.GetType().Name}");
+ }
+ }
+
[Fact]
public void Compile_AssignsCompiledExecution() {
// A SelectorNode AST produces a simple compiled delegate.
@@ -85,36 +104,16 @@ public void Compile_ConcurrentCallsSameAst_AllComplete() {
}
}
- [Fact]
- public void Compile_WaitsForOptimizedDelegate() {
- // Compile a node and wait briefly for the background compiler to produce the optimized delegate.
- SelectorNode ast = new SelectorNode();
- FixedAstNode node = new FixedAstNode(new SegmentedAddress(0x4000, 0), ast);
- using CfgNodeExecutionCompiler compiler = CreateCompiler();
- compiler.Compile(node);
-
- // Store the initially-assigned (interpreted) delegate reference.
- CfgNodeExecutionAction initial = node.CompiledExecution;
-
- // Wait up to 5 seconds for the optimized delegate to be swapped in.
- int waited = 0;
- while (ReferenceEquals(node.CompiledExecution, initial) && waited < 5000) {
- Thread.Sleep(50);
- waited += 50;
- }
-
- // The delegate should have been swapped (it may still be the interpreted one if
- // compilation is extremely slow, but under normal conditions it should differ).
- node.CompiledExecution.Should().NotBeNull();
- }
-
[Fact]
public void Compile_SecondCallPreventsStaleSwap() {
// Arrange: create two distinct ASTs so we can distinguish which delegate is active.
- SelectorNode ast1 = new SelectorNode();
- SelectorNode ast2 = new SelectorNode();
+ IVisitableAstNode ast1 = CreateAssignmentAst(10_000);
+ IVisitableAstNode ast2 = CreateAssignmentAst(1);
FixedAstNode node = new FixedAstNode(new SegmentedAddress(0x5000, 0), ast1);
- using CfgNodeExecutionCompiler compiler = CreateCompiler();
+ // Inline monitor creation so we can observe TotalSwapped for reliable synchronisation.
+ ILoggerService loggerService = Substitute.For();
+ CfgNodeExecutionCompilerMonitor monitor = new(loggerService);
+ using CfgNodeExecutionCompiler compiler = new(monitor, loggerService, JitMode.InterpretedThenCompiled);
// Act: first Compile() enqueues background task for ast1.
compiler.Compile(node);
@@ -125,22 +124,29 @@ public void Compile_SecondCallPreventsStaleSwap() {
compiler.Compile(node);
CfgNodeExecutionAction afterSecondCompile = node.CompiledExecution;
- // Wait until the background compiler swaps in the optimized delegate for the second AST.
- int waited = 0;
- while (ReferenceEquals(node.CompiledExecution, afterSecondCompile) && waited < 5000) {
- Thread.Sleep(50);
- waited += 50;
- }
+ SpinWait.SpinUntil(() => monitor.TotalSwapped >= 1, TimeSpan.FromSeconds(30)).Should().BeTrue(
+ "the second background compilation should complete and swap the delegate within 30 seconds");
- // Assert: the final delegate must be the optimized one from the second Compile() call,
- // not the stale compiled delegate from the first call.
+ // Assert: the final delegate must be the optimised one from the second Compile() call.
+ // Use ReferenceEquals booleans rather than NotBeSameAs to avoid a FluentAssertions crash
+ // when formatting compiled-expression delegates whose Method.DeclaringType is null.
CfgNodeExecutionAction finalDelegate = node.CompiledExecution;
- finalDelegate.Should().NotBeSameAs(afterFirstCompile,
+ ReferenceEquals(finalDelegate, afterFirstCompile).Should().BeFalse(
"the first background compilation should not overwrite the second Compile()'s delegate");
- finalDelegate.Should().NotBeSameAs(afterSecondCompile,
- "the background compiler should have produced an optimized delegate for the second compile");
+ ReferenceEquals(finalDelegate, afterSecondCompile).Should().BeFalse(
+ "the background compiler should have produced an optimised delegate for the second compile");
// The generation counter should reflect exactly 2 Compile() calls.
node.CompilationGeneration.Should().Be(2);
}
+
+ private static IVisitableAstNode CreateAssignmentAst(int assignmentCount) {
+ ParameterExpression marker = Expression.Variable(typeof(int), "marker");
+ List expressions = new(assignmentCount + 1);
+ for (int i = 0; i < assignmentCount; i++) {
+ expressions.Add(Expression.Assign(marker, Expression.Constant(i)));
+ }
+ expressions.Add(Expression.Empty());
+ return new ExpressionAstNode(Expression.Block([marker], expressions));
+ }
}
diff --git a/tests/Spice86.Tests/CfgCpu/InstructionsFeederTest.cs b/tests/Spice86.Tests/CfgCpu/InstructionsFeederTest.cs
index 1effd92feb..af7ae52f01 100644
--- a/tests/Spice86.Tests/CfgCpu/InstructionsFeederTest.cs
+++ b/tests/Spice86.Tests/CfgCpu/InstructionsFeederTest.cs
@@ -6,6 +6,7 @@ namespace Spice86.Tests.CfgCpu;
using Spice86.Core.Emulator.CPU;
using Spice86.Core.Emulator.CPU.Exceptions;
using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Instruction.ControlFlow;
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
using Spice86.Core.Emulator.CPU.CfgCpu.Feeder;
using Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor.Expressions;
using Spice86.Core.Emulator.CPU.CfgCpu.InstructionRenderer;
@@ -51,7 +52,7 @@ private InstructionsFeeder CreateInstructionsFeeder(IMmu mmu) {
_compiler = new CfgNodeExecutionCompiler(new CfgNodeExecutionCompilerMonitor(loggerService), loggerService, JitMode.InterpretedOnly);
return new InstructionsFeeder(emulatorBreakpointsManager, _memory, state, _instructionReplacer,
- _compiler);
+ _compiler, new CfgNodeIdAllocator());
}
private void WriteJumpNear(SegmentedAddress address) {
diff --git a/tests/Spice86.Tests/CfgCpu/SignatureReducerTest.cs b/tests/Spice86.Tests/CfgCpu/SignatureReducerTest.cs
index ec81a1868d..9af1a8a33f 100644
--- a/tests/Spice86.Tests/CfgCpu/SignatureReducerTest.cs
+++ b/tests/Spice86.Tests/CfgCpu/SignatureReducerTest.cs
@@ -2,8 +2,10 @@ namespace Spice86.Tests.CfgCpu;
using FluentAssertions;
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
using Spice86.Core.Emulator.CPU.CfgCpu.Feeder;
using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.Prefix;
using Spice86.Shared.Emulator.Memory;
using System.Collections.Immutable;
@@ -13,6 +15,7 @@ namespace Spice86.Tests.CfgCpu;
public class SignatureReducerTest {
private static readonly SegmentedAddress TestAddress = new(0x1000, 0);
private const uint DefaultValueAddress = 0x10001;
+ private static readonly CfgNodeIdAllocator _allocator = new();
private static SignatureReducer CreateReducer() {
InstructionReplacerRegistry replacerRegistry = new();
@@ -42,7 +45,7 @@ private static InstructionField CreateValueField(ushort value, uint phys
private static CfgInstruction CreateInstruction(ushort value, uint physicalAddress) {
InstructionField opcodeField = CreateOpcodeField();
InstructionField valueField = CreateValueField(value, physicalAddress);
- CfgInstruction instruction = new CfgInstruction(TestAddress, opcodeField, maxSuccessorsCount: 1);
+ CfgInstruction instruction = new CfgInstruction(_allocator.AllocateId(), TestAddress, opcodeField, new List(), maxSuccessorsCount: 1);
instruction.AddField(valueField);
return instruction;
}
@@ -188,7 +191,7 @@ private static InstructionField CreateOpcodeFieldWithValue(ushort opcode
private static CfgInstruction CreateInstructionWithOpcode(ushort opcode, ushort value, uint physicalAddress) {
InstructionField opcodeField = CreateOpcodeFieldWithValue(opcode);
InstructionField valueField = CreateValueField(value, physicalAddress);
- CfgInstruction instruction = new CfgInstruction(TestAddress, opcodeField, maxSuccessorsCount: 1);
+ CfgInstruction instruction = new CfgInstruction(_allocator.AllocateId(), TestAddress, opcodeField, new List(), maxSuccessorsCount: 1);
instruction.AddField(valueField);
return instruction;
}
diff --git a/tests/Spice86.Tests/CfgCpu/TestInstructionHelper.cs b/tests/Spice86.Tests/CfgCpu/TestInstructionHelper.cs
index 7a24c4cc81..c40165a85b 100644
--- a/tests/Spice86.Tests/CfgCpu/TestInstructionHelper.cs
+++ b/tests/Spice86.Tests/CfgCpu/TestInstructionHelper.cs
@@ -4,6 +4,7 @@ namespace Spice86.Tests.CfgCpu;
using Spice86.Core.Emulator.CPU.CfgCpu.InstructionRenderer;
using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
using Spice86.Core.Emulator.CPU.CfgCpu.Parser;
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
using Spice86.Core.Emulator.InterruptHandlers.Common.MemoryWriter;
using Spice86.Core.Emulator.Memory;
using Spice86.Core.Emulator.Memory.Mmu;
@@ -21,7 +22,7 @@ public class TestInstructionHelper {
public TestInstructionHelper() {
_memory = new Memory(new(), new Ram(0x100000), new A20Gate(), new RealModeMmu386(), false);
_state = new State(CpuModel.INTEL_80286);
- _parser = new InstructionParser(_memory, _state);
+ _parser = new InstructionParser(_memory, _state, new CfgNodeIdAllocator());
_renderer = new AstInstructionRenderer(AsmRenderingConfig.CreateSpice86Style());
}
diff --git a/tests/Spice86.Tests/ColdPathBreakpointTests.cs b/tests/Spice86.Tests/ColdPathBreakpointTests.cs
new file mode 100644
index 0000000000..351052d7a7
--- /dev/null
+++ b/tests/Spice86.Tests/ColdPathBreakpointTests.cs
@@ -0,0 +1,37 @@
+namespace Spice86.Tests;
+
+using Spice86.Core.Emulator;
+using Spice86.Core.Emulator.CPU;
+using Spice86.Core.Emulator.VM;
+using Spice86.Core.Emulator.VM.Breakpoint;
+using Spice86.Shared.Emulator.VM.Breakpoint;
+
+using FluentAssertions;
+using Xunit;
+
+public class ColdPathBreakpointTests {
+ [Fact]
+ public void ColdPath_ExecutionBreakpoint_FiresAtCorrectAddress() {
+ using Spice86DependencyInjection spice86DependencyInjection = new Spice86Creator("add").Create();
+ State state = spice86DependencyInjection.Machine.CpuState;
+ ProgramExecutor programExecutor = spice86DependencyInjection.ProgramExecutor;
+ EmulatorBreakpointsManager emulatorBreakpointsManager =
+ spice86DependencyInjection.Machine.EmulatorBreakpointsManager;
+
+ // Address of "add bx, ax" instruction in add.asm (F000:0009)
+ uint targetAddress = 0xF0009;
+ bool breakpointFired = false;
+ uint addressAtBreakpoint = 0;
+
+ emulatorBreakpointsManager.ToggleBreakPoint(new AddressBreakPoint(
+ BreakPointType.CPU_EXECUTION_ADDRESS, targetAddress, _ => {
+ breakpointFired = true;
+ addressAtBreakpoint = state.IpPhysicalAddress;
+ }, true), true);
+
+ programExecutor.Run();
+
+ breakpointFired.Should().BeTrue();
+ addressAtBreakpoint.Should().Be(targetAddress);
+ }
+}
diff --git a/tests/Spice86.Tests/CpuTests/SingleStepTests/SingleStepTestMinimalMachine.cs b/tests/Spice86.Tests/CpuTests/SingleStepTests/SingleStepTestMinimalMachine.cs
index 2ff9503274..ebcac11f85 100644
--- a/tests/Spice86.Tests/CpuTests/SingleStepTests/SingleStepTestMinimalMachine.cs
+++ b/tests/Spice86.Tests/CpuTests/SingleStepTests/SingleStepTestMinimalMachine.cs
@@ -48,7 +48,7 @@ public SingleStepTestMinimalMachine(CpuModel cpuModel) {
CfgNodeExecutionCompiler executionCompiler = new CfgNodeExecutionCompiler(new CfgNodeExecutionCompilerMonitor(loggerService), loggerService, JitMode.InterpretedOnly);
_cfgNodeExecutionCompiler = executionCompiler;
Cpu = new CfgCpu(memory, state, ioPortDispatcher, callbackHandler, dualPic,
- emulatorBreakpointsManager, functionCatalogue, false, false, true, loggerService,
+ emulatorBreakpointsManager, pauseHandler, functionCatalogue, false, false, true, loggerService,
executionCompiler);
}
diff --git a/tests/Spice86.Tests/HotPathBreakpointTests.cs b/tests/Spice86.Tests/HotPathBreakpointTests.cs
new file mode 100644
index 0000000000..dd80d4dcba
--- /dev/null
+++ b/tests/Spice86.Tests/HotPathBreakpointTests.cs
@@ -0,0 +1,199 @@
+namespace Spice86.Tests;
+
+using FluentAssertions;
+
+using Spice86.Core.Emulator;
+using Spice86.Core.Emulator.CPU;
+using Spice86.Core.Emulator.VM;
+using Spice86.Core.Emulator.VM.Breakpoint;
+using Spice86.Shared.Emulator.VM.Breakpoint;
+using Spice86.Shared.Utils;
+
+using Xunit;
+
+public class HotPathBreakpointTests {
+ [Fact]
+ public void ExecutionBreakpoint_OnInteriorInstruction_TriggersAndPausesAtCorrectAddress() {
+ // "mov BX,AX" at F000:002D is an interior instruction in the externalint loop body.
+ // Loop: inc AX (002C) / mov BX,AX (002D) / shr BX,1 (002F) / loop (0031)
+ const ushort segment = 0xF000;
+ const ushort movBxAxOffset = 0x002D;
+ uint expectedPhysicalAddress = MemoryUtils.ToPhysicalAddress(segment, movBxAxOffset);
+
+ using Spice86DependencyInjection spice86DependencyInjection = new Spice86Creator(
+ "externalint", maxCycles: 0xFFFFFFF, enablePit: true).Create();
+
+ Machine machine = spice86DependencyInjection.Machine;
+ State state = machine.CpuState;
+ EmulatorBreakpointsManager breakpointsManager = machine.EmulatorBreakpointsManager;
+ ProgramExecutor programExecutor = spice86DependencyInjection.ProgramExecutor;
+ IPauseHandler pauseHandler = machine.PauseHandler;
+
+ bool breakpointTriggered = false;
+ uint capturedIpPhysicalAddress = 0;
+ long capturedCycles = -1;
+ ushort capturedAx = 0;
+ ushort capturedBx = 0;
+
+ AddressBreakPoint breakpoint = new(
+ BreakPointType.CPU_EXECUTION_ADDRESS,
+ expectedPhysicalAddress,
+ bp => {
+ breakpointTriggered = true;
+ capturedIpPhysicalAddress = state.IpPhysicalAddress;
+ capturedCycles = state.Cycles;
+ capturedAx = state.AX;
+ capturedBx = state.BX;
+ pauseHandler.RequestPause("Execution breakpoint hit");
+ pauseHandler.Resume();
+ },
+ isRemovedOnTrigger: true);
+
+ breakpointsManager.ToggleBreakPoint(breakpoint, true);
+
+ programExecutor.Run();
+
+ breakpointTriggered.Should().BeTrue(
+ "the execution breakpoint on an interior instruction should fire during hot-path execution");
+ capturedIpPhysicalAddress.Should().Be(expectedPhysicalAddress,
+ "State.IpPhysicalAddress should match the breakpointed instruction address");
+
+ // inc AX has executed (AX incremented from 0 to 1 on first iteration),
+ // but mov BX,AX has NOT yet executed, so BX should still be 0.
+ capturedAx.Should().Be(1,
+ "AX should reflect the inc AX that ran before the breakpointed instruction");
+ capturedBx.Should().Be(0,
+ "BX should not yet reflect mov BX,AX since that instruction has not executed");
+
+ // Cycles should reflect all instructions executed before the breakpointed one.
+ capturedCycles.Should().BeGreaterThan(0,
+ "cycles should reflect execution of instructions prior to the breakpointed one");
+ }
+
+ [Fact]
+ public void ResumeAfterBreakpoint_ContinuesExecutionFromBreakpointedInstruction() {
+ // Breakpoint on "mov bx,ax" at F000:002D
+ const uint movBxAxAddress = 0xF002D;
+ // Next instruction "shr bx,1" at F000:002F
+ const uint shrBx1Address = 0xF002F;
+
+ using Spice86DependencyInjection spice86DependencyInjection = new Spice86Creator(
+ "externalint", maxCycles: 0xFFFFFFF, enablePit: true).Create();
+
+ Machine machine = spice86DependencyInjection.Machine;
+ State state = machine.CpuState;
+ EmulatorBreakpointsManager breakpointsManager = machine.EmulatorBreakpointsManager;
+ ProgramExecutor programExecutor = spice86DependencyInjection.ProgramExecutor;
+ IPauseHandler pauseHandler = machine.PauseHandler;
+
+ ushort capturedAxAtMovBxAx = 0;
+ ushort capturedBxAtShrBx1 = 0;
+ bool movBxAxHit = false;
+ bool shrBx1Hit = false;
+
+ AddressBreakPoint movBxAxBreakpoint = new(
+ BreakPointType.CPU_EXECUTION_ADDRESS,
+ movBxAxAddress,
+ bp => {
+ movBxAxHit = true;
+ capturedAxAtMovBxAx = state.AX;
+
+ // Set a breakpoint on the next instruction to verify mov bx,ax executed
+ AddressBreakPoint shrBreakpoint = new(
+ BreakPointType.CPU_EXECUTION_ADDRESS,
+ shrBx1Address,
+ bp2 => {
+ shrBx1Hit = true;
+ capturedBxAtShrBx1 = state.BX;
+ pauseHandler.RequestPause("shr bx,1 breakpoint hit");
+ pauseHandler.Resume();
+ },
+ isRemovedOnTrigger: true);
+ breakpointsManager.ToggleBreakPoint(shrBreakpoint, true);
+
+ pauseHandler.RequestPause("mov bx,ax breakpoint hit");
+ pauseHandler.Resume();
+ },
+ isRemovedOnTrigger: true);
+
+ breakpointsManager.ToggleBreakPoint(movBxAxBreakpoint, true);
+
+ programExecutor.Run();
+
+ movBxAxHit.Should().BeTrue("breakpoint on mov bx,ax should fire");
+ shrBx1Hit.Should().BeTrue("breakpoint on shr bx,1 should fire after resume");
+ capturedBxAtShrBx1.Should().Be(capturedAxAtMovBxAx,
+ "after resume, mov bx,ax should have executed, making BX equal to AX's value at that point");
+ }
+
+ [Fact]
+ public void BreakpointOnFirstInstructionOfBlock_TriggersCorrectly() {
+ // "inc ax" at F000:002C is the first instruction of the loop block
+ const uint incAxAddress = 0xF002C;
+
+ using Spice86DependencyInjection spice86DependencyInjection = new Spice86Creator(
+ "externalint", maxCycles: 0xFFFFFFF, enablePit: true).Create();
+
+ Machine machine = spice86DependencyInjection.Machine;
+ State state = machine.CpuState;
+ EmulatorBreakpointsManager breakpointsManager = machine.EmulatorBreakpointsManager;
+ ProgramExecutor programExecutor = spice86DependencyInjection.ProgramExecutor;
+ IPauseHandler pauseHandler = machine.PauseHandler;
+
+ bool breakpointFired = false;
+ uint capturedAddress = 0;
+
+ AddressBreakPoint breakpoint = new(
+ BreakPointType.CPU_EXECUTION_ADDRESS,
+ incAxAddress,
+ bp => {
+ breakpointFired = true;
+ capturedAddress = state.IpPhysicalAddress;
+ pauseHandler.RequestPause("inc ax breakpoint hit");
+ pauseHandler.Resume();
+ },
+ isRemovedOnTrigger: true);
+
+ breakpointsManager.ToggleBreakPoint(breakpoint, true);
+
+ programExecutor.Run();
+
+ breakpointFired.Should().BeTrue("breakpoint on the first instruction of the loop block should fire");
+ capturedAddress.Should().Be(incAxAddress,
+ "IpPhysicalAddress should match the first instruction of the loop block");
+ }
+
+ [Fact]
+ public void CycleBreakpoint_DuringHotPathExecution_TriggersAtExpectedCycle() {
+ const long targetCycle = 100;
+ using Spice86DependencyInjection spice86DependencyInjection = new Spice86Creator(
+ "externalint", maxCycles: 0xFFFFFFF, enablePit: true).Create();
+
+ Machine machine = spice86DependencyInjection.Machine;
+ State state = machine.CpuState;
+ EmulatorBreakpointsManager breakpointsManager = machine.EmulatorBreakpointsManager;
+ ProgramExecutor programExecutor = spice86DependencyInjection.ProgramExecutor;
+ IPauseHandler pauseHandler = machine.PauseHandler;
+
+ bool breakpointTriggered = false;
+ long capturedCycles = -1;
+
+ AddressBreakPoint breakpoint = new(
+ BreakPointType.CPU_CYCLES,
+ targetCycle,
+ bp => {
+ breakpointTriggered = true;
+ capturedCycles = state.Cycles;
+ pauseHandler.RequestPause("Cycle breakpoint hit");
+ pauseHandler.Resume();
+ },
+ isRemovedOnTrigger: true);
+
+ breakpointsManager.ToggleBreakPoint(breakpoint, true);
+
+ programExecutor.Run();
+
+ breakpointTriggered.Should().BeTrue("the cycle breakpoint should fire during hot-path execution");
+ capturedCycles.Should().Be(targetCycle, "the cycle count should match the breakpoint target");
+ }
+}
diff --git a/tests/Spice86.Tests/MachineTest.cs b/tests/Spice86.Tests/MachineTest.cs
index 134b4ddd63..a8e61c9fcd 100755
--- a/tests/Spice86.Tests/MachineTest.cs
+++ b/tests/Spice86.Tests/MachineTest.cs
@@ -8,13 +8,17 @@ namespace Spice86.Tests;
using Spice86.Core.CLI;
using Spice86.Core.Emulator.CPU;
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
using Spice86.Core.Emulator.CPU.CfgCpu.Feeder;
using Spice86.Core.Emulator.CPU.CfgCpu.InstructionRenderer;
using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.SelfModifying;
using Spice86.Core.Emulator.Errors;
using Spice86.Core.Emulator.IOPorts;
using Spice86.Core.Emulator.Memory;
+using Spice86.Core.Emulator.Mcp.Response;
using Spice86.Core.Emulator.StateSerialization;
+using Spice86.Core.Emulator.StateSerialization.ControlFlow;
using Spice86.Core.Emulator.VM;
using Spice86.Logging;
using Spice86.Shared.Emulator.Memory;
@@ -22,6 +26,8 @@ namespace Spice86.Tests;
using Spice86.Shared.Utils;
using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
using Xunit;
@@ -31,6 +37,12 @@ public class MachineTest
private readonly ListingExtractor _dumper = new(new(AsmRenderingConfig.CreateSpice86Style()));
+ private static readonly JsonSerializerOptions CfgBlocksJsonOptions = new() {
+ WriteIndented = true,
+ Converters = { new JsonStringEnumConverter() },
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ };
+
static MachineTest()
{
Log.Logger = new LoggerConfiguration()
@@ -176,6 +188,17 @@ public void TestRep(JitMode jitMode)
TestOneBin("rep", jitMode);
}
+ [Theory]
+ [MemberData(nameof(JitModes))]
+ public void TestReturnedTerminator(JitMode jitMode) {
+ byte[] expected = new byte[8];
+ expected[0x04] = 0x22;
+ expected[0x05] = 0x22;
+ expected[0x06] = 0x11;
+ expected[0x07] = 0x11;
+ TestOneBin("returnedterminator", expected, jitMode, maxCycles: 1000);
+ }
+
[Theory]
[MemberData(nameof(JitModes))]
public void TestRotate(JitMode jitMode)
@@ -209,6 +232,52 @@ public void TestSegpr(JitMode jitMode)
// Check that IRET is normally connected to the return target
Assert.Contains(divBy0NextInstruction, divBy0HandlerIret.Successors);
Assert.Contains(divBy0NextInstruction, divBy0HandlerIret.SuccessorsPerType[InstructionSuccessorType.Normal]);
+
+ // Block-level assertions: same scenario verified at the CfgBlock layer.
+
+ // divBy0 carries an extra CpuFault successor, so it is the Terminator of its block.
+ CfgBlock? divBy0Block = divBy0.ContainingBlock;
+ Assert.NotNull(divBy0Block);
+ divBy0Block.IsDiscoveryComplete.Should().BeTrue();
+ divBy0Block.Terminator.Should().BeSameAs(divBy0);
+
+ // The handler entry at F000:1100 is the entry of the handler block.
+ CfgBlock? handlerBlock = divBy0HandlerEntry.ContainingBlock;
+ Assert.NotNull(handlerBlock);
+ handlerBlock.IsDiscoveryComplete.Should().BeTrue();
+ handlerBlock.Entry.Should().BeSameAs(divBy0HandlerEntry);
+
+ // The IRET remains in the handler block: returned terminators are valid interior
+ // nodes, and the executor cold-steps them when entered directly.
+ CfgBlock? iretBlock = divBy0HandlerIret.ContainingBlock;
+ Assert.NotNull(iretBlock);
+ iretBlock.Should().BeSameAs(handlerBlock);
+ iretBlock.Entry.Should().BeSameAs(divBy0HandlerEntry);
+ iretBlock.Terminator.Should().BeSameAs(divBy0HandlerIret);
+ iretBlock.IsDiscoveryComplete.Should().BeTrue();
+
+ // The instruction following divBy0 in memory is the entry of the post-fault block.
+ CfgBlock? nextBlock = divBy0NextInstruction.ContainingBlock;
+ Assert.NotNull(nextBlock);
+ nextBlock.IsDiscoveryComplete.Should().BeTrue();
+ nextBlock.Entry.Should().BeSameAs(divBy0NextInstruction);
+
+ // The blocks are distinct.
+ divBy0Block.Should().NotBeSameAs(handlerBlock);
+ divBy0Block.Should().NotBeSameAs(nextBlock);
+ iretBlock.Should().NotBeSameAs(nextBlock);
+ handlerBlock.Should().NotBeSameAs(nextBlock);
+
+ // Block-level edges derived from the underlying instruction-level edges.
+ IEnumerable divBlockSuccessors = divBy0Block.Successors.Select(s => s.ContainingBlock);
+ // CpuFault edge: div block → handler block.
+ divBlockSuccessors.Should().Contain(handlerBlock);
+ // CallToReturn edge: div block → post-fault block (handler rewrote the stack
+ // to return past the div instead of retrying it).
+ divBlockSuccessors.Should().Contain(nextBlock);
+
+ // Normal edge: IRET block -> post-fault block.
+ iretBlock.Successors.Select(s => s.ContainingBlock).Should().Contain(nextBlock);
});
}
@@ -288,6 +357,28 @@ public void TestSelfModifyValue(JitMode jitMode)
// Code should have been modified so instruction should use memory and not stored value
Assert.False(immField.UseValue);
Assert.Equal(instruction.Address.Linear + 1, immField.PhysicalAddress);
+
+ // Block-level assertions: variant merging must keep the post-replacement
+ // instruction inside its CfgBlock.
+ CfgBlock? movBlock = instruction.ContainingBlock;
+ Assert.NotNull(movBlock);
+ Assert.Contains(instruction, movBlock.Instructions);
+ // The mov sits right after the jmp short 0x000D (a terminator), so it is the entry
+ // of its own block.
+ Assert.Same(instruction, movBlock.Entry);
+ // The block's discovery is complete and keeps both graph successors: the loop-back
+ // jump and the terminal HLT path.
+ Assert.True(movBlock.IsDiscoveryComplete);
+ CfgInstruction? loopJump = currentInstructions.GetAtAddress(new SegmentedAddress(0xF000, 0x000B));
+ CfgInstruction? hlt = currentInstructions.GetAtAddress(new SegmentedAddress(0xF000, 0x001C));
+ Assert.NotNull(loopJump);
+ Assert.NotNull(hlt);
+ movBlock.Successors.Should().BeEquivalentTo([loopJump, hlt]);
+ foreach (ICfgNode successor in movBlock.Successors) {
+ Assert.NotNull(successor.ContainingBlock);
+ Assert.Same(successor, successor.ContainingBlock.Entry);
+ Assert.True(successor.IsBlockTerminator);
+ }
});
}
@@ -309,7 +400,90 @@ public void TestSelfModifyInstructions(JitMode jitMode)
[MemberData(nameof(JitModes))]
public void TestSelfModifyCall(JitMode jitMode)
{
- TestOneBin("selfmodifycall", [], jitMode);
+ TestOneBin("selfmodifycall", [], jitMode, machine => {
+ // Block-level assertions: SelectorNode insertion via CreateSelectorNodeBetween
+ // must finalise the predecessor's CfgBlock and must not disturb the variant
+ // CfgInstructions' containing-block back-pointers.
+ CurrentInstructions currentInstructions = machine.CfgCpu.CfgNodeFeeder.InstructionsFeeder.CurrentInstructions;
+ // The call near 0x0017 sits at F000:000D and is the predecessor of the SelectorNode
+ // injected at F000:0010 once the second-iteration return point switches from `nop`
+ // to `hlt` (the byte at F000:0010 is patched mid-execution).
+ CfgInstruction? call = currentInstructions.GetAtAddress(new SegmentedAddress(0xF000, 0x000D));
+ Assert.NotNull(call);
+ SelectorNode selector = call.Successors.OfType().Single();
+
+ // The SelectorNode dispatches between the two variants (nop and hlt) at F000:0010.
+ Assert.Equal(new SegmentedAddress(0xF000, 0x0010), selector.Address);
+ ICfgNode[] variants = selector.Successors.ToArray();
+ Assert.Equal(2, variants.Length);
+ // Each variant is itself a CfgInstruction with its own non-null ContainingBlock.
+ foreach (ICfgNode variant in variants) {
+ CfgInstruction variantInstruction = Assert.IsAssignableFrom(variant);
+ Assert.NotNull(variantInstruction.ContainingBlock);
+ Assert.Contains(variantInstruction, variantInstruction.ContainingBlock.Instructions);
+ }
+
+ // The predecessor's (call's) CfgBlock is finalised: the call is itself a block
+ // terminator, so its block was discovery-complete from the moment the call landed
+ // in it, and the subsequent SelectorNode insertion did not regress that state.
+ CfgBlock? callBlock = call.ContainingBlock;
+ Assert.NotNull(callBlock);
+ Assert.True(callBlock.IsDiscoveryComplete);
+ Assert.True(callBlock.Terminator.IsBlockTerminator);
+ Assert.Same(call, callBlock.Terminator);
+ // The SelectorNode is reachable as a block-level successor (the underlying
+ // instruction-level edge call → selector aliases through CfgBlock.Successors).
+ Assert.Contains(selector, callBlock.Successors);
+ });
+ }
+
+ [Theory]
+ [MemberData(nameof(JitModes))]
+ public void TestSelfModifyTerminator(JitMode jitMode)
+ {
+ // Expected stack memory: 42 00 FF FF
+ // First push: AX=0xFFFF (first pass marker)
+ // Second push: AX=0x0042 (after patch, at 'done' label)
+ byte[] expected = new byte[4];
+ expected[0x00] = 0x42;
+ expected[0x01] = 0x00;
+ expected[0x02] = 0xFF;
+ expected[0x03] = 0xFF;
+ TestOneBin("selfmodifyterminator", expected, jitMode, machine => {
+ // Block-level assertions: Case T continuation — the SelectorNode injected at the
+ // terminator's address (F000:0019) is absorbed into the predecessor's block because
+ // the predecessor (or ax, ax at F000:0017) is a non-terminator whose
+ // NextInMemoryAddress matches the selector's address.
+ CurrentInstructions currentInstructions = machine.CfgCpu.CfgNodeFeeder.InstructionsFeeder.CurrentInstructions;
+
+ // The 'or ax, ax' at F000:0017 is the predecessor of the SelectorNode.
+ CfgInstruction? orAxAx = currentInstructions.GetAtAddress(new SegmentedAddress(0xF000, 0x0017));
+ Assert.NotNull(orAxAx);
+
+ // The SelectorNode at F000:0019 (where the jne was patched to jmp).
+ SelectorNode selector = orAxAx.Successors.OfType().Single();
+ Assert.Equal(new SegmentedAddress(0xF000, 0x0019), selector.Address);
+
+ // The selector is appended to the predecessor block by normal continuation.
+ CfgBlock? predecessorBlock = orAxAx.ContainingBlock;
+ Assert.NotNull(predecessorBlock);
+ CfgBlock? selectorBlock = selector.ContainingBlock;
+ Assert.NotNull(selectorBlock);
+ Assert.Same(predecessorBlock, selectorBlock);
+ Assert.Contains(orAxAx, predecessorBlock.Instructions);
+ Assert.Same(selector, selectorBlock.Terminator);
+ Assert.True(predecessorBlock.IsDiscoveryComplete);
+ Assert.True(selectorBlock.IsDiscoveryComplete);
+
+ // The selector dispatches between two variants (original jne and patched jmp).
+ ICfgNode[] variants = selector.Successors.ToArray();
+ Assert.Equal(2, variants.Length);
+ foreach (ICfgNode variant in variants) {
+ CfgInstruction variantInstruction = Assert.IsAssignableFrom(variant);
+ Assert.NotNull(variantInstruction.ContainingBlock);
+ Assert.Contains(variantInstruction, variantInstruction.ContainingBlock.Instructions);
+ }
+ });
}
[Theory]
@@ -321,6 +495,76 @@ public void TestExternalInt(JitMode jitMode)
TestOneBin("externalint", expected, jitMode, 0xFFFFFFF, true);
}
+ [Theory]
+ [MemberData(nameof(JitModes))]
+ public void TestDivFaultLoop(JitMode jitMode)
+ {
+ byte[] expected = new byte[4];
+ expected[0x00] = 0x03; // retrycount low
+ expected[0x01] = 0x00; // retrycount high
+ expected[0x02] = 0x02; // quotient low (10 / 5 = 2)
+ expected[0x03] = 0x00; // quotient high
+ TestOneBin("divfaultloop", expected, jitMode, machine => {
+ CurrentInstructions currentInstructions =
+ machine.CfgCpu.CfgNodeFeeder.InstructionsFeeder.CurrentInstructions;
+
+ // div bx at F000:0028
+ CfgInstruction? div = currentInstructions.GetAtAddress(new SegmentedAddress(0xF000, 0x0028));
+ // handler entry (push bp) at F000:0037
+ CfgInstruction? handlerEntry = currentInstructions.GetAtAddress(new SegmentedAddress(0xF000, 0x0037));
+ // handler iret at F000:0059
+ CfgInstruction? handlerIret = currentInstructions.GetAtAddress(new SegmentedAddress(0xF000, 0x0059));
+ // divblock entry (mov ax,10) at F000:0020 — the handler rewrites the return
+ // address so iret returns here instead of to the faulting div.
+ CfgInstruction? divblockEntry = currentInstructions.GetAtAddress(new SegmentedAddress(0xF000, 0x0020));
+ // post-fault instruction: mov bx,[cs:0x100] at F000:002A
+ CfgInstruction? postDiv = currentInstructions.GetAtAddress(new SegmentedAddress(0xF000, 0x002A));
+
+ Assert.NotNull(div);
+ Assert.NotNull(handlerEntry);
+ Assert.NotNull(handlerIret);
+ Assert.NotNull(divblockEntry);
+ Assert.NotNull(postDiv);
+
+ // Instruction-level: div has CpuFault edge to handler entry
+ Assert.Contains(handlerEntry, div.Successors);
+ Assert.Contains(handlerEntry, div.SuccessorsPerType[InstructionSuccessorType.CpuFault]);
+
+ // Instruction-level: div has CallToReturn edge to divblockEntry
+ // (handler rewrites return IP to divblock start)
+ Assert.Contains(divblockEntry, div.Successors);
+
+ // Block-level: div is a block terminator (CpuFault gives it multiple successor types)
+ CfgBlock? divBlock = div.ContainingBlock;
+ Assert.NotNull(divBlock);
+ divBlock.Terminator.Should().BeSameAs(div);
+ divBlock.IsDiscoveryComplete.Should().BeTrue();
+
+ // Block-level: divblockEntry is in the same block as div (the block entry is divblockEntry)
+ divBlock.Entry.Should().BeSameAs(divblockEntry);
+
+ // Block-level: handler entry starts its own block
+ CfgBlock? handlerBlock = handlerEntry.ContainingBlock;
+ Assert.NotNull(handlerBlock);
+ handlerBlock.Entry.Should().BeSameAs(handlerEntry);
+ handlerBlock.Should().NotBeSameAs(divBlock);
+
+ // Block-level: post-div instruction starts its own block
+ CfgBlock? postDivBlock = postDiv.ContainingBlock;
+ Assert.NotNull(postDivBlock);
+ postDivBlock.Entry.Should().BeSameAs(postDiv);
+ postDivBlock.Should().NotBeSameAs(divBlock);
+
+ // Block-level edges
+ IEnumerable divBlockSuccessors = divBlock.Successors.Select(s => s.ContainingBlock);
+ divBlockSuccessors.Should().Contain(handlerBlock, "CpuFault edge to handler");
+ // The CallToReturn edge from div goes to divblockEntry which is the entry of
+ // divBlock itself — so the block-level successor list includes divBlock (self-loop).
+ divBlockSuccessors.Should().Contain(divBlock,
+ "handler rewrites return to divblock entry, creating a self-loop at block level");
+ });
+ }
+
[Theory]
[MemberData(nameof(JitModes))]
public void TestLinearAddressSameButSegmentedDifferent(JitMode jitMode)
@@ -403,6 +647,144 @@ public void TestCallbacks(JitMode jitMode) {
// G) No direct edge from entry INT8 to INT1C handler
int8Entry.Successors.Should().NotContain(int1CHandlerEntry);
+
+ // ---------------------------------------------------------------
+ // Block-level assertions
+ // ---------------------------------------------------------------
+ CfgBlock? int8EntryBlock = int8Entry.ContainingBlock;
+ CfgBlock? int8HandlerEntryBlock = int8HandlerEntry.ContainingBlock;
+ CfgBlock? postInt8Block = postInt8.ContainingBlock;
+ CfgBlock? iret8Block = iret8.ContainingBlock;
+ CfgBlock? eoiCallbackBlock = eoiCallback.ContainingBlock;
+
+ Assert.NotNull(int8EntryBlock);
+ Assert.NotNull(int8HandlerEntryBlock);
+ Assert.NotNull(postInt8Block);
+ Assert.NotNull(iret8Block);
+ Assert.NotNull(eoiCallbackBlock);
+
+ int8EntryBlock.IsDiscoveryComplete.Should().BeTrue();
+ int8HandlerEntryBlock.IsDiscoveryComplete.Should().BeTrue();
+ postInt8Block.IsDiscoveryComplete.Should().BeTrue();
+ iret8Block.IsDiscoveryComplete.Should().BeTrue();
+ eoiCallbackBlock.IsDiscoveryComplete.Should().BeTrue();
+
+ // INT 8 (call) is a block terminator, so it ends its block.
+ int8EntryBlock.Terminator.Should().BeSameAs(int8Entry);
+
+ // The INT 8 handler entry callback has unbounded MaxSuccessorsCount,
+ // making it a block terminator on its own (single-instruction block).
+ int8HandlerEntryBlock.Entry.Should().BeSameAs(int8HandlerEntry);
+ int8HandlerEntryBlock.Terminator.Should().BeSameAs(int8HandlerEntry);
+
+ // The post-INT8 fall-through is the entry of its block.
+ postInt8Block.Entry.Should().BeSameAs(postInt8);
+
+ // IRET is a Return, hence a block terminator.
+ iret8Block.Terminator.Should().BeSameAs(iret8);
+
+ // The EOI callback is a callback (unbounded successors), so it is the
+ // terminator of its containing block.
+ eoiCallbackBlock.Terminator.Should().BeSameAs(eoiCallback);
+
+ // CfgBlock-to-block edges: block.Successors entries are CfgInstructions
+ // (the underlying terminator's successors). Follow ContainingBlock to
+ // get the corresponding blocks.
+ int8EntryBlock.Successors.Select(s => s.ContainingBlock)
+ .Should().Contain(int8HandlerEntryBlock); // Call edge
+ int8EntryBlock.Successors.Select(s => s.ContainingBlock)
+ .Should().Contain(postInt8Block); // CallToReturn edge
+
+ eoiCallbackBlock.Successors.Select(s => s.ContainingBlock)
+ .Should().Contain(iret8Block); // Normal fall-through to IRET
+
+ iret8Block.Successors.Select(s => s.ContainingBlock)
+ .Should().Contain(postInt8Block); // Normal IRET return edge
+ }
+
+ ///
+ /// Verifies the CfgBlock-level structure around STI / CLI. The fixture
+ /// sticli.bin (BIOS-style, entry at F000:FFF0) contains:
+ ///
+ /// mov ax, 0x1000
+ /// mov ds, ax
+ /// sti
+ /// mov ax, 0x1234
+ /// cli
+ /// mov [si], ax
+ /// hlt
+ ///
+ /// STI is a Block_Terminator and CLI is a Block_Starter, so the linker must
+ /// split the instruction stream into three distinct blocks chained via successors.
+ ///
+ [Theory]
+ [MemberData(nameof(JitModes))]
+ public void TestStiCli(JitMode jitMode) {
+ TestOneBin("sticli", [], jitMode, machine => {
+ machine.CpuState.IsRunning.Should().BeFalse(
+ "the program is expected to reach HLT");
+ machine.CpuState.InterruptShadowing.Should().BeFalse(
+ "InterruptShadowing arms for one instruction after STI and is consumed at the next boundary");
+
+ CurrentInstructions ci =
+ machine.CfgCpu.CfgNodeFeeder.InstructionsFeeder.CurrentInstructions;
+
+ // Code layout at F000:0000 (jumped to from the BIOS entry point at F000:FFF0):
+ // +0: B8 00 10 mov ax, 0x1000 (3 bytes)
+ // +3: 8E D8 mov ds, ax (2 bytes)
+ // +5: FB sti (1 byte) Block_Terminator
+ // +6: B8 34 12 mov ax, 0x1234 (3 bytes) middle block
+ // +9: FA cli (1 byte) Block_Starter
+ // +A: 89 04 mov [si], ax (2 bytes)
+ // +C: F4 hlt (1 byte)
+ SegmentedAddress stiAddr = new(0xF000, 0x0005);
+ SegmentedAddress movAxImmAddr = new(0xF000, 0x0006);
+ SegmentedAddress cliAddr = new(0xF000, 0x0009);
+
+ CfgInstruction? sti = ci.GetAtAddress(stiAddr);
+ CfgInstruction? movAxImm = ci.GetAtAddress(movAxImmAddr);
+ CfgInstruction? cli = ci.GetAtAddress(cliAddr);
+
+ Assert.NotNull(sti);
+ Assert.NotNull(movAxImm);
+ Assert.NotNull(cli);
+
+ sti.IsBlockTerminator.Should().BeTrue("STI is an explicit Block_Terminator");
+ cli.IsBlockStarter.Should().BeTrue("CLI is an explicit Block_Starter");
+ movAxImm.IsBlockTerminator.Should().BeFalse("the middle MOV is not itself a terminator");
+ movAxImm.IsBlockStarter.Should().BeFalse("the middle MOV is not itself a starter");
+
+ CfgBlock? stiBlock = sti.ContainingBlock;
+ CfgBlock? midBlock = movAxImm.ContainingBlock;
+ CfgBlock? cliBlock = cli.ContainingBlock;
+
+ Assert.NotNull(stiBlock);
+ Assert.NotNull(midBlock);
+ Assert.NotNull(cliBlock);
+
+ stiBlock.IsDiscoveryComplete.Should().BeTrue();
+ midBlock.IsDiscoveryComplete.Should().BeTrue();
+ cliBlock.IsDiscoveryComplete.Should().BeTrue();
+
+ // STI is the terminator of its block.
+ stiBlock.Terminator.Should().BeSameAs(sti);
+
+ // The middle block contains exactly one instruction (the MOV between STI and CLI).
+ midBlock.Entry.Should().BeSameAs(movAxImm);
+ midBlock.Terminator.Should().BeSameAs(movAxImm);
+
+ // CLI is the entry of its block.
+ cliBlock.Entry.Should().BeSameAs(cli);
+
+ // The three blocks must be distinct.
+ stiBlock.Should().NotBeSameAs(midBlock);
+ midBlock.Should().NotBeSameAs(cliBlock);
+ stiBlock.Should().NotBeSameAs(cliBlock);
+
+ // Block-level successors: STI -> MID -> CLI.
+ stiBlock.Successors.Select(s => s.ContainingBlock).Should().Contain(midBlock);
+ midBlock.Successors.Select(s => s.ContainingBlock).Should().Contain(cliBlock);
+ }, maxCycles: 1000);
}
[AssertionMethod]
@@ -421,17 +803,19 @@ private void TestOneBin(string binName, byte[] expected, JitMode jitMode, long m
Machine machine = spice86DependencyInjection.Machine;
CompareMemoryWithExpected(machine.Memory, expected);
CompareListingWithExpected(binName, machine);
+ CompareCfgBlocksJsonWithExpected(binName, machine);
}
[AssertionMethod]
- private void TestOneBin(string binName, byte[] expected, JitMode jitMode, Action assertions)
+ private void TestOneBin(string binName, byte[] expected, JitMode jitMode, Action assertions, long maxCycles = 100000L, bool enablePit = false, bool enableA20Gate = false)
{
- using Spice86Creator creator = new Spice86Creator(binName: binName, maxCycles: 100000L, enablePit: false, enableA20Gate: false, jitMode: jitMode);
+ using Spice86Creator creator = new Spice86Creator(binName: binName, maxCycles: maxCycles, enablePit: enablePit, enableA20Gate: enableA20Gate, jitMode: jitMode);
using Spice86DependencyInjection spice86DependencyInjection = creator.Create();
spice86DependencyInjection.ProgramExecutor.Run();
Machine machine = spice86DependencyInjection.Machine;
CompareMemoryWithExpected(machine.Memory, expected);
CompareListingWithExpected(binName, machine);
+ CompareCfgBlocksJsonWithExpected(binName, machine);
assertions(machine);
}
@@ -480,6 +864,7 @@ public void Test386ButNotProtectedMode(JitMode jitMode) {
// FF means test finished normally
Assert.Equal(0xFF, debugPortsHandler.PostValues.Last());
CompareListingWithExpected(binName, machine);
+ CompareCfgBlocksJsonWithExpected(binName, machine);
}
private class Test386ButNotProtectedModeHandler : DefaultIOPortHandler {
@@ -521,10 +906,21 @@ private static List GetExpectedListing(string binName)
}
private static void WriteExpectedListing(string binName, List expected) {
- string resPath = $"Resources/cpuTests/res/DumpedListing/{binName}.txt";
+ // Write directly to the source tree so the golden file is committed alongside the code.
+ // CallerFilePath gives us the location of MachineTest.cs in the source tree.
+ string sourceDir = GetDirectoryName(GetSourceFilePath());
+ string fileName = Path.GetFileName(binName) + ".txt";
+ string resPath = Path.Join(sourceDir, "Resources", "cpuTests", "res", "DumpedListing", fileName);
File.WriteAllLines(resPath, expected);
}
+ private static string GetSourceFilePath([System.Runtime.CompilerServices.CallerFilePath] string path = "") => path;
+
+ private static string GetDirectoryName(string path) {
+ return Path.GetDirectoryName(path)
+ ?? throw new InvalidOperationException($"No directory for path: {path}");
+ }
+
[AssertionMethod]
private static void CompareMemoryWithExpected(IMemory memory, byte[] expected)
{
@@ -542,4 +938,27 @@ private static void CompareMemoryWithExpected(IMemory memory, byte[] expected)
throw new Xunit.Sdk.XunitException("Memory diff:\n" + sb);
}
}
+
+ private void CompareCfgBlocksJsonWithExpected(string binName, Machine machine) {
+ CfgBlocksJsonExporter exporter = new(new CfgBlockGraphExporter());
+ CfgCpuGraph actualGraph = exporter.BuildGraph(machine.CfgCpu.ExecutionContextManager, null);
+
+ string actualJson = JsonSerializer.Serialize(actualGraph, CfgBlocksJsonOptions);
+ //WriteExpectedCfgBlocksJson(binName, actualJson);
+ string expectedJson = GetExpectedCfgBlocksJson(binName);
+ Assert.Equal(expectedJson, actualJson);
+ }
+
+ private static string GetExpectedCfgBlocksJson(string binName) {
+ string resPath = $"Resources/cpuTests/res/DumpedCfgBlocks/{binName}.json";
+ return File.ReadAllText(resPath);
+ }
+
+ private static void WriteExpectedCfgBlocksJson(string binName, string json) {
+ string sourceDir = GetDirectoryName(GetSourceFilePath());
+ string fileName = Path.GetFileName(binName) + ".json";
+ string resPath = Path.Join(sourceDir, "Resources", "cpuTests", "res", "DumpedCfgBlocks", fileName);
+ Directory.CreateDirectory(GetDirectoryName(resPath));
+ File.WriteAllText(resPath, json);
+ }
}
diff --git a/tests/Spice86.Tests/McpIntegrationContext.cs b/tests/Spice86.Tests/McpIntegrationContext.cs
index d0594bb850..fd6f06c8e3 100644
--- a/tests/Spice86.Tests/McpIntegrationContext.cs
+++ b/tests/Spice86.Tests/McpIntegrationContext.cs
@@ -14,15 +14,19 @@ namespace Spice86.Tests;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
+using System.Threading;
using System.Threading.Tasks;
internal sealed class McpIntegrationContext : IAsyncDisposable {
+ private static readonly TimeSpan DefaultPauseTimeout = TimeSpan.FromSeconds(30);
+
private readonly HttpClient _httpClient;
private readonly Uri _baseEndpoint;
private readonly Uri _endpoint;
private readonly Spice86Creator _creator;
private long _requestId = 1;
private string? _sessionId;
+ private Thread? _emulationThread;
private McpIntegrationContext(
Spice86Creator creator,
@@ -46,6 +50,35 @@ private McpIntegrationContext(
public McpHttpHost Host { get; }
public string? SessionId => _sessionId;
+ public void StartEmulationLoop() {
+ Services.PauseHandler.RequestPause("Starting paused for test");
+ _emulationThread = new Thread(() => {
+ Spice86.ProgramExecutor.Run();
+ }) { IsBackground = true };
+ _emulationThread.Start();
+ }
+
+ public bool WaitUntilPaused(TimeSpan timeout) {
+ if (Services.PauseHandler.IsPaused) {
+ return true;
+ }
+ using ManualResetEventSlim evt = new(false);
+ void OnPaused() => evt.Set();
+ Services.PauseHandler.Paused += OnPaused;
+ try {
+ if (Services.PauseHandler.IsPaused) {
+ return true;
+ }
+ return evt.Wait(timeout);
+ } finally {
+ Services.PauseHandler.Paused -= OnPaused;
+ }
+ }
+
+ public bool WaitUntilPaused() {
+ return WaitUntilPaused(DefaultPauseTimeout);
+ }
+
public static async Task CreateAsync(
string testProgramName,
bool enableXms = false,
@@ -95,6 +128,7 @@ public static async Task CreateAsync(
emsManager,
services.XmsManager,
services.BreakpointsManager,
+ services.CfgBlocksExporter,
services.LoggerService);
// Always assign optional devices, regardless of EMS/XMS enablement
@@ -203,6 +237,11 @@ public async Task GetHealthAsync() {
public async ValueTask DisposeAsync() {
_httpClient.Dispose();
Host.Dispose();
+ if (_emulationThread is { IsAlive: true }) {
+ Services.State.IsRunning = false;
+ Services.PauseHandler.Resume();
+ _emulationThread.Join(TimeSpan.FromSeconds(5));
+ }
Spice86.Dispose();
_creator.Dispose();
await Task.CompletedTask;
diff --git a/tests/Spice86.Tests/McpServerToolStateTests.cs b/tests/Spice86.Tests/McpServerToolStateTests.cs
index 054a164c24..5346dd2039 100644
--- a/tests/Spice86.Tests/McpServerToolStateTests.cs
+++ b/tests/Spice86.Tests/McpServerToolStateTests.cs
@@ -246,21 +246,22 @@ public async Task UserPausedEmulator_WhenAiSteps_ShouldAdvanceInstructionPointer
// Arrange
await using McpIntegrationContext context = await McpIntegrationContext.CreateAsync(TestProgramName);
await context.InitializeAsync();
- int initialIp = context.Services.State.IP;
- context.Services.PauseHandler.RequestPause("User requested pause before AI stepping");
- context.Services.PauseHandler.IsPaused.Should().BeTrue();
+ context.StartEmulationLoop();
+ context.WaitUntilPaused().Should().BeTrue();
+ ushort initialIp = context.Services.State.IP;
// Act
JsonDocument stepResponse = await context.CallToolAsync("step", new Dictionary());
- // Assert
+ // Assert — step is now fire-and-forget
JsonElement stepResult = McpJsonRpcAssertions.GetJsonRpcResult(stepResponse);
JsonElement structuredContent = McpJsonRpcAssertions.GetStructuredContent(stepResult);
McpJsonRpcAssertions.TryGetPropertyIgnoreCase(structuredContent, "success", out JsonElement success).Should().BeTrue();
success.GetBoolean().Should().BeTrue();
- McpJsonRpcAssertions.TryGetPropertyIgnoreCase(structuredContent, "cpuState", out JsonElement cpuState).Should().BeTrue();
- McpJsonRpcAssertions.TryGetPropertyIgnoreCase(cpuState, "ip", out JsonElement steppedIp).Should().BeTrue();
- steppedIp.GetInt32().Should().NotBe(initialIp);
+
+ // Wait for the emulation loop to execute and re-pause
+ context.WaitUntilPaused().Should().BeTrue();
+ context.Services.State.IP.Should().NotBe(initialIp);
context.Services.PauseHandler.IsPaused.Should().BeTrue();
}
@@ -1106,13 +1107,12 @@ public async Task Step_ShouldInitiateStepAsync() {
// Act
JsonDocument result = await context.CallToolAsync("step", new Dictionary());
- // Assert
+ // Assert — step is now fire-and-forget, returns success + message only
JsonElement structuredContent = McpJsonRpcAssertions.GetStructuredContent(McpJsonRpcAssertions.GetJsonRpcResult(result));
McpJsonRpcAssertions.TryGetPropertyIgnoreCase(structuredContent, "success", out JsonElement success).Should().BeTrue();
success.GetBoolean().Should().BeTrue();
- McpJsonRpcAssertions.TryGetPropertyIgnoreCase(structuredContent, "cpuState", out JsonElement cpuState).Should().BeTrue();
- McpJsonRpcAssertions.TryGetPropertyIgnoreCase(cpuState, "ip", out JsonElement instructionIp).Should().BeTrue();
- instructionIp.GetInt32().Should().BeGreaterThanOrEqualTo(0);
+ McpJsonRpcAssertions.TryGetPropertyIgnoreCase(structuredContent, "message", out JsonElement message).Should().BeTrue();
+ message.GetString().Should().NotBeNullOrWhiteSpace();
}
[Fact]
@@ -1202,10 +1202,13 @@ public async Task ReadCfgCpuGraph_ShouldExposeSegmentedAddressesAndCorrectEntryP
// Arrange
await using McpIntegrationContext context = await McpIntegrationContext.CreateAsync(TestProgramName);
await context.InitializeAsync();
+ context.StartEmulationLoop();
+ context.WaitUntilPaused().Should().BeTrue();
// Step to build up graph nodes
for (int i = 0; i < 5; i++) {
await context.CallToolAsync("step", new Dictionary());
+ context.WaitUntilPaused().Should().BeTrue();
}
// Act
@@ -1216,34 +1219,39 @@ public async Task ReadCfgCpuGraph_ShouldExposeSegmentedAddressesAndCorrectEntryP
// Assert
JsonElement structured = GetSuccessfulStructuredContent(response);
- // CurrentContextEntryPoint should be a SegmentedAddress object, not a string
+ // CurrentContextEntryPoint should be a string in "XXXX:XXXX" format
McpJsonRpcAssertions.TryGetPropertyIgnoreCase(structured, "currentContextEntryPoint", out JsonElement entryPoint).Should().BeTrue();
- entryPoint.ValueKind.Should().Be(JsonValueKind.Object);
- McpJsonRpcAssertions.TryGetPropertyIgnoreCase(entryPoint, "segment", out JsonElement _).Should().BeTrue();
- McpJsonRpcAssertions.TryGetPropertyIgnoreCase(entryPoint, "offset", out JsonElement _).Should().BeTrue();
+ entryPoint.ValueKind.Should().Be(JsonValueKind.String);
- // EntryPointAddresses should be an array of SegmentedAddress objects
+ // EntryPointAddresses should be an array of strings
McpJsonRpcAssertions.TryGetPropertyIgnoreCase(structured, "entryPointAddresses", out JsonElement entryPoints).Should().BeTrue();
entryPoints.ValueKind.Should().Be(JsonValueKind.Array);
if (entryPoints.GetArrayLength() > 0) {
JsonElement firstEntry = entryPoints[0];
- firstEntry.ValueKind.Should().Be(JsonValueKind.Object);
- McpJsonRpcAssertions.TryGetPropertyIgnoreCase(firstEntry, "segment", out JsonElement _).Should().BeTrue();
+ firstEntry.ValueKind.Should().Be(JsonValueKind.String);
}
// TotalEntryPoints should match the array length (distinct addresses, not instruction variants)
McpJsonRpcAssertions.TryGetPropertyIgnoreCase(structured, "totalEntryPoints", out JsonElement total).Should().BeTrue();
total.GetInt32().Should().Be(entryPoints.GetArrayLength());
- // LastExecutedAddress should be null or a SegmentedAddress object, never a "None" string
+ // LastExecutedAddress should be null or a string, never an object
McpJsonRpcAssertions.TryGetPropertyIgnoreCase(structured, "lastExecutedAddress", out JsonElement lastExec).Should().BeTrue();
if (lastExec.ValueKind != JsonValueKind.Null) {
- lastExec.ValueKind.Should().Be(JsonValueKind.Object);
- McpJsonRpcAssertions.TryGetPropertyIgnoreCase(lastExec, "segment", out JsonElement _).Should().BeTrue();
+ lastExec.ValueKind.Should().Be(JsonValueKind.String);
}
- // Nodes array must be present and contain graph nodes
- AssertGraphNodesPresent(structured);
+ // Blocks array must be present and contain CfgBlocks
+ AssertGraphBlocksPresent(structured);
+
+ // LastExecutedBlockId must be either null or a non-negative int matching one of the
+ // returned blocks.
+ McpJsonRpcAssertions.TryGetPropertyIgnoreCase(structured, "lastExecutedBlockId", out JsonElement lastExecBlockId).Should().BeTrue();
+ if (lastExec.ValueKind != JsonValueKind.Null) {
+ lastExecBlockId.ValueKind.Should().Be(JsonValueKind.Number);
+ } else {
+ lastExecBlockId.ValueKind.Should().Be(JsonValueKind.Null);
+ }
}
[Fact]
@@ -1251,10 +1259,13 @@ public async Task ReadCfgCpuGraph_WithNodeLimit_ShouldRespectLimitAsync() {
// Arrange
await using McpIntegrationContext context = await McpIntegrationContext.CreateAsync(TestProgramName);
await context.InitializeAsync();
+ context.StartEmulationLoop();
+ context.WaitUntilPaused().Should().BeTrue();
- // Step a few times to build up graph nodes
+ // Step a few times to build up graph blocks
for (int i = 0; i < 5; i++) {
await context.CallToolAsync("step", new Dictionary());
+ context.WaitUntilPaused().Should().BeTrue();
}
// First get full graph to know its size
@@ -1262,42 +1273,84 @@ public async Task ReadCfgCpuGraph_WithNodeLimit_ShouldRespectLimitAsync() {
["nodeLimit"] = null
});
JsonElement fullStructured = GetSuccessfulStructuredContent(fullResponse);
- McpJsonRpcAssertions.TryGetPropertyIgnoreCase(fullStructured, "nodes", out JsonElement fullNodes).Should().BeTrue();
- int fullCount = fullNodes.GetArrayLength();
- fullCount.Should().BeGreaterThan(0, "stepping should produce graph nodes");
+ McpJsonRpcAssertions.TryGetPropertyIgnoreCase(fullStructured, "blocks", out JsonElement fullBlocks).Should().BeTrue();
+ int fullCount = fullBlocks.GetArrayLength();
+ fullCount.Should().BeGreaterThan(0, "stepping should produce graph blocks");
// Act — request with a limit smaller than the full graph
- int requestedLimit = 2;
+ int requestedLimit = 1;
JsonDocument limitedResponse = await context.CallToolAsync("read_cfg_cpu_graph", new Dictionary {
["nodeLimit"] = requestedLimit
});
// Assert
JsonElement limitedStructured = GetSuccessfulStructuredContent(limitedResponse);
- AssertGraphNodesPresent(limitedStructured);
- McpJsonRpcAssertions.TryGetPropertyIgnoreCase(limitedStructured, "nodes", out JsonElement limitedNodes).Should().BeTrue();
- limitedNodes.GetArrayLength().Should().BeLessThanOrEqualTo(requestedLimit);
+ AssertGraphBlocksPresent(limitedStructured);
+ McpJsonRpcAssertions.TryGetPropertyIgnoreCase(limitedStructured, "blocks", out JsonElement limitedBlocks).Should().BeTrue();
+ limitedBlocks.GetArrayLength().Should().BeLessThanOrEqualTo(requestedLimit);
McpJsonRpcAssertions.TryGetPropertyIgnoreCase(limitedStructured, "truncated", out JsonElement truncated).Should().BeTrue();
if (fullCount > requestedLimit) {
truncated.GetBoolean().Should().BeTrue();
}
}
- private static void AssertGraphNodesPresent(JsonElement structured) {
- McpJsonRpcAssertions.TryGetPropertyIgnoreCase(structured, "nodes", out JsonElement nodes).Should().BeTrue();
- nodes.ValueKind.Should().Be(JsonValueKind.Array);
- if (nodes.GetArrayLength() > 0) {
- JsonElement node = nodes[0];
- node.ValueKind.Should().Be(JsonValueKind.Object);
- McpJsonRpcAssertions.TryGetPropertyIgnoreCase(node, "id", out JsonElement _).Should().BeTrue();
- McpJsonRpcAssertions.TryGetPropertyIgnoreCase(node, "address", out JsonElement addr).Should().BeTrue();
- addr.ValueKind.Should().Be(JsonValueKind.Object);
- McpJsonRpcAssertions.TryGetPropertyIgnoreCase(addr, "segment", out JsonElement _).Should().BeTrue();
- McpJsonRpcAssertions.TryGetPropertyIgnoreCase(node, "successorIds", out JsonElement succs).Should().BeTrue();
- succs.ValueKind.Should().Be(JsonValueKind.Array);
- McpJsonRpcAssertions.TryGetPropertyIgnoreCase(node, "predecessorIds", out JsonElement preds).Should().BeTrue();
+ [Fact]
+ public async Task ReadCfgCpuGraph_WithZeroNodeLimit_ShouldReturnUnboundedGraphAsync() {
+ // Arrange
+ await using McpIntegrationContext context = await McpIntegrationContext.CreateAsync(TestProgramName);
+ await context.InitializeAsync();
+ context.StartEmulationLoop();
+ context.WaitUntilPaused().Should().BeTrue();
+
+ for (int i = 0; i < 5; i++) {
+ await context.CallToolAsync("step", new Dictionary());
+ context.WaitUntilPaused().Should().BeTrue();
+ }
+
+ JsonDocument fullResponse = await context.CallToolAsync("read_cfg_cpu_graph", new Dictionary {
+ ["nodeLimit"] = null
+ });
+ JsonElement fullStructured = GetSuccessfulStructuredContent(fullResponse);
+ McpJsonRpcAssertions.TryGetPropertyIgnoreCase(fullStructured, "blocks", out JsonElement fullBlocks).Should().BeTrue();
+ fullBlocks.GetArrayLength().Should().BeGreaterThan(0, "stepping should produce graph blocks");
+
+ // Act
+ JsonDocument zeroLimitResponse = await context.CallToolAsync("read_cfg_cpu_graph", new Dictionary {
+ ["nodeLimit"] = 0
+ });
+
+ // Assert
+ JsonElement zeroLimitStructured = GetSuccessfulStructuredContent(zeroLimitResponse);
+ McpJsonRpcAssertions.TryGetPropertyIgnoreCase(zeroLimitStructured, "blocks", out JsonElement zeroLimitBlocks).Should().BeTrue();
+ zeroLimitBlocks.GetArrayLength().Should().Be(fullBlocks.GetArrayLength());
+
+ McpJsonRpcAssertions.TryGetPropertyIgnoreCase(zeroLimitStructured, "truncated", out JsonElement zeroLimitTruncated).Should().BeTrue();
+ zeroLimitTruncated.GetBoolean().Should().BeFalse();
+ }
+
+ private static void AssertGraphBlocksPresent(JsonElement structured) {
+ McpJsonRpcAssertions.TryGetPropertyIgnoreCase(structured, "blocks", out JsonElement blocks).Should().BeTrue();
+ blocks.ValueKind.Should().Be(JsonValueKind.Array);
+ if (blocks.GetArrayLength() > 0) {
+ JsonElement block = blocks[0];
+ block.ValueKind.Should().Be(JsonValueKind.Object);
+ McpJsonRpcAssertions.TryGetPropertyIgnoreCase(block, "id", out JsonElement _).Should().BeTrue();
+ McpJsonRpcAssertions.TryGetPropertyIgnoreCase(block, "entry", out JsonElement entryAddr).Should().BeTrue();
+ entryAddr.ValueKind.Should().Be(JsonValueKind.String);
+ McpJsonRpcAssertions.TryGetPropertyIgnoreCase(block, "term", out JsonElement termAddr).Should().BeTrue();
+ termAddr.ValueKind.Should().Be(JsonValueKind.String);
+ McpJsonRpcAssertions.TryGetPropertyIgnoreCase(block, "pred", out JsonElement preds).Should().BeTrue();
preds.ValueKind.Should().Be(JsonValueKind.Array);
- McpJsonRpcAssertions.TryGetPropertyIgnoreCase(node, "isLive", out JsonElement _).Should().BeTrue();
+ McpJsonRpcAssertions.TryGetPropertyIgnoreCase(block, "succ", out JsonElement succs).Should().BeTrue();
+ succs.ValueKind.Should().Be(JsonValueKind.Array);
+ McpJsonRpcAssertions.TryGetPropertyIgnoreCase(block, "asm", out JsonElement disasm).Should().BeTrue();
+ disasm.ValueKind.Should().Be(JsonValueKind.Array);
+ if (disasm.GetArrayLength() > 0) {
+ JsonElement entry = disasm[0];
+ entry.ValueKind.Should().Be(JsonValueKind.String);
+ string? entryStr = entry.GetString();
+ (entryStr == "selector" || (entryStr != null && entryStr.Contains('|'))).Should().BeTrue();
+ }
}
}
diff --git a/tests/Spice86.Tests/Resources/cpuTests/asmsrc/divfaultloop.asm b/tests/Spice86.Tests/Resources/cpuTests/asmsrc/divfaultloop.asm
new file mode 100644
index 0000000000..3637e3e4f0
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/asmsrc/divfaultloop.asm
@@ -0,0 +1,64 @@
+; compile it with fasm
+; Tests CPU fault (div by 0) where the fault handler modifies the return
+; address to retry from the block entry, creating a CpuFault edge from the
+; div instruction.
+;
+; Expected memory at 00:
+; 00: 03 00 - retry count (handler was called 3 times)
+; 02: 02 00 - final AX result from the successful division (10 / 5 = 2)
+;
+use16
+start:
+mov ax,0
+mov ss,ax
+mov sp,200h
+
+; set up INT 0 (divide error) handler
+mov word[0*4], divhandler
+mov word[0*4+2], cs
+
+; retrycount at CS:0x100, divisor at CS:0x102
+mov word[cs:0x100], 0
+mov word[cs:0x102], 0
+
+; --- Faulting block ---
+; The div will fault (divisor=0). INT 0 handler adjusts the return IP
+; on the stack to point to divblock (the mov ax) so the CPU re-executes
+; the whole sequence with BX reloaded from memory.
+divblock:
+mov ax,10
+mov bx,word[cs:0x102]
+div bx
+
+; Store results
+mov bx,word[cs:0x100]
+mov word[0x00],bx
+mov word[0x02],ax
+
+hlt
+
+; INT 0 handler: increment retrycount, on 3rd call set divisor to 5,
+; and adjust return IP to divblock so the mov bx reload happens.
+divhandler:
+push bp
+mov bp,sp
+push bx
+mov bx,word[cs:0x100]
+inc bx
+mov word[cs:0x100],bx
+cmp bx,3
+jne .skip
+mov word[cs:0x102],5
+.skip:
+; Adjust return IP on stack to divblock entry (mov ax instruction).
+; Stack layout: [BP+0]=old BP, [BP+2]=return IP, [BP+4]=return CS, [BP+6]=flags
+mov word[bp+2], divblock
+pop bx
+pop bp
+iret
+
+; bios entry point at offset fff0
+rb 65520-$
+jmp start
+rb 65535-$
+db 0ffh
diff --git a/tests/Spice86.Tests/Resources/cpuTests/asmsrc/returnedterminator.asm b/tests/Spice86.Tests/Resources/cpuTests/asmsrc/returnedterminator.asm
new file mode 100644
index 0000000000..5cae2c8bc3
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/asmsrc/returnedterminator.asm
@@ -0,0 +1,37 @@
+; compile it with fasm
+; Expected memory at 0000:0000:
+; 00 00 00 00 22 22 11 11
+;
+; If ExecuteNext hot-runs the completed body block from its entry while execution
+; is already at the interior jmp, the stack gets duplicate values at 0000:0000..0003.
+use16
+
+start:
+ mov ax, 0
+ mov ss, ax
+ mov sp, 8
+ jmp body
+
+body:
+ mov ax, 1111h
+ push ax
+
+ mov ax, 2222h
+ push ax
+
+block_terminator:
+ jmp done
+
+bad:
+ mov ax, 0DEADh
+ push ax
+ hlt
+
+done:
+ hlt
+
+; BIOS entry point at offset FFF0
+rb 65520-$
+ jmp start
+rb 65535-$
+ db 0FFh
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/asmsrc/selfmodifyterminator.asm b/tests/Spice86.Tests/Resources/cpuTests/asmsrc/selfmodifyterminator.asm
new file mode 100644
index 0000000000..00ef203437
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/asmsrc/selfmodifyterminator.asm
@@ -0,0 +1,79 @@
+;00: 42 00 ff ff
+; compile it with fasm
+; Test: self-modifying code that replaces the TERMINATOR of a block.
+;
+; A block ending with 'jne loop_top' is executed on the first iteration.
+; On the second iteration, code patches that jne into 'jmp short done'.
+; The emulator detects the modification, creates a SelectorNode at the
+; terminator's address, and dispatches to the new jmp variant.
+;
+; This exercises the scenario where the terminator of a block is replaced
+; by self-modifying code.
+;
+; Flow:
+; Iteration 1: counter=0, call handler (no patch), return, or ax,ax, jne taken
+; Iteration 2: counter=1, call handler (patches jne→jmp), return, or ax,ax,
+; selector dispatches to jmp short done
+;
+; Expected stack (memory at 0000:0000): 42 00 FF FF
+; [0000] = 0x0042 (AX pushed at 'done', second push)
+; [0002] = 0xFFFF (AX pushed on first iteration, first push)
+use16
+
+start:
+ ; Setup stack so two pushes fill addresses 0-3
+ mov ax, 0
+ mov ss, ax
+ mov sp, 4
+
+ ; First iteration marker
+ mov ax, 0FFFFh
+ mov byte [counter], 0
+
+loop_top:
+ ; Push current AX value
+ push ax
+
+ ; Call the handler — it may patch the terminator
+ call patch_handler
+
+ ; Set AX to second-iteration marker
+ mov ax, 0042h
+
+ ; This block: [mov ax, 0042h] [or ax, ax] [jne loop_top]
+ ; The jne is the terminator. On iteration 2 it will have been patched.
+ or ax, ax ; ZF=0 since AX=0x0042 != 0
+block_terminator:
+ jne loop_top ; iter 1: taken. iter 2: patched to jmp short done.
+
+ ; Fallthrough — should never be reached
+ mov ax, 0DEADh
+ push ax
+ hlt
+
+done:
+ push ax
+ hlt
+
+patch_handler:
+ cmp byte [counter], 0
+ je .no_patch
+
+ ; Patch the jne at block_terminator into jmp short done
+ mov ax, 0F000h
+ mov es, ax
+ mov byte [es:block_terminator], 0EBh
+ mov byte [es:block_terminator + 1], done - (block_terminator + 2)
+ ret
+
+.no_patch:
+ inc byte [counter]
+ ret
+
+counter db 0
+
+; BIOS entry point at offset FFF0
+rb 65520-$
+ jmp start
+rb 65535-$
+ db 0FFh
diff --git a/tests/Spice86.Tests/Resources/cpuTests/asmsrc/sticli.asm b/tests/Spice86.Tests/Resources/cpuTests/asmsrc/sticli.asm
new file mode 100644
index 0000000000..7873bade6d
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/asmsrc/sticli.asm
@@ -0,0 +1,33 @@
+; compile it with fasm
+; Test: STI/CLI block boundary detection.
+;
+; STI is a Block_Terminator and CLI is a Block_Starter, so the linker must:
+; - End the block containing STI on STI itself.
+; - Start a new block at the mov ax, 0x1234 that follows STI.
+; - End that middle block before CLI (since CLI is a Block_Starter).
+; - Open a third block whose entry is CLI.
+;
+; Flow:
+; mov ax, 0x1000 ; setup DS
+; mov ds, ax
+; sti ; Block_Terminator — ends first block
+; mov ax, 0x1234 ; middle block (single instruction)
+; cli ; Block_Starter — starts third block
+; mov [si], ax ; writes 0x1234 to DS:SI (1000:0000)
+; hlt ; stop
+use16
+
+start:
+ mov ax, 0x1000
+ mov ds, ax
+ sti
+ mov ax, 0x1234
+ cli
+ mov [si], ax
+ hlt
+
+; BIOS entry point at offset FFF0
+rb 65520-$
+ jmp start
+rb 65535-$
+ db 0FFh
diff --git a/tests/Spice86.Tests/Resources/cpuTests/divfaultloop.bin b/tests/Spice86.Tests/Resources/cpuTests/divfaultloop.bin
new file mode 100644
index 0000000000..705fb53a0d
Binary files /dev/null and b/tests/Spice86.Tests/Resources/cpuTests/divfaultloop.bin differ
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/add.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/add.json
new file mode 100644
index 0000000000..1dc1fa3164
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/add.json
@@ -0,0 +1,179 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:01A8",
+ "lastExecutedBlockId": 3,
+ "blocks": [
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:01A8",
+ "pred": [
+ 2
+ ],
+ "succ": [],
+ "asm": [
+ "BCA000|mov SP,0x00A0",
+ "B8FFFF|mov AX,0xFFFF",
+ "BB0100|mov BX,1",
+ "01C3|add BX,AX",
+ "A30000|mov word ptr DS:[0],AX",
+ "891E0200|mov word ptr DS:[2],BX",
+ "9C|pushf",
+ "BAFFFF|mov DX,0xFFFF",
+ "C7060400FFFF|mov word ptr DS:[4],0xFFFF",
+ "01160400|add word ptr DS:[4],DX",
+ "89160600|mov word ptr DS:[6],DX",
+ "9C|pushf",
+ "B90100|mov CX,1",
+ "C70608000200|mov word ptr DS:[8],2",
+ "030E0800|add CX,word ptr DS:[8]",
+ "890E0A00|mov word ptr DS:[0x000A],CX",
+ "9C|pushf",
+ "B80100|mov AX,1",
+ "05FF7F|add AX,0x7FFF",
+ "A30C00|mov word ptr DS:[0x000C],AX",
+ "9C|pushf",
+ "BD0080|mov BP,0x8000",
+ "83C5FF|add BP,-1",
+ "892E0E00|mov word ptr DS:[0x000E],BP",
+ "9C|pushf",
+ "BE83C7|mov SI,0xC783",
+ "81C62AEB|add SI,0xEB2A",
+ "89361000|mov word ptr DS:[0x0010],SI",
+ "9C|pushf",
+ "C70612006089|mov word ptr DS:[0x0012],0x8960",
+ "81061200950A|add word ptr DS:[0x0012],0x0A95",
+ "9C|pushf",
+ "C7061400E1F1|mov word ptr DS:[0x0014],0xF1E1",
+ "8306140064|add word ptr DS:[0x0014],0x0064",
+ "9C|pushf",
+ "C606160001|mov byte ptr DS:[0x0016],1",
+ "80061600FF|add byte ptr DS:[0x0016],0xFF",
+ "9C|pushf",
+ "B6FF|mov DH,0xFF",
+ "80C6FF|add DH,0xFF",
+ "89161700|mov word ptr DS:[0x0017],DX",
+ "9C|pushf",
+ "B001|mov AL,1",
+ "0402|add AL,2",
+ "A31900|mov word ptr DS:[0x0019],AX",
+ "9C|pushf",
+ "C6061B007F|mov byte ptr DS:[0x001B],0x7F",
+ "B501|mov CH,1",
+ "022E1B00|add CH,byte ptr DS:[0x001B]",
+ "890E1C00|mov word ptr DS:[0x001C],CX",
+ "9C|pushf",
+ "B380|mov BL,0x80",
+ "C6061E00FF|mov byte ptr DS:[0x001E],0xFF",
+ "001E1E00|add byte ptr DS:[0x001E],BL",
+ "891E1F00|mov word ptr DS:[0x001F],BX",
+ "9C|pushf",
+ "B0A6|mov AL,0xA6",
+ "B486|mov AH,0x86",
+ "00C4|add AH,AL",
+ "A32100|mov word ptr DS:[0x0021],AX",
+ "9C|pushf",
+ "B8FFFF|mov AX,0xFFFF",
+ "BB0100|mov BX,1",
+ "11C3|adc BX,AX",
+ "A32300|mov word ptr DS:[0x0023],AX",
+ "891E2500|mov word ptr DS:[0x0025],BX",
+ "9C|pushf",
+ "BAFFFF|mov DX,0xFFFF",
+ "C7062700FFFF|mov word ptr DS:[0x0027],0xFFFF",
+ "11162700|adc word ptr DS:[0x0027],DX",
+ "89162900|mov word ptr DS:[0x0029],DX",
+ "9C|pushf",
+ "B90100|mov CX,1",
+ "C7062B000200|mov word ptr DS:[0x002B],2",
+ "130E2B00|adc CX,word ptr DS:[0x002B]",
+ "890E2D00|mov word ptr DS:[0x002D],CX",
+ "9C|pushf",
+ "B80100|mov AX,1",
+ "15FF7F|adc AX,0x7FFF",
+ "A32F00|mov word ptr DS:[0x002F],AX",
+ "9C|pushf",
+ "BD0080|mov BP,0x8000",
+ "83D5FF|adc BP,-1",
+ "892E3100|mov word ptr DS:[0x0031],BP",
+ "9C|pushf",
+ "BED377|mov SI,0x77D3",
+ "81D62584|adc SI,0x8425",
+ "89363300|mov word ptr DS:[0x0033],SI",
+ "9C|pushf",
+ "C7063500A0EB|mov word ptr DS:[0x0035],0xEBA0",
+ "81163500C1D3|adc word ptr DS:[0x0035],0xD3C1",
+ "9C|pushf",
+ "C7063700507F|mov word ptr DS:[0x0037],0x7F50",
+ "83163700F5|adc word ptr DS:[0x0037],-11",
+ "9C|pushf",
+ "C606390001|mov byte ptr DS:[0x0039],1",
+ "80163900FF|adc byte ptr DS:[0x0039],0xFF",
+ "9C|pushf",
+ "B6FF|mov DH,0xFF",
+ "80D6FF|adc DH,0xFF",
+ "89163A00|mov word ptr DS:[0x003A],DX",
+ "9C|pushf",
+ "B001|mov AL,1",
+ "1402|adc AL,2",
+ "A33C00|mov word ptr DS:[0x003C],AX",
+ "9C|pushf",
+ "C6063E007F|mov byte ptr DS:[0x003E],0x7F",
+ "B501|mov CH,1",
+ "122E3E00|adc CH,byte ptr DS:[0x003E]",
+ "890E3F00|mov word ptr DS:[0x003F],CX",
+ "9C|pushf",
+ "B380|mov BL,0x80",
+ "C6064100FF|mov byte ptr DS:[0x0041],0xFF",
+ "101E4100|adc byte ptr DS:[0x0041],BL",
+ "891E4200|mov word ptr DS:[0x0042],BX",
+ "9C|pushf",
+ "B0B9|mov AL,0xB9",
+ "B4D3|mov AH,0xD3",
+ "10C4|adc AH,AL",
+ "A34400|mov word ptr DS:[0x0044],AX",
+ "9C|pushf",
+ "BFFFFF|mov DI,0xFFFF",
+ "47|inc DI",
+ "893E4600|mov word ptr DS:[0x0046],DI",
+ "9C|pushf",
+ "BDFF7F|mov BP,0x7FFF",
+ "FFC5|inc BP",
+ "892E4800|mov word ptr DS:[0x0048],BP",
+ "9C|pushf",
+ "C7064A001274|mov word ptr DS:[0x004A],0x7412",
+ "FF064A00|inc word ptr DS:[0x004A]",
+ "9C|pushf",
+ "B27F|mov DL,0x7F",
+ "FEC2|inc DL",
+ "89164C00|mov word ptr DS:[0x004C],DX",
+ "9C|pushf",
+ "C6064D00FF|mov byte ptr DS:[0x004D],0xFF",
+ "FE064D00|inc byte ptr DS:[0x004D]",
+ "9C|pushf",
+ "C6064E00B5|mov byte ptr DS:[0x004E],0xB5",
+ "FE064E00|inc byte ptr DS:[0x004E]",
+ "9C|pushf",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/bcdcnv.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/bcdcnv.json
new file mode 100644
index 0000000000..1844f1ba8d
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/bcdcnv.json
@@ -0,0 +1,390 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:0121",
+ "lastExecutedBlockId": 122,
+ "blocks": [
+ {
+ "id": 122,
+ "entry": "F000:00C1",
+ "term": "F000:0121",
+ "pred": [
+ 120
+ ],
+ "succ": [],
+ "asm": [
+ "B88200|mov AX,0x0082",
+ "2F|das",
+ "A32A00|mov word ptr DS:[0x002A],AX",
+ "9C|pushf",
+ "B89A05|mov AX,0x059A",
+ "2F|das",
+ "A32C00|mov word ptr DS:[0x002C],AX",
+ "9C|pushf",
+ "B8F654|mov AX,0x54F6",
+ "2F|das",
+ "A32E00|mov word ptr DS:[0x002E],AX",
+ "9C|pushf",
+ "B87FFF|mov AX,0xFF7F",
+ "98|cbw",
+ "A33000|mov word ptr DS:[0x0030],AX",
+ "89163200|mov word ptr DS:[0x0032],DX",
+ "9C|pushf",
+ "B88000|mov AX,0x0080",
+ "98|cbw",
+ "A33400|mov word ptr DS:[0x0034],AX",
+ "89163600|mov word ptr DS:[0x0036],DX",
+ "9C|pushf",
+ "B8EDF1|mov AX,0xF1ED",
+ "98|cbw",
+ "A33800|mov word ptr DS:[0x0038],AX",
+ "89163A00|mov word ptr DS:[0x003A],DX",
+ "9C|pushf",
+ "B80080|mov AX,0x8000",
+ "99|cwd",
+ "A33C00|mov word ptr DS:[0x003C],AX",
+ "89163E00|mov word ptr DS:[0x003E],DX",
+ "9C|pushf",
+ "B8FF7F|mov AX,0x7FFF",
+ "99|cwd",
+ "A34000|mov word ptr DS:[0x0040],AX",
+ "89164200|mov word ptr DS:[0x0042],DX",
+ "9C|pushf",
+ "B8F143|mov AX,0x43F1",
+ "99|cwd",
+ "A34400|mov word ptr DS:[0x0044],AX",
+ "89164600|mov word ptr DS:[0x0046],DX",
+ "9C|pushf",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 120,
+ "entry": "F000:00C0",
+ "term": "F000:00C0",
+ "pred": [
+ 114
+ ],
+ "succ": [
+ 122
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:0019",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 16
+ ],
+ "asm": [
+ "BB0100|mov BX,1",
+ "B90000|mov CX,0",
+ "BC9000|mov SP,0x0090",
+ "B80A00|mov AX,0x000A",
+ "37|aaa",
+ "A30000|mov word ptr DS:[0],AX",
+ "9C|pushf",
+ "B8F9FF|mov AX,0xFFF9",
+ "37|aaa",
+ "A30200|mov word ptr DS:[2],AX",
+ "9C|pushf",
+ "53|push BX"
+ ]
+ },
+ {
+ "id": 114,
+ "entry": "F000:00B7",
+ "term": "F000:00BF",
+ "pred": [
+ 112
+ ],
+ "succ": [
+ 120
+ ],
+ "asm": [
+ "B88BFF|mov AX,0xFF8B",
+ "2F|das",
+ "A32800|mov word ptr DS:[0x0028],AX",
+ "9C|pushf",
+ "51|push CX"
+ ]
+ },
+ {
+ "id": 16,
+ "entry": "F000:001A",
+ "term": "F000:001A",
+ "pred": [
+ 3
+ ],
+ "succ": [
+ 18
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 112,
+ "entry": "F000:00B6",
+ "term": "F000:00B6",
+ "pred": [
+ 106
+ ],
+ "succ": [
+ 114
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 18,
+ "entry": "F000:001B",
+ "term": "F000:0043",
+ "pred": [
+ 16
+ ],
+ "succ": [
+ 40
+ ],
+ "asm": [
+ "B8F9FF|mov AX,0xFFF9",
+ "37|aaa",
+ "A30400|mov word ptr DS:[4],AX",
+ "9C|pushf",
+ "B8505D|mov AX,0x5D50",
+ "37|aaa",
+ "A30600|mov word ptr DS:[6],AX",
+ "9C|pushf",
+ "B82647|mov AX,0x4726",
+ "37|aaa",
+ "A30800|mov word ptr DS:[8],AX",
+ "9C|pushf",
+ "B80A00|mov AX,0x000A",
+ "3F|aas",
+ "A30A00|mov word ptr DS:[0x000A],AX",
+ "9C|pushf",
+ "B8F9FF|mov AX,0xFFF9",
+ "3F|aas",
+ "A30C00|mov word ptr DS:[0x000C],AX",
+ "9C|pushf",
+ "53|push BX"
+ ]
+ },
+ {
+ "id": 106,
+ "entry": "F000:00AD",
+ "term": "F000:00B5",
+ "pred": [
+ 104
+ ],
+ "succ": [
+ 112
+ ],
+ "asm": [
+ "B8F8FF|mov AX,0xFFF8",
+ "2F|das",
+ "A32600|mov word ptr DS:[0x0026],AX",
+ "9C|pushf",
+ "51|push CX"
+ ]
+ },
+ {
+ "id": 40,
+ "entry": "F000:0044",
+ "term": "F000:0044",
+ "pred": [
+ 18
+ ],
+ "succ": [
+ 42
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 104,
+ "entry": "F000:00AC",
+ "term": "F000:00AC",
+ "pred": [
+ 82
+ ],
+ "succ": [
+ 106
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 42,
+ "entry": "F000:0045",
+ "term": "F000:006D",
+ "pred": [
+ 40
+ ],
+ "succ": [
+ 64
+ ],
+ "asm": [
+ "B8F9FF|mov AX,0xFFF9",
+ "3F|aas",
+ "A30E00|mov word ptr DS:[0x000E],AX",
+ "9C|pushf",
+ "B8C0DC|mov AX,0xDCC0",
+ "3F|aas",
+ "A31000|mov word ptr DS:[0x0010],AX",
+ "9C|pushf",
+ "B8FB5F|mov AX,0x5FFB",
+ "3F|aas",
+ "A31200|mov word ptr DS:[0x0012],AX",
+ "9C|pushf",
+ "B8AC00|mov AX,0x00AC",
+ "27|daa",
+ "A31400|mov word ptr DS:[0x0014],AX",
+ "9C|pushf",
+ "B8F9FF|mov AX,0xFFF9",
+ "27|daa",
+ "A31600|mov word ptr DS:[0x0016],AX",
+ "9C|pushf",
+ "53|push BX"
+ ]
+ },
+ {
+ "id": 82,
+ "entry": "F000:0083",
+ "term": "F000:00AB",
+ "pred": [
+ 80
+ ],
+ "succ": [
+ 104
+ ],
+ "asm": [
+ "B88200|mov AX,0x0082",
+ "27|daa",
+ "A31C00|mov word ptr DS:[0x001C],AX",
+ "9C|pushf",
+ "B83CCD|mov AX,0xCD3C",
+ "27|daa",
+ "A31E00|mov word ptr DS:[0x001E],AX",
+ "9C|pushf",
+ "B8003F|mov AX,0x3F00",
+ "27|daa",
+ "A32000|mov word ptr DS:[0x0020],AX",
+ "9C|pushf",
+ "B8AC00|mov AX,0x00AC",
+ "2F|das",
+ "A32200|mov word ptr DS:[0x0022],AX",
+ "9C|pushf",
+ "B8F9FF|mov AX,0xFFF9",
+ "2F|das",
+ "A32400|mov word ptr DS:[0x0024],AX",
+ "9C|pushf",
+ "53|push BX"
+ ]
+ },
+ {
+ "id": 64,
+ "entry": "F000:006E",
+ "term": "F000:006E",
+ "pred": [
+ 42
+ ],
+ "succ": [
+ 66
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 80,
+ "entry": "F000:0082",
+ "term": "F000:0082",
+ "pred": [
+ 74
+ ],
+ "succ": [
+ 82
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 66,
+ "entry": "F000:006F",
+ "term": "F000:0077",
+ "pred": [
+ 64
+ ],
+ "succ": [
+ 72
+ ],
+ "asm": [
+ "B8F8FF|mov AX,0xFFF8",
+ "27|daa",
+ "A31800|mov word ptr DS:[0x0018],AX",
+ "9C|pushf",
+ "51|push CX"
+ ]
+ },
+ {
+ "id": 74,
+ "entry": "F000:0079",
+ "term": "F000:0081",
+ "pred": [
+ 72
+ ],
+ "succ": [
+ 80
+ ],
+ "asm": [
+ "B88BFF|mov AX,0xFF8B",
+ "27|daa",
+ "A31A00|mov word ptr DS:[0x001A],AX",
+ "9C|pushf",
+ "51|push CX"
+ ]
+ },
+ {
+ "id": 72,
+ "entry": "F000:0078",
+ "term": "F000:0078",
+ "pred": [
+ 66
+ ],
+ "succ": [
+ 74
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/bitwise.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/bitwise.json
new file mode 100644
index 0000000000..d784c89ec3
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/bitwise.json
@@ -0,0 +1,234 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:01F2",
+ "lastExecutedBlockId": 166,
+ "blocks": [
+ {
+ "id": 166,
+ "entry": "F000:01DA",
+ "term": "F000:01F2",
+ "pred": [
+ 164
+ ],
+ "succ": [],
+ "asm": [
+ "F7D2|not DX",
+ "9C|pushf",
+ "89165000|mov word ptr DS:[0x0050],DX",
+ "F7161600|not word ptr DS:[0x0016]",
+ "9C|pushf",
+ "F6D2|not DL",
+ "9C|pushf",
+ "88165200|mov byte ptr DS:[0x0052],DL",
+ "F6161800|not byte ptr DS:[0x0018]",
+ "9C|pushf",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 164,
+ "entry": "F000:01D9",
+ "term": "F000:01D9",
+ "pred": [
+ 3
+ ],
+ "succ": [
+ 166
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:01D8",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 164
+ ],
+ "asm": [
+ "B85976|mov AX,0x7659",
+ "BBB84B|mov BX,0x4BB8",
+ "B9843C|mov CX,0x3C84",
+ "C7060000761B|mov word ptr DS:[0],0x1B76",
+ "C70602000B24|mov word ptr DS:[2],0x240B",
+ "BC0001|mov SP,0x0100",
+ "21C3|and BX,AX",
+ "9C|pushf",
+ "891E2000|mov word ptr DS:[0x0020],BX",
+ "230E0200|and CX,word ptr DS:[2]",
+ "9C|pushf",
+ "890E2200|mov word ptr DS:[0x0022],CX",
+ "210E0000|and word ptr DS:[0],CX",
+ "9C|pushf",
+ "257145|and AX,0x4571",
+ "9C|pushf",
+ "A32400|mov word ptr DS:[0x0024],AX",
+ "81E3E927|and BX,0x27E9",
+ "9C|pushf",
+ "891E2600|mov word ptr DS:[0x0026],BX",
+ "812602004935|and word ptr DS:[2],0x3549",
+ "9C|pushf",
+ "20C4|and AH,AL",
+ "9C|pushf",
+ "88262800|mov byte ptr DS:[0x0028],AH",
+ "220E0100|and CL,byte ptr DS:[1]",
+ "9C|pushf",
+ "880E2900|mov byte ptr DS:[0x0029],CL",
+ "202E0300|and byte ptr DS:[3],CH",
+ "9C|pushf",
+ "2446|and AL,0x46",
+ "9C|pushf",
+ "A22A00|mov byte ptr DS:[0x002A],AL",
+ "80E32D|and BL,0x2D",
+ "9C|pushf",
+ "881E2B00|mov byte ptr DS:[0x002B],BL",
+ "80260200C6|and byte ptr DS:[2],0xC6",
+ "9C|pushf",
+ "B8E305|mov AX,0x05E3",
+ "BB77F8|mov BX,0xF877",
+ "B9E84A|mov CX,0x4AE8",
+ "BA693B|mov DX,0x3B69",
+ "C7060400C030|mov word ptr DS:[4],0x30C0",
+ "C70606007557|mov word ptr DS:[6],0x5775",
+ "C706080066FE|mov word ptr DS:[8],0xFE66",
+ "09C3|or BX,AX",
+ "9C|pushf",
+ "891E2C00|mov word ptr DS:[0x002C],BX",
+ "0B0E0400|or CX,word ptr DS:[4]",
+ "9C|pushf",
+ "890E2E00|mov word ptr DS:[0x002E],CX",
+ "09060600|or word ptr DS:[6],AX",
+ "9C|pushf",
+ "0DC341|or AX,0x41C3",
+ "9C|pushf",
+ "A33000|mov word ptr DS:[0x0030],AX",
+ "81CA5DB0|or DX,0xB05D",
+ "9C|pushf",
+ "89163200|mov word ptr DS:[0x0032],DX",
+ "810E08004C8D|or word ptr DS:[8],0x8D4C",
+ "9C|pushf",
+ "08C4|or AH,AL",
+ "9C|pushf",
+ "88263400|mov byte ptr DS:[0x0034],AH",
+ "0A0E0500|or CL,byte ptr DS:[5]",
+ "9C|pushf",
+ "880E3500|mov byte ptr DS:[0x0035],CL",
+ "082E0600|or byte ptr DS:[6],CH",
+ "9C|pushf",
+ "0C43|or AL,0x43",
+ "9C|pushf",
+ "A23600|mov byte ptr DS:[0x0036],AL",
+ "80CB57|or BL,0x57",
+ "9C|pushf",
+ "881E3700|mov byte ptr DS:[0x0037],BL",
+ "800E070054|or byte ptr DS:[7],0x54",
+ "9C|pushf",
+ "B8B4D0|mov AX,0xD0B4",
+ "BBB81B|mov BX,0x1BB8",
+ "B9032B|mov CX,0x2B03",
+ "BAE6C3|mov DX,0xC3E6",
+ "C7060A003939|mov word ptr DS:[0x000A],0x3939",
+ "C7060C004B86|mov word ptr DS:[0x000C],0x864B",
+ "C7060E008785|mov word ptr DS:[0x000E],0x8587",
+ "31C3|xor BX,AX",
+ "9C|pushf",
+ "891E3800|mov word ptr DS:[0x0038],BX",
+ "330E0A00|xor CX,word ptr DS:[0x000A]",
+ "9C|pushf",
+ "890E3A00|mov word ptr DS:[0x003A],CX",
+ "31060C00|xor word ptr DS:[0x000C],AX",
+ "9C|pushf",
+ "35033D|xor AX,0x3D03",
+ "9C|pushf",
+ "A33C00|mov word ptr DS:[0x003C],AX",
+ "81F22D63|xor DX,0x632D",
+ "9C|pushf",
+ "89163E00|mov word ptr DS:[0x003E],DX",
+ "81360E0007CF|xor word ptr DS:[0x000E],0xCF07",
+ "9C|pushf",
+ "30C4|xor AH,AL",
+ "9C|pushf",
+ "88264000|mov byte ptr DS:[0x0040],AH",
+ "320E0B00|xor CL,byte ptr DS:[0x000B]",
+ "9C|pushf",
+ "880E4100|mov byte ptr DS:[0x0041],CL",
+ "302E0C00|xor byte ptr DS:[0x000C],CH",
+ "9C|pushf",
+ "34B6|xor AL,0xB6",
+ "9C|pushf",
+ "A24200|mov byte ptr DS:[0x0042],AL",
+ "80F3AE|xor BL,0xAE",
+ "9C|pushf",
+ "881E4300|mov byte ptr DS:[0x0043],BL",
+ "80360D00DF|xor byte ptr DS:[0x000D],0xDF",
+ "9C|pushf",
+ "B8374D|mov AX,0x4D37",
+ "BBE1DB|mov BX,0xDBE1",
+ "B94965|mov CX,0x6549",
+ "BAC45C|mov DX,0x5CC4",
+ "C7061000A8A8|mov word ptr DS:[0x0010],0xA8A8",
+ "C7061200F635|mov word ptr DS:[0x0012],0x35F6",
+ "C7061400004F|mov word ptr DS:[0x0014],0x4F00",
+ "85C3|and BX,AX",
+ "9C|pushf",
+ "891E4400|mov word ptr DS:[0x0044],BX",
+ "850E1000|and word ptr DS:[0x0010],CX",
+ "9C|pushf",
+ "890E4600|mov word ptr DS:[0x0046],CX",
+ "85061200|and word ptr DS:[0x0012],AX",
+ "9C|pushf",
+ "A96FDC|test AX,0xDC6F",
+ "9C|pushf",
+ "A34800|mov word ptr DS:[0x0048],AX",
+ "F7C24630|test DX,0x3046",
+ "9C|pushf",
+ "89164A00|mov word ptr DS:[0x004A],DX",
+ "F7061400E496|test word ptr DS:[0x0014],0x96E4",
+ "9C|pushf",
+ "84C4|and AH,AL",
+ "9C|pushf",
+ "88264C00|mov byte ptr DS:[0x004C],AH",
+ "840E0F00|and byte ptr DS:[0x000F],CL",
+ "9C|pushf",
+ "880E4D00|mov byte ptr DS:[0x004D],CL",
+ "842E1000|and byte ptr DS:[0x0010],CH",
+ "9C|pushf",
+ "A8C0|test AL,0xC0",
+ "9C|pushf",
+ "A24E00|mov byte ptr DS:[0x004E],AL",
+ "F6C3E0|test BL,0xE0",
+ "9C|pushf",
+ "881E4F00|mov byte ptr DS:[0x004F],BL",
+ "F6061100BB|test byte ptr DS:[0x0011],0xBB",
+ "9C|pushf",
+ "BAA5BF|mov DX,0xBFA5",
+ "C7061600E64B|mov word ptr DS:[0x0016],0x4BE6",
+ "C7061800D2E9|mov word ptr DS:[0x0018],0xE9D2",
+ "B8B112|mov AX,0x12B1",
+ "50|push AX"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/cmpneg.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/cmpneg.json
new file mode 100644
index 0000000000..6ca9ea9417
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/cmpneg.json
@@ -0,0 +1,126 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:0107",
+ "lastExecutedBlockId": 3,
+ "blocks": [
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:0107",
+ "pred": [
+ 2
+ ],
+ "succ": [],
+ "asm": [
+ "BC6000|mov SP,0x0060",
+ "B80100|mov AX,1",
+ "BB0200|mov BX,2",
+ "39D8|cmp AX,BX",
+ "A30000|mov word ptr DS:[0],AX",
+ "891E0200|mov word ptr DS:[2],BX",
+ "9C|pushf",
+ "BAFFFF|mov DX,0xFFFF",
+ "C7060400FFFF|mov word ptr DS:[4],0xFFFF",
+ "39160400|cmp word ptr DS:[4],DX",
+ "89160600|mov word ptr DS:[6],DX",
+ "9C|pushf",
+ "B9FFFF|mov CX,0xFFFF",
+ "C70608000100|mov word ptr DS:[8],1",
+ "390E0800|cmp word ptr DS:[8],CX",
+ "890E0A00|mov word ptr DS:[0x000A],CX",
+ "9C|pushf",
+ "B80080|mov AX,0x8000",
+ "83F801|cmp AX,1",
+ "A30C00|mov word ptr DS:[0x000C],AX",
+ "9C|pushf",
+ "BD0080|mov BP,0x8000",
+ "83FDFF|cmp BP,-1",
+ "892E0E00|mov word ptr DS:[0x000E],BP",
+ "9C|pushf",
+ "BE817F|mov SI,0x7F81",
+ "81FE3C90|cmp SI,0x903C",
+ "89361000|mov word ptr DS:[0x0010],SI",
+ "9C|pushf",
+ "C7061200C3EF|mov word ptr DS:[0x0012],0xEFC3",
+ "813E120064C6|cmp word ptr DS:[0x0012],0xC664",
+ "9C|pushf",
+ "C706140033E9|mov word ptr DS:[0x0014],0xE933",
+ "833E140064|cmp word ptr DS:[0x0014],0x0064",
+ "9C|pushf",
+ "C606160001|mov byte ptr DS:[0x0016],1",
+ "803E160002|cmp byte ptr DS:[0x0016],2",
+ "9C|pushf",
+ "B6FF|mov DH,0xFF",
+ "80FEFF|cmp DH,0xFF",
+ "89161700|mov word ptr DS:[0x0017],DX",
+ "9C|pushf",
+ "B0FF|mov AL,0xFF",
+ "3C01|cmp AL,1",
+ "A31900|mov word ptr DS:[0x0019],AX",
+ "9C|pushf",
+ "C6061B0080|mov byte ptr DS:[0x001B],0x80",
+ "B501|mov CH,1",
+ "3A2E1B00|cmp CH,byte ptr DS:[0x001B]",
+ "890E1C00|mov word ptr DS:[0x001C],CX",
+ "9C|pushf",
+ "B380|mov BL,0x80",
+ "C6061E007F|mov byte ptr DS:[0x001E],0x7F",
+ "381E1E00|cmp byte ptr DS:[0x001E],BL",
+ "891E1F00|mov word ptr DS:[0x001F],BX",
+ "9C|pushf",
+ "B0BC|mov AL,0xBC",
+ "B48E|mov AH,0x8E",
+ "38C4|cmp AH,AL",
+ "A32100|mov word ptr DS:[0x0021],AX",
+ "9C|pushf",
+ "B90000|mov CX,0",
+ "F7D9|neg CX",
+ "890E2200|mov word ptr DS:[0x0022],CX",
+ "9C|pushf",
+ "C7062400FF7F|mov word ptr DS:[0x0024],0x7FFF",
+ "F71E2400|neg word ptr DS:[0x0024]",
+ "9C|pushf",
+ "BD0080|mov BP,0x8000",
+ "F7DD|neg BP",
+ "892E2600|mov word ptr DS:[0x0026],BP",
+ "9C|pushf",
+ "C7062800E9AC|mov word ptr DS:[0x0028],0xACE9",
+ "F71E2800|neg word ptr DS:[0x0028]",
+ "9C|pushf",
+ "B400|mov AH,0",
+ "F6DC|neg AH",
+ "A32A00|mov word ptr DS:[0x002A],AX",
+ "9C|pushf",
+ "C6062C007F|mov byte ptr DS:[0x002C],0x7F",
+ "F61E2C00|neg byte ptr DS:[0x002C]",
+ "9C|pushf",
+ "B1C9|mov CL,0xC9",
+ "F6D9|neg CL",
+ "890E2D00|mov word ptr DS:[0x002D],CX",
+ "9C|pushf",
+ "C6062F0080|mov byte ptr DS:[0x002F],0x80",
+ "F61E2F00|neg byte ptr DS:[0x002F]",
+ "9C|pushf",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/control.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/control.json
new file mode 100644
index 0000000000..99bf3dd552
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/control.json
@@ -0,0 +1,140 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:002A",
+ "lastExecutedBlockId": 28,
+ "blocks": [
+ {
+ "id": 28,
+ "entry": "F000:001C",
+ "term": "F000:002A",
+ "pred": [
+ 23
+ ],
+ "succ": [],
+ "asm": [
+ "9C|pushf",
+ "5B|pop BX",
+ "B90000|mov CX,0",
+ "8ED9|mov DS,CX",
+ "A30000|mov word ptr DS:[0],AX",
+ "891E0200|mov word ptr DS:[2],BX",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 23,
+ "entry": "F000:0018",
+ "term": "F000:001B",
+ "pred": [
+ 21
+ ],
+ "succ": [
+ 28
+ ],
+ "asm": [
+ "F5|cmc",
+ "F9|stc",
+ "FD|std",
+ "FB|sti"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:000B",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 9
+ ],
+ "asm": [
+ "B80010|mov AX,0x1000",
+ "8ED0|mov SS,AX",
+ "BC0010|mov SP,0x1000",
+ "B9FFFE|mov CX,0xFEFF",
+ "51|push CX"
+ ]
+ },
+ {
+ "id": 21,
+ "entry": "F000:0017",
+ "term": "F000:0017",
+ "pred": [
+ 14
+ ],
+ "succ": [
+ 23
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 9,
+ "entry": "F000:000C",
+ "term": "F000:000C",
+ "pred": [
+ 3
+ ],
+ "succ": [
+ 11
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 14,
+ "entry": "F000:000F",
+ "term": "F000:0016",
+ "pred": [
+ 11
+ ],
+ "succ": [
+ 21
+ ],
+ "asm": [
+ "FA|cli",
+ "90|nop",
+ "9C|pushf",
+ "58|pop AX",
+ "BA0100|mov DX,1",
+ "52|push DX"
+ ]
+ },
+ {
+ "id": 11,
+ "entry": "F000:000D",
+ "term": "F000:000E",
+ "pred": [
+ 9
+ ],
+ "succ": [
+ 14
+ ],
+ "asm": [
+ "F8|clc",
+ "FC|cld"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/datatrnf.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/datatrnf.json
new file mode 100644
index 0000000000..0b9f88630a
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/datatrnf.json
@@ -0,0 +1,144 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:00C5",
+ "lastExecutedBlockId": 41,
+ "blocks": [
+ {
+ "id": 41,
+ "entry": "F000:0054",
+ "term": "F000:00C5",
+ "pred": [
+ 39
+ ],
+ "succ": [],
+ "asm": [
+ "C478D2|les DI,word ptr DS:[BX\u002BSI-46]",
+ "8DB33CFE|lea SI,BP\u002BDI-452",
+ "9C|pushf",
+ "B80000|mov AX,0",
+ "8ED8|mov DS,AX",
+ "893E0800|mov word ptr DS:[8],DI",
+ "8CC0|mov AX,ES",
+ "A30A00|mov word ptr DS:[0x000A],AX",
+ "89360C00|mov word ptr DS:[0x000C],SI",
+ "B80010|mov AX,0x1000",
+ "8ED8|mov DS,AX",
+ "8F4401|pop word ptr DS:[SI\u002B1]",
+ "877802|xchg DI,word ptr DS:[BX\u002BSI\u002B2]",
+ "FF7002|push word ptr DS:[BX\u002BSI\u002B2]",
+ "07|pop ES",
+ "8CC2|mov DX,ES",
+ "8CD8|mov AX,DS",
+ "B90000|mov CX,0",
+ "8ED9|mov DS,CX",
+ "893E0E00|mov word ptr DS:[0x000E],DI",
+ "89161000|mov word ptr DS:[0x0010],DX",
+ "8ED8|mov DS,AX",
+ "5A|pop DX",
+ "52|push DX",
+ "8FC1|pop CX",
+ "87CB|xchg CX,BX",
+ "8CD8|mov AX,DS",
+ "BA0000|mov DX,0",
+ "8EDA|mov DS,DX",
+ "891E1200|mov word ptr DS:[0x0012],BX",
+ "890E1400|mov word ptr DS:[0x0014],CX",
+ "8ED8|mov DS,AX",
+ "8C01|mov word ptr DS:[BX\u002BDI],ES",
+ "BB00B8|mov BX,0xB800",
+ "B8A1A0|mov AX,0xA0A1",
+ "D7|xlat byte ptr DS:[BX\u002BAL]",
+ "86E0|xchg AH,AL",
+ "D7|xlat byte ptr DS:[BX\u002BAL]",
+ "BA0000|mov DX,0",
+ "8EDA|mov DS,DX",
+ "A31600|mov word ptr DS:[0x0016],AX",
+ "BAB700|mov DX,0x00B7",
+ "B0FF|mov AL,0xFF",
+ "B0F1|mov AL,0xF1",
+ "A31800|mov word ptr DS:[0x0018],AX",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 39,
+ "entry": "F000:0053",
+ "term": "F000:0053",
+ "pred": [
+ 3
+ ],
+ "succ": [
+ 41
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:0052",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 39
+ ],
+ "asm": [
+ "B4ED|mov AH,0xED",
+ "9E|sahf",
+ "9F|lahf",
+ "88260000|mov byte ptr DS:[0],AH",
+ "88E0|mov AL,AH",
+ "B8B700|mov AX,0x00B7",
+ "89C2|mov DX,AX",
+ "B4A5|mov AH,0xA5",
+ "B0C7|mov AL,0xC7",
+ "A30200|mov word ptr DS:[2],AX",
+ "9E|sahf",
+ "9F|lahf",
+ "A32000|mov word ptr DS:[0x0020],AX",
+ "B852F7|mov AX,0xF752",
+ "89C3|mov BX,AX",
+ "B8C787|mov AX,0x87C7",
+ "93|xchg BX,AX",
+ "8ED8|mov DS,AX",
+ "C5B70D03|lds SI,word ptr DS:[BX\u002B0x030D]",
+ "B80000|mov AX,0",
+ "8CDB|mov BX,DS",
+ "8ED8|mov DS,AX",
+ "891E0400|mov word ptr DS:[4],BX",
+ "89360600|mov word ptr DS:[6],SI",
+ "8EDB|mov DS,BX",
+ "BBFFFF|mov BX,0xFFFF",
+ "B80010|mov AX,0x1000",
+ "B89857|mov AX,0x5798",
+ "8ED0|mov SS,AX",
+ "BC0900|mov SP,9",
+ "B9CDAB|mov CX,0xABCD",
+ "51|push CX",
+ "B9F18C|mov CX,0x8CF1",
+ "8EC1|mov ES,CX",
+ "06|push ES"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/div.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/div.json
new file mode 100644
index 0000000000..36685fe377
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/div.json
@@ -0,0 +1,499 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:031C",
+ "lastExecutedBlockId": 265,
+ "blocks": [
+ {
+ "id": 265,
+ "entry": "F000:02F6",
+ "term": "F000:031C",
+ "pred": [
+ 32,
+ 247
+ ],
+ "succ": [],
+ "asm": [
+ "83C502|add BP,2",
+ "A38A00|mov word ptr DS:[0x008A],AX",
+ "9C|pushf",
+ "C746000200|mov word ptr SS:[BP],2",
+ "D401|aam",
+ "83C502|add BP,2",
+ "A38C00|mov word ptr DS:[0x008C],AX",
+ "9C|pushf",
+ "B8FB3F|mov AX,0x3FFB",
+ "C746000200|mov word ptr SS:[BP],2",
+ "D40A|aam",
+ "83C502|add BP,2",
+ "A38E00|mov word ptr DS:[0x008E],AX",
+ "9C|pushf",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 32,
+ "entry": "F000:1000",
+ "term": "F000:101A",
+ "pred": [
+ 3,
+ 47,
+ 58,
+ 68,
+ 104,
+ 113,
+ 149,
+ 187,
+ 206,
+ 239,
+ 247
+ ],
+ "succ": [
+ 47,
+ 58,
+ 68,
+ 104,
+ 113,
+ 149,
+ 187,
+ 206,
+ 239,
+ 247,
+ 265
+ ],
+ "asm": [
+ "50|push AX",
+ "57|push DI",
+ "8B4600|mov AX,word ptr SS:[BP]",
+ "89E6|mov SI,SP",
+ "83C604|add SI,4",
+ "8B34|mov SI,word ptr DS:[SI]",
+ "897600|mov word ptr SS:[BP],SI",
+ "01C6|add SI,AX",
+ "89E7|mov DI,SP",
+ "83C704|add DI,4",
+ "8935|mov word ptr DS:[DI],SI",
+ "5F|pop DI",
+ "58|pop AX",
+ "CF|iret"
+ ]
+ },
+ {
+ "id": 247,
+ "entry": "F000:02C6",
+ "term": "F000:02F4",
+ "pred": [
+ 32,
+ 239
+ ],
+ "succ": [
+ 32,
+ 265
+ ],
+ "asm": [
+ "83C502|add BP,2",
+ "A38400|mov word ptr DS:[0x0084],AX",
+ "9C|pushf",
+ "C746000200|mov word ptr SS:[BP],2",
+ "D401|aam",
+ "83C502|add BP,2",
+ "A38600|mov word ptr DS:[0x0086],AX",
+ "9C|pushf",
+ "B8FFFF|mov AX,0xFFFF",
+ "C746000200|mov word ptr SS:[BP],2",
+ "D40A|aam",
+ "83C502|add BP,2",
+ "A38800|mov word ptr DS:[0x0088],AX",
+ "9C|pushf",
+ "B800FF|mov AX,0xFF00",
+ "C746000200|mov word ptr SS:[BP],2",
+ "D400|aam"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:005F",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 32,
+ 47
+ ],
+ "asm": [
+ "BCD000|mov SP,0x00D0",
+ "C70600000010|mov word ptr DS:[0],0x1000",
+ "C706020000F0|mov word ptr DS:[2],0xF000",
+ "BDD000|mov BP,0x00D0",
+ "BA0000|mov DX,0",
+ "B81400|mov AX,0x0014",
+ "BB0500|mov BX,5",
+ "C746000200|mov word ptr SS:[BP],2",
+ "F7F3|div BX",
+ "83C502|add BP,2",
+ "A38000|mov word ptr DS:[0x0080],AX",
+ "891E8200|mov word ptr DS:[0x0082],BX",
+ "89160400|mov word ptr DS:[4],DX",
+ "9C|pushf",
+ "BA20A3|mov DX,0xA320",
+ "B8DAC3|mov AX,0xC3DA",
+ "C7060600FFFF|mov word ptr DS:[6],0xFFFF",
+ "C746000400|mov word ptr SS:[BP],4",
+ "F7360600|div word ptr DS:[6]",
+ "83C502|add BP,2",
+ "A30800|mov word ptr DS:[8],AX",
+ "89160A00|mov word ptr DS:[0x000A],DX",
+ "9C|pushf",
+ "BAFFFF|mov DX,0xFFFF",
+ "B8FFFF|mov AX,0xFFFF",
+ "B90100|mov CX,1",
+ "C746000200|mov word ptr SS:[BP],2",
+ "F7F1|div CX"
+ ]
+ },
+ {
+ "id": 47,
+ "entry": "F000:0061",
+ "term": "F000:0081",
+ "pred": [
+ 3,
+ 32
+ ],
+ "succ": [
+ 32,
+ 58
+ ],
+ "asm": [
+ "83C502|add BP,2",
+ "A30C00|mov word ptr DS:[0x000C],AX",
+ "890E0E00|mov word ptr DS:[0x000E],CX",
+ "89161000|mov word ptr DS:[0x0010],DX",
+ "9C|pushf",
+ "BAFFFF|mov DX,0xFFFF",
+ "B8FFFF|mov AX,0xFFFF",
+ "C7061200FFFF|mov word ptr DS:[0x0012],0xFFFF",
+ "C746000400|mov word ptr SS:[BP],4",
+ "F7361200|div word ptr DS:[0x0012]"
+ ]
+ },
+ {
+ "id": 58,
+ "entry": "F000:0085",
+ "term": "F000:009E",
+ "pred": [
+ 32,
+ 47
+ ],
+ "succ": [
+ 32,
+ 68
+ ],
+ "asm": [
+ "83C502|add BP,2",
+ "A31400|mov word ptr DS:[0x0014],AX",
+ "89161600|mov word ptr DS:[0x0016],DX",
+ "9C|pushf",
+ "BAB4FB|mov DX,0xFBB4",
+ "B8DAC3|mov AX,0xC3DA",
+ "B98EAE|mov CX,0xAE8E",
+ "C746000200|mov word ptr SS:[BP],2",
+ "F7F1|div CX"
+ ]
+ },
+ {
+ "id": 68,
+ "entry": "F000:00A0",
+ "term": "F000:010D",
+ "pred": [
+ 32,
+ 58
+ ],
+ "succ": [
+ 32,
+ 104
+ ],
+ "asm": [
+ "83C502|add BP,2",
+ "A31800|mov word ptr DS:[0x0018],AX",
+ "890E1A00|mov word ptr DS:[0x001A],CX",
+ "89161C00|mov word ptr DS:[0x001C],DX",
+ "9C|pushf",
+ "BAC925|mov DX,0x25C9",
+ "B810F1|mov AX,0xF110",
+ "C746000200|mov word ptr SS:[BP],2",
+ "F7F0|div AX",
+ "83C502|add BP,2",
+ "A31E00|mov word ptr DS:[0x001E],AX",
+ "89162000|mov word ptr DS:[0x0020],DX",
+ "9C|pushf",
+ "B81400|mov AX,0x0014",
+ "BB0500|mov BX,5",
+ "C746000200|mov word ptr SS:[BP],2",
+ "F6F3|div BL",
+ "83C502|add BP,2",
+ "A32200|mov word ptr DS:[0x0022],AX",
+ "891E2400|mov word ptr DS:[0x0024],BX",
+ "89162600|mov word ptr DS:[0x0026],DX",
+ "9C|pushf",
+ "BA20A3|mov DX,0xA320",
+ "B8DAC3|mov AX,0xC3DA",
+ "C7062800FF00|mov word ptr DS:[0x0028],0x00FF",
+ "C746000400|mov word ptr SS:[BP],4",
+ "F6362800|div byte ptr DS:[0x0028]",
+ "83C502|add BP,2",
+ "A32A00|mov word ptr DS:[0x002A],AX",
+ "89162C00|mov word ptr DS:[0x002C],DX",
+ "9C|pushf",
+ "B8FFFF|mov AX,0xFFFF",
+ "B601|mov DH,1",
+ "C746000200|mov word ptr SS:[BP],2",
+ "F6F6|div DH"
+ ]
+ },
+ {
+ "id": 104,
+ "entry": "F000:010F",
+ "term": "F000:0128",
+ "pred": [
+ 32,
+ 68
+ ],
+ "succ": [
+ 32,
+ 113
+ ],
+ "asm": [
+ "83C502|add BP,2",
+ "A32E00|mov word ptr DS:[0x002E],AX",
+ "89163000|mov word ptr DS:[0x0030],DX",
+ "9C|pushf",
+ "B8FFFF|mov AX,0xFFFF",
+ "C7063200FFFF|mov word ptr DS:[0x0032],0xFFFF",
+ "C746000400|mov word ptr SS:[BP],4",
+ "F6363300|div byte ptr DS:[0x0033]"
+ ]
+ },
+ {
+ "id": 113,
+ "entry": "F000:012C",
+ "term": "F000:0197",
+ "pred": [
+ 32,
+ 104
+ ],
+ "succ": [
+ 32,
+ 149
+ ],
+ "asm": [
+ "83C502|add BP,2",
+ "A33400|mov word ptr DS:[0x0034],AX",
+ "89163600|mov word ptr DS:[0x0036],DX",
+ "9C|pushf",
+ "B88A00|mov AX,0x008A",
+ "B98EAE|mov CX,0xAE8E",
+ "C746000200|mov word ptr SS:[BP],2",
+ "F6F1|div CL",
+ "83C502|add BP,2",
+ "A33800|mov word ptr DS:[0x0038],AX",
+ "890E3A00|mov word ptr DS:[0x003A],CX",
+ "9C|pushf",
+ "BA6906|mov DX,0x0669",
+ "B8F389|mov AX,0x89F3",
+ "C746000200|mov word ptr SS:[BP],2",
+ "F6F0|div AL",
+ "83C502|add BP,2",
+ "A33C00|mov word ptr DS:[0x003C],AX",
+ "89163E00|mov word ptr DS:[0x003E],DX",
+ "9C|pushf",
+ "BA0000|mov DX,0",
+ "B81400|mov AX,0x0014",
+ "BBFA00|mov BX,0x00FA",
+ "C746000200|mov word ptr SS:[BP],2",
+ "F7FB|idiv BX",
+ "83C502|add BP,2",
+ "A34000|mov word ptr DS:[0x0040],AX",
+ "891E4200|mov word ptr DS:[0x0042],BX",
+ "89164400|mov word ptr DS:[0x0044],DX",
+ "9C|pushf",
+ "BA20A3|mov DX,0xA320",
+ "B8DAC3|mov AX,0xC3DA",
+ "C7064600FFFF|mov word ptr DS:[0x0046],0xFFFF",
+ "C746000400|mov word ptr SS:[BP],4",
+ "F73E4600|idiv word ptr DS:[0x0046]"
+ ]
+ },
+ {
+ "id": 149,
+ "entry": "F000:019B",
+ "term": "F000:020F",
+ "pred": [
+ 32,
+ 113
+ ],
+ "succ": [
+ 32,
+ 187
+ ],
+ "asm": [
+ "83C502|add BP,2",
+ "A34800|mov word ptr DS:[0x0048],AX",
+ "89164A00|mov word ptr DS:[0x004A],DX",
+ "9C|pushf",
+ "BAFFFF|mov DX,0xFFFF",
+ "B8FFFF|mov AX,0xFFFF",
+ "B90100|mov CX,1",
+ "C746000200|mov word ptr SS:[BP],2",
+ "F7F9|idiv CX",
+ "83C502|add BP,2",
+ "A34C00|mov word ptr DS:[0x004C],AX",
+ "890E4E00|mov word ptr DS:[0x004E],CX",
+ "89165000|mov word ptr DS:[0x0050],DX",
+ "9C|pushf",
+ "BAFFFF|mov DX,0xFFFF",
+ "B8FFFF|mov AX,0xFFFF",
+ "C7065200FFFF|mov word ptr DS:[0x0052],0xFFFF",
+ "C746000400|mov word ptr SS:[BP],4",
+ "F73E5200|idiv word ptr DS:[0x0052]",
+ "83C502|add BP,2",
+ "A35400|mov word ptr DS:[0x0054],AX",
+ "89165600|mov word ptr DS:[0x0056],DX",
+ "9C|pushf",
+ "BAB4FB|mov DX,0xFBB4",
+ "B8DAC3|mov AX,0xC3DA",
+ "B98EAE|mov CX,0xAE8E",
+ "C746000200|mov word ptr SS:[BP],2",
+ "F7F9|idiv CX",
+ "83C502|add BP,2",
+ "A35800|mov word ptr DS:[0x0058],AX",
+ "890E5A00|mov word ptr DS:[0x005A],CX",
+ "89165C00|mov word ptr DS:[0x005C],DX",
+ "9C|pushf",
+ "BAC925|mov DX,0x25C9",
+ "B810F1|mov AX,0xF110",
+ "C746000200|mov word ptr SS:[BP],2",
+ "F7F8|idiv AX"
+ ]
+ },
+ {
+ "id": 187,
+ "entry": "F000:0211",
+ "term": "F000:0249",
+ "pred": [
+ 32,
+ 149
+ ],
+ "succ": [
+ 32,
+ 206
+ ],
+ "asm": [
+ "83C502|add BP,2",
+ "A35E00|mov word ptr DS:[0x005E],AX",
+ "89166000|mov word ptr DS:[0x0060],DX",
+ "9C|pushf",
+ "B81400|mov AX,0x0014",
+ "BB0500|mov BX,5",
+ "C746000200|mov word ptr SS:[BP],2",
+ "F6FB|idiv BL",
+ "83C502|add BP,2",
+ "A36200|mov word ptr DS:[0x0062],AX",
+ "891E6400|mov word ptr DS:[0x0064],BX",
+ "89166600|mov word ptr DS:[0x0066],DX",
+ "9C|pushf",
+ "BA20A3|mov DX,0xA320",
+ "B8DAC3|mov AX,0xC3DA",
+ "C7066800FF00|mov word ptr DS:[0x0068],0x00FF",
+ "C746000400|mov word ptr SS:[BP],4",
+ "F63E6800|idiv byte ptr DS:[0x0068]"
+ ]
+ },
+ {
+ "id": 206,
+ "entry": "F000:024D",
+ "term": "F000:02AF",
+ "pred": [
+ 32,
+ 187
+ ],
+ "succ": [
+ 32,
+ 239
+ ],
+ "asm": [
+ "83C502|add BP,2",
+ "A36A00|mov word ptr DS:[0x006A],AX",
+ "89166C00|mov word ptr DS:[0x006C],DX",
+ "9C|pushf",
+ "B8FFFF|mov AX,0xFFFF",
+ "B601|mov DH,1",
+ "C746000200|mov word ptr SS:[BP],2",
+ "F6FE|idiv DH",
+ "83C502|add BP,2",
+ "A36E00|mov word ptr DS:[0x006E],AX",
+ "89167000|mov word ptr DS:[0x0070],DX",
+ "9C|pushf",
+ "B8FFFF|mov AX,0xFFFF",
+ "C7067200FFFF|mov word ptr DS:[0x0072],0xFFFF",
+ "C746000400|mov word ptr SS:[BP],4",
+ "F63E7300|idiv byte ptr DS:[0x0073]",
+ "83C502|add BP,2",
+ "A37400|mov word ptr DS:[0x0074],AX",
+ "89167600|mov word ptr DS:[0x0076],DX",
+ "9C|pushf",
+ "B88A00|mov AX,0x008A",
+ "B98EAE|mov CX,0xAE8E",
+ "C746000200|mov word ptr SS:[BP],2",
+ "F6F9|idiv CL",
+ "83C502|add BP,2",
+ "A37800|mov word ptr DS:[0x0078],AX",
+ "890E7A00|mov word ptr DS:[0x007A],CX",
+ "9C|pushf",
+ "BA6906|mov DX,0x0669",
+ "B8F389|mov AX,0x89F3",
+ "C746000200|mov word ptr SS:[BP],2",
+ "F6F8|idiv AL"
+ ]
+ },
+ {
+ "id": 239,
+ "entry": "F000:02B1",
+ "term": "F000:02C4",
+ "pred": [
+ 32,
+ 206
+ ],
+ "succ": [
+ 32,
+ 247
+ ],
+ "asm": [
+ "83C502|add BP,2",
+ "A37C00|mov word ptr DS:[0x007C],AX",
+ "89167E00|mov word ptr DS:[0x007E],DX",
+ "9C|pushf",
+ "B8FFFF|mov AX,0xFFFF",
+ "C746000200|mov word ptr SS:[BP],2",
+ "D400|aam"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/div2.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/div2.json
new file mode 100644
index 0000000000..cc0fb61d01
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/div2.json
@@ -0,0 +1,51 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:0029",
+ "lastExecutedBlockId": 3,
+ "blocks": [
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:0029",
+ "pred": [
+ 2
+ ],
+ "succ": [],
+ "asm": [
+ "FA|cli",
+ "31C0|xor AX,AX",
+ "8ED8|mov DS,AX",
+ "8ED0|mov SS,AX",
+ "BCFEFF|mov SP,0xFFFE",
+ "BD0001|mov BP,0x0100",
+ "C746F2C3E4|mov word ptr SS:[BP-14],0xE4C3",
+ "B80000|mov AX,0",
+ "BA0080|mov DX,0x8000",
+ "F776F2|div word ptr SS:[BP-14]",
+ "A30000|mov word ptr DS:[0],AX",
+ "89160200|mov word ptr DS:[2],DX",
+ "8B4EF2|mov CX,word ptr SS:[BP-14]",
+ "890E0400|mov word ptr DS:[4],CX",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/divfaultloop.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/divfaultloop.json
new file mode 100644
index 0000000000..c53b9feab0
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/divfaultloop.json
@@ -0,0 +1,131 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:0036",
+ "lastExecutedBlockId": 32,
+ "blocks": [
+ {
+ "id": 32,
+ "entry": "F000:002A",
+ "term": "F000:0036",
+ "pred": [],
+ "succ": [],
+ "asm": [
+ "2E8B1E0001|mov BX,word ptr CS:[0x0100]",
+ "891E0000|mov word ptr DS:[0],BX",
+ "A30200|mov word ptr DS:[2],AX",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:0019",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 27
+ ],
+ "asm": [
+ "B80000|mov AX,0",
+ "8ED0|mov SS,AX",
+ "BC0002|mov SP,0x0200",
+ "C70600003700|mov word ptr DS:[0],0x0037",
+ "8C0E0200|mov word ptr DS:[2],CS",
+ "2EC70600010000|mov word ptr CS:[0x0100],0",
+ "2EC70602010000|mov word ptr CS:[0x0102],0"
+ ]
+ },
+ {
+ "id": 27,
+ "entry": "F000:0020",
+ "term": "F000:0028",
+ "pred": [
+ 3,
+ 23,
+ 27
+ ],
+ "succ": [
+ 14,
+ 27
+ ],
+ "asm": [
+ "B80A00|mov AX,0x000A",
+ "2E8B1E0201|mov BX,word ptr CS:[0x0102]",
+ "F7F3|div BX"
+ ]
+ },
+ {
+ "id": 14,
+ "entry": "F000:0037",
+ "term": "F000:0049",
+ "pred": [
+ 27
+ ],
+ "succ": [
+ 23,
+ 29
+ ],
+ "asm": [
+ "55|push BP",
+ "89E5|mov BP,SP",
+ "53|push BX",
+ "2E8B1E0001|mov BX,word ptr CS:[0x0100]",
+ "43|inc BX",
+ "2E891E0001|mov word ptr CS:[0x0100],BX",
+ "83FB03|cmp BX,3",
+ "7507|jne short 0x0052"
+ ]
+ },
+ {
+ "id": 23,
+ "entry": "F000:0052",
+ "term": "F000:0059",
+ "pred": [
+ 14,
+ 29
+ ],
+ "succ": [
+ 27
+ ],
+ "asm": [
+ "C746022000|mov word ptr SS:[BP\u002B2],0x0020",
+ "5B|pop BX",
+ "5D|pop BP",
+ "CF|iret"
+ ]
+ },
+ {
+ "id": 29,
+ "entry": "F000:004B",
+ "term": "F000:004B",
+ "pred": [
+ 14
+ ],
+ "succ": [
+ 23
+ ],
+ "asm": [
+ "2EC70602010500|mov word ptr CS:[0x0102],5"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/externalint.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/externalint.json
new file mode 100644
index 0000000000..a8c466f1ae
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/externalint.json
@@ -0,0 +1,118 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 2,
+ "entryPointAddresses": [
+ "F000:FFF0",
+ "F000:003F"
+ ],
+ "lastExecutedAddress": "F000:003E",
+ "lastExecutedBlockId": 33,
+ "blocks": [
+ {
+ "id": 33,
+ "entry": "F000:0033",
+ "term": "F000:003E",
+ "pred": [
+ 24
+ ],
+ "succ": [],
+ "asm": [
+ "81FA0000|cmp DX,0",
+ "0F95C0|setne AL",
+ "0FB6C0|movsz AX,AL",
+ "50|push AX",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 27,
+ "entry": "F000:003F",
+ "term": "F000:0048",
+ "pred": [],
+ "succ": [],
+ "asm": [
+ "50|push AX",
+ "BA0100|mov DX,1",
+ "B020|mov AL,0x20",
+ "E620|out 0x20,AL",
+ "58|pop AX",
+ "CF|iret"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 24,
+ "entry": "F000:002C",
+ "term": "F000:0031",
+ "pred": [
+ 19,
+ 24
+ ],
+ "succ": [
+ 24,
+ 33
+ ],
+ "asm": [
+ "40|inc AX",
+ "89C3|mov BX,AX",
+ "D1EB|shr BX,1",
+ "E2F9|loop 0x002C"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:0025",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 19
+ ],
+ "asm": [
+ "B80000|mov AX,0",
+ "8ED0|mov SS,AX",
+ "BC0200|mov SP,2",
+ "BA0000|mov DX,0",
+ "C70620003F00|mov word ptr DS:[0x0020],0x003F",
+ "8C0E2200|mov word ptr DS:[0x0022],CS",
+ "B036|mov AL,0x36",
+ "E643|out 0x43,AL",
+ "B051|mov AL,0x51",
+ "E640|out 0x40,AL",
+ "B022|mov AL,0x22",
+ "E640|out 0x40,AL",
+ "B000|mov AL,0",
+ "E621|out 0x21,AL",
+ "FB|sti"
+ ]
+ },
+ {
+ "id": 19,
+ "entry": "F000:0026",
+ "term": "F000:0026",
+ "pred": [
+ 3
+ ],
+ "succ": [
+ 24
+ ],
+ "asm": [
+ "66B9FFFFFF00|mov ECX,0x00FFFFFF"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/interrupt.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/interrupt.json
new file mode 100644
index 0000000000..4388bb3731
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/interrupt.json
@@ -0,0 +1,232 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:4026",
+ "lastExecutedBlockId": 53,
+ "blocks": [
+ {
+ "id": 53,
+ "entry": "F000:4013",
+ "term": "F000:4026",
+ "pred": [
+ 50
+ ],
+ "succ": [],
+ "asm": [
+ "A30800|mov word ptr DS:[8],AX",
+ "891E0A00|mov word ptr DS:[0x000A],BX",
+ "890E0C00|mov word ptr DS:[0x000C],CX",
+ "89160E00|mov word ptr DS:[0x000E],DX",
+ "89261000|mov word ptr DS:[0x0010],SP",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 50,
+ "entry": "F000:400C",
+ "term": "F000:4012",
+ "pred": [
+ 48
+ ],
+ "succ": [
+ 53
+ ],
+ "asm": [
+ "C70610000050|mov word ptr DS:[0x0010],0x5000",
+ "CE|into"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:0019",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 12
+ ],
+ "asm": [
+ "BA0000|mov DX,0",
+ "8EDA|mov DS,DX",
+ "BC0010|mov SP,0x1000",
+ "8CD4|mov SP,SS",
+ "C7063400E0EB|mov word ptr DS:[0x0034],0xEBE0",
+ "C706360042E3|mov word ptr DS:[0x0036],0xE342",
+ "B8FF0E|mov AX,0x0EFF",
+ "50|push AX"
+ ]
+ },
+ {
+ "id": 48,
+ "entry": "F000:400B",
+ "term": "F000:400B",
+ "pred": [
+ 44
+ ],
+ "succ": [
+ 50
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 12,
+ "entry": "F000:001A",
+ "term": "F000:001A",
+ "pred": [
+ 3
+ ],
+ "succ": [
+ 14
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 44,
+ "entry": "F000:4002",
+ "term": "F000:400A",
+ "pred": [
+ 33,
+ 38
+ ],
+ "succ": [
+ 48
+ ],
+ "asm": [
+ "C606060006|mov byte ptr DS:[6],6",
+ "BAFF04|mov DX,0x04FF",
+ "52|push DX"
+ ]
+ },
+ {
+ "id": 14,
+ "entry": "F000:001B",
+ "term": "F000:0020",
+ "pred": [
+ 12
+ ],
+ "succ": [
+ 17,
+ 23
+ ],
+ "asm": [
+ "C606000000|mov byte ptr DS:[0],0",
+ "CD0D|int 0x0D"
+ ]
+ },
+ {
+ "id": 38,
+ "entry": "F000:3001",
+ "term": "F000:300B",
+ "pred": [
+ 33
+ ],
+ "succ": [
+ 44
+ ],
+ "asm": [
+ "C606050005|mov byte ptr DS:[5],5",
+ "59|pop CX",
+ "B90240|mov CX,0x4002",
+ "51|push CX",
+ "CF|iret"
+ ]
+ },
+ {
+ "id": 33,
+ "entry": "F000:0CEC",
+ "term": "F000:0CFD",
+ "pred": [
+ 17,
+ 26
+ ],
+ "succ": [
+ 38,
+ 44
+ ],
+ "asm": [
+ "C606040004|mov byte ptr DS:[4],4",
+ "C70610000130|mov word ptr DS:[0x0010],0x3001",
+ "C706120000F0|mov word ptr DS:[0x0012],0xF000",
+ "CE|into"
+ ]
+ },
+ {
+ "id": 17,
+ "entry": "E342:EBE0",
+ "term": "E342:EBE8",
+ "pred": [
+ 14,
+ 26
+ ],
+ "succ": [
+ 23,
+ 33
+ ],
+ "asm": [
+ "C606010001|mov byte ptr DS:[1],1",
+ "9C|pushf",
+ "58|pop AX",
+ "F8|clc",
+ "CF|iret"
+ ]
+ },
+ {
+ "id": 23,
+ "entry": "F000:0022",
+ "term": "F000:0027",
+ "pred": [
+ 14,
+ 17
+ ],
+ "succ": [
+ 26
+ ],
+ "asm": [
+ "C606020002|mov byte ptr DS:[2],2",
+ "FFE0|jmp near AX"
+ ]
+ },
+ {
+ "id": 26,
+ "entry": "F000:0CD7",
+ "term": "F000:0CEA",
+ "pred": [
+ 23
+ ],
+ "succ": [
+ 17,
+ 33
+ ],
+ "asm": [
+ "C606030003|mov byte ptr DS:[3],3",
+ "9C|pushf",
+ "5B|pop BX",
+ "C7060C00E0EB|mov word ptr DS:[0x000C],0xEBE0",
+ "C7060E0042E3|mov word ptr DS:[0x000E],0xE342",
+ "CD03|int 3"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/jmpmov.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/jmpmov.json
new file mode 100644
index 0000000000..767fc4aae3
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/jmpmov.json
@@ -0,0 +1,140 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:400C",
+ "lastExecutedBlockId": 42,
+ "blocks": [
+ {
+ "id": 42,
+ "entry": "F000:4001",
+ "term": "F000:400C",
+ "pred": [
+ 26
+ ],
+ "succ": [],
+ "asm": [
+ "8B40FD|mov AX,word ptr DS:[BX\u002BSI-3]",
+ "BA0000|mov DX,0",
+ "8EDA|mov DS,DX",
+ "A30000|mov word ptr DS:[0],AX",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 26,
+ "entry": "E342:FBE1",
+ "term": "E342:FC06",
+ "pred": [
+ 12
+ ],
+ "succ": [
+ 42
+ ],
+ "asm": [
+ "BA00F1|mov DX,0xF100",
+ "B83625|mov AX,0x2536",
+ "EF|out DX,AX",
+ "C7C00140|mov AX,0x4001",
+ "BB0125|mov BX,0x2501",
+ "8907|mov word ptr DS:[BX],AX",
+ "BF0200|mov DI,2",
+ "C60100|mov byte ptr DS:[BX\u002BDI],0",
+ "B504|mov CH,4",
+ "88E9|mov CL,CH",
+ "B500|mov CH,0",
+ "89CE|mov SI,CX",
+ "C640FFF0|mov byte ptr DS:[BX\u002BSI-1],0xF0",
+ "BE0300|mov SI,3",
+ "FF6AE8|jmp far dword ptr SS:[BP\u002BSI-24]"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:0000",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 5
+ ],
+ "asm": [
+ "EB0C|jmp short 0x000E"
+ ]
+ },
+ {
+ "id": 12,
+ "entry": "E342:EBE0",
+ "term": "E342:EC05",
+ "pred": [
+ 10
+ ],
+ "succ": [
+ 26
+ ],
+ "asm": [
+ "BB0010|mov BX,0x1000",
+ "8EDB|mov DS,BX",
+ "B4FB|mov AH,0xFB",
+ "B0E1|mov AL,0xE1",
+ "A30125|mov word ptr DS:[0x2501],AX",
+ "C70600260110|mov word ptr DS:[0x2600],0x1001",
+ "8E160026|mov SS,word ptr DS:[0x2600]",
+ "8C160126|mov word ptr DS:[0x2601],SS",
+ "8A160126|mov DL,byte ptr DS:[0x2601]",
+ "B600|mov DH,0",
+ "89D7|mov DI,DX",
+ "BD0625|mov BP,0x2506",
+ "FF63EA|jmp near word ptr SS:[BP\u002BDI-22]"
+ ]
+ },
+ {
+ "id": 5,
+ "entry": "F000:000E",
+ "term": "F000:0016",
+ "pred": [
+ 3
+ ],
+ "succ": [
+ 10
+ ],
+ "asm": [
+ "BB00F0|mov BX,0xF000",
+ "8EDB|mov DS,BX",
+ "A1F2FF|mov AX,word ptr DS:[0xFFF2]",
+ "FFE0|jmp near AX"
+ ]
+ },
+ {
+ "id": 10,
+ "entry": "F000:1290",
+ "term": "F000:1290",
+ "pred": [
+ 5
+ ],
+ "succ": [
+ 12
+ ],
+ "asm": [
+ "EAE0EB42E3|jmp far E342:EBE0"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/jump1.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/jump1.json
new file mode 100644
index 0000000000..e5ba7d6d8e
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/jump1.json
@@ -0,0 +1,1069 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:00CE",
+ "lastExecutedBlockId": 170,
+ "blocks": [
+ {
+ "id": 170,
+ "entry": "F000:00C8",
+ "term": "F000:00CE",
+ "pred": [
+ 168
+ ],
+ "succ": [],
+ "asm": [
+ "C70600003412|mov word ptr DS:[0],0x1234",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 168,
+ "entry": "F000:00C3",
+ "term": "F000:00C3",
+ "pred": [
+ 166
+ ],
+ "succ": [
+ 170
+ ],
+ "asm": [
+ "7803|js short 0x00C8"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:000C",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 10
+ ],
+ "asm": [
+ "B90000|mov CX,0",
+ "BB4000|mov BX,0x0040",
+ "BC0010|mov SP,0x1000",
+ "8ED4|mov SS,SP",
+ "53|push BX",
+ "EB02|jmp short 0x0010"
+ ]
+ },
+ {
+ "id": 166,
+ "entry": "F000:00C2",
+ "term": "F000:00C2",
+ "pred": [
+ 163
+ ],
+ "succ": [
+ 168
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 10,
+ "entry": "F000:0010",
+ "term": "F000:0011",
+ "pred": [
+ 3
+ ],
+ "succ": [
+ 13
+ ],
+ "asm": [
+ "F9|stc",
+ "77FB|ja short 0x000E"
+ ]
+ },
+ {
+ "id": 163,
+ "entry": "F000:00BE",
+ "term": "F000:00C1",
+ "pred": [
+ 161
+ ],
+ "succ": [
+ 166
+ ],
+ "asm": [
+ "BA8408|mov DX,0x0884",
+ "52|push DX"
+ ]
+ },
+ {
+ "id": 13,
+ "entry": "F000:0013",
+ "term": "F000:0014",
+ "pred": [
+ 10
+ ],
+ "succ": [
+ 16
+ ],
+ "asm": [
+ "F8|clc",
+ "7702|ja short 0x0018"
+ ]
+ },
+ {
+ "id": 161,
+ "entry": "F000:00BC",
+ "term": "F000:00BC",
+ "pred": [
+ 159
+ ],
+ "succ": [
+ 163
+ ],
+ "asm": [
+ "78FB|js short 0x00B9"
+ ]
+ },
+ {
+ "id": 16,
+ "entry": "F000:0018",
+ "term": "F000:0019",
+ "pred": [
+ 13
+ ],
+ "succ": [
+ 19
+ ],
+ "asm": [
+ "F9|stc",
+ "73FB|jae short 0x0016"
+ ]
+ },
+ {
+ "id": 159,
+ "entry": "F000:00B7",
+ "term": "F000:00B7",
+ "pred": [
+ 157
+ ],
+ "succ": [
+ 161
+ ],
+ "asm": [
+ "7A03|jp short 0x00BC"
+ ]
+ },
+ {
+ "id": 19,
+ "entry": "F000:001B",
+ "term": "F000:001C",
+ "pred": [
+ 16
+ ],
+ "succ": [
+ 22
+ ],
+ "asm": [
+ "F8|clc",
+ "7302|jae short 0x0020"
+ ]
+ },
+ {
+ "id": 157,
+ "entry": "F000:00B6",
+ "term": "F000:00B6",
+ "pred": [
+ 154
+ ],
+ "succ": [
+ 159
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 22,
+ "entry": "F000:0020",
+ "term": "F000:0020",
+ "pred": [
+ 19
+ ],
+ "succ": [
+ 24
+ ],
+ "asm": [
+ "72FC|jb short 0x001E"
+ ]
+ },
+ {
+ "id": 154,
+ "entry": "F000:00B2",
+ "term": "F000:00B5",
+ "pred": [
+ 152
+ ],
+ "succ": [
+ 157
+ ],
+ "asm": [
+ "BA0408|mov DX,0x0804",
+ "52|push DX"
+ ]
+ },
+ {
+ "id": 24,
+ "entry": "F000:0022",
+ "term": "F000:0023",
+ "pred": [
+ 22
+ ],
+ "succ": [
+ 27
+ ],
+ "asm": [
+ "F9|stc",
+ "7202|jb short 0x0027"
+ ]
+ },
+ {
+ "id": 152,
+ "entry": "F000:00B0",
+ "term": "F000:00B0",
+ "pred": [
+ 150
+ ],
+ "succ": [
+ 154
+ ],
+ "asm": [
+ "7AFB|jp short 0x00AD"
+ ]
+ },
+ {
+ "id": 27,
+ "entry": "F000:0027",
+ "term": "F000:0028",
+ "pred": [
+ 24
+ ],
+ "succ": [
+ 30
+ ],
+ "asm": [
+ "F8|clc",
+ "76FB|jbe short 0x0025"
+ ]
+ },
+ {
+ "id": 150,
+ "entry": "F000:00AB",
+ "term": "F000:00AB",
+ "pred": [
+ 148
+ ],
+ "succ": [
+ 152
+ ],
+ "asm": [
+ "7003|jo short 0x00B0"
+ ]
+ },
+ {
+ "id": 30,
+ "entry": "F000:002A",
+ "term": "F000:002A",
+ "pred": [
+ 27
+ ],
+ "succ": [
+ 32
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 148,
+ "entry": "F000:00AA",
+ "term": "F000:00AA",
+ "pred": [
+ 145
+ ],
+ "succ": [
+ 150
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 32,
+ "entry": "F000:002B",
+ "term": "F000:002B",
+ "pred": [
+ 30
+ ],
+ "succ": [
+ 34
+ ],
+ "asm": [
+ "7602|jbe short 0x002F"
+ ]
+ },
+ {
+ "id": 145,
+ "entry": "F000:00A6",
+ "term": "F000:00A9",
+ "pred": [
+ 143
+ ],
+ "succ": [
+ 148
+ ],
+ "asm": [
+ "BA0008|mov DX,0x0800",
+ "52|push DX"
+ ]
+ },
+ {
+ "id": 34,
+ "entry": "F000:002F",
+ "term": "F000:002F",
+ "pred": [
+ 32
+ ],
+ "succ": [
+ 36
+ ],
+ "asm": [
+ "51|push CX"
+ ]
+ },
+ {
+ "id": 143,
+ "entry": "F000:00A4",
+ "term": "F000:00A4",
+ "pred": [
+ 141
+ ],
+ "succ": [
+ 145
+ ],
+ "asm": [
+ "70FB|jo short 0x00A1"
+ ]
+ },
+ {
+ "id": 36,
+ "entry": "F000:0030",
+ "term": "F000:0030",
+ "pred": [
+ 34
+ ],
+ "succ": [
+ 38
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 141,
+ "entry": "F000:009F",
+ "term": "F000:009F",
+ "pred": [
+ 139
+ ],
+ "succ": [
+ 143
+ ],
+ "asm": [
+ "7903|jns short 0x00A4"
+ ]
+ },
+ {
+ "id": 38,
+ "entry": "F000:0031",
+ "term": "F000:0031",
+ "pred": [
+ 36
+ ],
+ "succ": [
+ 40
+ ],
+ "asm": [
+ "74FA|je short 0x002D"
+ ]
+ },
+ {
+ "id": 139,
+ "entry": "F000:009E",
+ "term": "F000:009E",
+ "pred": [
+ 137
+ ],
+ "succ": [
+ 141
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 40,
+ "entry": "F000:0033",
+ "term": "F000:0033",
+ "pred": [
+ 38
+ ],
+ "succ": [
+ 42
+ ],
+ "asm": [
+ "53|push BX"
+ ]
+ },
+ {
+ "id": 137,
+ "entry": "F000:009D",
+ "term": "F000:009D",
+ "pred": [
+ 135
+ ],
+ "succ": [
+ 139
+ ],
+ "asm": [
+ "51|push CX"
+ ]
+ },
+ {
+ "id": 42,
+ "entry": "F000:0034",
+ "term": "F000:0034",
+ "pred": [
+ 40
+ ],
+ "succ": [
+ 44
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 135,
+ "entry": "F000:009B",
+ "term": "F000:009B",
+ "pred": [
+ 133
+ ],
+ "succ": [
+ 137
+ ],
+ "asm": [
+ "79F6|jns short 0x0093"
+ ]
+ },
+ {
+ "id": 44,
+ "entry": "F000:0035",
+ "term": "F000:0035",
+ "pred": [
+ 42
+ ],
+ "succ": [
+ 46
+ ],
+ "asm": [
+ "7402|je short 0x0039"
+ ]
+ },
+ {
+ "id": 133,
+ "entry": "F000:009A",
+ "term": "F000:009A",
+ "pred": [
+ 130
+ ],
+ "succ": [
+ 135
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 46,
+ "entry": "F000:0039",
+ "term": "F000:003C",
+ "pred": [
+ 44
+ ],
+ "succ": [
+ 49
+ ],
+ "asm": [
+ "BAC008|mov DX,0x08C0",
+ "52|push DX"
+ ]
+ },
+ {
+ "id": 130,
+ "entry": "F000:0096",
+ "term": "F000:0099",
+ "pred": [
+ 128
+ ],
+ "succ": [
+ 133
+ ],
+ "asm": [
+ "BAFF0E|mov DX,0x0EFF",
+ "52|push DX"
+ ]
+ },
+ {
+ "id": 49,
+ "entry": "F000:003D",
+ "term": "F000:003D",
+ "pred": [
+ 46
+ ],
+ "succ": [
+ 51
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 128,
+ "entry": "F000:0091",
+ "term": "F000:0091",
+ "pred": [
+ 126
+ ],
+ "succ": [
+ 130
+ ],
+ "asm": [
+ "7B03|jnp short 0x0096"
+ ]
+ },
+ {
+ "id": 51,
+ "entry": "F000:003E",
+ "term": "F000:003E",
+ "pred": [
+ 49
+ ],
+ "succ": [
+ 53
+ ],
+ "asm": [
+ "7FF7|jg short 0x0037"
+ ]
+ },
+ {
+ "id": 126,
+ "entry": "F000:0090",
+ "term": "F000:0090",
+ "pred": [
+ 124
+ ],
+ "succ": [
+ 128
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 53,
+ "entry": "F000:0040",
+ "term": "F000:0043",
+ "pred": [
+ 51
+ ],
+ "succ": [
+ 56
+ ],
+ "asm": [
+ "BA8008|mov DX,0x0880",
+ "52|push DX"
+ ]
+ },
+ {
+ "id": 124,
+ "entry": "F000:008F",
+ "term": "F000:008F",
+ "pred": [
+ 122
+ ],
+ "succ": [
+ 126
+ ],
+ "asm": [
+ "51|push CX"
+ ]
+ },
+ {
+ "id": 56,
+ "entry": "F000:0044",
+ "term": "F000:0044",
+ "pred": [
+ 53
+ ],
+ "succ": [
+ 58
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 122,
+ "entry": "F000:008D",
+ "term": "F000:008D",
+ "pred": [
+ 120
+ ],
+ "succ": [
+ 124
+ ],
+ "asm": [
+ "7BF6|jnp short 0x0085"
+ ]
+ },
+ {
+ "id": 58,
+ "entry": "F000:0045",
+ "term": "F000:0045",
+ "pred": [
+ 56
+ ],
+ "succ": [
+ 60
+ ],
+ "asm": [
+ "7F02|jg short 0x0049"
+ ]
+ },
+ {
+ "id": 120,
+ "entry": "F000:008C",
+ "term": "F000:008C",
+ "pred": [
+ 117
+ ],
+ "succ": [
+ 122
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 60,
+ "entry": "F000:0049",
+ "term": "F000:004C",
+ "pred": [
+ 58
+ ],
+ "succ": [
+ 63
+ ],
+ "asm": [
+ "BA8000|mov DX,0x0080",
+ "52|push DX"
+ ]
+ },
+ {
+ "id": 117,
+ "entry": "F000:0088",
+ "term": "F000:008B",
+ "pred": [
+ 115
+ ],
+ "succ": [
+ 120
+ ],
+ "asm": [
+ "BA0400|mov DX,4",
+ "52|push DX"
+ ]
+ },
+ {
+ "id": 63,
+ "entry": "F000:004D",
+ "term": "F000:004D",
+ "pred": [
+ 60
+ ],
+ "succ": [
+ 65
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 115,
+ "entry": "F000:0083",
+ "term": "F000:0083",
+ "pred": [
+ 113
+ ],
+ "succ": [
+ 117
+ ],
+ "asm": [
+ "7103|jno short 0x0088"
+ ]
+ },
+ {
+ "id": 65,
+ "entry": "F000:004E",
+ "term": "F000:004E",
+ "pred": [
+ 63
+ ],
+ "succ": [
+ 67
+ ],
+ "asm": [
+ "7DF7|jge short 0x0047"
+ ]
+ },
+ {
+ "id": 113,
+ "entry": "F000:0082",
+ "term": "F000:0082",
+ "pred": [
+ 110
+ ],
+ "succ": [
+ 115
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 67,
+ "entry": "F000:0050",
+ "term": "F000:0050",
+ "pred": [
+ 65
+ ],
+ "succ": [
+ 69
+ ],
+ "asm": [
+ "51|push CX"
+ ]
+ },
+ {
+ "id": 110,
+ "entry": "F000:007E",
+ "term": "F000:0081",
+ "pred": [
+ 108
+ ],
+ "succ": [
+ 113
+ ],
+ "asm": [
+ "BAFF06|mov DX,0x06FF",
+ "52|push DX"
+ ]
+ },
+ {
+ "id": 69,
+ "entry": "F000:0051",
+ "term": "F000:0051",
+ "pred": [
+ 67
+ ],
+ "succ": [
+ 71
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 108,
+ "entry": "F000:007C",
+ "term": "F000:007C",
+ "pred": [
+ 106
+ ],
+ "succ": [
+ 110
+ ],
+ "asm": [
+ "71F6|jno short 0x0074"
+ ]
+ },
+ {
+ "id": 71,
+ "entry": "F000:0052",
+ "term": "F000:0052",
+ "pred": [
+ 69
+ ],
+ "succ": [
+ 73
+ ],
+ "asm": [
+ "7D02|jge short 0x0056"
+ ]
+ },
+ {
+ "id": 106,
+ "entry": "F000:007B",
+ "term": "F000:007B",
+ "pred": [
+ 103
+ ],
+ "succ": [
+ 108
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 73,
+ "entry": "F000:0056",
+ "term": "F000:0056",
+ "pred": [
+ 71
+ ],
+ "succ": [
+ 75
+ ],
+ "asm": [
+ "7CFC|jl short 0x0054"
+ ]
+ },
+ {
+ "id": 103,
+ "entry": "F000:0077",
+ "term": "F000:007A",
+ "pred": [
+ 101
+ ],
+ "succ": [
+ 106
+ ],
+ "asm": [
+ "BA0008|mov DX,0x0800",
+ "52|push DX"
+ ]
+ },
+ {
+ "id": 75,
+ "entry": "F000:0058",
+ "term": "F000:005B",
+ "pred": [
+ 73
+ ],
+ "succ": [
+ 78
+ ],
+ "asm": [
+ "BA0008|mov DX,0x0800",
+ "52|push DX"
+ ]
+ },
+ {
+ "id": 101,
+ "entry": "F000:0072",
+ "term": "F000:0072",
+ "pred": [
+ 99
+ ],
+ "succ": [
+ 103
+ ],
+ "asm": [
+ "7503|jne short 0x0077"
+ ]
+ },
+ {
+ "id": 78,
+ "entry": "F000:005C",
+ "term": "F000:005C",
+ "pred": [
+ 75
+ ],
+ "succ": [
+ 80
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 99,
+ "entry": "F000:0071",
+ "term": "F000:0071",
+ "pred": [
+ 96
+ ],
+ "succ": [
+ 101
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 80,
+ "entry": "F000:005D",
+ "term": "F000:005D",
+ "pred": [
+ 78
+ ],
+ "succ": [
+ 82
+ ],
+ "asm": [
+ "7C02|jl short 0x0061"
+ ]
+ },
+ {
+ "id": 96,
+ "entry": "F000:006D",
+ "term": "F000:0070",
+ "pred": [
+ 94
+ ],
+ "succ": [
+ 99
+ ],
+ "asm": [
+ "BABF0C|mov DX,0x0CBF",
+ "52|push DX"
+ ]
+ },
+ {
+ "id": 82,
+ "entry": "F000:0061",
+ "term": "F000:0061",
+ "pred": [
+ 80
+ ],
+ "succ": [
+ 84
+ ],
+ "asm": [
+ "51|push CX"
+ ]
+ },
+ {
+ "id": 94,
+ "entry": "F000:006B",
+ "term": "F000:006B",
+ "pred": [
+ 92
+ ],
+ "succ": [
+ 96
+ ],
+ "asm": [
+ "75FC|jne short 0x0069"
+ ]
+ },
+ {
+ "id": 84,
+ "entry": "F000:0062",
+ "term": "F000:0062",
+ "pred": [
+ 82
+ ],
+ "succ": [
+ 86
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 92,
+ "entry": "F000:0067",
+ "term": "F000:0067",
+ "pred": [
+ 90
+ ],
+ "succ": [
+ 94
+ ],
+ "asm": [
+ "7E02|jle short 0x006B"
+ ]
+ },
+ {
+ "id": 86,
+ "entry": "F000:0063",
+ "term": "F000:0063",
+ "pred": [
+ 84
+ ],
+ "succ": [
+ 88
+ ],
+ "asm": [
+ "7EFA|jle short 0x005F"
+ ]
+ },
+ {
+ "id": 90,
+ "entry": "F000:0066",
+ "term": "F000:0066",
+ "pred": [
+ 88
+ ],
+ "succ": [
+ 92
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 88,
+ "entry": "F000:0065",
+ "term": "F000:0065",
+ "pred": [
+ 86
+ ],
+ "succ": [
+ 90
+ ],
+ "asm": [
+ "53|push BX"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/jump2.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/jump2.json
new file mode 100644
index 0000000000..228cd4e856
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/jump2.json
@@ -0,0 +1,359 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:FFFE",
+ "lastExecutedBlockId": 59,
+ "blocks": [
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF5",
+ "pred": [],
+ "succ": [
+ 5,
+ 59
+ ],
+ "asm": [
+ "BC0010|mov SP,0x1000",
+ "8ED4|mov SS,SP",
+ "E80800|call near 0"
+ ]
+ },
+ {
+ "id": 59,
+ "entry": "F000:FFF8",
+ "term": "F000:FFFE",
+ "pred": [
+ 2,
+ 57
+ ],
+ "succ": [],
+ "asm": [
+ "8ED9|mov DS,CX",
+ "891E0000|mov word ptr DS:[0],BX",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 5,
+ "entry": "F000:0000",
+ "term": "F000:0008",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 11
+ ],
+ "asm": [
+ "BB00F0|mov BX,0xF000",
+ "8EDB|mov DS,BX",
+ "B89012|mov AX,0x1290",
+ "B90500|mov CX,5"
+ ]
+ },
+ {
+ "id": 57,
+ "entry": "F000:0010",
+ "term": "F000:0010",
+ "pred": [
+ 13,
+ 55
+ ],
+ "succ": [
+ 59
+ ],
+ "asm": [
+ "C3|ret near"
+ ]
+ },
+ {
+ "id": 11,
+ "entry": "F000:000B",
+ "term": "F000:000C",
+ "pred": [
+ 5,
+ 11
+ ],
+ "succ": [
+ 11,
+ 13
+ ],
+ "asm": [
+ "51|push CX",
+ "E2FD|loop 0x000B"
+ ]
+ },
+ {
+ "id": 55,
+ "entry": "F000:12AA",
+ "term": "F000:12AA",
+ "pred": [
+ 52
+ ],
+ "succ": [
+ 57
+ ],
+ "asm": [
+ "C20A00|ret near 0x000A"
+ ]
+ },
+ {
+ "id": 13,
+ "entry": "F000:000E",
+ "term": "F000:000E",
+ "pred": [
+ 11
+ ],
+ "succ": [
+ 15,
+ 57
+ ],
+ "asm": [
+ "FFD0|call near AX"
+ ]
+ },
+ {
+ "id": 52,
+ "entry": "F000:12A4",
+ "term": "F000:12A7",
+ "pred": [
+ 50
+ ],
+ "succ": [
+ 55
+ ],
+ "asm": [
+ "B90000|mov CX,0",
+ "E301|jcxz short 0x12AA"
+ ]
+ },
+ {
+ "id": 15,
+ "entry": "F000:1290",
+ "term": "F000:1293",
+ "pred": [
+ 13
+ ],
+ "succ": [
+ 18
+ ],
+ "asm": [
+ "B9FFFF|mov CX,0xFFFF",
+ "E1FB|loope 0x1290"
+ ]
+ },
+ {
+ "id": 50,
+ "entry": "F000:12A2",
+ "term": "F000:12A2",
+ "pred": [
+ 25,
+ 48
+ ],
+ "succ": [
+ 52
+ ],
+ "asm": [
+ "E3F9|jcxz short 0x129D"
+ ]
+ },
+ {
+ "id": 18,
+ "entry": "F000:1295",
+ "term": "F000:1298",
+ "pred": [
+ 15
+ ],
+ "succ": [
+ 21
+ ],
+ "asm": [
+ "BA4000|mov DX,0x0040",
+ "52|push DX"
+ ]
+ },
+ {
+ "id": 48,
+ "entry": "E342:EBF1",
+ "term": "E342:EBF1",
+ "pred": [
+ 46
+ ],
+ "succ": [
+ 50
+ ],
+ "asm": [
+ "CB|ret far"
+ ]
+ },
+ {
+ "id": 25,
+ "entry": "F000:129D",
+ "term": "F000:129D",
+ "pred": [
+ 23
+ ],
+ "succ": [
+ 27,
+ 50
+ ],
+ "asm": [
+ "9AE0EB42E3|call far E342:EBE0"
+ ]
+ },
+ {
+ "id": 21,
+ "entry": "F000:1299",
+ "term": "F000:1299",
+ "pred": [
+ 18
+ ],
+ "succ": [
+ 23
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 46,
+ "entry": "E342:EBEE",
+ "term": "E342:EBEE",
+ "pred": [
+ 43
+ ],
+ "succ": [
+ 48
+ ],
+ "asm": [
+ "E001|loopne 0xEBF1"
+ ]
+ },
+ {
+ "id": 27,
+ "entry": "E342:EBE0",
+ "term": "E342:EBE0",
+ "pred": [
+ 25
+ ],
+ "succ": [
+ 29,
+ 38
+ ],
+ "asm": [
+ "FF160030|call near word ptr DS:[0x3000]"
+ ]
+ },
+ {
+ "id": 23,
+ "entry": "F000:129A",
+ "term": "F000:129A",
+ "pred": [
+ 21
+ ],
+ "succ": [
+ 25
+ ],
+ "asm": [
+ "E101|loope 0x129D"
+ ]
+ },
+ {
+ "id": 43,
+ "entry": "E342:EBE9",
+ "term": "E342:EBEC",
+ "pred": [
+ 41
+ ],
+ "succ": [
+ 46
+ ],
+ "asm": [
+ "B90100|mov CX,1",
+ "E0FB|loopne 0xEBE9"
+ ]
+ },
+ {
+ "id": 29,
+ "entry": "E342:FDE0",
+ "term": "E342:FDE7",
+ "pred": [
+ 27
+ ],
+ "succ": [
+ 34,
+ 36
+ ],
+ "asm": [
+ "BBF02F|mov BX,0x2FF0",
+ "BE1000|mov SI,0x0010",
+ "52|push DX",
+ "FF5802|call far dword ptr DS:[BX\u002BSI\u002B2]"
+ ]
+ },
+ {
+ "id": 38,
+ "entry": "E342:EBE4",
+ "term": "E342:EBE7",
+ "pred": [
+ 27,
+ 36
+ ],
+ "succ": [
+ 41
+ ],
+ "asm": [
+ "BA0000|mov DX,0",
+ "52|push DX"
+ ]
+ },
+ {
+ "id": 41,
+ "entry": "E342:EBE8",
+ "term": "E342:EBE8",
+ "pred": [
+ 38
+ ],
+ "succ": [
+ 43
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 34,
+ "entry": "F000:4000",
+ "term": "F000:4000",
+ "pred": [
+ 29
+ ],
+ "succ": [
+ 36
+ ],
+ "asm": [
+ "CA0200|ret far 2"
+ ]
+ },
+ {
+ "id": 36,
+ "entry": "E342:FDEA",
+ "term": "E342:FDEA",
+ "pred": [
+ 29,
+ 34
+ ],
+ "succ": [
+ 38
+ ],
+ "asm": [
+ "C3|ret near"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/linearsamesegmenteddifferent.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/linearsamesegmenteddifferent.json
new file mode 100644
index 0000000000..b414437269
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/linearsamesegmenteddifferent.json
@@ -0,0 +1,104 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "FFEF:0116",
+ "lastExecutedBlockId": 20,
+ "blocks": [
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB16|jmp short 8"
+ ]
+ },
+ {
+ "id": 20,
+ "entry": "FFEF:0110",
+ "term": "FFEF:0116",
+ "pred": [
+ 18
+ ],
+ "succ": [],
+ "asm": [
+ "B80200|mov AX,2",
+ "A30000|mov word ptr DS:[0],AX",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0008",
+ "term": "F000:0019",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 11
+ ],
+ "asm": [
+ "BC0010|mov SP,0x1000",
+ "BB0010|mov BX,0x1000",
+ "8ED3|mov SS,BX",
+ "B80000|mov AX,0",
+ "6800F0|push 0xF000",
+ "68FDFE|push 0xFEFD",
+ "CB|ret far"
+ ]
+ },
+ {
+ "id": 18,
+ "entry": "FFEF:000D",
+ "term": "FFEF:000D",
+ "pred": [
+ 13
+ ],
+ "succ": [
+ 20
+ ],
+ "asm": [
+ "E90001|jmp near 0x0110"
+ ]
+ },
+ {
+ "id": 11,
+ "entry": "F000:FEFD",
+ "term": "F000:FEFD",
+ "pred": [
+ 3
+ ],
+ "succ": [
+ 13
+ ],
+ "asm": [
+ "E90001|jmp near 0"
+ ]
+ },
+ {
+ "id": 13,
+ "entry": "F000:0000",
+ "term": "F000:0007",
+ "pred": [
+ 11
+ ],
+ "succ": [
+ 18
+ ],
+ "asm": [
+ "B80100|mov AX,1",
+ "6AEF|push -17",
+ "6A0D|push 0x0D",
+ "CB|ret far"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/lockprefix.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/lockprefix.json
new file mode 100644
index 0000000000..2fc5b98177
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/lockprefix.json
@@ -0,0 +1,125 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:0057",
+ "lastExecutedBlockId": 36,
+ "blocks": [
+ {
+ "id": 36,
+ "entry": "F000:0057",
+ "term": "F000:0057",
+ "pred": [
+ 20,
+ 32
+ ],
+ "succ": [],
+ "asm": [
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 20,
+ "entry": "F000:005A",
+ "term": "F000:0068",
+ "pred": [
+ 3,
+ 28,
+ 32
+ ],
+ "succ": [
+ 28,
+ 32,
+ 36
+ ],
+ "asm": [
+ "55|push BP",
+ "89E5|mov BP,SP",
+ "A10400|mov AX,word ptr DS:[4]",
+ "894602|mov word ptr SS:[BP\u002B2],AX",
+ "5D|pop BP",
+ "FF060000|inc word ptr DS:[0]",
+ "CF|iret"
+ ]
+ },
+ {
+ "id": 32,
+ "entry": "F000:004E",
+ "term": "F000:0054",
+ "pred": [
+ 20,
+ 28
+ ],
+ "succ": [
+ 20,
+ 36
+ ],
+ "asm": [
+ "C70604005700|mov word ptr DS:[4],0x0057",
+ "F0FF|invalid"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:0040",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 20,
+ 28
+ ],
+ "asm": [
+ "31C0|xor AX,AX",
+ "8ED8|mov DS,AX",
+ "8ED0|mov SS,AX",
+ "BC0080|mov SP,0x8000",
+ "C70618005A00|mov word ptr DS:[0x0018],0x005A",
+ "8C0E1A00|mov word ptr DS:[0x001A],CS",
+ "C70600000000|mov word ptr DS:[0],0",
+ "C70602000000|mov word ptr DS:[2],0",
+ "C70610000100|mov word ptr DS:[0x0010],1",
+ "F001061000|add word ptr DS:[0x0010],AX",
+ "F0FF061000|inc word ptr DS:[0x0010]",
+ "F0FF0E1000|dec word ptr DS:[0x0010]",
+ "C70602000300|mov word ptr DS:[2],3",
+ "C70604004500|mov word ptr DS:[4],0x0045",
+ "F089|invalid"
+ ]
+ },
+ {
+ "id": 28,
+ "entry": "F000:0045",
+ "term": "F000:004B",
+ "pred": [
+ 3,
+ 20
+ ],
+ "succ": [
+ 20,
+ 32
+ ],
+ "asm": [
+ "C70604004E00|mov word ptr DS:[4],0x004E",
+ "F003|invalid"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/mul.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/mul.json
new file mode 100644
index 0000000000..6e08e3c370
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/mul.json
@@ -0,0 +1,208 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:01E7",
+ "lastExecutedBlockId": 3,
+ "blocks": [
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:01E7",
+ "pred": [
+ 2
+ ],
+ "succ": [],
+ "asm": [
+ "BCC000|mov SP,0x00C0",
+ "BB0300|mov BX,3",
+ "B80700|mov AX,7",
+ "BAFFFF|mov DX,0xFFFF",
+ "F7E3|mul BX",
+ "A30000|mov word ptr DS:[0],AX",
+ "89160200|mov word ptr DS:[2],DX",
+ "9C|pushf",
+ "BA20A3|mov DX,0xA320",
+ "B8FFFF|mov AX,0xFFFF",
+ "F7E2|mul DX",
+ "A30400|mov word ptr DS:[4],AX",
+ "89160600|mov word ptr DS:[6],DX",
+ "9C|pushf",
+ "B8FFFF|mov AX,0xFFFF",
+ "C70608000100|mov word ptr DS:[8],1",
+ "F7260800|mul word ptr DS:[8]",
+ "A30A00|mov word ptr DS:[0x000A],AX",
+ "89160C00|mov word ptr DS:[0x000C],DX",
+ "9C|pushf",
+ "B8FFFF|mov AX,0xFFFF",
+ "C7060E00FFFF|mov word ptr DS:[0x000E],0xFFFF",
+ "F7260E00|mul word ptr DS:[0x000E]",
+ "A31000|mov word ptr DS:[0x0010],AX",
+ "89161200|mov word ptr DS:[0x0012],DX",
+ "9C|pushf",
+ "B8DB46|mov AX,0x46DB",
+ "BD0000|mov BP,0",
+ "F7E5|mul BP",
+ "892E1400|mov word ptr DS:[0x0014],BP",
+ "A31600|mov word ptr DS:[0x0016],AX",
+ "89161800|mov word ptr DS:[0x0018],DX",
+ "9C|pushf",
+ "B8DB46|mov AX,0x46DB",
+ "BEEBEE|mov SI,0xEEEB",
+ "F7E6|mul SI",
+ "89361A00|mov word ptr DS:[0x001A],SI",
+ "A31C00|mov word ptr DS:[0x001C],AX",
+ "89161E00|mov word ptr DS:[0x001E],DX",
+ "9C|pushf",
+ "B314|mov BL,0x14",
+ "B807FF|mov AX,0xFF07",
+ "BAFFFF|mov DX,0xFFFF",
+ "F6E3|mul BL",
+ "A32000|mov word ptr DS:[0x0020],AX",
+ "89162200|mov word ptr DS:[0x0022],DX",
+ "9C|pushf",
+ "B524|mov CH,0x24",
+ "B8FF00|mov AX,0x00FF",
+ "F6E5|mul CH",
+ "A32400|mov word ptr DS:[0x0024],AX",
+ "89162600|mov word ptr DS:[0x0026],DX",
+ "9C|pushf",
+ "B8FF00|mov AX,0x00FF",
+ "C606280001|mov byte ptr DS:[0x0028],1",
+ "F6262800|mul byte ptr DS:[0x0028]",
+ "A32900|mov word ptr DS:[0x0029],AX",
+ "89162B00|mov word ptr DS:[0x002B],DX",
+ "9C|pushf",
+ "B8FFFF|mov AX,0xFFFF",
+ "C6062D00FF|mov byte ptr DS:[0x002D],0xFF",
+ "F6262D00|mul byte ptr DS:[0x002D]",
+ "A32E00|mov word ptr DS:[0x002E],AX",
+ "89162E00|mov word ptr DS:[0x002E],DX",
+ "9C|pushf",
+ "B8C500|mov AX,0x00C5",
+ "BA0000|mov DX,0",
+ "F6E2|mul DL",
+ "89163000|mov word ptr DS:[0x0030],DX",
+ "A33200|mov word ptr DS:[0x0032],AX",
+ "9C|pushf",
+ "B0B5|mov AL,0xB5",
+ "B6F9|mov DH,0xF9",
+ "F6E6|mul DH",
+ "89363400|mov word ptr DS:[0x0034],SI",
+ "A33600|mov word ptr DS:[0x0036],AX",
+ "89163800|mov word ptr DS:[0x0038],DX",
+ "9C|pushf",
+ "BB0300|mov BX,3",
+ "B80700|mov AX,7",
+ "BAFFFF|mov DX,0xFFFF",
+ "F7EB|imul BX",
+ "A33C00|mov word ptr DS:[0x003C],AX",
+ "89163E00|mov word ptr DS:[0x003E],DX",
+ "9C|pushf",
+ "BA20A3|mov DX,0xA320",
+ "B8FFFF|mov AX,0xFFFF",
+ "F7EA|imul DX",
+ "A34000|mov word ptr DS:[0x0040],AX",
+ "89164200|mov word ptr DS:[0x0042],DX",
+ "9C|pushf",
+ "B8FFFF|mov AX,0xFFFF",
+ "C70644000100|mov word ptr DS:[0x0044],1",
+ "F72E4400|imul word ptr DS:[0x0044]",
+ "A34600|mov word ptr DS:[0x0046],AX",
+ "89164800|mov word ptr DS:[0x0048],DX",
+ "9C|pushf",
+ "B8FFFF|mov AX,0xFFFF",
+ "C7064A00FFFF|mov word ptr DS:[0x004A],0xFFFF",
+ "F72E4A00|imul word ptr DS:[0x004A]",
+ "A34C00|mov word ptr DS:[0x004C],AX",
+ "89164E00|mov word ptr DS:[0x004E],DX",
+ "9C|pushf",
+ "B8DB46|mov AX,0x46DB",
+ "BD0000|mov BP,0",
+ "F7ED|imul BP",
+ "892E5000|mov word ptr DS:[0x0050],BP",
+ "A35200|mov word ptr DS:[0x0052],AX",
+ "89165400|mov word ptr DS:[0x0054],DX",
+ "9C|pushf",
+ "B8DB46|mov AX,0x46DB",
+ "BEEBEE|mov SI,0xEEEB",
+ "F7EE|imul SI",
+ "89365600|mov word ptr DS:[0x0056],SI",
+ "A35800|mov word ptr DS:[0x0058],AX",
+ "89165A00|mov word ptr DS:[0x005A],DX",
+ "9C|pushf",
+ "B314|mov BL,0x14",
+ "B807FF|mov AX,0xFF07",
+ "BAFFFF|mov DX,0xFFFF",
+ "F6EB|imul BL",
+ "A35C00|mov word ptr DS:[0x005C],AX",
+ "89165E00|mov word ptr DS:[0x005E],DX",
+ "9C|pushf",
+ "B524|mov CH,0x24",
+ "B8FF00|mov AX,0x00FF",
+ "F6ED|imul CH",
+ "A36000|mov word ptr DS:[0x0060],AX",
+ "89166200|mov word ptr DS:[0x0062],DX",
+ "9C|pushf",
+ "B8FF00|mov AX,0x00FF",
+ "C606640001|mov byte ptr DS:[0x0064],1",
+ "F62E6400|imul byte ptr DS:[0x0064]",
+ "A36500|mov word ptr DS:[0x0065],AX",
+ "89166700|mov word ptr DS:[0x0067],DX",
+ "9C|pushf",
+ "B8FFFF|mov AX,0xFFFF",
+ "C6066900FF|mov byte ptr DS:[0x0069],0xFF",
+ "F62E6900|imul byte ptr DS:[0x0069]",
+ "A36A00|mov word ptr DS:[0x006A],AX",
+ "89166A00|mov word ptr DS:[0x006A],DX",
+ "9C|pushf",
+ "B8C500|mov AX,0x00C5",
+ "BA0000|mov DX,0",
+ "F6EA|imul DL",
+ "89166C00|mov word ptr DS:[0x006C],DX",
+ "A36E00|mov word ptr DS:[0x006E],AX",
+ "9C|pushf",
+ "B0B5|mov AL,0xB5",
+ "B6F9|mov DH,0xF9",
+ "F6EE|imul DH",
+ "89367000|mov word ptr DS:[0x0070],SI",
+ "A37200|mov word ptr DS:[0x0072],AX",
+ "89167400|mov word ptr DS:[0x0074],DX",
+ "9C|pushf",
+ "B800FF|mov AX,0xFF00",
+ "D50A|aad",
+ "A37600|mov word ptr DS:[0x0076],AX",
+ "9C|pushf",
+ "B8FFFF|mov AX,0xFFFF",
+ "D512|aad",
+ "A37800|mov word ptr DS:[0x0078],AX",
+ "9C|pushf",
+ "B8FF00|mov AX,0x00FF",
+ "D5FF|aad",
+ "A37A00|mov word ptr DS:[0x007A],AX",
+ "9C|pushf",
+ "B82D53|mov AX,0x532D",
+ "D539|aad",
+ "A37C00|mov word ptr DS:[0x007C],AX",
+ "9C|pushf",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/rep.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/rep.json
new file mode 100644
index 0000000000..3af9e1479a
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/rep.json
@@ -0,0 +1,853 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:6026",
+ "lastExecutedBlockId": 216,
+ "blocks": [
+ {
+ "id": 216,
+ "entry": "F000:601B",
+ "term": "F000:6026",
+ "pred": [
+ 212
+ ],
+ "succ": [],
+ "asm": [
+ "BA0000|mov DX,0",
+ "8EDA|mov DS,DX",
+ "C70604003412|mov word ptr DS:[4],0x1234",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF5",
+ "pred": [],
+ "succ": [
+ 5
+ ],
+ "asm": [
+ "BC0010|mov SP,0x1000",
+ "8ED4|mov SS,SP",
+ "EB09|jmp short 0"
+ ]
+ },
+ {
+ "id": 212,
+ "entry": "F000:5031",
+ "term": "F000:5034",
+ "pred": [
+ 210
+ ],
+ "succ": [
+ 216
+ ],
+ "asm": [
+ "FC|cld",
+ "F2AF|repne scas AX,word ptr ES:[DI]",
+ "FFE1|jmp near CX"
+ ]
+ },
+ {
+ "id": 5,
+ "entry": "F000:0000",
+ "term": "F000:0021",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 23
+ ],
+ "asm": [
+ "F3A4|rep movs byte ptr ES:[DI],byte ptr DS:[SI]",
+ "F3A4|rep movs byte ptr ES:[DI],byte ptr DS:[SI]",
+ "F2A4|rep movs byte ptr ES:[DI],byte ptr DS:[SI]",
+ "F3A6|repe cmps byte ptr DS:[SI],byte ptr ES:[DI]",
+ "F3A6|repe cmps byte ptr DS:[SI],byte ptr ES:[DI]",
+ "F2A6|repne cmps byte ptr DS:[SI],byte ptr ES:[DI]",
+ "F3AE|repe scas AL,byte ptr ES:[DI]",
+ "F3AE|repe scas AL,byte ptr ES:[DI]",
+ "F2AE|repne scas AL,byte ptr ES:[DI]",
+ "F3AC|rep lods AL,byte ptr DS:[SI]",
+ "F3AC|rep lods AL,byte ptr DS:[SI]",
+ "F2AC|rep lods AL,byte ptr DS:[SI]",
+ "F3AA|rep stos byte ptr ES:[DI],AL",
+ "F3AA|rep stos byte ptr ES:[DI],AL",
+ "F2AA|rep stos byte ptr ES:[DI],AL",
+ "BB4000|mov BX,0x0040",
+ "53|push BX"
+ ]
+ },
+ {
+ "id": 210,
+ "entry": "F000:5030",
+ "term": "F000:5030",
+ "pred": [
+ 201
+ ],
+ "succ": [
+ 212
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 23,
+ "entry": "F000:0022",
+ "term": "F000:0022",
+ "pred": [
+ 5
+ ],
+ "succ": [
+ 25
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 201,
+ "entry": "F000:501D",
+ "term": "F000:502F",
+ "pred": [
+ 199
+ ],
+ "succ": [
+ 210
+ ],
+ "asm": [
+ "B92360|mov CX,0x6023",
+ "FD|std",
+ "BF0600|mov DI,6",
+ "BE0630|mov SI,0x3006",
+ "F2A7|repne cmps word ptr DS:[SI],word ptr ES:[DI]",
+ "B80010|mov AX,0x1000",
+ "BB0000|mov BX,0",
+ "53|push BX"
+ ]
+ },
+ {
+ "id": 25,
+ "entry": "F000:0023",
+ "term": "F000:003B",
+ "pred": [
+ 23
+ ],
+ "succ": [
+ 39
+ ],
+ "asm": [
+ "F3A4|rep movs byte ptr ES:[DI],byte ptr DS:[SI]",
+ "F2A4|rep movs byte ptr ES:[DI],byte ptr DS:[SI]",
+ "F3A6|repe cmps byte ptr DS:[SI],byte ptr ES:[DI]",
+ "F2A6|repne cmps byte ptr DS:[SI],byte ptr ES:[DI]",
+ "F3AE|repe scas AL,byte ptr ES:[DI]",
+ "F2AE|repne scas AL,byte ptr ES:[DI]",
+ "F3AC|rep lods AL,byte ptr DS:[SI]",
+ "F2AC|rep lods AL,byte ptr DS:[SI]",
+ "F3AA|rep stos byte ptr ES:[DI],AL",
+ "F2AA|rep stos byte ptr ES:[DI],AL",
+ "89F8|mov AX,DI",
+ "B410|mov AH,0x10",
+ "FFE0|jmp near AX"
+ ]
+ },
+ {
+ "id": 199,
+ "entry": "F000:501C",
+ "term": "F000:501C",
+ "pred": [
+ 196
+ ],
+ "succ": [
+ 201
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 39,
+ "entry": "F000:1000",
+ "term": "F000:1005",
+ "pred": [
+ 25
+ ],
+ "succ": [
+ 43
+ ],
+ "asm": [
+ "B90000|mov CX,0",
+ "F351|push CX",
+ "FFE4|jmp near SP"
+ ]
+ },
+ {
+ "id": 196,
+ "entry": "F000:5018",
+ "term": "F000:501B",
+ "pred": [
+ 192
+ ],
+ "succ": [
+ 199
+ ],
+ "asm": [
+ "BB0000|mov BX,0",
+ "53|push BX"
+ ]
+ },
+ {
+ "id": 43,
+ "entry": "F000:0FFE",
+ "term": "F000:0FFE",
+ "pred": [
+ 39
+ ],
+ "succ": [
+ 45
+ ],
+ "asm": [
+ "EB07|jmp short 0x1007"
+ ]
+ },
+ {
+ "id": 192,
+ "entry": "F000:5010",
+ "term": "F000:5015",
+ "pred": [
+ 188
+ ],
+ "succ": [
+ 196
+ ],
+ "asm": [
+ "B90200|mov CX,2",
+ "F2AA|rep stos byte ptr ES:[DI],AL",
+ "E301|jcxz short 0x5018"
+ ]
+ },
+ {
+ "id": 45,
+ "entry": "F000:1007",
+ "term": "F000:100C",
+ "pred": [
+ 43
+ ],
+ "succ": [
+ 49
+ ],
+ "asm": [
+ "B90A11|mov CX,0x110A",
+ "F351|push CX",
+ "FFE4|jmp near SP"
+ ]
+ },
+ {
+ "id": 188,
+ "entry": "F000:5008",
+ "term": "F000:500D",
+ "pred": [
+ 184
+ ],
+ "succ": [
+ 192
+ ],
+ "asm": [
+ "B90200|mov CX,2",
+ "F2AC|rep lods AL,byte ptr DS:[SI]",
+ "E301|jcxz short 0x5010"
+ ]
+ },
+ {
+ "id": 49,
+ "entry": "F000:0FFC",
+ "term": "F000:0FFC",
+ "pred": [
+ 45
+ ],
+ "succ": [
+ 51
+ ],
+ "asm": [
+ "EB10|jmp short 0x100E"
+ ]
+ },
+ {
+ "id": 184,
+ "entry": "F000:5000",
+ "term": "F000:5005",
+ "pred": [
+ 178
+ ],
+ "succ": [
+ 188
+ ],
+ "asm": [
+ "B90200|mov CX,2",
+ "F2A4|rep movs byte ptr ES:[DI],byte ptr DS:[SI]",
+ "E301|jcxz short 0x5008"
+ ]
+ },
+ {
+ "id": 51,
+ "entry": "F000:100E",
+ "term": "F000:100E",
+ "pred": [
+ 49
+ ],
+ "succ": [
+ 53
+ ],
+ "asm": [
+ "FFE1|jmp near CX"
+ ]
+ },
+ {
+ "id": 178,
+ "entry": "F000:403F",
+ "term": "F000:4049",
+ "pred": [
+ 176
+ ],
+ "succ": [
+ 184
+ ],
+ "asm": [
+ "BE0030|mov SI,0x3000",
+ "BF0000|mov DI,0",
+ "F2A7|repne cmps word ptr DS:[SI],word ptr ES:[DI]",
+ "F2AF|repne scas AX,word ptr ES:[DI]",
+ "FFE1|jmp near CX"
+ ]
+ },
+ {
+ "id": 53,
+ "entry": "F000:110A",
+ "term": "F000:1110",
+ "pred": [
+ 51
+ ],
+ "succ": [
+ 57
+ ],
+ "asm": [
+ "B90500|mov CX,5",
+ "BA0000|mov DX,0",
+ "52|push DX"
+ ]
+ },
+ {
+ "id": 176,
+ "entry": "F000:403E",
+ "term": "F000:403E",
+ "pred": [
+ 169
+ ],
+ "succ": [
+ 178
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 57,
+ "entry": "F000:1111",
+ "term": "F000:1111",
+ "pred": [
+ 53
+ ],
+ "succ": [
+ 59
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 169,
+ "entry": "F000:4030",
+ "term": "F000:403D",
+ "pred": [
+ 167
+ ],
+ "succ": [
+ 176
+ ],
+ "asm": [
+ "B80706|mov AX,0x0607",
+ "B90450|mov CX,0x5004",
+ "F3A7|repe cmps word ptr DS:[SI],word ptr ES:[DI]",
+ "F3AF|repe scas AX,word ptr ES:[DI]",
+ "BB4000|mov BX,0x0040",
+ "53|push BX"
+ ]
+ },
+ {
+ "id": 59,
+ "entry": "F000:1112",
+ "term": "F000:1118",
+ "pred": [
+ 57
+ ],
+ "succ": [
+ 64
+ ],
+ "asm": [
+ "F259|pop CX",
+ "89C8|mov AX,CX",
+ "B420|mov AH,0x20",
+ "FFE0|jmp near AX"
+ ]
+ },
+ {
+ "id": 167,
+ "entry": "F000:122C",
+ "term": "F000:122C",
+ "pred": [
+ 163
+ ],
+ "succ": [
+ 169
+ ],
+ "asm": [
+ "E9012E|jmp near 0x4030"
+ ]
+ },
+ {
+ "id": 64,
+ "entry": "F000:200A",
+ "term": "F000:200A",
+ "pred": [
+ 59
+ ],
+ "succ": [
+ 66
+ ],
+ "asm": [
+ "53|push BX"
+ ]
+ },
+ {
+ "id": 163,
+ "entry": "F000:4029",
+ "term": "F000:402E",
+ "pred": [
+ 161
+ ],
+ "succ": [
+ 167
+ ],
+ "asm": [
+ "B84000|mov AX,0x0040",
+ "F3AF|repe scas AX,word ptr ES:[DI]",
+ "FFE1|jmp near CX"
+ ]
+ },
+ {
+ "id": 66,
+ "entry": "F000:200B",
+ "term": "F000:200B",
+ "pred": [
+ 64
+ ],
+ "succ": [
+ 68
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 161,
+ "entry": "F000:4028",
+ "term": "F000:4028",
+ "pred": [
+ 158
+ ],
+ "succ": [
+ 163
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 68,
+ "entry": "F000:200C",
+ "term": "F000:202A",
+ "pred": [
+ 66
+ ],
+ "succ": [
+ 82
+ ],
+ "asm": [
+ "B90200|mov CX,2",
+ "BE0030|mov SI,0x3000",
+ "B800F0|mov AX,0xF000",
+ "8ED8|mov DS,AX",
+ "B80010|mov AX,0x1000",
+ "8EC0|mov ES,AX",
+ "BF0000|mov DI,0",
+ "F3A4|rep movs byte ptr ES:[DI],byte ptr DS:[SI]",
+ "8ED8|mov DS,AX",
+ "A10000|mov AX,word ptr DS:[0]",
+ "89FD|mov BP,DI",
+ "8902|mov word ptr SS:[BP\u002BSI],AX",
+ "E301|jcxz short 0x202D"
+ ]
+ },
+ {
+ "id": 158,
+ "entry": "F000:4024",
+ "term": "F000:4027",
+ "pred": [
+ 156
+ ],
+ "succ": [
+ 161
+ ],
+ "asm": [
+ "BB4000|mov BX,0x0040",
+ "53|push BX"
+ ]
+ },
+ {
+ "id": 82,
+ "entry": "F000:202D",
+ "term": "F000:202D",
+ "pred": [
+ 68
+ ],
+ "succ": [
+ 84
+ ],
+ "asm": [
+ "FF260430|jmp near word ptr DS:[0x3004]"
+ ]
+ },
+ {
+ "id": 156,
+ "entry": "F000:122F",
+ "term": "F000:122F",
+ "pred": [
+ 148
+ ],
+ "succ": [
+ 158
+ ],
+ "asm": [
+ "E9F22D|jmp near 0x4024"
+ ]
+ },
+ {
+ "id": 84,
+ "entry": "F000:0809",
+ "term": "F000:0809",
+ "pred": [
+ 82
+ ],
+ "succ": [
+ 86
+ ],
+ "asm": [
+ "E90128|jmp near 0x300D"
+ ]
+ },
+ {
+ "id": 148,
+ "entry": "F000:4012",
+ "term": "F000:4022",
+ "pred": [
+ 146
+ ],
+ "succ": [
+ 156
+ ],
+ "asm": [
+ "B93412|mov CX,0x1234",
+ "BE0030|mov SI,0x3000",
+ "BF0000|mov DI,0",
+ "B800F0|mov AX,0xF000",
+ "8ED8|mov DS,AX",
+ "F3A6|repe cmps byte ptr DS:[SI],byte ptr ES:[DI]",
+ "FFE1|jmp near CX"
+ ]
+ },
+ {
+ "id": 86,
+ "entry": "F000:300D",
+ "term": "F000:3018",
+ "pred": [
+ 84
+ ],
+ "succ": [
+ 92
+ ],
+ "asm": [
+ "B800F0|mov AX,0xF000",
+ "8ED8|mov DS,AX",
+ "B90100|mov CX,1",
+ "B80000|mov AX,0",
+ "50|push AX"
+ ]
+ },
+ {
+ "id": 146,
+ "entry": "F000:4011",
+ "term": "F000:4011",
+ "pred": [
+ 143
+ ],
+ "succ": [
+ 148
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 92,
+ "entry": "F000:3019",
+ "term": "F000:3019",
+ "pred": [
+ 86
+ ],
+ "succ": [
+ 94
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 143,
+ "entry": "F000:400D",
+ "term": "F000:4010",
+ "pred": [
+ 139
+ ],
+ "succ": [
+ 146
+ ],
+ "asm": [
+ "BB4000|mov BX,0x0040",
+ "53|push BX"
+ ]
+ },
+ {
+ "id": 94,
+ "entry": "F000:301A",
+ "term": "F000:3028",
+ "pred": [
+ 92
+ ],
+ "succ": [
+ 102
+ ],
+ "asm": [
+ "F3A5|rep movs word ptr ES:[DI],word ptr DS:[SI]",
+ "B80010|mov AX,0x1000",
+ "8ED8|mov DS,AX",
+ "A10200|mov AX,word ptr DS:[2]",
+ "89FD|mov BP,DI",
+ "8902|mov word ptr SS:[BP\u002BSI],AX",
+ "E301|jcxz short 0x302B"
+ ]
+ },
+ {
+ "id": 139,
+ "entry": "F000:4005",
+ "term": "F000:400A",
+ "pred": [
+ 137
+ ],
+ "succ": [
+ 143
+ ],
+ "asm": [
+ "B90400|mov CX,4",
+ "F3AB|rep stos word ptr ES:[DI],AX",
+ "E301|jcxz short 0x400D"
+ ]
+ },
+ {
+ "id": 102,
+ "entry": "F000:302B",
+ "term": "F000:302B",
+ "pred": [
+ 94
+ ],
+ "succ": [
+ 104
+ ],
+ "asm": [
+ "FF260830|jmp near word ptr DS:[0x3008]"
+ ]
+ },
+ {
+ "id": 137,
+ "entry": "F000:4004",
+ "term": "F000:4004",
+ "pred": [
+ 134
+ ],
+ "succ": [
+ 139
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ },
+ {
+ "id": 104,
+ "entry": "F000:0607",
+ "term": "F000:0607",
+ "pred": [
+ 102
+ ],
+ "succ": [
+ 106
+ ],
+ "asm": [
+ "E9252A|jmp near 0x302F"
+ ]
+ },
+ {
+ "id": 134,
+ "entry": "F000:4000",
+ "term": "F000:4003",
+ "pred": [
+ 127
+ ],
+ "succ": [
+ 137
+ ],
+ "asm": [
+ "BB0000|mov BX,0",
+ "53|push BX"
+ ]
+ },
+ {
+ "id": 106,
+ "entry": "F000:302F",
+ "term": "F000:3039",
+ "pred": [
+ 104
+ ],
+ "succ": [
+ 112
+ ],
+ "asm": [
+ "B800F0|mov AX,0xF000",
+ "8ED8|mov DS,AX",
+ "B90300|mov CX,3",
+ "F3AC|rep lods AL,byte ptr DS:[SI]",
+ "FFE0|jmp near AX"
+ ]
+ },
+ {
+ "id": 127,
+ "entry": "F000:304D",
+ "term": "F000:305A",
+ "pred": [
+ 125
+ ],
+ "succ": [
+ 134
+ ],
+ "asm": [
+ "B90200|mov CX,2",
+ "B80040|mov AX,0x4000",
+ "F3AB|rep stos word ptr ES:[DI],AX",
+ "B80010|mov AX,0x1000",
+ "8ED8|mov DS,AX",
+ "FF260600|jmp near word ptr DS:[6]"
+ ]
+ },
+ {
+ "id": 112,
+ "entry": "F000:F003",
+ "term": "F000:F003",
+ "pred": [
+ 106
+ ],
+ "succ": [
+ 114
+ ],
+ "asm": [
+ "E93640|jmp near 0x303C"
+ ]
+ },
+ {
+ "id": 125,
+ "entry": "F000:0102",
+ "term": "F000:0102",
+ "pred": [
+ 119
+ ],
+ "succ": [
+ 127
+ ],
+ "asm": [
+ "E9482F|jmp near 0x304D"
+ ]
+ },
+ {
+ "id": 114,
+ "entry": "F000:303C",
+ "term": "F000:303F",
+ "pred": [
+ 112
+ ],
+ "succ": [
+ 117
+ ],
+ "asm": [
+ "BB4000|mov BX,0x0040",
+ "53|push BX"
+ ]
+ },
+ {
+ "id": 119,
+ "entry": "F000:3041",
+ "term": "F000:304B",
+ "pred": [
+ 117
+ ],
+ "succ": [
+ 125
+ ],
+ "asm": [
+ "B800F0|mov AX,0xF000",
+ "8ED8|mov DS,AX",
+ "B90100|mov CX,1",
+ "F3AD|rep lods AX,word ptr DS:[SI]",
+ "FFE0|jmp near AX"
+ ]
+ },
+ {
+ "id": 117,
+ "entry": "F000:3040",
+ "term": "F000:3040",
+ "pred": [
+ 114
+ ],
+ "succ": [
+ 119
+ ],
+ "asm": [
+ "9D|popf"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/returnedterminator.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/returnedterminator.json
new file mode 100644
index 0000000000..761ceb8426
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/returnedterminator.json
@@ -0,0 +1,72 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:0019",
+ "lastExecutedBlockId": 14,
+ "blocks": [
+ {
+ "id": 14,
+ "entry": "F000:0019",
+ "term": "F000:0019",
+ "pred": [
+ 8
+ ],
+ "succ": [],
+ "asm": [
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 8,
+ "entry": "F000:000A",
+ "term": "F000:0012",
+ "pred": [
+ 3
+ ],
+ "succ": [
+ 14
+ ],
+ "asm": [
+ "B81111|mov AX,0x1111",
+ "50|push AX",
+ "B82222|mov AX,0x2222",
+ "50|push AX",
+ "EB05|jmp short 0x0019"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:0008",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 8
+ ],
+ "asm": [
+ "B80000|mov AX,0",
+ "8ED0|mov SS,AX",
+ "BC0800|mov SP,8",
+ "EB00|jmp short 0x000A"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/rotate.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/rotate.json
new file mode 100644
index 0000000000..6c123f2e50
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/rotate.json
@@ -0,0 +1,198 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:01BB",
+ "lastExecutedBlockId": 3,
+ "blocks": [
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:01BB",
+ "pred": [
+ 2
+ ],
+ "succ": [],
+ "asm": [
+ "B85E3B|mov AX,0x3B5E",
+ "BBA7C8|mov BX,0xC8A7",
+ "C70600007220|mov word ptr DS:[0],0x2072",
+ "C7060200793E|mov word ptr DS:[2],0x3E79",
+ "BCA000|mov SP,0x00A0",
+ "D1D0|rcl AX,1",
+ "9C|pushf",
+ "A32000|mov word ptr DS:[0x0020],AX",
+ "D1160000|rcl word ptr DS:[0],1",
+ "9C|pushf",
+ "B90001|mov CX,0x0100",
+ "D3D3|rcl BX,CL",
+ "9C|pushf",
+ "891E2200|mov word ptr DS:[0x0022],BX",
+ "B9FFFF|mov CX,0xFFFF",
+ "89DA|mov DX,BX",
+ "D3D2|rcl DX,CL",
+ "9C|pushf",
+ "89162400|mov word ptr DS:[0x0024],DX",
+ "B108|mov CL,8",
+ "D3D3|rcl BX,CL",
+ "9C|pushf",
+ "891E2600|mov word ptr DS:[0x0026],BX",
+ "B104|mov CL,4",
+ "D3160200|rcl word ptr DS:[2],CL",
+ "9C|pushf",
+ "BA0459|mov DX,0x5904",
+ "B87CBE|mov AX,0xBE7C",
+ "C70604002FD6|mov word ptr DS:[4],0xD62F",
+ "C7060600D86F|mov word ptr DS:[6],0x6FD8",
+ "D0D4|rcl AH,1",
+ "9C|pushf",
+ "A32800|mov word ptr DS:[0x0028],AX",
+ "D0160500|rcl byte ptr DS:[5],1",
+ "9C|pushf",
+ "B107|mov CL,7",
+ "D2D2|rcl DL,CL",
+ "9C|pushf",
+ "89162A00|mov word ptr DS:[0x002A],DX",
+ "D2160600|rcl byte ptr DS:[6],CL",
+ "9C|pushf",
+ "B8D615|mov AX,0x15D6",
+ "BB0783|mov BX,0x8307",
+ "C7060800B79A|mov word ptr DS:[8],0x9AB7",
+ "C7060A00B628|mov word ptr DS:[0x000A],0x28B6",
+ "D1D8|rcr AX,1",
+ "9C|pushf",
+ "A32C00|mov word ptr DS:[0x002C],AX",
+ "D11E0800|rcr word ptr DS:[8],1",
+ "9C|pushf",
+ "B90001|mov CX,0x0100",
+ "D3DB|rcr BX,CL",
+ "9C|pushf",
+ "891E2E00|mov word ptr DS:[0x002E],BX",
+ "B9FFFF|mov CX,0xFFFF",
+ "89DA|mov DX,BX",
+ "D3DA|rcr DX,CL",
+ "9C|pushf",
+ "89163000|mov word ptr DS:[0x0030],DX",
+ "B105|mov CL,5",
+ "D3DB|rcr BX,CL",
+ "9C|pushf",
+ "891E3200|mov word ptr DS:[0x0032],BX",
+ "B104|mov CL,4",
+ "D31E0A00|rcr word ptr DS:[0x000A],CL",
+ "9C|pushf",
+ "BAAA7E|mov DX,0x7EAA",
+ "B88D3A|mov AX,0x3A8D",
+ "C7060C0014A4|mov word ptr DS:[0x000C],0xA414",
+ "C7060E003828|mov word ptr DS:[0x000E],0x2838",
+ "D0DC|rcr AH,1",
+ "9C|pushf",
+ "A33400|mov word ptr DS:[0x0034],AX",
+ "D01E0D00|rcr byte ptr DS:[0x000D],1",
+ "9C|pushf",
+ "B107|mov CL,7",
+ "D2DA|rcr DL,CL",
+ "9C|pushf",
+ "89163600|mov word ptr DS:[0x0036],DX",
+ "D21E0E00|rcr byte ptr DS:[0x000E],CL",
+ "9C|pushf",
+ "B80D02|mov AX,0x020D",
+ "BB5A8D|mov BX,0x8D5A",
+ "C7061000DD28|mov word ptr DS:[0x0010],0x28DD",
+ "C70612004AD7|mov word ptr DS:[0x0012],0xD74A",
+ "D1C0|rol AX,1",
+ "9C|pushf",
+ "A33800|mov word ptr DS:[0x0038],AX",
+ "D1061000|rol word ptr DS:[0x0010],1",
+ "9C|pushf",
+ "B90001|mov CX,0x0100",
+ "D3C3|rol BX,CL",
+ "9C|pushf",
+ "891E3A00|mov word ptr DS:[0x003A],BX",
+ "B9FFFF|mov CX,0xFFFF",
+ "89DA|mov DX,BX",
+ "D3C2|rol DX,CL",
+ "9C|pushf",
+ "89163C00|mov word ptr DS:[0x003C],DX",
+ "B104|mov CL,4",
+ "D3C3|rol BX,CL",
+ "9C|pushf",
+ "891E3E00|mov word ptr DS:[0x003E],BX",
+ "B104|mov CL,4",
+ "D3061200|rol word ptr DS:[0x0012],CL",
+ "9C|pushf",
+ "BA099D|mov DX,0x9D09",
+ "B848C9|mov AX,0xC948",
+ "C7061400800B|mov word ptr DS:[0x0014],0x0B80",
+ "C7061600E848|mov word ptr DS:[0x0016],0x48E8",
+ "D0C4|rol AH,1",
+ "9C|pushf",
+ "A34000|mov word ptr DS:[0x0040],AX",
+ "D0061500|rol byte ptr DS:[0x0015],1",
+ "9C|pushf",
+ "B107|mov CL,7",
+ "D2C2|rol DL,CL",
+ "9C|pushf",
+ "89164200|mov word ptr DS:[0x0042],DX",
+ "D2061600|rol byte ptr DS:[0x0016],CL",
+ "9C|pushf",
+ "B85EF2|mov AX,0xF25E",
+ "BBB52E|mov BX,0x2EB5",
+ "C70618005101|mov word ptr DS:[0x0018],0x0151",
+ "C7061A003772|mov word ptr DS:[0x001A],0x7237",
+ "D1C8|ror AX,1",
+ "9C|pushf",
+ "A34400|mov word ptr DS:[0x0044],AX",
+ "D10E1800|ror word ptr DS:[0x0018],1",
+ "9C|pushf",
+ "B90001|mov CX,0x0100",
+ "D3CB|ror BX,CL",
+ "9C|pushf",
+ "891E4600|mov word ptr DS:[0x0046],BX",
+ "B9FFFF|mov CX,0xFFFF",
+ "89DA|mov DX,BX",
+ "D3CA|ror DX,CL",
+ "9C|pushf",
+ "89164800|mov word ptr DS:[0x0048],DX",
+ "B104|mov CL,4",
+ "D3CB|ror BX,CL",
+ "9C|pushf",
+ "891E4A00|mov word ptr DS:[0x004A],BX",
+ "B104|mov CL,4",
+ "D30E1A00|ror word ptr DS:[0x001A],CL",
+ "9C|pushf",
+ "BA8842|mov DX,0x4288",
+ "B8AB8B|mov AX,0x8BAB",
+ "C7061C00D95D|mov word ptr DS:[0x001C],0x5DD9",
+ "C7061E00F7C7|mov word ptr DS:[0x001E],0xC7F7",
+ "D0CC|ror AH,1",
+ "9C|pushf",
+ "A34C00|mov word ptr DS:[0x004C],AX",
+ "D00E1D00|ror byte ptr DS:[0x001D],1",
+ "9C|pushf",
+ "B107|mov CL,7",
+ "D2CA|ror DL,CL",
+ "9C|pushf",
+ "89164E00|mov word ptr DS:[0x004E],DX",
+ "D20E1E00|ror byte ptr DS:[0x001E],CL",
+ "9C|pushf",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/segpr.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/segpr.json
new file mode 100644
index 0000000000..1977b212f2
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/segpr.json
@@ -0,0 +1,130 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:121F",
+ "lastExecutedBlockId": 46,
+ "blocks": [
+ {
+ "id": 46,
+ "entry": "F000:1200",
+ "term": "F000:121F",
+ "pred": [
+ 40
+ ],
+ "succ": [],
+ "asm": [
+ "B920F1|mov CX,0xF120",
+ "8EC1|mov ES,CX",
+ "BE0002|mov SI,0x0200",
+ "BF010E|mov DI,0x0E01",
+ "26A6|cmps byte ptr ES:[SI],byte ptr ES:[DI]",
+ "9C|pushf",
+ "B80100|mov AX,1",
+ "8EC0|mov ES,AX",
+ "BF0600|mov DI,6",
+ "BE0014|mov SI,0x1400",
+ "B90600|mov CX,6",
+ "F32EA4|rep movs byte ptr ES:[DI],byte ptr CS:[SI]",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 40,
+ "entry": "F000:0065",
+ "term": "F000:0074",
+ "pred": [
+ 3,
+ 32
+ ],
+ "succ": [
+ 46
+ ],
+ "asm": [
+ "83EC06|sub SP,6",
+ "C70614000012|mov word ptr DS:[0x0014],0x1200",
+ "BB0500|mov BX,5",
+ "BE0300|mov SI,3",
+ "F336FF50FC|call near word ptr SS:[BX\u002BSI-4]"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:005F",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 32,
+ 40
+ ],
+ "asm": [
+ "BB00F1|mov BX,0xF100",
+ "8EC3|mov ES,BX",
+ "268B1E0000|mov BX,word ptr ES:[0]",
+ "26A10200|mov AX,word ptr ES:[2]",
+ "A30200|mov word ptr DS:[2],AX",
+ "BC0200|mov SP,2",
+ "2653|push BX",
+ "26C4160000|les DX,word ptr ES:[0]",
+ "89160400|mov word ptr DS:[4],DX",
+ "8CC2|mov DX,ES",
+ "89160600|mov word ptr DS:[6],DX",
+ "BF0500|mov DI,5",
+ "2E8D7317|lea SI,BP\u002BDI\u002B0x17",
+ "89360800|mov word ptr DS:[8],SI",
+ "BB0500|mov BX,5",
+ "B80500|mov AX,5",
+ "C7060A004523|mov word ptr DS:[0x000A],0x2345",
+ "BA00F1|mov DX,0xF100",
+ "8EC2|mov ES,DX",
+ "26D7|xlat byte ptr ES:[BX\u002BAL]",
+ "A30C00|mov word ptr DS:[0x000C],AX",
+ "B80100|mov AX,1",
+ "8ED0|mov SS,AX",
+ "C70610000600|mov word ptr DS:[0x0010],6",
+ "36FF060000|inc word ptr SS:[0]",
+ "BC2000|mov SP,0x0020",
+ "C70612000000|mov word ptr DS:[0x0012],0",
+ "36F3F7360200|div word ptr SS:[2]"
+ ]
+ },
+ {
+ "id": 32,
+ "entry": "F000:1100",
+ "term": "F000:1111",
+ "pred": [
+ 3
+ ],
+ "succ": [
+ 40
+ ],
+ "asm": [
+ "89E6|mov SI,SP",
+ "368B34|mov SI,word ptr SS:[SI]",
+ "89360E00|mov word ptr DS:[0x000E],SI",
+ "83C606|add SI,6",
+ "89E7|mov DI,SP",
+ "368935|mov word ptr SS:[DI],SI",
+ "CF|iret"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/selfmodifycall.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/selfmodifycall.json
new file mode 100644
index 0000000000..fd013843c9
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/selfmodifycall.json
@@ -0,0 +1,151 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:0010",
+ "lastExecutedBlockId": 24,
+ "blocks": [
+ {
+ "id": 24,
+ "entry": "F000:0010",
+ "term": "F000:0010",
+ "pred": [
+ 26
+ ],
+ "succ": [],
+ "asm": [
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 26,
+ "entry": "F000:0010",
+ "term": "F000:0010",
+ "pred": [
+ 12,
+ 17,
+ 19
+ ],
+ "succ": [
+ 14,
+ 24
+ ],
+ "asm": [
+ "selector"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:0008",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 17
+ ],
+ "asm": [
+ "B80000|mov AX,0",
+ "8ED0|mov SS,AX",
+ "BC0001|mov SP,0x0100",
+ "C6062B0000|mov byte ptr DS:[0x002B],0"
+ ]
+ },
+ {
+ "id": 14,
+ "entry": "F000:0010",
+ "dead": true,
+ "term": "F000:0015",
+ "pred": [
+ 26
+ ],
+ "succ": [
+ 17
+ ],
+ "asm": [
+ "90|nop",
+ "FE062B00|inc byte ptr DS:[0x002B]",
+ "EBF6|jmp short 0x000D"
+ ]
+ },
+ {
+ "id": 12,
+ "entry": "F000:002A",
+ "term": "F000:002A",
+ "pred": [
+ 9
+ ],
+ "succ": [
+ 26
+ ],
+ "asm": [
+ "C3|ret near"
+ ]
+ },
+ {
+ "id": 17,
+ "entry": "F000:000D",
+ "term": "F000:000D",
+ "pred": [
+ 3,
+ 14
+ ],
+ "succ": [
+ 9,
+ 26
+ ],
+ "asm": [
+ "E80700|call near 0x0017"
+ ]
+ },
+ {
+ "id": 19,
+ "entry": "F000:001E",
+ "term": "F000:0029",
+ "pred": [
+ 9
+ ],
+ "succ": [
+ 26
+ ],
+ "asm": [
+ "B800F0|mov AX,0xF000",
+ "8EC0|mov ES,AX",
+ "26C6061000F4|mov byte ptr ES:[0x0010],0xF4",
+ "C3|ret near"
+ ]
+ },
+ {
+ "id": 9,
+ "entry": "F000:0017",
+ "term": "F000:001C",
+ "pred": [
+ 17
+ ],
+ "succ": [
+ 12,
+ 19
+ ],
+ "asm": [
+ "803E2B0000|cmp byte ptr DS:[0x002B],0",
+ "740C|je short 0x002A"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/selfmodifyinstructions.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/selfmodifyinstructions.json
new file mode 100644
index 0000000000..225f3f98bb
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/selfmodifyinstructions.json
@@ -0,0 +1,133 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:1406",
+ "lastExecutedBlockId": 29,
+ "blocks": [
+ {
+ "id": 29,
+ "entry": "F000:1400",
+ "term": "F000:1406",
+ "pred": [
+ 22
+ ],
+ "succ": [],
+ "asm": [
+ "B90100|mov CX,1",
+ "41|inc CX",
+ "41|inc CX",
+ "51|push CX",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 22,
+ "entry": "F000:1400",
+ "term": "F000:1400",
+ "pred": [
+ 19
+ ],
+ "succ": [
+ 15,
+ 23,
+ 29
+ ],
+ "asm": [
+ "selector"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:0008",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 19
+ ],
+ "asm": [
+ "B80000|mov AX,0",
+ "8ED0|mov SS,AX",
+ "BC0600|mov SP,6",
+ "BE0008|mov SI,0x0800"
+ ]
+ },
+ {
+ "id": 15,
+ "entry": "F000:1400",
+ "dead": true,
+ "term": "F000:1407",
+ "pred": [
+ 22
+ ],
+ "succ": [
+ 19
+ ],
+ "asm": [
+ "B80100|mov AX,1",
+ "50|push AX",
+ "BE000C|mov SI,0x0C00",
+ "EA0B0000F0|jmp far F000:000B"
+ ]
+ },
+ {
+ "id": 23,
+ "entry": "F000:1400",
+ "dead": true,
+ "term": "F000:1409",
+ "pred": [
+ 22
+ ],
+ "succ": [
+ 19
+ ],
+ "asm": [
+ "BB0100|mov BX,1",
+ "D1E3|shl BX,1",
+ "53|push BX",
+ "BE0010|mov SI,0x1000",
+ "EA0B0000F0|jmp far F000:000B"
+ ]
+ },
+ {
+ "id": 19,
+ "entry": "F000:000B",
+ "term": "F000:001A",
+ "pred": [
+ 3,
+ 15,
+ 23
+ ],
+ "succ": [
+ 22
+ ],
+ "asm": [
+ "B800F0|mov AX,0xF000",
+ "8ED8|mov DS,AX",
+ "8EC0|mov ES,AX",
+ "BF0014|mov DI,0x1400",
+ "B9E803|mov CX,0x03E8",
+ "F3A5|rep movs word ptr ES:[DI],word ptr DS:[SI]",
+ "E9E313|jmp near 0x1400"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/selfmodifyterminator.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/selfmodifyterminator.json
new file mode 100644
index 0000000000..c349492c98
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/selfmodifyterminator.json
@@ -0,0 +1,170 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:0021",
+ "lastExecutedBlockId": 32,
+ "blocks": [
+ {
+ "id": 32,
+ "entry": "F000:0020",
+ "term": "F000:0021",
+ "pred": [
+ 30
+ ],
+ "succ": [],
+ "asm": [
+ "50|push AX",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 30,
+ "entry": "F000:0019",
+ "term": "F000:0019",
+ "pred": [
+ 17
+ ],
+ "succ": [
+ 32
+ ],
+ "asm": [
+ "EB05|jmp short 0x0020"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:000B",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 20
+ ],
+ "asm": [
+ "B80000|mov AX,0",
+ "8ED0|mov SS,AX",
+ "BC0400|mov SP,4",
+ "B8FFFF|mov AX,0xFFFF",
+ "C606400000|mov byte ptr DS:[0x0040],0"
+ ]
+ },
+ {
+ "id": 17,
+ "entry": "F000:0014",
+ "term": "F000:0019",
+ "pred": [
+ 14,
+ 20,
+ 22
+ ],
+ "succ": [
+ 29,
+ 30
+ ],
+ "asm": [
+ "B84200|mov AX,0x0042",
+ "09C0|or AX,AX",
+ "selector"
+ ]
+ },
+ {
+ "id": 20,
+ "entry": "F000:0010",
+ "term": "F000:0011",
+ "pred": [
+ 3,
+ 29
+ ],
+ "succ": [
+ 11,
+ 17
+ ],
+ "asm": [
+ "50|push AX",
+ "E80E00|call near 0x0022"
+ ]
+ },
+ {
+ "id": 29,
+ "entry": "F000:0019",
+ "dead": true,
+ "term": "F000:0019",
+ "pred": [
+ 17
+ ],
+ "succ": [
+ 20
+ ],
+ "asm": [
+ "75F5|jne short 0x0010"
+ ]
+ },
+ {
+ "id": 14,
+ "entry": "F000:003B",
+ "term": "F000:003F",
+ "pred": [
+ 11
+ ],
+ "succ": [
+ 17
+ ],
+ "asm": [
+ "FE064000|inc byte ptr DS:[0x0040]",
+ "C3|ret near"
+ ]
+ },
+ {
+ "id": 22,
+ "entry": "F000:0029",
+ "term": "F000:003A",
+ "pred": [
+ 11
+ ],
+ "succ": [
+ 17
+ ],
+ "asm": [
+ "B800F0|mov AX,0xF000",
+ "8EC0|mov ES,AX",
+ "26C6061900EB|mov byte ptr ES:[0x0019],0xEB",
+ "26C6061A0005|mov byte ptr ES:[0x001A],5",
+ "C3|ret near"
+ ]
+ },
+ {
+ "id": 11,
+ "entry": "F000:0022",
+ "term": "F000:0027",
+ "pred": [
+ 20
+ ],
+ "succ": [
+ 14,
+ 22
+ ],
+ "asm": [
+ "803E400000|cmp byte ptr DS:[0x0040],0",
+ "7412|je short 0x003B"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/selfmodifyvalue.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/selfmodifyvalue.json
new file mode 100644
index 0000000000..6847d132d1
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/selfmodifyvalue.json
@@ -0,0 +1,89 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:001C",
+ "lastExecutedBlockId": 18,
+ "blocks": [
+ {
+ "id": 18,
+ "entry": "F000:001C",
+ "term": "F000:001C",
+ "pred": [
+ 9
+ ],
+ "succ": [],
+ "asm": [
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 9,
+ "entry": "F000:000D",
+ "term": "F000:001A",
+ "pred": [
+ 15
+ ],
+ "succ": [
+ 15,
+ 18
+ ],
+ "asm": [
+ "B8____|mov AX,word ptr [0x000F000E]",
+ "50|push AX",
+ "2E891E0E00|mov word ptr CS:[0x000E],BX",
+ "4B|dec BX",
+ "83F800|cmp AX,0",
+ "75EF|jne short 0x000B"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:0008",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 15
+ ],
+ "asm": [
+ "B80000|mov AX,0",
+ "8ED0|mov SS,AX",
+ "BC0400|mov SP,4",
+ "BB0A00|mov BX,0x000A"
+ ]
+ },
+ {
+ "id": 15,
+ "entry": "F000:000B",
+ "term": "F000:000B",
+ "pred": [
+ 3,
+ 9
+ ],
+ "succ": [
+ 9
+ ],
+ "asm": [
+ "EB00|jmp short 0x000D"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/shifts.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/shifts.json
new file mode 100644
index 0000000000..1c3cdaf79b
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/shifts.json
@@ -0,0 +1,158 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:014D",
+ "lastExecutedBlockId": 3,
+ "blocks": [
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:014D",
+ "pred": [
+ 2
+ ],
+ "succ": [],
+ "asm": [
+ "B8C56E|mov AX,0x6EC5",
+ "BBA8B1|mov BX,0xB1A8",
+ "C70600003853|mov word ptr DS:[0],0x5338",
+ "C7060200FE31|mov word ptr DS:[2],0x31FE",
+ "BC8000|mov SP,0x0080",
+ "D1E0|shl AX,1",
+ "9C|pushf",
+ "A32000|mov word ptr DS:[0x0020],AX",
+ "D1260000|shl word ptr DS:[0],1",
+ "9C|pushf",
+ "B90001|mov CX,0x0100",
+ "D3E3|shl BX,CL",
+ "9C|pushf",
+ "891E2200|mov word ptr DS:[0x0022],BX",
+ "B9FFFF|mov CX,0xFFFF",
+ "89DA|mov DX,BX",
+ "D3E2|shl DX,CL",
+ "9C|pushf",
+ "89162400|mov word ptr DS:[0x0024],DX",
+ "B108|mov CL,8",
+ "D3E3|shl BX,CL",
+ "9C|pushf",
+ "891E2600|mov word ptr DS:[0x0026],BX",
+ "B104|mov CL,4",
+ "D2260200|shl byte ptr DS:[2],CL",
+ "9C|pushf",
+ "BA6F95|mov DX,0x956F",
+ "B89342|mov AX,0x4293",
+ "C7060400C033|mov word ptr DS:[4],0x33C0",
+ "C7060600FF64|mov word ptr DS:[6],0x64FF",
+ "D0E4|shl AH,1",
+ "9C|pushf",
+ "A32800|mov word ptr DS:[0x0028],AX",
+ "D0260500|shl byte ptr DS:[5],1",
+ "9C|pushf",
+ "B107|mov CL,7",
+ "D2E2|shl DL,CL",
+ "9C|pushf",
+ "89162A00|mov word ptr DS:[0x002A],DX",
+ "D2260600|shl byte ptr DS:[6],CL",
+ "9C|pushf",
+ "B872FB|mov AX,0xFB72",
+ "BBB9DF|mov BX,0xDFB9",
+ "C7060800BB1E|mov word ptr DS:[8],0x1EBB",
+ "C7060A002F74|mov word ptr DS:[0x000A],0x742F",
+ "D1F8|sar AX,1",
+ "9C|pushf",
+ "A32C00|mov word ptr DS:[0x002C],AX",
+ "D13E0800|sar word ptr DS:[8],1",
+ "9C|pushf",
+ "B90001|mov CX,0x0100",
+ "D3FB|sar BX,CL",
+ "9C|pushf",
+ "891E2E00|mov word ptr DS:[0x002E],BX",
+ "B9FFFF|mov CX,0xFFFF",
+ "89DA|mov DX,BX",
+ "D3FA|sar DX,CL",
+ "9C|pushf",
+ "89163000|mov word ptr DS:[0x0030],DX",
+ "B105|mov CL,5",
+ "D3FB|sar BX,CL",
+ "9C|pushf",
+ "891E3200|mov word ptr DS:[0x0032],BX",
+ "B104|mov CL,4",
+ "D23E0A00|sar byte ptr DS:[0x000A],CL",
+ "9C|pushf",
+ "BAB893|mov DX,0x93B8",
+ "B88866|mov AX,0x6688",
+ "C7060C00D4CA|mov word ptr DS:[0x000C],0xCAD4",
+ "C7060E00C96E|mov word ptr DS:[0x000E],0x6EC9",
+ "D0FC|sar AH,1",
+ "9C|pushf",
+ "A33400|mov word ptr DS:[0x0034],AX",
+ "D03E0D00|sar byte ptr DS:[0x000D],1",
+ "9C|pushf",
+ "B107|mov CL,7",
+ "D2FA|sar DL,CL",
+ "9C|pushf",
+ "89163600|mov word ptr DS:[0x0036],DX",
+ "D23E0E00|sar byte ptr DS:[0x000E],CL",
+ "9C|pushf",
+ "B8A17B|mov AX,0x7BA1",
+ "BBE854|mov BX,0x54E8",
+ "C7061000AABA|mov word ptr DS:[0x0010],0xBAAA",
+ "C70612003134|mov word ptr DS:[0x0012],0x3431",
+ "D1E8|shr AX,1",
+ "9C|pushf",
+ "A33800|mov word ptr DS:[0x0038],AX",
+ "D12E1000|shr word ptr DS:[0x0010],1",
+ "9C|pushf",
+ "B90001|mov CX,0x0100",
+ "D3EB|shr BX,CL",
+ "9C|pushf",
+ "891E3A00|mov word ptr DS:[0x003A],BX",
+ "B9FFFF|mov CX,0xFFFF",
+ "89DA|mov DX,BX",
+ "D3EA|shr DX,CL",
+ "9C|pushf",
+ "89163C00|mov word ptr DS:[0x003C],DX",
+ "B104|mov CL,4",
+ "D3EB|shr BX,CL",
+ "9C|pushf",
+ "891E3E00|mov word ptr DS:[0x003E],BX",
+ "B104|mov CL,4",
+ "D22E1200|shr byte ptr DS:[0x0012],CL",
+ "9C|pushf",
+ "BA1004|mov DX,0x0410",
+ "B82816|mov AX,0x1628",
+ "C7061400263B|mov word ptr DS:[0x0014],0x3B26",
+ "C70616000D8D|mov word ptr DS:[0x0016],0x8D0D",
+ "D0EC|shr AH,1",
+ "9C|pushf",
+ "A34000|mov word ptr DS:[0x0040],AX",
+ "D02E1500|shr byte ptr DS:[0x0015],1",
+ "9C|pushf",
+ "B107|mov CL,7",
+ "D2EA|shr DL,CL",
+ "9C|pushf",
+ "89164200|mov word ptr DS:[0x0042],DX",
+ "D22E1600|shr byte ptr DS:[0x0016],CL",
+ "9C|pushf",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/sticli.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/sticli.json
new file mode 100644
index 0000000000..3af3b31b17
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/sticli.json
@@ -0,0 +1,69 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:000C",
+ "lastExecutedBlockId": 9,
+ "blocks": [
+ {
+ "id": 9,
+ "entry": "F000:0009",
+ "term": "F000:000C",
+ "pred": [
+ 7
+ ],
+ "succ": [],
+ "asm": [
+ "FA|cli",
+ "8904|mov word ptr DS:[SI],AX",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 7,
+ "entry": "F000:0006",
+ "term": "F000:0006",
+ "pred": [
+ 3
+ ],
+ "succ": [
+ 9
+ ],
+ "asm": [
+ "B83412|mov AX,0x1234"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:0005",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 7
+ ],
+ "asm": [
+ "B80010|mov AX,0x1000",
+ "8ED8|mov DS,AX",
+ "FB|sti"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/strings.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/strings.json
new file mode 100644
index 0000000000..7014ee2756
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/strings.json
@@ -0,0 +1,263 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:D00B",
+ "lastExecutedBlockId": 70,
+ "blocks": [
+ {
+ "id": 70,
+ "entry": "F000:D000",
+ "term": "F000:D00B",
+ "pred": [
+ 66
+ ],
+ "succ": [],
+ "asm": [
+ "BA0000|mov DX,0",
+ "8EDA|mov DS,DX",
+ "C70602003412|mov word ptr DS:[2],0x1234",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ },
+ {
+ "id": 66,
+ "entry": "F000:8013",
+ "term": "F000:8017",
+ "pred": [
+ 61
+ ],
+ "succ": [
+ 70
+ ],
+ "asm": [
+ "B800D0|mov AX,0xD000",
+ "AB|stos word ptr ES:[DI],AX",
+ "FF6502|jmp near word ptr DS:[DI\u002B2]"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:000F",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 12
+ ],
+ "asm": [
+ "B900F0|mov CX,0xF000",
+ "8ED9|mov DS,CX",
+ "8EC1|mov ES,CX",
+ "BE0010|mov SI,0x1000",
+ "BF0120|mov DI,0x2001",
+ "A6|cmps byte ptr DS:[SI],byte ptr ES:[DI]",
+ "9C|pushf",
+ "C3|ret near"
+ ]
+ },
+ {
+ "id": 61,
+ "entry": "F000:1350",
+ "term": "F000:1354",
+ "pred": [
+ 56
+ ],
+ "succ": [
+ 66
+ ],
+ "asm": [
+ "B080|mov AL,0x80",
+ "FD|std",
+ "AA|stos byte ptr ES:[DI],AL",
+ "FF25|jmp near word ptr DS:[DI]"
+ ]
+ },
+ {
+ "id": 12,
+ "entry": "F000:0097",
+ "term": "F000:0099",
+ "pred": [
+ 3
+ ],
+ "succ": [
+ 16
+ ],
+ "asm": [
+ "A6|cmps byte ptr DS:[SI],byte ptr ES:[DI]",
+ "9C|pushf",
+ "C3|ret near"
+ ]
+ },
+ {
+ "id": 56,
+ "entry": "F000:1300",
+ "term": "F000:1305",
+ "pred": [
+ 49
+ ],
+ "succ": [
+ 61
+ ],
+ "asm": [
+ "8905|mov word ptr DS:[DI],AX",
+ "8EC2|mov ES,DX",
+ "AF|scas AX,word ptr ES:[DI]",
+ "7449|je short 0x1350"
+ ]
+ },
+ {
+ "id": 16,
+ "entry": "F000:0046",
+ "term": "F000:0048",
+ "pred": [
+ 12
+ ],
+ "succ": [
+ 20
+ ],
+ "asm": [
+ "A6|cmps byte ptr DS:[SI],byte ptr ES:[DI]",
+ "9C|pushf",
+ "C3|ret near"
+ ]
+ },
+ {
+ "id": 49,
+ "entry": "F000:80FF",
+ "term": "F000:8107",
+ "pred": [
+ 44
+ ],
+ "succ": [
+ 56
+ ],
+ "asm": [
+ "BF0220|mov DI,0x2002",
+ "8EC1|mov ES,CX",
+ "FC|cld",
+ "AE|scas AL,byte ptr ES:[DI]",
+ "9F|lahf",
+ "FFE0|jmp near AX"
+ ]
+ },
+ {
+ "id": 20,
+ "entry": "F000:0082",
+ "term": "F000:0084",
+ "pred": [
+ 16
+ ],
+ "succ": [
+ 24
+ ],
+ "asm": [
+ "A6|cmps byte ptr DS:[SI],byte ptr ES:[DI]",
+ "9C|pushf",
+ "C3|ret near"
+ ]
+ },
+ {
+ "id": 44,
+ "entry": "F000:80C2",
+ "term": "F000:80C7",
+ "pred": [
+ 36
+ ],
+ "succ": [
+ 49
+ ],
+ "asm": [
+ "8ED9|mov DS,CX",
+ "A5|movs word ptr ES:[DI],word ptr DS:[SI]",
+ "8EDA|mov DS,DX",
+ "FF6502|jmp near word ptr DS:[DI\u002B2]"
+ ]
+ },
+ {
+ "id": 24,
+ "entry": "F000:0812",
+ "term": "F000:0814",
+ "pred": [
+ 20
+ ],
+ "succ": [
+ 28
+ ],
+ "asm": [
+ "A7|cmps word ptr DS:[SI],word ptr ES:[DI]",
+ "9C|pushf",
+ "C3|ret near"
+ ]
+ },
+ {
+ "id": 36,
+ "entry": "F000:C200",
+ "term": "F000:C20E",
+ "pred": [
+ 33
+ ],
+ "succ": [
+ 44
+ ],
+ "asm": [
+ "BA0010|mov DX,0x1000",
+ "8EC2|mov ES,DX",
+ "BFFFFF|mov DI,0xFFFF",
+ "A4|movs byte ptr ES:[DI],byte ptr DS:[SI]",
+ "8EDA|mov DS,DX",
+ "C605C2|mov byte ptr DS:[DI],0xC2",
+ "FF25|jmp near word ptr DS:[DI]"
+ ]
+ },
+ {
+ "id": 28,
+ "entry": "F000:0883",
+ "term": "F000:0887",
+ "pred": [
+ 24
+ ],
+ "succ": [
+ 33
+ ],
+ "asm": [
+ "B410|mov AH,0x10",
+ "FD|std",
+ "AC|lods AL,byte ptr DS:[SI]",
+ "FFE0|jmp near AX"
+ ]
+ },
+ {
+ "id": 33,
+ "entry": "F000:10C2",
+ "term": "F000:10C3",
+ "pred": [
+ 28
+ ],
+ "succ": [
+ 36
+ ],
+ "asm": [
+ "AD|lods AX,word ptr DS:[SI]",
+ "FFE0|jmp near AX"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/sub.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/sub.json
new file mode 100644
index 0000000000..e8aed12785
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/sub.json
@@ -0,0 +1,179 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:01A8",
+ "lastExecutedBlockId": 3,
+ "blocks": [
+ {
+ "id": 3,
+ "entry": "F000:0000",
+ "term": "F000:01A8",
+ "pred": [
+ 2
+ ],
+ "succ": [],
+ "asm": [
+ "BCA000|mov SP,0x00A0",
+ "B80100|mov AX,1",
+ "BB0200|mov BX,2",
+ "29D8|sub AX,BX",
+ "A30000|mov word ptr DS:[0],AX",
+ "891E0200|mov word ptr DS:[2],BX",
+ "9C|pushf",
+ "BAFFFF|mov DX,0xFFFF",
+ "C7060400FFFF|mov word ptr DS:[4],0xFFFF",
+ "29160400|sub word ptr DS:[4],DX",
+ "89160600|mov word ptr DS:[6],DX",
+ "9C|pushf",
+ "B9FFFF|mov CX,0xFFFF",
+ "C70608000100|mov word ptr DS:[8],1",
+ "2B0E0800|sub CX,word ptr DS:[8]",
+ "890E0A00|mov word ptr DS:[0x000A],CX",
+ "9C|pushf",
+ "B80080|mov AX,0x8000",
+ "83E801|sub AX,1",
+ "A30C00|mov word ptr DS:[0x000C],AX",
+ "9C|pushf",
+ "BD0080|mov BP,0x8000",
+ "83EDFF|sub BP,-1",
+ "892E0E00|mov word ptr DS:[0x000E],BP",
+ "9C|pushf",
+ "BE817F|mov SI,0x7F81",
+ "81EE3C90|sub SI,0x903C",
+ "89361000|mov word ptr DS:[0x0010],SI",
+ "9C|pushf",
+ "C7061200C3EF|mov word ptr DS:[0x0012],0xEFC3",
+ "812E120064C6|sub word ptr DS:[0x0012],0xC664",
+ "9C|pushf",
+ "C706140033E9|mov word ptr DS:[0x0014],0xE933",
+ "832E140064|sub word ptr DS:[0x0014],0x0064",
+ "9C|pushf",
+ "C606160001|mov byte ptr DS:[0x0016],1",
+ "802E160002|sub byte ptr DS:[0x0016],2",
+ "9C|pushf",
+ "B6FF|mov DH,0xFF",
+ "80EEFF|sub DH,0xFF",
+ "89161700|mov word ptr DS:[0x0017],DX",
+ "9C|pushf",
+ "B0FF|mov AL,0xFF",
+ "2C01|sub AL,1",
+ "A31900|mov word ptr DS:[0x0019],AX",
+ "9C|pushf",
+ "C6061B0080|mov byte ptr DS:[0x001B],0x80",
+ "B501|mov CH,1",
+ "2A2E1B00|sub CH,byte ptr DS:[0x001B]",
+ "890E1C00|mov word ptr DS:[0x001C],CX",
+ "9C|pushf",
+ "B380|mov BL,0x80",
+ "C6061E007F|mov byte ptr DS:[0x001E],0x7F",
+ "281E1E00|sub byte ptr DS:[0x001E],BL",
+ "891E1F00|mov word ptr DS:[0x001F],BX",
+ "9C|pushf",
+ "B0BC|mov AL,0xBC",
+ "B48E|mov AH,0x8E",
+ "28C4|sub AH,AL",
+ "A32100|mov word ptr DS:[0x0021],AX",
+ "9C|pushf",
+ "B80100|mov AX,1",
+ "BB0200|mov BX,2",
+ "19C3|sbb BX,AX",
+ "A32300|mov word ptr DS:[0x0023],AX",
+ "891E2500|mov word ptr DS:[0x0025],BX",
+ "9C|pushf",
+ "BAFFFF|mov DX,0xFFFF",
+ "C7062700FFFF|mov word ptr DS:[0x0027],0xFFFF",
+ "19162700|sbb word ptr DS:[0x0027],DX",
+ "89162900|mov word ptr DS:[0x0029],DX",
+ "9C|pushf",
+ "B9FFFF|mov CX,0xFFFF",
+ "C7062B000100|mov word ptr DS:[0x002B],1",
+ "1B0E2B00|sbb CX,word ptr DS:[0x002B]",
+ "890E2D00|mov word ptr DS:[0x002D],CX",
+ "9C|pushf",
+ "B80080|mov AX,0x8000",
+ "83D801|sbb AX,1",
+ "A32F00|mov word ptr DS:[0x002F],AX",
+ "9C|pushf",
+ "BD0080|mov BP,0x8000",
+ "83DDFF|sbb BP,-1",
+ "892E3100|mov word ptr DS:[0x0031],BP",
+ "9C|pushf",
+ "BEC352|mov SI,0x52C3",
+ "81DE48E2|sbb SI,0xE248",
+ "89363300|mov word ptr DS:[0x0033],SI",
+ "9C|pushf",
+ "C70635004CE7|mov word ptr DS:[0x0035],0xE74C",
+ "811E3500C022|sbb word ptr DS:[0x0035],0x22C0",
+ "9C|pushf",
+ "C706370085FD|mov word ptr DS:[0x0037],0xFD85",
+ "831E3700F5|sbb word ptr DS:[0x0037],-11",
+ "9C|pushf",
+ "C606390001|mov byte ptr DS:[0x0039],1",
+ "801E390002|sbb byte ptr DS:[0x0039],2",
+ "9C|pushf",
+ "B6FF|mov DH,0xFF",
+ "80DEFF|sbb DH,0xFF",
+ "89163A00|mov word ptr DS:[0x003A],DX",
+ "9C|pushf",
+ "B0FF|mov AL,0xFF",
+ "1C01|sbb AL,1",
+ "A33C00|mov word ptr DS:[0x003C],AX",
+ "9C|pushf",
+ "C6063E0080|mov byte ptr DS:[0x003E],0x80",
+ "B501|mov CH,1",
+ "1A2E3E00|sbb CH,byte ptr DS:[0x003E]",
+ "890E3F00|mov word ptr DS:[0x003F],CX",
+ "9C|pushf",
+ "B380|mov BL,0x80",
+ "C6064100FF|mov byte ptr DS:[0x0041],0xFF",
+ "181E4100|sbb byte ptr DS:[0x0041],BL",
+ "891E4200|mov word ptr DS:[0x0042],BX",
+ "9C|pushf",
+ "B0B9|mov AL,0xB9",
+ "B4D3|mov AH,0xD3",
+ "18C4|sbb AH,AL",
+ "A34400|mov word ptr DS:[0x0044],AX",
+ "9C|pushf",
+ "BF0000|mov DI,0",
+ "4F|dec DI",
+ "893E4600|mov word ptr DS:[0x0046],DI",
+ "9C|pushf",
+ "BD0080|mov BP,0x8000",
+ "FFCD|dec BP",
+ "892E4800|mov word ptr DS:[0x0048],BP",
+ "9C|pushf",
+ "C7064A001274|mov word ptr DS:[0x004A],0x7412",
+ "FF0E4A00|dec word ptr DS:[0x004A]",
+ "9C|pushf",
+ "B200|mov DL,0",
+ "FECA|dec DL",
+ "89164C00|mov word ptr DS:[0x004C],DX",
+ "9C|pushf",
+ "C6064D0080|mov byte ptr DS:[0x004D],0x80",
+ "FE0E4D00|dec byte ptr DS:[0x004D]",
+ "9C|pushf",
+ "C6064E00B5|mov byte ptr DS:[0x004E],0xB5",
+ "FE0E4E00|dec byte ptr DS:[0x004E]",
+ "9C|pushf",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EB0E|jmp short 0"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/test386.json b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/test386.json
new file mode 100644
index 0000000000..a67d2fbb44
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedCfgBlocks/test386.json
@@ -0,0 +1,6024 @@
+{
+ "currentContextDepth": 1,
+ "currentContextEntryPoint": "F000:FFF0",
+ "totalEntryPoints": 1,
+ "entryPointAddresses": [
+ "F000:FFF0"
+ ],
+ "lastExecutedAddress": "F000:15C4",
+ "lastExecutedBlockId": 1665,
+ "blocks": [
+ {
+ "id": 1665,
+ "entry": "F000:15C3",
+ "term": "F000:15C4",
+ "pred": [
+ 1655
+ ],
+ "succ": [],
+ "asm": [
+ "FA|cli",
+ "F4|hlt"
+ ]
+ },
+ {
+ "id": 2,
+ "entry": "F000:FFF0",
+ "term": "F000:FFF0",
+ "pred": [],
+ "succ": [
+ 3
+ ],
+ "asm": [
+ "EA450000F0|jmp far F000:0045"
+ ]
+ },
+ {
+ "id": 1655,
+ "entry": "F000:15AF",
+ "term": "F000:15C2",
+ "pred": [
+ 1652
+ ],
+ "succ": [
+ 1665
+ ],
+ "asm": [
+ "8EC2|mov ES,DX",
+ "8EE9|mov GS,CX",
+ "BA0024|mov DX,0x2400",
+ "8EDA|mov DS,DX",
+ "BA0064|mov DX,0x6400",
+ "8EC2|mov ES,DX",
+ "B0FF|mov AL,0xFF",
+ "BA9909|mov DX,0x0999",
+ "EE|out DX,AL"
+ ]
+ },
+ {
+ "id": 3,
+ "entry": "F000:0045",
+ "term": "F000:0051",
+ "pred": [
+ 2
+ ],
+ "succ": [
+ 14
+ ],
+ "asm": [
+ "FA|cli",
+ "B000|mov AL,0",
+ "BA9909|mov DX,0x0999",
+ "EE|out DX,AL",
+ "6631C0|xor EAX,EAX",
+ "8ED8|mov DS,AX",
+ "B91100|mov CX,0x0011"
+ ]
+ },
+ {
+ "id": 1652,
+ "entry": "F000:15A6",
+ "term": "F000:15AD",
+ "pred": [
+ 1644
+ ],
+ "succ": [
+ 1655
+ ],
+ "asm": [
+ "6681FB78563412|cmp EBX,0x12345678",
+ "7518|jne short 0x15C7"
+ ]
+ },
+ {
+ "id": 14,
+ "entry": "F000:0054",
+ "term": "F000:0069",
+ "pred": [
+ 3,
+ 14
+ ],
+ "succ": [
+ 14,
+ 16
+ ],
+ "asm": [
+ "67C7048500000000C715|mov word ptr DS:[4*EAX],0x15C7",
+ "67C704850200000000F0|mov word ptr DS:[2\u002B4*EAX],0xF000",
+ "40|inc AX",
+ "E2E9|loop 0x0054"
+ ]
+ },
+ {
+ "id": 1644,
+ "entry": "F000:158A",
+ "term": "F000:15A4",
+ "pred": [
+ 1641
+ ],
+ "succ": [
+ 1652
+ ],
+ "asm": [
+ "8EC2|mov ES,DX",
+ "2666C70578563412|mov dword ptr ES:[DI],0x12345678",
+ "26C74504DEBC|mov word ptr ES:[DI\u002B4],0xBCDE",
+ "26660FB51D|lgs EBX,dword ptr ES:[DI]",
+ "8CE8|mov AX,GS",
+ "3DDEBC|cmp AX,0xBCDE",
+ "7521|jne short 0x15C7"
+ ]
+ },
+ {
+ "id": 16,
+ "entry": "F000:006B",
+ "term": "F000:0086",
+ "pred": [
+ 14
+ ],
+ "succ": [
+ 30
+ ],
+ "asm": [
+ "B80010|mov AX,0x1000",
+ "8ED0|mov SS,AX",
+ "BCFFFF|mov SP,0xFFFF",
+ "BA0020|mov DX,0x2000",
+ "8EDA|mov DS,DX",
+ "BA0060|mov DX,0x6000",
+ "8EC2|mov ES,DX",
+ "B001|mov AL,1",
+ "BA9909|mov DX,0x0999",
+ "EE|out DX,AL",
+ "B401|mov AH,1",
+ "9E|sahf",
+ "7324|jae short 0x00AC"
+ ]
+ },
+ {
+ "id": 1641,
+ "entry": "F000:1584",
+ "term": "F000:1588",
+ "pred": [
+ 1630
+ ],
+ "succ": [
+ 1644
+ ],
+ "asm": [
+ "81FB3412|cmp BX,0x1234",
+ "753D|jne short 0x15C7"
+ ]
+ },
+ {
+ "id": 30,
+ "entry": "F000:0088",
+ "term": "F000:0088",
+ "pred": [
+ 16
+ ],
+ "succ": [
+ 32
+ ],
+ "asm": [
+ "7223|jb short 0x00AD"
+ ]
+ },
+ {
+ "id": 1630,
+ "entry": "F000:1566",
+ "term": "F000:1582",
+ "pred": [
+ 1627
+ ],
+ "succ": [
+ 1641
+ ],
+ "asm": [
+ "8EC2|mov ES,DX",
+ "8EE1|mov FS,CX",
+ "8CE9|mov CX,GS",
+ "8CC2|mov DX,ES",
+ "26C7053412|mov word ptr ES:[DI],0x1234",
+ "26C74502CDAB|mov word ptr ES:[DI\u002B2],0xABCD",
+ "260FB51D|lgs BX,word ptr ES:[DI]",
+ "8CE8|mov AX,GS",
+ "3DCDAB|cmp AX,0xABCD",
+ "7543|jne short 0x15C7"
+ ]
+ },
+ {
+ "id": 32,
+ "entry": "F000:00AD",
+ "term": "F000:00AD",
+ "pred": [
+ 30
+ ],
+ "succ": [
+ 34
+ ],
+ "asm": [
+ "72DC|jb short 0x008B"
+ ]
+ },
+ {
+ "id": 1627,
+ "entry": "F000:155D",
+ "term": "F000:1564",
+ "pred": [
+ 1619
+ ],
+ "succ": [
+ 1630
+ ],
+ "asm": [
+ "6681FB78563412|cmp EBX,0x12345678",
+ "7561|jne short 0x15C7"
+ ]
+ },
+ {
+ "id": 34,
+ "entry": "F000:008B",
+ "term": "F000:008E",
+ "pred": [
+ 32
+ ],
+ "succ": [
+ 38
+ ],
+ "asm": [
+ "B440|mov AH,0x40",
+ "9E|sahf",
+ "751C|jne short 0x00AC"
+ ]
+ },
+ {
+ "id": 1619,
+ "entry": "F000:1541",
+ "term": "F000:155B",
+ "pred": [
+ 1616
+ ],
+ "succ": [
+ 1627
+ ],
+ "asm": [
+ "8EC2|mov ES,DX",
+ "2666C70578563412|mov dword ptr ES:[DI],0x12345678",
+ "26C74504DEBC|mov word ptr ES:[DI\u002B4],0xBCDE",
+ "26660FB41D|lfs EBX,dword ptr ES:[DI]",
+ "8CE0|mov AX,FS",
+ "3DDEBC|cmp AX,0xBCDE",
+ "756A|jne short 0x15C7"
+ ]
+ },
+ {
+ "id": 38,
+ "entry": "F000:0090",
+ "term": "F000:0090",
+ "pred": [
+ 34
+ ],
+ "succ": [
+ 40
+ ],
+ "asm": [
+ "741D|je short 0x00AF"
+ ]
+ },
+ {
+ "id": 1616,
+ "entry": "F000:1539",
+ "term": "F000:153D",
+ "pred": [
+ 1605
+ ],
+ "succ": [
+ 1619
+ ],
+ "asm": [
+ "81FB3412|cmp BX,0x1234",
+ "0F858600|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 40,
+ "entry": "F000:00AF",
+ "term": "F000:00AF",
+ "pred": [
+ 38
+ ],
+ "succ": [
+ 42
+ ],
+ "asm": [
+ "74E2|je short 0x0093"
+ ]
+ },
+ {
+ "id": 1605,
+ "entry": "F000:1519",
+ "term": "F000:1535",
+ "pred": [
+ 1602
+ ],
+ "succ": [
+ 1616
+ ],
+ "asm": [
+ "8EC2|mov ES,DX",
+ "8EC1|mov ES,CX",
+ "8CE1|mov CX,FS",
+ "8CC2|mov DX,ES",
+ "26C7053412|mov word ptr ES:[DI],0x1234",
+ "26C74502CDAB|mov word ptr ES:[DI\u002B2],0xABCD",
+ "260FB41D|lfs BX,word ptr ES:[DI]",
+ "8CE0|mov AX,FS",
+ "3DCDAB|cmp AX,0xABCD",
+ "0F858E00|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 42,
+ "entry": "F000:0093",
+ "term": "F000:0096",
+ "pred": [
+ 40
+ ],
+ "succ": [
+ 46
+ ],
+ "asm": [
+ "B404|mov AH,4",
+ "9E|sahf",
+ "7B14|jnp short 0x00AC"
+ ]
+ },
+ {
+ "id": 1602,
+ "entry": "F000:150E",
+ "term": "F000:1515",
+ "pred": [
+ 1594
+ ],
+ "succ": [
+ 1605
+ ],
+ "asm": [
+ "6681FB78563412|cmp EBX,0x12345678",
+ "0F85AE00|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 46,
+ "entry": "F000:0098",
+ "term": "F000:0098",
+ "pred": [
+ 42
+ ],
+ "succ": [
+ 48
+ ],
+ "asm": [
+ "7A17|jp short 0x00B1"
+ ]
+ },
+ {
+ "id": 1594,
+ "entry": "F000:14F1",
+ "term": "F000:150A",
+ "pred": [
+ 1591
+ ],
+ "succ": [
+ 1602
+ ],
+ "asm": [
+ "8EC2|mov ES,DX",
+ "2666C70578563412|mov dword ptr ES:[DI],0x12345678",
+ "26C74504DEBC|mov word ptr ES:[DI\u002B4],0xBCDE",
+ "2666C41D|les EBX,dword ptr ES:[DI]",
+ "8CC0|mov AX,ES",
+ "3DDEBC|cmp AX,0xBCDE",
+ "0F85B900|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 48,
+ "entry": "F000:00B1",
+ "term": "F000:00B1",
+ "pred": [
+ 46
+ ],
+ "succ": [
+ 50
+ ],
+ "asm": [
+ "7AE8|jp short 0x009B"
+ ]
+ },
+ {
+ "id": 1591,
+ "entry": "F000:14E9",
+ "term": "F000:14ED",
+ "pred": [
+ 1580
+ ],
+ "succ": [
+ 1594
+ ],
+ "asm": [
+ "81FB3412|cmp BX,0x1234",
+ "0F85D600|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 50,
+ "entry": "F000:009B",
+ "term": "F000:009E",
+ "pred": [
+ 48
+ ],
+ "succ": [
+ 54
+ ],
+ "asm": [
+ "B480|mov AH,0x80",
+ "9E|sahf",
+ "790C|jns short 0x00AC"
+ ]
+ },
+ {
+ "id": 1580,
+ "entry": "F000:14CA",
+ "term": "F000:14E5",
+ "pred": [
+ 1577
+ ],
+ "succ": [
+ 1591
+ ],
+ "asm": [
+ "8EC2|mov ES,DX",
+ "8ED9|mov DS,CX",
+ "8CC1|mov CX,ES",
+ "8CC2|mov DX,ES",
+ "26C7053412|mov word ptr ES:[DI],0x1234",
+ "26C74502CDAB|mov word ptr ES:[DI\u002B2],0xABCD",
+ "26C41D|les BX,word ptr ES:[DI]",
+ "8CC0|mov AX,ES",
+ "3DCDAB|cmp AX,0xABCD",
+ "0F85DE00|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 54,
+ "entry": "F000:00A0",
+ "term": "F000:00A0",
+ "pred": [
+ 50
+ ],
+ "succ": [
+ 56
+ ],
+ "asm": [
+ "7811|js short 0x00B3"
+ ]
+ },
+ {
+ "id": 1577,
+ "entry": "F000:14BF",
+ "term": "F000:14C6",
+ "pred": [
+ 1569
+ ],
+ "succ": [
+ 1580
+ ],
+ "asm": [
+ "6681FB78563412|cmp EBX,0x12345678",
+ "0F85FD00|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 56,
+ "entry": "F000:00B3",
+ "term": "F000:00B3",
+ "pred": [
+ 54
+ ],
+ "succ": [
+ 58
+ ],
+ "asm": [
+ "78EE|js short 0x00A3"
+ ]
+ },
+ {
+ "id": 1569,
+ "entry": "F000:14A2",
+ "term": "F000:14BB",
+ "pred": [
+ 1566
+ ],
+ "succ": [
+ 1577
+ ],
+ "asm": [
+ "8EC2|mov ES,DX",
+ "2666C70578563412|mov dword ptr ES:[DI],0x12345678",
+ "26C74504DEBC|mov word ptr ES:[DI\u002B4],0xBCDE",
+ "2666C51D|lds EBX,dword ptr ES:[DI]",
+ "8CD8|mov AX,DS",
+ "3DDEBC|cmp AX,0xBCDE",
+ "0F850801|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 58,
+ "entry": "F000:00A3",
+ "term": "F000:00A6",
+ "pred": [
+ 56
+ ],
+ "succ": [
+ 62
+ ],
+ "asm": [
+ "B441|mov AH,0x41",
+ "9E|sahf",
+ "7704|ja short 0x00AC"
+ ]
+ },
+ {
+ "id": 1566,
+ "entry": "F000:149A",
+ "term": "F000:149E",
+ "pred": [
+ 1555
+ ],
+ "succ": [
+ 1569
+ ],
+ "asm": [
+ "81FB3412|cmp BX,0x1234",
+ "0F852501|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 62,
+ "entry": "F000:00A8",
+ "term": "F000:00A8",
+ "pred": [
+ 58
+ ],
+ "succ": [
+ 64
+ ],
+ "asm": [
+ "760B|jbe short 0x00B5"
+ ]
+ },
+ {
+ "id": 1555,
+ "entry": "F000:147B",
+ "term": "F000:1496",
+ "pred": [
+ 1552
+ ],
+ "succ": [
+ 1566
+ ],
+ "asm": [
+ "8EC2|mov ES,DX",
+ "8ED1|mov SS,CX",
+ "8CD9|mov CX,DS",
+ "8CC2|mov DX,ES",
+ "26C7053412|mov word ptr ES:[DI],0x1234",
+ "26C74502CDAB|mov word ptr ES:[DI\u002B2],0xABCD",
+ "26C51D|lds BX,word ptr ES:[DI]",
+ "8CD8|mov AX,DS",
+ "3DCDAB|cmp AX,0xABCD",
+ "0F852D01|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 64,
+ "entry": "F000:00B5",
+ "term": "F000:00B5",
+ "pred": [
+ 62
+ ],
+ "succ": [
+ 66
+ ],
+ "asm": [
+ "76F3|jbe short 0x00AA"
+ ]
+ },
+ {
+ "id": 1552,
+ "entry": "F000:1470",
+ "term": "F000:1477",
+ "pred": [
+ 1544
+ ],
+ "succ": [
+ 1555
+ ],
+ "asm": [
+ "6681FB78563412|cmp EBX,0x12345678",
+ "0F854C01|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 66,
+ "entry": "F000:00AA",
+ "term": "F000:00AA",
+ "pred": [
+ 64
+ ],
+ "succ": [
+ 68
+ ],
+ "asm": [
+ "EB0B|jmp short 0x00B7"
+ ]
+ },
+ {
+ "id": 1544,
+ "entry": "F000:1452",
+ "term": "F000:146C",
+ "pred": [
+ 1541
+ ],
+ "succ": [
+ 1552
+ ],
+ "asm": [
+ "8EC2|mov ES,DX",
+ "2666C70578563412|mov dword ptr ES:[DI],0x12345678",
+ "26C74504DEBC|mov word ptr ES:[DI\u002B4],0xBCDE",
+ "26660FB21D|lss EBX,dword ptr ES:[DI]",
+ "8CD0|mov AX,SS",
+ "3DDEBC|cmp AX,0xBCDE",
+ "0F855701|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 68,
+ "entry": "F000:00B7",
+ "term": "F000:00BE",
+ "pred": [
+ 66
+ ],
+ "succ": [
+ 74
+ ],
+ "asm": [
+ "B4D4|mov AH,0xD4",
+ "9E|sahf",
+ "B80000|mov AX,0",
+ "9E|sahf",
+ "731B|jae short 0x00DB"
+ ]
+ },
+ {
+ "id": 1541,
+ "entry": "F000:144A",
+ "term": "F000:144E",
+ "pred": [
+ 1524
+ ],
+ "succ": [
+ 1544
+ ],
+ "asm": [
+ "81FB3412|cmp BX,0x1234",
+ "0F857501|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 74,
+ "entry": "F000:00DB",
+ "term": "F000:00DB",
+ "pred": [
+ 68
+ ],
+ "succ": [
+ 76
+ ],
+ "asm": [
+ "73E4|jae short 0x00C1"
+ ]
+ },
+ {
+ "id": 1524,
+ "entry": "F000:141B",
+ "term": "F000:1446",
+ "pred": [
+ 1522
+ ],
+ "succ": [
+ 1541
+ ],
+ "asm": [
+ "BA0023|mov DX,0x2300",
+ "8EDA|mov DS,DX",
+ "BA0063|mov DX,0x6300",
+ "8EC2|mov ES,DX",
+ "B006|mov AL,6",
+ "BA9909|mov DX,0x0999",
+ "EE|out DX,AL",
+ "BF0000|mov DI,0",
+ "8CD1|mov CX,SS",
+ "8CC2|mov DX,ES",
+ "26C7053412|mov word ptr ES:[DI],0x1234",
+ "26C74502CDAB|mov word ptr ES:[DI\u002B2],0xABCD",
+ "260FB21D|lss BX,word ptr ES:[DI]",
+ "8CD0|mov AX,SS",
+ "3DCDAB|cmp AX,0xABCD",
+ "0F857D01|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 76,
+ "entry": "F000:00C1",
+ "term": "F000:00C4",
+ "pred": [
+ 74
+ ],
+ "succ": [
+ 80
+ ],
+ "asm": [
+ "B495|mov AH,0x95",
+ "9E|sahf",
+ "7517|jne short 0x00DD"
+ ]
+ },
+ {
+ "id": 1522,
+ "entry": "F000:1417",
+ "term": "F000:1417",
+ "pred": [
+ 1502,
+ 1517
+ ],
+ "succ": [
+ 1524
+ ],
+ "asm": [
+ "0F83AC01|jae near 0x15C7"
+ ]
+ },
+ {
+ "id": 80,
+ "entry": "F000:00DD",
+ "term": "F000:00DD",
+ "pred": [
+ 76
+ ],
+ "succ": [
+ 82
+ ],
+ "asm": [
+ "75E8|jne short 0x00C7"
+ ]
+ },
+ {
+ "id": 1502,
+ "entry": "F000:13EE",
+ "term": "F000:13F2",
+ "pred": [
+ 1498
+ ],
+ "succ": [
+ 1506,
+ 1522
+ ],
+ "asm": [
+ "83C008|add AX,8",
+ "F9|stc",
+ "66CB|ret far"
+ ]
+ },
+ {
+ "id": 1517,
+ "entry": "F000:1407",
+ "term": "F000:1414",
+ "pred": [
+ 1515
+ ],
+ "succ": [
+ 1498,
+ 1522
+ ],
+ "asm": [
+ "F8|clc",
+ "66C704E5130000|mov dword ptr DS:[SI],0x000013E5",
+ "C7440400F0|mov word ptr DS:[SI\u002B4],0xF000",
+ "66FF1C|call far dword ptr DS:[SI]"
+ ]
+ },
+ {
+ "id": 82,
+ "entry": "F000:00C7",
+ "term": "F000:00CA",
+ "pred": [
+ 80
+ ],
+ "succ": [
+ 86
+ ],
+ "asm": [
+ "B4D1|mov AH,0xD1",
+ "9E|sahf",
+ "7B13|jnp short 0x00DF"
+ ]
+ },
+ {
+ "id": 1506,
+ "entry": "F000:13DF",
+ "term": "F000:13DF",
+ "pred": [
+ 1495,
+ 1502
+ ],
+ "succ": [
+ 1508
+ ],
+ "asm": [
+ "0F83E401|jae near 0x15C7"
+ ]
+ },
+ {
+ "id": 1498,
+ "entry": "F000:13E5",
+ "term": "F000:13EA",
+ "pred": [
+ 1495,
+ 1517
+ ],
+ "succ": [
+ 1502
+ ],
+ "asm": [
+ "83E808|sub AX,8",
+ "39C4|cmp SP,AX",
+ "0F85D901|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 1515,
+ "entry": "F000:1403",
+ "term": "F000:1403",
+ "pred": [
+ 1487,
+ 1510
+ ],
+ "succ": [
+ 1517
+ ],
+ "asm": [
+ "0F83C001|jae near 0x15C7"
+ ]
+ },
+ {
+ "id": 86,
+ "entry": "F000:00DF",
+ "term": "F000:00DF",
+ "pred": [
+ 82
+ ],
+ "succ": [
+ 88
+ ],
+ "asm": [
+ "7BEC|jnp short 0x00CD"
+ ]
+ },
+ {
+ "id": 1508,
+ "entry": "F000:13E3",
+ "term": "F000:13E3",
+ "pred": [
+ 1506
+ ],
+ "succ": [
+ 1510
+ ],
+ "asm": [
+ "EB12|jmp short 0x13F7"
+ ]
+ },
+ {
+ "id": 1495,
+ "entry": "F000:13D6",
+ "term": "F000:13D7",
+ "pred": [
+ 1493
+ ],
+ "succ": [
+ 1498,
+ 1506
+ ],
+ "asm": [
+ "F8|clc",
+ "669AE513000000F0|call far F000:13E5"
+ ]
+ },
+ {
+ "id": 1487,
+ "entry": "F000:13CE",
+ "term": "F000:13D2",
+ "pred": [
+ 1483
+ ],
+ "succ": [
+ 1491,
+ 1515
+ ],
+ "asm": [
+ "83C004|add AX,4",
+ "F9|stc",
+ "CB|ret far"
+ ]
+ },
+ {
+ "id": 1510,
+ "entry": "F000:13F7",
+ "term": "F000:1401",
+ "pred": [
+ 1508
+ ],
+ "succ": [
+ 1483,
+ 1515
+ ],
+ "asm": [
+ "F8|clc",
+ "C704C513|mov word ptr DS:[SI],0x13C5",
+ "C7440200F0|mov word ptr DS:[SI\u002B2],0xF000",
+ "FF1C|call far dword ptr DS:[SI]"
+ ]
+ },
+ {
+ "id": 88,
+ "entry": "F000:00CD",
+ "term": "F000:00D0",
+ "pred": [
+ 86
+ ],
+ "succ": [
+ 92
+ ],
+ "asm": [
+ "B455|mov AH,0x55",
+ "9E|sahf",
+ "790F|jns short 0x00E1"
+ ]
+ },
+ {
+ "id": 1493,
+ "entry": "F000:13C3",
+ "term": "F000:13C3",
+ "pred": [
+ 1491
+ ],
+ "succ": [
+ 1495
+ ],
+ "asm": [
+ "EB11|jmp short 0x13D6"
+ ]
+ },
+ {
+ "id": 1491,
+ "entry": "F000:13BF",
+ "term": "F000:13BF",
+ "pred": [
+ 1479,
+ 1487
+ ],
+ "succ": [
+ 1493
+ ],
+ "asm": [
+ "0F830402|jae near 0x15C7"
+ ]
+ },
+ {
+ "id": 1483,
+ "entry": "F000:13C5",
+ "term": "F000:13CA",
+ "pred": [
+ 1479,
+ 1510
+ ],
+ "succ": [
+ 1487
+ ],
+ "asm": [
+ "83E804|sub AX,4",
+ "39C4|cmp SP,AX",
+ "0F85F901|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 92,
+ "entry": "F000:00E1",
+ "term": "F000:00E1",
+ "pred": [
+ 88
+ ],
+ "succ": [
+ 94
+ ],
+ "asm": [
+ "79F0|jns short 0x00D3"
+ ]
+ },
+ {
+ "id": 1479,
+ "entry": "F000:13B7",
+ "term": "F000:13BA",
+ "pred": [
+ 1477
+ ],
+ "succ": [
+ 1483,
+ 1491
+ ],
+ "asm": [
+ "89E0|mov AX,SP",
+ "F8|clc",
+ "9AC51300F0|call far F000:13C5"
+ ]
+ },
+ {
+ "id": 94,
+ "entry": "F000:00D3",
+ "term": "F000:00D6",
+ "pred": [
+ 92
+ ],
+ "succ": [
+ 98
+ ],
+ "asm": [
+ "B494|mov AH,0x94",
+ "9E|sahf",
+ "770B|ja short 0x00E3"
+ ]
+ },
+ {
+ "id": 1477,
+ "entry": "F000:13B3",
+ "term": "F000:13B3",
+ "pred": [
+ 1459,
+ 1473
+ ],
+ "succ": [
+ 1479
+ ],
+ "asm": [
+ "0F831002|jae near 0x15C7"
+ ]
+ },
+ {
+ "id": 98,
+ "entry": "F000:00E3",
+ "term": "F000:00E3",
+ "pred": [
+ 94
+ ],
+ "succ": [
+ 100
+ ],
+ "asm": [
+ "77F4|ja short 0x00D9"
+ ]
+ },
+ {
+ "id": 1459,
+ "entry": "F000:1396",
+ "term": "F000:139A",
+ "pred": [
+ 1455
+ ],
+ "succ": [
+ 1463,
+ 1477
+ ],
+ "asm": [
+ "83C004|add AX,4",
+ "F9|stc",
+ "66C3|ret near"
+ ]
+ },
+ {
+ "id": 1473,
+ "entry": "F000:13A9",
+ "term": "F000:13B0",
+ "pred": [
+ 1471
+ ],
+ "succ": [
+ 1455,
+ 1477
+ ],
+ "asm": [
+ "F8|clc",
+ "66BB8D130000|mov EBX,0x0000138D",
+ "66FFD3|call near EBX"
+ ]
+ },
+ {
+ "id": 100,
+ "entry": "F000:00D9",
+ "term": "F000:00D9",
+ "pred": [
+ 98
+ ],
+ "succ": [
+ 102
+ ],
+ "asm": [
+ "EB0A|jmp short 0x00E5"
+ ]
+ },
+ {
+ "id": 1463,
+ "entry": "F000:1387",
+ "term": "F000:1387",
+ "pred": [
+ 1452,
+ 1459
+ ],
+ "succ": [
+ 1465
+ ],
+ "asm": [
+ "0F833C02|jae near 0x15C7"
+ ]
+ },
+ {
+ "id": 1455,
+ "entry": "F000:138D",
+ "term": "F000:1392",
+ "pred": [
+ 1452,
+ 1473
+ ],
+ "succ": [
+ 1459
+ ],
+ "asm": [
+ "83E804|sub AX,4",
+ "39C4|cmp SP,AX",
+ "0F853102|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 1471,
+ "entry": "F000:13A5",
+ "term": "F000:13A5",
+ "pred": [
+ 1444,
+ 1467
+ ],
+ "succ": [
+ 1473
+ ],
+ "asm": [
+ "0F831E02|jae near 0x15C7"
+ ]
+ },
+ {
+ "id": 102,
+ "entry": "F000:00E5",
+ "term": "F000:00EC",
+ "pred": [
+ 100
+ ],
+ "succ": [
+ 108
+ ],
+ "asm": [
+ "B400|mov AH,0",
+ "9E|sahf",
+ "B040|mov AL,0x40",
+ "D0E0|shl AL,1",
+ "7134|jno short 0x0122"
+ ]
+ },
+ {
+ "id": 1465,
+ "entry": "F000:138B",
+ "term": "F000:138B",
+ "pred": [
+ 1463
+ ],
+ "succ": [
+ 1467
+ ],
+ "asm": [
+ "EB12|jmp short 0x139F"
+ ]
+ },
+ {
+ "id": 1452,
+ "entry": "F000:1380",
+ "term": "F000:1381",
+ "pred": [
+ 1450
+ ],
+ "succ": [
+ 1455,
+ 1463
+ ],
+ "asm": [
+ "F8|clc",
+ "66E806000000|call near 0x138D"
+ ]
+ },
+ {
+ "id": 1444,
+ "entry": "F000:1378",
+ "term": "F000:137C",
+ "pred": [
+ 1440
+ ],
+ "succ": [
+ 1448,
+ 1471
+ ],
+ "asm": [
+ "83C002|add AX,2",
+ "F9|stc",
+ "C3|ret near"
+ ]
+ },
+ {
+ "id": 1467,
+ "entry": "F000:139F",
+ "term": "F000:13A3",
+ "pred": [
+ 1465
+ ],
+ "succ": [
+ 1440,
+ 1471
+ ],
+ "asm": [
+ "F8|clc",
+ "BB6F13|mov BX,0x136F",
+ "FFD3|call near BX"
+ ]
+ },
+ {
+ "id": 108,
+ "entry": "F000:00EE",
+ "term": "F000:00EE",
+ "pred": [
+ 102
+ ],
+ "succ": [
+ 110
+ ],
+ "asm": [
+ "7033|jo short 0x0123"
+ ]
+ },
+ {
+ "id": 1450,
+ "entry": "F000:136D",
+ "term": "F000:136D",
+ "pred": [
+ 1448
+ ],
+ "succ": [
+ 1452
+ ],
+ "asm": [
+ "EB11|jmp short 0x1380"
+ ]
+ },
+ {
+ "id": 1448,
+ "entry": "F000:1369",
+ "term": "F000:1369",
+ "pred": [
+ 1428,
+ 1444
+ ],
+ "succ": [
+ 1450
+ ],
+ "asm": [
+ "0F835A02|jae near 0x15C7"
+ ]
+ },
+ {
+ "id": 1440,
+ "entry": "F000:136F",
+ "term": "F000:1374",
+ "pred": [
+ 1428,
+ 1467
+ ],
+ "succ": [
+ 1444
+ ],
+ "asm": [
+ "83E802|sub AX,2",
+ "39C4|cmp SP,AX",
+ "0F854F02|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 110,
+ "entry": "F000:0123",
+ "term": "F000:0123",
+ "pred": [
+ 108
+ ],
+ "succ": [
+ 112
+ ],
+ "asm": [
+ "70CC|jo short 0x00F1"
+ ]
+ },
+ {
+ "id": 1428,
+ "entry": "F000:1350",
+ "term": "F000:1366",
+ "pred": [
+ 1425
+ ],
+ "succ": [
+ 1440,
+ 1448
+ ],
+ "asm": [
+ "BA0022|mov DX,0x2200",
+ "8EDA|mov DS,DX",
+ "BA0062|mov DX,0x6200",
+ "8EC2|mov ES,DX",
+ "B005|mov AL,5",
+ "BA9909|mov DX,0x0999",
+ "EE|out DX,AL",
+ "BE0000|mov SI,0",
+ "89E0|mov AX,SP",
+ "F8|clc",
+ "E80600|call near 0x136F"
+ ]
+ },
+ {
+ "id": 112,
+ "entry": "F000:00F1",
+ "term": "F000:00F1",
+ "pred": [
+ 110
+ ],
+ "succ": [
+ 114
+ ],
+ "asm": [
+ "7C2F|jl short 0x0122"
+ ]
+ },
+ {
+ "id": 1425,
+ "entry": "F000:1345",
+ "term": "F000:134C",
+ "pred": [
+ 1422
+ ],
+ "succ": [
+ 1428
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F857702|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 114,
+ "entry": "F000:00F3",
+ "term": "F000:00F3",
+ "pred": [
+ 112
+ ],
+ "succ": [
+ 116
+ ],
+ "asm": [
+ "7D30|jge short 0x0125"
+ ]
+ },
+ {
+ "id": 1422,
+ "entry": "F000:133A",
+ "term": "F000:1341",
+ "pred": [
+ 1415
+ ],
+ "succ": [
+ 1425
+ ],
+ "asm": [
+ "6681FEFFFF0100|cmp ESI,0x0001FFFF",
+ "0F858202|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 116,
+ "entry": "F000:0125",
+ "term": "F000:0125",
+ "pred": [
+ 114
+ ],
+ "succ": [
+ 118
+ ],
+ "asm": [
+ "7DCF|jge short 0x00F6"
+ ]
+ },
+ {
+ "id": 1415,
+ "entry": "F000:131D",
+ "term": "F000:1336",
+ "pred": [
+ 1412
+ ],
+ "succ": [
+ 1422
+ ],
+ "asm": [
+ "66B940000000|mov ECX,0x00000040",
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66BEFF000100|mov ESI,0x000100FF",
+ "F366A7|repe cmps dword ptr DS:[SI],dword ptr ES:[DI]",
+ "6683F900|cmp ECX,0",
+ "0F858D02|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 118,
+ "entry": "F000:00F6",
+ "term": "F000:00F6",
+ "pred": [
+ 116
+ ],
+ "succ": [
+ 120
+ ],
+ "asm": [
+ "7E2A|jle short 0x0122"
+ ]
+ },
+ {
+ "id": 1412,
+ "entry": "F000:1312",
+ "term": "F000:1319",
+ "pred": [
+ 1409
+ ],
+ "succ": [
+ 1415
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F85AA02|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 120,
+ "entry": "F000:00F8",
+ "term": "F000:00F8",
+ "pred": [
+ 118
+ ],
+ "succ": [
+ 122
+ ],
+ "asm": [
+ "7F2D|jg short 0x0127"
+ ]
+ },
+ {
+ "id": 1409,
+ "entry": "F000:1307",
+ "term": "F000:130E",
+ "pred": [
+ 1399
+ ],
+ "succ": [
+ 1412
+ ],
+ "asm": [
+ "6681FEFFFF0100|cmp ESI,0x0001FFFF",
+ "0F85B502|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 122,
+ "entry": "F000:0127",
+ "term": "F000:0127",
+ "pred": [
+ 120
+ ],
+ "succ": [
+ 124
+ ],
+ "asm": [
+ "7FD2|jg short 0x00FB"
+ ]
+ },
+ {
+ "id": 1399,
+ "entry": "F000:12DB",
+ "term": "F000:1303",
+ "pred": [
+ 1396
+ ],
+ "succ": [
+ 1409
+ ],
+ "asm": [
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66B800000000|mov EAX,0",
+ "66B940000000|mov ECX,0x00000040",
+ "F366AB|rep stos dword ptr ES:[DI],EAX",
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66B940000000|mov ECX,0x00000040",
+ "F366A5|rep movs dword ptr ES:[DI],dword ptr DS:[SI]",
+ "6683F900|cmp ECX,0",
+ "0F85C002|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 124,
+ "entry": "F000:00FB",
+ "term": "F000:00FE",
+ "pred": [
+ 122
+ ],
+ "succ": [
+ 128
+ ],
+ "asm": [
+ "B440|mov AH,0x40",
+ "9E|sahf",
+ "7C29|jl short 0x0129"
+ ]
+ },
+ {
+ "id": 1396,
+ "entry": "F000:12D0",
+ "term": "F000:12D7",
+ "pred": [
+ 1389
+ ],
+ "succ": [
+ 1399
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F85EC02|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 128,
+ "entry": "F000:0129",
+ "term": "F000:0129",
+ "pred": [
+ 124
+ ],
+ "succ": [
+ 130
+ ],
+ "asm": [
+ "7CD6|jl short 0x0101"
+ ]
+ },
+ {
+ "id": 1389,
+ "entry": "F000:12B3",
+ "term": "F000:12CC",
+ "pred": [
+ 1386
+ ],
+ "succ": [
+ 1396
+ ],
+ "asm": [
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66BEFF000100|mov ESI,0x000100FF",
+ "66B940000000|mov ECX,0x00000040",
+ "F366AF|repe scas EAX,dword ptr ES:[DI]",
+ "6683F900|cmp ECX,0",
+ "0F85F702|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 130,
+ "entry": "F000:0101",
+ "term": "F000:0101",
+ "pred": [
+ 128
+ ],
+ "succ": [
+ 132
+ ],
+ "asm": [
+ "7E28|jle short 0x012B"
+ ]
+ },
+ {
+ "id": 1386,
+ "entry": "F000:12A8",
+ "term": "F000:12AF",
+ "pred": [
+ 1383
+ ],
+ "succ": [
+ 1389
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F851403|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 132,
+ "entry": "F000:012B",
+ "term": "F000:012B",
+ "pred": [
+ 130
+ ],
+ "succ": [
+ 134
+ ],
+ "asm": [
+ "7ED7|jle short 0x0104"
+ ]
+ },
+ {
+ "id": 1383,
+ "entry": "F000:129D",
+ "term": "F000:12A4",
+ "pred": [
+ 1368
+ ],
+ "succ": [
+ 1386
+ ],
+ "asm": [
+ "6681FEFFFF0100|cmp ESI,0x0001FFFF",
+ "0F851F03|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 134,
+ "entry": "F000:0104",
+ "term": "F000:010A",
+ "pred": [
+ 132
+ ],
+ "succ": [
+ 137
+ ],
+ "asm": [
+ "66B901000000|mov ECX,1",
+ "E316|jcxz short 0x0122"
+ ]
+ },
+ {
+ "id": 1368,
+ "entry": "F000:126A",
+ "term": "F000:1299",
+ "pred": [
+ 1365
+ ],
+ "succ": [
+ 1383
+ ],
+ "asm": [
+ "66BFFF000100|mov EDI,0x000100FF",
+ "8CC2|mov DX,ES",
+ "8CD9|mov CX,DS",
+ "87D1|xchg DX,CX",
+ "8EC2|mov ES,DX",
+ "8ED9|mov DS,CX",
+ "6687FE|xchg EDI,ESI",
+ "66B940000000|mov ECX,0x00000040",
+ "F366AB|rep stos dword ptr ES:[DI],EAX",
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66B940000000|mov ECX,0x00000040",
+ "F366A7|repe cmps dword ptr DS:[SI],dword ptr ES:[DI]",
+ "6683F900|cmp ECX,0",
+ "0F852A03|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 137,
+ "entry": "F000:010C",
+ "term": "F000:0112",
+ "pred": [
+ 134
+ ],
+ "succ": [
+ 140
+ ],
+ "asm": [
+ "66B900000100|mov ECX,0x00010000",
+ "E319|jcxz short 0x012D"
+ ]
+ },
+ {
+ "id": 1365,
+ "entry": "F000:125F",
+ "term": "F000:1266",
+ "pred": [
+ 1353
+ ],
+ "succ": [
+ 1368
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F855D03|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 140,
+ "entry": "F000:012D",
+ "term": "F000:012D",
+ "pred": [
+ 137
+ ],
+ "succ": [
+ 142
+ ],
+ "asm": [
+ "E3E5|jcxz short 0x0114"
+ ]
+ },
+ {
+ "id": 1353,
+ "entry": "F000:1229",
+ "term": "F000:125B",
+ "pred": [
+ 1350
+ ],
+ "succ": [
+ 1365
+ ],
+ "asm": [
+ "FD|std",
+ "66B878563412|mov EAX,0x12345678",
+ "66BEFF000100|mov ESI,0x000100FF",
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66B878563412|mov EAX,0x12345678",
+ "66BEFF000100|mov ESI,0x000100FF",
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66B940000000|mov ECX,0x00000040",
+ "F366AB|rep stos dword ptr ES:[DI],EAX",
+ "6683F900|cmp ECX,0",
+ "0F856803|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 142,
+ "entry": "F000:0114",
+ "term": "F000:0114",
+ "pred": [
+ 140
+ ],
+ "succ": [
+ 144
+ ],
+ "asm": [
+ "67E30B|jecxz short 0x0122"
+ ]
+ },
+ {
+ "id": 1350,
+ "entry": "F000:121E",
+ "term": "F000:1225",
+ "pred": [
+ 1347
+ ],
+ "succ": [
+ 1353
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F859E03|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 144,
+ "entry": "F000:0117",
+ "term": "F000:011D",
+ "pred": [
+ 142
+ ],
+ "succ": [
+ 147
+ ],
+ "asm": [
+ "66B900000000|mov ECX,0",
+ "67E30F|jecxz short 0x012F"
+ ]
+ },
+ {
+ "id": 1347,
+ "entry": "F000:1213",
+ "term": "F000:121A",
+ "pred": [
+ 1340
+ ],
+ "succ": [
+ 1350
+ ],
+ "asm": [
+ "6681FEFFFF0100|cmp ESI,0x0001FFFF",
+ "0F85A903|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 147,
+ "entry": "F000:012F",
+ "term": "F000:012F",
+ "pred": [
+ 144
+ ],
+ "succ": [
+ 149
+ ],
+ "asm": [
+ "67E3EE|jecxz short 0x0120"
+ ]
+ },
+ {
+ "id": 1340,
+ "entry": "F000:11F7",
+ "term": "F000:120F",
+ "pred": [
+ 1337
+ ],
+ "succ": [
+ 1347
+ ],
+ "asm": [
+ "66B980000000|mov ECX,0x00000080",
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66BEFF000100|mov ESI,0x000100FF",
+ "F3A7|repe cmps word ptr DS:[SI],word ptr ES:[DI]",
+ "6683F900|cmp ECX,0",
+ "0F85B403|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 149,
+ "entry": "F000:0120",
+ "term": "F000:0120",
+ "pred": [
+ 147
+ ],
+ "succ": [
+ 151
+ ],
+ "asm": [
+ "EB10|jmp short 0x0132"
+ ]
+ },
+ {
+ "id": 1337,
+ "entry": "F000:11EC",
+ "term": "F000:11F3",
+ "pred": [
+ 1334
+ ],
+ "succ": [
+ 1340
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F85D003|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 151,
+ "entry": "F000:0132",
+ "term": "F000:0135",
+ "pred": [
+ 149
+ ],
+ "succ": [
+ 155
+ ],
+ "asm": [
+ "B401|mov AH,1",
+ "9E|sahf",
+ "0F83B700|jae near 0x01F0"
+ ]
+ },
+ {
+ "id": 1334,
+ "entry": "F000:11E1",
+ "term": "F000:11E8",
+ "pred": [
+ 1324
+ ],
+ "succ": [
+ 1337
+ ],
+ "asm": [
+ "6681FEFFFF0100|cmp ESI,0x0001FFFF",
+ "0F85DB03|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 155,
+ "entry": "F000:0139",
+ "term": "F000:0139",
+ "pred": [
+ 151
+ ],
+ "succ": [
+ 157
+ ],
+ "asm": [
+ "0F82B400|jb near 0x01F1"
+ ]
+ },
+ {
+ "id": 1324,
+ "entry": "F000:11B7",
+ "term": "F000:11DD",
+ "pred": [
+ 1321
+ ],
+ "succ": [
+ 1334
+ ],
+ "asm": [
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66B800000000|mov EAX,0",
+ "66B980000000|mov ECX,0x00000080",
+ "F3AB|rep stos word ptr ES:[DI],AX",
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66B980000000|mov ECX,0x00000080",
+ "F3A5|rep movs word ptr ES:[DI],word ptr DS:[SI]",
+ "6683F900|cmp ECX,0",
+ "0F85E603|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 157,
+ "entry": "F000:01F1",
+ "term": "F000:01F1",
+ "pred": [
+ 155
+ ],
+ "succ": [
+ 159
+ ],
+ "asm": [
+ "0F8249FF|jb near 0x013E"
+ ]
+ },
+ {
+ "id": 1321,
+ "entry": "F000:11AC",
+ "term": "F000:11B3",
+ "pred": [
+ 1314
+ ],
+ "succ": [
+ 1324
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F851004|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 159,
+ "entry": "F000:013E",
+ "term": "F000:0141",
+ "pred": [
+ 157
+ ],
+ "succ": [
+ 163
+ ],
+ "asm": [
+ "B440|mov AH,0x40",
+ "9E|sahf",
+ "0F85AB00|jne near 0x01F0"
+ ]
+ },
+ {
+ "id": 1314,
+ "entry": "F000:1190",
+ "term": "F000:11A8",
+ "pred": [
+ 1311
+ ],
+ "succ": [
+ 1321
+ ],
+ "asm": [
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66BEFF000100|mov ESI,0x000100FF",
+ "66B980000000|mov ECX,0x00000080",
+ "F3AF|repe scas AX,word ptr ES:[DI]",
+ "6683F900|cmp ECX,0",
+ "0F851B04|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 163,
+ "entry": "F000:0145",
+ "term": "F000:0145",
+ "pred": [
+ 159
+ ],
+ "succ": [
+ 165
+ ],
+ "asm": [
+ "0F84AC00|je near 0x01F5"
+ ]
+ },
+ {
+ "id": 1311,
+ "entry": "F000:1185",
+ "term": "F000:118C",
+ "pred": [
+ 1308
+ ],
+ "succ": [
+ 1314
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F853704|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 165,
+ "entry": "F000:01F5",
+ "term": "F000:01F5",
+ "pred": [
+ 163
+ ],
+ "succ": [
+ 167
+ ],
+ "asm": [
+ "0F8451FF|je near 0x014A"
+ ]
+ },
+ {
+ "id": 1308,
+ "entry": "F000:117A",
+ "term": "F000:1181",
+ "pred": [
+ 1293
+ ],
+ "succ": [
+ 1311
+ ],
+ "asm": [
+ "6681FEFFFF0100|cmp ESI,0x0001FFFF",
+ "0F854204|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 167,
+ "entry": "F000:014A",
+ "term": "F000:014D",
+ "pred": [
+ 165
+ ],
+ "succ": [
+ 171
+ ],
+ "asm": [
+ "B404|mov AH,4",
+ "9E|sahf",
+ "0F8B9F00|jnp near 0x01F0"
+ ]
+ },
+ {
+ "id": 1293,
+ "entry": "F000:1149",
+ "term": "F000:1176",
+ "pred": [
+ 1290
+ ],
+ "succ": [
+ 1308
+ ],
+ "asm": [
+ "66BFFF000100|mov EDI,0x000100FF",
+ "8CC2|mov DX,ES",
+ "8CD9|mov CX,DS",
+ "87D1|xchg DX,CX",
+ "8EC2|mov ES,DX",
+ "8ED9|mov DS,CX",
+ "6687FE|xchg EDI,ESI",
+ "66B980000000|mov ECX,0x00000080",
+ "F3AB|rep stos word ptr ES:[DI],AX",
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66B980000000|mov ECX,0x00000080",
+ "F3A7|repe cmps word ptr DS:[SI],word ptr ES:[DI]",
+ "6683F900|cmp ECX,0",
+ "0F854D04|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 171,
+ "entry": "F000:0151",
+ "term": "F000:0151",
+ "pred": [
+ 167
+ ],
+ "succ": [
+ 173
+ ],
+ "asm": [
+ "0F8AA400|jp near 0x01F9"
+ ]
+ },
+ {
+ "id": 1290,
+ "entry": "F000:113E",
+ "term": "F000:1145",
+ "pred": [
+ 1278
+ ],
+ "succ": [
+ 1293
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F857E04|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 173,
+ "entry": "F000:01F9",
+ "term": "F000:01F9",
+ "pred": [
+ 171
+ ],
+ "succ": [
+ 175
+ ],
+ "asm": [
+ "0F8A59FF|jp near 0x0156"
+ ]
+ },
+ {
+ "id": 1278,
+ "entry": "F000:1109",
+ "term": "F000:113A",
+ "pred": [
+ 1275
+ ],
+ "succ": [
+ 1290
+ ],
+ "asm": [
+ "FD|std",
+ "66B878563412|mov EAX,0x12345678",
+ "66BEFF000100|mov ESI,0x000100FF",
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66B878563412|mov EAX,0x12345678",
+ "66BEFF000100|mov ESI,0x000100FF",
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66B980000000|mov ECX,0x00000080",
+ "F3AB|rep stos word ptr ES:[DI],AX",
+ "6683F900|cmp ECX,0",
+ "0F858904|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 175,
+ "entry": "F000:0156",
+ "term": "F000:0159",
+ "pred": [
+ 173
+ ],
+ "succ": [
+ 179
+ ],
+ "asm": [
+ "B480|mov AH,0x80",
+ "9E|sahf",
+ "0F899300|jns near 0x01F0"
+ ]
+ },
+ {
+ "id": 1275,
+ "entry": "F000:10FE",
+ "term": "F000:1105",
+ "pred": [
+ 1272
+ ],
+ "succ": [
+ 1278
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F85BE04|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 179,
+ "entry": "F000:015D",
+ "term": "F000:015D",
+ "pred": [
+ 175
+ ],
+ "succ": [
+ 181
+ ],
+ "asm": [
+ "0F889C00|js near 0x01FD"
+ ]
+ },
+ {
+ "id": 1272,
+ "entry": "F000:10F3",
+ "term": "F000:10FA",
+ "pred": [
+ 1265
+ ],
+ "succ": [
+ 1275
+ ],
+ "asm": [
+ "6681FEFFFF0100|cmp ESI,0x0001FFFF",
+ "0F85C904|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 181,
+ "entry": "F000:01FD",
+ "term": "F000:01FD",
+ "pred": [
+ 179
+ ],
+ "succ": [
+ 183
+ ],
+ "asm": [
+ "0F8861FF|js near 0x0162"
+ ]
+ },
+ {
+ "id": 1265,
+ "entry": "F000:10D7",
+ "term": "F000:10EF",
+ "pred": [
+ 1262
+ ],
+ "succ": [
+ 1272
+ ],
+ "asm": [
+ "66B900010000|mov ECX,0x00000100",
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66BEFF000100|mov ESI,0x000100FF",
+ "F3A6|repe cmps byte ptr DS:[SI],byte ptr ES:[DI]",
+ "6683F900|cmp ECX,0",
+ "0F85D404|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 183,
+ "entry": "F000:0162",
+ "term": "F000:0165",
+ "pred": [
+ 181
+ ],
+ "succ": [
+ 187
+ ],
+ "asm": [
+ "B441|mov AH,0x41",
+ "9E|sahf",
+ "0F878700|ja near 0x01F0"
+ ]
+ },
+ {
+ "id": 1262,
+ "entry": "F000:10CC",
+ "term": "F000:10D3",
+ "pred": [
+ 1259
+ ],
+ "succ": [
+ 1265
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F85F004|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 187,
+ "entry": "F000:0169",
+ "term": "F000:0169",
+ "pred": [
+ 183
+ ],
+ "succ": [
+ 189
+ ],
+ "asm": [
+ "0F869400|jbe near 0x0201"
+ ]
+ },
+ {
+ "id": 1259,
+ "entry": "F000:10C1",
+ "term": "F000:10C8",
+ "pred": [
+ 1249
+ ],
+ "succ": [
+ 1262
+ ],
+ "asm": [
+ "6681FEFFFF0100|cmp ESI,0x0001FFFF",
+ "0F85FB04|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 189,
+ "entry": "F000:0201",
+ "term": "F000:0201",
+ "pred": [
+ 187
+ ],
+ "succ": [
+ 191
+ ],
+ "asm": [
+ "0F8668FF|jbe near 0x016D"
+ ]
+ },
+ {
+ "id": 1249,
+ "entry": "F000:1097",
+ "term": "F000:10BD",
+ "pred": [
+ 1246
+ ],
+ "succ": [
+ 1259
+ ],
+ "asm": [
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66B800000000|mov EAX,0",
+ "66B900010000|mov ECX,0x00000100",
+ "F3AA|rep stos byte ptr ES:[DI],AL",
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66B900010000|mov ECX,0x00000100",
+ "F3A4|rep movs byte ptr ES:[DI],byte ptr DS:[SI]",
+ "6683F900|cmp ECX,0",
+ "0F850605|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 191,
+ "entry": "F000:016D",
+ "term": "F000:016D",
+ "pred": [
+ 189
+ ],
+ "succ": [
+ 193
+ ],
+ "asm": [
+ "E99500|jmp near 0x0205"
+ ]
+ },
+ {
+ "id": 1246,
+ "entry": "F000:108C",
+ "term": "F000:1093",
+ "pred": [
+ 1239
+ ],
+ "succ": [
+ 1249
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F853005|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 193,
+ "entry": "F000:0205",
+ "term": "F000:020C",
+ "pred": [
+ 191
+ ],
+ "succ": [
+ 199
+ ],
+ "asm": [
+ "B4D4|mov AH,0xD4",
+ "9E|sahf",
+ "B80000|mov AX,0",
+ "9E|sahf",
+ "0F83A400|jae near 0x02B4"
+ ]
+ },
+ {
+ "id": 1239,
+ "entry": "F000:1070",
+ "term": "F000:1088",
+ "pred": [
+ 1236
+ ],
+ "succ": [
+ 1246
+ ],
+ "asm": [
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66BEFF000100|mov ESI,0x000100FF",
+ "66B900010000|mov ECX,0x00000100",
+ "F3AE|repe scas AL,byte ptr ES:[DI]",
+ "6683F900|cmp ECX,0",
+ "0F853B05|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 199,
+ "entry": "F000:02B4",
+ "term": "F000:02B4",
+ "pred": [
+ 193
+ ],
+ "succ": [
+ 201
+ ],
+ "asm": [
+ "0F8359FF|jae near 0x0211"
+ ]
+ },
+ {
+ "id": 1236,
+ "entry": "F000:1065",
+ "term": "F000:106C",
+ "pred": [
+ 1233
+ ],
+ "succ": [
+ 1239
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F855705|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 201,
+ "entry": "F000:0211",
+ "term": "F000:0214",
+ "pred": [
+ 199
+ ],
+ "succ": [
+ 205
+ ],
+ "asm": [
+ "B495|mov AH,0x95",
+ "9E|sahf",
+ "0F85A000|jne near 0x02B8"
+ ]
+ },
+ {
+ "id": 1233,
+ "entry": "F000:105A",
+ "term": "F000:1061",
+ "pred": [
+ 1218
+ ],
+ "succ": [
+ 1236
+ ],
+ "asm": [
+ "6681FEFFFF0100|cmp ESI,0x0001FFFF",
+ "0F856205|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 205,
+ "entry": "F000:02B8",
+ "term": "F000:02B8",
+ "pred": [
+ 201
+ ],
+ "succ": [
+ 207
+ ],
+ "asm": [
+ "0F855DFF|jne near 0x0219"
+ ]
+ },
+ {
+ "id": 1218,
+ "entry": "F000:1029",
+ "term": "F000:1056",
+ "pred": [
+ 1215
+ ],
+ "succ": [
+ 1233
+ ],
+ "asm": [
+ "66BFFF000100|mov EDI,0x000100FF",
+ "8CC2|mov DX,ES",
+ "8CD9|mov CX,DS",
+ "87D1|xchg DX,CX",
+ "8EC2|mov ES,DX",
+ "8ED9|mov DS,CX",
+ "6687FE|xchg EDI,ESI",
+ "66B900010000|mov ECX,0x00000100",
+ "F3AA|rep stos byte ptr ES:[DI],AL",
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66B900010000|mov ECX,0x00000100",
+ "F3A6|repe cmps byte ptr DS:[SI],byte ptr ES:[DI]",
+ "6683F900|cmp ECX,0",
+ "0F856D05|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 207,
+ "entry": "F000:0219",
+ "term": "F000:021C",
+ "pred": [
+ 205
+ ],
+ "succ": [
+ 211
+ ],
+ "asm": [
+ "B4D1|mov AH,0xD1",
+ "9E|sahf",
+ "0F8B9C00|jnp near 0x02BC"
+ ]
+ },
+ {
+ "id": 1215,
+ "entry": "F000:101E",
+ "term": "F000:1025",
+ "pred": [
+ 1203
+ ],
+ "succ": [
+ 1218
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F859E05|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 211,
+ "entry": "F000:02BC",
+ "term": "F000:02BC",
+ "pred": [
+ 207
+ ],
+ "succ": [
+ 213
+ ],
+ "asm": [
+ "0F8B61FF|jnp near 0x0221"
+ ]
+ },
+ {
+ "id": 1203,
+ "entry": "F000:0FE9",
+ "term": "F000:101A",
+ "pred": [
+ 1200
+ ],
+ "succ": [
+ 1215
+ ],
+ "asm": [
+ "FD|std",
+ "66B878563412|mov EAX,0x12345678",
+ "66BEFF000100|mov ESI,0x000100FF",
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66B878563412|mov EAX,0x12345678",
+ "66BEFF000100|mov ESI,0x000100FF",
+ "66BFFF000100|mov EDI,0x000100FF",
+ "66B900010000|mov ECX,0x00000100",
+ "F3AA|rep stos byte ptr ES:[DI],AL",
+ "6683F900|cmp ECX,0",
+ "0F85A905|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 213,
+ "entry": "F000:0221",
+ "term": "F000:0224",
+ "pred": [
+ 211
+ ],
+ "succ": [
+ 217
+ ],
+ "asm": [
+ "B455|mov AH,0x55",
+ "9E|sahf",
+ "0F899800|jns near 0x02C0"
+ ]
+ },
+ {
+ "id": 1200,
+ "entry": "F000:0FDE",
+ "term": "F000:0FE5",
+ "pred": [
+ 1197
+ ],
+ "succ": [
+ 1203
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F85DE05|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 217,
+ "entry": "F000:02C0",
+ "term": "F000:02C0",
+ "pred": [
+ 213
+ ],
+ "succ": [
+ 219
+ ],
+ "asm": [
+ "0F8965FF|jns near 0x0229"
+ ]
+ },
+ {
+ "id": 1197,
+ "entry": "F000:0FD3",
+ "term": "F000:0FDA",
+ "pred": [
+ 1190
+ ],
+ "succ": [
+ 1200
+ ],
+ "asm": [
+ "6681FE00000100|cmp ESI,0x00010000",
+ "0F85E905|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 219,
+ "entry": "F000:0229",
+ "term": "F000:022C",
+ "pred": [
+ 217
+ ],
+ "succ": [
+ 223
+ ],
+ "asm": [
+ "B494|mov AH,0x94",
+ "9E|sahf",
+ "0F879400|ja near 0x02C4"
+ ]
+ },
+ {
+ "id": 1190,
+ "entry": "F000:0FB6",
+ "term": "F000:0FCF",
+ "pred": [
+ 1187
+ ],
+ "succ": [
+ 1197
+ ],
+ "asm": [
+ "66B940000000|mov ECX,0x00000040",
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66BE00FF0100|mov ESI,0x0001FF00",
+ "F366A7|repe cmps dword ptr DS:[SI],dword ptr ES:[DI]",
+ "6683F900|cmp ECX,0",
+ "0F85F405|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 223,
+ "entry": "F000:02C4",
+ "term": "F000:02C4",
+ "pred": [
+ 219
+ ],
+ "succ": [
+ 225
+ ],
+ "asm": [
+ "0F8769FF|ja near 0x0231"
+ ]
+ },
+ {
+ "id": 1187,
+ "entry": "F000:0FAB",
+ "term": "F000:0FB2",
+ "pred": [
+ 1184
+ ],
+ "succ": [
+ 1190
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F851106|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 225,
+ "entry": "F000:0231",
+ "term": "F000:0231",
+ "pred": [
+ 223
+ ],
+ "succ": [
+ 227
+ ],
+ "asm": [
+ "E99400|jmp near 0x02C8"
+ ]
+ },
+ {
+ "id": 1184,
+ "entry": "F000:0FA0",
+ "term": "F000:0FA7",
+ "pred": [
+ 1174
+ ],
+ "succ": [
+ 1187
+ ],
+ "asm": [
+ "6681FE00000100|cmp ESI,0x00010000",
+ "0F851C06|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 227,
+ "entry": "F000:02C8",
+ "term": "F000:02CF",
+ "pred": [
+ 225
+ ],
+ "succ": [
+ 233
+ ],
+ "asm": [
+ "B400|mov AH,0",
+ "9E|sahf",
+ "B040|mov AL,0x40",
+ "D0E0|shl AL,1",
+ "0F81A700|jno near 0x037A"
+ ]
+ },
+ {
+ "id": 1174,
+ "entry": "F000:0F74",
+ "term": "F000:0F9C",
+ "pred": [
+ 1171
+ ],
+ "succ": [
+ 1184
+ ],
+ "asm": [
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66B800000000|mov EAX,0",
+ "66B940000000|mov ECX,0x00000040",
+ "F366AB|rep stos dword ptr ES:[DI],EAX",
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66B940000000|mov ECX,0x00000040",
+ "F366A5|rep movs dword ptr ES:[DI],dword ptr DS:[SI]",
+ "6683F900|cmp ECX,0",
+ "0F852706|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 233,
+ "entry": "F000:02D3",
+ "term": "F000:02D3",
+ "pred": [
+ 227
+ ],
+ "succ": [
+ 235
+ ],
+ "asm": [
+ "0F80A400|jo near 0x037B"
+ ]
+ },
+ {
+ "id": 1171,
+ "entry": "F000:0F69",
+ "term": "F000:0F70",
+ "pred": [
+ 1164
+ ],
+ "succ": [
+ 1174
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F855306|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 235,
+ "entry": "F000:037B",
+ "term": "F000:037B",
+ "pred": [
+ 233
+ ],
+ "succ": [
+ 237
+ ],
+ "asm": [
+ "0F8059FF|jo near 0x02D8"
+ ]
+ },
+ {
+ "id": 1164,
+ "entry": "F000:0F4C",
+ "term": "F000:0F65",
+ "pred": [
+ 1161
+ ],
+ "succ": [
+ 1171
+ ],
+ "asm": [
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66BE00FF0100|mov ESI,0x0001FF00",
+ "66B940000000|mov ECX,0x00000040",
+ "F366AF|repe scas EAX,dword ptr ES:[DI]",
+ "6683F900|cmp ECX,0",
+ "0F855E06|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 237,
+ "entry": "F000:02D8",
+ "term": "F000:02D8",
+ "pred": [
+ 235
+ ],
+ "succ": [
+ 239
+ ],
+ "asm": [
+ "0F8C9E00|jl near 0x037A"
+ ]
+ },
+ {
+ "id": 1161,
+ "entry": "F000:0F41",
+ "term": "F000:0F48",
+ "pred": [
+ 1158
+ ],
+ "succ": [
+ 1164
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F857B06|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 239,
+ "entry": "F000:02DC",
+ "term": "F000:02DC",
+ "pred": [
+ 237
+ ],
+ "succ": [
+ 241
+ ],
+ "asm": [
+ "0F8D9F00|jge near 0x037F"
+ ]
+ },
+ {
+ "id": 1158,
+ "entry": "F000:0F36",
+ "term": "F000:0F3D",
+ "pred": [
+ 1143
+ ],
+ "succ": [
+ 1161
+ ],
+ "asm": [
+ "6681FE00000100|cmp ESI,0x00010000",
+ "0F858606|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 241,
+ "entry": "F000:037F",
+ "term": "F000:037F",
+ "pred": [
+ 239
+ ],
+ "succ": [
+ 243
+ ],
+ "asm": [
+ "0F8D5EFF|jge near 0x02E1"
+ ]
+ },
+ {
+ "id": 1143,
+ "entry": "F000:0F03",
+ "term": "F000:0F32",
+ "pred": [
+ 1140
+ ],
+ "succ": [
+ 1158
+ ],
+ "asm": [
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "8CC2|mov DX,ES",
+ "8CD9|mov CX,DS",
+ "87D1|xchg DX,CX",
+ "8EC2|mov ES,DX",
+ "8ED9|mov DS,CX",
+ "6687FE|xchg EDI,ESI",
+ "66B940000000|mov ECX,0x00000040",
+ "F366AB|rep stos dword ptr ES:[DI],EAX",
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66B940000000|mov ECX,0x00000040",
+ "F366A7|repe cmps dword ptr DS:[SI],dword ptr ES:[DI]",
+ "6683F900|cmp ECX,0",
+ "0F859106|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 243,
+ "entry": "F000:02E1",
+ "term": "F000:02E1",
+ "pred": [
+ 241
+ ],
+ "succ": [
+ 245
+ ],
+ "asm": [
+ "0F8E9500|jle near 0x037A"
+ ]
+ },
+ {
+ "id": 1140,
+ "entry": "F000:0EF8",
+ "term": "F000:0EFF",
+ "pred": [
+ 1128
+ ],
+ "succ": [
+ 1143
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F85C406|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 245,
+ "entry": "F000:02E5",
+ "term": "F000:02E5",
+ "pred": [
+ 243
+ ],
+ "succ": [
+ 247
+ ],
+ "asm": [
+ "0F8F9A00|jg near 0x0383"
+ ]
+ },
+ {
+ "id": 1128,
+ "entry": "F000:0EC2",
+ "term": "F000:0EF4",
+ "pred": [
+ 1125
+ ],
+ "succ": [
+ 1140
+ ],
+ "asm": [
+ "FC|cld",
+ "66B878563412|mov EAX,0x12345678",
+ "66BE00FF0100|mov ESI,0x0001FF00",
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66B878563412|mov EAX,0x12345678",
+ "66BE00FF0100|mov ESI,0x0001FF00",
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66B940000000|mov ECX,0x00000040",
+ "F366AB|rep stos dword ptr ES:[DI],EAX",
+ "6683F900|cmp ECX,0",
+ "0F85CF06|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 247,
+ "entry": "F000:0383",
+ "term": "F000:0383",
+ "pred": [
+ 245
+ ],
+ "succ": [
+ 249
+ ],
+ "asm": [
+ "0F8F63FF|jg near 0x02EA"
+ ]
+ },
+ {
+ "id": 1125,
+ "entry": "F000:0EB7",
+ "term": "F000:0EBE",
+ "pred": [
+ 1122
+ ],
+ "succ": [
+ 1128
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F850507|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 249,
+ "entry": "F000:02EA",
+ "term": "F000:02ED",
+ "pred": [
+ 247
+ ],
+ "succ": [
+ 253
+ ],
+ "asm": [
+ "B440|mov AH,0x40",
+ "9E|sahf",
+ "0F8C9600|jl near 0x0387"
+ ]
+ },
+ {
+ "id": 1122,
+ "entry": "F000:0EAC",
+ "term": "F000:0EB3",
+ "pred": [
+ 1115
+ ],
+ "succ": [
+ 1125
+ ],
+ "asm": [
+ "6681FE00000100|cmp ESI,0x00010000",
+ "0F851007|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 253,
+ "entry": "F000:0387",
+ "term": "F000:0387",
+ "pred": [
+ 249
+ ],
+ "succ": [
+ 255
+ ],
+ "asm": [
+ "0F8C67FF|jl near 0x02F2"
+ ]
+ },
+ {
+ "id": 1115,
+ "entry": "F000:0E90",
+ "term": "F000:0EA8",
+ "pred": [
+ 1112
+ ],
+ "succ": [
+ 1122
+ ],
+ "asm": [
+ "66B980000000|mov ECX,0x00000080",
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66BE00FF0100|mov ESI,0x0001FF00",
+ "F3A7|repe cmps word ptr DS:[SI],word ptr ES:[DI]",
+ "6683F900|cmp ECX,0",
+ "0F851B07|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 255,
+ "entry": "F000:02F2",
+ "term": "F000:02F2",
+ "pred": [
+ 253
+ ],
+ "succ": [
+ 257
+ ],
+ "asm": [
+ "0F8E9500|jle near 0x038B"
+ ]
+ },
+ {
+ "id": 1112,
+ "entry": "F000:0E85",
+ "term": "F000:0E8C",
+ "pred": [
+ 1109
+ ],
+ "succ": [
+ 1115
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F853707|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 257,
+ "entry": "F000:038B",
+ "term": "F000:038B",
+ "pred": [
+ 255
+ ],
+ "succ": [
+ 259
+ ],
+ "asm": [
+ "0F8E68FF|jle near 0x02F7"
+ ]
+ },
+ {
+ "id": 1109,
+ "entry": "F000:0E7A",
+ "term": "F000:0E81",
+ "pred": [
+ 1099
+ ],
+ "succ": [
+ 1112
+ ],
+ "asm": [
+ "6681FE00000100|cmp ESI,0x00010000",
+ "0F854207|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 259,
+ "entry": "F000:02F7",
+ "term": "F000:02F7",
+ "pred": [
+ 257
+ ],
+ "succ": [
+ 261
+ ],
+ "asm": [
+ "E99500|jmp near 0x038F"
+ ]
+ },
+ {
+ "id": 1099,
+ "entry": "F000:0E50",
+ "term": "F000:0E76",
+ "pred": [
+ 1096
+ ],
+ "succ": [
+ 1109
+ ],
+ "asm": [
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66B800000000|mov EAX,0",
+ "66B980000000|mov ECX,0x00000080",
+ "F3AB|rep stos word ptr ES:[DI],AX",
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66B980000000|mov ECX,0x00000080",
+ "F3A5|rep movs word ptr ES:[DI],word ptr DS:[SI]",
+ "6683F900|cmp ECX,0",
+ "0F854D07|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 261,
+ "entry": "F000:038F",
+ "term": "F000:0395",
+ "pred": [
+ 259
+ ],
+ "succ": [
+ 265
+ ],
+ "asm": [
+ "66B900000200|mov ECX,0x00020000",
+ "66B800000000|mov EAX,0"
+ ]
+ },
+ {
+ "id": 1096,
+ "entry": "F000:0E45",
+ "term": "F000:0E4C",
+ "pred": [
+ 1089
+ ],
+ "succ": [
+ 1099
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F857707|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 265,
+ "entry": "F000:039B",
+ "term": "F000:039D",
+ "pred": [
+ 261,
+ 265
+ ],
+ "succ": [
+ 265,
+ 267
+ ],
+ "asm": [
+ "6640|inc EAX",
+ "E2FC|loop 0x039B"
+ ]
+ },
+ {
+ "id": 1089,
+ "entry": "F000:0E29",
+ "term": "F000:0E41",
+ "pred": [
+ 1086
+ ],
+ "succ": [
+ 1096
+ ],
+ "asm": [
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66BE00FF0100|mov ESI,0x0001FF00",
+ "66B980000000|mov ECX,0x00000080",
+ "F3AF|repe scas AX,word ptr ES:[DI]",
+ "6683F900|cmp ECX,0",
+ "0F858207|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 267,
+ "entry": "F000:039F",
+ "term": "F000:03A5",
+ "pred": [
+ 265
+ ],
+ "succ": [
+ 270
+ ],
+ "asm": [
+ "663D00000100|cmp EAX,0x00010000",
+ "0F851E12|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 1086,
+ "entry": "F000:0E1E",
+ "term": "F000:0E25",
+ "pred": [
+ 1083
+ ],
+ "succ": [
+ 1089
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F859E07|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 270,
+ "entry": "F000:03A9",
+ "term": "F000:03B0",
+ "pred": [
+ 267
+ ],
+ "succ": [
+ 273
+ ],
+ "asm": [
+ "6681F900000200|cmp ECX,0x00020000",
+ "0F851312|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 1083,
+ "entry": "F000:0E13",
+ "term": "F000:0E1A",
+ "pred": [
+ 1068
+ ],
+ "succ": [
+ 1086
+ ],
+ "asm": [
+ "6681FE00000100|cmp ESI,0x00010000",
+ "0F85A907|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 273,
+ "entry": "F000:03B4",
+ "term": "F000:03BA",
+ "pred": [
+ 270
+ ],
+ "succ": [
+ 277
+ ],
+ "asm": [
+ "66B900000200|mov ECX,0x00020000",
+ "66B800000000|mov EAX,0"
+ ]
+ },
+ {
+ "id": 1068,
+ "entry": "F000:0DE2",
+ "term": "F000:0E0F",
+ "pred": [
+ 1065
+ ],
+ "succ": [
+ 1083
+ ],
+ "asm": [
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "8CC2|mov DX,ES",
+ "8CD9|mov CX,DS",
+ "87D1|xchg DX,CX",
+ "8EC2|mov ES,DX",
+ "8ED9|mov DS,CX",
+ "6687FE|xchg EDI,ESI",
+ "66B980000000|mov ECX,0x00000080",
+ "F3AB|rep stos word ptr ES:[DI],AX",
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66B980000000|mov ECX,0x00000080",
+ "F3A7|repe cmps word ptr DS:[SI],word ptr ES:[DI]",
+ "6683F900|cmp ECX,0",
+ "0F85B407|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 277,
+ "entry": "F000:03C0",
+ "term": "F000:03C2",
+ "pred": [
+ 273,
+ 277
+ ],
+ "succ": [
+ 277,
+ 279
+ ],
+ "asm": [
+ "6640|inc EAX",
+ "67E2FB|loop 0x03C0"
+ ]
+ },
+ {
+ "id": 1065,
+ "entry": "F000:0DD7",
+ "term": "F000:0DDE",
+ "pred": [
+ 1053
+ ],
+ "succ": [
+ 1068
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F85E507|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 279,
+ "entry": "F000:03C5",
+ "term": "F000:03CB",
+ "pred": [
+ 277
+ ],
+ "succ": [
+ 282
+ ],
+ "asm": [
+ "663D00000200|cmp EAX,0x00020000",
+ "0F85F811|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 1053,
+ "entry": "F000:0DA2",
+ "term": "F000:0DD3",
+ "pred": [
+ 1050
+ ],
+ "succ": [
+ 1065
+ ],
+ "asm": [
+ "FC|cld",
+ "66B878563412|mov EAX,0x12345678",
+ "66BE00FF0100|mov ESI,0x0001FF00",
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66B878563412|mov EAX,0x12345678",
+ "66BE00FF0100|mov ESI,0x0001FF00",
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66B980000000|mov ECX,0x00000080",
+ "F3AB|rep stos word ptr ES:[DI],AX",
+ "6683F900|cmp ECX,0",
+ "0F85F007|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 282,
+ "entry": "F000:03CF",
+ "term": "F000:03D3",
+ "pred": [
+ 279
+ ],
+ "succ": [
+ 285
+ ],
+ "asm": [
+ "6683F900|cmp ECX,0",
+ "0F85F011|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 1050,
+ "entry": "F000:0D97",
+ "term": "F000:0D9E",
+ "pred": [
+ 1047
+ ],
+ "succ": [
+ 1053
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F852508|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 285,
+ "entry": "F000:03D7",
+ "term": "F000:03DA",
+ "pred": [
+ 282
+ ],
+ "succ": [
+ 290
+ ],
+ "asm": [
+ "B9FFFF|mov CX,0xFFFF",
+ "B80000|mov AX,0"
+ ]
+ },
+ {
+ "id": 1047,
+ "entry": "F000:0D8C",
+ "term": "F000:0D93",
+ "pred": [
+ 1040
+ ],
+ "succ": [
+ 1050
+ ],
+ "asm": [
+ "6681FE00000100|cmp ESI,0x00010000",
+ "0F853008|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 290,
+ "entry": "F000:03DD",
+ "term": "F000:03E1",
+ "pred": [
+ 285,
+ 290
+ ],
+ "succ": [
+ 290,
+ 292
+ ],
+ "asm": [
+ "40|inc AX",
+ "80FC00|cmp AH,0",
+ "E1FA|loope 0x03DD"
+ ]
+ },
+ {
+ "id": 1040,
+ "entry": "F000:0D70",
+ "term": "F000:0D88",
+ "pred": [
+ 1037
+ ],
+ "succ": [
+ 1047
+ ],
+ "asm": [
+ "66B900010000|mov ECX,0x00000100",
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66BE00FF0100|mov ESI,0x0001FF00",
+ "F3A6|repe cmps byte ptr DS:[SI],byte ptr ES:[DI]",
+ "6683F900|cmp ECX,0",
+ "0F853B08|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 292,
+ "entry": "F000:03E3",
+ "term": "F000:03E6",
+ "pred": [
+ 290
+ ],
+ "succ": [
+ 295
+ ],
+ "asm": [
+ "3D0001|cmp AX,0x0100",
+ "0F85DD11|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 1037,
+ "entry": "F000:0D65",
+ "term": "F000:0D6C",
+ "pred": [
+ 1034
+ ],
+ "succ": [
+ 1040
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F855708|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 295,
+ "entry": "F000:03EA",
+ "term": "F000:03EE",
+ "pred": [
+ 292
+ ],
+ "succ": [
+ 298
+ ],
+ "asm": [
+ "81F9FFFE|cmp CX,0xFEFF",
+ "0F85D511|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 1034,
+ "entry": "F000:0D5A",
+ "term": "F000:0D61",
+ "pred": [
+ 1024
+ ],
+ "succ": [
+ 1037
+ ],
+ "asm": [
+ "6681FE00000100|cmp ESI,0x00010000",
+ "0F856208|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 298,
+ "entry": "F000:03F2",
+ "term": "F000:03F5",
+ "pred": [
+ 295
+ ],
+ "succ": [
+ 303
+ ],
+ "asm": [
+ "B9FF00|mov CX,0x00FF",
+ "B80000|mov AX,0"
+ ]
+ },
+ {
+ "id": 1024,
+ "entry": "F000:0D30",
+ "term": "F000:0D56",
+ "pred": [
+ 1021
+ ],
+ "succ": [
+ 1034
+ ],
+ "asm": [
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66B800000000|mov EAX,0",
+ "66B900010000|mov ECX,0x00000100",
+ "F3AA|rep stos byte ptr ES:[DI],AL",
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66B900010000|mov ECX,0x00000100",
+ "F3A4|rep movs byte ptr ES:[DI],byte ptr DS:[SI]",
+ "6683F900|cmp ECX,0",
+ "0F856D08|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 303,
+ "entry": "F000:03F8",
+ "term": "F000:03FC",
+ "pred": [
+ 298,
+ 303
+ ],
+ "succ": [
+ 303,
+ 305
+ ],
+ "asm": [
+ "40|inc AX",
+ "80FC00|cmp AH,0",
+ "E1FA|loope 0x03F8"
+ ]
+ },
+ {
+ "id": 1021,
+ "entry": "F000:0D25",
+ "term": "F000:0D2C",
+ "pred": [
+ 1014
+ ],
+ "succ": [
+ 1024
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F859708|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 305,
+ "entry": "F000:03FE",
+ "term": "F000:0401",
+ "pred": [
+ 303
+ ],
+ "succ": [
+ 308
+ ],
+ "asm": [
+ "3DFF00|cmp AX,0x00FF",
+ "0F85C211|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 1014,
+ "entry": "F000:0D09",
+ "term": "F000:0D21",
+ "pred": [
+ 1011
+ ],
+ "succ": [
+ 1021
+ ],
+ "asm": [
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66BE00FF0100|mov ESI,0x0001FF00",
+ "66B900010000|mov ECX,0x00000100",
+ "F3AE|repe scas AL,byte ptr ES:[DI]",
+ "6683F900|cmp ECX,0",
+ "0F85A208|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 308,
+ "entry": "F000:0405",
+ "term": "F000:0408",
+ "pred": [
+ 305
+ ],
+ "succ": [
+ 311
+ ],
+ "asm": [
+ "83F900|cmp CX,0",
+ "0F85BB11|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 1011,
+ "entry": "F000:0CFE",
+ "term": "F000:0D05",
+ "pred": [
+ 1008
+ ],
+ "succ": [
+ 1014
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F85BE08|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 311,
+ "entry": "F000:040C",
+ "term": "F000:0412",
+ "pred": [
+ 308
+ ],
+ "succ": [
+ 316
+ ],
+ "asm": [
+ "66B900000200|mov ECX,0x00020000",
+ "66B800000000|mov EAX,0"
+ ]
+ },
+ {
+ "id": 1008,
+ "entry": "F000:0CF3",
+ "term": "F000:0CFA",
+ "pred": [
+ 993
+ ],
+ "succ": [
+ 1011
+ ],
+ "asm": [
+ "6681FE00000100|cmp ESI,0x00010000",
+ "0F85C908|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 316,
+ "entry": "F000:0418",
+ "term": "F000:0420",
+ "pred": [
+ 311,
+ 316
+ ],
+ "succ": [
+ 316,
+ 318
+ ],
+ "asm": [
+ "6640|inc EAX",
+ "66A900000100|test EAX,0x00010000",
+ "67E1F5|loope 0x0418"
+ ]
+ },
+ {
+ "id": 993,
+ "entry": "F000:0CC2",
+ "term": "F000:0CEF",
+ "pred": [
+ 990
+ ],
+ "succ": [
+ 1008
+ ],
+ "asm": [
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "8CC2|mov DX,ES",
+ "8CD9|mov CX,DS",
+ "87D1|xchg DX,CX",
+ "8EC2|mov ES,DX",
+ "8ED9|mov DS,CX",
+ "6687FE|xchg EDI,ESI",
+ "66B900010000|mov ECX,0x00000100",
+ "F3AA|rep stos byte ptr ES:[DI],AL",
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66B900010000|mov ECX,0x00000100",
+ "F3A6|repe cmps byte ptr DS:[SI],byte ptr ES:[DI]",
+ "6683F900|cmp ECX,0",
+ "0F85D408|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 318,
+ "entry": "F000:0423",
+ "term": "F000:0429",
+ "pred": [
+ 316
+ ],
+ "succ": [
+ 321
+ ],
+ "asm": [
+ "663D00000100|cmp EAX,0x00010000",
+ "0F859A11|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 990,
+ "entry": "F000:0CB7",
+ "term": "F000:0CBE",
+ "pred": [
+ 978
+ ],
+ "succ": [
+ 993
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F850509|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 321,
+ "entry": "F000:042D",
+ "term": "F000:0434",
+ "pred": [
+ 318
+ ],
+ "succ": [
+ 324
+ ],
+ "asm": [
+ "6681F900000100|cmp ECX,0x00010000",
+ "0F858F11|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 978,
+ "entry": "F000:0C82",
+ "term": "F000:0CB3",
+ "pred": [
+ 975
+ ],
+ "succ": [
+ 990
+ ],
+ "asm": [
+ "FC|cld",
+ "66B878563412|mov EAX,0x12345678",
+ "66BE00FF0100|mov ESI,0x0001FF00",
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66B878563412|mov EAX,0x12345678",
+ "66BE00FF0100|mov ESI,0x0001FF00",
+ "66BF00FF0100|mov EDI,0x0001FF00",
+ "66B900010000|mov ECX,0x00000100",
+ "F3AA|rep stos byte ptr ES:[DI],AL",
+ "6683F900|cmp ECX,0",
+ "0F851009|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 324,
+ "entry": "F000:0438",
+ "term": "F000:043B",
+ "pred": [
+ 321
+ ],
+ "succ": [
+ 329
+ ],
+ "asm": [
+ "B9FFFF|mov CX,0xFFFF",
+ "B80000|mov AX,0"
+ ]
+ },
+ {
+ "id": 975,
+ "entry": "F000:0C77",
+ "term": "F000:0C7E",
+ "pred": [
+ 967
+ ],
+ "succ": [
+ 978
+ ],
+ "asm": [
+ "6681FEFCFF0100|cmp ESI,0x0001FFFC",
+ "0F854509|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 329,
+ "entry": "F000:043E",
+ "term": "F000:0441",
+ "pred": [
+ 324,
+ 329
+ ],
+ "succ": [
+ 329,
+ 331
+ ],
+ "asm": [
+ "40|inc AX",
+ "A8FF|test AL,0xFF",
+ "E0FB|loopne 0x043E"
+ ]
+ },
+ {
+ "id": 967,
+ "entry": "F000:0C57",
+ "term": "F000:0C73",
+ "pred": [
+ 964
+ ],
+ "succ": [
+ 975
+ ],
+ "asm": [
+ "66BE00000100|mov ESI,0x00010000",
+ "66B878563412|mov EAX,0x12345678",
+ "2666678903|mov dword ptr ES:[EBX],EAX",
+ "6631C0|xor EAX,EAX",
+ "66AD|lods EAX,dword ptr DS:[SI]",
+ "663D78563412|cmp EAX,0x12345678",
+ "0F855009|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 331,
+ "entry": "F000:0443",
+ "term": "F000:0446",
+ "pred": [
+ 329
+ ],
+ "succ": [
+ 334
+ ],
+ "asm": [
+ "3D0001|cmp AX,0x0100",
+ "0F857D11|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 964,
+ "entry": "F000:0C4C",
+ "term": "F000:0C53",
+ "pred": [
+ 961
+ ],
+ "succ": [
+ 967
+ ],
+ "asm": [
+ "6681FEFCFF0100|cmp ESI,0x0001FFFC",
+ "0F857009|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 334,
+ "entry": "F000:044A",
+ "term": "F000:044E",
+ "pred": [
+ 331
+ ],
+ "succ": [
+ 337
+ ],
+ "asm": [
+ "81F9FFFE|cmp CX,0xFEFF",
+ "0F857511|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 961,
+ "entry": "F000:0C41",
+ "term": "F000:0C48",
+ "pred": [
+ 950
+ ],
+ "succ": [
+ 964
+ ],
+ "asm": [
+ "6681FFFCFF0100|cmp EDI,0x0001FFFC",
+ "0F857B09|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 337,
+ "entry": "F000:0452",
+ "term": "F000:0455",
+ "pred": [
+ 334
+ ],
+ "succ": [
+ 342
+ ],
+ "asm": [
+ "B9FF00|mov CX,0x00FF",
+ "B80000|mov AX,0"
+ ]
+ },
+ {
+ "id": 950,
+ "entry": "F000:0C0E",
+ "term": "F000:0C3D",
+ "pred": [
+ 947
+ ],
+ "succ": [
+ 961
+ ],
+ "asm": [
+ "66BE00000100|mov ESI,0x00010000",
+ "66BF00000100|mov EDI,0x00010000",
+ "66B878563412|mov EAX,0x12345678",
+ "3E66678903|mov dword ptr DS:[EBX],EAX",
+ "66B800000000|mov EAX,0",
+ "2666678903|mov dword ptr ES:[EBX],EAX",
+ "66A5|movs dword ptr ES:[DI],dword ptr DS:[SI]",
+ "66B878563412|mov EAX,0x12345678",
+ "2666673903|cmp dword ptr ES:[EBX],EAX",
+ "0F858609|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 342,
+ "entry": "F000:0458",
+ "term": "F000:045B",
+ "pred": [
+ 337,
+ 342
+ ],
+ "succ": [
+ 342,
+ 344
+ ],
+ "asm": [
+ "40|inc AX",
+ "A8FF|test AL,0xFF",
+ "E0FB|loopne 0x0458"
+ ]
+ },
+ {
+ "id": 947,
+ "entry": "F000:0C03",
+ "term": "F000:0C0A",
+ "pred": [
+ 940
+ ],
+ "succ": [
+ 950
+ ],
+ "asm": [
+ "6681FFFCFF0100|cmp EDI,0x0001FFFC",
+ "0F85B909|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 344,
+ "entry": "F000:045D",
+ "term": "F000:0460",
+ "pred": [
+ 342
+ ],
+ "succ": [
+ 347
+ ],
+ "asm": [
+ "3DFF00|cmp AX,0x00FF",
+ "0F856311|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 940,
+ "entry": "F000:0BE8",
+ "term": "F000:0BFF",
+ "pred": [
+ 937
+ ],
+ "succ": [
+ 947
+ ],
+ "asm": [
+ "66BF00000100|mov EDI,0x00010000",
+ "66B878563412|mov EAX,0x12345678",
+ "2666678903|mov dword ptr ES:[EBX],EAX",
+ "6683F800|cmp EAX,0",
+ "66AF|scas EAX,dword ptr ES:[DI]",
+ "0F85C409|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 347,
+ "entry": "F000:0464",
+ "term": "F000:0467",
+ "pred": [
+ 344
+ ],
+ "succ": [
+ 350
+ ],
+ "asm": [
+ "83F900|cmp CX,0",
+ "0F855C11|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 937,
+ "entry": "F000:0BDD",
+ "term": "F000:0BE4",
+ "pred": [
+ 934
+ ],
+ "succ": [
+ 940
+ ],
+ "asm": [
+ "6681FEFCFF0100|cmp ESI,0x0001FFFC",
+ "0F85DF09|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 350,
+ "entry": "F000:046B",
+ "term": "F000:0471",
+ "pred": [
+ 347
+ ],
+ "succ": [
+ 355
+ ],
+ "asm": [
+ "66B900000100|mov ECX,0x00010000",
+ "66B800000000|mov EAX,0"
+ ]
+ },
+ {
+ "id": 934,
+ "entry": "F000:0BD2",
+ "term": "F000:0BD9",
+ "pred": [
+ 931
+ ],
+ "succ": [
+ 937
+ ],
+ "asm": [
+ "6681FFFCFF0100|cmp EDI,0x0001FFFC",
+ "0F85EA09|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 355,
+ "entry": "F000:0477",
+ "term": "F000:047F",
+ "pred": [
+ 350,
+ 355
+ ],
+ "succ": [
+ 355,
+ 357
+ ],
+ "asm": [
+ "6640|inc EAX",
+ "66A9FFFF0000|test EAX,0x0000FFFF",
+ "67E0F5|loopne 0x0477"
+ ]
+ },
+ {
+ "id": 931,
+ "entry": "F000:0BCC",
+ "term": "F000:0BCE",
+ "pred": [
+ 923
+ ],
+ "succ": [
+ 934
+ ],
+ "asm": [
+ "66A7|cmps dword ptr DS:[SI],dword ptr ES:[DI]",
+ "0F85F509|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 357,
+ "entry": "F000:0482",
+ "term": "F000:0488",
+ "pred": [
+ 355
+ ],
+ "succ": [
+ 360
+ ],
+ "asm": [
+ "663D00000100|cmp EAX,0x00010000",
+ "0F853B11|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 923,
+ "entry": "F000:0BA8",
+ "term": "F000:0BC8",
+ "pred": [
+ 920
+ ],
+ "succ": [
+ 931
+ ],
+ "asm": [
+ "66BE00000100|mov ESI,0x00010000",
+ "66BF00000100|mov EDI,0x00010000",
+ "66BB00000000|mov EBX,0",
+ "3E66678903|mov dword ptr DS:[EBX],EAX",
+ "2666678903|mov dword ptr ES:[EBX],EAX",
+ "6683F800|cmp EAX,0",
+ "0F84FB09|je near 0x15C7"
+ ]
+ },
+ {
+ "id": 360,
+ "entry": "F000:048C",
+ "term": "F000:0490",
+ "pred": [
+ 357
+ ],
+ "succ": [
+ 363
+ ],
+ "asm": [
+ "6683F900|cmp ECX,0",
+ "0F853311|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 920,
+ "entry": "F000:0B9D",
+ "term": "F000:0BA4",
+ "pred": [
+ 910
+ ],
+ "succ": [
+ 923
+ ],
+ "asm": [
+ "6681FFFCFF0100|cmp EDI,0x0001FFFC",
+ "0F851F0A|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 363,
+ "entry": "F000:0494",
+ "term": "F000:04BB",
+ "pred": [
+ 360
+ ],
+ "succ": [
+ 376
+ ],
+ "asm": [
+ "B002|mov AL,2",
+ "BA9909|mov DX,0x0999",
+ "EE|out DX,AL",
+ "66B801000080|mov EAX,0x80000001",
+ "66F7E8|imul EAX",
+ "66B811223344|mov EAX,0x44332211",
+ "6689C3|mov EBX,EAX",
+ "66B955667788|mov ECX,0x88776655",
+ "66F7E1|mul ECX",
+ "66F7F1|div ECX",
+ "6639D8|cmp EAX,EBX",
+ "0F850811|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 910,
+ "entry": "F000:0B74",
+ "term": "F000:0B99",
+ "pred": [
+ 907
+ ],
+ "succ": [
+ 920
+ ],
+ "asm": [
+ "FD|std",
+ "66BF00000100|mov EDI,0x00010000",
+ "66BB00000000|mov EBX,0",
+ "66B800000000|mov EAX,0",
+ "2666678903|mov dword ptr ES:[EBX],EAX",
+ "66B878563412|mov EAX,0x12345678",
+ "66AB|stos dword ptr ES:[DI],EAX",
+ "2666673903|cmp dword ptr ES:[EBX],EAX",
+ "0F852A0A|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 376,
+ "entry": "F000:04BF",
+ "term": "F000:04D0",
+ "pred": [
+ 363
+ ],
+ "succ": [
+ 386
+ ],
+ "asm": [
+ "B003|mov AL,3",
+ "BA9909|mov DX,0x0999",
+ "EE|out DX,AL",
+ "BA0020|mov DX,0x2000",
+ "8ED2|mov SS,DX",
+ "31C0|xor AX,AX",
+ "8CD0|mov AX,SS",
+ "39D0|cmp AX,DX",
+ "0F85F310|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 907,
+ "entry": "F000:0B69",
+ "term": "F000:0B70",
+ "pred": [
+ 899
+ ],
+ "succ": [
+ 910
+ ],
+ "asm": [
+ "6681FEFEFF0100|cmp ESI,0x0001FFFE",
+ "0F85530A|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 386,
+ "entry": "F000:04D4",
+ "term": "F000:04DF",
+ "pred": [
+ 376
+ ],
+ "succ": [
+ 391
+ ],
+ "asm": [
+ "66B8FFFFFFFF|mov EAX,0xFFFFFFFF",
+ "668CD0|mov EAX,SS",
+ "39D0|cmp AX,DX",
+ "0F85E410|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 899,
+ "entry": "F000:0B51",
+ "term": "F000:0B65",
+ "pred": [
+ 896
+ ],
+ "succ": [
+ 907
+ ],
+ "asm": [
+ "66BE00000100|mov ESI,0x00010000",
+ "B87856|mov AX,0x5678",
+ "26678903|mov word ptr ES:[EBX],AX",
+ "6631C0|xor EAX,EAX",
+ "AD|lods AX,word ptr DS:[SI]",
+ "3D7856|cmp AX,0x5678",
+ "0F855E0A|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 391,
+ "entry": "F000:04E3",
+ "term": "F000:04F1",
+ "pred": [
+ 386
+ ],
+ "succ": [
+ 396
+ ],
+ "asm": [
+ "C7060000EFBE|mov word ptr DS:[0],0xBEEF",
+ "8C160000|mov word ptr DS:[0],SS",
+ "39160000|cmp word ptr DS:[0],DX",
+ "0F85D210|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 896,
+ "entry": "F000:0B46",
+ "term": "F000:0B4D",
+ "pred": [
+ 893
+ ],
+ "succ": [
+ 899
+ ],
+ "asm": [
+ "6681FEFEFF0100|cmp ESI,0x0001FFFE",
+ "0F85760A|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 396,
+ "entry": "F000:04F5",
+ "term": "F000:0503",
+ "pred": [
+ 391
+ ],
+ "succ": [
+ 404
+ ],
+ "asm": [
+ "8CD9|mov CX,DS",
+ "31C0|xor AX,AX",
+ "8ED0|mov SS,AX",
+ "8E160000|mov SS,word ptr DS:[0]",
+ "8CD0|mov AX,SS",
+ "39D0|cmp AX,DX",
+ "0F85C010|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 893,
+ "entry": "F000:0B3B",
+ "term": "F000:0B42",
+ "pred": [
+ 882
+ ],
+ "succ": [
+ 896
+ ],
+ "asm": [
+ "6681FFFEFF0100|cmp EDI,0x0001FFFE",
+ "0F85810A|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 404,
+ "entry": "F000:0507",
+ "term": "F000:0512",
+ "pred": [
+ 396
+ ],
+ "succ": [
+ 411
+ ],
+ "asm": [
+ "BA0020|mov DX,0x2000",
+ "8EDA|mov DS,DX",
+ "31C0|xor AX,AX",
+ "8CD8|mov AX,DS",
+ "39D0|cmp AX,DX",
+ "0F85B110|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 882,
+ "entry": "F000:0B15",
+ "term": "F000:0B37",
+ "pred": [
+ 879
+ ],
+ "succ": [
+ 893
+ ],
+ "asm": [
+ "66BE00000100|mov ESI,0x00010000",
+ "66BF00000100|mov EDI,0x00010000",
+ "B87856|mov AX,0x5678",
+ "3E678903|mov word ptr DS:[EBX],AX",
+ "B80000|mov AX,0",
+ "26678903|mov word ptr ES:[EBX],AX",
+ "A5|movs word ptr ES:[DI],word ptr DS:[SI]",
+ "B87856|mov AX,0x5678",
+ "26673903|cmp word ptr ES:[EBX],AX",
+ "0F858C0A|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 411,
+ "entry": "F000:0516",
+ "term": "F000:0521",
+ "pred": [
+ 404
+ ],
+ "succ": [
+ 416
+ ],
+ "asm": [
+ "66B8FFFFFFFF|mov EAX,0xFFFFFFFF",
+ "668CD8|mov EAX,DS",
+ "39D0|cmp AX,DX",
+ "0F85A210|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 879,
+ "entry": "F000:0B0A",
+ "term": "F000:0B11",
+ "pred": [
+ 872
+ ],
+ "succ": [
+ 882
+ ],
+ "asm": [
+ "6681FFFEFF0100|cmp EDI,0x0001FFFE",
+ "0F85B20A|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 416,
+ "entry": "F000:0525",
+ "term": "F000:0533",
+ "pred": [
+ 411
+ ],
+ "succ": [
+ 421
+ ],
+ "asm": [
+ "C7060000EFBE|mov word ptr DS:[0],0xBEEF",
+ "8C1E0000|mov word ptr DS:[0],DS",
+ "39160000|cmp word ptr DS:[0],DX",
+ "0F859010|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 872,
+ "entry": "F000:0AF5",
+ "term": "F000:0B06",
+ "pred": [
+ 869
+ ],
+ "succ": [
+ 879
+ ],
+ "asm": [
+ "66BF00000100|mov EDI,0x00010000",
+ "B87856|mov AX,0x5678",
+ "26678903|mov word ptr ES:[EBX],AX",
+ "83F800|cmp AX,0",
+ "AF|scas AX,word ptr ES:[DI]",
+ "0F85BD0A|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 421,
+ "entry": "F000:0537",
+ "term": "F000:0548",
+ "pred": [
+ 416
+ ],
+ "succ": [
+ 430
+ ],
+ "asm": [
+ "8CD9|mov CX,DS",
+ "31C0|xor AX,AX",
+ "8ED8|mov DS,AX",
+ "8EC1|mov ES,CX",
+ "268E1E0000|mov DS,word ptr ES:[0]",
+ "8CD8|mov AX,DS",
+ "39D0|cmp AX,DX",
+ "0F857B10|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 869,
+ "entry": "F000:0AEA",
+ "term": "F000:0AF1",
+ "pred": [
+ 866
+ ],
+ "succ": [
+ 872
+ ],
+ "asm": [
+ "6681FEFEFF0100|cmp ESI,0x0001FFFE",
+ "0F85D20A|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 430,
+ "entry": "F000:054C",
+ "term": "F000:0557",
+ "pred": [
+ 421
+ ],
+ "succ": [
+ 437
+ ],
+ "asm": [
+ "BA0020|mov DX,0x2000",
+ "8EC2|mov ES,DX",
+ "31C0|xor AX,AX",
+ "8CC0|mov AX,ES",
+ "39D0|cmp AX,DX",
+ "0F856C10|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 866,
+ "entry": "F000:0ADF",
+ "term": "F000:0AE6",
+ "pred": [
+ 863
+ ],
+ "succ": [
+ 869
+ ],
+ "asm": [
+ "6681FFFEFF0100|cmp EDI,0x0001FFFE",
+ "0F85DD0A|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 437,
+ "entry": "F000:055B",
+ "term": "F000:0566",
+ "pred": [
+ 430
+ ],
+ "succ": [
+ 442
+ ],
+ "asm": [
+ "66B8FFFFFFFF|mov EAX,0xFFFFFFFF",
+ "668CC0|mov EAX,ES",
+ "39D0|cmp AX,DX",
+ "0F855D10|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 863,
+ "entry": "F000:0ADA",
+ "term": "F000:0ADB",
+ "pred": [
+ 855
+ ],
+ "succ": [
+ 866
+ ],
+ "asm": [
+ "A7|cmps word ptr DS:[SI],word ptr ES:[DI]",
+ "0F85E80A|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 442,
+ "entry": "F000:056A",
+ "term": "F000:0578",
+ "pred": [
+ 437
+ ],
+ "succ": [
+ 447
+ ],
+ "asm": [
+ "C7060000EFBE|mov word ptr DS:[0],0xBEEF",
+ "8C060000|mov word ptr DS:[0],ES",
+ "39160000|cmp word ptr DS:[0],DX",
+ "0F854B10|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 855,
+ "entry": "F000:0AB9",
+ "term": "F000:0AD6",
+ "pred": [
+ 852
+ ],
+ "succ": [
+ 863
+ ],
+ "asm": [
+ "66BE00000100|mov ESI,0x00010000",
+ "66BF00000100|mov EDI,0x00010000",
+ "66BB00000000|mov EBX,0",
+ "3E678903|mov word ptr DS:[EBX],AX",
+ "26678903|mov word ptr ES:[EBX],AX",
+ "83F800|cmp AX,0",
+ "0F84ED0A|je near 0x15C7"
+ ]
+ },
+ {
+ "id": 447,
+ "entry": "F000:057C",
+ "term": "F000:058A",
+ "pred": [
+ 442
+ ],
+ "succ": [
+ 455
+ ],
+ "asm": [
+ "8CD9|mov CX,DS",
+ "31C0|xor AX,AX",
+ "8EC0|mov ES,AX",
+ "8E060000|mov ES,word ptr DS:[0]",
+ "8CC0|mov AX,ES",
+ "39D0|cmp AX,DX",
+ "0F853910|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 852,
+ "entry": "F000:0AAE",
+ "term": "F000:0AB5",
+ "pred": [
+ 842
+ ],
+ "succ": [
+ 855
+ ],
+ "asm": [
+ "6681FFFEFF0100|cmp EDI,0x0001FFFE",
+ "0F850E0B|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 455,
+ "entry": "F000:058E",
+ "term": "F000:0599",
+ "pred": [
+ 447
+ ],
+ "succ": [
+ 462
+ ],
+ "asm": [
+ "BA0020|mov DX,0x2000",
+ "8EE2|mov FS,DX",
+ "31C0|xor AX,AX",
+ "8CE0|mov AX,FS",
+ "39D0|cmp AX,DX",
+ "0F852A10|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 842,
+ "entry": "F000:0A8E",
+ "term": "F000:0AAA",
+ "pred": [
+ 839
+ ],
+ "succ": [
+ 852
+ ],
+ "asm": [
+ "FD|std",
+ "66BF00000100|mov EDI,0x00010000",
+ "66BB00000000|mov EBX,0",
+ "B80000|mov AX,0",
+ "26678903|mov word ptr ES:[EBX],AX",
+ "B87856|mov AX,0x5678",
+ "AB|stos word ptr ES:[DI],AX",
+ "26673903|cmp word ptr ES:[EBX],AX",
+ "0F85190B|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 462,
+ "entry": "F000:059D",
+ "term": "F000:05A8",
+ "pred": [
+ 455
+ ],
+ "succ": [
+ 467
+ ],
+ "asm": [
+ "66B8FFFFFFFF|mov EAX,0xFFFFFFFF",
+ "668CE0|mov EAX,FS",
+ "39D0|cmp AX,DX",
+ "0F851B10|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 839,
+ "entry": "F000:0A83",
+ "term": "F000:0A8A",
+ "pred": [
+ 831
+ ],
+ "succ": [
+ 842
+ ],
+ "asm": [
+ "6681FEFFFF0100|cmp ESI,0x0001FFFF",
+ "0F85390B|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 467,
+ "entry": "F000:05AC",
+ "term": "F000:05BA",
+ "pred": [
+ 462
+ ],
+ "succ": [
+ 472
+ ],
+ "asm": [
+ "C7060000EFBE|mov word ptr DS:[0],0xBEEF",
+ "8C260000|mov word ptr DS:[0],FS",
+ "39160000|cmp word ptr DS:[0],DX",
+ "0F850910|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 831,
+ "entry": "F000:0A6D",
+ "term": "F000:0A7F",
+ "pred": [
+ 828
+ ],
+ "succ": [
+ 839
+ ],
+ "asm": [
+ "66BE00000100|mov ESI,0x00010000",
+ "B078|mov AL,0x78",
+ "26678803|mov byte ptr ES:[EBX],AL",
+ "6631C0|xor EAX,EAX",
+ "AC|lods AL,byte ptr DS:[SI]",
+ "3C78|cmp AL,0x78",
+ "0F85440B|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 472,
+ "entry": "F000:05BE",
+ "term": "F000:05CC",
+ "pred": [
+ 467
+ ],
+ "succ": [
+ 480
+ ],
+ "asm": [
+ "8CD9|mov CX,DS",
+ "31C0|xor AX,AX",
+ "8EE0|mov FS,AX",
+ "8E260000|mov FS,word ptr DS:[0]",
+ "8CE0|mov AX,FS",
+ "39D0|cmp AX,DX",
+ "0F85F70F|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 828,
+ "entry": "F000:0A62",
+ "term": "F000:0A69",
+ "pred": [
+ 825
+ ],
+ "succ": [
+ 831
+ ],
+ "asm": [
+ "6681FEFFFF0100|cmp ESI,0x0001FFFF",
+ "0F855A0B|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 480,
+ "entry": "F000:05D0",
+ "term": "F000:05DB",
+ "pred": [
+ 472
+ ],
+ "succ": [
+ 487
+ ],
+ "asm": [
+ "BA0020|mov DX,0x2000",
+ "8EEA|mov GS,DX",
+ "31C0|xor AX,AX",
+ "8CE8|mov AX,GS",
+ "39D0|cmp AX,DX",
+ "0F85E80F|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 825,
+ "entry": "F000:0A57",
+ "term": "F000:0A5E",
+ "pred": [
+ 814
+ ],
+ "succ": [
+ 828
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F85650B|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 487,
+ "entry": "F000:05DF",
+ "term": "F000:05EA",
+ "pred": [
+ 480
+ ],
+ "succ": [
+ 492
+ ],
+ "asm": [
+ "66B8FFFFFFFF|mov EAX,0xFFFFFFFF",
+ "668CE8|mov EAX,GS",
+ "39D0|cmp AX,DX",
+ "0F85D90F|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 814,
+ "entry": "F000:0A34",
+ "term": "F000:0A53",
+ "pred": [
+ 811
+ ],
+ "succ": [
+ 825
+ ],
+ "asm": [
+ "66BE00000100|mov ESI,0x00010000",
+ "66BF00000100|mov EDI,0x00010000",
+ "B078|mov AL,0x78",
+ "3E678803|mov byte ptr DS:[EBX],AL",
+ "B000|mov AL,0",
+ "26678803|mov byte ptr ES:[EBX],AL",
+ "A4|movs byte ptr ES:[DI],byte ptr DS:[SI]",
+ "B078|mov AL,0x78",
+ "26673803|cmp byte ptr ES:[EBX],AL",
+ "0F85700B|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 492,
+ "entry": "F000:05EE",
+ "term": "F000:05FC",
+ "pred": [
+ 487
+ ],
+ "succ": [
+ 497
+ ],
+ "asm": [
+ "C7060000EFBE|mov word ptr DS:[0],0xBEEF",
+ "8C2E0000|mov word ptr DS:[0],GS",
+ "39160000|cmp word ptr DS:[0],DX",
+ "0F85C70F|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 811,
+ "entry": "F000:0A29",
+ "term": "F000:0A30",
+ "pred": [
+ 804
+ ],
+ "succ": [
+ 814
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F85930B|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 497,
+ "entry": "F000:0600",
+ "term": "F000:060E",
+ "pred": [
+ 492
+ ],
+ "succ": [
+ 505
+ ],
+ "asm": [
+ "8CD9|mov CX,DS",
+ "31C0|xor AX,AX",
+ "8EE8|mov GS,AX",
+ "8E2E0000|mov GS,word ptr DS:[0]",
+ "8CE8|mov AX,GS",
+ "39D0|cmp AX,DX",
+ "0F85B50F|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 804,
+ "entry": "F000:0A16",
+ "term": "F000:0A25",
+ "pred": [
+ 801
+ ],
+ "succ": [
+ 811
+ ],
+ "asm": [
+ "66BF00000100|mov EDI,0x00010000",
+ "B078|mov AL,0x78",
+ "26678803|mov byte ptr ES:[EBX],AL",
+ "3C00|cmp AL,0",
+ "AE|scas AL,byte ptr ES:[DI]",
+ "0F859E0B|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 505,
+ "entry": "F000:0612",
+ "term": "F000:062E",
+ "pred": [
+ 497
+ ],
+ "succ": [
+ 516
+ ],
+ "asm": [
+ "BA00F0|mov DX,0xF000",
+ "B80000|mov AX,0",
+ "8ED8|mov DS,AX",
+ "C70618003306|mov word ptr DS:[0x0018],0x0633",
+ "C7061A0000F0|mov word ptr DS:[0x001A],0xF000",
+ "B80010|mov AX,0x1000",
+ "8ED0|mov SS,AX",
+ "BCFFFF|mov SP,0xFFFF",
+ "8E|invalid"
+ ]
+ },
+ {
+ "id": 801,
+ "entry": "F000:0A0B",
+ "term": "F000:0A12",
+ "pred": [
+ 798
+ ],
+ "succ": [
+ 804
+ ],
+ "asm": [
+ "6681FEFFFF0100|cmp ESI,0x0001FFFF",
+ "0F85B10B|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 516,
+ "entry": "F000:0633",
+ "term": "F000:0636",
+ "pred": [
+ 505
+ ],
+ "succ": [
+ 519
+ ],
+ "asm": [
+ "83FCF9|cmp SP,-7",
+ "0F858D0F|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 798,
+ "entry": "F000:0A00",
+ "term": "F000:0A07",
+ "pred": [
+ 795
+ ],
+ "succ": [
+ 801
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F85BC0B|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 519,
+ "entry": "F000:063A",
+ "term": "F000:0648",
+ "pred": [
+ 516
+ ],
+ "succ": [
+ 523
+ ],
+ "asm": [
+ "36813EFBFF00F0|cmp word ptr SS:[0xFFFB],0xF000",
+ "36813EF9FF2E06|cmp word ptr SS:[0xFFF9],0x062E",
+ "0F857B0F|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 795,
+ "entry": "F000:09FB",
+ "term": "F000:09FC",
+ "pred": [
+ 787
+ ],
+ "succ": [
+ 798
+ ],
+ "asm": [
+ "A6|cmps byte ptr DS:[SI],byte ptr ES:[DI]",
+ "0F85C70B|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 523,
+ "entry": "F000:064C",
+ "term": "F000:0663",
+ "pred": [
+ 519
+ ],
+ "succ": [
+ 532
+ ],
+ "asm": [
+ "B80000|mov AX,0",
+ "8ED8|mov DS,AX",
+ "C7061800C715|mov word ptr DS:[0x0018],0x15C7",
+ "C7061A0000F0|mov word ptr DS:[0x001A],0xF000",
+ "31C0|xor AX,AX",
+ "8CC8|mov AX,CS",
+ "39D0|cmp AX,DX",
+ "0F85600F|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 787,
+ "entry": "F000:09DB",
+ "term": "F000:09F7",
+ "pred": [
+ 784
+ ],
+ "succ": [
+ 795
+ ],
+ "asm": [
+ "66BE00000100|mov ESI,0x00010000",
+ "66BF00000100|mov EDI,0x00010000",
+ "66BB00000000|mov EBX,0",
+ "3E678803|mov byte ptr DS:[EBX],AL",
+ "26678803|mov byte ptr ES:[EBX],AL",
+ "3C00|cmp AL,0",
+ "0F84CC0B|je near 0x15C7"
+ ]
+ },
+ {
+ "id": 532,
+ "entry": "F000:0667",
+ "term": "F000:0672",
+ "pred": [
+ 523
+ ],
+ "succ": [
+ 537
+ ],
+ "asm": [
+ "66B8FFFFFFFF|mov EAX,0xFFFFFFFF",
+ "668CC8|mov EAX,CS",
+ "39D0|cmp AX,DX",
+ "0F85510F|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 784,
+ "entry": "F000:09D0",
+ "term": "F000:09D7",
+ "pred": [
+ 774
+ ],
+ "succ": [
+ 787
+ ],
+ "asm": [
+ "6681FFFFFF0100|cmp EDI,0x0001FFFF",
+ "0F85EC0B|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 537,
+ "entry": "F000:0676",
+ "term": "F000:0684",
+ "pred": [
+ 532
+ ],
+ "succ": [
+ 542
+ ],
+ "asm": [
+ "C7060000EFBE|mov word ptr DS:[0],0xBEEF",
+ "8C0E0000|mov word ptr DS:[0],CS",
+ "39160000|cmp word ptr DS:[0],DX",
+ "0F853F0F|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 774,
+ "entry": "F000:09B2",
+ "term": "F000:09CC",
+ "pred": [
+ 771
+ ],
+ "succ": [
+ 784
+ ],
+ "asm": [
+ "FD|std",
+ "66BF00000100|mov EDI,0x00010000",
+ "66BB00000000|mov EBX,0",
+ "B000|mov AL,0",
+ "26678803|mov byte ptr ES:[EBX],AL",
+ "B078|mov AL,0x78",
+ "AA|stos byte ptr ES:[DI],AL",
+ "26673803|cmp byte ptr ES:[EBX],AL",
+ "0F85F70B|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 542,
+ "entry": "F000:0688",
+ "term": "F000:06A1",
+ "pred": [
+ 537
+ ],
+ "succ": [
+ 552
+ ],
+ "asm": [
+ "B80000|mov AX,0",
+ "8ED8|mov DS,AX",
+ "C7061800A806|mov word ptr DS:[0x0018],0x06A8",
+ "C7061A0000F0|mov word ptr DS:[0x001A],0xF000",
+ "B80010|mov AX,0x1000",
+ "8ED0|mov SS,AX",
+ "BCFFFF|mov SP,0xFFFF",
+ "8E|invalid"
+ ]
+ },
+ {
+ "id": 771,
+ "entry": "F000:09A7",
+ "term": "F000:09AE",
+ "pred": [
+ 763
+ ],
+ "succ": [
+ 774
+ ],
+ "asm": [
+ "6681FE00000100|cmp ESI,0x00010000",
+ "0F85150C|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 552,
+ "entry": "F000:06A8",
+ "term": "F000:06AB",
+ "pred": [
+ 542
+ ],
+ "succ": [
+ 555
+ ],
+ "asm": [
+ "83FCF9|cmp SP,-7",
+ "0F85180F|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 763,
+ "entry": "F000:0987",
+ "term": "F000:09A3",
+ "pred": [
+ 760
+ ],
+ "succ": [
+ 771
+ ],
+ "asm": [
+ "66BEFCFF0100|mov ESI,0x0001FFFC",
+ "66B878563412|mov EAX,0x12345678",
+ "2666678903|mov dword ptr ES:[EBX],EAX",
+ "6631C0|xor EAX,EAX",
+ "66AD|lods EAX,dword ptr DS:[SI]",
+ "663D78563412|cmp EAX,0x12345678",
+ "0F85200C|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 555,
+ "entry": "F000:06AF",
+ "term": "F000:06BD",
+ "pred": [
+ 552
+ ],
+ "succ": [
+ 559
+ ],
+ "asm": [
+ "36813EFBFF00F0|cmp word ptr SS:[0xFFFB],0xF000",
+ "36813EF9FFA106|cmp word ptr SS:[0xFFF9],0x06A1",
+ "0F85060F|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 760,
+ "entry": "F000:097C",
+ "term": "F000:0983",
+ "pred": [
+ 757
+ ],
+ "succ": [
+ 763
+ ],
+ "asm": [
+ "6681FE00000100|cmp ESI,0x00010000",
+ "0F85400C|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 559,
+ "entry": "F000:06C1",
+ "term": "F000:06FC",
+ "pred": [
+ 555
+ ],
+ "succ": [
+ 580
+ ],
+ "asm": [
+ "B80000|mov AX,0",
+ "8ED8|mov DS,AX",
+ "C7061800C715|mov word ptr DS:[0x0018],0x15C7",
+ "C7061A0000F0|mov word ptr DS:[0x001A],0xF000",
+ "BA0021|mov DX,0x2100",
+ "8EDA|mov DS,DX",
+ "BA0061|mov DX,0x6100",
+ "8EC2|mov ES,DX",
+ "B004|mov AL,4",
+ "BA9909|mov DX,0x0999",
+ "EE|out DX,AL",
+ "FC|cld",
+ "66BFFFFF0100|mov EDI,0x0001FFFF",
+ "66BBFFFF0000|mov EBX,0x0000FFFF",
+ "B000|mov AL,0",
+ "26678803|mov byte ptr ES:[EBX],AL",
+ "B078|mov AL,0x78",
+ "AA|stos byte ptr ES:[DI],AL",
+ "26673803|cmp byte ptr ES:[EBX],AL",
+ "0F85C70E|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 757,
+ "entry": "F000:0971",
+ "term": "F000:0978",
+ "pred": [
+ 746
+ ],
+ "succ": [
+ 760
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F854B0C|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 580,
+ "entry": "F000:0700",
+ "term": "F000:0707",
+ "pred": [
+ 559
+ ],
+ "succ": [
+ 583
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F85BC0E|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 746,
+ "entry": "F000:093E",
+ "term": "F000:096D",
+ "pred": [
+ 743
+ ],
+ "succ": [
+ 757
+ ],
+ "asm": [
+ "66BEFCFF0100|mov ESI,0x0001FFFC",
+ "66BFFCFF0100|mov EDI,0x0001FFFC",
+ "66B878563412|mov EAX,0x12345678",
+ "3E66678903|mov dword ptr DS:[EBX],EAX",
+ "66B800000000|mov EAX,0",
+ "2666678903|mov dword ptr ES:[EBX],EAX",
+ "66A5|movs dword ptr ES:[DI],dword ptr DS:[SI]",
+ "66B878563412|mov EAX,0x12345678",
+ "2666673903|cmp dword ptr ES:[EBX],EAX",
+ "0F85560C|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 583,
+ "entry": "F000:070B",
+ "term": "F000:0727",
+ "pred": [
+ 580
+ ],
+ "succ": [
+ 591
+ ],
+ "asm": [
+ "66BEFFFF0100|mov ESI,0x0001FFFF",
+ "66BFFFFF0100|mov EDI,0x0001FFFF",
+ "66BBFFFF0000|mov EBX,0x0000FFFF",
+ "3E678803|mov byte ptr DS:[EBX],AL",
+ "26678803|mov byte ptr ES:[EBX],AL",
+ "3C00|cmp AL,0",
+ "0F849C0E|je near 0x15C7"
+ ]
+ },
+ {
+ "id": 743,
+ "entry": "F000:0933",
+ "term": "F000:093A",
+ "pred": [
+ 736
+ ],
+ "succ": [
+ 746
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F85890C|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 591,
+ "entry": "F000:072B",
+ "term": "F000:072C",
+ "pred": [
+ 583
+ ],
+ "succ": [
+ 594
+ ],
+ "asm": [
+ "A6|cmps byte ptr DS:[SI],byte ptr ES:[DI]",
+ "0F85970E|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 736,
+ "entry": "F000:0918",
+ "term": "F000:092F",
+ "pred": [
+ 733
+ ],
+ "succ": [
+ 743
+ ],
+ "asm": [
+ "66BFFCFF0100|mov EDI,0x0001FFFC",
+ "66B878563412|mov EAX,0x12345678",
+ "2666678903|mov dword ptr ES:[EBX],EAX",
+ "6683F800|cmp EAX,0",
+ "66AF|scas EAX,dword ptr ES:[DI]",
+ "0F85940C|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 594,
+ "entry": "F000:0730",
+ "term": "F000:0737",
+ "pred": [
+ 591
+ ],
+ "succ": [
+ 597
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F858C0E|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 733,
+ "entry": "F000:090D",
+ "term": "F000:0914",
+ "pred": [
+ 730
+ ],
+ "succ": [
+ 736
+ ],
+ "asm": [
+ "6681FE00000100|cmp ESI,0x00010000",
+ "0F85AF0C|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 597,
+ "entry": "F000:073B",
+ "term": "F000:0742",
+ "pred": [
+ 594
+ ],
+ "succ": [
+ 600
+ ],
+ "asm": [
+ "6681FE00000100|cmp ESI,0x00010000",
+ "0F85810E|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 730,
+ "entry": "F000:0902",
+ "term": "F000:0909",
+ "pred": [
+ 727
+ ],
+ "succ": [
+ 733
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F85BA0C|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 600,
+ "entry": "F000:0746",
+ "term": "F000:0755",
+ "pred": [
+ 597
+ ],
+ "succ": [
+ 607
+ ],
+ "asm": [
+ "66BFFFFF0100|mov EDI,0x0001FFFF",
+ "B078|mov AL,0x78",
+ "26678803|mov byte ptr ES:[EBX],AL",
+ "3C00|cmp AL,0",
+ "AE|scas AL,byte ptr ES:[DI]",
+ "0F856E0E|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 727,
+ "entry": "F000:08FC",
+ "term": "F000:08FE",
+ "pred": [
+ 719
+ ],
+ "succ": [
+ 730
+ ],
+ "asm": [
+ "66A7|cmps dword ptr DS:[SI],dword ptr ES:[DI]",
+ "0F85C50C|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 607,
+ "entry": "F000:0759",
+ "term": "F000:0760",
+ "pred": [
+ 600
+ ],
+ "succ": [
+ 610
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F85630E|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 719,
+ "entry": "F000:08D8",
+ "term": "F000:08F8",
+ "pred": [
+ 716
+ ],
+ "succ": [
+ 727
+ ],
+ "asm": [
+ "66BEFCFF0100|mov ESI,0x0001FFFC",
+ "66BFFCFF0100|mov EDI,0x0001FFFC",
+ "66BBFCFF0000|mov EBX,0x0000FFFC",
+ "3E66678903|mov dword ptr DS:[EBX],EAX",
+ "2666678903|mov dword ptr ES:[EBX],EAX",
+ "6683F800|cmp EAX,0",
+ "0F84CB0C|je near 0x15C7"
+ ]
+ },
+ {
+ "id": 610,
+ "entry": "F000:0764",
+ "term": "F000:0783",
+ "pred": [
+ 607
+ ],
+ "succ": [
+ 621
+ ],
+ "asm": [
+ "66BEFFFF0100|mov ESI,0x0001FFFF",
+ "66BFFFFF0100|mov EDI,0x0001FFFF",
+ "B078|mov AL,0x78",
+ "3E678803|mov byte ptr DS:[EBX],AL",
+ "B000|mov AL,0",
+ "26678803|mov byte ptr ES:[EBX],AL",
+ "A4|movs byte ptr ES:[DI],byte ptr DS:[SI]",
+ "B078|mov AL,0x78",
+ "26673803|cmp byte ptr ES:[EBX],AL",
+ "0F85400E|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 716,
+ "entry": "F000:08CD",
+ "term": "F000:08D4",
+ "pred": [
+ 706
+ ],
+ "succ": [
+ 719
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F85EF0C|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 621,
+ "entry": "F000:0787",
+ "term": "F000:078E",
+ "pred": [
+ 610
+ ],
+ "succ": [
+ 624
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F85350E|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 706,
+ "entry": "F000:08A4",
+ "term": "F000:08C9",
+ "pred": [
+ 703
+ ],
+ "succ": [
+ 716
+ ],
+ "asm": [
+ "FC|cld",
+ "66BFFCFF0100|mov EDI,0x0001FFFC",
+ "66BBFCFF0000|mov EBX,0x0000FFFC",
+ "66B800000000|mov EAX,0",
+ "2666678903|mov dword ptr ES:[EBX],EAX",
+ "66B878563412|mov EAX,0x12345678",
+ "66AB|stos dword ptr ES:[DI],EAX",
+ "2666673903|cmp dword ptr ES:[EBX],EAX",
+ "0F85FA0C|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 624,
+ "entry": "F000:0792",
+ "term": "F000:0799",
+ "pred": [
+ 621
+ ],
+ "succ": [
+ 627
+ ],
+ "asm": [
+ "6681FE00000100|cmp ESI,0x00010000",
+ "0F852A0E|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 703,
+ "entry": "F000:0899",
+ "term": "F000:08A0",
+ "pred": [
+ 695
+ ],
+ "succ": [
+ 706
+ ],
+ "asm": [
+ "6681FE00000100|cmp ESI,0x00010000",
+ "0F85230D|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 627,
+ "entry": "F000:079D",
+ "term": "F000:07AF",
+ "pred": [
+ 624
+ ],
+ "succ": [
+ 635
+ ],
+ "asm": [
+ "66BEFFFF0100|mov ESI,0x0001FFFF",
+ "B078|mov AL,0x78",
+ "26678803|mov byte ptr ES:[EBX],AL",
+ "6631C0|xor EAX,EAX",
+ "AC|lods AL,byte ptr DS:[SI]",
+ "3C78|cmp AL,0x78",
+ "0F85140E|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 695,
+ "entry": "F000:0881",
+ "term": "F000:0895",
+ "pred": [
+ 692
+ ],
+ "succ": [
+ 703
+ ],
+ "asm": [
+ "66BEFEFF0100|mov ESI,0x0001FFFE",
+ "B87856|mov AX,0x5678",
+ "26678903|mov word ptr ES:[EBX],AX",
+ "6631C0|xor EAX,EAX",
+ "AD|lods AX,word ptr DS:[SI]",
+ "3D7856|cmp AX,0x5678",
+ "0F852E0D|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 635,
+ "entry": "F000:07B3",
+ "term": "F000:07BA",
+ "pred": [
+ 627
+ ],
+ "succ": [
+ 638
+ ],
+ "asm": [
+ "6681FE00000100|cmp ESI,0x00010000",
+ "0F85090E|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 692,
+ "entry": "F000:0876",
+ "term": "F000:087D",
+ "pred": [
+ 689
+ ],
+ "succ": [
+ 695
+ ],
+ "asm": [
+ "6681FE00000100|cmp ESI,0x00010000",
+ "0F85460D|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 638,
+ "entry": "F000:07BE",
+ "term": "F000:07DA",
+ "pred": [
+ 635
+ ],
+ "succ": [
+ 648
+ ],
+ "asm": [
+ "FC|cld",
+ "66BFFEFF0100|mov EDI,0x0001FFFE",
+ "66BBFEFF0000|mov EBX,0x0000FFFE",
+ "B80000|mov AX,0",
+ "26678903|mov word ptr ES:[EBX],AX",
+ "B87856|mov AX,0x5678",
+ "AB|stos word ptr ES:[DI],AX",
+ "26673903|cmp word ptr ES:[EBX],AX",
+ "0F85E90D|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 689,
+ "entry": "F000:086B",
+ "term": "F000:0872",
+ "pred": [
+ 678
+ ],
+ "succ": [
+ 692
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F85510D|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 648,
+ "entry": "F000:07DE",
+ "term": "F000:07E5",
+ "pred": [
+ 638
+ ],
+ "succ": [
+ 651
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F85DE0D|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 678,
+ "entry": "F000:0845",
+ "term": "F000:0867",
+ "pred": [
+ 675
+ ],
+ "succ": [
+ 689
+ ],
+ "asm": [
+ "66BEFEFF0100|mov ESI,0x0001FFFE",
+ "66BFFEFF0100|mov EDI,0x0001FFFE",
+ "B87856|mov AX,0x5678",
+ "3E678903|mov word ptr DS:[EBX],AX",
+ "B80000|mov AX,0",
+ "26678903|mov word ptr ES:[EBX],AX",
+ "A5|movs word ptr ES:[DI],word ptr DS:[SI]",
+ "B87856|mov AX,0x5678",
+ "26673903|cmp word ptr ES:[EBX],AX",
+ "0F855C0D|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 651,
+ "entry": "F000:07E9",
+ "term": "F000:0806",
+ "pred": [
+ 648
+ ],
+ "succ": [
+ 659
+ ],
+ "asm": [
+ "66BEFEFF0100|mov ESI,0x0001FFFE",
+ "66BFFEFF0100|mov EDI,0x0001FFFE",
+ "66BBFEFF0000|mov EBX,0x0000FFFE",
+ "3E678903|mov word ptr DS:[EBX],AX",
+ "26678903|mov word ptr ES:[EBX],AX",
+ "83F800|cmp AX,0",
+ "0F84BD0D|je near 0x15C7"
+ ]
+ },
+ {
+ "id": 675,
+ "entry": "F000:083A",
+ "term": "F000:0841",
+ "pred": [
+ 668
+ ],
+ "succ": [
+ 678
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F85820D|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 659,
+ "entry": "F000:080A",
+ "term": "F000:080B",
+ "pred": [
+ 651
+ ],
+ "succ": [
+ 662
+ ],
+ "asm": [
+ "A7|cmps word ptr DS:[SI],word ptr ES:[DI]",
+ "0F85B80D|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 668,
+ "entry": "F000:0825",
+ "term": "F000:0836",
+ "pred": [
+ 665
+ ],
+ "succ": [
+ 675
+ ],
+ "asm": [
+ "66BFFEFF0100|mov EDI,0x0001FFFE",
+ "B87856|mov AX,0x5678",
+ "26678903|mov word ptr ES:[EBX],AX",
+ "83F800|cmp AX,0",
+ "AF|scas AX,word ptr ES:[DI]",
+ "0F858D0D|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 662,
+ "entry": "F000:080F",
+ "term": "F000:0816",
+ "pred": [
+ 659
+ ],
+ "succ": [
+ 665
+ ],
+ "asm": [
+ "6681FF00000100|cmp EDI,0x00010000",
+ "0F85AD0D|jne near 0x15C7"
+ ]
+ },
+ {
+ "id": 665,
+ "entry": "F000:081A",
+ "term": "F000:0821",
+ "pred": [
+ 662
+ ],
+ "succ": [
+ 668
+ ],
+ "asm": [
+ "6681FE00000100|cmp ESI,0x00010000",
+ "0F85A20D|jne near 0x15C7"
+ ]
+ }
+ ],
+ "truncated": false
+}
\ No newline at end of file
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedListing/divfaultloop.txt b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedListing/divfaultloop.txt
new file mode 100644
index 0000000000..cb293192d9
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedListing/divfaultloop.txt
@@ -0,0 +1,24 @@
+F000:0000 mov AX,0
+F000:0003 mov SS,AX
+F000:0005 mov SP,0x0200
+F000:0008 mov word ptr DS:[0],0x0037
+F000:000E mov word ptr DS:[2],CS
+F000:0012 mov word ptr CS:[0x0100],0
+F000:0019 mov word ptr CS:[0x0102],0
+F000:0020 mov AX,0x000A
+F000:0023 mov BX,word ptr CS:[0x0102]
+F000:0028 div BX
+F000:0037 push BP
+F000:0038 mov BP,SP
+F000:003A push BX
+F000:003B mov BX,word ptr CS:[0x0100]
+F000:0040 inc BX
+F000:0041 mov word ptr CS:[0x0100],BX
+F000:0046 cmp BX,3
+F000:0049 jne short 0x0052
+F000:004B mov word ptr CS:[0x0102],5
+F000:0052 mov word ptr SS:[BP+2],0x0020
+F000:0057 pop BX
+F000:0058 pop BP
+F000:0059 iret
+F000:FFF0 jmp short 0
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedListing/returnedterminator.txt b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedListing/returnedterminator.txt
new file mode 100644
index 0000000000..e221839599
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedListing/returnedterminator.txt
@@ -0,0 +1,11 @@
+F000:0000 mov AX,0
+F000:0003 mov SS,AX
+F000:0005 mov SP,8
+F000:0008 jmp short 0x000A
+F000:000A mov AX,0x1111
+F000:000D push AX
+F000:000E mov AX,0x2222
+F000:0011 push AX
+F000:0012 jmp short 0x0019
+F000:0019 hlt
+F000:FFF0 jmp short 0
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedListing/selfmodifyterminator.txt b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedListing/selfmodifyterminator.txt
new file mode 100644
index 0000000000..084581297b
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedListing/selfmodifyterminator.txt
@@ -0,0 +1,24 @@
+F000:0000 mov AX,0
+F000:0003 mov SS,AX
+F000:0005 mov SP,4
+F000:0008 mov AX,0xFFFF
+F000:000B mov byte ptr DS:[0x0040],0
+F000:0010 push AX
+F000:0011 call near 0x0022
+F000:0014 mov AX,0x0042
+F000:0017 or AX,AX
+F000:0019 selector
+F000:0019 jmp short 0x0020
+F000:0019 jne short 0x0010
+F000:0020 push AX
+F000:0021 hlt
+F000:0022 cmp byte ptr DS:[0x0040],0
+F000:0027 je short 0x003B
+F000:0029 mov AX,0xF000
+F000:002C mov ES,AX
+F000:002E mov byte ptr ES:[0x0019],0xEB
+F000:0034 mov byte ptr ES:[0x001A],5
+F000:003A ret near
+F000:003B inc byte ptr DS:[0x0040]
+F000:003F ret near
+F000:FFF0 jmp short 0
diff --git a/tests/Spice86.Tests/Resources/cpuTests/res/DumpedListing/sticli.txt b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedListing/sticli.txt
new file mode 100644
index 0000000000..214dcf7d22
--- /dev/null
+++ b/tests/Spice86.Tests/Resources/cpuTests/res/DumpedListing/sticli.txt
@@ -0,0 +1,8 @@
+F000:0000 mov AX,0x1000
+F000:0003 mov DS,AX
+F000:0005 sti
+F000:0006 mov AX,0x1234
+F000:0009 cli
+F000:000A mov word ptr DS:[SI],AX
+F000:000C hlt
+F000:FFF0 jmp short 0
diff --git a/tests/Spice86.Tests/Resources/cpuTests/returnedterminator.bin b/tests/Spice86.Tests/Resources/cpuTests/returnedterminator.bin
new file mode 100644
index 0000000000..c46749aa52
Binary files /dev/null and b/tests/Spice86.Tests/Resources/cpuTests/returnedterminator.bin differ
diff --git a/tests/Spice86.Tests/Resources/cpuTests/selfmodifyterminator.bin b/tests/Spice86.Tests/Resources/cpuTests/selfmodifyterminator.bin
new file mode 100644
index 0000000000..c7c83453df
Binary files /dev/null and b/tests/Spice86.Tests/Resources/cpuTests/selfmodifyterminator.bin differ
diff --git a/tests/Spice86.Tests/Resources/cpuTests/sticli.bin b/tests/Spice86.Tests/Resources/cpuTests/sticli.bin
new file mode 100644
index 0000000000..43bcb4d6ec
Binary files /dev/null and b/tests/Spice86.Tests/Resources/cpuTests/sticli.bin differ
diff --git a/tests/Spice86.Tests/UI/CfgCpu/CfgCpuViewModelTest.cs b/tests/Spice86.Tests/UI/CfgCpu/CfgCpuViewModelTest.cs
new file mode 100644
index 0000000000..2ae160f9b2
--- /dev/null
+++ b/tests/Spice86.Tests/UI/CfgCpu/CfgCpuViewModelTest.cs
@@ -0,0 +1,700 @@
+namespace Spice86.Tests.UI.CfgCpu;
+
+using AvaloniaGraphControl;
+
+using FluentAssertions;
+
+using NSubstitute;
+
+using Spice86.Core.CLI;
+using Spice86.Core.Emulator.CPU;
+using Spice86.Core.Emulator.CPU.CfgCpu;
+using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
+using Spice86.Core.Emulator.CPU.CfgCpu.Feeder;
+using Spice86.Core.Emulator.CPU.CfgCpu.InstructionExecutor.Expressions;
+using Spice86.Core.Emulator.CPU.CfgCpu.InstructionRenderer;
+using Spice86.Core.Emulator.CPU.CfgCpu.Linker;
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
+using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.SelfModifying;
+using Spice86.Core.Emulator.StateSerialization.ControlFlow;
+using Spice86.Core.Emulator.Memory;
+using Spice86.Core.Emulator.Memory.Mmu;
+using Spice86.Core.Emulator.VM;
+using Spice86.Core.Emulator.VM.Breakpoint;
+using Spice86.Shared.Emulator.Memory;
+using Spice86.Shared.Interfaces;
+using Spice86.ViewModels;
+using Spice86.ViewModels.Services;
+
+using Xunit;
+
+///
+/// UI tests for validating block-centric rendering, indicators,
+/// search, edge styling, and table behaviour.
+///
+///
+///
+/// These tests build synthetic graphs through the production
+/// using parsed s. The view model is
+/// exercised at the model layer (no AXAML rendering): its behaviour is observed through the
+/// , ,
+/// and
+/// observables. The dashed-outline + ellipsis styling for in-discovery blocks is
+/// rendered by the AXAML view; here we assert the model-level contract surfaced on
+/// and the trailing "…" token in the node
+/// listing.
+///
+///
+/// Each test builds its own minimal CFG, wires a real
+/// via the same construction pattern as the existing CfgNodeFeederTest, and constructs a
+/// fresh that uses the synthesised CFG as its data source.
+/// The view model is then driven through its public
+/// so the same code path the UI uses is
+/// exercised end-to-end.
+///
+///
+public class CfgCpuViewModelTest : IDisposable {
+ private static readonly AsmRenderingConfig AsmConfig = AsmRenderingConfig.CreateSpice86Style();
+
+ ///
+ /// Test harness wiring a minimal real CFG infrastructure: ,
+ /// , , ,
+ /// and a real exposing a writable
+ /// .
+ ///
+ private sealed class Harness : IDisposable {
+ private readonly CfgNodeExecutionCompiler _compiler;
+ private readonly CfgNodeExecutionCompilerMonitor _monitor;
+ private readonly PauseHandler _pauseHandler;
+ private readonly ILoggerService _loggerService;
+
+ public Harness() {
+ _loggerService = Substitute.For();
+ AddressReadWriteBreakpoints memoryBreakpoints = new();
+ AddressReadWriteBreakpoints ioBreakpoints = new();
+ Memory = new Memory(memoryBreakpoints, new Ram(0x100000), new A20Gate(), new RealModeMmu386(), false);
+ State = new State(CpuModel.INTEL_80286);
+ _pauseHandler = new PauseHandler(_loggerService);
+ EmulatorBreakpointsManager breakpointsManager = new(_pauseHandler, State, Memory, memoryBreakpoints, ioBreakpoints);
+ InstructionReplacerRegistry replacerRegistry = new();
+ _monitor = new CfgNodeExecutionCompilerMonitor(_loggerService);
+ _compiler = new CfgNodeExecutionCompiler(_monitor, _loggerService, JitMode.InterpretedOnly);
+ CfgNodeFeeder cfgNodeFeeder = new(Memory, State, breakpointsManager, replacerRegistry, _compiler);
+ Linker = new NodeLinker(replacerRegistry, _compiler, new CfgNodeIdAllocator());
+ ContextManager = new ExecutionContextManager(
+ Memory, State, cfgNodeFeeder, replacerRegistry,
+ new Spice86.Core.Emulator.Function.FunctionCatalogue(),
+ useCodeOverride: false,
+ loggerService: _loggerService,
+ cpuHeavyLogger: null);
+ // Reuse the InstructionParser pattern from TestInstructionHelper but bind it to
+ // OUR memory/state so parsed instructions land at the addresses we wire through
+ // the linker.
+ InstructionHelper = new TestInstructionHelperBoundTo(Memory, State);
+ }
+
+ public Memory Memory { get; }
+
+ public State State { get; }
+
+ public NodeLinker Linker { get; }
+
+ public ExecutionContextManager ContextManager { get; }
+
+ public TestInstructionHelperBoundTo InstructionHelper { get; }
+
+ public ExecutionContext Context => ContextManager.CurrentExecutionContext;
+
+ public ICfgNode Link(ICfgNode from, ICfgNode to,
+ InstructionSuccessorType linkType = InstructionSuccessorType.Normal) =>
+ Linker.Link(linkType, from, to);
+
+ public void Dispose() {
+ _compiler.Dispose();
+ _monitor.Dispose();
+ _pauseHandler.Dispose();
+ }
+ }
+
+ ///
+ /// Minimal helper bound to a specific + pair (so
+ /// the parsed s are wired against the same memory the
+ /// harness uses). Mirrors the production parser plumbing without depending on a fresh
+ /// memory/state pair.
+ ///
+ private sealed class TestInstructionHelperBoundTo {
+ private readonly Memory _memory;
+ private readonly Spice86.Core.Emulator.CPU.CfgCpu.Parser.InstructionParser _parser;
+
+ public TestInstructionHelperBoundTo(Memory memory, State state) {
+ _memory = memory;
+ _parser = new Spice86.Core.Emulator.CPU.CfgCpu.Parser.InstructionParser(memory, state, new CfgNodeIdAllocator());
+ }
+
+ public CfgInstruction WriteAndParse(SegmentedAddress address,
+ Action write) {
+ Spice86.Core.Emulator.InterruptHandlers.Common.MemoryWriter.MemoryAsmWriter writer =
+ new(_memory, address);
+ write(writer);
+ return _parser.ParseInstructionAt(address);
+ }
+ }
+
+ private readonly Harness _harness;
+
+ public CfgCpuViewModelTest() {
+ _harness = new Harness();
+ }
+
+ public void Dispose() {
+ _harness.Dispose();
+ }
+
+ ///
+ /// Creates a fresh wired against the harness's real
+ /// , with a real and a
+ /// synchronous stand-in so command continuations execute
+ /// inline.
+ ///
+ private CfgCpuViewModel CreateViewModel() {
+ IUIDispatcher uiDispatcher = new InlineUIDispatcher();
+ IPauseHandler pauseHandler = Substitute.For();
+ NodeToString nodeToString = new(AsmConfig);
+ CfgBlockGraphExporter graphExporter = new();
+ return new CfgCpuViewModel(uiDispatcher, _harness.ContextManager, pauseHandler,
+ nodeToString, AsmConfig, graphExporter);
+ }
+
+ ///
+ /// Builds a tiny three-block linear chain. Returns the entry instructions of each block
+ /// in order along with the explicit terminators so tests can assert on per-block layout.
+ ///
+ ///
+ /// Layout:
+ ///
+ /// blk0: mov ax, 0x1234 ; @ 0x1000:0x0000
+ /// mov bx, 0x5678 ; @ 0x1000:0x0003
+ /// jmp short blk1 ; @ 0x1000:0x0006 (block terminator)
+ /// blk1: mov cx, 0xCAFE ; @ 0x1000:0x0008 (block entry)
+ /// ret ; @ 0x1000:0x000B (block terminator)
+ /// blk2: mov dx, 0xBEEF ; @ 0x1000:0x000C (block entry)
+ /// nop ; @ 0x1000:0x000F
+ /// retf ; @ 0x1000:0x0010 (block terminator)
+ ///
+ ///
+ private (CfgInstruction blk0Mov, CfgInstruction blk0Jmp, CfgInstruction blk1Mov,
+ CfgInstruction blk1Ret, CfgInstruction blk2Mov, CfgInstruction blk2Retf)
+ BuildLinearThreeBlockChain() {
+ SegmentedAddress addrBlk0Mov1 = new(0x1000, 0x0000);
+ // mov ax, 0x1234 -> B8 34 12 (3 bytes)
+ CfgInstruction blk0Mov1 = _harness.InstructionHelper.WriteAndParse(addrBlk0Mov1, w => {
+ w.WriteUInt8(0xB8);
+ w.WriteUInt8(0x34);
+ w.WriteUInt8(0x12);
+ });
+
+ SegmentedAddress addrBlk0Mov2 = new(0x1000, 0x0003);
+ // mov bx, 0x5678 -> BB 78 56 (3 bytes)
+ CfgInstruction blk0Mov2 = _harness.InstructionHelper.WriteAndParse(addrBlk0Mov2, w => {
+ w.WriteUInt8(0xBB);
+ w.WriteUInt8(0x78);
+ w.WriteUInt8(0x56);
+ });
+
+ SegmentedAddress addrBlk0Jmp = new(0x1000, 0x0006);
+ // jmp short +0 -> EB 00 (2 bytes), targets 0x0008
+ CfgInstruction blk0Jmp = _harness.InstructionHelper.WriteAndParse(addrBlk0Jmp, w => {
+ w.WriteUInt8(0xEB);
+ w.WriteUInt8(0x00);
+ });
+
+ SegmentedAddress addrBlk1Mov = new(0x1000, 0x0008);
+ // mov cx, 0xCAFE -> B9 FE CA (3 bytes)
+ CfgInstruction blk1Mov = _harness.InstructionHelper.WriteAndParse(addrBlk1Mov, w => {
+ w.WriteUInt8(0xB9);
+ w.WriteUInt8(0xFE);
+ w.WriteUInt8(0xCA);
+ });
+
+ SegmentedAddress addrBlk1Ret = new(0x1000, 0x000B);
+ // ret near -> C3 (1 byte)
+ CfgInstruction blk1Ret = _harness.InstructionHelper.WriteAndParse(addrBlk1Ret,
+ w => w.WriteUInt8(0xC3));
+
+ SegmentedAddress addrBlk2Mov = new(0x1000, 0x000C);
+ // mov dx, 0xBEEF -> BA EF BE (3 bytes)
+ CfgInstruction blk2Mov = _harness.InstructionHelper.WriteAndParse(addrBlk2Mov, w => {
+ w.WriteUInt8(0xBA);
+ w.WriteUInt8(0xEF);
+ w.WriteUInt8(0xBE);
+ });
+
+ SegmentedAddress addrBlk2Nop = new(0x1000, 0x000F);
+ CfgInstruction blk2Nop = _harness.InstructionHelper.WriteAndParse(addrBlk2Nop,
+ w => w.WriteUInt8(0x90));
+
+ SegmentedAddress addrBlk2Retf = new(0x1000, 0x0010);
+ // retf -> CB (1 byte)
+ CfgInstruction blk2Retf = _harness.InstructionHelper.WriteAndParse(addrBlk2Retf,
+ w => w.WriteUInt8(0xCB));
+
+ // Wire edges through the linker: this builds CfgBlocks with proper back-pointers and
+ // proper IsDiscoveryComplete transitions on each boundary.
+ _harness.Link(blk0Mov1, blk0Mov2);
+ _harness.Link(blk0Mov2, blk0Jmp);
+ _harness.Link(blk0Jmp, blk1Mov);
+ _harness.Link(blk1Mov, blk1Ret);
+ _harness.Link(blk1Ret, blk2Mov);
+ _harness.Link(blk2Mov, blk2Nop);
+ _harness.Link(blk2Nop, blk2Retf);
+
+ return (blk0Mov1, blk0Jmp, blk1Mov, blk1Ret, blk2Mov, blk2Retf);
+ }
+
+ private static Task UpdateGraphAndWait(CfgCpuViewModel viewModel) =>
+ viewModel.UpdateGraphCommand.ExecuteAsync(null);
+
+ private static Task SearchAndWait(CfgCpuViewModel viewModel) =>
+ viewModel.SearchNodeCommand.ExecuteAsync(null);
+
+ // -----------------------------------------------------------------------
+ // Tests
+ // -----------------------------------------------------------------------
+
+ ///
+ /// One graph node is rendered per , and each block's
+ /// rendered listing contains its contained instructions in order.
+ ///
+ [Fact]
+ public async Task Renders_OneNodePerCfgBlock_WithListing() {
+ // Arrange
+ (CfgInstruction blk0Mov, CfgInstruction blk0Jmp, CfgInstruction blk1Mov, CfgInstruction blk1Ret, CfgInstruction blk2Mov, CfgInstruction blk2Retf) nodes = BuildLinearThreeBlockChain();
+ _harness.ContextManager.ExecutingNode = nodes.blk0Mov;
+ CfgCpuViewModel viewModel = CreateViewModel();
+ viewModel.MaxNodesToDisplay = 50;
+
+ // Act
+ await UpdateGraphAndWait(viewModel);
+
+ // Assert — view model produces exactly one CfgGraphNode per CfgBlock visited.
+ viewModel.Graph.Should().NotBeNull("UpdateGraph must produce a non-null Graph");
+ viewModel.NumberOfNodes.Should().BeGreaterThan(0,
+ "the view model must visit and render at least one block");
+
+ // The CfgBlocks reachable from the seed: blk0, blk1, blk2 (three blocks).
+ viewModel.NumberOfNodes.Should().Be(3,
+ "exactly one CfgGraphNode is rendered per CfgBlock; the three-block chain " +
+ "should produce three graph nodes");
+
+ // Each block's listing concatenates its instructions in order. We verify the listing
+ // text of the entry block (which is what the user sees first in the graph view).
+ CfgBlock blk0Block = nodes.blk0Mov.ContainingBlock ?? throw new InvalidOperationException("blk0Mov.ContainingBlock should not be null");
+ CfgGraphNode entryBlockGraphNode = FindGraphNodeForBlock(viewModel, blk0Block);
+ string text = entryBlockGraphNode.ToString();
+ text.Should().Contain("mov", "the listing must include the rendered MOV instructions");
+ text.Should().Contain("jmp", "the listing must include the rendered JMP terminator");
+ // Order: the first MOV is rendered before the second MOV which is rendered before JMP.
+ int idxMov1 = text.IndexOf("0x1234", StringComparison.OrdinalIgnoreCase);
+ int idxMov2 = text.IndexOf("0x5678", StringComparison.OrdinalIgnoreCase);
+ int idxJmp = text.IndexOf("jmp", StringComparison.OrdinalIgnoreCase);
+ idxMov1.Should().BeGreaterThanOrEqualTo(0);
+ idxMov2.Should().BeGreaterThan(idxMov1,
+ "the listing must preserve instruction order — first MOV (0x1234) appears before second MOV (0x5678)");
+ idxJmp.Should().BeGreaterThan(idxMov2,
+ "the listing must preserve instruction order — second MOV appears before the JMP terminator");
+ }
+
+ ///
+ /// Block-to-block edges are styled by deriving the from
+ /// the source block's terminator's .
+ ///
+ [Fact]
+ public async Task Renders_BlockEdgesWithStyling() {
+ // Arrange
+ (CfgInstruction blk0Mov, CfgInstruction blk0Jmp, CfgInstruction blk1Mov, CfgInstruction blk1Ret, CfgInstruction blk2Mov, CfgInstruction blk2Retf) nodes = BuildLinearThreeBlockChain();
+ _harness.ContextManager.ExecutingNode = nodes.blk0Mov;
+ CfgCpuViewModel viewModel = CreateViewModel();
+ viewModel.MaxNodesToDisplay = 50;
+
+ // Act
+ await UpdateGraphAndWait(viewModel);
+
+ // Assert
+ viewModel.Graph.Should().NotBeNull();
+ CfgBlock blk0 = nodes.blk0Mov.ContainingBlock ?? throw new InvalidOperationException("blk0Mov.ContainingBlock should not be null");
+ CfgBlock blk1 = nodes.blk1Mov.ContainingBlock ?? throw new InvalidOperationException("blk1Mov.ContainingBlock should not be null");
+ CfgBlock blk2 = nodes.blk2Mov.ContainingBlock ?? throw new InvalidOperationException("blk2Mov.ContainingBlock should not be null");
+
+ // The blk0 → blk1 edge derives from blk0's terminator (a JMP) → CfgEdgeType.Jump.
+ CfgGraphEdgeLabel jmpEdge = FindEdgeLabelBetween(viewModel, blk0.Id, blk1.Id);
+ jmpEdge.EdgeType.Should().Be(CfgEdgeType.Jump,
+ "an edge whose source block ends in a JMP must be styled CfgEdgeType.Jump");
+
+ // The blk1 → blk2 edge derives from blk1's terminator (a near RET) → CfgEdgeType.Return.
+ CfgGraphEdgeLabel retEdge = FindEdgeLabelBetween(viewModel, blk1.Id, blk2.Id);
+ retEdge.EdgeType.Should().Be(CfgEdgeType.Return,
+ "an edge whose source block ends in a RET must be styled CfgEdgeType.Return");
+ }
+
+ ///
+ /// A block whose is false renders with a
+ /// distinct "stale" indicator surfaced as .
+ ///
+ [Fact]
+ public async Task IsLiveFalse_RendersStaleIndicator() {
+ // Arrange
+ (CfgInstruction blk0Mov, CfgInstruction blk0Jmp, CfgInstruction blk1Mov, CfgInstruction blk1Ret, CfgInstruction blk2Mov, CfgInstruction blk2Retf) nodes = BuildLinearThreeBlockChain();
+ _harness.ContextManager.ExecutingNode = nodes.blk0Mov;
+
+ // Mark every instruction in blk1 as non-live so blk1.IsLive observably becomes false.
+ nodes.blk1Mov.SetLive(false);
+ nodes.blk1Ret.SetLive(false);
+ CfgBlock blk1PreCheck = nodes.blk1Mov.ContainingBlock ?? throw new InvalidOperationException("blk1Mov.ContainingBlock should not be null");
+ blk1PreCheck.IsLive.Should().BeFalse("the test pre-condition requires blk1 to be non-live");
+
+ CfgCpuViewModel viewModel = CreateViewModel();
+ viewModel.MaxNodesToDisplay = 50;
+
+ // Act
+ await UpdateGraphAndWait(viewModel);
+
+ // Assert — the view model surfaces the IsLive flag on the rendered graph node so the
+ // AXAML view can apply stale styling.
+ CfgBlock blk0 = nodes.blk0Mov.ContainingBlock ?? throw new InvalidOperationException("blk0Mov.ContainingBlock should not be null");
+ CfgBlock blk1 = nodes.blk1Mov.ContainingBlock ?? throw new InvalidOperationException("blk1Mov.ContainingBlock should not be null");
+ CfgGraphNode liveBlk0 = FindGraphNodeForBlock(viewModel, blk0);
+ CfgGraphNode staleBlk1 = FindGraphNodeForBlock(viewModel, blk1);
+ liveBlk0.IsLive.Should().BeTrue(
+ "a block whose contained instructions are all live must render with IsLive=true");
+ staleBlk1.IsLive.Should().BeFalse(
+ "a block with at least one non-live instruction must render with IsLive=false so " +
+ "the AXAML view can apply the stale visual indicator");
+ }
+
+ ///
+ /// A block whose is false
+ /// renders with a trailing ellipsis on its listing and a discovery-complete flag exposed
+ /// to the view (the dashed outline is rendered by AXAML based on this flag).
+ ///
+ [Fact]
+ public async Task IsDiscoveryCompleteFalse_RendersDashedOutlineAndEllipsis() {
+ // Arrange — build a two-block chain where the second block is in-discovery: the
+ // linker creates the second block when the predecessor's terminator (a JMP) crosses
+ // a boundary, but the second block's tail is never closed because we never observe
+ // a boundary or another successor on it.
+ SegmentedAddress addrJmp = new(0x2000, 0x0000);
+ // jmp short +0 -> EB 00 (2 bytes), targets 0x0002
+ CfgInstruction jmp = _harness.InstructionHelper.WriteAndParse(addrJmp, w => {
+ w.WriteUInt8(0xEB);
+ w.WriteUInt8(0x00);
+ });
+
+ SegmentedAddress addrNop1 = new(0x2000, 0x0002);
+ CfgInstruction nop1 = _harness.InstructionHelper.WriteAndParse(addrNop1,
+ w => w.WriteUInt8(0x90));
+
+ SegmentedAddress addrNop2 = new(0x2000, 0x0003);
+ CfgInstruction nop2 = _harness.InstructionHelper.WriteAndParse(addrNop2,
+ w => w.WriteUInt8(0x90));
+
+ // Linking jmp → nop1 finalises the JMP block (the JMP is itself a terminator) and
+ // creates a new block starting at nop1.
+ _harness.Link(jmp, nop1);
+ // Linking nop1 → nop2 extends the second block; it remains in-discovery because no
+ // boundary is observed for nop2's successor yet.
+ _harness.Link(nop1, nop2);
+
+ // Verify pre-condition.
+ CfgBlock jmpBlock = jmp.ContainingBlock ?? throw new InvalidOperationException("jmp.ContainingBlock should not be null");
+ CfgBlock nopBlock = nop1.ContainingBlock ?? throw new InvalidOperationException("nop1.ContainingBlock should not be null");
+ nopBlock.IsDiscoveryComplete.Should().BeFalse(
+ "the pre-condition requires the second block to be in-discovery: no " +
+ "boundary has been observed for its successor yet");
+
+ _harness.ContextManager.ExecutingNode = jmp;
+ CfgCpuViewModel viewModel = CreateViewModel();
+ viewModel.MaxNodesToDisplay = 50;
+
+ // Act
+ await UpdateGraphAndWait(viewModel);
+
+ // Assert
+ CfgGraphNode rendered = FindGraphNodeForBlock(viewModel, nopBlock);
+ rendered.IsDiscoveryComplete.Should().BeFalse(
+ "a CfgBlock with IsDiscoveryComplete==false must surface that flag on its " +
+ "CfgGraphNode so the AXAML view can apply the dashed-outline indicator");
+
+ string text = rendered.ToString();
+ text.Should().EndWith("\u2026",
+ "an in-discovery block's listing must end with the trailing '…' marker");
+
+ // The JMP block (which IS discovery-complete because the JMP is itself a terminator)
+ // must NOT carry the trailing ellipsis nor the in-discovery flag.
+ CfgGraphNode jmpRendered = FindGraphNodeForBlock(viewModel, jmpBlock);
+ jmpRendered.IsDiscoveryComplete.Should().BeTrue(
+ "a discovery-complete block must surface IsDiscoveryComplete=true");
+ jmpRendered.ToString().Should().NotEndWith("\u2026",
+ "a discovery-complete block must NOT carry the trailing '…' marker");
+ }
+
+ ///
+ /// When the executing is contained in a block,
+ /// that block gets a red border (via ) and the
+ /// per-instruction line carries a single 🔴 marker.
+ ///
+ [Fact]
+ public async Task Executing_BlockHasRedBorderAndInstructionHasMarker() {
+ // Arrange
+ (CfgInstruction blk0Mov, CfgInstruction blk0Jmp, CfgInstruction blk1Mov, CfgInstruction blk1Ret, CfgInstruction blk2Mov, CfgInstruction blk2Retf) nodes = BuildLinearThreeBlockChain();
+ // Executing node is the second instruction inside blk0 (interior to the block, not the
+ // terminator) so the per-instruction marker landing on a non-entry, non-terminator
+ // instruction is verified.
+ CfgBlock blk0 = nodes.blk0Mov.ContainingBlock ?? throw new InvalidOperationException("blk0Mov.ContainingBlock should not be null");
+ ICfgNode blk0Mov2 = blk0.Instructions[1];
+ _harness.ContextManager.ExecutingNode = blk0Mov2;
+
+ CfgCpuViewModel viewModel = CreateViewModel();
+ viewModel.MaxNodesToDisplay = 50;
+
+ // Act
+ await UpdateGraphAndWait(viewModel);
+
+ // Assert — the containing block is flagged as executing (gets red border in the view).
+ CfgGraphNode renderedBlk0 = FindGraphNodeForBlock(viewModel, blk0);
+ renderedBlk0.IsExecuting.Should().BeTrue(
+ "the block containing the executing node must surface IsExecuting=true on its " +
+ "CfgGraphNode so the AXAML view can apply the red border");
+
+ // Exactly one 🔴 marker: on the per-instruction line only. The block title carries
+ // no dot — the red border is the block-level indicator instead.
+ string text = renderedBlk0.ToString();
+ int markerCount = CountSubstringOccurrences(text, "\U0001f534");
+ markerCount.Should().Be(1,
+ "exactly one 🔴 marker must appear: on the executing instruction line; " +
+ $"the block title must not carry a dot (red border is used instead); observed {markerCount} markers");
+
+ // The other blocks must not be marked.
+ CfgBlock blk1 = nodes.blk1Mov.ContainingBlock ?? throw new InvalidOperationException("blk1Mov.ContainingBlock should not be null");
+ CfgGraphNode renderedBlk1 = FindGraphNodeForBlock(viewModel, blk1);
+ renderedBlk1.IsExecuting.Should().BeFalse(
+ "blocks not containing the executing node must surface IsExecuting=false");
+ }
+
+ ///
+ /// Search navigates to the block containing an instruction matching the search
+ /// term. The search rebuilds the graph rooted on that block.
+ ///
+ [Fact]
+ public async Task Search_NavigatesToContainingBlock() {
+ // Arrange
+ (CfgInstruction blk0Mov, CfgInstruction blk0Jmp, CfgInstruction blk1Mov, CfgInstruction blk1Ret, CfgInstruction blk2Mov, CfgInstruction blk2Retf) nodes = BuildLinearThreeBlockChain();
+ _harness.ContextManager.ExecutingNode = nodes.blk0Mov;
+ CfgCpuViewModel viewModel = CreateViewModel();
+ // Set MaxNodesToDisplay to 1 so the initial BFS rooted at blk0 only renders blk0;
+ // searching for blk2's content should then re-root the BFS at blk2 and render blk2
+ // (rather than the initial blk0). This makes the navigation observable in the
+ // rendered Graph rather than just in transient StatusMessage state.
+ viewModel.MaxNodesToDisplay = 1;
+
+ // Build the initial graph so the searchable index is populated.
+ await UpdateGraphAndWait(viewModel);
+
+ // The 0xBEEF immediate is unique to blk2's first MOV; searching for it must navigate
+ // to blk2's containing block.
+ viewModel.SearchText = "0xBEEF";
+
+ // Act
+ await SearchAndWait(viewModel);
+
+ // Assert — after navigation, the rendered graph is re-rooted on blk2 and renders it.
+ // (With MaxNodesToDisplay=1, only the seed block is rendered, so we can directly
+ // verify the seed is now blk2, not blk0.)
+ viewModel.Graph.Should().NotBeNull();
+ viewModel.NumberOfNodes.Should().Be(1,
+ "MaxNodesToDisplay caps the BFS to one block; searching must re-root the BFS on " +
+ "the matched block");
+
+ // The single rendered block must be blk2 (the search target), not the original seed
+ // blk0. We verify this by checking that the searchable text from blk2's instructions
+ // is part of the rendered NodeEntries.
+ viewModel.NodeEntries.Should().Contain(entry =>
+ entry.Contains("0xBEEF", StringComparison.OrdinalIgnoreCase),
+ "after search, the rendered NodeEntries must come from blk2 — the block " +
+ "containing the matched instruction");
+ viewModel.NodeEntries.Should().NotContain(entry =>
+ entry.Contains("0x1234", StringComparison.OrdinalIgnoreCase),
+ "after search, the rendered NodeEntries must NOT come from blk0 (since the BFS " +
+ "is re-rooted on blk2 and MaxNodesToDisplay=1)");
+ }
+
+ ///
+ /// caps the number of CfgBlocks
+ /// rendered. The other controls (AutoFollow, search, table view, table filter) remain
+ /// available with their existing semantics.
+ ///
+ [Fact]
+ public async Task MaxNodesToDisplay_CapsBlockCount() {
+ // Arrange — three-block chain. Setting MaxNodesToDisplay to 2 must cap the BFS at
+ // two blocks.
+ (CfgInstruction blk0Mov, CfgInstruction blk0Jmp, CfgInstruction blk1Mov, CfgInstruction blk1Ret, CfgInstruction blk2Mov, CfgInstruction blk2Retf) nodes = BuildLinearThreeBlockChain();
+ _harness.ContextManager.ExecutingNode = nodes.blk0Mov;
+ CfgCpuViewModel viewModel = CreateViewModel();
+ viewModel.MaxNodesToDisplay = 2;
+
+ // Act
+ await UpdateGraphAndWait(viewModel);
+
+ // Assert — at most two blocks rendered.
+ viewModel.NumberOfNodes.Should().Be(2,
+ "MaxNodesToDisplay must cap the number of CfgBlocks visited by the BFS");
+
+ // Table filter is preserved: setting a filter must reduce the rendered table rows
+ // (the filter matches Address/Assembly/Type substrings).
+ int unfilteredRowCount = viewModel.TableNodes.Count;
+ unfilteredRowCount.Should().BeGreaterThan(0,
+ "the table view must initially contain rows derived from the rendered blocks");
+ viewModel.TableFilter = "ZZZNoMatchAtAll";
+ viewModel.TableNodes.Count.Should().Be(0,
+ "TableFilter must filter rendered rows by substring match; setting an " +
+ "impossible filter must produce zero rows");
+
+ viewModel.TableFilter = string.Empty;
+ viewModel.TableNodes.Count.Should().Be(unfilteredRowCount,
+ "clearing TableFilter must restore the previously-rendered rows");
+
+ // AutoFollow control is still settable.
+ viewModel.AutoFollow = false;
+ viewModel.AutoFollow.Should().BeFalse("AutoFollow control must remain available");
+ }
+
+ ///
+ /// A block whose terminator is a renders block-level
+ /// edges with styling.
+ ///
+ [Fact]
+ public async Task SelectorTerminatedBlock_UsesSelectorEdgeStyling() {
+ // Arrange — build a graph where two CfgInstructions land at the same address, which
+ // forces the linker to inject a SelectorNode between the predecessor and the two
+ // variants. The predecessor's CfgBlock terminates at the SelectorNode.
+ SegmentedAddress addrPred = new(0x3000, 0x0000);
+ // Use a NOP as the predecessor: a non-control-flow single-byte instruction.
+ CfgInstruction predecessor = _harness.InstructionHelper.WriteAndParse(addrPred,
+ w => w.WriteUInt8(0x90));
+
+ // First variant at 0x3000:0x0001 (NOP), parsed from current memory.
+ SegmentedAddress addrVariants = new(0x3000, 0x0001);
+ CfgInstruction variantA = _harness.InstructionHelper.WriteAndParse(addrVariants,
+ w => w.WriteUInt8(0x90));
+
+ // Overwrite memory at 0x3000:0x0001 with a different opcode so the second parse
+ // produces a CfgInstruction with a different signature, but the same address. We
+ // pick a HLT (0xF4) — a different one-byte instruction with a distinct signature.
+ CfgInstruction variantB = _harness.InstructionHelper.WriteAndParse(addrVariants,
+ w => w.WriteUInt8(0xF4));
+
+ // Wire predecessor → variantA first.
+ _harness.Link(predecessor, variantA);
+
+ // Now wire predecessor → variantB. Since variantA already occupies addrVariants in
+ // the predecessor's SuccessorsPerAddress, the linker injects a SelectorNode between
+ // predecessor and {variantA, variantB} via CreateSelectorNodeBetween.
+ _harness.Link(predecessor, variantB);
+
+ // The predecessor's block now has the SelectorNode as its terminator.
+ CfgBlock predecessorBlock = predecessor.ContainingBlock ?? throw new InvalidOperationException("predecessor.ContainingBlock should not be null");
+ predecessorBlock.Terminator.Should().BeOfType(
+ "the predecessor's block must have a SelectorNode as its terminator");
+
+ _harness.ContextManager.ExecutingNode = predecessor;
+ CfgCpuViewModel viewModel = CreateViewModel();
+ viewModel.MaxNodesToDisplay = 50;
+
+ // Act
+ await UpdateGraphAndWait(viewModel);
+
+ // Assert — every edge whose source is the SelectorNode-terminated block uses the
+ // Selector edge styling.
+ Graph graph = viewModel.Graph.Should().NotBeNull("UpdateGraph must produce a non-null Graph").And.BeOfType().Subject;
+ List selectorEdgeLabels = graph.Edges
+ .Select(e => e.Label as CfgGraphEdgeLabel)
+ .OfType()
+ .Where(l => l.EdgeType == CfgEdgeType.Selector)
+ .ToList();
+ selectorEdgeLabels.Should().NotBeEmpty(
+ "edges originating from a SelectorNode-terminated CfgBlock must be rendered with " +
+ "CfgEdgeType.Selector styling");
+ }
+
+ // -----------------------------------------------------------------------
+ // Helpers
+ // -----------------------------------------------------------------------
+
+ ///
+ /// Locates the rendered for in the
+ /// view model's . Throws if not found.
+ ///
+ private static CfgGraphNode FindGraphNodeForBlock(CfgCpuViewModel viewModel, CfgBlock block) {
+ Graph graph = viewModel.Graph.Should().NotBeNull("UpdateGraph must produce a non-null Graph").And.BeOfType().Subject;
+ foreach (Edge edge in graph.Edges) {
+ if (edge.Tail is CfgGraphNode tail && tail.NodeId == block.Id) {
+ return tail;
+ }
+ if (edge.Head is CfgGraphNode head && head.NodeId == block.Id) {
+ return head;
+ }
+ }
+ throw new InvalidOperationException(
+ $"CfgGraphNode for block id {block.Id} not found in the rendered graph; " +
+ $"the BFS may have stopped before reaching it.");
+ }
+
+ ///
+ /// Locates the edge label between the rendered nodes for and
+ /// . Throws if not found.
+ ///
+ private static CfgGraphEdgeLabel FindEdgeLabelBetween(
+ CfgCpuViewModel viewModel, int fromBlockId, int toBlockId) {
+ Graph graph = viewModel.Graph.Should().NotBeNull("UpdateGraph must produce a non-null Graph").And.BeOfType().Subject;
+ Edge? matchingEdge = graph.Edges.FirstOrDefault(edge =>
+ edge.Tail is CfgGraphNode from
+ && edge.Head is CfgGraphNode to
+ && from.NodeId == fromBlockId
+ && to.NodeId == toBlockId
+ && edge.Label is CfgGraphEdgeLabel);
+
+ if (matchingEdge?.Label is CfgGraphEdgeLabel label) {
+ return label;
+ }
+ throw new InvalidOperationException(
+ $"Edge from block {fromBlockId} to block {toBlockId} not found in the rendered graph");
+ }
+
+ /// Counts non-overlapping occurrences of in .
+ private static int CountSubstringOccurrences(string haystack, string needle) {
+ int count = 0;
+ int idx = 0;
+ while ((idx = haystack.IndexOf(needle, idx, StringComparison.Ordinal)) != -1) {
+ count++;
+ idx += needle.Length;
+ }
+ return count;
+ }
+}
+
+///
+/// Synchronous stand-in: runs callbacks inline so command
+/// continuations execute within the test thread without requiring an Avalonia
+/// pump.
+///
+internal sealed class InlineUIDispatcher : IUIDispatcher {
+ public Task InvokeAsync(Action callback, Avalonia.Threading.DispatcherPriority priority = default) {
+ callback();
+ return Task.CompletedTask;
+ }
+
+ public void Post(Action callback, Avalonia.Threading.DispatcherPriority priority = default) {
+ callback();
+ }
+
+ public bool CheckAccess() => true;
+}