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; +}