Skip to content

Commit dd051b6

Browse files
committed
feat: Speculative CFG discovery
1 parent 219865f commit dd051b6

93 files changed

Lines changed: 4460 additions & 1949 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

SPECULATIVE_CFG_EXPLORATION.md

Lines changed: 0 additions & 1471 deletions
This file was deleted.

SPECULATIVE_CFG_IMPLEMENTATION_PLAN.md

Lines changed: 0 additions & 430 deletions
This file was deleted.

src/Spice86.Core/CLI/Configuration.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,4 +357,17 @@ public sealed class Configuration : CommandSettings {
357357
[CommandOption("--AllowIvtAddress0")]
358358
public bool AllowIvtAddress0 { get; init; }
359359

360+
/// <summary>
361+
/// When true (default), the speculative CFG explorer is enabled: after each newly-observed
362+
/// instruction is parsed, the explorer performs a recursive-descent static decode of all
363+
/// statically-reachable successors that have not yet been observed, marking the resulting nodes
364+
/// as speculative. This allows the C# code generator to emit real branches for unobserved
365+
/// conditional arms instead of always falling back to <c>FailAsUntested</c>.
366+
/// When false, no speculative nodes, edges, or index entries are created; the graph content is
367+
/// byte-identical to the pre-feature baseline.
368+
/// </summary>
369+
[CommandOption("--EnableSpeculativeCfgExploration <ENABLESPECULATIVECFGEXPLORATION>")]
370+
[DefaultValue(true)]
371+
public bool EnableSpeculativeCfgExploration { get; init; } = true;
372+
360373
}

src/Spice86.Core/Emulator/CPU/CfgCpu/CfgCpu.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ public CfgCpu(IMemory memory, State state, IOPortDispatcher ioPortDispatcher, Ca
3434
DualPic dualPic, EmulatorBreakpointsManager emulatorBreakpointsManager,
3535
IPauseHandler pauseHandler,
3636
FunctionCatalogue functionCatalogue,
37-
bool useCodeOverride, bool failOnInvalidOpcode, bool allowIvtAddress0, ILoggerService loggerService, CfgNodeExecutionCompiler executionCompiler, SequentialIdAllocator idAllocator, CpuHeavyLogger? cpuHeavyLogger = null) {
37+
bool useCodeOverride, bool failOnInvalidOpcode, bool allowIvtAddress0, bool enableSpeculativeExploration, ILoggerService loggerService, CfgNodeExecutionCompiler executionCompiler, SequentialIdAllocator idAllocator, CpuHeavyLogger? cpuHeavyLogger = null) {
3838
_loggerService = loggerService;
3939
_state = state;
4040
_dualPic = dualPic;
4141
_cpuHeavyLogger = cpuHeavyLogger;
4242
_emulatorBreakpointsManager = emulatorBreakpointsManager;
4343
_pauseHandler = pauseHandler;
44-
CfgNodeFeeder = new(memory, state, emulatorBreakpointsManager, _replacerRegistry, executionCompiler, idAllocator);
44+
CfgNodeFeeder = new(memory, state, emulatorBreakpointsManager, _replacerRegistry, executionCompiler, idAllocator, enableSpeculativeExploration);
4545
_executionContextManager = new(memory, state, CfgNodeFeeder, _replacerRegistry, functionCatalogue, useCodeOverride, loggerService, cpuHeavyLogger);
4646
_instructionExecutionHelper = new(state, memory, ioPortDispatcher, callbackHandler, emulatorBreakpointsManager, _executionContextManager, failOnInvalidOpcode, allowIvtAddress0, loggerService);
4747
}

src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/CfgBlock.cs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ public sealed class CfgBlock : CfgNode {
2323
/// </summary>
2424
private int _nonLiveCounter;
2525

26+
/// <summary>
27+
/// Number of contained nodes whose <see cref="ICfgNode.IsSpeculative"/> is currently <c>true</c>.
28+
/// <see cref="IsSpeculative"/> is <c>(_speculativeCounter &gt; 0)</c>.
29+
/// </summary>
30+
private int _speculativeCounter;
31+
2632
private BlockNode? _cachedDisplayAst;
2733
private bool _isDisplayAstStale = true;
2834

@@ -50,6 +56,13 @@ public CfgBlock(int id, ICfgNode entry)
5056
/// </remarks>
5157
public override bool IsLive => _nonLiveCounter == 0;
5258

59+
/// <inheritdoc />
60+
/// <remarks>
61+
/// O(1). The block is speculative iff at least one contained instruction is speculative;
62+
/// computed from the maintained <see cref="_speculativeCounter"/> without iterating.
63+
/// </remarks>
64+
public override bool IsSpeculative => _speculativeCounter > 0;
65+
5366
/// <inheritdoc />
5467
/// <remarks>
5568
/// Always <c>null</c>: a <see cref="CfgBlock"/> is itself the container, not contained in one.
@@ -140,19 +153,22 @@ public override IVisitableAstNode DisplayAst {
140153

141154
/// <summary>
142155
/// Appends <paramref name="node"/> to the end of the block, making it the new
143-
/// <see cref="Terminator"/>. Updates the non-live counter from the node's live state.
156+
/// <see cref="Terminator"/>. Updates the non-live and speculative counters from the node's state.
144157
/// </summary>
145158
internal void Append(ICfgNode node) {
146159
_instructions.Add(node);
147160
if (!node.IsLive) {
148161
_nonLiveCounter++;
149162
}
163+
if (node.IsSpeculative) {
164+
_speculativeCounter++;
165+
}
150166
_isDisplayAstStale = true;
151167
}
152168

153169
/// <summary>
154170
/// Replaces the node at <paramref name="index"/> with <paramref name="newNode"/>, preserving
155-
/// order. Adjusts the non-live counter to reflect the new node's live state.
171+
/// order. Adjusts the non-live and speculative counters to reflect the new node's state.
156172
/// </summary>
157173
internal void ReplaceInPlace(int index, ICfgNode newNode) {
158174
ICfgNode old = _instructions[index];
@@ -163,18 +179,25 @@ internal void ReplaceInPlace(int index, ICfgNode newNode) {
163179
if (!newNode.IsLive) {
164180
_nonLiveCounter++;
165181
}
182+
if (old.IsSpeculative) {
183+
_speculativeCounter--;
184+
}
185+
if (newNode.IsSpeculative) {
186+
_speculativeCounter++;
187+
}
166188
_isDisplayAstStale = true;
167189
}
168190

169191
/// <summary>
170192
/// Removes nodes from <paramref name="splitIndex"/> through the end and returns them as a
171-
/// new list. The non-live counter is recomputed for the surviving prefix.
193+
/// new list. The non-live and speculative counters are recomputed for the surviving prefix.
172194
/// </summary>
173195
internal List<ICfgNode> SliceFrom(int splitIndex) {
174196
int tailLength = _instructions.Count - splitIndex;
175197
List<ICfgNode> tail = _instructions.GetRange(splitIndex, tailLength);
176198
_instructions.RemoveRange(splitIndex, tailLength);
177199
RecountNonLiveFromInstructions();
200+
RecountSpeculativeFromInstructions();
178201
_isDisplayAstStale = true;
179202
return tail;
180203
}
@@ -191,6 +214,18 @@ internal void OnContainedInstructionLiveChanged(bool nowLive) {
191214
}
192215
}
193216

217+
/// <summary>
218+
/// Counter choke-point invoked by <see cref="CfgInstruction.SetSpeculative"/> exactly once per
219+
/// actual <see cref="ICfgNode.IsSpeculative"/> transition of a contained instruction.
220+
/// </summary>
221+
internal void OnContainedInstructionSpeculativeChanged(bool nowSpeculative) {
222+
if (nowSpeculative) {
223+
_speculativeCounter++;
224+
} else {
225+
_speculativeCounter--;
226+
}
227+
}
228+
194229
/// <summary>
195230
/// Recomputes the non-live counter from scratch by iterating the contained instructions.
196231
/// Used after a split to re-base the counter on the new instruction set.
@@ -199,6 +234,14 @@ internal void RecountNonLiveFromInstructions() {
199234
_nonLiveCounter = _instructions.Count(node => !node.IsLive);
200235
}
201236

237+
/// <summary>
238+
/// Recomputes the speculative counter from scratch by iterating the contained instructions.
239+
/// Used after a split to re-base the counter on the new instruction set.
240+
/// </summary>
241+
internal void RecountSpeculativeFromInstructions() {
242+
_speculativeCounter = _instructions.Count(node => node.IsSpeculative);
243+
}
244+
202245
/// <summary>
203246
/// Returns the index of <paramref name="node"/> in the contained list, or <c>-1</c> if
204247
/// the node is not part of this block.

src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/CfgNode.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,21 @@ public CfgNodeExecutionAction<InstructionExecutionHelper> CompiledExecution {
4343
}
4444

4545
public abstract bool IsLive { get; }
46+
47+
/// <summary>
48+
/// Default is <c>false</c>. <see cref="ParsedInstruction.CfgInstruction"/> overrides this to
49+
/// expose its stored speculative flag, maintained by <see cref="SetSpeculative"/> calls.
50+
/// <see cref="SelectorNode"/> and <see cref="CfgBlock"/> keep the default <c>false</c>.
51+
/// </summary>
52+
public virtual bool IsSpeculative => false;
53+
54+
/// <summary>
55+
/// No-op default: <see cref="SelectorNode"/> and <see cref="CfgBlock"/> have no speculative
56+
/// state. <see cref="ParsedInstruction.CfgInstruction"/> overrides this to maintain its stored
57+
/// flag and notify its containing block.
58+
/// </summary>
59+
public virtual void SetSpeculative(bool isSpeculative) {
60+
}
4661

4762
public abstract void UpdateSuccessorCache();
4863
public abstract ICfgNode? GetNextSuccessor(InstructionExecutionHelper helper);

src/Spice86.Core/Emulator/CPU/CfgCpu/ControlFlowGraph/ICfgNode.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,24 @@ public interface ICfgNode : IEquatable<ICfgNode> {
3737
/// </summary>
3838
bool IsLive { get; }
3939

40+
/// <summary>
41+
/// Returns whether the node is speculative.
42+
/// A speculative node was decoded by the static explorer from memory bytes but has never been
43+
/// executed and confirmed. It is a separate axis from <see cref="IsLive"/>: a speculative node
44+
/// is always non-live, but a non-live observed node (e.g. SMC-evicted) is not speculative.
45+
/// Speculative nodes never enter <see cref="Feeder.CurrentInstructions"/>,
46+
/// <see cref="Feeder.PreviousInstructions"/>, or a <see cref="ParsedInstruction.SelfModifying.SelectorNode"/>.
47+
/// </summary>
48+
bool IsSpeculative { get; }
49+
50+
/// <summary>
51+
/// Updates the speculative provenance of this node. On an actual transition, keeps the
52+
/// containing <see cref="CfgBlock"/>'s speculative counter in sync. Setting speculative also
53+
/// forces the node non-live. No-op for node kinds that have no speculative state
54+
/// (<see cref="ParsedInstruction.SelfModifying.SelectorNode"/>, <see cref="CfgBlock"/>).
55+
/// </summary>
56+
void SetSpeculative(bool isSpeculative);
57+
4058
/// <summary>
4159
/// True when the node execution can lead to going back to previous execution context if the next to execute is the correct address
4260
/// </summary>

src/Spice86.Core/Emulator/CPU/CfgCpu/ExecutionContextManager.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,22 @@ private void RegisterCurrentInstructionAsEntryPoint(SegmentedAddress entryAddres
121121
nodes.Add(toExecute);
122122
}
123123

124+
/// <summary>
125+
/// Registers <paramref name="node"/> as a CFG entry point so it is treated as a generation root by
126+
/// the graph exporter and the function partitioner. Used to seed emulator-installed hardware
127+
/// interrupt handlers: these fire on external events with nondeterministic timing and may never be
128+
/// reached from the program's observed entry points during discovery, yet must still become
129+
/// generated overrides so generated code can service the interrupt.
130+
/// </summary>
131+
/// <param name="node">The handler entry instruction to register.</param>
132+
public void RegisterEntryPoint(CfgInstruction node) {
133+
if (!ExecutionContextEntryPoints.TryGetValue(node.Address, out ISet<CfgInstruction>? nodes)) {
134+
nodes = new HashSet<CfgInstruction>();
135+
ExecutionContextEntryPoints.Add(node.Address, nodes);
136+
}
137+
nodes.Add(node);
138+
}
139+
124140
public override void ReplaceInstruction(CfgInstruction oldInstruction, CfgInstruction newInstruction) {
125141
if (ExecutionContextEntryPoints.TryGetValue(newInstruction.Address, out ISet<CfgInstruction>? entriesAtAddress)
126142
&& entriesAtAddress.Remove(oldInstruction)) {

src/Spice86.Core/Emulator/CPU/CfgCpu/Feeder/CfgNodeFeeder.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,27 @@ namespace Spice86.Core.Emulator.CPU.CfgCpu.Feeder;
2121
public class CfgNodeFeeder {
2222
private readonly State _state;
2323
private readonly NodeLinker _nodeLinker;
24+
private readonly SpeculativeReachabilityPruner? _speculativePruner;
2425

2526
public CfgNodeFeeder(IMemory memory, State state, EmulatorBreakpointsManager emulatorBreakpointsManager,
26-
InstructionReplacerRegistry replacerRegistry, CfgNodeExecutionCompiler executionCompiler, SequentialIdAllocator idAllocator) {
27+
InstructionReplacerRegistry replacerRegistry, CfgNodeExecutionCompiler executionCompiler, SequentialIdAllocator idAllocator,
28+
bool enableSpeculativeExploration) {
2729
_state = state;
28-
InstructionsFeeder = new(emulatorBreakpointsManager, memory, state, replacerRegistry, executionCompiler, idAllocator);
2930
_nodeLinker = new(replacerRegistry, executionCompiler, idAllocator);
31+
InstructionsFeeder = new(emulatorBreakpointsManager, memory, state, replacerRegistry, executionCompiler, idAllocator,
32+
enableSpeculativeExploration, enableSpeculativeExploration ? _nodeLinker : null);
33+
if (enableSpeculativeExploration) {
34+
SpeculativeReachabilityPruner pruner = new(_nodeLinker, InstructionsFeeder.NodeIndex);
35+
_speculativePruner = pruner;
36+
InstructionsFeeder.SetSpeculativePruner(pruner);
37+
}
3038
}
3139

3240
public InstructionsFeeder InstructionsFeeder { get; }
3341

42+
/// <summary>Persistent graph index spanning all nodes (observed + speculative).</summary>
43+
public CfgNodeIndex NodeIndex => InstructionsFeeder.NodeIndex;
44+
3445
/// <summary>
3546
/// Parses or retrieves from cache the instruction at the current IP from memory.
3647
/// May trigger SignatureReducer and InstructionReplacerRegistry side effects.
@@ -92,6 +103,19 @@ private ICfgNode ReconcileGraphWithMemory(ExecutionContext executionContext, ICf
92103
$"From graph: {graphNodeAfterReconciliation}");
93104
}
94105

106+
// Provenance-gated path (Phase 5): when the stale graph node is speculative,
107+
// either promote it (memory match) or discard-and-replace (mismatch).
108+
// This must run BEFORE the generic SelectorNode path so that speculative nodes
109+
// never enter a SelectorNode (Core Invariant 4).
110+
if (graphNodeAfterReconciliation is CfgInstruction speculativeInGraph && speculativeInGraph.IsSpeculative) {
111+
SpeculativePromoter? promoter = InstructionsFeeder.SpeculativePromoter;
112+
if (speculativeInGraph.Signature.ListEquivalent(fromMemory.Signature.SignatureValue) && promoter is not null) {
113+
return promoter.Promote(speculativeInGraph);
114+
}
115+
_speculativePruner?.Sweep(speculativeInGraph);
116+
return fromMemory;
117+
}
118+
95119
// Genuinely different instructions at the same address. Inject a SelectorNode
96120
return _nodeLinker.CreateSelectorNodeBetween(fromMemory, (CfgInstruction)graphNodeAfterReconciliation);
97121
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
namespace Spice86.Core.Emulator.CPU.CfgCpu.Feeder;
2+
3+
using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
4+
using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
5+
using Spice86.Shared.Emulator.Memory;
6+
7+
/// <summary>
8+
/// Persistent graph index keyed by <c>(address, signature)</c>, spanning all nodes (observed and
9+
/// speculative). Unlike the hot memory caches <see cref="CurrentInstructions"/> /
10+
/// <see cref="PreviousInstructions"/>, this index has no memory-state semantics and no eviction on
11+
/// write: it is a durable lookup table for the CFG explorer and the cold-path promotion logic.
12+
///
13+
/// <para>Implements <see cref="IInstructionReplacer"/> so that signature-reducer fan-out via
14+
/// <see cref="InstructionReplacerRegistry"/> keeps the index coherent: when two instructions are
15+
/// merged the old key is replaced by the new key.</para>
16+
/// </summary>
17+
public class CfgNodeIndex : InstructionReplacer {
18+
private readonly Dictionary<SegmentedAddress, Dictionary<Signature, CfgInstruction>> _index = new();
19+
20+
/// <summary>
21+
/// Set of addresses proven byte-unstable between explore-time and execution-time.
22+
/// Speculation stops permanently at poisoned addresses.
23+
/// </summary>
24+
public HashSet<SegmentedAddress> PoisonSet { get; } = new();
25+
26+
public CfgNodeIndex(InstructionReplacerRegistry replacerRegistry) : base(replacerRegistry) {
27+
}
28+
29+
/// <summary>
30+
/// Inserts <paramref name="node"/> into the index.
31+
/// Idempotent: a second insertion with the same <c>(address, signature)</c> key is a no-op.
32+
/// </summary>
33+
public void Insert(CfgInstruction node) {
34+
Signature signature = node.Signature;
35+
if (!_index.TryGetValue(node.Address, out Dictionary<Signature, CfgInstruction>? atAddress)) {
36+
atAddress = new Dictionary<Signature, CfgInstruction>();
37+
_index[node.Address] = atAddress;
38+
}
39+
atAddress.TryAdd(signature, node);
40+
}
41+
42+
/// <summary>
43+
/// Removes the single <c>(address, signature)</c> entry for <paramref name="node"/>.
44+
/// Used only by the discard sweep; does not touch graph edges.
45+
/// </summary>
46+
public void Remove(CfgInstruction node) {
47+
if (!_index.TryGetValue(node.Address, out Dictionary<Signature, CfgInstruction>? atAddress)) {
48+
return;
49+
}
50+
atAddress.Remove(node.Signature);
51+
if (atAddress.Count == 0) {
52+
_index.Remove(node.Address);
53+
}
54+
}
55+
56+
/// <summary>
57+
/// Returns the node at <c>(address, signature)</c>, or <c>null</c> if not present.
58+
/// </summary>
59+
public CfgInstruction? TryGet(SegmentedAddress address, Signature signature) {
60+
if (!_index.TryGetValue(address, out Dictionary<Signature, CfgInstruction>? atAddress)) {
61+
return null;
62+
}
63+
atAddress.TryGetValue(signature, out CfgInstruction? node);
64+
return node;
65+
}
66+
67+
/// <summary>
68+
/// Returns all nodes indexed at <paramref name="address"/>, or an empty enumerable if none.
69+
/// </summary>
70+
public IEnumerable<CfgInstruction> GetAtAddress(SegmentedAddress address) {
71+
if (!_index.TryGetValue(address, out Dictionary<Signature, CfgInstruction>? atAddress)) {
72+
return [];
73+
}
74+
return atAddress.Values;
75+
}
76+
77+
/// <summary>
78+
/// Returns whether the index has any entry at <paramref name="address"/>.
79+
/// </summary>
80+
public bool HasAddress(SegmentedAddress address) => _index.ContainsKey(address);
81+
82+
/// <summary>
83+
/// Handles signature-reducer fan-out: replaces the index entry for
84+
/// <paramref name="oldInstruction"/> with <paramref name="newInstruction"/>.
85+
/// </summary>
86+
public override void ReplaceInstruction(CfgInstruction oldInstruction, CfgInstruction newInstruction) {
87+
if (!_index.TryGetValue(oldInstruction.Address, out Dictionary<Signature, CfgInstruction>? atAddress)) {
88+
return;
89+
}
90+
Signature oldSignature = oldInstruction.Signature;
91+
if (!atAddress.TryGetValue(oldSignature, out CfgInstruction? incumbent) || !incumbent.Equals(oldInstruction)) {
92+
return;
93+
}
94+
atAddress.Remove(oldSignature);
95+
atAddress.TryAdd(newInstruction.Signature, newInstruction);
96+
}
97+
}

0 commit comments

Comments
 (0)