From c1ba586e42194b683516512a446b432185259593 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Sun, 4 Oct 2026 03:54:29 +0300 Subject: [PATCH 001/211] Complete synthetic logical and control instruction milestones --- Copper68k.Tests/M68020BrianOperandTests.cs | 10 +- Copper68k.Tests/M68040InterpreterTests.cs | 10 +- Copper68k.Tests/M68060InterpreterTests.cs | 13 +- Copper68k.Tests/M68kTimingFormulaTests.cs | 2 +- .../Synthetic/ArchitecturalExpectation.cs | 19 +- .../Synthetic/ControlSpecification.cs | 24 +++ Copper68k.Tests/Synthetic/IntegerInventory.cs | 2 + .../Synthetic/LogicalSpecification.cs | 37 ++++ Copper68k.Tests/Synthetic/README.md | 40 +++- .../Synthetic/SyntheticAtomicTests.cs | 156 ++++++++++++++ .../Synthetic/SyntheticBitFieldTests.cs | 150 ++++++++++++++ .../Synthetic/SyntheticBitShiftTests.cs | 85 ++++++++ .../Synthetic/SyntheticBoundsTests.cs | 97 +++++++++ .../Synthetic/SyntheticControlTests.cs | 162 +++++++++++++++ .../Synthetic/SyntheticDeepAuditTests.cs | 16 ++ .../SyntheticExceptionControlTests.cs | 172 ++++++++++++++++ .../Synthetic/SyntheticExecution.cs | 10 +- .../Synthetic/SyntheticLogicalTests.cs | 91 +++++++++ Copper68k.Tests/Synthetic/SyntheticMachine.cs | 12 +- .../Synthetic/SyntheticModelSystemTests.cs | 155 ++++++++++++++ .../Synthetic/SyntheticModuleTests.cs | 130 ++++++++++++ .../Synthetic/SyntheticSystemTests.cs | 191 ++++++++++++++++++ Copper68k/M68010Interpreter.cs | 9 + Copper68k/M68030Interpreter.cs | 24 +-- Copper68k/M68040Support.cs | 45 +---- Copper68k/M68060Interpreter.cs | 9 +- Copper68k/M68EC020Interpreter.cs | 12 +- .../M68kAdvancedTimingInterpreter.Logical.cs | 115 +++++++++++ .../M68kAdvancedTimingInterpreter.Module.cs | 85 ++++++++ .../M68kAdvancedTimingInterpreter.System.cs | 163 +++++++++++++++ Copper68k/M68kAdvancedTimingInterpreter.cs | 167 ++++++++++----- Copper68k/M68kCore.cs | 99 +++++++-- Copper68k/M68kTimingEngine.cs | 1 + Copper68k/M68kTimingFormula.cs | 1 + ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 158 ++++++++++++++- .../test-copper68k-synthetic-mutations.ps1 | 37 +++- scripts/test-copper68k-synthetic.ps1 | 47 ++++- 37 files changed, 2389 insertions(+), 167 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/ControlSpecification.cs create mode 100644 Copper68k.Tests/Synthetic/LogicalSpecification.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticAtomicTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticBitFieldTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticBitShiftTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticBoundsTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticControlTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticLogicalTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticModuleTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticSystemTests.cs create mode 100644 Copper68k/M68kAdvancedTimingInterpreter.Logical.cs create mode 100644 Copper68k/M68kAdvancedTimingInterpreter.Module.cs create mode 100644 Copper68k/M68kAdvancedTimingInterpreter.System.cs diff --git a/Copper68k.Tests/M68020BrianOperandTests.cs b/Copper68k.Tests/M68020BrianOperandTests.cs index fd61de2b..3232b316 100644 --- a/Copper68k.Tests/M68020BrianOperandTests.cs +++ b/Copper68k.Tests/M68020BrianOperandTests.cs @@ -184,15 +184,19 @@ public void OrBriefIndexedResolvesAliasedDataIndexBeforeStore() } [Fact] - public void OrFullIndexedRemainsExplicitlyUnsupported() + public void OrFullIndexedExecutesWithLongBaseDisplacementAndPreservesUpperWord() { var bus = new ZeroWaitCodeBus(); WriteWords(bus, 0xF80000, 0x8C70, 0x0130, 0, 0x20); using var cpu = M68kCoreFactory.Default.CreateA1200Ec020(bus); cpu.Reset(0xF80000, 0x4000); cpu.State.D[6] = 0x12345678; - Assert.Throws(() => cpu.ExecuteInstruction()); - Assert.Equal(0x12345678u, cpu.State.D[6]); + cpu.State.A[0] = 0x5000; cpu.State.D[0] = 0; + bus.WriteWord(0x5020, 0x8001); + cpu.ExecuteInstruction(); + Assert.Equal(0x1234D679u, cpu.State.D[6]); + Assert.Equal(0xF80008u, cpu.State.ProgramCounter); + Assert.Equal(8, cpu.State.StatusRegister & 15); } [Theory] diff --git a/Copper68k.Tests/M68040InterpreterTests.cs b/Copper68k.Tests/M68040InterpreterTests.cs index ac22eb63..0794b65d 100644 --- a/Copper68k.Tests/M68040InterpreterTests.cs +++ b/Copper68k.Tests/M68040InterpreterTests.cs @@ -16,7 +16,7 @@ public void FactoryCreatesAccurateM68040Backend() } [Fact] - public void MovecM68060ProcessorConfigurationRegisterProbeRaisesLineF() + public void MovecM68060ProcessorConfigurationRegisterProbeRaisesIllegalInstruction() { var bus = new Copper68kTestBus(); WriteWords(bus, CodeBase, 0x4E7A, 0x1808); // MOVEC PCR,D1 @@ -28,18 +28,18 @@ public void MovecM68060ProcessorConfigurationRegisterProbeRaisesLineF() cpu.ExecuteInstruction(); - Assert.Equal(0x2000u, cpu.State.ProgramCounter); + Assert.Equal(0x3000u, cpu.State.ProgramCounter); Assert.Equal(4u, cpu.State.D[1]); Assert.Equal(StackBase - 8u, cpu.State.A[7]); Assert.Equal(CodeBase, bus.ReadLong(StackBase - 6u)); - Assert.Equal(11 * 4, bus.ReadWord(StackBase - 2u)); + Assert.Equal(4 * 4, bus.ReadWord(StackBase - 2u)); } [Fact] public void Move16CopiesAlignedBlockAndAdvancesAddressRegisters() { var bus = new Copper68kTestBus(); - WriteWords(bus, CodeBase, 0xF620, 0x1000); // MOVE16 (A0)+,(A1)+ + WriteWords(bus, CodeBase, 0xF620, 0x9000); // MOVE16 (A0)+,(A1)+ for (var offset = 0u; offset < 16; offset += 4) { bus.WriteLong(0x3000 + offset, 0x1111_0000u + offset); @@ -65,7 +65,7 @@ public void Move16CopiesAlignedBlockAndAdvancesAddressRegisters() public void Move16SameRegisterCopiesLineOverItselfAndAdvancesOnce() { var bus = new Copper68kTestBus(); - WriteWords(bus, CodeBase, 0xF620, 0x0000); // MOVE16 (A0)+,(A0)+ + WriteWords(bus, CodeBase, 0xF620, 0x8000); // MOVE16 (A0)+,(A0)+ for (var offset = 0u; offset < 16; offset += 4) { bus.WriteLong(0x3000 + offset, 0xA0A0_0000u + offset); diff --git a/Copper68k.Tests/M68060InterpreterTests.cs b/Copper68k.Tests/M68060InterpreterTests.cs index 139bf476..c0a2e7fd 100644 --- a/Copper68k.Tests/M68060InterpreterTests.cs +++ b/Copper68k.Tests/M68060InterpreterTests.cs @@ -128,7 +128,6 @@ public void CasAlignmentUsesOperandWidth(int opcode, uint address, bool trap) [Theory] [InlineData(0x003, 0x8000u)] [InlineData(0x004, 0x8000u)] - [InlineData(0x008, 0x20000000u)] public void UnimplementedSystemFeaturesStopExplicitly(int control, uint value) { var bus = new Copper68kTestBus(0x10000); @@ -138,6 +137,18 @@ public void UnimplementedSystemFeaturesStopExplicitly(int control, uint value) Assert.Throws(() => cpu.ExecuteInstruction()); } + [Fact] + public void BusControlEnableTransfersWithoutInventingExternalLockActivity() + { + var bus = new Copper68kTestBus(0x10000); + bus.WriteWords(0x1000, 0x4E7B, 0x0008, 0x4E7A, 0x1008); + using var cpu = M68kCoreFactory.Default.Create(M68kCpuModel.M68060, bus); + cpu.Reset(0x1000, 0x7000); cpu.State.D[0] = 0x20000000; + cpu.ExecuteInstruction(); cpu.ExecuteInstruction(); + Assert.Equal(0x20000000u, cpu.State.D[1]); + Assert.Equal(0x1008u, cpu.State.ProgramCounter); + } + [Theory] [InlineData(0x1000, 0x1230u)] [InlineData(0x0800, 0x04000208u)] diff --git a/Copper68k.Tests/M68kTimingFormulaTests.cs b/Copper68k.Tests/M68kTimingFormulaTests.cs index b612c5fc..a23f7315 100644 --- a/Copper68k.Tests/M68kTimingFormulaTests.cs +++ b/Copper68k.Tests/M68kTimingFormulaTests.cs @@ -192,7 +192,7 @@ public void MovemLongFormulaKeepsM68030HeadTailShapeWithFixedCycles() } private static bool IsDynamicTimingKey(M68kInstructionTimingKey key) - => key is M68kInstructionTimingKey.GeneralArithmetic or M68kInstructionTimingKey.GeneralMove or M68kInstructionTimingKey.GeneralMovem or M68kInstructionTimingKey.FullIndexedMoveToRegister or + => key is M68kInstructionTimingKey.GeneralLogical or M68kInstructionTimingKey.GeneralArithmetic or M68kInstructionTimingKey.GeneralMove or M68kInstructionTimingKey.GeneralMovem or M68kInstructionTimingKey.FullIndexedMoveToRegister or M68kInstructionTimingKey.FullIndexedMoveToMemory or M68kInstructionTimingKey.FullIndexedRegisterToMemory or M68kInstructionTimingKey.FullIndexedClear or M68kInstructionTimingKey.FullIndexedLea or M68kInstructionTimingKey.FullIndexedPea or M68kInstructionTimingKey.FullIndexedJump or M68kInstructionTimingKey.FullIndexedSubroutine or diff --git a/Copper68k.Tests/Synthetic/ArchitecturalExpectation.cs b/Copper68k.Tests/Synthetic/ArchitecturalExpectation.cs index 6bdbddef..7d68628f 100644 --- a/Copper68k.Tests/Synthetic/ArchitecturalExpectation.cs +++ b/Copper68k.Tests/Synthetic/ArchitecturalExpectation.cs @@ -8,6 +8,8 @@ internal sealed class ArchitecturalExpectation public Dictionary MemoryMasks { get; } = []; public HashSet ForbiddenOperandReads { get; } = []; public int? ExpectedByteOperandAccesses { get; set; } + public HashSet OperandAccessAddresses { get; } = []; + public List<(bool Write, int Width)>? ExpectedOperandTransfers { get; set; } public ushort Sr { get; set; } public ushort DefinedSrMask { get; set; } = 0xffff; public uint Pc { get; set; } @@ -15,7 +17,9 @@ internal sealed class ArchitecturalExpectation public bool Halted { get; set; } public int? ExceptionVector { get; set; } public uint? InactiveStackPointer { get; set; } - public uint? MasterStackPointer { get; init; } + public uint? MasterStackPointer { get; set; } + public Dictionary Read, uint Value)> ControlChecks { get; } = []; + public int? ExpectedDeviceResets { get; set; } public static ArchitecturalExpectation Capture(SyntheticMachine machine) => new() { @@ -41,10 +45,21 @@ public void Write(uint address, uint value, int width, ModelSpec model) if (state.A[i] != A[i]) return $"A{i} expected {A[i]:X8}, actual {state.A[i]:X8}"; } var supervisor = (Sr & 0x2000) != 0; - if (supervisor && state.SupervisorStackPointer != A[7]) return $"Active SSP expected {A[7]:X8}, actual {state.SupervisorStackPointer:X8}"; + var master = machine.Model.FullIndex && machine.Model.Id != "68060" && (Sr & 0x3000) == 0x3000; + if (supervisor && !master && state.SupervisorStackPointer != A[7]) return $"Active SSP expected {A[7]:X8}, actual {state.SupervisorStackPointer:X8}"; + if (master && state.MasterStackPointer != A[7]) return $"Active MSP expected {A[7]:X8}, actual {state.MasterStackPointer:X8}"; if (!supervisor && state.UserStackPointer != A[7]) return $"Active USP expected {A[7]:X8}, actual {state.UserStackPointer:X8}"; if (InactiveStackPointer.HasValue && InactiveStackPointer != (supervisor ? state.UserStackPointer : state.SupervisorStackPointer)) return "Inactive stack pointer changed"; if (MasterStackPointer.HasValue && MasterStackPointer != state.MasterStackPointer) return "Master stack pointer changed"; + foreach (var (name, check) in ControlChecks) + if (check.Read(state) != check.Value) return $"{name} expected {check.Value:X8}, actual {check.Read(state):X8}"; + if (ExpectedDeviceResets.HasValue && ExpectedDeviceResets != machine.Bus.DeviceResets) return "RESET device notification count differs"; + if (ExpectedOperandTransfers != null) + { + var transfers = machine.Bus.Accesses.Where(a => a.Kind is Copper68k.M68kBusAccessKind.CpuDataRead or Copper68k.M68kBusAccessKind.CpuDataWrite) + .Where(a => OperandAccessAddresses.Contains(a.Address)).Select(a => (a.Write, a.Width)); + if (!transfers.SequenceEqual(ExpectedOperandTransfers)) return "Operand transfer order/width/count differs"; + } if (ForbiddenOperandReads.Count != 0 || ExpectedByteOperandAccesses.HasValue) { var operandAccesses = machine.Bus.Accesses.Where(a => a.Kind is Copper68k.M68kBusAccessKind.CpuDataRead or Copper68k.M68kBusAccessKind.CpuDataWrite).ToArray(); diff --git a/Copper68k.Tests/Synthetic/ControlSpecification.cs b/Copper68k.Tests/Synthetic/ControlSpecification.cs new file mode 100644 index 00000000..c8b5377e --- /dev/null +++ b/Copper68k.Tests/Synthetic/ControlSpecification.cs @@ -0,0 +1,24 @@ +namespace Copper68k.Tests.Synthetic; + +// M68000PM table 3-19. These Boolean predicates are independent of production condition decoding. +internal static class ControlSpecification +{ + public static bool Condition(int condition, int ccr) + { + var c = (ccr & 1) != 0; var v = (ccr & 2) != 0; var z = (ccr & 4) != 0; var n = (ccr & 8) != 0; + return condition switch + { + 0 => true, 1 => false, 2 => !c && !z, 3 => c || z, 4 => !c, 5 => c, 6 => !z, 7 => z, + 8 => !v, 9 => v, 10 => !n, 11 => n, 12 => n == v, 13 => n != v, 14 => !z && n == v, _ => z || n != v + }; + } + + public static bool InstallTargetSentinel(SyntheticMachine machine, uint target, uint nextPc) + { + var physical = machine.Model.Physical(target); + if (physical >= SyntheticMachine.Code && physical < nextPc) return false; + machine.InitializePhysical(target, 0x4e71, 2); + machine.InitializePhysical(unchecked(target + 2), 0x4e71, 2); + return true; + } +} diff --git a/Copper68k.Tests/Synthetic/IntegerInventory.cs b/Copper68k.Tests/Synthetic/IntegerInventory.cs index 8bcb6c51..5ad4c9fb 100644 --- a/Copper68k.Tests/Synthetic/IntegerInventory.cs +++ b/Copper68k.Tests/Synthetic/IntegerInventory.cs @@ -7,6 +7,8 @@ internal sealed record InstructionSpec(string Family, int Milestone, string Size { public string ArchitecturalOutcome(ModelSpec model) { + if (Family is "MOVE16" or "CINV" or "CPUSH" && model.Id is not ("68040" or "68060")) return "line-F-vector-11"; + if (Family == "LPSTOP" && model.Id != "68060") return "line-F-vector-11"; if (Models == "010+" && model.Id == "68000") return "illegal-vector-4"; if (Models == "020+" && !model.FullIndex) return "illegal-vector-4"; if (Models == "020-only" && model.Id is not ("68020" or "68EC020" or "A1200")) return "illegal-vector-4"; diff --git a/Copper68k.Tests/Synthetic/LogicalSpecification.cs b/Copper68k.Tests/Synthetic/LogicalSpecification.cs new file mode 100644 index 00000000..556f08bd --- /dev/null +++ b/Copper68k.Tests/Synthetic/LogicalSpecification.cs @@ -0,0 +1,37 @@ +namespace Copper68k.Tests.Synthetic; + +// Independent Boolean and bit-by-bit specifications from M68000PM. Width +// truncation, zero counts and extend propagation never use production helpers. +internal static class LogicalSpecification +{ + public static readonly string[] BinaryFamilies = ["AND", "OR", "EOR", "ANDI", "ORI", "EORI"]; + public static readonly string[] UnaryFamilies = ["CLR", "NEG", "NEGX", "NOT", "TST", "TAS"]; + public static readonly string[] Shifts = ["ASR", "ASL", "LSR", "LSL", "ROXR", "ROXL", "ROR", "ROL"]; + public static uint Binary(string family, uint lhs, uint rhs) => family.StartsWith("AND") ? lhs & rhs : family.StartsWith("OR") ? lhs | rhs : lhs ^ rhs; + public static (uint Value, ushort Sr) Unary(string family, uint value, int width, ushort sr) + { + var mask = MoveSpecification.Mask(width); value &= mask; + if (family is "NEG" or "NEGX") return ArithmeticSpecification.Binary(0, value, width, sr, true, extend: family == "NEGX"); + var result = family == "CLR" ? 0u : family == "NOT" ? ~value & mask : value; + return (family == "TAS" ? value | 128 : result, SyntheticExecution.MoveFlags(sr, result, width)); + } + public static (uint Value, ushort Sr) Shift(string family, uint value, int count, int width, ushort sr) + { + var mask = MoveSpecification.Mask(width); var sign = 1u << (width * 8 - 1); + value &= mask; + var left = family.EndsWith('L'); var arithmetic = family.StartsWith("AS"); + var rotate = family.StartsWith("RO"); var throughExtend = family.StartsWith("ROX"); + var x = (sr & 16) != 0; var c = throughExtend && count == 0 && x; var v = false; + for (var step = 0; step < count; step++) + { + var oldSign = (value & sign) != 0; + c = left ? oldSign : (value & 1) != 0; + var incoming = rotate ? throughExtend ? x : c : !left && arithmetic && oldSign; + value = left ? ((value << 1) | (incoming ? 1u : 0)) & mask : (value >> 1) | (incoming ? sign : 0); + if (arithmetic && left && oldSign != ((value & sign) != 0)) v = true; + if (!rotate || throughExtend) x = c; + } + var flags = (x ? 16 : 0) | ((value & sign) != 0 ? 8 : 0) | (value == 0 ? 4 : 0) | (v ? 2 : 0) | (c ? 1 : 0); + return (value, (ushort)((sr & 0xffe0) | flags)); + } +} diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index e1834fba..b5f7a609 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -14,8 +14,9 @@ From the CopperMod root, run: ./scripts/test-copper68k-synthetic.ps1 ``` -MOVE/MOVEA, transfer/address and arithmetic/comparison operations execute -**2,945,168 logical cases** in **146 xUnit batches**, across seven models and the A1200 profile. +Milestones 1–5 execute MOVE/MOVEA, transfer/address, arithmetic/comparison, +logical/bit/shift/atomic and control/system operations in +**8,324,030 logical cases** across **338 xUnit batches**, across seven models and the A1200 profile. Ordinary `dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release` includes these batches. CI additionally validates every required report and exact count; missing reports, mismatches, unsupported execution and empty groups fail the gate. @@ -43,7 +44,7 @@ register encodings and preserved CCR bits. ./scripts/test-copper68k-synthetic.ps1 -Deep -Models 68000,68020 -Seed 123456 -Samples 20000 ``` -Recorded xorshift32 seeds add MOVE and arithmetic samples without expanding the deterministic +Recorded xorshift32 seeds add MOVE, arithmetic, logical and control samples without expanding the deterministic Cartesian product. Zero seeds, empty/unknown model selections and nonpositive sample counts are rejected. Each invocation uses a fresh report directory. @@ -84,12 +85,17 @@ format-2 instruction address on 020+ and RTE in both stack modes. ```powershell ./scripts/test-copper68k-synthetic-mutations.ps1 ./scripts/test-copper68k-synthetic-mutations.ps1 -Scope Arithmetic +./scripts/test-copper68k-synthetic-mutations.ps1 -Scope Logical +./scripts/test-copper68k-synthetic-mutations.ps1 -Scope Control ``` -Eleven isolated mutations prove detection of absolute-address decoding, extension +Twenty isolated mutations prove detection of absolute-address decoding, extension length, signed indexes, source/destination alias order, A7 byte stride and MOVE flags, arithmetic overflow, extend sticky zero, decimal alias ordering, PACK A7 -stride and the 060 divide frame. The command restores source in `finally`, rebuilds it and records each +stride and the 060 divide frame. Additional proofs cover bitfield V/C clearing, +negative offsets, CAS2 alias precedence and 040 failed writeback, CHK2 boundary Z, +CMP2 address width, RTE throwaway frames, MOVE16 postincrement and CACR command +readback. The command restores source in `finally`, rebuilds it and records each mutation, precise failing replacement case and source hash. A compilation failure does not count as detection. Do not run it concurrently with a build or edit of the same production file. @@ -99,3 +105,27 @@ unsupported boundary became legal execution were updated to assert the result. Cache, prefetch, detailed fault ordering, bus timing, JIT and native media tests remain separate. These semantic results preserve existing timing policy; they do not certify physical timing or OS compatibility. + +## Control and system qualification boundary + +Normal, throwaway and postinstruction RTE frames verify saved SR/PC, active and +inactive stack banks, format errors and privilege ordering. Trace tests cover +T1, T0 on applicable models, taken/untaken control flow, completed traps, +aborting exceptions and trace changes at batch boundaries. Interrupt tests cover +IPL, level 7, STOP wakeup, VBR and paired MSP/ISP frames with RTE restoration. +MOVEC verifies model selectors and masks; MOVES covers memory and register effects. +All five MOVE16 forms, cache-instruction privilege and LPSTOP are included. +CALLM/RTM cover 020 module frames, both argument options and an isolated internal +CPU-space access-control responder. An ordinary bus has no such responder and +produces a format error for type-1 modules; ordinary RAM is never used as one. + +Detailed RTE bus-fault restart/internal-state restoration on 010/020/030, +020/030 coprocessor midinstruction restoration and 040 access-fault pending +exceptions/writebacks remain **untested by this synthetic matrix**. They are +listed separately in report qualification boundaries rather than reported as +invalid frame formats or passing coverage. External BKPT instruction replacement, +physical MOVES function-code buses and LPSTOP CPU-space broadcast are also +unavailable qualification. Cache/prefetch/fault/JIT/native suites remain retained; +no physical pipeline/cache, FPU arithmetic, enabled MMU or OS qualification is +claimed. Undefined CAS2 overlapping-memory results, MOVES storing its own updated +address base, reserved module fields and simultaneous T1/T0 are excluded. diff --git a/Copper68k.Tests/Synthetic/SyntheticAtomicTests.cs b/Copper68k.Tests/Synthetic/SyntheticAtomicTests.cs new file mode 100644 index 00000000..8907e8f8 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticAtomicTests.cs @@ -0,0 +1,156 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticAtomicTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void CasBoundariesAddressingAndAliases(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "logical-cas"); + foreach (var width in new[] { 1, 2, 4 }) + { + foreach (var memory in ArithmeticSpecification.Boundaries(width)) + foreach (var compare in ArithmeticSpecification.Boundaries(width)) + for (var ccr = 0; ccr < 32; ccr++) + Cas(machine, report, width, new(2, 0), memory, compare, 1, 2, ccr); + foreach (var form in ArithmeticSpecification.Alterable().Where(f => f.Memory)) + for (var compare = 0; compare < 8; compare++) + for (var update = 0; update < 8; update++) + foreach (var equal in new[] { false, true }) + foreach (var supervisor in new[] { false, true }) + Cas(machine, report, width, form, equal ? 0x80u : 0x7fu, 0x80, compare, update, 31, supervisor: supervisor); + if (machine.Model.FullIndex) + foreach (var index in IndexFixture.FullStructures()) + foreach (var equal in new[] { false, true }) + Cas(machine, report, width, new(6, 0), equal ? 0x80u : 0x7fu, 0x80, 1, 2, 31, index: index); + foreach (var form in new[] { new OperandForm(7, 0), new OperandForm(7, 1) }) + Cas(machine, report, width, form, 0x80, 0x80, 1, 2, 31, + options: new(SourceAbsoluteWord: 0xff80, SourceAbsoluteLong: 0x10006000)); + foreach (var address in new[] { 0x4001u, 0x4002u, 0x4003u }) + Cas(machine, report, width, new(2, 0), 0x80, 0x80, 1, 2, 31, oddAddress: address); + } + report.Complete(output); + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void Cas2ComparisonsRegisterFieldsAndAliases(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "logical-cas2"); + foreach (var width in new[] { 2, 4 }) + { + foreach (var first in ArithmeticSpecification.Boundaries(width)) + foreach (var second in ArithmeticSpecification.Boundaries(width)) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var outcome in new[] { 0, 1, 2 }) + Cas2(machine, report, width, 8, 9, 2, 3, 4, 5, first, second, outcome, ccr); + for (var compare1 = 0; compare1 < 8; compare1++) + for (var compare2 = 0; compare2 < 8; compare2++) + for (var update1 = 0; update1 < 8; update1++) + for (var update2 = 0; update2 < 8; update2++) + foreach (var outcome in new[] { 0, 1, 2 }) + Cas2(machine, report, width, 8, 9, compare1, compare2, update1, update2, 0x8000, 0x7fff, outcome, 31); + for (var address1 = 0; address1 < 16; address1++) + for (var address2 = 0; address2 < 16; address2++) + { + if (address1 == address2) continue; // Overlapping memory updates are architecturally undefined. + foreach (var outcome in new[] { 0, 1, 2 }) + foreach (var supervisor in new[] { false, true }) + Cas2(machine, report, width, address1, address2, 2, 3, 4, 5, 0x8000, 0x7fff, outcome, 31, supervisor); + } + } + report.Complete(output); + } + + internal static void Cas(SyntheticMachine machine, CoverageBatch report, int width, OperandForm form, uint memory, + uint compare, int compareReg, int updateReg, int ccr, bool supervisor = true, IndexFixture? index = null, + AddressOptions? options = null, uint? oddAddress = null) + { + machine.Reset(ccr, supervisor); + machine.Core.State.D[updateReg] = ArithmeticSpecification.RegisterBits(0x96c3815a, width); + machine.Core.State.D[compareReg] = ArithmeticSpecification.RegisterBits(compare, width); + if (oddAddress.HasValue) machine.Core.State.A[0] = oddAddress.Value; + var opcode = (ushort)((width == 1 ? 0xac0 : width == 2 ? 0xcc0 : 0xec0) | form.Mode << 3 | form.Register); + var fixture = new OperandFixture(machine, [opcode, (ushort)(updateReg << 6 | compareReg)], form, width, memory, index: index, options: options); + var expected = fixture.Expected; + var unavailable = !machine.Model.FullIndex || machine.Model.Id == "68060" && (fixture.Address & (uint)(width - 1)) != 0; + if (unavailable) + { + // Unimplemented instructions are restarted by their software handler. + machine.Core.State.A.CopyTo(expected.A, 0); + SyntheticExecution.ExpectException(machine, expected, machine.Model.Id == "68060" ? 61 : 4); + for (var i = 0; i < width; i++) expected.ForbiddenOperandReads.Add(machine.Model.Physical(unchecked(fixture.Address + (uint)i))); + } + else + { + var actualCompare = expected.D[compareReg] & MoveSpecification.Mask(width); + expected.Sr = ArithmeticSpecification.Binary(fixture.Value, actualCompare, width, expected.Sr, true, compare: true).Sr; + var equal = fixture.Value == actualCompare; + if (equal) fixture.Write(expected.D[updateReg]); + else expected.D[compareReg] = (expected.D[compareReg] & ~MoveSpecification.Mask(width)) | fixture.Value; + expected.OperandAccessAddresses.Add(machine.Model.Physical(fixture.Address)); + expected.ExpectedOperandTransfers = [(false, width)]; + if (equal || machine.Model.Id is "68040" or "68060") expected.ExpectedOperandTransfers.Add((true, width)); + } + SyntheticExecution.Run(machine, expected, report, $"{machine.Model.Id}/CAS/{width}/{form.Id}/Dc{compareReg}/Du{updateReg}/{index?.Id ?? "brief"}/super={supervisor}/op={opcode:X4}/memory={memory:X8}/compare={compare:X8}/ccr={ccr:X2}/address={fixture.Address:X8}"); + } + + internal static void Cas2(SyntheticMachine machine, CoverageBatch report, int width, int address1Reg, int address2Reg, + int compare1, int compare2, int update1, int update2, uint value1, uint value2, int outcome, int ccr, bool supervisor = true) + { + machine.Reset(ccr, supervisor); + machine.Core.State.D[compare1] = ArithmeticSpecification.RegisterBits(value1, width); + machine.Core.State.D[compare2] = ArithmeticSpecification.RegisterBits(value2, width); + // Address selectors can alias compare/update registers. Choose all addresses independently before taking snapshots. + if (address1Reg < 8) machine.Core.State.D[address1Reg] = 0x5200; + if (address2Reg < 8) machine.Core.State.D[address2Reg] = 0x6200; + var address1 = address1Reg < 8 ? machine.Core.State.D[address1Reg] : machine.Core.State.A[address1Reg - 8]; + var address2 = address2Reg < 8 ? machine.Core.State.D[address2Reg] : machine.Core.State.A[address2Reg - 8]; + var mask = MoveSpecification.Mask(width); + var memory1 = (machine.Core.State.D[compare1] & mask) ^ (outcome == 1 ? 1u : 0); + var memory2 = (machine.Core.State.D[compare2] & mask) ^ (outcome == 2 ? 1u : 0); + for (var i = -8; i < width + 8; i++) + { + machine.InitializePhysical(unchecked(address1 + (uint)i), 0xa5, 1); + machine.InitializePhysical(unchecked(address2 + (uint)i), 0xa5, 1); + } + machine.InitializePhysical(address1, memory1, width); machine.InitializePhysical(address2, memory2, width); + var opcode = (ushort)(width == 2 ? 0xcfc : 0xefc); + var expected = SyntheticExecution.Prepare(machine, [opcode, (ushort)(address1Reg << 12 | update1 << 6 | compare1), (ushort)(address2Reg << 12 | update2 << 6 | compare2)]); + if (!machine.Model.FullIndex || machine.Model.Id == "68060") + { + SyntheticExecution.ExpectException(machine, expected, machine.Model.Id == "68060" ? 61 : 4); + for (var i = 0; i < width; i++) + { + expected.ForbiddenOperandReads.Add(machine.Model.Physical(address1 + (uint)i)); + expected.ForbiddenOperandReads.Add(machine.Model.Physical(address2 + (uint)i)); + } + } + else + { + var firstEqual = memory1 == (expected.D[compare1] & mask); + var secondEqual = memory2 == (expected.D[compare2] & mask); + expected.Sr = ArithmeticSpecification.Binary(firstEqual ? memory2 : memory1, expected.D[firstEqual ? compare2 : compare1], width, expected.Sr, true, compare: true).Sr; + if (firstEqual && secondEqual) + { + expected.Write(address1, expected.D[update1], width, machine.Model); + expected.Write(address2, expected.D[update2], width, machine.Model); + } + else + { + expected.D[compare2] = (expected.D[compare2] & ~mask) | memory2; + // M68000PM 4-68: Dc1 takes precedence when the compare register is aliased. + expected.D[compare1] = (expected.D[compare1] & ~mask) | memory1; + } + expected.OperandAccessAddresses.UnionWith([machine.Model.Physical(address1), machine.Model.Physical(address2)]); + expected.ExpectedOperandTransfers = [(false, width), (false, width)]; + if (firstEqual && secondEqual) expected.ExpectedOperandTransfers.AddRange([(true, width), (true, width)]); + else if (machine.Model.Id == "68040") expected.ExpectedOperandTransfers.Add((true, width)); + } + SyntheticExecution.Run(machine, expected, report, $"{machine.Model.Id}/CAS2/{width}/R{address1Reg}:R{address2Reg}/Dc{compare1}:Dc{compare2}/Du{update1}:Du{update2}/super={supervisor}/op={opcode:X4}/value1={value1:X8}/value2={value2:X8}/outcome={outcome}/ccr={ccr:X2}"); + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticBitFieldTests.cs b/Copper68k.Tests/Synthetic/SyntheticBitFieldTests.cs new file mode 100644 index 00000000..919a43c4 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticBitFieldTests.cs @@ -0,0 +1,150 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticBitFieldTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + internal static readonly string[] Families = ["BFTST", "BFEXTU", "BFCHG", "BFEXTS", "BFCLR", "BFFFO", "BFSET", "BFINS"]; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void BitFieldWidthsOffsetsFlagsAndRegisterAliases(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "logical-bitfield-values"); + for (var operation = 0; operation < 8; operation++) + { + for (var width = 1; width <= 32; width++) + for (var offset = 0; offset < 32; offset++) + foreach (var pattern in new[] { 0u, 0x81234567u }) + foreach (var form in new[] { new OperandForm(0, 0), new OperandForm(2, 0) }) + Case(machine, report, operation, form, offset, width, pattern, 31); + foreach (var width in new[] { 1, 7, 8, 15, 16, 31, 32 }) + foreach (var offset in new[] { 0, 7, 8, 15, 31 }) + foreach (var pattern in new[] { 0u, 1u, 0x80000000u, 0xffffffffu, 0x81234567u }) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var form in new[] { new OperandForm(0, 0), new OperandForm(2, 0) }) + Case(machine, report, operation, form, offset, width, pattern, ccr); + for (var target = 0; target < 8; target++) + for (var operand = 0; operand < 8; operand++) + for (var offsetReg = 0; offsetReg < 8; offsetReg++) + for (var widthReg = 0; widthReg < 8; widthReg++) + Case(machine, report, operation, new(0, target), -9, 37, 0x81234567, 31, operand, offsetReg, widthReg); + } + report.Complete(output); + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void BitFieldAddressingSignedOffsetsAndFullExtensions(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "logical-bitfield-addressing"); + for (var operation = 0; operation < 8; operation++) + { + var modifying = operation is 2 or 4 or 6 or 7; + foreach (var form in Forms(modifying)) + foreach (var supervisor in new[] { false, true }) + foreach (var offsetReg in new[] { -1, 2 }) + foreach (var widthReg in new[] { -1, 3 }) + Case(machine, report, operation, form, offsetReg < 0 ? 7 : -9, 32, 0x81234567, 31, 1, offsetReg, widthReg, supervisor: supervisor); + foreach (var offset in new[] { int.MinValue, -33, -8, -1, 0, 7, 31, 32, int.MaxValue }) + foreach (var width in new[] { 1, 8, 31, 32, 64, -1 }) + Case(machine, report, operation, new(2, 7), offset, width, 0x81234567, 31, 1, 2, 3, supervisor: false); + if (machine.Model.FullIndex) + foreach (var index in IndexFixture.FullStructures()) + foreach (var form in modifying ? new[] { new OperandForm(6, 0) } : new[] { new OperandForm(6, 0), new OperandForm(7, 3) }) + Case(machine, report, operation, form, -9, 32, 0x81234567, 31, 1, 2, 3, index); + foreach (var form in new[] { new OperandForm(7, 0), new OperandForm(7, 1) }) + Case(machine, report, operation, form, -1, 32, 0x81234567, 31, 1, 2, 3, + options: new(SourceAbsoluteWord: 0xff80, SourceAbsoluteLong: 0x10006000)); + } + report.Complete(output); + } + + internal static IEnumerable Forms(bool modifying) + { + foreach (var mode in new[] { 0, 2, 5, 6 }) + for (var reg = 0; reg < 8; reg++) yield return new(mode, reg); + for (var reg = 0; reg < (modifying ? 2 : 4); reg++) yield return new(7, reg); + } + + internal static void Case(SyntheticMachine machine, CoverageBatch report, int operation, OperandForm form, int offset, int width, + uint pattern, int ccr, int operand = 1, int offsetReg = -1, int widthReg = -1, IndexFixture? index = null, + bool supervisor = true, AddressOptions? options = null) + { + machine.Reset(ccr, supervisor); + machine.Core.State.D[operand] = 0x5a96c381; + if (offsetReg >= 0) machine.Core.State.D[offsetReg] = unchecked((uint)offset); + if (widthReg >= 0) machine.Core.State.D[widthReg] = unchecked((uint)width); + var opcode = (ushort)(0xe8c0 | operation << 8 | form.Mode << 3 | form.Register); + var extension = (ushort)(operand << 12 | (offsetReg >= 0 ? 0x800 | offsetReg << 6 : (offset & 31) << 6) | + (widthReg >= 0 ? 0x20 | widthReg : width & 31)); + var fixture = new OperandFixture(machine, [opcode, extension], form, form.Mode == 0 ? 4 : 1, pattern, index: index, options: options); + var expected = fixture.Expected; + var actualOffset = offsetReg >= 0 ? unchecked((int)expected.D[offsetReg]) : offset & 31; + var actualWidth = (int)(widthReg >= 0 ? expected.D[widthReg] & 31 : (uint)width & 31); + if (actualWidth == 0) actualWidth = 32; + var byteOffset = (long)Math.Floor(actualOffset / 8.0); + var firstAddress = unchecked(fixture.Address + (uint)byteOffset); + var firstBit = actualOffset - byteOffset * 8; + if (form.Memory) + { + // Independent patterned operand, with guards on both sides of the affected bytes. + for (var i = -4; i < 10; i++) + { + var address = machine.Model.Physical(unchecked(firstAddress + (uint)i)); + if (address >= SyntheticMachine.Code && address < fixture.NextPc + 4) continue; + var value = i >= 0 && i < 5 ? (byte)(pattern >> (24 - (i % 4) * 8)) : (byte)0xa5; + machine.Bus.Initialize(address, value, 1); expected.Memory[address] = value; + } + } + if (!machine.Model.FullIndex) + { + expected.A[7] = machine.Core.State.A[7]; + SyntheticExecution.ExpectException(machine, expected, 4); + for (var i = 0; i < 5; i++) expected.ForbiddenOperandReads.Add(machine.Model.Physical(unchecked(firstAddress + (uint)i))); + } + else + { + var originalRegister = expected.D[form.Register]; + uint field = 0; + for (var i = 0; i < actualWidth; i++) + { + var bit = form.Mode == 0 ? (originalRegister >> (31 - ((actualOffset + i) & 31))) & 1 : + (uint)(machine.Bus.Peek(machine.Model.Physical(unchecked(firstAddress + (uint)((firstBit + i) / 8)))) >> (7 - (int)((firstBit + i) % 8))) & 1; + field = (field << 1) | bit; + } + var mask = actualWidth == 32 ? uint.MaxValue : (1u << actualWidth) - 1; + var inserted = expected.D[operand] & mask; + var flagsValue = operation == 7 ? inserted : field; + expected.Sr = (ushort)((expected.Sr & 0xfff0) | (flagsValue == 0 ? 4 : 0) | ((flagsValue & (1u << (actualWidth - 1))) != 0 ? 8 : 0)); + if (operation is 1 or 3) expected.D[operand] = operation == 3 && (field & (1u << (actualWidth - 1))) != 0 ? field | ~mask : field; + if (operation == 5) + { + var first = 0; + while (first < actualWidth && (field & (1u << (actualWidth - 1 - first))) == 0) first++; + expected.D[operand] = unchecked((uint)(actualOffset + first)); + } + if (operation is 2 or 4 or 6 or 7) + { + var result = operation switch { 2 => field ^ mask, 4 => 0u, 6 => mask, _ => inserted }; + for (var i = 0; i < actualWidth; i++) + { + var set = (result & (1u << (actualWidth - 1 - i))) != 0; + if (form.Mode == 0) + { + var bit = 1u << (31 - ((actualOffset + i) & 31)); + expected.D[form.Register] = set ? expected.D[form.Register] | bit : expected.D[form.Register] & ~bit; + } + else + { + var address = machine.Model.Physical(unchecked(firstAddress + (uint)((firstBit + i) / 8))); + var bit = (byte)(1 << (7 - (int)((firstBit + i) % 8))); + expected.Memory[address] = set ? (byte)(expected.Memory[address] | bit) : (byte)(expected.Memory[address] & ~bit); + } + } + } + } + SyntheticExecution.Run(machine, expected, report, $"{machine.Model.Id}/{Families[operation]}/{form.Id}/operand=D{operand}/offset={(offsetReg >= 0 ? $"D{offsetReg}" : "immediate")}/width={(widthReg >= 0 ? $"D{widthReg}" : "immediate")}/{index?.Id ?? "brief"}/op={opcode:X4}/ext={extension:X4}/offset={actualOffset}/width={actualWidth}/pattern={pattern:X8}/ccr={ccr:X2}/super={supervisor}"); + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticBitShiftTests.cs b/Copper68k.Tests/Synthetic/SyntheticBitShiftTests.cs new file mode 100644 index 00000000..4d70e2be --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticBitShiftTests.cs @@ -0,0 +1,85 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticBitShiftTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void ShiftsCountsValuesAndFlags(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "logical-shifts"); + foreach (var family in LogicalSpecification.Shifts) + foreach (var width in new[] { 1, 2, 4 }) + foreach (var value in ArithmeticSpecification.Boundaries(width)) + for (var ccr = 0; ccr < 32; ccr++) + { + for (var count = 1; count <= 8; count++) Shift(machine, report, family, width, new(0, 0), value, (uint)count, ccr, false); + foreach (var count in new uint[] { 0, 1, 7, 8, 15, 16, 31, 32, 33, 63, 64, 65, 127, 0xffffffff }) + Shift(machine, report, family, width, new(0, 0), value, count, ccr, true); + } + foreach (var family in LogicalSpecification.Shifts) + for (var src = 0; src < 8; src++) + for (var dst = 0; dst < 8; dst++) + foreach (var width in new[] { 1, 2, 4 }) Shift(machine, report, family, width, new(0, dst), 0x80008001, 33, 31, true, src); + foreach (var family in LogicalSpecification.Shifts) + foreach (var form in ArithmeticSpecification.Alterable().Where(f => f.Memory)) + for (var ccr = 0; ccr < 32; ccr++) Shift(machine, report, family, 2, form, 0x8001, 1, ccr, false); + if (machine.Model.FullIndex) + foreach (var family in LogicalSpecification.Shifts) + foreach (var index in IndexFixture.FullStructures()) Shift(machine, report, family, 2, new(6, 0), 0x8001, 1, 31, false, index: index); + report.Complete(output); + } + + private static void Shift(SyntheticMachine machine, CoverageBatch report, string family, int width, OperandForm form, uint value, uint count, int ccr, bool registerCount, int countReg = 1, IndexFixture? index = null) + { + machine.Reset(ccr); var operation = Array.IndexOf(LogicalSpecification.Shifts, family); + var opcode = form.Mode == 0 ? (ushort)(0xe000 | ((registerCount ? countReg : (int)count & 7) << 9) | (operation & 1) << 8 | + ArithmeticSpecification.SizeField(width) << 6 | (registerCount ? 32 : 0) | (operation >> 1) << 3 | form.Register) : + (ushort)(0xe0c0 | operation << 8 | form.Mode << 3 | form.Register); + if (registerCount) machine.Core.State.D[countReg] = count; + var fixture = new OperandFixture(machine, [opcode], form, width, value, source: false, index: index); + var actualCount = form.Mode == 0 && registerCount ? (int)(fixture.Expected.D[countReg] & 63) : (int)count; + var result = LogicalSpecification.Shift(family, fixture.Value, actualCount, width, fixture.Expected.Sr); + fixture.Write(result.Value); fixture.Expected.Sr = result.Sr; + SyntheticExecution.Run(machine, fixture.Expected, report, $"{machine.Model.Id}/{family}/{width}/{form.Id}/count={(registerCount ? "Dn" : "immediate")}/r{countReg}/{index?.Id ?? "brief"}/op={opcode:X4}/value={value:X8}/count={count:X8}/ccr={ccr:X2}"); + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void BitOperationsModesCountsAndAliases(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "logical-bits"); + foreach (var family in new[] { "BTST", "BCHG", "BCLR", "BSET" }) + foreach (var immediate in new[] { false, true }) + { + foreach (var form in ArithmeticSpecification.Sources(1, false)) + { + if (family != "BTST" && form.Mode == 7 && form.Register > 1 || immediate && form.Mode == 7 && form.Register == 4) continue; + foreach (var bit in new uint[] { 0, 7, 8, 31, 32, 63, 255, 0xffffffff }) + for (var ccr = 0; ccr < 32; ccr++) Bit(machine, report, family, form, 0x80000081, bit, ccr, immediate); + } + for (var src = 0; src < 8; src++) + for (var dst = 0; dst < 8; dst++) Bit(machine, report, family, new(0, dst), 0x80000081, 31, 31, immediate, src); + if (machine.Model.FullIndex) + foreach (var index in IndexFixture.FullStructures()) + foreach (var form in family == "BTST" ? new[] { new OperandForm(6, 0), new OperandForm(7, 3) } : new[] { new OperandForm(6, 0) }) + Bit(machine, report, family, form, 0x81, 7, 31, immediate, index: index); + } + report.Complete(output); + } + private static void Bit(SyntheticMachine machine, CoverageBatch report, string family, OperandForm form, uint value, uint bit, int ccr, bool immediate, int reg = 1, IndexFixture? index = null) + { + machine.Reset(ccr); var operation = family == "BTST" ? 0 : family == "BCHG" ? 1 : family == "BCLR" ? 2 : 3; + var opcode = (ushort)((immediate ? 0x800 : 0x100 | reg << 9) | operation << 6 | form.Mode << 3 | form.Register); + machine.Core.State.D[reg] = bit; + var fixture = new OperandFixture(machine, immediate ? [opcode, (ushort)(bit & 255)] : [opcode], form, form.Mode == 0 ? 4 : 1, value, + source: family == "BTST", index: index); + var actualBit = immediate ? bit & 255 : fixture.Expected.D[reg]; + var mask = 1u << (int)(actualBit % (form.Mode == 0 ? 32u : 8u)); + fixture.Expected.Sr = (ushort)((fixture.Expected.Sr & ~4) | ((fixture.Value & mask) == 0 ? 4 : 0)); + if (operation != 0) fixture.Write(operation == 1 ? fixture.Value ^ mask : operation == 2 ? fixture.Value & ~mask : fixture.Value | mask); + SyntheticExecution.Run(machine, fixture.Expected, report, $"{machine.Model.Id}/{family}/{(form.Mode == 0 ? 4 : 1)}/{form.Id}/{(immediate ? "immediate" : "dynamic")}/r{reg}/{index?.Id ?? "brief"}/op={opcode:X4}/bit={bit:X8}/ccr={ccr:X2}"); + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticBoundsTests.cs b/Copper68k.Tests/Synthetic/SyntheticBoundsTests.cs new file mode 100644 index 00000000..4abda15a --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticBoundsTests.cs @@ -0,0 +1,97 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticBoundsTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void CheckBoundsValuesFlagsFormsAndRegisterAliases(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "system-bounds"); + foreach (var width in new[] { 2, 4 }) + foreach (var bound in ArithmeticSpecification.Boundaries(width)) + foreach (var value in ArithmeticSpecification.Boundaries(width)) + for (var ccr = 0; ccr < 32; ccr++) Chk(machine, report, width, new(0, 1), 0, bound, value, ccr); + foreach (var width in new[] { 2, 4 }) + foreach (var form in ArithmeticSpecification.Sources(width, false)) + for (var reg = 0; reg < 8; reg++) + foreach (var supervisor in new[] { false, true }) Chk(machine, report, width, form, reg, 7, 3, 31, supervisor); + if (machine.Model.FullIndex) + foreach (var index in IndexFixture.FullStructures()) + foreach (var width in new[] { 2, 4 }) + foreach (var form in new[] { new OperandForm(6, 7), new OperandForm(7, 3) }) Chk(machine, report, width, form, 7, 7, 8, 31, index: index); + foreach (var width in new[] { 1, 2, 4 }) + foreach (var lower in ArithmeticSpecification.Boundaries(width)) + foreach (var upper in ArithmeticSpecification.Boundaries(width)) + foreach (var value in ArithmeticSpecification.Boundaries(width)) + foreach (var address in new[] { false, true }) + foreach (var trap in new[] { false, true }) + Pair(machine, report, width, new(2, 0), 1, address, trap, lower, upper, value, 31); + foreach (var width in new[] { 1, 2, 4 }) + foreach (var form in ArithmeticSpecification.Sources(4).Where(f => f.Mode is 2 or 5 or 6 || f.Mode == 7 && f.Register < 4)) + for (var reg = 0; reg < 8; reg++) + foreach (var address in new[] { false, true }) + foreach (var trap in new[] { false, true }) Pair(machine, report, width, form, reg, address, trap, 1, 8, 3, 31); + foreach (var width in new[] { 1, 2, 4 }) + foreach (var trap in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) Pair(machine, report, width, new(2, 0), 1, false, trap, 1, 8, 8, ccr); + if (machine.Model.FullIndex) + foreach (var index in IndexFixture.FullStructures()) + foreach (var width in new[] { 1, 2, 4 }) + foreach (var form in new[] { new OperandForm(6, 7), new OperandForm(7, 3) }) Pair(machine, report, width, form, 7, false, true, 1, 8, 3, 31, index); + report.Complete(output); + } + + private static void Chk(SyntheticMachine machine, CoverageBatch report, int width, OperandForm form, int reg, uint bound, uint value, int ccr, bool supervisor = true, IndexFixture? index = null) + { + machine.Reset(ccr, supervisor); machine.Core.State.D[reg] = ArithmeticSpecification.RegisterBits(value, width); + var opcode = (ushort)((width == 2 ? 0x4180 : 0x4100) | reg << 9 | form.Mode << 3 | form.Register); + var fixture = new OperandFixture(machine, [opcode], form, width, bound, index: index ?? new IndexFixture(IndexRegister: reg == 7 ? 6 : 7)); var e = fixture.Expected; + if (width == 4 && !machine.Model.FullIndex) + { + machine.Core.State.A.CopyTo(e.A, 0); SyntheticExecution.ExpectException(machine, e, 4); + } + else + { + var number = ArithmeticSpecification.Signed(e.D[reg], width); var limit = ArithmeticSpecification.Signed(fixture.Value, width); + // Only X and the documented trapping N result are defined. + e.DefinedSrMask = (ushort)(number < 0 || number > limit ? 0xfff8 : 0xfff0); + if (number < 0 || number > limit) + { + e.Sr = (ushort)((e.Sr & ~8) | (number < 0 ? 8 : 0)); + SyntheticExecution.ExpectException(machine, e, 6, e.Pc); + } + } + SyntheticExecution.Run(machine, e, report, $"{machine.Model.Id}/CHK/{width}/{form.Id}/D{reg}/{index?.Id ?? "brief"}/op={opcode:X4}/bound={bound:X8}/value={value:X8}/ccr={ccr:X2}/super={supervisor}"); + } + private static void Pair(SyntheticMachine machine, CoverageBatch report, int width, OperandForm form, int reg, bool address, bool trap, uint lower, uint upper, uint value, int ccr, IndexFixture? index = null) + { + machine.Reset(ccr); + if (address) { if (reg == 7) machine.Core.State.SetActiveStackPointer(0x7800); else machine.Core.State.A[reg] = value; } + else machine.Core.State.D[reg] = ArithmeticSpecification.RegisterBits(value, width); + var opcode = (ushort)(0x00c0 | ArithmeticSpecification.SizeField(width) << 9 | form.Mode << 3 | form.Register); + var extension = (ushort)((address ? 0x8000 : 0) | reg << 12 | (trap ? 0x800 : 0)); + var fixture = new OperandFixture(machine, [opcode, extension], form, width, lower, index: index); var e = fixture.Expected; + for (var i = 0; i < width; i++) + { + var p = unchecked(fixture.Address + (uint)(width + i)); + if (machine.Model.Physical(p) >= SyntheticMachine.Code && machine.Model.Physical(p) < fixture.NextPc + 4) continue; + machine.InitializePhysical(p, (upper >> (8 * (width - i - 1))) & 255, 1); e.Write(p, (upper >> (8 * (width - i - 1))) & 255, 1, machine.Model); + } + var unsupported = !machine.Model.FullIndex || machine.Model.Id == "68060"; + if (unsupported) SyntheticExecution.ExpectException(machine, e, machine.Model.Id == "68060" ? 61 : 4); + else + { + var lo = ArithmeticSpecification.Signed(machine.PeekPhysical(fixture.Address, width), width); + var hi = ArithmeticSpecification.Signed(machine.PeekPhysical(fixture.Address + (uint)width, width), width); + var number = ArithmeticSpecification.Signed(address ? e.A[reg] : e.D[reg], address ? 4 : width); + var outside = lo <= hi ? number < lo || number > hi : number > hi && number < lo; + e.DefinedSrMask = 0xfff5; + e.Sr = (ushort)((e.Sr & ~5) | (outside ? 1 : 0) | (number == lo || number == hi ? 4 : 0)); + if (trap && outside) SyntheticExecution.ExpectException(machine, e, 6, e.Pc); + } + SyntheticExecution.Run(machine, e, report, $"{machine.Model.Id}/{(trap ? "CHK2" : "CMP2")}/{width}/{form.Id}/{(address ? "A" : "D")}{reg}/{index?.Id ?? "brief"}/op={opcode:X4}/bounds={lower:X8},{upper:X8}/value={value:X8}/ccr={ccr:X2}"); + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticControlTests.cs b/Copper68k.Tests/Synthetic/SyntheticControlTests.cs new file mode 100644 index 00000000..8641cbc5 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticControlTests.cs @@ -0,0 +1,162 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticControlTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void BranchConditionsDisplacementsAndSubroutineStacks(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "control-branches"); + for (var condition = 0; condition < 16; condition++) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var supervisor in new[] { false, true }) + foreach (var width in machine.Model.FullIndex ? new[] { 1, 2, 4 } : new[] { 1, 2 }) + foreach (var displacement in width == 1 ? new[] { -128, -2, 2, 126 } : width == 2 ? new[] { -32768, -2, 2, 32766 } : new[] { -0x10000002, -2, 2, 0x10000002 }) + Branch(machine, report, condition, width, displacement, ccr, supervisor); + report.Complete(output); + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void DecrementAndSetConditionsAllRegistersModesAndFlags(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "control-conditions"); + for (var condition = 0; condition < 16; condition++) + for (var ccr = 0; ccr < 32; ccr++) + { + for (var reg = 0; reg < 8; reg++) + foreach (var count in new[] { 0u, 1u, 0x7fffu, 0xffffu }) + Dbcc(machine, report, condition, reg, count, ccr, -16); + foreach (var form in ArithmeticSpecification.Alterable()) + Scc(machine, report, condition, form, ccr, supervisor: false); + } + for (var condition = 0; condition < 16; condition++) + { + foreach (var displacement in new[] { -32768, -2, 2, 32766 }) + Dbcc(machine, report, condition, 7, 1, 31, displacement); + if (machine.Model.FullIndex) + foreach (var index in IndexFixture.FullStructures()) Scc(machine, report, condition, new(6, 7), 31, index); + } + report.Complete(output); + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void ConditionalTrapsAllConditionsOperandsAndStackModes(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "control-trapcc"); + for (var condition = 0; condition < 16; condition++) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var width in new[] { 0, 2, 4 }) + foreach (var supervisor in new[] { false, true }) + { + machine.Reset(ccr, supervisor); + var words = new List { (ushort)(0x50f8 | condition << 8 | (width == 0 ? 4 : width == 2 ? 2 : 3)) }; + if (width > 0) words.Add(0x1234); + if (width == 4) words.Add(0x5678); + var expected = SyntheticExecution.Prepare(machine, words); + if (!machine.Model.FullIndex) SyntheticExecution.ExpectException(machine, expected, 4); + else if (ControlSpecification.Condition(condition, ccr)) SyntheticExecution.ExpectException(machine, expected, 7, expected.Pc); + SyntheticExecution.Run(machine, expected, report, $"{modelId}/TRAPcc/{width}/condition={condition}/super={supervisor}/op={words[0]:X4}/ccr={ccr:X2}"); + } + report.Complete(output); + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void JumpCallControlAddressingAndStackAliases(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "control-jumps"); + foreach (var subroutine in new[] { false, true }) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var form in SyntheticBitFieldTests.Forms(false).Where(f => f.Mode != 0)) + Jump(machine, report, subroutine, form, ccr, supervisor); + if (machine.Model.FullIndex) + foreach (var subroutine in new[] { false, true }) + foreach (var index in IndexFixture.FullStructures()) + foreach (var form in new[] { new OperandForm(6, 7), new OperandForm(7, 3) }) + Jump(machine, report, subroutine, form, 31, true, index); + foreach (var subroutine in new[] { false, true }) + foreach (var form in new[] { new OperandForm(6, 7), new OperandForm(7, 3) }) + Jump(machine, report, subroutine, form, 31, false, new(AddressIndex: true, IndexRegister: 7, LongIndex: true, Scale: 2)); + foreach (var subroutine in new[] { false, true }) + foreach (var form in new[] { new OperandForm(7, 0), new OperandForm(7, 1) }) + Jump(machine, report, subroutine, form, 31, true, + options: new(SourceAbsoluteWord: 0xff80, SourceAbsoluteLong: 0x10006000)); + report.Complete(output); + } + + internal static void Branch(SyntheticMachine machine, CoverageBatch report, int condition, int width, int displacement, int ccr, bool supervisor) + { + machine.Reset(ccr, supervisor); + var words = new List { (ushort)(0x6000 | condition << 8 | (width == 1 ? (byte)displacement : width == 4 ? 255 : 0)) }; + if (width == 4) words.Add((ushort)(displacement >> 16)); + if (width > 1) words.Add((ushort)displacement); + var nextPc = SyntheticMachine.Code + (uint)words.Count * 2; + var target = unchecked(SyntheticMachine.Code + 2 + (uint)displacement); + var taken = condition == 1 || ControlSpecification.Condition(condition, ccr); + var sentinel = ControlSpecification.InstallTargetSentinel(machine, target, nextPc); + var expected = SyntheticExecution.Prepare(machine, words); + if (taken) + { + expected.Pc = target; + if (condition == 1) { expected.A[7] -= 4; expected.Write(expected.A[7], nextPc, 4, machine.Model); } + } + SyntheticExecution.Run(machine, expected, report, $"{machine.Model.Id}/{(condition == 0 ? "BRA" : condition == 1 ? "BSR" : "Bcc")}/{width}/condition={condition}/super={supervisor}/op={words[0]:X4}/disp={displacement}/ccr={ccr:X2}", !taken || sentinel); + } + + internal static void Dbcc(SyntheticMachine machine, CoverageBatch report, int condition, int reg, uint count, int ccr, int displacement) + { + machine.Reset(ccr); machine.Core.State.D[reg] = 0xa55a0000 | count; + var target = unchecked(SyntheticMachine.Code + 2 + (uint)displacement); + var sentinel = ControlSpecification.InstallTargetSentinel(machine, target, SyntheticMachine.Code + 4); + var opcode = (ushort)(0x50c8 | condition << 8 | reg); + var expected = SyntheticExecution.Prepare(machine, [opcode, (ushort)displacement]); + var taken = false; + if (!ControlSpecification.Condition(condition, ccr)) + { + var result = (count - 1) & 65535; + expected.D[reg] = (expected.D[reg] & 0xffff0000) | result; + if (result != 65535) { expected.Pc = target; taken = true; } + } + SyntheticExecution.Run(machine, expected, report, $"{machine.Model.Id}/DBcc/W/D{reg}/condition={condition}/op={opcode:X4}/count={count:X4}/disp={displacement}/ccr={ccr:X2}", !taken || sentinel); + } + + internal static void Scc(SyntheticMachine machine, CoverageBatch report, int condition, OperandForm form, int ccr, IndexFixture? index = null, bool supervisor = true) + { + machine.Reset(ccr, supervisor); + var opcode = (ushort)(0x50c0 | condition << 8 | form.Mode << 3 | form.Register); + var fixture = new OperandFixture(machine, [opcode], form, 1, 0xa5, source: false, index: index); + fixture.Write(ControlSpecification.Condition(condition, ccr) ? 255u : 0); + SyntheticExecution.Run(machine, fixture.Expected, report, $"{machine.Model.Id}/Scc/B/{form.Id}/condition={condition}/{index?.Id ?? "brief"}/super={supervisor}/op={opcode:X4}/ccr={ccr:X2}"); + } + + internal static void Jump(SyntheticMachine machine, CoverageBatch report, bool subroutine, OperandForm form, int ccr, bool supervisor, + IndexFixture? index = null, AddressOptions? options = null) + { + machine.Reset(ccr, supervisor); + var opcode = (ushort)((subroutine ? 0x4e80 : 0x4ec0) | form.Mode << 3 | form.Register); + var words = new List { opcode }; + var d = (uint[])machine.Core.State.D.Clone(); var a = (uint[])machine.Core.State.A.Clone(); + var addressing = new AddressingFixture(machine, 2, words, d, a, 0, options ?? new(SourceIndex: index)); + var address = addressing.Resolve(form, true); + var expected = SyntheticExecution.Prepare(machine, words); + var nextPc = expected.Pc; + // A null-displacement PC pointer may straddle the following opcode. Use + // an even reference word so this legal jump fixture selects aligned code. + machine.InitializePhysical(nextPc, 0x4e70, 2); machine.InitializePhysical(nextPc + 2, 0x4e70, 2); + expected.Write(nextPc, 0x4e70, 2, machine.Model); expected.Write(nextPc + 2, 0x4e70, 2, machine.Model); + address = addressing.SourcePointerTarget?.Invoke() ?? address; + var sentinel = ControlSpecification.InstallTargetSentinel(machine, address, nextPc); + if (sentinel) { expected.Write(address, 0x4e71, 2, machine.Model); expected.Write(address + 2, 0x4e71, 2, machine.Model); } + machine.Start(); + if (subroutine) { expected.A[7] -= 4; expected.Write(expected.A[7], nextPc, 4, machine.Model); } + expected.Pc = address; + SyntheticExecution.Run(machine, expected, report, $"{machine.Model.Id}/{(subroutine ? "JSR" : "JMP")}/none/{form.Id}/{index?.Id ?? "brief"}/super={supervisor}/op={opcode:X4}/ccr={ccr:X2}", sentinel); + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticDeepAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticDeepAuditTests.cs index 190ceb63..50ffe90a 100644 --- a/Copper68k.Tests/Synthetic/SyntheticDeepAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticDeepAuditTests.cs @@ -28,6 +28,8 @@ public void RecordedSeedMoveSamples() var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == id)); var report = new CoverageBatch(id, $"move-seeded-{seed}"); var arithmetic = new CoverageBatch(id, $"arithmetic-seeded-{seed}"); + var logical = new CoverageBatch(id, $"logical-seeded-{seed}"); + var control = new CoverageBatch(id, $"control-seeded-{seed}"); var random = seed; // xorshift32; stable across framework versions. uint Next() { random ^= random << 13; random ^= random >> 17; random ^= random << 5; return random; } for (var i = 0; i < samples; i++) @@ -47,9 +49,23 @@ public void RecordedSeedMoveSamples() var source = family.EndsWith('Q') ? 1 + Next() % 8 : Next() & ~1u; SyntheticArithmeticTests.Case(machine, arithmetic, family, width, form, source, Next() & ~1u, (int)(Next() & 31), reg: (int)(Next() % 8), supervisor: (Next() & 1) != 0, scenario: $"seed={seed}/sample={i}"); + if (Next() % 4 == 0) + SyntheticBitFieldTests.Case(machine, logical, (int)(Next() % 8), new(2, 0), unchecked((int)Next()), (int)(1 + Next() % 32), Next(), (int)(Next() & 31), offsetReg: 2, widthReg: 3); + else + { + var logicalFamily = LogicalSpecification.BinaryFamilies.Concat(LogicalSpecification.UnaryFamilies).ToArray(); + var operation = logicalFamily[Next() % (uint)logicalFamily.Length]; + SyntheticLogicalTests.Case(machine, logical, operation, operation == "TAS" ? 1 : width, form, Next() & ~1u, Next() & ~1u, (int)(Next() & 31), reg: (int)(Next() % 8), supervisor: (Next() & 1) != 0); + } + if ((Next() & 1) == 0) + SyntheticControlTests.Dbcc(machine, control, (int)(Next() % 16), (int)(Next() % 8), Next(), (int)(Next() & 31), unchecked((short)(Next() & 0xfffe))); + else + SyntheticControlTests.Branch(machine, control, (int)(Next() % 16), machine.Model.FullIndex && Next() % 3 == 0 ? 4 : 2, unchecked((short)(Next() & 0xfffe)), (int)(Next() & 31), (Next() & 1) != 0); } report.Complete(output); arithmetic.Complete(output); + logical.Complete(output); + control.Complete(output); } } } diff --git a/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs b/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs new file mode 100644 index 00000000..08d4b1a9 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs @@ -0,0 +1,172 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticExceptionControlTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void RteFramesPrivilegeStackSelectionAndInvalidFormats(string modelId) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); var report = new CoverageBatch(modelId, "system-rte"); + foreach (var supervisor in new[] { false, true }) + foreach (var restoredSupervisor in new[] { false, true }) + for (var format = 0; format < 16; format++) + for (var ccr = 0; ccr < 32; ccr++) + { + m.Reset(ccr, supervisor); var sp = m.Core.State.A[7]; var sr = (ushort)((restoredSupervisor ? 0x2000 : 0) | (ccr ^ 31)); + m.InitializePhysical(sp, sr, 2); m.InitializePhysical(sp + 2, 0x6000, 4); m.InitializePhysical(sp + 6, (uint)(format << 12 | 0x24), 2); + if (m.Model.FullIndex && modelId != "68060" && format == 1) + { + var second = restoredSupervisor ? sp + 8 : 0x7800u; + m.InitializePhysical(second, sr, 2); m.InitializePhysical(second + 2, 0x6000, 4); m.InitializePhysical(second + 6, 0x24, 2); + } + m.InitializePhysical(0x6000, 0x4e71, 2); m.InitializePhysical(0x6002, 0x4e71, 2); + var e = SyntheticExecution.Prepare(m, [0x4e73]); + // Fault restart and coprocessor internal-state restoration remain in retained specialist suites. + var outside = modelId == "68040" ? format == 7 : + m.Model.FullIndex && modelId != "68060" && format is 9 or 10 or 11; + if (outside && supervisor) continue; + var size = modelId == "68000" ? 6u : format == 0 ? 8u : modelId == "68010" && format == 8 ? 58u : + m.Model.FullIndex && modelId != "68060" && format == 1 ? 8u : m.Model.FullIndex && format == 2 ? 12u : modelId is "68040" or "68060" && format == 3 ? 12u : modelId == "68060" && format == 4 ? 16u : 0; + if (!supervisor) SyntheticExecution.ExpectException(m, e, 8); + else if (size == 0) SyntheticExecution.ExpectException(m, e, 14); + else + { + e.A[7] = sp + size; SyntheticSystemTests.ApplyStatus(m, e, sr); + if (m.Model.FullIndex && modelId != "68060" && format == 1) e.A[7] += 8; + e.Pc = 0x6000; + } + SyntheticExecution.Run(m, e, report, $"{modelId}/RTE/frame{format:X}/super={supervisor}/restoreSuper={restoredSupervisor}/op=4E73/ccr={ccr:X2}"); + } + if (m.Model.FullIndex && modelId != "68060") + for (var ccr = 0; ccr < 32; ccr++) + { + m.Reset(ccr); var isp = m.Core.State.A[7]; + m.Core.State.SetMasterStackPointer(0x7400); + m.InitializePhysical(isp, 0x3000u | (uint)ccr, 2); m.InitializePhysical(isp + 2, 0x6100, 4); m.InitializePhysical(isp + 6, 0x1024, 2); + m.InitializePhysical(0x7400, 0x3000u | (uint)(ccr ^ 31), 2); m.InitializePhysical(0x7402, 0x6000, 4); m.InitializePhysical(0x7406, 0x24, 2); + m.InitializePhysical(0x6000, 0x4e71, 2); m.InitializePhysical(0x6002, 0x4e71, 2); + var e = SyntheticExecution.Prepare(m, [0x4e73]); m.Core.State.SetMasterStackPointer(0x7400); + e.Sr = (ushort)(0x3000 | (ccr ^ 31)); e.A[7] = 0x7408; e.MasterStackPointer = 0x7408; e.Pc = 0x6000; + e.ControlChecks["ISP after throwaway"] = (s => s.InterruptStackPointer, isp + 8); + SyntheticExecution.Run(m, e, report, $"{modelId}/RTE/frame1-to-frame0/MSP/op=4E73/ccr={ccr:X2}"); + } + report.Complete(output); + } + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void TraceRetirementTakenAndUntakenFlowStopsAndAbortingFaults(string modelId) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); var report = new CoverageBatch(modelId, "system-trace"); + foreach (var trace in m.Model.FullIndex && modelId != "68060" ? new[] { 0x8000, 0x4000 } : new[] { 0x8000 }) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var family in new[] { "NOP", "BRA", "BNE", "DBF", "STOP", "MOVEtoSR", "TRAP", "TRAPV", "ILLEGAL" }) + { + m.Reset(ccr, supervisor); m.Core.State.D[0] = 1; + var words = family switch { "NOP" => new ushort[] { 0x4e71 }, "BRA" => [0x6010], "BNE" => [0x6610], "DBF" => [0x51c8, 0x0010], + "STOP" => [0x4e72, 0x2700], "MOVEtoSR" => [0x46fc, 0x2700], "TRAP" => [0x4e43], "TRAPV" => [0x4e76], _ => [0x4afc] }; + m.InitializePhysical(0x1012, 0x4e71, 2); m.InitializePhysical(0x1014, 0x4e71, 2); + var e = SyntheticExecution.Prepare(m, words); m.Core.State.StatusRegister |= (ushort)trace; e.Sr |= (ushort)trace; + var abort = family == "ILLEGAL" || family is "STOP" or "MOVEtoSR" && !supervisor; + var flow = family == "BRA" || family == "BNE" && (ccr & 4) == 0 || family == "DBF"; + if (flow) { e.Pc = 0x1012; if (family == "DBF") e.D[0] = 0; } + if (family is "STOP" or "MOVEtoSR" && supervisor) { SyntheticSystemTests.ApplyStatus(m, e, 0x2700); flow = true; } + if (abort) SyntheticExecution.ExpectException(m, e, family == "ILLEGAL" ? 4 : 8); + else + { + if (family == "TRAP" || family == "TRAPV" && (ccr & 2) != 0) + { SyntheticExecution.ExpectException(m, e, family == "TRAP" ? 35 : 7, e.Pc); flow = true; } + if (trace == 0x8000 || flow) SyntheticExecution.ExpectException(m, e, 9, e.Pc); + } + SyntheticExecution.Run(m, e, report, $"{modelId}/trace-{family}/none/T={trace:X4}/super={supervisor}/op={words[0]:X4}/ccr={ccr:X2}", false); + } + foreach (var trace in m.Model.FullIndex && modelId != "68060" ? new[] { 0x8000, 0x4000 } : new[] { 0x8000 }) + foreach (var afterFirst in new[] { false, true }) + foreach (var selfBranch in new[] { false, true }) + { + m.Reset(31); var e = SyntheticExecution.Prepare(m, selfBranch ? [0x60fe] : new ushort[] { 0x4e71, 0x60fc }); + e.Sr |= (ushort)trace; + var tracedAddress = selfBranch ? SyntheticMachine.Code : afterFirst || trace == 0x4000 ? SyntheticMachine.Code + 2 : SyntheticMachine.Code; + var savedPc = tracedAddress == SyntheticMachine.Code && !selfBranch ? SyntheticMachine.Code + 2 : SyntheticMachine.Code; + SyntheticExecution.ExpectException(m, e, 9, savedPc); + if (m.Model.FullIndex) e.Write(e.A[7] + 8, tracedAddress, 4, m.Model); + var boundary = new TraceBoundary(m.Core.State, (ushort)trace, afterFirst); + ((IM68kBatchCore)m.Core).ExecuteInstructions(3, null, boundary); + var mismatch = e.Verify(m); + report.Record($"{modelId}/trace-batch/none/T={trace:X4}/afterFirst={afterFirst}/selfBranch={selfBranch}", mismatch == null ? "passing" : "mismatching", mismatch); + } + report.Complete(output); + } + private sealed class TraceBoundary(M68kCpuState state, ushort trace, bool afterFirst) : IM68kInstructionBoundary + { + private int retired; + public bool BeforeInstruction() + { + if (state.LastExceptionVector == 9) return false; + if (!afterFirst && retired == 0) state.StatusRegister |= trace; + return true; + } + public void AfterInstruction(long previousCycle, long currentCycle) + { + retired++; + if (afterFirst && retired == 1) state.StatusRegister |= trace; + } + } + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void InterruptMasksStopWakeVectorBaseAndMasterFrames(string modelId) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); var report = new CoverageBatch(modelId, "system-interrupt"); + for (var mask = 0; mask < 8; mask++) + for (var level = 1; level < 8; level++) + foreach (var supervisor in new[] { false, true }) + foreach (var stopped in new[] { false, true }) + { + m.Reset(31, supervisor); var e = SyntheticExecution.Prepare(m, [0x4e71]); e.Pc = SyntheticMachine.Code; + m.Core.State.StatusRegister = (ushort)((supervisor ? 0x2000 : 0) | mask << 8 | 31); e.Sr = m.Core.State.StatusRegister; + m.Core.State.Stopped = stopped; e.Stopped = stopped; + var accepted = level > mask || level == 7; + if (accepted) + { + SyntheticExecution.ExpectException(m, e, 28, SyntheticMachine.Code); e.Sr = (ushort)((e.Sr & 0xf8ff) | level << 8); e.Stopped = false; + } + try { m.Core.RequestInterrupt(level, 28 * 4); var mismatch = e.Verify(m); report.Record($"{modelId}/interrupt/none/mask={mask}/level={level}/super={supervisor}/stop={stopped}", mismatch == null ? "passing" : "mismatching", mismatch); } + catch (Exception ex) { report.Record($"{modelId}/interrupt/mask={mask}/level={level}", "mismatching", ex.Message); } + } + // The interrupt API takes a vector-table offset. VBR must relocate it. + if (modelId != "68000") + foreach (var supervisor in new[] { false, true }) + { + m.Reset(31, supervisor); _ = SyntheticExecution.Prepare(m, [0x4e71]); + m.Core.State.VectorBaseRegister = 0x20000; m.Core.State.StatusRegister = (ushort)((supervisor ? 0x2000 : 0) | 31); + m.InitializePhysical(0x20000 + 28 * 4, 0x6000, 4); + var e = ArchitecturalExpectation.Capture(m); + SyntheticExecution.ExpectException(m, e, 28, SyntheticMachine.Code); e.Sr = (ushort)((e.Sr & 0xf8ff) | 0x300); + m.Core.RequestInterrupt(3, 28 * 4); var mismatch = e.Verify(m); + report.Record($"{modelId}/interrupt/none/VBR/super={supervisor}", mismatch == null ? "passing" : "mismatching", mismatch); + } + if (m.Model.FullIndex && modelId != "68060") + foreach (var trace in new[] { 0, 0x8000, 0x4000 }) + for (var ccr = 0; ccr < 32; ccr++) + { + m.Reset(ccr); _ = SyntheticExecution.Prepare(m, [0x4e71]); var isp = m.Core.State.A[7]; + m.Core.State.SetMasterStackPointer(0x7400); m.Core.State.StatusRegister = (ushort)(trace | 0x3000 | ccr); + var e = ArchitecturalExpectation.Capture(m); var saved = e.Sr; + e.MasterStackPointer = 0x73f8; e.A[7] = isp - 8; e.Sr = (ushort)(0x2300 | ccr); e.Pc = 0x91c0; e.ExceptionVector = 28; + foreach (var (frame, format) in new[] { (0x73f8u, 0x70u), (isp - 8, 0x1070u) }) + { e.Write(frame, saved, 2, m.Model); e.Write(frame + 2, SyntheticMachine.Code, 4, m.Model); e.Write(frame + 6, format, 2, m.Model); } + m.Core.RequestInterrupt(3, 28 * 4); var mismatch = e.Verify(m); + report.Record($"{modelId}/interrupt/none/master/T={trace:X4}/ccr={ccr:X2}", mismatch == null ? "passing" : "mismatching", mismatch); + if (mismatch == null) + { + // Return from the handler through ISP throwaway and MSP frame. + m.InitializePhysical(e.Pc, 0x4e73, 2); e.Write(e.Pc, 0x4e73, 2, m.Model); + e.A[7] = 0x7400; e.MasterStackPointer = 0x7400; e.Sr = saved; e.Pc = SyntheticMachine.Code; e.ExceptionVector = null; + e.ControlChecks["ISP restored"] = (state => state.InterruptStackPointer, isp); + SyntheticExecution.Run(m, e, report, $"{modelId}/RTE/interrupt-master/T={trace:X4}/op=4E73/ccr={ccr:X2}", false); + } + } + report.Complete(output); + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticExecution.cs b/Copper68k.Tests/Synthetic/SyntheticExecution.cs index 18f55625..274b502c 100644 --- a/Copper68k.Tests/Synthetic/SyntheticExecution.cs +++ b/Copper68k.Tests/Synthetic/SyntheticExecution.cs @@ -18,7 +18,7 @@ public static ArchitecturalExpectation Prepare(SyntheticMachine machine, IReadOn public static void ExpectException(SyntheticMachine machine, ArchitecturalExpectation expected, int vector, uint? savedProgramCounter = null) { - var format2 = vector == 5 && machine.Model.FullIndex; + var format2 = vector is 5 or 6 or 7 or 9 && machine.Model.FullIndex; var frameSize = format2 ? 12u : machine.Model.Model == M68kCpuModel.M68000 ? 6u : 8u; var savedSr = expected.Sr; var savedPc = savedProgramCounter ?? SyntheticMachine.Code; @@ -36,17 +36,19 @@ public static void ExpectException(SyntheticMachine machine, ArchitecturalExpect if (frameSize >= 8) expected.Write(expected.A[7] + 6, (format2 ? 0x2000u : 0) | (uint)vector * 4, 2, machine.Model); if (format2) expected.Write(expected.A[7] + 8, SyntheticMachine.Code, 4, machine.Model); expected.Sr = (ushort)((savedSr | 0x2000) & ~0xc000); - expected.Pc = 0x9000u + (uint)vector * 0x10; + uint handler = 0; + for (var i = 0; i < 4; i++) handler = (handler << 8) | expected.Memory.GetValueOrDefault(machine.Model.Physical(machine.Core.State.VectorBaseRegister + (uint)vector * 4 + (uint)i)); + expected.Pc = handler; expected.ExceptionVector = vector; } - public static void Run(SyntheticMachine machine, ArchitecturalExpectation expected, CoverageBatch report, string id) + public static void Run(SyntheticMachine machine, ArchitecturalExpectation expected, CoverageBatch report, string id, bool followingInstruction = true) { try { machine.Core.ExecuteInstruction(); var mismatch = expected.Verify(machine); - if (mismatch == null && expected.ExceptionVector == null) + if (mismatch == null && expected.ExceptionVector == null && followingInstruction) { machine.Core.ExecuteInstruction(); expected.Pc += 2; diff --git a/Copper68k.Tests/Synthetic/SyntheticLogicalTests.cs b/Copper68k.Tests/Synthetic/SyntheticLogicalTests.cs new file mode 100644 index 00000000..17dc6037 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticLogicalTests.cs @@ -0,0 +1,91 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticLogicalTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void LogicalAndUnaryBoundaries(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "logical-boundaries"); + foreach (var width in new[] { 1, 2, 4 }) + foreach (var family in LogicalSpecification.BinaryFamilies) + foreach (var source in ArithmeticSpecification.Boundaries(width)) + foreach (var destination in ArithmeticSpecification.Boundaries(width)) + for (var ccr = 0; ccr < 32; ccr++) Case(machine, report, family, width, new(0, 0), source, destination, ccr); + foreach (var family in LogicalSpecification.UnaryFamilies) + foreach (var width in family == "TAS" ? new[] { 1 } : new[] { 1, 2, 4 }) + foreach (var value in ArithmeticSpecification.Boundaries(width)) + for (var ccr = 0; ccr < 32; ccr++) Case(machine, report, family, width, new(0, 0), 0, value, ccr); + report.Complete(output); + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void LogicalAndUnaryAddressing(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "logical-addressing"); + foreach (var width in new[] { 1, 2, 4 }) + foreach (var family in LogicalSpecification.BinaryFamilies.Concat(new[] { "AND.store", "OR.store" }).Concat(LogicalSpecification.UnaryFamilies)) + { + if (family == "TAS" && width != 1) continue; + var sourceEa = family is "AND" or "OR" || family == "TST" && machine.Model.FullIndex; + var forms = sourceEa ? ArithmeticSpecification.Sources(width, family == "TST") : ArithmeticSpecification.Alterable(); + foreach (var form in forms) + { + if (family.EndsWith(".store") && form.Mode == 0) continue; + foreach (var supervisor in new[] { false, true }) + Case(machine, report, family, width, form, 0x8002, 0x7ffffffe, 31, supervisor: supervisor); + } + if (machine.Model.FullIndex) + foreach (var index in IndexFixture.FullStructures()) + foreach (var form in sourceEa ? new[] { new OperandForm(6, 0), new OperandForm(7, 3) } : new[] { new OperandForm(6, 0) }) + Case(machine, report, family, width, form, 0x8002, 0x7ffffffe, 31, index: index); + for (var src = 0; src < 8; src++) + for (var dst = 0; dst < 8; dst++) + Case(machine, report, family.Replace(".store", ""), width, new(0, src), 0x8002, 0x7ffffffe, 31, reg: dst); + foreach (var index in new[] { new IndexFixture(Scale: 3), new IndexFixture(AddressIndex: true, LongIndex: true, Scale: 2) }) + Case(machine, report, family, width, new(6, 0), 0x8002, 0x7ffffffe, 31, index: index, negativeIndex: true); + foreach (var form in new[] { new OperandForm(7, 0), new OperandForm(7, 1) }) + Case(machine, report, family, width, form, 0x8002, 0x7ffffffe, 31, + options: new(SourceAbsoluteWord: 0xff80, DestinationAbsoluteWord: 0xff80, SourceAbsoluteLong: 0x10006000, DestinationAbsoluteLong: 0x10006000)); + } + report.Complete(output); + } + + internal static void Case(SyntheticMachine machine, CoverageBatch report, string family, int width, OperandForm form, uint source, uint destination, int ccr, + int reg = 1, bool supervisor = true, IndexFixture? index = null, AddressOptions? options = null, bool negativeIndex = false) + { + machine.Reset(ccr, supervisor); + if (negativeIndex) { machine.Core.State.D[7] = 0xffff8002; machine.Core.State.SetActiveStackPointer(0xffff8002); } + var unary = LogicalSpecification.UnaryFamilies.Contains(family); var immediate = family.EndsWith('I'); + var store = family.EndsWith(".store") || family == "EOR"; + var size = ArithmeticSpecification.SizeField(width); var words = new List(); + ushort opcode; + if (unary) opcode = (ushort)((family switch { "CLR" => 0x4200, "NEG" => 0x4400, "NEGX" => 0x4000, "NOT" => 0x4600, "TST" => 0x4a00, _ => 0x4ac0 }) | (family == "TAS" ? 0 : size << 6) | form.Mode << 3 | form.Register); + else if (immediate) opcode = (ushort)((family == "ORI" ? 0 : family == "ANDI" ? 0x200 : 0xa00) | size << 6 | form.Mode << 3 | form.Register); + else opcode = (ushort)((family.StartsWith("AND") ? 0xc000 : family.StartsWith("OR") ? 0x8000 : 0xb000) | reg << 9 | (size + (store ? 4 : 0)) << 6 | form.Mode << 3 | form.Register); + words.Add(opcode); + if (immediate) { if (width == 4) words.Add((ushort)(source >> 16)); words.Add((ushort)(source & MoveSpecification.Mask(width))); } + machine.Core.State.D[reg] = ArithmeticSpecification.RegisterBits(store ? source : destination, width); + var fixture = new OperandFixture(machine, words, form, width, unary || immediate || store ? destination : source, source: !store && !immediate && !unary, index: index, options: options); + var expected = fixture.Expected; + if (unary) + { + var result = LogicalSpecification.Unary(family, fixture.Value, width, expected.Sr); + expected.Sr = result.Sr; if (family != "TST") fixture.Write(result.Value); + } + else + { + var lhs = immediate || store ? fixture.Value : expected.D[reg]; + var rhs = immediate ? source : store ? expected.D[reg] : fixture.Value; + var result = LogicalSpecification.Binary(family, lhs, rhs) & MoveSpecification.Mask(width); + expected.Sr = SyntheticExecution.MoveFlags(expected.Sr, result, width); + if (immediate || store) fixture.Write(result); else expected.D[reg] = (expected.D[reg] & ~MoveSpecification.Mask(width)) | result; + } + SyntheticExecution.Run(machine, expected, report, $"{machine.Model.Id}/{family}/{width}/{form.Id}/r{reg}/{index?.Id ?? "brief"}/super={supervisor}/op={opcode:X4}/s={source:X8}/d={destination:X8}/ccr={ccr:X2}"); + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticMachine.cs b/Copper68k.Tests/Synthetic/SyntheticMachine.cs index de90fb52..b2bb7081 100644 --- a/Copper68k.Tests/Synthetic/SyntheticMachine.cs +++ b/Copper68k.Tests/Synthetic/SyntheticMachine.cs @@ -27,11 +27,17 @@ public IM68kCore Create(SparseRecordingBus bus) => A1200 internal readonly record struct BusAccess(uint Address, int Width, bool Write, uint Value, M68kBusAccessKind Kind); -internal sealed class SparseRecordingBus : IM68kBus +internal sealed class SparseRecordingBus : IM68kBus, IM68kModuleAccessBus { public Dictionary Memory { get; } = []; public List Accesses { get; } = []; - public void Clear() { Memory.Clear(); Accesses.Clear(); } + public Dictionary ModuleRegisters { get; } = []; + public bool ModuleResponder { get; set; } + public bool TryReadModuleByte(uint address, out byte value) { value = (byte)ModuleRegisters.GetValueOrDefault(address); return ModuleResponder; } + public bool TryWriteModuleByte(uint address, byte value) { if (ModuleResponder) ModuleRegisters[address] = value; return ModuleResponder; } + public bool TryWriteModuleLong(uint address, uint value) { if (ModuleResponder) ModuleRegisters[address] = value; return ModuleResponder; } + public int DeviceResets { get; private set; } + public void Clear() { Memory.Clear(); Accesses.Clear(); ModuleRegisters.Clear(); ModuleResponder = false; DeviceResets = 0; } public byte Peek(uint address) => Memory.GetValueOrDefault(address); public uint Peek(uint address, int width) { @@ -60,7 +66,7 @@ private void Write(uint address, uint value, int width, M68kBusAccessKind kind) public void WriteByte(uint address, byte value, ref long cycle, M68kBusAccessKind kind) => Write(address, value, 1, kind); public void WriteWord(uint address, ushort value, ref long cycle, M68kBusAccessKind kind) => Write(address, value, 2, kind); public void WriteLong(uint address, uint value, ref long cycle, M68kBusAccessKind kind) => Write(address, value, 4, kind); - public void ResetExternalDevices(long cycle) { } + public void ResetExternalDevices(long cycle) { DeviceResets++; } } internal sealed class SyntheticMachine(ModelSpec model) diff --git a/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs b/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs new file mode 100644 index 00000000..98ce4a0f --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs @@ -0,0 +1,155 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticModelSystemTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void MovesAllSizesRegistersMemoryFormsAndPrivilege(string modelId) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); var report = new CoverageBatch(modelId, "system-moves"); + foreach (var width in new[] { 1, 2, 4 }) + foreach (var form in ArithmeticSpecification.Alterable().Where(f => f.Memory)) + for (var g = 0; g < 16; g++) + foreach (var store in new[] { false, true }) + foreach (var supervisor in new[] { false, true }) Moves(m, report, width, form, g, store, supervisor, 31, 0x80000080); + foreach (var width in new[] { 1, 2, 4 }) + foreach (var value in ArithmeticSpecification.Boundaries(width)) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var g in new[] { 1, 9 }) + foreach (var store in new[] { false, true }) Moves(m, report, width, new(2, 0), g, store, true, ccr, value); + if (m.Model.FullIndex) + foreach (var index in IndexFixture.FullStructures()) + foreach (var width in new[] { 1, 2, 4 }) + foreach (var store in new[] { false, true }) Moves(m, report, width, new(6, 7), 0, store, true, 31, 0x80, index); + report.Complete(output); + } + private static void Moves(SyntheticMachine m, CoverageBatch report, int width, OperandForm form, int general, bool store, bool supervisor, int ccr, uint value, IndexFixture? index = null) + { + // MOVES An,(An)+ / -(An) store value is architecturally undefined. + if (store && general >= 8 && general - 8 == form.Register && form.Mode is 3 or 4) return; + m.Reset(ccr, supervisor); + if (store) { if (general < 8) m.Core.State.D[general] = value; else if (general == 15) m.Core.State.SetActiveStackPointer(value); else m.Core.State.A[general - 8] = value; } + var opcode = (ushort)(0x0e00 | ArithmeticSpecification.SizeField(width) << 6 | form.Mode << 3 | form.Register); + var fixture = new OperandFixture(m, [opcode, (ushort)(general << 12 | (store ? 0x800 : 0))], form, width, value, index: index ?? new(IndexRegister: general == 7 ? 6 : 7)); + var e = fixture.Expected; + m.Core.State.SourceFunctionCode = 1; m.Core.State.DestinationFunctionCode = 5; + e.ControlChecks["SFC"] = (s => s.SourceFunctionCode, 1); e.ControlChecks["DFC"] = (s => s.DestinationFunctionCode, 5); + if (modelIdOf(m) == "68000" || !supervisor) + { + m.Core.State.A.CopyTo(e.A, 0); SyntheticExecution.ExpectException(m, e, modelIdOf(m) == "68000" ? 4 : 8); + for (var i = 0; i < width; i++) e.ForbiddenOperandReads.Add(m.Model.Physical(fixture.Address + (uint)i)); + } + else if (store) e.Write(fixture.Address, general < 8 ? e.D[general] : e.A[general - 8], width, m.Model); + else if (general < 8) e.D[general] = (e.D[general] & ~MoveSpecification.Mask(width)) | fixture.Value; + else e.A[general - 8] = unchecked((uint)ArithmeticSpecification.Signed(fixture.Value, width)); + SyntheticExecution.Run(m, e, report, $"{m.Model.Id}/MOVES/{width}/{form.Id}/R{general}/store={store}/super={supervisor}/{index?.Id ?? "brief"}/op={opcode:X4}/value={value:X8}/ccr={ccr:X2}"); + } + private static string modelIdOf(SyntheticMachine m) => m.Model.Id; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void MovecControlInventoryMasksPrivilegeAndAllGeneralRegisters(string modelId) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); var report = new CoverageBatch(modelId, "system-movec"); + foreach (var control in new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 0x800, 0x801, 0x802, 0x803, 0x804, 0x805, 0x806, 0x807, 0x808, 0xfff }) + for (var general = 0; general < 16; general++) + foreach (var store in new[] { false, true }) + foreach (var supervisor in new[] { false, true }) + foreach (var ccr in new[] { 0, 31 }) + { + m.Reset(ccr, supervisor); + var value = control switch { 2 => modelId == "68060" ? 0xf880e000u : modelId == "68040" ? 0x80008000u : modelId == "68030" ? 0x3f1fu : 0xfu, 8 => 0xf0000000u, 0x808 => 0x83u, 0x802 => 0xfcu, _ => 0x7100u }; + if (store) { if (general < 8) m.Core.State.D[general] = value; else if (general == 15) m.Core.State.SetActiveStackPointer(0x7100); else m.Core.State.A[general - 8] = value; } + var op = (ushort)(store ? 0x4e7b : 0x4e7a); var e = SyntheticExecution.Prepare(m, [op, (ushort)(general << 12 | control)]); + var valid = modelId != "68000" && (control is 0 or 1 or 0x800 or 0x801 || m.Model.FullIndex && (control == 2 || control == 0x802 && modelId is not ("68040" or "68060") || control is 0x803 or 0x804 && modelId != "68060") || modelId is "68040" or "68060" && (control is >= 3 and <= 7 or 0x806 or 0x807 || control == 0x805 && modelId == "68040" || control is 8 or 0x808 && modelId == "68060")); + if (modelId == "68000" || !supervisor || !valid) SyntheticExecution.ExpectException(m, e, modelId == "68000" || supervisor ? 4 : 8); + else + { + var read = ControlReader(control); var actualValue = read(m.Core.State); + if (store) + { + var source = general < 8 ? e.D[general] : e.A[general - 8]; + var masked = control switch { 0 or 1 => source & 7, 2 when modelId == "68060" => source & 0xf880e000, 2 when modelId == "68040" => source & 0x80008000, 2 => source & (modelId == "68030" ? 0x3313u : 3u), 8 => source & 0xf0000000, 0x808 => 0x04300000 | source & 0x83, 0x806 or 0x807 when modelId == "68060" => source & 0xfffffe00, _ => source }; + e.ControlChecks[$"control {control:X3}"] = (read, masked); + if (control == 0x800) e.InactiveStackPointer = masked; + if (control == 0x803) e.MasterStackPointer = masked; + if (control == 0x804) e.A[7] = masked; + } + else if (general < 8) e.D[general] = actualValue; + else e.A[general - 8] = actualValue; + } + SyntheticExecution.Run(m, e, report, $"{modelId}/MOVEC/L/R{general}/control={control:X3}/store={store}/super={supervisor}/op={op:X4}/ccr={ccr:X2}"); + } + report.Complete(output); + } + private static Func ControlReader(int control) => control switch + { + 0 => s => s.SourceFunctionCode, 1 => s => s.DestinationFunctionCode, 2 => s => s.CacheControlRegister, + 3 => s => s.M68040Mmu.TranslationControl, 4 => s => s.M68040Mmu.InstructionTransparentTranslation0, + 5 => s => s.M68040Mmu.InstructionTransparentTranslation1, 6 => s => s.M68040Mmu.DataTransparentTranslation0, + 7 => s => s.M68040Mmu.DataTransparentTranslation1, 8 => s => s.M68060BusControl, + 0x800 => s => s.UserStackPointer, 0x801 => s => s.VectorBaseRegister, 0x802 => s => s.CacheAddressRegister, + 0x803 => s => s.MasterStackPointer, 0x804 => s => s.InterruptStackPointer, 0x805 => s => s.M68040Mmu.Status, + 0x806 => s => s.M68040Mmu.UserRootPointer, 0x807 => s => s.M68040Mmu.SupervisorRootPointer, + 0x808 => s => s.M68060ProcessorConfiguration, _ => throw new InvalidOperationException() + }; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void Move16CacheInstructionsBreakpointsAndLowPowerStop(string modelId) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); var report = new CoverageBatch(modelId, "system-model"); + for (var ccr = 0; ccr < 32; ccr++) + foreach (var supervisor in new[] { false, true }) + { + for (var breakpoint = 0; breakpoint < 8; breakpoint++) + { + m.Reset(ccr, supervisor); var op = (ushort)(0x4848 | breakpoint); var e = SyntheticExecution.Prepare(m, [op]); + SyntheticExecution.ExpectException(m, e, 4); SyntheticExecution.Run(m, e, report, $"{modelId}/BKPT/none/{breakpoint}/super={supervisor}/op={op:X4}/ccr={ccr:X2}"); + } + foreach (var push in new[] { false, true }) + for (var cache = 1; cache < 4; cache++) + for (var scope = 1; scope < 4; scope++) + for (var reg = 0; reg < 8; reg++) + { + m.Reset(ccr, supervisor); var op = (ushort)(0xf400 | (push ? 0x20 : 0) | cache << 6 | scope << 3 | reg); var e = SyntheticExecution.Prepare(m, [op]); + if (modelId is not ("68040" or "68060")) SyntheticExecution.ExpectException(m, e, 11); + else if (!supervisor) SyntheticExecution.ExpectException(m, e, 8); + SyntheticExecution.Run(m, e, report, $"{modelId}/{(push ? "CPUSH" : "CINV")}/{scope}/cache={cache}/A{reg}/super={supervisor}/op={op:X4}/ccr={ccr:X2}"); + } + foreach (var sr in new ushort[] { 0, 0x0700, 0x2000, 0x2700, 0x271f }) + { + m.Reset(ccr, supervisor); var e = SyntheticExecution.Prepare(m, [0xf800, 0x01c0, sr]); + if (modelId != "68060") SyntheticExecution.ExpectException(m, e, 11); + else if (!supervisor || (sr & 0x2000) == 0) SyntheticExecution.ExpectException(m, e, 8); + else { SyntheticSystemTests.ApplyStatus(m, e, sr); e.Stopped = true; } + SyntheticExecution.Run(m, e, report, $"{modelId}/LPSTOP/W/imm={sr:X4}/super={supervisor}/op=F800/ccr={ccr:X2}", !e.Stopped); + } + } + for (var form = 0; form < 5; form++) + for (var r = 0; r < 8; r++) + foreach (var other in form == 4 ? Enumerable.Range(0, 8) : new[] { 0 }) + for (var offset = 0; offset < 16; offset++) + foreach (var supervisor in new[] { false, true }) + { + var fixtureOffset = modelId is "68040" or "68060" ? offset : offset & ~1; + m.Reset(31, supervisor); if (r == 7) m.Core.State.SetActiveStackPointer(0x7000u + (uint)fixtureOffset); else m.Core.State.A[r] = 0x7000u + (uint)fixtureOffset; + if (form == 4 && other != r) { if (other == 7) m.Core.State.SetActiveStackPointer(0x7100u + (uint)(14 - fixtureOffset)); else m.Core.State.A[other] = 0x7100u + (uint)(14 - fixtureOffset); } + var source = form == 4 || (form & 1) == 0 ? m.Core.State.A[r] & ~15u : 0x7200; + var destination = form == 4 ? m.Core.State.A[other] & ~15u : (form & 1) == 0 ? 0x7200 : m.Core.State.A[r] & ~15u; + for (var i = -8; i < 24; i++) { m.InitializePhysical(source + (uint)i, (uint)(byte)(i * 7), 1); if (source != destination) m.InitializePhysical(destination + (uint)i, 0xa5, 1); } + var op = (ushort)(0xf600 | form << 3 | r); var e = SyntheticExecution.Prepare(m, form == 4 ? [op, (ushort)(0x8000 | other << 12)] : [op, 0, 0x720f]); + if (modelId is not ("68040" or "68060")) SyntheticExecution.ExpectException(m, e, 11); + else + { + for (var i = 0; i < 16; i++) e.Write(destination + (uint)i, m.PeekPhysical(source + (uint)i, 1), 1, m.Model); + if (form < 2 || form == 4) e.A[r] += 16; + if (form == 4 && other != r) e.A[other] += 16; + } + SyntheticExecution.Run(m, e, report, $"{modelId}/MOVE16/line/form={form}/A{r},A{other}/offset={offset}/super={supervisor}/op={op:X4}"); + } + report.Complete(output); + } +} + diff --git a/Copper68k.Tests/Synthetic/SyntheticModuleTests.cs b/Copper68k.Tests/Synthetic/SyntheticModuleTests.cs new file mode 100644 index 00000000..9853c7fc --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticModuleTests.cs @@ -0,0 +1,130 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticModuleTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void CallmDescriptorsFramesArgumentsAndAccessControl(string modelId) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); var report = new CoverageBatch(modelId, "system-callm"); + foreach (var form in ArithmeticSpecification.Sources(4).Where(f => f.Mode is 2 or 5 or 6 || f.Mode == 7 && f.Register < 4)) + for (var general = 0; general < 16; general++) + foreach (var option in new[] { 0, 4 }) + foreach (var count in new[] { 0, 1, 2, 15, 16, 255 }) + foreach (var supervisor in new[] { false, true }) Call(m, report, form, general, option, 0, count, supervisor, 31); + for (var ccr = 0; ccr < 32; ccr++) Call(m, report, new(2, 0), 9, 0, 0, 2, true, ccr); + if (m.Model.FullIndex) + foreach (var index in IndexFixture.FullStructures()) + foreach (var form in new[] { new OperandForm(6, 7), new OperandForm(7, 3) }) Call(m, report, form, 0, 0, 0, 2, true, 31, index: index); + foreach (var option in new[] { 0, 4 }) + foreach (var supervisor in new[] { false, true }) + for (var status = 0; status < 9; status++) Call(m, report, new(2, 0), 15, option, 1, 15, supervisor, 31, status); + foreach (var option in new[] { 1, 2, 3, 5, 6, 7 }) Call(m, report, new(2, 0), 0, option, 0, 0, true, 31); + foreach (var type in new[] { 2, 15, 16, 31 }) Call(m, report, new(2, 0), 0, 0, type, 0, true, 31); + foreach (var supervisor in new[] { false, true }) Call(m, report, new(2, 0), 0, 0, 1, 2, supervisor, 31); + report.Complete(output); + } + private static bool Supported(SyntheticMachine m) => m.Model.Id is "68020" or "68EC020" or "A1200"; + private static void Call(SyntheticMachine m, CoverageBatch report, OperandForm form, int general, int option, int type, int count, bool supervisor, int ccr, int status = -1, IndexFixture? index = null) + { + m.Reset(ccr, supervisor); + var argumentSp = m.Core.State.A[7]; + for (var i = 0; i < count; i++) m.InitializePhysical(argumentSp + (uint)i, (uint)(byte)(i * 11), 1); + var op = (ushort)(0x06c0 | form.Mode << 3 | form.Register); + var f = new OperandFixture(m, [op, (ushort)count], form, 4, (uint)option << 29 | (uint)type << 24, index: index); var e = f.Expected; + void Initialize(uint address, uint value, int width) + { + for (var i = 0; i < width; i++) + { + var p = m.Model.Physical(address + (uint)i); + if (p >= SyntheticMachine.Code && p < f.NextPc + 4) continue; + var b = value >> (8 * (width - 1 - i)) & 255; m.InitializePhysical(address + (uint)i, b, 1); e.Write(address + (uint)i, b, 1, m.Model); + } + } + Initialize(f.Address + 4, 0x6800, 4); Initialize(f.Address + 8, 0x7200, 4); Initialize(f.Address + 12, 0x7600, 4); + var control = m.PeekPhysical(f.Address, 4); var entry = m.PeekPhysical(f.Address + 4, 4); var data = m.PeekPhysical(f.Address + 8, 4); + Initialize(entry, (uint)general << 12, 2); Initialize(entry + 2, 0x4e71, 2); Initialize(entry + 4, 0x4e71, 2); + var oldSp = e.A[7]; + m.Start(); + if (status >= 0) { m.Bus.ModuleResponder = true; m.Bus.ModuleRegisters[0x10000] = 0x37; m.Bus.ModuleRegisters[0x1000c] = (uint)status; } + var actualOption = control >> 29; var actualType = control >> 24 & 31; + var error = actualOption is not (0 or 4) || actualType > 1 || (control & 0xffff) != 0 || actualType == 1 && status is <= 0 or > 7; + if (!Supported(m)) SyntheticExecution.ExpectException(m, e, 4); + else if (error) SyntheticExecution.ExpectException(m, e, 14); + else + { + var entryWord = m.PeekPhysical(entry, 2); var selected = (int)(entryWord >> 12); var savedData = selected < 8 ? e.D[selected] : e.A[selected - 8]; + var top = oldSp; + if (actualType == 1 && status >= 4) + { + top = m.PeekPhysical(f.Address + 12, 4); + if (actualOption == 0) { top -= (uint)count; for (var i = 0; i < count; i++) e.Write(top + (uint)i, m.PeekPhysical(oldSp + (uint)i, 1), 1, m.Model); } + } + var frame = top - 24; + e.Write(frame, (control >> 16 & 0xff00) | (actualType == 1 ? 0x37u : 0), 2, m.Model); + e.Write(frame + 2, (uint)ccr, 2, m.Model); e.Write(frame + 4, (uint)count, 2, m.Model); e.Write(frame + 6, 0, 2, m.Model); + e.Write(frame + 8, f.Address, 4, m.Model); e.Write(frame + 12, f.NextPc, 4, m.Model); e.Write(frame + 16, savedData, 4, m.Model); + e.Write(frame + 20, actualType == 1 || actualOption == 4 ? oldSp : 0, 4, m.Model); + // Reserved frame word and unused saved-SP slot are architecturally undefined. + for (var i = 0; i < 2; i++) e.MemoryMasks[m.Model.Physical(frame + 6 + (uint)i)] = 0; + if (actualType == 0 && actualOption == 0) + for (var i = 0; i < 4; i++) e.MemoryMasks[m.Model.Physical(frame + 20 + (uint)i)] = 0; + if (selected < 8) e.D[selected] = data; else e.A[selected - 8] = data; + e.A[7] = frame; e.Pc = entry + 2; + } + var id = $"{m.Model.Id}/CALLM/byte/{form.Id}/R{general}/opt={option}/type={type}/count={count}/super={supervisor}/status={status}/{index?.Id ?? "brief"}/op={op:X4}/ccr={ccr:X2}"; + SyntheticExecution.Run(m, e, report, id); + if (Supported(m) && actualType == 1 && status >= 0) + { + var check = m.Bus.ModuleRegisters.GetValueOrDefault(0x10004u) == (control >> 16 & 255) && m.Bus.ModuleRegisters.GetValueOrDefault(supervisor ? 0x10054u : 0x10044u) == f.Address; + report.Record(id + "/external-access-request", check ? "passing" : "mismatching", check ? null : "Descriptor/access-level request differs"); + } + } + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void RtmFramesRestoreAllGeneralRegistersConditionsAndArguments(string modelId) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); var report = new CoverageBatch(modelId, "system-rtm"); + for (var general = 0; general < 16; general++) + foreach (var option in new[] { 0, 4 }) + foreach (var type in new[] { 0, 1, 2 }) + foreach (var count in new[] { 0, 1, 15, 255 }) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + m.Reset(ccr, supervisor); var sp = m.Core.State.A[7]; + m.InitializePhysical(sp, (uint)(option << 13 | type << 8 | 0x37), 2); m.InitializePhysical(sp + 2, (uint)(ccr ^ 31), 2); + m.InitializePhysical(sp + 4, (uint)count, 2); m.InitializePhysical(sp + 6, 0, 2); m.InitializePhysical(sp + 8, 0x6200, 4); + m.InitializePhysical(sp + 12, 0x6000, 4); m.InitializePhysical(sp + 16, 0x7200, 4); m.InitializePhysical(sp + 20, 0x7300, 4); + m.InitializePhysical(0x6000, 0x4e71, 2); m.InitializePhysical(0x6002, 0x4e71, 2); + var op = (ushort)(0x06c0 | general); var e = SyntheticExecution.Prepare(m, [op]); + m.Bus.ModuleResponder = true; m.Bus.ModuleRegisters[0x1000c] = 1; + if (!Supported(m)) SyntheticExecution.ExpectException(m, e, 4); + else if (type > 1) SyntheticExecution.ExpectException(m, e, 14); + else + { + if (general < 8) e.D[general] = 0x7200; else e.A[general - 8] = 0x7200; + e.A[7] = (type == 1 || option == 4 ? 0x7300u : sp + 24) + (uint)count; + e.Sr = (ushort)((e.Sr & 0xffe0) | (ccr ^ 31)); e.Pc = 0x6000; + } + SyntheticExecution.Run(m, e, report, $"{modelId}/RTM/none/R{general}/opt={option}/type={type}/count={count}/super={supervisor}/op={op:X4}/ccr={ccr:X2}"); + } + foreach (var supervisor in new[] { false, true }) + foreach (var scenario in new[] { 1, 2, 3, 5, 6, 7, 100, 108, 200 }) + { + m.Reset(31, supervisor); var sp = m.Core.State.A[7]; + var invalidOption = scenario < 8; + var header = invalidOption ? scenario << 13 : 0x0137; + m.InitializePhysical(sp, (uint)header, 2); + m.InitializePhysical(sp + 2, 0, 2); m.InitializePhysical(sp + 4, 2, 2); + m.InitializePhysical(sp + 12, 0x6000, 4); m.InitializePhysical(sp + 16, 0x7200, 4); m.InitializePhysical(sp + 20, 0x7300, 4); + var e = SyntheticExecution.Prepare(m, [0x06c0]); + m.Bus.ModuleResponder = scenario != 200; m.Bus.ModuleRegisters[0x1000c] = scenario == 108 ? 8u : 0; + SyntheticExecution.ExpectException(m, e, Supported(m) ? 14 : 4); + SyntheticExecution.Run(m, e, report, $"{modelId}/RTM/invalid-frame-or-denied/scenario={scenario}/super={supervisor}/op=06C0/ccr=1F"); + } + report.Complete(output); + } +} + diff --git a/Copper68k.Tests/Synthetic/SyntheticSystemTests.cs b/Copper68k.Tests/Synthetic/SyntheticSystemTests.cs new file mode 100644 index 00000000..f9c7af31 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticSystemTests.cs @@ -0,0 +1,191 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticSystemTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void TrapsPrivilegeStopsAndResetPreserveArchitecturalState(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "system-basic"); + for (var ccr = 0; ccr < 32; ccr++) + foreach (var supervisor in new[] { false, true }) + { + for (var trap = 0; trap < 16; trap++) Basic(machine, report, "TRAP", (ushort)(0x4e40 | trap), ccr, supervisor); + foreach (var (family, opcode) in new[] { ("NOP", 0x4e71), ("RESET", 0x4e70), ("ILLEGAL", 0x4afc), ("LineA", 0xa123), ("LineF", 0xf123), ("TRAPV", 0x4e76) }) + Basic(machine, report, family, (ushort)opcode, ccr, supervisor); + foreach (var sr in new ushort[] { 0, 0x0700, 0x2000, 0x2700, 0x271f }) Basic(machine, report, "STOP", 0x4e72, ccr, supervisor, sr); + } + report.Complete(output); + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void StatusTransfersAllAddressingFlagsAndPrivilegeStates(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "system-status"); + foreach (var family in new[] { "MOVEfromSR", "MOVEfromCCR", "MOVEtoSR", "MOVEtoCCR" }) + { + var from = family.StartsWith("MOVEfrom"); + var forms = from ? ArithmeticSpecification.Alterable() : ArithmeticSpecification.Sources(2, false); + foreach (var form in forms) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) Status(machine, report, family, form, ccr, supervisor, 0x0700); + if (machine.Model.FullIndex) + foreach (var index in IndexFixture.FullStructures()) + foreach (var form in from ? new[] { new OperandForm(6, 7) } : new[] { new OperandForm(6, 7), new OperandForm(7, 3) }) + Status(machine, report, family, form, 31, true, 0x200f, index); + foreach (var sr in new uint[] { 0, 1, 0x1f, 0x2000, 0x2700, 0x071f, 0xffff }) + Status(machine, report, family, new(0, 7), 31, true, sr); + } + foreach (var family in new[] { "ORI", "ANDI", "EORI" }) + foreach (var fullSr in new[] { false, true }) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var value in new ushort[] { 0, 1, 0x1f, 0x0700, 0x2000, 0x271f }) + ImmediateStatus(machine, report, family, fullSr, value, ccr, supervisor); + for (var reg = 0; reg < 8; reg++) + foreach (var toUsp in new[] { false, true }) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + machine.Reset(ccr, supervisor); + var opcode = (ushort)(0x4e60 | (toUsp ? 0 : 8) | reg); + var expected = SyntheticExecution.Prepare(machine, [opcode]); + if (!supervisor) SyntheticExecution.ExpectException(machine, expected, 8); + else if (toUsp) expected.InactiveStackPointer = expected.A[reg]; + else expected.A[reg] = expected.InactiveStackPointer!.Value; + SyntheticExecution.Run(machine, expected, report, $"{modelId}/MOVEUSP/L/A{reg}/toUSP={toUsp}/super={supervisor}/op={opcode:X4}/ccr={ccr:X2}"); + } + report.Complete(output); + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void LinkUnlinkAndOrdinaryReturnsWithStackAliases(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "system-stack"); + for (var reg = 0; reg < 8; reg++) + foreach (var width in new[] { 2, 4 }) + foreach (var displacement in new[] { -32768, -16, 0, 16, 32766 }) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + machine.Reset(ccr, supervisor); + var opcode = (ushort)((width == 2 ? 0x4e50 : 0x4808) | reg); + var words = new List { opcode }; + if (width == 4) words.Add((ushort)(displacement >> 16)); words.Add((ushort)displacement); + var expected = SyntheticExecution.Prepare(machine, words); + if (width == 4 && !machine.Model.FullIndex) SyntheticExecution.ExpectException(machine, expected, 4); + else + { + var saved = expected.A[reg]; + expected.A[7] -= 4; + // LINK A7 stores the decremented SP (M68000PM 4-111 operation ordering). + expected.Write(expected.A[7], reg == 7 ? expected.A[7] : saved, 4, machine.Model); + expected.A[reg] = expected.A[7]; expected.A[7] = unchecked(expected.A[7] + (uint)displacement); + } + SyntheticExecution.Run(machine, expected, report, $"{modelId}/LINK/{width}/A{reg}/super={supervisor}/op={opcode:X4}/disp={displacement}/ccr={ccr:X2}"); + } + for (var reg = 0; reg < 8; reg++) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + machine.Reset(ccr, supervisor); + var frame = machine.Core.State.A[reg]; machine.InitializePhysical(frame, 0x6540, 4); + var opcode = (ushort)(0x4e58 | reg); var expected = SyntheticExecution.Prepare(machine, [opcode]); + expected.A[7] = frame + 4; expected.A[reg] = 0x6540; + SyntheticExecution.Run(machine, expected, report, $"{modelId}/UNLK/none/A{reg}/super={supervisor}/op={opcode:X4}/ccr={ccr:X2}"); + } + foreach (var family in new[] { "RTS", "RTR", "RTD" }) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var displacement in family == "RTD" ? new[] { -32768, -16, 0, 16, 32766 } : new[] { 0 }) + { + machine.Reset(ccr, supervisor); + var sp = machine.Core.State.A[7]; + machine.InitializePhysical(sp, family == "RTR" ? (uint)(0xa700 | (ccr ^ 31)) : 0x6000, family == "RTR" ? 2 : 4); + if (family == "RTR") machine.InitializePhysical(sp + 2, 0x6000, 4); + machine.InitializePhysical(0x6000, 0x4e71, 2); machine.InitializePhysical(0x6002, 0x4e71, 2); + var opcode = (ushort)(family == "RTS" ? 0x4e75 : family == "RTR" ? 0x4e77 : 0x4e74); + var expected = SyntheticExecution.Prepare(machine, family == "RTD" ? [opcode, (ushort)displacement] : [opcode]); + if (family == "RTD" && modelId == "68000") SyntheticExecution.ExpectException(machine, expected, 4); + else + { + expected.Pc = 0x6000; + expected.A[7] = unchecked(sp + (family == "RTR" ? 6u : 4u) + (uint)displacement); + if (family == "RTR") expected.Sr = (ushort)((expected.Sr & 0xffe0) | (ccr ^ 31)); + } + SyntheticExecution.Run(machine, expected, report, $"{modelId}/{family}/none/super={supervisor}/op={opcode:X4}/disp={displacement}/ccr={ccr:X2}"); + } + report.Complete(output); + } + + internal static void Basic(SyntheticMachine machine, CoverageBatch report, string family, ushort opcode, int ccr, bool supervisor, ushort immediate = 0) + { + machine.Reset(ccr, supervisor); + var expected = SyntheticExecution.Prepare(machine, family == "STOP" ? [opcode, immediate] : [opcode]); + if (family is "STOP" or "RESET" && !supervisor) SyntheticExecution.ExpectException(machine, expected, 8); + else if (family == "STOP") { ApplyStatus(machine, expected, immediate); expected.Stopped = true; } + else if (family == "RESET") expected.ExpectedDeviceResets = 1; + else if (family == "TRAP") SyntheticExecution.ExpectException(machine, expected, 32 + (opcode & 15), expected.Pc); + else if (family == "TRAPV" && (ccr & 2) != 0) SyntheticExecution.ExpectException(machine, expected, 7, expected.Pc); + else if (family is "ILLEGAL" or "LineA" or "LineF") SyntheticExecution.ExpectException(machine, expected, family == "ILLEGAL" ? 4 : family == "LineA" ? 10 : 11); + SyntheticExecution.Run(machine, expected, report, $"{machine.Model.Id}/{family}/none/super={supervisor}/op={opcode:X4}/imm={immediate:X4}/ccr={ccr:X2}", !expected.Stopped); + } + + internal static void Status(SyntheticMachine machine, CoverageBatch report, string family, OperandForm form, int ccr, bool supervisor, uint value, IndexFixture? index = null) + { + machine.Reset(ccr, supervisor); + var from = family.StartsWith("MOVEfrom"); + var opcode = (ushort)((family switch { "MOVEfromSR" => 0x40c0, "MOVEfromCCR" => 0x42c0, "MOVEtoCCR" => 0x44c0, _ => 0x46c0 }) | form.Mode << 3 | form.Register); + var fixture = new OperandFixture(machine, [opcode], form, 2, value, source: !from, index: index); + var expected = fixture.Expected; + var illegal = family == "MOVEfromCCR" && machine.Model.Id == "68000"; + var privileged = family == "MOVEtoSR" || family == "MOVEfromSR" && machine.Model.Id != "68000"; + if (illegal || privileged && !supervisor) + { + machine.Core.State.A.CopyTo(expected.A, 0); + SyntheticExecution.ExpectException(machine, expected, illegal ? 4 : 8); + if (form.Memory) for (var i = 0; i < 2; i++) expected.ForbiddenOperandReads.Add(machine.Model.Physical(fixture.Address + (uint)i)); + } + else if (from) fixture.Write(family == "MOVEfromSR" ? expected.Sr : (uint)ccr); + else if (family == "MOVEtoCCR") expected.Sr = (ushort)((expected.Sr & 0xffe0) | (int)(fixture.Value & 31)); + else ApplyStatus(machine, expected, (ushort)fixture.Value); + // Trace bits are covered by the explicit trace group; this register mask probe stops at the transfer boundary. + SyntheticExecution.Run(machine, expected, report, $"{machine.Model.Id}/{family}/W/{form.Id}/{index?.Id ?? "brief"}/super={supervisor}/op={opcode:X4}/value={value:X4}/ccr={ccr:X2}", (expected.Sr & 0xc000) == 0); + } + + internal static void ImmediateStatus(SyntheticMachine machine, CoverageBatch report, string family, bool fullSr, ushort immediate, int ccr, bool supervisor) + { + machine.Reset(ccr, supervisor); + var opcode = (ushort)((family == "ORI" ? 0 : family == "ANDI" ? 0x200 : 0xa00) | (fullSr ? 0x7c : 0x3c)); + var expected = SyntheticExecution.Prepare(machine, [opcode, immediate]); + if (fullSr && !supervisor) SyntheticExecution.ExpectException(machine, expected, 8); + else + { + var result = (ushort)(family == "ORI" ? expected.Sr | immediate : family == "ANDI" ? expected.Sr & immediate : expected.Sr ^ immediate); + if (fullSr) ApplyStatus(machine, expected, result); else expected.Sr = (ushort)((expected.Sr & 0xffe0) | (result & 31)); + } + SyntheticExecution.Run(machine, expected, report, $"{machine.Model.Id}/{family}to{(fullSr ? "SR" : "CCR")}/W/immediate/super={supervisor}/op={opcode:X4}/imm={immediate:X4}/ccr={ccr:X2}"); + } + + internal static void ApplyStatus(SyntheticMachine machine, ArchitecturalExpectation expected, ushort value) + { + var mask = machine.Model.FullIndex ? machine.Model.Id == "68060" ? 0xb71f : 0xf71f : 0xa71f; + var previous = expected.Sr; + value &= (ushort)mask; + var previousSupervisor = (previous & 0x2000) != 0; + var nextSupervisor = (value & 0x2000) != 0; + if (previousSupervisor != nextSupervisor) + { + var old = expected.A[7]; expected.A[7] = expected.InactiveStackPointer!.Value; expected.InactiveStackPointer = old; + } + if (machine.Model.FullIndex && machine.Model.Id != "68060" && nextSupervisor && (value & 0x1000) != 0) + expected.A[7] = expected.MasterStackPointer!.Value; + expected.Sr = value; + } +} diff --git a/Copper68k/M68010Interpreter.cs b/Copper68k/M68010Interpreter.cs index 600e4f0c..62525f65 100644 --- a/Copper68k/M68010Interpreter.cs +++ b/Copper68k/M68010Interpreter.cs @@ -158,8 +158,11 @@ internal M68010Interpreter( private static M68010AddressMaskedBus CreateBus(IM68kBus bus) => new(bus); + protected override bool SupportsMoves => true; + protected override bool TryExecuteModelSpecificLine4(ushort opcode, uint instructionPc) { + if (TryExecuteM68010StatusAndReturn(opcode, instructionPc)) return true; if (opcode is not (0x4E7A or 0x4E7B)) { return false; @@ -253,6 +256,9 @@ private bool TryReadControlRegister(int register, uint instructionPc, out uint v case 0x001: value = State.DestinationFunctionCode; return true; + case 0x800: + value = State.UserStackPointer; + return true; case 0x801: value = State.VectorBaseRegister; return true; @@ -273,6 +279,9 @@ private bool TryWriteControlRegister(int register, uint value, uint instructionP case 0x001: State.DestinationFunctionCode = value & 0x7; return true; + case 0x800: + State.SetUserStackPointer(value); + return true; case 0x801: State.VectorBaseRegister = value; return true; diff --git a/Copper68k/M68030Interpreter.cs b/Copper68k/M68030Interpreter.cs index 68c6f307..9c370edf 100644 --- a/Copper68k/M68030Interpreter.cs +++ b/Copper68k/M68030Interpreter.cs @@ -9,7 +9,6 @@ namespace Copper68k { internal sealed class M68030Interpreter : M68kAdvancedTimingInterpreter { - private uint _translationControl; public M68030Interpreter(IM68kBus bus) : this(bus, M68020CpuProfile.Ocs68030Accelerator14Mhz) @@ -41,29 +40,8 @@ internal M68030Interpreter( public override void Reset(uint programCounter, uint stackPointer) { base.Reset(programCounter, stackPointer); - _translationControl = 0; } - protected override bool TryReadControlRegister(int register, uint instructionPc, out uint value) - { - if (register == 0x003) - { - value = _translationControl; - return true; - } - - return base.TryReadControlRegister(register, instructionPc, out value); - } - - protected override bool TryWriteControlRegister(int register, uint value, uint instructionPc) - { - if (register == 0x003) - { - _translationControl = value; - return true; - } - - return base.TryWriteControlRegister(register, value, instructionPc); - } + // TC is transferred by PMOVE on the 030, not MOVEC selector 003. } } diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index f83d56b1..29718827 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -2120,11 +2120,6 @@ private uint Translate(uint address, M68kBusAccessKind accessKind, bool write, i if (!mmu.Enabled) { - if (mmu.Status != 0) - { - mmu.Status = 0; - } - return _allPhysicalAddressesMapped ? address : AcceptPhysicalAddress(address, byteCount, accessKind, write); @@ -2513,6 +2508,7 @@ internal M68040Interpreter( { ExternalInstructionWordReader = profile.FastInstructionFetch ? null : FetchWord }; + _approximateIntegerFallback.ArchitecturalTraceEnabled = false; } public override int ExecuteInstruction() @@ -2601,11 +2597,6 @@ protected override bool TryExecuteModelSpecificInstruction(ushort opcode) return false; } - if (TryExecuteMove16(opcode)) - { - return true; - } - if (TryExecuteMmuInstruction(opcode)) { return true; @@ -2682,8 +2673,9 @@ protected override bool TryReadControlRegister(int register, uint instructionPc, case 0x807: value = State.M68040Mmu.SupervisorRootPointer; return true; + case 0x802: case 0x808: - value = RaiseLineFControlRegister(instructionPc); + value = RaiseIllegalControlRegister(instructionPc); return false; default: return base.TryReadControlRegister(register, instructionPc, out value); @@ -2727,8 +2719,9 @@ protected override bool TryWriteControlRegister(int register, uint value, uint i State.M68040Mmu.SupervisorRootPointer = value; State.M68040Mmu.Flush(); return true; + case 0x802: case 0x808: - _ = RaiseLineFControlRegister(instructionPc); + _ = RaiseIllegalControlRegister(instructionPc); return false; default: return base.TryWriteControlRegister(register, value, instructionPc); @@ -2741,34 +2734,6 @@ private uint RaiseLineFControlRegister(uint instructionPc) return 0; } - private bool TryExecuteMove16(ushort opcode) - { - if ((opcode & 0xFFF8) != 0xF620) - { - return false; - } - - BeginInstruction(opcode); - _ = FetchWord(); - var sourceRegister = opcode & 7; - var extension = FetchWord(); - var destinationRegister = (extension >> 12) & 7; - var source = State.A[sourceRegister] & 0xFFFF_FFF0u; - var destination = State.A[destinationRegister] & 0xFFFF_FFF0u; - for (var offset = 0u; offset < 16; offset += 4) - { - WriteLong(destination + offset, ReadLong(source + offset)); - } - - State.A[sourceRegister] += 16; - if (destinationRegister != sourceRegister) - { - State.A[destinationRegister] += 16; - } - CompleteTiming(M68kInstructionTimingKey.Movec); - return true; - } - private bool TryExecuteMmuInstruction(ushort opcode) { if ((opcode & 0xFFC0) == 0xF500) diff --git a/Copper68k/M68060Interpreter.cs b/Copper68k/M68060Interpreter.cs index 9c600421..33172c0d 100644 --- a/Copper68k/M68060Interpreter.cs +++ b/Copper68k/M68060Interpreter.cs @@ -193,8 +193,7 @@ protected override bool TryWriteControlRegister(int register, uint value, uint p // cache capacity are outside the current execution policy. return base.TryWriteControlRegister(register, value & 0xF880_E000u, pc); case 0x008: - if (value != 0) throw Unavailable("BUSCR bus control", State.LastOpcode); - State.M68060BusControl = 0; return true; + State.M68060BusControl = value & 0xF000_0000; return true; case 0x800: State.SetUserStackPointer(value); return true; case 0x808: State.M68060ProcessorConfiguration = 0x0430_0000 | (value & 0x83); return true; @@ -223,8 +222,8 @@ internal override void RaiseFormat0Exception(int vector, uint pc, M68kInstructio var sr = State.StatusRegister; State.RecordException(vector, pc, sr); State.StatusRegister = (ushort)((sr | M68kCpuState.Supervisor) & ~M68kCpuState.Trace); - if (vector == 5) PushLong(State.LastInstructionProgramCounter); - PushWord((ushort)((vector == 5 ? 0x2000 : 0) | (vector * 4))); + if (vector is 5 or 6 or 7 or 9) PushLong(State.LastInstructionProgramCounter); + PushWord((ushort)((vector is 5 or 6 or 7 or 9 ? 0x2000 : 0) | (vector * 4))); PushLong(pc); PushWord(sr); State.ProgramCounter = ReadLong(State.VectorBaseRegister + (uint)vector * 4); @@ -234,7 +233,7 @@ internal override void RaiseFormat0Exception(int vector, uint pc, M68kInstructio public override void RequestInterrupt(int level, uint vectorAddress) { - if (level <= 0 || level <= ((State.StatusRegister >> 8) & 7)) return; + if (level <= 0 || level != 7 && level <= ((State.StatusRegister >> 8) & 7)) return; var sr = State.StatusRegister; var pc = State.ProgramCounter; State.Stopped = false; diff --git a/Copper68k/M68EC020Interpreter.cs b/Copper68k/M68EC020Interpreter.cs index 93baffd7..6d575e81 100644 --- a/Copper68k/M68EC020Interpreter.cs +++ b/Copper68k/M68EC020Interpreter.cs @@ -8,7 +8,7 @@ namespace Copper68k { - internal class M68EC020AddressMaskedBus : + internal class M68EC020AddressMaskedBus : IM68kModuleAccessBus, IM68kBus, IM68kFastMemoryBus, IM68kPhysicalAddressMap @@ -27,6 +27,16 @@ internal M68EC020AddressMaskedBus(IM68kBus bus) _physicalAddressMap = bus as IM68kPhysicalAddressMap; } + bool IM68kModuleAccessBus.TryReadModuleByte(uint address, out byte value) + { + if (_bus is IM68kModuleAccessBus module) return module.TryReadModuleByte(address, out value); + value = 0; return false; + } + bool IM68kModuleAccessBus.TryWriteModuleByte(uint address, byte value) + => _bus is IM68kModuleAccessBus module && module.TryWriteModuleByte(address, value); + bool IM68kModuleAccessBus.TryWriteModuleLong(uint address, uint value) + => _bus is IM68kModuleAccessBus module && module.TryWriteModuleLong(address, value); + internal static M68EC020AddressMaskedBus Create(IM68kBus bus) => bus is IM68kCodeReader ? new CodeReadableBus(bus) : new M68EC020AddressMaskedBus(bus); diff --git a/Copper68k/M68kAdvancedTimingInterpreter.Logical.cs b/Copper68k/M68kAdvancedTimingInterpreter.Logical.cs new file mode 100644 index 00000000..9f2eddce --- /dev/null +++ b/Copper68k/M68kAdvancedTimingInterpreter.Logical.cs @@ -0,0 +1,115 @@ +/* Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT */ +namespace Copper68k; + +internal partial class M68kAdvancedTimingInterpreter +{ + private static bool IsAdvancedBitIndexedInstruction(ushort opcode) => IsStatusTransfer(opcode) || (opcode & 0xffc0) == 0x06c0 || (opcode & 0xff00) == 0x0e00 || (opcode & 0xf9c0) is 0x00c0 or 0x02c0 or 0x04c0 || (opcode & 0xf140) == 0x4100 || (opcode & 0xf0c0) == 0x50c0 || (opcode & 0xf8c0) == 0xe8c0 || (opcode & 0xffc0) is 0x0ac0 or 0x0cc0 or 0x0ec0; + + private static bool IsGeneralLogical(ushort opcode) + { + var mode = (opcode >> 3) & 7; var reg = opcode & 7; var size = (opcode >> 6) & 3; + var alterable = mode != 1 && (mode != 7 || reg <= 1); + var data = mode != 1 && (mode != 7 || reg <= 4); + var top = opcode >> 12; var opmode = (opcode >> 6) & 7; + if (top is 8 or 12) return opmode < 3 ? data : opmode is >= 4 and <= 6 && mode >= 2 && alterable; + if (top == 11) return opmode is >= 4 and <= 6 && alterable; + if ((opcode & 0xff00) is 0x0000 or 0x0200 or 0x0a00) return size < 3 && alterable; + if ((opcode & 0xffc0) == 0x4ac0) return alterable; + if ((opcode & 0xff00) is 0x4000 or 0x4200 or 0x4400 or 0x4600 or 0x4a00) + return size < 3 && ((opcode & 0xff00) == 0x4a00 ? (mode != 1 || size != 0) && (mode != 7 || reg <= 4) : alterable); + if ((opcode & 0xf100) == 0x0100 || (opcode & 0xff00) == 0x0800) + return ((opcode >> 6) & 3) == 0 ? data && ((opcode & 0x100) != 0 || mode != 7 || reg != 4) : alterable; + return top == 14 && (size != 3 || (opcode & 0x0800) == 0 && mode >= 2 && alterable); + } + + private bool TryExecuteGeneralLogical(ushort opcode) + { + if (!IsGeneralLogical(opcode)) return false; + BeginInstruction(opcode); _ = FetchWord(); + var mode = (opcode >> 3) & 7; var reg = opcode & 7; var dr = (opcode >> 9) & 7; + var top = opcode >> 12; var opmode = (opcode >> 6) & 7; + var bit = (opcode & 0xf100) == 0x0100 || (opcode & 0xff00) == 0x0800; + var shift = top == 14; var memoryShift = shift && (opcode & 0xc0) == 0xc0; + var tas = (opcode & 0xffc0) == 0x4ac0; + var size = bit ? (mode == 0 ? M68kOperandSize.Long : M68kOperandSize.Byte) : + tas ? M68kOperandSize.Byte : memoryShift ? M68kOperandSize.Word : (M68kOperandSize)(1 << ((opcode >> 6) & 3)); + var unary = top == 4; + var immediate = !bit && top == 0; + var store = unary || immediate || bit || memoryShift || !shift && opmode >= 4; + uint argument = bit ? ((opcode & 0x100) != 0 ? State.D[dr] : (uint)FetchWord()) : + immediate ? (size == M68kOperandSize.Long ? FetchLong() : FetchWord()) : State.D[dr]; + var address = 0u; + uint value; + if (mode < 2 || shift && !memoryShift) value = mode == 1 && !shift ? State.A[reg] : State.D[reg]; + else if (mode == 7 && reg == 4) value = size == M68kOperandSize.Long ? FetchLong() : FetchWord(); + else + { + address = ResolveMoveAddress(mode, reg, size, opcode); + // CLR on 020+ performs a write without the 000's destination read. + value = (opcode & 0xff00) == 0x4200 ? 0 : ReadSized(address, size); + if (mode == 3) WriteGeneralRegister(true, reg, unchecked(address + M68kIntegerSemantics.AddressIncrement(reg, size))); + } + uint result; var write = true; + if (bit) + { + var mask = 1u << (int)(argument & (mode == 0 ? 31u : 7u)); + State.SetFlag(M68kCpuState.Zero, (value & mask) == 0); + result = ((opcode >> 6) & 3) switch { 1 => value ^ mask, 2 => value & ~mask, 3 => value | mask, _ => value }; + write = ((opcode >> 6) & 3) != 0; + } + else if (shift) + { + var count = memoryShift ? 1 : (opcode & 0x20) != 0 ? (int)(State.D[dr] & 63) : dr == 0 ? 8 : dr; + var type = memoryShift ? (opcode >> 9) & 3 : (opcode >> 3) & 3; + var shifted = M68kIntegerSemantics.Shift(value, count, size, type, (opcode & 0x100) != 0, State.GetFlag(M68kCpuState.Extend)); + result = shifted.Value; + SetMoveFlags(result, size); + State.SetFlag(M68kCpuState.Overflow, shifted.Overflow); + State.SetFlag(M68kCpuState.Carry, shifted.Carry); + if (shifted.ExtendChanged) State.SetFlag(M68kCpuState.Extend, shifted.Extend); + } + else if (unary) + { + var family = opcode & 0xff00; + result = family switch { 0x4200 => 0, 0x4600 => ~value, 0x4400 => unchecked(0u - value), + 0x4000 => unchecked(0u - value - (State.GetFlag(M68kCpuState.Extend) ? 1u : 0u)), _ => tas ? value | 128u : value }; + result &= M68kCpuState.Mask(size); + if (family == 0x4400) SetSubtractFlags(0, value, result, size); + else if (family == 0x4000) SetSubxFlags(0, value, result, size); + else SetMoveFlags(tas ? value : result, size); + write = tas || family != 0x4a00; + } + else + { + var destination = store ? value : State.D[dr]; var source = store ? argument : value; + var operation = immediate ? opcode & 0xff00 : top == 12 ? 0x0200 : top == 11 ? 0x0a00 : 0; + result = operation switch { 0x0200 => destination & source, 0x0a00 => destination ^ source, _ => destination | source }; + SetMoveFlags(result, size); + } + if (write) + { + if (shift && !memoryShift || store && mode == 0) WriteDataRegisterSized(reg, result, size); + else if (!store) WriteDataRegisterSized(dr, result, size); + else WriteSized(address, result, size); + } + CompleteGeneralLogicalTiming(mode, reg, write && store && mode >= 2 && !(shift && !memoryShift)); + return true; + } + + private void CompleteGeneralLogicalTiming(int mode, int reg, bool readModifyWrite) + { + var cycles = _profile.FixedInstructionNativeCycles ?? 4 + MoveEaPolicyCycles(mode, reg) + _indexedOperandExtraCycles; + var barriers = readModifyWrite ? M68kTimingBarrier.ReadModifyWrite : M68kTimingBarrier.None; + CompleteTimingPlan(_profile.Model == M68kAcceleratorModel.M68030 + ? M68kInstructionPlan.CreateHeadTail(M68kInstructionTimingKey.GeneralLogical, "integer logical general EA", cycles, 2, 0, barriers) + : M68kInstructionPlan.CreateFlat(M68kInstructionTimingKey.GeneralLogical, "integer logical general EA", cycles, barriers)); + } + + private void CompleteAtomicTiming() + { + var plan = _timing.GetPlan(M68kInstructionTimingKey.Nop); + plan = plan with { NativeCycles = _profile.FixedInstructionNativeCycles ?? plan.NativeCycles + _indexedOperandExtraCycles, + Barriers = plan.Barriers | M68kTimingBarrier.ReadModifyWrite }; + CompleteTimingPlan(plan); + } +} diff --git a/Copper68k/M68kAdvancedTimingInterpreter.Module.cs b/Copper68k/M68kAdvancedTimingInterpreter.Module.cs new file mode 100644 index 00000000..cbf8df0a --- /dev/null +++ b/Copper68k/M68kAdvancedTimingInterpreter.Module.cs @@ -0,0 +1,85 @@ +/* Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT */ +namespace Copper68k; + +// CPU-space module access-control registers are distinct from normal memory. +// A flat IM68kBus has no external access-control responder: type-1 calls then +// take the documented format error. This optional internal hook qualifies the +// integer module protocol without extending the public package API. +internal interface IM68kModuleAccessBus +{ + bool TryReadModuleByte(uint address, out byte value); + bool TryWriteModuleByte(uint address, byte value); + bool TryWriteModuleLong(uint address, uint value); +} + +internal partial class M68kAdvancedTimingInterpreter +{ + private bool TryExecuteModule(ushort opcode) + { + if ((opcode & 0xffc0) != 0x06c0) return false; + BeginInstruction(opcode); _ = FetchWord(); + var pc = State.LastInstructionProgramCounter; + if (_profile.Model != M68kAcceleratorModel.M68020) + { RaiseFormat0Exception(4, pc, M68kInstructionTimingKey.IllegalInstruction); return true; } + var mode = (opcode >> 3) & 7; var reg = opcode & 7; + if (mode < 2) + { + var sp = State.A[7]; var header = ReadWord(sp); var opt = header >> 13; var type = (header >> 8) & 31; + if (opt is not (0 or 4) || type > 1) { ModuleFormatError(pc); return true; } + var ccr = ReadWord(sp + 2); var count = ReadWord(sp + 4) & 255; + var nextPc = ReadLong(sp + 12); var pointer = ReadLong(sp + 16); + var nextSp = type == 1 || opt == 4 ? ReadLong(sp + 20) : sp + 24; + if (type == 1) + { + if (_bus is not IM68kModuleAccessBus access || !access.TryWriteModuleByte(0x10008, (byte)header) || + !access.TryReadModuleByte(0x1000c, out var status) || status is 0 or > 7) + { ModuleFormatError(pc); return true; } + } + WriteGeneralRegister(mode == 1, reg, pointer); + State.SetActiveStackPointer(unchecked(nextSp + (uint)count)); + State.StatusRegister = (ushort)((State.StatusRegister & 0xffe0) | (ccr & 31)); + State.ProgramCounter = nextPc; + CompleteTiming(M68kInstructionTimingKey.Rtd); return true; + } + if (mode is not (2 or 5 or 6) && !(mode == 7 && reg <= 3)) + { RaiseFormat0Exception(4, pc, M68kInstructionTimingKey.IllegalInstruction); return true; } + var argumentCount = FetchWord() & 255; + var descriptor = ResolveMoveAddress(mode, reg, M68kOperandSize.Long, opcode); + var control = ReadLong(descriptor); var option = control >> 29; var descriptorType = (control >> 24) & 31; + if (option is not (0 or 4) || descriptorType > 1 || (control & 0xffff) != 0) + { ModuleFormatError(pc); return true; } + var entry = ReadLong(descriptor + 4); var data = ReadLong(descriptor + 8); + var entryWord = ReadWord(entry); var general = (entryWord >> 12) & 7; var addressRegister = (entryWord & 0x8000) != 0; + var oldSp = State.A[7]; var frameTop = oldSp; byte savedAccess = 0; + if (descriptorType == 1) + { + if (_bus is not IM68kModuleAccessBus access || !access.TryReadModuleByte(0x10000, out savedAccess)) + { ModuleFormatError(pc); return true; } + _ = ReadByte(oldSp); + var functionCode = State.GetFlag(M68kCpuState.Supervisor) ? 5u : 1u; + if (!access.TryWriteModuleLong(0x10040 + functionCode * 4, descriptor) || + !access.TryWriteModuleByte(0x10004, (byte)(control >> 16)) || + !access.TryReadModuleByte(0x1000c, out var status) || status is 0 or > 7) + { ModuleFormatError(pc); return true; } + if (status >= 4) + { + frameTop = ReadLong(descriptor + 12); + if (option == 0) + { + frameTop = unchecked(frameTop - (uint)argumentCount); + for (var i = 0; i < argumentCount; i++) WriteByte(frameTop + (uint)i, ReadByte(oldSp + (uint)i)); + } + } + } + var savedData = ReadGeneralRegister(addressRegister, general); + var frame = unchecked(frameTop - 24); + WriteWord(frame, (ushort)((control >> 16 & 0xff00) | savedAccess)); + WriteWord(frame + 2, (ushort)(State.StatusRegister & 31)); WriteWord(frame + 4, (ushort)argumentCount); + WriteWord(frame + 6, 0); WriteLong(frame + 8, descriptor); WriteLong(frame + 12, State.ProgramCounter); + WriteLong(frame + 16, savedData); WriteLong(frame + 20, descriptorType == 1 || option == 4 ? oldSp : 0); + WriteGeneralRegister(addressRegister, general, data); + State.SetActiveStackPointer(frame); State.ProgramCounter = entry + 2; + CompleteGeneralLogicalTiming(mode, reg, false); return true; + } + private void ModuleFormatError(uint pc) => RaiseFormat0Exception(14, pc, M68kInstructionTimingKey.FormatError); +} diff --git a/Copper68k/M68kAdvancedTimingInterpreter.System.cs b/Copper68k/M68kAdvancedTimingInterpreter.System.cs new file mode 100644 index 00000000..747d6ccd --- /dev/null +++ b/Copper68k/M68kAdvancedTimingInterpreter.System.cs @@ -0,0 +1,163 @@ +/* Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT */ +using System; +namespace Copper68k; + +internal partial class M68kAdvancedTimingInterpreter +{ + private bool TryExecuteExtendedSystem(ushort opcode) + { + if ((opcode & 0xffc0) != 0xf600 && opcode != 0xf800) return false; + BeginInstruction(opcode); _ = FetchWord(); + var pc = State.LastInstructionProgramCounter; + if (opcode == 0xf800) + { + if (_profile.Model != M68kAcceleratorModel.M68060) + { RaiseFormat0Exception(11, pc, M68kInstructionTimingKey.LineFException); return true; } + if (!State.GetFlag(M68kCpuState.Supervisor)) + { RaiseFormat0Exception(8, pc, M68kInstructionTimingKey.PrivilegeViolation); return true; } + if (FetchWord() != 0x01c0) + { RaiseFormat0Exception(4, pc, M68kInstructionTimingKey.IllegalInstruction); return true; } + var immediate = FetchWord(); + if ((immediate & M68kCpuState.Supervisor) == 0) + { RaiseFormat0Exception(8, pc, M68kInstructionTimingKey.PrivilegeViolation); return true; } + State.StatusRegister = immediate; State.Stopped = true; + CompleteTiming(M68kInstructionTimingKey.Stop); + return true; + } + if (_profile.Model is not (M68kAcceleratorModel.M68040 or M68kAcceleratorModel.M68060)) + { RaiseFormat0Exception(11, pc, M68kInstructionTimingKey.LineFException); return true; } + var register = opcode & 7; var form = (opcode >> 3) & 7; + uint source, destination; var other = -1; + if (form == 4) + { + var extension = FetchWord(); + if ((extension & 0x8fff) != 0x8000) + { RaiseFormat0Exception(4, pc, M68kInstructionTimingKey.IllegalInstruction); return true; } + other = (extension >> 12) & 7; source = State.A[register]; destination = State.A[other]; + } + else if (form < 4) + { + var absolute = FetchLong(); + source = (form & 1) == 0 ? State.A[register] : absolute; + destination = (form & 1) == 0 ? absolute : State.A[register]; + } + else { RaiseFormat0Exception(4, pc, M68kInstructionTimingKey.IllegalInstruction); return true; } + source &= 0xfffffff0; destination &= 0xfffffff0; + Span line = stackalloc uint[4]; + for (var i = 0; i < 4; i++) line[i] = ReadLong(source + (uint)i * 4); + for (var i = 0; i < 4; i++) WriteLong(destination + (uint)i * 4, line[i]); + if (form == 4 || form < 2) WriteGeneralRegister(true, register, unchecked(State.A[register] + 16)); + if (other >= 0 && other != register) WriteGeneralRegister(true, other, unchecked(State.A[other] + 16)); + CompleteTiming(M68kInstructionTimingKey.Movec); + return true; + } + + private static bool IsStatusTransfer(ushort opcode) => (opcode & 0xffc0) is 0x40c0 or 0x42c0 or 0x44c0 or 0x46c0; + + // This path handles forms absent from the existing timing dispatch before + // the 040's 000 fallback can apply the earlier model's privilege rules. + private bool TryExecuteGeneralSystem(ushort opcode) + { + if ((opcode & 0xfff8) == 0x4848) + { + BeginInstruction(opcode); _ = FetchWord(); + // No breakpoint replacement device is exposed by IM68kBus. + RaiseFormat0Exception(4, State.LastInstructionProgramCounter, M68kInstructionTimingKey.IllegalInstruction); + return true; + } + if ((opcode & 0xff00) == 0x0e00) + { + var m = (opcode >> 3) & 7; var r = opcode & 7; var field = (opcode >> 6) & 3; + if (field == 3 || m < 2 || m == 7 && r > 1) return false; + BeginInstruction(opcode); _ = FetchWord(); + if (!State.GetFlag(M68kCpuState.Supervisor)) + { RaiseFormat0Exception(8, State.LastInstructionProgramCounter, M68kInstructionTimingKey.PrivilegeViolation); return true; } + var ext = FetchWord(); var general = (ext >> 12) & 7; + var ar = (ext & 0x8000) != 0; var store = (ext & 0x800) != 0; + var size = (M68kOperandSize)(1 << field); + var address = ResolveMoveAddress(m, r, size, opcode); + if (m == 3) WriteGeneralRegister(true, r, unchecked(address + M68kIntegerSemantics.AddressIncrement(r, size))); + if (store) WriteSized(address, ReadGeneralRegister(ar, general), size); + else + { + var value = ReadSized(address, size); + if (ar) WriteGeneralRegister(true, general, M68kCpuState.SignExtend(value, size)); + else WriteDataRegisterSized(general, value, size); + } + CompleteGeneralLogicalTiming(m, r, false); + return true; + } + if (opcode is 0x4e76 or 0x4e77) + { + BeginInstruction(opcode); _ = FetchWord(); + if (opcode == 0x4e76) + { + if (State.GetFlag(M68kCpuState.Overflow)) + { + RaiseFormat0Exception(7, State.ProgramCounter, M68kInstructionTimingKey.Nop); + return true; + } + } + else + { + var ccr = ReadWord(State.A[7]); State.SetActiveStackPointer(State.A[7] + 2); var pc = PullLong(); + State.StatusRegister = (ushort)((State.StatusRegister & 0xffe0) | (ccr & 31)); + State.ProgramCounter = pc; + } + CompleteTiming(M68kInstructionTimingKey.Nop); + return true; + } + if ((opcode & 0xf140) == 0x4100) + { + var checkMode = (opcode >> 3) & 7; var checkReg = opcode & 7; + if (checkMode == 1 || checkMode == 7 && checkReg > 4) return false; + BeginInstruction(opcode); _ = FetchWord(); + var size = (opcode & 0x80) != 0 ? M68kOperandSize.Word : M68kOperandSize.Long; + uint bound; + if (checkMode == 0) bound = State.D[checkReg]; + else if (checkMode == 7 && checkReg == 4) bound = size == M68kOperandSize.Long ? FetchLong() : FetchWord(); + else + { + var address = ResolveMoveAddress(checkMode, checkReg, size, opcode); bound = ReadSized(address, size); + if (checkMode == 3) WriteGeneralRegister(true, checkReg, address + (uint)size); + } + var value = SignExtendForSize(State.D[(opcode >> 9) & 7], size); + if (value < 0 || value > SignExtendForSize(bound, size)) + { + State.SetFlag(M68kCpuState.Negative, value < 0); + RaiseFormat0Exception(6, State.ProgramCounter, M68kInstructionTimingKey.IllegalInstruction); + } + else CompleteGeneralLogicalTiming(checkMode, checkReg, false); + return true; + } + if (!IsStatusTransfer(opcode)) return false; + var mode = (opcode >> 3) & 7; var reg = opcode & 7; + var family = opcode & 0xffc0; + var from = family is 0x40c0 or 0x42c0; + if (mode == 1 || mode == 7 && reg > (from ? 1 : 4)) return false; + BeginInstruction(opcode); _ = FetchWord(); + if (family is 0x40c0 or 0x46c0 && !State.GetFlag(M68kCpuState.Supervisor)) + { + RaiseFormat0Exception(8, State.LastInstructionProgramCounter, M68kInstructionTimingKey.PrivilegeViolation); + return true; + } + if (from) + WriteWordDestination(mode, reg, (ushort)(family == 0x40c0 ? State.StatusRegister : State.StatusRegister & 31), opcode); + else + { + ushort value; + if (mode == 0) value = (ushort)State.D[reg]; + else if (mode == 7 && reg == 4) value = FetchWord(); + else + { + var address = ResolveMoveAddress(mode, reg, M68kOperandSize.Word, opcode); + value = ReadWord(address); + if (mode == 3) WriteGeneralRegister(true, reg, unchecked(address + 2)); + } + State.StatusRegister = family == 0x46c0 ? value : (ushort)((State.StatusRegister & 0xffe0) | (value & 31)); + } + CompleteGeneralLogicalTiming(mode, reg, false); + return true; + } +} + diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index 55e2a88b..3fb474f4 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -4366,6 +4366,31 @@ public virtual int ExecuteInstruction() } private void ExecuteInstructionCore() + { + if ((State.StatusRegister & 0xC000) == 0 || State.Halted || State.Stopped) + { ExecuteInstructionBody(); return; } + var trace = State.StatusRegister & 0xC000; + var serial = State.ExceptionSequence; + var flow = false; + if (TryPeekOpcode(State.ProgramCounter, out var traceOpcode)) + { + flow = (traceOpcode >> 12) == 6 && (((traceOpcode >> 8) & 15) < 2 || CheckCondition((traceOpcode >> 8) & 15)) || + (traceOpcode & 0xF0F8) == 0x50C8 && !CheckCondition((traceOpcode >> 8) & 15) && (State.D[traceOpcode & 7] & 0xFFFF) != 0 || + (traceOpcode & 0xFFC0) is 0x4E80 or 0x4EC0 or 0x46C0 || + traceOpcode is 0x4E72 or 0x4E73 or 0x4E74 or 0x4E75 or 0x4E77 or 0x007C or 0x027C or 0x0A7C || + (traceOpcode & 0xFFC0) == 0x06C0; + } + ExecuteInstructionBody(); + var exception = State.ExceptionSequence != serial; + if (exception && State.LastExceptionVector is not (5 or 6 or 7 or >= 32 and <= 47)) return; + if ((trace & 0x8000) != 0 || flow || exception) + { + State.Stopped = false; + RaiseFormat0Exception(9, State.ProgramCounter, M68kInstructionTimingKey.IllegalInstruction); + } + } + + private void ExecuteInstructionBody() { if (State.Halted || State.Stopped) { @@ -4386,7 +4411,7 @@ private void ExecuteInstructionCore() throw new UnsupportedM68kTimingException(0, State.ProgramCounter, _profile); } - if (TryExecutePackUnpack(opcode)) return; + if (TryExecuteModule(opcode) || TryExecuteExtendedSystem(opcode) || TryExecutePackUnpack(opcode)) return; if (TryExecuteFastInstruction(opcode)) { @@ -4404,12 +4429,17 @@ private void ExecuteInstructionCore() if ((opcode & 0xff00) == 0x0c00 && ((opcode >> 6) & 3) != 3 && ((opcode >> 3) & 7) == 7 && (opcode & 7) is 2 or 3 && TryExecuteGeneralArithmetic(opcode)) return; + // TST gained An, PC-relative and immediate sources on 020; the 040 fallback is a 000 decoder. + if ((opcode & 0xff00) == 0x4a00 && (((opcode >> 3) & 7) == 1 || ((opcode >> 3) & 7) == 7 && (opcode & 7) >= 2) && TryExecuteGeneralLogical(opcode)) return; + + if (TryExecuteGeneralSystem(opcode)) return; + if (TryExecuteApproximateInstruction(opcode)) { return; } - if (TryExecuteGeneralExtendMemory(opcode) || TryExecuteGeneralArithmetic(opcode)) return; + if (TryExecuteGeneralExtendMemory(opcode) || TryExecuteGeneralArithmetic(opcode) || TryExecuteGeneralLogical(opcode)) return; if (TryExecuteGeneralMove(opcode) || TryExecuteGeneralMovem(opcode)) { @@ -4431,6 +4461,7 @@ private bool TryExecuteHotBlock( if (maxInstructions <= 0 || State.Halted || State.Stopped || + (State.StatusRegister & 0xC000) != 0 || (State.ProgramCounter & 1) != 0 || !TryGetHotBlock(State.ProgramCounter, out var cacheSlot, out var block)) { @@ -4491,6 +4522,15 @@ private bool TryExecuteHotBlock( return true; } + if ((State.StatusRegister & 0xC000) != 0) + { + var tracedCycle = State.Cycles; + ExecuteInstructionCore(); + boundary.AfterInstruction(tracedCycle, State.Cycles); + executedInstructions++; + return true; + } + var previousCycle = State.Cycles; if (!TryFetchHotOpcode(in hotInstruction, out var opcode) || opcode != hotInstruction.Opcode) @@ -4505,7 +4545,7 @@ private bool TryExecuteHotBlock( ExecuteHotInstruction(hotInstruction.Kind, opcode); boundary.AfterInstruction(previousCycle, State.Cycles); executedInstructions++; - if (State.Halted || State.Stopped) + if (State.Halted || State.Stopped || (State.StatusRegister & 0xC000) != 0) { return true; } @@ -4570,6 +4610,15 @@ private bool TryExecuteModelSpecificHotBlock( return true; } + if ((State.StatusRegister & 0xC000) != 0) + { + var tracedCycle = State.Cycles; + ExecuteInstructionCore(); + boundary.AfterInstruction(tracedCycle, State.Cycles); + executedInstructions++; + return true; + } + var previousCycle = State.Cycles; if (!TryFetchHotOpcode(in hotInstruction, out var opcode) || opcode != hotInstruction.Opcode) @@ -4595,7 +4644,7 @@ private bool TryExecuteModelSpecificHotBlock( boundary.AfterInstruction(previousCycle, State.Cycles); executedInstructions++; - if (State.Halted || State.Stopped) + if (State.Halted || State.Stopped || (State.StatusRegister & 0xC000) != 0) { return true; } @@ -4697,6 +4746,15 @@ private bool TryExecuteShortSelfBranchBlock( return true; } + if ((State.StatusRegister & 0xC000) != 0) + { + var tracedCycle = State.Cycles; + ExecuteInstructionCore(); + boundary.AfterInstruction(tracedCycle, State.Cycles); + executedInstructions++; + return true; + } + var previousCycle = State.Cycles; var opcode = _timedBus.ReadInstructionFetchWordHot( hotInstruction.Address, @@ -4742,7 +4800,7 @@ private bool TryExecuteShortSelfBranchBlock( _instructionPipe.Reset(State.ProgramCounter); boundary.AfterInstruction(previousCycle, State.Cycles); executedInstructions++; - if (State.Halted || State.Stopped) + if (State.Halted || State.Stopped || (State.StatusRegister & 0xC000) != 0) { return true; } @@ -5421,7 +5479,7 @@ public virtual void RequestInterrupt(int level, uint vectorAddress) } var mask = (State.StatusRegister >> 8) & 0x07; - if (level <= mask) + if (level <= mask && level != 7) { return; } @@ -5451,13 +5509,13 @@ public virtual void RequestInterrupt(int level, uint vectorAddress) (ushort)(Format0ExceptionFrame | ((int)vectorAddress & 0x0FFF))); State.SetMasterStackPointer(masterStackPointer); - State.StatusRegister = (ushort)((savedStatusRegister & 0xF8FF) | ((level & 7) << 8) | M68kCpuState.Supervisor); + State.StatusRegister = (ushort)((savedStatusRegister & 0x28FF) | ((level & 7) << 8) | M68kCpuState.Supervisor); State.ProgramCounter = vectorTarget; CompleteTiming(M68kInstructionTimingKey.InterruptAcknowledge); return; } - State.StatusRegister = (ushort)((State.StatusRegister & 0xE8FF) | ((level & 7) << 8) | M68kCpuState.Supervisor); + State.StatusRegister = (ushort)((State.StatusRegister & 0x28FF) | ((level & 7) << 8) | M68kCpuState.Supervisor); PushWord((ushort)(Format0ExceptionFrame | ((int)vectorAddress & 0x0FFF))); PushLong(State.ProgramCounter); PushWord(savedStatusRegister); @@ -8407,19 +8465,21 @@ private void ExecuteRte() return; } - var framePointer = State.A[7]; - var restoredStatus = ReadWord(framePointer); - var restoredPc = ReadLong(framePointer + 2); - var format = ReadWord(framePointer + 6); - if ((format & 0xF000) != Format0ExceptionFrame && (format & 0xF000) != 0x2000) + while (true) { - RaiseFormat0Exception(14, instructionPc, M68kInstructionTimingKey.FormatError); - return; + var framePointer = State.A[7]; + var restoredStatus = ReadWord(framePointer); + var restoredPc = ReadLong(framePointer + 2); + var format = ReadWord(framePointer + 6) >> 12; + var size = format switch { 0 or 1 => 8u, 2 => 12u, 3 when _profile.Model == M68kAcceleratorModel.M68040 => 12u, _ => 0u }; + if (size == 0) + { RaiseFormat0Exception(14, instructionPc, M68kInstructionTimingKey.FormatError); return; } + State.SetActiveStackPointer(framePointer + size); + State.StatusRegister = restoredStatus; + if (format == 1) continue; // Throwaway frame can select another stack. + State.ProgramCounter = restoredPc; + break; } - - State.SetActiveStackPointer(framePointer + ((format & 0xf000) == 0x2000 ? 12u : 8u)); - State.ProgramCounter = restoredPc; - State.StatusRegister = restoredStatus; CompleteTiming(M68kInstructionTimingKey.Rte); } @@ -8652,7 +8712,7 @@ private void ExecuteLinkLong(ushort opcode) _ = FetchWord(); var register = opcode & 7; var displacement = unchecked((int)FetchLong()); - PushLong(State.A[register]); + PushLong(register == 7 ? State.A[7] - 4 : State.A[register]); State.A[register] = State.A[7]; State.SetActiveStackPointer(State.A[7] + unchecked((uint)displacement)); CompleteTiming(M68kInstructionTimingKey.LinkLong); @@ -8664,7 +8724,7 @@ private void ExecuteLinkWord(ushort opcode) _ = FetchWord(); var register = opcode & 7; var displacement = unchecked((short)FetchWord()); - PushLong(State.A[register]); + PushLong(register == 7 ? State.A[7] - 4 : State.A[register]); State.A[register] = State.A[7]; State.SetActiveStackPointer(State.A[7] + unchecked((uint)displacement)); CompleteTiming(M68kInstructionTimingKey.LinkLong); @@ -8677,7 +8737,7 @@ private void ExecuteUnlink(ushort opcode) var register = opcode & 7; State.SetActiveStackPointer(State.A[register]); var restoredRegister = PullLong(); - State.A[register] = restoredRegister; + WriteGeneralRegister(true, register, restoredRegister); CompleteTiming(M68kInstructionTimingKey.LinkLong); } @@ -9411,8 +9471,8 @@ private void ExecuteClrBytePostIncrement(ushort opcode) var register = opcode & 7; var address = State.A[register]; WriteByte(address, 0); - State.A[register] = unchecked( - address + M68kIntegerSemantics.AddressIncrement(register, M68kOperandSize.Byte)); + WriteGeneralRegister(true, register, unchecked( + address + M68kIntegerSemantics.AddressIncrement(register, M68kOperandSize.Byte))); SetMoveFlags(0, M68kOperandSize.Byte); CompleteTiming(M68kInstructionTimingKey.ClrBytePostIncrement); } @@ -9476,7 +9536,7 @@ private void ExecuteClrLongPostIncrement(ushort opcode) var register = opcode & 7; var address = State.A[register]; WriteLong(address, 0); - State.A[register] = address + 4; + WriteGeneralRegister(true, register, address + 4); State.SetNegativeZero(0, M68kOperandSize.Long); State.SetFlag(M68kCpuState.Overflow, false); State.SetFlag(M68kCpuState.Carry, false); @@ -14445,15 +14505,19 @@ private void ExecuteCas(ushort opcode) _ = FetchWord(); var extension = FetchWord(); var size = DecodeCasSize(opcode); - var address = CalculateBitFieldBaseAddress((opcode >> 3) & 7, opcode & 7, opcode); + var mode = (opcode >> 3) & 7; var reg = opcode & 7; + var originalAddressRegister = State.A[reg]; + var address = ResolveMoveAddress(mode, reg, size, opcode); var compareRegister = extension & 7; if (_profile.Model == M68kAcceleratorModel.M68060 && (address & ((uint)size - 1)) != 0) { + if (mode == 4) WriteGeneralRegister(true, reg, originalAddressRegister); RaiseFormat0Exception(61, State.LastInstructionProgramCounter, M68kInstructionTimingKey.IllegalInstruction); return; } var updateRegister = (extension >> 6) & 7; var destination = ReadSized(address, size); + if (mode == 3) WriteGeneralRegister(true, reg, unchecked(address + M68kIntegerSemantics.AddressIncrement(reg, size))); var compare = State.D[compareRegister] & M68kCpuState.Mask(size); SetCompareFlagsPreserveExtend(destination, compare, size); @@ -14464,9 +14528,11 @@ private void ExecuteCas(ushort opcode) else { WriteDataRegisterSized(compareRegister, destination, size); + // 040/060 terminate a failed locked comparison by writing back the value read. + if (_profile.Model is M68kAcceleratorModel.M68040 or M68kAcceleratorModel.M68060) WriteSized(address, destination, size); } - CompleteTiming(M68kInstructionTimingKey.Nop); + CompleteAtomicTiming(); } private void ExecuteCas2(ushort opcode) @@ -14499,11 +14565,12 @@ private void ExecuteCas2(ushort opcode) destination1 == compare1 ? destination2 : destination1, destination1 == compare1 ? compare2 : compare1, size); - WriteDataRegisterSized(compareRegister1, destination1, size); WriteDataRegisterSized(compareRegister2, destination2, size); + WriteDataRegisterSized(compareRegister1, destination1, size); + if (_profile.Model == M68kAcceleratorModel.M68040) WriteSized(address1, destination1, size); } - CompleteTiming(M68kInstructionTimingKey.Nop); + CompleteAtomicTiming(); } private static M68kOperandSize DecodeCasSize(ushort opcode) @@ -14587,14 +14654,13 @@ private void ExecuteChk2Cmp2(ushort opcode) var trapOnOutOfRange = (extension & 0x0800) != 0; var lower = ReadSignedSized(address, size); var upper = ReadSignedSized(address + (uint)size, size); - var value = SignExtendForSize( - useAddressRegister ? State.A[register] : State.D[register], - size); + var value = useAddressRegister ? unchecked((int)State.A[register]) : SignExtendForSize(State.D[register], size); var outOfRange = lower <= upper ? value < lower || value > upper : value > upper && value < lower; State.SetFlag(M68kCpuState.Carry, outOfRange); + State.SetFlag(M68kCpuState.Zero, value == lower || value == upper); if (trapOnOutOfRange && outOfRange) { RaiseFormat0Exception(6, State.ProgramCounter, M68kInstructionTimingKey.IllegalInstruction); @@ -14847,7 +14913,7 @@ private uint CalculateBitFieldBaseAddress(int mode, int register, ushort opcode) } private static int FloorDivideBy8(int value) - => value >= 0 ? value / 8 : -((7 - value) / 8); + => value >> 3; private static uint BitFieldMask(int width) => width == 32 ? 0xFFFF_FFFFu : (1u << width) - 1u; @@ -14920,6 +14986,8 @@ private void SetBitFieldFlags(uint field, int width) { State.SetFlag(M68kCpuState.Negative, (field & (1u << (width - 1))) != 0); State.SetFlag(M68kCpuState.Zero, field == 0); + State.SetFlag(M68kCpuState.Overflow, false); + State.SetFlag(M68kCpuState.Carry, false); } private static int FindFirstSetBitOffset(uint field, int width) @@ -15752,8 +15820,8 @@ private void ExecuteNotMemory(ushort opcode, bool addressIndirect) WriteSized(address, result, size); SetMoveFlags(result, size); var postIncrement = (opcode & 0x38) == 0x18; - if (postIncrement) State.A[opcode & 7] = unchecked(address + - (size == M68kOperandSize.Byte && (opcode & 7) == 7 ? 2u : (uint)size)); + if (postIncrement) WriteGeneralRegister(true, opcode & 7, unchecked(address + + (size == M68kOperandSize.Byte && (opcode & 7) == 7 ? 2u : (uint)size))); CompleteTiming(postIncrement ? size switch { M68kOperandSize.Byte => M68kInstructionTimingKey.NotBytePostIncrement, @@ -16815,7 +16883,7 @@ private void ExecuteTstBytePostIncrement(ushort opcode) _ = FetchWord(); var register = opcode & 7; var value = ReadByte(State.A[register]); - State.A[register] += M68kIntegerSemantics.AddressIncrement(register, M68kOperandSize.Byte); + WriteGeneralRegister(true, register, unchecked(State.A[register] + M68kIntegerSemantics.AddressIncrement(register, M68kOperandSize.Byte))); State.SetNegativeZero(value, M68kOperandSize.Byte); State.SetFlag(M68kCpuState.Overflow, false); State.SetFlag(M68kCpuState.Carry, false); @@ -18257,13 +18325,13 @@ protected void BeginInstruction(ushort opcode) internal virtual void RaiseFormat0Exception(int vector, uint stackedProgramCounter, M68kInstructionTimingKey timingKey) { - // Zero divide is the format-2 exception in this integer slice; - // other callers retain their existing format-0 frame policy. + // MC68020UM table 6-5: completed CHK/divide/TRAPcc/TRAPV/trace + // exceptions retain the instruction address in a format-2 frame. var savedStatusRegister = State.StatusRegister; State.RecordException(vector, stackedProgramCounter, savedStatusRegister); - State.StatusRegister = (ushort)((State.StatusRegister | M68kCpuState.Supervisor) & ~M68kCpuState.Master); - if (vector == 5) PushLong(State.LastInstructionProgramCounter); - PushWord((ushort)((vector == 5 ? 0x2000 : Format0ExceptionFrame) | ((vector * 4) & 0x0FFF))); + State.StatusRegister = (ushort)((State.StatusRegister | M68kCpuState.Supervisor) & ~0xc000); + if (vector is 5 or 6 or 7 or 9) PushLong(State.LastInstructionProgramCounter); + PushWord((ushort)((vector is 5 or 6 or 7 or 9 ? 0x2000 : Format0ExceptionFrame) | ((vector * 4) & 0x0FFF))); PushLong(stackedProgramCounter); PushWord(savedStatusRegister); State.ProgramCounter = ReadLong(State.VectorBaseRegister + ((uint)vector * 4)); @@ -18333,6 +18401,9 @@ protected virtual bool TryReadControlRegister(int register, uint instructionPc, case 0x002: value = State.CacheControlRegister; return true; + case 0x800: + value = State.UserStackPointer; + return true; case 0x801: value = State.VectorBaseRegister; return true; @@ -18362,9 +18433,15 @@ protected virtual bool TryWriteControlRegister(int register, uint value, uint in State.DestinationFunctionCode = value & 0x7; return true; case 0x002: - State.CacheControlRegister = value; + if (_profile.Model is M68kAcceleratorModel.M68020 or M68kAcceleratorModel.M68030) + value &= _profile.Model == M68kAcceleratorModel.M68030 ? 0x3F1Fu : 0xFu; _timedBus.ResetInstructionFetchBuffer(); - _timing.ApplyCacheControl(State.CacheControlRegister, State.CacheAddressRegister); + _timing.ApplyCacheControl(value, State.CacheAddressRegister); + State.CacheControlRegister = _profile.Model is M68kAcceleratorModel.M68020 or M68kAcceleratorModel.M68030 + ? value & ~0x0C0Cu : value; // Clear commands always read as zero. + return true; + case 0x800: + State.SetUserStackPointer(value); return true; case 0x801: State.VectorBaseRegister = value; @@ -18944,7 +19021,7 @@ private uint CalculateIndexedOperandAddress(uint baseAddress, ushort extension, if (baseSize == 0 || (extension & 8) != 0 || indirect == 4 || (suppressIndex && indirect >= 4)) throw new UnsupportedM68kTimingException(opcode, State.LastInstructionProgramCounter, _profile); - if ((opcode >> 12) is >= 1 and <= 3 || (opcode & 0xfb80) == 0x4880 || IsArithmeticIndexedInstruction(opcode)) + if ((opcode >> 12) is >= 1 and <= 3 || (opcode & 0xfb80) == 0x4880 || (IsArithmeticIndexedInstruction(opcode) || IsGeneralLogical(opcode) || IsAdvancedBitIndexedInstruction(opcode))) _indexedOperandExtraCycles += 4 + (baseSize == 2 ? 2 : baseSize == 3 ? 6 : 0) + (indirect == 0 ? 0 : 5 + ((indirect & 3) >= 2 ? 2 : 0)); @@ -19020,7 +19097,7 @@ private uint CalculateBriefIndexedAddress(int baseRegister, ushort extension, us private uint CalculateBriefIndexedAddress(uint baseAddress, ushort extension, ushort opcode) { - if ((extension & 0x100) != 0 && IsArithmeticIndexedInstruction(opcode)) + if ((extension & 0x100) != 0 && (IsArithmeticIndexedInstruction(opcode) || IsGeneralLogical(opcode) || IsAdvancedBitIndexedInstruction(opcode))) return CalculateIndexedOperandAddress(baseAddress, extension, opcode); if (!M68kIntegerSemantics.TryCalculateM68020BriefIndexedAddress( baseAddress, diff --git a/Copper68k/M68kCore.cs b/Copper68k/M68kCore.cs index 0cb0eb07..241bd453 100644 --- a/Copper68k/M68kCore.cs +++ b/Copper68k/M68kCore.cs @@ -1351,7 +1351,7 @@ public sealed class M68kCpuState /// public const ushort ResetStatusRegister = 0x2700; private const ushort M68000StatusRegisterMask = Trace | Supervisor | 0x0700 | ConditionCodeMask; - private const ushort M68020StatusRegisterMask = Trace | Master | Supervisor | 0x0700 | ConditionCodeMask; + private const ushort M68020StatusRegisterMask = Trace | 0x4000 | Master | Supervisor | 0x0700 | ConditionCodeMask; private const ushort ConditionCodeMask = Carry | Overflow | Zero | Negative | Extend; /// @@ -1533,6 +1533,7 @@ public ushort StatusRegister public uint LastInstructionProgramCounter { get; set; } internal int LastExceptionVector { get; set; } = -1; + internal uint ExceptionSequence { get; private set; } internal int FirstExceptionVector { get; set; } = -1; @@ -1601,6 +1602,8 @@ internal void RecordException(int vector, uint stackedProgramCounter, ushort sav FirstExceptionA7 = A[7]; } + ExceptionSequence++; + if (M68060StackModeEnabled) M68060BusControl = (M68060BusControl & 0xA000_0000u) >> 1; LastExceptionVector = vector; LastExceptionStackedProgramCounter = stackedProgramCounter; LastExceptionStatusRegister = savedStatusRegister; @@ -1795,7 +1798,7 @@ private void SetStatusRegister(ushort value) } // MC68060UM 3.2.2.2 / 11.1.2: M is software state, not a stack selector. - value &= M68060StackModeEnabled ? M68020StatusRegisterMask : M68000StatusRegisterMask; + value &= M68060StackModeEnabled ? (ushort)(M68020StatusRegisterMask & ~0x4000) : M68000StatusRegisterMask; var wasSupervisor = (_statusRegister & Supervisor) != 0; var isSupervisor = (value & Supervisor) != 0; if (wasSupervisor != isSupervisor) @@ -2329,6 +2332,7 @@ private enum PlannedRetireMode : byte private byte _deferredCpuBusBatchAdmissionRetryInstructions; private uint _activeInstructionProgramCounter; private bool _instructionTracePending; + internal bool ArchitecturalTraceEnabled { get; set; } = true; private uint _dataAccessStackedProgramCounter; private ushort? _addressErrorInstructionWord; private bool? _addressErrorIsWriteOverride; @@ -9110,6 +9114,35 @@ private bool DecodeByOpcodeLine(ushort opcode, uint instructionPc) }; } + protected bool TryExecuteM68010StatusAndReturn(ushort opcode, uint instructionPc) + { + if (opcode == 0x4E74) + { + var displacement = unchecked((short)FetchWord()); + State.ProgramCounter = PullLong(); + State.SetActiveStackPointer(unchecked(State.A[7] + (uint)displacement)); + AddInstructionCycles(16); + return true; + } + if ((opcode & 0xFFC0) == 0x40C0 && !State.GetFlag(M68kCpuState.Supervisor)) + { + RaiseException(8, instructionPc, 34); + return true; + } + if ((opcode & 0xFFC0) != 0x42C0) return false; + var mode = (opcode >> 3) & 7; + var reg = opcode & 7; + if (!IsDataAlterableEffectiveAddress(mode, reg)) + { + RaiseException(4, instructionPc, 34); + return true; + } + var ea = ResolveEa(mode, reg, M68kOperandSize.Word, write: true); + ea.Write((uint)(State.StatusRegister & 0x001F)); + AddInstructionCycles(GetMoveFromSrCycles(mode, reg)); + return true; + } + protected virtual bool TryExecuteModelSpecificLine4(ushort opcode, uint instructionPc) { _ = opcode; @@ -9156,8 +9189,8 @@ protected virtual bool TryHandleModelSpecificExceptionFrame( ushort savedStatusRegister) { if (!_useM68020BriefIndexedAddressing) return false; - if (vector == 5) PushLong(State.LastInstructionProgramCounter); - PushWord((ushort)((vector == 5 ? 0x2000 : 0) | ((vector * 4) & 0x0fff))); + if (vector is 5 or 6 or 7 or 9) PushLong(State.LastInstructionProgramCounter); + PushWord((ushort)((vector is 5 or 6 or 7 or 9 ? 0x2000 : 0) | ((vector * 4) & 0x0fff))); PushLong(stackedProgramCounter); PushWord(savedStatusRegister); return true; @@ -9237,7 +9270,7 @@ public void RequestInterrupt(int level, uint vectorAddress) } var mask = (State.StatusRegister >> 8) & 0x07; - if (level <= mask) + if (level <= mask && level != 7) { return; } @@ -9274,6 +9307,7 @@ public void RequestInterrupt(int level, uint vectorAddress) : interruptStartCycle; _cpuRetireBusCycle = Math.Max(_cpuRetireBusCycle, interruptStartCycle); var savedStatusRegister = State.StatusRegister; + State.RecordException((int)(vectorAddress / 4), State.ProgramCounter, savedStatusRegister); State.StatusRegister = (ushort)((savedStatusRegister & ~M68kCpuState.Trace & 0xF8FF) | ((level & 7) << 8) | M68kCpuState.Supervisor); @@ -9860,8 +9894,33 @@ private bool DecodeBranch(ushort opcode, uint instructionPc) return true; } + protected virtual bool SupportsMoves => false; + private bool DecodeLine0(ushort opcode, uint instructionPc) { + if ((opcode & 0xff00) == 0x0e00 && SupportsMoves) + { + var movesMode = (opcode >> 3) & 7; var movesReg = opcode & 7; + var field = (opcode >> 6) & 3; + if (field == 3 || movesMode < 2 || !IsDataAlterableEffectiveAddress(movesMode, movesReg)) + { RaiseException(4, instructionPc, 34); return true; } + if (!State.GetFlag(M68kCpuState.Supervisor)) + { RaiseException(8, instructionPc, 34); return true; } + var extension = FetchWord(); var general = (extension >> 12) & 7; + var addressRegister = (extension & 0x8000) != 0; var store = (extension & 0x800) != 0; + var movesSize = (M68kOperandSize)(1 << field); + var movesEa = ResolveEa(movesMode, movesReg, movesSize, addressOnly: true); + if (movesMode == 3) SetAddressRegister(movesReg, unchecked(movesEa.Address + AddressIncrement(movesReg, movesSize))); + if (store) movesEa.Write(addressRegister ? State.A[general] : State.D[general]); + else + { + var value = movesEa.Read(); + if (addressRegister) SetAddressRegister(general, M68kCpuState.SignExtend(value, movesSize)); + else WriteDataRegister(general, value, movesSize); + } + AddInstructionCycles(12 + movesEa.EaCycles); + return true; + } if (DecodeImmediateToStatusRegister(opcode, instructionPc)) { return true; @@ -10264,18 +10323,19 @@ private bool DecodeLine4(ushort opcode, uint instructionPc) return true; } - var statusRegister = PullWord(); - var programCounter = PullLong(); + var framePointer = State.A[7]; + var statusRegister = ReadWord(framePointer); + var programCounter = ReadLong(framePointer + 2); + var frameSize = 6u; if (UsesFormatWordExceptionFrames) { - var format = PullWord(); - if (_useM68020BriefIndexedAddressing && (format & 0xf000) == 0x2000) _ = PullLong(); + var format = ReadWord(framePointer + 6); if (!IsSupportedRteFrameFormat(format)) - { - RaiseException(14, instructionPc, 34); - return true; - } + { RaiseException(14, instructionPc, 34); return true; } + frameSize = (format & 0xF000) == 0x8000 ? 58u : + (format & 0xF000) == 0x2000 ? 12u : 8u; } + State.SetActiveStackPointer(framePointer + frameSize); State.StatusRegister = statusRegister; AddInstructionCycles(20); @@ -10355,6 +10415,12 @@ private bool DecodeLine4(ushort opcode, uint instructionPc) return true; } + if (_useM68020BriefIndexedAddressing && !State.GetFlag(M68kCpuState.Supervisor)) + { + RaiseException(8, instructionPc, 34); + return true; + } + var ea = ResolveEa(mode, reg, M68kOperandSize.Word, write: true); if (mode == 3 && (ea.Address & 1) != 0) { @@ -10431,7 +10497,7 @@ private bool DecodeLine4(ushort opcode, uint instructionPc) if ((opcode & 0xFFF8) == 0x4E50) { var reg = opcode & 7; - PushLong(State.A[reg]); + PushLong(reg == 7 ? State.A[7] - 4 : State.A[reg]); var displacement = unchecked((short)FetchWord()); SetAddressRegister(reg, State.A[7]); State.SetActiveStackPointer((uint)(State.A[7] + displacement)); @@ -10739,6 +10805,9 @@ private bool DecodeLine5(ushort opcode) if ((opcode & 0xF0C0) == 0x50C0) { + // On 000/010 these later TRAPcc words are invalid Scc EAs. + // Reject before extension consumption or destination effects. + if (((opcode >> 3) & 7) == 7 && (opcode & 7) > 1) return false; var condition = (opcode >> 8) & 0x0F; var conditionEa = ResolveEa((opcode >> 3) & 7, opcode & 7, M68kOperandSize.Byte, write: true); var conditionTrue = CheckCondition(condition); @@ -14402,7 +14471,7 @@ private void AllowInstructionRetireAtCycleFloor() private void BeginInstructionCycleFloor(long startCycle) { // MC68000 UM 6.3.8: sample before executing, not from the resulting SR. - _instructionTracePending = (State.StatusRegister & M68kCpuState.Trace) != 0; + _instructionTracePending = ArchitecturalTraceEnabled && (State.StatusRegister & M68kCpuState.Trace) != 0; // Reaching the next instruction proves that any previous DBcc // transition was consumed normally rather than by an interrupt. _exceptionEntryNotBeforeCycle = 0; diff --git a/Copper68k/M68kTimingEngine.cs b/Copper68k/M68kTimingEngine.cs index e655a257..60f743b5 100644 --- a/Copper68k/M68kTimingEngine.cs +++ b/Copper68k/M68kTimingEngine.cs @@ -266,6 +266,7 @@ internal enum M68kInstructionTimingKey GeneralMove, GeneralMovem, GeneralArithmetic, + GeneralLogical, FullIndexedMoveToMemory, FullIndexedRegisterToMemory, FullIndexedClear, diff --git a/Copper68k/M68kTimingFormula.cs b/Copper68k/M68kTimingFormula.cs index ad87dd9a..3cd698c8 100644 --- a/Copper68k/M68kTimingFormula.cs +++ b/Copper68k/M68kTimingFormula.cs @@ -1675,6 +1675,7 @@ private static int CalculateLogicalCycles(M68kTimingDescriptor descriptor) M68kTimingOperandForm.PostIncrement => 6, M68kTimingOperandForm.Predecrement => 7, M68kTimingOperandForm.AddressDisplacement => 7, + M68kTimingOperandForm.PcDisplacement => 7, M68kTimingOperandForm.BriefIndexed => 9, M68kTimingOperandForm.AbsoluteWord => 7, M68kTimingOperandForm.AbsoluteLong => 8, diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 21404398..8fbc609a 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -20,8 +20,8 @@ model. A gap report alone does not complete it.** | 1. Framework and MOVE | Complete: semantic gate, 2026-10-04 | 631,184 deterministic cases across all eight profiles; zero mismatches, unsupported legal forms or untested required combinations. | | 2. Data transfer and address operations | Complete: semantic gate, 2026-10-04 | 270,240 deterministic cases for MOVEQ, MOVEM, MOVEP, LEA, PEA, EXG, EXT/EXTB and SWAP; unavailable forms raise their documented exceptions. | | 3. Arithmetic and comparison | Complete | ADD/SUB variants, quick/immediate/address/extend forms, comparisons, multiply/divide, decimal/packing operations; overflow, borrow, carry, sticky zero, exceptional operands. | -| 4. Logical, bit and shift operations | Planned / untested | Logical/immediate/unary operations, bit manipulation, shifts/rotates, bitfields, atomic integer operations; preservation and memory effects. | -| 5. Control and system operations | Planned / untested | Branches, conditions, calls/returns, stack frames, traps, privilege-sensitive transfers, STOP/RESET, model-specific integer/system instructions; exception frames, saved PC/SR, stack selection, interrupt/trace. | +| 4. Logical, bit and shift operations | Complete: semantic gate, 2026-10-04 | Logical/immediate/unary operations, bit manipulation, shifts/rotates, bitfields, atomic integer operations; preservation and memory effects. | +| 5. Control and system operations | Complete: scoped semantic gate, 2026-10-04 | Branches, conditions, calls/returns, stack frames, traps, privilege-sensitive transfers, STOP/RESET, model-specific integer/system instructions; exception frames, saved PC/SR, stack selection, interrupt/trace. | | 6. Reference qualification and consolidation | Planned | Independent reference audits across selected models, gap review, retire proven redundant tests; publish architectural combination coverage, not just xUnit counts. | For every promoted family, mismatches and emulator-level unsupported execution @@ -315,3 +315,157 @@ Evidence directories: `artifacts/m3-final/`, `artifacts/m3-seeded/`, `artifacts/m3-validation/`. Semantic correctness, approximate timing policy and physical timing qualification remain separate; no new physical timing or OS compatibility qualification is claimed. + +## Milestones 4 and 5 implementation checkpoint — 2026-10-04 + +The logical/bit/shift/atomic matrix adds **4,073,024 deterministic cases** in +64 batches. Control/system adds **1,305,838 cases** in 128 batches. Together +with milestones 1–3, the required semantic gate is **8,324,030 cases** in +**338 xUnit batches** across all seven models and the A1200 EC020 profile. +Every required named batch must exist with its exact count and zero mismatching, +unsupported or untested cases before promotion. Architectural unavailable +instructions execute their documented illegal, line-F or unimplemented-integer +exceptions, rather than being counted as emulator gaps. + +| Milestone 4 group | 000/010 per profile | 020+ per profile | +| --- | ---: | ---: | +| Logical/unary value and CCR boundaries | 31,808 | 31,808 | +| Logical/unary addressing | 6,660 | 9,944 | +| Register/memory shifts and rotates | 130,560 | 131,088 | +| Static/dynamic bit operations | 104,192 | 104,852 | +| Bitfield widths, offsets, values and aliases | 155,136 | 155,136 | +| Bitfield addressing and full extensions | 2,688 | 3,480 | +| CAS sizes, comparisons, addressing and aliases | 36,975 | 37,371 | +| CAS2 comparisons, register fields and aliases | 36,864 | 36,864 | + +Boolean and bit-by-bit shift expectations are independent of production helpers. +Bitfields independently extract/insert every selected bit, including signed +memory offsets, INT_MIN, wrapping register fields, width 32, dynamic offset/width +aliases and original-offset BFFFO results. CAS/CAS2 check comparison precedence, +partial registers, X preservation and selected read/write order and widths. +040/060 failed CAS writeback and the 060 misalignment/removed-CAS2 outcomes are +explicit. Overlapping CAS2 memory results are architecturally undefined/excluded. + +| Milestone 5 group | 000/010 per profile | 020+ per profile | +| --- | ---: | ---: | +| Branch conditions, displacements and subroutine stacks | 8,192 | 12,288 | +| DBcc/Scc registers, addressing and CCR | 42,048 | 43,104 | +| Conditional traps | 3,072 | 3,072 | +| JMP/JSR addressing and stack aliases | 3,592 | 3,856 | +| Traps, privilege, STOP/RESET and basic instructions | 1,728 | 1,728 | +| SR/CCR/USP transfers and immediate status operations | 16,540 | 16,936 | +| LINK/UNLK and ordinary returns | 6,080 | 6,080 | +| CHK/CHK2/CMP2 bounds, registers and addressing | 11,828 | 12,488 | +| MOVES register/memory transfers and privilege | 10,656 | 11,052 | +| BKPT, cache instructions, five MOVE16 forms, LPSTOP | 13,120 | 13,120 | +| MOVEC selectors, masks, general registers and privilege | 2,432 | 2,432 | +| RTE frames and stack selection | 2,048 | 020/030/EC020/A1200: 1,888; 040: 2,016; 060: 2,048 | +| Trace retirement and batch boundaries | 580 | 020/030/040/EC020/A1200: 1,160; 060: 580 | +| Interrupt masks, STOP wakeup, VBR and master frames | 000: 224; 010: 226 | 020/030/040/EC020/A1200: 418; 060: 226 | +| CALLM descriptors, frames, arguments and access requests | 10,832 | 020/EC020/A1200: 11,000; 030/040/060: 10,964 | +| RTM frames, arguments, denied access and invalid options | 24,594 | 24,594 | + +System scenarios verify exact stacked PC/SR, privilege before operand effects, +model-specific format-2 frames, inactive stack preservation, master interrupts +with paired MSP/ISP frames and real RTE restoration. T1 traces completed +instructions, T0 traces documented control flow on applicable models; completed +traps and aborting exceptions are distinguished. Batch-boundary trace changes +cannot bypass retirement through cached hot blocks. MOVE16 covers all five +register/absolute forms, line alignment, aliases and one postincrement for a +shared source/destination register. MOVEC exercises each model's selectors and +reserved/read-as-zero masks while keeping MMU translation disabled. + +CALLM/RTM are implemented for 020/EC020/A1200, including inline/indirect argument +options, all general-register selections, descriptor validation and type-1 +access-level requests. CPU-space access control uses an optional **internal** +responder separate from ordinary RAM. Public flat buses have no responder and +type-1 modules take a format error. The synthetic responder qualifies request +and architectural state behavior; it does not introduce a public package API or +qualify external module hardware timing. Reserved/unused frame fields are masked. + +Production corrections discovered by these matrices include: + +- Complete residual logical, bit, shift and system EA routes before approximate + fallback; admit legal full indexed forms in their selected execution paths. + Never retry an instruction after partial operand effects. +- Synchronize A7 operand changes with its architectural stack bank; retain + source-before-destination effects and existing brief/fixed-cycle timing policy. +- Clear bitfield V/C, avoid signed-offset overflow, preserve BFFFO's original + signed offset, implement CAS2 Dc1 failure precedence and failed atomic writeback. +- Implement missing 010 status/RTD/MOVES behavior; apply advanced SR privilege and + alignment rules before 040's early-integer fallback. +- Store decremented SP for LINK A7 and preserve the pulled value for UNLK A7. + CHK2 sets Z on either boundary; CMP2 compares an address register at full width + against sign-extended byte/word bounds. +- Correct model-specific MOVEC availability, USP transfers, CACR reserved and + clear-command masks, 060 BUSCR state and synchronous exception handling. + Normal disabled-MMU memory transfers no longer erase 040 MMUSR. +- Implement MOVE16's five forms, 060 LPSTOP and 020 CALLM/RTM. Replace permissive + historical MOVE16 extensions with the documented mandatory extension bit. +- Preserve synchronous master state, clear master state for interrupts, recognize + directly requested level 7, emit documented trap/trace frames and consume + throwaway RTE frames across stack switches. Keep 060's absent T0 bit masked. + +### Qualification boundaries + +The promoted gate covers the named semantic scenarios above. It **does not** +qualify faulted-instruction restart/internal-state restoration on 010 frame 8, +020/030 frames 9/A/B, or 040 frame 7 pending exceptions/writebacks. These remain +explicitly untested execution combinations in the qualified report; they are +not classified as architecturally invalid or as passing RTE coverage. Existing +cache, prefetch, bus ordering, detailed faults, JIT and native regressions remain +retained. A nested early-model address fault on an invalid odd supervisor stack +was observed during fixture development and remains an unresolved detailed-fault +edge; valid instruction fixtures do not hide it as a passing fault qualification. + +BKPT tests the documented outcome without an external replacement device. +Physical MOVES function-code bus spaces and LPSTOP CPU-space broadcast are +unqualified by the current public bus boundary. Undefined MOVES self-base stored +values, simultaneous T1/T0, reserved module frame fields and undefined atomic overlap +results are excluded explicitly. FPU arithmetic, enabled MMU translation, +physical cache/pipeline behavior and OS compatibility remain outside the roadmap. +Diagnostic 010/060 results continue to be separate from desktop readiness. + +### Validation evidence + +- Full restored-source Release CPU suite: **4,592 passed, 7 optional skips, + 0 failed**. Deep audit separately enabled: seed 68020, 10,000 cases per profile + in each of MOVE, arithmetic, logical and control, **320,000 passing cases**. + Both deterministic and deep report validation require every exact count and + zero mismatching/unsupported/untested required cases. +- All **20 targeted mutations** are detected by executed semantic scenarios, + including all eleven retained MOVE/arithmetic proofs and nine new logical/system + proofs. Mutated source is restored in `finally` and rebuilt with zero warnings. + The CACR readback proof required adding clear-command bits to the input values; + its replacement case is `68020/MOVEC/L/R0/control=002/store=True/super=True`. + Proof SHA-256: `787210877e40827c651cbcaf077e0a5ccbc4b62a5d6211dea278ae82a1aef56d`. +- Retained AHX tests: **18 passed**. Private package + `1.5.2-synthetic-dev.32` passes package/content validation; SHA-256 + `1147b920e313219bc5d95ddb87735d7767d58a89fb146a28a060d38cbfb6fca9`. + It is **not published**; existing published versions remain immutable. +- Isolated CopperScreen `d9beae8` checkout resolves this exact package through + `Copper68kBoundaryVersion`, preserving the existing NuGet dependency boundary. + Production Release build: **0 warnings/errors**; host **149 passed, 6 optional + media skips**; disk **74 passed**; separate engine diagnostics **1,080 passed**. +- Native 68000 Workbench 3.1 floppy boot at 0 and 2 MiB Fast RAM: **2 passed**. + Native A1200 EC020 eight-plane boot and cold reopen: **1 passed**. The retained + cycle, PC, framebuffer, RGB24/DOS proof checks and pinned inputs are unchanged. + No ROM/media files are committed. These are deterministic integration replays, + not throughput or physical-timing acceptance measurements. +- Specialized regressions are retained. Historical unsupported logical tests now + assert the completed instruction result; historical MOVE16 fixtures use the + documented extension bit, and the 040 invalid-MOVEC regression expects vector 4. + No regression has been retired without replacement proof. + +Evidence: `artifacts/m4-m5-final/`, `artifacts/m4-m5-seed-68020-fixed/`, +`artifacts/m4-m5-mutation-proof-final/`, `artifacts/synthetic-private-feed-32/`, +and the isolated consumer's `artifacts/m4-m5-validation/`. Reproduce with: + +```powershell +./scripts/test-copper68k-synthetic.ps1 +./scripts/test-copper68k-synthetic.ps1 -Deep -Seed 68020 -Samples 10000 +./scripts/test-copper68k-synthetic-mutations.ps1 +``` + +Cross-model independent external reference qualification and consolidation remain +milestone 6. Package publication remains a separately authorized release action. diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index 8ceee70b..6f6906c6 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -62,10 +62,39 @@ $mutations += @( { var address = unchecked(State.A[register] - 1u); '@; group='DecimalAndPacking'; milestone=3}, - @{name='060-divide-frame'; file='Copper68k/M68060Interpreter.cs'; before=' if (vector == 5) PushLong(State.LastInstructionProgramCounter);'; after=' // Mutant: missing format-2 instruction address.'; group='MultiplyDivideAddressingAndRegisterAliases'; milestone=3} + @{name='060-divide-frame'; file='Copper68k/M68060Interpreter.cs'; before=' if (vector is 5 or 6 or 7 or 9) PushLong(State.LastInstructionProgramCounter);'; after=' // Mutant: missing format-2 instruction address.'; group='MultiplyDivideAddressingAndRegisterAliases'; milestone=3} ) -if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { $_.milestone -ne 3 }) } +$mutations += @( + @{name='bitfield-clear-vc'; file=$advanced; before=@' + State.SetFlag(M68kCpuState.Zero, field == 0); + State.SetFlag(M68kCpuState.Overflow, false); + State.SetFlag(M68kCpuState.Carry, false); +'@; after=' State.SetFlag(M68kCpuState.Zero, field == 0);'; group='BitFieldWidthsOffsetsFlagsAndRegisterAliases'; milestone=4}, + @{name='bitfield-negative-offset'; file=$advanced; before=@' + private static int FloorDivideBy8(int value) + => value >> 3; +'@; after=@' + private static int FloorDivideBy8(int value) + => value / 8; +'@; group='BitFieldAddressingSignedOffsetsAndFullExtensions'; milestone=4}, + @{name='cas2-dc1-precedence'; file=$advanced; before=@' + WriteDataRegisterSized(compareRegister2, destination2, size); + WriteDataRegisterSized(compareRegister1, destination1, size); +'@; after=@' + WriteDataRegisterSized(compareRegister1, destination1, size); + WriteDataRegisterSized(compareRegister2, destination2, size); +'@; group='Cas2ComparisonsRegisterFieldsAndAliases'; milestone=4}, + @{name='040-cas2-failed-writeback'; file=$advanced; before=' if (_profile.Model == M68kAcceleratorModel.M68040) WriteSized(address1, destination1, size);'; after=' // Mutant: missing failed-CAS2 writeback.'; group='Cas2ComparisonsRegisterFieldsAndAliases'; milestone=4; model='68040'}, + @{name='chk2-boundary-z'; file=$advanced; before=' State.SetFlag(M68kCpuState.Zero, value == lower || value == upper);'; after=' State.SetFlag(M68kCpuState.Zero, false);'; group='CheckBoundsValuesFlagsFormsAndRegisterAliases'; milestone=5}, + @{name='cmp2-address-width'; file=$advanced; before=' var value = useAddressRegister ? unchecked((int)State.A[register]) : SignExtendForSize(State.D[register], size);'; after=' var value = SignExtendForSize(useAddressRegister ? State.A[register] : State.D[register], size);'; group='CheckBoundsValuesFlagsFormsAndRegisterAliases'; milestone=5}, + @{name='rte-throwaway'; file=$advanced; before=' if (format == 1) continue; // Throwaway frame can select another stack.'; after=' // Mutant: accept throwaway as final frame.'; group='RteFramesPrivilegeStackSelectionAndInvalidFormats'; milestone=5}, + @{name='move16-postincrement'; file='Copper68k/M68kAdvancedTimingInterpreter.System.cs'; before=' if (form == 4 || form < 2) WriteGeneralRegister(true, register, unchecked(State.A[register] + 16));'; after=' if (form == 4 || form < 2) WriteGeneralRegister(true, register, unchecked(State.A[register] + 4));'; group='Move16CacheInstructionsBreakpointsAndLowPowerStop'; milestone=5; model='68040'}, + @{name='cacr-clear-readback'; file=$advanced; before='? value & ~0x0C0Cu : value; // Clear commands always read as zero.'; after='? value : value; // Mutant: clear commands read back set.'; group='MovecControlInventoryMasksPrivilegeAndAllGeneralRegisters'; milestone=5} +) +if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } +if ($Scope -eq 'Logical') { $mutations = @($mutations | Where-Object { $_.milestone -eq 4 }) } +if ($Scope -eq 'Control') { $mutations = @($mutations | Where-Object { $_.milestone -eq 5 }) } $saved = @{} foreach ($mutation in $mutations) { $path = Join-Path $repo $mutation.file @@ -87,7 +116,7 @@ try { [Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR', $directory) try { [IO.File]::WriteAllText($path, $text.Replace($before, $after), [Text.UTF8Encoding]::new($false)) - $model = $(if ($mutation.name -eq '060-divide-frame') { '68060' } else { '68020' }) + $model = $(if ($mutation.model) { $mutation.model } elseif ($mutation.name -eq '060-divide-frame') { '68060' } else { '68020' }) & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~$($mutation.group)&DisplayName~$model" --logger "trx;LogFileName=mutation.trx" --results-directory $directory *> (Join-Path $directory 'run.log') $exitCode = $LASTEXITCODE $batches = @(Get-ChildItem -LiteralPath $directory -Filter '*.json' | ForEach-Object { Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json }) diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 75ee64f2..4fd07ab2 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -68,7 +68,31 @@ try { 'arithmetic-scenarios'=21072; 'arithmetic-extend'=25440; 'arithmetic-decimal'=$(if ($model -in @('68000','68010')) {111392} else {111458}); 'arithmetic-muldiv-boundaries'=26904; - 'arithmetic-muldiv-addressing'=$(if ($model -in @('68000','68010')) {5608} elseif ($model -eq '68060') {7208} else {7224})} + 'arithmetic-muldiv-addressing'=$(if ($model -in @('68000','68010')) {5608} elseif ($model -eq '68060') {7208} else {7224}); + 'control-branches'=$(if ($model -in @('68000','68010')) {8192} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {12288}); + 'control-conditions'=$(if ($model -in @('68000','68010')) {42048} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {43104}); + 'control-jumps'=$(if ($model -in @('68000','68010')) {3592} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {3856}); + 'control-trapcc'=3072; + 'logical-addressing'=$(if ($model -in @('68000','68010')) {6660} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {9944}); + 'logical-bitfield-addressing'=$(if ($model -in @('68000','68010')) {2688} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {3480}); + 'logical-bitfield-values'=155136; + 'logical-bits'=$(if ($model -in @('68000','68010')) {104192} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {104852}); + 'logical-boundaries'=31808; + 'logical-cas'=$(if ($model -in @('68000','68010')) {36975} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {37371}); + 'logical-cas2'=36864; + 'logical-shifts'=$(if ($model -in @('68000','68010')) {130560} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {131088}); + 'system-basic'=1728; + 'system-bounds'=$(if ($model -in @('68000','68010')) {11828} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {12488}); + 'system-callm'=$(if ($model -in @('68000','68010')) {10832} elseif ($model -in @('68020','68EC020','A1200')) {11000} elseif ($model -in @('68030','68040','68060')) {10964}); + 'system-interrupt'=$(if ($model -in @('68000')) {224} elseif ($model -in @('68010','68060')) {226} elseif ($model -in @('68020','68030','68040','68EC020','A1200')) {418}); + 'system-model'=13120; + 'system-movec'=2432; + 'system-moves'=$(if ($model -in @('68000','68010')) {10656} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {11052}); + 'system-rte'=$(if ($model -in @('68000','68010','68060')) {2048} elseif ($model -in @('68020','68030','68EC020','A1200')) {1888} elseif ($model -in @('68040')) {2016}); + 'system-rtm'=24594; + 'system-stack'=6080; + 'system-status'=$(if ($model -in @('68000','68010')) {16540} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {16936}); + 'system-trace'=$(if ($model -in @('68000','68010','68060')) {580} elseif ($model -in @('68020','68030','68040','68EC020','A1200')) {1160})} foreach ($group in $expected.Keys) { $report = Get-Content -LiteralPath (Join-Path $output "$model-$group.json") -Raw | ConvertFrom-Json if ($report.logicalCases -ne $expected[$group] -or $report.counts.passing -ne $expected[$group] -or @@ -78,10 +102,11 @@ try { } if ($Deep) { foreach ($model in $Models) { - $report = Get-Content -LiteralPath (Join-Path $output "$model-move-seeded-$Seed.json") -Raw | ConvertFrom-Json - if ($report.logicalCases -ne $Samples -or $report.counts.passing -ne $Samples) { throw "Incomplete seeded audit: $model" } - $arithmetic = Get-Content -LiteralPath (Join-Path $output "$model-arithmetic-seeded-$Seed.json") -Raw | ConvertFrom-Json - if ($arithmetic.logicalCases -ne $Samples -or $arithmetic.counts.passing -ne $Samples) { throw "Incomplete seeded arithmetic audit: $model" } + foreach ($group in @('move', 'arithmetic', 'logical', 'control')) { + $report = Get-Content -LiteralPath (Join-Path $output "$model-$group-seeded-$Seed.json") -Raw | ConvertFrom-Json + if ($report.logicalCases -ne $Samples -or $report.counts.passing -ne $Samples -or + $report.counts.mismatching -ne 0 -or $report.counts.unsupported -ne 0 -or $report.counts.untested -ne 0) { throw "Incomplete seeded audit: $model/$group" } + } } } $references = @() @@ -126,16 +151,22 @@ try { $references += 'WinUAE: available adapter is 68000 integer; generator supports other models, adapters/fixtures still need qualification' } $inventory = Get-Content -LiteralPath (Join-Path $output 'integer-inventory.json') -Raw | ConvertFrom-Json - foreach ($row in $inventory.combinations) { if ($row.Milestone -le 3) { $row.status = 'passing' } } + foreach ($row in $inventory.combinations) { + if ($row.Milestone -le 5) { $row.status = 'passing'; $row.note = 'Passing named semantic scenario batches; consult qualification boundaries for untested physical/internal protocols.' } + } + $inventory | Add-Member -NotePropertyName qualificationBoundaries -NotePropertyValue @('RTE fault restart/internal formats remain untested: 010 frame8 restart, 020/030 frames9/A/B, 040 frame7', 'BKPT external replacement responder is unavailable', 'MOVES physical function-code spaces and LPSTOP CPU-space broadcast are unqualified', 'CALLM/RTM type1 protocol uses an internal synthetic responder; normal public buses have none') $inventory | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'qualified-inventory.json') - @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=146; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; roadmapComplete=$false; + @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=338; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; roadmapComplete=$false; seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Other models remain unqualified by these adapters'}); + unavailableSystemCoverage=@('RTE bus-fault internal restart on 010/020/030', '020/030 coprocessor midinstruction restoration', '040 access-fault pending trace/writeback restoration', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast'); timing='Semantic gate; timing policy and physical qualification remain separate'} | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'summary.json') - Write-Host "Synthetic MOVE, transfer and arithmetic gates: $logicalCases logical cases; reports at $output" + Write-Host "Synthetic milestones 1-5 semantic gates: $logicalCases logical cases; reports at $output" } finally { foreach ($name in $savedEnvironment.Keys) { [Environment]::SetEnvironmentVariable($name, $savedEnvironment[$name]) } Pop-Location } +# Report-only validation does not execute a native command; clear an inherited native exit code. +$global:LASTEXITCODE = 0 From 86ce18b2ba95ebd2ea28ce42079d14600068b1fd Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Sun, 4 Oct 2026 03:55:21 +0300 Subject: [PATCH 002/211] Trim trailing blank lines in synthetic suite sources --- Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs | 1 - Copper68k.Tests/Synthetic/SyntheticModuleTests.cs | 1 - Copper68k/M68kAdvancedTimingInterpreter.System.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs b/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs index 98ce4a0f..406378ab 100644 --- a/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs @@ -152,4 +152,3 @@ public void Move16CacheInstructionsBreakpointsAndLowPowerStop(string modelId) report.Complete(output); } } - diff --git a/Copper68k.Tests/Synthetic/SyntheticModuleTests.cs b/Copper68k.Tests/Synthetic/SyntheticModuleTests.cs index 9853c7fc..5b3983a8 100644 --- a/Copper68k.Tests/Synthetic/SyntheticModuleTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticModuleTests.cs @@ -127,4 +127,3 @@ public void RtmFramesRestoreAllGeneralRegistersConditionsAndArguments(string mod report.Complete(output); } } - diff --git a/Copper68k/M68kAdvancedTimingInterpreter.System.cs b/Copper68k/M68kAdvancedTimingInterpreter.System.cs index 747d6ccd..1fc947ca 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.System.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.System.cs @@ -160,4 +160,3 @@ private bool TryExecuteGeneralSystem(ushort opcode) return true; } } - From 5ef8726c19e31f9e3b15099acdae96487a766fe2 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Sun, 4 Oct 2026 12:34:27 +0300 Subject: [PATCH 003/211] Add cross-model reference audits and correct 68040 T0 tracing --- .../M68kMusashiConformanceTests.cs | 15 +- .../M68kMusashiConformanceTests.md | 21 ++- Copper68k.Tests/M68kMusashiModelAuditTests.cs | 85 +++++++++++ Copper68k.Tests/M68kShiftTests.cs | 20 --- Copper68k.Tests/Synthetic/README.md | 18 ++- .../Synthetic/SyntheticBitShiftTests.cs | 7 + .../SyntheticExceptionControlTests.cs | 59 +++++++- Copper68k/M68kAdvancedTimingInterpreter.cs | 9 ++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 138 ++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 35 ++++- .../test-copper68k-synthetic-mutations.ps1 | 30 +++- scripts/test-copper68k-synthetic.ps1 | 27 ++-- 12 files changed, 412 insertions(+), 52 deletions(-) create mode 100644 Copper68k.Tests/M68kMusashiModelAuditTests.cs create mode 100644 docs/COPPER68K_REFERENCE_QUALIFICATION.md diff --git a/Copper68k.Tests/M68kMusashiConformanceTests.cs b/Copper68k.Tests/M68kMusashiConformanceTests.cs index 63ce688d..d422d134 100644 --- a/Copper68k.Tests/M68kMusashiConformanceTests.cs +++ b/Copper68k.Tests/M68kMusashiConformanceTests.cs @@ -5,7 +5,7 @@ namespace Copper68k.Tests; -public sealed class M68kMusashiConformanceTests +public sealed partial class M68kMusashiConformanceTests { private const string RunVariable = "COPPER68K_RUN_MUSASHI_M68000"; private const string PathVariable = "COPPER68K_MUSASHI_M68000_PATH"; @@ -42,6 +42,7 @@ public sealed class M68kMusashiConformanceTests private static readonly IReadOnlyDictionary KnownFailingM68040Programs = new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["chk2.bin"] = "at offset 0xEC encodes CHK2.W (02D7 8800) against long bounds 10000000/70000000; asserts a long-sized trap, contrary to full-width An comparison against sign-extended word bounds", ["cmp2.bin"] = "CMP2 byte carry expectation conflicts with the test source author's documented expectation" }; private static readonly IReadOnlyDictionary KnownFailingM68kRsExtraPrograms = @@ -488,21 +489,25 @@ private static M68kCpuModel GetM68kRsExtraCpuModel(string relativePath) return null; } - private void RunProgram(string file, int maxInstructions, M68kCpuModel cpuModel, MusashiBackend backend) + private int RunProgram(string file, int maxInstructions, M68kCpuModel cpuModel, MusashiBackend backend, bool a1200 = false) { var bus = new MusashiBus(); bus.LoadRom(File.ReadAllBytes(file)); bus.WriteLong(0, 0x0000_03F0); bus.WriteLong(4, MusashiBus.RomBase); - using var cpu = CreateCore(cpuModel, backend, bus); + using var cpu = a1200 ? M68kCoreFactory.Default.CreateA1200Ec020(bus) : CreateCore(cpuModel, backend, bus); cpu.Reset(MusashiBus.RomBase, 0x0000_03F0); + var recent = new Queue(); for (var instruction = 0; instruction < maxInstructions; instruction++) { + var previousPc = cpu.State.ProgramCounter; try { cpu.ExecuteInstruction(); + if (recent.Count == 16) recent.Dequeue(); + recent.Enqueue($"{previousPc:X8}:{cpu.State.LastOpcode:X4}->{cpu.State.ProgramCounter:X8} SR={cpu.State.StatusRegister:X4} D6={cpu.State.D[6]:X8}"); } catch (Exception ex) { @@ -527,12 +532,12 @@ private void RunProgram(string file, int maxInstructions, M68kCpuModel cpuModel, $"last PC=0x{cpu.State.LastInstructionProgramCounter:X8}, opcode=0x{cpu.State.LastOpcode:X4}, " + FormatDataRegisters(cpu.State) + $", {FormatAddressRegisters(cpu.State)}, {FormatLastException(cpu.State)}" + - $".{FormatStdout(bus)}"); + $".{FormatStdout(bus)}{Environment.NewLine}Recent retirement: {string.Join("; ", recent)}"); } if (bus.PassCount > 0) { - return; + return instruction + 1; } if (cpu.State.Stopped || cpu.State.Halted) diff --git a/Copper68k.Tests/M68kMusashiConformanceTests.md b/Copper68k.Tests/M68kMusashiConformanceTests.md index 18362f1d..3ab0966d 100644 --- a/Copper68k.Tests/M68kMusashiConformanceTests.md +++ b/Copper68k.Tests/M68kMusashiConformanceTests.md @@ -66,16 +66,18 @@ Useful MC68040 environment variables: - `COPPER68K_MUSASHI_M68040_MAX_INSTRUCTIONS`: per-program execution cap; default is `1000000`. - `COPPER68K_MUSASHI_M68040_INCLUDE_KNOWN_FAILING`: include MC68040 programs that currently expose unsupported or divergent behavior. -The MC68040 runner uses the interpreter backend. The default run excludes one -known divergent program so the test remains useful in regular explicit audit +The MC68040 runner uses the interpreter backend. The default run excludes two +reviewed incompatible programs so the test remains useful in regular explicit audit passes: +- `chk2.bin`: the final `02D7 8800` instruction is word-sized against long + bounds but asserts a long-sized trap; see the independent model audit notes. - `cmp2.bin`: the byte-size CMP2 carry expectation follows Musashi but conflicts with the test source author's inline note about the expected branch condition; SingleStepTests remain the authority when an instruction is covered there. -With that exclusion, the current MC68040 Musashi baseline covers the bitfield -programs, `cas.bin`, `chk2.bin`, long multiply/divide, `interrupt.bin`, +With those exclusions, the current MC68040 Musashi baseline covers the bitfield +programs, `cas.bin`, long multiply/divide, `interrupt.bin`, `jmp.bin`, `rtd.bin`, `shifts3.bin`, and `trapcc.bin`. Use `COPPER68K_MUSASHI_M68040_INCLUDE_KNOWN_FAILING=1` as an audit mode when working on the excluded program. @@ -124,3 +126,14 @@ exception differences, and a few exact instruction forms found by the fixture prologues. Use `COPPER68K_M68KRS_EXTRA_INCLUDE_KNOWN_FAILING=1` as audit mode when working down those buckets. + +## Cross-model integer audit + +`test-copper68k-synthetic.ps1 -MusashiPath ` reuses this +program runner for all selected models, including the public A1200 factory. +Both complete directories are required. The report records per-program/profile +passes, mismatches, explicit exclusions, input hashes and retirement counts. +See [qualification notes](../docs/COPPER68K_REFERENCE_QUALIFICATION.md) for +source pins, reviewed exclusions and the distinction between independent +software assertions and hardware qualification. Motorola manuals establish +architectural expectations; third-party assertions require review. diff --git a/Copper68k.Tests/M68kMusashiModelAuditTests.cs b/Copper68k.Tests/M68kMusashiModelAuditTests.cs new file mode 100644 index 00000000..a4f7991a --- /dev/null +++ b/Copper68k.Tests/M68kMusashiModelAuditTests.cs @@ -0,0 +1,85 @@ +using System.Security.Cryptography; +using System.Text.Json; +using Copper68k.Tests.Synthetic; +using Xunit.Sdk; + +namespace Copper68k.Tests; + +public sealed partial class M68kMusashiConformanceTests +{ + private const string ModelAuditRun = "COPPER68K_RUN_MUSASHI_MODEL_AUDIT"; + + [EnvironmentFact(ModelAuditRun, "audit pinned Musashi integer programs across selected CPU profiles")] + public void MusashiIntegerProgramsAcrossSelectedModelsWhenEnabled() + { + var root = Environment.GetEnvironmentVariable("COPPER68K_MUSASHI_MODEL_AUDIT_PATH"); + if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root)) throw new XunitException("Model audit requires the Musashi repository root."); + var selection = Environment.GetEnvironmentVariable("COPPER68K_SYNTHETIC_MODELS") ?? string.Join(',', ModelSpec.All.Select(m => m.Id)); + var selected = selection.Split(',', StringSplitOptions.TrimEntries); + if (selected.Length == 0 || selected.Distinct().Count() != selected.Length || selected.Any(id => !ModelSpec.All.Any(m => m.Id == id))) + throw new XunitException($"Empty, duplicate or unknown audit model selection: '{selection}'."); + var rows = new List(); + foreach (var id in selected) + { + var model = ModelSpec.All.Single(m => m.Id == id); + foreach (var directory in new[] { "mc68000", "mc68040" }) + { + var path = Path.Combine(root, "test", directory); + if (!Directory.Exists(path)) throw new XunitException($"Missing reference fixtures: {path}"); + var files = Directory.GetFiles(path, "*.bin").OrderBy(Path.GetFileName, StringComparer.Ordinal).ToArray(); + var requiredCount = directory == "mc68000" ? 60 : 18; + if (files.Length != requiredCount) throw new XunitException($"Incomplete pinned reference fixture selection: {path}; expected {requiredCount}, found {files.Length}."); + foreach (var file in files) + { + var name = Path.GetFileName(file); + var reason = ReferenceExclusion(model, directory, name); + var hash = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(file))).ToLowerInvariant(); + if (reason != null) { rows.Add(new(id, $"{directory}/{name}", hash, "excluded", 0, reason)); continue; } + try + { + var instructions = RunProgram(file, DefaultMaxInstructions, model.Model, MusashiBackend.Interpreter, model.A1200); + rows.Add(new(id, $"{directory}/{name}", hash, "passing", instructions, null)); + } + catch (Exception ex) { rows.Add(new(id, $"{directory}/{name}", hash, "mismatching", 0, ex.Message)); } + } + } + if (!rows.Any(r => r.Model == id && r.Status != "excluded")) throw new XunitException($"No executable reference programs selected for {id}."); + } + var output = Environment.GetEnvironmentVariable("COPPER68K_SYNTHETIC_REPORT_DIR"); + if (!string.IsNullOrWhiteSpace(output)) + { + Directory.CreateDirectory(output); + File.WriteAllText(Path.Combine(output, "musashi-model-audit.json"), JsonSerializer.Serialize(new + { + schema = 1, reference = "Musashi self-checking programs", selectedModels = selected, + qualification = "Independent software assertions; program counts are not instruction-combination or hardware qualification counts", + passing = rows.Count(r => r.Status == "passing"), mismatching = rows.Count(r => r.Status == "mismatching"), + excluded = rows.Count(r => r.Status == "excluded"), rows + }, new JsonSerializerOptions { WriteIndented = true })); + } + foreach (var model in selected) + _output.WriteLine($"{model}: {rows.Count(r => r.Model == model && r.Status == "passing")} reference programs passed, {rows.Count(r => r.Model == model && r.Status == "excluded")} excluded."); + var failures = rows.Where(r => r.Status == "mismatching").ToArray(); + Assert.True(failures.Length == 0, string.Join(Environment.NewLine, failures.Select(r => $"{r.Model}/{r.Program}: {r.Detail}"))); + } + + private static string? ReferenceExclusion(ModelSpec model, string directory, string name) + { + if (directory == "mc68000") + { + if (KnownIncompatiblePrograms.TryGetValue(name, out var reason)) return reason; + if (model.Id == "68060" && name == "movep.bin") return "060 hardware raises vector 61; this program assumes hardware MOVEP execution without a software handler."; + } + else + { + if (!model.FullIndex) return "Program requires 020+ instructions; no unavailable-instruction handler in the fixture."; + if (KnownFailingM68040Programs.TryGetValue(name, out var reason)) return reason; + if (model.Id == "68060" && name is "cas.bin" or "divs_long.bin" or "divu_long.bin" or "mul_long.bin") + return "Program contains integer forms unimplemented in 060 hardware and supplies no vector-61 software handler."; + if (model.Id == "68060" && name == "interrupt.bin") return "Program requires the 020/030/040 master stack and paired throwaway frame; 060 ignores M and has no MSP."; + } + return null; + } + + private sealed record ProgramAuditRow(string Model, string Program, string Sha256, string Status, int RetiredInstructions, string? Detail); +} diff --git a/Copper68k.Tests/M68kShiftTests.cs b/Copper68k.Tests/M68kShiftTests.cs index 93bc40f7..b55ae0ac 100644 --- a/Copper68k.Tests/M68kShiftTests.cs +++ b/Copper68k.Tests/M68kShiftTests.cs @@ -65,24 +65,4 @@ public void InvalidMemoryShiftEncodingRaisesIllegalInstructionException(ushort o Assert.Equal(0x1000u, bus.ReadLong(0x4FFC)); } - [Fact] - public void AslByteSetsOverflowWhenSignChanges() - { - var bus = new Copper68kTestBus(); - bus.WriteWords(0x1000, 0xE302); - - var cpu = new M68kInterpreter(bus); - cpu.Reset(0x1000, 0x8000); - cpu.State.D[2] = 0x6891_C884; - cpu.State.StatusRegister = 0x0700; - - cpu.ExecuteInstruction(); - - Assert.Equal(0x6891_C808u, cpu.State.D[2]); - Assert.True(cpu.State.GetFlag(M68kCpuState.Extend)); - Assert.True(cpu.State.GetFlag(M68kCpuState.Carry)); - Assert.True(cpu.State.GetFlag(M68kCpuState.Overflow)); - Assert.False(cpu.State.GetFlag(M68kCpuState.Negative)); - Assert.False(cpu.State.GetFlag(M68kCpuState.Zero)); - } } diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index b5f7a609..3f71b5d6 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -14,9 +14,9 @@ From the CopperMod root, run: ./scripts/test-copper68k-synthetic.ps1 ``` -Milestones 1–5 execute MOVE/MOVEA, transfer/address, arithmetic/comparison, +Milestones 1–5 execute MOVE/MOVEA, transfer/address, arithmetic/comparison, logical/bit/shift/atomic and control/system operations in -**8,324,030 logical cases** across **338 xUnit batches**, across seven models and the A1200 profile. +**8,333,190 logical cases** across **338 xUnit batches**, across seven models and the A1200 profile. Ordinary `dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release` includes these batches. CI additionally validates every required report and exact count; missing reports, mismatches, unsupported execution and empty groups fail the gate. @@ -89,7 +89,7 @@ format-2 instruction address on 020+ and RTE in both stack modes. ./scripts/test-copper68k-synthetic-mutations.ps1 -Scope Control ``` -Twenty isolated mutations prove detection of absolute-address decoding, extension +Twenty-two isolated mutations prove detection of absolute-address decoding, extension length, signed indexes, source/destination alias order, A7 byte stride and MOVE flags, arithmetic overflow, extend sticky zero, decimal alias ordering, PACK A7 stride and the 060 divide frame. Additional proofs cover bitfield V/C clearing, @@ -129,3 +129,15 @@ unavailable qualification. Cache/prefetch/fault/JIT/native suites remain retaine no physical pipeline/cache, FPU arithmetic, enabled MMU or OS qualification is claimed. Undefined CAS2 overlapping-memory results, MOVES storing its own updated address base, reserved module fields and simultaneous T1/T0 are excluded. + +## Milestone 6 reference work + +The pinned Musashi command now audits all selected profiles across both integer +program directories. It records passing/excluded programs and input identities; +missing/incomplete fixtures and mismatches fail. See +[reference qualification](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md) +for the 538 passing programs, 86 explicit exclusions, 040 T0 correction and +ASL regression replacement proof. Run the consolidation mutations with +`./scripts/test-copper68k-synthetic-mutations.ps1 -Scope Consolidation`. +Milestone 6 remains in progress; remaining implementation/reference gaps are +listed explicitly in that document. diff --git a/Copper68k.Tests/Synthetic/SyntheticBitShiftTests.cs b/Copper68k.Tests/Synthetic/SyntheticBitShiftTests.cs index 4d70e2be..217e3a9b 100644 --- a/Copper68k.Tests/Synthetic/SyntheticBitShiftTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticBitShiftTests.cs @@ -29,6 +29,13 @@ public void ShiftsCountsValuesAndFlags(string modelId) if (machine.Model.FullIndex) foreach (var family in LogicalSpecification.Shifts) foreach (var index in IndexFixture.FullStructures()) Shift(machine, report, family, 2, new(6, 0), 0x8001, 1, 31, false, index: index); + // Preserve the original ASL sign-change defect input while sharing expectations/verification. + machine.Reset(0, false); machine.Core.State.D[2] = 0x6891c884; + var historical = SyntheticExecution.Prepare(machine, [0xe302]); + var historicalResult = LogicalSpecification.Shift("ASL", 0x84, 1, 1, historical.Sr); + historical.D[2] = (historical.D[2] & 0xffffff00) | historicalResult.Value; + historical.Sr = historicalResult.Sr; + SyntheticExecution.Run(machine, historical, report, $"{modelId}/ASL/1/D2/historical-sign-change/op=E302/value=6891C884/count=1/ccr=00"); report.Complete(output); } diff --git a/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs b/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs index 08d4b1a9..46a8f2bd 100644 --- a/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs @@ -78,7 +78,7 @@ public void TraceRetirementTakenAndUntakenFlowStopsAndAbortingFaults(string mode { if (family == "TRAP" || family == "TRAPV" && (ccr & 2) != 0) { SyntheticExecution.ExpectException(m, e, family == "TRAP" ? 35 : 7, e.Pc); flow = true; } - if (trace == 0x8000 || flow) SyntheticExecution.ExpectException(m, e, 9, e.Pc); + if (trace == 0x8000 || flow || family == "NOP" && modelId == "68040") SyntheticExecution.ExpectException(m, e, 9, e.Pc); } SyntheticExecution.Run(m, e, report, $"{modelId}/trace-{family}/none/T={trace:X4}/super={supervisor}/op={words[0]:X4}/ccr={ccr:X2}", false); } @@ -88,7 +88,7 @@ public void TraceRetirementTakenAndUntakenFlowStopsAndAbortingFaults(string mode { m.Reset(31); var e = SyntheticExecution.Prepare(m, selfBranch ? [0x60fe] : new ushort[] { 0x4e71, 0x60fc }); e.Sr |= (ushort)trace; - var tracedAddress = selfBranch ? SyntheticMachine.Code : afterFirst || trace == 0x4000 ? SyntheticMachine.Code + 2 : SyntheticMachine.Code; + var tracedAddress = selfBranch ? SyntheticMachine.Code : afterFirst || trace == 0x4000 && modelId != "68040" ? SyntheticMachine.Code + 2 : SyntheticMachine.Code; var savedPc = tracedAddress == SyntheticMachine.Code && !selfBranch ? SyntheticMachine.Code + 2 : SyntheticMachine.Code; SyntheticExecution.ExpectException(m, e, 9, savedPc); if (m.Model.FullIndex) e.Write(e.A[7] + 8, tracedAddress, 4, m.Model); @@ -97,8 +97,63 @@ public void TraceRetirementTakenAndUntakenFlowStopsAndAbortingFaults(string mode var mismatch = e.Verify(m); report.Record($"{modelId}/trace-batch/none/T={trace:X4}/afterFirst={afterFirst}/selfBranch={selfBranch}", mismatch == null ? "passing" : "mismatching", mismatch); } + TraceSerializingIntegerInstructions(m, report); report.Complete(output); } + private static void TraceSerializingIntegerInstructions(SyntheticMachine m, CoverageBatch report) + { + // MC68040UM 8.2.6 lists these integer serializers as T0 tracing events. + // TAS is deliberately absent: its external branch signal is not a T0 event. + // MC68020/030 retain normal-flow semantics for this set; 060 has no T0. + foreach (var trace in m.Model.FullIndex && m.Model.Id != "68060" ? new[] { 0x8000, 0x4000 } : new[] { 0x8000 }) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var family in new[] { "NOP", "MOVES", "CAS", "CAS2", "MOVEfromUSP", "MOVEtoUSP", "MOVECfrom", "MOVECto", "CINV", "CPUSH", "TAS" }) + { + m.Reset(ccr, supervisor); + m.Core.State.D[0] = 0x8000; m.Core.State.D[1] = 0x96c3; + m.Core.State.D[2] = 0x8000; m.Core.State.D[3] = 0x7fff; + m.InitializePhysical(0x4000, 0x8000, 2); m.InitializePhysical(0x4100, 0x7fff, 2); + ushort[] words = family switch + { + "NOP" => [0x4e71], "MOVES" => [0x0e50, 0], "CAS" => [0x0cd0, 0x0040], + "CAS2" => [0x0cfc, 0x8102, 0x9143], "MOVEfromUSP" => [0x4e69], "MOVEtoUSP" => [0x4e61], + "MOVECfrom" => [0x4e7a, 0x0001], "MOVECto" => [0x4e7b, 0x0001], + "CINV" => [0xf498], "CPUSH" => [0xf4b8], _ => [0x4ad0] + }; + var e = SyntheticExecution.Prepare(m, words); + m.Core.State.SourceFunctionCode = 1; m.Core.State.DestinationFunctionCode = 5; + m.Core.State.StatusRegister |= (ushort)trace; e.Sr |= (ushort)trace; + var vector = family switch + { + "CAS" when !m.Model.FullIndex => 4, + "CAS2" when !m.Model.FullIndex => 4, + "CAS2" when m.Model.Id == "68060" => 61, + "MOVES" or "MOVECfrom" or "MOVECto" when m.Model.Id == "68000" => 4, + "CINV" or "CPUSH" when m.Model.Id is not ("68040" or "68060") => 11, + "MOVES" or "MOVEfromUSP" or "MOVEtoUSP" or "MOVECfrom" or "MOVECto" or "CINV" or "CPUSH" when !supervisor => 8, + _ => 0 + }; + if (vector != 0) SyntheticExecution.ExpectException(m, e, vector); + else + { + switch (family) + { + case "CAS": e.Write(0x4000, e.D[1], 2, m.Model); e.Sr = (ushort)((e.Sr & 0xfff0) | 4); break; + case "CAS2": + e.Write(0x4000, e.D[4], 2, m.Model); e.Write(0x4100, e.D[5], 2, m.Model); + e.Sr = (ushort)((e.Sr & 0xfff0) | 4); break; + case "MOVEfromUSP": e.A[1] = 0x7800; break; + case "MOVEtoUSP": e.InactiveStackPointer = e.A[1]; break; + case "MOVECfrom": e.D[0] = 5; break; + case "MOVECto": e.ControlChecks["DFC"] = (s => s.DestinationFunctionCode, e.D[0] & 7); break; + case "TAS": e.Write(0x4000, 0x80, 1, m.Model); e.Sr = (ushort)((e.Sr & 0xfff0) | 8); break; + } + if (trace == 0x8000 || m.Model.Id == "68040" && family != "TAS") SyntheticExecution.ExpectException(m, e, 9, e.Pc); + } + SyntheticExecution.Run(m, e, report, $"{m.Model.Id}/trace-serializer-{family}/T={trace:X4}/super={supervisor}/ccr={ccr:X2}", false); + } + } private sealed class TraceBoundary(M68kCpuState state, ushort trace, bool afterFirst) : IM68kInstructionBoundary { private int retired; diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index 3fb474f4..c4eb1a1a 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -4379,6 +4379,15 @@ private void ExecuteInstructionCore() (traceOpcode & 0xFFC0) is 0x4E80 or 0x4EC0 or 0x46C0 || traceOpcode is 0x4E72 or 0x4E73 or 0x4E74 or 0x4E75 or 0x4E77 or 0x007C or 0x027C or 0x0A7C || (traceOpcode & 0xFFC0) == 0x06C0; + // MC68040UM 8.2.6: serializers also trigger T0 tracing on 040. + // This is distinct from the external branch-status list (which includes TAS). + if (_profile.Model == M68kAcceleratorModel.M68040) + flow |= traceOpcode is 0x4E71 or 0x4E7A or 0x4E7B || + (traceOpcode & 0xFFF0) == 0x4E60 || + (traceOpcode & 0xFFC0) is 0x0AC0 or 0x0CC0 or 0x0EC0 || + (traceOpcode & 0xFF00) == 0x0E00 && (traceOpcode & 0x00C0) != 0x00C0 || + (traceOpcode & 0xFF20) is 0xF400 or 0xF420 || + (traceOpcode & 0xFFE0) == 0xF500 || (traceOpcode & 0xFFD8) == 0xF548; } ExecuteInstructionBody(); var exception = State.ExceptionSequence != serial; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md new file mode 100644 index 00000000..0fbe4efe --- /dev/null +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -0,0 +1,138 @@ +# Synthetic suite reference qualification + +Milestone 6 is in progress. This is scoped software-reference evidence, not +exhaustive external coverage, physical CPU qualification or desktop readiness. +68010 and 68060 remain diagnostic profiles. + +## Sources and reproducibility + +The [Motorola programmer reference](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) +and applicable processor manuals establish architectural expectations. Independent +software corpora are useful for finding disagreements; their assertions require +review when they disagree with documented behavior. + +The reused Musashi adapter executes self-checking programs from +[`kstenerud/Musashi`](https://github.com/kstenerud/Musashi/tree/72c1d74800f3087b45a0c1a7342601bbed898881/test), +pinned to `72c1d74800f3087b45a0c1a7342601bbed898881`. It does not execute the +Musashi CPU as a differential oracle. Each requested profile uses the public CPU +factory, including the A1200 EC020 factory. Both complete fixture directories +(60 mc68000 programs and 18 mc68040 programs) are required. Reports distinguish +passing, mismatching and explicitly excluded programs, record input SHA-256 +identities and retired instruction counts, and retain recent retirement diagnostics. +Program counts do not imply coverage of every instruction/address combination. + +```powershell +git clone https://github.com/kstenerud/Musashi artifacts/reference-musashi +git -C artifacts/reference-musashi checkout 72c1d74800f3087b45a0c1a7342601bbed898881 +./scripts/test-copper68k-synthetic.ps1 -Deep -Seed 68020 -Samples 10000 -MusashiPath artifacts/reference-musashi +``` + +Requested audits fail for missing fixture directories, incomplete selections, +unknown/duplicate model IDs or any mismatch. The command rejects modified pinned +inputs. SHA-256 identities are written to `musashi-inputs.json` and the per-program +`musashi-model-audit.json`; selected profiles must each execute programs. + +| Profile | Passing programs | Explicit exclusions | +| --- | ---: | ---: | +| 68000 | 56 | 22 | +| 68010 | 56 | 22 | +| 68EC020 | 72 | 6 | +| 68020 | 72 | 6 | +| 68030 | 72 | 6 | +| 68040 | 72 | 6 | +| 68060 | 66 | 12 | +| A1200 EC020 | 72 | 6 | +| Total | 538 | 86 | + +Four mc68000 fixtures retain the previous invalid-BCD/undefined-DIV-flags caveats. +000/010 exclude the advanced directory because its programs assume 020+ +instructions without unavailable-instruction handlers. All advanced profiles +exclude `cmp2.bin`'s conflicting carry assertion and `chk2.bin`: its final +`CHK2.W (A7),A0` at binary offset 0xEC (`02D7 8800`) uses long bounds +`10000000/70000000` but expects a long-sized out-of-range trap. Word-sized bounds +are instead `1000/0000`, sign-extended and compared against all 32 bits of A0; +the wrapped interval contains `70000001`. The binary agrees with its unsuffixed +source instruction, not the intended long operation. Inputs remain unchanged. +060 additionally excludes MOVEP, CHK2, CAS2 and 64-bit multiply/divide programs +without software handlers, and the interrupt program's required MSP/format-1 pair. + +## Disagreement found and corrected + +[MC68040UM section 8.2.6](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +lists NOP, MOVES, CAS/CAS2, MOVE USP, MOVEC and cache/MMU serializers among T0 +events. The earlier suite used 020/030 flow rules for 040. New scenarios reproduced +481 mismatches on 040 and none on the other profiles. Correcting the 040 classifier +passes all selected profiles. Tests include all initial CCRs, both privilege modes, +aborting exceptions and batch-boundary trace changes. TAS remains a negative case: +the external branch-status list differs from the architectural T0 list. +FPU tracing and enabled-MMU execution remain outside this roadmap. + +## Consolidation proof + +`M68kShiftTests.AslByteSetsOverflowWhenSignChanges` is replaced by +`SyntheticBitShiftTests.ShiftsCountsValuesAndFlags`. The named +`/ASL/1/D2/historical-sign-change/op=E302/value=6891C884/count=1/ccr=00` +scenario preserves its input and assertions through shared independent expectations. +It also checks exact PC, untouched registers/memory and a following sentinel on +all eight profiles. The larger shift matrix covers all CCRs and counts. + +Before retirement, the `000-asl-overflow` mutation disabled the production +68000 overflow update. Both the original regression and the synthetic replacement +failed in the same run. `040-t0-serializers` separately removes the new classifier +and reproduces the trace failure. Evidence is under +`artifacts/m6-mutation-proof-verified/`; `mutation-proof.json` records +`originalRegressionDetected=true` for the former and exact failing case IDs. +Sources are restored and rebuilt by the mutation command. + +```powershell +./scripts/test-copper68k-synthetic-mutations.ps1 -Scope Consolidation +``` + +Only this pure semantic duplicate is retired. Timing-policy, cache, prefetch, +bus ordering, detailed fault sequencing, JIT and native ROM/media tests remain. + +## Remaining qualification and implementation gaps + +- Existing SingleStepTests and WinUAE adapters remain available; no new external + corpus execution is claimed for them in this slice. WinUAE integer integration + currently selects 68000; multi-model native bridge/fixtures still need qualification. +- RTE internal restart remains untested: 010 format 8, 020/030 formats 9/A/B, + and 040 format 7. The current advanced decoder does not implement all these + legal restoration protocols; this is an implementation gap, not invalid encoding. +- Nested 000 address errors during exception stacking can recurse on an odd SSP. + The eventual fix needs documented double-fault halt behavior and fault-bus tests. +- 040 PFLUSH currently overconsumes an extension word; the PTEST mask comparison + is unreachable and its decoder also assumes an extension. Motorola's encodings + in PRM sections 6-35/6-71 are single-word instructions. These need a separate + correction, privilege checks and selected disabled-MMU semantic coverage. +- External BKPT replacement, physical MOVES function-code spaces, LPSTOP + CPU-space broadcast and real CALLM/RTM access-control responses remain unqualified. +- Exhaustive independent addressing references, hardware captures, physical + cache/pipeline timing, enabled MMU and FPU arithmetic are not claimed here. + +Milestone 6 remains open until the planned remaining reference work and review +are completed. Published packages are immutable; publication is a separate release. + +## Validation checkpoint + +The full CPU suite passes 4,593 tests with six optional external-reference skips. +The report gate passes all 8,333,190 deterministic cases in 338 batches, plus +320,000 seeded cases (seed 68020, 10,000 per profile/group). The pinned Musashi +audit passes 538 programs; 86 exclusions remain explicit. A requested missing +fixture audit fails as expected. AHX passes 18 consumer tests. + +Private package `1.5.2-synthetic-dev.33` has SHA-256 +`817c0bc8a27e9ec0ad768eeb9b3c39c93f2f487a6fcf978716ab14ee47c127e6`. +The isolated CopperScreen consumer at baseline `d9beae8` resolves that package +through NuGet, builds Release with zero warnings/errors, and passes 149 host, +74 disk and 1,080 separately built diagnostic tests. Six optional host/media +skips remain unavailable coverage. Native replay separately passes Workbench 3.1 +at 0/2 MiB Fast RAM and the A1200 eight-plane hard-disk boot with persistence +across a desktop reopen (three executed cases, no skips). These are correctness +results, not throughput or physical-timing qualification. Root consumer working +changes remain outside this validation checkout; the package is not published. + +Evidence: `artifacts/m6-final/`, `artifacts/m6-ahx/`, +`artifacts/m6-mutation-proof-verified/`, `artifacts/m6-negative-inputs/`, +`artifacts/synthetic-private-feed-33/`, and the isolated consumer's +`artifacts/m6-validation/`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 8fbc609a..3ba6ff91 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -22,7 +22,7 @@ model. A gap report alone does not complete it.** | 3. Arithmetic and comparison | Complete | ADD/SUB variants, quick/immediate/address/extend forms, comparisons, multiply/divide, decimal/packing operations; overflow, borrow, carry, sticky zero, exceptional operands. | | 4. Logical, bit and shift operations | Complete: semantic gate, 2026-10-04 | Logical/immediate/unary operations, bit manipulation, shifts/rotates, bitfields, atomic integer operations; preservation and memory effects. | | 5. Control and system operations | Complete: scoped semantic gate, 2026-10-04 | Branches, conditions, calls/returns, stack frames, traps, privilege-sensitive transfers, STOP/RESET, model-specific integer/system instructions; exception frames, saved PC/SR, stack selection, interrupt/trace. | -| 6. Reference qualification and consolidation | Planned | Independent reference audits across selected models, gap review, retire proven redundant tests; publish architectural combination coverage, not just xUnit counts. | +| 6. Reference qualification and consolidation | In progress | Independent reference audits across selected models, gap review, retire proven redundant tests; publish architectural combination coverage, not just xUnit counts. | For every promoted family, mismatches and emulator-level unsupported execution fail its gate. Documented processor exceptions (for example unimplemented integer @@ -316,11 +316,11 @@ Evidence directories: `artifacts/m3-final/`, `artifacts/m3-seeded/`, physical timing qualification remain separate; no new physical timing or OS compatibility qualification is claimed. -## Milestones 4 and 5 implementation checkpoint — 2026-10-04 +## Milestones 4 and 5 implementation checkpoint — 2026-10-04 The logical/bit/shift/atomic matrix adds **4,073,024 deterministic cases** in 64 batches. Control/system adds **1,305,838 cases** in 128 batches. Together -with milestones 1–3, the required semantic gate is **8,324,030 cases** in +with milestones 1–3, the required semantic gate is **8,324,030 cases** in **338 xUnit batches** across all seven models and the A1200 EC020 profile. Every required named batch must exist with its exact count and zero mismatching, unsupported or untested cases before promotion. Architectural unavailable @@ -469,3 +469,32 @@ and the isolated consumer's `artifacts/m4-m5-validation/`. Reproduce with: Cross-model independent external reference qualification and consolidation remain milestone 6. Package publication remains a separately authorized release action. + +## Milestone 6 progress: independent program audit and first consolidation + +The reused Musashi adapter now executes both pinned integer directories across +all eight profiles: 538 program/profile combinations pass; 86 are explicitly +excluded for processor availability or reviewed fixture assertions. Input +SHA-256 identities, exclusions and execution counts are reported separately +from architectural combination coverage. Missing or incomplete requested inputs +fail the audit. The synthetic deterministic gate now has 8,333,190 logical +cases in the same 338 batches, including additional trace scenarios and an +original ASL defect input preserved across every profile. + +Motorola manual review corrected the 040 T0 serializer rule after reproducing +481 mismatches. Two new isolated mutations detect the trace defect and 000 ASL +overflow loss. The original ASL regression and its synthetic replacement both +failed under the same mutation before retiring that pure semantic duplicate. +All specialized timing, prefetch, cache, fault, JIT and native tests remain. + +See [reference qualification and consolidation evidence](COPPER68K_REFERENCE_QUALIFICATION.md) +for pins, exclusions, replacement identifiers and remaining implementation gaps. +Milestone 6 is still in progress: internal RTE restart, double-fault halting, +040 PFLUSH/PTEST decoding and broader external references remain open. +Package publication remains a separately authorized release action. + +Validation passes the full CPU suite (4,593 tests; six optional skips), the +deterministic/seeded report gates, AHX and isolated CopperScreen consumers using +unpublished private package `1.5.2-synthetic-dev.33`. Native Workbench and A1200 +boot/persistence replays execute three cases without skips. Counts, package hash, +evidence locations and qualification boundaries are in the reference document. diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index 6f6906c6..18aadfe8 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -91,10 +91,27 @@ $mutations += @( @{name='move16-postincrement'; file='Copper68k/M68kAdvancedTimingInterpreter.System.cs'; before=' if (form == 4 || form < 2) WriteGeneralRegister(true, register, unchecked(State.A[register] + 16));'; after=' if (form == 4 || form < 2) WriteGeneralRegister(true, register, unchecked(State.A[register] + 4));'; group='Move16CacheInstructionsBreakpointsAndLowPowerStop'; milestone=5; model='68040'}, @{name='cacr-clear-readback'; file=$advanced; before='? value & ~0x0C0Cu : value; // Clear commands always read as zero.'; after='? value : value; // Mutant: clear commands read back set.'; group='MovecControlInventoryMasksPrivilegeAndAllGeneralRegisters'; milestone=5} ) +$mutations += @( + @{name='000-asl-overflow'; file='Copper68k/M68kCore.cs'; before=@' + State.SetFlag(M68kCpuState.Overflow, shifted.Overflow); + return shifted.Value; +'@; after=@' + State.SetFlag(M68kCpuState.Overflow, false); + return shifted.Value; +'@; group='ShiftsCountsValuesAndFlags'; milestone=6; model='68000'; legacyTest='AslByteSetsOverflowWhenSignChanges'; legacyFile='Copper68k.Tests/M68kShiftTests.cs'}, + @{name='040-t0-serializers'; file=$advanced; before=@' + if (_profile.Model == M68kAcceleratorModel.M68040) + flow |= traceOpcode is 0x4E71 or 0x4E7A or 0x4E7B || +'@; after=@' + if (false) + flow |= traceOpcode is 0x4E71 or 0x4E7A or 0x4E7B || +'@; group='TraceRetirementTakenAndUntakenFlowStopsAndAbortingFaults'; milestone=6; model='68040'} +) if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } if ($Scope -eq 'Logical') { $mutations = @($mutations | Where-Object { $_.milestone -eq 4 }) } if ($Scope -eq 'Control') { $mutations = @($mutations | Where-Object { $_.milestone -eq 5 }) } +if ($Scope -eq 'Consolidation') { $mutations = @($mutations | Where-Object { $_.milestone -eq 6 }) } $saved = @{} foreach ($mutation in $mutations) { $path = Join-Path $repo $mutation.file @@ -117,13 +134,20 @@ try { try { [IO.File]::WriteAllText($path, $text.Replace($before, $after), [Text.UTF8Encoding]::new($false)) $model = $(if ($mutation.model) { $mutation.model } elseif ($mutation.name -eq '060-divide-frame') { '68060' } else { '68020' }) - & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~$($mutation.group)&DisplayName~$model" --logger "trx;LogFileName=mutation.trx" --results-directory $directory *> (Join-Path $directory 'run.log') + $filter = "FullyQualifiedName~$($mutation.group)&DisplayName~$model" + $legacyPresent = $mutation.legacyTest -and [IO.File]::ReadAllText((Join-Path $repo $mutation.legacyFile)).Contains("void $($mutation.legacyTest)(") + if ($legacyPresent) { $filter += "|FullyQualifiedName~$($mutation.legacyTest)" } + & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --no-restore --filter $filter --logger "trx;LogFileName=mutation.trx" --results-directory $directory *> (Join-Path $directory 'run.log') $exitCode = $LASTEXITCODE $batches = @(Get-ChildItem -LiteralPath $directory -Filter '*.json' | ForEach-Object { Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json }) $failures = @($batches | ForEach-Object { $_.failures }) if ($exitCode -eq 0 -or $batches.Count -eq 0 -or $failures.Count -eq 0) { throw "Mutation survived or failed without executable semantic evidence: $($mutation.name)" } + if ($legacyPresent) { + [xml]$legacyTrx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + if (@($legacyTrx.TestRun.Results.UnitTestResult | Where-Object { $_.testName.EndsWith($mutation.legacyTest) -and $_.outcome -eq 'Failed' }).Count -ne 1) { throw 'Original regression did not detect the same mutation' } + } $results += @{mutation=$mutation.name; file=$mutation.file; sourceSha256=(Get-FileHash -LiteralPath $path).Hash; - detected=$true; replacementCase=$failures[0].id; diagnostic=$failures[0].reason} + detected=$true; replacementCase=$failures[0].id; diagnostic=$failures[0].reason; originalRegressionDetected=$legacyPresent} Write-Host "Detected $($mutation.name): $($failures[0].id)" } finally { [IO.File]::WriteAllBytes($path, $saved[$path]) } diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 4fd07ab2..bf5f986c 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -80,7 +80,7 @@ try { 'logical-boundaries'=31808; 'logical-cas'=$(if ($model -in @('68000','68010')) {36975} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {37371}); 'logical-cas2'=36864; - 'logical-shifts'=$(if ($model -in @('68000','68010')) {130560} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {131088}); + 'logical-shifts'=$(if ($model -in @('68000','68010')) {130561} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {131089}); 'system-basic'=1728; 'system-bounds'=$(if ($model -in @('68000','68010')) {11828} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {12488}); 'system-callm'=$(if ($model -in @('68000','68010')) {10832} elseif ($model -in @('68020','68EC020','A1200')) {11000} elseif ($model -in @('68030','68040','68060')) {10964}); @@ -92,7 +92,7 @@ try { 'system-rtm'=24594; 'system-stack'=6080; 'system-status'=$(if ($model -in @('68000','68010')) {16540} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {16936}); - 'system-trace'=$(if ($model -in @('68000','68010','68060')) {580} elseif ($model -in @('68020','68030','68040','68EC020','A1200')) {1160})} + 'system-trace'=$(if ($model -in @('68000','68010','68060')) {1284} elseif ($model -in @('68020','68030','68040','68EC020','A1200')) {2568})} foreach ($group in $expected.Keys) { $report = Get-Content -LiteralPath (Join-Path $output "$model-$group.json") -Raw | ConvertFrom-Json if ($report.logicalCases -ne $expected[$group] -or $report.counts.passing -ne $expected[$group] -or @@ -126,15 +126,18 @@ try { $resolved = Get-InputIdentity 'musashi' $MusashiPath '*.bin' $revision = (& git -C $resolved rev-parse HEAD).Trim() if ($revision -ne '72c1d74800f3087b45a0c1a7342601bbed898881') { throw 'Musashi source revision does not match adapter pin' } - Set-AuditEnvironment 'COPPER68K_RUN_MUSASHI_M68000' '1' - Set-AuditEnvironment 'COPPER68K_MUSASHI_M68000_PATH' $resolved - Set-AuditEnvironment 'COPPER68K_MUSASHI_M68000_FILTER' 'move' - Set-AuditEnvironment 'COPPER68K_MUSASHI_M68000_LIMIT' '0' - Set-AuditEnvironment 'COPPER68K_MUSASHI_M68000_CPU_MODEL' '68000' - Set-AuditEnvironment 'COPPER68K_MUSASHI_M68000_BACKEND' 'interpreter' - Set-AuditEnvironment 'COPPER68K_MUSASHI_M68000_INCLUDE_KNOWN_INCOMPATIBLE' '0' - Invoke-Tests 'FullyQualifiedName~MusashiM68000ProgramsPassInterpreterWhenEnabled' 'musashi-move-68000' - $references += 'Musashi: pinned 68000 MOVE programs; secondary software oracle' + & git -C $resolved diff --quiet HEAD -- test + if ($LASTEXITCODE -ne 0) { throw 'Musashi reference inputs differ from the pinned source' } + if (-not (Test-Path -LiteralPath (Join-Path $resolved 'test/mc68000'))) { throw 'Musashi model audit requires the repository root, not one corpus subfolder' } + Set-AuditEnvironment 'COPPER68K_RUN_MUSASHI_MODEL_AUDIT' '1' + Set-AuditEnvironment 'COPPER68K_MUSASHI_MODEL_AUDIT_PATH' $resolved + Invoke-Tests 'FullyQualifiedName~MusashiIntegerProgramsAcrossSelectedModelsWhenEnabled' 'musashi-model-audit' + $audit = Get-Content -LiteralPath (Join-Path $output 'musashi-model-audit.json') -Raw | ConvertFrom-Json + if ($audit.mismatching -ne 0 -or $audit.passing -eq 0 -or $audit.rows.Count -ne 78 * $Models.Count) { throw 'Incomplete Musashi program audit' } + foreach ($model in $Models) { + if (@($audit.rows | Where-Object { $_.Model -ceq $model -and $_.Status -eq 'passing' }).Count -eq 0) { throw "Missing passing reference selection for $model" } + } + $references += "Musashi: pinned independent integer programs across $($Models -join ','); $($audit.passing) passed, $($audit.excluded) explicitly excluded; software assertions, not hardware qualification" } if ($WinUaePath) { $resolved = Get-InputIdentity 'winuae' $WinUaePath '*.gz' @@ -158,7 +161,7 @@ try { $inventory | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'qualified-inventory.json') @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=338; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; roadmapComplete=$false; seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); - externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Other models remain unqualified by these adapters'}); + externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); unavailableSystemCoverage=@('RTE bus-fault internal restart on 010/020/030', '020/030 coprocessor midinstruction restoration', '040 access-fault pending trace/writeback restoration', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast'); timing='Semantic gate; timing policy and physical qualification remain separate'} | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'summary.json') From c4b924f7704ec529d7708827fe459e8c154ab8ad Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Sun, 4 Oct 2026 16:17:09 +0300 Subject: [PATCH 004/211] Correct 68040 single-word MMU decoding and privilege checks --- Copper68k.Tests/M68040MmuTests.cs | 130 +++++++++++++++++- Copper68k.Tests/Synthetic/README.md | 11 +- .../SyntheticM68040MmuInstructionTests.cs | 86 ++++++++++++ Copper68k/M68040Support.cs | 46 +++++-- Copper68k/M68kJitCore.cs | 2 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 66 ++++++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 +- scripts/test-copper68k-synthetic.ps1 | 4 +- 8 files changed, 336 insertions(+), 24 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040MmuInstructionTests.cs diff --git a/Copper68k.Tests/M68040MmuTests.cs b/Copper68k.Tests/M68040MmuTests.cs index 66ac03bf..2d7a82e9 100644 --- a/Copper68k.Tests/M68040MmuTests.cs +++ b/Copper68k.Tests/M68040MmuTests.cs @@ -1,3 +1,5 @@ +using Copper68k.Tests.Synthetic; + namespace Copper68k.Tests; public sealed class M68040MmuTests @@ -103,7 +105,7 @@ public void PflushInvalidatesCachedTranslation() bus, 0x1000, 0x2039, 0x0000, 0x2000, // MOVE.L $2000.L,D0 - 0xF500, 0x0000, // PFLUSH + 0xF518, // PFLUSHA (one word) 0x2239, 0x0000, 0x2000); // MOVE.L $2000.L,D1 bus.WriteLong(root + 4, 0x0000_1001); bus.WriteLong(root + 8, 0x0000_5001); @@ -118,12 +120,138 @@ public void PflushInvalidatesCachedTranslation() cpu.ExecuteInstruction(); bus.WriteLong(root + 8, 0x0000_6001); cpu.ExecuteInstruction(); + Assert.Equal(CodeBase + 8, cpu.State.ProgramCounter); cpu.ExecuteInstruction(); Assert.Equal(0x1111_2222u, cpu.State.D[0]); Assert.Equal(0x3333_4444u, cpu.State.D[1]); } + [Fact] + public void PtestUsesDfcAndOpcodeReadWriteBitAndRefreshesTheSelectedAtcEntry() + { + // This fixture checks routing within the retained flat MMU approximation. + // It deliberately makes no claim about real 040 table formats or MMUSR bits. + foreach (var dfc in new uint[] { 1, 2, 5, 6 }) + foreach (var read in new[] { false, true }) + for (var reg = 0; reg < 8; reg++) + { + var bus = new SparseRecordingBus(); + var opcode = (ushort)((read ? 0xf568 : 0xf548) | reg); + bus.Initialize(CodeBase, opcode, 2); + bus.Initialize(CodeBase + 2, 0x747b, 2); + // Flat-table index for logical $80009000: $80009 * 4 = $200024. + const uint userDescriptor = 0x204024, supervisorDescriptor = 0x206024; + bus.Initialize(userDescriptor, 0x5001, 4); + bus.Initialize(supervisorDescriptor, 0x6001, 4); + var cpu = M68kCoreFactory.Default.Create(M68kCpuModel.M68040, bus); + cpu.Reset(CodeBase, StackBase); + cpu.State.DestinationFunctionCode = dfc; + cpu.State.A[reg] = 0x80009000; + cpu.State.M68040Mmu.UserRootPointer = 0x4000; + cpu.State.M68040Mmu.SupervisorRootPointer = 0x6000; + cpu.State.M68040Mmu.TranslationControl = 0x80000000; + cpu.State.M68040Mmu.InstructionTransparentTranslation0 = 0x8000; // low-byte code only + var instructionSpace = dfc is 2 or 6; + var kind = instructionSpace ? M68kBusAccessKind.CpuInstructionFetch : M68kBusAccessKind.CpuDataRead; + var otherKind = instructionSpace ? M68kBusAccessKind.CpuDataRead : M68kBusAccessKind.CpuInstructionFetch; + var supervisor = dfc is 5 or 6; + var mmu = cpu.State.M68040Mmu; + Assert.True(mmu.TryTranslate(0x80009000, kind, false, supervisor, a => bus.Peek(a, 4), out _, out _)); + Assert.True(mmu.TryTranslate(0x80009000, otherKind, false, supervisor, a => bus.Peek(a, 4), out _, out _)); + var descriptor = supervisor ? supervisorDescriptor : userDescriptor; + bus.Initialize(descriptor, 0x7005, 4); // changed mapping, write-protected + bus.Accesses.Clear(); + + cpu.ExecuteInstruction(); + + Assert.Equal(CodeBase + 2, cpu.State.ProgramCounter); + Assert.Equal(-1, cpu.State.LastExceptionVector); + Assert.Single(bus.Accesses.Where(a => a.Address == descriptor && !a.Write && a.Width == 4)); + Assert.DoesNotContain(bus.Accesses, a => a.Address == 0x80009000); + // Probe's fault/result encoding is existing approximation policy. + Assert.Equal(read ? 0u : (supervisor ? 0x308u : 0x108u), mmu.Status); + Assert.True(mmu.TryTranslate(0x80009000, otherKind, false, supervisor, a => bus.Peek(a, 4), out var otherSpace, out _)); + Assert.Equal(supervisor ? 0x6000u : 0x5000u, otherSpace); // unselected I/D entry remains cached + cpu.ExecuteInstruction(); + Assert.Equal(123u, cpu.State.D[2]); + Assert.Equal(CodeBase + 4, cpu.State.ProgramCounter); + } + } + + [Theory] + [InlineData(0xf500)] + [InlineData(0xf508)] + [InlineData(0xf510)] + [InlineData(0xf518)] + [InlineData(0xf548)] + [InlineData(0xf568)] + public void UserMmuInstructionDoesNotFlushOrProbeAtc(int opcode) + { + var bus = new Copper68kTestBus(); + bus.WriteWords(CodeBase, [(ushort)opcode, 0x747b]); + bus.WriteLong(8 * 4, 0x9000); + bus.WriteLong(0x4008, 0x5001); + var cpu = M68kCoreFactory.Default.Create(M68kCpuModel.M68040, bus); + cpu.Reset(CodeBase, StackBase); + cpu.State.SetUserStackPointer(0x7800); + cpu.State.StatusRegister = 0x1f; + cpu.State.DestinationFunctionCode = 1; + cpu.State.A[0] = 0x2000; + var mmu = cpu.State.M68040Mmu; + mmu.UserRootPointer = 0x4000; + mmu.SupervisorRootPointer = 0x4000; + mmu.TranslationControl = 0x80000000; + mmu.InstructionTransparentTranslation0 = 0x8000; + // Flat mappings for supervisor vector and stack accesses during the fault. + bus.WriteLong(0x4000, 0x0001); + bus.WriteLong(0x401c, 0x7001); + Assert.True(mmu.TryTranslate(0x2000, M68kBusAccessKind.CpuDataRead, false, false, bus.ReadLong, out _, out _)); + bus.WriteLong(0x4008, 0x6001); + + cpu.ExecuteInstruction(); + + Assert.Equal(8, cpu.State.LastExceptionVector); + Assert.Equal(CodeBase, cpu.State.LastExceptionStackedProgramCounter); + Assert.True(mmu.TryTranslate(0x2000, M68kBusAccessKind.CpuDataRead, false, false, bus.ReadLong, out var stillCached, out _)); + Assert.Equal(0x5000u, stillCached); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void JitFallbackMmuInstructionsKeepNextInstructionAndPrivilegeBoundary(bool enableV2) + { + foreach (var opcode in new ushort[] { 0xf500, 0xf508, 0xf510, 0xf518, 0xf548, 0xf568 }) + foreach (var supervisor in new[] { false, true }) + { + var bus = new Copper68kTestBus(); + bus.WriteWords(CodeBase, [opcode, 0x747b, 0x4e71]); + bus.WriteLong(8 * 4, 0x9000); + using var jit = M68kJitCore.CreateM68040ForTesting(bus, enableV2); + jit.Reset(CodeBase, StackBase); + jit.State.SetUserStackPointer(0x7800); + jit.State.StatusRegister = (ushort)((supervisor ? 0x2000 : 0) | 0x1f); + jit.State.DestinationFunctionCode = 5; + jit.ExecuteInstruction(); + if (supervisor) + { + Assert.Equal(CodeBase + 2, jit.State.ProgramCounter); + Assert.Equal(0x201f, jit.State.StatusRegister); + jit.ExecuteInstruction(); + Assert.Equal(CodeBase + 4, jit.State.ProgramCounter); + Assert.Equal(123u, jit.State.D[2]); + } + else + { + Assert.Equal(8, jit.State.LastExceptionVector); + Assert.Equal(0x9000u, jit.State.ProgramCounter); + Assert.Equal(CodeBase, bus.ReadLong(StackBase - 6)); + Assert.Equal(0x1f, bus.ReadWord(StackBase - 8)); + } + } + } + [Fact] public void MmuFaultBuildsBusErrorExceptionFrame() { diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 3f71b5d6..dcbd7962 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -16,12 +16,18 @@ From the CopperMod root, run: Milestones 1–5 execute MOVE/MOVEA, transfer/address, arithmetic/comparison, logical/bit/shift/atomic and control/system operations in -**8,333,190 logical cases** across **338 xUnit batches**, across seven models and the A1200 profile. +**8,368,040 logical cases** across **339 xUnit batches**, across seven models and the A1200 profile. Ordinary `dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release` includes these batches. CI additionally validates every required report and exact count; missing reports, mismatches, unsupported execution and empty groups fail the gate. The following instruction sentinel verifies extension consumption. +The additional 040-only `system-mmu-disabled` group qualifies single-word +PFLUSH/PTEST decoding, privilege, CCR preservation and trace behavior with +translation disabled. MMUSR is undefined for disabled-MMU PTEST and is not +asserted. Enabled MMU operation remains outside the integer-family gate; +see the [qualification boundaries](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md). + Reports contain per-scenario identifiers and counts for passing, mismatching, unsupported and untested outcomes. `integer-inventory.json` lists all integer families and their model-specific architectural outcomes. The validated @@ -66,7 +72,8 @@ coverage. Cross-model external qualification remains milestone 6 work. Specifications use [M68000PM](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf), [MC68000UM](https://www.nxp.com/docs/en/reference-manual/MC68000UM.pdf), -[MC68020UM](https://www.nxp.com/docs/en/data-sheet/MC68020UM.pdf) and +[MC68020UM](https://www.nxp.com/docs/en/data-sheet/MC68020UM.pdf), +[MC68040UM](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf), and [MC68060UM](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf). Arithmetic expectations use mathematical ranges and BigInteger multiplication/division, not production semantics. The matrix covers all sizes and legal EA forms, all 32 diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040MmuInstructionTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040MmuInstructionTests.cs new file mode 100644 index 00000000..0b0c434b --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040MmuInstructionTests.cs @@ -0,0 +1,86 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// PRM 6-35/6-36, 6-70/6-71; MC68040UM 3.1.3: PTEST results are +// undefined with TC.E clear, and no table search occurs. This is decoder, +// privilege and trace qualification, not enabled-MMU qualification. +public sealed class SyntheticM68040MmuInstructionTests(ITestOutputHelper output) +{ + private static IEnumerable Opcodes() + { + for (var reg = 0; reg < 8; reg++) + { + yield return (ushort)(0xf500 | reg); // PFLUSHN (An) + yield return (ushort)(0xf508 | reg); // PFLUSH (An) + yield return (ushort)(0xf548 | reg); // PTESTW (An) + yield return (ushort)(0xf568 | reg); // PTESTR (An) + } + yield return 0xf510; // PFLUSHAN; canonical register field + yield return 0xf518; // PFLUSHA; canonical register field + } + + private static string Name(ushort opcode) => opcode switch + { + 0xf510 => "PFLUSHAN", 0xf518 => "PFLUSHA", + _ when (opcode & 0xfff8) == 0xf500 => "PFLUSHN", + _ when (opcode & 0xfff8) == 0xf508 => "PFLUSH", + _ when (opcode & 0xfff8) == 0xf548 => "PTESTW", + _ => "PTESTR" + }; + + [Fact, Trait("Suite", "Synthetic")] + public void SingleWordMmuInstructionsWithTranslationDisabled() + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); + var report = new CoverageBatch("68040", "system-mmu-disabled"); + foreach (var opcode in Opcodes()) + foreach (var dfc in new uint[] { 1, 2, 5, 6 }) + foreach (var supervisor in new[] { false, true }) + foreach (var trace in new[] { 0, 0x4000, 0x8000, 0xc000 }) + for (var ccr = 0; ccr < 32; ccr++) + { + m.Reset(ccr, supervisor); + m.Core.State.StatusRegister |= (ushort)trace; + for (var reg = 0; reg < 7; reg++) m.Core.State.A[reg] = 0x80002003u + (uint)reg * 0x1000; + var e = SyntheticExecution.Prepare(m, [opcode]); + m.Core.State.DestinationFunctionCode = dfc; + m.Core.State.M68040Mmu.UserRootPointer = 0x40004000; + m.Core.State.M68040Mmu.SupervisorRootPointer = 0x40008000; + e.ControlChecks["DFC"] = (s => s.DestinationFunctionCode, dfc); + e.ControlChecks["TC"] = (s => s.M68040Mmu.TranslationControl, 0); + e.ControlChecks["URP"] = (s => s.M68040Mmu.UserRootPointer, 0x40004000); + e.ControlChecks["SRP"] = (s => s.M68040Mmu.SupervisorRootPointer, 0x40008000); + // An is an address to probe/flush, never a memory operand to read. + e.ForbiddenOperandReads.Add(e.A[opcode & 7]); + if (!supervisor) SyntheticExecution.ExpectException(m, e, 8); + else if (trace != 0) SyntheticExecution.ExpectException(m, e, 9, SyntheticMachine.Code + 2); + SyntheticExecution.Run(m, e, report, $"68040/{Name(opcode)}/none/A{opcode & 7}/DFC={dfc}/super={supervisor}/trace={trace:X4}/ccr={ccr:X2}/op={opcode:X4}"); + } + foreach (var opcode in Opcodes()) + { + m.Reset(31); + var e = SyntheticExecution.Prepare(m, [opcode, 0x747b]); // MOVEQ #123,D2 follows immediately + e.Pc = SyntheticMachine.Code + 2; + m.Core.State.DestinationFunctionCode = 5; + var id = $"68040/{Name(opcode)}/none/A{opcode & 7}/following-MOVEQ/op={opcode:X4}"; + try + { + m.Core.ExecuteInstruction(); + var mismatch = e.Verify(m); + if (mismatch == null) + { + m.Core.ExecuteInstruction(); + e.Pc += 2; e.D[2] = 123; e.Sr &= 0xfff0; + mismatch = e.Verify(m); + } + report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); + } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException or UnsupportedM68040InstructionException) + { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); } + } + report.Complete(output); + } +} diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index 29718827..ac646da6 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -1781,6 +1781,9 @@ public void Probe( bool supervisor, Func readPhysicalLong) { + // PTEST searches the tables again, rather than reusing a stale ATC + // entry for the selected user/supervisor and instruction/data space. + _atc.Remove(CreateAtcKey(logicalAddress & 0xFFFF_F000u, accessKind, supervisor)); _ = TryTranslate(logicalAddress, accessKind, write, supervisor, readPhysicalLong, out _, out _); } @@ -2736,29 +2739,48 @@ private uint RaiseLineFControlRegister(uint instructionPc) private bool TryExecuteMmuInstruction(ushort opcode) { - if ((opcode & 0xFFC0) == 0xF500) + if ((opcode & 0xFFE0) == 0xF500) { BeginInstruction(opcode); + var instructionPc = State.ProgramCounter; _ = FetchWord(); - _ = FetchWord(); + if ((State.StatusRegister & M68kCpuState.Supervisor) == 0) + { + RaiseFormat0Exception(8, instructionPc, M68kInstructionTimingKey.PrivilegeViolation); + return true; + } + + // Preserve the existing conservative ATC invalidation policy. + // Selective/global-entry qualification belongs to enabled-MMU work. State.M68040Mmu.Flush(); CompleteTiming(M68kInstructionTimingKey.Movec); return true; } - if ((opcode & 0xFFC0) == 0xF548) + if ((opcode & 0xFFD8) == 0xF548) { BeginInstruction(opcode); + var instructionPc = State.ProgramCounter; _ = FetchWord(); - var extension = FetchWord(); - var write = (extension & 0x0200) != 0; - var address = State.A[opcode & 7]; - State.M68040Mmu.Probe( - address, - M68kBusAccessKind.CpuDataRead, - write, - (State.StatusRegister & M68kCpuState.Supervisor) != 0, - ReadPhysicalLong); + if ((State.StatusRegister & M68kCpuState.Supervisor) == 0) + { + RaiseFormat0Exception(8, instructionPc, M68kInstructionTimingKey.PrivilegeViolation); + return true; + } + + // PTEST has no extension word. R/W is opcode bit 5; DFC, + // rather than the executing supervisor SR, selects the space. + // With TC.E clear there is no table search and MMUSR is undefined. + if (State.M68040Mmu.Enabled) + { + var functionCode = State.DestinationFunctionCode & 7; + State.M68040Mmu.Probe( + State.A[opcode & 7], + (functionCode & 3) == 2 ? M68kBusAccessKind.CpuInstructionFetch : M68kBusAccessKind.CpuDataRead, + (opcode & 0x0020) == 0, + (functionCode & 4) != 0, + ReadPhysicalLong); + } CompleteTiming(M68kInstructionTimingKey.Movec); return true; } diff --git a/Copper68k/M68kJitCore.cs b/Copper68k/M68kJitCore.cs index d6bb55e2..d47ef2ee 100644 --- a/Copper68k/M68kJitCore.cs +++ b/Copper68k/M68kJitCore.cs @@ -2605,7 +2605,7 @@ private bool ShouldFlushM68040JitAfterFallback(uint instructionPc, ushort opcode return true; } - if ((opcode & 0xFFC0) == 0xF500 || (opcode & 0xFF00) == 0xF400) + if ((opcode & 0xFFE0) == 0xF500 || (opcode & 0xFFD8) == 0xF548 || (opcode & 0xFF00) == 0xF400) { return true; } diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 0fbe4efe..eba244ac 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -67,6 +67,36 @@ aborting exceptions and batch-boundary trace changes. TAS remains a negative cas the external branch-status list differs from the architectural T0 list. FPU tracing and enabled-MMU execution remain outside this roadmap. +### 040 MMU instruction decoding follow-up + +PRM sections 6-35/6-36 and 6-70/6-71 specify single-word PFLUSH and PTEST +encodings. The old PFLUSH consumed the following word, and the PTEST mask could +never match. Neither decoder checked privilege. The new disabled-MMU matrix +reproduced **34,850 mismatches** before the correction. + +The correction consumes only the opcode, checks privilege before ATC/probe +effects, and decodes PTEST's read/write bit and DFC space from the documented +fields. PTEST refreshes the selected cached translation before a table search. +JIT fallback invalidation recognizes the same instruction masks, including PTEST. +The existing instruction timing key/policy is preserved. + +The `68040/system-mmu-disabled` gate adds 34,850 cases in one batch: every +address register for page flush/probe forms, canonical PFLUSHA/PFLUSHAN words, +four defined DFC values, all 32 CCR values, user/supervisor execution and all +T0/T1 combinations. Exact next PC, exception frames, untouched registers and +memory, absence of operand reads and following MOVEQ sentinels are checked. +[MC68040UM section 3.1.3](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +specifies no table search with TC.E clear and undefined PTEST results in this +state. The matrix deliberately makes no MMUSR-value assertion. + +Focused enabled-MMU routing/ATC tests exercise the existing **flat-table +approximation**, separately from architectural qualification. PFLUSH still +conservatively flushes the entire ATC; selective page/global preservation, +real table formats, descriptor updates and architectural MMUSR contents remain +unqualified. Undefined DFC values and noncanonical global register fields are +excluded from this scoped gate. No enabled-MMU family is promoted in the integer +inventory, and no specialized regression is retired by this follow-up. + ## Consolidation proof `M68kShiftTests.AslByteSetsOverflowWhenSignChanges` is replaced by @@ -101,10 +131,9 @@ bus ordering, detailed fault sequencing, JIT and native ROM/media tests remain. legal restoration protocols; this is an implementation gap, not invalid encoding. - Nested 000 address errors during exception stacking can recurse on an odd SSP. The eventual fix needs documented double-fault halt behavior and fault-bus tests. -- 040 PFLUSH currently overconsumes an extension word; the PTEST mask comparison - is unreachable and its decoder also assumes an extension. Motorola's encodings - in PRM sections 6-35/6-71 are single-word instructions. These need a separate - correction, privilege checks and selected disabled-MMU semantic coverage. +- 040 enabled-MMU selective/global flushing, real translation-table formats, + descriptor updates and architectural MMUSR contents remain unqualified; + the single-word decoder and disabled-MMU instruction gate are now corrected. - External BKPT replacement, physical MOVES function-code spaces, LPSTOP CPU-space broadcast and real CALLM/RTM access-control responses remain unqualified. - Exhaustive independent addressing references, hardware captures, physical @@ -113,7 +142,7 @@ bus ordering, detailed fault sequencing, JIT and native ROM/media tests remain. Milestone 6 remains open until the planned remaining reference work and review are completed. Published packages are immutable; publication is a separate release. -## Validation checkpoint +## Validation checkpoint: initial reference audit The full CPU suite passes 4,593 tests with six optional external-reference skips. The report gate passes all 8,333,190 deterministic cases in 338 batches, plus @@ -136,3 +165,30 @@ Evidence: `artifacts/m6-final/`, `artifacts/m6-ahx/`, `artifacts/m6-mutation-proof-verified/`, `artifacts/m6-negative-inputs/`, `artifacts/synthetic-private-feed-33/`, and the isolated consumer's `artifacts/m6-validation/`. + +## Validation checkpoint: 040 MMU decoder follow-up + +The ordinary full CPU suite passes **4,601 tests**, with eight optional/opt-in +checks skipped and no failures. The strengthened final focused run passes all +17 batches/tests. Deterministic report validation passes **8,368,040 logical +cases in 339 batches**, including all 34,850 new MMU cases. The pinned Musashi +audit separately executes and passes 538 program/profile combinations, with +86 explicit exclusions. AHX passes 18 tests. Previous reports lacking the new +MMU group are rejected. The prior 320,000 seeded cases are historical evidence; +this bounded follow-up does not claim a new seeded run. + +Private package `1.5.2-synthetic-dev.34` has SHA-256 +`b81870fadf4036d97ec5c2297990fa76e26bfc22add2473e9b381961bbf26bdf`. +The isolated CopperScreen baseline `d9beae8` resolves that exact NuGet version +and builds Release with zero warnings/errors. Host tests pass 149 cases with +six optional/media skips; disk passes 74 and separate engine diagnostics pass +1,080 without skips. Native Workbench 3.1 at 0/2 MiB Fast RAM and A1200 +eight-plane hard-disk boot/reopen persistence separately pass all three cases +without skips. These are correctness replays, not performance qualification. +The package remains unpublished; root CopperScreen working changes are preserved. + +Evidence: `artifacts/mmu-decode-before/` (including the failing matrix and +missing-report rejection), `artifacts/mmu-decode-focused-final/`, +`artifacts/mmu-decode-final/`, `artifacts/mmu-decode-ahx/`, +`artifacts/synthetic-private-feed-34/`, and the isolated consumer's +`artifacts/mmu-decode-validation/` and `artifacts/mmu-decode-diagnostic-tests/`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 3ba6ff91..63112e73 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -489,8 +489,12 @@ All specialized timing, prefetch, cache, fault, JIT and native tests remain. See [reference qualification and consolidation evidence](COPPER68K_REFERENCE_QUALIFICATION.md) for pins, exclusions, replacement identifiers and remaining implementation gaps. -Milestone 6 is still in progress: internal RTE restart, double-fault halting, -040 PFLUSH/PTEST decoding and broader external references remain open. +Milestone 6 is still in progress: internal RTE restart, double-fault halting +and broader external references remain open. The follow-up single-word 040 +PFLUSH/PTEST decoder and privilege correction adds 34,850 disabled-MMU cases, +bringing the current deterministic gate to 8,368,040 cases in 339 batches. +PTEST MMUSR is undefined with translation disabled; enabled-MMU operation and +selective/global flushing remain outside this qualification. Package publication remains a separately authorized release action. Validation passes the full CPU suite (4,593 tests; six optional skips), the @@ -498,3 +502,10 @@ deterministic/seeded report gates, AHX and isolated CopperScreen consumers using unpublished private package `1.5.2-synthetic-dev.33`. Native Workbench and A1200 boot/persistence replays execute three cases without skips. Counts, package hash, evidence locations and qualification boundaries are in the reference document. + +The subsequent 040 decoder slice passes 4,601 ordinary CPU tests (eight opt-in +or optional skips), all 8,368,040 deterministic cases, the pinned cross-model +program audit and AHX. Isolated CopperScreen validation with unpublished +`1.5.2-synthetic-dev.34` passes the Release build, host/disk tests, separate +diagnostics and all three native boot/persistence cases. The reference document +records the package hash, reproduced failures, scoped MMU limits and evidence. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index bf5f986c..3f55f4b3 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -93,6 +93,7 @@ try { 'system-stack'=6080; 'system-status'=$(if ($model -in @('68000','68010')) {16540} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {16936}); 'system-trace'=$(if ($model -in @('68000','68010','68060')) {1284} elseif ($model -in @('68020','68030','68040','68EC020','A1200')) {2568})} + if ($model -eq '68040') { $expected['system-mmu-disabled'] = 34850 } foreach ($group in $expected.Keys) { $report = Get-Content -LiteralPath (Join-Path $output "$model-$group.json") -Raw | ConvertFrom-Json if ($report.logicalCases -ne $expected[$group] -or $report.counts.passing -ne $expected[$group] -or @@ -159,9 +160,10 @@ try { } $inventory | Add-Member -NotePropertyName qualificationBoundaries -NotePropertyValue @('RTE fault restart/internal formats remain untested: 010 frame8 restart, 020/030 frames9/A/B, 040 frame7', 'BKPT external replacement responder is unavailable', 'MOVES physical function-code spaces and LPSTOP CPU-space broadcast are unqualified', 'CALLM/RTM type1 protocol uses an internal synthetic responder; normal public buses have none') $inventory | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'qualified-inventory.json') - @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=338; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; roadmapComplete=$false; + @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=339; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; m68040DisabledMmuInstructionGate='passing'; roadmapComplete=$false; seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); + m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; unavailableSystemCoverage=@('RTE bus-fault internal restart on 010/020/030', '020/030 coprocessor midinstruction restoration', '040 access-fault pending trace/writeback restoration', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast'); timing='Semantic gate; timing policy and physical qualification remain separate'} | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'summary.json') From 3a673bffc44a033d20a2f9a71fa2d511d59e5a8e Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Sun, 4 Oct 2026 17:51:46 +0300 Subject: [PATCH 005/211] Halt 68000 address-error double faults until reset --- .../M68000JitDirectZeroWaitTests.cs | 61 ++++++ Copper68k.Tests/M68kAddressErrorTests.cs | 35 ++++ Copper68k.Tests/Synthetic/README.md | 11 +- .../SyntheticM68000DoubleFaultTests.cs | 115 ++++++++++ Copper68k/M68kCore.cs | 198 +++++++++++------- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 84 +++++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 23 +- scripts/test-copper68k-synthetic.ps1 | 4 +- 8 files changed, 447 insertions(+), 84 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68000DoubleFaultTests.cs diff --git a/Copper68k.Tests/M68000JitDirectZeroWaitTests.cs b/Copper68k.Tests/M68000JitDirectZeroWaitTests.cs index f0df5176..33f68b34 100644 --- a/Copper68k.Tests/M68000JitDirectZeroWaitTests.cs +++ b/Copper68k.Tests/M68000JitDirectZeroWaitTests.cs @@ -4,6 +4,67 @@ namespace Copper68k.Tests; public sealed class M68000JitDirectZeroWaitTests { + [Theory] + [InlineData(false)] + [InlineData(true)] + public void WarmJitTraceFallbackDoubleFaultMatchesInterpreterAndRequiresReset(bool enableV2) + { + foreach (var dispatch in Enum.GetValues()) + foreach (var oddStack in new[] { false, true }) + { + var referenceBus = new DirectZeroWaitBus(); + var jitBus = new DirectZeroWaitBus(); + foreach (var bus in new[] { referenceBus, jitBus }) + { + bus.WriteWords(0x1000, [0x3010, 0x60fc]); // MOVE.W (A0),D0; BRA.S start + bus.WriteWords(0x5000, [0x747b, 0x4e71]); + bus.WriteLongValue(3 * 4, oddStack ? 0x5000u : 0x5001u); + bus.WriteWords(0x2000, [0xcafe]); + } + var reference = new M68kInterpreter(referenceBus, dispatch); + using var jit = new M68kJitCore(jitBus, enableV2: enableV2); + reference.Reset(0x1000, 0x8000); jit.Reset(0x1000, 0x8000); + reference.State.A[0] = jit.State.A[0] = 0x2000; + Assert.Equal(400, reference.ExecuteInstructions(400, 500_000, new BatchBoundary())); + Assert.Equal(400, jit.ExecuteInstructions(400, 500_000, new BatchBoundary())); + Assert.True(enableV2 ? jit.Counters.V2TraceHits > 0 : jit.Counters.TraceHits > 0); + reference.State.A[0] = jit.State.A[0] = 0x2001; + // Architectural trace forces the established interpreter bridge. + // Compiled odd-access dispatch is a separate, unqualified path. + reference.State.StatusRegister = jit.State.StatusRegister = 0xa01f; + reference.State.SetActiveStackPointer(oddStack ? 0x8001u : 0x8000u); + jit.State.SetActiveStackPointer(oddStack ? 0x8001u : 0x8000u); + + Assert.Equal(1, reference.ExecuteInstructions(20, reference.State.Cycles + 1000, new BatchBoundary())); + Assert.Equal(1, jit.ExecuteInstructions(20, jit.State.Cycles + 1000, new BatchBoundary())); + Assert.True(jit.State.Halted); + Assert.False(jit.State.Stopped); + Assert.Equal(reference.State.D, jit.State.D); + Assert.Equal(reference.State.A, jit.State.A); + Assert.Equal(reference.State.ProgramCounter, jit.State.ProgramCounter); + Assert.Equal(reference.State.StatusRegister, jit.State.StatusRegister); + Assert.Equal(reference.State.LastOpcode, jit.State.LastOpcode); + Assert.Equal(reference.State.LastExceptionVector, jit.State.LastExceptionVector); + Assert.Equal(reference.State.LastExceptionStackedProgramCounter, jit.State.LastExceptionStackedProgramCounter); + Assert.Equal(reference.State.LastExceptionStatusRegister, jit.State.LastExceptionStatusRegister); + Assert.Equal(reference.State.Cycles, jit.State.Cycles); // retained machine-cycle policy + Assert.Equal(reference.State.NativeCycles, jit.State.NativeCycles); + Assert.Equal(referenceBus.WordWrites, jitBus.WordWrites); + var stack = jit.State.A[7]; var pc = jit.State.ProgramCounter; + var reads = jitBus.DataReads; var writes = jitBus.WordWrites.Count; + jit.RequestInterrupt(7, 31 * 4); + jit.BeginSubroutine(0x5000, 0x8000, 0x6000); + jit.ExecuteInstruction(); + Assert.True(jit.State.Halted); + Assert.Equal(stack, jit.State.A[7]); Assert.Equal(pc, jit.State.ProgramCounter); + Assert.Equal(reads, jitBus.DataReads); Assert.Equal(writes, jitBus.WordWrites.Count); + jit.Reset(0x5000, 0x8000); + jit.ExecuteInstruction(); + Assert.False(jit.State.Halted); + Assert.Equal(123u, jit.State.D[2]); + } + } + [Theory] [InlineData(false)] [InlineData(true)] diff --git a/Copper68k.Tests/M68kAddressErrorTests.cs b/Copper68k.Tests/M68kAddressErrorTests.cs index 22d80a51..31e1bd1f 100644 --- a/Copper68k.Tests/M68kAddressErrorTests.cs +++ b/Copper68k.Tests/M68kAddressErrorTests.cs @@ -4,6 +4,41 @@ namespace Copper68k.Tests; public sealed class M68kAddressErrorTests { + [Fact] + public void HostRequestedHaltStillAllowsExplicitSubroutineEntry() + { + var bus = new Copper68kTestBus(); + bus.WriteWords(0x4000, 0x747b, 0x4e71); + var cpu = M68kCoreFactory.Default.Create(M68kCpuModel.M68000, bus); + cpu.Reset(0x1000, 0x3000); + cpu.State.Halted = true; // host convention, not a latched architectural double fault + cpu.BeginSubroutine(0x4000, 0x3000, 0x5000); + cpu.ExecuteInstruction(); + Assert.False(cpu.State.Halted); + Assert.Equal(123u, cpu.State.D[2]); + Assert.Equal(0x2ffcu, cpu.State.A[7]); + Assert.Equal(0x5000u, bus.ReadLong(0x2ffc)); + } + + [Fact] + public void OddAddressErrorHandlerHaltsBeforeBuildingAnotherFrame() + { + var bus = new Copper68kTestBus(); + bus.WriteWords(0x1000, 0x3010, 0x4e71); + bus.WriteLong(3 * 4, 0x4001); + var cpu = M68kCoreFactory.Default.Create(M68kCpuModel.M68000, bus); + cpu.Reset(0x1000, 0x3000); + cpu.State.A[0] = 0x2001; + + cpu.ExecuteInstruction(); + + Assert.True(cpu.State.Halted); + Assert.False(cpu.State.Stopped); + Assert.Equal(0x2ff2u, cpu.State.A[7]); // first frame only + Assert.Equal(3, cpu.State.LastExceptionVector); + Assert.Equal(0x4001u, cpu.State.ProgramCounter); + } + [Fact] public void OddWordDataReadRaisesAddressErrorWith68000Frame() { diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index dcbd7962..b45fd1d6 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -16,7 +16,7 @@ From the CopperMod root, run: Milestones 1–5 execute MOVE/MOVEA, transfer/address, arithmetic/comparison, logical/bit/shift/atomic and control/system operations in -**8,368,040 logical cases** across **339 xUnit batches**, across seven models and the A1200 profile. +**8,368,680 logical cases** across **340 xUnit batches**, across seven models and the A1200 profile. Ordinary `dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release` includes these batches. CI additionally validates every required report and exact count; missing reports, mismatches, unsupported execution and empty groups fail the gate. @@ -28,6 +28,15 @@ translation disabled. MMUSR is undefined for disabled-MMU PTEST and is not asserted. Enabled MMU operation remains outside the integer-family gate; see the [qualification boundaries](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md). +The 000-only `system-double-fault` group adds 640 cases for address-error +entry, bad supervisor stacks, odd handler addresses, trap/interrupt entry, +CCR/trace preservation, halted bus inactivity and reset recovery. It preserves +the first frame and verifies that a fault in a successfully entered handler is +a new exception. External BERR and reset-vector fault signaling are unavailable +through the current public bus API. Compiled JIT odd-access dispatch remains a +separate gap; the tested warm-JIT bridge uses architectural trace to force the +established interpreter fallback. + Reports contain per-scenario identifiers and counts for passing, mismatching, unsupported and untested outcomes. `integer-inventory.json` lists all integer families and their model-specific architectural outcomes. The validated diff --git a/Copper68k.Tests/Synthetic/SyntheticM68000DoubleFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68000DoubleFaultTests.cs new file mode 100644 index 00000000..ccd67efe --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68000DoubleFaultTests.cs @@ -0,0 +1,115 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68000UM 6.3.9.1 / 6.3.10: an address error during group-0 +// exception processing halts the processor until external reset. +public sealed class SyntheticM68000DoubleFaultTests(ITestOutputHelper output) +{ + [Fact, Trait("Suite", "Synthetic")] + public void AddressErrorEntryHaltingAndResetRecovery() + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68000")); + var report = new CoverageBatch("68000", "system-double-fault"); + foreach (var kind in new[] { "odd-stack", "odd-handler", "trap-stack", "interrupt-stack", "handler-refault" }) + foreach (var supervisor in new[] { false, true }) + foreach (var trace in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + var id = $"68000/address-error/double-fault/{kind}/super={supervisor}/trace={trace}/ccr={ccr:X2}"; + try + { + m.Reset(ccr, supervisor); + if (trace) m.Core.State.StatusRegister |= 0x8000; + m.Core.State.A[0] = 0x2001; + var handler = kind == "odd-handler" ? 0x9001u : 0x9000u; + m.Bus.Initialize(3 * 4, handler, 4); + m.Bus.Initialize(0x9000, 0x3010, 2); // valid handler instruction may fault again normally + m.Bus.Initialize(0x9002, 0x4e71, 2); + _ = SyntheticExecution.Prepare(m, [(ushort)(kind == "trap-stack" ? 0x4e40 : 0x3010)]); + var oddStack = kind is "odd-stack" or "trap-stack" or "interrupt-stack"; + if (oddStack) + { + if (supervisor) m.Core.State.SetActiveStackPointer(0x4701); + else m.Core.State.SetInterruptStackPointer(0x8001); + } + var e = ArchitecturalExpectation.Capture(m); + var savedSr = e.Sr; + if (!supervisor) + { + var usp = e.A[7]; e.A[7] = e.InactiveStackPointer!.Value; e.InactiveStackPointer = usp; + } + e.Sr = (ushort)((savedSr | 0x2000) & ~0x8000); + e.ExceptionVector = 3; + e.ForbiddenOperandReads.Add(0x2001); + e.Pc = kind == "interrupt-stack" ? SyntheticMachine.Code : SyntheticMachine.Code + 2; + if (oddStack) + e.A[7] -= kind == "trap-stack" ? 8u : kind == "interrupt-stack" ? 10u : 4u; + else + { + AddressFrame(m, e, savedSr, SyntheticMachine.Code + 2); + e.Pc = handler; + } + e.Halted = kind != "handler-refault"; + if (kind == "interrupt-stack") m.Core.RequestInterrupt(7, 31 * 4); + else m.Core.ExecuteInstruction(); + var mismatch = e.Verify(m); + if (mismatch == null && kind == "handler-refault") + { + // Once a valid handler instruction begins, its operand fault + // starts a new exception; it is not a double fault. + AddressFrame(m, e, e.Sr, 0x9002); + m.Core.ExecuteInstruction(); + mismatch = e.Verify(m); + } + if (mismatch == null && e.Halted) + { + var accesses = m.Bus.Accesses.Count; + var lastOpcode = m.Core.State.LastOpcode; + m.Core.ExecuteInstruction(); + m.Core.RequestInterrupt(7, 31 * 4); + m.Core.BeginSubroutine(0x5000, 0x6000, 0x7000); + m.Core.SwitchTaskContext(new M68kCpuState()); + var batched = ((IM68kBatchCore)m.Core).ExecuteInstructions(10, m.Core.State.Cycles + 100, new Boundary()); + mismatch = e.Verify(m); + if (mismatch == null && (batched != 0 || m.Bus.Accesses.Count != accesses || m.Core.State.LastOpcode != lastOpcode)) + mismatch = "Halted CPU executed, accessed the bus or changed its opcode"; + } + if (mismatch == null) + { + m.Bus.Initialize(0x5000, 0x747b, 2); // MOVEQ #123,D2 after external reset + m.Bus.Initialize(0x5002, 0x4e71, 2); + m.Core.Reset(0x5000, 0x8000); + var reset = ArchitecturalExpectation.Capture(m); + reset.Pc = 0x5002; reset.D[2] = 123; + m.Core.ExecuteInstruction(); + mismatch = reset.Verify(m); + } + report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); + } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException) + { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); } + } + report.Complete(output); + } + + private static void AddressFrame(SyntheticMachine m, ArchitecturalExpectation e, ushort savedSr, uint savedPc) + { + // Fixed reference fixture for MOVE.W (A0),D0 with A0=$2001. + // Preserve every word of the first frame when handler entry halts. + e.A[7] -= 14; + e.Write(e.A[7], (savedSr & 0x2000) != 0 ? 0x3015u : 0x3011u, 2, m.Model); + e.Write(e.A[7] + 2, 0x2001, 4, m.Model); + e.Write(e.A[7] + 6, 0x3010, 2, m.Model); + e.Write(e.A[7] + 8, savedSr, 2, m.Model); + e.Write(e.A[7] + 10, savedPc, 4, m.Model); + } + + private sealed class Boundary : IM68kInstructionBoundary + { + public bool BeforeInstruction() => true; + public void AfterInstruction(long previousCycle, long currentCycle) { } + } +} diff --git a/Copper68k/M68kCore.cs b/Copper68k/M68kCore.cs index 241bd453..49cb1307 100644 --- a/Copper68k/M68kCore.cs +++ b/Copper68k/M68kCore.cs @@ -2332,6 +2332,8 @@ private enum PlannedRetireMode : byte private byte _deferredCpuBusBatchAdmissionRetryInstructions; private uint _activeInstructionProgramCounter; private bool _instructionTracePending; + private bool _processingM68000AddressError; + private bool _m68000DoubleFaultHalted; internal bool ArchitecturalTraceEnabled { get; set; } = true; private uint _dataAccessStackedProgramCounter; private ushort? _addressErrorInstructionWord; @@ -9199,6 +9201,8 @@ protected virtual bool TryHandleModelSpecificExceptionFrame( public void Reset(uint programCounter, uint stackPointer) { FlushDeferredCpuTimingBoundary(); + _processingM68000AddressError = false; + _m68000DoubleFaultHalted = false; Array.Clear(State.D); Array.Clear(State.A); State.ProgramCounter = programCounter; @@ -9223,6 +9227,7 @@ public void Reset(uint programCounter, uint stackPointer) public void SwitchTaskContext(M68kCpuState next) { ArgumentNullException.ThrowIfNull(next); + if (_m68000DoubleFaultHalted) return; FlushDeferredCpuTimingBoundary(); State.CopyTaskContextFrom(next); FlushPrefetch(); @@ -9255,6 +9260,9 @@ bool IM68000InterruptRecognition.HasRecognizedInterrupt( public void BeginSubroutine(uint address, uint stackPointer, uint returnAddress) { + // A host subroutine entry may resume an ordinary host-requested halt, + // but a catastrophic group-0 fault requires a CPU reset. + if (_m68000DoubleFaultHalted) return; State.SetActiveStackPointer(stackPointer); PushLong(returnAddress); SetProgramCounterAndFlushPrefetch(address); @@ -9264,7 +9272,7 @@ public void BeginSubroutine(uint address, uint stackPointer, uint returnAddress) public void RequestInterrupt(int level, uint vectorAddress) { - if (level <= 0) + if (level <= 0 || State.Halted) { return; } @@ -9275,70 +9283,78 @@ public void RequestInterrupt(int level, uint vectorAddress) return; } - State.Stopped = false; - // Recognition has already enforced the MC68000 IPL setup window - // against the instruction's late poll. Exception entry begins at - // the live architectural/physical tail; do not charge setup twice. - // A taken Bcc may leave a successor beyond its opcode/IPL fence. - // Retain that chained transfer only once the first target word is - // physically ready; otherwise the accepted interrupt abandons it. - var hasCancellableBranchSuccessor = - _exceptionEntryNotBeforeCycle < _cpuRetireBusCycle; - var preserveStartedBranchSuccessor = - M68000BranchInterruptTransition.HasRetainedCompletedTargetWord( - hasCancellableBranchSuccessor, - _prefetchCount, - _prefetchCompletedCycle0, - State.Cycles); - var interruptFenceCycle = - M68000BranchInterruptTransition.GetExceptionEntryFence( - preserveStartedBranchSuccessor, - _exceptionEntryNotBeforeCycle, - _prefetchCompletedCycle0); - var interruptStartCycle = Math.Max( - State.Cycles + _interruptEntryBranchTailAdjustmentCycles, - interruptFenceCycle); - _interruptEntryBranchTailAdjustmentCycles = 0; - _deferredCpuInstructionTiming?.FlushDeferredCpuInstructionTiming(ref interruptStartCycle); - State.Cycles = interruptStartCycle; - _exceptionEntryNotBeforeCycle = 0; - _cpuBusCycle = preserveStartedBranchSuccessor - ? Math.Max(_cpuBusCycle, interruptStartCycle) - : interruptStartCycle; - _cpuRetireBusCycle = Math.Max(_cpuRetireBusCycle, interruptStartCycle); - var savedStatusRegister = State.StatusRegister; - State.RecordException((int)(vectorAddress / 4), State.ProgramCounter, savedStatusRegister); - State.StatusRegister = (ushort)((savedStatusRegister & ~M68kCpuState.Trace & 0xF8FF) | - ((level & 7) << 8) | - M68kCpuState.Supervisor); - - var stackedProgramCounter = State.ProgramCounter; - var frameFormatWord = GetInterruptFrameFormatWord(vectorAddress); - var stackPointer = State.A[7] - (frameFormatWord.HasValue ? 8u : 6u); - State.SetActiveStackPointer(stackPointer); - - AdvanceInterruptInternalCycles(6); - WriteWord(stackPointer + 4, (ushort)stackedProgramCounter); - PerformInterruptAcknowledge(level); - AdvanceInterruptInternalCycles(4); - WriteWord(stackPointer, savedStatusRegister); - WriteWord(stackPointer + 2, (ushort)(stackedProgramCounter >> 16)); - if (frameFormatWord.HasValue) + try { - WriteWord(stackPointer + 6, frameFormatWord.Value); + State.Stopped = false; + // Recognition has already enforced the MC68000 IPL setup window + // against the instruction's late poll. Exception entry begins at + // the live architectural/physical tail; do not charge setup twice. + // A taken Bcc may leave a successor beyond its opcode/IPL fence. + // Retain that chained transfer only once the first target word is + // physically ready; otherwise the accepted interrupt abandons it. + var hasCancellableBranchSuccessor = + _exceptionEntryNotBeforeCycle < _cpuRetireBusCycle; + var preserveStartedBranchSuccessor = + M68000BranchInterruptTransition.HasRetainedCompletedTargetWord( + hasCancellableBranchSuccessor, + _prefetchCount, + _prefetchCompletedCycle0, + State.Cycles); + var interruptFenceCycle = + M68000BranchInterruptTransition.GetExceptionEntryFence( + preserveStartedBranchSuccessor, + _exceptionEntryNotBeforeCycle, + _prefetchCompletedCycle0); + var interruptStartCycle = Math.Max( + State.Cycles + _interruptEntryBranchTailAdjustmentCycles, + interruptFenceCycle); + _interruptEntryBranchTailAdjustmentCycles = 0; + _deferredCpuInstructionTiming?.FlushDeferredCpuInstructionTiming(ref interruptStartCycle); + State.Cycles = interruptStartCycle; + _exceptionEntryNotBeforeCycle = 0; + _cpuBusCycle = preserveStartedBranchSuccessor + ? Math.Max(_cpuBusCycle, interruptStartCycle) + : interruptStartCycle; + _cpuRetireBusCycle = Math.Max(_cpuRetireBusCycle, interruptStartCycle); + var savedStatusRegister = State.StatusRegister; + State.RecordException((int)(vectorAddress / 4), State.ProgramCounter, savedStatusRegister); + State.StatusRegister = (ushort)((savedStatusRegister & ~M68kCpuState.Trace & 0xF8FF) | + ((level & 7) << 8) | + M68kCpuState.Supervisor); + + var stackedProgramCounter = State.ProgramCounter; + var frameFormatWord = GetInterruptFrameFormatWord(vectorAddress); + var stackPointer = State.A[7] - (frameFormatWord.HasValue ? 8u : 6u); + State.SetActiveStackPointer(stackPointer); + + AdvanceInterruptInternalCycles(6); + WriteWord(stackPointer + 4, (ushort)stackedProgramCounter); + PerformInterruptAcknowledge(level); + AdvanceInterruptInternalCycles(4); + WriteWord(stackPointer, savedStatusRegister); + WriteWord(stackPointer + 2, (ushort)(stackedProgramCounter >> 16)); + if (frameFormatWord.HasValue) + { + WriteWord(stackPointer + 6, frameFormatWord.Value); + } + + var target = ReadLong(GetInterruptVectorAddress(vectorAddress)); + SetProgramCounterAndFlushPrefetch(target); + PrefetchInterruptTarget(target); + var completedCycle = interruptStartCycle + 44; + if (State.Cycles < completedCycle) + { + State.Cycles = completedCycle; + } + + _cpuBusCycle = Math.Max(_cpuBusCycle, State.Cycles); + _cpuRetireBusCycle = Math.Max(_cpuRetireBusCycle, State.Cycles); } - - var target = ReadLong(GetInterruptVectorAddress(vectorAddress)); - SetProgramCounterAndFlushPrefetch(target); - PrefetchInterruptTarget(target); - var completedCycle = interruptStartCycle + 44; - if (State.Cycles < completedCycle) + catch (M68kAddressErrorException) { - State.Cycles = completedCycle; + // Fault entry already completed or halted. Do not resume the + // interrupted exception sequence or expose an internal unwind. } - - _cpuBusCycle = Math.Max(_cpuBusCycle, State.Cycles); - _cpuRetireBusCycle = Math.Max(_cpuRetireBusCycle, State.Cycles); } void IM68kJitFallbackFetchSynchronization.SynchronizeInstructionFetch() @@ -14754,6 +14770,11 @@ private void RaiseAddressError( bool useDataAccessStackedProgramCounter = false) { _instructionTracePending = false; + if (_processingM68000AddressError) + { + HaltM68000DoubleFault(); + return; + } if (TryHandleModelSpecificAddressError( faultAddress, isWrite, @@ -14764,23 +14785,44 @@ private void RaiseAddressError( return; } - var exceptionCycleBase = Math.Max(Math.Max(State.Cycles, _cpuRetireBusCycle), _instructionCycleFloor); - var savedStatusRegister = State.StatusRegister; - var instructionWord = _addressErrorInstructionWord ?? State.LastOpcode; - var frameIsWrite = _addressErrorIsWriteOverride ?? isWrite; - var stackedProgramCounter = accessKind == M68kBusAccessKind.CpuInstructionFetch && - !useDataAccessStackedProgramCounter - ? State.ProgramCounter - : _dataAccessStackedProgramCounter; - State.RecordException(3, stackedProgramCounter, savedStatusRegister); - State.StatusRegister = (ushort)((savedStatusRegister | M68kCpuState.Supervisor) & ~M68kCpuState.Trace); - PushLong(stackedProgramCounter); - PushWord(savedStatusRegister); - PushWord(instructionWord); - PushLong(faultAddress); - PushWord(CreateBusErrorStatusWord(instructionWord, savedStatusRegister, frameIsWrite, accessKind)); - SetProgramCounterAndFlushPrefetch(ReadLong(0x0000_000C)); - AddInstructionCyclesFromBase(exceptionCycleBase, AddressErrorExceptionCycles); + _processingM68000AddressError = !_useM68020BriefIndexedAddressing; + try + { + var exceptionCycleBase = Math.Max(Math.Max(State.Cycles, _cpuRetireBusCycle), _instructionCycleFloor); + var savedStatusRegister = State.StatusRegister; + var instructionWord = _addressErrorInstructionWord ?? State.LastOpcode; + var frameIsWrite = _addressErrorIsWriteOverride ?? isWrite; + var stackedProgramCounter = accessKind == M68kBusAccessKind.CpuInstructionFetch && + !useDataAccessStackedProgramCounter + ? State.ProgramCounter + : _dataAccessStackedProgramCounter; + State.RecordException(3, stackedProgramCounter, savedStatusRegister); + State.StatusRegister = (ushort)((savedStatusRegister | M68kCpuState.Supervisor) & ~M68kCpuState.Trace); + PushLong(stackedProgramCounter); + PushWord(savedStatusRegister); + PushWord(instructionWord); + PushLong(faultAddress); + PushWord(CreateBusErrorStatusWord(instructionWord, savedStatusRegister, frameIsWrite, accessKind)); + SetProgramCounterAndFlushPrefetch(ReadLong(0x0000_000C)); + // Handler entry is still part of group-0 exception processing. + // Detect its odd instruction address before another frame or fetch. + if (_processingM68000AddressError && (State.ProgramCounter & 1) != 0) HaltM68000DoubleFault(); + AddInstructionCyclesFromBase(exceptionCycleBase, AddressErrorExceptionCycles); + } + finally + { + _processingM68000AddressError = false; + } + } + + private void HaltM68000DoubleFault() + { + _m68000DoubleFaultHalted = true; + State.Halted = true; + State.Stopped = false; + _instructionTracePending = false; + FlushPrefetch(); + _skipRetirePrefetchTopUp = true; } private static ushort CreateBusErrorStatusWord( diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index eba244ac..7aab6ab0 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -97,6 +97,45 @@ unqualified. Undefined DFC values and noncanonical global register fields are excluded from this scoped gate. No enabled-MMU family is promoted in the integer inventory, and no specialized regression is retired by this follow-up. +### 000 address-error double-fault follow-up + +[MC68000UM sections 6.3.9.1 and 6.3.10](https://www.nxp.com/docs/en/reference-manual/MC68000UM.pdf) +require halting when another bus/address error occurs while processing a group-0 +exception. Entry to its handler is part of that processing. A bounded odd-handler +test reproduced `Halted=false` before the fix, without invoking the recursive +odd-stack path in the old process. + +The 000 core now guards active address-error entry. A nested alignment fault +halts instead of recursively stacking another frame. An odd handler target also +halts before another frame or instruction fetch. Completed frame writes and +operand effects are preserved; the guard neither rolls back nor retries an +instruction. Halt abandons pending prefetch and trace work. Interrupts are ignored +while halted, and an aborted interrupt stack sequence unwinds without continuing +its writes or exposing the internal control-flow exception to the host. + +A private double-fault latch prevents host subroutine/task entry from waking this +architectural halt. The supplied-PC/SP `Reset` API clears it. The existing host +convention of explicitly setting `Halted` and subsequently entering a subroutine +continues to work; that convention is separately tested. No public API was added. +The 010 model hook and advanced compatibility mode retain their separate behavior. + +`68000/system-double-fault` adds 640 public-factory cases across all CCRs, +user/supervisor stacks and trace states: odd exception stacks, odd handler PCs, +trap/interrupt stacking, halted register/memory/bus inactivity and reset recovery. +Positive scenarios check that an operand fault after a valid handler instruction +begins creates a new exception normally. The first 14-byte frame is checked word +by word. Partial stack-pointer changes on failed entry follow the retained +execution ordering; no physical timing claim is made for the halted edge. + +Warm classic/V2 JIT tests compare all registers, PC/SR, exception metadata, +machine/native cycle policy and writes against scalar, kind-table and packed-plan +interpreters through the **architectural-trace-forced fallback**. Direct compiled +odd-access execution was also tried and exposed an independent host-exception +gap, recorded below. It is not silently included as passing JIT coverage. +The public bus API has no explicit external BERR/reset-vector fault signal; +this gate qualifies address-error double faults, not general bus-error handling. +All specialized regressions are retained. + ## Consolidation proof `M68kShiftTests.AslByteSetsOverflowWhenSignChanges` is replaced by @@ -129,8 +168,20 @@ bus ordering, detailed fault sequencing, JIT and native ROM/media tests remain. - RTE internal restart remains untested: 010 format 8, 020/030 formats 9/A/B, and 040 format 7. The current advanced decoder does not implement all these legal restoration protocols; this is an implementation gap, not invalid encoding. -- Nested 000 address errors during exception stacking can recurse on an odd SSP. - The eventual fix needs documented double-fault halt behavior and fault-bus tests. +- Nested 000 address errors during exception stacking previously recursed on an + odd SSP. This is corrected by the address-error double-fault slice above. External + BERR and reset-vector fault signaling remain unavailable through the current + public bus API; no general bus-error qualification is claimed. +- Compiled 000 JIT odd accesses throw a host emulation exception instead of + entering architectural address-error handling. The double-fault slice covers + the trace-forced interpreter bridge with a warm JIT cache, not this compiled + path. A future correction must guard before operand side effects; it must not + catch a partially executed instruction and retry it. + The recorded repro warms 400 instructions of `3010 60FC` at `$1000`, with + A0 initially `$2000`; then sets A0 to `$2001`, SR to `$201F`, and executes a + batch with classic or V2 JIT. Both throw before architectural fault entry. + The failed warm-JIT run is retained in + `artifacts/double-fault-focused/focused-jit.trx`. - 040 enabled-MMU selective/global flushing, real translation-table formats, descriptor updates and architectural MMUSR contents remain unqualified; the single-word decoder and disabled-MMU instruction gate are now corrected. @@ -192,3 +243,32 @@ missing-report rejection), `artifacts/mmu-decode-focused-final/`, `artifacts/mmu-decode-final/`, `artifacts/mmu-decode-ahx/`, `artifacts/synthetic-private-feed-34/`, and the isolated consumer's `artifacts/mmu-decode-validation/` and `artifacts/mmu-decode-diagnostic-tests/`. + +## Validation checkpoint: 000 address-error double faults + +The full ordinary CPU suite passes **4,606 tests**, with eight optional/opt-in +skips and no failures. The final focused run passes eight tests/batches, including +640 double-fault scenarios and warm classic/V2 trace-fallback comparisons against +all three interpreter dispatch modes. Deterministic report validation passes +**8,368,680 logical cases in 340 batches**. Reports missing the new fault group +are rejected. The pinned Musashi audit separately passes 538 program/profile +combinations with 86 explicit exclusions. AHX passes 18 tests. The earlier +320,000 seeded cases remain historical evidence; no new seeded audit is claimed. + +Private package `1.5.2-synthetic-dev.35` has SHA-256 +`20d13f6435f78dbd04691ef8e12da600d644363cc9cd79b6a1eee39665b2a815`. +The isolated CopperScreen baseline `d9beae8` resolves that exact NuGet version +and builds Release with zero warnings/errors. Host tests pass 149 cases with six +optional/media skips; disk passes 74 and separately built engine diagnostics pass +1,080 without skips. Native Workbench 3.1 at 0/2 MiB Fast RAM and A1200 +eight-plane hard-disk boot/reopen persistence separately pass all three cases +without skips. The package remains unpublished; root consumer changes are +preserved. This is correctness and retained timing-policy evidence, not physical +timing or throughput qualification. + +Evidence: `artifacts/double-fault-before/` (bounded failing handler test and +missing-report rejection), `artifacts/double-fault-focused/` (including the +separate compiled-JIT failure), `artifacts/double-fault-final/`, +`artifacts/double-fault-ahx/`, `artifacts/synthetic-private-feed-35/`, and the +isolated consumer's `artifacts/double-fault-validation/` and +`artifacts/double-fault-diagnostic-tests/`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 63112e73..b3f1d567 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -489,8 +489,8 @@ All specialized timing, prefetch, cache, fault, JIT and native tests remain. See [reference qualification and consolidation evidence](COPPER68K_REFERENCE_QUALIFICATION.md) for pins, exclusions, replacement identifiers and remaining implementation gaps. -Milestone 6 is still in progress: internal RTE restart, double-fault halting -and broader external references remain open. The follow-up single-word 040 +Milestone 6 is still in progress: internal RTE restart and broader external +references remain open. The follow-up single-word 040 PFLUSH/PTEST decoder and privilege correction adds 34,850 disabled-MMU cases, bringing the current deterministic gate to 8,368,040 cases in 339 batches. PTEST MMUSR is undefined with translation disabled; enabled-MMU operation and @@ -509,3 +509,22 @@ program audit and AHX. Isolated CopperScreen validation with unpublished `1.5.2-synthetic-dev.34` passes the Release build, host/disk tests, separate diagnostics and all three native boot/persistence cases. The reference document records the package hash, reproduced failures, scoped MMU limits and evidence. + +The subsequent 000 address-error double-fault slice adds 640 cases in one batch +for a current deterministic gate of 8,368,680 cases in 340 batches. Nested +address-error entry and odd error-handler addresses halt without recursive +stacking or retries. Interrupts and host task/subroutine entry do not wake the +latched double fault; the supplied-PC/SP reset API restores execution. Frame +contents, failed-entry ordering and normal re-faulting handlers are checked. +Warm classic/V2 JIT parity is scoped to the architectural-trace-forced interpreter +bridge. Direct compiled odd accesses currently throw a host exception and need +a separate correction before operand side effects. External BERR/reset-vector +fault signaling is unavailable through the current public bus API. No general +bus-error or physical timing qualification is claimed, and milestone 6 stays open. + +Validation of this slice passes 4,606 ordinary CPU tests (eight opt-in/optional +skips), all 8,368,680 deterministic cases, the pinned cross-model program audit +and AHX. Isolated CopperScreen Release, host/disk and separate diagnostic checks +pass with unpublished private package `1.5.2-synthetic-dev.35`; all three native +boot/persistence replays execute without skips. The reference document records +the package hash, qualified paths, separate JIT failure and evidence locations. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 3f55f4b3..634c792e 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -93,6 +93,7 @@ try { 'system-stack'=6080; 'system-status'=$(if ($model -in @('68000','68010')) {16540} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {16936}); 'system-trace'=$(if ($model -in @('68000','68010','68060')) {1284} elseif ($model -in @('68020','68030','68040','68EC020','A1200')) {2568})} + if ($model -eq '68000') { $expected['system-double-fault'] = 640 } if ($model -eq '68040') { $expected['system-mmu-disabled'] = 34850 } foreach ($group in $expected.Keys) { $report = Get-Content -LiteralPath (Join-Path $output "$model-$group.json") -Raw | ConvertFrom-Json @@ -160,10 +161,11 @@ try { } $inventory | Add-Member -NotePropertyName qualificationBoundaries -NotePropertyValue @('RTE fault restart/internal formats remain untested: 010 frame8 restart, 020/030 frames9/A/B, 040 frame7', 'BKPT external replacement responder is unavailable', 'MOVES physical function-code spaces and LPSTOP CPU-space broadcast are unqualified', 'CALLM/RTM type1 protocol uses an internal synthetic responder; normal public buses have none') $inventory | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'qualified-inventory.json') - @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=339; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; m68040DisabledMmuInstructionGate='passing'; roadmapComplete=$false; + @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=340; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; m68040DisabledMmuInstructionGate='passing'; m68000AddressErrorDoubleFaultGate='passing'; roadmapComplete=$false; seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; + m68000DoubleFaultScope='Address-error entry/handler faults; reset-only recovery; external BERR/reset-vector faults unavailable through the current public bus API'; unavailableSystemCoverage=@('RTE bus-fault internal restart on 010/020/030', '020/030 coprocessor midinstruction restoration', '040 access-fault pending trace/writeback restoration', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast'); timing='Semantic gate; timing policy and physical qualification remain separate'} | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'summary.json') From d69453ea78e3387d298228976fba66e5efd928a3 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Sun, 4 Oct 2026 18:23:40 +0300 Subject: [PATCH 006/211] Dispatch 68000 compiled alignment faults before operand effects --- .../M68000JitDirectZeroWaitTests.cs | 280 +++++++++++++++++- Copper68k.Tests/Synthetic/README.md | 8 + Copper68k/M68kJitAlignmentEmitter.cs | 124 ++++++++ Copper68k/M68kJitCore.cs | 27 ++ Copper68k/M68kJitEmitters.cs | 12 + docs/COPPER68K_REFERENCE_QUALIFICATION.md | 90 +++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 24 +- 7 files changed, 546 insertions(+), 19 deletions(-) create mode 100644 Copper68k/M68kJitAlignmentEmitter.cs diff --git a/Copper68k.Tests/M68000JitDirectZeroWaitTests.cs b/Copper68k.Tests/M68000JitDirectZeroWaitTests.cs index 33f68b34..a1df10f0 100644 --- a/Copper68k.Tests/M68000JitDirectZeroWaitTests.cs +++ b/Copper68k.Tests/M68000JitDirectZeroWaitTests.cs @@ -4,10 +4,282 @@ namespace Copper68k.Tests; public sealed class M68000JitDirectZeroWaitTests { + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void WarmJitAlignmentExitPreservesOperandOrderingAndExceptionFrame(int jitMode) + { + // Fixed Motorola encodings. Expected fault locations are independent of the JIT decoder. + (string Name, ushort[] Words, int OddRegister, uint FaultAddress)[] cases = + [ + ("word-read", [0x3010], 0, 0x2001), + ("long-read", [0x2010], 0, 0x2001), + ("word-post-read", [0x3018], 0, 0x2001), + ("long-pre-read", [0x2020], 0, 0x1ffd), + ("word-write", [0x3080], 0, 0x2001), + ("long-post-write", [0x20c0], 0, 0x2001), + ("word-pre-write", [0x3100], 0, 0x1fff), + ("source-post-destination-fault", [0x3298], 1, 0x3001), + ("source-post-destination-pre-fault", [0x3318], 1, 0x2fff), + ("post-alias", [0x30d8], 0, 0x2001), + ("pre-alias", [0x3120], 0, 0x1fff), + ("word-displacement", [0x3028, 0x0001], -1, 0x2001), + ("long-displacement", [0x2028, 0xffff], -1, 0x1fff), + ("data-word-index", [0x3030, 0x6000], 6, 0x2003), + ("data-long-index", [0x2030, 0x6800], 6, 0x2003), + ("address-index", [0x3030, 0x9000], 1, 0x5001), + ("destination-index", [0x3198, 0x6000], 6, 0x2005), + ("pc-index", [0x303b, 0x6000], 6, 0x1005), + ("cmpm-destination-fault", [0xb348], 1, 0x3001), + ("rmw", [0x5250], 0, 0x2001), + ("movem-read", [0x4cd8, 0x0003], 0, 0x2001), + ("movem-pre-write", [0x48e0, 0xc000], 0, 0x1fff) // Low word is the first predecrement bus transfer. + ]; + foreach (var scenario in cases) + foreach (var prefix in new[] { false, true }) + { + try + { + var referenceBus = new DirectZeroWaitBus(); + var jitBus = new DirectZeroWaitBus(); + var words = (prefix ? new ushort[] { 0x7a7b, 0x5285 } : []) // MOVEQ #123,D5; ADDQ.L #1,D5 + .Concat(scenario.Words).ToArray(); + var branch = (ushort)(0x6000 | ((-2 * (words.Length + 1)) & 0xff)); + foreach (var bus in new[] { referenceBus, jitBus }) + { + bus.WriteWords(0x1000, words.Concat(new[] { branch }).ToArray()); + bus.WriteWords(0x5000, [0x4e71]); + bus.WriteLongValue(12, 0x5000); + for (uint address = 0x1f00; address < 0x6000; address += 2) + if (address != 0x5000) bus.WriteWords(address, [0x1234]); + } + using var reference = new M68kInterpreter(referenceBus); + using var jit = new M68kJitCore(jitBus, enableV2: jitMode != 0, + enableV2BusAccess: jitMode != 0, enableV2FastRead: jitMode == 2); + reference.Reset(0x1000, 0x8000); jit.Reset(0x1000, 0x8000); + foreach (var state in new[] { reference.State, jit.State }) + { + state.A[0] = 0x2000; state.A[1] = 0x3000; state.D[6] = 2; + } + // Displacement forms with odd constant offsets need an odd base while warming. + if (scenario.OddRegister == -1) + reference.State.A[0] = jit.State.A[0] = 0x2001; + var loopInstructions = prefix ? 4 : 2; + Assert.Equal(400, reference.ExecuteInstructions(400, null, new BatchBoundary())); + Assert.Equal(400, jit.ExecuteInstructions(400, 500_000, new BatchBoundary())); + Assert.True(jit.Counters.TraceHits > 0, scenario.Name); + Assert.Equal(0x1000u, jit.State.ProgramCounter); + foreach (var state in new[] { reference.State, jit.State }) + { + state.A[0] = 0x2000; state.A[1] = 0x3000; state.D[6] = 2; + state.D[0] = 0xcafe1234; state.D[1] = 0x87654321; + state.D[5] = 0xdeadbeef; state.StatusRegister = 0x201f; + if (scenario.OddRegister == 6) state.D[6] = 3; + else if (scenario.OddRegister >= 0) state.A[scenario.OddRegister]++; + } + referenceBus.WordWrites.Clear(); jitBus.WordWrites.Clear(); + var referenceStart = reference.State.Cycles; var jitStart = jit.State.Cycles; + var referenceNativeStart = reference.State.NativeCycles; var jitNativeStart = jit.State.NativeCycles; + var fallbackBefore = jit.Counters.FallbackInstructions; + var faultInstructions = loopInstructions - 1; + Assert.Equal(faultInstructions, reference.ExecuteInstructions(faultInstructions, null, new BatchBoundary())); + Assert.Equal(faultInstructions, jit.ExecuteInstructions(faultInstructions, jit.State.Cycles + 100_000, new BatchBoundary())); + Assert.True(jit.Counters.FallbackInstructions > fallbackBefore, scenario.Name); + Assert.False(jit.State.Halted); + Assert.Equal(3, jit.State.LastExceptionVector); + Assert.Equal(0x5000u, jit.State.ProgramCounter); + Assert.Equal(0x7ff2u, jit.State.A[7]); + var faultAddress = scenario.FaultAddress + (prefix && scenario.Name == "pc-index" ? 4u : 0u); + Assert.Equal(faultAddress, jitBus.ReadLongValue(0x7ff4)); + // The IR can already contain the successor opcode on a write-side fault. + // Retained pipeline tests qualify that sequencing; here compare every frame word. + for (uint address = 0x7ff2; address < 0x8000; address += 2) + Assert.Equal(referenceBus.ReadHostWord(address), jitBus.ReadHostWord(address)); + Assert.Equal(reference.State.D, jit.State.D); + Assert.Equal(reference.State.A, jit.State.A); + Assert.Equal(reference.State.StatusRegister, jit.State.StatusRegister); + Assert.Equal(reference.State.LastOpcode, jit.State.LastOpcode); + Assert.Equal(reference.State.LastInstructionProgramCounter, jit.State.LastInstructionProgramCounter); + Assert.Equal(reference.State.LastExceptionStackedProgramCounter, jit.State.LastExceptionStackedProgramCounter); + Assert.Equal(reference.State.LastExceptionStatusRegister, jit.State.LastExceptionStatusRegister); + Assert.Equal(reference.State.Cycles - referenceStart, jit.State.Cycles - jitStart); + Assert.Equal(reference.State.NativeCycles - referenceNativeStart, jit.State.NativeCycles - jitNativeStart); + Assert.Equal(referenceBus.WordWrites, jitBus.WordWrites); + Assert.True(referenceBus.SnapshotMemory().AsSpan().SequenceEqual(jitBus.SnapshotMemory()), scenario.Name); + if (prefix) Assert.Equal(124u, jit.State.D[5]); + if (scenario.Name == "source-post-destination-fault") Assert.Equal(0x2002u, jit.State.A[0]); + } + catch (Exception error) + { + throw new Xunit.Sdk.XunitException($"{scenario.Name}/jit-{jitMode}/prefix-{prefix}: {error}"); + } + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void WarmJitColdConstantAddressFaultsLeaveCompletedGraphInstructionsIntact(bool enableV2) + { + (string Name, ushort[] Words, uint FaultAddress)[] cases = + [ + ("absolute-word", [0x3038, 0x2001], 0x2001), + ("negative-absolute-word", [0x3038, 0xffff], 0xffffffff), + ("absolute-long", [0x2039, 0x0000, 0x2001], 0x2001), + ("pc-displacement", [0x303a, 0x0001], 0x100b), + ("absolute-write", [0x33c0, 0x0000, 0x2001], 0x2001) + ]; + foreach (var scenario in cases) + { + var referenceBus = new DirectZeroWaitBus(); var jitBus = new DirectZeroWaitBus(); + foreach (var bus in new[] { referenceBus, jitBus }) + { + bus.WriteWords(0x1000, new ushort[] { 0x4a87, 0x6604, 0x60fa, 0x4e71 } + .Concat(scenario.Words).Concat(new ushort[] { 0x4e71 }).ToArray()); + bus.WriteWords(0x5000, [0x4e71]); bus.WriteLongValue(12, 0x5000); + } + using var reference = new M68kInterpreter(referenceBus); + using var jit = new M68kJitCore(jitBus, enableV2: enableV2, enableV2BusGraph: true); + reference.Reset(0x1000, 0x8000); jit.Reset(0x1000, 0x8000); + Assert.Equal(399, reference.ExecuteInstructions(399, null, new BatchBoundary())); + Assert.Equal(399, jit.ExecuteInstructions(399, 500_000, new BatchBoundary())); + Assert.True(enableV2 ? jit.Counters.V2TraceHits > 0 : jit.Counters.TraceHits > 0, scenario.Name); + reference.State.D[7] = jit.State.D[7] = 1; + reference.State.D[0] = jit.State.D[0] = 0xcafe1234; + referenceBus.WordWrites.Clear(); jitBus.WordWrites.Clear(); + var referenceStart = reference.State.Cycles; var jitStart = jit.State.Cycles; + Assert.Equal(3, reference.ExecuteInstructions(3, null, new BatchBoundary())); + Assert.Equal(3, jit.ExecuteInstructions(3, jit.State.Cycles + 100_000, new BatchBoundary())); + Assert.Equal(3, jit.State.LastExceptionVector); + Assert.Equal(0x5000u, jit.State.ProgramCounter); + Assert.Equal(scenario.FaultAddress, jitBus.ReadLongValue(0x7ff4)); + Assert.Equal(reference.State.D, jit.State.D); Assert.Equal(reference.State.A, jit.State.A); + Assert.Equal(reference.State.StatusRegister, jit.State.StatusRegister); + Assert.Equal(reference.State.LastExceptionStackedProgramCounter, jit.State.LastExceptionStackedProgramCounter); + Assert.Equal(reference.State.Cycles - referenceStart, jit.State.Cycles - jitStart); + Assert.Equal(referenceBus.WordWrites, jitBus.WordWrites); + Assert.True(referenceBus.SnapshotMemory().AsSpan().SequenceEqual(jitBus.SnapshotMemory()), scenario.Name); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void WarmJitLegalOddByteAndAddressOperandsRemainExecutable(bool enableV2) + { + var referenceBus = new DirectZeroWaitBus(); var jitBus = new DirectZeroWaitBus(); + foreach (var bus in new[] { referenceBus, jitBus }) + { + bus.WriteWords(0x1000, + [0x1010, 0x0308, 0x0000, 0x43d0, 0x4850, 0x588f, 0x60f2]); + // MOVE.B (A0),D0; MOVEP.W (0,A0),D1; LEA (A0),A1; PEA (A0); ADDQ.L #4,A7; BRA + bus.WriteLongValue(0x2000, 0x12345678); + } + using var reference = new M68kInterpreter(referenceBus); + using var jit = new M68kJitCore(jitBus, enableV2: enableV2); + reference.Reset(0x1000, 0x8000); jit.Reset(0x1000, 0x8000); + reference.State.A[0] = jit.State.A[0] = 0x2001; + Assert.Equal(600, reference.ExecuteInstructions(600, null, new BatchBoundary())); + Assert.Equal(600, jit.ExecuteInstructions(600, 500_000, new BatchBoundary())); + Assert.True(jit.Counters.TraceHits > 0); + Assert.Equal(-1, jit.State.LastExceptionVector); + Assert.Equal(0x2001u, jit.State.A[1]); + Assert.Equal(reference.State.D, jit.State.D); Assert.Equal(reference.State.A, jit.State.A); + Assert.Equal(reference.State.StatusRegister, jit.State.StatusRegister); + Assert.Equal(reference.State.Cycles, jit.State.Cycles); + Assert.True(referenceBus.SnapshotMemory().AsSpan().SequenceEqual(jitBus.SnapshotMemory())); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void WarmJitOddStackOperandsDispatchWithoutLeakingHostExceptions(bool enableV2) + { + (string Name, ushort[] Main, ushort[] Target, uint WarmInstructions)[] cases = + [ + ("pea", [0x4850, 0x588f, 0x60fa], [0x4e75], 399), // PEA (A0); ADDQ.L #4,A7; BRA + ("jsr", [0x4e90, 0x60fc], [0x4e75], 399), + ("bsr", [0x6100, 0x1ffe, 0x60fa], [0x4e75], 399), + ("rts", [0x4e75], [0x598f, 0x4ef9, 0x0000, 0x1000], 399) + ]; + foreach (var scenario in cases) + { + var referenceBus = new DirectZeroWaitBus(); var jitBus = new DirectZeroWaitBus(); + foreach (var bus in new[] { referenceBus, jitBus }) + { + bus.WriteWords(0x1000, scenario.Main); + bus.WriteWords(0x3000, scenario.Target); + bus.WriteWords(0x5000, [0x4e71]); + bus.WriteLongValue(12, 0x5000); + bus.WriteLongValue(0x8000, 0x3000); + } + using var reference = new M68kInterpreter(referenceBus); + using var jit = new M68kJitCore(jitBus, enableV2: enableV2); + reference.Reset(0x1000, 0x8000); jit.Reset(0x1000, 0x8000); + reference.State.A[0] = jit.State.A[0] = 0x3000; + Assert.Equal((int)scenario.WarmInstructions, reference.ExecuteInstructions((int)scenario.WarmInstructions, null, new BatchBoundary())); + Assert.Equal((int)scenario.WarmInstructions, jit.ExecuteInstructions((int)scenario.WarmInstructions, 500_000, new BatchBoundary())); + Assert.Equal(0x1000u, jit.State.ProgramCounter); + Assert.True(jit.Counters.TraceHits > 0, scenario.Name); + reference.State.SetActiveStackPointer(0x8001); jit.State.SetActiveStackPointer(0x8001); + referenceBus.WordWrites.Clear(); jitBus.WordWrites.Clear(); + Assert.Equal(1, reference.ExecuteInstructions(10, reference.State.Cycles + 1000, new BatchBoundary())); + Assert.Equal(1, jit.ExecuteInstructions(10, jit.State.Cycles + 1000, new BatchBoundary())); + Assert.True(jit.State.Halted, scenario.Name); + Assert.Equal(reference.State.ProgramCounter, jit.State.ProgramCounter); + Assert.Equal(reference.State.StatusRegister, jit.State.StatusRegister); + Assert.Equal(reference.State.D, jit.State.D); Assert.Equal(reference.State.A, jit.State.A); + Assert.Equal(reference.State.Cycles, jit.State.Cycles); + Assert.Equal(reference.State.NativeCycles, jit.State.NativeCycles); + Assert.Equal(referenceBus.WordWrites, jitBus.WordWrites); + Assert.True(referenceBus.SnapshotMemory().AsSpan().SequenceEqual(jitBus.SnapshotMemory()), scenario.Name); + } + } + [Theory] [InlineData(false)] [InlineData(true)] - public void WarmJitTraceFallbackDoubleFaultMatchesInterpreterAndRequiresReset(bool enableV2) + public void WarmJitOddJumpTargetsDispatchBeforeLinkAndFetchEffects(bool enableV2) + { + foreach (var link in new[] { false, true }) + { + var referenceBus = new DirectZeroWaitBus(); var jitBus = new DirectZeroWaitBus(); + foreach (var bus in new[] { referenceBus, jitBus }) + { + bus.WriteWords(0x1000, link ? [0x4e90, 0x60fc] : [0x4ed0]); + bus.WriteWords(0x3000, link ? [0x4e75] : [0x4ef9, 0x0000, 0x1000]); + bus.WriteWords(0x5000, [0x4e71]); bus.WriteLongValue(12, 0x5000); + } + using var reference = new M68kInterpreter(referenceBus); + using var jit = new M68kJitCore(jitBus, enableV2: enableV2); + reference.Reset(0x1000, 0x8000); jit.Reset(0x1000, 0x8000); + reference.State.A[0] = jit.State.A[0] = 0x3000; + var warmCount = link ? 399 : 400; + Assert.Equal(warmCount, reference.ExecuteInstructions(warmCount, null, new BatchBoundary())); + Assert.Equal(warmCount, jit.ExecuteInstructions(warmCount, 500_000, new BatchBoundary())); + Assert.Equal(0x1000u, jit.State.ProgramCounter); + Assert.True(jit.Counters.TraceHits > 0); + reference.State.A[0] = jit.State.A[0] = 0x3001; + referenceBus.WordWrites.Clear(); jitBus.WordWrites.Clear(); + Assert.Equal(1, reference.ExecuteInstructions(1, null, new BatchBoundary())); + Assert.Equal(1, jit.ExecuteInstructions(1, jit.State.Cycles + 1000, new BatchBoundary())); + Assert.Equal(3, jit.State.LastExceptionVector); + Assert.Equal(0x5000u, jit.State.ProgramCounter); + Assert.Equal(reference.State.D, jit.State.D); Assert.Equal(reference.State.A, jit.State.A); + Assert.Equal(reference.State.StatusRegister, jit.State.StatusRegister); + Assert.Equal(reference.State.Cycles, jit.State.Cycles); + Assert.Equal(referenceBus.WordWrites, jitBus.WordWrites); + Assert.True(referenceBus.SnapshotMemory().AsSpan().SequenceEqual(jitBus.SnapshotMemory())); + } + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public void WarmJitDoubleFaultMatchesInterpreterAndRequiresReset(bool enableV2, bool trace) { foreach (var dispatch in Enum.GetValues()) foreach (var oddStack in new[] { false, true }) @@ -29,9 +301,7 @@ public void WarmJitTraceFallbackDoubleFaultMatchesInterpreterAndRequiresReset(bo Assert.Equal(400, jit.ExecuteInstructions(400, 500_000, new BatchBoundary())); Assert.True(enableV2 ? jit.Counters.V2TraceHits > 0 : jit.Counters.TraceHits > 0); reference.State.A[0] = jit.State.A[0] = 0x2001; - // Architectural trace forces the established interpreter bridge. - // Compiled odd-access dispatch is a separate, unqualified path. - reference.State.StatusRegister = jit.State.StatusRegister = 0xa01f; + reference.State.StatusRegister = jit.State.StatusRegister = trace ? (ushort)0xa01f : (ushort)0x201f; reference.State.SetActiveStackPointer(oddStack ? 0x8001u : 0x8000u); jit.State.SetActiveStackPointer(oddStack ? 0x8001u : 0x8000u); @@ -907,6 +1177,8 @@ public DirectZeroWaitBus(bool realFastIsZeroWait = true) public List<(uint Address, ushort Value)> WordWrites { get; } = new(); + public byte[] SnapshotMemory() => (byte[])_memory.Clone(); + public byte ReadByte(uint address, ref long cycle, M68kBusAccessKind accessKind) { if (accessKind == M68kBusAccessKind.CpuDataRead) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index b45fd1d6..edbf6f87 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -157,3 +157,11 @@ ASL regression replacement proof. Run the consolidation mutations with `./scripts/test-copper68k-synthetic-mutations.ps1 -Scope Consolidation`. Milestone 6 remains in progress; remaining implementation/reference gaps are listed explicitly in that document. + +The 000 compiled-JIT alignment follow-up is retained in +`M68000JitDirectZeroWaitTests`: 180 logical warm-cache scenarios in 15 ordinary +xUnit batches. Instruction-boundary guards transfer word/long, stack and +JMP/JSR alignment faults to the accurate interpreter before operand effects. +These JIT regressions are reported separately from the synthetic inventory; +no cache/prefetch/bus-ordering regression is retired. See the reference +qualification document for reproduced failures, guard-removal proofs and scope. diff --git a/Copper68k/M68kJitAlignmentEmitter.cs b/Copper68k/M68kJitAlignmentEmitter.cs new file mode 100644 index 00000000..4eedb352 --- /dev/null +++ b/Copper68k/M68kJitAlignmentEmitter.cs @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2026 Ilkka Lehtoranta + * SPDX-License-Identifier: MIT + */ + +using System; +using System.Reflection.Emit; + +namespace Copper68k +{ + // Guard at the instruction boundary, before fetch consumption, register updates, + // flags or bus accesses. The interpreter then executes the instruction once and + // owns its precise address-error frame and any operand effects before the fault. + internal static class M68kJitAlignmentEmitter + { + public static void Emit( + ILGenerator il, + M68kDecodedInstruction instruction, + Action loadDataRegister, + Action loadAddressRegister, + Label exit) + { + switch (instruction.Operation) + { + case M68kJitOperation.Lea: + case M68kJitOperation.Movep: // Byte bus transfers permit odd addresses. + return; + case M68kJitOperation.Pea: + case M68kJitOperation.Bsr: + case M68kJitOperation.Rts: + loadAddressRegister(7); + EmitOddExit(il, exit); + return; + case M68kJitOperation.Jsr: + loadAddressRegister(7); + EmitOddExit(il, exit); + goto case M68kJitOperation.Jmp; + case M68kJitOperation.Jmp: + EmitEa(il, instruction.Source, loadDataRegister, loadAddressRegister, exit); + return; + } + + if (instruction.Size == M68kOperandSize.Byte) + { + return; + } + + EmitEa(il, instruction.Source, loadDataRegister, loadAddressRegister, exit); + EmitEa(il, instruction.Destination, loadDataRegister, loadAddressRegister, exit); + } + + private static void EmitEa( + ILGenerator il, + M68kDecodedEa ea, + Action loadDataRegister, + Action loadAddressRegister, + Label exit) + { + if (!ea.IsMemory) + { + return; + } + + switch (ea.Kind) + { + case M68kJitEaKind.AddressIndirect: + case M68kJitEaKind.AddressPostincrement: + case M68kJitEaKind.AddressPredecrement: + // Word/long increments and decrements are even, including aliases. + loadAddressRegister(ea.Register); + break; + case M68kJitEaKind.AddressDisplacement: + loadAddressRegister(ea.Register); + il.Emit(OpCodes.Ldc_I4, ea.Extension0 & 1); + il.Emit(OpCodes.Xor); + break; + case M68kJitEaKind.AddressIndex: + case M68kJitEaKind.PcIndex: + if (ea.Kind == M68kJitEaKind.AddressIndex) + { + loadAddressRegister(ea.Register); + } + else + { + il.Emit(OpCodes.Ldc_I4, (int)(ea.ExtensionAddress & 1)); + } + var indexRegister = (ea.Extension0 >> 12) & 7; + if ((ea.Extension0 & 0x8000) != 0) + { + loadAddressRegister(indexRegister); + } + else + { + loadDataRegister(indexRegister); + } + // 68000 brief indexes are unscaled; sign extension preserves parity. + il.Emit(OpCodes.Xor); + il.Emit(OpCodes.Ldc_I4, ea.Extension0 & 1); + il.Emit(OpCodes.Xor); + break; + case M68kJitEaKind.AbsoluteWord: + il.Emit(OpCodes.Ldc_I4, ea.Extension0 & 1); + break; + case M68kJitEaKind.AbsoluteLong: + il.Emit(OpCodes.Ldc_I4, ea.Extension1 & 1); + break; + case M68kJitEaKind.PcDisplacement: + il.Emit(OpCodes.Ldc_I4, (int)((ea.ExtensionAddress ^ ea.Extension0) & 1)); + break; + default: + throw new InvalidOperationException($"Unqualified 68000 alignment form: {ea.Kind}."); + } + + EmitOddExit(il, exit); + } + + private static void EmitOddExit(ILGenerator il, Label exit) + { + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.And); + il.Emit(OpCodes.Brtrue, exit); + } + } +} diff --git a/Copper68k/M68kJitCore.cs b/Copper68k/M68kJitCore.cs index d47ef2ee..c9c43c0c 100644 --- a/Copper68k/M68kJitCore.cs +++ b/Copper68k/M68kJitCore.cs @@ -3265,12 +3265,14 @@ private static TraceEntry CompileTraceFromPlan(M68kTraceCompilationPlan plan) plan.Root, plan.TraceInstructions, emitBoundaryCalls: true, + guardM68000Alignment: plan.M68000MemoryHelpers, pinM68000Registers: false); var pureCompiled = !compileV2Only && plan.PureCpuBatchEligible ? Compile( plan.Root, plan.TraceInstructions, emitBoundaryCalls: false, + guardM68000Alignment: plan.M68000MemoryHelpers, pinM68000Registers: plan.M68000MemoryHelpers) : null; var v2Compiled = plan.V2Trace.IsEmpty @@ -4077,6 +4079,7 @@ private static CompiledTrace Compile( uint root, ReadOnlySpan instructions, bool emitBoundaryCalls, + bool guardM68000Alignment, bool pinM68000Registers) { var method = new DynamicMethod( @@ -4120,6 +4123,10 @@ private static CompiledTrace Compile( for (var i = 0; i < instructions.Length; i++) { var instruction = instructions[i]; + if (guardM68000Alignment) + { + M68kOperationEmitter.EmitM68000AlignmentGuard(il, instruction, emitContext, returnLabels[i]); + } il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldc_I4, i); @@ -6153,6 +6160,10 @@ private static CompiledTrace CompileV2Pure( fastReadFailureEnabled, fastReadFailureLabel); context.EmitCanContinuePure(exit); + if (!useM68020BriefIndexedAddressing) + { + context.EmitM68000AlignmentGuard(instruction, exit); + } if (fastReadFailureEnabled) { context.EmitSaveFastReadFailureBookkeeping(); @@ -6280,6 +6291,10 @@ private static CompiledTrace CompileV2BusAccessBatch( var instruction = instructions[i]; il.MarkLabel(labels[i]); context.EmitCanContinue(exit); + if (useM68000MemoryHelpers) + { + context.EmitM68000AlignmentGuard(instruction, exit); + } context.EmitStartInstruction(instruction); EmitV2Instruction( il, @@ -6327,6 +6342,10 @@ private static CompiledTrace CompileV2BusAccessBatch( foreach (var instruction in instructions) { context.EmitCanContinue(exit); + if (useM68000MemoryHelpers) + { + context.EmitM68000AlignmentGuard(instruction, exit); + } context.EmitStartInstruction(instruction); EmitV2Instruction( il, @@ -15874,6 +15893,14 @@ public void EmitReturnZeroIfNoInstructions(Label returnZero) _il.Emit(OpCodes.Brfalse, returnZero); } + public void EmitM68000AlignmentGuard(M68kDecodedInstruction instruction, Label exit) + => M68kJitAlignmentEmitter.Emit( + _il, + instruction, + register => EmitLoadDataRegister(register, M68kOperandSize.Long), + EmitLoadAddressRegister, + exit); + public void EmitStartInstruction(M68kDecodedInstruction instruction) { if (!DeferredM68000CycleFloor) diff --git a/Copper68k/M68kJitEmitters.cs b/Copper68k/M68kJitEmitters.cs index 5c9c2a9e..a9d1560c 100644 --- a/Copper68k/M68kJitEmitters.cs +++ b/Copper68k/M68kJitEmitters.cs @@ -304,6 +304,18 @@ public static void EmitPinnedRegisterWriteback(ILGenerator il, TraceEmitContext } } + public static void EmitM68000AlignmentGuard( + ILGenerator il, + M68kDecodedInstruction instruction, + TraceEmitContext context, + Label exit) + => M68kJitAlignmentEmitter.Emit( + il, + instruction, + register => EmitLoadDataRegister(il, context, register, M68kOperandSize.Long), + register => EmitLoadAddressRegister(il, context, register), + exit); + public static M68kIlInstructionKind Emit( ILGenerator il, M68kDecodedInstruction instruction, diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 7aab6ab0..c8f8ee78 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -129,13 +129,51 @@ execution ordering; no physical timing claim is made for the halted edge. Warm classic/V2 JIT tests compare all registers, PC/SR, exception metadata, machine/native cycle policy and writes against scalar, kind-table and packed-plan -interpreters through the **architectural-trace-forced fallback**. Direct compiled -odd-access execution was also tried and exposed an independent host-exception -gap, recorded below. It is not silently included as passing JIT coverage. +interpreters through the **architectural-trace-forced fallback** at this first +checkpoint. Direct compiled odd-access execution exposed a separate host-exception +gap; the subsequent compiled-JIT alignment follow-up below corrects that path. The public bus API has no explicit external BERR/reset-vector fault signal; this gate qualifies address-error double faults, not general bus-error handling. All specialized regressions are retained. +### 000 compiled JIT alignment follow-up + +The warm `3010 60FC` repro failed with both classic and V2 JIT before this +correction: changing A0 from `$2000` to `$2001` with SR `$201F` exposed a host +exception instead of vector 3. The new instruction-boundary IL guards inspect +only address parity, before consuming instruction fetches, updating registers, +changing flags or accessing the bus. A failing guard returns completed trace +instructions and writes back their state; the accurate interpreter executes the +faulting instruction once. No compiled instruction is caught and retried. + +The guards apply only to the 68000 plan. They cover word/long memory operand +forms, displacement and unscaled brief indexes, absolute and PC-relative +addresses, stack accesses for PEA/BSR/JSR/RTS, and JMP/JSR target alignment. +Even word/long address increments preserve parity across operand aliases. Byte +operands, MOVEP's byte transfers and LEA's computed address remain legal at odd +addresses; PEA checks its stack rather than its pushed address value. The +low-level alignment assertions remain as defensive checks. + +`M68000JitDirectZeroWaitTests` executes **180 logical warm-cache scenarios in +15 xUnit batches**: 132 operand/prefix/dispatch-mode combinations, ten cold +absolute/PC-relative graph exits, eight odd-stack cases, four odd JMP/JSR target +cases, two legal odd-byte/address loops and 24 double-fault/trace/dispatch +combinations. This includes the earlier trace-only comparisons, now extended to +trace-disabled compiled execution. Fixed instruction encodings and fault +addresses supply independent assertions; architectural registers, exception +metadata, all frame words, completed writes and memory are compared with the +interpreter. Retained pipeline tests own IR/prefetch ordering, including write +faults whose IR already contains a successor opcode. + +Removing all guards reproduces seven failing batches, including a cold V2 +absolute access and odd V2 stack write. Removing MOVE destination guards +separately reproduces three failing batches for compiled writes. These mutations +are restored before the successful full run. No old regression is retired. +This gate preserves existing machine/native cycle policies; it is not physical +pipeline/timing qualification or exhaustive JIT exception-path qualification. +External BERR/reset-vector signaling remains unavailable through the public bus +API. Independent reference and advanced restart-frame gaps remain open. + ## Consolidation proof `M68kShiftTests.AslByteSetsOverflowWhenSignChanges` is replaced by @@ -172,16 +210,12 @@ bus ordering, detailed fault sequencing, JIT and native ROM/media tests remain. odd SSP. This is corrected by the address-error double-fault slice above. External BERR and reset-vector fault signaling remain unavailable through the current public bus API; no general bus-error qualification is claimed. -- Compiled 000 JIT odd accesses throw a host emulation exception instead of - entering architectural address-error handling. The double-fault slice covers - the trace-forced interpreter bridge with a warm JIT cache, not this compiled - path. A future correction must guard before operand side effects; it must not - catch a partially executed instruction and retry it. - The recorded repro warms 400 instructions of `3010 60FC` at `$1000`, with - A0 initially `$2000`; then sets A0 to `$2001`, SR to `$201F`, and executes a - batch with classic or V2 JIT. Both throw before architectural fault entry. - The failed warm-JIT run is retained in - `artifacts/double-fault-focused/focused-jit.trx`. +- Compiled 000 JIT word/long and stack alignment faults are corrected by the + instruction-boundary guards above. The original host-exception repro remains + in `artifacts/double-fault-focused/focused-jit.trx`; final warm-cache and + guard-removal evidence is under `artifacts/jit-address-error-focused/` and + `artifacts/jit-address-error-before/`. Other compiled exception/fetch paths + are not exhaustively qualified by this scoped gate. - 040 enabled-MMU selective/global flushing, real translation-table formats, descriptor updates and architectural MMUSR contents remain unqualified; the single-word decoder and disabled-MMU instruction gate are now corrected. @@ -272,3 +306,33 @@ separate compiled-JIT failure), `artifacts/double-fault-final/`, `artifacts/double-fault-ahx/`, `artifacts/synthetic-private-feed-35/`, and the isolated consumer's `artifacts/double-fault-validation/` and `artifacts/double-fault-diagnostic-tests/`. + +## Validation checkpoint: 000 compiled JIT alignment + +The full ordinary CPU suite passes **4,619 tests**, with eight optional/opt-in +skips and zero failures. All **8,368,680 synthetic cases in 340 batches** pass; +the JIT-specific warm-cache gate separately passes **180 logical scenarios in +15 batches**. The requested pinned Musashi audit executes without skips: 538 +program/profile combinations pass, with 86 explicit exclusions and zero +mismatches (source `72c1d74800f3087b45a0c1a7342601bbed898881`). AHX passes 18 tests. +The bounded warm-loop repro fails before correction; removing all guards detects +seven failing batches and removing MOVE destination guards detects three. +No new seeded audit or retirement of an existing regression is claimed. + +Private package `1.5.2-synthetic-dev.36` has SHA-256 +`96b16bd8bd2b9c865fe482501289456bcb8ca69a332b99552a80a649b995b6c5`. +It is not published. The isolated CopperScreen consumer retains its pinned NuGet +boundary and resolves this exact private version in both production and separate +diagnostic outputs. Its Release production build has zero warnings/errors; host +149, disk 74 and engine diagnostics 1,080 pass. The ordinary host invocation has +six optional/media skips. The separate native Workbench 3.1 0/2 MiB Fast RAM and +A1200 eight-plane hard-disk boot/reopen persistence invocation executes all three +cases without skips. Native replay results are correctness evidence, not host +throughput or physical timing qualification. Unrelated root changes are preserved. + +Evidence is in the CPU checkout's `artifacts/jit-address-error-before/`, +`artifacts/jit-address-error-focused/`, `artifacts/jit-address-error-final/`, +`artifacts/jit-address-error-ahx/` and `artifacts/synthetic-private-feed-36/`, and +the isolated consumer's `artifacts/jit-address-error-validation/`, +`artifacts/jit-address-error-diagnostic-tests/` and +`artifacts/jit-address-error-production.binlog`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index b3f1d567..feb32628 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -517,8 +517,8 @@ stacking or retries. Interrupts and host task/subroutine entry do not wake the latched double fault; the supplied-PC/SP reset API restores execution. Frame contents, failed-entry ordering and normal re-faulting handlers are checked. Warm classic/V2 JIT parity is scoped to the architectural-trace-forced interpreter -bridge. Direct compiled odd accesses currently throw a host exception and need -a separate correction before operand side effects. External BERR/reset-vector +bridge. That slice exposed a separate compiled odd-access host exception; the +subsequent JIT alignment correction below resolves it before operand effects. External BERR/reset-vector fault signaling is unavailable through the current public bus API. No general bus-error or physical timing qualification is claimed, and milestone 6 stays open. @@ -528,3 +528,23 @@ and AHX. Isolated CopperScreen Release, host/disk and separate diagnostic checks pass with unpublished private package `1.5.2-synthetic-dev.35`; all three native boot/persistence replays execute without skips. The reference document records the package hash, qualified paths, separate JIT failure and evidence locations. + +The subsequent 000 compiled-JIT alignment slice adds instruction-boundary parity +guards to classic, V2 bus/graph and V2 fast-read emission. Faulting instructions +enter the accurate interpreter before any compiled operand effects; completed +trace instructions write back normally. No partial instruction is retried. +Word/long operands, brief indexed and constant/PC-relative forms, stack accesses, +and JMP/JSR targets are covered; legal odd byte, MOVEP, LEA and PEA address values +are retained. The warm-cache suite executes 180 logical scenarios in 15 batches, +separate from the unchanged 8,368,680-case / 340-batch synthetic inventory. +Removing the guards and separately removing MOVE destination guards both +reproduce failures. No regression is retired, and milestone 6 remains open for +the reference/restart-frame qualification gaps recorded in the reference document. + +Validation of the compiled-JIT slice passes 4,619 ordinary CPU tests (eight +optional/opt-in skips), the unchanged deterministic gate, 538 pinned reference +program/profile combinations (86 explicit exclusions) and 18 AHX tests. Private +unpublished `1.5.2-synthetic-dev.36` passes the isolated CopperScreen production +build, host 149/disk 74/diagnostics 1,080 and all three native boot/persistence +cases without native skips. The reference document records package identity, +mutation evidence, retained timing policy and remaining qualification limits. From c6b26b8ec09412235c299cab9efef30045fe67de Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Sun, 4 Oct 2026 18:58:12 +0300 Subject: [PATCH 007/211] Correct 68010 format-8 frame structure and RTE validation --- Copper68k.Tests/M68010InterpreterTests.cs | 8 +- Copper68k.Tests/Synthetic/README.md | 16 +- .../Synthetic/SyntheticM68010Format8Tests.cs | 230 ++++++++++++++++++ Copper68k/M68010Interpreter.cs | 37 ++- Copper68k/M68kCore.cs | 71 +++--- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 78 +++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 23 ++ scripts/test-copper68k-synthetic.ps1 | 8 +- 8 files changed, 426 insertions(+), 45 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68010Format8Tests.cs diff --git a/Copper68k.Tests/M68010InterpreterTests.cs b/Copper68k.Tests/M68010InterpreterTests.cs index 20843760..4c6e1183 100644 --- a/Copper68k.Tests/M68010InterpreterTests.cs +++ b/Copper68k.Tests/M68010InterpreterTests.cs @@ -210,10 +210,10 @@ public void OddWordReadRaisesFormat8AddressErrorFrame() cpu.State.A[1] = 1; cpu.ExecuteInstruction(); Assert.Equal(0x2000u, cpu.State.ProgramCounter); - Assert.Equal(0x2FF8u, cpu.State.A[7]); - Assert.Equal(M68kCpuState.ResetStatusRegister, bus.ReadWord(0x2FF8)); - Assert.Equal(CodeBase, bus.ReadLong(0x2FFA)); - Assert.Equal(0x800Cu, bus.ReadWord(0x2FFE)); + Assert.Equal(0x2FC6u, cpu.State.A[7]); + Assert.Equal(M68kCpuState.ResetStatusRegister, bus.ReadWord(0x2FC6)); + Assert.Equal(CodeBase, bus.ReadLong(0x2FC8)); + Assert.Equal(0x800Cu, bus.ReadWord(0x2FCC)); } [Fact] diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index edbf6f87..3ad9f404 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -16,7 +16,7 @@ From the CopperMod root, run: Milestones 1–5 execute MOVE/MOVEA, transfer/address, arithmetic/comparison, logical/bit/shift/atomic and control/system operations in -**8,368,680 logical cases** across **340 xUnit batches**, across seven models and the A1200 profile. +**8,374,056 logical cases** across **343 xUnit batches**, across seven models and the A1200 profile. Ordinary `dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release` includes these batches. CI additionally validates every required report and exact count; missing reports, mismatches, unsupported execution and empty groups fail the gate. @@ -33,9 +33,17 @@ entry, bad supervisor stacks, odd handler addresses, trap/interrupt entry, CCR/trace preservation, halted bus inactivity and reset recovery. It preserves the first frame and verifies that a fault in a successfully entered handler is a new exception. External BERR and reset-vector fault signaling are unavailable -through the current public bus API. Compiled JIT odd-access dispatch remains a -separate gap; the tested warm-JIT bridge uses architectural trace to force the -established interpreter fallback. +through the current public bus API. The separate compiled-JIT warm-cache gate now covers 180 scenarios in 15 +batches, including guards that dispatch odd accesses before operand effects. + +The 010-only `system-format8-entry` (1,024 cases), `system-format8-rte` +(4,096) and `system-format8-double-fault` (256) groups qualify structural +prerequisites: the 58-byte address-error frame, 26 word writes with reserved +holes untouched, scoped read/write/fetch metadata, version rejection before +popping, tail probing before remaining reads, stack selection and halt/reset +recovery. Emulator version zero and zeroed input/internal buffers are private +conventions. Suspended-instruction continuation, RR behavior, RMW/prefetch buffer +semantics, external BERR and physical frame-cycle timing remain unqualified. Reports contain per-scenario identifiers and counts for passing, mismatching, unsupported and untested outcomes. `integer-inventory.json` lists all integer diff --git a/Copper68k.Tests/Synthetic/SyntheticM68010Format8Tests.cs b/Copper68k.Tests/Synthetic/SyntheticM68010Format8Tests.cs new file mode 100644 index 00000000..d4f4453c --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68010Format8Tests.cs @@ -0,0 +1,230 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticM68010Format8Tests(ITestOutputHelper output) +{ + [Fact] + public void GeneratedAddressErrorAllocatesLongFrame() + { + var machine = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68010")); + machine.Reset(31); + machine.InitializePhysical(0x1000, 0x3010, 2); // MOVE.W (A0),D0 + machine.Core.State.A[0] = 0x4001; + machine.Start(); + var stack = machine.Core.State.A[7]; + machine.Core.ExecuteInstruction(); + Assert.Equal(stack - 58, machine.Core.State.A[7]); + Assert.Equal(0x800cu, machine.PeekPhysical(stack - 52, 2)); + } + + [Fact] + public void Format8RteRejectsDifferentProcessorVersionBeforePoppingFrame() + { + var machine = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68010")); + machine.Reset(16); + var stack = machine.Core.State.A[7]; + machine.InitializePhysical(0x1000, 0x4e73, 2); + machine.InitializePhysical(stack, 0x001f, 2); + machine.InitializePhysical(stack + 2, 0x6000, 4); + machine.InitializePhysical(stack + 6, 0x800c, 2); + machine.InitializePhysical(stack + 26, 0x0400, 2); // Version 1; emulated version is 0. + machine.InitializePhysical(0x6000, 0x4e71, 2); + machine.Start(); + machine.Core.ExecuteInstruction(); + Assert.Equal(14, machine.Core.State.LastExceptionVector); + Assert.Equal(0x90e0u, machine.Core.State.ProgramCounter); + Assert.Equal(stack - 8, machine.Core.State.A[7]); + Assert.Equal(0x001fu, machine.PeekPhysical(stack, 2)); + Assert.Equal(0x800cu, machine.PeekPhysical(stack + 6, 2)); + } + + [Fact, Trait("Suite", "Synthetic")] + public void AddressErrorInformationWordsAndReservedHoles() + { + var m = NewMachine(); + var report = new CoverageBatch("68010", "system-format8-entry"); + foreach (var kind in new[] { "read-word", "read-long", "write-word", "write-long", "movea-word", "pre-read", "pre-write", "fetch" }) + foreach (var supervisor in new[] { false, true }) + foreach (var trace in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + var id = $"68010/format8/entry/{kind}/super={supervisor}/trace={trace}/ccr={ccr:X2}"; + Record(report, id, () => + { + m.Reset(ccr, supervisor); + if (trace) m.Core.State.StatusRegister |= 0x8000; + // Logical fault address retains its high byte; physical transfers wrap to 24 bits. + m.Core.State.A[0] = kind == "pre-read" ? 0xab004003u : kind == "pre-write" ? 0xab004005u : 0xab004001u; + m.Core.State.D[0] = 0x80011234; + var opcode = kind switch + { + "read-word" => 0x3010, "read-long" => 0x2010, "write-word" => 0x3080, + "write-long" => 0x2080, "movea-word" => 0x3250, "pre-read" => 0x3020, + "pre-write" => 0x2100, _ => 0x4ed0 + }; + m.InitializePhysical(0x4000, 0x5aa55aa5, 4); + // Mark the complete frame and its surroundings, including the three reserved holes. + var stack = supervisor ? 0x4700u : 0x8000u; + for (uint offset = 0; offset < 66; offset += 2) m.InitializePhysical(stack - 62 + offset, 0xbeef, 2); + var e = SyntheticExecution.Prepare(m, [(ushort)opcode]); + var savedSr = e.Sr; + // Preserve the existing MOVE fault-side-effect policy, separately from physical qualification. + if (kind == "write-word") savedSr = (ushort)((savedSr & 0xfff0) | 0); + if (kind == "pre-write") savedSr = (ushort)((savedSr & 0xfff0) | 8); + // Retained descending MOVE.L write policy commits its base only after a successful write. + if (kind == "pre-read") e.A[0] = 0xab004001; + SwitchToSupervisorStack(e); + e.Sr = (ushort)((savedSr | 0x2000) & ~0x8000); + e.ExceptionVector = 3; e.Pc = 0x9030; e.A[7] -= 58; + var fp = e.A[7]; + e.Write(fp, savedSr, 2, m.Model); e.Write(fp + 2, SyntheticMachine.Code, 4, m.Model); + e.Write(fp + 6, 0x800c, 2, m.Model); + var write = kind is "write-word" or "write-long" or "pre-write"; + var faultAddress = kind == "pre-write" ? 0xab004003u : 0xab004001u; + var ssw = (uint)((kind == "fetch" ? 0x2002 : 0x1001) | (write ? 0 : 0x0100) | (supervisor ? 4 : 0)); + e.Write(fp + 8, ssw, 2, m.Model); e.Write(fp + 10, faultAddress, 4, m.Model); + // Input/internal words are implementation placeholders, not qualified restart state. + e.Write(fp + 16, write ? kind == "write-long" ? 0x8001u : 0x1234u : 0, 2, m.Model); + e.Write(fp + 20, 0, 2, m.Model); e.Write(fp + 24, 0, 2, m.Model); + for (uint offset = 26; offset < 58; offset += 2) e.Write(fp + offset, 0, 2, m.Model); + m.Core.ExecuteInstruction(); + var mismatch = e.Verify(m); + if (mismatch != null) return mismatch; + var writes = m.Bus.Accesses.Where(a => a.Write).ToArray(); + if (writes.Length != 26 || writes.Any(a => a.Width != 2 || a.Address < fp || a.Address >= fp + 58)) + return "Frame must write exactly 26 information words"; + if (writes.Any(a => a.Address == fp + 14 || a.Address == fp + 18 || a.Address == fp + 22)) + return "Reserved frame word was written"; + if (m.Bus.Accesses.Any(a => a.Address is 0x4001 or 0x4003)) return "Faulting transfer reached the external bus"; + return null; + }); + } + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void RteVersionValidationTailProbeAndStackSelection() + { + var m = NewMachine(); + var report = new CoverageBatch("68010", "system-format8-rte"); + for (var version = 0; version < 16; version++) + foreach (var otherBits in new[] { 0, 0x03ff, 0xc000, 0xc3ff }) + foreach (var restoredSupervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + var id = $"68010/format8/RTE/version={version}/other={otherBits:X4}/restoreSuper={restoredSupervisor}/ccr={ccr:X2}"; + Record(report, id, () => + { + m.Reset(ccr); + var fp = m.Core.State.A[7]; + for (uint offset = 0; offset < 62; offset += 2) m.InitializePhysical(fp + offset, 0x5a5a, 2); + var restoredSr = (ushort)((restoredSupervisor ? 0x2700 : 0x0700) | (ccr ^ 31)); + m.InitializePhysical(fp, restoredSr, 2); m.InitializePhysical(fp + 2, 0x6000, 4); + m.InitializePhysical(fp + 6, 0x800c, 2); + m.InitializePhysical(fp + 26, (uint)((version << 10) | otherBits), 2); + m.InitializePhysical(0x6000, 0x747b, 2); m.InitializePhysical(0x6002, 0x4e71, 2); + var e = SyntheticExecution.Prepare(m, [0x4e73]); + if (version == 0) + { + e.A[7] += 58; SyntheticSystemTests.ApplyStatus(m, e, restoredSr); e.Pc = 0x6000; + } + else SyntheticExecution.ExpectException(m, e, 14); + m.Core.ExecuteInstruction(); + var mismatch = e.Verify(m); + if (mismatch != null) return mismatch; + var reads = m.Bus.Accesses.Where(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address >= fp && a.Address < fp + 58).ToArray(); + // Header first, version next, then last word before the rest. Reserved holes are not information words. + var offsets = new List<(uint Address, int Width)> { (fp, 2), (fp + 2, 4), (fp + 6, 2), (fp + 26, 2) }; + if (version == 0) + { + offsets.Add((fp + 56, 2)); + for (uint offset = 8; offset < 56; offset += 2) + if (offset is not (14 or 18 or 22 or 26)) offsets.Add((fp + offset, 2)); + } + if (!reads.Select(a => (a.Address, a.Width)).SequenceEqual(offsets)) return "RTE frame validation/probe read order differs"; + if (version == 0) + { + m.Core.ExecuteInstruction(); e.D[2] = 123; e.Pc += 2; + e.Sr = (ushort)((e.Sr & 0xfff0) | 0); // MOVEQ #123,D2 + mismatch = e.Verify(m); + } + return mismatch; + }); + } + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void DoubleFaultHaltsUntilReset() + { + var m = NewMachine(); + var report = new CoverageBatch("68010", "system-format8-double-fault"); + foreach (var oddStack in new[] { false, true }) + foreach (var supervisor in new[] { false, true }) + foreach (var trace in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + var id = $"68010/format8/double-fault/oddStack={oddStack}/super={supervisor}/trace={trace}/ccr={ccr:X2}"; + Record(report, id, () => + { + m.Reset(ccr, supervisor); + if (trace) m.Core.State.StatusRegister |= 0x8000; + m.Core.State.A[0] = 0x4001; + if (!oddStack) m.InitializePhysical(12, 0x9031, 4); + _ = SyntheticExecution.Prepare(m, [0x3010]); + if (oddStack) + { + if (supervisor) m.Core.State.SetActiveStackPointer(0x4701); + else m.Core.State.SetInterruptStackPointer(0x8001); + } + var e = ArchitecturalExpectation.Capture(m); + SwitchToSupervisorStack(e); + e.A[7] -= oddStack ? 2u : 58u; + e.Sr = (ushort)((e.Sr | 0x2000) & ~0x8000); + e.Pc = oddStack ? 0x1002u : 0x9031u; e.Halted = true; e.ExceptionVector = 3; + if (!oddStack) + { + var savedSr = m.Core.State.StatusRegister; + var fp = e.A[7]; + for (uint offset = 0; offset < 58; offset += 2) + if (offset is not (14 or 18 or 22)) e.Write(fp + offset, 0, 2, m.Model); + e.Write(fp, savedSr, 2, m.Model); e.Write(fp + 2, 0x1000, 4, m.Model); + e.Write(fp + 6, 0x800c, 2, m.Model); + e.Write(fp + 8, supervisor ? 0x1105u : 0x1101u, 2, m.Model); + e.Write(fp + 10, 0x4001, 4, m.Model); + } + m.Core.ExecuteInstruction(); + var mismatch = e.Verify(m); + if (mismatch != null) return mismatch; + var accesses = m.Bus.Accesses.Count; + m.Core.ExecuteInstruction(); m.Core.RequestInterrupt(7, 31 * 4); + m.Core.BeginSubroutine(0x5000, 0x6000, 0x7000); m.Core.SwitchTaskContext(new M68kCpuState()); + if (m.Bus.Accesses.Count != accesses) return "Halted CPU accessed the bus"; + mismatch = e.Verify(m); if (mismatch != null) return mismatch; + m.InitializePhysical(0x5000, 0x747b, 2); m.InitializePhysical(0x5002, 0x4e71, 2); + m.Core.Reset(0x5000, 0x8000); + var reset = ArchitecturalExpectation.Capture(m); reset.Pc = 0x5002; reset.D[2] = 123; + m.Core.ExecuteInstruction(); return reset.Verify(m); + }); + } + report.Complete(output); + } + + private static SyntheticMachine NewMachine() => new(ModelSpec.All.Single(x => x.Id == "68010")); + + private static void SwitchToSupervisorStack(ArchitecturalExpectation e) + { + if ((e.Sr & 0x2000) != 0) return; + var usp = e.A[7]; e.A[7] = e.InactiveStackPointer!.Value; e.InactiveStackPointer = usp; + } + + private static void Record(CoverageBatch report, string id, Func run) + { + try { var mismatch = run(); report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException) + { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); } + } +} diff --git a/Copper68k/M68010Interpreter.cs b/Copper68k/M68010Interpreter.cs index 62525f65..dccfd6ea 100644 --- a/Copper68k/M68010Interpreter.cs +++ b/Copper68k/M68010Interpreter.cs @@ -214,6 +214,23 @@ protected override uint GetInterruptVectorAddress(uint vectorAddress) protected override bool IsSupportedRteFrameFormat(ushort format) => (format & 0xF000) is 0x0000 or 0x8000; + // MC68000UM 6.4: validate the version before changing SP, then probe + // the final word before loading the remaining information words. + // Version zero is this emulator's private convention. Opaque restart + // state and suspended-instruction continuation are not implemented. + protected override bool ValidateRteFrame(ushort format, uint framePointer) + { + if ((format & 0xF000) != 0x8000) return true; + if ((ReadWord(framePointer + 26) & 0x3C00) != 0) return false; + _ = ReadWord(framePointer + 56); + for (uint offset = 8; offset < 56; offset += 2) + { + if (offset is 14 or 18 or 22 or 26) continue; + _ = ReadWord(framePointer + offset); + } + return true; + } + protected override bool TryHandleModelSpecificExceptionFrame( int vector, uint stackedProgramCounter, @@ -229,16 +246,28 @@ protected override bool TryHandleModelSpecificAddressError( uint faultAddress, bool isWrite, M68kBusAccessKind accessKind, - bool useDataAccessStackedProgramCounter) + bool useDataAccessStackedProgramCounter, + ushort dataOutput) { - _ = faultAddress; - _ = isWrite; - _ = accessKind; _ = useDataAccessStackedProgramCounter; var stackedProgramCounter = State.LastInstructionProgramCounter; var savedStatusRegister = State.StatusRegister; State.RecordException(3, stackedProgramCounter, savedStatusRegister); State.StatusRegister = (ushort)((savedStatusRegister | M68kCpuState.Supervisor) & ~M68kCpuState.Trace); + // Figure 6-8: 58 bytes, 26 information words, three unwritten + // reserved words. Zeroed internal/input buffers are placeholders; + // this structural frame does not encode resumable internal state. + for (var i = 0; i < 16; i++) PushWord(0); + PushWord(0); // instruction input buffer (unqualified) + State.SetActiveStackPointer(State.A[7] - 2); // reserved + PushWord(0); // data input buffer (unqualified) + State.SetActiveStackPointer(State.A[7] - 2); // reserved + PushWord(dataOutput); + State.SetActiveStackPointer(State.A[7] - 2); // reserved + PushLong(faultAddress); + var instruction = accessKind == M68kBusAccessKind.CpuInstructionFetch; + var functionCode = (instruction ? 2 : 1) | ((savedStatusRegister & M68kCpuState.Supervisor) != 0 ? 4 : 0); + PushWord((ushort)((instruction ? 0x2000 : 0x1000) | (isWrite ? 0 : 0x0100) | functionCode)); PushWord(0x800C); PushLong(stackedProgramCounter); PushWord(savedStatusRegister); diff --git a/Copper68k/M68kCore.cs b/Copper68k/M68kCore.cs index 49cb1307..90739dd8 100644 --- a/Copper68k/M68kCore.cs +++ b/Copper68k/M68kCore.cs @@ -2332,8 +2332,8 @@ private enum PlannedRetireMode : byte private byte _deferredCpuBusBatchAdmissionRetryInstructions; private uint _activeInstructionProgramCounter; private bool _instructionTracePending; - private bool _processingM68000AddressError; - private bool _m68000DoubleFaultHalted; + private bool _processingM68000FamilyAddressError; + private bool _addressErrorDoubleFaultHalted; internal bool ArchitecturalTraceEnabled { get; set; } = true; private uint _dataAccessStackedProgramCounter; private ushort? _addressErrorInstructionWord; @@ -9172,12 +9172,16 @@ protected virtual bool IsSupportedRteFrameFormat(ushort format) return true; } + protected virtual bool ValidateRteFrame(ushort format, uint framePointer) => true; + protected virtual bool TryHandleModelSpecificAddressError( uint faultAddress, bool isWrite, M68kBusAccessKind accessKind, - bool useDataAccessStackedProgramCounter) + bool useDataAccessStackedProgramCounter, + ushort dataOutput) { + _ = dataOutput; _ = faultAddress; _ = isWrite; _ = accessKind; @@ -9201,8 +9205,8 @@ protected virtual bool TryHandleModelSpecificExceptionFrame( public void Reset(uint programCounter, uint stackPointer) { FlushDeferredCpuTimingBoundary(); - _processingM68000AddressError = false; - _m68000DoubleFaultHalted = false; + _processingM68000FamilyAddressError = false; + _addressErrorDoubleFaultHalted = false; Array.Clear(State.D); Array.Clear(State.A); State.ProgramCounter = programCounter; @@ -9227,7 +9231,7 @@ public void Reset(uint programCounter, uint stackPointer) public void SwitchTaskContext(M68kCpuState next) { ArgumentNullException.ThrowIfNull(next); - if (_m68000DoubleFaultHalted) return; + if (_addressErrorDoubleFaultHalted) return; FlushDeferredCpuTimingBoundary(); State.CopyTaskContextFrom(next); FlushPrefetch(); @@ -9262,7 +9266,7 @@ public void BeginSubroutine(uint address, uint stackPointer, uint returnAddress) { // A host subroutine entry may resume an ordinary host-requested halt, // but a catastrophic group-0 fault requires a CPU reset. - if (_m68000DoubleFaultHalted) return; + if (_addressErrorDoubleFaultHalted) return; State.SetActiveStackPointer(stackPointer); PushLong(returnAddress); SetProgramCounterAndFlushPrefetch(address); @@ -10346,7 +10350,7 @@ private bool DecodeLine4(ushort opcode, uint instructionPc) if (UsesFormatWordExceptionFrames) { var format = ReadWord(framePointer + 6); - if (!IsSupportedRteFrameFormat(format)) + if (!IsSupportedRteFrameFormat(format) || !ValidateRteFrame(format, framePointer)) { RaiseException(14, instructionPc, 34); return true; } frameSize = (format & 0xF000) == 0x8000 ? 58u : (format & 0xF000) == 0x2000 ? 12u : 8u; @@ -13941,7 +13945,7 @@ private byte ReadByte(uint address) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private ushort ReadWord(uint address) + protected ushort ReadWord(uint address) { if (!_useM68020BriefIndexedAddressing && (address & 1) != 0) { @@ -14061,7 +14065,7 @@ private void WriteWord(uint address, ushort value) { if (!_useM68020BriefIndexedAddressing && (address & 1) != 0) { - ThrowOddAddressAccess(address, isWrite: true, M68kBusAccessKind.CpuDataWrite); + ThrowOddAddressAccess(address, isWrite: true, M68kBusAccessKind.CpuDataWrite, dataOutput: value); } var busAddress = GetCpuBusAddress(address); @@ -14095,7 +14099,7 @@ private void WriteLong(uint address, uint value) } if (!_useM68020BriefIndexedAddressing && (address & 1) != 0) { - ThrowOddAddressAccess(address, isWrite: true, M68kBusAccessKind.CpuDataWrite); + ThrowOddAddressAccess(address, isWrite: true, M68kBusAccessKind.CpuDataWrite, dataOutput: (ushort)(value >> 16)); } var busAddress = GetCpuBusAddress(address); @@ -14130,14 +14134,15 @@ private void ThrowOddAddressAccess( uint address, bool isWrite, M68kBusAccessKind accessKind, - bool useDataAccessStackedProgramCounter = false) + bool useDataAccessStackedProgramCounter = false, + ushort dataOutput = 0) { var faultCycle = BeginCpuBusAccessCycle(); _deferredCpuInstructionTiming?.FlushDeferredCpuInstructionTiming(ref faultCycle); State.Cycles = faultCycle; _cpuBusCycle = faultCycle; _cpuRetireBusCycle = Math.Max(_cpuRetireBusCycle, faultCycle); - RaiseAddressError(address, isWrite, accessKind, useDataAccessStackedProgramCounter); + RaiseAddressError(address, isWrite, accessKind, useDataAccessStackedProgramCounter, dataOutput); throw M68kAddressErrorException.Instance; } @@ -14767,27 +14772,31 @@ private void RaiseAddressError( uint faultAddress, bool isWrite, M68kBusAccessKind accessKind, - bool useDataAccessStackedProgramCounter = false) + bool useDataAccessStackedProgramCounter = false, + ushort dataOutput = 0) { _instructionTracePending = false; - if (_processingM68000AddressError) - { - HaltM68000DoubleFault(); - return; - } - if (TryHandleModelSpecificAddressError( - faultAddress, - isWrite, - accessKind, - useDataAccessStackedProgramCounter)) + if (_processingM68000FamilyAddressError) { - AddInstructionCycles(AddressErrorExceptionCycles); + HaltAddressErrorDoubleFault(); return; } - - _processingM68000AddressError = !_useM68020BriefIndexedAddressing; + _processingM68000FamilyAddressError = !_useM68020BriefIndexedAddressing; try { + if (TryHandleModelSpecificAddressError( + faultAddress, + isWrite, + accessKind, + useDataAccessStackedProgramCounter, + dataOutput)) + { + if (_processingM68000FamilyAddressError && (State.ProgramCounter & 1) != 0) + HaltAddressErrorDoubleFault(); + AddInstructionCycles(AddressErrorExceptionCycles); + return; + } + var exceptionCycleBase = Math.Max(Math.Max(State.Cycles, _cpuRetireBusCycle), _instructionCycleFloor); var savedStatusRegister = State.StatusRegister; var instructionWord = _addressErrorInstructionWord ?? State.LastOpcode; @@ -14806,18 +14815,18 @@ private void RaiseAddressError( SetProgramCounterAndFlushPrefetch(ReadLong(0x0000_000C)); // Handler entry is still part of group-0 exception processing. // Detect its odd instruction address before another frame or fetch. - if (_processingM68000AddressError && (State.ProgramCounter & 1) != 0) HaltM68000DoubleFault(); + if (_processingM68000FamilyAddressError && (State.ProgramCounter & 1) != 0) HaltAddressErrorDoubleFault(); AddInstructionCyclesFromBase(exceptionCycleBase, AddressErrorExceptionCycles); } finally { - _processingM68000AddressError = false; + _processingM68000FamilyAddressError = false; } } - private void HaltM68000DoubleFault() + private void HaltAddressErrorDoubleFault() { - _m68000DoubleFaultHalted = true; + _addressErrorDoubleFaultHalted = true; State.Halted = true; State.Stopped = false; _instructionTracePending = false; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index c8f8ee78..2da52963 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -117,7 +117,9 @@ A private double-fault latch prevents host subroutine/task entry from waking thi architectural halt. The supplied-PC/SP `Reset` API clears it. The existing host convention of explicitly setting `Halted` and subsequently entering a subroutine continues to work; that convention is separately tested. No public API was added. -The 010 model hook and advanced compatibility mode retain their separate behavior. +At this checkpoint the 010 model hook retained its separate behavior; the +subsequent 010 structural slice extends the same entry guard to that hook. +Advanced compatibility mode retains its separate behavior. `68000/system-double-fault` adds 640 public-factory cases across all CCRs, user/supervisor stacks and trace states: odd exception stacks, odd handler PCs, @@ -307,6 +309,50 @@ separate compiled-JIT failure), `artifacts/double-fault-final/`, isolated consumer's `artifacts/double-fault-validation/` and `artifacts/double-fault-diagnostic-tests/`. +### 010 format-8 structural follow-up + +[MC68000UM sections 6.3.9.2, 6.3.10 and 6.4, figures 6-8/6-9](https://www.nxp.com/docs/en/reference-manual/MC68000UM.pdf) +specify a 58-byte address/bus-error frame, with 26 information words written and +three reserved words left unwritten. The prior 010 address-error path allocated +only eight bytes while labelling the frame format 8. RTE then skipped 58 bytes +without reading or validating its internal version. Two bounded repros failed +before this correction: incorrect frame allocation and an incompatible version +accepted without vector 14. + +The 010 address-error path now allocates the complete frame, leaves offsets +14/18/22 untouched, and records the logical fault address, scoped SSW read/write, +instruction/data and function-code fields, and the faulting output word. The +ordinary word/long MOVE reads/writes, MOVEA read, predecrement forms and JMP target +fixtures distinguish logical 32-bit frame addresses from 24-bit physical transfers. +The existing saved-PC convention, partial MOVE side effects and exception cycle +policy are preserved; these are not new physical prefetch/timing qualification. +No instruction is retried after operand effects. + +RTE validates version bits 10-13 of the first internal word at SP+26 before +changing SP/SR. An incompatible version raises a format-error frame below the +intact original. For accepted version zero it probes SP+56 before reading the +remaining information words, skipping reserved holes, then performs the existing +structural pop and stack selection. Version zero is an emulator-private convention, +not a universal claim about hardware revisions. Zeroed input buffers and internal +words are placeholders. RR-controlled cycle/instruction continuation, interrupted +RMW semantics and physical prefetch buffer contents remain **unqualified**. This +slice must not be interpreted as completion of format-8 restart support. + +The shared active-entry guard now also encloses the 010 hook. An odd supervisor +stack or odd handler PC halts until the supplied-PC/SP reset API, preserving +completed writes and rejecting interrupt/task/subroutine wakeups. The public bus +API does not signal external BERR; tail-probe accessibility failures, double-BERR +loading behavior and bus-error restart cannot be qualified by this fixture. + +The three new batches execute **5,376 cases**: `system-format8-entry` 1,024, +`system-format8-rte` 4,096 and `system-format8-double-fault` 256. They cover all +CCRs, both stacks and trace states for entry/halt, all 16 version fields, independent +non-version bits, restored user/supervisor stacks, exact PC and a following MOVEQ +sentinel. Common verification checks all registers, defined SR, execution state, +memory and surroundings; the recording bus checks 26 information writes, reserved +holes, rejected odd transfers and version/probe/tail read order. Placeholder values +are tested as implementation conventions only. No old regression is retired. + ## Validation checkpoint: 000 compiled JIT alignment The full ordinary CPU suite passes **4,619 tests**, with eight optional/opt-in @@ -336,3 +382,33 @@ Evidence is in the CPU checkout's `artifacts/jit-address-error-before/`, the isolated consumer's `artifacts/jit-address-error-validation/`, `artifacts/jit-address-error-diagnostic-tests/` and `artifacts/jit-address-error-production.binlog`. + +## Validation checkpoint: 010 format-8 structure + +The full ordinary Release CPU suite passes **4,624 tests**, with eight optional/ +opt-in skips and zero failures. The final focused run passes 43 tests/batches. +Deterministic report validation passes **8,374,056 logical cases in 343 batches**, +including all 5,376 new structural cases. Previous reports missing the new groups +are rejected. The requested pinned Musashi audit executes and passes 538 program/ +profile combinations with 86 explicit exclusions and no mismatches, from revision +`72c1d74800f3087b45a0c1a7342601bbed898881`. AHX passes 18 tests. The two bounded +before-fix regressions fail, then pass after correction. No new seeded run or +regression retirement is claimed. + +Private package `1.5.2-synthetic-dev.37` has SHA-256 +`8c3fe8b434b5b9bef6ff57c5e8148739b560abcb6ebd93c49acb8dccf8de059a`. +It remains unpublished. The isolated CopperScreen baseline `d9beae8` resolves +this exact NuGet version in production and separate diagnostic outputs. Release +build passes with zero warnings/errors; host 149, disk 74 and engine diagnostics +1,080 pass. The ordinary host invocation has six optional/media skips; these are +unavailable coverage. Separate native Workbench 3.1 at 0/2 MiB Fast RAM and +A1200 eight-plane hard-disk boot/reopen persistence execute all three cases +without skips. These are correctness replays, not throughput or physical timing +qualification. Unrelated root working changes are preserved. + +Evidence: CPU `artifacts/format8-before/`, `artifacts/format8-focused/`, +`artifacts/format8-final/`, `artifacts/format8-ahx/` and +`artifacts/synthetic-private-feed-37/`; isolated consumer +`artifacts/format8-validation/`, `artifacts/format8-diagnostic-tests/` and +`artifacts/format8-production.binlog`. Full 010 suspended-instruction restart, +external BERR and physical timing qualification remain open. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index feb32628..634bd565 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -548,3 +548,26 @@ unpublished `1.5.2-synthetic-dev.36` passes the isolated CopperScreen production build, host 149/disk 74/diagnostics 1,080 and all three native boot/persistence cases without native skips. The reference document records package identity, mutation evidence, retained timing policy and remaining qualification limits. + +The subsequent 010 format-8 structural slice adds 5,376 deterministic cases in +three batches, bringing the semantic gate to 8,374,056 cases in 343 batches. +Address errors now allocate 58 bytes, write the 26 information words and preserve +the three reserved holes. RTE validates its private version before popping and +probes the final word before loading the tail. The shared address-error guard +also protects 010 frame construction and handler entry from recursive faults, +with reset-only halt recovery. Existing MOVE side effects, saved-PC convention +and exception timing policy are retained. + +Milestone 6 remains in progress: the structural frame uses placeholder input and +internal state, and does not resume a suspended instruction or implement RR +continuation. Version-zero acceptance is an emulator convention. RMW, physical +prefetch/stack-cycle sequencing, external BERR and the other advanced restart +formats remain unqualified. No regression is retired. The reference document +records the bounded before-fix failures, scoped gate and validation evidence. + +Validation of the structural slice passes 4,624 ordinary CPU tests (eight optional/ +opt-in skips), the 8,374,056-case gate, 538 pinned independent program/profile +combinations (86 explicit exclusions) and 18 AHX tests. Unpublished private package +`1.5.2-synthetic-dev.37` passes isolated CopperScreen Release, host 149, disk 74 and +separate diagnostics 1,080; all three native boot/persistence replays execute +without skips. The reference document records its hash and evidence. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 634c792e..c3894530 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -94,6 +94,11 @@ try { 'system-status'=$(if ($model -in @('68000','68010')) {16540} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {16936}); 'system-trace'=$(if ($model -in @('68000','68010','68060')) {1284} elseif ($model -in @('68020','68030','68040','68EC020','A1200')) {2568})} if ($model -eq '68000') { $expected['system-double-fault'] = 640 } + if ($model -eq '68010') { + $expected['system-format8-entry'] = 1024 + $expected['system-format8-rte'] = 4096 + $expected['system-format8-double-fault'] = 256 + } if ($model -eq '68040') { $expected['system-mmu-disabled'] = 34850 } foreach ($group in $expected.Keys) { $report = Get-Content -LiteralPath (Join-Path $output "$model-$group.json") -Raw | ConvertFrom-Json @@ -161,10 +166,11 @@ try { } $inventory | Add-Member -NotePropertyName qualificationBoundaries -NotePropertyValue @('RTE fault restart/internal formats remain untested: 010 frame8 restart, 020/030 frames9/A/B, 040 frame7', 'BKPT external replacement responder is unavailable', 'MOVES physical function-code spaces and LPSTOP CPU-space broadcast are unqualified', 'CALLM/RTM type1 protocol uses an internal synthetic responder; normal public buses have none') $inventory | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'qualified-inventory.json') - @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=340; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; m68040DisabledMmuInstructionGate='passing'; m68000AddressErrorDoubleFaultGate='passing'; roadmapComplete=$false; + @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=343; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; m68040DisabledMmuInstructionGate='passing'; m68000AddressErrorDoubleFaultGate='passing'; m68010Format8StructureGate='passing'; roadmapComplete=$false; seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; + m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; opaque restart/input state, RMW and physical timing remain unqualified'; m68000DoubleFaultScope='Address-error entry/handler faults; reset-only recovery; external BERR/reset-vector faults unavailable through the current public bus API'; unavailableSystemCoverage=@('RTE bus-fault internal restart on 010/020/030', '020/030 coprocessor midinstruction restoration', '040 access-fault pending trace/writeback restoration', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast'); timing='Semantic gate; timing policy and physical qualification remain separate'} | From ff059188b9246fa2cccd6eb5be7ab8d8ba727075 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Sun, 4 Oct 2026 20:24:28 +0300 Subject: [PATCH 008/211] Resume 68010 word MOVE cycles from format-8 frames --- Copper68k.Tests/Synthetic/README.md | 16 +- .../Synthetic/SyntheticM68010Format8Tests.cs | 12 +- .../SyntheticM68010MoveRestartTests.cs | 340 ++++++++++++++++++ Copper68k/M68010Interpreter.cs | 54 ++- Copper68k/M68010WordMoveResumeFrame.cs | 36 ++ Copper68k/M68kCore.cs | 52 +++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 113 +++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 24 ++ scripts/test-copper68k-synthetic.ps1 | 13 +- 9 files changed, 638 insertions(+), 22 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68010MoveRestartTests.cs create mode 100644 Copper68k/M68010WordMoveResumeFrame.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 3ad9f404..1f372fc3 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -16,7 +16,7 @@ From the CopperMod root, run: Milestones 1–5 execute MOVE/MOVEA, transfer/address, arithmetic/comparison, logical/bit/shift/atomic and control/system operations in -**8,374,056 logical cases** across **343 xUnit batches**, across seven models and the A1200 profile. +**8,501,832 logical cases** across **348 xUnit batches**, across seven models and the A1200 profile. Ordinary `dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release` includes these batches. CI additionally validates every required report and exact count; missing reports, mismatches, unsupported execution and empty groups fail the gate. @@ -41,9 +41,17 @@ The 010-only `system-format8-entry` (1,024 cases), `system-format8-rte` prerequisites: the 58-byte address-error frame, 26 word writes with reserved holes untouched, scoped read/write/fetch metadata, version rejection before popping, tail probing before remaining reads, stack selection and halt/reset -recovery. Emulator version zero and zeroed input/internal buffers are private -conventions. Suspended-instruction continuation, RR behavior, RMW/prefetch buffer -semantics, external BERR and physical frame-cycle timing remain unqualified. +recovery. Emulator version zero and internal images are private conventions. + +Five 010 `system-move-word-restart-*` groups add 127,776 cases for generated +word-MOVE/MOVEA images: source faults 62,208; destination faults 64,512; copied/ +nested/alias/prefetch/trace edges 640; user A7 192; malformed private images 224. +RR=0 retries only the stacked word cycle; RR=1 uses the input image or accepts +a software-completed write. Earlier operand reads and register effects are never +replayed. The private image contains continuation phase and completed prefetch +words, so copied frames need no side table. Long transfers, non-MOVE families, +foreign hardware internal images, RMW, external BERR and physical restart timing +remain unqualified; this is not full format-8 restart qualification. Reports contain per-scenario identifiers and counts for passing, mismatching, unsupported and untested outcomes. `integer-inventory.json` lists all integer diff --git a/Copper68k.Tests/Synthetic/SyntheticM68010Format8Tests.cs b/Copper68k.Tests/Synthetic/SyntheticM68010Format8Tests.cs index d4f4453c..651a9076 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68010Format8Tests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68010Format8Tests.cs @@ -88,7 +88,16 @@ public void AddressErrorInformationWordsAndReservedHoles() // Input/internal words are implementation placeholders, not qualified restart state. e.Write(fp + 16, write ? kind == "write-long" ? 0x8001u : 0x1234u : 0, 2, m.Model); e.Write(fp + 20, 0, 2, m.Model); e.Write(fp + 24, 0, 2, m.Model); - for (uint offset = 26; offset < 58; offset += 2) e.Write(fp + offset, 0, 2, m.Model); + for (uint offset = 26; offset < 58; offset += 2) + { + if (kind is "read-word" or "write-word" or "movea-word" or "pre-read") + { + // Private continuation contents have their own restart gate. + e.MemoryMasks[m.Model.Physical(fp + offset)] = 0; + e.MemoryMasks[m.Model.Physical(fp + offset + 1)] = 0; + } + else e.Write(fp + offset, 0, 2, m.Model); + } m.Core.ExecuteInstruction(); var mismatch = e.Verify(m); if (mismatch != null) return mismatch; @@ -194,6 +203,7 @@ public void DoubleFaultHaltsUntilReset() e.Write(fp + 6, 0x800c, 2, m.Model); e.Write(fp + 8, supervisor ? 0x1105u : 0x1101u, 2, m.Model); e.Write(fp + 10, 0x4001, 4, m.Model); + for (uint offset = 26; offset < 58; offset++) e.MemoryMasks[m.Model.Physical(fp + offset)] = 0; } m.Core.ExecuteInstruction(); var mismatch = e.Verify(m); diff --git a/Copper68k.Tests/Synthetic/SyntheticM68010MoveRestartTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68010MoveRestartTests.cs new file mode 100644 index 00000000..6da86041 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68010MoveRestartTests.cs @@ -0,0 +1,340 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticM68010MoveRestartTests(ITestOutputHelper output) +{ + [Fact] + public void SoftwareCompletedReadUsesBufferWithoutRepeatingPostincrement() + { + var m = NewMachine(0x3018); // MOVE.W (A0)+,D0 + m.Core.State.A[0] = 0x4001; + m.Start(); + m.Core.ExecuteInstruction(); + var fp = m.Core.State.A[7]; + Assert.Equal(0x4003u, m.Core.State.A[0]); + m.InitializePhysical(fp + 8, m.PeekPhysical(fp + 8, 2) | 0x8000, 2); + m.InitializePhysical(fp + 20, 0x8001, 2); + m.Bus.Accesses.Clear(); + m.Core.ExecuteInstruction(); // RTE completes the suspended MOVE, without decoding it again. + Assert.Equal(0x1002u, m.Core.State.ProgramCounter); + Assert.Equal(0xa55a8001u, m.Core.State.D[0]); + Assert.Equal(0x4003u, m.Core.State.A[0]); + Assert.Equal(0x2718, m.Core.State.StatusRegister); + Assert.DoesNotContain(m.Bus.Accesses, a => a.Address == 0x4001); + } + + [Fact] + public void SoftwareCompletedWriteDoesNotReadSourceOrRepeatItsIncrement() + { + var m = NewMachine(0x32d8); // MOVE.W (A0)+,(A1)+ + m.Core.State.A[0] = 0x4000; m.Core.State.A[1] = 0x4101; + m.InitializePhysical(0x4000, 0x8001, 2); + m.Start(); m.Core.ExecuteInstruction(); + var fp = m.Core.State.A[7]; + Assert.Equal(0x4002u, m.Core.State.A[0]); + m.InitializePhysical(fp + 8, m.PeekPhysical(fp + 8, 2) | 0x8000, 2); + m.InitializePhysical(0x4101, 0x8001, 2); // exception handler completes the write + m.InitializePhysical(0x4000, 0x1234, 2); + m.Bus.Accesses.Clear(); m.Core.ExecuteInstruction(); + Assert.Equal(0x1002u, m.Core.State.ProgramCounter); + Assert.Equal(0x4002u, m.Core.State.A[0]); + Assert.Equal(0x4103u, m.Core.State.A[1]); + Assert.DoesNotContain(m.Bus.Accesses, a => a.Address is 0x4000 or 0x4101); + } + + [Fact] + public void ProcessorRerunUsesStackedAddressDespiteRegisterCorrection() + { + var m = NewMachine(0x3010); // MOVE.W (A0),D0 + m.Core.State.A[0] = 0x4001; + m.Start(); m.Core.ExecuteInstruction(); + var fp = m.Core.State.A[7]; + m.Core.State.A[0] = 0x4000; // correcting only the register does not correct the stacked cycle + m.Bus.Accesses.Clear(); m.Core.ExecuteInstruction(); + Assert.Equal(0x9030u, m.Core.State.ProgramCounter); + Assert.Equal(3, m.Core.State.LastExceptionVector); + Assert.Equal(fp, m.Core.State.A[7]); + Assert.Equal(0x4001u, m.PeekPhysical(fp + 10, 4)); + Assert.Equal(0xa55a0022u, m.Core.State.D[0]); + Assert.Equal(0x4000u, m.Core.State.A[0]); + Assert.DoesNotContain(m.Bus.Accesses, a => a.Address == 0x4001); + } + + + private static readonly uint[] Values = [0, 1, 0x7fff, 0x8000, 0x8001, 0xffff]; + private static readonly OperandForm[] MemorySources = [new(2,0), new(3,0), new(4,0), new(5,0), new(6,0), new(7,0), new(7,1), new(7,2), new(7,3)]; + private static readonly OperandForm[] MemoryDestinations = [new(2,1), new(3,1), new(4,1), new(5,1), new(6,1), new(7,0), new(7,1)]; + + [Fact, Trait("Suite", "Synthetic")] + public void SourceFaultOperandFormsValuesAndCcr() + { + var report = new CoverageBatch("68010", "system-move-word-restart-source"); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68010")); + foreach (var source in MemorySources) + foreach (var destination in MemoryDestinations.Concat([new OperandForm(0,2), new OperandForm(1,2)])) + foreach (var value in Values) + foreach (var software in new[] { false, true }) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + Transfer(m, report, source, destination, value, ccr, supervisor, software, writeFault: false); + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void DestinationFaultOperandFormsValuesAndCcr() + { + var report = new CoverageBatch("68010", "system-move-word-restart-destination"); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68010")); + foreach (var source in MemorySources.Concat([new OperandForm(0,2), new OperandForm(1,2), new OperandForm(7,4)])) + foreach (var destination in MemoryDestinations) + foreach (var value in Values) + foreach (var software in new[] { false, true }) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + Transfer(m, report, source, destination, value, ccr, supervisor, software, writeFault: true); + report.Complete(output); + } + + private static void Transfer(SyntheticMachine m, CoverageBatch report, OperandForm source, OperandForm destination, + uint value, int ccr, bool supervisor, bool software, bool writeFault) + { + var opcode = MoveSpecification.Encode(2, source, destination); + var id = $"68010/MOVE.W/restart/{(writeFault ? "destination" : "source")}/{source}/{destination}/super={supervisor}/RR={(software ? 1 : 0)}/op={opcode:X4}/value={value:X4}/ccr={ccr:X2}"; + Record(report, id, () => + { + m.Reset(ccr, supervisor); + m.Core.State.D[2] = 0xa55a0000 | value; + m.Core.State.A[2] = 0x89ab0000 | value; + if (!writeFault && source.Mode is 2 or 3 or 4) m.Core.State.A[0] = source.Mode == 4 ? 0x4003u : 0x4001u; + if (writeFault && destination.Mode is 2 or 3 or 4) m.Core.State.A[1] = destination.Mode == 4 ? 0x4103u : 0x4101u; + var words = new List { opcode }; + var finalD = (uint[])m.Core.State.D.Clone(); var finalA = (uint[])m.Core.State.A.Clone(); + var options = new AddressOptions( + SourceDisplacement: (short)(writeFault ? 0x40 : 0x41), DestinationDisplacement: (short)(writeFault ? 0x61 : 0x60), + SourceAbsoluteWord: (ushort)(writeFault ? 0x6000 : 0x6001), DestinationAbsoluteWord: (ushort)(writeFault ? 0x6101 : 0x6100), + SourceAbsoluteLong: writeFault ? 0xab006200u : 0xab006201u, DestinationAbsoluteLong: writeFault ? 0xab006301u : 0xab006300u, + SourceIndex: new(BriefDisplacement: (sbyte)(writeFault ? 0x40 : 0x41)), + DestinationIndex: new(BriefDisplacement: (sbyte)(writeFault ? 0x61 : 0x60))); + var fixture = new AddressingFixture(m, 2, words, finalD, finalA, value, options); + var sourceAddress = source.Mode >= 2 ? fixture.Resolve(source, true) : 0; + var destinationAddress = destination.Mode >= 2 ? fixture.Resolve(destination, false) : 0; + if (source.Mode >= 2 && source != new OperandForm(7,4)) m.InitializePhysical(sourceAddress, value, 2); + var nextPc = fixture.NextPc; + m.InitializePhysical(nextPc, 0x7c7b, 2); m.InitializePhysical(nextPc + 2, 0x4e71, 2); + m.InitializePhysical(0x9030, 0x4e73, 2); m.InitializePhysical(0x9032, 0x4e71, 2); + for (var i = 0; i < words.Count; i++) m.InitializePhysical(0x1000 + (uint)i * 2, words[i], 2); + m.Start(); m.Core.ExecuteInstruction(); + if (m.Core.State.LastExceptionVector != 3) return "Fixture did not produce the required address error"; + var fp = m.Core.State.A[7]; var savedSr = (ushort)m.PeekPhysical(fp, 2); + if (m.PeekPhysical(fp + 28, 2) != 0xc010 || m.PeekPhysical(fp + 30, 2) != opcode || m.PeekPhysical(fp + 32, 2) != (writeFault ? 1u : 0u)) + return "Generated private image did not identify the suspended word-MOVE phase"; + var fault = writeFault ? destinationAddress : sourceAddress; + if (m.PeekPhysical(fp + 10, 4) != fault) return "Incorrect logical fault address"; + var rerunAddress = software ? fault : unchecked(fault + 1); + if (software) m.InitializePhysical(fp + 8, m.PeekPhysical(fp + 8, 2) | 0x8000, 2); + else m.InitializePhysical(fp + 10, rerunAddress, 4); + if (!writeFault) + { + // PC-relative reads use the established program-space/input-buffer convention. + var instructionBuffer = source.Mode == 7 && source.Register is 2 or 3; + m.InitializePhysical(fp + 20, instructionBuffer ? value ^ 0xffff : value, 2); + m.InitializePhysical(fp + 24, instructionBuffer ? value : value ^ 0xffff, 2); + if (!software) m.InitializePhysical(rerunAddress, value, 2); + } + else + { + if (source.Mode is 0 or 1) + { + if (source.Mode == 0) m.Core.State.D[2] ^= 0xffff; + else m.Core.State.A[2] ^= 0xffff; + } + else if (source != new OperandForm(7,4)) m.InitializePhysical(sourceAddress, value ^ 0xffff, 2); + if (software) m.InitializePhysical(rerunAddress, value, 2); // software performs the faulting write + } + var e = ArchitecturalExpectation.Capture(m); + e.A[7] = fp + 58; SyntheticSystemTests.ApplyStatus(m, e, savedSr); + for (var r = 0; r < 7; r++) + if (!(writeFault && source.Mode == 1 && source.Register == r)) e.A[r] = finalA[r]; + if (!writeFault && destination.Mode == 0) e.D[destination.Register] = (e.D[destination.Register] & 0xffff0000) | value; + if (!writeFault && destination.Mode == 1) e.A[destination.Register] = unchecked((uint)(int)(short)value); + if (destination.Mode != 1) e.Sr = SyntheticExecution.MoveFlags(e.Sr, value, 2); + e.Pc = nextPc; + if (destination.Mode >= 2) e.Write(writeFault ? rerunAddress : destinationAddress, value, 2, m.Model); + m.Bus.Accesses.Clear(); m.Core.ExecuteInstruction(); + var mismatch = e.Verify(m); if (mismatch != null) return mismatch; + var physicalFault = m.Model.Physical(fault); var physicalRerun = m.Model.Physical(rerunAddress); + if (m.Bus.Accesses.Any(a => a.Address == physicalFault)) return "Original odd bus transfer was repeated"; + if (writeFault && source.Mode >= 2 && source != new OperandForm(7,4) && m.Bus.Accesses.Any(a => a.Address == m.Model.Physical(sourceAddress))) + return "Completed source was read again"; + if (m.Bus.Accesses.Count(a => a.Address == physicalRerun && a.Write == writeFault && a.Width == 2) != (software ? 0 : 1)) + return "Faulted word cycle must rerun exactly once, or be supplied by software"; + m.Core.ExecuteInstruction(); e.D[6] = 123; e.Pc += 2; e.Sr = (ushort)(e.Sr & 0xfff0); + return e.Verify(m); + }); + } + + + [Fact, Trait("Suite", "Synthetic")] + public void CopiedNestedAliasedPrefetchedAndTracedContinuations() + { + var report = new CoverageBatch("68010", "system-move-word-restart-edges"); + foreach (var kind in new[] { "copied", "nested", "alias", "prefetched", "trace" }) + foreach (var software in new[] { false, true }) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + var id = $"68010/MOVE.W/restart/{kind}/super={supervisor}/RR={(software ? 1 : 0)}/ccr={ccr:X2}"; + Record(report, id, () => + { + var opcode = kind == "alias" ? (ushort)0x30d8 : kind == "nested" ? (ushort)0x32d8 : (ushort)0x3418; + var m = NewMachine(opcode); + m.Core.State.StatusRegister = (ushort)((supervisor ? 0x2700 : 0x0700) | ccr | (kind == "trace" ? 0x8000 : 0)); + m.Core.State.SetActiveStackPointer(supervisor ? 0x4700u : 0x7800u); + m.Core.State.A[0] = 0x4001; m.Core.State.A[1] = 0x4101; + m.InitializePhysical(0x1002, 0x7c7b, 2); + m.Start(); m.Core.ExecuteInstruction(); + var fp = m.Core.State.A[7]; + if (kind == "copied") + { + for (uint offset = 0; offset < 58; offset += 2) m.InitializePhysical(0x6800 + offset, m.PeekPhysical(fp + offset, 2), 2); + fp = 0x6800; m.Core.State.SetActiveStackPointer(fp); + } + var sourceRerun = software ? 0x4001u : 0x4002u; + if (software) + { m.InitializePhysical(fp + 8, m.PeekPhysical(fp + 8, 2) | 0x8000, 2); m.InitializePhysical(fp + 20, 0x8001, 2); } + else { m.InitializePhysical(fp + 10, sourceRerun, 4); m.InitializePhysical(sourceRerun, 0x8001, 2); } + if (kind == "prefetched") m.InitializePhysical(0x1002, 0x7cff, 2); // buffer must retain MOVEQ #123,D6 + var e = ArchitecturalExpectation.Capture(m); + var savedSr = (ushort)m.PeekPhysical(fp, 2); + e.A[7] = fp + 58; SyntheticSystemTests.ApplyStatus(m, e, savedSr); + e.Sr = SyntheticExecution.MoveFlags(e.Sr, 0x8001, 2); e.Pc = 0x1002; + m.Bus.Accesses.Clear(); + if (kind is "nested" or "alias") + { + // Source is completed once, then an independent destination fault suspends the same instruction. + m.Core.ExecuteInstruction(); + if (m.Core.State.ProgramCounter != 0x9030 || m.Core.State.LastExceptionVector != 3 || m.Core.State.A[0] != 0x4003) + return "Second operand fault did not preserve completed source state"; + var second = m.Core.State.A[7]; var destination = kind == "alias" ? 0x4003u : 0x4101u; + if (m.PeekPhysical(second + 16, 2) != 0x8001 || m.PeekPhysical(second + 32, 2) != 1) + return "Destination continuation lost the supplied source value or write phase"; + if (m.Bus.Accesses.Count(a => a.Address == sourceRerun) != (software ? 0 : 1)) return "Source cycle count differs before second fault"; + var writeRerun = software ? destination : destination + 1; + if (software) + { m.InitializePhysical(second + 8, m.PeekPhysical(second + 8, 2) | 0x8000, 2); m.InitializePhysical(destination, 0x8001, 2); } + else m.InitializePhysical(second + 10, writeRerun, 4); + e = ArchitecturalExpectation.Capture(m); + savedSr = (ushort)m.PeekPhysical(second, 2); + e.A[7] = second + 58; SyntheticSystemTests.ApplyStatus(m, e, savedSr); + e.A[kind == "alias" ? 0 : 1] += 2; + e.Pc = 0x1002; e.Write(writeRerun, 0x8001, 2, m.Model); + m.Bus.Accesses.Clear(); m.Core.ExecuteInstruction(); + if (m.Bus.Accesses.Any(a => a.Address == sourceRerun)) return "Source was retried after second fault"; + if (m.Bus.Accesses.Count(a => a.Write && a.Address == writeRerun) != (software ? 0 : 1)) return "Second fault write cycle count differs"; + } + else + { + e.D[2] = (e.D[2] & 0xffff0000) | 0x8001; + if (kind == "trace") SyntheticExecution.ExpectException(m, e, 9, 0x1002); + m.Core.ExecuteInstruction(); + if (m.Bus.Accesses.Count(a => a.Address == sourceRerun) != (software ? 0 : 1)) return "Source cycle repeated on continuation"; + } + var mismatch = e.Verify(m); if (mismatch != null) return mismatch; + if (kind != "trace") + { + m.Core.ExecuteInstruction(); e.Pc += 2; e.D[6] = 123; e.Sr = (ushort)(e.Sr & 0xfff0); + mismatch = e.Verify(m); + } + return mismatch; + }); + } + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void UserA7StridesAndMoveaStackSelection() + { + var report = new CoverageBatch("68010", "system-move-word-restart-a7"); + foreach (var kind in new[] { "post-read", "post-write", "movea" }) + foreach (var software in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + var id = $"68010/MOVE.W/restart/A7/{kind}/RR={(software ? 1 : 0)}/ccr={ccr:X2}"; + Record(report, id, () => + { + var opcode = kind == "post-write" ? (ushort)0x3ec0 : kind == "movea" ? (ushort)0x3e5f : (ushort)0x301f; + var m = NewMachine(opcode); + m.Core.State.StatusRegister = (ushort)(0x0700 | ccr); m.Core.State.SetActiveStackPointer(0x7801); + m.Core.State.D[0] = 0xa55a8001; + m.Start(); m.Core.ExecuteInstruction(); + var fp = m.Core.State.A[7]; var savedSr = (ushort)m.PeekPhysical(fp, 2); + var rerun = software ? 0x7801u : 0x7802u; + if (software) m.InitializePhysical(fp + 8, m.PeekPhysical(fp + 8, 2) | 0x8000, 2); + else m.InitializePhysical(fp + 10, rerun, 4); + if (kind == "post-write") { if (software) m.InitializePhysical(rerun, 0x8001, 2); } + else { m.InitializePhysical(fp + 20, 0x8001, 2); if (!software) m.InitializePhysical(rerun, 0x8001, 2); } + var e = ArchitecturalExpectation.Capture(m); + e.A[7] = fp + 58; SyntheticSystemTests.ApplyStatus(m, e, savedSr); + e.A[7] = kind == "movea" ? 0xffff8001u : 0x7803u; + if (kind != "movea") e.Sr = SyntheticExecution.MoveFlags(e.Sr, 0x8001, 2); + e.Pc = 0x1002; + if (kind == "post-write") e.Write(rerun, 0x8001, 2, m.Model); + else if (kind == "post-read") e.D[0] = (e.D[0] & 0xffff0000) | 0x8001; + m.Bus.Accesses.Clear(); m.Core.ExecuteInstruction(); + var mismatch = e.Verify(m); if (mismatch != null) return mismatch; + if (m.Bus.Accesses.Count(a => a.Address == rerun && a.Write == (kind == "post-write")) != (software ? 0 : 1)) return "A7 cycle repeated or software buffer ignored"; + return null; + }); + } + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void InvalidPrivateContinuationPreservesFrameBeforeFormatError() + { + var report = new CoverageBatch("68010", "system-move-word-restart-invalid"); + foreach (var corruption in new[] { "version", "opcode", "phase", "pc", "queue-address", "queue-count", "reserved" }) + for (var ccr = 0; ccr < 32; ccr++) + { + var id = $"68010/MOVE.W/restart/invalid/{corruption}/ccr={ccr:X2}"; + Record(report, id, () => + { + var m = NewMachine(0x3010); m.Core.State.StatusRegister = (ushort)(0x2700 | ccr); m.Core.State.A[0] = 0x4001; + m.Start(); m.Core.ExecuteInstruction(); var fp = m.Core.State.A[7]; + var (offset, value) = corruption switch + { + "version" => (26u, 0x0400u), "opcode" => (30u, 0x4e71u), "phase" => (32u, 2u), + "pc" => (36u, 0x1003u), "queue-address" => (40u, 0x1003u), "queue-count" => (46u, 3u), _ => (48u, 1u) + }; + m.InitializePhysical(fp + offset, value, 2); + var e = ArchitecturalExpectation.Capture(m); SyntheticExecution.ExpectException(m, e, 14, 0x9030); + m.Bus.Accesses.Clear(); m.Core.ExecuteInstruction(); + if (m.Bus.Accesses.Any(a => a.Address is 0x4000 or 0x4001)) return "Malformed continuation executed an operand cycle"; + return e.Verify(m); + }); + } + report.Complete(output); + } + + private static void Record(CoverageBatch report, string id, Func run) + { + try { var mismatch = run(); report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException) + { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); } + } + + private static SyntheticMachine NewMachine(ushort opcode) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68010")); + m.Reset(31); + m.InitializePhysical(0x1000, opcode, 2); + m.InitializePhysical(0x1002, 0x747b, 2); + m.InitializePhysical(0x1004, 0x4e71, 2); + m.InitializePhysical(0x9030, 0x4e73, 2); + m.InitializePhysical(0x9032, 0x4e71, 2); + return m; + } +} diff --git a/Copper68k/M68010Interpreter.cs b/Copper68k/M68010Interpreter.cs index dccfd6ea..b886d828 100644 --- a/Copper68k/M68010Interpreter.cs +++ b/Copper68k/M68010Interpreter.cs @@ -214,20 +214,52 @@ protected override uint GetInterruptVectorAddress(uint vectorAddress) protected override bool IsSupportedRteFrameFormat(ushort format) => (format & 0xF000) is 0x0000 or 0x8000; - // MC68000UM 6.4: validate the version before changing SP, then probe - // the final word before loading the remaining information words. - // Version zero is this emulator's private convention. Opaque restart - // state and suspended-instruction continuation are not implemented. + private ushort[]? _rteInternalWords; + private ushort _rteSpecialStatus, _rteDataOutput, _rteDataInput, _rteInstructionInput; + private uint _rteFaultAddress; + + // MC68000UM 6.4: validate before changing SP, then probe the final + // word before loading the rest. Unmarked images retain structural-only + // compatibility; marked images carry the private word-MOVE continuation. protected override bool ValidateRteFrame(ushort format, uint framePointer) { + _rteInternalWords = null; if ((format & 0xF000) != 0x8000) return true; - if ((ReadWord(framePointer + 26) & 0x3C00) != 0) return false; - _ = ReadWord(framePointer + 56); + var version = ReadWord(framePointer + 26); + if ((version & 0x3C00) != 0) return false; + var words = new ushort[16]; words[0] = version; + words[15] = ReadWord(framePointer + 56); + ushort addressHigh = 0; for (uint offset = 8; offset < 56; offset += 2) { if (offset is 14 or 18 or 22 or 26) continue; - _ = ReadWord(framePointer + offset); + var value = ReadWord(framePointer + offset); + if (offset >= 28) words[(offset - 26) / 2] = value; + else switch (offset) + { + case 8: _rteSpecialStatus = value; break; + case 10: addressHigh = value; break; + case 12: _rteFaultAddress = ((uint)addressHigh << 16) | value; break; + case 16: _rteDataOutput = value; break; + case 20: _rteDataInput = value; break; + case 24: _rteInstructionInput = value; break; + } } + if (words[1] == M68010WordMoveResumeFrame.Marker && !M68010WordMoveResumeFrame.IsValid(words)) return false; + _rteInternalWords = words; + return true; + } + + protected override bool TryResumeRteFrame(ushort format, uint framePointer, ushort statusRegister, uint programCounter) + { + if ((format & 0xF000) != 0x8000 || _rteInternalWords is not { } words || words[1] != M68010WordMoveResumeFrame.Marker) return false; + var specialStatus = _rteSpecialStatus; var faultAddress = _rteFaultAddress; + var output = _rteDataOutput; var input = _rteDataInput; var instructionInput = _rteInstructionInput; + _rteInternalWords = null; + State.SetActiveStackPointer(framePointer + 58); + State.StatusRegister = statusRegister; + AddInstructionCycles(20); // Retained RTE policy; physical restart timing is unqualified. + ResumeM68010WordMove(words, programCounter, faultAddress, specialStatus, output, input, instructionInput); return true; } @@ -249,15 +281,15 @@ protected override bool TryHandleModelSpecificAddressError( bool useDataAccessStackedProgramCounter, ushort dataOutput) { - _ = useDataAccessStackedProgramCounter; + var internalWords = CaptureM68010WordMoveResumeFrame(isWrite, accessKind, useDataAccessStackedProgramCounter); var stackedProgramCounter = State.LastInstructionProgramCounter; var savedStatusRegister = State.StatusRegister; State.RecordException(3, stackedProgramCounter, savedStatusRegister); State.StatusRegister = (ushort)((savedStatusRegister | M68kCpuState.Supervisor) & ~M68kCpuState.Trace); // Figure 6-8: 58 bytes, 26 information words, three unwritten - // reserved words. Zeroed internal/input buffers are placeholders; - // this structural frame does not encode resumable internal state. - for (var i = 0; i < 16; i++) PushWord(0); + // reserved words. Only marked word-MOVE images encode continuation; + // other internal/input state remains structurally unqualified. + for (var i = 15; i >= 0; i--) PushWord(internalWords?[i] ?? 0); PushWord(0); // instruction input buffer (unqualified) State.SetActiveStackPointer(State.A[7] - 2); // reserved PushWord(0); // data input buffer (unqualified) diff --git a/Copper68k/M68010WordMoveResumeFrame.cs b/Copper68k/M68010WordMoveResumeFrame.cs new file mode 100644 index 00000000..aa5294e1 --- /dev/null +++ b/Copper68k/M68010WordMoveResumeFrame.cs @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2026 Ilkka Lehtoranta + * SPDX-License-Identifier: MIT + */ + +namespace Copper68k; + +// Private format-8 internal image. This is not a Motorola silicon encoding. +// All continuation state is in the frame; copied/nested frames need no side table. +internal static class M68010WordMoveResumeFrame +{ + internal const ushort Marker = 0xc010; + internal static bool IsOpcodeSupported(ushort opcode, bool write) + { + if ((opcode & 0xf000) != 0x3000) return false; + var sourceMode = (opcode >> 3) & 7; var sourceReg = opcode & 7; + var destMode = (opcode >> 6) & 7; var destReg = (opcode >> 9) & 7; + var legalSource = sourceMode < 7 || sourceReg <= 4; + var legalDestination = destMode < 7 || destReg <= 1; + return legalSource && legalDestination && (write ? destMode >= 2 : sourceMode >= 2 && !(sourceMode == 7 && sourceReg == 4)); + } + + internal static bool IsValid(ushort[] words) + { + if (words.Length != 16 || words[0] != 0 || words[1] != Marker || words[3] > 1 || + !IsOpcodeSupported(words[2], words[3] == 1) || words[10] > 2 || + (Long(words, 4) & 1) != 0 || (Long(words, 6) & 1) != 0 || + (words[10] != 0 && Long(words, 6) != Long(words, 4))) return false; + for (var i = 11; i < 16; i++) if (words[i] != 0) return false; + return true; + } + + internal static uint Long(ushort[] words, int index) => ((uint)words[index] << 16) | words[index + 1]; + internal static void Long(ushort[] words, int index, uint value) + { words[index] = (ushort)(value >> 16); words[index + 1] = (ushort)value; } +} diff --git a/Copper68k/M68kCore.cs b/Copper68k/M68kCore.cs index 90739dd8..5ec8a58e 100644 --- a/Copper68k/M68kCore.cs +++ b/Copper68k/M68kCore.cs @@ -9172,6 +9172,57 @@ protected virtual bool IsSupportedRteFrameFormat(ushort format) return true; } + protected virtual bool TryResumeRteFrame(ushort format, uint framePointer, ushort statusRegister, uint programCounter) => false; + + protected ushort[]? CaptureM68010WordMoveResumeFrame(bool isWrite, M68kBusAccessKind accessKind, bool dataOperand) + { + if (!M68010WordMoveResumeFrame.IsOpcodeSupported(State.LastOpcode, isWrite) || + (accessKind == M68kBusAccessKind.CpuInstructionFetch && !dataOperand)) return null; + var words = new ushort[16]; + words[1] = M68010WordMoveResumeFrame.Marker; + words[2] = State.LastOpcode; words[3] = (ushort)(isWrite ? 1 : 0); + M68010WordMoveResumeFrame.Long(words, 4, State.ProgramCounter); + M68010WordMoveResumeFrame.Long(words, 6, _prefetchAddress); + words[8] = _prefetchWord0; words[9] = _prefetchWord1; words[10] = (ushort)_prefetchCount; + return words; + } + + // Continue at the suspended word bus transfer, never at the instruction + // decoder. Source EA side effects and completed source reads stay committed. + protected void ResumeM68010WordMove(ushort[] words, uint instructionPc, uint faultAddress, + ushort specialStatus, ushort dataOutput, ushort dataInput, ushort instructionInput) + { + var opcode = words[2]; var writing = words[3] != 0; + State.LastOpcode = opcode; State.LastInstructionProgramCounter = instructionPc; + _activeInstructionProgramCounter = instructionPc; + State.ProgramCounter = M68010WordMoveResumeFrame.Long(words, 4); + FlushPrefetch(); + _prefetchAddress = M68010WordMoveResumeFrame.Long(words, 6); + _prefetchWord0 = words[8]; _prefetchWord1 = words[9]; _prefetchCount = words[10]; + _prefetchCompletedCycle0 = _prefetchCompletedCycle1 = State.Cycles; + _instructionTracePending = State.GetFlag(M68kCpuState.Trace); + var softwareCompleted = (specialStatus & 0x8000) != 0; + var destinationMode = (opcode >> 6) & 7; var destinationRegister = (opcode >> 9) & 7; + _dataAccessStackedProgramCounter = instructionPc; + if (writing) + { + if (!softwareCompleted) WriteWord(faultAddress, dataOutput); + if (destinationMode == 3) SetAddressRegister(destinationRegister, State.A[destinationRegister] + 2); + PrefetchFallthroughAfterMemoryWriteback(); + return; + } + + _dataReadFaultAccessKind = (specialStatus & 0x2000) != 0 ? M68kBusAccessKind.CpuInstructionFetch : M68kBusAccessKind.CpuDataRead; + var value = softwareCompleted ? (specialStatus & 0x1000) != 0 ? dataInput : instructionInput : ReadWord(faultAddress); + var memoryDestination = destinationMode >= 2; + if (!memoryDestination || destinationMode == 4) PrefetchFallthroughAfterMoveSourceRead(); + var destination = ResolvePlannedEaWithoutPrefetchTopUp(destinationMode, destinationRegister, M68kOperandSize.Word, write: true); + if (destinationMode != 1) SetLogicFlags(value, M68kOperandSize.Word); + if (memoryDestination && MoveDestinationHasExtensionWord(destinationMode, destinationRegister)) PrefetchNextOpcodeBeforeMoveMemoryWriteback(); + WritePlannedEaValue(in destination, value); + if (memoryDestination && destinationMode != 4) PrefetchFallthroughAfterMemoryWriteback(); + } + protected virtual bool ValidateRteFrame(ushort format, uint framePointer) => true; protected virtual bool TryHandleModelSpecificAddressError( @@ -10352,6 +10403,7 @@ private bool DecodeLine4(ushort opcode, uint instructionPc) var format = ReadWord(framePointer + 6); if (!IsSupportedRteFrameFormat(format) || !ValidateRteFrame(format, framePointer)) { RaiseException(14, instructionPc, 34); return true; } + if (TryResumeRteFrame(format, framePointer, statusRegister, programCounter)) return true; frameSize = (format & 0xF000) == 0x8000 ? 58u : (format & 0xF000) == 0x2000 ? 12u : 8u; } diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 2da52963..bc993fb6 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -205,8 +205,9 @@ bus ordering, detailed fault sequencing, JIT and native ROM/media tests remain. - Existing SingleStepTests and WinUAE adapters remain available; no new external corpus execution is claimed for them in this slice. WinUAE integer integration currently selects 68000; multi-model native bridge/fixtures still need qualification. -- RTE internal restart remains untested: 010 format 8, 020/030 formats 9/A/B, - and 040 format 7. The current advanced decoder does not implement all these +- Generated 010 word-MOVE/MOVEA format-8 images now have the scoped continuation + gate below. Long transfers, other instruction families, foreign silicon images, + external bus faults, 020/030 formats 9/A/B and 040 format 7 remain unqualified. The current advanced decoder does not implement all these legal restoration protocols; this is an implementation gap, not invalid encoding. - Nested 000 address errors during exception stacking previously recursed on an odd SSP. This is corrected by the address-error double-fault slice above. External @@ -412,3 +413,111 @@ Evidence: CPU `artifacts/format8-before/`, `artifacts/format8-focused/`, `artifacts/format8-validation/`, `artifacts/format8-diagnostic-tests/` and `artifacts/format8-production.binlog`. Full 010 suspended-instruction restart, external BERR and physical timing qualification remain open. + +## 010 word-MOVE/MOVEA continuation + +[MC68000UM sections 6.3.9.2, 6.3.10 and 6.4](https://www.nxp.com/docs/en/reference-manual/MC68000UM.pdf) +describe resuming the suspended bus cycle/instruction after RTE. With RR clear, +the stacked fault address is used; correcting only the address register therefore +still causes an address error. With RR set, software has supplied the read buffer +image or completed the write. Re-decoding the instruction would incorrectly repeat +completed source reads, increments and other effects. Three bounded tests exposed +this distinction before the correction: both software-completed MOVE paths returned +to the original opcode, and register-only correction did not refault during RTE. + +Generated word-MOVE/MOVEA address-error frames now serialize a private continuation +in the internal information words. RTE loads/validates it while preserving the +previous version/probe read order. It pops the complete frame, selects the restored +stack, restores completed prefetch words, then continues at the pending word access. +There is no instruction replay, transaction rollback or per-frame side table. +A source fault continues with the supplied/rerun value and resolves only the still +unresolved destination. A destination fault uses the saved output word and completes +only the pending access/increment. A subsequent destination fault produces a fresh +continuation without repeating the completed source. Restored T1 takes its trace +exception after the suspended instruction completes. + +The following private image is **an emulator convention, not a silicon encoding**. +Its version word is zero and marker is `$C010`; other generated families keep their +previous placeholder internal words and remain unqualified for continuation. + +| Frame byte offset | Private word-MOVE contents | +| --- | --- | +| 26 | Version word, zero | +| 28 | Marker `$C010` | +| 30 | Suspended opcode | +| 32 | Pending source read (0) or destination write (1) | +| 34–37 | Next execution/extension PC at suspension | +| 38–41 | Completed prefetch queue address | +| 42, 44 | Queue words 0 and 1 | +| 46 | Queue count (0–2) | +| 48–57 | Unused private words, zero | + +Malformed marked images raise vector 14 below the intact old frame before popping. +This additional validation is a private-image convention. Unmarked accepted-version +images retain structural-only compatibility; they do not gain guessed silicon +restart semantics. The existing saved-PC convention and successful nonfaulting +instruction paths remain unchanged. RTE retains its existing 20-cycle policy; +physical resumed-cycle/pipeline timing, asynchronous prefetch and interrupt-entry +qualification are not established by this semantic gate. No public API changes. + +The new deterministic groups execute **127,776 logical cases in five batches**: +source 62,208; destination 64,512; copied/nested/alias/prefetch/trace 640; user A7 +192; invalid private images 224. Addressing fixtures and mathematical register/ +flag expectations are independent of production helpers. Canonical selectors +cover every word operand form with boundaries and all CCRs, both privilege modes +and RR settings, source/destination extension ordering and 24-bit physical wrapping +of logical high addresses. Additional cases cover MOVEA sign extension/flags, +partial D-register preservation, A7 bank selection, aliased postincrements, +serialized frame copies, multiple operand faults, buffered following code, trace, +and rejection of malformed opcode/phase/PC/queue/version/private words. + +Common verification checks registers, PC/SR, memory/surroundings and stack banks. +The recording bus checks that only the pending cycle reruns, that completed +source accesses never repeat, and that software-completed cycles stay off the bus. +Following MOVEQ sentinels expose extension/prefetch consumption errors. The older +structural group masks the now-private continuation words; their contents and +behavior have this dedicated gate. Disabling continuation reproduces all three +bounded failures again, with sources restored and rebuilt afterward. No existing +regression is retired. + +This is a bounded word-MOVE/MOVEA continuation implementation for emulator-generated +images, not a full 010 restart milestone. Long transfers, non-MOVE families, real +hardware internal-state encodings, RMW, external BERR, physical function-code +spaces, MMU/FPU and physical timing remain outside this gate. Milestone 6 stays open. + +### Validation checkpoint: word-MOVE continuation + +The ordinary full Release CPU suite passes **4,632 tests**, with eight optional/ +opt-in skips and zero failures. All 127,776 new continuation cases pass in the +full run. The three bounded regressions fail before correction and also fail +when the continuation hook is disabled. That mutation is restored/rebuilt before +final validation. Previous reports lacking the new groups are rejected. AHX +passes 18 tests. No new seeded audit or regression retirement is claimed. + +CPU evidence is under `artifacts/move-restart-before/`, +`artifacts/move-restart-focused/`, `artifacts/move-restart-mutation/`, +`artifacts/move-restart-final/` and `artifacts/move-restart-ahx/`. + +Report validation passes **8,501,832 deterministic cases in 348 batches**. The +requested pinned Musashi audit executes and passes 538 program/profile combinations +with 86 explicit exclusions and zero mismatches, from revision +`72c1d74800f3087b45a0c1a7342601bbed898881`. This software audit does not qualify +010 restart against hardware; the new continuation cases use the manual and +independent synthetic fixtures. + +Private unpublished package `1.5.2-synthetic-dev.38` has SHA-256 +`9b67e027a617dd66f2ba43bfcba959f3a5b09f7f6b430ad9af8efbc55cf3195c`. +The isolated CopperScreen baseline `d9beae8` resolves exactly that NuGet version +in production and separate diagnostic outputs. Release builds with zero warnings/ +errors; host 149, disk 74 and engine diagnostics 1,080 pass. Six ordinary host/ +media skips remain unavailable coverage. The separate native Workbench 3.1 at +0/2 MiB Fast RAM and A1200 eight-plane hard-disk boot/reopen persistence execute +all three cases without skips. These are correctness replays, not throughput +or physical timing qualification. Root working changes and dependency boundaries +are preserved; no package is published. + +Consumer evidence: `artifacts/move-restart-validation/`, +`artifacts/move-restart-diagnostic-tests/` and +`artifacts/move-restart-production.binlog`; CPU package evidence: +`artifacts/synthetic-private-feed-38/`. Long/non-MOVE/foreign frame restart and +the other remaining qualification gaps above stay open. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 634bd565..dc48d72c 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -571,3 +571,27 @@ combinations (86 explicit exclusions) and 18 AHX tests. Unpublished private pack `1.5.2-synthetic-dev.37` passes isolated CopperScreen Release, host 149, disk 74 and separate diagnostics 1,080; all three native boot/persistence replays execute without skips. The reference document records its hash and evidence. + +The subsequent 010 word-MOVE/MOVEA continuation slice adds 127,776 cases in five +batches, for a deterministic gate of 8,501,832 cases in 348 batches. Generated +private format-8 images now retain the pending source/write phase, next PC and +completed prefetch words. RTE resumes the stacked word cycle for RR=0, or uses +software-supplied buffers/completed writes for RR=1. It never re-decodes the +instruction or repeats earlier operand effects. Source/destination faults, copied +and nested frames, aliases, A7, trace and malformed private images are checked. +Disabling the continuation reproduces the three original bounded failures. + +The image is a private emulator encoding, not a hardware-internal layout. +Long/non-MOVE transfers, foreign silicon frames, RMW, external BERR and physical +restart timing remain unqualified. All normal successful paths retain their +existing ordering/timing policy; no public API changes or test retirement. +Milestone 6 remains in progress. The reference document records the private +layout, exact coverage, architectural source and qualification boundaries. + +Validation passes 4,632 ordinary CPU tests (eight optional/opt-in skips), all +8,501,832 deterministic cases, the requested 538 pinned program/profile audits +(86 explicit exclusions) and 18 AHX tests. Private unpublished +`1.5.2-synthetic-dev.38` passes isolated CopperScreen Release, host 149, disk 74, +separate diagnostics 1,080 and all three native boot/persistence cases without +native skips. The reference document records package identity, mutations, exact +scope and remaining gaps; milestone 6 is not marked complete. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index c3894530..900d7bae 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -98,6 +98,11 @@ try { $expected['system-format8-entry'] = 1024 $expected['system-format8-rte'] = 4096 $expected['system-format8-double-fault'] = 256 + $expected['system-move-word-restart-source'] = 62208 + $expected['system-move-word-restart-destination'] = 64512 + $expected['system-move-word-restart-edges'] = 640 + $expected['system-move-word-restart-a7'] = 192 + $expected['system-move-word-restart-invalid'] = 224 } if ($model -eq '68040') { $expected['system-mmu-disabled'] = 34850 } foreach ($group in $expected.Keys) { @@ -164,15 +169,15 @@ try { foreach ($row in $inventory.combinations) { if ($row.Milestone -le 5) { $row.status = 'passing'; $row.note = 'Passing named semantic scenario batches; consult qualification boundaries for untested physical/internal protocols.' } } - $inventory | Add-Member -NotePropertyName qualificationBoundaries -NotePropertyValue @('RTE fault restart/internal formats remain untested: 010 frame8 restart, 020/030 frames9/A/B, 040 frame7', 'BKPT external replacement responder is unavailable', 'MOVES physical function-code spaces and LPSTOP CPU-space broadcast are unqualified', 'CALLM/RTM type1 protocol uses an internal synthetic responder; normal public buses have none') + $inventory | Add-Member -NotePropertyName qualificationBoundaries -NotePropertyValue @('RTE fault restart/internal formats remain untested: 010 long/non-MOVE/foreign frame8 restart, 020/030 frames9/A/B, 040 frame7', 'BKPT external replacement responder is unavailable', 'MOVES physical function-code spaces and LPSTOP CPU-space broadcast are unqualified', 'CALLM/RTM type1 protocol uses an internal synthetic responder; normal public buses have none') $inventory | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'qualified-inventory.json') - @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=343; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; m68040DisabledMmuInstructionGate='passing'; m68000AddressErrorDoubleFaultGate='passing'; m68010Format8StructureGate='passing'; roadmapComplete=$false; + @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=348; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; m68040DisabledMmuInstructionGate='passing'; m68000AddressErrorDoubleFaultGate='passing'; m68010Format8StructureGate='passing'; m68010WordMoveRestartGate='passing'; roadmapComplete=$false; seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; - m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; opaque restart/input state, RMW and physical timing remain unqualified'; + m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; marked private word-MOVE/MOVEA images resume their faulted cycle; other opaque restart/input state, long transfers, RMW and physical timing remain unqualified'; m68000DoubleFaultScope='Address-error entry/handler faults; reset-only recovery; external BERR/reset-vector faults unavailable through the current public bus API'; - unavailableSystemCoverage=@('RTE bus-fault internal restart on 010/020/030', '020/030 coprocessor midinstruction restoration', '040 access-fault pending trace/writeback restoration', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast'); + unavailableSystemCoverage=@('010 long/non-MOVE/external bus-fault restart and 020/030 internal restart', '020/030 coprocessor midinstruction restoration', '040 access-fault pending trace/writeback restoration', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast'); timing='Semantic gate; timing policy and physical qualification remain separate'} | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'summary.json') Write-Host "Synthetic milestones 1-5 semantic gates: $logicalCases logical cases; reports at $output" From 2d30bd39e0a37c840e3a9647a5ceaea51b23a064 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Sun, 4 Oct 2026 23:03:29 +0300 Subject: [PATCH 009/211] Audit pinned 68000 corpus and correct LINK A7 sampling --- .../M68kSingleStepConformanceTests.cs | 57 +++++++-- .../M68kSingleStepConformanceTests.md | 26 ++++ Copper68k.Tests/Synthetic/README.md | 10 ++ .../Synthetic/SyntheticSystemTests.cs | 6 +- Copper68k/M68kAdvancedTimingInterpreter.cs | 4 +- Copper68k/M68kCore.cs | 3 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 111 +++++++++++++++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 26 ++++ scripts/test-copper68k-synthetic.ps1 | 28 ++++- 9 files changed, 250 insertions(+), 21 deletions(-) diff --git a/Copper68k.Tests/M68kSingleStepConformanceTests.cs b/Copper68k.Tests/M68kSingleStepConformanceTests.cs index f223b8a9..a481e7df 100644 --- a/Copper68k.Tests/M68kSingleStepConformanceTests.cs +++ b/Copper68k.Tests/M68kSingleStepConformanceTests.cs @@ -1,5 +1,7 @@ using System.Buffers.Binary; +using System.Security.Cryptography; using System.Text; +using System.Text.Json; using Copper68k; using Xunit.Abstractions; using Xunit.Sdk; @@ -39,6 +41,8 @@ public void OfficialSingleStepCorpusMatchesInterpreterWhenEnabled() var validateCycles = IsTruthy(Environment.GetEnvironmentVariable(ValidateCyclesVariable)); var limit = ParseLimit(Environment.GetEnvironmentVariable(LimitVariable)) ?? int.MaxValue; var backend = ParseBackend(Environment.GetEnvironmentVariable(BackendVariable)); + var audit = IsTruthy(Environment.GetEnvironmentVariable("COPPER68K_M68000_SINGLESTEP_AUDIT")); + if (audit && limit != int.MaxValue) throw new XunitException("A reference audit cannot use a case limit."); var files = Directory.EnumerateFiles(corpusPath, "*.json.bin", SearchOption.TopDirectoryOnly) .Where(path => includeUnverified || IsVerifiedCorpusFile(path)) .Where(path => string.IsNullOrWhiteSpace(filter) || @@ -52,12 +56,22 @@ public void OfficialSingleStepCorpusMatchesInterpreterWhenEnabled() } var executed = 0; + var rows = new List(); foreach (var file in files) { - var reader = new SingleStepBinaryReader(File.ReadAllBytes(file)); + var bytes = File.ReadAllBytes(file); + var reader = new SingleStepBinaryReader(bytes); + var passed = 0; + var mismatched = 0; + var failures = new List(); foreach (var test in reader.ReadTests()) { - RunCase(file, test, validateCycles, backend); + try { RunCase(file, test, validateCycles, backend); passed++; } + catch (XunitException ex) when (audit) + { + mismatched++; + if (failures.Count < 20) failures.Add(ex.Message); + } executed++; if (executed >= limit) { @@ -65,6 +79,8 @@ public void OfficialSingleStepCorpusMatchesInterpreterWhenEnabled() return; } } + if (passed + mismatched == 0) throw new XunitException($"Empty reference fixture: {file}"); + rows.Add(new(Path.GetFileName(file), Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(), passed, mismatched, failures)); } if (executed == 0) @@ -72,8 +88,28 @@ public void OfficialSingleStepCorpusMatchesInterpreterWhenEnabled() throw new XunitException("Selected SingleStepTests inputs contained no executable cases."); } _output.WriteLine($"Executed {executed} SingleStepTests/m68000 case(s) with {backend} backend."); + if (audit) + { + var output = Environment.GetEnvironmentVariable("COPPER68K_SYNTHETIC_REPORT_DIR"); + if (string.IsNullOrWhiteSpace(output)) throw new XunitException("Reference audit requires a report directory."); + Directory.CreateDirectory(output); + var excluded = Directory.GetFiles(corpusPath, "*.json.bin") + .Where(path => !includeUnverified && !IsVerifiedCorpusFile(path)) + .Select(Path.GetFileName).OrderBy(name => name, StringComparer.Ordinal).ToArray(); + File.WriteAllText(Path.Combine(output, "singlestep-model-audit.json"), JsonSerializer.Serialize(new + { + schema = 1, reference = "SingleStepTests/m68000", model = "68000", backend = backend.ToString(), + sourceRevision = Environment.GetEnvironmentVariable("COPPER68K_M68000_SINGLESTEP_SOURCE_REVISION"), + filter, validateCycles, executed, passing = rows.Sum(r => r.Passing), mismatching = rows.Sum(r => r.Mismatching), + excluded, qualification = "MAME microcoded software reference; architectural state and fixture final RAM, not bus transactions or physical timing", rows + }, new JsonSerializerOptions { WriteIndented = true })); + Assert.True(rows.All(r => r.Mismatching == 0), string.Join(Environment.NewLine, rows.Where(r => r.Mismatching != 0) + .Select(r => $"{r.File}: {r.Mismatching} mismatches; {r.Failures[0]}"))); + } } + private sealed record SingleStepAuditRow(string File, string Sha256, int Passing, int Mismatching, List Failures); + private static bool IsTruthy(string? value) => value is not null && (value.Equals("1", StringComparison.OrdinalIgnoreCase) || @@ -189,12 +225,19 @@ private static void RunCase(string file, SingleStepCase test, bool validateCycle } private static IM68kCore CreateCore(IM68kBus bus, SingleStepBackend backend) - => backend switch + { + var core = backend switch { - SingleStepBackend.Interpreter => new M68kInterpreter(bus), + SingleStepBackend.Interpreter => M68kCoreFactory.Default.Create(M68kCpuModel.M68000, bus), SingleStepBackend.Jit => M68kJitCore.CreateM68000(bus), _ => throw new InvalidOperationException($"Invalid SingleStepTests/m68000 backend: {backend}.") }; + // The corpus ends before pending trace entry (e.g. NOP 007 retains T1, + // the original SSP and sequential PC). Keep SR intact while selecting + // that boundary; synthetic trace tests qualify the full API boundary. + if (core is M68kInterpreter interpreter) interpreter.ArchitecturalTraceEnabled = false; + return core; + } private static void ApplyState(M68kCpuState state, SingleStepState expected) { @@ -291,13 +334,13 @@ private enum SingleStepBackend private sealed class CorpusBus : IM68kBus, IM68kCodeReader { - private readonly byte[] _memory = new byte[0x0100_0000]; + private readonly Dictionary _memory = new(); public byte ReadByte(uint address, ref long cycle, M68kBusAccessKind accessKind) { _ = cycle; _ = accessKind; - return _memory[Offset(address)]; + return _memory.GetValueOrDefault(Offset(address)); } public ushort ReadWord(uint address, ref long cycle, M68kBusAccessKind accessKind) @@ -358,7 +401,7 @@ public ushort ReadHostWord(uint address) public ushort ReadWord(uint address) { - return (ushort)((_memory[Offset(address)] << 8) | _memory[Offset(address + 1)]); + return (ushort)((_memory.GetValueOrDefault(Offset(address)) << 8) | _memory.GetValueOrDefault(Offset(address + 1))); } public void WriteWord(uint address, ushort value) diff --git a/Copper68k.Tests/M68kSingleStepConformanceTests.md b/Copper68k.Tests/M68kSingleStepConformanceTests.md index 0d64420f..3188b743 100644 --- a/Copper68k.Tests/M68kSingleStepConformanceTests.md +++ b/Copper68k.Tests/M68kSingleStepConformanceTests.md @@ -24,3 +24,29 @@ Useful environment variables: - `COPPER68K_M68000_SINGLESTEP_INCLUDE_UNVERIFIED`: include corpus files the upstream README marks as caveated. - `COPPER68K_M68000_SINGLESTEP_VALIDATE_CYCLES`: assert fixture total cycle counts in addition to final CPU state and RAM. - `COPPER68K_M68000_SINGLESTEP_BACKEND`: `interpreter` by default, or `jit` to run the MC68000 JIT backend. + +The interpreter adapter uses the public CPU factory and the existing internal +trace switch to stop before pending trace exception entry, matching the corpus +boundary without changing SR. Synthetic trace tests separately cover the ordinary +`ExecuteInstruction` boundary. JIT trace-boundary alignment is not qualified here. +Sparse fixture memory retains the 68000's intentional 24-bit wrapping. + +For a pinned, recorded audit use: + +```powershell +git -C third_party/SingleStepTests.m68000 checkout 64b253116a3de04aaac4346c43680960dc9b67e5 +./scripts/test-copper68k-synthetic.ps1 -SingleStepPath third_party/SingleStepTests.m68000 +``` + +This executes 312,500 cases across all 125 upstream-verified files by default. +TAS and TRAPV remain explicit upstream exclusions. `-SingleStepFilter MOVE` +selects a recorded subset. All 127 pinned files must be present and unchanged; +each selected file must execute all 2,500 cases. Missing/empty/mismatching audits +fail. `singlestep-model-audit.json` records per-file hashes, counts and bounded +failure diagnostics; `singlestep-inputs.json` identifies all inputs. Direct adapter +collection can be enabled with `COPPER68K_M68000_SINGLESTEP_AUDIT=1` and +`COPPER68K_SYNTHETIC_REPORT_DIR`; a case limit is rejected in that mode. +Only the script enforces the pinned source and full fixture selection. + +These are software-reference architectural-state/fixture-RAM checks. Bus +transactions, prefetch images and physical timing are not qualified by this audit. diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 1f372fc3..e871c30b 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -174,6 +174,16 @@ ASL regression replacement proof. Run the consolidation mutations with Milestone 6 remains in progress; remaining implementation/reference gaps are listed explicitly in that document. +The pinned SingleStepTests command now executes 312,500 68000 instruction-body +cases in 125 files, with TAS/TRAPV explicit upstream exclusions. It records +per-file hashes and case counts and rejects incomplete/mismatching requests: +`./scripts/test-copper68k-synthetic.ps1 -SingleStepPath artifacts/reference-singlestep`. +See the reference document for the pin, trace-boundary adaptation and the LINK A7 +disagreement that corrected both production behavior and a synthetic expectation. +The existing stack gate distinguishes 040 early decrement from the other models; +its logical count is unchanged. No new test retirement or hardware qualification +is claimed. + The 000 compiled-JIT alignment follow-up is retained in `M68000JitDirectZeroWaitTests`: 180 logical warm-cache scenarios in 15 ordinary xUnit batches. Instruction-boundary guards transfer word/long, stack and diff --git a/Copper68k.Tests/Synthetic/SyntheticSystemTests.cs b/Copper68k.Tests/Synthetic/SyntheticSystemTests.cs index f9c7af31..73356fd7 100644 --- a/Copper68k.Tests/Synthetic/SyntheticSystemTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticSystemTests.cs @@ -84,8 +84,10 @@ public void LinkUnlinkAndOrdinaryReturnsWithStackAliases(string modelId) { var saved = expected.A[reg]; expected.A[7] -= 4; - // LINK A7 stores the decremented SP (M68000PM 4-111 operation ordering). - expected.Write(expected.A[7], reg == 7 ? expected.A[7] : saved, 4, machine.Model); + // The prose describes pushing the original An; the shorthand SP/An + // assignments do not specify alias sampling across processors. + // Pinned MAME fixtures and WinUAE distinguish 040's early decrement. + expected.Write(expected.A[7], reg == 7 && modelId == "68040" ? expected.A[7] : saved, 4, machine.Model); expected.A[reg] = expected.A[7]; expected.A[7] = unchecked(expected.A[7] + (uint)displacement); } SyntheticExecution.Run(machine, expected, report, $"{modelId}/LINK/{width}/A{reg}/super={supervisor}/op={opcode:X4}/disp={displacement}/ccr={ccr:X2}"); diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index c4eb1a1a..599cc709 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -8721,7 +8721,7 @@ private void ExecuteLinkLong(ushort opcode) _ = FetchWord(); var register = opcode & 7; var displacement = unchecked((int)FetchLong()); - PushLong(register == 7 ? State.A[7] - 4 : State.A[register]); + PushLong(register == 7 && _profile.Model == M68kAcceleratorModel.M68040 ? State.A[7] - 4 : State.A[register]); State.A[register] = State.A[7]; State.SetActiveStackPointer(State.A[7] + unchecked((uint)displacement)); CompleteTiming(M68kInstructionTimingKey.LinkLong); @@ -8733,7 +8733,7 @@ private void ExecuteLinkWord(ushort opcode) _ = FetchWord(); var register = opcode & 7; var displacement = unchecked((short)FetchWord()); - PushLong(register == 7 ? State.A[7] - 4 : State.A[register]); + PushLong(register == 7 && _profile.Model == M68kAcceleratorModel.M68040 ? State.A[7] - 4 : State.A[register]); State.A[register] = State.A[7]; State.SetActiveStackPointer(State.A[7] + unchecked((uint)displacement)); CompleteTiming(M68kInstructionTimingKey.LinkLong); diff --git a/Copper68k/M68kCore.cs b/Copper68k/M68kCore.cs index 5ec8a58e..c959e275 100644 --- a/Copper68k/M68kCore.cs +++ b/Copper68k/M68kCore.cs @@ -10569,7 +10569,8 @@ private bool DecodeLine4(ushort opcode, uint instructionPc) if ((opcode & 0xFFF8) == 0x4E50) { var reg = opcode & 7; - PushLong(reg == 7 ? State.A[7] - 4 : State.A[reg]); + // 000/010 sample An before the stack decrement, including A7. + PushLong(State.A[reg]); var displacement = unchecked((short)FetchWord()); SetAddressRegister(reg, State.A[7]); State.SetActiveStackPointer((uint)(State.A[7] + displacement)); diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index bc993fb6..c28da014 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -58,6 +58,66 @@ without software handlers, and the interrupt program's required MSP/format-1 pai ## Disagreement found and corrected +### SingleStepTests boundary and LINK A7 + +The existing binary adapter now audits the complete pinned +[`SingleStepTests/m68000`](https://github.com/SingleStepTests/m68000/tree/64b253116a3de04aaac4346c43680960dc9b67e5) +revision `64b253116a3de04aaac4346c43680960dc9b67e5`: 127 files, with TAS and +TRAPV explicitly excluded according to the upstream README. The remaining +125 files each contain 2,500 cases. This is a MAME microcoded software reference, +not hardware captures. All register/stack banks, full architectural SR, converted +PC and fixture final RAM are compared. Transaction/prefetch images and physical +timing are not qualified by this semantic adapter. Fixture memory is sparse, +with intentional 24-bit wrapping for the 68000, and cores use the public factory. + +The first run executed 312,500 cases and reported 120,553 disagreements. Inspection +of NOP case 007 (`4E71`, initial/final SR `$8609`, unchanged SSP `$AE04C0`, only +one ordinary prefetch transaction) establishes that the corpus ends before +pending trace entry. Copper68k's ordinary `ExecuteInstruction` includes trace +entry. The adapter therefore selects the existing internal interpreter trace +switch without clearing/changing SR. This qualifies the instruction-body boundary; +the ordinary synthetic trace group retains full API trace qualification. The +script selects the interpreter explicitly; no JIT boundary audit is claimed. + +After boundary alignment, 312,174 cases passed and **326 LINK A7 cases** disagreed +in the pushed value. The old synthetic expectation inferred alias sampling from +the SP/An assignment shorthand in +[M68000PM 4-111](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf). +That shorthand alone is insufficient to distinguish overlapping registers on +individual models. The prose describes pushing the specified register before +loading the updated stack pointer. Pinned +[WinUAE generator LINK code](https://github.com/tonioni/WinUAE/blob/5d22d33632646efc3f747f03e82d28353e52722e/gencpu.cpp#L7044) +explicitly samples An before stack decrement except on 68040. Its source comment +labels the sequence cycle-exact confirmed; this is corroborating software-source +evidence, not an independently executed hardware test. The Musashi special-case +code instead uses the decremented value on every model, so its self-checking +program audit did not expose this distinction. + +The corrected synthetic stack expectation reproduced **3,840 mismatches** in +seven profile batches; 040 remained passing. 000/010 now push the original An. +Advanced word/long LINK does the same except for the preserved 040 early-decrement +rule. Displacement fetching, writes, register updates and timing keys remain in +their existing order. No instruction retry or public API change is introduced. +All **312,500 external cases** and the **48,640 synthetic stack cases** pass after +correction. No regression is retired by this slice. + +```powershell +git clone https://github.com/SingleStepTests/m68000 artifacts/reference-singlestep +git -C artifacts/reference-singlestep checkout 64b253116a3de04aaac4346c43680960dc9b67e5 +./scripts/test-copper68k-synthetic.ps1 -SingleStepPath artifacts/reference-singlestep +``` + +The command defaults to all upstream-verified files; `-SingleStepFilter MOVE` +requests a recorded subset. It requires the pinned revision and all 127 unchanged +inputs even for a subset, rejects an empty selection and checks 2,500 executed +cases plus matching SHA-256 per selected file. The adapter emits +`singlestep-model-audit.json`, including passing/mismatching case counts and up +to 20 precise diagnostics per file. A mismatch fails after collecting the report; +missing, malformed, empty or limited requested audits fail instead of passing. +The existing generic input manifest supplies hashes for excluded files too. +Other CPU models remain covered by the separate pinned Musashi program audit; +SingleStepTests here supplies only 68000 fixtures. + [MC68040UM section 8.2.6](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) lists NOP, MOVES, CAS/CAS2, MOVE USP, MOVEC and cache/MMU serializers among T0 events. The earlier suite used 020/030 flow rules for 040. New scenarios reproduced @@ -202,8 +262,9 @@ bus ordering, detailed fault sequencing, JIT and native ROM/media tests remain. ## Remaining qualification and implementation gaps -- Existing SingleStepTests and WinUAE adapters remain available; no new external - corpus execution is claimed for them in this slice. WinUAE integer integration +- SingleStepTests now has the pinned 312,500-case 68000 instruction-body audit + described above. WinUAE's executable adapter has no new fixture run in this + slice; the LINK source review is separate evidence. WinUAE integer integration currently selects 68000; multi-model native bridge/fixtures still need qualification. - Generated 010 word-MOVE/MOVEA format-8 images now have the scoped continuation gate below. Long transfers, other instruction families, foreign silicon images, @@ -446,11 +507,11 @@ previous placeholder internal words and remain unqualified for continuation. | 28 | Marker `$C010` | | 30 | Suspended opcode | | 32 | Pending source read (0) or destination write (1) | -| 34–37 | Next execution/extension PC at suspension | -| 38–41 | Completed prefetch queue address | +| 34–37 | Next execution/extension PC at suspension | +| 38–41 | Completed prefetch queue address | | 42, 44 | Queue words 0 and 1 | -| 46 | Queue count (0–2) | -| 48–57 | Unused private words, zero | +| 46 | Queue count (0–2) | +| 48–57 | Unused private words, zero | Malformed marked images raise vector 14 below the intact old frame before popping. This additional validation is a private-image convention. Unmarked accepted-version @@ -521,3 +582,41 @@ Consumer evidence: `artifacts/move-restart-validation/`, `artifacts/move-restart-production.binlog`; CPU package evidence: `artifacts/synthetic-private-feed-38/`. Long/non-MOVE/foreign frame restart and the other remaining qualification gaps above stay open. + +## Validation checkpoint: SingleStepTests and LINK alias sampling + +The full ordinary Release CPU suite passes **4,632 tests**, with eight optional/ +opt-in skips and zero failures. Report validation passes the unchanged +**8,501,832 deterministic cases / 348 batches**. The corrected stack expectation +detects 3,840 old-implementation mismatches across seven profiles; after correction +all 48,640 stack scenarios pass. The pinned SingleStepTests audit executes all +312,500 selected cases without mismatches or skips, with TAS/TRAPV explicit +exclusions. The pinned Musashi audit executes 538 passing program/profile +combinations and 86 explicit exclusions, without mismatches. AHX passes 18 tests. +No new seeded run or regression retirement is claimed. + +Requested missing fixtures, empty binary fixtures, unmatched selections and case +limits all execute a failing audit. Script requests with changed pinned inputs +and unmatched filters are rejected before execution; changed inputs are restored in +`finally`. The final input manifest matches the restored corpus. These negative +checks do not qualify omitted references or physical timing. + +Private unpublished package `1.5.2-synthetic-dev.39` has SHA-256 +`625f4d4f30fc05c78af253f596d5ccf6f82b3674c603db6b10d4860293133e33`. +Package/API validation passes. The isolated CopperScreen baseline `d9beae8` +resolves exactly this version in production and separate diagnostic outputs. +Release builds with zero warnings/errors; host 149, disk 74 and engine diagnostics +1,080 pass. Six ordinary host/media skips remain unavailable coverage. Separate +native Workbench 3.1 at 0/2 MiB Fast RAM and A1200 eight-plane hard-disk boot/reopen +persistence execute all three cases without skips. These are correctness replays, +not throughput or physical timing measurements. No package is published, and +root working changes and the NuGet dependency boundary are preserved. + +CPU evidence: `artifacts/m6-singlestep-discovery/`, +`artifacts/m6-singlestep-boundary/`, `artifacts/m6-link-before/`, +`artifacts/m6-singlestep-fixed/`, `artifacts/m6-singlestep-rejections/`, +`artifacts/m6-singlestep-final/`, `artifacts/m6-singlestep-ahx/` and +`artifacts/synthetic-private-feed-39/`. Isolated consumer evidence: +`artifacts/singlestep-validation/`, `artifacts/singlestep-diagnostic-tests/` and +`artifacts/singlestep-production.binlog`. Milestone 6 remains open for the +executable WinUAE/multi-model reference and restoration-protocol gaps above. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index dc48d72c..74b2f093 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -595,3 +595,29 @@ Validation passes 4,632 ordinary CPU tests (eight optional/opt-in skips), all separate diagnostics 1,080 and all three native boot/persistence cases without native skips. The reference document records package identity, mutations, exact scope and remaining gaps; milestone 6 is not marked complete. + +The subsequent SingleStepTests qualification slice adds a pinned independent +68000 instruction-body audit: 312,500 cases across 125 upstream-verified files, +with TAS/TRAPV explicit exclusions. The adapter preserves SR while selecting the +corpus's pre-trace boundary. Per-file counts/hashes and precise mismatches are +recorded; incomplete, changed, empty or limited requests fail. The script defaults +to all verified files and records any explicitly filtered subset. + +That audit exposed LINK A7 alias sampling, which the prior synthetic expectation +also got wrong. Pinned WinUAE source corroborates sampling the original An on +000/010/020/030/060 and the existing early-decrement behavior on 040. Corrected +independent expectations reproduce 3,840 pre-fix failures in seven profiles; +the corrected implementation passes all 312,500 reference cases and 48,640 stack +scenarios. Existing extension/write/update order and timing keys are preserved. +The deterministic logical count remains 8,501,832 in 348 batches. No regression +is retired; WinUAE executable multi-model qualification and advanced restoration +protocols remain open. Milestone 6 stays in progress; this is scoped software +evidence, not exhaustive external coverage or physical hardware qualification. + +Validation of the LINK/reference slice passes 4,632 ordinary CPU tests (eight +optional/opt-in skips), the unchanged 8,501,832-case gate, 312,500 pinned 68000 +reference cases, 538 pinned program/profile combinations (86 exclusions) and +18 AHX tests. Private unpublished `1.5.2-synthetic-dev.39` passes isolated +CopperScreen Release, host 149, disk 74, diagnostics 1,080 and all three native +boot/persistence cases without native skips. The reference document records +package identity, negative input checks, reproduced failures and scope. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 900d7bae..ebab376b 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -7,6 +7,7 @@ param( [string[]] $Models = @('68000', '68010', '68EC020', '68020', '68030', '68040', '68060', 'A1200'), [string] $OutputDirectory = 'artifacts/synthetic-suite', [string] $SingleStepPath, + [string] $SingleStepFilter = '', [string] $MusashiPath, [string] $WinUaePath, [string] $WinUaeLibrary, @@ -124,15 +125,36 @@ try { $references = @() if ($SingleStepPath) { $resolved = Get-InputIdentity 'singlestep' $SingleStepPath '*.json.bin' + $pin = '64b253116a3de04aaac4346c43680960dc9b67e5' + $revision = (& git -C $resolved rev-parse HEAD).Trim() + if ($revision -ne $pin) { throw 'SingleStepTests source revision does not match adapter pin' } + & git -C $resolved diff --quiet HEAD -- . + if ($LASTEXITCODE -ne 0) { throw 'SingleStepTests inputs differ from the pinned source' } + $fixtures = $(if (Test-Path -LiteralPath (Join-Path $resolved 'v1')) { Join-Path $resolved 'v1' } else { $resolved }) + $allFiles = @(Get-ChildItem -LiteralPath $fixtures -File -Filter '*.json.bin' | Sort-Object Name) + if ($allFiles.Count -ne 127) { throw 'Incomplete pinned SingleStepTests fixtures; expected 127 files' } + $selectedFiles = @($allFiles | Where-Object { $_.Name -notin @('TAS.json.bin', 'TRAPV.json.bin') -and + (!$SingleStepFilter -or $_.Name.Contains($SingleStepFilter, [StringComparison]::OrdinalIgnoreCase)) }) + if ($selectedFiles.Count -eq 0) { throw 'Empty SingleStepTests audit selection' } Set-AuditEnvironment 'COPPER68K_RUN_M68000_SINGLESTEP' '1' Set-AuditEnvironment 'COPPER68K_M68000_SINGLESTEP_PATH' $resolved - Set-AuditEnvironment 'COPPER68K_M68000_SINGLESTEP_FILTER' 'MOVE' + Set-AuditEnvironment 'COPPER68K_M68000_SINGLESTEP_FILTER' $SingleStepFilter Set-AuditEnvironment 'COPPER68K_M68000_SINGLESTEP_LIMIT' '0' Set-AuditEnvironment 'COPPER68K_M68000_SINGLESTEP_BACKEND' 'interpreter' Set-AuditEnvironment 'COPPER68K_M68000_SINGLESTEP_VALIDATE_CYCLES' '0' Set-AuditEnvironment 'COPPER68K_M68000_SINGLESTEP_INCLUDE_UNVERIFIED' '0' - Invoke-Tests 'FullyQualifiedName~OfficialSingleStepCorpusMatchesInterpreterWhenEnabled' 'singlestep-move-68000' - $references += 'SingleStepTests: 68000 MOVE; MAME generated, documented caveats, semantic audit' + Set-AuditEnvironment 'COPPER68K_M68000_SINGLESTEP_AUDIT' '1' + Set-AuditEnvironment 'COPPER68K_M68000_SINGLESTEP_SOURCE_REVISION' $pin + Invoke-Tests 'FullyQualifiedName~OfficialSingleStepCorpusMatchesInterpreterWhenEnabled' 'singlestep-68000' + $audit = Get-Content -LiteralPath (Join-Path $output 'singlestep-model-audit.json') -Raw | ConvertFrom-Json + if ($audit.mismatching -ne 0 -or $audit.passing -ne 2500 * $selectedFiles.Count -or + $audit.rows.Count -ne $selectedFiles.Count -or $audit.sourceRevision -ne $pin) { throw 'Incomplete SingleStepTests audit' } + foreach ($file in $selectedFiles) { + $row = @($audit.rows | Where-Object File -CEQ $file.Name) + if ($row.Count -ne 1 -or $row[0].Passing -ne 2500 -or $row[0].Mismatching -ne 0 -or + $row[0].Sha256 -ne (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash) { throw "Incomplete SingleStepTests fixture: $($file.Name)" } + } + $references += "SingleStepTests: pinned 68000 semantic audit; $($audit.passing) cases in $($audit.rows.Count) files; filter '$SingleStepFilter'; MAME generated, documented boundary/caveats" } if ($MusashiPath) { $resolved = Get-InputIdentity 'musashi' $MusashiPath '*.bin' From 2fe39d8cc9ddc9105748f75ee101e7e3d05a24a5 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Sun, 4 Oct 2026 23:39:08 +0300 Subject: [PATCH 010/211] Add pinned cross-model WinUAE discovery audit --- .../M68kWinUaeCpuTesterConformanceTests.cs | 40 +++++- .../M68kWinUaeCpuTesterConformanceTests.md | 73 +++++++++- .../M68kWinUaeInputValidationTests.cs | 68 +++++++++ Copper68k.Tests/M68kWinUaeModelAuditTests.cs | 136 ++++++++++++++++++ Copper68k.Tests/Synthetic/README.md | 5 + docs/COPPER68K_REFERENCE_QUALIFICATION.md | 39 +++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 20 +++ scripts/prepare-copper68k-winuae.ps1 | 126 ++++++++++++++++ scripts/winuae/cputestgen.ini | 43 ++++++ scripts/winuae/msc_dirent.h | 24 ++++ 10 files changed, 564 insertions(+), 10 deletions(-) create mode 100644 Copper68k.Tests/M68kWinUaeInputValidationTests.cs create mode 100644 Copper68k.Tests/M68kWinUaeModelAuditTests.cs create mode 100644 scripts/prepare-copper68k-winuae.ps1 create mode 100644 scripts/winuae/cputestgen.ini create mode 100644 scripts/winuae/msc_dirent.h diff --git a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs index 175c9584..5f893685 100644 --- a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs +++ b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; using Copper68k; +using Copper68k.Tests.Synthetic; using CopperFloat; using Xunit.Abstractions; using Xunit.Sdk; @@ -11,7 +12,7 @@ namespace Copper68k.Tests; public sealed class WinUaeCpuTesterCollection; [Collection("WinUAE CPU tester")] -public sealed class M68kWinUaeCpuTesterConformanceTests +public sealed partial class M68kWinUaeCpuTesterConformanceTests { private const string RunVariable = "COPPER68K_RUN_WINUAE_CPUTEST_M68000"; private const string RunM68040FpuVariable = "COPPER68K_RUN_WINUAE_CPUTEST_M68040_FPU"; @@ -291,6 +292,7 @@ private sealed class NativeTester : IDisposable private readonly NativeRunTests _runTests; private readonly NativeLastOutput? _lastOutput; private readonly NativeAddressingMask? _addressingMask; + private readonly NativeDestroy? _destroy; private readonly NativeCallback _callback; private Exception? _callbackException; private int _executedCases; @@ -298,6 +300,8 @@ private sealed class NativeTester : IDisposable private int _unmappedWrites; private string _lastCaseSummary = ""; private byte _cpuLevel; + private ModelSpec? _integerProfile; + private bool _corruptResult; private NativeTester(IntPtr library) { @@ -306,6 +310,7 @@ private NativeTester(IntPtr library) _runTests = GetDelegate(library, "M68KTester_run_tests"); _lastOutput = TryGetDelegate(library, "M68KTester_last_output"); _addressingMask = TryGetDelegate(library, "m68k_tester_addressing_mask"); + _destroy = TryGetDelegate(library, "M68KTester_destroy"); _callback = RunCopper68k; } @@ -332,7 +337,9 @@ public RunResult Run( string opcode, byte cpuLevel, bool checkUndefinedSr, - bool continueOnError) + bool continueOnError, + ModelSpec? integerProfile = null, + bool corruptResult = false) { _callbackException = null; _executedCases = 0; @@ -340,9 +347,14 @@ public RunResult Run( _unmappedWrites = 0; _lastCaseSummary = ""; _cpuLevel = cpuLevel; + _integerProfile = integerProfile; + _corruptResult = corruptResult; + if (integerProfile is not null && (_destroy is null || _addressingMask is null || _lastOutput is null)) + throw new XunitException("Multi-model integer audit requires the qualified native bridge exports (destroy, addressing mask and diagnostics)."); var corpusPathPtr = Marshal.StringToHGlobalAnsi(corpusPath); var opcodePtr = Marshal.StringToHGlobalAnsi(opcode); + var nativeContext = IntPtr.Zero; try { var settings = new NativeRunSettings @@ -354,6 +366,7 @@ public RunResult Run( }; var result = _init(corpusPathPtr, ref settings); + nativeContext = result.Context; if (result.Error != IntPtr.Zero) { throw new XunitException(Marshal.PtrToStringAnsi(result.Error) ?? "WinUAE cputest initialization failed."); @@ -390,6 +403,7 @@ public RunResult Run( } finally { + if (nativeContext != IntPtr.Zero) _destroy?.Invoke(nativeContext); Marshal.FreeHGlobal(opcodePtr); Marshal.FreeHGlobal(corpusPathPtr); } @@ -433,7 +447,9 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt var context = Marshal.PtrToStructure(contextPtr); var registers = Marshal.PtrToStructure(registersPtr); var bus = new NativeRangeBus(context, _addressingMask?.Invoke() ?? 0x00FF_FFFFu); - IM68kCore cpu = _cpuLevel == 4 + IM68kCore cpu = _integerProfile is not null + ? (_integerProfile.A1200 ? M68kCoreFactory.Default.CreateA1200Ec020(bus) : M68kCoreFactory.Default.Create(_integerProfile.Model, bus)) + : _cpuLevel == 4 ? new M68040Interpreter(bus, M68020CpuProfile.Ocs68040Accelerator25Mhz) : new M68kInterpreter(bus); _lastCaseSummary = FormatCaseSummary(context, registers, bus); @@ -444,6 +460,7 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt DeferredFpuException? deferredFpuException = null; var deferredFpuExceptionObserved = false; var executionTrace = ""; + var completed = false; for (var step = 0; step < MaxStepsPerCase; step++) { var instructionPc = cpu.State.ProgramCounter; @@ -458,7 +475,7 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt $"next=0x{cpu.State.ProgramCounter:X8},exc={cpu.State.LastExceptionVector},d4=0x{cpu.State.D[4]:X8}]"; if (cpu.State.LastExceptionVector >= 0) { - if (deferredFpuException is null && + if (_integerProfile is null && deferredFpuException is null && IsDeferredM68040FpuArithmeticException(cpu.State, bus, _cpuLevel)) { deferredFpuException = DeferredFpuException.Capture(cpu.State); @@ -473,14 +490,16 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt } deferredFpuException = null; + completed = true; break; } registers.Cycles += (uint)cycles; var traceSetAfterInstruction = (cpu.State.StatusRegister & M68kCpuState.Trace) != 0; - if (tracePending && !cpu.State.Stopped && !cpu.State.Halted) + if (_integerProfile is null && tracePending && !cpu.State.Stopped && !cpu.State.Halted) { RaiseHarnessTraceException(cpu.State, bus); + completed = true; break; } @@ -488,19 +507,23 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt (cpu.State.ProgramCounter == registers.EndPc || (registers.BranchTarget != 0xFFFF_FFFFu && cpu.State.ProgramCounter == registers.BranchTarget))) { + completed = true; break; } } + if (_integerProfile is not null && !completed) + throw new XunitException($"Reference case did not reach its boundary within {MaxStepsPerCase} instructions: {executionTrace}"); deferredFpuException?.Restore(cpu.State); _lastCaseSummary += $", trace={executionTrace}"; - if (_cpuLevel == 4 && bus.UnmappedReads != 0 && !deferredFpuExceptionObserved) + if (_integerProfile is null && _cpuLevel == 4 && bus.UnmappedReads != 0 && !deferredFpuExceptionObserved) { RestoreUnobservableFpuRead(cpu.State, bus, registers); } CopyRegisters(cpu.State, bus, _cpuLevel, ref registers); + if (_corruptResult) registers.Regs[0] ^= 1; Marshal.StructureToPtr(registers, registersPtr, fDeleteOld: false); _unmappedReads += bus.UnmappedReads; _unmappedWrites += bus.UnmappedWrites; @@ -548,7 +571,7 @@ private static void ApplyRegisters(IM68kCore cpu, NativeRegisters registers, byt var state = cpu.State; var supervisorMode = (registers.Sr & M68kCpuState.Supervisor) != 0; var activeStackPointer = supervisorMode - ? registers.Ssp + ? cpuLevel >= 2 && cpuLevel != 5 && (registers.Sr & 0x1000) != 0 ? registers.Msp : registers.Ssp : registers.Regs[15]; cpu.Reset(registers.Pc, registers.Ssp); @@ -781,6 +804,9 @@ private static T GetDelegate(IntPtr library, string exportName) [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate uint NativeAddressingMask(); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void NativeDestroy(IntPtr context); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void NativeCallback(IntPtr userData, IntPtr context, IntPtr registers); } diff --git a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md index e96fee02..a301863d 100644 --- a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md +++ b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md @@ -5,8 +5,9 @@ MC68000 data through the Copper68k interpreter by using the host-side runner shape from Copperline's `crates/cputest-runner`. This runner is disabled by default and is a secondary discovery net. -`SingleStepTests/m68000` remains the authority for Copper68k MC68000 semantics, -especially for undefined CCR/SR bits and other model-specific leftovers. +Processor documentation and verified hardware establish architectural expectations. +SingleStepTests and WinUAE are independent software references; disagreements, +especially involving undefined flags, need investigation before changing the CPU. Fetch Copperline and generate the external data outside tracked source files or at the default local path: @@ -19,7 +20,9 @@ bash third_party/Copperline/crates/cputest-runner/tools/cputest-gen.sh third_par `cputest-gen.sh` pins `emoon/m68k_cpu_tester_api` at the commit Copperline's vendored runner came from, builds the WinUAE gencpu chain with `c++`, writes the generated data under `third_party/winuae-cputest/68000`, and uses -`feature_flags_mode=1` so officially undefined flag bits are not verified. +`feature_flags_mode=1` to reduce CCR input combinations for instructions which +do not consume flags. Undefined output bits are masked separately by the native +runner's `check_undefined_sr` setting and each fixture header. For MC68040 FPU fixtures, use current WinUAE generator sources or apply the upstream FScc effective-address fix before generating the corpus. Older pinned @@ -90,3 +93,67 @@ The managed runner targets the native layout used by Copperline's vendored adds `exc010`, `endpc`, `branchtarget`, `cycles`, and `m68k_tester_addressing_mask()`, all of which are useful for the generated WinUAE sets. + +## Pinned integer audit across CPU models (milestone 6 checkpoint) + +The new opt-in `WinUaeIntegerFixturesAcrossSelectedModelsWhenEnabled` uses the +public CPU factory for all seven models and the A1200 profile. A1200 executes +its own core against the EC020 input set. The native runner is serialized and +releases its per-directory allocations. The managed callback requires an actual +instruction/exception boundary within 64 steps; it does not synthesize trace +exceptions or apply the legacy FPU adapter's result adjustments. + +On Windows, prepare external fixtures with PowerShell 7 and an x64 MSVC installation. The script +requires exact source revisions, rejects tracked source modifications and existing +output directories, records compiler/native/generator identities and each binary +input hash, and restores the compiler environment afterward. Generated files stay +outside tracked source. The pinned generator initializes its xorshift state to 1 +per test set; the preset uses one Basic round and full extension addressing. + +```powershell +git clone https://github.com/emoon/m68k_cpu_tester_api artifacts/reference-winuae-api +git -C artifacts/reference-winuae-api checkout 025b999239800357e95065fe5b9a15ea5b300fa7 +git clone https://github.com/LinuxJedi/Copperline artifacts/reference-copperline +git -C artifacts/reference-copperline checkout 7a83745d6c6159bc74ab0471578ffc8bc244e66e +./scripts/prepare-copper68k-winuae.ps1 ` + -GeneratorSource artifacts/reference-winuae-api ` + -RunnerSource artifacts/reference-copperline ` + -VcVars64 '/VC/Auxiliary/Build/vcvars64.bat' ` + -OutputDirectory artifacts/winuae-model-inputs + +$env:COPPER68K_RUN_WINUAE_MODEL_AUDIT = '1' +$env:COPPER68K_WINUAE_MODEL_PATH = (Resolve-Path artifacts/winuae-model-inputs).Path +$env:COPPER68K_WINUAE_CPUTEST_LIBRARY = (Resolve-Path artifacts/winuae-model-inputs/m68k_cpu_tester.dll).Path +$env:COPPER68K_SYNTHETIC_REPORT_DIR = Join-Path (Get-Location).Path 'artifacts/winuae-model-report' +dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release -p:Platform=AnyCPU ` + --filter FullyQualifiedName~WinUaeIntegerFixturesAcrossSelectedModelsWhenEnabled +# Remove these opt-in variables before running the ordinary suite. +``` + +`COPPER68K_SYNTHETIC_MODELS` optionally selects exact comma-separated profile IDs; +missing, empty, duplicate or unknown selections fail. Preflight requires all +expected opcode directories, memory images, sequential data files, matching model/ +address-width headers and unchanged manifested inputs. Six ordinary structural +regressions cover changed input, missing memory, empty selection, missing sequence, +incorrect address width and empty data. Structural placeholders never reach the +native runner. Every selected profile must also fail a deliberately corrupted NOP +register result before its audit proceeds. + +The bridge initializes the native CCR mask to `0xff` and honors undefined-SR +checking. The upstream wrapper left the mask at zero, silently reducing execution +to CCR-zero inputs and bypassing unchanged-register/SR assertions. The NOP corruption +probe exposed that false-success path. The bridge also closes each opcode header; +without that correction a long audit exhausted Windows stdio handles and aborted. +The wrapper inherits Copperline's aggregate failure return and version-16 format +support; the preparation script verifies unique patch anchors. + +`winuae-model-audit.json` records per-model/opcode directory results, callback +counts, diagnostics, input identities and corruption probes. A mismatch or empty +execution fails the requested audit. This checkpoint is discovery evidence: the +Basic preset omits bus/address faults, trace/M rounds and advanced restart protocols. +The pinned generator disables the high memory range in the 32-bit fixture headers; +this run does not independently qualify high 32-bit operand addresses. Physical +timing, cache, enabled MMU and FPU arithmetic remain outside this integer audit. +Generator candidate counts and executed callback counts are separate quantities. +The audit remains failing until reference/adapter/CPU disagreements are resolved; +its presence does not mark milestone 6 complete. diff --git a/Copper68k.Tests/M68kWinUaeInputValidationTests.cs b/Copper68k.Tests/M68kWinUaeInputValidationTests.cs new file mode 100644 index 00000000..718b9b5c --- /dev/null +++ b/Copper68k.Tests/M68kWinUaeInputValidationTests.cs @@ -0,0 +1,68 @@ +using System.Buffers.Binary; +using System.Security.Cryptography; +using Copper68k.Tests.Synthetic; +using Xunit.Sdk; +using static Copper68k.Tests.M68kWinUaeCpuTesterConformanceTests; + +namespace Copper68k.Tests; + +public sealed class M68kWinUaeInputValidationTests +{ + [Theory] + [InlineData("missing-memory", "memory image")] + [InlineData("empty-selection", "Incomplete or incompatible")] + [InlineData("changed-input", "Changed WinUAE fixture")] + [InlineData("missing-sequence", "header/data sequence")] + [InlineData("wrong-width", "model/address width")] + [InlineData("empty-data", "Incomplete or incompatible WinUAE data")] + public void PreflightRejectsIncompleteOrChangedInputs(string defect, string diagnostic) + { + var root = Path.Combine(Path.GetTempPath(), "copper68k-winuae-" + Guid.NewGuid().ToString("N")); + var model = ModelSpec.All.Single(m => m.Id == "68000"); + var cpuPath = Path.Combine(root, model.Id, model.Id); + Directory.CreateDirectory(cpuPath); + try + { + foreach (var memory in new[] { ("lmem.dat", 0x8000), ("hmem.dat", 0x8000), ("tmem.dat", 0x40000) }) + File.WriteAllBytes(Path.Combine(cpuPath, memory.Item1), new byte[memory.Item2]); + // Structural placeholders only: these files are never passed to native code. + var opcodes = new[] { "NOP" }.Concat(Enumerable.Range(1, 145).Select(n => $"STRUCTURAL-{n:D3}")).ToArray(); + foreach (var opcode in opcodes) + { + var directory = Path.Combine(cpuPath, opcode); + Directory.CreateDirectory(directory); + var header = new byte[98]; + BinaryPrimitives.WriteUInt32BigEndian(header, 16); + File.WriteAllBytes(Path.Combine(directory, "0000.dat"), header); + var data = new byte[18]; + BinaryPrimitives.WriteUInt32BigEndian(data, 16); + File.WriteAllBytes(Path.Combine(directory, "0001.dat"), data); + } + WinUaeManifest Manifest(string[]? selection = null) => new(1, GeneratorPin, RunnerPin, "unused", new[] { + new WinUaeProfile(model.Id, model.Id, 0, 24, selection ?? opcodes, + Directory.GetFiles(cpuPath, "*.dat", SearchOption.AllDirectories).Select(file => + new WinUaeInput(Path.GetRelativePath(Path.Combine(root, model.Id), file).Replace('\\', '/'), + new FileInfo(file).Length, Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(file))).ToLowerInvariant())).ToArray()) + }); + var manifest = Manifest(); + ValidateWinUaeProfile(root, manifest, model); + var nopPath = Path.Combine(cpuPath, "NOP"); + switch (defect) + { + case "missing-memory": File.Delete(Path.Combine(cpuPath, "tmem.dat")); manifest = Manifest(); break; + case "empty-selection": manifest = Manifest(Array.Empty()); break; + case "changed-input": File.AppendAllText(Path.Combine(nopPath, "0001.dat"), "changed"); break; + case "missing-sequence": File.Move(Path.Combine(nopPath, "0001.dat"), Path.Combine(nopPath, "0002.dat")); manifest = Manifest(); break; + case "wrong-width": + var header = File.ReadAllBytes(Path.Combine(nopPath, "0000.dat")); + BinaryPrimitives.WriteUInt32BigEndian(header.AsSpan(24), 0x80000000); + File.WriteAllBytes(Path.Combine(nopPath, "0000.dat"), header); + manifest = Manifest(); break; + case "empty-data": File.WriteAllBytes(Path.Combine(nopPath, "0001.dat"), Array.Empty()); manifest = Manifest(); break; + } + var error = Assert.Throws(() => ValidateWinUaeProfile(root, manifest, model)); + Assert.Contains(diagnostic, error.Message, StringComparison.Ordinal); + } + finally { Directory.Delete(root, recursive: true); } + } +} diff --git a/Copper68k.Tests/M68kWinUaeModelAuditTests.cs b/Copper68k.Tests/M68kWinUaeModelAuditTests.cs new file mode 100644 index 00000000..531b9848 --- /dev/null +++ b/Copper68k.Tests/M68kWinUaeModelAuditTests.cs @@ -0,0 +1,136 @@ +using System.Buffers.Binary; +using System.Security.Cryptography; +using System.Text.Json; +using Copper68k.Tests.Synthetic; +using Xunit.Sdk; + +namespace Copper68k.Tests; + +public sealed partial class M68kWinUaeCpuTesterConformanceTests +{ + private const string ModelAuditRun = "COPPER68K_RUN_WINUAE_MODEL_AUDIT"; + internal const string GeneratorPin = "025b999239800357e95065fe5b9a15ea5b300fa7"; + internal const string RunnerPin = "7a83745d6c6159bc74ab0471578ffc8bc244e66e"; + + [EnvironmentFact(ModelAuditRun, "audit pinned WinUAE integer fixtures across selected CPU profiles")] + public void WinUaeIntegerFixturesAcrossSelectedModelsWhenEnabled() + { + var root = Environment.GetEnvironmentVariable("COPPER68K_WINUAE_MODEL_PATH"); + var library = Environment.GetEnvironmentVariable(LibraryVariable); + var output = Environment.GetEnvironmentVariable("COPPER68K_SYNTHETIC_REPORT_DIR"); + if (string.IsNullOrWhiteSpace(root) || string.IsNullOrWhiteSpace(library) || string.IsNullOrWhiteSpace(output)) + throw new XunitException("Model audit requires fixture root, native library and report directory."); + var selection = Environment.GetEnvironmentVariable("COPPER68K_SYNTHETIC_MODELS") ?? string.Join(',', ModelSpec.All.Select(m => m.Id)); + var ids = selection.Split(',', StringSplitOptions.TrimEntries); + if (ids.Length == 0 || ids.Distinct().Count() != ids.Length || ids.Any(id => !ModelSpec.All.Any(m => m.Id == id))) + throw new XunitException($"Empty, duplicate or unknown model selection: {selection}"); + var manifest = JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(root, "manifest.json"))) + ?? throw new XunitException("Missing WinUAE input manifest."); + if (manifest.Schema != 1 || manifest.GeneratorCommit != GeneratorPin || manifest.RunnerCommit != RunnerPin) + throw new XunitException("WinUAE source identity does not match the qualified bridge pins."); + if (!File.Exists(library) || Hash(library) != manifest.NativeLibrarySha256) + throw new XunitException("WinUAE native library differs from the generated input manifest."); + // Preflight every requested profile before loading native code, which can + // terminate its process for malformed/missing binary fixture records. + foreach (var id in ids) ValidateWinUaeProfile(root, manifest, ModelSpec.All.Single(m => m.Id == id)); + var rows = new List(); + var probes = new List(); + Directory.CreateDirectory(output); + using var tester = NativeTester.Load(library); + foreach (var id in ids) + { + var model = ModelSpec.All.Single(m => m.Id == id); + var fixture = manifest.Profiles.Single(p => p.Id == FixtureId(id)); + var path = Path.Combine(root, fixture.Id); + // A native 'success' contract must detect a deliberately wrong result. + var probe = tester.Run(path, "NOP", fixture.CpuLevel, false, false, model, corruptResult: true); + var detected = !probe.Passed && probe.ExecutedCases > 0 && probe.Detail.Contains("WinUAE cputest reported failure", StringComparison.Ordinal); + probes.Add(new(id, detected, probe.ExecutedCases, probe.Detail)); + if (!detected) throw new XunitException($"Native register-corruption probe was not detected for {id}: {probe.Detail}"); + foreach (var opcode in fixture.Opcodes) + { + try + { + var result = tester.Run(path, opcode, fixture.CpuLevel, false, false, model); + rows.Add(new(id, opcode, result.Passed ? result.ExecutedCases > 0 ? "passing" : "untested" : "mismatching", + result.ExecutedCases, result.UnmappedReads, result.UnmappedWrites, result.Detail)); + } + catch (Exception ex) { rows.Add(new(id, opcode, "mismatching", 0, 0, 0, ex.Message)); } + } + _output.WriteLine($"{id}: {rows.Count(r => r.Model == id && r.Status == "passing")} directories passed; {rows.Where(r => r.Model == id).Sum(r => (long)r.ExecutedCases)} callbacks executed."); + } + File.WriteAllText(Path.Combine(output, "winuae-model-audit.json"), JsonSerializer.Serialize(new + { + schema = 1, reference = "WinUAE generator through Copperline native assertion runner", selectedModels = ids, + manifest.GeneratorCommit, manifest.RunnerCommit, manifest.NativeLibrarySha256, + manifestSha256 = Hash(Path.Combine(root, "manifest.json")), + qualification = "Pinned software assertions; integer state, defined SR and memory/frame effects. Basic preset omits bus/address faults, trace/M rounds, physical timing, cache/MMU/FPU and advanced RTE restart protocols. Callbacks differ from generator candidate counts.", + passing = rows.Count(r => r.Status == "passing"), mismatching = rows.Count(r => r.Status == "mismatching"), + untested = rows.Count(r => r.Status == "untested"), executedCases = rows.Sum(r => (long)r.ExecutedCases), probes, rows + }, new JsonSerializerOptions { WriteIndented = true })); + var failures = rows.Where(r => r.Status != "passing").ToArray(); + Assert.True(failures.Length == 0, string.Join(Environment.NewLine, failures.Select(r => $"{r.Model}/{r.Opcode}: {r.Detail}"))); + } + + private static string FixtureId(string id) => id == "A1200" ? "68EC020" : id; + private static string Hash(string path) => Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))).ToLowerInvariant(); + + internal static void ValidateWinUaeProfile(string root, WinUaeManifest manifest, ModelSpec model) + { + var fixture = manifest.Profiles.SingleOrDefault(p => p.Id == FixtureId(model.Id)) + ?? throw new XunitException($"Missing WinUAE profile: {model.Id}"); + var level = model.Id switch { "68000" => 0, "68010" => 1, "68030" => 3, "68040" => 4, "68060" => 5, _ => 2 }; + var cpu = level == 5 ? "68060" : $"{68000 + level * 10}"; + var requiredCount = model.Id switch { "68000" => 146, "68010" => 153, "68030" => 180, "68040" => 181, "68060" => 184, _ => 179 }; + if (fixture.CpuLevel != level || fixture.CpuDirectory != cpu || fixture.AddressBits != model.AddressBits || + fixture.Opcodes.Length != requiredCount || fixture.Opcodes.Distinct().Count() != requiredCount || !fixture.Opcodes.Contains("NOP")) + throw new XunitException($"Incomplete or incompatible WinUAE profile: {model.Id}"); + var path = Path.Combine(root, fixture.Id, cpu); + foreach (var memory in new[] { ("lmem.dat", 0x8000), ("hmem.dat", 0x8000), ("tmem.dat", 0x40000) }) + { + var file = Path.Combine(path, memory.Item1); + if (!File.Exists(file) || new FileInfo(file).Length != memory.Item2) + throw new XunitException($"Missing or incomplete WinUAE memory image: {model.Id}/{memory.Item1}"); + } + var actual = Directory.GetDirectories(path).Select(Path.GetFileName).OrderBy(n => n, StringComparer.Ordinal).ToArray(); + if (!actual.SequenceEqual(fixture.Opcodes.OrderBy(n => n, StringComparer.Ordinal))) + throw new XunitException($"Changed WinUAE directory selection: {model.Id}"); + if (fixture.Inputs.Length == 0 || fixture.Inputs.Select(i => i.Path).Distinct().Count() != fixture.Inputs.Length) + throw new XunitException($"Empty or duplicate WinUAE input selection: {model.Id}"); + var actualInputs = Directory.GetFiles(path, "*.dat", SearchOption.AllDirectories) + .Select(p => Path.GetRelativePath(Path.Combine(root, fixture.Id), p).Replace('\\', '/')).OrderBy(p => p, StringComparer.Ordinal); + if (!actualInputs.SequenceEqual(fixture.Inputs.Select(i => i.Path).OrderBy(p => p, StringComparer.Ordinal))) + throw new XunitException($"Changed WinUAE input selection: {model.Id}"); + foreach (var input in fixture.Inputs) + { + var file = Path.Combine(root, fixture.Id, input.Path); + if (new FileInfo(file).Length != input.Bytes || Hash(file) != input.Sha256) + throw new XunitException($"Changed WinUAE fixture: {model.Id}/{input.Path}"); + } + foreach (var opcode in fixture.Opcodes) + { + var directory = Path.Combine(path, opcode); + var files = Directory.GetFiles(directory, "*.dat").Select(Path.GetFileName).OrderBy(n => n, StringComparer.Ordinal).ToArray(); + if (files.Length < 2 || !files.SequenceEqual(Enumerable.Range(0, files.Length).Select(n => $"{n:D4}.dat"))) + throw new XunitException($"Missing WinUAE header/data sequence: {model.Id}/{opcode}"); + var header = File.ReadAllBytes(Path.Combine(directory, "0000.dat")); + if (header.Length < 98 || BinaryPrimitives.ReadUInt32BigEndian(header) != 16) + throw new XunitException($"Invalid WinUAE header: {model.Id}/{opcode}"); + var flags = BinaryPrimitives.ReadUInt32BigEndian(header.AsSpan(24)); + if (((flags >> 16) & 15) != level || ((flags & 0x80000000) == 0 ? 24 : 32) != model.AddressBits) + throw new XunitException($"Incompatible WinUAE model/address width: {model.Id}/{opcode}"); + foreach (var dataName in files.Skip(1)) + { + var data = File.ReadAllBytes(Path.Combine(directory, dataName!)); + if (data.Length <= 16 || !data.AsSpan(0, 8).SequenceEqual(header.AsSpan(0, 8))) + throw new XunitException($"Incomplete or incompatible WinUAE data: {model.Id}/{opcode}/{dataName}"); + } + } + } + + internal sealed record WinUaeManifest(int Schema, string GeneratorCommit, string RunnerCommit, string NativeLibrarySha256, WinUaeProfile[] Profiles); + internal sealed record WinUaeProfile(string Id, string CpuDirectory, byte CpuLevel, int AddressBits, string[] Opcodes, WinUaeInput[] Inputs); + internal sealed record WinUaeInput(string Path, long Bytes, string Sha256); + private sealed record WinUaeModelRow(string Model, string Opcode, string Status, int ExecutedCases, int UnmappedReads, int UnmappedWrites, string Detail); + private sealed record WinUaeProbeRow(string Model, bool Detected, int ExecutedCases, string Detail); +} diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index e871c30b..a3e228d3 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -191,3 +191,8 @@ JMP/JSR alignment faults to the accurate interpreter before operand effects. These JIT regressions are reported separately from the synthetic inventory; no cache/prefetch/bus-ordering regression is retired. See the reference qualification document for reproduced failures, guard-removal proofs and scope. + +The additional pinned Windows WinUAE model audit and preparation command are +documented in [WinUAE conformance](../M68kWinUaeCpuTesterConformanceTests.md#pinned-integer-audit-across-cpu-models-milestone-6-checkpoint). +It is an opt-in discovery gate that currently reports unresolved mismatches; +it does not replace the deterministic synthetic gate or complete milestone 6. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index c28da014..daf771fd 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -620,3 +620,42 @@ CPU evidence: `artifacts/m6-singlestep-discovery/`, `artifacts/singlestep-validation/`, `artifacts/singlestep-diagnostic-tests/` and `artifacts/singlestep-production.binlog`. Milestone 6 remains open for the executable WinUAE/multi-model reference and restoration-protocol gaps above. + +## WinUAE multi-model discovery checkpoint + +The opt-in integer audit now executes every selected profile using the public +factory, preflights pinned binary input identities and requires a native NOP +register-corruption probe to fail. The reproducible Windows preparation command +and source revisions are in +[WinUAE conformance](../Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md#pinned-integer-audit-across-cpu-models-milestone-6-checkpoint). +Native bridge fixes enable all requested CCR inputs/unchanged-register assertions, +close leaked header streams and release per-directory allocations. No production +CPU semantics, timing policy, package version or public API changes in this slice. + +The discovery run records 11,890,943 executed callbacks over 1,381 opcode/profile +directories: 1,295 passing, 86 mismatching, zero empty executions. All eight NOP +corruption probes are detected. Callback totals include partial executions in +failing directories; they are not a count of qualified passing architectural +combinations. Mismatches include instruction results, exception/undefined-flag +expectations and callback rejection of encodings, and require independent triage. +They are not yet classified as CPU defects. No mismatch is excluded to turn this +run green. Milestone 6 remains in progress; the ordinary deterministic gate and +previous consumer qualification remain separate evidence. + +Evidence: `artifacts/m6-winuae-models-discovery/` (CCR-zero false-success probe), +`artifacts/m6-winuae-models-full/` (stdio exhaustion), +`artifacts/m6-winuae-models-fixed/` (complete failing discovery), +`artifacts/m6-winuae-prepared/` (tracked preparation script output), +`artifacts/m6-winuae-checkpoint-audit/` and +`artifacts/m6-winuae-checkpoint-tests/`. Inputs and native binaries are local +artifacts, never committed. No release is published or old regression retired. + +The current ordinary Release CPU suite passes **4,638 tests**, with nine optional/ +opt-in skips and zero failures. This includes all six new input-validation +regressions; the newly added external model audit is optional in ordinary CI. +The fresh tracked preparation output reproduces the same 1,295 passing / 86 +mismatching groups and callback counts, with all eight corruption probes detected. +A wrong source revision is rejected before creating output. PowerShell syntax and +Git whitespace checks pass. Previous production/consumer evidence remains the +`1.5.2-synthetic-dev.39` checkpoint; no new consumer replay or package is claimed +for this test-only follow-up. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 74b2f093..b53665ae 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -621,3 +621,23 @@ reference cases, 538 pinned program/profile combinations (86 exclusions) and CopperScreen Release, host 149, disk 74, diagnostics 1,080 and all three native boot/persistence cases without native skips. The reference document records package identity, negative input checks, reproduced failures and scope. + +The subsequent WinUAE discovery checkpoint adds a reproducible pinned Windows +fixture/bridge preparation command and an opt-in audit across all eight profiles. +Preflight rejects missing, changed, empty or incompatible inputs; every model must +fail a deliberate NOP register-corruption probe. The native bridge now enables +CCR/unchanged-register assertions and closes leaked opcode-header streams. + +The complete discovery records 1,295 passing and 86 mismatching opcode/profile +directories, with 11,890,943 callbacks and all eight corruption probes detected. +These are discovery results, not completed qualification. Failing directories +include partial executions; reference/adapter/CPU causes remain to be separated. +The requested audit fails on any mismatch. No production CPU fix, package release +or regression retirement is included. Milestone 6 remains in progress, including +resolution of these disagreements and the previously recorded restoration gaps. + +Validation of this test-tooling checkpoint passes 4,638 ordinary CPU tests, with +nine optional/opt-in skips and zero failures. Fresh fixture generation reproduces +the failing audit counts above and all eight corruption probes. Source revision +rejection, script syntax and whitespace checks pass. Earlier production/consumer +validation remains separate; no new package or consumer replay is claimed. diff --git a/scripts/prepare-copper68k-winuae.ps1 b/scripts/prepare-copper68k-winuae.ps1 new file mode 100644 index 00000000..a4a82f8d --- /dev/null +++ b/scripts/prepare-copper68k-winuae.ps1 @@ -0,0 +1,126 @@ +#requires -Version 7.0 +[CmdletBinding()] +param( + [Parameter(Mandatory)] [string] $GeneratorSource, + [Parameter(Mandatory)] [string] $RunnerSource, + [Parameter(Mandatory)] [string] $VcVars64, + [string] $OutputDirectory = 'artifacts/winuae-model-inputs' +) +$ErrorActionPreference = 'Stop' +$repo = Split-Path -Parent $PSScriptRoot +$generatorPin = '025b999239800357e95065fe5b9a15ea5b300fa7' +$runnerPin = '7a83745d6c6159bc74ab0471578ffc8bc244e66e' +foreach ($source in @(@{path=$GeneratorSource; pin=$generatorPin}, @{path=$RunnerSource; pin=$runnerPin})) { + $revision = & git -C $source.path rev-parse HEAD + if ($LASTEXITCODE -ne 0 -or $revision -ne $source.pin) { throw "Incorrect source revision at $($source.path)" } + if (& git -C $source.path diff --name-only HEAD) { throw "Modified tracked source at $($source.path)" } +} +$generator = Join-Path (Resolve-Path -LiteralPath $GeneratorSource).Path 'gencpu' +$vendor = Join-Path (Resolve-Path -LiteralPath $RunnerSource).Path 'crates/cputest-runner/vendor' +$vcvars = (Resolve-Path -LiteralPath $VcVars64).Path +$output = [IO.Path]::GetFullPath($OutputDirectory, $repo) +if (Test-Path -LiteralPath $output) { throw "Use a new output directory: $output" } +New-Item -ItemType Directory -Path $output | Out-Null +$savedEnvironment = @{} +function Run-Compiler([string[]] $Arguments, [string] $Log) { + & cl @Arguments *> $Log + if ($LASTEXITCODE -ne 0) { throw "Compiler failed: $Log" } +} +function Patch-Once([string] $Text, [string] $Before, [string] $After) { + if (($Text.Split([string[]]@($Before), [StringSplitOptions]::None).Count - 1) -ne 1) { throw "Native patch anchor changed: $Before" } + return $Text.Replace($Before, $After) +} +try { + $settings = & $env:COMSPEC /c "`"$vcvars`" >nul && set" + if ($LASTEXITCODE -ne 0) { throw 'MSVC environment initialization failed' } + foreach ($setting in $settings) { + $index = $setting.IndexOf('=') + if ($index -le 0) { continue } + $name = $setting.Substring(0, $index) + $savedEnvironment[$name] = [Environment]::GetEnvironmentVariable($name) + [Environment]::SetEnvironmentVariable($name, $setting.Substring($index + 1)) + } + $compiler = (& cl 2>&1 | Out-String).Trim() + Push-Location $generator + try { + Run-Compiler @('/nologo','/O2','/EHsc','/w','/I.','/Iinclude','build68k.cpp','/Fe:build68k.exe') (Join-Path $output 'build68k.log') + & ./build68k.exe table68k | Set-Content -Encoding utf8 cpudefs.cpp + if ($LASTEXITCODE -ne 0) { throw 'Opcode table generation failed' } + Run-Compiler @('/nologo','/O2','/EHsc','/w','/I.','/Iinclude','gencpu.cpp','missing.cpp','readcpu.cpp','cpudefs.cpp','/Fe:gencpu_prog.exe') (Join-Path $output 'gencpu-build.log') + & ./gencpu_prog.exe . *> (Join-Path $output 'gencpu-run.log') + if ($LASTEXITCODE -ne 0) { throw 'CPU source generation failed' } + $cpp = @('cpudefs.cpp','cpuemu_90_test.cpp','cpuemu_91_test.cpp','cpuemu_92_test.cpp','cpuemu_93_test.cpp','cpuemu_94_test.cpp','cpuemu_95_test.cpp','cputbl_test.cpp','cputest.cpp','cputest_support.cpp','disasm.cpp','fpp.cpp','fpp_softfloat.cpp','ini.cpp','newcpu_common.cpp','readcpu.cpp','softfloat/softfloat.cpp','softfloat/softfloat_decimal.cpp','softfloat/softfloat_fpsp.cpp') + Run-Compiler (@('/nologo','/O2','/EHsc','/w','/I.','/Iinclude','/Icputest','/I../zlib','/DCPUEMU_90','/DCPUEMU_91','/DCPUEMU_92','/DCPUEMU_93','/DCPUEMU_94','/DCPUEMU_95','/DCPU_TESTER') + $cpp + @('/Fe:cputester.exe')) (Join-Path $output 'cputester-build.log') + } finally { Pop-Location } + + $native = [IO.File]::ReadAllText((Join-Path $vendor 'm68k_cpu_tester.c')) + $native = Patch-Once $native 'cpu_lvl = settings->cpu_level == 6 ? 5 : settings->cpu_level;' 'ccr_mask = 0xff; check_undefined_sr = settings->check_undefined_sr; cpu_lvl = settings->cpu_level == 6 ? 5 : settings->cpu_level;' + $native = Patch-Once $native 'context->opcode = settings->opcode;' 'context->stop_on_error = !settings->continue_on_error; context->opcode = settings->opcode;' + $native = Patch-Once $native 'int lvl2 = cpu_lvl;' 'fclose(f); int lvl2 = cpu_lvl;' + $native += @' + +const char* M68KTester_last_output(void) { return outbuffer; } +void M68KTester_destroy(M68KTesterContext* context) { + free(low_memory); low_memory = NULL; + free(high_memory); high_memory = NULL; + free(low_memory_temp); low_memory_temp = NULL; + free(high_memory_temp); high_memory_temp = NULL; + free(low_memory_back); low_memory_back = NULL; + free(high_memory_back); high_memory_back = NULL; + free(vbr_zero); vbr_zero = NULL; + free(absallocated); absallocated = NULL; test_memory = NULL; + free(context); +} +'@ + [IO.File]::WriteAllText((Join-Path $output 'winuae-native.c'), $native) + Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'winuae/msc_dirent.h') -Destination $output + Push-Location $output + try { + Run-Compiler @('/nologo','/O2','/w','/std:c11','/LD','/I.',"/I$vendor","/I$vendor/capstone-stub",'winuae-native.c','/Fe:m68k_cpu_tester.dll','/link','/EXPORT:M68KTester_init','/EXPORT:M68KTester_run_tests','/EXPORT:M68KTester_last_output','/EXPORT:m68k_tester_addressing_mask','/EXPORT:M68KTester_destroy') (Join-Path $output 'native-build.log') + } finally { Pop-Location } + + $profiles = @() + $baseIni = [IO.File]::ReadAllText((Join-Path $PSScriptRoot 'winuae/cputestgen.ini')).Replace("`r`n", "`n") + $index = 0 + foreach ($model in @( + @{id='68000'; cpu='68000'; width=24}, @{id='68010'; cpu='68010'; width=24}, + @{id='68EC020'; cpu='68020'; width=24}, @{id='68020'; cpu='68020'; width=32}, + @{id='68030'; cpu='68030'; width=32}, @{id='68040'; cpu='68040'; width=32}, + @{id='68060'; cpu='68060'; width=32})) { + $profileRoot = Join-Path $output $model.id + New-Item -ItemType Directory -Path $profileRoot | Out-Null + # Every invocation uses a fresh generator output path; stale data cannot fill a gap. + $prefix = 'copper68k-' + [guid]::NewGuid().ToString('N') + '/' + $config = $baseIni.Replace("cpu=68000`n", "cpu=$($model.cpu)`n").Replace('path=data/', "path=$prefix") + if ($model.width -eq 24) { $config = $config.Replace('cpu_address_space=68020','cpu_address_space=68030') } + else { $config = $config.Replace('test_high_memory_start=0x00ff8000','test_high_memory_start=0xffff8000').Replace('test_high_memory_end=0x01000000','test_high_memory_end=0xffffffff') } + [IO.File]::WriteAllText((Join-Path $profileRoot 'cputestgen.ini'), $config) + [IO.File]::WriteAllText((Join-Path $generator 'cputestgen.ini'), $config) + Push-Location $generator + try { + $generated = $prefix + $model.cpu + '_Basic' + New-Item -ItemType Directory -Path $generated -Force | Out-Null + & ./cputester.exe *> (Join-Path $profileRoot 'generation.log') + if ($LASTEXITCODE -ne 0) { throw "Fixture generation failed: $($model.id)" } + Copy-Item -LiteralPath $generated -Destination (Join-Path $profileRoot $model.cpu) -Recurse + } finally { Pop-Location } + $profiles += @{ + Id=$model.id; CpuDirectory=$model.cpu; CpuLevel=$index; AddressBits=$model.width + Opcodes=@(Get-ChildItem -LiteralPath (Join-Path $profileRoot $model.cpu) -Directory | Select-Object -ExpandProperty Name | Sort-Object) + Inputs=@(Get-ChildItem -LiteralPath $profileRoot -Recurse -File -Filter '*.dat' | Sort-Object FullName | ForEach-Object { + @{Path=[IO.Path]::GetRelativePath($profileRoot,$_.FullName).Replace('\','/'); Bytes=$_.Length; Sha256=(Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant()} + }) + } + if ($model.id -ne '68EC020') { $index++ } + } + @{ + Schema=1; GeneratorCommit=$generatorPin; RunnerCommit=$runnerPin; Profiles=$profiles + NativeLibrarySha256=(Get-FileHash -LiteralPath (Join-Path $output 'm68k_cpu_tester.dll') -Algorithm SHA256).Hash.ToLowerInvariant() + Compiler=$compiler; GeneratorExecutableSha256=(Get-FileHash -LiteralPath (Join-Path $generator 'cputester.exe') -Algorithm SHA256).Hash.ToLowerInvariant() + NativeSourceSha256=(Get-FileHash -LiteralPath (Join-Path $output 'winuae-native.c') -Algorithm SHA256).Hash.ToLowerInvariant() + Seed='Pinned generator xorshift state initialized to 1 per test set; one Basic round' + } | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'manifest.json') -Encoding utf8 + Write-Host "Prepared input manifest and native bridge: $output" +} finally { + foreach ($name in $savedEnvironment.Keys) { [Environment]::SetEnvironmentVariable($name, $savedEnvironment[$name]) } +} diff --git a/scripts/winuae/cputestgen.ini b/scripts/winuae/cputestgen.ini new file mode 100644 index 00000000..a17a44ca --- /dev/null +++ b/scripts/winuae/cputestgen.ini @@ -0,0 +1,43 @@ +[cputest] +cpu=68000 +cpu_address_space=68020 +fpu= +verbose=1 +path=data/ +feature_gzip=0 +test_low_memory_start=0x0000 +test_low_memory_end=0x8000 +test_high_memory_start=0x00ff8000 +test_high_memory_end=0x01000000 +high_rom= +test_memory_start=0x860000 +test_memory_size=0x40000 +opcode_memory_start=0x87ffa0 +test_rounds=1 +feature_exception3_data=0 +feature_exception3_instruction=0 +feature_target_src_ea= +feature_target_dst_ea= +feature_target_opcode_offset= +feature_safe_memory_start= +feature_safe_memory_size= +feature_safe_memory_mode= +feature_usp=0 +feature_exception_vectors= +feature_flags_mode=1 +feature_min_interrupt_mask=0 +feature_interrupts=0 +feature_sr_mask=0x0000 +feature_loop_mode=0 +feature_loop_mode_register=7 +feature_loop_mode_68010=0 +feature_full_extension_format=1 +feature_addressing_modes_src= +feature_addressing_modes_dst= +feature_instruction_size= +mode= +[test=Basic] +cpu=68000-68060 +enabled=1 +mode=all +feature_sr_mask=0x0000 diff --git a/scripts/winuae/msc_dirent.h b/scripts/winuae/msc_dirent.h new file mode 100644 index 00000000..fc82d0f8 --- /dev/null +++ b/scripts/winuae/msc_dirent.h @@ -0,0 +1,24 @@ +#pragma once +#include +#include +#include +#ifndef S_ISDIR +#define S_ISDIR(m) (((m) & _S_IFMT) == _S_IFDIR) +#endif +struct dirent { char d_name[MAX_PATH]; }; +typedef struct { HANDLE handle; WIN32_FIND_DATAA data; int first; struct dirent entry; } DIR; +static DIR* opendir(const char* path) { + char pattern[4096]; + if (strlen(path) + 3 >= sizeof(pattern)) return NULL; + strcpy(pattern, path); strcat(pattern, "/*"); + DIR* dir = (DIR*)calloc(1, sizeof(DIR)); + if (!dir) return NULL; + dir->handle = FindFirstFileA(pattern, &dir->data); dir->first = 1; + if (dir->handle == INVALID_HANDLE_VALUE) { free(dir); return NULL; } + return dir; +} +static struct dirent* readdir(DIR* dir) { + if (!dir || (!dir->first && !FindNextFileA(dir->handle, &dir->data))) return NULL; + dir->first = 0; strcpy(dir->entry.d_name, dir->data.cFileName); return &dir->entry; +} +static int closedir(DIR* dir) { if (!dir) return -1; FindClose(dir->handle); free(dir); return 0; } From 9f038270131d5c691aa0bb1e451cc988ceaef193 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 00:11:59 +0300 Subject: [PATCH 011/211] Verify WinUAE exception frames and defined status flags --- .../M68kWinUaeCpuTesterConformanceTests.cs | 45 ++++++++- .../M68kWinUaeCpuTesterConformanceTests.md | 56 +++++++++++ Copper68k.Tests/M68kWinUaeModelAuditTests.cs | 35 +++++-- Copper68k.Tests/WinUaeArchitecturalFlags.cs | 19 ++++ .../WinUaeArchitecturalFlagsTests.cs | 27 ++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 56 +++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 25 +++++ scripts/prepare-copper68k-winuae.ps1 | 14 ++- scripts/winuae/integer_validation.h | 94 +++++++++++++++++++ 9 files changed, 359 insertions(+), 12 deletions(-) create mode 100644 Copper68k.Tests/WinUaeArchitecturalFlags.cs create mode 100644 Copper68k.Tests/WinUaeArchitecturalFlagsTests.cs create mode 100644 scripts/winuae/integer_validation.h diff --git a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs index 5f893685..0a45210d 100644 --- a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs +++ b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs @@ -293,6 +293,9 @@ private sealed class NativeTester : IDisposable private readonly NativeLastOutput? _lastOutput; private readonly NativeAddressingMask? _addressingMask; private readonly NativeDestroy? _destroy; + private readonly NativeDefinedSr? _definedSr; + private readonly NativeCount? _frameChecks; + private readonly NativeCount? _maskedCases; private readonly NativeCallback _callback; private Exception? _callbackException; private int _executedCases; @@ -302,6 +305,10 @@ private sealed class NativeTester : IDisposable private byte _cpuLevel; private ModelSpec? _integerProfile; private bool _corruptResult; + private bool _corruptFrame; + private ushort _corruptSr; + private bool _corruptIgnoredSr; + private string _integerFamily = ""; private NativeTester(IntPtr library) { @@ -311,6 +318,9 @@ private NativeTester(IntPtr library) _lastOutput = TryGetDelegate(library, "M68KTester_last_output"); _addressingMask = TryGetDelegate(library, "m68k_tester_addressing_mask"); _destroy = TryGetDelegate(library, "M68KTester_destroy"); + _definedSr = TryGetDelegate(library, "M68KTester_set_defined_sr"); + _frameChecks = TryGetDelegate(library, "M68KTester_frame_checks"); + _maskedCases = TryGetDelegate(library, "M68KTester_masked_cases"); _callback = RunCopper68k; } @@ -339,7 +349,10 @@ public RunResult Run( bool checkUndefinedSr, bool continueOnError, ModelSpec? integerProfile = null, - bool corruptResult = false) + bool corruptResult = false, + bool corruptFrame = false, + ushort corruptSr = 0, + bool corruptIgnoredSr = false) { _callbackException = null; _executedCases = 0; @@ -349,8 +362,14 @@ public RunResult Run( _cpuLevel = cpuLevel; _integerProfile = integerProfile; _corruptResult = corruptResult; + _corruptFrame = corruptFrame; + _corruptSr = corruptSr; + _corruptIgnoredSr = corruptIgnoredSr; + _integerFamily = opcode; if (integerProfile is not null && (_destroy is null || _addressingMask is null || _lastOutput is null)) throw new XunitException("Multi-model integer audit requires the qualified native bridge exports (destroy, addressing mask and diagnostics)."); + if (integerProfile is not null && (_definedSr is null || _frameChecks is null || _maskedCases is null)) + throw new XunitException("Multi-model integer audit requires architectural SR masks and exception-frame validation exports."); var corpusPathPtr = Marshal.StringToHGlobalAnsi(corpusPath); var opcodePtr = Marshal.StringToHGlobalAnsi(opcode); @@ -426,6 +445,10 @@ public readonly record struct RunResult( int UnmappedWrites, string Detail); + public uint FrameChecks => _frameChecks?.Invoke() ?? 0; + public uint MaskedCases => _maskedCases?.Invoke() ?? 0; + public bool UnsupportedExecution => _callbackException?.InnerException is UnsupportedM68kTimingException; + public void Dispose() { if (_library != IntPtr.Zero) @@ -452,7 +475,7 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt : _cpuLevel == 4 ? new M68040Interpreter(bus, M68020CpuProfile.Ocs68040Accelerator25Mhz) : new M68kInterpreter(bus); - _lastCaseSummary = FormatCaseSummary(context, registers, bus); + _lastCaseSummary = (_integerProfile is null ? "" : $"family={_integerFamily}, ") + FormatCaseSummary(context, registers, bus); bus.CopyStackImage(registers.Regs[15], registers.Ssp, 0x20); ApplyRegisters(cpu, registers, _cpuLevel); @@ -523,7 +546,19 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt } CopyRegisters(cpu.State, bus, _cpuLevel, ref registers); + if (_integerProfile is not null) + { + var mask = WinUaeArchitecturalFlags.DefinedMask(_integerFamily, cpu.State.LastExceptionVector, (ushort)registers.Sr); + _definedSr!(mask); + if (_corruptIgnoredSr) registers.Sr ^= (uint)(~mask & 31); + } if (_corruptResult) registers.Regs[0] ^= 1; + registers.Sr ^= _corruptSr; + if (_corruptFrame && cpu.State.LastExceptionVector >= 0) + { + var address = registers.ExcFrame + (_cpuLevel == 0 ? 4u : 6u); + bus.WriteHostWord(address, (ushort)(bus.ReadHostWord(address) ^ 1)); + } Marshal.StructureToPtr(registers, registersPtr, fDeleteOld: false); _unmappedReads += bus.UnmappedReads; _unmappedWrites += bus.UnmappedWrites; @@ -807,6 +842,12 @@ private static T GetDelegate(IntPtr library, string exportName) [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void NativeDestroy(IntPtr context); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void NativeDefinedSr(ushort mask); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate uint NativeCount(); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void NativeCallback(IntPtr userData, IntPtr context, IntPtr registers); } diff --git a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md index a301863d..4fc17af8 100644 --- a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md +++ b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md @@ -157,3 +157,59 @@ timing, cache, enabled MMU and FPU arithmetic remain outside this integer audit. Generator candidate counts and executed callback counts are separate quantities. The audit remains failing until reference/adapter/CPU disagreements are resolved; its presence does not mark milestone 6 complete. + +## Architectural SR masks and exception-frame assertions + +The integer model audit requires the updated bridge exports. Older bridges which +skip exception-frame records are rejected before callbacks execute. Report schema +2 adds executed frame assertions, cases using architectural SR masks, probe kinds +and adapter/CPU assembly SHA-256 identities. The input manifest also records the +native integer-validator header hash. + +`WinUaeArchitecturalFlags` defines comparison masks independently of the production +CPU. Its source is [M68000PM](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) +sections 4-2, 4-69/71, 4-92/96, 4-141 and 4-170. CHK retains its defined trap N +bit and X; CHK2/CMP2 and decimal operations retain their defined flags. Division +masks depend on zero/overflow outcome, while defined C/V/X remain checked as +applicable. Exception identity and those outcome bits are independently asserted. +Normal execution results are never normalized to the reference. The bridge applies +these masks to both explicit SR updates and preservation checks. Fourteen ordinary +rule regressions retain every upper SR bit and X. + +The old Copperline `validate_exception` returned immediately after consuming each +length-prefixed record. It therefore verified exception numbers without frame +contents; the preceding report's generic frame-effects wording was too broad. +The replacement parser follows the pinned generator's `save_exception` layout. +It reconstructs expected saved SR/PC from independent fixture result records, +parses format/vector words and format-2/3/4 additional addresses, and compares +actual memory at the reported exception stack pointer. Normal 68000 six-byte +frames are also checked. Repeated-record markers retain the reference fields. +Standard trace frames produced by SR-changing instructions are included. +Unimplemented extra-trace/group-2/fault and restart-frame records fail explicitly. +Nothing silently skips those records to claim coverage. + +Every model must reject three distinct mutations: NOP D0, NOP's defined X, and +TRAP's actual frame memory (saved-PC word on 68000, format/vector word otherwise). +The frame mutation occurs after copying result registers, isolating memory +validation from the ordinary register assertions. A fourth control toggles only +CHK's documented undefined bits in returned SR; all those cases must still pass. +This proves both that defined assertions remain active and that undefined bits +are excluded from architectural comparisons. Probe callbacks are separate from +normal coverage totals. Native diagnostics now terminate appended byte/line data +so stale previous-case text cannot contaminate the reported failure. + +The stronger comparator exposes additional saved-PC disagreements, including +BKPT, CHK2 and TRAPcc. The pinned TRAPcc generator raises the exception before +synchronizing PC; local newer WinUAE revision +`6ae6fb6b84bb9517e0245a80fc9bdca1a8580dde`, `gencpu.cpp`'s `i_TRAPcc` case, +synchronizes PC before raising it. M68000PM 4-189 also specifies the next +instruction-word address. This is a reference disagreement requiring generator +qualification, not permission to change Copper68k to the old expected value. +Other saved-PC and result disagreements remain unclassified pending equivalent +manual/source audits. No family is excluded and the requested audit remains red. + +Schema 2 separates emulator-unsupported callbacks from value/frame mismatches. +Both fail the gate. The current classifier records 62 mismatching and 15 unsupported +groups (77 total), with zero untested; frame/masked-case counts and all 32 controls +are recorded separately from normal callbacks. Older bridges lacking these +assertions are rejected. Source/adapter/CPU disagreements remain open. diff --git a/Copper68k.Tests/M68kWinUaeModelAuditTests.cs b/Copper68k.Tests/M68kWinUaeModelAuditTests.cs index 531b9848..d1eee127 100644 --- a/Copper68k.Tests/M68kWinUaeModelAuditTests.cs +++ b/Copper68k.Tests/M68kWinUaeModelAuditTests.cs @@ -45,28 +45,45 @@ public void WinUaeIntegerFixturesAcrossSelectedModelsWhenEnabled() // A native 'success' contract must detect a deliberately wrong result. var probe = tester.Run(path, "NOP", fixture.CpuLevel, false, false, model, corruptResult: true); var detected = !probe.Passed && probe.ExecutedCases > 0 && probe.Detail.Contains("WinUAE cputest reported failure", StringComparison.Ordinal); - probes.Add(new(id, detected, probe.ExecutedCases, probe.Detail)); + probes.Add(new(id, "register", detected, probe.ExecutedCases, probe.Detail)); if (!detected) throw new XunitException($"Native register-corruption probe was not detected for {id}: {probe.Detail}"); + var srProbe = tester.Run(path, "NOP", fixture.CpuLevel, false, false, model, corruptSr: 0x10); + var srDetected = !srProbe.Passed && srProbe.ExecutedCases > 0 && srProbe.Detail.Contains("SR:", StringComparison.Ordinal); + probes.Add(new(id, "defined-sr", srDetected, srProbe.ExecutedCases, srProbe.Detail)); + if (!srDetected) throw new XunitException($"Native defined-SR corruption probe was not detected for {id}: {srProbe.Detail}"); + var frameProbe = tester.Run(path, "TRAP", fixture.CpuLevel, false, false, model, corruptFrame: true); + var frameDetected = !frameProbe.Passed && tester.FrameChecks > 0 && frameProbe.Detail.Contains("frame byte", StringComparison.Ordinal); + probes.Add(new(id, "exception-frame", frameDetected, frameProbe.ExecutedCases, frameProbe.Detail)); + if (!frameDetected) throw new XunitException($"Native exception-frame corruption probe was not detected for {id}: {frameProbe.Detail}"); + var undefinedProbe = tester.Run(path, "CHK.W", fixture.CpuLevel, false, false, model, corruptIgnoredSr: true); + var undefinedVerified = undefinedProbe.Passed && undefinedProbe.ExecutedCases > 0 && tester.MaskedCases == undefinedProbe.ExecutedCases; + probes.Add(new(id, "undefined-sr-ignored", undefinedVerified, undefinedProbe.ExecutedCases, undefinedProbe.Detail)); + if (!undefinedVerified) throw new XunitException($"Undefined-SR comparison control failed for {id}: {undefinedProbe.Detail}"); foreach (var opcode in fixture.Opcodes) { try { var result = tester.Run(path, opcode, fixture.CpuLevel, false, false, model); - rows.Add(new(id, opcode, result.Passed ? result.ExecutedCases > 0 ? "passing" : "untested" : "mismatching", - result.ExecutedCases, result.UnmappedReads, result.UnmappedWrites, result.Detail)); + rows.Add(new(id, opcode, result.Passed ? result.ExecutedCases > 0 ? "passing" : "untested" : tester.UnsupportedExecution ? "unsupported" : "mismatching", + result.ExecutedCases, result.UnmappedReads, result.UnmappedWrites, tester.FrameChecks, tester.MaskedCases, result.Detail)); } - catch (Exception ex) { rows.Add(new(id, opcode, "mismatching", 0, 0, 0, ex.Message)); } + catch (Exception ex) { rows.Add(new(id, opcode, "mismatching", 0, 0, 0, 0, 0, ex.Message)); } } _output.WriteLine($"{id}: {rows.Count(r => r.Model == id && r.Status == "passing")} directories passed; {rows.Where(r => r.Model == id).Sum(r => (long)r.ExecutedCases)} callbacks executed."); } File.WriteAllText(Path.Combine(output, "winuae-model-audit.json"), JsonSerializer.Serialize(new { - schema = 1, reference = "WinUAE generator through Copperline native assertion runner", selectedModels = ids, + schema = 2, reference = "WinUAE generator through Copperline native assertion runner", selectedModels = ids, manifest.GeneratorCommit, manifest.RunnerCommit, manifest.NativeLibrarySha256, manifestSha256 = Hash(Path.Combine(root, "manifest.json")), - qualification = "Pinned software assertions; integer state, defined SR and memory/frame effects. Basic preset omits bus/address faults, trace/M rounds, physical timing, cache/MMU/FPU and advanced RTE restart protocols. Callbacks differ from generator candidate counts.", + adapterAssemblySha256 = Hash(typeof(M68kWinUaeCpuTesterConformanceTests).Assembly.Location), + cpuAssemblySha256 = Hash(typeof(Copper68k.M68kCoreFactory).Assembly.Location), + flagAuthority = "M68000PM 4-2, 4-69/71, 4-92/96, 4-141, 4-170; comparison masks only, actual results unchanged", + qualification = "Pinned software assertions; integer state, independently defined SR masks, memory and Basic format 0/2/3/4 exception frames (68000 six-byte frames). Other exception records fail instead of being skipped. Basic preset omits bus/address faults, trace/M rounds, physical timing, cache/MMU/FPU and advanced RTE restart protocols. Callbacks differ from generator candidate counts.", passing = rows.Count(r => r.Status == "passing"), mismatching = rows.Count(r => r.Status == "mismatching"), - untested = rows.Count(r => r.Status == "untested"), executedCases = rows.Sum(r => (long)r.ExecutedCases), probes, rows + unsupported = rows.Count(r => r.Status == "unsupported"), + untested = rows.Count(r => r.Status == "untested"), executedCases = rows.Sum(r => (long)r.ExecutedCases), + exceptionFrames = rows.Sum(r => (long)r.ExceptionFrames), maskedSrCases = rows.Sum(r => (long)r.MaskedSrCases), probes, rows }, new JsonSerializerOptions { WriteIndented = true })); var failures = rows.Where(r => r.Status != "passing").ToArray(); Assert.True(failures.Length == 0, string.Join(Environment.NewLine, failures.Select(r => $"{r.Model}/{r.Opcode}: {r.Detail}"))); @@ -131,6 +148,6 @@ internal static void ValidateWinUaeProfile(string root, WinUaeManifest manifest, internal sealed record WinUaeManifest(int Schema, string GeneratorCommit, string RunnerCommit, string NativeLibrarySha256, WinUaeProfile[] Profiles); internal sealed record WinUaeProfile(string Id, string CpuDirectory, byte CpuLevel, int AddressBits, string[] Opcodes, WinUaeInput[] Inputs); internal sealed record WinUaeInput(string Path, long Bytes, string Sha256); - private sealed record WinUaeModelRow(string Model, string Opcode, string Status, int ExecutedCases, int UnmappedReads, int UnmappedWrites, string Detail); - private sealed record WinUaeProbeRow(string Model, bool Detected, int ExecutedCases, string Detail); + private sealed record WinUaeModelRow(string Model, string Opcode, string Status, int ExecutedCases, int UnmappedReads, int UnmappedWrites, uint ExceptionFrames, uint MaskedSrCases, string Detail); + private sealed record WinUaeProbeRow(string Model, string Kind, bool Detected, int ExecutedCases, string Detail); } diff --git a/Copper68k.Tests/WinUaeArchitecturalFlags.cs b/Copper68k.Tests/WinUaeArchitecturalFlags.cs new file mode 100644 index 00000000..face4485 --- /dev/null +++ b/Copper68k.Tests/WinUaeArchitecturalFlags.cs @@ -0,0 +1,19 @@ +namespace Copper68k.Tests; + +// Independent architectural comparison rules, M68000PM 4-69/71, 4-92/96, +// 4-2, 4-141 and 4-170. Undefined bits remain in the actual CPU result; +// only the reference comparator mask changes. Exception identity and defined +// V/N/C bits are independently checked by the native comparator. +internal static class WinUaeArchitecturalFlags +{ + internal static ushort DefinedMask(string family, int exception, ushort resultSr) => family switch + { + "CHK.W" or "CHK.L" when exception is -1 or 6 => exception == 6 ? (ushort)0xfff8 : (ushort)0xfff0, + "CHK2.B" or "CHK2.W" or "CHK2.L" or "CMP2.B" or "CMP2.W" or "CMP2.L" + when exception is -1 or 6 => 0xfff5, + "ABCD.B" or "SBCD.B" or "NBCD.B" when exception == -1 => 0xfff5, + "DIVS.W" or "DIVU.W" or "DIVL.L" when exception == 5 => 0xfff1, + "DIVS.W" or "DIVU.W" or "DIVL.L" when exception == -1 && (resultSr & 2) != 0 => 0xfff3, + _ => 0xffff + }; +} diff --git a/Copper68k.Tests/WinUaeArchitecturalFlagsTests.cs b/Copper68k.Tests/WinUaeArchitecturalFlagsTests.cs new file mode 100644 index 00000000..e534bee5 --- /dev/null +++ b/Copper68k.Tests/WinUaeArchitecturalFlagsTests.cs @@ -0,0 +1,27 @@ +namespace Copper68k.Tests; + +public sealed class WinUaeArchitecturalFlagsTests +{ + [Theory] + [InlineData("NOP", -1, 31, 0xffff)] + [InlineData("CHK.W", -1, 31, 0xfff0)] + [InlineData("CHK.L", 6, 31, 0xfff8)] + [InlineData("CHK.W", 3, 31, 0xffff)] + [InlineData("CHK2.W", 6, 31, 0xfff5)] + [InlineData("CMP2.B", -1, 31, 0xfff5)] + [InlineData("CHK2.L", 61, 31, 0xffff)] + [InlineData("ABCD.B", -1, 31, 0xfff5)] + [InlineData("SBCD.B", -1, 31, 0xfff5)] + [InlineData("NBCD.B", -1, 31, 0xfff5)] + [InlineData("DIVS.W", 5, 31, 0xfff1)] + [InlineData("DIVU.W", -1, 2, 0xfff3)] + [InlineData("DIVL.L", -1, 0, 0xffff)] + [InlineData("DIVS.W", 3, 31, 0xffff)] + public void MasksOnlyDocumentedUndefinedBits(string family, int exception, int resultSr, int mask) + { + var actual = WinUaeArchitecturalFlags.DefinedMask(family, exception, (ushort)resultSr); + Assert.Equal((ushort)mask, actual); + Assert.Equal(0xffe0, actual & 0xffe0); // All upper architectural SR bits remain checked. + Assert.NotEqual(0, actual & 0x10); // X always remains checked for these rules. + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index daf771fd..4a52c06f 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -659,3 +659,59 @@ A wrong source revision is rejected before creating output. PowerShell syntax an Git whitespace checks pass. Previous production/consumer evidence remains the `1.5.2-synthetic-dev.39` checkpoint; no new consumer replay or package is claimed for this test-only follow-up. + +## WinUAE defined flags and exception-frame verification + +The native wrapper's previous exception validator skipped modern frame records, +so earlier discovery counts did not independently qualify saved frame contents. +The new test-only parser checks normal six-byte 68000 frames, format/vector words, +saved SR/PC and format-2/3/4 extra addresses using independent fixture results. +Unsupported trace-extra, combined-fault or restart records fail explicitly. +Architectural SR masks now come from M68000PM rather than the pinned generator's +zero instruction-level undefined-mask field. CPU results stay unchanged; only +undefined comparisons are masked. Defined X/SR/register assertions remain active. + +Twenty-four mutations are rejected across eight profiles: wrong D0, defined X, +and actual exception-frame memory. Eight additional controls toggle only undefined +CHK flags and pass. The actual-frame mutation occurs after copying returned +registers, so it proves a separate memory assertion. Older frame-skipping bridges +are rejected before execution. The report records schema 2, assembly/input/native +identities, executed frame checks and cases using partial SR masks. + +The stronger audit records 1,304 passing and 77 mismatching opcode/profile groups, +11,133,876 callbacks, 1,371,000 exception-frame checks and 199,327 masked-SR cases. +Totals include partial failing groups; these are scoped software assertions, not +qualified hardware coverage. All 32 controls pass. No family is excluded to make +this run green. The initial stricter parser's trace-record rejection was corrected +to accept ordinary trace frames; unsupported extra records still fail. + +Remaining saved-PC disagreements require source qualification: the pinned TRAPcc +generator raises before advancing PC, while newer WinUAE source at revision +`6ae6fb6b84bb9517e0245a80fc9bdca1a8580dde` synchronizes PC first. M68000PM 4-189 +specifies the next instruction-word address. The old generated value cannot serve +as authority for changing the CPU. Other failing families still need independent +triage. Existing production behavior and timing policy remain untouched. + +Evidence: `artifacts/m6-winuae-frame-focused/`, +`artifacts/m6-winuae-frame-cpu/`, `artifacts/m6-winuae-basic-trace-inputs/`, +`artifacts/m6-winuae-basic-trace-audit/`, +`artifacts/m6-winuae-old-bridge-rejected/` and +`artifacts/m6-winuae-frame-verified/`. No package or old-test retirement is included; +milestone 6 remains in progress. + +The final schema-2 run distinguishes the 77 non-passing groups as **62 mismatching +and 15 emulator-unsupported**, with zero untested groups. Unsupported callbacks +are identified by their typed `UnsupportedM68kTimingException`; they remain gate +failures, including reserved/invalid encodings whose architectural classification +still needs review. No documented processor exception is treated as implementation +unsupported merely because its vector is raised. + +Ordinary Release validation passes **4,652 tests**, with nine optional/opt-in +skips and zero failures. The final focused run passes 20 rule/preflight tests +without skips; its separate external audit intentionally fails on the 77 unresolved +groups. All 32 mutation/acceptance controls pass. The old bridge is rejected with +the explicit missing-validation-export diagnostic. The final classifier report is +`artifacts/m6-winuae-frame-classified/winuae-model-audit.json`. Its counts reproduce +the stronger audit above. Preparation, PowerShell syntax and Git whitespace checks +pass. This follow-up changes only test tooling/documentation; existing .39 consumer +results remain prior evidence, without a new package or replay claim. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index b53665ae..897a659d 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -641,3 +641,28 @@ nine optional/opt-in skips and zero failures. Fresh fixture generation reproduce the failing audit counts above and all eight corruption probes. Source revision rejection, script syntax and whitespace checks pass. Earlier production/consumer validation remains separate; no new package or consumer replay is claimed. + +The next reference-comparator checkpoint corrects an audit blind spot: Copperline +skipped modern exception-frame records. A test-only parser now validates saved +SR/PC, format/vector words and format-2/3/4 addresses, including normal 68000 and +trace frames. Unsupported extra/restart records fail explicitly. Independent +M68000PM masks limit SR comparisons to defined bits without changing CPU results. +Twenty-four register/SR/frame mutations are rejected and eight undefined-flag +acceptance controls pass across the eight profiles. Old frame-skipping bridges +are rejected before callbacks. Report schema 2 records assembly/native/input +identities and actual frame/masked-case counts. + +The stronger audit records 1,304 passing / 77 mismatching groups, 11,133,876 +callbacks and 1,371,000 frame checks; failing groups may be partial. It remains +red without excluding any family. The pinned TRAPcc generator disagrees with newer +WinUAE source and the documented PC rule, so remaining mismatches require source/ +adapter/CPU triage rather than blindly changing the CPU. Milestone 6 remains open. + +Final classification splits the 77 non-passing groups into 62 mismatching and +15 emulator-unsupported, with zero untested; all still fail the requested gate. +The ordinary Release CPU suite passes 4,652 tests with nine optional skips and +zero failures. Twenty focused rule/preflight tests and all 32 native controls pass. +Old frame-skipping bridges are rejected. Test-only changes preserve the production +CPU, timing policy, package/API and prior consumer evidence. No release or regression +retirement is included. The reference document records the scoped evidence and +remaining generator/adapter/CPU classification work. diff --git a/scripts/prepare-copper68k-winuae.ps1 b/scripts/prepare-copper68k-winuae.ps1 index a4a82f8d..698e6522 100644 --- a/scripts/prepare-copper68k-winuae.ps1 +++ b/scripts/prepare-copper68k-winuae.ps1 @@ -57,6 +57,16 @@ try { $native = Patch-Once $native 'cpu_lvl = settings->cpu_level == 6 ? 5 : settings->cpu_level;' 'ccr_mask = 0xff; check_undefined_sr = settings->check_undefined_sr; cpu_lvl = settings->cpu_level == 6 ? 5 : settings->cpu_level;' $native = Patch-Once $native 'context->opcode = settings->opcode;' 'context->stop_on_error = !settings->continue_on_error; context->opcode = settings->opcode;' $native = Patch-Once $native 'int lvl2 = cpu_lvl;' 'fclose(f); int lvl2 = cpu_lvl;' + $start = $native.IndexOf('static uae_u8* validate_exception(', [StringComparison]::Ordinal); + $end = $native.IndexOf('// regs: registers before execution of test code', $start, [StringComparison]::Ordinal); + if ($start -lt 0 -or $end -le $start) { throw 'Exception-validator patch anchor changed' } + $native = $native.Substring(0, $start) + "#include `"integer_validation.h`"`n`n" + $native.Substring($end); + $native = Patch-Once $native 'vbr_zero = calloc(1, 1024);' 'vbr_zero = calloc(1, 1024); copper_defined_sr = 0xffff; copper_frame_checks = 0; copper_masked_cases = 0; last_exception_len = 0;' + if (($native.Split([string[]]@('sr_undefined_mask & test_ccrignoremask'), [StringSplitOptions]::None).Count - 1) -ne 2) { throw 'SR comparison patch anchors changed' } + $native = $native.Replace('sr_undefined_mask & test_ccrignoremask', 'sr_undefined_mask & test_ccrignoremask & copper_defined_sr') + $native = Patch-Once $native 'if ((last_registers.sr & test_ccrignoremask) != (test_regs.sr & test_ccrignoremask))' 'if ((last_registers.sr & test_ccrignoremask & sr_undefined_mask & copper_defined_sr) != (test_regs.sr & test_ccrignoremask & sr_undefined_mask & copper_defined_sr))' + $native = Patch-Once $native 'printf("%s\n", outbuffer);' '*outbp = 0; printf("%s\n", outbuffer);' + $native = $native.Replace("*outbp++ = '\n';", "*outbp++ = '\n'; *outbp = 0;") $native += @' const char* M68KTester_last_output(void) { return outbuffer; } @@ -74,9 +84,10 @@ void M68KTester_destroy(M68KTesterContext* context) { '@ [IO.File]::WriteAllText((Join-Path $output 'winuae-native.c'), $native) Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'winuae/msc_dirent.h') -Destination $output + Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'winuae/integer_validation.h') -Destination $output Push-Location $output try { - Run-Compiler @('/nologo','/O2','/w','/std:c11','/LD','/I.',"/I$vendor","/I$vendor/capstone-stub",'winuae-native.c','/Fe:m68k_cpu_tester.dll','/link','/EXPORT:M68KTester_init','/EXPORT:M68KTester_run_tests','/EXPORT:M68KTester_last_output','/EXPORT:m68k_tester_addressing_mask','/EXPORT:M68KTester_destroy') (Join-Path $output 'native-build.log') + Run-Compiler @('/nologo','/O2','/w','/std:c11','/LD','/I.',"/I$vendor","/I$vendor/capstone-stub",'winuae-native.c','/Fe:m68k_cpu_tester.dll','/link','/EXPORT:M68KTester_init','/EXPORT:M68KTester_run_tests','/EXPORT:M68KTester_last_output','/EXPORT:m68k_tester_addressing_mask','/EXPORT:M68KTester_destroy','/EXPORT:M68KTester_set_defined_sr','/EXPORT:M68KTester_frame_checks','/EXPORT:M68KTester_masked_cases') (Join-Path $output 'native-build.log') } finally { Pop-Location } $profiles = @() @@ -118,6 +129,7 @@ void M68KTester_destroy(M68KTesterContext* context) { NativeLibrarySha256=(Get-FileHash -LiteralPath (Join-Path $output 'm68k_cpu_tester.dll') -Algorithm SHA256).Hash.ToLowerInvariant() Compiler=$compiler; GeneratorExecutableSha256=(Get-FileHash -LiteralPath (Join-Path $generator 'cputester.exe') -Algorithm SHA256).Hash.ToLowerInvariant() NativeSourceSha256=(Get-FileHash -LiteralPath (Join-Path $output 'winuae-native.c') -Algorithm SHA256).Hash.ToLowerInvariant() + IntegerValidationSha256=(Get-FileHash -LiteralPath (Join-Path $output 'integer_validation.h') -Algorithm SHA256).Hash.ToLowerInvariant() Seed='Pinned generator xorshift state initialized to 1 per test set; one Basic round' } | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'manifest.json') -Encoding utf8 Write-Host "Prepared input manifest and native bridge: $output" diff --git a/scripts/winuae/integer_validation.h b/scripts/winuae/integer_validation.h new file mode 100644 index 00000000..09a98861 --- /dev/null +++ b/scripts/winuae/integer_validation.h @@ -0,0 +1,94 @@ +/* Test-only Basic-preset validation. Record layout is pinned to + * emoon/m68k_cpu_tester_api 025b999, gencpu/cputest.cpp save_exception. + * No trace, group-2/fault combination or undocumented restart image is skipped. + * An unsupported record fails the audit instead of claiming frame coverage. */ +static uae_u16 copper_defined_sr = 0xffff; +static unsigned int copper_frame_checks, copper_masked_cases; + +void M68KTester_set_defined_sr(uae_u16 mask) { + copper_defined_sr = mask; + if (mask != 0xffff) copper_masked_cases++; +} +unsigned int M68KTester_frame_checks(void) { return copper_frame_checks; } +unsigned int M68KTester_masked_cases(void) { return copper_masked_cases; } + +static uae_u8* validate_exception(struct registers* actual, uae_u8* p, + int excnum, int sameexc, int* experr) { + unsigned int length = *p++; + uae_u8* next = p; + uae_u8* fields; + int frame_length = 0; + uae_u8 expected[16] = {0}; + if (length == 0) goto unsupported; + if (length == 0xff) { + if (last_exception_len <= 0) goto unsupported; + fields = last_exception; + length = last_exception_len; + } else { + next = p + length; + memcpy(last_exception, p, length); + last_exception_len = length; + fields = last_exception; + } + uae_u8* end = fields + length; + /* First byte describes extra trace/group-2 exceptions. Basic has none. */ + if (*fields++ != 0 || excnum == 2 || excnum == 3) + goto unsupported; + expected[0] = last_registers.sr >> 8; + expected[1] = last_registers.sr; + pl(expected + 2, last_registers.pc); + if (cpu_lvl == 0) { + frame_length = 6; + } else { + if (end - fields < 2) goto unsupported; + unsigned int format = (fields[0] << 8) | fields[1]; + expected[6] = *fields++; + expected[7] = *fields++; + switch (format >> 12) { + case 0: frame_length = 8; break; + case 2: + case 3: + case 4: { + uae_u32 value = opcode_memory_addr; + if (fields >= end) goto unsupported; + fields = restore_rel_ordered(fields, &value); + pl(expected + 8, value); + frame_length = 12; + if ((format >> 12) == 4) { + value = opcode_memory_addr; + if (fields >= end) goto unsupported; + fields = restore_rel_ordered(fields, &value); + pl(expected + 12, value); + frame_length = 16; + } + break; + } + default: goto unsupported; + } + } + if (fields != end) goto unsupported; + if (!sameexc) return next; /* The caller independently fails vector mismatch. */ + uae_u8* observed = translate_to_native(actual->excframe); + if (!observed) goto unsupported; + copper_frame_checks++; + uae_u16 sr_mask = sr_undefined_mask & test_ccrignoremask & copper_defined_sr; + for (int index = 0; index < frame_length; index++) { + uae_u8 mask = index == 0 ? sr_mask >> 8 : index == 1 ? sr_mask : 0xff; + if ((expected[index] & mask) == (observed[index] & mask)) continue; + addinfo(); + sprintf(outbp, "Exception %d frame byte %d: expected %02x but got %02x (mask %02x)\n", + excnum, index, expected[index], observed[index], mask); + outbp += strlen(outbp); + errors++; + *experr = 1; + break; + } + return next; +unsupported: + addinfo(); + sprintf(outbp, "Unsupported or malformed Basic exception record (vector %d)\n", excnum); + outbp += strlen(outbp); + errors++; + *experr = 1; + return next; +} From fb86a4eebdb6d8a5e5345e76eb0662c9e7879053 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 00:34:46 +0300 Subject: [PATCH 012/211] Qualify illegal logical operands across CPU models --- Copper68k.Tests/M68kBitTests.cs | 13 ++- Copper68k.Tests/Synthetic/README.md | 11 ++- .../Synthetic/SyntheticInvalidLogicalTests.cs | 83 +++++++++++++++++++ Copper68k/M68kAdvancedTimingInterpreter.cs | 19 +++++ Copper68k/M68kCore.cs | 20 +---- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 69 +++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 26 ++++++ scripts/test-copper68k-synthetic.ps1 | 3 +- 8 files changed, 219 insertions(+), 25 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticInvalidLogicalTests.cs diff --git a/Copper68k.Tests/M68kBitTests.cs b/Copper68k.Tests/M68kBitTests.cs index 2e11f70c..a90364d3 100644 --- a/Copper68k.Tests/M68kBitTests.cs +++ b/Copper68k.Tests/M68kBitTests.cs @@ -23,18 +23,23 @@ public void DynamicBtstCanTestImmediateOperand() } [Fact] - public void ImmediateBtstCanTestImmediateOperand() + public void ImmediateBtstCannotTestImmediateOperand() { var bus = new Copper68kTestBus(); bus.WriteWords(0x1000, 0x083C, 0x0007, 0x3FC2); + bus.WriteLong(16, 0x5000); // Illegal-instruction vector var cpu = new M68kInterpreter(bus); cpu.Reset(0x1000, 0x8000); - cpu.State.StatusRegister = 0x2700; + cpu.State.StatusRegister = 0x271B; cpu.ExecuteInstruction(); - Assert.False(cpu.State.GetFlag(M68kCpuState.Zero)); - Assert.Equal(0x1006u, cpu.State.ProgramCounter); + // M68000PM 4-63 excludes #data for static BTST. Dynamic BTST's + // different legal table (4-62) remains covered separately above. + Assert.Equal(4, cpu.State.LastExceptionVector); + Assert.Equal(0x5000u, cpu.State.ProgramCounter); + Assert.Equal(0x271B, bus.ReadWord(0x7FFA)); + Assert.Equal(0x1000u, bus.ReadLong(0x7FFC)); } } diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index a3e228d3..67dca6e2 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -16,12 +16,21 @@ From the CopperMod root, run: Milestones 1–5 execute MOVE/MOVEA, transfer/address, arithmetic/comparison, logical/bit/shift/atomic and control/system operations in -**8,501,832 logical cases** across **348 xUnit batches**, across seven models and the A1200 profile. +**8,607,816 logical cases** across **356 xUnit batches**, across seven models and the A1200 profile. Ordinary `dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release` includes these batches. CI additionally validates every required report and exact count; missing reports, mismatches, unsupported execution and empty groups fail the gate. The following instruction sentinel verifies extension consumption. +The `logical-invalid-operands` group covers 207 assigned illegal opcode words +per profile: ORI/ANDI/EORI destinations and static/dynamic bit-operation +destinations, across both stacks and all 32 CCR inputs (13,248 cases per profile). +It checks preserved registers/CCR, saved fault PC/SR, exception frames and memory +canaries. CCR/SR immediate forms, dynamic BTST's legal immediate source and MOVEP +encodings remain in their legal groups. Unassigned mode-7 registers 5..7 are +outside this added matrix; their inclusion in a reference ILLEGAL directory does +not by itself establish complete illegal-opcode qualification. + The additional 040-only `system-mmu-disabled` group qualifies single-word PFLUSH/PTEST decoding, privilege, CCR preservation and trace behavior with translation disabled. MMUSR is undefined for disabled-MMU PTEST and is not diff --git a/Copper68k.Tests/Synthetic/SyntheticInvalidLogicalTests.cs b/Copper68k.Tests/Synthetic/SyntheticInvalidLogicalTests.cs new file mode 100644 index 00000000..f5c4fcbd --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticInvalidLogicalTests.cs @@ -0,0 +1,83 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticInvalidLogicalTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void InvalidImmediateAndBitDestinationsTrapWithoutOperandEffects(string modelId) + { + // M68000PM: ANDI/EORI/ORI use data-alterable destinations, except their + // separately encoded CCR/SR forms. Static BTST excludes #data whereas + // dynamic BTST permits it; BCHG/BCLR/BSET require data-alterable EAs. + // Unassigned mode-7 register encodings (5..7) are outside this matrix. + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "logical-invalid-operands"); + var opcodes = InvalidOpcodes().ToArray(); + Assert.Equal(207, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Select(x => x.Opcode).Distinct().Count()); + Assert.Contains(opcodes, x => x.Opcode == 0x0008); + Assert.Contains(opcodes, x => x.Opcode == 0x083c); + Assert.DoesNotContain(opcodes, x => x.Opcode == 0x013c); // BTST D0,#data + Assert.DoesNotContain(opcodes, x => x.Opcode == 0x0108); // MOVEP + Assert.DoesNotContain(opcodes, x => x.Opcode == 0x003c); // ORI to CCR + Assert.DoesNotContain(opcodes, x => x.Opcode == 0x007c); // ORI to SR + + foreach (var (opcode, family, form) in opcodes) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + machine.Reset(ccr, supervisor); + // Detect a mistaken operand read/write as well as register changes. + var forbidden = new List(); + for (var reg = 0; reg < 8; reg++) + for (var offset = -8; offset < 8; offset++) + { + var address = machine.Model.Physical(unchecked(machine.Core.State.A[reg] + (uint)offset)); + machine.Bus.Initialize(address, 0xa5, 1); + forbidden.Add(address); + } + var expected = SyntheticExecution.Prepare(machine, [opcode, 0x0011, 0x81a5, 0x4e71]); + expected.ForbiddenOperandReads.UnionWith(forbidden); + SyntheticExecution.ExpectException(machine, expected, 4); + SyntheticExecution.Run(machine, expected, report, + $"{modelId}/{family}/invalid/{form}/op={opcode:X4}/super={supervisor}/ccr={ccr:X2}"); + } + report.Complete(output); + } + + private static IEnumerable<(ushort Opcode, string Family, string Form)> InvalidOpcodes() + { + foreach (var (encoding, family) in new[] { (0x0000, "ORI"), (0x0200, "ANDI"), (0x0a00, "EORI") }) + for (var size = 0; size < 3; size++) + foreach (var form in NonAlterableDestinations()) + { + if (size < 2 && form.Mode == 7 && form.Register == 4) continue; // CCR/SR + yield return ((ushort)(encoding | size << 6 | form.Mode << 3 | form.Register), + $"{family}.{new[] { "B", "W", "L" }[size]}", form.Id); + } + + foreach (var (operation, family) in new[] { (0, "BTST"), (1, "BCHG"), (2, "BCLR"), (3, "BSET") }) + foreach (var form in NonAlterableDestinations()) + { + if (operation == 0 && form.Mode == 7 && form.Register is 2 or 3) continue; // Static PC-relative BTST is legal. + yield return ((ushort)(0x0800 | operation << 6 | form.Mode << 3 | form.Register), $"{family}.static", form.Id); + } + + // Dynamic mode-1 encodings belong to MOVEP; do not reinterpret them as + // illegal bit operations. Dynamic BTST has no invalid assigned mode-7 EA. + foreach (var (operation, family) in new[] { (1, "BCHG"), (2, "BCLR"), (3, "BSET") }) + for (var source = 0; source < 8; source++) + for (var destination = 2; destination <= 4; destination++) + yield return ((ushort)(0x0100 | source << 9 | operation << 6 | 7 << 3 | destination), + $"{family}.dynamic.D{source}", new OperandForm(7, destination).Id); + } + + private static IEnumerable NonAlterableDestinations() + { + for (var reg = 0; reg < 8; reg++) yield return new(1, reg); + for (var reg = 2; reg <= 4; reg++) yield return new(7, reg); + } +} diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index 599cc709..05f7e292 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -798,6 +798,25 @@ private static M68020OpcodeKind ClassifyBaseOpcode(ushort opcode) return M68020OpcodeKind.Movep; } + // M68000PM: these families have assigned illegal operand forms. + // Classify them before any operand fetch or fallback can have effects. + // CCR/SR forms were handled above; dynamic mode-1 words are MOVEP. + var logicalMode = (opcode >> 3) & 7; + var logicalRegister = opcode & 7; + if ((opcode & 0xFF00) is 0x0000 or 0x0200 or 0x0A00 && + ((opcode >> 6) & 3) < 3 && + (logicalMode == 1 || logicalMode == 7 && logicalRegister > 1)) + return M68020OpcodeKind.IllegalInstruction; + + if ((opcode & 0xFF00) == 0x0800 || (opcode & 0xF100) == 0x0100) + { + var maximumRegister = ((opcode >> 6) & 3) == 0 + ? (opcode & 0x100) != 0 ? 4 : 3 + : 1; + if (logicalMode == 1 || logicalMode == 7 && logicalRegister > maximumRegister) + return M68020OpcodeKind.IllegalInstruction; + } + if ((opcode & 0xF9C0) is 0x00C0 or 0x02C0 or 0x04C0) { var mode = (opcode >> 3) & 7; diff --git a/Copper68k/M68kCore.cs b/Copper68k/M68kCore.cs index c959e275..ddf0124b 100644 --- a/Copper68k/M68kCore.cs +++ b/Copper68k/M68kCore.cs @@ -10016,15 +10016,6 @@ private bool DecodeLine0(ushort opcode, uint instructionPc) var bitReg = opcode & 7; if (!IsDataAlterableEffectiveAddress(bitMode, bitReg)) { - if (bitMode == 7 && bitReg == 4 && operation == 0) - { - var immediateValue = FetchWord(); - var immediateMask = 1u << (int)(bit & 7); - State.SetFlag(M68kCpuState.Zero, (immediateValue & immediateMask) == 0); - AddInstructionCycles(8); - return true; - } - RaiseException(4, instructionPc, 34); return true; } @@ -10992,15 +10983,6 @@ private bool TryDecodeImmediateBtst(ushort opcode) } var bit = FetchWord() & 31; - if (mode == 7 && reg == 4) - { - var immediateValue = FetchWord(); - var immediateBit = bit & 7; - State.SetFlag(M68kCpuState.Zero, (immediateValue & (1u << immediateBit)) == 0); - AddInstructionCycles(8); - return true; - } - if (mode == 7 && reg == 1) { var address = FetchLong(); @@ -11026,7 +11008,7 @@ private static bool IsValidImmediateBtstEa(int mode, int reg) { 0 => true, 2 or 3 or 4 or 5 or 6 => true, - 7 => reg <= 4, + 7 => reg <= 3, _ => false }; } diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 4a52c06f..45dad04d 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -715,3 +715,72 @@ the explicit missing-validation-export diagnostic. The final classifier report i the stronger audit above. Preparation, PowerShell syntax and Git whitespace checks pass. This follow-up changes only test tooling/documentation; existing .39 consumer results remain prior evidence, without a new package or replay claim. + +## Assigned illegal logical operands — 2026-10-05 + +The pinned ILLEGAL fixtures exposed opcode `083C` (static BTST with an immediate +destination) and `0008` (ORI.B to A0). Independent authority is +[M68000PM](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf), especially +BTST 4-62/63 and the data-alterable tables for ANDI, EORI and ORI. Static BTST +excludes an immediate destination; dynamic BTST permits it. The distinction also +shows that the old `ImmediateBtstCanTestImmediateOperand` expectation was wrong. +That regression is corrected and retained; it is not retired as redundant. + +The shared `logical-invalid-operands` matrix independently enumerates 207 +assigned illegal logical/bit opcode words per profile, covering both stacks and +all 32 CCR states. Fixed encoding controls protect legal dynamic BTST, MOVEP and +CCR/SR forms from accidental inclusion. Registers, CCR, memory canaries, operand +reads, handler PC and saved SR/PC/frame contents are checked using the existing +common verifier. Unassigned mode-7 registers 5..7 remain outside this new matrix. +The legal addressing/bit/transfer gates separately protect legal neighboring +encodings; the implementation preserves their execution and timing policy. + +Before correction, 000/010/040 each fail 64 static-BTST cases, while 020/EC020/030/ +060/A1200 each report 13,248 unsupported executions. The corrected matrix passes +all **105,984 cases**. Advanced dispatch now maps assigned illegal forms to its +existing vector-4 path before operand effects; the 000 decoder removes the +incorrect static-immediate special case. No instruction is caught and retried. +The 64-batch focused legal/invalid check also passes without skips. The expanded +deterministic gate has **8,607,816 logical cases in 356 batches**. Missing the new +report group is explicitly rejected. + +Final ordinary Release validation passes **4,660 CPU tests**, with nine optional/ +opt-in skips and zero failures. The first full run's only failure was the stale +static-BTST expectation corrected above; the final complete run passes it. The +package/source/assembly identity snapshot is +`artifacts/m6-invalid-logical-package.json` (built from this checkpoint's working +changes before commit); no published version is changed. + +Fresh pinned references pass 312,500 SingleStepTests cases in 125 verified files +(TAS/TRAPV remain explicit exclusions) and 538 Musashi program/profile cases +(86 explicit exclusions). WinUAE still fails: **1,304 passing / 62 mismatching / +15 unsupported / zero untested groups**, over 11,136,661 callbacks, 1,373,785 +frame checks and 199,327 masked-SR cases. All 32 comparator controls pass. +It now reaches later ILLEGAL fixtures: `0C3C` on 000/010, `0408` on advanced +profiles and `0AC0` on 040. These require separate immediate-arithmetic/atomic +encoding and adapter review. The unchanged group totals do not mean unchanged +case coverage; the earlier failures are now passed before these later stops. +The pinned saved-PC and stack-mapping disagreements remain unresolved. + +Private **unpublished** NuGet `1.5.2-synthetic-dev.40` has SHA-256 +`e83bfe8d1a87004b4ee671f8b43f82e4b0bd45cb30bf94eb574d264ffcefedd3`. +The isolated CopperScreen baseline `d9beae8` resolves that exact version in +production and separate diagnostic assets. Its loaded CPU assembly hashes match +the tested CPU assembly. Release builds with zero warnings/errors; host 149, +disk 74, diagnostics 1,080 and all three native Workbench/A1200 boot/persistence +cases pass. Six optional host/media skips remain unavailable coverage; the +three requested native cases execute without skips. AHX also passes 18 tests. +These are correctness replays, not throughput/physical-timing qualification. + +Evidence: `artifacts/m6-invalid-logical-before/`, +`artifacts/m6-invalid-logical-after/`, `artifacts/m6-invalid-logical-cpu/`, +`artifacts/m6-invalid-logical-final/`, `artifacts/m6-invalid-logical-winuae/`, +`artifacts/m6-invalid-logical-singlestep/`, `artifacts/m6-invalid-logical-musashi/`, +`artifacts/m6-invalid-logical-missing-group/`, +`artifacts/m6-invalid-logical-ahx-results/` and +`artifacts/synthetic-private-feed-40/`; isolated consumer +`artifacts/invalid-logical-validation/`, +`artifacts/invalid-logical-diagnostic-tests/` and +`artifacts/invalid-logical-production.binlog`. +No package is published and no old test is retired. Milestone 6 remains in progress +for the unresolved reference and restoration qualification requirements above. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 897a659d..51d34a6f 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -666,3 +666,29 @@ Old frame-skipping bridges are rejected. Test-only changes preserve the producti CPU, timing policy, package/API and prior consumer evidence. No release or regression retirement is included. The reference document records the scoped evidence and remaining generator/adapter/CPU classification work. + +The assigned-invalid logical follow-up adds 105,984 public-factory cases +(13,248 per profile): 207 independently encoded illegal ORI/ANDI/EORI and bit +destination words, both stacks and all 32 CCR states. It distinguishes static +BTST's illegal immediate destination from legal dynamic BTST, MOVEP and CCR/SR +forms. Unassigned EA register encodings remain outside this added matrix. +The expanded deterministic gate is 8,607,816 logical cases in 356 batches, with +missing new reports rejected. Before correction, 000/010/040 each fail 64 cases; +the other five profiles each report 13,248 unsupported executions. All new cases +pass after correcting static BTST and advanced illegal-form dispatch. A stale +regression expectation is corrected from M68000PM and retained. + +The fresh pinned references retain 312,500 passing SingleStepTests cases and +538 Musashi program/profile passes with their explicit exclusions. WinUAE now +gets past the original invalid encodings and reaches later immediate-arithmetic/ +atomic fixtures; 62 mismatching and 15 unsupported groups remain gate failures. +All 32 comparator controls pass. Private unpublished .40 validates the isolated +CopperScreen NuGet boundary, Release build, host/disk/engine diagnostics and all +three native boot/persistence cases. No package release or test retirement is +included. The reference document records pins, before/after counts, exact scope +and remaining work. Milestone 6 remains in progress. + +Final validation of this slice passes 4,660 ordinary CPU tests with nine optional +skips and zero failures, all 8,607,816 deterministic cases, 18 AHX tests and the +scoped external/consumer replays above. The private .40 package identity and source/ +assembly hashes are recorded. No seeded run or physical qualification is added. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index ebab376b..d67d5fa2 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -79,6 +79,7 @@ try { 'logical-bitfield-values'=155136; 'logical-bits'=$(if ($model -in @('68000','68010')) {104192} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {104852}); 'logical-boundaries'=31808; + 'logical-invalid-operands'=13248; 'logical-cas'=$(if ($model -in @('68000','68010')) {36975} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {37371}); 'logical-cas2'=36864; 'logical-shifts'=$(if ($model -in @('68000','68010')) {130561} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {131089}); @@ -193,7 +194,7 @@ try { } $inventory | Add-Member -NotePropertyName qualificationBoundaries -NotePropertyValue @('RTE fault restart/internal formats remain untested: 010 long/non-MOVE/foreign frame8 restart, 020/030 frames9/A/B, 040 frame7', 'BKPT external replacement responder is unavailable', 'MOVES physical function-code spaces and LPSTOP CPU-space broadcast are unqualified', 'CALLM/RTM type1 protocol uses an internal synthetic responder; normal public buses have none') $inventory | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'qualified-inventory.json') - @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=348; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; m68040DisabledMmuInstructionGate='passing'; m68000AddressErrorDoubleFaultGate='passing'; m68010Format8StructureGate='passing'; m68010WordMoveRestartGate='passing'; roadmapComplete=$false; + @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=356; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; m68040DisabledMmuInstructionGate='passing'; m68000AddressErrorDoubleFaultGate='passing'; m68010Format8StructureGate='passing'; m68010WordMoveRestartGate='passing'; roadmapComplete=$false; seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; From fa09c0caee153b76581569c5f93f387755183ba6 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 00:55:35 +0300 Subject: [PATCH 013/211] Qualify illegal arithmetic and CAS operands --- Copper68k.Tests/M68kMusashiModelAuditTests.cs | 2 + .../Synthetic/InvalidOperandScenario.cs | 25 ++++++ Copper68k.Tests/Synthetic/README.md | 9 +- .../SyntheticInvalidArithmeticAtomicTests.cs | 81 ++++++++++++++++++ .../Synthetic/SyntheticInvalidLogicalTests.cs | 18 +--- Copper68k/M68kAdvancedTimingInterpreter.cs | 13 ++- Copper68k/M68kOpcodePlan.cs | 5 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 85 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 25 ++++++ scripts/test-copper68k-synthetic.ps1 | 4 +- 10 files changed, 245 insertions(+), 22 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/InvalidOperandScenario.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticInvalidArithmeticAtomicTests.cs diff --git a/Copper68k.Tests/M68kMusashiModelAuditTests.cs b/Copper68k.Tests/M68kMusashiModelAuditTests.cs index a4f7991a..366ea712 100644 --- a/Copper68k.Tests/M68kMusashiModelAuditTests.cs +++ b/Copper68k.Tests/M68kMusashiModelAuditTests.cs @@ -68,6 +68,8 @@ public void MusashiIntegerProgramsAcrossSelectedModelsWhenEnabled() if (directory == "mc68000") { if (KnownIncompatiblePrograms.TryGetValue(name, out var reason)) return reason; + if (!model.FullIndex && name == "move.bin") + return "Despite its directory, this program encodes 020-only PC-relative CMPI.B (0C3A) at offset 0x160; 000/010 correctly raise vector 4, and the fixture supplies no compatible handler. M68000PM 4-80."; if (model.Id == "68060" && name == "movep.bin") return "060 hardware raises vector 61; this program assumes hardware MOVEP execution without a software handler."; } else diff --git a/Copper68k.Tests/Synthetic/InvalidOperandScenario.cs b/Copper68k.Tests/Synthetic/InvalidOperandScenario.cs new file mode 100644 index 00000000..e1e7174f --- /dev/null +++ b/Copper68k.Tests/Synthetic/InvalidOperandScenario.cs @@ -0,0 +1,25 @@ +namespace Copper68k.Tests.Synthetic; + +internal static class InvalidOperandScenario +{ + public static void Run(SyntheticMachine machine, CoverageBatch report, ushort opcode, string description, + bool supervisor, int ccr) + { + machine.Reset(ccr, supervisor); + // Fixture initialization is separate from CPU accesses. Canaries detect + // a mistaken operand read/write in addition to register/CCR changes. + var forbidden = new List(); + for (var reg = 0; reg < 8; reg++) + for (var offset = -8; offset < 8; offset++) + { + var address = machine.Model.Physical(unchecked(machine.Core.State.A[reg] + (uint)offset)); + machine.Bus.Initialize(address, 0xa5, 1); + forbidden.Add(address); + } + var expected = SyntheticExecution.Prepare(machine, [opcode, 0x0011, 0x81a5, 0x4e71]); + expected.ForbiddenOperandReads.UnionWith(forbidden); + SyntheticExecution.ExpectException(machine, expected, 4); + SyntheticExecution.Run(machine, expected, report, + $"{machine.Model.Id}/{description}/op={opcode:X4}/super={supervisor}/ccr={ccr:X2}"); + } +} diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 67dca6e2..acd25541 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -16,7 +16,7 @@ From the CopperMod root, run: Milestones 1–5 execute MOVE/MOVEA, transfer/address, arithmetic/comparison, logical/bit/shift/atomic and control/system operations in -**8,607,816 logical cases** across **356 xUnit batches**, across seven models and the A1200 profile. +**8,683,848 logical cases** across **372 xUnit batches**, across seven models and the A1200 profile. Ordinary `dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release` includes these batches. CI additionally validates every required report and exact count; missing reports, mismatches, unsupported execution and empty groups fail the gate. @@ -31,6 +31,13 @@ encodings remain in their legal groups. Unassigned mode-7 registers 5..7 are outside this added matrix; their inclusion in a reference ILLEGAL directory does not by itself establish complete illegal-opcode qualification. +The same test-internal `InvalidOperandScenario` now drives immediate-arithmetic +and CAS invalid forms. `arithmetic-invalid-operands` covers 99 words on 000/010 +(6,336 cases per profile), or 93 on 020+ (5,952 cases), preserving legal +PC-relative CMPI on 020+. `logical-cas-invalid-operands` covers 54 words and +3,456 cases per profile. CAS2.W/.L have separate legal coverage; the unassigned +byte-CAS2 word and unassigned EA registers are outside these added matrices. + The additional 040-only `system-mmu-disabled` group qualifies single-word PFLUSH/PTEST decoding, privilege, CCR preservation and trace behavior with translation disabled. MMUSR is undefined for disabled-MMU PTEST and is not diff --git a/Copper68k.Tests/Synthetic/SyntheticInvalidArithmeticAtomicTests.cs b/Copper68k.Tests/Synthetic/SyntheticInvalidArithmeticAtomicTests.cs new file mode 100644 index 00000000..5deac7e7 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticInvalidArithmeticAtomicTests.cs @@ -0,0 +1,81 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticInvalidArithmeticAtomicTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void InvalidImmediateArithmeticDestinationsTrapBeforeOperandEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "arithmetic-invalid-operands"); + // M68000PM 4-10, 4-80, 4-180: ADDI/SUBI destinations are data + // alterable. CMPI excludes An/#data, but gained PC-relative EAs on 020. + // These are assigned operand encodings, not unassigned mode-7 registers. + var opcodes = ArithmeticOpcodes(machine.Model.FullIndex).ToArray(); + Assert.Equal(machine.Model.FullIndex ? 93 : 99, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Select(x => x.Opcode).Distinct().Count()); + Assert.Contains(opcodes, x => x.Opcode == 0x0408); // SUBI.B to A0 + Assert.Contains(opcodes, x => x.Opcode == 0x0c3c); // CMPI.B #data,#data + Assert.DoesNotContain(opcodes, x => x.Opcode == 0x0c00); // CMPI.B to D0 + Assert.Equal(!machine.Model.FullIndex, opcodes.Any(x => x.Opcode == 0x0c3a)); + Assert.Equal(!machine.Model.FullIndex, opcodes.Any(x => x.Opcode == 0x0c3b)); + + foreach (var (opcode, family, form) in opcodes) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, $"{family}/invalid/{form}", supervisor, ccr); + report.Complete(output); + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void InvalidCasDestinationsTrapBeforeExtensionOrOperandEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "logical-cas-invalid-operands"); + // M68000PM 4-67: CAS requires memory-alterable EAs. Mode 7/register + // 4 words encode CAS2.W/.L separately; its byte word is unassigned. + // All three are outside this assigned CAS-operand matrix. + var opcodes = CasOpcodes().ToArray(); + Assert.Equal(54, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Select(x => x.Opcode).Distinct().Count()); + Assert.Contains(opcodes, x => x.Opcode == 0x0ac0); // CAS.B D0 + Assert.DoesNotContain(opcodes, x => x.Opcode is 0x0afc or 0x0cfc or 0x0efc); + Assert.DoesNotContain(opcodes, x => x.Opcode == 0x0ad0); // CAS.B (A0) + + foreach (var (opcode, family, form) in opcodes) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, $"{family}/invalid/{form}", supervisor, ccr); + report.Complete(output); + } + + private static IEnumerable<(ushort Opcode, string Family, string Form)> ArithmeticOpcodes(bool pcRelativeCmpi) + { + foreach (var (encoding, family) in new[] { (0x0400, "SUBI"), (0x0600, "ADDI"), (0x0c00, "CMPI") }) + for (var size = 0; size < 3; size++) + foreach (var form in NonAlterableDestinations()) + { + if (family == "CMPI" && pcRelativeCmpi && form.Mode == 7 && form.Register is 2 or 3) continue; + yield return ((ushort)(encoding | size << 6 | form.Mode << 3 | form.Register), + $"{family}.{new[] { "B", "W", "L" }[size]}", form.Id); + } + } + + private static IEnumerable<(ushort Opcode, string Family, string Form)> CasOpcodes() + { + for (var size = 0; size < 3; size++) + foreach (var form in Enumerable.Range(0, 8).Select(r => new OperandForm(0, r)) + .Concat(NonAlterableDestinations().Where(f => f.Mode != 7 || f.Register != 4))) + yield return ((ushort)((size == 0 ? 0x0ac0 : size == 1 ? 0x0cc0 : 0x0ec0) | form.Mode << 3 | form.Register), + $"CAS.{new[] { "B", "W", "L" }[size]}", form.Id); + } + + private static IEnumerable NonAlterableDestinations() + { + for (var reg = 0; reg < 8; reg++) yield return new(1, reg); + for (var reg = 2; reg <= 4; reg++) yield return new(7, reg); + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticInvalidLogicalTests.cs b/Copper68k.Tests/Synthetic/SyntheticInvalidLogicalTests.cs index f5c4fcbd..c6fa2d46 100644 --- a/Copper68k.Tests/Synthetic/SyntheticInvalidLogicalTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticInvalidLogicalTests.cs @@ -28,23 +28,7 @@ public void InvalidImmediateAndBitDestinationsTrapWithoutOperandEffects(string m foreach (var (opcode, family, form) in opcodes) foreach (var supervisor in new[] { false, true }) for (var ccr = 0; ccr < 32; ccr++) - { - machine.Reset(ccr, supervisor); - // Detect a mistaken operand read/write as well as register changes. - var forbidden = new List(); - for (var reg = 0; reg < 8; reg++) - for (var offset = -8; offset < 8; offset++) - { - var address = machine.Model.Physical(unchecked(machine.Core.State.A[reg] + (uint)offset)); - machine.Bus.Initialize(address, 0xa5, 1); - forbidden.Add(address); - } - var expected = SyntheticExecution.Prepare(machine, [opcode, 0x0011, 0x81a5, 0x4e71]); - expected.ForbiddenOperandReads.UnionWith(forbidden); - SyntheticExecution.ExpectException(machine, expected, 4); - SyntheticExecution.Run(machine, expected, report, - $"{modelId}/{family}/invalid/{form}/op={opcode:X4}/super={supervisor}/ccr={ccr:X2}"); - } + InvalidOperandScenario.Run(machine, report, opcode, $"{family}/invalid/{form}", supervisor, ccr); report.Complete(output); } diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index 05f7e292..6a7c3cfe 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -808,6 +808,12 @@ private static M68020OpcodeKind ClassifyBaseOpcode(ushort opcode) (logicalMode == 1 || logicalMode == 7 && logicalRegister > 1)) return M68020OpcodeKind.IllegalInstruction; + if ((opcode & 0xFF00) is 0x0400 or 0x0600 or 0x0C00 && + ((opcode >> 6) & 3) < 3 && + (logicalMode == 1 || logicalMode == 7 && + logicalRegister > ((opcode & 0xFF00) == 0x0C00 ? 3 : 1))) + return M68020OpcodeKind.IllegalInstruction; + if ((opcode & 0xFF00) == 0x0800 || (opcode & 0xF100) == 0x0100) { var maximumRegister = ((opcode >> 6) & 3) == 0 @@ -2602,7 +2608,12 @@ private static M68020OpcodeKind ClassifyBaseOpcode(ushort opcode) if ((opcode & 0xFFC0) is 0x0AC0 or 0x0CC0 or 0x0EC0) { - return M68020OpcodeKind.Cas; + // CAS2.W/.L were handled above. CAS itself requires a memory + // alterable EA; never resolve a register or PC-relative form. + var mode = (opcode >> 3) & 7; + var register = opcode & 7; + return mode is >= 2 and <= 6 || mode == 7 && register <= 1 + ? M68020OpcodeKind.Cas : M68020OpcodeKind.IllegalInstruction; } if ((opcode & 0xF1C0) == 0x80C0) diff --git a/Copper68k/M68kOpcodePlan.cs b/Copper68k/M68kOpcodePlan.cs index 2e00c74c..1ca05f4a 100644 --- a/Copper68k/M68kOpcodePlan.cs +++ b/Copper68k/M68kOpcodePlan.cs @@ -885,10 +885,11 @@ private static bool TryCreateImmediateKind(ushort opcode, out M68kOpcodePlanKind }; var mode = (opcode >> 3) & 7; var register = opcode & 7; - var compareOnly = high == 0x0C00; + // This is the 000/010 plan inventory. CMPI does not write its EA, + // but still excludes immediate and PC-relative destinations here. if (mode == 1 || !IsSupportedReadableEa(mode, register, size) || - (!compareOnly && !IsSupportedWritableEa(mode, register, size))) + !IsSupportedWritableEa(mode, register, size)) { return false; } diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 45dad04d..e3d184e3 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -784,3 +784,88 @@ Evidence: `artifacts/m6-invalid-logical-before/`, `artifacts/invalid-logical-production.binlog`. No package is published and no old test is retired. Milestone 6 remains in progress for the unresolved reference and restoration qualification requirements above. + +## Assigned illegal arithmetic and CAS operands — 2026-10-05 + +The next pinned ILLEGAL failures were `0C3C` (CMPI.B with an immediate destination) +on 000/010, `0408` (SUBI.B to A0) on the advanced profiles, and `0AC0` (CAS.B D0) +on 040. Independent authority is M68000PM 4-10, 4-80 and 4-180 for immediate +arithmetic and 4-67 for CAS. CMPI excludes An/immediate destinations and gains +PC-relative forms on 020. CAS requires a memory-alterable destination; CAS2.W/.L +use separate words. The pinned WinUAE decoder tables corroborate these rules. + +The new immediate-arithmetic matrix covers 99 assigned illegal words on 000/010 +and 93 on 020+, both stacks and all 32 CCR states (48,384 cases). The CAS matrix +covers 54 words per profile under the same states (27,648 cases). Unassigned +mode-7 registers and the byte-CAS2 word remain outside this added matrix, with +legal word/long CAS2 retained in its existing gate. Fixed encoding controls protect +legal neighbors. These families reuse `InvalidOperandScenario`, extracted from +the preceding logical tests without changing that matrix's counts/expectations. +It uses the common register/CCR/memory/exception verifier and canaries; none of +its expectations call production decoders, EA, arithmetic or timing helpers. + +Before correction, immediate arithmetic reports 576 mismatches on each of 000/010 +and 5,952 unsupported executions on each of 020/EC020/030/060/A1200. Its existing +040 fallback already passes. CAS reports 3,456 mismatches per advanced profile +including 040; 000/010 already raise vector 4. This reproduces 21,888 mismatches +and 29,760 unsupported executions in the 76,032 new cases. + +The corrected 000/010 plan inventory applies the legal destination constraint +even when CMPI does not write memory. Advanced dispatch maps invalid immediate +arithmetic and CAS EAs to the existing vector-4 path before operand effects. +Legal 020+ PC-relative CMPI, memory CAS and CAS2 retain their existing execution +and timing policy. No instruction is caught and retried. All new cases pass, +alongside the 64-batch affected legal/invalid gate. The complete deterministic +gate now validates **8,683,848 logical cases in 372 batches**; missing either new +report group is explicitly rejected. Ordinary Release CPU validation passes +**4,676 tests**, with nine optional/opt-in skips and zero failures. + +Fresh SingleStepTests retains 312,500 passing cases across 125 verified files, +with TAS/TRAPV explicit exclusions. Musashi initially exposes a fixture caveat: +`mc68000/move.bin` encodes 020-only PC-relative CMPI.B (`0C3A`) at offset `0x160`, +against a nearby code operand. The source's local-label compare assembled to a +PC-relative EA. M68000PM 4-80 and the pinned WinUAE table require vector 4 on +000/010, but the program supplies no compatible handler. Its SHA-256 remains +`6fd7762aabf3b57b54e9a7919ff1917dea1c52b6fc12723c0c468ae3fde6086e`. +Those two program/profile rows are now explicitly excluded; the unchanged input +remains required and runs on all six applicable profiles. Final Musashi coverage +is **536 passing / 88 excluded / zero mismatching**, in 624 rows. The synthetic +matrix qualifies the unavailable CMPI form's architectural trap instead of +changing the CPU to execute it. Other program exclusions remain unchanged. + +The fresh WinUAE audit passes all **34,880** selected 000 ILLEGAL cases. Its total +is **1,305 passing / 61 mismatching / 15 unsupported / zero untested groups**, +over 11,180,443 callbacks, 1,417,565 frame checks and 199,327 masked-SR cases. +All 32 comparator controls pass. It reaches later failures: `40C8` on 010 and +`0E00` on advanced profiles, requiring status-transfer and MOVES legality/privilege +ordering review. No WinUAE family is excluded to make this run pass. Generator +saved-PC, adapter stack mapping and other reference/restore disagreements remain +unresolved. Callback totals include partial groups, not exhaustive coverage. + +Private **unpublished** NuGet `1.5.2-synthetic-dev.41` has SHA-256 +`47bcfbc205a4d8de43b2955ff83bf43c4df293c51e0bf3914dfc8f753f5e856a`. +The source/assembly snapshot is `artifacts/m6-invalid-arithmetic-package.json`; +it records these changes built before commit. The isolated CopperScreen baseline +`d9beae8` resolves the exact package through NuGet in production and separate +diagnostic assets; both loaded DLLs match the tested CPU assembly hash. Release +builds with zero warnings/errors; host 149, disk 74, diagnostics 1,080 and all +three requested native Workbench/A1200 boot/persistence cases pass. Six optional +host/media skips remain unavailable coverage; none of the three native cases +is skipped. AHX also passes 18 tests. No throughput or physical timing claim. + +Evidence: `artifacts/m6-invalid-arithmetic-before/`, +`artifacts/m6-invalid-arithmetic-after/`, `artifacts/m6-invalid-arithmetic-cpu/`, +`artifacts/m6-invalid-arithmetic-winuae/`, +`artifacts/m6-invalid-arithmetic-references/`, +`artifacts/m6-invalid-arithmetic-reference-final/`, +`artifacts/m6-invalid-arithmetic-missing-arithmetic-invalid-operands/`, +`artifacts/m6-invalid-arithmetic-missing-logical-cas-invalid-operands/`, +`artifacts/m6-invalid-arithmetic-ahx-results/`, +`artifacts/synthetic-private-feed-41/`; isolated consumer +`artifacts/invalid-arithmetic-validation/`, +`artifacts/invalid-arithmetic-diagnostic-tests/` and +`artifacts/invalid-arithmetic-production.binlog`. +The full CPU run preceded the test-only Musashi exclusion adjustment; the final +17-batch targeted run validates that adjustment and all new matrix cases, with +the production assembly unchanged. No package publication, old-test retirement, +seeded run or hardware qualification is added. Milestone 6 remains in progress. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 51d34a6f..5285e508 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -692,3 +692,28 @@ Final validation of this slice passes 4,660 ordinary CPU tests with nine optiona skips and zero failures, all 8,607,816 deterministic cases, 18 AHX tests and the scoped external/consumer replays above. The private .40 package identity and source/ assembly hashes are recorded. No seeded run or physical qualification is added. + +The next assigned-invalid follow-up adds 76,032 immediate-arithmetic/CAS cases +across all eight profiles, using a shared test-internal invalid-operand fixture. +CMPI's 020+ PC-relative forms and legal CAS2 words remain separate legal cases; +unassigned encodings are labeled outside the new matrices. The matrix reproduces +21,888 mismatches and 29,760 unsupported executions before correcting the 000/010 +CMPI plan constraint and advanced immediate/CAS legality dispatch. All new cases +pass, with successful operand ordering/timing policy unchanged and no partial retry. +The complete deterministic gate now has 8,683,848 logical cases in 372 batches, +and missing either new report group fails. Ordinary CPU validation passes 4,676 +tests, with nine optional skips and zero failures. + +Fresh SingleStepTests passes all 312,500 selected cases. Musashi discovers that +the `mc68000/move.bin` fixture itself contains a 020-only PC-relative CMPI and +has no compatible vector-4 handler. Those 000/010 rows are explicitly excluded +while the identical input executes on all six applicable profiles: 536 passing / +88 excluded, with the synthetic matrix qualifying the architectural trap. The +reference document records the source, exact word/offset/hash and manual rule. +WinUAE passes 34,880 selected 000 ILLEGAL cases, but still reports 61 mismatching +and 15 unsupported groups, now reaching status-transfer/MOVES invalid forms. +All 32 comparator controls pass. The private unpublished .41 package validates +isolated CopperScreen Release, host/disk/diagnostics and all three native replays; +AHX passes 18. A final targeted run validates the test-only exclusion adjustment +with the production assembly unchanged. No package release or test retirement +is included; milestone 6 remains in progress with its existing scope intact. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index d67d5fa2..157ea7ea 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -67,6 +67,7 @@ try { 'transfer-movep'=3708; 'transfer-movem'=$(if ($model -in @('68000','68010')) {3196} else {3592}); 'arithmetic-boundaries'=56448; 'arithmetic-addressing'=$(if ($model -in @('68000','68010')) {4667} else {8237}); 'arithmetic-scenarios'=21072; 'arithmetic-extend'=25440; + 'arithmetic-invalid-operands'=$(if ($model -in @('68000','68010')) {6336} else {5952}); 'arithmetic-decimal'=$(if ($model -in @('68000','68010')) {111392} else {111458}); 'arithmetic-muldiv-boundaries'=26904; 'arithmetic-muldiv-addressing'=$(if ($model -in @('68000','68010')) {5608} elseif ($model -eq '68060') {7208} else {7224}); @@ -80,6 +81,7 @@ try { 'logical-bits'=$(if ($model -in @('68000','68010')) {104192} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {104852}); 'logical-boundaries'=31808; 'logical-invalid-operands'=13248; + 'logical-cas-invalid-operands'=3456; 'logical-cas'=$(if ($model -in @('68000','68010')) {36975} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {37371}); 'logical-cas2'=36864; 'logical-shifts'=$(if ($model -in @('68000','68010')) {130561} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {131089}); @@ -194,7 +196,7 @@ try { } $inventory | Add-Member -NotePropertyName qualificationBoundaries -NotePropertyValue @('RTE fault restart/internal formats remain untested: 010 long/non-MOVE/foreign frame8 restart, 020/030 frames9/A/B, 040 frame7', 'BKPT external replacement responder is unavailable', 'MOVES physical function-code spaces and LPSTOP CPU-space broadcast are unqualified', 'CALLM/RTM type1 protocol uses an internal synthetic responder; normal public buses have none') $inventory | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'qualified-inventory.json') - @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=356; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; m68040DisabledMmuInstructionGate='passing'; m68000AddressErrorDoubleFaultGate='passing'; m68010Format8StructureGate='passing'; m68010WordMoveRestartGate='passing'; roadmapComplete=$false; + @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=372; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; m68040DisabledMmuInstructionGate='passing'; m68000AddressErrorDoubleFaultGate='passing'; m68010Format8StructureGate='passing'; m68010WordMoveRestartGate='passing'; roadmapComplete=$false; seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; From 424ad4e167d178c89b7dc89a6ad4f636444aba8c Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 01:11:14 +0300 Subject: [PATCH 014/211] Qualify illegal status and MOVES operands --- .../Synthetic/InvalidOperandScenario.cs | 4 +- Copper68k.Tests/Synthetic/README.md | 13 ++- .../Synthetic/SyntheticInvalidSystemTests.cs | 80 ++++++++++++++++++ Copper68k/M68040Support.cs | 4 +- Copper68k/M68kAdvancedTimingInterpreter.cs | 12 +++ Copper68k/M68kCore.cs | 16 +++- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 82 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 28 +++++++ scripts/test-copper68k-synthetic.ps1 | 9 +- 9 files changed, 239 insertions(+), 9 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticInvalidSystemTests.cs diff --git a/Copper68k.Tests/Synthetic/InvalidOperandScenario.cs b/Copper68k.Tests/Synthetic/InvalidOperandScenario.cs index e1e7174f..90f68435 100644 --- a/Copper68k.Tests/Synthetic/InvalidOperandScenario.cs +++ b/Copper68k.Tests/Synthetic/InvalidOperandScenario.cs @@ -3,7 +3,7 @@ namespace Copper68k.Tests.Synthetic; internal static class InvalidOperandScenario { public static void Run(SyntheticMachine machine, CoverageBatch report, ushort opcode, string description, - bool supervisor, int ccr) + bool supervisor, int ccr, ushort extensionWord = 0x0011) { machine.Reset(ccr, supervisor); // Fixture initialization is separate from CPU accesses. Canaries detect @@ -16,7 +16,7 @@ public static void Run(SyntheticMachine machine, CoverageBatch report, ushort op machine.Bus.Initialize(address, 0xa5, 1); forbidden.Add(address); } - var expected = SyntheticExecution.Prepare(machine, [opcode, 0x0011, 0x81a5, 0x4e71]); + var expected = SyntheticExecution.Prepare(machine, [opcode, extensionWord, 0x81a5, 0x4e71]); expected.ForbiddenOperandReads.UnionWith(forbidden); SyntheticExecution.ExpectException(machine, expected, 4); SyntheticExecution.Run(machine, expected, report, diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index acd25541..1ee23e97 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -16,7 +16,7 @@ From the CopperMod root, run: Milestones 1–5 execute MOVE/MOVEA, transfer/address, arithmetic/comparison, logical/bit/shift/atomic and control/system operations in -**8,683,848 logical cases** across **372 xUnit batches**, across seven models and the A1200 profile. +**8,761,672 logical cases** across **386 reporting xUnit batches**, across seven models and the A1200 profile. Ordinary `dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release` includes these batches. CI additionally validates every required report and exact count; missing reports, mismatches, unsupported execution and empty groups fail the gate. @@ -38,6 +38,17 @@ PC-relative CMPI on 020+. `logical-cas-invalid-operands` covers 54 words and 3,456 cases per profile. CAS2.W/.L have separate legal coverage; the unassigned byte-CAS2 word and unassigned EA registers are outside these added matrices. +`system-status-invalid-operands` covers 38 assigned illegal status-transfer +words (2,432 cases per profile). `system-moves-invalid-operands` covers 57 +illegal B/W/L operand words with valid load/store extensions (7,296 cases per +profile). Both reuse the canary fixture, both stacks and all 32 CCR inputs, +checking vector 4 before privilege or operand effects. Legal status/MOVES forms +remain in their existing groups; unassigned mode-7 registers and the CAS.L size +field are excluded from these matrices. Normal opcode prefetch is allowed. +The report summary derives its batch count from validated report identities +and counts instead of a hardcoded total. Nonreporting fixture/inventory checks +and opt-in audits are separate from these logical-case batches. + The additional 040-only `system-mmu-disabled` group qualifies single-word PFLUSH/PTEST decoding, privilege, CCR preservation and trace behavior with translation disabled. MMUSR is undefined for disabled-MMU PTEST and is not diff --git a/Copper68k.Tests/Synthetic/SyntheticInvalidSystemTests.cs b/Copper68k.Tests/Synthetic/SyntheticInvalidSystemTests.cs new file mode 100644 index 00000000..2da99f3c --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticInvalidSystemTests.cs @@ -0,0 +1,80 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticInvalidSystemTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void InvalidStatusOperandsTrapBeforePrivilegeAndOperandEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "system-status-invalid-operands"); + // M68000PM 4-122/124/125 and 6-18/20: from-SR/CCR require data + // alterable destinations; to-SR/CCR allow data sources, never An. + // Unassigned mode-7 registers 5..7 are outside this matrix. + var opcodes = StatusOpcodes().ToArray(); + Assert.Equal(38, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Select(x => x.Opcode).Distinct().Count()); + Assert.Contains(opcodes, x => x.Opcode == 0x40c8); + Assert.Contains(opcodes, x => x.Opcode == 0x42fc); + Assert.Contains(opcodes, x => x.Opcode == 0x46cf); + Assert.DoesNotContain(opcodes, x => x.Opcode is 0x40c0 or 0x42d0 or 0x44fa or 0x46fc); + + foreach (var (opcode, family, form) in opcodes) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, $"{family}/invalid/{form}", supervisor, ccr); + report.Complete(output); + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void InvalidMovesOperandsTrapBeforePrivilegeExtensionAndOperandEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "system-moves-invalid-operands"); + // M68000PM 6-25: B/W/L MOVES requires memory-alterable EAs. + // Size 3 encodes CAS.L separately; unassigned mode-7 registers are + // excluded. Both extension words are valid D0 load/store encodings. + var opcodes = MovesOpcodes().ToArray(); + Assert.Equal(57, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Select(x => x.Opcode).Distinct().Count()); + Assert.Contains(opcodes, x => x.Opcode == 0x0e00); + Assert.Contains(opcodes, x => x.Opcode == 0x0ebc); + Assert.DoesNotContain(opcodes, x => x.Opcode is 0x0e10 or 0x0e90 or 0x0ec0); + + foreach (var (opcode, family, form) in opcodes) + foreach (var store in new[] { false, true }) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, $"{family}/invalid/{form}/store={store}", + supervisor, ccr, store ? (ushort)0x0800 : (ushort)0x0000); + report.Complete(output); + } + + private static IEnumerable<(ushort Opcode, string Family, string Form)> StatusOpcodes() + { + foreach (var (encoding, family) in new[] { (0x40c0, "MOVEfromSR"), (0x42c0, "MOVEfromCCR"), + (0x44c0, "MOVEtoCCR"), (0x46c0, "MOVEtoSR") }) + foreach (var form in InvalidForms(false, family.StartsWith("MOVEfrom"))) + yield return ((ushort)(encoding | form.Mode << 3 | form.Register), family + ".W", form.Id); + } + + private static IEnumerable<(ushort Opcode, string Family, string Form)> MovesOpcodes() + { + for (var size = 0; size < 3; size++) + foreach (var form in InvalidForms(true, true)) + yield return ((ushort)(0x0e00 | size << 6 | form.Mode << 3 | form.Register), + $"MOVES.{new[] { "B", "W", "L" }[size]}", form.Id); + } + + private static IEnumerable InvalidForms(bool dataRegisters, bool pcAndImmediate) + { + if (dataRegisters) + for (var reg = 0; reg < 8; reg++) yield return new(0, reg); + for (var reg = 0; reg < 8; reg++) yield return new(1, reg); + if (pcAndImmediate) + for (var reg = 2; reg <= 4; reg++) yield return new(7, reg); + } +} diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index ac646da6..15750a4b 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -2581,7 +2581,9 @@ protected override bool TryExecuteModelSpecificInstruction(ushort opcode) { if (TryExecuteCacheMaintenance(opcode)) return true; - if ((opcode & 0xFF00) == 0x0E00 && (opcode & 0x00C0) != 0x00C0) + if ((opcode & 0xFF00) == 0x0E00 && (opcode & 0x00C0) != 0x00C0 && + ((opcode >> 3) & 7) >= 2 && + (((opcode >> 3) & 7) != 7 || (opcode & 7) <= 1)) { if ((State.StatusRegister & M68kCpuState.Supervisor) != 0) { diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index 6a7c3cfe..9089e5ce 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -734,6 +734,18 @@ private static M68020OpcodeKind ClassifyBaseOpcode(ushort opcode) return M68020OpcodeKind.ImmediateLogicalToStatusRegister; } + // Status transfers and MOVES have assigned illegal operand words. + // Decode legality before privilege checks or general-system fallback. + var systemMode = (opcode >> 3) & 7; + var systemRegister = opcode & 7; + if ((opcode & 0xFFC0) is 0x40C0 or 0x42C0 or 0x44C0 or 0x46C0 && + (systemMode == 1 || systemMode == 7 && + systemRegister > ((opcode & 0xFFC0) is 0x40C0 or 0x42C0 ? 1 : 4))) + return M68020OpcodeKind.IllegalInstruction; + if ((opcode & 0xFF00) == 0x0E00 && ((opcode >> 6) & 3) < 3 && + (systemMode < 2 || systemMode == 7 && systemRegister > 1)) + return M68020OpcodeKind.IllegalInstruction; + if ((opcode & 0xFFF8) == 0x40D0) { return M68020OpcodeKind.MoveStatusRegisterToAddressIndirect; diff --git a/Copper68k/M68kCore.cs b/Copper68k/M68kCore.cs index ddf0124b..52d4819c 100644 --- a/Copper68k/M68kCore.cs +++ b/Copper68k/M68kCore.cs @@ -9126,10 +9126,20 @@ protected bool TryExecuteM68010StatusAndReturn(ushort opcode, uint instructionPc AddInstructionCycles(16); return true; } - if ((opcode & 0xFFC0) == 0x40C0 && !State.GetFlag(M68kCpuState.Supervisor)) + if ((opcode & 0xFFC0) == 0x40C0) { - RaiseException(8, instructionPc, 34); - return true; + // An illegal operand never becomes a privileged instruction. + // Validate the 010 destination before testing supervisor state. + if (!IsDataAlterableEffectiveAddress((opcode >> 3) & 7, opcode & 7)) + { + RaiseException(4, instructionPc, 34); + return true; + } + if (!State.GetFlag(M68kCpuState.Supervisor)) + { + RaiseException(8, instructionPc, 34); + return true; + } } if ((opcode & 0xFFC0) != 0x42C0) return false; var mode = (opcode >> 3) & 7; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index e3d184e3..50d7b86e 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -869,3 +869,85 @@ The full CPU run preceded the test-only Musashi exclusion adjustment; the final 17-batch targeted run validates that adjustment and all new matrix cases, with the production assembly unchanged. No package publication, old-test retirement, seeded run or hardware qualification is added. Milestone 6 remains in progress. + +## Assigned illegal status-transfer and MOVES operands — 2026-10-05 + +The pinned ILLEGAL directory next exposed `40C8` (MOVE SR,A0) on 010 and +`0E00` (MOVES.B D0) on advanced profiles. M68000PM 4-122/124/125 and 6-18/20 +exclude address-register operands for status transfers; from-SR/CCR also exclude +PC-relative/immediate destinations. Section 6-25 requires memory-alterable +MOVES operands. The pinned decoder's assigned opcode tables and generated cases +corroborate vector 4 for these illegal forms, even in user state. Legal privileged +forms still raise vector 8 in user state. + +`system-status-invalid-operands` enumerates 38 assigned illegal words per profile: +11 each for MOVE from SR/CCR, eight each for MOVE to SR/CCR. Both stacks and all +32 CCR inputs give 19,456 cases across the eight profiles. The MOVES matrix +enumerates 57 B/W/L words with Dn, An and PC-relative/immediate operands, using +valid D0 load and store extensions, both stacks and all CCR inputs: 58,368 cases. +The shared canary fixture verifies registers, PC, defined SR, stack selection, +complete exception frames, preserved memory and absence of operand accesses. +Normal opcode prefetch is permitted. Unassigned mode-7 registers are outside +these matrices; MOVES size 3 is separate CAS.L encoding. Fixed reference examples +exclude legal status/MOVES neighbors, which remain in their existing gates. + +Before correction, the 010 status matrix has 352 mismatches. The 040 status +matrix has 704 unsupported executions, with 3,648 MOVES user-state mismatches. +The five other advanced profiles each have 2,432 unsupported status cases and +7,296 unsupported MOVES cases. Together, the new matrices detect **4,000 +mismatches and 49,344 unsupported executions** in 77,824 cases. Their corrected +results are all passing, alongside the 48-batch affected legal/invalid gate. + +The 010 path now validates MOVE-from-SR destinations before checking privilege. +Advanced dispatch classifies illegal status/MOVES operands through the existing +vector-4 path before effects. The 040 model-specific MOVES privilege path admits +only legal memory operands, so it cannot intercept illegal forms. Successful +execution ordering and the existing timing policy are preserved. No instruction +is caught and retried, and no package API changes. + +Ordinary Release CPU validation passes **4,692 tests**, with nine optional/opt-in +skips and zero failures. The deterministic report gate validates **8,761,672 +logical cases in 386 reporting batches**. Missing either new group, incorrect +report model identity and incorrect batch count are independently rejected. +The summary now derives batch totals from validated reports instead of a +hardcoded constant. The prior .41 checkpoint's summary stated 372 batches; its +required logical-case reports actually number 370. This fresh run adds 16. +Historical reports are retained unchanged; nonreporting checks and opt-in audits +are separate from the logical-case batch count. + +Fresh pinned SingleStepTests retains 312,500 passes in 125 files, with the same +TAS/TRAPV exclusions. Musashi retains 536 passes and 88 explicit exclusions +across eight profiles. AHX passes 18 tests. The WinUAE audit now passes all +**34,880** selected ILLEGAL callbacks on both 000 and 010. It still fails with +**1,306 passing / 60 mismatching / 15 unsupported / zero untested groups** over +11,242,795 callbacks, including 1,479,919 frame assertions and 199,327 masked-SR +cases. All 32 comparator controls pass. It reaches later `4008` (NEGX.B A0) +failures on EC020/020/030/060/A1200, and `4C08` (long multiply from A0) on 040. +These assigned invalid forms need broader unary/multiply operand qualification. +Reference saved-PC, adapter stack mapping and internal restoration gaps remain +open. No WinUAE family is excluded; totals include partial failing groups. + +Private **unpublished** NuGet `1.5.2-synthetic-dev.42` has SHA-256 +`553e3baa9d201e57b5bcbdacf469f051b359c74e23e096357944310e79258e08`. +`artifacts/m6-invalid-system-package.json` records source/package/assembly +identities built before commit. The isolated CopperScreen baseline `d9beae8` +resolves this exact NuGet package in production and separate diagnostics; both +loaded CPU DLLs match the tested assembly. Release build has zero warnings/errors; +host 149, disk 74, separate diagnostics 1,080 and all three requested native +Workbench/A1200 boot/persistence cases pass. Six optional host/media skips are +unavailable coverage; no requested native case is skipped. This is correctness +evidence, not throughput or physical timing qualification. + +Evidence: `artifacts/m6-invalid-system-before/`, +`artifacts/m6-invalid-system-after/`, `artifacts/m6-invalid-system-cpu/`, +`artifacts/m6-invalid-system-references/`, `artifacts/m6-invalid-system-winuae/`, +`artifacts/m6-invalid-system-guard-missing-status/`, +`artifacts/m6-invalid-system-guard-missing-moves/`, +`artifacts/m6-invalid-system-guard-wrong-model/`, +`artifacts/m6-invalid-system-guard-wrong-batches/`, +`artifacts/m6-invalid-system-ahx-results/`, `artifacts/synthetic-private-feed-42/`; +isolated consumer `artifacts/invalid-system-validation/`, +`artifacts/invalid-system-diagnostic-tests/`, +`artifacts/invalid-system-production.binlog`. No old test is retired, no package +is published and no new seeded or physical audit is claimed. Milestone 6 remains +in progress with the accepted scope unchanged. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 5285e508..1f031e5d 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -717,3 +717,31 @@ isolated CopperScreen Release, host/disk/diagnostics and all three native replay AHX passes 18. A final targeted run validates the test-only exclusion adjustment with the production assembly unchanged. No package release or test retirement is included; milestone 6 remains in progress with its existing scope intact. + +The status-transfer/MOVES follow-up adds 77,824 assigned-invalid cases across all +eight profiles, both stacks and all CCR inputs. The shared fixture proves vector +4, exact frames, preserved state/memory and absence of operand effects, including +valid MOVES load/store extensions. Unassigned encodings and legal CAS.L neighbors +remain separate. These cases detect 4,000 mismatches and 49,344 unsupported +executions before correcting 010 MOVE-from-SR privilege priority and advanced +status/MOVES legality dispatch. All new cases and the 48-batch affected gate pass. +Successful execution ordering/timing policy are unchanged; no partial retry. + +The fresh full CPU run passes 4,692 tests with nine optional skips and zero +failures. The deterministic gate validates 8,761,672 logical cases in 386 +reporting batches. Missing new reports and corrupt model/batch metadata fail. +Batch counts are now derived from validated reports; the prior hardcoded .41 +summary overstated its 370 required reporting batches by two. Historical evidence +remains unchanged, with this correction recorded in the reference document. + +WinUAE passes all 34,880 selected ILLEGAL callbacks on both 000 and 010. The +complete external audit still fails with 60 mismatching and 15 unsupported groups, +reaching later unary/multiply invalid operands. All 32 comparator controls pass. +Fresh SingleStepTests retains 312,500 passes and Musashi retains 536 passes / 88 +explicit exclusions; AHX passes 18. The private unpublished .42 package validates +the isolated CopperScreen NuGet boundary, Release build, host/disk/diagnostics and +all three native boot/persistence replays. Source/package/DLL identities, exact +before/after cases, exclusions and limitations are recorded in the reference +document. No release, regression retirement, seeded or physical qualification is +added. Milestone 6 remains in progress for the existing integer/reference and +restoration requirements. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 157ea7ea..756e9c82 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -58,6 +58,7 @@ try { Set-AuditEnvironment 'COPPER68K_SYNTHETIC_MODELS' ($Models -join ',') if (-not $ValidateReportsOnly) { Invoke-Tests $(if ($Deep) { 'Suite=Synthetic|Suite=SyntheticDeep' } else { 'Suite=Synthetic' }) 'deterministic' } $logicalCases = 0 + $logicalBatches = 0 foreach ($model in $knownModels) { $expected = @{'move-opcodes'=9726; 'move-values-ccr'=58368; 'move-invalid-operands'=2562; 'move-alignment'=12; 'move-extensions-aliases'=$(if ($model -in @('68000','68010')) {1756} else {7120}); @@ -92,10 +93,12 @@ try { 'system-model'=13120; 'system-movec'=2432; 'system-moves'=$(if ($model -in @('68000','68010')) {10656} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {11052}); + 'system-moves-invalid-operands'=7296; 'system-rte'=$(if ($model -in @('68000','68010','68060')) {2048} elseif ($model -in @('68020','68030','68EC020','A1200')) {1888} elseif ($model -in @('68040')) {2016}); 'system-rtm'=24594; 'system-stack'=6080; 'system-status'=$(if ($model -in @('68000','68010')) {16540} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {16936}); + 'system-status-invalid-operands'=2432; 'system-trace'=$(if ($model -in @('68000','68010','68060')) {1284} elseif ($model -in @('68020','68030','68040','68EC020','A1200')) {2568})} if ($model -eq '68000') { $expected['system-double-fault'] = 640 } if ($model -eq '68010') { @@ -111,9 +114,11 @@ try { if ($model -eq '68040') { $expected['system-mmu-disabled'] = 34850 } foreach ($group in $expected.Keys) { $report = Get-Content -LiteralPath (Join-Path $output "$model-$group.json") -Raw | ConvertFrom-Json - if ($report.logicalCases -ne $expected[$group] -or $report.counts.passing -ne $expected[$group] -or + if ($report.schema -ne 1 -or $report.model -cne $model -or $report.group -cne $group -or + $report.xunitBatches -ne 1 -or $report.logicalCases -ne $expected[$group] -or $report.counts.passing -ne $expected[$group] -or $report.counts.mismatching -ne 0 -or $report.counts.unsupported -ne 0 -or $report.counts.untested -ne 0) { throw "Incomplete gate: $model/$group" } $logicalCases += $report.logicalCases + $logicalBatches += $report.xunitBatches } } if ($Deep) { @@ -196,7 +201,7 @@ try { } $inventory | Add-Member -NotePropertyName qualificationBoundaries -NotePropertyValue @('RTE fault restart/internal formats remain untested: 010 long/non-MOVE/foreign frame8 restart, 020/030 frames9/A/B, 040 frame7', 'BKPT external replacement responder is unavailable', 'MOVES physical function-code spaces and LPSTOP CPU-space broadcast are unqualified', 'CALLM/RTM type1 protocol uses an internal synthetic responder; normal public buses have none') $inventory | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'qualified-inventory.json') - @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=372; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; m68040DisabledMmuInstructionGate='passing'; m68000AddressErrorDoubleFaultGate='passing'; m68010Format8StructureGate='passing'; m68010WordMoveRestartGate='passing'; roadmapComplete=$false; + @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=$logicalBatches; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; m68040DisabledMmuInstructionGate='passing'; m68000AddressErrorDoubleFaultGate='passing'; m68010Format8StructureGate='passing'; m68010WordMoveRestartGate='passing'; roadmapComplete=$false; seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; From 66d276e3fc1abd453d237697422bda8d6fa81127 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 01:43:37 +0300 Subject: [PATCH 015/211] Qualify invalid integer operands and 68060 debug instructions --- Copper68k.Tests/Synthetic/IntegerInventory.cs | 4 + .../SyntheticDebugInstructionTests.cs | 67 ++++++++++ .../SyntheticInvalidBoundsBitfieldTests.cs | 77 ++++++++++++ ...yntheticInvalidUnaryMultiplyDivideTests.cs | 119 ++++++++++++++++++ Copper68k/M68060Interpreter.cs | 31 +++++ Copper68k/M68kAdvancedTimingInterpreter.cs | 34 ++++- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 80 ++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 25 ++++ scripts/test-copper68k-synthetic.ps1 | 10 +- 9 files changed, 442 insertions(+), 5 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticDebugInstructionTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticInvalidBoundsBitfieldTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticInvalidUnaryMultiplyDivideTests.cs diff --git a/Copper68k.Tests/Synthetic/IntegerInventory.cs b/Copper68k.Tests/Synthetic/IntegerInventory.cs index 5ad4c9fb..4c190b57 100644 --- a/Copper68k.Tests/Synthetic/IntegerInventory.cs +++ b/Copper68k.Tests/Synthetic/IntegerInventory.cs @@ -89,6 +89,8 @@ private static IEnumerable Build() yield return new("MOVE16", 5, "16 bytes", "aligned line; model-defined register/absolute forms", "040+"); foreach (var name in new[] { "CINV", "CPUSH" }) yield return new(name, 5, "line,page,all", "instruction/data/both caches, privileged", "040+"); yield return new("LPSTOP", 5, "W", "immediate SR, privileged", "060"); + yield return new("HALT", 5, "none", "privileged debug halt; interrupts cannot restart", "060"); + yield return new("PULSE", 5, "none", "user/supervisor; integer state preserved; physical PST/debug commands unqualified", "060"); } } @@ -102,6 +104,8 @@ public void InventoryKeepsEveryPlannedIntegerFamilyVisible() Assert.Contains(IntegerInventory.All, s => s.Family == "MOVEA"); Assert.Contains(IntegerInventory.All, s => s.Family == "CALLM"); Assert.Contains(IntegerInventory.All, s => s.Family == "LPSTOP"); + Assert.Contains(IntegerInventory.All, s => s.Family == "HALT"); + Assert.Contains(IntegerInventory.All, s => s.Family == "PULSE"); var rows = from model in ModelSpec.All from spec in IntegerInventory.All select new { model = model.Id, model.Diagnostic, instruction = spec.Family, spec.Sizes, spec.OperandForms, spec.Milestone, outcome = spec.ArchitecturalOutcome(model), status = "untested", note = "Execution evidence is in named scenario batches; inventory membership is not a passing result." }; diff --git a/Copper68k.Tests/Synthetic/SyntheticDebugInstructionTests.cs b/Copper68k.Tests/Synthetic/SyntheticDebugInstructionTests.cs new file mode 100644 index 00000000..acb4363c --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticDebugInstructionTests.cs @@ -0,0 +1,67 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticDebugInstructionTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void DebugInstructionAliasesPrivilegeTraceAndHaltRecovery(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "system-debug-instructions"); + // MC68060UM 9.2.2 / 9-30: HALT is privileged; interrupts cannot + // restart it. PULSE is user-accessible and preserves integer state. + // Debug-port restart, pipeline toggling and PST pins are not exposed. + foreach (var opcode in new ushort[] { 0x4ac8, 0x4acc }) + foreach (var supervisor in new[] { false, true }) + foreach (var trace in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + machine.Reset(ccr, supervisor); + if (trace) machine.Core.State.StatusRegister |= 0x8000; + var expected = SyntheticExecution.Prepare(machine, [opcode]); + var halt = modelId == "68060" && opcode == 0x4ac8 && supervisor; + if (modelId != "68060") SyntheticExecution.ExpectException(machine, expected, 4); + else if (opcode == 0x4ac8 && !supervisor) SyntheticExecution.ExpectException(machine, expected, 8); + else if (halt) expected.Halted = true; + else if (trace) SyntheticExecution.ExpectException(machine, expected, 9, SyntheticMachine.Code + 2); + var id = $"{modelId}/{(opcode == 0x4ac8 ? "HALT" : "PULSE")}/super={supervisor}/trace={trace}/ccr={ccr:X2}"; + SyntheticExecution.Run(machine, expected, report, id + $"/op={opcode:X4}", !halt); + if (!halt) continue; + if (!machine.Core.State.Halted || machine.Core.State.ProgramCounter != expected.Pc) + { + foreach (var edge in new[] { "idle", "interrupt", "host-entry", "reset" }) + report.Record(id + $"/edge={edge}/op={opcode:X4}", "untested", "HALT prerequisite did not execute correctly"); + continue; + } + // Edge checks are additional named cases, not assertions counted as + // independent instruction combinations. Reset is the available + // public recovery operation; architectural debug restart is absent. + foreach (var edge in new[] { "idle", "interrupt", "host-entry", "reset" }) + { + try + { + var accessCount = machine.Bus.Accesses.Count; + if (edge == "idle") machine.Core.ExecuteInstruction(); + if (edge == "interrupt") + { machine.Core.RequestInterrupt(1, 100); machine.Core.RequestInterrupt(7, 124); } + if (edge == "host-entry") machine.Core.BeginSubroutine(0x1100, 0x7800, 0x1200); + string? mismatch; + if (edge == "reset") + { + machine.Reset(ccr, supervisor); + var recovered = SyntheticExecution.Prepare(machine, [0x4e71]); + machine.Core.ExecuteInstruction(); mismatch = recovered.Verify(machine); + } + else mismatch = expected.Verify(machine) ?? + (machine.Bus.Accesses.Count != accessCount ? "Halted CPU performed bus accesses" : null); + report.Record(id + $"/edge={edge}/op={opcode:X4}", mismatch == null ? "passing" : "mismatching", mismatch); + } + catch (Exception ex) { report.Record(id + $"/edge={edge}/op={opcode:X4}", "mismatching", ex.ToString()); } + } + } + report.Complete(output); + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticInvalidBoundsBitfieldTests.cs b/Copper68k.Tests/Synthetic/SyntheticInvalidBoundsBitfieldTests.cs new file mode 100644 index 00000000..f700c15e --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticInvalidBoundsBitfieldTests.cs @@ -0,0 +1,77 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticInvalidBoundsBitfieldTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void InvalidChkSourcesTrapWithoutBoundsOrFlagEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "system-chk-invalid-operands"); + // M68000PM 4-70: CHK.W/.L data sources exclude An. + var opcodes = ChkOpcodes().ToArray(); + Assert.Equal(128, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Select(x => x.Opcode).Distinct().Count()); + Assert.Contains(opcodes, x => x.Opcode == 0x4108); + Assert.Contains(opcodes, x => x.Opcode == 0x4f8f); + Assert.DoesNotContain(opcodes, x => x.Opcode is 0x4180 or 0x41bc or 0x41c8); + foreach (var (opcode, family, form) in opcodes) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, $"{family}/invalid/{form}", supervisor, ccr); + report.Complete(output); + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void InvalidBitfieldOperandsTrapBeforeExtensionOrOperandEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "logical-bitfield-invalid-operands"); + // M68000PM 4-31..52: Dn or control memory; no An/post/pre/#data. + // Read-only fields also allow PC-relative control EAs; mutating ones + // require alterable memory. Unassigned mode-7 registers are excluded. + var opcodes = BitfieldOpcodes().ToArray(); + Assert.Equal(208, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Select(x => x.Opcode).Distinct().Count()); + Assert.Contains(opcodes, x => x.Opcode == 0xe8c8); + Assert.Contains(opcodes, x => x.Opcode == 0xeafa); + Assert.Contains(opcodes, x => x.Opcode == 0xefdf); + Assert.DoesNotContain(opcodes, x => x.Opcode is 0xe8c0 or 0xe8d0 or 0xe8fa or 0xedfb); + foreach (var (opcode, family, form) in opcodes) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + // Immediate offset zero, width 32, source/destination D0 where used. + InvalidOperandScenario.Run(machine, report, opcode, $"{family}/invalid/{form}", supervisor, ccr, 0); + report.Complete(output); + } + + private static IEnumerable<(ushort Opcode, string Family, string Form)> ChkOpcodes() + { + foreach (var word in new[] { false, true }) + for (var destination = 0; destination < 8; destination++) + for (var source = 0; source < 8; source++) + yield return ((ushort)(0x4108 | (word ? 0x80 : 0) | destination << 9 | source), + word ? "CHK.W" : "CHK.L", $"A{source},D{destination}"); + } + + private static IEnumerable<(ushort Opcode, string Family, string Form)> BitfieldOpcodes() + { + var names = new[] { "BFTST", "BFEXTU", "BFCHG", "BFEXTS", "BFCLR", "BFFFO", "BFSET", "BFINS" }; + for (var operation = 0; operation < names.Length; operation++) + { + for (var mode = 1; mode <= 4; mode++) + { + if (mode == 2) continue; + for (var reg = 0; reg < 8; reg++) yield return Case(new(mode, reg)); + } + yield return Case(new(7, 4)); + if (operation is 2 or 4 or 6 or 7) + { yield return Case(new(7, 2)); yield return Case(new(7, 3)); } + (ushort, string, string) Case(OperandForm form) => + ((ushort)(0xe8c0 | operation << 8 | form.Mode << 3 | form.Register), names[operation], form.Id); + } + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticInvalidUnaryMultiplyDivideTests.cs b/Copper68k.Tests/Synthetic/SyntheticInvalidUnaryMultiplyDivideTests.cs new file mode 100644 index 00000000..a52f0498 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticInvalidUnaryMultiplyDivideTests.cs @@ -0,0 +1,119 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticInvalidUnaryMultiplyDivideTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void InvalidUnaryOperandsTrapWithoutOperandEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "logical-unary-invalid-operands"); + // M68000PM: CLR/NEG/NEGX/NOT/TAS/NBCD require data-alterable EAs. + // TST gained PC/immediate and word/long An sources on 020 (4-193). + // 020+ NBCD mode-1 words encode LINK.L instead (4-111). Size 3 and + // unassigned mode-7 registers are outside this matrix. + var opcodes = UnaryOpcodes(machine.Model.FullIndex, modelId == "68060").ToArray(); + Assert.Equal(modelId == "68060" ? 152 : machine.Model.FullIndex ? 154 : 187, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Select(x => x.Opcode).Distinct().Count()); + Assert.Contains(opcodes, x => x.Opcode == 0x4008); + Assert.Contains(opcodes, x => x.Opcode == 0x483c); + Assert.Equal(modelId != "68060", opcodes.Any(x => x.Opcode == 0x4ac8)); + Assert.Equal(modelId != "68060", opcodes.Any(x => x.Opcode == 0x4acc)); + Assert.Equal(!machine.Model.FullIndex, opcodes.Any(x => x.Opcode == 0x4a48)); + Assert.Equal(!machine.Model.FullIndex, opcodes.Any(x => x.Opcode == 0x4a3c)); + Assert.Equal(!machine.Model.FullIndex, opcodes.Any(x => x.Opcode == 0x4808)); + Assert.DoesNotContain(opcodes, x => x.Opcode is 0x4000 or 0x4810 or 0x4ad0 or 0x40c0); + + foreach (var (opcode, family, form) in opcodes) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, $"{family}/invalid/{form}", supervisor, ccr); + report.Complete(output); + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void InvalidWordMultiplyDivideSourcesTrapWithoutRegisterEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "arithmetic-muldiv-word-invalid-operands"); + // M68000PM 4-93/97/136/139: data sources exclude all An encodings. + var opcodes = WordOpcodes().ToArray(); + Assert.Equal(256, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Select(x => x.Opcode).Distinct().Count()); + Assert.Contains(opcodes, x => x.Opcode == 0x80c8); + Assert.Contains(opcodes, x => x.Opcode == 0xcfcf); + Assert.DoesNotContain(opcodes, x => x.Opcode is 0x80c0 or 0x81fc or 0xc0fa or 0xc1fb); + + foreach (var (opcode, family, form) in opcodes) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, $"{family}/invalid/{form}", supervisor, ccr); + report.Complete(output); + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void InvalidLongMultiplyDivideSourcesTrapBeforeExtensionSelectedOperations(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "arithmetic-muldiv-long-invalid-operands"); + // Long operations (020+) still exclude An (M68000PM 4-94/98/137/140). + // Valid signed/unsigned, 32/64-bit extensions cannot admit an invalid + // opcode EA, including 060's unavailable 64-bit operations. Reserved + // extension bits are outside this assigned-operand matrix. + var opcodes = LongOpcodes().ToArray(); + Assert.Equal(16, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Select(x => x.Opcode).Distinct().Count()); + Assert.Contains(opcodes, x => x.Opcode == 0x4c08); + Assert.Contains(opcodes, x => x.Opcode == 0x4c4f); + Assert.DoesNotContain(opcodes, x => x.Opcode is 0x4c00 or 0x4c7c); + + foreach (var (opcode, family, form) in opcodes) + foreach (var extension in new ushort[] { 0x0001, 0x0801, 0x1402, 0x1c02 }) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, $"{family}/invalid/{form}/ext={extension:X4}", + supervisor, ccr, extension); + report.Complete(output); + } + + private static IEnumerable<(ushort Opcode, string Family, string Form)> UnaryOpcodes(bool advancedTst, bool debug060) + { + foreach (var (encoding, family) in new[] { (0x4000, "NEGX"), (0x4200, "CLR"), + (0x4400, "NEG"), (0x4600, "NOT"), (0x4a00, "TST"), (0x4800, "NBCD"), (0x4ac0, "TAS") }) + for (var size = 0; size < (family is "NBCD" or "TAS" ? 1 : 3); size++) + foreach (var form in InvalidDestinations()) + { + if (family == "TST" && advancedTst && (form.Mode == 7 || size != 0)) continue; + if (family == "NBCD" && advancedTst && form.Mode == 1) continue; + // MC68060UM 9.2.2: HALT/PULSE reuse two TAS mode-1 words. + if (family == "TAS" && debug060 && form.Mode == 1 && form.Register is 0 or 4) continue; + yield return ((ushort)(encoding | size << 6 | form.Mode << 3 | form.Register), + $"{family}.{new[] { "B", "W", "L" }[size]}", form.Id); + } + } + + private static IEnumerable<(ushort Opcode, string Family, string Form)> WordOpcodes() + { + foreach (var (encoding, family) in new[] { (0x80c0, "DIVU"), (0x81c0, "DIVS"), + (0xc0c0, "MULU"), (0xc1c0, "MULS") }) + for (var destination = 0; destination < 8; destination++) + for (var source = 0; source < 8; source++) + yield return ((ushort)(encoding | destination << 9 | 0x08 | source), family + ".W", $"A{source},D{destination}"); + } + + private static IEnumerable<(ushort Opcode, string Family, string Form)> LongOpcodes() + { + foreach (var (encoding, family) in new[] { (0x4c00, "MULL"), (0x4c40, "DIVL") }) + for (var source = 0; source < 8; source++) + yield return ((ushort)(encoding | 0x08 | source), family + ".L", $"A{source}"); + } + + private static IEnumerable InvalidDestinations() + { + for (var reg = 0; reg < 8; reg++) yield return new(1, reg); + for (var reg = 2; reg <= 4; reg++) yield return new(7, reg); + } +} diff --git a/Copper68k/M68060Interpreter.cs b/Copper68k/M68060Interpreter.cs index 33172c0d..d5febf71 100644 --- a/Copper68k/M68060Interpreter.cs +++ b/Copper68k/M68060Interpreter.cs @@ -10,6 +10,8 @@ namespace Copper68k // it must never silently acquire the MC68040's different architecture. internal sealed class M68060Interpreter : M68kAdvancedTimingInterpreter { + private bool _debugInstructionHalted; + public M68060Interpreter(IM68kBus bus) : base(bus, M68020CpuProfile.Ocs68060Accelerator8x, new M68kCpuState(), enableM68020StackMode: false, hasModelSpecificInstructions: true, @@ -21,6 +23,7 @@ public M68060Interpreter(IM68kBus bus) public override void Reset(uint programCounter, uint stackPointer) { base.Reset(programCounter, stackPointer); + _debugInstructionHalted = false; State.EnableM68060StackMode(); State.M68060ProcessorConfiguration = 0x0430_0000; // First revision, ESS/DFP clear. State.M68060BusControl = 0; @@ -28,6 +31,27 @@ public override void Reset(uint programCounter, uint stackPointer) protected override bool TryExecuteModelSpecificInstruction(ushort opcode) { + if (opcode is 0x4AC8 or 0x4ACC) + { + // MC68060UM 9.2.2: these reuse TAS mode-1 words on earlier + // processors. PULSE changes no integer state; physical PST and + // debug-port pipeline commands are outside this bus boundary. + BeginInstruction(opcode); + var pc = State.ProgramCounter; + _ = FetchWord(); + if (opcode == 0x4AC8) + { + if (!State.GetFlag(M68kCpuState.Supervisor)) + { + RaiseFormat0Exception(8, pc, M68kInstructionTimingKey.PrivilegeViolation); + return true; + } + _debugInstructionHalted = true; + State.Halted = true; + } + CompleteTiming(M68kInstructionTimingKey.Nop); + return true; + } if (TryExecuteCacheMaintenance(opcode)) return true; if (opcode == 0x4E73) { Execute060Rte(); return true; } if ((opcode & 0xFFC0) is 0xF300 or 0xF340) return ExecuteFpuStateTransfer(opcode); @@ -233,6 +257,7 @@ internal override void RaiseFormat0Exception(int vector, uint pc, M68kInstructio public override void RequestInterrupt(int level, uint vectorAddress) { + if (_debugInstructionHalted) return; if (level <= 0 || level != 7 && level <= ((State.StatusRegister >> 8) & 7)) return; var sr = State.StatusRegister; var pc = State.ProgramCounter; @@ -247,6 +272,12 @@ public override void RequestInterrupt(int level, uint vectorAddress) CompleteTiming(M68kInstructionTimingKey.InterruptAcknowledge); } + public override void BeginSubroutine(uint address, uint stackPointer, uint returnAddress) + { + if (_debugInstructionHalted) return; + base.BeginSubroutine(address, stackPointer, returnAddress); + } + private M68kEmulationException Unavailable(string feature, ushort opcode) => new($"MC68060 experimental integer profile does not yet support {feature} (opcode ${opcode:X4} at ${State.ProgramCounter:X8})."); } diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index 9089e5ce..29c443c2 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -826,6 +826,21 @@ private static M68020OpcodeKind ClassifyBaseOpcode(ushort opcode) logicalRegister > ((opcode & 0xFF00) == 0x0C00 ? 3 : 1))) return M68020OpcodeKind.IllegalInstruction; + if ((opcode & 0xFF00) is 0x4000 or 0x4200 or 0x4400 or 0x4600 && + ((opcode >> 6) & 3) < 3 && + (logicalMode == 1 || logicalMode == 7 && logicalRegister > 1)) + return M68020OpcodeKind.IllegalInstruction; + if ((opcode & 0xFFC0) == 0x4AC0 && + (logicalMode == 1 || logicalMode == 7 && logicalRegister > 1)) + return M68020OpcodeKind.IllegalInstruction; + // NBCD mode-1 words are legal LINK.L on 020+. TST admits PC/#data + // and word/long An sources here, but byte An remains illegal. + if ((opcode & 0xFFC0) == 0x4800 && logicalMode == 7 && logicalRegister > 1 || + (opcode & 0xFFC0) == 0x4A00 && logicalMode == 1) + return M68020OpcodeKind.IllegalInstruction; + if ((opcode & 0xF140) == 0x4100 && logicalMode == 1) + return M68020OpcodeKind.IllegalInstruction; + if ((opcode & 0xFF00) == 0x0800 || (opcode & 0xF100) == 0x0100) { var maximumRegister = ((opcode >> 6) & 3) == 0 @@ -2610,7 +2625,12 @@ private static M68020OpcodeKind ClassifyBaseOpcode(ushort opcode) if ((opcode & 0xFFC0) is 0x4C00 or 0x4C40) { - return M68020OpcodeKind.LongMultiplyDivide; + // Reject illegal EAs before extension-selected operations, + // including the 060's unavailable 64-bit operation trap. + var mode = (opcode >> 3) & 7; + var register = opcode & 7; + return mode == 1 || mode == 7 && register > 4 + ? M68020OpcodeKind.IllegalInstruction : M68020OpcodeKind.LongMultiplyDivide; } if (opcode is 0x0CFC or 0x0EFC) @@ -3146,7 +3166,12 @@ private static M68020OpcodeKind ClassifyBaseOpcode(ushort opcode) if ((opcode & 0xF8C0) == 0xE8C0) { - return M68020OpcodeKind.BitField; + var mode = (opcode >> 3) & 7; + var register = opcode & 7; + var operation = (opcode >> 8) & 7; + var maximumAbsolute = operation is 2 or 4 or 6 or 7 ? 1 : 3; + return mode is 0 or 2 or 5 or 6 || mode == 7 && register <= maximumAbsolute + ? M68020OpcodeKind.BitField : M68020OpcodeKind.IllegalInstruction; } if ((opcode & 0xF1F8) == 0xE048) @@ -4432,6 +4457,9 @@ traceOpcode is 0x4E72 or 0x4E73 or 0x4E74 or 0x4E75 or 0x4E77 or 0x007C or 0x027 (traceOpcode & 0xFFE0) == 0xF500 || (traceOpcode & 0xFFD8) == 0xF548; } ExecuteInstructionBody(); + // HALT synchronizes and holds the processor before any subsequent + // exception processing. STOP remains traceable and is handled below. + if (State.Halted) return; var exception = State.ExceptionSequence != serial; if (exception && State.LastExceptionVector is not (5 or 6 or 7 or >= 32 and <= 47)) return; if ((trace & 0x8000) != 0 || flow || exception) @@ -5510,7 +5538,7 @@ public virtual void Reset(uint programCounter, uint stackPointer) } } - public void BeginSubroutine(uint address, uint stackPointer, uint returnAddress) + public virtual void BeginSubroutine(uint address, uint stackPointer, uint returnAddress) { State.SetActiveStackPointer(stackPointer); PushLong(returnAddress); diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 50d7b86e..1b615032 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -951,3 +951,83 @@ isolated consumer `artifacts/invalid-system-validation/`, `artifacts/invalid-system-production.binlog`. No old test is retired, no package is published and no new seeded or physical audit is claimed. Milestone 6 remains in progress with the accepted scope unchanged. + +## Unary, multiply/divide, CHK, bitfield and debug instructions — 2026-10-05 + +The next pinned ILLEGAL cases expose NEGX.B An, long multiply/divide An, +CHK An and illegal bitfield operands. M68000PM 4-31..52, 4-70, +4-93..98, 4-111, 4-136..142 and 4-193 provide the independent EA rules. +The new matrices cover all register encodings of these assigned invalid forms, +both stacks and all 32 initial CCR values. Long multiply/divide uses four valid +signed/unsigned and 32/64-bit extensions, proving that an invalid EA enters +vector 4 before the 060 unavailable-operation decision. Bitfields distinguish +read-only PC-relative sources from mutating destinations. The shared fixture +checks complete frames, preserved registers/memory and absent operand effects. +Unassigned mode-7 registers and reserved extension bits remain outside this +assigned-operand slice; existing legal matrices remain required. + +Two aliases require positive qualification. NBCD mode-1 words are LINK.L on +020+, and 060 TAS mode-1 words `4AC8`/`4ACC` are HALT/PULSE. Preliminary unary +fixtures incorrectly classified these aliases; the corrected baseline excludes +them and tests their legal behavior separately. These fixture errors are not +CPU defects. MC68060UM 9.2.2 / 9-30 defines privileged HALT, no interrupt restart, +and user-accessible PULSE. Both instructions now appear in the integer inventory. +HALT holds the next PC with no subsequent trace entry; PULSE preserves integer +state and follows ordinary T1 tracing. Reset recovers HALT; idle, interrupt and +host-entry operations cannot wake it. Physical PST signals, debug-port restart, +pipeline toggling and physical timing remain unavailable qualification. + +The six new groups contain **421,120 cases in 48 reporting batches**: +unary 82,944; word multiply/divide 131,072; long multiply/divide 32,768; +CHK 65,536; bitfield 106,496; debug instructions/recovery 2,304. The corrected +baseline against production `424ad4e` records **226,304 passing, 5,120 +mismatching, 189,440 unsupported and 256 untested cases**. The untested recovery +edges depend on HALT first executing successfully. Word multiply/divide already +passes; it needs no production correction. Advanced dispatch now rejects the +other assigned illegal operands before effects. The 060 handler intercepts its +legal debug aliases before TAS legality checks. Post-instruction tracing cannot +process a halted CPU; STOP retains its trace behavior. All 158 focused checks +pass, including all new cases, legal bitfields and affected trace/STOP checks. +Existing successful ordering and timing policy are preserved, with no partial +instruction retry and no public package API change. + +Final ordinary Release validation passes **4,740 CPU tests**, with nine optional +skips and zero failures. The report gate validates **9,182,792 logical cases in +434 reporting batches**; deleting each of the six new 000 reports independently +fails the selected-model report gate. Fresh SingleStepTests retains 312,500 +passes in 125 files and Musashi retains 536 passes / 88 explicit exclusions. +AHX passes 18 tests. Existing input pins and exclusion caveats apply unchanged. + +The fresh WinUAE audit remains failing: **1,308 passing, 60 mismatching, 13 +unsupported and zero untested groups**, with 11,263,752 callbacks, 1,500,873 +frame assertions and 199,327 masked-SR cases. All 32 comparator controls pass. +HALT and PULSE each pass their two selected callbacks, which alone do not qualify +supervisor HALT/recovery or trace; the synthetic matrix supplies those cases. +000/010 still pass all 34,880 selected ILLEGAL callbacks. Other advanced profiles +now reach unassigned CHK word `413D`; 040 reaches `F300` FPU/ILLEGAL expectations. +These, saved-PC reference caveats, adapter stack conventions and internal RTE +restoration remain separate work. No family is excluded to make the audit pass. + +Private **unpublished** NuGet `1.5.2-synthetic-dev.43` has SHA-256 +`5f2e9252a598a9e2d803eacfae89597c566f916f11c9c62bc22d6c7d8645b2ad`. +`artifacts/m6-invalid-integer-package.json` records production source and assembly +identities against `424ad4e`. The isolated CopperScreen baseline `d9beae8` +resolves this exact package for production and separate diagnostics. All four +loaded consumer CPU DLLs match SHA-256 +`72078b2730ddebd29106a6614cb9cbb082ba476c7d133519a6503a9d23aedca7`. +Release build has zero warnings/errors; host 149, disk 74 and separate engine +diagnostics 1,080 pass. All three native Workbench/A1200 boot and disk-persistence +cases pass without skips. Six optional host/media skips remain unavailable +coverage. These checks qualify correctness, not throughput or physical timing. + +Evidence: `artifacts/m6-invalid-integer-qualified-baseline/`, +`artifacts/m6-invalid-integer-final-focused/`, `artifacts/m6-invalid-integer-cpu/`, +`artifacts/m6-invalid-integer-references/`, `artifacts/m6-invalid-integer-winuae/`, +`artifacts/m6-invalid-integer-guard-*/`, `artifacts/m6-invalid-integer-ahx-results/` +and `artifacts/synthetic-private-feed-43/`; isolated consumer +`artifacts/invalid-integer-validation/`, +`artifacts/invalid-integer-diagnostic-tests/` and +`artifacts/invalid-integer-production.binlog`. Earlier preliminary unary runs +retain their fixture mistakes as historical evidence, not corrected counts. +No package publication, old-test retirement, seeded or physical audit is added. +Milestone 6 remains in progress with its existing scope intact. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 1f031e5d..f994e521 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -745,3 +745,28 @@ before/after cases, exclusions and limitations are recorded in the reference document. No release, regression retirement, seeded or physical qualification is added. Milestone 6 remains in progress for the existing integer/reference and restoration requirements. + +The next reference follow-up adds 421,120 cases in 48 reporting batches for +assigned illegal unary, word/long multiply/divide, CHK and bitfield operands, +plus 060 HALT/PULSE privilege, trace and recovery behavior. The corrected +baseline detects 5,120 mismatches and 189,440 unsupported executions, with +256 HALT recovery edges explicitly untested until their prerequisite works. +Legal LINK.L and HALT/PULSE aliases are separated from illegal NBCD/TAS forms; +both debug instructions are added to the complete integer inventory. Advanced +legality checks run before operand effects and unavailable-operation decisions. +HALT blocks subsequent tracing and cannot be restarted by interrupts or host +entry; STOP keeps its trace semantics. All 158 focused checks pass, including +the new cases, legal bitfields and affected trace/STOP behavior. Physical debug +signals, debug-port restart and pipeline commands remain unqualified. + +Final validation passes 4,740 CPU tests with nine optional skips and zero failures, +and 9,182,792 deterministic logical cases in 434 reporting batches. Missing each +new report fails the gate. Fresh SingleStepTests passes 312,500 selected cases, +Musashi passes 536 programs with 88 exclusions and AHX passes 18 tests. WinUAE +passes the selected HALT/PULSE cases but still fails with 60 mismatching and 13 +unsupported groups; all 32 comparator controls pass. The private unpublished +.43 package validates the isolated CopperScreen Release build, host/disk/engine +diagnostics and all three native boot/persistence replays. The reference document +records exact cases, source/package/assembly identities, preliminary fixture +corrections and remaining gaps. No release or regression retirement is included; +milestone 6 remains in progress. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 756e9c82..8d2e3bf4 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -69,6 +69,8 @@ try { 'arithmetic-boundaries'=56448; 'arithmetic-addressing'=$(if ($model -in @('68000','68010')) {4667} else {8237}); 'arithmetic-scenarios'=21072; 'arithmetic-extend'=25440; 'arithmetic-invalid-operands'=$(if ($model -in @('68000','68010')) {6336} else {5952}); + 'arithmetic-muldiv-word-invalid-operands'=16384; + 'arithmetic-muldiv-long-invalid-operands'=4096; 'arithmetic-decimal'=$(if ($model -in @('68000','68010')) {111392} else {111458}); 'arithmetic-muldiv-boundaries'=26904; 'arithmetic-muldiv-addressing'=$(if ($model -in @('68000','68010')) {5608} elseif ($model -eq '68060') {7208} else {7224}); @@ -82,11 +84,15 @@ try { 'logical-bits'=$(if ($model -in @('68000','68010')) {104192} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {104852}); 'logical-boundaries'=31808; 'logical-invalid-operands'=13248; + 'logical-unary-invalid-operands'=$(if ($model -in @('68000','68010')) {11968} elseif ($model -eq '68060') {9728} else {9856}); + 'logical-bitfield-invalid-operands'=13312; 'logical-cas-invalid-operands'=3456; 'logical-cas'=$(if ($model -in @('68000','68010')) {36975} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {37371}); 'logical-cas2'=36864; 'logical-shifts'=$(if ($model -in @('68000','68010')) {130561} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {131089}); 'system-basic'=1728; + 'system-chk-invalid-operands'=8192; + 'system-debug-instructions'=$(if ($model -eq '68060') {512} else {256}); 'system-bounds'=$(if ($model -in @('68000','68010')) {11828} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {12488}); 'system-callm'=$(if ($model -in @('68000','68010')) {10832} elseif ($model -in @('68020','68EC020','A1200')) {11000} elseif ($model -in @('68030','68040','68060')) {10964}); 'system-interrupt'=$(if ($model -in @('68000')) {224} elseif ($model -in @('68010','68060')) {226} elseif ($model -in @('68020','68030','68040','68EC020','A1200')) {418}); @@ -199,7 +205,7 @@ try { foreach ($row in $inventory.combinations) { if ($row.Milestone -le 5) { $row.status = 'passing'; $row.note = 'Passing named semantic scenario batches; consult qualification boundaries for untested physical/internal protocols.' } } - $inventory | Add-Member -NotePropertyName qualificationBoundaries -NotePropertyValue @('RTE fault restart/internal formats remain untested: 010 long/non-MOVE/foreign frame8 restart, 020/030 frames9/A/B, 040 frame7', 'BKPT external replacement responder is unavailable', 'MOVES physical function-code spaces and LPSTOP CPU-space broadcast are unqualified', 'CALLM/RTM type1 protocol uses an internal synthetic responder; normal public buses have none') + $inventory | Add-Member -NotePropertyName qualificationBoundaries -NotePropertyValue @('RTE fault restart/internal formats remain untested: 010 long/non-MOVE/foreign frame8 restart, 020/030 frames9/A/B, 040 frame7', 'BKPT external replacement responder is unavailable', 'MOVES physical function-code spaces and LPSTOP CPU-space broadcast are unqualified', 'CALLM/RTM type1 protocol uses an internal synthetic responder; normal public buses have none', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands are unavailable') $inventory | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'qualified-inventory.json') @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=$logicalBatches; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; m68040DisabledMmuInstructionGate='passing'; m68000AddressErrorDoubleFaultGate='passing'; m68010Format8StructureGate='passing'; m68010WordMoveRestartGate='passing'; roadmapComplete=$false; seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); @@ -207,7 +213,7 @@ try { m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; marked private word-MOVE/MOVEA images resume their faulted cycle; other opaque restart/input state, long transfers, RMW and physical timing remain unqualified'; m68000DoubleFaultScope='Address-error entry/handler faults; reset-only recovery; external BERR/reset-vector faults unavailable through the current public bus API'; - unavailableSystemCoverage=@('010 long/non-MOVE/external bus-fault restart and 020/030 internal restart', '020/030 coprocessor midinstruction restoration', '040 access-fault pending trace/writeback restoration', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast'); + unavailableSystemCoverage=@('010 long/non-MOVE/external bus-fault restart and 020/030 internal restart', '020/030 coprocessor midinstruction restoration', '040 access-fault pending trace/writeback restoration', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands'); timing='Semantic gate; timing policy and physical qualification remain separate'} | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'summary.json') Write-Host "Synthetic milestones 1-5 semantic gates: $logicalCases logical cases; reports at $output" From 4a32a1f64982573a1f2368c43781b5ba22d3595c Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 02:07:44 +0300 Subject: [PATCH 016/211] Correct PACK and UNPK stack word strides --- .../M68kWinUaeCpuTesterConformanceTests.cs | 11 +++ .../M68kWinUaeCpuTesterConformanceTests.md | 9 +++ Copper68k.Tests/M68kWinUaeModelAuditTests.cs | 9 ++- .../Synthetic/SyntheticExtendDecimalTests.cs | 62 ++++++++++++--- ...68kAdvancedTimingInterpreter.Arithmetic.cs | 21 +++-- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 79 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 29 +++++++ scripts/test-copper68k-synthetic.ps1 | 1 + 8 files changed, 200 insertions(+), 21 deletions(-) diff --git a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs index 0a45210d..d4c6219a 100644 --- a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs +++ b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs @@ -301,6 +301,7 @@ private sealed class NativeTester : IDisposable private int _executedCases; private int _unmappedReads; private int _unmappedWrites; + private int _terminalCases; private string _lastCaseSummary = ""; private byte _cpuLevel; private ModelSpec? _integerProfile; @@ -358,6 +359,7 @@ public RunResult Run( _executedCases = 0; _unmappedReads = 0; _unmappedWrites = 0; + _terminalCases = 0; _lastCaseSummary = ""; _cpuLevel = cpuLevel; _integerProfile = integerProfile; @@ -447,6 +449,7 @@ public readonly record struct RunResult( public uint FrameChecks => _frameChecks?.Invoke() ?? 0; public uint MaskedCases => _maskedCases?.Invoke() ?? 0; + public int TerminalCases => _terminalCases; public bool UnsupportedExecution => _callbackException?.InnerException is UnsupportedM68kTimingException; public void Dispose() @@ -518,6 +521,14 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt } registers.Cycles += (uint)cycles; + if (_integerProfile is not null && (cpu.State.Stopped || cpu.State.Halted)) + { + // Compare the actual terminal instruction boundary. Do not + // wake the CPU or advance PC to a following harness sentinel. + _terminalCases++; + completed = true; + break; + } var traceSetAfterInstruction = (cpu.State.StatusRegister & M68kCpuState.Trace) != 0; if (_integerProfile is null && tracePending && !cpu.State.Stopped && !cpu.State.Halted) { diff --git a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md index 4fc17af8..44d495c6 100644 --- a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md +++ b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md @@ -213,3 +213,12 @@ Both fail the gate. The current classifier records 62 mismatching and 15 unsuppo groups (77 total), with zero untested; frame/masked-case counts and all 32 controls are recorded separately from normal callbacks. Older bridges lacking these assertions are rejected. Source/adapter/CPU disagreements remain open. + +Integer callbacks also stop at an actual STOP/HALT boundary. The adapter does +not wake execution or advance PC to a following sentinel. Additive schema-2 +`TerminalCases` row counts and the `terminalCases` total record these comparisons. +The packing checkpoint observes one such callback: 060 STOP `4E72 0000` exposes +a privilege/state disagreement against the pinned reference. It remains a +failing result requiring model/reference qualification. FPU adapter behavior +is unchanged. Current results and pinned-input caveats are recorded in +[reference qualification](../docs/COPPER68K_REFERENCE_QUALIFICATION.md#packunpk-word-stride-and-terminal-reference-boundaries--2026-10-05). diff --git a/Copper68k.Tests/M68kWinUaeModelAuditTests.cs b/Copper68k.Tests/M68kWinUaeModelAuditTests.cs index d1eee127..acfccc40 100644 --- a/Copper68k.Tests/M68kWinUaeModelAuditTests.cs +++ b/Copper68k.Tests/M68kWinUaeModelAuditTests.cs @@ -65,9 +65,9 @@ public void WinUaeIntegerFixturesAcrossSelectedModelsWhenEnabled() { var result = tester.Run(path, opcode, fixture.CpuLevel, false, false, model); rows.Add(new(id, opcode, result.Passed ? result.ExecutedCases > 0 ? "passing" : "untested" : tester.UnsupportedExecution ? "unsupported" : "mismatching", - result.ExecutedCases, result.UnmappedReads, result.UnmappedWrites, tester.FrameChecks, tester.MaskedCases, result.Detail)); + result.ExecutedCases, result.UnmappedReads, result.UnmappedWrites, tester.FrameChecks, tester.MaskedCases, tester.TerminalCases, result.Detail)); } - catch (Exception ex) { rows.Add(new(id, opcode, "mismatching", 0, 0, 0, 0, 0, ex.Message)); } + catch (Exception ex) { rows.Add(new(id, opcode, "mismatching", 0, 0, 0, 0, 0, 0, ex.Message)); } } _output.WriteLine($"{id}: {rows.Count(r => r.Model == id && r.Status == "passing")} directories passed; {rows.Where(r => r.Model == id).Sum(r => (long)r.ExecutedCases)} callbacks executed."); } @@ -83,7 +83,8 @@ public void WinUaeIntegerFixturesAcrossSelectedModelsWhenEnabled() passing = rows.Count(r => r.Status == "passing"), mismatching = rows.Count(r => r.Status == "mismatching"), unsupported = rows.Count(r => r.Status == "unsupported"), untested = rows.Count(r => r.Status == "untested"), executedCases = rows.Sum(r => (long)r.ExecutedCases), - exceptionFrames = rows.Sum(r => (long)r.ExceptionFrames), maskedSrCases = rows.Sum(r => (long)r.MaskedSrCases), probes, rows + exceptionFrames = rows.Sum(r => (long)r.ExceptionFrames), maskedSrCases = rows.Sum(r => (long)r.MaskedSrCases), + terminalCases = rows.Sum(r => (long)r.TerminalCases), probes, rows }, new JsonSerializerOptions { WriteIndented = true })); var failures = rows.Where(r => r.Status != "passing").ToArray(); Assert.True(failures.Length == 0, string.Join(Environment.NewLine, failures.Select(r => $"{r.Model}/{r.Opcode}: {r.Detail}"))); @@ -148,6 +149,6 @@ internal static void ValidateWinUaeProfile(string root, WinUaeManifest manifest, internal sealed record WinUaeManifest(int Schema, string GeneratorCommit, string RunnerCommit, string NativeLibrarySha256, WinUaeProfile[] Profiles); internal sealed record WinUaeProfile(string Id, string CpuDirectory, byte CpuLevel, int AddressBits, string[] Opcodes, WinUaeInput[] Inputs); internal sealed record WinUaeInput(string Path, long Bytes, string Sha256); - private sealed record WinUaeModelRow(string Model, string Opcode, string Status, int ExecutedCases, int UnmappedReads, int UnmappedWrites, uint ExceptionFrames, uint MaskedSrCases, string Detail); + private sealed record WinUaeModelRow(string Model, string Opcode, string Status, int ExecutedCases, int UnmappedReads, int UnmappedWrites, uint ExceptionFrames, uint MaskedSrCases, int TerminalCases, string Detail); private sealed record WinUaeProbeRow(string Model, string Kind, bool Detected, int ExecutedCases, string Detail); } diff --git a/Copper68k.Tests/Synthetic/SyntheticExtendDecimalTests.cs b/Copper68k.Tests/Synthetic/SyntheticExtendDecimalTests.cs index eee7a416..97838a57 100644 --- a/Copper68k.Tests/Synthetic/SyntheticExtendDecimalTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticExtendDecimalTests.cs @@ -28,6 +28,38 @@ public void ExtendComparisonAndAliases(string modelId) report.Complete(output); } + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void PackingMemoryWordStrideAliasesAndAddressBoundaries(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "arithmetic-packing-memory"); + // Fixed M68000PM 4-157/196 examples: the unpacked operand is a + // contiguous two-byte value. A7's special stride applies to the + // packed byte operand, not independently to both halves of the word. + Assert.Equal(0x814f, 0x8140 | 8 | 7); // PACK -(A7),-(A0) + Assert.Equal(0x8f89, 0x8180 | 7 << 9 | 8 | 1); // UNPK -(A1),-(A7) + foreach (var family in new[] { "PACK", "UNPK" }) + for (var src = 0; src < 8; src++) + for (var dst = 0; dst < 8; dst++) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + Pack(machine, report, family, family == "UNPK" ? 0x85u : 0x1234u, + 0xffff, ccr, src, dst, true, supervisor); + // 000/010 lack both instructions; canonical cases above verify their + // vector-4 outcomes. Unaligned/wrapping operands are legal on 020+. + if (machine.Model.FullIndex) + foreach (var family in new[] { "PACK", "UNPK" }) + foreach (var value in new uint[] { 0, 0x1234, 0x9abc, 0x8000, 0xffff, 0x85, 0x99 }) + foreach (var adjustment in new ushort[] { 0, 1, 0xffff, 0xff, 0xff00, 0x8888 }) + foreach (var (src, dst) in new[] { (7, 0), (0, 7), (7, 7), (0, 0), (0, 1), (1, 0) }) + foreach (var supervisor in new[] { false, true }) + foreach (var (sourceBase, destinationBase) in new[] + { (0x4103u, 0x4705u), (0xffff8002u, 0xffff9001u), (1u, 0x01000001u), (0x4103u, 0x4103u) }) + Pack(machine, report, family, value, adjustment, 31, src, dst, true, + supervisor, sourceBase, destinationBase); + report.Complete(output); + } + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] public void DecimalAndPacking(string modelId) { @@ -134,23 +166,35 @@ private static void Nbcd(SyntheticMachine machine, CoverageBatch report, Operand SyntheticExecution.Run(machine, fixture.Expected, report, $"{machine.Model.Id}/NBCD/1/{form.Id}/{index?.Id ?? "brief"}/op={opcode:X4}/value={value:X2}/ccr={ccr:X2}"); } - private static void Pack(SyntheticMachine machine, CoverageBatch report, string family, uint value, ushort adjustment, int ccr, int src, int dst, bool memory, bool supervisor) + private static void Pack(SyntheticMachine machine, CoverageBatch report, string family, uint value, ushort adjustment, int ccr, int src, int dst, bool memory, bool supervisor, + uint? sourceBase = null, uint? destinationBase = null) { machine.Reset(ccr, supervisor); var unpack = family == "UNPK"; var opcode = (ushort)((unpack ? 0x8180 : 0x8140) | dst << 9 | (memory ? 8 : 0) | src); var sourceWidth = unpack ? 1 : 2; var destinationWidth = unpack ? 2 : 1; + if (sourceBase.HasValue) + { + if (src == 7) machine.Core.State.SetActiveStackPointer(sourceBase.Value); + else machine.Core.State.A[src] = sourceBase.Value; + if (dst != src) + { + if (dst == 7) machine.Core.State.SetActiveStackPointer(destinationBase!.Value); + else machine.Core.State.A[dst] = destinationBase!.Value; + } + } var a = (uint[])machine.Core.State.A.Clone(); var writes = new List(); uint source = 0; if (!memory) machine.Core.State.D[src] = ArithmeticSpecification.RegisterBits(value, sourceWidth); else { - for (var i = 0; i < sourceWidth; i++) - { - a[src] -= src == 7 ? 2u : 1u; - machine.InitializePhysical(a[src], value >> (8 * i), 1); - source |= (uint)machine.Bus.Peek(machine.Model.Physical(a[src])) << (8 * i); - } - for (var i = 0; i < destinationWidth; i++) { a[dst] -= dst == 7 ? 2u : 1u; writes.Add(a[dst]); } + foreach (var operandBase in new[] { a[src], a[dst] }) + for (var offset = -8; offset <= 4; offset++) + machine.InitializePhysical(unchecked(operandBase + (uint)offset), (uint)(0xd0 + offset), 1); + a[src] = unchecked(a[src] - (sourceWidth == 2 ? 2u : src == 7 ? 2u : 1u)); + machine.InitializePhysical(a[src], value, sourceWidth); + source = value & MoveSpecification.Mask(sourceWidth); + a[dst] = unchecked(a[dst] - (destinationWidth == 2 ? 2u : dst == 7 ? 2u : 1u)); + for (var i = 0; i < destinationWidth; i++) writes.Add(unchecked(a[dst] + (uint)(destinationWidth - 1 - i))); } var expected = SyntheticExecution.Prepare(machine, [opcode, adjustment]); if (!memory) source = expected.D[src]; @@ -166,6 +210,6 @@ private static void Pack(SyntheticMachine machine, CoverageBatch report, string for (var i = 0; i < writes.Count; i++) expected.Write(writes[i], result >> (8 * i), 1, machine.Model); } } - SyntheticExecution.Run(machine, expected, report, $"{machine.Model.Id}/{family}/packed/r{src}-r{dst}/memory={memory}/super={supervisor}/op={opcode:X4}/value={value:X4}/adjust={adjustment:X4}/ccr={ccr:X2}"); + SyntheticExecution.Run(machine, expected, report, $"{machine.Model.Id}/{family}/packed/r{src}-r{dst}/memory={memory}/super={supervisor}/source-base={sourceBase:X8}/destination-base={destinationBase:X8}/op={opcode:X4}/value={value:X4}/adjust={adjustment:X4}/ccr={ccr:X2}"); } } diff --git a/Copper68k/M68kAdvancedTimingInterpreter.Arithmetic.cs b/Copper68k/M68kAdvancedTimingInterpreter.Arithmetic.cs index 6e687f18..e6da2aa6 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.Arithmetic.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.Arithmetic.cs @@ -109,8 +109,12 @@ private bool TryExecutePackUnpack(ushort opcode) if (!memory) value = State.D[source]; else { - var low = ReadPredecrementByte(source); - value = unpack ? low : (uint)(ReadPredecrementByte(source) << 8 | low); + // The unpacked operand is one contiguous word, including at A7. + // Only the packed byte uses A7's two-byte stride. Preserve the + // existing low-byte-first transfer ordering and timing policy. + var sourceStride = unpack && source == 7 ? 2u : 1u; + var low = ReadPredecrementByte(source, sourceStride); + value = unpack ? low : (uint)(ReadPredecrementByte(source, sourceStride) << 8 | low); } var result = unpack ? (ushort)(((value & 0xf0) << 4 | (value & 15)) + adjustment) : (ushort)((ushort)(value + adjustment) & 0x0f0f); @@ -118,20 +122,21 @@ private bool TryExecutePackUnpack(ushort opcode) if (!memory) WriteDataRegisterSized(destination, result, unpack ? M68kOperandSize.Word : M68kOperandSize.Byte); else { - WritePredecrementByte(destination, (byte)result); - if (unpack) WritePredecrementByte(destination, (byte)(result >> 8)); + var destinationStride = !unpack && destination == 7 ? 2u : 1u; + WritePredecrementByte(destination, (byte)result, destinationStride); + if (unpack) WritePredecrementByte(destination, (byte)(result >> 8), destinationStride); } CompleteGeneralArithmeticTiming(memory ? 4 : 0, destination, memory); return true; } - private byte ReadPredecrementByte(int register) + private byte ReadPredecrementByte(int register, uint stride) { - var address = unchecked(State.A[register] - (register == 7 ? 2u : 1u)); + var address = unchecked(State.A[register] - stride); WriteGeneralRegister(true, register, address); return ReadByte(address); } - private void WritePredecrementByte(int register, byte value) + private void WritePredecrementByte(int register, byte value, uint stride) { - var address = unchecked(State.A[register] - (register == 7 ? 2u : 1u)); + var address = unchecked(State.A[register] - stride); WriteGeneralRegister(true, register, address); WriteByte(address, value); } } diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 1b615032..79d93bfe 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -1031,3 +1031,82 @@ and `artifacts/synthetic-private-feed-43/`; isolated consumer retain their fixture mistakes as historical evidence, not corrected counts. No package publication, old-test retirement, seeded or physical audit is added. Milestone 6 remains in progress with its existing scope intact. + +## PACK/UNPK word stride and terminal reference boundaries — 2026-10-05 + +PACK's unpacked source and UNPK's unpacked destination are contiguous words, +including when addressed through A7. Only the packed byte operand uses A7's +special two-byte stride. The original production code and synthetic fixture +both incorrectly applied that byte stride to each half of the word, consuming +four bytes and leaving a gap. Independent expectations now treat the word as +one two-byte operand. M68000PM 4-156..158 and 4-195..197 define the operand +diagrams and transformations; MC68020UM table 9-21 corroborates the word-side +operand access. Pinned WinUAE PACK/UNPK code agrees. The pinned Musashi +instruction source retains separate A7 byte decrements and is not corroborating +evidence for this correction; its passing self-checking program audit does not +contain a discriminating packing program. + +The new `arithmetic-packing-memory` group executes **89,728 cases in eight +reporting batches**: 8,192 per 000/010 profile and 12,224 per advanced profile. +Canonical cases exercise every source/destination register encoding, all CCR +states and both stacks. Advanced cases add boundary values and adjustments, +aliased bases, overlapping operands, odd addresses, negative high addresses +and external-address wrapping. Surrounding canaries, complete register state, +exact next PC and unchanged flags remain checked. Unavailable 000/010 packing +instructions must enter vector 4. Fixed opcode examples independently check +the fixture encoding. + +Against production `66d276e`, these revised expectations detect **14,208 +mismatches**, 2,368 per advanced profile, with zero unsupported or untested new +cases. The corrected existing decimal fixture additionally detects 32 A7 +packing failures per advanced profile. All 16 affected batches pass after the +production stride correction. Byte transfer order and the existing timing +policy are preserved; physical bus width, intermediate A7 visibility and +silicon timing are not qualified. No instruction is retried after operand +effects, no public API changes and no old regression is retired. + +The WinUAE adapter now ends an integer callback when execution becomes stopped +or halted. It compares actual state immediately, without waking the CPU, +advancing PC to a sentinel or normalizing the result. Schema 2 adds +`TerminalCases` to rows and `terminalCases` to totals. A fresh all-profile audit +passes all **12 PACK/UNPK groups / 70,720 callbacks**. Its complete result is +**1,320 passing, 48 mismatching, 13 unsupported and zero untested groups** over +11,311,137 callbacks, 1,500,873 exception frames and 199,327 masked-SR cases; +all 32 comparator controls pass. There is one terminal callback. The 060 STOP +case `4E72 0000` now exposes a state disagreement rather than waiting 64 steps: +Copper68k clears S and stops, while the pinned reference expects vector 8 and +the old SR. The applicable model rule and saved-PC reference need qualification; +this adapter change does not fix or hide that disagreement. No family is +excluded and the requested discovery audit remains failing. + +Evidence: `artifacts/m6-packing-before/`, `artifacts/m6-packing-after/`, +`artifacts/m6-packing-winuae/`, `artifacts/m6-terminal-winuae/` and +`artifacts/m6-packing-final-winuae/`. Earlier runs retain their identities and +counts. Milestone 6 remains in progress with its accepted scope intact. + +Final ordinary Release validation passes **4,748 CPU tests**, with nine optional +skips and zero failures. Fresh SingleStepTests passes 312,500 cases in 125 files; +Musashi passes 536 programs with 88 explicit exclusions; AHX passes 18 tests. +Existing source pins and reference caveats apply unchanged. The deterministic +report gate validates **9,272,520 logical cases in 442 reporting batches**. +Omitting the new 000 packing report fails the selected-model gate. + +Private **unpublished** NuGet `1.5.2-synthetic-dev.44` has SHA-256 +`a4230a7efa4b1b8e0bd374862822483a9caea4e14e9925f3bfa2ab280b2771a2`. +`artifacts/m6-packing-package.json` records source and assembly identities +against `66d276e`. The isolated CopperScreen baseline `d9beae8` resolves the +exact package in production and separate diagnostics. All four loaded CPU DLLs +match SHA-256 +`9404ef080ea6c1f6ba44b933df3683b42b6aff6e163100b4bff896074915b8a0`. +Release build has zero warnings/errors; host 149, disk 74 and engine diagnostics +1,080 pass. Two native Workbench floppy boot profiles and one native A1200 +boot/disk-persistence replay pass. An additionally selected Workbench hard-disk +theory is skipped without its HDF environment input; it is unavailable coverage, +not a replay. Six optional host/media skips remain unavailable. No new seeded, +physical timing or host-throughput qualification is claimed. + +Final evidence: `artifacts/m6-packing-cpu/`, `artifacts/m6-packing-references/`, +`artifacts/m6-packing-guard-missing/`, `artifacts/m6-packing-ahx-results/`, +`artifacts/synthetic-private-feed-44/`; isolated consumer +`artifacts/packing-validation/`, `artifacts/packing-diagnostic-tests/` and +`artifacts/packing-production.binlog`. No package is published. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index f994e521..c8b64ab4 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -770,3 +770,32 @@ diagnostics and all three native boot/persistence replays. The reference documen records exact cases, source/package/assembly identities, preliminary fixture corrections and remaining gaps. No release or regression retirement is included; milestone 6 remains in progress. + +The packing checkpoint corrects the A7 stride of PACK's word source and UNPK's +word destination. These are contiguous words; only the packed byte has A7's +special stride. Revised independent expectations expose the original error in +both production and the earlier fixture. The new `arithmetic-packing-memory` +group adds 89,728 cases in eight reporting batches across all profiles, covering +every memory register pair, both stacks, all CCR values, aliases, overlapping +operands and advanced address/value boundaries. Against `66d276e` it detects +14,208 mismatches; all new cases and existing decimal cases now pass. The report +gate requires this group and rejects its omission. Successful byte transfer +order and timing policy are preserved; physical bus qualification is separate. + +The pinned WinUAE audit now passes all 12 PACK/UNPK groups (70,720 callbacks). +An integer callback ends at a real STOP/HALT boundary and compares actual state, +without advancing PC to the sentinel. The resulting 060 STOP privilege/state +disagreement remains open. The complete audit still fails with 48 mismatching +and 13 unsupported groups; all 32 comparator controls pass. No family is +excluded to make it green. The reference qualification document records the +manual/source evidence, old fixture error, terminal counts and remaining gaps. +No regression retirement or package publication is included. Milestone 6 +remains in progress with its accepted scope intact. + +Final validation passes 4,748 CPU tests with nine optional skips and zero +failures, plus 9,272,520 deterministic logical cases in 442 reporting batches. +Fresh SingleStepTests/Musashi and AHX checks pass. The private unpublished .44 +package validates the isolated CopperScreen Release build, host/disk/engine +tests and three native Workbench/A1200 replays. An additionally selected +Workbench hard-disk theory without its HDF input is unavailable coverage. Exact +package, source, assembly and replay evidence is in the reference document. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 8d2e3bf4..20e75d5b 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -72,6 +72,7 @@ try { 'arithmetic-muldiv-word-invalid-operands'=16384; 'arithmetic-muldiv-long-invalid-operands'=4096; 'arithmetic-decimal'=$(if ($model -in @('68000','68010')) {111392} else {111458}); + 'arithmetic-packing-memory'=$(if ($model -in @('68000','68010')) {8192} else {12224}); 'arithmetic-muldiv-boundaries'=26904; 'arithmetic-muldiv-addressing'=$(if ($model -in @('68000','68010')) {5608} elseif ($model -eq '68060') {7208} else {7224}); 'control-branches'=$(if ($model -in @('68000','68010')) {8192} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {12288}); From 8ffc8dff8985eb93efb66ab754098b5b7bf5bd56 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 02:33:35 +0300 Subject: [PATCH 017/211] Qualify 68040 and 68060 trap trace priority --- .../M68kWinUaeCpuTesterConformanceTests.cs | 4 + .../M68kWinUaeCpuTesterConformanceTests.md | 40 +++++ Copper68k.Tests/M68kWinUaeModelAuditTests.cs | 6 +- .../M68kWinUaeTrapTraceAuditTests.cs | 82 ++++++++++ .../SyntheticExceptionControlTests.cs | 6 +- .../Synthetic/SyntheticTrapTraceTests.cs | 140 ++++++++++++++++++ Copper68k/M68kAdvancedTimingInterpreter.cs | 6 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 99 +++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 26 ++++ scripts/prepare-copper68k-winuae.ps1 | 30 +++- scripts/test-copper68k-synthetic.ps1 | 1 + scripts/winuae/trace-priority.patch | 5 + 12 files changed, 435 insertions(+), 10 deletions(-) create mode 100644 Copper68k.Tests/M68kWinUaeTrapTraceAuditTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticTrapTraceTests.cs create mode 100644 scripts/winuae/trace-priority.patch diff --git a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs index d4c6219a..92b33a7a 100644 --- a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs +++ b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs @@ -302,6 +302,7 @@ private sealed class NativeTester : IDisposable private int _unmappedReads; private int _unmappedWrites; private int _terminalCases; + private int _traceInputCases; private string _lastCaseSummary = ""; private byte _cpuLevel; private ModelSpec? _integerProfile; @@ -360,6 +361,7 @@ public RunResult Run( _unmappedReads = 0; _unmappedWrites = 0; _terminalCases = 0; + _traceInputCases = 0; _lastCaseSummary = ""; _cpuLevel = cpuLevel; _integerProfile = integerProfile; @@ -450,6 +452,7 @@ public readonly record struct RunResult( public uint FrameChecks => _frameChecks?.Invoke() ?? 0; public uint MaskedCases => _maskedCases?.Invoke() ?? 0; public int TerminalCases => _terminalCases; + public int TraceInputCases => _traceInputCases; public bool UnsupportedExecution => _callbackException?.InnerException is UnsupportedM68kTimingException; public void Dispose() @@ -472,6 +475,7 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt _ = userData; var context = Marshal.PtrToStructure(contextPtr); var registers = Marshal.PtrToStructure(registersPtr); + if ((registers.Sr & 0x8000) != 0) _traceInputCases++; var bus = new NativeRangeBus(context, _addressingMask?.Invoke() ?? 0x00FF_FFFFu); IM68kCore cpu = _integerProfile is not null ? (_integerProfile.A1200 ? M68kCoreFactory.Default.CreateA1200Ec020(bus) : M68kCoreFactory.Default.Create(_integerProfile.Model, bus)) diff --git a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md index 44d495c6..8b54f4b5 100644 --- a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md +++ b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md @@ -222,3 +222,43 @@ a privilege/state disagreement against the pinned reference. It remains a failing result requiring model/reference qualification. FPU adapter behavior is unchanged. Current results and pinned-input caveats are recorded in [reference qualification](../docs/COPPER68K_REFERENCE_QUALIFICATION.md#packunpk-word-stride-and-terminal-reference-boundaries--2026-10-05). + +## Qualified TRAP trace preset for 040/060 + +The separate `TraceTraps` preset adds incoming T1/S combinations and CCR 0/31 +for TRAP. It expects 256 callbacks, 128 with incoming T1 and 256 frame assertions +per model. Register, defined-X and frame corruption controls must all fail. +Missing/changed inputs, wrong model sets, unqualified source/patch/generator +identities and incomplete counts fail the requested audit. + +The pinned generator incorrectly reports a pending trace after a synchronous +trap on 040/060. [MC68040UM 8.3](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +and [MC68060UM 8.2.6/8.3](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf) +require suppression. The preparation script applies the one-line +[trace-priority patch](../scripts/winuae/trace-priority.patch) to a separate +source file, records source/patch/executable hashes and preserves the original +checkout and all Basic inputs. The test verifies both exact manifested source +bytes and its qualified normalized-text hash, so checkout line endings do not +change the authority. This is an explicitly corrected software reference; +unchanged upstream or physical hardware qualification is not claimed. + +```powershell +./scripts/prepare-copper68k-winuae.ps1 ` + -GeneratorSource artifacts/reference-winuae-api ` + -RunnerSource artifacts/reference-copperline ` + -VcVars64 '/VC/Auxiliary/Build/vcvars64.bat' ` + -Preset TraceTraps -OutputDirectory artifacts/winuae-trap-trace-inputs +$env:COPPER68K_RUN_WINUAE_TRAP_TRACE_AUDIT = '1' +$env:COPPER68K_WINUAE_TRAP_TRACE_PATH = (Resolve-Path artifacts/winuae-trap-trace-inputs).Path +$env:COPPER68K_WINUAE_CPUTEST_LIBRARY = (Resolve-Path artifacts/winuae-trap-trace-inputs/m68k_cpu_tester.dll).Path +$env:COPPER68K_SYNTHETIC_REPORT_DIR = Join-Path (Get-Location).Path 'artifacts/winuae-trap-trace-report' +dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release -p:Platform=AnyCPU ` + --filter FullyQualifiedName~WinUaeTrapTracePriorityAcross040And060WhenEnabled +# Remove these opt-in variables before running the ordinary suite. +``` + +`winuae-trap-trace-audit.json` records source/input/assembly identities and per-model +results, traced callback counts, frames and controls. Other traced families and +models, M-mode, fault combinations and advanced restart remain untested by this +preset. The broad Basic discovery audit retains its separate failures; this +focused preset cannot make it pass. diff --git a/Copper68k.Tests/M68kWinUaeModelAuditTests.cs b/Copper68k.Tests/M68kWinUaeModelAuditTests.cs index acfccc40..98a42b83 100644 --- a/Copper68k.Tests/M68kWinUaeModelAuditTests.cs +++ b/Copper68k.Tests/M68kWinUaeModelAuditTests.cs @@ -93,15 +93,15 @@ public void WinUaeIntegerFixturesAcrossSelectedModelsWhenEnabled() private static string FixtureId(string id) => id == "A1200" ? "68EC020" : id; private static string Hash(string path) => Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))).ToLowerInvariant(); - internal static void ValidateWinUaeProfile(string root, WinUaeManifest manifest, ModelSpec model) + internal static void ValidateWinUaeProfile(string root, WinUaeManifest manifest, ModelSpec model, bool traceTraps = false) { var fixture = manifest.Profiles.SingleOrDefault(p => p.Id == FixtureId(model.Id)) ?? throw new XunitException($"Missing WinUAE profile: {model.Id}"); var level = model.Id switch { "68000" => 0, "68010" => 1, "68030" => 3, "68040" => 4, "68060" => 5, _ => 2 }; var cpu = level == 5 ? "68060" : $"{68000 + level * 10}"; - var requiredCount = model.Id switch { "68000" => 146, "68010" => 153, "68030" => 180, "68040" => 181, "68060" => 184, _ => 179 }; + var requiredCount = traceTraps ? 1 : model.Id switch { "68000" => 146, "68010" => 153, "68030" => 180, "68040" => 181, "68060" => 184, _ => 179 }; if (fixture.CpuLevel != level || fixture.CpuDirectory != cpu || fixture.AddressBits != model.AddressBits || - fixture.Opcodes.Length != requiredCount || fixture.Opcodes.Distinct().Count() != requiredCount || !fixture.Opcodes.Contains("NOP")) + fixture.Opcodes.Length != requiredCount || fixture.Opcodes.Distinct().Count() != requiredCount || !fixture.Opcodes.Contains(traceTraps ? "TRAP" : "NOP")) throw new XunitException($"Incomplete or incompatible WinUAE profile: {model.Id}"); var path = Path.Combine(root, fixture.Id, cpu); foreach (var memory in new[] { ("lmem.dat", 0x8000), ("hmem.dat", 0x8000), ("tmem.dat", 0x40000) }) diff --git a/Copper68k.Tests/M68kWinUaeTrapTraceAuditTests.cs b/Copper68k.Tests/M68kWinUaeTrapTraceAuditTests.cs new file mode 100644 index 00000000..86a14ae6 --- /dev/null +++ b/Copper68k.Tests/M68kWinUaeTrapTraceAuditTests.cs @@ -0,0 +1,82 @@ +using System.Text.Json; +using Copper68k.Tests.Synthetic; +using Xunit.Sdk; + +namespace Copper68k.Tests; + +public sealed partial class M68kWinUaeCpuTesterConformanceTests +{ + private const string TracePresetNormalizedSourceHash = "863cc62fb5f3aa0b0a59a4380f2240e7f853e1207188015c086adb7f56050a49"; + private const string TracePresetPatchHash = "c8d648de711ba31f4340c911b3ddd91e6ea883375329a84eecdf5b537c076ade"; + + [EnvironmentFact("COPPER68K_RUN_WINUAE_TRAP_TRACE_AUDIT", "audit qualified WinUAE TRAP trace priority on 040/060")] + public void WinUaeTrapTracePriorityAcross040And060WhenEnabled() + { + var root = Environment.GetEnvironmentVariable("COPPER68K_WINUAE_TRAP_TRACE_PATH"); + var library = Environment.GetEnvironmentVariable(LibraryVariable); + var output = Environment.GetEnvironmentVariable("COPPER68K_SYNTHETIC_REPORT_DIR"); + if (string.IsNullOrWhiteSpace(root) || string.IsNullOrWhiteSpace(library) || string.IsNullOrWhiteSpace(output)) + throw new XunitException("Trace audit requires qualified fixture root, native library and report directory."); + var manifestFile = Path.Combine(root, "manifest.json"); + var json = File.ReadAllText(manifestFile); + var manifest = JsonSerializer.Deserialize(json) ?? throw new XunitException("Missing trace input manifest."); + using var identity = JsonDocument.Parse(json); + var fields = identity.RootElement; + var sourcePath = Path.Combine(root, "cputest-trace.cpp"); + var normalizedSourceHash = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData( + System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(sourcePath).Replace("\r\n", "\n")))).ToLowerInvariant(); + if (manifest.Schema != 1 || manifest.GeneratorCommit != GeneratorPin || manifest.RunnerCommit != RunnerPin || + fields.GetProperty("Preset").GetString() != "TraceTraps" || + normalizedSourceHash != TracePresetNormalizedSourceHash || + fields.GetProperty("TracePriorityPatchSha256").GetString() != TracePresetPatchHash || + Hash(sourcePath) != fields.GetProperty("TracePrioritySourceSha256").GetString() || + Hash(Path.Combine(root, "trace-priority.patch")) != TracePresetPatchHash || + Hash(Path.Combine(root, "cputester.exe")) != fields.GetProperty("GeneratorExecutableSha256").GetString() || + Hash(library) != manifest.NativeLibrarySha256) + throw new XunitException("Trace preset source, generator, patch or native identity is unqualified."); + if (manifest.Profiles.Length != 2 || !manifest.Profiles.Select(x => x.Id).Order().SequenceEqual(new[] { "68040", "68060" })) + throw new XunitException("Trace audit requires both 040 and 060, without empty or duplicate selections."); + var models = ModelSpec.All.Where(x => x.Id is "68040" or "68060").ToArray(); + foreach (var model in models) ValidateWinUaeProfile(root, manifest, model, traceTraps: true); + var rows = new List(); + var failures = new List(); + using var tester = NativeTester.Load(library); + foreach (var model in models) + { + var fixture = manifest.Profiles.Single(x => x.Id == model.Id); + var path = Path.Combine(root, model.Id); + // The focused preset must still compare registers, defined X and frames. + var registerProbe = tester.Run(path, "TRAP", fixture.CpuLevel, false, false, model, corruptResult: true); + var srProbe = tester.Run(path, "TRAP", fixture.CpuLevel, false, false, model, corruptSr: 0x10); + var frameProbe = tester.Run(path, "TRAP", fixture.CpuLevel, false, false, model, corruptFrame: true); + var frameDetected = !frameProbe.Passed && tester.FrameChecks > 0 && frameProbe.Detail.Contains("frame byte", StringComparison.Ordinal); + var controls = !registerProbe.Passed && registerProbe.ExecutedCases > 0 && + !srProbe.Passed && srProbe.ExecutedCases > 0 && srProbe.Detail.Contains("SR:", StringComparison.Ordinal) && frameDetected; + var result = tester.Run(path, "TRAP", fixture.CpuLevel, false, false, model); + var passing = controls && result.Passed && result.ExecutedCases == 256 && tester.TraceInputCases == 128 && tester.FrameChecks == 256; + rows.Add(new(model.Id, "TRAP", passing ? "passing" : tester.UnsupportedExecution ? "unsupported" : "mismatching", + result.ExecutedCases, tester.TraceInputCases, tester.FrameChecks, controls, result.Detail)); + if (!passing) failures.Add($"{model.Id}: controls={controls}, traced={tester.TraceInputCases}, frames={tester.FrameChecks}; {result.Detail}"); + _output.WriteLine($"{model.Id}: {result.ExecutedCases} TRAP callbacks, {tester.TraceInputCases} with incoming T1, {tester.FrameChecks} frame checks."); + } + Directory.CreateDirectory(output); + File.WriteAllText(Path.Combine(output, "winuae-trap-trace-audit.json"), JsonSerializer.Serialize(new + { + schema = 1, reference = "WinUAE TRAP trace preset with explicit Motorola-qualified generator correction", + manifest.GeneratorCommit, manifest.RunnerCommit, manifest.NativeLibrarySha256, + manifestSha256 = Hash(manifestFile), generatorSourceSha256 = Hash(sourcePath), + generatorNormalizedSourceSha256 = TracePresetNormalizedSourceHash, generatorPatchSha256 = TracePresetPatchHash, + adapterAssemblySha256 = Hash(typeof(M68kWinUaeCpuTesterConformanceTests).Assembly.Location), + cpuAssemblySha256 = Hash(typeof(Copper68k.M68kCoreFactory).Assembly.Location), + qualification = "TRAP only, 040/060, incoming T1/S and CCR 0/31; frame 0 and defined integer effects. Patched reference, not unchanged upstream or silicon qualification. Other traced families/models remain untested by this preset.", + passing = rows.Count(x => x.Status == "passing"), mismatching = rows.Count(x => x.Status == "mismatching"), + unsupported = rows.Count(x => x.Status == "unsupported"), executedCases = rows.Sum(x => x.ExecutedCases), + traceInputCases = rows.Sum(x => x.TraceInputCases), exceptionFrames = rows.Sum(x => (long)x.ExceptionFrames), + rows + }, new JsonSerializerOptions { WriteIndented = true })); + Assert.True(failures.Count == 0, string.Join(Environment.NewLine, failures)); + } + + private sealed record WinUaeTrapTraceRow(string Model, string Family, string Status, int ExecutedCases, + int TraceInputCases, uint ExceptionFrames, bool Controls, string Detail); +} diff --git a/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs b/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs index 46a8f2bd..8297efe7 100644 --- a/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs @@ -76,9 +76,11 @@ public void TraceRetirementTakenAndUntakenFlowStopsAndAbortingFaults(string mode if (abort) SyntheticExecution.ExpectException(m, e, family == "ILLEGAL" ? 4 : 8); else { - if (family == "TRAP" || family == "TRAPV" && (ccr & 2) != 0) + var trap = family == "TRAP" || family == "TRAPV" && (ccr & 2) != 0; + if (trap) { SyntheticExecution.ExpectException(m, e, family == "TRAP" ? 35 : 7, e.Pc); flow = true; } - if (trace == 0x8000 || flow || family == "NOP" && modelId == "68040") SyntheticExecution.ExpectException(m, e, 9, e.Pc); + if (!(trap && modelId is "68040" or "68060") && + (trace == 0x8000 || flow || family == "NOP" && modelId == "68040")) SyntheticExecution.ExpectException(m, e, 9, e.Pc); } SyntheticExecution.Run(m, e, report, $"{modelId}/trace-{family}/none/T={trace:X4}/super={supervisor}/op={words[0]:X4}/ccr={ccr:X2}", false); } diff --git a/Copper68k.Tests/Synthetic/SyntheticTrapTraceTests.cs b/Copper68k.Tests/Synthetic/SyntheticTrapTraceTests.cs new file mode 100644 index 00000000..4b4cdb2d --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticTrapTraceTests.cs @@ -0,0 +1,140 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticTrapTraceTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void SynchronousTrapTracePriorityFramesAndReturn(string modelId) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); + var report = new CoverageBatch(modelId, "system-trap-trace"); + foreach (var trace in m.Model.FullIndex && modelId != "68060" ? new[] { 0, 0x8000, 0x4000 } : new[] { 0, 0x8000 }) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var instruction in Instructions()) + { + var (e, vector, nextPc) = Prepare(m, instruction, trace, supervisor, ccr); + var sequence = m.Core.State.ExceptionSequence; + if (vector != 0) SyntheticExecution.ExpectException(m, e, vector, vector == 4 ? SyntheticMachine.Code : nextPc); + // MC68040UM 8.3 and MC68060UM 8.2.6/8.3 suppress a pending + // trace when a synchronous trap wins. Earlier models retain it. + var pendingTrace = trace != 0 && vector != 4 && vector != 0 && modelId is not ("68040" or "68060"); + if (pendingTrace || vector == 0 && trace == 0x8000) SyntheticExecution.ExpectException(m, e, 9, e.Pc); + var entries = (vector == 0 ? 0u : 1u) + (pendingTrace || vector == 0 && trace == 0x8000 ? 1u : 0u); + e.ControlChecks["exception entries"] = (s => s.ExceptionSequence, sequence + entries); + SyntheticExecution.Run(m, e, report, Id(m, instruction, trace, supervisor, ccr) + "/entry", false); + } + if (modelId is "68040" or "68060") ReturnAndResume(m, report); + report.Complete(output); + } + + private sealed record Instruction(string Id, ushort[] Words, uint Dividend = 0x12345678, uint Source = 0); + + private static IEnumerable Instructions() + { + for (var n = 0; n < 16; n++) yield return new($"TRAP-{n}", [(ushort)(0x4e40 | n)]); + yield return new("TRAPV", [0x4e76]); + yield return new("DIVU.W-zero", [0x80c1]); + yield return new("DIVS.W-zero", [0x81c1]); + yield return new("DIVU.L-zero", [0x4c41, 0x0001]); + yield return new("DIVS.L-zero", [0x4c41, 0x0801]); + yield return new("CHK.W-negative", [0x4181], 0xffff, 8); + yield return new("CHK.W-above", [0x4181], 9, 8); + yield return new("CHK.L-negative", [0x4101], 0xffffffff, 8); + yield return new("CHK.L-above", [0x4101], 9, 8); + foreach (var condition in new[] { "T", "F" }) + { + var prefix = condition == "T" ? 0x5000 : 0x5100; + yield return new($"TRAP{condition}-none", [(ushort)(prefix | 0xfc)]); + yield return new($"TRAP{condition}-word", [(ushort)(prefix | 0xfa), 0x1357]); + yield return new($"TRAP{condition}-long", [(ushort)(prefix | 0xfb), 0x1357, 0x9bdf]); + } + } + + private static (ArchitecturalExpectation Expected, int Vector, uint NextPc) Prepare( + SyntheticMachine m, Instruction instruction, int trace, bool supervisor, int ccr, bool returnHandler = false) + { + m.Reset(ccr, supervisor); + m.Core.State.D[0] = instruction.Dividend; m.Core.State.D[1] = instruction.Source; + var vector = instruction.Id.StartsWith("TRAP-") ? 32 + (instruction.Words[0] & 15) : + instruction.Id == "TRAPV" ? (ccr & 2) != 0 ? 7 : 0 : + instruction.Id.StartsWith("DIV") ? 5 : instruction.Id.StartsWith("CHK") ? 6 : + instruction.Id.StartsWith("TRAPT") ? 7 : 0; + var unavailable = !m.Model.FullIndex && (instruction.Id.Contains(".L") || instruction.Id.StartsWith("TRAPT") || instruction.Id.StartsWith("TRAPF")); + if (unavailable) vector = 4; + if (returnHandler) m.InitializePhysical((uint)(0x9000 + vector * 0x10), 0x4e73, 2); + var words = returnHandler ? instruction.Words.Concat(new ushort[] { 0x60fe }).ToArray() : instruction.Words; + var e = SyntheticExecution.Prepare(m, words); + var nextPc = SyntheticMachine.Code + (uint)instruction.Words.Length * 2; + e.Pc = nextPc; + m.Core.State.StatusRegister |= (ushort)trace; e.Sr |= (ushort)trace; + if (!unavailable && instruction.Id.StartsWith("DIV")) + { + e.Sr &= 0xfffe; e.DefinedSrMask = 0xfff1; // N/Z/V undefined; X preserved, C clear. + } + if (!unavailable && instruction.Id.StartsWith("CHK")) + { + e.Sr = (ushort)((e.Sr & ~8) | (instruction.Id.EndsWith("negative") ? 8 : 0)); + e.DefinedSrMask = 0xfff8; // X preserved, trapping N defined; Z/V/C undefined. + } + return (e, vector, nextPc); + } + + private static void ReturnAndResume(SyntheticMachine m, CoverageBatch report) + { + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var instruction in Instructions().Where(x => x.Id.StartsWith("TRAP-") || x.Id.StartsWith("DIV") && x.Id.Contains(".W") || x.Id.StartsWith("CHK.W") || x.Id.StartsWith("TRAPT"))) + { + var (e, vector, nextPc) = Prepare(m, instruction, 0x8000, supervisor, ccr, true); + var restoredSr = e.Sr; + var sequence = m.Core.State.ExceptionSequence; + SyntheticExecution.ExpectException(m, e, vector, nextPc); + e.ControlChecks["exception entries"] = (s => s.ExceptionSequence, sequence + 1); + var id = Id(m, instruction, 0x8000, supervisor, ccr) + "/return"; + if (!Step(m, e, report, id + "/entry")) + { + report.Record(id + "/RTE", "untested", "Trap entry prerequisite failed"); + report.Record(id + "/resumed-BRA", "untested", "Trap entry prerequisite failed"); + continue; + } + e.A[7] += vector >= 32 ? 8u : 12u; + SyntheticSystemTests.ApplyStatus(m, e, restoredSr); + e.Pc = nextPc; e.ExceptionVector = null; + if (!Step(m, e, report, id + "/RTE")) + { + report.Record(id + "/resumed-BRA", "untested", "RTE prerequisite failed"); + continue; + } + // A restored T1 traces the following instruction, never RTE itself. + // The self-branch makes its traced instruction address unambiguous. + SyntheticExecution.ExpectException(m, e, 9, nextPc); + e.Write(e.A[7] + 8, nextPc, 4, m.Model); + e.ControlChecks["exception entries"] = (s => s.ExceptionSequence, sequence + 2); + Step(m, e, report, id + "/resumed-BRA"); + } + } + + private static bool Step(SyntheticMachine m, ArchitecturalExpectation e, CoverageBatch report, string id) + { + try + { + m.Core.ExecuteInstruction(); + var mismatch = e.Verify(m); + report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); + return mismatch == null; + } + catch (Exception ex) + { + report.Record(id, ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException or UnsupportedM68040InstructionException ? "unsupported" : "mismatching", ex.Message); + return false; + } + } + + private static string Id(SyntheticMachine m, Instruction instruction, int trace, bool supervisor, int ccr) => + $"{m.Model.Id}/trap-trace/{instruction.Id}/op={instruction.Words[0]:X4}/T={trace:X4}/super={supervisor}/ccr={ccr:X2}"; +} diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index 29c443c2..151c998b 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -4461,7 +4461,11 @@ traceOpcode is 0x4E72 or 0x4E73 or 0x4E74 or 0x4E75 or 0x4E77 or 0x007C or 0x027 // exception processing. STOP remains traceable and is handled below. if (State.Halted) return; var exception = State.ExceptionSequence != serial; - if (exception && State.LastExceptionVector is not (5 or 6 or 7 or >= 32 and <= 47)) return; + // MC68040UM 8.3 and MC68060UM 8.2.6/8.3: a synchronous + // exception suppresses the pending trace on 040/060. Earlier + // models stack a trace after a completed instruction trap. + if (exception && (_profile.Model is M68kAcceleratorModel.M68040 or M68kAcceleratorModel.M68060 || + State.LastExceptionVector is not (5 or 6 or 7 or >= 32 and <= 47))) return; if ((trace & 0x8000) != 0 || flow || exception) { State.Stopped = false; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 79d93bfe..1e2bba5e 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -1110,3 +1110,102 @@ Final evidence: `artifacts/m6-packing-cpu/`, `artifacts/m6-packing-references/`, `artifacts/synthetic-private-feed-44/`; isolated consumer `artifacts/packing-validation/`, `artifacts/packing-diagnostic-tests/` and `artifacts/packing-production.binlog`. No package is published. + +## Synchronous trap and trace priority — 2026-10-05 + +The shared advanced core stacked a pending trace immediately after completed +instruction traps on every advanced model. This is wrong for 040 and 060. +[MC68040UM 8.3, 8-20](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +suppresses trace when a priority-3 synchronous exception wins; +[MC68060UM 8.2.6/8.3, 8-11/8-18](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf) +also suppresses it. After RTE restores T1, the next executed instruction is +traced. Earlier models retain their existing nested trace behavior. MC68020UM +6.1.11 explicitly describes trap processing followed immediately by trace +processing; its 6.1.4 wording about tracing after the trap handler's RTE is +inconsistent with that multiple-exception description. The explicit priority +example and WinUAE's model distinction corroborate the retained ordering. + +The new `system-trap-trace` group contains **50,496 cases in eight reporting +batches**: 3,968 on 000/010, 5,952 on EC020/020/030/A1200, 10,368 on 040 and +8,384 on 060. It covers all TRAP vectors, taken/untaken TRAPV, word/long divide +by zero, word/long CHK negative/above-bound traps, and all three immediate +lengths of true/false TRAPcc. Canonical fixtures use all initial CCRs and both +user/supervisor stacks, with no trace, T1 and applicable T0. Unsupported old-model +forms require vector 4. Defined flags, preserved registers/memory, complete +frames and exact exception-entry counts are checked. 040/060 additionally +execute RTE from real format-0/2 trap frames and resume a traced self-branch. +M-mode, physical fault sequencing and advanced internal restart are not added +to this group. + +Against `4a32a1f`, the revised group detects **8,224 mismatches**, with **5,888 +dependent return/resume cases untested** until correct entry works. There are +zero emulator-unsupported cases. The corrected older trace fixture additionally +detects 192 failures on 040 and 96 on 060. The production fix suppresses the +second exception only on those models. It preserves the first frame, operand +effects and existing timing plans; there is no instruction retry or public API +change. All 16 synthetic batches pass after correction. + +The pinned WinUAE generator independently contains the same overgeneralized +trace rule. Its unchanged Basic preset never enables incoming trace rounds, +so its passing TRAP groups did not expose this. A separate `TraceTraps` preset +applies the committed one-line [generator patch](../scripts/winuae/trace-priority.patch) +to a copied source file. Original tracked sources and all existing Basic +fixture identities remain unchanged. The corrected source, patch and compiler +output have explicit hashes; normalized-text authority handles checkout line +endings while exact manifested bytes must still match. This is a qualified +correction to an independent software reference, not unchanged upstream or +hardware evidence. The pinned newer local WinUAE `newcpu.cpp` revision +`6ae6fb6b84bb9517e0245a80fc9bdca1a8580dde`, `exception_check_trace`, also retains +pending instruction-trap trace only below 040. + +The qualified preset passes **512 TRAP callbacks / 512 frame assertions**, +including **256 incoming-T1 callbacks**, across 040/060. It covers T1/S +combinations and CCR 0/31; it does not claim all CCRs externally. All six +register, defined-X and frame corruption controls pass. Temporarily removing +the CPU correction makes both models fail at their first incoming-T1 callback +(fifth callback per model), preserving eight earlier frame comparisons. Native +diagnostics report expected TRAP vector 32 versus actual trace vector 9 and +incorrect saved SR. Empty profile selection and changed fixture data are +rejected before native execution. The requested audit also requires exact +256/128/256 callback/trace/frame counts per model. Other traced families/models, +extra trace/fault records and M-mode remain untested by this focused preset. +Preparation and audit commands are in [WinUAE conformance](../Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md#qualified-trap-trace-preset-for-040060). + +The unchanged broad Basic audit still fails: **1,320 passing, 48 mismatching, +13 unsupported and zero untested groups** over 11,311,137 callbacks, with +1,500,873 frames, 199,327 masked-SR cases and one terminal callback. All 32 +controls pass. The focused trace preset does not replace it or exclude families. +STOP, saved-PC reference disagreements, reserved words, advanced restoration +and the other previously recorded gaps remain open. + +Full ordinary Release CPU validation passes **4,756 tests**, with ten optional +skips and zero failures. The final focused gate passes 23 tests, including the +new trace audit and retained input-preflight checks. The deterministic gate +validates **9,323,016 logical cases in 450 reporting batches** and rejects a +missing new trace report. Fresh SingleStepTests passes 312,500 cases / 125 files, +Musashi passes 536 programs with 88 exclusions and AHX passes 18 tests. +Source pins, exclusions and physical-timing caveats apply unchanged. + +Private **unpublished** NuGet `1.5.2-synthetic-dev.45` has SHA-256 +`f44f0f2f8b4566dc3fabd4749aca585bb84807e3202fb4a25b30c0b91e004d47`. +`artifacts/m6-trap-trace-package.json` records source/assembly identities against +`4a32a1f`. The isolated CopperScreen `d9beae8` baseline resolves the exact package +in production and separate diagnostics. All four loaded CPU DLLs match SHA-256 +`37e71d96a4cb26e4c2309197d56c846ee6a81d73b290362598cf7f3e6b7f7614`. +Release build has zero warnings/errors; host 149, disk 74 and engine diagnostics +1,080 pass. Three native Workbench/A1200 boot and A1200 disk-persistence replays +pass with no skips. Six optional host/media skips remain unavailable coverage. + +Evidence: `artifacts/m6-trap-trace-before/`, `artifacts/m6-trap-trace-after/`, +`artifacts/m6-trap-trace-qualified-inputs/`, +`artifacts/m6-trap-trace-reference-before/`, +`artifacts/m6-trap-trace-final-qualified/`, `artifacts/m6-trap-trace-cpu/`, +`artifacts/m6-trap-trace-references/`, `artifacts/m6-trap-trace-basic-winuae/`, +`artifacts/m6-trap-trace-guard-*/`, `artifacts/m6-trap-trace-ahx-results/` and +`artifacts/synthetic-private-feed-45/`; consumer +`artifacts/trap-trace-validation/`, `artifacts/trap-trace-diagnostic-tests/` +and `artifacts/trap-trace-production.binlog`. The initial preparation attempt +failed on a relative source-file path; the corrected generator preparation +completes and preserves that failed attempt as historical evidence. No package +publication, regression retirement, seeded, host-performance or physical audit +is added. Milestone 6 remains in progress with its accepted scope unchanged. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index c8b64ab4..1ca07ef6 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -799,3 +799,29 @@ package validates the isolated CopperScreen Release build, host/disk/engine tests and three native Workbench/A1200 replays. An additionally selected Workbench hard-disk theory without its HDF input is unavailable coverage. Exact package, source, assembly and replay evidence is in the reference document. + +The next checkpoint qualifies synchronous trap/trace priority. 040/060 suppress +the pending trace after a trap; RTE restores T1 and the following instruction +is traced. Earlier models keep their nested trace behavior. The new +`system-trap-trace` group adds 50,496 cases in eight batches, covering every +TRAP vector, TRAPV, word/long divide-by-zero and CHK, all immediate TRAPcc forms, +all CCRs, both stacks and applicable T1/T0. The corrected baseline detects +8,224 mismatches with 5,888 dependent return cases untested until entry works; +all cases now pass. Existing trace expectations are corrected on 040/060. + +A separately manifested WinUAE `TraceTraps` preset applies an explicit manual- +qualified generator correction and passes 512 callbacks / frames, including +256 incoming-T1 cases on 040/060. Removing the CPU fix makes both profiles fail +their first T1 callback. Six corruption controls pass; empty profiles, changed +inputs and omitted deterministic reports fail. Other traced reference families +and models remain explicitly untested by this preset. The broad Basic audit +still has 48 mismatching and 13 unsupported groups and remains required. + +Full CPU validation passes 4,756 tests with ten optional skips; the deterministic +gate validates 9,323,016 logical cases in 450 reporting batches. Fresh external +SingleStepTests/Musashi and AHX checks pass. The private unpublished .45 package +validates the isolated CopperScreen Release build, host/disk/engine tests and +three native replays without skips. The reference document records exact +manual/source caveats, pins, package/assembly identities, failed-before evidence +and remaining requirements. No old test is retired or package published; +milestone 6 remains in progress with the accepted scope intact. diff --git a/scripts/prepare-copper68k-winuae.ps1 b/scripts/prepare-copper68k-winuae.ps1 index 698e6522..a99739ff 100644 --- a/scripts/prepare-copper68k-winuae.ps1 +++ b/scripts/prepare-copper68k-winuae.ps1 @@ -4,6 +4,7 @@ param( [Parameter(Mandatory)] [string] $GeneratorSource, [Parameter(Mandatory)] [string] $RunnerSource, [Parameter(Mandatory)] [string] $VcVars64, + [ValidateSet('Basic','TraceTraps')] [string] $Preset = 'Basic', [string] $OutputDirectory = 'artifacts/winuae-model-inputs' ) $ErrorActionPreference = 'Stop' @@ -49,8 +50,22 @@ try { Run-Compiler @('/nologo','/O2','/EHsc','/w','/I.','/Iinclude','gencpu.cpp','missing.cpp','readcpu.cpp','cpudefs.cpp','/Fe:gencpu_prog.exe') (Join-Path $output 'gencpu-build.log') & ./gencpu_prog.exe . *> (Join-Path $output 'gencpu-run.log') if ($LASTEXITCODE -ne 0) { throw 'CPU source generation failed' } - $cpp = @('cpudefs.cpp','cpuemu_90_test.cpp','cpuemu_91_test.cpp','cpuemu_92_test.cpp','cpuemu_93_test.cpp','cpuemu_94_test.cpp','cpuemu_95_test.cpp','cputbl_test.cpp','cputest.cpp','cputest_support.cpp','disasm.cpp','fpp.cpp','fpp_softfloat.cpp','ini.cpp','newcpu_common.cpp','readcpu.cpp','softfloat/softfloat.cpp','softfloat/softfloat_decimal.cpp','softfloat/softfloat_fpsp.cpp') + $testerSource = 'cputest.cpp' + if ($Preset -eq 'TraceTraps') { + # The pinned generator retained pending trace on 040/060. Qualify + # this separate preset against MC68040UM 8.3 / MC68060UM 8.2.6. + # Keep the original source and every prior Basic input unchanged. + $patchPath = Join-Path $PSScriptRoot 'winuae/trace-priority.patch' + $patchLines = [IO.File]::ReadAllLines($patchPath) + $before = ($patchLines | Where-Object { $_.StartsWith('-') -and -not $_.StartsWith('---') }).Substring(1) + $after = ($patchLines | Where-Object { $_.StartsWith('+') -and -not $_.StartsWith('+++') }).Substring(1) + $testerSource = Join-Path $output 'cputest-trace.cpp' + [IO.File]::WriteAllText($testerSource, (Patch-Once ([IO.File]::ReadAllText((Join-Path $generator 'cputest.cpp'))) $before $after)) + [IO.File]::WriteAllText((Join-Path $output 'trace-priority.patch'), ([IO.File]::ReadAllText($patchPath)).Replace("`r`n", "`n")) + } + $cpp = @('cpudefs.cpp','cpuemu_90_test.cpp','cpuemu_91_test.cpp','cpuemu_92_test.cpp','cpuemu_93_test.cpp','cpuemu_94_test.cpp','cpuemu_95_test.cpp','cputbl_test.cpp',$testerSource,'cputest_support.cpp','disasm.cpp','fpp.cpp','fpp_softfloat.cpp','ini.cpp','newcpu_common.cpp','readcpu.cpp','softfloat/softfloat.cpp','softfloat/softfloat_decimal.cpp','softfloat/softfloat_fpsp.cpp') Run-Compiler (@('/nologo','/O2','/EHsc','/w','/I.','/Iinclude','/Icputest','/I../zlib','/DCPUEMU_90','/DCPUEMU_91','/DCPUEMU_92','/DCPUEMU_93','/DCPUEMU_94','/DCPUEMU_95','/DCPU_TESTER') + $cpp + @('/Fe:cputester.exe')) (Join-Path $output 'cputester-build.log') + if ($Preset -eq 'TraceTraps') { Copy-Item -LiteralPath 'cputester.exe' -Destination $output } } finally { Pop-Location } $native = [IO.File]::ReadAllText((Join-Path $vendor 'm68k_cpu_tester.c')) @@ -92,12 +107,16 @@ void M68KTester_destroy(M68KTesterContext* context) { $profiles = @() $baseIni = [IO.File]::ReadAllText((Join-Path $PSScriptRoot 'winuae/cputestgen.ini')).Replace("`r`n", "`n") + if ($Preset -eq 'TraceTraps') { + $baseIni = $baseIni.Replace('[test=Basic]', '[test=TraceTraps]').Replace('mode=all', 'mode=TRAP').Replace('feature_sr_mask=0x0000', 'feature_sr_mask=0xa000') + } $index = 0 foreach ($model in @( @{id='68000'; cpu='68000'; width=24}, @{id='68010'; cpu='68010'; width=24}, @{id='68EC020'; cpu='68020'; width=24}, @{id='68020'; cpu='68020'; width=32}, @{id='68030'; cpu='68030'; width=32}, @{id='68040'; cpu='68040'; width=32}, @{id='68060'; cpu='68060'; width=32})) { + if ($Preset -eq 'TraceTraps' -and $model.id -notin @('68040','68060')) { continue } $profileRoot = Join-Path $output $model.id New-Item -ItemType Directory -Path $profileRoot | Out-Null # Every invocation uses a fresh generator output path; stale data cannot fill a gap. @@ -109,14 +128,14 @@ void M68KTester_destroy(M68KTesterContext* context) { [IO.File]::WriteAllText((Join-Path $generator 'cputestgen.ini'), $config) Push-Location $generator try { - $generated = $prefix + $model.cpu + '_Basic' + $generated = $prefix + $model.cpu + '_' + $Preset New-Item -ItemType Directory -Path $generated -Force | Out-Null & ./cputester.exe *> (Join-Path $profileRoot 'generation.log') if ($LASTEXITCODE -ne 0) { throw "Fixture generation failed: $($model.id)" } Copy-Item -LiteralPath $generated -Destination (Join-Path $profileRoot $model.cpu) -Recurse } finally { Pop-Location } $profiles += @{ - Id=$model.id; CpuDirectory=$model.cpu; CpuLevel=$index; AddressBits=$model.width + Id=$model.id; CpuDirectory=$model.cpu; CpuLevel=$(if ($Preset -eq 'TraceTraps') { if ($model.id -eq '68040') {4} else {5} } else {$index}); AddressBits=$model.width Opcodes=@(Get-ChildItem -LiteralPath (Join-Path $profileRoot $model.cpu) -Directory | Select-Object -ExpandProperty Name | Sort-Object) Inputs=@(Get-ChildItem -LiteralPath $profileRoot -Recurse -File -Filter '*.dat' | Sort-Object FullName | ForEach-Object { @{Path=[IO.Path]::GetRelativePath($profileRoot,$_.FullName).Replace('\','/'); Bytes=$_.Length; Sha256=(Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant()} @@ -126,11 +145,14 @@ void M68KTester_destroy(M68KTesterContext* context) { } @{ Schema=1; GeneratorCommit=$generatorPin; RunnerCommit=$runnerPin; Profiles=$profiles + Preset=$Preset + TracePrioritySourceSha256=$(if ($Preset -eq 'TraceTraps') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-trace.cpp')).Hash.ToLowerInvariant()} else {$null}) + TracePriorityPatchSha256=$(if ($Preset -eq 'TraceTraps') {(Get-FileHash -LiteralPath (Join-Path $output 'trace-priority.patch')).Hash.ToLowerInvariant()} else {$null}) NativeLibrarySha256=(Get-FileHash -LiteralPath (Join-Path $output 'm68k_cpu_tester.dll') -Algorithm SHA256).Hash.ToLowerInvariant() Compiler=$compiler; GeneratorExecutableSha256=(Get-FileHash -LiteralPath (Join-Path $generator 'cputester.exe') -Algorithm SHA256).Hash.ToLowerInvariant() NativeSourceSha256=(Get-FileHash -LiteralPath (Join-Path $output 'winuae-native.c') -Algorithm SHA256).Hash.ToLowerInvariant() IntegerValidationSha256=(Get-FileHash -LiteralPath (Join-Path $output 'integer_validation.h') -Algorithm SHA256).Hash.ToLowerInvariant() - Seed='Pinned generator xorshift state initialized to 1 per test set; one Basic round' + Seed="Pinned generator xorshift state initialized to 1 per test set; one $Preset round" } | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'manifest.json') -Encoding utf8 Write-Host "Prepared input manifest and native bridge: $output" } finally { diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 20e75d5b..d1e9bb5e 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -92,6 +92,7 @@ try { 'logical-cas2'=36864; 'logical-shifts'=$(if ($model -in @('68000','68010')) {130561} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {131089}); 'system-basic'=1728; + 'system-trap-trace'=$(if ($model -eq '68040') {10368} elseif ($model -eq '68060') {8384} elseif ($model -in @('68000','68010')) {3968} else {5952}); 'system-chk-invalid-operands'=8192; 'system-debug-instructions'=$(if ($model -eq '68060') {512} else {256}); 'system-bounds'=$(if ($model -in @('68000','68010')) {11828} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {12488}); diff --git a/scripts/winuae/trace-priority.patch b/scripts/winuae/trace-priority.patch new file mode 100644 index 00000000..b7ca0963 --- /dev/null +++ b/scripts/winuae/trace-priority.patch @@ -0,0 +1,5 @@ +--- a/gencpu/cputest.cpp ++++ b/gencpu/cputest.cpp +@@ +- if ((regs.t1 || regs.t0) && (test_exception == 5 || test_exception == 6 || test_exception == 7 || (test_exception >= 32 && test_exception <= 47) || (cpu_lvl == 1 && test_exception == 14))) { ++ if (cpu_lvl < 4 && (regs.t1 || regs.t0) && (test_exception == 5 || test_exception == 6 || test_exception == 7 || (test_exception >= 32 && test_exception <= 47) || (cpu_lvl == 1 && test_exception == 14))) { From e4f8bb6e9e86a5e326f4a2956bb60ff8ae8b31a8 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 02:52:03 +0300 Subject: [PATCH 018/211] Qualify 68040 and 68060 translation control masks --- Copper68k.Tests/M68040MmuTests.cs | 6 +- .../Synthetic/SyntheticModelSystemTests.cs | 2 +- .../SyntheticTranslationControlTests.cs | 78 +++++++++++++++ Copper68k/M68040Support.cs | 7 +- Copper68k/M68060Interpreter.cs | 5 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 96 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 27 ++++++ scripts/test-copper68k-synthetic.ps1 | 1 + 8 files changed, 214 insertions(+), 8 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticTranslationControlTests.cs diff --git a/Copper68k.Tests/M68040MmuTests.cs b/Copper68k.Tests/M68040MmuTests.cs index 2d7a82e9..3c065a5d 100644 --- a/Copper68k.Tests/M68040MmuTests.cs +++ b/Copper68k.Tests/M68040MmuTests.cs @@ -18,13 +18,13 @@ public void MovecTransfersM68040MmuControlRegisters() 0x4E7A, 0x1003); // MOVEC TC,D1 var cpu = new M68040Interpreter(bus, M68020CpuProfile.Ocs68040Accelerator25Mhz); cpu.Reset(CodeBase, StackBase); - cpu.State.D[0] = 0x0000_1234; + cpu.State.D[0] = 0x0000_4000; // Implemented page-size bit; translation disabled. cpu.ExecuteInstruction(); cpu.ExecuteInstruction(); - Assert.Equal(0x0000_1234u, cpu.State.M68040Mmu.TranslationControl); - Assert.Equal(0x0000_1234u, cpu.State.D[1]); + Assert.Equal(0x0000_4000u, cpu.State.M68040Mmu.TranslationControl); + Assert.Equal(0x0000_4000u, cpu.State.D[1]); } [Fact] diff --git a/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs b/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs index 406378ab..0d4c55b2 100644 --- a/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs @@ -71,7 +71,7 @@ public void MovecControlInventoryMasksPrivilegeAndAllGeneralRegisters(string mod if (store) { var source = general < 8 ? e.D[general] : e.A[general - 8]; - var masked = control switch { 0 or 1 => source & 7, 2 when modelId == "68060" => source & 0xf880e000, 2 when modelId == "68040" => source & 0x80008000, 2 => source & (modelId == "68030" ? 0x3313u : 3u), 8 => source & 0xf0000000, 0x808 => 0x04300000 | source & 0x83, 0x806 or 0x807 when modelId == "68060" => source & 0xfffffe00, _ => source }; + var masked = control switch { 0 or 1 => source & 7, 2 when modelId == "68060" => source & 0xf880e000, 2 when modelId == "68040" => source & 0x80008000, 2 => source & (modelId == "68030" ? 0x3313u : 3u), 3 when modelId == "68040" => source & 0xc000, 3 when modelId == "68060" => source & 0xfffe, 8 => source & 0xf0000000, 0x808 => 0x04300000 | source & 0x83, 0x806 or 0x807 when modelId == "68060" => source & 0xfffffe00, _ => source }; e.ControlChecks[$"control {control:X3}"] = (read, masked); if (control == 0x800) e.InactiveStackPointer = masked; if (control == 0x803) e.MasterStackPointer = masked; diff --git a/Copper68k.Tests/Synthetic/SyntheticTranslationControlTests.cs b/Copper68k.Tests/Synthetic/SyntheticTranslationControlTests.cs new file mode 100644 index 00000000..d06c7c72 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticTranslationControlTests.cs @@ -0,0 +1,78 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticTranslationControlTests(ITestOutputHelper output) +{ + [Theory, InlineData("68040"), InlineData("68060"), Trait("Suite", "Synthetic")] + public void MovecTranslationControlDisabledWritesReadbackAndReservedBits(string modelId) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); + var report = new CoverageBatch(modelId, "system-translation-control"); + // MC68040UM 3.1.2 / figure 3-4; MC68060UM 4.1.2 / figure 4-4. + // Expectations are fixed architectural masks, not production MMU helpers. + var mask = modelId == "68040" ? 0xc000u : 0xfffeu; + foreach (var value in Values()) + for (var general = 0; general < 15; general++) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + m.Reset(ccr, supervisor); + if (general < 8) m.Core.State.D[general] = value; + else m.Core.State.A[general - 8] = value; + var e = SyntheticExecution.Prepare(m, [0x4e7b, (ushort)(general << 12 | 3), 0x4e7a, (ushort)(general << 12 | 3)]); + e.Pc = SyntheticMachine.Code + 4; + var id = $"{modelId}/MOVEC/TC/R{general}/value={value:X8}/super={supervisor}/ccr={ccr:X2}"; + if (!supervisor) + { + SyntheticExecution.ExpectException(m, e, 8); + e.ControlChecks["TC untouched"] = (s => s.M68040Mmu.TranslationControl, 0); + SyntheticExecution.Run(m, e, report, id + "/privilege", false); + continue; + } + e.ControlChecks["TC stored"] = (s => s.M68040Mmu.TranslationControl, value & mask); + if (!Step(m, e, report, id + "/write")) + { + report.Record(id + "/read", "untested", "TC write failed; dependent readback not executed."); + continue; + } + e.Pc += 4; + if (general < 8) e.D[general] = value & mask; + else e.A[general - 8] = value & mask; + SyntheticExecution.Run(m, e, report, id + "/read"); + } + // Reading unimplemented bits must return zero even for an internally + // supplied state. Avoid the legacy private MMU enable bit (31) here. + foreach (var value in Values().Where(x => (x & 0x80000000) == 0)) + for (var general = 0; general < 16; general++) + for (var ccr = 0; ccr < 32; ccr++) + { + m.Reset(ccr); + var e = SyntheticExecution.Prepare(m, [0x4e7a, (ushort)(general << 12 | 3)]); + m.Core.State.M68040Mmu.TranslationControl = value; + if (general < 8) e.D[general] = value & mask; + else e.A[general - 8] = value & mask; + e.ControlChecks["read has no TC side effect"] = (s => s.M68040Mmu.TranslationControl, value); + SyntheticExecution.Run(m, e, report, $"{modelId}/MOVEC/TC/read-R{general}/internal={value:X8}/ccr={ccr:X2}"); + } + report.Complete(output); + } + + private static uint[] Values() => new uint[] { 0, 1, 0x4000, 0x7fff, 0xffff7fff, 0x1234, 0x7100, 0x55555555, 0xaaaa2aaa } + .Concat(Enumerable.Range(0, 32).Where(bit => bit != 15).Select(bit => 1u << bit)).Distinct().Order().ToArray(); + + private static bool Step(SyntheticMachine m, ArchitecturalExpectation e, CoverageBatch report, string id) + { + try + { + m.Core.ExecuteInstruction(); + var mismatch = e.Verify(m); + report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); + return mismatch == null; + } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException or UnsupportedM68040InstructionException) + { report.Record(id, "unsupported", ex.Message); return false; } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); return false; } + } +} diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index 15750a4b..86ef9fd0 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -2655,7 +2655,10 @@ protected override bool TryReadControlRegister(int register, uint instructionPc, switch (register) { case 0x003: - value = State.M68040Mmu.TranslationControl; + // MC68040UM 3.1.2: only E/P are implemented; reserved + // bits read as zero. Keep private MMU state conventions + // separate from the architectural MOVEC register image. + value = State.M68040Mmu.TranslationControl & 0xC000; return true; case 0x004: value = State.M68040Mmu.InstructionTransparentTranslation0; @@ -2694,7 +2697,7 @@ protected override bool TryWriteControlRegister(int register, uint value, uint i case 0x002: return base.TryWriteControlRegister(register, value & 0x8000_8000u, instructionPc); case 0x003: - State.M68040Mmu.TranslationControl = value; + State.M68040Mmu.TranslationControl = value & 0xC000; State.M68040Mmu.Flush(); return true; case 0x004: diff --git a/Copper68k/M68060Interpreter.cs b/Copper68k/M68060Interpreter.cs index d5febf71..0ee31bcc 100644 --- a/Copper68k/M68060Interpreter.cs +++ b/Copper68k/M68060Interpreter.cs @@ -195,7 +195,8 @@ protected override bool TryReadControlRegister(int register, uint pc, out uint v case 0x008: value = State.M68060BusControl; return true; case 0x800: value = State.UserStackPointer; return true; case 0x808: value = State.M68060ProcessorConfiguration; return true; - case 0x003: value = State.M68040Mmu.TranslationControl; return true; + // MC68060UM 4.1.2: bits 31-16 and bit 0 always read zero. + case 0x003: value = State.M68040Mmu.TranslationControl & 0xFFFE; return true; case 0x004: value = State.M68040Mmu.InstructionTransparentTranslation0; return true; case 0x005: value = State.M68040Mmu.InstructionTransparentTranslation1; return true; case 0x006: value = State.M68040Mmu.DataTransparentTranslation0; return true; @@ -223,7 +224,7 @@ protected override bool TryWriteControlRegister(int register, uint value, uint p State.M68060ProcessorConfiguration = 0x0430_0000 | (value & 0x83); return true; case 0x003: if ((value & 0x8000) != 0) throw Unavailable("enabled MMU translation", State.LastOpcode); - State.M68040Mmu.TranslationControl = value & 0xFFFF; return true; + State.M68040Mmu.TranslationControl = value & 0xFFFE; return true; case 0x004: case 0x005: case 0x006: case 0x007: if ((value & 0x8000) != 0) throw Unavailable("enabled transparent translation", State.LastOpcode); if (register == 4) State.M68040Mmu.InstructionTransparentTranslation0 = value; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 1e2bba5e..5d40239c 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -1209,3 +1209,99 @@ failed on a relative source-file path; the corrected generator preparation completes and preserves that failed attempt as historical evidence. No package publication, regression retirement, seeded, host-performance or physical audit is added. Milestone 6 remains in progress with its accepted scope unchanged. + +### Translation-control MOVEC register image — 2026-10-05 + +The broad audit stopped in `MOVEC2` at callback 31 on both 040 and 060. +040 wrote `FFFF7FFF` into TC without masking it, accidentally activating the +private MMU state's high-bit enable convention and faulting before readback. +060 returned `00007FFF`, including reserved bit zero, instead of `00007FFE`. +[MC68040UM 3.1.2 / figure 3-4](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +defines the implemented E/P bits and zero reads for the remaining bits. +[MC68060UM 4.1.2 / figure 4-4](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf) +defines zero reads for bits 31–16 and bit 0. Architectural MOVEC reads/writes +now use fixed masks `0000C000` and `0000FFFE`, respectively. The existing private +MMU state conventions and cache/ATC execution policy are retained; this does +not qualify enabled MMU translation, ATC flushing or physical timing. + +The prior canonical MOVEC fixture also expected an unmasked 040 TC write. +Its expectation is corrected; the retained MMU register-transfer regression +now uses the implemented page-size bit with translation disabled. No test is +retired. New independent `system-translation-control` batches contain +**145,280 cases**, 72,640 per model. They exercise 38 deterministic write +values (boundaries and walking bits), D0–D7/A0–A6, all 32 CCR states, privilege +rejection without TC changes, dependent readback and a following sentinel. +Another 35 internally supplied read values cover every general register, +including A7, and verify reads have no register-state side effect. The retained +canonical inventory covers the A7 write. Architectural E remains clear in the +new inputs; the raw-state read cases also avoid the private enable bit. +Nonzero writes to reserved bits are robustness samples and emulator storage +canonicalization checks, not claims about a legal hardware programming +sequence: both manuals require reserved bits to be written as zero. The +independent read expectations enforce the documented zero-read behavior. + +Against production `8ffc8df`, these batches record **80,768 passing, +45,312 mismatching, zero unsupported and 19,200 untested dependent phases**. +All 145,280 pass after the correction. A failed write never causes a partial +instruction retry or a claimed readback pass. The ordinary report gate requires +both new batches with their exact counts and rejects omission of the 040 report. + +The current unchanged broad WinUAE inputs/bridge still fail, with **1,320 +passing, 48 mismatching, 13 unsupported and zero untested groups**. They execute +11,311,153 callbacks, 1,500,881 frame assertions, 199,327 masked-SR cases and one +terminal callback; all 32 controls pass. Both `MOVEC2` failures advance eight +callbacks, from 31 to 39, and now stop at ITT0 (`FFFF6364` expected, +`FFFF7FFF` actual). Transparent-translation and root-pointer register masks +remain open; neither complete MOVEC-family qualification nor a green broad +audit is claimed. An initial run used the older frame-only bridge, producing +25 additional trace-record mismatches. That result is retained in +`artifacts/m6-tc-winuae/`; the comparable current result is +`artifacts/m6-tc-current-winuae/` using the earlier Basic manifest +`37cd8ddae8b61ac60e3a49ba835362fbf80d418f48c2539338e6541c9d85e31f` +and native library +`75f0c11352d4a8c1e84f32a49a5347ae4cb5bd9128f1501a33d843e6f99cb057`. +No inputs or comparison masks are altered to obtain this progress. + +Ordinary Release CPU validation passes **4,758 tests**, with ten optional +skips and zero failures; the focused MOVEC/MMU gate passes 26 tests. The report +gate validates **9,468,296 cases in 452 batches**, with `roadmapComplete=false`. +Pinned SingleStepTests passes 312,500 cases in 125 files; pinned Musashi passes +536 programs with 88 explicit exclusions. Input hashes, source pins and complete +selection are rechecked by the report/reference script. The existing qualified +040/060 TRAP trace audit passes 512 callbacks / 256 incoming-T1 cases / 512 +frames and all six controls against this CPU. AHX passes 18 tests. + +Private **unpublished** package `1.5.2-synthetic-dev.46` has SHA-256 +`d7ee51601e409f4f0663deda3d00667700d1bc5db881fb3ce47bd13e8972b110`; +`artifacts/m6-tc-package.json` records both changed production sources and the +tested CPU DLL identity +`b39bae5cfe09e45c124ae9a705d4e33b014936234dc9e558e548174d30103e4f`. +The isolated CopperScreen baseline `d9beae8` resolves that exact package in +production and separate diagnostics. All four loaded DLLs match. Release build +has zero warnings/errors; host 149, disk 74, engine 1,080 and three native +Workbench/A1200 boot and disk-persistence replays pass. Six optional host/media +skips remain unavailable coverage. The first host invocation used a nonexistent +project path and did not execute tests; its corrected invocation passes. + +Evidence: `artifacts/m6-tc-before/`, `artifacts/m6-tc-focused/`, +`artifacts/m6-tc-cpu/`, `artifacts/m6-tc-current-winuae/`, +`artifacts/m6-tc-trace-reference/`, `artifacts/m6-tc-missing-report-all/`, +`artifacts/m6-tc-ahx-results/` and `artifacts/synthetic-private-feed-46/`; +consumer `artifacts/tc-validation/`, `artifacts/tc-diagnostic-tests/` and +`artifacts/tc-production.binlog`. The initial missing-report probe lacked other +models' reports and did not isolate the new gate; the final probe includes every +other required batch and fails specifically for the missing 040 TC report. + +The 010 RTE investigation also distinguishes two unresolved observations. +The Basic corpus fails on format-error N/Z/V changes, not incoming trace; +the pinned generator explicitly derives these flags from the rejected format +word. Current WinUAE source (`6ae6fb6b84bb9517e0245a80fc9bdca1a8580dde`, +`newcpu.cpp`, `exception_check_trace`) and pinned generator `025b999` retain +trace for a 010 format error. MC68000UM 6.3.8 describes instruction-forced +exceptions preceding trace, while 6.4 calls version rejection an aborted RTE. +Those passages do not establish every partial CCR/trace effect at every +validation stage. No production or expectation change is made on this evidence +alone. Stage-specific qualified fixtures or verified hardware evidence remain +required. All earlier restoration, reference, adapter and consolidation gaps +remain open. No release, regression retirement or physical audit is added; +milestone 6 remains in progress with its full scope unchanged. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 1ca07ef6..aee81dd1 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -825,3 +825,30 @@ three native replays without skips. The reference document records exact manual/source caveats, pins, package/assembly identities, failed-before evidence and remaining requirements. No old test is retired or package published; milestone 6 remains in progress with the accepted scope intact. + +The translation-control follow-up corrects 040/060 MOVEC TC register images +against their processor manuals. Reserved bits read zero; disabled writes are +canonicalized to `0000C000` / `0000FFFE`. A 040 reserved high bit no longer +activates the private MMU enable convention. This preserves private state and +timing policies and does not qualify enabled MMU behavior. Corrected canonical +expectations and the retained register-transfer regression accompany 145,280 +new cases in two `system-translation-control` batches. They cover boundaries, +walking bits, general registers, every CCR, privilege, dependent readback, +unchanged state and a following sentinel. Nonzero reserved-bit writes are +explicit robustness/storage-policy cases; the defined zero-read rule is the +architectural expectation. The new cases detect 45,312 mismatches before the +fix, with 19,200 dependent phases untested until the prerequisite works; all +pass afterward. Missing the new 040 report fails the complete gate. + +Full CPU validation passes 4,758 tests with ten optional skips. The report gate +validates 9,468,296 logical cases in 452 reporting batches. Pinned external +SingleStepTests/Musashi, AHX and the qualified 040/060 TRAP trace audit pass. +The broad WinUAE audit still fails 48 mismatching and 13 unsupported groups; +both MOVEC2 failures advance eight callbacks to the next ITT0 mask disagreement. +Transparent-translation/root-pointer masks and stage-specific 010 format-error +CCR/trace qualification remain open. The private unpublished .46 package +validates the isolated CopperScreen Release build, host/disk/engine tests and +three native replays. Exact source/input/package identities, failed-before +proof, initial invalid invocations and remaining scope are recorded in the +reference document. No package publication or regression retirement is added; +milestone 6 remains in progress with its full accepted scope unchanged. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index d1e9bb5e..a2b09ce7 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -109,6 +109,7 @@ try { 'system-status-invalid-operands'=2432; 'system-trace'=$(if ($model -in @('68000','68010','68060')) {1284} elseif ($model -in @('68020','68030','68040','68EC020','A1200')) {2568})} if ($model -eq '68000') { $expected['system-double-fault'] = 640 } + if ($model -in @('68040','68060')) { $expected['system-translation-control'] = 72640 } if ($model -eq '68010') { $expected['system-format8-entry'] = 1024 $expected['system-format8-rte'] = 4096 From 964266e5fa3f8f1f5b79d016b4bc6c805e92697a Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 03:13:10 +0300 Subject: [PATCH 019/211] Qualify 68040 and 68060 transparent control registers --- .../Synthetic/SyntheticModelSystemTests.cs | 2 +- .../SyntheticMovecRegisterFixture.cs | 89 +++++++++++++++++++ .../SyntheticTranslationControlTests.cs | 65 ++------------ .../SyntheticTransparentControlTests.cs | 55 ++++++++++++ Copper68k/M68040Support.cs | 17 ++-- Copper68k/M68060Interpreter.cs | 10 ++- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 86 ++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 28 ++++++ scripts/test-copper68k-synthetic.ps1 | 4 + 9 files changed, 283 insertions(+), 73 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticTransparentControlTests.cs diff --git a/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs b/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs index 0d4c55b2..073db3d1 100644 --- a/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs @@ -71,7 +71,7 @@ public void MovecControlInventoryMasksPrivilegeAndAllGeneralRegisters(string mod if (store) { var source = general < 8 ? e.D[general] : e.A[general - 8]; - var masked = control switch { 0 or 1 => source & 7, 2 when modelId == "68060" => source & 0xf880e000, 2 when modelId == "68040" => source & 0x80008000, 2 => source & (modelId == "68030" ? 0x3313u : 3u), 3 when modelId == "68040" => source & 0xc000, 3 when modelId == "68060" => source & 0xfffe, 8 => source & 0xf0000000, 0x808 => 0x04300000 | source & 0x83, 0x806 or 0x807 when modelId == "68060" => source & 0xfffffe00, _ => source }; + var masked = control switch { 0 or 1 => source & 7, 2 when modelId == "68060" => source & 0xf880e000, 2 when modelId == "68040" => source & 0x80008000, 2 => source & (modelId == "68030" ? 0x3313u : 3u), 3 when modelId == "68040" => source & 0xc000, 3 when modelId == "68060" => source & 0xfffe, >= 4 and <= 7 when modelId is "68040" or "68060" => source & 0xffffe364, 8 => source & 0xf0000000, 0x808 => 0x04300000 | source & 0x83, 0x806 or 0x807 when modelId == "68060" => source & 0xfffffe00, _ => source }; e.ControlChecks[$"control {control:X3}"] = (read, masked); if (control == 0x800) e.InactiveStackPointer = masked; if (control == 0x803) e.MasterStackPointer = masked; diff --git a/Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs b/Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs new file mode 100644 index 00000000..d08e5f9e --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs @@ -0,0 +1,89 @@ +using Copper68k; + +namespace Copper68k.Tests.Synthetic; + +// Shared register-transfer fixture; masks and encodings are independent +// specifications, not results computed with production decoder/MMU helpers. +internal sealed record SyntheticMovecRegisterFixture(int Control, string Name, uint Mask, + Func Read, Action Initialize) +{ + private static readonly (string Name, Func Read)[] Preserved = + [ + ("TC", s => s.M68040Mmu.TranslationControl), + ("ITT0", s => s.M68040Mmu.InstructionTransparentTranslation0), + ("ITT1", s => s.M68040Mmu.InstructionTransparentTranslation1), + ("DTT0", s => s.M68040Mmu.DataTransparentTranslation0), + ("DTT1", s => s.M68040Mmu.DataTransparentTranslation1), + ("URP", s => s.M68040Mmu.UserRootPointer), + ("SRP", s => s.M68040Mmu.SupervisorRootPointer) + ]; + + internal void WritesAndReadback(SyntheticMachine m, CoverageBatch report, IEnumerable values, int storeRegisters = 16) + { + foreach (var value in values) + for (var general = 0; general < storeRegisters; general++) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + m.Reset(ccr, supervisor); + if (general < 8) m.Core.State.D[general] = value; + else if (general == 15) m.Core.State.SetActiveStackPointer(value); + else m.Core.State.A[general - 8] = value; + var e = SyntheticExecution.Prepare(m, [0x4e7b, (ushort)(general << 12 | Control), 0x4e7a, (ushort)(general << 12 | Control)]); + e.Pc = SyntheticMachine.Code + 4; + Preserve(m, e); + var id = $"{m.Model.Id}/MOVEC/L/{Name}/R{general}/value={value:X8}/super={supervisor}/ccr={ccr:X2}"; + if (!supervisor) + { + SyntheticExecution.ExpectException(m, e, 8); + SyntheticExecution.Run(m, e, report, id + "/privilege", false); + continue; + } + e.ControlChecks[Name] = (Read, value & Mask); + if (!Step(m, e, report, id + "/write")) + { + report.Record(id + "/read", "untested", $"{Name} write failed; dependent readback not executed."); + continue; + } + e.Pc += 4; + if (general < 8) e.D[general] = value & Mask; + else e.A[general - 8] = value & Mask; + SyntheticExecution.Run(m, e, report, id + "/read"); + } + } + + internal void RawReads(SyntheticMachine m, CoverageBatch report, IEnumerable values) + { + foreach (var value in values) + for (var general = 0; general < 16; general++) + for (var ccr = 0; ccr < 32; ccr++) + { + m.Reset(ccr); + var e = SyntheticExecution.Prepare(m, [0x4e7a, (ushort)(general << 12 | Control)]); + Initialize(m.Core.State, value); + Preserve(m, e); + if (general < 8) e.D[general] = value & Mask; + else e.A[general - 8] = value & Mask; + SyntheticExecution.Run(m, e, report, $"{m.Model.Id}/MOVEC/L/{Name}/read-R{general}/internal={value:X8}/ccr={ccr:X2}"); + } + } + + private static void Preserve(SyntheticMachine m, ArchitecturalExpectation e) + { + foreach (var (name, read) in Preserved) e.ControlChecks[name] = (read, read(m.Core.State)); + } + + private static bool Step(SyntheticMachine m, ArchitecturalExpectation e, CoverageBatch report, string id) + { + try + { + m.Core.ExecuteInstruction(); + var mismatch = e.Verify(m); + report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); + return mismatch == null; + } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException or UnsupportedM68040InstructionException) + { report.Record(id, "unsupported", ex.Message); return false; } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); return false; } + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticTranslationControlTests.cs b/Copper68k.Tests/Synthetic/SyntheticTranslationControlTests.cs index d06c7c72..3626125c 100644 --- a/Copper68k.Tests/Synthetic/SyntheticTranslationControlTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticTranslationControlTests.cs @@ -1,4 +1,3 @@ -using Copper68k; using Xunit.Abstractions; namespace Copper68k.Tests.Synthetic; @@ -11,68 +10,14 @@ public void MovecTranslationControlDisabledWritesReadbackAndReservedBits(string var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); var report = new CoverageBatch(modelId, "system-translation-control"); // MC68040UM 3.1.2 / figure 3-4; MC68060UM 4.1.2 / figure 4-4. - // Expectations are fixed architectural masks, not production MMU helpers. - var mask = modelId == "68040" ? 0xc000u : 0xfffeu; - foreach (var value in Values()) - for (var general = 0; general < 15; general++) - foreach (var supervisor in new[] { false, true }) - for (var ccr = 0; ccr < 32; ccr++) - { - m.Reset(ccr, supervisor); - if (general < 8) m.Core.State.D[general] = value; - else m.Core.State.A[general - 8] = value; - var e = SyntheticExecution.Prepare(m, [0x4e7b, (ushort)(general << 12 | 3), 0x4e7a, (ushort)(general << 12 | 3)]); - e.Pc = SyntheticMachine.Code + 4; - var id = $"{modelId}/MOVEC/TC/R{general}/value={value:X8}/super={supervisor}/ccr={ccr:X2}"; - if (!supervisor) - { - SyntheticExecution.ExpectException(m, e, 8); - e.ControlChecks["TC untouched"] = (s => s.M68040Mmu.TranslationControl, 0); - SyntheticExecution.Run(m, e, report, id + "/privilege", false); - continue; - } - e.ControlChecks["TC stored"] = (s => s.M68040Mmu.TranslationControl, value & mask); - if (!Step(m, e, report, id + "/write")) - { - report.Record(id + "/read", "untested", "TC write failed; dependent readback not executed."); - continue; - } - e.Pc += 4; - if (general < 8) e.D[general] = value & mask; - else e.A[general - 8] = value & mask; - SyntheticExecution.Run(m, e, report, id + "/read"); - } - // Reading unimplemented bits must return zero even for an internally - // supplied state. Avoid the legacy private MMU enable bit (31) here. - foreach (var value in Values().Where(x => (x & 0x80000000) == 0)) - for (var general = 0; general < 16; general++) - for (var ccr = 0; ccr < 32; ccr++) - { - m.Reset(ccr); - var e = SyntheticExecution.Prepare(m, [0x4e7a, (ushort)(general << 12 | 3)]); - m.Core.State.M68040Mmu.TranslationControl = value; - if (general < 8) e.D[general] = value & mask; - else e.A[general - 8] = value & mask; - e.ControlChecks["read has no TC side effect"] = (s => s.M68040Mmu.TranslationControl, value); - SyntheticExecution.Run(m, e, report, $"{modelId}/MOVEC/TC/read-R{general}/internal={value:X8}/ccr={ccr:X2}"); - } + var fixture = new SyntheticMovecRegisterFixture(3, "TC", modelId == "68040" ? 0xc000u : 0xfffeu, + s => s.M68040Mmu.TranslationControl, (s, value) => s.M68040Mmu.TranslationControl = value); + fixture.WritesAndReadback(m, report, Values(), storeRegisters: 15); + // Raw reads avoid the legacy private MMU enable bit (31). + fixture.RawReads(m, report, Values().Where(x => (x & 0x80000000) == 0)); report.Complete(output); } private static uint[] Values() => new uint[] { 0, 1, 0x4000, 0x7fff, 0xffff7fff, 0x1234, 0x7100, 0x55555555, 0xaaaa2aaa } .Concat(Enumerable.Range(0, 32).Where(bit => bit != 15).Select(bit => 1u << bit)).Distinct().Order().ToArray(); - - private static bool Step(SyntheticMachine m, ArchitecturalExpectation e, CoverageBatch report, string id) - { - try - { - m.Core.ExecuteInstruction(); - var mismatch = e.Verify(m); - report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); - return mismatch == null; - } - catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException or UnsupportedM68040InstructionException) - { report.Record(id, "unsupported", ex.Message); return false; } - catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); return false; } - } } diff --git a/Copper68k.Tests/Synthetic/SyntheticTransparentControlTests.cs b/Copper68k.Tests/Synthetic/SyntheticTransparentControlTests.cs new file mode 100644 index 00000000..bad43eed --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticTransparentControlTests.cs @@ -0,0 +1,55 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticTransparentControlTests(ITestOutputHelper output) +{ + public static IEnumerable TransparentRegisters => + from model in new[] { "68040", "68060" } from control in new[] { 4, 5, 6, 7 } select new object[] { model, control }; + public static IEnumerable RootRegisters => + from model in new[] { "68040", "68060" } from control in new[] { 0x806, 0x807 } select new object[] { model, control }; + + [Theory, MemberData(nameof(TransparentRegisters)), Trait("Suite", "Synthetic")] + public void MovecTransparentRegistersDisabledWritesAndZeroReadBits(string modelId, int control) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); + var report = new CoverageBatch(modelId, $"system-transparent-control-{control}"); + // MC68040UM 3.1.3 / figure 3-5; MC68060UM 4.1.3 / figure 4-5: + // bits 12-10, 7, 4, 3, 1 and 0 always read zero. E is clear. + var fixture = Fixture(control, 0xffffe364); + fixture.WritesAndReadback(m, report, TransparentValues()); + fixture.RawReads(m, report, TransparentValues()); + report.Complete(output); + } + + [Theory, MemberData(nameof(RootRegisters)), Trait("Suite", "Synthetic")] + public void MovecRootPointersPreserveLegalAlignedAddresses(string modelId, int control) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); + var report = new CoverageBatch(modelId, $"system-root-control-{control:X3}"); + // Both processor manuals 3.1.1/4.1.1 require bits 8-0 zero. + // Nonaligned writes are outside the architectural programming rule; + // do not impose 060's masking convention on 040 from that rule alone. + var fixture = Fixture(control, uint.MaxValue); + fixture.WritesAndReadback(m, report, RootValues()); + fixture.RawReads(m, report, RootValues()); + report.Complete(output); + } + + private static SyntheticMovecRegisterFixture Fixture(int control, uint mask) => control switch + { + 4 => new(control, "ITT0", mask, s => s.M68040Mmu.InstructionTransparentTranslation0, (s, v) => s.M68040Mmu.InstructionTransparentTranslation0 = v), + 5 => new(control, "ITT1", mask, s => s.M68040Mmu.InstructionTransparentTranslation1, (s, v) => s.M68040Mmu.InstructionTransparentTranslation1 = v), + 6 => new(control, "DTT0", mask, s => s.M68040Mmu.DataTransparentTranslation0, (s, v) => s.M68040Mmu.DataTransparentTranslation0 = v), + 7 => new(control, "DTT1", mask, s => s.M68040Mmu.DataTransparentTranslation1, (s, v) => s.M68040Mmu.DataTransparentTranslation1 = v), + 0x806 => new(control, "URP", mask, s => s.M68040Mmu.UserRootPointer, (s, v) => s.M68040Mmu.UserRootPointer = v), + 0x807 => new(control, "SRP", mask, s => s.M68040Mmu.SupervisorRootPointer, (s, v) => s.M68040Mmu.SupervisorRootPointer = v), + _ => throw new ArgumentOutOfRangeException(nameof(control)) + }; + + private static uint[] TransparentValues() => new uint[] { 0, 1, 0x4000, 0x7fff, 0xffff7fff, 0x1234, 0x7100, 0x55555555, 0xaaaa2aaa } + .Concat(Enumerable.Range(0, 32).Where(bit => bit != 15).Select(bit => 1u << bit)).Distinct().Order().ToArray(); + + private static uint[] RootValues() => new uint[] { 0, 0x200, 0x400, 0x800, 0x7000, 0x12345600, 0xfffffe00, 0x80000000 } + .Concat(Enumerable.Range(9, 23).Select(bit => 1u << bit)).Distinct().Order().ToArray(); +} diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index 86ef9fd0..22e8f4e1 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -2661,16 +2661,17 @@ protected override bool TryReadControlRegister(int register, uint instructionPc, value = State.M68040Mmu.TranslationControl & 0xC000; return true; case 0x004: - value = State.M68040Mmu.InstructionTransparentTranslation0; + // MC68040UM 3.1.3: reserved TTR bits always read zero. + value = State.M68040Mmu.InstructionTransparentTranslation0 & 0xFFFF_E364; return true; case 0x005: - value = State.M68040Mmu.InstructionTransparentTranslation1; + value = State.M68040Mmu.InstructionTransparentTranslation1 & 0xFFFF_E364; return true; case 0x006: - value = State.M68040Mmu.DataTransparentTranslation0; + value = State.M68040Mmu.DataTransparentTranslation0 & 0xFFFF_E364; return true; case 0x007: - value = State.M68040Mmu.DataTransparentTranslation1; + value = State.M68040Mmu.DataTransparentTranslation1 & 0xFFFF_E364; return true; case 0x805: value = State.M68040Mmu.Status; @@ -2701,19 +2702,19 @@ protected override bool TryWriteControlRegister(int register, uint value, uint i State.M68040Mmu.Flush(); return true; case 0x004: - State.M68040Mmu.InstructionTransparentTranslation0 = value; + State.M68040Mmu.InstructionTransparentTranslation0 = value & 0xFFFF_E364; State.M68040Mmu.Flush(); return true; case 0x005: - State.M68040Mmu.InstructionTransparentTranslation1 = value; + State.M68040Mmu.InstructionTransparentTranslation1 = value & 0xFFFF_E364; State.M68040Mmu.Flush(); return true; case 0x006: - State.M68040Mmu.DataTransparentTranslation0 = value; + State.M68040Mmu.DataTransparentTranslation0 = value & 0xFFFF_E364; State.M68040Mmu.Flush(); return true; case 0x007: - State.M68040Mmu.DataTransparentTranslation1 = value; + State.M68040Mmu.DataTransparentTranslation1 = value & 0xFFFF_E364; State.M68040Mmu.Flush(); return true; case 0x805: diff --git a/Copper68k/M68060Interpreter.cs b/Copper68k/M68060Interpreter.cs index 0ee31bcc..f4682b44 100644 --- a/Copper68k/M68060Interpreter.cs +++ b/Copper68k/M68060Interpreter.cs @@ -197,10 +197,11 @@ protected override bool TryReadControlRegister(int register, uint pc, out uint v case 0x808: value = State.M68060ProcessorConfiguration; return true; // MC68060UM 4.1.2: bits 31-16 and bit 0 always read zero. case 0x003: value = State.M68040Mmu.TranslationControl & 0xFFFE; return true; - case 0x004: value = State.M68040Mmu.InstructionTransparentTranslation0; return true; - case 0x005: value = State.M68040Mmu.InstructionTransparentTranslation1; return true; - case 0x006: value = State.M68040Mmu.DataTransparentTranslation0; return true; - case 0x007: value = State.M68040Mmu.DataTransparentTranslation1; return true; + // MC68060UM 4.1.3 has the same TTR zero-read mask as 040. + case 0x004: value = State.M68040Mmu.InstructionTransparentTranslation0 & 0xFFFF_E364; return true; + case 0x005: value = State.M68040Mmu.InstructionTransparentTranslation1 & 0xFFFF_E364; return true; + case 0x006: value = State.M68040Mmu.DataTransparentTranslation0 & 0xFFFF_E364; return true; + case 0x007: value = State.M68040Mmu.DataTransparentTranslation1 & 0xFFFF_E364; return true; case 0x806: value = State.M68040Mmu.UserRootPointer; return true; case 0x807: value = State.M68040Mmu.SupervisorRootPointer; return true; case 0x802: case 0x803: case 0x804: case 0x805: @@ -227,6 +228,7 @@ protected override bool TryWriteControlRegister(int register, uint value, uint p State.M68040Mmu.TranslationControl = value & 0xFFFE; return true; case 0x004: case 0x005: case 0x006: case 0x007: if ((value & 0x8000) != 0) throw Unavailable("enabled transparent translation", State.LastOpcode); + value &= 0xFFFF_E364; if (register == 4) State.M68040Mmu.InstructionTransparentTranslation0 = value; if (register == 5) State.M68040Mmu.InstructionTransparentTranslation1 = value; if (register == 6) State.M68040Mmu.DataTransparentTranslation0 = value; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 5d40239c..b3b345db 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -1305,3 +1305,89 @@ alone. Stage-specific qualified fixtures or verified hardware evidence remain required. All earlier restoration, reference, adapter and consolidation gaps remain open. No release, regression retirement or physical audit is added; milestone 6 remains in progress with its full scope unchanged. + +### Transparent-control and legal root-pointer MOVEC images — 2026-10-05 + +MOVEC now masks all four 040/060 transparent-translation registers to +`FFFFE364` on architectural reads and writes. [MC68040UM 3.1.3 / figure +3-5](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) and +[MC68060UM 4.1.3 / figure +4-5](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf) define bits 12–10, +7, 4, 3, 1 and 0 as always reading zero. Existing enabled-TTR rejection on 060, +040 cache/ATC policy and instruction execution order remain unchanged. Reserved +nonzero writes exercise robustness and storage canonicalization; they do not +claim compliant hardware programming. Enabled MMU/TTR translation and physical +timing remain outside this semantic qualification. + +A shared test-internal MOVEC fixture now serves TC, TTR and root-pointer tests. +It verifies registers, CCR, exact PC, privilege entry, untouched control state, +dependent readback and a following instruction sentinel. It computes expectations +from fixed encodings/masks without production decoder or MMU helpers. The two +existing TC batches retain their 145,280 logical cases and gain control-state +preservation checks; no old regression is retired. The older canonical MOVEC +fixture's unmasked TTR expectation is corrected independently. + +Eight `system-transparent-control-{4,5,6,7}` batches add **622,592 cases**: +77,824 per register/model, using 38 disabled values, every general register +including A7, both stacks, every CCR, privileged writes/readback and raw reads. +Four `system-root-control-{806,807}` batches add **221,184 cases**: 55,296 per +register/model with 27 legal aligned values and the same transfer dimensions. +Both manuals require root-pointer bits 8–0 to be written zero. That rule alone +does not define nonaligned write/read behavior: the pinned reference preserves +those bits on 040 and masks them on 060. This checkpoint qualifies legal aligned +transfers without changing root-pointer production behavior or claiming either +nonaligned convention as hardware authority. + +Before the TTR fix, against production `e4f8bb6`, the TTR groups record +**450,560 passing, 114,688 mismatching, zero unsupported and 57,344 untested +dependent readback cases**. All legal root and refactored TC groups already pass. +After correction, all **843,776 new cases** pass. A failed prerequisite write +never retries an instruction or claims a dependent readback pass. The focused +MOVEC/MMU gate passes 38 tests. + +The unchanged broad WinUAE audit advances 040 `MOVEC2` from its first ITT0 +failure to **16,384 passing callbacks**. The 060 sequence advances from callback +39 to callback 71 and now exposes BUSCR: after writing `FFFFFFFF`, the reference +expects `A0000000` while the CPU returns `F0000000`. Pinned reference code +preserves SL/SLE while MOVEC writes L/LE. BUSCR shadow-write and nested-exception +qualification remain open; no production correction is inferred solely from +that software agreement. The complete broad audit still fails, with **1,321 +passing, 47 mismatching, 13 unsupported and zero untested groups**, 11,327,530 +callbacks, 1,517,229 frame assertions, 199,327 masked-SR cases and one terminal +callback. All 32 comparator controls pass. Generator/runner pins, original +Basic manifest `37cd8ddae8b61ac60e3a49ba835362fbf80d418f48c2539338e6541c9d85e31f` +and library `75f0c11352d4a8c1e84f32a49a5347ae4cb5bd9128f1501a33d843e6f99cb057` +are unchanged. No family is excluded or comparison weakened to obtain progress. + +Ordinary Release CPU validation passes **4,770 tests**, with ten optional +skips and zero failures. The deterministic report gate validates **10,312,072 +logical cases in 464 reporting batches**, with `roadmapComplete=false`. +Omitting only the new 040 ITT0 report rejects the gate. Fresh pinned +SingleStepTests passes 312,500 cases in 125 files; pinned Musashi passes 536 +programs with 88 exclusions. The script rechecks source revisions, input +identities and complete selections. AHX passes 18 tests. The qualified 040/060 TRAP trace +audit passes 512 callbacks, 256 incoming-T1 cases, 512 frame assertions and +six comparator controls against this CPU; its original qualification caveats +apply unchanged. + +Private **unpublished** NuGet `1.5.2-synthetic-dev.47` has SHA-256 +`fee96e4233a340c0564f3975bb278fe0a94882ddb92073aac20fb21b63f87c20`. +`artifacts/m6-ttr-package.json` records both changed production source hashes +against `e4f8bb6` and CPU DLL identity +`b6311bcc1b25d5d18ed5a92780d589c3d9251f1efb0a2884b259ef85883adde0`. +The isolated CopperScreen `d9beae8` baseline resolves the exact package in +production and separate diagnostics; all four loaded CPU DLLs match. Release +build has zero warnings/errors. Host 149, disk 74, engine diagnostics 1,080 +and three native Workbench/A1200 boot and disk-persistence replays pass. +Six optional host/media skips remain unavailable coverage. + +Evidence includes `artifacts/m6-ttr-before/`, `artifacts/m6-ttr-focused/`, +`artifacts/m6-ttr-discovery/`, `artifacts/m6-ttr-winuae/`, +`artifacts/m6-ttr-full/`, `artifacts/m6-ttr-trace-reference/`, +`artifacts/m6-ttr-missing-report/`, `artifacts/m6-ttr-ahx-results/` +and `artifacts/synthetic-private-feed-47/`; +consumer `artifacts/ttr-validation/`, `artifacts/ttr-diagnostic-tests/` +and `artifacts/ttr-production.binlog`. All earlier restoration, 010 format-error, +reference-adapter and consolidation gaps remain open. No package publication, +regression retirement, seeded audit or host/physical timing qualification is +added; milestone 6 remains in progress with its accepted scope. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index aee81dd1..bf35aff6 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -852,3 +852,31 @@ three native replays. Exact source/input/package identities, failed-before proof, initial invalid invocations and remaining scope are recorded in the reference document. No package publication or regression retirement is added; milestone 6 remains in progress with its full accepted scope unchanged. + +The transparent-control checkpoint masks 040/060 ITT0/ITT1/DTT0/DTT1 MOVEC +images to `FFFFE364`, the processor manuals' defined zero-read bits. A shared +test-internal register fixture serves TC, TTR and root-pointer tests and checks +untouched control state as well as registers, CCR, PC, privilege, dependent +readback and sentinels. Eight new TTR batches add 622,592 cases; four legal +aligned root-pointer batches add 221,184. All 843,776 pass after the correction. +The TTR batches detect 114,688 mismatches before correction, with 57,344 +dependent cases explicitly untested. Refactored TC and legal root transfers +already pass against that baseline. Reserved writes are robustness samples; +nonaligned root-pointer behavior and enabled translation remain unqualified. + +The original broad WinUAE audit now passes the 040 MOVEC2 sequence and reaches +a later 060 BUSCR shadow-bit disagreement. Its overall result remains failing: +1,321 passing, 47 mismatching and 13 unsupported groups. No family is excluded +or expected state weakened. BUSCR, stage-specific 010 format-error behavior and +all previous restoration/reference/consolidation requirements remain open. +Milestone 6 stays in progress; no package publication or regression retirement +is included. Exact evidence and final validation are recorded in the reference +qualification document. + +Final validation passes 4,770 CPU tests with ten optional skips, and the gate +validates 10,312,072 logical cases in 464 reporting batches. Missing the new +040 ITT0 report fails the gate. Fresh pinned SingleStepTests/Musashi, AHX and +qualified TRAP trace checks pass. The private unpublished .47 package validates +the isolated CopperScreen Release build, host/disk/engine tests and three native +Workbench/A1200 replays. Exact input/source/package identities and qualification +limits remain in the reference document. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index a2b09ce7..d4a328b7 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -110,6 +110,10 @@ try { 'system-trace'=$(if ($model -in @('68000','68010','68060')) {1284} elseif ($model -in @('68020','68030','68040','68EC020','A1200')) {2568})} if ($model -eq '68000') { $expected['system-double-fault'] = 640 } if ($model -in @('68040','68060')) { $expected['system-translation-control'] = 72640 } + if ($model -in @('68040','68060')) { + foreach ($control in 4..7) { $expected["system-transparent-control-$control"] = 77824 } + foreach ($control in @('806','807')) { $expected["system-root-control-$control"] = 55296 } + } if ($model -eq '68010') { $expected['system-format8-entry'] = 1024 $expected['system-format8-rte'] = 4096 From ccac246620a3e45ac04af1e799297507c6630c65 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 03:33:20 +0300 Subject: [PATCH 020/211] Preserve 68060 lock shadows and validate MOVEC control fields --- .../Synthetic/SyntheticBusControlTests.cs | 142 ++++++++++++++++++ .../Synthetic/SyntheticModelSystemTests.cs | 4 +- .../SyntheticMovecControlEncodingTests.cs | 49 ++++++ .../SyntheticMovecRegisterFixture.cs | 18 ++- Copper68k/M68060Interpreter.cs | 8 +- Copper68k/M68kAdvancedTimingInterpreter.cs | 12 +- Copper68k/M68kCore.cs | 5 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 101 +++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 26 ++++ scripts/test-copper68k-synthetic.ps1 | 7 + 10 files changed, 359 insertions(+), 13 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticBusControlTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticMovecControlEncodingTests.cs diff --git a/Copper68k.Tests/Synthetic/SyntheticBusControlTests.cs b/Copper68k.Tests/Synthetic/SyntheticBusControlTests.cs new file mode 100644 index 00000000..85e465eb --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticBusControlTests.cs @@ -0,0 +1,142 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticBusControlTests(ITestOutputHelper output) +{ + private static readonly uint[] Shadows = [0, 0x10000000, 0x40000000, 0x50000000]; + + [Fact, Trait("Suite", "Synthetic")] + public void MovecBusControlWritesCommandsPreservesShadowsAndReadsAllBits() + { + var m = Machine(); var report = new CoverageBatch("68060", "system-bus-control"); + // MC68060UM 7.4: L/LE commands, SL/SLE exception snapshots. + // Preserve shadow writes as independently corroborated by pinned WinUAE + // newcpu_common.cpp, not by a production-derived expectation. + var fixture = new SyntheticMovecRegisterFixture(8, "BUSCR", 0xf0000000, + s => s.M68060BusControl, (s, v) => s.M68060BusControl = v, + (old, value) => (old & 0x50000000) | (value & 0xa0000000)); + foreach (var shadow in Shadows) fixture.WritesAndReadback(m, report, Values(), initial: shadow); + fixture.RawReads(m, report, Enumerable.Range(0, 16).Select(n => (uint)n << 28)); + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void BusControlTrapNestedTrapAndReturnsRetainLockSnapshots() + { + var m = Machine(); var report = new CoverageBatch("68060", "system-bus-control-exceptions"); + foreach (var image in Enumerable.Range(0, 16).Select(n => (uint)n << 28)) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + for (var trap = 0; trap < 16; trap++) + { + m.Reset(ccr, supervisor); + var firstHandler = 0x9000u + (uint)(32 + trap) * 16; + // Use TRAP #14 as nested vector, except when it is the first vector. + var nestedVector = trap == 14 ? 45 : 46; + var nestedPc = 0xd000u; + m.Bus.Initialize((uint)nestedVector * 4, nestedPc, 4); + m.InitializePhysical(firstHandler, (uint)(0x4e40 | nestedVector - 32), 2); + m.InitializePhysical(firstHandler + 2, 0x4e73, 2); + m.InitializePhysical(nestedPc, 0x4e73, 2); + var e = SyntheticExecution.Prepare(m, [(ushort)(0x4e40 | trap)]); + m.Core.State.M68060BusControl = image; + var originalSr = e.Sr; + var originalStack = e.A[7]; + var shadow = (image & 0x50000000) | ((image & 0xa0000000) >> 1); + var id = $"68060/BUSCR/trap={trap}/image={image:X8}/super={supervisor}/ccr={ccr:X2}"; + e.ControlChecks["BUSCR snapshot"] = (s => s.M68060BusControl, shadow); + SyntheticExecution.ExpectException(m, e, 32 + trap, SyntheticMachine.Code + 2); + if (!Step(m, e, report, id + "/entry")) { Untested(report, id, "nested", "nested-RTE", "outer-RTE"); continue; } + // MC68060AR section 5: processor never clears SL/SLE on nested entry. + var handlerSr = e.Sr; + SyntheticExecution.ExpectException(m, e, nestedVector, firstHandler + 2); + if (!Step(m, e, report, id + "/nested")) { Untested(report, id, "nested-RTE", "outer-RTE"); continue; } + e.A[7] += 8; e.Sr = handlerSr; e.Pc = firstHandler + 2; e.ExceptionVector = null; + if (!Step(m, e, report, id + "/nested-RTE")) { Untested(report, id, "outer-RTE"); continue; } + e.A[7] += 8; + SyntheticSystemTests.ApplyStatus(m, e, originalSr); + e.Pc = SyntheticMachine.Code + 2; + if (e.A[7] != originalStack) throw new InvalidOperationException("Independent return fixture stack imbalance"); + SyntheticExecution.Run(m, e, report, id + "/outer-RTE"); + } + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void BusControlPrivilegeTraceAndResetState() + { + var m = Machine(); var report = new CoverageBatch("68060", "system-bus-control-state"); + foreach (var image in Enumerable.Range(0, 16).Select(n => (uint)n << 28)) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var kind in new[] { "privilege-read", "privilege-write", "trace", "illegal" }) + { + var supervisor = kind is "trace" or "illegal"; + m.Reset(ccr, supervisor); + ushort[] words = kind switch { "privilege-read" => [0x4e7a, 8], "privilege-write" => [0x4e7b, 8], "illegal" => [0x4afc], _ => [0x4e71] }; + var e = SyntheticExecution.Prepare(m, words); + m.Core.State.M68060BusControl = image; + if (kind == "trace") { m.Core.State.StatusRegister |= 0x8000; e.Sr |= 0x8000; } + var vector = kind == "trace" ? 9 : kind == "illegal" ? 4 : 8; + SyntheticExecution.ExpectException(m, e, vector, kind == "trace" ? SyntheticMachine.Code + 2 : SyntheticMachine.Code); + e.ControlChecks["BUSCR snapshot"] = (s => s.M68060BusControl, (image & 0x50000000) | ((image & 0xa0000000) >> 1)); + SyntheticExecution.Run(m, e, report, $"68060/BUSCR/{kind}/image={image:X8}/ccr={ccr:X2}", false); + } + foreach (var image in Enumerable.Range(0, 16).Select(n => (uint)n << 28)) + { + m.Reset(); m.Core.State.M68060BusControl = image; + m.Core.Reset(SyntheticMachine.Code, 0x8000); + var e = ArchitecturalExpectation.Capture(m); + e.ControlChecks["BUSCR reset"] = (s => s.M68060BusControl, 0); + var mismatch = e.Verify(m); + report.Record($"68060/BUSCR/reset/image={image:X8}", mismatch == null ? "passing" : "mismatching", mismatch); + } + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void BusControlInterruptSnapshotsAndMaskedRequests() + { + var m = Machine(); var report = new CoverageBatch("68060", "system-bus-control-interrupts"); + foreach (var image in Enumerable.Range(0, 16).Select(n => (uint)n << 28)) + foreach (var supervisor in new[] { false, true }) + foreach (var stopped in new[] { false, true }) + foreach (var accepted in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + m.Reset(ccr, supervisor); + var e = SyntheticExecution.Prepare(m, [0x4e71]); e.Pc = SyntheticMachine.Code; + m.Core.State.StatusRegister = (ushort)((supervisor ? 0x2000 : 0) | (accepted ? 0 : 0x700) | ccr); + e.Sr = m.Core.State.StatusRegister; + m.Core.State.Stopped = stopped; e.Stopped = stopped; + m.Core.State.M68060BusControl = image; + e.ControlChecks["BUSCR"] = (s => s.M68060BusControl, accepted ? (image & 0x50000000) | ((image & 0xa0000000) >> 1) : image); + if (accepted) + { + SyntheticExecution.ExpectException(m, e, 28, SyntheticMachine.Code); + e.Sr = (ushort)((e.Sr & 0xf8ff) | 0x300); e.Stopped = false; + } + var id = $"68060/BUSCR/interrupt/image={image:X8}/super={supervisor}/stop={stopped}/accepted={accepted}/ccr={ccr:X2}"; + try + { + m.Core.RequestInterrupt(3, 28 * 4); + var mismatch = e.Verify(m); + report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); + } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); } + } + report.Complete(output); + } + + private static SyntheticMachine Machine() => new(ModelSpec.All.Single(m => m.Id == "68060")); + private static uint[] Values() => new uint[] { 0, uint.MaxValue, 0x50000000, 0xf0000000, 0x0fffffff, 0x55555555, 0xaaaaaaaa } + .Concat(Enumerable.Range(0, 32).Select(bit => 1u << bit)).Distinct().Order().ToArray(); + private static void Untested(CoverageBatch report, string id, params string[] phases) + { foreach (var phase in phases) report.Record(id + "/" + phase, "untested", "Prerequisite instruction failed; no partial instruction retry."); } + private static bool Step(SyntheticMachine m, ArchitecturalExpectation e, CoverageBatch report, string id) + { + try { m.Core.ExecuteInstruction(); var mismatch = e.Verify(m); report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); return mismatch == null; } + catch (Exception ex) { report.Record(id, ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException or UnsupportedM68040InstructionException ? "unsupported" : "mismatching", ex.ToString()); return false; } + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs b/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs index 073db3d1..0e7b74f6 100644 --- a/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs @@ -64,14 +64,14 @@ public void MovecControlInventoryMasksPrivilegeAndAllGeneralRegisters(string mod if (store) { if (general < 8) m.Core.State.D[general] = value; else if (general == 15) m.Core.State.SetActiveStackPointer(0x7100); else m.Core.State.A[general - 8] = value; } var op = (ushort)(store ? 0x4e7b : 0x4e7a); var e = SyntheticExecution.Prepare(m, [op, (ushort)(general << 12 | control)]); var valid = modelId != "68000" && (control is 0 or 1 or 0x800 or 0x801 || m.Model.FullIndex && (control == 2 || control == 0x802 && modelId is not ("68040" or "68060") || control is 0x803 or 0x804 && modelId != "68060") || modelId is "68040" or "68060" && (control is >= 3 and <= 7 or 0x806 or 0x807 || control == 0x805 && modelId == "68040" || control is 8 or 0x808 && modelId == "68060")); - if (modelId == "68000" || !supervisor || !valid) SyntheticExecution.ExpectException(m, e, modelId == "68000" || supervisor ? 4 : 8); + if (modelId == "68000" || !supervisor || !valid) SyntheticExecution.ExpectException(m, e, modelId == "68000" || supervisor || modelId == "68060" && !valid ? 4 : 8); else { var read = ControlReader(control); var actualValue = read(m.Core.State); if (store) { var source = general < 8 ? e.D[general] : e.A[general - 8]; - var masked = control switch { 0 or 1 => source & 7, 2 when modelId == "68060" => source & 0xf880e000, 2 when modelId == "68040" => source & 0x80008000, 2 => source & (modelId == "68030" ? 0x3313u : 3u), 3 when modelId == "68040" => source & 0xc000, 3 when modelId == "68060" => source & 0xfffe, >= 4 and <= 7 when modelId is "68040" or "68060" => source & 0xffffe364, 8 => source & 0xf0000000, 0x808 => 0x04300000 | source & 0x83, 0x806 or 0x807 when modelId == "68060" => source & 0xfffffe00, _ => source }; + var masked = control switch { 0 or 1 => source & 7, 2 when modelId == "68060" => source & 0xf880e000, 2 when modelId == "68040" => source & 0x80008000, 2 => source & (modelId == "68030" ? 0x3313u : 3u), 3 when modelId == "68040" => source & 0xc000, 3 when modelId == "68060" => source & 0xfffe, >= 4 and <= 7 when modelId is "68040" or "68060" => source & 0xffffe364, 8 => source & 0xa0000000, 0x808 => 0x04300000 | source & 0x83, 0x806 or 0x807 when modelId == "68060" => source & 0xfffffe00, _ => source }; e.ControlChecks[$"control {control:X3}"] = (read, masked); if (control == 0x800) e.InactiveStackPointer = masked; if (control == 0x803) e.MasterStackPointer = masked; diff --git a/Copper68k.Tests/Synthetic/SyntheticMovecControlEncodingTests.cs b/Copper68k.Tests/Synthetic/SyntheticMovecControlEncodingTests.cs new file mode 100644 index 00000000..2749d4fc --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticMovecControlEncodingTests.cs @@ -0,0 +1,49 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticMovecControlEncodingTests(ITestOutputHelper output) +{ + // MC68060UM programming models (3,4,5,7): availability is independent + // of the production decoder. 8.2.4 defines undefined MOVEC fields as illegal. + private static readonly int[] Legal060 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 0x800, 0x801, 0x806, 0x807, 0x808]; + + [Fact, Trait("Suite", "Synthetic")] + public void Movec060UndefinedControlFieldsPrecedePrivilegeAndValidUserFormsTrap() + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68060")); + var report = new CoverageBatch("68060", "system-movec-control-encodings"); + foreach (var control in Enumerable.Range(0, 4096).Except(Legal060)) + for (var general = 0; general < 16; general++) + foreach (var store in new[] { false, true }) + foreach (var supervisor in new[] { false, true }) + foreach (var ccr in new[] { 0, 31 }) Run(m, report, control, general, store, supervisor, ccr, 4); + foreach (var control in Legal060) + for (var general = 0; general < 16; general++) + foreach (var store in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) Run(m, report, control, general, store, false, ccr, 8); + report.Complete(output); + } + + private static void Run(SyntheticMachine m, CoverageBatch report, int control, int general, bool store, bool supervisor, int ccr, int vector) + { + m.Reset(ccr, supervisor); + var op = (ushort)(store ? 0x4e7b : 0x4e7a); + var e = SyntheticExecution.Prepare(m, [op, (ushort)(general << 12 | control)]); + SyntheticExecution.ExpectException(m, e, vector); + e.ControlChecks["TC untouched"] = (s => s.M68040Mmu.TranslationControl, 0); + e.ControlChecks["ITT0 untouched"] = (s => s.M68040Mmu.InstructionTransparentTranslation0, 0); + e.ControlChecks["ITT1 untouched"] = (s => s.M68040Mmu.InstructionTransparentTranslation1, 0); + e.ControlChecks["DTT0 untouched"] = (s => s.M68040Mmu.DataTransparentTranslation0, 0); + e.ControlChecks["DTT1 untouched"] = (s => s.M68040Mmu.DataTransparentTranslation1, 0); + e.ControlChecks["BUSCR untouched"] = (s => s.M68060BusControl, 0); + e.ControlChecks["PCR untouched"] = (s => s.M68060ProcessorConfiguration, 0x04300000); + e.ControlChecks["CACR untouched"] = (s => s.CacheControlRegister, 0); + e.ControlChecks["SFC untouched"] = (s => s.SourceFunctionCode, 0); + e.ControlChecks["DFC untouched"] = (s => s.DestinationFunctionCode, 0); + e.ControlChecks["VBR untouched"] = (s => s.VectorBaseRegister, 0); + e.ControlChecks["URP untouched"] = (s => s.M68040Mmu.UserRootPointer, 0); + e.ControlChecks["SRP untouched"] = (s => s.M68040Mmu.SupervisorRootPointer, 0); + SyntheticExecution.Run(m, e, report, $"68060/MOVEC/L/control={control:X3}/R{general}/store={store}/super={supervisor}/ccr={ccr:X2}/vector={vector}", false); + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs b/Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs index d08e5f9e..cb161682 100644 --- a/Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs +++ b/Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs @@ -5,7 +5,8 @@ namespace Copper68k.Tests.Synthetic; // Shared register-transfer fixture; masks and encodings are independent // specifications, not results computed with production decoder/MMU helpers. internal sealed record SyntheticMovecRegisterFixture(int Control, string Name, uint Mask, - Func Read, Action Initialize) + Func Read, Action Initialize, + Func? StoredValue = null) { private static readonly (string Name, Func Read)[] Preserved = [ @@ -15,10 +16,11 @@ private static readonly (string Name, Func Read)[] Preserved ("DTT0", s => s.M68040Mmu.DataTransparentTranslation0), ("DTT1", s => s.M68040Mmu.DataTransparentTranslation1), ("URP", s => s.M68040Mmu.UserRootPointer), - ("SRP", s => s.M68040Mmu.SupervisorRootPointer) + ("SRP", s => s.M68040Mmu.SupervisorRootPointer), + ("BUSCR", s => s.M68060BusControl) ]; - internal void WritesAndReadback(SyntheticMachine m, CoverageBatch report, IEnumerable values, int storeRegisters = 16) + internal void WritesAndReadback(SyntheticMachine m, CoverageBatch report, IEnumerable values, int storeRegisters = 16, uint initial = 0) { foreach (var value in values) for (var general = 0; general < storeRegisters; general++) @@ -30,24 +32,26 @@ internal void WritesAndReadback(SyntheticMachine m, CoverageBatch report, IEnume else if (general == 15) m.Core.State.SetActiveStackPointer(value); else m.Core.State.A[general - 8] = value; var e = SyntheticExecution.Prepare(m, [0x4e7b, (ushort)(general << 12 | Control), 0x4e7a, (ushort)(general << 12 | Control)]); + Initialize(m.Core.State, initial); e.Pc = SyntheticMachine.Code + 4; Preserve(m, e); - var id = $"{m.Model.Id}/MOVEC/L/{Name}/R{general}/value={value:X8}/super={supervisor}/ccr={ccr:X2}"; + var id = $"{m.Model.Id}/MOVEC/L/{Name}/R{general}/initial={initial:X8}/value={value:X8}/super={supervisor}/ccr={ccr:X2}"; if (!supervisor) { SyntheticExecution.ExpectException(m, e, 8); SyntheticExecution.Run(m, e, report, id + "/privilege", false); continue; } - e.ControlChecks[Name] = (Read, value & Mask); + var stored = StoredValue?.Invoke(initial, value) ?? value & Mask; + e.ControlChecks[Name] = (Read, stored); if (!Step(m, e, report, id + "/write")) { report.Record(id + "/read", "untested", $"{Name} write failed; dependent readback not executed."); continue; } e.Pc += 4; - if (general < 8) e.D[general] = value & Mask; - else e.A[general - 8] = value & Mask; + if (general < 8) e.D[general] = stored; + else e.A[general - 8] = stored; SyntheticExecution.Run(m, e, report, id + "/read"); } } diff --git a/Copper68k/M68060Interpreter.cs b/Copper68k/M68060Interpreter.cs index f4682b44..7b7ba8d0 100644 --- a/Copper68k/M68060Interpreter.cs +++ b/Copper68k/M68060Interpreter.cs @@ -188,6 +188,10 @@ private void Execute060Rte() CompleteTiming(M68kInstructionTimingKey.Rte); } + // MC68060UM 8.2.4: an undefined MOVEC control field is illegal. + protected override bool IsMovecControlEncodingValid(int register) => + register is >= 0 and <= 8 or 0x800 or 0x801 or 0x806 or 0x807 or 0x808; + protected override bool TryReadControlRegister(int register, uint pc, out uint value) { switch (register) @@ -219,7 +223,9 @@ protected override bool TryWriteControlRegister(int register, uint value, uint p // cache capacity are outside the current execution policy. return base.TryWriteControlRegister(register, value & 0xF880_E000u, pc); case 0x008: - State.M68060BusControl = value & 0xF000_0000; return true; + // L/LE are software commands; SL/SLE retain exception state. + State.M68060BusControl = (State.M68060BusControl & 0x5000_0000) | (value & 0xA000_0000); + return true; case 0x800: State.SetUserStackPointer(value); return true; case 0x808: State.M68060ProcessorConfiguration = 0x0430_0000 | (value & 0x83); return true; diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index 151c998b..96303ba3 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -8504,14 +8504,18 @@ protected virtual void ExecuteMovec(ushort opcode) BeginInstruction(opcode); var instructionPc = State.ProgramCounter; _ = FetchWord(); + var extension = FetchWord(); + if (!IsMovecControlEncodingValid(extension & 0x0FFF)) + { + RaiseFormat0Exception(4, instructionPc, M68kInstructionTimingKey.IllegalInstruction); + return; + } if ((State.StatusRegister & M68kCpuState.Supervisor) == 0) { - _ = FetchWord(); RaiseFormat0Exception(8, instructionPc, M68kInstructionTimingKey.PrivilegeViolation); return; } - var extension = FetchWord(); var generalRegister = (extension >> 12) & 7; var useAddressRegister = (extension & 0x8000) != 0; var controlRegister = extension & 0x0FFF; @@ -8537,6 +8541,10 @@ protected virtual void ExecuteMovec(ushort opcode) CompleteTiming(M68kInstructionTimingKey.Movec); } + // Most profiles retain their existing privilege-first convention. + // 060 additionally decodes control-field legality before privilege. + protected virtual bool IsMovecControlEncodingValid(int register) => true; + private void ExecuteRte() { BeginInstruction(0x4E73); diff --git a/Copper68k/M68kCore.cs b/Copper68k/M68kCore.cs index 52d4819c..d3377850 100644 --- a/Copper68k/M68kCore.cs +++ b/Copper68k/M68kCore.cs @@ -1603,7 +1603,10 @@ internal void RecordException(int vector, uint stackedProgramCounter, ushort sav } ExceptionSequence++; - if (M68060StackModeEnabled) M68060BusControl = (M68060BusControl & 0xA000_0000u) >> 1; + // MC68060AR section 5: nested exceptions must not clear SL/SLE. + // Capture active commands, retain prior shadows, then clear L/LE. + if (M68060StackModeEnabled) + M68060BusControl = (M68060BusControl & 0x5000_0000u) | ((M68060BusControl & 0xA000_0000u) >> 1); LastExceptionVector = vector; LastExceptionStackedProgramCounter = stackedProgramCounter; LastExceptionStatusRegister = savedStatusRegister; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index b3b345db..3a5165e4 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -1391,3 +1391,104 @@ and `artifacts/ttr-production.binlog`. All earlier restoration, 010 format-error reference-adapter and consolidation gaps remain open. No package publication, regression retirement, seeded audit or host/physical timing qualification is added; milestone 6 remains in progress with its accepted scope. + +### 060 BUSCR snapshots and MOVEC control-field legality — 2026-10-05 + +MOVEC BUSCR writes now update L/LE (`A0000000`) and preserve SL/SLE +(`50000000`). [MC68060UM 7.4](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf) +describes software lock commands and exception snapshots. Preserving shadows +across software writes is the interpretation of that snapshot model corroborated +by pinned WinUAE `newcpu_common.cpp`, rather than an explicit read-only sentence +or hardware measurement. Reserved nonzero writes are robustness samples; they +are canonicalized to implemented command bits. Raw reads cover all 16 legal +upper-nibble images, not arbitrary internally corrupted reserved bits. + +Exception entry now accumulates active L/LE into SL/SLE and clears L/LE while +retaining existing shadows. The user manual's generic copy wording does not +spell out nested retention. The later Motorola [MC68060AR section 5, printed +page 6](https://www.nxp.com/docs/en/supporting-information/MC68060AR.pdf) +explicitly states that the processor does not clear SL and nested exceptions +must not lose the lock state, with equivalent LE/SLE behavior. That clarification +is the independent expectation. Reset clears BUSCR; RTE does not automatically +restore software lock commands. Current WinUAE `newcpu.cpp` has a different +exception update, so this nested-state expectation is not inferred from +software agreement. Physical LOCK/LOCKE timing, cache bypass, locked access +faults and actual CAS2 software emulation remain unqualified. + +Three deterministic BUSCR groups add 315,408 cases: 247,808 command writes, +dependent readbacks, privilege and raw reads; 65,536 TRAP entries, nested TRAPs +and both RTE phases; and 2,064 privilege/trace/illegal/reset checks. They cover +all general registers including A7, all CCRs, both stacks and all four shadow +images. Against `964266e`, they detect respectively **119,808 / 15,360 / 896 +mismatches**, with **59,904 / 37,888 / zero** dependent cases untested. All +315,408 pass after the fix. A fourth group adds 4,096 interrupt cases across +accepted/masked requests and running/STOP state. Replacing only the exception +update with its original expression detects 896 interrupt mismatches; 3,200 +other cases pass. Production source is restored after each mutation. + +With BUSCR corrected, the unchanged external 060 `MOVEC2` sequence advances +from callback 71 to 73: undefined control field `009` in user state expected +vector 4 but got vector 8. [MC68060UM +8.2.4](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf) explicitly identifies +undefined MOVEC register fields as illegal. The pinned generator corroborates +legality before privilege on 060. The interpreter now validates that field before +privilege, fetching the same opcode/extension words as before and performing +no transfer on rejection. Other models keep their prior privilege convention; +legal 060 transfers retain their successful execution order and timing policy. +The canonical fixture's two erroneous expectations are corrected; none is retired. + +`system-movec-control-encodings` adds **536,832 cases**. Every one of the +4,082 undefined fields is tested in both directions, all 16 general registers, +both stacks and CCR 0/31; all 14 implemented fields are tested in user state +for every CCR and register. Assertions include saved PC/SR, frame/stack state, +untouched registers and control images. Disabling only the 060 legality check +detects **261,248 mismatches** with 275,584 passing and zero unsupported/untested +cases. All cases pass with the guard restored. BUSCR/encoding additions total +**856,336 cases in five reporting batches**. A failing prerequisite is never +retried or counted as a passing dependent phase. + +The unchanged broad WinUAE audit now passes 060 `MOVEC2` (**8,228 callbacks / +8,192 frame assertions**) and the already-passing 040 sequence. This is the +generated group's scope, not exhaustive external qualification of every MOVEC +register and initial image. Overall the audit remains failing: **1,322 passing, +46 mismatching, 13 unsupported and zero untested groups**, over 11,335,687 +callbacks, 1,525,385 frames, 199,327 masked-SR cases and one terminal callback. +All 32 controls pass. The original Basic manifest and library identities recorded +above are unchanged; no comparison mask or family exclusion is added. The +original PCR diagram uses EDEBUG bit 7 while pinned WinUAE writes bit 6; the +current addendum does not resolve that difference. No PCR production correction +is made from software agreement alone. + +Evidence includes `artifacts/m6-buscr-before/`, `artifacts/m6-buscr-focused/`, +`artifacts/m6-buscr-control-focused/`, `artifacts/m6-movec-legality-before/`, +`artifacts/m6-buscr-interrupt-before/`, `artifacts/m6-buscr-discovery/` +and `artifacts/m6-buscr-winuae/`. Milestone 6 remains in progress: prior +restoration, stage-specific 010 format-error, reference and consolidation +requirements remain. No package publication, regression retirement or physical +qualification is added. + +Final Release validation passes **4,775 CPU tests** with ten optional skips and +zero failures. The deterministic gate validates **11,168,408 logical cases in +469 reporting batches**, with `roadmapComplete=false`. Omitting only the new +060 control-encoding report rejects the gate. Fresh pinned SingleStepTests passes +312,500 selected cases in 125 files; Musashi passes 536 programs with 88 +exclusions. The script rechecks source revisions, exact inputs and selection. +AHX passes 18 tests. The qualified TRAP trace preset passes 512 callbacks, +256 incoming-T1 cases, 512 frames and six controls against this CPU. + +Private **unpublished** NuGet `1.5.2-synthetic-dev.48` has SHA-256 +`de7ce3911783486a52077c0213684c1f722037e064eda1131e67c27b4d00b7c5`. +`artifacts/m6-buscr-package.json` records all three changed production source +hashes against `964266e` and CPU assembly identity +`624cd5631ea0e33e5eba2e5de0aa85e6dd8a4c55541b561df1cccbfa9812ba15`. +The isolated CopperScreen `d9beae8` baseline resolves the exact package in +production and separate diagnostics, with all four loaded DLLs matching that +identity. Release build has zero warnings/errors. Host 149, disk 74 and engine +diagnostics 1,080 pass. Three native Workbench/A1200 boot and disk-persistence +replays pass with no skips; six optional host/media skips remain unavailable +coverage. Evidence: `artifacts/m6-buscr-full/`, +`artifacts/m6-buscr-missing-report/`, `artifacts/m6-buscr-trace-reference/`, +`artifacts/m6-buscr-ahx-results/` and `artifacts/synthetic-private-feed-48/`; +consumer `artifacts/buscr-validation/`, `artifacts/buscr-diagnostic-tests/` +and `artifacts/buscr-production.binlog`. No seeded, physical timing or host +performance qualification is claimed. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index bf35aff6..366ee2eb 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -880,3 +880,29 @@ qualified TRAP trace checks pass. The private unpublished .47 package validates the isolated CopperScreen Release build, host/disk/engine tests and three native Workbench/A1200 replays. Exact input/source/package identities and qualification limits remain in the reference document. + +The 060 BUSCR/control-field follow-up preserves exception shadow bits across +MOVEC writes and nested exception entry, while clearing active lock commands. +The later Motorola porting guide supplies the explicit nested-retention rule; +software-write preservation is a documented interpretation corroborated by the +pinned reference. Physical pin/cache/locked-access effects remain unqualified. +Undefined 060 MOVEC control fields now enter vector 4 before privilege checking, +with the same instruction fetch order and no transfer effects or retry. + +Five required batches add 856,336 cases for BUSCR writes/readback, all shadow +images, nested TRAP/RTE, privilege/trace/reset, accepted/masked interrupts and +every 060 control-field encoding. The original BUSCR code and isolated legality +and interrupt mutations are detected; all new cases pass with fixes restored. +The canonical expectations are corrected and the reusable fixture extended; +no old regression is retired. The unchanged WinUAE 060 MOVEC2 group now passes +8,228 callbacks and 8,192 frames. Its broader audit still fails 46 mismatching +and 13 unsupported groups. PCR reference/manual disagreement and all earlier +restoration, reference and consolidation requirements remain open. Milestone 6 +stays in progress; no package is published. + +Final validation passes 4,775 CPU tests with ten optional skips. The report gate +validates 11,168,408 cases in 469 batches and rejects the missing new 060 +control-encoding report. Fresh pinned SingleStepTests/Musashi, AHX and qualified +TRAP trace checks pass. The private unpublished .48 package passes the isolated +CopperScreen Release build, host/disk/engine checks and three native replays. +The reference document records exact identities, failed-before proofs and limits. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index d4a328b7..c7d056b7 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -114,6 +114,13 @@ try { foreach ($control in 4..7) { $expected["system-transparent-control-$control"] = 77824 } foreach ($control in @('806','807')) { $expected["system-root-control-$control"] = 55296 } } + if ($model -eq '68060') { + $expected['system-bus-control'] = 247808 + $expected['system-bus-control-exceptions'] = 65536 + $expected['system-bus-control-state'] = 2064 + $expected['system-bus-control-interrupts'] = 4096 + $expected['system-movec-control-encodings'] = 536832 + } if ($model -eq '68010') { $expected['system-format8-entry'] = 1024 $expected['system-format8-rte'] = 4096 From f249fe9cc72dd27245414eb63dca46dbb2264596 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 03:55:53 +0300 Subject: [PATCH 021/211] Qualify TRAPcc and CHK2 saved PCs in the reference generator --- Copper68k.Tests/M68kWinUaeModelAuditTests.cs | 6 +- .../M68kWinUaeTrapBoundsAuditTests.cs | 103 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 93 ++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 24 ++++ scripts/prepare-copper68k-winuae.ps1 | 35 +++++- scripts/test-copper68k-winuae-trap-bounds.ps1 | 36 ++++++ scripts/winuae/trap-bounds-pc.patch | 10 ++ 7 files changed, 299 insertions(+), 8 deletions(-) create mode 100644 Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs create mode 100644 scripts/test-copper68k-winuae-trap-bounds.ps1 create mode 100644 scripts/winuae/trap-bounds-pc.patch diff --git a/Copper68k.Tests/M68kWinUaeModelAuditTests.cs b/Copper68k.Tests/M68kWinUaeModelAuditTests.cs index 98a42b83..2cc78d79 100644 --- a/Copper68k.Tests/M68kWinUaeModelAuditTests.cs +++ b/Copper68k.Tests/M68kWinUaeModelAuditTests.cs @@ -93,15 +93,15 @@ public void WinUaeIntegerFixturesAcrossSelectedModelsWhenEnabled() private static string FixtureId(string id) => id == "A1200" ? "68EC020" : id; private static string Hash(string path) => Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))).ToLowerInvariant(); - internal static void ValidateWinUaeProfile(string root, WinUaeManifest manifest, ModelSpec model, bool traceTraps = false) + internal static void ValidateWinUaeProfile(string root, WinUaeManifest manifest, ModelSpec model, bool traceTraps = false, string[]? requiredOpcodes = null) { var fixture = manifest.Profiles.SingleOrDefault(p => p.Id == FixtureId(model.Id)) ?? throw new XunitException($"Missing WinUAE profile: {model.Id}"); var level = model.Id switch { "68000" => 0, "68010" => 1, "68030" => 3, "68040" => 4, "68060" => 5, _ => 2 }; var cpu = level == 5 ? "68060" : $"{68000 + level * 10}"; - var requiredCount = traceTraps ? 1 : model.Id switch { "68000" => 146, "68010" => 153, "68030" => 180, "68040" => 181, "68060" => 184, _ => 179 }; + var requiredCount = requiredOpcodes?.Length ?? (traceTraps ? 1 : model.Id switch { "68000" => 146, "68010" => 153, "68030" => 180, "68040" => 181, "68060" => 184, _ => 179 }); if (fixture.CpuLevel != level || fixture.CpuDirectory != cpu || fixture.AddressBits != model.AddressBits || - fixture.Opcodes.Length != requiredCount || fixture.Opcodes.Distinct().Count() != requiredCount || !fixture.Opcodes.Contains(traceTraps ? "TRAP" : "NOP")) + fixture.Opcodes.Length != requiredCount || fixture.Opcodes.Distinct().Count() != requiredCount || (requiredOpcodes is null ? !fixture.Opcodes.Contains(traceTraps ? "TRAP" : "NOP") : !fixture.Opcodes.Order().SequenceEqual(requiredOpcodes.Order()))) throw new XunitException($"Incomplete or incompatible WinUAE profile: {model.Id}"); var path = Path.Combine(root, fixture.Id, cpu); foreach (var memory in new[] { ("lmem.dat", 0x8000), ("hmem.dat", 0x8000), ("tmem.dat", 0x40000) }) diff --git a/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs b/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs new file mode 100644 index 00000000..964b529e --- /dev/null +++ b/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs @@ -0,0 +1,103 @@ +using System.Text.Json; +using Copper68k.Tests.Synthetic; +using Xunit.Sdk; + +namespace Copper68k.Tests; + +public sealed partial class M68kWinUaeCpuTesterConformanceTests +{ + private const string TrapBoundsSourceHash = "3ef386033792e585b093e55a44242b32d2cee16be7a597aa687bae7191ca449d"; + private const string TrapBoundsPatchHash = "ca94d93ec49447e853769bb93bd4e823fe313854aba59601161b35ef8f7bcca4"; + + [EnvironmentFact("COPPER68K_RUN_WINUAE_TRAP_BOUNDS_AUDIT", "audit qualified WinUAE TRAPcc/CHK2 saved PCs")] + public void WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled() + { + var root = Environment.GetEnvironmentVariable("COPPER68K_WINUAE_TRAP_BOUNDS_PATH"); + var library = Environment.GetEnvironmentVariable(LibraryVariable); + var output = Environment.GetEnvironmentVariable("COPPER68K_SYNTHETIC_REPORT_DIR"); + if (string.IsNullOrWhiteSpace(root) || string.IsNullOrWhiteSpace(library) || string.IsNullOrWhiteSpace(output)) + throw new XunitException("Trap/bounds audit requires qualified fixtures, native library and report directory."); + var manifestFile = Path.Combine(root, "manifest.json"); + var json = File.ReadAllText(manifestFile); + var manifest = JsonSerializer.Deserialize(json) ?? throw new XunitException("Missing trap/bounds manifest."); + using var identity = JsonDocument.Parse(json); + var fields = identity.RootElement; + var sourcePath = Path.Combine(root, "gencpu-trap-bounds.cpp"); + var normalizedSourceHash = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData( + System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(sourcePath).Replace("\r\n", "\n")))).ToLowerInvariant(); + if (manifest.Schema != 1 || manifest.GeneratorCommit != GeneratorPin || manifest.RunnerCommit != RunnerPin || + fields.GetProperty("Preset").GetString() != "TrapBounds" || normalizedSourceHash != TrapBoundsSourceHash || + fields.GetProperty("TrapBoundsPatchSha256").GetString() != TrapBoundsPatchHash || + Hash(sourcePath) != fields.GetProperty("TrapBoundsSourceSha256").GetString() || + Hash(Path.Combine(root, "trap-bounds-pc.patch")) != TrapBoundsPatchHash || + Hash(Path.Combine(root, "cputester.exe")) != fields.GetProperty("GeneratorExecutableSha256").GetString() || + Hash(library) != manifest.NativeLibrarySha256) + throw new XunitException("Trap/bounds source, patch, generator or native identity is unqualified."); + var profileIds = new[] { "68020", "68030", "68040", "68060", "68EC020" }; + if (manifest.Profiles.Length != profileIds.Length || !manifest.Profiles.Select(x => x.Id).Order().SequenceEqual(profileIds.Order())) + throw new XunitException("Trap/bounds audit requires all five advanced profiles, without empty or duplicate selections."); + var models = ModelSpec.All.Where(x => x.Id is not ("68000" or "68010")).ToArray(); + foreach (var model in models) ValidateWinUaeProfile(root, manifest, model, requiredOpcodes: TrapBoundsFamilies(model)); + var rows = new List(); + var probes = new List(); + using var tester = NativeTester.Load(library); + foreach (var model in models) + { + var fixture = manifest.Profiles.Single(x => x.Id == FixtureId(model.Id)); + var path = Path.Combine(root, fixture.Id); + foreach (var family in TrapBoundsFamilies(model)) + { + var register = tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptResult: true); + var sr = tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptSr: 0x10); + var frame = tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptFrame: true); + var detected = !register.Passed && register.ExecutedCases > 0 && !sr.Passed && sr.ExecutedCases > 0 && + sr.Detail.Contains("SR:", StringComparison.Ordinal) && !frame.Passed && tester.FrameChecks > 0 && + frame.Detail.Contains("frame byte", StringComparison.Ordinal); + probes.Add(new(model.Id, family, detected, register.ExecutedCases, sr.ExecutedCases, frame.ExecutedCases)); + var result = tester.Run(path, family, fixture.CpuLevel, false, false, model); + var expected = TrapBoundsCounts(model, family); + var passing = detected && result.Passed && result.ExecutedCases == expected.Cases && + tester.FrameChecks == expected.Frames && tester.MaskedCases == (family == "TRAPcc" ? 0 : expected.Cases); + rows.Add(new(model.Id, family, passing ? "passing" : tester.UnsupportedExecution ? "unsupported" : "mismatching", + result.ExecutedCases, tester.FrameChecks, tester.MaskedCases, detected, result.Detail)); + _output.WriteLine($"{model.Id}/{family}: {result.ExecutedCases} callbacks, {tester.FrameChecks} frames; controls={detected}."); + } + } + Directory.CreateDirectory(output); + File.WriteAllText(Path.Combine(output, "winuae-trap-bounds-audit.json"), JsonSerializer.Serialize(new + { + schema = 1, reference = "WinUAE trap/bounds preset with explicit Motorola-qualified saved-PC correction", + manifest.GeneratorCommit, manifest.RunnerCommit, manifest.NativeLibrarySha256, + manifestSha256 = Hash(manifestFile), generatorSourceSha256 = Hash(sourcePath), + generatorNormalizedSourceSha256 = TrapBoundsSourceHash, generatorPatchSha256 = TrapBoundsPatchHash, + adapterAssemblySha256 = Hash(typeof(M68kWinUaeCpuTesterConformanceTests).Assembly.Location), + cpuAssemblySha256 = Hash(typeof(Copper68k.M68kCoreFactory).Assembly.Location), + qualification = "TRAPcc on EC020/A1200/020/030/040/060; CHK2 B/W/L on EC020/A1200/020/030/040. Basic CCR 0/31 and user/supervisor, full extensions enabled, no incoming trace/bus faults. CHK2 is unavailable on 060 and is covered architecturally by synthetic tests, not this generated preset. Patched software reference, not unchanged upstream or silicon qualification.", + passing = rows.Count(x => x.Status == "passing"), mismatching = rows.Count(x => x.Status == "mismatching"), + unsupported = rows.Count(x => x.Status == "unsupported"), executedCases = rows.Sum(x => (long)x.ExecutedCases), + exceptionFrames = rows.Sum(x => (long)x.ExceptionFrames), maskedSrCases = rows.Sum(x => (long)x.MaskedSrCases), + probes, rows + }, new JsonSerializerOptions { WriteIndented = true })); + Assert.True(rows.All(x => x.Status == "passing"), string.Join(Environment.NewLine, + rows.Where(x => x.Status != "passing").Select(x => $"{x.Model}/{x.Family}: controls={x.Controls}; {x.Detail}"))); + } + + private static string[] TrapBoundsFamilies(ModelSpec model) => model.Id == "68060" + ? ["TRAPcc"] : ["CHK2.B", "CHK2.W", "CHK2.L", "TRAPcc"]; + private static (int Cases, uint Frames) TrapBoundsCounts(ModelSpec model, string family) => + (FixtureId(model.Id), family) switch + { + (_, "TRAPcc") => (156160, 78080), + ("68EC020", "CHK2.B") => (1074, 628), + ("68EC020", "CHK2.W") => (1048, 534), + ("68EC020", "CHK2.L") => (1130, 546), + (_, "CHK2.B") => (912, 542), + (_, "CHK2.W") => (900, 533), + (_, "CHK2.L") => (874, 406), + _ => throw new XunitException($"Unqualified trap/bounds family: {model.Id}/{family}") + }; + private sealed record WinUaeTrapBoundsRow(string Model, string Family, string Status, int ExecutedCases, + uint ExceptionFrames, uint MaskedSrCases, bool Controls, string Detail); + private sealed record WinUaeTrapBoundsProbe(string Model, string Family, bool Detected, + int RegisterCases, int SrCases, int FrameCases); +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 3a5165e4..9e472f85 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -1492,3 +1492,96 @@ coverage. Evidence: `artifacts/m6-buscr-full/`, consumer `artifacts/buscr-validation/`, `artifacts/buscr-diagnostic-tests/` and `artifacts/buscr-production.binlog`. No seeded, physical timing or host performance qualification is claimed. + +## TRAPcc and CHK2 saved-PC reference qualification (2026-10-05) + +The pinned Basic generator raises CHK2 and TRAPcc exceptions before committing +its pending PC offset. For example, `50FA 0095` at `0087FFA0` expects stacked +`0087FFA0` instead of `0087FFA4`; `00D0 0800` has the same discrepancy for CHK2.B. +These are reference defects, not evidence for changing the CPU. The authority is +[M68000PM 4-189](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf), +[MC68020UM 6.1.4](https://www.nxp.com/docs/en/data-sheet/MC68020UM.pdf), +[MC68040UM 8.2.3](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +and [MC68060UM 8.2.3](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf): +instruction traps save the following instruction address and retain the causing +instruction address separately. PM 4-71 defines CHK2's X/Z/C and undefined N/V. + +The new `TrapBounds` preset applies exactly two anchored additions of +`sync_m68k_pc()` in a copy of pinned `gencpu.cpp`, before the CHK2 and TRAPcc trap +conditions. It retains the generator/runner pins and native assertion bridge. +Original tracked reference sources, Basic inputs and trace preset stay intact. +The common profile preflight accepts an explicit required family set; its +existing Basic and TraceTraps defaults retain their strict original selections. +No production CPU code, timing policy, SR comparison mask or exception frame +expectation changes. A1200 uses the independently selected EC020 fixture profile. + +The focused preset requires all five advanced fixture profiles and audits all +six CPU profiles including A1200. CHK2 is unavailable in 060 hardware (UM 8.2.4), +so that model's focused selection is TRAPcc only; the existing synthetic bounds +suite retains its required architectural exception coverage. This does not +exclude any directory from the unchanged broad audit. + +| Profile | CHK2.B callbacks / frames | CHK2.W callbacks / frames | CHK2.L callbacks / frames | TRAPcc callbacks / frames | +| --- | --- | --- | --- | --- | +| EC020 and A1200, each | 1,074 / 628 | 1,048 / 534 | 1,130 / 546 | 156,160 / 78,080 | +| 020, 030 and 040, each | 912 / 542 | 900 / 533 | 874 / 406 | 156,160 / 78,080 | +| 060 | unavailable here | unavailable here | unavailable here | 156,160 / 78,080 | + +All **21 groups pass**, totaling **951,522 executed callbacks, 476,339 exception +frames and 14,562 callbacks with documented undefined SR bits masked**. Each +group must meet these exact counts, not merely execute one passing sample. +All 63 register/defined-X/frame corruption controls are detected. An isolated +CPU mutation saving the opcode PC for both families fails all 21 groups, at +31 callbacks and 21 frames; the original CPU source is restored byte-for-byte. +Separate copied-input preflight controls reject a missing data file, changed +fixture byte, empty profile selection and modified generator source, before +native execution. Evidence: `artifacts/m6-trap-bounds-command/`, +`artifacts/m6-trap-bounds-pc-mutation/` and `artifacts/m6-trap-bounds-preflight/`. + +Identities for `artifacts/m6-trap-bounds-qualified-inputs/`: + +- Manifest: `6d3fd2de241776b7fb22fa70225924932cc1c6c487e4931311916adab6b210ef`. +- Normalized patched source: `3ef386033792e585b093e55a44242b32d2cee16be7a597aa687bae7191ca449d`. +- Normalized patch: `ca94d93ec49447e853769bb93bd4e823fe313854aba59601161b35ef8f7bcca4`. +- Generator executable: `6b350167e05a2fc27a6320251488383384a86e9aa2e59e21f691853123cb0f9d`. +- Native bridge: `05ee1b8f5e6fbe67526cd5a53e7768e38e207091f4cc5ac11850700000bbac2d`. +- CPU assembly: `423405f59e9e0706a96fd2bd8f9d67b479032692e831a4f6e31480df6954b785`. +- Adapter assembly: `13a0baa1f50b368f3f0df818ca4a0b331496f512f29fa5f5acd7cf024ee4fe76`. + +Generator seeds initialize xorshift state to 1 per test set, one round. This +focused selection changes the random stream's family order relative to Basic; +its passing counts cannot be substituted into the old Basic report. Full-format +extensions are enabled, with CCR 0/31 and user/supervisor rounds; bus/address +faults, incoming trace/M rounds, physical timing and exhaustive architectural +combination coverage are not claimed. These are explicitly patched software +expectations, not unchanged upstream or hardware measurements. + +Reproduce with fresh output directories (supply the local pinned checkouts and +MSVC environment script; no generated binary/media is committed): + +```powershell +./scripts/prepare-copper68k-winuae.ps1 -GeneratorSource -RunnerSource -VcVars64 -Preset TrapBounds -OutputDirectory artifacts/trap-bounds-inputs +./scripts/test-copper68k-winuae-trap-bounds.ps1 -InputDirectory artifacts/trap-bounds-inputs -OutputDirectory artifacts/trap-bounds-audit +``` + +The audit command restores its environment and fails on missing, changed, empty +or mismatching inputs, skipped execution or a missing report. Fresh unchanged +Basic validation still reports **1,322 passing, 46 mismatching, 13 unsupported +and zero untested groups**, with 11,335,687 callbacks, 1,525,385 frame assertions, +199,327 masked-SR callbacks and all 32 controls passing. The Basic manifest and +library retain their previous identities. The existing qualified TRAP trace +audit still passes 512 callbacks, 256 incoming-T1 cases, 512 frames and six +controls. Evidence: `artifacts/m6-trap-bounds-broad/` and +`artifacts/m6-trap-bounds-trace/`. Milestone 6 remains in progress with all earlier +restoration, reference and consolidation requirements retained. No regression +is retired and no package is published in this follow-up. + +Final Release CPU validation passes **4,776 tests with ten optional skips** and +zero failures, including the enabled new reference audit. The deterministic gate +still validates **11,168,408 logical cases in 469 reporting batches** with +`roadmapComplete=false`. Fresh pinned SingleStepTests passes 312,500 selected +cases in 125 files; Musashi passes 536 programs with 88 exclusions. Source pins, +input hashes and selections are checked again. Evidence: +`artifacts/m6-trap-bounds-full/` and `artifacts/m6-trap-bounds-gate.log`. +Consumer package/replay qualification remains the earlier .48 evidence; this +follow-up changes only test adapters, scripts and documentation. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 366ee2eb..e37f88d6 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -906,3 +906,27 @@ control-encoding report. Fresh pinned SingleStepTests/Musashi, AHX and qualified TRAP trace checks pass. The private unpublished .48 package passes the isolated CopperScreen Release build, host/disk/engine checks and three native replays. The reference document records exact identities, failed-before proofs and limits. + +The TRAPcc/CHK2 reference follow-up qualifies a separate pinned `TrapBounds` +preset against Motorola's following-instruction saved-PC rules. Two missing PC +synchronizations are corrected in a copied generator source; CPU execution and +the unchanged Basic audit retain their behavior. Six advanced CPU profiles pass +21 groups, 951,522 callbacks and 476,339 frame checks. All 63 comparator controls +pass; an isolated wrong-saved-PC CPU mutation fails every group. Missing, changed, +empty and unqualified-source inputs are rejected before native execution. The +new dedicated audit command requires exact profile/family and callback/frame +coverage; it cannot pass from a skipped or empty selection. + +CHK2's 060 architectural unavailability remains covered synthetically; the +focused generated 060 profile contains only TRAPcc. This is patched software +qualification with CCR 0/31 and ordinary stacks, not physical qualification or +exhaustive trace/fault coverage. The unchanged broad audit still fails 46 +mismatching and 13 unsupported groups; its figures are not replaced by the new +preset. Milestone 6 remains in progress. No CPU source/package change or test +retirement is needed. Exact identities, scope and reproduction commands are in +`COPPER68K_REFERENCE_QUALIFICATION.md`. + +Final follow-up validation passes 4,776 CPU tests with ten optional skips, +including the enabled trap/bounds audit. The deterministic gate retains +11,168,408 cases in 469 batches and `roadmapComplete=false`; fresh pinned +SingleStepTests/Musashi and the earlier qualified trace preset pass. diff --git a/scripts/prepare-copper68k-winuae.ps1 b/scripts/prepare-copper68k-winuae.ps1 index a99739ff..47aaab8a 100644 --- a/scripts/prepare-copper68k-winuae.ps1 +++ b/scripts/prepare-copper68k-winuae.ps1 @@ -4,7 +4,7 @@ param( [Parameter(Mandatory)] [string] $GeneratorSource, [Parameter(Mandatory)] [string] $RunnerSource, [Parameter(Mandatory)] [string] $VcVars64, - [ValidateSet('Basic','TraceTraps')] [string] $Preset = 'Basic', + [ValidateSet('Basic','TraceTraps','TrapBounds')] [string] $Preset = 'Basic', [string] $OutputDirectory = 'artifacts/winuae-model-inputs' ) $ErrorActionPreference = 'Stop' @@ -47,7 +47,26 @@ try { Run-Compiler @('/nologo','/O2','/EHsc','/w','/I.','/Iinclude','build68k.cpp','/Fe:build68k.exe') (Join-Path $output 'build68k.log') & ./build68k.exe table68k | Set-Content -Encoding utf8 cpudefs.cpp if ($LASTEXITCODE -ne 0) { throw 'Opcode table generation failed' } - Run-Compiler @('/nologo','/O2','/EHsc','/w','/I.','/Iinclude','gencpu.cpp','missing.cpp','readcpu.cpp','cpudefs.cpp','/Fe:gencpu_prog.exe') (Join-Path $output 'gencpu-build.log') + $cpuGeneratorSource = 'gencpu.cpp' + if ($Preset -eq 'TrapBounds') { + # MC68020UM 6.1.4 / MC68040UM 8.2.3 require the next PC on + # instruction traps. Patch a copy; Basic keeps the pinned source. + $patchPath = Join-Path $PSScriptRoot 'winuae/trap-bounds-pc.patch' + $patch = [IO.File]::ReadAllText($patchPath).Replace("`r`n", "`n") + $source = [IO.File]::ReadAllText((Join-Path $generator 'gencpu.cpp')).Replace("`r`n", "`n") + $hunks = @($patch -split "(?m)^@@`n" | Select-Object -Skip 1) + if ($hunks.Count -ne 2) { throw 'Trap/bounds patch must contain exactly two hunks' } + foreach ($hunk in $hunks) { + $lines = $hunk.TrimEnd("`n").Split("`n") + $before = (($lines | Where-Object { $_.StartsWith('-') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" + $after = (($lines | Where-Object { $_.StartsWith('+') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" + $source = Patch-Once $source $before $after + } + $cpuGeneratorSource = Join-Path $output 'gencpu-trap-bounds.cpp' + [IO.File]::WriteAllText($cpuGeneratorSource, $source) + [IO.File]::WriteAllText((Join-Path $output 'trap-bounds-pc.patch'), $patch) + } + Run-Compiler @('/nologo','/O2','/EHsc','/w','/I.','/Iinclude',$cpuGeneratorSource,'missing.cpp','readcpu.cpp','cpudefs.cpp','/Fe:gencpu_prog.exe') (Join-Path $output 'gencpu-build.log') & ./gencpu_prog.exe . *> (Join-Path $output 'gencpu-run.log') if ($LASTEXITCODE -ne 0) { throw 'CPU source generation failed' } $testerSource = 'cputest.cpp' @@ -65,7 +84,7 @@ try { } $cpp = @('cpudefs.cpp','cpuemu_90_test.cpp','cpuemu_91_test.cpp','cpuemu_92_test.cpp','cpuemu_93_test.cpp','cpuemu_94_test.cpp','cpuemu_95_test.cpp','cputbl_test.cpp',$testerSource,'cputest_support.cpp','disasm.cpp','fpp.cpp','fpp_softfloat.cpp','ini.cpp','newcpu_common.cpp','readcpu.cpp','softfloat/softfloat.cpp','softfloat/softfloat_decimal.cpp','softfloat/softfloat_fpsp.cpp') Run-Compiler (@('/nologo','/O2','/EHsc','/w','/I.','/Iinclude','/Icputest','/I../zlib','/DCPUEMU_90','/DCPUEMU_91','/DCPUEMU_92','/DCPUEMU_93','/DCPUEMU_94','/DCPUEMU_95','/DCPU_TESTER') + $cpp + @('/Fe:cputester.exe')) (Join-Path $output 'cputester-build.log') - if ($Preset -eq 'TraceTraps') { Copy-Item -LiteralPath 'cputester.exe' -Destination $output } + if ($Preset -ne 'Basic') { Copy-Item -LiteralPath 'cputester.exe' -Destination $output } } finally { Pop-Location } $native = [IO.File]::ReadAllText((Join-Path $vendor 'm68k_cpu_tester.c')) @@ -110,18 +129,22 @@ void M68KTester_destroy(M68KTesterContext* context) { if ($Preset -eq 'TraceTraps') { $baseIni = $baseIni.Replace('[test=Basic]', '[test=TraceTraps]').Replace('mode=all', 'mode=TRAP').Replace('feature_sr_mask=0x0000', 'feature_sr_mask=0xa000') } - $index = 0 + if ($Preset -eq 'TrapBounds') { + $baseIni = $baseIni.Replace('[test=Basic]', '[test=TrapBounds]').Replace('mode=all', 'mode=TRAPcc,CHK2.B,CHK2.W,CHK2.L') + } foreach ($model in @( @{id='68000'; cpu='68000'; width=24}, @{id='68010'; cpu='68010'; width=24}, @{id='68EC020'; cpu='68020'; width=24}, @{id='68020'; cpu='68020'; width=32}, @{id='68030'; cpu='68030'; width=32}, @{id='68040'; cpu='68040'; width=32}, @{id='68060'; cpu='68060'; width=32})) { if ($Preset -eq 'TraceTraps' -and $model.id -notin @('68040','68060')) { continue } + if ($Preset -eq 'TrapBounds' -and $model.id -in @('68000','68010')) { continue } $profileRoot = Join-Path $output $model.id New-Item -ItemType Directory -Path $profileRoot | Out-Null # Every invocation uses a fresh generator output path; stale data cannot fill a gap. $prefix = 'copper68k-' + [guid]::NewGuid().ToString('N') + '/' $config = $baseIni.Replace("cpu=68000`n", "cpu=$($model.cpu)`n").Replace('path=data/', "path=$prefix") + if ($Preset -eq 'TrapBounds' -and $model.id -eq '68060') { $config = $config.Replace('mode=TRAPcc,CHK2.B,CHK2.W,CHK2.L', 'mode=TRAPcc') } if ($model.width -eq 24) { $config = $config.Replace('cpu_address_space=68020','cpu_address_space=68030') } else { $config = $config.Replace('test_high_memory_start=0x00ff8000','test_high_memory_start=0xffff8000').Replace('test_high_memory_end=0x01000000','test_high_memory_end=0xffffffff') } [IO.File]::WriteAllText((Join-Path $profileRoot 'cputestgen.ini'), $config) @@ -135,7 +158,7 @@ void M68KTester_destroy(M68KTesterContext* context) { Copy-Item -LiteralPath $generated -Destination (Join-Path $profileRoot $model.cpu) -Recurse } finally { Pop-Location } $profiles += @{ - Id=$model.id; CpuDirectory=$model.cpu; CpuLevel=$(if ($Preset -eq 'TraceTraps') { if ($model.id -eq '68040') {4} else {5} } else {$index}); AddressBits=$model.width + Id=$model.id; CpuDirectory=$model.cpu; CpuLevel=$(switch ($model.cpu) {'68000' {0} '68010' {1} '68020' {2} '68030' {3} '68040' {4} '68060' {5}}); AddressBits=$model.width Opcodes=@(Get-ChildItem -LiteralPath (Join-Path $profileRoot $model.cpu) -Directory | Select-Object -ExpandProperty Name | Sort-Object) Inputs=@(Get-ChildItem -LiteralPath $profileRoot -Recurse -File -Filter '*.dat' | Sort-Object FullName | ForEach-Object { @{Path=[IO.Path]::GetRelativePath($profileRoot,$_.FullName).Replace('\','/'); Bytes=$_.Length; Sha256=(Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant()} @@ -148,6 +171,8 @@ void M68KTester_destroy(M68KTesterContext* context) { Preset=$Preset TracePrioritySourceSha256=$(if ($Preset -eq 'TraceTraps') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-trace.cpp')).Hash.ToLowerInvariant()} else {$null}) TracePriorityPatchSha256=$(if ($Preset -eq 'TraceTraps') {(Get-FileHash -LiteralPath (Join-Path $output 'trace-priority.patch')).Hash.ToLowerInvariant()} else {$null}) + TrapBoundsSourceSha256=$(if ($Preset -eq 'TrapBounds') {(Get-FileHash -LiteralPath (Join-Path $output 'gencpu-trap-bounds.cpp')).Hash.ToLowerInvariant()} else {$null}) + TrapBoundsPatchSha256=$(if ($Preset -eq 'TrapBounds') {(Get-FileHash -LiteralPath (Join-Path $output 'trap-bounds-pc.patch')).Hash.ToLowerInvariant()} else {$null}) NativeLibrarySha256=(Get-FileHash -LiteralPath (Join-Path $output 'm68k_cpu_tester.dll') -Algorithm SHA256).Hash.ToLowerInvariant() Compiler=$compiler; GeneratorExecutableSha256=(Get-FileHash -LiteralPath (Join-Path $generator 'cputester.exe') -Algorithm SHA256).Hash.ToLowerInvariant() NativeSourceSha256=(Get-FileHash -LiteralPath (Join-Path $output 'winuae-native.c') -Algorithm SHA256).Hash.ToLowerInvariant() diff --git a/scripts/test-copper68k-winuae-trap-bounds.ps1 b/scripts/test-copper68k-winuae-trap-bounds.ps1 new file mode 100644 index 00000000..a98b9473 --- /dev/null +++ b/scripts/test-copper68k-winuae-trap-bounds.ps1 @@ -0,0 +1,36 @@ +#requires -Version 7.0 +[CmdletBinding()] +param( + [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $InputDirectory, + [string] $NativeLibrary, + [string] $OutputDirectory = 'artifacts/winuae-trap-bounds-audit' +) +$ErrorActionPreference = 'Stop' +$repo = Split-Path -Parent $PSScriptRoot +$root = (Resolve-Path -LiteralPath $InputDirectory).Path +if (-not $NativeLibrary) { $NativeLibrary = Join-Path $root 'm68k_cpu_tester.dll' } +$library = (Resolve-Path -LiteralPath $NativeLibrary).Path +$output = [IO.Path]::GetFullPath($OutputDirectory, $repo) +if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } +New-Item -ItemType Directory -Path $output | Out-Null +$settings = @{ + COPPER68K_RUN_WINUAE_TRAP_BOUNDS_AUDIT = '1' + COPPER68K_WINUAE_TRAP_BOUNDS_PATH = $root + COPPER68K_WINUAE_CPUTEST_LIBRARY = $library + COPPER68K_SYNTHETIC_REPORT_DIR = $output +} +$saved = @{} +try { + foreach ($name in $settings.Keys) { + $saved[$name] = [Environment]::GetEnvironmentVariable($name) + [Environment]::SetEnvironmentVariable($name, $settings[$name]) + } + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled' --logger 'trx;LogFileName=audit.trx' --results-directory $output + if ($LASTEXITCODE -ne 0) { throw 'Qualified trap/bounds audit failed' } + [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw + $counts = $trx.TestRun.ResultSummary.Counters + if ([int]$counts.executed -ne 1 -or [int]$counts.passed -ne 1) { throw 'Trap/bounds audit did not execute its required test' } + if (-not (Test-Path -LiteralPath (Join-Path $output 'winuae-trap-bounds-audit.json'))) { throw 'Trap/bounds coverage report is missing' } +} finally { + foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } +} diff --git a/scripts/winuae/trap-bounds-pc.patch b/scripts/winuae/trap-bounds-pc.patch new file mode 100644 index 00000000..158e2cc4 --- /dev/null +++ b/scripts/winuae/trap-bounds-pc.patch @@ -0,0 +1,10 @@ +--- a/gencpu/gencpu.cpp ++++ b/gencpu/gencpu.cpp +@@ + out("}\n"); ++ sync_m68k_pc(); + out("if ((extra & 0x800) && GET_CFLG()) {\n"); +@@ + fill_prefetch_0(); ++ sync_m68k_pc(); + out("if (cctrue(%d)) {\n", curi->cc); From eaddcacdfc582ab0b87cd86eee49fefc52c9830d Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 04:14:46 +0300 Subject: [PATCH 022/211] Qualify breakpoint exception PCs and share strict reference audits --- .../M68kMusashiConformanceTests.cs | 2 +- .../M68kWinUaeTrapBoundsAuditTests.cs | 72 +++++++----- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 108 ++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 25 ++++ scripts/prepare-copper68k-winuae.ps1 | 26 +++-- ...-copper68k-winuae-qualified-exceptions.ps1 | 40 +++++++ scripts/test-copper68k-winuae-trap-bounds.ps1 | 30 +---- scripts/winuae/breakpoint-pc.patch | 7 ++ 8 files changed, 247 insertions(+), 63 deletions(-) create mode 100644 scripts/test-copper68k-winuae-qualified-exceptions.ps1 create mode 100644 scripts/winuae/breakpoint-pc.patch diff --git a/Copper68k.Tests/M68kMusashiConformanceTests.cs b/Copper68k.Tests/M68kMusashiConformanceTests.cs index d422d134..fddb5a49 100644 --- a/Copper68k.Tests/M68kMusashiConformanceTests.cs +++ b/Copper68k.Tests/M68kMusashiConformanceTests.cs @@ -65,7 +65,7 @@ public sealed partial class M68kMusashiConformanceTests ["m68030/bin/move16_030.bin"] = "MOVE16 is not currently implemented", ["m68040/bin/32bit_disp.bin"] = "full-extension indexed addressing is not fully implemented", ["m68040/bin/arch_unaligned.bin"] = "unaligned access and address-error completion semantics still diverge from this fixture", - ["m68040/bin/bkpt.bin"] = "BKPT handler setup or related addressing forms are not currently implemented", + ["m68040/bin/bkpt.bin"] = "Optional fixture's breakpoint handler and expected frame have not been requalified; standalone BKPT illegal-exception fallback is covered by synthetic and qualified WinUAE audits", ["m68040/bin/callm_rtm.bin"] = "CALLM/RTM behavior is not currently implemented", ["m68040/bin/ec040_positive.bin"] = "EC040 control/MMU behavior is not currently modeled", ["m68040/bin/fpu_arith.bin"] = "MC68040 FPU arithmetic behavior is not currently modeled", diff --git a/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs b/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs index 964b529e..311de1cb 100644 --- a/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs +++ b/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs @@ -10,42 +10,59 @@ public sealed partial class M68kWinUaeCpuTesterConformanceTests private const string TrapBoundsPatchHash = "ca94d93ec49447e853769bb93bd4e823fe313854aba59601161b35ef8f7bcca4"; [EnvironmentFact("COPPER68K_RUN_WINUAE_TRAP_BOUNDS_AUDIT", "audit qualified WinUAE TRAPcc/CHK2 saved PCs")] - public void WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled() + public void WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled() => RunQualifiedExceptionPreset(new( + "TrapBounds", "TRAP_BOUNDS", "gencpu-trap-bounds.cpp", "trap-bounds-pc.patch", "TrapBounds", + TrapBoundsSourceHash, TrapBoundsPatchHash, "winuae-trap-bounds-audit.json", + ["68020", "68030", "68040", "68060", "68EC020"], + ["68EC020", "68020", "68030", "68040", "68060", "A1200"], TrapBoundsFamilies, TrapBoundsCounts, + "TRAPcc on EC020/A1200/020/030/040/060; CHK2 B/W/L on EC020/A1200/020/030/040. Basic CCR 0/31 and user/supervisor, full extensions enabled, no incoming trace/bus faults. CHK2 is unavailable on 060 and is covered architecturally by synthetic tests, not this generated preset. Patched software reference, not unchanged upstream or silicon qualification.")); + + [EnvironmentFact("COPPER68K_RUN_WINUAE_BREAKPOINT_AUDIT", "audit qualified WinUAE BKPT illegal-exception saved PCs")] + public void WinUaeBreakpointExceptionsAcrossSelectedModelsWhenEnabled() => RunQualifiedExceptionPreset(new( + "Breakpoints", "BREAKPOINT", "gencpu-breakpoints.cpp", "breakpoint-pc.patch", "Breakpoint", + "d83606b597e5bd38efc289e0ecbd1d41843e67ecedaac72e4b9335312d2ead21", "27b0fb21a7fadbe0bf92ae42074ceaf665d7c19a83efdc841bc7730befc87a7b", "winuae-breakpoint-audit.json", + ["68010", "68020", "68030", "68040", "68060", "68EC020"], + ["68010", "68EC020", "68020", "68030", "68040", "68060", "A1200"], + _ => ["BKPT"], (_, _) => (32, 32), + "BKPT illegal-exception fallback on 010/EC020/A1200/020/030/040/060. Basic CCR 0/31 and user/supervisor. No incoming trace/bus faults or external breakpoint instruction replacement; acknowledge pins and physical cycles are unqualified. Patched software reference, not unchanged upstream or silicon qualification.")); + + private void RunQualifiedExceptionPreset(QualifiedExceptionPreset preset) { - var root = Environment.GetEnvironmentVariable("COPPER68K_WINUAE_TRAP_BOUNDS_PATH"); - var library = Environment.GetEnvironmentVariable(LibraryVariable); + var root = Environment.GetEnvironmentVariable($"COPPER68K_WINUAE_{preset.EnvironmentKey}_PATH"); + var library = Environment.GetEnvironmentVariable($"COPPER68K_WINUAE_{preset.EnvironmentKey}_LIBRARY") + ?? Environment.GetEnvironmentVariable(LibraryVariable); var output = Environment.GetEnvironmentVariable("COPPER68K_SYNTHETIC_REPORT_DIR"); if (string.IsNullOrWhiteSpace(root) || string.IsNullOrWhiteSpace(library) || string.IsNullOrWhiteSpace(output)) - throw new XunitException("Trap/bounds audit requires qualified fixtures, native library and report directory."); + throw new XunitException($"{preset.Name} audit requires qualified fixtures, native library and report directory."); var manifestFile = Path.Combine(root, "manifest.json"); var json = File.ReadAllText(manifestFile); - var manifest = JsonSerializer.Deserialize(json) ?? throw new XunitException("Missing trap/bounds manifest."); + var manifest = JsonSerializer.Deserialize(json) ?? throw new XunitException($"Missing {preset.Name} manifest."); using var identity = JsonDocument.Parse(json); var fields = identity.RootElement; - var sourcePath = Path.Combine(root, "gencpu-trap-bounds.cpp"); + var sourcePath = Path.Combine(root, preset.SourceName); var normalizedSourceHash = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData( System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(sourcePath).Replace("\r\n", "\n")))).ToLowerInvariant(); if (manifest.Schema != 1 || manifest.GeneratorCommit != GeneratorPin || manifest.RunnerCommit != RunnerPin || - fields.GetProperty("Preset").GetString() != "TrapBounds" || normalizedSourceHash != TrapBoundsSourceHash || - fields.GetProperty("TrapBoundsPatchSha256").GetString() != TrapBoundsPatchHash || - Hash(sourcePath) != fields.GetProperty("TrapBoundsSourceSha256").GetString() || - Hash(Path.Combine(root, "trap-bounds-pc.patch")) != TrapBoundsPatchHash || + fields.GetProperty("Preset").GetString() != preset.Name || normalizedSourceHash != preset.SourceHash || + fields.GetProperty(preset.ManifestPrefix + "PatchSha256").GetString() != preset.PatchHash || + Hash(sourcePath) != fields.GetProperty(preset.ManifestPrefix + "SourceSha256").GetString() || + Hash(Path.Combine(root, preset.PatchName)) != preset.PatchHash || Hash(Path.Combine(root, "cputester.exe")) != fields.GetProperty("GeneratorExecutableSha256").GetString() || Hash(library) != manifest.NativeLibrarySha256) - throw new XunitException("Trap/bounds source, patch, generator or native identity is unqualified."); - var profileIds = new[] { "68020", "68030", "68040", "68060", "68EC020" }; + throw new XunitException($"{preset.Name} source, patch, generator or native identity is unqualified."); + var profileIds = preset.ProfileIds; if (manifest.Profiles.Length != profileIds.Length || !manifest.Profiles.Select(x => x.Id).Order().SequenceEqual(profileIds.Order())) - throw new XunitException("Trap/bounds audit requires all five advanced profiles, without empty or duplicate selections."); - var models = ModelSpec.All.Where(x => x.Id is not ("68000" or "68010")).ToArray(); - foreach (var model in models) ValidateWinUaeProfile(root, manifest, model, requiredOpcodes: TrapBoundsFamilies(model)); - var rows = new List(); - var probes = new List(); + throw new XunitException($"{preset.Name} audit requires its complete profile selection, without empty or duplicate selections."); + var models = preset.ModelIds.Select(id => ModelSpec.All.Single(x => x.Id == id)).ToArray(); + foreach (var model in models) ValidateWinUaeProfile(root, manifest, model, requiredOpcodes: preset.Families(model)); + var rows = new List(); + var probes = new List(); using var tester = NativeTester.Load(library); foreach (var model in models) { var fixture = manifest.Profiles.Single(x => x.Id == FixtureId(model.Id)); var path = Path.Combine(root, fixture.Id); - foreach (var family in TrapBoundsFamilies(model)) + foreach (var family in preset.Families(model)) { var register = tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptResult: true); var sr = tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptSr: 0x10); @@ -55,24 +72,24 @@ public void WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled() frame.Detail.Contains("frame byte", StringComparison.Ordinal); probes.Add(new(model.Id, family, detected, register.ExecutedCases, sr.ExecutedCases, frame.ExecutedCases)); var result = tester.Run(path, family, fixture.CpuLevel, false, false, model); - var expected = TrapBoundsCounts(model, family); + var expected = preset.Counts(model, family); var passing = detected && result.Passed && result.ExecutedCases == expected.Cases && - tester.FrameChecks == expected.Frames && tester.MaskedCases == (family == "TRAPcc" ? 0 : expected.Cases); + tester.FrameChecks == expected.Frames && tester.MaskedCases == (family.StartsWith("CHK2.", StringComparison.Ordinal) ? expected.Cases : 0); rows.Add(new(model.Id, family, passing ? "passing" : tester.UnsupportedExecution ? "unsupported" : "mismatching", result.ExecutedCases, tester.FrameChecks, tester.MaskedCases, detected, result.Detail)); _output.WriteLine($"{model.Id}/{family}: {result.ExecutedCases} callbacks, {tester.FrameChecks} frames; controls={detected}."); } } Directory.CreateDirectory(output); - File.WriteAllText(Path.Combine(output, "winuae-trap-bounds-audit.json"), JsonSerializer.Serialize(new + File.WriteAllText(Path.Combine(output, preset.ReportName), JsonSerializer.Serialize(new { - schema = 1, reference = "WinUAE trap/bounds preset with explicit Motorola-qualified saved-PC correction", + schema = 1, reference = $"WinUAE {preset.Name} preset with explicit Motorola-qualified saved-PC correction", manifest.GeneratorCommit, manifest.RunnerCommit, manifest.NativeLibrarySha256, manifestSha256 = Hash(manifestFile), generatorSourceSha256 = Hash(sourcePath), - generatorNormalizedSourceSha256 = TrapBoundsSourceHash, generatorPatchSha256 = TrapBoundsPatchHash, + generatorNormalizedSourceSha256 = preset.SourceHash, generatorPatchSha256 = preset.PatchHash, adapterAssemblySha256 = Hash(typeof(M68kWinUaeCpuTesterConformanceTests).Assembly.Location), cpuAssemblySha256 = Hash(typeof(Copper68k.M68kCoreFactory).Assembly.Location), - qualification = "TRAPcc on EC020/A1200/020/030/040/060; CHK2 B/W/L on EC020/A1200/020/030/040. Basic CCR 0/31 and user/supervisor, full extensions enabled, no incoming trace/bus faults. CHK2 is unavailable on 060 and is covered architecturally by synthetic tests, not this generated preset. Patched software reference, not unchanged upstream or silicon qualification.", + qualification = preset.Qualification, passing = rows.Count(x => x.Status == "passing"), mismatching = rows.Count(x => x.Status == "mismatching"), unsupported = rows.Count(x => x.Status == "unsupported"), executedCases = rows.Sum(x => (long)x.ExecutedCases), exceptionFrames = rows.Sum(x => (long)x.ExceptionFrames), maskedSrCases = rows.Sum(x => (long)x.MaskedSrCases), @@ -96,8 +113,11 @@ private static (int Cases, uint Frames) TrapBoundsCounts(ModelSpec model, string (_, "CHK2.L") => (874, 406), _ => throw new XunitException($"Unqualified trap/bounds family: {model.Id}/{family}") }; - private sealed record WinUaeTrapBoundsRow(string Model, string Family, string Status, int ExecutedCases, + private sealed record QualifiedExceptionPreset(string Name, string EnvironmentKey, string SourceName, string PatchName, + string ManifestPrefix, string SourceHash, string PatchHash, string ReportName, string[] ProfileIds, string[] ModelIds, + Func Families, Func Counts, string Qualification); + private sealed record WinUaeQualifiedExceptionRow(string Model, string Family, string Status, int ExecutedCases, uint ExceptionFrames, uint MaskedSrCases, bool Controls, string Detail); - private sealed record WinUaeTrapBoundsProbe(string Model, string Family, bool Detected, + private sealed record WinUaeQualifiedExceptionProbe(string Model, string Family, bool Detected, int RegisterCases, int SrCases, int FrameCases); } diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 9e472f85..fe8a42e3 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -1585,3 +1585,111 @@ input hashes and selections are checked again. Evidence: `artifacts/m6-trap-bounds-full/` and `artifacts/m6-trap-bounds-gate.log`. Consumer package/replay qualification remains the earlier .48 evidence; this follow-up changes only test adapters, scripts and documentation. + +## Breakpoint fallback qualification and shared audit runner (2026-10-05) + +The original Basic audit fails BKPT on all seven applicable CPU profiles at the +first callback: `4848` at `0087FFA0` expects stacked PC `0087FFA2`, while the CPU +saves `0087FFA0`. Pinned `gencpu.cpp` synchronizes its two-byte PC offset before +calling `op_illg`. The documented illegal-exception PC is the causing instruction, +so this is a reference defect. Authority: +[MC68000UM 5.1.4 / 6.3.6](https://www.nxp.com/docs/en/reference-manual/MC68000UM.pdf), +[MC68020UM 6.1.5 / 6.1.10](https://www.nxp.com/docs/en/data-sheet/MC68020UM.pdf), +[MC68040UM 8.2.4 / 8.2.8](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +and [MC68060UM 8.2.4 / 8.2.8](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf). +010 continues illegal-instruction processing after an acknowledge and does not +accept replacement data. 020/EC020 support an external replacement instruction, +or illegal processing on BERR. 040/060 enter illegal processing after TA or TEA. +The current public bus API provides no breakpoint acknowledge/replacement device; +only the integer-state illegal-exception fallback is qualified here. + +The new separate `Breakpoints` preset replaces the generator's PC synchronization +with `m68k_pc_offset = 0` in a copied source, leaving the opcode address available +to the exception constructor. Original reference sources and Basic inputs remain +unchanged. All eight BKPT encodings, both ordinary privilege states and CCR 0/31 +run on 010, EC020, A1200, 020, 030, 040 and 060: **32 callbacks and 32 frame checks +per profile, 224 total**, with zero masked SR bits. Every register/X/frame +corruption control is detected (**21 controls**). 000 does not implement BKPT; +its defined illegal-word handling remains covered by the synthetic suite. +No incoming trace/M/fault rounds, physical acknowledge cycles or external +instruction substitution are qualified by this preset. + +The test-internal `QualifiedExceptionPreset` runner shares exact source/patch/ +executable/library checks, complete profile/family and fixture preflight, +comparator controls, fixed callback/frame gates and coverage reporting between +Breakpoints and TrapBounds. Each preset has its own optional native-library +environment override, so both can run in one ordinary CPU-suite invocation. +The existing common library variable still works. The dedicated TrapBounds CLI +is retained as a forwarding command; the generic command executes either preset +with the same fresh-output, environment-restoration and non-skipped-test gates: + +```powershell +./scripts/prepare-copper68k-winuae.ps1 -GeneratorSource -RunnerSource -VcVars64 -Preset Breakpoints -OutputDirectory artifacts/breakpoint-inputs +./scripts/test-copper68k-winuae-qualified-exceptions.ps1 -Preset Breakpoints -InputDirectory artifacts/breakpoint-inputs -OutputDirectory artifacts/breakpoint-audit +``` + +An isolated CPU mutation saves opcode PC + 2 on BKPT, narrowly affecting the +advanced system path and BKPT-only illegal entry in the 000/010 core. All seven +reference groups detect it on their first callback. The existing synthetic +`system-model` batch detects **512 mismatches per profile, 4,096 total**, with +zero unsupported or untested cases; its other cases remain passing. Both source +files are restored byte-for-byte. This proves the saved-PC checks in the existing +synthetic coverage as well as the new reference preset; no old test is retired. +Six copied-input controls per preset (**12 total**) reject missing/changed data, +empty or duplicate profiles, an empty family set and modified reference source +before native execution. Evidence: `artifacts/m6-breakpoint-pc-mutation/` and +`artifacts/m6-breakpoint-preflight/`. + +The initial preparer attempt stopped without a manifest because its new preset +did not enter the source-patching branch. That failed directory is not an input +qualification. The corrected preparer uses a fresh v2 directory. Initial +callback-cardinality discovery rejected a provisional 128-case contract while +all native comparisons passed; the frozen required count is the actually +executed 32 per profile. Neither preparer failure nor discovery is reported as +an emulator mismatch or a passing audit. + +Qualified `artifacts/m6-breakpoint-qualified-inputs-v2/` identities: + +- Manifest: `a2503a7bd97335f92d19b8f52257fcf0029d92a46bcc994390e5fc530020519d`. +- Normalized source: `d83606b597e5bd38efc289e0ecbd1d41843e67ecedaac72e4b9335312d2ead21`. +- Normalized patch: `27b0fb21a7fadbe0bf92ae42074ceaf665d7c19a83efdc841bc7730befc87a7b`. +- Generator executable: `f6185f3231feac952bf321ffa5c0c61ec8a8600cf49f361ecc84290a51ed742c`. +- Native bridge: `f1b17372c36560a4557016c29bf9b438d9744dd7f9f67b533afcea5d42585f20`. + +Generator seeds initialize xorshift state to 1 per test set, one focused round. +Regenerating TrapBounds with the shared preparer retains its normalized source +and patch hashes and all 21 passing groups / 951,522 callbacks / 476,339 frames. +Both old and new CLIs execute their complete selection. Evidence: +`artifacts/m6-breakpoint-command/` and +`artifacts/m6-breakpoint-regenerated-trap-bounds/`. Qualification remains patched +software agreement, not unchanged upstream or hardware measurement. + +A stale BKPT exclusion was found in the optional **m68k-rs extra** adapter, not +the pinned Musashi suite. Its exact fixture is unavailable locally, so the +exclusion is retained with a corrected reason: standalone fallback is now +qualified, while that program's handler/frame assumptions still require audit. +No Musashi exclusion is removed, and absent optional inputs remain unavailable +coverage. Milestone 6 retains its earlier reference, advanced-restoration and +consolidation requirements. No production CPU correction, package publication, +physical timing claim or public API change is made in this follow-up. + +Final Release validation passes **4,777 CPU tests with ten optional skips** and +zero failures, including both enabled exception presets. The deterministic gate +validates **11,168,408 logical cases in 469 reporting batches** with +`roadmapComplete=false`. Fresh pinned SingleStepTests passes 312,500 selected +cases in 125 files; Musashi remains 536 passing programs and 88 exclusions. +The qualified 040/060 TRAP trace audit passes 512 callbacks, 256 incoming-T1 +cases, 512 frames and six controls. The unchanged Basic audit still fails: +1,322 passing, 46 mismatching, 13 unsupported and zero untested groups over +11,335,687 callbacks and 1,525,385 frames, with all 32 controls passing. It is +not overwritten or narrowed by the new preset. + +Evidence: `artifacts/m6-breakpoint-full/`, `artifacts/m6-breakpoint-gate.log`, +`artifacts/m6-breakpoint-final-command/`, `artifacts/m6-breakpoint-broad/` and +`artifacts/m6-breakpoint-trace/`. At baseline `f249fe9`, the unchanged production +source builds CPU assembly SHA-256 +`1a4b59a07ddb13f6cce318c901cca6d025c468b25a65db1c89aa7b52c31f8012`; +the final adapter assembly is +`ce24a4016c02922a6a99ecc856d54497b85ebfd51dc40fb9ed93bb6ac2d9c91d`. +Earlier .48 consumer-package qualification is retained; no new consumer package +or native replay is needed for these adapter/script/documentation changes. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index e37f88d6..74815af8 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -930,3 +930,28 @@ Final follow-up validation passes 4,776 CPU tests with ten optional skips, including the enabled trap/bounds audit. The deterministic gate retains 11,168,408 cases in 469 batches and `roadmapComplete=false`; fresh pinned SingleStepTests/Musashi and the earlier qualified trace preset pass. + +The breakpoint follow-up qualifies a separate corrected WinUAE `Breakpoints` +preset against Motorola's illegal-exception saved-PC rule. All eight encodings +pass on 010 and the six advanced profiles, at 224 callbacks / 224 frames with +all 21 comparator controls detected. A narrowly scoped wrong-saved-PC mutation +fails every reference group and 512 existing synthetic cases on each of all +eight profiles. Sources are restored exactly. Twelve preflight controls across +both exception presets reject missing, changed, empty, duplicate and unqualified +inputs. The TrapBounds and breakpoint adapters and CLIs now share their strict +runner, while preserving the old TrapBounds command and source qualification. +Regenerating TrapBounds retains its exact callback/frame coverage. + +External breakpoint replacement and physical acknowledge timing remain +unqualified. The optional m68k-rs extra BKPT exclusion is retained with a corrected +reason because its handler fixture is unavailable; it is not a Musashi exclusion +and not evidence of missing standalone BKPT execution. No regression is retired +and no CPU/package behavior changes. The original broad failures and all prior +milestone 6 requirements remain open; exact identities and scope are recorded in +`COPPER68K_REFERENCE_QUALIFICATION.md`. + +Final validation passes 4,777 CPU tests with ten optional skips, with both +exception reference presets enabled. The deterministic gate retains 11,168,408 +cases in 469 batches and `roadmapComplete=false`; fresh pinned SingleStepTests, +Musashi and the existing trace preset pass. The unchanged broad audit retains +46 mismatching and 13 unsupported groups, so milestone 6 remains in progress. diff --git a/scripts/prepare-copper68k-winuae.ps1 b/scripts/prepare-copper68k-winuae.ps1 index 47aaab8a..c5f23915 100644 --- a/scripts/prepare-copper68k-winuae.ps1 +++ b/scripts/prepare-copper68k-winuae.ps1 @@ -4,7 +4,7 @@ param( [Parameter(Mandatory)] [string] $GeneratorSource, [Parameter(Mandatory)] [string] $RunnerSource, [Parameter(Mandatory)] [string] $VcVars64, - [ValidateSet('Basic','TraceTraps','TrapBounds')] [string] $Preset = 'Basic', + [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints')] [string] $Preset = 'Basic', [string] $OutputDirectory = 'artifacts/winuae-model-inputs' ) $ErrorActionPreference = 'Stop' @@ -48,23 +48,27 @@ try { & ./build68k.exe table68k | Set-Content -Encoding utf8 cpudefs.cpp if ($LASTEXITCODE -ne 0) { throw 'Opcode table generation failed' } $cpuGeneratorSource = 'gencpu.cpp' - if ($Preset -eq 'TrapBounds') { - # MC68020UM 6.1.4 / MC68040UM 8.2.3 require the next PC on - # instruction traps. Patch a copy; Basic keeps the pinned source. - $patchPath = Join-Path $PSScriptRoot 'winuae/trap-bounds-pc.patch' + if ($Preset -in @('TrapBounds','Breakpoints')) { + # TrapBounds: following PC (MC68020UM 6.1.4 / MC68040UM 8.2.3). + # Breakpoints: opcode PC (illegal exception, UM 6.1.5 / 8.2.4). + # Patch a copy; Basic keeps the pinned source. + $sourceName = if ($Preset -eq 'TrapBounds') {'gencpu-trap-bounds.cpp'} else {'gencpu-breakpoints.cpp'} + $patchName = if ($Preset -eq 'TrapBounds') {'trap-bounds-pc.patch'} else {'breakpoint-pc.patch'} + $patchPath = Join-Path $PSScriptRoot ('winuae/' + $patchName) $patch = [IO.File]::ReadAllText($patchPath).Replace("`r`n", "`n") $source = [IO.File]::ReadAllText((Join-Path $generator 'gencpu.cpp')).Replace("`r`n", "`n") $hunks = @($patch -split "(?m)^@@`n" | Select-Object -Skip 1) - if ($hunks.Count -ne 2) { throw 'Trap/bounds patch must contain exactly two hunks' } + $requiredHunks = if ($Preset -eq 'TrapBounds') {2} else {1} + if ($hunks.Count -ne $requiredHunks) { throw "Incorrect $Preset patch hunk count" } foreach ($hunk in $hunks) { $lines = $hunk.TrimEnd("`n").Split("`n") $before = (($lines | Where-Object { $_.StartsWith('-') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" $after = (($lines | Where-Object { $_.StartsWith('+') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" $source = Patch-Once $source $before $after } - $cpuGeneratorSource = Join-Path $output 'gencpu-trap-bounds.cpp' + $cpuGeneratorSource = Join-Path $output $sourceName [IO.File]::WriteAllText($cpuGeneratorSource, $source) - [IO.File]::WriteAllText((Join-Path $output 'trap-bounds-pc.patch'), $patch) + [IO.File]::WriteAllText((Join-Path $output $patchName), $patch) } Run-Compiler @('/nologo','/O2','/EHsc','/w','/I.','/Iinclude',$cpuGeneratorSource,'missing.cpp','readcpu.cpp','cpudefs.cpp','/Fe:gencpu_prog.exe') (Join-Path $output 'gencpu-build.log') & ./gencpu_prog.exe . *> (Join-Path $output 'gencpu-run.log') @@ -132,6 +136,9 @@ void M68KTester_destroy(M68KTesterContext* context) { if ($Preset -eq 'TrapBounds') { $baseIni = $baseIni.Replace('[test=Basic]', '[test=TrapBounds]').Replace('mode=all', 'mode=TRAPcc,CHK2.B,CHK2.W,CHK2.L') } + if ($Preset -eq 'Breakpoints') { + $baseIni = $baseIni.Replace('[test=Basic]', '[test=Breakpoints]').Replace('mode=all', 'mode=BKPT') + } foreach ($model in @( @{id='68000'; cpu='68000'; width=24}, @{id='68010'; cpu='68010'; width=24}, @{id='68EC020'; cpu='68020'; width=24}, @{id='68020'; cpu='68020'; width=32}, @@ -139,6 +146,7 @@ void M68KTester_destroy(M68KTesterContext* context) { @{id='68060'; cpu='68060'; width=32})) { if ($Preset -eq 'TraceTraps' -and $model.id -notin @('68040','68060')) { continue } if ($Preset -eq 'TrapBounds' -and $model.id -in @('68000','68010')) { continue } + if ($Preset -eq 'Breakpoints' -and $model.id -eq '68000') { continue } $profileRoot = Join-Path $output $model.id New-Item -ItemType Directory -Path $profileRoot | Out-Null # Every invocation uses a fresh generator output path; stale data cannot fill a gap. @@ -173,6 +181,8 @@ void M68KTester_destroy(M68KTesterContext* context) { TracePriorityPatchSha256=$(if ($Preset -eq 'TraceTraps') {(Get-FileHash -LiteralPath (Join-Path $output 'trace-priority.patch')).Hash.ToLowerInvariant()} else {$null}) TrapBoundsSourceSha256=$(if ($Preset -eq 'TrapBounds') {(Get-FileHash -LiteralPath (Join-Path $output 'gencpu-trap-bounds.cpp')).Hash.ToLowerInvariant()} else {$null}) TrapBoundsPatchSha256=$(if ($Preset -eq 'TrapBounds') {(Get-FileHash -LiteralPath (Join-Path $output 'trap-bounds-pc.patch')).Hash.ToLowerInvariant()} else {$null}) + BreakpointSourceSha256=$(if ($Preset -eq 'Breakpoints') {(Get-FileHash -LiteralPath (Join-Path $output 'gencpu-breakpoints.cpp')).Hash.ToLowerInvariant()} else {$null}) + BreakpointPatchSha256=$(if ($Preset -eq 'Breakpoints') {(Get-FileHash -LiteralPath (Join-Path $output 'breakpoint-pc.patch')).Hash.ToLowerInvariant()} else {$null}) NativeLibrarySha256=(Get-FileHash -LiteralPath (Join-Path $output 'm68k_cpu_tester.dll') -Algorithm SHA256).Hash.ToLowerInvariant() Compiler=$compiler; GeneratorExecutableSha256=(Get-FileHash -LiteralPath (Join-Path $generator 'cputester.exe') -Algorithm SHA256).Hash.ToLowerInvariant() NativeSourceSha256=(Get-FileHash -LiteralPath (Join-Path $output 'winuae-native.c') -Algorithm SHA256).Hash.ToLowerInvariant() diff --git a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 new file mode 100644 index 00000000..fceba4a4 --- /dev/null +++ b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 @@ -0,0 +1,40 @@ +#requires -Version 7.0 +[CmdletBinding()] +param( + [ValidateSet('TrapBounds','Breakpoints')] [string] $Preset = 'TrapBounds', + [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $InputDirectory, + [string] $NativeLibrary, + [string] $OutputDirectory = 'artifacts/winuae-trap-bounds-audit' +) +$ErrorActionPreference = 'Stop' +$repo = Split-Path -Parent $PSScriptRoot +$root = (Resolve-Path -LiteralPath $InputDirectory).Path +if (-not $NativeLibrary) { $NativeLibrary = Join-Path $root 'm68k_cpu_tester.dll' } +$library = (Resolve-Path -LiteralPath $NativeLibrary).Path +$output = [IO.Path]::GetFullPath($OutputDirectory, $repo) +if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } +New-Item -ItemType Directory -Path $output | Out-Null +$key = if ($Preset -eq 'TrapBounds') {'TRAP_BOUNDS'} else {'BREAKPOINT'} +$testName = if ($Preset -eq 'TrapBounds') {'WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled'} else {'WinUaeBreakpointExceptionsAcrossSelectedModelsWhenEnabled'} +$reportName = if ($Preset -eq 'TrapBounds') {'winuae-trap-bounds-audit.json'} else {'winuae-breakpoint-audit.json'} +$settings = @{ + ('COPPER68K_RUN_WINUAE_' + $key + '_AUDIT') = '1' + ('COPPER68K_WINUAE_' + $key + '_PATH') = $root + ('COPPER68K_WINUAE_' + $key + '_LIBRARY') = $library + COPPER68K_SYNTHETIC_REPORT_DIR = $output +} +$saved = @{} +try { + foreach ($name in $settings.Keys) { + $saved[$name] = [Environment]::GetEnvironmentVariable($name) + [Environment]::SetEnvironmentVariable($name, $settings[$name]) + } + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter "FullyQualifiedName~$testName" --logger 'trx;LogFileName=audit.trx' --results-directory $output + if ($LASTEXITCODE -ne 0) { throw "Qualified $Preset audit failed" } + [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw + $counts = $trx.TestRun.ResultSummary.Counters + if ([int]$counts.executed -ne 1 -or [int]$counts.passed -ne 1) { throw "$Preset audit did not execute its required test" } + if (-not (Test-Path -LiteralPath (Join-Path $output $reportName))) { throw "$Preset coverage report is missing" } +} finally { + foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } +} diff --git a/scripts/test-copper68k-winuae-trap-bounds.ps1 b/scripts/test-copper68k-winuae-trap-bounds.ps1 index a98b9473..a499c31f 100644 --- a/scripts/test-copper68k-winuae-trap-bounds.ps1 +++ b/scripts/test-copper68k-winuae-trap-bounds.ps1 @@ -5,32 +5,6 @@ param( [string] $NativeLibrary, [string] $OutputDirectory = 'artifacts/winuae-trap-bounds-audit' ) +# Keep the existing command and its complete selection contract. $ErrorActionPreference = 'Stop' -$repo = Split-Path -Parent $PSScriptRoot -$root = (Resolve-Path -LiteralPath $InputDirectory).Path -if (-not $NativeLibrary) { $NativeLibrary = Join-Path $root 'm68k_cpu_tester.dll' } -$library = (Resolve-Path -LiteralPath $NativeLibrary).Path -$output = [IO.Path]::GetFullPath($OutputDirectory, $repo) -if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } -New-Item -ItemType Directory -Path $output | Out-Null -$settings = @{ - COPPER68K_RUN_WINUAE_TRAP_BOUNDS_AUDIT = '1' - COPPER68K_WINUAE_TRAP_BOUNDS_PATH = $root - COPPER68K_WINUAE_CPUTEST_LIBRARY = $library - COPPER68K_SYNTHETIC_REPORT_DIR = $output -} -$saved = @{} -try { - foreach ($name in $settings.Keys) { - $saved[$name] = [Environment]::GetEnvironmentVariable($name) - [Environment]::SetEnvironmentVariable($name, $settings[$name]) - } - & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled' --logger 'trx;LogFileName=audit.trx' --results-directory $output - if ($LASTEXITCODE -ne 0) { throw 'Qualified trap/bounds audit failed' } - [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw - $counts = $trx.TestRun.ResultSummary.Counters - if ([int]$counts.executed -ne 1 -or [int]$counts.passed -ne 1) { throw 'Trap/bounds audit did not execute its required test' } - if (-not (Test-Path -LiteralPath (Join-Path $output 'winuae-trap-bounds-audit.json'))) { throw 'Trap/bounds coverage report is missing' } -} finally { - foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } -} +& (Join-Path $PSScriptRoot 'test-copper68k-winuae-qualified-exceptions.ps1') -Preset TrapBounds -InputDirectory $InputDirectory -NativeLibrary $NativeLibrary -OutputDirectory $OutputDirectory diff --git a/scripts/winuae/breakpoint-pc.patch b/scripts/winuae/breakpoint-pc.patch new file mode 100644 index 00000000..f947052b --- /dev/null +++ b/scripts/winuae/breakpoint-pc.patch @@ -0,0 +1,7 @@ +--- a/gencpu/gencpu.cpp ++++ b/gencpu/gencpu.cpp +@@ + case i_BKPT: /* only needed for hardware emulators */ +- sync_m68k_pc(); ++ m68k_pc_offset = 0; + addcycles000(4); From 0041aec87693ac012962506970ea7eab02b1944f Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 04:31:25 +0300 Subject: [PATCH 023/211] Decode unassigned CHK effective addresses as illegal instructions --- .../SyntheticInvalidBoundsBitfieldTests.cs | 23 +++-- Copper68k/M68kAdvancedTimingInterpreter.cs | 5 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 87 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 23 +++++ scripts/test-copper68k-synthetic.ps1 | 2 +- 5 files changed, 132 insertions(+), 8 deletions(-) diff --git a/Copper68k.Tests/Synthetic/SyntheticInvalidBoundsBitfieldTests.cs b/Copper68k.Tests/Synthetic/SyntheticInvalidBoundsBitfieldTests.cs index f700c15e..967c7630 100644 --- a/Copper68k.Tests/Synthetic/SyntheticInvalidBoundsBitfieldTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticInvalidBoundsBitfieldTests.cs @@ -11,13 +11,19 @@ public void InvalidChkSourcesTrapWithoutBoundsOrFlagEffects(string modelId) { var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); var report = new CoverageBatch(modelId, "system-chk-invalid-operands"); - // M68000PM 4-70: CHK.W/.L data sources exclude An. + // M68000PM 4-70: CHK.W/.L data sources exclude An; mode 7 + // registers 5..7 have no assigned EA. MC68020UM 6.1.5 specifies + // vector 4 and the causing instruction PC for illegal encodings. var opcodes = ChkOpcodes().ToArray(); - Assert.Equal(128, opcodes.Length); + Assert.Equal(176, opcodes.Length); Assert.Equal(opcodes.Length, opcodes.Select(x => x.Opcode).Distinct().Count()); Assert.Contains(opcodes, x => x.Opcode == 0x4108); Assert.Contains(opcodes, x => x.Opcode == 0x4f8f); - Assert.DoesNotContain(opcodes, x => x.Opcode is 0x4180 or 0x41bc or 0x41c8); + Assert.Contains(opcodes, x => x.Opcode == 0x413d); + Assert.Contains(opcodes, x => x.Opcode == 0x41bd); + Assert.Contains(opcodes, x => x.Opcode == 0x4f3f); + Assert.Contains(opcodes, x => x.Opcode == 0x4fbf); + Assert.DoesNotContain(opcodes, x => x.Opcode is 0x4180 or 0x413b or 0x413c or 0x41bc or 0x41c8); foreach (var (opcode, family, form) in opcodes) foreach (var supervisor in new[] { false, true }) for (var ccr = 0; ccr < 32; ccr++) @@ -52,9 +58,14 @@ public void InvalidBitfieldOperandsTrapBeforeExtensionOrOperandEffects(string mo { foreach (var word in new[] { false, true }) for (var destination = 0; destination < 8; destination++) - for (var source = 0; source < 8; source++) - yield return ((ushort)(0x4108 | (word ? 0x80 : 0) | destination << 9 | source), - word ? "CHK.W" : "CHK.L", $"A{source},D{destination}"); + { + for (var source = 0; source < 8; source++) + yield return ((ushort)(0x4108 | (word ? 0x80 : 0) | destination << 9 | source), + word ? "CHK.W" : "CHK.L", $"A{source},D{destination}"); + for (var source = 5; source < 8; source++) + yield return ((ushort)(0x4138 | (word ? 0x80 : 0) | destination << 9 | source), + word ? "CHK.W" : "CHK.L", $"unassigned-EA(7,{source}),D{destination}"); + } } private static IEnumerable<(ushort Opcode, string Family, string Form)> BitfieldOpcodes() diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index 96303ba3..d8a20783 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -838,7 +838,10 @@ private static M68020OpcodeKind ClassifyBaseOpcode(ushort opcode) if ((opcode & 0xFFC0) == 0x4800 && logicalMode == 7 && logicalRegister > 1 || (opcode & 0xFFC0) == 0x4A00 && logicalMode == 1) return M68020OpcodeKind.IllegalInstruction; - if ((opcode & 0xF140) == 0x4100 && logicalMode == 1) + // CHK admits data sources, including immediate, but no An or + // unassigned mode-7 EA. Reject these words before execution. + if ((opcode & 0xF140) == 0x4100 && + (logicalMode == 1 || logicalMode == 7 && logicalRegister > 4)) return M68020OpcodeKind.IllegalInstruction; if ((opcode & 0xFF00) == 0x0800 || (opcode & 0xF100) == 0x0100) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index fe8a42e3..49a6f0d8 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -1693,3 +1693,90 @@ the final adapter assembly is `ce24a4016c02922a6a99ecc856d54497b85ebfd51dc40fb9ed93bb6ac2d9c91d`. Earlier .48 consumer-package qualification is retained; no new consumer package or native replay is needed for these adapter/script/documentation changes. + +## Unassigned CHK effective-address words (2026-10-05) + +The unchanged Basic ILLEGAL audit exposed `413D` (CHK.L, unassigned mode 7 / +register 5) at `0087FFA0` on EC020/A1200/020/030/060. The advanced classifier +rejected An but omitted unassigned mode-7 fields. The subsequent general CHK +handler correctly declined them, and execution threw an emulator timing exception +instead of entering architectural vector 4. The existing 000/010/040 paths already +handled those words correctly. + +[M68000PM 4-69/70](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) +defines CHK's data EAs, with mode-7 fields only 0..4. These added words are +explicitly labeled **unassigned EA encodings**, rather than legal operand forms. +[MC68020UM 6.1.5](https://www.nxp.com/docs/en/data-sheet/MC68020UM.pdf) +defines illegal first-word patterns as vector 4 with the causing instruction +address saved. The correction classifies only the CHK mask's An or mode-7 +registers 5..7 before operand execution. Immediate and PC-relative sources and +the adjacent LEA encoding remain admitted. There is no generic illegal fallback, +operand retry, timing-policy change or public API change. + +The existing `system-chk-invalid-operands` batch now contains **176 opcode words**: +128 An-source words plus 48 unassigned words, covering both sizes, all destination +registers, both stacks and every initial CCR. Fixed reference examples include +`413D`, `41BD`, `4F3F` and `4FBF`; legal neighbors and LEA are excluded explicitly. +Independent expectations check saved PC/SR, frame/stack state, all registers, +surrounding memory and forbidden operand reads. The gate requires **11,264 cases +per profile**, an addition of **24,576 cases across eight profiles**. No existing +regression is retired. + +Failed-before evidence: `artifacts/m6-chk-unassigned-before/` reports 8,192 passing +and 3,072 emulator-unsupported cases on each of EC020/A1200/020/030/060; the other +three profiles pass all 11,264. There are no mismatches or untested cases. The +restored classifier passes all **90,112 cases** with zero other statuses in +`artifacts/m6-chk-unassigned-focused/`. + +The unchanged broad audit now completes six additional callbacks and frames per +affected profile before encountering the next unassigned integer word, `4140`. +This separately requires decoding/encoding qualification; it is not treated as a +legal CHK byte form. The 040 `F300` illegal FPU operand/privilege-priority mismatch +also remains open. Overall Basic still fails **1,322 passing, 46 mismatching, +13 unsupported, zero untested groups**, over **11,335,717 callbacks and 1,525,415 +frames**, with 199,327 masked-SR cases and all 32 controls detected. The original +Basic manifest and native library are unchanged. No family exclusion or comparison +mask is added. Evidence: `artifacts/m6-chk-unassigned-broad-qualified/`. + +An initial invocation pointed to the wrong DLL filename; preflight rejected it +before callbacks. The corresponding first full-suite invocation was interrupted +after the setup error was identified. These are not passing validation evidence. +The corrected invocations use the manifest-qualified `m68k_cpu_tester.dll`. + +Private, **unpublished** package `1.5.2-synthetic-dev.49` has SHA-256 +`817d9d166f64135aa894c1af42fce6bbf75b6e5e2e7d099eaefca77be984d5a6`; +the packaged CPU assembly is +`2e37452c73dd5ae46dac1e8bd2fb9ed7f8c44e2493bea3c203120d3798aef583`. +`artifacts/m6-chk-unassigned-package.json` records changed-source and assembly +identities against `eaddcac`. The default-version CPU tested by the reference +adapter is `c01e30c9ba41db44cb37b329af7c3a42a2047890e3462403214f1cc37574bc5f`; +its version differs from the private package, with the same production source. + +Isolated CopperScreen baseline `d9beae8` resolves the exact .49 dependency in +production and separate diagnostic assets, and all four loaded DLLs match the +packaged assembly. Release build has zero warnings/errors. Host **149** pass with +six optional skips, disk **74**, engine **1,080**, and three native Workbench/A1200 +boot and disk-persistence replays pass with no skips. AHX passes **18** tests. +Consumer evidence: `artifacts/chk-unassigned-validation/`, +`artifacts/chk-unassigned-diagnostic-tests/` and +`artifacts/chk-unassigned-production.binlog`; CPU AHX results are in +`artifacts/m6-chk-unassigned-ahx-results/`. Native media is not committed. No +package publication, physical timing or host-throughput qualification is claimed. +Milestone 6 remains in progress with all earlier requirements retained. + +Final validation for this follow-up passes **4,777 CPU tests**, with ten optional +skips and zero failures. The deterministic gate verifies **11,192,984 logical +cases in 469 batches**, retaining `roadmapComplete=false`. It rejects an omitted +020 CHK report and the previous passing report's 8,192-case cardinality. Fresh +pinned SingleStepTests passes 312,500 selected cases in 125 files; Musashi passes +536 programs with 88 explicit exclusions. Both qualified exception presets run +in the full suite: BKPT 224 callbacks / 224 frames / 21 controls, and trap/bounds +951,522 callbacks / 476,339 frames / 63 controls. The separately enabled qualified +TRAP trace audit passes 512 callbacks / 512 frames / six controls. + +Evidence: `artifacts/m6-chk-unassigned-final-full/`, +`artifacts/m6-chk-unassigned-gate.log`, `artifacts/m6-chk-unassigned-gate-controls/` +and `artifacts/m6-chk-unassigned-trace/`. Ordinary CI uses the expanded existing +CHK matrix and updated strict cardinality. The full-suite skips remain unavailable +coverage; the independently run Basic audit remains failing. No seeded audit, +test consolidation, package publication or hardware qualification is added. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 74815af8..6cc42962 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -955,3 +955,26 @@ exception reference presets enabled. The deterministic gate retains 11,168,408 cases in 469 batches and `roadmapComplete=false`; fresh pinned SingleStepTests, Musashi and the existing trace preset pass. The unchanged broad audit retains 46 mismatching and 13 unsupported groups, so milestone 6 remains in progress. + +The unassigned-CHK-EA follow-up corrects advanced decoding of mode-7 registers +5..7 to enter documented illegal-instruction vector 4 before operand effects. +The existing CHK invalid-source matrix adds 48 opcode words, both stacks and +every CCR: 24,576 new cases across all eight profiles. Failed-before runs detect +3,072 unsupported cases per affected advanced profile; the restored guard passes +all 90,112 invalid-CHK cases, with legal neighbors retained. No regression is +retired. The unchanged broad audit advances to the next unassigned word `4140` +and retains its failing status; its remaining families are not excluded. + +The private unpublished .49 package passes the isolated CopperScreen Release +build, host/disk/engine checks and all three native Workbench/A1200 replays. +Source, package, loaded-DLL identities and precise scope are recorded in +`COPPER68K_REFERENCE_QUALIFICATION.md`. All earlier reference, advanced exception +restoration and consolidation requirements remain open; milestone 6 stays in +progress. No physical timing or new release is claimed. + +Final validation passes 4,777 CPU tests with ten optional skips; both qualified +exception presets execute. The deterministic gate verifies 11,192,984 cases in +469 batches, rejects missing/old-cardinality CHK reports and retains +`roadmapComplete=false`. Fresh pinned SingleStepTests/Musashi and the separately +qualified TRAP trace audit pass. The broad audit still fails 46 mismatching and +13 unsupported groups, so the full milestone 6 objective remains open. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index c7d056b7..dbb7a166 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -93,7 +93,7 @@ try { 'logical-shifts'=$(if ($model -in @('68000','68010')) {130561} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {131089}); 'system-basic'=1728; 'system-trap-trace'=$(if ($model -eq '68040') {10368} elseif ($model -eq '68060') {8384} elseif ($model -in @('68000','68010')) {3968} else {5952}); - 'system-chk-invalid-operands'=8192; + 'system-chk-invalid-operands'=11264; 'system-debug-instructions'=$(if ($model -eq '68060') {512} else {256}); 'system-bounds'=$(if ($model -in @('68000','68010')) {11828} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {12488}); 'system-callm'=$(if ($model -in @('68000','68010')) {10832} elseif ($model -in @('68020','68EC020','A1200')) {11000} elseif ($model -in @('68030','68040','68060')) {10964}); From 31a62212c3d1de3e56aaa31531bf1108c5b4f9fb Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 05:02:25 +0300 Subject: [PATCH 024/211] Decode illegal line-four instruction encodings before operand effects --- .../SyntheticInvalidAddressOperationTests.cs | 100 +++++++++++++++ ...yntheticInvalidUnaryMultiplyDivideTests.cs | 26 ++-- .../SyntheticUnassignedFourLineTests.cs | 68 +++++++++++ Copper68k/M68kAdvancedTimingInterpreter.cs | 31 +++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 114 ++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 24 ++++ scripts/test-copper68k-synthetic.ps1 | 6 +- 7 files changed, 359 insertions(+), 10 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticInvalidAddressOperationTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticUnassignedFourLineTests.cs diff --git a/Copper68k.Tests/Synthetic/SyntheticInvalidAddressOperationTests.cs b/Copper68k.Tests/Synthetic/SyntheticInvalidAddressOperationTests.cs new file mode 100644 index 00000000..0d6ac054 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticInvalidAddressOperationTests.cs @@ -0,0 +1,100 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticInvalidAddressOperationTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void InvalidControlAddressesTrapBeforeRegisterStackOrBranchEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "control-invalid-addresses"); + // M68000PM 4-108/109/110/159: control EAs only. PEA's mode 0 + // and mode 1 words are SWAP/BKPT, so they are not illegal PEA. + // 49C0..49C7 are EXTB.L aliases, covered by transfer-registers. + // Mode-7 registers 5..7 are explicitly unassigned EA encodings. + var opcodes = InvalidOpcodes().ToArray(); + Assert.Equal(372, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Select(x => x.Opcode).Distinct().Count()); + Assert.Contains(opcodes, x => x.Opcode == 0x41c0); + Assert.Contains(opcodes, x => x.Opcode == 0x4fff); + Assert.Contains(opcodes, x => x.Opcode == 0x4858); + Assert.Contains(opcodes, x => x.Opcode == 0x487c); + Assert.Contains(opcodes, x => x.Opcode == 0x4e80); + Assert.Contains(opcodes, x => x.Opcode == 0x4eff); + Assert.DoesNotContain(opcodes, x => x.Opcode is >= 0x49c0 and <= 0x49c7); + Assert.DoesNotContain(opcodes, x => x.Opcode is 0x4840 or 0x4848 or 0x41d0 or 0x41fb or 0x487b or 0x4e90 or 0x4ed0); + foreach (var (opcode, family, form) in opcodes) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, $"{family}/invalid/{form}", supervisor, ccr); + report.Complete(output); + } + + private static IEnumerable<(ushort Opcode, string Family, string Form)> InvalidOpcodes() + { + foreach (var form in InvalidForms()) + { + for (var destination = 0; destination < 8; destination++) + { + if (destination == 4 && form.Mode == 0) continue; // EXTB.L Dn + yield return ((ushort)(0x41c0 | destination << 9 | form.Mode << 3 | form.Register), + "LEA.L", $"{FormName(form)}->A{destination}"); + } + foreach (var (encoding, family) in new[] { (0x4e80, "JSR"), (0x4ec0, "JMP") }) + yield return ((ushort)(encoding | form.Mode << 3 | form.Register), family, FormName(form)); + if (form.Mode is not (0 or 1)) + yield return ((ushort)(0x4840 | form.Mode << 3 | form.Register), "PEA.L", FormName(form)); + } + } + + private static IEnumerable InvalidForms() + { + foreach (var mode in new[] { 0, 1, 3, 4 }) + for (var register = 0; register < 8; register++) yield return new(mode, register); + for (var register = 4; register < 8; register++) yield return new(7, register); + } + + private static string FormName(OperandForm form) => form.Mode == 7 && form.Register > 4 + ? $"unassigned-EA(7,{form.Register})" : form.Id; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void InvalidMovemAddressesTrapBeforeMaskOrTransferEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "transfer-movem-invalid-operands"); + // M68000PM 4-128..130: store admits control-alterable/predecrement; + // load admits control/postincrement. Store mode 0 is EXT, not MOVEM. + var opcodes = InvalidMovemOpcodes().ToArray(); + Assert.Equal(100, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Select(x => x.Opcode).Distinct().Count()); + Assert.Contains(opcodes, x => x.Opcode == 0x4888); + Assert.Contains(opcodes, x => x.Opcode == 0x48ff); + Assert.Contains(opcodes, x => x.Opcode == 0x4c80); + Assert.Contains(opcodes, x => x.Opcode == 0x4cff); + Assert.DoesNotContain(opcodes, x => x.Opcode is 0x4880 or 0x48c0 or 0x48a0 or 0x4c98 or 0x4cba); + foreach (var (opcode, family, form) in opcodes) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + // Nonempty mask selects D0 and A0; a mistaken dispatch must not + // update either register, any operand canary, or its base. + InvalidOperandScenario.Run(machine, report, opcode, $"{family}/invalid/{form}", supervisor, ccr, 0x0101); + report.Complete(output); + } + + private static IEnumerable<(ushort Opcode, string Family, string Form)> InvalidMovemOpcodes() + { + foreach (var load in new[] { false, true }) + foreach (var size in new[] { 2, 4 }) + { + foreach (var mode in load ? new[] { 0, 1, 4 } : new[] { 1, 3 }) + for (var register = 0; register < 8; register++) yield return Case(new(mode, register)); + for (var register = load ? 4 : 2; register < 8; register++) yield return Case(new(7, register)); + (ushort, string, string) Case(OperandForm form) => + ((ushort)(0x4880 | (load ? 0x0400 : 0) | (size == 4 ? 0x0040 : 0) | form.Mode << 3 | form.Register), + $"MOVEM.{(size == 4 ? "L" : "W")}", $"{(load ? "load" : "store")}/{FormName(form)}"); + } + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticInvalidUnaryMultiplyDivideTests.cs b/Copper68k.Tests/Synthetic/SyntheticInvalidUnaryMultiplyDivideTests.cs index a52f0498..57e148a4 100644 --- a/Copper68k.Tests/Synthetic/SyntheticInvalidUnaryMultiplyDivideTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticInvalidUnaryMultiplyDivideTests.cs @@ -14,9 +14,9 @@ public void InvalidUnaryOperandsTrapWithoutOperandEffects(string modelId) // M68000PM: CLR/NEG/NEGX/NOT/TAS/NBCD require data-alterable EAs. // TST gained PC/immediate and word/long An sources on 020 (4-193). // 020+ NBCD mode-1 words encode LINK.L instead (4-111). Size 3 and - // unassigned mode-7 registers are outside this matrix. + // unassigned mode-7 registers are outside this matrix except TST. var opcodes = UnaryOpcodes(machine.Model.FullIndex, modelId == "68060").ToArray(); - Assert.Equal(modelId == "68060" ? 152 : machine.Model.FullIndex ? 154 : 187, opcodes.Length); + Assert.Equal(modelId == "68060" ? 161 : machine.Model.FullIndex ? 163 : 196, opcodes.Length); Assert.Equal(opcodes.Length, opcodes.Select(x => x.Opcode).Distinct().Count()); Assert.Contains(opcodes, x => x.Opcode == 0x4008); Assert.Contains(opcodes, x => x.Opcode == 0x483c); @@ -25,6 +25,8 @@ public void InvalidUnaryOperandsTrapWithoutOperandEffects(string modelId) Assert.Equal(!machine.Model.FullIndex, opcodes.Any(x => x.Opcode == 0x4a48)); Assert.Equal(!machine.Model.FullIndex, opcodes.Any(x => x.Opcode == 0x4a3c)); Assert.Equal(!machine.Model.FullIndex, opcodes.Any(x => x.Opcode == 0x4808)); + Assert.Contains(opcodes, x => x.Opcode == 0x4a3d); + Assert.Contains(opcodes, x => x.Opcode == 0x4abf); Assert.DoesNotContain(opcodes, x => x.Opcode is 0x4000 or 0x4810 or 0x4ad0 or 0x40c0); foreach (var (opcode, family, form) in opcodes) @@ -84,14 +86,20 @@ public void InvalidLongMultiplyDivideSourcesTrapBeforeExtensionSelectedOperation foreach (var (encoding, family) in new[] { (0x4000, "NEGX"), (0x4200, "CLR"), (0x4400, "NEG"), (0x4600, "NOT"), (0x4a00, "TST"), (0x4800, "NBCD"), (0x4ac0, "TAS") }) for (var size = 0; size < (family is "NBCD" or "TAS" ? 1 : 3); size++) - foreach (var form in InvalidDestinations()) { - if (family == "TST" && advancedTst && (form.Mode == 7 || size != 0)) continue; - if (family == "NBCD" && advancedTst && form.Mode == 1) continue; - // MC68060UM 9.2.2: HALT/PULSE reuse two TAS mode-1 words. - if (family == "TAS" && debug060 && form.Mode == 1 && form.Register is 0 or 4) continue; - yield return ((ushort)(encoding | size << 6 | form.Mode << 3 | form.Register), - $"{family}.{new[] { "B", "W", "L" }[size]}", form.Id); + foreach (var form in InvalidDestinations()) + { + if (family == "TST" && advancedTst && (form.Mode == 7 || size != 0)) continue; + if (family == "NBCD" && advancedTst && form.Mode == 1) continue; + // MC68060UM 9.2.2: HALT/PULSE reuse two TAS mode-1 words. + if (family == "TAS" && debug060 && form.Mode == 1 && form.Register is 0 or 4) continue; + yield return ((ushort)(encoding | size << 6 | form.Mode << 3 | form.Register), + $"{family}.{new[] { "B", "W", "L" }[size]}", form.Id); + } + if (family == "TST") + for (var register = 5; register < 8; register++) + yield return ((ushort)(encoding | size << 6 | 0x38 | register), + $"TST.{new[] { "B", "W", "L" }[size]}", $"unassigned-EA(7,{register})"); } } diff --git a/Copper68k.Tests/Synthetic/SyntheticUnassignedFourLineTests.cs b/Copper68k.Tests/Synthetic/SyntheticUnassignedFourLineTests.cs new file mode 100644 index 00000000..40a7b2b6 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticUnassignedFourLineTests.cs @@ -0,0 +1,68 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticUnassignedFourLineTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void Unassigned4140WordsTrapBeforeOperandEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "system-unassigned-4140"); + // M68000PM 4-69/70, 4-110: CHK uses bit 6 = 0, and LEA + // requires bits 8..6 = 111. The 0100 rrr 101 mmm rrr pattern + // is unassigned, not a byte CHK or a legal LEA operand form. + // MC68020UM 6.1.5: illegal first words enter vector 4 at opcode PC. + var opcodes = Unassigned4140Words().ToArray(); + Assert.Equal(512, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Distinct().Count()); + Assert.Contains((ushort)0x4140, opcodes); + Assert.Contains((ushort)0x417c, opcodes); + Assert.Contains((ushort)0x4f7f, opcodes); + Assert.DoesNotContain((ushort)0x4100, opcodes); + Assert.DoesNotContain((ushort)0x4180, opcodes); + Assert.DoesNotContain((ushort)0x41c0, opcodes); + foreach (var opcode in opcodes) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, + $"UNASSIGNED/4140/field={(opcode >> 9) & 7}/ea=({(opcode >> 3) & 7},{opcode & 7})", + supervisor, ccr); + report.Complete(output); + } + + private static IEnumerable Unassigned4140Words() + { + for (var registerField = 0; registerField < 8; registerField++) + for (var mode = 0; mode < 8; mode++) + for (var register = 0; register < 8; register++) + yield return (ushort)(0x4140 | registerField << 9 | mode << 3 | register); + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void Unassigned4eSystemWordsTrapBeforeExtensionOrStackEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "system-unassigned-4e"); + // M68000PM instruction-format summary and MC68020UM 6.1.5: + // these first words are unassigned on the selected processors. The + // adjacent TRAP/LINK/UNLK/USP/return/system and MOVEC words are not + // part of this matrix; their availability is tested separately. + var opcodes = Enumerable.Range(0x4e00, 64).Select(x => (ushort)x) + .Concat(new ushort[] { 0x4e78, 0x4e79, 0x4e7c, 0x4e7d, 0x4e7e, 0x4e7f }).ToArray(); + Assert.Equal(70, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Distinct().Count()); + Assert.Contains((ushort)0x4e00, opcodes); + Assert.Contains((ushort)0x4e3f, opcodes); + Assert.Contains((ushort)0x4e78, opcodes); + Assert.Contains((ushort)0x4e7f, opcodes); + Assert.DoesNotContain(opcodes, x => x is 0x4e40 or 0x4e50 or 0x4e58 or 0x4e60 or 0x4e70 or 0x4e74 or 0x4e7a or 0x4e7b); + foreach (var opcode in opcodes) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, $"UNASSIGNED/4E/word={opcode:X4}", supervisor, ccr); + report.Complete(output); + } +} diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index d8a20783..cca73dc3 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -838,11 +838,42 @@ private static M68020OpcodeKind ClassifyBaseOpcode(ushort opcode) if ((opcode & 0xFFC0) == 0x4800 && logicalMode == 7 && logicalRegister > 1 || (opcode & 0xFFC0) == 0x4A00 && logicalMode == 1) return M68020OpcodeKind.IllegalInstruction; + if ((opcode & 0xFF00) == 0x4A00 && ((opcode >> 6) & 3) < 3 && + logicalMode == 7 && logicalRegister > 4) + return M68020OpcodeKind.IllegalInstruction; // CHK admits data sources, including immediate, but no An or // unassigned mode-7 EA. Reject these words before execution. if ((opcode & 0xF140) == 0x4100 && (logicalMode == 1 || logicalMode == 7 && logicalRegister > 4)) return M68020OpcodeKind.IllegalInstruction; + // M68000PM: CHK requires bit 6 = 0; LEA requires bits 8..6 + // = 111. The intervening 101 first-word pattern is unassigned. + if ((opcode & 0xF1C0) == 0x4140) + return M68020OpcodeKind.IllegalInstruction; + // Unassigned first words between the line-4 system groups. + // TRAP/LINK/UNLK/USP/returns and MOVEC retain their decoders. + if ((opcode & 0xFFC0) == 0x4E00 || + opcode is 0x4E78 or 0x4E79 or >= 0x4E7C and <= 0x4E7F) + return M68020OpcodeKind.IllegalInstruction; + // LEA/PEA/JMP/JSR require control EAs. Preserve PEA's SWAP + // and BKPT aliases, and LEA's 49C0..49C7 EXTB.L encodings. + if (logicalMode is 0 or 1 or 3 or 4 || logicalMode == 7 && logicalRegister > 3) + { + if ((opcode & 0xF1C0) == 0x41C0 && (opcode & 0xFFF8) != 0x49C0 || + (opcode & 0xFFC0) is 0x4E80 or 0x4EC0 || + (opcode & 0xFFC0) == 0x4840 && logicalMode is not (0 or 1)) + return M68020OpcodeKind.IllegalInstruction; + } + // MOVEM load allows control/postincrement, store allows + // control-alterable/predecrement. Store mode 0 is EXT.W/.L. + if ((opcode & 0xFB80) == 0x4880) + { + var load = (opcode & 0x0400) != 0; + if (load + ? logicalMode is 0 or 1 or 4 || logicalMode == 7 && logicalRegister > 3 + : logicalMode is 1 or 3 || logicalMode == 7 && logicalRegister > 1) + return M68020OpcodeKind.IllegalInstruction; + } if ((opcode & 0xFF00) == 0x0800 || (opcode & 0xF100) == 0x0100) { diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 49a6f0d8..ad178db6 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -1780,3 +1780,117 @@ and `artifacts/m6-chk-unassigned-trace/`. Ordinary CI uses the expanded existing CHK matrix and updated strict cardinality. The full-suite skips remain unavailable coverage; the independently run Basic audit remains failing. No seeded audit, test consolidation, package publication or hardware qualification is added. + +## Line-4 illegal and unassigned words (2026-10-05) + +The unchanged Basic ILLEGAL audit next exposed `4140`, invalid LEA `41C0`, +invalid MOVEM `4888`, unassigned TST `4A3D`, and unassigned system word `4E00`. +On EC020/A1200/020/030/060 these reached an emulator unsupported-timing exception +instead of architectural vector 4. The 000/010/040 paths already handled them. +The advanced classifier now rejects these narrowly identified encodings before +operand effects, using the existing illegal-exception path and timing policy. +There is no generic fallback, partial-instruction retry or public API change. + +[M68000PM](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) defines CHK +at 4-69/70 (bit 6 zero), LEA at 4-110 (bits 8..6 = 111), control EAs for +JMP/JSR/LEA/PEA at 4-108/109/110/159, MOVEM direction-specific EAs at 4-128..130, +and TST sizes/EAs at 4-192/193. Its instruction-format inventory leaves the +selected `4140` and `4E` words unassigned on these processors. +[MC68020UM 6.1.5](https://www.nxp.com/docs/en/data-sheet/MC68020UM.pdf) specifies +vector 4 and the causing instruction PC for unassigned first words. Tests label +unassigned first words and EA fields explicitly; these are selected-model rules, +not claims about future architectures. Assigned invalid operand forms remain +distinct from legal instruction coverage. + +The new and expanded mandatory matrices use independent vector-4 expectations, +both user/supervisor stacks and all 32 initial CCR images. They check saved PC/SR, +frame and bank selection, every register, surrounding memory and forbidden +operand reads. Fixed opcode examples and distinct-word counts audit encoding. + +| Batch | Opcode words | Cases per profile | Cases across eight profiles | +| --- | ---: | ---: | ---: | +| `system-unassigned-4140` | 512 | 32,768 | 262,144 | +| `control-invalid-addresses` | 372 | 23,808 | 190,464 | +| `transfer-movem-invalid-operands` | 100 | 6,400 | 51,200 | +| Added unassigned TST words in `logical-unary-invalid-operands` | 9 | 576 additional | 4,608 additional | +| `system-unassigned-4e` | 70 | 4,480 | 35,840 | + +The net addition is **544,256 cases and 32 reporting batches**. Legal aliases +remain covered by the existing transfer/control/system matrices: `49C0..49C7` +EXTB.L, PEA's SWAP/BKPT, MOVEM's EXT.W/L, and neighboring TRAP/LINK/UNLK/USP, +return and MOVEC words. TST size 3 remains TAS, including 060 HALT/PULSE rules. + +The initial control-EA fixture incorrectly included EXTB.L aliases as illegal +LEA. That was a test expectation defect: each advanced profile reported 512 +mismatches from correct EXTB.L execution. The corrected failed-before fixture +excludes those eight words, before the production control-EA guard is added. +It reports 23,808 unsupported cases on each affected profile. This is recorded +separately from the CPU defects, rather than used to alter correct EXTB behavior. + +Failed-before directories retain the evidence for each correction: + +- `artifacts/m6-unassigned-4140-before/`: 32,768 unsupported per affected profile. +- `artifacts/m6-invalid-control-qualified-before/`: 23,808 unsupported per affected + profile; earlier erroneous fixture is in `m6-invalid-control-before/`. +- `artifacts/m6-invalid-movem-before/`: 3,328 passing and 3,072 unsupported per + affected profile. +- `artifacts/m6-unassigned-tst-before/`: 576 additional unsupported per affected + profile, with previous unary cases passing. +- `artifacts/m6-unassigned-4e-before/`: 4,480 unsupported per affected profile. + +All three unaffected profiles pass these failed-before matrices. The corrected +final focused run passes **128 tests with zero skips/failures** in +`artifacts/m6-invalid-address-focused/`, including legal neighbors. No existing +regression is retired. + +The unchanged Basic audit now reaches the next illegal integer word `5008`, +after 9,574 callbacks per affected advanced profile. The 040 `F300` exception +priority mismatch remains. Basic still fails **1,322 passing, 46 mismatching, +13 unsupported and zero untested groups**, over **11,350,097 callbacks and +1,539,795 frames**. All 32 controls pass; 199,327 masked-SR cases and the +manifest/native-library identities remain unchanged. No family exclusion or +comparison-mask change is added. Evidence: `artifacts/m6-invalid-address-broad/`. +The intermediate broad runs retain each earlier failing word and progression. + +Private **unpublished** package `1.5.2-synthetic-dev.50` has SHA-256 +`3ed5f13504404f751f8d7aa563965a49fd137fe4da940e077aba294ae597994e`; +the packaged CPU DLL is +`8743b5ba04f7bcb416b63cda9a63329017613235a2927755c95234dff852438d`. +`artifacts/m6-invalid-address-package.json` records source and assembly identities +against `0041aec`. The default-version tested CPU DLL is +`da4bbd49e4a4083ac8a6da2ef19eea3941bc2132d9e4137e5c13ceeb3c866aea`; +the version differs from the private package with identical production source. +Milestone 6 remains in progress; its earlier reference, advanced restoration +and consolidation requirements remain open. No new seeded audit, physical +timing/host-throughput qualification or package publication is claimed. + +Final Release CPU validation passes **4,809 tests with ten optional skips** and +zero failures, with both qualified exception presets enabled. BKPT retains +224 callbacks / 224 frames / 21 controls; trap/bounds retains 951,522 callbacks / +476,339 frames / 63 controls. The separate qualified TRAP trace audit passes +512 callbacks / 512 frames / six controls. AHX passes 18 tests. Evidence: +`artifacts/m6-invalid-address-full/`, `artifacts/m6-invalid-address-trace/` and +`artifacts/m6-invalid-address-ahx-results/`. Optional skips are unavailable +coverage; the unchanged broad Basic audit remains failing. + +Isolated CopperScreen baseline `d9beae8` builds in Release with zero warnings or +errors against the exact .50 dependency. Host tests pass **149** with six optional +skips, disk **74**, separate engine diagnostics **1,080**, and three native +Workbench/A1200 boot and disk-persistence replays pass with no skips. The package +ZIP's CPU DLL, all four consumer DLLs and four resolved dependency assets match +the recorded package/version. Consumer evidence is retained in +`artifacts/invalid-address-validation/`, `artifacts/invalid-address-diagnostic-tests/`, +`artifacts/invalid-address-production.binlog` and +`artifacts/invalid-address-identities.json`. Native media and build artifacts +are not committed; the root CopperScreen user's changes are preserved. + +The strict deterministic gate verifies **11,737,240 logical cases in 501 batches** +and retains `roadmapComplete=false`. Fresh pinned SingleStepTests passes 312,500 +selected 68000 cases in 125 files; Musashi passes 536 programs with 88 exclusions. +The positive copied-report gate passes, while omission of each of the four new +020 reports and substitution of the old 9,856-case unary report are rejected. +Controls use separate directories and preserve the original evidence. See +`artifacts/m6-invalid-address-gate.log` and +`artifacts/m6-invalid-address-gate-controls-v2/controls.json`. Ordinary CI requires +the expanded matrices and exact per-profile cardinalities. The complete milestone +6 goal remains open despite these passing scoped gates. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 6cc42962..c15948f5 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -978,3 +978,27 @@ exception presets execute. The deterministic gate verifies 11,192,984 cases in `roadmapComplete=false`. Fresh pinned SingleStepTests/Musashi and the separately qualified TRAP trace audit pass. The broad audit still fails 46 mismatching and 13 unsupported groups, so the full milestone 6 objective remains open. + +The line-4 illegal-encoding follow-up fixes narrowly defined advanced decoder +gaps for unassigned `4140`/`4E` words, invalid control EAs and MOVEM EAs, and +unassigned TST EAs. Independent matrices add 544,256 cases across eight profiles, +using both stacks and every CCR. Failed-before runs distinguish emulator +unsupported execution from architectural vector 4. An initial fixture wrongly +included EXTB.L aliases as illegal LEA; its expectation is corrected before the +CPU guard, with legal EXTB/SWAP/BKPT/EXT and system neighbors retained. + +The unchanged broad audit advances to illegal integer word `5008` but retains +46 mismatching and 13 unsupported groups. No family exclusion, comparison-mask +change or regression retirement is made. The private .50 package is unpublished. +Exact source/package identities, failed-before proofs and qualification limits +are in `COPPER68K_REFERENCE_QUALIFICATION.md`. All earlier reference, advanced +exception restoration and consolidation requirements remain open; milestone 6 +stays in progress. + +Final follow-up validation passes 4,809 CPU tests with ten optional skips and both +qualified exception presets enabled. The strict gate verifies 11,737,240 logical +cases in 501 batches, rejects missing/new-group and stale-unary reports, and +retains `roadmapComplete=false`. Fresh pinned SingleStepTests/Musashi, qualified +TRAP trace and AHX pass. The private .50 package passes the isolated CopperScreen +Release build, host/disk/engine tests and all three native Workbench/A1200 replays, +with exact package/version and loaded-DLL identity checks. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index dbb7a166..aee179ae 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -66,6 +66,7 @@ try { 'move-address-boundary'=$(if ($model -in @('68000','68010')) {44} else {60}); 'transfer-registers'=17624; 'transfer-addresses'=$(if ($model -in @('68000','68010')) {8064} else {9252}); 'transfer-movep'=3708; 'transfer-movem'=$(if ($model -in @('68000','68010')) {3196} else {3592}); + 'transfer-movem-invalid-operands'=6400; 'arithmetic-boundaries'=56448; 'arithmetic-addressing'=$(if ($model -in @('68000','68010')) {4667} else {8237}); 'arithmetic-scenarios'=21072; 'arithmetic-extend'=25440; 'arithmetic-invalid-operands'=$(if ($model -in @('68000','68010')) {6336} else {5952}); @@ -79,13 +80,14 @@ try { 'control-conditions'=$(if ($model -in @('68000','68010')) {42048} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {43104}); 'control-jumps'=$(if ($model -in @('68000','68010')) {3592} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {3856}); 'control-trapcc'=3072; + 'control-invalid-addresses'=23808; 'logical-addressing'=$(if ($model -in @('68000','68010')) {6660} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {9944}); 'logical-bitfield-addressing'=$(if ($model -in @('68000','68010')) {2688} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {3480}); 'logical-bitfield-values'=155136; 'logical-bits'=$(if ($model -in @('68000','68010')) {104192} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {104852}); 'logical-boundaries'=31808; 'logical-invalid-operands'=13248; - 'logical-unary-invalid-operands'=$(if ($model -in @('68000','68010')) {11968} elseif ($model -eq '68060') {9728} else {9856}); + 'logical-unary-invalid-operands'=$(if ($model -in @('68000','68010')) {12544} elseif ($model -eq '68060') {10304} else {10432}); 'logical-bitfield-invalid-operands'=13312; 'logical-cas-invalid-operands'=3456; 'logical-cas'=$(if ($model -in @('68000','68010')) {36975} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {37371}); @@ -94,6 +96,8 @@ try { 'system-basic'=1728; 'system-trap-trace'=$(if ($model -eq '68040') {10368} elseif ($model -eq '68060') {8384} elseif ($model -in @('68000','68010')) {3968} else {5952}); 'system-chk-invalid-operands'=11264; + 'system-unassigned-4140'=32768; + 'system-unassigned-4e'=4480; 'system-debug-instructions'=$(if ($model -eq '68060') {512} else {256}); 'system-bounds'=$(if ($model -in @('68000','68010')) {11828} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {12488}); 'system-callm'=$(if ($model -in @('68000','68010')) {10832} elseif ($model -in @('68020','68EC020','A1200')) {11000} elseif ($model -in @('68030','68040','68060')) {10964}); From 281b91b71566164eea126d7318f26173942836cc Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 05:29:51 +0300 Subject: [PATCH 025/211] Qualify illegal quick binary and memory shift encodings --- .../SyntheticInvalidBinaryOperandTests.cs | 100 ++++++++++++ .../SyntheticInvalidMemoryShiftTests.cs | 46 ++++++ .../SyntheticInvalidQuickConditionTests.cs | 72 +++++++++ .../SyntheticUnassignedMoveQuickTests.cs | 33 ++++ Copper68k/M68kAdvancedTimingInterpreter.cs | 34 ++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 148 +++++++++++++++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 29 ++++ scripts/test-copper68k-synthetic.ps1 | 6 + 8 files changed, 464 insertions(+), 4 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticInvalidBinaryOperandTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticInvalidMemoryShiftTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticInvalidQuickConditionTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticUnassignedMoveQuickTests.cs diff --git a/Copper68k.Tests/Synthetic/SyntheticInvalidBinaryOperandTests.cs b/Copper68k.Tests/Synthetic/SyntheticInvalidBinaryOperandTests.cs new file mode 100644 index 00000000..b98d5f4e --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticInvalidBinaryOperandTests.cs @@ -0,0 +1,100 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticInvalidBinaryOperandTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void InvalidBinaryOperandsTrapBeforeArithmeticLogicalOrTransferEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "integer-binary-invalid-operands"); + // M68000PM ADD/SUB/CMP admit An sources for word/long; AND/OR never + // admit An. Their memory destinations and EOR's destinations exclude + // PC/#data. Every data-source EA table ends at mode-7 field 4. + // Opmodes 3/7 encode address arithmetic or word multiply/divide. + // Mode 0/1 destination aliases (extend/decimal/packing/CMPM/EXG) + // are excluded, and existing word-muldiv An tests remain separate. + var opcodes = InvalidWords().ToArray(); + Assert.Equal(1896, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Select(x => x.Opcode).Distinct().Count()); + Assert.Contains(opcodes, x => x.Opcode == 0x8008); // OR.B A0,D0 + Assert.Contains(opcodes, x => x.Opcode == 0xc08f); // AND.L A7,D0 + Assert.Contains(opcodes, x => x.Opcode == 0xd008); // ADD.B A0,D0 + Assert.Contains(opcodes, x => x.Opcode == 0x90ff); // SUBA.W unassigned EA + Assert.Contains(opcodes, x => x.Opcode == 0xbfbc); // EOR.L D7,#data + Assert.Contains(opcodes, x => x.Opcode == 0x80ff); // DIVU.W unassigned EA + Assert.DoesNotContain(opcodes, x => x.Opcode is 0x9048 or 0xb08f or 0xd0cc or + 0x803c or 0xc0fb or 0x80c8 or 0xc1c8 or 0x8100 or 0x8148 or + 0x8180 or 0xc140 or 0xc148 or 0xc188 or 0xb108 or 0xd100 or 0x9108); + foreach (var (opcode, family, form) in opcodes) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, $"{family}/invalid/{form}", supervisor, ccr); + report.Complete(output); + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void UnassignedLongAndRegisterDestinationWordsTrapWithoutExchangeEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "logical-unassigned-c180"); + // M68000PM 4-17: AND's destination-EA form requires memory. + // 4-105: EXG uses opmode fields 01000, 01001, 10001, excluding + // the C180..C187 register-EA words. These are unassigned, not EXG. + var opcodes = (from registerField in Enumerable.Range(0, 8) + from eaRegister in Enumerable.Range(0, 8) + select (ushort)(0xc180 | registerField << 9 | eaRegister)).ToArray(); + Assert.Equal(64, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Distinct().Count()); + Assert.Contains((ushort)0xc180, opcodes); + Assert.Contains((ushort)0xcf87, opcodes); + Assert.DoesNotContain(opcodes, x => x is 0xc140 or 0xc148 or 0xc188 or 0xc190); + foreach (var opcode in opcodes) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, + $"UNASSIGNED/C180/register-fields={(opcode >> 9) & 7},{opcode & 7}", supervisor, ccr); + report.Complete(output); + } + + private static IEnumerable<(ushort Opcode, string Family, string Form)> InvalidWords() + { + // Declarative instruction fields from the manual, independent of the + // production classifier. A non-data source permits word/long An. + foreach (var (line, family, dataOnly, operation3, operation7) in new[] + { + (0x8000, "OR", true, "DIVU.W", "DIVS.W"), + (0x9000, "SUB", false, "SUBA.W", "SUBA.L"), + (0xb000, "CMP", false, "CMPA.W", "CMPA.L"), + (0xc000, "AND", true, "MULU.W", "MULS.W"), + (0xd000, "ADD", false, "ADDA.W", "ADDA.L") + }) + for (var registerField = 0; registerField < 8; registerField++) + { + for (var size = 0; size < 3; size++) + { + var sized = $"{family}.{new[] { "B", "W", "L" }[size]}"; + if (dataOnly || size == 0) + for (var register = 0; register < 8; register++) + yield return Case(size, new(1, register), $"{sized}/EA->D{registerField}"); + for (var register = 5; register < 8; register++) + yield return Case(size, new(7, register), $"{sized}/EA->D{registerField}"); + var destinationFamily = family == "CMP" ? "EOR" : family; + for (var register = 2; register < 8; register++) + yield return Case(size + 4, new(7, register), + $"{destinationFamily}.{new[] { "B", "W", "L" }[size]}/D{registerField}->EA"); + } + foreach (var (opmode, operation) in new[] { (3, operation3), (7, operation7) }) + for (var register = 5; register < 8; register++) + yield return Case(opmode, new(7, register), $"{operation}/register-field={registerField}"); + + (ushort, string, string) Case(int opmode, OperandForm form, string operation) => + ((ushort)(line | registerField << 9 | opmode << 6 | form.Mode << 3 | form.Register), + operation, form.Mode == 7 && form.Register > 4 + ? $"unassigned-EA(7,{form.Register})" : form.Id); + } + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticInvalidMemoryShiftTests.cs b/Copper68k.Tests/Synthetic/SyntheticInvalidMemoryShiftTests.cs new file mode 100644 index 00000000..a8b188f3 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticInvalidMemoryShiftTests.cs @@ -0,0 +1,46 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticInvalidMemoryShiftTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void InvalidMemoryShiftOperandsTrapWithoutReadingOrUpdatingOperands(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "logical-memory-shift-invalid-operands"); + // M68000PM 4-24, 4-115, 4-162, 4-166: word-sized, count-one + // memory shifts/rotates admit memory-alterable EAs only. Register + // shifts and 020+ bitfields have separate first-word encodings. + var opcodes = InvalidWords().ToArray(); + Assert.Equal(176, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Select(x => x.Opcode).Distinct().Count()); + Assert.Contains(opcodes, x => x.Opcode == 0xe0c0); + Assert.Contains(opcodes, x => x.Opcode == 0xe3cf); + Assert.Contains(opcodes, x => x.Opcode == 0xe7ff); + Assert.DoesNotContain(opcodes, x => x.Opcode is 0xe040 or 0xe0d0 or 0xe7f9 or 0xe8c0 or 0xefc7); + foreach (var (opcode, family, form) in opcodes) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, $"{family}.W/memory/invalid/{form}", supervisor, ccr); + report.Complete(output); + } + + private static IEnumerable<(ushort Opcode, string Family, string Form)> InvalidWords() + { + foreach (var (encoding, family) in new[] { (0xe0c0, "ASR"), (0xe1c0, "ASL"), + (0xe2c0, "LSR"), (0xe3c0, "LSL"), (0xe4c0, "ROXR"), (0xe5c0, "ROXL"), + (0xe6c0, "ROR"), (0xe7c0, "ROL") }) + { + foreach (var mode in new[] { 0, 1 }) + for (var register = 0; register < 8; register++) yield return Case(new(mode, register)); + for (var register = 2; register < 8; register++) yield return Case(new(7, register)); + + (ushort, string, string) Case(OperandForm form) => + ((ushort)(encoding | form.Mode << 3 | form.Register), family, + form.Mode == 7 && form.Register > 4 ? $"unassigned-EA(7,{form.Register})" : form.Id); + } + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticInvalidQuickConditionTests.cs b/Copper68k.Tests/Synthetic/SyntheticInvalidQuickConditionTests.cs new file mode 100644 index 00000000..3e5517f1 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticInvalidQuickConditionTests.cs @@ -0,0 +1,72 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticInvalidQuickConditionTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void InvalidQuickDestinationsTrapBeforeRegisterOrOperandEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "arithmetic-quick-invalid-operands"); + // M68000PM 4-11/12, 4-181/182: alterable destinations only; An + // permits word/long, never byte. Mode-7 fields 5..7 are unassigned. + // Size 3 selects Scc/DBcc/TRAPcc, not a quick arithmetic size. + var opcodes = InvalidQuickWords().ToArray(); + Assert.Equal(416, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Select(x => x.Opcode).Distinct().Count()); + Assert.Contains(opcodes, x => x.Opcode == 0x5008); // ADDQ.B #8,A0 + Assert.Contains(opcodes, x => x.Opcode == 0x5f0f); // SUBQ.B #7,A7 + Assert.Contains(opcodes, x => x.Opcode == 0x50ba); // ADDQ.L #8,PC-relative + Assert.Contains(opcodes, x => x.Opcode == 0x5fbf); // SUBQ.L, unassigned EA + Assert.DoesNotContain(opcodes, x => x.Opcode is 0x5000 or 0x5048 or 0x508f or 0x51b9 or 0x50c8 or 0x50fa); + foreach (var (opcode, family, form) in opcodes) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, $"{family}/invalid/{form}", supervisor, ccr); + report.Complete(output); + } + + private static IEnumerable<(ushort Opcode, string Family, string Form)> InvalidQuickWords() + { + foreach (var subtract in new[] { false, true }) + for (var dataField = 0; dataField < 8; dataField++) + for (var size = 0; size < 3; size++) + { + if (size == 0) + for (var register = 0; register < 8; register++) yield return Case(new(1, register)); + for (var register = 2; register < 8; register++) yield return Case(new(7, register)); + + (ushort, string, string) Case(OperandForm form) => + ((ushort)(0x5000 | dataField << 9 | (subtract ? 0x0100 : 0) | size << 6 | form.Mode << 3 | form.Register), + $"{(subtract ? "SUBQ" : "ADDQ")}.{new[] { "B", "W", "L" }[size]}/count={(dataField == 0 ? 8 : dataField)}", + form.Mode == 7 && form.Register > 4 ? $"unassigned-EA(7,{form.Register})" : form.Id); + } + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void UnassignedConditionWordsTrapWithoutTestingOrUpdatingOperands(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "control-scc-unassigned-operands"); + // M68000PM 4-173: Scc admits mode-7 fields 0/1 only. Mode 1 is + // DBcc, and fields 2/3/4 are separate TRAPcc words on 020+. + // Only the remaining unassigned fields 5..7 enter this matrix. + var opcodes = (from condition in Enumerable.Range(0, 16) + from register in Enumerable.Range(5, 3) + select (ushort)(0x50f8 | condition << 8 | register)).ToArray(); + Assert.Equal(48, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Distinct().Count()); + Assert.Contains((ushort)0x50fd, opcodes); + Assert.Contains((ushort)0x5fff, opcodes); + Assert.DoesNotContain(opcodes, x => x is 0x50c0 or 0x50c8 or 0x50f8 or 0x50f9 or 0x50fa or 0x50fb or 0x50fc); + foreach (var opcode in opcodes) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, + $"UNASSIGNED/Scc/condition={(opcode >> 8) & 15}/EA(7,{opcode & 7})", supervisor, ccr); + report.Complete(output); + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticUnassignedMoveQuickTests.cs b/Copper68k.Tests/Synthetic/SyntheticUnassignedMoveQuickTests.cs new file mode 100644 index 00000000..e83d0816 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticUnassignedMoveQuickTests.cs @@ -0,0 +1,33 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticUnassignedMoveQuickTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void UnassignedMoveQuickWordsTrapBeforeRegisterOrFlagEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "transfer-moveq-unassigned-words"); + // M68000PM 4-134: MOVEQ's bit 8 is zero. The 0111 rrr 1 iiiiiiii + // first words are unassigned on the selected models, not MOVEQ aliases. + // Legal MOVEQ sign extension and CCR behavior remain in transfer-registers. + var opcodes = (from register in Enumerable.Range(0, 8) + from immediate in Enumerable.Range(0, 256) + select (ushort)(0x7100 | register << 9 | immediate)).ToArray(); + Assert.Equal(2048, opcodes.Length); + Assert.Equal(opcodes.Length, opcodes.Distinct().Count()); + Assert.Contains((ushort)0x7100, opcodes); + Assert.Contains((ushort)0x7180, opcodes); + Assert.Contains((ushort)0x7fff, opcodes); + Assert.DoesNotContain(opcodes, x => x is 0x7000 or 0x7080 or 0x7eff); + foreach (var opcode in opcodes) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, + $"UNASSIGNED/MOVEQ/register-field={(opcode >> 9) & 7}/immediate-field={opcode & 255:X2}", supervisor, ccr); + report.Complete(output); + } +} diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index cca73dc3..48c5fd53 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -815,6 +815,36 @@ private static M68020OpcodeKind ClassifyBaseOpcode(ushort opcode) // CCR/SR forms were handled above; dynamic mode-1 words are MOVEP. var logicalMode = (opcode >> 3) & 7; var logicalRegister = opcode & 7; + // Count-one word shifts/rotates require memory-alterable EAs. + // Bit 11 distinguishes the neighboring 020+ bitfield encodings. + if ((opcode & 0xF8C0) == 0xE0C0 && + (logicalMode < 2 || logicalMode == 7 && logicalRegister > 1)) + return M68020OpcodeKind.IllegalInstruction; + // Binary data sources end at mode-7 field 4; destinations end + // at field 1. ADD/SUB/CMP admit An for word/long, OR/AND do not. + // Destination mode 0/1 aliases and opmode 3/7 remain separate. + var integerLine = opcode >> 12; + // AND.L destination-Dn words have no assigned alias; EXG's + // long data/address exchange uses mode 1 (C188), not mode 0. + if ((opcode & 0xF1F8) == 0xC180) + return M68020OpcodeKind.IllegalInstruction; + if (integerLine is 8 or 9 or 11 or 12 or 13) + { + var opmode = (opcode >> 6) & 7; + if (logicalMode == 7 && logicalRegister > (opmode is >= 4 and <= 6 ? 1 : 4) || + logicalMode == 1 && opmode < 3 && (integerLine is 8 or 12 || opmode == 0)) + return M68020OpcodeKind.IllegalInstruction; + } + // ADDQ/SUBQ allow An only for word/long and otherwise require + // alterable EAs. Size 3 selects DBcc/Scc/TRAPcc, not arithmetic. + if ((opcode & 0xF000) == 0x5000) + { + var size = (opcode >> 6) & 3; + if (size < 3 + ? logicalMode == 1 && size == 0 || logicalMode == 7 && logicalRegister > 1 + : logicalMode == 7 && logicalRegister > 4) + return M68020OpcodeKind.IllegalInstruction; + } if ((opcode & 0xFF00) is 0x0000 or 0x0200 or 0x0A00 && ((opcode >> 6) & 3) < 3 && (logicalMode == 1 || logicalMode == 7 && logicalRegister > 1)) @@ -894,6 +924,10 @@ private static M68020OpcodeKind ClassifyBaseOpcode(ushort opcode) : M68020OpcodeKind.IllegalInstruction; } + // MOVEQ's bit 8 must be zero; other line-7 first words are + // unassigned on these models, not alternate MOVEQ encodings. + if ((opcode & 0xF100) == 0x7100) + return M68020OpcodeKind.IllegalInstruction; if ((opcode & 0xF100) == 0x7000) { return M68020OpcodeKind.Moveq; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index ad178db6..4f5f923b 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -262,10 +262,12 @@ bus ordering, detailed fault sequencing, JIT and native ROM/media tests remain. ## Remaining qualification and implementation gaps -- SingleStepTests now has the pinned 312,500-case 68000 instruction-body audit - described above. WinUAE's executable adapter has no new fixture run in this - slice; the LINK source review is separate evidence. WinUAE integer integration - currently selects 68000; multi-model native bridge/fixtures still need qualification. +- SingleStepTests has the pinned 312,500-case 68000 instruction-body audit. + The later checkpoints below add a pinned multi-model WinUAE bridge, the + unchanged failing Basic discovery audit, and separately qualified trace, + trap/bounds and breakpoint presets. Remaining Basic disagreements and + unsupported integer execution still require per-case qualification; passing + focused presets do not replace the failing broad audit. - Generated 010 word-MOVE/MOVEA format-8 images now have the scoped continuation gate below. Long transfers, other instruction families, foreign silicon images, external bus faults, 020/030 formats 9/A/B and 040 format 7 remain unqualified. The current advanced decoder does not implement all these @@ -1894,3 +1896,141 @@ Controls use separate directories and preserve the original evidence. See `artifacts/m6-invalid-address-gate-controls-v2/controls.json`. Ordinary CI requires the expanded matrices and exact per-profile cardinalities. The complete milestone 6 goal remains open despite these passing scoped gates. + +## Quick, binary and memory-shift illegal decoding (2026-10-05) + +The unchanged Basic ILLEGAL run exposed `5008`, `7100`, `8008`, `C180` and +`E0C0` in sequence. These words reached unsupported-timing exceptions on +EC020/A1200/020/030/060. Narrow classifier guards now enter the existing +architectural vector-4 path before operand effects. The correction preserves +legal execution ordering, timing policy and public API; no generic fallback or +retry after partial effects is introduced. + +Authority is [M68000PM](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf): +ADDQ/SUBQ 4-11/12 and 4-181/182 permit only alterable destinations and forbid byte +An; Scc 4-173 and the separate DBcc/TRAPcc formats distinguish the unassigned +condition words. MOVEQ 4-134 fixes bit 8 to zero. ADD/SUB/CMP source rules admit +word/long An, unlike AND/OR; destination tables exclude PC/immediate. The binary +matrix uses their explicit instruction/opmode tables, including address variants +and word multiply/divide source limits. EXG 4-105 admits opmodes 01000, 01001 and +10001, excluding the unassigned `C180` words. Memory shifts/rotates at 4-24, +4-115, 4-162 and 4-166 require memory-alterable operands. Unassigned first words +use the causing opcode PC per +[MC68020UM 6.1.5](https://www.nxp.com/docs/en/data-sheet/MC68020UM.pdf). + +Six required batches cover every selected illegal/unassigned opcode word, both +stacks and every initial CCR. Fixed examples, exact distinct-word counts and +legal exclusions audit the independent encoding fixtures. The common verifier +checks saved PC/SR, frame/bank selection, all registers, surrounding memory and +forbidden operand reads. Unassigned words/EA fields are labeled explicitly; +they are not included as legal instruction combinations. + +| Batch | Distinct words | Cases per profile | Added cases across eight profiles | +| --- | ---: | ---: | ---: | +| `arithmetic-quick-invalid-operands` | 416 | 26,624 | 212,992 | +| `control-scc-unassigned-operands` | 48 | 3,072 | 24,576 | +| `transfer-moveq-unassigned-words` | 2,048 | 131,072 | 1,048,576 | +| `integer-binary-invalid-operands` | 1,896 | 121,344 | 970,752 | +| `logical-unassigned-c180` | 64 | 4,096 | 32,768 | +| `logical-memory-shift-invalid-operands` | 176 | 11,264 | 90,112 | + +Total addition: **2,379,776 cases in 48 batches**. Quick size 3, DBcc/TRAPcc, +legal MOVEQ, ADDX/SUBX, CMPM, SBCD/PACK/UNPK, ABCD/EXG, and register shifts / +bitfields retain their separate legal or architectural-unavailability coverage. +Word-multiply/divide An invalid-source coverage is retained in its prior matrix, +rather than duplicated in the new binary batch. + +Failed-before runs have zero mismatches or untested cases. The 000/010/040 +profiles already pass all new matrices. Each affected advanced profile reports: + +- `artifacts/m6-invalid-quick-before/`: 26,624 unsupported quick cases and 3,072 + unsupported unassigned-condition cases. +- `artifacts/m6-unassigned-moveq-before/`: 131,072 unsupported cases. +- `artifacts/m6-invalid-binary-before/`: 17,408 passing / 103,936 unsupported. +- `artifacts/m6-unassigned-c180-before/`: 4,096 unsupported cases. +- `artifacts/m6-invalid-memory-shift-before/`: 11,264 unsupported cases. + +Restored focused runs retain the corresponding `*-focused/` reports. Quick and +legal arithmetic/control tests pass 80 xUnit tests; MOVEQ and legal transfers +pass 40; binary and legal arithmetic/logical/multiply/divide/decimal/transfer +tests pass 120; C180 and legal register transfers pass 16. These are scoped +checks, separate from the final full CPU suite. Invalid memory operands and +legal shifts/bitfields pass 40 tests with no skips or failures. + +The unchanged broad audit reaches line-F words on every advanced profile: +EC020/A1200/020 `F110` at callback 28,467; 030 `F520` at 28,551; 040 `F300` at +28,959; 060 `F23D` at 28,946. There is no remaining integer unsupported stop in +the ILLEGAL group, but these line-F exception disagreements are still failing +evidence requiring per-case source/manual/CPU qualification. The complete Basic +result is **1,322 passing / 51 mismatching / eight unsupported / zero untested +groups**, over **11,445,125 callbacks and 1,634,819 frames**. Five groups have +moved from unsupported execution to later architectural mismatches; they have +not become passing groups. All 32 controls pass. Masked-SR count 199,327 and +the original Basic manifest/native bridge remain unchanged. No family exclusion, +comparison mask or fixture alteration is added. See +`artifacts/m6-invalid-memory-shift-broad/`; intermediate failing audits are retained. + +Consolidation review retains `MoveqSignExtendsImmediateAndSetsFlags`, +`AddqWordDataRegisterAddsImmediateUpdatesFlagsAndPreservesUpperWord`, +`OrByteDataToDataRegisterUpdatesLowByteAndFlags` and the three EXG register tests +in `M68020InterpreterTests`: they assert native and elapsed timing policy in +addition to semantic state. The shared semantic matrices do not replace that +timing evidence. No regression is retired in this follow-up. The earlier proven +ASL retirement and all specialized bus/prefetch/cache/JIT/native regressions +remain retained as documented. The stale initial gap summary above is corrected +to describe the now-implemented multi-model bridge and its remaining failures. + +Milestone 6 remains in progress, including earlier reference disagreements, +advanced exception restoration and consolidation requirements. New seeded audits, +physical timing/host-throughput qualification and package publication are not +claimed by this checkpoint. + +Private **unpublished** package `1.5.2-synthetic-dev.51` has SHA-256 +`3967602830b825abe065684737788da47ea7109ba5e290c5838a97ad81853a45`; +the packaged CPU assembly is +`277818b4de5bb7638613b7cb26301ef6b14d691b711279cf91487ba20428bb27`. +`artifacts/m6-integer-illegal-package.json` records source and assembly identities +against `31a6221`; the default-version CPU used by the reference adapters is +`e73b8e1f89922399fad793a8f37025864b1bd4af3390dcf171d66de019572374`. +The assembly version differs from the private package; production source is +identical. + +Isolated CopperScreen baseline `d9beae8` builds in Release with zero warnings or +errors against the exact .51 dependency. Host **149** pass with six optional +skips, disk **74**, separate engine diagnostics **1,080**, and all three native +Workbench/A1200 boot and disk-persistence replays pass without skips. All four +loaded CPU DLLs match the package ZIP entry, and all four dependency assets +resolve exactly .51. Evidence: `artifacts/integer-illegal-validation/`, +`artifacts/integer-illegal-diagnostic-tests/`, +`artifacts/integer-illegal-production.binlog` and +`artifacts/integer-illegal-identities.json`. No native media/build artifacts are +committed, and unrelated root CopperScreen changes remain untouched. AHX passes +18 tests in `artifacts/m6-integer-illegal-ahx-results/`; the qualified TRAP trace +audit retains 512 callbacks / 512 frames / six controls in +`artifacts/m6-integer-illegal-trace/`. + +Final Release CPU validation passes **4,857 tests with ten optional skips** and +zero failures in `artifacts/m6-integer-illegal-full/`. Both qualified exception +presets execute: BKPT 224 callbacks / 224 frames / 21 controls, trap/bounds +951,522 callbacks / 476,339 frames / 63 controls. Optional skips remain +unavailable coverage, and the separately run Basic audit remains failing. + +The strict gate verifies **14,117,016 logical cases in 549 batches**, retaining +`roadmapComplete=false`. Fresh pinned SingleStepTests passes 312,500 selected +68000 cases in 125 files; Musashi passes 536 programs with 88 exclusions. +The positive copied-report gate passes, and omitting each of the six new 020 +reports is rejected. Original evidence is preserved in separate control folders. +See `artifacts/m6-integer-illegal-gate.log` and +`artifacts/m6-integer-illegal-gate-controls/controls.json`. Ordinary CI requires +every new per-profile report at its exact cardinality. + +Remaining Basic ILLEGAL diagnostics record expected/actual vectors: `F110` +EC020/A1200/020 and `F520` 030 expect 8 versus actual 11; 040 `F300` expects 11 +versus actual 8; 060 `F23D` expects 11 versus actual no exception. These require +instruction-format and exception-priority qualification, not a blanket line-F +exclusion. The eight remaining unsupported groups are the DIVL/MULL families +on EC020/A1200/020/030, first stopping at extension words `0031`, `0084` or +`5B2B`. Their reserved extension fields still need per-case qualification; +entire multiply/divide families are not excluded. Earlier restoration and +reference requirements remain open, so this checkpoint does not complete the +milestone 6 goal. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index c15948f5..ef761314 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1002,3 +1002,32 @@ retains `roadmapComplete=false`. Fresh pinned SingleStepTests/Musashi, qualified TRAP trace and AHX pass. The private .50 package passes the isolated CopperScreen Release build, host/disk/engine tests and all three native Workbench/A1200 replays, with exact package/version and loaded-DLL identity checks. + +The next illegal-integer follow-up adds six required matrices for ADDQ/SUBQ, +unassigned Scc-neighbor and MOVEQ words, invalid binary operands, unassigned +C180 words and invalid memory shifts. Their 2,379,776 cases across eight profiles +exercise every selected word, both stacks and all CCR states. Failed-before +coverage detects unsupported execution on five advanced profiles; narrow static +classification now enters vector 4 before operand effects. Legal data/address, +extend/decimal/packing, EXG/CMPM, DBcc/TRAPcc, register-shift and bitfield aliases +are preserved. No generic fallback, instruction retry or timing-policy change +is introduced. + +The unchanged broad ILLEGAL group reaches line-F exception disagreements on all +advanced profiles. Overall Basic remains failing: 1,322 passing, 51 mismatching +and eight unsupported groups. Five groups now reach later architectural +mismatches instead of stopping at unsupported integer execution. The original +inputs and masks remain intact. Older MOVEQ/ADDQ/OR/EXG tests retain distinct +timing-policy assertions and are not retired. Exact authorities, failed-before +evidence and remaining gaps are in `COPPER68K_REFERENCE_QUALIFICATION.md`. +Milestone 6 remains in progress with its full earlier requirements retained. + +Final follow-up validation passes 4,857 CPU tests with ten optional skips and +both qualified exception presets enabled. The strict gate verifies 14,117,016 +logical cases in 549 batches and rejects each missing new 020 report. Fresh +pinned SingleStepTests/Musashi, qualified TRAP trace and AHX pass. The private +unpublished .51 package passes the isolated CopperScreen Release build, +host/disk/engine tests and all three native Workbench/A1200 replays, with exact +package/version and loaded-DLL identity checks. `roadmapComplete=false` is +retained; line-F priority, reserved multiply/divide extensions and earlier +advanced restoration/reference/consolidation requirements remain open. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index aee179ae..c949f64d 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -65,11 +65,16 @@ try { 'move-register-overlap'=$(if ($model -in @('68000','68010')) {1801} else {2593}); 'move-address-boundary'=$(if ($model -in @('68000','68010')) {44} else {60}); 'transfer-registers'=17624; 'transfer-addresses'=$(if ($model -in @('68000','68010')) {8064} else {9252}); + 'transfer-moveq-unassigned-words'=131072; 'transfer-movep'=3708; 'transfer-movem'=$(if ($model -in @('68000','68010')) {3196} else {3592}); 'transfer-movem-invalid-operands'=6400; 'arithmetic-boundaries'=56448; 'arithmetic-addressing'=$(if ($model -in @('68000','68010')) {4667} else {8237}); 'arithmetic-scenarios'=21072; 'arithmetic-extend'=25440; 'arithmetic-invalid-operands'=$(if ($model -in @('68000','68010')) {6336} else {5952}); + 'arithmetic-quick-invalid-operands'=26624; + 'integer-binary-invalid-operands'=121344; + 'logical-unassigned-c180'=4096; + 'logical-memory-shift-invalid-operands'=11264; 'arithmetic-muldiv-word-invalid-operands'=16384; 'arithmetic-muldiv-long-invalid-operands'=4096; 'arithmetic-decimal'=$(if ($model -in @('68000','68010')) {111392} else {111458}); @@ -80,6 +85,7 @@ try { 'control-conditions'=$(if ($model -in @('68000','68010')) {42048} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {43104}); 'control-jumps'=$(if ($model -in @('68000','68010')) {3592} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {3856}); 'control-trapcc'=3072; + 'control-scc-unassigned-operands'=3072; 'control-invalid-addresses'=23808; 'logical-addressing'=$(if ($model -in @('68000','68010')) {6660} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {9944}); 'logical-bitfield-addressing'=$(if ($model -in @('68000','68010')) {2688} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {3480}); From 903f45cb396dcef91ebd44423af748fb308cef34 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 05:59:47 +0300 Subject: [PATCH 026/211] Qualify line-F state transfers and indexed restoration --- .../Synthetic/InvalidOperandScenario.cs | 4 +- .../Synthetic/SyntheticLineFEncodingTests.cs | 135 ++++++++++++++++++ .../Synthetic/SyntheticSystemTests.cs | 4 +- Copper68k/M68040Support.cs | 37 +++-- Copper68k/M68060Interpreter.cs | 12 ++ Copper68k/M68kAdvancedTimingInterpreter.cs | 21 +++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 132 +++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 29 ++++ scripts/test-copper68k-synthetic.ps1 | 7 +- 9 files changed, 363 insertions(+), 18 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticLineFEncodingTests.cs diff --git a/Copper68k.Tests/Synthetic/InvalidOperandScenario.cs b/Copper68k.Tests/Synthetic/InvalidOperandScenario.cs index 90f68435..29d9d006 100644 --- a/Copper68k.Tests/Synthetic/InvalidOperandScenario.cs +++ b/Copper68k.Tests/Synthetic/InvalidOperandScenario.cs @@ -3,7 +3,7 @@ namespace Copper68k.Tests.Synthetic; internal static class InvalidOperandScenario { public static void Run(SyntheticMachine machine, CoverageBatch report, ushort opcode, string description, - bool supervisor, int ccr, ushort extensionWord = 0x0011) + bool supervisor, int ccr, ushort extensionWord = 0x0011, int vector = 4) { machine.Reset(ccr, supervisor); // Fixture initialization is separate from CPU accesses. Canaries detect @@ -18,7 +18,7 @@ public static void Run(SyntheticMachine machine, CoverageBatch report, ushort op } var expected = SyntheticExecution.Prepare(machine, [opcode, extensionWord, 0x81a5, 0x4e71]); expected.ForbiddenOperandReads.UnionWith(forbidden); - SyntheticExecution.ExpectException(machine, expected, 4); + SyntheticExecution.ExpectException(machine, expected, vector); SyntheticExecution.Run(machine, expected, report, $"{machine.Model.Id}/{description}/op={opcode:X4}/super={supervisor}/ccr={ccr:X2}"); } diff --git a/Copper68k.Tests/Synthetic/SyntheticLineFEncodingTests.cs b/Copper68k.Tests/Synthetic/SyntheticLineFEncodingTests.cs new file mode 100644 index 00000000..5682ce65 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticLineFEncodingTests.cs @@ -0,0 +1,135 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticLineFEncodingTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void SaveRestoreFirstWordsSelectLineFOrPrivilegeBeforeOperandEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "system-linef-state-encodings"); + // MC68020UM 7.2.3.3/4, 7.5.2.2/3; MC68030UM 10.2.3.3/4, + // 10.5.2.2/3 (CpID 0 belongs to the internal MMU, not external CIRs). + // M68000PM 6-13/16; MC68060UM D-15/18 and 8.3: + // invalid first words are line-F, not privileged state transfers. + var opcodes = (from cpId in Enumerable.Range(0, 8) + from restore in new[] { false, true } + from ea in Enumerable.Range(0, 64) + select (Opcode: (ushort)(0xf100 | cpId << 9 | (restore ? 0x40 : 0) | ea), + CpId: cpId, Restore: restore, Mode: ea >> 3, Register: ea & 7)).ToArray(); + Assert.Equal(1024, opcodes.Select(x => x.Opcode).Distinct().Count()); + Assert.Contains(opcodes, x => x.Opcode == 0xf110 && x.CpId == 0 && !x.Restore && x.Mode == 2); + Assert.Contains(opcodes, x => x.Opcode == 0xf520 && x.CpId == 2 && !x.Restore && x.Mode == 4); + Assert.Contains(opcodes, x => x.Opcode == 0xf300 && x.CpId == 1 && x.Mode == 0); + Assert.Contains(opcodes, x => x.Opcode == 0xf37b && x.CpId == 1 && x.Restore && x.Mode == 7 && x.Register == 3); + foreach (var item in opcodes) + foreach (var supervisor in new[] { false, true }) + { + // These bit patterns name integer MMU instructions on 040/060, + // not external cpSAVE/cpRESTORE. Their separate MMU matrices own + // PFLUSH (F500..F51F), and 040 PTEST (F548..F54F/F568..F56F). + if (modelId is "68040" or "68060" && item.Opcode is >= 0xf500 and <= 0xf51f) continue; + if (modelId == "68040" && (item.Opcode is >= 0xf548 and <= 0xf54f or >= 0xf568 and <= 0xf56f)) continue; + var recognized = machine.Model.FullIndex && + (modelId == "68030" ? item.CpId != 0 : + modelId is "68040" or "68060" ? item.CpId == 1 : true); + var legalEa = item.Mode is 2 or 5 or 6 || + item.Mode == (item.Restore ? 3 : 4) || + (item.Mode == 7 && item.Register <= (item.Restore ? 3 : 1)); + // This batch qualifies first-word and privilege exceptions. Legal + // supervisor coprocessor/FPU state execution is a separate protocol, + // not counted as passing exception coverage or silently simulated. + if (supervisor && recognized && legalEa) continue; + var vector = recognized && legalEa ? 8 : 11; + var form = item.Mode == 7 && item.Register > 4 ? $"unassigned-EA(7,{item.Register})" : $"EA({item.Mode},{item.Register})"; + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, item.Opcode, + $"LineF/{(item.Restore ? "cpRESTORE" : "cpSAVE")}/CpID={item.CpId}/{form}/vector={vector}", + supervisor, ccr, vector: vector); + } + report.Complete(output); + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void UnassignedFpuOperandFieldsTakeLineFWithoutOperandEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "system-linef-unassigned-fpu-ea"); + // M68000PM 2-1 and floating-point operand tables: mode 7 registers + // 5..7 are unassigned. MC68060UM 8.2.4: an unrecognized F-line + // word uses vector 11, format 0 and the causing instruction PC. + // F27A/B/C are FTRAPcc, and F248..F24F are FDBcc; neither + // aliases the unassigned F27D/E/F conditional operand fields. + ushort[] words = [0xf23d, 0xf23e, 0xf23f, 0xf27d, 0xf27e, 0xf27f]; + Assert.Equal(0b111101, words[0] & 0x3f); + foreach (var opcode in words) + foreach (var command in new ushort[] { 0x4000, 0x8000, 0xc000, 0x4e71 }) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, + $"LineF/FPU/unassigned-EA(7,{opcode & 7})/command={command:X4}", + supervisor, ccr, command, vector: 11); + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void M68040PcRelativeRestoreUsesExtensionBaseAndFullIndexPointerOrder() + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == "68040")); + var report = new CoverageBatch("68040", "system-linef-pc-restore"); + var forms = new[] { (Form: new OperandForm(7, 2), Index: (IndexFixture?)null), + (Form: new OperandForm(7, 3), Index: (IndexFixture?)new()) } + .Concat(IndexFixture.FullStructures().Select(index => (new OperandForm(7, 3), (IndexFixture?)index))).ToArray(); + Assert.Equal(68, forms.Length); + foreach (var (form, index) in forms) + foreach (var header in new ushort[] { 0, 0x4100, 0x1234 }) + for (var ccr = 0; ccr < 32; ccr++) + { + machine.Reset(ccr); + machine.Core.State.D[7] = 0x100; + var words = new List { (ushort)(0xf340 | form.Mode << 3 | form.Register) }; + var d = (uint[])machine.Core.State.D.Clone(); + var a = (uint[])machine.Core.State.A.Clone(); + var fixture = new AddressingFixture(machine, 4, words, d, a, 0, + new AddressOptions(SourceIndex: index)); + var address = fixture.Resolve(form, source: true); + // Null BD + suppressed index can read the instruction's own + // extension as a frame header or an indirect pointer. Preserve + // the encoding, and build expectations from the fixture bytes. + for (var i = 0; i < words.Count; i++) machine.InitializePhysical(SyntheticMachine.Code + (uint)i * 2, words[i], 2); + machine.InitializePhysical(fixture.NextPc, 0x4e71, 2); + machine.InitializePhysical(fixture.NextPc + 2, 0x4e71, 2); + if (fixture.SourcePointerTarget != null) address = fixture.SourcePointerTarget(); + var selfHeader = address == SyntheticMachine.Code + 2; + if (selfHeader && header != 0x1234) continue; // This encoding cannot also contain a NULL/IDLE header. + if (!selfHeader) + { + for (var offset = -4; offset < 8; offset++) + machine.InitializePhysical(unchecked(address + (uint)offset), 0xa5, 1); + machine.InitializePhysical(address, (uint)header << 16, 4); + } + var expected = SyntheticExecution.Prepare(machine, words); + // Independent constants: PRM 6-12/13 NULL clears the programmer's + // model; IDLE preserves it. Invalid headers use format error 14. + machine.Core.State.M68040Fpu.Fpcr = 0x00001000; + machine.Core.State.M68040Fpu.Fpsr = 0x08000000; + machine.Core.State.M68040Fpu.Fpiar = 0x12345678; + expected.ControlChecks["FPCR"] = (s => s.M68040Fpu.Fpcr, header == 0 ? 0u : 0x00001000); + expected.ControlChecks["FPSR"] = (s => s.M68040Fpu.Fpsr, header == 0 ? 0u : 0x08000000); + expected.ControlChecks["FPIAR"] = (s => s.M68040Fpu.Fpiar, header == 0 ? 0u : 0x12345678); + if (header == 0x1234) SyntheticExecution.ExpectException(machine, expected, 14); + else + { + expected.ControlChecks["frame address"] = (s => s.M68040Fpu.LastStateFrameAddress, address); + expected.ControlChecks["frame header"] = (s => s.M68040Fpu.LastStateFrameHeader, header); + expected.ControlChecks["frame size"] = (s => s.M68040Fpu.LastStateFrameSize, 4); + } + SyntheticExecution.Run(machine, expected, report, + $"68040/FRESTORE/{form.Id}/{index?.Id ?? "displacement"}/header={(selfHeader ? "instruction-extension" : header.ToString("X4"))}/op={words[0]:X4}/ccr={ccr:X2}"); + } + report.Complete(output); + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticSystemTests.cs b/Copper68k.Tests/Synthetic/SyntheticSystemTests.cs index 73356fd7..a5ee4466 100644 --- a/Copper68k.Tests/Synthetic/SyntheticSystemTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticSystemTests.cs @@ -15,7 +15,9 @@ public void TrapsPrivilegeStopsAndResetPreserveArchitecturalState(string modelId foreach (var supervisor in new[] { false, true }) { for (var trap = 0; trap < 16; trap++) Basic(machine, report, "TRAP", (ushort)(0x4e40 | trap), ccr, supervisor); - foreach (var (family, opcode) in new[] { ("NOP", 0x4e71), ("RESET", 0x4e70), ("ILLEGAL", 0x4afc), ("LineA", 0xa123), ("LineF", 0xf123), ("TRAPV", 0x4e76) }) + // Type 110 is an unassigned F-line word on every selected model. + // F123 is instead a legal privileged cpSAVE on EC020/020. + foreach (var (family, opcode) in new[] { ("NOP", 0x4e71), ("RESET", 0x4e70), ("ILLEGAL", 0x4afc), ("LineA", 0xa123), ("LineF", 0xf1c0), ("TRAPV", 0x4e76) }) Basic(machine, report, family, (ushort)opcode, ccr, supervisor); foreach (var sr in new ushort[] { 0, 0x0700, 0x2000, 0x2700, 0x271f }) Basic(machine, report, "STOP", 0x4e72, ccr, supervisor, sr); } diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index 22e8f4e1..1c374d6c 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -2857,6 +2857,11 @@ private bool TryExecuteFpuInstruction(ushort opcode) BeginInstruction(opcode); _ = FetchWord(); + if ((opcode & 0x003F) >= 0x003D) + { + RaiseFormat0Exception(VectorLineF, State.LastInstructionProgramCounter, M68kInstructionTimingKey.LineFException); + return true; + } var extension = FetchWord(); return ExecuteFpuCommand(opcode, extension, useFastTiming: false); } @@ -3143,6 +3148,11 @@ private void ExecuteFpuConditional(ushort opcode) { BeginInstruction(opcode); _ = FetchWord(); + if ((opcode & 0x3F) >= 0x3D) + { + RaiseFormat0Exception(VectorLineF, State.LastInstructionProgramCounter, M68kInstructionTimingKey.LineFException); + return; + } var mode = (opcode >> 3) & 7; var register = opcode & 7; var extension = FetchWord(); @@ -3254,15 +3264,15 @@ private bool ExecuteFsave(ushort opcode) { var mode = (opcode >> 3) & 7; var register = opcode & 7; - if ((State.StatusRegister & M68kCpuState.Supervisor) == 0) + if (!IsValidCoprocessorStateFrameEa(opcode)) { - RaiseFormat0Exception(8, State.LastInstructionProgramCounter, M68kInstructionTimingKey.PrivilegeViolation); + RaiseFormat0Exception(VectorLineF, State.LastInstructionProgramCounter, M68kInstructionTimingKey.LineFException); return true; } - if (!IsValidFpuStateFrameEa(mode, register, restore: false)) + if ((State.StatusRegister & M68kCpuState.Supervisor) == 0) { - RaiseFormat0Exception(VectorLineF, State.LastInstructionProgramCounter, M68kInstructionTimingKey.LineFException); + RaiseFormat0Exception(8, State.LastInstructionProgramCounter, M68kInstructionTimingKey.PrivilegeViolation); return true; } @@ -3292,15 +3302,15 @@ private bool ExecuteFrestore(ushort opcode) { var mode = (opcode >> 3) & 7; var register = opcode & 7; - if ((State.StatusRegister & M68kCpuState.Supervisor) == 0) + if (!IsValidCoprocessorStateFrameEa(opcode)) { - RaiseFormat0Exception(8, State.LastInstructionProgramCounter, M68kInstructionTimingKey.PrivilegeViolation); + RaiseFormat0Exception(VectorLineF, State.LastInstructionProgramCounter, M68kInstructionTimingKey.LineFException); return true; } - if (!IsValidFpuStateFrameEa(mode, register, restore: true)) + if ((State.StatusRegister & M68kCpuState.Supervisor) == 0) { - RaiseFormat0Exception(VectorLineF, State.LastInstructionProgramCounter, M68kInstructionTimingKey.LineFException); + RaiseFormat0Exception(8, State.LastInstructionProgramCounter, M68kInstructionTimingKey.PrivilegeViolation); return true; } @@ -4092,7 +4102,9 @@ private uint GetFpuIndexedAddress(uint baseAddress) } var outerDisplacement = ReadFpuIndexedDisplacement(indirectSelection & 3, baseDisplacement: false); - if (indirectSelection >= 5) + // PRM table 2-2: IIS 001..011 adds the index before the pointer + // read; IIS 101..111 adds it after the pointer read. + if (indirectSelection <= 3) { var pointer = ReadLong(unchecked(baseValue + baseDisplacement + indexValue)); return unchecked(pointer + outerDisplacement); @@ -4128,11 +4140,6 @@ private uint ReadFpuIndexedDisplacement(int size, bool baseDisplacement) }; } - private static bool IsValidFpuStateFrameEa(int mode, int register, bool restore) - => mode is 2 or 5 or 6 || - (restore ? mode == 3 : mode == 4) || - (mode == 7 && register is 0 or 1); - private uint GetFpuStateFrameAddress(int mode, int register, uint byteSize, bool restore) { return mode switch @@ -4144,6 +4151,8 @@ private uint GetFpuStateFrameAddress(int mode, int register, uint byteSize, bool 6 => GetFpuIndexedAddress(State.A[register]), 7 when register == 0 => unchecked((uint)(int)(short)FetchWord()), 7 when register == 1 => FetchLong(), + 7 when register == 2 && restore => GetFpuPcDisplacementAddress(), + 7 when register == 3 && restore => GetFpuPcIndexedAddress(), _ => throw new UnsupportedM68kTimingException(State.LastOpcode, State.LastInstructionProgramCounter, _profile) }; } diff --git a/Copper68k/M68060Interpreter.cs b/Copper68k/M68060Interpreter.cs index 7b7ba8d0..ddf5e44b 100644 --- a/Copper68k/M68060Interpreter.cs +++ b/Copper68k/M68060Interpreter.cs @@ -64,6 +64,13 @@ protected override bool TryExecuteModelSpecificInstruction(ushort opcode) RaiseFormat0Exception(61, pc, M68kInstructionTimingKey.IllegalInstruction); return true; } + if ((opcode & 0xFF80) == 0xF200 && (opcode & 0x3F) >= 0x3D) + { + BeginInstruction(opcode); + _ = FetchWord(); + RaiseFormat0Exception(11, State.LastInstructionProgramCounter, M68kInstructionTimingKey.LineFException); + return true; + } if ((opcode & 0xFFF0) == 0xF200) return ExecuteFpuControlTransfer(opcode); if ((opcode & 0xFE00) == 0xF200) throw Unavailable("floating-point execution", opcode); @@ -117,6 +124,11 @@ private bool ExecuteFpuStateTransfer(ushort opcode) BeginInstruction(opcode); var pc = State.ProgramCounter; _ = FetchWord(); + if (!IsValidCoprocessorStateFrameEa(opcode)) + { + RaiseFormat0Exception(11, pc, M68kInstructionTimingKey.LineFException); + return true; + } if ((State.StatusRegister & M68kCpuState.Supervisor) == 0) { RaiseFormat0Exception(8, pc, M68kInstructionTimingKey.PrivilegeViolation); diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index 48c5fd53..2ffde9e0 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -5710,6 +5710,17 @@ private bool ExecuteDispatchedInstruction(ushort opcode, M68020OpcodeKind kind) case M68020OpcodeKind.LineFException: BeginInstruction(opcode); _ = FetchWord(); + // MC68020UM 7.5.2.2/3; MC68030UM 10.1, 10.5.2.2/3: + // legal cpSAVE/cpRESTORE checks privilege before accessing + // an absent coprocessor. 030 CpID 0 is the internal MMU. + if (_profile.Model is M68kAcceleratorModel.M68020 or M68kAcceleratorModel.M68030 && + (_profile.Model != M68kAcceleratorModel.M68030 || (opcode & 0x0E00) != 0) && + (opcode & 0xF180) == 0xF100 && IsValidCoprocessorStateFrameEa(opcode) && + (State.StatusRegister & M68kCpuState.Supervisor) == 0) + { + RaiseFormat0Exception(8, State.LastInstructionProgramCounter, M68kInstructionTimingKey.PrivilegeViolation); + return true; + } if (opcode == 0xFF00) { var token = FetchLong(); @@ -8416,6 +8427,16 @@ protected virtual bool TryExecuteApproximateInstruction(ushort opcode) return false; } + // cpSAVE permits control-alterable/predecrement; cpRESTORE permits + // control/postincrement, including PC-relative sources (M68000PM 6-13/16). + protected static bool IsValidCoprocessorStateFrameEa(ushort opcode) + { + var mode = (opcode >> 3) & 7; + var restore = (opcode & 0x40) != 0; + return mode is 2 or 5 or 6 || mode == (restore ? 3 : 4) || + (mode == 7 && (opcode & 7) <= (restore ? 3 : 1)); + } + protected virtual bool TryExecuteModelSpecificInstruction(ushort opcode) { _ = opcode; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 4f5f923b..36e30439 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -2034,3 +2034,135 @@ on EC020/A1200/020/030, first stopping at extension words `0031`, `0084` or entire multiply/divide families are not excluded. Earlier restoration and reference requirements remain open, so this checkpoint does not complete the milestone 6 goal. + +### Line-F state and operand qualification (2026-10-05) + +The unchanged Basic ILLEGAL diagnostics exposed incorrect privilege ordering on +EC020/020/030, invalid FSAVE/FRESTORE operands on 040/060, and unassigned FPU +operand fields on 060. The corrections use architectural exceptions before +operand effects, retaining the existing exception timing policy, host gateway +`FF00` and public API. No instruction is retried after partial effects. + +Authority is [MC68020UM 7.2.3.3/4 and 7.5.2.2/3](https://www.nxp.com/docs/en/data-sheet/MC68020UM.pdf) +and [MC68030UM 10.1, 10.2.3.3/4 and 10.5.2.2/3](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P2.pdf): +legal cpSAVE/cpRESTORE instructions check supervisor privilege before contacting +an absent coprocessor. Invalid first-word operands instead take vector 11. +The 030's CpID 0 denotes its internal MMU, so these words do not acquire the +external-coprocessor privilege rule. Existing supervisor absent-coprocessor +fallback behavior is retained; these tests do not qualify physical CIR/bus-fault +sequencing or implement an external coprocessor responder. + +[M68000PM 6-13/16](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) +and [MC68060UM D-15/18 and 8.2.4](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf) +define the state-transfer EAs and distinguish unrecognized first words from +unimplemented floating-point operations. Invalid 040/060 FSAVE/FRESTORE words +now take format-zero line-F before a privilege check. FPU command/conditional +mode-7 register fields 5..7 are unassigned and take line-F before command or +operand execution. Legal FDBcc/FTRAPcc neighbors retain their separate paths; +060 floating-point arithmetic and legal unimplemented FPU operations remain +outside this integer profile's qualification. + +The shared state-frame legality predicate admits legal PC-relative FRESTORE +sources. The 040 now executes those sources with the extension-word PC base. +Their independent fixtures also detected reversed preindexed/postindexed pointer +ordering in the existing 040 FPU EA helper, corrected according to M68000PM +table 2-2. This correction applies to that shared helper; legal timing-bearing +FPU, integer, JIT and consumer regressions remain required. + +| Required batch | Cases per profile | Scope | +| --- | ---: | --- | +| `system-linef-state-encodings` | 65,536 (000/010); 47,616 (EC020/A1200/020); 49,856 (030); 60,224 (040); 61,248 (060) | Save/restore first words, all CpID/EA encodings, both privilege states and all CCR values for required exceptions | +| `system-linef-unassigned-fpu-ea` | 1,536 | Six unassigned command/conditional words, four following extension patterns, both stacks and all CCR values | +| `system-linef-pc-restore` | 6,464 (040 only) | Displacement, brief index and all 66 legal full-format structures; NULL/IDLE/invalid frames and all CCR values | + +The addition is **464,000 logical cases in 17 batches**. Legal supervisor +coprocessor/FPU state protocols are explicitly outside the first-word exception +batch, not counted as passing exceptions. 040/060 PFLUSH and 040 PTEST overlap +these bit patterns but are integer MMU instructions; their existing separate +matrices own them. The first fixture version failed to distinguish those +overlaps; `artifacts/m6-linef-before/` is retained as fixture debugging, not CPU +failed-before evidence. The qualified baseline is +`artifacts/m6-linef-qualified-before/`: each EC020/A1200/020 has 17,920 privilege +mismatches, 030 has 15,680, 040 has 1,856 invalid-state priority mismatches, and +060 has 3,712 invalid-state mismatches. The original three unassigned FPU command +words have 384 unsupported 040 cases and 768 060 emulator-error mismatches. + +After command corrections, the additional conditional words are proved failing +in `artifacts/m6-linef-conditional-before/`: 040 has 576 unsupported cases and +060 has 768 emulator-error mismatches. Corrected PC fixtures preserve instruction +bytes when a null displacement and suppressed index makes the instruction's own +extension a header or pointer. Such direct self-reference can only test an +invalid frame, not simultaneously encode a NULL/IDLE header. The original PC +fixture debugging run is retained separately; qualified index-order failed-before +evidence has 3,456 mismatches in `artifacts/m6-linef-qualified-index-before/`. +Removing only the new PC address paths produces 6,464 unsupported cases in +`artifacts/m6-linef-pc-mutation/`; source is restored before final validation. + +The earlier generic line-F fixture `F123` was a legal privileged cpSAVE word on +EC020/020. Its common unassigned-word example is corrected to `F1C0`; the new +matrix retains the actual `F123` exception outcomes. This is an expectation +correction, not regression retirement. Existing specialized FPU state/timing, +bus/prefetch/cache, JIT and native tests remain retained. No additional old test +is retired in this follow-up. + +The unchanged final Basic audit now passes ILLEGAL on EC020, A1200 and 020 +(35,828 callbacks each), and 030 (34,780). The 000/010 groups retain 34,880 each. +040 advances to `F400` at callback 29,331; 060 advances past `F27D` to `F380` +at 29,074. Overall it still fails: **1,326 passing / 47 mismatching / eight +unsupported / zero untested groups**, over **11,473,937 callbacks and 1,663,635 +frames**. All 32 controls pass; masked-SR count 199,327, one terminal callback, +original manifest and native bridge remain unchanged. No exclusion, mask change +or fixture alteration makes the failing audit green. Evidence: +`artifacts/m6-linef-final-broad/`. + +`F400` needs further per-case qualification: WinUAE expects vector 11 whereas +M68000PM 6-3/6-9 explicitly assigns scope 00 an illegal-instruction trap. The +current CPU raises vector 4; the native diagnostic reports a subsequent trap as +"no exception", so the adapter boundary also needs inspection. No CPU change +is made merely to match this software disagreement. `F380` is a further +unassigned 060 F-line category still caught by its broad unsupported floating +execution path. The eight DIVL/MULL unsupported groups and all earlier advanced +restoration/reference/consolidation requirements remain open. Milestone 6 stays +in progress; these passing scopes do not establish roadmap completion. + +Final Release CPU validation passes **4,874 tests with ten optional skips** and +zero failures in `artifacts/m6-linef-full/`. Both qualified exception presets +execute: BKPT 224 callbacks / 224 frames / 21 controls, trap/bounds 951,522 +callbacks / 476,339 frames / 63 controls. The strict gate checks **14,581,016 +logical cases in 566 batches**, retains `roadmapComplete=false`, and freshly +passes pinned SingleStepTests (312,500 cases / 125 files) and Musashi (536 +programs / 88 exclusions). Evidence: `artifacts/m6-linef-gate.log` and the full +report directory. Additional seeded audits are not claimed. The separate +qualified TRAP trace run passes 512 callbacks / 512 frames / six controls in +`artifacts/m6-linef-qualified-trace/`; an initial invocation omitted the native +library environment variable, failed preflight and is retained separately. +AHX passes 18 tests in `artifacts/m6-linef-ahx-results/`. + +Private **unpublished** package `1.5.2-synthetic-dev.52` has SHA-256 +`16eab48210a68daf11e118d5fa55aa52cf93296dd455d0686a31f2cdc33352a6`; +the packaged CPU assembly is +`5453e81863793e8155a8c1505aa867f10f3c7e872fb34172cbe80988a5d667b8`. +`artifacts/m6-linef-package.json` records source/assembly identities against +`281b91b`; the default-version CPU used by the reference adapters is +`a3f298936ca3ea57eefbb63b2c491105ee210c66138f96c7b66631b48fd10e79`. +Packing uses separate artifact outputs, so it cannot replace the assembly used +by the full CPU run. Final source and default CPU hashes remain identical after +the reference gate. + +The positive copied-report gate passes. Omitting each of the three new report +types, retaining the old 768-case unassigned-FPU count, or retaining the +unqualified 6,528-case PC fixture count is rejected. Original evidence remains +intact in its source directory; controls use separate copies. Evidence: +`artifacts/m6-linef-gate-controls/controls.json`. Ordinary CI requires every +new per-profile report at its exact cardinality. + +Isolated CopperScreen baseline `d9beae8` builds in Release with zero warnings or +errors against the exact .52 dependency. Host **149** pass with six optional +skips, disk **74**, separate engine diagnostics **1,080**, and all three native +Workbench/A1200 boot and disk-persistence replays pass without skips. All four +loaded CPU DLLs match the package ZIP entry, and all four dependency assets +resolve exactly .52. Evidence: `artifacts/linef-validation/`, +`artifacts/linef-diagnostic-tests/`, `artifacts/linef-production.binlog` and +`artifacts/linef-identities.json`. No media/build artifacts are committed, +unrelated root CopperScreen changes remain untouched, and package publication +is not authorized or performed. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index ef761314..c188bcbb 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1031,3 +1031,32 @@ host/disk/engine tests and all three native Workbench/A1200 replays, with exact package/version and loaded-DLL identity checks. `roadmapComplete=false` is retained; line-F priority, reserved multiply/divide extensions and earlier advanced restoration/reference/consolidation requirements remain open. + +The line-F follow-up qualifies cpSAVE/cpRESTORE first words, illegal 040/060 +state-transfer operands and unassigned FPU command/conditional EAs. It corrects +020/030 privilege-before-absent-coprocessor behavior and 040/060 invalid-word +priority. Independent PC-relative 040 FRESTORE fixtures also expose and correct +reversed preindexed/postindexed pointer ordering. The new required batches add +464,000 cases across eight profiles. MMU opcode overlaps, legal supervisor +coprocessor/FPU protocols and self-referential frame/extension encodings are +explicitly distinguished; no FPU arithmetic qualification is claimed. The old +generic F123 example is corrected to an unassigned F1C0 word because F123 is a +legal privileged cpSAVE on EC020/020. No specialized regression is retired. + +The unchanged WinUAE ILLEGAL groups now pass on EC020/A1200/020/030 and retain +000/010 passing coverage. Basic still fails overall: 1,326 passing, 47 +mismatching and eight unsupported groups. 040 F400 has a reference/manual +exception disagreement, and 060 reaches further unassigned F380 decoding. +Reserved long multiply/divide extension qualification and all earlier advanced +restoration/reference/consolidation requirements remain open. Exact authorities, +failed-before evidence, mutation controls and caveats are maintained in +`COPPER68K_REFERENCE_QUALIFICATION.md`; milestone 6 remains **in progress**. + +Final validation passes 4,874 CPU tests with ten optional skips and both qualified +exception presets enabled. The strict gate verifies 14,581,016 logical cases in +566 batches, with fresh pinned SingleStepTests/Musashi audits. Qualified TRAP +trace and AHX pass. Private unpublished .52 passes the isolated CopperScreen +Release build, host/disk/engine tests and all three native Workbench/A1200 +replays, with exact package-resolution and loaded-DLL identity checks. Timing +policy is retained; physical timing, host performance and package publication +are not claimed. `roadmapComplete=false` remains unchanged. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index c949f64d..b05b5764 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -100,6 +100,8 @@ try { 'logical-cas2'=36864; 'logical-shifts'=$(if ($model -in @('68000','68010')) {130561} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {131089}); 'system-basic'=1728; + 'system-linef-state-encodings'=$(if ($model -in @('68000','68010')) {65536} elseif ($model -eq '68030') {49856} elseif ($model -eq '68040') {60224} elseif ($model -eq '68060') {61248} else {47616}); + 'system-linef-unassigned-fpu-ea'=1536; 'system-trap-trace'=$(if ($model -eq '68040') {10368} elseif ($model -eq '68060') {8384} elseif ($model -in @('68000','68010')) {3968} else {5952}); 'system-chk-invalid-operands'=11264; 'system-unassigned-4140'=32768; @@ -141,7 +143,10 @@ try { $expected['system-move-word-restart-a7'] = 192 $expected['system-move-word-restart-invalid'] = 224 } - if ($model -eq '68040') { $expected['system-mmu-disabled'] = 34850 } + if ($model -eq '68040') { + $expected['system-mmu-disabled'] = 34850 + $expected['system-linef-pc-restore'] = 6464 + } foreach ($group in $expected.Keys) { $report = Get-Content -LiteralPath (Join-Path $output "$model-$group.json") -Raw | ConvertFrom-Json if ($report.schema -ne 1 -or $report.model -cne $model -or $report.group -cne $group -or From 2787ebf4343207caa5166c06e4a510fcb1864f9c Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 06:15:53 +0300 Subject: [PATCH 027/211] Handle unassigned 68060 F-line categories architecturally --- .../Synthetic/SyntheticLineFEncodingTests.cs | 24 +++++++ Copper68k/M68060Interpreter.cs | 4 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 64 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 23 +++++++ scripts/test-copper68k-synthetic.ps1 | 1 + 5 files changed, 115 insertions(+), 1 deletion(-) diff --git a/Copper68k.Tests/Synthetic/SyntheticLineFEncodingTests.cs b/Copper68k.Tests/Synthetic/SyntheticLineFEncodingTests.cs index 5682ce65..fa2c01f9 100644 --- a/Copper68k.Tests/Synthetic/SyntheticLineFEncodingTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticLineFEncodingTests.cs @@ -75,6 +75,30 @@ public void UnassignedFpuOperandFieldsTakeLineFWithoutOperandEffects(string mode report.Complete(output); } + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void UnassignedFpuCategoriesTakeLineFWithoutOperandEffects(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "system-linef-unassigned-fpu-types"); + // MC68020UM 7.5.2.2: types 110/111 are not coprocessor + // instructions. MC68060UM 8.2.4: unrecognized F-line words use + // vector 11, format 0 and the faulting PC, rather than the FPU's + // unimplemented-operation exception or a host unsupported error. + var words = Enumerable.Range(0xf380, 128).Select(x => (ushort)x).ToArray(); + Assert.Equal(128, words.Distinct().Count()); + Assert.Equal(0b001, (words[0] >> 9) & 7); + Assert.Equal(0b110, (words[0] >> 6) & 7); + Assert.Equal(0b111, (words[^1] >> 6) & 7); + Assert.DoesNotContain(words, x => x is 0xf327 or 0xf35f or 0xf588 or 0xf5c8); + foreach (var opcode in words) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, opcode, + $"LineF/FPU/unassigned-type={(opcode >> 6) & 7}/field={opcode & 0x3f:X2}", + supervisor, ccr, vector: 11); + report.Complete(output); + } + [Fact, Trait("Suite", "Synthetic")] public void M68040PcRelativeRestoreUsesExtensionBaseAndFullIndexPointerOrder() { diff --git a/Copper68k/M68060Interpreter.cs b/Copper68k/M68060Interpreter.cs index ddf5e44b..c2b51798 100644 --- a/Copper68k/M68060Interpreter.cs +++ b/Copper68k/M68060Interpreter.cs @@ -72,7 +72,9 @@ protected override bool TryExecuteModelSpecificInstruction(ushort opcode) return true; } if ((opcode & 0xFFF0) == 0xF200) return ExecuteFpuControlTransfer(opcode); - if ((opcode & 0xFE00) == 0xF200) + // Types 110/111 are unassigned F-line words, not unsupported + // floating-point operations (MC68020UM 7.5.2.2; MC68060UM 8.2.4). + if ((opcode & 0xFE00) == 0xF200 && (opcode & 0x0180) != 0x0180) throw Unavailable("floating-point execution", opcode); if ((opcode & 0xFFE0) == 0xF500 || (opcode & 0xFFF8) is 0xF588 or 0xF5C8) throw Unavailable("MMU instructions", opcode); diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 36e30439..33807a3b 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -2166,3 +2166,67 @@ resolve exactly .52. Evidence: `artifacts/linef-validation/`, `artifacts/linef-identities.json`. No media/build artifacts are committed, unrelated root CopperScreen changes remain untouched, and package publication is not authorized or performed. + +### Unassigned FPU category follow-up (2026-10-05) + +MC68020UM 7.5.2.2 identifies coprocessor instruction types 110/111 as +unassigned. MC68060UM 8.2.4 assigns unrecognized F-line words vector 11 with +a format-zero frame and the causing instruction address. The 060 unavailable +floating-point guard incorrectly intercepted these words. Its narrow correction +allows the existing architectural line-F path to handle them before operand +effects. Actual floating-point operations retain their existing handling; no +FPU arithmetic, enabled-MMU, physical timing or instruction retry is added. + +The new required `system-linef-unassigned-fpu-types` batch covers every +`F380..F3FF` word (CpID 1), both stacks and all 32 CCR states: 8,192 cases per +profile, 65,536 across all eight profiles. Independent exception verification +checks saved PC/SR, stack selection, registers and memory. Fixed encoding +examples distinguish state-transfer words and CpID 2 PLPA aliases, which must +not be classified by a blanket category guard. Failed-before evidence in +`artifacts/m6-fpu-types-before/` has seven passing profiles and all 8,192 060 +cases mismatching because of emulator exceptions. The corrected focused run +passes all 107 selected tests without skips in +`artifacts/m6-fpu-types-focused/`. No specialized regression is retired. + +The unchanged Basic reference now reaches `F400` on both 040 and 060 at callback +29,331. This retains the previously documented illegal-instruction versus +line-F reference/manual disagreement and native adapter diagnostic caveat. +Overall Basic still fails: 1,326 passing, 47 mismatching, eight unsupported and +zero untested groups, with 11,474,194 callbacks. Input manifest, native bridge, +comparison masks and family selections remain unchanged. Evidence: +`artifacts/m6-fpu-types-broad/`. Reserved long multiply/divide extensions and +all earlier restoration/reference/consolidation requirements remain open. +Milestone 6 remains in progress; the passing bounded scope does not establish +roadmap completion. + +Final Release CPU validation passes **4,882 tests with ten optional skips** and +zero failures in `artifacts/m6-fpu-types-full/`. Qualified BKPT and trap/bounds +retain their exact 224 / 951,522 callbacks and 224 / 476,339 frame assertions. +The strict gate verifies **14,646,552 logical cases in 574 batches**, with +`roadmapComplete=false`, fresh pinned SingleStepTests (312,500 cases in 125 +files) and Musashi (536 programs, 88 exclusions). Separate qualified TRAP trace +passes 512 callbacks and 512 frames; AHX passes 18 tests. Evidence: +`artifacts/m6-fpu-types-gate.log`, `artifacts/m6-fpu-types-trace/` and +`artifacts/m6-fpu-types-ahx-results/`. The copied-report positive control passes; +omitting the new 060 report or reducing its count to 8,191 fails the gate. +Original reports remain intact. Evidence: +`artifacts/m6-fpu-types-gate-controls/controls.json`. + +Private **unpublished** package `1.5.2-synthetic-dev.53` has SHA-256 +`6157c9f4585cee92c15efc1173dfb33a15413eed8164d843c87439553f81b496`; +its CPU assembly is +`4118e59f278507443e8d763f0d75d800b6cc45d9b13082179528d13b08ddb7a0`. +`artifacts/m6-fpu-types-package.json` records source/package/assembly identities +against `903f45c`. The default-version CPU used by the reference adapters is +`e2cc6606d8b2ae05604f6413ec2fb45e3d30535cb5acd8a2c72590bc32e53a10`. +Packing uses separate outputs; final source and CPU hashes remain unchanged. + +Isolated CopperScreen baseline `d9beae8` builds in Release with zero warnings or +errors against .53. Host 149 pass with six optional skips, disk 74, separate +engine diagnostics 1,080, and all three native Workbench/A1200 boot and +disk-persistence replays pass without skips. All four loaded CPU DLLs match the +package ZIP entry, and all four assets resolve exactly .53. Evidence: +`artifacts/fpu-types-validation/`, `artifacts/fpu-types-diagnostic-tests/`, +`artifacts/fpu-types-production.binlog` and `artifacts/fpu-types-identities.json`. +Unrelated CopperScreen changes remain untouched; no media/artifacts or package +publication is included in this source checkpoint. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index c188bcbb..cae72838 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1060,3 +1060,26 @@ Release build, host/disk/engine tests and all three native Workbench/A1200 replays, with exact package-resolution and loaded-DLL identity checks. Timing policy is retained; physical timing, host performance and package publication are not claimed. `roadmapComplete=false` remains unchanged. + +The unassigned FPU category follow-up corrects the 060 unsupported-operation +guard for CpID 1 types 110/111. Every F380..F3FF word now enters the existing +architectural vector-11 path without operand effects. One new required batch +per profile adds 65,536 cases across eight profiles, both stacks and all CCR +states. Failed-before evidence detects all 8,192 060 cases; the other seven +profiles already pass. No FPU arithmetic, timing-policy change, instruction +retry or specialized-test retirement is introduced. + +The unchanged broad Basic reference advances 060 to F400, matching the existing +040 reference/manual disagreement. Its overall 1,326 passing, 47 mismatching +and eight unsupported groups remain explicitly failing. Reserved long +multiply/divide extensions and all earlier advanced restoration, reference and +consolidation requirements remain open. Milestone 6 stays **in progress**. + +Final validation passes 4,882 CPU tests with ten optional skips and both +qualified exception presets enabled. The strict gate verifies 14,646,552 +logical cases in 574 batches, with fresh pinned SingleStepTests/Musashi audits; +missing and stale-cardinality category reports fail its controls. Qualified +TRAP trace and AHX pass. Private unpublished .53 passes the isolated CopperScreen +Release build, host/disk/engine tests and all three native Workbench/A1200 +replays, with exact package and loaded-assembly identity checks. Timing policy +is retained; `roadmapComplete=false` remains unchanged. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index b05b5764..e856ea5f 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -102,6 +102,7 @@ try { 'system-basic'=1728; 'system-linef-state-encodings'=$(if ($model -in @('68000','68010')) {65536} elseif ($model -eq '68030') {49856} elseif ($model -eq '68040') {60224} elseif ($model -eq '68060') {61248} else {47616}); 'system-linef-unassigned-fpu-ea'=1536; + 'system-linef-unassigned-fpu-types'=8192; 'system-trap-trace'=$(if ($model -eq '68040') {10368} elseif ($model -eq '68060') {8384} elseif ($model -in @('68000','68010')) {3968} else {5952}); 'system-chk-invalid-operands'=11264; 'system-unassigned-4140'=32768; From e8140401e9ea37e2a05583eb3cea9f12c9b4a0a5 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 06:45:31 +0300 Subject: [PATCH 028/211] Qualify legal long arithmetic reference encodings and exceptions --- .../M68kWinUaeCpuTesterConformanceTests.cs | 15 +- .../M68kWinUaeCpuTesterConformanceTests.md | 67 ++++++- .../M68kWinUaeLongArithmeticAuditTests.cs | 179 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 152 ++++++++++++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 29 +++ scripts/prepare-copper68k-winuae.ps1 | 34 +++- .../test-copper68k-winuae-long-arithmetic.ps1 | 36 ++++ .../winuae/long-arithmetic-encodings.patch | 28 +++ .../long-arithmetic-unimplemented.patch | 37 ++++ 9 files changed, 565 insertions(+), 12 deletions(-) create mode 100644 Copper68k.Tests/M68kWinUaeLongArithmeticAuditTests.cs create mode 100644 scripts/test-copper68k-winuae-long-arithmetic.ps1 create mode 100644 scripts/winuae/long-arithmetic-encodings.patch create mode 100644 scripts/winuae/long-arithmetic-unimplemented.patch diff --git a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs index 92b33a7a..c6da77fe 100644 --- a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs +++ b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs @@ -311,6 +311,8 @@ private sealed class NativeTester : IDisposable private ushort _corruptSr; private bool _corruptIgnoredSr; private string _integerFamily = ""; + private bool _qualifyLongArithmetic; + private readonly Dictionary _longArithmeticForms = new(StringComparer.Ordinal); private NativeTester(IntPtr library) { @@ -354,7 +356,8 @@ public RunResult Run( bool corruptResult = false, bool corruptFrame = false, ushort corruptSr = 0, - bool corruptIgnoredSr = false) + bool corruptIgnoredSr = false, + bool qualifyLongArithmetic = false) { _callbackException = null; _executedCases = 0; @@ -370,6 +373,8 @@ public RunResult Run( _corruptSr = corruptSr; _corruptIgnoredSr = corruptIgnoredSr; _integerFamily = opcode; + _qualifyLongArithmetic = qualifyLongArithmetic; + _longArithmeticForms.Clear(); if (integerProfile is not null && (_destroy is null || _addressingMask is null || _lastOutput is null)) throw new XunitException("Multi-model integer audit requires the qualified native bridge exports (destroy, addressing mask and diagnostics)."); if (integerProfile is not null && (_definedSr is null || _frameChecks is null || _maskedCases is null)) @@ -454,6 +459,7 @@ public readonly record struct RunResult( public int TerminalCases => _terminalCases; public int TraceInputCases => _traceInputCases; public bool UnsupportedExecution => _callbackException?.InnerException is UnsupportedM68kTimingException; + public IReadOnlyDictionary LongArithmeticForms => _longArithmeticForms; public void Dispose() { @@ -483,6 +489,13 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt ? new M68040Interpreter(bus, M68020CpuProfile.Ocs68040Accelerator25Mhz) : new M68kInterpreter(bus); _lastCaseSummary = (_integerProfile is null ? "" : $"family={_integerFamily}, ") + FormatCaseSummary(context, registers, bus); + if (_qualifyLongArithmetic) + { + // Inspect immutable fixture bytes; never normalize input in the bridge. + var form = QualifyLongArithmeticEncoding(bus.ReadHostWord(registers.Pc), + bus.ReadHostWord(registers.Pc + 2), _integerFamily); + _longArithmeticForms[form] = _longArithmeticForms.GetValueOrDefault(form) + 1; + } bus.CopyStackImage(registers.Regs[15], registers.Ssp, 0x20); ApplyRegisters(cpu, registers, _cpuLevel); diff --git a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md index 8b54f4b5..b22efd22 100644 --- a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md +++ b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md @@ -209,9 +209,11 @@ Other saved-PC and result disagreements remain unclassified pending equivalent manual/source audits. No family is excluded and the requested audit remains red. Schema 2 separates emulator-unsupported callbacks from value/frame mismatches. -Both fail the gate. The current classifier records 62 mismatching and 15 unsupported -groups (77 total), with zero untested; frame/masked-case counts and all 32 controls -are recorded separately from normal callbacks. Older bridges lacking these +Both fail the gate. The initial schema-2 checkpoint recorded 62 mismatching and +15 unsupported groups (77 total), with zero untested. The current Basic audit +records 1,326 passing, 47 mismatching and eight reserved-encoding unsupported +groups, with zero untested. Frame/masked-case counts and all 32 controls are +recorded separately from normal callbacks. Older bridges lacking these assertions are rejected. Source/adapter/CPU disagreements remain open. Integer callbacks also stop at an actual STOP/HALT boundary. The adapter does @@ -262,3 +264,62 @@ results, traced callback counts, frames and controls. Other traced families and models, M-mode, fault combinations and advanced restart remain untested by this preset. The broad Basic discovery audit retains its separate failures; this focused preset cannot make it pass. + +## Qualified long multiply/divide preset + +`LongArithmetic` independently qualifies MULL.L and DIVL.L on EC020/A1200, +020, 030, 040 and 060. The original Basic corpus is preserved. Its 020/030 +long-arithmetic cases contain nonzero reserved extension fields, so their +unsupported execution cannot qualify documented arithmetic semantics. + +[M68000PM 4-94/98/136/140](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) +marks extension bits 15 and 9..3 as zero. It also declares a 64-bit multiply +with Dh == Dl undefined. The separate +[encoding-selection patch](../scripts/winuae/long-arithmetic-encodings.patch) +clears reserved fields on every applicable model and selects a distinct Dh +for that undefined multiply form **before reference execution**. Legal divide +register aliases remain selected. The bridge checks every callback's raw words +and rejects unqualified inputs; it never changes fixture operands or returned +CPU results to satisfy the reference. + +The pinned generator also synchronizes PC and reads the operand before detecting +060 unavailable 64-bit operations. Its later rollback path incorrectly restores +postincrement/predecrement for a completed divide-by-zero trap. The separate +[060 exception patch](../scripts/winuae/long-arithmetic-unimplemented.patch) +detects vector 61 before EA effects, saves the causing instruction PC as required +by [MC68060UM 8.2.4/C.2.2](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf), +and leaves ordinary divide-by-zero EA effects intact. The latter expectation +interprets the manual's 8.3 group-3 completion rule with PRM 2.2.4/5 addressing +semantics; hardware corroboration is not claimed. Copper68k already implements +these expectations. No production CPU, timing or flag-mask change is required. + +```powershell +./scripts/prepare-copper68k-winuae.ps1 ` + -GeneratorSource artifacts/reference-winuae-api ` + -RunnerSource artifacts/reference-copperline ` + -VcVars64 '/VC/Auxiliary/Build/vcvars64.bat' ` + -Preset LongArithmetic -OutputDirectory artifacts/winuae-long-arithmetic-inputs +./scripts/test-copper68k-winuae-long-arithmetic.ps1 ` + -InputDirectory artifacts/winuae-long-arithmetic-inputs ` + -OutputDirectory artifacts/winuae-long-arithmetic-report +``` + +The audit requires both qualified source/patch identities, the recorded generator +executable/native bridge, complete profile/family selections, all exact input +hashes, and pinned per-family callback/frame/masked-CCR/architectural-form counts. +It executes 28,418 callbacks and 4,992 frames in twelve groups. Every group must +reject register and defined-X corruption; exception-bearing groups must reject +frame corruption. DIVL groups must accept changes confined to documented +undefined flags. All four signed/unsigned and 32/64-bit categories are required, +including 060 vector-61 outcomes. Coverage reports record 12,420 distinct +model/family/sign/width/EA/register combinations; this does not imply exhaustive +external addressing or operand coverage. + +`winuae-long-arithmetic-audit.json` keeps passing, mismatching, unsupported, +untested and explicitly excluded encodings/profiles separate. The standalone +command restores its environment and fails on missing fixtures, empty/duplicate +selection, mismatches or incomplete scope. Ordinary CI runs the independent +encoding checks; this external audit remains opt-in. No new seeded audit, +trace/bus-fault/MMU/cache/pipeline qualification or old regression retirement is +claimed. Full current evidence and remaining milestone requirements are in +[reference qualification](../docs/COPPER68K_REFERENCE_QUALIFICATION.md#long-arithmetic-reference-qualification-2026-10-05). diff --git a/Copper68k.Tests/M68kWinUaeLongArithmeticAuditTests.cs b/Copper68k.Tests/M68kWinUaeLongArithmeticAuditTests.cs new file mode 100644 index 00000000..8f83cfcb --- /dev/null +++ b/Copper68k.Tests/M68kWinUaeLongArithmeticAuditTests.cs @@ -0,0 +1,179 @@ +using System.Text.Json; +using Copper68k.Tests.Synthetic; +using Xunit.Sdk; + +namespace Copper68k.Tests; + +public sealed partial class M68kWinUaeCpuTesterConformanceTests +{ + internal const string LongArithmeticSourceHash = "0012b9bd9cd2a172c4c321a8cf8e9f1095d32f824286518e61c9d4e75bc5eb43"; + internal const string LongArithmeticPatchHash = "44e8dacde5a11e9cbd2e15119362a09b35c2e109fbbdbecbefa92929e89b32ea"; + internal const string LongArithmeticCpuSourceHash = "f1ec43c3658de9ae99e6e60f8376979f562c3b55d338a92e5d4f4849434b0799"; + internal const string LongArithmeticCpuPatchHash = "75a9e8d7ce2f534d2b16910dffe436e9d8396b3f470628bc59de9852898e0733"; + private static readonly string[] LongArithmeticFamilies = ["DIVL.L", "MULL.L"]; + private static readonly string[] LongArithmeticProfiles = ["68EC020", "68020", "68030", "68040", "68060"]; + + [EnvironmentFact("COPPER68K_RUN_WINUAE_LONG_ARITHMETIC_AUDIT", "audit qualified legal WinUAE long multiply/divide encodings")] + public void WinUaeLongArithmeticAcrossAdvancedModelsWhenEnabled() + { + var root = Environment.GetEnvironmentVariable("COPPER68K_WINUAE_LONG_ARITHMETIC_PATH"); + var library = Environment.GetEnvironmentVariable("COPPER68K_WINUAE_LONG_ARITHMETIC_LIBRARY"); + var output = Environment.GetEnvironmentVariable("COPPER68K_SYNTHETIC_REPORT_DIR"); + if (string.IsNullOrWhiteSpace(root) || string.IsNullOrWhiteSpace(library) || string.IsNullOrWhiteSpace(output)) + throw new XunitException("LongArithmetic audit requires qualified fixtures, native library and report directory."); + var manifest = ValidateLongArithmeticIdentity(root, library); + var models = LongArithmeticProfiles.Append("A1200").Select(id => ModelSpec.All.Single(m => m.Id == id)).ToArray(); + foreach (var model in models) ValidateWinUaeProfile(root, manifest, model, requiredOpcodes: LongArithmeticFamilies); + var rows = new List(); + var failures = new List(); + using var tester = NativeTester.Load(library); + foreach (var model in models) + foreach (var family in LongArithmeticFamilies) + { + var fixture = manifest.Profiles.Single(p => p.Id == FixtureId(model.Id)); + var path = Path.Combine(root, fixture.Id); + var register = tester.Run(path, family, fixture.CpuLevel, false, false, model, + corruptResult: true, qualifyLongArithmetic: true); + var sr = tester.Run(path, family, fixture.CpuLevel, false, false, model, + corruptSr: 0x10, qualifyLongArithmetic: true); + var frameRequired = family == "DIVL.L" || model.Id == "68060"; + var frame = frameRequired ? tester.Run(path, family, fixture.CpuLevel, false, false, model, + corruptFrame: true, qualifyLongArithmetic: true) : default; + var frameDetected = !frameRequired || (!frame.Passed && tester.FrameChecks > 0 && + frame.Detail.Contains("frame byte", StringComparison.Ordinal)); + var ignoredRequired = family == "DIVL.L"; + var ignored = ignoredRequired ? tester.Run(path, family, fixture.CpuLevel, false, false, model, + corruptIgnoredSr: true, qualifyLongArithmetic: true) : default; + var ignoredAccepted = !ignoredRequired || (ignored.Passed && ignored.ExecutedCases > 0 && tester.MaskedCases > 0); + var controls = !register.Passed && register.ExecutedCases > 0 && + !sr.Passed && sr.ExecutedCases > 0 && sr.Detail.Contains("SR:", StringComparison.Ordinal) && + frameDetected && ignoredAccepted; + var result = tester.Run(path, family, fixture.CpuLevel, false, false, model, qualifyLongArithmetic: true); + var forms = new SortedDictionary(tester.LongArithmeticForms.ToDictionary(x => x.Key, x => x.Value), StringComparer.Ordinal); + var expected = LongArithmeticCounts(model.Id, family); + var passing = controls && result.Passed && result.ExecutedCases == expected.Cases && + tester.FrameChecks == expected.Frames && tester.MaskedCases == expected.Masked && + forms.Count == expected.Forms && forms.Values.Sum() == result.ExecutedCases && + new[] { "signed/32/", "signed/64/", "unsigned/32/", "unsigned/64/" }.All(prefix => forms.Keys.Any(k => k.StartsWith(prefix, StringComparison.Ordinal))); + var status = passing ? "passing" : tester.UnsupportedExecution ? "unsupported" : result.ExecutedCases == 0 ? "untested" : "mismatching"; + var detail = $"{result.Detail} Expected/actual callbacks={expected.Cases}/{result.ExecutedCases}, frames={expected.Frames}/{tester.FrameChecks}, masked={expected.Masked}/{tester.MaskedCases}, forms={expected.Forms}/{forms.Count}."; + rows.Add(new(model.Id, family, status, result.ExecutedCases, tester.FrameChecks, + tester.MaskedCases, controls, register.ExecutedCases, sr.ExecutedCases, frameRequired, + frameDetected, frame.ExecutedCases, ignoredRequired, ignoredAccepted, ignored.ExecutedCases, forms, detail)); + if (!passing) failures.Add($"{model.Id}/{family}: controls={controls}; {detail}"); + _output.WriteLine($"{model.Id}/{family}: {result.ExecutedCases} callbacks, {tester.FrameChecks} frames; {status}; controls={controls}."); + } + Directory.CreateDirectory(output); + File.WriteAllText(Path.Combine(output, "winuae-long-arithmetic-audit.json"), JsonSerializer.Serialize(new + { + schema = 1, reference = "WinUAE LongArithmetic preset with Motorola-qualified legal extension selection", + manifest.GeneratorCommit, manifest.RunnerCommit, manifest.NativeLibrarySha256, + manifestSha256 = Hash(Path.Combine(root, "manifest.json")), generatorNormalizedSourceSha256 = LongArithmeticSourceHash, + generatorPatchSha256 = LongArithmeticPatchHash, + cpuGeneratorNormalizedSourceSha256 = LongArithmeticCpuSourceHash, cpuGeneratorPatchSha256 = LongArithmeticCpuPatchHash, + adapterAssemblySha256 = Hash(typeof(M68kWinUaeCpuTesterConformanceTests).Assembly.Location), + cpuAssemblySha256 = Hash(typeof(M68kCoreFactory).Assembly.Location), + selectedModels = models.Select(m => m.Id), + excludedProfiles = new[] { new { model = "68000", reason = "Long multiply/divide is architecturally unavailable; synthetic instruction/exception coverage is retained." }, new { model = "68010", reason = "Long multiply/divide is architecturally unavailable; synthetic instruction/exception coverage is retained." } }, + excludedEncodings = new[] { "Nonzero reserved extension fields (bits 15 and 9..3): not qualified as architectural arithmetic by this preset.", "64-bit multiply with Dh == Dl: result architecturally undefined; the generator selects distinct registers before reference execution." }, + passing = rows.Count(r => r.Status == "passing"), mismatching = rows.Count(r => r.Status == "mismatching"), + unsupported = rows.Count(r => r.Status == "unsupported"), untested = rows.Count(r => r.Status == "untested"), + executedCases = rows.Sum(r => r.ExecutedCases), exceptionFrames = rows.Sum(r => r.ExceptionFrames), + maskedSrCases = rows.Sum(r => r.MaskedSrCases), architecturalForms = rows.Sum(r => r.Forms.Count), + qualification = "MULL.L/DIVL.L on EC020/A1200/020/030/040/060; extension reserved fields zeroed before reference execution; undefined 64-bit multiply Dh==Dl replaced by a distinct Dh. DIVL aliases retained. Both signs and 32/64-bit forms remain selected, including 060 architectural unimplemented-integer exceptions. MC68060UM C.2.2/8.2.4 qualifies a separate 060 early vector-61 path with the causing instruction PC, before operand EA effects; 8.3 group-3 traps occur after instruction execution, interpreted with PRM 2.2.4/5 to preserve divide-by-zero postincrement/predecrement. No bridge input normalization, family exclusion or changed flag mask. Patched software reference, not unchanged Basic or hardware/timing qualification. Basic CCR 0/31; full addressing extensions enabled, no incoming trace/bus faults.", + rows + }, new JsonSerializerOptions { WriteIndented = true })); + Assert.True(failures.Count == 0, string.Join(Environment.NewLine, failures)); + } + + private static (int Cases, uint Frames, uint Masked, int Forms) LongArithmeticCounts(string model, string family) => + (FixtureId(model), family) switch + { + ("68EC020", "DIVL.L") => (2422, 132, 686, 1054), + ("68EC020", "MULL.L") => (2456, 0, 0, 1077), + ("68060", "DIVL.L") => (3178, 2068, 94, 1422), + ("68060", "MULL.L") => (3226, 2018, 0, 1411), + ("68020" or "68030" or "68040", "DIVL.L") => (2040, 214, 584, 881), + ("68020" or "68030" or "68040", "MULL.L") => (2046, 0, 0, 894), + _ => throw new XunitException($"Unqualified LongArithmetic family: {model}/{family}") + }; + + private sealed record LongArithmeticRow(string Model, string Family, string Status, int ExecutedCases, + uint ExceptionFrames, uint MaskedSrCases, bool Controls, int RegisterControlCases, int SrControlCases, + bool FrameControlRequired, bool FrameDetected, int FrameControlCases, bool IgnoredControlRequired, + bool IgnoredAccepted, int IgnoredControlCases, SortedDictionary Forms, string Detail); + + internal static WinUaeManifest ValidateLongArithmeticIdentity(string root, string library) + { + var json = File.ReadAllText(Path.Combine(root, "manifest.json")); + var manifest = JsonSerializer.Deserialize(json) ?? throw new XunitException("Missing LongArithmetic manifest."); + using var document = JsonDocument.Parse(json); + var fields = document.RootElement; + var source = Path.Combine(root, "cputest-long-arithmetic.cpp"); + var normalizedHash = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData( + System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(source).Replace("\r\n", "\n")))).ToLowerInvariant(); + var cpuSource = Path.Combine(root, "gencpu-long-arithmetic.cpp"); + var cpuNormalizedHash = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData( + System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(cpuSource).Replace("\r\n", "\n")))).ToLowerInvariant(); + if (manifest.Schema != 1 || manifest.GeneratorCommit != GeneratorPin || manifest.RunnerCommit != RunnerPin || + fields.GetProperty("Preset").GetString() != "LongArithmetic" || normalizedHash != LongArithmeticSourceHash || + Hash(source) != fields.GetProperty("LongArithmeticSourceSha256").GetString() || + fields.GetProperty("LongArithmeticPatchSha256").GetString() != LongArithmeticPatchHash || + Hash(Path.Combine(root, "long-arithmetic-encodings.patch")) != LongArithmeticPatchHash || + cpuNormalizedHash != LongArithmeticCpuSourceHash || + Hash(cpuSource) != fields.GetProperty("LongArithmeticCpuSourceSha256").GetString() || + fields.GetProperty("LongArithmeticCpuPatchSha256").GetString() != LongArithmeticCpuPatchHash || + Hash(Path.Combine(root, "long-arithmetic-unimplemented.patch")) != LongArithmeticCpuPatchHash || + Hash(Path.Combine(root, "cputester.exe")) != fields.GetProperty("GeneratorExecutableSha256").GetString() || + Hash(library) != manifest.NativeLibrarySha256) + throw new XunitException("LongArithmetic source, patch, generator or native identity is unqualified."); + if (manifest.Profiles.Length != LongArithmeticProfiles.Length || + !manifest.Profiles.Select(p => p.Id).Order().SequenceEqual(LongArithmeticProfiles.Order())) + throw new XunitException("LongArithmetic requires its complete profile selection without empty or duplicate selections."); + return manifest; + } + + // Independent fixture encoding check: M68000PM 4-94/98/136/140. It must + // reject unqualified input rather than reinterpret it or calculate CPU results. + internal static string QualifyLongArithmeticEncoding(ushort opcode, ushort extension, string family) + { + var expected = family switch { "MULL.L" => 0x4c00, "DIVL.L" => 0x4c40, _ => -1 }; + var mode = (opcode >> 3) & 7; + var register = opcode & 7; + if (expected < 0 || (opcode & 0xffc0) != expected || mode == 1 || (mode == 7 && register > 4) || + (extension & 0x83f8) != 0 || + (family == "MULL.L" && (extension & 0x0400) != 0 && ((extension >> 12) & 7) == (extension & 7))) + throw new XunitException($"Unqualified long-arithmetic encoding: {family}/{opcode:X4}/{extension:X4}"); + return $"{((extension & 0x0800) != 0 ? "signed" : "unsigned")}/{((extension & 0x0400) != 0 ? "64" : "32")}/ea={mode}:{register}/primary={(extension >> 12) & 7}/secondary={extension & 7}"; + } +} + +public sealed class M68kWinUaeLongArithmeticEncodingTests +{ + [Theory] + [InlineData(0x4c00, 0x0000, "MULL.L", "unsigned/32/ea=0:0/primary=0/secondary=0")] + [InlineData(0x4c39, 0x7c06, "MULL.L", "signed/64/ea=7:1/primary=7/secondary=6")] + [InlineData(0x4c7b, 0x4404, "DIVL.L", "unsigned/64/ea=7:3/primary=4/secondary=4")] + [InlineData(0x4c7c, 0x3803, "DIVL.L", "signed/32/ea=7:4/primary=3/secondary=3")] + public void FixedLegalEncodingsRemainDistinct(int opcode, int extension, string family, string expected) => + Assert.Equal(expected, M68kWinUaeCpuTesterConformanceTests.QualifyLongArithmeticEncoding((ushort)opcode, (ushort)extension, family)); + + [Theory] + [InlineData(0x8000)] [InlineData(0x0200)] [InlineData(0x0100)] [InlineData(0x0080)] + [InlineData(0x0040)] [InlineData(0x0020)] [InlineData(0x0010)] [InlineData(0x0008)] + public void EveryReservedExtensionFieldIsRejected(int bit) + { + foreach (var (opcode, family) in new[] { (0x4c00, "MULL.L"), (0x4c40, "DIVL.L") }) + Assert.Throws(() => M68kWinUaeCpuTesterConformanceTests.QualifyLongArithmeticEncoding((ushort)opcode, (ushort)bit, family)); + } + + [Theory] + [InlineData(0x4c00, 0x0400, "MULL.L")] + [InlineData(0x4c39, 0x7c07, "MULL.L")] + [InlineData(0x4c08, 0x0000, "MULL.L")] + [InlineData(0x4c7d, 0x0000, "DIVL.L")] + [InlineData(0x4c00, 0x0000, "DIVL.L")] + [InlineData(0x4c40, 0x0000, "MULL.L")] + public void UndefinedAliasesAndInvalidWordsAreRejected(int opcode, int extension, string family) => + Assert.Throws(() => M68kWinUaeCpuTesterConformanceTests.QualifyLongArithmeticEncoding((ushort)opcode, (ushort)extension, family)); +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 33807a3b..969baf66 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -34,17 +34,22 @@ inputs. SHA-256 identities are written to `musashi-inputs.json` and the per-prog | Profile | Passing programs | Explicit exclusions | | --- | ---: | ---: | -| 68000 | 56 | 22 | -| 68010 | 56 | 22 | +| 68000 | 55 | 23 | +| 68010 | 55 | 23 | | 68EC020 | 72 | 6 | | 68020 | 72 | 6 | | 68030 | 72 | 6 | | 68040 | 72 | 6 | | 68060 | 66 | 12 | | A1200 EC020 | 72 | 6 | -| Total | 538 | 86 | +| Total | 536 | 88 | Four mc68000 fixtures retain the previous invalid-BCD/undefined-DIV-flags caveats. +000/010 additionally exclude `mc68000/move.bin`: despite its directory, offset +0x160 encodes 020-only PC-relative CMPI.B (`0C3A`), with no compatible +unavailable-instruction handler. The current synthetic CMPI and unavailable +instruction coverage is retained. These are the current audit counts; earlier +dated checkpoint counts below remain historical evidence. 000/010 exclude the advanced directory because its programs assume 020+ instructions without unavailable-instruction handlers. All advanced profiles exclude `cmp2.bin`'s conflicting carry assertion and `chk2.bin`: its final @@ -2230,3 +2235,144 @@ package ZIP entry, and all four assets resolve exactly .53. Evidence: `artifacts/fpu-types-production.binlog` and `artifacts/fpu-types-identities.json`. Unrelated CopperScreen changes remain untouched; no media/artifacts or package publication is included in this source checkpoint. + +### Long arithmetic reference qualification (2026-10-05) + +The eight Basic DIVL.L/MULL.L unsupported groups on EC020/A1200/020/030 +first fail on extensions such as `0031`, `0084` and `5B2B`. These contain +reserved fields, not legal long-arithmetic combinations. The pinned WinUAE +`cputest.cpp`, `handle_specials_extra`, already clears mask `83F8` on 040/060 +but leaves it random on 020/030. [M68000PM 4-94/98/136/140](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) +assigns those fields zero; 4-136/140 explicitly labels Dh == Dl with a 64-bit +multiply result undefined. Matching that software output would not establish +documented legal execution. The original inputs and their failing results remain +retained; no CPU guard, comparison mask or Basic family selection is relaxed. + +The separate `LongArithmetic` preset patches copies of the pinned sources. Its +encoding-selection patch clears the reserved fields on all applicable models +and selects a distinct Dh for the undefined 64-bit multiply alias, before +computing the expected reference result. Divide-register aliases remain legal +and selected. Fixed examples validate both families' encodings; each of the +eight reserved fields, undefined multiply aliases, invalid EAs and wrong-family +words is independently rejected. All 18 new encoding checks pass in ordinary +CI. Every native callback is additionally checked without input normalization. +000/010 long arithmetic remains architecturally unavailable and covered by the +synthetic suite, not this advanced-model reference preset. + +The first generated run passes ten groups but fails both 060 groups: vector 61 +saves opcode PC + 4 in WinUAE, while [MC68060UM 8.2.4/C.2.2](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf) +requires the causing instruction PC. The source emits operand reads and PC +synchronization before `m68k_mull`/`m68k_divl` detect the unavailable operation. +Evidence: `artifacts/m6-long-arithmetic-first/`; ten passing, two mismatching, +zero unsupported groups. This exploratory run predates the final two-source +identity gate and is failed-before diagnostic evidence, not qualified success. + +After early unimplemented detection, 060 MULL.L passes 3,226 callbacks / 2,018 +frames; DIVL.L reaches callback 2,319. `4C5F 0002`, a 32-bit divide through +`(A7)+` by zero, retains its four-byte increment in Copper68k but the generator +undoes it. `needmmufixup` is commented as undoing unavailable-instruction effects; +the generated false-result branch also applies it to a divide-by-zero exception. +The second failed run is retained in +`artifacts/m6-long-arithmetic-qualified-first/`, with eleven passing groups. + +The final CPU-generation patch separates these paths. Unavailable 060 64-bit +operations enter vector 61 before EA reads/effects, with the causing PC. +Ordinary divide-by-zero retains EA effects. The latter is an explicitly stated +interpretation of MC68060UM 8.3, which places divide-by-zero in group 3 after +instruction execution, together with PRM 2.2.4/5 increment/decrement semantics. +It is also consistent with the source's stated rollback purpose. Hardware +corroboration is not claimed. No production CPU, timing, arithmetic or flag-mask +change is required. Additional 060 candidates become valid reference exceptions +when the generator no longer attempts unavailable operands; they are counted, +not silently dropped. + +Final pinned identities: + +| Artifact | SHA-256 | +| --- | --- | +| Normalized `cputest-long-arithmetic.cpp` | `0012b9bd9cd2a172c4c321a8cf8e9f1095d32f824286518e61c9d4e75bc5eb43` | +| `long-arithmetic-encodings.patch` | `44e8dacde5a11e9cbd2e15119362a09b35c2e109fbbdbecbefa92929e89b32ea` | +| Normalized `gencpu-long-arithmetic.cpp` | `f1ec43c3658de9ae99e6e60f8376979f562c3b55d338a92e5d4f4849434b0799` | +| `long-arithmetic-unimplemented.patch` | `75a9e8d7ce2f534d2b16910dffe436e9d8396b3f470628bc59de9852898e0733` | +| Final input manifest | `f3340970bb5f014c672ddba27e26f93d434f74270c4efd1ec38ca5b1ab330247` | +| Final native DLL | `4ae1547993a1c358582bea4ed529bc7455977cb508d66a46a7e9580ca5a679cb` | +| Final generator executable | `78c8790ba4c9e168320f030d67724fec45c2756f5adcf416dae2cd8616fca47a` | + +Generator/runner commits retain `025b999239800357e95065fe5b9a15ea5b300fa7` +and `7a83745d6c6159bc74ab0471578ffc8bc244e66e`. Source copies, actual +source hashes, patches, compiler identity, all input hashes and executable/DLL +identities are in `artifacts/m6-long-arithmetic-final-inputs/manifest.json`. +Generation uses the pinned xorshift seed initialized to 1 and one Basic-style +CCR 0/31 round; full EA extensions are enabled. Other seeds, incoming trace, +fault rounds, physical MMU/cache/pipeline and timing remain unqualified. + +Required per-profile callback/frame/masked-SR/form counts: + +| Profile | DIVL.L | MULL.L | +| --- | --- | --- | +| EC020 and A1200, each | 2,422 / 132 / 686 / 1,054 | 2,456 / 0 / 0 / 1,077 | +| 020, 030 and 040, each | 2,040 / 214 / 584 / 881 | 2,046 / 0 / 0 / 894 | +| 060 | 3,178 / 2,068 / 94 / 1,422 | 3,226 / 2,018 / 0 / 1,411 | +| Total | 14,142 / 2,974 / 3,218 / 6,173 | 14,276 / 2,018 / 0 / 6,247 | + +All twelve groups pass, with **28,418 callbacks, 4,992 frames and 12,420 +architectural forms**. Each form identifies model/family, sign, 32/64-bit +selection, source EA fields and both destination register fields. All four +sign/width categories are mandatory; this sample does not imply exhaustive +external coverage of every addressing extension/value. Register and defined-X +corruption controls are required on all groups, frame corruption on seven +exception-bearing groups, and undefined-flag-only acceptance on all six DIVL +groups: **37 controls**. They pass in +`artifacts/m6-long-arithmetic-final-controls/`. + +Eight copied-input preflight controls reject missing manifest/memory, +empty families/profiles, changed data, duplicate profiles and unqualified +source/patch identities before native execution. Evidence: +`artifacts/m6-long-arithmetic-preflight-controls/controls.json`. A production +saved-PC mutation independently fails both 060 reference groups and 8,972 +boundary / 2,904 addressing synthetic cases. A 2,045-versus-2,046 cardinality +mutation fails all three 020/030/040 MULL groups, despite the native comparison +passing. Evidence: `artifacts/m6-long-arithmetic-mutation-controls/`. All source +mutations are restored before final validation. No specialized regression is +retired. + +The reproducible preparation/audit commands and environment contract are in +[the adapter guide](../Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md#qualified-long-multiplydivide-preset). +The new audit fails missing/empty/unqualified selections, unsupported legal +execution, mismatches and incomplete exact counts. The original Basic audit's +eight reserved-extension unsupported groups remain visible; the new separate +scope qualifies documented long-arithmetic inputs without making that broad +audit green. All earlier advanced restoration, other reference disagreements +and consolidation requirements remain open. Milestone 6 remains in progress. + +Final Release validation passes **4,901 tests with ten optional skips**, zero +failures, in `artifacts/m6-long-arithmetic-full/`, including BKPT, trap/bounds +and LongArithmetic. Their exact counts and comparator controls pass. The strict +gate retains **14,646,552 logical cases / 574 batches**, `roadmapComplete=false`, +and freshly passes pinned SingleStepTests (312,500 cases / 125 files) and Musashi +(536 programs / 88 exclusions). The opening Musashi table is reconciled to those +current counts; earlier dated records remain unchanged. Separate qualified TRAP +trace passes 512 callbacks / 512 frames / six controls in +`artifacts/m6-long-arithmetic-trace/`. The gate log is +`artifacts/m6-long-arithmetic-gate.log`. + +The unchanged broad Basic audit still fails at exactly 1,326 passing, 47 +mismatching, eight unsupported and zero untested groups: 11,474,194 callbacks, +1,663,891 frames, 199,327 masked-SR cases, one terminal callback and all 32 +controls. The original manifest/native identities remain unchanged. Evidence: +`artifacts/m6-long-arithmetic-broad/`. This preserves the 040/060 F400 disagreement +and the earlier advanced restoration/system/reference requirements. + +`artifacts/m6-long-arithmetic-identities.json` records test/script/assembly hashes +against baseline `2787ebf`. The CPU source tree is unchanged: +`12753a6d2bb677f36cae03f3193f370828f50a66`. The default CPU assembly is +`73af4f3eb4ef2e218b89f88b55b8a56f2d759f4d567cdb1bbd9a99165a6c8fce`; +the adapter is +`281ac75e9761ea5d23899d8ffb2a41eab2f11c2c78f96e71e222316f5b8b804e`. +The CPU's informational version now includes `2787ebf`, so its binary hash +differs from the prior checkpoint's pre-commit build despite unchanged CPU +source. All final source/assembly identities remain stable after the gate. +Pinned tracked WinUAE/runner sources are clean. No production CPU, package or +consumer change is made in this follow-up; the prior isolated .53 consumer +evidence remains separate. No new package publication or host-performance +measurement is claimed. Unrelated root CopperScreen changes remain untouched. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index cae72838..4ed5174f 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1083,3 +1083,32 @@ TRAP trace and AHX pass. Private unpublished .53 passes the isolated CopperScree Release build, host/disk/engine tests and all three native Workbench/A1200 replays, with exact package and loaded-assembly identity checks. Timing policy is retained; `roadmapComplete=false` remains unchanged. + +The long-arithmetic follow-up qualifies the reserved-extension failures in the +Basic reference. A separate pinned `LongArithmetic` preset selects documented +MULL.L/DIVL.L encodings before generating expected results; undefined 64-bit +multiply register aliases are explicitly excluded while legal divide aliases +remain. Two copied source patches also qualify 060 vector-61 opcode PCs before +EA effects and preserve completed divide-by-zero EA updates. The latter is a +documented interpretation of the manual's group-3 completion rule, not hardware +qualification. No production CPU or comparison-mask change is made. + +The complete advanced-model preset passes twelve groups: 28,418 callbacks, +4,992 frames and 12,420 model/family/sign/width/EA/register forms. Both signs +and 32/64-bit selections, including 060 architectural exceptions, are required. +All 37 corruption/undefined-flag controls pass; eight preflight defects fail. +Wrong-saved-PC and stale-cardinality mutations detect their intended defects; +sources are restored. Eighteen independent encoding checks join ordinary CI. +The original Basic corpus and its reserved-field failures remain intact. No +specialized test is retired; broader reference and restoration/consolidation +requirements keep milestone 6 **in progress**. + +Final validation passes 4,901 CPU tests with ten optional skips, including the +three qualified presets. The strict gate retains 14,646,552 logical cases in +574 batches with fresh pinned SingleStepTests/Musashi. Qualified TRAP trace +passes separately. The original Basic audit retains exactly its 1,326 passing, +47 mismatching and eight reserved-encoding unsupported groups; those failures +remain visible. CPU sources and timing policy are unchanged; no new package or +consumer modification is needed. Source/input/assembly identities, historical +failures and limitations are maintained in the reference record. The goal stays +active with `roadmapComplete=false`. diff --git a/scripts/prepare-copper68k-winuae.ps1 b/scripts/prepare-copper68k-winuae.ps1 index c5f23915..899bb7d6 100644 --- a/scripts/prepare-copper68k-winuae.ps1 +++ b/scripts/prepare-copper68k-winuae.ps1 @@ -4,7 +4,7 @@ param( [Parameter(Mandatory)] [string] $GeneratorSource, [Parameter(Mandatory)] [string] $RunnerSource, [Parameter(Mandatory)] [string] $VcVars64, - [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints')] [string] $Preset = 'Basic', + [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints','LongArithmetic')] [string] $Preset = 'Basic', [string] $OutputDirectory = 'artifacts/winuae-model-inputs' ) $ErrorActionPreference = 'Stop' @@ -48,17 +48,17 @@ try { & ./build68k.exe table68k | Set-Content -Encoding utf8 cpudefs.cpp if ($LASTEXITCODE -ne 0) { throw 'Opcode table generation failed' } $cpuGeneratorSource = 'gencpu.cpp' - if ($Preset -in @('TrapBounds','Breakpoints')) { + if ($Preset -in @('TrapBounds','Breakpoints','LongArithmetic')) { # TrapBounds: following PC (MC68020UM 6.1.4 / MC68040UM 8.2.3). # Breakpoints: opcode PC (illegal exception, UM 6.1.5 / 8.2.4). # Patch a copy; Basic keeps the pinned source. - $sourceName = if ($Preset -eq 'TrapBounds') {'gencpu-trap-bounds.cpp'} else {'gencpu-breakpoints.cpp'} - $patchName = if ($Preset -eq 'TrapBounds') {'trap-bounds-pc.patch'} else {'breakpoint-pc.patch'} + $sourceName = switch ($Preset) {'TrapBounds' {'gencpu-trap-bounds.cpp'} 'Breakpoints' {'gencpu-breakpoints.cpp'} 'LongArithmetic' {'gencpu-long-arithmetic.cpp'}} + $patchName = switch ($Preset) {'TrapBounds' {'trap-bounds-pc.patch'} 'Breakpoints' {'breakpoint-pc.patch'} 'LongArithmetic' {'long-arithmetic-unimplemented.patch'}} $patchPath = Join-Path $PSScriptRoot ('winuae/' + $patchName) $patch = [IO.File]::ReadAllText($patchPath).Replace("`r`n", "`n") $source = [IO.File]::ReadAllText((Join-Path $generator 'gencpu.cpp')).Replace("`r`n", "`n") $hunks = @($patch -split "(?m)^@@`n" | Select-Object -Skip 1) - $requiredHunks = if ($Preset -eq 'TrapBounds') {2} else {1} + $requiredHunks = switch ($Preset) {'Breakpoints' {1} 'TrapBounds' {2} 'LongArithmetic' {3}} if ($hunks.Count -ne $requiredHunks) { throw "Incorrect $Preset patch hunk count" } foreach ($hunk in $hunks) { $lines = $hunk.TrimEnd("`n").Split("`n") @@ -86,6 +86,22 @@ try { [IO.File]::WriteAllText($testerSource, (Patch-Once ([IO.File]::ReadAllText((Join-Path $generator 'cputest.cpp'))) $before $after)) [IO.File]::WriteAllText((Join-Path $output 'trace-priority.patch'), ([IO.File]::ReadAllText($patchPath)).Replace("`r`n", "`n")) } + if ($Preset -eq 'LongArithmetic') { + # M68000PM 4-94/98/136/140: reserved extension fields are zero; + # Dh == Dl with a 64-bit multiply has undefined results. Select + # legal inputs before reference execution, never in the CPU bridge. + $patchName = 'long-arithmetic-encodings.patch' + $patch = [IO.File]::ReadAllText((Join-Path $PSScriptRoot ('winuae/' + $patchName))).Replace("`r`n", "`n") + $source = [IO.File]::ReadAllText((Join-Path $generator 'cputest.cpp')).Replace("`r`n", "`n") + $hunks = @($patch -split "(?m)^@@`n" | Select-Object -Skip 1) + if ($hunks.Count -ne 1) { throw 'Incorrect LongArithmetic patch hunk count' } + $lines = $hunks[0].TrimEnd("`n").Split("`n") + $before = (($lines | Where-Object { $_.StartsWith('-') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" + $after = (($lines | Where-Object { $_.StartsWith('+') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" + $testerSource = Join-Path $output 'cputest-long-arithmetic.cpp' + [IO.File]::WriteAllText($testerSource, (Patch-Once $source $before $after)) + [IO.File]::WriteAllText((Join-Path $output $patchName), $patch) + } $cpp = @('cpudefs.cpp','cpuemu_90_test.cpp','cpuemu_91_test.cpp','cpuemu_92_test.cpp','cpuemu_93_test.cpp','cpuemu_94_test.cpp','cpuemu_95_test.cpp','cputbl_test.cpp',$testerSource,'cputest_support.cpp','disasm.cpp','fpp.cpp','fpp_softfloat.cpp','ini.cpp','newcpu_common.cpp','readcpu.cpp','softfloat/softfloat.cpp','softfloat/softfloat_decimal.cpp','softfloat/softfloat_fpsp.cpp') Run-Compiler (@('/nologo','/O2','/EHsc','/w','/I.','/Iinclude','/Icputest','/I../zlib','/DCPUEMU_90','/DCPUEMU_91','/DCPUEMU_92','/DCPUEMU_93','/DCPUEMU_94','/DCPUEMU_95','/DCPU_TESTER') + $cpp + @('/Fe:cputester.exe')) (Join-Path $output 'cputester-build.log') if ($Preset -ne 'Basic') { Copy-Item -LiteralPath 'cputester.exe' -Destination $output } @@ -139,6 +155,9 @@ void M68KTester_destroy(M68KTesterContext* context) { if ($Preset -eq 'Breakpoints') { $baseIni = $baseIni.Replace('[test=Basic]', '[test=Breakpoints]').Replace('mode=all', 'mode=BKPT') } + if ($Preset -eq 'LongArithmetic') { + $baseIni = $baseIni.Replace('[test=Basic]', '[test=LongArithmetic]').Replace('mode=all', 'mode=MULL.L,DIVL.L') + } foreach ($model in @( @{id='68000'; cpu='68000'; width=24}, @{id='68010'; cpu='68010'; width=24}, @{id='68EC020'; cpu='68020'; width=24}, @{id='68020'; cpu='68020'; width=32}, @@ -147,6 +166,7 @@ void M68KTester_destroy(M68KTesterContext* context) { if ($Preset -eq 'TraceTraps' -and $model.id -notin @('68040','68060')) { continue } if ($Preset -eq 'TrapBounds' -and $model.id -in @('68000','68010')) { continue } if ($Preset -eq 'Breakpoints' -and $model.id -eq '68000') { continue } + if ($Preset -eq 'LongArithmetic' -and $model.id -in @('68000','68010')) { continue } $profileRoot = Join-Path $output $model.id New-Item -ItemType Directory -Path $profileRoot | Out-Null # Every invocation uses a fresh generator output path; stale data cannot fill a gap. @@ -183,6 +203,10 @@ void M68KTester_destroy(M68KTesterContext* context) { TrapBoundsPatchSha256=$(if ($Preset -eq 'TrapBounds') {(Get-FileHash -LiteralPath (Join-Path $output 'trap-bounds-pc.patch')).Hash.ToLowerInvariant()} else {$null}) BreakpointSourceSha256=$(if ($Preset -eq 'Breakpoints') {(Get-FileHash -LiteralPath (Join-Path $output 'gencpu-breakpoints.cpp')).Hash.ToLowerInvariant()} else {$null}) BreakpointPatchSha256=$(if ($Preset -eq 'Breakpoints') {(Get-FileHash -LiteralPath (Join-Path $output 'breakpoint-pc.patch')).Hash.ToLowerInvariant()} else {$null}) + LongArithmeticSourceSha256=$(if ($Preset -eq 'LongArithmetic') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-long-arithmetic.cpp')).Hash.ToLowerInvariant()} else {$null}) + LongArithmeticPatchSha256=$(if ($Preset -eq 'LongArithmetic') {(Get-FileHash -LiteralPath (Join-Path $output 'long-arithmetic-encodings.patch')).Hash.ToLowerInvariant()} else {$null}) + LongArithmeticCpuSourceSha256=$(if ($Preset -eq 'LongArithmetic') {(Get-FileHash -LiteralPath (Join-Path $output 'gencpu-long-arithmetic.cpp')).Hash.ToLowerInvariant()} else {$null}) + LongArithmeticCpuPatchSha256=$(if ($Preset -eq 'LongArithmetic') {(Get-FileHash -LiteralPath (Join-Path $output 'long-arithmetic-unimplemented.patch')).Hash.ToLowerInvariant()} else {$null}) NativeLibrarySha256=(Get-FileHash -LiteralPath (Join-Path $output 'm68k_cpu_tester.dll') -Algorithm SHA256).Hash.ToLowerInvariant() Compiler=$compiler; GeneratorExecutableSha256=(Get-FileHash -LiteralPath (Join-Path $generator 'cputester.exe') -Algorithm SHA256).Hash.ToLowerInvariant() NativeSourceSha256=(Get-FileHash -LiteralPath (Join-Path $output 'winuae-native.c') -Algorithm SHA256).Hash.ToLowerInvariant() diff --git a/scripts/test-copper68k-winuae-long-arithmetic.ps1 b/scripts/test-copper68k-winuae-long-arithmetic.ps1 new file mode 100644 index 00000000..f25ffa80 --- /dev/null +++ b/scripts/test-copper68k-winuae-long-arithmetic.ps1 @@ -0,0 +1,36 @@ +#requires -Version 7.0 +[CmdletBinding()] +param( + [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $InputDirectory, + [string] $NativeLibrary, + [string] $OutputDirectory = 'artifacts/winuae-long-arithmetic-audit' +) +$ErrorActionPreference = 'Stop' +$repo = Split-Path -Parent $PSScriptRoot +$root = (Resolve-Path -LiteralPath $InputDirectory).Path +if (-not $NativeLibrary) { $NativeLibrary = Join-Path $root 'm68k_cpu_tester.dll' } +$library = (Resolve-Path -LiteralPath $NativeLibrary).Path +$output = [IO.Path]::GetFullPath($OutputDirectory, $repo) +if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } +New-Item -ItemType Directory -Path $output | Out-Null +$settings = @{ + COPPER68K_RUN_WINUAE_LONG_ARITHMETIC_AUDIT = '1' + COPPER68K_WINUAE_LONG_ARITHMETIC_PATH = $root + COPPER68K_WINUAE_LONG_ARITHMETIC_LIBRARY = $library + COPPER68K_SYNTHETIC_REPORT_DIR = $output +} +$saved = @{} +try { + foreach ($name in $settings.Keys) { + $saved[$name] = [Environment]::GetEnvironmentVariable($name) + [Environment]::SetEnvironmentVariable($name, $settings[$name]) + } + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~WinUaeLongArithmeticAcrossAdvancedModelsWhenEnabled|FullyQualifiedName~M68kWinUaeLongArithmeticEncodingTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output + if ($LASTEXITCODE -ne 0) { throw 'Qualified LongArithmetic audit failed' } + [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw + $counts = $trx.TestRun.ResultSummary.Counters + if ([int]$counts.executed -ne 19 -or [int]$counts.passed -ne 19) { throw 'LongArithmetic audit did not execute its complete test selection' } + if (-not (Test-Path -LiteralPath (Join-Path $output 'winuae-long-arithmetic-audit.json'))) { throw 'LongArithmetic coverage report is missing' } +} finally { + foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } +} diff --git a/scripts/winuae/long-arithmetic-encodings.patch b/scripts/winuae/long-arithmetic-encodings.patch new file mode 100644 index 00000000..a4929bd1 --- /dev/null +++ b/scripts/winuae/long-arithmetic-encodings.patch @@ -0,0 +1,28 @@ +--- a/gencpu/cputest.cpp ++++ b/gencpu/cputest.cpp +@@ + case i_DIVL: + case i_MULL: + { +- if (cpu_lvl >= 4) { +- // same as BF instructions but also other bits need clearing +- // or results are unexplained.. +- uae_u16 extra = get_word_test(opcode_memory_start + 2); +- if (extra & 0x83f8) { +- extra &= ~0x83f8; +- put_word_test(opcode_memory_start + 2, extra); +- } +- } ++ // M68000PM 4-94/98/136/140: these extension fields are zero. ++ // Qualify legal encodings on every model before reference execution. ++ uae_u16 extra = get_word_test(opcode_memory_start + 2); ++ extra &= ~0x83f8; ++ // A 64-bit multiply with Dh == Dl has undefined results (4-136/140). ++ // Select a distinct Dh while retaining all other generated fields. ++ if (dp->mnemo == i_MULL && (extra & 0x0400) && ++ ((extra >> 12) & 7) == (extra & 7)) { ++ extra ^= 1; ++ } ++ put_word_test(opcode_memory_start + 2, extra); + break; + } diff --git a/scripts/winuae/long-arithmetic-unimplemented.patch b/scripts/winuae/long-arithmetic-unimplemented.patch new file mode 100644 index 00000000..d32e203d --- /dev/null +++ b/scripts/winuae/long-arithmetic-unimplemented.patch @@ -0,0 +1,37 @@ +--- a/gencpu/gencpu.cpp ++++ b/gencpu/gencpu.cpp +@@ + case i_MULL: + case i_DIVL: ++ // The 060 unimplemented form now traps before any EA effects. ++ // A real divide-by-zero is a completed group-3 instruction trap ++ // (MC68060UM 8.3); do not undo its postincrement/predecrement. ++ if (cpu_level == 5) ++ return false; ++ return true; + case i_CAS: + return true; +@@ + case i_DIVL: + genamode(curi, curi->smode, "srcreg", curi->size, "extra", 1, 0, 0); ++ // MC68060UM C.2.2: vector 61 saves the causing instruction PC. ++ // Detect the unavailable form before EA reads or register effects. ++ if (cpu_level == 5) { ++ out("if (extra & 0x0400) {\n"); ++ out("op_unimpl(opcode);\n"); ++ write_return_cycles(0); ++ out("}\n"); ++ } + genamode(curi, curi->dmode, "dstreg", curi->size, "dst", 1, 0, 0); +@@ + case i_MULL: + genamode(curi, curi->smode, "srcreg", curi->size, "extra", 1, 0, 0); ++ // MC68060UM C.2.2: vector 61 saves the causing instruction PC. ++ // Detect the unavailable form before EA reads or register effects. ++ if (cpu_level == 5) { ++ out("if (extra & 0x0400) {\n"); ++ out("op_unimpl(opcode);\n"); ++ write_return_cycles(0); ++ out("}\n"); ++ } + genamode(curi, curi->dmode, "dstreg", curi->size, "dst", 1, 0, 0); From 7845c5ee06db83885c3f69cd1f0951c9e4e77090 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 07:11:15 +0300 Subject: [PATCH 029/211] Qualify word division flags across all CPU models --- .../M68kWinUaeCpuTesterConformanceTests.cs | 13 +- .../M68kWinUaeCpuTesterConformanceTests.md | 44 ++++++ .../M68kWinUaeTrapBoundsAuditTests.cs | 35 +++-- .../M68kWinUaeWordDivisionAuditTests.cs | 70 +++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 133 +++++++++++++++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 33 +++++ scripts/prepare-copper68k-winuae.ps1 | 15 +- ...-copper68k-winuae-qualified-exceptions.ps1 | 18 ++- scripts/winuae/word-division-carry.patch | 8 ++ 9 files changed, 347 insertions(+), 22 deletions(-) create mode 100644 Copper68k.Tests/M68kWinUaeWordDivisionAuditTests.cs create mode 100644 scripts/winuae/word-division-carry.patch diff --git a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs index c6da77fe..2dbf8e42 100644 --- a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs +++ b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs @@ -313,6 +313,8 @@ private sealed class NativeTester : IDisposable private string _integerFamily = ""; private bool _qualifyLongArithmetic; private readonly Dictionary _longArithmeticForms = new(StringComparer.Ordinal); + private Func? _fixtureClassifier; + private readonly Dictionary _fixtureForms = new(StringComparer.Ordinal); private NativeTester(IntPtr library) { @@ -357,7 +359,8 @@ public RunResult Run( bool corruptFrame = false, ushort corruptSr = 0, bool corruptIgnoredSr = false, - bool qualifyLongArithmetic = false) + bool qualifyLongArithmetic = false, + Func? fixtureClassifier = null) { _callbackException = null; _executedCases = 0; @@ -375,6 +378,8 @@ public RunResult Run( _integerFamily = opcode; _qualifyLongArithmetic = qualifyLongArithmetic; _longArithmeticForms.Clear(); + _fixtureClassifier = fixtureClassifier; + _fixtureForms.Clear(); if (integerProfile is not null && (_destroy is null || _addressingMask is null || _lastOutput is null)) throw new XunitException("Multi-model integer audit requires the qualified native bridge exports (destroy, addressing mask and diagnostics)."); if (integerProfile is not null && (_definedSr is null || _frameChecks is null || _maskedCases is null)) @@ -460,6 +465,7 @@ public readonly record struct RunResult( public int TraceInputCases => _traceInputCases; public bool UnsupportedExecution => _callbackException?.InnerException is UnsupportedM68kTimingException; public IReadOnlyDictionary LongArithmeticForms => _longArithmeticForms; + public IReadOnlyDictionary FixtureForms => _fixtureForms; public void Dispose() { @@ -496,6 +502,11 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt bus.ReadHostWord(registers.Pc + 2), _integerFamily); _longArithmeticForms[form] = _longArithmeticForms.GetValueOrDefault(form) + 1; } + if (_fixtureClassifier is not null) + { + var form = _fixtureClassifier(bus.ReadHostWord(registers.Pc), (ushort)registers.Sr); + _fixtureForms[form] = _fixtureForms.GetValueOrDefault(form) + 1; + } bus.CopyStackImage(registers.Regs[15], registers.Ssp, 0x20); ApplyRegisters(cpu, registers, _cpuLevel); diff --git a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md index b22efd22..2c1fda78 100644 --- a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md +++ b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.md @@ -323,3 +323,47 @@ encoding checks; this external audit remains opt-in. No new seeded audit, trace/bus-fault/MMU/cache/pipeline qualification or old regression retirement is claimed. Full current evidence and remaining milestone requirements are in [reference qualification](../docs/COPPER68K_REFERENCE_QUALIFICATION.md#long-arithmetic-reference-qualification-2026-10-05). + +## Qualified word division preset + +`WordDivision` qualifies DIVS.W and DIVU.W on all seven CPU models and the +A1200 profile. [M68000PM 4-92/96](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) +defines carry as cleared, including overflow and divide-by-zero, while X is +preserved. N/Z are undefined on overflow; N/Z/V are undefined on divide-by-zero. +The pinned generator's `setdivuflags` helper omits the carry clear on 020/030, +despite its own comment specifying C=0. A separate +[generator patch](../scripts/winuae/word-division-carry.patch) explicitly clears +carry after unsigned word overflow. Copper68k already implements this behavior. +No CPU result normalization, flag-mask change or physical hardware claim is made. + +```powershell +./scripts/prepare-copper68k-winuae.ps1 ` + -GeneratorSource artifacts/reference-winuae-api ` + -RunnerSource artifacts/reference-copperline ` + -VcVars64 '/VC/Auxiliary/Build/vcvars64.bat' ` + -Preset WordDivision -OutputDirectory artifacts/winuae-word-division-inputs +./scripts/test-copper68k-winuae-qualified-exceptions.ps1 ` + -Preset WordDivision -InputDirectory artifacts/winuae-word-division-inputs ` + -OutputDirectory artifacts/winuae-word-division-report +``` + +The command requires the qualified source/patch, generator/native identities, +complete profiles/families and exact input hashes. All 134,928 callbacks, 37,804 +frames, 87,936 masked-CCR cases and 11,392 model/family/EA/register/input-SR +combinations are pinned. Immutable raw opcode/SR classification rejects invalid +words or profile states and never calls production helpers. Fourteen fixed +encoding/profile checks run in ordinary CI. Every external group must reject +register, X, carry and frame corruption and accept changes confined to undefined +flags: 80 controls in total. A wrong callback count also fails the gate. + +`winuae-word-division-audit.json` reports passing, mismatching, unsupported and +untested scope separately, and records architectural forms, source/input/assembly +identities and controls. The command requires all fifteen selected xUnit tests +to execute successfully. The shared qualified audit runner retains the existing +TrapBounds and Breakpoints scopes. Full indexing is enabled in generation, but +this preset does not prove every indexed structural combination or operand +boundary; the synthetic matrices retain that coverage. Incoming trace, bus +faults, physical timing and enabled MMU remain outside this preset. Original +Basic inputs and their failing discovery report remain separate. See the +[qualification record](../docs/COPPER68K_REFERENCE_QUALIFICATION.md#word-division-reference-qualification-2026-10-05) +for mutation/rejection evidence and remaining milestone requirements. diff --git a/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs b/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs index 311de1cb..5844f9c0 100644 --- a/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs +++ b/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs @@ -70,20 +70,32 @@ private void RunQualifiedExceptionPreset(QualifiedExceptionPreset preset) var detected = !register.Passed && register.ExecutedCases > 0 && !sr.Passed && sr.ExecutedCases > 0 && sr.Detail.Contains("SR:", StringComparison.Ordinal) && !frame.Passed && tester.FrameChecks > 0 && frame.Detail.Contains("frame byte", StringComparison.Ordinal); - probes.Add(new(model.Id, family, detected, register.ExecutedCases, sr.ExecutedCases, frame.ExecutedCases)); - var result = tester.Run(path, family, fixture.CpuLevel, false, false, model); + var carry = preset.ArithmeticFlagControls ? tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptSr: 1) : default; + var carryDetected = !preset.ArithmeticFlagControls || (!carry.Passed && carry.ExecutedCases > 0 && carry.Detail.Contains("SR:", StringComparison.Ordinal)); + var ignored = preset.ArithmeticFlagControls ? tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptIgnoredSr: true) : default; + var ignoredAccepted = !preset.ArithmeticFlagControls || (ignored.Passed && ignored.ExecutedCases > 0 && tester.MaskedCases > 0); + detected &= carryDetected && ignoredAccepted; + probes.Add(new(model.Id, family, detected, register.ExecutedCases, sr.ExecutedCases, frame.ExecutedCases, + preset.ArithmeticFlagControls, carryDetected, carry.ExecutedCases, ignoredAccepted, ignored.ExecutedCases)); + var result = tester.Run(path, family, fixture.CpuLevel, false, false, model, + fixtureClassifier: preset.ClassifyForm is null ? null : (opcode, inputSr) => preset.ClassifyForm(opcode, inputSr, family)); var expected = preset.Counts(model, family); + var forms = new SortedDictionary(tester.FixtureForms.ToDictionary(x => x.Key, x => x.Value), StringComparer.Ordinal); + var expectedForms = preset.FormCounts?.Invoke(model, family) ?? 0; + var expectedMasked = preset.MaskedCounts?.Invoke(model, family) ?? (family.StartsWith("CHK2.", StringComparison.Ordinal) ? (uint)expected.Cases : 0); var passing = detected && result.Passed && result.ExecutedCases == expected.Cases && - tester.FrameChecks == expected.Frames && tester.MaskedCases == (family.StartsWith("CHK2.", StringComparison.Ordinal) ? expected.Cases : 0); - rows.Add(new(model.Id, family, passing ? "passing" : tester.UnsupportedExecution ? "unsupported" : "mismatching", - result.ExecutedCases, tester.FrameChecks, tester.MaskedCases, detected, result.Detail)); + tester.FrameChecks == expected.Frames && tester.MaskedCases == expectedMasked && + forms.Count == expectedForms && (preset.ClassifyForm is null || forms.Values.Sum() == result.ExecutedCases); + var detail = $"{result.Detail} Expected/actual callbacks={expected.Cases}/{result.ExecutedCases}, frames={expected.Frames}/{tester.FrameChecks}, masked={expectedMasked}/{tester.MaskedCases}, forms={expectedForms}/{forms.Count}."; + rows.Add(new(model.Id, family, passing ? "passing" : tester.UnsupportedExecution ? "unsupported" : result.ExecutedCases == 0 ? "untested" : "mismatching", + result.ExecutedCases, tester.FrameChecks, tester.MaskedCases, detected, forms, detail)); _output.WriteLine($"{model.Id}/{family}: {result.ExecutedCases} callbacks, {tester.FrameChecks} frames; controls={detected}."); } } Directory.CreateDirectory(output); File.WriteAllText(Path.Combine(output, preset.ReportName), JsonSerializer.Serialize(new { - schema = 1, reference = $"WinUAE {preset.Name} preset with explicit Motorola-qualified saved-PC correction", + schema = 1, reference = $"WinUAE {preset.Name} preset with explicit Motorola-qualified reference correction", manifest.GeneratorCommit, manifest.RunnerCommit, manifest.NativeLibrarySha256, manifestSha256 = Hash(manifestFile), generatorSourceSha256 = Hash(sourcePath), generatorNormalizedSourceSha256 = preset.SourceHash, generatorPatchSha256 = preset.PatchHash, @@ -92,7 +104,9 @@ private void RunQualifiedExceptionPreset(QualifiedExceptionPreset preset) qualification = preset.Qualification, passing = rows.Count(x => x.Status == "passing"), mismatching = rows.Count(x => x.Status == "mismatching"), unsupported = rows.Count(x => x.Status == "unsupported"), executedCases = rows.Sum(x => (long)x.ExecutedCases), + untested = rows.Count(x => x.Status == "untested"), exceptionFrames = rows.Sum(x => (long)x.ExceptionFrames), maskedSrCases = rows.Sum(x => (long)x.MaskedSrCases), + architecturalForms = rows.Sum(x => x.Forms.Count), probes, rows }, new JsonSerializerOptions { WriteIndented = true })); Assert.True(rows.All(x => x.Status == "passing"), string.Join(Environment.NewLine, @@ -115,9 +129,12 @@ private static (int Cases, uint Frames) TrapBoundsCounts(ModelSpec model, string }; private sealed record QualifiedExceptionPreset(string Name, string EnvironmentKey, string SourceName, string PatchName, string ManifestPrefix, string SourceHash, string PatchHash, string ReportName, string[] ProfileIds, string[] ModelIds, - Func Families, Func Counts, string Qualification); + Func Families, Func Counts, string Qualification, + bool ArithmeticFlagControls = false, Func? MaskedCounts = null, + Func? ClassifyForm = null, Func? FormCounts = null); private sealed record WinUaeQualifiedExceptionRow(string Model, string Family, string Status, int ExecutedCases, - uint ExceptionFrames, uint MaskedSrCases, bool Controls, string Detail); + uint ExceptionFrames, uint MaskedSrCases, bool Controls, SortedDictionary Forms, string Detail); private sealed record WinUaeQualifiedExceptionProbe(string Model, string Family, bool Detected, - int RegisterCases, int SrCases, int FrameCases); + int RegisterCases, int SrCases, int FrameCases, bool ArithmeticFlagControls, + bool CarryDetected, int CarryCases, bool IgnoredAccepted, int IgnoredCases); } diff --git a/Copper68k.Tests/M68kWinUaeWordDivisionAuditTests.cs b/Copper68k.Tests/M68kWinUaeWordDivisionAuditTests.cs new file mode 100644 index 00000000..d27f0e9d --- /dev/null +++ b/Copper68k.Tests/M68kWinUaeWordDivisionAuditTests.cs @@ -0,0 +1,70 @@ +using Copper68k.Tests.Synthetic; +using Xunit.Sdk; + +namespace Copper68k.Tests; + +public sealed partial class M68kWinUaeCpuTesterConformanceTests +{ + [EnvironmentFact("COPPER68K_RUN_WINUAE_WORD_DIVISION_AUDIT", "audit qualified WinUAE word division flags and exceptions")] + public void WinUaeWordDivisionAcrossSelectedModelsWhenEnabled() => RunQualifiedExceptionPreset(new( + "WordDivision", "WORD_DIVISION", "gencpu-word-division.cpp", "word-division-carry.patch", "WordDivision", + "254873bb8a1b59b081e98defa72fda572d5a2c93c33bb644c8ac07c4257e5c5e", "7eb90f09aaad56d164d0eafb9f66ddfc31becde9951a832820d5fdcc7973a83f", "winuae-word-division-audit.json", + ["68000", "68010", "68EC020", "68020", "68030", "68040", "68060"], + ["68000", "68010", "68EC020", "68020", "68030", "68040", "68060", "A1200"], + _ => ["DIVS.W", "DIVU.W"], (model, family) => { var counts = WordDivisionCounts(model, family); return (counts.Cases, counts.Frames); }, + "DIVS.W/DIVU.W across all selected profiles. M68000PM 4-92/96: carry is cleared, including overflow and divide-by-zero; X is preserved; N/Z undefined on overflow, N/Z/V undefined on divide-by-zero. A copied generator explicitly clears carry after DIVU overflow; the pinned helper omitted this on 020/030. Existing independently defined flag masks and CPU results are unchanged. Basic CCR 0/31, both stacks, full extensions enabled; no incoming trace/bus faults or physical timing. Patched software reference, not unchanged Basic or silicon qualification.", + ArithmeticFlagControls: true, MaskedCounts: (model, family) => WordDivisionCounts(model, family).Masked, + ClassifyForm: QualifyWordDivisionEncoding, FormCounts: (model, family) => WordDivisionCounts(model, family).Forms)); + + private static (int Cases, uint Frames, uint Masked, int Forms) WordDivisionCounts(ModelSpec model, string family) => + (FixtureId(model.Id), family) switch + { + ("68000", "DIVS.W") => (5738, 1590, 3530, 736), + ("68000", "DIVU.W") => (6166, 1838, 4158, 736), + ("68010", "DIVS.W") => (5782, 1632, 3836, 736), + ("68010", "DIVU.W") => (6058, 1792, 4032, 736), + ("68EC020", "DIVS.W") => (9690, 2734, 6074, 784), + ("68EC020", "DIVU.W") => (9670, 2670, 7132, 752), + ("68020" or "68030" or "68040" or "68060", "DIVS.W") => (9246, 2570, 5816, 688), + ("68020" or "68030" or "68040" or "68060", "DIVU.W") => (8870, 2466, 5676, 656), + _ => throw new XunitException($"Unqualified word-division family: {model.Id}/{family}") + }; + // M68000PM 4-93/97: fixed first-word encoding and legal data sources. + // This classifies immutable input; it never decodes production EAs or + // calculates an expected arithmetic/flag result from production helpers. + internal static string QualifyWordDivisionEncoding(ushort opcode, ushort inputSr, string family) + { + var expected = family switch { "DIVU.W" => 0x80c0, "DIVS.W" => 0x81c0, _ => -1 }; + var mode = (opcode >> 3) & 7; + var register = opcode & 7; + if (expected < 0 || (opcode & 0xf1c0) != expected || mode == 1 || (mode == 7 && register > 4) || + (inputSr & ~0x201f) != 0 || (inputSr & 31) is not (0 or 31)) + throw new XunitException($"Unqualified word-division encoding/profile: {family}/{opcode:X4}/{inputSr:X4}"); + return $"{family}/ea={mode}:{register}/destination={(opcode >> 9) & 7}/sr={inputSr:X4}"; + } +} + +public sealed class M68kWinUaeWordDivisionEncodingTests +{ + [Theory] + [InlineData(0x80c0, 0x0000, "DIVU.W", "DIVU.W/ea=0:0/destination=0/sr=0000")] + [InlineData(0x84df, 0x001f, "DIVU.W", "DIVU.W/ea=3:7/destination=2/sr=001F")] + [InlineData(0x8ee6, 0x2000, "DIVU.W", "DIVU.W/ea=4:6/destination=7/sr=2000")] + [InlineData(0x8efc, 0x201f, "DIVU.W", "DIVU.W/ea=7:4/destination=7/sr=201F")] + [InlineData(0x81c0, 0x0000, "DIVS.W", "DIVS.W/ea=0:0/destination=0/sr=0000")] + [InlineData(0x85d7, 0x001f, "DIVS.W", "DIVS.W/ea=2:7/destination=2/sr=001F")] + [InlineData(0x8ffb, 0x2000, "DIVS.W", "DIVS.W/ea=7:3/destination=7/sr=2000")] + [InlineData(0x83f8, 0x201f, "DIVS.W", "DIVS.W/ea=7:0/destination=1/sr=201F")] + public void FixedReferenceExamplesRemainDistinct(int opcode, int sr, string family, string expected) => + Assert.Equal(expected, M68kWinUaeCpuTesterConformanceTests.QualifyWordDivisionEncoding((ushort)opcode, (ushort)sr, family)); + + [Theory] + [InlineData(0x80c8, 0x0000, "DIVU.W")] + [InlineData(0x81fd, 0x0000, "DIVS.W")] + [InlineData(0x80c0, 0x0000, "DIVS.W")] + [InlineData(0x4c40, 0x0000, "DIVU.W")] + [InlineData(0x80c0, 0xa000, "DIVU.W")] + [InlineData(0x81c0, 0x0012, "DIVS.W")] + public void InvalidFormsAndUnqualifiedProfileStatesAreRejected(int opcode, int sr, string family) => + Assert.Throws(() => M68kWinUaeCpuTesterConformanceTests.QualifyWordDivisionEncoding((ushort)opcode, (ushort)sr, family)); +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 969baf66..6c854cbc 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -270,7 +270,7 @@ bus ordering, detailed fault sequencing, JIT and native ROM/media tests remain. - SingleStepTests has the pinned 312,500-case 68000 instruction-body audit. The later checkpoints below add a pinned multi-model WinUAE bridge, the unchanged failing Basic discovery audit, and separately qualified trace, - trap/bounds and breakpoint presets. Remaining Basic disagreements and + trap/bounds, breakpoint, legal long-arithmetic and word-division presets. Remaining Basic disagreements and unsupported integer execution still require per-case qualification; passing focused presets do not replace the failing broad audit. - Generated 010 word-MOVE/MOVEA format-8 images now have the scoped continuation @@ -2376,3 +2376,134 @@ Pinned tracked WinUAE/runner sources are clean. No production CPU, package or consumer change is made in this follow-up; the prior isolated .53 consumer evidence remains separate. No new package publication or host-performance measurement is claimed. Unrelated root CopperScreen changes remain untouched. + +## Word division reference qualification (2026-10-05) + +This follow-up qualifies a remaining defined-flag disagreement without changing +production CPU behavior. In the original Basic report, `DIVU.W` on +EC020/A1200/020/030 first fails at `84C0`: D2=`FFFFFFFF`, D0=`00000010`, +initial CCR=`1F`. The unsigned quotient overflows. The reference leaves C=1; +Copper68k returns C=0. Evidence remains in +`artifacts/m6-long-arithmetic-broad/winuae-model-audit.json` (EC020/A1200 callback +1,480; 020/030 callback 1,344). The unsigned 000/010/040/060 groups and every +signed group pass their original Basic inputs. + +[M68000PM 4-92/96](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) +defines X preservation and cleared carry for signed/unsigned division. Overflow +sets V and leaves the destination unchanged; N/Z are undefined. Divide-by-zero +traps with undefined N/Z/V and cleared carry. The pinned generator's +`newcpu_common.cpp:setdivuflags` documents C=0 for 020, but its 020/030 branch +only sets V and conditionally N. It omits the carry clear. The separate +[`WordDivision` patch](../scripts/winuae/word-division-carry.patch) adds an +explicit C clear after that helper in a copied `gencpu.cpp`. Signed division, +normal unsigned results, divide-by-zero and other model behavior retain their +existing paths. N/Z values and timing are not changed. This is a manual-qualified +software reference correction, not hardware observation or unchanged upstream. + +The preparation command retains pinned generator +`025b999239800357e95065fe5b9a15ea5b300fa7` and runner +`7a83745d6c6159bc74ab0471578ffc8bc244e66e`, untouched tracked sources and all +original Basic inputs. The copied normalized source is +`254873bb8a1b59b081e98defa72fda572d5a2c93c33bb644c8ac07c4257e5c5e`; +the patch is +`7eb90f09aaad56d164d0eafb9f66ddfc31becde9951a832820d5fdcc7973a83f`. +Prepared evidence: `artifacts/m6-word-division-inputs/`. Its manifest is +`a9aa908a2367fe46189c98e3778f34f6504d7d0a3784705116ecc030cc40abde`, +generator executable +`b0983c76a403017ad4e4a4480c4c571ace760f0ee07af01478ac3f8f829ab615`, +and native bridge +`dee99883158721c1166cd510977973667165f7ffc8d5c9641621b6a60eb4fa8b`. +All `.dat` input hashes are retained and checked before native loading. + +The complete profile/family selection, fixed callbacks, frames, masked-SR +counts and operand/profile forms are required. A reusable test-internal native +callback classifier observes immutable opcode/SR fields, rejects illegal data +sources, incorrect families and SR states outside this preset, and records +EA/register/input-SR combinations without production decoders or EA helpers. +Fourteen fixed encoding/profile examples join ordinary CI. These forms count +distinct first-word fields and initial SR; they do not imply exhaustive external +indexed structures or values. Basic generation uses CCR 0/31, user/supervisor +states and enabled full extensions, without incoming trace or bus faults. + +| Profile | DIVS callbacks / frames / masked / forms | DIVU callbacks / frames / masked / forms | +| --- | ---: | ---: | +| 68000 | 5,738 / 1,590 / 3,530 / 736 | 6,166 / 1,838 / 4,158 / 736 | +| 68010 | 5,782 / 1,632 / 3,836 / 736 | 6,058 / 1,792 / 4,032 / 736 | +| EC020 and A1200, each | 9,690 / 2,734 / 6,074 / 784 | 9,670 / 2,670 / 7,132 / 752 | +| 020, 030, 040 and 060, each | 9,246 / 2,570 / 5,816 / 688 | 8,870 / 2,466 / 5,676 / 656 | + +All sixteen groups pass: **134,928 callbacks, 37,804 frames, 87,936 masked-SR +cases and 11,392 architectural combinations**. Per-group register, defined-X, +defined-C and exception-frame corruption must fail; changes confined to +documented undefined flags must pass. All **80 controls** pass. The existing +independent flag masks remain unchanged, including defined C in overflow/trap +comparisons. No actual CPU result or fixture operand is modified to match. +The standalone command executes fifteen tests with zero skips, and writes +`winuae-word-division-audit.json` with distinct passing/mismatching/unsupported/ +untested counts. Evidence: `artifacts/m6-word-division-final/`. + +```powershell +./scripts/prepare-copper68k-winuae.ps1 ` + -GeneratorSource artifacts/reference-winuae-api ` + -RunnerSource artifacts/reference-copperline ` + -VcVars64 '/VC/Auxiliary/Build/vcvars64.bat' ` + -Preset WordDivision -OutputDirectory artifacts/winuae-word-division-inputs +./scripts/test-copper68k-winuae-qualified-exceptions.ps1 ` + -Preset WordDivision -InputDirectory artifacts/winuae-word-division-inputs ` + -OutputDirectory artifacts/winuae-word-division-report +``` + +Eight copied-input controls reject missing manifest/memory, empty families, +changed data, empty/duplicate profiles, unqualified source and unqualified patch +before native execution. The original qualified inputs remain intact. Evidence: +`artifacts/m6-word-division-preflight-controls/controls.json`. + +A temporary production mutation preserves incoming C only on unsigned word +overflow. The reference audit detects all six advanced DIVU groups; every other +group passes. The existing synthetic boundary matrix detects **448 mismatches +per advanced profile / 2,688 total**, while all sixteen addressing/boundary +reports contain zero unsupported or untested cases. Classic profiles and the +addressing groups pass; this mutation does not exercise overflow in their +chosen addressing samples. A separate expected-count mutation (000 DIVU +6,166 to 6,165) fails only that group despite native arithmetic passing. +Evidence: `artifacts/m6-word-division-mutation-controls-final/controls.json`. +Both source files are restored byte-for-byte before final validation. The first +control script incorrectly expected addressing mismatches too; its assertion +failed, its `finally` restored sources, and the corrected control run verifies +the measured boundary-only scope. Exploratory count/form discovery reports are +retained separately and never labeled passing gates. + +This slice does not retire a regression, produce/publish a package, change the +CPU/public API/timing policy or broaden physical qualification. The prior private +.53 consumer evidence remains separate because production source is unchanged. +Milestone 6 remains in progress, including advanced restoration, other reference +disagreements, broader independent coverage and consolidation review. + +Final Release CPU validation passes **4,916 tests / ten optional skips / zero +failures**, including WordDivision, LongArithmetic, TrapBounds and Breakpoints. +The strict gate passes **14,646,552 logical cases / 574 batches**, with +`roadmapComplete=false`; fresh pinned SingleStepTests passes 312,500 cases in +125 files, and Musashi passes 536 programs with 88 explicit exclusions. Evidence: +`artifacts/m6-word-division-full/`, `artifacts/m6-word-division-full.log` and +`artifacts/m6-word-division-gate.log`. + +The fresh original Basic audit retains exactly **1,326 passing / 47 mismatching / +eight unsupported / zero untested** groups: 11,474,194 callbacks, 1,663,891 +frames, 199,327 masked-SR cases and one terminal callback. All 32 controls pass. +Evidence: `artifacts/m6-word-division-broad/`. The unchanged original manifest +`37cd8ddae8b61ac60e3a49ba835362fbf80d418f48c2539338e6541c9d85e31f` +and native library +`75f0c11352d4a8c1e84f32a49a5347ae4cb5bd9128f1501a33d843e6f99cb057` +remain intact. These failures are not relabeled by the qualified preset. + +`artifacts/m6-word-division-identities.json` records exact source/script/assembly +hashes against baseline `e8140401e9ea37e2a05583eb3cea9f12c9b4a0a5`. +The unchanged production source tree is +`12753a6d2bb677f36cae03f3193f370828f50a66`; its default assembly is +`b56545b9695b2b18f2c906e330152885fd52b28d6166f544166ea1910d836102`, +and the adapter assembly is +`2a858963843343689c0637de0843f1d61ccdd9e48f6862d102adbb15dfb7bc7e`. +The CPU informational version now embeds baseline `e814040`; the different +assembly hash does not imply a CPU source change. Validated source/assembly +identities remain stable after the strict gate. PowerShell parsing and local +documentation link checks pass. No additional seeded audit is claimed. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 4ed5174f..032cabe7 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1112,3 +1112,36 @@ remain visible. CPU sources and timing policy are unchanged; no new package or consumer modification is needed. Source/input/assembly identities, historical failures and limitations are maintained in the reference record. The goal stays active with `roadmapComplete=false`. + +### Milestone 6 word-division qualification — 2026-10-05 + +The pinned WinUAE unsigned-word-overflow helper omitted carry clearing on +020/030 despite the programmer reference and its own C=0 comment. A separate +`WordDivision` preset qualifies this defined-flag behavior through a copied CPU +generator. Existing CPU behavior and independent flag masks remain unchanged; +the original Basic corpus and its disagreements remain visible. + +All eight profiles pass DIVS.W/DIVU.W: 134,928 callbacks, 37,804 exception frames, +87,936 masked-SR cases and 11,392 model/family/EA/register/input-SR combinations. +The shared qualified audit requires exact complete selections, pinned identities, +callback/frame/mask/form counts and raw input classification. Fourteen fixed +encoding/profile checks join ordinary CI. All 80 register/X/C/frame/undefined-flag +controls pass; eight missing/changed/empty/duplicate/unqualified inputs fail +preflight. A carry mutation causes six reference disagreements and 2,688 synthetic +boundary mismatches; a stale expected count fails its exact group. Sources are +restored before final validation. The addressing groups pass that mutation and +are explicitly not claimed as overflow detection. No old regression is retired. + +The [reference record](COPPER68K_REFERENCE_QUALIFICATION.md#word-division-reference-qualification-2026-10-05) +maintains commands, exact inputs, corrections, failure evidence and scope limits. +Advanced restoration, other reference disagreements, independent coverage and +consolidation remain required. Milestone 6 stays **in progress**; no package +publication, consumer change, physical timing or enabled-MMU/FPU qualification +is included. + +Final validation passes 4,916 Release CPU tests with ten optional skips, including +all four qualified presets. The strict gate retains 14,646,552 logical cases in +574 batches with fresh pinned SingleStepTests/Musashi audits. The fresh original +Basic audit retains 1,326 passing, 47 mismatching, eight unsupported and zero +untested groups; all 32 controls pass. It remains failing. Source/input/assembly +identities stay stable; the goal remains active with `roadmapComplete=false`. diff --git a/scripts/prepare-copper68k-winuae.ps1 b/scripts/prepare-copper68k-winuae.ps1 index 899bb7d6..48f46c6d 100644 --- a/scripts/prepare-copper68k-winuae.ps1 +++ b/scripts/prepare-copper68k-winuae.ps1 @@ -4,7 +4,7 @@ param( [Parameter(Mandatory)] [string] $GeneratorSource, [Parameter(Mandatory)] [string] $RunnerSource, [Parameter(Mandatory)] [string] $VcVars64, - [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints','LongArithmetic')] [string] $Preset = 'Basic', + [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints','LongArithmetic','WordDivision')] [string] $Preset = 'Basic', [string] $OutputDirectory = 'artifacts/winuae-model-inputs' ) $ErrorActionPreference = 'Stop' @@ -48,17 +48,17 @@ try { & ./build68k.exe table68k | Set-Content -Encoding utf8 cpudefs.cpp if ($LASTEXITCODE -ne 0) { throw 'Opcode table generation failed' } $cpuGeneratorSource = 'gencpu.cpp' - if ($Preset -in @('TrapBounds','Breakpoints','LongArithmetic')) { + if ($Preset -in @('TrapBounds','Breakpoints','LongArithmetic','WordDivision')) { # TrapBounds: following PC (MC68020UM 6.1.4 / MC68040UM 8.2.3). # Breakpoints: opcode PC (illegal exception, UM 6.1.5 / 8.2.4). # Patch a copy; Basic keeps the pinned source. - $sourceName = switch ($Preset) {'TrapBounds' {'gencpu-trap-bounds.cpp'} 'Breakpoints' {'gencpu-breakpoints.cpp'} 'LongArithmetic' {'gencpu-long-arithmetic.cpp'}} - $patchName = switch ($Preset) {'TrapBounds' {'trap-bounds-pc.patch'} 'Breakpoints' {'breakpoint-pc.patch'} 'LongArithmetic' {'long-arithmetic-unimplemented.patch'}} + $sourceName = switch ($Preset) {'TrapBounds' {'gencpu-trap-bounds.cpp'} 'Breakpoints' {'gencpu-breakpoints.cpp'} 'LongArithmetic' {'gencpu-long-arithmetic.cpp'} 'WordDivision' {'gencpu-word-division.cpp'}} + $patchName = switch ($Preset) {'TrapBounds' {'trap-bounds-pc.patch'} 'Breakpoints' {'breakpoint-pc.patch'} 'LongArithmetic' {'long-arithmetic-unimplemented.patch'} 'WordDivision' {'word-division-carry.patch'}} $patchPath = Join-Path $PSScriptRoot ('winuae/' + $patchName) $patch = [IO.File]::ReadAllText($patchPath).Replace("`r`n", "`n") $source = [IO.File]::ReadAllText((Join-Path $generator 'gencpu.cpp')).Replace("`r`n", "`n") $hunks = @($patch -split "(?m)^@@`n" | Select-Object -Skip 1) - $requiredHunks = switch ($Preset) {'Breakpoints' {1} 'TrapBounds' {2} 'LongArithmetic' {3}} + $requiredHunks = switch ($Preset) {'Breakpoints' {1} 'TrapBounds' {2} 'LongArithmetic' {3} 'WordDivision' {1}} if ($hunks.Count -ne $requiredHunks) { throw "Incorrect $Preset patch hunk count" } foreach ($hunk in $hunks) { $lines = $hunk.TrimEnd("`n").Split("`n") @@ -158,6 +158,9 @@ void M68KTester_destroy(M68KTesterContext* context) { if ($Preset -eq 'LongArithmetic') { $baseIni = $baseIni.Replace('[test=Basic]', '[test=LongArithmetic]').Replace('mode=all', 'mode=MULL.L,DIVL.L') } + if ($Preset -eq 'WordDivision') { + $baseIni = $baseIni.Replace('[test=Basic]', '[test=WordDivision]').Replace('mode=all', 'mode=DIVU.W,DIVS.W') + } foreach ($model in @( @{id='68000'; cpu='68000'; width=24}, @{id='68010'; cpu='68010'; width=24}, @{id='68EC020'; cpu='68020'; width=24}, @{id='68020'; cpu='68020'; width=32}, @@ -207,6 +210,8 @@ void M68KTester_destroy(M68KTesterContext* context) { LongArithmeticPatchSha256=$(if ($Preset -eq 'LongArithmetic') {(Get-FileHash -LiteralPath (Join-Path $output 'long-arithmetic-encodings.patch')).Hash.ToLowerInvariant()} else {$null}) LongArithmeticCpuSourceSha256=$(if ($Preset -eq 'LongArithmetic') {(Get-FileHash -LiteralPath (Join-Path $output 'gencpu-long-arithmetic.cpp')).Hash.ToLowerInvariant()} else {$null}) LongArithmeticCpuPatchSha256=$(if ($Preset -eq 'LongArithmetic') {(Get-FileHash -LiteralPath (Join-Path $output 'long-arithmetic-unimplemented.patch')).Hash.ToLowerInvariant()} else {$null}) + WordDivisionSourceSha256=$(if ($Preset -eq 'WordDivision') {(Get-FileHash -LiteralPath (Join-Path $output 'gencpu-word-division.cpp')).Hash.ToLowerInvariant()} else {$null}) + WordDivisionPatchSha256=$(if ($Preset -eq 'WordDivision') {(Get-FileHash -LiteralPath (Join-Path $output 'word-division-carry.patch')).Hash.ToLowerInvariant()} else {$null}) NativeLibrarySha256=(Get-FileHash -LiteralPath (Join-Path $output 'm68k_cpu_tester.dll') -Algorithm SHA256).Hash.ToLowerInvariant() Compiler=$compiler; GeneratorExecutableSha256=(Get-FileHash -LiteralPath (Join-Path $generator 'cputester.exe') -Algorithm SHA256).Hash.ToLowerInvariant() NativeSourceSha256=(Get-FileHash -LiteralPath (Join-Path $output 'winuae-native.c') -Algorithm SHA256).Hash.ToLowerInvariant() diff --git a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 index fceba4a4..a0623d4f 100644 --- a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 +++ b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 @@ -1,7 +1,7 @@ #requires -Version 7.0 [CmdletBinding()] param( - [ValidateSet('TrapBounds','Breakpoints')] [string] $Preset = 'TrapBounds', + [ValidateSet('TrapBounds','Breakpoints','WordDivision')] [string] $Preset = 'TrapBounds', [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $InputDirectory, [string] $NativeLibrary, [string] $OutputDirectory = 'artifacts/winuae-trap-bounds-audit' @@ -14,9 +14,15 @@ $library = (Resolve-Path -LiteralPath $NativeLibrary).Path $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } New-Item -ItemType Directory -Path $output | Out-Null -$key = if ($Preset -eq 'TrapBounds') {'TRAP_BOUNDS'} else {'BREAKPOINT'} -$testName = if ($Preset -eq 'TrapBounds') {'WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled'} else {'WinUaeBreakpointExceptionsAcrossSelectedModelsWhenEnabled'} -$reportName = if ($Preset -eq 'TrapBounds') {'winuae-trap-bounds-audit.json'} else {'winuae-breakpoint-audit.json'} +$key = switch ($Preset) {'TrapBounds' {'TRAP_BOUNDS'} 'Breakpoints' {'BREAKPOINT'} 'WordDivision' {'WORD_DIVISION'}} +$testName = switch ($Preset) {'TrapBounds' {'WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled'} 'Breakpoints' {'WinUaeBreakpointExceptionsAcrossSelectedModelsWhenEnabled'} 'WordDivision' {'WinUaeWordDivisionAcrossSelectedModelsWhenEnabled'}} +$reportName = switch ($Preset) {'TrapBounds' {'winuae-trap-bounds-audit.json'} 'Breakpoints' {'winuae-breakpoint-audit.json'} 'WordDivision' {'winuae-word-division-audit.json'}} +$filter = "FullyQualifiedName~$testName" +$expectedTests = 1 +if ($Preset -eq 'WordDivision') { + $filter += '|FullyQualifiedName~M68kWinUaeWordDivisionEncodingTests' + $expectedTests = 15 +} $settings = @{ ('COPPER68K_RUN_WINUAE_' + $key + '_AUDIT') = '1' ('COPPER68K_WINUAE_' + $key + '_PATH') = $root @@ -29,11 +35,11 @@ try { $saved[$name] = [Environment]::GetEnvironmentVariable($name) [Environment]::SetEnvironmentVariable($name, $settings[$name]) } - & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter "FullyQualifiedName~$testName" --logger 'trx;LogFileName=audit.trx' --results-directory $output + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter $filter --logger 'trx;LogFileName=audit.trx' --results-directory $output if ($LASTEXITCODE -ne 0) { throw "Qualified $Preset audit failed" } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counts = $trx.TestRun.ResultSummary.Counters - if ([int]$counts.executed -ne 1 -or [int]$counts.passed -ne 1) { throw "$Preset audit did not execute its required test" } + if ([int]$counts.executed -ne $expectedTests -or [int]$counts.passed -ne $expectedTests) { throw "$Preset audit did not execute its complete test selection" } if (-not (Test-Path -LiteralPath (Join-Path $output $reportName))) { throw "$Preset coverage report is missing" } } finally { foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } diff --git a/scripts/winuae/word-division-carry.patch b/scripts/winuae/word-division-carry.patch new file mode 100644 index 00000000..3b1c99be --- /dev/null +++ b/scripts/winuae/word-division-carry.patch @@ -0,0 +1,8 @@ +--- gencpu.cpp (025b999239800357e95065fe5b9a15ea5b300fa7) ++++ gencpu-word-division.cpp (M68000PM 4-96 defined carry on DIVU overflow) +@@ + out("if (newv > 0xffff) {\n"); + out("setdivuflags((uae_u32)dst, (uae_u16)src);\n"); ++ out("SET_CFLG(0);\n"); + out("} else {\n"); + genflags (flag_logical, sz_word, "newv", "", ""); From 22f31d0aeb8429220ba4cc4a7617b193b17a74ea Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 07:37:03 +0300 Subject: [PATCH 030/211] Expose unqualified 68040 access-frame restoration --- Copper68k.Tests/Synthetic/README.md | 11 ++ .../SyntheticExceptionControlTests.cs | 4 +- .../SyntheticM68040AccessFrameAuditTests.cs | 182 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 109 +++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 25 +++ scripts/test-copper68k-040-access-frames.ps1 | 136 +++++++++++++ 6 files changed, 466 insertions(+), 1 deletion(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs create mode 100644 scripts/test-copper68k-040-access-frames.ps1 diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 1ee23e97..b1b4bba4 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -223,3 +223,14 @@ The additional pinned Windows WinUAE model audit and preparation command are documented in [WinUAE conformance](../M68kWinUaeCpuTesterConformanceTests.md#pinned-integer-audit-across-cpu-models-milestone-6-checkpoint). It is an opt-in discovery gate that currently reports unresolved mismatches; it does not replace the deterministic synthetic gate or complete milestone 6. + +The independent 040 access-frame discovery command is +`./scripts/test-copper68k-040-access-frames.ps1 -OutputDirectory artifacts/040-access-frame-audit`. +It currently **fails**: required format-7 normal/trace restoration disagrees, +and CM/CU/CP continuation fixtures remain explicitly untested. Short-frame +controls qualify the fixture; failed prerequisites leave later phases untested. +The command checks exact selections, combinations, counts and source/assembly +identities. Existing outputs must be validated with `-ValidateReportsOnly` or a +fresh output directory used. It cannot pass merely by fixing normal/trace return. +See the [reference record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-access-frame-restoration-discovery-2026-10-05) +for failed-before evidence, temporary probe controls and the remaining scope. diff --git a/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs b/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs index 8297efe7..f9358430 100644 --- a/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs @@ -24,7 +24,9 @@ public void RteFramesPrivilegeStackSelectionAndInvalidFormats(string modelId) } m.InitializePhysical(0x6000, 0x4e71, 2); m.InitializePhysical(0x6002, 0x4e71, 2); var e = SyntheticExecution.Prepare(m, [0x4e73]); - // Fault restart and coprocessor internal-state restoration remain in retained specialist suites. + // These legal restoration protocols remain implementation/qualification + // gaps, not invalid formats. The separate 040 access-frame discovery + // command reports its required gaps and fails until they are qualified. var outside = modelId == "68040" ? format == 7 : m.Model.FullIndex && modelId != "68060" && format is 9 or 10 or 11; if (outside && supervisor) continue; diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs new file mode 100644 index 00000000..ab8949e1 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -0,0 +1,182 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Discovery gate, deliberately separate from the promoted ordinary CI matrix. +// MC68040UM 8.4.6.2/7 is the expectation source, not WinUAE's non-MMU RTE. +public sealed class SyntheticM68040AccessFrameAuditTests(ITestOutputHelper output) +{ + internal const string Enable = "COPPER68K_RUN_040_ACCESS_FRAME_AUDIT"; + private static readonly ushort[] TraceStates = [0, 0x8000, 0x4000]; + private static readonly ushort[] AccessStates = [0, 0x0105, 0x0505, 0x0905]; + private static readonly string[] Banks = ["user", "ISP", "MSP"]; + private const uint Target = 0x6000, TraceHandler = 0x9090; + + [EnvironmentFact(Enable, "audit complete 040 format-7 restoration prerequisites"), Trait("Suite", "ReferenceDiscovery")] + public void ExistingShortFramesValidateTheRestorationFixture() => Audit("rte-access-controls", [0, 2, 3], false); + + [EnvironmentFact(Enable, "audit complete 040 format-7 restoration prerequisites"), Trait("Suite", "ReferenceDiscovery")] + public void AccessFrameWithoutContinuationRestoresAndTracesFollowingInstruction() => Audit("rte-access-normal", [7], false); + + [EnvironmentFact(Enable, "audit complete 040 format-7 restoration prerequisites"), Trait("Suite", "ReferenceDiscovery")] + public void PendingTraceConvertsTheFrameBeforeReturningAndResuming() => Audit("rte-access-trace", [7], true); + + [EnvironmentFact(Enable, "audit complete 040 format-7 restoration prerequisites"), Trait("Suite", "ReferenceDiscovery")] + public void RemainingContinuationProtocolsCannotDisappearFromTheGate() + { + var report = new CoverageBatch("68040", "rte-access-continuations"); + // These are explicit prerequisite gaps, not asserted format-error behavior. + // CM needs full MOVEM replay fixtures; CU/CP need independently qualified + // pending-FPU state. FPU arithmetic being out of scope does not prove RTE. + foreach (var bank in Banks) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var form in new[] { "CM-indirect", "CM-postincrement", "CM-predecrement", "CM-displacement", + "CM-indexed", "CM-absolute", "CM-PC-relative", "CU-pending-unimplemented", "CP-pending-postinstruction" }) + report.Record($"68040/RTE/format7/{form}/bank={bank}/op=4E73/ccr={ccr:X2}", "untested", + "Required continuation protocol has no independently qualified execution fixture yet"); + report.Complete(output); + } + + private void Audit(string group, int[] formats, bool pendingTrace) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); + var report = new CoverageBatch("68040", group); + foreach (var format in formats) + foreach (var bank in Banks) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var trace in TraceStates) + foreach (var vbr in new[] { 0u, 0x10000u }) + foreach (var ea in new[] { 0x5ffau, 0x12345678u }) + foreach (var access in format == 7 ? AccessStates : new ushort[] { 0 }) + { + var (e, frame, savedSr) = Prepare(m, format, bank, ccr, trace, vbr, ea, + (ushort)(access | (pendingTrace ? 0x2000 : 0))); + var id = $"68040/RTE/format{format}/{(pendingTrace ? "CT" : "normal")}/bank={bank}/T={trace:X4}/VBR={vbr:X8}/EA={ea:X8}/SSW={access:X4}/op=4E73/ccr={ccr:X2}"; + var sequence = m.Core.State.ExceptionSequence; + var end = frame + (format == 7 ? 60u : format == 0 ? 8u : 12u); + Restore(e, bank, end, savedSr); + e.Pc = Target; + if (pendingTrace) ExpectTrace(m, e, bank, frame + 48, savedSr, ea, sequence + 1); + else e.ControlChecks["no exception during RTE"] = (s => s.ExceptionSequence, sequence); + if (!Step(m, e, report, id + "/RTE")) + { + if (pendingTrace) report.Record(id + "/return", "untested", "RTE prerequisite failed"); + report.Record(id + "/following-BRA", "untested", "RTE prerequisite failed"); + continue; + } + if (pendingTrace) + { + // Return from the pending trace, using the newly converted frame. + Restore(e, bank, end, savedSr); + e.Pc = Target; e.ExceptionVector = null; + e.ControlChecks["no exception during RTE"] = (s => s.ExceptionSequence, sequence + 1); + if (!Step(m, e, report, id + "/return")) + { + report.Record(id + "/following-BRA", "untested", "Pending trace return prerequisite failed"); + continue; + } + } + e.ControlChecks.Remove("no exception during RTE"); + if (trace != 0) ExpectTrace(m, e, bank, end - 12, savedSr, Target, sequence + (pendingTrace ? 2u : 1u)); + else e.ControlChecks["no following exception"] = (s => s.ExceptionSequence, sequence + (pendingTrace ? 1u : 0u)); + Step(m, e, report, id + "/following-BRA"); + } + report.Complete(output); + } + + private static (ArchitecturalExpectation Expected, uint Frame, ushort SavedSr) Prepare( + SyntheticMachine m, int format, string bank, int ccr, ushort trace, uint vbr, uint ea, ushort ssw) + { + m.Reset(ccr); + var frame = bank == "MSP" ? 0x7400u : 0x4700u; + // Guard the whole frame and both neighbors. WB status/data are populated + // even when not used: RTE must not replay the handler's pending writebacks. + for (var i = -16; i < 80; i++) m.InitializePhysical(unchecked(frame + (uint)i), (uint)(0x5a ^ i), 1); + var savedSr = SavedStatus(bank, trace, ccr); + m.InitializePhysical(frame, savedSr, 2); + m.InitializePhysical(frame + 2, Target, 4); + m.InitializePhysical(frame + 6, (uint)(format << 12) | 8, 2); + m.InitializePhysical(frame + 8, ea, 4); + m.InitializePhysical(frame + 12, ssw, 2); + foreach (var offset in new[] { 14u, 16u, 18u }) m.InitializePhysical(frame + offset, 0x85, 2); + foreach (var offset in new[] { 24u, 32u, 40u }) + { + m.InitializePhysical(frame + offset, 0x4200 + offset, 4); + m.InitializePhysical(frame + offset + 4, 0xc3a55a3c, 4); + m.InitializePhysical(0x4200 + offset, 0x96abcdef, 4); + } + m.InitializePhysical(Target, 0x60fe, 2); // self BRA; discriminates restored T0/T1. + m.InitializePhysical(Target + 2, 0x4e71, 2); + m.InitializePhysical(TraceHandler, 0x4e73, 2); + m.InitializePhysical(TraceHandler + 2, 0x4e71, 2); + m.InitializePhysical(vbr + 9 * 4, TraceHandler, 4); + _ = SyntheticExecution.Prepare(m, [0x4e73]); + m.Core.State.VectorBaseRegister = vbr; + m.Core.State.SetMasterStackPointer(0x7400); + if (bank == "MSP") m.Core.State.StatusRegister |= 0x1000; + var e = ArchitecturalExpectation.Capture(m); + e.OperandAccessAddresses.UnionWith(new uint[] { 0x4218, 0x4220, 0x4228 }); + e.ExpectedOperandTransfers = []; // Pending writebacks belong to the handler. + return (e, frame, savedSr); + } + + internal static ushort SavedStatus(string bank, ushort trace, int ccr) => + (ushort)((bank switch { "user" => 0, "ISP" => 0x2000, "MSP" => 0x3000, _ => throw new ArgumentException("Unknown stack bank") }) | trace | (ccr ^ 31)); + + private static void Restore(ArchitecturalExpectation e, string bank, uint stack, ushort sr) + { + e.Sr = sr; + e.A[7] = bank == "user" ? 0x7800u : stack; + e.InactiveStackPointer = bank == "user" ? stack : 0x7800u; + if (bank == "MSP") e.MasterStackPointer = stack; + } + + private static void ExpectTrace(SyntheticMachine m, ArchitecturalExpectation e, string bank, + uint stack, ushort sr, uint tracedInstruction, uint sequence) + { + e.ControlChecks.Remove("no exception during RTE"); + e.ControlChecks["exception entries"] = (s => s.ExceptionSequence, sequence); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, Target); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, sr); + e.Sr = (ushort)((sr | 0x2000) & ~0xc000); + e.A[7] = stack; + e.InactiveStackPointer = 0x7800; + if (bank == "MSP") e.MasterStackPointer = stack; + e.Write(stack, sr, 2, m.Model); + e.Write(stack + 2, Target, 4, m.Model); + e.Write(stack + 6, 0x2024, 2, m.Model); + e.Write(stack + 8, tracedInstruction, 4, m.Model); + e.Pc = TraceHandler; e.ExceptionVector = 9; + } + + private static bool Step(SyntheticMachine m, ArchitecturalExpectation e, CoverageBatch report, string id) + { + try + { + m.Core.ExecuteInstruction(); + var mismatch = e.Verify(m); + report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); + return mismatch == null; + } + catch (Exception ex) + { + report.Record(id, ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException or UnsupportedM68040InstructionException + ? "unsupported" : "mismatching", ex.Message); + return false; + } + } +} + +public sealed class M68040AccessFrameFixtureTests +{ + [Theory] + [InlineData("user", 0, 0, 0x001f)] + [InlineData("user", 0x8000, 31, 0x8000)] + [InlineData("ISP", 0, 31, 0x2000)] + [InlineData("ISP", 0x4000, 0, 0x601f)] + [InlineData("MSP", 0x8000, 0, 0xb01f)] + [InlineData("MSP", 0x4000, 31, 0x7000)] + public void SavedStatusHasIndependentFixedReferenceExamples(string bank, int trace, int ccr, int expected) => + Assert.Equal((ushort)expected, SyntheticM68040AccessFrameAuditTests.SavedStatus(bank, (ushort)trace, ccr)); +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 6c854cbc..8280e2f6 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -277,6 +277,8 @@ bus ordering, detailed fault sequencing, JIT and native ROM/media tests remain. gate below. Long transfers, other instruction families, foreign silicon images, external bus faults, 020/030 formats 9/A/B and 040 format 7 remain unqualified. The current advanced decoder does not implement all these legal restoration protocols; this is an implementation gap, not invalid encoding. + The [040 access-frame discovery gate](#040-access-frame-restoration-discovery-2026-10-05) + now exposes normal/trace failures and preserves CM/CU/CP prerequisites explicitly. - Nested 000 address errors during exception stacking previously recursed on an odd SSP. This is corrected by the address-error double-fault slice above. External BERR and reset-vector fault signaling remain unavailable through the current @@ -2507,3 +2509,110 @@ The CPU informational version now embeds baseline `e814040`; the different assembly hash does not imply a CPU source change. Validated source/assembly identities remain stable after the strict gate. PowerShell parsing and local documentation link checks pass. No additional seeded audit is claimed. + +## 040 access-frame restoration discovery — 2026-10-05 + +This checkpoint adds an independent, deliberately failing restoration audit; +**no production CPU correction is committed**. The existing Basic WinUAE RTE +successes do not establish advanced-frame continuation correctness. The ordinary +synthetic RTE matrix skips the relevant legal formats, and its former comment +incorrectly suggested that retained specialist tests supplied the missing proof. +That comment now identifies the implementation/qualification gap. + +Authority is [MC68040UM](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf), +sections 8.4.6.2/7. A normal format-7 return consumes 60 bytes. CT creates the +trace frame at old SP+48 using the saved EA. CM replays MOVEM operands with the +saved address where required. Pending writebacks belong to the handler. +Simultaneous continuation bits are undefined and excluded. The trace vector +offset 0x24 follows the normal trace-frame interpretation; it is not a silicon +observation. Short-frame controls use sections 8.4.1/3/4. + +The pinned [WinUAE MMU helper](https://raw.githubusercontent.com/tonioni/WinUAE/6ae6fb6b84bb9517e0245a80fc9bdca1a8580dde/cpummu.cpp) +was inspected, not executed as an oracle. Its CT staging and CM saved-EA state +are distinct from the pinned non-MMU generator's skip-only format-7 path +(`025b999239800357e95065fe5b9a15ea5b300fa7`, `gencpu/gencpu.cpp`). Neither path +certifies all continuation protocols. The primary PDF was inspected through the +web reader; direct NXP download was unavailable, so no local PDF hash is claimed. + +Run the complete discovery command: + +```powershell +./scripts/test-copper68k-040-access-frames.ps1 -OutputDirectory artifacts/040-access-frame-audit +``` + +The command rejects reused outputs, missing identities/reports, empty or partial +test selection, changed source/assembly inputs, foreign combinations, negative +counts and stale group/combination cardinalities. `-ValidateReportsOnly` verifies +existing reports against the current inputs; it does not turn historical failures +into success. Each request executes four xUnit batches and six fixed saved-SR +examples. Reports separate actual execution phases from dependent untested phases. + +Canonical fixtures cover user, ISP and MSP returns, all 32 CCRs, separate T0/T1 +and no-trace SR states, default/relocated VBR, two saved EAs and four SSW access +states. The sparse recording bus guards the complete frame, its neighbors and +pending writeback destinations, including absence of their operand transfers. +Normal return executes a following self-BRA; CT additionally returns through the +new trace frame before the following BRA. This checks restored trace retirement, +exact PC/SR/stack banks, registers, memory, saved exception diagnostics and entry +count. Expectations use independent constants and never production EA helpers. + +| Group | Passing phases | Mismatching phases | Unsupported | Untested phases | +| --- | ---: | ---: | ---: | ---: | +| Existing formats 0/2/3 controls | 6,912 | 0 | 0 | 0 | +| Format-7 normal return | 0 | 4,608 | 0 | 4,608 | +| Format-7 pending trace | 0 | 4,608 | 0 | 9,216 | +| CM/CU/CP prerequisite inventory | 0 | 0 | 0 | 864 | +| Total | 6,912 | 9,216 | 0 | 14,688 | + +These are **30,816 logical phases**, not 30,816 executed instructions: 864 are +explicit inventory gaps and 13,824 depend on failed RTE prerequisites. The +continuation inventory names seven CM addressing categories plus CU and CP for +each bank/CCR; it is not a completed size/opcode/full-index/alias matrix. Those +more detailed combinations still require fixtures and qualification. + +Failed-before evidence is `artifacts/m6-rte-access-before/before.trx` and the +fresh command output recorded below. All attempted format-7 returns enter the +incorrect format-error path; their dependent phases are not retried. The current +CPU source tree remains `12753a6d2bb677f36cae03f3193f370828f50a66` at baseline +`7845c5ee06db83885c3f69cd1f0951c9e4e77090`. + +Temporary, uncommitted probes qualify the downstream audit phases. A normal/CT +prototype passes all 29,952 executable phases while the complete gate still +fails on 864 continuation gaps. A skip-only prototype and a wrong traced-address +prototype each preserve the 6,912 short-frame controls and 9,216 normal phases +but fail all 4,608 pending-trace prerequisites. The latter distinguishes saved +EA from the RTE opcode address. These probes establish detection and fixture +reachability, not a shipped fix or independent hardware qualification. Source +is restored byte-for-byte and rebuilt after probing. No instruction fallback, +retry, changed timing policy or package publication is introduced. + +The next implementation must qualify MOVEM saved-EA/replay and pending CU/CP +protocols, alongside normal/CT return. Detailed frame-validation faults, odd +return PCs, throwaway-to-access frames, real access-fault entry and other-model +advanced restoration remain open. FPU arithmetic, enabled MMU and physical +pipeline/cache/timing remain outside the roadmap. No specialized regression is +retired. Milestone 6 and the full goal remain **in progress**; +`roadmapComplete=false` remains unchanged. + +Final unchanged-CPU evidence is `artifacts/m6-rte-access-restored/`: the command +exits 1 with the exact table above and writes a failing `audit-summary.json`. +`identities.json` records the actual tracked CPU-file hashes (separate from the +committed tree), fixture/command hashes and loaded build assemblies. The restored +RTE source SHA-256 is +`31ca43359abf7aff20a54e8507e5876a7c980fc73732556e2c320d70c7f3407f`. +Probe evidence is `artifacts/m6-rte-access-probes-final/controls.json`; all three +probe runs leave the complete gate failing. Earlier command-development failures +and the initial compile typo remain historical failures, not successful audits. + +All thirteen specific report/input corruption controls fail for their intended +reason (`artifacts/m6-rte-access-preflight-controls/controls.json`): missing report, +missing identity, empty selection, stale cardinality, negative status, foreign +model/combination, duplicate fixture input, changed/empty CPU source identity, +changed fixture/command identity, changed assembly identity and missing status. +The restored Release focused selection passes 22 tests with four discovery skips +and zero failures (`artifacts/m6-rte-access-focused/focused.trx`). Those skips are +unavailable ordinary-CI restoration coverage; the explicit discovery command +executes them and fails as recorded above. Production CPU sources remain +unchanged, so no new NuGet consumer validation or release is claimed or required +for this audit-only checkpoint. Earlier full-suite, strict deterministic and +external-reference records are retained unchanged; they do not certify format 7. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 032cabe7..304e63c7 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1145,3 +1145,28 @@ all four qualified presets. The strict gate retains 14,646,552 logical cases in Basic audit retains 1,326 passing, 47 mismatching, eight unsupported and zero untested groups; all 32 controls pass. It remains failing. Source/input/assembly identities stay stable; the goal remains active with `roadmapComplete=false`. + +### Milestone 6 advanced-restoration discovery — 2026-10-05 + +The new independent 040 access-frame audit reproduces 9,216 format-7 RTE +mismatches, retains 14,688 prerequisite/continuation phases as untested and +passes 6,912 short-frame control phases. Its complete 30,816-phase selection is +explicitly failing. Six fixed saved-SR examples join ordinary CI; the four +restoration batches remain an opt-in discovery gate until implementation and +reference qualification are complete. + +A temporary normal/CT prototype reaches all downstream phases, but CM/CU/CP +gaps still fail the command. Skip-only and wrong-traced-address probes detect +all CT prerequisites. Production sources are restored; no CPU fix, package, +consumer change, timing-policy change or regression retirement is included. +The [reference record](COPPER68K_REFERENCE_QUALIFICATION.md#040-access-frame-restoration-discovery-2026-10-05) +maintains the full scope, authorities, failure evidence and limitations. Legal +advanced restoration on all applicable models, other reference disagreements, +independent combination coverage and consolidation remain required. Milestone 6 +stays **in progress** with `roadmapComplete=false`. + +Final Release focused validation passes 22 tests with four explicit discovery +skips. The complete requested discovery executes all four batches and six fixed +examples, exits 1 and records exactly the required failures/gaps. All thirteen +specific malformed-input/report controls are detected. No earlier green gate +is relabeled as advanced-restoration coverage. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 new file mode 100644 index 00000000..ec219f60 --- /dev/null +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -0,0 +1,136 @@ +#requires -Version 7.0 +[CmdletBinding()] +param( + [string] $OutputDirectory = 'artifacts/040-access-frame-audit', + [switch] $ValidateReportsOnly +) +$ErrorActionPreference = 'Stop' +$repo = Split-Path -Parent $PSScriptRoot +$output = [IO.Path]::GetFullPath($OutputDirectory, $repo) +$expected = [ordered]@{ + 'rte-access-controls' = @{cases=6912; combinations=108} + 'rte-access-normal' = @{cases=9216; combinations=144} + 'rte-access-trace' = @{cases=13824; combinations=144} + 'rte-access-continuations' = @{cases=864; combinations=27} +} +$testExit = 0 +if (-not $ValidateReportsOnly) { + if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } + New-Item -ItemType Directory -Path $output | Out-Null + $settings = @{ COPPER68K_RUN_040_ACCESS_FRAME_AUDIT='1'; COPPER68K_SYNTHETIC_REPORT_DIR=$output } + $saved = @{} + try { + foreach ($name in $settings.Keys) { + $saved[$name] = [Environment]::GetEnvironmentVariable($name) + [Environment]::SetEnvironmentVariable($name, $settings[$name]) + } + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output + $testExit = $LASTEXITCODE + } finally { + foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } + } + $identity = [ordered]@{ + schema=1; reference='MC68040UM'; url='https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf' + sections=@('8.4.1','8.4.3','8.4.4','8.4.6.2','8.4.6.7'); softwareReferenceExecuted=$false + sourceCommit=(& git -C $repo rev-parse HEAD); cpuCommittedTree=(& git -C $repo rev-parse HEAD:Copper68k) + cpuSourceFiles=@(& git -C $repo ls-files 'Copper68k/*') | ForEach-Object { + @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} + } + inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','scripts/test-copper68k-040-access-frames.ps1') | ForEach-Object { + @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} + } + assemblies=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | ForEach-Object { + @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} + } + limitations=@('Canonical synthetic frames, not hardware captures or enabled-MMU access faults', + 'CM/CU/CP continuation inventory remains untested and fails the gate', + 'Multiple continuation bits are architecturally undefined and excluded', + 'Detailed fault validation, odd return PCs, throwaway-to-access frames and physical timing remain unqualified') + } + $identity | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'identities.json') +} +# Validate the complete selection even if xUnit reported mismatches, so an early +# failure cannot hide missing reports, empty scopes or stale cardinalities. +$identity = Get-Content -LiteralPath (Join-Path $output 'identities.json') -Raw | ConvertFrom-Json +if ($identity.schema -ne 1 -or $identity.reference -cne 'MC68040UM' -or $identity.softwareReferenceExecuted -ne $false -or + $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.cpuCommittedTree -notmatch '^[a-f0-9]{40}$' -or + @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 2 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 2 -or + @($identity.assemblies).Count -ne 2) { throw '040 access-frame input identity is missing or incomplete' } +foreach ($input in $identity.inputs) { + $path = Join-Path $repo $input.file + if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','scripts/test-copper68k-040-access-frames.ps1') -or + $input.sha256 -cne (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()) { throw '040 access-frame fixture/command identity differs' } +} +$cpuFiles = @(& git -C $repo ls-files 'Copper68k/*') +if (@($identity.cpuSourceFiles).Count -ne $cpuFiles.Count -or @($identity.cpuSourceFiles.file | Sort-Object -Unique).Count -ne $cpuFiles.Count) { throw '040 access-frame CPU source selection differs' } +foreach ($source in $identity.cpuSourceFiles) { + if ($source.file -notin $cpuFiles -or $source.sha256 -cne (Get-FileHash -LiteralPath (Join-Path $repo $source.file) -Algorithm SHA256).Hash.ToLowerInvariant()) { throw '040 access-frame CPU source identity differs' } +} +foreach ($assembly in $identity.assemblies) { + if ($assembly.file -notin @('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') -or + $assembly.sha256 -cne (Get-FileHash -LiteralPath (Join-Path $repo $assembly.file) -Algorithm SHA256).Hash.ToLowerInvariant()) { throw '040 access-frame assembly identity differs' } +} +[xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw +$counters = $trx.TestRun.ResultSummary.Counters +if ([int]$counters.executed -ne 10 -or [int]$counters.total -ne 10 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (4 batches, 6 fixed examples)' +} +$totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} +foreach ($group in $expected.Keys) { + $report = Get-Content -LiteralPath (Join-Path $output "68040-$group.json") -Raw | ConvertFrom-Json + if ($report.schema -ne 1 -or $report.model -cne '68040' -or $report.group -cne $group -or $report.xunitBatches -ne 1) { throw "$group identity/schema differs" } + if ($report.logicalCases -ne $expected[$group].cases -or @($report.combinations.PSObject.Properties).Count -ne $expected[$group].combinations) { throw "$group cardinality differs" } + if (@($report.counts.PSObject.Properties).Count -ne 4) { throw "$group status selection differs" } + $caseTotal = 0 + foreach ($status in @($totals.Keys)) { + $value = $report.counts.$status + if ($null -eq $value -or $value -isnot [long] -and $value -isnot [int] -or $value -lt 0) { throw "$group invalid $status count" } + $caseTotal += $value; $totals[$status] += $value + } + if ($caseTotal -ne $report.logicalCases) { throw "$group status totals differ" } + $combinationTotals = @{passing=0; mismatching=0; unsupported=0; untested=0} + $expectedCombinations = @{} + if ($group -eq 'rte-access-continuations') { + foreach ($bank in @('user','ISP','MSP')) { + foreach ($form in @('CM-indirect','CM-postincrement','CM-predecrement','CM-displacement','CM-indexed','CM-absolute','CM-PC-relative','CU-pending-unimplemented','CP-pending-postinstruction')) { + $expectedCombinations["68040/RTE/format7/$form/bank=$bank"] = 32 + } + } + } else { + $formats = if ($group -eq 'rte-access-controls') { @(0,2,3) } else { @(7) } + $protocol = if ($group -eq 'rte-access-trace') { 'CT' } else { 'normal' } + $phases = if ($protocol -eq 'CT') { 3 } else { 2 } + foreach ($format in $formats) { + foreach ($bank in @('user','ISP','MSP')) { + foreach ($trace in @(0,0x8000,0x4000)) { + foreach ($vbr in @(0,0x10000)) { + foreach ($ea in @(0x5ffa,0x12345678)) { + foreach ($ssw in $(if ($format -eq 7) { @(0,0x0105,0x0505,0x0905) } else { @(0) })) { + $key = '68040/RTE/format{0}/{1}/bank={2}/T={3:X4}/VBR={4:X8}/EA={5:X8}/SSW={6:X4}' -f $format,$protocol,$bank,$trace,$vbr,$ea,$ssw + $expectedCombinations[$key] = 32 * $phases + } + } + } + } + } + } + } + foreach ($combination in $report.combinations.PSObject.Properties) { + if (-not $expectedCombinations.ContainsKey($combination.Name)) { throw "$group foreign combination" } + $combinationCases = 0 + foreach ($entry in $combination.Value.PSObject.Properties) { + if ($entry.Name -notin $totals.Keys -or $entry.Value -isnot [long] -and $entry.Value -isnot [int] -or $entry.Value -lt 0) { throw "$group invalid combination status" } + $combinationTotals[$entry.Name] += $entry.Value + $combinationCases += $entry.Value + } + if ($combinationCases -ne $expectedCombinations[$combination.Name]) { throw "$group combination cardinality differs" } + } + foreach ($status in @($totals.Keys)) { + if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } + } +} +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 10 -and + ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 +@{schema=1; model='68040'; logicalCases=30816; xunitBatches=4; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | + ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') +if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } From 76b63de9d915c3a8f7fc382b6dd62c73894d9192 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 09:34:32 +0300 Subject: [PATCH 031/211] Restore 68040 access frames and saved-address MOVEM continuations --- .../M68040MovemContinuationStateTests.cs | 134 ++++++++++++++++ Copper68k.Tests/Synthetic/README.md | 11 +- .../SyntheticExceptionControlTests.cs | 5 +- .../SyntheticM68040AccessFrameAuditTests.cs | 15 +- .../SyntheticM68040MovemContinuationTests.cs | 143 ++++++++++++++++++ .../M68kAdvancedTimingInterpreter.Movem.cs | 10 +- .../M68kAdvancedTimingInterpreter.Rte.cs | 64 ++++++++ Copper68k/M68kAdvancedTimingInterpreter.cs | 5 + Copper68k/M68kJitCore.cs | 3 + docs/COPPER68K_REFERENCE_QUALIFICATION.md | 95 +++++++++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 29 ++++ scripts/test-copper68k-040-access-frames.ps1 | 55 +++++-- scripts/test-copper68k-synthetic.ps1 | 14 +- 13 files changed, 546 insertions(+), 37 deletions(-) create mode 100644 Copper68k.Tests/M68040MovemContinuationStateTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs create mode 100644 Copper68k/M68kAdvancedTimingInterpreter.Rte.cs diff --git a/Copper68k.Tests/M68040MovemContinuationStateTests.cs b/Copper68k.Tests/M68040MovemContinuationStateTests.cs new file mode 100644 index 00000000..db118be5 --- /dev/null +++ b/Copper68k.Tests/M68040MovemContinuationStateTests.cs @@ -0,0 +1,134 @@ +using Copper68k.Tests.Synthetic; + +namespace Copper68k.Tests; + +public sealed class M68040MovemContinuationStateTests +{ + [Theory] + [InlineData("reset")] + [InlineData("interrupt")] + [InlineData("once")] + public void SavedAddressHasOneOwnerAndDefinedLifetime(string scenario) + { + var bus = Fixture(); + var core = M68kCoreFactory.Default.Create(M68kCpuModel.M68040, bus); + Initialize(core); + core.ExecuteInstruction(); + Assert.Equal(0x1000u, core.State.ProgramCounter); + Assert.True(((M68kAdvancedTimingInterpreter)core).HasPendingM68040MovemContinuation); + if (scenario == "reset") + { + core.Reset(0x1000, 0x473c); + core.State.A[0] = 0x4200; + core.ExecuteInstruction(); + Assert.Equal(0xdeadbeefu, core.State.D[1]); + } + else + { + if (scenario == "interrupt") + { + core.RequestInterrupt(3, 28 * 4); + core.ExecuteInstruction(); // handler NOP must not consume continuation + Assert.True(((M68kAdvancedTimingInterpreter)core).HasPendingM68040MovemContinuation); + core.ExecuteInstruction(); // handler RTE + Assert.Equal(0x1000u, core.State.ProgramCounter); + } + core.ExecuteInstruction(); + Assert.Equal(0xcafebabeu, core.State.D[1]); + Assert.Equal(0x1006u, core.State.ProgramCounter); + Assert.False(((M68kAdvancedTimingInterpreter)core).HasPendingM68040MovemContinuation); + core.State.ProgramCounter = 0x1000; + core.ExecuteInstruction(); // ordinary address calculation resumes + Assert.Equal(0xdeadbeefu, core.State.D[1]); + } + Assert.False(((M68kAdvancedTimingInterpreter)core).HasPendingM68040MovemContinuation); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void WarmCompiledMovemCannotBypassTheSavedAddress(bool v2) + { + // Exercise a warmed trace in both compiler versions. + var bus = Fixture(); + using var core = M68kJitCore.CreateM68040ForTesting(new ContinuationJitBus(bus), v2); + core.Reset(0x1000, 0x4700); core.State.A[0] = 0x4200; + core.State.CacheControlRegister = 1; + for (var n = 0; n < 300; n++) ExecuteOne(core); + core.State.ProgramCounter = 0x1000; + var warmedHits = Hits(core); + ExecuteOne(core); + Assert.True(Hits(core) > warmedHits, $"The MOVEM entry must execute through a warmed compiled trace: rejected={core.Counters.V2RejectedCandidates}, decode={core.Counters.V2RejectedDecode}, ea={core.Counters.V2RejectedUnsupportedEa}, operation={core.Counters.V2RejectedUnsupportedOperation}, chip={core.Counters.V2RejectedChipRam}, empty={core.Counters.V2RejectedEmpty}, fallback={core.Counters.FallbackInstructions}"); + Assert.Equal(0xdeadbeefu, core.State.D[1]); + core.State.ProgramCounter = 0x9000; + core.State.SetActiveStackPointer(0x4700); + ExecuteOne(core); // RTE into the already warmed MOVEM entry + Assert.Equal(0x1000u, core.State.ProgramCounter); + var before = core.Counters; + bus.Accesses.Clear(); + ExecuteOne(core); + Assert.Equal(0xcafebabeu, core.State.D[1]); + Assert.Equal(0x1006u, core.State.ProgramCounter); + Assert.Equal(before.TraceHits + before.V2TraceHits, Hits(core)); + Assert.Equal(before.FallbackInstructions + 1, core.Counters.FallbackInstructions); + Assert.DoesNotContain(bus.Accesses, x => !x.Write && x.Kind == M68kBusAccessKind.CpuDataRead && x.Address == (0x4210u)); + core.State.ProgramCounter = 0x1000; + ExecuteOne(core); + Assert.Equal(0xdeadbeefu, core.State.D[1]); + Assert.True(Hits(core) > before.TraceHits + before.V2TraceHits); + } + + private static void ExecuteOne(M68kJitCore core) + => Assert.Equal(1, core.ExecuteInstructions(1, core.State.Cycles + 1000, new ContinuationBoundary())); + + private sealed class ContinuationBoundary : IM68kBusAccessTraceBatchBoundary + { + public bool BeforeInstruction() => true; + public void AfterInstruction(long previous, long current) { } + public bool TryBeginBusAccessTraceBatch(M68kCpuState state, long target, out long batchTarget) + { batchTarget = target; return true; } + public void AfterBusAccessTraceBatch(long previous, long current, int count) { } + } + + private static long Hits(M68kJitCore core) => core.Counters.TraceHits + core.Counters.V2TraceHits; + private static void Initialize(IM68kCore core) + { + core.Reset(0x9000, 0x4700); core.State.A[0] = 0x4200; + } + private static SparseRecordingBus Fixture() + { + var bus = new SparseRecordingBus(); + bus.Initialize(0x1000, 0x4cf0, 2); // MOVEM.L (d8,A0,D0.W),D1 + bus.Initialize(0x1002, 2, 2); bus.Initialize(0x1004, 0x0010, 2); + bus.Initialize(0x1006, 0x4e71, 2); bus.Initialize(0x1008, 0x60f6, 2); + bus.Initialize(0x100a, 0x4e71, 2); + bus.Initialize(0x4000, 0xcafebabe, 4); bus.Initialize(0x4210, 0xdeadbeef, 4); + bus.Initialize(0x9000, 0x4e73, 2); bus.Initialize(0x9002, 0x4e71, 2); + bus.Initialize(28 * 4, 0x91c0, 4); + bus.Initialize(0x91c0, 0x4e71, 2); bus.Initialize(0x91c2, 0x4e73, 2); + bus.Initialize(0x4700, 0x2000, 2); bus.Initialize(0x4702, 0x1000, 4); + bus.Initialize(0x4706, 0x7008, 2); bus.Initialize(0x4708, 0x4000, 4); bus.Initialize(0x470c, 0x1005, 2); + return bus; + } + + private sealed class ContinuationJitBus(SparseRecordingBus bus) : IM68kBus, IM68kJitBus + { + public void ResetExternalDevices(long cycle) => bus.ResetExternalDevices(cycle); + public event Action? JitCodeRangeWritten; + public bool IsJitCodeAddress(uint address, int count, M68kBusAccessKind kind) => address >= 0x1000 && address + count <= 0x100c; + public bool IsJitReadOnlyCodeAddress(uint address, int count, M68kBusAccessKind kind) => false; + public ushort ReadJitCodeWord(uint address) => (ushort)bus.Peek(address, 2); + public uint GetJitCodePageGeneration(uint address) => 0; + public bool JitCodeRangeGenerationMatches(uint address, int count, uint first, uint last) => first == 0 && last == 0; + public bool TryCaptureJitCodeSnapshot(uint address, int count, out M68kJitCodeSnapshot snapshot) { snapshot = default; return false; } + public byte ReadByte(uint address, ref long cycle, M68kBusAccessKind kind) => bus.ReadByte(address, ref cycle, kind); + public ushort ReadWord(uint address, ref long cycle, M68kBusAccessKind kind) => bus.ReadWord(address, ref cycle, kind); + public uint ReadLong(uint address, ref long cycle, M68kBusAccessKind kind) => bus.ReadLong(address, ref cycle, kind); + public void WriteByte(uint address, byte value, ref long cycle, M68kBusAccessKind kind) + { bus.WriteByte(address, value, ref cycle, kind); JitCodeRangeWritten?.Invoke(address, 1); } + public void WriteWord(uint address, ushort value, ref long cycle, M68kBusAccessKind kind) + { bus.WriteWord(address, value, ref cycle, kind); JitCodeRangeWritten?.Invoke(address, 2); } + public void WriteLong(uint address, uint value, ref long cycle, M68kBusAccessKind kind) + { bus.WriteLong(address, value, ref cycle, kind); JitCodeRangeWritten?.Invoke(address, 4); } + } +} diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index b1b4bba4..c6f97919 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -226,11 +226,14 @@ it does not replace the deterministic synthetic gate or complete milestone 6. The independent 040 access-frame discovery command is `./scripts/test-copper68k-040-access-frames.ps1 -OutputDirectory artifacts/040-access-frame-audit`. -It currently **fails**: required format-7 normal/trace restoration disagrees, -and CM/CU/CP continuation fixtures remain explicitly untested. Short-frame -controls qualify the fixture; failed prerequisites leave later phases untested. +It currently **fails** on 192 explicit CU/CP continuation requirements. Normal, +CT trace and CM MOVEM restoration now pass 264,960 logical phases, including +short-frame controls. Five promoted batches run in ordinary CI. The MOVEM +matrices cover every legal opcode word and all 66 full-index structures; +separate state/JIT regressions check reset, interrupts and warmed V1/V2 traces. +Failed prerequisites leave later phases untested. The command checks exact selections, combinations, counts and source/assembly identities. Existing outputs must be validated with `-ValidateReportsOnly` or a fresh output directory used. It cannot pass merely by fixing normal/trace return. See the [reference record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-access-frame-restoration-discovery-2026-10-05) -for failed-before evidence, temporary probe controls and the remaining scope. +for failed-before evidence, mutation controls and the remaining scope. diff --git a/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs b/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs index f9358430..f293adbf 100644 --- a/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs @@ -27,11 +27,10 @@ public void RteFramesPrivilegeStackSelectionAndInvalidFormats(string modelId) // These legal restoration protocols remain implementation/qualification // gaps, not invalid formats. The separate 040 access-frame discovery // command reports its required gaps and fails until they are qualified. - var outside = modelId == "68040" ? format == 7 : - m.Model.FullIndex && modelId != "68060" && format is 9 or 10 or 11; + var outside = modelId is "68EC020" or "68020" or "68030" or "A1200" && format is 9 or 10 or 11; if (outside && supervisor) continue; var size = modelId == "68000" ? 6u : format == 0 ? 8u : modelId == "68010" && format == 8 ? 58u : - m.Model.FullIndex && modelId != "68060" && format == 1 ? 8u : m.Model.FullIndex && format == 2 ? 12u : modelId is "68040" or "68060" && format == 3 ? 12u : modelId == "68060" && format == 4 ? 16u : 0; + m.Model.FullIndex && modelId != "68060" && format == 1 ? 8u : m.Model.FullIndex && format == 2 ? 12u : modelId is "68040" or "68060" && format == 3 ? 12u : modelId == "68060" && format == 4 ? 16u : modelId == "68040" && format == 7 ? 60u : 0; if (!supervisor) SyntheticExecution.ExpectException(m, e, 8); else if (size == 0) SyntheticExecution.ExpectException(m, e, 14); else diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index ab8949e1..00944449 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -3,7 +3,7 @@ namespace Copper68k.Tests.Synthetic; -// Discovery gate, deliberately separate from the promoted ordinary CI matrix. +// Qualified ordinary-CI paths plus the remaining explicit discovery gate. // MC68040UM 8.4.6.2/7 is the expectation source, not WinUAE's non-MMU RTE. public sealed class SyntheticM68040AccessFrameAuditTests(ITestOutputHelper output) { @@ -13,13 +13,13 @@ public sealed class SyntheticM68040AccessFrameAuditTests(ITestOutputHelper outpu private static readonly string[] Banks = ["user", "ISP", "MSP"]; private const uint Target = 0x6000, TraceHandler = 0x9090; - [EnvironmentFact(Enable, "audit complete 040 format-7 restoration prerequisites"), Trait("Suite", "ReferenceDiscovery")] + [Fact, Trait("Suite", "Synthetic")] public void ExistingShortFramesValidateTheRestorationFixture() => Audit("rte-access-controls", [0, 2, 3], false); - [EnvironmentFact(Enable, "audit complete 040 format-7 restoration prerequisites"), Trait("Suite", "ReferenceDiscovery")] + [Fact, Trait("Suite", "Synthetic")] public void AccessFrameWithoutContinuationRestoresAndTracesFollowingInstruction() => Audit("rte-access-normal", [7], false); - [EnvironmentFact(Enable, "audit complete 040 format-7 restoration prerequisites"), Trait("Suite", "ReferenceDiscovery")] + [Fact, Trait("Suite", "Synthetic")] public void PendingTraceConvertsTheFrameBeforeReturningAndResuming() => Audit("rte-access-trace", [7], true); [EnvironmentFact(Enable, "audit complete 040 format-7 restoration prerequisites"), Trait("Suite", "ReferenceDiscovery")] @@ -27,12 +27,11 @@ public void RemainingContinuationProtocolsCannotDisappearFromTheGate() { var report = new CoverageBatch("68040", "rte-access-continuations"); // These are explicit prerequisite gaps, not asserted format-error behavior. - // CM needs full MOVEM replay fixtures; CU/CP need independently qualified - // pending-FPU state. FPU arithmetic being out of scope does not prove RTE. + // CU/CP need independently qualified pending-FPU state. The separate + // MOVEM matrices cover CM; FPU arithmetic being out of scope does not prove RTE. foreach (var bank in Banks) for (var ccr = 0; ccr < 32; ccr++) - foreach (var form in new[] { "CM-indirect", "CM-postincrement", "CM-predecrement", "CM-displacement", - "CM-indexed", "CM-absolute", "CM-PC-relative", "CU-pending-unimplemented", "CP-pending-postinstruction" }) + foreach (var form in new[] { "CU-pending-unimplemented", "CP-pending-postinstruction" }) report.Record($"68040/RTE/format7/{form}/bank={bank}/op=4E73/ccr={ccr:X2}", "untested", "Required continuation protocol has no independently qualified execution fixture yet"); report.Complete(output); diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs new file mode 100644 index 00000000..34d18fef --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs @@ -0,0 +1,143 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticM68040MovemContinuationTests(ITestOutputHelper output) +{ + private static readonly string[] Banks = ["user", "ISP", "MSP"]; + private static readonly ushort[] Masks = [0x0001, 0x0101, 0xffff]; + + [Fact, Trait("Suite", "Synthetic")] + public void EveryLegalMovemWordResumesFromItsCalculatedAddress() + { + var report = new CoverageBatch("68040", "rte-access-movem-opcodes"); + var m = NewMachine(); + foreach (var load in new[] { false, true }) + foreach (var width in new[] { 2, 4 }) + for (var mode = 2; mode < 8; mode++) + for (var register = 0; register < 8; register++) + { + var legal = mode is 2 or 5 or 6 || mode == (load ? 3 : 4) || mode == 7 && register <= (load ? 3 : 1); + if (!legal) continue; + foreach (var bank in Banks) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var mask in Masks) Run(m, report, load, width, mode, register, mask, bank, ccr, null); + } + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void FullIndexContinuationConsumesExtensionsWithoutReadingPointerChains() + { + var report = new CoverageBatch("68040", "rte-access-movem-full-index"); + var m = NewMachine(); + foreach (var (load, mode, register) in new[] { (false, 6, 0), (true, 6, 0), (true, 7, 3) }) + foreach (var width in new[] { 2, 4 }) + foreach (var index in IndexFixture.FullStructures()) + foreach (var bank in Banks) + for (var ccr = 0; ccr < 32; ccr++) Run(m, report, load, width, mode, register, 0x0101, bank, ccr, index); + report.Complete(output); + } + + private static void Run(SyntheticMachine m, CoverageBatch report, bool load, int width, int mode, int register, + ushort mask, string bank, int ccr, IndexFixture? index) + { + m.Reset(ccr); + m.Core.State.D[0] = 0x200; + var opcode = (ushort)((load ? 0x4c80 : 0x4880) | (width == 4 ? 0x40 : 0) | mode << 3 | register); + var words = new List { opcode, mask }; + var d = (uint[])m.Core.State.D.Clone(); var a = (uint[])m.Core.State.A.Clone(); + var options = new AddressOptions(SourceIndex: index ?? new(IndexRegister: 0, BriefDisplacement: 0x40)); + var fixture = new AddressingFixture(m, width, words, d, a, 0, options); + var address = fixture.Resolve(new OperandForm(mode, register), true); + if (mode == 4) address += (uint)width; // MOVEM's calculated predecrement EA precedes the first decrement. + var transfers = Enumerable.Range(0, 16).Where(bit => (mask & 1 << bit) != 0).ToArray(); + var targets = transfers.Select((_, n) => mode == 4 ? address - (uint)((n + 1) * width) : address + (uint)(n * width)).ToArray(); + for (var n = 0; n < targets.Length; n++) + { + m.InitializePhysical(targets[n], width == 2 ? 0x8001u + (uint)n : 0x89abcdefu + (uint)n, width); + m.InitializePhysical(targets[n] - 1, 0x96, 1); + m.InitializePhysical(targets[n] + (uint)width, 0x69, 1); + } + var frame = bank == "MSP" ? 0x7400u : 0x4700u; + for (var n = -16; n < 80; n++) m.InitializePhysical(unchecked(frame + (uint)n), (uint)(n ^ 0x5a), 1); + var savedSr = SyntheticM68040AccessFrameAuditTests.SavedStatus(bank, 0, ccr); + m.InitializePhysical(frame, savedSr, 2); m.InitializePhysical(frame + 2, SyntheticMachine.Code, 4); + m.InitializePhysical(frame + 6, 0x7008, 2); m.InitializePhysical(frame + 8, address, 4); + m.InitializePhysical(frame + 12, 0x1005, 2); + m.InitializePhysical(0x9000, 0x4e73, 2); m.InitializePhysical(0x9002, 0x4e71, 2); + _ = SyntheticExecution.Prepare(m, words); + m.Core.State.SetMasterStackPointer(0x7400); + if (bank == "MSP") m.Core.State.StatusRegister |= 0x1000; + // A partial load can overwrite its own base or index. Pointer memory may + // change in the handler. Restart uses the calculated EA, not these inputs. + if (load && mode is 2 or 6 && register != 7) m.Core.State.A[register] = 0x12348000; + if (mode == 6 || mode == 7 && register == 3) m.Core.State.D[index?.IndexRegister ?? 0] = 0x12345000; + m.Core.State.ProgramCounter = 0x9000; + var e = ArchitecturalExpectation.Capture(m); + var sequence = m.Core.State.ExceptionSequence; + e.Sr = savedSr; e.Pc = SyntheticMachine.Code; + e.A[7] = bank == "user" ? 0x7800u : frame + 60; + e.InactiveStackPointer = bank == "user" ? frame + 60 : 0x7800u; + if (bank == "MSP") e.MasterStackPointer = frame + 60; + e.ControlChecks["no exception"] = (s => s.ExceptionSequence, sequence); + var id = $"68040/MOVEM-CM/{(load ? "load" : "store")}/{width}/ea={mode}:{register}/{index?.Id ?? "brief"}/bank={bank}/mask={mask:X4}/op={opcode:X4}/ccr={ccr:X2}"; + if (!Step(m, e, report, id + "/RTE")) + { report.Record(id + "/MOVEM", "untested", "RTE prerequisite failed"); report.Record(id + "/NOP", "untested", "RTE prerequisite failed"); return; } + e.Pc = SyntheticMachine.Code + (uint)words.Count * 2; + var registers = e.D.Concat(e.A).ToArray(); + for (var n = 0; n < transfers.Length; n++) + { + var destination = mode == 4 ? 15 - transfers[n] : transfers[n]; + if (load) + { + var value = Read(e.Memory, targets[n], width); + if (width == 2) value = unchecked((uint)(int)(short)value); + if (destination < 8) e.D[destination] = value; + else e.A[destination - 8] = value; + } + else + { + var value = registers[destination]; + if (mode == 4 && destination == register + 8) value -= (uint)width; + e.Write(targets[n], value, width, m.Model); + } + } + if (mode is 3 or 4) e.A[register] = unchecked(address + (mode == 4 ? 0u - (uint)(transfers.Length * width) : (uint)(transfers.Length * width))); + if (bank == "MSP") e.MasterStackPointer = e.A[7]; + e.OperandAccessAddresses.UnionWith(targets); + e.ExpectedOperandTransfers = Enumerable.Repeat((!load, width), transfers.Length).ToList(); + if (index is { Full: true, Indirect: not 0 }) + { + // Only reads at the saved operand addresses are allowed during MOVEM; + // any reconstructed pointer read would add another data transfer. + e.ControlChecks["operand read count"] = (_ => (uint)m.Bus.Accesses.Count(x => !x.Write && x.Kind == M68kBusAccessKind.CpuDataRead), load ? (uint)transfers.Length : 0u); + } + m.Bus.Accesses.Clear(); + if (!Step(m, e, report, id + "/MOVEM")) + { report.Record(id + "/NOP", "untested", "MOVEM prerequisite failed"); return; } + e.Pc += 2; + Step(m, e, report, id + "/NOP"); + } + + private static uint Read(Dictionary memory, uint address, int width) + { + uint value = 0; + for (var n = 0; n < width; n++) value = (value << 8) | memory.GetValueOrDefault(address + (uint)n); + return value; + } + private static SyntheticMachine NewMachine() => new(ModelSpec.All.Single(x => x.Id == "68040")); + private static bool Step(SyntheticMachine m, ArchitecturalExpectation e, CoverageBatch report, string id) + { + try + { + m.Core.ExecuteInstruction(); + var mismatch = e.Verify(m); + report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); + return mismatch == null; + } + catch (Exception ex) + { report.Record(id, ex is UnsupportedM68kTimingException or UnsupportedM68040InstructionException ? "unsupported" : "mismatching", ex.Message); return false; } + } +} diff --git a/Copper68k/M68kAdvancedTimingInterpreter.Movem.cs b/Copper68k/M68kAdvancedTimingInterpreter.Movem.cs index a4603915..a88cf4db 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.Movem.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.Movem.cs @@ -5,7 +5,7 @@ namespace Copper68k; internal partial class M68kAdvancedTimingInterpreter { - private bool TryExecuteGeneralMovem(ushort opcode) + private bool TryExecuteGeneralMovem(ushort opcode, uint? continuationAddress = null) { if ((opcode & 0xfb80) != 0x4880) return false; var mode = (opcode >> 3) & 7; @@ -26,7 +26,13 @@ private bool TryExecuteGeneralMovem(ushort opcode) Span registers = stackalloc uint[16]; State.D.CopyTo(registers); State.A.CopyTo(registers[8..]); - var address = mode is 3 or 4 ? State.A[baseRegister] : ResolveMoveAddress(mode, baseRegister, size, opcode); + uint address; + if (continuationAddress.HasValue) + { + ConsumeMovemAddressExtensions(mode, baseRegister, opcode); + address = continuationAddress.Value; + } + else address = mode is 3 or 4 ? State.A[baseRegister] : ResolveMoveAddress(mode, baseRegister, size, opcode); for (var bit = 0; bit < 16; bit++) { if ((mask & (1 << bit)) == 0) continue; diff --git a/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs b/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs new file mode 100644 index 00000000..695f2765 --- /dev/null +++ b/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs @@ -0,0 +1,64 @@ +/* Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT */ +namespace Copper68k; + +internal partial class M68kAdvancedTimingInterpreter +{ + private (uint Pc, uint Address)? _m68040MovemContinuation; + internal bool HasPendingM68040MovemContinuation => _m68040MovemContinuation.HasValue; + + private bool TryRestoreM68040AccessFrame(uint frame, ushort sr, uint pc) + { + var continuation = ReadWord(frame + 12) & 0xf000; + // CU/CP need qualified pending-FPU state; multiple bits are undefined. + // Keep those explicit gaps on the existing rejected-frame path for now. + if (continuation is not (0 or 0x1000 or 0x2000)) return false; + var address = continuation == 0 ? 0u : ReadLong(frame + 8); + State.SetActiveStackPointer(unchecked(frame + 60)); + State.StatusRegister = sr; + State.ProgramCounter = pc; + if (continuation == 0x1000) + _m68040MovemContinuation = (pc, address); + if (continuation == 0x2000) + { + // MC68040UM 8.4.6.2: old SP+48 is the new format-2 frame. + // The instruction address is the saved EA, not the RTE opcode PC. + State.RecordException(9, pc, sr); + State.StatusRegister = (ushort)((sr | M68kCpuState.Supervisor) & ~0xc000); + PushLong(address); + PushWord(0x2024); + PushLong(pc); + PushWord(sr); + State.ProgramCounter = ReadLong(State.VectorBaseRegister + 36); + CompleteTiming(M68kInstructionTimingKey.IllegalInstruction); + } + else CompleteTiming(M68kInstructionTimingKey.Rte); + return true; + } + + private bool TryExecuteM68040MovemContinuation(ushort opcode) + { + if (_m68040MovemContinuation is not { } saved || saved.Pc != State.ProgramCounter) return false; + // Consume once at the restored instruction. An interrupt handler at a + // different PC must not consume the suspended instruction's saved EA. + _m68040MovemContinuation = null; + return TryExecuteGeneralMovem(opcode, saved.Address); + } + + private void ConsumeMovemAddressExtensions(int mode, int register, ushort opcode) + { + if (mode == 5 || mode == 7 && register is 0 or 2) { _ = FetchWord(); return; } + if (mode == 7 && register == 1) { _ = FetchLong(); return; } + if (mode != 6 && !(mode == 7 && register == 3)) return; + var extension = FetchWord(); + if ((extension & 0x100) == 0) return; + var baseSize = (extension >> 4) & 3; + var indirect = extension & 7; + if (baseSize == 0 || (extension & 8) != 0 || indirect == 4 || ((extension & 0x40) != 0 && indirect >= 4)) + throw new UnsupportedM68kTimingException(opcode, State.LastInstructionProgramCounter, _profile); + if (baseSize == 2) _ = FetchWord(); + if (baseSize == 3) _ = FetchLong(); + if ((indirect & 3) == 2) _ = FetchWord(); + if ((indirect & 3) == 3) _ = FetchLong(); + // Deliberately no index/base arithmetic or pointer-chain data access. + } +} diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index 2ffde9e0..c6bd1da3 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -4562,6 +4562,7 @@ private void ExecuteInstructionBody() throw new UnsupportedM68kTimingException(0, State.ProgramCounter, _profile); } + if (TryExecuteM68040MovemContinuation(opcode)) return; if (TryExecuteModule(opcode) || TryExecuteExtendedSystem(opcode) || TryExecutePackUnpack(opcode)) return; if (TryExecuteFastInstruction(opcode)) @@ -4612,6 +4613,7 @@ private bool TryExecuteHotBlock( if (maxInstructions <= 0 || State.Halted || State.Stopped || + HasPendingM68040MovemContinuation || (State.StatusRegister & 0xC000) != 0 || (State.ProgramCounter & 1) != 0 || !TryGetHotBlock(State.ProgramCounter, out var cacheSlot, out var block)) @@ -5573,6 +5575,7 @@ private void ExecuteFastDbcc(ushort opcode) public virtual void Reset(uint programCounter, uint stackPointer) { + _m68040MovemContinuation = null; Array.Clear(State.D); Array.Clear(State.A); State.ProgramCounter = programCounter; @@ -8651,6 +8654,8 @@ private void ExecuteRte() var restoredStatus = ReadWord(framePointer); var restoredPc = ReadLong(framePointer + 2); var format = ReadWord(framePointer + 6) >> 12; + if (format == 7 && _profile.Model == M68kAcceleratorModel.M68040 && + TryRestoreM68040AccessFrame(framePointer, restoredStatus, restoredPc)) return; var size = format switch { 0 or 1 => 8u, 2 => 12u, 3 when _profile.Model == M68kAcceleratorModel.M68040 => 12u, _ => 0u }; if (size == 0) { RaiseFormat0Exception(14, instructionPc, M68kInstructionTimingKey.FormatError); return; } diff --git a/Copper68k/M68kJitCore.cs b/Copper68k/M68kJitCore.cs index c9c43c0c..20d938f6 100644 --- a/Copper68k/M68kJitCore.cs +++ b/Copper68k/M68kJitCore.cs @@ -1155,6 +1155,9 @@ private int TryExecuteTrace( bool allowV2TraceHandoff = true) { _pendingFallbackReason = M68kJitFallbackReason.Unknown; + // An RTE MOVEM continuation must consume its saved EA before any + // warmed compiled route can perform ordinary address calculation. + if (_fallback is M68kAdvancedTimingInterpreter { HasPendingM68040MovemContinuation: true }) return 0; // Architectural tracing requires an exception after each instruction. if ((State.StatusRegister & M68kCpuState.Trace) != 0) return 0; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 8280e2f6..5e982ade 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -275,10 +275,12 @@ bus ordering, detailed fault sequencing, JIT and native ROM/media tests remain. focused presets do not replace the failing broad audit. - Generated 010 word-MOVE/MOVEA format-8 images now have the scoped continuation gate below. Long transfers, other instruction families, foreign silicon images, - external bus faults, 020/030 formats 9/A/B and 040 format 7 remain unqualified. The current advanced decoder does not implement all these + external bus faults, 020/030 formats 9/A/B and the remaining 040 format-7 + CU/CP and detailed fault protocols remain unqualified. The advanced decoder does not implement all these legal restoration protocols; this is an implementation gap, not invalid encoding. The [040 access-frame discovery gate](#040-access-frame-restoration-discovery-2026-10-05) - now exposes normal/trace failures and preserves CM/CU/CP prerequisites explicitly. + retains the original failed-before record. The latest checkpoint below promotes + normal/CT/CM synthetic returns and preserves CU/CP prerequisites explicitly. - Nested 000 address errors during exception stacking previously recursed on an odd SSP. This is corrected by the address-error double-fault slice above. External BERR and reset-vector fault signaling remain unavailable through the current @@ -2616,3 +2618,92 @@ executes them and fails as recorded above. Production CPU sources remain unchanged, so no new NuGet consumer validation or release is claimed or required for this audit-only checkpoint. Earlier full-suite, strict deterministic and external-reference records are retained unchanged; they do not certify format 7. + +## 040 normal, CT and MOVEM restoration checkpoint — 2026-10-05 + +This checkpoint implements normal, CT and CM format-7 RTE restoration in the +advanced 68040 interpreter. Normal return consumes the 60-byte frame. CT creates +its format-2 trace frame at old SP+48, using the saved EA as the instruction +address. CM preserves a one-shot PC/address pair and restarts MOVEM after EA +calculation: extension words are consumed without calculating an index or reading +an indirect pointer chain. Reset clears the pair; an interrupt handler at another +PC preserves it. Interpreter hot blocks and warmed V1/V2 JIT traces defer to the +interpreter until the continuation is consumed. Ordinary MOVEM calculation, +operand ordering and the existing timing policy remain unchanged. + +Authority is MC68040UM sections 8.4.6.2 and 8.4.6.7, as cited in the discovery +record. Applying the saved EA to every legal MOVEM addressing mode interprets +8.4.6.7's restart-after-EA rule; 8.4.6.2 specifically discusses indexed and +PC-relative calculation. The pinned WinUAE MMU generator is documentary agreement, +not an executed oracle or silicon qualification. Physical restart timing remains +unqualified. CU/CP still need independently qualified pending-FPU state; they are +implementation gaps, not invalid encodings or exclusions caused by the FPU +arithmetic boundary. Multiple continuation bits remain undefined/excluded. + +| Promoted batch | Passing logical phases | Architectural combinations | +| --- | ---: | ---: | +| Formats 0/2/3 controls | 6,912 | 108 | +| Format-7 normal return | 9,216 | 144 | +| Format-7 CT trace conversion/return | 13,824 | 144 | +| Every legal MOVEM opcode word, W/L, three masks | 120,960 | 1,260 | +| All 66 full-index structures, W/L, load/store/PC source | 114,048 | 1,188 | +| Total promoted | 264,960 | 2,844 | + +The two MOVEM matrices contain 40,320 and 38,016 scenarios respectively; each +scenario checks RTE, MOVEM and a following NOP. Opcode enumeration covers all +140 legal MOVEM first words. All 32 CCRs and user/ISP/MSP banks are included. +Expectations use the shared independent addressing fixture, check registers, +memory guards, operand transfer widths/order, exact extension consumption and +absence of pointer-chain reads. Five additional ordinary xUnit scenarios prove +reset/interrupt/one-shot lifetime and warmed V1/V2 compiled entry before and after +the fallback. V2 uses its bus-batch boundary and explicit cycle target; the test +requires a compiled execution witness, not interpreter/JIT agreement alone. + +Fresh discovery evidence is `artifacts/m6-rte-movem-discovery-qualified/`: +12 executed xUnit cases, 11 passing and one intentional CU/CP inventory failure. +The command exits 1 and records 265,152 logical phases: 264,960 passing, zero +mismatching/unsupported and 192 explicitly untested CU/CP requirements in six +combinations. `roadmapComplete=false`. The five promoted batches run in ordinary +CI; the remaining inventory executes only when explicitly requested. Detailed +validation faults, odd return PCs, throwaway-to-access frames, real access-fault +entry/writeback handling, other-model restoration and the broader reference gaps +remain required. No specialized regression is retired. + +Repaired failed-before evidence is `artifacts/m6-rte-current-probes/before/`: +the original CPU fails all 40,320/38,016 RTE prerequisites and leaves +80,640/76,032 dependent phases untested. Removing the pending-continuation JIT +guard fails both warmed compiler tests. Restoring source byte-for-byte passes +all seven focused matrix/state tests. Evidence is +`artifacts/m6-rte-current-probes/controls.json`. Earlier fixture/command development +failures remain recorded and are not relabeled as successful qualification. +All thirteen specific malformed-input/report controls still detect their intended +defect (`artifacts/m6-rte-continuation-preflight-controls/controls.json`). + +Full Release CPU validation passes 4,932 tests with eleven optional/opt-in skips +and zero failures, including all four qualified WinUAE presets +(`artifacts/m6-rte-continuation-full/full.trx`). The broad Basic discovery record +above remains unresolved; this checkpoint does not certify its disagreements. + +Fresh private, unpublished NuGet `1.5.2-synthetic-dev.54` validates the production +fix through isolated CopperScreen baseline +`d9beae8b88be24032221e3482942a249c03c27d3`: Release solution build has zero +warnings/errors, host 149 passed/six optional skips, disk 74 passed, separate +engine diagnostics 1,080 passed, native Workbench boot two passed and A1200 +AGA boot/persistence one passed without skips. Four restored assets select the +exact package and all four loaded CPU DLLs match its SHA-256 +`57570f04cc1bd2cbff2103ca5dfc0912e9f1e74de86ef1ce02a14d1c95b3e861`. +Package SHA-256 is +`7f0ef9ce3c2a645b5b9202bb3f54bd1273a2e5724ea639ec83cee95fa0103884`. +Consumer identities/results are under the isolated worktree's +`artifacts/rte-continuation-identities.json` and +`artifacts/rte-continuation-validation/`. Published packages remain immutable; +no package publication or root CopperScreen dependency change is included. +Milestone 6 and the full reference/consolidation goal remain **in progress**. + +The strict report gate verifies **14,911,576 passing logical cases in 579 reporting +batches**, with fresh pinned SingleStepTests (312,500 cases / 125 files) and +Musashi (536 programs / 88 explicit exclusions across all eight profiles). +Evidence is `artifacts/m6-rte-continuation-gate-final.log` and the full-run +`summary.json`; `roadmapComplete=false`. An initial verifier invocation incorrectly +requested zero-count 040-only reports for other models and failed; the corrected +model-scoped selection is the successful invocation recorded here. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 304e63c7..338a98f3 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1170,3 +1170,32 @@ skips. The complete requested discovery executes all four batches and six fixed examples, exits 1 and records exactly the required failures/gaps. All thirteen specific malformed-input/report controls are detected. No earlier green gate is relabeled as advanced-restoration coverage. + +### Milestone 6 normal/CT/CM implementation — 2026-10-05 + +The advanced 040 interpreter now restores normal and CT format-7 frames and +restarts CM MOVEM from its saved EA without recomputing indexes or reading pointer +chains. Five ordinary-CI batches pass 264,960 logical phases, including every +legal MOVEM word and all full-index structures. Separate reset/interrupt/one-shot +and warmed V1/V2 JIT tests prove continuation ownership. Original-CPU and omitted +JIT-guard probes detect the defects; restored focused tests pass. The exact +selection/report corruption controls remain effective. + +The explicit discovery command still fails: 192 required CU/CP cases remain +untested. Detailed frame faults, real access-fault entry/writeback handling, +other-model advanced restoration, independent combination qualification and +consolidation remain required. Milestone 6 is **in progress**, with +`roadmapComplete=false`; this does not narrow the accepted completion gate. + +Full Release CPU validation passes 4,932 tests, eleven optional/opt-in skips and +zero failures with all four qualified presets enabled. Isolated unpublished +`1.5.2-synthetic-dev.54` validates CopperScreen through NuGet: clean Release build, +host 149/six skips, disk 74, engine 1,080, native Workbench two and A1200 one +without skips. Package/assets/loaded DLL identities match. Published versions and +the root CopperScreen checkout are untouched. See the +[reference record](COPPER68K_REFERENCE_QUALIFICATION.md#040-normal-ct-and-movem-restoration-checkpoint--2026-10-05) +for architectural authority, exact coverage, evidence and remaining scope. + +The strict gate verifies 14,911,576 passing logical cases in 579 reporting batches, +with fresh pinned SingleStepTests (312,500 cases / 125 files) and Musashi +(536 programs / 88 exclusions across eight profiles); roadmapComplete=false. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index ec219f60..fa6b0c8b 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -11,7 +11,9 @@ $expected = [ordered]@{ 'rte-access-controls' = @{cases=6912; combinations=108} 'rte-access-normal' = @{cases=9216; combinations=144} 'rte-access-trace' = @{cases=13824; combinations=144} - 'rte-access-continuations' = @{cases=864; combinations=27} + 'rte-access-movem-opcodes' = @{cases=120960; combinations=1260} + 'rte-access-movem-full-index' = @{cases=114048; combinations=1188} + 'rte-access-continuations' = @{cases=192; combinations=6} } $testExit = 0 if (-not $ValidateReportsOnly) { @@ -24,7 +26,7 @@ if (-not $ValidateReportsOnly) { $saved[$name] = [Environment]::GetEnvironmentVariable($name) [Environment]::SetEnvironmentVariable($name, $settings[$name]) } - & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output $testExit = $LASTEXITCODE } finally { foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } @@ -33,17 +35,17 @@ if (-not $ValidateReportsOnly) { schema=1; reference='MC68040UM'; url='https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf' sections=@('8.4.1','8.4.3','8.4.4','8.4.6.2','8.4.6.7'); softwareReferenceExecuted=$false sourceCommit=(& git -C $repo rev-parse HEAD); cpuCommittedTree=(& git -C $repo rev-parse HEAD:Copper68k) - cpuSourceFiles=@(& git -C $repo ls-files 'Copper68k/*') | ForEach-Object { + cpuSourceFiles=@(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } - inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','scripts/test-copper68k-040-access-frames.ps1') | ForEach-Object { + inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } assemblies=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } limitations=@('Canonical synthetic frames, not hardware captures or enabled-MMU access faults', - 'CM/CU/CP continuation inventory remains untested and fails the gate', + 'CU/CP continuation inventory remains untested and fails the gate', 'Multiple continuation bits are architecturally undefined and excluded', 'Detailed fault validation, odd return PCs, throwaway-to-access frames and physical timing remain unqualified') } @@ -54,14 +56,14 @@ if (-not $ValidateReportsOnly) { $identity = Get-Content -LiteralPath (Join-Path $output 'identities.json') -Raw | ConvertFrom-Json if ($identity.schema -ne 1 -or $identity.reference -cne 'MC68040UM' -or $identity.softwareReferenceExecuted -ne $false -or $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.cpuCommittedTree -notmatch '^[a-f0-9]{40}$' -or - @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 2 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 2 -or + @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 3 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 3 -or @($identity.assemblies).Count -ne 2) { throw '040 access-frame input identity is missing or incomplete' } foreach ($input in $identity.inputs) { $path = Join-Path $repo $input.file - if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','scripts/test-copper68k-040-access-frames.ps1') -or + if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1') -or $input.sha256 -cne (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()) { throw '040 access-frame fixture/command identity differs' } } -$cpuFiles = @(& git -C $repo ls-files 'Copper68k/*') +$cpuFiles = @(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') if (@($identity.cpuSourceFiles).Count -ne $cpuFiles.Count -or @($identity.cpuSourceFiles.file | Sort-Object -Unique).Count -ne $cpuFiles.Count) { throw '040 access-frame CPU source selection differs' } foreach ($source in $identity.cpuSourceFiles) { if ($source.file -notin $cpuFiles -or $source.sha256 -cne (Get-FileHash -LiteralPath (Join-Path $repo $source.file) -Algorithm SHA256).Hash.ToLowerInvariant()) { throw '040 access-frame CPU source identity differs' } @@ -72,8 +74,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 10 -or [int]$counters.total -ne 10 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (4 batches, 6 fixed examples)' +if ([int]$counters.executed -ne 12 -or [int]$counters.total -ne 12 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (6 batches, 6 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -92,10 +94,35 @@ foreach ($group in $expected.Keys) { $expectedCombinations = @{} if ($group -eq 'rte-access-continuations') { foreach ($bank in @('user','ISP','MSP')) { - foreach ($form in @('CM-indirect','CM-postincrement','CM-predecrement','CM-displacement','CM-indexed','CM-absolute','CM-PC-relative','CU-pending-unimplemented','CP-pending-postinstruction')) { + foreach ($form in @('CU-pending-unimplemented','CP-pending-postinstruction')) { $expectedCombinations["68040/RTE/format7/$form/bank=$bank"] = 32 } } + } elseif ($group -in @('rte-access-movem-opcodes','rte-access-movem-full-index')) { + foreach ($load in @($false,$true)) { + foreach ($width in @(2,4)) { + foreach ($mode in 2..7) { + foreach ($register in 0..7) { + if ($mode -notin @(2,5,6) -and $mode -ne $(if ($load) {3} else {4}) -and -not ($mode -eq 7 -and $register -le $(if ($load) {3} else {1}))) { continue } + $indexes=@('brief') + $masks=@(1,0x0101,0xffff) + if ($group -eq 'rte-access-movem-full-index') { + if (-not (($mode -eq 6 -and $register -eq 0) -or ($load -and $mode -eq 7 -and $register -eq 3))) { continue } + $indexes=@() + $masks=@(0x0101) + foreach ($bs in @($false,$true)) { foreach ($suppressed in @($false,$true)) { foreach ($bd in 1..3) { foreach ($iis in @(0,1,2,3,5,6,7)) { + if ($suppressed -and $iis -ge 5) { continue } + $indexes += "full/bs=$bs/is=$suppressed/bd=$bd/iis=$iis" + } } } } + } + foreach ($index in $indexes) { foreach ($bank in @('user','ISP','MSP')) { foreach ($mask in $masks) { + $key = '68040/MOVEM-CM/{0}/{1}/ea={2}:{3}/{4}/bank={5}/mask={6:X4}' -f $(if ($load) {'load'} else {'store'}),$width,$mode,$register,$index,$bank,$mask + $expectedCombinations[$key] = 96 + } } } + } + } + } + } } else { $formats = if ($group -eq 'rte-access-controls') { @(0,2,3) } else { @(7) } $protocol = if ($group -eq 'rte-access-trace') { 'CT' } else { 'normal' } @@ -116,7 +143,7 @@ foreach ($group in $expected.Keys) { } } foreach ($combination in $report.combinations.PSObject.Properties) { - if (-not $expectedCombinations.ContainsKey($combination.Name)) { throw "$group foreign combination" } + if (-not $expectedCombinations.ContainsKey($combination.Name)) { throw "$group foreign combination: $($combination.Name); first expected: $($expectedCombinations.Keys | Sort-Object | Select-Object -First 1)" } $combinationCases = 0 foreach ($entry in $combination.Value.PSObject.Properties) { if ($entry.Name -notin $totals.Keys -or $entry.Value -isnot [long] -and $entry.Value -isnot [int] -or $entry.Value -lt 0) { throw "$group invalid combination status" } @@ -129,8 +156,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 10 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 12 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=30816; xunitBatches=4; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=265152; xunitBatches=6; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index e856ea5f..2e495007 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -115,8 +115,8 @@ try { 'system-movec'=2432; 'system-moves'=$(if ($model -in @('68000','68010')) {10656} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {11052}); 'system-moves-invalid-operands'=7296; - 'system-rte'=$(if ($model -in @('68000','68010','68060')) {2048} elseif ($model -in @('68020','68030','68EC020','A1200')) {1888} elseif ($model -in @('68040')) {2016}); - 'system-rtm'=24594; + 'system-rte'=$(if ($model -in @('68000','68010','68060')) {2048} elseif ($model -in @('68020','68030','68EC020','A1200')) {1888} elseif ($model -in @('68040')) {2080}); + 'system-rtm'=24594; 'system-stack'=6080; 'system-status'=$(if ($model -in @('68000','68010')) {16540} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {16936}); 'system-status-invalid-operands'=2432; @@ -145,6 +145,11 @@ try { $expected['system-move-word-restart-invalid'] = 224 } if ($model -eq '68040') { + $expected['rte-access-controls'] = 6912 + $expected['rte-access-normal'] = 9216 + $expected['rte-access-trace'] = 13824 + $expected['rte-access-movem-opcodes'] = 120960 + $expected['rte-access-movem-full-index'] = 114048 $expected['system-mmu-disabled'] = 34850 $expected['system-linef-pc-restore'] = 6464 } @@ -235,15 +240,16 @@ try { foreach ($row in $inventory.combinations) { if ($row.Milestone -le 5) { $row.status = 'passing'; $row.note = 'Passing named semantic scenario batches; consult qualification boundaries for untested physical/internal protocols.' } } - $inventory | Add-Member -NotePropertyName qualificationBoundaries -NotePropertyValue @('RTE fault restart/internal formats remain untested: 010 long/non-MOVE/foreign frame8 restart, 020/030 frames9/A/B, 040 frame7', 'BKPT external replacement responder is unavailable', 'MOVES physical function-code spaces and LPSTOP CPU-space broadcast are unqualified', 'CALLM/RTM type1 protocol uses an internal synthetic responder; normal public buses have none', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands are unavailable') + $inventory | Add-Member -NotePropertyName qualificationBoundaries -NotePropertyValue @('RTE fault restart/internal formats remain untested: 010 long/non-MOVE/foreign frame8 restart, 020/030 frames9/A/B, 040 frame7 CU/CP and detailed access-fault restoration', 'BKPT external replacement responder is unavailable', 'MOVES physical function-code spaces and LPSTOP CPU-space broadcast are unqualified', 'CALLM/RTM type1 protocol uses an internal synthetic responder; normal public buses have none', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands are unavailable') $inventory | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'qualified-inventory.json') @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=$logicalBatches; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; m68040DisabledMmuInstructionGate='passing'; m68000AddressErrorDoubleFaultGate='passing'; m68010Format8StructureGate='passing'; m68010WordMoveRestartGate='passing'; roadmapComplete=$false; seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; + m68040AccessFrameScope='Synthetic normal/CT/CM returns, every legal MOVEM word and full-index structure; CU/CP, detailed fault validation, real fault entry and physical timing remain unqualified'; m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; marked private word-MOVE/MOVEA images resume their faulted cycle; other opaque restart/input state, long transfers, RMW and physical timing remain unqualified'; m68000DoubleFaultScope='Address-error entry/handler faults; reset-only recovery; external BERR/reset-vector faults unavailable through the current public bus API'; - unavailableSystemCoverage=@('010 long/non-MOVE/external bus-fault restart and 020/030 internal restart', '020/030 coprocessor midinstruction restoration', '040 access-fault pending trace/writeback restoration', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands'); + unavailableSystemCoverage=@('010 long/non-MOVE/external bus-fault restart and 020/030 internal restart', '020/030 coprocessor midinstruction restoration', '040 access-fault CU/CP, detailed frame-validation/fault entry and writeback-handler qualification', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands'); timing='Semantic gate; timing policy and physical qualification remain separate'} | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'summary.json') Write-Host "Synthetic milestones 1-5 semantic gates: $logicalCases logical cases; reports at $output" From 9fa9f84ade885165cf4d69992f6de50e975fe041 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 10:12:40 +0300 Subject: [PATCH 032/211] Preserve 68040 pending FPU vectors across access-frame returns --- .../M68040FpuContinuationStateTests.cs | 185 ++++++++++++++++++ Copper68k.Tests/Synthetic/README.md | 12 +- .../SyntheticM68040AccessFrameAuditTests.cs | 19 +- .../SyntheticM68040FpuContinuationTests.cs | 79 ++++++++ Copper68k/M68040PendingFpuExceptions.cs | 37 ++++ Copper68k/M68040Support.cs | 2 + .../M68kAdvancedTimingInterpreter.Rte.cs | 32 ++- Copper68k/M68kAdvancedTimingInterpreter.cs | 1 + Copper68k/M68kCore.cs | 2 + Copper68k/M68kJitCore.cs | 2 + docs/COPPER68K_REFERENCE_QUALIFICATION.md | 99 +++++++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 27 +++ scripts/test-copper68k-040-access-frames.ps1 | 42 ++-- scripts/test-copper68k-synthetic.ps1 | 8 +- 14 files changed, 506 insertions(+), 41 deletions(-) create mode 100644 Copper68k.Tests/M68040FpuContinuationStateTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs create mode 100644 Copper68k/M68040PendingFpuExceptions.cs diff --git a/Copper68k.Tests/M68040FpuContinuationStateTests.cs b/Copper68k.Tests/M68040FpuContinuationStateTests.cs new file mode 100644 index 00000000..62069488 --- /dev/null +++ b/Copper68k.Tests/M68040FpuContinuationStateTests.cs @@ -0,0 +1,185 @@ +using Copper68k.Tests.Synthetic; +using CopperFloat; + +namespace Copper68k.Tests; + +public sealed class M68040FpuContinuationStateTests +{ + [Theory] + [InlineData("accurate", false, "resume")] + [InlineData("accurate", true, "resume")] + [InlineData("accurate", false, "nested")] + [InlineData("accurate", true, "nested")] + [InlineData("accurate", false, "reset")] + [InlineData("accurate", true, "reset")] + [InlineData("accurate", false, "redirect")] + [InlineData("accurate", true, "redirect")] + [InlineData("v1", false, "resume")] + [InlineData("v1", true, "resume")] + [InlineData("v2", false, "resume")] + [InlineData("v2", true, "resume")] + public void DeliveryRetainsItsVectorUntilCompletion(string engine, bool post, string scenario) + { + var bus = new DeliveryBus(); + var memory = bus.Memory; + memory.Initialize(0x1000, post ? 0xf210u : 0xf200u, 2); + memory.Initialize(0x1002, post ? 0x6880u : 0x040eu, 2); // FMOVE.X FP1,(A0) / FSIN.X FP1,FP0 + memory.Initialize(0x1004, 0x4e71, 2); memory.Initialize(0x1006, 0x60f8, 2); + memory.Initialize(0x9000, 0x4e73, 2); memory.Initialize(0x9002, 0x4e71, 2); + memory.Initialize(11 * 4, 0x6800, 4); memory.Initialize(55 * 4, 0x6800, 4); + memory.Initialize(2 * 4, 0x6900, 4); memory.Initialize(0x6800, 0x4e73, 2); + IM68kCore core = engine == "accurate" ? M68kCoreFactory.Default.Create(M68kCpuModel.M68040, bus) + : M68kJitCore.CreateM68040ForTesting(bus, engine == "v2"); + using var disposable = core as IDisposable; + core.Reset(0x1000, 0x8000); core.State.A[0] = 0x4200; core.State.CacheControlRegister = 1; + core.State.M68040Fpu.FP[1] = ExtF80.FromBits(0x3fff, 0x8000000000000000); + var format = post ? 3 : 2; var vector = post ? 55 : 11; + for (var n = 0; n < (engine == "accurate" ? 1 : 300); n++) + { core.State.ProgramCounter = 0x1000; core.State.SetActiveStackPointer(0x8000); ExecuteOne(core); } + Assert.Null(core.State.M68040PendingFpuExceptions.Find(format, 0x1004)); + core.State.ProgramCounter = 0x1000; core.State.SetActiveStackPointer(0x8000); + var hits = core is M68kJitCore jit ? jit.Counters.TraceHits + jit.Counters.V2TraceHits : 0; + ExecuteOne(core); + if (core is M68kJitCore warmed) + Assert.True(warmed.Counters.TraceHits + warmed.Counters.V2TraceHits > hits, $"Require an actual warmed compiled entry: compiled={warmed.Counters.CompiledTraces}, rejected={warmed.Counters.V2RejectedCandidates}, operation={warmed.Counters.V2RejectedUnsupportedOperation}, ea={warmed.Counters.V2RejectedUnsupportedEa}, hits={warmed.Counters.TraceHits}/{warmed.Counters.V2TraceHits}, fallback={warmed.Counters.FallbackInstructions}, exits={warmed.Counters.V2SideExits}"); + core.State.ProgramCounter = 0x1000; core.State.SetActiveStackPointer(0x8000); + // Unsupported extended data creates the documented post-instruction vector 55. + if (post) core.State.M68040Fpu.FP[1] = ExtF80.FromBits(0, 1); + bus.FaultVectorAddress = (uint)vector * 4; + ExecuteOne(core); + var pending = core.State.M68040PendingFpuExceptions.Find(format, 0x1004); + Assert.NotNull(pending); Assert.Equal(vector, pending.Vector); + Assert.Equal(2, core.State.LastExceptionVector); + // This injected internal fault uses the existing approximate entry. It + // does not qualify hardware format-7 generation or enabled MMU behavior. + if (scenario == "reset") + { + core.Reset(0x1004, 0x8000); + Assert.Null(core.State.M68040PendingFpuExceptions.Find(format, 0x1004)); + return; + } + if (scenario == "nested") + { + core.State.ProgramCounter = 0x1000; core.State.SetActiveStackPointer(0x7a00); + ExecuteOne(core); // Equal-valued nested delivery completes independently. + Assert.Same(pending, core.State.M68040PendingFpuExceptions.Find(format, 0x1004)); + } + // FPU context/register changes cannot change integer delivery ownership. + core.State.M68040Fpu.Reset(); core.State.M68040Fpu.Fpsr = 0x8000; + Assert.Same(pending, core.State.M68040PendingFpuExceptions.Find(format, 0x1004)); + var returnPc = scenario == "redirect" ? 0x6000u : 0x1004u; + memory.Initialize(0x8000, 0x2000, 2); memory.Initialize(0x8002, returnPc, 4); + memory.Initialize(0x8006, 0x7008, 2); memory.Initialize(0x8008, 0x4200, 4); + memory.Initialize(0x800c, post ? 0x8005u : 0x4005u, 2); + core.State.ProgramCounter = 0x9000; core.State.SetActiveStackPointer(0x8000); + ExecuteOne(core); + Assert.Equal(vector, core.State.LastExceptionVector); Assert.Equal(0x6800u, core.State.ProgramCounter); + Assert.Equal(0x8030u, core.State.A[7]); + Assert.Equal((uint)(format << 12 | vector * 4), memory.Peek(0x8036, 2)); + Assert.Equal(returnPc, memory.Peek(0x8032, 4)); + Assert.Equal(0x4200u, memory.Peek(0x8038, 4)); + Assert.Null(core.State.M68040PendingFpuExceptions.Find(format, 0x1004)); + } + + [Fact] + public void ForeignCpFrameWithoutDeliveryStateIsAnExplicitGap() + { + var bus = new SparseRecordingBus(); + bus.Initialize(0x1000, 0x4e73, 2); bus.Initialize(0x8000, 0x2000, 2); + bus.Initialize(0x8002, 0x6000, 4); bus.Initialize(0x8006, 0x7008, 2); + bus.Initialize(0x8008, 0x4200, 4); bus.Initialize(0x800c, 0x8005, 2); + var cpu = M68kCoreFactory.Default.Create(M68kCpuModel.M68040, bus); + cpu.Reset(0x1000, 0x8000); var sequence = cpu.State.ExceptionSequence; + Assert.Throws(() => cpu.ExecuteInstruction()); + Assert.Equal(sequence, cpu.State.ExceptionSequence); Assert.Equal(0x8000u, cpu.State.A[7]); + Assert.DoesNotContain(bus.Accesses, a => a.Write); + } + + [Theory] + [InlineData("accurate")] + [InlineData("v1")] + [InlineData("v2")] + public void PostInstructionReturnDoesNotRepeatACompletedOperandStore(string engine) + { + var bus = new DeliveryBus(); var memory = bus.Memory; + memory.Initialize(0x1000, 0xf210, 2); memory.Initialize(0x1002, 0x6480, 2); // FMOVE.S FP1,(A0) + memory.Initialize(0x1004, 0x4e71, 2); memory.Initialize(0x1006, 0x60f8, 2); + memory.Initialize(0x9000, 0x4e73, 2); memory.Initialize(0x9002, 0x4e71, 2); + memory.Initialize(53 * 4, 0x6800, 4); memory.Initialize(2 * 4, 0x6900, 4); + IM68kCore core = engine == "accurate" ? M68kCoreFactory.Default.Create(M68kCpuModel.M68040, bus) + : M68kJitCore.CreateM68040ForTesting(bus, engine == "v2"); + using var disposable = core as IDisposable; + core.Reset(0x1000, 0x8000); core.State.A[0] = 0x4200; core.State.CacheControlRegister = 1; + core.State.M68040Fpu.FP[1] = ExtF80.FromBits(0x3fff, 0x8000000000000000); + for (var n = 0; n < (engine == "accurate" ? 1 : 300); n++) + { core.State.ProgramCounter = 0x1000; ExecuteOne(core); } + core.State.ProgramCounter = 0x1000; + var hits = core is M68kJitCore jit ? jit.Counters.TraceHits + jit.Counters.V2TraceHits : 0; + ExecuteOne(core); + if (core is M68kJitCore warmed) + Assert.True(warmed.Counters.TraceHits + warmed.Counters.V2TraceHits > hits); + core.State.ProgramCounter = 0x1000; + core.State.M68040Fpu.FP[1] = ExtF80.FromBits(0x7ffe, ulong.MaxValue); + bus.FaultVectorAddress = 53 * 4; + memory.Accesses.Clear(); ExecuteOne(core); + // Fixed IEEE format boundary: largest finite extended value overflows + // single precision to positive infinity before the post-exception fetch. + Assert.Equal(0x7f800000u, memory.Peek(0x4200, 4)); + Assert.Single(memory.Accesses.Where(a => a.Write && a.Address == 0x4200)); + Assert.Equal(53, core.State.M68040PendingFpuExceptions.Find(3, 0x1004)!.Vector); + memory.Initialize(0x8000, 0x2000, 2); memory.Initialize(0x8002, 0x1004, 4); + memory.Initialize(0x8006, 0x7008, 2); memory.Initialize(0x8008, 0x4200, 4); + memory.Initialize(0x800c, 0x8005, 2); + core.State.ProgramCounter = 0x9000; core.State.SetActiveStackPointer(0x8000); + memory.Accesses.Clear(); ExecuteOne(core); + Assert.Equal(53, core.State.LastExceptionVector); Assert.Equal(0x6800u, core.State.ProgramCounter); + Assert.Equal(0x7f800000u, memory.Peek(0x4200, 4)); + Assert.DoesNotContain(memory.Accesses, a => a.Write && a.Address == 0x4200); + Assert.DoesNotContain(memory.Accesses, a => a.Address == 0x1000 && a.Kind == M68kBusAccessKind.CpuInstructionFetch); + Assert.Null(core.State.M68040PendingFpuExceptions.Find(3, 0x1004)); + } + + private static void ExecuteOne(IM68kCore core) + { + if (core is M68kJitCore jit) Assert.Equal(1, jit.ExecuteInstructions(1, jit.State.Cycles + 1000, new Boundary())); + else core.ExecuteInstruction(); + } + private sealed class Boundary : IM68kBusAccessTraceBatchBoundary, IM68kPureCpuTraceBatchBoundary + { + public bool BeforeInstruction() => true; + public void AfterInstruction(long previous, long current) { } + public bool TryBeginBusAccessTraceBatch(M68kCpuState state, long target, out long batchTarget) + { batchTarget = target; return true; } + public void AfterBusAccessTraceBatch(long previous, long current, int count) { } + public bool TryBeginPureCpuTraceBatch(M68kCpuState state, long target, out long batchTarget) + { batchTarget = target; return true; } + public void AfterPureCpuTraceBatch(long previous, long current, int count) { } + } + private sealed class DeliveryBus : IM68kBus, IM68kJitBus + { + public SparseRecordingBus Memory { get; } = new(); + public uint? FaultVectorAddress { get; set; } + public event Action? JitCodeRangeWritten; + public bool IsJitCodeAddress(uint address, int count, M68kBusAccessKind kind) => address >= 0x1000 && address + count <= 0x1008; + public bool IsJitReadOnlyCodeAddress(uint address, int count, M68kBusAccessKind kind) => false; + public ushort ReadJitCodeWord(uint address) => (ushort)Memory.Peek(address, 2); + public uint GetJitCodePageGeneration(uint address) => 0; + public bool JitCodeRangeGenerationMatches(uint address, int count, uint first, uint last) => first == 0 && last == 0; + public bool TryCaptureJitCodeSnapshot(uint address, int count, out M68kJitCodeSnapshot snapshot) { snapshot = default; return false; } + public void ResetExternalDevices(long cycle) => Memory.ResetExternalDevices(cycle); + public byte ReadByte(uint address, ref long cycle, M68kBusAccessKind kind) => Memory.ReadByte(address, ref cycle, kind); + public ushort ReadWord(uint address, ref long cycle, M68kBusAccessKind kind) => Memory.ReadWord(address, ref cycle, kind); + public uint ReadLong(uint address, ref long cycle, M68kBusAccessKind kind) + { + if (address == FaultVectorAddress) + { FaultVectorAddress = null; throw new M68040MmuFaultException(new(address, kind, false, 0x400, 0x1004)); } + return Memory.ReadLong(address, ref cycle, kind); + } + public void WriteByte(uint address, byte value, ref long cycle, M68kBusAccessKind kind) + { Memory.WriteByte(address, value, ref cycle, kind); JitCodeRangeWritten?.Invoke(address, 1); } + public void WriteWord(uint address, ushort value, ref long cycle, M68kBusAccessKind kind) + { Memory.WriteWord(address, value, ref cycle, kind); JitCodeRangeWritten?.Invoke(address, 2); } + public void WriteLong(uint address, uint value, ref long cycle, M68kBusAccessKind kind) + { Memory.WriteLong(address, value, ref cycle, kind); JitCodeRangeWritten?.Invoke(address, 4); } + } +} diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index c6f97919..839c21e7 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -226,11 +226,15 @@ it does not replace the deterministic synthetic gate or complete milestone 6. The independent 040 access-frame discovery command is `./scripts/test-copper68k-040-access-frames.ps1 -OutputDirectory artifacts/040-access-frame-audit`. -It currently **fails** on 192 explicit CU/CP continuation requirements. Normal, -CT trace and CM MOVEM restoration now pass 264,960 logical phases, including -short-frame controls. Five promoted batches run in ordinary CI. The MOVEM +It currently **fails** on 576 explicit fault/context requirements. Normal, +CT, CM, CU and CP restoration now pass 375,552 logical phases, including +short-frame controls. Seven promoted batches run in ordinary CI. CP uses the +original suspended delivery vector; context-transferred frames without that +state remain an implementation gap. The MOVEM matrices cover every legal opcode word and all 66 full-index structures; -separate state/JIT regressions check reset, interrupts and warmed V1/V2 traces. +separate state/JIT regressions check reset, interrupts, nested FPU delivery, +completed operand-store preservation and warmed V1/V2 traces. These fixtures do +not certify hardware access-fault generation or enabled-MMU operation. Failed prerequisites leave later phases untested. The command checks exact selections, combinations, counts and source/assembly identities. Existing outputs must be validated with `-ValidateReportsOnly` or a diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 00944449..658437bd 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -23,17 +23,16 @@ public sealed class SyntheticM68040AccessFrameAuditTests(ITestOutputHelper outpu public void PendingTraceConvertsTheFrameBeforeReturningAndResuming() => Audit("rte-access-trace", [7], true); [EnvironmentFact(Enable, "audit complete 040 format-7 restoration prerequisites"), Trait("Suite", "ReferenceDiscovery")] - public void RemainingContinuationProtocolsCannotDisappearFromTheGate() + public void RemainingFaultProtocolsCannotDisappearFromTheGate() { - var report = new CoverageBatch("68040", "rte-access-continuations"); - // These are explicit prerequisite gaps, not asserted format-error behavior. - // CU/CP need independently qualified pending-FPU state. The separate - // MOVEM matrices cover CM; FPU arithmetic being out of scope does not prove RTE. + var report = new CoverageBatch("68040", "rte-access-remaining-protocols"); + // Preserve the unqualified protocols explicitly after promoting CU/CP. foreach (var bank in Banks) for (var ccr = 0; ccr < 32; ccr++) - foreach (var form in new[] { "CU-pending-unimplemented", "CP-pending-postinstruction" }) + foreach (var form in new[] { "frame-validation-fault", "odd-user-trace-PC", "throwaway-to-access", + "real-access-fault-entry", "writeback-handler", "CP-context-transferred-vector" }) report.Record($"68040/RTE/format7/{form}/bank={bank}/op=4E73/ccr={ccr:X2}", "untested", - "Required continuation protocol has no independently qualified execution fixture yet"); + "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } @@ -84,7 +83,7 @@ private void Audit(string group, int[] formats, bool pendingTrace) report.Complete(output); } - private static (ArchitecturalExpectation Expected, uint Frame, ushort SavedSr) Prepare( + internal static (ArchitecturalExpectation Expected, uint Frame, ushort SavedSr) Prepare( SyntheticMachine m, int format, string bank, int ccr, ushort trace, uint vbr, uint ea, ushort ssw) { m.Reset(ccr); @@ -123,7 +122,7 @@ private static (ArchitecturalExpectation Expected, uint Frame, ushort SavedSr) P internal static ushort SavedStatus(string bank, ushort trace, int ccr) => (ushort)((bank switch { "user" => 0, "ISP" => 0x2000, "MSP" => 0x3000, _ => throw new ArgumentException("Unknown stack bank") }) | trace | (ccr ^ 31)); - private static void Restore(ArchitecturalExpectation e, string bank, uint stack, ushort sr) + internal static void Restore(ArchitecturalExpectation e, string bank, uint stack, ushort sr) { e.Sr = sr; e.A[7] = bank == "user" ? 0x7800u : stack; @@ -149,7 +148,7 @@ private static void ExpectTrace(SyntheticMachine m, ArchitecturalExpectation e, e.Pc = TraceHandler; e.ExceptionVector = 9; } - private static bool Step(SyntheticMachine m, ArchitecturalExpectation e, CoverageBatch report, string id) + internal static bool Step(SyntheticMachine m, ArchitecturalExpectation e, CoverageBatch report, string id) { try { diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs new file mode 100644 index 00000000..9506c1ea --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs @@ -0,0 +1,79 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 8.3, 8.4.6.2/7. Synthetic suspended delivery is an input, +// not an arithmetic oracle or qualification of real access-fault frame entry. +public sealed class SyntheticM68040FpuContinuationTests(ITestOutputHelper output) +{ + [Fact, Trait("Suite", "Synthetic")] + public void UnimplementedContinuationConvertsToFormatTwo() => Audit(false); + + [Fact, Trait("Suite", "Synthetic")] + public void PostInstructionContinuationUsesTheOriginalVector() => Audit(true); + + private void Audit(bool post) + { + var report = new CoverageBatch("68040", post ? "rte-access-fpu-post" : "rte-access-fpu-unimplemented"); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); + foreach (var vector in post ? new[] {49, 50, 51, 52, 53, 54, 55} : new[] {11}) + foreach (var bank in new[] {"user", "ISP", "MSP"}) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var trace in new ushort[] {0, 0x8000, 0x4000}) + foreach (var vbr in new[] {0u, 0x10000u}) + foreach (var ea in new[] {0x5ffau, 0x12345678u}) + foreach (var access in new ushort[] {0, 0x0105, 0x0505, 0x0905}) + { + var (e, frame, sr) = SyntheticM68040AccessFrameAuditTests.Prepare(m, 7, bank, ccr, trace, + vbr, ea, (ushort)(access | (post ? 0x8000 : 0x4000))); + const uint target = 0x6000, handler = 0x9090; + var format = post ? 3 : 2; + // A prior event selected this vector before delivery was suspended. + // Handler-visible FPU registers deliberately suggest another event. + m.Core.State.M68040PendingFpuExceptions.Begin(format, vector, target); + m.Core.State.M68040Fpu.Fpcr = 0; + m.Core.State.M68040Fpu.Fpsr = 0x08008198; + m.Core.State.M68040Fpu.Fpiar = 0x1234abcd; + m.InitializePhysical(vbr + (uint)vector * 4, handler, 4); + e.Write(vbr + (uint)vector * 4, handler, 4, m.Model); + e.ControlChecks["FPCR preserved"] = (s => s.M68040Fpu.Fpcr, 0); + e.ControlChecks["FPSR preserved"] = (s => s.M68040Fpu.Fpsr, 0x08008198); + e.ControlChecks["FPIAR preserved"] = (s => s.M68040Fpu.Fpiar, 0x1234abcd); + e.ControlChecks["pending delivery consumed"] = (s => s.M68040PendingFpuExceptions.Find(format, target) == null ? 0u : 1u, 0); + var sequence = m.Core.State.ExceptionSequence; + e.ControlChecks["exception entries"] = (s => s.ExceptionSequence, sequence + 1); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, target); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, sr); + e.Sr = (ushort)((sr | 0x2000) & ~0xc000); + e.A[7] = frame + 48; e.InactiveStackPointer = 0x7800; + if (bank == "MSP") e.MasterStackPointer = frame + 48; + e.Write(frame + 48, sr, 2, m.Model); + e.Write(frame + 50, target, 4, m.Model); + e.Write(frame + 54, (uint)(format << 12 | vector * 4), 2, m.Model); + e.Write(frame + 56, ea, 4, m.Model); + e.Pc = handler; e.ExceptionVector = vector; + var id = $"68040/RTE/format7/{(post ? "CP" : "CU")}/vector={vector}/bank={bank}/T={trace:X4}/VBR={vbr:X8}/EA={ea:X8}/SSW={access:X4}/op=4E73/ccr={ccr:X2}"; + if (!SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/RTE")) + { report.Record(id + "/return", "untested", "RTE prerequisite failed"); report.Record(id + "/following-BRA", "untested", "RTE prerequisite failed"); continue; } + SyntheticM68040AccessFrameAuditTests.Restore(e, bank, frame + 60, sr); + e.Pc = target; e.ExceptionVector = null; + if (!SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/return")) + { report.Record(id + "/following-BRA", "untested", "Handler return prerequisite failed"); continue; } + // A handler may separately service the original trace. Here it simply + // returns; the restored trace bits apply to the following self-BRA. + if (trace != 0) + { + e.ControlChecks["exception entries"] = (s => s.ExceptionSequence, sequence + 2); + e.Sr = (ushort)((sr | 0x2000) & ~0xc000); + e.A[7] = frame + 48; e.InactiveStackPointer = 0x7800; + if (bank == "MSP") e.MasterStackPointer = frame + 48; + e.Write(frame + 48, sr, 2, m.Model); e.Write(frame + 50, target, 4, m.Model); + e.Write(frame + 54, 0x2024, 2, m.Model); e.Write(frame + 56, target, 4, m.Model); + e.Pc = handler; e.ExceptionVector = 9; + } + SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/following-BRA"); + } + report.Complete(output); + } +} diff --git a/Copper68k/M68040PendingFpuExceptions.cs b/Copper68k/M68040PendingFpuExceptions.cs new file mode 100644 index 00000000..2ca8bc93 --- /dev/null +++ b/Copper68k/M68040PendingFpuExceptions.cs @@ -0,0 +1,37 @@ +/* Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT */ +using System.Collections.Generic; + +namespace Copper68k; + +// Exception delivery state belongs to the integer pipeline, not the live FPSR +// or an FSAVE image. Preserve suspended delivery while a handler uses the FPU. +internal sealed class M68040PendingFpuExceptions +{ + internal sealed record Entry(int Format, int Vector, uint ProgramCounter); + private List? _entries; + + internal Entry Begin(int format, int vector, uint pc) + { + var entry = new Entry(format, vector, pc); + (_entries ??= []).Add(entry); + return entry; + } + + internal Entry? Find(int format, uint? pc = null) + { + if (_entries == null) return null; + for (var n = _entries.Count - 1; n >= 0; n--) + if (_entries[n].Format == format && (!pc.HasValue || _entries[n].ProgramCounter == pc.Value)) return _entries[n]; + return null; + } + + internal void Complete(Entry? entry) + { + if (entry == null || _entries == null) return; + // Equal-valued nested entries still have distinct delivery ownership. + for (var n = _entries.Count - 1; n >= 0; n--) + if (ReferenceEquals(_entries[n], entry)) { _entries.RemoveAt(n); return; } + } + + internal void Reset() => _entries?.Clear(); +} diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index 1c374d6c..1b92abb6 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -4198,6 +4198,7 @@ private void RaiseFpuFormatException( uint stackedProgramCounter, uint effectiveAddress) { + var pending = State.M68040PendingFpuExceptions.Begin(format, vector, stackedProgramCounter); var savedStatusRegister = State.StatusRegister; State.RecordException(vector, stackedProgramCounter, savedStatusRegister); State.StatusRegister = (ushort)((State.StatusRegister | M68kCpuState.Supervisor) & ~M68kCpuState.Master); @@ -4206,6 +4207,7 @@ private void RaiseFpuFormatException( PushLong(stackedProgramCounter); PushWord(savedStatusRegister); State.ProgramCounter = ReadLong(State.VectorBaseRegister + ((uint)vector * 4)); + State.M68040PendingFpuExceptions.Complete(pending); CompleteTiming(M68kInstructionTimingKey.LineFException); } diff --git a/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs b/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs index 695f2765..0da7ea98 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs @@ -9,27 +9,39 @@ internal partial class M68kAdvancedTimingInterpreter private bool TryRestoreM68040AccessFrame(uint frame, ushort sr, uint pc) { var continuation = ReadWord(frame + 12) & 0xf000; - // CU/CP need qualified pending-FPU state; multiple bits are undefined. - // Keep those explicit gaps on the existing rejected-frame path for now. - if (continuation is not (0 or 0x1000 or 0x2000)) return false; + // Multiple continuation bits are undefined (MC68040UM 8.4.6.7). + if (continuation is not (0 or 0x1000 or 0x2000 or 0x4000 or 0x8000)) return false; + var pending = continuation == 0x8000 + ? State.M68040PendingFpuExceptions.Find(3) + : continuation == 0x4000 ? State.M68040PendingFpuExceptions.Find(2) : null; + // A foreign/context-transferred CP frame without its selected vector is + // still an explicit implementation gap. Do not invent a format error or + // reselect a vector from handler-modified FPCR/FPSR registers. + if (continuation == 0x8000 && pending == null) + throw new UnsupportedM68kTimingException(0x4e73, State.LastInstructionProgramCounter, _profile); var address = continuation == 0 ? 0u : ReadLong(frame + 8); State.SetActiveStackPointer(unchecked(frame + 60)); State.StatusRegister = sr; State.ProgramCounter = pc; if (continuation == 0x1000) _m68040MovemContinuation = (pc, address); - if (continuation == 0x2000) + if (continuation >= 0x2000) { - // MC68040UM 8.4.6.2: old SP+48 is the new format-2 frame. - // The instruction address is the saved EA, not the RTE opcode PC. - State.RecordException(9, pc, sr); + // MC68040UM 8.4.6.2/7: convert the pending exception frame at + // old SP+48. CT/CU use format 2; CP uses format 3. Trace is deferred + // to the CU/CP handler, as indicated by the saved SR trace bits. + var vector = continuation == 0x8000 ? pending!.Vector : continuation == 0x4000 ? 11 : 9; + var format = continuation == 0x8000 ? 3 : 2; + State.RecordException(vector, pc, sr); State.StatusRegister = (ushort)((sr | M68kCpuState.Supervisor) & ~0xc000); PushLong(address); - PushWord(0x2024); + PushWord((ushort)(format << 12 | vector * 4)); PushLong(pc); PushWord(sr); - State.ProgramCounter = ReadLong(State.VectorBaseRegister + 36); - CompleteTiming(M68kInstructionTimingKey.IllegalInstruction); + State.ProgramCounter = ReadLong(State.VectorBaseRegister + (uint)vector * 4); + if (pending != null && (continuation == 0x8000 || pending.Vector == 11)) + State.M68040PendingFpuExceptions.Complete(pending); + CompleteTiming(continuation == 0x2000 ? M68kInstructionTimingKey.IllegalInstruction : M68kInstructionTimingKey.LineFException); } else CompleteTiming(M68kInstructionTimingKey.Rte); return true; diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index c6bd1da3..d910bf87 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -5596,6 +5596,7 @@ public virtual void Reset(uint programCounter, uint stackPointer) State.CacheControlRegister = 0; State.CacheAddressRegister = 0; State.M68040Fpu.Reset(); + State.M68040PendingFpuExceptions.Reset(); State.M68040Mmu.Reset(); State.Cycles = 0; State.NativeCycles = 0; diff --git a/Copper68k/M68kCore.cs b/Copper68k/M68kCore.cs index d3377850..02c15a9b 100644 --- a/Copper68k/M68kCore.cs +++ b/Copper68k/M68kCore.cs @@ -1497,6 +1497,8 @@ public ushort StatusRegister internal M68040FpuState M68040Fpu { get; } = new M68040FpuState(); + internal M68040PendingFpuExceptions M68040PendingFpuExceptions { get; } = new(); + internal M68040MmuState M68040Mmu { get; } = new M68040MmuState(); internal uint M68060ProcessorConfiguration { get; set; } = 0x0430_0000; diff --git a/Copper68k/M68kJitCore.cs b/Copper68k/M68kJitCore.cs index 20d938f6..e79bfc1d 100644 --- a/Copper68k/M68kJitCore.cs +++ b/Copper68k/M68kJitCore.cs @@ -2585,6 +2585,7 @@ private void RaiseM68040FpuFormatException( uint effectiveAddress, int cycles) { + var pending = State.M68040PendingFpuExceptions.Begin(format, vector, stackedProgramCounter); var savedStatusRegister = State.StatusRegister; State.RecordException(vector, stackedProgramCounter, savedStatusRegister); State.StatusRegister = (ushort)((State.StatusRegister | M68kCpuState.Supervisor) & ~M68kCpuState.Master); @@ -2593,6 +2594,7 @@ private void RaiseM68040FpuFormatException( PushLong(stackedProgramCounter); PushWord(savedStatusRegister); State.ProgramCounter = ReadLong(State.VectorBaseRegister + ((uint)vector * 4)); + State.M68040PendingFpuExceptions.Complete(pending); AddCycles(cycles); } diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 5e982ade..1cbb497c 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -276,11 +276,12 @@ bus ordering, detailed fault sequencing, JIT and native ROM/media tests remain. - Generated 010 word-MOVE/MOVEA format-8 images now have the scoped continuation gate below. Long transfers, other instruction families, foreign silicon images, external bus faults, 020/030 formats 9/A/B and the remaining 040 format-7 - CU/CP and detailed fault protocols remain unqualified. The advanced decoder does not implement all these + CP context transfer and detailed fault protocols remain unqualified. The advanced decoder does not implement all these legal restoration protocols; this is an implementation gap, not invalid encoding. The [040 access-frame discovery gate](#040-access-frame-restoration-discovery-2026-10-05) retains the original failed-before record. The latest checkpoint below promotes - normal/CT/CM synthetic returns and preserves CU/CP prerequisites explicitly. + normal/CT/CM/CU/CP synthetic returns and preserves the remaining fault/context + prerequisites explicitly. - Nested 000 address errors during exception stacking previously recursed on an odd SSP. This is corrected by the address-error double-fault slice above. External BERR and reset-vector fault signaling remain unavailable through the current @@ -2707,3 +2708,97 @@ Evidence is `artifacts/m6-rte-continuation-gate-final.log` and the full-run `summary.json`; `roadmapComplete=false`. An initial verifier invocation incorrectly requested zero-count 040-only reports for other models and failed; the corrected model-scoped selection is the successful invocation recorded here. + +## 040 CU/CP pending delivery checkpoint — 2026-10-05 + +RTE now converts CU frames to format 2/vector 11 and CP frames to format 3/the +original pending FPU vector, at old SP+48. Saved PC/SR/EA and the supervisor stack +bank are preserved; saved trace bits reach the new handler, which is responsible +for servicing the original trace. Authority is MC68040UM sections 8.3 and +8.4.6.2/7. The selected vector is integer exception-delivery state, distinct from +handler-visible FPCR/FPSR/FPIAR and FSAVE data. The interpreter and JIT retain it +until vector fetching succeeds. Nested completed deliveries preserve a suspended +outer event, including equal-valued entries. CPU reset clears delivery state; +FPU context/register reset does not. A redirected saved PC does not reselect or +lose the pending vector. Ordinary exception frame and timing policies remain. + +The new CU matrix passes 13,824 logical phases/144 combinations. The CP matrix +passes 96,768 phases/1,008 combinations for vectors 49–55. Both use all 32 CCRs, +user/ISP/MSP banks, T0/T1/no trace, two VBRs/EAs and four lower SSW patterns. +Each scenario checks conversion, handler RTE and a following self-BRA; common +verification checks registers, exact PC/SR, guarded memory and writeback +non-replay. Fixed pending vectors are fixture inputs, not arithmetic expectations. +Deliberately inconsistent handler FPU control registers prove vector ownership. +BSUN is not a post-instruction FMOVE exception and is outside this CP selection. + +Sixteen additional ordinary xUnit scenarios exercise actual FSIN unimplemented +and FMOVE unsupported/overflow exception delivery. A one-shot injected internal +fault at the vector fetch suspends delivery. Accurate, warmed V1 and warmed V2 +entries preserve the event; compiled entry is mandatory in JIT scenarios. A +completed single-precision operand store is not fetched or written again during +CP return. Reset, nested delivery and redirected return PCs are covered. Foreign +CP frames without their original selected vector fail explicitly as unsupported, +without a fabricated architectural format error or operand retry. These tests +use the existing approximate internal fault entry and independently construct +the returning format-7 frame; they do not qualify real access-error frame entry, +FPU arithmetic, enabled MMU, silicon timing or foreign/context-transfer recovery. + +The complete discovery selection is still failing: +`artifacts/m6-fpu-continuation-discovery-final/` executes fourteen xUnit cases, +with thirteen passing and one inventory failure. Its summary reports 376,128 +logical phases: 375,552 passing, zero mismatching/unsupported and 576 explicitly +untested requirements. The inventory expands the previously documented gaps +into six named categories across three banks/all CCRs: frame-validation faults, +odd user trace PCs, throwaway-to-access return, real access-fault entry, +writeback-handler qualification and CP context-transferred vector recovery. +The latter remains necessary; preserving an original local pending event does +not solve migrating that event to another CPU/context. No requirement is removed +from the milestone or relabeled as invalid. + +Failed-before CU/CP evidence is `artifacts/m6-fpu-continuation-before/`: 4,608 +and 32,256 mismatching RTE prerequisites, with 9,216/64,512 dependent phases +untested. A constant-49 CP-vector mutation passes only that vector's scope and +fails exactly 27,648 prerequisites, leaving 55,296 dependent phases untested. +Premature delivery-state consumption fails all fifteen actual-delivery scenarios; +the explicit missing-state gap test still passes. Source is restored byte-for-byte +and all 73 focused FPU/matrix/lifetime/MOVEM tests pass. Evidence is +`artifacts/m6-fpu-continuation-probes/controls.json`. All thirteen specific +malformed input/report controls still detect their intended defects in +`artifacts/m6-fpu-continuation-preflight-controls/controls.json`. Earlier failed +V2 fixture attempts remain recorded; the qualified FSIN route proves a compiled +entry rather than accepting a fallback as V2 coverage. + +Final Release CPU validation passes 4,950 tests, with eleven optional/opt-in +skips and zero failures; all four qualified WinUAE presets are enabled. +The strict report gate verifies 15,022,168 passing logical cases in 581 reporting +batches, with fresh pinned SingleStepTests (312,500 cases / 125 files) and +Musashi (536 programs / 88 explicit exclusions across eight profiles). +Evidence is `artifacts/m6-fpu-continuation-full/` and +`artifacts/m6-fpu-continuation-gate.log`; `roadmapComplete=false`. + +Isolated unpublished NuGet `1.5.2-synthetic-dev.55` validates CopperScreen at +baseline `d9beae8b88be24032221e3482942a249c03c27d3`: Release build has zero +warnings/errors; host 149 passing/six optional skips, disk 74, separate engine +diagnostics 1,080, native Workbench boots two and A1200 AGA boot/persistence one, +without native skips. Four restored assets and four loaded CPU assemblies match +the package. Package SHA-256 is +`e804107087da31910386ca8d5ea46b622c71482bd46a4dc927751f2c07312f11`; +CPU assembly SHA-256 is +`3f09d72f14ae9dd9242845ecd745421a461b35dc1019825dc9edb763a46c69a8`. +Consumer evidence is its `artifacts/fpu-continuation-identities.json`, +`artifacts/fpu-continuation-validation/` and separate diagnostic outputs. +Published package versions and root CopperScreen dependencies are unchanged. + +The fresh broad Basic audit in `artifacts/m6-fpu-continuation-broad-final/` +still fails: 1,326 passing directories, 47 mismatching and eight unsupported, +with 11,474,194 executed callbacks, 1,663,891 exception frames and 199,327 +masked-SR cases. All 32 comparator controls detect their intended defects. +The pinned generator, runner, native library and input manifest are unchanged. +An initial invocation used the wrong native-library path and was rejected before +execution; its failure remains in `artifacts/m6-fpu-continuation-broad/`. +The corrected invocation verifies the manifest's exact library identity. + +Other-model advanced restoration, the broad reference disagreements, independent +combination qualification and consolidation remain required. Existing specialized +regressions are retained. Milestone 6 and the goal remain **in progress** with +`roadmapComplete=false`; publication remains a separate release step. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 338a98f3..f6c47342 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1199,3 +1199,30 @@ for architectural authority, exact coverage, evidence and remaining scope. The strict gate verifies 14,911,576 passing logical cases in 579 reporting batches, with fresh pinned SingleStepTests (312,500 cases / 125 files) and Musashi (536 programs / 88 exclusions across eight profiles); roadmapComplete=false. + +### Milestone 6 CU/CP delivery implementation — 2026-10-05 + +CU/CP RTE now converts the access frame to the pending exception and retains its +selected vector across interrupted delivery, nested handlers and FPU context +changes. Sixteen state/JIT scenarios prove actual delivery ownership, reset, +redirected PCs and no repeated completed operand store. Independent CU and CP +matrices add 110,592 passing phases in two ordinary-CI reporting batches. Original +return code and vector/lifetime mutations reproduce failures; restored focused +tests and corruption controls pass. + +The complete discovery command remains failing: 375,552 passing phases and 576 +explicitly untested fault/context requirements. Real access-frame entry, +validation faults, odd user trace PCs, throwaway frames, writeback-handler +qualification and CP context-transferred vector recovery remain required, as do +other-model restoration, broader independent coverage and consolidation. The +[reference record](COPPER68K_REFERENCE_QUALIFICATION.md#040-cucp-pending-delivery-checkpoint--2026-10-05) +records the exact scope, manual authority and evidence. Milestone 6 and the full +goal remain **in progress**; `roadmapComplete=false`. No FPU arithmetic, enabled +MMU, physical timing or OS qualification is inferred. + +Final validation passes 4,950 Release CPU tests (eleven optional/opt-in skips), +15,022,168 logical cases in 581 strict reporting batches, fresh pinned +SingleStepTests and Musashi audits. Private unpublished `.55` validates the +isolated CopperScreen Release build, host/disk/engine tests and all three native +boot replays through NuGet. Package/assets/loaded DLL identities match; see the +reference record for counts and hashes. No package publication is included. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index fa6b0c8b..df253514 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -13,7 +13,9 @@ $expected = [ordered]@{ 'rte-access-trace' = @{cases=13824; combinations=144} 'rte-access-movem-opcodes' = @{cases=120960; combinations=1260} 'rte-access-movem-full-index' = @{cases=114048; combinations=1188} - 'rte-access-continuations' = @{cases=192; combinations=6} + 'rte-access-fpu-unimplemented' = @{cases=13824; combinations=144} + 'rte-access-fpu-post' = @{cases=96768; combinations=1008} + 'rte-access-remaining-protocols' = @{cases=576; combinations=18} } $testExit = 0 if (-not $ValidateReportsOnly) { @@ -26,7 +28,7 @@ if (-not $ValidateReportsOnly) { $saved[$name] = [Environment]::GetEnvironmentVariable($name) [Environment]::SetEnvironmentVariable($name, $settings[$name]) } - & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output $testExit = $LASTEXITCODE } finally { foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } @@ -38,14 +40,14 @@ if (-not $ValidateReportsOnly) { cpuSourceFiles=@(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } - inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1') | ForEach-Object { + inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } assemblies=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } limitations=@('Canonical synthetic frames, not hardware captures or enabled-MMU access faults', - 'CU/CP continuation inventory remains untested and fails the gate', + 'CP context transfer and detailed fault protocol inventory remain untested and fail the gate', 'Multiple continuation bits are architecturally undefined and excluded', 'Detailed fault validation, odd return PCs, throwaway-to-access frames and physical timing remain unqualified') } @@ -56,11 +58,11 @@ if (-not $ValidateReportsOnly) { $identity = Get-Content -LiteralPath (Join-Path $output 'identities.json') -Raw | ConvertFrom-Json if ($identity.schema -ne 1 -or $identity.reference -cne 'MC68040UM' -or $identity.softwareReferenceExecuted -ne $false -or $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.cpuCommittedTree -notmatch '^[a-f0-9]{40}$' -or - @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 3 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 3 -or + @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 4 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 4 -or @($identity.assemblies).Count -ne 2) { throw '040 access-frame input identity is missing or incomplete' } foreach ($input in $identity.inputs) { $path = Join-Path $repo $input.file - if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1') -or + if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs') -or $input.sha256 -cne (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()) { throw '040 access-frame fixture/command identity differs' } } $cpuFiles = @(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') @@ -74,8 +76,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 12 -or [int]$counters.total -ne 12 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (6 batches, 6 fixed examples)' +if ([int]$counters.executed -ne 14 -or [int]$counters.total -ne 14 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (8 batches, 6 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -92,12 +94,28 @@ foreach ($group in $expected.Keys) { if ($caseTotal -ne $report.logicalCases) { throw "$group status totals differ" } $combinationTotals = @{passing=0; mismatching=0; unsupported=0; untested=0} $expectedCombinations = @{} - if ($group -eq 'rte-access-continuations') { + if ($group -eq 'rte-access-remaining-protocols') { foreach ($bank in @('user','ISP','MSP')) { - foreach ($form in @('CU-pending-unimplemented','CP-pending-postinstruction')) { + foreach ($form in @('frame-validation-fault','odd-user-trace-PC','throwaway-to-access','real-access-fault-entry','writeback-handler','CP-context-transferred-vector')) { $expectedCombinations["68040/RTE/format7/$form/bank=$bank"] = 32 } } + } elseif ($group -in @('rte-access-fpu-unimplemented','rte-access-fpu-post')) { + $post = $group -eq 'rte-access-fpu-post' + foreach ($vector in $(if ($post) {49..55} else {@(11)})) { + foreach ($bank in @('user','ISP','MSP')) { + foreach ($trace in @(0,0x8000,0x4000)) { + foreach ($vbr in @(0,0x10000)) { + foreach ($ea in @(0x5ffa,0x12345678)) { + foreach ($ssw in @(0,0x0105,0x0505,0x0905)) { + $key = '68040/RTE/format7/{0}/vector={1}/bank={2}/T={3:X4}/VBR={4:X8}/EA={5:X8}/SSW={6:X4}' -f $(if ($post) {'CP'} else {'CU'}),$vector,$bank,$trace,$vbr,$ea,$ssw + $expectedCombinations[$key] = 96 + } + } + } + } + } + } } elseif ($group -in @('rte-access-movem-opcodes','rte-access-movem-full-index')) { foreach ($load in @($false,$true)) { foreach ($width in @(2,4)) { @@ -156,8 +174,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 12 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 14 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=265152; xunitBatches=6; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=376128; xunitBatches=8; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 2e495007..7e4db19c 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -145,6 +145,8 @@ try { $expected['system-move-word-restart-invalid'] = 224 } if ($model -eq '68040') { + $expected['rte-access-fpu-unimplemented'] = 13824 + $expected['rte-access-fpu-post'] = 96768 $expected['rte-access-controls'] = 6912 $expected['rte-access-normal'] = 9216 $expected['rte-access-trace'] = 13824 @@ -240,16 +242,16 @@ try { foreach ($row in $inventory.combinations) { if ($row.Milestone -le 5) { $row.status = 'passing'; $row.note = 'Passing named semantic scenario batches; consult qualification boundaries for untested physical/internal protocols.' } } - $inventory | Add-Member -NotePropertyName qualificationBoundaries -NotePropertyValue @('RTE fault restart/internal formats remain untested: 010 long/non-MOVE/foreign frame8 restart, 020/030 frames9/A/B, 040 frame7 CU/CP and detailed access-fault restoration', 'BKPT external replacement responder is unavailable', 'MOVES physical function-code spaces and LPSTOP CPU-space broadcast are unqualified', 'CALLM/RTM type1 protocol uses an internal synthetic responder; normal public buses have none', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands are unavailable') + $inventory | Add-Member -NotePropertyName qualificationBoundaries -NotePropertyValue @('RTE fault restart/internal formats remain untested: 010 long/non-MOVE/foreign frame8 restart, 020/030 frames9/A/B, 040 frame7 CP context transfer and detailed access-fault restoration', 'BKPT external replacement responder is unavailable', 'MOVES physical function-code spaces and LPSTOP CPU-space broadcast are unqualified', 'CALLM/RTM type1 protocol uses an internal synthetic responder; normal public buses have none', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands are unavailable') $inventory | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'qualified-inventory.json') @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=$logicalBatches; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; m68040DisabledMmuInstructionGate='passing'; m68000AddressErrorDoubleFaultGate='passing'; m68010Format8StructureGate='passing'; m68010WordMoveRestartGate='passing'; roadmapComplete=$false; seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; - m68040AccessFrameScope='Synthetic normal/CT/CM returns, every legal MOVEM word and full-index structure; CU/CP, detailed fault validation, real fault entry and physical timing remain unqualified'; + m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure; CP context transfer, detailed fault validation, real fault entry and physical timing remain unqualified'; m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; marked private word-MOVE/MOVEA images resume their faulted cycle; other opaque restart/input state, long transfers, RMW and physical timing remain unqualified'; m68000DoubleFaultScope='Address-error entry/handler faults; reset-only recovery; external BERR/reset-vector faults unavailable through the current public bus API'; - unavailableSystemCoverage=@('010 long/non-MOVE/external bus-fault restart and 020/030 internal restart', '020/030 coprocessor midinstruction restoration', '040 access-fault CU/CP, detailed frame-validation/fault entry and writeback-handler qualification', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands'); + unavailableSystemCoverage=@('010 long/non-MOVE/external bus-fault restart and 020/030 internal restart', '020/030 coprocessor midinstruction restoration', '040 access-fault CP context transfer, detailed frame-validation/fault entry and writeback-handler qualification', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands'); timing='Semantic gate; timing policy and physical qualification remain separate'} | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'summary.json') Write-Host "Synthetic milestones 1-5 semantic gates: $logicalCases logical cases; reports at $output" From 129000240897edf7d2fa312ec1c1c2e2f945375a Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 10:38:06 +0300 Subject: [PATCH 033/211] Qualify chained 68040 throwaway frames across stack banks --- Copper68k.Tests/Synthetic/README.md | 15 +- .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- .../SyntheticM68040ThrowawayTests.cs | 150 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 85 +++++++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 26 +++ scripts/test-copper68k-040-access-frames.ps1 | 34 ++-- .../test-copper68k-synthetic-mutations.ps1 | 23 ++- scripts/test-copper68k-synthetic.ps1 | 4 +- 8 files changed, 319 insertions(+), 20 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 839c21e7..de812cac 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -226,9 +226,9 @@ it does not replace the deterministic synthetic gate or complete milestone 6. The independent 040 access-frame discovery command is `./scripts/test-copper68k-040-access-frames.ps1 -OutputDirectory artifacts/040-access-frame-audit`. -It currently **fails** on 576 explicit fault/context requirements. Normal, -CT, CM, CU and CP restoration now pass 375,552 logical phases, including -short-frame controls. Seven promoted batches run in ordinary CI. CP uses the +It currently **fails** on 480 explicit fault/context requirements. Normal, +CT, CM, CU and CP restoration and chained throwaways now pass 887,040 logical +phases, including short-frame controls. Nine promoted batches run in ordinary CI. CP uses the original suspended delivery vector; context-transferred frames without that state remain an implementation gap. The MOVEM matrices cover every legal opcode word and all 66 full-index structures; @@ -236,6 +236,15 @@ separate state/JIT regressions check reset, interrupts, nested FPU delivery, completed operand-store preservation and warmed V1/V2 traces. These fixtures do not certify hardware access-fault generation or enabled-MMU operation. Failed prerequisites leave later phases untested. +The throwaway matrices independently track all three stack pointers through one +or two discarded frames, every start/intermediate/tail/result bank selection, +even/odd data-stack addresses, all CCRs and T0/T1/no trace. Discarded odd PCs are +never fetched; terminal short/access frames preserve guarded memory and pending +writebacks. CM's following MOVEM uses the saved EA. The maintained consolidation +mutation command proves early chain termination and missing stack selection are +detected, then restores and rebuilds source. Detailed validation/access faults, +odd final user trace PCs, real access-fault entry, writeback handlers and CP context +transfer remain required; no regression is retired by this checkpoint. The command checks exact selections, combinations, counts and source/assembly identities. Existing outputs must be validated with `-ValidateReportsOnly` or a fresh output directory used. It cannot pass merely by fixing normal/trace return. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 658437bd..03c7367e 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -29,7 +29,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() // Preserve the unqualified protocols explicitly after promoting CU/CP. foreach (var bank in Banks) for (var ccr = 0; ccr < 32; ccr++) - foreach (var form in new[] { "frame-validation-fault", "odd-user-trace-PC", "throwaway-to-access", + foreach (var form in new[] { "frame-validation-fault", "odd-user-trace-PC", "real-access-fault-entry", "writeback-handler", "CP-context-transferred-vector" }) report.Record($"68040/RTE/format7/{form}/bank={bank}/op=4E73/ccr={ccr:X2}", "untested", "Required fault/context protocol has no independently qualified execution fixture yet"); diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs new file mode 100644 index 00000000..dc043af2 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs @@ -0,0 +1,150 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 8.4.2: a throwaway selects any of the three stacks and may +// lead to another throwaway. Stack locations and frame encodings are inputs; +// independent pointer bookkeeping below is not production RTE/EA logic. +public sealed class SyntheticM68040ThrowawayTests(ITestOutputHelper output) +{ + private static readonly string[] Banks = ["user", "ISP", "MSP"]; + private const uint Target = 0x6000, Handler = 0x9090, Operand = 0x4200; + + [Fact, Trait("Suite", "Synthetic")] + public void ChainedThrowawaysSelectEveryStackBeforeShortFrames() => Audit(false); + + [Fact, Trait("Suite", "Synthetic")] + public void ChainedThrowawaysSelectEveryStackBeforeAccessContinuations() => Audit(true); + + private void Audit(bool access) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); + var report = new CoverageBatch("68040", access ? "rte-throwaway-access" : "rte-throwaway-controls"); + var forms = access ? new[] { "normal", "CM", "CT", "CU", "CP49", "CP50", "CP51", "CP52", "CP53", "CP54", "CP55" } + : new[] { "format0", "format2", "format3" }; + foreach (var start in new[] { "ISP", "MSP" }) + foreach (var tail in Banks) + foreach (var middle in new[] { "none", "user", "ISP", "MSP" }) + foreach (var result in Banks) + foreach (var trace in new ushort[] { 0, 0x8000, 0x4000 }) + foreach (var alignment in new uint[] { 0, 1 }) + foreach (var form in forms) + for (var ccr = 0; ccr < 32; ccr++) + Run(m, report, start, tail, middle, result, trace, alignment, form, ccr, access); + report.Complete(output); + } + + private static void Run(SyntheticMachine m, CoverageBatch report, string start, string tail, string middle, + string result, ushort trace, uint alignment, string form, int ccr, bool access) + { + m.Reset(ccr); + var pointers = new Dictionary { ["user"] = 0x7800 + alignment, ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; + foreach (var location in pointers.Values) + for (var n = -16; n < 112; n++) m.InitializePhysical(unchecked(location + (uint)n), (uint)(n ^ 0x5a), 1); + var path = middle == "none" ? new[] { start, tail } : new[] { start, middle, tail }; + for (var n = 0; n < path.Length - 1; n++) + { + var at = pointers[path[n]]; + // The discarded PC is odd and must never become an instruction fetch. + m.InitializePhysical(at, Status(path[n + 1], trace == 0x8000 ? (ushort)0x4000 : (ushort)0x8000, ccr), 2); + m.InitializePhysical(at + 2, 0xdead0001u + (uint)n * 2, 4); + m.InitializePhysical(at + 6, 0x1024, 2); + pointers[path[n]] += 8; + } + var frame = pointers[tail]; + var savedSr = Status(result, trace, ccr ^ 31); + var format = access ? 7 : form == "format0" ? 0 : form == "format2" ? 2 : 3; + var vector = form == "CT" ? 9 : form == "CU" ? 11 : form.StartsWith("CP") ? int.Parse(form[2..]) : 0; + var continuation = form == "CM" ? 0x1000 : form == "CT" ? 0x2000 : form == "CU" ? 0x4000 : vector >= 49 ? 0x8000 : 0; + m.InitializePhysical(frame, savedSr, 2); m.InitializePhysical(frame + 2, Target, 4); + m.InitializePhysical(frame + 6, (uint)(format << 12) | 8, 2); + m.InitializePhysical(frame + 8, Operand, 4); m.InitializePhysical(frame + 12, (uint)(continuation | 0x0105), 2); + // Handler-owned writebacks remain untouched even through chained returns. + foreach (var offset in new[] { 14u, 16u, 18u }) m.InitializePhysical(frame + offset, 0x85, 2); + foreach (var offset in new[] { 24u, 32u, 40u }) + { + m.InitializePhysical(frame + offset, 0x3200 + offset, 4); + m.InitializePhysical(frame + offset + 4, 0xc3a55a3c, 4); + m.InitializePhysical(0x3200 + offset, 0x96abcdef, 4); + } + if (form == "CM") + { + m.InitializePhysical(Target, 0x4cd0, 2); m.InitializePhysical(Target + 2, 3, 2); // MOVEM.L (A0),D0-D1 + m.InitializePhysical(Target + 4, 0x60fe, 2); + m.InitializePhysical(Operand, 0x89abcdef, 4); m.InitializePhysical(Operand + 4, 0x12345678, 4); + m.Core.State.A[0] = 0x4300; // saved EA must override the handler's base. + } + else m.InitializePhysical(Target, 0x60fe, 2); + m.InitializePhysical(Handler, 0x4e73, 2); m.InitializePhysical(Handler + 2, 0x4e71, 2); + const uint vbr = 0x10000; + foreach (var v in new[] { 9, 11, 49, 50, 51, 52, 53, 54, 55 }) m.InitializePhysical(vbr + (uint)v * 4, Handler, 4); + _ = SyntheticExecution.Prepare(m, [0x4e73]); + m.Core.State.SetUserStackPointer(0x7800 + alignment); + m.Core.State.SetInterruptStackPointer(0x4700 + alignment); + m.Core.State.SetMasterStackPointer(0x7400 + alignment); + m.Core.State.StatusRegister = Status(start, 0, ccr); + m.Core.State.VectorBaseRegister = vbr; + if (continuation == 0x8000) m.Core.State.M68040PendingFpuExceptions.Begin(3, vector, Target); + if (continuation == 0x4000) m.Core.State.M68040PendingFpuExceptions.Begin(2, 11, Target); + var e = ArchitecturalExpectation.Capture(m); + e.OperandAccessAddresses.UnionWith(new uint[] { 0x3218, 0x3220, 0x3228 }); + e.ExpectedOperandTransfers = []; + foreach (var pc in new uint[] { 0xdead0001, 0xdead0003 }) e.ForbiddenOperandReads.Add(pc); + e.ControlChecks["discarded PCs never fetched"] = (_ => m.Bus.Accesses.Any(a => + !a.Write && a.Address is 0xdead0001 or 0xdead0003) ? 1u : 0u, 0); + if (vector != 0 && vector != 9) + e.ControlChecks["pending delivery consumed"] = (s => s.M68040PendingFpuExceptions.Find(vector == 11 ? 2 : 3) == null ? 0u : 1u, 0); + var sequence = m.Core.State.ExceptionSequence; + e.ControlChecks["exception entries"] = (s => s.ExceptionSequence, sequence); + pointers[tail] += access ? 60u : format == 0 ? 8u : 12u; + SetStacks(e, pointers, savedSr); e.Pc = Target; + var id = $"68040/RTE/throwaway/{form}/start={start}/middle={middle}/tail={tail}/result={result}/T={trace:X4}/align={alignment}/op=4E73/ccr={ccr:X2}"; + if (vector != 0) ExpectException(m, e, pointers, savedSr, vector, Target, Operand, sequence + 1); + if (!SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/RTE")) + { + if (vector != 0) report.Record(id + "/handler-return", "untested", "RTE prerequisite failed"); + report.Record(id + "/following", "untested", "RTE prerequisite failed"); return; + } + if (vector != 0) + { + pointers[ExceptionBank(savedSr)] += 12; + SetStacks(e, pointers, savedSr); e.Pc = Target; e.ExceptionVector = null; + if (!SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/handler-return")) + { report.Record(id + "/following", "untested", "Handler return prerequisite failed"); return; } + } + e.Pc = form == "CM" ? Target + 4 : Target; + if (form == "CM") { e.D[0] = 0x89abcdef; e.D[1] = 0x12345678; } + if (trace == 0x8000 || trace == 0x4000 && form != "CM") + ExpectException(m, e, pointers, savedSr, 9, e.Pc, Target, sequence + (vector != 0 ? 2u : 1u)); + SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/following"); + } + + private static ushort Status(string bank, ushort trace, int ccr) => + (ushort)((bank == "ISP" ? 0x2000 : bank == "MSP" ? 0x3000 : 0) | trace | ccr); + private static string ExceptionBank(ushort sr) => (sr & 0x3000) == 0x3000 ? "MSP" : "ISP"; + private static void SetStacks(ArchitecturalExpectation e, Dictionary pointers, ushort sr) + { + e.Sr = sr; + e.A[7] = pointers[(sr & 0x2000) == 0 ? "user" : ExceptionBank(sr)]; + e.InactiveStackPointer = pointers[(sr & 0x2000) == 0 ? "ISP" : "user"]; + e.MasterStackPointer = pointers["MSP"]; + e.ControlChecks["USP"] = (s => s.UserStackPointer, pointers["user"]); + e.ControlChecks["ISP"] = (s => s.InterruptStackPointer, pointers["ISP"]); + e.ControlChecks["MSP"] = (s => s.MasterStackPointer, pointers["MSP"]); + } + private static void ExpectException(SyntheticMachine m, ArchitecturalExpectation e, Dictionary pointers, + ushort sr, int vector, uint pc, uint address, uint sequence) + { + var bank = ExceptionBank(sr); pointers[bank] -= 12; + SetStacks(e, pointers, (ushort)((sr | 0x2000) & ~0xc000)); + e.Pc = Handler; e.ExceptionVector = vector; + e.ControlChecks["exception entries"] = (s => s.ExceptionSequence, sequence); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, pc); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, sr); + var stack = pointers[bank]; + e.Write(stack, sr, 2, m.Model); e.Write(stack + 2, pc, 4, m.Model); + e.Write(stack + 6, (uint)((vector >= 49 ? 3 : 2) << 12 | vector * 4), 2, m.Model); + e.Write(stack + 8, address, 4, m.Model); + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 1cbb497c..65665763 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -280,7 +280,7 @@ bus ordering, detailed fault sequencing, JIT and native ROM/media tests remain. legal restoration protocols; this is an implementation gap, not invalid encoding. The [040 access-frame discovery gate](#040-access-frame-restoration-discovery-2026-10-05) retains the original failed-before record. The latest checkpoint below promotes - normal/CT/CM/CU/CP synthetic returns and preserves the remaining fault/context + normal/CT/CM/CU/CP synthetic returns and chained throwaways, preserving the remaining fault/context prerequisites explicitly. - Nested 000 address errors during exception stacking previously recursed on an odd SSP. This is corrected by the address-error double-fault slice above. External @@ -2802,3 +2802,86 @@ Other-model advanced restoration, the broad reference disagreements, independent combination qualification and consolidation remain required. Existing specialized regressions are retained. Milestone 6 and the goal remain **in progress** with `roadmapComplete=false`; publication remains a separate release step. + +## 040 chained throwaway qualification — 2026-10-05 + +MC68040UM section 8.4.2 permits a throwaway frame to select any of the three +stacks, including another throwaway before the final frame. The previous +ordinary-CI fixture covered only ISP-to-MSP followed by format 0. New matrices +qualify every one/two-throwaway bank path from an initially privileged ISP/MSP, +all three final restored banks, all 32 initial CCRs, T0/T1/no final trace and +even/odd data-stack addresses. Discarded PCs are odd and never fetched; discarded +SRs deliberately differ in trace state, without simultaneous T0/T1. There is +no privilege recheck after a throwaway selects USP. Independent pointer tracking +checks USP/ISP/MSP together, including repeated frames on the same stack. + +| New ordinary-CI group | Passing phases | Combinations | +| --- | ---: | ---: | +| Chained short-frame controls (formats 0/2/3) | 82,944 | 1,296 | +| Chained format-7 normal/CM/CT/CU/CP (vectors 49–55) | 428,544 | 4,752 | +| Total new | 511,488 | 6,048 | + +Each scenario checks RTE and the following BRA or CM MOVEM. Pending CT/CU/CP +scenarios also return through the converted handler frame. Exact PC/SR/registers, +every stack bank, guarded frame/neighbor memory, discarded-PC nonfetch, +handler-owned writeback non-replay and pending-vector consumption are checked. +CM executes MOVEM.L (A0),D0–D1 using the saved EA despite a changed A0. Restored +trace applies to the following instruction. These are synthetic return frames, +not captures or qualification of real fault entry, physical bus transfer order, +enabled MMU or timing. Production CPU source is unchanged from `9fa9f84`. + +The maintained command +`./scripts/test-copper68k-synthetic-mutations.ps1 -Scope Consolidation` +now includes both chain termination and missing stack selection. Early +termination fails 193,536 RTE prerequisites, leaving 317,952 dependent phases +untested. Missing selection passes 85,248 phases on unchanged-stack paths and +fails 161,280 RTE prerequisites, leaving 264,960 dependent phases untested. +Neither mutation is accepted on compiler failure or a missing batch: both +complete new reporting selections must run and fail. Reports preserve exact +failing case IDs and status totals. All four consolidation mutations detect +their defects; `finally` restores source byte-for-byte and rebuilds it. +Evidence is `artifacts/m6-throwaway-mutations-final/mutation-proof.json`. +An earlier fixture used simultaneous T0/T1 in a discarded SR; that input was +removed and the proofs rerun rather than included as required defined coverage. + +The complete discovery command still exits 1 with one inventory failure: +`artifacts/m6-throwaway-discovery-final/` reports 887,520 logical phases, +887,040 passing, zero mismatching/unsupported and 480 explicitly untested. +Sixteen xUnit cases execute: fifteen pass and the remaining inventory fails. +The promoted throwaway requirement is replaced by these executable matrices; +validation faults, odd final user trace PCs, real access-fault entry, +writeback-handler qualification and CP context transfer retain their named +requirements across all banks/CCRs. Exact report-combination selection and +fixture/source/assembly identities are verified. No regression is retired. +Milestone 6 and the full goal remain **in progress**, with all other-model and +broad independent qualification/consolidation requirements preserved. + +The next odd-PC requirement has an executable exploratory reproduction at +`artifacts/m6-rte-odd-probe/`, with output +`artifacts/m6-rte-odd-probe-output-with-baseline.log`. Twenty-four public-factory +cases cover short formats 0/2/3 and normal format 7, ISP/MSP entry and user return +with no trace/T1/T0. RTE returns to odd PC `$6001` without changing the exception +sequence; the following execution takes vector 3 with a format-0 frame. In the +sixteen traced cases, the stacked SR still has S clear. MC68040UM sections 8.4, +8.2.2 and 8.4.6.7 instead require an address error during user traced restoration, +S set in the saved SR and a format-2 address-error frame. This is a reproduced +defect, not passing qualification. Exact saved PC, fault timing, stack bank/pop +ordering and pending-continuation priorities need independent qualification in +the fixing matrix; its named inventory requirement remains. Probe CPU assembly +SHA-256 is `c03904a1e9fb60ebe542450c50c2929b9e5865389041aad688ced528f9217ed0`. +Initial probe dependency/setup failures are retained separately and not counted +as execution evidence. The current checkpoint does not change CPU behavior. + +Final Release CPU validation passes 4,952 tests, eleven optional/opt-in skips +and zero failures with all four qualified WinUAE presets enabled. The strict +report gate passes 15,533,656 logical cases in 583 reporting batches, including +fresh pinned SingleStepTests (312,500 cases / 125 files) and Musashi (536 +programs / 88 explicit exclusions across eight profiles). Evidence is +`artifacts/m6-throwaway-full/` and `artifacts/m6-throwaway-gate.log`. +All thirteen malformed-input/report controls reject their intended defects in +`artifacts/m6-throwaway-preflight-controls/controls.json`. The latest CPU source +tree remains `e7de8cba19a6b182b8cb329db8f79194eed51f35`, unchanged from the +preceding private `.55` consumer-validated production checkpoint. Consumer +replays and package publication are not repeated for this test/reporting-only +change. All previously recorded native results and broad discovery disagreements +remain in force, with their original scope and identities. No package is published. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index f6c47342..795f499f 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1226,3 +1226,29 @@ SingleStepTests and Musashi audits. Private unpublished `.55` validates the isolated CopperScreen Release build, host/disk/engine tests and all three native boot replays through NuGet. Package/assets/loaded DLL identities match; see the reference record for counts and hashes. No package publication is included. + +### Milestone 6 throwaway chaining qualification — 2026-10-05 + +Independent short/access matrices add 511,488 passing phases for every one/two +throwaway bank path, all restored banks/CCRs/trace states and even/odd stack data +addresses. Normal/CM/CT/CU/CP conversion, exact stack/PC/SR, following instruction, +discarded-PC nonfetch and handler writeback non-replay are checked. Production +source is unchanged from `9fa9f84`. Maintained consolidation mutations detect +premature termination and missing stack selection, then restore/rebuild source. + +The complete 040 discovery remains failing: 887,040 passing phases, zero +mismatching/unsupported and 480 untested validation/fault/context requirements. +The throwaway inventory item is promoted to executable coverage; the remaining +five categories and all broader milestone requirements are retained. No old +regression is retired. See the +[reference record](COPPER68K_REFERENCE_QUALIFICATION.md#040-chained-throwaway-qualification--2026-10-05). +Milestone 6 and the goal remain **in progress**; `roadmapComplete=false`. + +Validation passes 4,952 Release CPU tests (eleven optional/opt-in skips), +15,533,656 logical cases in 583 strict reporting batches, and fresh pinned +SingleStepTests/Musashi audits. All four maintained consolidation mutations +detect their defects and all thirteen report/input controls remain effective. +Production source and the preceding private `.55` consumer evidence are +unchanged; no new package is published. An exploratory odd-user-trace-PC probe +reproduces the next restoration defect; it remains required fixing work rather +than passing qualification. See the reference record for exact evidence. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index df253514..34ee79bb 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -15,7 +15,9 @@ $expected = [ordered]@{ 'rte-access-movem-full-index' = @{cases=114048; combinations=1188} 'rte-access-fpu-unimplemented' = @{cases=13824; combinations=144} 'rte-access-fpu-post' = @{cases=96768; combinations=1008} - 'rte-access-remaining-protocols' = @{cases=576; combinations=18} + 'rte-throwaway-controls' = @{cases=82944; combinations=1296} + 'rte-throwaway-access' = @{cases=428544; combinations=4752} + 'rte-access-remaining-protocols' = @{cases=480; combinations=15} } $testExit = 0 if (-not $ValidateReportsOnly) { @@ -28,19 +30,19 @@ if (-not $ValidateReportsOnly) { $saved[$name] = [Environment]::GetEnvironmentVariable($name) [Environment]::SetEnvironmentVariable($name, $settings[$name]) } - & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output $testExit = $LASTEXITCODE } finally { foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } } $identity = [ordered]@{ schema=1; reference='MC68040UM'; url='https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf' - sections=@('8.4.1','8.4.3','8.4.4','8.4.6.2','8.4.6.7'); softwareReferenceExecuted=$false + sections=@('8.4.1','8.4.2','8.4.3','8.4.4','8.4.6.2','8.4.6.7'); softwareReferenceExecuted=$false sourceCommit=(& git -C $repo rev-parse HEAD); cpuCommittedTree=(& git -C $repo rev-parse HEAD:Copper68k) cpuSourceFiles=@(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } - inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs') | ForEach-Object { + inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } assemblies=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | ForEach-Object { @@ -49,7 +51,7 @@ if (-not $ValidateReportsOnly) { limitations=@('Canonical synthetic frames, not hardware captures or enabled-MMU access faults', 'CP context transfer and detailed fault protocol inventory remain untested and fail the gate', 'Multiple continuation bits are architecturally undefined and excluded', - 'Detailed fault validation, odd return PCs, throwaway-to-access frames and physical timing remain unqualified') + 'Detailed fault validation, odd return PCs and physical timing remain unqualified') } $identity | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'identities.json') } @@ -58,11 +60,11 @@ if (-not $ValidateReportsOnly) { $identity = Get-Content -LiteralPath (Join-Path $output 'identities.json') -Raw | ConvertFrom-Json if ($identity.schema -ne 1 -or $identity.reference -cne 'MC68040UM' -or $identity.softwareReferenceExecuted -ne $false -or $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.cpuCommittedTree -notmatch '^[a-f0-9]{40}$' -or - @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 4 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 4 -or + @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 5 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 5 -or @($identity.assemblies).Count -ne 2) { throw '040 access-frame input identity is missing or incomplete' } foreach ($input in $identity.inputs) { $path = Join-Path $repo $input.file - if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs') -or + if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs') -or $input.sha256 -cne (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()) { throw '040 access-frame fixture/command identity differs' } } $cpuFiles = @(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') @@ -76,8 +78,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 14 -or [int]$counters.total -ne 14 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (8 batches, 6 fixed examples)' +if ([int]$counters.executed -ne 16 -or [int]$counters.total -ne 16 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (10 batches, 6 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -96,10 +98,18 @@ foreach ($group in $expected.Keys) { $expectedCombinations = @{} if ($group -eq 'rte-access-remaining-protocols') { foreach ($bank in @('user','ISP','MSP')) { - foreach ($form in @('frame-validation-fault','odd-user-trace-PC','throwaway-to-access','real-access-fault-entry','writeback-handler','CP-context-transferred-vector')) { + foreach ($form in @('frame-validation-fault','odd-user-trace-PC','real-access-fault-entry','writeback-handler','CP-context-transferred-vector')) { $expectedCombinations["68040/RTE/format7/$form/bank=$bank"] = 32 } } + } elseif ($group -in @('rte-throwaway-controls','rte-throwaway-access')) { + $forms = if ($group -eq 'rte-throwaway-controls') { @('format0','format2','format3') } else { @('normal','CM','CT','CU','CP49','CP50','CP51','CP52','CP53','CP54','CP55') } + foreach ($start in @('ISP','MSP')) { foreach ($tail in @('user','ISP','MSP')) { foreach ($middle in @('none','user','ISP','MSP')) { + foreach ($result in @('user','ISP','MSP')) { foreach ($trace in @(0,0x8000,0x4000)) { foreach ($alignment in @(0,1)) { foreach ($form in $forms) { + $key = '68040/RTE/throwaway/{0}/start={1}/middle={2}/tail={3}/result={4}/T={5:X4}/align={6}' -f $form,$start,$middle,$tail,$result,$trace,$alignment + $expectedCombinations[$key] = 32 * $(if ($form -in @('CT','CU') -or $form.StartsWith('CP')) {3} else {2}) + } } } } + } } } } elseif ($group -in @('rte-access-fpu-unimplemented','rte-access-fpu-post')) { $post = $group -eq 'rte-access-fpu-post' foreach ($vector in $(if ($post) {49..55} else {@(11)})) { @@ -174,8 +184,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 14 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 16 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=376128; xunitBatches=8; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=887520; xunitBatches=10; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index 18aadfe8..c13a9cd3 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -105,7 +105,17 @@ $mutations += @( '@; after=@' if (false) flow |= traceOpcode is 0x4E71 or 0x4E7A or 0x4E7B || -'@; group='TraceRetirementTakenAndUntakenFlowStopsAndAbortingFaults'; milestone=6; model='68040'} +'@; group='TraceRetirementTakenAndUntakenFlowStopsAndAbortingFaults'; milestone=6; model='68040'}, + @{name='040-throwaway-chain-stop'; file=$advanced; before=' if (format == 1) continue; // Throwaway frame can select another stack.'; + after=' // Mutant: stop before the final chained frame.'; + group='SyntheticM68040ThrowawayTests'; milestone=6; model='68040'; throwaway=$true}, + @{name='040-throwaway-stack-selection'; file=$advanced; before=@' + State.StatusRegister = restoredStatus; + if (format == 1) continue; // Throwaway frame can select another stack. +'@; after=@' + if (format != 1) State.StatusRegister = restoredStatus; + if (format == 1) continue; // Mutant: retain the old stack selector. +'@; group='SyntheticM68040ThrowawayTests'; milestone=6; model='68040'; throwaway=$true} ) if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } @@ -142,12 +152,21 @@ try { $batches = @(Get-ChildItem -LiteralPath $directory -Filter '*.json' | ForEach-Object { Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json }) $failures = @($batches | ForEach-Object { $_.failures }) if ($exitCode -eq 0 -or $batches.Count -eq 0 -or $failures.Count -eq 0) { throw "Mutation survived or failed without executable semantic evidence: $($mutation.name)" } + if ($mutation.throwaway) { + [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + if ($trx.TestRun.ResultSummary.Counters.executed -ne 2 -or $trx.TestRun.ResultSummary.Counters.failed -ne 2 -or + $batches.Count -ne 2 -or @($batches | Where-Object { $_.group -eq 'rte-throwaway-controls' -and $_.logicalCases -eq 82944 }).Count -ne 1 -or + @($batches | Where-Object { $_.group -eq 'rte-throwaway-access' -and $_.logicalCases -eq 428544 }).Count -ne 1) { + throw 'Throwaway mutation did not execute its complete two-batch selection' + } + } if ($legacyPresent) { [xml]$legacyTrx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if (@($legacyTrx.TestRun.Results.UnitTestResult | Where-Object { $_.testName.EndsWith($mutation.legacyTest) -and $_.outcome -eq 'Failed' }).Count -ne 1) { throw 'Original regression did not detect the same mutation' } } $results += @{mutation=$mutation.name; file=$mutation.file; sourceSha256=(Get-FileHash -LiteralPath $path).Hash; - detected=$true; replacementCase=$failures[0].id; diagnostic=$failures[0].reason; originalRegressionDetected=$legacyPresent} + detected=$true; replacementCase=$failures[0].id; diagnostic=$failures[0].reason; originalRegressionDetected=$legacyPresent; + reports=@($batches | ForEach-Object { @{group=$_.group; logicalCases=$_.logicalCases; counts=$_.counts} })} Write-Host "Detected $($mutation.name): $($failures[0].id)" } finally { [IO.File]::WriteAllBytes($path, $saved[$path]) } diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 7e4db19c..e90dee79 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -145,6 +145,8 @@ try { $expected['system-move-word-restart-invalid'] = 224 } if ($model -eq '68040') { + $expected['rte-throwaway-controls'] = 82944 + $expected['rte-throwaway-access'] = 428544 $expected['rte-access-fpu-unimplemented'] = 13824 $expected['rte-access-fpu-post'] = 96768 $expected['rte-access-controls'] = 6912 @@ -248,7 +250,7 @@ try { seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; - m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure; CP context transfer, detailed fault validation, real fault entry and physical timing remain unqualified'; + m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks; CP context transfer, detailed fault validation, real fault entry and physical timing remain unqualified'; m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; marked private word-MOVE/MOVEA images resume their faulted cycle; other opaque restart/input state, long transfers, RMW and physical timing remain unqualified'; m68000DoubleFaultScope='Address-error entry/handler faults; reset-only recovery; external BERR/reset-vector faults unavailable through the current public bus API'; unavailableSystemCoverage=@('010 long/non-MOVE/external bus-fault restart and 020/030 internal restart', '020/030 coprocessor midinstruction restoration', '040 access-fault CP context transfer, detailed frame-validation/fault entry and writeback-handler qualification', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands'); From a194c12de87ad13401586a4a0f89eb87250e727d Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 11:13:43 +0300 Subject: [PATCH 034/211] Fix and qualify 68040 odd-PC returns and address-error frames --- Copper68k.Tests/M68040OddReturnStateTests.cs | 106 ++++++++++++ Copper68k.Tests/Synthetic/README.md | 20 ++- .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- .../SyntheticM68040OddReturnTests.cs | 156 ++++++++++++++++++ .../M68kAdvancedTimingInterpreter.Rte.cs | 37 ++++- Copper68k/M68kAdvancedTimingInterpreter.cs | 6 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 120 ++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 35 ++++ scripts/test-copper68k-040-access-frames.ps1 | 44 +++-- .../test-copper68k-synthetic-mutations.ps1 | 23 ++- scripts/test-copper68k-synthetic.ps1 | 5 +- 11 files changed, 530 insertions(+), 24 deletions(-) create mode 100644 Copper68k.Tests/M68040OddReturnStateTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs diff --git a/Copper68k.Tests/M68040OddReturnStateTests.cs b/Copper68k.Tests/M68040OddReturnStateTests.cs new file mode 100644 index 00000000..2f7ac7e5 --- /dev/null +++ b/Copper68k.Tests/M68040OddReturnStateTests.cs @@ -0,0 +1,106 @@ +using Copper68k.Tests.Synthetic; + +namespace Copper68k.Tests; + +public sealed class M68040OddReturnStateTests +{ + [Theory] + [InlineData("accurate")] + [InlineData("v1")] + [InlineData("v2")] + public void WarmDispatchFallsBackForTheOddReturnBeforeTargetAccess(string engine) + { + var bus = new SparseRecordingBus(); + var executionBus = new ImmutableCodeBus(bus); + bus.Initialize(0x2000, 0x60fe, 2); // warm a compiled self branch + bus.Initialize(0x1000, 0x4e73, 2); bus.Initialize(0x1002, 0x4e71, 2); + bus.Initialize(0x9000, 0x4e73, 2); bus.Initialize(0x9002, 0x4e71, 2); + bus.Initialize(0x9190, 0x4e73, 2); bus.Initialize(0x9192, 0x4e71, 2); + bus.Initialize(12, 0x9190, 4); bus.Initialize(55 * 4, 0x9000, 4); + IM68kCore core = engine == "accurate" ? M68kCoreFactory.Default.Create(M68kCpuModel.M68040, executionBus) + : M68kJitCore.CreateM68040ForTesting(executionBus, engine == "v2"); + using var disposable = core as IDisposable; + core.Reset(0x2000, 0x4700); core.State.CacheControlRegister = 1; + for (var n = 0; n < (engine == "accurate" ? 1 : 300); n++) Execute(core); + if (core is M68kJitCore warmed) + Assert.True(warmed.Counters.TraceHits + warmed.Counters.V2TraceHits > 0, "Require actual compiled warm dispatch before testing RTE fallback"); + foreach (var form in new[] { "format0", "format2", "format3", "CM", "CP55" }) + foreach (var master in new[] { false, true }) + foreach (var result in new[] { "user", "ISP", "MSP" }) + foreach (var trace in new ushort[] { 0, 0x8000, 0x4000 }) + foreach (var ccr in new[] { 0, 31 }) + { + var frame = master ? 0x7400u : 0x4700u; + var saved = (ushort)((result == "ISP" ? 0x2000 : result == "MSP" ? 0x3000 : 0) | trace | (ccr ^ 31)); + var initial = (ushort)((master ? 0x3000 : 0x2000) | ccr); + var format = form == "format0" ? 0 : form == "format2" ? 2 : form == "format3" ? 3 : 7; + var size = format == 0 ? 8u : format == 7 ? 60u : 12u; + core.State.SetUserStackPointer(0x7800); core.State.SetInterruptStackPointer(0x4700); core.State.SetMasterStackPointer(0x7400); + core.State.StatusRegister = initial; core.State.ProgramCounter = 0x1000; + bus.Initialize(frame, saved, 2); bus.Initialize(frame + 2, 0x6001, 4); + bus.Initialize(frame + 6, (uint)(format << 12) | 8, 2); + bus.Initialize(frame + 8, 0x4200, 4); bus.Initialize(frame + 12, form == "CP55" ? 0x8005u : form == "CM" ? 0x1005u : 0x0105u, 2); + if (form == "CP55") core.State.M68040PendingFpuExceptions.Begin(3, 55, 0x6001); + var d = (uint[])core.State.D.Clone(); var a = core.State.A.Take(7).ToArray(); + bus.Accesses.Clear(); + var fallbacks = core is M68kJitCore jit ? jit.Counters.FallbackInstructions : 0; + Execute(core); + if (form == "CP55") + { + Assert.Equal(0x9000u, core.State.ProgramCounter); Assert.Equal(55, core.State.LastExceptionVector); + Assert.Null(core.State.M68040PendingFpuExceptions.Find(3)); + Execute(core); // converted handler return now raises address error + } + if (core is M68kJitCore fallback) + Assert.True(fallback.Counters.FallbackInstructions > fallbacks, "RTE must use the interpreter fallback under warmed dispatch"); + var expectedSr = (ushort)((saved | 0x2000) & ~0xc000); + var bank = result == "MSP" ? "MSP" : "ISP"; + var sp = (bank == "MSP" ? 0x7400u : 0x4700u) + (bank == (master ? "MSP" : "ISP") ? size : 0) - 12; + var stacked = form == "CP55" ? expectedSr : initial; + Assert.Equal(0x9190u, core.State.ProgramCounter); Assert.Equal(3, core.State.LastExceptionVector); + Assert.Equal(expectedSr, core.State.StatusRegister); Assert.Equal(sp, core.State.A[7]); + Assert.Equal((uint)stacked, bus.Peek(sp, 2)); + Assert.Equal(form == "CP55" ? 0x9000u : 0x1000u, bus.Peek(sp + 2, 4)); + Assert.Equal(0x200cu, bus.Peek(sp + 6, 2)); Assert.Equal(0x6000u, bus.Peek(sp + 8, 4)); + Assert.Equal(d, core.State.D); Assert.Equal(a, core.State.A.Take(7).ToArray()); + Assert.DoesNotContain(bus.Accesses, access => access.Address is 0x6001 or 0x4200); + Assert.Equal(0x7800u, core.State.UserStackPointer); + } + } + + private static void Execute(IM68kCore core) + { + if (core is M68kJitCore jit) Assert.Equal(1, jit.ExecuteInstructions(1, core.State.Cycles + 1000, new Boundary())); + else core.ExecuteInstruction(); + } + private sealed class Boundary : IM68kBusAccessTraceBatchBoundary, IM68kPureCpuTraceBatchBoundary + { + public bool BeforeInstruction() => true; + public void AfterInstruction(long previous, long current) { } + public bool TryBeginBusAccessTraceBatch(M68kCpuState state, long target, out long batchTarget) + { batchTarget = target; return true; } + public void AfterBusAccessTraceBatch(long previous, long current, int count) { } + public bool TryBeginPureCpuTraceBatch(M68kCpuState state, long target, out long batchTarget) + { batchTarget = target; return true; } + public void AfterPureCpuTraceBatch(long previous, long current, int count) { } + } + private sealed class ImmutableCodeBus(SparseRecordingBus memory) : IM68kBus, IM68kJitBus + { + public event Action? JitCodeRangeWritten { add { } remove { } } + public bool IsJitCodeAddress(uint address, int count, M68kBusAccessKind kind) => + address >= 0x1000 && address + count <= 0x1004 || address >= 0x2000 && address + count <= 0x2004 || + address >= 0x9000 && address + count <= 0x9004 || address >= 0x9190 && address + count <= 0x9194; + public bool IsJitReadOnlyCodeAddress(uint address, int count, M68kBusAccessKind kind) => IsJitCodeAddress(address, count, kind); + public ushort ReadJitCodeWord(uint address) => (ushort)memory.Peek(address, 2); + public uint GetJitCodePageGeneration(uint address) => 0; + public bool JitCodeRangeGenerationMatches(uint address, int count, uint first, uint last) => first == 0 && last == 0; + public bool TryCaptureJitCodeSnapshot(uint address, int count, out M68kJitCodeSnapshot snapshot) { snapshot = default; return false; } + public void ResetExternalDevices(long cycle) => memory.ResetExternalDevices(cycle); + public byte ReadByte(uint address, ref long cycle, M68kBusAccessKind kind) => memory.ReadByte(address, ref cycle, kind); + public ushort ReadWord(uint address, ref long cycle, M68kBusAccessKind kind) => memory.ReadWord(address, ref cycle, kind); + public uint ReadLong(uint address, ref long cycle, M68kBusAccessKind kind) => memory.ReadLong(address, ref cycle, kind); + public void WriteByte(uint address, byte value, ref long cycle, M68kBusAccessKind kind) => memory.WriteByte(address, value, ref cycle, kind); + public void WriteWord(uint address, ushort value, ref long cycle, M68kBusAccessKind kind) => memory.WriteWord(address, value, ref cycle, kind); + public void WriteLong(uint address, uint value, ref long cycle, M68kBusAccessKind kind) => memory.WriteLong(address, value, ref cycle, kind); + } +} diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index de812cac..003636dd 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -179,8 +179,8 @@ CPU-space access-control responder. An ordinary bus has no such responder and produces a format error for type-1 modules; ordinary RAM is never used as one. Detailed RTE bus-fault restart/internal-state restoration on 010/020/030, -020/030 coprocessor midinstruction restoration and 040 access-fault pending -exceptions/writebacks remain **untested by this synthetic matrix**. They are +020/030 coprocessor midinstruction restoration and detailed 040 access-fault +entry/validation, writeback handlers and CP context transfer remain **untested**. They are listed separately in report qualification boundaries rather than reported as invalid frame formats or passing coverage. External BKPT instruction replacement, physical MOVES function-code buses and LPSTOP CPU-space broadcast are also @@ -227,8 +227,9 @@ it does not replace the deterministic synthetic gate or complete milestone 6. The independent 040 access-frame discovery command is `./scripts/test-copper68k-040-access-frames.ps1 -OutputDirectory artifacts/040-access-frame-audit`. It currently **fails** on 480 explicit fault/context requirements. Normal, -CT, CM, CU and CP restoration and chained throwaways now pass 887,040 logical -phases, including short-frame controls. Nine promoted batches run in ordinary CI. CP uses the +CT, CM, CU and CP restoration, chained throwaways and direct odd-PC returns now +pass 1,124,352 logical phases, including short-frame and odd-fetch controls. +Twelve promoted batches run in ordinary CI. CP uses the original suspended delivery vector; context-transferred frames without that state remain an implementation gap. The MOVEM matrices cover every legal opcode word and all 66 full-index structures; @@ -243,8 +244,17 @@ never fetched; terminal short/access frames preserve guarded memory and pending writebacks. CM's following MOVEM uses the saved EA. The maintained consolidation mutation command proves early chain termination and missing stack selection are detected, then restores and rebuilds source. Detailed validation/access faults, -odd final user trace PCs, real access-fault entry, writeback handlers and CP context +chained odd-PC saved-SR provenance, real access-fault entry, writeback handlers and CP context transfer remain required; no regression is retired by this checkpoint. +Direct short/normal/CM odd returns now raise a format-2 address error during RTE, +with the causing instruction PC and fault address A0 cleared. CT/CU/CP delivery +takes priority; its handler return then produces the address error. The matrices +check both return phases, explicit software repair and the following instruction. +Saved-SR image ordering follows documentary pinned WinUAE source rather than an +executed hardware oracle; chained user-tail provenance remains an explicit gap. +Separate accurate/V1/V2 tests run 540 state scenarios and witness warmed compiled +dispatch followed by RTE fallback. Run the four discriminating mutations with +`./scripts/test-copper68k-synthetic-mutations.ps1 -Scope Rte040`. The command checks exact selections, combinations, counts and source/assembly identities. Existing outputs must be validated with `-ValidateReportsOnly` or a fresh output directory used. It cannot pass merely by fixing normal/trace return. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 03c7367e..d2250868 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -29,7 +29,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() // Preserve the unqualified protocols explicitly after promoting CU/CP. foreach (var bank in Banks) for (var ccr = 0; ccr < 32; ccr++) - foreach (var form in new[] { "frame-validation-fault", "odd-user-trace-PC", + foreach (var form in new[] { "frame-validation-fault", "odd-PC-chained-SR-provenance", "real-access-fault-entry", "writeback-handler", "CP-context-transferred-vector" }) report.Record($"68040/RTE/format7/{form}/bank={bank}/op=4E73/ccr={ccr:X2}", "untested", "Required fault/context protocol has no independently qualified execution fixture yet"); diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs new file mode 100644 index 00000000..7c511f48 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs @@ -0,0 +1,156 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 8.2.2, 8.4, 8.4.3/6.7. The additional SR image ordering +// agrees with documentary WinUAE 5d22d336 (not an executed hardware oracle). +// Direct ISP/MSP frames only; chained user-frame SR provenance stays explicit. +public sealed class SyntheticM68040OddReturnTests(ITestOutputHelper output) +{ + private const uint Rte = 0x1000, PendingHandler = 0x9090, AddressHandler = 0x9190, Repaired = 0x6200; + private static readonly string[] Banks = ["user", "ISP", "MSP"]; + + [Fact, Trait("Suite", "Synthetic")] + public void OddShortNormalAndMovemReturnsRaiseAddressErrorDuringRte() => Audit(false); + + [Fact, Trait("Suite", "Synthetic")] + public void PendingExceptionsPrecedeTheOddReturnAddressError() => Audit(true); + + private void Audit(bool pending) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); + var report = new CoverageBatch("68040", pending ? "rte-odd-pending" : "rte-odd-normal"); + foreach (var form in pending ? new[] {"CT", "CU", "CP49", "CP50", "CP51", "CP52", "CP53", "CP54", "CP55"} + : new[] {"format0", "format2", "format3", "normal", "CM"}) + foreach (var entry in new[] {"ISP", "MSP"}) + foreach (var result in Banks) + foreach (var trace in new ushort[] {0, 0x8000, 0x4000}) + foreach (var target in new uint[] {0x6001, 0xff002003}) + foreach (var vbr in new uint[] {0, 0x10000}) + foreach (var alignment in new uint[] {0, 1}) + for (var ccr = 0; ccr < 32; ccr++) + { + m.Reset(ccr); + var frame = (entry == "ISP" ? 0x4700u : 0x7400u) + alignment; + var sr = Status(result, trace, ccr ^ 31); + var initialSr = Status(entry, 0, ccr); + var format = form == "format0" ? 0 : form == "format2" ? 2 : form == "format3" ? 3 : 7; + var vector = form == "CT" ? 9 : form == "CU" ? 11 : form.StartsWith("CP") ? int.Parse(form[2..]) : 0; + var ssw = form == "CM" ? 0x1005 : form == "CT" ? 0x2005 : form == "CU" ? 0x4005 : vector >= 49 ? 0x8005 : 0x0105; + for (var n = -20; n < 84; n++) m.InitializePhysical(unchecked(frame + (uint)n), (uint)(n ^ 0x5a), 1); + m.InitializePhysical(frame, sr, 2); m.InitializePhysical(frame + 2, target, 4); + m.InitializePhysical(frame + 6, (uint)(format << 12) | 8, 2); + m.InitializePhysical(frame + 8, 0x4200, 4); m.InitializePhysical(frame + 12, (uint)ssw, 2); + foreach (var offset in new[] {14u, 16u, 18u}) m.InitializePhysical(frame + offset, 0x85, 2); + foreach (var offset in new[] {24u, 32u, 40u}) + { + m.InitializePhysical(frame + offset, 0x3200 + offset, 4); + m.InitializePhysical(frame + offset + 4, 0xc3a55a3c, 4); + m.InitializePhysical(0x3200 + offset, 0x96abcdef, 4); + } + m.InitializePhysical(AddressHandler, 0x4e73, 2); m.InitializePhysical(AddressHandler + 2, 0x4e71, 2); + m.InitializePhysical(PendingHandler, 0x4e73, 2); m.InitializePhysical(PendingHandler + 2, 0x4e71, 2); + m.InitializePhysical(Repaired, 0x60fe, 2); + m.InitializePhysical(vbr + 12, AddressHandler, 4); + foreach (var v in new[] {9, 11, 49, 50, 51, 52, 53, 54, 55}) m.InitializePhysical(vbr + (uint)v * 4, PendingHandler, 4); + _ = SyntheticExecution.Prepare(m, [0x4e73]); + m.Core.State.SetMasterStackPointer(0x7400 + alignment); + m.Core.State.SetInterruptStackPointer(0x4700 + alignment); + m.Core.State.StatusRegister = initialSr; m.Core.State.VectorBaseRegister = vbr; + if (form == "CU") m.Core.State.M68040PendingFpuExceptions.Begin(2, vector, target); + if (vector >= 49) m.Core.State.M68040PendingFpuExceptions.Begin(3, vector, target); + var e = ArchitecturalExpectation.Capture(m); + var pointers = new Dictionary { ["user"] = 0x7800, ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; + pointers[entry] += format == 7 ? 60u : format == 0 ? 8u : 12u; + e.OperandAccessAddresses.UnionWith(new uint[] {0x3218, 0x3220, 0x3228, 0x4200}); + e.ExpectedOperandTransfers = []; + e.ControlChecks["odd target never fetched"] = (_ => m.Bus.Accesses.Any(a => !a.Write && a.Address == target) ? 1u : 0u, 0); + if (!pending) e.ControlChecks["no MOVEM continuation after odd target"] = (_ => + ((M68kAdvancedTimingInterpreter)m.Core).HasPendingM68040MovemContinuation ? 1u : 0u, 0); + if (vector == 11 || vector >= 49) e.ControlChecks["pending delivery consumed"] = (s => + s.M68040PendingFpuExceptions.Find(vector == 11 ? 2 : 3) == null ? 0u : 1u, 0); + var sequence = m.Core.State.ExceptionSequence; + var id = $"68040/RTE/odd/{form}/entry={entry}/result={result}/T={trace:X4}/target={target:X8}/VBR={vbr:X8}/align={alignment}/op=4E73/ccr={ccr:X2}"; + if (pending) Exception(m, e, pointers, sr, sr, vector, target, 0x4200, PendingHandler, sequence + 1); + else Exception(m, e, pointers, sr, initialSr, 3, Rte, target & ~1u, AddressHandler, sequence + 1); + var phases = pending ? new[] {"pending-return", "repaired-return", "following-BRA"} : new[] {"repaired-return", "following-BRA"}; + if (!SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/RTE")) + { foreach (var phase in phases) report.Record(id + "/" + phase, "untested", "RTE prerequisite failed"); continue; } + if (pending) + { + pointers[SupervisorBank(sr)] += 12; + var handlerSr = (ushort)((sr | 0x2000) & ~0xc000); + Exception(m, e, pointers, sr, handlerSr, 3, PendingHandler, target & ~1u, AddressHandler, sequence + 2); + if (!SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/pending-return")) + { report.Record(id + "/repaired-return", "untested", "Pending return prerequisite failed"); report.Record(id + "/following-BRA", "untested", "Pending return prerequisite failed"); continue; } + } + // Address faults require software repair. Never retry the consumed RTE + // or a partially completed operand. Return to a new, even instruction. + var addressFrame = pointers[SupervisorBank(sr)]; + m.InitializePhysical(addressFrame, sr, 2); e.Write(addressFrame, sr, 2, m.Model); + m.InitializePhysical(addressFrame + 2, Repaired, 4); e.Write(addressFrame + 2, Repaired, 4, m.Model); + pointers[SupervisorBank(sr)] += 12; + SetStacks(e, pointers, sr); e.Pc = Repaired; e.ExceptionVector = null; + if (!SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/repaired-return")) + { report.Record(id + "/following-BRA", "untested", "Repair return prerequisite failed"); continue; } + if (trace != 0) Exception(m, e, pointers, sr, sr, 9, Repaired, Repaired, PendingHandler, sequence + (pending ? 3u : 2u)); + SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/following-BRA"); + } + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void OddInstructionFetchUsesTheArchitecturalAddressErrorFrame() + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); + var report = new CoverageBatch("68040", "address-error-fetch-040"); + foreach (var bank in Banks) + foreach (var trace in new ushort[] {0, 0x8000, 0x4000}) + foreach (var target in new uint[] {0x6001, 0xff002003}) + foreach (var vbr in new uint[] {0, 0x10000}) + foreach (var alignment in new uint[] {0, 1}) + for (var ccr = 0; ccr < 32; ccr++) + { + m.Reset(ccr); + _ = SyntheticExecution.Prepare(m, [0x4e71]); + m.InitializePhysical(vbr + 12, AddressHandler, 4); + m.Core.State.SetMasterStackPointer(0x7400 + alignment); + m.Core.State.SetInterruptStackPointer(0x4700 + alignment); + var sr = Status(bank, trace, ccr); + m.Core.State.StatusRegister = sr; m.Core.State.ProgramCounter = target; m.Core.State.VectorBaseRegister = vbr; + var pointers = new Dictionary { ["user"] = 0x7800, ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; + var e = ArchitecturalExpectation.Capture(m); + e.ControlChecks["odd target never fetched"] = (_ => m.Bus.Accesses.Any(a => !a.Write && a.Address == target) ? 1u : 0u, 0); + Exception(m, e, pointers, sr, sr, 3, target, target & ~1u, AddressHandler, m.Core.State.ExceptionSequence + 1); + SyntheticM68040AccessFrameAuditTests.Step(m, e, report, + $"68040/fetch/odd/bank={bank}/T={trace:X4}/target={target:X8}/VBR={vbr:X8}/align={alignment}/op=0000/ccr={ccr:X2}"); + } + report.Complete(output); + } + + private static ushort Status(string bank, ushort trace, int ccr) => + (ushort)((bank == "ISP" ? 0x2000 : bank == "MSP" ? 0x3000 : 0) | trace | ccr); + private static string SupervisorBank(ushort sr) => (sr & 0x1000) != 0 ? "MSP" : "ISP"; + private static void SetStacks(ArchitecturalExpectation e, Dictionary pointers, ushort sr) + { + e.Sr = sr; e.A[7] = pointers[(sr & 0x2000) == 0 ? "user" : SupervisorBank(sr)]; + e.InactiveStackPointer = pointers[(sr & 0x2000) == 0 ? "ISP" : "user"]; e.MasterStackPointer = pointers["MSP"]; + e.ControlChecks["USP"] = (s => s.UserStackPointer, pointers["user"]); + e.ControlChecks["ISP"] = (s => s.InterruptStackPointer, pointers["ISP"]); + e.ControlChecks["MSP"] = (s => s.MasterStackPointer, pointers["MSP"]); + } + private static void Exception(SyntheticMachine m, ArchitecturalExpectation e, Dictionary pointers, + ushort restoredSr, ushort savedSr, int vector, uint pc, uint address, uint handler, uint sequence) + { + var bank = SupervisorBank(restoredSr); pointers[bank] -= 12; + SetStacks(e, pointers, (ushort)((restoredSr | 0x2000) & ~0xc000)); + e.Pc = handler; e.ExceptionVector = vector; + e.ControlChecks["exception entries"] = (s => s.ExceptionSequence, sequence); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, pc); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, savedSr); + var stack = pointers[bank]; + e.Write(stack, savedSr, 2, m.Model); e.Write(stack + 2, pc, 4, m.Model); + e.Write(stack + 6, (uint)((vector >= 49 ? 3 : 2) << 12 | vector * 4), 2, m.Model); e.Write(stack + 8, address, 4, m.Model); + } +} diff --git a/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs b/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs index 0da7ea98..6710497c 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs @@ -8,6 +8,7 @@ internal partial class M68kAdvancedTimingInterpreter private bool TryRestoreM68040AccessFrame(uint frame, ushort sr, uint pc) { + var priorSr = State.StatusRegister; var continuation = ReadWord(frame + 12) & 0xf000; // Multiple continuation bits are undefined (MC68040UM 8.4.6.7). if (continuation is not (0 or 0x1000 or 0x2000 or 0x4000 or 0x8000)) return false; @@ -23,8 +24,6 @@ private bool TryRestoreM68040AccessFrame(uint frame, ushort sr, uint pc) State.SetActiveStackPointer(unchecked(frame + 60)); State.StatusRegister = sr; State.ProgramCounter = pc; - if (continuation == 0x1000) - _m68040MovemContinuation = (pc, address); if (continuation >= 0x2000) { // MC68040UM 8.4.6.2/7: convert the pending exception frame at @@ -43,10 +42,42 @@ private bool TryRestoreM68040AccessFrame(uint frame, ushort sr, uint pc) State.M68040PendingFpuExceptions.Complete(pending); CompleteTiming(continuation == 0x2000 ? M68kInstructionTimingKey.IllegalInstruction : M68kInstructionTimingKey.LineFException); } - else CompleteTiming(M68kInstructionTimingKey.Rte); + else + { + if (TryRaiseM68040RteAddressError(pc, priorSr, State.LastInstructionProgramCounter)) return true; + if (continuation == 0x1000) _m68040MovemContinuation = (pc, address); + CompleteTiming(M68kInstructionTimingKey.Rte); + } return true; } + private bool TryRaiseM68040RteAddressError(uint target, ushort priorSr, uint rtePc) + { + if (_profile.Model != M68kAcceleratorModel.M68040 || (target & 1) == 0) return false; + // MC68040UM 8.4 requires S in the saved SR on a traced user return. + // Documentary WinUAE retains the pre-restoration SR image, while the + // restored SR selects the live exception bank and CCR. Chained USP SR + // provenance remains separately unqualified rather than disappearing. + if ((State.StatusRegister & 0x2000) == 0 && (State.StatusRegister & 0xc000) != 0) + priorSr |= M68kCpuState.Supervisor; + RaiseM68040AddressError(rtePc, target, priorSr); + return true; + } + + private void RaiseM68040AddressError(uint instructionPc, uint faultAddress, ushort savedSr) + { + // MC68040UM 8.2.2, 8.4.3/6.7: format 2, fault address with A0 + // cleared, and the instruction which caused the failed prefetch. + State.RecordException(3, instructionPc, savedSr); + State.StatusRegister = (ushort)((State.StatusRegister | M68kCpuState.Supervisor) & ~0xc000); + PushLong(faultAddress & ~1u); + PushWord(0x200c); + PushLong(instructionPc); + PushWord(savedSr); + State.ProgramCounter = ReadLong(State.VectorBaseRegister + 12); + CompleteTiming(M68kInstructionTimingKey.IllegalInstruction); + } + private bool TryExecuteM68040MovemContinuation(ushort opcode) { if (_m68040MovemContinuation is not { } saved || saved.Pc != State.ProgramCounter) return false; diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index d910bf87..3c2136cc 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -4553,7 +4553,9 @@ private void ExecuteInstructionBody() { var instructionPc = State.ProgramCounter; BeginInstruction(0); - RaiseFormat0Exception(3, instructionPc, M68kInstructionTimingKey.IllegalInstruction); + if (_profile.Model == M68kAcceleratorModel.M68040) + RaiseM68040AddressError(instructionPc, instructionPc, State.StatusRegister); + else RaiseFormat0Exception(3, instructionPc, M68kInstructionTimingKey.IllegalInstruction); return; } @@ -8652,6 +8654,7 @@ private void ExecuteRte() while (true) { var framePointer = State.A[7]; + var priorSr = State.StatusRegister; var restoredStatus = ReadWord(framePointer); var restoredPc = ReadLong(framePointer + 2); var format = ReadWord(framePointer + 6) >> 12; @@ -8663,6 +8666,7 @@ private void ExecuteRte() State.SetActiveStackPointer(framePointer + size); State.StatusRegister = restoredStatus; if (format == 1) continue; // Throwaway frame can select another stack. + if (TryRaiseM68040RteAddressError(restoredPc, priorSr, instructionPc)) return; State.ProgramCounter = restoredPc; break; } diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 65665763..e8c7e752 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -2885,3 +2885,123 @@ preceding private `.55` consumer-validated production checkpoint. Consumer replays and package publication are not repeated for this test/reporting-only change. All previously recorded native results and broad discovery disagreements remain in force, with their original scope and identities. No package is published. + +## 040 odd-PC return correction — 2026-10-05 + +The advanced 040 interpreter now takes an address error during a direct RTE +return to an odd instruction address. Short formats 0/2/3 and normal/CM format 7 +pop their original frame and restore the selected stack before entering vector 3. +The address-error frame is format 2 (`$200c`), saves the RTE instruction PC and +stores the failed prefetch address with A0 cleared. Standalone odd instruction +fetch uses the same architectural frame, saving the current instruction PC. +Neither route reads the odd target. A rejected CM return installs no MOVEM +continuation and never retries completed operands. Existing timing keys are +preserved; this is not physical timing qualification. + +The primary authority is [MC68040UM](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +sections 8.2.2, 8.4, 8.4.3 and 8.4.6.7. The manual explicitly requires S set +in the stacked SR when restoring a traced user PC, and gives CT/CU/CP precedence +over an odd returned PC. Those pending exceptions therefore convert/deliver +first; their handler's later RTE takes the address error. The original saved EA +and pending vector remain intact until that delivery completes. + +Additional pre-restoration SR-image ordering is documentary software evidence: +[WinUAE gencpu.cpp](https://github.com/tonioni/WinUAE/blob/5d22d33632646efc3f747f03e82d28353e52722e/gencpu.cpp) +passes `oldsr` to `exception3_read_prefetch_68040bug`, and +[newcpu.cpp](https://github.com/tonioni/WinUAE/blob/5d22d33632646efc3f747f03e82d28353e52722e/newcpu.cpp) +uses that SR for the stacked image while retaining restored live SR. The source +pin is `5d22d33632646efc3f747f03e82d28353e52722e`; local identities under +`artifacts/m6-rte-address-winuae/` are: + +| Documentary file | SHA-256 | +| --- | --- | +| newcpu.cpp | `eb538884af9c14012b083db9b1e18106f1b351be7e3aca59767d915911beb6f5` | +| gencpu.cpp | `e993c18b19c68a98ec5404c2ee2983dba0f17252cff42a1ba932c92d8c13f094` | +| cpummu.cpp | `a833e9adcddbe68483bad603b1a8b8554393f21f1d319b2b138bb3fbabd94a08` | + +These source files were inspected, not executed as a new reference oracle or +observed on hardware. They are distinct from the executed emoon tester/generator +pin `025b999…` already recorded above. Chained throwaways selecting a user tail +leave a saved-SR provenance question between that software path and the manual's +S requirement. The direct ISP/MSP matrices do not qualify it. Its original +inventory scope remains as `odd-PC-chained-SR-provenance` (three banks/all CCRs), +alongside frame-validation faults, real access-fault entry, writeback handlers +and CP context transfer. No requirement is removed or relabeled invalid. + +| New ordinary-CI group | Passing phases | Combinations | +| --- | ---: | ---: | +| Direct short/normal/CM odd returns | 69,120 | 720 | +| CT/CU/CP odd-return priority and handler returns | 165,888 | 1,296 | +| Standalone odd instruction fetch | 2,304 | 72 | +| Total new | 237,312 | 2,088 | + +The independent fixtures cover all CCRs, distinct restored CCRs, ISP/MSP entry, +all restored banks, T0/T1/no trace, two odd target addresses including a full +32-bit address, two VBRs and even/odd stack data addresses. They check exact +registers, every stack pointer, frame and neighbor memory, saved PC/SR, exception +sequence, pending delivery consumption and operand/writeback non-replay. A +handler explicitly repairs the frame to a new even PC, then returns and executes +the following BRA/trace; the consumed faulty RTE is never retried. + +Before the production fix, `artifacts/m6-rte-odd-before/` records 66,816 +mismatching prerequisites, 41,472 passing pending-delivery phases and 129,024 +dependent phases untested. After the fix all new phases pass. Separate +`M68040OddReturnStateTests` execute 540 scenarios across accurate/V1/V2 dispatch; +both JIT engines must witness compiled warm dispatch and actual RTE fallback. +This does not claim a compiled RTE implementation. Together with retained +FPU/MOVEM continuation tests, the qualified focused selection passes 24 cases +in `artifacts/m6-rte-odd-state-qualified/state.trx`. + +The maintained `./scripts/test-copper68k-synthetic-mutations.ps1 -Scope Rte040` +requires all three complete reports to execute for each mutation. Wrong frame +format and uncleared A0 each fail 66,816 phases, pass 41,472 pending prerequisites +and leave 129,024 downstream phases untested. Substituting restored SR for the +saved image fails 50,688, passes 85,248 and leaves 101,376 untested. Taking the +address error ahead of CT/CU/CP fails 41,472, passes 71,424 and leaves 124,416 +untested. All four mutants fail semantically, not on compilation or missing +selection; exact IDs/counts are retained in +`artifacts/m6-rte-odd-mutations/mutation-proof.json`. Source is restored +byte-for-byte and rebuilt. No specialized regression is retired. + +Milestone 6 and the goal remain **in progress** with `roadmapComplete=false`. +Other-model restoration, broader independent qualification, the recorded Basic +disagreements and consolidation remain required. Enabled MMU, FPU arithmetic, +physical timing and OS compatibility are outside this roadmap. + +Isolated unpublished NuGet `1.5.2-synthetic-dev.56` validates CopperScreen at +baseline `d9beae8b88be24032221e3482942a249c03c27d3`: Release build has zero +warnings/errors; host 149 passing/six optional skips, disk 74, separate engine +diagnostics 1,080 and native Workbench boots two/A1200 AGA persistence one pass +without native skips. Four restored assets and four loaded CPU assemblies match +the package. Package SHA-256 is +`8377ea7330a5a4f22b944735e4ea611bdb43bfe0d1ba218da37f69994d5dc540`; +CPU assembly SHA-256 is +`1c0a788adf1443bf4cf34049115080083dcab7c2f568065335e5994944ffe26c`. +Consumer evidence is its `artifacts/rte-odd-identities.json`, +`artifacts/rte-odd-validation/` and separate diagnostic outputs. Published +package versions, the root CopperScreen dependencies and unrelated changes are +preserved. No package is published. + +Final Release CPU validation passes 4,958 tests, eleven optional/opt-in skips +and zero failures with all four qualified WinUAE presets enabled. The strict +gate verifies 15,770,968 passing logical cases in 586 reporting batches, plus +fresh pinned SingleStepTests (312,500 cases / 125 files) and Musashi (536 +programs / 88 explicit exclusions across eight profiles). Evidence is +`artifacts/m6-rte-odd-full/` and `artifacts/m6-rte-odd-gate.log`. + +Complete 040 discovery remains failing in `artifacts/m6-rte-odd-discovery/`: +1,124,832 logical phases, 1,124,352 passing, zero mismatching/unsupported and +480 explicitly untested. Nineteen xUnit cases execute: eighteen pass and the +remaining inventory fails. Exact combination, cardinality, input/source and +assembly selections are checked; passing direct odd returns does not complete +the broader return/fault gate. + +All thirteen malformed-input/report controls detect their specific defects in +`artifacts/m6-rte-odd-preflight-controls/controls.json`. The fresh broad Basic +audit still fails in `artifacts/m6-rte-odd-broad/`: 1,326 passing directories, +47 mismatching and eight unsupported, with 11,474,194 callbacks, 1,663,891 +exception frames and 199,327 masked-SR cases. All 32 comparator controls detect +their defects. Generator/runner/native-library/manifest pins match the preceding +record; each model/opcode row retains its status and callback/frame/mask counts. +This checkpoint neither hides those disagreements nor treats discovery as a +passing qualification gate. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 795f499f..dba5ca3a 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1252,3 +1252,38 @@ Production source and the preceding private `.55` consumer evidence are unchanged; no new package is published. An exploratory odd-user-trace-PC probe reproduces the next restoration defect; it remains required fixing work rather than passing qualification. See the reference record for exact evidence. + +### Milestone 6 direct odd-PC return implementation — 2026-10-05 + +040 direct odd-PC returns now take a format-2 address error during RTE, saving +the causing instruction PC and the aligned fault address without fetching the +odd target. CT/CU/CP delivery has priority; its handler return then takes the +address error. CM retains no unusable continuation. Three independent ordinary +CI batches add 237,312 passing phases; 540 accurate/V1/V2 state scenarios require +warmed compiled dispatch and RTE fallback. Four maintained `-Scope Rte040` +mutations detect frame, address, saved-SR and pending-priority defects, then +restore/rebuild production source. + +The direct saved-SR image follows documentary pinned WinUAE source; it is not +an executed hardware oracle. The chained user-tail SR question retains its +explicit inventory requirement. Complete 040 discovery still requires 480 +fault/context cases; other-model restoration, broader independent coverage and +consolidation remain required. No regression is retired. The +[reference record](COPPER68K_REFERENCE_QUALIFICATION.md#040-odd-pc-return-correction--2026-10-05) +records primary manual authority, source identities and failed-before/mutation +evidence. Milestone 6 and the full goal remain **in progress** with +`roadmapComplete=false`; public release authorization remains separate. + +Validation passes 4,958 Release CPU tests (eleven optional/opt-in skips), +15,770,968 logical cases in 586 strict reporting batches and fresh pinned +SingleStepTests/Musashi audits. Complete 040 discovery passes 1,124,352 phases, +has no mismatches/unsupported promoted phases and still fails on 480 named +untested requirements. Isolated unpublished `.56` validates CopperScreen through +NuGet: clean Release build, host 149/six skips, disk 74, separate engine 1,080, +and all three native Workbench/A1200 replays pass without skips. Package/assets/ +loaded DLL identities match. No public package is published. + +All thirteen input/report controls detect their defects. Fresh broad Basic +discovery retains 1,326 passing directories, 47 mismatching and eight unsupported, +with the same per-model/opcode statuses and callback/frame/mask counts; its 32 +comparator controls remain effective. These gaps remain required work. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index 34ee79bb..55880de7 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -17,6 +17,9 @@ $expected = [ordered]@{ 'rte-access-fpu-post' = @{cases=96768; combinations=1008} 'rte-throwaway-controls' = @{cases=82944; combinations=1296} 'rte-throwaway-access' = @{cases=428544; combinations=4752} + 'rte-odd-normal' = @{cases=69120; combinations=720} + 'rte-odd-pending' = @{cases=165888; combinations=1296} + 'address-error-fetch-040' = @{cases=2304; combinations=72} 'rte-access-remaining-protocols' = @{cases=480; combinations=15} } $testExit = 0 @@ -30,19 +33,19 @@ if (-not $ValidateReportsOnly) { $saved[$name] = [Environment]::GetEnvironmentVariable($name) [Environment]::SetEnvironmentVariable($name, $settings[$name]) } - & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output $testExit = $LASTEXITCODE } finally { foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } } $identity = [ordered]@{ schema=1; reference='MC68040UM'; url='https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf' - sections=@('8.4.1','8.4.2','8.4.3','8.4.4','8.4.6.2','8.4.6.7'); softwareReferenceExecuted=$false + sections=@('8.2.2','8.4','8.4.1','8.4.2','8.4.3','8.4.4','8.4.6.2','8.4.6.7'); softwareReferenceExecuted=$false sourceCommit=(& git -C $repo rev-parse HEAD); cpuCommittedTree=(& git -C $repo rev-parse HEAD:Copper68k) cpuSourceFiles=@(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } - inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs') | ForEach-Object { + inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } assemblies=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | ForEach-Object { @@ -51,7 +54,8 @@ if (-not $ValidateReportsOnly) { limitations=@('Canonical synthetic frames, not hardware captures or enabled-MMU access faults', 'CP context transfer and detailed fault protocol inventory remain untested and fail the gate', 'Multiple continuation bits are architecturally undefined and excluded', - 'Detailed fault validation, odd return PCs and physical timing remain unqualified') + 'Detailed fault validation, chained odd-PC SR provenance and physical timing remain unqualified', + 'Direct odd-RTE saved-SR ordering uses documentary WinUAE 5d22d336, not an executed hardware oracle') } $identity | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'identities.json') } @@ -60,11 +64,11 @@ if (-not $ValidateReportsOnly) { $identity = Get-Content -LiteralPath (Join-Path $output 'identities.json') -Raw | ConvertFrom-Json if ($identity.schema -ne 1 -or $identity.reference -cne 'MC68040UM' -or $identity.softwareReferenceExecuted -ne $false -or $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.cpuCommittedTree -notmatch '^[a-f0-9]{40}$' -or - @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 5 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 5 -or + @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 6 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 6 -or @($identity.assemblies).Count -ne 2) { throw '040 access-frame input identity is missing or incomplete' } foreach ($input in $identity.inputs) { $path = Join-Path $repo $input.file - if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs') -or + if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs') -or $input.sha256 -cne (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()) { throw '040 access-frame fixture/command identity differs' } } $cpuFiles = @(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') @@ -78,8 +82,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 16 -or [int]$counters.total -ne 16 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (10 batches, 6 fixed examples)' +if ([int]$counters.executed -ne 19 -or [int]$counters.total -ne 19 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (13 batches, 6 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -98,10 +102,28 @@ foreach ($group in $expected.Keys) { $expectedCombinations = @{} if ($group -eq 'rte-access-remaining-protocols') { foreach ($bank in @('user','ISP','MSP')) { - foreach ($form in @('frame-validation-fault','odd-user-trace-PC','real-access-fault-entry','writeback-handler','CP-context-transferred-vector')) { + foreach ($form in @('frame-validation-fault','odd-PC-chained-SR-provenance','real-access-fault-entry','writeback-handler','CP-context-transferred-vector')) { $expectedCombinations["68040/RTE/format7/$form/bank=$bank"] = 32 } } + } elseif ($group -in @('rte-odd-normal','rte-odd-pending','address-error-fetch-040')) { + if ($group -eq 'address-error-fetch-040') { + foreach ($bank in @('user','ISP','MSP')) { foreach ($trace in @(0,0x8000,0x4000)) { foreach ($target in @(0x6001,0xff002003u)) { + foreach ($vbr in @(0,0x10000)) { foreach ($alignment in @(0,1)) { + $key = '68040/fetch/odd/bank={0}/T={1:X4}/target={2:X8}/VBR={3:X8}/align={4}' -f $bank,$trace,$target,$vbr,$alignment + $expectedCombinations[$key] = 32 + } } + } } } + } else { + $pending = $group -eq 'rte-odd-pending' + $forms = if ($pending) { @('CT','CU','CP49','CP50','CP51','CP52','CP53','CP54','CP55') } else { @('format0','format2','format3','normal','CM') } + foreach ($form in $forms) { foreach ($entry in @('ISP','MSP')) { foreach ($result in @('user','ISP','MSP')) { foreach ($trace in @(0,0x8000,0x4000)) { + foreach ($target in @(0x6001,0xff002003u)) { foreach ($vbr in @(0,0x10000)) { foreach ($alignment in @(0,1)) { + $key = '68040/RTE/odd/{0}/entry={1}/result={2}/T={3:X4}/target={4:X8}/VBR={5:X8}/align={6}' -f $form,$entry,$result,$trace,$target,$vbr,$alignment + $expectedCombinations[$key] = 32 * $(if ($pending) {4} else {3}) + } } } + } } } } + } } elseif ($group -in @('rte-throwaway-controls','rte-throwaway-access')) { $forms = if ($group -eq 'rte-throwaway-controls') { @('format0','format2','format3') } else { @('normal','CM','CT','CU','CP49','CP50','CP51','CP52','CP53','CP54','CP55') } foreach ($start in @('ISP','MSP')) { foreach ($tail in @('user','ISP','MSP')) { foreach ($middle in @('none','user','ISP','MSP')) { @@ -184,8 +206,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 16 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 19 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=887520; xunitBatches=10; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=1124832; xunitBatches=13; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index c13a9cd3..aea003e0 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -115,13 +115,23 @@ $mutations += @( '@; after=@' if (format != 1) State.StatusRegister = restoredStatus; if (format == 1) continue; // Mutant: retain the old stack selector. -'@; group='SyntheticM68040ThrowawayTests'; milestone=6; model='68040'; throwaway=$true} +'@; group='SyntheticM68040ThrowawayTests'; milestone=6; model='68040'; throwaway=$true}, + @{name='040-address-error-format'; file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs'; before=' PushWord(0x200c);'; after=' PushWord(0x000c);'; + group='SyntheticM68040OddReturnTests'; milestone=6; model='68040'; odd=$true}, + @{name='040-address-error-a0'; file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs'; before=' PushLong(faultAddress & ~1u);'; after=' PushLong(faultAddress);'; + group='SyntheticM68040OddReturnTests'; milestone=6; model='68040'; odd=$true}, + @{name='040-rte-saved-sr-image'; file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs'; before=' RaiseM68040AddressError(rtePc, target, priorSr);'; after=' RaiseM68040AddressError(rtePc, target, (ushort)(State.StatusRegister | 0x2000));'; + group='SyntheticM68040OddReturnTests'; milestone=6; model='68040'; odd=$true}, + @{name='040-rte-pending-priority'; file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs'; before=' if (continuation >= 0x2000)'; + after=" if (TryRaiseM68040RteAddressError(pc, priorSr, State.LastInstructionProgramCounter)) return true;`n if (continuation >= 0x2000)"; + group='SyntheticM68040OddReturnTests'; milestone=6; model='68040'; odd=$true} ) if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } if ($Scope -eq 'Logical') { $mutations = @($mutations | Where-Object { $_.milestone -eq 4 }) } if ($Scope -eq 'Control') { $mutations = @($mutations | Where-Object { $_.milestone -eq 5 }) } if ($Scope -eq 'Consolidation') { $mutations = @($mutations | Where-Object { $_.milestone -eq 6 }) } +if ($Scope -eq 'Rte040') { $mutations = @($mutations | Where-Object { $_.odd }) } $saved = @{} foreach ($mutation in $mutations) { $path = Join-Path $repo $mutation.file @@ -160,6 +170,15 @@ try { throw 'Throwaway mutation did not execute its complete two-batch selection' } } + if ($mutation.odd) { + [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + if ($trx.TestRun.ResultSummary.Counters.executed -ne 3 -or $batches.Count -ne 3 -or + @($batches | Where-Object { $_.group -eq 'rte-odd-normal' -and $_.logicalCases -eq 69120 }).Count -ne 1 -or + @($batches | Where-Object { $_.group -eq 'rte-odd-pending' -and $_.logicalCases -eq 165888 }).Count -ne 1 -or + @($batches | Where-Object { $_.group -eq 'address-error-fetch-040' -and $_.logicalCases -eq 2304 }).Count -ne 1) { + throw 'Odd-return mutation did not execute its complete three-batch selection' + } + } if ($legacyPresent) { [xml]$legacyTrx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if (@($legacyTrx.TestRun.Results.UnitTestResult | Where-Object { $_.testName.EndsWith($mutation.legacyTest) -and $_.outcome -eq 'Failed' }).Count -ne 1) { throw 'Original regression did not detect the same mutation' } diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index e90dee79..0a46a5c5 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -145,6 +145,9 @@ try { $expected['system-move-word-restart-invalid'] = 224 } if ($model -eq '68040') { + $expected['rte-odd-normal'] = 69120 + $expected['rte-odd-pending'] = 165888 + $expected['address-error-fetch-040'] = 2304 $expected['rte-throwaway-controls'] = 82944 $expected['rte-throwaway-access'] = 428544 $expected['rte-access-fpu-unimplemented'] = 13824 @@ -250,7 +253,7 @@ try { seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; - m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks; CP context transfer, detailed fault validation, real fault entry and physical timing remain unqualified'; + m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks and direct odd-PC returns with format-2 address errors and pending-exception priority; chained odd-PC saved-SR provenance, CP context transfer, detailed fault validation, real fault entry and physical timing remain unqualified'; m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; marked private word-MOVE/MOVEA images resume their faulted cycle; other opaque restart/input state, long transfers, RMW and physical timing remain unqualified'; m68000DoubleFaultScope='Address-error entry/handler faults; reset-only recovery; external BERR/reset-vector faults unavailable through the current public bus API'; unavailableSystemCoverage=@('010 long/non-MOVE/external bus-fault restart and 020/030 internal restart', '020/030 coprocessor midinstruction restoration', '040 access-fault CP context transfer, detailed frame-validation/fault entry and writeback-handler qualification', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands'); From cb9679d56c65ba9f5e8a73565f2cb7280b243710 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 11:40:08 +0300 Subject: [PATCH 035/211] Qualify 68060 LPSTOP encodings and exception priority --- Copper68k.Tests/Synthetic/README.md | 13 +++ .../Synthetic/SyntheticLowPowerStopTests.cs | 86 ++++++++++++++++ .../M68kAdvancedTimingInterpreter.System.cs | 7 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 99 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 31 ++++++ .../test-copper68k-synthetic-mutations.ps1 | 27 ++++- scripts/test-copper68k-synthetic.ps1 | 3 + 7 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticLowPowerStopTests.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 003636dd..1c78ccbe 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -260,3 +260,16 @@ identities. Existing outputs must be validated with `-ValidateReportsOnly` or a fresh output directory used. It cannot pass merely by fixing normal/trace return. See the [reference record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-access-frame-restoration-discovery-2026-10-05) for failed-before evidence, mutation controls and the remaining scope. + +The LPSTOP qualification adds 228,350 ordinary-CI cases in nine batches. +On 060, every second opcode word except the fixed `$01c0` encoding is checked +in both privilege modes (131,070 cases). These unrecognized F-line forms must +take vector 11 before privilege handling. Eight profile batches add 12,160 +cases each for individual encoding-bit changes, immediate SR boundaries, all +initial CCRs, user/supervisor mode and incoming trace. Legal 060 LPSTOP retains +its S-clear privilege rule, trace behavior and stopped-state nonretirement. +Exception frames preserve the original SR and opcode PC according to the manual. +Run its three discriminating controls with +`./scripts/test-copper68k-synthetic-mutations.ps1 -Scope LowPowerStop`. +Physical broadcast/pins and the separate ordinary STOP S-clear disagreement +remain unqualified; no specialized regression is retired. diff --git a/Copper68k.Tests/Synthetic/SyntheticLowPowerStopTests.cs b/Copper68k.Tests/Synthetic/SyntheticLowPowerStopTests.cs new file mode 100644 index 00000000..a2de5606 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticLowPowerStopTests.cs @@ -0,0 +1,86 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68060UM D-19/20 fixes the second opcode word at 01C0; 8.2.4/5 +// distinguishes unrecognized F-line encodings from privileged instructions. +// Broadcast/pin behavior and physical prefetch ordering are not qualified here. +public sealed class SyntheticLowPowerStopTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Fact, Trait("Suite", "Synthetic")] + public void EveryM68060UnrecognizedSecondWordRaisesLineFBeforePrivilege() + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68060")); + var report = new CoverageBatch("68060", "system-lpstop-extensions"); + for (var extension = 0; extension <= ushort.MaxValue; extension++) + { + if (extension == 0x01c0) continue; + foreach (var supervisor in new[] { false, true }) + Run(m, report, (ushort)extension, 0x271f, supervisor, 0, 0, + $"68060/LPSTOP/unrecognized/high={extension >> 8:X2}/super={supervisor}/op=F800/ext={extension:X4}/ccr=00"); + } + report.Complete(output); + } + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void EncodingPrivilegeStatusAndIncomingTraceHaveIndependentOutcomes(string modelId) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); + var report = new CoverageBatch(modelId, "system-lpstop-values"); + var extensions = Enumerable.Range(0, 16).Select(bit => (ushort)(0x01c0 ^ (1 << bit))) + .Concat(new ushort[] { 0, 0xffff, 0x01c0 }).ToArray(); + Assert.Equal(19, extensions.Distinct().Count()); + foreach (var extension in extensions) + foreach (var immediate in new ushort[] { 0, 0x071f, 0x2000, 0x271f, 0xa01f }) + foreach (var supervisor in new[] { false, true }) + foreach (var trace in new ushort[] { 0, 0x8000 }) + for (var ccr = 0; ccr < 32; ccr++) + Run(m, report, extension, immediate, supervisor, trace, ccr, + $"{modelId}/LPSTOP/encoding={extension:X4}/imm={immediate:X4}/super={supervisor}/T={trace:X4}/op=F800/ccr={ccr:X2}"); + report.Complete(output); + } + + private static void Run(SyntheticMachine m, CoverageBatch report, ushort extension, ushort immediate, + bool supervisor, ushort trace, int ccr, string id) + { + m.Reset(ccr, supervisor); + // Fixed encoding and following sentinel; no production decoder is used. + var e = SyntheticExecution.Prepare(m, [0xf800, extension, immediate]); + m.Core.State.StatusRegister |= trace; e.Sr |= trace; + var vector = m.Model.Id != "68060" || extension != 0x01c0 ? 11 + : !supervisor || (immediate & 0x2000) == 0 ? 8 : 0; + if (vector != 0) SyntheticExecution.ExpectException(m, e, vector); + else + { + SyntheticSystemTests.ApplyStatus(m, e, immediate); + if (trace != 0) SyntheticExecution.ExpectException(m, e, 9, e.Pc); + else e.Stopped = true; + } + if (vector != 0) + { + var savedSr = (ushort)((supervisor ? 0x2700 : 0x0700) | trace | ccr); + e.ControlChecks["saved instruction PC"] = (s => s.LastExceptionStackedProgramCounter, SyntheticMachine.Code); + e.ControlChecks["saved original SR"] = (s => s.LastExceptionStatusRegister, savedSr); + } + // Frame/neighbor memory, registers, stacks and flags are compared by the + // shared verifier. A stopped CPU must not retire the following sentinel. + try + { + m.Core.ExecuteInstruction(); + var mismatch = e.Verify(m); + if (mismatch == null && e.Stopped) + { + var sequence = m.Core.State.ExceptionSequence; + m.Core.ExecuteInstruction(); + mismatch = e.Verify(m); + if (mismatch == null && m.Core.State.ExceptionSequence != sequence) mismatch = "Stopped CPU entered an unexpected exception"; + } + report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); + } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException or UnsupportedM68040InstructionException) + { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); } + } +} diff --git a/Copper68k/M68kAdvancedTimingInterpreter.System.cs b/Copper68k/M68kAdvancedTimingInterpreter.System.cs index 1fc947ca..d1b0d5ec 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.System.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.System.cs @@ -13,10 +13,13 @@ private bool TryExecuteExtendedSystem(ushort opcode) { if (_profile.Model != M68kAcceleratorModel.M68060) { RaiseFormat0Exception(11, pc, M68kInstructionTimingKey.LineFException); return true; } + // MC68060UM D-19/20 fixes the second opcode word at 01C0. + // Unrecognized F-line encodings take vector 11 (8.2.4), before + // the privilege check for a recognized instruction (8.2.5). + if (FetchWord() != 0x01c0) + { RaiseFormat0Exception(11, pc, M68kInstructionTimingKey.LineFException); return true; } if (!State.GetFlag(M68kCpuState.Supervisor)) { RaiseFormat0Exception(8, pc, M68kInstructionTimingKey.PrivilegeViolation); return true; } - if (FetchWord() != 0x01c0) - { RaiseFormat0Exception(4, pc, M68kInstructionTimingKey.IllegalInstruction); return true; } var immediate = FetchWord(); if ((immediate & M68kCpuState.Supervisor) == 0) { RaiseFormat0Exception(8, pc, M68kInstructionTimingKey.PrivilegeViolation); return true; } diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index e8c7e752..ee230719 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -3005,3 +3005,102 @@ their defects. Generator/runner/native-library/manifest pins match the preceding record; each model/opcode row retains its status and callback/frame/mask counts. This checkpoint neither hides those disagreements nor treats discovery as a passing qualification gate. + +## LPSTOP encoding and exception-priority qualification — 2026-10-05 + +The 68060 executor recognized LPSTOP's first word but checked privilege before +its fixed second opcode word, and then classified malformed second words as +vector 4. It now recognizes the complete encoding before privilege handling; +an unrecognized second word takes line-F vector 11 without changing SR or +entering STOP. Legal LPSTOP's S-clear rejection, trace and stop timing policy +are retained. No instruction is retried and the public package API is unchanged. + +[MC68060UM D-19/20](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf) +defines `F800 01C0 immediate` and explicitly makes an attempt to clear S a +privilege violation. Sections 8.2.4/5 distinguish unrecognized F-line encodings +from recognized privileged instructions, save the original SR, and save the +instruction's opcode PC. Section 8.2.6 specifies incoming trace and no stopped +state on a traced LPSTOP. Those manual rules determine the expectations. +The inspected pinned generators also validate `01C0` before privilege and use +vector 11 for an invalid word, but retain the previously recorded saved-PC +disagreement for legal user-mode LPSTOP. They do not override the manual. + +| New ordinary-CI group | Passing cases | Reporting batches | +| --- | ---: | ---: | +| Every unrecognized 060 second opcode word, both privilege modes | 131,070 | 1 | +| Encoding/status/CCR/trace cases across eight profiles | 97,280 | 8 | +| Total new | 228,350 | 9 | + +The exhaustive group enumerates all 65,535 non-`01C0` words independently of +production decoding, with canonical immediate/CCR inputs. The second group +uses the legal word, each of its sixteen individual bit changes, zero/all ones, +five SR boundary images, both privilege states, incoming T1/no trace and all +32 CCRs. It covers model-specific absence, correct vector/frame/PC/SR, all +registers and stacks, untouched memory and a sentinel which cannot retire while +stopped. A legal traced LPSTOP takes vector 9 with format 2 and next PC. These +groups qualify semantic outcomes, not physical prefetch ordering, broadcast bus +cycles, PST pins, clock quiescence or silicon timing. + +Against production `a194c12`, `artifacts/m6-lpstop-before/` records 142,590 +mismatches, 85,760 passing cases and no unsupported/untested new cases. Both +affected 060 batches fail and the seven other profiles pass. After the correction +all 228,350 pass; the affected system/model/trace selection passes 81 tests in +`artifacts/m6-lpstop-after/`. + +`./scripts/test-copper68k-synthetic-mutations.ps1 -Scope LowPowerStop` +requires both complete 060 batches to execute for each control. Vector-4 +substitution fails 142,590 cases and passes 640. An early privilege check fails +71,295 and passes 71,935. Removing the existing S-clear check fails 128 and +passes 143,102. All fail semantically; no compiler/selection failure counts as +proof. Source is restored byte-for-byte and rebuilt. Evidence is +`artifacts/m6-lpstop-mutations-qualified/mutation-proof.json`; the initial +mixed-line-ending anchor failure is retained separately and is not execution +evidence. No old regression is retired. + +The 040 chained user-tail SR question remains unresolved: the manual's traced +user S requirement and documentary WinUAE's prior user SR differ. That finding +does not turn the required inventory item into passing coverage. Ordinary 060 +STOP's S-clear software disagreement also remains open: the generator labels +the behavior undocumented, while LPSTOP's S-clear rule is explicit. Existing +Basic saved-PC, reserved-word and other reference disagreements are retained. +Milestone 6 and the full goal remain **in progress** with `roadmapComplete=false`. + +Private unpublished NuGet `1.5.2-synthetic-dev.57` validates isolated CopperScreen +at baseline `d9beae8b88be24032221e3482942a249c03c27d3`: Release build has zero +warnings/errors; host 149 passing/six optional skips, disk 74, separate engine +diagnostics 1,080, native Workbench boots two and A1200 persistence one pass with +zero native skips. Four restored assets and four loaded CPU assemblies match +the package. Package SHA-256 is +`d6d90ced1bf74824ab4e73a4918d0833689d11c17c55c5ea03c8f4597c60c059`; +CPU assembly SHA-256 is +`de37702d5301d7dfddd792b7c2d776434b048c5969537c32115d09c1a143a0a1`. +Evidence is the consumer's `artifacts/lpstop-identities.json`, +`artifacts/lpstop-validation/` and separate diagnostic outputs. Root CopperScreen +dependencies/unrelated changes and published package versions are preserved. +No package is published. + +Final Release CPU validation passes 4,967 tests, eleven optional/opt-in skips and +zero failures with all four qualified WinUAE presets enabled. The strict gate +verifies 15,999,318 passing logical cases in 595 reporting batches, plus fresh +pinned SingleStepTests (312,500 cases / 125 files) and Musashi (536 programs / +88 explicit exclusions across eight profiles). Evidence is +`artifacts/m6-lpstop-full/` and `artifacts/m6-lpstop-gate.log`. + +Five negative report controls reject their specific defect: missing 060 extension +enumeration, missing 000 absence profile, empty enumeration, stale value count +and foreign model. Evidence is `artifacts/m6-lpstop-report-controls/controls.json`. +The fresh 040 discovery in `artifacts/m6-lpstop-040-discovery/` still executes all +nineteen cases, passes eighteen and fails its remaining inventory: 1,124,352 +passing phases, zero mismatching/unsupported and 480 explicitly untested. Its +input/source/assembly identities describe this production checkpoint. Earlier +thirteen 040 report-corruption proofs remain retained with their original source +identities; they are not relabeled as freshly rerun here. + +Fresh Basic discovery still fails in `artifacts/m6-lpstop-broad/`: 1,326 passing +directories, 47 mismatching and eight unsupported, with 11,474,194 callbacks, +1,663,891 frames and 199,327 masked-SR cases. All 32 comparator controls detect +their defects. Every model/opcode row retains its preceding status and callback/ +frame/mask counts; the generator, runner, native-library and input-manifest pins +are unchanged. In particular, the first legal user-mode LPSTOP case still exposes +the reference's opcode-PC-plus-two disagreement. Correcting malformed encodings +does not normalize that result or make the broad audit passing. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index dba5ca3a..34b70651 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1287,3 +1287,34 @@ All thirteen input/report controls detect their defects. Fresh broad Basic discovery retains 1,326 passing directories, 47 mismatching and eight unsupported, with the same per-model/opcode statuses and callback/frame/mask counts; its 32 comparator controls remain effective. These gaps remain required work. + +### Milestone 6 LPSTOP encoding qualification — 2026-10-05 + +060 LPSTOP now validates its fixed second opcode word before privilege handling +and raises line-F for unrecognized encodings. Nine independent CI batches add +228,350 cases: every malformed second word in both privilege modes, plus +encoding/status/CCR/trace boundaries across all eight profiles. The manual's +original SR/opcode-PC frame rules, trace behavior, S-clear rejection and stopped +sentinel nonretirement are checked. Three maintained mutations detect wrong +vector, early privilege handling and an omitted S-clear check, then restore and +rebuild source. No regression is retired. + +See the [reference record](COPPER68K_REFERENCE_QUALIFICATION.md#lpstop-encoding-and-exception-priority-qualification--2026-10-05) +for authority, failed-before counts and mutation evidence. Chained odd-PC SR, +ordinary STOP's undocumented software rule, legal LPSTOP saved-PC reference +disagreement, physical broadcast and all earlier milestone requirements remain +open. Milestone 6 and the full goal remain **in progress**; `roadmapComplete=false`. + +Validation passes 4,967 Release CPU tests (eleven optional/opt-in skips), +15,999,318 logical cases in 595 strict reporting batches and fresh pinned +SingleStepTests/Musashi audits. Private unpublished `.57` validates CopperScreen +through NuGet: clean Release build, host 149/six optional skips, disk 74, separate +engine 1,080 and all three native Workbench/A1200 replays pass without skips. +Package/assets/loaded DLL identities match. No public package is published. + +Five specific missing/empty/stale/foreign-model report controls reject their +defects. Fresh complete 040 discovery still fails on 480 named untested cases, +with 1,124,352 passing phases and no promoted mismatches. Fresh broad Basic +discovery retains 1,326 passing directories, 47 mismatching and eight unsupported; +its per-model/opcode statuses and counts are unchanged, and all 32 controls pass. +These unresolved requirements remain part of the goal. diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index aea003e0..da54aa65 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','LowPowerStop')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -124,7 +124,21 @@ $mutations += @( group='SyntheticM68040OddReturnTests'; milestone=6; model='68040'; odd=$true}, @{name='040-rte-pending-priority'; file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs'; before=' if (continuation >= 0x2000)'; after=" if (TryRaiseM68040RteAddressError(pc, priorSr, State.LastInstructionProgramCounter)) return true;`n if (continuation >= 0x2000)"; - group='SyntheticM68040OddReturnTests'; milestone=6; model='68040'; odd=$true} + group='SyntheticM68040OddReturnTests'; milestone=6; model='68040'; odd=$true}, + @{name='060-lpstop-linef'; file='Copper68k/M68kAdvancedTimingInterpreter.System.cs'; before=@' + if (FetchWord() != 0x01c0) + { RaiseFormat0Exception(11, pc, M68kInstructionTimingKey.LineFException); return true; } +'@; after=@' + if (FetchWord() != 0x01c0) + { RaiseFormat0Exception(4, pc, M68kInstructionTimingKey.IllegalInstruction); return true; } +'@; group='SyntheticLowPowerStopTests'; milestone=6; model='68060'; lpstop=$true}, + @{name='060-lpstop-privilege-priority'; file='Copper68k/M68kAdvancedTimingInterpreter.System.cs'; before=' if (FetchWord() != 0x01c0)'; after=@' + if (!State.GetFlag(M68kCpuState.Supervisor)) + { RaiseFormat0Exception(8, pc, M68kInstructionTimingKey.PrivilegeViolation); return true; } + if (FetchWord() != 0x01c0) +'@; group='SyntheticLowPowerStopTests'; milestone=6; model='68060'; lpstop=$true}, + @{name='060-lpstop-clear-s'; file='Copper68k/M68kAdvancedTimingInterpreter.System.cs'; before=' if ((immediate & M68kCpuState.Supervisor) == 0)'; + after=' if (false)'; group='SyntheticLowPowerStopTests'; milestone=6; model='68060'; lpstop=$true} ) if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } @@ -132,6 +146,7 @@ if ($Scope -eq 'Logical') { $mutations = @($mutations | Where-Object { $_.milest if ($Scope -eq 'Control') { $mutations = @($mutations | Where-Object { $_.milestone -eq 5 }) } if ($Scope -eq 'Consolidation') { $mutations = @($mutations | Where-Object { $_.milestone -eq 6 }) } if ($Scope -eq 'Rte040') { $mutations = @($mutations | Where-Object { $_.odd }) } +if ($Scope -eq 'LowPowerStop') { $mutations = @($mutations | Where-Object { $_.lpstop }) } $saved = @{} foreach ($mutation in $mutations) { $path = Join-Path $repo $mutation.file @@ -179,6 +194,14 @@ try { throw 'Odd-return mutation did not execute its complete three-batch selection' } } + if ($mutation.lpstop) { + [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + if ($trx.TestRun.ResultSummary.Counters.executed -ne 2 -or $batches.Count -ne 2 -or + @($batches | Where-Object { $_.model -eq '68060' -and $_.group -eq 'system-lpstop-extensions' -and $_.logicalCases -eq 131070 }).Count -ne 1 -or + @($batches | Where-Object { $_.model -eq '68060' -and $_.group -eq 'system-lpstop-values' -and $_.logicalCases -eq 12160 }).Count -ne 1) { + throw 'LPSTOP mutation did not execute its complete two-batch selection' + } + } if ($legacyPresent) { [xml]$legacyTrx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if (@($legacyTrx.TestRun.Results.UnitTestResult | Where-Object { $_.testName.EndsWith($mutation.legacyTest) -and $_.outcome -eq 'Failed' }).Count -ne 1) { throw 'Original regression did not detect the same mutation' } diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 0a46a5c5..8bccc54a 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -112,6 +112,7 @@ try { 'system-callm'=$(if ($model -in @('68000','68010')) {10832} elseif ($model -in @('68020','68EC020','A1200')) {11000} elseif ($model -in @('68030','68040','68060')) {10964}); 'system-interrupt'=$(if ($model -in @('68000')) {224} elseif ($model -in @('68010','68060')) {226} elseif ($model -in @('68020','68030','68040','68EC020','A1200')) {418}); 'system-model'=13120; + 'system-lpstop-values'=12160; 'system-movec'=2432; 'system-moves'=$(if ($model -in @('68000','68010')) {10656} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {11052}); 'system-moves-invalid-operands'=7296; @@ -128,6 +129,7 @@ try { foreach ($control in @('806','807')) { $expected["system-root-control-$control"] = 55296 } } if ($model -eq '68060') { + $expected['system-lpstop-extensions'] = 131070 $expected['system-bus-control'] = 247808 $expected['system-bus-control-exceptions'] = 65536 $expected['system-bus-control-state'] = 2064 @@ -254,6 +256,7 @@ try { externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks and direct odd-PC returns with format-2 address errors and pending-exception priority; chained odd-PC saved-SR provenance, CP context transfer, detailed fault validation, real fault entry and physical timing remain unqualified'; + m68060LowPowerStopScope='Every unrecognized second opcode word in both privilege modes; fixed encoding, status, CCR and incoming trace cases across all profiles. Opcode-PC exception frames follow MC68060UM 8.2.4/5; physical broadcast, pins and ordinary STOP S-clear software disagreement remain unqualified'; m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; marked private word-MOVE/MOVEA images resume their faulted cycle; other opaque restart/input state, long transfers, RMW and physical timing remain unqualified'; m68000DoubleFaultScope='Address-error entry/handler faults; reset-only recovery; external BERR/reset-vector faults unavailable through the current public bus API'; unavailableSystemCoverage=@('010 long/non-MOVE/external bus-fault restart and 020/030 internal restart', '020/030 coprocessor midinstruction restoration', '040 access-fault CP context transfer, detailed frame-validation/fault entry and writeback-handler qualification', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands'); From dd807db53cf9df441b8a20d6d71e2e4cd1c6dc32 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 17:27:52 +0300 Subject: [PATCH 036/211] Qualify LPSTOP reference frames and consolidate duplicate coverage --- .../M68kWinUaeCpuTesterConformanceTests.cs | 12 +- .../M68kWinUaeLowPowerStopAuditTests.cs | 68 ++++++++++ .../M68kWinUaeTrapBoundsAuditTests.cs | 12 +- Copper68k.Tests/Synthetic/README.md | 23 +++- .../Synthetic/SyntheticLowPowerStopTests.cs | 2 +- .../Synthetic/SyntheticModelSystemTests.cs | 10 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 128 ++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 11 ++ scripts/prepare-copper68k-winuae.ps1 | 18 ++- .../test-copper68k-synthetic-mutations.ps1 | 18 ++- scripts/test-copper68k-synthetic.ps1 | 4 +- ...-copper68k-winuae-qualified-exceptions.ps1 | 12 +- scripts/winuae/lpstop-fetch-pc.patch | 15 ++ 13 files changed, 298 insertions(+), 35 deletions(-) create mode 100644 Copper68k.Tests/M68kWinUaeLowPowerStopAuditTests.cs create mode 100644 scripts/winuae/lpstop-fetch-pc.patch diff --git a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs index 2dbf8e42..e0068213 100644 --- a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs +++ b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs @@ -314,6 +314,7 @@ private sealed class NativeTester : IDisposable private bool _qualifyLongArithmetic; private readonly Dictionary _longArithmeticForms = new(StringComparer.Ordinal); private Func? _fixtureClassifier; + private Func? _fixtureWordsClassifier; private readonly Dictionary _fixtureForms = new(StringComparer.Ordinal); private NativeTester(IntPtr library) @@ -360,7 +361,8 @@ public RunResult Run( ushort corruptSr = 0, bool corruptIgnoredSr = false, bool qualifyLongArithmetic = false, - Func? fixtureClassifier = null) + Func? fixtureClassifier = null, + Func? fixtureWordsClassifier = null) { _callbackException = null; _executedCases = 0; @@ -379,6 +381,7 @@ public RunResult Run( _qualifyLongArithmetic = qualifyLongArithmetic; _longArithmeticForms.Clear(); _fixtureClassifier = fixtureClassifier; + _fixtureWordsClassifier = fixtureWordsClassifier; _fixtureForms.Clear(); if (integerProfile is not null && (_destroy is null || _addressingMask is null || _lastOutput is null)) throw new XunitException("Multi-model integer audit requires the qualified native bridge exports (destroy, addressing mask and diagnostics)."); @@ -507,6 +510,13 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt var form = _fixtureClassifier(bus.ReadHostWord(registers.Pc), (ushort)registers.Sr); _fixtureForms[form] = _fixtureForms.GetValueOrDefault(form) + 1; } + if (_fixtureWordsClassifier is not null) + { + // Read immutable input before any CPU execution or stack copying. + var form = _fixtureWordsClassifier(bus.ReadHostWord(registers.Pc), + bus.ReadHostWord(registers.Pc + 2), bus.ReadHostWord(registers.Pc + 4), (ushort)registers.Sr); + _fixtureForms[form] = _fixtureForms.GetValueOrDefault(form) + 1; + } bus.CopyStackImage(registers.Regs[15], registers.Ssp, 0x20); ApplyRegisters(cpu, registers, _cpuLevel); diff --git a/Copper68k.Tests/M68kWinUaeLowPowerStopAuditTests.cs b/Copper68k.Tests/M68kWinUaeLowPowerStopAuditTests.cs new file mode 100644 index 00000000..60e6e949 --- /dev/null +++ b/Copper68k.Tests/M68kWinUaeLowPowerStopAuditTests.cs @@ -0,0 +1,68 @@ +using Xunit.Sdk; + +namespace Copper68k.Tests; + +public sealed partial class M68kWinUaeCpuTesterConformanceTests +{ + [EnvironmentFact("COPPER68K_RUN_WINUAE_LPSTOP_AUDIT", "audit qualified WinUAE LPSTOP encoding and privilege exception frames")] + public void WinUaeLowPowerStopExceptionsWhenEnabled() => RunQualifiedExceptionPreset(new( + "LowPowerStop", "LPSTOP", "gencpu-lpstop.cpp", "lpstop-fetch-pc.patch", "LowPowerStop", + "69ceec2d63bf35e27990142ca2e9e72f9c36dd2f4fba5113d17552c1ccbc35ca", "64cb8ff58d5a3a4fb4f217e3327fb1748d73f85521e3c9be98af7fd08f3a47bf", + "winuae-lpstop-audit.json", ["68060"], ["68060"], _ => ["LPSTOP"], (_, _) => (245760, 245760), + "MC68060UM D-19/20 and 8.2.4/5: fixed F800/01C0 encoding, malformed second-word Line-F priority, original SR and opcode-PC privilege frames. A copied generator uses the existing nonadvancing instruction-fetch helper for LPSTOP fixed offsets; get_wordi_test advances PC as a side effect, corrupting the second fetch address and saved PC. Basic and CPU results are unchanged. One seeded round, incoming CCR 0/31 and user/supervisor, no incoming trace. The generator skips stopped outcomes: this preset qualifies exceptions only; stopped state, full CCR, trace and complete malformed-word enumeration remain synthetic coverage. No physical broadcast/pin/timing qualification.", + FormCounts: (_, _) => 14, ClassifyWords: QualifyLowPowerStopEncoding, + ExpectedForms: (_, _) => LowPowerStopFormDistribution())); + + internal static IReadOnlyDictionary LowPowerStopFormDistribution() + { + // Pinned one-round corpus: extension recognition, immediate S and + // incoming S/CCR are reported separately. Stopped candidates are skipped. + var forms = new SortedDictionary(StringComparer.Ordinal); + foreach (var sr in new ushort[] { 0, 0x001f, 0x2000, 0x201f }) + foreach (var immediateS in new[] { 0, 1 }) + { + forms[$"LPSTOP/encoding=unrecognized/immediateS={immediateS}/sr={sr:X4}"] = 24576; + if ((sr & 0x2000) == 0 || immediateS == 0) + forms[$"LPSTOP/encoding=recognized/immediateS={immediateS}/sr={sr:X4}"] = 8192; + } + return forms; + } + + internal static string QualifyLowPowerStopEncoding(ushort opcode, ushort extension, ushort immediate, ushort inputSr) + { + if (opcode != 0xf800 || inputSr is not (0x0000 or 0x001f or 0x2000 or 0x201f)) + throw new XunitException($"Unqualified LPSTOP encoding/profile: {opcode:X4}/{extension:X4}/{immediate:X4}/{inputSr:X4}"); + if (extension == 0x01c0 && (inputSr & 0x2000) != 0 && (immediate & 0x2000) != 0) + throw new XunitException("Stopped LPSTOP outcomes are absent from the qualified exception preset."); + return $"LPSTOP/encoding={(extension == 0x01c0 ? "recognized" : "unrecognized")}/immediateS={(immediate >> 13) & 1}/sr={inputSr:X4}"; + } +} + +public sealed class M68kWinUaeLowPowerStopEncodingTests +{ + [Fact] + public void PinnedExceptionDistributionIncludesBothPrivilegeStatesAndExcludesStoppedForms() + { + var forms = M68kWinUaeCpuTesterConformanceTests.LowPowerStopFormDistribution(); + Assert.Equal(14, forms.Count); + Assert.Equal(245760, forms.Values.Sum()); + Assert.Equal(196608, forms.Where(x => x.Key.Contains("encoding=unrecognized/", StringComparison.Ordinal)).Sum(x => x.Value)); + Assert.DoesNotContain("LPSTOP/encoding=recognized/immediateS=1/sr=201F", forms.Keys); + } + [Theory] + [InlineData(0xf800, 0x01c0, 0x0000, 0x0000, "LPSTOP/encoding=recognized/immediateS=0/sr=0000")] + [InlineData(0xf800, 0x01c0, 0x271f, 0x001f, "LPSTOP/encoding=recognized/immediateS=1/sr=001F")] + [InlineData(0xf800, 0x01c0, 0x071f, 0x201f, "LPSTOP/encoding=recognized/immediateS=0/sr=201F")] + [InlineData(0xf800, 0x01c1, 0x271f, 0x2000, "LPSTOP/encoding=unrecognized/immediateS=1/sr=2000")] + [InlineData(0xf800, 0xffff, 0xffff, 0x001f, "LPSTOP/encoding=unrecognized/immediateS=1/sr=001F")] + public void FixedManualExamplesDistinguishEncodingAndPrivilege(int opcode, int extension, int immediate, int sr, string expected) => + Assert.Equal(expected, M68kWinUaeCpuTesterConformanceTests.QualifyLowPowerStopEncoding((ushort)opcode, (ushort)extension, (ushort)immediate, (ushort)sr)); + + [Theory] + [InlineData(0xf801, 0x01c0, 0, 0)] + [InlineData(0xf800, 0x01c0, 0, 0xa000)] + [InlineData(0xf800, 0x01c0, 0, 0x0001)] + [InlineData(0xf800, 0x01c0, 0x2000, 0x2000)] + public void ForeignOpcodesTraceCcrAndStoppedOutcomesAreRejected(int opcode, int extension, int immediate, int sr) => + Assert.Throws(() => M68kWinUaeCpuTesterConformanceTests.QualifyLowPowerStopEncoding((ushort)opcode, (ushort)extension, (ushort)immediate, (ushort)sr)); +} diff --git a/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs b/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs index 5844f9c0..535dff4c 100644 --- a/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs +++ b/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs @@ -78,15 +78,17 @@ private void RunQualifiedExceptionPreset(QualifiedExceptionPreset preset) probes.Add(new(model.Id, family, detected, register.ExecutedCases, sr.ExecutedCases, frame.ExecutedCases, preset.ArithmeticFlagControls, carryDetected, carry.ExecutedCases, ignoredAccepted, ignored.ExecutedCases)); var result = tester.Run(path, family, fixture.CpuLevel, false, false, model, - fixtureClassifier: preset.ClassifyForm is null ? null : (opcode, inputSr) => preset.ClassifyForm(opcode, inputSr, family)); + fixtureClassifier: preset.ClassifyForm is null ? null : (opcode, inputSr) => preset.ClassifyForm(opcode, inputSr, family), + fixtureWordsClassifier: preset.ClassifyWords); var expected = preset.Counts(model, family); var forms = new SortedDictionary(tester.FixtureForms.ToDictionary(x => x.Key, x => x.Value), StringComparer.Ordinal); var expectedForms = preset.FormCounts?.Invoke(model, family) ?? 0; + var formDistributionMatches = preset.ExpectedForms is null || forms.SequenceEqual(preset.ExpectedForms(model, family).OrderBy(x => x.Key, StringComparer.Ordinal)); var expectedMasked = preset.MaskedCounts?.Invoke(model, family) ?? (family.StartsWith("CHK2.", StringComparison.Ordinal) ? (uint)expected.Cases : 0); var passing = detected && result.Passed && result.ExecutedCases == expected.Cases && tester.FrameChecks == expected.Frames && tester.MaskedCases == expectedMasked && - forms.Count == expectedForms && (preset.ClassifyForm is null || forms.Values.Sum() == result.ExecutedCases); - var detail = $"{result.Detail} Expected/actual callbacks={expected.Cases}/{result.ExecutedCases}, frames={expected.Frames}/{tester.FrameChecks}, masked={expectedMasked}/{tester.MaskedCases}, forms={expectedForms}/{forms.Count}."; + forms.Count == expectedForms && formDistributionMatches && ((preset.ClassifyForm is null && preset.ClassifyWords is null) || forms.Values.Sum() == result.ExecutedCases); + var detail = $"{result.Detail} Expected/actual callbacks={expected.Cases}/{result.ExecutedCases}, frames={expected.Frames}/{tester.FrameChecks}, masked={expectedMasked}/{tester.MaskedCases}, forms={expectedForms}/{forms.Count}, formDistributionMatches={formDistributionMatches}."; rows.Add(new(model.Id, family, passing ? "passing" : tester.UnsupportedExecution ? "unsupported" : result.ExecutedCases == 0 ? "untested" : "mismatching", result.ExecutedCases, tester.FrameChecks, tester.MaskedCases, detected, forms, detail)); _output.WriteLine($"{model.Id}/{family}: {result.ExecutedCases} callbacks, {tester.FrameChecks} frames; controls={detected}."); @@ -131,7 +133,9 @@ private sealed record QualifiedExceptionPreset(string Name, string EnvironmentKe string ManifestPrefix, string SourceHash, string PatchHash, string ReportName, string[] ProfileIds, string[] ModelIds, Func Families, Func Counts, string Qualification, bool ArithmeticFlagControls = false, Func? MaskedCounts = null, - Func? ClassifyForm = null, Func? FormCounts = null); + Func? ClassifyForm = null, Func? FormCounts = null, + Func? ClassifyWords = null, + Func>? ExpectedForms = null); private sealed record WinUaeQualifiedExceptionRow(string Model, string Family, string Status, int ExecutedCases, uint ExceptionFrames, uint MaskedSrCases, bool Controls, SortedDictionary Forms, string Detail); private sealed record WinUaeQualifiedExceptionProbe(string Model, string Family, bool Detected, diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 1c78ccbe..510632a1 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -261,10 +261,10 @@ fresh output directory used. It cannot pass merely by fixing normal/trace return See the [reference record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-access-frame-restoration-discovery-2026-10-05) for failed-before evidence, mutation controls and the remaining scope. -The LPSTOP qualification adds 228,350 ordinary-CI cases in nine batches. +The LPSTOP qualification runs 267,262 ordinary-CI cases in nine batches. On 060, every second opcode word except the fixed `$01c0` encoding is checked in both privilege modes (131,070 cases). These unrecognized F-line forms must -take vector 11 before privilege handling. Eight profile batches add 12,160 +take vector 11 before privilege handling. Eight profile batches add 17,024 cases each for individual encoding-bit changes, immediate SR boundaries, all initial CCRs, user/supervisor mode and incoming trace. Legal 060 LPSTOP retains its S-clear privilege rule, trace behavior and stopped-state nonretirement. @@ -272,4 +272,21 @@ Exception frames preserve the original SR and opcode PC according to the manual. Run its three discriminating controls with `./scripts/test-copper68k-synthetic-mutations.ps1 -Scope LowPowerStop`. Physical broadcast/pins and the separate ordinary STOP S-clear disagreement -remain unqualified; no specialized regression is retired. +remain unqualified. The former 320-case-per-profile LPSTOP loop is replaced by +the encoding/status matrix after shared S-clear mutation proof; MOVE16, cache, +breakpoint and specialized regressions remain. + +The independent LPSTOP exception preset uses a copied pinned WinUAE generator +with nonadvancing fixed-offset fetches. Its 245,760 callbacks validate actual +CPU results, original SR and opcode-PC frames in fourteen encoding/immediate-S/ +incoming-S-and-CCR combinations. The generator skips stopped outcomes and this +preset has no incoming trace: these remain synthetic coverage. It requires exact +source/patch/input identities, all combinations and their recorded distributions; +missing fixtures or empty selections fail. Register/SR/frame corruption probes +must fail too. Prepare with `./scripts/prepare-copper68k-winuae.ps1 -Preset +LowPowerStop` and the same pinned-source/compiler arguments as other presets; +run with `./scripts/test-copper68k-winuae-qualified-exceptions.ps1 -Preset +LowPowerStop -InputDirectory -OutputDirectory `. +The [reference record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#lpstop-independent-exception-qualification-and-consolidation-2026-10-05) +records the source correction, corpus limitations and retirement proof. The +original Basic disagreement remains retained; this is a separate qualified preset. diff --git a/Copper68k.Tests/Synthetic/SyntheticLowPowerStopTests.cs b/Copper68k.Tests/Synthetic/SyntheticLowPowerStopTests.cs index a2de5606..b6eb3741 100644 --- a/Copper68k.Tests/Synthetic/SyntheticLowPowerStopTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticLowPowerStopTests.cs @@ -33,7 +33,7 @@ public void EncodingPrivilegeStatusAndIncomingTraceHaveIndependentOutcomes(strin .Concat(new ushort[] { 0, 0xffff, 0x01c0 }).ToArray(); Assert.Equal(19, extensions.Distinct().Count()); foreach (var extension in extensions) - foreach (var immediate in new ushort[] { 0, 0x071f, 0x2000, 0x271f, 0xa01f }) + foreach (var immediate in new ushort[] { 0, 0x0700, 0x071f, 0x2000, 0x2700, 0x271f, 0xa01f }) foreach (var supervisor in new[] { false, true }) foreach (var trace in new ushort[] { 0, 0x8000 }) for (var ccr = 0; ccr < 32; ccr++) diff --git a/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs b/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs index 0e7b74f6..4d914939 100644 --- a/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs @@ -97,7 +97,7 @@ public void MovecControlInventoryMasksPrivilegeAndAllGeneralRegisters(string mod }; [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] - public void Move16CacheInstructionsBreakpointsAndLowPowerStop(string modelId) + public void Move16CacheInstructionsAndBreakpoints(string modelId) { var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); var report = new CoverageBatch(modelId, "system-model"); for (var ccr = 0; ccr < 32; ccr++) @@ -118,14 +118,6 @@ public void Move16CacheInstructionsBreakpointsAndLowPowerStop(string modelId) else if (!supervisor) SyntheticExecution.ExpectException(m, e, 8); SyntheticExecution.Run(m, e, report, $"{modelId}/{(push ? "CPUSH" : "CINV")}/{scope}/cache={cache}/A{reg}/super={supervisor}/op={op:X4}/ccr={ccr:X2}"); } - foreach (var sr in new ushort[] { 0, 0x0700, 0x2000, 0x2700, 0x271f }) - { - m.Reset(ccr, supervisor); var e = SyntheticExecution.Prepare(m, [0xf800, 0x01c0, sr]); - if (modelId != "68060") SyntheticExecution.ExpectException(m, e, 11); - else if (!supervisor || (sr & 0x2000) == 0) SyntheticExecution.ExpectException(m, e, 8); - else { SyntheticSystemTests.ApplyStatus(m, e, sr); e.Stopped = true; } - SyntheticExecution.Run(m, e, report, $"{modelId}/LPSTOP/W/imm={sr:X4}/super={supervisor}/op=F800/ccr={ccr:X2}", !e.Stopped); - } } for (var form = 0; form < 5; form++) for (var r = 0; r < 8; r++) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index ee230719..d3d51318 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -3104,3 +3104,131 @@ frame/mask counts; the generator, runner, native-library and input-manifest pins are unchanged. In particular, the first legal user-mode LPSTOP case still exposes the reference's opcode-PC-plus-two disagreement. Correcting malformed encodings does not normalize that result or make the broad audit passing. + +## LPSTOP independent exception qualification and consolidation, 2026-10-05 + +[MC68060UM](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf) D-19/20 +defines the fixed `F800 01C0` encoding and S-clear privilege violation; +8.2.4/5 defines Line-F recognition and original-SR/opcode-PC exception frames. +Inspection of the executed generator pin explains its saved-PC disagreement: +`get_wordi_test(offset)` fetches at the given offset, then advances `regs.pc` +by two. LPSTOP calls it for fixed offsets two and four, so the first fetch +changes the saved exception PC and the second fetch address. The separate +`LowPowerStop` preset patches a copy of `gencpu.cpp` to use its existing +nonadvancing `get_word_test_prefetch` helper for both words. It does not change +CPU results, comparison masks, the original Basic corpus or tracked upstream +source. Physical fetch ordering/cycles are not qualified by this correction. + +Generator `025b999239800357e95065fe5b9a15ea5b300fa7` and assertion-runner +`7a83745d6c6159bc74ab0471578ffc8bc244e66e` remain pinned. The normalized copied +generator SHA-256 is +`69ceec2d63bf35e27990142ca2e9e72f9c36dd2f4fba5113d17552c1ccbc35ca`; +`lpstop-fetch-pc.patch` is +`64cb8ff58d5a3a4fb4f217e3327fb1748d73f85521e3c9be98af7fd08f3a47bf`. +The fresh `artifacts/m6-lpstop-qualified-inputs-v2/manifest.json` SHA-256 is +`2cff30fc342e5c3953193eb2e85b08b89cab3d15bb87a992fc41a7a7ca30278e`; +it records every input file, configuration, compiler, executable and bridge +identity. Generator executable SHA-256 is +`2e57b5685af5f18dc2c7856d44c9c38e7bd2c0ebd89dd0b0babb9fcc26409b5b`; +native bridge SHA-256 is +`17e8b5c7ae0edc6fa4b79c4ee52d797b1d65a1e7423a80770197603cd2d22b3f`. + +The first generated selection had 147,456 passing exception callbacks, but +malformed encodings did not request automatic supervisor rounds. It is retained +as discovery, not the final qualified scope. The maintained preset explicitly +requests `feature_sr_mask=0x2000` for both privilege states. The pinned generator +uses a recognized second word when immediate bits 6/7 are zero, otherwise a +seeded unrecognized word. It skips legal supervisor stopped outcomes. Thus this +corpus does not cover every immediate/extension pair or stopped/trace behavior. +The final exception corpus passes **245,760 callbacks and frames**, zero masked +SR cases, mismatches, unsupported or untested selected directories. Its input +classifier reads immutable opcode/extension/immediate/SR bytes before CPU +execution, requires the canonical first word and CCR 0/31 with no incoming +trace, and rejects stopped or foreign profile inputs. Exact form distributions +are checked, not just their total: + +| Encoding / immediate S | Incoming SR combinations | Cases per combination | Total | +| --- | --- | ---: | ---: | +| Recognized / S=0 | `0000`, `001F`, `2000`, `201F` | 8,192 | 32,768 | +| Recognized / S=1 | `0000`, `001F` | 8,192 | 16,384 | +| Unrecognized / S=0 | `0000`, `001F`, `2000`, `201F` | 24,576 | 98,304 | +| Unrecognized / S=1 | `0000`, `001F`, `2000`, `201F` | 24,576 | 98,304 | +| Total | 14 combinations | | 245,760 | + +Register, defined-SR and frame-byte corruptions each fail on an executed case. +The maintained command requires the opt-in audit plus ten encoding/distribution +tests to execute and pass, with a nonempty coverage report: + +```powershell +./scripts/prepare-copper68k-winuae.ps1 -GeneratorSource -RunnerSource -VcVars64 -Preset LowPowerStop -OutputDirectory +./scripts/test-copper68k-winuae-qualified-exceptions.ps1 -Preset LowPowerStop -InputDirectory -OutputDirectory +``` + +Evidence is `artifacts/m6-lpstop-reference-final/` and the full-suite +`winuae-lpstop-audit.json`. Preliminary reports intentionally fail while exact +corpus counts are being discovered; they are not relabeled as qualified gates. +Seven isolated input/source controls fail for their specific defects: empty +profile/family/input selections, missing/changed data, changed generator and +changed patch. See `artifacts/m6-lpstop-reference-controls-v2/controls.json`. +The first control attempt retains partial evidence: removing a memory image +returns a different diagnostic from the helper's expected missing-data selection +message. It is not counted as the complete seven-control proof. + +### LPSTOP duplicate retirement proof + +The former LPSTOP loop in +`SyntheticModelSystemTests.Move16CacheInstructionsBreakpointsAndLowPowerStop` +tested the legal extension, immediates `0000/0700/2000/2700/271F`, all 32 +initial CCRs and both privilege states: 320 cases per profile, 2,560 total. +The replacement +`SyntheticLowPowerStopTests.EncodingPrivilegeStatusAndIncomingTraceHaveIndependentOutcomes` +now includes those exact five values plus `071F/A01F`, with the legal extension, +eighteen malformed encoding controls, both privilege states, incoming T1/no +trace and all CCRs. Its former-compatible cases use the same public factory, +stack/neighbor fixtures and full architectural checks. For stopped outcomes it +also verifies a second execution cannot retire the sentinel or add an exception. +The exhaustive malformed-word group remains separate. + +Before removing the old loop, the maintained S-clear mutation ran both tests: +the old batch detects 64 mismatches and the replacement detects 192. The +first replacement witness is +`68060/LPSTOP/encoding=01C0/imm=0000/super=True/T=0000/op=F800/ccr=00`. +The exhaustive group still passes under this mutation, demonstrating a distinct +status-rule defect rather than encoding failure. Wrong-vector and early- +privilege mutations detect 147,198 and 73,599 mismatches respectively. Source +restores byte-for-byte and rebuilds. Proof is +`artifacts/m6-lpstop-consolidation-proof/mutation-proof.json`. + +Only the duplicate LPSTOP loop is retired. Its remaining test is renamed +`Move16CacheInstructionsAndBreakpoints`, and its MOVE16 mutation selection is +updated. No cache/prefetch/bus/fault/JIT/native test is retired. Ordinary-CI +LPSTOP coverage is now 267,262 cases in nine batches: 131,070 exhaustive cases +and 17,024 boundary/status cases per profile. The residual `system-model` +batch has 12,800 cases per profile. Historical counts above describe their +original checkpoints and are not rewritten. + +Final validation in `artifacts/m6-lpstop-consolidated-full/` passes **4,978 CPU +tests**, eleven optional skips and zero failures, with all five qualified WinUAE +presets enabled. The strict gate in `artifacts/m6-lpstop-consolidated-gate.log` +verifies **16,035,670 passing logical cases / 595 reporting batches**; fresh +SingleStepTests and Musashi audits pass 312,500 cases / 125 files and 536 +programs / 88 explicit exclusions. CPU production source remains `cb9679d`; +this checkpoint changes test/reference infrastructure only. Its full-suite CPU +assembly SHA-256 is +`4e718d58cd6c24df668b498943e5e38a770c14ec04cfbf3bbd756cbe06d14f9e`; +adapter SHA-256 is +`290709cd4f3a447c106015f3ecc4881e1d8647888490e1b89d68709945d037d7`. +Six fresh report controls reject missing enumeration/absence profiles, empty +enumeration, stale boundary counts, foreign model and the old duplicate loop's +13,120-case count. See +`artifacts/m6-lpstop-consolidated-report-controls/controls.json`. +Consumer/package validation remains the preceding unpublished `.57` evidence +with its original binary identities; it is not claimed as a new consumer replay. +No package is published or dependency boundary changed. + +The original Basic LPSTOP failure remains preserved. This qualified exception +preset resolves its documented reference-fetch issue for a separate selection; +it does not make Basic passing or close ordinary STOP's S-clear disagreement. +Advanced restoration/fault, chained SR provenance, broader reference and +consolidation gaps remain required. Milestone 6 and the active goal remain +**in progress**, with `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 34b70651..f4ef225a 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -122,6 +122,17 @@ identities, replacement proofs and remaining required coverage here as work progresses. No milestone is complete merely because an inventory exists or a partial matrix passes. +Latest milestone-6 checkpoint, 2026-10-05: independent LPSTOP exception +qualification passes 245,760 pinned WinUAE callbacks/frames in fourteen +architectural combinations. The new synthetic matrix includes all former SR +values and passes 267,262 cases; shared mutation proof permits removal of the +duplicate 320-case-per-profile LPSTOP loop. MOVE16/cache/BKPT and specialized +regressions remain. The full CPU suite passes 4,978 tests (eleven optional skips), +and the strict gate passes 16,035,670 logical cases in 595 reporting batches. +See the [qualification and retirement record](COPPER68K_REFERENCE_QUALIFICATION.md#lpstop-independent-exception-qualification-and-consolidation-2026-10-05). +Milestone 6 remains in progress: this checkpoint does not close the advanced +restoration, reference disagreement or broader consolidation requirements. + ### First implementation checkpoint — 2026-10-04 The [suite guide](../Copper68k.Tests/Synthetic/README.md) describes fixtures, diff --git a/scripts/prepare-copper68k-winuae.ps1 b/scripts/prepare-copper68k-winuae.ps1 index 48f46c6d..5f40c33c 100644 --- a/scripts/prepare-copper68k-winuae.ps1 +++ b/scripts/prepare-copper68k-winuae.ps1 @@ -4,7 +4,7 @@ param( [Parameter(Mandatory)] [string] $GeneratorSource, [Parameter(Mandatory)] [string] $RunnerSource, [Parameter(Mandatory)] [string] $VcVars64, - [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints','LongArithmetic','WordDivision')] [string] $Preset = 'Basic', + [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop')] [string] $Preset = 'Basic', [string] $OutputDirectory = 'artifacts/winuae-model-inputs' ) $ErrorActionPreference = 'Stop' @@ -48,17 +48,17 @@ try { & ./build68k.exe table68k | Set-Content -Encoding utf8 cpudefs.cpp if ($LASTEXITCODE -ne 0) { throw 'Opcode table generation failed' } $cpuGeneratorSource = 'gencpu.cpp' - if ($Preset -in @('TrapBounds','Breakpoints','LongArithmetic','WordDivision')) { + if ($Preset -in @('TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop')) { # TrapBounds: following PC (MC68020UM 6.1.4 / MC68040UM 8.2.3). # Breakpoints: opcode PC (illegal exception, UM 6.1.5 / 8.2.4). # Patch a copy; Basic keeps the pinned source. - $sourceName = switch ($Preset) {'TrapBounds' {'gencpu-trap-bounds.cpp'} 'Breakpoints' {'gencpu-breakpoints.cpp'} 'LongArithmetic' {'gencpu-long-arithmetic.cpp'} 'WordDivision' {'gencpu-word-division.cpp'}} - $patchName = switch ($Preset) {'TrapBounds' {'trap-bounds-pc.patch'} 'Breakpoints' {'breakpoint-pc.patch'} 'LongArithmetic' {'long-arithmetic-unimplemented.patch'} 'WordDivision' {'word-division-carry.patch'}} + $sourceName = switch ($Preset) {'TrapBounds' {'gencpu-trap-bounds.cpp'} 'Breakpoints' {'gencpu-breakpoints.cpp'} 'LongArithmetic' {'gencpu-long-arithmetic.cpp'} 'WordDivision' {'gencpu-word-division.cpp'} 'LowPowerStop' {'gencpu-lpstop.cpp'}} + $patchName = switch ($Preset) {'TrapBounds' {'trap-bounds-pc.patch'} 'Breakpoints' {'breakpoint-pc.patch'} 'LongArithmetic' {'long-arithmetic-unimplemented.patch'} 'WordDivision' {'word-division-carry.patch'} 'LowPowerStop' {'lpstop-fetch-pc.patch'}} $patchPath = Join-Path $PSScriptRoot ('winuae/' + $patchName) $patch = [IO.File]::ReadAllText($patchPath).Replace("`r`n", "`n") $source = [IO.File]::ReadAllText((Join-Path $generator 'gencpu.cpp')).Replace("`r`n", "`n") $hunks = @($patch -split "(?m)^@@`n" | Select-Object -Skip 1) - $requiredHunks = switch ($Preset) {'Breakpoints' {1} 'TrapBounds' {2} 'LongArithmetic' {3} 'WordDivision' {1}} + $requiredHunks = switch ($Preset) {'Breakpoints' {1} 'TrapBounds' {2} 'LongArithmetic' {3} 'WordDivision' {1} 'LowPowerStop' {2}} if ($hunks.Count -ne $requiredHunks) { throw "Incorrect $Preset patch hunk count" } foreach ($hunk in $hunks) { $lines = $hunk.TrimEnd("`n").Split("`n") @@ -161,6 +161,11 @@ void M68KTester_destroy(M68KTesterContext* context) { if ($Preset -eq 'WordDivision') { $baseIni = $baseIni.Replace('[test=Basic]', '[test=WordDivision]').Replace('mode=all', 'mode=DIVU.W,DIVS.W') } + if ($Preset -eq 'LowPowerStop') { + # Request both privilege states even for malformed encodings: Line-F + # alone does not request the generator's automatic supervisor round. + $baseIni = $baseIni.Replace('[test=Basic]', '[test=LowPowerStop]').Replace('mode=all', 'mode=LPSTOP').Replace('feature_sr_mask=0x0000', 'feature_sr_mask=0x2000') + } foreach ($model in @( @{id='68000'; cpu='68000'; width=24}, @{id='68010'; cpu='68010'; width=24}, @{id='68EC020'; cpu='68020'; width=24}, @{id='68020'; cpu='68020'; width=32}, @@ -170,6 +175,7 @@ void M68KTester_destroy(M68KTesterContext* context) { if ($Preset -eq 'TrapBounds' -and $model.id -in @('68000','68010')) { continue } if ($Preset -eq 'Breakpoints' -and $model.id -eq '68000') { continue } if ($Preset -eq 'LongArithmetic' -and $model.id -in @('68000','68010')) { continue } + if ($Preset -eq 'LowPowerStop' -and $model.id -ne '68060') { continue } $profileRoot = Join-Path $output $model.id New-Item -ItemType Directory -Path $profileRoot | Out-Null # Every invocation uses a fresh generator output path; stale data cannot fill a gap. @@ -212,6 +218,8 @@ void M68KTester_destroy(M68KTesterContext* context) { LongArithmeticCpuPatchSha256=$(if ($Preset -eq 'LongArithmetic') {(Get-FileHash -LiteralPath (Join-Path $output 'long-arithmetic-unimplemented.patch')).Hash.ToLowerInvariant()} else {$null}) WordDivisionSourceSha256=$(if ($Preset -eq 'WordDivision') {(Get-FileHash -LiteralPath (Join-Path $output 'gencpu-word-division.cpp')).Hash.ToLowerInvariant()} else {$null}) WordDivisionPatchSha256=$(if ($Preset -eq 'WordDivision') {(Get-FileHash -LiteralPath (Join-Path $output 'word-division-carry.patch')).Hash.ToLowerInvariant()} else {$null}) + LowPowerStopSourceSha256=$(if ($Preset -eq 'LowPowerStop') {(Get-FileHash -LiteralPath (Join-Path $output 'gencpu-lpstop.cpp')).Hash.ToLowerInvariant()} else {$null}) + LowPowerStopPatchSha256=$(if ($Preset -eq 'LowPowerStop') {(Get-FileHash -LiteralPath (Join-Path $output 'lpstop-fetch-pc.patch')).Hash.ToLowerInvariant()} else {$null}) NativeLibrarySha256=(Get-FileHash -LiteralPath (Join-Path $output 'm68k_cpu_tester.dll') -Algorithm SHA256).Hash.ToLowerInvariant() Compiler=$compiler; GeneratorExecutableSha256=(Get-FileHash -LiteralPath (Join-Path $generator 'cputester.exe') -Algorithm SHA256).Hash.ToLowerInvariant() NativeSourceSha256=(Get-FileHash -LiteralPath (Join-Path $output 'winuae-native.c') -Algorithm SHA256).Hash.ToLowerInvariant() diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index da54aa65..442d4113 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -88,7 +88,7 @@ $mutations += @( @{name='chk2-boundary-z'; file=$advanced; before=' State.SetFlag(M68kCpuState.Zero, value == lower || value == upper);'; after=' State.SetFlag(M68kCpuState.Zero, false);'; group='CheckBoundsValuesFlagsFormsAndRegisterAliases'; milestone=5}, @{name='cmp2-address-width'; file=$advanced; before=' var value = useAddressRegister ? unchecked((int)State.A[register]) : SignExtendForSize(State.D[register], size);'; after=' var value = SignExtendForSize(useAddressRegister ? State.A[register] : State.D[register], size);'; group='CheckBoundsValuesFlagsFormsAndRegisterAliases'; milestone=5}, @{name='rte-throwaway'; file=$advanced; before=' if (format == 1) continue; // Throwaway frame can select another stack.'; after=' // Mutant: accept throwaway as final frame.'; group='RteFramesPrivilegeStackSelectionAndInvalidFormats'; milestone=5}, - @{name='move16-postincrement'; file='Copper68k/M68kAdvancedTimingInterpreter.System.cs'; before=' if (form == 4 || form < 2) WriteGeneralRegister(true, register, unchecked(State.A[register] + 16));'; after=' if (form == 4 || form < 2) WriteGeneralRegister(true, register, unchecked(State.A[register] + 4));'; group='Move16CacheInstructionsBreakpointsAndLowPowerStop'; milestone=5; model='68040'}, + @{name='move16-postincrement'; file='Copper68k/M68kAdvancedTimingInterpreter.System.cs'; before=' if (form == 4 || form < 2) WriteGeneralRegister(true, register, unchecked(State.A[register] + 16));'; after=' if (form == 4 || form < 2) WriteGeneralRegister(true, register, unchecked(State.A[register] + 4));'; group='Move16CacheInstructionsAndBreakpoints'; milestone=5; model='68040'}, @{name='cacr-clear-readback'; file=$advanced; before='? value & ~0x0C0Cu : value; // Clear commands always read as zero.'; after='? value : value; // Mutant: clear commands read back set.'; group='MovecControlInventoryMasksPrivilegeAndAllGeneralRegisters'; milestone=5} ) $mutations += @( @@ -138,7 +138,8 @@ $mutations += @( if (FetchWord() != 0x01c0) '@; group='SyntheticLowPowerStopTests'; milestone=6; model='68060'; lpstop=$true}, @{name='060-lpstop-clear-s'; file='Copper68k/M68kAdvancedTimingInterpreter.System.cs'; before=' if ((immediate & M68kCpuState.Supervisor) == 0)'; - after=' if (false)'; group='SyntheticLowPowerStopTests'; milestone=6; model='68060'; lpstop=$true} + after=' if (false)'; group='SyntheticLowPowerStopTests'; milestone=6; model='68060'; lpstop=$true; + legacyTest='Move16CacheInstructionsBreakpointsAndLowPowerStop'; legacyFile='Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs'; legacyModel='68060'} ) if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } @@ -171,7 +172,10 @@ try { $model = $(if ($mutation.model) { $mutation.model } elseif ($mutation.name -eq '060-divide-frame') { '68060' } else { '68020' }) $filter = "FullyQualifiedName~$($mutation.group)&DisplayName~$model" $legacyPresent = $mutation.legacyTest -and [IO.File]::ReadAllText((Join-Path $repo $mutation.legacyFile)).Contains("void $($mutation.legacyTest)(") - if ($legacyPresent) { $filter += "|FullyQualifiedName~$($mutation.legacyTest)" } + if ($legacyPresent) { + $filter += "|FullyQualifiedName~$($mutation.legacyTest)" + if ($mutation.legacyModel) { $filter += "&DisplayName~$($mutation.legacyModel)" } + } & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --no-restore --filter $filter --logger "trx;LogFileName=mutation.trx" --results-directory $directory *> (Join-Path $directory 'run.log') $exitCode = $LASTEXITCODE $batches = @(Get-ChildItem -LiteralPath $directory -Filter '*.json' | ForEach-Object { Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json }) @@ -196,15 +200,17 @@ try { } if ($mutation.lpstop) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw - if ($trx.TestRun.ResultSummary.Counters.executed -ne 2 -or $batches.Count -ne 2 -or + $expectedBatches = 2 + [int][bool]$legacyPresent + if ($trx.TestRun.ResultSummary.Counters.executed -ne $expectedBatches -or $batches.Count -ne $expectedBatches -or @($batches | Where-Object { $_.model -eq '68060' -and $_.group -eq 'system-lpstop-extensions' -and $_.logicalCases -eq 131070 }).Count -ne 1 -or - @($batches | Where-Object { $_.model -eq '68060' -and $_.group -eq 'system-lpstop-values' -and $_.logicalCases -eq 12160 }).Count -ne 1) { + @($batches | Where-Object { $_.model -eq '68060' -and $_.group -eq 'system-lpstop-values' -and $_.logicalCases -eq 17024 }).Count -ne 1 -or + ($legacyPresent -and @($batches | Where-Object { $_.model -eq '68060' -and $_.group -eq 'system-model' -and $_.logicalCases -eq 13120 }).Count -ne 1)) { throw 'LPSTOP mutation did not execute its complete two-batch selection' } } if ($legacyPresent) { [xml]$legacyTrx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw - if (@($legacyTrx.TestRun.Results.UnitTestResult | Where-Object { $_.testName.EndsWith($mutation.legacyTest) -and $_.outcome -eq 'Failed' }).Count -ne 1) { throw 'Original regression did not detect the same mutation' } + if (@($legacyTrx.TestRun.Results.UnitTestResult | Where-Object { ($_.testName.EndsWith($mutation.legacyTest) -or $_.testName.Contains($mutation.legacyTest + '(')) -and $_.outcome -eq 'Failed' }).Count -ne 1) { throw 'Original regression did not detect the same mutation' } } $results += @{mutation=$mutation.name; file=$mutation.file; sourceSha256=(Get-FileHash -LiteralPath $path).Hash; detected=$true; replacementCase=$failures[0].id; diagnostic=$failures[0].reason; originalRegressionDetected=$legacyPresent; diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 8bccc54a..3bc79ebc 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -111,8 +111,8 @@ try { 'system-bounds'=$(if ($model -in @('68000','68010')) {11828} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {12488}); 'system-callm'=$(if ($model -in @('68000','68010')) {10832} elseif ($model -in @('68020','68EC020','A1200')) {11000} elseif ($model -in @('68030','68040','68060')) {10964}); 'system-interrupt'=$(if ($model -in @('68000')) {224} elseif ($model -in @('68010','68060')) {226} elseif ($model -in @('68020','68030','68040','68EC020','A1200')) {418}); - 'system-model'=13120; - 'system-lpstop-values'=12160; + 'system-model'=12800; + 'system-lpstop-values'=17024; 'system-movec'=2432; 'system-moves'=$(if ($model -in @('68000','68010')) {10656} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {11052}); 'system-moves-invalid-operands'=7296; diff --git a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 index a0623d4f..12b456aa 100644 --- a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 +++ b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 @@ -1,7 +1,7 @@ #requires -Version 7.0 [CmdletBinding()] param( - [ValidateSet('TrapBounds','Breakpoints','WordDivision')] [string] $Preset = 'TrapBounds', + [ValidateSet('TrapBounds','Breakpoints','WordDivision','LowPowerStop')] [string] $Preset = 'TrapBounds', [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $InputDirectory, [string] $NativeLibrary, [string] $OutputDirectory = 'artifacts/winuae-trap-bounds-audit' @@ -14,15 +14,19 @@ $library = (Resolve-Path -LiteralPath $NativeLibrary).Path $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } New-Item -ItemType Directory -Path $output | Out-Null -$key = switch ($Preset) {'TrapBounds' {'TRAP_BOUNDS'} 'Breakpoints' {'BREAKPOINT'} 'WordDivision' {'WORD_DIVISION'}} -$testName = switch ($Preset) {'TrapBounds' {'WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled'} 'Breakpoints' {'WinUaeBreakpointExceptionsAcrossSelectedModelsWhenEnabled'} 'WordDivision' {'WinUaeWordDivisionAcrossSelectedModelsWhenEnabled'}} -$reportName = switch ($Preset) {'TrapBounds' {'winuae-trap-bounds-audit.json'} 'Breakpoints' {'winuae-breakpoint-audit.json'} 'WordDivision' {'winuae-word-division-audit.json'}} +$key = switch ($Preset) {'TrapBounds' {'TRAP_BOUNDS'} 'Breakpoints' {'BREAKPOINT'} 'WordDivision' {'WORD_DIVISION'} 'LowPowerStop' {'LPSTOP'}} +$testName = switch ($Preset) {'TrapBounds' {'WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled'} 'Breakpoints' {'WinUaeBreakpointExceptionsAcrossSelectedModelsWhenEnabled'} 'WordDivision' {'WinUaeWordDivisionAcrossSelectedModelsWhenEnabled'} 'LowPowerStop' {'WinUaeLowPowerStopExceptionsWhenEnabled'}} +$reportName = switch ($Preset) {'TrapBounds' {'winuae-trap-bounds-audit.json'} 'Breakpoints' {'winuae-breakpoint-audit.json'} 'WordDivision' {'winuae-word-division-audit.json'} 'LowPowerStop' {'winuae-lpstop-audit.json'}} $filter = "FullyQualifiedName~$testName" $expectedTests = 1 if ($Preset -eq 'WordDivision') { $filter += '|FullyQualifiedName~M68kWinUaeWordDivisionEncodingTests' $expectedTests = 15 } +if ($Preset -eq 'LowPowerStop') { + $filter += '|FullyQualifiedName~M68kWinUaeLowPowerStopEncodingTests' + $expectedTests = 11 +} $settings = @{ ('COPPER68K_RUN_WINUAE_' + $key + '_AUDIT') = '1' ('COPPER68K_WINUAE_' + $key + '_PATH') = $root diff --git a/scripts/winuae/lpstop-fetch-pc.patch b/scripts/winuae/lpstop-fetch-pc.patch new file mode 100644 index 00000000..3c1d3464 --- /dev/null +++ b/scripts/winuae/lpstop-fetch-pc.patch @@ -0,0 +1,15 @@ +--- a/gencpu/gencpu.cpp ++++ b/gencpu/gencpu.cpp +@@ + case i_LPSTOP: /* 68060 */ +- out("uae_u16 sw = %s(2);\n", srcwi); ++ out("uae_u16 sw = get_word_test_prefetch(2);\n"); + out("if (sw != 0x01c0) {\n"); +@@ + out("if (!(regs.sr & 0x2000)) {\n"); + out("Exception(8);\n"); + write_return_cycles(0); + out("}\n"); +- out("uae_u16 newsr = %s(4);\n", srcwi); ++ out("uae_u16 newsr = get_word_test_prefetch(4);\n"); + out("if (!(newsr & 0x2000)) {\n"); From 73f94aa782194093f71475d0d916f17efd6da34a Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 17:49:01 +0300 Subject: [PATCH 037/211] Qualify legal MOVES inputs across selected CPU profiles --- .../M68kWinUaeLowPowerStopAuditTests.cs | 2 +- Copper68k.Tests/M68kWinUaeMovesAuditTests.cs | 90 +++++++++++++++ .../M68kWinUaeTrapBoundsAuditTests.cs | 4 +- Copper68k.Tests/Synthetic/README.md | 21 ++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 104 ++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 ++- scripts/prepare-copper68k-winuae.ps1 | 20 +++- scripts/test-copper68k-synthetic.ps1 | 2 +- ...-copper68k-winuae-qualified-exceptions.ps1 | 12 +- scripts/winuae/moves-encodings.patch | 23 ++++ 10 files changed, 278 insertions(+), 15 deletions(-) create mode 100644 Copper68k.Tests/M68kWinUaeMovesAuditTests.cs create mode 100644 scripts/winuae/moves-encodings.patch diff --git a/Copper68k.Tests/M68kWinUaeLowPowerStopAuditTests.cs b/Copper68k.Tests/M68kWinUaeLowPowerStopAuditTests.cs index 60e6e949..a568e6c5 100644 --- a/Copper68k.Tests/M68kWinUaeLowPowerStopAuditTests.cs +++ b/Copper68k.Tests/M68kWinUaeLowPowerStopAuditTests.cs @@ -10,7 +10,7 @@ public void WinUaeLowPowerStopExceptionsWhenEnabled() => RunQualifiedExceptionPr "69ceec2d63bf35e27990142ca2e9e72f9c36dd2f4fba5113d17552c1ccbc35ca", "64cb8ff58d5a3a4fb4f217e3327fb1748d73f85521e3c9be98af7fd08f3a47bf", "winuae-lpstop-audit.json", ["68060"], ["68060"], _ => ["LPSTOP"], (_, _) => (245760, 245760), "MC68060UM D-19/20 and 8.2.4/5: fixed F800/01C0 encoding, malformed second-word Line-F priority, original SR and opcode-PC privilege frames. A copied generator uses the existing nonadvancing instruction-fetch helper for LPSTOP fixed offsets; get_wordi_test advances PC as a side effect, corrupting the second fetch address and saved PC. Basic and CPU results are unchanged. One seeded round, incoming CCR 0/31 and user/supervisor, no incoming trace. The generator skips stopped outcomes: this preset qualifies exceptions only; stopped state, full CCR, trace and complete malformed-word enumeration remain synthetic coverage. No physical broadcast/pin/timing qualification.", - FormCounts: (_, _) => 14, ClassifyWords: QualifyLowPowerStopEncoding, + FormCounts: (_, _) => 14, ClassifyWords: (_, opcode, extension, immediate, sr, _) => QualifyLowPowerStopEncoding(opcode, extension, immediate, sr), ExpectedForms: (_, _) => LowPowerStopFormDistribution())); internal static IReadOnlyDictionary LowPowerStopFormDistribution() diff --git a/Copper68k.Tests/M68kWinUaeMovesAuditTests.cs b/Copper68k.Tests/M68kWinUaeMovesAuditTests.cs new file mode 100644 index 00000000..6623276a --- /dev/null +++ b/Copper68k.Tests/M68kWinUaeMovesAuditTests.cs @@ -0,0 +1,90 @@ +using Copper68k.Tests.Synthetic; +using Xunit.Sdk; + +namespace Copper68k.Tests; + +public sealed partial class M68kWinUaeCpuTesterConformanceTests +{ + [EnvironmentFact("COPPER68K_RUN_WINUAE_MOVES_AUDIT", "audit qualified WinUAE MOVES legal encodings, operands and privilege frames")] + public void WinUaeMovesAcrossSelectedModelsWhenEnabled() => RunQualifiedExceptionPreset(new( + "Moves", "MOVES", "cputest-moves.cpp", "moves-encodings.patch", "Moves", + "8a7f2930a6f28a48170834481fcb449eee07a92dd7d6df96f8b6675d082692a7", "e79367079bbbbe4fda3b326cf7c6a5cf15e08f021cc3137b1fc3b3aeee756368", + "winuae-moves-audit.json", ["68010", "68EC020", "68020", "68030", "68040", "68060"], + ["68010", "68EC020", "68020", "68030", "68040", "68060", "A1200"], + _ => ["MOVES.B", "MOVES.W", "MOVES.L"], (model, family) => { var counts = MovesCounts(model, family); return (counts.Cases, counts.Frames); }, + "M68000PM 6-24/25/26: B/W/L memory-alterable operands, reserved extension fields zero, preserved CCR, partial Dn loads and sign-extended An loads; same-An postincrement/predecrement store values are explicitly undefined. A copied input generator clears reserved fields and excludes those store candidates before execution, leaving random consumption and independent expected CPU results intact. Original Basic inputs and failures are retained. All supported selected profiles, with 68000 absence covered synthetically; CCR 0/31, user/supervisor, one seeded round and full extensions enabled. The pinned generator and public bus use a flat address space; physical SFC/DFC spaces, cache coherency, bus ordering, trace/fault restart and timing are unqualified.", + FormCounts: (model, family) => MovesCounts(model, family).Forms, ClassifyWords: QualifyMovesEncoding)); + + private static (int Cases, uint Frames, int Forms) MovesCounts(ModelSpec model, string family) => + (FixtureId(model.Id), family) switch + { + ("68010", "MOVES.B") => (5410, 3844, 2760), + ("68010", "MOVES.W") => (4748, 3770, 2460), + ("68010", "MOVES.L") => (4810, 3778, 2468), + ("68EC020", "MOVES.B") => (5368, 3936, 3032), + ("68EC020", "MOVES.W") => (5346, 3980, 3022), + ("68EC020", "MOVES.L") => (5476, 3892, 3030), + ("68020" or "68030" or "68040" or "68060", "MOVES.B") => (5238, 4268, 2846), + ("68020" or "68030" or "68040" or "68060", "MOVES.W") => (5214, 4300, 2752), + ("68020" or "68030" or "68040" or "68060", "MOVES.L") => (5334, 4198, 2918), + _ => throw new XunitException($"Unqualified MOVES family: {model.Id}/{family}") + }; + + // Manual fields and immutable input only, never the production decoder/EA. + internal static string QualifyMovesEncoding(ModelSpec model, ushort opcode, ushort extension, ushort eaExtension, ushort inputSr, string family) + { + var size = (opcode >> 6) & 3; + var mode = (opcode >> 3) & 7; + var eaRegister = opcode & 7; + var general = extension >> 12; + var store = (extension & 0x0800) != 0; + var expected = size switch { 0 => "MOVES.B", 1 => "MOVES.W", 2 => "MOVES.L", _ => "invalid" }; + if (model.Id == "68000" || (opcode & 0xff00) != 0x0e00 || family != expected || mode < 2 || + (mode == 7 && eaRegister > 1) || (extension & 0x07ff) != 0 || + inputSr is not (0x0000 or 0x001f or 0x2000 or 0x201f) || + (store && general >= 8 && general - 8 == eaRegister && mode is 3 or 4)) + throw new XunitException($"Unqualified MOVES encoding/profile: {model.Id}/{family}/{opcode:X4}/{extension:X4}/{inputSr:X4}"); + var index = "none"; + if (mode == 6) + { + if (!model.FullIndex || (eaExtension & 0x0100) == 0) + index = model.FullIndex ? "brief" : "brief-unscaled"; + else + { + var displacement = (eaExtension >> 4) & 3; + var indirect = eaExtension & 7; + if ((eaExtension & 8) != 0 || displacement == 0 || indirect == 4) + throw new XunitException($"Unqualified MOVES full extension: {eaExtension:X4}"); + index = $"full/BS={(eaExtension >> 7) & 1}/IS={(eaExtension >> 6) & 1}/BD={displacement}/IIS={indirect}"; + } + } + return $"{family}/ea={mode}:{eaRegister}/R={general}/store={store}/index={index}/sr={inputSr:X4}"; + } +} + +public sealed class M68kWinUaeMovesEncodingTests +{ + [Theory] + [InlineData("68010", 0x0e90, 0x5800, 0, 0x2000, "MOVES.L", "MOVES.L/ea=2:0/R=5/store=True/index=none/sr=2000")] + [InlineData("68010", 0x0e1f, 0xf000, 0, 0x001f, "MOVES.B", "MOVES.B/ea=3:7/R=15/store=False/index=none/sr=001F")] + [InlineData("68020", 0x0e72, 0x9800, 0x7912, 0x201f, "MOVES.W", "MOVES.W/ea=6:2/R=9/store=True/index=full/BS=0/IS=0/BD=1/IIS=2/sr=201F")] + [InlineData("68010", 0x0e72, 0x9800, 0x7912, 0x201f, "MOVES.W", "MOVES.W/ea=6:2/R=9/store=True/index=brief-unscaled/sr=201F")] + [InlineData("A1200", 0x0eb9, 0xb000, 0, 0, "MOVES.L", "MOVES.L/ea=7:1/R=11/store=False/index=none/sr=0000")] + public void FixedManualExamplesDistinguishSizeRegistersDirectionAndIndex(string modelId, int opcode, int extension, int eaExtension, int sr, string family, string expected) => + Assert.Equal(expected, M68kWinUaeCpuTesterConformanceTests.QualifyMovesEncoding(ModelSpec.All.Single(x => x.Id == modelId), (ushort)opcode, (ushort)extension, (ushort)eaExtension, (ushort)sr, family)); + + [Theory] + [InlineData("68000", 0x0e90, 0x5800, 0, 0x2000, "MOVES.L")] + [InlineData("68010", 0x0e9d, 0xda5e, 0, 0x2000, "MOVES.L")] + [InlineData("68010", 0x0e9d, 0xd800, 0, 0x2000, "MOVES.L")] + [InlineData("68020", 0x0ea0, 0x8800, 0, 0x2000, "MOVES.L")] + [InlineData("68020", 0x0eba, 0x1000, 0, 0, "MOVES.L")] + [InlineData("68020", 0x0ef0, 0x1000, 0, 0, "MOVES.L")] + [InlineData("68020", 0x0e90, 0x5801, 0, 0x2000, "MOVES.L")] + [InlineData("68020", 0x0e90, 0x5800, 0, 0x2000, "MOVES.W")] + [InlineData("68020", 0x0e90, 0x5800, 0, 0xa000, "MOVES.L")] + [InlineData("68020", 0x0eb0, 0x5800, 0x7900, 0x2000, "MOVES.L")] + [InlineData("68020", 0x0eb0, 0x5800, 0x7914, 0x2000, "MOVES.L")] + public void ReservedUndefinedUnavailableAndForeignFormsAreRejected(string modelId, int opcode, int extension, int eaExtension, int sr, string family) => + Assert.Throws(() => M68kWinUaeCpuTesterConformanceTests.QualifyMovesEncoding(ModelSpec.All.Single(x => x.Id == modelId), (ushort)opcode, (ushort)extension, (ushort)eaExtension, (ushort)sr, family)); +} diff --git a/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs b/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs index 535dff4c..7208e610 100644 --- a/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs +++ b/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs @@ -79,7 +79,7 @@ private void RunQualifiedExceptionPreset(QualifiedExceptionPreset preset) preset.ArithmeticFlagControls, carryDetected, carry.ExecutedCases, ignoredAccepted, ignored.ExecutedCases)); var result = tester.Run(path, family, fixture.CpuLevel, false, false, model, fixtureClassifier: preset.ClassifyForm is null ? null : (opcode, inputSr) => preset.ClassifyForm(opcode, inputSr, family), - fixtureWordsClassifier: preset.ClassifyWords); + fixtureWordsClassifier: preset.ClassifyWords is null ? null : (opcode, extension, followingWord, inputSr) => preset.ClassifyWords(model, opcode, extension, followingWord, inputSr, family)); var expected = preset.Counts(model, family); var forms = new SortedDictionary(tester.FixtureForms.ToDictionary(x => x.Key, x => x.Value), StringComparer.Ordinal); var expectedForms = preset.FormCounts?.Invoke(model, family) ?? 0; @@ -134,7 +134,7 @@ private sealed record QualifiedExceptionPreset(string Name, string EnvironmentKe Func Families, Func Counts, string Qualification, bool ArithmeticFlagControls = false, Func? MaskedCounts = null, Func? ClassifyForm = null, Func? FormCounts = null, - Func? ClassifyWords = null, + Func? ClassifyWords = null, Func>? ExpectedForms = null); private sealed record WinUaeQualifiedExceptionRow(string Model, string Family, string Status, int ExecutedCases, uint ExceptionFrames, uint MaskedSrCases, bool Controls, SortedDictionary Forms, string Detail); diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 510632a1..18348307 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -290,3 +290,24 @@ LowPowerStop -InputDirectory -OutputDirectory ` The [reference record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#lpstop-independent-exception-qualification-and-consolidation-2026-10-05) records the source correction, corpus limitations and retirement proof. The original Basic disagreement remains retained; this is a separate qualified preset. + +Legal MOVES B/W/L reference inputs have a separate `Moves` preset. It clears the +eleven reserved extension bits and excludes same-An postincrement/predecrement +stores before reference execution, as M68000PM 6-24/25/26 requires. Original +Basic inputs and undefined-store disagreements remain preserved. All three +families on 010/EC020/A1200/020/030/040/060 pass 110,492 callbacks, with 86,072 +validated privilege frames and 59,920 recorded architectural forms. The report +identifies operand mode/register, general register, direction, incoming S/CCR, +brief indexing and full-format suppression/displacement/indirection structure. +It is a seeded flat-address-space sample, not physical function-code, bus, +cache, trace/fault-restart or exhaustive indexed-value qualification. +Prepare with `./scripts/prepare-copper68k-winuae.ps1 -Preset Moves` and the pinned +source/compiler arguments; run +`./scripts/test-copper68k-winuae-qualified-exceptions.ps1 -Preset Moves +-InputDirectory -OutputDirectory `. +Missing/incompatible fixtures or empty selections fail. Fixed encoding examples +and register/SR/frame corruption controls run alongside actual CPU comparisons. +The 68000 unavailable outcome remains in synthetic coverage. The existing +MOVES privilege-before-extension bus-ordering regression is retained. +The [MOVES qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#moves-legal-input-reference-qualification-2026-10-05) +documents identities, complete selections, failed discovery and limitations. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index d3d51318..78f264dd 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -3232,3 +3232,107 @@ it does not make Basic passing or close ordinary STOP's S-clear disagreement. Advanced restoration/fault, chained SR provenance, broader reference and consolidation gaps remain required. Milestone 6 and the active goal remain **in progress**, with `roadmapComplete=false`. + +## MOVES legal-input reference qualification, 2026-10-05 + +The retained Basic 68010 `MOVES.L` failure at callback 783 has opcode `0E9D`, +extension `DA5E`, SR `2000`: it stores A5 through `(A5)+`. The native reference +expects the original value and Copper68k stores the incremented value. +[M68000PM](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) 6-26 marks +same-An postincrement/predecrement stored values undefined; 6-24/25 fixes the +extension's lower eleven fields at zero. This input violates both qualifications. +The synthetic MOVES matrix already excludes undefined stores and uses canonical +extension words. No production change is warranted by this disagreement. + +The separate `Moves` preset qualifies legal sampled inputs for 010, EC020, +020, 030, 040 and 060, with A1200 executing the EC020 fixture through its own +public factory. The generator does not supply MOVES on 000; its documented +unavailable outcome remains synthetic coverage. A copied `cputest.cpp` masks +reserved fields and rejects undefined same-An store candidates before reference +execution. Random consumption remains intact. The generator's direction check +now reads the fixed instruction extension at `opcode_memory_start+2`, rather +than the final EA extension at `pc-2`. Neither CPU results nor comparator masks +are normalized. Original Basic inputs, sources and mismatching rows remain. + +Pins remain generator `025b999239800357e95065fe5b9a15ea5b300fa7` and runner +`7a83745d6c6159bc74ab0471578ffc8bc244e66e`. Normalized copied source SHA-256 is +`8a7f2930a6f28a48170834481fcb449eee07a92dd7d6df96f8b6675d082692a7`; +`moves-encodings.patch` is +`e79367079bbbbe4fda3b326cf7c6a5cf15e08f021cc3137b1fc3b3aeee756368`. +The fresh `artifacts/m6-moves-qualified-inputs-v2/manifest.json` SHA-256 is +`8b4d4c819af6d7b011384253cb20b06e9be42502a7981bc7e4e8939ed8f61125`; +each of its six profiles has three families and twelve pinned input files. +Generator executable SHA-256 is +`987f3d22f6529541ccbbe9508a32a03bd2f161d6af2b83a8c0799ac15a99d0fc`; +native bridge SHA-256 is +`efec6cced943fcbea7312e380b102d2c483d0eb56ac2eb1aceee47481aabb5a0`. + +The input classifier checks immutable words before stack copying or CPU +execution. It rejects wrong family/size, non-memory-alterable EAs, reserved +extension bits, undefined stores, foreign S/CCR profiles and reserved full- +format structural fields. Ordinary 010 indexing is labeled unscaled brief, +including ignored format bits. Advanced full-index forms report base/index +suppression, base displacement length and pre/post/no memory indirection. +This records actual sampled combinations rather than inferring them from a test +count. Exact callback/frame/form counts and complete model/family selections +are required: + +| Profile(s), each | Family | Callbacks | Privilege frames | Recorded forms | +| --- | --- | ---: | ---: | ---: | +| 010 | B | 5,410 | 3,844 | 2,760 | +| 010 | W | 4,748 | 3,770 | 2,460 | +| 010 | L | 4,810 | 3,778 | 2,468 | +| EC020 / A1200 | B | 5,368 | 3,936 | 3,032 | +| EC020 / A1200 | W | 5,346 | 3,980 | 3,022 | +| EC020 / A1200 | L | 5,476 | 3,892 | 3,030 | +| 020 / 030 / 040 / 060 | B | 5,238 | 4,268 | 2,846 | +| 020 / 030 / 040 / 060 | W | 5,214 | 4,300 | 2,752 | +| 020 / 030 / 040 / 060 | L | 5,334 | 4,198 | 2,918 | +| All seven profiles | B/W/L | 110,492 | 86,072 | 59,920 | + +All 21 directories pass, with zero mismatching, unsupported or untested selected +directories and no masked SR cases. Each directory's independent register, +defined-SR and frame-byte corruptions fail, for 63 comparator controls. The +command also requires sixteen fixed encoding/profile tests to pass. Evidence +is `artifacts/m6-moves-reference-qualified/`: + +```powershell +./scripts/prepare-copper68k-winuae.ps1 -GeneratorSource -RunnerSource -VcVars64 -Preset Moves -OutputDirectory +./scripts/test-copper68k-winuae-qualified-exceptions.ps1 -Preset Moves -InputDirectory -OutputDirectory +``` + +The initial patch affected only the generator's exact-target EA path. The +callback classifier rejected its still-reserved random inputs before execution: +all 21 selected directories are explicitly untested, with zero callbacks. This +failed discovery is retained in `artifacts/m6-moves-reference-discovery/`; it +is not counted as CPU failure or passing coverage. The corrected selection's +first count-discovery report intentionally fails while exact count expectations +are established; subsequent qualified evidence uses the fixed expectations. +Seven isolated input/source controls reject empty profile/family/input +selections, missing/changed data, changed generator and changed patch. Their +specific failures and identities are recorded in +`artifacts/m6-moves-reference-controls/controls.json`. + +Qualification is limited to the one-round flat-address-space corpus, incoming +CCR 0/31 and user/supervisor mode. It does not establish physical SFC/DFC spaces, +cache coherency, bus ordering, trace/fault restart, timing or exhaustive +indexed-value combinations. The existing 040 privilege-before-extension bus +regression is retained; no regression is retired here. Required broader +restoration and reference gaps remain, and milestone 6 stays in progress. + +The full Release CPU suite in `artifacts/m6-moves-full/` passes 4,995 tests, +eleven optional skips and zero failures, with all six qualified WinUAE presets +enabled. Every preset reports zero mismatching, unsupported or untested selected +directories with the same CPU/adapter identities. CPU assembly SHA-256 is +`bf8abb3a4603d55e19ad38ef64293569b2f6cea8a9ec251ccfe08e70f42f6a35`; +adapter SHA-256 is +`5d7d21a851072f49812bdc27164024e2b50ac060a1add15d96424b4751a80bf3`. +The strict gate in `artifacts/m6-moves-gate.log` validates 16,035,670 passing +logical cases in 595 reporting batches, with `roadmapComplete=false`. Fresh +SingleStepTests passes 312,500 cases in 125 files; Musashi passes 536 programs, +with 88 explicit exclusions. Their new evidence retains the pinned inputs and +does not broaden the documented software-reference qualification. +Production CPU source remains unchanged from `cb9679d`; prior unpublished `.57` +consumer evidence retains its original package/assembly identities, without a +new consumer replay or package publication claim. Unrelated CopperScreen working +changes and its NuGet boundary remain preserved. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index f4ef225a..78b1dbcb 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -122,7 +122,20 @@ identities, replacement proofs and remaining required coverage here as work progresses. No milestone is complete merely because an inventory exists or a partial matrix passes. -Latest milestone-6 checkpoint, 2026-10-05: independent LPSTOP exception +Latest milestone-6 reference checkpoint, 2026-10-05: legal sampled MOVES B/W/L +passes 110,492 independent callbacks, 86,072 privilege frames and 59,920 +recorded architectural combinations across seven profiles. The retained Basic +010 MOVES.L failure uses an undefined same-An store and noncanonical extension; +no CPU change is required. A copied generator selects legal inputs before +execution; exact identities/selections/counts and corruption controls are required. +The full CPU suite passes 4,995 tests, with eleven optional skips and all six +qualified WinUAE presets enabled. See the +[MOVES qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#moves-legal-input-reference-qualification-2026-10-05). +The strict gate remains passing at 16,035,670 logical cases in 595 reporting +batches, with fresh pinned SingleStepTests and Musashi audits and the complete +required-gap inventory retained. + +Preceding milestone-6 checkpoint, 2026-10-05: independent LPSTOP exception qualification passes 245,760 pinned WinUAE callbacks/frames in fourteen architectural combinations. The new synthetic matrix includes all former SR values and passes 267,262 cases; shared mutation proof permits removal of the diff --git a/scripts/prepare-copper68k-winuae.ps1 b/scripts/prepare-copper68k-winuae.ps1 index 5f40c33c..833fa5ff 100644 --- a/scripts/prepare-copper68k-winuae.ps1 +++ b/scripts/prepare-copper68k-winuae.ps1 @@ -4,7 +4,7 @@ param( [Parameter(Mandatory)] [string] $GeneratorSource, [Parameter(Mandatory)] [string] $RunnerSource, [Parameter(Mandatory)] [string] $VcVars64, - [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop')] [string] $Preset = 'Basic', + [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop','Moves')] [string] $Preset = 'Basic', [string] $OutputDirectory = 'artifacts/winuae-model-inputs' ) $ErrorActionPreference = 'Stop' @@ -86,19 +86,21 @@ try { [IO.File]::WriteAllText($testerSource, (Patch-Once ([IO.File]::ReadAllText((Join-Path $generator 'cputest.cpp'))) $before $after)) [IO.File]::WriteAllText((Join-Path $output 'trace-priority.patch'), ([IO.File]::ReadAllText($patchPath)).Replace("`r`n", "`n")) } - if ($Preset -eq 'LongArithmetic') { + if ($Preset -in @('LongArithmetic','Moves')) { # M68000PM 4-94/98/136/140: reserved extension fields are zero; - # Dh == Dl with a 64-bit multiply has undefined results. Select + # Dh == Dl with a 64-bit multiply has undefined results. MOVES + # 6-24/25/26 similarly fixes reserved fields and excludes undefined + # same-An postincrement/predecrement store values. Select # legal inputs before reference execution, never in the CPU bridge. - $patchName = 'long-arithmetic-encodings.patch' + $patchName = $(if ($Preset -eq 'Moves') { 'moves-encodings.patch' } else { 'long-arithmetic-encodings.patch' }) $patch = [IO.File]::ReadAllText((Join-Path $PSScriptRoot ('winuae/' + $patchName))).Replace("`r`n", "`n") $source = [IO.File]::ReadAllText((Join-Path $generator 'cputest.cpp')).Replace("`r`n", "`n") $hunks = @($patch -split "(?m)^@@`n" | Select-Object -Skip 1) - if ($hunks.Count -ne 1) { throw 'Incorrect LongArithmetic patch hunk count' } + if ($hunks.Count -ne 1) { throw "Incorrect $Preset input patch hunk count" } $lines = $hunks[0].TrimEnd("`n").Split("`n") $before = (($lines | Where-Object { $_.StartsWith('-') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" $after = (($lines | Where-Object { $_.StartsWith('+') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" - $testerSource = Join-Path $output 'cputest-long-arithmetic.cpp' + $testerSource = Join-Path $output $(if ($Preset -eq 'Moves') { 'cputest-moves.cpp' } else { 'cputest-long-arithmetic.cpp' }) [IO.File]::WriteAllText($testerSource, (Patch-Once $source $before $after)) [IO.File]::WriteAllText((Join-Path $output $patchName), $patch) } @@ -166,6 +168,9 @@ void M68KTester_destroy(M68KTesterContext* context) { # alone does not request the generator's automatic supervisor round. $baseIni = $baseIni.Replace('[test=Basic]', '[test=LowPowerStop]').Replace('mode=all', 'mode=LPSTOP').Replace('feature_sr_mask=0x0000', 'feature_sr_mask=0x2000') } + if ($Preset -eq 'Moves') { + $baseIni = $baseIni.Replace('[test=Basic]', '[test=Moves]').Replace('mode=all', 'mode=MOVES.B,MOVES.W,MOVES.L').Replace('feature_sr_mask=0x0000', 'feature_sr_mask=0x2000') + } foreach ($model in @( @{id='68000'; cpu='68000'; width=24}, @{id='68010'; cpu='68010'; width=24}, @{id='68EC020'; cpu='68020'; width=24}, @{id='68020'; cpu='68020'; width=32}, @@ -176,6 +181,7 @@ void M68KTester_destroy(M68KTesterContext* context) { if ($Preset -eq 'Breakpoints' -and $model.id -eq '68000') { continue } if ($Preset -eq 'LongArithmetic' -and $model.id -in @('68000','68010')) { continue } if ($Preset -eq 'LowPowerStop' -and $model.id -ne '68060') { continue } + if ($Preset -eq 'Moves' -and $model.id -eq '68000') { continue } $profileRoot = Join-Path $output $model.id New-Item -ItemType Directory -Path $profileRoot | Out-Null # Every invocation uses a fresh generator output path; stale data cannot fill a gap. @@ -220,6 +226,8 @@ void M68KTester_destroy(M68KTesterContext* context) { WordDivisionPatchSha256=$(if ($Preset -eq 'WordDivision') {(Get-FileHash -LiteralPath (Join-Path $output 'word-division-carry.patch')).Hash.ToLowerInvariant()} else {$null}) LowPowerStopSourceSha256=$(if ($Preset -eq 'LowPowerStop') {(Get-FileHash -LiteralPath (Join-Path $output 'gencpu-lpstop.cpp')).Hash.ToLowerInvariant()} else {$null}) LowPowerStopPatchSha256=$(if ($Preset -eq 'LowPowerStop') {(Get-FileHash -LiteralPath (Join-Path $output 'lpstop-fetch-pc.patch')).Hash.ToLowerInvariant()} else {$null}) + MovesSourceSha256=$(if ($Preset -eq 'Moves') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-moves.cpp')).Hash.ToLowerInvariant()} else {$null}) + MovesPatchSha256=$(if ($Preset -eq 'Moves') {(Get-FileHash -LiteralPath (Join-Path $output 'moves-encodings.patch')).Hash.ToLowerInvariant()} else {$null}) NativeLibrarySha256=(Get-FileHash -LiteralPath (Join-Path $output 'm68k_cpu_tester.dll') -Algorithm SHA256).Hash.ToLowerInvariant() Compiler=$compiler; GeneratorExecutableSha256=(Get-FileHash -LiteralPath (Join-Path $generator 'cputester.exe') -Algorithm SHA256).Hash.ToLowerInvariant() NativeSourceSha256=(Get-FileHash -LiteralPath (Join-Path $output 'winuae-native.c') -Algorithm SHA256).Hash.ToLowerInvariant() diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 3bc79ebc..c1b7688b 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -243,7 +243,7 @@ try { Set-AuditEnvironment 'COPPER68K_WINUAE_CPUTEST_AUDIT' '1' Set-AuditEnvironment 'COPPER68K_WINUAE_CPUTEST_AUDIT_OUTPUT' (Join-Path $output 'winuae-audit.tsv') Invoke-Tests 'FullyQualifiedName~WinUaeM68000CpuTesterPassesInterpreterWhenEnabled' 'winuae-68000' - $references += 'WinUAE: available adapter is 68000 integer; generator supports other models, adapters/fixtures still need qualification' + $references += 'WinUAE: this command option audits the legacy 68000 integer adapter; separately pinned multi-model qualified presets use test-copper68k-winuae-qualified-exceptions.ps1 and test-copper68k-winuae-long-arithmetic.ps1' } $inventory = Get-Content -LiteralPath (Join-Path $output 'integer-inventory.json') -Raw | ConvertFrom-Json foreach ($row in $inventory.combinations) { diff --git a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 index 12b456aa..e7bf65ff 100644 --- a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 +++ b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 @@ -1,7 +1,7 @@ #requires -Version 7.0 [CmdletBinding()] param( - [ValidateSet('TrapBounds','Breakpoints','WordDivision','LowPowerStop')] [string] $Preset = 'TrapBounds', + [ValidateSet('TrapBounds','Breakpoints','WordDivision','LowPowerStop','Moves')] [string] $Preset = 'TrapBounds', [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $InputDirectory, [string] $NativeLibrary, [string] $OutputDirectory = 'artifacts/winuae-trap-bounds-audit' @@ -14,9 +14,9 @@ $library = (Resolve-Path -LiteralPath $NativeLibrary).Path $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } New-Item -ItemType Directory -Path $output | Out-Null -$key = switch ($Preset) {'TrapBounds' {'TRAP_BOUNDS'} 'Breakpoints' {'BREAKPOINT'} 'WordDivision' {'WORD_DIVISION'} 'LowPowerStop' {'LPSTOP'}} -$testName = switch ($Preset) {'TrapBounds' {'WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled'} 'Breakpoints' {'WinUaeBreakpointExceptionsAcrossSelectedModelsWhenEnabled'} 'WordDivision' {'WinUaeWordDivisionAcrossSelectedModelsWhenEnabled'} 'LowPowerStop' {'WinUaeLowPowerStopExceptionsWhenEnabled'}} -$reportName = switch ($Preset) {'TrapBounds' {'winuae-trap-bounds-audit.json'} 'Breakpoints' {'winuae-breakpoint-audit.json'} 'WordDivision' {'winuae-word-division-audit.json'} 'LowPowerStop' {'winuae-lpstop-audit.json'}} +$key = switch ($Preset) {'TrapBounds' {'TRAP_BOUNDS'} 'Breakpoints' {'BREAKPOINT'} 'WordDivision' {'WORD_DIVISION'} 'LowPowerStop' {'LPSTOP'} 'Moves' {'MOVES'}} +$testName = switch ($Preset) {'TrapBounds' {'WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled'} 'Breakpoints' {'WinUaeBreakpointExceptionsAcrossSelectedModelsWhenEnabled'} 'WordDivision' {'WinUaeWordDivisionAcrossSelectedModelsWhenEnabled'} 'LowPowerStop' {'WinUaeLowPowerStopExceptionsWhenEnabled'} 'Moves' {'WinUaeMovesAcrossSelectedModelsWhenEnabled'}} +$reportName = switch ($Preset) {'TrapBounds' {'winuae-trap-bounds-audit.json'} 'Breakpoints' {'winuae-breakpoint-audit.json'} 'WordDivision' {'winuae-word-division-audit.json'} 'LowPowerStop' {'winuae-lpstop-audit.json'} 'Moves' {'winuae-moves-audit.json'}} $filter = "FullyQualifiedName~$testName" $expectedTests = 1 if ($Preset -eq 'WordDivision') { @@ -27,6 +27,10 @@ if ($Preset -eq 'LowPowerStop') { $filter += '|FullyQualifiedName~M68kWinUaeLowPowerStopEncodingTests' $expectedTests = 11 } +if ($Preset -eq 'Moves') { + $filter += '|FullyQualifiedName~M68kWinUaeMovesEncodingTests' + $expectedTests = 17 +} $settings = @{ ('COPPER68K_RUN_WINUAE_' + $key + '_AUDIT') = '1' ('COPPER68K_WINUAE_' + $key + '_PATH') = $root diff --git a/scripts/winuae/moves-encodings.patch b/scripts/winuae/moves-encodings.patch new file mode 100644 index 00000000..8fffa8c2 --- /dev/null +++ b/scripts/winuae/moves-encodings.patch @@ -0,0 +1,23 @@ +--- a/gencpu/cputest.cpp ++++ b/gencpu/cputest.cpp +@@ + if (dp->mnemo == i_MOVES) { +- // MOVES is stupid +- if (!(get_word_test(pc - 2) & 0x0800)) { ++ // M68000PM 6-24/25: reserved extension fields are zero. ++ uae_u16 extra = get_word_test(opcode_memory_start + 2) & 0xf800; ++ put_word_test(opcode_memory_start + 2, extra); ++ // 6-26: same-An postincrement/predecrement stores are undefined. ++ // Exclude candidates before reference execution; retain random ++ // consumption and never normalize an observed CPU result. ++ if ((extra & 0x8800) == 0x8800 && ++ (dp->dmode == Aipi || dp->dmode == Apdi) && ++ ((extra >> 12) & 7) == dp->dreg) { ++ memcpy(opcode_memory, oldcodebytes, sizeof(oldcodebytes)); ++ continue; ++ } ++ // Read the fixed extension word, not the final EA extension. ++ if (!(extra & 0x0800)) { + uae_u32 vv = dstea; + dstea = srcea; + srcea = vv; From 76c7a6580a6513ae0920cfaa489b176e14749f3c Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 18:13:57 +0300 Subject: [PATCH 038/211] Qualify legal CAS inputs and 68060 exception PCs --- Copper68k.Tests/M68kWinUaeCasAuditTests.cs | 86 ++++++++++++ .../M68kWinUaeCpuTesterConformanceTests.cs | 12 +- .../M68kWinUaeTrapBoundsAuditTests.cs | 41 +++++- Copper68k.Tests/Synthetic/README.md | 23 ++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 126 ++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 20 ++- scripts/prepare-copper68k-winuae.ps1 | 27 ++-- ...-copper68k-winuae-qualified-exceptions.ps1 | 12 +- scripts/winuae/cas-encodings.patch | 10 ++ scripts/winuae/cas-unimplemented-pc.patch | 10 ++ 10 files changed, 345 insertions(+), 22 deletions(-) create mode 100644 Copper68k.Tests/M68kWinUaeCasAuditTests.cs create mode 100644 scripts/winuae/cas-encodings.patch create mode 100644 scripts/winuae/cas-unimplemented-pc.patch diff --git a/Copper68k.Tests/M68kWinUaeCasAuditTests.cs b/Copper68k.Tests/M68kWinUaeCasAuditTests.cs new file mode 100644 index 00000000..cbda071a --- /dev/null +++ b/Copper68k.Tests/M68kWinUaeCasAuditTests.cs @@ -0,0 +1,86 @@ +using Copper68k.Tests.Synthetic; +using Xunit.Sdk; + +namespace Copper68k.Tests; + +public sealed partial class M68kWinUaeCpuTesterConformanceTests +{ + [EnvironmentFact("COPPER68K_RUN_WINUAE_CAS_AUDIT", "audit qualified WinUAE CAS encodings and 060 unimplemented-integer frames")] + public void WinUaeCasAcrossAdvancedModelsWhenEnabled() => RunQualifiedExceptionPreset(new( + "Cas", "CAS", "gencpu-cas.cpp", "cas-unimplemented-pc.patch", "Cas", + "40e65f21b503437993d1a704c7552b8b6f8ab17493c109583c13e00c93d77b10", "a27200bdbac2441b1b63651f02590d2894eadc3257df54097fd8a50a5c0d8646", + "winuae-cas-audit.json", ["68EC020", "68020", "68030", "68040", "68060"], + ["68EC020", "68020", "68030", "68040", "68060", "A1200"], + _ => ["CAS.B", "CAS.W", "CAS.L"], (model, family) => { var counts = CasCounts(model, family); return (counts.Cases, counts.Frames); }, + "M68000PM 4-66/67: B/W/L memory-alterable operands, reserved extension fields zero, compare/update semantics and preserved X. MC68060UM C.2.2 and figure 8-3: misaligned W/L operands take vector 61 with the causing instruction PC. A copied generator clears reserved fields before execution and corrects its advanced exception PC; the original Basic corpus and failures remain. EC020/A1200/020/030/040/060, CCR 0/31 and user/supervisor, one seeded round and full extensions enabled. 000/010 absence is covered synthetically. CAS2, incoming trace, operand faults/restart, external lock cycles, physical bus ordering, cache and timing remain unqualified. The software generator can read an operand before checking alignment; this preset compares architectural state and frames, not bus activity or silicon.", + FormCounts: (model, family) => CasCounts(model, family).Forms, ClassifyWords: QualifyCasEncoding, + InputIdentity: new("cputest-cas.cpp", "cas-encodings.patch", + "6b24d70455aa39ed6894ad2a2253d60bf4b8b487be4e2dada194a0589eb53b5b", "cb45000b18f7d4a11dcb0fec7130202c7918ac7fd42c9940effa248b1991fba2"), + AllowFrameFreeFamilies: true, SavedPcControls: true)); + + private static (int Cases, uint Frames, int Forms) CasCounts(ModelSpec model, string family) => + (FixtureId(model.Id), family) switch + { + ("68EC020", "CAS.B") => (3358, 0, 2620), + ("68EC020", "CAS.W") => (2924, 0, 2340), + ("68EC020", "CAS.L") => (3476, 0, 2736), + ("68020" or "68030" or "68040" or "68060", "CAS.B") => (2348, 0, 1826), + ("68020" or "68030" or "68040", "CAS.W") => (2442, 0, 1904), + ("68020" or "68030" or "68040", "CAS.L") => (2652, 0, 2034), + ("68060", "CAS.W") => (2442, 894, 1904), + ("68060", "CAS.L") => (2652, 1572, 2034), + _ => throw new XunitException($"Unqualified CAS family: {model.Id}/{family}") + }; + + // Classify immutable input with fixed manual fields, not production EA/decoder helpers. + internal static string QualifyCasEncoding(ModelSpec model, ushort opcode, ushort extension, ushort eaExtension, ushort inputSr, string family) + { + var size = (opcode >> 9) & 3; + var mode = (opcode >> 3) & 7; + var reg = opcode & 7; + var expected = size switch { 1 => "CAS.B", 2 => "CAS.W", 3 => "CAS.L", _ => "invalid" }; + if (!model.FullIndex || (opcode & 0xf9c0) != 0x08c0 || family != expected || mode < 2 || + (mode == 7 && reg > 1) || (extension & 0xfe38) != 0 || inputSr is not (0 or 0x001f or 0x2000 or 0x201f)) + throw new XunitException($"Unqualified CAS encoding/profile: {model.Id}/{family}/{opcode:X4}/{extension:X4}/{inputSr:X4}"); + var index = "none"; + if (mode == 6) + { + index = "brief"; + if ((eaExtension & 0x0100) != 0) + { + var displacement = (eaExtension >> 4) & 3; + var indirect = eaExtension & 7; + if ((eaExtension & 8) != 0 || displacement == 0 || indirect == 4) + throw new XunitException($"Unqualified CAS full extension: {eaExtension:X4}"); + index = $"full/BS={(eaExtension >> 7) & 1}/IS={(eaExtension >> 6) & 1}/BD={displacement}/IIS={indirect}"; + } + } + return $"{family}/ea={mode}:{reg}/Dc={extension & 7}/Du={(extension >> 6) & 7}/index={index}/sr={inputSr:X4}"; + } +} + +public sealed class M68kWinUaeCasEncodingTests +{ + [Theory] + [InlineData("68020", 0x0ad0, 0x01c7, 0, 0x2000, "CAS.B", "CAS.B/ea=2:0/Dc=7/Du=7/index=none/sr=2000")] + [InlineData("68060", 0x0ce3, 0x0043, 0, 0, "CAS.W", "CAS.W/ea=4:3/Dc=3/Du=1/index=none/sr=0000")] + [InlineData("68060", 0x0ee3, 0x00c1, 0, 0x001f, "CAS.L", "CAS.L/ea=4:3/Dc=1/Du=3/index=none/sr=001F")] + [InlineData("A1200", 0x0cf2, 0x0102, 0x7912, 0x201f, "CAS.W", "CAS.W/ea=6:2/Dc=2/Du=4/index=full/BS=0/IS=0/BD=1/IIS=2/sr=201F")] + public void FixedManualExamplesDistinguishSizeRegistersAddressingAndIndex(string modelId, int opcode, int extension, int eaExtension, int sr, string family, string expected) => + Assert.Equal(expected, M68kWinUaeCpuTesterConformanceTests.QualifyCasEncoding(ModelSpec.All.Single(x => x.Id == modelId), (ushort)opcode, (ushort)extension, (ushort)eaExtension, (ushort)sr, family)); + + [Theory] + [InlineData("68000", 0x0cd0, 0x0040, 0, 0, "CAS.W")] + [InlineData("68010", 0x0cd0, 0x0040, 0, 0, "CAS.W")] + [InlineData("68020", 0x0ac0, 0, 0, 0, "CAS.B")] + [InlineData("68020", 0x0cfa, 0, 0, 0, "CAS.W")] + [InlineData("68020", 0x0efc, 0, 0, 0, "CAS.L")] + [InlineData("68020", 0x0cd0, 0x0200, 0, 0, "CAS.W")] + [InlineData("68020", 0x0cd0, 8, 0, 0, "CAS.W")] + [InlineData("68020", 0x0cd0, 0, 0, 0, "CAS.L")] + [InlineData("68020", 0x0cd0, 0, 0, 0xa000, "CAS.W")] + [InlineData("68020", 0x0cf0, 0, 0x7900, 0, "CAS.W")] + [InlineData("68020", 0x0cf0, 0, 0x7914, 0, "CAS.W")] + public void ReservedUnavailableAndForeignFormsAreRejected(string modelId, int opcode, int extension, int eaExtension, int sr, string family) => + Assert.Throws(() => M68kWinUaeCpuTesterConformanceTests.QualifyCasEncoding(ModelSpec.All.Single(x => x.Id == modelId), (ushort)opcode, (ushort)extension, (ushort)eaExtension, (ushort)sr, family)); +} diff --git a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs index e0068213..18d0c762 100644 --- a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs +++ b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs @@ -308,6 +308,7 @@ private sealed class NativeTester : IDisposable private ModelSpec? _integerProfile; private bool _corruptResult; private bool _corruptFrame; + private bool _corruptSavedPc; private ushort _corruptSr; private bool _corruptIgnoredSr; private string _integerFamily = ""; @@ -362,7 +363,8 @@ public RunResult Run( bool corruptIgnoredSr = false, bool qualifyLongArithmetic = false, Func? fixtureClassifier = null, - Func? fixtureWordsClassifier = null) + Func? fixtureWordsClassifier = null, + bool corruptSavedPc = false) { _callbackException = null; _executedCases = 0; @@ -375,6 +377,7 @@ public RunResult Run( _integerProfile = integerProfile; _corruptResult = corruptResult; _corruptFrame = corruptFrame; + _corruptSavedPc = corruptSavedPc; _corruptSr = corruptSr; _corruptIgnoredSr = corruptIgnoredSr; _integerFamily = opcode; @@ -608,6 +611,13 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt var address = registers.ExcFrame + (_cpuLevel == 0 ? 4u : 6u); bus.WriteHostWord(address, (ushort)(bus.ReadHostWord(address) ^ 1)); } + if (_corruptSavedPc && cpu.State.LastExceptionVector >= 0) + { + // Recreate the advanced-PC reference defect without modifying + // the expected fixture or production instruction execution. + var address = registers.ExcFrame + 4; + bus.WriteHostWord(address, unchecked((ushort)(bus.ReadHostWord(address) + 4))); + } Marshal.StructureToPtr(registers, registersPtr, fDeleteOld: false); _unmappedReads += bus.UnmappedReads; _unmappedWrites += bus.UnmappedWrites; diff --git a/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs b/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs index 7208e610..176a5d60 100644 --- a/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs +++ b/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs @@ -40,6 +40,17 @@ private void RunQualifiedExceptionPreset(QualifiedExceptionPreset preset) using var identity = JsonDocument.Parse(json); var fields = identity.RootElement; var sourcePath = Path.Combine(root, preset.SourceName); + if (preset.InputIdentity is { } input) + { + var inputPath = Path.Combine(root, input.SourceName); + var normalizedInputHash = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData( + System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(inputPath).Replace("\r\n", "\n")))).ToLowerInvariant(); + if (normalizedInputHash != input.SourceHash || + Hash(inputPath) != fields.GetProperty(preset.ManifestPrefix + "InputSourceSha256").GetString() || + fields.GetProperty(preset.ManifestPrefix + "InputPatchSha256").GetString() != input.PatchHash || + Hash(Path.Combine(root, input.PatchName)) != input.PatchHash) + throw new XunitException($"{preset.Name} input source or patch identity is unqualified."); + } var normalizedSourceHash = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData( System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(sourcePath).Replace("\r\n", "\n")))).ToLowerInvariant(); if (manifest.Schema != 1 || manifest.GeneratorCommit != GeneratorPin || manifest.RunnerCommit != RunnerPin || @@ -64,23 +75,34 @@ private void RunQualifiedExceptionPreset(QualifiedExceptionPreset preset) var path = Path.Combine(root, fixture.Id); foreach (var family in preset.Families(model)) { + var expected = preset.Counts(model, family); + // A frame corruption control applies only to a corpus containing + // exception frames. Zero-frame selections still require exact + // zero-frame counts and independent register/SR controls. + var frameRequired = !preset.AllowFrameFreeFamilies || expected.Frames != 0; var register = tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptResult: true); var sr = tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptSr: 0x10); - var frame = tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptFrame: true); + var frame = frameRequired ? tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptFrame: true) : default; + var frameDetected = !frameRequired || (!frame.Passed && tester.FrameChecks > 0 && + frame.Detail.Contains("frame byte", StringComparison.Ordinal)); + var pcRequired = preset.SavedPcControls && frameRequired; + var pc = pcRequired ? tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptSavedPc: true) : default; + var pcDetected = !pcRequired || (!pc.Passed && pc.ExecutedCases > 0 && tester.FrameChecks > 0 && + pc.Detail.Contains("frame byte 5", StringComparison.Ordinal)); var detected = !register.Passed && register.ExecutedCases > 0 && !sr.Passed && sr.ExecutedCases > 0 && - sr.Detail.Contains("SR:", StringComparison.Ordinal) && !frame.Passed && tester.FrameChecks > 0 && - frame.Detail.Contains("frame byte", StringComparison.Ordinal); + sr.Detail.Contains("SR:", StringComparison.Ordinal) && frameDetected && pcDetected; var carry = preset.ArithmeticFlagControls ? tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptSr: 1) : default; var carryDetected = !preset.ArithmeticFlagControls || (!carry.Passed && carry.ExecutedCases > 0 && carry.Detail.Contains("SR:", StringComparison.Ordinal)); var ignored = preset.ArithmeticFlagControls ? tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptIgnoredSr: true) : default; var ignoredAccepted = !preset.ArithmeticFlagControls || (ignored.Passed && ignored.ExecutedCases > 0 && tester.MaskedCases > 0); detected &= carryDetected && ignoredAccepted; probes.Add(new(model.Id, family, detected, register.ExecutedCases, sr.ExecutedCases, frame.ExecutedCases, - preset.ArithmeticFlagControls, carryDetected, carry.ExecutedCases, ignoredAccepted, ignored.ExecutedCases)); + preset.ArithmeticFlagControls, carryDetected, carry.ExecutedCases, ignoredAccepted, ignored.ExecutedCases, + frameRequired, frameDetected, pcRequired, pcDetected, pc.ExecutedCases, + pcRequired ? pc.Detail : null)); var result = tester.Run(path, family, fixture.CpuLevel, false, false, model, fixtureClassifier: preset.ClassifyForm is null ? null : (opcode, inputSr) => preset.ClassifyForm(opcode, inputSr, family), fixtureWordsClassifier: preset.ClassifyWords is null ? null : (opcode, extension, followingWord, inputSr) => preset.ClassifyWords(model, opcode, extension, followingWord, inputSr, family)); - var expected = preset.Counts(model, family); var forms = new SortedDictionary(tester.FixtureForms.ToDictionary(x => x.Key, x => x.Value), StringComparer.Ordinal); var expectedForms = preset.FormCounts?.Invoke(model, family) ?? 0; var formDistributionMatches = preset.ExpectedForms is null || forms.SequenceEqual(preset.ExpectedForms(model, family).OrderBy(x => x.Key, StringComparer.Ordinal)); @@ -101,6 +123,7 @@ private void RunQualifiedExceptionPreset(QualifiedExceptionPreset preset) manifest.GeneratorCommit, manifest.RunnerCommit, manifest.NativeLibrarySha256, manifestSha256 = Hash(manifestFile), generatorSourceSha256 = Hash(sourcePath), generatorNormalizedSourceSha256 = preset.SourceHash, generatorPatchSha256 = preset.PatchHash, + inputIdentity = preset.InputIdentity, adapterAssemblySha256 = Hash(typeof(M68kWinUaeCpuTesterConformanceTests).Assembly.Location), cpuAssemblySha256 = Hash(typeof(Copper68k.M68kCoreFactory).Assembly.Location), qualification = preset.Qualification, @@ -135,10 +158,14 @@ private sealed record QualifiedExceptionPreset(string Name, string EnvironmentKe bool ArithmeticFlagControls = false, Func? MaskedCounts = null, Func? ClassifyForm = null, Func? FormCounts = null, Func? ClassifyWords = null, - Func>? ExpectedForms = null); + Func>? ExpectedForms = null, + QualifiedInputIdentity? InputIdentity = null, bool AllowFrameFreeFamilies = false, bool SavedPcControls = false); + private sealed record QualifiedInputIdentity(string SourceName, string PatchName, string SourceHash, string PatchHash); private sealed record WinUaeQualifiedExceptionRow(string Model, string Family, string Status, int ExecutedCases, uint ExceptionFrames, uint MaskedSrCases, bool Controls, SortedDictionary Forms, string Detail); private sealed record WinUaeQualifiedExceptionProbe(string Model, string Family, bool Detected, int RegisterCases, int SrCases, int FrameCases, bool ArithmeticFlagControls, - bool CarryDetected, int CarryCases, bool IgnoredAccepted, int IgnoredCases); + bool CarryDetected, int CarryCases, bool IgnoredAccepted, int IgnoredCases, + bool FrameControlRequired, bool FrameDetected, bool SavedPcControlRequired, + bool SavedPcDetected, int SavedPcCases, string? SavedPcDetail); } diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 18348307..aac5c90e 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -311,3 +311,26 @@ The 68000 unavailable outcome remains in synthetic coverage. The existing MOVES privilege-before-extension bus-ordering regression is retained. The [MOVES qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#moves-legal-input-reference-qualification-2026-10-05) documents identities, complete selections, failed discovery and limitations. + +Legal CAS B/W/L reference inputs use the `Cas` preset on EC020/A1200/020/030/ +040/060. A copied input generator clears reserved extension fields; a separate +copied CPU generator fixes the 060 misalignment vector-61 saved PC to the +causing instruction, as MC68060UM C.2.2 requires. The original Basic saved-PC +disagreement is retained. All eighteen selected directories pass 49,284 +callbacks, 2,466 exception frames and 38,448 recorded architectural forms. +The report records size, EA register/mode, compare/update register, incoming +S/CCR and indexed structure. Fixed encodings, exact counts and complete +selections are required. Register/SR controls apply to every directory; frame +and saved-PC corruption controls apply to the two 060 W/L directories with +exceptions. Zero-frame directories must report exactly zero frames; their +inapplicable frame controls are labeled explicitly. +Prepare with `./scripts/prepare-copper68k-winuae.ps1 -Preset Cas` and the pinned +source/compiler arguments; run +`./scripts/test-copper68k-winuae-qualified-exceptions.ps1 -Preset Cas +-InputDirectory -OutputDirectory `. +Both source/patch identities and all fixture hashes are checked. This is a +seeded software-reference qualification with CCR 0/31 and both privilege states, +not physical lock/bus, cache, trace/fault restart, timing or exhaustive indexed- +value qualification. CAS2 remains separate; 000/010 absence remains synthetic. +The [CAS qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#cas-legal-input-and-unimplemented-frame-reference-qualification-2026-10-05) +records the original disagreement, controls and remaining scope. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 78f264dd..3fcc7220 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -3336,3 +3336,129 @@ Production CPU source remains unchanged from `cb9679d`; prior unpublished `.57` consumer evidence retains its original package/assembly identities, without a new consumer replay or package publication claim. Unrelated CopperScreen working changes and its NuGet boundary remain preserved. + +### CAS legal-input and unimplemented-frame reference qualification — 2026-10-05 + +The retained Basic audit stops at callback 273 for both 060 CAS.W and CAS.L. +Its first misaligned predecrement operand is `-(A3)` with initial A3 `00007FFF`: +opcodes `0CE3` / `0EE3`, extensions `0043` / `00C1`, initial SR `0000`, +instruction PC `0087FFA0`. The CPU takes vector 61 and saves `0087FFA0`; +the reference expects `0087FFA4`. [MC68060UM C.2.2 and figure +8-3](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf) require a format-0 +frame pointing to the unimplemented instruction. Section 7.7.6 identifies +misaligned CAS as an unimplemented-integer case. Copper68k already has the +documented PC; no production change is made. The pinned generator's CAS path +calls `sync_m68k_pc_noreset()` before its exception helper, which stacks that +advanced PC. `cas-unimplemented-pc.patch` changes only this path in a copied +generator to restore `regs.instruction_pc` before vector 61. The original Basic +corpus, generator and failed evidence remain unchanged. + +The separate `Cas` preset selects B/W/L on EC020/020/030/040/060, with A1200 +reusing EC020 inputs. [M68000PM 4-66/67](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) +defines memory-alterable operands, fixed-zero reserved extension fields and +compare/update registers. `cas-encodings.patch` clears all fields except Du/Dc +in the copied input generator before reference execution. The bridge rejects +noncanonical inputs; it never normalizes an input, CPU result or expected frame. +The immutable-word classifier records size, EA mode/register, Dc/Du, initial +S/CCR and brief/full indexed structure. Reserved full-format fields, unsupported +profiles, foreign families and incoming SR values outside `0000`, `001F`, +`2000`, `201F` are rejected. + +The exact passing selections are: + +| Profile(s), each | Family | Callbacks | Exception frames | Recorded forms | +| --- | --- | ---: | ---: | ---: | +| EC020 / A1200 | B | 3,358 | 0 | 2,620 | +| EC020 / A1200 | W | 2,924 | 0 | 2,340 | +| EC020 / A1200 | L | 3,476 | 0 | 2,736 | +| 020 / 030 / 040 / 060 | B | 2,348 | 0 | 1,826 | +| 020 / 030 / 040 | W | 2,442 | 0 | 1,904 | +| 020 / 030 / 040 | L | 2,652 | 0 | 2,034 | +| 060 | W | 2,442 | 894 | 1,904 | +| 060 | L | 2,652 | 1,572 | 2,034 | +| All six profiles | B/W/L | 49,284 | 2,466 | 38,448 | + +All eighteen directories pass with zero mismatching, unsupported or untested +selected directories and zero masked SR cases. Fifteen fixed encoding/profile +tests also pass. Independent register and defined-SR corruptions fail for all +directories. Frame-format/vector-word and saved-PC corruptions fail in the two +060 W/L directories, giving forty applicable comparator controls. Adding four +to the saved PC recreates the original discrepancy; both controls fail after +517 callbacks with frame byte 5 expected `A0`, actual `A4`. Zero-frame directories +require exactly zero frames and explicitly label their frame controls inapplicable; +they are not evidence of exception-frame coverage. The first count-discovery +report intentionally fails against placeholder zero counts and remains in +`artifacts/m6-cas-reference-discovery/`. Subsequent passing evidence with fixed +counts and saved-PC controls is `artifacts/m6-cas-reference-qualified-v2/`. + +Preparation and audit commands: + +```powershell +./scripts/prepare-copper68k-winuae.ps1 -GeneratorSource -RunnerSource -VcVars64 -Preset Cas -OutputDirectory +./scripts/test-copper68k-winuae-qualified-exceptions.ps1 -Preset Cas -InputDirectory -OutputDirectory +``` + +The generator remains pinned to `025b999239800357e95065fe5b9a15ea5b300fa7` +and runner to `7a83745d6c6159bc74ab0471578ffc8bc244e66e`. Required normalized +source/patch SHA-256 identities are: + +| Input | SHA-256 | +| --- | --- | +| Copied CPU generator | `40e65f21b503437993d1a704c7552b8b6f8ab17493c109583c13e00c93d77b10` | +| CPU-generator patch | `a27200bdbac2441b1b63651f02590d2894eadc3257df54097fd8a50a5c0d8646` | +| Copied input generator | `6b24d70455aa39ed6894ad2a2253d60bf4b8b487be4e2dada194a0589eb53b5b` | +| Input-generator patch | `cb45000b18f7d4a11dcb0fec7130202c7918ac7fd42c9940effa248b1991fba2` | + +The manifest in `artifacts/m6-cas-qualified-inputs/` has SHA-256 +`a1cf7a6d6af743c2f01a51a92d4270a149a692aaee3235f2b241a379a9d3bb76`; +generator executable SHA-256 is +`610d0815532612eb6485cd2cab86ab655e5f2180501dae2d9c4a999668790cc8`; +native library SHA-256 is +`0aff0778debe3b56ca5cee818f32f835afa46015eb1efc83641f39711272b7fd`. +Complete profiles/families, data and memory images, input hashes, both copied +sources and both patches are validated before execution. Nine isolated controls +reject empty profiles/families/input selections, missing or changed input data, +changed CPU generator/patch and changed input generator/patch. Each fails for +its intended reason in `artifacts/m6-cas-reference-controls/controls.json`. + +This is one seeded round with CCR 0/31 and user/supervisor states, with full +addressing extensions enabled. It is a corrected software reference, not an +unchanged upstream or silicon oracle. The reference reads its operand before +checking 060 alignment; this audit qualifies architectural state and frames, +not bus accesses or fault sequencing. Physical locks/bus ordering, cache, +incoming trace, operand faults/restart, timing and exhaustive signed/scaled +indexed-value combinations remain unqualified. CAS2 (including the separate +040 alias-order disagreement) is not promoted by this preset. Unavailable +000/010 CAS remains synthetic coverage. No regression is retired, no production +CPU change or package release is made, and all required RTE/restart/reference/ +consolidation gaps remain. Milestone 6 stays in progress. + +The next CAS2 reference follow-up has a distinct manual rule. M68000PM 4-68 +states that if Dc1 and Dc2 name the same register and comparison fails, memory +operand 1 is stored there. The retained 040 CAS2.W witness is `0CFC 8083 E043` +(both compare fields D3), with different operand addresses A0/A6. Copper68k +returns operand 1 (`FFFF0001`); the reference expects operand 2 (`FFFFC700`). +The pinned generator explicitly reverses the compare-register write order on +040. This identifies a separate reference correction to qualify, not a CPU fix +or completed CAS2 audit. CAS2 inputs, applicable frame controls and independent +coverage still need their own validation; none are silently included above. + +The full Release CPU suite in `artifacts/m6-cas-full/` passes 5,011 tests, +eleven optional skips and zero failures, with all seven qualified WinUAE presets +enabled. All seven reports have zero mismatching, unsupported or untested +selected directories and matching CPU/adapter identities. CPU assembly SHA-256 +is `dd9fab1d6aebb4c35ad48e6c8443fc19dd6d52dc0ede9f5d7a49f11013d188c7`; +adapter SHA-256 is +`1a427ea255554770f5891b82be5e29a159ff6984b722c7b0be8e6e29f325fa17`. +Production source remains unchanged since `cb9679d`; this test/reference-only +checkpoint retains the preceding unpublished `.57` consumer evidence with its +original package/assembly identities. It does not claim a fresh consumer replay, +binary equality with `.57`, or package publication. Unrelated CopperScreen +changes and its pinned NuGet boundary remain preserved. + +The strict gate in `artifacts/m6-cas-gate.log` validates 16,035,670 passing +logical cases in 595 reporting batches, with `roadmapComplete=false` and the +complete required-gap inventory retained. Fresh pinned SingleStepTests passes +312,500 cases in 125 files; Musashi passes 536 programs with 88 explicit +exclusions. Their input identities and complete selections are rechecked; this +does not broaden the references' documented qualification or close other gaps. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 78b1dbcb..919916a8 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -122,7 +122,25 @@ identities, replacement proofs and remaining required coverage here as work progresses. No milestone is complete merely because an inventory exists or a partial matrix passes. -Latest milestone-6 reference checkpoint, 2026-10-05: legal sampled MOVES B/W/L +Latest milestone-6 reference checkpoint, 2026-10-05: legal sampled CAS B/W/L +passes 49,284 independent callbacks, 2,466 unimplemented-integer frames and +38,448 recorded architectural combinations across EC020/A1200/020/030/040/060. +The CPU already saves the documented 060 instruction PC; a separate copied +reference generator corrects its advanced PC and selects canonical extensions. +All eighteen directories and fifteen fixed encoding/profile tests pass, with +forty applicable comparator controls (including two saved-PC controls) and nine +input/source rejection controls. Both copied source/patch identities are required. +See the [CAS qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#cas-legal-input-and-unimplemented-frame-reference-qualification-2026-10-05). +No CPU behavior changes, regression retirement or package publication are added. +The original Basic results and all broader restoration/reference/consolidation +requirements remain retained; milestone 6 is still **in progress**. +The full Release CPU suite passes 5,011 tests, with eleven optional skips and +all seven qualified WinUAE presets enabled against the same CPU/adapter binaries. +The strict gate passes 16,035,670 logical cases in 595 reporting batches, with +fresh pinned SingleStepTests (312,500 cases / 125 files) and Musashi (536 programs / +88 exclusions). `roadmapComplete=false` and all required gaps remain explicit. + +Preceding milestone-6 reference checkpoint, 2026-10-05: legal sampled MOVES B/W/L passes 110,492 independent callbacks, 86,072 privilege frames and 59,920 recorded architectural combinations across seven profiles. The retained Basic 010 MOVES.L failure uses an undefined same-An store and noncanonical extension; diff --git a/scripts/prepare-copper68k-winuae.ps1 b/scripts/prepare-copper68k-winuae.ps1 index 833fa5ff..fbd18eb6 100644 --- a/scripts/prepare-copper68k-winuae.ps1 +++ b/scripts/prepare-copper68k-winuae.ps1 @@ -4,7 +4,7 @@ param( [Parameter(Mandatory)] [string] $GeneratorSource, [Parameter(Mandatory)] [string] $RunnerSource, [Parameter(Mandatory)] [string] $VcVars64, - [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop','Moves')] [string] $Preset = 'Basic', + [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop','Moves','Cas')] [string] $Preset = 'Basic', [string] $OutputDirectory = 'artifacts/winuae-model-inputs' ) $ErrorActionPreference = 'Stop' @@ -48,17 +48,17 @@ try { & ./build68k.exe table68k | Set-Content -Encoding utf8 cpudefs.cpp if ($LASTEXITCODE -ne 0) { throw 'Opcode table generation failed' } $cpuGeneratorSource = 'gencpu.cpp' - if ($Preset -in @('TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop')) { + if ($Preset -in @('TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop','Cas')) { # TrapBounds: following PC (MC68020UM 6.1.4 / MC68040UM 8.2.3). # Breakpoints: opcode PC (illegal exception, UM 6.1.5 / 8.2.4). # Patch a copy; Basic keeps the pinned source. - $sourceName = switch ($Preset) {'TrapBounds' {'gencpu-trap-bounds.cpp'} 'Breakpoints' {'gencpu-breakpoints.cpp'} 'LongArithmetic' {'gencpu-long-arithmetic.cpp'} 'WordDivision' {'gencpu-word-division.cpp'} 'LowPowerStop' {'gencpu-lpstop.cpp'}} - $patchName = switch ($Preset) {'TrapBounds' {'trap-bounds-pc.patch'} 'Breakpoints' {'breakpoint-pc.patch'} 'LongArithmetic' {'long-arithmetic-unimplemented.patch'} 'WordDivision' {'word-division-carry.patch'} 'LowPowerStop' {'lpstop-fetch-pc.patch'}} + $sourceName = switch ($Preset) {'TrapBounds' {'gencpu-trap-bounds.cpp'} 'Breakpoints' {'gencpu-breakpoints.cpp'} 'LongArithmetic' {'gencpu-long-arithmetic.cpp'} 'WordDivision' {'gencpu-word-division.cpp'} 'LowPowerStop' {'gencpu-lpstop.cpp'} 'Cas' {'gencpu-cas.cpp'}} + $patchName = switch ($Preset) {'TrapBounds' {'trap-bounds-pc.patch'} 'Breakpoints' {'breakpoint-pc.patch'} 'LongArithmetic' {'long-arithmetic-unimplemented.patch'} 'WordDivision' {'word-division-carry.patch'} 'LowPowerStop' {'lpstop-fetch-pc.patch'} 'Cas' {'cas-unimplemented-pc.patch'}} $patchPath = Join-Path $PSScriptRoot ('winuae/' + $patchName) $patch = [IO.File]::ReadAllText($patchPath).Replace("`r`n", "`n") $source = [IO.File]::ReadAllText((Join-Path $generator 'gencpu.cpp')).Replace("`r`n", "`n") $hunks = @($patch -split "(?m)^@@`n" | Select-Object -Skip 1) - $requiredHunks = switch ($Preset) {'Breakpoints' {1} 'TrapBounds' {2} 'LongArithmetic' {3} 'WordDivision' {1} 'LowPowerStop' {2}} + $requiredHunks = switch ($Preset) {'Breakpoints' {1} 'TrapBounds' {2} 'LongArithmetic' {3} 'WordDivision' {1} 'LowPowerStop' {2} 'Cas' {1}} if ($hunks.Count -ne $requiredHunks) { throw "Incorrect $Preset patch hunk count" } foreach ($hunk in $hunks) { $lines = $hunk.TrimEnd("`n").Split("`n") @@ -86,13 +86,14 @@ try { [IO.File]::WriteAllText($testerSource, (Patch-Once ([IO.File]::ReadAllText((Join-Path $generator 'cputest.cpp'))) $before $after)) [IO.File]::WriteAllText((Join-Path $output 'trace-priority.patch'), ([IO.File]::ReadAllText($patchPath)).Replace("`r`n", "`n")) } - if ($Preset -in @('LongArithmetic','Moves')) { + if ($Preset -in @('LongArithmetic','Moves','Cas')) { # M68000PM 4-94/98/136/140: reserved extension fields are zero; # Dh == Dl with a 64-bit multiply has undefined results. MOVES # 6-24/25/26 similarly fixes reserved fields and excludes undefined - # same-An postincrement/predecrement store values. Select + # same-An postincrement/predecrement store values. CAS 4-67 allows + # only Du/Dc extension fields. Select # legal inputs before reference execution, never in the CPU bridge. - $patchName = $(if ($Preset -eq 'Moves') { 'moves-encodings.patch' } else { 'long-arithmetic-encodings.patch' }) + $patchName = switch ($Preset) {'Moves' {'moves-encodings.patch'} 'Cas' {'cas-encodings.patch'} default {'long-arithmetic-encodings.patch'}} $patch = [IO.File]::ReadAllText((Join-Path $PSScriptRoot ('winuae/' + $patchName))).Replace("`r`n", "`n") $source = [IO.File]::ReadAllText((Join-Path $generator 'cputest.cpp')).Replace("`r`n", "`n") $hunks = @($patch -split "(?m)^@@`n" | Select-Object -Skip 1) @@ -100,7 +101,7 @@ try { $lines = $hunks[0].TrimEnd("`n").Split("`n") $before = (($lines | Where-Object { $_.StartsWith('-') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" $after = (($lines | Where-Object { $_.StartsWith('+') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" - $testerSource = Join-Path $output $(if ($Preset -eq 'Moves') { 'cputest-moves.cpp' } else { 'cputest-long-arithmetic.cpp' }) + $testerSource = Join-Path $output $(switch ($Preset) {'Moves' {'cputest-moves.cpp'} 'Cas' {'cputest-cas.cpp'} default {'cputest-long-arithmetic.cpp'}}) [IO.File]::WriteAllText($testerSource, (Patch-Once $source $before $after)) [IO.File]::WriteAllText((Join-Path $output $patchName), $patch) } @@ -171,6 +172,9 @@ void M68KTester_destroy(M68KTesterContext* context) { if ($Preset -eq 'Moves') { $baseIni = $baseIni.Replace('[test=Basic]', '[test=Moves]').Replace('mode=all', 'mode=MOVES.B,MOVES.W,MOVES.L').Replace('feature_sr_mask=0x0000', 'feature_sr_mask=0x2000') } + if ($Preset -eq 'Cas') { + $baseIni = $baseIni.Replace('[test=Basic]', '[test=Cas]').Replace('mode=all', 'mode=CAS.B,CAS.W,CAS.L').Replace('feature_sr_mask=0x0000', 'feature_sr_mask=0x2000') + } foreach ($model in @( @{id='68000'; cpu='68000'; width=24}, @{id='68010'; cpu='68010'; width=24}, @{id='68EC020'; cpu='68020'; width=24}, @{id='68020'; cpu='68020'; width=32}, @@ -182,6 +186,7 @@ void M68KTester_destroy(M68KTesterContext* context) { if ($Preset -eq 'LongArithmetic' -and $model.id -in @('68000','68010')) { continue } if ($Preset -eq 'LowPowerStop' -and $model.id -ne '68060') { continue } if ($Preset -eq 'Moves' -and $model.id -eq '68000') { continue } + if ($Preset -eq 'Cas' -and $model.id -in @('68000','68010')) { continue } $profileRoot = Join-Path $output $model.id New-Item -ItemType Directory -Path $profileRoot | Out-Null # Every invocation uses a fresh generator output path; stale data cannot fill a gap. @@ -228,6 +233,10 @@ void M68KTester_destroy(M68KTesterContext* context) { LowPowerStopPatchSha256=$(if ($Preset -eq 'LowPowerStop') {(Get-FileHash -LiteralPath (Join-Path $output 'lpstop-fetch-pc.patch')).Hash.ToLowerInvariant()} else {$null}) MovesSourceSha256=$(if ($Preset -eq 'Moves') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-moves.cpp')).Hash.ToLowerInvariant()} else {$null}) MovesPatchSha256=$(if ($Preset -eq 'Moves') {(Get-FileHash -LiteralPath (Join-Path $output 'moves-encodings.patch')).Hash.ToLowerInvariant()} else {$null}) + CasSourceSha256=$(if ($Preset -eq 'Cas') {(Get-FileHash -LiteralPath (Join-Path $output 'gencpu-cas.cpp')).Hash.ToLowerInvariant()} else {$null}) + CasPatchSha256=$(if ($Preset -eq 'Cas') {(Get-FileHash -LiteralPath (Join-Path $output 'cas-unimplemented-pc.patch')).Hash.ToLowerInvariant()} else {$null}) + CasInputSourceSha256=$(if ($Preset -eq 'Cas') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-cas.cpp')).Hash.ToLowerInvariant()} else {$null}) + CasInputPatchSha256=$(if ($Preset -eq 'Cas') {(Get-FileHash -LiteralPath (Join-Path $output 'cas-encodings.patch')).Hash.ToLowerInvariant()} else {$null}) NativeLibrarySha256=(Get-FileHash -LiteralPath (Join-Path $output 'm68k_cpu_tester.dll') -Algorithm SHA256).Hash.ToLowerInvariant() Compiler=$compiler; GeneratorExecutableSha256=(Get-FileHash -LiteralPath (Join-Path $generator 'cputester.exe') -Algorithm SHA256).Hash.ToLowerInvariant() NativeSourceSha256=(Get-FileHash -LiteralPath (Join-Path $output 'winuae-native.c') -Algorithm SHA256).Hash.ToLowerInvariant() diff --git a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 index e7bf65ff..41d9e7cd 100644 --- a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 +++ b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 @@ -1,7 +1,7 @@ #requires -Version 7.0 [CmdletBinding()] param( - [ValidateSet('TrapBounds','Breakpoints','WordDivision','LowPowerStop','Moves')] [string] $Preset = 'TrapBounds', + [ValidateSet('TrapBounds','Breakpoints','WordDivision','LowPowerStop','Moves','Cas')] [string] $Preset = 'TrapBounds', [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $InputDirectory, [string] $NativeLibrary, [string] $OutputDirectory = 'artifacts/winuae-trap-bounds-audit' @@ -14,9 +14,9 @@ $library = (Resolve-Path -LiteralPath $NativeLibrary).Path $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } New-Item -ItemType Directory -Path $output | Out-Null -$key = switch ($Preset) {'TrapBounds' {'TRAP_BOUNDS'} 'Breakpoints' {'BREAKPOINT'} 'WordDivision' {'WORD_DIVISION'} 'LowPowerStop' {'LPSTOP'} 'Moves' {'MOVES'}} -$testName = switch ($Preset) {'TrapBounds' {'WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled'} 'Breakpoints' {'WinUaeBreakpointExceptionsAcrossSelectedModelsWhenEnabled'} 'WordDivision' {'WinUaeWordDivisionAcrossSelectedModelsWhenEnabled'} 'LowPowerStop' {'WinUaeLowPowerStopExceptionsWhenEnabled'} 'Moves' {'WinUaeMovesAcrossSelectedModelsWhenEnabled'}} -$reportName = switch ($Preset) {'TrapBounds' {'winuae-trap-bounds-audit.json'} 'Breakpoints' {'winuae-breakpoint-audit.json'} 'WordDivision' {'winuae-word-division-audit.json'} 'LowPowerStop' {'winuae-lpstop-audit.json'} 'Moves' {'winuae-moves-audit.json'}} +$key = switch ($Preset) {'TrapBounds' {'TRAP_BOUNDS'} 'Breakpoints' {'BREAKPOINT'} 'WordDivision' {'WORD_DIVISION'} 'LowPowerStop' {'LPSTOP'} 'Moves' {'MOVES'} 'Cas' {'CAS'}} +$testName = switch ($Preset) {'TrapBounds' {'WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled'} 'Breakpoints' {'WinUaeBreakpointExceptionsAcrossSelectedModelsWhenEnabled'} 'WordDivision' {'WinUaeWordDivisionAcrossSelectedModelsWhenEnabled'} 'LowPowerStop' {'WinUaeLowPowerStopExceptionsWhenEnabled'} 'Moves' {'WinUaeMovesAcrossSelectedModelsWhenEnabled'} 'Cas' {'WinUaeCasAcrossAdvancedModelsWhenEnabled'}} +$reportName = switch ($Preset) {'TrapBounds' {'winuae-trap-bounds-audit.json'} 'Breakpoints' {'winuae-breakpoint-audit.json'} 'WordDivision' {'winuae-word-division-audit.json'} 'LowPowerStop' {'winuae-lpstop-audit.json'} 'Moves' {'winuae-moves-audit.json'} 'Cas' {'winuae-cas-audit.json'}} $filter = "FullyQualifiedName~$testName" $expectedTests = 1 if ($Preset -eq 'WordDivision') { @@ -31,6 +31,10 @@ if ($Preset -eq 'Moves') { $filter += '|FullyQualifiedName~M68kWinUaeMovesEncodingTests' $expectedTests = 17 } +if ($Preset -eq 'Cas') { + $filter += '|FullyQualifiedName~M68kWinUaeCasEncodingTests' + $expectedTests = 16 +} $settings = @{ ('COPPER68K_RUN_WINUAE_' + $key + '_AUDIT') = '1' ('COPPER68K_WINUAE_' + $key + '_PATH') = $root diff --git a/scripts/winuae/cas-encodings.patch b/scripts/winuae/cas-encodings.patch new file mode 100644 index 00000000..402dbf6e --- /dev/null +++ b/scripts/winuae/cas-encodings.patch @@ -0,0 +1,10 @@ +--- a/gencpu/cputest.cpp ++++ b/gencpu/cputest.cpp +@@ ++ if (dp->mnemo == i_CAS) { ++ // M68000PM 4-67: only Du/Dc fields are nonzero. ++ uae_u16 extra = get_word_test(opcode_memory_start + 2) & 0x01c7; ++ put_word_test(opcode_memory_start + 2, extra); ++ } + if (dp->mnemo == i_MOVES) { + // MOVES is stupid diff --git a/scripts/winuae/cas-unimplemented-pc.patch b/scripts/winuae/cas-unimplemented-pc.patch new file mode 100644 index 00000000..15793f65 --- /dev/null +++ b/scripts/winuae/cas-unimplemented-pc.patch @@ -0,0 +1,10 @@ +--- a/gencpu/gencpu.cpp ++++ b/gencpu/gencpu.cpp +@@ + if (mmufixupcnt) + out("cpu_restore_fixup();\n"); +- sync_m68k_pc_noreset(); ++ // MC68060UM C.2.2 / figure 8-3: vector 61 saves the ++ // unimplemented CAS instruction, not an advanced EA PC. ++ out("m68k_setpc(regs.instruction_pc);\n"); + out("op_unimpl (opcode);\n"); From d71320539e45f652ef4d5067e687a2c668a80623 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 18:41:39 +0300 Subject: [PATCH 039/211] Qualify CAS2 compare aliases and unimplemented frames --- Copper68k.Tests/M68kWinUaeCas2AuditTests.cs | 90 +++++++++++++ .../M68kWinUaeCpuTesterConformanceTests.cs | 45 ++++++- .../M68kWinUaeTrapBoundsAuditTests.cs | 21 ++- Copper68k.Tests/Synthetic/README.md | 19 +++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 122 ++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 21 ++- scripts/prepare-copper68k-winuae.ps1 | 25 ++-- ...-copper68k-winuae-qualified-exceptions.ps1 | 12 +- scripts/winuae/cas2-compare-alias.patch | 18 +++ scripts/winuae/cas2-overlap-inputs.patch | 27 ++++ 10 files changed, 379 insertions(+), 21 deletions(-) create mode 100644 Copper68k.Tests/M68kWinUaeCas2AuditTests.cs create mode 100644 scripts/winuae/cas2-compare-alias.patch create mode 100644 scripts/winuae/cas2-overlap-inputs.patch diff --git a/Copper68k.Tests/M68kWinUaeCas2AuditTests.cs b/Copper68k.Tests/M68kWinUaeCas2AuditTests.cs new file mode 100644 index 00000000..97b78848 --- /dev/null +++ b/Copper68k.Tests/M68kWinUaeCas2AuditTests.cs @@ -0,0 +1,90 @@ +using Copper68k.Tests.Synthetic; +using Xunit.Sdk; + +namespace Copper68k.Tests; + +public sealed partial class M68kWinUaeCpuTesterConformanceTests +{ + [EnvironmentFact("COPPER68K_RUN_WINUAE_CAS2_AUDIT", "audit qualified WinUAE CAS2 compare aliases and 060 unimplemented frames")] + public void WinUaeCas2AcrossAdvancedModelsWhenEnabled() => RunQualifiedExceptionPreset(new( + "Cas2", "CAS2", "gencpu-cas2.cpp", "cas2-compare-alias.patch", "Cas2", + "beeb1f112867f1f11aa18b172c4607fe572db3f7500c8ef509a657423d640917", "134b61047dfafb91f38374b5151b940ada98dee32bfec251b6a6855043092888", + "winuae-cas2-audit.json", ["68EC020", "68020", "68030", "68040", "68060"], + ["68EC020", "68020", "68030", "68040", "68060", "A1200"], + _ => ["CAS2.W", "CAS2.L"], (model, family) => { var c = Cas2Counts(model, family); return (c.Cases, c.Frames); }, + "M68000PM 4-66/68: canonical W/L extension fields, compare/update semantics, preserved X, shared compare register receives operand 1 on failure; overlapping memory updates are undefined. MC68060UM C.2.2: CAS2 takes vector 61 at the causing instruction PC. Copied CPU generator corrects 040 compare-register alias order. Original Basic corpus/failures retained; no result normalization. CCR 0/31, both privilege modes, one seeded round. Not physical bus locks/order, cache, trace, fault restart, timing or exhaustive architectural-combination qualification. 000/010 absence remains synthetic.", + FormCounts: (model, family) => Cas2Counts(model, family).Forms, AllowFrameFreeFamilies: true, + SavedPcControls: true, ClassifyRegisters: QualifyCas2Encoding, + CompareAliasControls: (model, _) => model.Id == "68040", + InputIdentity: new("cputest-cas2.cpp", "cas2-overlap-inputs.patch", + "4edf1108dd8aab27ea8f61177c8e2166fca5b54e374f759ed388e3c804d5b93e", "dbf0c78f5b88bf2f2ddc0d656a52bca190ec6b970b9e3c6b82cc48d5eac5fc0d"))); + + private static (int Cases, uint Frames, int Forms) Cas2Counts(ModelSpec model, string family) => + (FixtureId(model.Id), family) switch + { + ("68EC020", "CAS2.W") => (174, 0, 174), + ("68EC020", "CAS2.L") => (154, 0, 154), + ("68020" or "68030" or "68040", "CAS2.W") => (42, 0, 42), + ("68020" or "68030" or "68040", "CAS2.L") => (34, 0, 34), + ("68060", "CAS2.W") => (1148, 1148, 1096), + ("68060", "CAS2.L") => (1148, 1148, 1092), + _ => throw new XunitException($"Unqualified CAS2 family: {model.Id}/{family}") + }; + + internal static string QualifyCas2Encoding(ModelSpec model, ushort opcode, ushort first, ushort second, + ushort inputSr, IReadOnlyList registers, string family) + { + var width = opcode == 0x0cfc ? 2 : opcode == 0x0efc ? 4 : 0; + if (!model.FullIndex || registers.Count != 16 || width == 0 || family != (width == 2 ? "CAS2.W" : "CAS2.L") || + ((first | second) & 0x0e38) != 0 || inputSr is not (0 or 0x001f or 0x2000 or 0x201f)) + throw new XunitException($"Unqualified CAS2 encoding/profile: {model.Id}/{family}/{opcode:X4}/{first:X4}/{second:X4}/{inputSr:X4}"); + var r1 = first >> 12; var r2 = second >> 12; + var address1 = registers[r1]; var address2 = registers[r2]; + var overlap = false; + for (var i = 0; i < width; i++) + for (var j = 0; j < width; j++) + overlap |= model.Physical(unchecked(address1 + (uint)i)) == model.Physical(unchecked(address2 + (uint)j)); + if (overlap && model.Id != "68060") + throw new XunitException($"Unqualified overlapping CAS2 operands: {model.Id}/{family}/{address1:X8}/{address2:X8}"); + return $"{family}/Rn={r1}:{r2}/Dc={first & 7}:{second & 7}/Du={(first >> 6) & 7}:{(second >> 6) & 7}/alias={(first & 7) == (second & 7)}/align={address1 & (uint)(width - 1)}:{address2 & (uint)(width - 1)}/overlap={overlap}/sr={inputSr:X4}"; + } +} + +public sealed class M68kWinUaeCas2EncodingTests +{ + private static uint[] Registers() => Enumerable.Range(0, 16).Select(x => (uint)(x * 0x100)).ToArray(); + [Theory] + [InlineData("68040", 0x0cfc, 0x8083, 0xe043, 0, "CAS2.W", "CAS2.W/Rn=8:14/Dc=3:3/Du=2:1/alias=True/align=0:0/overlap=False/sr=0000")] + [InlineData("A1200", 0x0efc, 0x11c7, 0xf000, 0x201f, "CAS2.L", "CAS2.L/Rn=1:15/Dc=7:0/Du=7:0/alias=False/align=0:0/overlap=False/sr=201F")] + public void FixedManualExamplesDistinguishRegistersAliasesAndSize(string modelId, int opcode, int first, int second, int sr, string family, string expected) => + Assert.Equal(expected, M68kWinUaeCpuTesterConformanceTests.QualifyCas2Encoding(ModelSpec.All.Single(x => x.Id == modelId), (ushort)opcode, (ushort)first, (ushort)second, (ushort)sr, Registers(), family)); + [Theory] + [InlineData("68000", 0x0cfc, 0x8083, 0xe043, 0, "CAS2.W")] + [InlineData("68010", 0x0cfc, 0x8083, 0xe043, 0, "CAS2.W")] + [InlineData("68020", 0x0afc, 0x8083, 0xe043, 0, "CAS2.W")] + [InlineData("68020", 0x0cfc, 0x8283, 0xe043, 0, "CAS2.W")] + [InlineData("68020", 0x0cfc, 0x8083, 0xe04b, 0, "CAS2.W")] + [InlineData("68020", 0x0cfc, 0x8083, 0xe043, 0, "CAS2.L")] + [InlineData("68020", 0x0cfc, 0x8083, 0xe043, 0xa000, "CAS2.W")] + public void ReservedUnavailableAndForeignFormsAreRejected(string modelId, int opcode, int first, int second, int sr, string family) => + Assert.Throws(() => M68kWinUaeCpuTesterConformanceTests.QualifyCas2Encoding(ModelSpec.All.Single(x => x.Id == modelId), (ushort)opcode, (ushort)first, (ushort)second, (ushort)sr, Registers(), family)); + [Fact] + public void OverlapIsExcludedOnlyWhenHardwareCanUpdateTheMemoryOperands() + { + var registers = Registers(); registers[14] = registers[8] + 1; + Assert.Throws(() => M68kWinUaeCpuTesterConformanceTests.QualifyCas2Encoding(ModelSpec.All.Single(x => x.Id == "68040"), 0x0cfc, 0x8083, 0xe043, 0, registers, "CAS2.W")); + Assert.Contains("overlap=True", M68kWinUaeCpuTesterConformanceTests.QualifyCas2Encoding(ModelSpec.All.Single(x => x.Id == "68060"), 0x0cfc, 0x8083, 0xe043, 0, registers, "CAS2.W")); + } + [Fact] + public void OverlapChecksPhysicalWidthAndTransfersWrappingAtTheBoundary() + { + var ec = ModelSpec.All.Single(x => x.Id == "68EC020"); + var full = ModelSpec.All.Single(x => x.Id == "68020"); + var registers = Registers(); registers[14] = registers[8] + 0x01000000; + Assert.Throws(() => M68kWinUaeCpuTesterConformanceTests.QualifyCas2Encoding(ec, 0x0cfc, 0x8083, 0xe043, 0, registers, "CAS2.W")); + Assert.Contains("overlap=False", M68kWinUaeCpuTesterConformanceTests.QualifyCas2Encoding(full, 0x0cfc, 0x8083, 0xe043, 0, registers, "CAS2.W")); + registers[8] = 0xffffffff; registers[14] = 0; + Assert.Throws(() => M68kWinUaeCpuTesterConformanceTests.QualifyCas2Encoding(full, 0x0efc, 0x8083, 0xe043, 0, registers, "CAS2.L")); + Assert.Throws(() => M68kWinUaeCpuTesterConformanceTests.QualifyCas2Encoding(full, 0x0efc, 0x8083, 0xe043, 0, [], "CAS2.L")); + } +} diff --git a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs index 18d0c762..94b36847 100644 --- a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs +++ b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs @@ -309,6 +309,7 @@ private sealed class NativeTester : IDisposable private bool _corruptResult; private bool _corruptFrame; private bool _corruptSavedPc; + private bool _corruptCas2CompareAlias; private ushort _corruptSr; private bool _corruptIgnoredSr; private string _integerFamily = ""; @@ -316,6 +317,7 @@ private sealed class NativeTester : IDisposable private readonly Dictionary _longArithmeticForms = new(StringComparer.Ordinal); private Func? _fixtureClassifier; private Func? _fixtureWordsClassifier; + private Func, string>? _fixtureRegisterClassifier; private readonly Dictionary _fixtureForms = new(StringComparer.Ordinal); private NativeTester(IntPtr library) @@ -364,7 +366,9 @@ public RunResult Run( bool qualifyLongArithmetic = false, Func? fixtureClassifier = null, Func? fixtureWordsClassifier = null, - bool corruptSavedPc = false) + bool corruptSavedPc = false, + Func, string>? fixtureRegisterClassifier = null, + bool corruptCas2CompareAlias = false) { _callbackException = null; _executedCases = 0; @@ -378,6 +382,7 @@ public RunResult Run( _corruptResult = corruptResult; _corruptFrame = corruptFrame; _corruptSavedPc = corruptSavedPc; + _corruptCas2CompareAlias = corruptCas2CompareAlias; _corruptSr = corruptSr; _corruptIgnoredSr = corruptIgnoredSr; _integerFamily = opcode; @@ -385,6 +390,7 @@ public RunResult Run( _longArithmeticForms.Clear(); _fixtureClassifier = fixtureClassifier; _fixtureWordsClassifier = fixtureWordsClassifier; + _fixtureRegisterClassifier = fixtureRegisterClassifier; _fixtureForms.Clear(); if (integerProfile is not null && (_destroy is null || _addressingMask is null || _lastOutput is null)) throw new XunitException("Multi-model integer audit requires the qualified native bridge exports (destroy, addressing mask and diagnostics)."); @@ -521,8 +527,41 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt _fixtureForms[form] = _fixtureForms.GetValueOrDefault(form) + 1; } bus.CopyStackImage(registers.Regs[15], registers.Ssp, 0x20); + if (_fixtureRegisterClassifier is not null) + { + // The native input stores USP in slot A7 even when S is set. + // Select its actual initial stack without executing production EA. + var inputRegisters = (uint[])registers.Regs.Clone(); + if ((registers.Sr & 0x2000) != 0) inputRegisters[15] = registers.Ssp; + var form = _fixtureRegisterClassifier(bus.ReadHostWord(registers.Pc), + bus.ReadHostWord(registers.Pc + 2), bus.ReadHostWord(registers.Pc + 4), + (ushort)registers.Sr, inputRegisters); + _fixtureForms[form] = _fixtureForms.GetValueOrDefault(form) + 1; + } ApplyRegisters(cpu, registers, _cpuLevel); + uint? corruptAliasValue = null; + var corruptAliasRegister = 0; + var corruptAliasWidth = 0; + if (_corruptCas2CompareAlias) + { + var opcode = bus.ReadHostWord(registers.Pc); + var first = bus.ReadHostWord(registers.Pc + 2); + var second = bus.ReadHostWord(registers.Pc + 4); + if (opcode is 0x0cfc or 0x0efc && (first & 7) == (second & 7)) + { + // Capture operand 2 independently before execution. On a + // failed aliased comparison, substituting it recreates the + // old reference defect (manual requires operand 1). + var selector = second >> 12; + var address = selector == 15 && (registers.Sr & 0x2000) != 0 + ? registers.Ssp : registers.Regs[selector]; + corruptAliasRegister = first & 7; + corruptAliasWidth = opcode == 0x0cfc ? 2 : 4; + corruptAliasValue = corruptAliasWidth == 2 ? bus.ReadHostWord(address) : + ((uint)bus.ReadHostWord(address) << 16) | bus.ReadHostWord(unchecked(address + 2)); + } + } registers.Cycles = 0; DeferredFpuException? deferredFpuException = null; var deferredFpuExceptionObserved = false; @@ -598,6 +637,10 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt } CopyRegisters(cpu.State, bus, _cpuLevel, ref registers); + if (corruptAliasValue.HasValue && cpu.State.LastExceptionVector < 0 && (registers.Sr & 4) == 0) + registers.Regs[corruptAliasRegister] = corruptAliasWidth == 2 + ? (registers.Regs[corruptAliasRegister] & 0xffff0000) | corruptAliasValue.Value + : corruptAliasValue.Value; if (_integerProfile is not null) { var mask = WinUaeArchitecturalFlags.DefinedMask(_integerFamily, cpu.State.LastExceptionVector, (ushort)registers.Sr); diff --git a/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs b/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs index 176a5d60..1e906ee1 100644 --- a/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs +++ b/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs @@ -89,8 +89,12 @@ private void RunQualifiedExceptionPreset(QualifiedExceptionPreset preset) var pc = pcRequired ? tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptSavedPc: true) : default; var pcDetected = !pcRequired || (!pc.Passed && pc.ExecutedCases > 0 && tester.FrameChecks > 0 && pc.Detail.Contains("frame byte 5", StringComparison.Ordinal)); + var aliasRequired = preset.CompareAliasControls?.Invoke(model, family) == true; + var alias = aliasRequired ? tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptCas2CompareAlias: true) : default; + var aliasDetected = !aliasRequired || (!alias.Passed && alias.ExecutedCases > 0 && + alias.Detail.Contains(":D", StringComparison.Ordinal)); var detected = !register.Passed && register.ExecutedCases > 0 && !sr.Passed && sr.ExecutedCases > 0 && - sr.Detail.Contains("SR:", StringComparison.Ordinal) && frameDetected && pcDetected; + sr.Detail.Contains("SR:", StringComparison.Ordinal) && frameDetected && pcDetected && aliasDetected; var carry = preset.ArithmeticFlagControls ? tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptSr: 1) : default; var carryDetected = !preset.ArithmeticFlagControls || (!carry.Passed && carry.ExecutedCases > 0 && carry.Detail.Contains("SR:", StringComparison.Ordinal)); var ignored = preset.ArithmeticFlagControls ? tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptIgnoredSr: true) : default; @@ -99,17 +103,19 @@ private void RunQualifiedExceptionPreset(QualifiedExceptionPreset preset) probes.Add(new(model.Id, family, detected, register.ExecutedCases, sr.ExecutedCases, frame.ExecutedCases, preset.ArithmeticFlagControls, carryDetected, carry.ExecutedCases, ignoredAccepted, ignored.ExecutedCases, frameRequired, frameDetected, pcRequired, pcDetected, pc.ExecutedCases, - pcRequired ? pc.Detail : null)); + pcRequired ? pc.Detail : null, aliasRequired, aliasDetected, alias.ExecutedCases, + aliasRequired ? alias.Detail : null)); var result = tester.Run(path, family, fixture.CpuLevel, false, false, model, fixtureClassifier: preset.ClassifyForm is null ? null : (opcode, inputSr) => preset.ClassifyForm(opcode, inputSr, family), - fixtureWordsClassifier: preset.ClassifyWords is null ? null : (opcode, extension, followingWord, inputSr) => preset.ClassifyWords(model, opcode, extension, followingWord, inputSr, family)); + fixtureWordsClassifier: preset.ClassifyWords is null ? null : (opcode, extension, followingWord, inputSr) => preset.ClassifyWords(model, opcode, extension, followingWord, inputSr, family), + fixtureRegisterClassifier: preset.ClassifyRegisters is null ? null : (opcode, extension, followingWord, inputSr, registers) => preset.ClassifyRegisters(model, opcode, extension, followingWord, inputSr, registers, family)); var forms = new SortedDictionary(tester.FixtureForms.ToDictionary(x => x.Key, x => x.Value), StringComparer.Ordinal); var expectedForms = preset.FormCounts?.Invoke(model, family) ?? 0; var formDistributionMatches = preset.ExpectedForms is null || forms.SequenceEqual(preset.ExpectedForms(model, family).OrderBy(x => x.Key, StringComparer.Ordinal)); var expectedMasked = preset.MaskedCounts?.Invoke(model, family) ?? (family.StartsWith("CHK2.", StringComparison.Ordinal) ? (uint)expected.Cases : 0); var passing = detected && result.Passed && result.ExecutedCases == expected.Cases && tester.FrameChecks == expected.Frames && tester.MaskedCases == expectedMasked && - forms.Count == expectedForms && formDistributionMatches && ((preset.ClassifyForm is null && preset.ClassifyWords is null) || forms.Values.Sum() == result.ExecutedCases); + forms.Count == expectedForms && formDistributionMatches && ((preset.ClassifyForm is null && preset.ClassifyWords is null && preset.ClassifyRegisters is null) || forms.Values.Sum() == result.ExecutedCases); var detail = $"{result.Detail} Expected/actual callbacks={expected.Cases}/{result.ExecutedCases}, frames={expected.Frames}/{tester.FrameChecks}, masked={expectedMasked}/{tester.MaskedCases}, forms={expectedForms}/{forms.Count}, formDistributionMatches={formDistributionMatches}."; rows.Add(new(model.Id, family, passing ? "passing" : tester.UnsupportedExecution ? "unsupported" : result.ExecutedCases == 0 ? "untested" : "mismatching", result.ExecutedCases, tester.FrameChecks, tester.MaskedCases, detected, forms, detail)); @@ -159,7 +165,9 @@ private sealed record QualifiedExceptionPreset(string Name, string EnvironmentKe Func? ClassifyForm = null, Func? FormCounts = null, Func? ClassifyWords = null, Func>? ExpectedForms = null, - QualifiedInputIdentity? InputIdentity = null, bool AllowFrameFreeFamilies = false, bool SavedPcControls = false); + QualifiedInputIdentity? InputIdentity = null, bool AllowFrameFreeFamilies = false, bool SavedPcControls = false, + Func, string, string>? ClassifyRegisters = null, + Func? CompareAliasControls = null); private sealed record QualifiedInputIdentity(string SourceName, string PatchName, string SourceHash, string PatchHash); private sealed record WinUaeQualifiedExceptionRow(string Model, string Family, string Status, int ExecutedCases, uint ExceptionFrames, uint MaskedSrCases, bool Controls, SortedDictionary Forms, string Detail); @@ -167,5 +175,6 @@ private sealed record WinUaeQualifiedExceptionProbe(string Model, string Family, int RegisterCases, int SrCases, int FrameCases, bool ArithmeticFlagControls, bool CarryDetected, int CarryCases, bool IgnoredAccepted, int IgnoredCases, bool FrameControlRequired, bool FrameDetected, bool SavedPcControlRequired, - bool SavedPcDetected, int SavedPcCases, string? SavedPcDetail); + bool SavedPcDetected, int SavedPcCases, string? SavedPcDetail, + bool CompareAliasControlRequired, bool CompareAliasDetected, int CompareAliasCases, string? CompareAliasDetail); } diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index aac5c90e..27823afd 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -334,3 +334,22 @@ not physical lock/bus, cache, trace/fault restart, timing or exhaustive indexed- value qualification. CAS2 remains separate; 000/010 absence remains synthetic. The [CAS qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#cas-legal-input-and-unimplemented-frame-reference-qualification-2026-10-05) records the original disagreement, controls and remaining scope. + +CAS2 W/L has a separate `Cas2` preset for EC020/A1200/020/030/040/060. It corrects +the pinned 040 reference's compare-alias result to operand 1 (M68000PM 4-68), +and excludes undefined overlapping memory-update candidates before reference +execution. On 060 overlapping candidates remain selected because CAS2 takes +vector 61 without memory updates. The input classifier independently checks +physical overlap, including boundary wrap, and records register/address selectors, +compare/update aliases, alignment and incoming S/CCR. Twelve directories pass +3,180 callbacks, 2,296 exception frames and 3,072 recorded forms. Both source/ +patch identities, all fixture hashes, complete selections and exact counts are +required. Comparator controls recreate both W/L alias defects and corrupt 060 +saved PCs; eleven fixed encoding/overlap tests also run. This is a small seeded +software-reference sample, not exhaustive combinations or physical qualification. +Prepare with `./scripts/prepare-copper68k-winuae.ps1 -Preset Cas2` and the pinned +source/compiler arguments; run +`./scripts/test-copper68k-winuae-qualified-exceptions.ps1 -Preset Cas2 +-InputDirectory -OutputDirectory `. +See the [CAS2 qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#cas2-compare-alias-and-unimplemented-frame-reference-qualification-2026-10-05) +for retained failed discovery, controls, exclusions and remaining scope. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 3fcc7220..aed2adb0 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -3462,3 +3462,125 @@ complete required-gap inventory retained. Fresh pinned SingleStepTests passes 312,500 cases in 125 files; Musashi passes 536 programs with 88 explicit exclusions. Their input identities and complete selections are rechecked; this does not broaden the references' documented qualification or close other gaps. + +### CAS2 compare-alias and unimplemented-frame reference qualification — 2026-10-05 + +The preceding CAS investigation identified the unchanged Basic 040 CAS2.W +witness `0CFC 8083 E043`: both compare fields name D3, with different operand +addresses A0/A6. Copper68k returns operand 1 (`FFFF0001`); the reference expects +operand 2 (`FFFFC700`). [M68000PM 4-68](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) +explicitly assigns operand 1 to a shared compare register on comparison failure. +The pinned generator reverses the register-update order on 040. A copied CPU +generator now writes operand 2 first, then operand 1, for both W/L, preserving +the final operand-1 value when Dc1 == Dc2. Nonaliased results and CPU production +behavior remain unchanged. The original Basic inputs, source and disagreement +remain retained. + +The first `Cas2` discovery inputs also contain overlapping memory operands. +The immutable input classifier rejects them before CPU execution, including +`FFFFFFFF` and zero long transfers whose physical bytes overlap after wrapping. +M68000PM 4-68 marks overlapping memory-update results undefined. A separate +copied input generator now excludes overlapping operand ranges before reference +execution, independently of the comparison outcome. It checks every physical +byte using the profile's external address width and selects A7's actual initial +USP/ISP according to S. It writes the generator's normal skip marker, undoes +fixture write history and resets frame compression state. Existing upstream +canonicalization already clears reserved CAS2 extension fields; the bridge +verifies them rather than changing inputs. Each of EC020/020/030/040 excludes +372 generated CCR/S candidates, recorded in its generation log. On 060 every +CAS2 is an unimplemented-integer exception, so overlap candidates are retained: +there is no ambiguous memory update. [MC68060UM C.2.2](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf) +requires vector 61 and an instruction-PC format-0 frame. + +The classifier uses immutable instruction words and initial registers, with +explicit stack selection rather than production decoder/EA helpers. It records +W/L, both general-register address selectors, both compare/update registers, +shared-compare aliases, alignment residues, overlap and incoming S/CCR. Fixed +tests distinguish 24/32-bit physical aliasing and transfers wrapping at the +address boundary. Required selected counts are: + +| Profile(s), each | Family | Callbacks | Exception frames | Recorded forms | +| --- | --- | ---: | ---: | ---: | +| EC020 / A1200 | W | 174 | 0 | 174 | +| EC020 / A1200 | L | 154 | 0 | 154 | +| 020 / 030 / 040 | W | 42 | 0 | 42 | +| 020 / 030 / 040 | L | 34 | 0 | 34 | +| 060 | W | 1,148 | 1,148 | 1,096 | +| 060 | L | 1,148 | 1,148 | 1,092 | +| All six profiles | W/L | 3,180 | 2,296 | 3,072 | + +All twelve selected directories pass with zero mismatching, unsupported or +untested directories and no masked SR cases. Eleven fixed encoding/profile/ +overlap tests also pass. Every directory detects register and defined-SR +corruptions; the two 060 directories also detect frame-word and saved-PC +corruptions. The two 040 directories detect a separate alias-result corruption +that substitutes the independently captured original operand 2 on a failed +shared-register comparison. The W control fails at callback 29 on the original +`0CFC 8083 E043` witness, expected D3 `FFFF0001`, actual `FFFFC700`. The L control +fails on its first callback (`0EFC E047 1147`), expected D7 `C700FD02`, actual +`0001004F`. These thirty applicable controls prove the comparisons detect the +old reference defect; inapplicable frame controls are labeled, not counted as +coverage. Compare-alias forms occur in every selected family/profile; these +counts do not establish exhaustive combinations or success/failure distributions. + +The initial overlapping-input failures remain in +`artifacts/m6-cas2-reference-discovery/`; the subsequent count-discovery run +intentionally fails against placeholder counts in +`artifacts/m6-cas2-reference-counts/`. Neither is passing qualified evidence. +The final exact-count run in `artifacts/m6-cas2-reference-qualified/` passes +twelve xUnit tests (eleven fixed cases and the generated audit). Nine isolated +controls reject empty profile/family/input selections, missing/changed data, +changed CPU-generator source/patch and changed input-generator source/patch; +`artifacts/m6-cas2-reference-controls/controls.json` records specific failures. + +```powershell +./scripts/prepare-copper68k-winuae.ps1 -GeneratorSource -RunnerSource -VcVars64 -Preset Cas2 -OutputDirectory +./scripts/test-copper68k-winuae-qualified-exceptions.ps1 -Preset Cas2 -InputDirectory -OutputDirectory +``` + +The generator pin remains `025b999239800357e95065fe5b9a15ea5b300fa7`, runner +pin `7a83745d6c6159bc74ab0471578ffc8bc244e66e`. Required source/patch SHA-256: + +| Input | SHA-256 | +| --- | --- | +| Copied CPU generator | `beeb1f112867f1f11aa18b172c4607fe572db3f7500c8ef509a657423d640917` | +| CPU-generator patch | `134b61047dfafb91f38374b5151b940ada98dee32bfec251b6a6855043092888` | +| Copied input generator | `4edf1108dd8aab27ea8f61177c8e2166fca5b54e374f759ed388e3c804d5b93e` | +| Input-generator patch | `dbf0c78f5b88bf2f2ddc0d656a52bca190ec6b970b9e3c6b82cc48d5eac5fc0d` | + +The complete fixture manifest has SHA-256 +`3d819a0373d698496e1db0d440745128eddf227ba066a11d70d35f7e124fccf4`; +generator executable SHA-256 +`9bc0646013661b2e2a1882a321ee1d177fe34eb78298abcc19ef36a8eda9202f`; +native library SHA-256 +`32178feb2c58e1c483fa23507400353efa7a2c06c7bc76cb6227b41818654003`. +All profiles/families, data and memory images, input hashes, both copied sources +and both patches are required before execution. Inputs are in +`artifacts/m6-cas2-qualified-inputs/`, with one seeded round, CCR 0/31 and both +privilege states. The small implemented-model selections are reported as actual +software-reference samples, not comprehensive architectural qualification. + +Physical lock/bus order, cache, trace, operand-fault restart, physical timing +and exhaustive register/value/alias combinations remain unqualified. Unavailable +000/010 outcomes remain synthetic. No production fix, regression retirement, +consumer replay or package publication is added here. This promotes only the +documented sampled reference scope; the full required restoration/reference/ +consolidation roadmap remains in progress. + +Full Release CPU validation in `artifacts/m6-cas2-full/` passes 5,023 tests, +eleven optional skips and zero failures, with all eight qualified WinUAE presets +enabled. Every preset passes its exact selection/count controls with matching +CPU/adapter identities. CPU assembly SHA-256 is +`df73a9759389296aaa6300ec682d27fe67d37f66033bda8d78836044edc60132`; +adapter SHA-256 is +`c1d3cca379061aff66c065b2fb5b61e24eb3ae8e88e4e834ab698e07ec4df0f5`. +Production CPU source remains unchanged since `cb9679d`; preceding private +unpublished `.57` consumer evidence retains its original package/binary +identities, without a new consumer replay or cross-checkpoint binary-equality +claim. CopperScreen's unrelated changes and pinned NuGet boundary are preserved. + +The strict report gate passes 16,035,670 deterministic logical cases in 595 +xUnit batches. Pinned SingleStepTests passes 312,500 cases across 125 files; +Musashi passes 536 programs with 88 explicit exclusions. Input identities and +complete selections are rechecked. `roadmapComplete=false` remains explicit; +these passing scoped gates do not close the remaining milestone-6 requirements. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 919916a8..eb3091fb 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -122,7 +122,26 @@ identities, replacement proofs and remaining required coverage here as work progresses. No milestone is complete merely because an inventory exists or a partial matrix passes. -Latest milestone-6 reference checkpoint, 2026-10-05: legal sampled CAS B/W/L +Latest milestone-6 reference checkpoint, 2026-10-05: sampled CAS2 W/L passes +3,180 callbacks, 2,296 unimplemented-integer frames and 3,072 recorded forms +across EC020/A1200/020/030/040/060. The CPU already implements the manual's +shared-compare operand-1 rule; a copied reference corrects 040 alias ordering. +A copied input generator excludes overlapping memory-update candidates before +execution, with independent physical-width/wrap checks in the bridge; 060 +unimplemented cases retain overlapping inputs. Twelve directories and eleven +fixed cases pass, with thirty comparator controls and nine input/source controls. +Both source/patch identities, complete selections and exact counts are required. +The [CAS2 qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#cas2-compare-alias-and-unimplemented-frame-reference-qualification-2026-10-05) +retains failed discovery and reports the limited seeded scope honestly. No +production fix, regression retirement or package publication is added. The full +milestone-6 restoration/reference/consolidation requirements remain in progress. +Full Release CPU validation passes 5,023 tests with eleven optional skips and +all eight qualified WinUAE presets enabled against the same CPU/adapter binaries. +The strict gate passes 16,035,670 logical cases in 595 batches; pinned +SingleStepTests passes 312,500 cases in 125 files, and Musashi passes 536 programs +with 88 explicit exclusions. `roadmapComplete=false` remains explicit. + +Preceding milestone-6 reference checkpoint, 2026-10-05: legal sampled CAS B/W/L passes 49,284 independent callbacks, 2,466 unimplemented-integer frames and 38,448 recorded architectural combinations across EC020/A1200/020/030/040/060. The CPU already saves the documented 060 instruction PC; a separate copied diff --git a/scripts/prepare-copper68k-winuae.ps1 b/scripts/prepare-copper68k-winuae.ps1 index fbd18eb6..f176dd75 100644 --- a/scripts/prepare-copper68k-winuae.ps1 +++ b/scripts/prepare-copper68k-winuae.ps1 @@ -4,7 +4,7 @@ param( [Parameter(Mandatory)] [string] $GeneratorSource, [Parameter(Mandatory)] [string] $RunnerSource, [Parameter(Mandatory)] [string] $VcVars64, - [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop','Moves','Cas')] [string] $Preset = 'Basic', + [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop','Moves','Cas','Cas2')] [string] $Preset = 'Basic', [string] $OutputDirectory = 'artifacts/winuae-model-inputs' ) $ErrorActionPreference = 'Stop' @@ -48,17 +48,17 @@ try { & ./build68k.exe table68k | Set-Content -Encoding utf8 cpudefs.cpp if ($LASTEXITCODE -ne 0) { throw 'Opcode table generation failed' } $cpuGeneratorSource = 'gencpu.cpp' - if ($Preset -in @('TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop','Cas')) { + if ($Preset -in @('TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop','Cas','Cas2')) { # TrapBounds: following PC (MC68020UM 6.1.4 / MC68040UM 8.2.3). # Breakpoints: opcode PC (illegal exception, UM 6.1.5 / 8.2.4). # Patch a copy; Basic keeps the pinned source. - $sourceName = switch ($Preset) {'TrapBounds' {'gencpu-trap-bounds.cpp'} 'Breakpoints' {'gencpu-breakpoints.cpp'} 'LongArithmetic' {'gencpu-long-arithmetic.cpp'} 'WordDivision' {'gencpu-word-division.cpp'} 'LowPowerStop' {'gencpu-lpstop.cpp'} 'Cas' {'gencpu-cas.cpp'}} - $patchName = switch ($Preset) {'TrapBounds' {'trap-bounds-pc.patch'} 'Breakpoints' {'breakpoint-pc.patch'} 'LongArithmetic' {'long-arithmetic-unimplemented.patch'} 'WordDivision' {'word-division-carry.patch'} 'LowPowerStop' {'lpstop-fetch-pc.patch'} 'Cas' {'cas-unimplemented-pc.patch'}} + $sourceName = switch ($Preset) {'TrapBounds' {'gencpu-trap-bounds.cpp'} 'Breakpoints' {'gencpu-breakpoints.cpp'} 'LongArithmetic' {'gencpu-long-arithmetic.cpp'} 'WordDivision' {'gencpu-word-division.cpp'} 'LowPowerStop' {'gencpu-lpstop.cpp'} 'Cas' {'gencpu-cas.cpp'} 'Cas2' {'gencpu-cas2.cpp'}} + $patchName = switch ($Preset) {'TrapBounds' {'trap-bounds-pc.patch'} 'Breakpoints' {'breakpoint-pc.patch'} 'LongArithmetic' {'long-arithmetic-unimplemented.patch'} 'WordDivision' {'word-division-carry.patch'} 'LowPowerStop' {'lpstop-fetch-pc.patch'} 'Cas' {'cas-unimplemented-pc.patch'} 'Cas2' {'cas2-compare-alias.patch'}} $patchPath = Join-Path $PSScriptRoot ('winuae/' + $patchName) $patch = [IO.File]::ReadAllText($patchPath).Replace("`r`n", "`n") $source = [IO.File]::ReadAllText((Join-Path $generator 'gencpu.cpp')).Replace("`r`n", "`n") $hunks = @($patch -split "(?m)^@@`n" | Select-Object -Skip 1) - $requiredHunks = switch ($Preset) {'Breakpoints' {1} 'TrapBounds' {2} 'LongArithmetic' {3} 'WordDivision' {1} 'LowPowerStop' {2} 'Cas' {1}} + $requiredHunks = switch ($Preset) {'Breakpoints' {1} 'TrapBounds' {2} 'LongArithmetic' {3} 'WordDivision' {1} 'LowPowerStop' {2} 'Cas' {1} 'Cas2' {2}} if ($hunks.Count -ne $requiredHunks) { throw "Incorrect $Preset patch hunk count" } foreach ($hunk in $hunks) { $lines = $hunk.TrimEnd("`n").Split("`n") @@ -86,14 +86,14 @@ try { [IO.File]::WriteAllText($testerSource, (Patch-Once ([IO.File]::ReadAllText((Join-Path $generator 'cputest.cpp'))) $before $after)) [IO.File]::WriteAllText((Join-Path $output 'trace-priority.patch'), ([IO.File]::ReadAllText($patchPath)).Replace("`r`n", "`n")) } - if ($Preset -in @('LongArithmetic','Moves','Cas')) { + if ($Preset -in @('LongArithmetic','Moves','Cas','Cas2')) { # M68000PM 4-94/98/136/140: reserved extension fields are zero; # Dh == Dl with a 64-bit multiply has undefined results. MOVES # 6-24/25/26 similarly fixes reserved fields and excludes undefined # same-An postincrement/predecrement store values. CAS 4-67 allows # only Du/Dc extension fields. Select # legal inputs before reference execution, never in the CPU bridge. - $patchName = switch ($Preset) {'Moves' {'moves-encodings.patch'} 'Cas' {'cas-encodings.patch'} default {'long-arithmetic-encodings.patch'}} + $patchName = switch ($Preset) {'Moves' {'moves-encodings.patch'} 'Cas' {'cas-encodings.patch'} 'Cas2' {'cas2-overlap-inputs.patch'} default {'long-arithmetic-encodings.patch'}} $patch = [IO.File]::ReadAllText((Join-Path $PSScriptRoot ('winuae/' + $patchName))).Replace("`r`n", "`n") $source = [IO.File]::ReadAllText((Join-Path $generator 'cputest.cpp')).Replace("`r`n", "`n") $hunks = @($patch -split "(?m)^@@`n" | Select-Object -Skip 1) @@ -101,7 +101,7 @@ try { $lines = $hunks[0].TrimEnd("`n").Split("`n") $before = (($lines | Where-Object { $_.StartsWith('-') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" $after = (($lines | Where-Object { $_.StartsWith('+') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" - $testerSource = Join-Path $output $(switch ($Preset) {'Moves' {'cputest-moves.cpp'} 'Cas' {'cputest-cas.cpp'} default {'cputest-long-arithmetic.cpp'}}) + $testerSource = Join-Path $output $(switch ($Preset) {'Moves' {'cputest-moves.cpp'} 'Cas' {'cputest-cas.cpp'} 'Cas2' {'cputest-cas2.cpp'} default {'cputest-long-arithmetic.cpp'}}) [IO.File]::WriteAllText($testerSource, (Patch-Once $source $before $after)) [IO.File]::WriteAllText((Join-Path $output $patchName), $patch) } @@ -172,6 +172,9 @@ void M68KTester_destroy(M68KTesterContext* context) { if ($Preset -eq 'Moves') { $baseIni = $baseIni.Replace('[test=Basic]', '[test=Moves]').Replace('mode=all', 'mode=MOVES.B,MOVES.W,MOVES.L').Replace('feature_sr_mask=0x0000', 'feature_sr_mask=0x2000') } + if ($Preset -eq 'Cas2') { + $baseIni = $baseIni.Replace('[test=Basic]', '[test=Cas2]').Replace('mode=all', 'mode=CAS2.W,CAS2.L').Replace('feature_sr_mask=0x0000', 'feature_sr_mask=0x2000') + } if ($Preset -eq 'Cas') { $baseIni = $baseIni.Replace('[test=Basic]', '[test=Cas]').Replace('mode=all', 'mode=CAS.B,CAS.W,CAS.L').Replace('feature_sr_mask=0x0000', 'feature_sr_mask=0x2000') } @@ -186,7 +189,7 @@ void M68KTester_destroy(M68KTesterContext* context) { if ($Preset -eq 'LongArithmetic' -and $model.id -in @('68000','68010')) { continue } if ($Preset -eq 'LowPowerStop' -and $model.id -ne '68060') { continue } if ($Preset -eq 'Moves' -and $model.id -eq '68000') { continue } - if ($Preset -eq 'Cas' -and $model.id -in @('68000','68010')) { continue } + if ($Preset -in @('Cas','Cas2') -and $model.id -in @('68000','68010')) { continue } $profileRoot = Join-Path $output $model.id New-Item -ItemType Directory -Path $profileRoot | Out-Null # Every invocation uses a fresh generator output path; stale data cannot fill a gap. @@ -233,6 +236,10 @@ void M68KTester_destroy(M68KTesterContext* context) { LowPowerStopPatchSha256=$(if ($Preset -eq 'LowPowerStop') {(Get-FileHash -LiteralPath (Join-Path $output 'lpstop-fetch-pc.patch')).Hash.ToLowerInvariant()} else {$null}) MovesSourceSha256=$(if ($Preset -eq 'Moves') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-moves.cpp')).Hash.ToLowerInvariant()} else {$null}) MovesPatchSha256=$(if ($Preset -eq 'Moves') {(Get-FileHash -LiteralPath (Join-Path $output 'moves-encodings.patch')).Hash.ToLowerInvariant()} else {$null}) + Cas2InputSourceSha256=$(if ($Preset -eq 'Cas2') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-cas2.cpp')).Hash.ToLowerInvariant()} else {$null}) + Cas2InputPatchSha256=$(if ($Preset -eq 'Cas2') {(Get-FileHash -LiteralPath (Join-Path $output 'cas2-overlap-inputs.patch')).Hash.ToLowerInvariant()} else {$null}) + Cas2SourceSha256=$(if ($Preset -eq 'Cas2') {(Get-FileHash -LiteralPath (Join-Path $output 'gencpu-cas2.cpp')).Hash.ToLowerInvariant()} else {$null}) + Cas2PatchSha256=$(if ($Preset -eq 'Cas2') {(Get-FileHash -LiteralPath (Join-Path $output 'cas2-compare-alias.patch')).Hash.ToLowerInvariant()} else {$null}) CasSourceSha256=$(if ($Preset -eq 'Cas') {(Get-FileHash -LiteralPath (Join-Path $output 'gencpu-cas.cpp')).Hash.ToLowerInvariant()} else {$null}) CasPatchSha256=$(if ($Preset -eq 'Cas') {(Get-FileHash -LiteralPath (Join-Path $output 'cas-unimplemented-pc.patch')).Hash.ToLowerInvariant()} else {$null}) CasInputSourceSha256=$(if ($Preset -eq 'Cas') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-cas.cpp')).Hash.ToLowerInvariant()} else {$null}) diff --git a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 index 41d9e7cd..375a706b 100644 --- a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 +++ b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 @@ -1,7 +1,7 @@ #requires -Version 7.0 [CmdletBinding()] param( - [ValidateSet('TrapBounds','Breakpoints','WordDivision','LowPowerStop','Moves','Cas')] [string] $Preset = 'TrapBounds', + [ValidateSet('TrapBounds','Breakpoints','WordDivision','LowPowerStop','Moves','Cas','Cas2')] [string] $Preset = 'TrapBounds', [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $InputDirectory, [string] $NativeLibrary, [string] $OutputDirectory = 'artifacts/winuae-trap-bounds-audit' @@ -14,9 +14,9 @@ $library = (Resolve-Path -LiteralPath $NativeLibrary).Path $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } New-Item -ItemType Directory -Path $output | Out-Null -$key = switch ($Preset) {'TrapBounds' {'TRAP_BOUNDS'} 'Breakpoints' {'BREAKPOINT'} 'WordDivision' {'WORD_DIVISION'} 'LowPowerStop' {'LPSTOP'} 'Moves' {'MOVES'} 'Cas' {'CAS'}} -$testName = switch ($Preset) {'TrapBounds' {'WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled'} 'Breakpoints' {'WinUaeBreakpointExceptionsAcrossSelectedModelsWhenEnabled'} 'WordDivision' {'WinUaeWordDivisionAcrossSelectedModelsWhenEnabled'} 'LowPowerStop' {'WinUaeLowPowerStopExceptionsWhenEnabled'} 'Moves' {'WinUaeMovesAcrossSelectedModelsWhenEnabled'} 'Cas' {'WinUaeCasAcrossAdvancedModelsWhenEnabled'}} -$reportName = switch ($Preset) {'TrapBounds' {'winuae-trap-bounds-audit.json'} 'Breakpoints' {'winuae-breakpoint-audit.json'} 'WordDivision' {'winuae-word-division-audit.json'} 'LowPowerStop' {'winuae-lpstop-audit.json'} 'Moves' {'winuae-moves-audit.json'} 'Cas' {'winuae-cas-audit.json'}} +$key = switch ($Preset) {'TrapBounds' {'TRAP_BOUNDS'} 'Breakpoints' {'BREAKPOINT'} 'WordDivision' {'WORD_DIVISION'} 'LowPowerStop' {'LPSTOP'} 'Moves' {'MOVES'} 'Cas' {'CAS'} 'Cas2' {'CAS2'}} +$testName = switch ($Preset) {'TrapBounds' {'WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled'} 'Breakpoints' {'WinUaeBreakpointExceptionsAcrossSelectedModelsWhenEnabled'} 'WordDivision' {'WinUaeWordDivisionAcrossSelectedModelsWhenEnabled'} 'LowPowerStop' {'WinUaeLowPowerStopExceptionsWhenEnabled'} 'Moves' {'WinUaeMovesAcrossSelectedModelsWhenEnabled'} 'Cas' {'WinUaeCasAcrossAdvancedModelsWhenEnabled'} 'Cas2' {'WinUaeCas2AcrossAdvancedModelsWhenEnabled'}} +$reportName = switch ($Preset) {'TrapBounds' {'winuae-trap-bounds-audit.json'} 'Breakpoints' {'winuae-breakpoint-audit.json'} 'WordDivision' {'winuae-word-division-audit.json'} 'LowPowerStop' {'winuae-lpstop-audit.json'} 'Moves' {'winuae-moves-audit.json'} 'Cas' {'winuae-cas-audit.json'} 'Cas2' {'winuae-cas2-audit.json'}} $filter = "FullyQualifiedName~$testName" $expectedTests = 1 if ($Preset -eq 'WordDivision') { @@ -35,6 +35,10 @@ if ($Preset -eq 'Cas') { $filter += '|FullyQualifiedName~M68kWinUaeCasEncodingTests' $expectedTests = 16 } +if ($Preset -eq 'Cas2') { + $filter += '|FullyQualifiedName~M68kWinUaeCas2EncodingTests' + $expectedTests = 12 +} $settings = @{ ('COPPER68K_RUN_WINUAE_' + $key + '_AUDIT') = '1' ('COPPER68K_WINUAE_' + $key + '_PATH') = $root diff --git a/scripts/winuae/cas2-compare-alias.patch b/scripts/winuae/cas2-compare-alias.patch new file mode 100644 index 00000000..60496274 --- /dev/null +++ b/scripts/winuae/cas2-compare-alias.patch @@ -0,0 +1,18 @@ +--- a/gencpu/gencpu.cpp ++++ b/gencpu/gencpu.cpp +@@ + if (cpu_level >= 4) { + // 68040: register update order swapped +- out("m68k_dreg(regs, (extra >> 16) & 7) = (m68k_dreg(regs, (extra >> 16) & 7) & ~0xffff) | (dst1 & 0xffff);\n"); + out("m68k_dreg(regs, (extra >> 0) & 7) = (m68k_dreg(regs, (extra >> 0) & 7) & ~0xffff) | (dst2 & 0xffff);\n"); ++ // M68000PM 4-68: a shared Dc receives memory operand 1. ++ out("m68k_dreg(regs, (extra >> 16) & 7) = (m68k_dreg(regs, (extra >> 16) & 7) & ~0xffff) | (dst1 & 0xffff);\n"); + } else { +@@ + if (cpu_level >= 4) { + // 68040: register update order swapped +- out("m68k_dreg(regs, (extra >> 16) & 7) = dst1;\n"); + out("m68k_dreg(regs, (extra >> 0) & 7) = dst2;\n"); ++ // M68000PM 4-68: a shared Dc receives memory operand 1. ++ out("m68k_dreg(regs, (extra >> 16) & 7) = dst1;\n"); + } else { diff --git a/scripts/winuae/cas2-overlap-inputs.patch b/scripts/winuae/cas2-overlap-inputs.patch new file mode 100644 index 00000000..2e144bba --- /dev/null +++ b/scripts/winuae/cas2-overlap-inputs.patch @@ -0,0 +1,27 @@ +--- a/gencpu/cputest.cpp ++++ b/gencpu/cputest.cpp +@@ + // execute test instruction(s) ++ // M68000PM 4-68: overlapping memory updates are undefined. ++ // Select before reference execution, including physical wrap ++ // and A7's actual initial user/supervisor stack. On 060 no ++ // operand is updated: every CAS2 takes vector 61. ++ if (dp->mnemo == i_CAS2 && cpu_lvl < 5) { ++ int width = dp->size == sz_word ? 2 : 4; ++ int r1 = get_word_test(opcode_memory_start + 2) >> 12; ++ int r2 = get_word_test(opcode_memory_start + 4) >> 12; ++ uae_u32 a1 = r1 == 15 && (regs.sr & 0x2000) ? regs.isp : regs.regs[r1]; ++ uae_u32 a2 = r2 == 15 && (regs.sr & 0x2000) ? regs.isp : regs.regs[r2]; ++ bool overlap = false; ++ for (int i = 0; i < width; i++) ++ for (int j = 0; j < width; j++) ++ overlap |= ((a1 + i) & addressing_mask) == ((a2 + j) & addressing_mask); ++ if (overlap) { ++ wprintf(_T(" CAS2 overlapping operands excluded before reference execution\n")); ++ *dst++ = CT_END_SKIP; ++ last_exception_len = -1; ++ undo_memory(ahist, ahcnt_start); ++ continue; ++ } ++ } + execute_ins(pc - endopcodesize, branch_target_pc, dp); From 230009a07b5ad2b4aefe1eb1fa9cde25d3dd87b3 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 19:06:51 +0300 Subject: [PATCH 040/211] Qualify cache instruction encodings and consolidate duplicates --- .../M68kWinUaeCacheEncodingAuditTests.cs | 58 ++++++++ Copper68k.Tests/Synthetic/IntegerInventory.cs | 2 +- Copper68k.Tests/Synthetic/README.md | 22 +++ .../Synthetic/SyntheticCacheEncodingTests.cs | 33 +++++ .../Synthetic/SyntheticModelSystemTests.cs | 15 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 134 ++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 22 ++- scripts/prepare-copper68k-winuae.ps1 | 26 +++- .../test-copper68k-synthetic-mutations.ps1 | 63 +++++++- scripts/test-copper68k-synthetic.ps1 | 4 +- ...-copper68k-winuae-qualified-exceptions.ps1 | 12 +- scripts/winuae/cache-encodings.patch | 21 +++ 12 files changed, 391 insertions(+), 21 deletions(-) create mode 100644 Copper68k.Tests/M68kWinUaeCacheEncodingAuditTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticCacheEncodingTests.cs create mode 100644 scripts/winuae/cache-encodings.patch diff --git a/Copper68k.Tests/M68kWinUaeCacheEncodingAuditTests.cs b/Copper68k.Tests/M68kWinUaeCacheEncodingAuditTests.cs new file mode 100644 index 00000000..a0480726 --- /dev/null +++ b/Copper68k.Tests/M68kWinUaeCacheEncodingAuditTests.cs @@ -0,0 +1,58 @@ +using Copper68k.Tests.Synthetic; +using Xunit.Sdk; + +namespace Copper68k.Tests; + +public sealed partial class M68kWinUaeCpuTesterConformanceTests +{ + [EnvironmentFact("COPPER68K_RUN_WINUAE_CACHE_ENCODINGS_AUDIT", "audit qualified WinUAE cache scope-zero exception encodings")] + public void WinUaeCacheScopeZeroAcrossSelectedModelsWhenEnabled() => RunQualifiedExceptionPreset(new( + "CacheEncodings", "CACHE_ENCODINGS", "cputest-cache-encodings.cpp", "cache-encodings.patch", "CacheEncodings", + "5f6df41a9b6e5e96088c0c81c65f31d9363a1904e6a37c3001ebf4cf4105ab92", "ef393d99b50198f1c0139de12d890f1f2c3cc0675bee19d22259de698c68e32e", "winuae-cache-encodings-audit.json", + ["68000", "68010", "68EC020", "68020", "68030", "68040", "68060"], + ModelSpec.All.Select(x => x.Id).ToArray(), _ => ["ILLEGAL"], (_, _) => (256, 256), + "M68000PM 6-4/9 and MC68060UM D-12: every cache scope-00 word takes vector 4 on 040/060, before privilege effects; earlier profiles take vector 11. Copied input generator corrects that exception and selects only these encodings before reference execution. All 64 scope-00 words, CCR 0/31 and both privilege states; exact form distribution required. Original Basic inputs/failures retained. Legal cache operations remain upstream-excluded and synthetic coverage. No physical cache/bus effects, fault restart, incoming trace or timing qualification.", + FormCounts: (_, _) => 256, ClassifyWords: (model, opcode, _, _, sr, family) => QualifyCacheScopeEncoding(model, opcode, sr, family), + ExpectedForms: CacheScopeForms, SavedPcControls: true)); + + internal static string QualifyCacheScopeEncoding(ModelSpec model, ushort opcode, ushort sr, string family) + { + if ((opcode & 0xff18) != 0xf400 || family != "ILLEGAL" || sr is not (0 or 0x1f or 0x2000 or 0x201f)) + throw new XunitException($"Unqualified cache scope encoding: {model.Id}/{family}/{opcode:X4}/{sr:X4}"); + return $"ILLEGAL/op={opcode:X4}/sr={sr:X4}/vector={(model.Id is "68040" or "68060" ? 4 : 11)}"; + } + + private static IReadOnlyDictionary CacheScopeForms(ModelSpec model, string family) + { + var forms = new SortedDictionary(StringComparer.Ordinal); + // Fixed independent encoding expansion, not decoder/classifier output. + foreach (var prefix in new[] { 0xf400, 0xf420, 0xf440, 0xf460, 0xf480, 0xf4a0, 0xf4c0, 0xf4e0 }) + for (var register = 0; register < 8; register++) + foreach (var sr in new[] { 0, 0x1f, 0x2000, 0x201f }) + forms.Add($"ILLEGAL/op={prefix + register:X4}/sr={sr:X4}/vector={(model.Id is "68040" or "68060" ? 4 : 11)}", 1); + return forms; + } +} + +public sealed class M68kWinUaeCacheScopeEncodingTests +{ + [Theory] + [InlineData("68040", 0xf400, 0, "ILLEGAL/op=F400/sr=0000/vector=4")] + [InlineData("68060", 0xf467, 0x201f, "ILLEGAL/op=F467/sr=201F/vector=4")] + [InlineData("68060", 0xf4e7, 0x1f, "ILLEGAL/op=F4E7/sr=001F/vector=4")] + [InlineData("68000", 0xf400, 0x2000, "ILLEGAL/op=F400/sr=2000/vector=11")] + [InlineData("68030", 0xf4c0, 0x201f, "ILLEGAL/op=F4C0/sr=201F/vector=11")] + [InlineData("A1200", 0xf420, 0, "ILLEGAL/op=F420/sr=0000/vector=11")] + public void FixedManualExamplesDistinguishScopeAndModel(string modelId, int opcode, int sr, string expected) => + Assert.Equal(expected, M68kWinUaeCpuTesterConformanceTests.QualifyCacheScopeEncoding(ModelSpec.All.Single(x => x.Id == modelId), (ushort)opcode, (ushort)sr, "ILLEGAL")); + + [Theory] + [InlineData(0xf408, 0, "ILLEGAL")] + [InlineData(0xf410, 0, "ILLEGAL")] + [InlineData(0xf418, 0, "ILLEGAL")] + [InlineData(0xf500, 0, "ILLEGAL")] + [InlineData(0xf400, 0xa000, "ILLEGAL")] + [InlineData(0xf400, 0, "CINVL")] + public void LegalScopesForeignFamiliesAndUnrequestedStatusAreRejected(int opcode, int sr, string family) => + Assert.Throws(() => M68kWinUaeCpuTesterConformanceTests.QualifyCacheScopeEncoding(ModelSpec.All.Single(x => x.Id == "68040"), (ushort)opcode, (ushort)sr, family)); +} diff --git a/Copper68k.Tests/Synthetic/IntegerInventory.cs b/Copper68k.Tests/Synthetic/IntegerInventory.cs index 4c190b57..27a6b6bd 100644 --- a/Copper68k.Tests/Synthetic/IntegerInventory.cs +++ b/Copper68k.Tests/Synthetic/IntegerInventory.cs @@ -87,7 +87,7 @@ private static IEnumerable Build() foreach (var name in new[] { "ANDI", "ORI", "EORI" }) foreach (var target in new[] { "CCR", "SR" }) yield return new(name + " to " + target, 5, "W", "immediate; SR privileged"); yield return new("MOVE16", 5, "16 bytes", "aligned line; model-defined register/absolute forms", "040+"); - foreach (var name in new[] { "CINV", "CPUSH" }) yield return new(name, 5, "line,page,all", "instruction/data/both caches, privileged", "040+"); + foreach (var name in new[] { "CINV", "CPUSH" }) yield return new(name, 5, "line,page,all; scope 00 illegal", "neither/instruction/data/both caches, privileged", "040+"); yield return new("LPSTOP", 5, "W", "immediate SR, privileged", "060"); yield return new("HALT", 5, "none", "privileged debug halt; interrupts cannot restart", "060"); yield return new("PULSE", 5, "none", "user/supervisor; integer state preserved; physical PST/debug commands unqualified", "060"); diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 27823afd..0c7fd8c5 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -353,3 +353,25 @@ source/compiler arguments; run -InputDirectory -OutputDirectory `. See the [CAS2 qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#cas2-compare-alias-and-unimplemented-frame-reference-qualification-2026-10-05) for retained failed discovery, controls, exclusions and remaining scope. + +The `CacheEncodings` preset qualifies every scope-zero cache word on all eight +profiles: 64 opcode words, CCR 0/31 and both privilege states, totaling 2,048 +callbacks and exception frames. M68000PM 6-4/9 and MC68060UM D-12 specify vector +4 on 040/060; earlier profiles take line-F vector 11. A copied input generator +corrects that reference exception and selects these inputs before execution. +Exact opcode/status distributions, source/patch/input identities, complete +selections and register/SR/frame/saved-PC controls are required. Twelve fixed +encoding cases run with the audit. The original Basic disagreement is retained. +Prepare with `./scripts/prepare-copper68k-winuae.ps1 -Preset CacheEncodings` +and the pinned source/compiler arguments; run +`./scripts/test-copper68k-winuae-qualified-exceptions.ps1 -Preset CacheEncodings +-InputDirectory -OutputDirectory `. +Upstream excludes actual cache operations; they are not external-reference +coverage. The ordinary `system-cache-encodings` matrix enumerates every F4xx word, +all CCRs and both privilege states (131,072 cases), including neither-cache +no-operations and illegal-scope priority. `-Scope CacheEncodings` in the mutation +script proves vector, priority, privilege, flag and extension-length detection. +The duplicated cache loop is retired after shared mutation proof; MOVE16, +breakpoints, specialized cache/prefetch/bus/JIT/native regressions remain. +See the [cache encoding record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#cache-encoding-reference-qualification-and-consolidation-2026-10-05) +for exact identities, replacement cases and qualification limits. diff --git a/Copper68k.Tests/Synthetic/SyntheticCacheEncodingTests.cs b/Copper68k.Tests/Synthetic/SyntheticCacheEncodingTests.cs new file mode 100644 index 00000000..c96fbab7 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticCacheEncodingTests.cs @@ -0,0 +1,33 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticCacheEncodingTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void EveryCacheOpcodeIncludesInvalidScopeAndNeitherCache(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); + var report = new CoverageBatch(modelId, "system-cache-encodings"); + // M68000PM 6-4/9 and MC68060UM D-12: scope 00 is illegal, + // cache 00 is a legal no-operation for the other scopes. Enumerate + // every F4xx word, including ignored An fields in all-cache forms. + for (var bits = 0; bits < 256; bits++) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + machine.Reset(ccr, supervisor); + var opcode = (ushort)(0xf400 | bits); + var scope = (bits >> 3) & 3; + var expected = SyntheticExecution.Prepare(machine, [opcode]); + if (modelId is not ("68040" or "68060")) SyntheticExecution.ExpectException(machine, expected, 11); + else if (scope == 0) SyntheticExecution.ExpectException(machine, expected, 4); + else if (!supervisor) SyntheticExecution.ExpectException(machine, expected, 8); + SyntheticExecution.Run(machine, expected, report, + $"{modelId}/{((bits & 32) == 0 ? "CINV" : "CPUSH")}/scope={scope}/cache={bits >> 6}/A{bits & 7}/super={supervisor}/ccr={ccr:X2}/op={opcode:X4}"); + } + report.Complete(output); + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs b/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs index 4d914939..a3de35a8 100644 --- a/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs @@ -97,7 +97,7 @@ public void MovecControlInventoryMasksPrivilegeAndAllGeneralRegisters(string mod }; [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] - public void Move16CacheInstructionsAndBreakpoints(string modelId) + public void Move16TransfersAndBreakpoints(string modelId) { var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); var report = new CoverageBatch(modelId, "system-model"); for (var ccr = 0; ccr < 32; ccr++) @@ -108,16 +108,9 @@ public void Move16CacheInstructionsAndBreakpoints(string modelId) m.Reset(ccr, supervisor); var op = (ushort)(0x4848 | breakpoint); var e = SyntheticExecution.Prepare(m, [op]); SyntheticExecution.ExpectException(m, e, 4); SyntheticExecution.Run(m, e, report, $"{modelId}/BKPT/none/{breakpoint}/super={supervisor}/op={op:X4}/ccr={ccr:X2}"); } - foreach (var push in new[] { false, true }) - for (var cache = 1; cache < 4; cache++) - for (var scope = 1; scope < 4; scope++) - for (var reg = 0; reg < 8; reg++) - { - m.Reset(ccr, supervisor); var op = (ushort)(0xf400 | (push ? 0x20 : 0) | cache << 6 | scope << 3 | reg); var e = SyntheticExecution.Prepare(m, [op]); - if (modelId is not ("68040" or "68060")) SyntheticExecution.ExpectException(m, e, 11); - else if (!supervisor) SyntheticExecution.ExpectException(m, e, 8); - SyntheticExecution.Run(m, e, report, $"{modelId}/{(push ? "CPUSH" : "CINV")}/{scope}/cache={cache}/A{reg}/super={supervisor}/op={op:X4}/ccr={ccr:X2}"); - } + // Cache semantics moved to the exhaustive F4xx matrix after + // shared X-flag and extension-length mutation proof. Specialized + // physical cache/prefetch regressions are retained separately. } for (var form = 0; form < 5; form++) for (var r = 0; r < 8; r++) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index aed2adb0..d3eb0bfe 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -3584,3 +3584,137 @@ xUnit batches. Pinned SingleStepTests passes 312,500 cases across 125 files; Musashi passes 536 programs with 88 explicit exclusions. Input identities and complete selections are rechecked. `roadmapComplete=false` remains explicit; these passing scoped gates do not close the remaining milestone-6 requirements. + +### Cache encoding reference qualification and consolidation — 2026-10-05 + +The retained Basic 040/060 ILLEGAL failures stop at callback 29,331 with `F400`, +initial SR `0000` and opcode PC `0087FFA0`. Copper68k takes vector 4; the pinned +generator expects line-F vector 11. [M68000PM 6-4/9](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) +assigns illegal-instruction traps to cache scope `00` and makes cache `00` a +no-operation for other scopes. [MC68060UM D-12](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf) +repeats the scope-zero rule. The production CPU already follows these rules; +no production correction or timing change is needed. + +The old native diagnostic's “got no exception” is not an absent CPU exception. +The adapter trace records `exc=4`, and the pinned native tester special-cases +vector 4 because its completion sentinel also uses ILLEGAL. At vendor +`m68k_cpu_tester.c` lines 1289–1307 it describes a vector-4 mismatch at the +matching PC as no exception. No result/frame/mask normalization is introduced +to make that wording disappear. The new preset compares vector 4 and validates +its real frame; frame and saved-PC corruption are independently rejected. +Original Basic inputs, manifest, bridge and failures remain unchanged. + +`SyntheticCacheEncodingTests.EveryCacheOpcodeIncludesInvalidScopeAndNeitherCache` +enumerates all 256 F4xx words, all 32 CCR states and both privilege states on +every profile. The independent expectation checks all registers, stack banks, +defined SR, exact PC, exception frames, terminal state and surrounding memory; +successful instructions also execute a following sentinel. Scope-zero on +040/060 takes vector 4 before privilege; valid scopes in user mode take vector +8, including cache-zero forms. Older profiles take vector 11. All **131,072** +cases pass in eight `system-cache-encodings` batches. These are architectural +semantic cases; empty fixture caches do not establish physical invalidation, +dirty-line writeback, enabled-MMU translation, bus faults or pipeline timing. + +The separate `CacheEncodings` reference preset copies pinned `cputest.cpp`. +Its two-hunk patch corrects `op_illg_1` only for `F4xx` scope zero on 040/060, +and selects only those words in the ILLEGAL family before reference execution. +The CPU generator and native comparator remain unchanged. Upstream's +`isunsupported` excludes actual CINV/CPUSH operations, so they are explicitly +not promoted as independent reference coverage. Initial exploratory generation +requested those families and produced only ILLEGAL; it is retained in +`artifacts/m6-cache-encodings-inputs/`. The final configuration explicitly +requests only ILLEGAL in `artifacts/m6-cache-encodings-qualified-inputs/`. + +Every selected profile, including A1200's EC020 fixture reuse, executes all 64 +scope-zero words with CCR 0/31 and both privilege states: **256 callbacks, +frames and distinct forms per profile; 2,048 total**. Expectations require the +complete fixed opcode/status distribution with multiplicity one, not just a +snapshot distinct count. All eight directories pass with zero mismatching, +unsupported, untested or masked-SR cases. Twelve fixed examples protect model +distinctions, legal neighboring scopes and rejected foreign/status inputs. +The focused command passes thirteen xUnit tests. All **32** applicable +register/SR/frame/saved-PC comparator corruptions are detected. Evidence: +`artifacts/m6-cache-encodings-reference/`. + +Seven isolated negative controls reject empty profiles, families or input +selections; missing or changed fixtures; and changed copied source or patch. +Each executes exactly one failing audit for its intended reason. The original +inputs remain intact. Evidence: +`artifacts/m6-cache-encodings-reference-controls/controls.json`. An initial +control-helper invocation had a trailing-comma parser error before execution; +it is not counted as reference evidence. + +Pinned generator remains `025b999239800357e95065fe5b9a15ea5b300fa7`; native +runner remains `7a83745d6c6159bc74ab0471578ffc8bc244e66e`. Exact final identities: + +| Input | SHA-256 | +| --- | --- | +| Copied input/reference source | `5f6df41a9b6e5e96088c0c81c65f31d9363a1904e6a37c3001ebf4cf4105ab92` | +| Two-hunk patch | `ef393d99b50198f1c0139de12d890f1f2c3cc0675bee19d22259de698c68e32e` | +| Manifest | `c1e7170e4272e6fcb54d816ecc335016529511320b5d056c151023cd2a3bd41f` | +| Generator executable | `0d7f05c598c611403ba541842967fda9d8b6c87eb62cf39e9ea946ef67da325c` | +| Native library | `d531d1725958be836a90051dc645a6caf3ebccac63c59144282e921a6d342bd5` | + +Preparation uses the existing pinned source/compiler arguments with +`-Preset CacheEncodings`; validation uses +`./scripts/test-copper68k-winuae-qualified-exceptions.ps1 -Preset CacheEncodings +-InputDirectory artifacts/m6-cache-encodings-qualified-inputs +-OutputDirectory `. Requested missing or incomplete audits fail. + +`./scripts/test-copper68k-synthetic-mutations.ps1 -Scope CacheEncodings` +requires the complete 16,384-case 040 batch for each mutation, and both batches +when the original regression is present. Evidence in +`artifacts/m6-cache-encodings-mutations-qualified/mutation-proof.json`: + +| Mutation | Replacement mismatches | Original loop mismatches | +| --- | ---: | ---: | +| Scope-zero vector 11 | 4,096 | Inapplicable: absent old cases | +| Privilege before scope recognition | 2,048 | Inapplicable: absent old cases | +| Neither-cache user privilege bypass | 1,536 | Inapplicable: absent old cases | +| X set instead of preserved | 3,072 | 2,304 | +| Extra extension word consumed | 6,144 | 4,608 | + +The flag and extension controls fail both original and replacement matrices; +source is restored byte-for-byte and rebuilt after each run. An initial harness +attempt placed its TRX check before execution and failed before testing; it is +retained separately in `artifacts/m6-cache-encodings-mutations/`, not counted as +mutation proof. The new enumeration includes every old cache case: push 0/1, +cache 1/2/3, scope 1/2/3, An 0..7, user/supervisor and CCR 0..31, exactly 9,216 +cases per profile. For example the old `68040/CINV/1/cache=1/A0/super=True/ +op=F448/ccr=00` maps to `68040/CINV/scope=1/cache=1/A0/super=True/ccr=00/op=F448` +with the same machine initialization, preserved-state expectation and sentinel. +The complete ordinary matrix additionally covers scope-zero and neither-cache. + +This permits removal of only the duplicate cache loop from +`SyntheticModelSystemTests`. The retained method is named +`Move16TransfersAndBreakpoints`, still exercising 512 breakpoint and 3,072 +MOVE16 scenarios per profile. Its `system-model` gate changes from 12,800 to +3,584, with the separate 16,384 cache gate required for each profile. The focused +consolidated selection passes sixteen tests, 131,072 cache plus 28,672 retained +MOVE16/breakpoint cases. Specialized cache, prefetch, bus-ordering, fault, JIT +and native regressions remain. No production CPU or public API change, package +publication or fresh consumer replay is introduced. The full remaining +restoration/reference/consolidation requirements and `roadmapComplete=false` +remain explicit. + +Copied-report gate controls pass a complete prior-report baseline merged with +the final sixteen cache/MOVE16 reports, then reject a missing 040 cache report, +a reduced 16,383-case cache count and the stale 12,800-case retained-loop count. +Original reports are preserved. This copied baseline proves gate detection, +not a fresh full-suite run. Evidence: +`artifacts/m6-cache-encodings-gate-controls/controls.json`. + +Final full Release CPU validation in `artifacts/m6-cache-encodings-full/` +passes **5,044 tests, eleven optional skips and zero failures**. All nine +qualified WinUAE presets pass their exact selections/counts against matching +CPU/adapter assemblies: CPU SHA-256 +`1926da25083ab44ac860da0d40b14df043c6e5a78356f47922d4e50c19854c0a`, +adapter SHA-256 +`ac2f9767ab33f266998db37e23f6f57eeea9840c845481eb8641d91f9e45a2db`. +The strict gate passes **16,093,014 logical cases in 603 batches**. Fresh pinned +SingleStepTests passes 312,500 cases across 125 files; Musashi passes 536 programs +with 88 explicit exclusions. Binary identities remain unchanged through the +strict gate. Production CPU source remains unchanged since `cb9679d`; the +preceding unpublished `.57` consumer evidence keeps its own original source, +package and binary identities. No new consumer replay or cross-checkpoint binary +equality is claimed. All remaining required roadmap gaps remain in progress. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index eb3091fb..7432fd01 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -122,7 +122,27 @@ identities, replacement proofs and remaining required coverage here as work progresses. No milestone is complete merely because an inventory exists or a partial matrix passes. -Latest milestone-6 reference checkpoint, 2026-10-05: sampled CAS2 W/L passes +Latest milestone-6 checkpoint, 2026-10-05: the exhaustive cache-encoding matrix +passes 131,072 cases across eight profiles, including illegal scope-zero and +neither-cache forms. Independent scope-zero reference qualification passes +2,048 callbacks/frames with exact opcode/status distributions and 32 comparator +controls. A separate copied reference follows the manual's 040/060 vector-4 +rule; the original Basic line-F disagreement remains retained. Five mutations +are detected; shared X-flag and extension-length evidence permits retirement +of the duplicated 9,216-case-per-profile cache loop. MOVE16, breakpoint and +specialized cache/prefetch/bus/JIT/native regressions remain. The +[cache encoding record](COPPER68K_REFERENCE_QUALIFICATION.md#cache-encoding-reference-qualification-and-consolidation-2026-10-05) +maintains exact identities, replacement mapping and qualification limits. +Milestone 6 remains in progress; production CPU source is unchanged. +Full Release CPU validation passes 5,044 tests with eleven optional skips, +all nine qualified WinUAE presets and matching CPU/adapter identities. The strict +gate passes 16,093,014 logical cases in 603 batches; pinned SingleStepTests passes +312,500 cases in 125 files, and Musashi passes 536 programs with 88 exclusions. +Seven specific input/source controls and three copied-report controls reject +their intended defects. `roadmapComplete=false` and the full required-gap +inventory remain explicit. + +Preceding milestone-6 reference checkpoint, 2026-10-05: sampled CAS2 W/L passes 3,180 callbacks, 2,296 unimplemented-integer frames and 3,072 recorded forms across EC020/A1200/020/030/040/060. The CPU already implements the manual's shared-compare operand-1 rule; a copied reference corrects 040 alias ordering. diff --git a/scripts/prepare-copper68k-winuae.ps1 b/scripts/prepare-copper68k-winuae.ps1 index f176dd75..7116c94d 100644 --- a/scripts/prepare-copper68k-winuae.ps1 +++ b/scripts/prepare-copper68k-winuae.ps1 @@ -4,7 +4,7 @@ param( [Parameter(Mandatory)] [string] $GeneratorSource, [Parameter(Mandatory)] [string] $RunnerSource, [Parameter(Mandatory)] [string] $VcVars64, - [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop','Moves','Cas','Cas2')] [string] $Preset = 'Basic', + [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop','Moves','Cas','Cas2','CacheEncodings')] [string] $Preset = 'Basic', [string] $OutputDirectory = 'artifacts/winuae-model-inputs' ) $ErrorActionPreference = 'Stop' @@ -74,6 +74,25 @@ try { & ./gencpu_prog.exe . *> (Join-Path $output 'gencpu-run.log') if ($LASTEXITCODE -ne 0) { throw 'CPU source generation failed' } $testerSource = 'cputest.cpp' + if ($Preset -eq 'CacheEncodings') { + # Correct only the scope-00 architectural exception; select its + # ILLEGAL inputs before execution. Upstream excludes actual cache + # handlers; their semantic outcomes remain synthetic coverage. + $patchPath = Join-Path $PSScriptRoot 'winuae/cache-encodings.patch' + $patch = [IO.File]::ReadAllText($patchPath).Replace("`r`n", "`n") + $source = [IO.File]::ReadAllText((Join-Path $generator 'cputest.cpp')).Replace("`r`n", "`n") + $hunks = @($patch -split "(?m)^@@`n" | Select-Object -Skip 1) + if ($hunks.Count -ne 2) { throw 'Incorrect CacheEncodings patch hunk count' } + foreach ($hunk in $hunks) { + $lines = $hunk.TrimEnd("`n").Split("`n") + $before = (($lines | Where-Object { $_.StartsWith('-') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" + $after = (($lines | Where-Object { $_.StartsWith('+') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" + $source = Patch-Once $source $before $after + } + $testerSource = Join-Path $output 'cputest-cache-encodings.cpp' + [IO.File]::WriteAllText($testerSource, $source) + [IO.File]::WriteAllText((Join-Path $output 'cache-encodings.patch'), $patch) + } if ($Preset -eq 'TraceTraps') { # The pinned generator retained pending trace on 040/060. Qualify # this separate preset against MC68040UM 8.3 / MC68060UM 8.2.6. @@ -178,6 +197,9 @@ void M68KTester_destroy(M68KTesterContext* context) { if ($Preset -eq 'Cas') { $baseIni = $baseIni.Replace('[test=Basic]', '[test=Cas]').Replace('mode=all', 'mode=CAS.B,CAS.W,CAS.L').Replace('feature_sr_mask=0x0000', 'feature_sr_mask=0x2000') } + if ($Preset -eq 'CacheEncodings') { + $baseIni = $baseIni.Replace('[test=Basic]', '[test=CacheEncodings]').Replace('mode=all', 'mode=ILLEGAL').Replace('feature_sr_mask=0x0000', 'feature_sr_mask=0x2000') + } foreach ($model in @( @{id='68000'; cpu='68000'; width=24}, @{id='68010'; cpu='68010'; width=24}, @{id='68EC020'; cpu='68020'; width=24}, @{id='68020'; cpu='68020'; width=32}, @@ -237,6 +259,8 @@ void M68KTester_destroy(M68KTesterContext* context) { MovesSourceSha256=$(if ($Preset -eq 'Moves') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-moves.cpp')).Hash.ToLowerInvariant()} else {$null}) MovesPatchSha256=$(if ($Preset -eq 'Moves') {(Get-FileHash -LiteralPath (Join-Path $output 'moves-encodings.patch')).Hash.ToLowerInvariant()} else {$null}) Cas2InputSourceSha256=$(if ($Preset -eq 'Cas2') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-cas2.cpp')).Hash.ToLowerInvariant()} else {$null}) + CacheEncodingsSourceSha256=$(if ($Preset -eq 'CacheEncodings') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-cache-encodings.cpp')).Hash.ToLowerInvariant()} else {$null}) + CacheEncodingsPatchSha256=$(if ($Preset -eq 'CacheEncodings') {(Get-FileHash -LiteralPath (Join-Path $output 'cache-encodings.patch')).Hash.ToLowerInvariant()} else {$null}) Cas2InputPatchSha256=$(if ($Preset -eq 'Cas2') {(Get-FileHash -LiteralPath (Join-Path $output 'cas2-overlap-inputs.patch')).Hash.ToLowerInvariant()} else {$null}) Cas2SourceSha256=$(if ($Preset -eq 'Cas2') {(Get-FileHash -LiteralPath (Join-Path $output 'gencpu-cas2.cpp')).Hash.ToLowerInvariant()} else {$null}) Cas2PatchSha256=$(if ($Preset -eq 'Cas2') {(Get-FileHash -LiteralPath (Join-Path $output 'cas2-compare-alias.patch')).Hash.ToLowerInvariant()} else {$null}) diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index 442d4113..b0e4e77c 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','LowPowerStop')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -88,7 +88,7 @@ $mutations += @( @{name='chk2-boundary-z'; file=$advanced; before=' State.SetFlag(M68kCpuState.Zero, value == lower || value == upper);'; after=' State.SetFlag(M68kCpuState.Zero, false);'; group='CheckBoundsValuesFlagsFormsAndRegisterAliases'; milestone=5}, @{name='cmp2-address-width'; file=$advanced; before=' var value = useAddressRegister ? unchecked((int)State.A[register]) : SignExtendForSize(State.D[register], size);'; after=' var value = SignExtendForSize(useAddressRegister ? State.A[register] : State.D[register], size);'; group='CheckBoundsValuesFlagsFormsAndRegisterAliases'; milestone=5}, @{name='rte-throwaway'; file=$advanced; before=' if (format == 1) continue; // Throwaway frame can select another stack.'; after=' // Mutant: accept throwaway as final frame.'; group='RteFramesPrivilegeStackSelectionAndInvalidFormats'; milestone=5}, - @{name='move16-postincrement'; file='Copper68k/M68kAdvancedTimingInterpreter.System.cs'; before=' if (form == 4 || form < 2) WriteGeneralRegister(true, register, unchecked(State.A[register] + 16));'; after=' if (form == 4 || form < 2) WriteGeneralRegister(true, register, unchecked(State.A[register] + 4));'; group='Move16CacheInstructionsAndBreakpoints'; milestone=5; model='68040'}, + @{name='move16-postincrement'; file='Copper68k/M68kAdvancedTimingInterpreter.System.cs'; before=' if (form == 4 || form < 2) WriteGeneralRegister(true, register, unchecked(State.A[register] + 16));'; after=' if (form == 4 || form < 2) WriteGeneralRegister(true, register, unchecked(State.A[register] + 4));'; group='Move16TransfersAndBreakpoints'; milestone=5; model='68040'}, @{name='cacr-clear-readback'; file=$advanced; before='? value & ~0x0C0Cu : value; // Clear commands always read as zero.'; after='? value : value; // Mutant: clear commands read back set.'; group='MovecControlInventoryMasksPrivilegeAndAllGeneralRegisters'; milestone=5} ) $mutations += @( @@ -141,6 +141,55 @@ $mutations += @( after=' if (false)'; group='SyntheticLowPowerStopTests'; milestone=6; model='68060'; lpstop=$true; legacyTest='Move16CacheInstructionsBreakpointsAndLowPowerStop'; legacyFile='Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs'; legacyModel='68060'} ) +$mutations += @( + @{name='cache-scope-linef'; file=$advanced; before=@' + if (scope == 0) + { + RaiseFormat0Exception(4, pc, M68kInstructionTimingKey.IllegalInstruction); +'@; after=@' + if (scope == 0) + { + RaiseFormat0Exception(11, pc, M68kInstructionTimingKey.LineFException); +'@; group='EveryCacheOpcodeIncludesInvalidScopeAndNeitherCache'; milestone=6; model='68040'; cache=$true}, + @{name='cache-scope-privilege-priority'; file=$advanced; before=@' + var scope = (opcode >> 3) & 3; + if (scope == 0) +'@; after=@' + var scope = (opcode >> 3) & 3; + if ((State.StatusRegister & M68kCpuState.Supervisor) == 0) + { RaiseFormat0Exception(8, pc, M68kInstructionTimingKey.PrivilegeViolation); return true; } + if (scope == 0) +'@; group='EveryCacheOpcodeIncludesInvalidScopeAndNeitherCache'; milestone=6; model='68040'; cache=$true}, + @{name='cache-neither-privilege'; file=$advanced; before=@' + if ((State.StatusRegister & M68kCpuState.Supervisor) == 0) + { + RaiseFormat0Exception(8, pc, M68kInstructionTimingKey.PrivilegeViolation); + return true; + } + var caches = (opcode >> 6) & 3; +'@; after=@' + if ((State.StatusRegister & M68kCpuState.Supervisor) == 0 && ((opcode >> 6) & 3) != 0) + { + RaiseFormat0Exception(8, pc, M68kInstructionTimingKey.PrivilegeViolation); + return true; + } + var caches = (opcode >> 6) & 3; +'@; group='EveryCacheOpcodeIncludesInvalidScopeAndNeitherCache'; milestone=6; model='68040'; cache=$true}, + @{name='cache-preserve-x'; file=$advanced; before=' var caches = (opcode >> 6) & 3;'; after=@' + State.SetFlag(M68kCpuState.Extend, true); + var caches = (opcode >> 6) & 3; +'@; group='EveryCacheOpcodeIncludesInvalidScopeAndNeitherCache'; milestone=6; model='68040'; cache=$true; + legacyTest='Move16CacheInstructionsAndBreakpoints'; legacyFile='Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs'; legacyModel='68040'}, + @{name='cache-extension-length'; file=$advanced; before=@' + _ = FetchWord(); + var scope = (opcode >> 3) & 3; +'@; after=@' + _ = FetchWord(); + _ = FetchWord(); + var scope = (opcode >> 3) & 3; +'@; group='EveryCacheOpcodeIncludesInvalidScopeAndNeitherCache'; milestone=6; model='68040'; cache=$true; + legacyTest='Move16CacheInstructionsAndBreakpoints'; legacyFile='Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs'; legacyModel='68040'} +) if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } if ($Scope -eq 'Logical') { $mutations = @($mutations | Where-Object { $_.milestone -eq 4 }) } @@ -148,6 +197,7 @@ if ($Scope -eq 'Control') { $mutations = @($mutations | Where-Object { $_.milest if ($Scope -eq 'Consolidation') { $mutations = @($mutations | Where-Object { $_.milestone -eq 6 }) } if ($Scope -eq 'Rte040') { $mutations = @($mutations | Where-Object { $_.odd }) } if ($Scope -eq 'LowPowerStop') { $mutations = @($mutations | Where-Object { $_.lpstop }) } +if ($Scope -eq 'CacheEncodings') { $mutations = @($mutations | Where-Object { $_.cache }) } $saved = @{} foreach ($mutation in $mutations) { $path = Join-Path $repo $mutation.file @@ -208,6 +258,15 @@ try { throw 'LPSTOP mutation did not execute its complete two-batch selection' } } + if ($mutation.cache) { + [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + $expectedBatches = 1 + [int][bool]$legacyPresent + if ($trx.TestRun.ResultSummary.Counters.executed -ne $expectedBatches -or $batches.Count -ne $expectedBatches -or + @($batches | Where-Object { $_.model -eq '68040' -and $_.group -eq 'system-cache-encodings' -and $_.logicalCases -eq 16384 }).Count -ne 1 -or + ($legacyPresent -and @($batches | Where-Object { $_.model -eq '68040' -and $_.group -eq 'system-model' -and $_.logicalCases -eq 12800 }).Count -ne 1)) { + throw 'Cache mutation did not execute its complete selection' + } + } if ($legacyPresent) { [xml]$legacyTrx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if (@($legacyTrx.TestRun.Results.UnitTestResult | Where-Object { ($_.testName.EndsWith($mutation.legacyTest) -or $_.testName.Contains($mutation.legacyTest + '(')) -and $_.outcome -eq 'Failed' }).Count -ne 1) { throw 'Original regression did not detect the same mutation' } diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index c1b7688b..3374e9bf 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -111,7 +111,8 @@ try { 'system-bounds'=$(if ($model -in @('68000','68010')) {11828} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {12488}); 'system-callm'=$(if ($model -in @('68000','68010')) {10832} elseif ($model -in @('68020','68EC020','A1200')) {11000} elseif ($model -in @('68030','68040','68060')) {10964}); 'system-interrupt'=$(if ($model -in @('68000')) {224} elseif ($model -in @('68010','68060')) {226} elseif ($model -in @('68020','68030','68040','68EC020','A1200')) {418}); - 'system-model'=12800; + 'system-model'=3584; + 'system-cache-encodings'=16384; 'system-lpstop-values'=17024; 'system-movec'=2432; 'system-moves'=$(if ($model -in @('68000','68010')) {10656} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {11052}); @@ -257,6 +258,7 @@ try { m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks and direct odd-PC returns with format-2 address errors and pending-exception priority; chained odd-PC saved-SR provenance, CP context transfer, detailed fault validation, real fault entry and physical timing remain unqualified'; m68060LowPowerStopScope='Every unrecognized second opcode word in both privilege modes; fixed encoding, status, CCR and incoming trace cases across all profiles. Opcode-PC exception frames follow MC68060UM 8.2.4/5; physical broadcast, pins and ordinary STOP S-clear software disagreement remain unqualified'; + cacheInstructionEncodingScope='Every F4xx opcode, all CCRs and both privilege states across eight profiles; scope zero is illegal on 040/060, including neither-cache forms, before privilege effects. Empty fixture caches qualify architectural state and exception outcomes, not physical invalidation, writeback, bus faults or timing'; m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; marked private word-MOVE/MOVEA images resume their faulted cycle; other opaque restart/input state, long transfers, RMW and physical timing remain unqualified'; m68000DoubleFaultScope='Address-error entry/handler faults; reset-only recovery; external BERR/reset-vector faults unavailable through the current public bus API'; unavailableSystemCoverage=@('010 long/non-MOVE/external bus-fault restart and 020/030 internal restart', '020/030 coprocessor midinstruction restoration', '040 access-fault CP context transfer, detailed frame-validation/fault entry and writeback-handler qualification', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands'); diff --git a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 index 375a706b..2dda9539 100644 --- a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 +++ b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 @@ -1,7 +1,7 @@ #requires -Version 7.0 [CmdletBinding()] param( - [ValidateSet('TrapBounds','Breakpoints','WordDivision','LowPowerStop','Moves','Cas','Cas2')] [string] $Preset = 'TrapBounds', + [ValidateSet('TrapBounds','Breakpoints','WordDivision','LowPowerStop','Moves','Cas','Cas2','CacheEncodings')] [string] $Preset = 'TrapBounds', [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $InputDirectory, [string] $NativeLibrary, [string] $OutputDirectory = 'artifacts/winuae-trap-bounds-audit' @@ -14,9 +14,9 @@ $library = (Resolve-Path -LiteralPath $NativeLibrary).Path $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } New-Item -ItemType Directory -Path $output | Out-Null -$key = switch ($Preset) {'TrapBounds' {'TRAP_BOUNDS'} 'Breakpoints' {'BREAKPOINT'} 'WordDivision' {'WORD_DIVISION'} 'LowPowerStop' {'LPSTOP'} 'Moves' {'MOVES'} 'Cas' {'CAS'} 'Cas2' {'CAS2'}} -$testName = switch ($Preset) {'TrapBounds' {'WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled'} 'Breakpoints' {'WinUaeBreakpointExceptionsAcrossSelectedModelsWhenEnabled'} 'WordDivision' {'WinUaeWordDivisionAcrossSelectedModelsWhenEnabled'} 'LowPowerStop' {'WinUaeLowPowerStopExceptionsWhenEnabled'} 'Moves' {'WinUaeMovesAcrossSelectedModelsWhenEnabled'} 'Cas' {'WinUaeCasAcrossAdvancedModelsWhenEnabled'} 'Cas2' {'WinUaeCas2AcrossAdvancedModelsWhenEnabled'}} -$reportName = switch ($Preset) {'TrapBounds' {'winuae-trap-bounds-audit.json'} 'Breakpoints' {'winuae-breakpoint-audit.json'} 'WordDivision' {'winuae-word-division-audit.json'} 'LowPowerStop' {'winuae-lpstop-audit.json'} 'Moves' {'winuae-moves-audit.json'} 'Cas' {'winuae-cas-audit.json'} 'Cas2' {'winuae-cas2-audit.json'}} +$key = switch ($Preset) {'TrapBounds' {'TRAP_BOUNDS'} 'Breakpoints' {'BREAKPOINT'} 'WordDivision' {'WORD_DIVISION'} 'LowPowerStop' {'LPSTOP'} 'Moves' {'MOVES'} 'Cas' {'CAS'} 'Cas2' {'CAS2'} 'CacheEncodings' {'CACHE_ENCODINGS'}} +$testName = switch ($Preset) {'TrapBounds' {'WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled'} 'Breakpoints' {'WinUaeBreakpointExceptionsAcrossSelectedModelsWhenEnabled'} 'WordDivision' {'WinUaeWordDivisionAcrossSelectedModelsWhenEnabled'} 'LowPowerStop' {'WinUaeLowPowerStopExceptionsWhenEnabled'} 'Moves' {'WinUaeMovesAcrossSelectedModelsWhenEnabled'} 'Cas' {'WinUaeCasAcrossAdvancedModelsWhenEnabled'} 'Cas2' {'WinUaeCas2AcrossAdvancedModelsWhenEnabled'} 'CacheEncodings' {'WinUaeCacheScopeZeroAcrossSelectedModelsWhenEnabled'}} +$reportName = switch ($Preset) {'TrapBounds' {'winuae-trap-bounds-audit.json'} 'Breakpoints' {'winuae-breakpoint-audit.json'} 'WordDivision' {'winuae-word-division-audit.json'} 'LowPowerStop' {'winuae-lpstop-audit.json'} 'Moves' {'winuae-moves-audit.json'} 'Cas' {'winuae-cas-audit.json'} 'Cas2' {'winuae-cas2-audit.json'} 'CacheEncodings' {'winuae-cache-encodings-audit.json'}} $filter = "FullyQualifiedName~$testName" $expectedTests = 1 if ($Preset -eq 'WordDivision') { @@ -39,6 +39,10 @@ if ($Preset -eq 'Cas2') { $filter += '|FullyQualifiedName~M68kWinUaeCas2EncodingTests' $expectedTests = 12 } +if ($Preset -eq 'CacheEncodings') { + $filter += '|FullyQualifiedName~M68kWinUaeCacheScopeEncodingTests' + $expectedTests = 13 +} $settings = @{ ('COPPER68K_RUN_WINUAE_' + $key + '_AUDIT') = '1' ('COPPER68K_WINUAE_' + $key + '_PATH') = $root diff --git a/scripts/winuae/cache-encodings.patch b/scripts/winuae/cache-encodings.patch new file mode 100644 index 00000000..e42062e4 --- /dev/null +++ b/scripts/winuae/cache-encodings.patch @@ -0,0 +1,21 @@ +--- cputest.cpp ++++ cputest-cache-encodings.cpp +@@ + uae_u32 REGPARAM2 op_illg_1(uae_u32 opcode) + { +- if ((opcode & 0xf000) == 0xf000) { ++ // M68000PM 6-4/9 and MC68060UM D-12: scope 00 is illegal. ++ if (cpu_lvl >= 4 && (opcode & 0xff18) == 0xf400) { ++ test_exception = 4; ++ } else if ((opcode & 0xf000) == 0xf000) { + if (currprefs.cpu_model == 68030) { +@@ + // match requested mnemonic + if (dp->mnemo != lookup->mnemo) + continue; ++ // Keep only cache scope-00 encodings in this preset's ILLEGAL ++ // family. Actual cache handlers stay excluded by upstream policy. ++ if (lookup->mnemo == i_ILLG && (opcode & 0xff18) != 0xf400) ++ continue; + + if (lookup->mnemo != i_ILLG) { From 7c14dbeeac1ef398d8106eb9ed76fc52db17af50 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 19:45:21 +0300 Subject: [PATCH 041/211] Correct 68040 RTE validation access-fault frames --- Copper68k.Tests/Synthetic/README.md | 15 ++ .../SyntheticM68040AccessFrameAuditTests.cs | 4 +- .../SyntheticM68040RteValidationFaultTests.cs | 150 ++++++++++++++++++ Copper68k.Tests/Synthetic/SyntheticMachine.cs | 6 +- Copper68k/M68040Support.cs | 41 ++++- .../M68kAdvancedTimingInterpreter.Rte.cs | 26 ++- Copper68k/M68kAdvancedTimingInterpreter.cs | 6 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 111 +++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 28 ++++ scripts/test-copper68k-040-access-frames.ps1 | 47 ++++-- .../test-copper68k-synthetic-mutations.ps1 | 18 ++- scripts/test-copper68k-synthetic.ps1 | 5 +- 12 files changed, 431 insertions(+), 26 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 0c7fd8c5..77c472d0 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -258,6 +258,21 @@ dispatch followed by RTE fallback. Run the four discriminating mutations with The command checks exact selections, combinations, counts and source/assembly identities. Existing outputs must be validated with `-ValidateReportsOnly` or a fresh output directory used. It cannot pass merely by fixing normal/trace return. +Two ordinary-CI physical-map batches additionally cover supervisor-stack RTE +validation faults and handler returns (223,872 phases). The direct matrix uses +all CCRs; one/two-throwaway chains use CCR 0/31. Every byte of each SR, PC, format, +SSW and required continuation-EA read is rejected in turn, with T0/T1/no trace, +even/odd data-stack addresses and zero/nonzero VBR. The format-7 frame preserves +the incomplete original frame and committed stack selection. Expectations mask +only undefined SSW X, EA and invalid writeback/push data. The recording bus +rejects the original operand range through the existing internal physical-map +interface with translation disabled; it does not synthesize a CPU exception. +Run the five fault-frame/width/address/PC/continuation-read mutations with +`./scripts/test-copper68k-synthetic-mutations.ps1 -Scope RteValidationFault`. +User-tail validation, faults during internal restoration or fault entry, other +instructions' fault frames and software writeback handlers remain required. +These two phases do not qualify re-execution of the repaired original RTE, +enabled MMU operation, a public BERR interface or physical timing. See the [reference record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-access-frame-restoration-discovery-2026-10-05) for failed-before evidence, mutation controls and the remaining scope. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index d2250868..17a15433 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -32,7 +32,9 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() foreach (var form in new[] { "frame-validation-fault", "odd-PC-chained-SR-provenance", "real-access-fault-entry", "writeback-handler", "CP-context-transferred-vector" }) report.Record($"68040/RTE/format7/{form}/bank={bank}/op=4E73/ccr={ccr:X2}", "untested", - "Required fault/context protocol has no independently qualified execution fixture yet"); + form == "frame-validation-fault" + ? "Supervisor-stack physical validation reads are covered separately; user-tail validation and internal-restoration/double-fault distinctions remain required" + : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs new file mode 100644 index 00000000..41b47f38 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs @@ -0,0 +1,150 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 8.4.6.7: validation faults preserve the incomplete frame. +// Physical map rejection exercises the real logical bus with translation off. +// This does not inject a production exception or calculate an EA with CPU code. +public sealed class SyntheticM68040RteValidationFaultTests(ITestOutputHelper output) +{ + private const uint Handler = 0x9020, Target = 0x6000; + private static readonly string[] SupervisorBanks = ["ISP", "MSP"]; + private static readonly string[] Forms = ["format0", "format2", "format3", "invalid4", "invalid15", + "normal", "CM", "CT", "CU", "CP"]; + + [Fact, Trait("Suite", "Synthetic")] + public void DirectValidationFaultsPreserveEveryCcrAndOriginalFrame() => Audit(false); + + [Fact, Trait("Suite", "Synthetic")] + public void ValidationFaultsRetainCommittedThrowawaysAndStackSelection() => Audit(true); + + private void Audit(bool chained) + { + var bus = new ValidationFaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + var report = new CoverageBatch("68040", chained ? "rte-validation-physical-chained" : "rte-validation-physical-direct"); + var paths = new List(); + foreach (var start in SupervisorBanks) + { + if (!chained) paths.Add([start]); + else foreach (var tail in SupervisorBanks) + { + paths.Add([start, tail]); + foreach (var middle in SupervisorBanks) paths.Add([start, middle, tail]); + } + } + foreach (var path in paths) + foreach (var ccr in chained ? new[] { 0, 31 } : Enumerable.Range(0, 32)) + foreach (var trace in new ushort[] { 0, 0x8000, 0x4000 }) + foreach (var alignment in new uint[] { 0, 1 }) + foreach (var vbr in new uint[] { 0, 0x10000 }) + foreach (var form in Forms) + foreach (var (offset, width) in Reads(form)) + for (var faultByte = 0; faultByte < width; faultByte++) + Run(m, bus, report, path, ccr, trace, alignment, vbr, form, offset, width, faultByte); + report.Complete(output); + } + + internal static IEnumerable<(uint Offset, int Width)> Reads(string form) + { + yield return (0, 2); yield return (2, 4); yield return (6, 2); + if (form.StartsWith("format") || form.StartsWith("invalid")) yield break; + yield return (12, 2); + if (form != "normal") yield return (8, 4); + } + + private static void Run(SyntheticMachine m, ValidationFaultBus bus, CoverageBatch report, + string[] path, int ccr, ushort trace, uint alignment, uint vbr, string form, uint offset, int width, int faultByte) + { + bus.Disarm(); m.Reset(ccr); + var pointers = new Dictionary { ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; + foreach (var at in pointers.Values) + for (var n = -76; n < 80; n++) m.InitializePhysical(unchecked(at + (uint)n), (uint)(n ^ 0x5a), 1); + for (var n = 0; n < path.Length - 1; n++) + { + var at = pointers[path[n]]; + m.InitializePhysical(at, Status(path[n + 1], trace, ccr ^ 31), 2); + m.InitializePhysical(at + 2, 0xdead0001u + (uint)n * 2, 4); + m.InitializePhysical(at + 6, 0x1024, 2); + pointers[path[n]] += 8; + } + var tail = path[^1]; var frame = pointers[tail]; + var format = form.StartsWith("format") ? int.Parse(form[6..]) : form.StartsWith("invalid") ? int.Parse(form[7..]) : 7; + var continuation = form switch { "CM" => 0x1000, "CT" => 0x2000, "CU" => 0x4000, "CP" => 0x8000, _ => 0 }; + // Deliberately different S/M/CCR/trace bits: a fault must not install this SR. + m.InitializePhysical(frame, 0x801fu ^ (uint)ccr, 2); + m.InitializePhysical(frame + 2, Target, 4); + m.InitializePhysical(frame + 6, (uint)(format << 12) | 8, 2); + m.InitializePhysical(frame + 8, 0x12345678, 4); + m.InitializePhysical(frame + 12, (uint)continuation | 0x0105, 2); + m.InitializePhysical(Handler, 0x4e73, 2); m.InitializePhysical(Handler + 2, 0x7001, 2); + m.InitializePhysical(vbr + 8, Handler, 4); + _ = SyntheticExecution.Prepare(m, [0x4e73]); + m.Core.State.SetInterruptStackPointer(0x4700 + alignment); + m.Core.State.SetMasterStackPointer(0x7400 + alignment); + m.Core.State.StatusRegister = Status(path[0], trace, ccr); + m.Core.State.VectorBaseRegister = vbr; + if (form == "CU") m.Core.State.M68040PendingFpuExceptions.Begin(2, 11, Target); + if (form == "CP") m.Core.State.M68040PendingFpuExceptions.Begin(3, 49, Target); + var e = ArchitecturalExpectation.Capture(m); + var priorSr = Status(tail, trace, path.Length > 1 ? ccr ^ 31 : ccr); + var accessFrame = frame - 60; + var sequence = m.Core.State.ExceptionSequence; + pointers[tail] = accessFrame; + SetStacks(e, pointers, tail, (ushort)(priorSr & ~0xc000)); + e.Pc = Handler; e.ExceptionVector = 2; + e.ControlChecks["exception entries"] = (s => s.ExceptionSequence, sequence + 1); + e.ControlChecks["saved RTE PC"] = (s => s.LastExceptionStackedProgramCounter, SyntheticMachine.Code); + e.ControlChecks["saved pre-validation SR"] = (s => s.LastExceptionStatusRegister, priorSr); + e.ControlChecks["fault rejected once"] = (_ => (uint)bus.Rejected.Count, 1); + e.ControlChecks["original access address"] = (_ => bus.Rejected.Count == 1 ? bus.Rejected[0].Address : uint.MaxValue, frame + offset); + e.ControlChecks["original access width"] = (_ => bus.Rejected.Count == 1 ? (uint)bus.Rejected[0].Width : 0, (uint)width); + e.ControlChecks["discarded PCs never fetched"] = (_ => bus.Accesses.Any(a => a.Address is 0xdead0001 or 0xdead0003) ? 1u : 0u, 0); + e.ControlChecks["handler sentinel never executed"] = (s => s.D[0], e.D[0]); + if (form is "CU" or "CP") e.ControlChecks["pending delivery retained"] = + (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) != null ? 1u : 0u, 1); + e.Write(accessFrame, priorSr, 2, m.Model); e.Write(accessFrame + 2, SyntheticMachine.Code, 4, m.Model); + e.Write(accessFrame + 6, 0x7008, 2, m.Model); + // EA/invalid WB data are undefined. Verify defined status/address fields, + // all untouched original-frame bytes and all neighboring memory instead. + for (uint n = 8; n < 60; n++) e.MemoryMasks[accessFrame + n] = 0; + e.Write(accessFrame + 12, width == 2 ? 0x0145u : 0x0105u, 2, m.Model); + e.MemoryMasks[accessFrame + 12] = 0xff; e.MemoryMasks[accessFrame + 13] = 0x7f; // SSW X undefined + foreach (var at in new uint[] { 14, 16, 18 }) + { + e.Write(accessFrame + at, 0, 2, m.Model); + e.MemoryMasks[accessFrame + at + 1] = 0x80; // WB valid bit; no pending write + } + e.Write(accessFrame + 20, frame + offset, 4, m.Model); + for (uint n = 20; n < 24; n++) e.MemoryMasks[accessFrame + n] = 0xff; + bus.Arm(frame + offset + (uint)faultByte); + var id = $"68040/RTE/validation-physical/{form}/path={string.Join('-', path)}/T={trace:X4}/align={alignment}/VBR={vbr:X8}/read={offset}:{width}/fault-byte={faultByte}/op=4E73/ccr={ccr:X2}"; + if (!SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/fault-entry")) + { report.Record(id + "/handler-return", "untested", "Validation fault entry prerequisite failed"); return; } + pointers[tail] = frame; + SetStacks(e, pointers, tail, priorSr); e.Pc = SyntheticMachine.Code; e.ExceptionVector = null; + SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/handler-return"); + } + + private static ushort Status(string bank, ushort trace, int ccr) => (ushort)((bank == "MSP" ? 0x3000 : 0x2000) | trace | ccr); + private static void SetStacks(ArchitecturalExpectation e, Dictionary pointers, string bank, ushort sr) + { + e.Sr = sr; e.A[7] = pointers[bank]; e.InactiveStackPointer = 0x7800; e.MasterStackPointer = pointers["MSP"]; + e.ControlChecks["ISP"] = (s => s.InterruptStackPointer, pointers["ISP"]); + e.ControlChecks["MSP"] = (s => s.MasterStackPointer, pointers["MSP"]); + } + + internal sealed class ValidationFaultBus : SparseRecordingBus, IM68kPhysicalAddressMap + { + private uint? faultByte; + public List<(uint Address, int Width)> Rejected { get; } = []; + public void Disarm() { faultByte = null; Rejected.Clear(); } + public void Arm(uint at) { faultByte = at; Rejected.Clear(); } + public bool IsCpuPhysicalAddressMapped(uint address, int byteCount, M68kBusAccessKind kind) + { + if (kind != M68kBusAccessKind.CpuDataRead || faultByte is not { } at || unchecked(at - address) >= byteCount) return true; + faultByte = null; Rejected.Add((address, byteCount)); return false; + } + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticMachine.cs b/Copper68k.Tests/Synthetic/SyntheticMachine.cs index b2bb7081..31d4a575 100644 --- a/Copper68k.Tests/Synthetic/SyntheticMachine.cs +++ b/Copper68k.Tests/Synthetic/SyntheticMachine.cs @@ -27,7 +27,7 @@ public IM68kCore Create(SparseRecordingBus bus) => A1200 internal readonly record struct BusAccess(uint Address, int Width, bool Write, uint Value, M68kBusAccessKind Kind); -internal sealed class SparseRecordingBus : IM68kBus, IM68kModuleAccessBus +internal class SparseRecordingBus : IM68kBus, IM68kModuleAccessBus { public Dictionary Memory { get; } = []; public List Accesses { get; } = []; @@ -69,10 +69,10 @@ private void Write(uint address, uint value, int width, M68kBusAccessKind kind) public void ResetExternalDevices(long cycle) { DeviceResets++; } } -internal sealed class SyntheticMachine(ModelSpec model) +internal sealed class SyntheticMachine(ModelSpec model, SparseRecordingBus? bus = null) { public const uint Code = 0x1000; - public SparseRecordingBus Bus { get; } = new(); + public SparseRecordingBus Bus { get; } = bus ?? new(); private IM68kCore? core; public IM68kCore Core => core ??= Model.Create(Bus); public ModelSpec Model { get; } = model; diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index 1b92abb6..83342727 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -1859,7 +1859,9 @@ internal readonly record struct M68040MmuFault( M68kBusAccessKind AccessKind, bool Write, uint Status, - uint? StackedProgramCounter = null); + uint? StackedProgramCounter = null, + int ByteCount = 0, + bool RteFrameValidation = false); internal sealed class UnsupportedM68040InstructionException : M68kEmulationException { @@ -2143,7 +2145,7 @@ private uint AcceptPhysicalAddress( return address; } - return ThrowPhysicalAddressFault(address, accessKind, write); + return ThrowPhysicalAddressFault(address, accessKind, write, byteCount); } [MethodImpl(MethodImplOptions.NoInlining)] @@ -2179,12 +2181,13 @@ private uint TranslateEnabled(uint address, M68kBusAccessKind accessKind, bool w private uint ThrowPhysicalAddressFault( uint address, M68kBusAccessKind accessKind, - bool write) + bool write, + int byteCount) { var supervisor = (_state.StatusRegister & M68kCpuState.Supervisor) != 0; throw new M68040MmuFaultException( WithStackedProgramCounter( - CreatePhysicalAddressFault(address, accessKind, write, supervisor), + CreatePhysicalAddressFault(address, accessKind, write, supervisor) with { ByteCount = byteCount }, address, accessKind)); } @@ -4222,7 +4225,10 @@ private void RaiseMmuFault(M68040MmuFault fault) (fault.AccessKind == M68kBusAccessKind.CpuInstructionFetch ? fault.LogicalAddress : State.LastInstructionProgramCounter); - RaiseFormat0Exception(VectorBusError, stackedProgramCounter, M68kInstructionTimingKey.IllegalInstruction); + if (fault.RteFrameValidation) + RaiseRteValidationAccessFault(fault, stackedProgramCounter); + else + RaiseFormat0Exception(VectorBusError, stackedProgramCounter, M68kInstructionTimingKey.IllegalInstruction); } finally { @@ -4230,6 +4236,31 @@ private void RaiseMmuFault(M68040MmuFault fault) } } + private void RaiseRteValidationAccessFault(M68040MmuFault fault, uint instructionPc) + { + // MC68040UM 8.4.6.7: preserve the frame being validated. Only + // pre-commit reads carry this marker; faults in exception delivery + // or internal restoration must not retry partially completed work. + var savedSr = State.StatusRegister; + State.RecordException(VectorBusError, instructionPc, savedSr); + State.StatusRegister = (ushort)((savedSr | M68kCpuState.Supervisor) & ~0xc000); + // No pending pipeline writebacks in this synchronous read path. + // Undefined EA/writeback/push data use zero as an implementation + // convention, not a hardware-defined result. ATC/MA/LK/TT are clear; + // SSW SIZE describes the original read, TM its data function code. + for (var n = 0; n < 9; n++) PushLong(0); + PushLong(fault.LogicalAddress); + PushWord(0); PushWord(0); PushWord(0); + var modifier = (savedSr & M68kCpuState.Supervisor) != 0 ? 5 : 1; + PushWord((ushort)(0x0100 | (fault.ByteCount == 2 ? 0x0040 : 0) | modifier)); + PushLong(0); + PushWord(0x7008); + PushLong(instructionPc); + PushWord(savedSr); + State.ProgramCounter = ReadLong(State.VectorBaseRegister + 8); + CompleteTiming(M68kInstructionTimingKey.IllegalInstruction); + } + private uint ReadPhysicalLong(uint physicalAddress) { var cycle = State.Cycles; diff --git a/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs b/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs index 6710497c..b7d52ff8 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs @@ -6,10 +6,32 @@ internal partial class M68kAdvancedTimingInterpreter private (uint Pc, uint Address)? _m68040MovemContinuation; internal bool HasPendingM68040MovemContinuation => _m68040MovemContinuation.HasValue; + private ushort ReadRteFrameWord(uint address) + { + try { return ReadWord(address); } + catch (M68040MmuFaultException ex) when (IsM68040PhysicalRteValidationFault(ex.Fault)) + { + throw new M68040MmuFaultException(ex.Fault with { RteFrameValidation = true }); + } + } + + private uint ReadRteFrameLong(uint address) + { + try { return ReadLong(address); } + catch (M68040MmuFaultException ex) when (IsM68040PhysicalRteValidationFault(ex.Fault)) + { + throw new M68040MmuFaultException(ex.Fault with { RteFrameValidation = true }); + } + } + + private bool IsM68040PhysicalRteValidationFault(M68040MmuFault fault) => + _profile.Model == M68kAcceleratorModel.M68040 && !State.M68040Mmu.Enabled && + fault.ByteCount is 2 or 4 && !fault.Write && fault.AccessKind == M68kBusAccessKind.CpuDataRead; + private bool TryRestoreM68040AccessFrame(uint frame, ushort sr, uint pc) { var priorSr = State.StatusRegister; - var continuation = ReadWord(frame + 12) & 0xf000; + var continuation = ReadRteFrameWord(frame + 12) & 0xf000; // Multiple continuation bits are undefined (MC68040UM 8.4.6.7). if (continuation is not (0 or 0x1000 or 0x2000 or 0x4000 or 0x8000)) return false; var pending = continuation == 0x8000 @@ -20,7 +42,7 @@ private bool TryRestoreM68040AccessFrame(uint frame, ushort sr, uint pc) // reselect a vector from handler-modified FPCR/FPSR registers. if (continuation == 0x8000 && pending == null) throw new UnsupportedM68kTimingException(0x4e73, State.LastInstructionProgramCounter, _profile); - var address = continuation == 0 ? 0u : ReadLong(frame + 8); + var address = continuation == 0 ? 0u : ReadRteFrameLong(frame + 8); State.SetActiveStackPointer(unchecked(frame + 60)); State.StatusRegister = sr; State.ProgramCounter = pc; diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index 3c2136cc..097585a4 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -8655,9 +8655,9 @@ private void ExecuteRte() { var framePointer = State.A[7]; var priorSr = State.StatusRegister; - var restoredStatus = ReadWord(framePointer); - var restoredPc = ReadLong(framePointer + 2); - var format = ReadWord(framePointer + 6) >> 12; + var restoredStatus = ReadRteFrameWord(framePointer); + var restoredPc = ReadRteFrameLong(framePointer + 2); + var format = ReadRteFrameWord(framePointer + 6) >> 12; if (format == 7 && _profile.Model == M68kAcceleratorModel.M68040 && TryRestoreM68040AccessFrame(framePointer, restoredStatus, restoredPc)) return; var size = format switch { 0 or 1 => 8u, 2 => 12u, 3 when _profile.Model == M68kAcceleratorModel.M68040 => 12u, _ => 0u }; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index d3eb0bfe..6c25bfde 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -3718,3 +3718,114 @@ strict gate. Production CPU source remains unchanged since `cb9679d`; the preceding unpublished `.57` consumer evidence keeps its own original source, package and binary identities. No new consumer replay or cross-checkpoint binary equality is claimed. All remaining required roadmap gaps remain in progress. + +### 040 physical RTE validation fault correction — 2026-10-05 + +[MC68040UM 8.4.6.7](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +requires a format-7 access frame for a fault during RTE frame validation, +preserving the incomplete original frame. Sections 8.4.6.2/4 and table 7-1 +define transfer metadata and size encodings. The expectation source is the +processor manual; no hardware oracle or WinUAE fault execution is claimed. + +The existing internal physical-address map can reject a read even with the MMU +disabled. Previously that path lost the original access width and built an +eight-byte format-0 vector-2 frame. The new fixtures reproduce **81,408 direct +and 30,528 chained entry mismatches**, with the corresponding handler returns +explicitly untested after the failed prerequisite. Evidence: +`artifacts/m6-rte-validation-fault-before/`. No production exception is injected +by the fixture: a one-shot map rejection reaches the real logical bus through +the public CPU factory. + +The correction retains physical access width in internal fault metadata and +marks only pre-commit RTE validation reads. Fault entry stacks 60 bytes, with +the original RTE PC, live pre-validation SR, read/size/data-space attributes, +the original transfer address and no pending writebacks. Undefined EA and +writeback/push data use zero as an implementation convention. Successfully +consumed throwaways retain their stack-pointer and SR effects; no instruction +is automatically retried after partial side effects. Enabled-MMU faults and +other instructions' existing fault paths are unchanged and remain unqualified. +No public package API changes are introduced. The existing exception timing +key remains `IllegalInstruction`; physical timing is not qualified. + +`SyntheticM68040RteValidationFaultTests` provides two ordinary-CI batches: + +| Group | Passing phases | Architectural combinations | CCRs per combination | +| --- | ---: | ---: | --- | +| `rte-validation-physical-direct` | 162,816 | 2,544 | All 32, entry and handler return | +| `rte-validation-physical-chained` | 61,056 | 15,264 | 0/31, entry and handler return | + +The matrix rejects every byte of the selected SR/PC/format/SSW/continuation-EA +read. It covers two direct and twelve one/two-throwaway supervisor-bank paths, +T0/T1/no trace, even/odd data-stack addresses, and zero/nonzero VBR. Forms are +short formats 0/2/3, unsupported 040 formats 4/15, and normal/CM/CT/CU/CP access +frames. Different candidate restoration SRs expose premature installation. +Common verification checks all registers, stack banks, exact PC/SR, exception +entry count, saved PC/SR, guarded original-frame and neighboring memory, and +pending CU/CP retention. The handler's following sentinel and discarded odd +PCs must not execute. SSW X, EA and invalid writeback/push data are masked; +writeback valid bits and the fault address are checked. Evidence: +`artifacts/m6-rte-validation-fault-fixed/` and the final full-suite reports. + +The maintained `-Scope RteValidationFault` mutation command requires both +complete batches and restores/rebuilds production source after testing: + +| Mutation | Direct entry mismatches | Chained entry mismatches | +| --- | ---: | ---: | +| Wrong format word | 81,408 | 30,528 | +| Wrong word/long size | 81,408 | 30,528 | +| Fault address incremented | 81,408 | 30,528 | +| Saved RTE PC incremented | 81,408 | 30,528 | +| Continuation EA loses validation marker | 12,288 | 4,608 | + +Failed prerequisites retain untested handler-return phases instead of reporting +them as passing. Evidence: +`artifacts/m6-rte-validation-fault-mutations/mutation-proof.json`. +No historical regression is retired by this checkpoint. + +The complete discovery command validates the exact independent byte-range +distribution, seven fixture/command identities and CPU/assembly identities. +It passes **1,348,224 phases**, with zero mismatching/unsupported promoted +phases, and still fails on **480 named untested fault/context requirements**. +Supervisor physical validation is not a replacement for user-tail validation, +internal restoration/double faults, other real instruction fault entry, +writeback-handler execution or CP context transfer. Re-execution of a repaired +original RTE, warmed JIT fault cases, public BERR signaling, physical bus beats +and enabled-MMU operation are not qualified by these two phases. Evidence: +`artifacts/m6-rte-validation-fault-discovery/`. + +Five copied-report/source controls reject a missing validation report, shortened +case count, foreign byte-range combination, missing fixture identity and changed +CPU identity for their specific reasons. The baseline retains the 480 inventory +failures and is not called a passing roadmap. Evidence: +`artifacts/m6-rte-validation-fault-report-controls/controls.json`. + +Full Release CPU validation in `artifacts/m6-rte-validation-fault-full/` passes +**5,046 tests, eleven optional skips and zero failures**. All nine qualified +WinUAE presets pass their existing exact callback/frame selections against the +same CPU and adapter assemblies. CPU SHA-256: +`91e634457a2736f4c98a19abeac75bab3562601caabbce967e30a3a564af7efb`; +adapter SHA-256: +`19359ce94782ea27c4226691c97e4abab6b939b905343043aa626f9883b2027e`. +The strict gate passes **16,316,886 logical cases in 605 batches**, with fresh +pinned SingleStepTests (312,500 cases / 125 files) and Musashi (536 programs / +88 explicit exclusions). Identities remain unchanged through the strict gate. + +Isolated CopperScreen at baseline `d9beae8b88be24032221e3482942a249c03c27d3` +validates private unpublished NuGet `1.5.2-synthetic-dev.58`: Release build has +zero warnings/errors; host 149 passing/six optional skips, disk 74, separate +engine diagnostics 1,080, and native Workbench boots two/A1200 persistence one +pass without native skips. All four assets and loaded assemblies match the +package and the CPU binary above. Package SHA-256: +`e6b829df02b2eb449d68149832c4c569ae6a3f53e3c9774135a867fe490a52e9`. +Evidence: the consumer's `artifacts/rte-validation-fault-identities.json`, +`artifacts/rte-validation-fault-validation-v2/` and separate diagnostic outputs. +The first consumer helper failed during restore because its URI argument was +interpreted as a local source; the validated rerun uses an explicit isolated +NuGet configuration. A preliminary ROM extraction identity check rejected raw +bytes because the native test pins the supplied ZIP; no media was overwritten. +Neither attempt is counted as semantic replay evidence. + +Root CopperScreen changes, its pinned dependency boundary and published package +versions remain untouched. No package is published. The complete restoration, +reference and consolidation requirements remain in progress, with +`roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 7432fd01..6de6c654 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1399,3 +1399,31 @@ with 1,124,352 passing phases and no promoted mismatches. Fresh broad Basic discovery retains 1,326 passing directories, 47 mismatching and eight unsupported; its per-model/opcode statuses and counts are unchanged, and all 32 controls pass. These unresolved requirements remain part of the goal. + +### Milestone 6 physical RTE validation faults — 2026-10-05 + +040 physical map faults during pre-commit RTE validation now build a format-7 +frame with the original access width/address, RTE PC and live SR, preserving the +incomplete frame and committed throwaway stack effects. Two ordinary-CI batches +add 223,872 passing entry/handler-return phases across direct and chained +supervisor stacks. Five maintained mutations detect format, size, address, PC +and continuation-read marker defects, then restore/rebuild source. Five report/ +identity controls reject their intended defects. No regression is retired. + +Full Release CPU validation passes 5,046 tests (eleven optional skips), all nine +qualified WinUAE presets, 16,316,886 strict logical cases in 605 batches and fresh +pinned SingleStepTests/Musashi audits. Isolated unpublished `.58` validates the +CopperScreen Release build, host/disk/engine suites and three native boot/ +persistence replays through NuGet, with matching package/assets/loaded binaries. +Published versions and the root CopperScreen checkout remain untouched. + +Complete 040 discovery still fails on 480 named untested fault/context cases, +with 1,348,224 passing phases and no promoted mismatch/unsupported execution. +User-tail validation, internal-restoration/double faults, re-execution after +software repair, warmed JIT fault cases, other instruction fault entry, +writeback handlers, CP context transfer and all broader remaining reference/ +consolidation requirements remain open. See the +[reference record](COPPER68K_REFERENCE_QUALIFICATION.md#040-physical-rte-validation-fault-correction--2026-10-05) +for exact scope, failed-before evidence, authorities and identities. Milestone 6 +and the full goal remain **in progress** with `roadmapComplete=false`; publication +requires separate release authorization. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index 55880de7..8c12e529 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -20,6 +20,8 @@ $expected = [ordered]@{ 'rte-odd-normal' = @{cases=69120; combinations=720} 'rte-odd-pending' = @{cases=165888; combinations=1296} 'address-error-fetch-040' = @{cases=2304; combinations=72} + 'rte-validation-physical-direct' = @{cases=162816; combinations=2544} + 'rte-validation-physical-chained' = @{cases=61056; combinations=15264} 'rte-access-remaining-protocols' = @{cases=480; combinations=15} } $testExit = 0 @@ -33,7 +35,7 @@ if (-not $ValidateReportsOnly) { $saved[$name] = [Environment]::GetEnvironmentVariable($name) [Environment]::SetEnvironmentVariable($name, $settings[$name]) } - & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests|FullyQualifiedName~SyntheticM68040RteValidationFaultTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output $testExit = $LASTEXITCODE } finally { foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } @@ -45,16 +47,16 @@ if (-not $ValidateReportsOnly) { cpuSourceFiles=@(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } - inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs') | ForEach-Object { + inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } assemblies=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } - limitations=@('Canonical synthetic frames, not hardware captures or enabled-MMU access faults', + limitations=@('Synthetic frames and supervisor-stack physical map faults, not hardware captures or enabled-MMU access faults', 'CP context transfer and detailed fault protocol inventory remain untested and fail the gate', 'Multiple continuation bits are architecturally undefined and excluded', - 'Detailed fault validation, chained odd-PC SR provenance and physical timing remain unqualified', + 'User-tail validation, internal-restoration/double faults, chained odd-PC SR provenance and physical timing remain unqualified', 'Direct odd-RTE saved-SR ordering uses documentary WinUAE 5d22d336, not an executed hardware oracle') } $identity | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'identities.json') @@ -64,11 +66,11 @@ if (-not $ValidateReportsOnly) { $identity = Get-Content -LiteralPath (Join-Path $output 'identities.json') -Raw | ConvertFrom-Json if ($identity.schema -ne 1 -or $identity.reference -cne 'MC68040UM' -or $identity.softwareReferenceExecuted -ne $false -or $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.cpuCommittedTree -notmatch '^[a-f0-9]{40}$' -or - @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 6 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 6 -or + @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 7 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 7 -or @($identity.assemblies).Count -ne 2) { throw '040 access-frame input identity is missing or incomplete' } foreach ($input in $identity.inputs) { $path = Join-Path $repo $input.file - if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs') -or + if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs') -or $input.sha256 -cne (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()) { throw '040 access-frame fixture/command identity differs' } } $cpuFiles = @(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') @@ -82,8 +84,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 19 -or [int]$counters.total -ne 19 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (13 batches, 6 fixed examples)' +if ([int]$counters.executed -ne 21 -or [int]$counters.total -ne 21 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (15 batches, 6 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -106,6 +108,31 @@ foreach ($group in $expected.Keys) { $expectedCombinations["68040/RTE/format7/$form/bank=$bank"] = 32 } } + } elseif ($group -in @('rte-validation-physical-direct','rte-validation-physical-chained')) { + $chained = $group -eq 'rte-validation-physical-chained' + $paths = if ($chained) { @('ISP-ISP','ISP-ISP-ISP','ISP-MSP-ISP','ISP-MSP','ISP-ISP-MSP','ISP-MSP-MSP', + 'MSP-ISP','MSP-ISP-ISP','MSP-MSP-ISP','MSP-MSP','MSP-ISP-MSP','MSP-MSP-MSP') } else { @('ISP','MSP') } + foreach ($path in $paths) { + foreach ($trace in @(0,0x8000,0x4000)) { + foreach ($alignment in @(0,1)) { + foreach ($vbr in @(0,0x10000)) { + foreach ($form in @('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP')) { + # Independent fixed byte ranges; no production or fixture read plan. + $reads = @('0:2','2:4','6:2') + if ($form -in @('normal','CM','CT','CU','CP')) { $reads += '12:2' } + if ($form -in @('CM','CT','CU','CP')) { $reads += '8:4' } + foreach ($read in $reads) { + $width = [int]$read.Split(':')[1] + for ($byte=0; $byte -lt $width; $byte++) { + $key = '68040/RTE/validation-physical/{0}/path={1}/T={2:X4}/align={3}/VBR={4:X8}/read={5}/fault-byte={6}' -f $form,$path,$trace,$alignment,$vbr,$read,$byte + $expectedCombinations[$key] = if ($chained) { 4 } else { 64 } + } + } + } + } + } + } + } } elseif ($group -in @('rte-odd-normal','rte-odd-pending','address-error-fetch-040')) { if ($group -eq 'address-error-fetch-040') { foreach ($bank in @('user','ISP','MSP')) { foreach ($trace in @(0,0x8000,0x4000)) { foreach ($target in @(0x6001,0xff002003u)) { @@ -206,8 +233,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 19 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 21 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=1124832; xunitBatches=13; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=1348704; xunitBatches=15; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index b0e4e77c..c44cd086 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -190,12 +190,20 @@ $mutations += @( '@; group='EveryCacheOpcodeIncludesInvalidScopeAndNeitherCache'; milestone=6; model='68040'; cache=$true; legacyTest='Move16CacheInstructionsAndBreakpoints'; legacyFile='Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs'; legacyModel='68040'} ) +$mutations += @( + @{name='rte-validation-frame'; file='Copper68k/M68040Support.cs'; before=' PushWord(0x7008);'; after=' PushWord(0x0008);'; group='SyntheticM68040RteValidationFaultTests'; milestone=6; model='68040'; validationFault=$true}, + @{name='rte-validation-width'; file='Copper68k/M68040Support.cs'; before='(fault.ByteCount == 2 ? 0x0040 : 0)'; after='(fault.ByteCount == 2 ? 0 : 0x0040)'; group='SyntheticM68040RteValidationFaultTests'; milestone=6; model='68040'; validationFault=$true}, + @{name='rte-validation-address'; file='Copper68k/M68040Support.cs'; before=' PushLong(fault.LogicalAddress);'; after=' PushLong(fault.LogicalAddress + 1);'; group='SyntheticM68040RteValidationFaultTests'; milestone=6; model='68040'; validationFault=$true}, + @{name='rte-validation-saved-pc'; file='Copper68k/M68040Support.cs'; before=' RaiseRteValidationAccessFault(fault, stackedProgramCounter);'; after=' RaiseRteValidationAccessFault(fault, stackedProgramCounter + 2);'; group='SyntheticM68040RteValidationFaultTests'; milestone=6; model='68040'; validationFault=$true}, + @{name='rte-validation-continuation-read'; file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs'; before='var address = continuation == 0 ? 0u : ReadRteFrameLong(frame + 8);'; after='var address = continuation == 0 ? 0u : ReadLong(frame + 8);'; group='SyntheticM68040RteValidationFaultTests'; milestone=6; model='68040'; validationFault=$true} +) if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } if ($Scope -eq 'Logical') { $mutations = @($mutations | Where-Object { $_.milestone -eq 4 }) } if ($Scope -eq 'Control') { $mutations = @($mutations | Where-Object { $_.milestone -eq 5 }) } if ($Scope -eq 'Consolidation') { $mutations = @($mutations | Where-Object { $_.milestone -eq 6 }) } if ($Scope -eq 'Rte040') { $mutations = @($mutations | Where-Object { $_.odd }) } +if ($Scope -eq 'RteValidationFault') { $mutations = @($mutations | Where-Object { $_.validationFault }) } if ($Scope -eq 'LowPowerStop') { $mutations = @($mutations | Where-Object { $_.lpstop }) } if ($Scope -eq 'CacheEncodings') { $mutations = @($mutations | Where-Object { $_.cache }) } $saved = @{} @@ -248,6 +256,14 @@ try { throw 'Odd-return mutation did not execute its complete three-batch selection' } } + if ($mutation.validationFault) { + [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + if ($trx.TestRun.ResultSummary.Counters.executed -ne 2 -or $batches.Count -ne 2 -or + @($batches | Where-Object { $_.group -eq 'rte-validation-physical-direct' -and $_.logicalCases -eq 162816 }).Count -ne 1 -or + @($batches | Where-Object { $_.group -eq 'rte-validation-physical-chained' -and $_.logicalCases -eq 61056 }).Count -ne 1) { + throw 'RTE validation-fault mutation did not execute its complete two-batch selection' + } + } if ($mutation.lpstop) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw $expectedBatches = 2 + [int][bool]$legacyPresent diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 3374e9bf..44250177 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -148,6 +148,8 @@ try { $expected['system-move-word-restart-invalid'] = 224 } if ($model -eq '68040') { + $expected['rte-validation-physical-direct'] = 162816 + $expected['rte-validation-physical-chained'] = 61056 $expected['rte-odd-normal'] = 69120 $expected['rte-odd-pending'] = 165888 $expected['address-error-fetch-040'] = 2304 @@ -256,7 +258,8 @@ try { seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; - m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks and direct odd-PC returns with format-2 address errors and pending-exception priority; chained odd-PC saved-SR provenance, CP context transfer, detailed fault validation, real fault entry and physical timing remain unqualified'; + m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, and supervisor-stack physical RTE validation fault entry/handler return; chained odd-PC saved-SR provenance, user-tail/internal-restoration faults, CP context transfer, other real fault entry and physical timing remain unqualified'; + m68040RtePhysicalValidationScope=@{logicalCases=223872; batches=2; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); trace=@(0,0x8000,0x4000); alignment=@(0,1); vbr=@(0,0x10000); faults='Each byte of SR/PC/format/SSW/continuation-EA reads; 14 direct/one/two-throwaway supervisor paths'; phases=@('fault entry','handler return'); undefined='EA and invalid writeback/push data; SSW X'; remaining='User-tail and internal-restoration/double faults, other instruction faults and software writeback handlers'}; m68060LowPowerStopScope='Every unrecognized second opcode word in both privilege modes; fixed encoding, status, CCR and incoming trace cases across all profiles. Opcode-PC exception frames follow MC68060UM 8.2.4/5; physical broadcast, pins and ordinary STOP S-clear software disagreement remain unqualified'; cacheInstructionEncodingScope='Every F4xx opcode, all CCRs and both privilege states across eight profiles; scope zero is illegal on 040/060, including neither-cache forms, before privilege effects. Empty fixture caches qualify architectural state and exception outcomes, not physical invalidation, writeback, bus faults or timing'; m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; marked private word-MOVE/MOVEA images resume their faulted cycle; other opaque restart/input state, long transfers, RMW and physical timing remain unqualified'; From be81a657356e4730e4d5b7515ee437edb5cc0e12 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 20:27:34 +0300 Subject: [PATCH 042/211] Halt 68040 access-fault entry on a second bus fault --- Copper68k.Tests/Synthetic/README.md | 20 ++ .../SyntheticM68040AccessDoubleFaultTests.cs | 310 ++++++++++++++++++ .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- Copper68k/M68040Support.cs | 37 ++- Copper68k/M68kJitCore.cs | 21 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 98 ++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 35 ++ scripts/test-copper68k-040-access-frames.ps1 | 52 ++- .../test-copper68k-synthetic-mutations.ps1 | 43 ++- scripts/test-copper68k-synthetic.ps1 | 6 + 10 files changed, 606 insertions(+), 18 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 77c472d0..f13a5aca 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -390,3 +390,23 @@ The duplicated cache loop is retired after shared mutation proof; MOVE16, breakpoints, specialized cache/prefetch/bus/JIT/native regressions remain. See the [cache encoding record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#cache-encoding-reference-qualification-and-consolidation-2026-10-05) for exact identities, replacement cases and qualification limits. + +040 access-fault entry has five additional ordinary CI batches: +`rte-access-entry-double-fault` (98,304 complete halt/host-entry/reset scenarios), +`rte-access-handler-refault` (3,840 nested handler exceptions), and +`access-double-fault-dispatch-accurate`, `-v1`, `-v2` (1,088 each). The first +matrix rejects every byte of the format-7 stack and vector access across both +supervisor banks, valid incoming trace states, all CCRs, odd/even stacks and VBRs. +The dispatch groups require actual compiled warm execution, RTE fallback or a +compiled operand-fault side exit, and B/W/L read/write access widths. Halted +state cannot be resumed by interrupt, task or subroutine entry; external reset +restores those host operations and instruction execution. Partial frame state +and stack ordering remain opaque. The approximate short generic operand-fault +frame is not promoted as architectural format-7 coverage. + +Run `./scripts/test-copper68k-synthetic-mutations.ps1 -Scope AccessDoubleFault` +for maintained fatal-latch, reset, compiled-entry, master-stack and six classic +memory-routing mutations. The complete 040 audit includes these combinations +and fixture identities while retaining its failing untested protocol inventory. +Handler-entry prefetch, internal-restoration faults, active accurate-batch fault +delivery and repaired-original-RTE retry remain required work. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs new file mode 100644 index 00000000..85e4bfd3 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs @@ -0,0 +1,310 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 7.6.3, 8.2.1: faults while stacking or fetching the access-error +// vector halt until external reset. The partially constructed frame is opaque; +// its write ordering/SP are not hardware-qualified by this semantic fixture. +public sealed class SyntheticM68040AccessDoubleFaultTests(ITestOutputHelper output) +{ + private const uint Handler = 0x9020; + private static readonly ushort[] Traces = [0, 0x8000, 0x4000]; // C000 is undefined. + + [Theory, Trait("Suite", "Synthetic")] + [InlineData("accurate")] + [InlineData("v1")] + [InlineData("v2")] + public void WarmDispatchHaltsBothRteFallbackAndOperandFaultEntry(string engine) + { + var report = new CoverageBatch("68040", "access-double-fault-dispatch-" + engine); + foreach (var form in new[] { "RTE", "MOVE.B.read", "MOVE.W.read", "MOVE.L.read", "MOVE.B.write", "MOVE.W.write", "MOVE.L.write" }) + foreach (var bank in new[] { "ISP", "MSP" }) + foreach (var ccr in new[] { 0, 31 }) + foreach (var alignment in new uint[] { 0, 1 }) + foreach (var stage in new[] { "stack", "vector" }) + for (var faultByte = 0; faultByte < (stage == "vector" ? 4 : form == "RTE" ? 60 : 8); faultByte++) + { + var opcode = form switch { "RTE" => 0x4e73, "MOVE.B.read" => 0x1010, "MOVE.W.read" => 0x3010, + "MOVE.L.read" => 0x2010, "MOVE.B.write" => 0x1080, "MOVE.W.write" => 0x3080, _ => 0x2080 }; + var write = form.EndsWith("write"); + var id = $"68040/access-double-fault/dispatch={engine}/form={form}/bank={bank}/align={alignment}/stage={stage}/byte={faultByte}/op={opcode:X4}/ccr={ccr:X2}"; + try + { + var bus = new FaultBus(); var executionBus = new ImmutableCodeBus(bus); + bus.Initialize(0x2000, (uint)(form == "RTE" ? 0x3010 : opcode), 2); bus.Initialize(0x2002, 0x60fc, 2); // MOVE; BRA + bus.Initialize(0x1000, 0x4e73, 2); bus.Initialize(0x1002, 0x4e71, 2); + bus.Initialize(Handler, 0x4e73, 2); bus.Initialize(Handler + 2, 0x4e71, 2); + bus.Initialize(0x5000, 0x747b, 2); bus.Initialize(0x5002, 0x4e71, 2); + bus.Initialize(8, Handler, 4); bus.Initialize(0x4200, 0x1234, 2); + IM68kCore core = engine == "accurate" ? M68kCoreFactory.Default.Create(M68kCpuModel.M68040, executionBus) + : M68kJitCore.CreateM68040ForTesting(executionBus, engine == "v2"); + using var disposable = core as IDisposable; + core.Reset(0x2000, 0x4700); core.State.A[0] = 0x4200; core.State.CacheControlRegister = 1; + for (var n = 0; n < 300; n++) Execute(core); + if (core is M68kJitCore warm && warm.Counters.TraceHits + warm.Counters.V2TraceHits == 0) + throw new InvalidOperationException("No actual compiled warm dispatch"); + // Keep compiled code, clear dispatch continuation from the warm loop. + if (core is M68kJitCore prepared) prepared.ResetForBenchmark(form == "RTE" ? 0x1000u : 0x2000u, 0x4700); + else core.Reset(form == "RTE" ? 0x1000u : 0x2000u, 0x4700); + core.State.A[0] = 0x4200 + alignment; core.State.CacheControlRegister = 1; + var frame = (bank == "MSP" ? 0x7400u : 0x4700u) + alignment; + core.State.SetInterruptStackPointer(0x4700 + alignment); core.State.SetMasterStackPointer(0x7400 + alignment); + core.State.StatusRegister = (ushort)((bank == "MSP" ? 0x3000 : 0x2000) | ccr); + core.State.ProgramCounter = form == "RTE" ? 0x1000u : 0x2000u; + bus.Initialize(frame, 0x2000, 2); bus.Initialize(frame + 2, 0x6000, 4); + bus.Initialize(frame + 6, 0x7008, 2); bus.Initialize(frame + 8, 0x12345678, 4); + bus.Initialize(frame + 12, 0x1105, 2); + var sequence = core.State.ExceptionSequence; + var fallbacks = core is M68kJitCore j ? j.Counters.FallbackInstructions : 0; + var sideExits = core is M68kJitCore j2 ? j2.Counters.SideExits : 0; + bus.Arm(form == "RTE" ? frame : 0x4200 + alignment, + write ? M68kBusAccessKind.CpuDataWrite : M68kBusAccessKind.CpuDataRead); + // The existing generic MOVE fault path uses an approximate short + // frame. Only its fatal-entry behavior is qualified here. + var stackStart = frame - (form == "RTE" ? 60u : 8u); + bus.Arm(stage == "vector" ? 8 + (uint)faultByte : stackStart + (uint)faultByte, + stage == "vector" ? M68kBusAccessKind.CpuDataRead : M68kBusAccessKind.CpuDataWrite); + Execute(core); + if (!core.State.Halted || core.State.Stopped || bus.Rejected.Count != 2 || + bus.Accesses.Count != bus.Rejected[^1].AccessCount || core.State.ExceptionSequence != sequence + 1) + throw new InvalidOperationException($"Fatal entry: halted={core.State.Halted}, stopped={core.State.Stopped}, rejections={bus.Rejected.Count}, accesses={bus.Accesses.Count}, fatalAt={(bus.Rejected.Count > 0 ? bus.Rejected[^1].AccessCount : -1)}, sequence={core.State.ExceptionSequence - sequence}"); + var operandWidth = form == "RTE" || form.Contains(".W.") ? 2 : form.Contains(".B.") ? 1 : 4; + if (bus.Rejected[0].Width != operandWidth || bus.Rejected[0].Kind != + (write ? M68kBusAccessKind.CpuDataWrite : M68kBusAccessKind.CpuDataRead)) + throw new InvalidOperationException("Original operand access width/direction differs"); + if (core is M68kJitCore jit) + { + if (form == "RTE" && jit.Counters.FallbackInstructions <= fallbacks) + throw new InvalidOperationException("RTE did not reach interpreter fallback"); + if (form != "RTE" && (jit.Counters.FallbackInstructions != fallbacks || jit.Counters.SideExits <= sideExits)) + throw new InvalidOperationException("Operand fault did not reach the compiled fault side exit"); + } + var s = core.State; var d = (uint[])s.D.Clone(); var a = (uint[])s.A.Clone(); + var pc = s.ProgramCounter; var sr = s.StatusRegister; var accesses = bus.Accesses.Count; + var memory = new Dictionary(bus.Memory); + core.RequestInterrupt(7, 31 * 4); core.BeginSubroutine(0x5000, 0x6000, 0x7000); + core.SwitchTaskContext(new M68kCpuState()); + var batch = ((IM68kBatchCore)core).ExecuteInstructions(10, s.Cycles + 100, new Boundary()); + if (batch != 0 || !s.Halted || s.Stopped || !s.D.SequenceEqual(d) || !s.A.SequenceEqual(a) || + s.ProgramCounter != pc || s.StatusRegister != sr || s.ExceptionSequence != sequence + 1 || + bus.Accesses.Count != accesses || !memory.OrderBy(x => x.Key).SequenceEqual(bus.Memory.OrderBy(x => x.Key))) + throw new InvalidOperationException("Host entry resumed or changed fatal CPU state"); + bus.Disarm(); core.Reset(0x6000, 0x8000); + core.BeginSubroutine(0x5000, 0x8000, 0x7000); Execute(core); + if (s.Halted || s.Stopped || s.D[2] != 123 || s.ProgramCounter != 0x5002) + throw new InvalidOperationException("External reset did not restart execution"); + report.Record(id, "passing", null); + } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void EveryStackAndVectorByteCanHaltAccessFaultEntryUntilReset() + { + var bus = new FaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + var report = new CoverageBatch("68040", "rte-access-entry-double-fault"); + foreach (var bank in new[] { "ISP", "MSP" }) + foreach (var trace in Traces) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var alignment in new uint[] { 0, 1 }) + foreach (var vbr in new uint[] { 0, 0x10000 }) + foreach (var firstOffset in new uint[] { 0, 8 }) + for (var secondByte = 0; secondByte < 64; secondByte++) + { + var id = $"68040/RTE/access-entry-double-fault/bank={bank}/T={trace:X4}/align={alignment}/VBR={vbr:X8}/first={firstOffset}/second={secondByte}/op=4E73/ccr={ccr:X2}"; + try + { + var frame = Prepare(m, bus, bank, trace, ccr, alignment, vbr); + var before = ArchitecturalExpectation.Capture(m); + var sequence = m.Core.State.ExceptionSequence; + bus.Arm(frame + firstOffset, M68kBusAccessKind.CpuDataRead); + bus.Arm(secondByte < 60 ? frame - 60 + (uint)secondByte : vbr + 8 + (uint)secondByte - 60, + secondByte < 60 ? M68kBusAccessKind.CpuDataWrite : M68kBusAccessKind.CpuDataRead); + m.Core.ExecuteInstruction(); + var s = m.Core.State; + string? mismatch = null; + if (!s.Halted || s.Stopped) mismatch = "Second access fault must halt, not STOP"; + else if (bus.Rejected.Count != 2 || bus.Accesses.Count != bus.Rejected[^1].AccessCount) + mismatch = "Expected two rejections and no bus transfer after the fatal rejection"; + else if (s.StatusRegister != (before.Sr & ~0xc000) || s.LastExceptionStatusRegister != before.Sr || + s.LastExceptionStackedProgramCounter != SyntheticMachine.Code || s.LastExceptionVector != 2 || + s.ExceptionSequence != sequence + 1 || s.M68040Mmu.BypassTranslation) + mismatch = "Fault entry SR/provenance/sequence or translation bypass differs"; + else if (!s.D.SequenceEqual(before.D) || !s.A.Take(7).SequenceEqual(before.A.Take(7)) || + s.UserStackPointer != before.InactiveStackPointer || + (bank == "ISP" ? s.MasterStackPointer != 0x7400 + alignment : s.InterruptStackPointer != 0x4700 + alignment)) + mismatch = "Fault entry changed unrelated registers or inactive stacks"; + else if (s.A[7] < frame - 60 || s.A[7] > frame) + mismatch = "Partial frame escaped its bounded stack region"; + else if (bus.Accesses.Where(a => a.Write).Any(a => a.Kind != M68kBusAccessKind.CpuDataWrite || + a.Address < frame - 60 || (ulong)a.Address + (uint)a.Width > frame)) + mismatch = "Fault entry wrote outside the new frame"; + else if (before.Memory.Keys.Concat(bus.Memory.Keys).Distinct().Any(at => + (at < frame - 60 || at >= frame) && before.Memory.GetValueOrDefault(at) != bus.Peek(at))) + mismatch = "Fault entry changed original frame or surrounding memory"; + if (mismatch == null) + { + // Opaque partial state is frozen after HALT, not used to infer + // hardware stacking order. Ordinary host entry cannot restart it. + var frozen = ArchitecturalExpectation.Capture(m); frozen.Halted = true; + frozen.ExceptionVector = 2; + frozen.ControlChecks["exception sequence"] = (x => x.ExceptionSequence, sequence + 1); + frozen.ControlChecks["last opcode"] = (x => x.LastOpcode, s.LastOpcode); + var accesses = bus.Accesses.Count; + m.Core.ExecuteInstruction(); + m.Core.RequestInterrupt(7, 31 * 4); + m.Core.BeginSubroutine(0x5000, 0x6000, 0x7000); + m.Core.SwitchTaskContext(new M68kCpuState()); + var batch = ((IM68kBatchCore)m.Core).ExecuteInstructions(10, s.Cycles + 100, new Boundary()); + mismatch = frozen.Verify(m); + if (mismatch == null && (batch != 0 || bus.Accesses.Count != accesses || bus.Rejected.Count != 2)) + mismatch = "Halted CPU performed a transfer or executed a batch"; + } + if (mismatch == null) + { + bus.Disarm(); m.InitializePhysical(0x5000, 0x747b, 2); // MOVEQ #123,D2 + m.InitializePhysical(0x5002, 0x4e71, 2); + m.Core.Reset(0x6000, 0x8000); + m.Core.BeginSubroutine(0x5000, 0x8000, 0x7000); + var reset = ArchitecturalExpectation.Capture(m); reset.Pc = 0x5002; reset.D[2] = 123; + m.Core.ExecuteInstruction(); mismatch = reset.Verify(m); + } + report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); + } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void HandlerValidationRefaultStartsANewException() + { + var bus = new FaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + var report = new CoverageBatch("68040", "rte-access-handler-refault"); + foreach (var bank in new[] { "ISP", "MSP" }) + foreach (var trace in Traces) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var alignment in new uint[] { 0, 1 }) + foreach (var vbr in new uint[] { 0, 0x10000 }) + foreach (var offset in new uint[] { 0, 2, 6, 12, 8 }) + { + var id = $"68040/RTE/access-handler-refault/bank={bank}/T={trace:X4}/align={alignment}/VBR={vbr:X8}/read={offset}/op=4E73/ccr={ccr:X2}"; + try + { + var frame = Prepare(m, bus, bank, trace, ccr, alignment, vbr); + var before = ArchitecturalExpectation.Capture(m); + var sequence = m.Core.State.ExceptionSequence; + bus.Arm(frame, M68kBusAccessKind.CpuDataRead); + m.Core.ExecuteInstruction(); + if (m.Core.State.Halted || m.Core.State.ProgramCounter != Handler || m.Core.State.A[7] != frame - 60) + throw new InvalidOperationException("First access-fault entry prerequisite failed"); + // Set CM to require the EA continuation read as well as header/SSW. + // This is handler preparation, not an expectation derived from CPU output. + m.InitializePhysical(frame - 60 + 12, 0x1145, 2); + var e = ArchitecturalExpectation.Capture(m); + bus.Arm(frame - 60 + offset, M68kBusAccessKind.CpuDataRead); + var nested = frame - 120; + e.A[7] = nested; if (bank == "MSP") e.MasterStackPointer = nested; + e.Pc = Handler; e.ExceptionVector = 2; + e.ControlChecks["new exception"] = (s => s.ExceptionSequence, sequence + 2); + e.ControlChecks["handler PC saved"] = (s => s.LastExceptionStackedProgramCounter, Handler); + e.ControlChecks["new fault rejected once"] = (_ => (uint)bus.Rejected.Count, 2); + e.Write(nested, e.Sr, 2, m.Model); e.Write(nested + 2, Handler, 4, m.Model); + e.Write(nested + 6, 0x7008, 2, m.Model); + for (uint n = 8; n < 60; n++) e.MemoryMasks[nested + n] = 0; + e.Write(nested + 12, offset is 2 or 8 ? 0x0105u : 0x0145u, 2, m.Model); + e.MemoryMasks[nested + 12] = 0xff; e.MemoryMasks[nested + 13] = 0x7f; + foreach (var at in new uint[] { 14, 16, 18 }) + { e.Write(nested + at, 0, 2, m.Model); e.MemoryMasks[nested + at + 1] = 0x80; } + e.Write(nested + 20, frame - 60 + offset, 4, m.Model); + for (uint n = 20; n < 24; n++) e.MemoryMasks[nested + n] = 0xff; + m.Core.ExecuteInstruction(); + var mismatch = e.Verify(m); + if (mismatch == null && (m.Core.State.LastExceptionStatusRegister != (before.Sr & ~0xc000) || + bus.Rejected[^1].Width != (offset is 2 or 8 ? 4 : 2))) + mismatch = "Nested validation fault saved the wrong SR or used the wrong width"; + report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); + } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + report.Complete(output); + } + + private static uint Prepare(SyntheticMachine m, FaultBus bus, string bank, ushort trace, int ccr, uint alignment, uint vbr) + { + bus.Disarm(); m.Reset(ccr); + var frame = (bank == "MSP" ? 0x7400u : 0x4700u) + alignment; + for (var n = -136; n < 80; n++) m.InitializePhysical(unchecked(frame + (uint)n), (uint)(n ^ 0x5a), 1); + m.InitializePhysical(frame, 0x801fu ^ (uint)ccr, 2); + m.InitializePhysical(frame + 2, 0x6000, 4); m.InitializePhysical(frame + 6, 0x7008, 2); + m.InitializePhysical(frame + 8, 0x12345678, 4); m.InitializePhysical(frame + 12, 0x1105, 2); + m.InitializePhysical(Handler, 0x4e73, 2); m.InitializePhysical(Handler + 2, 0x7001, 2); + m.InitializePhysical(vbr + 8, Handler, 4); + _ = SyntheticExecution.Prepare(m, [0x4e73]); + m.Core.State.SetInterruptStackPointer(0x4700 + alignment); + m.Core.State.SetMasterStackPointer(0x7400 + alignment); + m.Core.State.StatusRegister = (ushort)((bank == "MSP" ? 0x3000 : 0x2000) | trace | ccr); + m.Core.State.VectorBaseRegister = vbr; + return frame; + } + + private sealed class FaultBus : SparseRecordingBus, IM68kPhysicalAddressMap + { + private readonly List<(uint At, M68kBusAccessKind Kind)> holes = []; + public List<(uint Address, int Width, M68kBusAccessKind Kind, int AccessCount)> Rejected { get; } = []; + public void Disarm() { holes.Clear(); Rejected.Clear(); } + public void Arm(uint at, M68kBusAccessKind kind) => holes.Add((at, kind)); + public bool IsCpuPhysicalAddressMapped(uint address, int byteCount, M68kBusAccessKind kind) + { + var i = holes.FindIndex(h => h.Kind == kind && unchecked(h.At - address) < byteCount); + if (i < 0) return true; + holes.RemoveAt(i); Rejected.Add((address, byteCount, kind, Accesses.Count)); return false; + } + } + + private static void Execute(IM68kCore core) + { + if (core is M68kJitCore jit) + { + if (jit.ExecuteInstructions(1, core.State.Cycles + 1000, new Boundary()) != 1) + throw new InvalidOperationException("Expected one instruction boundary"); + } + else core.ExecuteInstruction(); + } + + private sealed class ImmutableCodeBus(FaultBus memory) : IM68kBus, IM68kJitBus, IM68kPhysicalAddressMap + { + public bool IsCpuPhysicalAddressMapped(uint address, int count, M68kBusAccessKind kind) => memory.IsCpuPhysicalAddressMapped(address, count, kind); + public event Action? JitCodeRangeWritten { add { } remove { } } + public bool IsJitCodeAddress(uint address, int count, M68kBusAccessKind kind) => + address >= 0x1000 && address + count <= 0x1004 || address >= 0x2000 && address + count <= 0x2004 || + address >= 0x5000 && address + count <= 0x5004 || address >= Handler && address + count <= Handler + 4; + public bool IsJitReadOnlyCodeAddress(uint address, int count, M68kBusAccessKind kind) => IsJitCodeAddress(address, count, kind); + public ushort ReadJitCodeWord(uint address) => (ushort)memory.Peek(address, 2); + public uint GetJitCodePageGeneration(uint address) => 0; + public bool JitCodeRangeGenerationMatches(uint address, int count, uint first, uint last) => first == 0 && last == 0; + public bool TryCaptureJitCodeSnapshot(uint address, int count, out M68kJitCodeSnapshot snapshot) { snapshot = default; return false; } + public void ResetExternalDevices(long cycle) => memory.ResetExternalDevices(cycle); + public byte ReadByte(uint address, ref long cycle, M68kBusAccessKind kind) => memory.ReadByte(address, ref cycle, kind); + public ushort ReadWord(uint address, ref long cycle, M68kBusAccessKind kind) => memory.ReadWord(address, ref cycle, kind); + public uint ReadLong(uint address, ref long cycle, M68kBusAccessKind kind) => memory.ReadLong(address, ref cycle, kind); + public void WriteByte(uint address, byte value, ref long cycle, M68kBusAccessKind kind) => memory.WriteByte(address, value, ref cycle, kind); + public void WriteWord(uint address, ushort value, ref long cycle, M68kBusAccessKind kind) => memory.WriteWord(address, value, ref cycle, kind); + public void WriteLong(uint address, uint value, ref long cycle, M68kBusAccessKind kind) => memory.WriteLong(address, value, ref cycle, kind); + } + + private sealed class Boundary : IM68kBusAccessTraceBatchBoundary, IM68kPureCpuTraceBatchBoundary + { + public bool BeforeInstruction() => true; + public void AfterInstruction(long previousCycle, long currentCycle) { } + public bool TryBeginBusAccessTraceBatch(M68kCpuState state, long target, out long batchTarget) { batchTarget = target; return true; } + public void AfterBusAccessTraceBatch(long previous, long current, int count) { } + public bool TryBeginPureCpuTraceBatch(M68kCpuState state, long target, out long batchTarget) { batchTarget = target; return true; } + public void AfterPureCpuTraceBatch(long previous, long current, int count) { } + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 17a15433..0a6abd9a 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -33,7 +33,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() "real-access-fault-entry", "writeback-handler", "CP-context-transferred-vector" }) report.Record($"68040/RTE/format7/{form}/bank={bank}/op=4E73/ccr={ccr:X2}", "untested", form == "frame-validation-fault" - ? "Supervisor-stack physical validation reads are covered separately; user-tail validation and internal-restoration/double-fault distinctions remain required" + ? "Supervisor validation and access-fault stacking/vector double faults are covered separately; user-tail validation, internal restoration, handler-entry prefetch and active accurate-batch delivery remain required" : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index 83342727..562b940f 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -2462,7 +2462,7 @@ public void Restore(M68kCpuState state) } } - internal sealed class M68040Interpreter : M68kAdvancedTimingInterpreter + internal sealed class M68040Interpreter : M68kAdvancedTimingInterpreter, IM68kCore { private const int VectorBusError = 2; private const int VectorLineF = 11; @@ -2470,6 +2470,7 @@ internal sealed class M68040Interpreter : M68kAdvancedTimingInterpreter private readonly IM68kStablePhysicalAddressMap? _stablePhysicalAddressMap; private readonly M68kInterpreter _approximateIntegerFallback; private uint _observedPhysicalAddressMapGeneration; + private bool _accessErrorDoubleFaultHalted; public M68040Interpreter(IM68kBus bus) : this(bus, M68020CpuProfile.Ocs68040Accelerator25Mhz) @@ -2562,11 +2563,40 @@ private void InvalidateInstructionCacheForHostCodeVisibilityChange() public override void Reset(uint programCounter, uint stackPointer) { + _accessErrorDoubleFaultHalted = false; base.Reset(programCounter, stackPointer); State.M68040Fpu.Reset(); State.M68040Mmu.Reset(); } + public override void BeginSubroutine(uint address, uint stackPointer, uint returnAddress) + { + if (_accessErrorDoubleFaultHalted) return; + base.BeginSubroutine(address, stackPointer, returnAddress); + } + + public void SwitchTaskContext(M68kCpuState next) + { + ArgumentNullException.ThrowIfNull(next); + if (_accessErrorDoubleFaultHalted) return; + State.CopyTaskContextFrom(next); + } + + public override void RequestInterrupt(int level, uint vectorAddress) + { + if (State.Halted) return; + base.RequestInterrupt(level, vectorAddress); + } + + internal void LatchAccessErrorDoubleFault() + { + // MC68040UM 7.6.3 / 8.2.1: only external reset may restart this + // halt. Do not retry stacking or record a second exception entry. + _accessErrorDoubleFaultHalted = true; + State.Halted = true; + State.Stopped = false; + } + protected override bool TryExecuteFastModelSpecificInstruction(ushort opcode) { if (opcode != 0xF200) @@ -4230,6 +4260,11 @@ private void RaiseMmuFault(M68040MmuFault fault) else RaiseFormat0Exception(VectorBusError, stackedProgramCounter, M68kInstructionTimingKey.IllegalInstruction); } + catch (M68040MmuFaultException) + { + LatchAccessErrorDoubleFault(); + CompleteTiming(M68kInstructionTimingKey.IllegalInstruction); + } finally { State.M68040Mmu.BypassTranslation = bypass; diff --git a/Copper68k/M68kJitCore.cs b/Copper68k/M68kJitCore.cs index e79bfc1d..77c7d028 100644 --- a/Copper68k/M68kJitCore.cs +++ b/Copper68k/M68kJitCore.cs @@ -2506,6 +2506,11 @@ private bool HandleM68040CompiledMmuFault(M68040MmuFault fault, IM68kInstruction State.M68040Mmu.Status = faultStatus; RaiseM68040Format0Exception(2, stackedProgramCounter, 34); } + catch (M68040MmuFaultException) + { + ((M68040Interpreter)_fallback).LatchAccessErrorDoubleFault(); + AddCycles(34); // Retain the compiled access-fault timing policy. + } finally { State.M68040Mmu.BypassTranslation = bypass; @@ -2556,7 +2561,7 @@ private void RaiseM68040Format0Exception(int vector, uint stackedProgramCounter, { var savedStatusRegister = State.StatusRegister; State.RecordException(vector, stackedProgramCounter, savedStatusRegister); - State.StatusRegister = (ushort)((State.StatusRegister | M68kCpuState.Supervisor) & ~M68kCpuState.Master); + State.StatusRegister = (ushort)((State.StatusRegister | M68kCpuState.Supervisor) & ~0xc000); PushWord((ushort)((vector * 4) & 0x0FFF)); PushLong(stackedProgramCounter); PushWord(savedStatusRegister); @@ -13912,7 +13917,7 @@ private byte ReadPhysicalByte(uint address, M68kBusAccessKind accessKind) private ushort ReadWord(uint address, M68kBusAccessKind accessKind = M68kBusAccessKind.CpuDataRead) { - if ((address & 1) != 0) + if (_cpuModel == M68kJitCpuModel.M68000 && (address & 1) != 0) { throw new M68kEmulationException($"Odd MC68000 word read at 0x{address:X8}."); } @@ -13937,7 +13942,7 @@ private ushort ReadPhysicalWord(uint address, M68kBusAccessKind accessKind) private uint ReadLong(uint address) { - if ((address & 1) != 0) + if (_cpuModel == M68kJitCpuModel.M68000 && (address & 1) != 0) { throw new M68kEmulationException($"Odd MC68000 long read at 0x{address:X8}."); } @@ -13993,7 +13998,7 @@ private void WritePhysicalByte(uint address, byte value, M68kBusAccessKind acces private void WriteWord(uint address, ushort value) { - if ((address & 1) != 0) + if (_cpuModel == M68kJitCpuModel.M68000 && (address & 1) != 0) { throw new M68kEmulationException($"Odd MC68000 word write at 0x{address:X8}."); } @@ -14025,7 +14030,7 @@ private void WritePhysicalWord(uint address, ushort value, M68kBusAccessKind acc private void WriteLong(uint address, uint value) { - if ((address & 1) != 0) + if (_cpuModel == M68kJitCpuModel.M68000 && (address & 1) != 0) { throw new M68kEmulationException($"Odd MC68000 long write at 0x{address:X8}."); } @@ -14164,6 +14169,7 @@ private uint ReadClassicCompiledMemoryValue(uint address, M68kOperandSize size) private uint ReadClassicCompiledMemoryByte(uint address) { + if (_cpuModel != M68kJitCpuModel.M68000) return ReadByte(address); BeginClassicCompiledBusPhase(prefetchBeforeAccess: false, prefetchPhase: 0); address = Normalize(address); var cycle = _classicCompiledCpuBusCycle; @@ -14194,6 +14200,7 @@ private uint ReadClassicCompiledMemoryByte(uint address) private uint ReadClassicCompiledMemoryWord(uint address) { + if (_cpuModel != M68kJitCpuModel.M68000) return ReadWord(address); BeginClassicCompiledBusPhase(prefetchBeforeAccess: false, prefetchPhase: 0); address = Normalize(address); if ((address & 1) != 0) @@ -14231,6 +14238,7 @@ private uint ReadClassicCompiledMemoryWord(uint address) private uint ReadClassicCompiledMemoryLong(uint address) { + if (_cpuModel != M68kJitCpuModel.M68000) return ReadLong(address); BeginClassicCompiledBusPhase(prefetchBeforeAccess: false, prefetchPhase: 0); address = Normalize(address); if ((address & 1) != 0) @@ -14269,6 +14277,7 @@ private void WriteClassicCompiledMemoryValue(uint address, uint value, M68kOpera private void WriteClassicCompiledMemoryByte(uint address, uint value) { + if (_cpuModel != M68kJitCpuModel.M68000) { WriteByte(address, (byte)value); return; } BeginClassicCompiledBusPhase(prefetchBeforeAccess: true, prefetchPhase: 4); address = Normalize(address); var cycle = _classicCompiledCpuBusCycle; @@ -14298,6 +14307,7 @@ private void WriteClassicCompiledMemoryByte(uint address, uint value) private void WriteClassicCompiledMemoryWord(uint address, uint value) { + if (_cpuModel != M68kJitCpuModel.M68000) { WriteWord(address, (ushort)value); return; } BeginClassicCompiledBusPhase(prefetchBeforeAccess: true, prefetchPhase: 4); address = Normalize(address); if ((address & 1) != 0) @@ -14334,6 +14344,7 @@ private void WriteClassicCompiledMemoryWord(uint address, uint value) private void WriteClassicCompiledMemoryLong(uint address, uint value) { + if (_cpuModel != M68kJitCpuModel.M68000) { WriteLong(address, value); return; } BeginClassicCompiledBusPhase(prefetchBeforeAccess: true, prefetchPhase: 4); address = Normalize(address); if ((address & 1) != 0) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 6c25bfde..c7e8d62a 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -3829,3 +3829,101 @@ Root CopperScreen changes, its pinned dependency boundary and published package versions remain untouched. No package is published. The complete restoration, reference and consolidation requirements remain in progress, with `roadmapComplete=false`. + +### 040 access-fault entry halt qualification — 2026-10-05 + +Authority: [MC68040UM](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +7.6.3, 8.2.1 and exception-entry figure 8-1. A further access fault during +stacking or vector fetch halts until external reset; a fault during an executing +handler starts a new exception. This audit uses real translation-disabled +physical-map rejections, independent addresses and public factory execution. +It does not inject a production exception or call CPU frame/EA helpers. + +The interpreter previously allowed the second fault to escape to its host. +Its new private fatal latch persists through host subroutine/task entry and +interrupt requests. Compiled entry shares that latch, without retrying partial +stacking or recording another exception. Existing interpreter timing keys and +the compiled 34-cycle exception policy are retained; neither is a physical +timing qualification. A separate classic-emitter defect bypassed the +model-aware B/W/L helpers. Those six paths now use 040 address-map checks and +unaligned data access; the 000 pipeline paths retain their existing behavior. +Compiled exception entry also preserves M and clears trace like the interpreter. + +| Ordinary group | Logical scenarios | Architectural combinations | +| --- | ---: | ---: | +| `rte-access-entry-double-fault` | 98,304 | 3,072 | +| `rte-access-handler-refault` | 3,840 | 120 | +| `access-double-fault-dispatch-accurate` | 1,088 | 544 | +| `access-double-fault-dispatch-v1` | 1,088 | 544 | +| `access-double-fault-dispatch-v2` | 1,088 | 544 | + +Each fatal scenario verifies no transfer after rejection, unrelated register/ +memory preservation, inactivity through host entry and batches, then external +reset and a MOVEQ sentinel. Partial frame contents and exact SP/write ordering +are opaque. The main matrix includes every byte of the 60-byte frame and +four-byte vector, both supervisor banks, all CCRs, three defined trace states, +odd/even stacks and two VBRs. T1+T0 is undefined and excluded. The handler control +faults all five validation reads after the first entry has completed and checks +the new defined frame while preserving both older frames. + +Dispatch cases require actual warmed compiled code before selecting RTE fallback +or compiled MOVE B/W/L read/write side exits. They check access widths, odd/even +data and stacks, both supervisor banks and CCR 0/31. Generic operand faults still +use the existing approximate short frame; these cases qualify fatal entry only, +not general architectural format-7 restart. Accurate active-batch fault delivery, +handler-entry prefetch, internal-state restoration, repaired-original RTE retry, +user-tail validation, writeback handlers and CP context transfer remain required. +The 480-case untested inventory remains in the complete audit. No old regression +is retired and milestone 6 remains in progress. + +Failed-before evidence: `artifacts/m6-access-entry-double-fault-before/` records +98,304 escaped second faults, with the 3,840 handler controls passing. Warmed +dispatch failures in `artifacts/m6-access-entry-double-fault-dispatch-before/` +also exposed compiled exception and odd-stack faults; later classic-dispatch +diagnostics identified the missing model-aware memory routing. These discovery +runs are not acceptance evidence. Validation records below bind the final +selection and binaries; no public package release is authorized by this work. + +Final complete 040 discovery in +`artifacts/m6-access-entry-double-fault-discovery/` executes 26 xUnit tests: +25 pass and the retained inventory fails on 480 untested cases. All 20 reports, +six fixed examples, 1,453,632 passing scenarios, fixture/source/assembly identities +and independent combination distributions validate. Five missing-report, shortened +count, foreign-combination and fixture/CPU identity controls reject their intended +defects in `artifacts/m6-access-entry-double-fault-report-controls/controls.json`. + +Ten maintained mutations in +`artifacts/m6-access-entry-double-fault-mutations-v3/mutation-proof.json` +detect fatal-latch, reset, compiled halt, master-stack and all six classic +memory-routing defects. Each executes all three 1,088-case dispatch batches. +The latch and reset mutations fail 1,088 per engine; compiled halt fails 576 +per JIT engine, master-stack selection 192 per JIT engine, and each specialized +memory route fails 96 classic cases. Source is restored and rebuilt afterward. +A preliminary reset mutation survived, exposing a missing host-entry check after +external reset; the final fixtures verify both restart and restored host entry. +An intermediate mutation run rejected a mixed-newline anchor before execution; +the final run normalizes source line endings and detects all ten mutations. +Neither preliminary run is final mutation acceptance. + +Full Release CPU validation in `artifacts/m6-access-entry-double-fault-full/` +passes **5,051 tests, eleven optional skips and zero failures**. All nine +qualified WinUAE presets retain their exact callback/frame counts, checked in +`qualified-preset-identities.json`. The strict gate passes **16,422,294 logical +cases in 610 reporting batches**, with fresh pinned SingleStepTests (312,500 / +125 files) and Musashi (536 programs / 88 explicit exclusions). CPU SHA-256: +`216d59e53e3190a9f9faffa0d7790554cbf2528c1d7d41c48b6f3da22bd4751c`; +test/reference adapter SHA-256: +`d343fb7cd582a6e881153280e781e3b84b93d3bd352014d239b67beb4eb3780f`. +Both identities remain unchanged through the strict gate. + +Isolated CopperScreen baseline `d9beae8b88be24032221e3482942a249c03c27d3` +passes through unpublished private NuGet `1.5.2-synthetic-dev.59`: Release build +zero warnings/errors, host 149/six optional skips, disk 74, separate engine +diagnostics 1,080 and three native Workbench/A1200 boot/persistence replays with +no native skips. Four assets and loaded CPU assemblies match the package and +the CPU binary above. Package SHA-256: +`1b8622efefe5364447020a4f3ce1d4bb9c0766b86bcfa50bc4a559ed53bcbe1d`. +Consumer evidence is under `artifacts/access-entry-double-fault-validation-v2/`, +separate diagnostic outputs and `artifacts/access-entry-double-fault-identities.json`. +Root CopperScreen edits and published packages remain untouched. All remaining +reference/consolidation requirements persist with `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 6de6c654..bb04dbc7 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1427,3 +1427,38 @@ consolidation requirements remain open. See the for exact scope, failed-before evidence, authorities and identities. Milestone 6 and the full goal remain **in progress** with `roadmapComplete=false`; publication requires separate release authorization. + +### Milestone 6 access-fault entry halt qualification — 2026-10-05 + +040 interpreter and compiled access-fault entry now latch HALT on a second +stacking/vector fault, with no retry of partial side effects. External reset +clears the latch; interrupt, host task/subroutine and batch execution cannot +restart it. Classic B/W/L memory emitters now use the model-aware 040 helpers, +including physical-map rejection and unaligned accesses. Compiled fault entry +preserves master-stack selection and clears trace. Existing timing keys and +the compiled exception-cycle policy remain intact; physical timing is separate. + +Five ordinary batches add 105,408 passing scenarios: every byte of format-7 +stack/vector rejection, later handler refaults and warmed accurate/classic/V2 +dispatch with B/W/L read/write faults. Ten maintained mutations detect latch, +reset, compiled halt, stack and six memory-routing defects. Five report/source +controls reject missing, shortened, foreign and stale identities. No old test +is retired. Partial stacking order and generic short operand-fault frames are +not promoted as architectural format-7 restart qualification. + +Full CPU validation passes 5,051 tests (eleven optional skips), all nine +qualified WinUAE presets, 16,422,294 strict logical cases in 610 batches and +fresh pinned SingleStepTests/Musashi audits. Private unpublished `.59` validates +the isolated CopperScreen Release build, host/disk/separate engine suites and +all three native Workbench/A1200 replays through NuGet; package/assets/loaded +CPU binaries match. No public package is published. + +Complete 040 discovery has 1,453,632 passing scenarios, no promoted mismatch/ +unsupported cases and 480 retained untested requirements. Active accurate-batch +fault delivery, internal-restoration faults, handler-entry prefetch, repaired-RTE +retry, user-tail validation, chained SR provenance, real general access-fault +frames, writeback/context transfer and earlier broader qualification/ +consolidation requirements remain open. The +[reference record](COPPER68K_REFERENCE_QUALIFICATION.md#040-access-fault-entry-halt-qualification--2026-10-05) +records authorities, exact identities, failed discovery, mutation evidence and +scope. Milestone 6 and the full goal remain **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index 8c12e529..ceeb2a55 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -22,6 +22,11 @@ $expected = [ordered]@{ 'address-error-fetch-040' = @{cases=2304; combinations=72} 'rte-validation-physical-direct' = @{cases=162816; combinations=2544} 'rte-validation-physical-chained' = @{cases=61056; combinations=15264} + 'rte-access-entry-double-fault' = @{cases=98304; combinations=3072} + 'rte-access-handler-refault' = @{cases=3840; combinations=120} + 'access-double-fault-dispatch-accurate' = @{cases=1088; combinations=544} + 'access-double-fault-dispatch-v1' = @{cases=1088; combinations=544} + 'access-double-fault-dispatch-v2' = @{cases=1088; combinations=544} 'rte-access-remaining-protocols' = @{cases=480; combinations=15} } $testExit = 0 @@ -35,19 +40,19 @@ if (-not $ValidateReportsOnly) { $saved[$name] = [Environment]::GetEnvironmentVariable($name) [Environment]::SetEnvironmentVariable($name, $settings[$name]) } - & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests|FullyQualifiedName~SyntheticM68040RteValidationFaultTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests|FullyQualifiedName~SyntheticM68040RteValidationFaultTests|FullyQualifiedName~SyntheticM68040AccessDoubleFaultTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output $testExit = $LASTEXITCODE } finally { foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } } $identity = [ordered]@{ schema=1; reference='MC68040UM'; url='https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf' - sections=@('8.2.2','8.4','8.4.1','8.4.2','8.4.3','8.4.4','8.4.6.2','8.4.6.7'); softwareReferenceExecuted=$false + sections=@('7.6.3','8.2.1','8.2.2','8.4','8.4.1','8.4.2','8.4.3','8.4.4','8.4.6.2','8.4.6.7'); softwareReferenceExecuted=$false sourceCommit=(& git -C $repo rev-parse HEAD); cpuCommittedTree=(& git -C $repo rev-parse HEAD:Copper68k) cpuSourceFiles=@(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } - inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs') | ForEach-Object { + inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } assemblies=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | ForEach-Object { @@ -56,7 +61,7 @@ if (-not $ValidateReportsOnly) { limitations=@('Synthetic frames and supervisor-stack physical map faults, not hardware captures or enabled-MMU access faults', 'CP context transfer and detailed fault protocol inventory remain untested and fail the gate', 'Multiple continuation bits are architecturally undefined and excluded', - 'User-tail validation, internal-restoration/double faults, chained odd-PC SR provenance and physical timing remain unqualified', + 'User-tail validation, internal-restoration double faults, handler-entry prefetch, active accurate-batch fault delivery, chained odd-PC SR provenance and physical timing remain unqualified', 'Direct odd-RTE saved-SR ordering uses documentary WinUAE 5d22d336, not an executed hardware oracle') } $identity | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'identities.json') @@ -66,11 +71,11 @@ if (-not $ValidateReportsOnly) { $identity = Get-Content -LiteralPath (Join-Path $output 'identities.json') -Raw | ConvertFrom-Json if ($identity.schema -ne 1 -or $identity.reference -cne 'MC68040UM' -or $identity.softwareReferenceExecuted -ne $false -or $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.cpuCommittedTree -notmatch '^[a-f0-9]{40}$' -or - @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 7 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 7 -or + @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 8 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 8 -or @($identity.assemblies).Count -ne 2) { throw '040 access-frame input identity is missing or incomplete' } foreach ($input in $identity.inputs) { $path = Join-Path $repo $input.file - if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs') -or + if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs') -or $input.sha256 -cne (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()) { throw '040 access-frame fixture/command identity differs' } } $cpuFiles = @(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') @@ -84,8 +89,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 21 -or [int]$counters.total -ne 21 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (15 batches, 6 fixed examples)' +if ([int]$counters.executed -ne 26 -or [int]$counters.total -ne 26 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (20 batches, 6 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -108,6 +113,33 @@ foreach ($group in $expected.Keys) { $expectedCombinations["68040/RTE/format7/$form/bank=$bank"] = 32 } } + } elseif ($group -in @('rte-access-entry-double-fault','rte-access-handler-refault')) { + foreach ($bank in @('ISP','MSP')) { foreach ($trace in @(0,0x8000,0x4000)) { foreach ($alignment in @(0,1)) { + foreach ($vbr in @(0,0x10000)) { + if ($group -eq 'rte-access-entry-double-fault') { + foreach ($first in @(0,8)) { foreach ($second in 0..63) { + $key = '68040/RTE/access-entry-double-fault/bank={0}/T={1:X4}/align={2}/VBR={3:X8}/first={4}/second={5}' -f $bank,$trace,$alignment,$vbr,$first,$second + $expectedCombinations[$key] = 32 + } } + } else { + foreach ($read in @(0,2,6,12,8)) { + $key = '68040/RTE/access-handler-refault/bank={0}/T={1:X4}/align={2}/VBR={3:X8}/read={4}' -f $bank,$trace,$alignment,$vbr,$read + $expectedCombinations[$key] = 32 + } + } + } + } } } + } elseif ($group.StartsWith('access-double-fault-dispatch-')) { + $engine = $group.Substring('access-double-fault-dispatch-'.Length) + foreach ($form in @('RTE','MOVE.B.read','MOVE.W.read','MOVE.L.read','MOVE.B.write','MOVE.W.write','MOVE.L.write')) { + foreach ($bank in @('ISP','MSP')) { foreach ($alignment in @(0,1)) { foreach ($stage in @('stack','vector')) { + $width = if ($stage -eq 'vector') {4} elseif ($form -eq 'RTE') {60} else {8} + for ($byte=0; $byte -lt $width; $byte++) { + $key = '68040/access-double-fault/dispatch={0}/form={1}/bank={2}/align={3}/stage={4}/byte={5}' -f $engine,$form,$bank,$alignment,$stage,$byte + $expectedCombinations[$key] = 2 + } + } } } + } } elseif ($group -in @('rte-validation-physical-direct','rte-validation-physical-chained')) { $chained = $group -eq 'rte-validation-physical-chained' $paths = if ($chained) { @('ISP-ISP','ISP-ISP-ISP','ISP-MSP-ISP','ISP-MSP','ISP-ISP-MSP','ISP-MSP-MSP', @@ -233,8 +265,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 21 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 26 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=1348704; xunitBatches=15; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=1454112; xunitBatches=20; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index c44cd086..24c260da 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','AccessDoubleFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -197,6 +197,37 @@ $mutations += @( @{name='rte-validation-saved-pc'; file='Copper68k/M68040Support.cs'; before=' RaiseRteValidationAccessFault(fault, stackedProgramCounter);'; after=' RaiseRteValidationAccessFault(fault, stackedProgramCounter + 2);'; group='SyntheticM68040RteValidationFaultTests'; milestone=6; model='68040'; validationFault=$true}, @{name='rte-validation-continuation-read'; file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs'; before='var address = continuation == 0 ? 0u : ReadRteFrameLong(frame + 8);'; after='var address = continuation == 0 ? 0u : ReadLong(frame + 8);'; group='SyntheticM68040RteValidationFaultTests'; milestone=6; model='68040'; validationFault=$true} ) +$doubleFaultMutations = @( + @{name='040-double-fault-latch'; file='Copper68k/M68040Support.cs'; before=' _accessErrorDoubleFaultHalted = true;'; after=' _accessErrorDoubleFaultHalted = false;'}, + @{name='040-double-fault-reset'; file='Copper68k/M68040Support.cs'; before=' _accessErrorDoubleFaultHalted = false;'; after=' // Mutant: external reset retains fatal latch.'}, + @{name='040-compiled-double-fault-halt'; file='Copper68k/M68kJitCore.cs'; before=' ((M68040Interpreter)_fallback).LatchAccessErrorDoubleFault();'; after=' // Mutant: compiled fatal entry fails to latch HALT.'}, + @{name='040-compiled-master-stack'; file='Copper68k/M68kJitCore.cs'; before=@' + private void RaiseM68040Format0Exception(int vector, uint stackedProgramCounter, int cycles) + { + var savedStatusRegister = State.StatusRegister; + State.RecordException(vector, stackedProgramCounter, savedStatusRegister); + State.StatusRegister = (ushort)((State.StatusRegister | M68kCpuState.Supervisor) & ~0xc000); +'@; after=@' + private void RaiseM68040Format0Exception(int vector, uint stackedProgramCounter, int cycles) + { + var savedStatusRegister = State.StatusRegister; + State.RecordException(vector, stackedProgramCounter, savedStatusRegister); + State.StatusRegister = (ushort)((State.StatusRegister | M68kCpuState.Supervisor) & ~M68kCpuState.Master); +'@} +) +foreach ($size in @('Byte','Word','Long')) { + $doubleFaultMutations += @{name="040-classic-read-$size"; file='Copper68k/M68kJitCore.cs'; + before=" if (_cpuModel != M68kJitCpuModel.M68000) return Read$size(address);"; + after=" // Mutant: classic $size read bypasses model-aware helper."} + $cast = if ($size -eq 'Byte') {'(byte)'} elseif ($size -eq 'Word') {'(ushort)'} else {''} + $doubleFaultMutations += @{name="040-classic-write-$size"; file='Copper68k/M68kJitCore.cs'; + before=" if (_cpuModel != M68kJitCpuModel.M68000) { Write$size(address, ${cast}value); return; }"; + after=" // Mutant: classic $size write bypasses model-aware helper."} +} +foreach ($mutation in $doubleFaultMutations) { + $mutation.group='WarmDispatchHaltsBothRteFallbackAndOperandFaultEntry'; $mutation.model='68040' + $mutation.milestone=6; $mutation.doubleFault=$true; $mutations += $mutation +} if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } if ($Scope -eq 'Logical') { $mutations = @($mutations | Where-Object { $_.milestone -eq 4 }) } @@ -204,6 +235,7 @@ if ($Scope -eq 'Control') { $mutations = @($mutations | Where-Object { $_.milest if ($Scope -eq 'Consolidation') { $mutations = @($mutations | Where-Object { $_.milestone -eq 6 }) } if ($Scope -eq 'Rte040') { $mutations = @($mutations | Where-Object { $_.odd }) } if ($Scope -eq 'RteValidationFault') { $mutations = @($mutations | Where-Object { $_.validationFault }) } +if ($Scope -eq 'AccessDoubleFault') { $mutations = @($mutations | Where-Object { $_.doubleFault }) } if ($Scope -eq 'LowPowerStop') { $mutations = @($mutations | Where-Object { $_.lpstop }) } if ($Scope -eq 'CacheEncodings') { $mutations = @($mutations | Where-Object { $_.cache }) } $saved = @{} @@ -264,6 +296,15 @@ try { throw 'RTE validation-fault mutation did not execute its complete two-batch selection' } } + if ($mutation.doubleFault) { + [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + if ($trx.TestRun.ResultSummary.Counters.executed -ne 3 -or $batches.Count -ne 3) { throw 'Double-fault mutation omitted dispatch batches' } + foreach ($engine in @('accurate','v1','v2')) { + if (@($batches | Where-Object { $_.model -eq '68040' -and $_.group -ceq "access-double-fault-dispatch-$engine" -and $_.logicalCases -eq 1088 }).Count -ne 1) { + throw "Double-fault mutation omitted complete $engine selection" + } + } + } if ($mutation.lpstop) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw $expectedBatches = 2 + [int][bool]$legacyPresent diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 44250177..9ea17359 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -148,6 +148,11 @@ try { $expected['system-move-word-restart-invalid'] = 224 } if ($model -eq '68040') { + $expected['rte-access-entry-double-fault'] = 98304 + $expected['rte-access-handler-refault'] = 3840 + $expected['access-double-fault-dispatch-accurate'] = 1088 + $expected['access-double-fault-dispatch-v1'] = 1088 + $expected['access-double-fault-dispatch-v2'] = 1088 $expected['rte-validation-physical-direct'] = 162816 $expected['rte-validation-physical-chained'] = 61056 $expected['rte-odd-normal'] = 69120 @@ -260,6 +265,7 @@ try { m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, and supervisor-stack physical RTE validation fault entry/handler return; chained odd-PC saved-SR provenance, user-tail/internal-restoration faults, CP context transfer, other real fault entry and physical timing remain unqualified'; m68040RtePhysicalValidationScope=@{logicalCases=223872; batches=2; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); trace=@(0,0x8000,0x4000); alignment=@(0,1); vbr=@(0,0x10000); faults='Each byte of SR/PC/format/SSW/continuation-EA reads; 14 direct/one/two-throwaway supervisor paths'; phases=@('fault entry','handler return'); undefined='EA and invalid writeback/push data; SSW X'; remaining='User-tail and internal-restoration/double faults, other instruction faults and software writeback handlers'}; + m68040AccessDoubleFaultScope=@{logicalCases=105408; batches=5; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; dispatchCcr=@(0,31); engines=@('accurate','classic','V2'); faults='Every byte of format-7 stack/vector; later handler validation refaults; warmed RTE fallback and compiled B/W/L operand read/write side exits'; recovery='External reset, including restored host entry; interrupt, task/subroutine entry and halted batches remain inactive'; undefined='Partial stack write order, contents and exact SP'; remaining='Internal restoration, handler-entry prefetch, active accurate-batch fault delivery, repaired original RTE retry, user-tail validation and general architectural access-fault restart'}; m68060LowPowerStopScope='Every unrecognized second opcode word in both privilege modes; fixed encoding, status, CCR and incoming trace cases across all profiles. Opcode-PC exception frames follow MC68060UM 8.2.4/5; physical broadcast, pins and ordinary STOP S-clear software disagreement remain unqualified'; cacheInstructionEncodingScope='Every F4xx opcode, all CCRs and both privilege states across eight profiles; scope zero is illegal on 040/060, including neither-cache forms, before privilege effects. Empty fixture caches qualify architectural state and exception outcomes, not physical invalidation, writeback, bus faults or timing'; m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; marked private word-MOVE/MOVEA images resume their faulted cycle; other opaque restart/input state, long transfers, RMW and physical timing remain unqualified'; From c46999824c6ab62b6220543e26c791e07e12e961 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 21:00:04 +0300 Subject: [PATCH 043/211] Handle 68040 physical faults during batch execution --- Copper68k.Tests/Synthetic/README.md | 16 +- .../SyntheticM68040AccessDoubleFaultTests.cs | 2 +- .../SyntheticM68040BatchFaultTests.cs | 259 ++++++++++++++++++ .../SyntheticM68040RteValidationFaultTests.cs | 24 +- Copper68k/M68040Support.cs | 6 + Copper68k/M68kAdvancedTimingInterpreter.cs | 95 ++++--- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 62 +++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 32 +++ scripts/test-copper68k-040-access-frames.ps1 | 2 +- scripts/test-copper68k-synthetic.ps1 | 5 +- 10 files changed, 457 insertions(+), 46 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index f13a5aca..a23c8a48 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -408,5 +408,17 @@ Run `./scripts/test-copper68k-synthetic-mutations.ps1 -Scope AccessDoubleFault` for maintained fatal-latch, reset, compiled-entry, master-stack and six classic memory-routing mutations. The complete 040 audit includes these combinations and fixture identities while retaining its failing untested protocol inventory. -Handler-entry prefetch, internal-restoration faults, active accurate-batch fault -delivery and repaired-original-RTE retry remain required work. +Handler-entry prefetch, internal-restoration faults and repaired-original-RTE +retry remain required work. + +`SyntheticM68040BatchFaultTests` adds two ordinary CI batches: +`rte-validation-batch` (129,024 scenarios) and `access-fault-batch-dispatch` +(10,368). They cover cold execution, warmed cached blocks, model-specific mixed +blocks and the self-branch path; completed prefixes, instruction caps, boundary +denial and cycle deadlines; both supervisor stacks and partial MOVE side effects. +The fixtures check instruction counts, callback counts, handler sentinels and +each byte of selected physical reads/writes. RTE validation uses independent +architectural format-7 expectations. Generic operand/fetch frames and scalar/ +batch cycle and bus-order equality verify existing execution policy, not general +architectural restart or physical timing. Dedicated discovery combination and +maintained mutation integration remain a later qualification step. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs index 85e4bfd3..14d85226 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs @@ -253,7 +253,7 @@ private static uint Prepare(SyntheticMachine m, FaultBus bus, string bank, ushor return frame; } - private sealed class FaultBus : SparseRecordingBus, IM68kPhysicalAddressMap + internal class FaultBus : SparseRecordingBus, IM68kPhysicalAddressMap { private readonly List<(uint At, M68kBusAccessKind Kind)> holes = []; public List<(uint Address, int Width, M68kBusAccessKind Kind, int AccessCount)> Rejected { get; } = []; diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs new file mode 100644 index 00000000..7b61b86b --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs @@ -0,0 +1,259 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Architectural validation frames: MC68040UM 8.4.6.7. Cached-block batching, +// callbacks and cycle equality are emulator execution/timing-policy contracts. +// Generic operand/fetch faults still use an approximate short frame: this audit +// does not promote it as architectural format-7 restart coverage. +public sealed class SyntheticM68040BatchFaultTests(ITestOutputHelper output) +{ + private const uint Handler = 0x9020; + private static readonly ModelSpec Model = ModelSpec.All.Single(x => x.Id == "68040"); + + [Fact, Trait("Suite", "Synthetic")] + public void RteValidationFaultsDeliverOnceAcrossColdAndCachedBatchPrefixes() + { + var scalarBus = new CodeBus(); var batchBus = new CodeBus(); + var scalar = new SyntheticMachine(Model, scalarBus); var batch = new SyntheticMachine(Model, batchBus); + var report = new CoverageBatch("68040", "rte-validation-batch"); + foreach (var cached in new[] { false, true }) + foreach (var prefix in new[] { 0, 1, 3 }) + foreach (var bank in new[] { "ISP", "MSP" }) + foreach (var trace in new ushort[] { 0, 0x8000, 0x4000 }) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var alignment in new uint[] { 0, 1 }) + foreach (var vbr in new uint[] { 0, 0x10000 }) + foreach (var stop in new[] { "cap", "before" }) + foreach (var (offset, width) in SyntheticM68040RteValidationFaultTests.Reads("CM")) + for (var faultByte = 0; faultByte < width; faultByte++) + { + var id = $"68040/RTE/validation-batch/cached={cached}/prefix={prefix}/bank={bank}/T={trace:X4}/align={alignment}/VBR={vbr:X8}/stop={stop}/read={offset}:{width}/byte={faultByte}/op=4E73/ccr={ccr:X2}"; + Run(scalar, scalarBus, batch, batchBus, report, id, "RTE", cached, prefix, bank, trace, ccr, + alignment, vbr, stop, "entry", offset, width, faultByte); + } + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void CachedOperandMixedAndSelfBranchFaultsPreserveCountsAndPartialEffects() + { + var scalarBus = new CodeBus(); var batchBus = new CodeBus(); + var scalar = new SyntheticMachine(Model, scalarBus); var batch = new SyntheticMachine(Model, batchBus); + var report = new CoverageBatch("68040", "access-fault-batch-dispatch"); + foreach (var form in new[] { "load", "store", "mixed-load", "mixed-store", "partial-store", "self-fetch" }) + foreach (var prefix in new[] { 0, 1, 3 }) + foreach (var bank in new[] { "ISP", "MSP" }) + foreach (var ccr in new[] { 0, 31 }) + foreach (var alignment in new uint[] { 0, 1 }) + foreach (var vbr in new uint[] { 0, 0x10000 }) + foreach (var stop in new[] { "cap", "before", "cycle" }) + foreach (var outcome in new[] { "entry", "fatal-stack", "fatal-vector" }) + for (var faultByte = 0; faultByte < 4; faultByte++) + { + var opcode = Opcode(form); + var id = $"68040/access-fault/batch/{form}/prefix={prefix}/bank={bank}/align={alignment}/VBR={vbr:X8}/stop={stop}/outcome={outcome}/byte={faultByte}/op={opcode:X4}/ccr={ccr:X2}"; + Run(scalar, scalarBus, batch, batchBus, report, id, form, true, prefix, bank, 0, ccr, + alignment, vbr, stop, outcome, 0, 4, faultByte); + } + report.Complete(output); + } + + private static void Run(SyntheticMachine scalar, CodeBus scalarBus, SyntheticMachine batch, CodeBus batchBus, + CoverageBatch report, string id, string form, bool cached, int prefix, string bank, ushort trace, int ccr, + uint alignment, uint vbr, string stop, string outcome, uint offset, int width, int faultByte) + { + try + { + var first = Prepare(scalar, scalarBus, form, cached, prefix, bank, trace, ccr, alignment, vbr, outcome, offset, width, faultByte); + var second = Prepare(batch, batchBus, form, cached, prefix, bank, trace, ccr, alignment, vbr, outcome, offset, width, faultByte); + var startCycle = scalar.Core.State.Cycles; var startNative = scalar.Core.State.NativeCycles; + for (var n = 0; n <= prefix; n++) + { + if (!first.Boundary.BeforeInstruction()) throw new InvalidOperationException("Scalar prefix boundary unexpectedly denied"); + var priorCycle = scalar.Core.State.Cycles; + scalar.Core.ExecuteInstruction(); first.Boundary.AfterInstruction(priorCycle, scalar.Core.State.Cycles); + } + var elapsed = scalar.Core.State.Cycles - startCycle; + var nativeElapsed = scalar.Core.State.NativeCycles - startNative; + var batchStart = batch.Core.State.Cycles; var batchNative = batch.Core.State.NativeCycles; + var count = ((IM68kBatchCore)batch.Core).ExecuteInstructions(stop == "cap" ? prefix + 1 : prefix + 8, + stop == "cycle" ? batchStart + elapsed : null, second.Boundary); + string? mismatch = null; + if (count != prefix + 1 || second.Boundary.Completed != prefix + 1 || + second.Boundary.BeforeCalls != prefix + 1 + (stop == "before" && outcome == "entry" ? 1 : 0)) + mismatch = $"Batch count/callbacks differ: count={count}, before={second.Boundary.BeforeCalls}, after={second.Boundary.Completed}"; + else if (elapsed <= 0 || batch.Core.State.Cycles - batchStart != elapsed || batch.Core.State.NativeCycles - batchNative != nativeElapsed) + mismatch = "Batch changed the scalar machine/native timing policy"; + else if (!scalarBus.Accesses.SequenceEqual(batchBus.Accesses)) + mismatch = "Batch repeated, omitted or reordered a CPU bus transfer"; + else if (scalarBus.Rejected.Count != (outcome == "entry" ? 1 : 2) || batchBus.Rejected.Count != scalarBus.Rejected.Count || + batchBus.Rejected[0].Address != second.FaultAddress || batchBus.Rejected[0].Width != width) + mismatch = "Fault width/address/count differs"; + else if (cached && form is "load" or "store" or "mixed-load" or "mixed-store" or "self-fetch" && + (batchBus.RootPeeksAtArm == 0 || batchBus.RootPeeks != batchBus.RootPeeksAtArm)) + mismatch = "Cached dispatch fixture did not retain its warmed root block"; + if (mismatch == null) + { + mismatch = outcome == "entry" ? first.Expected.Verify(scalar) : VerifyFatal(scalar, scalarBus, first); + mismatch ??= outcome == "entry" ? second.Expected.Verify(batch) : VerifyFatal(batch, batchBus, second); + } + if (mismatch == null && outcome == "entry") + { + // Ensure exception delivery did not consume the handler sentinel, + // leave a stale direct opcode, or retain incoming pending trace. + foreach (var (machine, fixture) in new[] { (scalar, first), (batch, second) }) + { + fixture.Expected.D[2] = 123; fixture.Expected.Pc = Handler + 2; + fixture.Expected.Sr = SyntheticExecution.MoveFlags(fixture.Expected.Sr, 123, 4); + machine.Core.ExecuteInstruction(); mismatch ??= fixture.Expected.Verify(machine); + } + } + report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); + } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException or UnsupportedM68040InstructionException) + { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + + private sealed record Fixture(ArchitecturalExpectation Expected, FaultBoundary Boundary, uint Frame, uint FaultAddress, uint Sequence); + + private static Fixture Prepare(SyntheticMachine m, CodeBus bus, string form, bool cached, int prefix, + string bank, ushort trace, int ccr, uint alignment, uint vbr, string outcome, uint offset, int width, int faultByte) + { + bus.Disarm(); bus.RootPeeks = 0; bus.RootPeeksAtArm = 0; bus.AllowPeek = cached; m.Reset(ccr); + var frame = (bank == "MSP" ? 0x7400u : 0x4700u) + alignment; + for (var n = -76; n < 80; n++) m.InitializePhysical(unchecked(frame + (uint)n), (uint)(n ^ 0x5a), 1); + m.InitializePhysical(frame, 0x801fu ^ (uint)ccr, 2); m.InitializePhysical(frame + 2, 0x6000, 4); + m.InitializePhysical(frame + 6, 0x7008, 2); m.InitializePhysical(frame + 8, 0x12345678, 4); + m.InitializePhysical(frame + 12, 0x1105, 2); + m.InitializePhysical(0x4200 + alignment, 0x12345678, 4); m.InitializePhysical(0x4300 + alignment, 0x9abcdef0, 4); + m.InitializePhysical(vbr + 8, Handler, 4); m.InitializePhysical(Handler, 0x747b, 2); + m.InitializePhysical(Handler + 2, 0x4e71, 2); + var words = new List(); + var mixed = form.StartsWith("mixed"); + for (var n = 0; n < prefix; n++) + { + if (mixed && n == 0) { words.Add(0xf200); words.Add(0x0080); } // FMOVE FP0,FP1, no arithmetic. + else words.Add(0x5287); // ADDQ.L #1,D7: completed prefix survives the fault. + } + var faultPc = SyntheticMachine.Code + (uint)words.Count * 2; + if (form == "self-fetch") { words = [0x4e71, 0x4e71, 0x4e71]; faultPc = 0x1006; } + words.Add(Opcode(form)); + if (form.EndsWith("store") && form != "partial-store") + { words.Add(0); words.Add((ushort)(0x4300 + alignment)); } + for (var n = 0; n < words.Count; n++) m.InitializePhysical(SyntheticMachine.Code + (uint)n * 2, words[n], 2); + m.InitializePhysical(SyntheticMachine.Code + (uint)words.Count * 2, 0x4e71, 2); + m.Start(); + m.Core.State.SetInterruptStackPointer(0x4700 + alignment); m.Core.State.SetMasterStackPointer(0x7400 + alignment); + m.Core.State.StatusRegister = (ushort)((bank == "MSP" ? 0x3000 : 0x2000) | ccr); + m.Core.State.A[0] = 0x4200 + alignment; m.Core.State.A[1] = 0x4304 + alignment; + m.Core.State.VectorBaseRegister = vbr; + if (form == "self-fetch") m.Core.State.ProgramCounter = faultPc; + var beforeWarm = ArchitecturalExpectation.Capture(m); + if (cached) + { + var warmCount = form == "RTE" ? prefix : form == "self-fetch" ? 4 : prefix + 1; + if (warmCount > 0 && ((IM68kBatchCore)m.Core).ExecuteInstructions(warmCount, null, new WarmBoundary()) != warmCount) + throw new InvalidOperationException("Warm prefix did not execute completely"); + beforeWarm.D.CopyTo(m.Core.State.D, 0); + for (var n = 0; n < 7; n++) m.Core.State.A[n] = beforeWarm.A[n]; + m.Core.State.SetInterruptStackPointer(0x4700 + alignment); m.Core.State.SetMasterStackPointer(0x7400 + alignment); + m.Core.State.StatusRegister = beforeWarm.Sr; m.Core.State.ProgramCounter = beforeWarm.Pc; + m.Core.State.M68040Fpu.Reset(); + bus.Memory.Clear(); foreach (var item in beforeWarm.Memory) bus.Memory.Add(item.Key, item.Value); + } + bus.Accesses.Clear(); + var e = ArchitecturalExpectation.Capture(m); + var savedSr = (ushort)((bank == "MSP" ? 0x3000 : 0x2000) | trace | ccr); + e.Sr = (ushort)(savedSr & ~0xc000); e.Pc = Handler; e.ExceptionVector = 2; + e.D[7] += form == "self-fetch" ? 0u : (uint)(prefix - (mixed && prefix > 0 ? 1 : 0)); + if (form == "partial-store") { e.A[0] += 4; e.A[1] -= 4; } + var kind = form == "self-fetch" ? M68kBusAccessKind.CpuInstructionFetch : form.EndsWith("store") ? M68kBusAccessKind.CpuDataWrite : M68kBusAccessKind.CpuDataRead; + var faultAddress = form == "RTE" ? frame + offset : form == "self-fetch" ? 0x1004u : form.EndsWith("store") ? 0x4300 + alignment : 0x4200 + alignment; + var stackedPc = form == "self-fetch" ? faultAddress : faultPc; + var sequence = m.Core.State.ExceptionSequence; + e.ControlChecks["single exception"] = (s => s.ExceptionSequence, sequence + 1); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, savedSr); + e.ControlChecks["saved PC (generic short-frame policy where applicable)"] = (s => s.LastExceptionStackedProgramCounter, stackedPc); + e.ControlChecks["bypass cleared"] = (s => s.M68040Mmu.BypassTranslation ? 1u : 0u, 0); + var accessFrame = frame - (form == "RTE" ? 60u : 8u); + e.A[7] = accessFrame; if (bank == "MSP") e.MasterStackPointer = accessFrame; + if (form == "RTE") + SyntheticM68040RteValidationFaultTests.ExpectAccessFrame(m, e, accessFrame, savedSr, faultPc, faultAddress, width); + else + { + // Existing generic delivery policy, not architectural frame qualification. + e.Write(accessFrame, savedSr, 2, m.Model); e.Write(accessFrame + 2, stackedPc, 4, m.Model); + e.Write(accessFrame + 6, 8, 2, m.Model); + } + var boundary = new FaultBoundary(m.Core, bus, prefix, savedSr, faultAddress + (uint)faultByte, kind, + outcome == "fatal-stack" ? accessFrame : outcome == "fatal-vector" ? vbr + 8 : null, + outcome == "fatal-stack" ? M68kBusAccessKind.CpuDataWrite : M68kBusAccessKind.CpuDataRead); + return new(e, boundary, frame, faultAddress, sequence); + } + + private static string? VerifyFatal(SyntheticMachine m, CodeBus bus, Fixture fixture) + { + var s = m.Core.State; var e = fixture.Expected; + if (!s.Halted || s.Stopped || s.StatusRegister != e.Sr || s.LastExceptionVector != 2 || + s.LastExceptionStatusRegister != e.ControlChecks["saved SR"].Value || s.ExceptionSequence != fixture.Sequence + 1 || + s.M68040Mmu.BypassTranslation) return "Batch did not retain fatal exception state/provenance"; + if (!s.D.SequenceEqual(e.D) || !s.A.Take(7).SequenceEqual(e.A.Take(7))) return "Fatal batch changed unrelated or partially committed registers"; + if (s.A[7] < e.A[7] || s.A[7] > fixture.Frame || bus.Accesses.Count != bus.Rejected[^1].AccessCount) + return "Fatal batch changed memory after rejection or escaped its partial frame"; + if (bus.Accesses.Where(a => a.Write).Any(a => a.Address < e.A[7] || (ulong)a.Address + (uint)a.Width > fixture.Frame)) + return "Fatal batch wrote outside its partial frame"; + // Original operand accesses are reads only before the rejected write. + // Compare outside the opaque partial frame against the initial memory. + if (e.Memory.Keys.Concat(bus.Memory.Keys).Distinct().Any(at => (at < e.A[7] || at >= fixture.Frame) && + e.Memory.GetValueOrDefault(at) != bus.Peek(at))) return "Fatal batch changed original operands/frame/neighbors"; + return null; + } + + private static ushort Opcode(string form) => form switch + { + "RTE" => 0x4e73, "load" or "mixed-load" => 0x2018, "store" or "mixed-store" => 0x23c0, + "partial-store" => 0x2318, "self-fetch" => 0x60fe, _ => throw new ArgumentException("Unknown form") + }; + + private sealed class CodeBus : SyntheticM68040AccessDoubleFaultTests.FaultBus, IM68kCodeReader + { + public bool AllowPeek { get; set; } + public int RootPeeks { get; set; } + public int RootPeeksAtArm { get; set; } + public ushort ReadHostWord(uint address) + { + if (!AllowPeek) throw M68kCodeReadException.Instance; + if (address >= SyntheticMachine.Code && address < 0x1100) RootPeeks++; + return (ushort)Peek(address, 2); + } + } + + private sealed class WarmBoundary : IM68kInstructionBoundary + { + public bool BeforeInstruction() => true; + public void AfterInstruction(long previous, long current) { } + } + + private sealed class FaultBoundary(IM68kCore core, CodeBus bus, int prefix, ushort incomingSr, + uint firstByte, M68kBusAccessKind firstKind, uint? secondByte, M68kBusAccessKind secondKind) : IM68kInstructionBoundary + { + public int BeforeCalls { get; private set; } + public int Completed { get; private set; } + public bool BeforeInstruction() + { + BeforeCalls++; + if (Completed > prefix) return false; + if (Completed == prefix) + { + core.State.StatusRegister = incomingSr; + bus.RootPeeksAtArm = bus.RootPeeks; + bus.Arm(firstByte, firstKind); if (secondByte.HasValue) bus.Arm(secondByte.Value, secondKind); + } + return true; + } + public void AfterInstruction(long previous, long current) => Completed++; + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs index 41b47f38..8c38bb6d 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs @@ -104,7 +104,20 @@ private static void Run(SyntheticMachine m, ValidationFaultBus bus, CoverageBatc e.ControlChecks["handler sentinel never executed"] = (s => s.D[0], e.D[0]); if (form is "CU" or "CP") e.ControlChecks["pending delivery retained"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) != null ? 1u : 0u, 1); - e.Write(accessFrame, priorSr, 2, m.Model); e.Write(accessFrame + 2, SyntheticMachine.Code, 4, m.Model); + ExpectAccessFrame(m, e, accessFrame, priorSr, SyntheticMachine.Code, frame + offset, width); + bus.Arm(frame + offset + (uint)faultByte); + var id = $"68040/RTE/validation-physical/{form}/path={string.Join('-', path)}/T={trace:X4}/align={alignment}/VBR={vbr:X8}/read={offset}:{width}/fault-byte={faultByte}/op=4E73/ccr={ccr:X2}"; + if (!SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/fault-entry")) + { report.Record(id + "/handler-return", "untested", "Validation fault entry prerequisite failed"); return; } + pointers[tail] = frame; + SetStacks(e, pointers, tail, priorSr); e.Pc = SyntheticMachine.Code; e.ExceptionVector = null; + SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/handler-return"); + } + + internal static void ExpectAccessFrame(SyntheticMachine m, ArchitecturalExpectation e, uint accessFrame, + ushort priorSr, uint instructionPc, uint faultAddress, int width) + { + e.Write(accessFrame, priorSr, 2, m.Model); e.Write(accessFrame + 2, instructionPc, 4, m.Model); e.Write(accessFrame + 6, 0x7008, 2, m.Model); // EA/invalid WB data are undefined. Verify defined status/address fields, // all untouched original-frame bytes and all neighboring memory instead. @@ -116,15 +129,8 @@ private static void Run(SyntheticMachine m, ValidationFaultBus bus, CoverageBatc e.Write(accessFrame + at, 0, 2, m.Model); e.MemoryMasks[accessFrame + at + 1] = 0x80; // WB valid bit; no pending write } - e.Write(accessFrame + 20, frame + offset, 4, m.Model); + e.Write(accessFrame + 20, faultAddress, 4, m.Model); for (uint n = 20; n < 24; n++) e.MemoryMasks[accessFrame + n] = 0xff; - bus.Arm(frame + offset + (uint)faultByte); - var id = $"68040/RTE/validation-physical/{form}/path={string.Join('-', path)}/T={trace:X4}/align={alignment}/VBR={vbr:X8}/read={offset}:{width}/fault-byte={faultByte}/op=4E73/ccr={ccr:X2}"; - if (!SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/fault-entry")) - { report.Record(id + "/handler-return", "untested", "Validation fault entry prerequisite failed"); return; } - pointers[tail] = frame; - SetStacks(e, pointers, tail, priorSr); e.Pc = SyntheticMachine.Code; e.ExceptionVector = null; - SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/handler-return"); } private static ushort Status(string bank, ushort trace, int ccr) => (ushort)((bank == "MSP" ? 0x3000 : 0x2000) | trace | ccr); diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index 562b940f..d57729f1 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -2537,6 +2537,12 @@ public override int ExecuteInstruction() } } + protected override bool TryHandleM68040ExecutionFault(M68040MmuFault fault) + { + RaiseMmuFault(fault); + return true; + } + private void InvalidateInstructionCacheForHostCodeVisibilityChange() { var stableMap = _stablePhysicalAddressMap; diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index 097585a4..f672e420 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -4501,6 +4501,20 @@ public virtual int ExecuteInstruction() } private void ExecuteInstructionCore() + { + try + { + ExecuteInstructionWithTrace(); + } + catch (M68040MmuFaultException ex) + { + if (!TryHandleM68040ExecutionFault(ex.Fault)) throw; + } + } + + protected virtual bool TryHandleM68040ExecutionFault(M68040MmuFault fault) => false; + + private void ExecuteInstructionWithTrace() { if ((State.StatusRegister & 0xC000) == 0 || State.Halted || State.Stopped) { ExecuteInstructionBody(); return; } @@ -4687,20 +4701,26 @@ private bool TryExecuteHotBlock( } var previousCycle = State.Cycles; - if (!TryFetchHotOpcode(in hotInstruction, out var opcode) || - opcode != hotInstruction.Opcode) + var exitBlock = false; + try { - _hotBlocks![cacheSlot].Valid = false; - ExecuteInstructionCore(); - boundary.AfterInstruction(previousCycle, State.Cycles); - executedInstructions++; - return true; + if (!TryFetchHotOpcode(in hotInstruction, out var opcode) || + opcode != hotInstruction.Opcode) + { + _hotBlocks![cacheSlot].Valid = false; + ExecuteInstructionCore(); + exitBlock = true; + } + else ExecuteHotInstruction(hotInstruction.Kind, opcode); + } + catch (M68040MmuFaultException ex) + { + if (!TryHandleM68040ExecutionFault(ex.Fault)) throw; + exitBlock = true; } - - ExecuteHotInstruction(hotInstruction.Kind, opcode); boundary.AfterInstruction(previousCycle, State.Cycles); executedInstructions++; - if (State.Halted || State.Stopped || (State.StatusRegister & 0xC000) != 0) + if (exitBlock || State.Halted || State.Stopped || (State.StatusRegister & 0xC000) != 0) { return true; } @@ -4775,31 +4795,31 @@ private bool TryExecuteModelSpecificHotBlock( } var previousCycle = State.Cycles; - if (!TryFetchHotOpcode(in hotInstruction, out var opcode) || - opcode != hotInstruction.Opcode) - { - _hotBlocks![cacheSlot].Valid = false; - ExecuteInstructionCore(); - boundary.AfterInstruction(previousCycle, State.Cycles); - executedInstructions++; - return true; - } - - if (hotInstruction.Kind == M68kAdvancedFastKind.M68040FpuRegister) + var exitBlock = false; + try { - if (!TryExecuteFastModelSpecificInstruction(opcode)) + if (!TryFetchHotOpcode(in hotInstruction, out var opcode) || + opcode != hotInstruction.Opcode) + { + _hotBlocks![cacheSlot].Valid = false; + ExecuteInstructionCore(); + exitBlock = true; + } + else if (hotInstruction.Kind == M68kAdvancedFastKind.M68040FpuRegister) { - throw new InvalidOperationException("The MC68040 FPU hot instruction was not handled."); + if (!TryExecuteFastModelSpecificInstruction(opcode)) + throw new InvalidOperationException("The MC68040 FPU hot instruction was not handled."); } + else ExecuteHotInstruction(hotInstruction.Kind, opcode); } - else + catch (M68040MmuFaultException ex) { - ExecuteHotInstruction(hotInstruction.Kind, opcode); + if (!TryHandleM68040ExecutionFault(ex.Fault)) throw; + exitBlock = true; } - boundary.AfterInstruction(previousCycle, State.Cycles); executedInstructions++; - if (State.Halted || State.Stopped || (State.StatusRegister & 0xC000) != 0) + if (exitBlock || State.Halted || State.Stopped || (State.StatusRegister & 0xC000) != 0) { return true; } @@ -4911,11 +4931,22 @@ private bool TryExecuteShortSelfBranchBlock( } var previousCycle = State.Cycles; - var opcode = _timedBus.ReadInstructionFetchWordHot( - hotInstruction.Address, - out var cacheHit, - out var requiresSynchronization, - out var completedMachineCycle); + ushort opcode; + bool cacheHit, requiresSynchronization; + long completedMachineCycle; + try + { + opcode = _timedBus.ReadInstructionFetchWordHot( + hotInstruction.Address, out cacheHit, + out requiresSynchronization, out completedMachineCycle); + } + catch (M68040MmuFaultException ex) + { + if (!TryHandleM68040ExecutionFault(ex.Fault)) throw; + boundary.AfterInstruction(previousCycle, State.Cycles); + executedInstructions++; + return true; + } if (opcode != hotInstruction.Opcode) { _hotBlocks![cacheSlot].Valid = false; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index c7e8d62a..aa3284f7 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -3927,3 +3927,65 @@ Consumer evidence is under `artifacts/access-entry-double-fault-validation-v2/`, separate diagnostic outputs and `artifacts/access-entry-double-fault-identities.json`. Root CopperScreen edits and published packages remain untouched. All remaining reference/consolidation requirements persist with `roadmapComplete=false`. + +## 040 accurate batch-fault checkpoint — 2026-10-05 + +Physical faults previously escaped accurate batch execution, including cached +blocks, although scalar execution delivered them. Shared instruction execution +now offers the 040 fault handler; normal/model-specific cached blocks and cached +self-branches also deliver faults and count the failed instruction exactly once. +Boundary callbacks remain outside fault handling. Completed prefixes and partial +MOVE address-register updates are preserved without retry. + +`SyntheticM68040BatchFaultTests` contributes `rte-validation-batch` (129,024 +scenarios, 4,032 combinations) and `access-fault-batch-dispatch` (10,368 scenarios, +5,184 combinations). The former combines cold/warmed execution, prefixes 0/1/3, +ISP/MSP, all CCRs, defined incoming trace modes, alignment, VBR, instruction-cap/ +boundary limits and each byte of the five validation reads. The latter covers +warmed load/store, mixed model-specific prefixes, partial source/destination +updates and self-branch fetch faults; instruction caps, boundary denial and cycle +deadlines; successful entry and fatal stack/vector faults. Counts, callbacks, +handler sentinels, surrounding state and exact scalar/batch transfer sequences +are checked. Host code peeks are separate from CPU accesses. + +Architectural validation-frame expectations follow MC68040UM 8.4.6.7, and fatal +entry follows 7.6.3/8.2.1. Existing generic short operand/fetch frames remain +approximate and unqualified as architectural format-7 restart. Scalar/batch +machine/native cycle equality and bus-order equality qualify execution policy, +not physical timing. The FMOVE prefix selects model-specific block dispatch; +it does not qualify FPU arithmetic. Enabled MMU operation remains outside scope. + +Failed-before evidence in `artifacts/m6-batch-fault-before/before.trx` records +two failed batches and all 139,392 scenarios mismatching due to escaped faults. +`artifacts/m6-batch-fault-fixed/fixed.trx` records two passing batches, with zero +mismatches, unsupported execution or untested promoted scenarios. The strict +ordinary gate requires both exact report counts. No old regression is retired. +Dedicated discovery distribution and maintained per-path mutation integration +are pending; the existing 480-case untested inventory and broader qualification +requirements remain. This checkpoint does not complete milestone 6 or authorize +public package publication. + +Final validation in `artifacts/m6-batch-fault-full/` passes **5,053 Release CPU +tests, eleven optional skips and zero failures**. All nine qualified WinUAE +presets retain their exact directory/callback/frame selections and matching +CPU/adapter identities in `qualified-preset-identities.json`. The strict gate +passes **16,561,686 logical cases in 612 reporting batches**, with fresh pinned +SingleStepTests (312,500 cases / 125 files) and Musashi (536 programs / 88 explicit +exclusions). Four controls in `artifacts/m6-batch-fault-report-controls/controls.json` +reject missing and shortened reports for each new batch, then restore them. +Audits and package validation use the same compiled snapshot with `--no-build`. +CPU SHA-256: `178df423e5c24ff107d41ba49dd5789d432626260b67cc3cb95dce5c5ca4d1f0`; +test/reference adapter SHA-256: +`30535c74aa5af87576df8835d52bc7755d6f8cd4d2999e325781a93de90f5e80`. + +Isolated CopperScreen baseline `d9beae8b88be24032221e3482942a249c03c27d3` +passes through unpublished private NuGet `1.5.2-synthetic-dev.60`: Release build +zero warnings/errors, host 149/six optional skips, disk 74, separate engine 1,080 +and three native Workbench/A1200 boot/persistence replays without native skips. +Four assets and loaded CPU assemblies match the package and CPU above. Package +SHA-256: `e83f68bf8572cdf0b00a63eda6abd5b3a7b4be5a21d80dcbadb2f29fe6f8b001`. +Consumer evidence is under `artifacts/batch-fault-validation/`, separate +diagnostic outputs and `artifacts/batch-fault-identities.json`. Root CopperScreen +changes and public packages remain untouched. Dedicated 040 discovery was not +rerun or extended in this checkpoint; its previously recorded 480 untested +requirements remain, with `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index bb04dbc7..e1b31fee 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1462,3 +1462,35 @@ consolidation requirements remain open. The [reference record](COPPER68K_REFERENCE_QUALIFICATION.md#040-access-fault-entry-halt-qualification--2026-10-05) records authorities, exact identities, failed discovery, mutation evidence and scope. Milestone 6 and the full goal remain **in progress**, `roadmapComplete=false`. + +### Milestone 6 accurate 040 batch-fault checkpoint — 2026-10-05 + +Accurate 040 batch execution now delivers physical faults through the same +exception handler as scalar execution. Cold, normal cached, model-specific +cached and self-branch paths preserve completed instruction counts and boundary +callbacks. A failed instruction is not retried after partial operand effects. +Other models retain their existing fault behavior and the existing timing policy. + +Two ordinary-CI reporting batches add 139,392 scenarios: 129,024 supervisor RTE +validation faults and 10,368 operand/fetch dispatch cases. They check warmed +block use, completed prefixes, both supervisor stacks, all CCRs for RTE, +selected physical byte faults, trace, odd/even addresses, VBR, batch limits, +fatal second faults and handler sentinels. Both batches failed completely +before correction and pass after it. Architectural RTE expectations are +independent; generic short frames and scalar/batch bus and cycle equality are +existing execution-policy checks, not architectural restart or physical timing. + +No regression is retired. Dedicated 040 discovery combination integration and +maintained per-path mutation integration remain pending. The 480-case discovery +inventory is retained, along with user-tail/internal-restoration faults, entry +prefetch, repaired-original-RTE retry, general architectural format-7 restart, +writeback/context transfer and earlier reference/consolidation gaps. Milestone 6 +and the full goal remain **in progress** with `roadmapComplete=false`. +See the [checkpoint record](COPPER68K_REFERENCE_QUALIFICATION.md#040-accurate-batch-fault-checkpoint--2026-10-05). + +Validation passes 5,053 Release CPU tests with eleven optional skips, all nine +qualified WinUAE presets and 16,561,686 strict logical cases in 612 batches. +Fresh pinned SingleStepTests/Musashi audits pass. Four report controls reject +missing/shortened new batches. Isolated unpublished `.60` passes the CopperScreen +Release build, host/disk/separate engine suites and all three native boot replays; +the package, assets and loaded CPU binaries match. No public release is made. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index ceeb2a55..15fc0b45 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -61,7 +61,7 @@ if (-not $ValidateReportsOnly) { limitations=@('Synthetic frames and supervisor-stack physical map faults, not hardware captures or enabled-MMU access faults', 'CP context transfer and detailed fault protocol inventory remain untested and fail the gate', 'Multiple continuation bits are architecturally undefined and excluded', - 'User-tail validation, internal-restoration double faults, handler-entry prefetch, active accurate-batch fault delivery, chained odd-PC SR provenance and physical timing remain unqualified', + 'User-tail validation, internal-restoration double faults, handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified; selected active accurate-batch paths have separate ordinary-CI coverage pending dedicated discovery integration', 'Direct odd-RTE saved-SR ordering uses documentary WinUAE 5d22d336, not an executed hardware oracle') } $identity | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'identities.json') diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 9ea17359..8572016e 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -148,6 +148,8 @@ try { $expected['system-move-word-restart-invalid'] = 224 } if ($model -eq '68040') { + $expected['rte-validation-batch'] = 129024 + $expected['access-fault-batch-dispatch'] = 10368 $expected['rte-access-entry-double-fault'] = 98304 $expected['rte-access-handler-refault'] = 3840 $expected['access-double-fault-dispatch-accurate'] = 1088 @@ -265,7 +267,8 @@ try { m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, and supervisor-stack physical RTE validation fault entry/handler return; chained odd-PC saved-SR provenance, user-tail/internal-restoration faults, CP context transfer, other real fault entry and physical timing remain unqualified'; m68040RtePhysicalValidationScope=@{logicalCases=223872; batches=2; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); trace=@(0,0x8000,0x4000); alignment=@(0,1); vbr=@(0,0x10000); faults='Each byte of SR/PC/format/SSW/continuation-EA reads; 14 direct/one/two-throwaway supervisor paths'; phases=@('fault entry','handler return'); undefined='EA and invalid writeback/push data; SSW X'; remaining='User-tail and internal-restoration/double faults, other instruction faults and software writeback handlers'}; - m68040AccessDoubleFaultScope=@{logicalCases=105408; batches=5; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; dispatchCcr=@(0,31); engines=@('accurate','classic','V2'); faults='Every byte of format-7 stack/vector; later handler validation refaults; warmed RTE fallback and compiled B/W/L operand read/write side exits'; recovery='External reset, including restored host entry; interrupt, task/subroutine entry and halted batches remain inactive'; undefined='Partial stack write order, contents and exact SP'; remaining='Internal restoration, handler-entry prefetch, active accurate-batch fault delivery, repaired original RTE retry, user-tail validation and general architectural access-fault restart'}; + m68040AccessDoubleFaultScope=@{logicalCases=105408; batches=5; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; dispatchCcr=@(0,31); engines=@('accurate','classic','V2'); faults='Every byte of format-7 stack/vector; later handler validation refaults; warmed RTE fallback and compiled B/W/L operand read/write side exits'; recovery='External reset, including restored host entry; interrupt, task/subroutine entry and halted batches remain inactive'; undefined='Partial stack write order, contents and exact SP'; remaining='Internal restoration, handler-entry prefetch, repaired original RTE retry, user-tail validation and general architectural access-fault restart'}; + m68040BatchFaultScope=@{logicalCases=139392; batches=2; translationEnabled=$false; paths=@('cold','cached','model-specific cached','self-branch cached'); limits=@('instruction cap','boundary denial','cycle deadline'); architectural='Supervisor RTE validation frames and fatal second-fault halt'; policy='Generic short operand/fetch frames, scalar/batch bus order and machine/native cycle equality'; remaining='General format-7 restart, entry prefetch, user-tail/internal-restoration faults and dedicated discovery/mutation-gate integration'}; m68060LowPowerStopScope='Every unrecognized second opcode word in both privilege modes; fixed encoding, status, CCR and incoming trace cases across all profiles. Opcode-PC exception frames follow MC68060UM 8.2.4/5; physical broadcast, pins and ordinary STOP S-clear software disagreement remain unqualified'; cacheInstructionEncodingScope='Every F4xx opcode, all CCRs and both privilege states across eight profiles; scope zero is illegal on 040/060, including neither-cache forms, before privilege effects. Empty fixture caches qualify architectural state and exception outcomes, not physical invalidation, writeback, bus faults or timing'; m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; marked private word-MOVE/MOVEA images resume their faulted cycle; other opaque restart/input state, long transfers, RMW and physical timing remain unqualified'; From 9ab8d56030898e520c42142a59ce1191e5965d83 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 21:14:23 +0300 Subject: [PATCH 044/211] Qualify 68040 batch-fault coverage and mutation gates --- Copper68k.Tests/Synthetic/README.md | 17 +++- .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 71 ++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 28 +++++- scripts/test-copper68k-040-access-frames.ps1 | 47 ++++++++-- .../test-copper68k-synthetic-mutations.ps1 | 94 ++++++++++++++++++- scripts/test-copper68k-synthetic.ps1 | 2 +- 7 files changed, 246 insertions(+), 15 deletions(-) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index a23c8a48..6ec1137b 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -420,5 +420,18 @@ The fixtures check instruction counts, callback counts, handler sentinels and each byte of selected physical reads/writes. RTE validation uses independent architectural format-7 expectations. Generic operand/fetch frames and scalar/ batch cycle and bus-order equality verify existing execution policy, not general -architectural restart or physical timing. Dedicated discovery combination and -maintained mutation integration remain a later qualification step. +architectural restart or physical timing. The dedicated 040 discovery command +requires these reports, independently enumerates their 4,032 and 5,184 +combinations and binds their fixture and CPU identities. It executes 28 tests +(22 reporting batches and six fixed examples) while retaining the failing +480-case untested protocol inventory. + +Run `./scripts/test-copper68k-synthetic-mutations.ps1 -Scope BatchFault` to +remove handling separately from cold, normal cached, model-specific cached and +self-branch execution, change the self-branch count, omit the cached callback, +or retry after partial operand effects. Every mutation must execute both full +batches and detect mismatches in its intended path; a build failure, empty +selection or failure only in another path is insufficient. Sources are restored +and rebuilt even when qualification fails. General format-7 restart, user-tail +and internal-restoration faults, handler-entry prefetch and repaired-RTE retry +remain required work. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 0a6abd9a..a026deea 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -33,7 +33,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() "real-access-fault-entry", "writeback-handler", "CP-context-transferred-vector" }) report.Record($"68040/RTE/format7/{form}/bank={bank}/op=4E73/ccr={ccr:X2}", "untested", form == "frame-validation-fault" - ? "Supervisor validation and access-fault stacking/vector double faults are covered separately; user-tail validation, internal restoration, handler-entry prefetch and active accurate-batch delivery remain required" + ? "Supervisor validation, access-fault stacking/vector double faults and selected accurate-batch paths are covered separately; user-tail validation, internal restoration and handler-entry prefetch remain required" : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index aa3284f7..663ba3c9 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -3989,3 +3989,74 @@ diagnostic outputs and `artifacts/batch-fault-identities.json`. Root CopperScree changes and public packages remain untouched. Dedicated 040 discovery was not rerun or extended in this checkpoint; its previously recorded 480 untested requirements remain, with `roadmapComplete=false`. + +## 040 batch-fault gate and mutation qualification — 2026-10-05 + +The dedicated `test-copper68k-040-access-frames.ps1` selection now includes both +ordinary batch-fault groups. Its independent enumeration specifies cached/cold +execution, prefix lengths, stack banks, valid trace modes, alignment, VBR, limits, +fault outcomes and bytes. Validation read ranges are fixed in the command rather +than imported from the C# fixture. Each of 4,032 RTE combinations must contain +32 CCR cases; each of 5,184 dispatch combinations contains CCR 0/31. Independent +enumeration cardinalities are themselves checked. The command requires nine +distinct fixture/command identities, exact CPU sources and both compiled binaries. + +Fresh `artifacts/m6-batch-fault-discovery/` executes 28 tests: 27 pass and the +retained inventory fails on 480 untested cases. All 22 reports and six fixed +examples are present, with **1,593,024 passing scenarios, zero mismatches and +zero unsupported execution**. Including the inventory gives 1,593,504 logical +cases. The command validates these distributions and identities before reporting +the expected incomplete gate. Its inventory reason now acknowledges selected +active-batch coverage without removing any user-tail or internal-restoration gap. + +Seven maintained mutations execute all 139,392 cases in both batch-fault groups +on every run. Detection requires mismatches in the intended combination, in +addition to complete xUnit/report selection. Thus a compiler failure, empty +selection or unrelated failure cannot satisfy qualification. Evidence: +`artifacts/m6-batch-fault-mutations/mutation-proof.json` and per-mutation reports. + +| Mutation | RTE mismatches | Dispatch mismatches | Intended detection | +| --- | ---: | ---: | --- | +| Cold delivery removed | 129,024 | 1,728 | Cold RTE and slow partial MOVE path | +| Normal cached delivery removed | 0 | 4,608 | Warm load/store and mixed forms without a model-specific prefix | +| Model-specific cached delivery removed | 0 | 2,304 | Mixed blocks with completed FPU register-transfer prefix | +| Self-branch delivery removed | 0 | 1,728 | Warm self-branch physical fetch | +| Self-branch count doubled | 0 | 1,728 | Instruction-count contract | +| Cached callback omitted on fault | 0 | 4,608 | Boundary callback contract | +| Failed instruction retried | 129,024 | 3,456 | Successful-entry cases, including partial MOVE effects | + +All mutation cases execute with zero unsupported or untested outcomes. The retry +mutation explicitly rewinds the failed PC after delivery and executes again; +partial-store combinations detect repeated operand effects independently of +scalar/batch agreement. Production source is restored byte-for-byte and rebuilt +with zero warnings/errors. No regression is retired and no CPU behavior changes +are added by this qualification checkpoint. Generic short frames, callback/count +and bus/cycle policies retain their previously documented scope; this does not +qualify general architectural restart, enabled MMU, FPU arithmetic or physical +timing. + +Eleven copied-input controls in +`artifacts/m6-batch-fault-dedicated-report-controls/controls.json` reject missing, +shortened, foreign and redistributed combinations for both new groups, plus a +missing fixture identity and altered CPU-source/assembly identities. Distribution +controls preserve total cases and combination counts, proving that fixed counts +per architectural combination are enforced independently of aggregate totals. +Every control fails for its specific intended reason. + +Qualification starts from `c46999824c6ab62b6220543e26c791e07e12e961`, CPU tree +`6bb8bd68a93ba23bf79d1d077d0b34c48e5e6679`. Current discovery identities record +CPU SHA-256 `fc36b2ce0d0f4a816d08e322836d7d59feb333d71d7909fe3b747f3b44e247c9` +and test/reference adapter SHA-256 +`c1163ad174e5c900dda725b6726427ad0bf1a72aef54e0cfe71a6381aa79b87c`. +These rebuilt identities belong to this discovery run; the preceding full CPU, +external-reference and private-consumer evidence remains separately bound to its +recorded binaries. Ordinary report validation retains 16,561,686 logical cases +in 612 batches; this checkpoint revalidates those reports rather than claiming +another full ordinary execution or external audit. No new production change, +consumer package or public release is introduced. + +Dedicated batch-fault integration and its mutation requirement are now satisfied. +The 480-case inventory, user-tail/internal-restoration faults, handler-entry +prefetch, repaired-original-RTE retry, general format-7 restart, writeback/context +transfer and the earlier model/reference/consolidation requirements remain open. +Milestone 6 remains **in progress**, with `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index e1b31fee..5126fc02 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -122,7 +122,7 @@ identities, replacement proofs and remaining required coverage here as work progresses. No milestone is complete merely because an inventory exists or a partial matrix passes. -Latest milestone-6 checkpoint, 2026-10-05: the exhaustive cache-encoding matrix +Earlier milestone-6 checkpoint, 2026-10-05: the exhaustive cache-encoding matrix passes 131,072 cases across eight profiles, including illegal scope-zero and neither-cache forms. Independent scope-zero reference qualification passes 2,048 callbacks/frames with exact opcode/status distributions and 32 comparator @@ -1494,3 +1494,29 @@ Fresh pinned SingleStepTests/Musashi audits pass. Four report controls reject missing/shortened new batches. Isolated unpublished `.60` passes the CopperScreen Release build, host/disk/separate engine suites and all three native boot replays; the package, assets and loaded CPU binaries match. No public release is made. + +### Milestone 6 batch-fault qualification gate — 2026-10-05 + +The dedicated 040 audit now includes both batch-fault groups and independently +enumerates every required combination, using fixed validation read ranges rather +than the C# fixture's read plan. It requires nine distinct fixture/command inputs, +CPU source and assembly identities, and 28 executed tests: 22 reporting batches +and six fixed examples. It verifies 1,593,024 passing scenarios, zero mismatches +or unsupported execution, and the retained 480 named untested requirements. +The complete gate therefore still fails; milestone 6 is not complete. + +`-Scope BatchFault` adds seven maintained mutations: four distinct delivery +paths, self-branch instruction count, cached callbacks and retry after partial +effects. Every mutation executes both complete batches and must produce semantic +mismatches in its intended path. All seven are detected, with no unsupported or +untested mutation cases; production sources are restored and rebuilt. No new +production behavior, public API, package release or regression retirement is +included. The [qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040-batch-fault-gate-and-mutation-qualification--2026-10-05) +preserves detailed counts, identities, controls and remaining work. + +Eleven copied-input controls reject missing/short/foreign/distributed new reports +and missing fixture or changed CPU/binary identities. Ordinary report validation +retains 16,561,686 cases in 612 batches; full CPU/external/consumer evidence is +retained from the preceding production checkpoint, rather than rerun for this +test/gate-only change. The complete required-gap inventory remains explicit and +`roadmapComplete=false`. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index 15fc0b45..e3861eec 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -27,6 +27,8 @@ $expected = [ordered]@{ 'access-double-fault-dispatch-accurate' = @{cases=1088; combinations=544} 'access-double-fault-dispatch-v1' = @{cases=1088; combinations=544} 'access-double-fault-dispatch-v2' = @{cases=1088; combinations=544} + 'rte-validation-batch' = @{cases=129024; combinations=4032} + 'access-fault-batch-dispatch' = @{cases=10368; combinations=5184} 'rte-access-remaining-protocols' = @{cases=480; combinations=15} } $testExit = 0 @@ -40,7 +42,7 @@ if (-not $ValidateReportsOnly) { $saved[$name] = [Environment]::GetEnvironmentVariable($name) [Environment]::SetEnvironmentVariable($name, $settings[$name]) } - & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests|FullyQualifiedName~SyntheticM68040RteValidationFaultTests|FullyQualifiedName~SyntheticM68040AccessDoubleFaultTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests|FullyQualifiedName~SyntheticM68040RteValidationFaultTests|FullyQualifiedName~SyntheticM68040AccessDoubleFaultTests|FullyQualifiedName~SyntheticM68040BatchFaultTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output $testExit = $LASTEXITCODE } finally { foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } @@ -52,7 +54,7 @@ if (-not $ValidateReportsOnly) { cpuSourceFiles=@(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } - inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs') | ForEach-Object { + inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } assemblies=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | ForEach-Object { @@ -61,7 +63,8 @@ if (-not $ValidateReportsOnly) { limitations=@('Synthetic frames and supervisor-stack physical map faults, not hardware captures or enabled-MMU access faults', 'CP context transfer and detailed fault protocol inventory remain untested and fail the gate', 'Multiple continuation bits are architecturally undefined and excluded', - 'User-tail validation, internal-restoration double faults, handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified; selected active accurate-batch paths have separate ordinary-CI coverage pending dedicated discovery integration', + 'Selected active accurate-batch paths verify counts/callbacks and scalar/batch bus/cycle policy; generic short operand/fetch frames do not qualify architectural format-7 restart', + 'User-tail validation, internal-restoration double faults, handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', 'Direct odd-RTE saved-SR ordering uses documentary WinUAE 5d22d336, not an executed hardware oracle') } $identity | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'identities.json') @@ -71,11 +74,11 @@ if (-not $ValidateReportsOnly) { $identity = Get-Content -LiteralPath (Join-Path $output 'identities.json') -Raw | ConvertFrom-Json if ($identity.schema -ne 1 -or $identity.reference -cne 'MC68040UM' -or $identity.softwareReferenceExecuted -ne $false -or $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.cpuCommittedTree -notmatch '^[a-f0-9]{40}$' -or - @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 8 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 8 -or + @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 9 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 9 -or @($identity.assemblies).Count -ne 2) { throw '040 access-frame input identity is missing or incomplete' } foreach ($input in $identity.inputs) { $path = Join-Path $repo $input.file - if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs') -or + if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs') -or $input.sha256 -cne (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()) { throw '040 access-frame fixture/command identity differs' } } $cpuFiles = @(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') @@ -89,8 +92,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 26 -or [int]$counters.total -ne 26 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (20 batches, 6 fixed examples)' +if ([int]$counters.executed -ne 28 -or [int]$counters.total -ne 28 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (22 batches, 6 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -113,6 +116,31 @@ foreach ($group in $expected.Keys) { $expectedCombinations["68040/RTE/format7/$form/bank=$bank"] = 32 } } + } elseif ($group -eq 'rte-validation-batch') { + foreach ($cached in @($false,$true)) { foreach ($prefix in @(0,1,3)) { foreach ($bank in @('ISP','MSP')) { + foreach ($trace in @(0,0x8000,0x4000)) { foreach ($alignment in @(0,1)) { foreach ($vbr in @(0,0x10000)) { + foreach ($stop in @('cap','before')) { + # Fixed validation read ranges, independent of the C# fixture. + foreach ($read in @('0:2','2:4','6:2','12:2','8:4')) { + for ($byte=0; $byte -lt [int]$read.Split(':')[1]; $byte++) { + $key = '68040/RTE/validation-batch/cached={0}/prefix={1}/bank={2}/T={3:X4}/align={4}/VBR={5:X8}/stop={6}/read={7}/byte={8}' -f $cached,$prefix,$bank,$trace,$alignment,$vbr,$stop,$read,$byte + $expectedCombinations[$key] = 32 + } + } + } + } } } + } } } + } elseif ($group -eq 'access-fault-batch-dispatch') { + foreach ($form in @('load','store','mixed-load','mixed-store','partial-store','self-fetch')) { + foreach ($prefix in @(0,1,3)) { foreach ($bank in @('ISP','MSP')) { foreach ($alignment in @(0,1)) { + foreach ($vbr in @(0,0x10000)) { foreach ($stop in @('cap','before','cycle')) { + foreach ($outcome in @('entry','fatal-stack','fatal-vector')) { foreach ($byte in 0..3) { + $key = '68040/access-fault/batch/{0}/prefix={1}/bank={2}/align={3}/VBR={4:X8}/stop={5}/outcome={6}/byte={7}' -f $form,$prefix,$bank,$alignment,$vbr,$stop,$outcome,$byte + $expectedCombinations[$key] = 2 + } } + } } + } } } + } } elseif ($group -in @('rte-access-entry-double-fault','rte-access-handler-refault')) { foreach ($bank in @('ISP','MSP')) { foreach ($trace in @(0,0x8000,0x4000)) { foreach ($alignment in @(0,1)) { foreach ($vbr in @(0,0x10000)) { @@ -251,6 +279,7 @@ foreach ($group in $expected.Keys) { } } } + if ($expectedCombinations.Count -ne $expected[$group].combinations) { throw "$group independent combination selection differs" } foreach ($combination in $report.combinations.PSObject.Properties) { if (-not $expectedCombinations.ContainsKey($combination.Name)) { throw "$group foreign combination: $($combination.Name); first expected: $($expectedCombinations.Keys | Sort-Object | Select-Object -First 1)" } $combinationCases = 0 @@ -265,8 +294,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 26 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 28 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=1454112; xunitBatches=20; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=1593504; xunitBatches=22; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index 24c260da..d6d7e673 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','AccessDoubleFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -228,6 +228,84 @@ foreach ($mutation in $doubleFaultMutations) { $mutation.group='WarmDispatchHaltsBothRteFallbackAndOperandFaultEntry'; $mutation.model='68040' $mutation.milestone=6; $mutation.doubleFault=$true; $mutations += $mutation } +$slowFault = @' + ExecuteInstructionWithTrace(); + } + catch (M68040MmuFaultException ex) + { + if (!TryHandleM68040ExecutionFault(ex.Fault)) throw; + } +'@ +$hotFault = @' + exitBlock = true; + } + else ExecuteHotInstruction(hotInstruction.Kind, opcode); + } + catch (M68040MmuFaultException ex) + { + if (!TryHandleM68040ExecutionFault(ex.Fault)) throw; + exitBlock = true; + } + boundary.AfterInstruction(previousCycle, State.Cycles); + executedInstructions++; +'@ +$modelFault = @' + if (!TryExecuteFastModelSpecificInstruction(opcode)) + throw new InvalidOperationException("The MC68040 FPU hot instruction was not handled."); + } + else ExecuteHotInstruction(hotInstruction.Kind, opcode); + } + catch (M68040MmuFaultException ex) + { + if (!TryHandleM68040ExecutionFault(ex.Fault)) throw; + exitBlock = true; + } +'@ +$selfFault = @' + opcode = _timedBus.ReadInstructionFetchWordHot( + hotInstruction.Address, out cacheHit, + out requiresSynchronization, out completedMachineCycle); + } + catch (M68040MmuFaultException ex) + { + if (!TryHandleM68040ExecutionFault(ex.Fault)) throw; + boundary.AfterInstruction(previousCycle, State.Cycles); + executedInstructions++; + return true; + } +'@ +$retryFault = @' + protected override bool TryHandleM68040ExecutionFault(M68040MmuFault fault) + { + RaiseMmuFault(fault); + return true; + } +'@ +$batchFaultMutations = @( + @{name='040-batch-cold-delivery'; file=$advanced; before=$slowFault; after=$slowFault.Replace('if (!TryHandleM68040ExecutionFault(ex.Fault)) throw;', 'throw;'); proofGroup='rte-validation-batch'; proofCombination='cached=False'}, + @{name='040-batch-hot-delivery'; file=$advanced; before=$hotFault; after=$hotFault.Replace('if (!TryHandleM68040ExecutionFault(ex.Fault)) throw;', 'throw;'); proofGroup='access-fault-batch-dispatch'; proofCombination='/batch/load/'}, + @{name='040-batch-model-delivery'; file=$advanced; before=$modelFault; after=$modelFault.Replace('if (!TryHandleM68040ExecutionFault(ex.Fault)) throw;', 'throw;'); proofGroup='access-fault-batch-dispatch'; proofCombination='/batch/mixed-load/'}, + @{name='040-batch-self-delivery'; file=$advanced; before=$selfFault; after=$selfFault.Replace('if (!TryHandleM68040ExecutionFault(ex.Fault)) throw;', 'throw;'); proofGroup='access-fault-batch-dispatch'; proofCombination='/batch/self-fetch/'}, + @{name='040-batch-self-count'; file=$advanced; before=$selfFault; after=$selfFault.Replace('executedInstructions++;', 'executedInstructions += 2;'); proofGroup='access-fault-batch-dispatch'; proofCombination='/batch/self-fetch/'}, + @{name='040-batch-hot-callback'; file=$advanced; before=$hotFault; after=$hotFault.Replace('boundary.AfterInstruction(previousCycle, State.Cycles);', 'if (!exitBlock) boundary.AfterInstruction(previousCycle, State.Cycles);'); proofGroup='access-fault-batch-dispatch'; proofCombination='/batch/load/'}, + @{name='040-batch-retry-partial'; file='Copper68k/M68040Support.cs'; before=$retryFault; after=@' + protected override bool TryHandleM68040ExecutionFault(M68040MmuFault fault) + { + var failedPc = State.LastInstructionProgramCounter; + RaiseMmuFault(fault); + if (!State.Halted) + { + State.ProgramCounter = failedPc; + base.ExecuteInstruction(); + } + return true; + } +'@; proofGroup='access-fault-batch-dispatch'; proofCombination='/batch/partial-store/'} +) +foreach ($mutation in $batchFaultMutations) { + $mutation.group='SyntheticM68040BatchFaultTests'; $mutation.model='68040' + $mutation.milestone=6; $mutation.batchFault=$true; $mutations += $mutation +} if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } if ($Scope -eq 'Logical') { $mutations = @($mutations | Where-Object { $_.milestone -eq 4 }) } @@ -236,6 +314,7 @@ if ($Scope -eq 'Consolidation') { $mutations = @($mutations | Where-Object { $_. if ($Scope -eq 'Rte040') { $mutations = @($mutations | Where-Object { $_.odd }) } if ($Scope -eq 'RteValidationFault') { $mutations = @($mutations | Where-Object { $_.validationFault }) } if ($Scope -eq 'AccessDoubleFault') { $mutations = @($mutations | Where-Object { $_.doubleFault }) } +if ($Scope -eq 'BatchFault') { $mutations = @($mutations | Where-Object { $_.batchFault }) } if ($Scope -eq 'LowPowerStop') { $mutations = @($mutations | Where-Object { $_.lpstop }) } if ($Scope -eq 'CacheEncodings') { $mutations = @($mutations | Where-Object { $_.cache }) } $saved = @{} @@ -296,6 +375,18 @@ try { throw 'RTE validation-fault mutation did not execute its complete two-batch selection' } } + if ($mutation.batchFault) { + [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + if ($trx.TestRun.ResultSummary.Counters.executed -ne 2 -or $batches.Count -ne 2 -or + @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq 'rte-validation-batch' -and $_.logicalCases -eq 129024 }).Count -ne 1 -or + @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq 'access-fault-batch-dispatch' -and $_.logicalCases -eq 10368 }).Count -ne 1) { + throw 'Batch-fault mutation omitted its complete two-batch selection' + } + $target = @($batches | Where-Object group -CEQ $mutation.proofGroup)[0] + if (@($target.combinations.PSObject.Properties | Where-Object { + $_.Name.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.Value.mismatching -gt 0 + }).Count -eq 0) { throw "Batch-fault mutation did not detect its intended path: $($mutation.name)" } + } if ($mutation.doubleFault) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if ($trx.TestRun.ResultSummary.Counters.executed -ne 3 -or $batches.Count -ne 3) { throw 'Double-fault mutation omitted dispatch batches' } @@ -330,6 +421,7 @@ try { } $results += @{mutation=$mutation.name; file=$mutation.file; sourceSha256=(Get-FileHash -LiteralPath $path).Hash; detected=$true; replacementCase=$failures[0].id; diagnostic=$failures[0].reason; originalRegressionDetected=$legacyPresent; + proofGroup=$mutation.proofGroup; proofCombination=$mutation.proofCombination; reports=@($batches | ForEach-Object { @{group=$_.group; logicalCases=$_.logicalCases; counts=$_.counts} })} Write-Host "Detected $($mutation.name): $($failures[0].id)" } diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 8572016e..249c4bd4 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -268,7 +268,7 @@ try { m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, and supervisor-stack physical RTE validation fault entry/handler return; chained odd-PC saved-SR provenance, user-tail/internal-restoration faults, CP context transfer, other real fault entry and physical timing remain unqualified'; m68040RtePhysicalValidationScope=@{logicalCases=223872; batches=2; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); trace=@(0,0x8000,0x4000); alignment=@(0,1); vbr=@(0,0x10000); faults='Each byte of SR/PC/format/SSW/continuation-EA reads; 14 direct/one/two-throwaway supervisor paths'; phases=@('fault entry','handler return'); undefined='EA and invalid writeback/push data; SSW X'; remaining='User-tail and internal-restoration/double faults, other instruction faults and software writeback handlers'}; m68040AccessDoubleFaultScope=@{logicalCases=105408; batches=5; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; dispatchCcr=@(0,31); engines=@('accurate','classic','V2'); faults='Every byte of format-7 stack/vector; later handler validation refaults; warmed RTE fallback and compiled B/W/L operand read/write side exits'; recovery='External reset, including restored host entry; interrupt, task/subroutine entry and halted batches remain inactive'; undefined='Partial stack write order, contents and exact SP'; remaining='Internal restoration, handler-entry prefetch, repaired original RTE retry, user-tail validation and general architectural access-fault restart'}; - m68040BatchFaultScope=@{logicalCases=139392; batches=2; translationEnabled=$false; paths=@('cold','cached','model-specific cached','self-branch cached'); limits=@('instruction cap','boundary denial','cycle deadline'); architectural='Supervisor RTE validation frames and fatal second-fault halt'; policy='Generic short operand/fetch frames, scalar/batch bus order and machine/native cycle equality'; remaining='General format-7 restart, entry prefetch, user-tail/internal-restoration faults and dedicated discovery/mutation-gate integration'}; + m68040BatchFaultScope=@{logicalCases=139392; batches=2; translationEnabled=$false; paths=@('cold','cached','model-specific cached','self-branch cached'); limits=@('instruction cap','boundary denial','cycle deadline'); architectural='Supervisor RTE validation frames and fatal second-fault halt'; policy='Generic short operand/fetch frames, scalar/batch bus order and machine/native cycle equality'; qualification='Dedicated discovery enumerates independent combinations; BatchFault scope proves four delivery paths, count, callback and retry defects'; remaining='General format-7 restart, entry prefetch and user-tail/internal-restoration faults'}; m68060LowPowerStopScope='Every unrecognized second opcode word in both privilege modes; fixed encoding, status, CCR and incoming trace cases across all profiles. Opcode-PC exception frames follow MC68060UM 8.2.4/5; physical broadcast, pins and ordinary STOP S-clear software disagreement remain unqualified'; cacheInstructionEncodingScope='Every F4xx opcode, all CCRs and both privilege states across eight profiles; scope zero is illegal on 040/060, including neither-cache forms, before privilege effects. Empty fixture caches qualify architectural state and exception outcomes, not physical invalidation, writeback, bus faults or timing'; m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; marked private word-MOVE/MOVEA images resume their faulted cycle; other opaque restart/input state, long transfers, RMW and physical timing remain unqualified'; From 36a8edf266dc682036d638fc9a5bb3cd0985439a Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 21:47:03 +0300 Subject: [PATCH 045/211] Qualify executed 68040 RTE frame repair and retry --- Copper68k.Tests/Synthetic/README.md | 36 +++- .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- .../SyntheticM68040RteRepairTests.cs | 196 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 87 ++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 36 ++++ scripts/test-copper68k-040-access-frames.ps1 | 53 ++++- .../test-copper68k-synthetic-mutations.ps1 | 30 ++- scripts/test-copper68k-synthetic.ps1 | 7 +- 8 files changed, 427 insertions(+), 20 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 6ec1137b..aae0dec5 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -408,8 +408,8 @@ Run `./scripts/test-copper68k-synthetic-mutations.ps1 -Scope AccessDoubleFault` for maintained fatal-latch, reset, compiled-entry, master-stack and six classic memory-routing mutations. The complete 040 audit includes these combinations and fixture identities while retaining its failing untested protocol inventory. -Handler-entry prefetch, internal-restoration faults and repaired-original-RTE -retry remain required work. +Handler-entry prefetch and internal-restoration faults remain required work; +executed repair/retry coverage is described below. `SyntheticM68040BatchFaultTests` adds two ordinary CI batches: `rte-validation-batch` (129,024 scenarios) and `access-fault-batch-dispatch` @@ -422,8 +422,8 @@ architectural format-7 expectations. Generic operand/fetch frames and scalar/ batch cycle and bus-order equality verify existing execution policy, not general architectural restart or physical timing. The dedicated 040 discovery command requires these reports, independently enumerates their 4,032 and 5,184 -combinations and binds their fixture and CPU identities. It executes 28 tests -(22 reporting batches and six fixed examples) while retaining the failing +combinations and binds their fixture and CPU identities. With repair coverage it +executes 30 tests (24 reporting batches and six fixed examples), retaining the failing 480-case untested protocol inventory. Run `./scripts/test-copper68k-synthetic-mutations.ps1 -Scope BatchFault` to @@ -433,5 +433,29 @@ or retry after partial operand effects. Every mutation must execute both full batches and detect mismatches in its intended path; a build failure, empty selection or failure only in another path is insufficient. Sources are restored and rebuilt even when qualification fails. General format-7 restart, user-tail -and internal-restoration faults, handler-entry prefetch and repaired-RTE retry -remain required work. +and internal-restoration faults and handler-entry prefetch remain required work. + +`SyntheticM68040RteRepairTests` executes the access-error handler's stores, +handler RTE, original RTE retry and following instruction in two ordinary CI +batches: `rte-repair-boundaries` (143,424 phases, 540 combinations) and +`rte-repair-chained` (768,960 phases, 45,792 combinations). The first covers all +32 CCRs and incoming/restored trace states; the second rejects each byte of +every selected validation read after one/two consumed supervisor throwaways, +with odd/even stacks, VBR and boundary CCRs. Both restore user/ISP/MSP results +and cover formats 0/2/3, invalid 4/15 repaired to 0, and normal/CM/CT/CU/CP49. +The handler repairs SR, PC and format using fixed MOVE encodings, then explicitly +clears saved incoming trace. Expectations check each store, exact PC, untouched +registers/memory, stack banks, no repeated throwaway read, saved MOVEM EA, +pending conversion/return and trace on the following instruction. This does not +qualify untouched incoming-trace retry, user-tail validation, enabled MMU, +internal-restoration faults, arbitrary CP context transfer or physical timing. + +The dedicated 040 audit requires these complete reports and independently +enumerates their combinations. Its 30 tests comprise 24 reporting batches and +six fixed examples; the 480-case untested inventory still fails the full gate. +Run `./scripts/test-copper68k-synthetic-mutations.ps1 -Scope RteRepair` to corrupt +the short-frame return PC, MOVEM continuation EA or pending exception's stacked +SR. Each mutation must execute both full batches and mismatch in its intended +retry/following phase, rather than failing at an unrelated prerequisite. The +[qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-executed-rte-repair-and-retry--2026-10-05) +records current evidence and remaining scope. No existing regression is retired. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index a026deea..99b098a0 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -33,7 +33,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() "real-access-fault-entry", "writeback-handler", "CP-context-transferred-vector" }) report.Record($"68040/RTE/format7/{form}/bank={bank}/op=4E73/ccr={ccr:X2}", "untested", form == "frame-validation-fault" - ? "Supervisor validation, access-fault stacking/vector double faults and selected accurate-batch paths are covered separately; user-tail validation, internal restoration and handler-entry prefetch remain required" + ? "Supervisor validation, executed repair/retry, access-fault stacking/vector double faults and selected accurate-batch paths are covered separately; user-tail validation, internal restoration and handler-entry prefetch remain required" : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs new file mode 100644 index 00000000..863f0734 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs @@ -0,0 +1,196 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 8.4.2 and 8.4.6.7: software may repair the preserved frame. +// The handler really executes its stores and RTE; initialization is not repair. +// Supervisor validation tails only. Internal restoration faults and user tails +// require separate architectural evidence and remain in the discovery inventory. +public sealed class SyntheticM68040RteRepairTests(ITestOutputHelper output) +{ + private const uint Handler = 0x9020, PendingHandler = 0x9090, Target = 0x6000, Operand = 0x4200; + private static readonly string[] SupervisorBanks = ["ISP", "MSP"]; + private static readonly string[] Forms = ["format0", "format2", "format3", "invalid4", "invalid15", + "normal", "CM", "CT", "CU", "CP"]; + + [Fact, Trait("Suite", "Synthetic")] + public void ExecutedRepairRestoresAllCcrAndTraceStates() => Audit(false); + + [Fact, Trait("Suite", "Synthetic")] + public void ExecutedRepairRetriesEveryValidationReadAfterCommittedThrowaways() => Audit(true); + + private void Audit(bool chained) + { + var bus = new SyntheticM68040RteValidationFaultTests.ValidationFaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + var report = new CoverageBatch("68040", chained ? "rte-repair-chained" : "rte-repair-boundaries"); + foreach (var start in SupervisorBanks) + foreach (var tail in chained ? SupervisorBanks : [start]) + foreach (var middle in chained ? new[] { "none", "ISP", "MSP" } : ["none"]) + foreach (var result in new[] { "user", "ISP", "MSP" }) + foreach (var incoming in chained ? new ushort[] { 0x8000 } : [0, 0x8000, 0x4000]) + foreach (var restoredTrace in new ushort[] { 0, 0x8000, 0x4000 }) + foreach (var alignment in chained ? new uint[] { 0, 1 } : [0]) + foreach (var vbr in chained ? new uint[] { 0, 0x10000 } : [0x10000]) + foreach (var ccr in chained ? new[] { 0, 31 } : Enumerable.Range(0, 32)) + foreach (var form in Forms) + foreach (var (offset, width) in chained ? SyntheticM68040RteValidationFaultTests.Reads(form) : [(0u, 2)]) + for (var faultByte = 0; faultByte < (chained ? width : 1); faultByte++) + { + var path = !chained ? new[] { start } : middle == "none" ? [start, tail] : new[] { start, middle, tail }; + Run(m, bus, report, path, result, incoming, restoredTrace, alignment, vbr, ccr, form, offset, width, faultByte); + } + report.Complete(output); + } + + private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTests.ValidationFaultBus bus, + CoverageBatch report, string[] path, string result, ushort incoming, ushort restoredTrace, + uint alignment, uint vbr, int ccr, string form, uint offset, int width, int faultByte) + { + bus.Disarm(); m.Reset(ccr); + var pointers = new Dictionary { ["user"] = 0x7800 + alignment, ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; + foreach (var at in pointers.Values) + for (var n = -76; n < 112; n++) m.InitializePhysical(unchecked(at + (uint)n), (uint)(n ^ 0x5a), 1); + var discarded = new List(); + for (var n = 0; n < path.Length - 1; n++) + { + var at = pointers[path[n]]; discarded.Add(at); + m.InitializePhysical(at, Status(path[n + 1], incoming, ccr ^ 31), 2); + m.InitializePhysical(at + 2, 0xdead0001u + (uint)n * 2, 4); + m.InitializePhysical(at + 6, 0x1024, 2); + pointers[path[n]] += 8; + } + var tail = path[^1]; var frame = pointers[tail]; var accessFrame = frame - 60; + var format = form.StartsWith("format") ? int.Parse(form[6..]) : form.StartsWith("invalid") ? int.Parse(form[7..]) : 7; + var repairedFormat = form.StartsWith("invalid") ? 0 : format; + var continuation = form switch { "CM" => 0x1000, "CT" => 0x2000, "CU" => 0x4000, "CP" => 0x8000, _ => 0 }; + var vector = form switch { "CT" => 9, "CU" => 11, "CP" => 49, _ => 0 }; + var savedSr = Status(result, restoredTrace, ccr ^ 31); + var priorSr = Status(tail, incoming, path.Length > 1 ? ccr ^ 31 : ccr); + // Every repair changes SR and PC. Invalid formats are repaired to format 0. + m.InitializePhysical(frame, 0x801fu ^ (uint)ccr, 2); + m.InitializePhysical(frame + 2, Target + 2, 4); + m.InitializePhysical(frame + 6, (uint)(format << 12) | 8, 2); + m.InitializePhysical(frame + 8, Operand, 4); + m.InitializePhysical(frame + 12, (uint)continuation | 0x0105, 2); + m.InitializePhysical(Target, form == "CM" ? 0x4cd0u : 0x60feu, 2); + m.InitializePhysical(Target + 2, form == "CM" ? 3u : 0x7e7eu, 2); + m.InitializePhysical(Target + 4, 0x60fe, 2); + m.InitializePhysical(Operand, 0x89abcdef, 4); m.InitializePhysical(Operand + 4, 0x12345678, 4); + m.Core.State.A[0] = 0x4300; // CM must use the saved EA, not this live base. + // Fixed reference encodings: MOVE.W #imm,(xxx).L; MOVE.L #imm,(xxx).L; + // MOVE.W #imm,(A7); RTE. Clearing the saved incoming trace is an explicit + // handler action, so retry trace comes solely from the repaired frame. + ushort[] handler = [0x33fc, savedSr, (ushort)(frame >> 16), (ushort)frame, + 0x23fc, 0, (ushort)Target, (ushort)((frame + 2) >> 16), (ushort)(frame + 2), + 0x33fc, (ushort)((repairedFormat << 12) | 8), (ushort)((frame + 6) >> 16), (ushort)(frame + 6), + 0x3ebc, (ushort)(priorSr & ~0xc000), 0x4e73, 0x7e7e]; + for (var n = 0; n < handler.Length; n++) m.InitializePhysical(Handler + (uint)n * 2, handler[n], 2); + m.InitializePhysical(PendingHandler, 0x4e73, 2); m.InitializePhysical(PendingHandler + 2, 0x7e7e, 2); + m.InitializePhysical(vbr + 8, Handler, 4); + foreach (var v in new[] { 9, 11, 49 }) m.InitializePhysical(vbr + (uint)v * 4, PendingHandler, 4); + _ = SyntheticExecution.Prepare(m, [0x4e73]); + m.Core.State.SetUserStackPointer(0x7800 + alignment); + m.Core.State.SetInterruptStackPointer(0x4700 + alignment); + m.Core.State.SetMasterStackPointer(0x7400 + alignment); + m.Core.State.StatusRegister = Status(path[0], incoming, ccr); m.Core.State.VectorBaseRegister = vbr; + if (form == "CU") m.Core.State.M68040PendingFpuExceptions.Begin(2, 11, Target); + if (form == "CP") m.Core.State.M68040PendingFpuExceptions.Begin(3, 49, Target); + var e = ArchitecturalExpectation.Capture(m); var sequence = m.Core.State.ExceptionSequence; + e.ControlChecks["fault rejected once"] = (_ => (uint)bus.Rejected.Count, 1); + e.ControlChecks["rejected original address"] = (_ => bus.Rejected.Count == 1 ? bus.Rejected[0].Address : uint.MaxValue, frame + offset); + e.ControlChecks["rejected original width"] = (_ => bus.Rejected.Count == 1 ? (uint)bus.Rejected[0].Width : 0, (uint)width); + e.ControlChecks["discarded PCs never fetched"] = (_ => bus.Accesses.Any(a => a.Address is 0xdead0001 or 0xdead0003) ? 1u : 0u, 0); + e.ControlChecks["sentinel never executed"] = (s => s.D[7], e.D[7]); + if (form is "CU" or "CP") e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) == null ? 0u : 1u, 1); + pointers[tail] = accessFrame; SetStacks(e, pointers, (ushort)(priorSr & ~0xc000)); e.Pc = Handler; e.ExceptionVector = 2; + ExpectProvenance(e, sequence + 1, SyntheticMachine.Code, priorSr); + SyntheticM68040RteValidationFaultTests.ExpectAccessFrame(m, e, accessFrame, priorSr, SyntheticMachine.Code, frame + offset, width); + bus.Arm(frame + offset + (uint)faultByte); + var phases = new List { "fault-entry", "repair-SR", "repair-PC", "repair-format", "clear-incoming-trace", "handler-return", "retry-RTE" }; + if (vector != 0) phases.Add("pending-return"); phases.Add("following"); + var id = $"68040/RTE/repair/{form}/path={string.Join('-', path)}/result={result}/incoming={incoming:X4}/T={restoredTrace:X4}/align={alignment}/VBR={vbr:X8}/read={offset}:{width}/fault-byte={faultByte}/op=4E73/ccr={ccr:X2}"; + var phase = 0; + bool Step() + { + var passed = SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/" + phases[phase++]); + if (!passed) while (phase < phases.Count) report.Record(id + "/" + phases[phase++], "untested", "Repair prerequisite failed"); + return passed; + } + if (!Step()) return; + var repairStart = bus.Accesses.Count; + // Each store has independent flag and memory expectations, exact next PC, + // unchanged stack/register guards and no new exception. + foreach (var (at, value, bytes, next) in new[] { + (frame, (uint)savedSr, 2, Handler + 8), (frame + 2, Target, 4, Handler + 18), + (frame + 6, (uint)((repairedFormat << 12) | 8), 2, Handler + 26), + (accessFrame, (uint)(priorSr & ~0xc000), 2, Handler + 30) }) + { + e.Write(at, value, bytes, m.Model); e.Sr = SyntheticExecution.MoveFlags(e.Sr, value, bytes); e.Pc = next; + if (!Step()) return; + } + var repairWrites = bus.Accesses.Skip(repairStart).Where(a => a.Write).ToArray(); + e.ControlChecks["four exact repair writes"] = (_ => repairWrites.Length == 4 && + repairWrites[0] == new BusAccess(frame, 2, true, savedSr, M68kBusAccessKind.CpuDataWrite) && + repairWrites[1] == new BusAccess(frame + 2, 4, true, Target, M68kBusAccessKind.CpuDataWrite) && + repairWrites[2] == new BusAccess(frame + 6, 2, true, (uint)((repairedFormat << 12) | 8), M68kBusAccessKind.CpuDataWrite) && + repairWrites[3] == new BusAccess(accessFrame, 2, true, (uint)(priorSr & ~0xc000), M68kBusAccessKind.CpuDataWrite) ? 1u : 0u, 1); + var resumeSr = (ushort)(priorSr & ~0xc000); + pointers[tail] = frame; SetStacks(e, pointers, resumeSr); e.Pc = SyntheticMachine.Code; e.ExceptionVector = null; + if (!Step()) return; + var retryStart = bus.Accesses.Count; + // Only retry itself is constrained here. Later pending frames may occupy + // memory formerly used by consumed throwaways and may be read legitimately. + e.ControlChecks["consumed throwaways not read by retry"] = (_ => bus.Accesses.Skip(retryStart).Any(a => + !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && discarded.Any(at => unchecked(a.Address - at) < 8)) ? 1u : 0u, 0); + pointers[tail] += repairedFormat == 7 ? 60u : repairedFormat == 0 ? 8u : 12u; + SetStacks(e, pointers, savedSr); e.Pc = Target; + if (vector != 0) + { + ExpectException(m, e, pointers, savedSr, vector, Target, Operand, sequence + 2); + if (form is "CU" or "CP") e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) == null ? 0u : 1u, 0); + } + if (!Step()) return; + e.ControlChecks.Remove("consumed throwaways not read by retry"); + if (vector != 0) + { + pointers[ExceptionBank(savedSr)] += 12; SetStacks(e, pointers, savedSr); e.Pc = Target; e.ExceptionVector = null; + if (!Step()) return; + } + e.Pc = form == "CM" ? Target + 4 : Target; + if (form == "CM") { e.D[0] = 0x89abcdef; e.D[1] = 0x12345678; } + if (restoredTrace == 0x8000 || restoredTrace == 0x4000 && form != "CM") + ExpectException(m, e, pointers, savedSr, 9, e.Pc, Target, sequence + (vector != 0 ? 3u : 2u)); + Step(); + } + + private static ushort Status(string bank, ushort trace, int ccr) => (ushort)((bank == "MSP" ? 0x3000 : bank == "ISP" ? 0x2000 : 0) | trace | ccr); + private static string ExceptionBank(ushort sr) => (sr & 0x3000) == 0x3000 ? "MSP" : "ISP"; + private static void SetStacks(ArchitecturalExpectation e, Dictionary pointers, ushort sr) + { + e.Sr = sr; e.A[7] = pointers[(sr & 0x2000) == 0 ? "user" : ExceptionBank(sr)]; + e.InactiveStackPointer = pointers[(sr & 0x2000) == 0 ? "ISP" : "user"]; e.MasterStackPointer = pointers["MSP"]; + foreach (var bank in new[] { "user", "ISP", "MSP" }) + { + var expected = pointers[bank]; + e.ControlChecks[bank] = (s => bank == "user" ? s.UserStackPointer : bank == "MSP" ? s.MasterStackPointer : s.InterruptStackPointer, expected); + } + } + private static void ExpectProvenance(ArchitecturalExpectation e, uint sequence, uint pc, ushort sr) + { + e.ControlChecks["exception entries"] = (s => s.ExceptionSequence, sequence); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, pc); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, sr); + } + private static void ExpectException(SyntheticMachine m, ArchitecturalExpectation e, Dictionary pointers, + ushort sr, int vector, uint pc, uint address, uint sequence) + { + var bank = ExceptionBank(sr); pointers[bank] -= 12; + SetStacks(e, pointers, (ushort)((sr | 0x2000) & ~0xc000)); e.Pc = PendingHandler; e.ExceptionVector = vector; + ExpectProvenance(e, sequence, pc, sr); + var at = pointers[bank]; e.Write(at, sr, 2, m.Model); e.Write(at + 2, pc, 4, m.Model); + e.Write(at + 6, (uint)((vector >= 49 ? 3 : 2) << 12 | vector * 4), 2, m.Model); e.Write(at + 8, address, 4, m.Model); + for (uint n = 0; n < 12; n++) e.MemoryMasks[at + n] = 255; + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 663ba3c9..e3dbfa6b 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -4060,3 +4060,90 @@ The 480-case inventory, user-tail/internal-restoration faults, handler-entry prefetch, repaired-original-RTE retry, general format-7 restart, writeback/context transfer and the earlier model/reference/consolidation requirements remain open. Milestone 6 remains **in progress**, with `roadmapComplete=false`. + +## 040 executed RTE repair and retry — 2026-10-05 + +The authority is [MC68040UM](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +8.4.2, 8.4.6.7 and 8.2.6: throwaways commit their stack/SR changes, +validation faults preserve the attempted frame for software repair, and trace +depends on instruction entry versus restored trace state. These are independent +synthetic architectural expectations, not hardware captures or software-reference +agreement. Enabled MMU, physical timing and opaque internal fault phases are +outside this execution fixture. + +`SyntheticM68040RteRepairTests` adds two ordinary CI batches: + +| Report | Logical instruction phases | Architectural combinations | +| --- | ---: | ---: | +| `rte-repair-boundaries` | 143,424 | 540 | +| `rte-repair-chained` | 768,960 | 45,792 | + +The first covers all 32 CCRs, both entry supervisor banks, all incoming/restored +trace states and all three restored stack banks using a canonical SR validation +fault. The second faults each byte of the selected SR/PC/format/SSW/continuation +EA reads after every one/two-throwaway supervisor path, with CCR 0/31, incoming +T1, all restored trace states, both VBRs, odd/even stacks and all result banks. +Both cover formats 0/2/3, invalid 4/15 repaired to 0, normal format 7 and CM, +CT, CU and original-vector CP49. CP50–55 are separately qualified by existing +positive continuation fixtures, not by these repair cases. + +Repair is executed code: fixed MOVE.W/MOVE.L immediate instruction encodings +write the original SR/PC/format, then MOVE.W explicitly clears incoming trace in +the fault frame. Each store checks its flags, exact PC, unchanged registers and +surrounding memory; four exact bus writes are verified before handler return. +Handler RTE returns to the original RTE with the current supervisor tail still +selected. Retry may not reread consumed throwaways. It restores the repaired +frame, returns/converts pending delivery, resumes MOVEM using saved EA rather +than the handler's live base, and applies repaired trace to the following +instruction. Untouched incoming-trace retry is explicitly not claimed. + +The initial fixture run failed because `3EFC` encoded postincrement. Correcting +it to indirect `3EBC` fixed the fixture; this was not a production CPU defect. +The final complete 040 audit in `artifacts/m6-rte-repair-audit/` executes 30 tests: +29 pass and the retained inventory test fails, with 2,505,408 passing phases, +zero mismatches/unsupported execution and 480 untested requirements. Independent +PowerShell enumeration requires 24 reporting batches, six fixed examples and +ten distinct fixture/command identities. Nine copied-input controls in +`artifacts/m6-rte-repair-controls/controls.json` reject missing, shortened, +foreign and redistributed repair combinations for both new reports and an +omitted repair fixture identity. Redistribution preserves aggregate counts and +combination cardinality, proving per-combination counts are enforced. + +`-Scope RteRepair` in the maintained mutation command executes both complete +batches for every mutation. Required diagnostic phases are enforced; a compile +failure, empty selection or mismatch only at an unrelated prerequisite cannot +satisfy qualification. + +| Mutation | Boundary mismatches | Chained mismatches | Required phase | +| --- | ---: | ---: | --- | +| Short-frame return PC +2 | 13,824 | 70,848 | Retry RTE | +| Saved MOVEM EA +4 | 1,728 | 12,096 | Following MOVEM | +| Pending exception stacks prior SR | 5,184 | 32,256 | Retry RTE conversion | + +All three are detected in `artifacts/m6-rte-repair-mutations/mutation-proof.json`; +prerequisite-dependent later phases are labeled untested during mutations, with +no unsupported execution. Production sources are restored byte-for-byte and +rebuilt with zero warnings/errors. No existing regression is retired. + +Qualification begins at `9ab8d56030898e520c42142a59ce1191e5965d83`, CPU tree +`6bb8bd68a93ba23bf79d1d077d0b34c48e5e6679`. Discovery records CPU SHA-256 +`e93cb1cdd30a88f9c6fceb829d80e5e2442b240a6751b977c390a2d1c9872af3` +and test/reference adapter SHA-256 +`61f468ab7e75aebdaeea466f5315b0ceba2649e66b35bac6c3c79ad8e1d1686f`. +These are this rebuilt test snapshot's identities; the earlier full CPU, +external-reference and private `.60` consumer evidence retains its own binary +identities. No production CPU change, new consumer package or public release is +introduced in this checkpoint. + +The fresh ordinary synthetic command in `artifacts/m6-rte-repair-ordinary/` +passes 616 tests with no failures or skips and validates 17,474,070 logical +cases in 614 reporting batches across all eight profiles. Its CPU and adapter +binaries match the dedicated audit identities above. External reference audits +were not requested/executed in this run; their preceding pinned evidence remains +separately identified. The full goal is not implied by the passing semantic gate. + +User-tail validation, internal restoration/double faults, handler-entry prefetch, +untouched incoming-trace retry, chained odd-PC provenance, general real format-7 +restart, writeback/context transfer and earlier model/reference/consolidation +requirements remain open. The 480-case inventory and `roadmapComplete=false` +are preserved. Milestone 6 and the full goal remain **in progress**. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 5126fc02..0289c092 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1520,3 +1520,39 @@ retains 16,561,686 cases in 612 batches; full CPU/external/consumer evidence is retained from the preceding production checkpoint, rather than rerun for this test/gate-only change. The complete required-gap inventory remains explicit and `roadmapComplete=false`. + +### Milestone 6 executed RTE repair and retry — 2026-10-05 + +Two ordinary-CI batches execute a real access-error repair handler, its RTE, +the original RTE again, optional pending exception return and the following +instruction. They add 912,384 logical phases across 46,332 combinations. Direct +boundaries cover all CCRs and incoming/restored trace states; chained fixtures +fault each byte of selected validation reads after committed supervisor +throwaways, with both VBRs and odd/even stacks. Repaired results select any of +the three stack banks. Formats 0/2/3, invalid 4/15 repaired to 0, and normal, +CM, CT, CU and original-vector CP49 returns are covered. + +The handler's three original-frame stores and saved incoming-trace clear have +fixed instruction encodings and independent state/memory expectations. Retry +must not read consumed throwaways; MOVEM uses the restored EA, pending delivery +uses the repaired SR/PC, and following trace uses the repaired trace bits. +`-Scope RteRepair` detects wrong return PC, saved MOVEM EA and pending stacked +SR in the intended retry/following phase. Every mutation executes both complete +batches; sources are restored and rebuilt. + +The dedicated 040 gate includes these independently enumerated reports and +fixture identities. The 480-case inventory stays explicit: user-tail validation, +internal restoration, handler-entry prefetch, untouched incoming-trace retry, +general real format-7 restart, writeback/context transfer and all earlier +model/reference/consolidation requirements remain open. No regression is +retired, CPU production behavior changed or package published. Milestone 6 +and the goal remain **in progress**, with `roadmapComplete=false`. +See the [qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040-executed-rte-repair-and-retry--2026-10-05). + +Fresh ordinary validation passes 616 tests without skips/failures and checks +17,474,070 logical cases in 614 reporting batches across all eight profiles. +Complete 040 discovery has 2,505,408 passing phases, zero mismatches or +unsupported execution and the retained 480 untested requirements. All three +repair mutations and nine report/identity controls detect their intended defects. +Previous full CPU/external/private-consumer evidence is retained separately; +no new external or consumer execution is claimed for this test/gate checkpoint. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index e3861eec..3264f9b6 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -29,6 +29,8 @@ $expected = [ordered]@{ 'access-double-fault-dispatch-v2' = @{cases=1088; combinations=544} 'rte-validation-batch' = @{cases=129024; combinations=4032} 'access-fault-batch-dispatch' = @{cases=10368; combinations=5184} + 'rte-repair-boundaries' = @{cases=143424; combinations=540} + 'rte-repair-chained' = @{cases=768960; combinations=45792} 'rte-access-remaining-protocols' = @{cases=480; combinations=15} } $testExit = 0 @@ -42,19 +44,19 @@ if (-not $ValidateReportsOnly) { $saved[$name] = [Environment]::GetEnvironmentVariable($name) [Environment]::SetEnvironmentVariable($name, $settings[$name]) } - & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests|FullyQualifiedName~SyntheticM68040RteValidationFaultTests|FullyQualifiedName~SyntheticM68040AccessDoubleFaultTests|FullyQualifiedName~SyntheticM68040BatchFaultTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests|FullyQualifiedName~SyntheticM68040RteValidationFaultTests|FullyQualifiedName~SyntheticM68040AccessDoubleFaultTests|FullyQualifiedName~SyntheticM68040BatchFaultTests|FullyQualifiedName~SyntheticM68040RteRepairTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output $testExit = $LASTEXITCODE } finally { foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } } $identity = [ordered]@{ schema=1; reference='MC68040UM'; url='https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf' - sections=@('7.6.3','8.2.1','8.2.2','8.4','8.4.1','8.4.2','8.4.3','8.4.4','8.4.6.2','8.4.6.7'); softwareReferenceExecuted=$false + sections=@('7.6.3','8.2.1','8.2.2','8.2.6','8.4','8.4.1','8.4.2','8.4.3','8.4.4','8.4.6.2','8.4.6.7'); softwareReferenceExecuted=$false sourceCommit=(& git -C $repo rev-parse HEAD); cpuCommittedTree=(& git -C $repo rev-parse HEAD:Copper68k) cpuSourceFiles=@(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } - inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs') | ForEach-Object { + inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } assemblies=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | ForEach-Object { @@ -64,6 +66,7 @@ if (-not $ValidateReportsOnly) { 'CP context transfer and detailed fault protocol inventory remain untested and fail the gate', 'Multiple continuation bits are architecturally undefined and excluded', 'Selected active accurate-batch paths verify counts/callbacks and scalar/batch bus/cycle policy; generic short operand/fetch frames do not qualify architectural format-7 restart', + 'Executed supervisor-tail frame repair explicitly clears saved incoming trace; untouched incoming-trace retry and user-tail repair are not qualified', 'User-tail validation, internal-restoration double faults, handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', 'Direct odd-RTE saved-SR ordering uses documentary WinUAE 5d22d336, not an executed hardware oracle') } @@ -74,11 +77,11 @@ if (-not $ValidateReportsOnly) { $identity = Get-Content -LiteralPath (Join-Path $output 'identities.json') -Raw | ConvertFrom-Json if ($identity.schema -ne 1 -or $identity.reference -cne 'MC68040UM' -or $identity.softwareReferenceExecuted -ne $false -or $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.cpuCommittedTree -notmatch '^[a-f0-9]{40}$' -or - @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 9 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 9 -or + @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 10 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 10 -or @($identity.assemblies).Count -ne 2) { throw '040 access-frame input identity is missing or incomplete' } foreach ($input in $identity.inputs) { $path = Join-Path $repo $input.file - if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs') -or + if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs') -or $input.sha256 -cne (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()) { throw '040 access-frame fixture/command identity differs' } } $cpuFiles = @(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') @@ -92,8 +95,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 28 -or [int]$counters.total -ne 28 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (22 batches, 6 fixed examples)' +if ([int]$counters.executed -ne 30 -or [int]$counters.total -ne 30 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (24 batches, 6 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -116,6 +119,38 @@ foreach ($group in $expected.Keys) { $expectedCombinations["68040/RTE/format7/$form/bank=$bank"] = 32 } } + } elseif ($group -in @('rte-repair-boundaries','rte-repair-chained')) { + $chained = $group -eq 'rte-repair-chained' + $paths = if ($chained) { @('ISP-ISP','ISP-ISP-ISP','ISP-MSP-ISP','ISP-MSP','ISP-ISP-MSP','ISP-MSP-MSP', + 'MSP-ISP','MSP-ISP-ISP','MSP-MSP-ISP','MSP-MSP','MSP-ISP-MSP','MSP-MSP-MSP') } else { @('ISP','MSP') } + foreach ($path in $paths) { foreach ($result in @('user','ISP','MSP')) { + foreach ($incoming in $(if ($chained) { @(0x8000) } else { @(0,0x8000,0x4000) })) { + foreach ($trace in @(0,0x8000,0x4000)) { + foreach ($alignment in $(if ($chained) { @(0,1) } else { @(0) })) { + foreach ($vbr in $(if ($chained) { @(0,0x10000) } else { @(0x10000) })) { + foreach ($form in @('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP')) { + # Literal read ranges are independent of the fixture iterator. + $reads = @('0:2') + if ($chained) { + $reads += @('2:4','6:2') + if ($form -in @('normal','CM','CT','CU','CP')) { $reads += '12:2' } + if ($form -in @('CM','CT','CU','CP')) { $reads += '8:4' } + } + foreach ($read in $reads) { + $bytes = if ($chained) { [int]$read.Split(':')[1] } else { 1 } + for ($byte=0; $byte -lt $bytes; $byte++) { + $key = '68040/RTE/repair/{0}/path={1}/result={2}/incoming={3:X4}/T={4:X4}/align={5}/VBR={6:X8}/read={7}/fault-byte={8}' -f $form,$path,$result,$incoming,$trace,$alignment,$vbr,$read,$byte + $phases = if ($form -in @('CT','CU','CP')) { 9 } else { 8 } + $ccrs = if ($chained) { 2 } else { 32 } + $expectedCombinations[$key] = $phases * $ccrs + } + } + } + } + } + } + } + } } } elseif ($group -eq 'rte-validation-batch') { foreach ($cached in @($false,$true)) { foreach ($prefix in @(0,1,3)) { foreach ($bank in @('ISP','MSP')) { foreach ($trace in @(0,0x8000,0x4000)) { foreach ($alignment in @(0,1)) { foreach ($vbr in @(0,0x10000)) { @@ -294,8 +329,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 28 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 30 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=1593504; xunitBatches=22; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=2505888; xunitBatches=24; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index d6d7e673..9bc2a30b 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -306,6 +306,19 @@ foreach ($mutation in $batchFaultMutations) { $mutation.group='SyntheticM68040BatchFaultTests'; $mutation.model='68040' $mutation.milestone=6; $mutation.batchFault=$true; $mutations += $mutation } +$repairMutations = @( + @{name='040-repair-short-pc'; file=$advanced; before=' State.ProgramCounter = restoredPc;'; + after=' State.ProgramCounter = restoredPc + 2;'; proofCombination='/repair/format0/'; proofPhase='/retry-RTE'}, + @{name='040-repair-movem-ea'; file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs'; + before=' if (continuation == 0x1000) _m68040MovemContinuation = (pc, address);'; + after=' if (continuation == 0x1000) _m68040MovemContinuation = (pc, address + 4);'; proofCombination='/repair/CM/'; proofPhase='/following'}, + @{name='040-repair-pending-sr'; file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs'; + before=' PushWord(sr);'; after=' PushWord(priorSr);'; proofCombination='/repair/CT/'; proofPhase='/retry-RTE'} +) +foreach ($mutation in $repairMutations) { + $mutation.group='SyntheticM68040RteRepairTests'; $mutation.model='68040' + $mutation.milestone=6; $mutation.repair=$true; $mutations += $mutation +} if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } if ($Scope -eq 'Logical') { $mutations = @($mutations | Where-Object { $_.milestone -eq 4 }) } @@ -313,6 +326,7 @@ if ($Scope -eq 'Control') { $mutations = @($mutations | Where-Object { $_.milest if ($Scope -eq 'Consolidation') { $mutations = @($mutations | Where-Object { $_.milestone -eq 6 }) } if ($Scope -eq 'Rte040') { $mutations = @($mutations | Where-Object { $_.odd }) } if ($Scope -eq 'RteValidationFault') { $mutations = @($mutations | Where-Object { $_.validationFault }) } +if ($Scope -eq 'RteRepair') { $mutations = @($mutations | Where-Object { $_.repair }) } if ($Scope -eq 'AccessDoubleFault') { $mutations = @($mutations | Where-Object { $_.doubleFault }) } if ($Scope -eq 'BatchFault') { $mutations = @($mutations | Where-Object { $_.batchFault }) } if ($Scope -eq 'LowPowerStop') { $mutations = @($mutations | Where-Object { $_.lpstop }) } @@ -375,6 +389,18 @@ try { throw 'RTE validation-fault mutation did not execute its complete two-batch selection' } } + if ($mutation.repair) { + [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + if ($trx.TestRun.ResultSummary.Counters.executed -ne 2 -or $batches.Count -ne 2 -or + @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq 'rte-repair-boundaries' -and $_.logicalCases -eq 143424 }).Count -ne 1 -or + @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq 'rte-repair-chained' -and $_.logicalCases -eq 768960 }).Count -ne 1) { + throw 'RTE repair mutation omitted its complete two-batch selection' + } + if (@($failures | Where-Object { + $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and + $_.id.EndsWith($mutation.proofPhase, [StringComparison]::Ordinal) + }).Count -eq 0) { throw "RTE repair mutation did not detect its intended phase: $($mutation.name)" } + } if ($mutation.batchFault) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if ($trx.TestRun.ResultSummary.Counters.executed -ne 2 -or $batches.Count -ne 2 -or @@ -421,7 +447,7 @@ try { } $results += @{mutation=$mutation.name; file=$mutation.file; sourceSha256=(Get-FileHash -LiteralPath $path).Hash; detected=$true; replacementCase=$failures[0].id; diagnostic=$failures[0].reason; originalRegressionDetected=$legacyPresent; - proofGroup=$mutation.proofGroup; proofCombination=$mutation.proofCombination; + proofGroup=$mutation.proofGroup; proofCombination=$mutation.proofCombination; proofPhase=$mutation.proofPhase; reports=@($batches | ForEach-Object { @{group=$_.group; logicalCases=$_.logicalCases; counts=$_.counts} })} Write-Host "Detected $($mutation.name): $($failures[0].id)" } diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 249c4bd4..33ce3f11 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -148,6 +148,8 @@ try { $expected['system-move-word-restart-invalid'] = 224 } if ($model -eq '68040') { + $expected['rte-repair-boundaries'] = 143424 + $expected['rte-repair-chained'] = 768960 $expected['rte-validation-batch'] = 129024 $expected['access-fault-batch-dispatch'] = 10368 $expected['rte-access-entry-double-fault'] = 98304 @@ -265,9 +267,10 @@ try { seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; - m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, and supervisor-stack physical RTE validation fault entry/handler return; chained odd-PC saved-SR provenance, user-tail/internal-restoration faults, CP context transfer, other real fault entry and physical timing remain unqualified'; + m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, and executed repair/retry after committed supervisor throwaways; chained odd-PC saved-SR provenance, user-tail/internal-restoration faults, CP context transfer, other real fault entry and physical timing remain unqualified'; + m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='User-tail validation, internal restoration, untouched incoming-trace retry and other real access faults'}; m68040RtePhysicalValidationScope=@{logicalCases=223872; batches=2; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); trace=@(0,0x8000,0x4000); alignment=@(0,1); vbr=@(0,0x10000); faults='Each byte of SR/PC/format/SSW/continuation-EA reads; 14 direct/one/two-throwaway supervisor paths'; phases=@('fault entry','handler return'); undefined='EA and invalid writeback/push data; SSW X'; remaining='User-tail and internal-restoration/double faults, other instruction faults and software writeback handlers'}; - m68040AccessDoubleFaultScope=@{logicalCases=105408; batches=5; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; dispatchCcr=@(0,31); engines=@('accurate','classic','V2'); faults='Every byte of format-7 stack/vector; later handler validation refaults; warmed RTE fallback and compiled B/W/L operand read/write side exits'; recovery='External reset, including restored host entry; interrupt, task/subroutine entry and halted batches remain inactive'; undefined='Partial stack write order, contents and exact SP'; remaining='Internal restoration, handler-entry prefetch, repaired original RTE retry, user-tail validation and general architectural access-fault restart'}; + m68040AccessDoubleFaultScope=@{logicalCases=105408; batches=5; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; dispatchCcr=@(0,31); engines=@('accurate','classic','V2'); faults='Every byte of format-7 stack/vector; later handler validation refaults; warmed RTE fallback and compiled B/W/L operand read/write side exits'; recovery='External reset, including restored host entry; interrupt, task/subroutine entry and halted batches remain inactive'; undefined='Partial stack write order, contents and exact SP'; remaining='Internal restoration, handler-entry prefetch, user-tail validation and general architectural access-fault restart'}; m68040BatchFaultScope=@{logicalCases=139392; batches=2; translationEnabled=$false; paths=@('cold','cached','model-specific cached','self-branch cached'); limits=@('instruction cap','boundary denial','cycle deadline'); architectural='Supervisor RTE validation frames and fatal second-fault halt'; policy='Generic short operand/fetch frames, scalar/batch bus order and machine/native cycle equality'; qualification='Dedicated discovery enumerates independent combinations; BatchFault scope proves four delivery paths, count, callback and retry defects'; remaining='General format-7 restart, entry prefetch and user-tail/internal-restoration faults'}; m68060LowPowerStopScope='Every unrecognized second opcode word in both privilege modes; fixed encoding, status, CCR and incoming trace cases across all profiles. Opcode-PC exception frames follow MC68060UM 8.2.4/5; physical broadcast, pins and ordinary STOP S-clear software disagreement remain unqualified'; cacheInstructionEncodingScope='Every F4xx opcode, all CCRs and both privilege states across eight profiles; scope zero is illegal on 040/060, including neither-cache forms, before privilege effects. Empty fixture caches qualify architectural state and exception outcomes, not physical invalidation, writeback, bus faults or timing'; From d99f7c80efd42399e2c367fccdf14ff853f695f2 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 22:17:03 +0300 Subject: [PATCH 046/211] Fix 68040 instruction-fetch access frames and qualify restart --- Copper68k.Tests/Synthetic/README.md | 28 +++++ .../SyntheticM68040AccessFrameAuditTests.cs | 2 + .../SyntheticM68040BatchFaultTests.cs | 9 +- .../SyntheticM68040InstructionFaultTests.cs | 106 +++++++++++++++++ Copper68k/M68040Support.cs | 18 ++- Copper68k/M68kAdvancedTimingInterpreter.cs | 5 + docs/COPPER68K_REFERENCE_QUALIFICATION.md | 107 ++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 40 +++++++ scripts/test-copper68k-040-access-frames.ps1 | 36 ++++-- .../test-copper68k-synthetic-mutations.ps1 | 28 ++++- scripts/test-copper68k-synthetic.ps1 | 5 +- 11 files changed, 366 insertions(+), 18 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index aae0dec5..7f9c3e48 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -459,3 +459,31 @@ SR. Each mutation must execute both full batches and mismatch in its intended retry/following phase, rather than failing at an unrelated prerequisite. The [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-executed-rte-repair-and-retry--2026-10-05) records current evidence and remaining scope. No existing regression is retired. + +Physical instruction-fetch faults now use a 60-byte format-7 frame with separate +executing-instruction PC and prefetch fault address, instruction TM and no valid +writebacks. Accurate scalar, normal cached and self-branch boundaries retain the +executing PC even when the rejected long covers an extension or the other half +of a fetched long. Data operand/writeback restart remains a separate required +protocol; its existing short-frame policy is not promoted by this correction. + +`SyntheticM68040InstructionFaultTests` adds `instruction-fault-frame` (36,864 +phases / 576 combinations) and `instruction-fault-restart` (79,872 / 624) to +ordinary CI. Fixed opcode/extension/following-opcode/self-branch fixtures check +each byte of the required prefetch, all CCRs, all stack banks, odd/even stacks, +both VBRs, frame trace states and restart values. The handler really executes +RTE, followed by the original instruction and a branch sentinel. These fixtures +use the cache-disabled accurate factory with MMU off; speculative prefetch +deferral, enabled-cache behavior, compiled instruction-PC provenance, data +writebacks/restart and physical timing remain unqualified. + +The complete 040 command now requires 32 tests (26 reports, six fixed examples), +eleven fixture/command identities and both new independent combination matrices, +while retaining the failing 480-case inventory. `-Scope InstructionFault` in +the maintained mutation command detects a short frame, data TM and prefetch PC +substitution, requiring complete executable selection and intended fault-entry +diagnostics. `-Scope BatchFault` retains the complete seven-mutation campaign +including updated architectural self-fetch frames. No regression is retired. +The [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-instruction-fetch-access-faults--2026-10-05) +records current execution, historical proof, mutations, identities and remaining +scope. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 99b098a0..7bbe6836 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -34,6 +34,8 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() report.Record($"68040/RTE/format7/{form}/bank={bank}/op=4E73/ccr={ccr:X2}", "untested", form == "frame-validation-fault" ? "Supervisor validation, executed repair/retry, access-fault stacking/vector double faults and selected accurate-batch paths are covered separately; user-tail validation, internal restoration and handler-entry prefetch remain required" + : form == "real-access-fault-entry" + ? "Cache-disabled accurate instruction-fetch frames/restart are covered separately; data fault/writeback restart, enabled-cache deferral and compiled instruction-PC provenance remain required" : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs index 7b61b86b..d2a58e50 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs @@ -172,16 +172,19 @@ private static Fixture Prepare(SyntheticMachine m, CodeBus bus, string form, boo if (form == "partial-store") { e.A[0] += 4; e.A[1] -= 4; } var kind = form == "self-fetch" ? M68kBusAccessKind.CpuInstructionFetch : form.EndsWith("store") ? M68kBusAccessKind.CpuDataWrite : M68kBusAccessKind.CpuDataRead; var faultAddress = form == "RTE" ? frame + offset : form == "self-fetch" ? 0x1004u : form.EndsWith("store") ? 0x4300 + alignment : 0x4200 + alignment; - var stackedPc = form == "self-fetch" ? faultAddress : faultPc; + var stackedPc = faultPc; var sequence = m.Core.State.ExceptionSequence; e.ControlChecks["single exception"] = (s => s.ExceptionSequence, sequence + 1); e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, savedSr); e.ControlChecks["saved PC (generic short-frame policy where applicable)"] = (s => s.LastExceptionStackedProgramCounter, stackedPc); e.ControlChecks["bypass cleared"] = (s => s.M68040Mmu.BypassTranslation ? 1u : 0u, 0); - var accessFrame = frame - (form == "RTE" ? 60u : 8u); + var accessFrame = frame - (form is "RTE" or "self-fetch" ? 60u : 8u); e.A[7] = accessFrame; if (bank == "MSP") e.MasterStackPointer = accessFrame; - if (form == "RTE") + if (form is "RTE" or "self-fetch") + { SyntheticM68040RteValidationFaultTests.ExpectAccessFrame(m, e, accessFrame, savedSr, faultPc, faultAddress, width); + if (form == "self-fetch") e.Write(accessFrame + 12, 0x0106, 2, m.Model); + } else { // Existing generic delivery policy, not architectural frame qualification. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs new file mode 100644 index 00000000..f335df34 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs @@ -0,0 +1,106 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 8.2.1, 8.4.6/7: instruction faults use format 7 without +// writebacks; FA describes the prefetch, while PC restarts its instruction. +// Cache disabled, physical-map rejection, scalar accurate public factory. +// This does not qualify speculative prefetch deferral or physical bus timing. +public sealed class SyntheticM68040InstructionFaultTests(ITestOutputHelper output) +{ + private const uint Handler = 0x9020; + private static readonly string[] Forms = ["opcode", "extension-low", "next-opcode", "self-branch"]; + + [Fact, Trait("Suite", "Synthetic")] + public void PhysicalInstructionFaultsStackDefinedFormatSevenFields() => Audit(false); + + [Fact, Trait("Suite", "Synthetic")] + public void InstructionFaultHandlerReturnRestartsTheOriginalInstruction() => Audit(true); + + private void Audit(bool restart) + { + var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + var report = new CoverageBatch("68040", restart ? "instruction-fault-restart" : "instruction-fault-frame"); + foreach (var form in Forms) + foreach (var bank in new[] { "user", "ISP", "MSP" }) + foreach (var trace in restart ? new ushort[] { 0 } : [0, 0x8000, 0x4000]) + foreach (var alignment in new uint[] { 0, 1 }) + foreach (var vbr in new uint[] { 0, 0x10000 }) + foreach (var value in !restart || form == "self-branch" ? new uint[] { 2 } : form == "extension-low" + ? [0, 0x7fffffff, 0x80000000, 0xffffffff] : [0, 0x7f, 0xffffff80, 0xffffffff]) + for (var ccr = 0; ccr < 32; ccr++) + for (var faultByte = 0; faultByte < 4; faultByte++) + Run(m, bus, report, form, bank, trace, alignment, vbr, value, ccr, faultByte, restart); + report.Complete(output); + } + + private static void Run(SyntheticMachine m, SyntheticM68040AccessDoubleFaultTests.FaultBus bus, CoverageBatch report, + string form, string bank, ushort trace, uint alignment, uint vbr, uint value, int ccr, int faultByte, bool restart) + { + bus.Disarm(); m.Reset(ccr); + foreach (var stack in new uint[] { 0x4700 + alignment, 0x7400 + alignment, 0x7800 + alignment }) + for (var n = -76; n < 80; n++) m.InitializePhysical(unchecked(stack + (uint)n), (uint)(n ^ 0x5a), 1); + var moveq = (ushort)(0x7000 | (value & 255)); + ushort[] words = form switch { + "extension-low" => [0x203c, (ushort)(value >> 16), (ushort)value, 0x60fe], + "next-opcode" => [0x7201, 0x4e71, moveq, 0x60fe], + "self-branch" => [0x4e71, 0x4e71, 0x4e71, 0x60fe], + _ => [moveq, 0x60fe] + }; + m.InitializePhysical(Handler, 0x4e73, 2); m.InitializePhysical(Handler + 2, 0x7e7e, 2); + m.InitializePhysical(vbr + 8, Handler, 4); + _ = SyntheticExecution.Prepare(m, words); + if (form == "next-opcode") { m.Core.ExecuteInstruction(); m.Core.ExecuteInstruction(); } + var instructionPc = form == "next-opcode" ? 0x1004u : form == "self-branch" ? 0x1006u : 0x1000u; + var fetchAddress = form is "self-branch" or "extension-low" or "next-opcode" ? 0x1004u : 0x1000u; + var nextPc = form is "extension-low" or "next-opcode" or "self-branch" ? 0x1006u : 0x1002u; + var sr = (ushort)((bank == "MSP" ? 0x3000 : bank == "ISP" ? 0x2000 : 0) | trace | ccr); + m.Core.State.SetUserStackPointer(0x7800 + alignment); + m.Core.State.SetInterruptStackPointer(0x4700 + alignment); m.Core.State.SetMasterStackPointer(0x7400 + alignment); + m.Core.State.StatusRegister = sr; m.Core.State.ProgramCounter = instructionPc; m.Core.State.VectorBaseRegister = vbr; + var e = ArchitecturalExpectation.Capture(m); + var sequence = m.Core.State.ExceptionSequence; + var superStack = bank == "MSP" ? 0x7400u + alignment : 0x4700u + alignment; + var frame = superStack - 60; + e.Sr = (ushort)((sr | 0x2000) & ~0xc000); e.A[7] = frame; + e.InactiveStackPointer = 0x7800 + alignment; if (bank == "MSP") e.MasterStackPointer = frame; + e.Pc = Handler; e.ExceptionVector = 2; + e.ControlChecks["saved instruction PC"] = (s => s.LastExceptionStackedProgramCounter, instructionPc); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, sr); + e.ControlChecks["single access error"] = (s => s.ExceptionSequence, sequence + 1); + e.ControlChecks["rejected one long prefetch"] = (_ => bus.Rejected.Count == 1 && + bus.Rejected[0].Address == fetchAddress && bus.Rejected[0].Width == 4 && + bus.Rejected[0].Kind == M68kBusAccessKind.CpuInstructionFetch ? 1u : 0u, 1); + e.ControlChecks["no translation bypass leak"] = (s => s.M68040Mmu.BypassTranslation ? 1u : 0u, 0); + e.Write(frame, sr, 2, m.Model); e.Write(frame + 2, instructionPc, 4, m.Model); e.Write(frame + 6, 0x7008, 2, m.Model); + for (uint n = 8; n < 60; n++) e.MemoryMasks[frame + n] = 0; + e.Write(frame + 12, bank == "user" ? 0x0102u : 0x0106u, 2, m.Model); + e.MemoryMasks[frame + 12] = 255; e.MemoryMasks[frame + 13] = 0x7f; // X undefined. + foreach (var offset in new uint[] { 14, 16, 18 }) { + e.Write(frame + offset, 0, 2, m.Model); e.MemoryMasks[frame + offset + 1] = 0x80; + } + e.Write(frame + 20, fetchAddress, 4, m.Model); + for (uint n = 20; n < 24; n++) e.MemoryMasks[frame + n] = 255; + bus.Arm(fetchAddress + (uint)faultByte, M68kBusAccessKind.CpuInstructionFetch); + var id = $"68040/access-error/instruction/{form}/bank={bank}/T={trace:X4}/align={alignment}/VBR={vbr:X8}/value={value:X8}/byte={faultByte}/op={words[form == "next-opcode" ? 2 : form == "self-branch" ? 3 : 0]:X4}/ccr={ccr:X2}"; + var phases = restart ? new[] { "fault-entry", "handler-return", "restart", "following" } : ["fault-entry", "handler-return"]; + var phase = 0; + bool Step() { + var ok = SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/" + phases[phase++]); + if (!ok) while (phase < phases.Length) report.Record(id + "/" + phases[phase++], "untested", "Instruction fault prerequisite failed"); + return ok; + } + if (!Step()) return; + e.Sr = sr; e.A[7] = bank == "user" ? 0x7800 + alignment : superStack; + e.InactiveStackPointer = bank == "user" ? superStack : 0x7800 + alignment; + if (bank == "MSP") e.MasterStackPointer = superStack; + e.Pc = instructionPc; e.ExceptionVector = null; + if (!Step() || !restart) return; + e.Pc = nextPc; + if (form != "self-branch") { e.D[0] = value; e.Sr = SyntheticExecution.MoveFlags(sr, value, 4); } + if (!Step()) return; + Step(); + } +} diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index d57729f1..f44305f7 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -2539,6 +2539,10 @@ public override int ExecuteInstruction() protected override bool TryHandleM68040ExecutionFault(M68040MmuFault fault) { + // The prefetch bus address may cover an extension or the other + // half of an aligned long. Restart the executing instruction. + if (!State.M68040Mmu.Enabled && fault.AccessKind == M68kBusAccessKind.CpuInstructionFetch) + fault = fault with { StackedProgramCounter = ExecutionBoundaryProgramCounter }; RaiseMmuFault(fault); return true; } @@ -4263,6 +4267,8 @@ private void RaiseMmuFault(M68040MmuFault fault) : State.LastInstructionProgramCounter); if (fault.RteFrameValidation) RaiseRteValidationAccessFault(fault, stackedProgramCounter); + else if (!State.M68040Mmu.Enabled && fault.AccessKind == M68kBusAccessKind.CpuInstructionFetch) + RaiseUnbufferedReadAccessFault(fault, stackedProgramCounter, instruction: true); else RaiseFormat0Exception(VectorBusError, stackedProgramCounter, M68kInstructionTimingKey.IllegalInstruction); } @@ -4278,10 +4284,14 @@ private void RaiseMmuFault(M68040MmuFault fault) } private void RaiseRteValidationAccessFault(M68040MmuFault fault, uint instructionPc) + => RaiseUnbufferedReadAccessFault(fault, instructionPc, instruction: false); + + private void RaiseUnbufferedReadAccessFault(M68040MmuFault fault, uint instructionPc, bool instruction) { - // MC68040UM 8.4.6.7: preserve the frame being validated. Only - // pre-commit reads carry this marker; faults in exception delivery - // or internal restoration must not retry partially completed work. + // MC68040UM 8.4.6/7: instruction-prefetch faults have no pending + // writebacks. Pre-commit RTE validation reads use the same layout, + // preserving the original frame; data operand/write restart is a + // separate path and must not be implemented by replaying effects. var savedSr = State.StatusRegister; State.RecordException(VectorBusError, instructionPc, savedSr); State.StatusRegister = (ushort)((savedSr | M68kCpuState.Supervisor) & ~0xc000); @@ -4292,7 +4302,7 @@ private void RaiseRteValidationAccessFault(M68040MmuFault fault, uint instructio for (var n = 0; n < 9; n++) PushLong(0); PushLong(fault.LogicalAddress); PushWord(0); PushWord(0); PushWord(0); - var modifier = (savedSr & M68kCpuState.Supervisor) != 0 ? 5 : 1; + var modifier = ((savedSr & M68kCpuState.Supervisor) != 0 ? 4 : 0) | (instruction ? 2 : 1); PushWord((ushort)(0x0100 | (fault.ByteCount == 2 ? 0x0040 : 0) | modifier)); PushLong(0); PushWord(0x7008); diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index f672e420..690af41e 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -4500,8 +4500,11 @@ public virtual int ExecuteInstruction() return (int)(State.Cycles - startCycles); } + protected uint ExecutionBoundaryProgramCounter { get; private set; } + private void ExecuteInstructionCore() { + ExecutionBoundaryProgramCounter = State.ProgramCounter; try { ExecuteInstructionWithTrace(); @@ -4702,6 +4705,7 @@ private bool TryExecuteHotBlock( var previousCycle = State.Cycles; var exitBlock = false; + ExecutionBoundaryProgramCounter = State.ProgramCounter; try { if (!TryFetchHotOpcode(in hotInstruction, out var opcode) || @@ -4932,6 +4936,7 @@ private bool TryExecuteShortSelfBranchBlock( var previousCycle = State.Cycles; ushort opcode; + ExecutionBoundaryProgramCounter = State.ProgramCounter; bool cacheHit, requiresSynchronization; long completedMachineCycle; try diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index e3dbfa6b..c65ea576 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -4147,3 +4147,110 @@ untouched incoming-trace retry, chained odd-PC provenance, general real format-7 restart, writeback/context transfer and earlier model/reference/consolidation requirements remain open. The 480-case inventory and `roadmapComplete=false` are preserved. Milestone 6 and the full goal remain **in progress**. + +## 040 instruction-fetch access faults — 2026-10-05 + +The authority is [MC68040UM](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +8.2.1 and 8.4.6, including 8.4.6.2/7. An instruction access error uses a +60-byte format-7 frame: PC identifies the executing instruction, FA identifies +the faulted prefetch, TM selects instruction space, and no writeback is valid. +These are manual-derived synthetic expectations, not hardware captures. The +fixture selects the existing cache-disabled accurate long-prefetch policy; +its four-byte access width is not physical pipeline/bus qualification. + +Production fault entry now uses that layout for MMU-disabled instruction fetch. +Accurate scalar, normal hot-block and self-branch boundaries record the executing +PC before fetching; an extension or aligned long's other half must not become +the restart PC. Existing exception timing policy and partial operand effects +are preserved. Data errors retain their explicitly unqualified short-frame +policy: writebacks/restart cannot be implemented by changing a tag and replaying +an instruction after partial effects. Compiled fetch-PC provenance and enabled +cache/speculative prefetch deferral remain unqualified. + +`SyntheticM68040InstructionFaultTests` adds two ordinary CI batches: + +| Report | Logical instruction phases | Architectural combinations | +| --- | ---: | ---: | +| `instruction-fault-frame` | 36,864 | 576 | +| `instruction-fault-restart` | 79,872 | 624 | + +Fixed opcode, extension-low, following-opcode and self-branch encodings cover +all CCRs, user/ISP/MSP stacks, odd/even stacks, both VBRs and rejection at every +byte of the selected long prefetch. Frame/handler-return coverage includes +incoming T0/T1; deterministic restart values execute handler RTE, the original +instruction and a following branch. Expectations check defined frame fields, +inactive stacks, unchanged registers, surrounding memory, single fault delivery +and restored translation bypass. Undefined frame fields are masked explicitly. + +The first following-opcode fixture remained in an already fetched long; +adding a fixed NOP prefix makes the rejected fetch observable. This was a +fixture correction. With that corrected fixture, historical production source +at `36a8edf266dc682036d638fc9a5bb3cd0985439a` fails all 38,400 fault-entry +cases: 18,432 frame and 19,968 restart mismatches, with later prerequisite +phases explicitly untested. Evidence is in +`artifacts/m6-instruction-fault-before-v2/`; historical CPU SHA-256 is +`f4756485a7945d5fa49e01be6af0c0332d279d60be364c48a435ba7dccc4d313` +and historical adapter SHA-256 is +`c6aab37470dada0f9ceb25aa7899437d09c81019e31c16e200d8a7a44dee3c18`. + +The maintained `-Scope InstructionFault` command executes both complete batches +for each mutation and requires an intended fault-entry mismatch. Short-frame +and data-TM substitutions each produce 18,432/19,968 mismatches; prefetch-PC +substitution produces 9,216/7,680 mismatches. Prerequisite-dependent phases are +untested during mutations, not successful coverage. All three mutations detect +their intended defect in `artifacts/m6-instruction-fault-mutations/`. +The full seven-mutation `BatchFault` campaign also passes with updated +architectural self-fetch expectations in +`artifacts/m6-instruction-fault-batch-mutations/`. Sources are restored +byte-for-byte and rebuilt. No regression is retired. + +The complete dedicated command in `artifacts/m6-instruction-fault-audit/` +executes 32 tests: 26 reporting batches and six fixed examples. It records +2,622,144 passing phases, zero mismatches/unsupported execution and the retained +480 untested requirements. Its intentional inventory failure remains a failed +full discovery gate. Independent enumeration requires eleven fixture/command +identities and fixed per-combination counts. Nine copied-input controls in +`artifacts/m6-instruction-fault-controls/` reject missing, shortened, foreign and +redistributed reports for both new groups, plus omitted fixture identity. +Redistribution preserves aggregate counts and combination cardinality. + +Qualification starts from HEAD `36a8edf266dc682036d638fc9a5bb3cd0985439a`. +The audit's committed-tree identity describes that starting point; recorded +working-source hashes describe the correction. Qualified CPU SHA-256 is +`3cc890bd66d7c5cc403a00a6d2aed556983a74a42595be90c60babb799eeac0d` +and test/reference adapter SHA-256 is +`5540e61568601aa580a039f18cde12e312680ed79a6fab6a56d2476e40034be1`. +Dedicated, full-suite, external-reference and private-consumer executions below +all match these binaries. + +Fresh full Release CPU validation in `artifacts/m6-instruction-fault-full/` +passes 5,057 tests with zero failures and eleven optional skips. All nine pinned +WinUAE presets match their exact directory/callback/frame counts and binaries; +`qualified-preset-identities.json` records each selection. Generator source is +`025b999239800357e95065fe5b9a15ea5b300fa7` and runner source is +`7a83745d6c6159bc74ab0471578ffc8bc244e66e`. Strict report validation checks +17,590,806 ordinary logical cases in 616 reporting batches across all eight +profiles. Fresh external audits pass SingleStepTests' 312,500 cases in 125 +selected files at `64b253116a3de04aaac4346c43680960dc9b67e5` and Musashi's +536 programs with 88 explicit exclusions at +`72c1d74800f3087b45a0c1a7342601bbed898881`. These are scoped software-reference +results, not exhaustive architectural combinations or silicon evidence. + +Isolated CopperScreen baseline `d9beae8b88be24032221e3482942a249c03c27d3` +consumes unpublished private `1.5.2-synthetic-dev.61`, package SHA-256 +`5422e6153e28380b9898fdb58d97fb992d982bd94642f911b31573997420a020`. +The package's embedded CPU, four asset manifests and four loaded CPU DLLs match. +Release build has zero warnings/errors; host tests pass 149 with six optional +skips, disk tests pass 74, separate engine diagnostics pass 1,080, and all three +native Workbench/A1200 boot/persistence replays pass without native skips. +Evidence and identities are under the isolated consumer's +`artifacts/instruction-fault-validation/` and +`artifacts/instruction-fault-identities.json`. Root consumer changes and its +published dependency pin remain preserved. No public package/API is released. + +Data writebacks/restart, compiled fetch-PC provenance, enabled-cache/speculative +prefetch, handler-entry prefetch, user-tail/internal-restoration faults, +untouched incoming-trace retry, context transfer and all earlier model/reference +and consolidation requirements remain open. Original Basic's 47 mismatching and +eight unsupported groups are not reclassified. The 480-case inventory and +`roadmapComplete=false` remain explicit; milestone 6 is **in progress**. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 0289c092..ef5eddcb 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1556,3 +1556,43 @@ unsupported execution and the retained 480 untested requirements. All three repair mutations and nine report/identity controls detect their intended defects. Previous full CPU/external/private-consumer evidence is retained separately; no new external or consumer execution is claimed for this test/gate checkpoint. + +### Milestone 6 instruction-fetch access faults — 2026-10-05 + +Cache-disabled accurate instruction-fetch faults now stack format 7 rather than +the generic 8-byte frame. The executing instruction boundary supplies the saved +PC; the independent FA field retains the aligned prefetch address. This covers +opcode, extension and following-opcode faults, including the cached self-branch +path. Instruction function code is distinguished from data and all writebacks +are invalid. Data fault/writeback restart is still required and is not implemented +by changing a frame tag or replaying partial operand effects. + +Two ordinary batches add 116,736 passing phases across 1,200 combinations. They +check all CCRs, all three stack banks, incoming trace for frame/handler return, +odd/even stacks, VBRs, each byte of the selected long prefetch and deterministic +restart values. Handler RTE, original instruction execution and following branch +sentinels are verified. Historical source fails every fault-entry fixture. +Three maintained `InstructionFault` mutations detect short frame, wrong TM and +prefetch PC substitution; all seven `BatchFault` mutations are retained and +detected with architectural self-fetch expectations. Nine report controls reject +missing/short/foreign/distributed reports and omitted fixture identity. + +The complete 040 discovery selection is 32 tests: 26 reporting batches and six +fixed examples. It records 2,622,144 passing phases, zero mismatches/unsupported +execution and the preserved 480-case untested inventory. Data writebacks/restart, +compiled instruction-fault PC provenance, enabled-cache/speculative prefetch, +handler-entry prefetch, user-tail/internal-restoration faults, untouched incoming +trace retry, context transfer and all earlier model/reference/consolidation +requirements remain explicit. No regression is retired or public package/API +released. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +Fresh full Release CPU validation passes 5,057 tests with eleven optional skips +and no failures; all nine pinned WinUAE selections match their exact counts and +binaries. Strict ordinary validation checks 17,590,806 cases in 616 reporting +batches. Fresh pinned SingleStepTests passes 312,500 cases in 125 files and +Musashi passes 536 programs with 88 explicit exclusions. Isolated CopperScreen +through private unpublished `.61` passes Release build, host/disk tests, separate +engine diagnostics and all three native Workbench/A1200 replays; package, +assets and loaded CPU identities match. Software references and existing timing +policy remain separate from hardware/physical timing qualification. See the +[qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040-instruction-fetch-access-faults--2026-10-05). diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index 3264f9b6..3f92a40a 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -31,6 +31,8 @@ $expected = [ordered]@{ 'access-fault-batch-dispatch' = @{cases=10368; combinations=5184} 'rte-repair-boundaries' = @{cases=143424; combinations=540} 'rte-repair-chained' = @{cases=768960; combinations=45792} + 'instruction-fault-frame' = @{cases=36864; combinations=576} + 'instruction-fault-restart' = @{cases=79872; combinations=624} 'rte-access-remaining-protocols' = @{cases=480; combinations=15} } $testExit = 0 @@ -44,7 +46,7 @@ if (-not $ValidateReportsOnly) { $saved[$name] = [Environment]::GetEnvironmentVariable($name) [Environment]::SetEnvironmentVariable($name, $settings[$name]) } - & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests|FullyQualifiedName~SyntheticM68040RteValidationFaultTests|FullyQualifiedName~SyntheticM68040AccessDoubleFaultTests|FullyQualifiedName~SyntheticM68040BatchFaultTests|FullyQualifiedName~SyntheticM68040RteRepairTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests|FullyQualifiedName~SyntheticM68040RteValidationFaultTests|FullyQualifiedName~SyntheticM68040AccessDoubleFaultTests|FullyQualifiedName~SyntheticM68040BatchFaultTests|FullyQualifiedName~SyntheticM68040RteRepairTests|FullyQualifiedName~SyntheticM68040InstructionFaultTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output $testExit = $LASTEXITCODE } finally { foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } @@ -56,7 +58,7 @@ if (-not $ValidateReportsOnly) { cpuSourceFiles=@(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } - inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs') | ForEach-Object { + inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } assemblies=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | ForEach-Object { @@ -65,7 +67,8 @@ if (-not $ValidateReportsOnly) { limitations=@('Synthetic frames and supervisor-stack physical map faults, not hardware captures or enabled-MMU access faults', 'CP context transfer and detailed fault protocol inventory remain untested and fail the gate', 'Multiple continuation bits are architecturally undefined and excluded', - 'Selected active accurate-batch paths verify counts/callbacks and scalar/batch bus/cycle policy; generic short operand/fetch frames do not qualify architectural format-7 restart', + 'Selected active accurate-batch paths verify counts/callbacks and scalar/batch bus/cycle policy; generic short operand frames do not qualify architectural format-7 data restart', + 'Instruction-fault fixtures use cache-disabled accurate execution and physical-map rejection; speculative deferral, enabled caches/MMU and compiled fetch PC provenance remain unqualified', 'Executed supervisor-tail frame repair explicitly clears saved incoming trace; untouched incoming-trace retry and user-tail repair are not qualified', 'User-tail validation, internal-restoration double faults, handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', 'Direct odd-RTE saved-SR ordering uses documentary WinUAE 5d22d336, not an executed hardware oracle') @@ -77,11 +80,11 @@ if (-not $ValidateReportsOnly) { $identity = Get-Content -LiteralPath (Join-Path $output 'identities.json') -Raw | ConvertFrom-Json if ($identity.schema -ne 1 -or $identity.reference -cne 'MC68040UM' -or $identity.softwareReferenceExecuted -ne $false -or $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.cpuCommittedTree -notmatch '^[a-f0-9]{40}$' -or - @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 10 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 10 -or + @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 11 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 11 -or @($identity.assemblies).Count -ne 2) { throw '040 access-frame input identity is missing or incomplete' } foreach ($input in $identity.inputs) { $path = Join-Path $repo $input.file - if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs') -or + if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs') -or $input.sha256 -cne (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()) { throw '040 access-frame fixture/command identity differs' } } $cpuFiles = @(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') @@ -95,8 +98,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 30 -or [int]$counters.total -ne 30 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (24 batches, 6 fixed examples)' +if ([int]$counters.executed -ne 32 -or [int]$counters.total -ne 32 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (26 batches, 6 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -119,6 +122,21 @@ foreach ($group in $expected.Keys) { $expectedCombinations["68040/RTE/format7/$form/bank=$bank"] = 32 } } + } elseif ($group -in @('instruction-fault-frame','instruction-fault-restart')) { + $restart = $group -eq 'instruction-fault-restart' + foreach ($form in @('opcode','extension-low','next-opcode','self-branch')) { + foreach ($bank in @('user','ISP','MSP')) { foreach ($trace in $(if ($restart) { @(0) } else { @(0,0x8000,0x4000) })) { + foreach ($alignment in @(0,1)) { foreach ($vbr in @(0,0x10000)) { + $values = if (-not $restart -or $form -eq 'self-branch') { @(2L) } elseif ($form -eq 'extension-low') { + @(0L,2147483647L,2147483648L,4294967295L) + } else { @(0L,127L,4294967168L,4294967295L) } + foreach ($value in $values) { foreach ($byte in 0..3) { + $key = '68040/access-error/instruction/{0}/bank={1}/T={2:X4}/align={3}/VBR={4:X8}/value={5:X8}/byte={6}' -f $form,$bank,$trace,$alignment,$vbr,$value,$byte + $expectedCombinations[$key] = if ($restart) {128} else {64} + } } + } } + } } + } } elseif ($group -in @('rte-repair-boundaries','rte-repair-chained')) { $chained = $group -eq 'rte-repair-chained' $paths = if ($chained) { @('ISP-ISP','ISP-ISP-ISP','ISP-MSP-ISP','ISP-MSP','ISP-ISP-MSP','ISP-MSP-MSP', @@ -329,8 +347,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 30 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 32 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=2505888; xunitBatches=24; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=2622624; xunitBatches=26; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index 9bc2a30b..e6ae0046 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','InstructionFault','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -277,6 +277,10 @@ $selfFault = @' $retryFault = @' protected override bool TryHandleM68040ExecutionFault(M68040MmuFault fault) { + // The prefetch bus address may cover an extension or the other + // half of an aligned long. Restart the executing instruction. + if (!State.M68040Mmu.Enabled && fault.AccessKind == M68kBusAccessKind.CpuInstructionFetch) + fault = fault with { StackedProgramCounter = ExecutionBoundaryProgramCounter }; RaiseMmuFault(fault); return true; } @@ -319,6 +323,17 @@ foreach ($mutation in $repairMutations) { $mutation.group='SyntheticM68040RteRepairTests'; $mutation.model='68040' $mutation.milestone=6; $mutation.repair=$true; $mutations += $mutation } +$instructionMutations = @( + @{name='040-instruction-short-frame'; before=' RaiseUnbufferedReadAccessFault(fault, stackedProgramCounter, instruction: true);'; + after=' RaiseFormat0Exception(VectorBusError, stackedProgramCounter, M68kInstructionTimingKey.IllegalInstruction);'; proofCombination='/instruction/opcode/'}, + @{name='040-instruction-data-tm'; before='(instruction ? 2 : 1)'; after='1'; proofCombination='/instruction/opcode/'}, + @{name='040-instruction-prefetch-pc'; before=' fault = fault with { StackedProgramCounter = ExecutionBoundaryProgramCounter };'; + after=' fault = fault with { StackedProgramCounter = fault.LogicalAddress };'; proofCombination='/instruction/extension-low/'} +) +foreach ($mutation in $instructionMutations) { + $mutation.file='Copper68k/M68040Support.cs'; $mutation.group='SyntheticM68040InstructionFaultTests'; $mutation.model='68040' + $mutation.milestone=6; $mutation.instructionFault=$true; $mutation.proofPhase='/fault-entry'; $mutations += $mutation +} if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } if ($Scope -eq 'Logical') { $mutations = @($mutations | Where-Object { $_.milestone -eq 4 }) } @@ -327,6 +342,7 @@ if ($Scope -eq 'Consolidation') { $mutations = @($mutations | Where-Object { $_. if ($Scope -eq 'Rte040') { $mutations = @($mutations | Where-Object { $_.odd }) } if ($Scope -eq 'RteValidationFault') { $mutations = @($mutations | Where-Object { $_.validationFault }) } if ($Scope -eq 'RteRepair') { $mutations = @($mutations | Where-Object { $_.repair }) } +if ($Scope -eq 'InstructionFault') { $mutations = @($mutations | Where-Object { $_.instructionFault }) } if ($Scope -eq 'AccessDoubleFault') { $mutations = @($mutations | Where-Object { $_.doubleFault }) } if ($Scope -eq 'BatchFault') { $mutations = @($mutations | Where-Object { $_.batchFault }) } if ($Scope -eq 'LowPowerStop') { $mutations = @($mutations | Where-Object { $_.lpstop }) } @@ -389,6 +405,16 @@ try { throw 'RTE validation-fault mutation did not execute its complete two-batch selection' } } + if ($mutation.instructionFault) { + [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + if ($trx.TestRun.ResultSummary.Counters.executed -ne 2 -or $batches.Count -ne 2 -or + @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq 'instruction-fault-frame' -and $_.logicalCases -eq 36864 }).Count -ne 1 -or + @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq 'instruction-fault-restart' -and $_.logicalCases -eq 79872 }).Count -ne 1 -or + @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and + $_.id.EndsWith($mutation.proofPhase, [StringComparison]::Ordinal) }).Count -eq 0) { + throw 'Instruction-fault mutation omitted complete cases or intended semantic failure' + } + } if ($mutation.repair) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if ($trx.TestRun.ResultSummary.Counters.executed -ne 2 -or $batches.Count -ne 2 -or diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 33ce3f11..885153af 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -148,6 +148,8 @@ try { $expected['system-move-word-restart-invalid'] = 224 } if ($model -eq '68040') { + $expected['instruction-fault-frame'] = 36864 + $expected['instruction-fault-restart'] = 79872 $expected['rte-repair-boundaries'] = 143424 $expected['rte-repair-chained'] = 768960 $expected['rte-validation-batch'] = 129024 @@ -269,9 +271,10 @@ try { m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, and executed repair/retry after committed supervisor throwaways; chained odd-PC saved-SR provenance, user-tail/internal-restoration faults, CP context transfer, other real fault entry and physical timing remain unqualified'; m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='User-tail validation, internal restoration, untouched incoming-trace retry and other real access faults'}; + m68040InstructionFaultScope=@{logicalCases=116736; batches=2; translationEnabled=$false; instructionCacheEnabled=$false; engine='accurate public factory plus cached self-branch batch'; forms=@('opcode','extension-low','next-opcode','self-branch'); banks=@('user','ISP','MSP'); ccr='all 32'; trace='All defined incoming states for frame/return; restart trace zero'; fields='Format7 SR, executing PC distinct from prefetch FA, RW/SIZE/TM and invalid writebacks'; phases=@('fault entry','handler return','restart','following'); remaining='Data writebacks/restart, speculative prefetch deferral, compiled instruction-fault PC provenance, cache-enabled fetches and physical timing'}; m68040RtePhysicalValidationScope=@{logicalCases=223872; batches=2; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); trace=@(0,0x8000,0x4000); alignment=@(0,1); vbr=@(0,0x10000); faults='Each byte of SR/PC/format/SSW/continuation-EA reads; 14 direct/one/two-throwaway supervisor paths'; phases=@('fault entry','handler return'); undefined='EA and invalid writeback/push data; SSW X'; remaining='User-tail and internal-restoration/double faults, other instruction faults and software writeback handlers'}; m68040AccessDoubleFaultScope=@{logicalCases=105408; batches=5; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; dispatchCcr=@(0,31); engines=@('accurate','classic','V2'); faults='Every byte of format-7 stack/vector; later handler validation refaults; warmed RTE fallback and compiled B/W/L operand read/write side exits'; recovery='External reset, including restored host entry; interrupt, task/subroutine entry and halted batches remain inactive'; undefined='Partial stack write order, contents and exact SP'; remaining='Internal restoration, handler-entry prefetch, user-tail validation and general architectural access-fault restart'}; - m68040BatchFaultScope=@{logicalCases=139392; batches=2; translationEnabled=$false; paths=@('cold','cached','model-specific cached','self-branch cached'); limits=@('instruction cap','boundary denial','cycle deadline'); architectural='Supervisor RTE validation frames and fatal second-fault halt'; policy='Generic short operand/fetch frames, scalar/batch bus order and machine/native cycle equality'; qualification='Dedicated discovery enumerates independent combinations; BatchFault scope proves four delivery paths, count, callback and retry defects'; remaining='General format-7 restart, entry prefetch and user-tail/internal-restoration faults'}; + m68040BatchFaultScope=@{logicalCases=139392; batches=2; translationEnabled=$false; paths=@('cold','cached','model-specific cached','self-branch cached'); limits=@('instruction cap','boundary denial','cycle deadline'); architectural='Supervisor RTE validation frames, unbuffered self-branch instruction-fault frame and fatal second-fault halt'; policy='Generic short operand frames, scalar/batch bus order and machine/native cycle equality'; qualification='Dedicated discovery enumerates independent combinations; BatchFault scope proves four delivery paths, count, callback and retry defects'; remaining='General data format-7 restart, entry prefetch and user-tail/internal-restoration faults'}; m68060LowPowerStopScope='Every unrecognized second opcode word in both privilege modes; fixed encoding, status, CCR and incoming trace cases across all profiles. Opcode-PC exception frames follow MC68060UM 8.2.4/5; physical broadcast, pins and ordinary STOP S-clear software disagreement remain unqualified'; cacheInstructionEncodingScope='Every F4xx opcode, all CCRs and both privilege states across eight profiles; scope zero is illegal on 040/060, including neither-cache forms, before privilege effects. Empty fixture caches qualify architectural state and exception outcomes, not physical invalidation, writeback, bus faults or timing'; m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; marked private word-MOVE/MOVEA images resume their faulted cycle; other opaque restart/input state, long transfers, RMW and physical timing remain unqualified'; From 92922a7aabe51413fdec40d25cbc9beba0ad6632 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 22:40:38 +0300 Subject: [PATCH 047/211] Keep 040 speculative host reads inside their supported boundary --- Copper68k.Tests/Synthetic/README.md | 18 +++ .../SyntheticM68040HandlerPrefetchTests.cs | 146 ++++++++++++++++++ Copper68k/M68040Support.cs | 2 + Copper68k/M68kAdvancedTimingInterpreter.cs | 6 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 104 +++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 32 ++++ scripts/test-copper68k-040-access-frames.ps1 | 35 ++++- .../test-copper68k-synthetic-mutations.ps1 | 28 +++- scripts/test-copper68k-synthetic.ps1 | 3 + 9 files changed, 362 insertions(+), 12 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 7f9c3e48..42c9ad21 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -487,3 +487,21 @@ including updated architectural self-fetch frames. No regression is retired. The [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-instruction-fetch-access-faults--2026-10-05) records current execution, historical proof, mutations, identities and remaining scope. + +`SyntheticM68040HandlerPrefetchTests` adds two ordinary reports with 24,576 +cases. They check that a denied cold batch makes no CPU fetch/state change, and +that an instruction-fetch fault after an executed handler MOVEQ/NOP prefix starts +a new format-7 exception with precise handler PC/SR. The accurate 040 engine +requires a physical host code reader for speculative hot-block construction; +the logical bus's real-fetch fallback cannot run before an instruction boundary. +`-Scope HandlerPrefetch` restores that defect and requires intended denial proof. + +The two entry-prefetch reports are enabled only by the complete 040 discovery +command. They reject each byte of the required four-long window and currently +fail all 393,216 cases: deferred demand fetch does not perform architectural +handler-entry prefetch. The dedicated command requires 36 tests (30 reports, +six fixed examples), twelve fixture identities and all four independent new +combination matrices. It preserves the earlier 480 untested requirements. +These entry mismatches are not a passing ordinary-CI qualification. See the +[qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-host-reader-boundary-and-handler-prefetch-discovery--2026-10-05) +for authorities, evidence and the required follow-up correction. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs new file mode 100644 index 00000000..fab2e9bd --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs @@ -0,0 +1,146 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 8.1 figure 8-1 / 7.6.3: handler-entry prefetch belongs +// to exception processing; a later fault in executing handler code does not. +// Discovery remains failing until the full required entry window is modeled. +public sealed class SyntheticM68040HandlerPrefetchTests(ITestOutputHelper output) +{ + private const string Enable = "COPPER68K_RUN_040_ACCESS_FRAME_AUDIT"; + private const uint Handler = 0x9020; + + [EnvironmentFact(Enable, "qualify the complete 040 handler-entry prefetch window"), Trait("Suite", "ReferenceDiscovery")] + public void EveryRequiredScalarEntryPrefetchByteHaltsBeforeHandlerExecution() => Audit("scalar", true); + + [EnvironmentFact(Enable, "qualify the complete 040 handler-entry prefetch window"), Trait("Suite", "ReferenceDiscovery")] + public void EveryRequiredBatchEntryPrefetchByteHaltsBeforeHandlerExecution() => Audit("batch", true); + + [Theory, Trait("Suite", "Synthetic")] + [InlineData("scalar")] + [InlineData("batch")] + public void FetchFaultAfterHandlerExecutionStartsANewException(string route) => Audit(route, false); + + private void Audit(string route, bool entry) + { + var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + var report = new CoverageBatch("68040", $"handler-prefetch-{(entry ? "entry" : "executing")}-{route}"); + foreach (var form in new[] { "opcode", "extension", "RTE" }) + foreach (var bank in form == "RTE" ? new[] { "ISP", "MSP" } : ["user", "ISP", "MSP"]) + foreach (var trace in new ushort[] { 0, 0x8000, 0x4000 }) + foreach (var alignment in new uint[] { 0, 1 }) + foreach (var vbr in new uint[] { 0, 0x10000 }) + foreach (var handlerOffset in entry ? new uint[] { 0, 2, 4, 6 } : [0]) + for (var faultByte = 0; faultByte < (entry ? 16 : 4); faultByte++) + for (var ccr = 0; ccr < 32; ccr++) + { + var id = $"68040/access-error/handler-prefetch/{form}/route={route}/bank={bank}/T={trace:X4}/align={alignment}/VBR={vbr:X8}/handler={handlerOffset}/byte={faultByte}/op={(form == "RTE" ? 0x4e73 : form == "extension" ? 0x203c : 0x7002):X4}/ccr={ccr:X2}"; + try { Run(m, bus, route, entry, form, bank, trace, alignment, vbr, handlerOffset, faultByte, ccr); report.Record(id, "passing", null); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + report.Complete(output); + } + + private static void Run(SyntheticMachine m, SyntheticM68040AccessDoubleFaultTests.FaultBus bus, + string route, bool entry, string form, string bank, ushort trace, uint alignment, uint vbr, + uint handlerOffset, int faultByte, int ccr) + { + bus.Disarm(); m.Reset(ccr); + var sr = (ushort)((bank == "MSP" ? 0x3000 : bank == "ISP" ? 0x2000 : 0) | trace | ccr); + var stack = (bank == "MSP" ? 0x7400u : 0x4700u) + alignment; + var frame = stack - 60; + foreach (var at in new uint[] { 0x4700 + alignment, 0x7400 + alignment, 0x7800 + alignment }) + for (var n = -140; n < 100; n++) m.InitializePhysical(unchecked(at + (uint)n), (uint)(n ^ 0x5a), 1); + m.InitializePhysical(stack, 0x201f, 2); m.InitializePhysical(stack + 2, 0x6000, 4); + m.InitializePhysical(stack + 6, 8, 2); + for (uint n = 0; n < 24; n += 2) m.InitializePhysical(Handler + n, 0x4e71, 2); + m.InitializePhysical(Handler + 4, entry ? 0x4e71u : 0x7002u, 2); + m.InitializePhysical(Handler + handlerOffset, 0x7201, 2); // MOVEQ #1,D1 + m.InitializePhysical(vbr + 8, Handler + handlerOffset, 4); + _ = SyntheticExecution.Prepare(m, form == "RTE" ? [0x4e73] : form == "extension" ? [0x203c, 0x1234, 0x5678] : [0x7002]); + var s = m.Core.State; + s.SetUserStackPointer(0x7800 + alignment); s.SetInterruptStackPointer(0x4700 + alignment); + s.SetMasterStackPointer(0x7400 + alignment); s.StatusRegister = sr; s.VectorBaseRegister = vbr; + var before = ArchitecturalExpectation.Capture(m); var sequence = s.ExceptionSequence; + var firstAddress = form == "RTE" ? stack : form == "extension" ? 0x1004u : 0x1000u; + bus.Arm(firstAddress, form == "RTE" ? M68kBusAccessKind.CpuDataRead : M68kBusAccessKind.CpuInstructionFetch); + if (entry) bus.Arm(Handler + (uint)faultByte, M68kBusAccessKind.CpuInstructionFetch); + if (route == "batch") + { + var denied = new DeniedBoundary(); var accesses = bus.Accesses.Count; + var count = ((IM68kBatchCore)m.Core).ExecuteInstructions(1, s.Cycles + 1000, denied); + if (count != 0 || denied.Before != 1 || denied.After != 0 || bus.Rejected.Count != 0 || + bus.Accesses.Count != accesses || before.Verify(m) is { }) + throw new InvalidOperationException("Denied cold batch performed a speculative CPU fetch before its boundary"); + } + Execute(m.Core, route); + if (bus.Rejected.Count == 0 || bus.Rejected[0].Address != firstAddress || s.ExceptionSequence != sequence + 1) + throw new InvalidOperationException("Original access-error entry prerequisite differs"); + if (entry) + { + if (!s.Halted || s.Stopped || bus.Rejected.Count != 2) + throw new InvalidOperationException($"Handler-entry prefetch must halt before execution: halted={s.Halted}, rejections={bus.Rejected.Count}"); + var rejected = bus.Rejected[1]; + if (rejected.Kind != M68kBusAccessKind.CpuInstructionFetch || rejected.Width != 4 || + rejected.Address != (Handler + (uint)faultByte & ~3u) || bus.Accesses.Count != rejected.AccessCount) + throw new InvalidOperationException("Wrong entry prefetch or transfer after fatal rejection"); + if (!s.D.SequenceEqual(before.D) || !s.A.Take(7).SequenceEqual(before.A.Take(7))) + throw new InvalidOperationException("Handler instruction or unrelated register changed during entry"); + var frozen = ArchitecturalExpectation.Capture(m); frozen.Halted = true; + var accesses = bus.Accesses.Count; + m.Core.RequestInterrupt(7, 31 * 4); m.Core.BeginSubroutine(0x6000, 0x8000, 0x7000); + m.Core.SwitchTaskContext(new M68kCpuState()); Execute(m.Core, route, halted: true); + var mismatch = frozen.Verify(m); + if (mismatch != null || bus.Accesses.Count != accesses || s.ExceptionSequence != sequence + 1) + throw new InvalidOperationException("Host entry resumed fatal state: " + mismatch); + bus.Disarm(); m.InitializePhysical(0x6000, 0x747b, 2); m.Core.Reset(0x6000, 0x8000); + Execute(m.Core, route); + if (s.Halted || s.Stopped || s.D[2] != 123 || s.ProgramCounter != 0x6002) + throw new InvalidOperationException("External reset did not resume the halted CPU"); + return; + } + if (s.Halted || s.ProgramCounter != Handler || s.A[7] != frame) + throw new InvalidOperationException("Access-error entry did not reach the handler"); + if (!s.D.SequenceEqual(before.D) || !s.A.Take(7).SequenceEqual(before.A.Take(7))) + throw new InvalidOperationException("Faulted instruction or handler prefix executed during entry"); + Execute(m.Core, route); Execute(m.Core, route); // MOVEQ; NOP before the next long. + var handlerSr = (ushort)(((sr | 0x2000) & ~0xc000 & ~15) | 0); + if (s.ProgramCounter != Handler + 4 || s.D[1] != 1 || s.StatusRegister != handlerSr || s.A[7] != frame) + throw new InvalidOperationException("Executed handler prefix prerequisite differs"); + var e = ArchitecturalExpectation.Capture(m); + var nested = frame - 60; e.A[7] = nested; if (bank == "MSP") e.MasterStackPointer = nested; + e.Pc = Handler; e.ExceptionVector = 2; + e.ControlChecks["nested exception"] = (x => x.ExceptionSequence, sequence + 2); + e.ControlChecks["handler PC saved"] = (x => x.LastExceptionStackedProgramCounter, Handler + 4); + e.ControlChecks["handler SR saved"] = (x => x.LastExceptionStatusRegister, handlerSr); + e.ControlChecks["exactly two rejections"] = (_ => (uint)bus.Rejected.Count, 2); + SyntheticM68040RteValidationFaultTests.ExpectAccessFrame(m, e, nested, handlerSr, Handler + 4, Handler + 4, 4); + e.Write(nested + 12, 0x0106, 2, m.Model); // Instruction TM, not data TM. + bus.Arm(Handler + 4 + (uint)faultByte, M68kBusAccessKind.CpuInstructionFetch); + Execute(m.Core, route); + var error = e.Verify(m); + if (error != null) throw new InvalidOperationException(error); + } + + private static void Execute(IM68kCore core, string route, bool halted = false) + { + if (route == "scalar") core.ExecuteInstruction(); + else if (((IM68kBatchCore)core).ExecuteInstructions(1, core.State.Cycles + 1000, new Boundary()) != (halted ? 0 : 1)) + throw new InvalidOperationException("Wrong batch instruction count"); + } + + private sealed class Boundary : IM68kInstructionBoundary + { + public bool BeforeInstruction() => true; + public void AfterInstruction(long previousCycle, long currentCycle) { } + } + + private sealed class DeniedBoundary : IM68kInstructionBoundary + { + public int Before, After; + public bool BeforeInstruction() { Before++; return false; } + public void AfterInstruction(long previousCycle, long currentCycle) => After++; + } +} diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index f44305f7..47c2f966 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -1950,6 +1950,8 @@ public M68040LogicalBus(IM68kBus physicalBus, M68kCpuState state) internal IM68kBus PhysicalBus => _physicalBus; + internal bool HasHostCodeReader => _codeReader != null; + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool CanUseDirectIdentityAccess(uint address, int byteCount) => _allPhysicalAddressesMapped && diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index 690af41e..f007ccbd 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -4375,7 +4375,11 @@ internal M68kAdvancedTimingInterpreter( M68kAcceleratorModel.M68040 => M68040FastKinds, _ => M68020FastKinds }; - _codeReader = bus as IM68kCodeReader; + // The 040 logical bus can implement host reads using real fetches. + // Those must execute inside an instruction boundary, never during + // speculative hot-block construction. + _codeReader = bus is M68040LogicalBus { HasHostCodeReader: false } + ? null : bus as IM68kCodeReader; _jitBus = bus as IM68kJitBus; _physicalAddressMap = bus as IM68kPhysicalAddressMap; _hasModelSpecificInstructions = hasModelSpecificInstructions; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index c65ea576..800d4436 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -4254,3 +4254,107 @@ untouched incoming-trace retry, context transfer and all earlier model/reference and consolidation requirements remain open. Original Basic's 47 mismatching and eight unsupported groups are not reclassified. The 480-case inventory and `roadmapComplete=false` remain explicit; milestone 6 is **in progress**. + +## 040 host-reader boundary and handler-prefetch discovery — 2026-10-05 + +The authority for the handler boundary is +[MC68040UM](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +8.1, figure 8-1, and 7.6.3. Entry prefetch precedes handler execution and a +fault during entry halts the processor; a later fault in executing handler code +starts another exception. Four required entry longs must be distinguished from +the demand-driven frontend's current fetch policy. Software-reference agreement +or a successful later handler instruction cannot qualify this entry window. + +Investigation also found that the logical 040 bus advertises `IM68kCodeReader` +even when its physical bus has no host code reader. Its fallback host read then +performs a real instruction fetch during speculative hot-block construction, +before `BeforeInstruction`, and can leak the physical fault to the host. The +accurate interpreter now admits hot-block construction only when the physical +bus supplies a host code reader. Other buses execute through the ordinary +instruction boundary and fault handler. This preserves the existing timing +keys and available host-reader fast path; no throughput or physical timing +qualification is inferred. + +`SyntheticM68040HandlerPrefetchTests` supplies fixed opcode, extension and RTE +validation faults. Instruction faults cover user/ISP/MSP; RTE begins on ISP/MSP. +All CCRs, incoming trace states, odd/even stacks, VBRs and rejected bytes are +included. A denied cold batch must make one before callback, no after callback, +no fetch/rejection, and no state/memory change. After a valid handler MOVEQ/NOP +prefix, a new instruction-fetch fault must preserve the prefix, stack an +independently checked format-7 frame and retain precise handler PC/SR. + +| Report | Logical cases | Architectural combinations | Status | +| --- | ---: | ---: | --- | +| `handler-prefetch-executing-scalar` | 12,288 | 384 | Passing ordinary CI | +| `handler-prefetch-executing-batch` | 12,288 | 384 | Passing ordinary CI after correction | +| `handler-prefetch-entry-scalar` | 196,608 | 6,144 | Failing reference discovery | +| `handler-prefetch-entry-batch` | 196,608 | 6,144 | Failing reference discovery | + +The entry reports reject each byte of the four-long window with four handler +word offsets. They require fatal halt before handler execution, no second +exception or subsequent bus transfer, frozen host-entry behavior and external +reset recovery. Current source fails at the entry prerequisite: no handler +prefetch is attempted during exception processing. Subsequent halt/reset checks +are therefore not yet qualified by this fixture. These failures are actual +executed mismatches, not exclusions or renamed passing policy coverage. + +Before correction, the focused executing-handler audit passes its scalar batch +but fails all 12,288 batch cases; evidence is in +`artifacts/m6-handler-prefetch-positive-v2/`. The maintained mutation command +`-Scope HandlerPrefetch` reinstates the old host-reader admission rule, executes +both full ordinary reports and requires a denied-boundary mismatch. It detects +the defect in `artifacts/m6-handler-prefetch-mutations-v2/`; production sources are +restored byte-for-byte and rebuilt. No historical regression is retired. + +The complete run in `artifacts/m6-handler-prefetch-audit/` executes 36 tests: +33 pass and three discovery tests fail. It checks 3,040,416 logical cases: +2,646,720 passing, 393,216 entry-prefetch mismatches, zero unsupported execution +and 480 untested inventory cases. Seventeen copied-input controls in +`artifacts/m6-handler-prefetch-controls/` reject omitted, shortened, foreign and +redistributed combinations in all four new reports, plus missing fixture +identity. Controls require their specific diagnostic; a pre-existing discovery +failure cannot satisfy corruption detection. Redistribution preserves aggregate +counts and combination cardinality. + +The source baseline is `d99f7c80efd42399e2c367fccdf14ff853f695f2`; the dedicated +manifest distinguishes that committed tree from corrected working-source +hashes. Qualified CPU SHA-256 is +`adae3e327e83199dbc804346b7eb1814c05b2f05b89a799d42cfb8e6043b0146` +and test/reference adapter SHA-256 is +`4dabd59a3453a4dd1320224a04dcefa9dfa5045066da3f83facffedaea0f80d7`. +The isolated consumer at `d9beae8b88be24032221e3482942a249c03c27d3` +consumes unpublished private `.62`, package SHA-256 +`da4a2dd6c57d2248809d0b873dd4f96f96040bcce98416a4543e665598abd955`. +Release build passes with zero warnings/errors; host 149/six optional skips, +disk 74, separate engine diagnostics 1,080 and all three native Workbench/A1200 +boot/persistence replays pass, with no native skips. The embedded CPU, four +assets and four loaded DLLs match. Evidence is under the isolated consumer's +`artifacts/handler-prefetch-validation/` and +`artifacts/handler-prefetch-identities.json`; root CopperScreen edits and its +published dependency pin are preserved. No public package is released. + +Fresh full Release CPU execution in `artifacts/m6-handler-prefetch-full/` +passes 5,059 tests with zero failures and thirteen opt-in skips. Two skips are +the required handler-entry discovery tests executed separately above; their +failures are not successful full-goal coverage. All nine pinned WinUAE presets +match exact directory/callback/frame counts and the same CPU/adapter binaries; +`qualified-preset-identities.json` records each selection. Generator and runner +pins remain `025b999239800357e95065fe5b9a15ea5b300fa7` and +`7a83745d6c6159bc74ab0471578ffc8bc244e66e`. Strict ordinary report validation +checks 17,615,382 cases in 618 batches across all eight profiles. Fresh pinned +SingleStepTests passes 312,500 cases / 125 files at +`64b253116a3de04aaac4346c43680960dc9b67e5`; Musashi passes 536 programs with +88 explicit exclusions at `72c1d74800f3087b45a0c1a7342601bbed898881`. +The restored mutation sources/binaries match this qualified snapshot. These +software-reference selections do not qualify the missing handler-entry window, +silicon behavior, physical timing or the full roadmap. + +The dedicated gate independently enumerates all four new reports and requires +36 tests, 30 reporting batches, six fixed examples and twelve fixture/command +identities. The new entry mismatches must fail it; the earlier 480-case required +inventory remains intact. Completing entry prefetch requires a real buffered +entry/fault protocol that handles the complete window and distinguishes faults +after execution starts. Suppressing the discovery test, fetching only a safe +first word or retrying an instruction after operand side effects is insufficient. +All earlier model, restart, writeback, reference and consolidation requirements +remain open. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index ef5eddcb..0c2bcd1d 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1596,3 +1596,35 @@ engine diagnostics and all three native Workbench/A1200 replays; package, assets and loaded CPU identities match. Software references and existing timing policy remain separate from hardware/physical timing qualification. See the [qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040-instruction-fetch-access-faults--2026-10-05). + +### Milestone 6 host-reader boundary and handler-entry discovery — 2026-10-05 + +The accurate 040 engine no longer builds speculative hot blocks through a +logical host-read fallback that performs real CPU fetches before the instruction +boundary. Buses without a physical host code reader use ordinary execution; +buses with a host reader retain the fast path. Two ordinary batches add 24,576 +passing cases for denied cold-batch boundaries and faults after an executed +handler prefix. A maintained mutation restores the old admission rule and must +fail at the denied boundary. + +Two reference-discovery batches execute the complete four-long handler-entry +prefetch requirement: 393,216 mismatching cases across scalar/batch routes. +They do not promote the existing deferred demand-fetch policy as architectural +entry qualification. The complete 040 gate now requires 36 tests, 30 reports, +six fixed examples and twelve fixture identities, while preserving its 480-case +untested inventory. The next correction must implement buffered entry prefetch, +fatal entry faults and the boundary to later handler execution; all earlier +requirements remain open. No regression or public API/package is retired or +released. Milestone 6 remains **in progress**, `roadmapComplete=false`. +See the [qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040-host-reader-boundary-and-handler-prefetch-discovery--2026-10-05). + +Validation passes 5,059 Release CPU tests with thirteen opt-in skips and all nine +pinned WinUAE selections. Strict ordinary reporting checks 17,615,382 cases in +618 batches; fresh SingleStepTests passes 312,500 cases / 125 files and Musashi +passes 536 programs with 88 exclusions. All seventeen report/identity controls +detect their intended corruptions. Isolated CopperScreen through unpublished +private `.62` passes Release build, host/disk suites, separate engine diagnostics +and all three native replays with matching package/assets/CPU binaries. The +complete 040 audit intentionally fails: 2,646,720 passing cases, 393,216 +handler-entry mismatches and 480 untested requirements. Ordinary skips and +software references do not imply completion of that required discovery scope. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index 3f92a40a..f8b4f2b3 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -33,6 +33,10 @@ $expected = [ordered]@{ 'rte-repair-chained' = @{cases=768960; combinations=45792} 'instruction-fault-frame' = @{cases=36864; combinations=576} 'instruction-fault-restart' = @{cases=79872; combinations=624} + 'handler-prefetch-entry-scalar' = @{cases=196608; combinations=6144} + 'handler-prefetch-entry-batch' = @{cases=196608; combinations=6144} + 'handler-prefetch-executing-scalar' = @{cases=12288; combinations=384} + 'handler-prefetch-executing-batch' = @{cases=12288; combinations=384} 'rte-access-remaining-protocols' = @{cases=480; combinations=15} } $testExit = 0 @@ -46,7 +50,7 @@ if (-not $ValidateReportsOnly) { $saved[$name] = [Environment]::GetEnvironmentVariable($name) [Environment]::SetEnvironmentVariable($name, $settings[$name]) } - & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests|FullyQualifiedName~SyntheticM68040RteValidationFaultTests|FullyQualifiedName~SyntheticM68040AccessDoubleFaultTests|FullyQualifiedName~SyntheticM68040BatchFaultTests|FullyQualifiedName~SyntheticM68040RteRepairTests|FullyQualifiedName~SyntheticM68040InstructionFaultTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests|FullyQualifiedName~SyntheticM68040RteValidationFaultTests|FullyQualifiedName~SyntheticM68040AccessDoubleFaultTests|FullyQualifiedName~SyntheticM68040BatchFaultTests|FullyQualifiedName~SyntheticM68040RteRepairTests|FullyQualifiedName~SyntheticM68040InstructionFaultTests|FullyQualifiedName~SyntheticM68040HandlerPrefetchTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output $testExit = $LASTEXITCODE } finally { foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } @@ -58,7 +62,7 @@ if (-not $ValidateReportsOnly) { cpuSourceFiles=@(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } - inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs') | ForEach-Object { + inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } assemblies=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | ForEach-Object { @@ -80,11 +84,11 @@ if (-not $ValidateReportsOnly) { $identity = Get-Content -LiteralPath (Join-Path $output 'identities.json') -Raw | ConvertFrom-Json if ($identity.schema -ne 1 -or $identity.reference -cne 'MC68040UM' -or $identity.softwareReferenceExecuted -ne $false -or $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.cpuCommittedTree -notmatch '^[a-f0-9]{40}$' -or - @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 11 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 11 -or + @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 12 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 12 -or @($identity.assemblies).Count -ne 2) { throw '040 access-frame input identity is missing or incomplete' } foreach ($input in $identity.inputs) { $path = Join-Path $repo $input.file - if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs') -or + if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs') -or $input.sha256 -cne (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()) { throw '040 access-frame fixture/command identity differs' } } $cpuFiles = @(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') @@ -98,8 +102,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 32 -or [int]$counters.total -ne 32 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (26 batches, 6 fixed examples)' +if ([int]$counters.executed -ne 36 -or [int]$counters.total -ne 36 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (30 batches, 6 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -122,6 +126,21 @@ foreach ($group in $expected.Keys) { $expectedCombinations["68040/RTE/format7/$form/bank=$bank"] = 32 } } + } elseif ($group.StartsWith('handler-prefetch-', [StringComparison]::Ordinal)) { + $entry = $group.StartsWith('handler-prefetch-entry-', [StringComparison]::Ordinal) + $route = if ($group.EndsWith('-scalar', [StringComparison]::Ordinal)) { 'scalar' } else { 'batch' } + foreach ($form in @('opcode','extension','RTE')) { + foreach ($bank in $(if ($form -eq 'RTE') { @('ISP','MSP') } else { @('user','ISP','MSP') })) { + foreach ($trace in @(0,0x8000,0x4000)) { foreach ($alignment in @(0,1)) { foreach ($vbr in @(0,0x10000)) { + foreach ($offset in $(if ($entry) { @(0,2,4,6) } else { @(0) })) { + foreach ($byte in 0..$(if ($entry) {15} else {3})) { + $key = '68040/access-error/handler-prefetch/{0}/route={1}/bank={2}/T={3:X4}/align={4}/VBR={5:X8}/handler={6}/byte={7}' -f $form,$route,$bank,$trace,$alignment,$vbr,$offset,$byte + $expectedCombinations[$key] = 32 + } + } + } } } + } + } } elseif ($group -in @('instruction-fault-frame','instruction-fault-restart')) { $restart = $group -eq 'instruction-fault-restart' foreach ($form in @('opcode','extension-low','next-opcode','self-branch')) { @@ -347,8 +366,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 32 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 36 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=2622624; xunitBatches=26; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=3040416; xunitBatches=30; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index e6ae0046..74d8c30d 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','InstructionFault','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','InstructionFault','HandlerPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -334,6 +334,13 @@ foreach ($mutation in $instructionMutations) { $mutation.file='Copper68k/M68040Support.cs'; $mutation.group='SyntheticM68040InstructionFaultTests'; $mutation.model='68040' $mutation.milestone=6; $mutation.instructionFault=$true; $mutation.proofPhase='/fault-entry'; $mutations += $mutation } +$mutations += @{ + name='040-host-reader-speculative-fetch'; file=$advanced; before=@' + _codeReader = bus is M68040LogicalBus { HasHostCodeReader: false } + ? null : bus as IM68kCodeReader; +'@; after=' _codeReader = bus as IM68kCodeReader;'; + group='FetchFaultAfterHandlerExecutionStartsANewException'; model='68040'; milestone=6; handlerPrefetch=$true +} if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } if ($Scope -eq 'Logical') { $mutations = @($mutations | Where-Object { $_.milestone -eq 4 }) } @@ -343,6 +350,7 @@ if ($Scope -eq 'Rte040') { $mutations = @($mutations | Where-Object { $_.odd }) if ($Scope -eq 'RteValidationFault') { $mutations = @($mutations | Where-Object { $_.validationFault }) } if ($Scope -eq 'RteRepair') { $mutations = @($mutations | Where-Object { $_.repair }) } if ($Scope -eq 'InstructionFault') { $mutations = @($mutations | Where-Object { $_.instructionFault }) } +if ($Scope -eq 'HandlerPrefetch') { $mutations = @($mutations | Where-Object { $_.handlerPrefetch }) } if ($Scope -eq 'AccessDoubleFault') { $mutations = @($mutations | Where-Object { $_.doubleFault }) } if ($Scope -eq 'BatchFault') { $mutations = @($mutations | Where-Object { $_.batchFault }) } if ($Scope -eq 'LowPowerStop') { $mutations = @($mutations | Where-Object { $_.lpstop }) } @@ -405,6 +413,16 @@ try { throw 'RTE validation-fault mutation did not execute its complete two-batch selection' } } + if ($mutation.handlerPrefetch) { + [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + if ($trx.TestRun.ResultSummary.Counters.executed -ne 2 -or $batches.Count -ne 2 -or + @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq 'handler-prefetch-executing-scalar' -and $_.logicalCases -eq 12288 }).Count -ne 1 -or + @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq 'handler-prefetch-executing-batch' -and $_.logicalCases -eq 12288 }).Count -ne 1 -or + @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains('/route=batch/', [StringComparison]::Ordinal) -and + $_.reason.StartsWith('Denied cold batch', [StringComparison]::Ordinal) }).Count -eq 0) { + throw 'Host-reader mutation omitted complete cases or intended boundary failure' + } + } if ($mutation.instructionFault) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if ($trx.TestRun.ResultSummary.Counters.executed -ne 2 -or $batches.Count -ne 2 -or @@ -471,11 +489,15 @@ try { [xml]$legacyTrx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if (@($legacyTrx.TestRun.Results.UnitTestResult | Where-Object { ($_.testName.EndsWith($mutation.legacyTest) -or $_.testName.Contains($mutation.legacyTest + '(')) -and $_.outcome -eq 'Failed' }).Count -ne 1) { throw 'Original regression did not detect the same mutation' } } + $proofFailure = if ($mutation.handlerPrefetch) { + @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains('/route=batch/', [StringComparison]::Ordinal) -and + $_.reason.StartsWith('Denied cold batch', [StringComparison]::Ordinal) })[0] + } else { $failures[0] } $results += @{mutation=$mutation.name; file=$mutation.file; sourceSha256=(Get-FileHash -LiteralPath $path).Hash; - detected=$true; replacementCase=$failures[0].id; diagnostic=$failures[0].reason; originalRegressionDetected=$legacyPresent; + detected=$true; replacementCase=$proofFailure.id; diagnostic=$proofFailure.reason; originalRegressionDetected=$legacyPresent; proofGroup=$mutation.proofGroup; proofCombination=$mutation.proofCombination; proofPhase=$mutation.proofPhase; reports=@($batches | ForEach-Object { @{group=$_.group; logicalCases=$_.logicalCases; counts=$_.counts} })} - Write-Host "Detected $($mutation.name): $($failures[0].id)" + Write-Host "Detected $($mutation.name): $($proofFailure.id)" } finally { [IO.File]::WriteAllBytes($path, $saved[$path]) } } diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 885153af..4235413d 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -173,6 +173,8 @@ try { $expected['rte-access-trace'] = 13824 $expected['rte-access-movem-opcodes'] = 120960 $expected['rte-access-movem-full-index'] = 114048 + $expected['handler-prefetch-executing-scalar'] = 12288 + $expected['handler-prefetch-executing-batch'] = 12288 $expected['system-mmu-disabled'] = 34850 $expected['system-linef-pc-restore'] = 6464 } @@ -269,6 +271,7 @@ try { seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; + m68040HandlerPrefetchScope=@{logicalCases=24576; batches=2; routes=@('accurate scalar','accurate batch'); bus='Physical map with no host code reader'; qualified='Denied cold boundaries perform no CPU fetch/state change; instruction faults after executed handler prefix preserve prefix and stack precise PC/SR'; entryDiscoveryCases=393216; entryDiscovery='Separate complete 040 gate executes the four-long entry window and currently fails it; these are not passing ordinary cases'; remaining='Buffered handler-entry prefetch/fatal protocol, host-reader/compiled fetch provenance, enabled-cache deferral and physical timing'}; m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, and executed repair/retry after committed supervisor throwaways; chained odd-PC saved-SR provenance, user-tail/internal-restoration faults, CP context transfer, other real fault entry and physical timing remain unqualified'; m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='User-tail validation, internal restoration, untouched incoming-trace retry and other real access faults'}; m68040InstructionFaultScope=@{logicalCases=116736; batches=2; translationEnabled=$false; instructionCacheEnabled=$false; engine='accurate public factory plus cached self-branch batch'; forms=@('opcode','extension-low','next-opcode','self-branch'); banks=@('user','ISP','MSP'); ccr='all 32'; trace='All defined incoming states for frame/return; restart trace zero'; fields='Format7 SR, executing PC distinct from prefetch FA, RW/SIZE/TM and invalid writebacks'; phases=@('fault entry','handler return','restart','following'); remaining='Data writebacks/restart, speculative prefetch deferral, compiled instruction-fault PC provenance, cache-enabled fetches and physical timing'}; From dc27536ad4b79d4b5b1ae284c8df43255bda47c3 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 23:14:25 +0300 Subject: [PATCH 048/211] Prefetch and retain 68040 access-error handler entry --- Copper68k.Tests/Synthetic/README.md | 37 +++- .../SyntheticM68040HandlerPrefetchTests.cs | 173 +++++++++++++++--- Copper68k/M68040Support.cs | 21 +++ Copper68k/M68kTimingEngine.cs | 56 +++++- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 123 +++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 37 ++++ scripts/test-copper68k-040-access-frames.ps1 | 31 +++- .../test-copper68k-synthetic-mutations.ps1 | 36 +++- scripts/test-copper68k-synthetic.ps1 | 8 +- 9 files changed, 482 insertions(+), 40 deletions(-) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 42c9ad21..efda5d16 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -496,12 +496,33 @@ requires a physical host code reader for speculative hot-block construction; the logical bus's real-fetch fallback cannot run before an instruction boundary. `-Scope HandlerPrefetch` restores that defect and requires intended denial proof. -The two entry-prefetch reports are enabled only by the complete 040 discovery -command. They reject each byte of the required four-long window and currently -fail all 393,216 cases: deferred demand fetch does not perform architectural -handler-entry prefetch. The dedicated command requires 36 tests (30 reports, -six fixed examples), twelve fixture identities and all four independent new -combination matrices. It preserves the earlier 480 untested requirements. -These entry mismatches are not a passing ordinary-CI qualification. See the +At historical checkpoint `92922a7`, the two entry-prefetch reports were enabled +only by the complete 040 discovery command. They rejected each byte of the +four-long window and failed all 393,216 cases: deferred demand fetch did not +perform architectural handler-entry prefetch. That command required 36 tests +(30 reports, six fixed examples), twelve fixture identities and four independent +new combination matrices, preserving 480 untested requirements. Those results +remain failed historical discovery, not current ordinary qualification. See the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-host-reader-boundary-and-handler-prefetch-discovery--2026-10-05) -for authorities, evidence and the required follow-up correction. +for authorities and historical evidence. + +### Current 040 access-error handler entry + +The 040 access-error handler-entry window is now an ordinary eight-batch gate +(457,728 cases): four aligned longwords are acquired before handler execution, +entry rejections and odd handler PCs halt, retained words survive memory changes, +and selected branch/host/task/map transitions invalidate stale entry data. The +later-handler fault is at handler+16, beyond the retained entry window. Coverage +uses cache/MMU-disabled accurate scalar/batch execution with physical-map faults; +other exception paths, enabled-cache speculation, compiled fetch provenance and +physical timing remain unqualified. Historical failed discovery is preserved. + +```powershell +./scripts/test-copper68k-synthetic-mutations.ps1 -Scope EntryPrefetch -OutputDirectory artifacts/entry-prefetch-mutations +``` + +This mutation command requires both routes and the intended semantic mismatch +for each of five defects. The complete 040 audit still fails its retained +480-case protocol inventory; passing ordinary handler tests do not complete +milestone 6. See the +[reference record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-access-error-handler-entry-window--2026-10-05). diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs index fab2e9bd..b6e6c91e 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs @@ -5,16 +5,14 @@ namespace Copper68k.Tests.Synthetic; // MC68040UM 8.1 figure 8-1 / 7.6.3: handler-entry prefetch belongs // to exception processing; a later fault in executing handler code does not. -// Discovery remains failing until the full required entry window is modeled. public sealed class SyntheticM68040HandlerPrefetchTests(ITestOutputHelper output) { - private const string Enable = "COPPER68K_RUN_040_ACCESS_FRAME_AUDIT"; private const uint Handler = 0x9020; - [EnvironmentFact(Enable, "qualify the complete 040 handler-entry prefetch window"), Trait("Suite", "ReferenceDiscovery")] + [Fact, Trait("Suite", "Synthetic")] public void EveryRequiredScalarEntryPrefetchByteHaltsBeforeHandlerExecution() => Audit("scalar", true); - [EnvironmentFact(Enable, "qualify the complete 040 handler-entry prefetch window"), Trait("Suite", "ReferenceDiscovery")] + [Fact, Trait("Suite", "Synthetic")] public void EveryRequiredBatchEntryPrefetchByteHaltsBeforeHandlerExecution() => Audit("batch", true); [Theory, Trait("Suite", "Synthetic")] @@ -22,18 +20,128 @@ public sealed class SyntheticM68040HandlerPrefetchTests(ITestOutputHelper output [InlineData("batch")] public void FetchFaultAfterHandlerExecutionStartsANewException(string route) => Audit(route, false); - private void Audit(string route, bool entry) + [Theory, Trait("Suite", "Synthetic")] + [InlineData("scalar")] + [InlineData("batch")] + public void OddHandlerAddressHaltsEntryWithoutFetchingOrStackingAgain(string route) => Audit(route, true, odd: true); + + [Theory, Trait("Suite", "Synthetic")] + [InlineData("scalar")] + [InlineData("batch")] + public void EntryFetchDataIsRetainedUntilConsumedOrItsContextChanges(string route) + { + var bus = new StableFaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + var report = new CoverageBatch("68040", $"handler-prefetch-retention-{route}"); + foreach (var mode in new[] { "linear", "branch", "subroutine", "task", "map" }) + foreach (var bank in new[] { "user", "ISP", "MSP" }) + foreach (var offset in new uint[] { 0, 2, 4, 6 }) + foreach (var alignment in new uint[] { 0, 1 }) + foreach (var vbr in new uint[] { 0, 0x10000 }) + for (var ccr = 0; ccr < 32; ccr++) + { + var id = $"68040/access-error/handler-retention/{mode}/route={route}/bank={bank}/handler={offset}/align={alignment}/VBR={vbr:X8}/op=7002/ccr={ccr:X2}"; + try + { + bus.Disarm(); m.Reset(ccr); + for (uint n = 0; n < 16; n += 2) m.InitializePhysical(Handler + n, 0x7401 + n / 2, 2); + if (mode == "branch") m.InitializePhysical(Handler + offset, 0x6002, 2); + m.InitializePhysical(Handler + 16, 0x7455, 2); + m.InitializePhysical(vbr + 8, Handler + offset, 4); + _ = SyntheticExecution.Prepare(m, [0x7002]); + var s = m.Core.State; + s.SetUserStackPointer(0x7800 + alignment); s.SetInterruptStackPointer(0x4700 + alignment); + s.SetMasterStackPointer(0x7400 + alignment); + s.StatusRegister = (ushort)((bank == "MSP" ? 0x3000 : bank == "ISP" ? 0x2000 : 0) | ccr); + s.VectorBaseRegister = vbr; + bus.Arm(0x1000, M68kBusAccessKind.CpuInstructionFetch); + Execute(m.Core, route); + if (s.Halted || s.ProgramCounter != Handler + offset) + throw new InvalidOperationException($"Retention entry prerequisite differs: PC={s.ProgramCounter:X8}, halted={s.Halted}, rejected={bus.Rejected.Count}"); + RequireCompleteWindow(bus); + // Deliberate guest-visible code replacement proves the CPU + // retains fetched values, rather than re-reading the window. + for (uint n = 0; n < 16; n += 2) m.InitializePhysical(Handler + n, 0x747f, 2); + var e = ArchitecturalExpectation.Capture(m); + var accesses = bus.Accesses.Count; + if (mode == "linear") + { + for (var n = offset; n < 16; n += 2) + { + e.Pc = Handler + n + 2; e.D[2] = 1 + n / 2; + e.Sr = (ushort)(s.StatusRegister & ~15); + Execute(m.Core, route); + if (e.Verify(m) is { } error) throw new InvalidOperationException("Retained entry value differs: " + error); + } + if (bus.Accesses.Count != accesses) + throw new InvalidOperationException("Consumed entry window was fetched twice"); + e.Pc = Handler + 18; e.D[2] = 0x55; + } + else if (mode == "branch") + { + Execute(m.Core, route); + if (s.ProgramCounter != Handler + offset + 4 || bus.Accesses.Count != accesses) + throw new InvalidOperationException("Retained branch prerequisite differs"); + e.Pc = Handler + offset + 6; e.D[2] = 127; e.Sr = (ushort)(e.Sr & ~15); + } + else + { + if (mode == "subroutine") m.Core.BeginSubroutine(Handler + offset, s.A[7], 0x6000); + if (mode == "task") + { + var task = new M68kCpuState(); task.CopyTaskContextFrom(s); + m.Core.SwitchTaskContext(task); + } + if (mode == "map") bus.ChangeMap(); + e = ArchitecturalExpectation.Capture(m); + e.Pc = Handler + offset + 2; e.D[2] = 127; e.Sr = (ushort)(e.Sr & ~15); + } + Execute(m.Core, route); + if (e.Verify(m) is { } mismatch) throw new InvalidOperationException("Entry context invalidation differs: " + mismatch); + report.Record(id, "passing", null); + } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + report.Complete(output); + } + + private static void RequireCompleteWindow(SyntheticM68040AccessDoubleFaultTests.FaultBus bus) + { + var fetches = bus.Accesses.Where(a => a.Kind == M68kBusAccessKind.CpuInstructionFetch && + unchecked(a.Address - Handler) < 16).ToArray(); + if (fetches.Length != 4 || fetches.Any(a => a.Write || a.Width != 4) || + !fetches.Select(a => a.Address).SequenceEqual(new uint[] { Handler, Handler + 4, Handler + 8, Handler + 12 })) + throw new InvalidOperationException("Entry did not fetch exactly four aligned longs before execution: " + + string.Join(",", fetches.Select(a => $"{a.Address:X8}:{a.Width}"))); + } + + private sealed class StableFaultBus : SyntheticM68040AccessDoubleFaultTests.FaultBus, IM68kStablePhysicalAddressMap + { + public uint CpuPhysicalAddressMapGeneration { get; private set; } + public void ChangeMap() => CpuPhysicalAddressMapGeneration++; + public new void Disarm() { base.Disarm(); ChangeMap(); } + public new void Arm(uint at, M68kBusAccessKind kind) { base.Arm(at, kind); ChangeMap(); } + public new bool IsCpuPhysicalAddressMapped(uint address, int count, M68kBusAccessKind kind) + { + if (count > 4) return false; // Page probes are queries, not CPU accesses. + var mapped = base.IsCpuPhysicalAddressMapped(address, count, kind); + if (!mapped) ChangeMap(); // The one-shot hole has just disappeared. + return mapped; + } + } + + private void Audit(string route, bool entry, bool odd = false) { var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); - var report = new CoverageBatch("68040", $"handler-prefetch-{(entry ? "entry" : "executing")}-{route}"); + var report = new CoverageBatch("68040", $"handler-prefetch-{(odd ? "odd" : entry ? "entry" : "executing")}-{route}"); foreach (var form in new[] { "opcode", "extension", "RTE" }) foreach (var bank in form == "RTE" ? new[] { "ISP", "MSP" } : ["user", "ISP", "MSP"]) foreach (var trace in new ushort[] { 0, 0x8000, 0x4000 }) foreach (var alignment in new uint[] { 0, 1 }) foreach (var vbr in new uint[] { 0, 0x10000 }) - foreach (var handlerOffset in entry ? new uint[] { 0, 2, 4, 6 } : [0]) - for (var faultByte = 0; faultByte < (entry ? 16 : 4); faultByte++) + foreach (var handlerOffset in odd ? new uint[] { 1, 3, 5, 7 } : entry ? [0, 2, 4, 6] : [0]) + for (var faultByte = odd ? -1 : 0; faultByte < (odd ? 0 : entry ? 16 : 4); faultByte++) for (var ccr = 0; ccr < 32; ccr++) { var id = $"68040/access-error/handler-prefetch/{form}/route={route}/bank={bank}/T={trace:X4}/align={alignment}/VBR={vbr:X8}/handler={handlerOffset}/byte={faultByte}/op={(form == "RTE" ? 0x4e73 : form == "extension" ? 0x203c : 0x7002):X4}/ccr={ccr:X2}"; @@ -55,8 +163,8 @@ private static void Run(SyntheticMachine m, SyntheticM68040AccessDoubleFaultTest for (var n = -140; n < 100; n++) m.InitializePhysical(unchecked(at + (uint)n), (uint)(n ^ 0x5a), 1); m.InitializePhysical(stack, 0x201f, 2); m.InitializePhysical(stack + 2, 0x6000, 4); m.InitializePhysical(stack + 6, 8, 2); - for (uint n = 0; n < 24; n += 2) m.InitializePhysical(Handler + n, 0x4e71, 2); - m.InitializePhysical(Handler + 4, entry ? 0x4e71u : 0x7002u, 2); + for (uint n = 0; n < 32; n += 2) m.InitializePhysical(Handler + n, 0x4e71, 2); + m.InitializePhysical(Handler + 16, entry ? 0x4e71u : 0x7002u, 2); m.InitializePhysical(Handler + handlerOffset, 0x7201, 2); // MOVEQ #1,D1 m.InitializePhysical(vbr + 8, Handler + handlerOffset, 4); _ = SyntheticExecution.Prepare(m, form == "RTE" ? [0x4e73] : form == "extension" ? [0x203c, 0x1234, 0x5678] : [0x7002]); @@ -66,7 +174,7 @@ private static void Run(SyntheticMachine m, SyntheticM68040AccessDoubleFaultTest var before = ArchitecturalExpectation.Capture(m); var sequence = s.ExceptionSequence; var firstAddress = form == "RTE" ? stack : form == "extension" ? 0x1004u : 0x1000u; bus.Arm(firstAddress, form == "RTE" ? M68kBusAccessKind.CpuDataRead : M68kBusAccessKind.CpuInstructionFetch); - if (entry) bus.Arm(Handler + (uint)faultByte, M68kBusAccessKind.CpuInstructionFetch); + if (entry && faultByte >= 0) bus.Arm(Handler + (uint)faultByte, M68kBusAccessKind.CpuInstructionFetch); if (route == "batch") { var denied = new DeniedBoundary(); var accesses = bus.Accesses.Count; @@ -80,14 +188,34 @@ private static void Run(SyntheticMachine m, SyntheticM68040AccessDoubleFaultTest throw new InvalidOperationException("Original access-error entry prerequisite differs"); if (entry) { - if (!s.Halted || s.Stopped || bus.Rejected.Count != 2) + var odd = faultByte < 0; + if (!s.Halted || s.Stopped || bus.Rejected.Count != (odd ? 1 : 2)) throw new InvalidOperationException($"Handler-entry prefetch must halt before execution: halted={s.Halted}, rejections={bus.Rejected.Count}"); - var rejected = bus.Rejected[1]; - if (rejected.Kind != M68kBusAccessKind.CpuInstructionFetch || rejected.Width != 4 || - rejected.Address != (Handler + (uint)faultByte & ~3u) || bus.Accesses.Count != rejected.AccessCount) - throw new InvalidOperationException("Wrong entry prefetch or transfer after fatal rejection"); + if (odd) + { + if (bus.Accesses.Any(a => a.Kind == M68kBusAccessKind.CpuInstructionFetch && unchecked(a.Address - Handler) < 16)) + throw new InvalidOperationException("Odd entry performed a handler fetch"); + } + else + { + var rejected = bus.Rejected[1]; + if (rejected.Kind != M68kBusAccessKind.CpuInstructionFetch || rejected.Width != 4 || + rejected.Address != (Handler + (uint)faultByte & ~3u) || bus.Accesses.Count != rejected.AccessCount) + throw new InvalidOperationException("Wrong entry prefetch or transfer after fatal rejection"); + } if (!s.D.SequenceEqual(before.D) || !s.A.Take(7).SequenceEqual(before.A.Take(7))) throw new InvalidOperationException("Handler instruction or unrelated register changed during entry"); + before.Sr = (ushort)((sr | 0x2000) & ~0xc000); before.A[7] = frame; + before.InactiveStackPointer = 0x7800 + alignment; + if (bank == "MSP") before.MasterStackPointer = frame; + before.Pc = Handler + handlerOffset; before.Halted = true; before.ExceptionVector = 2; + before.ControlChecks["saved instruction PC"] = (x => x.LastExceptionStackedProgramCounter, 0x1000); + before.ControlChecks["saved SR"] = (x => x.LastExceptionStatusRegister, sr); + before.ControlChecks["single exception"] = (x => x.ExceptionSequence, sequence + 1); + before.ControlChecks["translation bypass restored"] = (x => x.M68040Mmu.BypassTranslation ? 1u : 0u, 0); + SyntheticM68040RteValidationFaultTests.ExpectAccessFrame(m, before, frame, sr, 0x1000, firstAddress, form == "RTE" ? 2 : 4); + if (form != "RTE") before.Write(frame + 12, bank == "user" ? 0x0102u : 0x0106u, 2, m.Model); + if (before.Verify(m) is { } frameError) throw new InvalidOperationException("Fatal entry frame or untouched memory differs: " + frameError); var frozen = ArchitecturalExpectation.Capture(m); frozen.Halted = true; var accesses = bus.Accesses.Count; m.Core.RequestInterrupt(7, 31 * 4); m.Core.BeginSubroutine(0x6000, 0x8000, 0x7000); @@ -103,22 +231,25 @@ private static void Run(SyntheticMachine m, SyntheticM68040AccessDoubleFaultTest } if (s.Halted || s.ProgramCounter != Handler || s.A[7] != frame) throw new InvalidOperationException("Access-error entry did not reach the handler"); + RequireCompleteWindow(bus); if (!s.D.SequenceEqual(before.D) || !s.A.Take(7).SequenceEqual(before.A.Take(7))) throw new InvalidOperationException("Faulted instruction or handler prefix executed during entry"); - Execute(m.Core, route); Execute(m.Core, route); // MOVEQ; NOP before the next long. + // These eight words were fetched during entry. A later demand fetch + // is outside that window and belongs to handler execution. + for (var n = 0; n < 8; n++) Execute(m.Core, route); var handlerSr = (ushort)(((sr | 0x2000) & ~0xc000 & ~15) | 0); - if (s.ProgramCounter != Handler + 4 || s.D[1] != 1 || s.StatusRegister != handlerSr || s.A[7] != frame) + if (s.ProgramCounter != Handler + 16 || s.D[1] != 1 || s.StatusRegister != handlerSr || s.A[7] != frame) throw new InvalidOperationException("Executed handler prefix prerequisite differs"); var e = ArchitecturalExpectation.Capture(m); var nested = frame - 60; e.A[7] = nested; if (bank == "MSP") e.MasterStackPointer = nested; e.Pc = Handler; e.ExceptionVector = 2; e.ControlChecks["nested exception"] = (x => x.ExceptionSequence, sequence + 2); - e.ControlChecks["handler PC saved"] = (x => x.LastExceptionStackedProgramCounter, Handler + 4); + e.ControlChecks["handler PC saved"] = (x => x.LastExceptionStackedProgramCounter, Handler + 16); e.ControlChecks["handler SR saved"] = (x => x.LastExceptionStatusRegister, handlerSr); e.ControlChecks["exactly two rejections"] = (_ => (uint)bus.Rejected.Count, 2); - SyntheticM68040RteValidationFaultTests.ExpectAccessFrame(m, e, nested, handlerSr, Handler + 4, Handler + 4, 4); + SyntheticM68040RteValidationFaultTests.ExpectAccessFrame(m, e, nested, handlerSr, Handler + 16, Handler + 16, 4); e.Write(nested + 12, 0x0106, 2, m.Model); // Instruction TM, not data TM. - bus.Arm(Handler + 4 + (uint)faultByte, M68kBusAccessKind.CpuInstructionFetch); + bus.Arm(Handler + 16 + (uint)faultByte, M68kBusAccessKind.CpuInstructionFetch); Execute(m.Core, route); var error = e.Verify(m); if (error != null) throw new InvalidOperationException(error); diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index 47c2f966..e17a1cb8 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -2584,6 +2584,7 @@ public override void Reset(uint programCounter, uint stackPointer) public override void BeginSubroutine(uint address, uint stackPointer, uint returnAddress) { if (_accessErrorDoubleFaultHalted) return; + _timedBus.ResetInstructionFetchBuffer(); base.BeginSubroutine(address, stackPointer, returnAddress); } @@ -2592,6 +2593,8 @@ public void SwitchTaskContext(M68kCpuState next) ArgumentNullException.ThrowIfNull(next); if (_accessErrorDoubleFaultHalted) return; State.CopyTaskContextFrom(next); + _timedBus.ResetInstructionFetchBuffer(); + DiscardInstructionPrefetch(); } public override void RequestInterrupt(int level, uint vectorAddress) @@ -4311,6 +4314,24 @@ private void RaiseUnbufferedReadAccessFault(M68040MmuFault fault, uint instructi PushLong(instructionPc); PushWord(savedSr); State.ProgramCounter = ReadLong(State.VectorBaseRegister + 8); + DiscardInstructionPrefetch(); + if ((State.ProgramCounter & 1) != 0) + { + // An odd handler PC is an address error during access-error + // entry, not an instruction which can create a second frame. + LatchAccessErrorDoubleFault(); + } + else + { + if (_timedBus.PrefetchM68040AccessErrorHandler(State.ProgramCounter)) + { + // The entry window was fetched using the current host map. + // A map change while rejecting the original access must not + // invalidate that newly acquired window on the next step. + _observedPhysicalAddressMapGeneration = + _stablePhysicalAddressMap?.CpuPhysicalAddressMapGeneration ?? 0; + } + } CompleteTiming(M68kInstructionTimingKey.IllegalInstruction); } diff --git a/Copper68k/M68kTimingEngine.cs b/Copper68k/M68kTimingEngine.cs index 60f743b5..f054a2d1 100644 --- a/Copper68k/M68kTimingEngine.cs +++ b/Copper68k/M68kTimingEngine.cs @@ -1612,6 +1612,13 @@ internal sealed class M68kTimedBusAdapter private uint _fetchMmuGeneration; private uint _fetchMapGeneration; private bool _fetchSupervisor; + private uint _exceptionFetchBase; + private uint _exceptionFetchNext; + private uint _exceptionFetchFirst; + private uint _exceptionFetchSecond; + private uint _exceptionFetchThird; + private uint _exceptionFetchFourth; + private bool _exceptionFetchValid; public M68kTimedBusAdapter( IM68kBus bus, @@ -1688,7 +1695,32 @@ internal ushort ReadInstructionFetchWordHot( return value; } - internal void ResetInstructionFetchBuffer() => _fetchValidLongs = 0; + internal void ResetInstructionFetchBuffer() + { + _fetchValidLongs = 0; + _exceptionFetchValid = false; + } + + internal bool PrefetchM68040AccessErrorHandler(uint address) + { + ResetInstructionFetchBuffer(); + if (!_useM68040UncachedHalfLine || _timing.InstructionCache.Enabled || _state.M68040Mmu.Enabled) + return false; + // MC68040UM 8.1, figure 8-1: four longwords precede handler + // execution. Table 7-3 starts uncached fetches at a half-line. + // Publish only a complete window; a failed entry remains fatal. + _exceptionFetchBase = address & ~7u; + _exceptionFetchFirst = ReadM68040InstructionLong(_exceptionFetchBase, out _); + _exceptionFetchSecond = ReadM68040InstructionLong(unchecked(_exceptionFetchBase + 4), out _); + _exceptionFetchThird = ReadM68040InstructionLong(unchecked(_exceptionFetchBase + 8), out _); + _exceptionFetchFourth = ReadM68040InstructionLong(unchecked(_exceptionFetchBase + 12), out _); + _exceptionFetchNext = address; + _fetchSupervisor = (_state.StatusRegister & M68kCpuState.Supervisor) != 0; + _fetchMmuGeneration = _state.M68040Mmu.Generation; + _fetchMapGeneration = _physicalAddressMap?.CpuPhysicalAddressMapGeneration ?? 0; + _exceptionFetchValid = true; + return true; + } private ushort ReadM68040UncachedInstructionWord( uint address, @@ -1703,6 +1735,28 @@ private ushort ReadM68040UncachedInstructionWord( var supervisor = (_state.StatusRegister & M68kCpuState.Supervisor) != 0; var mmuGeneration = _state.M68040Mmu.Generation; var mapGeneration = _physicalAddressMap?.CpuPhysicalAddressMapGeneration ?? 0; + if (_exceptionFetchValid) + { + var offset = unchecked(address - _exceptionFetchBase); + if (address == _exceptionFetchNext && offset < 16 && + _fetchSupervisor == supervisor && _fetchMmuGeneration == mmuGeneration && + _fetchMapGeneration == mapGeneration) + { + var retained = (offset >> 2) switch { + 0 => _exceptionFetchFirst, 1 => _exceptionFetchSecond, + 2 => _exceptionFetchThird, _ => _exceptionFetchFourth + }; + _exceptionFetchNext = unchecked(address + 2); + if (offset == 14) _exceptionFetchValid = false; + cacheHit = false; + requiresSynchronization = false; + completedMachineCycle = _state.Cycles; + return (ushort)(retained >> ((address & 2) == 0 ? 16 : 0)); + } + // A changed flow/context must not turn the entry window into + // an instruction cache, including a branch back into it. + ResetInstructionFetchBuffer(); + } if (_fetchHalfLineAddress != halfLine || _fetchSupervisor != supervisor || _fetchMmuGeneration != mmuGeneration || _fetchMapGeneration != mapGeneration) { diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 800d4436..9de68a50 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -4358,3 +4358,126 @@ after execution starts. Suppressing the discovery test, fetching only a safe first word or retrying an instruction after operand side effects is insufficient. All earlier model, restart, writeback, reference and consolidation requirements remain open. Milestone 6 remains **in progress**, `roadmapComplete=false`. +## 040 access-error handler entry window — 2026-10-05 + +This checkpoint addresses the preceding handler-entry discovery rather than +discarding its failed evidence. The authoritative scope is cache-disabled, +MMU-disabled accurate 040 scalar and batch execution after instruction-fetch +faults and pre-commit supervisor RTE validation faults. Other entry routes, +enabled caches/MMU, compiled fetch provenance and physical pipeline timing remain +unqualified. + +The documentary reading was checked against the image of MC68040UM figure 8-1 +(printed 8-3), not just extracted text: four longwords precede instruction +execution, and an entry bus/address error leads to HALT. Section 8.2.1 also +distinguishes errors during access-error processing from later handler execution. +This is the exception-entry interpretation; it does not override the ordinary +speculative-fetch deferral rules in 7.6.1/8.2.1. Suppressed unused prefetches and +enabled-cache line fills still require separate qualification. + +An independently archived Motorola 1993 manual was inspected at PDF page 224 +(one based): [scan](https://ftpmirror.your.org/pub/misc/bitsavers/components/motorola/68000/68040/MC68040_Users_Manual_1993.pdf), +17,233,053 bytes, SHA-256 +`93741393f70656941e413beb232c060a5e4e0218ea2adc2f5b392f9165454a7f`. +The [NXP reference](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +remains the primary citation. The scan is documentary evidence, not hardware or +an executed reference CPU. Neither manual nor ROM/media files are committed. + +The timed bus now acquires and retains four aligned longwords beginning at the +uncached half-line boundary. Entry is published only after all four transfers +succeed. An entry rejection halts before handler execution; an odd handler +address halts without another fetch or frame. Original format-7 fields, +registers, neighboring memory and translation-bypass restoration are checked. +HALT remains sticky across interrupts and host entry until external reset. +Sequential consumption uses retained data; selected branch, subroutine, task +and host-map changes discard stale data. Host map changes during a rejected +access cannot discard newly fetched entry data on the next scalar step. +Existing timing-plan keys are preserved; newly required bus accesses do not +establish physical timing accuracy. + +The later-handler fault fixture now executes eight retained words before +rejecting the demand fetch at handler+16. Its previous handler+4 fault would +target data already acquired during entry and could not prove later demand +fault delivery. Historical report counts and failures remain unchanged in the +preceding record; fresh results describe the revised fixture explicitly. + +| Group, each scalar/batch | Cases | Combinations | +| --- | ---: | ---: | +| Entry rejection | 196,608 | 6,144 | +| Later handler demand fault | 12,288 | 384 | +| Entry retention/context change | 7,680 | 240 | +| Odd handler address | 12,288 | 384 | + +All eight groups are ordinary CI requirements: **457,728 cases**. The two +formerly optional entry discovery tests are promoted; their existing fault-byte +matrix remains intact. Retention fixtures replace handler code after entry to +prove that fetched values are retained rather than read again. The stable-map +fixture rejects page probes without consuming one-shot CPU faults, advances its +generation on actual map changes, and records real accesses separately. + +Before host-entry/map invalidation was corrected, the final retention fixture in +`artifacts/m6-entry-retention-before-v3/` reports 4,608 passing and 3,072 +mismatching cases per route. Intermediate fixture-development outputs are not +qualification evidence. Five maintained `EntryPrefetch` mutations separately +remove the entry window, its fourth long, retained data, subroutine invalidation +and task invalidation. Each requires complete scalar/batch selections and its +specific mismatch, not merely a failing process. + +The complete 040 gate requires **40 executed tests, 34 reporting batches, six +fixed examples and twelve input identities**. Its independent iterator checks +all eight groups' exact combinations. The preserved 480-case protocol inventory +still fails completion; this checkpoint does not remove it. No old regression is +retired. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +Fresh full Release CPU validation in `artifacts/m6-entry-window-full/` passes +**5,065 tests with eleven optional skips and zero failures**. The required +inventory discovery is executed separately; its ordinary skip is not successful +coverage. All nine qualified WinUAE presets match their exact directory, +callback and exception-frame counts and current CPU/adapter identities in +`qualified-preset-identities.json`. Their generator/runner pins, original Basic +failures and documented reference caveats remain unchanged; this does not +promote the original Basic selection. + +The fresh strict report gate checks **18,048,534 deterministic cases in 624 +reporting batches** across all eight profiles. Fresh pinned SingleStepTests +passes **312,500 cases in 125 files**, source +`64b253116a3de04aaac4346c43680960dc9b67e5`. Fresh Musashi passes **536 programs +with 88 explicit exclusions**, source +`72c1d74800f3087b45a0c1a7342601bbed898881`. These independent software audits +retain their original architectural and timing limits. + +The complete 040 run in `artifacts/m6-entry-window-audit/` executes 40 tests: +39 pass and the retained inventory test fails. All **3,079,872 executed cases +pass**, with zero mismatches/unsupported execution and **480 untested** cases. +Source, assembly, input selection and independently enumerated combinations +validate before the gate rejects the inventory. All **33** report/identity +controls in `artifacts/m6-entry-window-controls/` detect their specific missing, +shortened, foreign, redistributed or omitted-fixture corruption. + +All five new mutations are detected in `artifacts/m6-entry-window-mutations/`: +per route, omitted entry mismatches 196,608 cases, omitted fourth long 49,152, +discarded data 3,072, stale subroutine context 1,536 and stale task context +1,536. The refreshed host-reader mutation in +`artifacts/m6-entry-window-host-mutation/` passes all 12,288 scalar cases and +mismatches all 12,288 batch cases at the denied boundary. Mutations restore +source bytes and rebuild successfully; no mutated binary is used for acceptance. + +Verified CPU SHA-256 is +`8b0a29e30ecafa1a0acdd2bb3c9e53e9026c9bff5ed61f899edd90ab2ce01131`; +test/reference adapter is +`9452ec20f94b35e0ef081adb050dca67223e6a261226f548aee72ad270f7c970`. +The manifest identifies starting commit `92922a7` and separately hashes the +working source actually built; its starting committed CPU tree is not relabeled +as the correction's tree. + +Isolated CopperScreen baseline +`d9beae8b88be24032221e3482942a249c03c27d3` consumes unpublished private +`1.5.2-synthetic-dev.63`, package SHA-256 +`68d5e7f3158c682df451e3c156a4387bbb6a87e0a65c6589ccac2d7c4c260133`. +Release builds with zero warnings/errors; host tests pass 149 with six optional +skips, disk tests pass 74, separate engine diagnostics pass 1,080, and all three +native Workbench/A1200 boot/persistence replays pass without native skips. +Embedded CPU, four package assets and four loaded assemblies match the audited +CPU. Evidence is in the isolated consumer's `artifacts/entry-window-validation/` +and `entry-window-identities.json`. Root CopperScreen changes and its 1.5.1 pin +remain untouched. No package is published and no throughput claim is made. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 0c2bcd1d..29ac31a0 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1628,3 +1628,40 @@ and all three native replays with matching package/assets/CPU binaries. The complete 040 audit intentionally fails: 2,646,720 passing cases, 393,216 handler-entry mismatches and 480 untested requirements. Ordinary skips and software references do not imply completion of that required discovery scope. +### Milestone 6 access-error handler entry window — 2026-10-05 + +The accurate, cache/MMU-disabled 040 instruction-fault and supervisor RTE +validation paths now acquire and retain the four-long handler-entry window. +Entry bus faults and odd handler addresses halt before instruction execution; +later demand faults preserve the executed prefix and start another format-7 +exception. Retention and selected flow/host/task/map invalidation are tested +independently of production fetch calculations. The former handler+4 demand +fixture moves beyond the retained window to handler+16. + +The complete 393,216-case entry-byte matrix is promoted into ordinary CI. +Retention/context and odd-address groups add 39,936 cases; all eight handler +groups total 457,728 cases. Five maintained mutations target missing entry, +missing fourth long, discarded data and stale subroutine/task contexts. The +complete 040 audit requires 40 tests, 34 reports, six fixed examples and twelve +input identities, with independently enumerated combinations. + +Historical failed discovery and intermediate fixture-development results remain +separate from fresh qualification. Other exception entry routes, enabled-cache +deferral, compiled fetch provenance, data writeback/restart and every earlier +model/reference/consolidation requirement remain required. The existing +480-case untested inventory is retained and continues to fail the complete +gate. No regression is retired or public API/package released. Milestone 6 +remains **in progress**, `roadmapComplete=false`. See the +[qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040-access-error-handler-entry-window--2026-10-05). + +Fresh validation passes 5,065 Release CPU tests with eleven optional skips, +all nine pinned WinUAE presets, and the strict 18,048,534-case / 624-batch gate. +Fresh SingleStepTests passes 312,500 cases in 125 files; Musashi passes 536 +programs with 88 exclusions. All five entry mutations, the refreshed host-reader +mutation and 33 report/identity controls detect their intended failures. +The complete 040 audit has 3,079,872 passing cases, zero mismatches/unsupported +execution and the retained 480 untested requirements; its gate remains failed. +Isolated CopperScreen through unpublished private `.63` passes Release build, +host/disk suites, 1,080 separate engine diagnostics and all three native replays, +with matching package/assets/CPU binaries. Evidence and limits are recorded in +the qualification record; this checkpoint does not complete milestone 6. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index f8b4f2b3..83df3dcd 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -37,6 +37,10 @@ $expected = [ordered]@{ 'handler-prefetch-entry-batch' = @{cases=196608; combinations=6144} 'handler-prefetch-executing-scalar' = @{cases=12288; combinations=384} 'handler-prefetch-executing-batch' = @{cases=12288; combinations=384} + 'handler-prefetch-retention-scalar' = @{cases=7680; combinations=240} + 'handler-prefetch-retention-batch' = @{cases=7680; combinations=240} + 'handler-prefetch-odd-scalar' = @{cases=12288; combinations=384} + 'handler-prefetch-odd-batch' = @{cases=12288; combinations=384} 'rte-access-remaining-protocols' = @{cases=480; combinations=15} } $testExit = 0 @@ -57,7 +61,7 @@ if (-not $ValidateReportsOnly) { } $identity = [ordered]@{ schema=1; reference='MC68040UM'; url='https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf' - sections=@('7.6.3','8.2.1','8.2.2','8.2.6','8.4','8.4.1','8.4.2','8.4.3','8.4.4','8.4.6.2','8.4.6.7'); softwareReferenceExecuted=$false + sections=@('7.6.1','7.6.3','8.1 figure 8-1','8.2.1','8.2.2','8.2.6','8.4','8.4.1','8.4.2','8.4.3','8.4.4','8.4.6.2','8.4.6.7'); softwareReferenceExecuted=$false sourceCommit=(& git -C $repo rev-parse HEAD); cpuCommittedTree=(& git -C $repo rev-parse HEAD:Copper68k) cpuSourceFiles=@(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} @@ -74,7 +78,7 @@ if (-not $ValidateReportsOnly) { 'Selected active accurate-batch paths verify counts/callbacks and scalar/batch bus/cycle policy; generic short operand frames do not qualify architectural format-7 data restart', 'Instruction-fault fixtures use cache-disabled accurate execution and physical-map rejection; speculative deferral, enabled caches/MMU and compiled fetch PC provenance remain unqualified', 'Executed supervisor-tail frame repair explicitly clears saved incoming trace; untouched incoming-trace retry and user-tail repair are not qualified', - 'User-tail validation, internal-restoration double faults, handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', + 'User-tail validation, internal-restoration double faults, cache/MMU/compiled handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', 'Direct odd-RTE saved-SR ordering uses documentary WinUAE 5d22d336, not an executed hardware oracle') } $identity | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'identities.json') @@ -102,8 +106,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 36 -or [int]$counters.total -ne 36 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (30 batches, 6 fixed examples)' +if ([int]$counters.executed -ne 40 -or [int]$counters.total -ne 40 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (34 batches, 6 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -126,14 +130,25 @@ foreach ($group in $expected.Keys) { $expectedCombinations["68040/RTE/format7/$form/bank=$bank"] = 32 } } + } elseif ($group.StartsWith('handler-prefetch-retention-', [StringComparison]::Ordinal)) { + $route = if ($group.EndsWith('-scalar', [StringComparison]::Ordinal)) { 'scalar' } else { 'batch' } + foreach ($mode in @('linear','branch','subroutine','task','map')) { + foreach ($bank in @('user','ISP','MSP')) { foreach ($offset in @(0,2,4,6)) { + foreach ($alignment in @(0,1)) { foreach ($vbr in @(0,0x10000)) { + $key = '68040/access-error/handler-retention/{0}/route={1}/bank={2}/handler={3}/align={4}/VBR={5:X8}' -f $mode,$route,$bank,$offset,$alignment,$vbr + $expectedCombinations[$key] = 32 + } } + } } + } } elseif ($group.StartsWith('handler-prefetch-', [StringComparison]::Ordinal)) { $entry = $group.StartsWith('handler-prefetch-entry-', [StringComparison]::Ordinal) + $odd = $group.StartsWith('handler-prefetch-odd-', [StringComparison]::Ordinal) $route = if ($group.EndsWith('-scalar', [StringComparison]::Ordinal)) { 'scalar' } else { 'batch' } foreach ($form in @('opcode','extension','RTE')) { foreach ($bank in $(if ($form -eq 'RTE') { @('ISP','MSP') } else { @('user','ISP','MSP') })) { foreach ($trace in @(0,0x8000,0x4000)) { foreach ($alignment in @(0,1)) { foreach ($vbr in @(0,0x10000)) { - foreach ($offset in $(if ($entry) { @(0,2,4,6) } else { @(0) })) { - foreach ($byte in 0..$(if ($entry) {15} else {3})) { + foreach ($offset in $(if ($odd) { @(1,3,5,7) } elseif ($entry) { @(0,2,4,6) } else { @(0) })) { + foreach ($byte in $(if ($odd) { @(-1) } elseif ($entry) { 0..15 } else { 0..3 })) { $key = '68040/access-error/handler-prefetch/{0}/route={1}/bank={2}/T={3:X4}/align={4}/VBR={5:X8}/handler={6}/byte={7}' -f $form,$route,$bank,$trace,$alignment,$vbr,$offset,$byte $expectedCombinations[$key] = 32 } @@ -366,8 +381,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 36 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 40 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=3040416; xunitBatches=30; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=3080352; xunitBatches=34; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index 74d8c30d..db6d8a81 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','InstructionFault','HandlerPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -341,6 +341,26 @@ $mutations += @{ '@; after=' _codeReader = bus as IM68kCodeReader;'; group='FetchFaultAfterHandlerExecutionStartsANewException'; model='68040'; milestone=6; handlerPrefetch=$true } +$entryMutations = @( + @{name='040-entry-missing-window'; file='Copper68k/M68040Support.cs'; before=' if (_timedBus.PrefetchM68040AccessErrorHandler(State.ProgramCounter))'; + after=' if (State.Stopped) // Mutant: omit handler entry prefetch.'; group='EveryRequired'; reportPrefix='handler-prefetch-entry-'; cases=196608; proofCombination='/byte=0/'; proofReason='Handler-entry prefetch must halt'}, + @{name='040-entry-final-long'; file='Copper68k/M68kTimingEngine.cs'; before=' _exceptionFetchFourth = ReadM68040InstructionLong(unchecked(_exceptionFetchBase + 12), out _);'; + after=' _exceptionFetchFourth = 0; // Mutant: only three entry longs.'; group='EveryRequired'; reportPrefix='handler-prefetch-entry-'; cases=196608; proofCombination='/byte=12/'; proofReason='Handler-entry prefetch must halt'}, + @{name='040-entry-refetch'; file='Copper68k/M68kTimingEngine.cs'; before=' _exceptionFetchValid = true;'; + after=' _exceptionFetchValid = false; // Mutant: discard acquired entry data.'; group='EntryFetchDataIsRetainedUntilConsumedOrItsContextChanges'; reportPrefix='handler-prefetch-retention-'; cases=7680; proofCombination='/handler-retention/linear/'; proofReason='Retained entry value differs'}, + @{name='040-entry-subroutine-context'; file='Copper68k/M68040Support.cs'; before=@' + _timedBus.ResetInstructionFetchBuffer(); + base.BeginSubroutine(address, stackPointer, returnAddress); +'@; after=' base.BeginSubroutine(address, stackPointer, returnAddress);'; group='EntryFetchDataIsRetainedUntilConsumedOrItsContextChanges'; reportPrefix='handler-prefetch-retention-'; cases=7680; proofCombination='/handler-retention/subroutine/'; proofReason='Entry context invalidation differs'}, + @{name='040-entry-task-context'; file='Copper68k/M68040Support.cs'; before=@' + State.CopyTaskContextFrom(next); + _timedBus.ResetInstructionFetchBuffer(); + DiscardInstructionPrefetch(); +'@; after=' State.CopyTaskContextFrom(next);'; group='EntryFetchDataIsRetainedUntilConsumedOrItsContextChanges'; reportPrefix='handler-prefetch-retention-'; cases=7680; proofCombination='/handler-retention/task/'; proofReason='Entry context invalidation differs'} +) +foreach ($mutation in $entryMutations) { + $mutation.model='68040'; $mutation.milestone=6; $mutation.entryPrefetch=$true; $mutations += $mutation +} if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } if ($Scope -eq 'Logical') { $mutations = @($mutations | Where-Object { $_.milestone -eq 4 }) } @@ -351,6 +371,7 @@ if ($Scope -eq 'RteValidationFault') { $mutations = @($mutations | Where-Object if ($Scope -eq 'RteRepair') { $mutations = @($mutations | Where-Object { $_.repair }) } if ($Scope -eq 'InstructionFault') { $mutations = @($mutations | Where-Object { $_.instructionFault }) } if ($Scope -eq 'HandlerPrefetch') { $mutations = @($mutations | Where-Object { $_.handlerPrefetch }) } +if ($Scope -eq 'EntryPrefetch') { $mutations = @($mutations | Where-Object { $_.entryPrefetch }) } if ($Scope -eq 'AccessDoubleFault') { $mutations = @($mutations | Where-Object { $_.doubleFault }) } if ($Scope -eq 'BatchFault') { $mutations = @($mutations | Where-Object { $_.batchFault }) } if ($Scope -eq 'LowPowerStop') { $mutations = @($mutations | Where-Object { $_.lpstop }) } @@ -423,6 +444,16 @@ try { throw 'Host-reader mutation omitted complete cases or intended boundary failure' } } + if ($mutation.entryPrefetch) { + [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + if ($trx.TestRun.ResultSummary.Counters.executed -ne 2 -or $batches.Count -ne 2 -or + @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq ($mutation.reportPrefix + 'scalar') -and $_.logicalCases -eq $mutation.cases }).Count -ne 1 -or + @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq ($mutation.reportPrefix + 'batch') -and $_.logicalCases -eq $mutation.cases }).Count -ne 1 -or + @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and + $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) }).Count -eq 0) { + throw 'Entry-prefetch mutation omitted complete cases or intended semantic failure' + } + } if ($mutation.instructionFault) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if ($trx.TestRun.ResultSummary.Counters.executed -ne 2 -or $batches.Count -ne 2 -or @@ -492,6 +523,9 @@ try { $proofFailure = if ($mutation.handlerPrefetch) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains('/route=batch/', [StringComparison]::Ordinal) -and $_.reason.StartsWith('Denied cold batch', [StringComparison]::Ordinal) })[0] + } elseif ($mutation.entryPrefetch) { + @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and + $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] } else { $failures[0] } $results += @{mutation=$mutation.name; file=$mutation.file; sourceSha256=(Get-FileHash -LiteralPath $path).Hash; detected=$true; replacementCase=$proofFailure.id; diagnostic=$proofFailure.reason; originalRegressionDetected=$legacyPresent; diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 4235413d..f057db66 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -175,6 +175,12 @@ try { $expected['rte-access-movem-full-index'] = 114048 $expected['handler-prefetch-executing-scalar'] = 12288 $expected['handler-prefetch-executing-batch'] = 12288 + $expected['handler-prefetch-entry-scalar'] = 196608 + $expected['handler-prefetch-entry-batch'] = 196608 + $expected['handler-prefetch-retention-scalar'] = 7680 + $expected['handler-prefetch-retention-batch'] = 7680 + $expected['handler-prefetch-odd-scalar'] = 12288 + $expected['handler-prefetch-odd-batch'] = 12288 $expected['system-mmu-disabled'] = 34850 $expected['system-linef-pc-restore'] = 6464 } @@ -271,7 +277,7 @@ try { seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; - m68040HandlerPrefetchScope=@{logicalCases=24576; batches=2; routes=@('accurate scalar','accurate batch'); bus='Physical map with no host code reader'; qualified='Denied cold boundaries perform no CPU fetch/state change; instruction faults after executed handler prefix preserve prefix and stack precise PC/SR'; entryDiscoveryCases=393216; entryDiscovery='Separate complete 040 gate executes the four-long entry window and currently fails it; these are not passing ordinary cases'; remaining='Buffered handler-entry prefetch/fatal protocol, host-reader/compiled fetch provenance, enabled-cache deferral and physical timing'}; + m68040HandlerPrefetchScope=@{logicalCases=457728; batches=8; routes=@('accurate scalar','accurate batch'); bus='Physical map with no host code reader, including stable-map generation'; qualified='Four-long access-error entry window, every entry byte, odd handler addresses, retained data, flow/host/task/map invalidation and later instruction faults; denied cold boundaries perform no CPU fetch/state change'; remaining='Other exception-entry paths, host-reader/compiled fetch provenance, enabled caches/MMU, ordinary speculative deferral and physical timing'}; m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, and executed repair/retry after committed supervisor throwaways; chained odd-PC saved-SR provenance, user-tail/internal-restoration faults, CP context transfer, other real fault entry and physical timing remain unqualified'; m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='User-tail validation, internal restoration, untouched incoming-trace retry and other real access faults'}; m68040InstructionFaultScope=@{logicalCases=116736; batches=2; translationEnabled=$false; instructionCacheEnabled=$false; engine='accurate public factory plus cached self-branch batch'; forms=@('opcode','extension-low','next-opcode','self-branch'); banks=@('user','ISP','MSP'); ccr='all 32'; trace='All defined incoming states for frame/return; restart trace zero'; fields='Format7 SR, executing PC distinct from prefetch FA, RW/SIZE/TM and invalid writebacks'; phases=@('fault entry','handler return','restart','following'); remaining='Data writebacks/restart, speculative prefetch deferral, compiled instruction-fault PC provenance, cache-enabled fetches and physical timing'}; From 32761ccd6f296e4355c5d63f099d03ccbcdbf84c Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Mon, 5 Oct 2026 23:40:26 +0300 Subject: [PATCH 049/211] Qualify 68040 user-stack RTE faults and software repair --- Copper68k.Tests/Synthetic/README.md | 27 ++ .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- .../SyntheticM68040RteRepairTests.cs | 4 +- .../SyntheticM68040RteValidationFaultTests.cs | 3 +- .../SyntheticM68040UserRteFaultTests.cs | 238 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 106 ++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 42 ++++ scripts/test-copper68k-040-access-frames.ps1 | 61 ++++- .../test-copper68k-synthetic-mutations.ps1 | 43 +++- scripts/test-copper68k-synthetic.ps1 | 15 +- 10 files changed, 521 insertions(+), 20 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index efda5d16..49336476 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -526,3 +526,30 @@ for each of five defects. The complete 040 audit still fails its retained 480-case protocol inventory; passing ordinary handler tests do not complete milestone 6. See the [reference record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-access-error-handler-entry-window--2026-10-05). + +### User-stack RTE validation and software repair + +`SyntheticM68040UserRteFaultTests` adds four ordinary scalar/batch reports with +1,687,040 cases. It combines documented throwaway live-SR behavior with general +supervisor exception entry; the combined USP fault path has no executed hardware +oracle. Both user M values select the expected exception stack, with TM=1 and +preserved user frame/consumed throwaways. Bare handler return is followed by a +privilege fault at RTE. A seven-store software handler repairs the frame, builds +a fresh throwaway bridge and explicitly sets saved S before retry. Canonical +SR reads use all CCRs; structural cases reject every validation read byte. Repair +checks following traces, saved MOVEM EA and pending CT/CU/CP49 delivery. + +```powershell +./scripts/test-copper68k-synthetic-mutations.ps1 -Scope UserRteFault -OutputDirectory artifacts/user-rte-mutations +``` + +The command requires complete scalar/batch fault reports and each mutation's +intended saved-S, TM or stack-selection mismatch. The complete 040 command now +checks 44 tests / 38 reports / six fixed examples / thirteen input identities, +while keeping the failing 480-case remaining-protocol inventory. Internal +restoration, untouched trace retry, chained odd-PC provenance and broader +reference/consolidation work remain required. Production CPU code and public +packages are unchanged; no old regression is retired. See the +[qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-user-tail-validation-and-software-repair--2026-10-05) +for the distinction between this manual-derived software qualification and +unobserved hardware behavior. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 7bbe6836..6298ebe5 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -33,7 +33,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() "real-access-fault-entry", "writeback-handler", "CP-context-transferred-vector" }) report.Record($"68040/RTE/format7/{form}/bank={bank}/op=4E73/ccr={ccr:X2}", "untested", form == "frame-validation-fault" - ? "Supervisor validation, executed repair/retry, access-fault stacking/vector double faults and selected accurate-batch paths are covered separately; user-tail validation, internal restoration and handler-entry prefetch remain required" + ? "Supervisor and user-tail validation/repair, access-fault stacking/vector/entry-prefetch double faults and selected accurate-batch paths have separate software coverage; internal restoration, untouched-trace retry and physical/hardware qualification remain required" : form == "real-access-fault-entry" ? "Cache-disabled accurate instruction-fetch frames/restart are covered separately; data fault/writeback restart, enabled-cache deferral and compiled instruction-PC provenance remain required" : "Required fault/context protocol has no independently qualified execution fixture yet"); diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs index 863f0734..07022a22 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs @@ -5,8 +5,8 @@ namespace Copper68k.Tests.Synthetic; // MC68040UM 8.4.2 and 8.4.6.7: software may repair the preserved frame. // The handler really executes its stores and RTE; initialization is not repair. -// Supervisor validation tails only. Internal restoration faults and user tails -// require separate architectural evidence and remain in the discovery inventory. +// Supervisor validation tails only. User-tail repair is covered separately by +// SyntheticM68040UserRteFaultTests; internal restoration remains unqualified. public sealed class SyntheticM68040RteRepairTests(ITestOutputHelper output) { private const uint Handler = 0x9020, PendingHandler = 0x9090, Target = 0x6000, Operand = 0x4200; diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs index 8c38bb6d..c9093afd 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs @@ -122,7 +122,8 @@ internal static void ExpectAccessFrame(SyntheticMachine m, ArchitecturalExpectat // EA/invalid WB data are undefined. Verify defined status/address fields, // all untouched original-frame bytes and all neighboring memory instead. for (uint n = 8; n < 60; n++) e.MemoryMasks[accessFrame + n] = 0; - e.Write(accessFrame + 12, width == 2 ? 0x0145u : 0x0105u, 2, m.Model); + var transferModifier = (priorSr & 0x2000) != 0 ? 5u : 1u; + e.Write(accessFrame + 12, (width == 2 ? 0x0140u : 0x0100u) | transferModifier, 2, m.Model); e.MemoryMasks[accessFrame + 12] = 0xff; e.MemoryMasks[accessFrame + 13] = 0x7f; // SSW X undefined foreach (var at in new uint[] { 14, 16, 18 }) { diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs new file mode 100644 index 00000000..c6c9bf96 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs @@ -0,0 +1,238 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 2.2.2.1, 8.1, 8.2.1/5, 8.4.2 and 8.4.6.7 composed: +// throwaways install live SR; an access fault saves it before selecting a +// supervisor stack. This is a manual-derived software qualification, not a +// hardware observation of this unusual user-tail combination. Read ordering +// and synchronous rejection widths retain the emulator's existing policy. +public sealed class SyntheticM68040UserRteFaultTests(ITestOutputHelper output) +{ + private const uint Handler = 0x9020, PendingHandler = 0x9090, PrivilegeHandler = 0x9070, Target = 0x6000, Operand = 0x4200; + private static readonly string[] Forms = ["format0", "format2", "format3", "invalid4", "invalid15", "normal", "CM", "CT", "CU", "CP"]; + + [Fact, Trait("Suite", "Synthetic")] + public void UserTailFaultsAndBareReturnsUseLiveStatusScalar() => Audit(false, false); + [Fact, Trait("Suite", "Synthetic")] + public void UserTailFaultsAndBareReturnsUseLiveStatusBatch() => Audit(false, true); + [Fact, Trait("Suite", "Synthetic")] + public void ExecutedUserTailRepairRebuildsAThrowawayBridgeScalar() => Audit(true, false); + [Fact, Trait("Suite", "Synthetic")] + public void ExecutedUserTailRepairRebuildsAThrowawayBridgeBatch() => Audit(true, true); + + private void Audit(bool repair, bool batch) + { + var bus = new SyntheticM68040RteValidationFaultTests.ValidationFaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + var report = new CoverageBatch("68040", $"rte-user-{(repair ? "repair" : "fault")}-{(batch ? "batch" : "scalar")}"); + foreach (var matrix in new[] { "boundaries", "structure" }) + foreach (var start in new[] { "ISP", "MSP" }) + foreach (var middle in matrix == "structure" ? new[] { "none", "user", "ISP", "MSP" } : ["none"]) + foreach (var userMaster in new[] { false, true }) + foreach (var ccr in matrix == "boundaries" ? Enumerable.Range(0, 32) : new[] { 0, 31 }) + foreach (var incoming in repair ? new ushort[] { 0x8000 } : [0, 0x8000, 0x4000]) + foreach (var alignment in new uint[] { 0, 1 }) + foreach (var vbr in new uint[] { 0, 0x10000 }) + foreach (var result in repair && matrix == "boundaries" ? new[] { "user", "ISP", "MSP" } : ["ISP"]) + foreach (var restoredTrace in repair && matrix == "boundaries" ? new ushort[] { 0, 0x8000, 0x4000 } : [0]) + foreach (var form in Forms) + foreach (var (offset, width) in matrix == "structure" ? SyntheticM68040RteValidationFaultTests.Reads(form) : [(0u, 2)]) + for (var faultByte = 0; faultByte < (matrix == "structure" ? width : 1); faultByte++) + { + var path = middle == "none" ? new[] { start, "user" } : [start, middle, "user"]; + Run(m, bus, report, repair, batch, matrix, path, userMaster, ccr, incoming, alignment, vbr, + result, restoredTrace, form, offset, width, faultByte); + } + report.Complete(output); + } + + private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTests.ValidationFaultBus bus, + CoverageBatch report, bool repair, bool batch, string matrix, string[] path, bool userMaster, int ccr, + ushort incoming, uint alignment, uint vbr, string result, ushort restoredTrace, string form, uint offset, int width, int faultByte) + { + bus.Disarm(); m.Reset(ccr); + var pointers = new Dictionary { ["user"] = 0x7800 + alignment, ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; + foreach (var at in pointers.Values) + for (var n = -76; n < 112; n++) m.InitializePhysical(unchecked(at + (uint)n), (uint)(n ^ 0x5a), 1); + var discarded = new List(); + for (var n = 0; n < path.Length - 1; n++) + { + var at = pointers[path[n]]; discarded.Add(at); + m.InitializePhysical(at, Status(path[n + 1], incoming, ccr ^ 31, userMaster), 2); + m.InitializePhysical(at + 2, 0xdead0001u + (uint)n * 2, 4); + m.InitializePhysical(at + 6, 0x1024, 2); + pointers[path[n]] += 8; + } + var frame = pointers["user"]; + var priorSr = Status("user", incoming, ccr ^ 31, userMaster); + var exceptionBank = userMaster ? "MSP" : "ISP"; + var bridge = pointers[exceptionBank]; var accessFrame = bridge - 60; + var format = form.StartsWith("format") ? int.Parse(form[6..]) : form.StartsWith("invalid") ? int.Parse(form[7..]) : 7; + var repairedFormat = form.StartsWith("invalid") ? 0 : format; + var continuation = form switch { "CM" => 0x1000, "CT" => 0x2000, "CU" => 0x4000, "CP" => 0x8000, _ => 0 }; + var vector = form switch { "CT" => 9, "CU" => 11, "CP" => 49, _ => 0 }; + var savedSr = Status(result, restoredTrace, ccr ^ 31); + var retrySr = (ushort)((priorSr | 0x2000) & ~0xc000); + m.InitializePhysical(frame, 0x801fu ^ (uint)ccr, 2); + m.InitializePhysical(frame + 2, Target + 2, 4); + m.InitializePhysical(frame + 6, (uint)(format << 12) | 8, 2); + m.InitializePhysical(frame + 8, Operand, 4); + m.InitializePhysical(frame + 12, (uint)continuation | 0x0105, 2); + m.InitializePhysical(Target, form == "CM" ? 0x4cd0u : 0x60feu, 2); + m.InitializePhysical(Target + 2, form == "CM" ? 3u : 0x7e7eu, 2); + m.InitializePhysical(Target + 4, 0x60fe, 2); + m.InitializePhysical(Operand, 0x89abcdef, 4); m.InitializePhysical(Operand + 4, 0x12345678, 4); + m.Core.State.A[0] = 0x4300; + var stores = new List<(uint At, uint Value, int Width)>(); + var handler = new List(); + if (repair) + { + // Repair the untouched user frame, then create a fresh throwaway on + // the selected supervisor stack. Patch saved SR explicitly so the + // handler returns in supervisor mode, able to retry privileged RTE. + // The new bridge reselects USP; consumed frames are never replayed. + stores.AddRange([(frame, savedSr, 2), (frame + 2, Target, 4), (frame + 6, (uint)(repairedFormat << 12 | 8), 2), + (bridge, (uint)(priorSr & ~0xc000), 2), (bridge + 2, 0xdead0005, 4), (bridge + 6, 0x1024, 2), (accessFrame, retrySr, 2)]); + foreach (var (at, value, bytes) in stores) + { + // Fixed encodings MOVE.W/L #imm,(xxx).L, independent of decoder. + handler.Add(bytes == 2 ? (ushort)0x33fc : (ushort)0x23fc); + if (bytes == 4) handler.Add((ushort)(value >> 16)); + handler.Add((ushort)value); handler.Add((ushort)(at >> 16)); handler.Add((ushort)at); + } + } + handler.Add(0x4e73); handler.Add(0x7e7e); + for (var n = 0; n < handler.Count; n++) m.InitializePhysical(Handler + (uint)n * 2, handler[n], 2); + m.InitializePhysical(PendingHandler, 0x4e73, 2); m.InitializePhysical(PendingHandler + 2, 0x7e7e, 2); + m.InitializePhysical(PrivilegeHandler, 0x7e7e, 2); + m.InitializePhysical(vbr + 8, Handler, 4); m.InitializePhysical(vbr + 32, PrivilegeHandler, 4); + foreach (var v in new[] { 9, 11, 49 }) m.InitializePhysical(vbr + (uint)v * 4, PendingHandler, 4); + _ = SyntheticExecution.Prepare(m, [0x4e73]); + m.Core.State.SetUserStackPointer(0x7800 + alignment); m.Core.State.SetInterruptStackPointer(0x4700 + alignment); + m.Core.State.SetMasterStackPointer(0x7400 + alignment); m.Core.State.StatusRegister = Status(path[0], incoming, ccr); + m.Core.State.VectorBaseRegister = vbr; + if (form == "CU") m.Core.State.M68040PendingFpuExceptions.Begin(2, 11, Target); + if (form == "CP") m.Core.State.M68040PendingFpuExceptions.Begin(3, 49, Target); + var e = ArchitecturalExpectation.Capture(m); var sequence = m.Core.State.ExceptionSequence; + e.ControlChecks["one rejection"] = (_ => (uint)bus.Rejected.Count, 1); + e.ControlChecks["original read address"] = (_ => bus.Rejected.Count == 1 ? bus.Rejected[0].Address : uint.MaxValue, frame + offset); + e.ControlChecks["original read width"] = (_ => bus.Rejected.Count == 1 ? (uint)bus.Rejected[0].Width : 0, (uint)width); + e.ControlChecks["discarded PCs not fetched"] = (_ => bus.Accesses.Any(a => a.Address is 0xdead0001 or 0xdead0003 or 0xdead0005) ? 1u : 0u, 0); + e.ControlChecks["sentinel not executed"] = (s => s.D[7], e.D[7]); + if (form is "CU" or "CP") e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) == null ? 0u : 1u, 1); + pointers[exceptionBank] = accessFrame; SetStacks(e, pointers, retrySr); e.Pc = Handler; e.ExceptionVector = 2; + Provenance(e, sequence + 1, SyntheticMachine.Code, priorSr); + SyntheticM68040RteValidationFaultTests.ExpectAccessFrame(m, e, accessFrame, priorSr, SyntheticMachine.Code, frame + offset, width); + bus.Arm(frame + offset + (uint)faultByte); + var phases = repair ? new List { "fault-entry", "repair-SR", "repair-PC", "repair-format", "bridge-SR", "bridge-PC", "bridge-format", "repair-return-SR", "handler-return", "retry-RTE" } + : new List { "fault-entry", "bare-return", "privilege" }; + if (repair) { if (vector != 0) phases.Add("pending-return"); phases.Add("following"); } + var id = $"68040/RTE/user-{(repair ? "repair" : "fault")}/{matrix}/{form}/path={string.Join('-', path)}/M={userMaster}/incoming={incoming:X4}/align={alignment}/VBR={vbr:X8}/result={result}/T={restoredTrace:X4}/read={offset}:{width}/byte={faultByte}/op=4E73/ccr={ccr:X2}"; + var phase = 0; + bool Step() + { + string? mismatch; + try + { + Execute(m.Core, batch); mismatch = e.Verify(m); + if (mismatch != null) mismatch += $"; savedSR={m.Core.State.LastExceptionStatusRegister:X4}, USP={m.Core.State.UserStackPointer:X8}, ISP={m.Core.State.InterruptStackPointer:X8}, MSP={m.Core.State.MasterStackPointer:X8}"; + } + catch (NotSupportedException ex) { report.Record(id + "/" + phases[phase++], "unsupported", ex.Message); return Skip(); } + catch (Exception ex) { mismatch = ex.Message; } + report.Record(id + "/" + phases[phase++], mismatch == null ? "passing" : "mismatching", mismatch); + return mismatch == null || Skip(); + } + bool Skip() { while (phase < phases.Count) report.Record(id + "/" + phases[phase++], "untested", "User-tail prerequisite failed"); return false; } + if (!Step()) return; + var repairStart = bus.Accesses.Count; uint handlerPc = Handler; + foreach (var (at, value, bytes) in stores) + { + e.Write(at, value, bytes, m.Model); e.Sr = SyntheticExecution.MoveFlags(e.Sr, value, bytes); + handlerPc += bytes == 2 ? 8u : 10u; e.Pc = handlerPc; + if (!Step()) return; + } + if (repair) + e.ControlChecks["seven exact repair stores"] = (_ => bus.Accesses.Skip(repairStart).Where(a => a.Write).SequenceEqual( + stores.Select(s => new BusAccess(s.At, s.Width, true, s.Value, M68kBusAccessKind.CpuDataWrite))) ? 1u : 0u, 1); + pointers[exceptionBank] = bridge; SetStacks(e, pointers, repair ? retrySr : priorSr); + e.Write(accessFrame, repair ? retrySr : priorSr, 2, m.Model); e.Pc = SyntheticMachine.Code; e.ExceptionVector = null; + if (!Step()) return; + if (!repair) + { + pointers[exceptionBank] -= 8; SetStacks(e, pointers, retrySr); e.Pc = PrivilegeHandler; e.ExceptionVector = 8; + Provenance(e, sequence + 2, SyntheticMachine.Code, priorSr); + e.Write(bridge - 8, priorSr, 2, m.Model); e.Write(bridge - 6, SyntheticMachine.Code, 4, m.Model); e.Write(bridge - 2, 0x0020, 2, m.Model); + for (uint n = 0; n < 8; n++) e.MemoryMasks[bridge - 8 + n] = 255; + Step(); return; + } + e.ControlChecks.Remove("seven exact repair stores"); + var retryStart = bus.Accesses.Count; + e.ControlChecks["old throwaways not reread"] = (_ => bus.Accesses.Skip(retryStart).Any(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && + discarded.Any(at => unchecked(a.Address - at) < 8)) ? 1u : 0u, 0); + pointers[exceptionBank] += 8; pointers["user"] += repairedFormat == 7 ? 60u : repairedFormat == 0 ? 8u : 12u; + SetStacks(e, pointers, savedSr); e.Pc = Target; + if (vector != 0) + { + Pending(m, e, pointers, savedSr, vector, Target, Operand, sequence + 2); + if (form is "CU" or "CP") e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) == null ? 0u : 1u, 0); + } + if (!Step()) return; + e.ControlChecks.Remove("old throwaways not reread"); + if (vector != 0) + { + pointers[ExceptionBank(savedSr)] += 12; SetStacks(e, pointers, savedSr); e.Pc = Target; e.ExceptionVector = null; + if (!Step()) return; + } + e.Pc = form == "CM" ? Target + 4 : Target; + if (form == "CM") { e.D[0] = 0x89abcdef; e.D[1] = 0x12345678; } + if (restoredTrace == 0x8000 || restoredTrace == 0x4000 && form != "CM") + Pending(m, e, pointers, savedSr, 9, e.Pc, Target, sequence + (vector != 0 ? 3u : 2u)); + Step(); + } + + private static ushort Status(string bank, ushort trace, int ccr, bool userMaster = false) => + (ushort)((bank == "MSP" ? 0x3000 : bank == "ISP" ? 0x2000 : userMaster ? 0x1000 : 0) | trace | ccr); + private static string ExceptionBank(ushort sr) => (sr & 0x1000) != 0 ? "MSP" : "ISP"; + private static void SetStacks(ArchitecturalExpectation e, Dictionary pointers, ushort sr) + { + e.Sr = sr; e.A[7] = pointers[(sr & 0x2000) == 0 ? "user" : ExceptionBank(sr)]; + e.InactiveStackPointer = pointers[(sr & 0x2000) == 0 ? "ISP" : "user"]; e.MasterStackPointer = pointers["MSP"]; + foreach (var bank in new[] { "user", "ISP", "MSP" }) + { + var expected = pointers[bank]; + e.ControlChecks[bank] = (s => bank == "user" ? s.UserStackPointer : bank == "ISP" ? s.InterruptStackPointer : s.MasterStackPointer, expected); + } + } + private static void Provenance(ArchitecturalExpectation e, uint sequence, uint pc, ushort sr) + { + e.ControlChecks["exception entries"] = (s => s.ExceptionSequence, sequence); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, pc); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, sr); + } + private static void Pending(SyntheticMachine m, ArchitecturalExpectation e, Dictionary pointers, + ushort sr, int vector, uint pc, uint address, uint sequence) + { + var bank = ExceptionBank(sr); pointers[bank] -= 12; + SetStacks(e, pointers, (ushort)((sr | 0x2000) & ~0xc000)); e.Pc = PendingHandler; e.ExceptionVector = vector; + Provenance(e, sequence, pc, sr); + var at = pointers[bank]; e.Write(at, sr, 2, m.Model); e.Write(at + 2, pc, 4, m.Model); + e.Write(at + 6, (uint)((vector >= 49 ? 3 : 2) << 12 | vector * 4), 2, m.Model); e.Write(at + 8, address, 4, m.Model); + for (uint n = 0; n < 12; n++) e.MemoryMasks[at + n] = 255; + } + private static void Execute(IM68kCore core, bool batch) + { + if (!batch) { core.ExecuteInstruction(); return; } + var boundary = new Boundary(); + var count = ((IM68kBatchCore)core).ExecuteInstructions(1, core.State.Cycles + 1000, boundary); + if (count != 1 || boundary.Before != 1 || boundary.After != 1) + throw new InvalidOperationException($"Batch count/callbacks differ: {count}/{boundary.Before}/{boundary.After}"); + } + private sealed class Boundary : IM68kInstructionBoundary + { + public int Before, After; + public bool BeforeInstruction() { Before++; return true; } + public void AfterInstruction(long previousCycle, long currentCycle) => After++; + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 9de68a50..17030e5b 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -4481,3 +4481,109 @@ Embedded CPU, four package assets and four loaded assemblies match the audited CPU. Evidence is in the isolated consumer's `artifacts/entry-window-validation/` and `entry-window-identities.json`. Root CopperScreen changes and its 1.5.1 pin remain untouched. No package is published and no throughput claim is made. + +### 040 user-tail validation and software repair — 2026-10-05 + +`SyntheticM68040UserRteFaultTests` qualifies the MMU/cache-disabled accurate +factory and one-instruction batching against independently composed manual +expectations. [MC68040UM](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +2.2.2.1 defines stack selection; 8.4.2/figure 8-6 install live SR when consuming +throwaways, including USP; 8.1/8.2.1 preserve current SR and enter supervisor +mode for an access exception; 8.2.5 defines privilege faults; 8.4.6.7 preserves +an incompletely validated frame. The previously inspected 1993 scan additionally +provides figure 8-6 at PDF page 243 (one based), printed 8-22, with the same +SHA-256 recorded above. No new reference input or media is committed. + +This expectation is an **inference combining those documented rules**. The +manual's usual description of stacking below the incomplete frame assumes a +supervisor validation stack; it does not explicitly show the combined USP fault +path. These tests instead require the general exception's supervisor stack, +chosen by retained M, and preserve the user frame. No hardware measurement or +executed WinUAE oracle for this combination is claimed. The historical WinUAE +source snapshot in `artifacts/m6-rte-address-winuae/` uses live SR after a +throwaway and changes from USP to the M-selected supervisor stack on exception; +that is documentary corroboration only, not an independent execution audit. +Hardware provenance of the combined case stays unqualified and cannot be +inferred from interpreter/batch agreement. + +Both user M values are exercised, with each initial supervisor stack and optional +middle throwaway on user/ISP/MSP. The fault saves live intermediate SR and the +original RTE PC in format 7, reports user-data TM=1 and the rejected read's FA/ +SIZE, and leaves the unvalidated user SR/PC/frame and memory outside the new +access frame intact. Consumed throwaway memory can be reused by exception +stacking; its committed stack-pointer effects survive and it is not replayed. +Incoming trace is cleared for handler execution. A bare RTE return restores +user mode; the following RTE raises privilege violation on the selected +supervisor stack. Pending CU/CP objects stay pending on this path. + +The repair path executes three stores to the original user frame, three to a +fresh supervisor throwaway bridge and one to the access frame's saved SR. +It explicitly sets S and clears incoming trace for the handler return. The +retried supervisor RTE consumes the fresh bridge, selects USP and completes +the repaired frame without rereading any consumed throwaway or fetching its +discarded PC. Repair-store address/width/value/count, exact next PC, preserved +registers/stacks/memory, following traces, CM saved-EA accesses and CT/CU/CP49 +delivery are independently checked. FPU arithmetic is not in this scope. + +| Group, each scalar/batch | Cases | Combinations | +| --- | ---: | ---: | +| User validation / bare return / privilege | 168,192 | 20,832 | +| Executed repair / bridge / retry / following | 675,328 | 8,224 | + +The four ordinary groups total **1,687,040 cases**. Canonical SR-read cases use +all 32 CCRs; structural cases use 0/31 while rejecting each byte of every +validation read. Every admitted frame form is retained: 0/2/3, invalid 4/15, +normal format 7 and CM/CT/CU/CP49. Invalid formats are repaired to format 0. +All three incoming trace states are covered by fault groups; repair explicitly +clears saved incoming T1 and checks all three restored traces and stacks in its +canonical cases. Untouched incoming-trace retry is still required separately. + +The first focused run in `artifacts/m6-user-rte-first/` passes all four tests +with no skips, mismatches, unsupported execution or untested phases. The +production CPU source is unchanged. Three maintained `UserRteFault` mutations +in `artifacts/m6-user-rte-mutations/` each execute both complete fault batches: +saved-S corruption and supervisor-data TM mismatch 56,064 fault-entry cases per +route (112,128 dependent phases untested); forced ISP mismatches 28,032 per route, +leaves 56,064 dependent phases untested and passes the 84,096 M-clear phases. +Proof requires the intended SR/SSW diagnostic on each route. Source bytes are +restored and rebuilt before acceptance validation. No old test is retired. + +The full 040 command now requires **44 executed tests, 38 reporting batches, +six fixed examples and thirteen input identities**, with independent +combination enumeration for all four new groups. Its preserved 480-case +inventory still fails the complete gate; internal-restoration faults, chained +odd-PC SR provenance, general data/writeback/context-transfer protocols and +all broader reference/consolidation requirements remain open. Selected software +qualification does not replace physical bus sequencing/timing or enabled-MMU/ +cache coverage. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +Fresh full Release validation in `artifacts/m6-user-rte-full/` passes **5,069 +tests with eleven optional skips and zero failures**. All nine qualified WinUAE +presets match the previously pinned exact directory/callback/frame counts and +the current CPU/adapter identities in `qualified-preset-identities.json`. +The strict report gate passes **19,735,574 cases / 628 reporting batches**. +Fresh pinned SingleStepTests passes **312,500 cases / 125 files**; fresh Musashi +passes **536 programs with 88 explicit exclusions**. Their source pins and +software-reference limits remain unchanged. Both fresh reference tests execute +without skips; they are separate from the optional skips in the full run. + +The complete 040 audit in `artifacts/m6-user-rte-audit/` executes 44 tests: +43 pass and the required inventory fails. All **4,766,912 executable cases +pass**, with zero mismatches/unsupported execution; **480 remain untested**. +All source/input/binary and independent combination checks validate before the +inventory rejection. All **17** new malformed-report/identity controls in +`artifacts/m6-user-rte-controls/` reject their specific missing, shortened, +foreign, redistributed or omitted-fixture corruption. A generic inventory +failure is never accepted as proof that a negative control worked. + +CPU assembly SHA-256 is +`dccd468b1a26090501e310acc6cafd1ef3cc3fb6b3fb63fc48f0f614869f889e`; +test/reference adapter is +`ae19b94ecd01f7c4ce908e1779f511ef411371e8c8f6e4a4e963bf97d85cab86`. +The manifest records starting commit `dc27536`, the committed CPU tree and +separate current source/fixture hashes. Production CPU source has no changes +from that commit. This rebuild's informational version includes `dc27536`; +its binary identity is not relabeled as the preceding checkpoint's binary. +No new consumer validation or private/public package is generated for this +test-only checkpoint; preceding unpublished `.63` evidence remains scoped to +its own unchanged package and binary. Root CopperScreen work is untouched. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 29ac31a0..d3af08be 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1665,3 +1665,45 @@ Isolated CopperScreen through unpublished private `.63` passes Release build, host/disk suites, 1,080 separate engine diagnostics and all three native replays, with matching package/assets/CPU binaries. Evidence and limits are recorded in the qualification record; this checkpoint does not complete milestone 6. + +### Milestone 6 user-tail RTE validation and executed repair — 2026-10-05 + +Four ordinary-CI batches add **1,687,040 passing cases** for throwaways selecting +USP, including both values of the live user M bit. Fault expectations compose +MC68040UM's documented live-SR throwaway behavior with general access-exception +entry; this is manual-derived software qualification, not an observed hardware +reference for the unusual combination. The selected supervisor stack receives +the access frame, while the incomplete user frame stays intact and consumed +throwaway stack-pointer effects survive. A bare return restores user mode and +the next RTE raises privilege violation. A real seven-store handler instead repairs the user frame, constructs +a new throwaway bridge and explicitly sets saved S before retrying RTE. + +Canonical SR-read cases cover all 32 CCRs. Structural cases reject every byte of +SR/PC/format/SSW/continuation-EA reads, one/two-throwaway paths through any stack, +both alignments/VBRs and all supported frame/continuation forms. Repair covers +all restored stacks/traces in its canonical group, continuation delivery and +the following instruction. Three maintained `UserRteFault` mutations detect +saved-S corruption, supervisor-data TM and forced ISP selection on both routes. +Production CPU source is unchanged; the shared test frame checker now derives +TM from the independently specified fault SR. + +The complete 040 gate requires 44 tests, 38 reporting batches, six fixed examples +and thirteen input identities. Its independent iterator checks each new +combination and cardinality. The 480-case inventory remains failing for internal +restoration, chained odd-PC provenance, general data/writeback/context transfer +and other pending requirements. Untouched incoming-trace retry and all earlier +model/reference/consolidation work remain required. No regression is retired, +public API changed or package released. Milestone 6 remains **in progress**; +`roadmapComplete=false`. See the +[qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040-user-tail-validation-and-software-repair--2026-10-05). + +Fresh full Release validation passes 5,069 CPU tests with eleven optional skips +and zero failures. All nine pinned WinUAE presets retain their exact counts and +matching CPU/adapter identities. The strict ordinary gate passes 19,735,574 +cases / 628 batches; fresh SingleStepTests passes 312,500 cases / 125 files and +Musashi passes 536 programs with 88 exclusions. The dedicated 040 run passes +4,766,912 cases with zero mismatches/unsupported execution, retaining its 480 +untested requirements and failing completion. All three mutations and seventeen +report/identity controls detect their intended failures. No consumer or package +change is needed for this test-only checkpoint; preceding `.63` consumer +evidence remains historical evidence of its own qualified binary. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index 83df3dcd..a27913f2 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -41,6 +41,10 @@ $expected = [ordered]@{ 'handler-prefetch-retention-batch' = @{cases=7680; combinations=240} 'handler-prefetch-odd-scalar' = @{cases=12288; combinations=384} 'handler-prefetch-odd-batch' = @{cases=12288; combinations=384} + 'rte-user-fault-scalar' = @{cases=168192; combinations=20832} + 'rte-user-fault-batch' = @{cases=168192; combinations=20832} + 'rte-user-repair-scalar' = @{cases=675328; combinations=8224} + 'rte-user-repair-batch' = @{cases=675328; combinations=8224} 'rte-access-remaining-protocols' = @{cases=480; combinations=15} } $testExit = 0 @@ -54,19 +58,19 @@ if (-not $ValidateReportsOnly) { $saved[$name] = [Environment]::GetEnvironmentVariable($name) [Environment]::SetEnvironmentVariable($name, $settings[$name]) } - & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests|FullyQualifiedName~SyntheticM68040RteValidationFaultTests|FullyQualifiedName~SyntheticM68040AccessDoubleFaultTests|FullyQualifiedName~SyntheticM68040BatchFaultTests|FullyQualifiedName~SyntheticM68040RteRepairTests|FullyQualifiedName~SyntheticM68040InstructionFaultTests|FullyQualifiedName~SyntheticM68040HandlerPrefetchTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests|FullyQualifiedName~SyntheticM68040RteValidationFaultTests|FullyQualifiedName~SyntheticM68040AccessDoubleFaultTests|FullyQualifiedName~SyntheticM68040BatchFaultTests|FullyQualifiedName~SyntheticM68040RteRepairTests|FullyQualifiedName~SyntheticM68040InstructionFaultTests|FullyQualifiedName~SyntheticM68040HandlerPrefetchTests|FullyQualifiedName~SyntheticM68040UserRteFaultTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output $testExit = $LASTEXITCODE } finally { foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } } $identity = [ordered]@{ schema=1; reference='MC68040UM'; url='https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf' - sections=@('7.6.1','7.6.3','8.1 figure 8-1','8.2.1','8.2.2','8.2.6','8.4','8.4.1','8.4.2','8.4.3','8.4.4','8.4.6.2','8.4.6.7'); softwareReferenceExecuted=$false + sections=@('2.2.2.1','7.6.1','7.6.3','8.1 figure 8-1','8.2.1','8.2.2','8.2.5','8.2.6','8.4','8.4.1','8.4.2','8.4.3','8.4.4','8.4.6.2','8.4.6.7'); softwareReferenceExecuted=$false sourceCommit=(& git -C $repo rev-parse HEAD); cpuCommittedTree=(& git -C $repo rev-parse HEAD:Copper68k) cpuSourceFiles=@(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } - inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs') | ForEach-Object { + inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } assemblies=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | ForEach-Object { @@ -77,7 +81,8 @@ if (-not $ValidateReportsOnly) { 'Multiple continuation bits are architecturally undefined and excluded', 'Selected active accurate-batch paths verify counts/callbacks and scalar/batch bus/cycle policy; generic short operand frames do not qualify architectural format-7 data restart', 'Instruction-fault fixtures use cache-disabled accurate execution and physical-map rejection; speculative deferral, enabled caches/MMU and compiled fetch PC provenance remain unqualified', - 'Executed supervisor-tail frame repair explicitly clears saved incoming trace; untouched incoming-trace retry and user-tail repair are not qualified', + 'Executed supervisor-tail repair clears saved incoming trace; user-tail repair explicitly sets S and builds a new throwaway bridge. Untouched incoming-trace retry remains required', + 'User-tail fault expectations compose documented throwaway live-SR rules with general supervisor exception entry; unusual combined hardware behavior has not been observed', 'User-tail validation, internal-restoration double faults, cache/MMU/compiled handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', 'Direct odd-RTE saved-SR ordering uses documentary WinUAE 5d22d336, not an executed hardware oracle') } @@ -88,11 +93,11 @@ if (-not $ValidateReportsOnly) { $identity = Get-Content -LiteralPath (Join-Path $output 'identities.json') -Raw | ConvertFrom-Json if ($identity.schema -ne 1 -or $identity.reference -cne 'MC68040UM' -or $identity.softwareReferenceExecuted -ne $false -or $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.cpuCommittedTree -notmatch '^[a-f0-9]{40}$' -or - @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 12 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 12 -or + @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 13 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 13 -or @($identity.assemblies).Count -ne 2) { throw '040 access-frame input identity is missing or incomplete' } foreach ($input in $identity.inputs) { $path = Join-Path $repo $input.file - if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs') -or + if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs') -or $input.sha256 -cne (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()) { throw '040 access-frame fixture/command identity differs' } } $cpuFiles = @(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') @@ -106,8 +111,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 40 -or [int]$counters.total -ne 40 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (34 batches, 6 fixed examples)' +if ([int]$counters.executed -ne 44 -or [int]$counters.total -ne 44 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (38 batches, 6 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -130,6 +135,42 @@ foreach ($group in $expected.Keys) { $expectedCombinations["68040/RTE/format7/$form/bank=$bank"] = 32 } } + } elseif ($group.StartsWith('rte-user-', [StringComparison]::Ordinal)) { + $repair = $group.StartsWith('rte-user-repair-', [StringComparison]::Ordinal) + foreach ($matrix in @('boundaries','structure')) { + foreach ($start in @('ISP','MSP')) { + foreach ($middle in $(if ($matrix -eq 'structure') { @('none','user','ISP','MSP') } else { @('none') })) { + $path = if ($middle -eq 'none') { "$start-user" } else { "$start-$middle-user" } + foreach ($master in @($false,$true)) { + foreach ($incoming in $(if ($repair) { @(0x8000) } else { @(0,0x8000,0x4000) })) { + foreach ($alignment in @(0,1)) { foreach ($vbr in @(0,0x10000)) { + foreach ($result in $(if ($repair -and $matrix -eq 'boundaries') { @('user','ISP','MSP') } else { @('ISP') })) { + foreach ($trace in $(if ($repair -and $matrix -eq 'boundaries') { @(0,0x8000,0x4000) } else { @(0) })) { + foreach ($form in @('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP')) { + $reads = @(@{offset=0; width=2}) + if ($matrix -eq 'structure') { + $reads += @(@{offset=2; width=4}, @{offset=6; width=2}) + if ($form -in @('normal','CM','CT','CU','CP')) { + $reads += @{offset=12; width=2} + if ($form -ne 'normal') { $reads += @{offset=8; width=4} } + } + } + foreach ($read in $reads) { + foreach ($byte in $(if ($matrix -eq 'structure') { 0..($read.width - 1) } else { @(0) })) { + $key = '68040/RTE/user-{0}/{1}/{2}/path={3}/M={4}/incoming={5:X4}/align={6}/VBR={7:X8}/result={8}/T={9:X4}/read={10}:{11}/byte={12}' -f $(if ($repair) {'repair'} else {'fault'}),$matrix,$form,$path,$master,$incoming,$alignment,$vbr,$result,$trace,$read.offset,$read.width,$byte + $phases = if (-not $repair) { 3 } elseif ($form -in @('CT','CU','CP')) { 12 } else { 11 } + $expectedCombinations[$key] = $(if ($matrix -eq 'boundaries') {32} else {2}) * $phases + } + } + } + } + } + } } + } + } + } + } + } } elseif ($group.StartsWith('handler-prefetch-retention-', [StringComparison]::Ordinal)) { $route = if ($group.EndsWith('-scalar', [StringComparison]::Ordinal)) { 'scalar' } else { 'batch' } foreach ($mode in @('linear','branch','subroutine','task','map')) { @@ -381,8 +422,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 40 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 44 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=3080352; xunitBatches=34; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=4767392; xunitBatches=38; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index db6d8a81..6031bc2b 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','UserRteFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -361,6 +361,28 @@ $entryMutations = @( foreach ($mutation in $entryMutations) { $mutation.model='68040'; $mutation.milestone=6; $mutation.entryPrefetch=$true; $mutations += $mutation } +$userRteMutations = @( + @{name='040-user-validation-saved-s'; before=@' + var savedSr = State.StatusRegister; + State.RecordException(VectorBusError, instructionPc, savedSr); +'@; after=@' + var savedSr = (ushort)(State.StatusRegister | M68kCpuState.Supervisor); + State.RecordException(VectorBusError, instructionPc, savedSr); +'@; proofCombination='/M=False/'; proofReason='saved SR expected'}, + @{name='040-user-validation-supervisor-tm'; before=' var modifier = ((savedSr & M68kCpuState.Supervisor) != 0 ? 4 : 0) | (instruction ? 2 : 1);'; + after=' var modifier = ((savedSr & M68kCpuState.Supervisor) != 0 ? 4 : 0) | (instruction ? 2 : 5);'; proofCombination='/M=False/'; proofReason='Memory'}, + @{name='040-user-validation-forced-isp'; before=@' + State.RecordException(VectorBusError, instructionPc, savedSr); + State.StatusRegister = (ushort)((savedSr | M68kCpuState.Supervisor) & ~0xc000); +'@; after=@' + State.RecordException(VectorBusError, instructionPc, savedSr); + State.StatusRegister = (ushort)((savedSr | M68kCpuState.Supervisor) & ~0xd000); +'@; proofCombination='/M=True/'; proofReason='SR expected'} +) +foreach ($mutation in $userRteMutations) { + $mutation.file='Copper68k/M68040Support.cs'; $mutation.model='68040'; $mutation.milestone=6 + $mutation.group='UserTailFaultsAndBareReturnsUseLiveStatus'; $mutation.userRte=$true; $mutations += $mutation +} if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } if ($Scope -eq 'Logical') { $mutations = @($mutations | Where-Object { $_.milestone -eq 4 }) } @@ -372,6 +394,7 @@ if ($Scope -eq 'RteRepair') { $mutations = @($mutations | Where-Object { $_.repa if ($Scope -eq 'InstructionFault') { $mutations = @($mutations | Where-Object { $_.instructionFault }) } if ($Scope -eq 'HandlerPrefetch') { $mutations = @($mutations | Where-Object { $_.handlerPrefetch }) } if ($Scope -eq 'EntryPrefetch') { $mutations = @($mutations | Where-Object { $_.entryPrefetch }) } +if ($Scope -eq 'UserRteFault') { $mutations = @($mutations | Where-Object { $_.userRte }) } if ($Scope -eq 'AccessDoubleFault') { $mutations = @($mutations | Where-Object { $_.doubleFault }) } if ($Scope -eq 'BatchFault') { $mutations = @($mutations | Where-Object { $_.batchFault }) } if ($Scope -eq 'LowPowerStop') { $mutations = @($mutations | Where-Object { $_.lpstop }) } @@ -454,6 +477,21 @@ try { throw 'Entry-prefetch mutation omitted complete cases or intended semantic failure' } } + if ($mutation.userRte) { + [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + if ($trx.TestRun.ResultSummary.Counters.executed -ne 2 -or $trx.TestRun.ResultSummary.Counters.failed -ne 2 -or $batches.Count -ne 2 -or + @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq 'rte-user-fault-scalar' -and $_.logicalCases -eq 168192 }).Count -ne 1 -or + @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq 'rte-user-fault-batch' -and $_.logicalCases -eq 168192 }).Count -ne 1) { + throw 'User-tail mutation omitted complete scalar/batch selections' + } + foreach ($route in @('scalar','batch')) { + $routeFailures = @($batches | Where-Object group -CEQ "rte-user-fault-$route")[0].failures + if (@($routeFailures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and + $_.id.EndsWith('/fault-entry', [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) }).Count -eq 0) { + throw "User-tail mutation omitted intended $route architectural failure" + } + } + } if ($mutation.instructionFault) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if ($trx.TestRun.ResultSummary.Counters.executed -ne 2 -or $batches.Count -ne 2 -or @@ -526,6 +564,9 @@ try { } elseif ($mutation.entryPrefetch) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] + } elseif ($mutation.userRte) { + @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and + $_.id.EndsWith('/fault-entry', [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] } else { $failures[0] } $results += @{mutation=$mutation.name; file=$mutation.file; sourceSha256=(Get-FileHash -LiteralPath $path).Hash; detected=$true; replacementCase=$proofFailure.id; diagnostic=$proofFailure.reason; originalRegressionDetected=$legacyPresent; diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index f057db66..2869e10a 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -181,6 +181,10 @@ try { $expected['handler-prefetch-retention-batch'] = 7680 $expected['handler-prefetch-odd-scalar'] = 12288 $expected['handler-prefetch-odd-batch'] = 12288 + $expected['rte-user-fault-scalar'] = 168192 + $expected['rte-user-fault-batch'] = 168192 + $expected['rte-user-repair-scalar'] = 675328 + $expected['rte-user-repair-batch'] = 675328 $expected['system-mmu-disabled'] = 34850 $expected['system-linef-pc-restore'] = 6464 } @@ -278,12 +282,13 @@ try { externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; m68040HandlerPrefetchScope=@{logicalCases=457728; batches=8; routes=@('accurate scalar','accurate batch'); bus='Physical map with no host code reader, including stable-map generation'; qualified='Four-long access-error entry window, every entry byte, odd handler addresses, retained data, flow/host/task/map invalidation and later instruction faults; denied cold boundaries perform no CPU fetch/state change'; remaining='Other exception-entry paths, host-reader/compiled fetch provenance, enabled caches/MMU, ordinary speculative deferral and physical timing'}; - m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, and executed repair/retry after committed supervisor throwaways; chained odd-PC saved-SR provenance, user-tail/internal-restoration faults, CP context transfer, other real fault entry and physical timing remain unqualified'; - m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='User-tail validation, internal restoration, untouched incoming-trace retry and other real access faults'}; + m68040UserTailScope=@{logicalCases=1687040; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); authority='Composition of MC68040UM 2.2.2.1, 8.1, 8.2.1/5, 8.4.2 and 8.4.6.7; software qualification, no executed hardware reference'; userM=@(0,1); initialBanks=@('ISP','MSP'); middleBanks=@('none','user','ISP','MSP'); ccr='all 32 canonical SR reads, 0/31 structural reads'; forms='0/2/3, invalid 4/15, normal/CM/CT/CU/CP49'; qualified='Live intermediate SR, supervisor exception stack selected by M, user-data TM=1, preserved USP frame and committed throwaways, bare return followed by privilege violation, executed seven-store repair with fresh throwaway bridge and following trace/continuation'; remaining='Internal restoration faults, untouched trace retry, physical bus-order/timing and hardware observation of composed user-tail fault behavior'}; + m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, executed repair/retry after committed supervisor throwaways, and manual-derived user-tail fault/privilege/bridge repair; chained odd-PC saved-SR provenance, internal-restoration faults, untouched-trace retry, CP context transfer, other real fault entry and physical timing remain unqualified'; + m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='Internal restoration, untouched incoming-trace retry and other real access faults; user-tail repair is qualified separately'}; m68040InstructionFaultScope=@{logicalCases=116736; batches=2; translationEnabled=$false; instructionCacheEnabled=$false; engine='accurate public factory plus cached self-branch batch'; forms=@('opcode','extension-low','next-opcode','self-branch'); banks=@('user','ISP','MSP'); ccr='all 32'; trace='All defined incoming states for frame/return; restart trace zero'; fields='Format7 SR, executing PC distinct from prefetch FA, RW/SIZE/TM and invalid writebacks'; phases=@('fault entry','handler return','restart','following'); remaining='Data writebacks/restart, speculative prefetch deferral, compiled instruction-fault PC provenance, cache-enabled fetches and physical timing'}; - m68040RtePhysicalValidationScope=@{logicalCases=223872; batches=2; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); trace=@(0,0x8000,0x4000); alignment=@(0,1); vbr=@(0,0x10000); faults='Each byte of SR/PC/format/SSW/continuation-EA reads; 14 direct/one/two-throwaway supervisor paths'; phases=@('fault entry','handler return'); undefined='EA and invalid writeback/push data; SSW X'; remaining='User-tail and internal-restoration/double faults, other instruction faults and software writeback handlers'}; - m68040AccessDoubleFaultScope=@{logicalCases=105408; batches=5; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; dispatchCcr=@(0,31); engines=@('accurate','classic','V2'); faults='Every byte of format-7 stack/vector; later handler validation refaults; warmed RTE fallback and compiled B/W/L operand read/write side exits'; recovery='External reset, including restored host entry; interrupt, task/subroutine entry and halted batches remain inactive'; undefined='Partial stack write order, contents and exact SP'; remaining='Internal restoration, handler-entry prefetch, user-tail validation and general architectural access-fault restart'}; - m68040BatchFaultScope=@{logicalCases=139392; batches=2; translationEnabled=$false; paths=@('cold','cached','model-specific cached','self-branch cached'); limits=@('instruction cap','boundary denial','cycle deadline'); architectural='Supervisor RTE validation frames, unbuffered self-branch instruction-fault frame and fatal second-fault halt'; policy='Generic short operand frames, scalar/batch bus order and machine/native cycle equality'; qualification='Dedicated discovery enumerates independent combinations; BatchFault scope proves four delivery paths, count, callback and retry defects'; remaining='General data format-7 restart, entry prefetch and user-tail/internal-restoration faults'}; + m68040RtePhysicalValidationScope=@{logicalCases=223872; batches=2; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); trace=@(0,0x8000,0x4000); alignment=@(0,1); vbr=@(0,0x10000); faults='Each byte of SR/PC/format/SSW/continuation-EA reads; 14 direct/one/two-throwaway supervisor paths'; phases=@('fault entry','handler return'); undefined='EA and invalid writeback/push data; SSW X'; remaining='Internal-restoration/double faults, other instruction faults and software writeback handlers; user-tail validation is qualified separately'}; + m68040AccessDoubleFaultScope=@{logicalCases=105408; batches=5; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; dispatchCcr=@(0,31); engines=@('accurate','classic','V2'); faults='Every byte of format-7 stack/vector; later handler validation refaults; warmed RTE fallback and compiled B/W/L operand read/write side exits'; recovery='External reset, including restored host entry; interrupt, task/subroutine entry and halted batches remain inactive'; undefined='Partial stack write order, contents and exact SP'; remaining='Internal restoration, other handler-entry paths and general architectural access-fault restart'}; + m68040BatchFaultScope=@{logicalCases=139392; batches=2; translationEnabled=$false; paths=@('cold','cached','model-specific cached','self-branch cached'); limits=@('instruction cap','boundary denial','cycle deadline'); architectural='Supervisor RTE validation frames, unbuffered self-branch instruction-fault frame and fatal second-fault halt'; policy='Generic short operand frames, scalar/batch bus order and machine/native cycle equality'; qualification='Dedicated discovery enumerates independent combinations; BatchFault scope proves four delivery paths, count, callback and retry defects'; remaining='General data format-7 restart, other entry paths and internal-restoration faults'}; m68060LowPowerStopScope='Every unrecognized second opcode word in both privilege modes; fixed encoding, status, CCR and incoming trace cases across all profiles. Opcode-PC exception frames follow MC68060UM 8.2.4/5; physical broadcast, pins and ordinary STOP S-clear software disagreement remain unqualified'; cacheInstructionEncodingScope='Every F4xx opcode, all CCRs and both privilege states across eight profiles; scope zero is illegal on 040/060, including neither-cache forms, before privilege effects. Empty fixture caches qualify architectural state and exception outcomes, not physical invalidation, writeback, bus faults or timing'; m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; marked private word-MOVE/MOVEA images resume their faulted cycle; other opaque restart/input state, long transfers, RMW and physical timing remain unqualified'; From b884aee8a7274a505f0925ac5828d9ffb7a7ff5a Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 00:19:24 +0300 Subject: [PATCH 050/211] Qualify preserved trace after 68040 RTE repair --- Copper68k.Tests/Synthetic/README.md | 30 +++++ .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- .../SyntheticM68040RteRepairTests.cs | 82 ++++++++++--- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 110 ++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 40 +++++++ scripts/test-copper68k-040-access-frames.ps1 | 29 +++-- .../test-copper68k-synthetic-mutations.ps1 | 45 ++++++- scripts/test-copper68k-synthetic.ps1 | 9 +- 8 files changed, 315 insertions(+), 32 deletions(-) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 49336476..43020868 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -553,3 +553,33 @@ packages are unchanged; no old regression is retired. See the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-user-tail-validation-and-software-repair--2026-10-05) for the distinction between this manual-derived software qualification and unobserved hardware behavior. + +### Preserved incoming trace after RTE repair + +Four additional `SyntheticM68040RteRepairTests` reports cover 2,729,088 phases +through accurate scalar and one-instruction batch execution. The real handler +repairs the original SR/PC/format with three stores, leaving the access frame's +saved SR untouched. Handler return restores incoming T1/T0. Successful original +RTE completion traces using the repaired SR/PC even when its restored trace +bits are clear. The trace-handler RTE and following instruction also check that +the CM saved MOVEM address survives the intervening handler. + +Canonical cases cover all 32 CCRs. Chained cases reject every validation read +byte, use CCR 0/31, both alignments/VBRs and all twelve supervisor throwaway +paths, including middle stacks. Both groups cross incoming/restored trace and +restored user/ISP/MSP stacks. Forms are 0/2/3, invalid 4/15 repaired to 0 and +normal/CM format 7. Each throwaway in a case uses the same incoming trace value; +pending CT/CU/CP, a user-tail trace bridge and mixed-epoch trace provenance remain +separate required gaps. + +```powershell +./scripts/test-copper68k-synthetic-mutations.ps1 -Scope RteRetryTrace -OutputDirectory artifacts/rte-retry-trace-mutations +``` + +The command requires all four complete reports and the intended semantic +failure for restored-bit trace gating, missing T0 RTE classification and early +CM continuation consumption. The complete 040 gate now checks 48 tests, 42 +reports, six fixed examples and thirteen input identities. Its retained +480-case remaining-protocol inventory still fails completion. No production +CPU change, package publication or regression retirement is included. See the +[qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-preserved-trace-rte-repair--2026-10-05). diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 6298ebe5..34c14939 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -33,7 +33,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() "real-access-fault-entry", "writeback-handler", "CP-context-transferred-vector" }) report.Record($"68040/RTE/format7/{form}/bank={bank}/op=4E73/ccr={ccr:X2}", "untested", form == "frame-validation-fault" - ? "Supervisor and user-tail validation/repair, access-fault stacking/vector/entry-prefetch double faults and selected accurate-batch paths have separate software coverage; internal restoration, untouched-trace retry and physical/hardware qualification remain required" + ? "Supervisor/user-tail validation and normal/CM preserved-trace retry have separate software coverage; internal restoration and pending/user/mixed-epoch trace protocols remain required. Physical/hardware behavior is separately unqualified" : form == "real-access-fault-entry" ? "Cache-disabled accurate instruction-fetch frames/restart are covered separately; data fault/writeback restart, enabled-cache deferral and compiled instruction-PC provenance remain required" : "Required fault/context protocol has no independently qualified execution fixture yet"); diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs index 07022a22..e647fce0 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs @@ -20,33 +20,46 @@ public sealed class SyntheticM68040RteRepairTests(ITestOutputHelper output) [Fact, Trait("Suite", "Synthetic")] public void ExecutedRepairRetriesEveryValidationReadAfterCommittedThrowaways() => Audit(true); - private void Audit(bool chained) + [Fact, Trait("Suite", "Synthetic")] + public void RepairWithUntouchedIncomingTraceBoundariesScalar() => Audit(false, true); + + [Fact, Trait("Suite", "Synthetic")] + public void RepairWithUntouchedIncomingTraceBoundariesBatch() => Audit(false, true, true); + + [Fact, Trait("Suite", "Synthetic")] + public void RepairWithUntouchedIncomingTraceChainedScalar() => Audit(true, true); + + [Fact, Trait("Suite", "Synthetic")] + public void RepairWithUntouchedIncomingTraceChainedBatch() => Audit(true, true, true); + + private void Audit(bool chained, bool keepTrace = false, bool batch = false) { var bus = new SyntheticM68040RteValidationFaultTests.ValidationFaultBus(); var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); - var report = new CoverageBatch("68040", chained ? "rte-repair-chained" : "rte-repair-boundaries"); + var report = new CoverageBatch("68040", keepTrace ? $"rte-retry-trace-{(chained ? "chained" : "boundaries")}-{(batch ? "batch" : "scalar")}" + : chained ? "rte-repair-chained" : "rte-repair-boundaries"); foreach (var start in SupervisorBanks) foreach (var tail in chained ? SupervisorBanks : [start]) foreach (var middle in chained ? new[] { "none", "ISP", "MSP" } : ["none"]) foreach (var result in new[] { "user", "ISP", "MSP" }) - foreach (var incoming in chained ? new ushort[] { 0x8000 } : [0, 0x8000, 0x4000]) + foreach (var incoming in chained && !keepTrace ? new ushort[] { 0x8000 } : [0, 0x8000, 0x4000]) foreach (var restoredTrace in new ushort[] { 0, 0x8000, 0x4000 }) foreach (var alignment in chained ? new uint[] { 0, 1 } : [0]) foreach (var vbr in chained ? new uint[] { 0, 0x10000 } : [0x10000]) foreach (var ccr in chained ? new[] { 0, 31 } : Enumerable.Range(0, 32)) - foreach (var form in Forms) + foreach (var form in keepTrace ? Forms.Where(f => f is not ("CT" or "CU" or "CP")) : Forms) foreach (var (offset, width) in chained ? SyntheticM68040RteValidationFaultTests.Reads(form) : [(0u, 2)]) for (var faultByte = 0; faultByte < (chained ? width : 1); faultByte++) { var path = !chained ? new[] { start } : middle == "none" ? [start, tail] : new[] { start, middle, tail }; - Run(m, bus, report, path, result, incoming, restoredTrace, alignment, vbr, ccr, form, offset, width, faultByte); + Run(m, bus, report, path, result, incoming, restoredTrace, alignment, vbr, ccr, form, offset, width, faultByte, keepTrace, batch); } report.Complete(output); } private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTests.ValidationFaultBus bus, CoverageBatch report, string[] path, string result, ushort incoming, ushort restoredTrace, - uint alignment, uint vbr, int ccr, string form, uint offset, int width, int faultByte) + uint alignment, uint vbr, int ccr, string form, uint offset, int width, int faultByte, bool keepTrace, bool batch) { bus.Disarm(); m.Reset(ccr); var pointers = new Dictionary { ["user"] = 0x7800 + alignment, ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; @@ -80,12 +93,13 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes m.InitializePhysical(Operand, 0x89abcdef, 4); m.InitializePhysical(Operand + 4, 0x12345678, 4); m.Core.State.A[0] = 0x4300; // CM must use the saved EA, not this live base. // Fixed reference encodings: MOVE.W #imm,(xxx).L; MOVE.L #imm,(xxx).L; - // MOVE.W #imm,(A7); RTE. Clearing the saved incoming trace is an explicit - // handler action, so retry trace comes solely from the repaired frame. + // MOVE.W #imm,(A7); RTE. Legacy repair explicitly clears saved incoming + // trace. The retry-trace fixture does not store to the access frame SR. ushort[] handler = [0x33fc, savedSr, (ushort)(frame >> 16), (ushort)frame, 0x23fc, 0, (ushort)Target, (ushort)((frame + 2) >> 16), (ushort)(frame + 2), 0x33fc, (ushort)((repairedFormat << 12) | 8), (ushort)((frame + 6) >> 16), (ushort)(frame + 6), 0x3ebc, (ushort)(priorSr & ~0xc000), 0x4e73, 0x7e7e]; + if (keepTrace) handler = [.. handler.Take(13), 0x4e73, 0x7e7e]; for (var n = 0; n < handler.Length; n++) m.InitializePhysical(Handler + (uint)n * 2, handler[n], 2); m.InitializePhysical(PendingHandler, 0x4e73, 2); m.InitializePhysical(PendingHandler + 2, 0x7e7e, 2); m.InitializePhysical(vbr + 8, Handler, 4); @@ -108,13 +122,17 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes ExpectProvenance(e, sequence + 1, SyntheticMachine.Code, priorSr); SyntheticM68040RteValidationFaultTests.ExpectAccessFrame(m, e, accessFrame, priorSr, SyntheticMachine.Code, frame + offset, width); bus.Arm(frame + offset + (uint)faultByte); - var phases = new List { "fault-entry", "repair-SR", "repair-PC", "repair-format", "clear-incoming-trace", "handler-return", "retry-RTE" }; + var phases = new List { "fault-entry", "repair-SR", "repair-PC", "repair-format" }; + if (!keepTrace) phases.Add("clear-incoming-trace"); + phases.AddRange(["handler-return", "retry-RTE"]); + if (keepTrace && incoming != 0) phases.Add("retry-trace-return"); if (vector != 0) phases.Add("pending-return"); phases.Add("following"); - var id = $"68040/RTE/repair/{form}/path={string.Join('-', path)}/result={result}/incoming={incoming:X4}/T={restoredTrace:X4}/align={alignment}/VBR={vbr:X8}/read={offset}:{width}/fault-byte={faultByte}/op=4E73/ccr={ccr:X2}"; + var id = $"68040/RTE/{(keepTrace ? "retry-trace" : "repair")}/{form}/path={string.Join('-', path)}/result={result}/incoming={incoming:X4}/T={restoredTrace:X4}/align={alignment}/VBR={vbr:X8}/read={offset}:{width}/fault-byte={faultByte}/op=4E73/ccr={ccr:X2}"; var phase = 0; bool Step() { - var passed = SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/" + phases[phase++]); + var caseId = id + "/" + phases[phase++]; + var passed = batch ? BatchStep(m, e, report, caseId) : SyntheticM68040AccessFrameAuditTests.Step(m, e, report, caseId); if (!passed) while (phase < phases.Count) report.Record(id + "/" + phases[phase++], "untested", "Repair prerequisite failed"); return passed; } @@ -125,18 +143,18 @@ bool Step() foreach (var (at, value, bytes, next) in new[] { (frame, (uint)savedSr, 2, Handler + 8), (frame + 2, Target, 4, Handler + 18), (frame + 6, (uint)((repairedFormat << 12) | 8), 2, Handler + 26), - (accessFrame, (uint)(priorSr & ~0xc000), 2, Handler + 30) }) + (accessFrame, (uint)(priorSr & ~0xc000), 2, Handler + 30) }.Where(s => !keepTrace || s.Item1 != accessFrame)) { e.Write(at, value, bytes, m.Model); e.Sr = SyntheticExecution.MoveFlags(e.Sr, value, bytes); e.Pc = next; if (!Step()) return; } var repairWrites = bus.Accesses.Skip(repairStart).Where(a => a.Write).ToArray(); - e.ControlChecks["four exact repair writes"] = (_ => repairWrites.Length == 4 && + e.ControlChecks["exact repair writes"] = (_ => repairWrites.Length == (keepTrace ? 3 : 4) && repairWrites[0] == new BusAccess(frame, 2, true, savedSr, M68kBusAccessKind.CpuDataWrite) && repairWrites[1] == new BusAccess(frame + 2, 4, true, Target, M68kBusAccessKind.CpuDataWrite) && repairWrites[2] == new BusAccess(frame + 6, 2, true, (uint)((repairedFormat << 12) | 8), M68kBusAccessKind.CpuDataWrite) && - repairWrites[3] == new BusAccess(accessFrame, 2, true, (uint)(priorSr & ~0xc000), M68kBusAccessKind.CpuDataWrite) ? 1u : 0u, 1); - var resumeSr = (ushort)(priorSr & ~0xc000); + (keepTrace || repairWrites[3] == new BusAccess(accessFrame, 2, true, (uint)(priorSr & ~0xc000), M68kBusAccessKind.CpuDataWrite)) ? 1u : 0u, 1); + var resumeSr = keepTrace ? priorSr : (ushort)(priorSr & ~0xc000); pointers[tail] = frame; SetStacks(e, pointers, resumeSr); e.Pc = SyntheticMachine.Code; e.ExceptionVector = null; if (!Step()) return; var retryStart = bus.Accesses.Count; @@ -151,8 +169,19 @@ bool Step() ExpectException(m, e, pointers, savedSr, vector, Target, Operand, sequence + 2); if (form is "CU" or "CP") e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) == null ? 0u : 1u, 0); } + // MC68040UM 8.2.6: the suspended RTE is traced only once it completes. + // Both T1 and T0 trace RTE; the repaired frame's SR is saved in that + // trace frame, independently of the trace bits which caused entry. + var retryTrace = keepTrace && incoming != 0; + if (retryTrace) ExpectException(m, e, pointers, savedSr, 9, Target, SyntheticMachine.Code, sequence + 2); if (!Step()) return; e.ControlChecks.Remove("consumed throwaways not read by retry"); + e.ControlChecks.Remove("exact repair writes"); + if (retryTrace) + { + pointers[ExceptionBank(savedSr)] += 12; SetStacks(e, pointers, savedSr); e.Pc = Target; e.ExceptionVector = null; + if (!Step()) return; + } if (vector != 0) { pointers[ExceptionBank(savedSr)] += 12; SetStacks(e, pointers, savedSr); e.Pc = Target; e.ExceptionVector = null; @@ -161,7 +190,7 @@ bool Step() e.Pc = form == "CM" ? Target + 4 : Target; if (form == "CM") { e.D[0] = 0x89abcdef; e.D[1] = 0x12345678; } if (restoredTrace == 0x8000 || restoredTrace == 0x4000 && form != "CM") - ExpectException(m, e, pointers, savedSr, 9, e.Pc, Target, sequence + (vector != 0 ? 3u : 2u)); + ExpectException(m, e, pointers, savedSr, 9, e.Pc, Target, sequence + (vector != 0 ? 3u : retryTrace ? 3u : 2u)); Step(); } @@ -193,4 +222,25 @@ private static void ExpectException(SyntheticMachine m, ArchitecturalExpectation e.Write(at + 6, (uint)((vector >= 49 ? 3 : 2) << 12 | vector * 4), 2, m.Model); e.Write(at + 8, address, 4, m.Model); for (uint n = 0; n < 12; n++) e.MemoryMasks[at + n] = 255; } + + private static bool BatchStep(SyntheticMachine m, ArchitecturalExpectation e, CoverageBatch report, string id) + { + try + { + var boundary = new Boundary(); + var count = ((IM68kBatchCore)m.Core).ExecuteInstructions(1, m.Core.State.Cycles + 1000, boundary); + var mismatch = count != 1 || boundary.Before != 1 || boundary.After != 1 + ? $"Batch count/callbacks differ: {count}/{boundary.Before}/{boundary.After}" : e.Verify(m); + report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); + return mismatch == null; + } + catch (NotSupportedException ex) { report.Record(id, "unsupported", ex.Message); return false; } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); return false; } + } + private sealed class Boundary : IM68kInstructionBoundary + { + public int Before, After; + public bool BeforeInstruction() { Before++; return true; } + public void AfterInstruction(long previousCycle, long currentCycle) => After++; + } } diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 17030e5b..7d128128 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -4587,3 +4587,113 @@ its binary identity is not relabeled as the preceding checkpoint's binary. No new consumer validation or private/public package is generated for this test-only checkpoint; preceding unpublished `.63` evidence remains scoped to its own unchanged package and binary. Root CopperScreen work is untouched. + +## 040 preserved-trace RTE repair — 2026-10-05 + +This test-only checkpoint extends executed supervisor-tail repair without +changing production CPU code. Expectations follow +[MC68040UM](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) sections +8.2.6, 8.4.2 and 8.4.6.7: an access fault defers tracing until the suspended +instruction completes, incoming trace controls that completion, RTE is a T0 +flow change, and the trace frame saves the resulting SR and next PC. Synthetic +repair/continuation frames provide manual-derived software qualification; +there is no executed hardware reference for these combined scenarios. + +The real handler performs exactly three original-frame SR/PC/format stores. +It leaves the access frame's saved SR untouched. Its RTE restores the original +incoming trace state; the retried original RTE then completes the repaired +frame. T1/T0 produce a trace with the repaired SR/PC even when the repaired +SR clears trace. The trace-handler RTE returns to the target, whose following +BRA or MOVEM verifies restored trace, stacks and CM continuation delivery. +CM must retain its saved operand address across the intervening trace-handler +RTE, instead of consuming it there or using the live address register. + +| Group, each scalar/batch | Cases | Combinations | +| --- | ---: | ---: | +| Canonical CCR boundaries | 92,736 | 378 | +| Chained validation read bytes | 1,271,808 | 82,944 | + +The four groups total **2,729,088 phases**. Canonical direct SR-read faults +cover all 32 CCRs. Structural cases use CCR 0/31 and reject every byte of each +SR/PC/format/SSW/continuation-address validation read, with both alignments, +both VBRs and twelve one/two-throwaway supervisor paths. Each group crosses +incoming 0/T1/T0, restored 0/T1/T0 and restored user/ISP/MSP. Forms are 0/2/3, +invalid 4/15 repaired to 0 and normal/CM format 7. Consumed throwaway PCs are +never fetched; committed stack effects are preserved and retry cannot reread +their frames. Both public scalar and one-instruction batch execution verify +architectural state and unchanged-memory guards; batch count/callbacks are +also checked. + +The initial focused run in `artifacts/m6-rte-retry-trace-first/` executes all +four tests with zero skips, mismatches, unsupported execution or untested +phases. The earlier explicit-trace-clear repair groups keep their original +case identifiers/cardinalities and separate mutation selection. + +Pending CT/CU/CP interactions, the user-tail trace bridge and mixed trace +values across consumed throwaways remain required separately. This checkpoint +uses the same incoming trace value throughout each throwaway chain. Internal +restoration, chained odd-PC saved-SR provenance, general data/writeback/context +transfer and all broader model/reference/consolidation requirements remain +open. The 480-case remaining-protocol inventory continues to fail the complete +gate. Physical pipeline/cache timing, enabled MMU, FPU arithmetic and OS +compatibility remain outside this roadmap. No old regression is retired, +public API changed or package released; milestone 6 remains **in progress** +with `roadmapComplete=false`. + +The three maintained `RteRetryTrace` mutations in +`artifacts/m6-rte-retry-trace-mutations/` each execute all four complete reports +and require their intended phase and architectural diagnostic on both routes. +Counts below are per route, with boundary/chained values respectively: + +| Mutation | Mismatching | Dependent phases untested | Required diagnostic | +| --- | ---: | ---: | --- | +| Require restored trace bits | 2,688 / 36,864 | 5,376 / 73,728 | Incoming T1 with restored trace clear, retry PC | +| Omit RTE from T0 flow changes | 4,032 / 55,296 | 8,064 / 110,592 | Incoming T0 with restored trace clear, retry PC | +| Consume CM in trace-handler RTE | 1,152 / 24,192 | 0 / 0 | CM following instruction, D0 from saved EA | + +Mutations are restored byte-for-byte and the original source rebuilt before +acceptance. These failures prove the new checks discriminate the target defects; +they are not relabeled as production mismatches. + +The refreshed legacy `RteRepair` campaign in +`artifacts/m6-rte-retry-trace-legacy-mutations/` still executes exactly its two +original reports (143,424 and 768,960 cases). All three retry-PC, saved MOVEM EA +and pending saved-SR mutations are detected at the required retry/following +phase. Its explicit method filter preserves that selection after adding the +four new facts to the shared fixture class. + +Acceptance validation finished on 2026-10-06. Fresh full Release execution in +`artifacts/m6-rte-retry-trace-full/` passes **5,073 tests, zero failures and +eleven optional skips**. All nine qualified WinUAE presets match their pinned +directory/callback/frame counts and current CPU/adapter identities, recorded +in `qualified-preset-identities.json`. Optional unavailable coverage remains +separate from these executed reference selections. + +The dedicated `artifacts/m6-rte-retry-trace-audit/` run executes **48 tests: +47 passing and the required inventory failing**, with no skips. All +**7,496,000 executable cases pass**; there are zero mismatches/unsupported +cases and **480 untested requirements**. All 42 reports, six fixed examples, +thirteen fixture/command identities and independently enumerated combinations +validate before the inventory rejection. All **17** malformed-report/identity +controls in `artifacts/m6-rte-retry-trace-controls/` reject their specific +missing, shortened, foreign, redistributed or omitted-fixture corruption; +the generic inventory failure cannot satisfy a negative control. + +CPU assembly SHA-256 is +`21a9f11e4bdcad46880db2643a04e6997e2adf172e6d3edbc799b68b18956dd2`; +test/reference adapter is +`e800f3f49757dbc39bb3b972d63f11eb92a1b0839a9d0d04e5ee4fbde59b4445`. +The audit manifest records starting commit `32761cc`, its committed CPU tree +and actual current source/fixture hashes. Production source is unchanged from +that commit; the newly built binary is recorded under its own identity. +No consumer replay or private/public package is generated for this test-only +checkpoint. Prior unpublished `.63` consumer evidence remains scoped to its +own package and binary. Unrelated root CopperScreen changes remain untouched. + +The final strict gate in `artifacts/m6-rte-retry-trace-strict.log` passes +**22,464,662 logical cases / 632 reporting batches**. Fresh pinned +SingleStepTests passes **312,500 cases / 125 files** and Musashi passes +**536 programs with 88 explicit exclusions**. Both reference tests execute +without skips; their source pins and software-reference limits remain +unchanged. The full-run optional skips are not credited as reference coverage. +The ordinary summary retains `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index d3af08be..42546346 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1707,3 +1707,43 @@ untested requirements and failing completion. All three mutations and seventeen report/identity controls detect their intended failures. No consumer or package change is needed for this test-only checkpoint; preceding `.63` consumer evidence remains historical evidence of its own qualified binary. + +### Milestone 6 preserved incoming trace after RTE repair — 2026-10-05 + +Four ordinary scalar/batch reports add **2,729,088 phases** for supervisor-tail +repair that leaves the access frame's saved SR untouched. The real handler +executes exactly three original-frame stores. Its return restores incoming +T1/T0, and the original RTE traces on completion using repaired SR/PC even +when its restored SR clears trace. An intervening trace-handler return and +the following instruction verify that CM retains its saved MOVEM address. +Expectations follow MC68040UM trace and RTE rules, with software fixtures rather +than an executed hardware oracle. + +Canonical faults cover all CCRs. Structural faults cover every validation-read +byte, twelve supervisor throwaway paths, both alignments/VBRs and all incoming/ +restored trace and restored stack combinations. Forms are 0/2/3, invalid 4/15 +repaired to 0 and normal/CM format 7. Three maintained `RteRetryTrace` mutations +target restored-bit trace gating, omitted T0 RTE classification and CM consumed +by the trace handler. The complete 040 gate requires 48 tests, 42 reporting +batches, six fixed examples and thirteen input identities, checking each +combination independently. + +Pending CT/CU/CP, user-tail trace bridge and mixed trace values within throwaway +chains remain required separately, along with the earlier internal-restoration, +chained odd-PC, general data/writeback/context-transfer and broader model/ +reference/consolidation work. The 480-case inventory remains required and +fails completion. Production CPU source is unchanged; no public API/package +change or regression retirement is included. Milestone 6 stays **in progress**, +`roadmapComplete=false`. See the +[qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040-preserved-trace-rte-repair--2026-10-05). + +Fresh validation completed on 2026-10-06: **5,073** full Release CPU tests pass, +with eleven optional skips and zero failures. All nine pinned WinUAE selections +match their exact counts and CPU/adapter identities. The strict ordinary gate +passes **22,464,662 cases / 632 batches**; fresh SingleStepTests passes 312,500 +cases / 125 files and Musashi passes 536 programs with 88 exclusions. All three +new mutations, three refreshed legacy repair mutations and seventeen new +report/identity controls detect their intended defects. The complete 040 run +has 7,496,000 passing cases, zero mismatches/unsupported execution and the +retained 480 untested requirements; it continues to fail completion. No new +consumer or package validation is needed for this test-only checkpoint. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index a27913f2..441f491f 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -31,6 +31,10 @@ $expected = [ordered]@{ 'access-fault-batch-dispatch' = @{cases=10368; combinations=5184} 'rte-repair-boundaries' = @{cases=143424; combinations=540} 'rte-repair-chained' = @{cases=768960; combinations=45792} + 'rte-retry-trace-boundaries-scalar' = @{cases=92736; combinations=378} + 'rte-retry-trace-boundaries-batch' = @{cases=92736; combinations=378} + 'rte-retry-trace-chained-scalar' = @{cases=1271808; combinations=82944} + 'rte-retry-trace-chained-batch' = @{cases=1271808; combinations=82944} 'instruction-fault-frame' = @{cases=36864; combinations=576} 'instruction-fault-restart' = @{cases=79872; combinations=624} 'handler-prefetch-entry-scalar' = @{cases=196608; combinations=6144} @@ -81,9 +85,9 @@ if (-not $ValidateReportsOnly) { 'Multiple continuation bits are architecturally undefined and excluded', 'Selected active accurate-batch paths verify counts/callbacks and scalar/batch bus/cycle policy; generic short operand frames do not qualify architectural format-7 data restart', 'Instruction-fault fixtures use cache-disabled accurate execution and physical-map rejection; speculative deferral, enabled caches/MMU and compiled fetch PC provenance remain unqualified', - 'Executed supervisor-tail repair clears saved incoming trace; user-tail repair explicitly sets S and builds a new throwaway bridge. Untouched incoming-trace retry remains required', + 'Legacy supervisor repair clears saved trace; normal/CM retry-trace fixtures preserve it through handler return and trace the completed RTE. User/pending/mixed-epoch incoming-trace protocols remain required', 'User-tail fault expectations compose documented throwaway live-SR rules with general supervisor exception entry; unusual combined hardware behavior has not been observed', - 'User-tail validation, internal-restoration double faults, cache/MMU/compiled handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', + 'User-tail untouched-trace retry, internal-restoration double faults, cache/MMU/compiled handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', 'Direct odd-RTE saved-SR ordering uses documentary WinUAE 5d22d336, not an executed hardware oracle') } $identity | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'identities.json') @@ -111,8 +115,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 44 -or [int]$counters.total -ne 44 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (38 batches, 6 fixed examples)' +if ([int]$counters.executed -ne 48 -or [int]$counters.total -ne 48 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (42 batches, 6 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -212,16 +216,17 @@ foreach ($group in $expected.Keys) { } } } } } - } elseif ($group -in @('rte-repair-boundaries','rte-repair-chained')) { - $chained = $group -eq 'rte-repair-chained' + } elseif ($group -in @('rte-repair-boundaries','rte-repair-chained') -or $group.StartsWith('rte-retry-trace-', [StringComparison]::Ordinal)) { + $keepTrace = $group.StartsWith('rte-retry-trace-', [StringComparison]::Ordinal) + $chained = $group -eq 'rte-repair-chained' -or $group.StartsWith('rte-retry-trace-chained-', [StringComparison]::Ordinal) $paths = if ($chained) { @('ISP-ISP','ISP-ISP-ISP','ISP-MSP-ISP','ISP-MSP','ISP-ISP-MSP','ISP-MSP-MSP', 'MSP-ISP','MSP-ISP-ISP','MSP-MSP-ISP','MSP-MSP','MSP-ISP-MSP','MSP-MSP-MSP') } else { @('ISP','MSP') } foreach ($path in $paths) { foreach ($result in @('user','ISP','MSP')) { - foreach ($incoming in $(if ($chained) { @(0x8000) } else { @(0,0x8000,0x4000) })) { + foreach ($incoming in $(if ($chained -and -not $keepTrace) { @(0x8000) } else { @(0,0x8000,0x4000) })) { foreach ($trace in @(0,0x8000,0x4000)) { foreach ($alignment in $(if ($chained) { @(0,1) } else { @(0) })) { foreach ($vbr in $(if ($chained) { @(0,0x10000) } else { @(0x10000) })) { - foreach ($form in @('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP')) { + foreach ($form in $(if ($keepTrace) { @('format0','format2','format3','invalid4','invalid15','normal','CM') } else { @('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP') })) { # Literal read ranges are independent of the fixture iterator. $reads = @('0:2') if ($chained) { @@ -232,8 +237,8 @@ foreach ($group in $expected.Keys) { foreach ($read in $reads) { $bytes = if ($chained) { [int]$read.Split(':')[1] } else { 1 } for ($byte=0; $byte -lt $bytes; $byte++) { - $key = '68040/RTE/repair/{0}/path={1}/result={2}/incoming={3:X4}/T={4:X4}/align={5}/VBR={6:X8}/read={7}/fault-byte={8}' -f $form,$path,$result,$incoming,$trace,$alignment,$vbr,$read,$byte - $phases = if ($form -in @('CT','CU','CP')) { 9 } else { 8 } + $key = '68040/RTE/{0}/{1}/path={2}/result={3}/incoming={4:X4}/T={5:X4}/align={6}/VBR={7:X8}/read={8}/fault-byte={9}' -f $(if ($keepTrace) {'retry-trace'} else {'repair'}),$form,$path,$result,$incoming,$trace,$alignment,$vbr,$read,$byte + $phases = if ($keepTrace) { $(if ($incoming -eq 0) {7} else {8}) } elseif ($form -in @('CT','CU','CP')) { 9 } else { 8 } $ccrs = if ($chained) { 2 } else { 32 } $expectedCombinations[$key] = $phases * $ccrs } @@ -422,8 +427,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 44 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 48 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=4767392; xunitBatches=38; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=7496480; xunitBatches=42; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index 6031bc2b..6fdec0fa 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','UserRteFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','RteRetryTrace','UserRteFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -323,6 +323,24 @@ foreach ($mutation in $repairMutations) { $mutation.group='SyntheticM68040RteRepairTests'; $mutation.model='68040' $mutation.milestone=6; $mutation.repair=$true; $mutations += $mutation } +$retryTraceMutations = @( + @{name='040-retry-trace-restored-bits'; file=$advanced; + before=' if ((trace & 0x8000) != 0 || flow || exception)'; + after=' if ((State.StatusRegister & 0xc000) != 0 && ((trace & 0x8000) != 0 || flow || exception))'; + proofCombination='/incoming=8000/T=0000/'; proofPhase='/retry-RTE'; proofReason='PC expected'}, + @{name='040-retry-trace-t0-rte'; file=$advanced; + before='traceOpcode is 0x4E72 or 0x4E73 or 0x4E74 or 0x4E75 or 0x4E77 or 0x007C or 0x027C or 0x0A7C ||'; + after='traceOpcode is 0x4E72 or 0x4E74 or 0x4E75 or 0x4E77 or 0x007C or 0x027C or 0x0A7C ||'; + proofCombination='/incoming=4000/T=0000/'; proofPhase='/retry-RTE'; proofReason='PC expected'}, + @{name='040-retry-trace-movem-handler-consumption'; file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs'; + before=' if (_m68040MovemContinuation is not { } saved || saved.Pc != State.ProgramCounter) return false;'; + after=' if (_m68040MovemContinuation is not { } saved) return false;'; + proofCombination='/retry-trace/CM/'; proofPhase='/following'; proofReason='D0 expected'} +) +foreach ($mutation in $retryTraceMutations) { + $mutation.group='RepairWithUntouchedIncomingTrace'; $mutation.model='68040'; $mutation.milestone=6 + $mutation.retryTrace=$true; $mutations += $mutation +} $instructionMutations = @( @{name='040-instruction-short-frame'; before=' RaiseUnbufferedReadAccessFault(fault, stackedProgramCounter, instruction: true);'; after=' RaiseFormat0Exception(VectorBusError, stackedProgramCounter, M68kInstructionTimingKey.IllegalInstruction);'; proofCombination='/instruction/opcode/'}, @@ -391,6 +409,7 @@ if ($Scope -eq 'Consolidation') { $mutations = @($mutations | Where-Object { $_. if ($Scope -eq 'Rte040') { $mutations = @($mutations | Where-Object { $_.odd }) } if ($Scope -eq 'RteValidationFault') { $mutations = @($mutations | Where-Object { $_.validationFault }) } if ($Scope -eq 'RteRepair') { $mutations = @($mutations | Where-Object { $_.repair }) } +if ($Scope -eq 'RteRetryTrace') { $mutations = @($mutations | Where-Object { $_.retryTrace }) } if ($Scope -eq 'InstructionFault') { $mutations = @($mutations | Where-Object { $_.instructionFault }) } if ($Scope -eq 'HandlerPrefetch') { $mutations = @($mutations | Where-Object { $_.handlerPrefetch }) } if ($Scope -eq 'EntryPrefetch') { $mutations = @($mutations | Where-Object { $_.entryPrefetch }) } @@ -422,6 +441,11 @@ try { [IO.File]::WriteAllText($path, $text.Replace($before, $after), [Text.UTF8Encoding]::new($false)) $model = $(if ($mutation.model) { $mutation.model } elseif ($mutation.name -eq '060-divide-frame') { '68060' } else { '68020' }) $filter = "FullyQualifiedName~$($mutation.group)&DisplayName~$model" + if ($mutation.repair) { + # Keep the original repair proof's two complete batches; the + # new retained-trace facts have their own four-route gate. + $filter = 'FullyQualifiedName~ExecutedRepairRestoresAllCcrAndTraceStates|FullyQualifiedName~ExecutedRepairRetriesEveryValidationReadAfterCommittedThrowaways' + } $legacyPresent = $mutation.legacyTest -and [IO.File]::ReadAllText((Join-Path $repo $mutation.legacyFile)).Contains("void $($mutation.legacyTest)(") if ($legacyPresent) { $filter += "|FullyQualifiedName~$($mutation.legacyTest)" @@ -502,6 +526,22 @@ try { throw 'Instruction-fault mutation omitted complete cases or intended semantic failure' } } + if ($mutation.retryTrace) { + [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + if ($trx.TestRun.ResultSummary.Counters.executed -ne 4 -or $trx.TestRun.ResultSummary.Counters.failed -ne 4 -or $batches.Count -ne 4) { + throw 'Retry-trace mutation omitted its complete four-batch selection' + } + foreach ($matrix in @('boundaries','chained')) { + foreach ($route in @('scalar','batch')) { + $target = @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq "rte-retry-trace-$matrix-$route" }) + if ($target.Count -ne 1 -or $target[0].logicalCases -ne $(if ($matrix -eq 'boundaries') {92736} else {1271808}) -or + @($target[0].failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and + $_.id.EndsWith($mutation.proofPhase, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) }).Count -eq 0) { + throw "Retry-trace mutation omitted intended $matrix/$route semantic failure" + } + } + } + } if ($mutation.repair) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if ($trx.TestRun.ResultSummary.Counters.executed -ne 2 -or $batches.Count -ne 2 -or @@ -564,6 +604,9 @@ try { } elseif ($mutation.entryPrefetch) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] + } elseif ($mutation.retryTrace) { + @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and + $_.id.EndsWith($mutation.proofPhase, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] } elseif ($mutation.userRte) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.id.EndsWith('/fault-entry', [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 2869e10a..0cc72fbf 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -152,6 +152,10 @@ try { $expected['instruction-fault-restart'] = 79872 $expected['rte-repair-boundaries'] = 143424 $expected['rte-repair-chained'] = 768960 + $expected['rte-retry-trace-boundaries-scalar'] = 92736 + $expected['rte-retry-trace-boundaries-batch'] = 92736 + $expected['rte-retry-trace-chained-scalar'] = 1271808 + $expected['rte-retry-trace-chained-batch'] = 1271808 $expected['rte-validation-batch'] = 129024 $expected['access-fault-batch-dispatch'] = 10368 $expected['rte-access-entry-double-fault'] = 98304 @@ -283,8 +287,9 @@ try { m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; m68040HandlerPrefetchScope=@{logicalCases=457728; batches=8; routes=@('accurate scalar','accurate batch'); bus='Physical map with no host code reader, including stable-map generation'; qualified='Four-long access-error entry window, every entry byte, odd handler addresses, retained data, flow/host/task/map invalidation and later instruction faults; denied cold boundaries perform no CPU fetch/state change'; remaining='Other exception-entry paths, host-reader/compiled fetch provenance, enabled caches/MMU, ordinary speculative deferral and physical timing'}; m68040UserTailScope=@{logicalCases=1687040; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); authority='Composition of MC68040UM 2.2.2.1, 8.1, 8.2.1/5, 8.4.2 and 8.4.6.7; software qualification, no executed hardware reference'; userM=@(0,1); initialBanks=@('ISP','MSP'); middleBanks=@('none','user','ISP','MSP'); ccr='all 32 canonical SR reads, 0/31 structural reads'; forms='0/2/3, invalid 4/15, normal/CM/CT/CU/CP49'; qualified='Live intermediate SR, supervisor exception stack selected by M, user-data TM=1, preserved USP frame and committed throwaways, bare return followed by privilege violation, executed seven-store repair with fresh throwaway bridge and following trace/continuation'; remaining='Internal restoration faults, untouched trace retry, physical bus-order/timing and hardware observation of composed user-tail fault behavior'}; - m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, executed repair/retry after committed supervisor throwaways, and manual-derived user-tail fault/privilege/bridge repair; chained odd-PC saved-SR provenance, internal-restoration faults, untouched-trace retry, CP context transfer, other real fault entry and physical timing remain unqualified'; - m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='Internal restoration, untouched incoming-trace retry and other real access faults; user-tail repair is qualified separately'}; + m68040RteRetryTraceScope=@{logicalCases=2729088; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); faultTailBanks=@('ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredBanks=@('user','ISP','MSP'); restoredTrace=@(0,0x8000,0x4000); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM'; qualified='Three original-frame repair stores leave saved access SR untouched; handler return restores incoming trace; successful original RTE triggers T1/T0 trace with restored SR/PC; trace-handler return and following instruction retain MOVEM EA and stack selection'; ccr='all 32 canonical, 0/31 every chained read byte'; remaining='Pending CT/CU/CP interactions, user-tail trace bridge, mixed trace values within consumed throwaways, internal restoration and hardware/physical timing'}; + m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, executed repair/retry after committed supervisor throwaways, and manual-derived user-tail fault/privilege/bridge repair; chained odd-PC saved-SR provenance, internal-restoration faults, pending/user/mixed-epoch untouched-trace retry, CP context transfer, other real fault entry and physical timing remain unqualified'; + m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='Internal restoration, pending/user/mixed-epoch untouched incoming-trace retry and other real access faults; normal/CM preserved-trace and user-tail repair are qualified separately'}; m68040InstructionFaultScope=@{logicalCases=116736; batches=2; translationEnabled=$false; instructionCacheEnabled=$false; engine='accurate public factory plus cached self-branch batch'; forms=@('opcode','extension-low','next-opcode','self-branch'); banks=@('user','ISP','MSP'); ccr='all 32'; trace='All defined incoming states for frame/return; restart trace zero'; fields='Format7 SR, executing PC distinct from prefetch FA, RW/SIZE/TM and invalid writebacks'; phases=@('fault entry','handler return','restart','following'); remaining='Data writebacks/restart, speculative prefetch deferral, compiled instruction-fault PC provenance, cache-enabled fetches and physical timing'}; m68040RtePhysicalValidationScope=@{logicalCases=223872; batches=2; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); trace=@(0,0x8000,0x4000); alignment=@(0,1); vbr=@(0,0x10000); faults='Each byte of SR/PC/format/SSW/continuation-EA reads; 14 direct/one/two-throwaway supervisor paths'; phases=@('fault entry','handler return'); undefined='EA and invalid writeback/push data; SSW X'; remaining='Internal-restoration/double faults, other instruction faults and software writeback handlers; user-tail validation is qualified separately'}; m68040AccessDoubleFaultScope=@{logicalCases=105408; batches=5; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; dispatchCcr=@(0,31); engines=@('accurate','classic','V2'); faults='Every byte of format-7 stack/vector; later handler validation refaults; warmed RTE fallback and compiled B/W/L operand read/write side exits'; recovery='External reset, including restored host entry; interrupt, task/subroutine entry and halted batches remain inactive'; undefined='Partial stack write order, contents and exact SP'; remaining='Internal restoration, other handler-entry paths and general architectural access-fault restart'}; From b59300c8b3c1f9c7560fa245deddda3fadba7a79 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 01:02:06 +0300 Subject: [PATCH 051/211] Qualify pending 68040 delivery with preserved trace --- Copper68k.Tests/Synthetic/README.md | 31 +++++ .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- .../SyntheticM68040RteRepairTests.cs | 32 ++++-- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 106 ++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 43 +++++++ scripts/test-copper68k-040-access-frames.ps1 | 29 +++-- .../test-copper68k-synthetic-mutations.ps1 | 30 ++++- scripts/test-copper68k-synthetic.ps1 | 11 +- 8 files changed, 255 insertions(+), 29 deletions(-) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 43020868..fab127e2 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -583,3 +583,34 @@ reports, six fixed examples and thirteen input identities. Its retained 480-case remaining-protocol inventory still fails completion. No production CPU change, package publication or regression retirement is included. See the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-preserved-trace-rte-repair--2026-10-05). + +### Pending-exception delivery with preserved incoming trace + +Four `PendingRepairWithPreservedIncomingTrace` reports add 1,824,768 phases +for CT/CU/CP49 after supervisor-tail repair. The handler preserves the access +frame's SR. The original RTE must deliver the pending exception without an +extra automatic trace of that RTE; repaired SR/PC/EA, format/vector and pending +consumption remain visible to its handler. A bare pending-handler RTE returns, +and the following BRA obeys the repaired trace bits. This fixture does not +emulate the pending handler's software service of the original trace. + +Canonical cases cover all CCRs; chained cases reject every validation-read byte +with CCR 0/31. Both routes cross all incoming/restored traces and restored +stacks, twelve supervisor throwaway paths and both alignments/VBRs. The three +continuation forms have separate case identifiers. CP vectors other than 49, +software trace service, user-tail trace bridges and mixed-epoch trace provenance +are not qualified by these new groups. Existing untraced CP-vector coverage +remains separate. + +```powershell +./scripts/test-copper68k-synthetic-mutations.ps1 -Scope RtePendingTrace -OutputDirectory artifacts/rte-pending-trace-mutations +``` + +The complete 040 gate now requires 52 tests, 46 reports, six fixed examples and +thirteen input identities. Its independent iterator checks every combination +and cardinality. The required 480-case inventory still prevents completion. +See the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-pending-delivery-with-preserved-trace--2026-10-06). + +The three result states are user M=0, ISP M=0 and MSP M=1. Restored user M=1 +is not covered here; its exception-bank selection remains a required separate +qualification, alongside the pending software trace-service and other gaps. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 34c14939..f5f5f8a1 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -33,7 +33,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() "real-access-fault-entry", "writeback-handler", "CP-context-transferred-vector" }) report.Record($"68040/RTE/format7/{form}/bank={bank}/op=4E73/ccr={ccr:X2}", "untested", form == "frame-validation-fault" - ? "Supervisor/user-tail validation and normal/CM preserved-trace retry have separate software coverage; internal restoration and pending/user/mixed-epoch trace protocols remain required. Physical/hardware behavior is separately unqualified" + ? "Supervisor/user-tail validation and normal/CM and pending CT/CU/CP preserved-trace retry have separate software coverage; internal restoration and user/mixed-epoch trace protocols remain required. Physical/hardware behavior is separately unqualified" : form == "real-access-fault-entry" ? "Cache-disabled accurate instruction-fetch frames/restart are covered separately; data fault/writeback restart, enabled-cache deferral and compiled instruction-PC provenance remain required" : "Required fault/context protocol has no independently qualified execution fixture yet"); diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs index e647fce0..7ab224bb 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs @@ -32,11 +32,24 @@ public sealed class SyntheticM68040RteRepairTests(ITestOutputHelper output) [Fact, Trait("Suite", "Synthetic")] public void RepairWithUntouchedIncomingTraceChainedBatch() => Audit(true, true, true); - private void Audit(bool chained, bool keepTrace = false, bool batch = false) + [Fact, Trait("Suite", "Synthetic")] + public void PendingRepairWithPreservedIncomingTraceBoundariesScalar() => Audit(false, true, false, true); + + [Fact, Trait("Suite", "Synthetic")] + public void PendingRepairWithPreservedIncomingTraceBoundariesBatch() => Audit(false, true, true, true); + + [Fact, Trait("Suite", "Synthetic")] + public void PendingRepairWithPreservedIncomingTraceChainedScalar() => Audit(true, true, false, true); + + [Fact, Trait("Suite", "Synthetic")] + public void PendingRepairWithPreservedIncomingTraceChainedBatch() => Audit(true, true, true, true); + + private void Audit(bool chained, bool keepTrace = false, bool batch = false, bool pendingTrace = false) { var bus = new SyntheticM68040RteValidationFaultTests.ValidationFaultBus(); var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); - var report = new CoverageBatch("68040", keepTrace ? $"rte-retry-trace-{(chained ? "chained" : "boundaries")}-{(batch ? "batch" : "scalar")}" + var traceGroup = pendingTrace ? "rte-pending-trace" : "rte-retry-trace"; + var report = new CoverageBatch("68040", keepTrace ? $"{traceGroup}-{(chained ? "chained" : "boundaries")}-{(batch ? "batch" : "scalar")}" : chained ? "rte-repair-chained" : "rte-repair-boundaries"); foreach (var start in SupervisorBanks) foreach (var tail in chained ? SupervisorBanks : [start]) @@ -47,19 +60,19 @@ private void Audit(bool chained, bool keepTrace = false, bool batch = false) foreach (var alignment in chained ? new uint[] { 0, 1 } : [0]) foreach (var vbr in chained ? new uint[] { 0, 0x10000 } : [0x10000]) foreach (var ccr in chained ? new[] { 0, 31 } : Enumerable.Range(0, 32)) - foreach (var form in keepTrace ? Forms.Where(f => f is not ("CT" or "CU" or "CP")) : Forms) + foreach (var form in Forms.Where(f => pendingTrace ? f is "CT" or "CU" or "CP" : !keepTrace || f is not ("CT" or "CU" or "CP"))) foreach (var (offset, width) in chained ? SyntheticM68040RteValidationFaultTests.Reads(form) : [(0u, 2)]) for (var faultByte = 0; faultByte < (chained ? width : 1); faultByte++) { var path = !chained ? new[] { start } : middle == "none" ? [start, tail] : new[] { start, middle, tail }; - Run(m, bus, report, path, result, incoming, restoredTrace, alignment, vbr, ccr, form, offset, width, faultByte, keepTrace, batch); + Run(m, bus, report, path, result, incoming, restoredTrace, alignment, vbr, ccr, form, offset, width, faultByte, keepTrace, batch, pendingTrace); } report.Complete(output); } private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTests.ValidationFaultBus bus, CoverageBatch report, string[] path, string result, ushort incoming, ushort restoredTrace, - uint alignment, uint vbr, int ccr, string form, uint offset, int width, int faultByte, bool keepTrace, bool batch) + uint alignment, uint vbr, int ccr, string form, uint offset, int width, int faultByte, bool keepTrace, bool batch, bool pendingTrace) { bus.Disarm(); m.Reset(ccr); var pointers = new Dictionary { ["user"] = 0x7800 + alignment, ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; @@ -125,9 +138,9 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes var phases = new List { "fault-entry", "repair-SR", "repair-PC", "repair-format" }; if (!keepTrace) phases.Add("clear-incoming-trace"); phases.AddRange(["handler-return", "retry-RTE"]); - if (keepTrace && incoming != 0) phases.Add("retry-trace-return"); + if (keepTrace && incoming != 0 && vector == 0) phases.Add("retry-trace-return"); if (vector != 0) phases.Add("pending-return"); phases.Add("following"); - var id = $"68040/RTE/{(keepTrace ? "retry-trace" : "repair")}/{form}/path={string.Join('-', path)}/result={result}/incoming={incoming:X4}/T={restoredTrace:X4}/align={alignment}/VBR={vbr:X8}/read={offset}:{width}/fault-byte={faultByte}/op=4E73/ccr={ccr:X2}"; + var id = $"68040/RTE/{(pendingTrace ? "pending-trace" : keepTrace ? "retry-trace" : "repair")}/{form}/path={string.Join('-', path)}/result={result}/incoming={incoming:X4}/T={restoredTrace:X4}/align={alignment}/VBR={vbr:X8}/read={offset}:{width}/fault-byte={faultByte}/op=4E73/ccr={ccr:X2}"; var phase = 0; bool Step() { @@ -172,7 +185,10 @@ bool Step() // MC68040UM 8.2.6: the suspended RTE is traced only once it completes. // Both T1 and T0 trace RTE; the repaired frame's SR is saved in that // trace frame, independently of the trace bits which caused entry. - var retryTrace = keepTrace && incoming != 0; + // MC68040UM 8.3: pending CT/CU/CP processing wins; no extra automatic + // trace of RTE may obscure the converted exception frame. CU/CP trace + // emulation belongs to software, which sees the repaired saved SR. + var retryTrace = keepTrace && incoming != 0 && vector == 0; if (retryTrace) ExpectException(m, e, pointers, savedSr, 9, Target, SyntheticMachine.Code, sequence + 2); if (!Step()) return; e.ControlChecks.Remove("consumed throwaways not read by retry"); diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 7d128128..4a3ce962 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -4697,3 +4697,109 @@ SingleStepTests passes **312,500 cases / 125 files** and Musashi passes without skips; their source pins and software-reference limits remain unchanged. The full-run optional skips are not credited as reference coverage. The ordinary summary retains `roadmapComplete=false`. + +## 040 pending delivery with preserved trace — 2026-10-06 + +This checkpoint extends the real three-store supervisor-tail repair fixture +to CT/CU/CP49 with the access frame's saved SR untouched. Expectations use +[MC68040UM](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) 8.3, +8.4.6.2 and 8.4.6.7: RTE immediately delivers the indicated pending exception; +CU/CP take precedence over tracing, leaving software to inspect saved trace +bits. CT already delivers its pending trace. An extra automatic trace of the +converting RTE must not obscure that frame. These are manual-derived synthetic +contexts, not observed hardware faults or qualification of FPU arithmetic. + +The handler changes the original frame's SR/PC/format with exactly three real +MOVE stores. Its return restores incoming 0/T1/T0. The retried original RTE +converts the repaired pending frame, preserving repaired SR/PC/EA and using +format 2 for CT/CU or format 3 for CP49. Its handler sees the original pending +vector and CU/CP context consumption. Incoming trace cannot cause a second +automatic exception at this boundary. A bare pending-handler RTE then returns; +the following self-BRA traces according to the repaired saved SR. + +| Group, each scalar/batch | Cases | Combinations | +| --- | ---: | ---: | +| Canonical CCR boundaries | 41,472 | 162 | +| Chained validation read bytes | 870,912 | 54,432 | + +All four reports total **1,824,768 phases**. The canonical SR-read case covers +all 32 CCRs. Structural cases use CCR 0/31, every rejected validation-read byte, +twelve supervisor paths, both alignments/VBRs and all incoming/restored trace +and restored user/ISP/MSP combinations. No consumed throwaway may be replayed +or its discarded PC fetched. Exact repair writes, preserved architectural +state/memory and public batch counts/callbacks remain checked. The initial +focused run in `artifacts/m6-pending-trace-first/` passes all four tests with +zero skips, mismatches, unsupported execution or untested phases. + +The pending handler here deliberately performs a bare RTE. Its required +software service of the original trace is not claimed as executed coverage; +the saved trace bits needed by that service are checked independently. Other +CP vectors retain their earlier distinct continuation coverage but are not +qualified by this new preserved-trace repair matrix. User-tail trace bridges, +mixed-epoch trace provenance, internal restoration, chained odd-PC SR, +general data/writeback/context-transfer and all earlier model/reference/ +consolidation requirements remain open. The 480-case inventory continues to +fail the complete gate. Enabled MMU, FPU arithmetic, physical pipeline/cache +timing and OS compatibility remain outside this roadmap. No regression is +retired, API changed or package released; milestone 6 remains **in progress** +with `roadmapComplete=false`. + +Three maintained `RtePendingTrace` mutations in +`artifacts/m6-pending-trace-mutations/` each execute all four complete reports. +The proof requires the intended retry/following phase and architectural +diagnostic in both routes and both matrices. Counts below are per route, +with canonical/chained values respectively: + +| Mutation | Mismatching | Dependent phases untested | Required diagnostic | +| --- | ---: | ---: | --- | +| Extra automatic RTE trace | 3,456 / 72,576 | 6,912 / 145,152 | CT retry, extra frame changes A7 | +| Stack incoming instead of repaired SR | 5,184 / 96,768 | 10,368 / 193,536 | CT retry, saved-frame memory | +| Suppress following self-BRA trace | 3,456 / 72,576 | 0 / 0 | Incoming/restored T1, following PC | + +Production source bytes are restored and rebuilt before acceptance. The new +pending scope shares the validator with the existing normal/CM trace scope; +each retains its own four-report cardinalities and intended proof identifiers. +No historical failing mutation result is presented as a production mismatch. + +The dedicated run in `artifacts/m6-pending-trace-audit/` executes **52 tests: +51 pass and the required inventory fails**, with no skips. All **9,320,768 +executable cases pass**, with zero mismatches/unsupported execution and **480 +untested requirements**. All 46 reports, six fixed examples, thirteen input +identities and independently enumerated combinations validate before the +inventory rejection. This gate remains failed; the new software qualification +does not complete milestone 6. + +The manifest records starting commit `b884aee`, committed CPU tree +`795fd12d6c0237a22a5f92f4a96cc4823e0364c1` and current source/fixture hashes. +CPU assembly SHA-256 is +`82bc2ed7607ab5c99006a65dc5f32a310730b8abd458a0acf7189c8dc6451e5d`; +test/reference adapter is +`2b72c0dc66b33c3f79e26aceebf384b2e40f7c3ea81be496dce183df69de34ce`. +Production source is unchanged from that commit; this newly built binary has +its own recorded identity. No new consumer replay or private/public package +is generated for this test-only checkpoint. Prior unpublished `.63` consumer +evidence remains scoped to its own package/binary; root CopperScreen work is +untouched. + +Fresh full Release validation in `artifacts/m6-pending-trace-full/` passes +**5,077 tests with zero failures and eleven optional skips**. All nine +qualified WinUAE presets match the pinned exact selections and current +CPU/adapter identities in `qualified-preset-identities.json`. Their executed +coverage is separate from optional unavailable tests. Historical Basic +mismatches/unsupported groups retain their original status. + +The strict ordinary gate in `artifacts/m6-pending-trace-strict.log` passes +**24,289,430 logical cases / 636 batches**. Fresh pinned SingleStepTests passes +**312,500 cases / 125 files** and Musashi passes **536 programs with 88 explicit +exclusions**. Both fresh reference tests execute without skips; their pinned +sources and software-reference caveats remain unchanged. All **17** controls +in `artifacts/m6-pending-trace-controls/` reject their specific missing, +shortened, foreign, redistributed or omitted-fixture corruption. The ordinary +summary retains `roadmapComplete=false`. + +The current result selector specifies user mode with M=0, ISP with M=0 and +MSP with M=1. Restored user mode with M=1 is **not covered** by this matrix; +its subsequent exception-bank selection needs separate qualification. This +is distinct from the already qualified initial user-tail fault M values and +from a user-tail trace bridge. It remains a required stack-state combination, +along with the other named unqualified protocols above. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 42546346..11655d07 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1747,3 +1747,46 @@ report/identity controls detect their intended defects. The complete 040 run has 7,496,000 passing cases, zero mismatches/unsupported execution and the retained 480 untested requirements; it continues to fail completion. No new consumer or package validation is needed for this test-only checkpoint. + +### Milestone 6 pending delivery with preserved trace — 2026-10-06 + +Four scalar/batch groups add **1,824,768 phases** for repaired CT/CU/CP49 +frames with untouched incoming trace. They verify pending conversion instead +of an extra automatic RTE trace, repaired saved SR/PC/EA, format/vector, +pending consumption, handler return and the following traced BRA. Canonical +cases use all CCRs; structural cases use CCR 0/31 and every validation-read +byte across twelve supervisor paths, both alignments/VBRs and all incoming/ +restored traces and restored stacks. Expectations compose MC68040UM 8.3 and +8.4.6.2/7 with the existing repair/exception-entry fixtures. + +The first focused run passes all four groups without skips or missing phases. +Three maintained `RtePendingTrace` mutations target extra RTE trace delivery, +incorrect saved SR and suppressed following BRA trace. The complete 040 gate +now requires 52 tests, 46 reports, six fixed examples and thirteen input +identities; it independently enumerates the new combinations. Production CPU +source is unchanged outside temporary mutation checks. + +These fixtures use a bare pending-handler RTE. The handler's software trace +service, preserved-trace repair with other CP vectors, user-tail trace bridges +and mixed-epoch trace provenance remain required separately. All earlier +internal-restoration, chained odd-PC, data/writeback/context-transfer and broader +model/reference/consolidation work remains open. The 480-case protocol inventory +continues to fail completion. No regression is retired, public API changed or +package released. Milestone 6 stays **in progress**, `roadmapComplete=false`. +See the [qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040-pending-delivery-with-preserved-trace--2026-10-06). + +Fresh full Release validation passes **5,077 tests**, with eleven optional skips +and zero failures. All nine pinned WinUAE selections match their exact counts +and CPU/adapter identities. Strict reporting passes **24,289,430 cases / 636 +batches**; fresh SingleStepTests passes 312,500 cases / 125 files and Musashi +passes 536 programs with 88 explicit exclusions. All three mutations and +seventeen new report/identity controls detect their intended defects. The +complete 040 audit executes 52 tests (51 pass, inventory fails), with 9,320,768 +passing cases, zero mismatches/unsupported execution and 480 untested +requirements. No new consumer/package validation is needed for this test-only +checkpoint; prior `.63` evidence remains scoped to its own binary. + +The repaired result selector covers user M=0, ISP M=0 and MSP M=1. Restored user +M=1 and its exception-bank selection remain **untested** separately from the +initial user-tail M qualification and user-tail trace bridge. This required +stack-state combination remains part of milestone 6. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index 441f491f..36c1b133 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -35,6 +35,10 @@ $expected = [ordered]@{ 'rte-retry-trace-boundaries-batch' = @{cases=92736; combinations=378} 'rte-retry-trace-chained-scalar' = @{cases=1271808; combinations=82944} 'rte-retry-trace-chained-batch' = @{cases=1271808; combinations=82944} + 'rte-pending-trace-boundaries-scalar' = @{cases=41472; combinations=162} + 'rte-pending-trace-boundaries-batch' = @{cases=41472; combinations=162} + 'rte-pending-trace-chained-scalar' = @{cases=870912; combinations=54432} + 'rte-pending-trace-chained-batch' = @{cases=870912; combinations=54432} 'instruction-fault-frame' = @{cases=36864; combinations=576} 'instruction-fault-restart' = @{cases=79872; combinations=624} 'handler-prefetch-entry-scalar' = @{cases=196608; combinations=6144} @@ -69,7 +73,7 @@ if (-not $ValidateReportsOnly) { } $identity = [ordered]@{ schema=1; reference='MC68040UM'; url='https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf' - sections=@('2.2.2.1','7.6.1','7.6.3','8.1 figure 8-1','8.2.1','8.2.2','8.2.5','8.2.6','8.4','8.4.1','8.4.2','8.4.3','8.4.4','8.4.6.2','8.4.6.7'); softwareReferenceExecuted=$false + sections=@('2.2.2.1','7.6.1','7.6.3','8.1 figure 8-1','8.2.1','8.2.2','8.2.5','8.2.6','8.3','8.4','8.4.1','8.4.2','8.4.3','8.4.4','8.4.6.2','8.4.6.7'); softwareReferenceExecuted=$false sourceCommit=(& git -C $repo rev-parse HEAD); cpuCommittedTree=(& git -C $repo rev-parse HEAD:Copper68k) cpuSourceFiles=@(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} @@ -85,7 +89,7 @@ if (-not $ValidateReportsOnly) { 'Multiple continuation bits are architecturally undefined and excluded', 'Selected active accurate-batch paths verify counts/callbacks and scalar/batch bus/cycle policy; generic short operand frames do not qualify architectural format-7 data restart', 'Instruction-fault fixtures use cache-disabled accurate execution and physical-map rejection; speculative deferral, enabled caches/MMU and compiled fetch PC provenance remain unqualified', - 'Legacy supervisor repair clears saved trace; normal/CM retry-trace fixtures preserve it through handler return and trace the completed RTE. User/pending/mixed-epoch incoming-trace protocols remain required', + 'Legacy supervisor repair clears saved trace; normal/CM retry traces completion, while pending CT/CU/CP49 delivery suppresses an extra RTE trace. Other CP vectors under preserved-trace repair, software trace service and user-tail/mixed-epoch protocols remain required', 'User-tail fault expectations compose documented throwaway live-SR rules with general supervisor exception entry; unusual combined hardware behavior has not been observed', 'User-tail untouched-trace retry, internal-restoration double faults, cache/MMU/compiled handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', 'Direct odd-RTE saved-SR ordering uses documentary WinUAE 5d22d336, not an executed hardware oracle') @@ -115,8 +119,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 48 -or [int]$counters.total -ne 48 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (42 batches, 6 fixed examples)' +if ([int]$counters.executed -ne 52 -or [int]$counters.total -ne 52 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (46 batches, 6 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -216,9 +220,10 @@ foreach ($group in $expected.Keys) { } } } } } - } elseif ($group -in @('rte-repair-boundaries','rte-repair-chained') -or $group.StartsWith('rte-retry-trace-', [StringComparison]::Ordinal)) { - $keepTrace = $group.StartsWith('rte-retry-trace-', [StringComparison]::Ordinal) - $chained = $group -eq 'rte-repair-chained' -or $group.StartsWith('rte-retry-trace-chained-', [StringComparison]::Ordinal) + } elseif ($group -in @('rte-repair-boundaries','rte-repair-chained') -or $group.StartsWith('rte-retry-trace-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-pending-trace-', [StringComparison]::Ordinal)) { + $pendingTrace = $group.StartsWith('rte-pending-trace-', [StringComparison]::Ordinal) + $keepTrace = $pendingTrace -or $group.StartsWith('rte-retry-trace-', [StringComparison]::Ordinal) + $chained = $group -eq 'rte-repair-chained' -or $group.StartsWith('rte-retry-trace-chained-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-pending-trace-chained-', [StringComparison]::Ordinal) $paths = if ($chained) { @('ISP-ISP','ISP-ISP-ISP','ISP-MSP-ISP','ISP-MSP','ISP-ISP-MSP','ISP-MSP-MSP', 'MSP-ISP','MSP-ISP-ISP','MSP-MSP-ISP','MSP-MSP','MSP-ISP-MSP','MSP-MSP-MSP') } else { @('ISP','MSP') } foreach ($path in $paths) { foreach ($result in @('user','ISP','MSP')) { @@ -226,7 +231,7 @@ foreach ($group in $expected.Keys) { foreach ($trace in @(0,0x8000,0x4000)) { foreach ($alignment in $(if ($chained) { @(0,1) } else { @(0) })) { foreach ($vbr in $(if ($chained) { @(0,0x10000) } else { @(0x10000) })) { - foreach ($form in $(if ($keepTrace) { @('format0','format2','format3','invalid4','invalid15','normal','CM') } else { @('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP') })) { + foreach ($form in $(if ($pendingTrace) { @('CT','CU','CP') } elseif ($keepTrace) { @('format0','format2','format3','invalid4','invalid15','normal','CM') } else { @('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP') })) { # Literal read ranges are independent of the fixture iterator. $reads = @('0:2') if ($chained) { @@ -237,8 +242,8 @@ foreach ($group in $expected.Keys) { foreach ($read in $reads) { $bytes = if ($chained) { [int]$read.Split(':')[1] } else { 1 } for ($byte=0; $byte -lt $bytes; $byte++) { - $key = '68040/RTE/{0}/{1}/path={2}/result={3}/incoming={4:X4}/T={5:X4}/align={6}/VBR={7:X8}/read={8}/fault-byte={9}' -f $(if ($keepTrace) {'retry-trace'} else {'repair'}),$form,$path,$result,$incoming,$trace,$alignment,$vbr,$read,$byte - $phases = if ($keepTrace) { $(if ($incoming -eq 0) {7} else {8}) } elseif ($form -in @('CT','CU','CP')) { 9 } else { 8 } + $key = '68040/RTE/{0}/{1}/path={2}/result={3}/incoming={4:X4}/T={5:X4}/align={6}/VBR={7:X8}/read={8}/fault-byte={9}' -f $(if ($pendingTrace) {'pending-trace'} elseif ($keepTrace) {'retry-trace'} else {'repair'}),$form,$path,$result,$incoming,$trace,$alignment,$vbr,$read,$byte + $phases = if ($pendingTrace) { 8 } elseif ($keepTrace) { $(if ($incoming -eq 0) {7} else {8}) } elseif ($form -in @('CT','CU','CP')) { 9 } else { 8 } $ccrs = if ($chained) { 2 } else { 32 } $expectedCombinations[$key] = $phases * $ccrs } @@ -427,8 +432,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 48 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 52 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=7496480; xunitBatches=42; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=9321248; xunitBatches=46; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index 6fdec0fa..242d2d4a 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','RteRetryTrace','UserRteFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','UserRteFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -341,6 +341,23 @@ foreach ($mutation in $retryTraceMutations) { $mutation.group='RepairWithUntouchedIncomingTrace'; $mutation.model='68040'; $mutation.milestone=6 $mutation.retryTrace=$true; $mutations += $mutation } +$pendingTraceMutations = @( + @{name='040-pending-trace-extra-rte-trace'; file=$advanced; + before=' if (exception && (_profile.Model is M68kAcceleratorModel.M68040 or M68kAcceleratorModel.M68060 ||'; + after=' if (exception && State.LastExceptionVector is not (9 or 11 or 49) && (_profile.Model is M68kAcceleratorModel.M68040 or M68kAcceleratorModel.M68060 ||'; + proofCombination='/pending-trace/CT/'; proofPhase='/retry-RTE'; proofReason='A7 expected'}, + @{name='040-pending-trace-saved-incoming-sr'; file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs'; + before=' PushWord(sr);'; after=' PushWord(priorSr);'; + proofCombination='/pending-trace/CT/'; proofPhase='/retry-RTE'; proofReason='Memory'}, + @{name='040-pending-trace-following-bra-suppressed'; file=$advanced; + before=' if ((trace & 0x8000) != 0 || flow || exception)'; + after=' if (State.ProgramCounter != State.LastInstructionProgramCounter && ((trace & 0x8000) != 0 || flow || exception))'; + proofCombination='/incoming=8000/T=8000/'; proofPhase='/following'; proofReason='PC expected'} +) +foreach ($mutation in $pendingTraceMutations) { + $mutation.group='PendingRepairWithPreservedIncomingTrace'; $mutation.model='68040'; $mutation.milestone=6 + $mutation.pendingTrace=$true; $mutations += $mutation +} $instructionMutations = @( @{name='040-instruction-short-frame'; before=' RaiseUnbufferedReadAccessFault(fault, stackedProgramCounter, instruction: true);'; after=' RaiseFormat0Exception(VectorBusError, stackedProgramCounter, M68kInstructionTimingKey.IllegalInstruction);'; proofCombination='/instruction/opcode/'}, @@ -410,6 +427,7 @@ if ($Scope -eq 'Rte040') { $mutations = @($mutations | Where-Object { $_.odd }) if ($Scope -eq 'RteValidationFault') { $mutations = @($mutations | Where-Object { $_.validationFault }) } if ($Scope -eq 'RteRepair') { $mutations = @($mutations | Where-Object { $_.repair }) } if ($Scope -eq 'RteRetryTrace') { $mutations = @($mutations | Where-Object { $_.retryTrace }) } +if ($Scope -eq 'RtePendingTrace') { $mutations = @($mutations | Where-Object { $_.pendingTrace }) } if ($Scope -eq 'InstructionFault') { $mutations = @($mutations | Where-Object { $_.instructionFault }) } if ($Scope -eq 'HandlerPrefetch') { $mutations = @($mutations | Where-Object { $_.handlerPrefetch }) } if ($Scope -eq 'EntryPrefetch') { $mutations = @($mutations | Where-Object { $_.entryPrefetch }) } @@ -526,15 +544,17 @@ try { throw 'Instruction-fault mutation omitted complete cases or intended semantic failure' } } - if ($mutation.retryTrace) { + if ($mutation.retryTrace -or $mutation.pendingTrace) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if ($trx.TestRun.ResultSummary.Counters.executed -ne 4 -or $trx.TestRun.ResultSummary.Counters.failed -ne 4 -or $batches.Count -ne 4) { throw 'Retry-trace mutation omitted its complete four-batch selection' } foreach ($matrix in @('boundaries','chained')) { foreach ($route in @('scalar','batch')) { - $target = @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq "rte-retry-trace-$matrix-$route" }) - if ($target.Count -ne 1 -or $target[0].logicalCases -ne $(if ($matrix -eq 'boundaries') {92736} else {1271808}) -or + $prefix = if ($mutation.pendingTrace) {'rte-pending-trace'} else {'rte-retry-trace'} + $cases = if ($mutation.pendingTrace) {if ($matrix -eq 'boundaries') {41472} else {870912}} else {if ($matrix -eq 'boundaries') {92736} else {1271808}} + $target = @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq "$prefix-$matrix-$route" }) + if ($target.Count -ne 1 -or $target[0].logicalCases -ne $cases -or @($target[0].failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.id.EndsWith($mutation.proofPhase, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) }).Count -eq 0) { throw "Retry-trace mutation omitted intended $matrix/$route semantic failure" @@ -604,7 +624,7 @@ try { } elseif ($mutation.entryPrefetch) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] - } elseif ($mutation.retryTrace) { + } elseif ($mutation.retryTrace -or $mutation.pendingTrace) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.id.EndsWith($mutation.proofPhase, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] } elseif ($mutation.userRte) { diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 0cc72fbf..6bdf4a77 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -156,6 +156,10 @@ try { $expected['rte-retry-trace-boundaries-batch'] = 92736 $expected['rte-retry-trace-chained-scalar'] = 1271808 $expected['rte-retry-trace-chained-batch'] = 1271808 + $expected['rte-pending-trace-boundaries-scalar'] = 41472 + $expected['rte-pending-trace-boundaries-batch'] = 41472 + $expected['rte-pending-trace-chained-scalar'] = 870912 + $expected['rte-pending-trace-chained-batch'] = 870912 $expected['rte-validation-batch'] = 129024 $expected['access-fault-batch-dispatch'] = 10368 $expected['rte-access-entry-double-fault'] = 98304 @@ -287,9 +291,10 @@ try { m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; m68040HandlerPrefetchScope=@{logicalCases=457728; batches=8; routes=@('accurate scalar','accurate batch'); bus='Physical map with no host code reader, including stable-map generation'; qualified='Four-long access-error entry window, every entry byte, odd handler addresses, retained data, flow/host/task/map invalidation and later instruction faults; denied cold boundaries perform no CPU fetch/state change'; remaining='Other exception-entry paths, host-reader/compiled fetch provenance, enabled caches/MMU, ordinary speculative deferral and physical timing'}; m68040UserTailScope=@{logicalCases=1687040; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); authority='Composition of MC68040UM 2.2.2.1, 8.1, 8.2.1/5, 8.4.2 and 8.4.6.7; software qualification, no executed hardware reference'; userM=@(0,1); initialBanks=@('ISP','MSP'); middleBanks=@('none','user','ISP','MSP'); ccr='all 32 canonical SR reads, 0/31 structural reads'; forms='0/2/3, invalid 4/15, normal/CM/CT/CU/CP49'; qualified='Live intermediate SR, supervisor exception stack selected by M, user-data TM=1, preserved USP frame and committed throwaways, bare return followed by privilege violation, executed seven-store repair with fresh throwaway bridge and following trace/continuation'; remaining='Internal restoration faults, untouched trace retry, physical bus-order/timing and hardware observation of composed user-tail fault behavior'}; - m68040RteRetryTraceScope=@{logicalCases=2729088; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); faultTailBanks=@('ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredBanks=@('user','ISP','MSP'); restoredTrace=@(0,0x8000,0x4000); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM'; qualified='Three original-frame repair stores leave saved access SR untouched; handler return restores incoming trace; successful original RTE triggers T1/T0 trace with restored SR/PC; trace-handler return and following instruction retain MOVEM EA and stack selection'; ccr='all 32 canonical, 0/31 every chained read byte'; remaining='Pending CT/CU/CP interactions, user-tail trace bridge, mixed trace values within consumed throwaways, internal restoration and hardware/physical timing'}; - m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, executed repair/retry after committed supervisor throwaways, and manual-derived user-tail fault/privilege/bridge repair; chained odd-PC saved-SR provenance, internal-restoration faults, pending/user/mixed-epoch untouched-trace retry, CP context transfer, other real fault entry and physical timing remain unqualified'; - m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='Internal restoration, pending/user/mixed-epoch untouched incoming-trace retry and other real access faults; normal/CM preserved-trace and user-tail repair are qualified separately'}; + m68040RtePendingTraceScope=@{logicalCases=1824768; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); forms='CT/CU/CP49'; incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); qualified='Three original-frame stores preserve access SR; returned incoming trace cannot create an extra RTE trace on pending conversion; saved SR/PC/EA, format/vector, pending consumption, handler return and following traced BRA are checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='Other CP vectors under preserved-trace repair, user-tail trace bridge, mixed-epoch provenance, software trace-service routines, internal restoration and physical/hardware qualification'}; + m68040RteRetryTraceScope=@{logicalCases=2729088; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); faultTailBanks=@('ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredBanks=@('user','ISP','MSP'); restoredTrace=@(0,0x8000,0x4000); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM'; qualified='Three original-frame repair stores leave saved access SR untouched; handler return restores incoming trace; successful original RTE triggers T1/T0 trace with restored SR/PC; trace-handler return and following instruction retain MOVEM EA and stack selection'; ccr='all 32 canonical, 0/31 every chained read byte'; remaining='User-tail trace bridge, mixed trace values within consumed throwaways, internal restoration and hardware/physical timing'}; + m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, executed repair/retry after committed supervisor throwaways, and manual-derived user-tail fault/privilege/bridge repair; chained odd-PC saved-SR provenance, internal-restoration faults, user/mixed-epoch untouched-trace retry, CP context transfer, other real fault entry and physical timing remain unqualified'; + m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='Internal restoration, user/mixed-epoch untouched incoming-trace retry and other real access faults; normal/CM preserved-trace and user-tail repair are qualified separately'}; m68040InstructionFaultScope=@{logicalCases=116736; batches=2; translationEnabled=$false; instructionCacheEnabled=$false; engine='accurate public factory plus cached self-branch batch'; forms=@('opcode','extension-low','next-opcode','self-branch'); banks=@('user','ISP','MSP'); ccr='all 32'; trace='All defined incoming states for frame/return; restart trace zero'; fields='Format7 SR, executing PC distinct from prefetch FA, RW/SIZE/TM and invalid writebacks'; phases=@('fault entry','handler return','restart','following'); remaining='Data writebacks/restart, speculative prefetch deferral, compiled instruction-fault PC provenance, cache-enabled fetches and physical timing'}; m68040RtePhysicalValidationScope=@{logicalCases=223872; batches=2; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); trace=@(0,0x8000,0x4000); alignment=@(0,1); vbr=@(0,0x10000); faults='Each byte of SR/PC/format/SSW/continuation-EA reads; 14 direct/one/two-throwaway supervisor paths'; phases=@('fault entry','handler return'); undefined='EA and invalid writeback/push data; SSW X'; remaining='Internal-restoration/double faults, other instruction faults and software writeback handlers; user-tail validation is qualified separately'}; m68040AccessDoubleFaultScope=@{logicalCases=105408; batches=5; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; dispatchCcr=@(0,31); engines=@('accurate','classic','V2'); faults='Every byte of format-7 stack/vector; later handler validation refaults; warmed RTE fallback and compiled B/W/L operand read/write side exits'; recovery='External reset, including restored host entry; interrupt, task/subroutine entry and halted batches remain inactive'; undefined='Partial stack write order, contents and exact SP'; remaining='Internal restoration, other handler-entry paths and general architectural access-fault restart'}; From e26a8cc3cfd3caf27e0e525bf0eba9f239134e59 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 01:51:26 +0300 Subject: [PATCH 052/211] Qualify restored user master bit after 68040 RTE repair --- Copper68k.Tests/Synthetic/README.md | 30 +++++ .../SyntheticM68040RteRepairTests.cs | 33 +++-- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 124 ++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 50 +++++++ scripts/test-copper68k-040-access-frames.ps1 | 29 ++-- .../test-copper68k-synthetic-mutations.ps1 | 29 +++- scripts/test-copper68k-synthetic.ps1 | 5 + 7 files changed, 274 insertions(+), 26 deletions(-) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index fab127e2..445a328e 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -614,3 +614,33 @@ See the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#0 The three result states are user M=0, ISP M=0 and MSP M=1. Restored user M=1 is not covered here; its exception-bank selection remains a required separate qualification, alongside the pending software trace-service and other gaps. + +### Restored user mode with M=1 + +Four `RestoredUserMaster` reports check 1,517,952 phases after a supervisor-tail +validation fault and real software repair. The original frame restores S=0,M=1. +User execution uses USP regardless of M; synchronous completion trace, pending +CT/CU/CP49 conversion and a following traced instruction select MSP. Expectations +compose MC68040UM 2.2.2.1 and 8.1 with 8.2.6, 8.3 and 8.4.6.7. The shared +exception-bank helper now tests M independently of pre-exception S. + +Canonical cases cover all 32 CCRs. Chained cases reject every validation-read +byte, with CCR 0/31, all twelve supervisor throwaway paths and both alignments/ +VBRs. Both routes cross incoming/restored 0/T1/T0 and forms 0/2/3, invalid 4/15 +repaired to 0, normal/CM and pending CT/CU/CP49. All three stack pointers, saved +SR/PC/EA, exact repair stores, pending consumption, handler returns and following +instruction state are checked. The original result-state reports retain their +own cardinalities. + +```powershell +./scripts/test-copper68k-synthetic-mutations.ps1 -Scope RteUserMaster -OutputDirectory artifacts/rte-user-master-mutations +``` + +The dedicated gate requires 56 tests, 50 reports, six fixed examples and thirteen +input identities, independently checking each combination. The 480-case required +inventory still prevents completion. Other pending vectors, software trace +service, user-tail trace bridges, mixed-epoch provenance and all earlier +internal/data/writeback/reference gaps remain required. These synthetic results +are software qualification, not hardware observations or physical timing. + +See the [restored-user qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-restored-user-m1--2026-10-06). diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs index 7ab224bb..acec27e8 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs @@ -44,35 +44,47 @@ public sealed class SyntheticM68040RteRepairTests(ITestOutputHelper output) [Fact, Trait("Suite", "Synthetic")] public void PendingRepairWithPreservedIncomingTraceChainedBatch() => Audit(true, true, true, true); - private void Audit(bool chained, bool keepTrace = false, bool batch = false, bool pendingTrace = false) + [Fact, Trait("Suite", "Synthetic")] + public void RestoredUserMasterBoundariesScalar() => Audit(false, true, userMaster: true); + + [Fact, Trait("Suite", "Synthetic")] + public void RestoredUserMasterBoundariesBatch() => Audit(false, true, true, userMaster: true); + + [Fact, Trait("Suite", "Synthetic")] + public void RestoredUserMasterChainedScalar() => Audit(true, true, userMaster: true); + + [Fact, Trait("Suite", "Synthetic")] + public void RestoredUserMasterChainedBatch() => Audit(true, true, true, userMaster: true); + + private void Audit(bool chained, bool keepTrace = false, bool batch = false, bool pendingTrace = false, bool userMaster = false) { var bus = new SyntheticM68040RteValidationFaultTests.ValidationFaultBus(); var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); - var traceGroup = pendingTrace ? "rte-pending-trace" : "rte-retry-trace"; + var traceGroup = userMaster ? "rte-user-master" : pendingTrace ? "rte-pending-trace" : "rte-retry-trace"; var report = new CoverageBatch("68040", keepTrace ? $"{traceGroup}-{(chained ? "chained" : "boundaries")}-{(batch ? "batch" : "scalar")}" : chained ? "rte-repair-chained" : "rte-repair-boundaries"); foreach (var start in SupervisorBanks) foreach (var tail in chained ? SupervisorBanks : [start]) foreach (var middle in chained ? new[] { "none", "ISP", "MSP" } : ["none"]) - foreach (var result in new[] { "user", "ISP", "MSP" }) + foreach (var result in userMaster ? new[] { "user-M" } : ["user", "ISP", "MSP"]) foreach (var incoming in chained && !keepTrace ? new ushort[] { 0x8000 } : [0, 0x8000, 0x4000]) foreach (var restoredTrace in new ushort[] { 0, 0x8000, 0x4000 }) foreach (var alignment in chained ? new uint[] { 0, 1 } : [0]) foreach (var vbr in chained ? new uint[] { 0, 0x10000 } : [0x10000]) foreach (var ccr in chained ? new[] { 0, 31 } : Enumerable.Range(0, 32)) - foreach (var form in Forms.Where(f => pendingTrace ? f is "CT" or "CU" or "CP" : !keepTrace || f is not ("CT" or "CU" or "CP"))) + foreach (var form in Forms.Where(f => userMaster || (pendingTrace ? f is "CT" or "CU" or "CP" : !keepTrace || f is not ("CT" or "CU" or "CP")))) foreach (var (offset, width) in chained ? SyntheticM68040RteValidationFaultTests.Reads(form) : [(0u, 2)]) for (var faultByte = 0; faultByte < (chained ? width : 1); faultByte++) { var path = !chained ? new[] { start } : middle == "none" ? [start, tail] : new[] { start, middle, tail }; - Run(m, bus, report, path, result, incoming, restoredTrace, alignment, vbr, ccr, form, offset, width, faultByte, keepTrace, batch, pendingTrace); + Run(m, bus, report, path, result, incoming, restoredTrace, alignment, vbr, ccr, form, offset, width, faultByte, keepTrace, batch, pendingTrace, userMaster); } report.Complete(output); } private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTests.ValidationFaultBus bus, CoverageBatch report, string[] path, string result, ushort incoming, ushort restoredTrace, - uint alignment, uint vbr, int ccr, string form, uint offset, int width, int faultByte, bool keepTrace, bool batch, bool pendingTrace) + uint alignment, uint vbr, int ccr, string form, uint offset, int width, int faultByte, bool keepTrace, bool batch, bool pendingTrace, bool userMaster) { bus.Disarm(); m.Reset(ccr); var pointers = new Dictionary { ["user"] = 0x7800 + alignment, ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; @@ -140,7 +152,7 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes phases.AddRange(["handler-return", "retry-RTE"]); if (keepTrace && incoming != 0 && vector == 0) phases.Add("retry-trace-return"); if (vector != 0) phases.Add("pending-return"); phases.Add("following"); - var id = $"68040/RTE/{(pendingTrace ? "pending-trace" : keepTrace ? "retry-trace" : "repair")}/{form}/path={string.Join('-', path)}/result={result}/incoming={incoming:X4}/T={restoredTrace:X4}/align={alignment}/VBR={vbr:X8}/read={offset}:{width}/fault-byte={faultByte}/op=4E73/ccr={ccr:X2}"; + var id = $"68040/RTE/{(userMaster ? "user-master" : pendingTrace ? "pending-trace" : keepTrace ? "retry-trace" : "repair")}/{form}/path={string.Join('-', path)}/result={result}/incoming={incoming:X4}/T={restoredTrace:X4}/align={alignment}/VBR={vbr:X8}/read={offset}:{width}/fault-byte={faultByte}/op=4E73/ccr={ccr:X2}"; var phase = 0; bool Step() { @@ -210,8 +222,11 @@ bool Step() Step(); } - private static ushort Status(string bank, ushort trace, int ccr) => (ushort)((bank == "MSP" ? 0x3000 : bank == "ISP" ? 0x2000 : 0) | trace | ccr); - private static string ExceptionBank(ushort sr) => (sr & 0x3000) == 0x3000 ? "MSP" : "ISP"; + private static ushort Status(string bank, ushort trace, int ccr) => (ushort)((bank == "MSP" ? 0x3000 : bank == "ISP" ? 0x2000 : bank == "user-M" ? 0x1000 : 0) | trace | ccr); + // MC68040UM 2.2.2.1 / 8.1: user execution uses USP regardless of M. + // A synchronous exception sets S, preserves M and therefore selects MSP + // whenever M was set, including the restored S=0,M=1 state. + private static string ExceptionBank(ushort sr) => (sr & 0x1000) != 0 ? "MSP" : "ISP"; private static void SetStacks(ArchitecturalExpectation e, Dictionary pointers, ushort sr) { e.Sr = sr; e.A[7] = pointers[(sr & 0x2000) == 0 ? "user" : ExceptionBank(sr)]; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 4a3ce962..5b9c7cd8 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -4803,3 +4803,127 @@ its subsequent exception-bank selection needs separate qualification. This is distinct from the already qualified initial user-tail fault M values and from a user-tail trace bridge. It remains a required stack-state combination, along with the other named unqualified protocols above. + +### 040 restored user M=1 — 2026-10-06 + +`SyntheticM68040RteRepairTests` now includes a separate restored user S=0,M=1 +matrix through accurate scalar and one-instruction batch execution. MC68040UM +2.2.2.1 makes USP active in user mode regardless of M; 8.1 forces S and clears +trace for a synchronous exception, preserving M and selecting MSP. The shared +independent exception expectation must therefore use M alone to select that +future supervisor bank. Existing user M=0 / ISP / MSP reports retain their +original case identities and cardinalities. No production CPU correction was +required by the new focused run. + +The real handler repairs SR/PC/format with exactly three MOVE stores while +preserving the access frame's saved incoming trace. The original RTE restores +S=0,M=1, then normal/CM completion trace or CT/CU/CP49 pending conversion must use +MSP. Handler return restores USP without clearing M, and the following BRA or +CM MOVEM obeys the restored trace. Every phase checks architectural registers, +all three stack pointers, saved SR/PC/EA, exception sequence, format/vector, +pending context consumption, guarded memory and batch counts/callbacks. CM +still uses the saved operand address across intervening trace handlers. + +| Group, each scalar/batch | Cases | Combinations | +| --- | ---: | ---: | +| Canonical CCR boundaries | 44,736 | 180 | +| Chained validation-read bytes | 714,240 | 45,792 | + +The four reports total **1,517,952 checked phases**. Canonical cases cover all +32 CCRs. Chained cases use CCR 0/31 and every byte of each documented validation +read, twelve supervisor throwaway paths, both alignments/VBRs and all incoming/ +restored 0/T1/T0. Forms are 0/2/3, invalid 4/15 repaired to 0, normal/CM and +CT/CU/CP49. The initial run at `artifacts/m6-user-master-focused/` passes all four +tests without skips, mismatches, unsupported execution or untested phases. + +The complete 040 command requires 56 executed tests, 50 reports, six fixed +examples and thirteen input identities. Its independent iterator checks every +new path/result/trace/form/read/fault-byte combination and expected cardinality. +The ordinary strict command also requires the four new reports. Maintained +mutation scope `RteUserMaster` requires the intended semantic diagnostic from +all four reports for incorrect user stack selection, pending M clearing and +trace M clearing. It restores exact production source bytes before rebuilding. + +These are manually derived software expectations, composing the documented +stack-selection and exception-entry rules with RTE continuation rules; they +are not observed hardware faults or independent FPU arithmetic qualification. +The pending handler performs a bare RTE rather than the remaining software +trace-service protocol. This closes the restored user M=1 combination in these +supervisor-tail repair programs, separately from initial user-tail M and +user-tail trace bridges. Other CP vectors, software trace service, mixed-epoch +provenance, internal restoration, chained odd-PC SR, general data/writeback/ +context-transfer and the broader selected-model/reference/consolidation work +remain required. The 480-case inventory still fails completion. Milestone 6 +remains **in progress**, `roadmapComplete=false`. Enabled MMU, FPU arithmetic, +physical pipeline/cache timing and OS compatibility remain outside the roadmap. +No regression is retired, public API changed or package published. + +The maintained `RteUserMaster` proof in `artifacts/m6-user-master-mutations-v2/` +detects all three mutations. Each executes all four complete reports and +requires the intended retry/following phase and diagnostic in both routes and +both matrices. Counts below are per route, canonical/chained respectively: + +| Mutation | Mismatching | Dependent phases untested | Required diagnostic | +| --- | ---: | ---: | --- | +| Select MSP during user execution | 5,760 / 91,584 | 10,176 / 164,736 | Format-0 retry, A7 differs | +| Clear M during pending conversion | 1,728 / 36,288 | 3,456 / 72,576 | CT retry, SR loses M | +| Clear M during trace entry | 4,672 / 72,000 | 5,376 / 73,728 | Format-0 following instruction, SR loses M | + +The first proof attempt expected an A7 diagnostic for pending M clearing, but +the common verifier reports the SR mismatch first. The proof checker now +requires that precise missing-M SR diagnostic; all three mutations were rerun +in a fresh directory. The initial failed proof attempt remains at +`artifacts/m6-user-master-mutations/`; it is not acceptance evidence. Exact +production source bytes match the preceding checkpoint after restoration, and +the restored Release rebuild succeeds with zero warnings/errors. + +The first complete run executes all 56 tests: 55 pass and the required inventory +fails. Its report validator then rejects `rte-user-master-boundaries-scalar` +because the older `rte-user-` prefix selector also matched the new group. The +selector now names `rte-user-fault-` and `rte-user-repair-` explicitly, preserving +both iterators' independent scopes. The original run remains at +`artifacts/m6-user-master-audit/`; acceptance uses a fresh complete run rather +than rewriting that run's command identity or results. + +Fresh full Release validation in `artifacts/m6-user-master-full/` passes **5,081 +tests with zero failures and eleven optional skips**. All nine qualified WinUAE +presets match their exact selections and current CPU/adapter identities in +`qualified-preset-identities.json`. CPU assembly SHA-256 is +`9765973d30625e2e15c1f76159391fe8a1c7c39c7b0cce1a80187748f382700e`; +test/reference adapter is +`70a3f5a04b5e47a872cfdc12036747bf30ca95b8829e65865338b43d216be4b8`. +The build starts from commit `b59300c`; production CPU source is unchanged, +while the tested fixture source has its own recorded hashes. These binaries +are not relabeled as built from the subsequent checkpoint commit. + +No new consumer validation or private/public package is produced for this +test-only checkpoint. Prior unpublished `.63` evidence retains its own +package/binary scope. Root CopperScreen changes and the pinned dependency +boundary are untouched. Original Basic reference mismatches/unsupported groups +remain historical failures, separate from the qualified selections above. + +The strict ordinary gate in `artifacts/m6-user-master-strict.log` passes +**25,807,382 logical cases / 640 batches**. Fresh pinned SingleStepTests passes +**312,500 cases / 125 files**, and Musashi passes **536 programs with 88 explicit +exclusions**. Both reference tests execute once without skips, with fixture +identities and source pins checked by the maintained command. These are scoped +software references rather than hardware qualification. The ordinary summary +retains `roadmapComplete=false` and the new restored-user scope separately. + +The fresh corrected run in `artifacts/m6-user-master-audit-v2/` executes **56 +tests: 55 pass and the required inventory fails**, with no skips. All +**10,838,720 executable phases pass**, with zero mismatches or unsupported +execution and **480 untested requirements**. The verifier validates all 50 +reports, six fixed examples, thirteen fixture/command identities and every +independently enumerated combination before rejecting the inventory. Logical +coverage totals 10,839,200 including the required untested rows. The manifest +records starting commit `b59300c`, CPU tree +`795fd12d6c0237a22a5f92f4a96cc4823e0364c1` and current input/source/binary hashes. +This gate remains failed and does not establish milestone-6 completion. + +All **17** controls in `artifacts/m6-user-master-controls/` reject their intended +missing, shortened, foreign, redistributed or omitted-fixture corruption with +the specific expected diagnostic, rather than the generic inventory rejection. +Both scalar/batch canonical and chained restored-user reports are covered. All +current fixture/source/binary identities still match after reference audits. +Production CPU source remains unchanged; milestone 6 remains **in progress**. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 11655d07..6c00f5ef 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1790,3 +1790,53 @@ The repaired result selector covers user M=0, ISP M=0 and MSP M=1. Restored user M=1 and its exception-bank selection remain **untested** separately from the initial user-tail M qualification and user-tail trace bridge. This required stack-state combination remains part of milestone 6. + +### Milestone 6 restored user M=1 — 2026-10-06 + +The required restored S=0,M=1 state now has four separate scalar/batch reports, +adding **1,517,952 phases**. The initial focused run passes all four without skips +or missing phases. User execution retains USP; completion trace, pending +CT/CU/CP49 delivery and following trace select MSP while preserving M. Independent +expectations compose MC68040UM 2.2.2.1 and 8.1 with the existing trace/repair rules. +The test expectation helper selects the synchronous exception bank from M rather +than requiring pre-exception S as well. No production CPU correction was needed. + +Canonical cases cover all CCRs. Structural cases cover every validation-read +byte, CCR 0/31, twelve supervisor paths, both alignments/VBRs and all incoming/ +restored 0/T1/T0. Forms are 0/2/3, repaired invalid 4/15, normal/CM and CT/CU/CP49. +The maintained `RteUserMaster` mutation scope targets selecting MSP during user +execution and clearing M during pending conversion or trace entry. The complete +040 gate now requires 56 tests, 50 reports, six fixed examples and thirteen input +identities, with an independent combination iterator. + +This closes the restored user M=1 gap for these supervisor-tail repair programs; +initial user-tail M and user-tail trace bridges are distinct protocols. Other CP +vectors, software trace service, mixed-epoch provenance, internal restoration, +chained odd-PC, general data/writeback/context transfer and the broader model/ +reference/consolidation requirements remain open. The required 480-case inventory +continues to fail completion. Milestone 6 remains **in progress** and +`roadmapComplete=false`. No package/API change or regression retirement is made. + +See the [restored-user qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040-restored-user-m1--2026-10-06). + +Fresh full Release validation passes **5,081 tests**, with eleven optional skips +and zero failures. All nine pinned qualified WinUAE selections match their exact +counts and current CPU/adapter identities. Strict reporting passes **25,807,382 +cases / 640 batches**; fresh SingleStepTests passes 312,500 cases / 125 files and +Musashi passes 536 programs with 88 explicit exclusions. All three new mutations +are detected, and exact production source bytes are restored. The first audit's +report selector collision is corrected with explicit user-tail prefixes; that +failed verifier attempt remains distinct from the new complete audit run. + +The corrected complete 040 audit executes all 56 tests (55 pass, inventory +fails), checks all 50 reports and independently enumerates their combinations. +It records **10,838,720 passing phases**, zero mismatches/unsupported execution +and the retained **480 untested requirements**. The gate remains failed, with +no skips or omitted scope. No new consumer/package validation is needed for +this test-only checkpoint; prior `.63` evidence remains scoped to its own binary. + +Seventeen fresh missing/short/foreign/redistributed-report and missing-fixture +identity controls reject their specific corruption diagnostics. Source/fixture/ +binary identities still match after all acceptance checks. Milestone 6 remains +**in progress**; the restored user M=1 software slice is qualified, while the +full required restoration/reference/consolidation scope remains retained. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index 36c1b133..8f2ef48f 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -39,6 +39,10 @@ $expected = [ordered]@{ 'rte-pending-trace-boundaries-batch' = @{cases=41472; combinations=162} 'rte-pending-trace-chained-scalar' = @{cases=870912; combinations=54432} 'rte-pending-trace-chained-batch' = @{cases=870912; combinations=54432} + 'rte-user-master-boundaries-scalar' = @{cases=44736; combinations=180} + 'rte-user-master-boundaries-batch' = @{cases=44736; combinations=180} + 'rte-user-master-chained-scalar' = @{cases=714240; combinations=45792} + 'rte-user-master-chained-batch' = @{cases=714240; combinations=45792} 'instruction-fault-frame' = @{cases=36864; combinations=576} 'instruction-fault-restart' = @{cases=79872; combinations=624} 'handler-prefetch-entry-scalar' = @{cases=196608; combinations=6144} @@ -119,8 +123,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 52 -or [int]$counters.total -ne 52 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (46 batches, 6 fixed examples)' +if ([int]$counters.executed -ne 56 -or [int]$counters.total -ne 56 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (50 batches, 6 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -143,7 +147,7 @@ foreach ($group in $expected.Keys) { $expectedCombinations["68040/RTE/format7/$form/bank=$bank"] = 32 } } - } elseif ($group.StartsWith('rte-user-', [StringComparison]::Ordinal)) { + } elseif ($group.StartsWith('rte-user-fault-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-user-repair-', [StringComparison]::Ordinal)) { $repair = $group.StartsWith('rte-user-repair-', [StringComparison]::Ordinal) foreach ($matrix in @('boundaries','structure')) { foreach ($start in @('ISP','MSP')) { @@ -220,18 +224,19 @@ foreach ($group in $expected.Keys) { } } } } } - } elseif ($group -in @('rte-repair-boundaries','rte-repair-chained') -or $group.StartsWith('rte-retry-trace-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-pending-trace-', [StringComparison]::Ordinal)) { + } elseif ($group -in @('rte-repair-boundaries','rte-repair-chained') -or $group.StartsWith('rte-retry-trace-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-pending-trace-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-user-master-', [StringComparison]::Ordinal)) { + $userMaster = $group.StartsWith('rte-user-master-', [StringComparison]::Ordinal) $pendingTrace = $group.StartsWith('rte-pending-trace-', [StringComparison]::Ordinal) - $keepTrace = $pendingTrace -or $group.StartsWith('rte-retry-trace-', [StringComparison]::Ordinal) - $chained = $group -eq 'rte-repair-chained' -or $group.StartsWith('rte-retry-trace-chained-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-pending-trace-chained-', [StringComparison]::Ordinal) + $keepTrace = $userMaster -or $pendingTrace -or $group.StartsWith('rte-retry-trace-', [StringComparison]::Ordinal) + $chained = $group -eq 'rte-repair-chained' -or $group.StartsWith('rte-retry-trace-chained-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-pending-trace-chained-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-user-master-chained-', [StringComparison]::Ordinal) $paths = if ($chained) { @('ISP-ISP','ISP-ISP-ISP','ISP-MSP-ISP','ISP-MSP','ISP-ISP-MSP','ISP-MSP-MSP', 'MSP-ISP','MSP-ISP-ISP','MSP-MSP-ISP','MSP-MSP','MSP-ISP-MSP','MSP-MSP-MSP') } else { @('ISP','MSP') } - foreach ($path in $paths) { foreach ($result in @('user','ISP','MSP')) { + foreach ($path in $paths) { foreach ($result in $(if ($userMaster) { @('user-M') } else { @('user','ISP','MSP') })) { foreach ($incoming in $(if ($chained -and -not $keepTrace) { @(0x8000) } else { @(0,0x8000,0x4000) })) { foreach ($trace in @(0,0x8000,0x4000)) { foreach ($alignment in $(if ($chained) { @(0,1) } else { @(0) })) { foreach ($vbr in $(if ($chained) { @(0,0x10000) } else { @(0x10000) })) { - foreach ($form in $(if ($pendingTrace) { @('CT','CU','CP') } elseif ($keepTrace) { @('format0','format2','format3','invalid4','invalid15','normal','CM') } else { @('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP') })) { + foreach ($form in $(if ($pendingTrace) { @('CT','CU','CP') } elseif ($keepTrace -and -not $userMaster) { @('format0','format2','format3','invalid4','invalid15','normal','CM') } else { @('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP') })) { # Literal read ranges are independent of the fixture iterator. $reads = @('0:2') if ($chained) { @@ -242,8 +247,8 @@ foreach ($group in $expected.Keys) { foreach ($read in $reads) { $bytes = if ($chained) { [int]$read.Split(':')[1] } else { 1 } for ($byte=0; $byte -lt $bytes; $byte++) { - $key = '68040/RTE/{0}/{1}/path={2}/result={3}/incoming={4:X4}/T={5:X4}/align={6}/VBR={7:X8}/read={8}/fault-byte={9}' -f $(if ($pendingTrace) {'pending-trace'} elseif ($keepTrace) {'retry-trace'} else {'repair'}),$form,$path,$result,$incoming,$trace,$alignment,$vbr,$read,$byte - $phases = if ($pendingTrace) { 8 } elseif ($keepTrace) { $(if ($incoming -eq 0) {7} else {8}) } elseif ($form -in @('CT','CU','CP')) { 9 } else { 8 } + $key = '68040/RTE/{0}/{1}/path={2}/result={3}/incoming={4:X4}/T={5:X4}/align={6}/VBR={7:X8}/read={8}/fault-byte={9}' -f $(if ($userMaster) {'user-master'} elseif ($pendingTrace) {'pending-trace'} elseif ($keepTrace) {'retry-trace'} else {'repair'}),$form,$path,$result,$incoming,$trace,$alignment,$vbr,$read,$byte + $phases = if ($pendingTrace -or ($userMaster -and $form -in @('CT','CU','CP'))) { 8 } elseif ($keepTrace) { $(if ($incoming -eq 0) {7} else {8}) } elseif ($form -in @('CT','CU','CP')) { 9 } else { 8 } $ccrs = if ($chained) { 2 } else { 32 } $expectedCombinations[$key] = $phases * $ccrs } @@ -432,8 +437,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 52 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 56 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=9321248; xunitBatches=46; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=10839200; xunitBatches=50; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index 242d2d4a..6f3ae4b7 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','UserRteFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','UserRteFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -358,6 +358,24 @@ foreach ($mutation in $pendingTraceMutations) { $mutation.group='PendingRepairWithPreservedIncomingTrace'; $mutation.model='68040'; $mutation.milestone=6 $mutation.pendingTrace=$true; $mutations += $mutation } +$userMasterMutations = @( + @{name='040-user-master-selects-msp-in-user-mode'; file='Copper68k/M68kCore.cs'; + before=' return UserStackPointer;'; + after=' return (statusRegister & Master) != 0 ? MasterStackPointer : UserStackPointer;'; + proofCombination='/user-master/format0/'; proofPhase='/retry-RTE'; proofReason='A7 expected'}, + @{name='040-user-master-pending-clears-m'; file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs'; + before=' State.StatusRegister = (ushort)((sr | M68kCpuState.Supervisor) & ~0xc000);'; + after=' State.StatusRegister = (ushort)((sr | M68kCpuState.Supervisor) & ~0xd000);'; + proofCombination='/user-master/CT/'; proofPhase='/retry-RTE'; proofReason='SR expected'}, + @{name='040-user-master-trace-clears-m'; file=$advanced; + before=' State.StatusRegister = (ushort)((State.StatusRegister | M68kCpuState.Supervisor) & ~0xc000);'; + after=' State.StatusRegister = (ushort)((State.StatusRegister | M68kCpuState.Supervisor) & ~0xd000);'; + proofCombination='/user-master/format0/'; proofPhase='/following'; proofReason='SR expected'} +) +foreach ($mutation in $userMasterMutations) { + $mutation.group='RestoredUserMaster'; $mutation.model='68040'; $mutation.milestone=6 + $mutation.userMaster=$true; $mutations += $mutation +} $instructionMutations = @( @{name='040-instruction-short-frame'; before=' RaiseUnbufferedReadAccessFault(fault, stackedProgramCounter, instruction: true);'; after=' RaiseFormat0Exception(VectorBusError, stackedProgramCounter, M68kInstructionTimingKey.IllegalInstruction);'; proofCombination='/instruction/opcode/'}, @@ -428,6 +446,7 @@ if ($Scope -eq 'RteValidationFault') { $mutations = @($mutations | Where-Object if ($Scope -eq 'RteRepair') { $mutations = @($mutations | Where-Object { $_.repair }) } if ($Scope -eq 'RteRetryTrace') { $mutations = @($mutations | Where-Object { $_.retryTrace }) } if ($Scope -eq 'RtePendingTrace') { $mutations = @($mutations | Where-Object { $_.pendingTrace }) } +if ($Scope -eq 'RteUserMaster') { $mutations = @($mutations | Where-Object { $_.userMaster }) } if ($Scope -eq 'InstructionFault') { $mutations = @($mutations | Where-Object { $_.instructionFault }) } if ($Scope -eq 'HandlerPrefetch') { $mutations = @($mutations | Where-Object { $_.handlerPrefetch }) } if ($Scope -eq 'EntryPrefetch') { $mutations = @($mutations | Where-Object { $_.entryPrefetch }) } @@ -544,15 +563,15 @@ try { throw 'Instruction-fault mutation omitted complete cases or intended semantic failure' } } - if ($mutation.retryTrace -or $mutation.pendingTrace) { + if ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if ($trx.TestRun.ResultSummary.Counters.executed -ne 4 -or $trx.TestRun.ResultSummary.Counters.failed -ne 4 -or $batches.Count -ne 4) { throw 'Retry-trace mutation omitted its complete four-batch selection' } foreach ($matrix in @('boundaries','chained')) { foreach ($route in @('scalar','batch')) { - $prefix = if ($mutation.pendingTrace) {'rte-pending-trace'} else {'rte-retry-trace'} - $cases = if ($mutation.pendingTrace) {if ($matrix -eq 'boundaries') {41472} else {870912}} else {if ($matrix -eq 'boundaries') {92736} else {1271808}} + $prefix = if ($mutation.userMaster) {'rte-user-master'} elseif ($mutation.pendingTrace) {'rte-pending-trace'} else {'rte-retry-trace'} + $cases = if ($mutation.userMaster) {if ($matrix -eq 'boundaries') {44736} else {714240}} elseif ($mutation.pendingTrace) {if ($matrix -eq 'boundaries') {41472} else {870912}} else {if ($matrix -eq 'boundaries') {92736} else {1271808}} $target = @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq "$prefix-$matrix-$route" }) if ($target.Count -ne 1 -or $target[0].logicalCases -ne $cases -or @($target[0].failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and @@ -624,7 +643,7 @@ try { } elseif ($mutation.entryPrefetch) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] - } elseif ($mutation.retryTrace -or $mutation.pendingTrace) { + } elseif ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.id.EndsWith($mutation.proofPhase, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] } elseif ($mutation.userRte) { diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 6bdf4a77..798da33b 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -160,6 +160,10 @@ try { $expected['rte-pending-trace-boundaries-batch'] = 41472 $expected['rte-pending-trace-chained-scalar'] = 870912 $expected['rte-pending-trace-chained-batch'] = 870912 + $expected['rte-user-master-boundaries-scalar'] = 44736 + $expected['rte-user-master-boundaries-batch'] = 44736 + $expected['rte-user-master-chained-scalar'] = 714240 + $expected['rte-user-master-chained-batch'] = 714240 $expected['rte-validation-batch'] = 129024 $expected['access-fault-batch-dispatch'] = 10368 $expected['rte-access-entry-double-fault'] = 98304 @@ -291,6 +295,7 @@ try { m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; m68040HandlerPrefetchScope=@{logicalCases=457728; batches=8; routes=@('accurate scalar','accurate batch'); bus='Physical map with no host code reader, including stable-map generation'; qualified='Four-long access-error entry window, every entry byte, odd handler addresses, retained data, flow/host/task/map invalidation and later instruction faults; denied cold boundaries perform no CPU fetch/state change'; remaining='Other exception-entry paths, host-reader/compiled fetch provenance, enabled caches/MMU, ordinary speculative deferral and physical timing'}; m68040UserTailScope=@{logicalCases=1687040; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); authority='Composition of MC68040UM 2.2.2.1, 8.1, 8.2.1/5, 8.4.2 and 8.4.6.7; software qualification, no executed hardware reference'; userM=@(0,1); initialBanks=@('ISP','MSP'); middleBanks=@('none','user','ISP','MSP'); ccr='all 32 canonical SR reads, 0/31 structural reads'; forms='0/2/3, invalid 4/15, normal/CM/CT/CU/CP49'; qualified='Live intermediate SR, supervisor exception stack selected by M, user-data TM=1, preserved USP frame and committed throwaways, bare return followed by privilege violation, executed seven-store repair with fresh throwaway bridge and following trace/continuation'; remaining='Internal restoration faults, untouched trace retry, physical bus-order/timing and hardware observation of composed user-tail fault behavior'}; + m68040RteUserMasterScope=@{logicalCases=1517952; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); restoredSr='S=0,M=1'; forms=@('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP49'); incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); qualified='User execution retains USP; synchronous completion trace, pending conversion and following trace select MSP while preserving M; all three stack banks, saved SR/PC/EA, pending consumption and handler returns checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='User-tail trace bridge, mixed-epoch provenance, other CP vectors, software trace-service routines and internal restoration'}; m68040RtePendingTraceScope=@{logicalCases=1824768; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); forms='CT/CU/CP49'; incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); qualified='Three original-frame stores preserve access SR; returned incoming trace cannot create an extra RTE trace on pending conversion; saved SR/PC/EA, format/vector, pending consumption, handler return and following traced BRA are checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='Other CP vectors under preserved-trace repair, user-tail trace bridge, mixed-epoch provenance, software trace-service routines, internal restoration and physical/hardware qualification'}; m68040RteRetryTraceScope=@{logicalCases=2729088; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); faultTailBanks=@('ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredBanks=@('user','ISP','MSP'); restoredTrace=@(0,0x8000,0x4000); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM'; qualified='Three original-frame repair stores leave saved access SR untouched; handler return restores incoming trace; successful original RTE triggers T1/T0 trace with restored SR/PC; trace-handler return and following instruction retain MOVEM EA and stack selection'; ccr='all 32 canonical, 0/31 every chained read byte'; remaining='User-tail trace bridge, mixed trace values within consumed throwaways, internal restoration and hardware/physical timing'}; m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, executed repair/retry after committed supervisor throwaways, and manual-derived user-tail fault/privilege/bridge repair; chained odd-PC saved-SR provenance, internal-restoration faults, user/mixed-epoch untouched-trace retry, CP context transfer, other real fault entry and physical timing remain unqualified'; From 635ecde475695a1744cde826c8ad5bc20c2ea497 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 02:38:50 +0300 Subject: [PATCH 053/211] Qualify preserved trace across 68040 CP vectors --- Copper68k.Tests/Synthetic/README.md | 30 +++++ .../SyntheticM68040RteRepairTests.cs | 53 +++++++-- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 105 ++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 50 +++++++++ scripts/test-copper68k-040-access-frames.ps1 | 35 +++--- .../test-copper68k-synthetic-mutations.ps1 | 29 ++++- scripts/test-copper68k-synthetic.ps1 | 9 +- 7 files changed, 277 insertions(+), 34 deletions(-) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 445a328e..90f80bc2 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -644,3 +644,33 @@ internal/data/writeback/reference gaps remain required. These synthetic results are software qualification, not hardware observations or physical timing. See the [restored-user qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-restored-user-m1--2026-10-06). + +### Preserved-trace repair for CP vectors 50–55 + +Four `OtherCpVectorsPreserveTrace` reports add 4,866,048 checked phases through +accurate scalar and one-instruction batch execution. They restore user M=0, +user M=1, ISP or MSP after a physical validation fault and three executed repair +stores. The CP event must retain its originally selected vector despite +conflicting handler-visible FPCR/FPSR/FPIAR. Format-3 saved SR/PC/EA, pending +context consumption, stack selection, handler return and following trace are +checked. Pending delivery cannot generate an extra automatic RTE trace. + +Canonical cases cover all CCRs; chained cases use CCR 0/31, every validation-read +byte, twelve supervisor paths and both alignments/VBRs. Both groups cross +incoming/restored 0/T1/T0 and all six vectors. Existing CP49 reports remain +separate. MC68040UM 9.6.2 explicitly permits vector 55 as a post-instruction +exception for register-to-memory unsupported data types; BSUN vector 48 is +outside this CP selection. FPU arithmetic is outside this suite. + +```powershell +./scripts/test-copper68k-synthetic-mutations.ps1 -Scope RteCpVectors -OutputDirectory artifacts/rte-cp-vectors-mutations +``` + +The gate requires the intended wrong-vector, extra-RTE-trace or unconsumed-context +diagnostic in all four complete reports. The complete 040 audit requires 60 tests, +54 reports, six fixed examples and thirteen input identities. The retained +480-case inventory still prevents completion. Pending software trace service, +user-tail trace bridges, mixed-epoch provenance and the earlier context/internal/ +data/writeback/reference gaps remain required. + +See the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md) for identities, proofs and remaining scope. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs index acec27e8..fd37a0d2 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs @@ -56,35 +56,48 @@ public sealed class SyntheticM68040RteRepairTests(ITestOutputHelper output) [Fact, Trait("Suite", "Synthetic")] public void RestoredUserMasterChainedBatch() => Audit(true, true, true, userMaster: true); - private void Audit(bool chained, bool keepTrace = false, bool batch = false, bool pendingTrace = false, bool userMaster = false) + [Fact, Trait("Suite", "Synthetic")] + public void OtherCpVectorsPreserveTraceBoundariesScalar() => Audit(false, true, cpVectors: true); + + [Fact, Trait("Suite", "Synthetic")] + public void OtherCpVectorsPreserveTraceBoundariesBatch() => Audit(false, true, true, cpVectors: true); + + [Fact, Trait("Suite", "Synthetic")] + public void OtherCpVectorsPreserveTraceChainedScalar() => Audit(true, true, cpVectors: true); + + [Fact, Trait("Suite", "Synthetic")] + public void OtherCpVectorsPreserveTraceChainedBatch() => Audit(true, true, true, cpVectors: true); + + private void Audit(bool chained, bool keepTrace = false, bool batch = false, bool pendingTrace = false, bool userMaster = false, bool cpVectors = false) { var bus = new SyntheticM68040RteValidationFaultTests.ValidationFaultBus(); var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); - var traceGroup = userMaster ? "rte-user-master" : pendingTrace ? "rte-pending-trace" : "rte-retry-trace"; + var traceGroup = cpVectors ? "rte-cp-vectors" : userMaster ? "rte-user-master" : pendingTrace ? "rte-pending-trace" : "rte-retry-trace"; var report = new CoverageBatch("68040", keepTrace ? $"{traceGroup}-{(chained ? "chained" : "boundaries")}-{(batch ? "batch" : "scalar")}" : chained ? "rte-repair-chained" : "rte-repair-boundaries"); foreach (var start in SupervisorBanks) foreach (var tail in chained ? SupervisorBanks : [start]) foreach (var middle in chained ? new[] { "none", "ISP", "MSP" } : ["none"]) - foreach (var result in userMaster ? new[] { "user-M" } : ["user", "ISP", "MSP"]) + foreach (var result in cpVectors ? new[] { "user", "user-M", "ISP", "MSP" } : userMaster ? ["user-M"] : ["user", "ISP", "MSP"]) foreach (var incoming in chained && !keepTrace ? new ushort[] { 0x8000 } : [0, 0x8000, 0x4000]) foreach (var restoredTrace in new ushort[] { 0, 0x8000, 0x4000 }) foreach (var alignment in chained ? new uint[] { 0, 1 } : [0]) foreach (var vbr in chained ? new uint[] { 0, 0x10000 } : [0x10000]) foreach (var ccr in chained ? new[] { 0, 31 } : Enumerable.Range(0, 32)) - foreach (var form in Forms.Where(f => userMaster || (pendingTrace ? f is "CT" or "CU" or "CP" : !keepTrace || f is not ("CT" or "CU" or "CP")))) + foreach (var form in cpVectors ? new[] { "CP50", "CP51", "CP52", "CP53", "CP54", "CP55" } + : Forms.Where(f => userMaster || (pendingTrace ? f is "CT" or "CU" or "CP" : !keepTrace || f is not ("CT" or "CU" or "CP")))) foreach (var (offset, width) in chained ? SyntheticM68040RteValidationFaultTests.Reads(form) : [(0u, 2)]) for (var faultByte = 0; faultByte < (chained ? width : 1); faultByte++) { var path = !chained ? new[] { start } : middle == "none" ? [start, tail] : new[] { start, middle, tail }; - Run(m, bus, report, path, result, incoming, restoredTrace, alignment, vbr, ccr, form, offset, width, faultByte, keepTrace, batch, pendingTrace, userMaster); + Run(m, bus, report, path, result, incoming, restoredTrace, alignment, vbr, ccr, form, offset, width, faultByte, keepTrace, batch, pendingTrace, userMaster, cpVectors); } report.Complete(output); } private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTests.ValidationFaultBus bus, CoverageBatch report, string[] path, string result, ushort incoming, ushort restoredTrace, - uint alignment, uint vbr, int ccr, string form, uint offset, int width, int faultByte, bool keepTrace, bool batch, bool pendingTrace, bool userMaster) + uint alignment, uint vbr, int ccr, string form, uint offset, int width, int faultByte, bool keepTrace, bool batch, bool pendingTrace, bool userMaster, bool cpVectors) { bus.Disarm(); m.Reset(ccr); var pointers = new Dictionary { ["user"] = 0x7800 + alignment, ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; @@ -102,8 +115,9 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes var tail = path[^1]; var frame = pointers[tail]; var accessFrame = frame - 60; var format = form.StartsWith("format") ? int.Parse(form[6..]) : form.StartsWith("invalid") ? int.Parse(form[7..]) : 7; var repairedFormat = form.StartsWith("invalid") ? 0 : format; - var continuation = form switch { "CM" => 0x1000, "CT" => 0x2000, "CU" => 0x4000, "CP" => 0x8000, _ => 0 }; - var vector = form switch { "CT" => 9, "CU" => 11, "CP" => 49, _ => 0 }; + var post = form.StartsWith("CP", StringComparison.Ordinal); + var continuation = post ? 0x8000 : form switch { "CM" => 0x1000, "CT" => 0x2000, "CU" => 0x4000, _ => 0 }; + var vector = post ? form == "CP" ? 49 : int.Parse(form[2..]) : form switch { "CT" => 9, "CU" => 11, _ => 0 }; var savedSr = Status(result, restoredTrace, ccr ^ 31); var priorSr = Status(tail, incoming, path.Length > 1 ? ccr ^ 31 : ccr); // Every repair changes SR and PC. Invalid formats are repaired to format 0. @@ -129,20 +143,35 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes m.InitializePhysical(PendingHandler, 0x4e73, 2); m.InitializePhysical(PendingHandler + 2, 0x7e7e, 2); m.InitializePhysical(vbr + 8, Handler, 4); foreach (var v in new[] { 9, 11, 49 }) m.InitializePhysical(vbr + (uint)v * 4, PendingHandler, 4); + if (cpVectors) m.InitializePhysical(vbr + (uint)vector * 4, PendingHandler, 4); _ = SyntheticExecution.Prepare(m, [0x4e73]); m.Core.State.SetUserStackPointer(0x7800 + alignment); m.Core.State.SetInterruptStackPointer(0x4700 + alignment); m.Core.State.SetMasterStackPointer(0x7400 + alignment); m.Core.State.StatusRegister = Status(path[0], incoming, ccr); m.Core.State.VectorBaseRegister = vbr; if (form == "CU") m.Core.State.M68040PendingFpuExceptions.Begin(2, 11, Target); - if (form == "CP") m.Core.State.M68040PendingFpuExceptions.Begin(3, 49, Target); + if (post) m.Core.State.M68040PendingFpuExceptions.Begin(3, vector, Target); + if (cpVectors) + { + // The pending vector is an input selected before suspension. These + // deliberately conflicting registers must not reselect its event. + m.Core.State.M68040Fpu.Fpcr = 0; + m.Core.State.M68040Fpu.Fpsr = 0x08008198; + m.Core.State.M68040Fpu.Fpiar = 0x1234abcd; + } var e = ArchitecturalExpectation.Capture(m); var sequence = m.Core.State.ExceptionSequence; + if (cpVectors) + { + e.ControlChecks["FPCR preserved"] = (s => s.M68040Fpu.Fpcr, 0); + e.ControlChecks["FPSR preserved"] = (s => s.M68040Fpu.Fpsr, 0x08008198); + e.ControlChecks["FPIAR preserved"] = (s => s.M68040Fpu.Fpiar, 0x1234abcd); + } e.ControlChecks["fault rejected once"] = (_ => (uint)bus.Rejected.Count, 1); e.ControlChecks["rejected original address"] = (_ => bus.Rejected.Count == 1 ? bus.Rejected[0].Address : uint.MaxValue, frame + offset); e.ControlChecks["rejected original width"] = (_ => bus.Rejected.Count == 1 ? (uint)bus.Rejected[0].Width : 0, (uint)width); e.ControlChecks["discarded PCs never fetched"] = (_ => bus.Accesses.Any(a => a.Address is 0xdead0001 or 0xdead0003) ? 1u : 0u, 0); e.ControlChecks["sentinel never executed"] = (s => s.D[7], e.D[7]); - if (form is "CU" or "CP") e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) == null ? 0u : 1u, 1); + if (form == "CU" || post) e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) == null ? 0u : 1u, 1); pointers[tail] = accessFrame; SetStacks(e, pointers, (ushort)(priorSr & ~0xc000)); e.Pc = Handler; e.ExceptionVector = 2; ExpectProvenance(e, sequence + 1, SyntheticMachine.Code, priorSr); SyntheticM68040RteValidationFaultTests.ExpectAccessFrame(m, e, accessFrame, priorSr, SyntheticMachine.Code, frame + offset, width); @@ -152,7 +181,7 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes phases.AddRange(["handler-return", "retry-RTE"]); if (keepTrace && incoming != 0 && vector == 0) phases.Add("retry-trace-return"); if (vector != 0) phases.Add("pending-return"); phases.Add("following"); - var id = $"68040/RTE/{(userMaster ? "user-master" : pendingTrace ? "pending-trace" : keepTrace ? "retry-trace" : "repair")}/{form}/path={string.Join('-', path)}/result={result}/incoming={incoming:X4}/T={restoredTrace:X4}/align={alignment}/VBR={vbr:X8}/read={offset}:{width}/fault-byte={faultByte}/op=4E73/ccr={ccr:X2}"; + var id = $"68040/RTE/{(cpVectors ? "cp-vectors" : userMaster ? "user-master" : pendingTrace ? "pending-trace" : keepTrace ? "retry-trace" : "repair")}/{form}/path={string.Join('-', path)}/result={result}/incoming={incoming:X4}/T={restoredTrace:X4}/align={alignment}/VBR={vbr:X8}/read={offset}:{width}/fault-byte={faultByte}/op=4E73/ccr={ccr:X2}"; var phase = 0; bool Step() { @@ -192,7 +221,7 @@ bool Step() if (vector != 0) { ExpectException(m, e, pointers, savedSr, vector, Target, Operand, sequence + 2); - if (form is "CU" or "CP") e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) == null ? 0u : 1u, 0); + if (form == "CU" || post) e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) == null ? 0u : 1u, 0); } // MC68040UM 8.2.6: the suspended RTE is traced only once it completes. // Both T1 and T0 trace RTE; the repaired frame's SR is saved in that diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 5b9c7cd8..e7f93d8f 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -4927,3 +4927,108 @@ the specific expected diagnostic, rather than the generic inventory rejection. Both scalar/batch canonical and chained restored-user reports are covered. All current fixture/source/binary identities still match after reference audits. Production CPU source remains unchanged; milestone 6 remains **in progress**. + +### 040 preserved-trace CP vectors 50–55 — 2026-10-06 + +`SyntheticM68040RteRepairTests` adds four `OtherCpVectorsPreserveTrace` reports +for the remaining original CP vectors 50–55 after a physical validation fault +and real three-store software repair. Existing CP49, CT/CU, normal/CM and restored +user-M matrices retain their own report identities and cardinalities. Expectations +compose MC68040UM 8.3, 8.4.6.2 and 8.4.6.7 with 9.6 table 9-9. Section 9.6.2 +explicitly permits vector 55 as a post-instruction exception for opclass 011 +unsupported data types. BSUN48 is outside this CP selection. These are synthetic +suspended contexts, not FPU arithmetic or observed hardware faults. + +The pending vector is an input selected before suspension. Handler-visible +FPCR=0, FPSR=08008198 and FPIAR=1234ABCD deliberately suggest another event; +RTE must preserve them and deliver the original vector. After three executed +stores repair SR/PC/format without changing the access frame's SR, handler return +restores incoming 0/T1/T0. The original RTE converts to format 3, with repaired +saved SR/PC/EA and no extra automatic RTE trace. Pending consumption, exception +sequence, all three stack pointers, guarded memory and batch counts/callbacks +are checked. A bare pending-handler RTE returns, and the following self-BRA +obeys the repaired trace bits. + +| Group, each scalar/batch | Phases | Combinations | +| --- | ---: | ---: | +| Canonical CCR boundaries | 110,592 | 432 | +| Chained validation-read bytes | 2,322,432 | 145,152 | + +All four reports total **4,866,048 checked phases**. Canonical cases cover all +32 CCRs. Structural cases reject every documented validation-read byte, use +CCR 0/31 and all twelve supervisor throwaway paths, both alignments/VBRs and +all incoming/restored traces. All four restored states (user M=0/M=1, ISP/MSP) +and all six vectors have separate identifiers. Consumed throwaways cannot be +replayed or their odd discarded PCs fetched. The initial focused run at +`artifacts/m6-cp-vectors-focused/` passes all four tests, with zero skips, +mismatches, unsupported execution or untested phases. + +The maintained `RteCpVectors` mutation scope requires the intended wrong-vector, +extra-RTE-trace or unconsumed-context diagnostic in each complete route/matrix. +The complete 040 command independently enumerates every CP50–55 combination; +it requires 60 tests, 54 reports, six fixed examples and thirteen input +identities. Ordinary reporting also requires all four groups, with their scope +recorded separately from the earlier CP49 matrices. + +Pending software trace service remains unqualified: the pending handler here +performs a bare RTE. User-tail trace bridges, mixed-epoch provenance, internal +restoration, chained odd-PC SR, general data/writeback/context transfer and the +broader model/reference/consolidation work remain required. The 480-case +inventory still fails completion. Milestone 6 remains **in progress**, +`roadmapComplete=false`. Enabled MMU, FPU arithmetic, physical pipeline/cache +timing and OS compatibility remain outside the roadmap. No old regression is +retired, public API changed or package published. + +The maintained `RteCpVectors` run in `artifacts/m6-cp-vectors-mutations/` detects +all three mutations. Each executes all four complete reports and requires the +intended retry-RTE diagnostic in both routes and matrices. Counts below are per +route, canonical/chained respectively: + +| Mutation | Mismatching | Dependent phases untested | Required diagnostic | +| --- | ---: | ---: | --- | +| Force original vector to 49 | 13,824 / 290,304 | 27,648 / 580,608 | CP50 retry delivers 49 | +| Add automatic RTE trace after CP | 9,216 / 193,536 | 18,432 / 387,072 | CP50 retry delivers 9 | +| Leave pending context unconsumed | 13,824 / 290,304 | 27,648 / 580,608 | CP50 retry retains pending delivery | + +Exact production source bytes match the preceding checkpoint after restoration; +the restored Release rebuild succeeds with zero warnings/errors. Deliberate +mutation failures and their dependent untested phases are proof evidence, not +production mismatches. The shared proof validator retains each earlier trace/ +restored-user scope's separate four-report cardinalities and identifiers. + +The complete current 040 run in `artifacts/m6-cp-vectors-audit/` executes all +**60 tests: 59 pass, one required inventory failure, zero skips**. All 54 reports, +six fixed examples and thirteen fixture/command identities are present. The +independent combination verifier checks **15,704,768 passing phases**, zero +mismatches/unsupported execution and the retained **480 untested requirements** +(15,705,248 total). The command's final rejection is exactly that inventory; +it is not a passing completion gate. + +The fresh full Release run in `artifacts/m6-cp-vectors-full/` passes **5,085 tests** +with eleven optional skips and zero failures. All nine qualified WinUAE presets +match their exact directory/callback/frame selections and CPU/adapter identities. +The acceptance build starts from `e26a8cc3cfd3caf27e0e525bf0eba9f239134e59` plus +the recorded fixture changes, with unchanged committed CPU tree +`795fd12d6c0237a22a5f92f4a96cc4823e0364c1`. CPU SHA-256 is +`614599a2840b2fa5860e27038873259cf74fea625144eebdd6a9a1a0a294b1d1`; +test/adapter SHA-256 is +`1a42b10c1bb128323b8254e9e655eadd4b73cb18202a2fe4550797c8939168ba`. +These identities describe the validation build, not a rebuild of the subsequent +checkpoint commit. The complete manifest's source, input and binary hashes all +match current bytes. + +Strict ordinary validation checks **30,673,430 logical cases in 644 batches**. +Fresh pinned SingleStepTests passes **312,500 cases in 125 files**; Musashi +passes **536 programs with 88 explicit exclusions** across all eight profiles. +Both requested adapters execute exactly one passing test with zero skips, and +all current acceptance binary/source/fixture identities still match afterward. +The original Basic reference failures remain separate historical evidence. + +All **17 fresh controls** in `artifacts/m6-cp-vectors-controls/` reject their +specific missing, shortened, foreign, redistributed or omitted-fixture +corruption. Both canonical/chained scalar/batch CP-vector reports are covered; +the controls require the intended diagnostic rather than accepting the generic +inventory rejection. Current source, fixture and assembly identities still +match after all checks. No production CPU fix, consumer rerun, package release +or old-test retirement is needed for this test-only addition. Milestone 6 +remains **in progress**. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 6c00f5ef..ef02467b 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1840,3 +1840,53 @@ identity controls reject their specific corruption diagnostics. Source/fixture/ binary identities still match after all acceptance checks. Milestone 6 remains **in progress**; the restored user M=1 software slice is qualified, while the full required restoration/reference/consolidation scope remains retained. + +### Milestone 6 preserved-trace CP vectors 50–55 — 2026-10-06 + +Four separate scalar/batch reports add **4,866,048 phases** for repaired CP50–55 +frames while preserving incoming trace. The initial focused run passes all four +without skips or missing phases. They cross restored user M=0/M=1, ISP/MSP, +incoming/restored 0/T1/T0 and all six original vectors. Canonical cases cover all +CCRs; structural cases reject every validation-read byte, use CCR 0/31 and all +twelve supervisor throwaway paths, with both alignments/VBRs. + +The pending vector is selected before suspension; conflicting FPCR/FPSR/FPIAR +values must neither reselect it nor change during the integer repair protocol. +Saved format-3 SR/PC/EA, pending consumption, all stack banks, handler return and +following traced BRA are checked. The independent expectations compose +MC68040UM 8.3, 8.4.6.2/7 and 9.6 table 9-9; 9.6.2 confirms vector-55 post-instruction +support for register-to-memory unsupported data types. BSUN48 is excluded from +this CP selection; arithmetic and actual FPU event generation remain outside +scope. + +The maintained `RteCpVectors` scope probes forced vector 49, extra automatic RTE +trace and an unconsumed context, requiring intended diagnostics in all four +reports. The complete 040 gate now requires 60 tests, 54 reports, six fixed +examples and thirteen input identities. CP49 retains its separate earlier +qualification. No production CPU correction was needed by the focused run. + +This closes preserved-trace supervisor-tail repair for the remaining CP vectors. +Software trace service, user-tail trace bridges, mixed-epoch provenance, internal +restoration, chained odd-PC, general data/writeback/context transfer and the +broader model/reference/consolidation requirements remain open. The required +480-case inventory continues to fail completion. Milestone 6 remains **in progress** +and `roadmapComplete=false`; no API/package change or regression retirement is +included. + +See the [qualification record](COPPER68K_REFERENCE_QUALIFICATION.md) for identities, proofs and remaining scope. + +Current acceptance passes **5,085 full Release CPU tests**, with eleven optional +skips and zero failures. All nine qualified WinUAE selections and identities +match; strict reporting checks **30,673,430 cases in 644 batches**. Fresh +SingleStepTests passes 312,500 cases / 125 files, and Musashi passes 536 programs +with 88 explicit exclusions. Both requested reference adapters execute without +skips. The complete 040 command executes 60 tests (59 passing, one required +inventory failure), with 15,704,768 passing phases, zero mismatches/unsupported +execution and the retained 480 untested requirements. Every independently +enumerated combination and all thirteen fixture/command identities match. + +All three maintained CP-vector mutations are detected with their intended +diagnostics in all four reports. Seventeen fresh report/fixture controls reject +their specific corruptions. Restored production source bytes and current +acceptance identities match; this is a test-only checkpoint. Milestone 6 remains +**in progress**, with the full required scope retained above. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index 8f2ef48f..e3868d78 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -43,6 +43,10 @@ $expected = [ordered]@{ 'rte-user-master-boundaries-batch' = @{cases=44736; combinations=180} 'rte-user-master-chained-scalar' = @{cases=714240; combinations=45792} 'rte-user-master-chained-batch' = @{cases=714240; combinations=45792} + 'rte-cp-vectors-boundaries-scalar' = @{cases=110592; combinations=432} + 'rte-cp-vectors-boundaries-batch' = @{cases=110592; combinations=432} + 'rte-cp-vectors-chained-scalar' = @{cases=2322432; combinations=145152} + 'rte-cp-vectors-chained-batch' = @{cases=2322432; combinations=145152} 'instruction-fault-frame' = @{cases=36864; combinations=576} 'instruction-fault-restart' = @{cases=79872; combinations=624} 'handler-prefetch-entry-scalar' = @{cases=196608; combinations=6144} @@ -77,7 +81,7 @@ if (-not $ValidateReportsOnly) { } $identity = [ordered]@{ schema=1; reference='MC68040UM'; url='https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf' - sections=@('2.2.2.1','7.6.1','7.6.3','8.1 figure 8-1','8.2.1','8.2.2','8.2.5','8.2.6','8.3','8.4','8.4.1','8.4.2','8.4.3','8.4.4','8.4.6.2','8.4.6.7'); softwareReferenceExecuted=$false + sections=@('2.2.2.1','7.6.1','7.6.3','8.1 figure 8-1','8.2.1','8.2.2','8.2.5','8.2.6','8.3','8.4','8.4.1','8.4.2','8.4.3','8.4.4','8.4.6.2','8.4.6.7','9.6 table 9-9','9.6.2'); softwareReferenceExecuted=$false sourceCommit=(& git -C $repo rev-parse HEAD); cpuCommittedTree=(& git -C $repo rev-parse HEAD:Copper68k) cpuSourceFiles=@(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} @@ -93,7 +97,7 @@ if (-not $ValidateReportsOnly) { 'Multiple continuation bits are architecturally undefined and excluded', 'Selected active accurate-batch paths verify counts/callbacks and scalar/batch bus/cycle policy; generic short operand frames do not qualify architectural format-7 data restart', 'Instruction-fault fixtures use cache-disabled accurate execution and physical-map rejection; speculative deferral, enabled caches/MMU and compiled fetch PC provenance remain unqualified', - 'Legacy supervisor repair clears saved trace; normal/CM retry traces completion, while pending CT/CU/CP49 delivery suppresses an extra RTE trace. Other CP vectors under preserved-trace repair, software trace service and user-tail/mixed-epoch protocols remain required', + 'Legacy supervisor repair clears saved trace; normal/CM retry traces completion, while pending CT/CU/CP49 and separate CP50-55 delivery suppress extra RTE trace. Software trace service and user-tail/mixed-epoch protocols remain required', 'User-tail fault expectations compose documented throwaway live-SR rules with general supervisor exception entry; unusual combined hardware behavior has not been observed', 'User-tail untouched-trace retry, internal-restoration double faults, cache/MMU/compiled handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', 'Direct odd-RTE saved-SR ordering uses documentary WinUAE 5d22d336, not an executed hardware oracle') @@ -123,8 +127,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 56 -or [int]$counters.total -ne 56 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (50 batches, 6 fixed examples)' +if ([int]$counters.executed -ne 60 -or [int]$counters.total -ne 60 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (54 batches, 6 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -224,31 +228,32 @@ foreach ($group in $expected.Keys) { } } } } } - } elseif ($group -in @('rte-repair-boundaries','rte-repair-chained') -or $group.StartsWith('rte-retry-trace-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-pending-trace-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-user-master-', [StringComparison]::Ordinal)) { + } elseif ($group -in @('rte-repair-boundaries','rte-repair-chained') -or $group.StartsWith('rte-retry-trace-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-pending-trace-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-user-master-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-cp-vectors-', [StringComparison]::Ordinal)) { + $cpVectors = $group.StartsWith('rte-cp-vectors-', [StringComparison]::Ordinal) $userMaster = $group.StartsWith('rte-user-master-', [StringComparison]::Ordinal) $pendingTrace = $group.StartsWith('rte-pending-trace-', [StringComparison]::Ordinal) - $keepTrace = $userMaster -or $pendingTrace -or $group.StartsWith('rte-retry-trace-', [StringComparison]::Ordinal) - $chained = $group -eq 'rte-repair-chained' -or $group.StartsWith('rte-retry-trace-chained-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-pending-trace-chained-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-user-master-chained-', [StringComparison]::Ordinal) + $keepTrace = $cpVectors -or $userMaster -or $pendingTrace -or $group.StartsWith('rte-retry-trace-', [StringComparison]::Ordinal) + $chained = $group -eq 'rte-repair-chained' -or $group.StartsWith('rte-retry-trace-chained-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-pending-trace-chained-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-user-master-chained-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-cp-vectors-chained-', [StringComparison]::Ordinal) $paths = if ($chained) { @('ISP-ISP','ISP-ISP-ISP','ISP-MSP-ISP','ISP-MSP','ISP-ISP-MSP','ISP-MSP-MSP', 'MSP-ISP','MSP-ISP-ISP','MSP-MSP-ISP','MSP-MSP','MSP-ISP-MSP','MSP-MSP-MSP') } else { @('ISP','MSP') } - foreach ($path in $paths) { foreach ($result in $(if ($userMaster) { @('user-M') } else { @('user','ISP','MSP') })) { + foreach ($path in $paths) { foreach ($result in $(if ($cpVectors) { @('user','user-M','ISP','MSP') } elseif ($userMaster) { @('user-M') } else { @('user','ISP','MSP') })) { foreach ($incoming in $(if ($chained -and -not $keepTrace) { @(0x8000) } else { @(0,0x8000,0x4000) })) { foreach ($trace in @(0,0x8000,0x4000)) { foreach ($alignment in $(if ($chained) { @(0,1) } else { @(0) })) { foreach ($vbr in $(if ($chained) { @(0,0x10000) } else { @(0x10000) })) { - foreach ($form in $(if ($pendingTrace) { @('CT','CU','CP') } elseif ($keepTrace -and -not $userMaster) { @('format0','format2','format3','invalid4','invalid15','normal','CM') } else { @('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP') })) { + foreach ($form in $(if ($cpVectors) { @('CP50','CP51','CP52','CP53','CP54','CP55') } elseif ($pendingTrace) { @('CT','CU','CP') } elseif ($keepTrace -and -not $userMaster) { @('format0','format2','format3','invalid4','invalid15','normal','CM') } else { @('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP') })) { # Literal read ranges are independent of the fixture iterator. $reads = @('0:2') if ($chained) { $reads += @('2:4','6:2') - if ($form -in @('normal','CM','CT','CU','CP')) { $reads += '12:2' } - if ($form -in @('CM','CT','CU','CP')) { $reads += '8:4' } + if ($cpVectors -or $form -in @('normal','CM','CT','CU','CP')) { $reads += '12:2' } + if ($cpVectors -or $form -in @('CM','CT','CU','CP')) { $reads += '8:4' } } foreach ($read in $reads) { $bytes = if ($chained) { [int]$read.Split(':')[1] } else { 1 } for ($byte=0; $byte -lt $bytes; $byte++) { - $key = '68040/RTE/{0}/{1}/path={2}/result={3}/incoming={4:X4}/T={5:X4}/align={6}/VBR={7:X8}/read={8}/fault-byte={9}' -f $(if ($userMaster) {'user-master'} elseif ($pendingTrace) {'pending-trace'} elseif ($keepTrace) {'retry-trace'} else {'repair'}),$form,$path,$result,$incoming,$trace,$alignment,$vbr,$read,$byte - $phases = if ($pendingTrace -or ($userMaster -and $form -in @('CT','CU','CP'))) { 8 } elseif ($keepTrace) { $(if ($incoming -eq 0) {7} else {8}) } elseif ($form -in @('CT','CU','CP')) { 9 } else { 8 } + $key = '68040/RTE/{0}/{1}/path={2}/result={3}/incoming={4:X4}/T={5:X4}/align={6}/VBR={7:X8}/read={8}/fault-byte={9}' -f $(if ($cpVectors) {'cp-vectors'} elseif ($userMaster) {'user-master'} elseif ($pendingTrace) {'pending-trace'} elseif ($keepTrace) {'retry-trace'} else {'repair'}),$form,$path,$result,$incoming,$trace,$alignment,$vbr,$read,$byte + $phases = if ($cpVectors -or $pendingTrace -or ($userMaster -and $form -in @('CT','CU','CP'))) { 8 } elseif ($keepTrace) { $(if ($incoming -eq 0) {7} else {8}) } elseif ($form -in @('CT','CU','CP')) { 9 } else { 8 } $ccrs = if ($chained) { 2 } else { 32 } $expectedCombinations[$key] = $phases * $ccrs } @@ -437,8 +442,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 56 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 60 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=10839200; xunitBatches=50; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=15705248; xunitBatches=54; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index 6f3ae4b7..3cbe6c38 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','UserRteFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','UserRteFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -376,6 +376,24 @@ foreach ($mutation in $userMasterMutations) { $mutation.group='RestoredUserMaster'; $mutation.model='68040'; $mutation.milestone=6 $mutation.userMaster=$true; $mutations += $mutation } +$cpVectorMutations = @( + @{name='040-cp-vectors-force-49'; file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs'; + before=' var vector = continuation == 0x8000 ? pending!.Vector : continuation == 0x4000 ? 11 : 9;'; + after=' var vector = continuation == 0x8000 ? 49 : continuation == 0x4000 ? 11 : 9;'; + proofCombination='/cp-vectors/CP50/'; proofPhase='/retry-RTE'; proofReason='Expected exception 50, actual 49'}, + @{name='040-cp-vectors-extra-rte-trace'; file=$advanced; + before=' if (exception && (_profile.Model is M68kAcceleratorModel.M68040 or M68kAcceleratorModel.M68060 ||'; + after=' if (exception && State.LastExceptionVector is not (50 or 51 or 52 or 53 or 54 or 55) && (_profile.Model is M68kAcceleratorModel.M68040 or M68kAcceleratorModel.M68060 ||'; + proofCombination='/cp-vectors/CP50/'; proofPhase='/retry-RTE'; proofReason='Expected exception 50, actual 9'}, + @{name='040-cp-vectors-context-not-consumed'; file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs'; + before=' State.M68040PendingFpuExceptions.Complete(pending);'; + after=' _ = State.M68040PendingFpuExceptions.Find(3);'; + proofCombination='/cp-vectors/CP50/'; proofPhase='/retry-RTE'; proofReason='pending delivery expected'} +) +foreach ($mutation in $cpVectorMutations) { + $mutation.group='OtherCpVectorsPreserveTrace'; $mutation.model='68040'; $mutation.milestone=6 + $mutation.cpVectors=$true; $mutations += $mutation +} $instructionMutations = @( @{name='040-instruction-short-frame'; before=' RaiseUnbufferedReadAccessFault(fault, stackedProgramCounter, instruction: true);'; after=' RaiseFormat0Exception(VectorBusError, stackedProgramCounter, M68kInstructionTimingKey.IllegalInstruction);'; proofCombination='/instruction/opcode/'}, @@ -447,6 +465,7 @@ if ($Scope -eq 'RteRepair') { $mutations = @($mutations | Where-Object { $_.repa if ($Scope -eq 'RteRetryTrace') { $mutations = @($mutations | Where-Object { $_.retryTrace }) } if ($Scope -eq 'RtePendingTrace') { $mutations = @($mutations | Where-Object { $_.pendingTrace }) } if ($Scope -eq 'RteUserMaster') { $mutations = @($mutations | Where-Object { $_.userMaster }) } +if ($Scope -eq 'RteCpVectors') { $mutations = @($mutations | Where-Object { $_.cpVectors }) } if ($Scope -eq 'InstructionFault') { $mutations = @($mutations | Where-Object { $_.instructionFault }) } if ($Scope -eq 'HandlerPrefetch') { $mutations = @($mutations | Where-Object { $_.handlerPrefetch }) } if ($Scope -eq 'EntryPrefetch') { $mutations = @($mutations | Where-Object { $_.entryPrefetch }) } @@ -563,15 +582,15 @@ try { throw 'Instruction-fault mutation omitted complete cases or intended semantic failure' } } - if ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster) { + if ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if ($trx.TestRun.ResultSummary.Counters.executed -ne 4 -or $trx.TestRun.ResultSummary.Counters.failed -ne 4 -or $batches.Count -ne 4) { throw 'Retry-trace mutation omitted its complete four-batch selection' } foreach ($matrix in @('boundaries','chained')) { foreach ($route in @('scalar','batch')) { - $prefix = if ($mutation.userMaster) {'rte-user-master'} elseif ($mutation.pendingTrace) {'rte-pending-trace'} else {'rte-retry-trace'} - $cases = if ($mutation.userMaster) {if ($matrix -eq 'boundaries') {44736} else {714240}} elseif ($mutation.pendingTrace) {if ($matrix -eq 'boundaries') {41472} else {870912}} else {if ($matrix -eq 'boundaries') {92736} else {1271808}} + $prefix = if ($mutation.cpVectors) {'rte-cp-vectors'} elseif ($mutation.userMaster) {'rte-user-master'} elseif ($mutation.pendingTrace) {'rte-pending-trace'} else {'rte-retry-trace'} + $cases = if ($mutation.cpVectors) {if ($matrix -eq 'boundaries') {110592} else {2322432}} elseif ($mutation.userMaster) {if ($matrix -eq 'boundaries') {44736} else {714240}} elseif ($mutation.pendingTrace) {if ($matrix -eq 'boundaries') {41472} else {870912}} else {if ($matrix -eq 'boundaries') {92736} else {1271808}} $target = @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq "$prefix-$matrix-$route" }) if ($target.Count -ne 1 -or $target[0].logicalCases -ne $cases -or @($target[0].failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and @@ -643,7 +662,7 @@ try { } elseif ($mutation.entryPrefetch) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] - } elseif ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster) { + } elseif ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.id.EndsWith($mutation.proofPhase, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] } elseif ($mutation.userRte) { diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 798da33b..e6baf500 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -164,6 +164,10 @@ try { $expected['rte-user-master-boundaries-batch'] = 44736 $expected['rte-user-master-chained-scalar'] = 714240 $expected['rte-user-master-chained-batch'] = 714240 + $expected['rte-cp-vectors-boundaries-scalar'] = 110592 + $expected['rte-cp-vectors-boundaries-batch'] = 110592 + $expected['rte-cp-vectors-chained-scalar'] = 2322432 + $expected['rte-cp-vectors-chained-batch'] = 2322432 $expected['rte-validation-batch'] = 129024 $expected['access-fault-batch-dispatch'] = 10368 $expected['rte-access-entry-double-fault'] = 98304 @@ -295,8 +299,9 @@ try { m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; m68040HandlerPrefetchScope=@{logicalCases=457728; batches=8; routes=@('accurate scalar','accurate batch'); bus='Physical map with no host code reader, including stable-map generation'; qualified='Four-long access-error entry window, every entry byte, odd handler addresses, retained data, flow/host/task/map invalidation and later instruction faults; denied cold boundaries perform no CPU fetch/state change'; remaining='Other exception-entry paths, host-reader/compiled fetch provenance, enabled caches/MMU, ordinary speculative deferral and physical timing'}; m68040UserTailScope=@{logicalCases=1687040; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); authority='Composition of MC68040UM 2.2.2.1, 8.1, 8.2.1/5, 8.4.2 and 8.4.6.7; software qualification, no executed hardware reference'; userM=@(0,1); initialBanks=@('ISP','MSP'); middleBanks=@('none','user','ISP','MSP'); ccr='all 32 canonical SR reads, 0/31 structural reads'; forms='0/2/3, invalid 4/15, normal/CM/CT/CU/CP49'; qualified='Live intermediate SR, supervisor exception stack selected by M, user-data TM=1, preserved USP frame and committed throwaways, bare return followed by privilege violation, executed seven-store repair with fresh throwaway bridge and following trace/continuation'; remaining='Internal restoration faults, untouched trace retry, physical bus-order/timing and hardware observation of composed user-tail fault behavior'}; - m68040RteUserMasterScope=@{logicalCases=1517952; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); restoredSr='S=0,M=1'; forms=@('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP49'); incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); qualified='User execution retains USP; synchronous completion trace, pending conversion and following trace select MSP while preserving M; all three stack banks, saved SR/PC/EA, pending consumption and handler returns checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='User-tail trace bridge, mixed-epoch provenance, other CP vectors, software trace-service routines and internal restoration'}; - m68040RtePendingTraceScope=@{logicalCases=1824768; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); forms='CT/CU/CP49'; incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); qualified='Three original-frame stores preserve access SR; returned incoming trace cannot create an extra RTE trace on pending conversion; saved SR/PC/EA, format/vector, pending consumption, handler return and following traced BRA are checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='Other CP vectors under preserved-trace repair, user-tail trace bridge, mixed-epoch provenance, software trace-service routines, internal restoration and physical/hardware qualification'}; + m68040RteCpVectorsScope=@{logicalCases=4866048; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); vectors=@(50,51,52,53,54,55); restoredBanks=@('user','user-M','ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); qualified='Original CP vector survives validation faults, three repair stores and incoming trace; format-3 saved SR/PC/EA, pending-context consumption, all stack banks, FPU register preservation, handler return and following traced BRA checked'; ccr='all 32 canonical, 0/31 every validation-read byte'; remaining='Pending software trace service, user-tail trace bridge, mixed-epoch provenance, context transfer and internal restoration'}; + m68040RteUserMasterScope=@{logicalCases=1517952; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); restoredSr='S=0,M=1'; forms=@('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP49'); incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); qualified='User execution retains USP; synchronous completion trace, pending conversion and following trace select MSP while preserving M; all three stack banks, saved SR/PC/EA, pending consumption and handler returns checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='User-tail trace bridge, mixed-epoch provenance, software trace-service routines and internal restoration'}; + m68040RtePendingTraceScope=@{logicalCases=1824768; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); forms='CT/CU/CP49'; incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); qualified='Three original-frame stores preserve access SR; returned incoming trace cannot create an extra RTE trace on pending conversion; saved SR/PC/EA, format/vector, pending consumption, handler return and following traced BRA are checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='User-tail trace bridge, mixed-epoch provenance, software trace-service routines, internal restoration and physical/hardware qualification'}; m68040RteRetryTraceScope=@{logicalCases=2729088; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); faultTailBanks=@('ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredBanks=@('user','ISP','MSP'); restoredTrace=@(0,0x8000,0x4000); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM'; qualified='Three original-frame repair stores leave saved access SR untouched; handler return restores incoming trace; successful original RTE triggers T1/T0 trace with restored SR/PC; trace-handler return and following instruction retain MOVEM EA and stack selection'; ccr='all 32 canonical, 0/31 every chained read byte'; remaining='User-tail trace bridge, mixed trace values within consumed throwaways, internal restoration and hardware/physical timing'}; m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, executed repair/retry after committed supervisor throwaways, and manual-derived user-tail fault/privilege/bridge repair; chained odd-PC saved-SR provenance, internal-restoration faults, user/mixed-epoch untouched-trace retry, CP context transfer, other real fault entry and physical timing remain unqualified'; m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='Internal restoration, user/mixed-epoch untouched incoming-trace retry and other real access faults; normal/CM preserved-trace and user-tail repair are qualified separately'}; From e52243255c7dd0d1f608954399915810eaeafd78 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 03:53:40 +0300 Subject: [PATCH 054/211] Qualify executed 68040 software trace service --- Copper68k.Tests/Synthetic/README.md | 35 +++++ .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- .../SyntheticM68040RteRepairTests.cs | 90 ++++++++++--- .../SyntheticM68040SoftwareTraceProgram.cs | 126 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 89 +++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 49 +++++++ scripts/test-copper68k-040-access-frames.ps1 | 50 ++++--- .../test-copper68k-synthetic-mutations.ps1 | 42 +++++- scripts/test-copper68k-synthetic.ps1 | 11 +- 9 files changed, 452 insertions(+), 42 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040SoftwareTraceProgram.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 90f80bc2..f4b16c96 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -674,3 +674,38 @@ user-tail trace bridges, mixed-epoch provenance and the earlier context/internal data/writeback/reference gaps remain required. See the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md) for identities, proofs and remaining scope. + +### Executed pending-exception software trace service + +`SoftwareTraceService` adds separate scalar/batch canonical and chained reports. +After the real validation fault and three-store repair, a CU/CP handler executes +BTST/BNE on the saved SR. Synthetic CU metadata selects linear or taken-flow PC +completion; post-instruction CP metadata selects a completed two-word FMOVE. +T1 always requests service, while T0 requests service only for CU taken flow. +The handler adjusts the PC when required, converts format 3 to format 2, supplies +the original instruction address and reads vector 9 from the relocated table. +It pushes that software call target and executes RTS, preserving all registers +and leaving the trace frame on the selected exception stack. + +The trace handler writes one marker, optionally clears stacked trace bits and +executes RTE. Every instruction checks state, guarded memory and all stack banks; +software calls preserve the hardware exception counter and provenance. The +following BRA independently checks resumed hardware tracing. Canonical cases +cross all CCRs, incoming/restored traces, four restored banks and CU/CP49–55. +Structural cases cover every validation-read byte, all twelve supervisor paths, +both alignments/VBRs and CCR 0/31, with incoming T1 and all restored traces. + +The nominal CU/CP instruction completion metadata is an input to this integer +handler protocol. Actual FPU emulation/arithmetic and hardware fault observations +remain outside the qualified scope. User-tail and mixed-epoch trace bridges, +internal restoration and all other retained restoration/reference gaps remain +required. Fixed golden program words audit the fixture encodings separately. + +```powershell +./scripts/test-copper68k-synthetic-mutations.ps1 -Scope RteSoftwareTrace -OutputDirectory artifacts/rte-software-trace-mutations +``` + +The four reports require 514,560 canonical / 3,601,920 chained phases per route, +8,232,960 total. The complete 040 audit requires 65 tests, 58 reporting batches, +seven fixed examples and fourteen input identities. The retained 480-case +inventory continues to fail completion; milestone 6 remains in progress. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index f5f5f8a1..bae831c2 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -33,7 +33,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() "real-access-fault-entry", "writeback-handler", "CP-context-transferred-vector" }) report.Record($"68040/RTE/format7/{form}/bank={bank}/op=4E73/ccr={ccr:X2}", "untested", form == "frame-validation-fault" - ? "Supervisor/user-tail validation and normal/CM and pending CT/CU/CP preserved-trace retry have separate software coverage; internal restoration and user/mixed-epoch trace protocols remain required. Physical/hardware behavior is separately unqualified" + ? "Supervisor/user-tail validation, normal/CM and pending CT/CU/CP preserved-trace retry, and executed pending software trace service have separate software coverage; internal restoration and user/mixed-epoch trace protocols remain required. Physical/hardware behavior is separately unqualified" : form == "real-access-fault-entry" ? "Cache-disabled accurate instruction-fetch frames/restart are covered separately; data fault/writeback restart, enabled-cache deferral and compiled instruction-PC provenance remain required" : "Required fault/context protocol has no independently qualified execution fixture yet"); diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs index fd37a0d2..ab9fa7cb 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs @@ -68,36 +68,71 @@ public sealed class SyntheticM68040RteRepairTests(ITestOutputHelper output) [Fact, Trait("Suite", "Synthetic")] public void OtherCpVectorsPreserveTraceChainedBatch() => Audit(true, true, true, cpVectors: true); - private void Audit(bool chained, bool keepTrace = false, bool batch = false, bool pendingTrace = false, bool userMaster = false, bool cpVectors = false) + [Fact, Trait("Suite", "Synthetic")] + public void SoftwareTraceServiceBoundariesScalar() => Audit(false, true, traceService: true); + + [Fact, Trait("Suite", "Synthetic")] + public void SoftwareTraceServiceBoundariesBatch() => Audit(false, true, true, traceService: true); + + [Fact, Trait("Suite", "Synthetic")] + public void SoftwareTraceServiceChainedScalar() => Audit(true, true, traceService: true); + + [Fact, Trait("Suite", "Synthetic")] + public void SoftwareTraceServiceChainedBatch() => Audit(true, true, true, traceService: true); + + [Fact] + public void SoftwareTraceProgramsUseFixedReferenceEncodings() + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); + m.Reset(0); + new SyntheticM68040SoftwareTraceProgram("CP49", false, PendingHandler, Target, 0x10000).Initialize(m); + ushort[] cp = [0x0817, 7, 0x6602, 0x4e73, 0x3f7c, 0x2024, 6, + 0x2f7c, 0, 0x5ffc, 8, 0x2f39, 1, 0x0024, 0x4e75, 0x7e7e]; + for (var n = 0; n < cp.Length; n++) Assert.Equal(cp[n], Word(PendingHandler + (uint)n * 2)); + m.Reset(0); + new SyntheticM68040SoftwareTraceProgram("CU-flow", true, PendingHandler, Target, 0).Initialize(m); + ushort[] cu = [0x2f7c, 0, 0x6004, 2, 0x0817, 7, 0x6608, 0x0817, 6, 0x6602, 0x4e73, + 0x3f7c, 0x2024, 6, 0x2f7c, 0, 0x6000, 8, 0x2f39, 0, 0x0024, 0x4e75, 0x7e7e]; + for (var n = 0; n < cu.Length; n++) Assert.Equal(cu[n], Word(PendingHandler + (uint)n * 2)); + ushort[] trace = [0x23fc, 0x1357, 0x9bdf, 0, 0x8000, 0x0257, 0x3fff, 0x4e73, 0x7e7e]; + for (var n = 0; n < trace.Length; n++) Assert.Equal(trace[n], Word(SyntheticM68040SoftwareTraceProgram.TraceHandler + (uint)n * 2)); + ushort Word(uint at) => (ushort)((m.Bus.Peek(at) << 8) | m.Bus.Peek(at + 1)); + } + + private void Audit(bool chained, bool keepTrace = false, bool batch = false, bool pendingTrace = false, bool userMaster = false, bool cpVectors = false, bool traceService = false) { var bus = new SyntheticM68040RteValidationFaultTests.ValidationFaultBus(); var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); - var traceGroup = cpVectors ? "rte-cp-vectors" : userMaster ? "rte-user-master" : pendingTrace ? "rte-pending-trace" : "rte-retry-trace"; + var traceGroup = traceService ? "rte-software-trace" : cpVectors ? "rte-cp-vectors" : userMaster ? "rte-user-master" : pendingTrace ? "rte-pending-trace" : "rte-retry-trace"; var report = new CoverageBatch("68040", keepTrace ? $"{traceGroup}-{(chained ? "chained" : "boundaries")}-{(batch ? "batch" : "scalar")}" : chained ? "rte-repair-chained" : "rte-repair-boundaries"); foreach (var start in SupervisorBanks) foreach (var tail in chained ? SupervisorBanks : [start]) foreach (var middle in chained ? new[] { "none", "ISP", "MSP" } : ["none"]) - foreach (var result in cpVectors ? new[] { "user", "user-M", "ISP", "MSP" } : userMaster ? ["user-M"] : ["user", "ISP", "MSP"]) - foreach (var incoming in chained && !keepTrace ? new ushort[] { 0x8000 } : [0, 0x8000, 0x4000]) + foreach (var result in cpVectors || traceService ? new[] { "user", "user-M", "ISP", "MSP" } : userMaster ? ["user-M"] : ["user", "ISP", "MSP"]) + // Canonical service cases cross every incoming trace. Structural cases + // use T1 while independently crossing every restored trace/read byte. + foreach (var incoming in chained && (!keepTrace || traceService) ? new ushort[] { 0x8000 } : [0, 0x8000, 0x4000]) foreach (var restoredTrace in new ushort[] { 0, 0x8000, 0x4000 }) foreach (var alignment in chained ? new uint[] { 0, 1 } : [0]) foreach (var vbr in chained ? new uint[] { 0, 0x10000 } : [0x10000]) foreach (var ccr in chained ? new[] { 0, 31 } : Enumerable.Range(0, 32)) - foreach (var form in cpVectors ? new[] { "CP50", "CP51", "CP52", "CP53", "CP54", "CP55" } + foreach (var form in traceService ? new[] { "CU-linear", "CU-flow", "CP49", "CP50", "CP51", "CP52", "CP53", "CP54", "CP55" } + : cpVectors ? ["CP50", "CP51", "CP52", "CP53", "CP54", "CP55"] : Forms.Where(f => userMaster || (pendingTrace ? f is "CT" or "CU" or "CP" : !keepTrace || f is not ("CT" or "CU" or "CP")))) + foreach (var clearServiceTrace in traceService ? new[] { false, true } : [false]) foreach (var (offset, width) in chained ? SyntheticM68040RteValidationFaultTests.Reads(form) : [(0u, 2)]) for (var faultByte = 0; faultByte < (chained ? width : 1); faultByte++) { var path = !chained ? new[] { start } : middle == "none" ? [start, tail] : new[] { start, middle, tail }; - Run(m, bus, report, path, result, incoming, restoredTrace, alignment, vbr, ccr, form, offset, width, faultByte, keepTrace, batch, pendingTrace, userMaster, cpVectors); + Run(m, bus, report, path, result, incoming, restoredTrace, alignment, vbr, ccr, form, offset, width, faultByte, keepTrace, batch, pendingTrace, userMaster, cpVectors, traceService, clearServiceTrace); } report.Complete(output); } private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTests.ValidationFaultBus bus, CoverageBatch report, string[] path, string result, ushort incoming, ushort restoredTrace, - uint alignment, uint vbr, int ccr, string form, uint offset, int width, int faultByte, bool keepTrace, bool batch, bool pendingTrace, bool userMaster, bool cpVectors) + uint alignment, uint vbr, int ccr, string form, uint offset, int width, int faultByte, bool keepTrace, bool batch, bool pendingTrace, bool userMaster, bool cpVectors, bool traceService, bool clearServiceTrace) { bus.Disarm(); m.Reset(ccr); var pointers = new Dictionary { ["user"] = 0x7800 + alignment, ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; @@ -116,8 +151,9 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes var format = form.StartsWith("format") ? int.Parse(form[6..]) : form.StartsWith("invalid") ? int.Parse(form[7..]) : 7; var repairedFormat = form.StartsWith("invalid") ? 0 : format; var post = form.StartsWith("CP", StringComparison.Ordinal); - var continuation = post ? 0x8000 : form switch { "CM" => 0x1000, "CT" => 0x2000, "CU" => 0x4000, _ => 0 }; - var vector = post ? form == "CP" ? 49 : int.Parse(form[2..]) : form switch { "CT" => 9, "CU" => 11, _ => 0 }; + var unimplemented = form.StartsWith("CU", StringComparison.Ordinal); + var continuation = post ? 0x8000 : unimplemented ? 0x4000 : form switch { "CM" => 0x1000, "CT" => 0x2000, _ => 0 }; + var vector = post ? form == "CP" ? 49 : int.Parse(form[2..]) : unimplemented ? 11 : form == "CT" ? 9 : 0; var savedSr = Status(result, restoredTrace, ccr ^ 31); var priorSr = Status(tail, incoming, path.Length > 1 ? ccr ^ 31 : ccr); // Every repair changes SR and PC. Invalid formats are repaired to format 0. @@ -143,15 +179,17 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes m.InitializePhysical(PendingHandler, 0x4e73, 2); m.InitializePhysical(PendingHandler + 2, 0x7e7e, 2); m.InitializePhysical(vbr + 8, Handler, 4); foreach (var v in new[] { 9, 11, 49 }) m.InitializePhysical(vbr + (uint)v * 4, PendingHandler, 4); - if (cpVectors) m.InitializePhysical(vbr + (uint)vector * 4, PendingHandler, 4); + if (cpVectors || traceService) m.InitializePhysical(vbr + (uint)vector * 4, PendingHandler, 4); + var service = traceService ? new SyntheticM68040SoftwareTraceProgram(form, clearServiceTrace, PendingHandler, Target, vbr) : null; + service?.Initialize(m); _ = SyntheticExecution.Prepare(m, [0x4e73]); m.Core.State.SetUserStackPointer(0x7800 + alignment); m.Core.State.SetInterruptStackPointer(0x4700 + alignment); m.Core.State.SetMasterStackPointer(0x7400 + alignment); m.Core.State.StatusRegister = Status(path[0], incoming, ccr); m.Core.State.VectorBaseRegister = vbr; - if (form == "CU") m.Core.State.M68040PendingFpuExceptions.Begin(2, 11, Target); + if (unimplemented) m.Core.State.M68040PendingFpuExceptions.Begin(2, 11, Target); if (post) m.Core.State.M68040PendingFpuExceptions.Begin(3, vector, Target); - if (cpVectors) + if (cpVectors || traceService) { // The pending vector is an input selected before suspension. These // deliberately conflicting registers must not reselect its event. @@ -160,7 +198,7 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes m.Core.State.M68040Fpu.Fpiar = 0x1234abcd; } var e = ArchitecturalExpectation.Capture(m); var sequence = m.Core.State.ExceptionSequence; - if (cpVectors) + if (cpVectors || traceService) { e.ControlChecks["FPCR preserved"] = (s => s.M68040Fpu.Fpcr, 0); e.ControlChecks["FPSR preserved"] = (s => s.M68040Fpu.Fpsr, 0x08008198); @@ -171,7 +209,7 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes e.ControlChecks["rejected original width"] = (_ => bus.Rejected.Count == 1 ? (uint)bus.Rejected[0].Width : 0, (uint)width); e.ControlChecks["discarded PCs never fetched"] = (_ => bus.Accesses.Any(a => a.Address is 0xdead0001 or 0xdead0003) ? 1u : 0u, 0); e.ControlChecks["sentinel never executed"] = (s => s.D[7], e.D[7]); - if (form == "CU" || post) e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) == null ? 0u : 1u, 1); + if (unimplemented || post) e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(unimplemented ? 2 : 3) == null ? 0u : 1u, 1); pointers[tail] = accessFrame; SetStacks(e, pointers, (ushort)(priorSr & ~0xc000)); e.Pc = Handler; e.ExceptionVector = 2; ExpectProvenance(e, sequence + 1, SyntheticMachine.Code, priorSr); SyntheticM68040RteValidationFaultTests.ExpectAccessFrame(m, e, accessFrame, priorSr, SyntheticMachine.Code, frame + offset, width); @@ -180,8 +218,10 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes if (!keepTrace) phases.Add("clear-incoming-trace"); phases.AddRange(["handler-return", "retry-RTE"]); if (keepTrace && incoming != 0 && vector == 0) phases.Add("retry-trace-return"); - if (vector != 0) phases.Add("pending-return"); phases.Add("following"); - var id = $"68040/RTE/{(cpVectors ? "cp-vectors" : userMaster ? "user-master" : pendingTrace ? "pending-trace" : keepTrace ? "retry-trace" : "repair")}/{form}/path={string.Join('-', path)}/result={result}/incoming={incoming:X4}/T={restoredTrace:X4}/align={alignment}/VBR={vbr:X8}/read={offset}:{width}/fault-byte={faultByte}/op=4E73/ccr={ccr:X2}"; + if (service != null) phases.AddRange(service.Phases(restoredTrace)); + else if (vector != 0) phases.Add("pending-return"); + phases.Add("following"); + var id = $"68040/RTE/{(traceService ? "software-trace" : cpVectors ? "cp-vectors" : userMaster ? "user-master" : pendingTrace ? "pending-trace" : keepTrace ? "retry-trace" : "repair")}/{form}/path={string.Join('-', path)}/result={result}/incoming={incoming:X4}/T={restoredTrace:X4}/align={alignment}/VBR={vbr:X8}/read={offset}:{width}/fault-byte={faultByte}{(traceService ? $"/clear={clearServiceTrace}" : "")}/op=4E73/ccr={ccr:X2}"; var phase = 0; bool Step() { @@ -221,7 +261,7 @@ bool Step() if (vector != 0) { ExpectException(m, e, pointers, savedSr, vector, Target, Operand, sequence + 2); - if (form == "CU" || post) e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) == null ? 0u : 1u, 0); + if (unimplemented || post) e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(unimplemented ? 2 : 3) == null ? 0u : 1u, 0); } // MC68040UM 8.2.6: the suspended RTE is traced only once it completes. // Both T1 and T0 trace RTE; the repaired frame's SR is saved in that @@ -234,6 +274,22 @@ bool Step() if (!Step()) return; e.ControlChecks.Remove("consumed throwaways not read by retry"); e.ControlChecks.Remove("exact repair writes"); + if (service != null) + { + var bank = ExceptionBank(savedSr); + if (!service.Run(m, e, pointers[bank], savedSr, Step)) return; + pointers[bank] += 12; + var returnedSr = service.ReturnedSr(savedSr); + SetStacks(e, pointers, returnedSr); e.Pc = service.ResumePc; e.ExceptionVector = null; + if (!Step()) return; // The service route ends in a real RTE. + if ((returnedSr & 0xc000) != 0) + { + ExpectException(m, e, pointers, returnedSr, 9, service.ResumePc, service.ResumePc, sequence + 3); + e.Pc = SyntheticM68040SoftwareTraceProgram.TraceHandler; + } + Step(); + return; + } if (retryTrace) { pointers[ExceptionBank(savedSr)] += 12; SetStacks(e, pointers, savedSr); e.Pc = Target; e.ExceptionVector = null; diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040SoftwareTraceProgram.cs b/Copper68k.Tests/Synthetic/SyntheticM68040SoftwareTraceProgram.cs new file mode 100644 index 00000000..94956543 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040SoftwareTraceProgram.cs @@ -0,0 +1,126 @@ +using Copper68k; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 8.2.6 / 8.3: software inspects the saved trace condition, +// converts the pending frame and directly calls the trace handler. This is +// an integer protocol fixture; no floating-point operation is emulated here. +internal sealed class SyntheticM68040SoftwareTraceProgram(string form, bool clearTrace, + uint handler, uint target, uint vbr) +{ + internal const uint TraceHandler = 0x9180, Marker = 0x8000, MarkerValue = 0x13579bdf; + private bool Cu => form.StartsWith("CU", StringComparison.Ordinal); + private bool Flow => form == "CU-flow"; + internal uint ResumePc => Cu ? target + (Flow ? 4u : 2u) : target; + // Synthetic instruction metadata: CU starts at target, while CP supplies + // the start of a completed two-word FMOVE. No FPU opcode is decoded here. + private uint TraceAddress => Cu ? target : target - 4; + private uint TestPc => handler + (Cu ? 8u : 0u); + private uint ServicePc => TestPc + (Flow ? 14u : 8u); + private bool Eligible(ushort sr) => (sr & 0x8000) != 0 || Flow && (sr & 0x4000) != 0; + internal ushort ReturnedSr(ushort sr) => clearTrace && Eligible(sr) ? (ushort)(sr & 0x3fff) : sr; + + internal void Initialize(SyntheticMachine m) + { + var words = new List(); + // Fixed reference encodings: MOVE.L #imm,2(A7); BTST #7/#6,(A7); + // BNE.s; RTE; MOVE.W #$2024,6(A7); MOVE.L #address,8(A7); + // MOVE.L (vector).L,-(A7); RTS. The temporary RTS target is not an + // exception frame. The frame itself remains on the original bank. + if (Cu) words.AddRange([0x2f7c, (ushort)(ResumePc >> 16), (ushort)ResumePc, 2]); + words.AddRange([0x0817, 7, (ushort)(Flow ? 0x6608 : 0x6602)]); + if (Flow) words.AddRange([0x0817, 6, 0x6602]); + words.Add(0x4e73); + words.AddRange([0x3f7c, 0x2024, 6, + 0x2f7c, (ushort)(TraceAddress >> 16), (ushort)TraceAddress, 8, + 0x2f39, (ushort)((vbr + 36) >> 16), (ushort)(vbr + 36), 0x4e75, 0x7e7e]); + for (var n = 0; n < words.Count; n++) m.InitializePhysical(handler + (uint)n * 2, words[n], 2); + m.InitializePhysical(vbr + 36, TraceHandler, 4); + ushort[] trace = clearTrace + ? [0x23fc, (ushort)(MarkerValue >> 16), (ushort)(MarkerValue & 0xffff), 0, (ushort)Marker, 0x0257, 0x3fff, 0x4e73, 0x7e7e] + : [0x23fc, (ushort)(MarkerValue >> 16), (ushort)(MarkerValue & 0xffff), 0, (ushort)Marker, 0x4e73, 0x7e7e]; + for (var n = 0; n < trace.Length; n++) m.InitializePhysical(TraceHandler + (uint)n * 2, trace[n], 2); + for (var n = -4; n < 8; n++) m.InitializePhysical(unchecked(Marker + (uint)n), 0xa5, 1); + m.InitializePhysical(Marker, 0, 4); + m.InitializePhysical(ResumePc, 0x60fe, 2); + m.InitializePhysical(ResumePc + 2, 0x7e7e, 2); + } + + internal IEnumerable Phases(ushort sr) + { + if (Cu) yield return "emulate-PC"; + yield return "test-T1"; yield return "branch-T1"; + if (Flow && (sr & 0x8000) == 0) { yield return "test-T0"; yield return "branch-T0"; } + if (Eligible(sr)) + { + yield return "trace-format"; yield return "trace-address"; + yield return "trace-vector-push"; yield return "trace-direct-call"; + yield return "trace-marker"; + if (clearTrace) yield return "trace-clear"; + } + yield return "pending-return"; + } + + // Each expected transition is independent of production decode/EA/flags. + // All registers, all three stack pointers, frame/guard memory and exception + // provenance remain verified by the caller at every executed instruction. + internal bool Run(SyntheticMachine m, ArchitecturalExpectation e, uint frame, + ushort sr, Func step) + { + var writes = m.Bus.Accesses.Count; + e.ControlChecks["software marker writes"] = (_ => (uint)m.Bus.Accesses.Skip(writes) + .Count(a => a.Write && a.Address == Marker), 0); + if (Cu) + { + e.Write(frame + 2, ResumePc, 4, m.Model); + e.Sr = SyntheticExecution.MoveFlags(e.Sr, ResumePc, 4); e.Pc = TestPc; + if (!step()) return false; + } + e.Sr = BitFlags(e.Sr, (sr & 0x8000) != 0); e.Pc = TestPc + 4; + if (!step()) return false; + e.Pc = (sr & 0x8000) != 0 ? ServicePc : TestPc + 6; + if (!step()) return false; + if (Flow && (sr & 0x8000) == 0) + { + e.Sr = BitFlags(e.Sr, (sr & 0x4000) != 0); e.Pc = TestPc + 10; + if (!step()) return false; + e.Pc = (sr & 0x4000) != 0 ? ServicePc : TestPc + 12; + if (!step()) return false; + } + if (!Eligible(sr)) return true; // Caller verifies the untraced RTE. + e.Write(frame + 6, 0x2024, 2, m.Model); + e.Sr = SyntheticExecution.MoveFlags(e.Sr, 0x2024, 2); e.Pc = ServicePc + 6; + if (!step()) return false; + e.Write(frame + 8, TraceAddress, 4, m.Model); + e.Sr = SyntheticExecution.MoveFlags(e.Sr, TraceAddress, 4); e.Pc = ServicePc + 14; + if (!step()) return false; + SetActiveStack(e, frame - 4); + e.Write(frame - 4, TraceHandler, 4, m.Model); + e.Sr = SyntheticExecution.MoveFlags(e.Sr, TraceHandler, 4); e.Pc = ServicePc + 20; + if (!step()) return false; + SetActiveStack(e, frame); e.Pc = TraceHandler; + if (!step()) return false; + e.Write(Marker, MarkerValue, 4, m.Model); + e.Sr = SyntheticExecution.MoveFlags(e.Sr, MarkerValue, 4); e.Pc = TraceHandler + 10; + e.ControlChecks["software marker writes"] = (_ => (uint)m.Bus.Accesses.Skip(writes) + .Count(a => a.Write && a.Address == Marker), 1); + if (!step()) return false; + if (clearTrace) + { + var value = (ushort)(sr & 0x3fff); + e.Write(frame, value, 2, m.Model); + e.Sr = (ushort)((e.Sr & 0xfff0) | (value == 0 ? 4 : 0)); e.Pc = TraceHandler + 14; + if (!step()) return false; + } + return true; // Caller verifies real RTE and restored stack selection. + } + + private static ushort BitFlags(ushort sr, bool set) => (ushort)((sr & ~4) | (set ? 0 : 4)); + private static void SetActiveStack(ArchitecturalExpectation e, uint sp) + { + e.A[7] = sp; + var bank = (e.Sr & 0x1000) != 0 ? "MSP" : "ISP"; + e.ControlChecks[bank] = (e.ControlChecks[bank].Read, sp); + if (bank == "MSP") e.MasterStackPointer = sp; + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index e7f93d8f..8aef5768 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -5032,3 +5032,92 @@ inventory rejection. Current source, fixture and assembly identities still match after all checks. No production CPU fix, consumer rerun, package release or old-test retirement is needed for this test-only addition. Milestone 6 remains **in progress**. + +### 040 executed pending software trace service — 2026-10-06 + +The four `SoftwareTraceService` reports extend the real validation-fault and +three-store repair fixture with executed integer CU/CP trace-service programs. +Expectations compose MC68040UM 8.2.6, 8.3 and 8.4.6.2/7: the pending condition +has priority, and software must inspect the saved trace condition and adjust +the frame before directly calling the trace handler. These are software-handler +protocol tests with supplied instruction-completion metadata, not an FPU +arithmetic emulator or observed hardware fault. + +CU cases supply linear or taken-flow completion, adjusting saved PC to target+2 +or target+4. CP cases supply a completed two-word FMOVE starting at target−4, +with its next PC already saved. T1 requests trace service; T0 requests it only +for taken-flow CU, since post-instruction CP is FMOVE-to-memory. Live handler +trace bits are clear, so the executed BTSTs must inspect the stacked SR byte. +The handler converts the format/vector word to $2024, supplies the original +instruction address, reads vector 9 through VBR, pushes the target and uses RTS +for a direct software call. This temporary call target is not an exception +frame. Every step preserves all registers and checks all three stack banks. + +The trace handler writes one marker, optionally clears stacked trace bits and +executes RTE. The software path does not change the hardware exception counter, +vector or saved-provenance registers. A following self-BRA checks hardware +tracing from the returned SR separately. Canonical cases cross all 32 CCRs, +incoming/restored 0/T1/T0, all four restored banks and CU/CP49–55. Structural +cases use CCR 0/31, incoming T1, every validation-read byte, all twelve supervisor +paths, both alignments/VBRs and all restored traces/vectors/service policies. + +| Group, each scalar/batch | Phases | Combinations | +| --- | ---: | ---: | +| Canonical service programs | 514,560 | 1,296 | +| Chained service programs | 3,601,920 | 145,152 | + +The maintained `RteSoftwareTrace` scope requires intended saved-SR-bit-test, +short-format-2-return and original-vector diagnostics in all four reports. +Golden fixed program words audit instruction encodings separately. The complete +040 selection requires 65 tests, 58 reports, seven fixed examples and fourteen +fixture/command identities. User-tail trace bridges, mixed-epoch provenance, +internal restoration and the other retained model/reference/consolidation gaps +remain required. The 480-case inventory is retained and still prevents milestone +completion. No production CPU fix, public API change, package release or old-test +retirement is included at this implementation stage. + +All three maintained mutations in `artifacts/m6-software-trace-mutations/` are +detected with the intended diagnostic in all four complete reports. Counts below +are per route, canonical/chained respectively; unsupported execution is zero: + +| Mutation | Mismatching | Dependent phases untested | Required diagnostic | +| --- | ---: | ---: | --- | +| Invert the saved-SR BTST result | 41,472 / 290,304 | 215,040 / 1,505,280 | CU-linear test-T1 SR mismatch | +| Pop only eight bytes for format 2 | 19,968 / 139,776 | 19,968 / 139,776 | CU-linear return has wrong inactive stack pointer | +| Replace original CP vector with 49 | 27,648 / 193,536 | 161,280 / 1,128,960 | CP50 retry delivers 49 | + +The script restores exact production bytes and rebuilds successfully with zero +warnings/errors. Every production source hash matches the preceding CP-vector +manifest. Deliberate mutation failures and dependent untested phases are proof +evidence, not production gaps. The restored acceptance build starts from +`635ecde475695a1744cde826c8ad5bc20c2ea497` plus the recorded fixture changes: +CPU SHA-256 is +`60748e5e34c12f541b1dcaa16928459c58211b041f33bffe029e38ed15d39139`; +test/adapter SHA-256 is +`5d4a79638b0b266e82234c305dbeef28c449b8a3c0bd256ac06542ed224d5384`. +The complete 040 run in `artifacts/m6-software-trace-audit/` executes all +**65 tests: 64 pass, one required inventory failure, zero skips**. All 58 reports, +seven fixed examples and fourteen input identities are present. Independent +combination enumeration verifies **23,937,728 passing phases**, zero mismatches +or unsupported execution and exactly **480 untested requirements** (23,938,208 +total). The command rejects precisely that inventory after all selection and +identity checks; it is not a passing completion gate. All current source, +fixture and binary hashes match the acceptance manifest. + +The fresh full Release selection in `artifacts/m6-software-trace-full/` passes +**5,090 tests**, with eleven optional skips and zero failures. All nine qualified +WinUAE presets match their exact directory/callback/frame selections and the +CPU/adapter identities above. Strict reporting verifies **38,906,390 logical +cases in 648 batches** across all eight profiles. Fresh pinned SingleStepTests +passes **312,500 cases in 125 files**; Musashi passes **536 programs with 88 +explicit exclusions**. Each requested adapter executes one passing test with +zero skips. All current acceptance source/input/binary identities match after +those references. All **17 fresh controls** in +`artifacts/m6-software-trace-controls/` reject their specific missing, shortened, +foreign, redistributed or omitted-fixture corruption. All four software-trace +reports and the new fixture identity are covered; each control requires its +intended diagnostic rather than accepting the generic inventory rejection. +Current source, fixture and assembly identities still match after all checks. +This qualifies the test-only software-trace checkpoint; no production CPU fix, +consumer rerun, package release or old-test retirement is included. Milestone 6 +remains **in progress** with the full remaining scope retained. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index ef02467b..c81666c0 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1890,3 +1890,52 @@ diagnostics in all four reports. Seventeen fresh report/fixture controls reject their specific corruptions. Restored production source bytes and current acceptance identities match; this is a test-only checkpoint. Milestone 6 remains **in progress**, with the full required scope retained above. + +### Milestone 6 pending software trace-service programs — 2026-10-06 + +Four `SoftwareTraceService` reports add executed CU/CP integer trace-service +programs after validation fault, repair and pending-frame conversion. All four +restored banks, CU linear/taken-flow completion metadata and original CP49–55 +vectors are explicit. The handler checks the stacked T1/T0 condition, advances +the CU PC when required, sets trace format/address and directly calls vector 9 +through a real table read and stack/RTS sequence. The trace handler writes one +marker, preserves or clears saved trace and returns using RTE. A software call +must not masquerade as a hardware exception entry. + +Canonical cases cover all CCRs and incoming/restored traces. Structural cases +use CCR 0/31 and incoming T1, retaining every validation-read byte, all twelve +supervisor paths, both alignments/VBRs and all restored traces/vectors/service +policies. Separate grouping avoids repeating the canonical incoming-trace +cross product at every structural read. The expected counts are 514,560 / +3,601,920 phases and 1,296 / 145,152 combinations per scalar/batch route. + +Golden instruction words audit the program fixture separately. Maintained +mutation probes target the saved-SR bit test, format-2 return size and original +pending vector, requiring intended diagnostics in each of the four reports. +The complete 040 command now requires 65 tests, 58 reports, seven fixed examples +and fourteen input identities, including the new test-internal program helper. +The 480-case remaining-protocol inventory is retained unchanged in cardinality. + +CU/CP completion metadata is synthetic input; actual FPU emulation/arithmetic +and hardware observations are not claimed. User-tail trace bridges, mixed-epoch +provenance, internal restoration, data/writeback/context transfer, other models' +restoration and broader independent qualification/consolidation remain required. +Milestone 6 remains **in progress**, `roadmapComplete=false`. + +The three maintained software-trace mutations are detected in each complete +scalar/batch canonical/chained report with their intended semantic diagnostic. +Production bytes are restored exactly and the clean implementation rebuilds +without warnings/errors. Fresh complete 040 acceptance executes 65 tests (64 +passing, one required inventory failure), checks 58 reports/seven fixed examples +and all fourteen fixture/command identities. It verifies 23,937,728 passing phases, +zero mismatches/unsupported execution and exactly 480 untested requirements. +Full Release CPU acceptance passes 5,090 tests with eleven optional skips and +zero failures. All nine qualified WinUAE selections and identities match; strict +reporting checks 38,906,390 cases in 648 batches. Fresh requested SingleStepTests +and Musashi adapters execute without skips, passing 312,500 cases / 125 files +and 536 programs / 88 explicit exclusions respectively. All seventeen fresh +report/fixture integrity controls reject their specific corruptions. Current +production source, fixture and assembly identities still match. This test-only +checkpoint is qualified; milestone 6 remains **in progress** with its required +scope retained. See the qualification record for the acceptance build identity +and remaining scope. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index e3868d78..92abd46b 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -47,6 +47,10 @@ $expected = [ordered]@{ 'rte-cp-vectors-boundaries-batch' = @{cases=110592; combinations=432} 'rte-cp-vectors-chained-scalar' = @{cases=2322432; combinations=145152} 'rte-cp-vectors-chained-batch' = @{cases=2322432; combinations=145152} + 'rte-software-trace-boundaries-scalar' = @{cases=514560; combinations=1296} + 'rte-software-trace-boundaries-batch' = @{cases=514560; combinations=1296} + 'rte-software-trace-chained-scalar' = @{cases=3601920; combinations=145152} + 'rte-software-trace-chained-batch' = @{cases=3601920; combinations=145152} 'instruction-fault-frame' = @{cases=36864; combinations=576} 'instruction-fault-restart' = @{cases=79872; combinations=624} 'handler-prefetch-entry-scalar' = @{cases=196608; combinations=6144} @@ -86,7 +90,7 @@ if (-not $ValidateReportsOnly) { cpuSourceFiles=@(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } - inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs') | ForEach-Object { + inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040SoftwareTraceProgram.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } assemblies=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | ForEach-Object { @@ -97,7 +101,7 @@ if (-not $ValidateReportsOnly) { 'Multiple continuation bits are architecturally undefined and excluded', 'Selected active accurate-batch paths verify counts/callbacks and scalar/batch bus/cycle policy; generic short operand frames do not qualify architectural format-7 data restart', 'Instruction-fault fixtures use cache-disabled accurate execution and physical-map rejection; speculative deferral, enabled caches/MMU and compiled fetch PC provenance remain unqualified', - 'Legacy supervisor repair clears saved trace; normal/CM retry traces completion, while pending CT/CU/CP49 and separate CP50-55 delivery suppress extra RTE trace. Software trace service and user-tail/mixed-epoch protocols remain required', + 'Legacy supervisor repair clears saved trace; normal/CM retry traces completion, while pending CT/CU/CP49 and separate CP50-55 delivery suppress extra RTE trace. Executed integer trace service has separate coverage; user-tail/mixed-epoch protocols remain required', 'User-tail fault expectations compose documented throwaway live-SR rules with general supervisor exception entry; unusual combined hardware behavior has not been observed', 'User-tail untouched-trace retry, internal-restoration double faults, cache/MMU/compiled handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', 'Direct odd-RTE saved-SR ordering uses documentary WinUAE 5d22d336, not an executed hardware oracle') @@ -109,11 +113,11 @@ if (-not $ValidateReportsOnly) { $identity = Get-Content -LiteralPath (Join-Path $output 'identities.json') -Raw | ConvertFrom-Json if ($identity.schema -ne 1 -or $identity.reference -cne 'MC68040UM' -or $identity.softwareReferenceExecuted -ne $false -or $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.cpuCommittedTree -notmatch '^[a-f0-9]{40}$' -or - @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 13 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 13 -or + @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 14 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 14 -or @($identity.assemblies).Count -ne 2) { throw '040 access-frame input identity is missing or incomplete' } foreach ($input in $identity.inputs) { $path = Join-Path $repo $input.file - if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs') -or + if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040SoftwareTraceProgram.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs') -or $input.sha256 -cne (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()) { throw '040 access-frame fixture/command identity differs' } } $cpuFiles = @(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') @@ -127,7 +131,7 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 60 -or [int]$counters.total -ne 60 -or [int]$counters.notExecuted -ne 0) { +if ([int]$counters.executed -ne 65 -or [int]$counters.total -ne 65 -or [int]$counters.notExecuted -ne 0) { throw '040 access-frame audit did not execute its complete selection (54 batches, 6 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} @@ -228,36 +232,50 @@ foreach ($group in $expected.Keys) { } } } } } - } elseif ($group -in @('rte-repair-boundaries','rte-repair-chained') -or $group.StartsWith('rte-retry-trace-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-pending-trace-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-user-master-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-cp-vectors-', [StringComparison]::Ordinal)) { + } elseif ($group -in @('rte-repair-boundaries','rte-repair-chained') -or $group.StartsWith('rte-retry-trace-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-pending-trace-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-user-master-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-cp-vectors-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-software-trace-', [StringComparison]::Ordinal)) { + $softwareTrace = $group.StartsWith('rte-software-trace-', [StringComparison]::Ordinal) $cpVectors = $group.StartsWith('rte-cp-vectors-', [StringComparison]::Ordinal) $userMaster = $group.StartsWith('rte-user-master-', [StringComparison]::Ordinal) $pendingTrace = $group.StartsWith('rte-pending-trace-', [StringComparison]::Ordinal) - $keepTrace = $cpVectors -or $userMaster -or $pendingTrace -or $group.StartsWith('rte-retry-trace-', [StringComparison]::Ordinal) - $chained = $group -eq 'rte-repair-chained' -or $group.StartsWith('rte-retry-trace-chained-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-pending-trace-chained-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-user-master-chained-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-cp-vectors-chained-', [StringComparison]::Ordinal) + $keepTrace = $softwareTrace -or $cpVectors -or $userMaster -or $pendingTrace -or $group.StartsWith('rte-retry-trace-', [StringComparison]::Ordinal) + $chained = $group -eq 'rte-repair-chained' -or $group.StartsWith('rte-retry-trace-chained-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-pending-trace-chained-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-user-master-chained-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-cp-vectors-chained-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-software-trace-chained-', [StringComparison]::Ordinal) $paths = if ($chained) { @('ISP-ISP','ISP-ISP-ISP','ISP-MSP-ISP','ISP-MSP','ISP-ISP-MSP','ISP-MSP-MSP', 'MSP-ISP','MSP-ISP-ISP','MSP-MSP-ISP','MSP-MSP','MSP-ISP-MSP','MSP-MSP-MSP') } else { @('ISP','MSP') } - foreach ($path in $paths) { foreach ($result in $(if ($cpVectors) { @('user','user-M','ISP','MSP') } elseif ($userMaster) { @('user-M') } else { @('user','ISP','MSP') })) { - foreach ($incoming in $(if ($chained -and -not $keepTrace) { @(0x8000) } else { @(0,0x8000,0x4000) })) { + foreach ($path in $paths) { foreach ($result in $(if ($cpVectors -or $softwareTrace) { @('user','user-M','ISP','MSP') } elseif ($userMaster) { @('user-M') } else { @('user','ISP','MSP') })) { + foreach ($incoming in $(if ($chained -and (-not $keepTrace -or $softwareTrace)) { @(0x8000) } else { @(0,0x8000,0x4000) })) { foreach ($trace in @(0,0x8000,0x4000)) { foreach ($alignment in $(if ($chained) { @(0,1) } else { @(0) })) { foreach ($vbr in $(if ($chained) { @(0,0x10000) } else { @(0x10000) })) { - foreach ($form in $(if ($cpVectors) { @('CP50','CP51','CP52','CP53','CP54','CP55') } elseif ($pendingTrace) { @('CT','CU','CP') } elseif ($keepTrace -and -not $userMaster) { @('format0','format2','format3','invalid4','invalid15','normal','CM') } else { @('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP') })) { + foreach ($form in $(if ($softwareTrace) { @('CU-linear','CU-flow','CP49','CP50','CP51','CP52','CP53','CP54','CP55') } elseif ($cpVectors) { @('CP50','CP51','CP52','CP53','CP54','CP55') } elseif ($pendingTrace) { @('CT','CU','CP') } elseif ($keepTrace -and -not $userMaster) { @('format0','format2','format3','invalid4','invalid15','normal','CM') } else { @('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP') })) { + foreach ($clear in $(if ($softwareTrace) { @($false,$true) } else { @($false) })) { # Literal read ranges are independent of the fixture iterator. $reads = @('0:2') if ($chained) { $reads += @('2:4','6:2') - if ($cpVectors -or $form -in @('normal','CM','CT','CU','CP')) { $reads += '12:2' } - if ($cpVectors -or $form -in @('CM','CT','CU','CP')) { $reads += '8:4' } + if ($softwareTrace -or $cpVectors -or $form -in @('normal','CM','CT','CU','CP')) { $reads += '12:2' } + if ($softwareTrace -or $cpVectors -or $form -in @('CM','CT','CU','CP')) { $reads += '8:4' } } foreach ($read in $reads) { $bytes = if ($chained) { [int]$read.Split(':')[1] } else { 1 } for ($byte=0; $byte -lt $bytes; $byte++) { - $key = '68040/RTE/{0}/{1}/path={2}/result={3}/incoming={4:X4}/T={5:X4}/align={6}/VBR={7:X8}/read={8}/fault-byte={9}' -f $(if ($cpVectors) {'cp-vectors'} elseif ($userMaster) {'user-master'} elseif ($pendingTrace) {'pending-trace'} elseif ($keepTrace) {'retry-trace'} else {'repair'}),$form,$path,$result,$incoming,$trace,$alignment,$vbr,$read,$byte + $key = '68040/RTE/{0}/{1}/path={2}/result={3}/incoming={4:X4}/T={5:X4}/align={6}/VBR={7:X8}/read={8}/fault-byte={9}' -f $(if ($softwareTrace) {'software-trace'} elseif ($cpVectors) {'cp-vectors'} elseif ($userMaster) {'user-master'} elseif ($pendingTrace) {'pending-trace'} elseif ($keepTrace) {'retry-trace'} else {'repair'}),$form,$path,$result,$incoming,$trace,$alignment,$vbr,$read,$byte $phases = if ($cpVectors -or $pendingTrace -or ($userMaster -and $form -in @('CT','CU','CP'))) { 8 } elseif ($keepTrace) { $(if ($incoming -eq 0) {7} else {8}) } elseif ($form -in @('CT','CU','CP')) { 9 } else { 8 } + if ($softwareTrace) { + $key += "/clear=$clear" + # Literal program transitions, not fixture Phases(). + $phases = 10 + if ($form.StartsWith('CU')) { $phases++ } + if ($form -eq 'CU-flow' -and $trace -ne 0x8000) { $phases += 2 } + if ($trace -eq 0x8000 -or ($form -eq 'CU-flow' -and $trace -eq 0x4000)) { + $phases += 5 + if ($clear) { $phases++ } + } + } $ccrs = if ($chained) { 2 } else { 32 } $expectedCombinations[$key] = $phases * $ccrs } } + } } } } @@ -442,8 +460,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 60 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 65 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=15705248; xunitBatches=54; fixedExamples=6; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=23938208; xunitBatches=58; fixedExamples=7; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index 3cbe6c38..f8602865 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','UserRteFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','RteSoftwareTrace','UserRteFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -394,6 +394,37 @@ foreach ($mutation in $cpVectorMutations) { $mutation.group='OtherCpVectorsPreserveTrace'; $mutation.model='68040'; $mutation.milestone=6 $mutation.cpVectors=$true; $mutations += $mutation } +$softwareTraceMutations = @( + @{name='040-software-trace-bit-test'; file=$advanced; before=@' + private void ExecuteBtstByteImmediateAddressIndirect(ushort opcode) + { + BeginInstruction(opcode); + _ = FetchWord(); + var bit = FetchWord() & 7; + var value = ReadByte(State.A[opcode & 7]); + State.SetFlag(M68kCpuState.Zero, (value & (1 << bit)) == 0); +'@; after=@' + private void ExecuteBtstByteImmediateAddressIndirect(ushort opcode) + { + BeginInstruction(opcode); + _ = FetchWord(); + var bit = FetchWord() & 7; + var value = ReadByte(State.A[opcode & 7]); + State.SetFlag(M68kCpuState.Zero, (value & (1 << bit)) != 0); +'@; proofCombination='/software-trace/CU-linear/'; proofPhase='/test-T1'; proofReason='SR expected'}, + @{name='040-software-trace-short-return'; file=$advanced; + before=' var size = format switch { 0 or 1 => 8u, 2 => 12u, 3 when _profile.Model == M68kAcceleratorModel.M68040 => 12u, _ => 0u };'; + after=' var size = format switch { 0 or 1 => 8u, 2 => 8u, 3 when _profile.Model == M68kAcceleratorModel.M68040 => 12u, _ => 0u };'; + proofCombination='/software-trace/CU-linear/'; proofPhase='/pending-return'; proofReason='Inactive stack pointer changed'}, + @{name='040-software-trace-original-vector'; file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs'; + before=' var vector = continuation == 0x8000 ? pending!.Vector : continuation == 0x4000 ? 11 : 9;'; + after=' var vector = continuation == 0x8000 ? 49 : continuation == 0x4000 ? 11 : 9;'; + proofCombination='/software-trace/CP50/'; proofPhase='/retry-RTE'; proofReason='Expected exception 50, actual 49'} +) +foreach ($mutation in $softwareTraceMutations) { + $mutation.group='SoftwareTraceService'; $mutation.model='68040'; $mutation.milestone=6 + $mutation.softwareTrace=$true; $mutations += $mutation +} $instructionMutations = @( @{name='040-instruction-short-frame'; before=' RaiseUnbufferedReadAccessFault(fault, stackedProgramCounter, instruction: true);'; after=' RaiseFormat0Exception(VectorBusError, stackedProgramCounter, M68kInstructionTimingKey.IllegalInstruction);'; proofCombination='/instruction/opcode/'}, @@ -466,6 +497,7 @@ if ($Scope -eq 'RteRetryTrace') { $mutations = @($mutations | Where-Object { $_. if ($Scope -eq 'RtePendingTrace') { $mutations = @($mutations | Where-Object { $_.pendingTrace }) } if ($Scope -eq 'RteUserMaster') { $mutations = @($mutations | Where-Object { $_.userMaster }) } if ($Scope -eq 'RteCpVectors') { $mutations = @($mutations | Where-Object { $_.cpVectors }) } +if ($Scope -eq 'RteSoftwareTrace') { $mutations = @($mutations | Where-Object { $_.softwareTrace }) } if ($Scope -eq 'InstructionFault') { $mutations = @($mutations | Where-Object { $_.instructionFault }) } if ($Scope -eq 'HandlerPrefetch') { $mutations = @($mutations | Where-Object { $_.handlerPrefetch }) } if ($Scope -eq 'EntryPrefetch') { $mutations = @($mutations | Where-Object { $_.entryPrefetch }) } @@ -582,15 +614,15 @@ try { throw 'Instruction-fault mutation omitted complete cases or intended semantic failure' } } - if ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors) { + if ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors -or $mutation.softwareTrace) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if ($trx.TestRun.ResultSummary.Counters.executed -ne 4 -or $trx.TestRun.ResultSummary.Counters.failed -ne 4 -or $batches.Count -ne 4) { throw 'Retry-trace mutation omitted its complete four-batch selection' } foreach ($matrix in @('boundaries','chained')) { foreach ($route in @('scalar','batch')) { - $prefix = if ($mutation.cpVectors) {'rte-cp-vectors'} elseif ($mutation.userMaster) {'rte-user-master'} elseif ($mutation.pendingTrace) {'rte-pending-trace'} else {'rte-retry-trace'} - $cases = if ($mutation.cpVectors) {if ($matrix -eq 'boundaries') {110592} else {2322432}} elseif ($mutation.userMaster) {if ($matrix -eq 'boundaries') {44736} else {714240}} elseif ($mutation.pendingTrace) {if ($matrix -eq 'boundaries') {41472} else {870912}} else {if ($matrix -eq 'boundaries') {92736} else {1271808}} + $prefix = if ($mutation.softwareTrace) {'rte-software-trace'} elseif ($mutation.cpVectors) {'rte-cp-vectors'} elseif ($mutation.userMaster) {'rte-user-master'} elseif ($mutation.pendingTrace) {'rte-pending-trace'} else {'rte-retry-trace'} + $cases = if ($mutation.softwareTrace) {if ($matrix -eq 'boundaries') {514560} else {3601920}} elseif ($mutation.cpVectors) {if ($matrix -eq 'boundaries') {110592} else {2322432}} elseif ($mutation.userMaster) {if ($matrix -eq 'boundaries') {44736} else {714240}} elseif ($mutation.pendingTrace) {if ($matrix -eq 'boundaries') {41472} else {870912}} else {if ($matrix -eq 'boundaries') {92736} else {1271808}} $target = @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq "$prefix-$matrix-$route" }) if ($target.Count -ne 1 -or $target[0].logicalCases -ne $cases -or @($target[0].failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and @@ -662,7 +694,7 @@ try { } elseif ($mutation.entryPrefetch) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] - } elseif ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors) { + } elseif ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors -or $mutation.softwareTrace) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.id.EndsWith($mutation.proofPhase, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] } elseif ($mutation.userRte) { diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index e6baf500..b4096e48 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -168,6 +168,10 @@ try { $expected['rte-cp-vectors-boundaries-batch'] = 110592 $expected['rte-cp-vectors-chained-scalar'] = 2322432 $expected['rte-cp-vectors-chained-batch'] = 2322432 + $expected['rte-software-trace-boundaries-scalar'] = 514560 + $expected['rte-software-trace-boundaries-batch'] = 514560 + $expected['rte-software-trace-chained-scalar'] = 3601920 + $expected['rte-software-trace-chained-batch'] = 3601920 $expected['rte-validation-batch'] = 129024 $expected['access-fault-batch-dispatch'] = 10368 $expected['rte-access-entry-double-fault'] = 98304 @@ -299,9 +303,10 @@ try { m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; m68040HandlerPrefetchScope=@{logicalCases=457728; batches=8; routes=@('accurate scalar','accurate batch'); bus='Physical map with no host code reader, including stable-map generation'; qualified='Four-long access-error entry window, every entry byte, odd handler addresses, retained data, flow/host/task/map invalidation and later instruction faults; denied cold boundaries perform no CPU fetch/state change'; remaining='Other exception-entry paths, host-reader/compiled fetch provenance, enabled caches/MMU, ordinary speculative deferral and physical timing'}; m68040UserTailScope=@{logicalCases=1687040; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); authority='Composition of MC68040UM 2.2.2.1, 8.1, 8.2.1/5, 8.4.2 and 8.4.6.7; software qualification, no executed hardware reference'; userM=@(0,1); initialBanks=@('ISP','MSP'); middleBanks=@('none','user','ISP','MSP'); ccr='all 32 canonical SR reads, 0/31 structural reads'; forms='0/2/3, invalid 4/15, normal/CM/CT/CU/CP49'; qualified='Live intermediate SR, supervisor exception stack selected by M, user-data TM=1, preserved USP frame and committed throwaways, bare return followed by privilege violation, executed seven-store repair with fresh throwaway bridge and following trace/continuation'; remaining='Internal restoration faults, untouched trace retry, physical bus-order/timing and hardware observation of composed user-tail fault behavior'}; - m68040RteCpVectorsScope=@{logicalCases=4866048; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); vectors=@(50,51,52,53,54,55); restoredBanks=@('user','user-M','ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); qualified='Original CP vector survives validation faults, three repair stores and incoming trace; format-3 saved SR/PC/EA, pending-context consumption, all stack banks, FPU register preservation, handler return and following traced BRA checked'; ccr='all 32 canonical, 0/31 every validation-read byte'; remaining='Pending software trace service, user-tail trace bridge, mixed-epoch provenance, context transfer and internal restoration'}; - m68040RteUserMasterScope=@{logicalCases=1517952; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); restoredSr='S=0,M=1'; forms=@('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP49'); incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); qualified='User execution retains USP; synchronous completion trace, pending conversion and following trace select MSP while preserving M; all three stack banks, saved SR/PC/EA, pending consumption and handler returns checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='User-tail trace bridge, mixed-epoch provenance, software trace-service routines and internal restoration'}; - m68040RtePendingTraceScope=@{logicalCases=1824768; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); forms='CT/CU/CP49'; incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); qualified='Three original-frame stores preserve access SR; returned incoming trace cannot create an extra RTE trace on pending conversion; saved SR/PC/EA, format/vector, pending consumption, handler return and following traced BRA are checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='User-tail trace bridge, mixed-epoch provenance, software trace-service routines, internal restoration and physical/hardware qualification'}; + m68040RteCpVectorsScope=@{logicalCases=4866048; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); vectors=@(50,51,52,53,54,55); restoredBanks=@('user','user-M','ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); qualified='Original CP vector survives validation faults, three repair stores and incoming trace; format-3 saved SR/PC/EA, pending-context consumption, all stack banks, FPU register preservation, handler return and following traced BRA checked'; ccr='all 32 canonical, 0/31 every validation-read byte'; remaining='User-tail trace bridge, mixed-epoch provenance, context transfer and internal restoration'}; + m68040RteSoftwareTraceScope=@{logicalCases=8232960; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); forms=@('CU-linear','CU-flow','CP49','CP50','CP51','CP52','CP53','CP54','CP55'); restoredBanks=@('user','user-M','ISP','MSP'); canonicalIncoming=@(0,0x8000,0x4000); structuralIncoming=@(0x8000); restoredTrace=@(0,0x8000,0x4000); ccr='all 32 canonical, 0/31 every validation-read byte'; qualified='Executed integer handler inspects saved T1/T0, advances emulated CU PC, converts format/address, reads vector 9 and directly calls software trace service; trace handler preserves or clears saved trace and really returns; hardware exception counter does not change for software calls'; remaining='Actual FPU emulation, user-tail trace bridge, mixed-epoch provenance, internal restoration and hardware/physical qualification'}; + m68040RteUserMasterScope=@{logicalCases=1517952; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); restoredSr='S=0,M=1'; forms=@('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP49'); incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); qualified='User execution retains USP; synchronous completion trace, pending conversion and following trace select MSP while preserving M; all three stack banks, saved SR/PC/EA, pending consumption and handler returns checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='User-tail trace bridge, mixed-epoch provenance, internal restoration'}; + m68040RtePendingTraceScope=@{logicalCases=1824768; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); forms='CT/CU/CP49'; incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); qualified='Three original-frame stores preserve access SR; returned incoming trace cannot create an extra RTE trace on pending conversion; saved SR/PC/EA, format/vector, pending consumption, handler return and following traced BRA are checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='User-tail trace bridge, mixed-epoch provenance, internal restoration and physical/hardware qualification'}; m68040RteRetryTraceScope=@{logicalCases=2729088; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); faultTailBanks=@('ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredBanks=@('user','ISP','MSP'); restoredTrace=@(0,0x8000,0x4000); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM'; qualified='Three original-frame repair stores leave saved access SR untouched; handler return restores incoming trace; successful original RTE triggers T1/T0 trace with restored SR/PC; trace-handler return and following instruction retain MOVEM EA and stack selection'; ccr='all 32 canonical, 0/31 every chained read byte'; remaining='User-tail trace bridge, mixed trace values within consumed throwaways, internal restoration and hardware/physical timing'}; m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, executed repair/retry after committed supervisor throwaways, and manual-derived user-tail fault/privilege/bridge repair; chained odd-PC saved-SR provenance, internal-restoration faults, user/mixed-epoch untouched-trace retry, CP context transfer, other real fault entry and physical timing remain unqualified'; m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='Internal restoration, user/mixed-epoch untouched incoming-trace retry and other real access faults; normal/CM preserved-trace and user-tail repair are qualified separately'}; From 47d6400a29002f86b96dc73afa6d10f82c0d8c62 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 05:19:47 +0300 Subject: [PATCH 055/211] Qualify preserved trace across 68040 user-tail RTE repair --- Copper68k.Tests/Synthetic/README.md | 26 +++++ .../SyntheticM68040UserRteFaultTests.cs | 73 +++++++++----- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 99 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 38 +++++++ scripts/test-copper68k-040-access-frames.ps1 | 54 ++++++++-- .../test-copper68k-synthetic-mutations.ps1 | 29 +++++- scripts/test-copper68k-synthetic.ps1 | 19 ++-- 7 files changed, 294 insertions(+), 44 deletions(-) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index f4b16c96..b45569eb 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -709,3 +709,29 @@ The four reports require 514,560 canonical / 3,601,920 chained phases per route, 8,232,960 total. The complete 040 audit requires 65 tests, 58 reporting batches, seven fixed examples and fourteen input identities. The retained 480-case inventory continues to fail completion; milestone 6 remains in progress. + +### User-tail bridge with preserved incoming trace + +`SyntheticM68040UserRteFaultTests.UserTailBridgePreservesTrace*` extends the +seven-store repair program without replacing its original trace-clearing groups. +The new bridge and access return retain incoming trace; only the access return +adds S so privileged RTE can retry. Completion tracing uses incoming 0/T1/T0 +independently of repaired trace. CT/CU/CP49–55 take priority over an automatic +RTE trace. Every return and following BRA/MOVEM checks all stack banks, +exception provenance, guarded memory and consumed throwaway effects. + +Canonical scalar/batch groups each contain 873,984 phases / 2,304 combinations +with every CCR. Structural groups each contain 3,502,080 phases / 145,920 +combinations, using CCR 0/31 and incoming T1 at every validation-read byte, both +alignments/VBRs and every initial/middle user/ISP/MSP path. Both original user M +values and all restored user/user-M/ISP/MSP states are covered. The four groups +add 8,752,128 phases to ordinary CI and the complete 040 audit. + +Run the focused selection with +`dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --filter FullyQualifiedName~UserTailBridgePreservesTrace`. +Use `./scripts/test-copper68k-synthetic-mutations.ps1 -Scope UserRteTrace` for +restored-bits substitution, original CP-vector loss and premature MOVEM +continuation consumption. Proof requires the intended diagnostic in all four +complete reports. Expectations compose MC68040UM trace, throwaway and pending +exception rules; no hardware execution is claimed. Mixed-epoch provenance, +user-tail software trace-service programs and internal restoration remain open. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs index c6c9bf96..1c06a978 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs @@ -22,35 +22,46 @@ public sealed class SyntheticM68040UserRteFaultTests(ITestOutputHelper output) [Fact, Trait("Suite", "Synthetic")] public void ExecutedUserTailRepairRebuildsAThrowawayBridgeBatch() => Audit(true, true); - private void Audit(bool repair, bool batch) + [Fact, Trait("Suite", "Synthetic")] + public void UserTailBridgePreservesTraceBoundariesScalar() => Audit(true, false, true, false); + [Fact, Trait("Suite", "Synthetic")] + public void UserTailBridgePreservesTraceBoundariesBatch() => Audit(true, true, true, false); + [Fact, Trait("Suite", "Synthetic")] + public void UserTailBridgePreservesTraceChainedScalar() => Audit(true, false, true, true); + [Fact, Trait("Suite", "Synthetic")] + public void UserTailBridgePreservesTraceChainedBatch() => Audit(true, true, true, true); + + private void Audit(bool repair, bool batch, bool keepTrace = false, bool chained = false) { var bus = new SyntheticM68040RteValidationFaultTests.ValidationFaultBus(); var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); - var report = new CoverageBatch("68040", $"rte-user-{(repair ? "repair" : "fault")}-{(batch ? "batch" : "scalar")}"); - foreach (var matrix in new[] { "boundaries", "structure" }) + var report = new CoverageBatch("68040", keepTrace ? $"rte-user-trace-{(chained ? "chained" : "boundaries")}-{(batch ? "batch" : "scalar")}" : $"rte-user-{(repair ? "repair" : "fault")}-{(batch ? "batch" : "scalar")}"); + foreach (var matrix in keepTrace ? new[] { chained ? "structure" : "boundaries" } : ["boundaries", "structure"]) foreach (var start in new[] { "ISP", "MSP" }) foreach (var middle in matrix == "structure" ? new[] { "none", "user", "ISP", "MSP" } : ["none"]) foreach (var userMaster in new[] { false, true }) foreach (var ccr in matrix == "boundaries" ? Enumerable.Range(0, 32) : new[] { 0, 31 }) - foreach (var incoming in repair ? new ushort[] { 0x8000 } : [0, 0x8000, 0x4000]) - foreach (var alignment in new uint[] { 0, 1 }) - foreach (var vbr in new uint[] { 0, 0x10000 }) - foreach (var result in repair && matrix == "boundaries" ? new[] { "user", "ISP", "MSP" } : ["ISP"]) - foreach (var restoredTrace in repair && matrix == "boundaries" ? new ushort[] { 0, 0x8000, 0x4000 } : [0]) - foreach (var form in Forms) + // All incoming trace states have canonical CCR coverage. Structural + // rejection cases use T1 and independently cross all restored traces. + foreach (var incoming in keepTrace && !chained || !repair ? new ushort[] { 0, 0x8000, 0x4000 } : [0x8000]) + foreach (var alignment in keepTrace && !chained ? new uint[] { 0 } : [0, 1]) + foreach (var vbr in keepTrace && !chained ? new uint[] { 0x10000 } : [0, 0x10000]) + foreach (var result in keepTrace ? new[] { "user", "user-M", "ISP", "MSP" } : repair && matrix == "boundaries" ? ["user", "ISP", "MSP"] : ["ISP"]) + foreach (var restoredTrace in keepTrace || repair && matrix == "boundaries" ? new ushort[] { 0, 0x8000, 0x4000 } : [0]) + foreach (var form in keepTrace ? Forms.Concat(["CP50", "CP51", "CP52", "CP53", "CP54", "CP55"]) : Forms) foreach (var (offset, width) in matrix == "structure" ? SyntheticM68040RteValidationFaultTests.Reads(form) : [(0u, 2)]) for (var faultByte = 0; faultByte < (matrix == "structure" ? width : 1); faultByte++) { var path = middle == "none" ? new[] { start, "user" } : [start, middle, "user"]; Run(m, bus, report, repair, batch, matrix, path, userMaster, ccr, incoming, alignment, vbr, - result, restoredTrace, form, offset, width, faultByte); + result, restoredTrace, form, offset, width, faultByte, keepTrace); } report.Complete(output); } private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTests.ValidationFaultBus bus, CoverageBatch report, bool repair, bool batch, string matrix, string[] path, bool userMaster, int ccr, - ushort incoming, uint alignment, uint vbr, string result, ushort restoredTrace, string form, uint offset, int width, int faultByte) + ushort incoming, uint alignment, uint vbr, string result, ushort restoredTrace, string form, uint offset, int width, int faultByte, bool keepTrace) { bus.Disarm(); m.Reset(ccr); var pointers = new Dictionary { ["user"] = 0x7800 + alignment, ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; @@ -71,10 +82,12 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes var bridge = pointers[exceptionBank]; var accessFrame = bridge - 60; var format = form.StartsWith("format") ? int.Parse(form[6..]) : form.StartsWith("invalid") ? int.Parse(form[7..]) : 7; var repairedFormat = form.StartsWith("invalid") ? 0 : format; - var continuation = form switch { "CM" => 0x1000, "CT" => 0x2000, "CU" => 0x4000, "CP" => 0x8000, _ => 0 }; - var vector = form switch { "CT" => 9, "CU" => 11, "CP" => 49, _ => 0 }; + var post = form.StartsWith("CP", StringComparison.Ordinal); + var continuation = post ? 0x8000 : form switch { "CM" => 0x1000, "CT" => 0x2000, "CU" => 0x4000, _ => 0 }; + var vector = post ? form == "CP" ? 49 : int.Parse(form[2..]) : form switch { "CT" => 9, "CU" => 11, _ => 0 }; var savedSr = Status(result, restoredTrace, ccr ^ 31); - var retrySr = (ushort)((priorSr | 0x2000) & ~0xc000); + var handlerSr = (ushort)((priorSr | 0x2000) & ~0xc000); + var retrySr = keepTrace ? (ushort)(priorSr | 0x2000) : handlerSr; m.InitializePhysical(frame, 0x801fu ^ (uint)ccr, 2); m.InitializePhysical(frame + 2, Target + 2, 4); m.InitializePhysical(frame + 6, (uint)(format << 12) | 8, 2); @@ -92,9 +105,11 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes // Repair the untouched user frame, then create a fresh throwaway on // the selected supervisor stack. Patch saved SR explicitly so the // handler returns in supervisor mode, able to retry privileged RTE. - // The new bridge reselects USP; consumed frames are never replayed. + // The preserved-trace route retains incoming trace in both the + // access return and new bridge. The bridge reselects USP; consumed + // frames are never replayed and its discarded PC is never fetched. stores.AddRange([(frame, savedSr, 2), (frame + 2, Target, 4), (frame + 6, (uint)(repairedFormat << 12 | 8), 2), - (bridge, (uint)(priorSr & ~0xc000), 2), (bridge + 2, 0xdead0005, 4), (bridge + 6, 0x1024, 2), (accessFrame, retrySr, 2)]); + (bridge, keepTrace ? priorSr : (uint)(priorSr & ~0xc000), 2), (bridge + 2, 0xdead0005, 4), (bridge + 6, 0x1024, 2), (accessFrame, retrySr, 2)]); foreach (var (at, value, bytes) in stores) { // Fixed encodings MOVE.W/L #imm,(xxx).L, independent of decoder. @@ -109,27 +124,29 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes m.InitializePhysical(PrivilegeHandler, 0x7e7e, 2); m.InitializePhysical(vbr + 8, Handler, 4); m.InitializePhysical(vbr + 32, PrivilegeHandler, 4); foreach (var v in new[] { 9, 11, 49 }) m.InitializePhysical(vbr + (uint)v * 4, PendingHandler, 4); + if (post) m.InitializePhysical(vbr + (uint)vector * 4, PendingHandler, 4); _ = SyntheticExecution.Prepare(m, [0x4e73]); m.Core.State.SetUserStackPointer(0x7800 + alignment); m.Core.State.SetInterruptStackPointer(0x4700 + alignment); m.Core.State.SetMasterStackPointer(0x7400 + alignment); m.Core.State.StatusRegister = Status(path[0], incoming, ccr); m.Core.State.VectorBaseRegister = vbr; if (form == "CU") m.Core.State.M68040PendingFpuExceptions.Begin(2, 11, Target); - if (form == "CP") m.Core.State.M68040PendingFpuExceptions.Begin(3, 49, Target); + if (post) m.Core.State.M68040PendingFpuExceptions.Begin(3, vector, Target); var e = ArchitecturalExpectation.Capture(m); var sequence = m.Core.State.ExceptionSequence; e.ControlChecks["one rejection"] = (_ => (uint)bus.Rejected.Count, 1); e.ControlChecks["original read address"] = (_ => bus.Rejected.Count == 1 ? bus.Rejected[0].Address : uint.MaxValue, frame + offset); e.ControlChecks["original read width"] = (_ => bus.Rejected.Count == 1 ? (uint)bus.Rejected[0].Width : 0, (uint)width); e.ControlChecks["discarded PCs not fetched"] = (_ => bus.Accesses.Any(a => a.Address is 0xdead0001 or 0xdead0003 or 0xdead0005) ? 1u : 0u, 0); e.ControlChecks["sentinel not executed"] = (s => s.D[7], e.D[7]); - if (form is "CU" or "CP") e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) == null ? 0u : 1u, 1); - pointers[exceptionBank] = accessFrame; SetStacks(e, pointers, retrySr); e.Pc = Handler; e.ExceptionVector = 2; + if (form == "CU" || post) e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) == null ? 0u : 1u, 1); + pointers[exceptionBank] = accessFrame; SetStacks(e, pointers, handlerSr); e.Pc = Handler; e.ExceptionVector = 2; Provenance(e, sequence + 1, SyntheticMachine.Code, priorSr); SyntheticM68040RteValidationFaultTests.ExpectAccessFrame(m, e, accessFrame, priorSr, SyntheticMachine.Code, frame + offset, width); bus.Arm(frame + offset + (uint)faultByte); var phases = repair ? new List { "fault-entry", "repair-SR", "repair-PC", "repair-format", "bridge-SR", "bridge-PC", "bridge-format", "repair-return-SR", "handler-return", "retry-RTE" } : new List { "fault-entry", "bare-return", "privilege" }; - if (repair) { if (vector != 0) phases.Add("pending-return"); phases.Add("following"); } - var id = $"68040/RTE/user-{(repair ? "repair" : "fault")}/{matrix}/{form}/path={string.Join('-', path)}/M={userMaster}/incoming={incoming:X4}/align={alignment}/VBR={vbr:X8}/result={result}/T={restoredTrace:X4}/read={offset}:{width}/byte={faultByte}/op=4E73/ccr={ccr:X2}"; + var retryTrace = keepTrace && incoming != 0 && vector == 0; + if (repair) { if (retryTrace) phases.Add("retry-trace-return"); if (vector != 0) phases.Add("pending-return"); phases.Add("following"); } + var id = $"68040/RTE/user-{(keepTrace ? "trace" : repair ? "repair" : "fault")}/{matrix}/{form}/path={string.Join('-', path)}/M={userMaster}/incoming={incoming:X4}/align={alignment}/VBR={vbr:X8}/result={result}/T={restoredTrace:X4}/read={offset}:{width}/byte={faultByte}/op=4E73/ccr={ccr:X2}"; var phase = 0; bool Step() { @@ -161,7 +178,7 @@ bool Step() if (!Step()) return; if (!repair) { - pointers[exceptionBank] -= 8; SetStacks(e, pointers, retrySr); e.Pc = PrivilegeHandler; e.ExceptionVector = 8; + pointers[exceptionBank] -= 8; SetStacks(e, pointers, handlerSr); e.Pc = PrivilegeHandler; e.ExceptionVector = 8; Provenance(e, sequence + 2, SyntheticMachine.Code, priorSr); e.Write(bridge - 8, priorSr, 2, m.Model); e.Write(bridge - 6, SyntheticMachine.Code, 4, m.Model); e.Write(bridge - 2, 0x0020, 2, m.Model); for (uint n = 0; n < 8; n++) e.MemoryMasks[bridge - 8 + n] = 255; @@ -176,11 +193,15 @@ bool Step() if (vector != 0) { Pending(m, e, pointers, savedSr, vector, Target, Operand, sequence + 2); - if (form is "CU" or "CP") e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) == null ? 0u : 1u, 0); + if (form == "CU" || post) e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) == null ? 0u : 1u, 0); } + // The retried instruction begins with incoming trace, even if the + // repaired SR clears it. CT/CU/CP conversion wins over RTE tracing; + // no automatic trace may overwrite the converted pending frame. + if (retryTrace) Pending(m, e, pointers, savedSr, 9, Target, SyntheticMachine.Code, sequence + 2); if (!Step()) return; e.ControlChecks.Remove("old throwaways not reread"); - if (vector != 0) + if (vector != 0 || retryTrace) { pointers[ExceptionBank(savedSr)] += 12; SetStacks(e, pointers, savedSr); e.Pc = Target; e.ExceptionVector = null; if (!Step()) return; @@ -188,12 +209,12 @@ bool Step() e.Pc = form == "CM" ? Target + 4 : Target; if (form == "CM") { e.D[0] = 0x89abcdef; e.D[1] = 0x12345678; } if (restoredTrace == 0x8000 || restoredTrace == 0x4000 && form != "CM") - Pending(m, e, pointers, savedSr, 9, e.Pc, Target, sequence + (vector != 0 ? 3u : 2u)); + Pending(m, e, pointers, savedSr, 9, e.Pc, Target, sequence + (vector != 0 || retryTrace ? 3u : 2u)); Step(); } private static ushort Status(string bank, ushort trace, int ccr, bool userMaster = false) => - (ushort)((bank == "MSP" ? 0x3000 : bank == "ISP" ? 0x2000 : userMaster ? 0x1000 : 0) | trace | ccr); + (ushort)((bank == "MSP" ? 0x3000 : bank == "ISP" ? 0x2000 : bank == "user-M" || userMaster ? 0x1000 : 0) | trace | ccr); private static string ExceptionBank(ushort sr) => (sr & 0x1000) != 0 ? "MSP" : "ISP"; private static void SetStacks(ArchitecturalExpectation e, Dictionary pointers, ushort sr) { diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 8aef5768..f08d7534 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -5121,3 +5121,102 @@ Current source, fixture and assembly identities still match after all checks. This qualifies the test-only software-trace checkpoint; no production CPU fix, consumer rerun, package release or old-test retirement is included. Milestone 6 remains **in progress** with the full remaining scope retained. + +### 040 user-tail bridge with preserved trace — 2026-10-06 + +The four `UserTailBridgePreservesTrace` groups compose MC68040UM 2.2.2.1, +8.1/8.2.1/8.2.6, 8.3, 8.4.2 and 8.4.6.7. The existing fault fixture consumes +throwaways into USP, rejects one validation read and enters the M-selected +supervisor stack. Seven real repair stores patch the user frame, build a new +throwaway bridge and add S to the access return. The new groups retain incoming +T1/T0 in both the bridge and access return instead of clearing them. The live +fault handler remains untraced. + +The handler's RTE returns to supervisor code with the original trace condition; +the retried RTE consumes the new bridge and completes the repaired user frame. +Normal/CM completion traces even if repaired SR clears trace. CT/CU/CP49–55 +instead deliver the pending frame with no extra automatic RTE trace. The +trace/pending handler return and following BRA/MOVEM independently verify the +restored trace condition, all stack banks, saved SR/PC/address, exception +provenance, original pending consumption and CM continuation lifetime. + +This is an inference combining documented rules for a synthetic software +repair program. It is not a hardware measurement or independently executed +WinUAE oracle for the combined user-tail path. Physical timing, enabled MMU, +FPU arithmetic and opaque internal restoration are not qualified here. + +| Group, each scalar/batch | Phases | Combinations | +| --- | ---: | ---: | +| Canonical preserved-trace bridge | 873,984 | 2,304 | +| Structural validation read bytes | 3,502,080 | 145,920 | + +Canonical cases use all 32 CCRs and incoming 0/T1/T0 with alignment zero and +VBR $10000. Structural cases use CCR 0/31 and incoming T1, reject each byte of +every validation read, and cross both alignments/VBRs and all initial/middle +paths. All groups retain both original user M values, every restored +user/user-M/ISP/MSP state, every restored trace condition, formats 0/2/3, +invalid 4/15 repaired to 0 and normal/CM/CT/CU/CP49–55. These four ordinary +groups add 8,752,128 phases. No production CPU or public API change is included. + +`artifacts/m6-user-trace-first/` and `artifacts/m6-user-trace-structure/` pass +all four focused reports with no skips, mismatches, unsupported execution or +untested phases. All three maintained `UserRteTrace` mutations now detect their +intended diagnostic in every complete report. Complete 040 acceptance executes +69 tests (62 reports, seven fixed examples) with all fourteen input identities. +It reports 32,689,856 passing phases, zero mismatches/unsupported execution and +480 untested requirements. Its sole failing test rejects that incomplete +inventory; all 68 other tests pass without skips. Mixed-epoch +trace provenance, user-tail software trace-service programs and every other +retained model/reference/consolidation requirement remain open; milestone 6 +stays **in progress**. + +The first maintained `UserRteTrace` mutation, `040-user-trace-restored-bits`, +executes all four complete groups with zero skips or unsupported execution. +Each canonical route detects 7,168 mismatches, leaves 14,336 dependent phases +untested and passes 852,480 phases. Each structural route detects 32,768, +leaves 65,536 dependent phases untested and passes 3,403,776. Every report +contains the intended retry-RTE PC diagnostic with incoming T1 and repaired +trace clear. This proves that restored trace bits cannot replace the incoming +instruction's tracing decision. These deliberate mutation failures are proof +evidence, not production failures. + +The original-CP-vector mutation also executes all four complete groups without +skips or unsupported execution. Each canonical route detects 27,648 mismatches, +leaves 55,296 dependent phases untested and passes 791,040 phases. Each +structural route detects 129,024, leaves 258,048 dependent phases untested and +passes 3,115,008. All reports include CP50 delivered as vector 49 at retry-RTE; +the other-vector cases retain their full cardinalities. + +The MOVEM handler-consumption mutation completes all four reports with zero +skips, unsupported execution or dependent untested phases. Each canonical route +detects 3,072 mismatches and passes 870,912 phases; each structural route detects +21,504 and passes 3,480,576. The intended following-instruction D0 diagnostic +shows that the saved EA must survive the trace-handler RTE until MOVEM resumes +at its recorded PC. No old regression is retired by these proofs. + +The script restores production sources byte-for-byte and rebuilds with zero +warnings/errors. Every current production source hash matches the preceding +software-trace manifest; the committed CPU tree remains +`795fd12d6c0237a22a5f92f4a96cc4823e0364c1`. The restored acceptance build starts +from `e52243255c7dd0d1f608954399915810eaeafd78` plus the recorded fixture changes. +CPU SHA-256 is +`51e5454dddc852170e8d44d33ee10834d92f2533c00170892b0356dfd2673963`; +test/adapter SHA-256 is +`4eacd6fe96c9918e481bb764dd97ff59eb615880ae9fff6875fca295dbd91902`. +The fresh full Release CPU selection passes 5,094 tests with eleven optional +skips and zero failures. All nine qualified WinUAE presets match exact +selections and current CPU/adapter identities. Strict ordinary reporting +verifies 47,658,518 logical cases in 652 batches across all eight profiles, +with `roadmapComplete=false`. Fresh pinned SingleStepTests passes 312,500 cases +in 125 files; Musashi passes 536 programs with 88 explicit exclusions. Both +requested adapter tests execute once and pass without skips. Source, fixture +and assembly identities still match after the reference audits. + +All seventeen fresh report/fixture integrity controls reject their specific +missing, shortened, foreign, redistributed or omitted-fixture corruption. +Every canonical/structural scalar/batch report is covered; generic inventory +rejection alone cannot satisfy a control. Current source, fixture and assembly +identities still match after all checks. This test-only checkpoint is qualified; +milestone 6 remains **in progress** with all required remaining scope retained. +No CPU production fix, consumer rerun, package release or old-test retirement +is included. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index c81666c0..ecbaa822 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1939,3 +1939,41 @@ production source, fixture and assembly identities still match. This test-only checkpoint is qualified; milestone 6 remains **in progress** with its required scope retained. See the qualification record for the acceptance build identity and remaining scope. + +### Milestone 6 user-tail bridge preserves trace — 2026-10-06 + +The next four scalar/batch groups retain incoming trace through the executed +seven-store user-tail repair. The access return adds S while retaining M/T1/T0; +the fresh supervisor throwaway restores the original user SR and selects USP. +The original instruction then completes with its incoming trace condition, +independently of the repaired SR. Pending CT/CU/CP49–55 conversion suppresses +an extra automatic RTE trace. Returns and the following BRA/MOVEM verify saved +SR/PC/address, all stack banks and continuation lifetime without replaying +consumed throwaways or their discarded PCs. + +Canonical groups each require 873,984 phases / 2,304 combinations across all +CCRs and incoming 0/T1/T0. Structural groups each require 3,502,080 phases / +145,920 combinations with CCR 0/31, incoming T1, every validation-read byte, +both alignments/VBRs and all initial/middle paths. Both original user M states +and all restored user/user-M/ISP/MSP states are retained. These add 8,752,128 +phases; the complete 040 command must execute 69 tests, 62 reports, seven fixed +examples and fourteen input identities, still retaining its 480-case inventory. + +Initial canonical and structural execution passes all four reports without a +CPU production change. All three maintained mutations now detect their intended +diagnostic in every complete report; production bytes are restored exactly and +the implementation rebuilds without warnings/errors. Complete 040 acceptance +reports 32,689,856 passing phases, zero mismatches/unsupported execution and +480 untested requirements. All 69 tests execute without skips; its sole failure +is the retained inventory. Fresh full Release CPU acceptance passes 5,094 tests +with eleven optional skips and zero failures; all nine qualified WinUAE +selections match the frozen source/binary identities. Strict ordinary reporting +verifies 47,658,518 cases in 652 batches. Fresh pinned SingleStepTests and Musashi +audits pass 312,500 cases / 125 files and 536 programs / 88 explicit exclusions; +both adapter tests execute without skips. All current identities still match. +All seventeen fresh report/fixture integrity controls reject their specific +corruptions; all current identities still match afterward. This test-only +checkpoint is qualified. Mixed-epoch trace provenance, +user-tail software trace-service programs, internal restoration and all other +retained model/reference/consolidation requirements remain required. +Milestone 6 stays **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index 92abd46b..54584e3e 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -65,6 +65,10 @@ $expected = [ordered]@{ 'rte-user-fault-batch' = @{cases=168192; combinations=20832} 'rte-user-repair-scalar' = @{cases=675328; combinations=8224} 'rte-user-repair-batch' = @{cases=675328; combinations=8224} + 'rte-user-trace-boundaries-scalar' = @{cases=873984; combinations=2304} + 'rte-user-trace-boundaries-batch' = @{cases=873984; combinations=2304} + 'rte-user-trace-chained-scalar' = @{cases=3502080; combinations=145920} + 'rte-user-trace-chained-batch' = @{cases=3502080; combinations=145920} 'rte-access-remaining-protocols' = @{cases=480; combinations=15} } $testExit = 0 @@ -101,9 +105,9 @@ if (-not $ValidateReportsOnly) { 'Multiple continuation bits are architecturally undefined and excluded', 'Selected active accurate-batch paths verify counts/callbacks and scalar/batch bus/cycle policy; generic short operand frames do not qualify architectural format-7 data restart', 'Instruction-fault fixtures use cache-disabled accurate execution and physical-map rejection; speculative deferral, enabled caches/MMU and compiled fetch PC provenance remain unqualified', - 'Legacy supervisor repair clears saved trace; normal/CM retry traces completion, while pending CT/CU/CP49 and separate CP50-55 delivery suppress extra RTE trace. Executed integer trace service has separate coverage; user-tail/mixed-epoch protocols remain required', + 'Legacy repair clears saved trace; separate supervisor and user-tail bridge groups preserve incoming trace, check normal/CM completion and pending CT/CU/CP49-55 priority. Executed integer trace service has separate supervisor-tail coverage; mixed-epoch and user-tail software trace-service protocols remain required', 'User-tail fault expectations compose documented throwaway live-SR rules with general supervisor exception entry; unusual combined hardware behavior has not been observed', - 'User-tail untouched-trace retry, internal-restoration double faults, cache/MMU/compiled handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', + 'Mixed-epoch trace provenance, user-tail software trace service, internal-restoration double faults, cache/MMU/compiled handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', 'Direct odd-RTE saved-SR ordering uses documentary WinUAE 5d22d336, not an executed hardware oracle') } $identity | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'identities.json') @@ -131,8 +135,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 65 -or [int]$counters.total -ne 65 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (54 batches, 6 fixed examples)' +if ([int]$counters.executed -ne 69 -or [int]$counters.total -ne 69 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (62 batches, 7 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -155,6 +159,44 @@ foreach ($group in $expected.Keys) { $expectedCombinations["68040/RTE/format7/$form/bank=$bank"] = 32 } } + } elseif ($group.StartsWith('rte-user-trace-', [StringComparison]::Ordinal)) { + $chained = $group.StartsWith('rte-user-trace-chained-', [StringComparison]::Ordinal) + foreach ($start in @('ISP','MSP')) { + foreach ($middle in $(if ($chained) { @('none','user','ISP','MSP') } else { @('none') })) { + $path = if ($middle -eq 'none') { "$start-user" } else { "$start-$middle-user" } + foreach ($master in @($false,$true)) { + foreach ($incoming in $(if ($chained) { @(0x8000) } else { @(0,0x8000,0x4000) })) { + foreach ($alignment in $(if ($chained) { @(0,1) } else { @(0) })) { + foreach ($vbr in $(if ($chained) { @(0,0x10000) } else { @(0x10000) })) { + foreach ($result in @('user','user-M','ISP','MSP')) { + foreach ($trace in @(0,0x8000,0x4000)) { + foreach ($form in @('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP','CP50','CP51','CP52','CP53','CP54','CP55')) { + $reads = @('0:2') + if ($chained) { + $reads += @('2:4','6:2') + if ($form -notin @('format0','format2','format3','invalid4','invalid15')) { $reads += '12:2' } + if ($form -notin @('format0','format2','format3','invalid4','invalid15','normal')) { $reads += '8:4' } + } + foreach ($read in $reads) { + $parts = $read.Split(':') + for ($byte=0; $byte -lt $(if ($chained) {[int]$parts[1]} else {1}); $byte++) { + $key = '68040/RTE/user-trace/{0}/{1}/path={2}/M={3}/incoming={4:X4}/align={5}/VBR={6:X8}/result={7}/T={8:X4}/read={9}:{10}/byte={11}' -f $(if ($chained) {'structure'} else {'boundaries'}),$form,$path,$master,$incoming,$alignment,$vbr,$result,$trace,$parts[0],$parts[1],$byte + # Seven stores, two RTEs, entry and following; pending + # conversion or incoming trace adds one handler return. + $pending = $form -in @('CT','CU','CP','CP50','CP51','CP52','CP53','CP54','CP55') + $phases = if ($pending -or $incoming -ne 0) {12} else {11} + $expectedCombinations[$key] = $(if ($chained) {2} else {32}) * $phases + } + } + } + } + } + } + } + } + } + } + } } elseif ($group.StartsWith('rte-user-fault-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-user-repair-', [StringComparison]::Ordinal)) { $repair = $group.StartsWith('rte-user-repair-', [StringComparison]::Ordinal) foreach ($matrix in @('boundaries','structure')) { @@ -460,8 +502,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 65 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 69 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=23938208; xunitBatches=58; fixedExamples=7; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=32690336; xunitBatches=62; fixedExamples=7; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index f8602865..406df848 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','RteSoftwareTrace','UserRteFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','RteSoftwareTrace','UserRteFault','UserRteTrace','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -425,6 +425,24 @@ foreach ($mutation in $softwareTraceMutations) { $mutation.group='SoftwareTraceService'; $mutation.model='68040'; $mutation.milestone=6 $mutation.softwareTrace=$true; $mutations += $mutation } +$userTraceMutations = @( + @{name='040-user-trace-restored-bits'; file=$advanced; + before=' if ((trace & 0x8000) != 0 || flow || exception)'; + after=' if ((State.StatusRegister & 0xc000) != 0 && ((trace & 0x8000) != 0 || flow || exception))'; + proofCombination='/incoming=8000/align=0/VBR=00010000/result=user/T=0000/'; proofPhase='/retry-RTE'; proofReason='PC expected'}, + @{name='040-user-trace-original-cp-vector'; file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs'; + before=' var vector = continuation == 0x8000 ? pending!.Vector : continuation == 0x4000 ? 11 : 9;'; + after=' var vector = continuation == 0x8000 ? 49 : continuation == 0x4000 ? 11 : 9;'; + proofCombination='/CP50/'; proofPhase='/retry-RTE'; proofReason='Expected exception 50, actual 49'}, + @{name='040-user-trace-movem-handler-consumption'; file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs'; + before=' if (_m68040MovemContinuation is not { } saved || saved.Pc != State.ProgramCounter) return false;'; + after=' if (_m68040MovemContinuation is not { } saved) return false;'; + proofCombination='/CM/'; proofPhase='/following'; proofReason='D0 expected'} +) +foreach ($mutation in $userTraceMutations) { + $mutation.group='UserTailBridgePreservesTrace'; $mutation.model='68040'; $mutation.milestone=6 + $mutation.userTrace=$true; $mutations += $mutation +} $instructionMutations = @( @{name='040-instruction-short-frame'; before=' RaiseUnbufferedReadAccessFault(fault, stackedProgramCounter, instruction: true);'; after=' RaiseFormat0Exception(VectorBusError, stackedProgramCounter, M68kInstructionTimingKey.IllegalInstruction);'; proofCombination='/instruction/opcode/'}, @@ -502,6 +520,7 @@ if ($Scope -eq 'InstructionFault') { $mutations = @($mutations | Where-Object { if ($Scope -eq 'HandlerPrefetch') { $mutations = @($mutations | Where-Object { $_.handlerPrefetch }) } if ($Scope -eq 'EntryPrefetch') { $mutations = @($mutations | Where-Object { $_.entryPrefetch }) } if ($Scope -eq 'UserRteFault') { $mutations = @($mutations | Where-Object { $_.userRte }) } +if ($Scope -eq 'UserRteTrace') { $mutations = @($mutations | Where-Object { $_.userTrace }) } if ($Scope -eq 'AccessDoubleFault') { $mutations = @($mutations | Where-Object { $_.doubleFault }) } if ($Scope -eq 'BatchFault') { $mutations = @($mutations | Where-Object { $_.batchFault }) } if ($Scope -eq 'LowPowerStop') { $mutations = @($mutations | Where-Object { $_.lpstop }) } @@ -614,15 +633,15 @@ try { throw 'Instruction-fault mutation omitted complete cases or intended semantic failure' } } - if ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors -or $mutation.softwareTrace) { + if ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors -or $mutation.softwareTrace -or $mutation.userTrace) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if ($trx.TestRun.ResultSummary.Counters.executed -ne 4 -or $trx.TestRun.ResultSummary.Counters.failed -ne 4 -or $batches.Count -ne 4) { throw 'Retry-trace mutation omitted its complete four-batch selection' } foreach ($matrix in @('boundaries','chained')) { foreach ($route in @('scalar','batch')) { - $prefix = if ($mutation.softwareTrace) {'rte-software-trace'} elseif ($mutation.cpVectors) {'rte-cp-vectors'} elseif ($mutation.userMaster) {'rte-user-master'} elseif ($mutation.pendingTrace) {'rte-pending-trace'} else {'rte-retry-trace'} - $cases = if ($mutation.softwareTrace) {if ($matrix -eq 'boundaries') {514560} else {3601920}} elseif ($mutation.cpVectors) {if ($matrix -eq 'boundaries') {110592} else {2322432}} elseif ($mutation.userMaster) {if ($matrix -eq 'boundaries') {44736} else {714240}} elseif ($mutation.pendingTrace) {if ($matrix -eq 'boundaries') {41472} else {870912}} else {if ($matrix -eq 'boundaries') {92736} else {1271808}} + $prefix = if ($mutation.userTrace) {'rte-user-trace'} elseif ($mutation.softwareTrace) {'rte-software-trace'} elseif ($mutation.cpVectors) {'rte-cp-vectors'} elseif ($mutation.userMaster) {'rte-user-master'} elseif ($mutation.pendingTrace) {'rte-pending-trace'} else {'rte-retry-trace'} + $cases = if ($mutation.userTrace) {if ($matrix -eq 'boundaries') {873984} else {3502080}} elseif ($mutation.softwareTrace) {if ($matrix -eq 'boundaries') {514560} else {3601920}} elseif ($mutation.cpVectors) {if ($matrix -eq 'boundaries') {110592} else {2322432}} elseif ($mutation.userMaster) {if ($matrix -eq 'boundaries') {44736} else {714240}} elseif ($mutation.pendingTrace) {if ($matrix -eq 'boundaries') {41472} else {870912}} else {if ($matrix -eq 'boundaries') {92736} else {1271808}} $target = @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq "$prefix-$matrix-$route" }) if ($target.Count -ne 1 -or $target[0].logicalCases -ne $cases -or @($target[0].failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and @@ -694,7 +713,7 @@ try { } elseif ($mutation.entryPrefetch) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] - } elseif ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors -or $mutation.softwareTrace) { + } elseif ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors -or $mutation.softwareTrace -or $mutation.userTrace) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.id.EndsWith($mutation.proofPhase, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] } elseif ($mutation.userRte) { diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index b4096e48..a5f37ee9 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -205,6 +205,10 @@ try { $expected['rte-user-fault-batch'] = 168192 $expected['rte-user-repair-scalar'] = 675328 $expected['rte-user-repair-batch'] = 675328 + $expected['rte-user-trace-boundaries-scalar'] = 873984 + $expected['rte-user-trace-boundaries-batch'] = 873984 + $expected['rte-user-trace-chained-scalar'] = 3502080 + $expected['rte-user-trace-chained-batch'] = 3502080 $expected['system-mmu-disabled'] = 34850 $expected['system-linef-pc-restore'] = 6464 } @@ -302,14 +306,15 @@ try { externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; m68040HandlerPrefetchScope=@{logicalCases=457728; batches=8; routes=@('accurate scalar','accurate batch'); bus='Physical map with no host code reader, including stable-map generation'; qualified='Four-long access-error entry window, every entry byte, odd handler addresses, retained data, flow/host/task/map invalidation and later instruction faults; denied cold boundaries perform no CPU fetch/state change'; remaining='Other exception-entry paths, host-reader/compiled fetch provenance, enabled caches/MMU, ordinary speculative deferral and physical timing'}; - m68040UserTailScope=@{logicalCases=1687040; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); authority='Composition of MC68040UM 2.2.2.1, 8.1, 8.2.1/5, 8.4.2 and 8.4.6.7; software qualification, no executed hardware reference'; userM=@(0,1); initialBanks=@('ISP','MSP'); middleBanks=@('none','user','ISP','MSP'); ccr='all 32 canonical SR reads, 0/31 structural reads'; forms='0/2/3, invalid 4/15, normal/CM/CT/CU/CP49'; qualified='Live intermediate SR, supervisor exception stack selected by M, user-data TM=1, preserved USP frame and committed throwaways, bare return followed by privilege violation, executed seven-store repair with fresh throwaway bridge and following trace/continuation'; remaining='Internal restoration faults, untouched trace retry, physical bus-order/timing and hardware observation of composed user-tail fault behavior'}; - m68040RteCpVectorsScope=@{logicalCases=4866048; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); vectors=@(50,51,52,53,54,55); restoredBanks=@('user','user-M','ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); qualified='Original CP vector survives validation faults, three repair stores and incoming trace; format-3 saved SR/PC/EA, pending-context consumption, all stack banks, FPU register preservation, handler return and following traced BRA checked'; ccr='all 32 canonical, 0/31 every validation-read byte'; remaining='User-tail trace bridge, mixed-epoch provenance, context transfer and internal restoration'}; + m68040UserTailScope=@{logicalCases=1687040; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); authority='Composition of MC68040UM 2.2.2.1, 8.1, 8.2.1/5, 8.4.2 and 8.4.6.7; software qualification, no executed hardware reference'; userM=@(0,1); initialBanks=@('ISP','MSP'); middleBanks=@('none','user','ISP','MSP'); ccr='all 32 canonical SR reads, 0/31 structural reads'; forms='0/2/3, invalid 4/15, normal/CM/CT/CU/CP49'; qualified='Live intermediate SR, supervisor exception stack selected by M, user-data TM=1, preserved USP frame and committed throwaways, bare return followed by privilege violation, executed seven-store repair with fresh throwaway bridge and following trace/continuation'; remaining='Internal restoration faults, mixed-epoch trace provenance, user-tail software trace service, physical bus-order/timing and hardware observation of composed user-tail fault behavior'}; + m68040UserTailTraceScope=@{logicalCases=8752128; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); userM=@(0,1); initialBanks=@('ISP','MSP'); middleBanks=@('none','user','ISP','MSP'); restoredBanks=@('user','user-M','ISP','MSP'); canonicalIncoming=@(0,0x8000,0x4000); structuralIncoming=@(0x8000); restoredTrace=@(0,0x8000,0x4000); forms='0/2/3, invalid 4/15, normal/CM/CT/CU/CP49-55'; ccr='all 32 canonical, 0/31 every validation-read byte'; qualified='Seven real stores preserve incoming trace while adding S only to the access return; fresh supervisor throwaway restores user SR, original RTE completion traces independently of restored bits, CT/CU/CP priority suppresses extra trace; handler return, following flow/MOVEM and all stack banks checked'; remaining='User-tail software trace service, mixed-epoch trace provenance, internal restoration and hardware/physical qualification'}; + m68040RteCpVectorsScope=@{logicalCases=4866048; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); vectors=@(50,51,52,53,54,55); restoredBanks=@('user','user-M','ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); qualified='Original CP vector survives validation faults, three repair stores and incoming trace; format-3 saved SR/PC/EA, pending-context consumption, all stack banks, FPU register preservation, handler return and following traced BRA checked'; ccr='all 32 canonical, 0/31 every validation-read byte'; remaining='User-tail software trace service, mixed-epoch provenance, context transfer and internal restoration'}; m68040RteSoftwareTraceScope=@{logicalCases=8232960; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); forms=@('CU-linear','CU-flow','CP49','CP50','CP51','CP52','CP53','CP54','CP55'); restoredBanks=@('user','user-M','ISP','MSP'); canonicalIncoming=@(0,0x8000,0x4000); structuralIncoming=@(0x8000); restoredTrace=@(0,0x8000,0x4000); ccr='all 32 canonical, 0/31 every validation-read byte'; qualified='Executed integer handler inspects saved T1/T0, advances emulated CU PC, converts format/address, reads vector 9 and directly calls software trace service; trace handler preserves or clears saved trace and really returns; hardware exception counter does not change for software calls'; remaining='Actual FPU emulation, user-tail trace bridge, mixed-epoch provenance, internal restoration and hardware/physical qualification'}; - m68040RteUserMasterScope=@{logicalCases=1517952; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); restoredSr='S=0,M=1'; forms=@('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP49'); incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); qualified='User execution retains USP; synchronous completion trace, pending conversion and following trace select MSP while preserving M; all three stack banks, saved SR/PC/EA, pending consumption and handler returns checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='User-tail trace bridge, mixed-epoch provenance, internal restoration'}; - m68040RtePendingTraceScope=@{logicalCases=1824768; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); forms='CT/CU/CP49'; incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); qualified='Three original-frame stores preserve access SR; returned incoming trace cannot create an extra RTE trace on pending conversion; saved SR/PC/EA, format/vector, pending consumption, handler return and following traced BRA are checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='User-tail trace bridge, mixed-epoch provenance, internal restoration and physical/hardware qualification'}; - m68040RteRetryTraceScope=@{logicalCases=2729088; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); faultTailBanks=@('ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredBanks=@('user','ISP','MSP'); restoredTrace=@(0,0x8000,0x4000); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM'; qualified='Three original-frame repair stores leave saved access SR untouched; handler return restores incoming trace; successful original RTE triggers T1/T0 trace with restored SR/PC; trace-handler return and following instruction retain MOVEM EA and stack selection'; ccr='all 32 canonical, 0/31 every chained read byte'; remaining='User-tail trace bridge, mixed trace values within consumed throwaways, internal restoration and hardware/physical timing'}; - m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, executed repair/retry after committed supervisor throwaways, and manual-derived user-tail fault/privilege/bridge repair; chained odd-PC saved-SR provenance, internal-restoration faults, user/mixed-epoch untouched-trace retry, CP context transfer, other real fault entry and physical timing remain unqualified'; - m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='Internal restoration, user/mixed-epoch untouched incoming-trace retry and other real access faults; normal/CM preserved-trace and user-tail repair are qualified separately'}; + m68040RteUserMasterScope=@{logicalCases=1517952; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); restoredSr='S=0,M=1'; forms=@('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP49'); incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); qualified='User execution retains USP; synchronous completion trace, pending conversion and following trace select MSP while preserving M; all three stack banks, saved SR/PC/EA, pending consumption and handler returns checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='User-tail software trace service, mixed-epoch provenance, internal restoration'}; + m68040RtePendingTraceScope=@{logicalCases=1824768; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); forms='CT/CU/CP49'; incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); qualified='Three original-frame stores preserve access SR; returned incoming trace cannot create an extra RTE trace on pending conversion; saved SR/PC/EA, format/vector, pending consumption, handler return and following traced BRA are checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='User-tail software trace service, mixed-epoch provenance, internal restoration and physical/hardware qualification'}; + m68040RteRetryTraceScope=@{logicalCases=2729088; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); faultTailBanks=@('ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredBanks=@('user','ISP','MSP'); restoredTrace=@(0,0x8000,0x4000); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM'; qualified='Three original-frame repair stores leave saved access SR untouched; handler return restores incoming trace; successful original RTE triggers T1/T0 trace with restored SR/PC; trace-handler return and following instruction retain MOVEM EA and stack selection'; ccr='all 32 canonical, 0/31 every chained read byte'; remaining='User-tail software trace service, mixed trace values within consumed throwaways, internal restoration and hardware/physical timing'}; + m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, executed repair/retry after committed supervisor throwaways, and manual-derived user-tail fault/privilege/bridge repair; chained odd-PC saved-SR provenance, internal-restoration faults, user-tail software trace service/mixed-epoch trace provenance, CP context transfer, other real fault entry and physical timing remain unqualified'; + m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='Internal restoration, user-tail software trace service/mixed-epoch trace provenance and other real access faults; normal/CM preserved-trace and user-tail repair are qualified separately'}; m68040InstructionFaultScope=@{logicalCases=116736; batches=2; translationEnabled=$false; instructionCacheEnabled=$false; engine='accurate public factory plus cached self-branch batch'; forms=@('opcode','extension-low','next-opcode','self-branch'); banks=@('user','ISP','MSP'); ccr='all 32'; trace='All defined incoming states for frame/return; restart trace zero'; fields='Format7 SR, executing PC distinct from prefetch FA, RW/SIZE/TM and invalid writebacks'; phases=@('fault entry','handler return','restart','following'); remaining='Data writebacks/restart, speculative prefetch deferral, compiled instruction-fault PC provenance, cache-enabled fetches and physical timing'}; m68040RtePhysicalValidationScope=@{logicalCases=223872; batches=2; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); trace=@(0,0x8000,0x4000); alignment=@(0,1); vbr=@(0,0x10000); faults='Each byte of SR/PC/format/SSW/continuation-EA reads; 14 direct/one/two-throwaway supervisor paths'; phases=@('fault entry','handler return'); undefined='EA and invalid writeback/push data; SSW X'; remaining='Internal-restoration/double faults, other instruction faults and software writeback handlers; user-tail validation is qualified separately'}; m68040AccessDoubleFaultScope=@{logicalCases=105408; batches=5; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; dispatchCcr=@(0,31); engines=@('accurate','classic','V2'); faults='Every byte of format-7 stack/vector; later handler validation refaults; warmed RTE fallback and compiled B/W/L operand read/write side exits'; recovery='External reset, including restored host entry; interrupt, task/subroutine entry and halted batches remain inactive'; undefined='Partial stack write order, contents and exact SP'; remaining='Internal restoration, other handler-entry paths and general architectural access-fault restart'}; From 53251330d7cb04fe7cb0e5d970da30002dbadfc5 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 07:15:43 +0300 Subject: [PATCH 056/211] Qualify software trace service after 68040 user-tail repair --- Copper68k.Tests/Synthetic/README.md | 25 +++++ .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- .../SyntheticM68040UserRteFaultTests.cs | 75 +++++++++++--- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 97 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 47 +++++++++ scripts/test-copper68k-040-access-frames.ps1 | 38 ++++++-- .../test-copper68k-synthetic-mutations.ps1 | 19 +++- scripts/test-copper68k-synthetic.ps1 | 23 +++-- 8 files changed, 289 insertions(+), 37 deletions(-) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index b45569eb..0f44e14d 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -735,3 +735,28 @@ continuation consumption. Proof requires the intended diagnostic in all four complete reports. Expectations compose MC68040UM trace, throwaway and pending exception rules; no hardware execution is claimed. Mixed-epoch provenance, user-tail software trace-service programs and internal restoration remain open. + +### Executed software trace service after user-tail repair + +`SyntheticM68040UserRteFaultTests.UserTailSoftwareTraceService*` combines the +seven-store bridge with the shared integer service program. CU linear/flow and +CP49–55 cases test repaired saved trace, supplied completion-PC adjustment, +frame conversion, vector-9 lookup, direct call, marker once, optional saved-trace +clear, real RTE and the following BRA. Both original user M values and every +restored user/user-M/ISP/MSP bank are covered. This composes documented rules; +no hardware or FPU arithmetic qualification is claimed. + +Canonical scalar/batch reports each require 1,360,896 phases / 2,592 combinations; +structural reports each require 6,350,848 phases / 193,536 combinations. These +add 15,423,488 phases. The complete 040 selection requires 73 tests, 66 reports, +seven fixed examples and fourteen input identities; its retained 480-case +inventory still fails completion. This test-only checkpoint is qualified by +fresh full Release, strict reporting, reference audits and seventeen report +integrity controls; milestone 6 remains in progress. Production CPU source is +unchanged. See the plan and qualification record for exact evidence and limits. + +Use `--filter FullyQualifiedName~UserTailSoftwareTraceService` for focused +execution and `./scripts/test-copper68k-synthetic-mutations.ps1 -Scope UserRteSoftwareTrace` +for all four complete groups under each mutation. Intended BTST, return-length +and CP-vector diagnostics are required; unrelated failures do not qualify a +proof. Mixed-epoch trace provenance and internal restoration remain open. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index bae831c2..0009c525 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -33,7 +33,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() "real-access-fault-entry", "writeback-handler", "CP-context-transferred-vector" }) report.Record($"68040/RTE/format7/{form}/bank={bank}/op=4E73/ccr={ccr:X2}", "untested", form == "frame-validation-fault" - ? "Supervisor/user-tail validation, normal/CM and pending CT/CU/CP preserved-trace retry, and executed pending software trace service have separate software coverage; internal restoration and user/mixed-epoch trace protocols remain required. Physical/hardware behavior is separately unqualified" + ? "Supervisor/user-tail validation, normal/CM and pending CT/CU/CP preserved-trace retry, and executed supervisor/user-tail pending software trace service have separate software coverage; internal restoration and mixed-epoch trace protocols remain required. Physical/hardware behavior is separately unqualified" : form == "real-access-fault-entry" ? "Cache-disabled accurate instruction-fetch frames/restart are covered separately; data fault/writeback restart, enabled-cache deferral and compiled instruction-PC provenance remain required" : "Required fault/context protocol has no independently qualified execution fixture yet"); diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs index 1c06a978..6152ed3e 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs @@ -31,11 +31,21 @@ public sealed class SyntheticM68040UserRteFaultTests(ITestOutputHelper output) [Fact, Trait("Suite", "Synthetic")] public void UserTailBridgePreservesTraceChainedBatch() => Audit(true, true, true, true); - private void Audit(bool repair, bool batch, bool keepTrace = false, bool chained = false) + [Fact, Trait("Suite", "Synthetic")] + public void UserTailSoftwareTraceServiceBoundariesScalar() => Audit(true, false, true, false, true); + [Fact, Trait("Suite", "Synthetic")] + public void UserTailSoftwareTraceServiceBoundariesBatch() => Audit(true, true, true, false, true); + [Fact, Trait("Suite", "Synthetic")] + public void UserTailSoftwareTraceServiceChainedScalar() => Audit(true, false, true, true, true); + [Fact, Trait("Suite", "Synthetic")] + public void UserTailSoftwareTraceServiceChainedBatch() => Audit(true, true, true, true, true); + + private void Audit(bool repair, bool batch, bool keepTrace = false, bool chained = false, bool traceService = false) { var bus = new SyntheticM68040RteValidationFaultTests.ValidationFaultBus(); var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); - var report = new CoverageBatch("68040", keepTrace ? $"rte-user-trace-{(chained ? "chained" : "boundaries")}-{(batch ? "batch" : "scalar")}" : $"rte-user-{(repair ? "repair" : "fault")}-{(batch ? "batch" : "scalar")}"); + var traceGroup = traceService ? "rte-user-software-trace" : "rte-user-trace"; + var report = new CoverageBatch("68040", keepTrace ? $"{traceGroup}-{(chained ? "chained" : "boundaries")}-{(batch ? "batch" : "scalar")}" : $"rte-user-{(repair ? "repair" : "fault")}-{(batch ? "batch" : "scalar")}"); foreach (var matrix in keepTrace ? new[] { chained ? "structure" : "boundaries" } : ["boundaries", "structure"]) foreach (var start in new[] { "ISP", "MSP" }) foreach (var middle in matrix == "structure" ? new[] { "none", "user", "ISP", "MSP" } : ["none"]) @@ -48,20 +58,22 @@ private void Audit(bool repair, bool batch, bool keepTrace = false, bool chained foreach (var vbr in keepTrace && !chained ? new uint[] { 0x10000 } : [0, 0x10000]) foreach (var result in keepTrace ? new[] { "user", "user-M", "ISP", "MSP" } : repair && matrix == "boundaries" ? ["user", "ISP", "MSP"] : ["ISP"]) foreach (var restoredTrace in keepTrace || repair && matrix == "boundaries" ? new ushort[] { 0, 0x8000, 0x4000 } : [0]) - foreach (var form in keepTrace ? Forms.Concat(["CP50", "CP51", "CP52", "CP53", "CP54", "CP55"]) : Forms) + foreach (var form in traceService ? new[] { "CU-linear", "CU-flow", "CP49", "CP50", "CP51", "CP52", "CP53", "CP54", "CP55" } + : keepTrace ? Forms.Concat(["CP50", "CP51", "CP52", "CP53", "CP54", "CP55"]) : Forms) + foreach (var clearServiceTrace in traceService ? new[] { false, true } : [false]) foreach (var (offset, width) in matrix == "structure" ? SyntheticM68040RteValidationFaultTests.Reads(form) : [(0u, 2)]) for (var faultByte = 0; faultByte < (matrix == "structure" ? width : 1); faultByte++) { var path = middle == "none" ? new[] { start, "user" } : [start, middle, "user"]; Run(m, bus, report, repair, batch, matrix, path, userMaster, ccr, incoming, alignment, vbr, - result, restoredTrace, form, offset, width, faultByte, keepTrace); + result, restoredTrace, form, offset, width, faultByte, keepTrace, traceService, clearServiceTrace); } report.Complete(output); } private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTests.ValidationFaultBus bus, CoverageBatch report, bool repair, bool batch, string matrix, string[] path, bool userMaster, int ccr, - ushort incoming, uint alignment, uint vbr, string result, ushort restoredTrace, string form, uint offset, int width, int faultByte, bool keepTrace) + ushort incoming, uint alignment, uint vbr, string result, ushort restoredTrace, string form, uint offset, int width, int faultByte, bool keepTrace, bool traceService, bool clearServiceTrace) { bus.Disarm(); m.Reset(ccr); var pointers = new Dictionary { ["user"] = 0x7800 + alignment, ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; @@ -83,8 +95,9 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes var format = form.StartsWith("format") ? int.Parse(form[6..]) : form.StartsWith("invalid") ? int.Parse(form[7..]) : 7; var repairedFormat = form.StartsWith("invalid") ? 0 : format; var post = form.StartsWith("CP", StringComparison.Ordinal); - var continuation = post ? 0x8000 : form switch { "CM" => 0x1000, "CT" => 0x2000, "CU" => 0x4000, _ => 0 }; - var vector = post ? form == "CP" ? 49 : int.Parse(form[2..]) : form switch { "CT" => 9, "CU" => 11, _ => 0 }; + var unimplemented = form.StartsWith("CU", StringComparison.Ordinal); + var continuation = post ? 0x8000 : unimplemented ? 0x4000 : form switch { "CM" => 0x1000, "CT" => 0x2000, _ => 0 }; + var vector = post ? form == "CP" ? 49 : int.Parse(form[2..]) : unimplemented ? 11 : form == "CT" ? 9 : 0; var savedSr = Status(result, restoredTrace, ccr ^ 31); var handlerSr = (ushort)((priorSr | 0x2000) & ~0xc000); var retrySr = keepTrace ? (ushort)(priorSr | 0x2000) : handlerSr; @@ -125,19 +138,35 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes m.InitializePhysical(vbr + 8, Handler, 4); m.InitializePhysical(vbr + 32, PrivilegeHandler, 4); foreach (var v in new[] { 9, 11, 49 }) m.InitializePhysical(vbr + (uint)v * 4, PendingHandler, 4); if (post) m.InitializePhysical(vbr + (uint)vector * 4, PendingHandler, 4); + var service = traceService ? new SyntheticM68040SoftwareTraceProgram(form, clearServiceTrace, PendingHandler, Target, vbr) : null; + service?.Initialize(m); _ = SyntheticExecution.Prepare(m, [0x4e73]); m.Core.State.SetUserStackPointer(0x7800 + alignment); m.Core.State.SetInterruptStackPointer(0x4700 + alignment); m.Core.State.SetMasterStackPointer(0x7400 + alignment); m.Core.State.StatusRegister = Status(path[0], incoming, ccr); m.Core.State.VectorBaseRegister = vbr; - if (form == "CU") m.Core.State.M68040PendingFpuExceptions.Begin(2, 11, Target); + if (unimplemented) m.Core.State.M68040PendingFpuExceptions.Begin(2, 11, Target); if (post) m.Core.State.M68040PendingFpuExceptions.Begin(3, vector, Target); + if (traceService) + { + // The recorded pending event must survive conflicting FPU registers. + // These inputs exercise integer delivery only, not FPU arithmetic. + m.Core.State.M68040Fpu.Fpcr = 0; + m.Core.State.M68040Fpu.Fpsr = 0x08008198; + m.Core.State.M68040Fpu.Fpiar = 0x1234abcd; + } var e = ArchitecturalExpectation.Capture(m); var sequence = m.Core.State.ExceptionSequence; + if (traceService) + { + e.ControlChecks["FPCR preserved"] = (s => s.M68040Fpu.Fpcr, 0); + e.ControlChecks["FPSR preserved"] = (s => s.M68040Fpu.Fpsr, 0x08008198); + e.ControlChecks["FPIAR preserved"] = (s => s.M68040Fpu.Fpiar, 0x1234abcd); + } e.ControlChecks["one rejection"] = (_ => (uint)bus.Rejected.Count, 1); e.ControlChecks["original read address"] = (_ => bus.Rejected.Count == 1 ? bus.Rejected[0].Address : uint.MaxValue, frame + offset); e.ControlChecks["original read width"] = (_ => bus.Rejected.Count == 1 ? (uint)bus.Rejected[0].Width : 0, (uint)width); e.ControlChecks["discarded PCs not fetched"] = (_ => bus.Accesses.Any(a => a.Address is 0xdead0001 or 0xdead0003 or 0xdead0005) ? 1u : 0u, 0); e.ControlChecks["sentinel not executed"] = (s => s.D[7], e.D[7]); - if (form == "CU" || post) e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) == null ? 0u : 1u, 1); + if (unimplemented || post) e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(unimplemented ? 2 : 3) == null ? 0u : 1u, 1); pointers[exceptionBank] = accessFrame; SetStacks(e, pointers, handlerSr); e.Pc = Handler; e.ExceptionVector = 2; Provenance(e, sequence + 1, SyntheticMachine.Code, priorSr); SyntheticM68040RteValidationFaultTests.ExpectAccessFrame(m, e, accessFrame, priorSr, SyntheticMachine.Code, frame + offset, width); @@ -145,8 +174,14 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes var phases = repair ? new List { "fault-entry", "repair-SR", "repair-PC", "repair-format", "bridge-SR", "bridge-PC", "bridge-format", "repair-return-SR", "handler-return", "retry-RTE" } : new List { "fault-entry", "bare-return", "privilege" }; var retryTrace = keepTrace && incoming != 0 && vector == 0; - if (repair) { if (retryTrace) phases.Add("retry-trace-return"); if (vector != 0) phases.Add("pending-return"); phases.Add("following"); } - var id = $"68040/RTE/user-{(keepTrace ? "trace" : repair ? "repair" : "fault")}/{matrix}/{form}/path={string.Join('-', path)}/M={userMaster}/incoming={incoming:X4}/align={alignment}/VBR={vbr:X8}/result={result}/T={restoredTrace:X4}/read={offset}:{width}/byte={faultByte}/op=4E73/ccr={ccr:X2}"; + if (repair) + { + if (retryTrace) phases.Add("retry-trace-return"); + if (service != null) phases.AddRange(service.Phases(restoredTrace)); + else if (vector != 0) phases.Add("pending-return"); + phases.Add("following"); + } + var id = $"68040/RTE/user-{(traceService ? "software-trace" : keepTrace ? "trace" : repair ? "repair" : "fault")}/{matrix}/{form}/path={string.Join('-', path)}/M={userMaster}/incoming={incoming:X4}/align={alignment}/VBR={vbr:X8}/result={result}/T={restoredTrace:X4}/read={offset}:{width}/byte={faultByte}{(traceService ? $"/clear={clearServiceTrace}" : "")}/op=4E73/ccr={ccr:X2}"; var phase = 0; bool Step() { @@ -193,7 +228,7 @@ bool Step() if (vector != 0) { Pending(m, e, pointers, savedSr, vector, Target, Operand, sequence + 2); - if (form == "CU" || post) e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) == null ? 0u : 1u, 0); + if (unimplemented || post) e.ControlChecks["pending delivery"] = (s => s.M68040PendingFpuExceptions.Find(unimplemented ? 2 : 3) == null ? 0u : 1u, 0); } // The retried instruction begins with incoming trace, even if the // repaired SR clears it. CT/CU/CP conversion wins over RTE tracing; @@ -201,6 +236,22 @@ bool Step() if (retryTrace) Pending(m, e, pointers, savedSr, 9, Target, SyntheticMachine.Code, sequence + 2); if (!Step()) return; e.ControlChecks.Remove("old throwaways not reread"); + if (service != null) + { + var bank = ExceptionBank(savedSr); + if (!service.Run(m, e, pointers[bank], savedSr, Step)) return; + pointers[bank] += 12; + var returnedSr = service.ReturnedSr(savedSr); + SetStacks(e, pointers, returnedSr); e.Pc = service.ResumePc; e.ExceptionVector = null; + if (!Step()) return; + if ((returnedSr & 0xc000) != 0) + { + Pending(m, e, pointers, returnedSr, 9, service.ResumePc, service.ResumePc, sequence + 3); + e.Pc = SyntheticM68040SoftwareTraceProgram.TraceHandler; + } + Step(); + return; + } if (vector != 0 || retryTrace) { pointers[ExceptionBank(savedSr)] += 12; SetStacks(e, pointers, savedSr); e.Pc = Target; e.ExceptionVector = null; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index f08d7534..6938297d 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -5220,3 +5220,100 @@ identities still match after all checks. This test-only checkpoint is qualified; milestone 6 remains **in progress** with all required remaining scope retained. No CPU production fix, consumer rerun, package release or old-test retirement is included. + +### 040 user-tail executed software trace service — 2026-10-06 + +The new four `UserTailSoftwareTraceService` groups reuse the fixed-reference +integer program from supervisor-tail qualification after the seven-store +user-tail repair bridge. Expectations compose MC68040UM 2.2.2.1, 8.1/8.2.1, +8.2.6, 8.3 and 8.4.2/8.4.6.7. Pending CU/CP suppresses automatic RTE tracing; +the software inspects repaired saved T1/T0, adjusts the supplied CU completion +PC, converts the frame and fetches/calls vector 9 through a real stack/RTS +sequence. Its marker is written exactly once when eligible. Trace-retaining +and trace-clearing returns execute real RTE, followed by BRA to distinguish +software service from subsequent hardware tracing. + +The fixture verifies registers, all three stack banks, guarded memory, +exception sequence and saved PC/SR at each instruction. Original pending CP +vectors 49–55 must survive deliberately conflicting FPCR/FPSR/FPIAR values. +The temporary direct-call stack word is not an exception frame. No FPU opcode +or arithmetic is executed, and no combined user-tail hardware oracle is claimed. + +| Group, each scalar/batch | Phases | Combinations | +| --- | ---: | ---: | +| Canonical user-tail software service | 1,360,896 | 2,592 | +| Structural validation read bytes | 6,350,848 | 193,536 | + +Canonical cases cross every CCR and incoming 0/T1/T0. Structural cases use +CCR 0/31 and incoming T1, every validation-read byte, both alignments/VBRs and +all eight initial/middle paths. Both original user M values, every restored +user/user-M/ISP/MSP state and all three restored trace conditions are retained. +Each combination independently selects a trace-retaining/clearing service. +The maintained PowerShell command independently enumerates the literal program +transitions and matches all new counts; the earlier bridge counts are unchanged. + +Initial canonical execution passes both tests without a production CPU fix. +Fresh report-producing canonical and structural runs now pass all four reports +with zero skips, mismatches, unsupported execution or untested phases. Their +15,423,488 total phases and exact combination counts match the independent +command enumeration. These are focused results from the initial build, not +final restored-build acceptance. The maintained +`UserRteSoftwareTrace` scope adds saved-SR BTST inversion, short format-2 return +and original CP-vector loss, requiring intended diagnostics in each of four +complete reports. All three probes are detected in all four complete reports; +every production source byte matches the preceding qualified manifest after +restoration. Fresh acceptance below qualifies this test-only checkpoint. No package +release or test retirement is included. Mixed-epoch trace provenance, internal +restoration and all other required model/reference/consolidation scope remain +open; milestone 6 stays **in progress**. + +The first maintained probe, `040-user-software-trace-bit-test`, executes all +four complete groups with zero skips or unsupported execution. Each canonical +route detects 82,944 mismatches, leaves 430,080 dependent phases untested and +passes 847,872 phases. Each structural route detects 387,072, leaves 2,007,040 +dependent phases untested and passes 3,956,736. Every report contains the +intended CU-linear `test-T1` SR diagnostic. This qualifies the probe's ability +to detect a wrong saved-SR bit test; these deliberate failures are not +production failures. + +The short-format-2-return probe also executes all four complete reports with +zero skips or unsupported execution. Each canonical route detects 39,936 +mismatches, leaves 39,936 dependent phases untested and passes 1,281,024 phases. +Each structural route detects 186,368, leaves 186,368 dependent phases untested +and passes 5,978,112. Every report contains the intended CU-linear +`pending-return` inactive-stack-pointer diagnostic. This distinguishes the +required 12-byte pop from a wrong 8-byte return, including inactive stack banks. +The original-vector probe executes all four complete reports with zero skips +or unsupported execution. Each canonical route detects 55,296 mismatches, +leaves 322,560 dependent phases untested and passes 983,040 phases. Each +structural route detects 258,048 mismatches, leaves 1,505,280 dependent phases +untested and passes 4,587,520. Every report contains the intended CP50 +`retry-RTE` diagnostic, expected exception 50 versus actual 49. + +The restored acceptance build records CPU SHA256 +`c427185573ad05ef6feca134d067ce41951f06c18002c429f73589ac306298cc` +and adapter SHA256 +`459397c204b6b221b1dad6dbb5aba28ea91ca13713f4b4ee29a7fd00b3930111`. +Its source checkpoint is `47d6400a29002f86b96dc73afa6d10f82c0d8c62` +plus the recorded fixture/command changes; these binaries are not relabeled as +the later commit containing this record. + +Fresh full Release validation in `artifacts/m6-user-service-full/` passes +**5,098 tests**, with eleven optional skips and zero failures. All nine qualified +WinUAE presets match their exact selections and both assembly identities. +Strict ordinary reporting verifies **63,082,006 logical cases / 656 batches**, +with `roadmapComplete=false`. Fresh pinned SingleStepTests passes 312,500 cases +from 125 files; Musashi passes 536 programs with 88 explicit exclusions and +624 total rows. Both explicit reference selections execute one test with no skip. + +The complete 040 command in `artifacts/m6-user-service-audit/` executes all +**73 tests: 72 pass and the sole failure is the required 480-case inventory**. +Its 66 reports contain **48,113,344 passing phases**, zero mismatches or +unsupported execution, and 480 named untested requirements. Seven fixed +examples and fourteen fixture/command identities are retained. Seventeen fresh +controls reject their specific missing, shortened, foreign, redistributed or +omitted-fixture corruption. All production source, fixture/input and assembly +identities still match after reference and integrity checks. Milestone 6 remains +**in progress**; mixed-epoch provenance, internal restoration and all retained +model/reference/consolidation scope remain required. No CPU production fix, +package release, consumer rerun or old-test retirement is included. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index ecbaa822..533009e3 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1977,3 +1977,50 @@ checkpoint is qualified. Mixed-epoch trace provenance, user-tail software trace-service programs, internal restoration and all other retained model/reference/consolidation requirements remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +### Milestone 6 user-tail software trace service — 2026-10-06 + +Four new `UserTailSoftwareTraceService` scalar/batch groups compose the existing +seven-store user-tail repair bridge with the shared executed integer trace +service. CU linear/flow cases and CP49–55 inspect repaired saved T1/T0, adjust +the supplied CU completion PC, convert the pending frame, fetch vector 9 and +call the trace handler through a temporary stack/RTS target. The handler writes +its marker once and either retains or clears saved trace before a real RTE. +The following BRA checks resumed hardware tracing independently of the software +call; hardware exception sequence and provenance must remain unchanged during +the integer software service. No FPU opcode or arithmetic is executed. + +Canonical groups each require 1,360,896 phases / 2,592 combinations; structural +groups each require 6,350,848 phases / 193,536 combinations. Canonical cases +cross all CCRs and incoming 0/T1/T0; structural cases use CCR 0/31, incoming T1, +all fourteen validation-read bytes, both alignments/VBRs and all initial/middle +paths. Both original user M values, all restored user/user-M/ISP/MSP states, +restored trace conditions and trace-service return choices are covered. + +These four groups add 15,423,488 phases. The maintained complete 040 command +must execute 73 tests, 66 reports, seven fixed examples and fourteen input +identities, with 48,113,344 passing phases plus the retained 480-case inventory. +Independent PowerShell command enumeration matches every new combination count +and preserves the earlier user-tail bridge counts. Initial canonical execution +passes two tests without a production CPU change. Fresh report-producing +canonical and structural runs pass all four groups, 15,423,488 total phases, +with no skips, mismatches, unsupported execution or untested phases. These +initial-build focused results are not final restored-build acceptance. The maintained +`UserRteSoftwareTrace` mutation scope requires inverted saved-SR BTST, a short +format-2 return and original CP-vector loss to produce their intended diagnostic +in all four complete reports, then restores production bytes exactly. The +three-probe command detects every intended defect in all four complete reports +and restores production source bytes exactly. Fresh full Release acceptance +passes 5,098 tests with eleven optional skips and zero failures; all nine +qualified WinUAE presets match exact selections and assembly identities. +Strict reporting verifies 63,082,006 logical cases / 656 batches. Fresh +SingleStepTests passes 312,500 cases / 125 files; Musashi passes 536 programs +with 88 explicit exclusions. The complete 040 command executes all 73 tests, +passes 72 and fails only its retained 480-case inventory, with 48,113,344 passing +phases and zero mismatches or unsupported execution. Seventeen integrity +controls reject their specific corruption. Current source, fixture/input and +assembly identities still match after all checks. + +This test-only checkpoint is qualified. Mixed-epoch provenance, +internal restoration, all earlier model/reference/consolidation requirements +and the complete roadmap remain required; milestone 6 stays **in progress**. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index 54584e3e..ceb742e9 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -69,6 +69,10 @@ $expected = [ordered]@{ 'rte-user-trace-boundaries-batch' = @{cases=873984; combinations=2304} 'rte-user-trace-chained-scalar' = @{cases=3502080; combinations=145920} 'rte-user-trace-chained-batch' = @{cases=3502080; combinations=145920} + 'rte-user-software-trace-boundaries-scalar' = @{cases=1360896; combinations=2592} + 'rte-user-software-trace-boundaries-batch' = @{cases=1360896; combinations=2592} + 'rte-user-software-trace-chained-scalar' = @{cases=6350848; combinations=193536} + 'rte-user-software-trace-chained-batch' = @{cases=6350848; combinations=193536} 'rte-access-remaining-protocols' = @{cases=480; combinations=15} } $testExit = 0 @@ -105,9 +109,9 @@ if (-not $ValidateReportsOnly) { 'Multiple continuation bits are architecturally undefined and excluded', 'Selected active accurate-batch paths verify counts/callbacks and scalar/batch bus/cycle policy; generic short operand frames do not qualify architectural format-7 data restart', 'Instruction-fault fixtures use cache-disabled accurate execution and physical-map rejection; speculative deferral, enabled caches/MMU and compiled fetch PC provenance remain unqualified', - 'Legacy repair clears saved trace; separate supervisor and user-tail bridge groups preserve incoming trace, check normal/CM completion and pending CT/CU/CP49-55 priority. Executed integer trace service has separate supervisor-tail coverage; mixed-epoch and user-tail software trace-service protocols remain required', + 'Legacy repair clears saved trace; separate supervisor and user-tail bridge groups preserve incoming trace, check normal/CM completion and pending CT/CU/CP49-55 priority. Executed integer trace service has separate supervisor/user-tail coverage; mixed-epoch trace protocols remain required', 'User-tail fault expectations compose documented throwaway live-SR rules with general supervisor exception entry; unusual combined hardware behavior has not been observed', - 'Mixed-epoch trace provenance, user-tail software trace service, internal-restoration double faults, cache/MMU/compiled handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', + 'Mixed-epoch trace provenance, internal-restoration double faults, cache/MMU/compiled handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', 'Direct odd-RTE saved-SR ordering uses documentary WinUAE 5d22d336, not an executed hardware oracle') } $identity | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'identities.json') @@ -135,8 +139,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 69 -or [int]$counters.total -ne 69 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (62 batches, 7 fixed examples)' +if ([int]$counters.executed -ne 73 -or [int]$counters.total -ne 73 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (66 batches, 7 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -159,8 +163,9 @@ foreach ($group in $expected.Keys) { $expectedCombinations["68040/RTE/format7/$form/bank=$bank"] = 32 } } - } elseif ($group.StartsWith('rte-user-trace-', [StringComparison]::Ordinal)) { - $chained = $group.StartsWith('rte-user-trace-chained-', [StringComparison]::Ordinal) + } elseif ($group.StartsWith('rte-user-trace-', [StringComparison]::Ordinal) -or $group.StartsWith('rte-user-software-trace-', [StringComparison]::Ordinal)) { + $userSoftwareTrace = $group.StartsWith('rte-user-software-trace-', [StringComparison]::Ordinal) + $chained = $group.Contains('-chained-', [StringComparison]::Ordinal) foreach ($start in @('ISP','MSP')) { foreach ($middle in $(if ($chained) { @('none','user','ISP','MSP') } else { @('none') })) { $path = if ($middle -eq 'none') { "$start-user" } else { "$start-$middle-user" } @@ -170,7 +175,8 @@ foreach ($group in $expected.Keys) { foreach ($vbr in $(if ($chained) { @(0,0x10000) } else { @(0x10000) })) { foreach ($result in @('user','user-M','ISP','MSP')) { foreach ($trace in @(0,0x8000,0x4000)) { - foreach ($form in @('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP','CP50','CP51','CP52','CP53','CP54','CP55')) { + foreach ($form in $(if ($userSoftwareTrace) { @('CU-linear','CU-flow','CP49','CP50','CP51','CP52','CP53','CP54','CP55') } else { @('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP','CP50','CP51','CP52','CP53','CP54','CP55') })) { + foreach ($clear in $(if ($userSoftwareTrace) { @($false,$true) } else { @($false) })) { $reads = @('0:2') if ($chained) { $reads += @('2:4','6:2') @@ -180,14 +186,26 @@ foreach ($group in $expected.Keys) { foreach ($read in $reads) { $parts = $read.Split(':') for ($byte=0; $byte -lt $(if ($chained) {[int]$parts[1]} else {1}); $byte++) { - $key = '68040/RTE/user-trace/{0}/{1}/path={2}/M={3}/incoming={4:X4}/align={5}/VBR={6:X8}/result={7}/T={8:X4}/read={9}:{10}/byte={11}' -f $(if ($chained) {'structure'} else {'boundaries'}),$form,$path,$master,$incoming,$alignment,$vbr,$result,$trace,$parts[0],$parts[1],$byte + $key = '68040/RTE/user-{0}/{1}/{2}/path={3}/M={4}/incoming={5:X4}/align={6}/VBR={7:X8}/result={8}/T={9:X4}/read={10}:{11}/byte={12}' -f $(if ($userSoftwareTrace) {'software-trace'} else {'trace'}),$(if ($chained) {'structure'} else {'boundaries'}),$form,$path,$master,$incoming,$alignment,$vbr,$result,$trace,$parts[0],$parts[1],$byte # Seven stores, two RTEs, entry and following; pending # conversion or incoming trace adds one handler return. $pending = $form -in @('CT','CU','CP','CP50','CP51','CP52','CP53','CP54','CP55') $phases = if ($pending -or $incoming -ne 0) {12} else {11} + if ($userSoftwareTrace) { + $key += "/clear=$clear" + # Literal program transitions; do not call fixture Phases(). + $phases = 14 + if ($form.StartsWith('CU')) { $phases++ } + if ($form -eq 'CU-flow' -and $trace -ne 0x8000) { $phases += 2 } + if ($trace -eq 0x8000 -or ($form -eq 'CU-flow' -and $trace -eq 0x4000)) { + $phases += 5 + if ($clear) { $phases++ } + } + } $expectedCombinations[$key] = $(if ($chained) {2} else {32}) * $phases } } + } } } } @@ -502,8 +520,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 69 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 73 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=32690336; xunitBatches=62; fixedExamples=7; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=48113824; xunitBatches=66; fixedExamples=7; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index 406df848..ff79618b 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','RteSoftwareTrace','UserRteFault','UserRteTrace','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','RteSoftwareTrace','UserRteFault','UserRteTrace','UserRteSoftwareTrace','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -443,6 +443,14 @@ foreach ($mutation in $userTraceMutations) { $mutation.group='UserTailBridgePreservesTrace'; $mutation.model='68040'; $mutation.milestone=6 $mutation.userTrace=$true; $mutations += $mutation } +foreach ($template in $softwareTraceMutations) { + $mutation = $template.Clone() + $mutation.Remove('softwareTrace') + $mutation.name = $template.name.Replace('040-software-trace-', '040-user-software-trace-') + $mutation.proofCombination = $template.proofCombination.Replace('/software-trace/', '/') + $mutation.group='UserTailSoftwareTraceService'; $mutation.userSoftwareTrace=$true + $mutations += $mutation +} $instructionMutations = @( @{name='040-instruction-short-frame'; before=' RaiseUnbufferedReadAccessFault(fault, stackedProgramCounter, instruction: true);'; after=' RaiseFormat0Exception(VectorBusError, stackedProgramCounter, M68kInstructionTimingKey.IllegalInstruction);'; proofCombination='/instruction/opcode/'}, @@ -521,6 +529,7 @@ if ($Scope -eq 'HandlerPrefetch') { $mutations = @($mutations | Where-Object { $ if ($Scope -eq 'EntryPrefetch') { $mutations = @($mutations | Where-Object { $_.entryPrefetch }) } if ($Scope -eq 'UserRteFault') { $mutations = @($mutations | Where-Object { $_.userRte }) } if ($Scope -eq 'UserRteTrace') { $mutations = @($mutations | Where-Object { $_.userTrace }) } +if ($Scope -eq 'UserRteSoftwareTrace') { $mutations = @($mutations | Where-Object { $_.userSoftwareTrace }) } if ($Scope -eq 'AccessDoubleFault') { $mutations = @($mutations | Where-Object { $_.doubleFault }) } if ($Scope -eq 'BatchFault') { $mutations = @($mutations | Where-Object { $_.batchFault }) } if ($Scope -eq 'LowPowerStop') { $mutations = @($mutations | Where-Object { $_.lpstop }) } @@ -633,15 +642,15 @@ try { throw 'Instruction-fault mutation omitted complete cases or intended semantic failure' } } - if ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors -or $mutation.softwareTrace -or $mutation.userTrace) { + if ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors -or $mutation.softwareTrace -or $mutation.userTrace -or $mutation.userSoftwareTrace) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if ($trx.TestRun.ResultSummary.Counters.executed -ne 4 -or $trx.TestRun.ResultSummary.Counters.failed -ne 4 -or $batches.Count -ne 4) { throw 'Retry-trace mutation omitted its complete four-batch selection' } foreach ($matrix in @('boundaries','chained')) { foreach ($route in @('scalar','batch')) { - $prefix = if ($mutation.userTrace) {'rte-user-trace'} elseif ($mutation.softwareTrace) {'rte-software-trace'} elseif ($mutation.cpVectors) {'rte-cp-vectors'} elseif ($mutation.userMaster) {'rte-user-master'} elseif ($mutation.pendingTrace) {'rte-pending-trace'} else {'rte-retry-trace'} - $cases = if ($mutation.userTrace) {if ($matrix -eq 'boundaries') {873984} else {3502080}} elseif ($mutation.softwareTrace) {if ($matrix -eq 'boundaries') {514560} else {3601920}} elseif ($mutation.cpVectors) {if ($matrix -eq 'boundaries') {110592} else {2322432}} elseif ($mutation.userMaster) {if ($matrix -eq 'boundaries') {44736} else {714240}} elseif ($mutation.pendingTrace) {if ($matrix -eq 'boundaries') {41472} else {870912}} else {if ($matrix -eq 'boundaries') {92736} else {1271808}} + $prefix = if ($mutation.userSoftwareTrace) {'rte-user-software-trace'} elseif ($mutation.userTrace) {'rte-user-trace'} elseif ($mutation.softwareTrace) {'rte-software-trace'} elseif ($mutation.cpVectors) {'rte-cp-vectors'} elseif ($mutation.userMaster) {'rte-user-master'} elseif ($mutation.pendingTrace) {'rte-pending-trace'} else {'rte-retry-trace'} + $cases = if ($mutation.userSoftwareTrace) {if ($matrix -eq 'boundaries') {1360896} else {6350848}} elseif ($mutation.userTrace) {if ($matrix -eq 'boundaries') {873984} else {3502080}} elseif ($mutation.softwareTrace) {if ($matrix -eq 'boundaries') {514560} else {3601920}} elseif ($mutation.cpVectors) {if ($matrix -eq 'boundaries') {110592} else {2322432}} elseif ($mutation.userMaster) {if ($matrix -eq 'boundaries') {44736} else {714240}} elseif ($mutation.pendingTrace) {if ($matrix -eq 'boundaries') {41472} else {870912}} else {if ($matrix -eq 'boundaries') {92736} else {1271808}} $target = @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq "$prefix-$matrix-$route" }) if ($target.Count -ne 1 -or $target[0].logicalCases -ne $cases -or @($target[0].failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and @@ -713,7 +722,7 @@ try { } elseif ($mutation.entryPrefetch) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] - } elseif ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors -or $mutation.softwareTrace -or $mutation.userTrace) { + } elseif ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors -or $mutation.softwareTrace -or $mutation.userTrace -or $mutation.userSoftwareTrace) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.id.EndsWith($mutation.proofPhase, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] } elseif ($mutation.userRte) { diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index a5f37ee9..a296e9ee 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -209,6 +209,10 @@ try { $expected['rte-user-trace-boundaries-batch'] = 873984 $expected['rte-user-trace-chained-scalar'] = 3502080 $expected['rte-user-trace-chained-batch'] = 3502080 + $expected['rte-user-software-trace-boundaries-scalar'] = 1360896 + $expected['rte-user-software-trace-boundaries-batch'] = 1360896 + $expected['rte-user-software-trace-chained-scalar'] = 6350848 + $expected['rte-user-software-trace-chained-batch'] = 6350848 $expected['system-mmu-disabled'] = 34850 $expected['system-linef-pc-restore'] = 6464 } @@ -306,15 +310,16 @@ try { externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'}; m68040HandlerPrefetchScope=@{logicalCases=457728; batches=8; routes=@('accurate scalar','accurate batch'); bus='Physical map with no host code reader, including stable-map generation'; qualified='Four-long access-error entry window, every entry byte, odd handler addresses, retained data, flow/host/task/map invalidation and later instruction faults; denied cold boundaries perform no CPU fetch/state change'; remaining='Other exception-entry paths, host-reader/compiled fetch provenance, enabled caches/MMU, ordinary speculative deferral and physical timing'}; - m68040UserTailScope=@{logicalCases=1687040; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); authority='Composition of MC68040UM 2.2.2.1, 8.1, 8.2.1/5, 8.4.2 and 8.4.6.7; software qualification, no executed hardware reference'; userM=@(0,1); initialBanks=@('ISP','MSP'); middleBanks=@('none','user','ISP','MSP'); ccr='all 32 canonical SR reads, 0/31 structural reads'; forms='0/2/3, invalid 4/15, normal/CM/CT/CU/CP49'; qualified='Live intermediate SR, supervisor exception stack selected by M, user-data TM=1, preserved USP frame and committed throwaways, bare return followed by privilege violation, executed seven-store repair with fresh throwaway bridge and following trace/continuation'; remaining='Internal restoration faults, mixed-epoch trace provenance, user-tail software trace service, physical bus-order/timing and hardware observation of composed user-tail fault behavior'}; - m68040UserTailTraceScope=@{logicalCases=8752128; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); userM=@(0,1); initialBanks=@('ISP','MSP'); middleBanks=@('none','user','ISP','MSP'); restoredBanks=@('user','user-M','ISP','MSP'); canonicalIncoming=@(0,0x8000,0x4000); structuralIncoming=@(0x8000); restoredTrace=@(0,0x8000,0x4000); forms='0/2/3, invalid 4/15, normal/CM/CT/CU/CP49-55'; ccr='all 32 canonical, 0/31 every validation-read byte'; qualified='Seven real stores preserve incoming trace while adding S only to the access return; fresh supervisor throwaway restores user SR, original RTE completion traces independently of restored bits, CT/CU/CP priority suppresses extra trace; handler return, following flow/MOVEM and all stack banks checked'; remaining='User-tail software trace service, mixed-epoch trace provenance, internal restoration and hardware/physical qualification'}; - m68040RteCpVectorsScope=@{logicalCases=4866048; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); vectors=@(50,51,52,53,54,55); restoredBanks=@('user','user-M','ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); qualified='Original CP vector survives validation faults, three repair stores and incoming trace; format-3 saved SR/PC/EA, pending-context consumption, all stack banks, FPU register preservation, handler return and following traced BRA checked'; ccr='all 32 canonical, 0/31 every validation-read byte'; remaining='User-tail software trace service, mixed-epoch provenance, context transfer and internal restoration'}; - m68040RteSoftwareTraceScope=@{logicalCases=8232960; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); forms=@('CU-linear','CU-flow','CP49','CP50','CP51','CP52','CP53','CP54','CP55'); restoredBanks=@('user','user-M','ISP','MSP'); canonicalIncoming=@(0,0x8000,0x4000); structuralIncoming=@(0x8000); restoredTrace=@(0,0x8000,0x4000); ccr='all 32 canonical, 0/31 every validation-read byte'; qualified='Executed integer handler inspects saved T1/T0, advances emulated CU PC, converts format/address, reads vector 9 and directly calls software trace service; trace handler preserves or clears saved trace and really returns; hardware exception counter does not change for software calls'; remaining='Actual FPU emulation, user-tail trace bridge, mixed-epoch provenance, internal restoration and hardware/physical qualification'}; - m68040RteUserMasterScope=@{logicalCases=1517952; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); restoredSr='S=0,M=1'; forms=@('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP49'); incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); qualified='User execution retains USP; synchronous completion trace, pending conversion and following trace select MSP while preserving M; all three stack banks, saved SR/PC/EA, pending consumption and handler returns checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='User-tail software trace service, mixed-epoch provenance, internal restoration'}; - m68040RtePendingTraceScope=@{logicalCases=1824768; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); forms='CT/CU/CP49'; incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); qualified='Three original-frame stores preserve access SR; returned incoming trace cannot create an extra RTE trace on pending conversion; saved SR/PC/EA, format/vector, pending consumption, handler return and following traced BRA are checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='User-tail software trace service, mixed-epoch provenance, internal restoration and physical/hardware qualification'}; - m68040RteRetryTraceScope=@{logicalCases=2729088; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); faultTailBanks=@('ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredBanks=@('user','ISP','MSP'); restoredTrace=@(0,0x8000,0x4000); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM'; qualified='Three original-frame repair stores leave saved access SR untouched; handler return restores incoming trace; successful original RTE triggers T1/T0 trace with restored SR/PC; trace-handler return and following instruction retain MOVEM EA and stack selection'; ccr='all 32 canonical, 0/31 every chained read byte'; remaining='User-tail software trace service, mixed trace values within consumed throwaways, internal restoration and hardware/physical timing'}; - m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, executed repair/retry after committed supervisor throwaways, and manual-derived user-tail fault/privilege/bridge repair; chained odd-PC saved-SR provenance, internal-restoration faults, user-tail software trace service/mixed-epoch trace provenance, CP context transfer, other real fault entry and physical timing remain unqualified'; - m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='Internal restoration, user-tail software trace service/mixed-epoch trace provenance and other real access faults; normal/CM preserved-trace and user-tail repair are qualified separately'}; + m68040UserTailScope=@{logicalCases=1687040; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); authority='Composition of MC68040UM 2.2.2.1, 8.1, 8.2.1/5, 8.4.2 and 8.4.6.7; software qualification, no executed hardware reference'; userM=@(0,1); initialBanks=@('ISP','MSP'); middleBanks=@('none','user','ISP','MSP'); ccr='all 32 canonical SR reads, 0/31 structural reads'; forms='0/2/3, invalid 4/15, normal/CM/CT/CU/CP49'; qualified='Live intermediate SR, supervisor exception stack selected by M, user-data TM=1, preserved USP frame and committed throwaways, bare return followed by privilege violation, executed seven-store repair with fresh throwaway bridge and following trace/continuation'; remaining='Internal restoration faults, mixed-epoch trace provenance, physical bus-order/timing and hardware observation of composed user-tail fault behavior'}; + m68040UserTailTraceScope=@{logicalCases=8752128; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); userM=@(0,1); initialBanks=@('ISP','MSP'); middleBanks=@('none','user','ISP','MSP'); restoredBanks=@('user','user-M','ISP','MSP'); canonicalIncoming=@(0,0x8000,0x4000); structuralIncoming=@(0x8000); restoredTrace=@(0,0x8000,0x4000); forms='0/2/3, invalid 4/15, normal/CM/CT/CU/CP49-55'; ccr='all 32 canonical, 0/31 every validation-read byte'; qualified='Seven real stores preserve incoming trace while adding S only to the access return; fresh supervisor throwaway restores user SR, original RTE completion traces independently of restored bits, CT/CU/CP priority suppresses extra trace; handler return, following flow/MOVEM and all stack banks checked'; remaining='mixed-epoch trace provenance, internal restoration and hardware/physical qualification'}; + m68040UserTailSoftwareTraceScope=@{logicalCases=15423488; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); userM=@(0,1); initialBanks=@('ISP','MSP'); middleBanks=@('none','user','ISP','MSP'); restoredBanks=@('user','user-M','ISP','MSP'); forms=@('CU-linear','CU-flow','CP49','CP50','CP51','CP52','CP53','CP54','CP55'); canonicalIncoming=@(0,0x8000,0x4000); structuralIncoming=@(0x8000); restoredTrace=@(0,0x8000,0x4000); ccr='all 32 canonical, 0/31 every validation-read byte'; qualified='Seven real user-tail repair stores retain trace; pending CU/CP preserves original event; integer handler tests repaired saved T1/T0, adjusts supplied CU completion PC, converts frame, reads vector 9, calls via stack/RTS, writes marker once when eligible, optionally clears saved trace and really returns; following BRA checks resumed hardware tracing without new hardware exception provenance during software service'; remaining='Mixed-epoch provenance, internal restoration, CP context transfer and hardware/physical qualification'; outsideRoadmap='FPU arithmetic and enabled-MMU operation'}; + m68040RteCpVectorsScope=@{logicalCases=4866048; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); vectors=@(50,51,52,53,54,55); restoredBanks=@('user','user-M','ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); qualified='Original CP vector survives validation faults, three repair stores and incoming trace; format-3 saved SR/PC/EA, pending-context consumption, all stack banks, FPU register preservation, handler return and following traced BRA checked'; ccr='all 32 canonical, 0/31 every validation-read byte'; remaining='mixed-epoch provenance, context transfer and internal restoration'}; + m68040RteSoftwareTraceScope=@{logicalCases=8232960; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); forms=@('CU-linear','CU-flow','CP49','CP50','CP51','CP52','CP53','CP54','CP55'); restoredBanks=@('user','user-M','ISP','MSP'); canonicalIncoming=@(0,0x8000,0x4000); structuralIncoming=@(0x8000); restoredTrace=@(0,0x8000,0x4000); ccr='all 32 canonical, 0/31 every validation-read byte'; qualified='Executed integer handler inspects saved T1/T0, advances emulated CU PC, converts format/address, reads vector 9 and directly calls software trace service; trace handler preserves or clears saved trace and really returns; hardware exception counter does not change for software calls'; remaining='Actual FPU emulation and hardware execution remain outside this integer fixture; mixed-epoch provenance, internal restoration and hardware/physical qualification'}; + m68040RteUserMasterScope=@{logicalCases=1517952; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); restoredSr='S=0,M=1'; forms=@('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP49'); incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); qualified='User execution retains USP; synchronous completion trace, pending conversion and following trace select MSP while preserving M; all three stack banks, saved SR/PC/EA, pending consumption and handler returns checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='mixed-epoch provenance, internal restoration'}; + m68040RtePendingTraceScope=@{logicalCases=1824768; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); forms='CT/CU/CP49'; incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); qualified='Three original-frame stores preserve access SR; returned incoming trace cannot create an extra RTE trace on pending conversion; saved SR/PC/EA, format/vector, pending consumption, handler return and following traced BRA are checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='mixed-epoch provenance, internal restoration and physical/hardware qualification'}; + m68040RteRetryTraceScope=@{logicalCases=2729088; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); faultTailBanks=@('ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredBanks=@('user','ISP','MSP'); restoredTrace=@(0,0x8000,0x4000); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM'; qualified='Three original-frame repair stores leave saved access SR untouched; handler return restores incoming trace; successful original RTE triggers T1/T0 trace with restored SR/PC; trace-handler return and following instruction retain MOVEM EA and stack selection'; ccr='all 32 canonical, 0/31 every chained read byte'; remaining='mixed trace values within consumed throwaways, internal restoration and hardware/physical timing'}; + m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, executed repair/retry after committed supervisor throwaways, and manual-derived user-tail fault/privilege/bridge repair; chained odd-PC saved-SR provenance, internal-restoration faults, mixed-epoch trace provenance, CP context transfer, other real fault entry and physical timing remain unqualified'; + m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='Internal restoration, mixed-epoch trace provenance and other real access faults; normal/CM preserved-trace and user-tail repair are qualified separately'}; m68040InstructionFaultScope=@{logicalCases=116736; batches=2; translationEnabled=$false; instructionCacheEnabled=$false; engine='accurate public factory plus cached self-branch batch'; forms=@('opcode','extension-low','next-opcode','self-branch'); banks=@('user','ISP','MSP'); ccr='all 32'; trace='All defined incoming states for frame/return; restart trace zero'; fields='Format7 SR, executing PC distinct from prefetch FA, RW/SIZE/TM and invalid writebacks'; phases=@('fault entry','handler return','restart','following'); remaining='Data writebacks/restart, speculative prefetch deferral, compiled instruction-fault PC provenance, cache-enabled fetches and physical timing'}; m68040RtePhysicalValidationScope=@{logicalCases=223872; batches=2; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); trace=@(0,0x8000,0x4000); alignment=@(0,1); vbr=@(0,0x10000); faults='Each byte of SR/PC/format/SSW/continuation-EA reads; 14 direct/one/two-throwaway supervisor paths'; phases=@('fault entry','handler return'); undefined='EA and invalid writeback/push data; SSW X'; remaining='Internal-restoration/double faults, other instruction faults and software writeback handlers; user-tail validation is qualified separately'}; m68040AccessDoubleFaultScope=@{logicalCases=105408; batches=5; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; dispatchCcr=@(0,31); engines=@('accurate','classic','V2'); faults='Every byte of format-7 stack/vector; later handler validation refaults; warmed RTE fallback and compiled B/W/L operand read/write side exits'; recovery='External reset, including restored host entry; interrupt, task/subroutine entry and halted batches remain inactive'; undefined='Partial stack write order, contents and exact SP'; remaining='Internal restoration, other handler-entry paths and general architectural access-fault restart'}; From 5e90768279da33092d2da3f14e870a6a7881da44 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 08:57:08 +0300 Subject: [PATCH 057/211] Qualify mixed trace epochs in 68040 throwaway chains --- Copper68k.Tests/Synthetic/README.md | 18 +++ .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- .../SyntheticM68040ThrowawayTests.cs | 152 +++++++++++++++--- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 55 +++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 42 +++++ scripts/test-copper68k-040-access-frames.ps1 | 34 +++- .../test-copper68k-synthetic-mutations.ps1 | 47 +++++- scripts/test-copper68k-synthetic.ps1 | 5 + 8 files changed, 327 insertions(+), 28 deletions(-) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 0f44e14d..f9c937f8 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -760,3 +760,21 @@ execution and `./scripts/test-copper68k-synthetic-mutations.ps1 -Scope UserRteSo for all four complete groups under each mutation. Intended BTST, return-length and CP-vector diagnostics are required; unrelated failures do not qualify a proof. Mixed-epoch trace provenance and internal restoration remain open. + +### Successful mixed-epoch throwaway chains + +`SyntheticM68040ThrowawayTests.MixedEpochTrace*` independently crosses incoming, +first/second throwaway and final trace states 0/T1/T0. It includes user M=1, +all stack aliases, short frames and every defined access continuation. Incoming +trace controls completion of the original RTE; pending CT/CU/CP has priority. +CM also executes MOVEM, its optional trace return and a following BRA. + +Canonical scalar/batch reports each require 1,152,000 phases / 12,096 combinations; +structural reports each require 3,744,000 phases / 628,992 combinations. The +`MixedEpochRte` mutation scope requires intended provenance, T0 and MOVEM-lifetime +diagnostics in all four complete reports, including a second-throwaway SR +diagnostic in both structural reports. Focused restored-build qualification passes +all four reports and six independent SR encoding examples, with zero skips. +The separate current inventory still fails its 480 required cases. Previous +broader qualification remains a separate checkpoint. Mixed epochs +during validation faults, repair/retry and internal restoration remain required. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 0009c525..a64dcb43 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -33,7 +33,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() "real-access-fault-entry", "writeback-handler", "CP-context-transferred-vector" }) report.Record($"68040/RTE/format7/{form}/bank={bank}/op=4E73/ccr={ccr:X2}", "untested", form == "frame-validation-fault" - ? "Supervisor/user-tail validation, normal/CM and pending CT/CU/CP preserved-trace retry, and executed supervisor/user-tail pending software trace service have separate software coverage; internal restoration and mixed-epoch trace protocols remain required. Physical/hardware behavior is separately unqualified" + ? "Validation, preserved-trace repair/software service and successful mixed-epoch throwaway chains have separate software coverage; mixed epochs during validation faults, repair/retry and internal restoration remain required. Physical/hardware behavior is separately unqualified" : form == "real-access-fault-entry" ? "Cache-disabled accurate instruction-fetch frames/restart are covered separately; data fault/writeback restart, enabled-cache deferral and compiled instruction-PC provenance remain required" : "Required fault/context protocol has no independently qualified execution fixture yet"); diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs index dc043af2..31cedfc9 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs @@ -17,6 +17,52 @@ public sealed class SyntheticM68040ThrowawayTests(ITestOutputHelper output) [Fact, Trait("Suite", "Synthetic")] public void ChainedThrowawaysSelectEveryStackBeforeAccessContinuations() => Audit(true); + [Fact, Trait("Suite", "Synthetic")] + public void MixedEpochTraceCanonicalScalar() => AuditMixedEpoch(false, false); + [Fact, Trait("Suite", "Synthetic")] + public void MixedEpochTraceCanonicalBatch() => AuditMixedEpoch(false, true); + [Fact, Trait("Suite", "Synthetic")] + public void MixedEpochTraceStructuralScalar() => AuditMixedEpoch(true, false); + [Fact, Trait("Suite", "Synthetic")] + public void MixedEpochTraceStructuralBatch() => AuditMixedEpoch(true, true); + + private readonly record struct Epochs(ushort Incoming, ushort First, ushort Second, string Matrix, uint Vbr); + private static readonly ushort[] TraceStates = [0, 0x8000, 0x4000]; + private static readonly string[] MixedBanks = ["user", "user-M", "ISP", "MSP"]; + + [Theory] + [InlineData("user", 0, 0, 0x0000)] + [InlineData("user", 0x8000, 31, 0x801f)] + [InlineData("user-M", 0, 0, 0x1000)] + [InlineData("user-M", 0x8000, 31, 0x901f)] + [InlineData("ISP", 0x4000, 0, 0x6000)] + [InlineData("MSP", 0x4000, 31, 0x701f)] + public void FixtureStatusHasIndependentReferenceExamples(string bank, int trace, int ccr, int expected) => + Assert.Equal((ushort)expected, Status(bank, (ushort)trace, ccr)); + + private void AuditMixedEpoch(bool structural, bool batch) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); + var matrix = structural ? "structure" : "canonical"; + var report = new CoverageBatch("68040", $"rte-mixed-epoch-{matrix}-{(batch ? "batch" : "scalar")}"); + foreach (var start in new[] { "ISP", "MSP" }) + foreach (var tail in MixedBanks) + foreach (var middle in structural ? new[] { "none", "user", "user-M", "ISP", "MSP" } : ["none"]) + foreach (var result in MixedBanks) + foreach (var incoming in TraceStates) + foreach (var first in TraceStates) + foreach (var second in middle == "none" ? new ushort[] { 0 } : TraceStates) + foreach (var trace in TraceStates) + foreach (var alignment in structural ? new uint[] { 0, 1 } : [0]) + foreach (var vbr in structural ? new uint[] { 0, 0x10000 } : [0x10000]) + foreach (var form in new[] { "format0", "format2", "format3", "normal", "CM", "CT", "CU", + "CP49", "CP50", "CP51", "CP52", "CP53", "CP54", "CP55" }) + foreach (var ccr in structural ? new[] { 0, 31 } : Enumerable.Range(0, 32)) + Run(m, report, start, tail, middle, result, trace, alignment, form, ccr, + !form.StartsWith("format", StringComparison.Ordinal), new Epochs(incoming, first, second, matrix, vbr), batch); + report.Complete(output); + } + private void Audit(bool access) { var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); @@ -36,7 +82,7 @@ private void Audit(bool access) } private static void Run(SyntheticMachine m, CoverageBatch report, string start, string tail, string middle, - string result, ushort trace, uint alignment, string form, int ccr, bool access) + string result, ushort trace, uint alignment, string form, int ccr, bool access, Epochs? epochs = null, bool batch = false) { m.Reset(ccr); var pointers = new Dictionary { ["user"] = 0x7800 + alignment, ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; @@ -45,14 +91,16 @@ private static void Run(SyntheticMachine m, CoverageBatch report, string start, var path = middle == "none" ? new[] { start, tail } : new[] { start, middle, tail }; for (var n = 0; n < path.Length - 1; n++) { - var at = pointers[path[n]]; + var at = pointers[PhysicalBank(path[n])]; // The discarded PC is odd and must never become an instruction fetch. - m.InitializePhysical(at, Status(path[n + 1], trace == 0x8000 ? (ushort)0x4000 : (ushort)0x8000, ccr), 2); + var throwawayTrace = epochs is { } mixed ? n == 0 ? mixed.First : mixed.Second + : trace == 0x8000 ? (ushort)0x4000 : (ushort)0x8000; + m.InitializePhysical(at, Status(path[n + 1], throwawayTrace, ccr), 2); m.InitializePhysical(at + 2, 0xdead0001u + (uint)n * 2, 4); m.InitializePhysical(at + 6, 0x1024, 2); - pointers[path[n]] += 8; + pointers[PhysicalBank(path[n])] += 8; } - var frame = pointers[tail]; + var frame = pointers[PhysicalBank(tail)]; var savedSr = Status(result, trace, ccr ^ 31); var format = access ? 7 : form == "format0" ? 0 : form == "format2" ? 2 : 3; var vector = form == "CT" ? 9 : form == "CU" ? 11 : form.StartsWith("CP") ? int.Parse(form[2..]) : 0; @@ -77,17 +125,31 @@ private static void Run(SyntheticMachine m, CoverageBatch report, string start, } else m.InitializePhysical(Target, 0x60fe, 2); m.InitializePhysical(Handler, 0x4e73, 2); m.InitializePhysical(Handler + 2, 0x4e71, 2); - const uint vbr = 0x10000; + var vbr = epochs?.Vbr ?? 0x10000; foreach (var v in new[] { 9, 11, 49, 50, 51, 52, 53, 54, 55 }) m.InitializePhysical(vbr + (uint)v * 4, Handler, 4); _ = SyntheticExecution.Prepare(m, [0x4e73]); m.Core.State.SetUserStackPointer(0x7800 + alignment); m.Core.State.SetInterruptStackPointer(0x4700 + alignment); m.Core.State.SetMasterStackPointer(0x7400 + alignment); - m.Core.State.StatusRegister = Status(start, 0, ccr); + m.Core.State.StatusRegister = Status(start, epochs?.Incoming ?? 0, ccr); m.Core.State.VectorBaseRegister = vbr; if (continuation == 0x8000) m.Core.State.M68040PendingFpuExceptions.Begin(3, vector, Target); if (continuation == 0x4000) m.Core.State.M68040PendingFpuExceptions.Begin(2, 11, Target); + if (epochs != null) + { + // Context inputs intentionally disagree with the original event; + // no FPU instruction or arithmetic is executed by these fixtures. + m.Core.State.M68040Fpu.Fpcr = 0; + m.Core.State.M68040Fpu.Fpsr = 0x08008198; + m.Core.State.M68040Fpu.Fpiar = 0x1234abcd; + } var e = ArchitecturalExpectation.Capture(m); + if (epochs != null) + { + e.ControlChecks["FPCR preserved"] = (s => s.M68040Fpu.Fpcr, 0); + e.ControlChecks["FPSR preserved"] = (s => s.M68040Fpu.Fpsr, 0x08008198); + e.ControlChecks["FPIAR preserved"] = (s => s.M68040Fpu.Fpiar, 0x1234abcd); + } e.OperandAccessAddresses.UnionWith(new uint[] { 0x3218, 0x3220, 0x3228 }); e.ExpectedOperandTransfers = []; foreach (var pc in new uint[] { 0xdead0001, 0xdead0003 }) e.ForbiddenOperandReads.Add(pc); @@ -97,32 +159,62 @@ private static void Run(SyntheticMachine m, CoverageBatch report, string start, e.ControlChecks["pending delivery consumed"] = (s => s.M68040PendingFpuExceptions.Find(vector == 11 ? 2 : 3) == null ? 0u : 1u, 0); var sequence = m.Core.State.ExceptionSequence; e.ControlChecks["exception entries"] = (s => s.ExceptionSequence, sequence); - pointers[tail] += access ? 60u : format == 0 ? 8u : 12u; + pointers[PhysicalBank(tail)] += access ? 60u : format == 0 ? 8u : 12u; SetStacks(e, pointers, savedSr); e.Pc = Target; var id = $"68040/RTE/throwaway/{form}/start={start}/middle={middle}/tail={tail}/result={result}/T={trace:X4}/align={alignment}/op=4E73/ccr={ccr:X2}"; - if (vector != 0) ExpectException(m, e, pointers, savedSr, vector, Target, Operand, sequence + 1); - if (!SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/RTE")) + if (epochs is { } epoch) + id = $"68040/RTE/mixed-epoch/{epoch.Matrix}/{form}/start={start}/middle={middle}/tail={tail}/result={result}/incoming={epoch.Incoming:X4}/first={epoch.First:X4}/second={(middle == "none" ? "none" : epoch.Second.ToString("X4"))}/T={trace:X4}/align={alignment}/VBR={vbr:X8}/op=4E73/ccr={ccr:X2}"; + // MC68040UM 8.2.6: the incoming instruction trace condition is latched + // before any throwaway installs SR. Pending delivery wins (8.3/8.4.6.7). + var rteTrace = vector == 0 && epochs is { Incoming: not 0 }; + var phases = new List { "RTE" }; + if (vector != 0 || rteTrace) phases.Add("handler-return"); + phases.Add("following"); + if (epochs != null && form == "CM") + { + if (trace == 0x8000) phases.Add("movem-trace-return"); + phases.Add("following-BRA"); + } + var phase = 0; + bool Step() { - if (vector != 0) report.Record(id + "/handler-return", "untested", "RTE prerequisite failed"); - report.Record(id + "/following", "untested", "RTE prerequisite failed"); return; + var current = phases[phase++]; + if (ExecuteStep(m, e, report, id + "/" + current, batch)) return true; + foreach (var remaining in phases.Skip(phase)) + report.Record(id + "/" + remaining, "untested", "Earlier mixed-epoch/throwaway phase failed"); + return false; } - if (vector != 0) + if (vector != 0) ExpectException(m, e, pointers, savedSr, vector, Target, Operand, sequence + 1); + else if (rteTrace) ExpectException(m, e, pointers, savedSr, 9, Target, SyntheticMachine.Code, sequence + 1); + if (!Step()) return; + if (vector != 0 || rteTrace) { pointers[ExceptionBank(savedSr)] += 12; SetStacks(e, pointers, savedSr); e.Pc = Target; e.ExceptionVector = null; - if (!SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/handler-return")) - { report.Record(id + "/following", "untested", "Handler return prerequisite failed"); return; } + if (!Step()) return; } e.Pc = form == "CM" ? Target + 4 : Target; if (form == "CM") { e.D[0] = 0x89abcdef; e.D[1] = 0x12345678; } if (trace == 0x8000 || trace == 0x4000 && form != "CM") - ExpectException(m, e, pointers, savedSr, 9, e.Pc, Target, sequence + (vector != 0 ? 2u : 1u)); - SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/following"); + ExpectException(m, e, pointers, savedSr, 9, e.Pc, Target, sequence + (vector != 0 || rteTrace ? 2u : 1u)); + if (!Step() || epochs == null || form != "CM") return; + var exceptions = sequence + (rteTrace ? 1u : 0u); + if (trace == 0x8000) + { + exceptions++; + pointers[ExceptionBank(savedSr)] += 12; + SetStacks(e, pointers, savedSr); e.Pc = Target + 4; e.ExceptionVector = null; + if (!Step()) return; + } + e.Pc = Target + 4; + if (trace != 0) ExpectException(m, e, pointers, savedSr, 9, Target + 4, Target + 4, exceptions + 1); + Step(); } private static ushort Status(string bank, ushort trace, int ccr) => - (ushort)((bank == "ISP" ? 0x2000 : bank == "MSP" ? 0x3000 : 0) | trace | ccr); - private static string ExceptionBank(ushort sr) => (sr & 0x3000) == 0x3000 ? "MSP" : "ISP"; + (ushort)((bank == "ISP" ? 0x2000 : bank == "MSP" ? 0x3000 : bank == "user-M" ? 0x1000 : 0) | trace | ccr); + private static string PhysicalBank(string bank) => bank == "user-M" ? "user" : bank; + private static string ExceptionBank(ushort sr) => (sr & 0x1000) != 0 ? "MSP" : "ISP"; private static void SetStacks(ArchitecturalExpectation e, Dictionary pointers, ushort sr) { e.Sr = sr; @@ -147,4 +239,26 @@ private static void ExpectException(SyntheticMachine m, ArchitecturalExpectation e.Write(stack + 6, (uint)((vector >= 49 ? 3 : 2) << 12 | vector * 4), 2, m.Model); e.Write(stack + 8, address, 4, m.Model); } + + private static bool ExecuteStep(SyntheticMachine m, ArchitecturalExpectation e, CoverageBatch report, string id, bool batch) + { + if (!batch) return SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id); + try + { + var boundary = new Boundary(); + var count = ((IM68kBatchCore)m.Core).ExecuteInstructions(1, m.Core.State.Cycles + 1000, boundary); + var mismatch = count != 1 || boundary.Before != 1 || boundary.After != 1 + ? $"Batch count/callbacks differ: {count}/{boundary.Before}/{boundary.After}" : e.Verify(m); + report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); + return mismatch == null; + } + catch (NotSupportedException ex) { report.Record(id, "unsupported", ex.Message); return false; } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); return false; } + } + private sealed class Boundary : IM68kInstructionBoundary + { + public int Before, After; + public bool BeforeInstruction() { Before++; return true; } + public void AfterInstruction(long previousCycle, long currentCycle) => After++; + } } diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 6938297d..cb9e3c7f 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -5317,3 +5317,58 @@ identities still match after reference and integrity checks. Milestone 6 remains **in progress**; mixed-epoch provenance, internal restoration and all retained model/reference/consolidation scope remain required. No CPU production fix, package release, consumer rerun or old-test retirement is included. + +### 040 successful mixed-epoch throwaway chains — focused qualification + +The four `SyntheticM68040ThrowawayTests.MixedEpochTrace*` groups independently +select incoming, first/second throwaway and final restored trace states 0/T1/T0. +Expectations compose MC68040UM 8.2.6, 8.3, 8.4.2 and 8.4.6.7; no combined +hardware capture or FPU arithmetic oracle is claimed. User M=1 and aliases of +USP with distinct M values are explicit. Pending CT/CU/CP49–55 retains priority +and the original vector despite conflicting FPU registers. The CM route checks +the saved EA through an RTE-completion trace, MOVEM, its optional trace return +and a following BRA. Registers, all stack banks, guarded memory, discarded PCs, +saved PC/SR and exception sequence are checked at each scalar/batch phase. + +| Group, each scalar/batch | Phases | Combinations | +| --- | ---: | ---: | +| Canonical one-throwaway, every CCR | 1,152,000 | 12,096 | +| Structural one/two-throwaway, CCR 0/31 | 3,744,000 | 628,992 | + +After mutation restoration, fresh focused Release execution passes ten tests +with zero skips: these four complete reports and six independent fixed SR +encoding examples. All 9,792,000 phases pass with zero mismatches, unsupported +execution or untested phases. Every combination and its weight is checked +against the maintained command's independent literal PowerShell enumeration. +The original throwaway groups retain their identifiers and 82,944 / 428,544 +phases; both passed the initial focused selection. + +Four complete `MixedEpochRte` mutation probes detect intended restored-bits, +T0, MOVEM-lifetime and intermediate-SR defects in every report. Both structural +reports additionally retain the second-throwaway SR diagnostic. The T1-leak +probe targets trace-cleared final SR to keep both one/two-throwaway diagnostics +within bounded failure recording; the initial broader probe exhausted that +recording and was rejected, not credited. The rerun qualifies all four probes. +Every production source byte matches the preceding qualified manifest afterward. + +Frozen build starts from `53251330d7cb04fe7cb0e5d970da30002dbadfc5` plus these +test changes; production tree remains `795fd12d6c0237a22a5f92f4a96cc4823e0364c1`. +CPU SHA256: `1061e146cbf1507df3fb83ce9aa3fdb9157234f4e470bb172ff396cef79be710`. +Adapter SHA256: `592ffd91de0848bc6e49b13f87734eaec66fefc7c5c938483761634573fd48f3`. +`artifacts/m6-mixed-epoch-restored/qualification.json` records all seven current +fixture/command/assembly identities. These identities are not relabeled after +commit. The fresh inventory selection executes one failing test, with no skips, +retaining exactly 480 untested cases / fifteen combinations. + +The expanded complete 040 command requires 83 tests, 70 reports, thirteen fixed +examples and fourteen input identities: 57,905,824 phases including that +480-case inventory. Ordinary reporting requires 72,874,006 cases / 660 batches. +Those are expanded selection requirements, not a fresh complete-suite result. +The preceding full-suite, complete 040, external-reference and report-integrity +qualification remains separate historical evidence; it was not rerun or +relabeled for this test-only checkpoint. + +Mixed epochs during validation faults, repair/retry and internal restoration +remain required, alongside every retained data/context/model/reference and +consolidation requirement. Milestone 6 stays **in progress**. No production +CPU fix, package release, consumer rerun or regression retirement is included. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 533009e3..0ae7f21f 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2024,3 +2024,45 @@ assembly identities still match after all checks. This test-only checkpoint is qualified. Mixed-epoch provenance, internal restoration, all earlier model/reference/consolidation requirements and the complete roadmap remain required; milestone 6 stays **in progress**. + +### Milestone 6 successful mixed-epoch throwaway chains — focused qualification + +Four `SyntheticM68040ThrowawayTests.MixedEpochTrace*` groups independently vary +the instruction's incoming trace, the first and (where present) second +throwaway trace, and the final restored trace. Each uses states 0/T1/T0. +Short formats 0/2/3 and format-7 normal/CM/CT/CU/CP49–55 cover every initial +ISP/MSP, intermediate/tail user/user-M/ISP/MSP and restored bank. Canonical +one-throwaway cases use every CCR; structural cases use CCR 0/31, one/two +throwaways, both alignments and VBRs, including aliases of USP with different +user M values. Scalar and one-instruction batch routes check every phase, +registers/stack banks, guarded memory and exception provenance. The CM program +returns a MOVEM trace and executes BRA, separating the incoming RTE trace from +restored-state instruction tracing and saved-EA lifetime. Pending vectors retain +the original event despite conflicting FPU registers; no FPU opcode is executed. + +After mutation restoration, fresh focused Release execution passes ten tests +without skips: four complete reports plus six independent SR encoding examples. +Canonical reports each pass 1,152,000 phases / 12,096 combinations; structural +reports each pass 3,744,000 phases / 628,992 combinations. All 9,792,000 phases +pass without mismatches, unsupported execution or untested phases. Independent +command enumeration checks every combination and weight. All four complete +mutation probes detect their intended semantic defect, including second-throwaway +SR provenance in both structural reports. Exact production bytes are restored. +The original two throwaway reports retain their identifiers/cardinalities and +passed the initial focused selection. + +The complete 040 command now requires 83 tests, 70 reports, thirteen fixed +examples and 57,905,824 phases including the retained 480-case inventory. +A fresh inventory selection still fails its one executed test with exactly +480 untested cases / fifteen combinations and no skips. Expanded ordinary +coverage requires 72,874,006 cases / 660 batches. These are complete-selection +requirements; prior broader qualification is retained as separate evidence, +not represented as a freshly rerun complete suite. Frozen source/binary +identities and scope are recorded in `COPPER68K_REFERENCE_QUALIFICATION.md`. + +Expectations compose MC68040UM 8.2.6, 8.3, 8.4.2 and 8.4.6.7. Successful chains +do not qualify mixed epochs during validation faults, repair/retry, internal +restoration or chained odd-PC saved-SR provenance. All earlier data/restart, +context-transfer, model/reference/consolidation requirements remain required. +Milestone 6 stays **in progress**, with `roadmapComplete=false`. This is a +test-only checkpoint; no package release or old-test retirement is included. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index ceb742e9..e3e072af 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -17,6 +17,10 @@ $expected = [ordered]@{ 'rte-access-fpu-post' = @{cases=96768; combinations=1008} 'rte-throwaway-controls' = @{cases=82944; combinations=1296} 'rte-throwaway-access' = @{cases=428544; combinations=4752} + 'rte-mixed-epoch-canonical-scalar' = @{cases=1152000; combinations=12096} + 'rte-mixed-epoch-canonical-batch' = @{cases=1152000; combinations=12096} + 'rte-mixed-epoch-structure-scalar' = @{cases=3744000; combinations=628992} + 'rte-mixed-epoch-structure-batch' = @{cases=3744000; combinations=628992} 'rte-odd-normal' = @{cases=69120; combinations=720} 'rte-odd-pending' = @{cases=165888; combinations=1296} 'address-error-fetch-040' = @{cases=2304; combinations=72} @@ -109,9 +113,9 @@ if (-not $ValidateReportsOnly) { 'Multiple continuation bits are architecturally undefined and excluded', 'Selected active accurate-batch paths verify counts/callbacks and scalar/batch bus/cycle policy; generic short operand frames do not qualify architectural format-7 data restart', 'Instruction-fault fixtures use cache-disabled accurate execution and physical-map rejection; speculative deferral, enabled caches/MMU and compiled fetch PC provenance remain unqualified', - 'Legacy repair clears saved trace; separate supervisor and user-tail bridge groups preserve incoming trace, check normal/CM completion and pending CT/CU/CP49-55 priority. Executed integer trace service has separate supervisor/user-tail coverage; mixed-epoch trace protocols remain required', + 'Legacy repair clears saved trace; separate supervisor and user-tail bridge groups preserve incoming trace, check normal/CM completion and pending CT/CU/CP49-55 priority. Executed integer trace service and successful mixed-epoch throwaway chains have separate coverage; mixed epochs during faults, repair/retry and internal restoration remain required', 'User-tail fault expectations compose documented throwaway live-SR rules with general supervisor exception entry; unusual combined hardware behavior has not been observed', - 'Mixed-epoch trace provenance, internal-restoration double faults, cache/MMU/compiled handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', + 'Mixed epochs during validation faults and repair/retry, internal-restoration double faults, cache/MMU/compiled handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', 'Direct odd-RTE saved-SR ordering uses documentary WinUAE 5d22d336, not an executed hardware oracle') } $identity | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'identities.json') @@ -139,8 +143,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 73 -or [int]$counters.total -ne 73 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (66 batches, 7 fixed examples)' +if ([int]$counters.executed -ne 83 -or [int]$counters.total -ne 83 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (70 batches, 13 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -437,6 +441,24 @@ foreach ($group in $expected.Keys) { } } } } } } } } + } elseif ($group.StartsWith('rte-mixed-epoch-', [StringComparison]::Ordinal)) { + $structure = $group.Contains('-structure-', [StringComparison]::Ordinal) + foreach ($start in @('ISP','MSP')) { foreach ($tail in @('user','user-M','ISP','MSP')) { + foreach ($middle in $(if ($structure) {@('none','user','user-M','ISP','MSP')} else {@('none')})) { + foreach ($result in @('user','user-M','ISP','MSP')) { foreach ($incoming in @(0,0x8000,0x4000)) { + foreach ($first in @(0,0x8000,0x4000)) { foreach ($second in $(if ($middle -eq 'none') {@(0)} else {@(0,0x8000,0x4000)})) { + foreach ($trace in @(0,0x8000,0x4000)) { foreach ($alignment in $(if ($structure) {@(0,1)} else {@(0)})) { + foreach ($vbr in $(if ($structure) {@(0,0x10000)} else {@(0x10000)})) { + foreach ($form in @('format0','format2','format3','normal','CM','CT','CU','CP49','CP50','CP51','CP52','CP53','CP54','CP55')) { + $secondName = if ($middle -eq 'none') {'none'} else {'{0:X4}' -f $second} + $key = '68040/RTE/mixed-epoch/{0}/{1}/start={2}/middle={3}/tail={4}/result={5}/incoming={6:X4}/first={7:X4}/second={8}/T={9:X4}/align={10}/VBR={11:X8}' -f $(if ($structure) {'structure'} else {'canonical'}),$form,$start,$middle,$tail,$result,$incoming,$first,$secondName,$trace,$alignment,$vbr + # Literal executed program: RTE, optional exception return, following + # operation; CM additionally returns its T1 trace and executes BRA. + $phases = 2 + if ($incoming -ne 0 -or $form -in @('CT','CU') -or $form.StartsWith('CP')) {$phases++} + if ($form -eq 'CM') {$phases++; if ($trace -eq 0x8000) {$phases++}} + $expectedCombinations[$key] = $(if ($structure) {2} else {32}) * $phases + } } } } } } } } } } } } elseif ($group -in @('rte-throwaway-controls','rte-throwaway-access')) { $forms = if ($group -eq 'rte-throwaway-controls') { @('format0','format2','format3') } else { @('normal','CM','CT','CU','CP49','CP50','CP51','CP52','CP53','CP54','CP55') } foreach ($start in @('ISP','MSP')) { foreach ($tail in @('user','ISP','MSP')) { foreach ($middle in @('none','user','ISP','MSP')) { @@ -520,8 +542,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 73 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 83 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=48113824; xunitBatches=66; fixedExamples=7; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=57905824; xunitBatches=70; fixedExamples=13; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index ff79618b..30a19f25 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','RteSoftwareTrace','UserRteFault','UserRteTrace','UserRteSoftwareTrace','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','RteSoftwareTrace','UserRteFault','UserRteTrace','UserRteSoftwareTrace','MixedEpochRte','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -341,6 +341,27 @@ foreach ($mutation in $retryTraceMutations) { $mutation.group='RepairWithUntouchedIncomingTrace'; $mutation.model='68040'; $mutation.milestone=6 $mutation.retryTrace=$true; $mutations += $mutation } +$mixedEpochMutations = @() +foreach ($template in $retryTraceMutations) { + $mutation = $template.Clone() + $mutation.Remove('retryTrace') + $mutation.name = $template.name.Replace('040-retry-trace-', '040-mixed-epoch-') + $mutation.proofCombination = if ($template.name -like '*restored-bits') {'/incoming=8000/first=0000/second=none/T=0000/'} + elseif ($template.name -like '*t0-rte') {'/incoming=4000/first=0000/second=none/T=0000/'} else {'/CM/'} + $mutation.proofPhase = if ($template.proofPhase -eq '/retry-RTE') {'/RTE'} else {$template.proofPhase} + $mixedEpochMutations += $mutation +} +$mixedEpochMutations += @{name='040-mixed-epoch-intermediate-final-sr'; file=$advanced; + before=' State.StatusRegister = restoredStatus;'; + # Isolate a leaked intermediate T1 into a trace-cleared final SR. Mutating + # every trace difference exhausts bounded diagnostics before two throwaways. + after=' State.StatusRegister = format == 1 || (priorSr & 0xc000) != 0x8000 || (restoredStatus & 0xc000) != 0 ? restoredStatus : (ushort)(restoredStatus | 0x8000);'; + proofCombination='/incoming=0000/first=8000/second=none/T=0000/'; proofPhase='/RTE'; proofReason='SR expected'; + proofSecondCombination='/middle=user/tail=user/result=user/incoming=0000/first=0000/second=8000/T=0000/'} +foreach ($mutation in $mixedEpochMutations) { + $mutation.group='MixedEpochTrace'; $mutation.model='68040'; $mutation.milestone=6 + $mutation.mixedEpoch=$true; $mutations += $mutation +} $pendingTraceMutations = @( @{name='040-pending-trace-extra-rte-trace'; file=$advanced; before=' if (exception && (_profile.Model is M68kAcceleratorModel.M68040 or M68kAcceleratorModel.M68060 ||'; @@ -530,6 +551,7 @@ if ($Scope -eq 'EntryPrefetch') { $mutations = @($mutations | Where-Object { $_. if ($Scope -eq 'UserRteFault') { $mutations = @($mutations | Where-Object { $_.userRte }) } if ($Scope -eq 'UserRteTrace') { $mutations = @($mutations | Where-Object { $_.userTrace }) } if ($Scope -eq 'UserRteSoftwareTrace') { $mutations = @($mutations | Where-Object { $_.userSoftwareTrace }) } +if ($Scope -eq 'MixedEpochRte') { $mutations = @($mutations | Where-Object { $_.mixedEpoch }) } if ($Scope -eq 'AccessDoubleFault') { $mutations = @($mutations | Where-Object { $_.doubleFault }) } if ($Scope -eq 'BatchFault') { $mutations = @($mutations | Where-Object { $_.batchFault }) } if ($Scope -eq 'LowPowerStop') { $mutations = @($mutations | Where-Object { $_.lpstop }) } @@ -642,6 +664,27 @@ try { throw 'Instruction-fault mutation omitted complete cases or intended semantic failure' } } + if ($mutation.mixedEpoch) { + [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + if ($trx.TestRun.ResultSummary.Counters.executed -ne 4 -or $trx.TestRun.ResultSummary.Counters.failed -ne 4 -or + $trx.TestRun.ResultSummary.Counters.notExecuted -ne 0 -or $batches.Count -ne 4) { + throw 'Mixed-epoch mutation omitted complete four-batch execution' + } + foreach ($matrix in @('canonical','structure')) { foreach ($route in @('scalar','batch')) { + $target = @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq "rte-mixed-epoch-$matrix-$route" }) + $cases = if ($matrix -eq 'canonical') {1152000} else {3744000} + if ($target.Count -ne 1 -or $target[0].logicalCases -ne $cases -or $target[0].counts.unsupported -ne 0 -or + @($target[0].failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and + $_.id.EndsWith($mutation.proofPhase, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) }).Count -eq 0) { + throw 'Mixed-epoch mutation omitted complete cases or intended semantic diagnostic' + } + if ($matrix -eq 'structure' -and $mutation.proofSecondCombination -and + @($target[0].failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofSecondCombination, [StringComparison]::Ordinal) -and + $_.id.EndsWith($mutation.proofPhase, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) }).Count -eq 0) { + throw 'Mixed-epoch mutation omitted its second-throwaway provenance diagnostic' + } + } } + } if ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors -or $mutation.softwareTrace -or $mutation.userTrace -or $mutation.userSoftwareTrace) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if ($trx.TestRun.ResultSummary.Counters.executed -ne 4 -or $trx.TestRun.ResultSummary.Counters.failed -ne 4 -or $batches.Count -ne 4) { @@ -722,7 +765,7 @@ try { } elseif ($mutation.entryPrefetch) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] - } elseif ($mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors -or $mutation.softwareTrace -or $mutation.userTrace -or $mutation.userSoftwareTrace) { + } elseif ($mutation.mixedEpoch -or $mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors -or $mutation.softwareTrace -or $mutation.userTrace -or $mutation.userSoftwareTrace) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.id.EndsWith($mutation.proofPhase, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] } elseif ($mutation.userRte) { diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index a296e9ee..53f8b88b 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -186,6 +186,10 @@ try { $expected['address-error-fetch-040'] = 2304 $expected['rte-throwaway-controls'] = 82944 $expected['rte-throwaway-access'] = 428544 + $expected['rte-mixed-epoch-canonical-scalar'] = 1152000 + $expected['rte-mixed-epoch-canonical-batch'] = 1152000 + $expected['rte-mixed-epoch-structure-scalar'] = 3744000 + $expected['rte-mixed-epoch-structure-batch'] = 3744000 $expected['rte-access-fpu-unimplemented'] = 13824 $expected['rte-access-fpu-post'] = 96768 $expected['rte-access-controls'] = 6912 @@ -312,6 +316,7 @@ try { m68040HandlerPrefetchScope=@{logicalCases=457728; batches=8; routes=@('accurate scalar','accurate batch'); bus='Physical map with no host code reader, including stable-map generation'; qualified='Four-long access-error entry window, every entry byte, odd handler addresses, retained data, flow/host/task/map invalidation and later instruction faults; denied cold boundaries perform no CPU fetch/state change'; remaining='Other exception-entry paths, host-reader/compiled fetch provenance, enabled caches/MMU, ordinary speculative deferral and physical timing'}; m68040UserTailScope=@{logicalCases=1687040; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); authority='Composition of MC68040UM 2.2.2.1, 8.1, 8.2.1/5, 8.4.2 and 8.4.6.7; software qualification, no executed hardware reference'; userM=@(0,1); initialBanks=@('ISP','MSP'); middleBanks=@('none','user','ISP','MSP'); ccr='all 32 canonical SR reads, 0/31 structural reads'; forms='0/2/3, invalid 4/15, normal/CM/CT/CU/CP49'; qualified='Live intermediate SR, supervisor exception stack selected by M, user-data TM=1, preserved USP frame and committed throwaways, bare return followed by privilege violation, executed seven-store repair with fresh throwaway bridge and following trace/continuation'; remaining='Internal restoration faults, mixed-epoch trace provenance, physical bus-order/timing and hardware observation of composed user-tail fault behavior'}; m68040UserTailTraceScope=@{logicalCases=8752128; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); userM=@(0,1); initialBanks=@('ISP','MSP'); middleBanks=@('none','user','ISP','MSP'); restoredBanks=@('user','user-M','ISP','MSP'); canonicalIncoming=@(0,0x8000,0x4000); structuralIncoming=@(0x8000); restoredTrace=@(0,0x8000,0x4000); forms='0/2/3, invalid 4/15, normal/CM/CT/CU/CP49-55'; ccr='all 32 canonical, 0/31 every validation-read byte'; qualified='Seven real stores preserve incoming trace while adding S only to the access return; fresh supervisor throwaway restores user SR, original RTE completion traces independently of restored bits, CT/CU/CP priority suppresses extra trace; handler return, following flow/MOVEM and all stack banks checked'; remaining='mixed-epoch trace provenance, internal restoration and hardware/physical qualification'}; + m68040MixedEpochThrowawayScope=@{logicalCases=9792000; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); incoming=@(0,0x8000,0x4000); firstThrowaway=@(0,0x8000,0x4000); secondThrowaway=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); banks=@('user','user-M','ISP','MSP'); canonicalCcr='all 32'; structuralCcr=@(0,31); qualified='Successful one/two-throwaway chains independently vary instruction, first/second throwaway and restored trace; final SR selects exception bank, pending CT/CU/CP49-55 wins, CM survives an RTE completion trace and following MOVEM/trace return/BRA; all stack banks, guarded memory, original pending vectors and preserved FPU registers checked'; remaining='Mixed epochs during validation faults, handler repair/retry, internal restoration and chained odd-PC saved-SR provenance; data restart/context transfer and broader qualification remain required'; outsideRoadmap='FPU arithmetic, enabled-MMU operation and physical timing'}; m68040UserTailSoftwareTraceScope=@{logicalCases=15423488; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); userM=@(0,1); initialBanks=@('ISP','MSP'); middleBanks=@('none','user','ISP','MSP'); restoredBanks=@('user','user-M','ISP','MSP'); forms=@('CU-linear','CU-flow','CP49','CP50','CP51','CP52','CP53','CP54','CP55'); canonicalIncoming=@(0,0x8000,0x4000); structuralIncoming=@(0x8000); restoredTrace=@(0,0x8000,0x4000); ccr='all 32 canonical, 0/31 every validation-read byte'; qualified='Seven real user-tail repair stores retain trace; pending CU/CP preserves original event; integer handler tests repaired saved T1/T0, adjusts supplied CU completion PC, converts frame, reads vector 9, calls via stack/RTS, writes marker once when eligible, optionally clears saved trace and really returns; following BRA checks resumed hardware tracing without new hardware exception provenance during software service'; remaining='Mixed-epoch provenance, internal restoration, CP context transfer and hardware/physical qualification'; outsideRoadmap='FPU arithmetic and enabled-MMU operation'}; m68040RteCpVectorsScope=@{logicalCases=4866048; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); vectors=@(50,51,52,53,54,55); restoredBanks=@('user','user-M','ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); qualified='Original CP vector survives validation faults, three repair stores and incoming trace; format-3 saved SR/PC/EA, pending-context consumption, all stack banks, FPU register preservation, handler return and following traced BRA checked'; ccr='all 32 canonical, 0/31 every validation-read byte'; remaining='mixed-epoch provenance, context transfer and internal restoration'}; m68040RteSoftwareTraceScope=@{logicalCases=8232960; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); forms=@('CU-linear','CU-flow','CP49','CP50','CP51','CP52','CP53','CP54','CP55'); restoredBanks=@('user','user-M','ISP','MSP'); canonicalIncoming=@(0,0x8000,0x4000); structuralIncoming=@(0x8000); restoredTrace=@(0,0x8000,0x4000); ccr='all 32 canonical, 0/31 every validation-read byte'; qualified='Executed integer handler inspects saved T1/T0, advances emulated CU PC, converts format/address, reads vector 9 and directly calls software trace service; trace handler preserves or clears saved trace and really returns; hardware exception counter does not change for software calls'; remaining='Actual FPU emulation and hardware execution remain outside this integer fixture; mixed-epoch provenance, internal restoration and hardware/physical qualification'}; From 78d6dceb5d3577f5f6cc9ca7a0c268916f7d507d Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 11:40:17 +0300 Subject: [PATCH 058/211] Qualify mixed-epoch 68040 RTE validation faults --- Copper68k.Tests/Synthetic/README.md | 28 ++++ .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- .../SyntheticM68040RteValidationFaultTests.cs | 138 ++++++++++++++---- .../SyntheticM68040ThrowawayTests.cs | 40 +++-- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 54 +++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 42 ++++++ scripts/test-copper68k-040-access-frames.ps1 | 40 ++++- .../test-copper68k-synthetic-mutations.ps1 | 59 +++++++- scripts/test-copper68k-synthetic.ps1 | 7 +- 9 files changed, 360 insertions(+), 50 deletions(-) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index f9c937f8..3308b1c4 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -778,3 +778,31 @@ all four reports and six independent SR encoding examples, with zero skips. The separate current inventory still fails its 480 required cases. Previous broader qualification remains a separate checkpoint. Mixed epochs during validation faults, repair/retry and internal restoration remain required. + +### Mixed-epoch validation fault capture + +`SyntheticM68040RteValidationFaultTests.MixedEpochFaultCapture*` crosses +independent incoming and committed throwaway trace states, all stack banks and +user M values. Canonical cases cross final trace/CCR values at the PC read; +structural cases reject every validation-transfer byte in one/two-throwaway +chains, including aliases. Each phase checks the last committed SR, saved PC, +registers, guarded memory, stack banks, exact rejected access and retained +pending CP vector. Bare handler return and user-tail privilege failure are +executed; supervisor retry and original trace resumption remain unqualified. + +Canonical scalar/batch reports each require 276,480 phases / 3,456 combinations; +structural reports each require 3,556,800 phases / 711,360 combinations. The +complete 040 selection now requires 87 tests / 74 reports / thirteen fixed +examples, including its retained 480-case failing inventory. Expectations +compose MC68040UM exception and RTE rules; no combined hardware capture is +claimed. See the qualification record for the executed validation scope. + +Use `--filter FullyQualifiedName~MixedEpochFaultCapture` for focused execution +and `./scripts/test-copper68k-synthetic-mutations.ps1 -Scope MixedEpochFault` +for lost live T1, premature CCR installation and skipped aliased-USP fixture +probes. CPU probes must fail all four complete groups with the intended saved-SR +diagnostic; the fixture probe must preserve both canonical groups and fail both +structural groups. The lost-T1 probe additionally requires second-throwaway +trace diagnostics in both structural reports. Shared test-only stack/status +fixtures retain independent fixed SR examples. Failed initial fixture evidence +is not qualification evidence. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index a64dcb43..8c1b67c8 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -33,7 +33,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() "real-access-fault-entry", "writeback-handler", "CP-context-transferred-vector" }) report.Record($"68040/RTE/format7/{form}/bank={bank}/op=4E73/ccr={ccr:X2}", "untested", form == "frame-validation-fault" - ? "Validation, preserved-trace repair/software service and successful mixed-epoch throwaway chains have separate software coverage; mixed epochs during validation faults, repair/retry and internal restoration remain required. Physical/hardware behavior is separately unqualified" + ? "Validation, preserved-trace repair/software service, successful mixed-epoch chains and mixed-epoch validation fault capture have separate software coverage; mixed-epoch repair/retry, original trace suspension/resumption and internal restoration remain required. Physical/hardware behavior is separately unqualified" : form == "real-access-fault-entry" ? "Cache-disabled accurate instruction-fetch frames/restart are covered separately; data fault/writeback restart, enabled-cache deferral and compiled instruction-PC provenance remain required" : "Required fault/context protocol has no independently qualified execution fixture yet"); diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs index c9093afd..0420cd7b 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs @@ -8,7 +8,7 @@ namespace Copper68k.Tests.Synthetic; // This does not inject a production exception or calculate an EA with CPU code. public sealed class SyntheticM68040RteValidationFaultTests(ITestOutputHelper output) { - private const uint Handler = 0x9020, Target = 0x6000; + private const uint Handler = 0x9020, Target = 0x6000, PrivilegeHandler = 0x9070; private static readonly string[] SupervisorBanks = ["ISP", "MSP"]; private static readonly string[] Forms = ["format0", "format2", "format3", "invalid4", "invalid15", "normal", "CM", "CT", "CU", "CP"]; @@ -19,6 +19,49 @@ public sealed class SyntheticM68040RteValidationFaultTests(ITestOutputHelper out [Fact, Trait("Suite", "Synthetic")] public void ValidationFaultsRetainCommittedThrowawaysAndStackSelection() => Audit(true); + [Fact, Trait("Suite", "Synthetic")] + public void MixedEpochFaultCaptureCanonicalScalar() => AuditMixed(false, false); + [Fact, Trait("Suite", "Synthetic")] + public void MixedEpochFaultCaptureCanonicalBatch() => AuditMixed(false, true); + [Fact, Trait("Suite", "Synthetic")] + public void MixedEpochFaultCaptureStructuralScalar() => AuditMixed(true, false); + [Fact, Trait("Suite", "Synthetic")] + public void MixedEpochFaultCaptureStructuralBatch() => AuditMixed(true, true); + + private readonly record struct Epochs(ushort Incoming, ushort First, ushort Second, ushort Final, string Matrix); + private static readonly ushort[] Traces = [0, 0x8000, 0x4000]; + private static readonly string[] Banks = ["user", "user-M", "ISP", "MSP"]; + + private void AuditMixed(bool structural, bool batch) + { + var bus = new ValidationFaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + var matrix = structural ? "structure" : "canonical"; + var report = new CoverageBatch("68040", $"rte-mixed-fault-{matrix}-{(batch ? "batch" : "scalar")}"); + foreach (var start in SupervisorBanks) + foreach (var tail in Banks) + foreach (var middle in structural ? new[] { "none", "user", "user-M", "ISP", "MSP" } : ["none"]) + foreach (var incoming in Traces) + foreach (var first in Traces) + foreach (var second in middle == "none" ? new ushort[] { 0 } : Traces) + // Canonical cases cross every final trace independently. Structural + // cases hold the uncommitted final SR at T1 and cross every read byte. + foreach (var final in structural ? new ushort[] { 0x8000 } : Traces) + foreach (var alignment in structural ? new uint[] { 0, 1 } : [0]) + foreach (var vbr in structural ? new uint[] { 0, 0x10000 } : [0x10000]) + foreach (var form in new[] { "format0", "format2", "format3", "invalid4", "invalid15", "normal", "CM", "CT", "CU", + "CP49", "CP50", "CP51", "CP52", "CP53", "CP54", "CP55" }) + foreach (var ccr in structural ? new[] { 0, 31 } : Enumerable.Range(0, 32)) + foreach (var (offset, width) in structural ? Reads(form) : [(2u, 4)]) + for (var faultByte = 0; faultByte < (structural ? width : 1); faultByte++) + { + var path = middle == "none" ? new[] { start, tail } : [start, middle, tail]; + Run(m, bus, report, path, ccr, incoming, alignment, vbr, form, offset, width, faultByte, + new Epochs(incoming, first, second, final, matrix), batch); + } + report.Complete(output); + } + private void Audit(bool chained) { var bus = new ValidationFaultBus(); @@ -55,44 +98,62 @@ private void Audit(bool chained) } private static void Run(SyntheticMachine m, ValidationFaultBus bus, CoverageBatch report, - string[] path, int ccr, ushort trace, uint alignment, uint vbr, string form, uint offset, int width, int faultByte) + string[] path, int ccr, ushort trace, uint alignment, uint vbr, string form, uint offset, int width, int faultByte, + Epochs? epochs = null, bool batch = false) { bus.Disarm(); m.Reset(ccr); - var pointers = new Dictionary { ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; - foreach (var at in pointers.Values) + var pointers = new Dictionary { ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment, + ["user"] = 0x7800 + (epochs != null ? alignment : 0) }; + foreach (var at in epochs != null ? pointers.Values : pointers.Where(p => p.Key != "user").Select(p => p.Value)) for (var n = -76; n < 80; n++) m.InitializePhysical(unchecked(at + (uint)n), (uint)(n ^ 0x5a), 1); for (var n = 0; n < path.Length - 1; n++) { - var at = pointers[path[n]]; - m.InitializePhysical(at, Status(path[n + 1], trace, ccr ^ 31), 2); + var at = pointers[M68040StackFixture.PhysicalBank(path[n])]; + var throwawayTrace = epochs is { } ep ? n == 0 ? ep.First : ep.Second : trace; + m.InitializePhysical(at, Status(path[n + 1], throwawayTrace, ccr ^ 31), 2); m.InitializePhysical(at + 2, 0xdead0001u + (uint)n * 2, 4); m.InitializePhysical(at + 6, 0x1024, 2); - pointers[path[n]] += 8; + pointers[M68040StackFixture.PhysicalBank(path[n])] += 8; } - var tail = path[^1]; var frame = pointers[tail]; + var tail = path[^1]; var frame = pointers[M68040StackFixture.PhysicalBank(tail)]; var format = form.StartsWith("format") ? int.Parse(form[6..]) : form.StartsWith("invalid") ? int.Parse(form[7..]) : 7; - var continuation = form switch { "CM" => 0x1000, "CT" => 0x2000, "CU" => 0x4000, "CP" => 0x8000, _ => 0 }; + var post = form.StartsWith("CP", StringComparison.Ordinal); + var vector = post ? form == "CP" ? 49 : int.Parse(form[2..]) : 11; + var continuation = post ? 0x8000 : form switch { "CM" => 0x1000, "CT" => 0x2000, "CU" => 0x4000, _ => 0 }; // Deliberately different S/M/CCR/trace bits: a fault must not install this SR. - m.InitializePhysical(frame, 0x801fu ^ (uint)ccr, 2); + m.InitializePhysical(frame, epochs is { } final ? Status("user", final.Final, ccr) : 0x801fu ^ (uint)ccr, 2); m.InitializePhysical(frame + 2, Target, 4); m.InitializePhysical(frame + 6, (uint)(format << 12) | 8, 2); m.InitializePhysical(frame + 8, 0x12345678, 4); m.InitializePhysical(frame + 12, (uint)continuation | 0x0105, 2); m.InitializePhysical(Handler, 0x4e73, 2); m.InitializePhysical(Handler + 2, 0x7001, 2); m.InitializePhysical(vbr + 8, Handler, 4); + if (epochs != null) { m.InitializePhysical(vbr + 32, PrivilegeHandler, 4); m.InitializePhysical(PrivilegeHandler, 0x7e7e, 2); } _ = SyntheticExecution.Prepare(m, [0x4e73]); m.Core.State.SetInterruptStackPointer(0x4700 + alignment); m.Core.State.SetMasterStackPointer(0x7400 + alignment); + // Start at the input base, not the pointer advanced while constructing + // aliased throwaways. The processor must consume every user frame. + if (epochs != null) m.Core.State.SetUserStackPointer(0x7800 + alignment); m.Core.State.StatusRegister = Status(path[0], trace, ccr); m.Core.State.VectorBaseRegister = vbr; if (form == "CU") m.Core.State.M68040PendingFpuExceptions.Begin(2, 11, Target); - if (form == "CP") m.Core.State.M68040PendingFpuExceptions.Begin(3, 49, Target); + if (post) m.Core.State.M68040PendingFpuExceptions.Begin(3, vector, Target); + if (epochs != null) + { + m.Core.State.M68040Fpu.Fpcr = 0; + m.Core.State.M68040Fpu.Fpsr = 0x08008198; + m.Core.State.M68040Fpu.Fpiar = 0x1234abcd; + } var e = ArchitecturalExpectation.Capture(m); - var priorSr = Status(tail, trace, path.Length > 1 ? ccr ^ 31 : ccr); - var accessFrame = frame - 60; + var liveTrace = epochs is { } live ? path.Length == 2 ? live.First : live.Second : trace; + var priorSr = Status(tail, liveTrace, path.Length > 1 ? ccr ^ 31 : ccr); + var exceptionBank = M68040StackFixture.ExceptionBank(priorSr); + var bridge = pointers[exceptionBank]; var accessFrame = bridge - 60; var sequence = m.Core.State.ExceptionSequence; - pointers[tail] = accessFrame; - SetStacks(e, pointers, tail, (ushort)(priorSr & ~0xc000)); + pointers[exceptionBank] = accessFrame; + var handlerSr = (ushort)((priorSr | 0x2000) & ~0xc000); + SetStacks(e, pointers, handlerSr); e.Pc = Handler; e.ExceptionVector = 2; e.ControlChecks["exception entries"] = (s => s.ExceptionSequence, sequence + 1); e.ControlChecks["saved RTE PC"] = (s => s.LastExceptionStackedProgramCounter, SyntheticMachine.Code); @@ -102,16 +163,41 @@ private static void Run(SyntheticMachine m, ValidationFaultBus bus, CoverageBatc e.ControlChecks["original access width"] = (_ => bus.Rejected.Count == 1 ? (uint)bus.Rejected[0].Width : 0, (uint)width); e.ControlChecks["discarded PCs never fetched"] = (_ => bus.Accesses.Any(a => a.Address is 0xdead0001 or 0xdead0003) ? 1u : 0u, 0); e.ControlChecks["handler sentinel never executed"] = (s => s.D[0], e.D[0]); - if (form is "CU" or "CP") e.ControlChecks["pending delivery retained"] = + if (form == "CU" || post) e.ControlChecks["pending delivery retained"] = (s => s.M68040PendingFpuExceptions.Find(form == "CU" ? 2 : 3) != null ? 1u : 0u, 1); + if (epochs != null) + { + e.ControlChecks["FPCR preserved"] = (s => s.M68040Fpu.Fpcr, 0); + e.ControlChecks["FPSR preserved"] = (s => s.M68040Fpu.Fpsr, 0x08008198); + e.ControlChecks["FPIAR preserved"] = (s => s.M68040Fpu.Fpiar, 0x1234abcd); + if (post) e.ControlChecks["pending original vector retained"] = + (s => s.M68040PendingFpuExceptions.Find(3)?.Vector is { } v ? (uint)v : uint.MaxValue, (uint)vector); + } ExpectAccessFrame(m, e, accessFrame, priorSr, SyntheticMachine.Code, frame + offset, width); bus.Arm(frame + offset + (uint)faultByte); var id = $"68040/RTE/validation-physical/{form}/path={string.Join('-', path)}/T={trace:X4}/align={alignment}/VBR={vbr:X8}/read={offset}:{width}/fault-byte={faultByte}/op=4E73/ccr={ccr:X2}"; - if (!SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/fault-entry")) - { report.Record(id + "/handler-return", "untested", "Validation fault entry prerequisite failed"); return; } - pointers[tail] = frame; - SetStacks(e, pointers, tail, priorSr); e.Pc = SyntheticMachine.Code; e.ExceptionVector = null; - SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id + "/handler-return"); + if (epochs is { } mixed) + id = $"68040/RTE/mixed-epoch-validation/{mixed.Matrix}/{form}/path={string.Join('-', path)}/incoming={mixed.Incoming:X4}/first={mixed.First:X4}/second={(path.Length == 2 ? "none" : mixed.Second.ToString("X4"))}/final={mixed.Final:X4}/align={alignment}/VBR={vbr:X8}/read={offset}:{width}/fault-byte={faultByte}/op=4E73/ccr={ccr:X2}"; + var userTail = epochs != null && (priorSr & 0x2000) == 0; + var phases = userTail ? new[] { "fault-entry", "handler-return", "privilege" } : ["fault-entry", "handler-return"]; + var phase = 0; + bool Step() + { + if (M68040StackFixture.Step(m, e, report, id + "/" + phases[phase++], batch)) return true; + while (phase < phases.Length) report.Record(id + "/" + phases[phase++], "untested", "Validation prerequisite failed"); + return false; + } + if (!Step()) return; + pointers[exceptionBank] = bridge; + SetStacks(e, pointers, priorSr); e.Pc = SyntheticMachine.Code; e.ExceptionVector = null; + if (!Step() || !userTail) return; + pointers[exceptionBank] -= 8; + SetStacks(e, pointers, handlerSr); e.Pc = PrivilegeHandler; e.ExceptionVector = 8; + e.ControlChecks["exception entries"] = (s => s.ExceptionSequence, sequence + 2); + e.Write(bridge - 8, priorSr, 2, m.Model); e.Write(bridge - 6, SyntheticMachine.Code, 4, m.Model); + e.Write(bridge - 2, 0x0020, 2, m.Model); + for (uint n = 0; n < 8; n++) e.MemoryMasks[bridge - 8 + n] = 255; + Step(); } internal static void ExpectAccessFrame(SyntheticMachine m, ArchitecturalExpectation e, uint accessFrame, @@ -134,13 +220,9 @@ internal static void ExpectAccessFrame(SyntheticMachine m, ArchitecturalExpectat for (uint n = 20; n < 24; n++) e.MemoryMasks[accessFrame + n] = 0xff; } - private static ushort Status(string bank, ushort trace, int ccr) => (ushort)((bank == "MSP" ? 0x3000 : 0x2000) | trace | ccr); - private static void SetStacks(ArchitecturalExpectation e, Dictionary pointers, string bank, ushort sr) - { - e.Sr = sr; e.A[7] = pointers[bank]; e.InactiveStackPointer = 0x7800; e.MasterStackPointer = pointers["MSP"]; - e.ControlChecks["ISP"] = (s => s.InterruptStackPointer, pointers["ISP"]); - e.ControlChecks["MSP"] = (s => s.MasterStackPointer, pointers["MSP"]); - } + private static ushort Status(string bank, ushort trace, int ccr) => M68040StackFixture.Status(bank, trace, ccr); + private static void SetStacks(ArchitecturalExpectation e, Dictionary pointers, ushort sr) => + M68040StackFixture.SetStacks(e, pointers, sr); internal sealed class ValidationFaultBus : SparseRecordingBus, IM68kPhysicalAddressMap { diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs index 31cedfc9..7514cdcf 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs @@ -212,19 +212,11 @@ bool Step() } private static ushort Status(string bank, ushort trace, int ccr) => - (ushort)((bank == "ISP" ? 0x2000 : bank == "MSP" ? 0x3000 : bank == "user-M" ? 0x1000 : 0) | trace | ccr); - private static string PhysicalBank(string bank) => bank == "user-M" ? "user" : bank; - private static string ExceptionBank(ushort sr) => (sr & 0x1000) != 0 ? "MSP" : "ISP"; - private static void SetStacks(ArchitecturalExpectation e, Dictionary pointers, ushort sr) - { - e.Sr = sr; - e.A[7] = pointers[(sr & 0x2000) == 0 ? "user" : ExceptionBank(sr)]; - e.InactiveStackPointer = pointers[(sr & 0x2000) == 0 ? "ISP" : "user"]; - e.MasterStackPointer = pointers["MSP"]; - e.ControlChecks["USP"] = (s => s.UserStackPointer, pointers["user"]); - e.ControlChecks["ISP"] = (s => s.InterruptStackPointer, pointers["ISP"]); - e.ControlChecks["MSP"] = (s => s.MasterStackPointer, pointers["MSP"]); - } + M68040StackFixture.Status(bank, trace, ccr); + private static string PhysicalBank(string bank) => M68040StackFixture.PhysicalBank(bank); + private static string ExceptionBank(ushort sr) => M68040StackFixture.ExceptionBank(sr); + private static void SetStacks(ArchitecturalExpectation e, Dictionary pointers, ushort sr) => + M68040StackFixture.SetStacks(e, pointers, sr); private static void ExpectException(SyntheticMachine m, ArchitecturalExpectation e, Dictionary pointers, ushort sr, int vector, uint pc, uint address, uint sequence) { @@ -241,6 +233,28 @@ private static void ExpectException(SyntheticMachine m, ArchitecturalExpectation } private static bool ExecuteStep(SyntheticMachine m, ArchitecturalExpectation e, CoverageBatch report, string id, bool batch) + => M68040StackFixture.Step(m, e, report, id, batch); +} + +// Shared test-only stack/status fixtures. Fixed literal examples above validate +// the encoding independently; no production stack or exception helper is used. +internal static class M68040StackFixture +{ + internal static ushort Status(string bank, ushort trace, int ccr) => + (ushort)((bank == "ISP" ? 0x2000 : bank == "MSP" ? 0x3000 : bank == "user-M" ? 0x1000 : 0) | trace | ccr); + internal static string PhysicalBank(string bank) => bank == "user-M" ? "user" : bank; + internal static string ExceptionBank(ushort sr) => (sr & 0x1000) != 0 ? "MSP" : "ISP"; + internal static void SetStacks(ArchitecturalExpectation e, Dictionary pointers, ushort sr) + { + e.Sr = sr; + e.A[7] = pointers[(sr & 0x2000) == 0 ? "user" : ExceptionBank(sr)]; + e.InactiveStackPointer = pointers[(sr & 0x2000) == 0 ? "ISP" : "user"]; + e.MasterStackPointer = pointers["MSP"]; + e.ControlChecks["USP"] = (s => s.UserStackPointer, pointers["user"]); + e.ControlChecks["ISP"] = (s => s.InterruptStackPointer, pointers["ISP"]); + e.ControlChecks["MSP"] = (s => s.MasterStackPointer, pointers["MSP"]); + } + internal static bool Step(SyntheticMachine m, ArchitecturalExpectation e, CoverageBatch report, string id, bool batch) { if (!batch) return SyntheticM68040AccessFrameAuditTests.Step(m, e, report, id); try diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index cb9e3c7f..98790f4c 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -5372,3 +5372,57 @@ Mixed epochs during validation faults, repair/retry and internal restoration remain required, alongside every retained data/context/model/reference and consolidation requirement. Milestone 6 stays **in progress**. No production CPU fix, package release, consumer rerun or regression retirement is included. + +### Mixed-epoch validation fault capture — qualified checkpoint + +`SyntheticM68040RteValidationFaultTests.MixedEpochFaultCapture*` extends physical +read rejection to independent incoming/first/second trace states and all stack +banks, including aliases of user/user-M. Canonical cases cross final trace/CCR +and reject the PC read; structural cases hold final T1 and reject every transfer +byte. Source SR/PC/format and continuation reads are real bus operations. +Expectations compose MC68040UM 8.1, 8.2.5/6, 8.3, 8.4.2 and 8.4.6.7; they are +software qualification, not a hardware capture of combined fault behavior. + +Every phase checks register/stack/memory preservation, saved live SR, fault PC, +exact access width, original pending CP vector and preserved FPU registers. A +bare return is followed by a privilege failure on user tails; supervisor retry +is not executed and original trace deferral is not inferred. Structural tests +caught a test-fixture bug: USP had been initialized after pointer construction, +skipping an aliased throwaway. The input base now remains unconsumed; failed +initial evidence is retained separately and not credited. + +| Group, each scalar/batch | Phases | Combinations | +| --- | ---: | ---: | +| Canonical PC-read boundaries | 276,480 | 3,456 | +| Structural validation bytes | 3,556,800 | 711,360 | + +Corrected initial focused reports pass all 7,666,560 phases. Three complete +`MixedEpochFault` probes qualify lost live T1, premature CCR installation and +the aliased-USP fixture error. CPU probes fail all four groups; the fixture +probe preserves both canonical groups and fails both structural groups. Intended +saved-SR diagnostics, including second-throwaway provenance in both structural +reports, are required. Every production source byte is restored against the +preceding qualified manifest. + +Fresh restored Release build succeeds with zero warnings/errors. Focused +execution passes ten tests with zero skips: four complete reports contain +7,666,560 passing phases, with zero mismatches, unsupported execution or +untested phases, plus six fixed SR encodings. Every new combination and its +weight matches independent literal PowerShell enumeration. Fourteen retained +tests passed before the fixture correction, which only affects new mixed-epoch +cases; that evidence remains separate. A fresh inventory selection executes +one expected failing test with zero skips, retaining 480 untested requirements +/ fifteen combinations. + +Frozen build starts from `5e90768279da33092d2da3f14e870a6a7881da44` plus these +test changes; production tree remains `795fd12d6c0237a22a5f92f4a96cc4823e0364c1`. +CPU SHA256: `e553a838521fb957c377ecfa65cab36c31f0d651e8d3ec705fb68963465ed41a`. +Adapter SHA256: `da2b8dd9d5e1263563a64486703b89ccba3e203d1d1a81de0dfc99141c880425`. +`artifacts/m6-mixed-fault-restored/qualification.json` records all eight current +fixture/command/assembly identities. Build identities are not relabeled after +commit. Broader full-suite, complete 040 and external-reference qualification +retains its original checkpoint; it was not rerun for this test-only slice. +Expanded 040/ordinary requirements are recorded in the plan. Trace +deferral/resumption, repair/retry, internal restoration and every remaining +model/reference/consolidation requirement stay open. No CPU fix, consumer +rerun, package release or additional regression retirement is included. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 0ae7f21f..2eace352 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2066,3 +2066,45 @@ restoration or chained odd-PC saved-SR provenance. All earlier data/restart, context-transfer, model/reference/consolidation requirements remain required. Milestone 6 stays **in progress**, with `roadmapComplete=false`. This is a test-only checkpoint; no package release or old-test retirement is included. + +### Milestone 6 mixed-epoch validation fault capture — qualified checkpoint + +Four `MixedEpochFaultCapture*` groups reuse the real physical rejection bus, +frame expectations and a shared test-only stack/status/batch fixture. Initial +instruction, first and second throwaway trace states are independent. Canonical +one-throwaway cases cross every final trace and CCR while rejecting the PC read; +structural one/two-throwaway cases hold the uncommitted final SR at T1 and reject +every byte of every validation transfer at both alignments/VBRs. Every stack +bank, user M value and alias participates. Short/invalid formats and all defined +format-7 continuations, including CP49–55, preserve original pending vectors +and conflicting FPU registers. Fault entry, bare handler return and user-tail +privilege failure check the last committed SR, source-frame preservation, exact +PC, stack banks, rejected access/width and exception sequence. These cases do +not execute supervisor retry or infer a private original-instruction trace latch. + +Canonical scalar/batch reports each contain 276,480 phases / 3,456 combinations; +structural reports each contain 3,556,800 phases / 711,360 combinations. All +7,666,560 initial phases pass after correcting an aliased-USP initialization +error in the test fixture; the earlier failed structural reports are not credited. +Independent maintained-command enumeration matches the counts. Fourteen retained +tests, including six fixed SR encodings, passed before that fixture correction; +the correction only affects new mixed-epoch cases. Three complete probes qualify +lost live T1, premature CCR installation and the aliased-USP fixture error, with +the intended diagnostics and exact production source byte restoration. Fresh +restored Release build succeeds with zero warnings/errors; focused execution +passes ten tests with zero skips, comprising four complete new reports and six +fixed SR encodings. All 7,666,560 phases pass with zero mismatches, unsupported +execution or untested phases. Every combination and weight matches independent +literal enumeration. The fresh inventory selection executes one expected +failing test with zero skips and retains 480 untested / fifteen combinations. +Frozen identities and exact scope are recorded in the qualification document; +previous broad CPU/reference results remain separate historical evidence. +Expanded complete 040 requirements are 87 tests / 74 reports / thirteen fixed +examples and 65,572,384 phases including the retained 480-case inventory. +Expanded ordinary requirements are 80,540,566 phases / 664 batches. These are +requirements, not claims of fresh complete-suite execution. + +Expectations compose MC68040UM 8.1, 8.2.5/6, 8.3, 8.4.2 and 8.4.6.7. Trace +suspension/resumption across differing epochs, executed repair/retry, internal +restoration, all earlier data/context/model/reference and consolidation scope +remain required. Milestone 6 stays in progress; no CPU defect or release is claimed. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index e3e072af..a2c11907 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -26,6 +26,10 @@ $expected = [ordered]@{ 'address-error-fetch-040' = @{cases=2304; combinations=72} 'rte-validation-physical-direct' = @{cases=162816; combinations=2544} 'rte-validation-physical-chained' = @{cases=61056; combinations=15264} + 'rte-mixed-fault-canonical-scalar' = @{cases=276480; combinations=3456} + 'rte-mixed-fault-canonical-batch' = @{cases=276480; combinations=3456} + 'rte-mixed-fault-structure-scalar' = @{cases=3556800; combinations=711360} + 'rte-mixed-fault-structure-batch' = @{cases=3556800; combinations=711360} 'rte-access-entry-double-fault' = @{cases=98304; combinations=3072} 'rte-access-handler-refault' = @{cases=3840; combinations=120} 'access-double-fault-dispatch-accurate' = @{cases=1088; combinations=544} @@ -115,7 +119,7 @@ if (-not $ValidateReportsOnly) { 'Instruction-fault fixtures use cache-disabled accurate execution and physical-map rejection; speculative deferral, enabled caches/MMU and compiled fetch PC provenance remain unqualified', 'Legacy repair clears saved trace; separate supervisor and user-tail bridge groups preserve incoming trace, check normal/CM completion and pending CT/CU/CP49-55 priority. Executed integer trace service and successful mixed-epoch throwaway chains have separate coverage; mixed epochs during faults, repair/retry and internal restoration remain required', 'User-tail fault expectations compose documented throwaway live-SR rules with general supervisor exception entry; unusual combined hardware behavior has not been observed', - 'Mixed epochs during validation faults and repair/retry, internal-restoration double faults, cache/MMU/compiled handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', + 'Mixed-epoch fault entry/bare return and user-tail privilege failure have separate coverage; mixed-epoch repair/retry and original trace suspension/resumption, internal-restoration double faults, cache/MMU/compiled handler-entry prefetch, chained odd-PC SR provenance and physical timing remain unqualified', 'Direct odd-RTE saved-SR ordering uses documentary WinUAE 5d22d336, not an executed hardware oracle') } $identity | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'identities.json') @@ -143,8 +147,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 83 -or [int]$counters.total -ne 83 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (70 batches, 13 fixed examples)' +if ([int]$counters.executed -ne 87 -or [int]$counters.total -ne 87 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (74 batches, 13 fixed examples)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -398,6 +402,32 @@ foreach ($group in $expected.Keys) { } } } } } + } elseif ($group.StartsWith('rte-mixed-fault-', [StringComparison]::Ordinal)) { + $structure = $group.Contains('-structure-', [StringComparison]::Ordinal) + foreach ($start in @('ISP','MSP')) { foreach ($tail in @('user','user-M','ISP','MSP')) { + foreach ($middle in $(if ($structure) {@('none','user','user-M','ISP','MSP')} else {@('none')})) { + foreach ($incoming in @(0,0x8000,0x4000)) { foreach ($first in @(0,0x8000,0x4000)) { + foreach ($second in $(if ($middle -eq 'none') {@(0)} else {@(0,0x8000,0x4000)})) { + foreach ($final in $(if ($structure) {@(0x8000)} else {@(0,0x8000,0x4000)})) { + foreach ($alignment in $(if ($structure) {@(0,1)} else {@(0)})) { + foreach ($vbr in $(if ($structure) {@(0,0x10000)} else {@(0x10000)})) { + foreach ($form in @('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP49','CP50','CP51','CP52','CP53','CP54','CP55')) { + # Literal validation transfer ranges, independent of C# fixtures. + $reads = @('2:4') + if ($structure) { + $reads = @('0:2','2:4','6:2') + if ($form -in @('normal','CM','CT','CU') -or $form.StartsWith('CP')) {$reads += '12:2'} + if ($form -in @('CM','CT','CU') -or $form.StartsWith('CP')) {$reads += '8:4'} + } + $path = if ($middle -eq 'none') {"$start-$tail"} else {"$start-$middle-$tail"} + $secondName = if ($middle -eq 'none') {'none'} else {'{0:X4}' -f $second} + foreach ($read in $reads) { for ($byte=0; $byte -lt $(if ($structure) {[int]$read.Split(':')[1]} else {1}); $byte++) { + $key = '68040/RTE/mixed-epoch-validation/{0}/{1}/path={2}/incoming={3:X4}/first={4:X4}/second={5}/final={6:X4}/align={7}/VBR={8:X8}/read={9}/fault-byte={10}' -f $(if ($structure) {'structure'} else {'canonical'}),$form,$path,$incoming,$first,$secondName,$final,$alignment,$vbr,$read,$byte + # Fault entry and bare return; a user tail then attempts RTE + # in user mode, producing the documented privilege exception. + $expectedCombinations[$key] = $(if ($structure) {2} else {32}) * $(if ($tail.StartsWith('user')) {3} else {2}) + } } + } } } } } } } } } } } elseif ($group -in @('rte-validation-physical-direct','rte-validation-physical-chained')) { $chained = $group -eq 'rte-validation-physical-chained' $paths = if ($chained) { @('ISP-ISP','ISP-ISP-ISP','ISP-MSP-ISP','ISP-MSP','ISP-ISP-MSP','ISP-MSP-MSP', @@ -542,8 +572,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 83 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 87 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=57905824; xunitBatches=70; fixedExamples=13; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=65572384; xunitBatches=74; fixedExamples=13; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index 30a19f25..860d608a 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','RteSoftwareTrace','UserRteFault','UserRteTrace','UserRteSoftwareTrace','MixedEpochRte','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','RteSoftwareTrace','UserRteFault','UserRteTrace','UserRteSoftwareTrace','MixedEpochRte','MixedEpochFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -362,6 +362,29 @@ foreach ($mutation in $mixedEpochMutations) { $mutation.group='MixedEpochTrace'; $mutation.model='68040'; $mutation.milestone=6 $mutation.mixedEpoch=$true; $mutations += $mutation } +$mixedFaultMutations = @( + @{name='040-mixed-fault-lost-live-t1'; file=$advanced; + before=' State.StatusRegister = restoredStatus;'; + # A newly installed T1 must survive the next validation fault. Narrow + # the injection so both one/two-throwaway proofs fit bounded reports. + after=' State.StatusRegister = format == 1 && (priorSr & 0xc000) == 0 && (restoredStatus & 0xc000) == 0x8000 ? (ushort)(restoredStatus & ~0x8000) : restoredStatus;'; + proofCombination='/path=ISP-user/incoming=0000/first=8000/second=none/'; proofReason='saved pre-validation SR expected'; + proofSecondCombination='/path=ISP-user-user/incoming=0000/first=0000/second=8000/'}, + @{name='040-mixed-fault-premature-ccr'; file=$advanced; + before=' var restoredStatus = ReadRteFrameWord(framePointer);'; + after=@' + var restoredStatus = ReadRteFrameWord(framePointer); + State.StatusRegister = (ushort)((State.StatusRegister & ~0x001f) | (restoredStatus & 0x001f)); +'@; proofCombination='/path=ISP-user/incoming=0000/first=0000/second=none/'; proofReason='SR expected'}, + @{name='040-mixed-fault-skipped-user-fixture'; file='Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs'; + before=' if (epochs != null) m.Core.State.SetUserStackPointer(0x7800 + alignment);'; + after=' if (epochs != null) m.Core.State.SetUserStackPointer(pointers["user"]);'; fixtureSkip=$true; + proofCombination='/path=ISP-user-user/incoming=0000/first=0000/second=8000/'; proofReason='saved pre-validation SR expected'} +) +foreach ($mutation in $mixedFaultMutations) { + $mutation.group='MixedEpochFaultCapture'; $mutation.model='68040'; $mutation.milestone=6 + $mutation.mixedFault=$true; $mutation.proofPhase='/fault-entry'; $mutations += $mutation +} $pendingTraceMutations = @( @{name='040-pending-trace-extra-rte-trace'; file=$advanced; before=' if (exception && (_profile.Model is M68kAcceleratorModel.M68040 or M68kAcceleratorModel.M68060 ||'; @@ -552,6 +575,7 @@ if ($Scope -eq 'UserRteFault') { $mutations = @($mutations | Where-Object { $_.u if ($Scope -eq 'UserRteTrace') { $mutations = @($mutations | Where-Object { $_.userTrace }) } if ($Scope -eq 'UserRteSoftwareTrace') { $mutations = @($mutations | Where-Object { $_.userSoftwareTrace }) } if ($Scope -eq 'MixedEpochRte') { $mutations = @($mutations | Where-Object { $_.mixedEpoch }) } +if ($Scope -eq 'MixedEpochFault') { $mutations = @($mutations | Where-Object { $_.mixedFault }) } if ($Scope -eq 'AccessDoubleFault') { $mutations = @($mutations | Where-Object { $_.doubleFault }) } if ($Scope -eq 'BatchFault') { $mutations = @($mutations | Where-Object { $_.batchFault }) } if ($Scope -eq 'LowPowerStop') { $mutations = @($mutations | Where-Object { $_.lpstop }) } @@ -664,6 +688,37 @@ try { throw 'Instruction-fault mutation omitted complete cases or intended semantic failure' } } + if ($mutation.mixedFault) { + [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + $failed = if ($mutation.fixtureSkip) {2} else {4} + if ($trx.TestRun.ResultSummary.Counters.total -ne 4 -or $trx.TestRun.ResultSummary.Counters.executed -ne 4 -or + $trx.TestRun.ResultSummary.Counters.failed -ne $failed -or $trx.TestRun.ResultSummary.Counters.passed -ne (4-$failed) -or + $trx.TestRun.ResultSummary.Counters.notExecuted -ne 0 -or $batches.Count -ne 4) { + throw 'Mixed-fault mutation omitted complete four-batch execution' + } + foreach ($matrix in @('canonical','structure')) { foreach ($route in @('scalar','batch')) { + $target = @($batches | Where-Object { $_.model -ceq '68040' -and $_.group -ceq "rte-mixed-fault-$matrix-$route" }) + $cases = if ($matrix -eq 'canonical') {276480} else {3556800} + $combinations = if ($matrix -eq 'canonical') {3456} else {711360} + if ($target.Count -ne 1 -or $target[0].logicalCases -ne $cases -or $target[0].counts.unsupported -ne 0 -or + @($target[0].combinations.PSObject.Properties).Count -ne $combinations) { throw 'Mixed-fault mutation selection differs' } + if ($mutation.fixtureSkip -and $matrix -eq 'canonical') { + if ($target[0].counts.passing -ne $cases -or $target[0].counts.mismatching -ne 0 -or $target[0].counts.untested -ne 0) { + throw 'Fixture alias control unexpectedly changes canonical execution' + } + continue + } + if (@($target[0].failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination,[StringComparison]::Ordinal) -and + $_.id.EndsWith($mutation.proofPhase,[StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason,[StringComparison]::Ordinal) }).Count -eq 0) { + throw 'Mixed-fault mutation omitted its intended diagnostic' + } + if ($matrix -eq 'structure' -and $mutation.proofSecondCombination -and @($target[0].failures | Where-Object { + $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofSecondCombination,[StringComparison]::Ordinal) -and + $_.id.EndsWith($mutation.proofPhase,[StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason,[StringComparison]::Ordinal) }).Count -eq 0) { + throw 'Mixed-fault mutation omitted second-throwaway SR provenance' + } + } } + } if ($mutation.mixedEpoch) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if ($trx.TestRun.ResultSummary.Counters.executed -ne 4 -or $trx.TestRun.ResultSummary.Counters.failed -ne 4 -or @@ -765,7 +820,7 @@ try { } elseif ($mutation.entryPrefetch) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] - } elseif ($mutation.mixedEpoch -or $mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors -or $mutation.softwareTrace -or $mutation.userTrace -or $mutation.userSoftwareTrace) { + } elseif ($mutation.mixedFault -or $mutation.mixedEpoch -or $mutation.retryTrace -or $mutation.pendingTrace -or $mutation.userMaster -or $mutation.cpVectors -or $mutation.softwareTrace -or $mutation.userTrace -or $mutation.userSoftwareTrace) { @($failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination, [StringComparison]::Ordinal) -and $_.id.EndsWith($mutation.proofPhase, [StringComparison]::Ordinal) -and $_.reason.StartsWith($mutation.proofReason, [StringComparison]::Ordinal) })[0] } elseif ($mutation.userRte) { diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 53f8b88b..20f817e8 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -181,6 +181,10 @@ try { $expected['access-double-fault-dispatch-v2'] = 1088 $expected['rte-validation-physical-direct'] = 162816 $expected['rte-validation-physical-chained'] = 61056 + $expected['rte-mixed-fault-canonical-scalar'] = 276480 + $expected['rte-mixed-fault-canonical-batch'] = 276480 + $expected['rte-mixed-fault-structure-scalar'] = 3556800 + $expected['rte-mixed-fault-structure-batch'] = 3556800 $expected['rte-odd-normal'] = 69120 $expected['rte-odd-pending'] = 165888 $expected['address-error-fetch-040'] = 2304 @@ -323,7 +327,8 @@ try { m68040RteUserMasterScope=@{logicalCases=1517952; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); restoredSr='S=0,M=1'; forms=@('format0','format2','format3','invalid4','invalid15','normal','CM','CT','CU','CP49'); incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); qualified='User execution retains USP; synchronous completion trace, pending conversion and following trace select MSP while preserving M; all three stack banks, saved SR/PC/EA, pending consumption and handler returns checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='mixed-epoch provenance, internal restoration'}; m68040RtePendingTraceScope=@{logicalCases=1824768; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); forms='CT/CU/CP49'; incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); qualified='Three original-frame stores preserve access SR; returned incoming trace cannot create an extra RTE trace on pending conversion; saved SR/PC/EA, format/vector, pending consumption, handler return and following traced BRA are checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='mixed-epoch provenance, internal restoration and physical/hardware qualification'}; m68040RteRetryTraceScope=@{logicalCases=2729088; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); faultTailBanks=@('ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredBanks=@('user','ISP','MSP'); restoredTrace=@(0,0x8000,0x4000); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM'; qualified='Three original-frame repair stores leave saved access SR untouched; handler return restores incoming trace; successful original RTE triggers T1/T0 trace with restored SR/PC; trace-handler return and following instruction retain MOVEM EA and stack selection'; ccr='all 32 canonical, 0/31 every chained read byte'; remaining='mixed trace values within consumed throwaways, internal restoration and hardware/physical timing'}; - m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, executed repair/retry after committed supervisor throwaways, and manual-derived user-tail fault/privilege/bridge repair; chained odd-PC saved-SR provenance, internal-restoration faults, mixed-epoch trace provenance, CP context transfer, other real fault entry and physical timing remain unqualified'; + m68040MixedEpochFaultScope=@{logicalCases=7666560; batches=4; routes=@('accurate scalar','one-instruction batch'); qualified='Independent incoming/first/second trace states; all banks and user M aliases; canonical final trace/CCR and structural every validation-read byte; last committed SR, bare handler return, user privilege failure and retained original pending vector'; remaining='Executed mixed-epoch repair/retry, original instruction trace suspension/resumption, internal restoration and hardware capture'}; + m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, successful mixed-epoch chains and validation fault capture, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, executed repair/retry after committed supervisor throwaways, and manual-derived user-tail fault/privilege/bridge repair; chained odd-PC saved-SR provenance, internal-restoration faults, mixed-epoch repair/retry and original trace suspension/resumption, CP context transfer, other real fault entry and physical timing remain unqualified'; m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='Internal restoration, mixed-epoch trace provenance and other real access faults; normal/CM preserved-trace and user-tail repair are qualified separately'}; m68040InstructionFaultScope=@{logicalCases=116736; batches=2; translationEnabled=$false; instructionCacheEnabled=$false; engine='accurate public factory plus cached self-branch batch'; forms=@('opcode','extension-low','next-opcode','self-branch'); banks=@('user','ISP','MSP'); ccr='all 32'; trace='All defined incoming states for frame/return; restart trace zero'; fields='Format7 SR, executing PC distinct from prefetch FA, RW/SIZE/TM and invalid writebacks'; phases=@('fault entry','handler return','restart','following'); remaining='Data writebacks/restart, speculative prefetch deferral, compiled instruction-fault PC provenance, cache-enabled fetches and physical timing'}; m68040RtePhysicalValidationScope=@{logicalCases=223872; batches=2; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); trace=@(0,0x8000,0x4000); alignment=@(0,1); vbr=@(0,0x10000); faults='Each byte of SR/PC/format/SSW/continuation-EA reads; 14 direct/one/two-throwaway supervisor paths'; phases=@('fault entry','handler return'); undefined='EA and invalid writeback/push data; SSW X'; remaining='Internal-restoration/double faults, other instruction faults and software writeback handlers; user-tail validation is qualified separately'}; From 72102e90c3a78dd6538a4c6eca2d564e5bcad116 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 11:55:23 +0300 Subject: [PATCH 059/211] Add mixed-epoch 68040 RTE retry discovery --- .../M68040MixedEpochRetryDiscoveryTests.cs | 45 ++++++++++++ Copper68k.Tests/Synthetic/README.md | 28 ++++++++ .../SyntheticM68040RteRepairTests.cs | 18 +++-- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 53 ++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 28 ++++++++ ...st-copper68k-040-mixed-retry-discovery.ps1 | 72 +++++++++++++++++++ 6 files changed, 238 insertions(+), 6 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/M68040MixedEpochRetryDiscoveryTests.cs create mode 100644 scripts/test-copper68k-040-mixed-retry-discovery.ps1 diff --git a/Copper68k.Tests/Synthetic/M68040MixedEpochRetryDiscoveryTests.cs b/Copper68k.Tests/Synthetic/M68040MixedEpochRetryDiscoveryTests.cs new file mode 100644 index 00000000..95696ae2 --- /dev/null +++ b/Copper68k.Tests/Synthetic/M68040MixedEpochRetryDiscoveryTests.cs @@ -0,0 +1,45 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Discovery, not a promoted architectural qualification. MC68040UM 8.2.6 +// gives the original-instruction trace rule, but its interaction with committed +// throwaways and a fresh validation retry still needs independent qualification. +// Keep both differing directions visible; never use current CPU behavior as +// the expected answer or add a private continuation latch to make this pass. +public sealed class M68040MixedEpochRetryDiscoveryTests(ITestOutputHelper output) +{ + internal const string Enable = "COPPER68K_RUN_040_MIXED_RETRY_DISCOVERY"; + private static readonly ushort[] Traces = [0, 0x8000, 0x4000]; + + [EnvironmentFact(Enable, "qualify original trace across mixed-epoch RTE repair/retry"), Trait("Suite", "ReferenceDiscovery")] + public void OriginalTraceDeferralScalar() => Audit(false); + + [EnvironmentFact(Enable, "qualify original trace across mixed-epoch RTE repair/retry"), Trait("Suite", "ReferenceDiscovery")] + public void OriginalTraceDeferralBatch() => Audit(true); + + private void Audit(bool batch) + { + var bus = new SyntheticM68040RteValidationFaultTests.ValidationFaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + var report = new CoverageBatch("68040", $"rte-mixed-retry-discovery-{(batch ? "batch" : "scalar")}"); + foreach (var start in new[] { "ISP", "MSP" }) + foreach (var tail in new[] { "ISP", "MSP" }) + foreach (var middle in new[] { "none", "user", "user-M", "ISP", "MSP" }) + foreach (var incoming in Traces) + foreach (var first in Traces) + foreach (var second in middle == "none" ? new ushort[] { 0 } : Traces) + foreach (var result in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var restored in Traces) + foreach (var ccr in new[] { 0, 31 }) + foreach (var form in new[] { "format0", "normal", "CM" }) + { + var path = middle == "none" ? new[] { start, tail } : [start, middle, tail]; + SyntheticM68040RteRepairTests.Run(m, bus, report, path, result, incoming, restored, + 0, 0x10000, ccr, form, 2, 4, 0, keepTrace: true, batch: batch, + pendingTrace: false, userMaster: false, cpVectors: false, traceService: false, + clearServiceTrace: false, throwawayTraces: (first, second)); + } + report.Complete(output); + } +} diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 3308b1c4..2adc9e8c 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -806,3 +806,31 @@ structural groups. The lost-T1 probe additionally requires second-throwaway trace diagnostics in both structural reports. Shared test-only stack/status fixtures retain independent fixed SR examples. Failed initial fixture evidence is not qualification evidence. + +### Mixed-epoch repair/retry discovery + +`M68040MixedEpochRetryDiscoveryTests` executes physical PC-read rejection, +three real repair stores, handler RTE, retry and following instruction. Initial, +first/second throwaway and repaired trace values vary independently; supervisor +tails include one/two throwaways, user/user-M intermediate aliases, every +restored stack bank, CCR 0/31 and format0/normal/CM. Its expectation applies +MC68040UM 8.2.6's original-instruction trace deferral to validation retry. That +composition still needs independent qualification. This discovery is not a +promoted family or a confirmed production defect; the existing required +inventory remains intact. + +Run `./scripts/test-copper68k-040-mixed-retry-discovery.ps1` explicitly. It +requires two executed tests without skips, records current source/binary +identities, independently validates all 16,848 combinations and 258,336 phases +per route, and fails on mismatches, unsupported execution, untested phases or +missing inputs/reports/selections. Use `-ValidateReportsOnly -OutputDirectory` +with recorded outputs to validate them again. Ordinary synthetic CI excludes +these `ReferenceDiscovery` cases; an optional skip is unavailable coverage. + +The current scalar/batch selections each contain 220,896 passing, 14,976 +mismatching, zero unsupported and 22,464 untested phases. Retry uses the last +committed trace state where the hypothesis requires the original state. Both +lost and newly introduced trace directions, including second throwaways, are +retained. No private trace latch, guessed CT flag or production change is +introduced to force agreement. See the qualification record for scope and +independent-reference caveats. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs index ab9fa7cb..b6b427ea 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs @@ -130,9 +130,10 @@ private void Audit(bool chained, bool keepTrace = false, bool batch = false, boo report.Complete(output); } - private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTests.ValidationFaultBus bus, + internal static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTests.ValidationFaultBus bus, CoverageBatch report, string[] path, string result, ushort incoming, ushort restoredTrace, - uint alignment, uint vbr, int ccr, string form, uint offset, int width, int faultByte, bool keepTrace, bool batch, bool pendingTrace, bool userMaster, bool cpVectors, bool traceService, bool clearServiceTrace) + uint alignment, uint vbr, int ccr, string form, uint offset, int width, int faultByte, bool keepTrace, bool batch, bool pendingTrace, bool userMaster, bool cpVectors, bool traceService, bool clearServiceTrace, + (ushort First, ushort Second)? throwawayTraces = null) { bus.Disarm(); m.Reset(ccr); var pointers = new Dictionary { ["user"] = 0x7800 + alignment, ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; @@ -141,11 +142,13 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes var discarded = new List(); for (var n = 0; n < path.Length - 1; n++) { - var at = pointers[path[n]]; discarded.Add(at); - m.InitializePhysical(at, Status(path[n + 1], incoming, ccr ^ 31), 2); + var bank = M68040StackFixture.PhysicalBank(path[n]); + var at = pointers[bank]; discarded.Add(at); + var trace = throwawayTraces is { } epochs ? n == 0 ? epochs.First : epochs.Second : incoming; + m.InitializePhysical(at, Status(path[n + 1], trace, ccr ^ 31), 2); m.InitializePhysical(at + 2, 0xdead0001u + (uint)n * 2, 4); m.InitializePhysical(at + 6, 0x1024, 2); - pointers[path[n]] += 8; + pointers[bank] += 8; } var tail = path[^1]; var frame = pointers[tail]; var accessFrame = frame - 60; var format = form.StartsWith("format") ? int.Parse(form[6..]) : form.StartsWith("invalid") ? int.Parse(form[7..]) : 7; @@ -155,7 +158,8 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes var continuation = post ? 0x8000 : unimplemented ? 0x4000 : form switch { "CM" => 0x1000, "CT" => 0x2000, _ => 0 }; var vector = post ? form == "CP" ? 49 : int.Parse(form[2..]) : unimplemented ? 11 : form == "CT" ? 9 : 0; var savedSr = Status(result, restoredTrace, ccr ^ 31); - var priorSr = Status(tail, incoming, path.Length > 1 ? ccr ^ 31 : ccr); + var committedTrace = throwawayTraces is { } live ? path.Length == 2 ? live.First : live.Second : incoming; + var priorSr = Status(tail, committedTrace, path.Length > 1 ? ccr ^ 31 : ccr); // Every repair changes SR and PC. Invalid formats are repaired to format 0. m.InitializePhysical(frame, 0x801fu ^ (uint)ccr, 2); m.InitializePhysical(frame + 2, Target + 2, 4); @@ -222,6 +226,8 @@ private static void Run(SyntheticMachine m, SyntheticM68040RteValidationFaultTes else if (vector != 0) phases.Add("pending-return"); phases.Add("following"); var id = $"68040/RTE/{(traceService ? "software-trace" : cpVectors ? "cp-vectors" : userMaster ? "user-master" : pendingTrace ? "pending-trace" : keepTrace ? "retry-trace" : "repair")}/{form}/path={string.Join('-', path)}/result={result}/incoming={incoming:X4}/T={restoredTrace:X4}/align={alignment}/VBR={vbr:X8}/read={offset}:{width}/fault-byte={faultByte}{(traceService ? $"/clear={clearServiceTrace}" : "")}/op=4E73/ccr={ccr:X2}"; + if (throwawayTraces is { } mixed) + id = $"68040/RTE/mixed-retry-discovery/{form}/path={string.Join('-', path)}/result={result}/incoming={incoming:X4}/first={mixed.First:X4}/second={(path.Length == 2 ? "none" : mixed.Second.ToString("X4"))}/T={restoredTrace:X4}/align={alignment}/VBR={vbr:X8}/read={offset}:{width}/fault-byte={faultByte}/op=4E73/ccr={ccr:X2}"; var phase = 0; bool Step() { diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 98790f4c..fd99bcf4 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -5426,3 +5426,56 @@ Expanded 040/ordinary requirements are recorded in the plan. Trace deferral/resumption, repair/retry, internal restoration and every remaining model/reference/consolidation requirement stay open. No CPU fix, consumer rerun, package release or additional regression retirement is included. + +### Mixed-epoch RTE retry discovery — unresolved trace composition + +The previous repaired-chain fixtures reused incoming trace in every throwaway, +so they could not distinguish original-instruction trace deferral from tracing +a fresh retry with the last committed SR. The new `M68040MixedEpochRetryDiscoveryTests` +passes independent first/second trace values to the shared test-only repair +fixture. Physical PC-read rejection, saved live SR/PC, committed stack pointers, +three real repair stores, handler return and the original RTE retry are executed. +The matrix includes one/two throwaways, all intermediate aliases including +user M=1, supervisor fault tails, all restored banks, CCR 0/31 and format0/normal/CM. + +| Each scalar/batch report | Phases | +| --- | ---: | +| Passing | 220,896 | +| Mismatching | 14,976 | +| Unsupported | 0 | +| Untested after a failed prerequisite | 22,464 | +| Total / combinations | 258,336 / 16,848 | + +Both tests execute and fail with zero skips. The current core traces the retry +using its entry SR, which contains the last committed throwaway trace. Applying +MC68040UM 8.2.6's original-instruction deferral rule instead predicts the opposite +outcome when the trace classifications differ: original T1/T0 with a cleared +throwaway loses the predicted trace; original zero with a traced throwaway gains +one. Both directions and the second-throwaway diagnostic are retained in both +reports. Equal classifications pass and mismatch counts localize the divergence +to retry. The interaction of the general deferral rule with RTE validation +restart remains an unqualified composition, not confirmed hardware behavior. +MC68040UM 8.4.2/8.4.6.7 and the addendum do not establish an extra private trace +latch here. Documentary WinUAE source was inspected, not executed as a fault +oracle. No CPU fix or guessed CT/latch behavior is introduced. + +The explicit `test-copper68k-040-mixed-retry-discovery.ps1` command validates every +combination and phase weight using literal independent enumeration. It retains +the failed summary with `architecturallyQualified=false`, `passed=false` and +`roadmapComplete=false`. Five negative integrity controls reject missing +identities/fixture identity, empty selection, missing report and missing +combination for their intended reasons. Three retained boundary/encoding tests +pass with zero skips. Release build has zero warnings/errors. + +Frozen build starts from `78d6dceb5d3577f5f6cc9ca7a0c268916f7d507d` plus these +test changes; production tree remains `795fd12d6c0237a22a5f92f4a96cc4823e0364c1`. +CPU SHA256: `084d3c734d22aa40271c5700a67a385041dd5c27448832777f9b3161a0c14cec`. +Adapter SHA256: `978a24ef1123392a681b9eff9d12ff77fbe00dd293396c7b2f24df95de12d87f`. +`artifacts/m6-mixed-retry-discovery-final/identities.json` records production and +fixture/command/assembly identities; the corresponding summary retains all +counts. These build identities are not relabeled after commit. No previous +broader qualification is rerun or relabeled. Ordinary/complete-040 coverage +requirements remain those of the preceding checkpoint; these separately gated +discovery cases do not replace its 480-case inventory. Independent trace +qualification, complete executed mixed-epoch repair/retry, internal restoration, +all other model/reference gaps and consolidation remain required. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 2eace352..d17886e2 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2108,3 +2108,31 @@ Expectations compose MC68040UM 8.1, 8.2.5/6, 8.3, 8.4.2 and 8.4.6.7. Trace suspension/resumption across differing epochs, executed repair/retry, internal restoration, all earlier data/context/model/reference and consolidation scope remain required. Milestone 6 stays in progress; no CPU defect or release is claimed. + +### Milestone 6 original-trace retry discovery — unresolved + +A dedicated discovery command now distinguishes original incoming tracing from +the trace state installed by consumed throwaways. It executes physical PC-read +faults, three repair stores, access-handler return, retry and following +instruction. One/two-throwaway paths cover supervisor tails, all intermediate +stack aliases and all restored banks; format0/normal/CM, CCR 0/31 and independent +initial/first/second/final trace values are explicit. + +Both scalar/batch selections execute without skips and each report 258,336 +phases / 16,848 independently enumerated combinations: 220,896 passing, +14,976 mismatching, zero unsupported and 22,464 untested phases. Current retry +tracing follows the last committed state, disagreeing in both directions with +the original-instruction deferral hypothesis composed from MC68040UM 8.2.6. +Equal trace classifications pass; failure is localized to the retry boundary. +The composition is still independently unqualified. No architectural CPU defect +or physical trace result is claimed, and no trace latch/CT workaround is added. + +`scripts/test-copper68k-040-mixed-retry-discovery.ps1` records source/binary +identities, verifies complete selections and every combination/phase weight, +and correctly fails this current result. Five integrity controls reject missing +identities, a missing fixture identity, an empty selection, a missing report and +a missing combination. Three retained boundary/encoding tests pass. This is +discovery evidence rather than promoted ordinary-CI coverage; complete 040 and +ordinary counts from the preceding checkpoint remain unchanged. The existing +480-case inventory and every unresolved data/context/model/reference and +consolidation requirement remain required. Milestone 6 stays **in progress**. diff --git a/scripts/test-copper68k-040-mixed-retry-discovery.ps1 b/scripts/test-copper68k-040-mixed-retry-discovery.ps1 new file mode 100644 index 00000000..b2eeff94 --- /dev/null +++ b/scripts/test-copper68k-040-mixed-retry-discovery.ps1 @@ -0,0 +1,72 @@ +#requires -Version 7.0 +[CmdletBinding()] +param([string]$OutputDirectory='artifacts/040-mixed-retry-discovery', [switch]$ValidateReportsOnly) +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +$manifest=Join-Path $output 'identities.json' +$fixtures=@('Copper68k.Tests/Synthetic/M68040MixedEpochRetryDiscoveryTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs', + 'Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs', + 'Copper68k.Tests/Synthetic/SyntheticMachine.cs','Copper68k.Tests/Synthetic/SyntheticExecution.cs','Copper68k.Tests/Synthetic/SyntheticMoveTests.cs', + 'scripts/test-copper68k-040-mixed-retry-discovery.ps1','Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') +if(-not $ValidateReportsOnly) { + if(Test-Path -LiteralPath $output){throw "Use fresh outputs: $output"} + New-Item -ItemType Directory -Path $output|Out-Null + & dotnet build (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release *> (Join-Path $output 'build.log') + if($LASTEXITCODE -ne 0){throw 'Discovery build failed'} + $identity=@{schema=1;sourceCommit=(& git -C $repo rev-parse HEAD);cpuCommittedTree=(& git -C $repo rev-parse HEAD:Copper68k); + reference='MC68040UM';url='https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf';sections=@('8.2.6','8.4.2','8.4.6.7'); + expectation='Original-instruction trace deferral composed with committed throwaways and validation retry; independently unqualified'; + softwareReferenceExecuted=$false;architecturallyQualified=$false;inputs=@()} + $files=@(& git -C $repo ls-files 'Copper68k/*') + $fixtures + foreach($file in $files){$identity.inputs+=@{file=$file;sha256=(Get-FileHash (Join-Path $repo $file)).Hash.ToLowerInvariant()}} + $identity|ConvertTo-Json -Depth 8|Set-Content $manifest + $settings=@{COPPER68K_RUN_040_MIXED_RETRY_DISCOVERY='1';COPPER68K_SYNTHETIC_REPORT_DIR=$output};$saved=@{} + try { + foreach($name in $settings.Keys){$saved[$name]=[Environment]::GetEnvironmentVariable($name);[Environment]::SetEnvironmentVariable($name,$settings[$name])} + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --no-build --no-restore --filter 'FullyQualifiedName~M68040MixedEpochRetryDiscoveryTests' --logger 'trx;LogFileName=discovery.trx' --results-directory $output *> (Join-Path $output 'run.log') + $testExit=$LASTEXITCODE + } finally {foreach($name in $saved.Keys){[Environment]::SetEnvironmentVariable($name,$saved[$name])}} +} +if(-not (Test-Path -LiteralPath $manifest)){throw 'Missing discovery identities'} +$identity=Get-Content $manifest -Raw|ConvertFrom-Json +if($identity.schema -ne 1 -or $identity.reference -cne 'MC68040UM' -or $identity.architecturallyQualified -ne $false -or + $identity.sourceCommit -notmatch '^[0-9a-f]{40}$' -or $identity.cpuCommittedTree -notmatch '^[0-9a-f]{40}$'){throw 'Incomplete discovery identities'} +$requiredInputs=@(& git -C $repo ls-files 'Copper68k/*') + $fixtures +if(@($identity.inputs).Count -ne $requiredInputs.Count -or @(Compare-Object ($requiredInputs|Sort-Object) ($identity.inputs.file|Sort-Object)).Count){throw 'Missing or unexpected discovery fixture identity'} +foreach($row in $identity.inputs){if((Get-FileHash (Join-Path $repo $row.file)).Hash.ToLowerInvariant() -cne $row.sha256){throw "Changed discovery input: $($row.file)"}} +[xml]$trx=Get-Content (Join-Path $output 'discovery.trx') -Raw +$c=$trx.TestRun.ResultSummary.Counters +if([int]$c.total -ne 2 -or [int]$c.executed -ne 2 -or [int]$c.notExecuted -ne 0){throw 'Discovery must execute both complete selections without skips'} +$expected=@{} +foreach($start in @('ISP','MSP')){foreach($tail in @('ISP','MSP')){foreach($middle in @('none','user','user-M','ISP','MSP')){ +foreach($incoming in @(0,0x8000,0x4000)){foreach($first in @(0,0x8000,0x4000)){ +foreach($second in $(if($middle -eq 'none'){@(0)}else{@(0,0x8000,0x4000)})){ +foreach($result in @('user','user-M','ISP','MSP')){foreach($trace in @(0,0x8000,0x4000)){foreach($form in @('format0','normal','CM')){ + $path=if($middle -eq 'none'){"$start-$tail"}else{"$start-$middle-$tail"} + $secondName=if($middle -eq 'none'){'none'}else{'{0:X4}' -f $second} + $key='68040/RTE/mixed-retry-discovery/{0}/path={1}/result={2}/incoming={3:X4}/first={4:X4}/second={5}/T={6:X4}/align=0/VBR=00010000/read=2:4/fault-byte=0' -f $form,$path,$result,$incoming,$first,$secondName,$trace + # Two CCR values, seven phases, plus trace return for original T1/T0. + $expected[$key]=2 * $(if($incoming -eq 0){7}else{8}) +}}}}}}}}} +if($expected.Count -ne 16848 -or ($expected.Values|Measure-Object -Sum).Sum -ne 258336){throw 'Independent discovery enumeration differs'} +$totals=@{passing=0;mismatching=0;unsupported=0;untested=0};$reports=@() +foreach($route in @('scalar','batch')) { + $group="rte-mixed-retry-discovery-$route" + $r=Get-Content (Join-Path $output "68040-$group.json") -Raw|ConvertFrom-Json -AsHashtable + if($r.model -cne '68040' -or $r.group -cne $group -or $r.logicalCases -ne 258336 -or $r.xunitBatches -ne 1 -or $r.combinations.Count -ne 16848){throw 'Incomplete discovery report'} + $sums=@{passing=0;mismatching=0;unsupported=0;untested=0} + foreach($key in $r.combinations.Keys) { + if(-not $expected.ContainsKey($key)){throw "Unexpected combination: $key"} + $sum=0 + foreach($status in $r.combinations[$key].Keys){if(-not $sums.ContainsKey($status)){throw 'Unknown coverage status'};$n=[long]$r.combinations[$key][$status];if($n -lt 0){throw 'Negative coverage count'};$sum+=$n;$sums[$status]+=$n} + if($sum -ne $expected[$key]){throw "Incomplete combination: $key"} + } + foreach($status in $sums.Keys){if($r.counts[$status] -ne $sums[$status]){throw 'Coverage totals differ'};$totals[$status]+=$sums[$status]} + $reports+=@{group=$group;logicalCases=$r.logicalCases;combinations=$r.combinations.Count;counts=$r.counts} +} +$passed=([int]$c.failed -eq 0 -and [int]$c.passed -eq 2 -and ($totals.mismatching+$totals.unsupported+$totals.untested) -eq 0) +if(-not $ValidateReportsOnly -and (($testExit -eq 0) -ne $passed)){throw 'Test exit and coverage disagree'} +@{schema=1;logicalCases=516672;reports=$reports;counts=$totals;passed=$passed;architecturallyQualified=$false;roadmapComplete=$false;expectation=$identity.expectation}| + ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'discovery-summary.json') +if(-not $passed){throw "Original-trace discovery remains mismatching or incomplete: $($totals|ConvertTo-Json -Compress)"} From 2f66bcaa03d9e34efa82df80634003f2bfbf8c96 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 12:32:42 +0300 Subject: [PATCH 060/211] Qualify chained 68040 RTE odd-PC SR handoff --- Copper68k.Tests/Synthetic/README.md | 38 +++++ .../SyntheticM68040AccessFrameAuditTests.cs | 2 + .../SyntheticM68040ChainedOddReturnTests.cs | 121 ++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 108 +++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 30 ++++ scripts/reference/m68040-rte-handoff.cpp | 151 +++++++++++++++++ scripts/test-copper68k-040-access-frames.ps1 | 34 +++- scripts/test-copper68k-040-rte-handoff.ps1 | 152 ++++++++++++++++++ .../test-copper68k-synthetic-mutations.ps1 | 28 +++- scripts/test-copper68k-synthetic.ps1 | 5 + 10 files changed, 660 insertions(+), 9 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040ChainedOddReturnTests.cs create mode 100644 scripts/reference/m68040-rte-handoff.cpp create mode 100644 scripts/test-copper68k-040-rte-handoff.ps1 diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 2adc9e8c..451db6e1 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -834,3 +834,41 @@ lost and newly introduced trace directions, including second throwaways, are retained. No private trace latch, guessed CT flag or production change is introduced to force agreement. See the qualification record for scope and independent-reference caveats. + +### Chained odd-PC short-frame SR handoff + +`SyntheticM68040ChainedOddReturnTests` adds four ordinary synthetic batches for +format0/2/3 tails following one/two throwaways. Canonical scalar/batch groups +each execute 82,944 cases / 2,592 combinations with every initial CCR; structural +groups each execute 539,136 cases / 269,568 combinations with CCR 0/31, both +alignments, both VBRs, low/high odd PCs, independent incoming/first/second/final +trace states, every stack and the user M=1 alias. Full register/memory checks, +consumed pointers, discarded/odd PC non-fetch, validation-read order and the +format-2 address-error image are checked. + +Run `./scripts/test-copper68k-040-rte-handoff.ps1` with a pristine WinUAE checkout +at `5d22d33632646efc3f747f03e82d28353e52722e` passed as `-ReferenceDirectory`. +The default location is `artifacts/reference-winuae-rte-modern`; the command +requires PowerShell 7 and MSVC (override `-VcVars` for a different installation). +It builds the pinned generator with CPU_TESTER enabled, extracts the untouched +generated 040 RTE and cputest SR helpers, and executes their secondary-SR +handoff in a transport harness. Two literal controls distinguish the original, +last-throwaway and final SR. The handoff/live state and frame reads are compared +with CPU results; documented format-2 entry and the traced-user S-bit correction +are composed separately. This is an executed software handoff reference, not +a complete WinUAE exception engine or a hardware/timing qualification. + +The command requires four executed tests without skips, exact independently +enumerated combinations, every reference row, current source/binary identities +and zero mismatches/unsupported/untested cases. Missing inputs, empty selections +and out-of-scope reference operations fail. Recheck frozen outputs using +`-ValidateReportsOnly -OutputDirectory`. Never substitute this observer for a +validation bus-fault oracle: the generated 040 test RTE does not handle that +fault protocol. Format7 continuations/foreign context and the original-trace +retry discovery remain explicit requirements. + +`./scripts/test-copper68k-synthetic-mutations.ps1 -Scope ChainedOddRte` proves +that both complete canonical batches detect substitution of the final SR, +loss of the committed trace bits and omission of the traced-user S correction. +It requires the intended saved-SR diagnostics and restores/rebuilds production +source. No old hardware/integration regression is retired by this qualification. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 8c1b67c8..209aa708 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -36,6 +36,8 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() ? "Validation, preserved-trace repair/software service, successful mixed-epoch chains and mixed-epoch validation fault capture have separate software coverage; mixed-epoch repair/retry, original trace suspension/resumption and internal restoration remain required. Physical/hardware behavior is separately unqualified" : form == "real-access-fault-entry" ? "Cache-disabled accurate instruction-fetch frames/restart are covered separately; data fault/writeback restart, enabled-cache deferral and compiled instruction-PC provenance remain required" + : form == "odd-PC-chained-SR-provenance" + ? "Chained format0/2/3 SR handoff, stack consumption and reads have a separate generated WinUAE software reference; format7 normal/CM and pending/foreign-context chains remain required. Full reference exception entry and hardware behavior remain unqualified" : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040ChainedOddReturnTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040ChainedOddReturnTests.cs new file mode 100644 index 00000000..508e720c --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040ChainedOddReturnTests.cs @@ -0,0 +1,121 @@ +using System.Globalization; +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 8.4.2/3, MC68040UMAD general-operation item 3. The last +// committed throwaway SR is the secondary image handed to address-error entry +// by pinned WinUAE's generated 040 RTE. The separate native audit executes that +// handoff, without claiming a complete WinUAE exception-frame or fault oracle. +public sealed class SyntheticM68040ChainedOddReturnTests(ITestOutputHelper output) +{ + private static readonly string[] Banks = ["user", "user-M", "ISP", "MSP"]; + private static readonly ushort[] Traces = [0, 0x8000, 0x4000]; + private const uint Handler = 0x9190; + + [Fact, Trait("Suite", "Synthetic")] + public void ChainedOddShortFrameCanonicalScalar() => Audit(false, false); + [Fact, Trait("Suite", "Synthetic")] + public void ChainedOddShortFrameCanonicalBatch() => Audit(false, true); + [Fact, Trait("Suite", "Synthetic")] + public void ChainedOddShortFrameStructuralScalar() => Audit(true, false); + [Fact, Trait("Suite", "Synthetic")] + public void ChainedOddShortFrameStructuralBatch() => Audit(true, true); + + private void Audit(bool structural, bool batch) + { + var group = $"rte-chained-odd-{(structural ? "structure" : "canonical")}-{(batch ? "batch" : "scalar")}"; + var report = new CoverageBatch("68040", group); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); + var export = Environment.GetEnvironmentVariable("COPPER68K_040_RTE_HANDOFF_EXPORT"); + using var rows = string.IsNullOrWhiteSpace(export) ? null : new StreamWriter(Path.Combine(export, group + ".rows")); + var index = 0; + foreach (var start in new[] { "ISP", "MSP" }) + foreach (var tail in Banks) + foreach (var middle in structural ? new[] { "none", "user", "user-M", "ISP", "MSP" } : ["none"]) + foreach (var result in Banks) + foreach (var incoming in Traces) + foreach (var first in Traces) + foreach (var second in middle == "none" ? new ushort[] { 0 } : Traces) + foreach (var trace in Traces) + foreach (var target in structural ? new uint[] { 0x6001, 0xff002003 } : [0x6001]) + foreach (var alignment in structural ? new uint[] { 0, 1 } : [0]) + foreach (var vbr in structural ? new uint[] { 0, 0x10000 } : [0x10000]) + foreach (var format in new[] { 0, 2, 3 }) + foreach (var ccr in structural ? new[] { 0, 31 } : Enumerable.Range(0, 32)) + { + m.Reset(ccr); + var pointers = new Dictionary { ["user"] = 0x7800 + alignment, ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; + foreach (var location in pointers.Values) + for (var n = -20; n < 80; n++) m.InitializePhysical(unchecked(location + (uint)n), (uint)(n ^ 0x5a), 1); + var path = middle == "none" ? new[] { start, tail } : new[] { start, middle, tail }; + var frames = new List<(uint Address, ushort Sr, uint Pc, ushort Format)>(); + for (var n = 0; n < path.Length - 1; n++) + { + var at = pointers[Physical(path[n])]; + frames.Add((at, Status(path[n + 1], n == 0 ? first : second, ccr), 0xdead0001u + (uint)n * 2, 0x1024)); + pointers[Physical(path[n])] += 8; + } + var restoredSr = Status(result, trace, ccr ^ 31); + frames.Add((pointers[Physical(tail)], restoredSr, target, (ushort)(format << 12 | 8))); + foreach (var f in frames) + { + m.InitializePhysical(f.Address, f.Sr, 2); m.InitializePhysical(f.Address + 2, f.Pc, 4); + m.InitializePhysical(f.Address + 6, f.Format, 2); m.InitializePhysical(f.Address + 8, 0x4200, 4); + } + pointers[Physical(tail)] += format == 0 ? 8u : 12u; + _ = SyntheticExecution.Prepare(m, [0x4e73]); + m.InitializePhysical(vbr + 12, Handler, 4); m.InitializePhysical(Handler, 0x60fe, 2); + m.Core.State.SetUserStackPointer(0x7800 + alignment); + m.Core.State.SetInterruptStackPointer(0x4700 + alignment); + m.Core.State.SetMasterStackPointer(0x7400 + alignment); + var initialSr = Status(start, incoming, ccr); + m.Core.State.StatusRegister = initialSr; m.Core.State.VectorBaseRegister = vbr; + var e = ArchitecturalExpectation.Capture(m); + var priorSr = frames[^2].Sr; + // Documented 040 traced-user-return correction is applied to the + // secondary SR image, not substituted with the final restored SR. + var savedSr = (ushort)(priorSr | ((restoredSr & 0x2000) == 0 && trace != 0 ? 0x2000 : 0)); + var bank = (restoredSr & 0x1000) == 0 ? "ISP" : "MSP"; + pointers[bank] -= 12; + var sp = pointers[bank]; + e.Sr = (ushort)((restoredSr | 0x2000) & ~0xc000); e.Pc = Handler; e.ExceptionVector = 3; + e.A[7] = sp; e.InactiveStackPointer = pointers["user"]; e.MasterStackPointer = pointers["MSP"]; + e.ControlChecks["USP"] = (s => s.UserStackPointer, pointers["user"]); + e.ControlChecks["ISP"] = (s => s.InterruptStackPointer, pointers["ISP"]); + e.ControlChecks["MSP"] = (s => s.MasterStackPointer, pointers["MSP"]); + e.ControlChecks["saved SR provenance"] = (s => s.LastExceptionStatusRegister, savedSr); + e.ControlChecks["saved RTE PC"] = (s => s.LastExceptionStackedProgramCounter, SyntheticMachine.Code); + e.ControlChecks["single address error"] = (s => s.ExceptionSequence, m.Core.State.ExceptionSequence + 1); + e.ControlChecks["odd targets never fetched"] = (_ => m.Bus.Accesses.Any(a => !a.Write && + (a.Address == target || frames.Take(frames.Count - 1).Any(f => f.Pc == a.Address))) ? 1u : 0u, 0); + var expectedReads = frames.SelectMany(f => new[] { (f.Address, 2), (f.Address + 2, 4), (f.Address + 6, 2) }).ToArray(); + var addresses = expectedReads.Select(r => r.Item1).ToHashSet(); + e.ControlChecks["frame validation read order"] = (_ => m.Bus.Accesses.Where(a => !a.Write && addresses.Contains(a.Address)) + .Select(a => (a.Address, a.Width)).SequenceEqual(expectedReads) ? 0u : 1u, 0); + e.Write(sp, savedSr, 2, m.Model); e.Write(sp + 2, SyntheticMachine.Code, 4, m.Model); + e.Write(sp + 6, 0x200c, 2, m.Model); e.Write(sp + 8, target & ~1u, 4, m.Model); + var secondName = middle == "none" ? "none" : $"{second:X4}"; + var id = $"68040/RTE/chained-odd/format{format}/path={string.Join('-', path)}/result={result}/incoming={incoming:X4}/first={first:X4}/second={secondName}/T={trace:X4}/target={target:X8}/align={alignment}/VBR={vbr:X8}/op=4E73/ccr={ccr:X2}"; + M68040StackFixture.Step(m, e, report, id, batch); + if (rows != null) + { + var s = m.Core.State; + var input = new List { (uint)index, initialSr, 0x7800 + alignment, 0x4700 + alignment, 0x7400 + alignment, SyntheticMachine.Code, (uint)frames.Count }; + foreach (var f in frames) input.AddRange([f.Address, f.Sr, f.Pc, f.Format]); + var actual = new List { s.LastExceptionStatusRegister, s.StatusRegister, s.ProgramCounter, s.LastExceptionStackedProgramCounter, + m.PeekPhysical(s.A[7] + 8, 4), s.UserStackPointer, s.InterruptStackPointer, s.MasterStackPointer, (uint)expectedReads.Length }; + foreach (var a in m.Bus.Accesses.Where(a => !a.Write && addresses.Contains(a.Address))) actual.AddRange([a.Address, (uint)a.Width]); + rows.WriteLine(string.Join(' ', input.Select(Hex)) + " | " + string.Join(' ', actual.Select(Hex))); + } + index++; + } + report.Complete(output); + } + + private static string Hex(uint value) => value.ToString("X", CultureInfo.InvariantCulture); + private static string Physical(string bank) => bank == "user-M" ? "user" : bank; + private static ushort Status(string bank, ushort trace, int ccr) => + (ushort)((bank == "ISP" ? 0x2000 : bank == "MSP" ? 0x3000 : bank == "user-M" ? 0x1000 : 0) | trace | ccr); +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index fd99bcf4..0e3d7268 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -5479,3 +5479,111 @@ requirements remain those of the preceding checkpoint; these separately gated discovery cases do not replace its 480-case inventory. Independent trace qualification, complete executed mixed-epoch repair/retry, internal restoration, all other model/reference gaps and consolidation remain required. + +### Chained odd-PC short-frame SR handoff — executed software reference + +The required odd-PC provenance work now has an executed reference for chained +format0/2/3 tails. `SyntheticM68040ChainedOddReturnTests` prepares independent +frame addresses and raw words, one/two throwaways, every stack and user M=1 +alias, independent incoming/first/second/final trace states, all canonical CCRs +and structural CCR 0/31, alignment 0/1, VBR 0/0x10000 and low/high odd PCs. +Full architectural register/memory checks retain surrounding frame guards, +consumed USP/ISP/MSP, validation reads in order, non-fetch of discarded/odd PCs, +the saved RTE PC/SR and format-2 fault address. Scalar and one-instruction batch +execution share the public factory and retained batch boundary checks. + +| Fresh report | Passing cases | Combinations | Per-combination CCR weight | +| --- | ---: | ---: | ---: | +| Canonical scalar | 82,944 | 2,592 | 32 | +| Canonical batch | 82,944 | 2,592 | 32 | +| Structural scalar | 539,136 | 269,568 | 2 | +| Structural batch | 539,136 | 269,568 | 2 | +| Total | 1,244,160 | 544,320 | — | + +Fresh Release build succeeds with zero .NET warnings/errors. Four tests execute +and pass with zero skips; every report has zero mismatches, unsupported or +untested cases. The native reference independently compares every row and +agrees in all 1,244,160 cases. Literal PowerShell enumeration verifies every +architectural combination and weight, separately from xUnit counts. + +`scripts/test-copper68k-040-rte-handoff.ps1` requires a pristine official WinUAE +checkout at `5d22d33632646efc3f747f03e82d28353e52722e`. It compiles the original +opcode builder and generator, with the single CPU_TESTER configuration change, +then extracts the untouched generated `op_4e73_94_test_ff` and original +`MakeFromSR_x`, `MakeFromSR_T0`, `MakeFromSR` from `cputest.cpp`. The transport +implements sparse initialized memory, read recording, flags and the odd-PC +callback. Unexpected reads/exceptions, out-of-scope frames, missing/malformed +rows and empty selections fail. No Copper68k decoder, EA, arithmetic or timing +helper produces reference outcomes. Two literal native controls discriminate +one/two-throwaway SR provenance and consumed stacks independently of exported +CPU fixtures. + +The generated function passes its last committed throwaway SR as the secondary +image to odd-PC entry, while the restored SR governs live flags/stack selection. +The observer stops at that callback. Its tuple is composed with architectural +format-2 entry and the traced-user saved-S correction described in the +[MC68040 addendum](https://www.nxp.com/docs/en/reference-manual/MC68040UMAD.pdf), +general-operation item 3 (page 2). This validates software handoff/read/stack +agreement and the explicitly composed frame expectation. It does **not** execute +WinUAE's full exception engine or establish hardware, cache, MMU, bus fault, +trace-retry or physical timing behavior. The pinned +[tester limitations](https://raw.githubusercontent.com/tonioni/WinUAE/5d22d33632646efc3f747f03e82d28353e52722e/cputest/readme.txt) +remain relevant. In particular, the generated 040 RTE lacks validation-read +bus-fault handling and cannot qualify the unresolved original-trace retry +discovery. No latch/CT workaround or CPU correction is inferred from this audit. + +The native generator uses the upstream Unicode configuration and MSVC +19.51.36260 x64. One unused `oldpc` warning arises in untouched generated RTE; +it is retained in the native build log. Initial generator attempts used the +wrong table input or missing Unicode/link configuration and are not accepted +reference results. Initial C# fixture compilation used the wrong batch API/helper +name; those errors were corrected before any successful execution. The initial +maintained reference invocation also inherited MSVC's `Platform=x64` in its +.NET build; that preliminary comparison is not the acceptance record. The final +command restores compiler environment before building/testing the regular +Release assemblies. It records exact pristine source, extracted/generated +inputs, compiler, assemblies, fixture rows, native outputs, reports and TRX hashes. + +`ChainedOddRte` mutation proofs execute both complete canonical routes per probe: + +| Probe | Mismatches per scalar/batch report | Required diagnostic | +| --- | ---: | --- | +| Substitute final restored SR | 82,944 | Saved SR expected 0000, actual 001F | +| Lose committed throwaway trace | 55,296 | Saved SR expected 8000, actual 0000 | +| Omit traced-user saved S | 13,824 | Saved SR expected 2000, actual 0000 | + +Each probe executes two failing tests, zero skips and 82,944 complete cases / +2,592 combinations per route. The runner requires the intended provenance +diagnostic and restores every production byte; the restored build succeeds. +The seven retained direct-return/fixed-SR tests pass without skips. The required +format7 protocol inventory is separately rerun: one expected failing test, +zero skips, exactly 480 untested cases / fifteen combinations. Its chained +odd-PC entry now identifies short-frame reference coverage while retaining +normal/CM and pending/foreign-context work. + +Seven integrity controls reject missing identities/fixture identity, an empty +xUnit selection, a missing combination, a wrong combination weight, and native +missing/empty fixture inputs for their intended reasons. Output checksums are +updated only in the isolated semantic-corruption controls so checksum rejection +cannot stand in for the deeper combination/selection checks. The complete-040 +gate's new branches are separately exercised against all four accepted reports; +its 78-report / 66,816,544-case requirement sum is checked without claiming a +fresh execution of the older complete selection. + +Frozen acceptance build begins at `72102e90c3a78dd6538a4c6eca2d564e5bcad116` +plus these test/command changes. Production tree remains +`795fd12d6c0237a22a5f92f4a96cc4823e0364c1`; no production CPU byte changes. +CPU SHA256: `ac077d65570a3fc72972b0e3521a456f90838f925b1f9a3314e16d3977eda545`. +Adapter SHA256: `6f01ae15524f2d3cee820a632d91e146b9317e1efbc0b03dffedafe5f7c8b1db`. +`artifacts/m6-chained-odd-reference-final/identities.json` and +`qualification-summary.json` retain exact input/evidence hashes and counts; +build identities are not relabeled after commit. Mutation and retained/inventory +evidence stays in separate `m6-chained-odd-*` directories. Older broad CPU, +complete-040, external adapter and consumer qualifications are not rerun or +relabeled by this test-only change. Expanded requirement counts are in the plan. + +Format7 chained odd-PC continuation/foreign-context provenance, original-trace +repair/retry, internal restoration, real data/writeback restart, earlier model +reference gaps and consolidation remain required. Milestone 6 stays **in +progress**, `roadmapComplete=false`. No old regression is retired, public API +changed, consumer dependency advanced or package published. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index d17886e2..c1bd0cbd 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2136,3 +2136,33 @@ discovery evidence rather than promoted ordinary-CI coverage; complete 040 and ordinary counts from the preceding checkpoint remain unchanged. The existing 480-case inventory and every unresolved data/context/model/reference and consolidation requirement remain required. Milestone 6 stays **in progress**. + +### Milestone 6 chained odd-PC short-frame software handoff + +Four ordinary scalar/batch matrices cover one/two throwaways before format0/2/3 +odd-PC returns. Independent trace epochs, all stack/user-M aliases, every CCR +in canonical groups and structural CCR boundaries/alignments/VBRs/high PCs +produce 1,244,160 cases: 82,944 / 2,592 combinations per canonical route and +539,136 / 269,568 combinations per structural route. + +An explicit native reference command executes untouched generated WinUAE 040 +RTE and cputest SR helpers from pin `5d22d33632646efc3f747f03e82d28353e52722e`. +The observed secondary SR comes from the last committed throwaway; restored SR +selects live flags and stacks. Architectural format-2 entry and the addendum's +traced-user saved-S correction are composed independently of CPU helpers. +Two literal native controls, three canonical mutation probes and strict +source/binary/row/combination checks protect this scoped software qualification. +The harness stops at the address-error callback: full external exception entry, +validation-fault behavior and hardware/timing are not qualified by it. + +Expanded ordinary requirements are 81,784,726 cases / 668 batches. The complete +040 selection now requires 91 tests / 78 reports / thirteen fixed examples, +66,816,544 phases including the unchanged 480-case remaining-protocol inventory, +and fifteen fixture/command input identities. These expanded requirements do +not relabel older broad suite results as current execution. + +The inventory retains format7 chained odd-PC normal/CM/pending/foreign-context +requirements, mixed-epoch repair/retry and original-instruction trace deferral, +internal restoration, data/writeback restart, broader model reference audits +and consolidation. Milestone 6 stays **in progress**, `roadmapComplete=false`. +No production CPU fix, package publication or regression retirement is included. diff --git a/scripts/reference/m68040-rte-handoff.cpp b/scripts/reference/m68040-rte-handoff.cpp new file mode 100644 index 00000000..8c539301 --- /dev/null +++ b/scripts/reference/m68040-rte-handoff.cpp @@ -0,0 +1,151 @@ +// Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT +// Transport for untouched, generated WinUAE RTE + cputest SR helpers. +// No bus faults, full exception entry, caches, MMU, or timing are simulated. +// Unexpected reads/callbacks and malformed/empty inputs fail the audit. +#include +#include +#include +#include +#include +#include +#include +#include +using uae_u16 = uint16_t; +using uae_u32 = uint32_t; +using uaecptr = uint32_t; +#define REGPARAM2 +#define m68k_areg(r,n) ((r).a[n]) +struct Regs { uint32_t a[8]{}, usp{}, isp{}, msp{}, pc{}; uint16_t sr{}; int m{}, s{}, t0{}, t1{}, intmask{}, ipl[1]{}; } regs; +struct Prefs { int cpu_model = 68040; } currprefs; +int flag_SPCFLAG_TRACE, cpu_stopped; +int xflag, nflag, zflag, vflag, cflag, traceActivations; +#define SET_XFLG(v) (xflag=(v)) +#define SET_NFLG(v) (nflag=(v)) +#define SET_ZFLG(v) (zflag=(v)) +#define SET_VFLG(v) (vflag=(v)) +#define SET_CFLG(v) (cflag=(v)) +void activate_trace() { traceActivations++; } +std::unordered_map memory; +std::vector reads; +uint32_t read(uint32_t address, int width) { + reads.push_back(address); reads.push_back(width); + uint32_t value = 0; + for (int n=0;nsecond; + } + return value; +} +uint16_t get_word_test(uint32_t a) { return static_cast(read(a,2)); } +uint32_t get_long_test(uint32_t a) { return read(a,4); } +void put(uint32_t a, uint32_t value, int width) { + for (int n=0;n(value >> (8*(width-1-n))); +} +uint32_t m68k_getpci() { return regs.pc; } +void m68k_setpci_j(uint32_t pc) { regs.pc=pc; } +void Exception(int) { throw std::runtime_error("Unexpected privilege exception"); } +void Exception_cpu(int) { throw std::runtime_error("Unexpected format exception"); } +uint16_t handoffSr; +uint32_t faultPc; +int callbackCount; +void exception3_read_prefetch_68040bug(uint32_t opcode, uint32_t address, uint16_t secondarySr) { + if (opcode != 0x4e73 || !(address&1)) throw std::runtime_error("Unexpected callback"); + handoffSr=secondarySr; faultPc=address; callbackCount++; +} +// Produced only by strict extraction from the pinned source. Never hand-edit. +#include "winuae-sr.inc" +#include "winuae-rte.inc" + +std::vector parse(const std::string& line) { + std::istringstream stream(line); std::vector values; + std::string token; + while (stream>>token) { + size_t end=0; const auto value=std::stoull(token,&end,16); + if (end != token.size() || value>UINT32_MAX) throw std::runtime_error("Invalid hexadecimal input"); + values.push_back(static_cast(value)); + } + return values; +} +// Literal controls discriminate original, last-throwaway and final SR images. +void fixedControls() { + regs={}; memory.clear(); reads.clear(); callbackCount=0; + regs.sr=0xa01f; regs.s=1; regs.t1=1; regs.a[7]=regs.isp=0x4700; + regs.usp=0x7800; regs.msp=0x7400; regs.pc=0x1000; + put(0x4700,0x101f,2); put(0x4702,0xdead0001,4); put(0x4706,0x1024,2); + put(0x7800,0x6000,2); put(0x7802,0x6001,4); put(0x7806,0x2008,2); + op_4e73_94_test_ff(0x4e73); + if (callbackCount!=1 || handoffSr!=0x101f || regs.sr!=0x6000 || regs.a[7]!=0x4708 || + regs.usp!=0x780c || faultPc!=0x6001 || reads!=std::vector{0x4700,2,0x4702,4,0x4706,2,0x7800,2,0x7802,4,0x7806,2}) + throw std::runtime_error("Fixed one-throwaway handoff failed"); + regs={}; memory.clear(); reads.clear(); callbackCount=0; + regs.sr=0x3000; regs.s=regs.m=1; regs.a[7]=regs.msp=0x7401; + regs.usp=0x7801; regs.isp=0x4701; regs.pc=0x1000; + put(0x7401,0xa01f,2); put(0x7403,0xdead0001,4); put(0x7407,0x1024,2); + put(0x4701,0x5000,2); put(0x4703,0xdead0003,4); put(0x4707,0x1024,2); + put(0x7801,0x301f,2); put(0x7803,0xff002003,4); put(0x7807,0x0008,2); + op_4e73_94_test_ff(0x4e73); + if (callbackCount!=1 || handoffSr!=0x5000 || regs.sr!=0x301f || regs.a[7]!=0x7409 || + regs.usp!=0x7809 || regs.isp!=0x4709 || faultPc!=0xff002003 || reads.size()!=18) + throw std::runtime_error("Fixed two-throwaway handoff failed"); +} + +int main(int argc,char** argv) { + try { + if (argc!=5) throw std::runtime_error("Usage: observer rows output expected-count summary"); + fixedControls(); + const auto required=std::stoul(argv[3]); + if (!required) throw std::runtime_error("Empty required selection"); + std::ifstream input(argv[1]); std::ofstream output(argv[2]); + if (!input || !output) throw std::runtime_error("Missing input/output"); + std::string line; size_t count=0, mismatches=0; + while (std::getline(input,line)) { + const auto delimiter=line.find(" | "); + if (delimiter==std::string::npos) throw std::runtime_error("Missing actual-state delimiter"); + const auto fixture=parse(line.substr(0,delimiter)), actual=parse(line.substr(delimiter+3)); + if (fixture.size()<7 || fixture[0]!=count || fixture[6]<2 || fixture[6]>3 || fixture.size()!=7+4*fixture[6]) + throw std::runtime_error("Missing, reordered or invalid fixture"); + regs={}; memory.clear(); reads.clear(); callbackCount=traceActivations=0; + flag_SPCFLAG_TRACE=cpu_stopped=0; + regs.sr=static_cast(fixture[1]); + regs.s=(regs.sr>>13)&1; regs.m=(regs.sr>>12)&1; + regs.t0=(regs.sr>>14)&1; regs.t1=(regs.sr>>15)&1; regs.intmask=(regs.sr>>8)&7; + regs.usp=fixture[2]; regs.isp=fixture[3]; regs.msp=fixture[4]; regs.pc=fixture[5]; + regs.a[7]=regs.s ? regs.m ? regs.msp : regs.isp : regs.usp; + for (size_t n=7;n>12; + if ((n+4 expected{static_cast(saved),static_cast(live),0x9190,regs.pc,faultPc&~1u,usp,isp,msp,static_cast(reads.size()/2)}; + expected.insert(expected.end(),reads.begin(),reads.end()); + output<nul && set" + if($LASTEXITCODE -ne 0){throw 'MSVC setup failed'} + foreach($setting in $settings){$index=$setting.IndexOf('=');if($index -gt 0){$name=$setting.Substring(0,$index);$savedCompilerEnvironment[$name]=[Environment]::GetEnvironmentVariable($name);[Environment]::SetEnvironmentVariable($name,$setting.Substring($index+1))}} + $text=[IO.File]::ReadAllText((Join-Path $source 'gencpu.cpp')) + if(($text.Split([string[]]@('#define CPU_TESTER 0'),[StringSplitOptions]::None).Count-1) -ne 1){throw 'Reference generator configuration differs'} + [IO.File]::WriteAllText((Join-Path $output 'gencpu-test-mode.cpp'),$text.Replace('#define CPU_TESTER 0','#define CPU_TESTER 1')) + Push-Location $output + try { + & cl /nologo /O2 /EHsc /w /DWIN32 /DUNICODE /D_UNICODE "/I$source/include" "/I$source/od-win32" "/I$source" (Join-Path $source 'build68k.cpp') /Fe:build68k.exe *> build68k.log + if($LASTEXITCODE -ne 0){throw 'Reference opcode builder failed'} + Get-Content -LiteralPath (Join-Path $source 'table68k')|& ./build68k.exe|Set-Content -Encoding utf8 cpudefs.cpp + if($LASTEXITCODE -ne 0 -or -not (Select-String -LiteralPath cpudefs.cpp -SimpleMatch 'n_defs68k = 232')){throw 'Missing reference opcode definitions'} + & cl /nologo /O2 /EHsc /w /DWIN32 /DUNICODE /D_UNICODE "/I$source/include" "/I$source/od-win32" "/I$source" gencpu-test-mode.cpp (Join-Path $source 'readcpu.cpp') (Join-Path $source 'missing.cpp') (Join-Path $source 'od-win32/unicode.cpp') cpudefs.cpp user32.lib /Fe:gencpu.exe *> generator-build.log + if($LASTEXITCODE -ne 0){throw 'Reference generator build failed'} + & ./gencpu.exe *> generator-run.log + if($LASTEXITCODE -ne 0){throw 'Reference generation failed'} + $rte=FunctionText ([IO.File]::ReadAllText((Join-Path $output 'cpuemu_94_test.cpp'))) 'void REGPARAM2 op_4e73_94_test_ff(uae_u32 opcode)' + if(-not $rte.Contains('oldsr = newsr;') -or -not $rte.Contains('exception3_read_prefetch_68040bug(opcode, newpc, oldsr);')){throw 'Reference handoff structure differs'} + [IO.File]::WriteAllText((Join-Path $output 'winuae-rte.inc'),$rte) + $text=[IO.File]::ReadAllText((Join-Path $source 'cputest.cpp')) + $sr=@('void MakeFromSR_x(int t0trace)','void REGPARAM2 MakeFromSR_T0(void)','void REGPARAM2 MakeFromSR(void)')|ForEach-Object {FunctionText $text $_} + [IO.File]::WriteAllText((Join-Path $output 'winuae-sr.inc'),($sr -join "`n`n")) + & cl /nologo /O2 /EHsc /W4 "/I$output" (Join-Path $repo 'scripts/reference/m68040-rte-handoff.cpp') /Fe:observer.exe *> observer-build.log + if($LASTEXITCODE -ne 0){throw 'Reference observer build failed'} + $compiler=(& cl 2>&1|Out-String).Trim() + } finally { + Pop-Location + foreach($name in $savedCompilerEnvironment.Keys){[Environment]::SetEnvironmentVariable($name,$savedCompilerEnvironment[$name])} + } + & dotnet build (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release *> (Join-Path $output 'build.log') + if($LASTEXITCODE -ne 0){throw 'CPU suite build failed'} + $saved=@{} + try { + foreach($name in @('COPPER68K_SYNTHETIC_REPORT_DIR','COPPER68K_040_RTE_HANDOFF_EXPORT')){$saved[$name]=[Environment]::GetEnvironmentVariable($name);[Environment]::SetEnvironmentVariable($name,$output)} + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --no-build --no-restore --filter 'FullyQualifiedName~SyntheticM68040ChainedOddReturnTests' --logger 'trx;LogFileName=handoff.trx' --results-directory $output *> (Join-Path $output 'run.log') + $testExit=$LASTEXITCODE + } finally {foreach($name in $saved.Keys){[Environment]::SetEnvironmentVariable($name,$saved[$name])}} + foreach($group in $groups.Keys) { + & (Join-Path $output 'observer.exe') (Join-Path $output "$group.rows") (Join-Path $output "$group.reference") $groups[$group].cases (Join-Path $output "$group.reference.json") *> (Join-Path $output "$group.reference.log") + if($LASTEXITCODE -ne 0){throw "Reference comparison failed: $group"} + } + $identity=@{schema=1;sourceCommit=(& git -C $repo rev-parse HEAD);cpuCommittedTree=(& git -C $repo rev-parse HEAD:Copper68k); + reference='WinUAE-generated-040-RTE-SR-handoff';referenceCommit=$pin;referenceDirectory=$source; + softwareReferenceExecuted=$true;fullExceptionOracle=$false;hardwareQualified=$false;roadmapComplete=$false; + compiler=$compiler;compilerFlags='/O2 /EHsc /DWIN32 /DUNICODE /D_UNICODE; observer /W4'; + extraction='Untouched generated op_4e73_94_test_ff and cputest MakeFromSR_x/T0/MakeFromSR; CPU_TESTER configuration only'; + manual='https://www.nxp.com/docs/en/reference-manual/MC68040UMAD.pdf';manualRule='General operation item 3, traced user return saved S bit';inputs=@();referenceInputs=@();generated=@();evidence=@()} + foreach($file in (@(& git -C $repo ls-files 'Copper68k/*') + $fixtures)){$identity.inputs+=@{file=$file;sha256=(Hash (Join-Path $repo $file))}} + foreach($file in @('build68k.cpp','table68k','gencpu.cpp','readcpu.cpp','missing.cpp','od-win32/unicode.cpp','cputest.cpp') + @(& git -C $source ls-files 'include/*' 'od-win32/sysconfig.h')){$identity.referenceInputs+=@{file=$file;sha256=(Hash (Join-Path $source $file))}} + foreach($file in $generated){$identity.generated+=@{file=$file;sha256=(Hash (Join-Path $output $file))}} + foreach($file in @('handoff.trx') + @($groups.Keys|ForEach-Object {"68040-$_.json";"$_.rows";"$_.reference";"$_.reference.json"})){$identity.evidence+=@{file=$file;sha256=(Hash (Join-Path $output $file))}} + $identity|ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'identities.json') + if($testExit -ne 0){throw 'Synthetic comparison failed'} +} +$identity=Get-Content (Join-Path $output 'identities.json') -Raw|ConvertFrom-Json +if($identity.schema -ne 1 -or $identity.reference -cne 'WinUAE-generated-040-RTE-SR-handoff' -or $identity.referenceCommit -cne $pin -or + $identity.softwareReferenceExecuted -ne $true -or $identity.fullExceptionOracle -ne $false -or $identity.hardwareQualified -ne $false -or + $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.cpuCommittedTree -notmatch '^[a-f0-9]{40}$'){throw 'Incomplete reference identity'} +$required=@(& git -C $repo ls-files 'Copper68k/*') + $fixtures +if(@($identity.inputs).Count -ne $required.Count -or @(Compare-Object ($required|Sort-Object) ($identity.inputs.file|Sort-Object)).Count){throw 'Missing fixture/CPU identity'} +foreach($row in $identity.inputs){if((Hash (Join-Path $repo $row.file)) -cne $row.sha256){throw "Changed fixture/CPU input: $($row.file)"}} +if(@($identity.generated).Count -ne $generated.Count -or @(Compare-Object ($generated|Sort-Object) ($identity.generated.file|Sort-Object)).Count){throw 'Missing generated reference identity'} +foreach($row in $identity.generated){if((Hash (Join-Path $output $row.file)) -cne $row.sha256){throw 'Changed generated reference input'}} +$requiredEvidence=@('handoff.trx') + @($groups.Keys|ForEach-Object {"68040-$_.json";"$_.rows";"$_.reference";"$_.reference.json"}) +if(@($identity.evidence).Count -ne $requiredEvidence.Count -or @(Compare-Object ($requiredEvidence|Sort-Object) ($identity.evidence.file|Sort-Object)).Count){throw 'Missing reference evidence identity'} +foreach($row in $identity.evidence){if((Hash (Join-Path $output $row.file)) -cne $row.sha256){throw 'Changed reference evidence'}} +$requiredReference=@('build68k.cpp','table68k','gencpu.cpp','readcpu.cpp','missing.cpp','od-win32/unicode.cpp','cputest.cpp') + @(& git -C $source ls-files 'include/*' 'od-win32/sysconfig.h') +if((& git -C $source rev-parse HEAD) -cne $pin -or (& git -C $source status --porcelain) -or @($identity.referenceInputs).Count -ne $requiredReference.Count -or + @(Compare-Object ($requiredReference|Sort-Object) ($identity.referenceInputs.file|Sort-Object)).Count){throw 'Missing or changed pinned reference source'} +foreach($row in $identity.referenceInputs){if((Hash (Join-Path $source $row.file)) -cne $row.sha256){throw 'Changed reference source input'}} +[xml]$trx=Get-Content (Join-Path $output 'handoff.trx') -Raw +$c=$trx.TestRun.ResultSummary.Counters +if([int]$c.total -ne 4 -or [int]$c.executed -ne 4 -or [int]$c.passed -ne 4 -or [int]$c.failed -ne 0 -or [int]$c.notExecuted -ne 0){throw 'Incomplete/failed handoff selection'} +$total=0 +$canonicalKeys=CombinationKeys $false +$structuralKeys=CombinationKeys $true +if($canonicalKeys.Count -ne 2592 -or $structuralKeys.Count -ne 269568){throw 'Independent combination enumeration differs'} +foreach($group in $groups.Keys) { + $r=Get-Content (Join-Path $output "68040-$group.json") -Raw|ConvertFrom-Json -AsHashtable + $expected=$groups[$group] + if($r.schema -ne 1 -or $r.model -cne '68040' -or $r.group -cne $group -or $r.xunitBatches -ne 1 -or $r.logicalCases -ne $expected.cases -or + $r.combinations.Count -ne $expected.combinations -or $r.counts.passing -ne $expected.cases -or $r.counts.mismatching -ne 0 -or $r.counts.unsupported -ne 0 -or $r.counts.untested -ne 0){throw "Incomplete handoff coverage: $group"} + $keys=$canonicalKeys + if($group.Contains('structure')){$keys=$structuralKeys} + foreach($key in $r.combinations.Keys){$statuses=$r.combinations[$key];if(-not $keys.Contains($key) -or $statuses.Count -ne 1 -or $statuses.passing -ne $expected.weight){throw "Incomplete combination: $key"}} + $reference=Get-Content (Join-Path $output "$group.reference.json") -Raw|ConvertFrom-Json + if($reference.cases -ne $expected.cases -or $reference.mismatches -ne 0 -or $reference.fixedControls -ne 2 -or $reference.fullExceptionOracle -ne $false -or $reference.hardwareQualified -ne $false){throw 'Incomplete reference comparison'} + $total+=$r.logicalCases +} +@{schema=1;logicalCases=$total;batches=4;passing=$total;mismatching=0;unsupported=0;untested=0; + softwareReferenceExecuted=$true;referenceCommit=$pin;fullExceptionOracle=$false;hardwareQualified=$false;roadmapComplete=$false; + scope='Chained odd-PC format 0/2/3 SR handoff, stack consumption and read order; architectural format-2/S-bit composition'; + remaining='Format-7 pending/foreign context, original-trace fault/retry, internal/data restart and broader model reference/consolidation'}| + ConvertTo-Json -Depth 5|Set-Content (Join-Path $output 'qualification-summary.json') +Write-Output "Qualified $total software handoffs in four batches; full exception/hardware qualification and roadmap remain incomplete." diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index 860d608a..41dad0e1 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','RteSoftwareTrace','UserRteFault','UserRteTrace','UserRteSoftwareTrace','MixedEpochRte','MixedEpochFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','ChainedOddRte','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','RteSoftwareTrace','UserRteFault','UserRteTrace','UserRteSoftwareTrace','MixedEpochRte','MixedEpochFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -555,12 +555,27 @@ foreach ($mutation in $userRteMutations) { $mutation.file='Copper68k/M68040Support.cs'; $mutation.model='68040'; $mutation.milestone=6 $mutation.group='UserTailFaultsAndBareReturnsUseLiveStatus'; $mutation.userRte=$true; $mutations += $mutation } +foreach ($mutation in @( + @{name='chained-odd-final-sr'; file=$advanced; before='if (TryRaiseM68040RteAddressError(restoredPc, priorSr, instructionPc)) return;'; + after='if (TryRaiseM68040RteAddressError(restoredPc, restoredStatus, instructionPc)) return;'; + proofCombination='/result=user/incoming=0000/first=0000/second=none/T=0000/'}, + @{name='chained-odd-lost-committed-trace'; file=$advanced; before=' var priorSr = State.StatusRegister;'; + after=' var priorSr = (ushort)(State.StatusRegister & ~0xc000);'; + proofCombination='/result=user/incoming=0000/first=8000/second=none/T=0000/'}, + @{name='chained-odd-user-s-bit'; file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs'; before=' priorSr |= M68kCpuState.Supervisor;'; + after=' priorSr |= 0;'; + proofCombination='/result=user/incoming=0000/first=0000/second=none/T=8000/'} +)) { + $mutation.group='ChainedOddShortFrameCanonical'; $mutation.model='68040'; $mutation.milestone=6 + $mutation.chainedOdd=$true; $mutations += $mutation +} if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } if ($Scope -eq 'Logical') { $mutations = @($mutations | Where-Object { $_.milestone -eq 4 }) } if ($Scope -eq 'Control') { $mutations = @($mutations | Where-Object { $_.milestone -eq 5 }) } if ($Scope -eq 'Consolidation') { $mutations = @($mutations | Where-Object { $_.milestone -eq 6 }) } if ($Scope -eq 'Rte040') { $mutations = @($mutations | Where-Object { $_.odd }) } +if ($Scope -eq 'ChainedOddRte') { $mutations = @($mutations | Where-Object { $_.chainedOdd }) } if ($Scope -eq 'RteValidationFault') { $mutations = @($mutations | Where-Object { $_.validationFault }) } if ($Scope -eq 'RteRepair') { $mutations = @($mutations | Where-Object { $_.repair }) } if ($Scope -eq 'RteRetryTrace') { $mutations = @($mutations | Where-Object { $_.retryTrace }) } @@ -618,6 +633,17 @@ try { $batches = @(Get-ChildItem -LiteralPath $directory -Filter '*.json' | ForEach-Object { Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json }) $failures = @($batches | ForEach-Object { $_.failures }) if ($exitCode -eq 0 -or $batches.Count -eq 0 -or $failures.Count -eq 0) { throw "Mutation survived or failed without executable semantic evidence: $($mutation.name)" } + if ($mutation.chainedOdd) { + [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + $c=$trx.TestRun.ResultSummary.Counters + if ($c.total -ne 2 -or $c.executed -ne 2 -or $c.failed -ne 2 -or $c.notExecuted -ne 0 -or $batches.Count -ne 2) { throw 'Chained odd mutation omitted complete selections' } + foreach ($route in @('scalar','batch')) { + $r=@($batches | Where-Object group -CEQ "rte-chained-odd-canonical-$route") + if ($r.Count -ne 1 -or $r[0].logicalCases -ne 82944 -or @($r[0].combinations.PSObject.Properties).Count -ne 2592 -or + @($r[0].failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination,[StringComparison]::Ordinal) -and + $_.reason.StartsWith('saved SR provenance',[StringComparison]::Ordinal) }).Count -eq 0) { throw "Missing intended chained odd $route diagnostic" } + } + } if ($mutation.throwaway) { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if ($trx.TestRun.ResultSummary.Counters.executed -ne 2 -or $trx.TestRun.ResultSummary.Counters.failed -ne 2 -or diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 20f817e8..c2e1e348 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -186,6 +186,10 @@ try { $expected['rte-mixed-fault-structure-scalar'] = 3556800 $expected['rte-mixed-fault-structure-batch'] = 3556800 $expected['rte-odd-normal'] = 69120 + $expected['rte-chained-odd-canonical-scalar'] = 82944 + $expected['rte-chained-odd-canonical-batch'] = 82944 + $expected['rte-chained-odd-structure-scalar'] = 539136 + $expected['rte-chained-odd-structure-batch'] = 539136 $expected['rte-odd-pending'] = 165888 $expected['address-error-fetch-040'] = 2304 $expected['rte-throwaway-controls'] = 82944 @@ -328,6 +332,7 @@ try { m68040RtePendingTraceScope=@{logicalCases=1824768; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); forms='CT/CU/CP49'; incoming=@(0,0x8000,0x4000); restoredTrace=@(0,0x8000,0x4000); faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); qualified='Three original-frame stores preserve access SR; returned incoming trace cannot create an extra RTE trace on pending conversion; saved SR/PC/EA, format/vector, pending consumption, handler return and following traced BRA are checked'; ccr='all 32 canonical, 0/31 each validation-read byte'; remaining='mixed-epoch provenance, internal restoration and physical/hardware qualification'}; m68040RteRetryTraceScope=@{logicalCases=2729088; batches=4; translationEnabled=$false; instructionCacheEnabled=$false; routes=@('accurate scalar','one-instruction batch'); faultTailBanks=@('ISP','MSP'); incoming=@(0,0x8000,0x4000); restoredBanks=@('user','ISP','MSP'); restoredTrace=@(0,0x8000,0x4000); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM'; qualified='Three original-frame repair stores leave saved access SR untouched; handler return restores incoming trace; successful original RTE triggers T1/T0 trace with restored SR/PC; trace-handler return and following instruction retain MOVEM EA and stack selection'; ccr='all 32 canonical, 0/31 every chained read byte'; remaining='mixed trace values within consumed throwaways, internal restoration and hardware/physical timing'}; m68040MixedEpochFaultScope=@{logicalCases=7666560; batches=4; routes=@('accurate scalar','one-instruction batch'); qualified='Independent incoming/first/second trace states; all banks and user M aliases; canonical final trace/CCR and structural every validation-read byte; last committed SR, bare handler return, user privilege failure and retained original pending vector'; remaining='Executed mixed-epoch repair/retry, original instruction trace suspension/resumption, internal restoration and hardware capture'}; + m68040ChainedOddScope=@{logicalCases=1244160; batches=4; routes=@('accurate scalar','one-instruction batch'); forms='0/2/3'; qualified='One/two throwaways, all banks/user M aliases, independent trace epochs, CCR, SR provenance, consumed stacks, frame reads and format-2 address-error image'; referenceCommand='scripts/test-copper68k-040-rte-handoff.ps1'; referenceScope='Untouched generated WinUAE 040 RTE and cputest SR helper handoff; documented frame/S-bit composition'; remaining='Format7 continuations/foreign context, full reference exception entry and hardware qualification'}; m68040AccessFrameScope='Synthetic normal/CT/CM/CU/CP returns with original pending vectors, every legal MOVEM word and full-index structure, one/two throwaways selecting all stacks, successful mixed-epoch chains and validation fault capture, direct odd-PC returns, supervisor-stack physical RTE validation fault entry/handler return, executed repair/retry after committed supervisor throwaways, and manual-derived user-tail fault/privilege/bridge repair; chained odd-PC saved-SR provenance, internal-restoration faults, mixed-epoch repair/retry and original trace suspension/resumption, CP context transfer, other real fault entry and physical timing remain unqualified'; m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='Internal restoration, mixed-epoch trace provenance and other real access faults; normal/CM preserved-trace and user-tail repair are qualified separately'}; m68040InstructionFaultScope=@{logicalCases=116736; batches=2; translationEnabled=$false; instructionCacheEnabled=$false; engine='accurate public factory plus cached self-branch batch'; forms=@('opcode','extension-low','next-opcode','self-branch'); banks=@('user','ISP','MSP'); ccr='all 32'; trace='All defined incoming states for frame/return; restart trace zero'; fields='Format7 SR, executing PC distinct from prefetch FA, RW/SIZE/TM and invalid writebacks'; phases=@('fault entry','handler return','restart','following'); remaining='Data writebacks/restart, speculative prefetch deferral, compiled instruction-fault PC provenance, cache-enabled fetches and physical timing'}; From 826351ff0709d2e990b6b62692f068038e06af75 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 12:50:07 +0300 Subject: [PATCH 061/211] Qualify chained 68040 access-frame SR handoff --- Copper68k.Tests/Synthetic/README.md | 25 +++++- .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- .../SyntheticM68040ChainedOddReturnTests.cs | 51 +++++++++-- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 84 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 29 +++++++ scripts/reference/m68040-rte-handoff.cpp | 25 ++++-- scripts/test-copper68k-040-access-frames.ps1 | 17 ++-- scripts/test-copper68k-040-rte-handoff.ps1 | 23 +++-- .../test-copper68k-synthetic-mutations.ps1 | 38 +++++++-- scripts/test-copper68k-synthetic.ps1 | 4 + 10 files changed, 264 insertions(+), 34 deletions(-) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 451db6e1..e05d1766 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -852,7 +852,7 @@ The default location is `artifacts/reference-winuae-rte-modern`; the command requires PowerShell 7 and MSVC (override `-VcVars` for a different installation). It builds the pinned generator with CPU_TESTER enabled, extracts the untouched generated 040 RTE and cputest SR helpers, and executes their secondary-SR -handoff in a transport harness. Two literal controls distinguish the original, +handoff in a transport harness. Three literal controls distinguish the original, last-throwaway and final SR. The handoff/live state and frame reads are compared with CPU results; documented format-2 entry and the traced-user S-bit correction are composed separately. This is an executed software handoff reference, not @@ -864,7 +864,7 @@ and zero mismatches/unsupported/untested cases. Missing inputs, empty selections and out-of-scope reference operations fail. Recheck frozen outputs using `-ValidateReportsOnly -OutputDirectory`. Never substitute this observer for a validation bus-fault oracle: the generated 040 test RTE does not handle that -fault protocol. Format7 continuations/foreign context and the original-trace +fault protocol. Format7 pending/foreign context and the original-trace retry discovery remain explicit requirements. `./scripts/test-copper68k-synthetic-mutations.ps1 -Scope ChainedOddRte` proves @@ -872,3 +872,24 @@ that both complete canonical batches detect substitution of the final SR, loss of the committed trace bits and omission of the traced-user S correction. It requires the intended saved-SR diagnostics and restores/rebuilds production source. No old hardware/integration regression is retired by this qualification. + +Use `-AccessFrames` to execute the four normal/CM format7 groups: canonical +scalar/batch each 55,296 cases / 1,728 combinations, structural scalar/batch +each 359,424 / 179,712. The observer qualifies SR handoff, 60-byte consumption +and header reads. It receives declared SSW/EA inputs and rejects CT/CU/CP or +undefined continuation flags, because its generated RTE skips their protocol. +Separate synthetic checks cover SSW/CM-EA validation order, writeback guards, +the address-error image and absence of an armed MOVEM continuation after the +odd return. These checks do not acquire a full external continuation oracle +merely by agreeing on the header handoff. + +Revalidation of access-profile outputs also requires `-AccessFrames`. +`-Scope ChainedOddAccessRte` adds five maintained complete canonical probes: +final-SR substitution, lost committed trace, omitted saved S, skipped CM EA +read and premature MOVEM continuation before address-error delivery. Pending +and foreign-context odd-return chains remain in the 480-case required inventory. +The no-continuation assertion preserves Copper68k's existing policy; the MMU +WinUAE helper arms CM before the odd-PC check, whereas this non-MMU observer +skips that helper. Its passing header comparison does not settle CM lifetime +across address-error delivery/software repair. That protocol remains required +reference research, as recorded in the qualification document and plan. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 209aa708..b5dc0963 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -37,7 +37,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() : form == "real-access-fault-entry" ? "Cache-disabled accurate instruction-fetch frames/restart are covered separately; data fault/writeback restart, enabled-cache deferral and compiled instruction-PC provenance remain required" : form == "odd-PC-chained-SR-provenance" - ? "Chained format0/2/3 SR handoff, stack consumption and reads have a separate generated WinUAE software reference; format7 normal/CM and pending/foreign-context chains remain required. Full reference exception entry and hardware behavior remain unqualified" + ? "Chained format0/2/3 and format7 normal/CM SR handoff, stack consumption and header reads have separate generated WinUAE software coverage; access-frame validation/no-replay checks are synthetic. CT/CU/CP pending/foreign-context chains, full reference exception entry and hardware behavior remain unqualified" : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040ChainedOddReturnTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040ChainedOddReturnTests.cs index 508e720c..2350e405 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040ChainedOddReturnTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040ChainedOddReturnTests.cs @@ -22,10 +22,18 @@ public sealed class SyntheticM68040ChainedOddReturnTests(ITestOutputHelper outpu public void ChainedOddShortFrameStructuralScalar() => Audit(true, false); [Fact, Trait("Suite", "Synthetic")] public void ChainedOddShortFrameStructuralBatch() => Audit(true, true); + [Fact, Trait("Suite", "Synthetic")] + public void ChainedOddAccessFrameCanonicalScalar() => Audit(false, false, true); + [Fact, Trait("Suite", "Synthetic")] + public void ChainedOddAccessFrameCanonicalBatch() => Audit(false, true, true); + [Fact, Trait("Suite", "Synthetic")] + public void ChainedOddAccessFrameStructuralScalar() => Audit(true, false, true); + [Fact, Trait("Suite", "Synthetic")] + public void ChainedOddAccessFrameStructuralBatch() => Audit(true, true, true); - private void Audit(bool structural, bool batch) + private void Audit(bool structural, bool batch, bool access = false) { - var group = $"rte-chained-odd-{(structural ? "structure" : "canonical")}-{(batch ? "batch" : "scalar")}"; + var group = $"rte-chained-odd-{(access ? "access-" : "")}{(structural ? "structure" : "canonical")}-{(batch ? "batch" : "scalar")}"; var report = new CoverageBatch("68040", group); var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); var export = Environment.GetEnvironmentVariable("COPPER68K_040_RTE_HANDOFF_EXPORT"); @@ -42,13 +50,13 @@ private void Audit(bool structural, bool batch) foreach (var target in structural ? new uint[] { 0x6001, 0xff002003 } : [0x6001]) foreach (var alignment in structural ? new uint[] { 0, 1 } : [0]) foreach (var vbr in structural ? new uint[] { 0, 0x10000 } : [0x10000]) - foreach (var format in new[] { 0, 2, 3 }) + foreach (var form in access ? new[] { "normal", "CM" } : ["format0", "format2", "format3"]) foreach (var ccr in structural ? new[] { 0, 31 } : Enumerable.Range(0, 32)) { m.Reset(ccr); var pointers = new Dictionary { ["user"] = 0x7800 + alignment, ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; foreach (var location in pointers.Values) - for (var n = -20; n < 80; n++) m.InitializePhysical(unchecked(location + (uint)n), (uint)(n ^ 0x5a), 1); + for (var n = -20; n < 112; n++) m.InitializePhysical(unchecked(location + (uint)n), (uint)(n ^ 0x5a), 1); var path = middle == "none" ? new[] { start, tail } : new[] { start, middle, tail }; var frames = new List<(uint Address, ushort Sr, uint Pc, ushort Format)>(); for (var n = 0; n < path.Length - 1; n++) @@ -58,13 +66,27 @@ private void Audit(bool structural, bool batch) pointers[Physical(path[n])] += 8; } var restoredSr = Status(result, trace, ccr ^ 31); + var format = access ? 7 : form == "format0" ? 0 : form == "format2" ? 2 : 3; frames.Add((pointers[Physical(tail)], restoredSr, target, (ushort)(format << 12 | 8))); foreach (var f in frames) { m.InitializePhysical(f.Address, f.Sr, 2); m.InitializePhysical(f.Address + 2, f.Pc, 4); m.InitializePhysical(f.Address + 6, f.Format, 2); m.InitializePhysical(f.Address + 8, 0x4200, 4); } - pointers[Physical(tail)] += format == 0 ? 8u : 12u; + var tailFrame = frames[^1].Address; + var ssw = form == "CM" ? 0x1005u : 0x0105u; + if (access) + { + m.InitializePhysical(tailFrame + 12, ssw, 2); + foreach (var offset in new[] { 14u, 16u, 18u }) m.InitializePhysical(tailFrame + offset, 0x85, 2); + foreach (var offset in new[] { 24u, 32u, 40u }) + { + m.InitializePhysical(tailFrame + offset, 0x3200 + offset, 4); + m.InitializePhysical(tailFrame + offset + 4, 0xc3a55a3c, 4); + m.InitializePhysical(0x3200 + offset, 0x96abcdef, 4); + } + } + pointers[Physical(tail)] += access ? 60u : format == 0 ? 8u : 12u; _ = SyntheticExecution.Prepare(m, [0x4e73]); m.InitializePhysical(vbr + 12, Handler, 4); m.InitializePhysical(Handler, 0x60fe, 2); m.Core.State.SetUserStackPointer(0x7800 + alignment); @@ -91,19 +113,32 @@ private void Audit(bool structural, bool batch) e.ControlChecks["odd targets never fetched"] = (_ => m.Bus.Accesses.Any(a => !a.Write && (a.Address == target || frames.Take(frames.Count - 1).Any(f => f.Pc == a.Address))) ? 1u : 0u, 0); var expectedReads = frames.SelectMany(f => new[] { (f.Address, 2), (f.Address + 2, 4), (f.Address + 6, 2) }).ToArray(); + var validationReads = expectedReads.Concat(access ? form == "CM" ? new[] { (tailFrame + 12, 2), (tailFrame + 8, 4) } + : [(tailFrame + 12, 2)] : []).ToArray(); var addresses = expectedReads.Select(r => r.Item1).ToHashSet(); - e.ControlChecks["frame validation read order"] = (_ => m.Bus.Accesses.Where(a => !a.Write && addresses.Contains(a.Address)) - .Select(a => (a.Address, a.Width)).SequenceEqual(expectedReads) ? 0u : 1u, 0); + var validationAddresses = validationReads.Select(r => r.Item1).ToHashSet(); + e.ControlChecks["frame validation read order"] = (_ => m.Bus.Accesses.Where(a => !a.Write && validationAddresses.Contains(a.Address)) + .Select(a => (a.Address, a.Width)).SequenceEqual(validationReads) ? 0u : 1u, 0); + if (access) + { + e.ControlChecks["no MOVEM continuation after odd return"] = (_ => ((M68kAdvancedTimingInterpreter)m.Core).HasPendingM68040MovemContinuation ? 1u : 0u, 0); + e.ControlChecks["no writeback replay"] = (_ => m.Bus.Accesses.Any(a => a.Address is 0x3218 or 0x3220 or 0x3228) ? 1u : 0u, 0); + } e.Write(sp, savedSr, 2, m.Model); e.Write(sp + 2, SyntheticMachine.Code, 4, m.Model); e.Write(sp + 6, 0x200c, 2, m.Model); e.Write(sp + 8, target & ~1u, 4, m.Model); var secondName = middle == "none" ? "none" : $"{second:X4}"; - var id = $"68040/RTE/chained-odd/format{format}/path={string.Join('-', path)}/result={result}/incoming={incoming:X4}/first={first:X4}/second={secondName}/T={trace:X4}/target={target:X8}/align={alignment}/VBR={vbr:X8}/op=4E73/ccr={ccr:X2}"; + var id = $"68040/RTE/chained-odd/{form}/path={string.Join('-', path)}/result={result}/incoming={incoming:X4}/first={first:X4}/second={secondName}/T={trace:X4}/target={target:X8}/align={alignment}/VBR={vbr:X8}/op=4E73/ccr={ccr:X2}"; M68040StackFixture.Step(m, e, report, id, batch); if (rows != null) { var s = m.Core.State; var input = new List { (uint)index, initialSr, 0x7800 + alignment, 0x4700 + alignment, 0x7400 + alignment, SyntheticMachine.Code, (uint)frames.Count }; foreach (var f in frames) input.AddRange([f.Address, f.Sr, f.Pc, f.Format]); + // Reference observes header/SR handoff only. Declare the SSW/EA + // so it rejects pending/undefined forms rather than silently + // treating them as normal. Extra validation reads above are + // checked independently by the architectural fixture. + if (access) input.AddRange([ssw, 0x4200]); var actual = new List { s.LastExceptionStatusRegister, s.StatusRegister, s.ProgramCounter, s.LastExceptionStackedProgramCounter, m.PeekPhysical(s.A[7] + 8, 4), s.UserStackPointer, s.InterruptStackPointer, s.MasterStackPointer, (uint)expectedReads.Length }; foreach (var a in m.Bus.Accesses.Where(a => !a.Write && addresses.Contains(a.Address))) actual.AddRange([a.Address, (uint)a.Width]); diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 0e3d7268..305b0462 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -5587,3 +5587,87 @@ repair/retry, internal restoration, real data/writeback restart, earlier model reference gaps and consolidation remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`. No old regression is retired, public API changed, consumer dependency advanced or package published. + +### Chained odd-PC normal/CM access-frame handoff + +The same fixture now covers normal and CM format7 tails. Source/destination +stack aliases and all independent trace epochs retain the short-frame matrix; +the access frame additionally populates guarded WB status/data, checks SSW and +CM saved-EA validation order, consumes sixty bytes and takes the composed +format-2 address error without replaying software-owned writebacks. The existing +Copper68k policy of not retaining a MOVEM continuation after an odd return is +checked separately; the native observer cannot qualify that continuation lifetime. + +| Fresh access report | Passing cases | Combinations | +| --- | ---: | ---: | +| Canonical scalar | 55,296 | 1,728 | +| Canonical batch | 55,296 | 1,728 | +| Structural scalar | 359,424 | 179,712 | +| Structural batch | 359,424 | 179,712 | +| Total | 829,440 | 362,880 | + +Release build succeeds with zero .NET warnings/errors. Four access tests execute +and pass, with zero skips; every report has zero mismatches, unsupported or +untested cases. Canonical combinations have all 32 initial CCRs, structural +combinations have CCR 0/31. Every combination and weight is independently +enumerated. All 829,440 observed header/SR/stack handoffs agree with pinned +generated WinUAE. The current short profile is also rerun after the fixture/ +observer changes: four tests pass without skips and all 1,244,160 cases agree. +Together the two current profiles contain 2,073,600 passing software cases. +Native compilation retains the unused `oldpc` warning from untouched source. + +The explicit `-AccessFrames` profile records its identity and accepts only +declared normal/CM SSW values. The generated test RTE skips the SSW/EA protocol, +so the observer compares only its header reads and SR/stack handoff. Synthetic +validation/no-replay checks and documented frame/S-bit composition remain +separate. No SSW helper or production continuation calculation is injected into +the reference. Three literal native controls include one/two-throwaway short +frames and the sixty-byte access tail. CT/CU/CP and undefined continuation +inputs are rejected; wrong-profile report revalidation is also rejected. + +This boundary matters for CM lifetime. The pinned MMU RTE generator calls +`m68k_do_rte_mmu040` before checking the odd return; that helper arms its MOVEM +restart state when CM is set. The non-MMU test generator used by this observer +does not call it. This documentary difference is retained as required +continuation/fault-lifetime research, not hidden by a passing header comparison +or classified as a proven non-MMU CPU defect. Full external restoration, +CM state across software repair, hardware behavior and physical timing are not +qualified by this observer. Enabled-MMU operation remains outside the roadmap. + +Five maintained `ChainedOddAccessRte` probes each execute both complete +canonical groups (55,296 cases / 1,728 combinations per route) with two failing +tests, zero skips and the intended diagnostic: + +| Probe | Mismatches per route | Diagnostic | +| --- | ---: | --- | +| Substitute final restored SR | 55,296 | Saved SR 0000 versus 001F | +| Lose committed trace | 36,864 | Saved SR 8000 versus 0000 | +| Omit traced-user saved S | 9,216 | Saved SR 2000 versus 0000 | +| Skip CM saved-EA validation | 27,648 | Validation read order | +| Arm MOVEM before odd-PC delivery | 27,648 | Retained continuation policy | + +Production source bytes are restored and rebuilt. Twelve intended negative +controls reject missing identity/fixture identity, empty xUnit selection, +missing combination, wrong weight, native missing/empty fixtures, CT/CU/CP/ +undefined SSWs and mismatched profile. The required inventory executes one +expected failing test, zero skips, retaining 480 untested cases / fifteen +combinations. Its odd-PC entry distinguishes qualified header/SR coverage from +the pending/foreign-context and full continuation behavior still required. + +Frozen acceptance source starts at `2f66bcaa03d9e34efa82df80634003f2bfbf8c96` +plus these test/command changes; production tree remains +`795fd12d6c0237a22a5f92f4a96cc4823e0364c1`. +CPU SHA256: `141b6fc84c140873d46e4f6bf49f909ac8d6b86ba506b3efcbffac92c0fb4db8`. +Adapter SHA256: `e84d20e2e3c49a73bc976091c415f3327363e1780fa76ef9174c3f2bf19e126e`. +`artifacts/m6-chained-access-reference-final/identities.json` and its summary +record current fixture/source/extracted-code/binary/row/evidence identities and +the explicit access profile. Current short-profile evidence is separately +`artifacts/m6-chained-access-short-retained/`; earlier snapshots are not relabeled. +Expanded ordinary/complete-040 requirements are recorded in the plan; older +broad CPU, complete selection and consumer results are not newly rerun here. + +Pending/foreign odd-return context, CM fault/repair lifetime, original-trace +repair/retry, internal/data/writeback restoration, broader model reference +qualification and consolidation remain required. Milestone 6 stays **in +progress**, `roadmapComplete=false`. No CPU source change, package publication +or additional regression retirement is included. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index c1bd0cbd..82c6bc5e 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2166,3 +2166,32 @@ requirements, mixed-epoch repair/retry and original-instruction trace deferral, internal restoration, data/writeback restart, broader model reference audits and consolidation. Milestone 6 stays **in progress**, `roadmapComplete=false`. No production CPU fix, package publication or regression retirement is included. + +### Milestone 6 chained odd-PC normal/CM access frames + +Four additional ordinary matrices extend one/two-throwaway odd returns to +normal and CM format7 tails: canonical scalar/batch each require 55,296 cases / +1,728 combinations; structural scalar/batch each require 359,424 / 179,712. +All banks/user-M aliases, independent trace epochs, canonical CCRs, structural +CCR boundaries/alignments/VBRs/low-high odd PCs remain covered. The added +829,440 cases check 60-byte consumption, SSW/CM-EA validation ordering, guarded +writebacks, last committed saved-SR provenance and no MOVEM continuation after +odd-PC delivery. + +The native handoff command gains an explicit `-AccessFrames` profile. It observes +only generated WinUAE SR/stack/header behavior and composes address-error entry +with documented rules. The generated test RTE skips SSW/EA continuation work; +therefore it rejects CT/CU/CP and undefined forms, and never certifies full +format7 restoration or hardware behavior. Three literal native controls and +five complete canonical mutation probes protect the qualified and synthetic +portions separately. Profile identity is mandatory on report revalidation. + +Ordinary coverage now requires 82,614,166 cases / 672 batches. The complete 040 +gate requires 95 tests / 82 reports / thirteen fixed examples and 67,645,984 +phases including the unchanged 480-case inventory; fifteen fixture/command +identities remain mandatory. Expanded counts are requirements, not a relabeling +of earlier broad executions. CM continuation lifetime across odd-return fault/ +software repair, pending/foreign-context odd-return chains, +original-trace fault/retry, internal/data/writeback restoration, broader model +reference audits and consolidation remain required. Milestone 6 stays **in +progress**, `roadmapComplete=false`; no CPU fix, release or regression retirement. diff --git a/scripts/reference/m68040-rte-handoff.cpp b/scripts/reference/m68040-rte-handoff.cpp index 8c539301..1da5c5e4 100644 --- a/scripts/reference/m68040-rte-handoff.cpp +++ b/scripts/reference/m68040-rte-handoff.cpp @@ -88,6 +88,15 @@ void fixedControls() { if (callbackCount!=1 || handoffSr!=0x5000 || regs.sr!=0x301f || regs.a[7]!=0x7409 || regs.usp!=0x7809 || regs.isp!=0x4709 || faultPc!=0xff002003 || reads.size()!=18) throw std::runtime_error("Fixed two-throwaway handoff failed"); + regs={}; memory.clear(); reads.clear(); callbackCount=0; + regs.sr=0x201f; regs.s=1; regs.a[7]=regs.isp=0x4700; + regs.usp=0x7800; regs.msp=0x7400; regs.pc=0x1000; + put(0x4700,0x501f,2); put(0x4702,0xdead0001,4); put(0x4706,0x1024,2); + put(0x7800,0x301f,2); put(0x7802,0xff002003,4); put(0x7806,0x7008,2); + op_4e73_94_test_ff(0x4e73); + if (callbackCount!=1 || handoffSr!=0x501f || regs.sr!=0x301f || regs.a[7]!=0x7400 || + regs.usp!=0x783c || regs.isp!=0x4708 || reads.size()!=12) + throw std::runtime_error("Fixed access-frame handoff failed"); } int main(int argc,char** argv) { @@ -103,8 +112,14 @@ int main(int argc,char** argv) { const auto delimiter=line.find(" | "); if (delimiter==std::string::npos) throw std::runtime_error("Missing actual-state delimiter"); const auto fixture=parse(line.substr(0,delimiter)), actual=parse(line.substr(delimiter+3)); - if (fixture.size()<7 || fixture[0]!=count || fixture[6]<2 || fixture[6]>3 || fixture.size()!=7+4*fixture[6]) + if (fixture.size()<15 || fixture[0]!=count || fixture[6]<2 || fixture[6]>3) throw std::runtime_error("Missing, reordered or invalid fixture"); + const size_t headerEnd=7+4*fixture[6]; + if (fixture.size()>12)==7; + if (fixture.size()!=headerEnd+(access?2:0)) throw std::runtime_error("Unexpected reference frame payload"); + if (access && fixture[headerEnd]!=0x0105 && fixture[headerEnd]!=0x1005) + throw std::runtime_error("Out-of-scope continuation: pending/undefined frames need a full restoration oracle"); regs={}; memory.clear(); reads.clear(); callbackCount=traceActivations=0; flag_SPCFLAG_TRACE=cpu_stopped=0; regs.sr=static_cast(fixture[1]); @@ -112,9 +127,9 @@ int main(int argc,char** argv) { regs.t0=(regs.sr>>14)&1; regs.t1=(regs.sr>>15)&1; regs.intmask=(regs.sr>>8)&7; regs.usp=fixture[2]; regs.isp=fixture[3]; regs.msp=fixture[4]; regs.pc=fixture[5]; regs.a[7]=regs.s ? regs.m ? regs.msp : regs.isp : regs.usp; - for (size_t n=7;n>12; - if ((n+4 (Join-Path $output 'run.log') + $filter=if($AccessFrames){'FullyQualifiedName~ChainedOddAccessFrame'}else{'FullyQualifiedName~ChainedOddShortFrame'} + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --no-build --no-restore --filter $filter --logger 'trx;LogFileName=handoff.trx' --results-directory $output *> (Join-Path $output 'run.log') $testExit=$LASTEXITCODE } finally {foreach($name in $saved.Keys){[Environment]::SetEnvironmentVariable($name,$saved[$name])}} foreach($group in $groups.Keys) { @@ -98,7 +106,7 @@ if(-not $ValidateReportsOnly) { } $identity=@{schema=1;sourceCommit=(& git -C $repo rev-parse HEAD);cpuCommittedTree=(& git -C $repo rev-parse HEAD:Copper68k); reference='WinUAE-generated-040-RTE-SR-handoff';referenceCommit=$pin;referenceDirectory=$source; - softwareReferenceExecuted=$true;fullExceptionOracle=$false;hardwareQualified=$false;roadmapComplete=$false; + softwareReferenceExecuted=$true;fullExceptionOracle=$false;hardwareQualified=$false;roadmapComplete=$false;accessFrames=[bool]$AccessFrames; compiler=$compiler;compilerFlags='/O2 /EHsc /DWIN32 /DUNICODE /D_UNICODE; observer /W4'; extraction='Untouched generated op_4e73_94_test_ff and cputest MakeFromSR_x/T0/MakeFromSR; CPU_TESTER configuration only'; manual='https://www.nxp.com/docs/en/reference-manual/MC68040UMAD.pdf';manualRule='General operation item 3, traced user return saved S bit';inputs=@();referenceInputs=@();generated=@();evidence=@()} @@ -113,6 +121,7 @@ $identity=Get-Content (Join-Path $output 'identities.json') -Raw|ConvertFrom-Jso if($identity.schema -ne 1 -or $identity.reference -cne 'WinUAE-generated-040-RTE-SR-handoff' -or $identity.referenceCommit -cne $pin -or $identity.softwareReferenceExecuted -ne $true -or $identity.fullExceptionOracle -ne $false -or $identity.hardwareQualified -ne $false -or $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.cpuCommittedTree -notmatch '^[a-f0-9]{40}$'){throw 'Incomplete reference identity'} +if($identity.accessFrames -ne [bool]$AccessFrames){throw 'Reference profile differs; specify -AccessFrames for format7 outputs'} $required=@(& git -C $repo ls-files 'Copper68k/*') + $fixtures if(@($identity.inputs).Count -ne $required.Count -or @(Compare-Object ($required|Sort-Object) ($identity.inputs.file|Sort-Object)).Count){throw 'Missing fixture/CPU identity'} foreach($row in $identity.inputs){if((Hash (Join-Path $repo $row.file)) -cne $row.sha256){throw "Changed fixture/CPU input: $($row.file)"}} @@ -131,7 +140,7 @@ if([int]$c.total -ne 4 -or [int]$c.executed -ne 4 -or [int]$c.passed -ne 4 -or [ $total=0 $canonicalKeys=CombinationKeys $false $structuralKeys=CombinationKeys $true -if($canonicalKeys.Count -ne 2592 -or $structuralKeys.Count -ne 269568){throw 'Independent combination enumeration differs'} +if($canonicalKeys.Count -ne $(if($AccessFrames){1728}else{2592}) -or $structuralKeys.Count -ne $(if($AccessFrames){179712}else{269568})){throw 'Independent combination enumeration differs'} foreach($group in $groups.Keys) { $r=Get-Content (Join-Path $output "68040-$group.json") -Raw|ConvertFrom-Json -AsHashtable $expected=$groups[$group] @@ -141,12 +150,12 @@ foreach($group in $groups.Keys) { if($group.Contains('structure')){$keys=$structuralKeys} foreach($key in $r.combinations.Keys){$statuses=$r.combinations[$key];if(-not $keys.Contains($key) -or $statuses.Count -ne 1 -or $statuses.passing -ne $expected.weight){throw "Incomplete combination: $key"}} $reference=Get-Content (Join-Path $output "$group.reference.json") -Raw|ConvertFrom-Json - if($reference.cases -ne $expected.cases -or $reference.mismatches -ne 0 -or $reference.fixedControls -ne 2 -or $reference.fullExceptionOracle -ne $false -or $reference.hardwareQualified -ne $false){throw 'Incomplete reference comparison'} + if($reference.cases -ne $expected.cases -or $reference.mismatches -ne 0 -or $reference.fixedControls -ne 3 -or $reference.fullExceptionOracle -ne $false -or $reference.hardwareQualified -ne $false){throw 'Incomplete reference comparison'} $total+=$r.logicalCases } @{schema=1;logicalCases=$total;batches=4;passing=$total;mismatching=0;unsupported=0;untested=0; softwareReferenceExecuted=$true;referenceCommit=$pin;fullExceptionOracle=$false;hardwareQualified=$false;roadmapComplete=$false; - scope='Chained odd-PC format 0/2/3 SR handoff, stack consumption and read order; architectural format-2/S-bit composition'; + accessFrames=[bool]$AccessFrames;scope=$(if($AccessFrames){'Chained odd-PC normal/CM format7 SR handoff, 60-byte consumption and header reads; SSW/EA validation, no CM/writeback replay and address-error frame checked synthetically'}else{'Chained odd-PC format 0/2/3 SR handoff, stack consumption and read order; architectural format-2/S-bit composition'}); remaining='Format-7 pending/foreign context, original-trace fault/retry, internal/data restart and broader model reference/consolidation'}| ConvertTo-Json -Depth 5|Set-Content (Join-Path $output 'qualification-summary.json') Write-Output "Qualified $total software handoffs in four batches; full exception/hardware qualification and roadmap remain incomplete." diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index 41dad0e1..ac89af4c 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','ChainedOddRte','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','RteSoftwareTrace','UserRteFault','UserRteTrace','UserRteSoftwareTrace','MixedEpochRte','MixedEpochFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','ChainedOddRte','ChainedOddAccessRte','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','RteSoftwareTrace','UserRteFault','UserRteTrace','UserRteSoftwareTrace','MixedEpochRte','MixedEpochFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -569,13 +569,37 @@ foreach ($mutation in @( $mutation.group='ChainedOddShortFrameCanonical'; $mutation.model='68040'; $mutation.milestone=6 $mutation.chainedOdd=$true; $mutations += $mutation } +foreach ($mutation in @( + @{name='chained-access-odd-final-sr'; before='if (TryRaiseM68040RteAddressError(pc, priorSr, State.LastInstructionProgramCounter)) return true;'; + after='if (TryRaiseM68040RteAddressError(pc, sr, State.LastInstructionProgramCounter)) return true;'; + proofCombination='/normal/path=ISP-user/result=user/incoming=0000/first=0000/second=none/T=0000/'; proofReason='saved SR provenance'}, + @{name='chained-access-odd-lost-trace'; before=' var priorSr = State.StatusRegister;'; + after=' var priorSr = (ushort)(State.StatusRegister & ~0xc000);'; + proofCombination='/normal/path=ISP-user/result=user/incoming=0000/first=8000/second=none/T=0000/'; proofReason='saved SR provenance'}, + @{name='chained-access-odd-user-s'; before=' priorSr |= M68kCpuState.Supervisor;'; after=' priorSr |= 0;'; + proofCombination='/normal/path=ISP-user/result=user/incoming=0000/first=0000/second=none/T=8000/'; proofReason='saved SR provenance'}, + @{name='chained-access-odd-missing-ea-read'; before=' var address = continuation == 0 ? 0u : ReadRteFrameLong(frame + 8);'; + after=' var address = 0u;'; proofCombination='/CM/path=ISP-user/result=user/incoming=0000/first=0000/second=none/T=0000/'; proofReason='frame validation read order'}, + @{name='chained-access-odd-premature-movem'; before=@' + if (TryRaiseM68040RteAddressError(pc, priorSr, State.LastInstructionProgramCounter)) return true; + if (continuation == 0x1000) _m68040MovemContinuation = (pc, address); +'@; after=@' + if (continuation == 0x1000) _m68040MovemContinuation = (pc, address); + if (TryRaiseM68040RteAddressError(pc, priorSr, State.LastInstructionProgramCounter)) return true; +'@; proofCombination='/CM/path=ISP-user/result=user/incoming=0000/first=0000/second=none/T=0000/'; proofReason='no MOVEM continuation after odd return'} +)) { + $mutation.file='Copper68k/M68kAdvancedTimingInterpreter.Rte.cs' + $mutation.group='ChainedOddAccessFrameCanonical'; $mutation.model='68040'; $mutation.milestone=6 + $mutation.chainedOdd=$true; $mutation.chainedOddAccess=$true; $mutations += $mutation +} if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } if ($Scope -eq 'Logical') { $mutations = @($mutations | Where-Object { $_.milestone -eq 4 }) } if ($Scope -eq 'Control') { $mutations = @($mutations | Where-Object { $_.milestone -eq 5 }) } if ($Scope -eq 'Consolidation') { $mutations = @($mutations | Where-Object { $_.milestone -eq 6 }) } if ($Scope -eq 'Rte040') { $mutations = @($mutations | Where-Object { $_.odd }) } -if ($Scope -eq 'ChainedOddRte') { $mutations = @($mutations | Where-Object { $_.chainedOdd }) } +if ($Scope -eq 'ChainedOddRte') { $mutations = @($mutations | Where-Object { $_.chainedOdd -and -not $_.chainedOddAccess }) } +if ($Scope -eq 'ChainedOddAccessRte') { $mutations = @($mutations | Where-Object { $_.chainedOddAccess }) } if ($Scope -eq 'RteValidationFault') { $mutations = @($mutations | Where-Object { $_.validationFault }) } if ($Scope -eq 'RteRepair') { $mutations = @($mutations | Where-Object { $_.repair }) } if ($Scope -eq 'RteRetryTrace') { $mutations = @($mutations | Where-Object { $_.retryTrace }) } @@ -637,11 +661,15 @@ try { [xml]$trx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw $c=$trx.TestRun.ResultSummary.Counters if ($c.total -ne 2 -or $c.executed -ne 2 -or $c.failed -ne 2 -or $c.notExecuted -ne 0 -or $batches.Count -ne 2) { throw 'Chained odd mutation omitted complete selections' } + $prefix=if($mutation.chainedOddAccess){'rte-chained-odd-access-canonical'}else{'rte-chained-odd-canonical'} + $cases=if($mutation.chainedOddAccess){55296}else{82944} + $combinations=if($mutation.chainedOddAccess){1728}else{2592} + $reason=if($mutation.proofReason){$mutation.proofReason}else{'saved SR provenance'} foreach ($route in @('scalar','batch')) { - $r=@($batches | Where-Object group -CEQ "rte-chained-odd-canonical-$route") - if ($r.Count -ne 1 -or $r[0].logicalCases -ne 82944 -or @($r[0].combinations.PSObject.Properties).Count -ne 2592 -or + $r=@($batches | Where-Object group -CEQ "$prefix-$route") + if ($r.Count -ne 1 -or $r[0].logicalCases -ne $cases -or @($r[0].combinations.PSObject.Properties).Count -ne $combinations -or @($r[0].failures | Where-Object { $_.status -ceq 'mismatching' -and $_.id.Contains($mutation.proofCombination,[StringComparison]::Ordinal) -and - $_.reason.StartsWith('saved SR provenance',[StringComparison]::Ordinal) }).Count -eq 0) { throw "Missing intended chained odd $route diagnostic" } + $_.reason.StartsWith($reason,[StringComparison]::Ordinal) }).Count -eq 0) { throw "Missing intended chained odd $route diagnostic" } } } if ($mutation.throwaway) { diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index c2e1e348..a584c001 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -190,6 +190,10 @@ try { $expected['rte-chained-odd-canonical-batch'] = 82944 $expected['rte-chained-odd-structure-scalar'] = 539136 $expected['rte-chained-odd-structure-batch'] = 539136 + $expected['rte-chained-odd-access-canonical-scalar'] = 55296 + $expected['rte-chained-odd-access-canonical-batch'] = 55296 + $expected['rte-chained-odd-access-structure-scalar'] = 359424 + $expected['rte-chained-odd-access-structure-batch'] = 359424 $expected['rte-odd-pending'] = 165888 $expected['address-error-fetch-040'] = 2304 $expected['rte-throwaway-controls'] = 82944 From b88f95016bab947eaee92b04282d0dee2bd399b4 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 13:21:07 +0300 Subject: [PATCH 062/211] Record 68040 CM lifetime software-reference discovery --- .../M68040CmLifetimeDiscoveryTests.cs | 130 +++++++++++++ Copper68k.Tests/Synthetic/README.md | 36 ++++ .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 83 ++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 29 +++ scripts/reference/m68040-cm-lifetime.cpp | 177 ++++++++++++++++++ ...st-copper68k-040-cm-lifetime-discovery.ps1 | 154 +++++++++++++++ 7 files changed, 610 insertions(+), 1 deletion(-) create mode 100644 Copper68k.Tests/Synthetic/M68040CmLifetimeDiscoveryTests.cs create mode 100644 scripts/reference/m68040-cm-lifetime.cpp create mode 100644 scripts/test-copper68k-040-cm-lifetime-discovery.ps1 diff --git a/Copper68k.Tests/Synthetic/M68040CmLifetimeDiscoveryTests.cs b/Copper68k.Tests/Synthetic/M68040CmLifetimeDiscoveryTests.cs new file mode 100644 index 00000000..b4f1d23c --- /dev/null +++ b/Copper68k.Tests/Synthetic/M68040CmLifetimeDiscoveryTests.cs @@ -0,0 +1,130 @@ +using System.Globalization; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Software discovery: pinned WinUAE's MMU RTE arms CM before its odd-PC +// callback. Its generated MOVEM consumes that global state after repair. +// The native adapter composes exception entry; neither that composition nor +// continuation lifetime is a full external/hardware architectural oracle. +public sealed class M68040CmLifetimeDiscoveryTests(ITestOutputHelper output) +{ + internal const string Enable = "COPPER68K_RUN_040_CM_LIFETIME_DISCOVERY"; + private static readonly string[] Banks = ["user", "user-M", "ISP", "MSP"]; + private const uint Handler = 0x9190; + + [EnvironmentFact(Enable, "compare CM lifetime across odd-PC RTE repair"), Trait("Suite", "ReferenceDiscovery")] + public void MovemContinuationLifetimeScalar() => Audit(false); + + [EnvironmentFact(Enable, "compare CM lifetime across odd-PC RTE repair"), Trait("Suite", "ReferenceDiscovery")] + public void MovemContinuationLifetimeBatch() => Audit(true); + + private void Audit(bool batch) + { + var group = $"rte-cm-lifetime-discovery-{(batch ? "batch" : "scalar")}"; + var report = new CoverageBatch("68040", group); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); + var export = Environment.GetEnvironmentVariable("COPPER68K_040_CM_LIFETIME_EXPORT"); + using var rows = string.IsNullOrWhiteSpace(export) ? null : new StreamWriter(Path.Combine(export, group + ".rows")); + uint index = 0; + foreach (var start in new[] { "ISP", "MSP" }) + foreach (var result in Banks) + foreach (var odd in new[] { false, true }) + foreach (var cm in new[] { false, true }) + foreach (var alignment in new uint[] { 0, 1 }) + foreach (var vbr in new uint[] { 0, 0x10000 }) + foreach (var ccr in Enumerable.Range(0, 32)) + { + m.Reset(ccr); + var pointers = new Dictionary { ["user"] = 0x7800 + alignment, ["ISP"] = 0x4700 + alignment, ["MSP"] = 0x7400 + alignment }; + foreach (var p in pointers.Values) + for (var n = -20; n < 112; n++) m.InitializePhysical(unchecked(p + (uint)n), (uint)(n ^ 0x5a), 1); + var initialSr = M68040StackFixture.Status(start, 0, ccr); + var resultSr = M68040StackFixture.Status(result, 0, ccr ^ 31); + var frame = pointers[start]; + m.InitializePhysical(frame, resultSr, 2); m.InitializePhysical(frame + 2, odd ? 0x6001u : 0x6000u, 4); + m.InitializePhysical(frame + 6, 0x7008, 2); m.InitializePhysical(frame + 8, 0x4200, 4); + m.InitializePhysical(frame + 12, cm ? 0x1005u : 0x0105u, 2); + Install(m, 0x6000, [0x4cfa, 3, 0x0ffc, 0x4e71]); // PC base 6004 + 4092 = 7000. + Install(m, Handler, [0x3ebc, resultSr, 0x2f7c, 0, 0x6000, 2, 0x4e73]); + m.InitializePhysical(vbr + 12, Handler, 4); + m.InitializePhysical(0x4200, 0x89abcdef, 4); m.InitializePhysical(0x4204, 0x12345678, 4); + m.InitializePhysical(0x7000, 0xdeadbeef, 4); m.InitializePhysical(0x7004, 0x789abcde, 4); + _ = SyntheticExecution.Prepare(m, [0x4e73]); + m.Core.State.SetUserStackPointer(pointers["user"]); + m.Core.State.SetInterruptStackPointer(pointers["ISP"]); + m.Core.State.SetMasterStackPointer(pointers["MSP"]); + m.Core.State.StatusRegister = initialSr; m.Core.State.VectorBaseRegister = vbr; + var e = ArchitecturalExpectation.Capture(m); + var id = $"68040/RTE/cm-lifetime/{(cm ? "CM" : "normal")}/entry={start}/result={result}/odd={(odd ? 1 : 0)}/align={alignment}/VBR={vbr:X8}"; + var actual = new List(); + var valid = true; var executed = 0u; + void Phase(string name, ushort opcode) + { + var phaseId = $"{id}/phase={name}/op={opcode:X4}/ccr={ccr:X2}"; + if (!valid) { report.Record(phaseId, "untested", "Previous phase mismatched the software discovery expectation"); return; } + valid = M68040StackFixture.Step(m, e, report, phaseId, batch); + actual.AddRange(Snapshot(m)); executed++; + } + pointers[start] += 60; + var validation = new List<(uint, int)> { (frame, 2), (frame + 2, 4), (frame + 6, 2), (frame + 12, 2) }; + if (cm) validation.Add((frame + 8, 4)); + var validationAddresses = validation.Select(r => r.Item1).ToHashSet(); + e.ControlChecks["format7 validation order"] = (_ => m.Bus.Accesses.Where(a => !a.Write && validationAddresses.Contains(a.Address)) + .Select(a => (a.Address, a.Width)).SequenceEqual(validation) ? 0u : 1u, 0); + if (odd) + { + var bank = M68040StackFixture.ExceptionBank(resultSr); pointers[bank] -= 12; + var sp = pointers[bank]; + M68040StackFixture.SetStacks(e, pointers, (ushort)(resultSr | 0x2000)); + e.Pc = Handler; e.ExceptionVector = 3; + e.ControlChecks["one address error"] = (s => s.ExceptionSequence, m.Core.State.ExceptionSequence + 1); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, initialSr); + e.ControlChecks["saved RTE PC"] = (s => s.LastExceptionStackedProgramCounter, SyntheticMachine.Code); + e.Write(sp, initialSr, 2, m.Model); e.Write(sp + 2, SyntheticMachine.Code, 4, m.Model); + e.Write(sp + 6, 0x200c, 2, m.Model); e.Write(sp + 8, 0x6000, 4, m.Model); + } + else { M68040StackFixture.SetStacks(e, pointers, resultSr); e.Pc = 0x6000; } + Phase("initial-RTE", 0x4e73); + e.ControlChecks.Remove("format7 validation order"); + if (odd) + { + var sp = e.A[7]; + e.Write(sp, resultSr, 2, m.Model); e.Pc = Handler + 4; + e.Sr = (ushort)((e.Sr & 0xfff0) | (resultSr == 0 ? 4 : 0)); + Phase("repair-SR", 0x3ebc); + e.Write(sp + 2, 0x6000, 4, m.Model); e.Pc = Handler + 12; + e.Sr &= 0xfff0; + Phase("repair-PC", 0x2f7c); + pointers[M68040StackFixture.ExceptionBank(resultSr)] += 12; + M68040StackFixture.SetStacks(e, pointers, resultSr); e.Pc = 0x6000; + Phase("handler-RTE", 0x4e73); + } + e.Pc = 0x6006; + e.D[0] = cm ? 0x89abcdefu : 0xdeadbeefu; e.D[1] = cm ? 0x12345678u : 0x789abcdeu; + var source = cm ? 0x4200u : 0x7000u; + var accessStart = m.Bus.Accesses.Count; + e.ControlChecks["MOVEM operand address/order"] = (_ => m.Bus.Accesses.Skip(accessStart).Where(a => !a.Write && a.Address is 0x4200 or 0x4204 or 0x7000 or 0x7004) + .Select(a => (a.Address, a.Width)).SequenceEqual(new[] { (source, 4), (source + 4, 4) }) ? 0u : 1u, 0); + Phase("MOVEM", 0x4cfa); + e.ControlChecks.Remove("MOVEM operand address/order"); + e.Pc = 0x6008; Phase("sentinel", 0x4e71); + if (rows != null) + { + uint[] input = [index, start == "MSP" ? 1u : 0u, (uint)Array.IndexOf(Banks, result), odd ? 1u : 0u, cm ? 1u : 0u, alignment, vbr, (uint)ccr]; + rows.WriteLine(string.Join(' ', input.Select(Hex)) + " | " + Hex(executed) + " " + string.Join(' ', actual.Select(Hex))); + } + index++; + } + report.Complete(output); + } + + private static void Install(SyntheticMachine m, uint address, ushort[] words) + { for (var n = 0; n < words.Length; n++) m.InitializePhysical(address + (uint)n * 2, words[n], 2); } + private static uint[] Snapshot(SyntheticMachine m) + { + var s = m.Core.State; + return [s.ProgramCounter, s.StatusRegister, s.UserStackPointer, s.InterruptStackPointer, s.MasterStackPointer, .. s.D, .. s.A]; + } + private static string Hex(uint value) => value.ToString("X", CultureInfo.InvariantCulture); +} diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index e05d1766..f0e7cf03 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -893,3 +893,39 @@ WinUAE helper arms CM before the odd-PC check, whereas this non-MMU observer skips that helper. Its passing header comparison does not settle CM lifetime across address-error delivery/software repair. That protocol remains required reference research, as recorded in the qualification document and plan. + +### CM continuation lifetime discovery + +`M68040CmLifetimeDiscoveryTests` follows a normal/CM format7 return through an +odd-PC address error, two executed repair stores, handler RTE, PC-relative +MOVEM.L and a following NOP. Even-PC controls omit the repair path. Every CCR, +entry ISP/MSP, restored user/user-M/ISP/MSP, both stack alignments and both VBRs +produce 4,096 scenarios / 18,432 phases / 576 phase combinations per route. +All registers, stack pointers, guarded memory and selected operand read order +are checked. A failed prerequisite leaves later phases explicitly untested. + +Run `./scripts/test-copper68k-040-cm-lifetime-discovery.ps1` with the same pristine +WinUAE pin and MSVC options as the handoff command. This builds the original +full generator with CPU_TESTER=0, then extracts unchanged `_31` RTE, repair +MOVE, MOVEM and NOP functions, the original `m68k_do_rte_mmu040` helper and +cputest SR helpers. Four literal normal/CM, even/odd controls validate the +program and saved-versus-recomputed addresses. Sparse physical transport +rejects uninitialized reads. The adapter composes address-error entry; it +does **not** execute full WinUAE `Exception_mmu`, enabled MMU, trace/interrupt +delivery, physical timing or hardware behavior. + +The current scalar/batch routes each contain 16,384 passing, 1,024 mismatching, +zero unsupported and 1,024 untested phases. Only odd-PC CM MOVEM disagrees: +Copper68k loads DEADBEEF from recomputed 7000; the reference retains CM through +repair and loads 89ABCDEF from saved 4200. Following sentinels in those scenarios +remain untested. All earlier repair phases, even-PC CM and normal controls agree. +This is an executed software discrepancy, not a promoted architectural defect. +No continuation latch or production edit is introduced to force agreement. + +The command records pinned source, fixture, generated-code, binary and evidence +identities; independently checks all phase keys, CCR weights and fixture rows; +and fails on mismatches, unsupported/untested execution or missing/empty inputs. +`-ValidateReportsOnly -OutputDirectory` also executes the frozen native observer +and compares its regenerated results. Ordinary synthetic and complete-040 +selections exclude these gated `ReferenceDiscovery` tests. The 480-case required +inventory and broader milestone 6 scope remain intact. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index b5dc0963..3ca7d378 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -37,7 +37,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() : form == "real-access-fault-entry" ? "Cache-disabled accurate instruction-fetch frames/restart are covered separately; data fault/writeback restart, enabled-cache deferral and compiled instruction-PC provenance remain required" : form == "odd-PC-chained-SR-provenance" - ? "Chained format0/2/3 and format7 normal/CM SR handoff, stack consumption and header reads have separate generated WinUAE software coverage; access-frame validation/no-replay checks are synthetic. CT/CU/CP pending/foreign-context chains, full reference exception entry and hardware behavior remain unqualified" + ? "Chained format0/2/3 and format7 normal/CM SR handoff, stack consumption and header reads have separate generated WinUAE software coverage; access-frame validation/no-replay checks are synthetic. Separate MMU-path CM lifetime discovery disagrees after odd-PC fault/repair; its exception entry is composed, not a full architectural oracle. CT/CU/CP pending/foreign-context chains, full reference exception entry and hardware behavior remain unqualified" : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 305b0462..1519697b 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -5671,3 +5671,86 @@ repair/retry, internal/data/writeback restoration, broader model reference qualification and consolidation remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`. No CPU source change, package publication or additional regression retirement is included. + +### Executed 68040 CM lifetime discovery across odd-PC repair + +The documentary CM difference above is now exercised rather than inferred from +source alone. `scripts/test-copper68k-040-cm-lifetime-discovery.ps1` builds the +unchanged full WinUAE generator at `5d22d33632646efc3f747f03e82d28353e52722e`. +Strict extraction retains original generated `op_4e73_31_ff`, MOVE.W immediate +through A7, MOVE.L immediate to 2(A7), PC-relative MOVEM.L and NOP. The original +`m68k_do_rte_mmu040` from `cpummu.cpp` and cputest SR helpers are also unchanged. +No function body is rewritten or replaced with Copper68k arithmetic/EA helpers. + +Four literal normal/CM × even/odd controls execute a fixed program. The return +target is 6000/6001; the format7 CM EA is 4200; MOVEM's encoded PC base 6004 plus +0FFC selects ordinary 7000. Those addresses contain different D0/D1 values. +An odd return reaches an address-error boundary, writes the intended SR and +even PC into the frame with real instructions, returns, then executes MOVEM. +In this software model the helper arms CM before the odd-PC callback, both +repair stores and handler RTE leave it armed, and MOVEM uses 4200 and clears CM. +Every selected generated instruction is checked against a literal opcode at +its PC; sparse physical transport rejects uninitialized reads. + +The crucial boundary remains explicit: the adapter composes documented +format-2 address-error entry after the callback. It does **not** execute full +WinUAE `Exception_mmu` or the run loop, enabled translation, IRQ/trace delivery, +cache/pipeline behavior or physical timing. The pinned MMU and non-MMU exception +paths have different SR handling; this harness must not certify their full +equivalence or establish hardware continuation lifetime. `newcpu.cpp` is pinned +as source context, not claimed as executed exception processing. + +The CPU discovery varies entry ISP/MSP, restored user/user-M/ISP/MSP, normal/CM, +even/odd PC, stack alignment 0/1, VBR 0/10000 and every initial CCR. Each scalar +and batch route has 4,096 scenarios, 576 phase combinations and 18,432 logical +phases. All registers, physical stack pointers, PC/SR, exception metadata, +guarded memory and selected validation/operand read order are checked. Batched +steps require exactly one instruction and both boundary callbacks. Prerequisite +failure is recorded as untested later execution; no instruction is retried. + +| Current discovery route | Passing | Mismatching | Unsupported | Untested | +| --- | ---: | ---: | ---: | ---: | +| Scalar | 16,384 | 1,024 | 0 | 1,024 | +| Batch | 16,384 | 1,024 | 0 | 1,024 | +| Total | 32,768 | 2,048 | 0 | 2,048 | + +Both tests execute without skips and correctly fail. Every mismatch is the +odd-PC CM MOVEM phase: D0 expected 89ABCDEF from saved 4200, actual DEADBEEF from +recomputed 7000. Only those scenarios' following NOPs are untested. Normal and +even-PC CM controls, address-error entry composition, repair stores and handler +returns agree at their observed boundaries. Native comparison of all public +register/PC/SR/stack snapshots gives exactly the same phase classifications. +This is a localized executed software-reference discrepancy, not a proven +architectural production defect. No guessed continuation latch is added. + +Fresh final evidence is `artifacts/m6-cm-lifetime-final/`; command exit 1 is the +expected current discovery failure. Release build has zero .NET warnings/errors; +native warnings are retained only in untouched generated source. The command +records complete current CPU/fixture, pinned source, generated/extracted code, +binary and row/evidence hashes. It independently enumerates all keys, 32-CCR +weights and 4,096 ordered fixture rows per route. Report-only revalidation also +executes the frozen native observer and requires byte-identical reference +results. Thirteen integrity controls reject missing identity/fixture identity, +empty selection, missing combination, wrong weight, duplicated fixture, +missing reference row, altered reference summary, native missing/empty inputs, +empty required selection, out-of-scope fixture and reordered index. They are +recorded in `artifacts/m6-cm-lifetime-integrity-final/controls.json`. + +Eight retained tests pass without skips: six fixed saved-SR examples and both +existing CM MOVEM matrices, totalling 235,008 passing generated cases. The fresh +required inventory has one expected failing test without skips and retains +480 untested cases / fifteen combinations. Its explanation now names the +unqualified lifetime discrepancy. Ordinary synthetic/complete-040 selections +exclude the two gated discovery tests and their previous counts remain +requirements; older broad suites/consumer results are not relabeled as rerun. + +Frozen build starts at `826351ff0709d2e990b6b62692f068038e06af75` plus these +test/command inputs. Production tree remains +`795fd12d6c0237a22a5f92f4a96cc4823e0364c1`. +CPU SHA256: `89cefdfbcc27b2d64e0d5fc7dc764f02402406883ac2b21a5b88978553bd1c78`. +Adapter SHA256: `d31a06c3ca60d1644433acef41ffe826509f8bac376494064bffad3da5f00315`. +The frozen identities, rather than current post-commit build output, identify +this run. Full CM fault/repair lifetime, pending/foreign contexts, original-trace +retry, internal/data/writeback restoration, broader model reference coverage +and consolidation remain required. Milestone 6 stays **in progress**, +`roadmapComplete=false`; no CPU fix, package release or regression retirement. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 82c6bc5e..0b07a78e 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2195,3 +2195,32 @@ software repair, pending/foreign-context odd-return chains, original-trace fault/retry, internal/data/writeback restoration, broader model reference audits and consolidation remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`; no CPU fix, release or regression retirement. + +### Milestone 6 CM continuation lifetime software discovery + +The previous documentary MMU/non-MMU difference now has executed evidence. +A dedicated gated scalar/batch matrix runs normal/CM format7 returns, even/odd +PCs, actual SR/PC repair stores, handler RTE, PC-relative MOVEM.L and a following +sentinel. Each route covers 4,096 scenarios / 18,432 phases / 576 combinations: +every CCR, entry ISP/MSP, all restored banks including user-M, alignments 0/1 +and VBR 0/10000. Full architectural/guarded-memory checks accompany snapshots. + +`scripts/test-copper68k-040-cm-lifetime-discovery.ps1` executes untouched full +generated WinUAE MMU RTE/MOVE/MOVEM/NOP, the original CM helper and SR helpers +from pin `5d22d33632646efc3f747f03e82d28353e52722e`. Four literal controls +distinguish saved and recomputed addresses. The adapter composes address-error +entry, so this is not a complete external exception or hardware oracle. +Per route: 16,384 passing, 1,024 mismatching, zero unsupported and 1,024 untested +phases. Every mismatch is the odd-PC CM MOVEM's D0: saved 89ABCDEF versus +recomputed DEADBEEF; its following sentinel remains untested. Earlier repair +phases, normal controls and even-PC CM agree. This localizes a software-reference +discrepancy without proving which continuation lifetime hardware requires. + +The audit records pinned source/input/generated/binary/evidence identities, +independently enumerates keys/weights/rows, executes frozen native replay during +report revalidation and correctly fails the discrepancy. Required coverage is +not reduced: ordinary/complete-040 counts remain unchanged, the 480-case +inventory remains explicitly untested, and broader reference/consolidation work +remains required. This discovery is excluded from ordinary synthetic CI and +the complete-040 promoted selection. Milestone 6 stays **in progress**, +`roadmapComplete=false`; no CPU fix, package publication or regression retirement. diff --git a/scripts/reference/m68040-cm-lifetime.cpp b/scripts/reference/m68040-cm-lifetime.cpp new file mode 100644 index 00000000..67d50e1f --- /dev/null +++ b/scripts/reference/m68040-cm-lifetime.cpp @@ -0,0 +1,177 @@ +// Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT +// Untouched WinUAE MMU RTE/MOVEM and repair instructions on physical memory. +// The odd-PC exception frame is composed by this adapter: this is software +// discovery, not full Exception_mmu, enabled MMU, trace, timing or hardware proof. +#include +#include +#include +#include +#include +#include +#include +#include +using uae_u16=uint16_t; using uae_u32=uint32_t; +using uae_s16=int16_t; using uae_s32=int32_t; using uaecptr=uint32_t; +#define REGPARAM2 +#define CYCLE_UNIT 2 +#define MMUDEBUGMISC 0 +#define MMU_SSW_CM 0x1000 +#define MMU_SSW_CT 0x2000 +#define SPCFLAG_MMURESTART 1 +#define m68k_areg(r,n) ((r).a[n]) +#define m68k_dreg(r,n) ((r).d[n]) +struct Regs { uint32_t d[8]{},a[8]{},usp{},isp{},msp{},pc{},instruction_pc{}; uint16_t sr{}; int m{},s{},t0{},t1{},intmask{},ipl[1]{}; } regs; +struct Prefs { int cpu_model=68040; } currprefs; +int flag_SPCFLAG_TRACE,cpu_stopped,xflag,nflag,zflag,vflag,cflag; +#define SET_XFLG(v) (xflag=(v)) +#define SET_NFLG(v) (nflag=(v)) +#define SET_ZFLG(v) (zflag=(v)) +#define SET_VFLG(v) (vflag=(v)) +#define SET_CFLG(v) (cflag=(v)) +#define GET_XFLG() xflag +#define GET_NFLG() nflag +#define GET_ZFLG() zflag +#define GET_VFLG() vflag +#define GET_CFLG() cflag +#define CLEAR_CZNV() (cflag=zflag=nflag=vflag=0) +void activate_trace() { throw std::runtime_error("Trace is outside this discovery"); } +void check_t0_trace() { throw std::runtime_error("T0 is outside this discovery"); } +bool mmu_restart; int mmu040_movem,specialFlags; uint32_t mmu040_movem_ea; +int movem_index1[256],movem_next[256]; +void set_special(int flags) { specialFlags|=flags; } +std::unordered_map memory; +uint32_t read(uint32_t address,int width) { + uint32_t value=0; + for(int n=0;nsecond; + } + return value; +} +void put(uint32_t address,uint32_t value,int width) { + for(int n=0;n(value>>(8*(width-1-n))); +} +uint16_t get_word_mmu040(uint32_t a) { return static_cast(read(a,2)); } +uint32_t get_long_mmu040(uint32_t a) { return read(a,4); } +void put_word_mmu040(uint32_t a,uint16_t v) { put(a,v,2); } +void put_long_mmu040(uint32_t a,uint32_t v) { put(a,v,4); } +uint16_t get_iword_mmu040(int offset) { return get_word_mmu040(regs.pc+offset); } +uint32_t get_ilong_mmu040(int offset) { return get_long_mmu040(regs.pc+offset); } +uint32_t m68k_getpci() { return regs.pc; } +void m68k_setpci(uint32_t value) { regs.pc=value; } +void m68k_incpci(int offset) { regs.pc+=offset; } +void Exception(int) { throw std::runtime_error("Unexpected privilege exception"); } +void Exception_cpu(int) { throw std::runtime_error("Unexpected format exception"); } +int callbackCount,cmAtCallback; uint16_t secondarySr; uint32_t oddPc; +void exception3_read_prefetch_68040bug(uint32_t opcode,uint32_t address,uint16_t sr) { + if(opcode!=0x4e73 || address!=0x6001) throw std::runtime_error("Unexpected odd-PC callback"); + callbackCount++; secondarySr=sr; oddPc=address; cmAtCallback=mmu040_movem; +} +// Strictly extracted unchanged from pinned source/generated output. +#include "winuae-cm-sr.inc" +#include "winuae-cm-rte-helper.inc" +#include "winuae-cm-ops.inc" + +std::vector snapshot() { + MakeSR(); + std::vector v{regs.pc,regs.sr, + regs.s?regs.usp:regs.a[7],regs.s&&!regs.m?regs.a[7]:regs.isp, + regs.s&®s.m?regs.a[7]:regs.msp}; + v.insert(v.end(),regs.d,regs.d+8); v.insert(v.end(),regs.a,regs.a+8); + return v; +} +void step(uint32_t opcode,uae_u32 (*fn)(uae_u32)) { + MakeSR(); mmu_restart=true; regs.instruction_pc=regs.pc; + if(read(regs.pc,2)!=opcode) throw std::runtime_error("Instruction sentinel differs"); + fn(opcode); +} +// Explicitly composed boundary. Does not invoke WinUAE Exception_mmu. +void composeAddressError() { + regs.sr=static_cast((regs.sr|0x2000)&~0xc000); + MakeFromSR(); regs.a[7]-=12; + put(regs.a[7],secondarySr,2); put(regs.a[7]+2,0x1000,4); + put(regs.a[7]+6,0x200c,2); put(regs.a[7]+8,oddPc&~1u,4); + regs.pc=0x9190; +} +std::vector> execute(const std::vector& f) { + if(f.size()!=8 || f[1]>1 || f[2]>3 || f[3]>1 || f[4]>1 || f[5]>1 || + (f[6]!=0 && f[6]!=0x10000) || f[7]>31) throw std::runtime_error("Out-of-scope fixture"); + const bool odd=f[3]!=0,cm=f[4]!=0; + const uint16_t initialSr=static_cast((f[1]?0x3000:0x2000)|f[7]); + const uint16_t resultSr=static_cast((f[2]==1?0x1000:f[2]==2?0x2000:f[2]==3?0x3000:0)|(f[7]^31)); + regs={}; memory.clear(); mmu040_movem=0; mmu040_movem_ea=0; + callbackCount=cmAtCallback=specialFlags=cpu_stopped=flag_SPCFLAG_TRACE=0; + for(int n=0;n<8;n++) {regs.d[n]=0xa55a0022u+n*0x100;regs.a[n]=0x4000u+n*0x100;} + regs.d[7]=2; regs.usp=0x7800+f[5]; regs.isp=0x4700+f[5]; regs.msp=0x7400+f[5]; + regs.sr=initialSr; regs.s=1; regs.m=static_cast(f[1]); regs.a[7]=f[1]?regs.msp:regs.isp; regs.pc=0x1000; + MakeFromSR(); + for(const auto p:{regs.usp,regs.isp,regs.msp}) for(int n=-20;n<112;n++) put(p+n,static_cast(n^0x5a),1); + const auto frame=regs.a[7]; + put(frame,resultSr,2); put(frame+2,odd?0x6001:0x6000,4); put(frame+6,0x7008,2); + put(frame+8,0x4200,4); put(frame+12,cm?0x1005:0x0105,2); + put(0x1000,0x4e73,2); put(f[6]+12,0x9190,4); + put(0x6000,0x4cfa,2); put(0x6002,3,2); put(0x6004,0x0ffc,2); put(0x6006,0x4e71,2); + put(0x9190,0x3ebc,2); put(0x9192,resultSr,2); + put(0x9194,0x2f7c,2); put(0x9196,0x6000,4); put(0x919a,2,2); put(0x919c,0x4e73,2); + put(0x4200,0x89abcdef,4); put(0x4204,0x12345678,4); + put(0x7000,0xdeadbeef,4); put(0x7004,0x789abcde,4); + std::vector> states; + step(0x4e73,op_4e73_31_ff); + if(callbackCount!=(odd?1:0) || mmu040_movem!=(cm?1:0) || (odd && cmAtCallback!=(cm?1:0))) + throw std::runtime_error("RTE callback/CM lifetime differs"); + if(odd) composeAddressError(); + states.push_back(snapshot()); + if(odd) { + step(0x3ebc,op_30bc_31_ff); states.push_back(snapshot()); + step(0x2f7c,op_217c_31_ff); states.push_back(snapshot()); + step(0x4e73,op_4e73_31_ff); states.push_back(snapshot()); + } + if(regs.pc!=0x6000 || mmu040_movem!=(cm?1:0)) throw std::runtime_error("Repair lost native CM state"); + step(0x4cfa,op_4cfa_31_ff); states.push_back(snapshot()); + if(regs.pc!=0x6006 || mmu040_movem || regs.d[0]!=(cm?0x89abcdefu:0xdeadbeefu) || + regs.d[1]!=(cm?0x12345678u:0x789abcdeu)) throw std::runtime_error("MOVEM address/lifetime control failed"); + step(0x4e71,op_4e71_31_ff); states.push_back(snapshot()); + if(regs.pc!=0x6008 || callbackCount!=(odd?1:0) || states.size()!=(odd?6u:3u)) throw std::runtime_error("Following sentinel failed"); + return states; +} +std::vector parse(const std::string& text) { + std::istringstream stream(text); std::string token; std::vector values; + while(stream>>token) {size_t end=0;auto value=std::stoull(token,&end,16); + if(end!=token.size() || value>UINT32_MAX) throw std::runtime_error("Invalid hexadecimal input"); + values.push_back(static_cast(value));} + return values; +} +int main(int argc,char** argv) { + try { + for(int mask=1;mask<256;mask++) {int bit=0;while(!(mask&(1<expected.size() || actual.size()!=1+actual[0]*21) + throw std::runtime_error("Missing/invalid CPU phase states"); + output<=actual[0]) {untested++;continue;} + std::vector observed(actual.begin()+1+n*21,actual.begin()+1+(n+1)*21); + if(observed==expected[n]) passing++; + else {if(mismatches<20) std::cerr<<"Mismatch row "<nul && set" + if($LASTEXITCODE -ne 0){throw 'MSVC setup failed'} + foreach($setting in $settings){$index=$setting.IndexOf('=');if($index -gt 0){$name=$setting.Substring(0,$index);$savedCompiler[$name]=[Environment]::GetEnvironmentVariable($name);[Environment]::SetEnvironmentVariable($name,$setting.Substring($index+1))}} + Push-Location $output + try { + & cl /nologo /O2 /EHsc /w /DWIN32 /DUNICODE /D_UNICODE "/I$source/include" "/I$source/od-win32" "/I$source" (Join-Path $source 'build68k.cpp') /Fe:build68k.exe *> build68k.log + if($LASTEXITCODE -ne 0){throw 'Reference opcode builder failed'} + Get-Content -LiteralPath (Join-Path $source 'table68k')|& ./build68k.exe|Set-Content -Encoding utf8 cpudefs.cpp + if($LASTEXITCODE -ne 0 -or -not (Select-String -LiteralPath cpudefs.cpp -SimpleMatch 'n_defs68k = 232')){throw 'Missing reference opcode definitions'} + & cl /nologo /O2 /EHsc /w /DWIN32 /DUNICODE /D_UNICODE "/I$source/include" "/I$source/od-win32" "/I$source" (Join-Path $source 'gencpu.cpp') (Join-Path $source 'readcpu.cpp') (Join-Path $source 'missing.cpp') (Join-Path $source 'od-win32/unicode.cpp') cpudefs.cpp user32.lib /Fe:gencpu.exe *> generator-build.log + if($LASTEXITCODE -ne 0){throw 'Reference generator build failed'} + & ./gencpu.exe *> generator-run.log + if($LASTEXITCODE -ne 0){throw 'Reference generation failed'} + $text=[IO.File]::ReadAllText((Join-Path $source 'cputest.cpp')) + $sr=@('void REGPARAM2 MakeSR(void)','void MakeFromSR_x(int t0trace)','void REGPARAM2 MakeFromSR_T0(void)','void REGPARAM2 MakeFromSR(void)')|ForEach-Object {FunctionText $text $_} + [IO.File]::WriteAllText((Join-Path $output 'winuae-cm-sr.inc'),($sr -join "`n`n")) + [IO.File]::WriteAllText((Join-Path $output 'winuae-cm-rte-helper.inc'),(FunctionText ([IO.File]::ReadAllText((Join-Path $source 'cpummu.cpp'))) 'void m68k_do_rte_mmu040 (uaecptr a7)')) + $text=[IO.File]::ReadAllText((Join-Path $output 'cpuemu_31.cpp')) + $ops=@('4e73','30bc','217c','4cfa','4e71')|ForEach-Object {FunctionText $text "uae_u32 REGPARAM2 op_$($_)_31_ff(uae_u32 opcode)"} + [IO.File]::WriteAllText((Join-Path $output 'winuae-cm-ops.inc'),($ops -join "`n`n")) + & cl /nologo /O2 /EHsc /W4 "/I$output" (Join-Path $repo 'scripts/reference/m68040-cm-lifetime.cpp') /Fe:observer.exe *> observer-build.log + if($LASTEXITCODE -ne 0){throw 'Reference observer build failed'} + $compiler=(& cl 2>&1|Out-String).Trim() + } finally {Pop-Location;foreach($name in $savedCompiler.Keys){[Environment]::SetEnvironmentVariable($name,$savedCompiler[$name])}} + & dotnet build (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release *> (Join-Path $output 'build.log') + if($LASTEXITCODE -ne 0){throw 'CPU discovery build failed'} + $settings=@{COPPER68K_RUN_040_CM_LIFETIME_DISCOVERY='1';COPPER68K_SYNTHETIC_REPORT_DIR=$output;COPPER68K_040_CM_LIFETIME_EXPORT=$output};$saved=@{} + try { + foreach($name in $settings.Keys){$saved[$name]=[Environment]::GetEnvironmentVariable($name);[Environment]::SetEnvironmentVariable($name,$settings[$name])} + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --no-build --no-restore --filter 'FullyQualifiedName~M68040CmLifetimeDiscoveryTests' --logger 'trx;LogFileName=discovery.trx' --results-directory $output *> (Join-Path $output 'run.log') + $testExit=$LASTEXITCODE + } finally {foreach($name in $saved.Keys){[Environment]::SetEnvironmentVariable($name,$saved[$name])}} + foreach($group in $groups) { + & (Join-Path $output 'observer.exe') (Join-Path $output "$group.rows") (Join-Path $output "$group.reference") 4096 (Join-Path $output "$group.reference.json") *> (Join-Path $output "$group.reference.log") + if($LASTEXITCODE -notin @(0,1)){throw "Reference execution failed: $group"} + } + $identity=@{schema=1;sourceCommit=(& git -C $repo rev-parse HEAD);cpuCommittedTree=(& git -C $repo rev-parse HEAD:Copper68k); + reference='WinUAE-MMU-040-CM-lifetime-discovery';referenceCommit=$pin;referenceDirectory=$source; + softwareReferenceExecuted=$true;composedExceptionBoundary=$true;fullExceptionOracle=$false;architecturallyQualified=$false;hardwareQualified=$false;roadmapComplete=$false; + compiler=$compiler;compilerFlags='/O2 /EHsc /DWIN32 /DUNICODE /D_UNICODE; observer /W4'; + extraction='Untouched full generated _31 RTE/MOVE/MOVEM/NOP, cpummu m68k_do_rte_mmu040 and cputest SR helpers; original CPU_TESTER=0'; + caveats='Physical memory transport; manual odd-PC exception composition; no full Exception_mmu, enabled MMU, trace, IRQ, timing or hardware oracle'; + inputs=@();referenceInputs=@();generated=@();evidence=@()} + foreach($file in (@(& git -C $repo ls-files 'Copper68k/*')+$fixtures)){$identity.inputs+=@{file=$file;sha256=(Hash (Join-Path $repo $file))}} + foreach($file in $referenceFiles){$identity.referenceInputs+=@{file=$file;sha256=(Hash (Join-Path $source $file))}} + foreach($file in $generated){$identity.generated+=@{file=$file;sha256=(Hash (Join-Path $output $file))}} + foreach($file in $evidence){$identity.evidence+=@{file=$file;sha256=(Hash (Join-Path $output $file))}} + $identity|ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'identities.json') +} +$identity=Get-Content (Join-Path $output 'identities.json') -Raw|ConvertFrom-Json +if($identity.schema -ne 1 -or $identity.reference -cne 'WinUAE-MMU-040-CM-lifetime-discovery' -or $identity.referenceCommit -cne $pin -or + $identity.softwareReferenceExecuted -ne $true -or $identity.composedExceptionBoundary -ne $true -or $identity.fullExceptionOracle -ne $false -or + $identity.architecturallyQualified -ne $false -or $identity.hardwareQualified -ne $false -or $identity.roadmapComplete -ne $false -or + $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.cpuCommittedTree -notmatch '^[a-f0-9]{40}$'){throw 'Incomplete CM discovery identity'} +CheckIdentities $identity.inputs (@(& git -C $repo ls-files 'Copper68k/*')+$fixtures) $repo 'fixture/CPU' +CheckIdentities $identity.referenceInputs $referenceFiles $source 'reference source' +CheckIdentities $identity.generated $generated $output 'generated reference' +CheckIdentities $identity.evidence $evidence $output 'evidence' +[xml]$trx=Get-Content (Join-Path $output 'discovery.trx') -Raw +$c=$trx.TestRun.ResultSummary.Counters +if([int]$c.total -ne 2 -or [int]$c.executed -ne 2 -or [int]$c.notExecuted -ne 0 -or ([int]$c.passed+[int]$c.failed) -ne 2){throw 'Discovery must execute both complete selections without skips'} +$keys=[Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) +$fixtureRows=[Collections.Generic.List[string]]::new() +$index=0 +foreach($start in @('ISP','MSP')){foreach($result in @('user','user-M','ISP','MSP')){foreach($odd in @(0,1)){foreach($cm in @(0,1)){ +foreach($alignment in @(0,1)){foreach($vbr in @(0,0x10000)){ + $prefix='68040/RTE/cm-lifetime/{0}/entry={1}/result={2}/odd={3}/align={4}/VBR={5:X8}' -f $(if($cm){'CM'}else{'normal'}),$start,$result,$odd,$alignment,$vbr + foreach($phase in $(if($odd){@('initial-RTE','repair-SR','repair-PC','handler-RTE','MOVEM','sentinel')}else{@('initial-RTE','MOVEM','sentinel')})){ + if(-not $keys.Add("$prefix/phase=$phase")){throw 'Duplicated independent combination'} + } + foreach($ccr in 0..31){$values=@($index,$(if($start -eq 'MSP'){1}else{0}),[array]::IndexOf(@('user','user-M','ISP','MSP'),$result),$odd,$cm,$alignment,$vbr,$ccr);$fixtureRows.Add(($values|ForEach-Object {'{0:X}' -f $_}) -join ' ');$index++} +}}}}}} +if($keys.Count -ne 576 -or $fixtureRows.Count -ne 4096){throw 'Independent CM discovery enumeration differs'} +$replay=Join-Path $output ('reference-recheck-'+[guid]::NewGuid().ToString('N')) +if($ValidateReportsOnly){New-Item -ItemType Directory $replay|Out-Null} +$totals=@{passing=0;mismatching=0;unsupported=0;untested=0};$reports=@();$passedBatches=0 +foreach($group in $groups) { + $r=Get-Content (Join-Path $output "68040-$group.json") -Raw|ConvertFrom-Json -AsHashtable + if($r.schema -ne 1 -or $r.model -cne '68040' -or $r.group -cne $group -or $r.xunitBatches -ne 1 -or $r.logicalCases -ne 18432 -or $r.combinations.Count -ne 576){throw 'Incomplete CM discovery report'} + $sums=@{passing=0;mismatching=0;unsupported=0;untested=0} + foreach($key in $r.combinations.Keys) { + if(-not $keys.Contains($key)){throw "Unexpected combination: $key"} + $weight=0 + foreach($status in $r.combinations[$key].Keys){if(-not $sums.ContainsKey($status)){throw 'Unknown coverage status'};$n=[long]$r.combinations[$key][$status];if($n -lt 0){throw 'Negative coverage count'};$weight+=$n;$sums[$status]+=$n} + if($weight -ne 32){throw "Incomplete CCR combination: $key"} + } + foreach($status in $sums.Keys){if($sums[$status] -ne $r.counts[$status]){throw 'Coverage totals differ'};$totals[$status]+=$sums[$status]} + $lines=[IO.File]::ReadAllLines((Join-Path $output "$group.rows")) + if($lines.Count -ne 4096){throw 'Missing CPU reference rows'} + for($n=0;$n -lt $lines.Count;$n++) { + $split=$lines[$n].Split(' | ',[StringSplitOptions]::None) + if($split.Count -ne 2 -or $split[0] -cne $fixtureRows[$n]){throw 'Missing/reordered reference fixture combination'} + } + if($ValidateReportsOnly) { + & (Join-Path $output 'observer.exe') (Join-Path $output "$group.rows") (Join-Path $replay "$group.reference") 4096 (Join-Path $replay "$group.reference.json") *> (Join-Path $replay "$group.log") + if($LASTEXITCODE -notin @(0,1) -or (Hash (Join-Path $replay "$group.reference")) -cne (Hash (Join-Path $output "$group.reference")) -or + (Hash (Join-Path $replay "$group.reference.json")) -cne (Hash (Join-Path $output "$group.reference.json"))){throw 'Recorded reference differs from executed replay'} + } + $reference=Get-Content (Join-Path $output "$group.reference.json") -Raw|ConvertFrom-Json + if($reference.rows -ne 4096 -or $reference.phases -ne 18432 -or $reference.fixedControls -ne 4 -or $reference.composedExceptionBoundary -ne $true -or + $reference.fullExceptionOracle -ne $false -or $reference.hardwareQualified -ne $false -or $reference.passing -ne $sums.passing -or + $reference.mismatching -ne $sums.mismatching -or $reference.untested -ne $sums.untested -or $sums.unsupported -ne 0){throw 'CPU/reference phase classifications differ'} + if(($sums.mismatching+$sums.unsupported+$sums.untested) -eq 0){$passedBatches++} + $reports+=@{group=$group;logicalCases=18432;combinations=576;rows=4096;counts=$r.counts} +} +if([int]$c.passed -ne $passedBatches -or [int]$c.failed -ne (2-$passedBatches)){throw 'xUnit and discovery classifications differ'} +$passed=$passedBatches -eq 2 +if(-not $ValidateReportsOnly -and (($testExit -eq 0) -ne $passed)){throw 'Test exit and discovery classifications differ'} +@{schema=1;logicalCases=36864;batches=2;reports=$reports;counts=$totals;passed=$passed;softwareReferenceExecuted=$true; + architecturallyQualified=$false;fullExceptionOracle=$false;hardwareQualified=$false;roadmapComplete=$false; + referenceCommit=$pin;composedExceptionBoundary=$true;caveats=$identity.caveats}| + ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'discovery-summary.json') +if(-not $passed){throw "CM lifetime software discovery remains mismatching or incomplete: $($totals|ConvertTo-Json -Compress)"} From 11124e9a24fb13c832ed569626ec2b76f6242696 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 13:44:48 +0300 Subject: [PATCH 063/211] Execute scoped 68040 MMU exception entry in CM audit --- .../M68040CmLifetimeDiscoveryTests.cs | 7 + Copper68k.Tests/Synthetic/README.md | 19 +++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 74 ++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 26 ++++ scripts/reference/m68040-cm-lifetime.cpp | 137 +++++++++++++++++- ...st-copper68k-040-cm-lifetime-discovery.ps1 | 64 +++++--- 6 files changed, 299 insertions(+), 28 deletions(-) diff --git a/Copper68k.Tests/Synthetic/M68040CmLifetimeDiscoveryTests.cs b/Copper68k.Tests/Synthetic/M68040CmLifetimeDiscoveryTests.cs index b4f1d23c..fadba5c4 100644 --- a/Copper68k.Tests/Synthetic/M68040CmLifetimeDiscoveryTests.cs +++ b/Copper68k.Tests/Synthetic/M68040CmLifetimeDiscoveryTests.cs @@ -26,6 +26,7 @@ private void Audit(bool batch) var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); var export = Environment.GetEnvironmentVariable("COPPER68K_040_CM_LIFETIME_EXPORT"); using var rows = string.IsNullOrWhiteSpace(export) ? null : new StreamWriter(Path.Combine(export, group + ".rows")); + using var frameRows = string.IsNullOrWhiteSpace(export) ? null : new StreamWriter(Path.Combine(export, group + ".frames")); uint index = 0; foreach (var start in new[] { "ISP", "MSP" }) foreach (var result in Banks) @@ -86,6 +87,12 @@ void Phase(string name, ushort opcode) } else { M68040StackFixture.SetStacks(e, pointers, resultSr); e.Pc = 0x6000; } Phase("initial-RTE", 0x4e73); + if (frameRows != null) + { + var sp = m.Core.State.A[7]; + uint[] frameActual = odd ? [index, sp, m.PeekPhysical(sp, 2), m.PeekPhysical(sp + 2, 4), m.PeekPhysical(sp + 6, 2), m.PeekPhysical(sp + 8, 4)] : [index, 0, 0, 0, 0, 0]; + frameRows.WriteLine(string.Join(' ', frameActual.Select(Hex))); + } e.ControlChecks.Remove("format7 validation order"); if (odd) { diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index f0e7cf03..643c02c6 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -929,3 +929,22 @@ and fails on mismatches, unsupported/untested execution or missing/empty inputs. and compares its regenerated results. Ordinary synthetic and complete-040 selections exclude these gated `ReferenceDiscovery` tests. The 480-case required inventory and broader milestone 6 scope remain intact. + +Use `-MmuExceptionEntry` for the additional executed-entry profile. It replaces +the composed native boundary with unchanged `newcpu.cpp` SR helpers, the +odd-PC callback and exception dispatch, `Exception_mmu`, trace clearing and +`newcpu_common.cpp` frame construction. The original `fill_prefetch` executes +its compatibility-disabled early return. Physical transport records the actual +frame; unavailable exception, interrupt and compatible-cache paths fail. +Translation, run-loop behavior, trace delivery and hardware remain unqualified. +This profile requires the matching switch during report revalidation. + +The fixture exports raw frame SP/SR/PC/format/fault address immediately after +the initial return, before repair can overwrite them. In the current executed +MMU profile all 2,048 odd frames per route differ only in saved SR: the reference +stacks the restored SR, while Copper68k stacks the secondary incoming SR. All +other frame fields agree. CM still survives the native entry and repair, so +the MOVEM phase classifications remain unchanged. Both discoveries fail their +requested audit; neither software path's answer is promoted to hardware truth. +The passing handoff qualification and current continuation policy remain +separate from this raw-frame/CM lifetime research. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 1519697b..120b6c52 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -5754,3 +5754,77 @@ this run. Full CM fault/repair lifetime, pending/foreign contexts, original-trac retry, internal/data/writeback restoration, broader model reference coverage and consolidation remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`; no CPU fix, package release or regression retirement. + +### Scoped MMU exception entry executed during CM lifetime discovery + +The CM command adds `-MmuExceptionEntry` to replace its composed native boundary +with unchanged exception code from the same WinUAE pin. In addition to generated +RTE/repair/MOVEM/NOP and the original CM helper, this profile extracts native +`newcpu.cpp` MakeSR/MakeFromSR, exception3 handling, `Exception`/`ExceptionX`, +`Exception_mmu`, trace clearing, and `newcpu_common.cpp`'s frame builder. The +original `fill_prefetch` executes its `cpu_compatible=0` early return. No function +body is edited. The selected frame's physical writes and vector fetch are real +reference operations; the adapter supplies memory, cycle and disabled-feature +transport. Other exception, interrupt and compatible-cache paths fail explicitly. +It is scoped software entry coverage, not enabled translation, complete run-loop, +trace-delivery, cache/pipeline, timing or hardware qualification. + +Four literal controls still distinguish normal/CM and even/odd returns. The +native CM state survives actual exception entry, both repair stores and handler +RTE, then selects saved 4200 for MOVEM and clears. The fixture now additionally +exports raw frame SP/SR/PC/format/fault address immediately after the initial +return, before the repair stores erase evidence. Native execution compares these +bytes independently of the existing CPU fixture's composed frame expectation. + +Both scalar/batch tests execute without skips, with 4,096 scenarios / 18,432 +instruction phases / 576 combinations per route. Their phase classifications +remain 16,384 passing, 1,024 mismatching, zero unsupported and 1,024 untested; +only odd-PC CM MOVEM's D0 disagrees and its following sentinel is untested. +The new raw-frame comparison additionally checks 2,048 actual odd frames per +route. All differ only in saved SR: the MMU reference stacks restored SR; +Copper68k stacks the incoming secondary SR. All SP/PC/format/fault-address fields +agree, including both alignments and VBRs. Even scenarios export zero placeholders +and are not counted as actual frame comparisons. Total instruction phases remain +36,864; 4,096 frame comparisons/mismatches are reported separately. + +The source paths therefore disagree in a way a register-only comparison cannot +reveal. The previous composed reference and the retained non-MMU handoff path +must not be relabeled as this MMU exception execution. Nor should a software-path +vote override processor documentation. The [MC68040 addendum](https://www.nxp.com/docs/en/reference-manual/MC68040UMAD.pdf), +general-operation item 3, describes the traced-user-return saved-S correction; +this no-trace matrix does not qualify its complete interaction with CM. The +[MC68040 manual](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf), +8.4.6.2/7, specifies saved-EA MOVEM restart but does not settle every intervening +odd-return fault/repair lifetime in this composition. The saved-SR difference +and continuation persistence remain software discovery, not confirmed CPU defects. + +Final executed-entry evidence is +`artifacts/m6-cm-exception-comparison-final/`; the shared default profile is +freshly rerun in `artifacts/m6-cm-exception-default-final/`. Both requested +commands correctly exit 1. The default's unchanged instruction classifications +have zero raw frame comparisons, explicitly distinguished from agreement on +frames. Release builds have zero .NET warnings/errors; native warnings are +retained at untouched source sites. Source/input/generated/extracted/binary/ +evidence identities and the profile are mandatory. Report-only revalidation +executes the frozen observer and requires identical reference outputs. Supplying +the wrong profile fails before qualification can be inferred. + +Thirteen retained default controls and eight new executed-entry controls reject +invalid evidence. The latter cover wrong profile, missing frame identity, +truncated exported frame rows, altered reference frame bytes, and native missing, +empty, reordered or extra frame inputs. Fresh final controls are +`artifacts/m6-cm-exception-default-controls/controls.json` and +`artifacts/m6-cm-exception-entry-controls/controls.json`. Preliminary failed +harness builds/controls are not acceptance evidence. No current full CPU or +consumer rerun is claimed for these test/adapter-only changes. + +Frozen source starts at `b88f95016bab947eaee92b04282d0dee2bd399b4` plus the +recorded fixture/adapter/command changes. Production tree remains +`795fd12d6c0237a22a5f92f4a96cc4823e0364c1`. +CPU SHA256: `5bada3aedb413d5c9df20214fb7112e0edd8b4bc5d2977c89b61dc5b1070f20b`. +Adapter SHA256: `cdd945f93cb27493982fd5164834b10dc0b309b2307b53ba47156f22b3672201`. +The 480-case required inventory, ordinary/complete-040 requirements, other model +audits and consolidation scope remain intact. Full CM lifetime, pending/foreign +contexts, original-trace retry and internal/data/writeback restoration remain +required. Milestone 6 stays **in progress**, `roadmapComplete=false`; no production +CPU fix, package publication or regression retirement is included. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 0b07a78e..463a03ad 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2224,3 +2224,29 @@ inventory remains explicitly untested, and broader reference/consolidation work remains required. This discovery is excluded from ordinary synthetic CI and the complete-040 promoted selection. Milestone 6 stays **in progress**, `roadmapComplete=false`; no CPU fix, package publication or regression retirement. + +### Milestone 6 executed MMU exception entry and CM lifetime + +The CM command adds an explicit `-MmuExceptionEntry` profile. It executes +untouched pinned WinUAE native SR helpers, exception3 callback/dispatch, +`Exception_mmu`, frame builder and trace clearing. Original `fill_prefetch` +executes its compatibility-disabled return; physical transport does not enable +translation, caches or a run-loop/IRQ/trace/hardware oracle. Normal/CM, even/odd, +CCR/bank/alignment/VBR selections remain complete at their stated scope. + +Raw frames are captured before either repair store. Per route, all 2,048 odd +frames disagree only in stacked SR: the MMU path stacks the restored image; +Copper68k stacks the incoming secondary image. SP, stacked PC, format and fault +address agree. The MMU path still retains CM through exception entry and repair, +and the 18,432 instruction-phase classifications per route remain 16,384 passing, +1,024 mismatching, zero unsupported and 1,024 untested. These raw-frame +comparisons are reported separately from logical instruction counts. + +The default composed profile is rerun with unchanged classifications. Both +requested audits fail, and report revalidation executes the frozen native code. +The new profile is explicit in source/binary/evidence identities; eight dedicated +frame/profile integrity controls and thirteen retained default controls reject +invalid inputs. No software path is selected as hardware truth by majority or +compatibility. Full CM lifetime, pending/foreign contexts and every broader +reference/consolidation requirement remain required. Milestone 6 stays **in +progress**; ordinary and complete-040 coverage requirements are unchanged. diff --git a/scripts/reference/m68040-cm-lifetime.cpp b/scripts/reference/m68040-cm-lifetime.cpp index 67d50e1f..817eb992 100644 --- a/scripts/reference/m68040-cm-lifetime.cpp +++ b/scripts/reference/m68040-cm-lifetime.cpp @@ -1,7 +1,8 @@ // Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT // Untouched WinUAE MMU RTE/MOVEM and repair instructions on physical memory. -// The odd-PC exception frame is composed by this adapter: this is software -// discovery, not full Exception_mmu, enabled MMU, trace, timing or hardware proof. +// Default: composed exception boundary. WINUAE_EXECUTED_MMU_EXCEPTION uses +// untouched exception dispatch/frame construction on physical transport. +// Neither profile qualifies enabled MMU, trace, run-loop timing or hardware. #include #include #include @@ -17,11 +18,25 @@ using uae_s16=int16_t; using uae_s32=int32_t; using uaecptr=uint32_t; #define MMUDEBUGMISC 0 #define MMU_SSW_CM 0x1000 #define MMU_SSW_CT 0x2000 -#define SPCFLAG_MMURESTART 1 +#define SPCFLAG_MMURESTART 0x010000 #define m68k_areg(r,n) ((r).a[n]) #define m68k_dreg(r,n) ((r).d[n]) -struct Regs { uint32_t d[8]{},a[8]{},usp{},isp{},msp{},pc{},instruction_pc{}; uint16_t sr{}; int m{},s{},t0{},t1{},intmask{},ipl[1]{}; } regs; -struct Prefs { int cpu_model=68040; } currprefs; +struct Regs { + uint32_t d[8]{},a[8]{},usp{},isp{},msp{},pc{},instruction_pc{}; uint16_t sr{}; + int m{},s{},t0{},t1{},intmask{},ipl[1]{}; +#ifdef WINUAE_EXECUTED_MMU_EXCEPTION + uint32_t vbr{},buscr{},instruction_pc_user_exception{},mmu_ssw{},mmu_fslw{},mmu_fault_addr{}; + uint32_t fp_ea{},fp_opword{},wb2_address{},wb3_data{},mmu_effective_addr{}; + int exception{},loop_mode{},ipl_pin{},stopped{},ir{},ird{},read_buffer{},write_buffer{},irc{},wb2_status{},wb3_status{}; + int prefetch020_valid[3]{},pipeline_r8[2]{},pipeline_pos{},pipeline_stop{},prefetch020[3]{}; +#endif +} regs; +struct Prefs { + int cpu_model=68040; +#ifdef WINUAE_EXECUTED_MMU_EXCEPTION + int mmu_model=68040,cachesize{},cpu_compatible{},cpu_memory_cycle_exact{}; +#endif +} currprefs; int flag_SPCFLAG_TRACE,cpu_stopped,xflag,nflag,zflag,vflag,cflag; #define SET_XFLG(v) (xflag=(v)) #define SET_NFLG(v) (nflag=(v)) @@ -61,15 +76,73 @@ uint32_t get_ilong_mmu040(int offset) { return get_long_mmu040(regs.pc+offset); uint32_t m68k_getpci() { return regs.pc; } void m68k_setpci(uint32_t value) { regs.pc=value; } void m68k_incpci(int offset) { regs.pc+=offset; } +#ifndef WINUAE_EXECUTED_MMU_EXCEPTION void Exception(int) { throw std::runtime_error("Unexpected privilege exception"); } +#else +void REGPARAM2 Exception(int); +#endif void Exception_cpu(int) { throw std::runtime_error("Unexpected format exception"); } int callbackCount,cmAtCallback; uint16_t secondarySr; uint32_t oddPc; +#ifndef WINUAE_EXECUTED_MMU_EXCEPTION void exception3_read_prefetch_68040bug(uint32_t opcode,uint32_t address,uint16_t sr) { if(opcode!=0x4e73 || address!=0x6001) throw std::runtime_error("Unexpected odd-PC callback"); callbackCount++; secondarySr=sr; oddPc=address; cmAtCallback=mmu040_movem; } +#else +// Transport declarations used by untouched functions. Other exception paths +// fail explicitly; no architectural frame or saved SR is composed here. +#define _T(v) v +#define MAX_MMU030_ACCESS 9 +#define MMU030_STATEFLAG1_FMOVEM 0x2000 +#define MMU030_SSW_RW 0x40 +#define CACHE_DISABLE_ALLOCATE 0x08 +#define CPU_HALT_DOUBLE_FAULT 2 +#define SPCFLAG_TRACE 0x000040 +#define SPCFLAG_DOTRACE 0x000080 +#define SPCFLAG_INT 0x000008 +#define SPCFLAG_DOINT 0x000100 +#define sz_word 1 +bool m68k_accurate_ipl=false,cpu_tracer=false,transportSupervisor; +struct { int state; } cputrace; +uint32_t cache_default_data,last_fault_for_exception_3,last_addr_for_exception_3,last_op_for_exception_3; +int last_di_for_exception_3,last_fc_for_exception_3,last_size_for_exception_3,cpucycleunit=2; +bool last_writeaccess_for_exception_3,last_notinstruction_for_exception_3; +uint16_t last_sr_for_exception3; +uint32_t mmu040_move16[4],mmu030_state[3],mmu030_fmovem_store[2],mmu030_disp_store[2],mm030_stageb_address; +int mmu030_idx,mmu030_idx_done,mmu030_opcode; +struct Mmu030Access { uint32_t val; } mmu030_ad[MAX_MMU030_ACCESS+1]; +uint32_t m68k_getpc() { return regs.pc; } +void unset_special(int flags) { specialFlags&=~flags; } +void mmu_set_super(int value) { transportSupervisor=value!=0; } +void m68k_resumestopped() { if(regs.stopped) throw std::runtime_error("STOP outside exception profile"); } +void x_do_cycles(int) {} // Host cycle transport; timing is not compared. +uint32_t x_get_long(uint32_t a) { + if(regs.exception!=3 || a!=regs.vbr+12) throw std::runtime_error("Unexpected exception vector read"); + callbackCount++;cmAtCallback=mmu040_movem; + return get_long_mmu040(a); +} +void x_put_long(uint32_t a,uint32_t value) { put_long_mmu040(a,value); } +void x_put_word(uint32_t a,uint32_t value) { put(a,value,2); } +uint16_t x_get_word(uint32_t) { throw std::runtime_error("Non-040 prefetch outside profile"); } +void reset_pipeline_state() { throw std::runtime_error("Compatible pipeline outside profile"); } +void fill_icache040(uint32_t) { throw std::runtime_error("Compatible cache outside profile"); } +void fill_prefetch_020() { throw std::runtime_error("020 prefetch outside profile"); } +void fill_prefetch_030() { throw std::runtime_error("030 prefetch outside profile"); } +int iack_cycle(int) { throw std::runtime_error("Interrupt outside exception profile"); } +void cpu_halt(int) { throw std::runtime_error("Double fault outside exception profile"); } +void write_log(const char*,...) { throw std::runtime_error("Unexpected reference diagnostic"); } +void Exception_mmu030(int,uint32_t) { throw std::runtime_error("030 outside exception profile"); } +void Exception_normal(int) { throw std::runtime_error("Non-MMU dispatch outside exception profile"); } +void Exception_build_stack_frame_common(uint32_t,uint32_t,uint32_t,int,int) { throw std::runtime_error("Non-address frame outside exception profile"); } +static void exception3_read_special(uint32_t,uint32_t,int,int); +#endif // Strictly extracted unchanged from pinned source/generated output. +#ifdef WINUAE_EXECUTED_MMU_EXCEPTION +#include "winuae-cm-native-sr.inc" +#include "winuae-cm-exception.inc" +#else #include "winuae-cm-sr.inc" +#endif #include "winuae-cm-rte-helper.inc" #include "winuae-cm-ops.inc" @@ -87,6 +160,7 @@ void step(uint32_t opcode,uae_u32 (*fn)(uae_u32)) { fn(opcode); } // Explicitly composed boundary. Does not invoke WinUAE Exception_mmu. +#ifndef WINUAE_EXECUTED_MMU_EXCEPTION void composeAddressError() { regs.sr=static_cast((regs.sr|0x2000)&~0xc000); MakeFromSR(); regs.a[7]-=12; @@ -94,6 +168,8 @@ void composeAddressError() { put(regs.a[7]+6,0x200c,2); put(regs.a[7]+8,oddPc&~1u,4); regs.pc=0x9190; } +#endif +std::vector entryFrame; std::vector> execute(const std::vector& f) { if(f.size()!=8 || f[1]>1 || f[2]>3 || f[3]>1 || f[4]>1 || f[5]>1 || (f[6]!=0 && f[6]!=0x10000) || f[7]>31) throw std::runtime_error("Out-of-scope fixture"); @@ -105,7 +181,15 @@ std::vector> execute(const std::vector& f) { for(int n=0;n<8;n++) {regs.d[n]=0xa55a0022u+n*0x100;regs.a[n]=0x4000u+n*0x100;} regs.d[7]=2; regs.usp=0x7800+f[5]; regs.isp=0x4700+f[5]; regs.msp=0x7400+f[5]; regs.sr=initialSr; regs.s=1; regs.m=static_cast(f[1]); regs.a[7]=f[1]?regs.msp:regs.isp; regs.pc=0x1000; +#ifdef WINUAE_EXECUTED_MMU_EXCEPTION + transportSupervisor=true; +#endif MakeFromSR(); +#ifdef WINUAE_EXECUTED_MMU_EXCEPTION + regs.vbr=f[6];cache_default_data=0; + last_fault_for_exception_3=last_addr_for_exception_3=last_op_for_exception_3=0; + last_di_for_exception_3=last_fc_for_exception_3=last_size_for_exception_3=0;last_sr_for_exception3=0; +#endif for(const auto p:{regs.usp,regs.isp,regs.msp}) for(int n=-20;n<112;n++) put(p+n,static_cast(n^0x5a),1); const auto frame=regs.a[7]; put(frame,resultSr,2); put(frame+2,odd?0x6001:0x6000,4); put(frame+6,0x7008,2); @@ -120,7 +204,20 @@ std::vector> execute(const std::vector& f) { step(0x4e73,op_4e73_31_ff); if(callbackCount!=(odd?1:0) || mmu040_movem!=(cm?1:0) || (odd && cmAtCallback!=(cm?1:0))) throw std::runtime_error("RTE callback/CM lifetime differs"); +#ifndef WINUAE_EXECUTED_MMU_EXCEPTION if(odd) composeAddressError(); +#endif + entryFrame=odd?std::vector{regs.a[7],read(regs.a[7],2),read(regs.a[7]+2,4),read(regs.a[7]+6,2),read(regs.a[7]+8,4)}:std::vector(5,0); + if(odd) { +#ifdef WINUAE_EXECUTED_MMU_EXCEPTION + if(entryFrame[1]!=resultSr || last_sr_for_exception3!=initialSr || last_fault_for_exception_3!=0x6000 || !transportSupervisor) + throw std::runtime_error("Executed MMU frame/SR control failed"); +#else + if(entryFrame[1]!=initialSr) throw std::runtime_error("Composed frame/SR control failed"); +#endif + if(entryFrame[2]!=0x1000 || entryFrame[3]!=0x200c || entryFrame[4]!=0x6000) + throw std::runtime_error("Address frame control failed"); + } states.push_back(snapshot()); if(odd) { step(0x3ebc,op_30bc_31_ff); states.push_back(snapshot()); @@ -146,19 +243,34 @@ int main(int argc,char** argv) { try { for(int mask=1;mask<256;mask++) {int bit=0;while(!(mask&(1<(frameActual.begin()+1,frameActual.end())!=entryFrame) frameMismatches++; +#endif if(actual.empty() || !actual[0] || actual[0]>expected.size() || actual.size()!=1+actual[0]*21) throw std::runtime_error("Missing/invalid CPU phase states"); output<=actual[0]) {untested++;continue;} @@ -169,9 +281,18 @@ int main(int argc,char** argv) { output<<'\n'; rows++; } if(rows!=required) throw std::runtime_error("Missing reference rows"); +#ifdef WINUAE_EXECUTED_MMU_EXCEPTION + if(std::getline(frameInput,line)) throw std::runtime_error("Unexpected extra frame rows"); +#endif std::ofstream summary(argv[4]); if(!summary) throw std::runtime_error("Missing summary output"); - summary<<"{\"rows\":"< generator-run.log if($LASTEXITCODE -ne 0){throw 'Reference generation failed'} - $text=[IO.File]::ReadAllText((Join-Path $source 'cputest.cpp')) - $sr=@('void REGPARAM2 MakeSR(void)','void MakeFromSR_x(int t0trace)','void REGPARAM2 MakeFromSR_T0(void)','void REGPARAM2 MakeFromSR(void)')|ForEach-Object {FunctionText $text $_} - [IO.File]::WriteAllText((Join-Path $output 'winuae-cm-sr.inc'),($sr -join "`n`n")) + if($MmuExceptionEntry) { + $text=[IO.File]::ReadAllText((Join-Path $source 'newcpu.cpp')) + $sr=@('void REGPARAM2 MakeSR(void)','static void MakeFromSR_x(int t0trace)','void REGPARAM2 MakeFromSR_T0(void)','void REGPARAM2 MakeFromSR(void)')|ForEach-Object {FunctionText $text $_} + [IO.File]::WriteAllText((Join-Path $output 'winuae-cm-native-sr.inc'),($sr -join "`n`n")) + $exceptions=@(FunctionText ([IO.File]::ReadAllText((Join-Path $source 'newcpu_common.cpp'))) 'void Exception_build_stack_frame(uae_u32 oldpc, uae_u32 currpc, uae_u32 ssw, int nr, int format)') + foreach($signature in @('static bool internalexception(int nr)','static void exception_check_trace (int nr)','static void exception_debug (int nr)','void fill_prefetch (void)','static void Exception_mmu (int nr, uaecptr oldpc)','static void ExceptionX (int nr, uaecptr address, uaecptr oldpc)','void REGPARAM2 Exception(int nr)','static void exception3f(uae_u32 opcode, uaecptr addr, bool writeaccess, bool instructionaccess, bool notinstruction, uaecptr pc, int size, int fc, uae_u16 secondarysr)','static void exception3_read_special(uae_u32 opcode, uaecptr addr, int size, int fc)','void exception3_read_prefetch_68040bug(uae_u32 opcode, uaecptr addr, uae_u16 secondarysr)')){$exceptions+=FunctionText $text $signature} + [IO.File]::WriteAllText((Join-Path $output 'winuae-cm-exception.inc'),($exceptions -join "`n`n")) + } else { + $text=[IO.File]::ReadAllText((Join-Path $source 'cputest.cpp')) + $sr=@('void REGPARAM2 MakeSR(void)','void MakeFromSR_x(int t0trace)','void REGPARAM2 MakeFromSR_T0(void)','void REGPARAM2 MakeFromSR(void)')|ForEach-Object {FunctionText $text $_} + [IO.File]::WriteAllText((Join-Path $output 'winuae-cm-sr.inc'),($sr -join "`n`n")) + } [IO.File]::WriteAllText((Join-Path $output 'winuae-cm-rte-helper.inc'),(FunctionText ([IO.File]::ReadAllText((Join-Path $source 'cpummu.cpp'))) 'void m68k_do_rte_mmu040 (uaecptr a7)')) $text=[IO.File]::ReadAllText((Join-Path $output 'cpuemu_31.cpp')) $ops=@('4e73','30bc','217c','4cfa','4e71')|ForEach-Object {FunctionText $text "uae_u32 REGPARAM2 op_$($_)_31_ff(uae_u32 opcode)"} [IO.File]::WriteAllText((Join-Path $output 'winuae-cm-ops.inc'),($ops -join "`n`n")) - & cl /nologo /O2 /EHsc /W4 "/I$output" (Join-Path $repo 'scripts/reference/m68040-cm-lifetime.cpp') /Fe:observer.exe *> observer-build.log + [string[]]$profileFlags=if($MmuExceptionEntry){@('/DWINUAE_EXECUTED_MMU_EXCEPTION')}else{@()} + & cl /nologo /O2 /EHsc /W4 @profileFlags "/I$output" (Join-Path $repo 'scripts/reference/m68040-cm-lifetime.cpp') /Fe:observer.exe *> observer-build.log if($LASTEXITCODE -ne 0){throw 'Reference observer build failed'} $compiler=(& cl 2>&1|Out-String).Trim() } finally {Pop-Location;foreach($name in $savedCompiler.Keys){[Environment]::SetEnvironmentVariable($name,$savedCompiler[$name])}} @@ -72,15 +87,16 @@ if(-not $ValidateReportsOnly) { $testExit=$LASTEXITCODE } finally {foreach($name in $saved.Keys){[Environment]::SetEnvironmentVariable($name,$saved[$name])}} foreach($group in $groups) { - & (Join-Path $output 'observer.exe') (Join-Path $output "$group.rows") (Join-Path $output "$group.reference") 4096 (Join-Path $output "$group.reference.json") *> (Join-Path $output "$group.reference.log") + [string[]]$frameArguments=if($MmuExceptionEntry){@((Join-Path $output "$group.frames"))}else{@()} + & (Join-Path $output 'observer.exe') (Join-Path $output "$group.rows") (Join-Path $output "$group.reference") 4096 (Join-Path $output "$group.reference.json") @frameArguments *> (Join-Path $output "$group.reference.log") if($LASTEXITCODE -notin @(0,1)){throw "Reference execution failed: $group"} } $identity=@{schema=1;sourceCommit=(& git -C $repo rev-parse HEAD);cpuCommittedTree=(& git -C $repo rev-parse HEAD:Copper68k); reference='WinUAE-MMU-040-CM-lifetime-discovery';referenceCommit=$pin;referenceDirectory=$source; - softwareReferenceExecuted=$true;composedExceptionBoundary=$true;fullExceptionOracle=$false;architecturallyQualified=$false;hardwareQualified=$false;roadmapComplete=$false; - compiler=$compiler;compilerFlags='/O2 /EHsc /DWIN32 /DUNICODE /D_UNICODE; observer /W4'; - extraction='Untouched full generated _31 RTE/MOVE/MOVEM/NOP, cpummu m68k_do_rte_mmu040 and cputest SR helpers; original CPU_TESTER=0'; - caveats='Physical memory transport; manual odd-PC exception composition; no full Exception_mmu, enabled MMU, trace, IRQ, timing or hardware oracle'; + softwareReferenceExecuted=$true;composedExceptionBoundary=(-not $MmuExceptionEntry);mmuExceptionEntryExecuted=[bool]$MmuExceptionEntry;fullExceptionOracle=$false;architecturallyQualified=$false;hardwareQualified=$false;roadmapComplete=$false; + compiler=$compiler;compilerFlags=('/O2 /EHsc /DWIN32 /DUNICODE /D_UNICODE; observer /W4 '+($profileFlags -join ' ')); + extraction=$(if($MmuExceptionEntry){'Untouched full generated _31 RTE/MOVE/MOVEM/NOP, cpummu CM helper, newcpu SR/exception3/ExceptionX/Exception_mmu and newcpu_common frame builder; original CPU_TESTER=0'}else{'Untouched full generated _31 RTE/MOVE/MOVEM/NOP, cpummu m68k_do_rte_mmu040 and cputest SR helpers; original CPU_TESTER=0'}); + caveats=$(if($MmuExceptionEntry){'Physical transport; executed scoped MMU exception entry/frame writes and original fill_prefetch compatibility-disabled return; no cache/translation/run-loop/trace/IRQ/timing/hardware oracle; unavailable paths fail'}else{'Physical memory transport; manual odd-PC exception composition; no full Exception_mmu, enabled MMU, trace, IRQ, timing or hardware oracle'}); inputs=@();referenceInputs=@();generated=@();evidence=@()} foreach($file in (@(& git -C $repo ls-files 'Copper68k/*')+$fixtures)){$identity.inputs+=@{file=$file;sha256=(Hash (Join-Path $repo $file))}} foreach($file in $referenceFiles){$identity.referenceInputs+=@{file=$file;sha256=(Hash (Join-Path $source $file))}} @@ -90,9 +106,10 @@ if(-not $ValidateReportsOnly) { } $identity=Get-Content (Join-Path $output 'identities.json') -Raw|ConvertFrom-Json if($identity.schema -ne 1 -or $identity.reference -cne 'WinUAE-MMU-040-CM-lifetime-discovery' -or $identity.referenceCommit -cne $pin -or - $identity.softwareReferenceExecuted -ne $true -or $identity.composedExceptionBoundary -ne $true -or $identity.fullExceptionOracle -ne $false -or + $identity.softwareReferenceExecuted -ne $true -or $identity.fullExceptionOracle -ne $false -or $identity.architecturallyQualified -ne $false -or $identity.hardwareQualified -ne $false -or $identity.roadmapComplete -ne $false -or $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.cpuCommittedTree -notmatch '^[a-f0-9]{40}$'){throw 'Incomplete CM discovery identity'} +if($identity.mmuExceptionEntryExecuted -ne [bool]$MmuExceptionEntry -or $identity.composedExceptionBoundary -ne (-not $MmuExceptionEntry)){throw 'CM exception profile differs; specify -MmuExceptionEntry for executed-entry outputs'} CheckIdentities $identity.inputs (@(& git -C $repo ls-files 'Copper68k/*')+$fixtures) $repo 'fixture/CPU' CheckIdentities $identity.referenceInputs $referenceFiles $source 'reference source' CheckIdentities $identity.generated $generated $output 'generated reference' @@ -114,7 +131,7 @@ foreach($alignment in @(0,1)){foreach($vbr in @(0,0x10000)){ if($keys.Count -ne 576 -or $fixtureRows.Count -ne 4096){throw 'Independent CM discovery enumeration differs'} $replay=Join-Path $output ('reference-recheck-'+[guid]::NewGuid().ToString('N')) if($ValidateReportsOnly){New-Item -ItemType Directory $replay|Out-Null} -$totals=@{passing=0;mismatching=0;unsupported=0;untested=0};$reports=@();$passedBatches=0 +$totals=@{passing=0;mismatching=0;unsupported=0;untested=0};$reports=@();$passedBatches=0;$frameMismatches=0;$frameComparisons=0 foreach($group in $groups) { $r=Get-Content (Join-Path $output "68040-$group.json") -Raw|ConvertFrom-Json -AsHashtable if($r.schema -ne 1 -or $r.model -cne '68040' -or $r.group -cne $group -or $r.xunitBatches -ne 1 -or $r.logicalCases -ne 18432 -or $r.combinations.Count -ne 576){throw 'Incomplete CM discovery report'} @@ -133,22 +150,29 @@ foreach($group in $groups) { if($split.Count -ne 2 -or $split[0] -cne $fixtureRows[$n]){throw 'Missing/reordered reference fixture combination'} } if($ValidateReportsOnly) { - & (Join-Path $output 'observer.exe') (Join-Path $output "$group.rows") (Join-Path $replay "$group.reference") 4096 (Join-Path $replay "$group.reference.json") *> (Join-Path $replay "$group.log") + [string[]]$frameArguments=if($MmuExceptionEntry){@((Join-Path $output "$group.frames"))}else{@()} + & (Join-Path $output 'observer.exe') (Join-Path $output "$group.rows") (Join-Path $replay "$group.reference") 4096 (Join-Path $replay "$group.reference.json") @frameArguments *> (Join-Path $replay "$group.log") if($LASTEXITCODE -notin @(0,1) -or (Hash (Join-Path $replay "$group.reference")) -cne (Hash (Join-Path $output "$group.reference")) -or (Hash (Join-Path $replay "$group.reference.json")) -cne (Hash (Join-Path $output "$group.reference.json"))){throw 'Recorded reference differs from executed replay'} } $reference=Get-Content (Join-Path $output "$group.reference.json") -Raw|ConvertFrom-Json - if($reference.rows -ne 4096 -or $reference.phases -ne 18432 -or $reference.fixedControls -ne 4 -or $reference.composedExceptionBoundary -ne $true -or + if($reference.rows -ne 4096 -or $reference.phases -ne 18432 -or $reference.fixedControls -ne 4 -or $reference.composedExceptionBoundary -ne (-not $MmuExceptionEntry) -or $reference.mmuExceptionEntryExecuted -ne [bool]$MmuExceptionEntry -or $reference.fullExceptionOracle -ne $false -or $reference.hardwareQualified -ne $false -or $reference.passing -ne $sums.passing -or $reference.mismatching -ne $sums.mismatching -or $reference.untested -ne $sums.untested -or $sums.unsupported -ne 0){throw 'CPU/reference phase classifications differ'} + if($MmuExceptionEntry) { + $frameLines=[IO.File]::ReadAllLines((Join-Path $output "$group.frames")) + if($frameLines.Count -ne 4096 -or $reference.frameComparisons -ne 2048 -or $reference.PSObject.Properties.Name -cnotcontains 'frameMismatches' -or $reference.frameMismatches -lt 0 -or $reference.frameMismatches -gt 4096){throw 'Missing or invalid exception frame comparison'} + $frameMismatches+=$reference.frameMismatches + $frameComparisons+=$reference.frameComparisons + } elseif($reference.frameMismatches -ne 0 -or $reference.frameComparisons -ne 0){throw 'Unexpected composed-profile frame comparison'} if(($sums.mismatching+$sums.unsupported+$sums.untested) -eq 0){$passedBatches++} $reports+=@{group=$group;logicalCases=18432;combinations=576;rows=4096;counts=$r.counts} } if([int]$c.passed -ne $passedBatches -or [int]$c.failed -ne (2-$passedBatches)){throw 'xUnit and discovery classifications differ'} -$passed=$passedBatches -eq 2 -if(-not $ValidateReportsOnly -and (($testExit -eq 0) -ne $passed)){throw 'Test exit and discovery classifications differ'} +$passed=$passedBatches -eq 2 -and $frameMismatches -eq 0 +if(-not $ValidateReportsOnly -and (($testExit -eq 0) -ne ($passedBatches -eq 2))){throw 'Test exit and discovery classifications differ'} @{schema=1;logicalCases=36864;batches=2;reports=$reports;counts=$totals;passed=$passed;softwareReferenceExecuted=$true; architecturallyQualified=$false;fullExceptionOracle=$false;hardwareQualified=$false;roadmapComplete=$false; - referenceCommit=$pin;composedExceptionBoundary=$true;caveats=$identity.caveats}| + referenceCommit=$pin;composedExceptionBoundary=(-not $MmuExceptionEntry);mmuExceptionEntryExecuted=[bool]$MmuExceptionEntry;frameMismatches=$frameMismatches;frameComparisons=$frameComparisons;caveats=$identity.caveats}| ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'discovery-summary.json') -if(-not $passed){throw "CM lifetime software discovery remains mismatching or incomplete: $($totals|ConvertTo-Json -Compress)"} +if(-not $passed){throw "CM lifetime software discovery remains mismatching or incomplete: $($totals|ConvertTo-Json -Compress); frame mismatches=$frameMismatches"} From a71460dcecd219a751556f8070b26ee44beca7aa Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 14:06:16 +0300 Subject: [PATCH 064/211] Qualify 68060 PCR fields and consolidate proven regression --- Copper68k.Tests/M68060InterpreterTests.cs | 18 +-- Copper68k.Tests/Synthetic/README.md | 17 +++ .../SyntheticMovecRegisterFixture.cs | 7 +- .../Synthetic/SyntheticPcrTests.cs | 96 +++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 92 ++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 30 ++++ scripts/test-copper68k-060-pcr.ps1 | 132 ++++++++++++++++++ .../test-copper68k-synthetic-mutations.ps1 | 31 +++- scripts/test-copper68k-synthetic.ps1 | 4 + 9 files changed, 408 insertions(+), 19 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticPcrTests.cs create mode 100644 scripts/test-copper68k-060-pcr.ps1 diff --git a/Copper68k.Tests/M68060InterpreterTests.cs b/Copper68k.Tests/M68060InterpreterTests.cs index c0a2e7fd..46eb2464 100644 --- a/Copper68k.Tests/M68060InterpreterTests.cs +++ b/Copper68k.Tests/M68060InterpreterTests.cs @@ -45,21 +45,9 @@ public void RemovedIntegersTrapBeforeOperandEffects(int opcode, int extension) } } - [Fact] - public void ProcessorIdentificationIsReadOnlyAndResetClearsControls() - { - var bus = new Copper68kTestBus(0x10000); - bus.WriteWords(0x1000, 0x4E7B, 0x0808, 0x4E7A, 0x1808); - using var cpu = M68kCoreFactory.Default.Create(M68kCpuModel.M68060, bus); - cpu.Reset(0x1000, 0x7000); - cpu.State.D[0] = uint.MaxValue; - cpu.ExecuteInstruction(); cpu.ExecuteInstruction(); - Assert.Equal(0x04300083u, cpu.State.D[1]); - cpu.Reset(0x1004, 0x7000); - cpu.ExecuteInstruction(); - Assert.Equal(0x04300000u, cpu.State.D[1]); - Assert.Equal(0x2700, cpu.State.StatusRegister); - } + // PCR identification/reset regression consolidated into SyntheticPcrTests. + // Replacement cases and three before/after mutation proofs are recorded in + // docs/COPPER68K_REFERENCE_QUALIFICATION.md (68060 PCR qualification). [Theory] [InlineData(0x802)] // No CAAR, MSP, ISP or MMUSR. diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 643c02c6..19455315 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -948,3 +948,20 @@ the MOVEM phase classifications remain unchanged. Both discoveries fail their requested audit; neither software path's answer is promoted to hardware truth. The passing handoff qualification and current continuation policy remain separate from this raw-frame/CM lifetime research. + +68060 PCR fields use `SyntheticPcrTests` and the shared MOVEC register fixture. +All defined EDEBUG/DFP/ESS combinations, read-only identification/revision probes, +privilege, general registers, CCR preservation and actual reset/readback are +covered. `system-pcr-reserved-policy` separately retains the old invalid-reserved +write mask convention; it is not architectural maskset qualification. MC68060DE +I14/I15's bit-5 workarounds and the pinned WinUAE bit-6 EDEBUG disagreement remain +explicit. Physical debug output, superscalar timing and pending-FPU behavior +are outside these register-field checks. + +`./scripts/test-copper68k-060-pcr.ps1` runs four matrices (305,152 cases) and checks +source/binary/evidence identities, exact nonempty test selection, independently +enumerated combination keys and weights. `-ValidateReportsOnly` requires the +same output directory and identities. Three `-Scope ProcessorConfiguration` +mutations prove the replacements detect the retired PCR identification/reset +fact's defects. The qualification document records before/after proof, scope and +remaining requirements; the whole roadmap remains incomplete. diff --git a/Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs b/Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs index cb161682..eca4a67d 100644 --- a/Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs +++ b/Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs @@ -17,15 +17,16 @@ private static readonly (string Name, Func Read)[] Preserved ("DTT1", s => s.M68040Mmu.DataTransparentTranslation1), ("URP", s => s.M68040Mmu.UserRootPointer), ("SRP", s => s.M68040Mmu.SupervisorRootPointer), - ("BUSCR", s => s.M68060BusControl) + ("BUSCR", s => s.M68060BusControl), + ("PCR", s => s.M68060ProcessorConfiguration) ]; - internal void WritesAndReadback(SyntheticMachine m, CoverageBatch report, IEnumerable values, int storeRegisters = 16, uint initial = 0) + internal void WritesAndReadback(SyntheticMachine m, CoverageBatch report, IEnumerable values, int storeRegisters = 16, uint initial = 0, IEnumerable? conditionCodes = null) { foreach (var value in values) for (var general = 0; general < storeRegisters; general++) foreach (var supervisor in new[] { false, true }) - for (var ccr = 0; ccr < 32; ccr++) + foreach (var ccr in conditionCodes ?? Enumerable.Range(0, 32)) { m.Reset(ccr, supervisor); if (general < 8) m.Core.State.D[general] = value; diff --git a/Copper68k.Tests/Synthetic/SyntheticPcrTests.cs b/Copper68k.Tests/Synthetic/SyntheticPcrTests.cs new file mode 100644 index 00000000..0f97ca0a --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticPcrTests.cs @@ -0,0 +1,96 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticPcrTests(ITestOutputHelper output) +{ + // MC68060UM 3.2.2.5 / figure 3-5 / 11.1.2.1.1 / D-22: + // first MC68060 revision, EDEBUG=7, DFP=1, ESS=0; ID/revision read-only. + // Physical debug output, superscalar timing and FPU arithmetic are not tested. + private const uint Identification = 0x04300000; + private static readonly uint[] Controls = [0, 1, 2, 3, 0x80, 0x81, 0x82, 0x83]; + private static SyntheticMachine Machine() => new(ModelSpec.All.Single(m => m.Id == "68060")); + private static SyntheticMovecRegisterFixture Fixture() => new(0x808, "PCR", uint.MaxValue, + s => s.M68060ProcessorConfiguration, (s, v) => s.M68060ProcessorConfiguration = Identification | v, + (_, value) => Identification | (value & 0x83)); + + [Fact, Trait("Suite", "Synthetic")] + public void PcrDefinedControlsAllRegistersPrivilegeAndCcr() + { + var m = Machine(); var report = new CoverageBatch("68060", "system-pcr-defined"); + var fixture = Fixture(); + foreach (var initial in Controls) fixture.WritesAndReadback(m, report, Controls, initial: initial); + fixture.RawReads(m, report, Controls.Select(v => Identification | v)); + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void PcrIdentificationAndRevisionWritesAreIgnored() + { + var m = Machine(); var report = new CoverageBatch("68060", "system-pcr-identification"); + // Bits 8..31 are explicitly ignored on write. Never set reserved 6..2 here. + var high = new uint[] { 0, 0xffffff00, 0x55555500, 0xaaaaaa00 } + .Concat(Enumerable.Range(8, 24).Select(bit => 1u << bit)).Distinct().Order().ToArray(); + Assert.Equal(28, high.Length); + var values = high.SelectMany(v => Controls.Select(c => v | c)).ToArray(); + foreach (var initial in Controls) Fixture().WritesAndReadback(m, report, values, initial: initial, conditionCodes: [0, 31]); + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void PcrReservedWritesRetainRepositoryProfilePolicy() + { + // These writes violate the manual's requirement to keep bits 6..2 zero. + // Preserve the existing exact-mask regression as repository policy only. + // MC68060DE I14/I15 assign bit 5 on specific masksets; no hardware claim. + var m = Machine(); var report = new CoverageBatch("68060", "system-pcr-reserved-policy"); + uint[] values = [4, 8, 16, 32, 64, 0x7c, 0xffffff7c, uint.MaxValue]; + foreach (var initial in Controls) Fixture().WritesAndReadback(m, report, values, initial: initial, conditionCodes: [0, 31]); + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void PcrResetClearsDefinedControlsFromBothStacks() + { + var m = Machine(); var report = new CoverageBatch("68060", "system-pcr-reset"); + foreach (var image in Controls) + for (var general = 0; general < 16; general++) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + m.Reset(ccr); + m.Core.State.D[0] = image; + var e = SyntheticExecution.Prepare(m, [0x4e7b, 0x0808, 0x4e7a, (ushort)(general << 12 | 0x808)]); + e.Pc = SyntheticMachine.Code + 4; + e.ControlChecks["PCR"] = (s => s.M68060ProcessorConfiguration, Identification | image); + var id = $"68060/MOVEC/L/PCR/reset/R{general}/super={supervisor}/image={image:X8}"; + var sample = $"/op=4E7B/ccr={ccr:X2}"; + if (!Step(m, e, report, id + "/write" + sample)) { Untested(report, id, sample, "reset", "read"); continue; } + m.Core.State.StatusRegister = (ushort)((supervisor ? 0x2700 : 0x0700) | ccr); + e = ArchitecturalExpectation.Capture(m); + Array.Clear(e.D); Array.Clear(e.A); e.A[7] = 0x8000; + e.Sr = 0x2700; e.Pc = SyntheticMachine.Code + 4; + e.InactiveStackPointer = 0; e.MasterStackPointer = 0x8000; + e.ControlChecks["PCR"] = (s => s.M68060ProcessorConfiguration, Identification); + // Reset's register clearing is a public API convention, while PCR + // control clearing and supervisor/IPL state are architectural rules. + m.Core.Reset(e.Pc, 0x8000); + var mismatch = e.Verify(m); + report.Record(id + "/reset" + sample, mismatch == null ? "passing" : "mismatching", mismatch); + if (mismatch != null) { Untested(report, id, sample, "read"); continue; } + if (general < 8) e.D[general] = Identification; else e.A[general - 8] = Identification; + e.Pc += 4; + SyntheticExecution.Run(m, e, report, id + "/read" + sample); + } + report.Complete(output); + } + + private static void Untested(CoverageBatch report, string id, string sample, params string[] phases) + { foreach (var phase in phases) report.Record(id + "/" + phase + sample, "untested", "Prerequisite failed; no instruction retry."); } + private static bool Step(SyntheticMachine m, ArchitecturalExpectation e, CoverageBatch report, string id) + { + try { m.Core.ExecuteInstruction(); var mismatch = e.Verify(m); report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); return mismatch == null; } + catch (Exception ex) { report.Record(id, ex is UnsupportedM68kOpcodeException or UnsupportedM68kTimingException or UnsupportedM68040InstructionException ? "unsupported" : "mismatching", ex.ToString()); return false; } + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 120b6c52..b799c1f1 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -5828,3 +5828,95 @@ audits and consolidation scope remain intact. Full CM lifetime, pending/foreign contexts, original-trace retry and internal/data/writeback restoration remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`; no production CPU fix, package publication or regression retirement is included. + +### 68060 PCR qualification and consolidation (2026-10-06) + +`SyntheticPcrTests` adds four ordinary-CI batches and **305,152 logical cases**. +Expectations come from [MC68060UM revision 1](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf), +3.2.2.5 / figure 3-5, 11.1.2.1.1 and D-22. Identification is 0430, the selected +first-revision byte is zero, EDEBUG is bit 7, DFP bit 1 and ESS bit 0. The +identification/revision fields ignore writes; the defined controls clear on +reset. The [1998 addendum](https://www.nxp.com/docs/en/reference-manual/MC68060UMAD.pdf) +does not amend this register diagram. + +| Group | Logical cases | Architectural combinations | Qualification | +| --- | ---: | ---: | --- | +| `system-pcr-defined` | 102,400 | 102,400 | All eight initial/control images, sixteen general registers, all CCRs, supervisor writes/readback and user privilege rejection; all defined raw read images | +| `system-pcr-identification` | 172,032 | 172,032 | Every read-only bit, all-set/alternating high patterns, all eight control images, registers and both stacks, CCR 0/31 | +| `system-pcr-reserved-policy` | 6,144 | 6,144 | Existing repository mask policy for reserved writes; explicitly excluded from architectural qualification | +| `system-pcr-reset` | 24,576 | 768 | Actual write, external API reset and instruction readback, all defined control images, incoming stacks, registers and CCRs | + +The reusable MOVEC register fixture gains optional CCR selections and guards the +PCR during other control transfers. Tests check all architectural registers, +exact PC, SR/CCR, stack state, execution state, unchanged controls and memory. +Successful readbacks include a following NOP. Failed writes/reset prerequisites +leave dependent phases explicitly untested; no partially executed instruction +is retried. Initial control images are fixture setup, not decoder-derived +expectations. Reset register clearing and the saved MSP initialization are +public API conventions, distinguished from the processor's PCR/SR reset rules. + +The manual requires reserved bits 6..2 to remain zero. Moreover, +[MC68060DE rev 4.0](https://www.nxp.com/docs/en/errata/MC68060DE.pdf), I14/I15, +assign bit 5 to a bypass workaround on specified masksets. Reserved writes +therefore remain a named repository-policy batch, never promoted as proof that +every physical maskset ignores these bits. Physical debug output, superscalar +timing, pending-FPU synchronization and FPU arithmetic are not qualified here. +The fixtures start with no outstanding floating-point work. + +Pinned WinUAE `5d22d33632646efc3f747f03e82d28353e52722e` still writes EDEBUG using +bit 6 in `newcpu_common.cpp` lines 172-173. Its unchanged source SHA256 is +`ef87202a97c0135128e7367a0fc09c627d29fc606d5de7fc06ad5f49d8bb9cbc`. +This software/manual disagreement remains open. No reference mask, production +CPU behavior or original Basic discrepancy is changed to obtain agreement. +Ordinary STOP's S-clear disagreement also remains required. + +The dedicated command independently enumerates every combination and its +weight, verifies exactly four executed tests, rejects failed/unsupported/untested +cases, and records exact production/fixture/command, binary and evidence input +identities. Report revalidation checks the same source/binary identities and +selection. Eight integrity controls reject missing manifests, wrong profiles, +missing/duplicate source identities, missing reports, empty execution, incorrect +weights and substituted combinations. None changes the original accepted files. + +```powershell +./scripts/test-copper68k-060-pcr.ps1 -OutputDirectory artifacts/m6-pcr-qualified-final +./scripts/test-copper68k-060-pcr.ps1 -ValidateReportsOnly -OutputDirectory artifacts/m6-pcr-qualified-final +./scripts/test-copper68k-synthetic-mutations.ps1 -Scope ProcessorConfiguration +``` + +Retirement: `M68060InterpreterTests.ProcessorIdentificationIsReadOnlyAndResetClearsControls` +is replaced only after all three mutations fail both the original fact and the +complete replacement matrix. Before retirement each selection executes two +failed tests without skips; afterward each executes the replacement without skips. + +| Mutation | Replacement witness | Mismatching / dependent untested | +| --- | --- | ---: | +| EDEBUG moved to bit 6 | `PCR/R0/initial=00000000/value=00000080/super=True/ccr=00/write` | 16,384 / 16,384 | +| Identification writable | `PCR/R0/initial=00000000/value=00000000/super=True/ccr=00/write` | 57,344 / 57,344 | +| Reset retains controls | `PCR/reset/R0/super=False/image=00000001/reset/op=4E7B/ccr=00` | 7,168 / 7,168 | + +The old all-ones write is retained explicitly in the reserved-policy matrix +(`R0/initial=00000000/value=FFFFFFFF/super=True/ccr=00/write` and `/read`); +reset/readback from its resulting image 83 is covered for every destination, +including R1. No specialized instruction, cache, bus, JIT or native regression +is retired. Mutation sources are restored byte-for-byte and rebuilt successfully. + +Fresh acceptance: four PCR tests pass with no skips, **305,152 cases** and no +mismatches/unsupported/untested cases. Retained shared-fixture/control coverage +passes **46 tests**, including **1,236,864 cases / fifteen reports** plus 31 +fixed 68060 checks, with no skips. Evidence is in `artifacts/m6-pcr-qualified-final`, +`artifacts/m6-pcr-retained`, `artifacts/m6-pcr-mutations-before-retirement`, +`artifacts/m6-pcr-mutations-after-retirement` and `artifacts/m6-pcr-integrity`. +The first reset attempt had an incorrect fixture MSP expectation and is excluded +from acceptance; its trace remains under `artifacts/m6-pcr-first` in test outputs. + +Source starts at `11124e9a24fb13c832ed569626ec2b76f6242696` plus recorded test/command +changes. Production tree remains `795fd12d6c0237a22a5f92f4a96cc4823e0364c1`. +CPU SHA256: `a44b20aed377d1789607415ed50fde48a8ebea38cbc49cc50a08cc0ad4a0aff8`. +Test assembly SHA256: `0ebdff1cff679ada0488fd29735e99e10a705ef33ca3f8798ecfe838706a554f`. +The ordinary CI requirement grows to **82,919,318 cases / 676 reports**; this +is an expanded requirement, not a new broad-suite execution claim. Complete-040 +requirements and the 480-case remaining inventory are unchanged. Full CM +lifetime, advanced exception/restoration and broader model audits/consolidation +remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`. +No production CPU fix, consumer replay or package publication is included. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 463a03ad..219cde62 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2250,3 +2250,33 @@ invalid inputs. No software path is selected as hardware truth by majority or compatibility. Full CM lifetime, pending/foreign contexts and every broader reference/consolidation requirement remain required. Milestone 6 stays **in progress**; ordinary and complete-040 coverage requirements are unchanged. + +### Milestone 6 68060 PCR fields and demonstrated consolidation + +Four new ordinary matrices add 305,152 cases: 102,400 defined-field transfers, +172,032 identification/revision writes, 6,144 explicitly nonarchitectural +reserved-write policy cases, and 24,576 actual write/reset/read phases. The +selected first-revision PCR and writable bit positions come from MC68060UM +3.2.2.5 / figure 3-5. Registers, stacks, all canonical CCRs, privilege, exact PC, +readback/sentinels, untouched controls and memory remain checked. Read-only +bit probes and reserved-policy samples use CCR 0/31 as a separate group. + +The shared MOVEC fixture now preserves PCR during other control transfers. All +four new tests and 46 retained tests pass without skips; retained shared-fixture +coverage adds 1,236,864 passing cases in fifteen reporting batches. The new +audit command checks identities, precise architectural keys/weights and exactly +four executed tests. Eight negative report controls reject incomplete inputs. + +Three maintained PCR mutations fail both the original identification/reset fact +and its complete replacements before retirement, and still fail afterward. +Only that demonstrated duplicate is removed; its all-ones write remains in an +explicit repository-policy batch. The original manual requires reserved bits +zero; maskset errata give bit 5 a physical workaround role. This policy batch +does not establish maskset behavior or debug/FPU/superscalar timing correctness. + +The pinned WinUAE bit-6 EDEBUG/software disagreement and ordinary STOP S-clear +rule remain open. Ordinary coverage now requires 82,919,318 cases / 676 reports; +complete-040 requirements and the 480-case untested inventory stay intact. No +new broad-suite result, production CPU fix, consumer replay or package release +is claimed. Full CM lifetime, advanced restoration, broader reference audits +and remaining consolidation are still required; milestone 6 stays **in progress**. diff --git a/scripts/test-copper68k-060-pcr.ps1 b/scripts/test-copper68k-060-pcr.ps1 new file mode 100644 index 00000000..dfc74bee --- /dev/null +++ b/scripts/test-copper68k-060-pcr.ps1 @@ -0,0 +1,132 @@ +[CmdletBinding()] +param([switch]$ValidateReportsOnly, [string]$OutputDirectory = 'artifacts/060-pcr') +$ErrorActionPreference = 'Stop' +$repo = Split-Path -Parent $PSScriptRoot +$output = [IO.Path]::GetFullPath($OutputDirectory, $repo) +if ((Test-Path -LiteralPath $output) -and -not $ValidateReportsOnly) { $output = Join-Path $output ([guid]::NewGuid().ToString('N')) } +$identityPath = Join-Path $output 'identity.json' +$controls = @(0, 1, 2, 3, 0x80, 0x81, 0x82, 0x83) +$groups = [ordered]@{'system-pcr-defined'=102400; 'system-pcr-identification'=172032; 'system-pcr-reserved-policy'=6144; 'system-pcr-reset'=24576} +function Identity([string]$RelativePath) { + $path = Join-Path $repo $RelativePath + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Missing PCR input: $RelativePath" } + return @{path=$RelativePath; sha256=(Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash} +} +function Check-Identities($Items) { + foreach ($item in $Items) { if ((Identity $item.path).sha256 -cne $item.sha256) { throw "PCR input identity changed: $($item.path)" } } +} +function Add-Key($Keys, [string]$Id, [int]$Weight = 1) { + if (-not $Keys.TryAdd($Id, $Weight)) { throw "Duplicate expected PCR key: $Id" } +} +function Write-Keys($Keys, $Values, $Ccrs) { + foreach ($initial in $controls) { foreach ($value in $Values) { foreach ($general in 0..15) { + foreach ($supervisor in @($false, $true)) { foreach ($ccr in $Ccrs) { + $id = '68060/MOVEC/L/PCR/R{0}/initial={1:X8}/value={2:X8}/super={3}/ccr={4:X2}' -f $general,$initial,$value,$supervisor,$ccr + if (-not $supervisor) { Add-Key $Keys "$id/privilege" } + else { Add-Key $Keys "$id/write"; Add-Key $Keys "$id/read" } + } } + } } } +} +function Expected-Keys([string]$Group) { + $keys = [Collections.Generic.Dictionary[string,int]]::new([StringComparer]::Ordinal) + switch ($Group) { + 'system-pcr-defined' { + Write-Keys $keys $controls (0..31) + foreach ($image in $controls) { foreach ($general in 0..15) { foreach ($ccr in 0..31) { + Add-Key $keys ('68060/MOVEC/L/PCR/read-R{0}/internal={1:X8}/ccr={2:X2}' -f $general,(0x04300000 -bor $image),$ccr) + } } } + } + 'system-pcr-identification' { + $high = [Collections.Generic.HashSet[uint32]]::new() + foreach ($v in @(0, [uint32]0xffffff00L, [uint32]0x55555500L, [uint32]0xaaaaaa00L)) { [void]$high.Add($v) } + foreach ($bit in 8..31) { [void]$high.Add([uint32]([uint64]1 -shl $bit)) } + $values = @($high | Sort-Object | ForEach-Object { $v=$_; $controls | ForEach-Object { [uint32]($v -bor $_) } }) + if ($values.Count -ne 224) { throw 'Empty/incomplete PCR identification selection' } + Write-Keys $keys $values @(0,31) + } + 'system-pcr-reserved-policy' { Write-Keys $keys @(4,8,16,32,64,0x7c,[uint32]0xffffff7cL,[uint32]::MaxValue) @(0,31) } + 'system-pcr-reset' { + foreach ($general in 0..15) { foreach ($supervisor in @($false,$true)) { foreach ($image in $controls) { foreach ($phase in @('write','reset','read')) { + Add-Key $keys ('68060/MOVEC/L/PCR/reset/R{0}/super={1}/image={2:X8}/{3}' -f $general,$supervisor,$image,$phase) 32 + } } } } + } + default { throw "Unknown PCR group: $Group" } + } + return ,$keys +} +$savedReports = [Environment]::GetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR') +Push-Location $repo +try { + $inputPaths = @(& git ls-files Copper68k) + $inputPaths += @('Copper68k.Tests/Synthetic/SyntheticPcrTests.cs', 'Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs', + 'Copper68k.Tests/Synthetic/SyntheticMachine.cs', 'Copper68k.Tests/Synthetic/ArchitecturalExpectation.cs', + 'Copper68k.Tests/Synthetic/SyntheticExecution.cs', 'Copper68k.Tests/Synthetic/SyntheticMoveTests.cs', + 'Copper68k.Tests/Copper68k.Tests.csproj', 'Copper68k.Tests/M68060InterpreterTests.cs', + 'scripts/test-copper68k-060-pcr.ps1', 'scripts/test-copper68k-synthetic.ps1', 'scripts/test-copper68k-synthetic-mutations.ps1') + $inputPaths = @($inputPaths | Sort-Object -Unique) + $binaryPaths = @('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') + $evidencePaths = @($groups.Keys | ForEach-Object { [IO.Path]::GetRelativePath($repo,(Join-Path $output "68060-$_.json")) }) + + @([IO.Path]::GetRelativePath($repo,(Join-Path $output 'pcr.trx'))) + if (-not $ValidateReportsOnly) { + New-Item -ItemType Directory -Path $output -Force | Out-Null + $inputs = @($inputPaths | Sort-Object -Unique | ForEach-Object { Identity $_ }) + [Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR', $output) + & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --filter 'FullyQualifiedName~SyntheticPcrTests' --logger 'trx;LogFileName=pcr.trx' --results-directory $output *> (Join-Path $output 'run.log') + if ($LASTEXITCODE -ne 0) { throw "PCR execution failed; see $output/run.log" } + Check-Identities $inputs + $binaries = @($binaryPaths | ForEach-Object { Identity $_ }) + @{schema=1; profile='68060-pcr-manual-fields-and-explicit-policy'; sourceRevision=(& git rev-parse HEAD).Trim(); + inputs=$inputs; binaries=@($binaries); + evidence=@($evidencePaths | ForEach-Object { Identity $_ })} | + ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $identityPath + } + if (-not (Test-Path -LiteralPath $identityPath -PathType Leaf)) { throw 'Missing PCR identity manifest' } + $identity = Get-Content -LiteralPath $identityPath -Raw | ConvertFrom-Json + if ($identity.schema -ne 1 -or $identity.profile -cne '68060-pcr-manual-fields-and-explicit-policy' -or + $identity.sourceRevision -cnotmatch '^[0-9a-f]{40}$' -or @($identity.inputs).Count -lt 11 -or + @($identity.binaries).Count -ne 2 -or @($identity.evidence).Count -ne 5) { throw 'Incomplete PCR identity manifest' } + foreach ($selection in @(@{actual=@($identity.inputs); required=$inputPaths}, @{actual=@($identity.binaries); required=$binaryPaths}, @{actual=@($identity.evidence); required=$evidencePaths})) { + $names = @($selection.actual | ForEach-Object { $_.path }) + if ($names.Count -ne $selection.required.Count -or @($names | Select-Object -Unique).Count -ne $names.Count -or + @($names | Where-Object { $_ -cnotin $selection.required }).Count -ne 0) { throw 'PCR input selection differs from required identities' } + foreach ($item in $selection.actual) { if ($item.sha256 -cnotmatch '^[0-9A-F]{64}$') { throw 'Malformed PCR input hash' } } + } + Check-Identities $identity.inputs; Check-Identities $identity.binaries; Check-Identities $identity.evidence + [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'pcr.trx') -Raw + $c = $trx.TestRun.ResultSummary.Counters + if ($c.total -ne 4 -or $c.executed -ne 4 -or $c.passed -ne 4 -or $c.failed -ne 0 -or $c.notExecuted -ne 0) { throw 'Empty or incomplete PCR test selection' } + $methods = @('PcrDefinedControlsAllRegistersPrivilegeAndCcr','PcrIdentificationAndRevisionWritesAreIgnored', + 'PcrReservedWritesRetainRepositoryProfilePolicy','PcrResetClearsDefinedControlsFromBothStacks') + foreach ($method in $methods) { + if (@($trx.TestRun.Results.UnitTestResult | Where-Object { $_.testName -ceq "Copper68k.Tests.Synthetic.SyntheticPcrTests.$method" -and $_.outcome -ceq 'Passed' }).Count -ne 1) { throw "Missing PCR execution: $method" } + } + $coverage = @() + foreach ($group in $groups.Keys) { + $report = Get-Content -LiteralPath (Join-Path $output "68060-$group.json") -Raw | ConvertFrom-Json -AsHashtable + if ($report.schema -ne 1 -or $report.model -cne '68060' -or $report.group -cne $group -or $report.xunitBatches -ne 1 -or + $report.logicalCases -ne $groups[$group] -or $report.counts.passing -ne $groups[$group] -or + $report.counts.mismatching -ne 0 -or $report.counts.unsupported -ne 0 -or $report.counts.untested -ne 0 -or $report.failures.Count -ne 0) { throw "Incomplete PCR gate: $group" } + $keys = Expected-Keys $group + if ($keys.Count -ne $report.combinations.Count) { throw "PCR combination selection differs: $group" } + foreach ($key in $keys.Keys) { + if (-not $report.combinations.Contains($key) -or $report.combinations[$key].Count -ne 1 -or $report.combinations[$key].passing -ne $keys[$key]) { + throw "Missing/misweighted PCR combination: $key" + } + } + $coverage += @{group=$group; logicalCases=$groups[$group]; combinations=$keys.Count; + qualification=$(if ($group -eq 'system-pcr-reserved-policy') {'Repository mask policy; architecturally excluded reserved writes'} + elseif ($group -eq 'system-pcr-reset') {'Manual PCR/SR reset rules plus public API reset convention'} else {'Manual-defined PCR register fields'})} + } + @{schema=1; model='68060'; logicalCases=305152; xunitBatches=4; coverage=$coverage; roadmapComplete=$false; + softwareReferenceAgreement=$false; physicalTimingQualified=$false; + reference=@{manual='MC68060UM revision 1, section 3.2.2.5 / figure 3-5 / 11.1.2.1.1 / D-22'; + url='https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf'; addendum='MC68060UMAD rev 0.1, 1998-03-25'; + masksetCaveat='MC68060DE rev 4.0 I14/I15 assign PCR bit 5 for particular masks; excluded from defined-field matrix'}; + remaining='Pinned WinUAE bit-6 EDEBUG discrepancy, ordinary STOP S-clear disagreement, FPU/debug physical effects and all broader reference/consolidation requirements remain'} | + ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'summary.json') + Write-Host "PCR qualified fields and explicit policy: 305152 cases / 4 batches; reports at $output" +} +finally { + [Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR', $savedReports) + Pop-Location +} diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index ac89af4c..e74f06d9 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','ChainedOddRte','ChainedOddAccessRte','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','RteSoftwareTrace','UserRteFault','UserRteTrace','UserRteSoftwareTrace','MixedEpochRte','MixedEpochFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','ChainedOddRte','ChainedOddAccessRte','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','RteSoftwareTrace','UserRteFault','UserRteTrace','UserRteSoftwareTrace','MixedEpochRte','MixedEpochFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings','ProcessorConfiguration')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -592,6 +592,21 @@ foreach ($mutation in @( $mutation.group='ChainedOddAccessFrameCanonical'; $mutation.model='68040'; $mutation.milestone=6 $mutation.chainedOdd=$true; $mutation.chainedOddAccess=$true; $mutations += $mutation } +$mutations += @( + @{name='060-pcr-edebug-position'; file='Copper68k/M68060Interpreter.cs'; before='State.M68060ProcessorConfiguration = 0x0430_0000 | (value & 0x83);'; + after='State.M68060ProcessorConfiguration = 0x0430_0000 | (value & 0x43);'; group='PcrDefinedControlsAllRegistersPrivilegeAndCcr'; pcr=$true; + expectedGroup='system-pcr-defined'; expectedCases=102400; milestone=6; + legacyTest='ProcessorIdentificationIsReadOnlyAndResetClearsControls'; legacyFile='Copper68k.Tests/M68060InterpreterTests.cs'}, + @{name='060-pcr-writable-identification'; file='Copper68k/M68060Interpreter.cs'; before='State.M68060ProcessorConfiguration = 0x0430_0000 | (value & 0x83);'; + after='State.M68060ProcessorConfiguration = value & 0xffffff83;'; group='PcrIdentificationAndRevisionWritesAreIgnored'; pcr=$true; + expectedGroup='system-pcr-identification'; expectedCases=172032; milestone=6; + legacyTest='ProcessorIdentificationIsReadOnlyAndResetClearsControls'; legacyFile='Copper68k.Tests/M68060InterpreterTests.cs'}, + @{name='060-pcr-reset-retains-controls'; file='Copper68k/M68060Interpreter.cs'; before='State.M68060ProcessorConfiguration = 0x0430_0000; // First revision, ESS/DFP clear.'; + after='// Mutant: reset retains previous PCR controls.'; group='PcrResetClearsDefinedControlsFromBothStacks'; pcr=$true; + expectedGroup='system-pcr-reset'; expectedCases=24576; milestone=6; + legacyTest='ProcessorIdentificationIsReadOnlyAndResetClearsControls'; legacyFile='Copper68k.Tests/M68060InterpreterTests.cs'} +) +if ($Scope -eq 'ProcessorConfiguration') { $mutations = @($mutations | Where-Object { $_.pcr }) } if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } if ($Scope -eq 'Logical') { $mutations = @($mutations | Where-Object { $_.milestone -eq 4 }) } @@ -642,6 +657,7 @@ try { [IO.File]::WriteAllText($path, $text.Replace($before, $after), [Text.UTF8Encoding]::new($false)) $model = $(if ($mutation.model) { $mutation.model } elseif ($mutation.name -eq '060-divide-frame') { '68060' } else { '68020' }) $filter = "FullyQualifiedName~$($mutation.group)&DisplayName~$model" + if ($mutation.pcr) { $filter = "FullyQualifiedName~$($mutation.group)" } if ($mutation.repair) { # Keep the original repair proof's two complete batches; the # new retained-trace facts have their own four-route gate. @@ -864,6 +880,19 @@ try { throw 'Cache mutation did not execute its complete selection' } } + if ($mutation.pcr) { + [xml]$pcrTrx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + $expectedTests = 1 + [int][bool]$legacyPresent + if ($pcrTrx.TestRun.ResultSummary.Counters.executed -ne $expectedTests -or + $pcrTrx.TestRun.ResultSummary.Counters.failed -ne $expectedTests -or + $pcrTrx.TestRun.ResultSummary.Counters.notExecuted -ne 0 -or $batches.Count -ne 1 -or + $batches[0].model -cne '68060' -or $batches[0].group -cne $mutation.expectedGroup -or + $batches[0].logicalCases -ne $mutation.expectedCases -or $batches[0].counts.unsupported -ne 0 -or + $batches[0].counts.mismatching -le 0 -or + @($batches[0].failures | Where-Object { $_.reason.StartsWith('PCR expected ', [StringComparison]::Ordinal) }).Count -eq 0) { + throw 'PCR mutation did not execute and detect its complete declared matrix' + } + } if ($legacyPresent) { [xml]$legacyTrx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if (@($legacyTrx.TestRun.Results.UnitTestResult | Where-Object { ($_.testName.EndsWith($mutation.legacyTest) -or $_.testName.Contains($mutation.legacyTest + '(')) -and $_.outcome -eq 'Failed' }).Count -ne 1) { throw 'Original regression did not detect the same mutation' } diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index a584c001..7df5e77f 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -136,6 +136,10 @@ try { $expected['system-bus-control-state'] = 2064 $expected['system-bus-control-interrupts'] = 4096 $expected['system-movec-control-encodings'] = 536832 + $expected['system-pcr-defined'] = 102400 + $expected['system-pcr-identification'] = 172032 + $expected['system-pcr-reserved-policy'] = 6144 + $expected['system-pcr-reset'] = 24576 } if ($model -eq '68010') { $expected['system-format8-entry'] = 1024 From e2c456bcf140196497807baa3a7f44569eca64a3 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 14:33:56 +0300 Subject: [PATCH 065/211] Record 68060 STOP software-reference discovery --- .../Synthetic/M68060StopDiscoveryTests.cs | 104 +++++++++++++ Copper68k.Tests/Synthetic/README.md | 25 +++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 104 +++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 33 ++++ scripts/reference/m68060-stop.cpp | 108 +++++++++++++ scripts/test-copper68k-060-stop-discovery.ps1 | 142 ++++++++++++++++++ 6 files changed, 516 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/M68060StopDiscoveryTests.cs create mode 100644 scripts/reference/m68060-stop.cpp create mode 100644 scripts/test-copper68k-060-stop-discovery.ps1 diff --git a/Copper68k.Tests/Synthetic/M68060StopDiscoveryTests.cs b/Copper68k.Tests/Synthetic/M68060StopDiscoveryTests.cs new file mode 100644 index 00000000..d02d0735 --- /dev/null +++ b/Copper68k.Tests/Synthetic/M68060StopDiscoveryTests.cs @@ -0,0 +1,104 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Discovery of pinned WinUAE's undocumented ordinary STOP S-clear rule. +// Privilege/trace frames use manual rules; no full native exception or IRQ oracle. +public sealed class M68060StopDiscoveryTests(ITestOutputHelper output) +{ + internal const string Enable = "COPPER68K_RUN_060_STOP_DISCOVERY"; + [EnvironmentFact(Enable, "compare 68060 ordinary STOP software-reference outcomes"), Trait("Suite", "ReferenceDiscovery")] + public void DefinedStatusImagesScalar() => Audit(false); + [EnvironmentFact(Enable, "compare 68060 ordinary STOP software-reference outcomes"), Trait("Suite", "ReferenceDiscovery")] + public void DefinedStatusImagesBatch() => Audit(true); + + private void Audit(bool batch) + { + var group = "stop-discovery-" + (batch ? "batch" : "scalar"); + var report = new CoverageBatch("68060", group); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68060")); + var export = Environment.GetEnvironmentVariable("COPPER68K_060_STOP_EXPORT"); + if (string.IsNullOrWhiteSpace(export)) throw new InvalidOperationException("STOP discovery requires an export directory"); + using var rows = new StreamWriter(Path.Combine(export, group + ".rows")); + uint index = 0; + foreach (var matrix in new[] { "images", "ccr" }) + foreach (var supervisor in new[] { false, true }) + foreach (var master in new[] { 0, 0x1000 }) + foreach (var trace in new[] { 0, 0x8000 }) + foreach (var ccr in matrix == "images" ? new[] { 0, 31 } : Enumerable.Range(0, 32)) + foreach (var resultS in new[] { 0, 0x2000 }) + foreach (var resultM in new[] { 0, 0x1000 }) + foreach (var resultT in new[] { 0, 0x8000 }) + foreach (var ipl in matrix == "images" ? Enumerable.Range(0, 8) : new[] { 0, 7 }) + foreach (var resultCcr in Enumerable.Range(0, 32)) + { + m.Reset(ccr, supervisor); + var sr = (ushort)((supervisor ? 0x2700 : 0x0700) | master | trace | ccr); + var immediate = (ushort)(resultS | resultM | resultT | ipl << 8 | resultCcr); + m.Core.State.StatusRegister = sr; + foreach (var stack in new[] { 0x4700u, 0x7800u }) + for (var offset = -20; offset < 16; offset++) m.InitializePhysical(unchecked(stack + (uint)offset), (uint)(offset ^ 0x5a), 1); + var e = SyntheticExecution.Prepare(m, [0x4e72, immediate]); + var initialD = (uint[])e.D.Clone(); var initialA = (uint[])e.A.Clone(); + var vector = !supervisor || resultS == 0 ? 8 : trace != 0 ? 9 : 0; + var boundarySr = vector == 8 ? sr : immediate; + var boundaryPc = vector == 8 ? SyntheticMachine.Code : SyntheticMachine.Code + 4; + e.ControlChecks["PCR preserved"] = (s => s.M68060ProcessorConfiguration, 0x04300000); + if (vector == 8) SyntheticExecution.ExpectException(m, e, 8); + else + { + SyntheticSystemTests.ApplyStatus(m, e, immediate); + if (vector == 9) SyntheticExecution.ExpectException(m, e, 9, e.Pc); + else e.Stopped = true; + } + if (vector != 0) + { + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, boundarySr); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, boundaryPc); + } + var id = $"68060/STOP/{matrix}/super={supervisor}/M={master:X4}/T={trace:X4}/S={resultS:X4}/newM={resultM:X4}/newT={resultT:X4}/IPL={ipl}"; + var sample = $"/op=4E72/ccr={ccr:X2}/newCcr={resultCcr:X2}"; + var valid = M68040StackFixture.Step(m, e, report, id + "/execute" + sample, batch); + var actualVector = Math.Max(0, m.Core.State.LastExceptionVector); + var actualSr = actualVector == 0 ? m.Core.State.StatusRegister : m.Core.State.LastExceptionStatusRegister; + var actualPc = actualVector == 0 ? m.Core.State.ProgramCounter : m.Core.State.LastExceptionStackedProgramCounter; + var fields = new uint[] { index++, sr, immediate, (uint)vector, boundarySr, boundaryPc, + (uint)actualVector, actualSr, actualPc, 0x7800, 0x4700 }.Concat(initialD).Concat(initialA); + rows.WriteLine(string.Join(' ', fields.Select(v => v.ToString("X8")))); + if (vector == 0) + { + if (!valid) report.Record(id + "/inert" + sample, "untested", "STOP prerequisite failed; no retry"); + else + { + // Architectural stopped state, independent of idle timing. + if (batch) + { + var boundary = new Boundary(); + var count = ((Copper68k.IM68kBatchCore)m.Core).ExecuteInstructions(1, m.Core.State.Cycles + 1000, boundary); + // The existing batch API counts an idle attempt as one + // logical step. It must preserve architectural state; + // this count is not physical instruction retirement. + var mismatch = count != 1 || boundary.Before != 1 || boundary.After != 1 + ? $"Stopped batch count/callbacks differ: {count}/{boundary.Before}/{boundary.After}" + : e.Verify(m); + report.Record(id + "/inert" + sample, mismatch == null ? "passing" : "mismatching", mismatch); + } + else + { + m.Core.ExecuteInstruction(); + var mismatch = e.Verify(m); + report.Record(id + "/inert" + sample, mismatch == null ? "passing" : "mismatching", mismatch); + } + } + } + } + Assert.Equal(163840u, index); + report.Complete(output); + } + private sealed class Boundary : Copper68k.IM68kInstructionBoundary + { + public int Before, After; + public bool BeforeInstruction() { Before++; return true; } + public void AfterInstruction(long previousCycle, long currentCycle) => After++; + } +} diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 19455315..96431c74 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -965,3 +965,28 @@ same output directory and identities. Three `-Scope ProcessorConfiguration` mutations prove the replacements detect the retired PCR identification/reset fact's defects. The qualification document records before/after proof, scope and remaining requirements; the whole roadmap remains incomplete. + +`M68060StopDiscoveryTests` is a gated `ReferenceDiscovery` selection for ordinary +68060 STOP. Run `./scripts/test-copper68k-060-stop-discovery.ps1` with the same +pristine WinUAE pin and MSVC setup as the CM command. Scalar/batch each cover +163,840 defined-status/CCR scenarios plus 20,480 inert attempts, with exact +register, stack, PC, SR, memory and exception-boundary checks. Incoming IPL is +fixed at 7; target status images cover all defined bits. Batch idle steps use +the existing API count of one, without retiring the following instruction. + +The observer executes unchanged generated `_33` STOP and native SR/stop helpers. +It observes privilege callbacks, normalizes stopped PC to the architectural +next PC and composes incoming trace; native exception frames, run-loop trace, +IRQ and hardware timing are outside scope. Four literal controls protect the +transport. The pinned source calls its S-clear privilege rule undocumented; +it disagrees with Copper68k on 40,960 cases per route. Each route otherwise has +143,360 passing cases, zero unsupported and zero untested cases. No CPU fix is +selected from this software disagreement. + +`-ValidateReportsOnly -OutputDirectory` checks the same exact identities, +independently enumerated keys/weights/fixture rows and both named test executions, +then reruns the frozen observer. Both commands currently fail explicitly on +81,920 combined discrepancies. Missing/empty/malformed inputs must also fail. +The qualification document records thirteen integrity controls and retained +test evidence. Discovery does not promote coverage into ordinary CI or +complete-040, retire regressions or complete milestone 6. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index b799c1f1..c7d27650 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -5920,3 +5920,107 @@ requirements and the 480-case remaining inventory are unchanged. Full CM lifetime, advanced exception/restoration and broader model audits/consolidation remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`. No production CPU fix, consumer replay or package publication is included. + +### 68060 ordinary STOP software-reference discovery (2026-10-06) + +`M68060StopDiscoveryTests` compares scalar and batch execution with untouched +pinned WinUAE `5d22d33632646efc3f747f03e82d28353e52722e`. The original +`gencpu.cpp` STOP section labels the target-S-clear behavior undocumented: +68060 rejects it immediately, before transferring the new SR. Copper68k +currently transfers that SR and either stops or delivers incoming trace. +This is a software-reference discrepancy, not a qualified architectural fix. + +[M68000PM](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf), STOP, +6-85, describes SR transfer, advancing PC and stopping execution when the +incoming processor state is supervisor. [MC68060UM revision 1](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf), +8.2.5 and 8.2.6, supplies incoming privilege and trace rules and format-zero/ +format-two boundaries. Its LPSTOP entry D-19 explicitly rejects a new S-clear +status image; that statement is not an explicit ordinary-STOP rule. These +manual sections do not settle the undocumented ordinary-STOP discrepancy. +No guessed production restriction or timing change is introduced. + +Each route enumerates 163,840 STOP scenarios: 32,768 target-status image cases +and 131,072 canonical CCR cases. Target images use defined mask B71F; both +incoming stacks, initial/new M and T1, all target IPLs in the image group and +IPL 0/7 in the CCR group are covered. Incoming IPL is fixed at 7. The image +group uses input CCR 0/31; the CCR group covers all 32 by 32 combinations. +Successful nontraced STOP adds 20,480 inert attempts, giving **184,320 logical +cases / 720 combinations / one xUnit batch per route**. + +All registers, stack pointers, PC, SR/CCR, stopped state, exception boundaries, +guarded memory and preserved PCR are checked independently. Privilege saves +original SR/opcode PC; trace saves loaded SR/next PC. The batch API counts an +idle attempt as one logical step, with one before/after callback. This is an +existing API convention, not physical instruction retirement. Inert attempts +must leave architectural state unchanged and never execute the following NOP. +There is no instruction retry after partial effects. + +The observer executes unchanged generated `op_4e72_33_ff` from CPU_TESTER=0, +and original `MakeSR`, `MakeFromSR_x`, `MakeFromSR_STOP`, `m68k_set_stop` and +`do_cycles_stop` functions. Four literal accepted, incoming-user, target-S-clear +and incoming-trace controls guard transport. The original native stopped PC +remains at the opcode until resume; comparison explicitly normalizes the +architectural next PC to 1004. Incoming trace is a composed boundary, not +native trace/run-loop execution. Privilege is an observed native callback; +native exception entry and frame writes are not executed. The native helper's +stopped reentry is checked separately from Copper68k's inert architectural +state. No IRQ, enabled MMU, cache, bus-cycle or hardware oracle is claimed. + +| Route | Passing | Mismatching | Unsupported | Untested | +| --- | ---: | ---: | ---: | ---: | +| Scalar | 143,360 | 40,960 | 0 | 0 | +| Batch | 143,360 | 40,960 | 0 | 0 | + +Both tests execute and fail without skips. Every mismatch has incoming +supervisor state and a target S-clear image; incoming trace does not override +the reference's privilege rejection. Per route the native observer executes +163,840 rows, reporting 122,880 matching and 40,960 mismatching boundaries; +the extra 20,480 passing logical cases are Copper68k inert attempts. These +different denominators are intentional. Full frame checks belong to the +synthetic manual expectation, not to the native boundary-only comparison. + +```powershell +./scripts/test-copper68k-060-stop-discovery.ps1 -OutputDirectory artifacts/m6-stop-discovery-final +./scripts/test-copper68k-060-stop-discovery.ps1 -ValidateReportsOnly -OutputDirectory artifacts/m6-stop-discovery-final +``` + +Both commands terminate with the explicit **81,920 software-reference +mismatches** failure after validating and replaying both routes. They record +pristine pinned sources, exact fixture/CPU sources, generated/extracted code, +binaries and evidence identities. Validation independently enumerates keys, +weights, fixture ordering, defined SR selections and canonical registers/ +stacks; it requires both named tests with no skips. Frozen report validation +executes the observer again and requires byte-identical outputs. Missing, +empty or malformed native selections fail rather than count as coverage. + +Thirteen integrity controls reject missing manifests, wrong profiles, missing +source identities/fixtures/reports, empty execution, substituted keys, altered +weights, wrong registers and reordered rows, plus missing/empty/wrong-register +native fixtures. Semantic controls refresh the altered evidence hash to test +content validation as well as identity validation. Originals remain unchanged. +Evidence is under `artifacts/m6-stop-discovery-final`, +`artifacts/m6-stop-discovery-revalidate.log` and `artifacts/m6-stop-integrity`. +Preliminary `m6-stop-discovery-first` had a fixture build error; `initial` and +`debug` incorrectly expected zero batch idle steps. These attempts are excluded +from the final classifications; no production defect is inferred from them. + +Fresh Release build has zero .NET warnings/errors. The observer has two +warnings in unchanged extracted reference code (SR narrowing and unused +opcode). Retained STOP/LPSTOP, system privilege, trace, idle-batch and 68060 +coverage passes **75 tests without skips**, containing **331,582 cases / 25 +reports** and fifty fixed examples, under `artifacts/m6-stop-retained-final`. +Source starts at `a71460dcecd219a751556f8070b26ee44beca7aa` plus recorded fixture/ +observer/command changes; production tree stays +`795fd12d6c0237a22a5f92f4a96cc4823e0364c1`. +CPU SHA256: `424896b6ca007b4f6294c25e6672b6a560ceee4ef5b2cf6afa17a5e8e4cdc1cd`. +Test SHA256: `4f34672737725504a906666ad6abef23653b25e976a12036e7415bb510cb9043`. + +The discovery remains outside ordinary synthetic CI and promoted complete-040 +coverage. Their requirements stay **82,919,318 cases / 676 reports** and +**95 tests / 82 reports / thirteen fixed examples / 67,645,984 phases including +inventory** respectively. The 480 untested inventory cases and all broader +qualification/consolidation requirements remain. Ordinary STOP's architectural +S-clear rule, full external trace/exception/IRQ qualification and PCR's software/ +manual disagreement are still open. Milestone 6 stays **in progress**, +`architecturallyQualified=false`, `roadmapComplete=false`. No CPU fix, regression +retirement, consumer replay or package release is included. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 219cde62..d5e0402f 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2280,3 +2280,36 @@ complete-040 requirements and the 480-case untested inventory stay intact. No new broad-suite result, production CPU fix, consumer replay or package release is claimed. Full CM lifetime, advanced restoration, broader reference audits and remaining consolidation are still required; milestone 6 stays **in progress**. + +### Milestone 6 68060 ordinary STOP discovery + +Two gated discovery matrices cover every defined target SR image and all +canonical input/target CCR combinations, incoming user/supervisor stacks, +initial/new M and T1, target IPLs and inert stopped attempts. Incoming IPL is +fixed at 7. Scalar and batch each execute 184,320 logical cases / 720 +combinations: 143,360 passing, 40,960 mismatching, zero unsupported/untested. +Both named tests execute without skips. All discrepancies are supervisor STOP +with a new S-clear image. + +Untouched pinned WinUAE generated STOP and native SR/stop helpers reject these +operands before SR transfer. That source calls the behavior undocumented; +MC68060UM explicitly states it for LPSTOP but does not settle ordinary STOP. +The observer executes the native privilege callback, composes incoming trace +and normalizes stopped PC; full external exception/frame/IRQ/run-loop/hardware +qualification is not claimed. No production CPU behavior is changed. + +The command validates exact identities, independently generated keys/weights, +fixture ordering, SR selection and canonical initial registers/stacks, then +replays frozen native code. Both fresh execution and report revalidation fail +with the same 81,920 combined software-reference mismatches. Thirteen integrity +controls reject malformed/missing/empty selections. Seventy-five retained tests +pass without skips, covering 331,582 cases / 25 reports and fifty fixed checks. +The corrected fixture preserves the existing batch idle-step convention; +preliminary fixture failures are excluded from final evidence. + +This remains an explicitly unqualified discovery outside ordinary CI and the +promoted complete-040 selection. Their counts and the 480-case required untested +inventory remain unchanged. Ordinary STOP's S-clear architectural rule, full +CM lifetime, advanced restoration, PCR disagreement, broader independent model +qualification and consolidation all remain required. Milestone 6 stays **in +progress**, `roadmapComplete=false`; no regression retirement or release. diff --git a/scripts/reference/m68060-stop.cpp b/scripts/reference/m68060-stop.cpp new file mode 100644 index 00000000..9e0f0d45 --- /dev/null +++ b/scripts/reference/m68060-stop.cpp @@ -0,0 +1,108 @@ +// Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT +// Unchanged generated 060 STOP plus original SR/stop helpers on physical transport. +// Trace entry/frame/IRQ/run-loop behavior is not executed or qualified here. +#include +#include +#include +#include +#include +#include +#include +using uae_u16=uint16_t; using uae_u32=uint32_t; using uae_s16=int16_t; +#define REGPARAM2 +#define CYCLE_UNIT 2 +#define SPCFLAG_TRACE 0x40 +#define SPCFLAG_DOTRACE 0x80 +#define SPCFLAG_INT 8 +#define SPCFLAG_DOINT 0x100 +#define LED_CPU 0 +#define m68k_areg(r,n) ((r).a[n]) +struct Regs { uint32_t d[8]{},a[8]{},usp{},isp{},msp{},pc{},spcflags{}; uint16_t sr{},ir{}; int s{},m{},t0{},t1{},intmask{},stopped{},ipl[1]{},ipl_pin{}; } regs; +struct Prefs { int cpu_model=68060,mmu_model{},cachesize{},cpu_compatible{},cpu_memory_cycle_exact{}; } currprefs; +struct Gui { int cpu_stopped{}; } gui_data; +int cpu_last_stop_vpos=-1,vpos{},xflag,nflag,zflag,vflag,cflag,callbacks,callbackVector,reads,cpucycleunit=1; +bool m68k_accurate_ipl=false; +#define SET_XFLG(v) (xflag=(v)) +#define SET_NFLG(v) (nflag=(v)) +#define SET_ZFLG(v) (zflag=(v)) +#define SET_VFLG(v) (vflag=(v)) +#define SET_CFLG(v) (cflag=(v)) +#define GET_XFLG() xflag +#define GET_NFLG() nflag +#define GET_ZFLG() zflag +#define GET_VFLG() vflag +#define GET_CFLG() cflag +void set_special(int v) { regs.spcflags|=v; } +void unset_special(int v) { regs.spcflags&=~v; } +void activate_trace() { unset_special(SPCFLAG_TRACE); set_special(SPCFLAG_DOTRACE); } +void gui_led(int,int,int) {} +void mmu_set_super(int) { throw std::runtime_error("Enabled MMU outside STOP profile"); } +void checkint() {} // No asserted IRQ; interrupt/run-loop behavior is outside profile. +void do_cycles(int) {} // Host timing transport, not a timing oracle. +void x_do_cycles(int) { throw std::runtime_error("Compatible cycles outside profile"); } +uint16_t immediate; +uint16_t get_iword_mmu060(int offset) { if(offset!=2 || regs.pc!=0x1000) throw std::runtime_error("Unexpected instruction read"); reads++; return immediate; } +void Exception(int vector) { if(vector!=8 || ++callbacks!=1) throw std::runtime_error("Unexpected exception callback"); callbackVector=vector; } +#include "winuae-stop-sr.inc" +void m68k_setstopped(int type) { m68k_set_stop(type); } +#include "winuae-stop-op.inc" +struct Outcome { uint32_t vector,sr,pc,rawStopped; }; +Outcome execute(uint16_t inputSr,uint16_t target,const std::vector& values) { + regs={}; gui_data={}; callbacks=callbackVector=reads=0; + regs.sr=inputSr; regs.s=(inputSr>>13)&1; regs.m=(inputSr>>12)&1; + regs.t1=(inputSr>>15)&1; regs.intmask=(inputSr>>8)&7; + xflag=(inputSr>>4)&1;nflag=(inputSr>>3)&1;zflag=(inputSr>>2)&1;vflag=(inputSr>>1)&1;cflag=inputSr&1; + regs.pc=0x1000;regs.usp=0x7800;regs.isp=regs.msp=0x4700; + for(int n=0;n<8;n++){regs.d[n]=values[n];regs.a[n]=values[n+8];} + if(regs.t1) set_special(SPCFLAG_TRACE); + immediate=target; op_4e72_33_ff(0x4e72); MakeSR(); + if(callbackVector) { + if(regs.stopped || regs.sr!=inputSr || regs.pc!=0x1000 || reads!=(regs.s?1:0)) throw std::runtime_error("Privilege callback transport differs"); + for(int n=0;n<8;n++)if(regs.d[n]!=values[n]||regs.a[n]!=values[n+8])throw std::runtime_error("Rejected STOP changed registers"); + if(regs.usp!=0x7800||regs.isp!=0x4700)throw std::runtime_error("Rejected STOP changed stacks"); + return {8,regs.sr,regs.pc,static_cast(regs.stopped)}; + } + if(reads!=1 || regs.stopped!=1 || regs.pc!=0x1000) throw std::runtime_error("STOP transport differs"); + // The original stopped PC remains at the opcode until resume. The manual + // architectural next PC is +4. Incoming trace composes a post-load boundary; + // full native trace processing/frame writes are deliberately not simulated. + const auto result=Outcome{static_cast((inputSr&0x8000)?9:0),regs.sr,0x1004,1}; + const auto oldReads=reads; op_4e72_33_ff(0x4e72); MakeSR(); + if(reads!=oldReads || regs.sr!=result.sr || regs.pc!=0x1000 || regs.stopped!=1 || callbacks) throw std::runtime_error("Stopped re-entry changed state"); + for(int n=0;n<8;n++) if(regs.d[n]!=values[n] || regs.a[n]!=values[n+8]) throw std::runtime_error("STOP changed unrelated registers"); + if(regs.usp!=0x7800||regs.isp!=0x4700)throw std::runtime_error("STOP changed stacks"); + return result; +} +std::vector defaults(uint16_t sr) { + std::vector v; + for(int n=0;n<8;n++)v.push_back(n==7?2:0xa55a0022u+n*0x100); + for(int n=0;n<8;n++)v.push_back(n==7?((sr&0x2000)?0x4700:0x7800):0x4000+n*0x100); + return v; +} +void controls() { + const uint16_t inputs[]={0x271f,0x001f,0x271f,0xa71f},targets[]={0x2010,0x2710,0x0010,0x3010}; + const uint32_t vectors[]={0,8,8,9},statuses[]={0x2010,0x001f,0x271f,0x3010},pcs[]={0x1004,0x1000,0x1000,0x1004}; + for(int n=0;n<4;n++){const auto r=execute(inputs[n],targets[n],defaults(inputs[n]));if(r.vector!=vectors[n]||r.sr!=statuses[n]||r.pc!=pcs[n])throw std::runtime_error("Literal STOP control failed");} +} +int main(int argc,char**argv) { + try { + controls(); + if(argc!=4 || std::string(argv[3])!="163840")throw std::runtime_error("STOP audit needs fixture/output/count"); + std::ifstream input(argv[1]);std::ofstream output(argv[2]);if(!input||!output)throw std::runtime_error("Missing STOP fixture/output"); + std::string line;uint32_t index=0,passing=0,mismatching=0; + while(std::getline(input,line)) { + std::istringstream row(line);std::vector v;uint64_t value; + while(row>>std::hex>>value){if(value>UINT32_MAX)throw std::runtime_error("Wide STOP field");v.push_back(static_cast(value));} + if(!row.eof()||v.size()!=27||v[0]!=index||index>=163840||(v[1]&~0xb71f)||(v[2]&~0xb71f)||v[9]!=0x7800||v[10]!=0x4700)throw std::runtime_error("Malformed/out-of-scope STOP fixture"); + const auto initial=defaults(static_cast(v[1])); + for(int n=0;n<16;n++)if(v[n+11]!=initial[n])throw std::runtime_error("STOP initial registers differ from canonical fixture"); + const auto r=execute(static_cast(v[1]),static_cast(v[2]),std::vector(v.begin()+11,v.end())); + if(r.vector!=v[3]||r.sr!=v[4]||r.pc!=v[5])throw std::runtime_error("Declared software expectation differs from executed STOP"); + const bool match=r.vector==v[6]&&r.sr==v[7]&&r.pc==v[8];if(match)passing++;else mismatching++; + output<nul && set" + if($LASTEXITCODE -ne 0){throw 'MSVC setup failed'} + foreach($setting in $settings){$at=$setting.IndexOf('=');if($at -gt 0){$name=$setting.Substring(0,$at);$savedCompiler[$name]=[Environment]::GetEnvironmentVariable($name);[Environment]::SetEnvironmentVariable($name,$setting.Substring($at+1))}} + Push-Location $output + try { + & cl /nologo /O2 /EHsc /w /DWIN32 /DUNICODE /D_UNICODE "/I$source/include" "/I$source/od-win32" "/I$source" (Join-Path $source 'build68k.cpp') /Fe:build68k.exe *> build68k.log + if($LASTEXITCODE -ne 0){throw 'Opcode builder failed'} + Get-Content -LiteralPath (Join-Path $source 'table68k')|& ./build68k.exe|Set-Content -Encoding utf8 cpudefs.cpp + if($LASTEXITCODE -ne 0 -or -not (Select-String -LiteralPath cpudefs.cpp -SimpleMatch 'n_defs68k = 232')){throw 'Missing opcode definitions'} + & cl /nologo /O2 /EHsc /w /DWIN32 /DUNICODE /D_UNICODE "/I$source/include" "/I$source/od-win32" "/I$source" (Join-Path $source 'gencpu.cpp') (Join-Path $source 'readcpu.cpp') (Join-Path $source 'missing.cpp') (Join-Path $source 'od-win32/unicode.cpp') cpudefs.cpp user32.lib /Fe:gencpu.exe *> generator-build.log + if($LASTEXITCODE -ne 0){throw 'Generator build failed'} + & ./gencpu.exe *> generator-run.log + if($LASTEXITCODE -ne 0){throw 'Reference generation failed'} + $native=[IO.File]::ReadAllText((Join-Path $source 'newcpu.cpp')) + $helpers=@('void REGPARAM2 MakeSR(void)','static void MakeFromSR_x(int t0trace)','void REGPARAM2 MakeFromSR_STOP(void)', + 'static void m68k_set_stop(int stoptype)','void do_cycles_stop(int c)') + ($helpers|ForEach-Object{FunctionText $native $_}) -join "`n`n"|Set-Content -Encoding utf8 winuae-stop-sr.inc + FunctionText ([IO.File]::ReadAllText((Join-Path $output 'cpuemu_33.cpp'))) 'uae_u32 REGPARAM2 op_4e72_33_ff(uae_u32 opcode)'|Set-Content -Encoding utf8 winuae-stop-op.inc + & cl /nologo /O2 /EHsc /W4 "/I$output" (Join-Path $repo 'scripts/reference/m68060-stop.cpp') /Fe:observer.exe *> observer-build.log + if($LASTEXITCODE -ne 0){throw 'STOP observer build failed'} + }finally{Pop-Location;foreach($name in $savedCompiler.Keys){[Environment]::SetEnvironmentVariable($name,$savedCompiler[$name])}} + & dotnet build (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release *> (Join-Path $output 'build.log') + if($LASTEXITCODE -ne 0){throw 'STOP fixture build failed'} + $saved=@{};$settings=@{COPPER68K_RUN_060_STOP_DISCOVERY='1';COPPER68K_SYNTHETIC_REPORT_DIR=$output;COPPER68K_060_STOP_EXPORT=$output} + try{ + foreach($name in $settings.Keys){$saved[$name]=[Environment]::GetEnvironmentVariable($name);[Environment]::SetEnvironmentVariable($name,$settings[$name])} + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --no-build --no-restore --filter 'FullyQualifiedName~M68060StopDiscoveryTests' --logger 'trx;LogFileName=discovery.trx' --results-directory $output *> (Join-Path $output 'run.log') + }finally{foreach($name in $saved.Keys){[Environment]::SetEnvironmentVariable($name,$saved[$name])}} + foreach($group in $groups){ + & (Join-Path $output 'observer.exe') (Join-Path $output "$group.rows") (Join-Path $output "$group.reference") 163840 *> (Join-Path $output "$group.reference.log") + if($LASTEXITCODE -notin @(0,1)){throw "STOP reference execution failed: $group"} + } + $identity=@{schema=1;reference='WinUAE-060-STOP-discovery';referenceCommit=$pin;sourceCommit=(& git -C $repo rev-parse HEAD); + softwareReferenceExecuted=$true;traceBoundaryComposed=$true;fullExceptionOracle=$false;architecturallyQualified=$false;roadmapComplete=$false; + inputs=@();referenceInputs=@();generated=@();evidence=@()} + foreach($file in (@(& git -C $repo ls-files 'Copper68k/*')+$fixtures)){$identity.inputs+=@{file=$file;sha256=(Hash (Join-Path $repo $file))}} + foreach($file in $referenceFiles){$identity.referenceInputs+=@{file=$file;sha256=(Hash (Join-Path $source $file))}} + foreach($file in $generated){$identity.generated+=@{file=$file;sha256=(Hash (Join-Path $output $file))}} + foreach($file in $evidence){$identity.evidence+=@{file=$file;sha256=(Hash (Join-Path $output $file))}} + $identity|ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'identities.json') +} +$identity=Get-Content (Join-Path $output 'identities.json') -Raw|ConvertFrom-Json +if($identity.schema -ne 1 -or $identity.reference -cne 'WinUAE-060-STOP-discovery' -or $identity.referenceCommit -cne $pin -or + $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.softwareReferenceExecuted -ne $true -or $identity.traceBoundaryComposed -ne $true -or + $identity.fullExceptionOracle -ne $false -or $identity.architecturallyQualified -ne $false -or $identity.roadmapComplete -ne $false){throw 'Incomplete STOP discovery identity'} +Check-Identities $identity.inputs (@(& git -C $repo ls-files 'Copper68k/*')+$fixtures) $repo 'fixture/CPU' +Check-Identities $identity.referenceInputs $referenceFiles $source 'reference' +Check-Identities $identity.generated $generated $output 'generated' +Check-Identities $identity.evidence $evidence $output 'evidence' +[xml]$trx=Get-Content (Join-Path $output 'discovery.trx') -Raw +$c=$trx.TestRun.ResultSummary.Counters +if($c.total -ne 2 -or $c.executed -ne 2 -or $c.notExecuted -ne 0 -or ([int]$c.passed+[int]$c.failed) -ne 2){throw 'STOP discovery must execute both complete routes'} +foreach($method in @('DefinedStatusImagesScalar','DefinedStatusImagesBatch')){ + if(@($trx.TestRun.Results.UnitTestResult|Where-Object{$_.testName -ceq "Copper68k.Tests.Synthetic.M68060StopDiscoveryTests.$method" -and $_.outcome -cin @('Passed','Failed')}).Count -ne 1){throw "Missing STOP execution: $method"} +} +$keys=[Collections.Generic.Dictionary[string,int]]::new([StringComparer]::Ordinal) +$fixture=[Collections.Generic.List[string]]::new();$index=0 +foreach($matrix in @('images','ccr')){foreach($supervisor in @($false,$true)){foreach($master in @(0,0x1000)){foreach($trace in @(0,0x8000)){ +foreach($ccr in $(if($matrix -eq 'images'){@(0,31)}else{0..31})){foreach($s in @(0,0x2000)){foreach($m in @(0,0x1000)){foreach($t in @(0,0x8000)){ +foreach($ipl in $(if($matrix -eq 'images'){0..7}else{@(0,7)})){foreach($nextCcr in 0..31){ + $sr=$(if($supervisor){0x2700}else{0x0700}) -bor $master -bor $trace -bor $ccr + $immediate=$s -bor $m -bor $t -bor ($ipl -shl 8) -bor $nextCcr + $vector=if(-not $supervisor -or -not $s){8}elseif($trace){9}else{0} + $boundarySr=if($vector -eq 8){$sr}else{$immediate};$pc=if($vector -eq 8){0x1000}else{0x1004} + $prefix='68060/STOP/{0}/super={1}/M={2:X4}/T={3:X4}/S={4:X4}/newM={5:X4}/newT={6:X4}/IPL={7}' -f $matrix,$supervisor,$master,$trace,$s,$m,$t,$ipl + foreach($phase in $(if($vector){@('execute')}else{@('execute','inert')})){ + $key="$prefix/$phase";if(-not $keys.ContainsKey($key)){$keys[$key]=0};$keys[$key]++ + } + $fixture.Add(('{0:X8} {1:X8} {2:X8} {3:X8} {4:X8} {5:X8}' -f $index++,$sr,$immediate,$vector,$boundarySr,$pc)) +}}}}}}}}}} +if($fixture.Count -ne 163840 -or $keys.Count -ne 720){throw "Independent STOP selection differs: $($fixture.Count)/$($keys.Count)"} +$mismatches=0;$summaries=@() +foreach($group in $groups){ + $report=Get-Content (Join-Path $output "68060-$group.json") -Raw|ConvertFrom-Json -AsHashtable + if($report.schema -ne 1 -or $report.model -cne '68060' -or $report.group -cne $group -or $report.xunitBatches -ne 1 -or $report.logicalCases -ne 184320 -or + $report.counts.unsupported -ne 0 -or $report.counts.untested -ne 0 -or ($report.counts.passing+$report.counts.mismatching) -ne 184320 -or $report.combinations.Count -ne $keys.Count){throw 'Incomplete STOP report'} + foreach($key in $keys.Keys){if(-not $report.combinations.Contains($key) -or (@($report.combinations[$key].Values)|Measure-Object -Sum).Sum -ne $keys[$key]){throw "Missing/misweighted STOP combination: $key"}} + $rows=[IO.File]::ReadAllLines((Join-Path $output "$group.rows"));$reference=[IO.File]::ReadAllLines((Join-Path $output "$group.reference")) + if($rows.Count -ne $fixture.Count -or $reference.Count -ne $fixture.Count){throw 'Incomplete STOP rows'} + $nativeMismatch=0 + for($i=0;$i -lt $fixture.Count;$i++){ + $fields=$rows[$i].Split(' ');if($fields.Count -ne 27 -or ($fields[0..5] -join ' ') -cne $fixture[$i]){throw "STOP fixture order/selection differs: $i"} + $numbers=[uint32[]]::new(9);for($n=0;$n -lt 9;$n++){$numbers[$n]=[uint32]::Parse($fields[$n],[Globalization.NumberStyles]::HexNumber)} + $initial=@('00007800','00004700','A55A0022','A55A0122','A55A0222','A55A0322','A55A0422','A55A0522','A55A0622','00000002', + '00004000','00004100','00004200','00004300','00004400','00004500','00004600',$(if($numbers[1] -band 0x2000){'00004700'}else{'00007800'})) + if(($fields[9..26] -join ' ') -cne ($initial -join ' ')){throw "STOP initial registers/stacks differ: $i"} + $refFields=$reference[$i].Split(' ');if($refFields.Count -ne 6){throw "Malformed STOP reference row: $i"} + $ref=[uint32[]]::new(6);for($n=0;$n -lt 6;$n++){$ref[$n]=[uint32]::Parse($refFields[$n],[Globalization.NumberStyles]::HexNumber)} + $match=$numbers[3] -eq $numbers[6] -and $numbers[4] -eq $numbers[7] -and $numbers[5] -eq $numbers[8] + if($ref.Count -ne 6 -or $ref[0] -ne $i -or $ref[1] -ne $numbers[3] -or $ref[2] -ne $numbers[4] -or $ref[3] -ne $numbers[5] -or + $ref[4] -ne [int]($numbers[3] -ne 8) -or $ref[5] -ne [int]$match){throw "STOP reference outcome differs: $i"} + if(-not $match){$nativeMismatch++} + } + if($nativeMismatch -ne $report.counts.mismatching){throw "STOP native/comparator mismatch counts differ: group=$group native=$nativeMismatch report=$($report.counts.mismatching) fixtures=$($fixture.Count)"} + & (Join-Path $output 'observer.exe') (Join-Path $output "$group.rows") (Join-Path $output "$group.replayed") 163840 *> (Join-Path $output "$group.replay.log") + if($LASTEXITCODE -notin @(0,1) -or (Hash (Join-Path $output "$group.replayed")) -cne (Hash (Join-Path $output "$group.reference"))){throw 'Frozen STOP reference replay differs'} + $mismatches+=$report.counts.mismatching;$summaries+=@{group=$group;logicalCases=184320;combinations=$keys.Count;counts=$report.counts} +} +@{schema=1;softwareReferenceExecuted=$true;fullExceptionOracle=$false;traceBoundaryComposed=$true;architecturallyQualified=$false;roadmapComplete=$false; + groups=$summaries;remaining='Ordinary STOP S-clear architectural outcome, full trace/exception/IRQ delivery and broader qualification/consolidation remain required'}|ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'summary.json') +if($mismatches){throw "STOP software-reference discovery found $mismatches mismatches; not an architectural CPU-fix claim"} From 90e3afa55bb56a5db3c580133b30606a84249b3c Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 15:14:14 +0300 Subject: [PATCH 066/211] Qualify 68010 MOVEC and consolidate proven regressions --- Copper68k.Tests/M68010InterpreterTests.cs | 60 ---------- Copper68k.Tests/Synthetic/README.md | 19 +++ .../Synthetic/SyntheticM68010MovecTests.cs | 103 ++++++++++++++++ .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- .../SyntheticMovecRegisterFixture.cs | 59 ++++++--- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 113 ++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 36 ++++++ scripts/test-copper68k-010-movec.ps1 | 106 ++++++++++++++++ .../test-copper68k-synthetic-mutations.ps1 | 59 ++++++++- scripts/test-copper68k-synthetic.ps1 | 4 + 10 files changed, 481 insertions(+), 80 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68010MovecTests.cs create mode 100644 scripts/test-copper68k-010-movec.ps1 diff --git a/Copper68k.Tests/M68010InterpreterTests.cs b/Copper68k.Tests/M68010InterpreterTests.cs index 4c6e1183..0c30cbb4 100644 --- a/Copper68k.Tests/M68010InterpreterTests.cs +++ b/Copper68k.Tests/M68010InterpreterTests.cs @@ -13,66 +13,6 @@ public void FactoryCreatesM68010CoreWithoutM68020StackMode() Assert.False(interpreter.State.M68020StackModeEnabled); } - [Fact] - public void MovecTransfersVectorBaseRegister() - { - var bus = new ZeroWaitCodeBus(); - WriteWords( - bus, - CodeBase, - 0x4E7B, 0x0801, // MOVEC D0,VBR - 0x4E7A, 0x1801); // MOVEC VBR,D1 - var cpu = new M68010Interpreter(bus); - cpu.Reset(CodeBase, 0x3000); - cpu.State.D[0] = 0x0000_0400; - cpu.ExecuteInstruction(); - cpu.ExecuteInstruction(); - Assert.Equal(0x0000_0400u, cpu.State.VectorBaseRegister); - Assert.Equal(0x0000_0400u, cpu.State.D[1]); - Assert.False(cpu.State.M68020StackModeEnabled); - } - - [Theory] - [InlineData((ushort)0x000, 0xFFFF_FFFEu, 0x0000_0006u)] - [InlineData((ushort)0x001, 0xFFFF_FFFDu, 0x0000_0005u)] - [InlineData((ushort)0x801, 0x1234_5678u, 0x1234_5678u)] - public void MovecTransfersSupportedControlRegisters( - ushort controlRegister, - uint sourceValue, - uint expectedValue) - { - var bus = new ZeroWaitCodeBus(); - WriteWords( - bus, - CodeBase, - 0x4E7B, controlRegister, // MOVEC D0,control - 0x4E7A, (ushort)(0x1000 | controlRegister)); // MOVEC control,D1 - var cpu = new M68010Interpreter(bus); - cpu.Reset(CodeBase, 0x3000); - cpu.State.D[0] = sourceValue; - - cpu.ExecuteInstruction(); - cpu.ExecuteInstruction(); - - Assert.Equal(expectedValue, cpu.State.D[1]); - Assert.Equal(CodeBase + 8u, cpu.State.ProgramCounter); - } - - [Fact] - public void MovecVectorBaseToA7UpdatesActiveSupervisorStackPointer() - { - var bus = new ZeroWaitCodeBus(); - WriteWords(bus, CodeBase, 0x4E7A, 0xF801); // MOVEC VBR,A7 - var cpu = new M68010Interpreter(bus); - cpu.Reset(CodeBase, 0x3000); - cpu.State.VectorBaseRegister = 0x0000_4800; - - cpu.ExecuteInstruction(); - - Assert.Equal(0x0000_4800u, cpu.State.A[7]); - Assert.Equal(0x0000_4800u, cpu.State.SupervisorStackPointer); - } - [Fact] public void MovecInUserModeRaisesPrivilegeViolationWithoutChangingControlRegister() { diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 96431c74..c795a5a8 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -990,3 +990,22 @@ then reruns the frozen observer. Both commands currently fail explicitly on The qualification document records thirteen integrity controls and retained test evidence. Discovery does not promote coverage into ordinary CI or complete-040, retire regressions or complete milestone 6. + +68010 MOVEC uses `SyntheticM68010MovecTests` and the shared control-register +fixture. Four ordinary groups cover all legal source/readback register pairs, +function-code masks from every initial image, all raw register bits, every +undefined selector and legal user-mode transfers. All registers, active/inactive +stacks, SR/CCR, PC, preserved controls and memory are checked. USP initialization +and A7 source aliases are explicit. Failed writes leave readbacks untested; +successful reads include a following NOP. This is nontraced IPL7 register +qualification, separate from physical function-code spaces, restart and timing. + +`./scripts/test-copper68k-010-movec.ps1` runs four matrices (2,271,744 cases) and +checks exact input/binary/evidence identities, named test selection and +independently enumerated combination keys/weights. `-ValidateReportsOnly` +requires the same complete output and inputs. Ten report integrity controls +reject malformed selections. `-Scope Movec010` adds six maintained mutations, +with original/replacement detection before and after the demonstrated retirement +of three duplicate methods. The qualification record maps each original to its +replacement; factory, exception, interrupt and specialized regressions remain. +The broader roadmap is still incomplete. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68010MovecTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68010MovecTests.cs new file mode 100644 index 00000000..d7678e80 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68010MovecTests.cs @@ -0,0 +1,103 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticM68010MovecTests(ITestOutputHelper output) +{ + // MC68000UM figure 2-3 and 6.3.6/7; M68000PM MOVEC 6-22/23. + // Legal selectors, register widths and masks are independent specifications. + private static readonly int[] Legal = [0, 1, 0x800, 0x801]; + private static readonly uint[] PairedValues = [0, 7, 0x400, 0x12345678, 0xfffffffd, 0xfffffffe, uint.MaxValue]; + private static SyntheticMachine Machine() => new(ModelSpec.All.Single(m => m.Id == "68010")); + private static uint[] FunctionValues() => Enumerable.Range(0, 8).Select(v => (uint)v) + .Concat(Enumerable.Range(3, 29).Select(bit => 1u << bit)) + .Concat([0xfffffff8u, uint.MaxValue, 0xaaaaaaabu, 0x55555554u]).Distinct().Order().ToArray(); + private static uint[] AddressValues() => Enumerable.Range(0, 32).Select(bit => 1u << bit) + .Concat([0u, 7u, 0x400u, 0x4800u, 0x12345678u, uint.MaxValue, 0xaaaaaaaau, 0x55555555u]).Distinct().Order().ToArray(); + + [Fact, Trait("Suite", "Synthetic")] + public void Movec010EveryLegalControlRegisterPairPrivilegeAndCcr() + { + var m = Machine(); var report = new CoverageBatch("68010", "system-010-movec-pairs"); + foreach (var control in Legal) Fixture(control).WritesAndReadback(m, report, PairedValues, + initial: Initial(control), readRegisters: Enumerable.Range(0, 16), groupConditionCodes: true); + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void Movec010FunctionCodeMasksFromEveryInitialImage() + { + var m = Machine(); var report = new CoverageBatch("68010", "system-010-movec-masks"); + var values = FunctionValues(); Assert.Equal(41, values.Length); + foreach (var control in new[] { 0, 1 }) + for (uint initial = 0; initial < 8; initial++) Fixture(control).WritesAndReadback(m, report, values, + initial: initial, groupConditionCodes: true); + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void Movec010RawControlImagesPreserveAllBitsAndActiveStack() + { + var m = Machine(); var report = new CoverageBatch("68010", "system-010-movec-reads"); + foreach (var control in Legal) Fixture(control).RawReads(m, report, + control < 2 ? Enumerable.Range(0, 8).Select(v => (uint)v) : AddressValues(), groupConditionCodes: true); + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void Movec010UndefinedSelectorsAndLegalUserFormsHaveExactFrames() + { + var m = Machine(); var report = new CoverageBatch("68010", "system-010-movec-encodings"); + foreach (var control in Enumerable.Range(0, 4096).Except(Legal)) + for (var general = 0; general < 16; general++) + foreach (var store in new[] { false, true }) + foreach (var supervisor in new[] { false, true }) + foreach (var ccr in new[] { 0, 31 }) Rejected(m, report, control, general, store, supervisor, ccr); + foreach (var control in Legal) + for (var general = 0; general < 16; general++) + foreach (var store in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) Rejected(m, report, control, general, store, false, ccr); + report.Complete(output); + } + + private static void Rejected(SyntheticMachine m, CoverageBatch report, int control, int general, bool store, bool supervisor, int ccr) + { + m.Reset(ccr, supervisor); Vectors(m); + var opcode = (ushort)(store ? 0x4e7b : 0x4e7a); + SyntheticExecution.Prepare(m, [opcode, (ushort)(general << 12 | control)]); + Controls(m.Core.State); + var e = ArchitecturalExpectation.Capture(m); e.Pc = SyntheticMachine.Code + 4; + // 010 incoming user privilege is checked before the selector is decoded. + SyntheticExecution.ExpectException(m, e, supervisor ? 4 : 8); + e.ControlChecks["SFC preserved"] = (s => s.SourceFunctionCode, 1); + e.ControlChecks["DFC preserved"] = (s => s.DestinationFunctionCode, 5); + e.ControlChecks["VBR preserved"] = (s => s.VectorBaseRegister, 0x400); + e.ControlChecks["stack model preserved"] = (s => s.M68020StackModeEnabled ? 1u : 0, 0); + SyntheticExecution.Run(m, e, report, + $"68010/MOVEC/L/control={control:X3}/R{general}/store={store}/super={supervisor}/op={opcode:X4}/ccr={ccr:X2}", false); + } + + private static uint Initial(int control) => control switch { 0 => 1, 1 => 5, 0x800 => 0x7800, _ => 0x400 }; + private static void Vectors(SyntheticMachine m) + { + m.InitializePhysical(0x410, 0x9040, 4); + m.InitializePhysical(0x420, 0x9080, 4); + } + private static void Controls(M68kCpuState s) + { + s.SourceFunctionCode = 1; s.DestinationFunctionCode = 5; s.VectorBaseRegister = 0x400; + } + private static SyntheticMovecRegisterFixture Fixture(int control) => control switch + { + 0 => new(0, "SFC", 7, s => s.SourceFunctionCode, (s, v) => s.SourceFunctionCode = v, + PrepareMemory: Vectors, InitializeControls: Controls), + 1 => new(1, "DFC", 7, s => s.DestinationFunctionCode, (s, v) => s.DestinationFunctionCode = v, + PrepareMemory: Vectors, InitializeControls: Controls), + 0x800 => new(0x800, "USP", uint.MaxValue, s => s.UserStackPointer, (s, v) => s.SetUserStackPointer(v), + UserStackControl: true, PrepareMemory: Vectors, InitializeControls: Controls), + 0x801 => new(0x801, "VBR", uint.MaxValue, s => s.VectorBaseRegister, (s, v) => s.VectorBaseRegister = v, + PrepareMemory: Vectors, InitializeControls: Controls), + _ => throw new ArgumentOutOfRangeException(nameof(control)) + }; +} diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 3ca7d378..cb2a00e0 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -37,7 +37,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() : form == "real-access-fault-entry" ? "Cache-disabled accurate instruction-fetch frames/restart are covered separately; data fault/writeback restart, enabled-cache deferral and compiled instruction-PC provenance remain required" : form == "odd-PC-chained-SR-provenance" - ? "Chained format0/2/3 and format7 normal/CM SR handoff, stack consumption and header reads have separate generated WinUAE software coverage; access-frame validation/no-replay checks are synthetic. Separate MMU-path CM lifetime discovery disagrees after odd-PC fault/repair; its exception entry is composed, not a full architectural oracle. CT/CU/CP pending/foreign-context chains, full reference exception entry and hardware behavior remain unqualified" + ? "Chained format0/2/3 and format7 normal/CM SR handoff, stack consumption and header reads have separate generated WinUAE software coverage; access-frame validation/no-replay checks are synthetic. Separate composed-entry and executed-MMU-entry CM lifetime discovery profiles disagree after odd-PC fault/repair; the executed profile also exposes stacked-SR disagreement. Neither qualifies the full run loop or hardware. CT/CU/CP pending/foreign-context chains, full reference exception qualification and hardware behavior remain unqualified" : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs b/Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs index eca4a67d..61089f97 100644 --- a/Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs +++ b/Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs @@ -6,10 +6,14 @@ namespace Copper68k.Tests.Synthetic; // specifications, not results computed with production decoder/MMU helpers. internal sealed record SyntheticMovecRegisterFixture(int Control, string Name, uint Mask, Func Read, Action Initialize, - Func? StoredValue = null) + Func? StoredValue = null, bool UserStackControl = false, + Action? PrepareMemory = null, Action? InitializeControls = null) { private static readonly (string Name, Func Read)[] Preserved = [ + ("SFC", s => s.SourceFunctionCode), + ("DFC", s => s.DestinationFunctionCode), + ("VBR", s => s.VectorBaseRegister), ("TC", s => s.M68040Mmu.TranslationControl), ("ITT0", s => s.M68040Mmu.InstructionTransparentTranslation0), ("ITT1", s => s.M68040Mmu.InstructionTransparentTranslation1), @@ -21,61 +25,84 @@ private static readonly (string Name, Func Read)[] Preserved ("PCR", s => s.M68060ProcessorConfiguration) ]; - internal void WritesAndReadback(SyntheticMachine m, CoverageBatch report, IEnumerable values, int storeRegisters = 16, uint initial = 0, IEnumerable? conditionCodes = null) + internal void WritesAndReadback(SyntheticMachine m, CoverageBatch report, IEnumerable values, int storeRegisters = 16, + uint initial = 0, IEnumerable? conditionCodes = null, IEnumerable? readRegisters = null, bool groupConditionCodes = false) { foreach (var value in values) for (var general = 0; general < storeRegisters; general++) + foreach (var destination in readRegisters ?? [general]) foreach (var supervisor in new[] { false, true }) foreach (var ccr in conditionCodes ?? Enumerable.Range(0, 32)) { m.Reset(ccr, supervisor); - if (general < 8) m.Core.State.D[general] = value; - else if (general == 15) m.Core.State.SetActiveStackPointer(value); - else m.Core.State.A[general - 8] = value; - var e = SyntheticExecution.Prepare(m, [0x4e7b, (ushort)(general << 12 | Control), 0x4e7a, (ushort)(general << 12 | Control)]); + SetGeneral(m.Core.State, general, value); + PrepareMemory?.Invoke(m); + SyntheticExecution.Prepare(m, [0x4e7b, (ushort)(general << 12 | Control), 0x4e7a, (ushort)(destination << 12 | Control)]); + InitializeControls?.Invoke(m.Core.State); Initialize(m.Core.State, initial); + // USP initialization may alias a user-mode A7 source. Source setup + // is last, and the expectation captures that intended initial state. + SetGeneral(m.Core.State, general, value); + var e = ArchitecturalExpectation.Capture(m); e.Pc = SyntheticMachine.Code + 4; Preserve(m, e); - var id = $"{m.Model.Id}/MOVEC/L/{Name}/R{general}/initial={initial:X8}/value={value:X8}/super={supervisor}/ccr={ccr:X2}"; + var id = $"{m.Model.Id}/MOVEC/L/{Name}/R{general}/initial={initial:X8}/value={value:X8}/super={supervisor}"; + if (readRegisters != null) id += $"/read-R{destination}"; + if (!groupConditionCodes) id += $"/ccr={ccr:X2}"; + var sample = groupConditionCodes ? $"/op=4E7B/ccr={ccr:X2}" : ""; + var readSample = groupConditionCodes ? $"/op=4E7A/ccr={ccr:X2}" : ""; if (!supervisor) { SyntheticExecution.ExpectException(m, e, 8); - SyntheticExecution.Run(m, e, report, id + "/privilege", false); + SyntheticExecution.Run(m, e, report, id + "/privilege" + sample, false); continue; } var stored = StoredValue?.Invoke(initial, value) ?? value & Mask; e.ControlChecks[Name] = (Read, stored); - if (!Step(m, e, report, id + "/write")) + if (UserStackControl) e.InactiveStackPointer = stored; + if (!Step(m, e, report, id + "/write" + sample)) { - report.Record(id + "/read", "untested", $"{Name} write failed; dependent readback not executed."); + report.Record(id + "/read" + readSample, "untested", $"{Name} write failed; dependent readback not executed."); continue; } e.Pc += 4; - if (general < 8) e.D[general] = stored; - else e.A[general - 8] = stored; - SyntheticExecution.Run(m, e, report, id + "/read"); + if (destination < 8) e.D[destination] = stored; + else e.A[destination - 8] = stored; + SyntheticExecution.Run(m, e, report, id + "/read" + readSample); } } - internal void RawReads(SyntheticMachine m, CoverageBatch report, IEnumerable values) + internal void RawReads(SyntheticMachine m, CoverageBatch report, IEnumerable values, bool groupConditionCodes = false) { foreach (var value in values) for (var general = 0; general < 16; general++) for (var ccr = 0; ccr < 32; ccr++) { m.Reset(ccr); - var e = SyntheticExecution.Prepare(m, [0x4e7a, (ushort)(general << 12 | Control)]); + PrepareMemory?.Invoke(m); + SyntheticExecution.Prepare(m, [0x4e7a, (ushort)(general << 12 | Control)]); + InitializeControls?.Invoke(m.Core.State); Initialize(m.Core.State, value); + var e = ArchitecturalExpectation.Capture(m); + e.Pc = SyntheticMachine.Code + 4; Preserve(m, e); if (general < 8) e.D[general] = value & Mask; else e.A[general - 8] = value & Mask; - SyntheticExecution.Run(m, e, report, $"{m.Model.Id}/MOVEC/L/{Name}/read-R{general}/internal={value:X8}/ccr={ccr:X2}"); + SyntheticExecution.Run(m, e, report, $"{m.Model.Id}/MOVEC/L/{Name}/read-R{general}/internal={value:X8}{(groupConditionCodes ? "/op=4E7A" : "")}/ccr={ccr:X2}"); } } + private static void SetGeneral(M68kCpuState state, int general, uint value) + { + if (general < 8) state.D[general] = value; + else if (general == 15) state.SetActiveStackPointer(value); + else state.A[general - 8] = value; + } + private static void Preserve(SyntheticMachine m, ArchitecturalExpectation e) { foreach (var (name, read) in Preserved) e.ControlChecks[name] = (read, read(m.Core.State)); + if (m.Model.Id == "68010") e.ControlChecks["010 stack model"] = (s => s.M68020StackModeEnabled ? 1u : 0, 0); } private static bool Step(SyntheticMachine m, ArchitecturalExpectation e, CoverageBatch report, string id) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index c7d27650..c6cd220e 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -6024,3 +6024,116 @@ S-clear rule, full external trace/exception/IRQ qualification and PCR's software manual disagreement are still open. Milestone 6 stays **in progress**, `architecturallyQualified=false`, `roadmapComplete=false`. No CPU fix, regression retirement, consumer replay or package release is included. + +### 68010 MOVEC register qualification and consolidation (2026-10-06) + +`SyntheticM68010MovecTests` adds **2,271,744 passing logical cases / four +ordinary-CI batches**. Independent specifications come from +[MC68000UM ninth edition](https://www.nxp.com/docs/en/reference-manual/MC68000UM.pdf), +figure 2-3 and 6.3.6/7, and +[M68000PM MOVEC](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf), +6-22/23. MC68010 legal selectors are SFC 000, DFC 001, USP 800 and VBR 801. +Function codes have three implemented bits; USP/VBR retain all 32 register +bits despite the external 24-bit address bus. Transfers preserve CCR. Incoming +user privilege takes vector 8; undefined selectors in supervisor state take +vector 4, both saving opcode PC in an eight-byte format-zero frame. + +| Group | Logical cases | Combinations | Selection | +| --- | ---: | ---: | --- | +| `system-010-movec-pairs` | 688,128 | 21,504 | Four controls, every source/readback register pair, seven values, both privilege states and all CCRs | +| `system-010-movec-masks` | 1,007,616 | 31,488 | Both function codes, all eight initial images, 41 low-bit/high-bit/pattern samples, every register, privilege and CCR | +| `system-010-movec-reads` | 48,128 | 1,504 | All eight function-code images and 39 full-width USP/VBR images, every register and CCR | +| `system-010-movec-encodings` | 527,872 | 262,016 | All 4,092 undefined selectors, register encodings and directions in both privilege states at CCR 0/31; all legal user forms at every CCR | + +The shared MOVEC fixture supports distinct write/read registers, USP effects, +independent vector-memory/control initialization and grouped CCR weights. It +now also preserves SFC, DFC and VBR when another control is transferred. Source +setup follows control initialization, so a user-mode A7 source has the intended +USP alias. Expectations capture setup before execution; result masks and legal +selectors never use production decoders, arithmetic or timing helpers. Every +general register, active/inactive stack, defined SR bit, exact PC, unchanged +control, initialized memory and new memory write is checked. Successful +readbacks include a following NOP; a failed write leaves readback untested and +is never retried. Read phases identify their actual 4E7A opcode separately. +The 010 stack-model check expects false independently of its observed value. + +These are nontraced IPL7 register/encoding semantics, not qualification of +physical function-code spaces, exception bus ordering, prefetch, format-8 +restart, interrupts or timing. Diagnostic 010 coverage does not establish +desktop readiness. Existing specialized tests for these protocols remain. +No newly executed external CPU or hardware oracle is claimed by this manual +qualification. Existing independent model audits remain separately recorded. + +The dedicated command checks exact fixture/CPU/command sources, binaries and +evidence identities, exactly four named passing tests with no skips, and +independently enumerated combination keys and weights. Requested execution +and report revalidation reject missing inputs, empty selections, mismatches, +unsupported execution and untested cases. Ten integrity controls reject missing +manifests, wrong profiles, missing/duplicate sources, missing reports, empty +execution, wrong keys/weights/models and substituted test names. Content +controls refresh the changed evidence hash; originals remain unchanged. + +```powershell +./scripts/test-copper68k-010-movec.ps1 -OutputDirectory artifacts/m6-010-movec-qualified-final +./scripts/test-copper68k-010-movec.ps1 -ValidateReportsOnly -OutputDirectory artifacts/m6-010-movec-qualified-final +./scripts/test-copper68k-synthetic-mutations.ps1 -Scope Movec010 +``` + +Retirement is limited to three methods in `M68010InterpreterTests`: +`MovecTransfersVectorBaseRegister`, `MovecTransfersSupportedControlRegisters` +(three theory rows), and `MovecVectorBaseToA7UpdatesActiveSupervisorStackPointer`. +These five xUnit cases have explicit replacements: + +- D0→VBR→D1, value 00000400: the pairs matrix's VBR/R0/read-R1 write/read. +- D0→SFC→D1, FFFFFFFE→6; D0→DFC→D1, FFFFFFFD→5; D0→VBR→D1, + 12345678: corresponding pairs cases, plus the complete mask/read matrices. +- VBR→A7, 00004800: reads matrix VBR/read-R15/internal=00004800. +- The old no-020-stack assertion: the independent 010 stack-model check in + every shared transfer, plus the retained public factory regression. + +The fixed old encodings 4E7B/0801, 4E7A/1801 and 4E7A/F801 correspond to those +generated cases. Six maintained mutations fail both the original regression +and its complete replacement before retirement, and still fail afterward: + +| Mutation | Original witness | Replacement mismatching / dependent untested | +| --- | --- | ---: | +| SFC ignores its three-bit mask | Supported-control SFC theory row | 135,168 / 135,168 | +| DFC ignores its three-bit mask | Supported-control DFC theory row | 135,168 / 135,168 | +| VBR truncated to 24 bits | Supported-control VBR theory row | 32,768 / 32,768 | +| VBR loses bit 10 | Vector-base transfer fact | 40,960 / 40,960 | +| A7 loses saved SSP synchronization | VBR-to-A7 fact | 3,008 / 0 | +| Wrong 020 stack model enabled | Vector-base transfer fact | 458,752 / 229,376 | + +Before retirement each theory probe executes four tests: two fail, two pass; +each fact probe executes two failing tests. After retirement each executes +one failing replacement test. No probe skips execution or counts compilation +failure as detection. Production sources are restored byte-for-byte and the +restored Release build has zero warnings/errors. Preliminary proof attempts +had an overbroad legacy-name selection and an inaccessible setter in the +stack-model mutation; those failed commands are excluded. Final proof is in +`artifacts/m6-010-movec-mutations-before-final` and +`artifacts/m6-010-movec-mutations-after-final`. + +Fresh command execution and frozen report revalidation both pass all four +matrices, with zero mismatches/unsupported/untested cases and no skips, in +`artifacts/m6-010-movec-qualified-final`. All **69 retained tests pass without +skips**, including **1,633,168 cases / thirty reports** and 39 fixed 010/060 +examples, in `artifacts/m6-010-movec-retained`. This covers every other shared +MOVEC fixture family and the old model-control inventory. Integrity evidence +is in `artifacts/m6-010-movec-integrity`. + +Source starts at `e2c456bcf140196497807baa3a7f44569eca64a3` plus recorded test/ +command changes. Production tree remains +`795fd12d6c0237a22a5f92f4a96cc4823e0364c1`. +CPU SHA256: `d151cb2e5d3ab78e5b118cfe7e8f69f01db38a345b1fd2baabc61c995a013fe5`. +Test SHA256: `a36059542a07a006749d14b98f1aea479c5698e7c50a419f09bbaac115ea6396`. +Ordinary CI now requires **85,191,062 cases / 680 reports**; this is an expanded +requirement, not a fresh full-suite execution result. Complete-040 requirements +remain unchanged. Its updated inventory wording recognizes both composed and +executed MMU-entry CM discoveries; neither becomes hardware qualification. +Fresh inventory execution still fails with exactly **480 untested cases / +fifteen combinations**, without skips, in `artifacts/m6-010-movec-inventory-final`. +Advanced 010/020/030 restoration, full CM/fault lifetime, STOP/PCR disagreement, +broader model audits and consolidation remain required. Milestone 6 stays **in +progress**, `roadmapComplete=false`; no production fix, consumer replay or +package publication is included. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index d5e0402f..f36ebbbd 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2313,3 +2313,39 @@ inventory remain unchanged. Ordinary STOP's S-clear architectural rule, full CM lifetime, advanced restoration, PCR disagreement, broader independent model qualification and consolidation all remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`; no regression retirement or release. + +### Milestone 6 68010 MOVEC qualification and consolidation + +Four ordinary matrices add 2,271,744 cases: every legal control/source/readback +register pair, complete function-code masks/initial images, raw full-width +USP/VBR images, every undefined selector and all legal user forms. All canonical +CCRs are covered; undefined selectors use CCR 0/31. Incoming trace is clear and +IPL is 7. Expectations use MC68000UM figure 2-3 / 6.3.6/7 and M68000PM MOVEC +6-22/23 independently of production helpers. All registers, stack banks, SR, +exact PC, controls and memory are compared, including following sentinels and +dependent untested phases after a failed prerequisite. + +The shared MOVEC fixture gains distinct readback registers, USP handling and +independent control/vector initialization. It now preserves SFC/DFC/VBR during +other transfers and explicitly expects the 010 stack model. Every new case and +independently enumerated combination passes the identity/selection/weight gate; +frozen report validation also passes. Ten malformed-report controls are rejected. +Sixty-nine retained tests pass without skips, comprising 1,633,168 cases / +thirty reports and 39 fixed examples. + +Six maintained mutations detect masks, VBR width/bit 10, saved A7-bank state +and wrong stack-model configuration. Each fails both the original and complete +replacement before retirement, and the replacement afterward. Only three +documented duplicate MOVEC methods (five xUnit cases) are retired. Factory, +privilege, exception-frame, interrupt, restart, prefetch and other specialized +regressions remain. Mutation sources are restored byte-for-byte; no CPU behavior, +timing policy, public API or package release changes. + +Ordinary coverage requirements become 85,191,062 cases / 680 reports; this is +not a new broad-suite execution claim. Complete-040 requirements stay intact. +Its remaining inventory now accurately names both composed and executed MMU +entry discoveries, and still fails with 480 untested cases / fifteen combinations. +Physical function-code spaces, bus ordering and advanced 010 restoration are +not qualified by MOVEC register tests. All remaining CM/restoration, STOP/PCR, +independent-model and consolidation requirements remain required. Milestone 6 +stays **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-010-movec.ps1 b/scripts/test-copper68k-010-movec.ps1 new file mode 100644 index 00000000..16df4dd1 --- /dev/null +++ b/scripts/test-copper68k-010-movec.ps1 @@ -0,0 +1,106 @@ +[CmdletBinding()] +param([switch]$ValidateReportsOnly,[string]$OutputDirectory='artifacts/010-movec') +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +$groups=[ordered]@{'system-010-movec-pairs'=688128;'system-010-movec-masks'=1007616;'system-010-movec-reads'=48128;'system-010-movec-encodings'=527872} +$names=@{0='SFC';1='DFC';2048='USP';2049='VBR'} +$initial=@{0=1;1=5;2048=0x7800;2049=0x400} +function Identity($Path){ + $file=Join-Path $repo $Path + if(-not(Test-Path -LiteralPath $file -PathType Leaf)){throw "Missing 010 MOVEC input: $Path"} + return @{path=$Path;sha256=(Get-FileHash -LiteralPath $file -Algorithm SHA256).Hash} +} +function Add-Key($Keys,[string]$Key,[int]$Weight){if(-not $Keys.TryAdd($Key,$Weight)){throw "Duplicate 010 MOVEC combination: $Key"}} +function Write-Keys($Keys,$Controls,$Values,$Initials,[bool]$Pairs){ + foreach($control in $Controls){foreach($image in $Initials[$control]){foreach($value in $Values){foreach($general in 0..15){ + foreach($destination in $(if($Pairs){0..15}else{@($general)})){foreach($supervisor in @($false,$true)){ + $key='68010/MOVEC/L/{0}/R{1}/initial={2:X8}/value={3:X8}/super={4}' -f $names[$control],$general,$image,$value,$supervisor + if($Pairs){$key+="/read-R$destination"} + if($supervisor){Add-Key $Keys "$key/write" 32;Add-Key $Keys "$key/read" 32}else{Add-Key $Keys "$key/privilege" 32} + }}}}}} +} +function Expected-Keys($Group){ + $keys=[Collections.Generic.Dictionary[string,int]]::new([StringComparer]::Ordinal) + switch($Group){ + 'system-010-movec-pairs'{Write-Keys $keys @(0,1,0x800,0x801) @([uint32]0,[uint32]7,[uint32]0x400,[uint32]0x12345678,[uint32]0xfffffffdL,[uint32]0xfffffffeL,[uint32]::MaxValue) $initial $true} + 'system-010-movec-masks'{ + $values=[Collections.Generic.HashSet[uint32]]::new();foreach($v in 0..7){[void]$values.Add($v)} + foreach($bit in 3..31){[void]$values.Add([uint32]([uint64]1 -shl $bit))} + foreach($v in @([uint32]0xfffffff8L,[uint32]::MaxValue,[uint32]0xaaaaaaabL,[uint32]0x55555554)){[void]$values.Add($v)} + if($values.Count -ne 41){throw 'Incomplete 010 function-code samples'} + Write-Keys $keys @(0,1) @($values|Sort-Object) @{0=0..7;1=0..7} $false + } + 'system-010-movec-reads'{ + $addresses=[Collections.Generic.HashSet[uint32]]::new();foreach($bit in 0..31){[void]$addresses.Add([uint32]([uint64]1 -shl $bit))} + foreach($v in @(0,7,0x400,0x4800,0x12345678,[uint32]::MaxValue,[uint32]0xaaaaaaaaL,[uint32]0x55555555)){[void]$addresses.Add($v)} + if($addresses.Count -ne 39){throw 'Incomplete 010 address-control samples'} + foreach($control in @(0,1,0x800,0x801)){foreach($value in $(if($control -lt 2){0..7}else{@($addresses|Sort-Object)})){foreach($general in 0..15){ + Add-Key $keys ('68010/MOVEC/L/{0}/read-R{1}/internal={2:X8}' -f $names[$control],$general,[uint32]$value) 32 + }}} + } + 'system-010-movec-encodings'{ + foreach($control in 0..4095){foreach($general in 0..15){foreach($store in @($false,$true)){ + foreach($supervisor in $(if($names.ContainsKey($control)){@($false)}else{@($false,$true)})){ + Add-Key $keys ('68010/MOVEC/L/control={0:X3}/R{1}/store={2}/super={3}' -f $control,$general,$store,$supervisor) $(if($names.ContainsKey($control)){32}else{2}) + } + }}} + } + default{throw "Unknown 010 MOVEC selection: $Group"} + } + return ,$keys +} +$savedReports=[Environment]::GetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR') +Push-Location $repo +try{ + $inputs=@(& git ls-files Copper68k)+@('Copper68k.Tests/Synthetic/SyntheticM68010MovecTests.cs','Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs', + 'Copper68k.Tests/Synthetic/SyntheticMachine.cs','Copper68k.Tests/Synthetic/SyntheticExecution.cs','Copper68k.Tests/Synthetic/ArchitecturalExpectation.cs', + 'Copper68k.Tests/Synthetic/SyntheticMoveTests.cs','Copper68k.Tests/M68010InterpreterTests.cs','Copper68k.Tests/Copper68k.Tests.csproj', + 'scripts/test-copper68k-010-movec.ps1','scripts/test-copper68k-synthetic.ps1','scripts/test-copper68k-synthetic-mutations.ps1') + $binaries=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') + $evidence=@($groups.Keys|ForEach-Object{[IO.Path]::GetRelativePath($repo,(Join-Path $output "68010-$_.json"))})+ + @([IO.Path]::GetRelativePath($repo,(Join-Path $output 'movec.trx'))) + if(-not $ValidateReportsOnly){ + if(Test-Path -LiteralPath $output){throw "Use fresh 010 MOVEC outputs: $output"} + New-Item -ItemType Directory -Path $output|Out-Null + $original=@($inputs|ForEach-Object{Identity $_}) + [Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$output) + & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --filter 'FullyQualifiedName~SyntheticM68010MovecTests' --logger 'trx;LogFileName=movec.trx' --results-directory $output *> (Join-Path $output 'run.log') + if($LASTEXITCODE -ne 0){throw "010 MOVEC execution failed: $output/run.log"} + foreach($item in $original){if((Identity $item.path).sha256 -cne $item.sha256){throw '010 MOVEC input changed during execution'}} + @{schema=1;profile='68010-movec-manual-registers-and-encodings';sourceRevision=(& git rev-parse HEAD); + inputs=$original;binaries=@($binaries|ForEach-Object{Identity $_});evidence=@($evidence|ForEach-Object{Identity $_})}| + ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'identity.json') + } + $id=Get-Content (Join-Path $output 'identity.json') -Raw|ConvertFrom-Json + if($id.schema -ne 1 -or $id.profile -cne '68010-movec-manual-registers-and-encodings' -or $id.sourceRevision -cnotmatch '^[a-f0-9]{40}$'){throw 'Incomplete 010 MOVEC identity'} + foreach($selection in @(@{actual=@($id.inputs);required=$inputs},@{actual=@($id.binaries);required=$binaries},@{actual=@($id.evidence);required=$evidence})){ + $actual=@($selection.actual.path) + if($actual.Count -ne $selection.required.Count -or @($actual|Select-Object -Unique).Count -ne $actual.Count -or + @(Compare-Object ($actual|Sort-Object) ($selection.required|Sort-Object)).Count){throw 'Incomplete/duplicate 010 MOVEC input selection'} + foreach($item in $selection.actual){if($item.sha256 -cnotmatch '^[A-F0-9]{64}$' -or (Identity $item.path).sha256 -cne $item.sha256){throw "Changed/malformed 010 MOVEC input: $($item.path)"}} + } + [xml]$trx=Get-Content (Join-Path $output 'movec.trx') -Raw;$c=$trx.TestRun.ResultSummary.Counters + if($c.total -ne 4 -or $c.executed -ne 4 -or $c.passed -ne 4 -or $c.failed -ne 0 -or $c.notExecuted -ne 0){throw 'Incomplete 010 MOVEC test selection'} + foreach($method in @('Movec010EveryLegalControlRegisterPairPrivilegeAndCcr','Movec010FunctionCodeMasksFromEveryInitialImage', + 'Movec010RawControlImagesPreserveAllBitsAndActiveStack','Movec010UndefinedSelectorsAndLegalUserFormsHaveExactFrames')){ + if(@($trx.TestRun.Results.UnitTestResult|Where-Object{$_.testName -ceq "Copper68k.Tests.Synthetic.SyntheticM68010MovecTests.$method" -and $_.outcome -ceq 'Passed'}).Count -ne 1){throw "Missing 010 MOVEC execution: $method"} + } + $coverage=@() + foreach($group in $groups.Keys){ + $r=Get-Content (Join-Path $output "68010-$group.json") -Raw|ConvertFrom-Json -AsHashtable + if($r.schema -ne 1 -or $r.model -cne '68010' -or $r.group -cne $group -or $r.xunitBatches -ne 1 -or + $r.logicalCases -ne $groups[$group] -or $r.counts.passing -ne $groups[$group] -or $r.counts.mismatching -ne 0 -or + $r.counts.unsupported -ne 0 -or $r.counts.untested -ne 0 -or $r.failures.Count -ne 0){throw "Incomplete 010 MOVEC gate: $group"} + $keys=Expected-Keys $group + if($r.combinations.Count -ne $keys.Count){throw "010 MOVEC combination selection differs: $group"} + foreach($key in $keys.Keys){if(-not $r.combinations.Contains($key) -or $r.combinations[$key].Count -ne 1 -or $r.combinations[$key].passing -ne $keys[$key]){throw "Missing/misweighted 010 MOVEC combination: $key"}} + $coverage+=@{group=$group;logicalCases=$groups[$group];combinations=$keys.Count} + } + @{schema=1;model='68010';logicalCases=2271744;xunitBatches=4;coverage=$coverage;roadmapComplete=$false; + reference='MC68000UM figure 2-3 and 6.3.6/7; M68000PM MOVEC 6-22/23'; + scope='Nontraced IPL7 register/encoding semantics; no function-code bus spaces, restart, prefetch or physical timing oracle'; + remaining='Advanced 010/020/030 restoration, 040 CM/fault lifetime, 060 STOP/PCR disagreement, broader audits and consolidation remain'}| + ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'summary.json') + Write-Host "010 MOVEC qualified: 2271744 cases / 4 tests; $output" +}finally{[Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$savedReports);Pop-Location} diff --git a/scripts/test-copper68k-synthetic-mutations.ps1 b/scripts/test-copper68k-synthetic-mutations.ps1 index e74f06d9..bf38bb75 100644 --- a/scripts/test-copper68k-synthetic-mutations.ps1 +++ b/scripts/test-copper68k-synthetic-mutations.ps1 @@ -1,5 +1,5 @@ [CmdletBinding()] -param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','ChainedOddRte','ChainedOddAccessRte','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','RteSoftwareTrace','UserRteFault','UserRteTrace','UserRteSoftwareTrace','MixedEpochRte','MixedEpochFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings','ProcessorConfiguration')] [string]$Scope = 'All') +param([string]$OutputDirectory = 'artifacts/synthetic-mutations', [ValidateSet('All','Move','Arithmetic','Logical','Control','Consolidation','Rte040','ChainedOddRte','ChainedOddAccessRte','RteValidationFault','RteRepair','RteRetryTrace','RtePendingTrace','RteUserMaster','RteCpVectors','RteSoftwareTrace','UserRteFault','UserRteTrace','UserRteSoftwareTrace','MixedEpochRte','MixedEpochFault','InstructionFault','HandlerPrefetch','EntryPrefetch','AccessDoubleFault','BatchFault','LowPowerStop','CacheEncodings','ProcessorConfiguration','Movec010')] [string]$Scope = 'All') $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -606,6 +606,46 @@ $mutations += @( expectedGroup='system-pcr-reset'; expectedCases=24576; milestone=6; legacyTest='ProcessorIdentificationIsReadOnlyAndResetClearsControls'; legacyFile='Copper68k.Tests/M68060InterpreterTests.cs'} ) +foreach ($mutation in @( + @{name='010-movec-sfc-mask'; before='State.SourceFunctionCode = value & 0x7;'; after='State.SourceFunctionCode = value;'; + group='Movec010FunctionCodeMasksFromEveryInitialImage'; expectedGroup='system-010-movec-masks'; expectedCases=1007616; + legacyTest='MovecTransfersSupportedControlRegisters'; legacyCases=3; proofReason='SFC expected '}, + @{name='010-movec-dfc-mask'; before='State.DestinationFunctionCode = value & 0x7;'; after='State.DestinationFunctionCode = value;'; + group='Movec010FunctionCodeMasksFromEveryInitialImage'; expectedGroup='system-010-movec-masks'; expectedCases=1007616; + legacyTest='MovecTransfersSupportedControlRegisters'; legacyCases=3; proofReason='DFC expected '}, + @{name='010-movec-vbr-width'; before='State.VectorBaseRegister = value;'; after='State.VectorBaseRegister = value & 0x00ffffff;'; + group='Movec010EveryLegalControlRegisterPairPrivilegeAndCcr'; expectedGroup='system-010-movec-pairs'; expectedCases=688128; + legacyTest='MovecTransfersSupportedControlRegisters'; legacyCases=3; proofReason='VBR expected '}, + @{name='010-movec-vbr-bit10'; before='State.VectorBaseRegister = value;'; after='State.VectorBaseRegister = value & ~0x400u;'; + group='Movec010EveryLegalControlRegisterPairPrivilegeAndCcr'; expectedGroup='system-010-movec-pairs'; expectedCases=688128; + legacyTest='MovecTransfersVectorBaseRegister'; legacyCases=1; proofReason='VBR expected '}, + @{name='010-movec-a7-bank'; before='State.SetActiveStackPointer(value);'; after='State.A[register] = value;'; + group='Movec010RawControlImagesPreserveAllBitsAndActiveStack'; expectedGroup='system-010-movec-reads'; expectedCases=48128; + legacyTest='MovecVectorBaseToA7UpdatesActiveSupervisorStackPointer'; legacyCases=1; proofReason='Active SSP expected '}, + @{name='010-movec-stack-model'; before=@' + public M68010Interpreter(IM68kBus bus) + : base( + CreateBus(bus), + default, + opcodePlanDispatch: M68kCoreFactory.M68000OpcodePlanDispatch) + { + } +'@; after=@' + public M68010Interpreter(IM68kBus bus) + : base( + CreateBus(bus), + default, + opcodePlanDispatch: M68kCoreFactory.M68000OpcodePlanDispatch) + { + State.EnableM68020StackMode(); + } +'@; group='Movec010EveryLegalControlRegisterPairPrivilegeAndCcr'; expectedGroup='system-010-movec-pairs'; expectedCases=688128; + legacyTest='MovecTransfersVectorBaseRegister'; legacyCases=1; proofReason='010 stack model expected '} +)) { + $mutation.file='Copper68k/M68010Interpreter.cs';$mutation.movec010=$true;$mutation.milestone=6 + $mutation.legacyFile='Copper68k.Tests/M68010InterpreterTests.cs';$mutations+=$mutation +} +if ($Scope -eq 'Movec010') { $mutations = @($mutations | Where-Object { $_.movec010 }) } if ($Scope -eq 'ProcessorConfiguration') { $mutations = @($mutations | Where-Object { $_.pcr }) } if ($Scope -eq 'Move') { $mutations = @($mutations | Where-Object { -not $_.milestone }) } if ($Scope -eq 'Arithmetic') { $mutations = @($mutations | Where-Object { $_.milestone -eq 3 }) } @@ -657,7 +697,7 @@ try { [IO.File]::WriteAllText($path, $text.Replace($before, $after), [Text.UTF8Encoding]::new($false)) $model = $(if ($mutation.model) { $mutation.model } elseif ($mutation.name -eq '060-divide-frame') { '68060' } else { '68020' }) $filter = "FullyQualifiedName~$($mutation.group)&DisplayName~$model" - if ($mutation.pcr) { $filter = "FullyQualifiedName~$($mutation.group)" } + if ($mutation.pcr -or $mutation.movec010) { $filter = "FullyQualifiedName~$($mutation.group)" } if ($mutation.repair) { # Keep the original repair proof's two complete batches; the # new retained-trace facts have their own four-route gate. @@ -665,7 +705,8 @@ try { } $legacyPresent = $mutation.legacyTest -and [IO.File]::ReadAllText((Join-Path $repo $mutation.legacyFile)).Contains("void $($mutation.legacyTest)(") if ($legacyPresent) { - $filter += "|FullyQualifiedName~$($mutation.legacyTest)" + $legacyFilter = if ($mutation.movec010) { "Copper68k.Tests.M68010InterpreterTests.$($mutation.legacyTest)" } else { $mutation.legacyTest } + $filter += "|FullyQualifiedName~$legacyFilter" if ($mutation.legacyModel) { $filter += "&DisplayName~$($mutation.legacyModel)" } } & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --no-restore --filter $filter --logger "trx;LogFileName=mutation.trx" --results-directory $directory *> (Join-Path $directory 'run.log') @@ -893,6 +934,18 @@ try { throw 'PCR mutation did not execute and detect its complete declared matrix' } } + if ($mutation.movec010) { + [xml]$movecTrx=Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw + $c=$movecTrx.TestRun.ResultSummary.Counters + $expectedTests=1+$(if($legacyPresent){$mutation.legacyCases}else{0}) + if($c.total -ne $expectedTests -or $c.executed -ne $expectedTests -or $c.failed -ne (1+[int][bool]$legacyPresent) -or + $c.notExecuted -ne 0 -or $batches.Count -ne 1 -or $batches[0].model -cne '68010' -or + $batches[0].group -cne $mutation.expectedGroup -or $batches[0].logicalCases -ne $mutation.expectedCases -or + $batches[0].counts.unsupported -ne 0 -or + @($batches[0].failures|Where-Object{$_.status -ceq 'mismatching' -and $_.reason.StartsWith($mutation.proofReason,[StringComparison]::Ordinal)}).Count -eq 0){ + throw '010 MOVEC mutation omitted its complete selection or intended semantic diagnostic' + } + } if ($legacyPresent) { [xml]$legacyTrx = Get-Content -LiteralPath (Join-Path $directory 'mutation.trx') -Raw if (@($legacyTrx.TestRun.Results.UnitTestResult | Where-Object { ($_.testName.EndsWith($mutation.legacyTest) -or $_.testName.Contains($mutation.legacyTest + '(')) -and $_.outcome -eq 'Failed' }).Count -ne 1) { throw 'Original regression did not detect the same mutation' } diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 7df5e77f..03f7a75e 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -142,6 +142,10 @@ try { $expected['system-pcr-reset'] = 24576 } if ($model -eq '68010') { + $expected['system-010-movec-pairs'] = 688128 + $expected['system-010-movec-masks'] = 1007616 + $expected['system-010-movec-reads'] = 48128 + $expected['system-010-movec-encodings'] = 527872 $expected['system-format8-entry'] = 1024 $expected['system-format8-rte'] = 4096 $expected['system-format8-double-fault'] = 256 From d1d467633c2531136fa78223ea06a8c8cc12627c Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 15:52:35 +0300 Subject: [PATCH 067/211] Record 68010 rejected RTE format reference discovery --- .../M68010RteFormatDiscoveryTests.cs | 110 ++++++++++++ Copper68k.Tests/Synthetic/README.md | 22 +++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 132 ++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 31 ++++ scripts/reference/m68010-rte-format.cpp | 122 +++++++++++++ ...est-copper68k-010-rte-format-discovery.ps1 | 165 ++++++++++++++++++ 6 files changed, 582 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/M68010RteFormatDiscoveryTests.cs create mode 100644 scripts/reference/m68010-rte-format.cpp create mode 100644 scripts/test-copper68k-010-rte-format-discovery.ps1 diff --git a/Copper68k.Tests/Synthetic/M68010RteFormatDiscoveryTests.cs b/Copper68k.Tests/Synthetic/M68010RteFormatDiscoveryTests.cs new file mode 100644 index 00000000..321c4abc --- /dev/null +++ b/Copper68k.Tests/Synthetic/M68010RteFormatDiscoveryTests.cs @@ -0,0 +1,110 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Pinned WinUAE changes N/Z/V before format rejection. The manual does not +// establish those failure-path flags. This software disagreement is discovery, +// not a promoted architectural expectation or a full native exception oracle. +public sealed class M68010RteFormatDiscoveryTests(ITestOutputHelper output) +{ + internal const string Enable = "COPPER68K_RUN_010_RTE_FORMAT_DISCOVERY"; + [EnvironmentFact(Enable, "compare 68010 rejected RTE formats with pinned WinUAE"), Trait("Suite", "ReferenceDiscovery")] + public void InvalidFormatWordsAndConditionCodesScalar() => Audit(false); + [EnvironmentFact(Enable, "compare 68010 rejected RTE formats with pinned WinUAE"), Trait("Suite", "ReferenceDiscovery")] + public void InvalidFormatWordsAndConditionCodesBatch() => Audit(true); + + private void Audit(bool batch) + { + var group = "rte-format-discovery-" + (batch ? "batch" : "scalar"); + var report = new CoverageBatch("68010", group); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68010")); + var export = Environment.GetEnvironmentVariable("COPPER68K_010_RTE_FORMAT_EXPORT"); + if (string.IsNullOrWhiteSpace(export)) throw new InvalidOperationException("RTE format discovery requires an export directory"); + using var rows = new StreamWriter(Path.Combine(export, group + ".rows")); + uint index = 0; + foreach (var matrix in new[] { "words", "ccr" }) + foreach (var format in Enumerable.Range(0, 65536).Where(f => f >> 12 is not (0 or 8))) + { + if (matrix == "ccr" && (format & 4095) is not (0 or 4 or 0x24 or 0x3fc or 0x7fc or 0xfff)) continue; + foreach (var supervisor in new[] { false, true }) + foreach (var trace in matrix == "words" ? new[] { 0 } : new[] { 0, 0x8000 }) + foreach (var stackedSr in matrix == "words" ? new[] { 0xa71f } : new[] { 0, 31, 0x2000, 0x201f, 0x8000, 0x801f, 0xa000, 0xa01f }) + foreach (var target in matrix == "words" ? new[] { 0xffff6001u } : new[] { 0x6000u, 0x6001u, 0xffff6000u, 0xffff6001u }) + foreach (var ccr in matrix == "words" ? new[] { 0, 31 } : Enumerable.Range(0, 32)) + { + m.Reset(ccr, supervisor); + m.Core.State.StatusRegister |= (ushort)trace; + var fp = m.Core.State.A[7]; + foreach (var stack in new[] { 0x4700u, 0x7800u, 0x8000u }) + for (var offset = -20; offset < 64; offset++) m.InitializePhysical(unchecked(stack + (uint)offset), (uint)(offset ^ 0x5a), 1); + m.InitializePhysical(fp, (uint)stackedSr, 2); + m.InitializePhysical(fp + 2, target, 4); + m.InitializePhysical(fp + 6, (uint)format, 2); + var e = SyntheticExecution.Prepare(m, [0x4e73]); + var incoming = e.Sr; + var initialUsp = m.Core.State.UserStackPointer; + var initialSsp = m.Core.State.SupervisorStackPointer; + var vector = supervisor ? 14 : 8; + // These are independently encoded software-source expectations. + // X/C are preserved, N is the format sign, Z/V are cleared. + var boundarySr = supervisor ? (ushort)((incoming & ~14) | ((format & 0x8000) != 0 ? 8 : 0)) : incoming; + e.Sr = boundarySr; + // Check every other bit/register/memory location even when N/Z/V + // disagree. Compare these three bits explicitly after verification. + e.DefinedSrMask = 0xfff1; + SyntheticExecution.ExpectException(m, e, vector); + e.ControlChecks["saved SR other bits"] = (s => (uint)(s.LastExceptionStatusRegister & 0xfff1), (uint)(boundarySr & 0xfff1)); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, SyntheticMachine.Code); + e.ControlChecks["stack model"] = (s => s.M68020StackModeEnabled ? 1u : 0u, 0); + var id = $"68010/RTE/{matrix}/format={format:X4}/super={supervisor}/T={trace:X4}/stackedSR={stackedSr:X4}/target={target:X8}/op=4E73/ccr={ccr:X2}"; + string? mismatch; + var status = "passing"; + var invariantsMatch = false; + try + { + if (batch) + { + var boundary = new Boundary(); + var count = ((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, boundary); + mismatch = count != 1 || boundary.Before != 1 || boundary.After != 1 ? "RTE batch count/boundaries differ" : e.Verify(m); + } + else { m.Core.ExecuteInstruction(); mismatch = e.Verify(m); } + if (mismatch == null) + { + var reads = m.Bus.Accesses.Where(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address >= fp && a.Address < fp + 58).ToArray(); + var expected = supervisor ? new[] { (fp, 2), (fp + 2, 4), (fp + 6, 2) } : []; + if (!reads.Select(a => (a.Address, a.Width)).SequenceEqual(expected)) mismatch = "Rejected frame read set/order differs from repository transport policy"; + if (m.Bus.Accesses.Any(a => a.Address == m.Model.Physical(target))) mismatch = "Rejected RTE fetched stacked target"; + var actualSaved = m.Core.State.LastExceptionStatusRegister; + if (m.PeekPhysical(e.A[7], 2) != actualSaved || m.Core.State.StatusRegister != ((actualSaved | 0x2000) & ~0x8000)) + mismatch = "Rejected RTE saved/current SR consistency differs"; + invariantsMatch = mismatch == null; + if (mismatch == null && actualSaved != boundarySr) + mismatch = $"Rejected RTE software-reference saved SR expected {boundarySr:X4}, actual {actualSaved:X4}"; + } + if (mismatch != null) status = "mismatching"; + } + catch (Exception ex) when (ex is UnsupportedM68kOpcodeException or UnsupportedM68kTimingException) + { status = "unsupported"; mismatch = ex.Message; } + catch (Exception ex) { status = "mismatching"; mismatch = ex.ToString(); } + report.Record(id, status, mismatch); + var state = m.Core.State; + var actualVector = Math.Max(0, state.LastExceptionVector); + var saved = actualVector == 0 ? state.StatusRegister : state.LastExceptionStatusRegister; + var fields = new uint[] { index++, incoming, (uint)stackedSr, target, (uint)format, (uint)vector, boundarySr, + (uint)actualVector, saved, state.LastExceptionStackedProgramCounter, state.A[7], state.ProgramCounter, + status == "passing" ? 1u : 0u, initialUsp, initialSsp, invariantsMatch ? 1u : 0u }; + rows.WriteLine(string.Join(' ', fields.Select(v => v.ToString("X8")))); + } + } + Assert.Equal(573440u, index); + report.Complete(output); + } + private sealed class Boundary : IM68kInstructionBoundary + { + public int Before, After; + public bool BeforeInstruction() { Before++; return true; } + public void AfterInstruction(long previousCycle, long currentCycle) => After++; + } +} diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index c795a5a8..bb8a8af9 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1009,3 +1009,25 @@ with original/replacement detection before and after the demonstrated retirement of three duplicate methods. The qualification record maps each original to its replacement; factory, exception, interrupt and specialized regressions remain. The broader roadmap is still incomplete. + +### 68010 rejected RTE format discovery + +`M68010RteFormatDiscoveryTests` is a gated software-reference audit, outside +ordinary synthetic CI and promoted coverage. Run: + +```powershell +./scripts/test-copper68k-010-rte-format-discovery.ps1 -ReferenceDirectory artifacts/reference-winuae-rte-modern -OutputDirectory artifacts/010-rte-format-discovery +./scripts/test-copper68k-010-rte-format-discovery.ps1 -ValidateReportsOnly -OutputDirectory artifacts/010-rte-format-discovery +``` + +Both commands currently fail on rejected-format CCR disagreement. Each scalar/ +batch route executes every invalid format word plus a separate CCR/trace/header +matrix. The native observer executes untouched generic and compatible 68010 RTE +to their exception callback, at the existing modern WinUAE pin. Eight literal +controls include valid short-frame returns. Exact input order, combination keys, +weights, classifications and frozen replay are mandatory. Surrounding-state +agreement is recorded independently on every row, even on CCR mismatch. +Frame entry is composed; +full native exception/trace processing, physical bus timing and long-frame restart +remain unqualified. No production fix or regression retirement follows from this +software discrepancy. Details are in `docs/COPPER68K_REFERENCE_QUALIFICATION.md`. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index c6cd220e..323e8d55 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -6137,3 +6137,135 @@ Advanced 010/020/030 restoration, full CM/fault lifetime, STOP/PCR disagreement, broader model audits and consolidation remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`; no production fix, consumer replay or package publication is included. + +### Rejected 68010 RTE format software-reference discovery (2026-10-06) + +The remaining invalid-RTE CCR/trace work now has an executed discovery rather +than an inferred decoder expectation. `M68010RteFormatDiscoveryTests` exercises +scalar and one-instruction batch execution through the public 68010 factory. +`scripts/test-copper68k-010-rte-format-discovery.ps1` generates, extracts and +executes untouched WinUAE generic `op_4e73_4_ff` and compatible +`op_4e73_11_ff`, together with original MakeSR / MakeFromSR helpers, at pin +`5d22d33632646efc3f747f03e82d28353e52722e`. + +The [pinned generator](https://github.com/tonioni/WinUAE/blob/5d22d33632646efc3f747f03e82d28353e52722e/gencpu.cpp#L7012) +sets N from the sign of an invalid format word, clears Z/V and preserves X/C +before its vector-14 callback. Copper68k instead preserves all incoming CCR +bits. Generic and compatible functions agree on these flags, privilege precedence, +callback PC, unconsumed stack and unchanged registers. They share generator +provenance, so agreement between them is not two independent hardware oracles. + +[MC68000UM section 6.4](https://www.nxp.com/docs/en/reference-manual/MC68000UM.pdf) +defines format rejection before the stack is consumed. Section 6.3.7 defines +privilege rejection and section 6.3.8 describes trace/aborted-instruction rules. +These passages do not specifically establish N/Z/V on an invalid-format failure. +The executed software result is therefore an unresolved architectural qualification +question; no CPU fix is selected solely from this discrepancy. + +Each route executes the following separate deterministic groups within one batch: + +| Matrix | Selection | Cases / combinations | +| --- | --- | ---: | +| Words | All 57,344 words with format nibble 1–7 or 9–F, user/supervisor, CCR 0/31, fixed stacked A71F and odd FFFF6001 target | 229,376 / 114,688 | +| CCR/header | Fourteen invalid formats, offsets 000/004/024/3FC/7FC/FFF, user/supervisor, incoming T clear/set, eight stacked SRs, four even/odd/high-address target PCs and every incoming CCR | 344,064 / 10,752 | +| Total, each route | Both matrices | **573,440 / 125,440** | + +Incoming IPL is 7. The CCR/header matrix's stacked SRs are 0000/001F, +2000/201F, 8000/801F and A000/A01F; targets are 00006000/00006001 and +FFFF6000/FFFF6001. Entire invalid words include reserved low fields, but this +does not assert behavior for otherwise valid reserved-format encodings. Valid +format0/8 words and version/internal format8 restoration are outside this matrix. + +Each route reports **336,896 passing / 236,544 mismatching / zero unsupported / +zero untested cases**. Both named tests execute and fail without skips. Every +user-mode row rejects with vector8 and unchanged CCR. Supervisor rows reject +with vector14 and expose only the N/Z/V disagreement. For example incoming +271F, format1000, stacked A71F and targetFFFF6001 yields software saved SR2711 +versus Copper68k271F, with saved PC1000, handler90E0 and SP46F8 on both sides. +Changing stacked S/T/CCR or the target never bypasses rejection. + +All other SR bits, D/A registers, stack banks, execution state, control-stack +configuration, original frame bytes and surroundings are compared even when +N/Z/V mismatch. Raw stacked SR must equal the CPU's recorded saved SR, and +the current SR must follow exception-entry clearing. Repository header read sets +are checked separately, and the rejected target must never be fetched. There is +no retry, dependent sentinel or partially abandoned operand phase. + +Every row exports a separate surrounding-state result before the CCR comparison; +all **573,440 results per route pass**. The native observer and report validator +require that result independently, so a CCR mismatch cannot conceal a register, +frame, callback, stack-bank or access invariant failure. + +The native observer checks unconsumed stack/registers at the exception callback. +Generic physical transport reads SR, both PC words and format; compatible source +reads SR, format and PC high before rejection. This difference is retained and +explicitly checked, not normalized into a physical bus-order claim. Native +exception-frame entry, trace/IRQ run-loop delivery and timing are not executed; +the format0 frame and vector fixtures are compositions in the full CPU comparison. +Long-frame version checks, restart, prefetch fault sequencing and bus errors are +not qualified by these generated functions. Eight literal controls per observer +invocation check low/high format rejection, user privilege and valid short returns +on both implementations. + +Fresh execution in `artifacts/m6-010-rte-format-qualified` completes all synthetic +and native rows and returns exit1 explicitly for **473,088 combined discrepancies**. +Report-only revalidation also executes the frozen observer and requires identical +outputs. The command verifies the exact two named executions, all pinned source / +generated / fixture / CPU/test binary / evidence identities, every independently +enumerated key and weight, all fixture row ordering and all per-combination +classifications. Missing or empty fixtures/selections cannot pass. The preliminary +`artifacts/m6-010-rte-format-first` observer transport failed its literal controls +because wrapper long-read evaluation order was unspecified; it is excluded. +Sequential transport reads fixed that wrapper, leaving extracted functions intact. + +The second preliminary audit recorded the same counts but initialized the native +inactive SSP to 4700 in user mode, while the CPU fixture uses 8000. This bank was +not consumed before callback, but the final audit aligns and exports both initial +stack banks and independently verifies them on every row. The extracted RTE +hash remains unchanged. Preliminary reports are not the accepted final identities. + +The retained ordinary `system-rte` group's invalid-010 CCR preservation assertion +remains a repository behavior check; it is not independent flag qualification. +This separate gated discovery keeps the software disagreement visible and failing. + +Twenty-three integrity controls in `artifacts/m6-010-rte-format-integrity-proof` +reject missing manifests/inputs/fixtures/reports, duplicate identities, wrong +profiles/models/named or empty selections, changed keys/weights/classifications, +reordered rows, altered native SR, wrong initial stack banks and failed +surrounding-state outcomes. Native controls additionally reject missing/empty +inputs, valid-format substitution, wrong declared SR/saved PC, wrong stack-bank +images and hidden surrounding-state failure. Content changes refresh evidence +hashes before validation, so these exercise semantic gates rather than merely +detecting changed hashes. All subprocess statuses and expected reasons are checked; +the control harness itself completes with exit0. Original evidence stays intact. + +Retained evidence is `artifacts/m6-010-rte-format-retained-qualified`: **25 passing tests / +zero failures / zero skips**, with **2,404,896 passing cases / twelve reports** +and thirteen fixed examples. Selection includes all current MOVEC, format8, +word-MOVE restart and `M68010InterpreterTests` cases. No full CPU or consumer +replay is claimed for this test/observer-only change. Fresh Release build has +zero .NET warnings/errors; native extracted code emits conversion, unused-variable +and unreachable-branch warnings on the bounded throw-on-outside-profile transport. + +Frozen source starts at `90e3afa55bb56a5db3c580133b30606a84249b3c` plus the +recorded test/observer inputs. Committed production CPU tree remains +`795fd12d6c0237a22a5f92f4a96cc4823e0364c1`. +CPU SHA256: `d3ce6efdd0d54539e788d3eb8b3bc5ca092732352e067470d4ba0e55eccad157`. +Test SHA256: `54dca077898ab6f7e75bced71b4b01f409cf5cee7ae8c70612494766139304c8`. +Extracted instruction SHA256: `23ddb060c814ba5f4c6ee9d79e26ba0a036f0787229c8fdf1f6efa36f473da4f`. +Observer SHA256: `40c73a34417c8238d3472f5e09957cbeba6b13db02734fb89869e232f10caa20`. + +```powershell +./scripts/test-copper68k-010-rte-format-discovery.ps1 -OutputDirectory artifacts/010-rte-format-discovery +./scripts/test-copper68k-010-rte-format-discovery.ps1 -ValidateReportsOnly -OutputDirectory artifacts/010-rte-format-discovery +``` + +Both requested commands fail for the preserved mismatch. This discovery stays +outside ordinary synthetic CI and complete-040 selection. Ordinary requirements +remain 85,191,062 cases / 680 reports, not a fresh broad-suite result. Complete-040 +and its 480-case untested inventory remain unchanged. No regression is retired, +production CPU/timing/API changed, private consumer package refreshed or public +package published. Invalid-RTE flag architecture, long/RMW/foreign 010 restoration, +020/030 advanced frames, full CM/fault lifetime, STOP/PCR discrepancies, broader +independent model audits and consolidation remain required. Milestone 6 stays +**in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index f36ebbbd..872e0050 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2349,3 +2349,34 @@ Physical function-code spaces, bus ordering and advanced 010 restoration are not qualified by MOVEC register tests. All remaining CM/restoration, STOP/PCR, independent-model and consolidation requirements remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +### Milestone 6 rejected 68010 RTE format discovery + +Two gated scalar/batch audits execute 573,440 cases / 125,440 combinations each. +Every invalid format word is enumerated with CCR 0/31 and both privilege states; +a separate canonical-offset matrix covers all CCRs, incoming trace, both privilege +states, eight stacked SR images and four even/odd/high-address PC images. + +Untouched generic and compatible WinUAE 68010 RTE functions at the existing +modern pin both change N/Z/V before format rejection. Copper68k preserves these +bits. Each route records 336,896 passing and 236,544 mismatching cases, with zero +unsupported or untested cases. Every other register/defined bit, stack bank, +original frame byte and exception-header field is checked even on CCR mismatch; +both named tests fail without skips. The command independently verifies all keys, +weights, rows and classifications and executes frozen observer replay. Separate +surrounding-state outcomes must pass on every row, even on CCR disagreement. Native +exception entry is composed, and the manual does not settle the failure-path +flags; this is software discovery, not an architectural CPU-fix gate. + +Fresh execution and frozen revalidation explicitly fail the 473,088 combined +CCR discrepancies. Twenty-three integrity controls reject incomplete or altered +fixtures, identities, selections, classifications, initial stack banks and hidden +surrounding-state failures. Original evidence and production source remain intact. + +The ordinary 68010 retained selection passes 25 tests without skips, comprising +2,404,896 cases / twelve reports plus thirteen fixed examples. Production CPU +source, timing policy, public API, ordinary CI requirements, complete-040 counts +and packages remain unchanged. No regression is retired. Rejected RTE CCR +architecture, long/RMW/foreign format8 continuation, full exception/trace entry, +and all other reference/consolidation requirements remain required. Milestone 6 +stays **in progress**, `roadmapComplete=false`. diff --git a/scripts/reference/m68010-rte-format.cpp b/scripts/reference/m68010-rte-format.cpp new file mode 100644 index 00000000..6e35b6ba --- /dev/null +++ b/scripts/reference/m68010-rte-format.cpp @@ -0,0 +1,122 @@ +// Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT +// Untouched generated 010 RTE, generic and compatible, to exception callback. +// No native exception frames, IRQ, trace/run loop, format8 restart or timing oracle. +#include +#include +#include +#include +#include +#include +#include +using uae_u16=uint16_t; using uae_u32=uint32_t; using uaecptr=uint32_t; +using uae_s16=int16_t; using uae_s32=int32_t; +#define REGPARAM2 +#define CYCLE_UNIT 2 +#define SPCFLAG_TRACE 0x40 +#define SPCFLAG_DOTRACE 0x80 +#define SPCFLAG_INT 8 +#define SPCFLAG_DOINT 0x100 +#define m68k_areg(r,n) ((r).a[n]) +struct Regs { uint32_t d[8]{},a[8]{},usp{},isp{},msp{},pc{},spcflags{}; uint16_t sr{},ir{},irc{}; int s{},m{},t0{},t1{},intmask{},stopped{},ipl[1]{},ipl_pin{}; } regs; +struct Prefs { int cpu_model=68010,mmu_model{},cachesize{},cpu_compatible{},cpu_memory_cycle_exact{}; } currprefs; +bool m68k_accurate_ipl=false,hardware_bus_error=false; +int xflag,nflag,zflag,vflag,cflag,callbacks,callbackVector; +uint16_t frameSr,frameFormat;uint32_t framePc,frameBase; +std::vector reads; +#define SET_XFLG(v) (xflag=(v)) +#define SET_NFLG(v) (nflag=(v)) +#define SET_ZFLG(v) (zflag=(v)) +#define SET_VFLG(v) (vflag=(v)) +#define SET_CFLG(v) (cflag=(v)) +#define GET_XFLG() xflag +#define GET_NFLG() nflag +#define GET_ZFLG() zflag +#define GET_VFLG() vflag +#define GET_CFLG() cflag +void set_special(int v) { regs.spcflags|=v; } +void unset_special(int v) { regs.spcflags&=~v; } +void activate_trace() { unset_special(SPCFLAG_TRACE);set_special(SPCFLAG_DOTRACE); } +void mmu_set_super(int) { throw std::runtime_error("Enabled MMU outside RTE profile"); } +uint32_t m68k_getpc(){return regs.pc;} +uint32_t m68k_getpci(){return regs.pc;} +void m68k_setpc_j(uint32_t pc){regs.pc=pc;} +void m68k_setpci_j(uint32_t pc){regs.pc=pc;} +void m68k_incpci(uint32_t n){regs.pc+=n;} +void Exception(int vector){if(vector!=8 && vector!=14)throw std::runtime_error("Unexpected RTE exception");if(++callbacks!=1)throw std::runtime_error("Repeated RTE exception");callbackVector=vector;} +void Exception_cpu(int vector){Exception(vector);} +void exception3_read_access(uint32_t,uint32_t,int,int){throw std::runtime_error("Odd stack outside profile");} +void exception3_read_prefetch(uint32_t,uint32_t){throw std::runtime_error("Odd successful return outside profile");} +void exception3_read_prefetch_only(uint32_t,uint32_t){throw std::runtime_error("Odd successful return outside profile");} +void exception2_read(uint32_t,uint32_t,int,int){throw std::runtime_error("Bus fault outside profile");} +void exception2_fetch_opcode(uint32_t,int,int){throw std::runtime_error("Fetch fault outside profile");} +uint16_t get_word(uint32_t address){ + reads.push_back(address); + if(address==frameBase)return frameSr; + if(address==frameBase+2)return static_cast(framePc>>16); + if(address==frameBase+4)return static_cast(framePc); + if(address==frameBase+6)return frameFormat; + throw std::runtime_error("Unexpected frame/operand read"); +} +uint16_t get_word_000(uint32_t address){return get_word(address);} +uint32_t get_long(uint32_t address){const auto high=get_word(address);const auto low=get_word(address+2);return (static_cast(high)<<16)|low;} +uint16_t get_word_000_prefetch(int offset){if(regs.pc!=0x6000 || (offset!=0 && offset!=2))throw std::runtime_error("Unexpected prefetch");return regs.irc=0x4e71;} +#include "winuae-rte-format-sr.inc" +#include "winuae-rte-format-op.inc" +struct Outcome{uint32_t vector,sr,pc,sp;}; +Outcome execute(uint16_t initial,uint16_t stacked,uint32_t target,uint16_t format,bool compatible){ + regs={};callbacks=callbackVector=0;reads.clear(); + regs.sr=initial;regs.s=(initial>>13)&1;regs.t1=(initial>>15)&1;regs.intmask=(initial>>8)&7; + xflag=(initial>>4)&1;nflag=(initial>>3)&1;zflag=(initial>>2)&1;vflag=(initial>>1)&1;cflag=initial&1; + regs.usp=0x7800;regs.isp=regs.s?0x4700:0x8000;regs.pc=0x1000; + for(int n=0;n<8;n++){regs.d[n]=n==7?2:0xa55a0022u+n*0x100;regs.a[n]=n==7?(regs.s?0x4700:0x7800):0x4000+n*0x100;} + frameSr=stacked;framePc=target;frameFormat=format;frameBase=regs.a[7]; + if(compatible)op_4e73_11_ff(0x4e73);else op_4e73_4_ff(0x4e73); + MakeSR(); + if(callbackVector){ + const auto expected=regs.s?(compatible?std::vector{frameBase,frameBase+6,frameBase+2}:std::vector{frameBase,frameBase+2,frameBase+4,frameBase+6}):std::vector{}; + if(reads!=expected || regs.a[7]!=frameBase || regs.usp!=0x7800 || regs.isp!=((initial&0x2000)?0x4700u:0x8000u) || regs.pc!=0x1000)throw std::runtime_error("Rejected RTE transport/state differs"); + for(int n=0;n<7;n++)if(regs.a[n]!=0x4000u+n*0x100)throw std::runtime_error("Rejected RTE changed address register"); + for(int n=0;n<8;n++)if(regs.d[n]!=(n==7?2:0xa55a0022u+n*0x100))throw std::runtime_error("Rejected RTE changed data register"); + } + return {static_cast(callbackVector),regs.sr,regs.pc,regs.a[7]}; +} +void controls(){ + for(bool compatible:{false,true}){ + auto r=execute(0x271f,0xa000,0xffff6001,0x1000,compatible); + if(r.vector!=14||r.sr!=0x2711||r.pc!=0x1000||r.sp!=0x4700)throw std::runtime_error("Low-format literal control failed"); + r=execute(0xa700,0x1f,0x6001,0xf024,compatible); + if(r.vector!=14||r.sr!=0xa708||r.pc!=0x1000||r.sp!=0x4700)throw std::runtime_error("High-format literal control failed"); + r=execute(0x871f,0xa000,0xffff6001,0xf024,compatible); + if(r.vector!=8||r.sr!=0x871f||r.pc!=0x1000||r.sp!=0x7800)throw std::runtime_error("Privilege literal control failed"); + r=execute(0x271f,0x2005,0x6000,0x24,compatible); + if(r.vector!=0||r.sr!=0x2005||r.pc!=0x6000||r.sp!=0x4708)throw std::runtime_error("Valid short-frame literal control failed"); + } +} +int main(int argc,char**argv){ + try{ + controls(); + if(argc!=4||std::string(argv[3])!="573440")throw std::runtime_error("RTE audit requires fixture/output/count"); + std::ifstream input(argv[1]);std::ofstream output(argv[2]);if(!input||!output)throw std::runtime_error("Missing RTE fixture/output"); + std::string line;uint32_t index=0,passing=0,mismatching=0; + while(std::getline(input,line)){ + std::istringstream row(line);std::vector v;uint64_t value; + while(row>>std::hex>>value){if(value>UINT32_MAX)throw std::runtime_error("Wide RTE field");v.push_back(static_cast(value));} + if(!row.eof()||v.size()!=16||v[0]!=index||index>=573440||(v[1]&~0xa71f)||(v[2]&~0xa71f)||v[4]>0xffff||v[4]>>12==0||v[4]>>12==8||v[12]>1||v[13]!=0x7800||v[14]!=((v[1]&0x2000)?0x4700u:0x8000u)||v[15]>1)throw std::runtime_error("Malformed/out-of-scope RTE fixture"); + if(v[15]!=1)throw std::runtime_error("Other RTE architectural state mismatched"); + const auto a=execute(static_cast(v[1]),static_cast(v[2]),v[3],static_cast(v[4]),false); + const auto b=execute(static_cast(v[1]),static_cast(v[2]),v[3],static_cast(v[4]),true); + if(a.vector!=b.vector||a.sr!=b.sr||a.pc!=b.pc||a.sp!=b.sp)throw std::runtime_error("Generic/compatible RTE disagree"); + if(a.vector!=v[5]||a.sr!=v[6]||a.pc!=0x1000||a.sp!=((v[1]&0x2000)?0x4700u:0x7800u))throw std::runtime_error("Declared RTE expectation differs from executed source"); + // Exception entry is composed using the documented format0 size and + // repository vector fixtures. No native frame/trace-entry execution. + const uint32_t sp=(v[1]&0x2000)?0x46f8:0x7ff8; + const bool match=a.vector==v[7]&&a.sr==v[8]&&a.pc==v[9]&&sp==v[10]&&v[11]==0x9000+a.vector*0x10; + if(match!=(v[12]!=0))throw std::runtime_error("Full synthetic/transport comparator differs"); + if(match)passing++;else mismatching++; + output<nul && set" + if($LASTEXITCODE -ne 0){throw 'MSVC setup failed'} + foreach($setting in $settings){$at=$setting.IndexOf('=');if($at -gt 0){$name=$setting.Substring(0,$at);$savedCompiler[$name]=[Environment]::GetEnvironmentVariable($name);[Environment]::SetEnvironmentVariable($name,$setting.Substring($at+1))}} + Push-Location $output + try{ + & cl /nologo /O2 /EHsc /w /DWIN32 /DUNICODE /D_UNICODE "/I$source/include" "/I$source/od-win32" "/I$source" (Join-Path $source 'build68k.cpp') /Fe:build68k.exe *> build68k.log + if($LASTEXITCODE -ne 0){throw 'Opcode builder failed'} + Get-Content -LiteralPath (Join-Path $source 'table68k')|& ./build68k.exe|Set-Content -Encoding utf8 cpudefs.cpp + if($LASTEXITCODE -ne 0 -or -not (Select-String -LiteralPath cpudefs.cpp -SimpleMatch 'n_defs68k = 232')){throw 'Missing opcode definitions'} + & cl /nologo /O2 /EHsc /w /DWIN32 /DUNICODE /D_UNICODE "/I$source/include" "/I$source/od-win32" "/I$source" (Join-Path $source 'gencpu.cpp') (Join-Path $source 'readcpu.cpp') (Join-Path $source 'missing.cpp') (Join-Path $source 'od-win32/unicode.cpp') cpudefs.cpp user32.lib /Fe:gencpu.exe *> generator-build.log + if($LASTEXITCODE -ne 0){throw 'Generator build failed'} + & ./gencpu.exe *> generator-run.log + if($LASTEXITCODE -ne 0){throw 'Reference generation failed'} + $native=[IO.File]::ReadAllText((Join-Path $source 'newcpu.cpp')) + (@('void REGPARAM2 MakeSR(void)','static void MakeFromSR_x(int t0trace)','void REGPARAM2 MakeFromSR_T0(void)','void REGPARAM2 MakeFromSR(void)')| + ForEach-Object{FunctionText $native $_}) -join "`n`n"|Set-Content -Encoding utf8 winuae-rte-format-sr.inc + (FunctionText ([IO.File]::ReadAllText((Join-Path $output 'cpuemu_0.cpp'))) 'uae_u32 REGPARAM2 op_4e73_4_ff(uae_u32 opcode)')+"`n`n"+ + (FunctionText ([IO.File]::ReadAllText((Join-Path $output 'cpuemu_11.cpp'))) 'uae_u32 REGPARAM2 op_4e73_11_ff(uae_u32 opcode)')| + Set-Content -Encoding utf8 winuae-rte-format-op.inc + & cl /nologo /O2 /EHsc /W4 "/I$output" (Join-Path $repo 'scripts/reference/m68010-rte-format.cpp') /Fe:observer.exe *> observer-build.log + if($LASTEXITCODE -ne 0){throw 'RTE observer build failed'} + }finally{Pop-Location;foreach($name in $savedCompiler.Keys){[Environment]::SetEnvironmentVariable($name,$savedCompiler[$name])}} + & dotnet build (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release *> (Join-Path $output 'build.log') + if($LASTEXITCODE -ne 0){throw 'RTE fixture build failed'} + $saved=@{};$settings=@{COPPER68K_RUN_010_RTE_FORMAT_DISCOVERY='1';COPPER68K_SYNTHETIC_REPORT_DIR=$output;COPPER68K_010_RTE_FORMAT_EXPORT=$output} + try{ + foreach($name in $settings.Keys){$saved[$name]=[Environment]::GetEnvironmentVariable($name);[Environment]::SetEnvironmentVariable($name,$settings[$name])} + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --no-build --no-restore --filter 'FullyQualifiedName~M68010RteFormatDiscoveryTests' --logger 'trx;LogFileName=discovery.trx' --results-directory $output *> (Join-Path $output 'run.log') + $testExit=$LASTEXITCODE + if($testExit -notin @(0,1)){throw "RTE tests did not execute: $testExit"} + }finally{foreach($name in $saved.Keys){[Environment]::SetEnvironmentVariable($name,$saved[$name])}} + foreach($group in $groups){ + & (Join-Path $output 'observer.exe') (Join-Path $output "$group.rows") (Join-Path $output "$group.reference") 573440 *> (Join-Path $output "$group.reference.log") + if($LASTEXITCODE -notin @(0,1)){throw "RTE reference execution failed: $group"} + } + $identity=@{schema=1;reference='WinUAE-010-RTE-format-discovery';referenceCommit=$pin;sourceCommit=(& git -C $repo rev-parse HEAD); + softwareReferenceExecuted=$true;nativeImplementations=2;nativeLiteralControls=8;exceptionEntryComposed=$true;fullExceptionOracle=$false;architecturallyQualified=$false;roadmapComplete=$false; + inputs=@();referenceInputs=@();generated=@();evidence=@()} + Check-Identities $originalInputs $sourceFiles $repo 'fixture/CPU source during execution' + Check-Identities $originalReferenceInputs $referenceFiles $source 'reference during execution' + $identity.inputs=$originalInputs+@($fixtures|Where-Object{$_ -like '*.dll'}|ForEach-Object{@{file=$_;sha256=(Hash (Join-Path $repo $_))}}) + $identity.referenceInputs=$originalReferenceInputs + foreach($file in $generated){$identity.generated+=@{file=$file;sha256=(Hash (Join-Path $output $file))}} + foreach($file in $evidence){$identity.evidence+=@{file=$file;sha256=(Hash (Join-Path $output $file))}} + $identity|ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'identities.json') +} +$identity=Get-Content (Join-Path $output 'identities.json') -Raw|ConvertFrom-Json +if($identity.schema -ne 1 -or $identity.reference -cne 'WinUAE-010-RTE-format-discovery' -or $identity.referenceCommit -cne $pin -or + $identity.sourceCommit -cnotmatch '^[a-f0-9]{40}$' -or $identity.softwareReferenceExecuted -ne $true -or $identity.nativeImplementations -ne 2 -or + $identity.nativeLiteralControls -ne 8 -or $identity.exceptionEntryComposed -ne $true -or $identity.fullExceptionOracle -ne $false -or + $identity.architecturallyQualified -ne $false -or $identity.roadmapComplete -ne $false){throw 'Incomplete RTE discovery identity'} +Check-Identities $identity.inputs (@(& git -C $repo ls-files 'Copper68k/*')+$fixtures) $repo 'fixture/CPU' +Check-Identities $identity.referenceInputs $referenceFiles $source 'reference' +Check-Identities $identity.generated $generated $output 'generated' +Check-Identities $identity.evidence $evidence $output 'evidence' +[xml]$trx=Get-Content (Join-Path $output 'discovery.trx') -Raw +$c=$trx.TestRun.ResultSummary.Counters +if($c.total -ne 2 -or $c.executed -ne 2 -or $c.notExecuted -ne 0 -or ([int]$c.passed+[int]$c.failed) -ne 2){throw 'RTE discovery must execute both complete routes'} +foreach($method in @('InvalidFormatWordsAndConditionCodesScalar','InvalidFormatWordsAndConditionCodesBatch')){ + if(@($trx.TestRun.Results.UnitTestResult|Where-Object{$_.testName -ceq "Copper68k.Tests.Synthetic.M68010RteFormatDiscoveryTests.$method" -and $_.outcome -cin @('Passed','Failed')}).Count -ne 1){throw "Missing RTE execution: $method"} +} +$keys=[Collections.Generic.Dictionary[string,int]]::new([StringComparer]::Ordinal) +$fixtureKeys=[Collections.Generic.List[string]]::new() +$fixture=[Collections.Generic.List[string]]::new();$index=0 +foreach($matrix in @('words','ccr')){foreach($format in 0..65535){ +if(($format -shr 12) -in @(0,8) -or ($matrix -eq 'ccr' -and ($format -band 4095) -notin @(0,4,0x24,0x3fc,0x7fc,0xfff))){continue} +foreach($supervisor in @($false,$true)){foreach($trace in $(if($matrix -eq 'words'){@(0)}else{@(0,0x8000)})){ +foreach($stacked in $(if($matrix -eq 'words'){@(0xa71f)}else{@(0,31,0x2000,0x201f,0x8000,0x801f,0xa000,0xa01f)})){ +foreach($target in $(if($matrix -eq 'words'){@([uint32]0xffff6001L)}else{@([uint32]0x6000,[uint32]0x6001,[uint32]0xffff6000L,[uint32]0xffff6001L)})){ + $key='68010/RTE/{0}/format={1:X4}/super={2}/T={3:X4}/stackedSR={4:X4}/target={5:X8}' -f $matrix,$format,$supervisor,$trace,$stacked,$target + if(-not $keys.TryAdd($key,$(if($matrix -eq 'words'){2}else{32}))){throw "Duplicate RTE combination: $key"} + foreach($ccr in $(if($matrix -eq 'words'){@(0,31)}else{0..31})){ + $sr=$(if($supervisor){0x2700}else{0x700}) -bor $trace -bor $ccr + $vector=if($supervisor){14}else{8} + $boundary=if($supervisor){($sr -band 0xfff1) -bor $(if($format -band 0x8000){8}else{0})}else{$sr} + $fixture.Add(('{0:X8} {1:X8} {2:X8} {3:X8} {4:X8} {5:X8} {6:X8}' -f $index++,$sr,$stacked,$target,$format,$vector,$boundary)) + $fixtureKeys.Add($key) + } +}}}}}} +if($fixture.Count -ne 573440 -or $keys.Count -ne 125440){throw "Independent RTE selection differs: $($fixture.Count)/$($keys.Count)"} +$mismatches=0;$summaries=@() +foreach($group in $groups){ + $report=Get-Content (Join-Path $output "68010-$group.json") -Raw|ConvertFrom-Json -AsHashtable + if($report.schema -ne 1 -or $report.model -cne '68010' -or $report.group -cne $group -or $report.xunitBatches -ne 1 -or $report.logicalCases -ne 573440 -or + $report.counts.unsupported -ne 0 -or $report.counts.untested -ne 0 -or ($report.counts.passing+$report.counts.mismatching) -ne 573440 -or $report.combinations.Count -ne $keys.Count){throw 'Incomplete RTE report'} + foreach($key in $keys.Keys){if(-not $report.combinations.Contains($key) -or (@($report.combinations[$key].Values)|Measure-Object -Sum).Sum -ne $keys[$key] -or + @($report.combinations[$key].Keys|Where-Object{$_ -cnotin @('passing','mismatching')}).Count){throw "Missing/misweighted RTE combination: $key"}} + $rows=[IO.File]::ReadAllLines((Join-Path $output "$group.rows"));$reference=[IO.File]::ReadAllLines((Join-Path $output "$group.reference")) + if($rows.Count -ne $fixture.Count -or $reference.Count -ne $fixture.Count){throw 'Incomplete RTE rows'} + $nativeMismatch=0 + $classified=[Collections.Generic.Dictionary[string,int[]]]::new([StringComparer]::Ordinal) + for($i=0;$i -lt $fixture.Count;$i++){ + $fields=$rows[$i].Split(' ');if($fields.Count -ne 16 -or ($fields[0..6] -join ' ') -cne $fixture[$i]){throw "RTE fixture order/selection differs: $i"} + $numbers=[uint32[]]::new(16);for($n=0;$n -lt 16;$n++){$numbers[$n]=[uint32]::Parse($fields[$n],[Globalization.NumberStyles]::HexNumber)} + if($numbers[13] -ne 0x7800 -or $numbers[14] -ne $(if($numbers[1] -band 0x2000){0x4700}else{0x8000})){throw "RTE initial stack banks differ: $i"} + if($numbers[15] -ne 1){throw "RTE surrounding architectural state differs: $i"} + $refFields=$reference[$i].Split(' ');if($refFields.Count -ne 6){throw "Malformed RTE reference row: $i"} + $ref=[uint32[]]::new(6);for($n=0;$n -lt 6;$n++){$ref[$n]=[uint32]::Parse($refFields[$n],[Globalization.NumberStyles]::HexNumber)} + $sp=if($numbers[1] -band 0x2000){0x46f8}else{0x7ff8} + $match=$numbers[5] -eq $numbers[7] -and $numbers[6] -eq $numbers[8] -and $numbers[9] -eq 0x1000 -and $numbers[10] -eq $sp -and $numbers[11] -eq (0x9000+$numbers[5]*16) + if($ref[0] -ne $i -or $ref[1] -ne $numbers[5] -or $ref[2] -ne $numbers[6] -or $ref[3] -ne 0x1000 -or + $ref[4] -ne $(if($numbers[1] -band 0x2000){0x4700}else{0x7800}) -or $ref[5] -ne [int]$match -or $numbers[12] -ne [int]$match){throw "RTE reference/comparator outcome differs: $i"} + if(-not $match){$nativeMismatch++} + $key=$fixtureKeys[$i] + if(-not $classified.ContainsKey($key)){$classified[$key]=[int[]]@(0,0)} + $classified[$key][[int](-not $match)]++ + } + if($nativeMismatch -ne $report.counts.mismatching){throw 'RTE native/comparator mismatch counts differ'} + foreach($key in $keys.Keys){ + if([int]$report.combinations[$key].passing -ne $classified[$key][0] -or [int]$report.combinations[$key].mismatching -ne $classified[$key][1]){ + throw "RTE per-combination classifications differ: $key" + } + } + & (Join-Path $output 'observer.exe') (Join-Path $output "$group.rows") (Join-Path $output "$group.replayed") 573440 *> (Join-Path $output "$group.replay.log") + if($LASTEXITCODE -notin @(0,1) -or (Hash (Join-Path $output "$group.replayed")) -cne (Hash (Join-Path $output "$group.reference"))){throw 'Frozen RTE reference replay differs'} + $mismatches+=$report.counts.mismatching;$summaries+=@{group=$group;logicalCases=573440;combinations=$keys.Count;surroundingStatePassing=573440;counts=$report.counts} +} +@{schema=1;softwareReferenceExecuted=$true;nativeImplementations=2;nativeLiteralControls=8;fullExceptionOracle=$false;exceptionEntryComposed=$true; + architecturallyQualified=$false;roadmapComplete=$false;groups=$summaries; + remaining='Rejected RTE CCR architecture, full native exception/trace frames, long/RMW/foreign 010 restart and broader model qualification/consolidation remain required'}| + ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'summary.json') +if($mismatches){throw "RTE software-reference discovery found $mismatches mismatches; not an architectural CPU-fix claim"} From f028c34be4fa5e08fbe93ac2f54476360220c86b Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 17:17:18 +0300 Subject: [PATCH 068/211] Fix 68010 long MOVE fault continuation and qualify restart coverage --- Copper68k.Tests/Synthetic/README.md | 23 +- .../Synthetic/SyntheticM68010Format8Tests.cs | 16 +- .../SyntheticM68010LongMoveRestartTests.cs | 391 ++++++++++++++++++ Copper68k/M68010Interpreter.cs | 15 +- Copper68k/M68010LongMoveResumeFrame.cs | 31 ++ Copper68k/M68kCore.cs | 90 ++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 132 ++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 38 ++ .../test-copper68k-010-long-move-restart.ps1 | 103 +++++ scripts/test-copper68k-synthetic.ps1 | 11 +- 10 files changed, 837 insertions(+), 13 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68010LongMoveRestartTests.cs create mode 100644 Copper68k/M68010LongMoveResumeFrame.cs create mode 100644 scripts/test-copper68k-010-long-move-restart.ps1 diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index bb8a8af9..67546f75 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -76,9 +76,26 @@ nested/alias/prefetch/trace edges 640; user A7 192; malformed private images 224 RR=0 retries only the stacked word cycle; RR=1 uses the input image or accepts a software-completed write. Earlier operand reads and register effects are never replayed. The private image contains continuation phase and completed prefetch -words, so copied frames need no side table. Long transfers, non-MOVE families, -foreign hardware internal images, RMW, external BERR and physical restart timing -remain unqualified; this is not full format-8 restart qualification. +words, so copied frames need no side table. + +Five `system-move-long-restart-*` groups add 344,192 scenarios: source 165,888; +destination 172,032; copied/nested/alias/prefetch/trace edges 5,120; user A7 512; +malformed images 640. Two fixed regressions additionally reproduce the original +missing continuation. The source/destination matrices use eight long boundaries, +all CCRs, both privilege modes and independent RR choices for each word. Each +scenario verifies the next fault, completed transfer and following sentinel. +Descending writes preserve the pending base until both words complete. Completed +source data, the original operand address and remaining phase live in the frame. +Redirecting one stacked cycle affects just that word; the next word retains its +original address, and overlapping redirected writes obey transfer order. This +address convention and the `C110` internal marker are private emulator encodings. + +Run `scripts/test-copper68k-010-long-move-restart.ps1` for source/binary/evidence +identities and independently enumerated combination weights; `-ValidateReportsOnly` +requires the same inputs and exact seven named executions. The ordinary synthetic +gate requires the five reports. Non-MOVE families, foreign hardware internal +images, RMW, external BERR and physical restart timing remain unqualified; this +is not full format-8 restart qualification or an executed external restart oracle. Reports contain per-scenario identifiers and counts for passing, mismatching, unsupported and untested outcomes. `integer-inventory.json` lists all integer diff --git a/Copper68k.Tests/Synthetic/SyntheticM68010Format8Tests.cs b/Copper68k.Tests/Synthetic/SyntheticM68010Format8Tests.cs index 651a9076..bc07158c 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68010Format8Tests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68010Format8Tests.cs @@ -85,12 +85,13 @@ public void AddressErrorInformationWordsAndReservedHoles() var faultAddress = kind == "pre-write" ? 0xab004003u : 0xab004001u; var ssw = (uint)((kind == "fetch" ? 0x2002 : 0x1001) | (write ? 0 : 0x0100) | (supervisor ? 4 : 0)); e.Write(fp + 8, ssw, 2, m.Model); e.Write(fp + 10, faultAddress, 4, m.Model); - // Input/internal words are implementation placeholders, not qualified restart state. + // Input buffers remain placeholders. Marked private word/long + // images have their own continuation gate; foreign state is unqualified. e.Write(fp + 16, write ? kind == "write-long" ? 0x8001u : 0x1234u : 0, 2, m.Model); e.Write(fp + 20, 0, 2, m.Model); e.Write(fp + 24, 0, 2, m.Model); for (uint offset = 26; offset < 58; offset += 2) { - if (kind is "read-word" or "write-word" or "movea-word" or "pre-read") + if (kind is "read-word" or "write-word" or "movea-word" or "pre-read" or "read-long" or "write-long" or "pre-write") { // Private continuation contents have their own restart gate. e.MemoryMasks[m.Model.Physical(fp + offset)] = 0; @@ -101,6 +102,17 @@ public void AddressErrorInformationWordsAndReservedHoles() m.Core.ExecuteInstruction(); var mismatch = e.Verify(m); if (mismatch != null) return mismatch; + if (m.PeekPhysical(fp + 26, 2) != 0) return "Generated internal image version must be zero"; + if (kind is "read-long" or "write-long" or "pre-write") + { + // Independent fixed encoding examples. Prefetch queue contents + // are checked functionally by the dedicated continuation suite. + var phase = kind == "read-long" ? 0u : kind == "pre-write" ? 4u : 2u; + if (m.PeekPhysical(fp + 28, 2) != 0xc110 || m.PeekPhysical(fp + 30, 2) != opcode || + m.PeekPhysical(fp + 32, 2) != (write ? 1u : 0u) || m.PeekPhysical(fp + 34, 4) != 0x1002 || + m.PeekPhysical(fp + 48, 2) != phase || m.PeekPhysical(fp + 50, 4) != (write ? 0x80011234u : 0u) || + m.PeekPhysical(fp + 54, 4) != 0xab004001) return "Incorrect private long continuation image"; + } var writes = m.Bus.Accesses.Where(a => a.Write).ToArray(); if (writes.Length != 26 || writes.Any(a => a.Width != 2 || a.Address < fp || a.Address >= fp + 58)) return "Frame must write exactly 26 information words"; diff --git a/Copper68k.Tests/Synthetic/SyntheticM68010LongMoveRestartTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68010LongMoveRestartTests.cs new file mode 100644 index 00000000..692476b3 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68010LongMoveRestartTests.cs @@ -0,0 +1,391 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticM68010LongMoveRestartTests(ITestOutputHelper output) +{ + [Fact] + public void SourceWordsContinueWithoutRepeatingPostincrement() + { + var m = Machine(0x2018); // MOVE.L (A0)+,D0 + m.Core.State.A[0] = 0x4001; + m.Start(); m.Core.ExecuteInstruction(); + Assert.Equal(0x4001u, m.Core.State.A[0]); + SupplyRead(m, 0x8001); + m.Core.ExecuteInstruction(); + Assert.Equal(0x9030u, m.Core.State.ProgramCounter); + Assert.Equal(0x4003u, m.PeekPhysical(m.Core.State.A[7] + 10, 4)); + Assert.Equal(0x4001u, m.Core.State.A[0]); + Assert.Equal(0xa55a0022u, m.Core.State.D[0]); + SupplyRead(m, 0x7fff); + m.Bus.Accesses.Clear(); m.Core.ExecuteInstruction(); + Assert.Equal(0x80017fffu, m.Core.State.D[0]); + Assert.Equal(0x4005u, m.Core.State.A[0]); + Assert.Equal(0x1002u, m.Core.State.ProgramCounter); + Assert.Equal(0x2718, m.Core.State.StatusRegister); + Assert.DoesNotContain(m.Bus.Accesses, a => a.Address is 0x4001 or 0x4003); + } + + [Fact] + public void DescendingWriteRetainsSourceAndCommitsBaseOnce() + { + var m = Machine(0x2300); // MOVE.L D0,-(A1) + m.Core.State.D[0] = 0x80017fff; m.Core.State.A[1] = 0x4105; + m.Start(); m.Core.ExecuteInstruction(); + Assert.Equal(0x4105u, m.Core.State.A[1]); + Assert.Equal(0x4103u, m.PeekPhysical(m.Core.State.A[7] + 10, 4)); + CompleteWrite(m, 0x7fff); + m.Core.State.D[0] = 0x12345678; + m.Core.ExecuteInstruction(); + Assert.Equal(0x9030u, m.Core.State.ProgramCounter); + Assert.Equal(0x4101u, m.PeekPhysical(m.Core.State.A[7] + 10, 4)); + Assert.Equal(0x8001u, m.PeekPhysical(m.Core.State.A[7] + 16, 2)); + Assert.Equal(0x4105u, m.Core.State.A[1]); + CompleteWrite(m, 0x8001); + m.Bus.Accesses.Clear(); m.Core.ExecuteInstruction(); + Assert.Equal(0x4101u, m.Core.State.A[1]); + Assert.Equal(0x80017fffu, m.PeekPhysical(0x4101, 4)); + Assert.Equal(0x12345678u, m.Core.State.D[0]); + Assert.Equal(0x1002u, m.Core.State.ProgramCounter); + Assert.DoesNotContain(m.Bus.Accesses, a => a.Address is 0x4101 or 0x4103); + } + + private static SyntheticMachine Machine(ushort opcode) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68010")); + m.Reset(0x10); m.InitializePhysical(0x1000, opcode, 2); + m.InitializePhysical(0x1002, 0x7c7b, 2); m.InitializePhysical(0x1004, 0x4e71, 2); + m.InitializePhysical(0x9030, 0x4e73, 2); m.InitializePhysical(0x9032, 0x4e71, 2); + return m; + } + + private static void SupplyRead(SyntheticMachine m, ushort value) + { + var fp = m.Core.State.A[7]; + m.InitializePhysical(fp + 8, m.PeekPhysical(fp + 8, 2) | 0x8000, 2); + m.InitializePhysical(fp + 20, value, 2); + } + + private static void CompleteWrite(SyntheticMachine m, ushort value) + { + var fp = m.Core.State.A[7]; + m.InitializePhysical(fp + 8, m.PeekPhysical(fp + 8, 2) | 0x8000, 2); + m.InitializePhysical(m.PeekPhysical(fp + 10, 4), value, 2); + } + + private static readonly uint[] Values = [0, 1, 0x7fffffff, 0x80000000, 0x80017fff, 0xffffffff, 0xffff0000, 0x0000ffff]; + private static readonly OperandForm[] MemorySources = [new(2,0), new(3,0), new(4,0), new(5,0), new(6,0), new(7,0), new(7,1), new(7,2), new(7,3)]; + private static readonly OperandForm[] MemoryDestinations = [new(2,1), new(3,1), new(4,1), new(5,1), new(6,1), new(7,0), new(7,1)]; + + [Fact, Trait("Suite", "Synthetic")] + public void SourceFaultOperandFormsValuesAndCcr() + { + var report = new CoverageBatch("68010", "system-move-long-restart-source"); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68010")); + foreach (var source in MemorySources) + foreach (var destination in MemoryDestinations.Concat([new OperandForm(0,2), new OperandForm(1,2)])) + foreach (var value in Values) + for (var rr = 0; rr < 4; rr++) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + Transfer(m, report, source, destination, value, ccr, supervisor, rr, false); + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void DestinationFaultOperandFormsValuesAndCcr() + { + var report = new CoverageBatch("68010", "system-move-long-restart-destination"); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68010")); + foreach (var source in MemorySources.Concat([new OperandForm(0,2), new OperandForm(1,2), new OperandForm(7,4)])) + foreach (var destination in MemoryDestinations) + foreach (var value in Values) + for (var rr = 0; rr < 4; rr++) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + Transfer(m, report, source, destination, value, ccr, supervisor, rr, true); + report.Complete(output); + } + + private static void Transfer(SyntheticMachine m, CoverageBatch report, OperandForm source, OperandForm destination, + uint value, int ccr, bool supervisor, int rr, bool writeFault) + { + var opcode = MoveSpecification.Encode(4, source, destination); + var id = $"68010/MOVE.L/restart/{(writeFault ? "destination" : "source")}/{source.Id}/{destination.Id}/super={supervisor}/RR={rr}/op={opcode:X4}/value={value:X8}/ccr={ccr:X2}"; + Record(report, id, () => + { + m.Reset(ccr, supervisor); + if (source.Mode == 0) m.Core.State.D[2] = value; + if (source.Mode == 1) m.Core.State.A[2] = value; + if (!writeFault && source.Mode is 2 or 3 or 4) m.Core.State.A[0] = source.Mode == 4 ? 0x4005u : 0x4001u; + if (writeFault && destination.Mode is 2 or 3 or 4) m.Core.State.A[1] = destination.Mode == 4 ? 0x4105u : 0x4101u; + var words = new List { opcode }; + var finalD = (uint[])m.Core.State.D.Clone(); var finalA = (uint[])m.Core.State.A.Clone(); + var options = new AddressOptions( + SourceDisplacement: (short)(writeFault ? 0x40 : 0x41), DestinationDisplacement: (short)(writeFault ? 0x61 : 0x60), + SourceAbsoluteWord: (ushort)(writeFault ? 0x6000 : 0x6001), DestinationAbsoluteWord: (ushort)(writeFault ? 0x6101 : 0x6100), + SourceAbsoluteLong: writeFault ? 0xab006200u : 0xab006201u, DestinationAbsoluteLong: writeFault ? 0xab006301u : 0xab006300u, + SourceIndex: new(BriefDisplacement: (sbyte)(writeFault ? 0x40 : 0x41)), + DestinationIndex: new(BriefDisplacement: (sbyte)(writeFault ? 0x61 : 0x60))); + var fixture = new AddressingFixture(m, 4, words, finalD, finalA, value, options); + var sourceAddress = source.Mode >= 2 ? fixture.Resolve(source, true) : 0; + var destinationAddress = destination.Mode >= 2 ? fixture.Resolve(destination, false) : 0; + if (source.Mode >= 2 && source != new OperandForm(7,4)) m.InitializePhysical(sourceAddress, value, 4); + var nextPc = fixture.NextPc; + m.InitializePhysical(nextPc, 0x7c7b, 2); m.InitializePhysical(nextPc + 2, 0x4e71, 2); + m.InitializePhysical(0x9030, 0x4e73, 2); m.InitializePhysical(0x9032, 0x4e71, 2); + for (var i = 0; i < words.Count; i++) m.InitializePhysical(0x1000 + (uint)i * 2, words[i], 2); + m.Start(); m.Core.ExecuteInstruction(); + if (m.Core.State.LastExceptionVector != 3 || m.Core.State.ProgramCounter != 0x9030) return "Missing initial address error"; + var fp = m.Core.State.A[7]; var savedSr = (ushort)m.PeekPhysical(fp, 2); + var originalSr = (ushort)((supervisor ? 0x2700 : 0x0700) | ccr); + if ((savedSr & 0xfff0) != (originalSr & 0xfff0) || (destination.Mode == 1 && savedSr != originalSr)) + return "Initial fault changed preserved SR bits or MOVEA flags"; + var descending = writeFault && destination.Mode == 4; + var operand = writeFault ? destinationAddress : sourceAddress; + var firstFault = descending ? operand + 2 : operand; + var secondFault = descending ? operand : operand + 2; + var phase = writeFault ? descending ? 4u : 2u : 0u; + var firstValue = (ushort)(descending ? value : value >> 16); + var secondValue = (ushort)(descending ? value >> 16 : value); + if (m.PeekPhysical(fp + 28, 2) != 0xc110 || m.PeekPhysical(fp + 30, 2) != opcode || + m.PeekPhysical(fp + 32, 2) != (writeFault ? 1u : 0u) || m.PeekPhysical(fp + 48, 2) != phase || + m.PeekPhysical(fp + 50, 4) != (writeFault ? value : 0) || m.PeekPhysical(fp + 54, 4) != operand || + m.PeekPhysical(fp + 10, 4) != firstFault) return "Incorrect first pending cycle image"; + for (var r = 0; r < 8; r++) + { + if (m.Core.State.D[r] != finalD[r]) return $"D{r} changed before first fault"; + if (r == 7) continue; + var expected = finalA[r]; + if (!writeFault && source.Mode == 3 && source.Register == r) expected -= 4; + if (destination.Mode == 3 && destination.Register == r) expected -= 4; + if (destination.Mode == 4 && destination.Register == r) expected += 4; + if (m.Core.State.A[r] != expected) return $"A{r} changed incorrectly before first fault"; + } + // Once the source has completed, handler changes cannot cause a re-read. + if (writeFault) + { + if (source.Mode == 0) { m.Core.State.D[2] ^= 0xffffffff; finalD[2] ^= 0xffffffff; } + else if (source.Mode == 1) { m.Core.State.A[2] ^= 0xffffffff; finalA[2] ^= 0xffffffff; } + else if (source != new OperandForm(7,4)) m.InitializePhysical(sourceAddress, value ^ 0xffffffff, 4); + } + var firstSoftware = (rr & 1) != 0; var secondSoftware = (rr & 2) != 0; + var firstAddress = PrepareCycle(m, fp, firstFault, firstValue, firstSoftware, writeFault, source); + var intermediate = ArchitecturalExpectation.Capture(m); + intermediate.Write(fp + 8, m.PeekPhysical(fp + 8, 2) & 0x7fff, 2, m.Model); + intermediate.Write(fp + 10, secondFault, 4, m.Model); + intermediate.Write(fp + 16, writeFault ? secondValue : 0u, 2, m.Model); + intermediate.Write(fp + 20, 0, 2, m.Model); intermediate.Write(fp + 24, 0, 2, m.Model); + intermediate.Write(fp + 48, phase + 1, 2, m.Model); + intermediate.Write(fp + 50, writeFault ? value : value & 0xffff0000, 4, m.Model); + if (writeFault && !firstSoftware) intermediate.Write(firstAddress, firstValue, 2, m.Model); + m.Bus.Accesses.Clear(); m.Core.ExecuteInstruction(); + var mismatch = intermediate.Verify(m); if (mismatch != null) return "second-fault: " + mismatch; + if (m.Core.State.LastExceptionVector != 3 || m.PeekPhysical(fp + 2, 4) != 0x1000) return "Second cycle did not suspend original instruction"; + if (!CycleCount(m, firstAddress, firstFault, firstSoftware, writeFault)) return "First word repeated or missing"; + var secondAddress = PrepareCycle(m, fp, secondFault, secondValue, secondSoftware, writeFault, source); + var e = ArchitecturalExpectation.Capture(m); + e.A[7] = fp + 58; SyntheticSystemTests.ApplyStatus(m, e, savedSr); + for (var r = 0; r < 7; r++) e.A[r] = finalA[r]; + finalD.CopyTo(e.D, 0); + if (!writeFault && destination.Mode == 0) e.D[destination.Register] = value; + if (!writeFault && destination.Mode == 1) e.A[destination.Register] = value; + if (destination.Mode != 1) e.Sr = SyntheticExecution.MoveFlags(e.Sr, value, 4); + e.Pc = nextPc; + if (writeFault && !secondSoftware) e.Write(secondAddress, secondValue, 2, m.Model); + if (!writeFault && destination.Mode >= 2) e.Write(destinationAddress, value, 4, m.Model); + m.Bus.Accesses.Clear(); m.Core.ExecuteInstruction(); + mismatch = e.Verify(m); if (mismatch != null) return "completion: " + mismatch; + if (!CycleCount(m, secondAddress, secondFault, secondSoftware, writeFault)) return "Second word repeated or missing"; + if (writeFault && source.Mode >= 2 && source != new OperandForm(7,4) && m.Bus.Accesses.Any(a => a.Address == m.Model.Physical(sourceAddress))) + return "Completed source was re-read"; + m.Core.ExecuteInstruction(); e.D[6] = 123; e.Pc += 2; e.Sr = (ushort)(e.Sr & 0xfff0); + return e.Verify(m); + }); + } + + private static uint PrepareCycle(SyntheticMachine m, uint fp, uint fault, ushort value, bool software, bool write, OperandForm source) + { + if (software) m.InitializePhysical(fp + 8, m.PeekPhysical(fp + 8, 2) | 0x8000, 2); + else m.InitializePhysical(fp + 10, fault + 1, 4); + var address = software ? fault : fault + 1; + if (write) { if (software) m.InitializePhysical(address, value, 2); } + else + { + var instruction = source.Mode == 7 && source.Register is 2 or 3; + m.InitializePhysical(fp + 20, instruction ? (uint)(value ^ 0xffff) : value, 2); + m.InitializePhysical(fp + 24, instruction ? value : (uint)(value ^ 0xffff), 2); + if (!software) m.InitializePhysical(address, value, 2); + } + return address; + } + + private static bool CycleCount(SyntheticMachine m, uint address, uint fault, bool software, bool write) + => !m.Bus.Accesses.Any(a => a.Address == m.Model.Physical(fault)) && + m.Bus.Accesses.Count(a => a.Address == m.Model.Physical(address) && a.Width == 2 && a.Write == write) == (software ? 0 : 1); + + [Fact, Trait("Suite", "Synthetic")] + public void CopiedNestedAliasedPrefetchedAndTracedContinuations() + { + var report = new CoverageBatch("68010", "system-move-long-restart-edges"); + foreach (var kind in new[] { "copied", "nested", "alias", "prefetched", "trace" }) + for (var rr = 0; rr < 16; rr++) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + var id = $"68010/MOVE.L/restart/{kind}/super={supervisor}/RR={rr}/op=20/ccr={ccr:X2}"; + Record(report, id, () => + { + var memory = kind is "nested" or "alias"; + var opcode = kind == "alias" ? (ushort)0x20d8 : kind == "nested" ? (ushort)0x22d8 : (ushort)0x2418; + var m = Machine(opcode); + m.Core.State.StatusRegister = (ushort)((supervisor ? 0x2700 : 0x0700) | ccr | (kind == "trace" ? 0x8000 : 0)); + m.Core.State.SetActiveStackPointer(supervisor ? 0x4700u : 0x7800u); + m.Core.State.A[0] = 0x4001; m.Core.State.A[1] = 0x4101; + m.Start(); m.Core.ExecuteInstruction(); + var fp = m.Core.State.A[7]; + if (kind == "copied") + { + for (uint offset = 0; offset < 58; offset += 2) m.InitializePhysical(0x6800 + offset, m.PeekPhysical(fp + offset, 2), 2); + // Execute an unrelated fault in the handler before returning + // to the copied image. Its continuation cannot overwrite this one. + m.InitializePhysical(0x5000, 0x2011, 2); m.InitializePhysical(0x5002, 0x4e71, 2); + m.Core.SwitchTaskContext(m.Core.State); // discard current prefetch through public API + m.Core.State.ProgramCounter = 0x5000; + m.Core.ExecuteInstruction(); + if (m.Core.State.LastExceptionVector != 3 || m.PeekPhysical(m.Core.State.A[7] + 10, 4) != 0x4101) return "Nested handler fault fixture failed"; + fp = 0x6800; m.Core.State.SetActiveStackPointer(fp); + } + var pending = memory ? 4 : 2; + var destination = kind == "alias" ? 0x4005u : 0x4101u; + var writtenBytes = new Dictionary(); + for (var phase = 0; phase < pending; phase++) + { + var writing = phase >= 2; var low = (phase & 1) != 0; + var fault = (writing ? destination : 0x4001u) + (low ? 2u : 0); + if (m.Core.State.ProgramCounter != 0x9030 || m.PeekPhysical(fp + 10, 4) != fault) return $"Pending cycle {phase} address differs"; + var value = low ? (ushort)0x7fff : (ushort)0x8001; + var software = (rr & (1 << phase)) != 0; + var address = PrepareCycle(m, fp, fault, value, software, writing, new(3,0)); + if (writing) + { + writtenBytes[m.Model.Physical(address)] = (byte)(value >> 8); + writtenBytes[m.Model.Physical(address + 1)] = (byte)value; + } + if (kind == "prefetched") m.InitializePhysical(0x1002, 0x7cff, 2); + var e = ArchitecturalExpectation.Capture(m); + var sr = (ushort)m.PeekPhysical(fp, 2); + if (phase == pending - 1) + { + e.A[7] = fp + 58; SyntheticSystemTests.ApplyStatus(m, e, sr); + e.A[0] = kind == "alias" ? 0x4009u : 0x4005u; + if (kind == "nested") e.A[1] = 0x4105; + if (!memory) e.D[2] = 0x80017fff; + e.Sr = SyntheticExecution.MoveFlags(e.Sr, 0x80017fff, 4); e.Pc = 0x1002; + if (writing && !software) e.Write(address, value, 2, m.Model); + if (kind == "trace") SyntheticExecution.ExpectException(m, e, 9, 0x1002); + m.Bus.Accesses.Clear(); m.Core.ExecuteInstruction(); + var mismatch = e.Verify(m); if (mismatch != null) return "completion: " + mismatch; + if (!CycleCount(m, address, fault, software, writing)) return "Final cycle count differs"; + if (kind != "trace") + { + m.Core.ExecuteInstruction(); e.D[6] = 123; e.Pc += 2; e.Sr = (ushort)(e.Sr & 0xfff0); + mismatch = e.Verify(m); if (mismatch != null) return "sentinel: " + mismatch; + } + } + else + { + m.Bus.Accesses.Clear(); m.Core.ExecuteInstruction(); fp = m.Core.State.A[7]; + if (!CycleCount(m, address, fault, software, writing)) return $"Cycle {phase} repeated or missing"; + if (m.Core.State.A[0] != (phase < 1 ? 0x4001u : 0x4005u)) return "Source postincrement repeated or committed early"; + if (m.Core.State.A[1] != 0x4101 || m.Core.State.D[2] != 0xa55a0222) return "Destination changed before its complete transfer"; + if (phase == 1 && memory && m.PeekPhysical(fp + 50, 4) != 0x80017fff) return "Completed source lost before destination fault"; + } + } + // Redirecting just one word can overlap the other word. The later + // transfer must win byte by byte, in the declared transfer order. + if (writtenBytes.Any(pair => m.Bus.Peek(pair.Key) != pair.Value)) return "Destination byte contents differ"; + return null; + }); + } + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void UserStackLongOperandsPreserveSupervisorBank() + { + var report = new CoverageBatch("68010", "system-move-long-restart-a7"); + foreach (var kind in new[] { "source-post", "source-pre", "dest-post", "dest-pre" }) + for (var rr = 0; rr < 4; rr++) + for (var ccr = 0; ccr < 32; ccr++) + Record(report, $"68010/MOVE.L/restart/A7/{kind}/RR={rr}/op=20/ccr={ccr:X2}", () => + { + var writing = kind.StartsWith("dest"); var pre = kind.EndsWith("pre"); + var source = writing ? new OperandForm(0,0) : new OperandForm(pre ? 4 : 3,7); + var destination = writing ? new OperandForm(pre ? 4 : 3,7) : new OperandForm(0,0); + var m = Machine(MoveSpecification.Encode(4, source, destination)); + m.Core.State.StatusRegister = (ushort)(0x0700 | ccr); + m.Core.State.SetActiveStackPointer(pre ? 0x7805u : 0x7801u); m.Core.State.D[0] = 0x80017fff; + m.Start(); m.Core.ExecuteInstruction(); + var fp = m.Core.State.A[7]; + if (fp != 0x7fc6 || m.Core.State.UserStackPointer != (pre && !writing ? 0x7801u : pre ? 0x7805u : 0x7801u)) return "Wrong stack bank or early A7 update"; + for (var phase = 0; phase < 2; phase++) + { + var low = writing && pre ? phase == 0 : phase == 1; + var fault = 0x7801u + (low ? 2u : 0); + if (m.PeekPhysical(fp + 10, 4) != fault) return "Wrong A7 word address"; + PrepareCycle(m, fp, fault, low ? (ushort)0x7fff : (ushort)0x8001, (rr & (1 << phase)) != 0, writing, source); + var e = ArchitecturalExpectation.Capture(m); + if (phase == 1) + { + e.A[7] = fp + 58; SyntheticSystemTests.ApplyStatus(m, e, (ushort)m.PeekPhysical(fp, 2)); + e.A[7] = pre ? 0x7801u : 0x7805u; + e.D[0] = 0x80017fff; e.Sr = SyntheticExecution.MoveFlags(e.Sr, 0x80017fff, 4); e.Pc = 0x1002; + if (writing && (rr & 2) == 0) e.Write(fault + 1, low ? 0x7fffu : 0x8001u, 2, m.Model); + } + m.Core.ExecuteInstruction(); + if (phase == 1) return e.Verify(m); + if (m.Core.State.A[7] != fp || m.Core.State.ProgramCounter != 0x9030) return "A7 first word did not suspend"; + } + return "Empty A7 continuation"; + }); + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void InvalidPrivateContinuationPreservesFrameBeforeFormatError() + { + var report = new CoverageBatch("68010", "system-move-long-restart-invalid"); + foreach (var writing in new[] { false, true }) + foreach (var corruption in new[] { "version", "opcode", "direction", "phase", "pc", "queue-address", "queue-count", "operand-parity", "value", "descending" }) + for (var ccr = 0; ccr < 32; ccr++) + Record(report, $"68010/MOVE.L/restart/invalid/{corruption}/write={writing}/op=20/ccr={ccr:X2}", () => + { + var m = Machine(writing ? (ushort)0x2280 : (ushort)0x2010); + m.Core.State.StatusRegister = (ushort)(0x2700 | ccr); m.Core.State.A[0] = 0x4001; m.Core.State.A[1] = 0x4101; + m.Start(); m.Core.ExecuteInstruction(); var fp = m.Core.State.A[7]; + var (offset, value) = corruption switch + { + "version" => (26u, 0x0400u), "opcode" => (30u, 0x4e71u), "direction" => (32u, writing ? 0u : 1u), + "phase" => (48u, 6u), "pc" => (36u, 0x1003u), "queue-address" => (40u, 0x1003u), + "queue-count" => (46u, 3u), "operand-parity" => (56u, 0x4000u), + "value" => writing ? (48u, 0u) : (52u, 1u), _ => (48u, writing ? 4u : 2u) + }; + m.InitializePhysical(fp + offset, value, 2); + var e = ArchitecturalExpectation.Capture(m); SyntheticExecution.ExpectException(m, e, 14, 0x9030); + m.Bus.Accesses.Clear(); m.Core.ExecuteInstruction(); + if (m.Bus.Accesses.Any(a => a.Address is 0x4000 or 0x4001 or 0x4101)) return "Invalid image executed operand cycle"; + return e.Verify(m); + }); + report.Complete(output); + } + + private static void Record(CoverageBatch report, string id, Func run) + { + try { var mismatch = run(); report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException) + { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); } + } +} diff --git a/Copper68k/M68010Interpreter.cs b/Copper68k/M68010Interpreter.cs index b886d828..545981fd 100644 --- a/Copper68k/M68010Interpreter.cs +++ b/Copper68k/M68010Interpreter.cs @@ -220,7 +220,7 @@ protected override bool IsSupportedRteFrameFormat(ushort format) // MC68000UM 6.4: validate before changing SP, then probe the final // word before loading the rest. Unmarked images retain structural-only - // compatibility; marked images carry the private word-MOVE continuation. + // compatibility; marked images carry private word/long-MOVE continuations. protected override bool ValidateRteFrame(ushort format, uint framePointer) { _rteInternalWords = null; @@ -246,20 +246,24 @@ protected override bool ValidateRteFrame(ushort format, uint framePointer) } } if (words[1] == M68010WordMoveResumeFrame.Marker && !M68010WordMoveResumeFrame.IsValid(words)) return false; + if (words[1] == M68010LongMoveResumeFrame.Marker && !M68010LongMoveResumeFrame.IsValid(words)) return false; _rteInternalWords = words; return true; } protected override bool TryResumeRteFrame(ushort format, uint framePointer, ushort statusRegister, uint programCounter) { - if ((format & 0xF000) != 0x8000 || _rteInternalWords is not { } words || words[1] != M68010WordMoveResumeFrame.Marker) return false; + if ((format & 0xF000) != 0x8000 || _rteInternalWords is not { } words || + words[1] is not (M68010WordMoveResumeFrame.Marker or M68010LongMoveResumeFrame.Marker)) return false; var specialStatus = _rteSpecialStatus; var faultAddress = _rteFaultAddress; var output = _rteDataOutput; var input = _rteDataInput; var instructionInput = _rteInstructionInput; _rteInternalWords = null; State.SetActiveStackPointer(framePointer + 58); State.StatusRegister = statusRegister; AddInstructionCycles(20); // Retained RTE policy; physical restart timing is unqualified. - ResumeM68010WordMove(words, programCounter, faultAddress, specialStatus, output, input, instructionInput); + if (words[1] == M68010LongMoveResumeFrame.Marker) + ResumeM68010LongMove(words, programCounter, faultAddress, specialStatus, output, input, instructionInput); + else ResumeM68010WordMove(words, programCounter, faultAddress, specialStatus, output, input, instructionInput); return true; } @@ -281,13 +285,14 @@ protected override bool TryHandleModelSpecificAddressError( bool useDataAccessStackedProgramCounter, ushort dataOutput) { - var internalWords = CaptureM68010WordMoveResumeFrame(isWrite, accessKind, useDataAccessStackedProgramCounter); + var internalWords = CaptureM68010LongMoveResumeFrame(isWrite, accessKind, useDataAccessStackedProgramCounter) + ?? CaptureM68010WordMoveResumeFrame(isWrite, accessKind, useDataAccessStackedProgramCounter); var stackedProgramCounter = State.LastInstructionProgramCounter; var savedStatusRegister = State.StatusRegister; State.RecordException(3, stackedProgramCounter, savedStatusRegister); State.StatusRegister = (ushort)((savedStatusRegister | M68kCpuState.Supervisor) & ~M68kCpuState.Trace); // Figure 6-8: 58 bytes, 26 information words, three unwritten - // reserved words. Only marked word-MOVE images encode continuation; + // reserved words. Only marked word/long-MOVE images encode continuation; // other internal/input state remains structurally unqualified. for (var i = 15; i >= 0; i--) PushWord(internalWords?[i] ?? 0); PushWord(0); // instruction input buffer (unqualified) diff --git a/Copper68k/M68010LongMoveResumeFrame.cs b/Copper68k/M68010LongMoveResumeFrame.cs new file mode 100644 index 00000000..ffc4c3bf --- /dev/null +++ b/Copper68k/M68010LongMoveResumeFrame.cs @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2026 Ilkka Lehtoranta + * SPDX-License-Identifier: MIT + */ + +namespace Copper68k; + +// Private format-8 image, not a Motorola silicon encoding. The original +// operand address remains fixed when a handler redirects one pending word. +internal static class M68010LongMoveResumeFrame +{ + internal const ushort Marker = 0xc110; + internal static bool IsOpcodeSupported(ushort opcode, bool write) + => (opcode & 0xf000) == 0x2000 && + M68010WordMoveResumeFrame.IsOpcodeSupported((ushort)((opcode & 0x0fff) | 0x3000), write); + + internal static bool IsValid(ushort[] words) + { + if (words.Length != 16 || words[0] != 0 || words[1] != Marker || words[3] > 1 || + !IsOpcodeSupported(words[2], words[3] != 0) || words[10] > 2 || words[11] > 5 || + (words[3] == 0) != (words[11] < 2) || + (M68010WordMoveResumeFrame.Long(words, 4) & 1) != 0 || + (M68010WordMoveResumeFrame.Long(words, 6) & 1) != 0 || + (words[10] != 0 && M68010WordMoveResumeFrame.Long(words, 6) != M68010WordMoveResumeFrame.Long(words, 4)) || + (M68010WordMoveResumeFrame.Long(words, 14) & 1) == 0) return false; + if (words[11] == 0 && (words[12] != 0 || words[13] != 0)) return false; + if (words[11] == 1 && words[13] != 0) return false; + var predecrement = ((words[2] >> 6) & 7) == 4; + return words[11] < 2 || predecrement == (words[11] >= 4); + } +} diff --git a/Copper68k/M68kCore.cs b/Copper68k/M68kCore.cs index 02c15a9b..f984db05 100644 --- a/Copper68k/M68kCore.cs +++ b/Copper68k/M68kCore.cs @@ -9189,6 +9189,92 @@ protected virtual bool IsSupportedRteFrameFormat(ushort format) protected virtual bool TryResumeRteFrame(ushort format, uint framePointer, ushort statusRegister, uint programCounter) => false; + private bool _m68010LongMovePending; + private ushort _m68010LongMovePhase; + private uint _m68010LongMoveValue, _m68010LongMoveAddress; + + private void PrepareM68010LongMoveFault(uint address, uint value, ushort phase) + { + if (!UsesFormatWordExceptionFrames || _useM68020BriefIndexedAddressing || + !M68010LongMoveResumeFrame.IsOpcodeSupported(State.LastOpcode, phase >= 2)) return; + _m68010LongMovePending = true; _m68010LongMovePhase = phase; + _m68010LongMoveValue = value; _m68010LongMoveAddress = address; + } + + protected ushort[]? CaptureM68010LongMoveResumeFrame(bool isWrite, M68kBusAccessKind accessKind, bool dataOperand) + { + if (!_m68010LongMovePending || (isWrite != (_m68010LongMovePhase >= 2)) || + !M68010LongMoveResumeFrame.IsOpcodeSupported(State.LastOpcode, isWrite) || + (accessKind == M68kBusAccessKind.CpuInstructionFetch && !dataOperand)) return null; + var words = new ushort[16]; words[1] = M68010LongMoveResumeFrame.Marker; + words[2] = State.LastOpcode; words[3] = (ushort)(isWrite ? 1 : 0); + M68010WordMoveResumeFrame.Long(words, 4, State.ProgramCounter); + M68010WordMoveResumeFrame.Long(words, 6, _prefetchAddress); + words[8] = _prefetchWord0; words[9] = _prefetchWord1; words[10] = (ushort)_prefetchCount; + words[11] = _m68010LongMovePhase; + M68010WordMoveResumeFrame.Long(words, 12, _m68010LongMoveValue); + M68010WordMoveResumeFrame.Long(words, 14, _m68010LongMoveAddress); + return words; + } + + // Resume pending word cycles from the frame. No decoder replay, source + // re-read or repeated EA side effects; successful long paths stay intact. + protected void ResumeM68010LongMove(ushort[] words, uint instructionPc, uint faultAddress, + ushort specialStatus, ushort dataOutput, ushort dataInput, ushort instructionInput) + { + var opcode = words[2]; + State.LastOpcode = opcode; State.LastInstructionProgramCounter = instructionPc; + _activeInstructionProgramCounter = instructionPc; + State.ProgramCounter = M68010WordMoveResumeFrame.Long(words, 4); + FlushPrefetch(); + _prefetchAddress = M68010WordMoveResumeFrame.Long(words, 6); + _prefetchWord0 = words[8]; _prefetchWord1 = words[9]; _prefetchCount = words[10]; + _prefetchCompletedCycle0 = _prefetchCompletedCycle1 = State.Cycles; + _instructionTracePending = State.GetFlag(M68kCpuState.Trace); + _dataAccessStackedProgramCounter = instructionPc; + _dataReadFaultAccessKind = (specialStatus & 0x2000) != 0 ? M68kBusAccessKind.CpuInstructionFetch : M68kBusAccessKind.CpuDataRead; + _m68010LongMovePending = true; _m68010LongMovePhase = words[11]; + _m68010LongMoveValue = M68010WordMoveResumeFrame.Long(words, 12); + _m68010LongMoveAddress = M68010WordMoveResumeFrame.Long(words, 14); + var softwareCompleted = (specialStatus & 0x8000) != 0; + var destinationMode = (opcode >> 6) & 7; var destinationRegister = (opcode >> 9) & 7; + if (_m68010LongMovePhase >= 2) + { + if (!softwareCompleted) WriteWord(faultAddress, dataOutput); + if (_m68010LongMovePhase is 2 or 4) + { + var descending = _m68010LongMovePhase == 4; + _m68010LongMovePhase++; + WriteWord(descending ? _m68010LongMoveAddress : unchecked(_m68010LongMoveAddress + 2), + (ushort)(descending ? _m68010LongMoveValue >> 16 : _m68010LongMoveValue)); + } + _m68010LongMovePending = false; + if (destinationMode == 3) SetAddressRegister(destinationRegister, State.A[destinationRegister] + 4); + if (destinationMode == 4) SetAddressRegister(destinationRegister, _m68010LongMoveAddress); + SetLogicFlags(_m68010LongMoveValue, M68kOperandSize.Long); + PrefetchFallthroughAfterMemoryWriteback(); + return; + } + + var pending = softwareCompleted ? (specialStatus & 0x1000) != 0 ? dataInput : instructionInput : ReadWord(faultAddress); + if (_m68010LongMovePhase == 0) + { + _m68010LongMoveValue = (uint)pending << 16; + _m68010LongMovePhase = 1; + pending = ReadWord(unchecked(_m68010LongMoveAddress + 2)); + } + var value = _m68010LongMoveValue | pending; + _m68010LongMovePending = false; + if (((opcode >> 3) & 7) == 3) SetAddressRegister(opcode & 7, State.A[opcode & 7] + 4); + var memoryDestination = destinationMode >= 2; + if (!memoryDestination || destinationMode == 4) PrefetchFallthroughAfterMoveSourceRead(); + var destination = ResolvePlannedEaWithoutPrefetchTopUp(destinationMode, destinationRegister, M68kOperandSize.Long, write: true); + if (destinationMode != 1) SetLogicFlags(value, M68kOperandSize.Long); + if (memoryDestination && MoveDestinationHasExtensionWord(destinationMode, destinationRegister)) PrefetchNextOpcodeBeforeMoveMemoryWriteback(); + WritePlannedEaValue(in destination, value); + if (memoryDestination && destinationMode != 4) PrefetchFallthroughAfterMemoryWriteback(); + } + protected ushort[]? CaptureM68010WordMoveResumeFrame(bool isWrite, M68kBusAccessKind accessKind, bool dataOperand) { if (!M68010WordMoveResumeFrame.IsOpcodeSupported(State.LastOpcode, isWrite) || @@ -14033,6 +14119,7 @@ protected uint ReadLong(uint address) } if (!_useM68020BriefIndexedAddressing && (address & 1) != 0) { + PrepareM68010LongMoveFault(address, 0, 0); ThrowOddAddressAccess(address, isWrite: false, _dataReadFaultAccessKind, useDataAccessStackedProgramCounter: true); } @@ -14149,6 +14236,7 @@ private void WriteLong(uint address, uint value) } if (!_useM68020BriefIndexedAddressing && (address & 1) != 0) { + PrepareM68010LongMoveFault(address, value, 2); ThrowOddAddressAccess(address, isWrite: true, M68kBusAccessKind.CpuDataWrite, dataOutput: (ushort)(value >> 16)); } @@ -14174,6 +14262,7 @@ private void WriteLong(uint address, uint value) private void WriteLongDescending(uint address, uint value) { + if ((address & 1) != 0) PrepareM68010LongMoveFault(address, value, 4); WriteWord(address + 2, (ushort)value); WriteWord(address, (ushort)(value >> 16)); } @@ -14541,6 +14630,7 @@ private void AllowInstructionRetireAtCycleFloor() [MethodImpl(MethodImplOptions.AggressiveInlining)] private void BeginInstructionCycleFloor(long startCycle) { + _m68010LongMovePending = false; // MC68000 UM 6.3.8: sample before executing, not from the resulting SR. _instructionTracePending = ArchitecturalTraceEnabled && (State.StatusRegister & M68kCpuState.Trace) != 0; // Reaching the next instruction proves that any previous DBcc diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 323e8d55..53c5ef26 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -6269,3 +6269,135 @@ package published. Invalid-RTE flag architecture, long/RMW/foreign 010 restorati 020/030 advanced frames, full CM/fault lifetime, STOP/PCR discrepancies, broader independent model audits and consolidation remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +### 68010 private long MOVE/MOVEA continuation — 2026-10-06 + +[MC68000UM](https://www.nxp.com/docs/en/reference-manual/MC68000UM.pdf) +6.3.9.2, 6.3.10 and 6.4 require continuation from restored internal state rather +than decoding the instruction again. The transfer buffers in figure 6-8 are +16 bits. RR chooses a processor rerun or software-completed transfer, with +DF/IF selecting the read buffer. This supports per-word continuation of a long +operand. It does not specify the silicon's internal word encoding. Locked RMW +has separate whole-cycle semantics; TAS is byte sized and cannot be qualified +using alignment faults. External non-MMU BERR signaling remains unavailable +through the current public bus contract. + +The new `M68010LongMoveResumeFrame` image is explicitly **private**, alongside +the existing `C010` word image. Its `C110` marker identifies these internal words: + +| Internal words | Meaning | +| --- | --- | +| 0–3 | Version zero, marker, original opcode, read/write direction | +| 4–10 | Extension PC, prefetch address, two prefetched words, queue count | +| 11 | Phase 0/1 source high/low; 2/3 ascending write high/low; 4/5 descending write low/high | +| 12–13 | Accumulated source input or complete source output | +| 14–15 | Original logical operand base | + +The stacked fault address identifies only the pending word. A handler redirect +does not change the subsequent word's original address. Mixed RR choices can +therefore produce overlapping destinations; the later word wins byte by byte. +This address convention is a repository choice, not inferred silicon behavior. +Only generated odd-alignment MOVE/MOVEA images are promoted. Unsupported private +images reject before frame consumption; foreign unmarked images retain existing +structural compatibility and remain unqualified for continuation. + +The core records a long read fault before the first word and an ascending or +descending write fault before its first word. RTE loads the marked image and +continues word cycles directly. It never repeats source reads, operand decoding +or already committed EA effects. A successful source transfer increments once; +a successful destination transfer commits postincrement or delayed predecrement +once. MOVEA preserves flags; MOVE uses the completed 32-bit source. New capture +hooks run only on odd long accesses. Successful long paths retain their original +access widths/order and timing policy. The pending context clears before further +prefetch/operand work or the next instruction. No public API changes. + +`SyntheticM68010LongMoveRestartTests` adds five ordinary reports: + +| Group | Scenarios | Architectural combinations | +| --- | ---: | ---: | +| Source faults | 165,888 | 648 | +| Destination faults | 172,032 | 672 | +| Copied/nested/alias/prefetch/trace | 5,120 | 160 | +| User A7 | 512 | 16 | +| Invalid private images | 640 | 20 | +| Total | **344,192** | **1,516** | + +Counts describe multi-phase scenarios, not individual bus cycles or xUnit test +counts. Two fixed regressions additionally demonstrate the old missing source +continuation and descending-write continuation. Both failed at the first RTE +before the fix (`artifacts/m6-010-long-restart-before/before.trx`). Seven named +tests now pass without skips. Canonical matrices use nine memory sources, seven +memory destinations, legal register/immediate alternatives, eight long values, +32 CCRs, both privilege states and four independent two-word RR combinations. +They check initial register effects, the next complete frame/register/memory +state, completed instruction state and a following MOVEQ sentinel. Completed +sources are changed by the handler to expose accidental re-reads. Existing +initial fault-flag/PC policy is retained; no new independent silicon oracle is +claimed for initial fault flags or instruction-space bus signaling. + +```powershell +./scripts/test-copper68k-010-long-move-restart.ps1 -OutputDirectory artifacts/010-long-move-restart +./scripts/test-copper68k-010-long-move-restart.ps1 -ValidateReportsOnly -OutputDirectory artifacts/010-long-move-restart +``` + +Accepted scoped evidence is `artifacts/m6-010-long-restart-qualified-current/`. +Source identity is frozen before execution and rechecked afterward. CPU, test +assembly and every report/TRX identity must match. Validation independently +enumerates all combinations and weights and requires the exact seven methods. +Fresh and frozen validations pass. All fourteen controls in +`artifacts/m6-010-long-restart-controls-current/controls.json` reject their intended +identity, selection or report error; altered report hashes are refreshed so +semantic errors cannot hide behind hash rejection. Original evidence is intact. + +Register destinations start with distinct values when the source is memory; +the fixture cannot conceal an omitted register write. Preserved SR bits and +MOVEA flags are checked against the independent initial SR. The retained +format-8 structural test now checks fixed `C110` image examples instead of +expecting obsolete zero internal words, while retaining all header, reserved-hole, +surrounding-memory and bus-write checks. + +The retained selection passes 21 tests without skips: 133,152 scenarios / eight +reports plus thirteen fixed examples (`artifacts/m6-010-long-restart-retained-final/`). +Six isolated production mutations detect disabled capture, lost source high word, +repeated source increment, wrong remaining address, premature/wrong descending +base commit and lost completed output. Both fixed regressions remain unchanged +after fixture strengthening; each mutation restores source byte-for-byte. +Evidence: `artifacts/m6-010-long-restart-mutations/mutations.json`. + +Pinned external semantic audits pass: SingleStepTests 312,500 cases / 125 files +at `64b253116a3de04aaac4346c43680960dc9b67e5`, and Musashi 536 passing / +88 excluded combinations at `72c1d74800f3087b45a0c1a7342601bbed898881`. +Accepted evidence is `artifacts/m6-010-long-restart-external-qualified/`; it uses +the same production CPU and the earlier test adapter, recorded separately. +These audits do not supply an external long-continuation oracle. AHX retains +18 passing tests without skips (`artifacts/m6-010-long-restart-ahx/`). + +The first full CPU run records 5,128 passing, one failing and nineteen skipped +tests (`artifacts/m6-010-long-restart-full/`). Its sole failure was the structural +test's obsolete zero-internal-image expectation described above. This failed +run remains intact and is not accepted as a passing semantic gate. The corrected +full rerun, including nine pinned WinUAE selections, is still pending at this +commit (`artifacts/m6-010-long-restart-full-final/`). Discovery lists 5,116 test +descriptors; one theory expands into 33 runtime rows, giving 5,148 executions. +The ordinary gate now requires 85,535,254 scenarios / 685 reports. + +An isolated CopperScreen consumer at `d9beae8b88be24032221e3482942a249c03c27d3` +passes its Release build, host tests (149 passing / six optional skips), disk +tests (74), separate engine diagnostics (1,080), and three native boot/persistence +replays without native skips. Private unpublished package +`1.5.2-synthetic-dev.64` SHA256 is +`b84e9328c6a74618aaa9d0ed54c13e89019b970496029cd5029bbf9439453bb7`. +Its CPU and all four loaded consumer assemblies match +`0145566d23551d1dd8793784d2d83b5fdbe9a9ad2db6c9199012b6f9af80308a`; +the final scoped test adapter is +`180db7219a99097c9c24d7cae0cff17f5b9aa27798d241861ece75d59f1e8367`. +Consumer evidence is under the isolated checkout's +`artifacts/010-long-restart-validation/` and +`artifacts/010-long-restart-identities.json`. Root CopperScreen changes and its +dependency pin remain untouched. This is correctness evidence, not throughput +or physical timing qualification. + +No regression is retired, public package published, hardware timing qualified, +or full format-8 restart claimed. Non-MOVE/foreign/RMW/external-BERR restoration, +rejected-RTE CCR architecture and all other reference/consolidation gaps remain +required. Milestone 6 stays **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 872e0050..b9ac6153 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2380,3 +2380,41 @@ and packages remain unchanged. No regression is retired. Rejected RTE CCR architecture, long/RMW/foreign format8 continuation, full exception/trace entry, and all other reference/consolidation requirements remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +### 68010 long MOVE/MOVEA continuation — 2026-10-06 + +Generated private format-8 images now continue long transfers as two pending +word cycles. A completed source read is not repeated; postincrement commits +after both source words, and descending destination predecrement commits after +both writes. Input/output buffers and independent RR choices apply per word. +The frame retains accumulated input, full output, original operand address, +extension PC and completed prefetch. Redirecting one pending word leaves the +other word's original address intact; this is an explicit private convention. +Copied/nested images need no side table. Normal successful transfer ordering, +public API and existing timing policy remain unchanged. + +The two bounded failures reproduce before the fix. Five ordinary groups add +344,192 scenarios / 1,516 combinations: source 165,888; destination 172,032; +copied/nested/alias/prefetch/trace 5,120; user A7 512; malformed images 640. +All seven named tests pass without skips. Source and destination fixtures are +independent of production decoding, including extension words, 24-bit physical +addresses, final flags, saved values, untouched registers and surrounding memory. +Each canonical scenario checks the second fault, completed transfer and sentinel. +The dedicated command independently checks identities, exact selections, keys +and weights; fresh and frozen validations pass. Fourteen integrity controls +reject changed identities, empty/wrong selections and redistributed coverage. + +The ordinary semantic gate now requires **85,535,254 scenarios / 685 reports**. +These counts include the new requirements, not proof of pending broad execution. +The retained 68010 selection passes 21 tests without skips; six production +mutations detect the targeted continuation defects. Pinned SingleStepTests and +Musashi semantic audits pass. The isolated unpublished `.64` consumer passes +its Release build, host/disk/engine tests and all three native replays. The first +full CPU run failed only an obsolete structural-image expectation; that test +now checks explicit private-image examples and passes its retained selection. +The corrected full rerun is pending at this commit. See the reference document +for identities, exclusions and preserved failed evidence. No regression is +retired or public package published. Long non-MOVE, foreign hardware images, RMW and external bus errors +remain required, along with all other milestone-6 reference/consolidation gaps. +This private continuation is not hardware-internal or physical timing +qualification. Milestone 6 stays **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-010-long-move-restart.ps1 b/scripts/test-copper68k-010-long-move-restart.ps1 new file mode 100644 index 00000000..f0c75b0f --- /dev/null +++ b/scripts/test-copper68k-010-long-move-restart.ps1 @@ -0,0 +1,103 @@ +[CmdletBinding()] +param([switch]$ValidateReportsOnly,[string]$OutputDirectory='artifacts/010-long-move-restart') +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +$groups=[ordered]@{'system-move-long-restart-source'=165888;'system-move-long-restart-destination'=172032; + 'system-move-long-restart-edges'=5120;'system-move-long-restart-a7'=512;'system-move-long-restart-invalid'=640} +function Identity($Path){ + $file=Join-Path $repo $Path + if(-not(Test-Path -LiteralPath $file -PathType Leaf)){throw "Missing 010 long MOVE input: $Path"} + return @{path=$Path;sha256=(Get-FileHash -LiteralPath $file -Algorithm SHA256).Hash} +} +function Add-Key($Keys,[string]$Key,[int]$Weight){if(-not $Keys.TryAdd($Key,$Weight)){throw "Duplicate 010 long MOVE combination: $Key"}} +function Expected-Keys($Group){ + $keys=[Collections.Generic.Dictionary[string,int]]::new([StringComparer]::Ordinal) + switch($Group){ + {$_ -in @('system-move-long-restart-source','system-move-long-restart-destination')}{ + $writing=$Group.EndsWith('destination') + $sources=@('(A0)','(A0)+','-(A0)','d16(A0)','index(A0)','abs.w','abs.l','d16(PC)','index(PC)') + $destinations=@('(A1)','(A1)+','-(A1)','d16(A1)','index(A1)','abs.w','abs.l') + if($writing){$sources+=@('D2','A2','immediate')}else{$destinations+=@('D2','A2')} + foreach($source in $sources){foreach($destination in $destinations){foreach($rr in 0..3){foreach($supervisor in @($false,$true)){ + Add-Key $keys ("68010/MOVE.L/restart/"+$(if($writing){'destination'}else{'source'})+"/$source/$destination/super=$supervisor/RR=$rr") 256 + }}}} + } + 'system-move-long-restart-edges'{ + foreach($kind in @('copied','nested','alias','prefetched','trace')){foreach($rr in 0..15){foreach($supervisor in @($false,$true)){ + Add-Key $keys "68010/MOVE.L/restart/$kind/super=$supervisor/RR=$rr" 32 + }}} + } + 'system-move-long-restart-a7'{ + foreach($kind in @('source-post','source-pre','dest-post','dest-pre')){foreach($rr in 0..3){Add-Key $keys "68010/MOVE.L/restart/A7/$kind/RR=$rr" 32}} + } + 'system-move-long-restart-invalid'{ + foreach($writing in @($false,$true)){foreach($corruption in @('version','opcode','direction','phase','pc','queue-address','queue-count','operand-parity','value','descending')){ + Add-Key $keys "68010/MOVE.L/restart/invalid/$corruption/write=$writing" 32 + }} + } + default{throw "Unknown 010 long MOVE selection: $Group"} + } + return ,$keys +} +$savedReports=[Environment]::GetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR') +Push-Location $repo +try{ + $inputs=@((@(& git ls-files Copper68k)+@('Copper68k/M68010LongMoveResumeFrame.cs', + 'Copper68k.Tests/Synthetic/SyntheticM68010LongMoveRestartTests.cs','Copper68k.Tests/Synthetic/SyntheticMachine.cs', + 'Copper68k.Tests/Synthetic/SyntheticM68010Format8Tests.cs', + 'Copper68k.Tests/Synthetic/AddressingFixtures.cs','Copper68k.Tests/Synthetic/MoveSpecification.cs', + 'Copper68k.Tests/Synthetic/SyntheticExecution.cs','Copper68k.Tests/Synthetic/SyntheticSystemTests.cs', + 'Copper68k.Tests/Synthetic/ArchitecturalExpectation.cs','Copper68k.Tests/Synthetic/SyntheticMoveTests.cs', + 'Copper68k.Tests/Copper68k.Tests.csproj','scripts/test-copper68k-010-long-move-restart.ps1', + 'scripts/test-copper68k-synthetic.ps1'))|Sort-Object -Unique) + $binaries=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') + $evidence=@($groups.Keys|ForEach-Object{[IO.Path]::GetRelativePath($repo,(Join-Path $output "68010-$_.json"))})+ + @([IO.Path]::GetRelativePath($repo,(Join-Path $output 'restart.trx'))) + if(-not $ValidateReportsOnly){ + if(Test-Path -LiteralPath $output){throw "Use fresh 010 long MOVE outputs: $output"} + New-Item -ItemType Directory -Path $output|Out-Null + $original=@($inputs|ForEach-Object{Identity $_}) + [Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$output) + & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --filter 'FullyQualifiedName~SyntheticM68010LongMoveRestartTests' --logger 'trx;LogFileName=restart.trx' --results-directory $output *> (Join-Path $output 'run.log') + if($LASTEXITCODE -ne 0){throw "010 long MOVE execution failed: $output/run.log"} + foreach($item in $original){if((Identity $item.path).sha256 -cne $item.sha256){throw '010 long MOVE input changed during execution'}} + @{schema=1;profile='68010-long-move-private-word-continuation';sourceRevision=(& git rev-parse HEAD); + inputs=$original;binaries=@($binaries|ForEach-Object{Identity $_});evidence=@($evidence|ForEach-Object{Identity $_})}| + ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'identity.json') + } + $id=Get-Content (Join-Path $output 'identity.json') -Raw|ConvertFrom-Json + if($id.schema -ne 1 -or $id.profile -cne '68010-long-move-private-word-continuation' -or $id.sourceRevision -cnotmatch '^[a-f0-9]{40}$'){throw 'Incomplete 010 long MOVE identity'} + foreach($selection in @(@{actual=@($id.inputs);required=$inputs},@{actual=@($id.binaries);required=$binaries},@{actual=@($id.evidence);required=$evidence})){ + $actual=@($selection.actual.path) + if($actual.Count -ne $selection.required.Count -or @($actual|Select-Object -Unique).Count -ne $actual.Count -or + @(Compare-Object ($actual|Sort-Object) ($selection.required|Sort-Object)).Count){throw 'Incomplete/duplicate 010 long MOVE input selection'} + foreach($item in $selection.actual){if($item.sha256 -cnotmatch '^[A-F0-9]{64}$' -or (Identity $item.path).sha256 -cne $item.sha256){throw "Changed/malformed 010 long MOVE input: $($item.path)"}} + } + [xml]$trx=Get-Content (Join-Path $output 'restart.trx') -Raw;$c=$trx.TestRun.ResultSummary.Counters + if($c.total -ne 7 -or $c.executed -ne 7 -or $c.passed -ne 7 -or $c.failed -ne 0 -or $c.notExecuted -ne 0){throw 'Incomplete 010 long MOVE test selection'} + foreach($method in @('SourceWordsContinueWithoutRepeatingPostincrement','DescendingWriteRetainsSourceAndCommitsBaseOnce', + 'SourceFaultOperandFormsValuesAndCcr','DestinationFaultOperandFormsValuesAndCcr', + 'CopiedNestedAliasedPrefetchedAndTracedContinuations','UserStackLongOperandsPreserveSupervisorBank', + 'InvalidPrivateContinuationPreservesFrameBeforeFormatError')){ + if(@($trx.TestRun.Results.UnitTestResult|Where-Object{$_.testName -ceq "Copper68k.Tests.Synthetic.SyntheticM68010LongMoveRestartTests.$method" -and $_.outcome -ceq 'Passed'}).Count -ne 1){throw "Missing 010 long MOVE execution: $method"} + } + $coverage=@() + foreach($group in $groups.Keys){ + $r=Get-Content (Join-Path $output "68010-$group.json") -Raw|ConvertFrom-Json -AsHashtable + if($r.schema -ne 1 -or $r.model -cne '68010' -or $r.group -cne $group -or $r.xunitBatches -ne 1 -or + $r.logicalCases -ne $groups[$group] -or $r.counts.passing -ne $groups[$group] -or $r.counts.mismatching -ne 0 -or + $r.counts.unsupported -ne 0 -or $r.counts.untested -ne 0 -or $r.failures.Count -ne 0){throw "Incomplete 010 long MOVE gate: $group"} + $keys=Expected-Keys $group + if($r.combinations.Count -ne $keys.Count){throw "010 long MOVE combination selection differs: $group"} + foreach($key in $keys.Keys){if(-not $r.combinations.Contains($key) -or $r.combinations[$key].Count -ne 1 -or $r.combinations[$key].passing -ne $keys[$key]){throw "Missing/misweighted 010 long MOVE combination: $key"}} + $coverage+=@{group=$group;logicalCases=$groups[$group];combinations=$keys.Count} + } + @{schema=1;model='68010';logicalCases=344192;xunitBatches=5;fixedRegressions=2;coverage=$coverage;roadmapComplete=$false; + reference='MC68000UM 6.3.9.2/6.3.10/6.4; M68000PM MOVE/MOVEA'; + scope='Generated private long-MOVE/MOVEA images, two 16-bit cycles, independent RR choices and original per-word addresses; retained timing policy'; + remaining='No executed external continuation oracle: foreign silicon images, external BERR, RMW, non-MOVE restoration and physical restart timing remain unqualified'}| + ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'summary.json') + Write-Host "010 private long MOVE qualified: 344192 scenarios / 5 reports + 2 fixed regressions; $output" +}finally{[Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$savedReports);Pop-Location} +$global:LASTEXITCODE=0 diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 03f7a75e..1568e0ac 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -154,6 +154,11 @@ try { $expected['system-move-word-restart-edges'] = 640 $expected['system-move-word-restart-a7'] = 192 $expected['system-move-word-restart-invalid'] = 224 + $expected['system-move-long-restart-source'] = 165888 + $expected['system-move-long-restart-destination'] = 172032 + $expected['system-move-long-restart-edges'] = 5120 + $expected['system-move-long-restart-a7'] = 512 + $expected['system-move-long-restart-invalid'] = 640 } if ($model -eq '68040') { $expected['instruction-fault-frame'] = 36864 @@ -327,7 +332,7 @@ try { foreach ($row in $inventory.combinations) { if ($row.Milestone -le 5) { $row.status = 'passing'; $row.note = 'Passing named semantic scenario batches; consult qualification boundaries for untested physical/internal protocols.' } } - $inventory | Add-Member -NotePropertyName qualificationBoundaries -NotePropertyValue @('RTE fault restart/internal formats remain untested: 010 long/non-MOVE/foreign frame8 restart, 020/030 frames9/A/B, 040 frame7 CP context transfer and detailed access-fault restoration', 'BKPT external replacement responder is unavailable', 'MOVES physical function-code spaces and LPSTOP CPU-space broadcast are unqualified', 'CALLM/RTM type1 protocol uses an internal synthetic responder; normal public buses have none', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands are unavailable') + $inventory | Add-Member -NotePropertyName qualificationBoundaries -NotePropertyValue @('RTE fault restart/internal formats remain untested: 010 non-MOVE/foreign frame8 restart, 020/030 frames9/A/B, 040 frame7 CP context transfer and detailed access-fault restoration', 'BKPT external replacement responder is unavailable', 'MOVES physical function-code spaces and LPSTOP CPU-space broadcast are unqualified', 'CALLM/RTM type1 protocol uses an internal synthetic responder; normal public buses have none', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands are unavailable') $inventory | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'qualified-inventory.json') @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=$logicalBatches; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; m68040DisabledMmuInstructionGate='passing'; m68000AddressErrorDoubleFaultGate='passing'; m68010Format8StructureGate='passing'; m68010WordMoveRestartGate='passing'; roadmapComplete=$false; seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); @@ -353,9 +358,9 @@ try { m68040BatchFaultScope=@{logicalCases=139392; batches=2; translationEnabled=$false; paths=@('cold','cached','model-specific cached','self-branch cached'); limits=@('instruction cap','boundary denial','cycle deadline'); architectural='Supervisor RTE validation frames, unbuffered self-branch instruction-fault frame and fatal second-fault halt'; policy='Generic short operand frames, scalar/batch bus order and machine/native cycle equality'; qualification='Dedicated discovery enumerates independent combinations; BatchFault scope proves four delivery paths, count, callback and retry defects'; remaining='General data format-7 restart, other entry paths and internal-restoration faults'}; m68060LowPowerStopScope='Every unrecognized second opcode word in both privilege modes; fixed encoding, status, CCR and incoming trace cases across all profiles. Opcode-PC exception frames follow MC68060UM 8.2.4/5; physical broadcast, pins and ordinary STOP S-clear software disagreement remain unqualified'; cacheInstructionEncodingScope='Every F4xx opcode, all CCRs and both privilege states across eight profiles; scope zero is illegal on 040/060, including neither-cache forms, before privilege effects. Empty fixture caches qualify architectural state and exception outcomes, not physical invalidation, writeback, bus faults or timing'; - m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; marked private word-MOVE/MOVEA images resume their faulted cycle; other opaque restart/input state, long transfers, RMW and physical timing remain unqualified'; + m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; marked private word/long-MOVE/MOVEA images resume pending word cycles without replay; non-MOVE/foreign restart/input state, external BERR, RMW and physical timing remain unqualified'; m68000DoubleFaultScope='Address-error entry/handler faults; reset-only recovery; external BERR/reset-vector faults unavailable through the current public bus API'; - unavailableSystemCoverage=@('010 long/non-MOVE/external bus-fault restart and 020/030 internal restart', '020/030 coprocessor midinstruction restoration', '040 access-fault CP context transfer, detailed frame-validation/fault entry and writeback-handler qualification', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands'); + unavailableSystemCoverage=@('010 non-MOVE/external bus-fault restart and 020/030 internal restart', '020/030 coprocessor midinstruction restoration', '040 access-fault CP context transfer, detailed frame-validation/fault entry and writeback-handler qualification', 'External BKPT replacement responder', 'Physical MOVES function-code spaces and LPSTOP CPU-space broadcast', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands'); timing='Semantic gate; timing policy and physical qualification remain separate'} | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'summary.json') Write-Host "Synthetic milestones 1-5 semantic gates: $logicalCases logical cases; reports at $output" From ea7b5acb128fc77d46bb8e77d329aacb1008aef1 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 17:32:28 +0300 Subject: [PATCH 069/211] Make 68010 continuation mutation qualification reproducible --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 35 +++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 7 +- ...test-copper68k-010-long-move-mutations.ps1 | 145 ++++++++++++++++++ 3 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 scripts/test-copper68k-010-long-move-mutations.ps1 diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 53c5ef26..0d44748c 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -6364,6 +6364,41 @@ base commit and lost completed output. Both fixed regressions remain unchanged after fixture strengthening; each mutation restores source byte-for-byte. Evidence: `artifacts/m6-010-long-restart-mutations/mutations.json`. +A seventh targeted mutation removes only the memory-source continuation's D2 +write. All 18,432 D2-destination scenarios detect it; the other 147,456 source +scenarios remain passing, with every combination independently classified. +This proves that the distinct initial destination values expose the omitted +write instead of allowing a coincidentally correct register value. Source is +restored byte-for-byte and normal CPU/test assemblies remain unchanged. +Evidence: `artifacts/m6-010-long-restart-register-mutation/mutation-identity.json`. + +The maintained command below repeats all seven proofs using isolated build +outputs and temporarily mutates the CPU source in the selected checkout. +Do not edit that source or build from it concurrently. The command refuses to +overwrite a concurrently changed source. It checks exact test names and failed +methods, captured mutation source, source/input/binary/evidence identities, and +all 648 register-mutation combinations. Frozen validation fails for incomplete +or altered selections. Its checks are targeted defect proofs, not additional +passing emulator semantics or permission to retire unrelated regressions. + +```powershell +./scripts/test-copper68k-010-long-move-mutations.ps1 -OutputDirectory artifacts/010-long-move-mutations +./scripts/test-copper68k-010-long-move-mutations.ps1 -ValidateReportsOnly -OutputDirectory artifacts/010-long-move-mutations +``` + +Fresh execution and frozen validation pass at +`artifacts/m6-010-long-restart-maintained-mutations-final/`: thirteen exact +executions, comprising twelve fixed regression executions and the complete +165,888-case source matrix under the register-write mutation. Eight altered +profile/selection/source/execution/classification controls reject their intended +error after changed evidence hashes are refreshed. The restoration guard also +restores the expected mutation and rejects a concurrent edit without overwriting +it, using an isolated text fixture. Evidence is +`artifacts/m6-010-long-restart-mutation-controls/controls.json` and +`artifacts/m6-010-long-restart-mutation-controls/concurrency-guard.json`. +The preliminary maintained-command output precedes the exact failed-method +and concurrent-edit guards; it is superseded by this final accepted output. + Pinned external semantic audits pass: SingleStepTests 312,500 cases / 125 files at `64b253116a3de04aaac4346c43680960dc9b67e5`, and Musashi 536 passing / 88 excluded combinations at `72c1d74800f3087b45a0c1a7342601bbed898881`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index b9ac6153..5d8e918b 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2406,8 +2406,11 @@ reject changed identities, empty/wrong selections and redistributed coverage. The ordinary semantic gate now requires **85,535,254 scenarios / 685 reports**. These counts include the new requirements, not proof of pending broad execution. -The retained 68010 selection passes 21 tests without skips; six production -mutations detect the targeted continuation defects. Pinned SingleStepTests and +The retained 68010 selection passes 21 tests without skips; seven production +mutations detect the targeted continuation defects, including all 18,432 cases +affected by an omitted destination-register write. A maintained mutation command +reproduces the proofs with isolated builds, exact execution identities and +independent combination checks. Pinned SingleStepTests and Musashi semantic audits pass. The isolated unpublished `.64` consumer passes its Release build, host/disk/engine tests and all three native replays. The first full CPU run failed only an obsolete structural-image expectation; that test diff --git a/scripts/test-copper68k-010-long-move-mutations.ps1 b/scripts/test-copper68k-010-long-move-mutations.ps1 new file mode 100644 index 00000000..221947d4 --- /dev/null +++ b/scripts/test-copper68k-010-long-move-mutations.ps1 @@ -0,0 +1,145 @@ +[CmdletBinding()] +param([switch]$ValidateReportsOnly,[string]$OutputDirectory='artifacts/010-long-move-mutations') +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +$source=Join-Path $repo 'Copper68k/M68kCore.cs' +$fixedMethods=@('SourceWordsContinueWithoutRepeatingPostincrement','DescendingWriteRetainsSourceAndCommitsBaseOnce') +$mutations=@( + @{name='disabled-capture';old='if (!_m68010LongMovePending || (isWrite != (_m68010LongMovePhase >= 2)) ||';new='if (true || (isWrite != (_m68010LongMovePhase >= 2)) ||';failed=2;passed=0}, + @{name='lost-source-high';old='_m68010LongMoveValue = (uint)pending << 16;';new='_m68010LongMoveValue = pending;';failed=1;passed=1}, + @{name='repeated-source-increment';old='if (((opcode >> 3) & 7) == 3) SetAddressRegister(opcode & 7, State.A[opcode & 7] + 4);';new='if (((opcode >> 3) & 7) == 3) SetAddressRegister(opcode & 7, State.A[opcode & 7] + 8);';failed=1;passed=1}, + @{name='wrong-remaining-address';old='pending = ReadWord(unchecked(_m68010LongMoveAddress + 2));';new='pending = ReadWord(unchecked(_m68010LongMoveAddress + 4));';failed=1;passed=1}, + @{name='wrong-descending-commit';old='if (destinationMode == 4) SetAddressRegister(destinationRegister, _m68010LongMoveAddress);';new='if (destinationMode == 4) SetAddressRegister(destinationRegister, _m68010LongMoveAddress + 4);';failed=1;passed=1}, + @{name='lost-completed-output';old='_m68010LongMoveValue = M68010WordMoveResumeFrame.Long(words, 12);';new='_m68010LongMoveValue = M68010WordMoveResumeFrame.Long(words, 12) & 0xffff;';failed=2;passed=0}, + @{name='omitted-register-write';old='WritePlannedEaValue(in destination, value);';new='if (destinationMode != 0 || destinationRegister != 2) WritePlannedEaValue(in destination, value);';failed=1;passed=0;matrix=$true} +) +function Identity([string]$Path){ + $file=Join-Path $repo $Path + if(-not(Test-Path -LiteralPath $file -PathType Leaf)){throw "Missing mutation input: $Path"} + return @{path=$Path;sha256=(Get-FileHash -LiteralPath $file).Hash} +} +function Mutated-Source([string]$Text,$Mutation){ + if($Mutation.matrix){ + $start=$Text.IndexOf('protected void ResumeM68010LongMove(') + $end=$Text.IndexOf('protected ushort[]? CaptureM68010WordMoveResumeFrame(',$start) + if($start -lt 0 -or $end -le $start){throw 'Missing long continuation scope'} + $scope=$Text.Substring($start,$end-$start) + if(($scope.Split($Mutation.old).Length-1) -ne 1){throw 'Nonunique register mutation anchor'} + return $Text.Substring(0,$start)+$scope.Replace($Mutation.old,$Mutation.new)+$Text.Substring($end) + } + if(($Text.Split($Mutation.old).Length-1) -ne 1){throw "Nonunique mutation anchor: $($Mutation.name)"} + return $Text.Replace($Mutation.old,$Mutation.new) +} +function Validate-Selection($Actual,[string[]]$Required){ + $paths=@($Actual.path) + if($paths.Count -ne $Required.Count -or @($paths|Select-Object -Unique).Count -ne $paths.Count -or + @(Compare-Object ($paths|Sort-Object) ($Required|Sort-Object)).Count){throw 'Incomplete/duplicate mutation identity selection'} + foreach($entry in $Actual){if($entry.sha256 -cnotmatch '^[A-F0-9]{64}$' -or (Identity $entry.path).sha256 -cne $entry.sha256){throw "Changed mutation evidence: $($entry.path)"}} +} +function Restore-Source([byte[]]$Bytes,[string]$ExpectedHash,[string]$OriginalHash){ + $current=(Get-FileHash -LiteralPath $source).Hash + if($current -ceq $OriginalHash){return} + if($current -cne $ExpectedHash){throw 'CPU source changed concurrently; current file left intact instead of overwriting edits'} + [IO.File]::WriteAllBytes($source,$Bytes) +} +$saved=[Environment]::GetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR') +Push-Location $repo +try { + $inputs=@(@(& git ls-files Copper68k Copper68k.Tests)+@('scripts/test-copper68k-010-long-move-mutations.ps1')|Sort-Object -Unique) + $normal=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll'|Where-Object{Test-Path -LiteralPath (Join-Path $repo $_)}) + if(-not $ValidateReportsOnly){ + if(Test-Path -LiteralPath $output){throw 'Use a fresh mutation output directory'} + New-Item -ItemType Directory -Path $output|Out-Null + $original=@($inputs|ForEach-Object{Identity $_}) + $originalNormal=@($normal|ForEach-Object{Identity $_}) + $bytes=[IO.File]::ReadAllBytes($source) + $text=[IO.File]::ReadAllText($source) + $sourceHash=(Get-FileHash -LiteralPath $source).Hash + $expectedSourceHash=$sourceHash + $records=@() + try { + foreach($mutation in $mutations){ + $directory=Join-Path $output $mutation.name + New-Item -ItemType Directory -Path $directory|Out-Null + $mutated=Mutated-Source $text $mutation + [IO.File]::WriteAllText((Join-Path $directory 'mutated-M68kCore.cs'),$mutated) + if((Get-FileHash -LiteralPath $source).Hash -cne $sourceHash){throw 'CPU source changed before mutation; current file left intact'} + [IO.File]::WriteAllText($source,$mutated) + $expectedSourceHash=(Get-FileHash -LiteralPath $source).Hash + $methods=if($mutation.matrix){@('SourceFaultOperandFormsValuesAndCcr')}else{$fixedMethods} + $filter=($methods|ForEach-Object{"FullyQualifiedName~SyntheticM68010LongMoveRestartTests.$_"}) -join '|' + [Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$directory) + & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path (Join-Path $directory 'build') --filter $filter --logger 'trx;LogFileName=mutation.trx' --results-directory $directory *> (Join-Path $directory 'run.log') + $code=$LASTEXITCODE + Restore-Source $bytes $expectedSourceHash $sourceHash + if($code -ne 1){throw "Mutation did not fail: $($mutation.name)"} + $selected=@('mutation.trx','mutated-M68kCore.cs','run.log', + 'build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll') + if($mutation.matrix){$selected+='68010-system-move-long-restart-source.json'} + $records+=@{name=$mutation.name;exit=$code;evidence=@($selected|ForEach-Object{Identity ([IO.Path]::GetRelativePath($repo,(Join-Path $directory $_)))})} + } + } finally {Restore-Source $bytes $expectedSourceHash $sourceHash} + foreach($entry in @($original)+@($originalNormal)){if((Identity $entry.path).sha256 -cne $entry.sha256){throw "Mutation changed original input: $($entry.path)"}} + @{schema=1;profile='68010-long-move-seven-mutation-proofs';sourceRevision=(& git rev-parse HEAD); + inputs=$original;normalBinaries=$originalNormal;sourceRestored=$true;mutations=$records}| + ConvertTo-Json -Depth 10|Set-Content (Join-Path $output 'identity.json') + } + $id=Get-Content (Join-Path $output 'identity.json') -Raw|ConvertFrom-Json + if($id.schema -ne 1 -or $id.profile -cne '68010-long-move-seven-mutation-proofs' -or $id.sourceRevision -cnotmatch '^[a-f0-9]{40}$' -or $id.sourceRestored -ne $true){throw 'Incomplete mutation profile'} + Validate-Selection @($id.inputs) $inputs + Validate-Selection @($id.normalBinaries) $normal + if($id.mutations.Count -ne 7 -or @($id.mutations.name|Select-Object -Unique).Count -ne 7){throw 'Incomplete/duplicate mutation selection'} + $text=[IO.File]::ReadAllText($source) + foreach($mutation in $mutations){ + $row=@($id.mutations|Where-Object name -CEQ $mutation.name) + if($row.Count -ne 1 -or $row[0].exit -ne 1){throw "Missing executed mutation: $($mutation.name)"} + $directory=Join-Path $output $mutation.name + $selected=@('mutation.trx','mutated-M68kCore.cs','run.log', + 'build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll') + if($mutation.matrix){$selected+='68010-system-move-long-restart-source.json'} + $paths=@($selected|ForEach-Object{[IO.Path]::GetRelativePath($repo,(Join-Path $directory $_))}) + Validate-Selection @($row[0].evidence) $paths + if([IO.File]::ReadAllText((Join-Path $directory 'mutated-M68kCore.cs')) -cne (Mutated-Source $text $mutation)){throw 'Mutation scope differs from intended change'} + [xml]$trx=Get-Content (Join-Path $directory 'mutation.trx') -Raw + $c=$trx.TestRun.ResultSummary.Counters + $methods=if($mutation.matrix){@('SourceFaultOperandFormsValuesAndCcr')}else{$fixedMethods} + $names=@($methods|ForEach-Object{"Copper68k.Tests.Synthetic.SyntheticM68010LongMoveRestartTests.$_"}) + $results=@($trx.TestRun.Results.UnitTestResult) + if($c.total -ne $names.Count -or $c.executed -ne $names.Count -or $c.failed -ne $mutation.failed -or + $c.passed -ne $mutation.passed -or $c.notExecuted -ne 0 -or $results.Count -ne $names.Count -or + @(Compare-Object ($results.testName|Sort-Object) ($names|Sort-Object)).Count -ne 0 -or + @($results|Where-Object outcome -NOTIN @('Passed','Failed')).Count -ne 0){throw "Wrong mutation test selection/outcomes: $($mutation.name)"} + $failedMethods=switch($mutation.name){ + {$_ -in @('disabled-capture','lost-completed-output')}{ $fixedMethods } + 'wrong-descending-commit'{@('DescendingWriteRetainsSourceAndCommitsBaseOnce')} + 'omitted-register-write'{@('SourceFaultOperandFormsValuesAndCcr')} + default{@('SourceWordsContinueWithoutRepeatingPostincrement')} + } + $failedNames=@($failedMethods|ForEach-Object{"Copper68k.Tests.Synthetic.SyntheticM68010LongMoveRestartTests.$_"}) + $actualFailures=@($results|Where-Object outcome -CEQ Failed|ForEach-Object testName) + if(@(Compare-Object ($actualFailures|Sort-Object) ($failedNames|Sort-Object)).Count){throw "Wrong failed mutation method: $($mutation.name)"} + foreach($counter in @('error','timeout','aborted','inconclusive','passedButRunAborted','notRunnable','disconnected','inProgress','pending')){ + if([int]$c.$counter -ne 0){throw "Nonzero mutation terminal counter: $counter"} + } + if($mutation.matrix){ + $r=Get-Content (Join-Path $directory '68010-system-move-long-restart-source.json') -Raw|ConvertFrom-Json -AsHashtable + if($r.schema -ne 1 -or $r.model -cne '68010' -or $r.group -cne 'system-move-long-restart-source' -or + $r.logicalCases -ne 165888 -or $r.xunitBatches -ne 1 -or $r.counts.passing -ne 147456 -or + $r.counts.mismatching -ne 18432 -or $r.counts.unsupported -ne 0 -or $r.counts.untested -ne 0 -or $r.combinations.Count -ne 648){throw 'Incomplete register mutation coverage'} + $sources=@('(A0)','(A0)+','-(A0)','d16(A0)','index(A0)','abs.w','abs.l','d16(PC)','index(PC)') + $destinations=@('(A1)','(A1)+','-(A1)','d16(A1)','index(A1)','abs.w','abs.l','D2','A2') + foreach($from in $sources){foreach($to in $destinations){foreach($rr in 0..3){foreach($supervisor in @($false,$true)){ + $key="68010/MOVE.L/restart/source/$from/$to/super=$supervisor/RR=$rr" + $kind=if($to -ceq 'D2'){'mismatching'}else{'passing'} + if(-not $r.combinations.Contains($key) -or $r.combinations[$key].Count -ne 1 -or $r.combinations[$key][$kind] -ne 256){throw "Misclassified mutation combination: $key"} + }}}} + } + } + @{schema=1;mutationProofs=7;executedTests=13;fixedTests=12;matrixCases=165888;matrixDetected=18432; + sourceRestored=$true;normalBinariesUnchanged=$true;roadmapComplete=$false; + scope='Targeted emulator continuation defects; no historical regression retired or hardware restart qualification'}| + ConvertTo-Json -Depth 6|Set-Content (Join-Path $output 'summary.json') + Write-Host "Seven 010 long MOVE mutations qualified; exact test/key outcomes and source restoration verified: $output" +} finally {[Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$saved);Pop-Location} +$global:LASTEXITCODE=0 From c3c9fd2f8a21760de90d0d7d1d844e15633174bd Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 18:04:12 +0300 Subject: [PATCH 070/211] Qualify full-width EXG and consolidate proven regression --- .../M68kInterpreterCoreBehaviorTests.cs | 13 -- Copper68k.Tests/Synthetic/README.md | 32 +++++ .../Synthetic/SyntheticExgRegisterTests.cs | 79 +++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 100 +++++++++++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 38 +++++- scripts/test-copper68k-exg-mutations.ps1 | 127 ++++++++++++++++++ scripts/test-copper68k-exg.ps1 | 91 +++++++++++++ scripts/test-copper68k-synthetic.ps1 | 1 + 8 files changed, 463 insertions(+), 18 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticExgRegisterTests.cs create mode 100644 scripts/test-copper68k-exg-mutations.ps1 create mode 100644 scripts/test-copper68k-exg.ps1 diff --git a/Copper68k.Tests/M68kInterpreterCoreBehaviorTests.cs b/Copper68k.Tests/M68kInterpreterCoreBehaviorTests.cs index 5395944e..fe01c293 100644 --- a/Copper68k.Tests/M68kInterpreterCoreBehaviorTests.cs +++ b/Copper68k.Tests/M68kInterpreterCoreBehaviorTests.cs @@ -3587,19 +3587,6 @@ public void MoveUspUsesCorrectSupervisorDirection() Assert.Equal(0x3000u, cpu.State.A[7]); } [Fact] - public void ExgAddressRegistersSwapsFullLongValues() - { - var bus = new TestBus(); - Write(bus.Memory, 0x1000, 0xC5, 0x4E); // EXG A2,A6 - var cpu = new M68kInterpreter(bus); - cpu.Reset(0x1000, 0x3000); - cpu.State.A[2] = 0x0000_0040; - cpu.State.A[6] = 0x00C0_0276; - cpu.ExecuteInstruction(); - Assert.Equal(0x00C0_0276u, cpu.State.A[2]); - Assert.Equal(0x0000_0040u, cpu.State.A[6]); - } - [Fact] public void AbcdDataRegisterUsesStickyZeroAndDecimalCarry() { var bus = new TestBus(); diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 67546f75..f357c72a 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -112,6 +112,38 @@ address boundaries, invalid opwords and alignment. Full indexed fixtures cover LEA/PEA and MOVEM, and cover spaced MOVEP bytes, masks, aliases, model exceptions, register encodings and preserved CCR bits. +The `transfer-exg-wide` group adds **88,704 scenarios / 10,584 combinations** +across eight profiles. DD/AA/AD exchanges exercise every register binding with +wide values, then eight value pairs and all 32 CCRs for canonical bindings, +self aliases and A7. User/ISP banks apply on every model; 020/030/040 and A1200 +also cover MSP. Architectural An values remain full 32-bit values even on a +24-bit bus. Registers, SR, exact PC, inactive stack banks, unchanged memory, +absence of operand transfers and a following NOP are checked independently. +Three fixed opcode examples validate fixture encoding. + +```powershell +./scripts/test-copper68k-exg.ps1 -OutputDirectory artifacts/exg-wide +./scripts/test-copper68k-exg.ps1 -ValidateReportsOnly -OutputDirectory artifacts/exg-wide +./scripts/test-copper68k-exg-mutations.ps1 -OutputDirectory artifacts/exg-mutations +./scripts/test-copper68k-exg-mutations.ps1 -ValidateReportsOnly -OutputDirectory artifacts/exg-mutations +``` + +The positive command checks exact model/test selections, source/binary/evidence +identities and independently enumerated keys and weights. The mutation command +temporarily modifies only AA exchange source, builds in isolated outputs and +restores original bytes; do not edit that source or build from it concurrently. +It detects word truncation and lost latched input with exact per-key outcomes. +`-IncludeHistoricalRegression` is for a pre-retirement source containing the +old `ExgAddressRegistersSwapsFullLongValues` method; it must fail on a checkout +where that method is absent. The exact retirement witness and before/after +proofs are recorded in the +[qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#exg-wide-register-qualification-and-consolidation--2026-10-06). + +The expanded ordinary gate requires **85,623,958 scenarios / 693 reports**. +The preceding full CPU run and this scoped EXG execution have separate source +and assembly identities; combined report validation does not imply a fresh +full-suite execution of the expanded source. Milestone 6 remains incomplete. + ## Seeded and external audits ```powershell diff --git a/Copper68k.Tests/Synthetic/SyntheticExgRegisterTests.cs b/Copper68k.Tests/Synthetic/SyntheticExgRegisterTests.cs new file mode 100644 index 00000000..30b25f07 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticExgRegisterTests.cs @@ -0,0 +1,79 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// M68000PM EXG: full long-register exchange; CCR unaffected. External address +// width does not truncate architectural An values, including active A7 banks. +public sealed class SyntheticExgRegisterTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + private static readonly (uint Left, uint Right)[] Values = + [ + (0x00c00276, 0x00000040), // Exact historical-regression input, A6/A2. + (0x89abcdef, 0x12345678), + (0, uint.MaxValue), (uint.MaxValue, 0), + (0x7fffffff, 0x80000000), (0xffff0000, 0x0000ffff), + (0x5555aaaa, 0xaaaa5555), (1, 1) + ]; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void WideValuesAliasesAndActiveStackBanks(string modelId) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); + var report = new CoverageBatch(modelId, "transfer-exg-wide"); + var banks = m.Model.FullIndex && modelId != "68060" ? new[] { "user", "ISP", "MSP" } : ["user", "ISP"]; + foreach (var bank in banks) + foreach (var kind in new[] { 0x40, 0x48, 0x88 }) + { + // Separate register enumeration and value/CCR boundaries avoid an + // unnecessary Cartesian product while every binding sees wide data. + for (var source = 0; source < 8; source++) + for (var destination = 0; destination < 8; destination++) + for (var pair = 0; pair < 2; pair++) + Case(m, report, kind, source, destination, bank, pair, 0, "registers"); + foreach (var (source, destination) in new[] { (6, 2), (2, 2), (7, 3), (2, 7), (7, 7) }) + for (var pair = 0; pair < Values.Length; pair++) + for (var ccr = 0; ccr < 32; ccr++) + Case(m, report, kind, source, destination, bank, pair, ccr, "boundaries"); + } + report.Complete(output); + } + + private static ushort Encode(int kind, int source, int destination) + => (ushort)(0xc100 | destination << 9 | kind | source); + + private static void Case(SyntheticMachine m, CoverageBatch report, int kind, int source, int destination, + string bank, int pair, int ccr, string scenario) + { + m.Reset(ccr, bank != "user"); + if (bank == "MSP") + { + m.Core.State.StatusRegister |= 0x1000; + m.Core.State.SetActiveStackPointer(0x7400); + } + // Alias cases have a single input register. The second assignment + // establishes its initial value before the independent snapshot. + if (kind == 0x40) m.Core.State.D[source] = Values[pair].Left; + else m.Core.State.A[source] = Values[pair].Left; + if (kind == 0x48) m.Core.State.A[destination] = Values[pair].Right; + else m.Core.State.D[destination] = Values[pair].Right; + var opcode = Encode(kind, source, destination); + var e = SyntheticExecution.Prepare(m, [opcode]); + if (kind == 0x40) (e.D[source], e.D[destination]) = (e.D[destination], e.D[source]); + else if (kind == 0x48) (e.A[source], e.A[destination]) = (e.A[destination], e.A[source]); + else (e.A[source], e.D[destination]) = (e.D[destination], e.A[source]); + if (bank == "MSP") e.MasterStackPointer = e.A[7]; + e.ExpectedOperandTransfers = []; + e.ControlChecks["no exception entry"] = (s => s.ExceptionSequence, m.Core.State.ExceptionSequence); + var family = kind == 0x40 ? "DD" : kind == 0x48 ? "AA" : "AD"; + SyntheticExecution.Run(m, e, report, + $"{m.Model.Id}/EXG.L/{family}/bank={bank}/r{source}->r{destination}/{scenario}/pair={pair}/op={opcode:X4}/ccr={ccr:X2}"); + } + + [Theory] + [InlineData(0x48, 6, 2, 0xc54e)] + [InlineData(0x40, 0, 0, 0xc140)] + [InlineData(0x88, 7, 3, 0xc78f)] + public void FixtureEncodingMatchesFixedExamples(int kind, int source, int destination, int expected) + => Assert.Equal((ushort)expected, Encode(kind, source, destination)); +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 0d44748c..50d296bb 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -6411,10 +6411,17 @@ The first full CPU run records 5,128 passing, one failing and nineteen skipped tests (`artifacts/m6-010-long-restart-full/`). Its sole failure was the structural test's obsolete zero-internal-image expectation described above. This failed run remains intact and is not accepted as a passing semantic gate. The corrected -full rerun, including nine pinned WinUAE selections, is still pending at this -commit (`artifacts/m6-010-long-restart-full-final/`). Discovery lists 5,116 test +full rerun passes 5,129 tests with zero failures and nineteen unavailable tests +(`artifacts/m6-010-long-restart-full-final/`). Exact method/theory/skip selection, +all nine pinned WinUAE input/CPU/adapter identities and the ordinary semantic +gate pass strict verification (`full-identity.json`). Discovery lists 5,116 test descriptors; one theory expands into 33 runtime rows, giving 5,148 executions. -The ordinary gate now requires 85,535,254 scenarios / 685 reports. +This run validates 85,535,254 scenarios / 685 reports. It precedes the later EXG +fixture and retirement, which receive separate scoped execution below; it is +not a broad run of that subsequently expanded test source. The source snapshot +in `artifacts/m6-010-long-restart-full-final-inputs.json` was captured while the +run was active, not before execution. Its recorded normal assemblies remained +unchanged throughout the run and subsequent isolated EXG builds. An isolated CopperScreen consumer at `d9beae8b88be24032221e3482942a249c03c27d3` passes its Release build, host tests (149 passing / six optional skips), disk @@ -6436,3 +6443,90 @@ No regression is retired, public package published, hardware timing qualified, or full format-8 restart claimed. Non-MOVE/foreign/RMW/external-BERR restoration, rejected-RTE CCR architecture and all other reference/consolidation gaps remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +### EXG wide-register qualification and consolidation — 2026-10-06 + +M68000PM EXG 4-105 specifies long-register DD/AA/DA exchanges with unchanged +CCR ([manual](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf)). +Architectural address registers retain all 32 bits independently of external +bus width. The existing shared register-transfer matrix used An values below +64 KiB, so it did not prove replacement coverage for the old full-long fact. +The added `SyntheticExgRegisterTests` fixture fills that gap without changing +production CPU behavior or consulting production expectation helpers. + +| Scope | Scenarios per profile | Combinations per profile | +| --- | ---: | ---: | +| 68000, 68010, 68060: user and ISP | 8,448 | 1,008 | +| 68EC020, 68020, 68030, 68040, A1200: user, ISP and MSP | 12,672 | 1,512 | +| All eight profiles | **88,704** | **10,584** | + +Each bank separately enumerates every DD/AA/AD register binding with two wide +value pairs (384 scenarios) and five canonical/alias/A7 bindings with eight +pairs and every CCR (3,840). Register snapshots independently determine results, +including self aliases. Checks cover full architectural values, unchanged +SR/CCR, exact PC, every untouched register/inactive stack bank, guarded memory, +no operand bus transfers or exception entry, and a following NOP. Counts are +scenarios, not twice-counted sentinel phases. Three fixed encodings include +`C54E`, the historical exchange word. + +```powershell +./scripts/test-copper68k-exg.ps1 -OutputDirectory artifacts/exg-wide +./scripts/test-copper68k-exg.ps1 -ValidateReportsOnly -OutputDirectory artifacts/exg-wide +./scripts/test-copper68k-exg-mutations.ps1 -OutputDirectory artifacts/exg-mutations +./scripts/test-copper68k-exg-mutations.ps1 -ValidateReportsOnly -OutputDirectory artifacts/exg-mutations +``` + +Fresh and frozen positive qualification pass eleven exact executions without +skips at `artifacts/m6-exg-wide-qualified-final/`. Source inputs are captured +before execution and checked afterward. CPU, test, report/TRX/log identities, +model/group selection and every independently enumerated key/weight must match. +Isolated CPU SHA256 is +`f71d94abe6be78af6eb966c605fdde04fac79f5fdfa745714f1d68a4a3b139c9`; +test adapter is +`3e8779eda6a8740c5e75278652e0e4c2b3474506afbd2456f600d6f5c26b99bd`. +Isolated build identities differ from the recorded normal/consumer outputs; +they do not imply different production source behavior. + +Only `M68kInterpreterCoreBehaviorTests.ExgAddressRegistersSwapsFullLongValues` +is retired. Its exact replacement is +`68000/EXG.L/AA/bank=ISP/r6->r2/boundaries/pair=0/op=C54E/ccr=00`, +with the same opcode and values, supervisor SR `2700` and stronger preservation +checks. Both full 68000 matrix and historical fact detect both targeted mutations: + +| Mutation in AA exchange | Mismatching / passing replacement scenarios | +| --- | ---: | +| Truncate both results to a word | 2,096 / 6,352 | +| Read the already-overwritten register instead of latched input | 1,568 / 6,880 | + +Pre-retirement positive evidence passes twelve tests at +`artifacts/m6-exg-wide-before-retirement-final/`. Pre-retirement proofs at +`artifacts/m6-exg-wide-mutations-before/` execute two failing tests per mutation; +post-retirement proofs at `artifacts/m6-exg-wide-mutations-after/` execute one. +Each command requires every one of 1,008 keys with the expected classification, +all failure identifiers and the exact historical witness. Fresh and frozen +validation pass before and after retirement. The command captures mutation +source, restores original bytes and rejects concurrent edits without overwriting +them. Normal CPU/test assemblies retain the identities recorded above for the +full run. No unrelated regression is retired. + +Twelve controls in `artifacts/m6-exg-wide-controls-final/controls.json` reject +wrong profiles, empty input/mutation selections, missing reports, foreign test +selection, wrong keys/weights, altered mutation source, wrong classifications +and a missing historical witness. Altered evidence hashes are refreshed so +semantic checks must reject the content itself. Original evidence is retained. +The preliminary scripted positive run failed a test-name array-construction +check despite passing emulator tests; the preliminary controls stopped on a +single-result XML indexing error. Both are excluded and superseded by the final +outputs. Frozen positive validation attempted while mutation source was active +correctly rejected the changed input; it passes after source restoration. + +The ordinary gate now requires **85,623,958 scenarios / 693 reports**. Combined +report validation in `artifacts/m6-exg-wide-combined-reports/` uses the prior +strictly verified 685-report full run plus eight freshly qualified EXG reports; +it is not a fresh full-suite run of the changed test source. The historical +full run, long-continuation audits and pre-retirement proofs retain their original +input identities; they are not rehashed to pretend to execute later source. +No new consumer execution or package publication is needed for this test-only +slice. The complete-040 inventory's 480 untested cases, all broader restoration +and software disagreements, independent coverage and further consolidation +remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 5d8e918b..73d0044d 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2405,7 +2405,7 @@ and weights; fresh and frozen validations pass. Fourteen integrity controls reject changed identities, empty/wrong selections and redistributed coverage. The ordinary semantic gate now requires **85,535,254 scenarios / 685 reports**. -These counts include the new requirements, not proof of pending broad execution. +The corrected full run validates these counts; its scope precedes the later EXG addition. The retained 68010 selection passes 21 tests without skips; seven production mutations detect the targeted continuation defects, including all 18,432 cases affected by an omitted destination-register write. A maintained mutation command @@ -2415,9 +2415,43 @@ Musashi semantic audits pass. The isolated unpublished `.64` consumer passes its Release build, host/disk/engine tests and all three native replays. The first full CPU run failed only an obsolete structural-image expectation; that test now checks explicit private-image examples and passes its retained selection. -The corrected full rerun is pending at this commit. See the reference document +The corrected full rerun passes 5,129 tests, with zero failures and nineteen +unavailable tests, including nine pinned WinUAE selections. See the reference document for identities, exclusions and preserved failed evidence. No regression is retired or public package published. Long non-MOVE, foreign hardware images, RMW and external bus errors remain required, along with all other milestone-6 reference/consolidation gaps. This private continuation is not hardware-internal or physical timing qualification. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +### Milestone 6 EXG wide-register qualification and consolidation — 2026-10-06 + +The original shared register-transfer matrix used small An values and could +not detect word truncation. A new ordinary EXG matrix adds 88,704 scenarios / +10,584 independently checked combinations across eight profiles. It separates +all DD/AA/AD register bindings from eight value boundaries/all CCRs, including +self aliases, A7 and every supported active stack bank. Full register values, +unchanged flags, exact PC, untouched registers/banks/memory, absence of operand +bus transfers and a following NOP are checked. Three fixed examples validate +fixture encoding against M68000PM EXG 4-105. + +Fresh and frozen qualification pass all eleven named tests without skips. +Word-truncation and lost-latched-input mutations fail both the old fact and +complete 68000 replacement before retirement, and the replacement afterward. +Only `ExgAddressRegistersSwapsFullLongValues` is retired, mapped to +`68000/EXG.L/AA/bank=ISP/r6->r2/boundaries/pair=0/op=C54E/ccr=00`. +The replacement uses the same opcode/values and stronger preservation checks. +Twelve integrity controls reject their intended errors. CPU source restores +byte-for-byte, and normal CPU/test assemblies remain unchanged. + +Ordinary requirements become 85,623,958 scenarios / 693 reports. The preceding +full run passes 5,129 tests / zero failures / nineteen unavailable tests and +validates its 85,535,254 scenarios / 685 reports, including nine pinned WinUAE +selections. Later EXG tests execute separately in isolated outputs. Combined +report validation is not a fresh broad run of the expanded test source. +See the [qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#exg-wide-register-qualification-and-consolidation--2026-10-06) +for scoped identities, exact retirement proof and rejected preliminary evidence. +Specialized hardware/timing/prefetch/bus/JIT/native regressions remain. No CPU +behavior, timing policy, public API or package changes are made in this slice. +The complete-040 inventory still retains 480 untested cases, and all broader +restoration, reference disagreements and consolidation requirements remain. +Milestone 6 stays **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-exg-mutations.ps1 b/scripts/test-copper68k-exg-mutations.ps1 new file mode 100644 index 00000000..8df065a4 --- /dev/null +++ b/scripts/test-copper68k-exg-mutations.ps1 @@ -0,0 +1,127 @@ +[CmdletBinding()] +param([switch]$ValidateReportsOnly,[switch]$IncludeHistoricalRegression,[string]$OutputDirectory='artifacts/exg-mutations') +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +$source=Join-Path $repo 'Copper68k/M68kCore.cs' +$mutations=@( + @{name='word-truncation';first='SetAddressRegister(left, State.A[right] & 0xffff);';second='SetAddressRegister(right, value & 0xffff);';mismatching=2096;passing=6352}, + @{name='lost-latched-input';first='SetAddressRegister(left, State.A[right]);';second='SetAddressRegister(right, State.A[left]);';mismatching=1568;passing=6880} +) +function Identity([string]$Path){ + $file=Join-Path $repo $Path + if(-not(Test-Path -LiteralPath $file -PathType Leaf)){throw "Missing EXG mutation input: $Path"} + return @{path=$Path;sha256=(Get-FileHash -LiteralPath $file).Hash} +} +function Validate-Selection($Actual,[string[]]$Required){ + $paths=@($Actual.path) + if($paths.Count -ne $Required.Count -or @($paths|Select-Object -Unique).Count -ne $paths.Count -or + @(Compare-Object ($paths|Sort-Object) ($Required|Sort-Object)).Count){throw 'Incomplete/duplicate EXG mutation identity selection'} + foreach($entry in $Actual){if($entry.sha256 -cnotmatch '^[A-F0-9]{64}$' -or (Identity $entry.path).sha256 -cne $entry.sha256){throw "Changed EXG mutation evidence: $($entry.path)"}} +} +function Mutated-Source([string]$Text,$Mutation){ + $start=$Text.IndexOf('if ((opcode & 0xF1F8) == 0xC148)') + $end=$Text.IndexOf('if ((opcode & 0xF1F8) == 0xC188)',$start) + if($start -lt 0 -or $end -le $start){throw 'Missing AA exchange scope'} + $scope=$Text.Substring($start,$end-$start) + foreach($anchor in @('SetAddressRegister(left, State.A[right]);','SetAddressRegister(right, value);')){ + if(($scope.Split($anchor).Length-1) -ne 1){throw 'Nonunique EXG mutation anchor'} + } + $scope=$scope.Replace('SetAddressRegister(left, State.A[right]);',$Mutation.first).Replace('SetAddressRegister(right, value);',$Mutation.second) + return $Text.Substring(0,$start)+$scope+$Text.Substring($end) +} +function Restore-Source([byte[]]$Bytes,[string]$ExpectedHash,[string]$OriginalHash){ + $current=(Get-FileHash -LiteralPath $source).Hash + if($current -ceq $OriginalHash){return} + if($current -cne $ExpectedHash){throw 'CPU source changed concurrently; current file left intact'} + [IO.File]::WriteAllBytes($source,$Bytes) +} +$saved=[Environment]::GetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR') +Push-Location $repo +try { + $inputs=@(@(& git ls-files Copper68k)+@('Copper68k.Tests/Copper68k.Tests.csproj', + 'Copper68k.Tests/Synthetic/SyntheticExgRegisterTests.cs','Copper68k.Tests/Synthetic/SyntheticMachine.cs', + 'Copper68k.Tests/Synthetic/ArchitecturalExpectation.cs','Copper68k.Tests/Synthetic/SyntheticExecution.cs', + 'Copper68k.Tests/Synthetic/SyntheticMoveTests.cs','scripts/test-copper68k-exg-mutations.ps1')|Sort-Object -Unique) + if($IncludeHistoricalRegression){$inputs+='Copper68k.Tests/M68kInterpreterCoreBehaviorTests.cs'} + $normal=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll'|Where-Object{Test-Path -LiteralPath (Join-Path $repo $_)}) + if(-not $ValidateReportsOnly){ + if(Test-Path -LiteralPath $output){throw 'Use a fresh EXG mutation directory'} + New-Item -ItemType Directory -Path $output|Out-Null + $original=@($inputs|ForEach-Object{Identity $_});$originalNormal=@($normal|ForEach-Object{Identity $_}) + $bytes=[IO.File]::ReadAllBytes($source);$text=[IO.File]::ReadAllText($source) + $sourceHash=(Get-FileHash -LiteralPath $source).Hash;$expectedSourceHash=$sourceHash + $rows=@() + try { + foreach($mutation in $mutations){ + $directory=Join-Path $output $mutation.name + New-Item -ItemType Directory -Path $directory|Out-Null + $mutated=Mutated-Source $text $mutation + [IO.File]::WriteAllText((Join-Path $directory 'mutated-M68kCore.cs'),$mutated) + if((Get-FileHash -LiteralPath $source).Hash -cne $sourceHash){throw 'CPU source changed before EXG mutation'} + [IO.File]::WriteAllText($source,$mutated);$expectedSourceHash=(Get-FileHash -LiteralPath $source).Hash + [Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$directory) + $filter='(FullyQualifiedName~SyntheticExgRegisterTests.WideValuesAliasesAndActiveStackBanks&DisplayName~68000)' + if($IncludeHistoricalRegression){$filter+='|FullyQualifiedName~ExgAddressRegistersSwapsFullLongValues'} + & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path (Join-Path $directory 'build') --filter $filter --logger 'trx;LogFileName=mutation.trx' --results-directory $directory *> (Join-Path $directory 'run.log') + $code=$LASTEXITCODE + Restore-Source $bytes $expectedSourceHash $sourceHash + if($code -ne 1){throw "EXG mutation did not fail: $($mutation.name)"} + $paths=@('mutation.trx','run.log','mutated-M68kCore.cs','68000-transfer-exg-wide.json', + 'build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'| + ForEach-Object{[IO.Path]::GetRelativePath($repo,(Join-Path $directory $_))}) + $rows+=@{name=$mutation.name;exit=$code;evidence=@($paths|ForEach-Object{Identity $_})} + } + } finally {Restore-Source $bytes $expectedSourceHash $sourceHash} + foreach($entry in @($original)+@($originalNormal)){if((Identity $entry.path).sha256 -cne $entry.sha256){throw 'EXG mutation changed original source/binaries'}} + @{schema=1;profile='integer-exg-retirement-proofs';includeHistoricalRegression=[bool]$IncludeHistoricalRegression; + sourceRevision=(& git rev-parse HEAD);sourceRestored=$true;inputs=$original;normalBinaries=$originalNormal;mutations=$rows}| + ConvertTo-Json -Depth 10|Set-Content (Join-Path $output 'identity.json') + } + $id=Get-Content (Join-Path $output 'identity.json') -Raw|ConvertFrom-Json + if($id.schema -ne 1 -or $id.profile -cne 'integer-exg-retirement-proofs' -or $id.includeHistoricalRegression -ne [bool]$IncludeHistoricalRegression -or $id.sourceRestored -ne $true -or $id.sourceRevision -cnotmatch '^[a-f0-9]{40}$'){throw 'Incomplete EXG mutation profile'} + Validate-Selection @($id.inputs) $inputs;Validate-Selection @($id.normalBinaries) $normal + if($id.mutations.Count -ne 2 -or @($id.mutations.name|Select-Object -Unique).Count -ne 2){throw 'Incomplete/duplicate EXG mutations'} + $text=[IO.File]::ReadAllText($source) + foreach($mutation in $mutations){ + $row=@($id.mutations|Where-Object name -CEQ $mutation.name) + if($row.Count -ne 1 -or $row[0].exit -ne 1){throw 'Missing executed EXG mutation'} + $directory=Join-Path $output $mutation.name + $paths=@('mutation.trx','run.log','mutated-M68kCore.cs','68000-transfer-exg-wide.json', + 'build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'| + ForEach-Object{[IO.Path]::GetRelativePath($repo,(Join-Path $directory $_))}) + Validate-Selection @($row[0].evidence) $paths + if([IO.File]::ReadAllText((Join-Path $directory 'mutated-M68kCore.cs')) -cne (Mutated-Source $text $mutation)){throw 'Wrong EXG mutation source scope'} + $names=@('Copper68k.Tests.Synthetic.SyntheticExgRegisterTests.WideValuesAliasesAndActiveStackBanks(modelId: "68000")') + if($IncludeHistoricalRegression){$names+='Copper68k.Tests.M68kInterpreterCoreBehaviorTests.ExgAddressRegistersSwapsFullLongValues'} + [xml]$trx=Get-Content (Join-Path $directory 'mutation.trx') -Raw + $c=$trx.TestRun.ResultSummary.Counters;$results=@($trx.TestRun.Results.UnitTestResult) + if($c.total -ne $names.Count -or $c.executed -ne $names.Count -or $c.failed -ne $names.Count -or $c.passed -ne 0 -or + $results.Count -ne $names.Count -or @($results|Where-Object outcome -CNE Failed).Count -ne 0 -or + @(Compare-Object ($results.testName|Sort-Object) ($names|Sort-Object)).Count -ne 0){throw 'Incomplete EXG mutation execution'} + $r=Get-Content (Join-Path $directory '68000-transfer-exg-wide.json') -Raw|ConvertFrom-Json -AsHashtable + if($r.schema -ne 1 -or $r.model -cne '68000' -or $r.group -cne 'transfer-exg-wide' -or $r.logicalCases -ne 8448 -or + $r.xunitBatches -ne 1 -or $r.counts.mismatching -ne $mutation.mismatching -or $r.counts.passing -ne $mutation.passing -or + $r.counts.unsupported -ne 0 -or $r.counts.untested -ne 0 -or $r.combinations.Count -ne 1008 -or $r.failures.Count -ne $mutation.mismatching){throw 'Incomplete EXG mutation coverage'} + foreach($bank in @('user','ISP')){foreach($family in @('DD','AA','AD')){ + foreach($scenario in @('registers','boundaries')){ + $bindings=if($scenario -ceq 'registers'){@(foreach($from in 0..7){foreach($to in 0..7){"${from}:$to"}})}else{@('6:2','2:2','7:3','2:7','7:7')} + $pairs=if($scenario -ceq 'registers'){0..1}else{0..7};$weight=if($scenario -ceq 'registers'){1}else{32} + foreach($binding in $bindings){$from,$to=$binding.Split(':');foreach($pair in $pairs){ + $bad=$family -ceq 'AA' -and $(if($mutation.name -ceq 'lost-latched-input'){ $from -cne $to -and $pair -ne 7 } + elseif($from -ceq $to){$pair -in @(1,2,4,6)}else{$pair -ne 7}) + $kind=if($bad){'mismatching'}else{'passing'} + $key="68000/EXG.L/$family/bank=$bank/r$from->r$to/$scenario/pair=$pair" + if(-not $r.combinations.Contains($key) -or $r.combinations[$key].Count -ne 1 -or $r.combinations[$key][$kind] -ne $weight){throw "Wrong EXG mutation combination: $key"} + }} + } + }} + $witness='68000/EXG.L/AA/bank=ISP/r6->r2/boundaries/pair=0/op=C54E/ccr=00' + if(@($r.failures|Where-Object{$_.id -ceq $witness -and $_.status -ceq 'mismatching'}).Count -ne 1){throw 'Historical EXG witness missing'} + } + @{schema=1;mutationProofs=2;includeHistoricalRegression=[bool]$IncludeHistoricalRegression;sourceRestored=$true; + witness='68000/EXG.L/AA/bank=ISP/r6->r2/boundaries/pair=0/op=C54E/ccr=00';roadmapComplete=$false}| + ConvertTo-Json -Depth 6|Set-Content (Join-Path $output 'summary.json') + Write-Host "Both EXG mutations detect the complete replacement and historical witness; includeHistorical=$IncludeHistoricalRegression" +} finally {[Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$saved);Pop-Location} +$global:LASTEXITCODE=0 diff --git a/scripts/test-copper68k-exg.ps1 b/scripts/test-copper68k-exg.ps1 new file mode 100644 index 00000000..26517800 --- /dev/null +++ b/scripts/test-copper68k-exg.ps1 @@ -0,0 +1,91 @@ +[CmdletBinding()] +param([switch]$ValidateReportsOnly,[switch]$IncludeHistoricalRegression,[string]$OutputDirectory='artifacts/exg-wide') +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +$models=@('68000','68010','68EC020','68020','68030','68040','68060','A1200') +function Identity([string]$Path){ + $file=Join-Path $repo $Path + if(-not(Test-Path -LiteralPath $file -PathType Leaf)){throw "Missing EXG input: $Path"} + return @{path=$Path;sha256=(Get-FileHash -LiteralPath $file).Hash} +} +function Validate-Selection($Actual,[string[]]$Required){ + $paths=@($Actual.path) + if($paths.Count -ne $Required.Count -or @($paths|Select-Object -Unique).Count -ne $paths.Count -or + @(Compare-Object ($paths|Sort-Object) ($Required|Sort-Object)).Count){throw 'Incomplete/duplicate EXG identity selection'} + foreach($entry in $Actual){if($entry.sha256 -cnotmatch '^[A-F0-9]{64}$' -or (Identity $entry.path).sha256 -cne $entry.sha256){throw "Changed EXG input: $($entry.path)"}} +} +function Expected-Keys([string]$Model){ + $keys=[Collections.Generic.Dictionary[string,int]]::new([StringComparer]::Ordinal) + $banks=if($Model -in @('68EC020','68020','68030','68040','A1200')){@('user','ISP','MSP')}else{@('user','ISP')} + foreach($bank in $banks){foreach($family in @('DD','AA','AD')){ + foreach($source in 0..7){foreach($destination in 0..7){foreach($pair in 0..1){ + if(-not $keys.TryAdd("$Model/EXG.L/$family/bank=$bank/r$source->r$destination/registers/pair=$pair",1)){throw 'Duplicate EXG register key'} + }}} + foreach($registers in @('6:2','2:2','7:3','2:7','7:7')){foreach($pair in 0..7){ + $source,$destination=$registers.Split(':') + if(-not $keys.TryAdd("$Model/EXG.L/$family/bank=$bank/r$source->r$destination/boundaries/pair=$pair",32)){throw 'Duplicate EXG value key'} + }} + }} + return ,$keys +} +$saved=[Environment]::GetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR') +Push-Location $repo +try { + $inputs=@(@(& git ls-files Copper68k)+@('Copper68k.Tests/Copper68k.Tests.csproj', + 'Copper68k.Tests/Synthetic/SyntheticExgRegisterTests.cs','Copper68k.Tests/Synthetic/SyntheticMachine.cs', + 'Copper68k.Tests/Synthetic/ArchitecturalExpectation.cs','Copper68k.Tests/Synthetic/SyntheticExecution.cs', + 'Copper68k.Tests/Synthetic/SyntheticMoveTests.cs','scripts/test-copper68k-exg.ps1')| + Sort-Object -Unique) + if($IncludeHistoricalRegression){$inputs+='Copper68k.Tests/M68kInterpreterCoreBehaviorTests.cs'} + $binaries=@('build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'| + ForEach-Object{[IO.Path]::GetRelativePath($repo,(Join-Path $output $_))}) + $evidence=@(@($models|ForEach-Object{"$_-transfer-exg-wide.json"})+@('exg.trx','run.log')| + ForEach-Object{[IO.Path]::GetRelativePath($repo,(Join-Path $output $_))}) + if(-not $ValidateReportsOnly){ + if(Test-Path -LiteralPath $output){throw 'Use a fresh EXG output directory'} + New-Item -ItemType Directory -Path $output|Out-Null + $original=@($inputs|ForEach-Object{Identity $_}) + [Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$output) + $filter='FullyQualifiedName~SyntheticExgRegisterTests' + if($IncludeHistoricalRegression){$filter+='|FullyQualifiedName~ExgAddressRegistersSwapsFullLongValues'} + & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path (Join-Path $output 'build') --filter $filter --logger 'trx;LogFileName=exg.trx' --results-directory $output *> (Join-Path $output 'run.log') + if($LASTEXITCODE -ne 0){throw "EXG execution failed: $output/run.log"} + foreach($entry in $original){if((Identity $entry.path).sha256 -cne $entry.sha256){throw 'EXG source changed during execution'}} + @{schema=1;profile='integer-exg-wide';includeHistoricalRegression=[bool]$IncludeHistoricalRegression; + sourceRevision=(& git rev-parse HEAD);inputs=$original;binaries=@($binaries|ForEach-Object{Identity $_}); + evidence=@($evidence|ForEach-Object{Identity $_})}|ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'identity.json') + } + $id=Get-Content (Join-Path $output 'identity.json') -Raw|ConvertFrom-Json + if($id.schema -ne 1 -or $id.profile -cne 'integer-exg-wide' -or $id.includeHistoricalRegression -ne [bool]$IncludeHistoricalRegression -or $id.sourceRevision -cnotmatch '^[a-f0-9]{40}$'){throw 'Incomplete EXG identity profile'} + Validate-Selection @($id.inputs) $inputs + Validate-Selection @($id.binaries) $binaries + Validate-Selection @($id.evidence) $evidence + $prefix='Copper68k.Tests.Synthetic.SyntheticExgRegisterTests.' + $names=@($models|ForEach-Object{$prefix+'WideValuesAliasesAndActiveStackBanks(modelId: "'+$_+'")'})+ + @(($prefix+'FixtureEncodingMatchesFixedExamples(kind: 72, source: 6, destination: 2, expected: 50510)'), + ($prefix+'FixtureEncodingMatchesFixedExamples(kind: 64, source: 0, destination: 0, expected: 49472)'), + ($prefix+'FixtureEncodingMatchesFixedExamples(kind: 136, source: 7, destination: 3, expected: 51087)')) + if($IncludeHistoricalRegression){$names+='Copper68k.Tests.M68kInterpreterCoreBehaviorTests.ExgAddressRegistersSwapsFullLongValues'} + [xml]$trx=Get-Content (Join-Path $output 'exg.trx') -Raw + $c=$trx.TestRun.ResultSummary.Counters;$results=@($trx.TestRun.Results.UnitTestResult) + if($c.total -ne $names.Count -or $c.executed -ne $names.Count -or $c.passed -ne $names.Count -or $c.failed -ne 0 -or + $results.Count -ne $names.Count -or @($results|Where-Object outcome -CNE Passed).Count -ne 0 -or + @(Compare-Object ($results.testName|Sort-Object) ($names|Sort-Object)).Count -ne 0){throw 'Incomplete EXG test selection'} + $coverage=@() + foreach($model in $models){ + $keys=Expected-Keys $model + $cases=if($model -in @('68000','68010','68060')){8448}else{12672} + $r=Get-Content (Join-Path $output "$model-transfer-exg-wide.json") -Raw|ConvertFrom-Json -AsHashtable + if($r.schema -ne 1 -or $r.model -cne $model -or $r.group -cne 'transfer-exg-wide' -or $r.xunitBatches -ne 1 -or + $r.logicalCases -ne $cases -or $r.counts.passing -ne $cases -or $r.counts.mismatching -ne 0 -or + $r.counts.unsupported -ne 0 -or $r.counts.untested -ne 0 -or $r.failures.Count -ne 0 -or $r.combinations.Count -ne $keys.Count){throw "Incomplete EXG coverage: $model"} + foreach($key in $keys.Keys){if(-not $r.combinations.Contains($key) -or $r.combinations[$key].Count -ne 1 -or $r.combinations[$key].passing -ne $keys[$key]){throw "Missing/misweighted EXG combination: $key"}} + $coverage+=@{model=$model;logicalCases=$cases;combinations=$keys.Count} + } + @{schema=1;logicalCases=88704;xunitBatches=8;fixedExamples=3;coverage=$coverage;roadmapComplete=$false; + reference='M68000PM EXG, 4-105';scope='Architectural long register values, all bindings, CCR, aliases and active stack banks; following NOP; no timing qualification'}| + ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'summary.json') + Write-Host "EXG wide-register gate: 88704 passing scenarios / 10584 combinations across eight profiles; $output" +} finally {[Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$saved);Pop-Location} +$global:LASTEXITCODE=0 diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 1568e0ac..c51e4758 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -65,6 +65,7 @@ try { 'move-register-overlap'=$(if ($model -in @('68000','68010')) {1801} else {2593}); 'move-address-boundary'=$(if ($model -in @('68000','68010')) {44} else {60}); 'transfer-registers'=17624; 'transfer-addresses'=$(if ($model -in @('68000','68010')) {8064} else {9252}); + 'transfer-exg-wide'=$(if ($model -in @('68000','68010','68060')) {8448} else {12672}); 'transfer-moveq-unassigned-words'=131072; 'transfer-movep'=3708; 'transfer-movem'=$(if ($model -in @('68000','68010')) {3196} else {3592}); 'transfer-movem-invalid-operands'=6400; From 6ad4db6063370268b7ed5227f8f32734d5c2ba63 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 18:38:36 +0300 Subject: [PATCH 071/211] Qualify supplied 68040 writeback handler execution --- Copper68k.Tests/Synthetic/README.md | 35 ++- .../SyntheticM68040AccessFrameAuditTests.cs | 4 +- .../SyntheticM68040WritebackProgram.cs | 150 +++++++++++ .../SyntheticM68040WritebackTests.cs | 241 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 106 ++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 33 +++ scripts/test-copper68k-040-access-frames.ps1 | 40 ++- ...test-copper68k-040-writeback-mutations.ps1 | 110 ++++++++ scripts/test-copper68k-040-writebacks.ps1 | 99 +++++++ scripts/test-copper68k-synthetic.ps1 | 3 + 10 files changed, 810 insertions(+), 11 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040WritebackProgram.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040WritebackTests.cs create mode 100644 scripts/test-copper68k-040-writeback-mutations.ps1 create mode 100644 scripts/test-copper68k-040-writebacks.ps1 diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index f357c72a..3d3a4b5c 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -139,11 +139,44 @@ where that method is absent. The exact retirement witness and before/after proofs are recorded in the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#exg-wide-register-qualification-and-consolidation--2026-10-06). -The expanded ordinary gate requires **85,623,958 scenarios / 693 reports**. +At the EXG checkpoint the ordinary gate required **85,623,958 scenarios / 693 reports**. The preceding full CPU run and this scoped EXG execution have separate source and assembly identities; combined report validation does not imply a fresh full-suite execution of the expanded source. Milestone 6 remains incomplete. +### Supplied 040 write-back handler + +Four `rte-writeback-{canonical|structure}-{scalar|batch}` groups add **147,456 +whole-program scenarios / 39,168 combinations**. A real integer handler saves +scratch registers, reads validity/size/function-code fields, aligns WB1 data, +completes WB1→WB2→WB3 using MOVES, clears each valid bit, restores DFC/registers +and executes RTE. Every instruction is checked, including untouched state and +memory, operand writes, invalid-slot skips, restored stacks and following trace. +Bounded witnesses expose a missing lane rotation and reversed stage order. + +```powershell +./scripts/test-copper68k-040-writebacks.ps1 -OutputDirectory artifacts/040-writebacks +./scripts/test-copper68k-040-writebacks.ps1 -ValidateReportsOnly -OutputDirectory artifacts/040-writebacks +./scripts/test-copper68k-040-writeback-mutations.ps1 -OutputDirectory artifacts/040-writeback-mutations +./scripts/test-copper68k-040-writeback-mutations.ps1 -ValidateReportsOnly -OutputDirectory artifacts/040-writeback-mutations +``` + +The mutation command temporarily modifies the test handler program, builds in +isolated outputs and restores original bytes. Do not edit that program or build +from it concurrently. It verifies a passing baseline, exact failure methods and +reasons, source/input/binary/evidence identities and preservation of normal +assemblies. These are handler-fixture proofs; no CPU defect or retirement is +claimed. The positive command independently validates all keys and weights. + +Frames are supplied inputs consistent with MC68040UM table 8-6. No real data +fault or enabled-MMU operation creates them. Cache pushes/MOVE16 lines, nested +WB2/3 faults, actual data-fault construction and physical function-code spaces +remain unqualified. The complete-040 gate retains all 480 remaining cases and +now requires 67,793,440 cases / 86 reports and 24 fixed controls/witnesses. The +ordinary gate requires **85,771,414 scenarios / 697 reports**. Combined frozen +report validation is separate from fresh broad execution. See the +[qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-supplied-write-back-handler-qualification--2026-10-06). + ## Seeded and external audits ```powershell diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index cb2a00e0..02a34ccf 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -38,7 +38,9 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() ? "Cache-disabled accurate instruction-fetch frames/restart are covered separately; data fault/writeback restart, enabled-cache deferral and compiled instruction-PC provenance remain required" : form == "odd-PC-chained-SR-provenance" ? "Chained format0/2/3 and format7 normal/CM SR handoff, stack consumption and header reads have separate generated WinUAE software coverage; access-frame validation/no-replay checks are synthetic. Separate composed-entry and executed-MMU-entry CM lifetime discovery profiles disagree after odd-PC fault/repair; the executed profile also exposes stacked-SR disagreement. Neither qualifies the full run loop or hardware. CT/CU/CP pending/foreign-context chains, full reference exception qualification and hardware behavior remain unqualified" - : "Required fault/context protocol has no independently qualified execution fixture yet"); + : form == "writeback-handler" + ? "Executed normal B/W/L supplied-frame handler separately covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks; nested WB2/3 faults, cache-push/MOVE16 line cleanup and actual data-fault construction remain required. Physical function-code spaces and hardware timing are unqualified" + : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040WritebackProgram.cs b/Copper68k.Tests/Synthetic/SyntheticM68040WritebackProgram.cs new file mode 100644 index 00000000..029fa375 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040WritebackProgram.cs @@ -0,0 +1,150 @@ +namespace Copper68k.Tests.Synthetic; + +// Integer exception-handler program for supplied normal format-7 writebacks. +// MC68040UM 8.4.6.3/5/7: WB1 is memory aligned, WB2/3 register aligned; +// software completes WB1 -> WB2 -> WB3. This does not generate a data fault. +internal sealed class SyntheticM68040WritebackProgram +{ + internal sealed record Instruction(uint Pc, ushort[] Words, string Operation, int Slot = 0, string? Target = null); + internal const uint Entry = SyntheticMachine.Code; + internal readonly List Instructions = []; + internal readonly Dictionary Labels = []; + private uint pc = Entry; + internal static int StatusOffset(int slot) => 20 - 2 * slot; + internal static int AddressOffset(int slot) => 48 - 8 * slot; + internal static int DataOffset(int slot) => AddressOffset(slot) + 4; + + internal SyntheticM68040WritebackProgram() + { + Emit("save", 0, [0x48e7, 0xf0c0]); // MOVEM.L D0-D3/A0-A1,-(A7) + Emit("frame", 0, [0x43ef, 24]); // LEA 24(A7),A1 + Emit("save-DFC", 0, [0x4e7a, 0x3001]); + for (var slot = 1; slot <= 3; slot++) + { + Emit("zero-status", slot, [0x7400]); + Emit("status", slot, [0x1429, (ushort)(StatusOffset(slot) + 1)]); + Emit("valid", slot, [0x0802, 7]); + Branch("invalid", slot, $"next{slot}", 0x6700); + Emit("data", slot, [0x2029, (ushort)DataOffset(slot)]); + Emit("address", slot, [0x2069, (ushort)AddressOffset(slot)]); + Emit("zero-DFC", slot, [0x7200]); + Emit("DFC-byte", slot, [0x1229, (ushort)(StatusOffset(slot) + 1)]); + Emit("DFC-mask", slot, [0x0281, 0, 7]); + Emit("DFC-write", slot, [0x4e7b, 0x1001]); + if (slot == 1) + { + Emit("lane-address", slot, [0x2208]); + Emit("lane-mask", slot, [0x0281, 0, 3]); + Emit("lane-count", slot, [0xe789]); // LSL.L #3,D1 + Emit("lane-rotate", slot, [0xe3b8]); // ROL.L D1,D0 + } + Emit("size", slot, [0x0282, 0, 0x60]); + Branch("long-branch", slot, $"long{slot}", 0x6700); + Emit("byte-compare", slot, [0x0c82, 0, 0x20]); + Branch("byte-branch", slot, $"byte{slot}", 0x6700); + if (slot == 1) Emit("word-align", slot, [0x4840]); // SWAP D0 + Emit("store-word", slot, [0x0e50, 0x0800]); // MOVES.W D0,(A0) + Branch("stored", slot, $"clear{slot}", 0x6000); + Mark($"byte{slot}"); + if (slot == 1) Emit("byte-align", slot, [0xe198]); // ROL.L #8,D0 + Emit("store-byte", slot, [0x0e10, 0x0800]); + Branch("stored", slot, $"clear{slot}", 0x6000); + Mark($"long{slot}"); + Emit("store-long", slot, [0x0e90, 0x0800]); + Mark($"clear{slot}"); + Emit("clear-valid", slot, [0x08a9, 7, (ushort)(StatusOffset(slot) + 1)]); + Mark($"next{slot}"); + } + Emit("restore-DFC", 0, [0x4e7b, 0x3001]); + Emit("restore", 0, [0x4cdf, 0x030f]); + Emit("return", 0, [0x4e73]); + foreach (var instruction in Instructions.Where(i => i.Target != null)) + { + var displacement = checked((short)(Labels[instruction.Target!] - (long)(instruction.Pc + 2))); + instruction.Words[1] = unchecked((ushort)displacement); + } + } + private void Emit(string operation, int slot, ushort[] words, string? target = null) + { Instructions.Add(new(pc, words, operation, slot, target)); pc += (uint)words.Length * 2; } + private void Branch(string operation, int slot, string target, ushort opcode) => Emit(operation, slot, [opcode, 0], target); + private void Mark(string label) => Labels.Add(label, pc); + internal void Initialize(SyntheticMachine m) + { + foreach (var instruction in Instructions) + for (var n = 0; n < instruction.Words.Length; n++) + m.InitializePhysical(instruction.Pc + (uint)n * 2, instruction.Words[n], 2); + } + + internal void Expect(Instruction i, SyntheticMachine m, ArchitecturalExpectation e, + uint frame, ushort[] statuses, uint[] addresses, uint[] data, uint[] originalD, uint[] originalA, uint originalDfc, + Dictionary stacks, List<(uint Address, int Width, uint Value)> stores) + { + e.Pc = i.Pc + (uint)i.Words.Length * 2; + var slot = i.Slot - 1; + uint value; + switch (i.Operation) + { + case "save": + M68040StackFixture.SetStacks(e, stacks, e.Sr); + stacks[M68040StackFixture.ExceptionBank(e.Sr)] = frame - 24; + M68040StackFixture.SetStacks(e, stacks, e.Sr); + uint[] saved = [originalD[0], originalD[1], originalD[2], originalD[3], originalA[0], originalA[1]]; + for (var n = 0; n < saved.Length; n++) e.Write(frame - 24 + (uint)n * 4, saved[n], 4, m.Model); + break; + case "frame": e.A[1] = frame; break; + case "save-DFC": e.D[3] = originalDfc; break; + case "zero-status": e.D[2] = 0; Flags(e, 0, 4); break; + case "status": e.D[2] = statuses[slot]; Flags(e, e.D[2], 1); break; + case "valid": e.Sr = (ushort)((e.Sr & ~4) | ((statuses[slot] & 0x80) == 0 ? 4 : 0)); break; + case "invalid": if ((statuses[slot] & 0x80) == 0) e.Pc = Labels[i.Target!]; break; + case "data": e.D[0] = data[slot]; Flags(e, e.D[0], 4); break; + case "address": e.A[0] = addresses[slot]; break; + case "zero-DFC": e.D[1] = 0; Flags(e, 0, 4); break; + case "DFC-byte": e.D[1] = statuses[slot]; Flags(e, e.D[1], 1); break; + case "DFC-mask": e.D[1] &= 7; Flags(e, e.D[1], 4); break; + case "DFC-write": e.ControlChecks["DFC"] = (s => s.DestinationFunctionCode, e.D[1]); break; + case "lane-address": e.D[1] = addresses[slot]; Flags(e, e.D[1], 4); break; + case "lane-mask": e.D[1] &= 3; Flags(e, e.D[1], 4); break; + case "lane-count": e.D[1] <<= 3; e.Sr &= 0xffef; Flags(e, e.D[1], 4); break; + case "lane-rotate": + var count = (int)e.D[1]; e.D[0] = RotateBytes(e.D[0], count / 8); + Flags(e, e.D[0], 4); if (count != 0 && (e.D[0] & 1) != 0) e.Sr |= 1; + break; + case "size": e.D[2] &= 0x60; Flags(e, e.D[2], 4); break; + case "long-branch": if ((statuses[slot] & 0x60) == 0) e.Pc = Labels[i.Target!]; break; + case "byte-compare": Flags(e, e.D[2] - 0x20, 4); break; + case "byte-branch": if ((statuses[slot] & 0x60) == 0x20) e.Pc = Labels[i.Target!]; break; + case "word-align": e.D[0] = (e.D[0] << 16) | (e.D[0] >> 16); Flags(e, e.D[0], 4); break; + case "byte-align": e.D[0] = RotateBytes(e.D[0], 1); Flags(e, e.D[0], 4); e.Sr |= (ushort)(e.D[0] & 1); break; + case "store-byte": case "store-word": case "store-long": + var width = i.Operation == "store-byte" ? 1 : i.Operation == "store-word" ? 2 : 4; + value = e.D[0] & (width == 1 ? 0xffu : width == 2 ? 0xffffu : uint.MaxValue); + e.Write(addresses[slot], value, width, m.Model); stores.Add((addresses[slot], width, value)); + break; + case "stored": e.Pc = Labels[i.Target!]; break; + case "clear-valid": e.Write(frame + (uint)StatusOffset(i.Slot) + 1, (uint)(statuses[slot] & 0x7f), 1, m.Model); e.Sr &= 0xfffb; break; + case "restore-DFC": e.ControlChecks["DFC"] = (s => s.DestinationFunctionCode, originalDfc); break; + case "restore": + Array.Copy(originalD, e.D, 4); Array.Copy(originalA, e.A, 2); + stacks[M68040StackFixture.ExceptionBank(e.Sr)] = frame; + M68040StackFixture.SetStacks(e, stacks, e.Sr); break; + // RTE/trace are checked by the caller against the independently supplied header. + case "return": break; + default: throw new InvalidOperationException(i.Operation); + } + } + private static void Flags(ArchitecturalExpectation e, uint value, int width) + { + var mask = width == 1 ? 0xffu : width == 2 ? 0xffffu : uint.MaxValue; + var sign = width == 1 ? 0x80u : width == 2 ? 0x8000u : 0x80000000u; + value &= mask; + e.Sr = (ushort)((e.Sr & 0xfff0) | (value == 0 ? 4 : 0) | ((value & sign) != 0 ? 8 : 0)); + } + // Byte-array permutation, independent of the production rotate implementation. + private static uint RotateBytes(uint value, int lanes) + { + byte[] bytes = [(byte)(value >> 24), (byte)(value >> 16), (byte)(value >> 8), (byte)value]; + uint result = 0; for (var n = 0; n < 4; n++) result = result << 8 | bytes[(n + lanes) & 3]; + return result; + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040WritebackTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040WritebackTests.cs new file mode 100644 index 00000000..24f464c3 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040WritebackTests.cs @@ -0,0 +1,241 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticM68040WritebackTests(ITestOutputHelper output) +{ + private static readonly string[] Banks = ["user", "user-M", "ISP", "MSP"]; + private static readonly uint[] Values = [0, uint.MaxValue, 0x89abcdef, 0x12345678]; + private static readonly int[][] Patterns = [[1, 1, 1], [2, 2, 2], [4, 4, 4], [1, 2, 4]]; + private const uint Target = 0x6000, TraceHandler = 0x9090; + + [Fact, Trait("Suite", "Synthetic")] + public void CanonicalWritebacksAllLanesSizesValuesAndCcrScalar() => Audit(false, false); + [Fact, Trait("Suite", "Synthetic")] + public void CanonicalWritebacksAllLanesSizesValuesAndCcrBatch() => Audit(false, true); + [Fact, Trait("Suite", "Synthetic")] + public void WritebackValidityOrderOverlapAndRestoredTraceScalar() => Audit(true, false); + [Fact, Trait("Suite", "Synthetic")] + public void WritebackValidityOrderOverlapAndRestoredTraceBatch() => Audit(true, true); + + private void Audit(bool structural, bool batch) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); + var program = new SyntheticM68040WritebackProgram(); + var report = new CoverageBatch("68040", $"rte-writeback-{(structural ? "structure" : "canonical")}-{(batch ? "batch" : "scalar")}"); + foreach (var handlerBank in new[] { "ISP", "MSP" }) + foreach (var restoredBank in Banks) + foreach (var lane in Enumerable.Range(0, 4)) + { + if (!structural) + { + for (var slot = 1; slot <= 3; slot++) + foreach (var width in new[] { 1, 2, 4 }) + for (var pair = 0; pair < Values.Length; pair++) + for (var ccr = 0; ccr < 32; ccr++) + Case(m, program, report, batch, false, handlerBank, restoredBank, lane, 1 << (slot - 1), + [width, width, width], $"slot{slot}-size{width}", pair, ccr, 0, "distinct"); + } + else + { + for (var mask = 0; mask < 8; mask++) + for (var pattern = 0; pattern < Patterns.Length; pattern++) + foreach (var layout in new[] { "distinct", "same", "overlap" }) + foreach (var trace in new ushort[] { 0, 0x8000, 0x4000 }) + foreach (var pair in new[] { 2, 3 }) + foreach (var ccr in new[] { 0, 31 }) + Case(m, program, report, batch, true, handlerBank, restoredBank, lane, mask, + Patterns[pattern], $"pattern{pattern}", pair, ccr, trace, layout); + } + } + report.Complete(output); + } + + private static (string Status, string? Reason) Case(SyntheticMachine m, SyntheticM68040WritebackProgram program, CoverageBatch report, + bool batch, bool structural, string handlerBank, string restoredBank, int lane, int mask, int[] widths, + string sizes, int pair, int ccr, ushort trace, string layout) + { + var id = $"68040/RTE/writebacks/{(structural ? "structure" : "canonical")}/handler={handlerBank}/restore={restoredBank}" + + $"/lane={lane}/valid={mask}/{sizes}/layout={layout}/T={trace:X4}/pair={pair}/op=4E73/ccr={ccr:X2}"; + m.Reset(ccr); + var frame = (handlerBank == "ISP" ? 0x4700u : 0x7400u) + (uint)(structural ? lane & 1 : 0); + var stacks = new Dictionary { ["user"] = 0x7800, ["ISP"] = 0x4700, ["MSP"] = 0x7400 }; + stacks[handlerBank] = frame; + for (var n = -32; n < 80; n++) m.InitializePhysical(unchecked(frame + (uint)n), (uint)(n ^ 0xa5), 1); + var savedSr = (ushort)(0x0700 | M68040StackFixture.Status(restoredBank, trace, ccr ^ 31)); + m.InitializePhysical(frame, savedSr, 2); m.InitializePhysical(frame + 2, Target, 4); + m.InitializePhysical(frame + 6, 0x7008, 2); m.InitializePhysical(frame + 8, 0x12345678, 4); + for (var n = -4; n < 84; n++) m.InitializePhysical(unchecked(0x4200u + (uint)n), (uint)(n ^ 0x6a), 1); + var statuses = new ushort[3]; var addresses = new uint[3]; var encoded = new uint[3]; + var intended = new List<(uint Address, int Width, uint Value)>(); + for (var slot = 1; slot <= 3; slot++) + { + var index = slot - 1; + addresses[index] = 0x4200u + (uint)lane + (uint)(layout == "distinct" ? index * 24 : layout == "overlap" ? index : 0); + var value = Values[pair] ^ (uint)(index * 0x11111111); + var width = widths[index]; + var transfer = value & (width == 1 ? 0xffu : width == 2 ? 0xffffu : uint.MaxValue); + statuses[index] = (ushort)((mask & (1 << index)) != 0 + ? 0x80 | (width == 1 ? 0x20 : width == 2 ? 0x40 : 0) | (slot == 2 ? 1 : 5) : 0); + encoded[index] = slot == 1 ? MemoryAligned(transfer, width, (int)(addresses[index] & 3)) : value; + m.InitializePhysical(frame + (uint)SyntheticM68040WritebackProgram.StatusOffset(slot), statuses[index], 2); + m.InitializePhysical(frame + (uint)SyntheticM68040WritebackProgram.AddressOffset(slot), addresses[index], 4); + m.InitializePhysical(frame + (uint)SyntheticM68040WritebackProgram.DataOffset(slot), encoded[index], 4); + if ((mask & (1 << index)) != 0) intended.Add((addresses[index], width, transfer)); + } + // Table 8-6: normal physical write has valid WB1 and FA=WB1A; + // supplied write-page-fault images have WB2, while read images can + // have only WB3. No actual page fault/MMU operation is executed here. + var header = FaultHeader(statuses, addresses); + m.InitializePhysical(frame + 12, header.Ssw, 2); m.InitializePhysical(frame + 20, header.FaultAddress, 4); + program.Initialize(m); + m.InitializePhysical(Target, 0x60fe, 2); m.InitializePhysical(Target + 2, 0x7e7e, 2); + m.InitializePhysical(TraceHandler, 0x4e73, 2); m.InitializePhysical(TraceHandler + 2, 0x7e7e, 2); + m.InitializePhysical(36, TraceHandler, 4); + m.Start(); + m.Core.State.SetUserStackPointer(stacks["user"]); m.Core.State.SetInterruptStackPointer(stacks["ISP"]); + m.Core.State.SetMasterStackPointer(stacks["MSP"]); + m.Core.State.StatusRegister = (ushort)(0x0700 | M68040StackFixture.Status(handlerBank, 0, ccr)); + m.Core.State.DestinationFunctionCode = (uint)(ccr & 7); + m.Core.State.M68040Fpu.Fpcr = 0x60; m.Core.State.M68040Fpu.Fpsr = 0x12340000; m.Core.State.M68040Fpu.Fpiar = 0x87654321; + var e = ArchitecturalExpectation.Capture(m); + var sequence = m.Core.State.ExceptionSequence; + var originalD = (uint[])e.D.Clone(); var originalA = (uint[])e.A.Clone(); var originalDfc = m.Core.State.DestinationFunctionCode; + var stores = new List<(uint Address, int Width, uint Value)>(); + M68040StackFixture.SetStacks(e, stacks, e.Sr); + e.ControlChecks["SFC"] = (s => s.SourceFunctionCode, m.Core.State.SourceFunctionCode); + e.ControlChecks["DFC"] = (s => s.DestinationFunctionCode, originalDfc); + e.ControlChecks["FPCR preserved"] = (s => s.M68040Fpu.Fpcr, 0x60); + e.ControlChecks["FPSR preserved"] = (s => s.M68040Fpu.Fpsr, 0x12340000); + e.ControlChecks["FPIAR preserved"] = (s => s.M68040Fpu.Fpiar, 0x87654321); + e.ControlChecks["no handler exception"] = (s => s.ExceptionSequence, sequence); + e.ControlChecks["writeback order/width/value"] = (_ => m.Bus.Accesses + .Where(a => a.Write && a.Address is >= 0x4200 and < 0x4254) + .Select(a => (a.Address, a.Width, a.Value)).SequenceEqual(stores) ? 0u : 1u, 0); + for (var slot = 1; slot <= 3; slot++) + if ((mask & (1 << (slot - 1))) == 0) + for (var n = 0; n < 8; n++) e.ForbiddenOperandReads.Add(frame + (uint)SyntheticM68040WritebackProgram.AddressOffset(slot) + (uint)n); + var steps = 0; + string? failure = null; var status = "passing"; + try + { + while (e.Pc != Target) + { + if (++steps > 120) throw new InvalidOperationException("Handler did not terminate"); + var instruction = program.Instructions.Single(i => i.Pc == e.Pc); + program.Expect(instruction, m, e, frame, statuses, addresses, encoded, originalD, originalA, originalDfc, stacks, stores); + if (instruction.Operation == "return") + { stacks[handlerBank] = frame + 60; M68040StackFixture.SetStacks(e, stacks, savedSr); e.Pc = Target; } + failure = Execute(m, e, batch); + if (failure != null) { failure = $"{instruction.Operation}/WB{instruction.Slot}/PC={instruction.Pc:X8}: {failure}"; break; } + } + if (failure == null && !stores.SequenceEqual(intended)) failure = "Writeback results differ from independently intended operands"; + if (failure == null) + { + if (trace != 0) + { + var bank = M68040StackFixture.ExceptionBank(savedSr); stacks[bank] -= 12; var sp = stacks[bank]; + M68040StackFixture.SetStacks(e, stacks, (ushort)((savedSr | 0x2000) & 0x3fff)); + e.Write(sp, savedSr, 2, m.Model); e.Write(sp + 2, Target, 4, m.Model); + e.Write(sp + 6, 0x2024, 2, m.Model); e.Write(sp + 8, Target, 4, m.Model); + e.Pc = TraceHandler; e.ExceptionVector = 9; + e.ControlChecks["no handler exception"] = (s => s.ExceptionSequence, sequence + 1); + e.ControlChecks["trace saved SR"] = (s => s.LastExceptionStatusRegister, savedSr); + } + failure = Execute(m, e, batch); + if (failure != null) failure = "following BRA: " + failure; + if (failure == null && trace != 0) + { + stacks[M68040StackFixture.ExceptionBank(savedSr)] += 12; + M68040StackFixture.SetStacks(e, stacks, savedSr); e.Pc = Target; e.ExceptionVector = null; + failure = Execute(m, e, batch); if (failure != null) failure = "trace RTE: " + failure; + } + } + if (failure != null) status = "mismatching"; + } + catch (Exception ex) { failure = ex.ToString(); status = ex is UnsupportedM68kOpcodeException or UnsupportedM68kTimingException or UnsupportedM68040InstructionException ? "unsupported" : "mismatching"; } + report.Record(id, status, failure); + return (status, failure); + } + + private static (ushort Ssw, uint FaultAddress) FaultHeader(ushort[] statuses, uint[] addresses) + => (statuses[0] & 0x80) != 0 ? ((ushort)(statuses[0] & 0x67), addresses[0]) + : (statuses[1] & 0x80) != 0 ? ((ushort)(0x400 | (statuses[1] & 0x67)), addresses[1]) + : ((ushort)0x0105, 0x4300); + + [Theory] + [InlineData(1, 0x25, 0x4200u)] + [InlineData(2, 0x441, 0x4218u)] + [InlineData(4, 0x105, 0x4300u)] + [InlineData(0, 0x105, 0x4300u)] + public void SuppliedHeaderMatchesManualFrameCombinations(int mask, int ssw, uint faultAddress) + { + ushort[] statuses = [(ushort)((mask & 1) != 0 ? 0xa5 : 0), (ushort)((mask & 2) != 0 ? 0xc1 : 0), (ushort)((mask & 4) != 0 ? 0x85 : 0)]; + Assert.Equal(((ushort)ssw, faultAddress), FaultHeader(statuses, [0x4200, 0x4218, 0x4230])); + } + + [Fact] + public void HandlerCompletesMemoryAlignedByteAtLaneOne() + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); + var result = Case(m, new(), new("68040", "bounded-control"), false, false, "MSP", "user-M", 1, 1, + [1, 1, 1], "slot1-size1", 2, 31, 0, "distinct"); + Assert.True(result.Status == "passing", result.Reason); + } + + [Fact] + public void HandlerCompletesOverlappingMixedWritebacksInOrder() + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); + var result = Case(m, new(), new("68040", "bounded-control"), false, true, "MSP", "ISP", 3, 7, + [1, 2, 4], "pattern3", 3, 0, 0x4000, "overlap"); + Assert.True(result.Status == "passing", result.Reason); + } + + private static string? Execute(SyntheticMachine m, ArchitecturalExpectation e, bool batch) + { + if (!batch) m.Core.ExecuteInstruction(); + else + { + var boundary = new Boundary(); + var count = ((IM68kBatchCore)m.Core).ExecuteInstructions(1, m.Core.State.Cycles + 1000, boundary); + if (count != 1 || boundary.Before != 1 || boundary.After != 1) return "Batch count/callbacks differ"; + } + return e.Verify(m); + } + private sealed class Boundary : IM68kInstructionBoundary + { + internal int Before, After; + public bool BeforeInstruction() { Before++; return true; } + public void AfterInstruction(long previousCycle, long currentCycle) => After++; + } + + // Deposit independently chosen operand bytes in their bus lanes. Poison + // unused lanes, so reading WB1 as register aligned cannot accidentally pass. + private static uint MemoryAligned(uint value, int width, int lane) + { + byte[] bytes = [0xd1, 0xe2, 0xf3, 0x04]; + for (var n = 0; n < width; n++) bytes[(lane + n) & 3] = (byte)(value >> (8 * (width - n - 1))); + uint result = 0; foreach (var b in bytes) result = result << 8 | b; return result; + } + + [Theory] + [InlineData(0x78u, 1, 0, 0x78e2f304u)] + [InlineData(0x78u, 1, 3, 0xd1e2f378u)] + [InlineData(0x1234u, 2, 3, 0x34e2f312u)] + [InlineData(0x12345678u, 4, 1, 0x78123456u)] + public void MemoryAlignedFixtureMatchesManualExamples(uint value, int width, int lane, uint expected) + => Assert.Equal(expected, MemoryAligned(value, width, lane)); + + [Fact] + public void HandlerUsesFixedReferenceEncodings() + { + var program = new SyntheticM68040WritebackProgram(); + var prefix = program.Instructions.Take(6).SelectMany(i => i.Words).ToArray(); + Assert.Equal(new ushort[] { 0x48e7, 0xf0c0, 0x43ef, 24, 0x4e7a, 0x3001, 0x7400, 0x1429, 19, 0x0802, 7 }, prefix); + Assert.Equal(new ushort[] { 0xe3b8 }, program.Instructions.Single(i => i.Operation == "lane-rotate").Words); + Assert.Equal(new ushort[] { 0x4cdf, 0x030f }, program.Instructions.Single(i => i.Operation == "restore").Words); + Assert.Equal(new ushort[] { 0x4e73 }, program.Instructions.Last().Words); + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 50d296bb..0b368f78 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -6530,3 +6530,109 @@ No new consumer execution or package publication is needed for this test-only slice. The complete-040 inventory's 480 untested cases, all broader restoration and software disagreements, independent coverage and further consolidation remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +### 040 supplied write-back handler qualification — 2026-10-06 + +[MC68040UM](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +8.4.6.3/5/7, tables 8-5/6, define the frame's write-back validity, transfer size, +function code, alignment and handler obligations. The audited local manual +(`artifacts/mc68040um-1993-entry-audit.pdf`) SHA256 is +`93741393f70656941e413beb232c060a5e4e0218ea2adc2f5b392f9165454a7f`. +WB1 bytes occupy memory lanes; WB2/WB3 byte/word values occupy low register bits. +Software must complete pending stores in WB1→WB2→WB3 order before returning. +RTE does not perform these stores for the handler. + +`SyntheticM68040WritebackProgram` executes a generic integer handler: save +D0–D3/A0–A1, retain the supplied frame pointer and original DFC, inspect each +valid bit, load its data/address, select DFC, normalize WB1 lanes, select B/W/L +MOVES, clear that valid bit, restore DFC/registers and RTE. It branches on actual +frame bytes. Instruction transitions, flags, registers, all stack banks, memory +and bus writes are checked independently after every step. Following BRA and +trace-handler RTE check restored T1/T0, SR, exception provenance and stacks. +Invalid slots must not read their pointer/data fields. Overlaps expose store +ordering; final address/size/data/order expectations come from intended operands, +independently of the program's normalization calculations. + +| Group, each scalar/batch route | Program scenarios | Combinations | Weight | +| --- | ---: | ---: | ---: | +| Canonical | 36,864 | 1,152 | 32 CCRs | +| Structural | 36,864 | 18,432 | CCR 0/31 | +| Both groups, both routes | **147,456** | **39,168** | | + +Canonical cases cover both handler banks, four restored banks, all three slots, +B/W/L sizes, four address lanes and four value images. Structural cases cover +all eight valid masks, four uniform/mixed-size patterns, distinct/equal/overlapping +destinations, four address lanes, both frame alignments, two value images and +restored trace 0/T1/T0. Handler trace is clear. Initial/restored CCRs differ; +scratch registers and original DFC must survive. FPCR/FPSR/FPIAR are preserved, +but no floating-point operation or context transfer is performed. Counts describe +whole programs; checked instructions and following sentinels are not counted again. + +Supplied headers follow table 8-6: valid WB1 implies a normal write with +FA=WB1A; WB2 without WB1 uses a supplied write-page-fault image; reads have +neither WB1 nor WB2. Invalid statuses are zero. The earlier test-only draft used +read status for all masks and is excluded from architectural qualification. +These frames are initialization, not CPU-generated data faults. Translation +and caches are disabled during handler execution; the test does not execute an +enabled-MMU page fault or qualify its construction. + +```powershell +./scripts/test-copper68k-040-writebacks.ps1 -OutputDirectory artifacts/040-writebacks +./scripts/test-copper68k-040-writebacks.ps1 -ValidateReportsOnly -OutputDirectory artifacts/040-writebacks +./scripts/test-copper68k-040-writeback-mutations.ps1 -OutputDirectory artifacts/040-writeback-mutations +./scripts/test-copper68k-040-writeback-mutations.ps1 -ValidateReportsOnly -OutputDirectory artifacts/040-writeback-mutations +``` + +Accepted fresh/frozen qualification at `artifacts/m6-writeback-qualified-final/` +passes all fifteen exact executions without skips: four report batches, nine +fixed encoding/alignment/header examples and two bounded program witnesses. +Source inputs are captured before execution and checked afterward; source, +binary, report/TRX/log identities and exact names/keys/weights must match. +Isolated CPU SHA256 is +`bd11ad1ad39668a80582d4458fe707be63c44880d15b2a380950b2c3edb17caf`; +test adapter is +`e17c25b909f390cfd718796eaae4a9eeb338ae3f60a004581f7684a09df234e1`. +These isolated build identities differ from previous normal/consumer assemblies; +production CPU source and normal assemblies are unchanged. + +Two maintained test-handler mutations qualify discrimination at +`artifacts/m6-writeback-mutations-final/`. A two-test baseline passes. Replacing +WB1's lane rotation with NOP fails both witnesses at the checked rotation +transition; reversing stage order fails only the overlapping mixed-width witness, +whose independent final store-order check catches the defect. Exact failures, +methods and reasons are required, not merely a nonzero test exit. Mutation source, +input, CPU/test and TRX/log identities are captured; original program bytes and +normal assemblies are restored/preserved. This mutates the test handler, not the +CPU, and supplies no historical CPU-defect or regression-retirement claim. + +Eight controls at `artifacts/m6-writeback-controls-final/controls.json` reject +wrong profile, empty/duplicate inputs, missing reports, foreign test selection, +substituted keys, redistributed weights and hidden unsupported execution. +Seven mutation controls at `artifacts/m6-writeback-mutation-controls/controls.json` +reject wrong profile, empty inputs/mutations, altered source, wrong failed method, +wrong failure reason and wrong execution status. Changed evidence hashes are +refreshed, requiring semantic rejection. The isolated `concurrency-guard.json` +proves expected restoration and refusal to overwrite a concurrent edit. +The first run's absolute exception-counter expectation was a fixture error +(Reset retains that counter); its failed evidence is preserved and excluded. + +Ordinary requirements become **85,771,414 scenarios / 697 reports**. Combined +validation at `artifacts/m6-writeback-combined-reports/` uses the preceding +strictly verified full run, separately qualified EXG reports and these four new +reports. It is not a fresh broad run of the expanded source. Complete-040 +requirements now contain **67,793,440 cases / 86 reports and 24 fixed +controls/witnesses**, with exactly 110 discovered executions. Static requirement +verification is recorded in `artifacts/m6-writeback-complete-audit-requirements.json`; +the new complete-audit enumeration branch separately matches every qualified key +and weight (`artifacts/m6-writeback-complete-audit-key-check.json`). No passing +fresh complete-040 execution is claimed. + +Fresh inventory execution at `artifacts/m6-writeback-remaining-inventory/` +fails its one exact method without skips, retaining **480 untested cases / +fifteen combinations**, independently verified. Its write-back entry now states +this partial coverage accurately. Nested WB2/3 faults, cache pushes/MOVE16 line +cleanup, actual data-fault creation/restart, physical function-code spaces and +full handler/reference/hardware qualification remain required. Other advanced +restoration and software disagreements are unchanged. No production fix, +regression retirement, new consumer replay or package publication is included. +Milestone 6 stays **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 73d0044d..87f33ac4 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2455,3 +2455,36 @@ behavior, timing policy, public API or package changes are made in this slice. The complete-040 inventory still retains 480 untested cases, and all broader restoration, reference disagreements and consolidation requirements remain. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +### Milestone 6 supplied 040 write-back handler — 2026-10-06 + +Four ordinary matrices add 147,456 whole-program scenarios / 39,168 combinations +for scalar and one-instruction batch routes. The handler executes register saves, +validity/size/DFC decoding, WB1 memory-lane normalization, ordered WB1→WB2→WB3 +MOVES stores, valid-bit clearing, register/DFC restoration and RTE. Canonical +cases cover every slot, B/W/L size, address lane, four values, all CCRs and +handler/restored banks. Structural cases cover all valid masks, uniform/mixed +sizes, distinct/equal/overlapping operands, frame alignment and restored trace. +Every instruction checks state and memory; final operands/order are independently +chosen. Header examples distinguish normal writes, supplied write-page-fault +images and reads according to MC68040UM table 8-6. + +Fresh and frozen qualification pass fifteen exact executions without skips, +including nine fixed examples and two bounded witnesses. Lost WB1 rotation and +reversed stage order are detected by independently chosen results/order; baseline +and mutated execution identities, exact failing methods/reasons and restoration +are checked. Eight positive and seven mutation integrity controls reject their +intended errors; an isolated restoration-guard check preserves concurrent edits. +No CPU source or normal assemblies change, and no regression is retired. + +Ordinary requirements become 85,771,414 scenarios / 697 reports. Complete-040 +requirements become 67,793,440 cases / 86 reports and 24 fixed controls/witnesses; +its discovery selection lists exactly 110 executions. These are expanded +requirements, not a passing fresh complete-040 run. Fresh remaining-inventory +execution fails exactly 480 untested cases / fifteen combinations. Nested WB2/3 +faults, push/MOVE16 line cleanup and actual data-fault construction remain required, +along with all other advanced restoration/reference/consolidation gaps. Supplied +frames do not qualify enabled-MMU operation, physical function-code spaces, +hardware or timing. See the +[qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040-supplied-write-back-handler-qualification--2026-10-06). +Milestone 6 stays **in progress**, `roadmapComplete=false`; no package publication. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index 691d76dc..31c34b33 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -89,6 +89,10 @@ $expected = [ordered]@{ 'rte-user-software-trace-boundaries-batch' = @{cases=1360896; combinations=2592} 'rte-user-software-trace-chained-scalar' = @{cases=6350848; combinations=193536} 'rte-user-software-trace-chained-batch' = @{cases=6350848; combinations=193536} + 'rte-writeback-canonical-scalar' = @{cases=36864; combinations=1152} + 'rte-writeback-canonical-batch' = @{cases=36864; combinations=1152} + 'rte-writeback-structure-scalar' = @{cases=36864; combinations=18432} + 'rte-writeback-structure-batch' = @{cases=36864; combinations=18432} 'rte-access-remaining-protocols' = @{cases=480; combinations=15} } $testExit = 0 @@ -102,19 +106,19 @@ if (-not $ValidateReportsOnly) { $saved[$name] = [Environment]::GetEnvironmentVariable($name) [Environment]::SetEnvironmentVariable($name, $settings[$name]) } - & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests|FullyQualifiedName~SyntheticM68040ChainedOddReturnTests|FullyQualifiedName~SyntheticM68040RteValidationFaultTests|FullyQualifiedName~SyntheticM68040AccessDoubleFaultTests|FullyQualifiedName~SyntheticM68040BatchFaultTests|FullyQualifiedName~SyntheticM68040RteRepairTests|FullyQualifiedName~SyntheticM68040InstructionFaultTests|FullyQualifiedName~SyntheticM68040HandlerPrefetchTests|FullyQualifiedName~SyntheticM68040UserRteFaultTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests|FullyQualifiedName~SyntheticM68040ChainedOddReturnTests|FullyQualifiedName~SyntheticM68040RteValidationFaultTests|FullyQualifiedName~SyntheticM68040AccessDoubleFaultTests|FullyQualifiedName~SyntheticM68040BatchFaultTests|FullyQualifiedName~SyntheticM68040RteRepairTests|FullyQualifiedName~SyntheticM68040InstructionFaultTests|FullyQualifiedName~SyntheticM68040HandlerPrefetchTests|FullyQualifiedName~SyntheticM68040UserRteFaultTests|FullyQualifiedName~SyntheticM68040WritebackTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output $testExit = $LASTEXITCODE } finally { foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } } $identity = [ordered]@{ schema=1; reference='MC68040UM'; url='https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf' - sections=@('2.2.2.1','7.6.1','7.6.3','8.1 figure 8-1','8.2.1','8.2.2','8.2.5','8.2.6','8.3','8.4','8.4.1','8.4.2','8.4.3','8.4.4','8.4.6.2','8.4.6.7','9.6 table 9-9','9.6.2'); softwareReferenceExecuted=$false + sections=@('2.2.2.1','7.6.1','7.6.3','8.1 figure 8-1','8.2.1','8.2.2','8.2.5','8.2.6','8.3','8.4','8.4.1','8.4.2','8.4.3','8.4.4','8.4.6.2','8.4.6.3','8.4.6.5','8.4.6.7 table 8-6','9.6 table 9-9','9.6.2'); softwareReferenceExecuted=$false sourceCommit=(& git -C $repo rev-parse HEAD); cpuCommittedTree=(& git -C $repo rev-parse HEAD:Copper68k) cpuSourceFiles=@(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } - inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ChainedOddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040SoftwareTraceProgram.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs') | ForEach-Object { + inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ChainedOddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040SoftwareTraceProgram.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040WritebackTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040WritebackProgram.cs') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } assemblies=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | ForEach-Object { @@ -137,11 +141,11 @@ if (-not $ValidateReportsOnly) { $identity = Get-Content -LiteralPath (Join-Path $output 'identities.json') -Raw | ConvertFrom-Json if ($identity.schema -ne 1 -or $identity.reference -cne 'MC68040UM' -or $identity.softwareReferenceExecuted -ne $false -or $identity.sourceCommit -notmatch '^[a-f0-9]{40}$' -or $identity.cpuCommittedTree -notmatch '^[a-f0-9]{40}$' -or - @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 15 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 15 -or + @($identity.cpuSourceFiles).Count -eq 0 -or @($identity.inputs).Count -ne 17 -or @($identity.inputs.file | Sort-Object -Unique).Count -ne 17 -or @($identity.assemblies).Count -ne 2) { throw '040 access-frame input identity is missing or incomplete' } foreach ($input in $identity.inputs) { $path = Join-Path $repo $input.file - if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ChainedOddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040SoftwareTraceProgram.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs') -or + if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ChainedOddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040SoftwareTraceProgram.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040WritebackTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040WritebackProgram.cs') -or $input.sha256 -cne (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()) { throw '040 access-frame fixture/command identity differs' } } $cpuFiles = @(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') @@ -155,8 +159,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 95 -or [int]$counters.total -ne 95 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (82 batches, 13 fixed examples)' +if ([int]$counters.executed -ne 110 -or [int]$counters.total -ne 110 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (86 batches, 24 fixed examples/witnesses)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -179,6 +183,24 @@ foreach ($group in $expected.Keys) { $expectedCombinations["68040/RTE/format7/$form/bank=$bank"] = 32 } } + } elseif ($group.StartsWith('rte-writeback-', [StringComparison]::Ordinal)) { + $structure=$group.Contains('structure',[StringComparison]::Ordinal) + $matrix=if($structure){'structure'}else{'canonical'} + foreach($handler in @('ISP','MSP')){foreach($restored in @('user','user-M','ISP','MSP')){foreach($lane in 0..3){ + $prefix="68040/RTE/writebacks/$matrix/handler=$handler/restore=$restored/lane=$lane" + if(-not $structure){ + foreach($slot in 1..3){foreach($width in @(1,2,4)){foreach($pair in 0..3){ + $mask=1 -shl ($slot-1) + $expectedCombinations["$prefix/valid=$mask/slot$slot-size$width/layout=distinct/T=0000/pair=$pair"]=32 + }}} + }else{ + foreach($mask in 0..7){foreach($pattern in 0..3){foreach($layout in @('distinct','same','overlap')){ + foreach($trace in @('0000','8000','4000')){foreach($pair in @(2,3)){ + $expectedCombinations["$prefix/valid=$mask/pattern$pattern/layout=$layout/T=$trace/pair=$pair"]=2 + }} + }}} + } + }}} } elseif ($group.StartsWith('rte-chained-odd-', [StringComparison]::Ordinal)) { $structural=$group.Contains('structure',[StringComparison]::Ordinal) $access=$group.Contains('access-',[StringComparison]::Ordinal) @@ -595,8 +617,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 95 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 110 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=67645984; xunitBatches=82; fixedExamples=13; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=67793440; xunitBatches=86; fixedExamples=24; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-040-writeback-mutations.ps1 b/scripts/test-copper68k-040-writeback-mutations.ps1 new file mode 100644 index 00000000..6065b846 --- /dev/null +++ b/scripts/test-copper68k-040-writeback-mutations.ps1 @@ -0,0 +1,110 @@ +[CmdletBinding()] +param([switch]$ValidateReportsOnly,[string]$OutputDirectory='artifacts/040-writeback-mutations') +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +$programPath='Copper68k.Tests/Synthetic/SyntheticM68040WritebackProgram.cs' +$source=Join-Path $repo $programPath +$prefix='Copper68k.Tests.Synthetic.SyntheticM68040WritebackTests.' +$names=@(($prefix+'HandlerCompletesMemoryAlignedByteAtLaneOne'),($prefix+'HandlerCompletesOverlappingMixedWritebacksInOrder')) +$mutations=@( + @{name='missing-lane-rotation';anchor='Emit("lane-rotate", slot, [0xe3b8]);';replacement='Emit("lane-rotate", slot, [0x4e71]);';failed=$names;reason='lane-rotate/WB1'}, + @{name='reversed-writebacks';anchor='for (var slot = 1; slot <= 3; slot++)';replacement='for (var slot = 3; slot >= 1; slot--)';failed=@($names[1]);reason='Writeback results differ from independently intended operands'} +) +function Identity([string]$Path){ + $file=Join-Path $repo $Path + if(-not(Test-Path -LiteralPath $file -PathType Leaf)){throw "Missing writeback mutation input: $Path"} + return @{path=$Path;sha256=(Get-FileHash -LiteralPath $file).Hash} +} +function Validate-Selection($Actual,[string[]]$Required){ + $paths=@($Actual.path) + if($paths.Count -ne $Required.Count -or @($paths|Select-Object -Unique).Count -ne $paths.Count -or + ($paths.Count -gt 0 -and @(Compare-Object ($paths|Sort-Object) ($Required|Sort-Object)).Count)){throw 'Incomplete/duplicate writeback mutation identity selection'} + foreach($entry in $Actual){if($entry.sha256 -cnotmatch '^[A-F0-9]{64}$' -or (Identity $entry.path).sha256 -cne $entry.sha256){throw "Changed writeback mutation input: $($entry.path)"}} +} +function Mutated-Source([string]$Text,$Mutation){ + if(($Text.Split($Mutation.anchor).Length-1) -ne 1){throw 'Nonunique writeback mutation anchor'} + return $Text.Replace($Mutation.anchor,$Mutation.replacement) +} +function Restore-Source([byte[]]$Bytes,[string]$ExpectedHash,[string]$OriginalHash){ + $current=(Get-FileHash -LiteralPath $source).Hash + if($current -ceq $OriginalHash){return} + if($current -cne $ExpectedHash){throw 'Writeback program changed concurrently; current file left intact'} + [IO.File]::WriteAllBytes($source,$Bytes) +} +function Evidence-Paths([string]$Directory,[bool]$Mutation){ + $leaves=@('witnesses.trx','run.log','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll') + if($Mutation){$leaves+='mutated-program.cs'} + return @($leaves|ForEach-Object{[IO.Path]::GetRelativePath($repo,(Join-Path $Directory $_))}) +} +function Check-Execution([string]$Directory,[string[]]$Failed,[string]$Reason=''){ + [xml]$trx=Get-Content (Join-Path $Directory 'witnesses.trx') -Raw + $c=$trx.TestRun.ResultSummary.Counters;$results=@($trx.TestRun.Results.UnitTestResult) + $actualFailed=@($results|Where-Object outcome -CEQ Failed|ForEach-Object testName|Sort-Object) + if($c.total -ne 2 -or $c.executed -ne 2 -or $c.failed -ne $Failed.Count -or $c.passed -ne (2-$Failed.Count) -or + $results.Count -ne 2 -or @($results|Where-Object{$_.outcome -cnotin @('Passed','Failed')}).Count -ne 0 -or + @(Compare-Object ($results.testName|Sort-Object) ($names|Sort-Object)).Count -ne 0 -or + $actualFailed.Count -ne $Failed.Count -or + ($Failed.Count -gt 0 -and @(Compare-Object $actualFailed @($Failed|Sort-Object)).Count -ne 0)){throw 'Incomplete writeback mutation execution'} + foreach($result in @($results|Where-Object outcome -CEQ Failed)){ + if($result.Output.ErrorInfo.Message -notmatch [regex]::Escape($Reason)){throw 'Writeback mutation failed for another reason'} + } +} +Push-Location $repo +try { + $inputs=@(@(& git ls-files Copper68k)+@('Copper68k.Tests/Copper68k.Tests.csproj',$programPath, + 'Copper68k.Tests/Synthetic/SyntheticM68040WritebackTests.cs','Copper68k.Tests/Synthetic/SyntheticMachine.cs', + 'Copper68k.Tests/Synthetic/ArchitecturalExpectation.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs', + 'Copper68k.Tests/Synthetic/SyntheticMoveTests.cs','scripts/test-copper68k-040-writeback-mutations.ps1')|Sort-Object -Unique) + $normal=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll'|Where-Object{Test-Path -LiteralPath (Join-Path $repo $_)}) + $filter='FullyQualifiedName~HandlerCompletesMemoryAlignedByteAtLaneOne|FullyQualifiedName~HandlerCompletesOverlappingMixedWritebacksInOrder' + if(-not $ValidateReportsOnly){ + if(Test-Path -LiteralPath $output){throw 'Use a fresh writeback mutation directory'} + New-Item -ItemType Directory -Path $output|Out-Null + $original=@($inputs|ForEach-Object{Identity $_});$originalNormal=@($normal|ForEach-Object{Identity $_}) + $bytes=[IO.File]::ReadAllBytes($source);$text=[IO.File]::ReadAllText($source) + $sourceHash=(Get-FileHash -LiteralPath $source).Hash;$expectedHash=$sourceHash + $baseline=Join-Path $output 'baseline';New-Item -ItemType Directory -Path $baseline|Out-Null + & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path (Join-Path $baseline 'build') --filter $filter --logger 'trx;LogFileName=witnesses.trx' --results-directory $baseline *> (Join-Path $baseline 'run.log') + if($LASTEXITCODE -ne 0){throw 'Writeback mutation baseline failed'} + Check-Execution $baseline @() + $rows=@() + try { + foreach($mutation in $mutations){ + $directory=Join-Path $output $mutation.name;New-Item -ItemType Directory -Path $directory|Out-Null + $mutated=Mutated-Source $text $mutation + [IO.File]::WriteAllText((Join-Path $directory 'mutated-program.cs'),$mutated) + if((Get-FileHash -LiteralPath $source).Hash -cne $sourceHash){throw 'Writeback program changed before mutation'} + [IO.File]::WriteAllText($source,$mutated);$expectedHash=(Get-FileHash -LiteralPath $source).Hash + & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path (Join-Path $directory 'build') --filter $filter --logger 'trx;LogFileName=witnesses.trx' --results-directory $directory *> (Join-Path $directory 'run.log') + $code=$LASTEXITCODE + Restore-Source $bytes $expectedHash $sourceHash + if($code -ne 1){throw "Writeback mutation did not fail: $($mutation.name)"} + Check-Execution $directory @($mutation.failed) $mutation.reason + $rows+=@{name=$mutation.name;exit=$code;evidence=@((Evidence-Paths $directory $true)|ForEach-Object{Identity $_})} + } + } finally {Restore-Source $bytes $expectedHash $sourceHash} + foreach($entry in @($original)+@($originalNormal)){if((Identity $entry.path).sha256 -cne $entry.sha256){throw 'Writeback mutations changed original source/binaries'}} + @{schema=1;profile='m68040-writeback-handler-defect-proofs';sourceRevision=(& git rev-parse HEAD);sourceRestored=$true; + inputs=$original;normalBinaries=$originalNormal;baseline=@((Evidence-Paths $baseline $false)|ForEach-Object{Identity $_});mutations=$rows}| + ConvertTo-Json -Depth 10|Set-Content (Join-Path $output 'identity.json') + } + $id=Get-Content (Join-Path $output 'identity.json') -Raw|ConvertFrom-Json + if($id.schema -ne 1 -or $id.profile -cne 'm68040-writeback-handler-defect-proofs' -or $id.sourceRestored -ne $true -or $id.sourceRevision -cnotmatch '^[a-f0-9]{40}$'){throw 'Incomplete writeback mutation profile'} + Validate-Selection @($id.inputs) $inputs;Validate-Selection @($id.normalBinaries) $normal + $baseline=Join-Path $output 'baseline';Validate-Selection @($id.baseline) (Evidence-Paths $baseline $false);Check-Execution $baseline @() + if($id.mutations.Count -ne 2 -or @($id.mutations.name|Select-Object -Unique).Count -ne 2){throw 'Incomplete/duplicate writeback mutations'} + $text=[IO.File]::ReadAllText($source) + foreach($mutation in $mutations){ + $row=@($id.mutations|Where-Object name -CEQ $mutation.name) + if($row.Count -ne 1 -or $row[0].exit -ne 1){throw 'Missing executed writeback mutation'} + $directory=Join-Path $output $mutation.name;Validate-Selection @($row[0].evidence) (Evidence-Paths $directory $true) + if([IO.File]::ReadAllText((Join-Path $directory 'mutated-program.cs')) -cne (Mutated-Source $text $mutation)){throw 'Wrong writeback mutation source scope'} + Check-Execution $directory @($mutation.failed) $mutation.reason + } + @{schema=1;mutationProofs=2;sourceRestored=$true;productionCpuMutation=$false;roadmapComplete=$false; + scope='Handler fixture defects: lost WB1 lane rotation and reversed stage order; no emulator bug or regression retirement claimed'}| + ConvertTo-Json -Depth 6|Set-Content (Join-Path $output 'summary.json') + Write-Host 'Both independent writeback witnesses detect their targeted handler defects; original program/CPU/binaries preserved.' +} finally {Pop-Location} +$global:LASTEXITCODE=0 diff --git a/scripts/test-copper68k-040-writebacks.ps1 b/scripts/test-copper68k-040-writebacks.ps1 new file mode 100644 index 00000000..bbb12d75 --- /dev/null +++ b/scripts/test-copper68k-040-writebacks.ps1 @@ -0,0 +1,99 @@ +[CmdletBinding()] +param([switch]$ValidateReportsOnly,[string]$OutputDirectory='artifacts/040-writebacks') +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +$groups=@('rte-writeback-canonical-scalar','rte-writeback-canonical-batch','rte-writeback-structure-scalar','rte-writeback-structure-batch') +function Identity([string]$Path){ + $file=Join-Path $repo $Path + if(-not(Test-Path -LiteralPath $file -PathType Leaf)){throw "Missing writeback input: $Path"} + return @{path=$Path;sha256=(Get-FileHash -LiteralPath $file).Hash} +} +function Validate-Selection($Actual,[string[]]$Required){ + $paths=@($Actual.path) + if($paths.Count -ne $Required.Count -or @($paths|Select-Object -Unique).Count -ne $paths.Count -or + @(Compare-Object ($paths|Sort-Object) ($Required|Sort-Object)).Count){throw 'Incomplete/duplicate writeback identity selection'} + foreach($entry in $Actual){if($entry.sha256 -cnotmatch '^[A-F0-9]{64}$' -or (Identity $entry.path).sha256 -cne $entry.sha256){throw "Changed writeback input: $($entry.path)"}} +} +function Expected-Keys([bool]$Structure){ + $keys=[Collections.Generic.Dictionary[string,int]]::new([StringComparer]::Ordinal) + $matrix=if($Structure){'structure'}else{'canonical'} + foreach($handler in @('ISP','MSP')){foreach($restored in @('user','user-M','ISP','MSP')){foreach($lane in 0..3){ + $prefix="68040/RTE/writebacks/$matrix/handler=$handler/restore=$restored/lane=$lane" + if(-not $Structure){ + foreach($slot in 1..3){foreach($width in @(1,2,4)){foreach($pair in 0..3){ + $mask=1 -shl ($slot-1) + if(-not $keys.TryAdd("$prefix/valid=$mask/slot$slot-size$width/layout=distinct/T=0000/pair=$pair",32)){throw 'Duplicate canonical writeback key'} + }}} + }else{ + foreach($mask in 0..7){foreach($pattern in 0..3){foreach($layout in @('distinct','same','overlap')){ + foreach($trace in @('0000','8000','4000')){foreach($pair in @(2,3)){ + if(-not $keys.TryAdd("$prefix/valid=$mask/pattern$pattern/layout=$layout/T=$trace/pair=$pair",2)){throw 'Duplicate structural writeback key'} + }} + }}} + } + }}} + return ,$keys +} +$saved=[Environment]::GetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR') +Push-Location $repo +try { + $inputs=@(@(& git ls-files Copper68k)+@('Copper68k.Tests/Copper68k.Tests.csproj', + 'Copper68k.Tests/Synthetic/SyntheticM68040WritebackTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040WritebackProgram.cs', + 'Copper68k.Tests/Synthetic/SyntheticMachine.cs','Copper68k.Tests/Synthetic/ArchitecturalExpectation.cs', + 'Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticMoveTests.cs', + 'scripts/test-copper68k-040-writebacks.ps1')|Sort-Object -Unique) + $binaries=@('build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'| + ForEach-Object{[IO.Path]::GetRelativePath($repo,(Join-Path $output $_))}) + $evidence=@(@($groups|ForEach-Object{"68040-$_.json"})+@('writebacks.trx','run.log')| + ForEach-Object{[IO.Path]::GetRelativePath($repo,(Join-Path $output $_))}) + if(-not $ValidateReportsOnly){ + if(Test-Path -LiteralPath $output){throw 'Use a fresh writeback output directory'} + New-Item -ItemType Directory -Path $output|Out-Null + $original=@($inputs|ForEach-Object{Identity $_}) + [Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$output) + & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path (Join-Path $output 'build') --filter 'FullyQualifiedName~SyntheticM68040WritebackTests' --logger 'trx;LogFileName=writebacks.trx' --results-directory $output *> (Join-Path $output 'run.log') + if($LASTEXITCODE -ne 0){throw "Writeback execution failed: $output/run.log"} + foreach($entry in $original){if((Identity $entry.path).sha256 -cne $entry.sha256){throw 'Writeback source changed during execution'}} + @{schema=1;profile='m68040-supplied-normal-writebacks';sourceRevision=(& git rev-parse HEAD);inputs=$original; + binaries=@($binaries|ForEach-Object{Identity $_});evidence=@($evidence|ForEach-Object{Identity $_})}| + ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'identity.json') + } + $id=Get-Content (Join-Path $output 'identity.json') -Raw|ConvertFrom-Json + if($id.schema -ne 1 -or $id.profile -cne 'm68040-supplied-normal-writebacks' -or $id.sourceRevision -cnotmatch '^[a-f0-9]{40}$'){throw 'Incomplete writeback identity profile'} + Validate-Selection @($id.inputs) $inputs;Validate-Selection @($id.binaries) $binaries;Validate-Selection @($id.evidence) $evidence + $prefix='Copper68k.Tests.Synthetic.SyntheticM68040WritebackTests.' + $names=@('CanonicalWritebacksAllLanesSizesValuesAndCcrScalar','CanonicalWritebacksAllLanesSizesValuesAndCcrBatch', + 'WritebackValidityOrderOverlapAndRestoredTraceScalar','WritebackValidityOrderOverlapAndRestoredTraceBatch','HandlerUsesFixedReferenceEncodings', + 'HandlerCompletesMemoryAlignedByteAtLaneOne','HandlerCompletesOverlappingMixedWritebacksInOrder'| + ForEach-Object{$prefix+$_})+ + @(($prefix+'MemoryAlignedFixtureMatchesManualExamples(value: 120, width: 1, lane: 0, expected: 2028139268)'), + ($prefix+'MemoryAlignedFixtureMatchesManualExamples(value: 120, width: 1, lane: 3, expected: 3521311608)'), + ($prefix+'MemoryAlignedFixtureMatchesManualExamples(value: 4660, width: 2, lane: 3, expected: 887288594)'), + ($prefix+'MemoryAlignedFixtureMatchesManualExamples(value: 305419896, width: 4, lane: 1, expected: 2014458966)'), + ($prefix+'SuppliedHeaderMatchesManualFrameCombinations(mask: 1, ssw: 37, faultAddress: 16896)'), + ($prefix+'SuppliedHeaderMatchesManualFrameCombinations(mask: 2, ssw: 1089, faultAddress: 16920)'), + ($prefix+'SuppliedHeaderMatchesManualFrameCombinations(mask: 4, ssw: 261, faultAddress: 17152)'), + ($prefix+'SuppliedHeaderMatchesManualFrameCombinations(mask: 0, ssw: 261, faultAddress: 17152)')) + [xml]$trx=Get-Content (Join-Path $output 'writebacks.trx') -Raw + $c=$trx.TestRun.ResultSummary.Counters;$results=@($trx.TestRun.Results.UnitTestResult) + if($c.total -ne 15 -or $c.executed -ne 15 -or $c.passed -ne 15 -or $c.failed -ne 0 -or + $results.Count -ne 15 -or @($results|Where-Object outcome -CNE Passed).Count -ne 0 -or + @(Compare-Object ($results.testName|Sort-Object) ($names|Sort-Object)).Count){throw 'Incomplete writeback execution selection'} + $coverage=@() + foreach($group in $groups){ + $structure=$group.Contains('structure');$keys=Expected-Keys $structure + $r=Get-Content (Join-Path $output "68040-$group.json") -Raw|ConvertFrom-Json -AsHashtable + if($r.schema -ne 1 -or $r.model -cne '68040' -or $r.group -cne $group -or $r.xunitBatches -ne 1 -or + $r.logicalCases -ne 36864 -or $r.counts.passing -ne 36864 -or $r.counts.mismatching -ne 0 -or + $r.counts.unsupported -ne 0 -or $r.counts.untested -ne 0 -or $r.failures.Count -ne 0 -or $r.combinations.Count -ne $keys.Count){throw "Incomplete writeback coverage: $group"} + foreach($key in $keys.Keys){if(-not $r.combinations.Contains($key) -or $r.combinations[$key].Count -ne 1 -or $r.combinations[$key].passing -ne $keys[$key]){throw "Missing/misweighted writeback combination: $key"}} + $coverage+=@{group=$group;scenarios=36864;combinations=$keys.Count} + } + @{schema=1;logicalCases=147456;xunitBatches=4;fixedExamples=9;boundedWitnesses=2;coverage=$coverage;roadmapComplete=$false; + reference='MC68040UM 8.4.6.3/5/7';scope='Executed handler for supplied normal byte/word/long format-7 writebacks with translation/cache disabled'; + remaining='Actual data-fault creation, nested WB2/3 faults, cache pushes/MOVE16 lines, physical function-code spaces and hardware/physical timing remain unqualified'}| + ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'summary.json') + Write-Host "040 supplied writebacks: 147456 passing program scenarios / 39168 combinations; $output" +} finally {[Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$saved);Pop-Location} +$global:LASTEXITCODE=0 diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index c51e4758..56843a48 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -162,6 +162,9 @@ try { $expected['system-move-long-restart-invalid'] = 640 } if ($model -eq '68040') { + foreach ($matrix in @('canonical','structure')) { foreach ($route in @('scalar','batch')) { + $expected["rte-writeback-$matrix-$route"] = 36864 + } } $expected['instruction-fault-frame'] = 36864 $expected['instruction-fault-restart'] = 79872 $expected['rte-repair-boundaries'] = 143424 From e487d2ddc2fb40d297cb7e761f8884e94ec69449 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 20:23:34 +0300 Subject: [PATCH 072/211] Fix and qualify 040 MOVE write-fault recovery --- .../Synthetic/MoveSpecification.cs | 4 + Copper68k.Tests/Synthetic/README.md | 29 ++ .../SyntheticM68040AccessDoubleFaultTests.cs | 9 +- .../SyntheticM68040AccessFrameAuditTests.cs | 4 +- .../SyntheticM68040BatchFaultTests.cs | 31 +- .../SyntheticM68040OperandWriteFaultTests.cs | 306 ++++++++++++++++++ .../SyntheticM68040WritebackProgram.cs | 5 +- Copper68k/M68040Support.cs | 112 ++++++- Copper68k/M68kAdvancedTimingInterpreter.cs | 6 + Copper68k/M68kCore.cs | 12 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 144 +++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 45 +++ scripts/copper68k-040-operand-write-keys.ps1 | 48 +++ scripts/test-copper68k-040-access-frames.ps1 | 29 +- ...-copper68k-040-operand-write-mutations.ps1 | 118 +++++++ scripts/test-copper68k-040-operand-writes.ps1 | 79 +++++ scripts/test-copper68k-synthetic.ps1 | 7 +- 17 files changed, 945 insertions(+), 43 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040OperandWriteFaultTests.cs create mode 100644 scripts/copper68k-040-operand-write-keys.ps1 create mode 100644 scripts/test-copper68k-040-operand-write-mutations.ps1 create mode 100644 scripts/test-copper68k-040-operand-writes.ps1 diff --git a/Copper68k.Tests/Synthetic/MoveSpecification.cs b/Copper68k.Tests/Synthetic/MoveSpecification.cs index d03db7e6..c4347167 100644 --- a/Copper68k.Tests/Synthetic/MoveSpecification.cs +++ b/Copper68k.Tests/Synthetic/MoveSpecification.cs @@ -51,6 +51,8 @@ internal sealed class MoveFixture public OperandForm Source { get; } public OperandForm Destination { get; } public int Width { get; } + public uint DestinationAddress { get; private set; } + public uint OperandValue { get; private set; } public string Id { get; } private readonly uint value; private readonly int ccr; @@ -113,6 +115,8 @@ public void Prepare() operand = machine.PeekPhysical(sourceAddress, Width); } destinationAddress = addressing.DestinationPointerTarget?.Invoke() ?? destinationAddress; + DestinationAddress = destinationAddress; + OperandValue = operand; ExpectedMemory = new(machine.Bus.Memory); if (Destination.Mode == 0) { diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 3d3a4b5c..71851ee6 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1113,3 +1113,32 @@ Frame entry is composed; full native exception/trace processing, physical bus timing and long-frame restart remain unqualified. No production fix or regression retirement follows from this software discrepancy. Details are in `docs/COPPER68K_REFERENCE_QUALIFICATION.md`. + +### 68040 actual MOVE write faults + +`SyntheticM68040OperandWriteFaultTests` rejects actual destination stores through +the host physical map, with translation disabled. Six ordinary scalar/batch +matrices check fault entry, an integer WB1 completion handler, RTE and the +following instruction. They cover all 7,350 legal memory-destination MOVE +opcodes per route, 9,216 lane/size/value/CCR/stack scenarios and 14,256 full-index, +alias and trace scenarios. Total: 61,644 programs / 43,788 combinations. +Defined format-7 fields and memory lanes are checked independently; undefined +frame bytes are opaque handler inputs. A separate witness checks that a rejected +trace-frame store cannot change the completed MOVE destination. + +```powershell +./scripts/test-copper68k-040-operand-writes.ps1 -OutputDirectory artifacts/040-operand-writes +./scripts/test-copper68k-040-operand-writes.ps1 -ValidateReportsOnly -OutputDirectory artifacts/040-operand-writes +./scripts/test-copper68k-040-operand-write-mutations.ps1 -OutputDirectory artifacts/040-operand-write-mutations +./scripts/test-copper68k-040-operand-write-mutations.ps1 -ValidateReportsOnly -OutputDirectory artifacts/040-operand-write-mutations +``` + +The commands require exact source/binary/evidence identities, twelve nonempty +executions and independently enumerated keys/weights. Seven production mutations +run six bounded witnesses each, after a six-test baseline; precise failing +methods/reasons are mandatory and source restoration preserves concurrent edits. +Successful transfer order/access widths and existing timing policy are retained; +the faulted instruction is never retried. This qualifies normal MOVE writes, +not other integer/read/MOVEM/MOVE16 faults, nested writeback recovery, enabled MMU +or physical timing. The complete-040 gate retains its 480 untested scenarios; +milestone 6 remains incomplete. See `docs/COPPER68K_REFERENCE_QUALIFICATION.md`. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs index 14d85226..39082d97 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs @@ -23,7 +23,7 @@ public void WarmDispatchHaltsBothRteFallbackAndOperandFaultEntry(string engine) foreach (var ccr in new[] { 0, 31 }) foreach (var alignment in new uint[] { 0, 1 }) foreach (var stage in new[] { "stack", "vector" }) - for (var faultByte = 0; faultByte < (stage == "vector" ? 4 : form == "RTE" ? 60 : 8); faultByte++) + for (var faultByte = 0; faultByte < (stage == "vector" ? 4 : form == "RTE" || engine == "accurate" && form.EndsWith("write") ? 60 : 8); faultByte++) { var opcode = form switch { "RTE" => 0x4e73, "MOVE.B.read" => 0x1010, "MOVE.W.read" => 0x3010, "MOVE.L.read" => 0x2010, "MOVE.B.write" => 0x1080, "MOVE.W.write" => 0x3080, _ => 0x2080 }; @@ -60,9 +60,10 @@ public void WarmDispatchHaltsBothRteFallbackAndOperandFaultEntry(string engine) var sideExits = core is M68kJitCore j2 ? j2.Counters.SideExits : 0; bus.Arm(form == "RTE" ? frame : 0x4200 + alignment, write ? M68kBusAccessKind.CpuDataWrite : M68kBusAccessKind.CpuDataRead); - // The existing generic MOVE fault path uses an approximate short - // frame. Only its fatal-entry behavior is qualified here. - var stackStart = frame - (form == "RTE" ? 60u : 8u); + // Accurate MOVE writes now create format 7. Read and compiled + // operand paths retain their separate short-frame gap; only + // fatal entry is qualified for those routes here. + var stackStart = frame - (form == "RTE" || engine == "accurate" && write ? 60u : 8u); bus.Arm(stage == "vector" ? 8 + (uint)faultByte : stackStart + (uint)faultByte, stage == "vector" ? M68kBusAccessKind.CpuDataRead : M68kBusAccessKind.CpuDataWrite); Execute(core); diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 02a34ccf..f99538a8 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -35,11 +35,11 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() form == "frame-validation-fault" ? "Validation, preserved-trace repair/software service, successful mixed-epoch chains and mixed-epoch validation fault capture have separate software coverage; mixed-epoch repair/retry, original trace suspension/resumption and internal restoration remain required. Physical/hardware behavior is separately unqualified" : form == "real-access-fault-entry" - ? "Cache-disabled accurate instruction-fetch frames/restart are covered separately; data fault/writeback restart, enabled-cache deferral and compiled instruction-PC provenance remain required" + ? "Cache-disabled accurate instruction-fetch restart and actual legal MOVE memory-destination faults through WB1 handler/RTE are covered separately; other integer/read/MOVEM/MOVE16 faults, enabled-cache deferral and compiled fault pipelines remain required" : form == "odd-PC-chained-SR-provenance" ? "Chained format0/2/3 and format7 normal/CM SR handoff, stack consumption and header reads have separate generated WinUAE software coverage; access-frame validation/no-replay checks are synthetic. Separate composed-entry and executed-MMU-entry CM lifetime discovery profiles disagree after odd-PC fault/repair; the executed profile also exposes stacked-SR disagreement. Neither qualifies the full run loop or hardware. CT/CU/CP pending/foreign-context chains, full reference exception qualification and hardware behavior remain unqualified" : form == "writeback-handler" - ? "Executed normal B/W/L supplied-frame handler separately covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks; nested WB2/3 faults, cache-push/MOVE16 line cleanup and actual data-fault construction remain required. Physical function-code spaces and hardware timing are unqualified" + ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks; actual legal MOVE destination faults exercise WB1 through handler/RTE. Nested WB2/3, cache-push/MOVE16 lines and other data-fault construction remain required. Physical function-code spaces and hardware timing are unqualified" : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs index d2a58e50..8c7763e0 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs @@ -5,8 +5,8 @@ namespace Copper68k.Tests.Synthetic; // Architectural validation frames: MC68040UM 8.4.6.7. Cached-block batching, // callbacks and cycle equality are emulator execution/timing-policy contracts. -// Generic operand/fetch faults still use an approximate short frame: this audit -// does not promote it as architectural format-7 restart coverage. +// MOVE writes and instruction fetch/validation use architectural format 7. +// Operand reads retain a separate approximate short-frame gap. public sealed class SyntheticM68040BatchFaultTests(ITestOutputHelper output) { private const uint Handler = 0x9020; @@ -167,23 +167,38 @@ private static Fixture Prepare(SyntheticMachine m, CodeBus bus, string form, boo bus.Accesses.Clear(); var e = ArchitecturalExpectation.Capture(m); var savedSr = (ushort)((bank == "MSP" ? 0x3000 : 0x2000) | trace | ccr); + var incomingSr = savedSr; + var write = form.EndsWith("store"); + var storeValue = form == "partial-store" ? 0x12345678u : m.Core.State.D[0]; + if (write) savedSr = (ushort)((savedSr & 0xfff0) | (storeValue == 0 ? 4 : 0) | ((storeValue & 0x80000000) != 0 ? 8 : 0)); e.Sr = (ushort)(savedSr & ~0xc000); e.Pc = Handler; e.ExceptionVector = 2; e.D[7] += form == "self-fetch" ? 0u : (uint)(prefix - (mixed && prefix > 0 ? 1 : 0)); if (form == "partial-store") { e.A[0] += 4; e.A[1] -= 4; } var kind = form == "self-fetch" ? M68kBusAccessKind.CpuInstructionFetch : form.EndsWith("store") ? M68kBusAccessKind.CpuDataWrite : M68kBusAccessKind.CpuDataRead; var faultAddress = form == "RTE" ? frame + offset : form == "self-fetch" ? 0x1004u : form.EndsWith("store") ? 0x4300 + alignment : 0x4200 + alignment; - var stackedPc = faultPc; + var stackedPc = write ? faultPc + (form == "partial-store" ? 2u : 6u) : faultPc; var sequence = m.Core.State.ExceptionSequence; e.ControlChecks["single exception"] = (s => s.ExceptionSequence, sequence + 1); e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, savedSr); - e.ControlChecks["saved PC (generic short-frame policy where applicable)"] = (s => s.LastExceptionStackedProgramCounter, stackedPc); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, stackedPc); e.ControlChecks["bypass cleared"] = (s => s.M68040Mmu.BypassTranslation ? 1u : 0u, 0); - var accessFrame = frame - (form is "RTE" or "self-fetch" ? 60u : 8u); + var accessFrame = frame - (write || form is "RTE" or "self-fetch" ? 60u : 8u); e.A[7] = accessFrame; if (bank == "MSP") e.MasterStackPointer = accessFrame; - if (form is "RTE" or "self-fetch") + if (write || form is "RTE" or "self-fetch") { - SyntheticM68040RteValidationFaultTests.ExpectAccessFrame(m, e, accessFrame, savedSr, faultPc, faultAddress, width); + SyntheticM68040RteValidationFaultTests.ExpectAccessFrame(m, e, accessFrame, savedSr, stackedPc, faultAddress, width); if (form == "self-fetch") e.Write(accessFrame + 12, 0x0106, 2, m.Model); + if (write) + { + var modifier = bank == "MSP" || bank == "ISP" ? 5u : 1u; + e.Write(accessFrame + 12, modifier, 2, m.Model); + e.Write(accessFrame + 18, 0x80 | modifier, 2, m.Model); + e.Write(accessFrame + 40, faultAddress, 4, m.Model); + for (uint n = 18; n < 20; n++) e.MemoryMasks[accessFrame + n] = 255; + for (uint n = 40; n < 48; n++) e.MemoryMasks[accessFrame + n] = 255; + for (var n = 0; n < 4; n++) + e.Memory[accessFrame + 44 + ((faultAddress + (uint)n) & 3)] = (byte)(storeValue >> (8 * (3 - n))); + } } else { @@ -191,7 +206,7 @@ private static Fixture Prepare(SyntheticMachine m, CodeBus bus, string form, boo e.Write(accessFrame, savedSr, 2, m.Model); e.Write(accessFrame + 2, stackedPc, 4, m.Model); e.Write(accessFrame + 6, 8, 2, m.Model); } - var boundary = new FaultBoundary(m.Core, bus, prefix, savedSr, faultAddress + (uint)faultByte, kind, + var boundary = new FaultBoundary(m.Core, bus, prefix, incomingSr, faultAddress + (uint)faultByte, kind, outcome == "fatal-stack" ? accessFrame : outcome == "fatal-vector" ? vbr + 8 : null, outcome == "fatal-stack" ? M68kBusAccessKind.CpuDataWrite : M68kBusAccessKind.CpuDataRead); return new(e, boundary, frame, faultAddress, sequence); diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040OperandWriteFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040OperandWriteFaultTests.cs new file mode 100644 index 00000000..077d89e5 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040OperandWriteFaultTests.cs @@ -0,0 +1,306 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 8.4.6, 8.4.6.5 table 8-5 and 8.4.6.7: +// a normal physical write fault is completed by the handler, without replaying +// the MOVE or its operand reads/address-register updates. +public sealed class SyntheticM68040OperandWriteFaultTests(ITestOutputHelper output) +{ + private const uint Handler = 0xa000, TraceHandler = 0xb000; + private static readonly string[] Banks = ["user", "user-M", "ISP", "MSP"]; + [Fact, Trait("Suite", "Synthetic")] + public void TraceFrameWriteFaultCannotChangeCompletedMoveDestination() + { + var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + m.Reset(31); m.Core.State.D[0] = 0x12345678; m.Core.State.A[0] = 0x4200; + m.InitializePhysical(0x1000, 0x30c0, 2); m.InitializePhysical(0x1002, 0x4e71, 2); + m.InitializePhysical(0x9020, 0x4e71, 2); m.InitializePhysical(0x9022, 0x4e71, 2); + m.Start(); m.Core.State.StatusRegister |= 0x8000; + var sequence = m.Core.State.ExceptionSequence; + // MOVE completes, then the trace format-word store is rejected. That + // exception write must not be classified as another MOVE operand. + // Only preservation/routing is qualified here; the trace-entry fault's + // complete format/recovery protocol remains a separate gap. + bus.Arm(0x4700 - 12 + 6, M68kBusAccessKind.CpuDataWrite); + m.Core.ExecuteInstruction(); + Assert.Equal(0x4202u, m.Core.State.A[0]); + Assert.Equal(0x5678u, m.PeekPhysical(0x4200, 2)); + Assert.Single(bus.Accesses.Where(a => a.Write && a.Address == 0x4200)); + Assert.Single(bus.Rejected); + Assert.Equal(sequence + 2, m.Core.State.ExceptionSequence); + Assert.Equal(2, m.Core.State.LastExceptionVector); + Assert.False(m.Core.State.Halted); + } + [Theory, Trait("Suite", "Synthetic")] + [InlineData(false)] + [InlineData(true)] + public void EveryMemoryDestinationMoveOpcodeFaultsAndReturns(bool batch) + { + var report = new CoverageBatch("68040", "move-write-fault-opcodes-" + (batch ? "batch" : "scalar")); + foreach (var opcode in MoveSpecification.Opcodes().Where(op => ((op >> 6) & 7) >= 2)) + Case(report, batch, opcode, 0x89abcdee, 31, "ISP", 0, "opcode"); + report.Complete(output); + } + + [Theory, Trait("Suite", "Synthetic")] + [InlineData(false)] + [InlineData(true)] + public void EveryLaneSizeValueCcrAndStackBank(bool batch) + { + var report = new CoverageBatch("68040", "move-write-fault-boundaries-" + (batch ? "batch" : "scalar")); + foreach (var width in new[] { 1, 2, 4 }) + foreach (var bank in Banks) + foreach (var lane in Enumerable.Range(0, 4)) + foreach (var pair in Enumerable.Range(0, 6)) + for (var ccr = 0; ccr < 32; ccr++) + { + var sign = 1u << (width * 8 - 1); + uint[] values = [0, 1, sign - 1, sign, sign + 1, MoveSpecification.Mask(width)]; + var opcode = MoveSpecification.Encode(width, new(0, 0), new(3, 7)); + Case(report, batch, opcode, values[pair], ccr, bank, 0, $"boundary/lane={lane}/pair={pair}", + customize: m => m.Core.State.SetActiveStackPointer((bank == "MSP" ? 0x7400u : bank.StartsWith("user") ? 0x7800u : 0x4700u) + (uint)lane)); + } + report.Complete(output); + } + + [Theory, Trait("Suite", "Synthetic")] + [InlineData(false)] + [InlineData(true)] + public void FullIndexedAliasesAndTraceContinuation(bool batch) + { + var report = new CoverageBatch("68040", "move-write-fault-indexed-" + (batch ? "batch" : "scalar")); + foreach (var spec in IndexFixture.FullStructures()) + foreach (var width in new[] { 1, 2, 4 }) + foreach (var register in new[] { 0, 7 }) + foreach (var bank in Banks) + foreach (var trace in new ushort[] { 0, 0x8000, 0x4000 }) + foreach (var form in new[] { "source", "destination", "dual" }) + { + var source = new OperandForm(form == "destination" ? 3 : 6, register); + var destination = new OperandForm(form == "source" ? 3 : 6, register); + var opcode = MoveSpecification.Encode(width, source, destination); + Case(report, batch, opcode, 0x89abcdee, 31, bank, trace, $"indexed/{spec.Id}/reg={register}/form={form}", + new(SourceIndex: spec, DestinationIndex: spec with { AddressIndex = true, IndexRegister = register, LongIndex = true, Scale = 1 })); + } + report.Complete(output); + } + + [Fact, Trait("Suite", "Synthetic")] + public void PredecrementFaultCommitsAliasedBaseOnce() + { + var result = Case(new("68040", "bounded-control"), false, 0x2120, 0x89abcdee, 31, "MSP", 0, "predecrement-alias"); + Assert.True(result.Status == "passing", result.Reason); + } + + [Fact, Trait("Suite", "Synthetic")] + public void FullIndexedFaultKeepsLatchedOperandAndPendingTrace() + { + var index = new IndexFixture(Full: true, BaseSize: 3, Indirect: 7); + var result = Case(new("68040", "bounded-control"), true, 0x21b0, 0x89abcdee, 31, "user-M", 0x8000, "full-index-trace", + new(index, index with { AddressIndex = true, IndexRegister = 0, LongIndex = true, Scale = 1 })); + Assert.True(result.Status == "passing", result.Reason); + } + + private static (string Status, string? Reason) Case(CoverageBatch report, bool batch, ushort opcode, uint value, int ccr, + string bank, ushort trace, string scenario, AddressOptions? options = null, Action? customize = null) + { + var source = new OperandForm((opcode >> 3) & 7, opcode & 7); + var destination = new OperandForm((opcode >> 6) & 7, (opcode >> 9) & 7); + var id = $"68040/MOVE/write-fault/{scenario}/size={MoveSpecification.Width(opcode)}/source={source.Id}/destination={destination.Id}/bank={bank}/T={trace:X4}/op={opcode:X4}/v={value:X8}/ccr={ccr:X2}"; + string? mismatch = null; var status = "passing"; + try + { + var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + var fixture = new MoveFixture(m, opcode, value, ccr, !bank.StartsWith("user"), options: options, customize: x => + { + if (bank == "MSP") { x.Core.State.StatusRegister |= 0x1000; x.Core.State.SetActiveStackPointer(0x7400); } + customize?.Invoke(x); + }); + fixture.Prepare(); + var originalA7 = m.Core.State.A[7]; + var stacks = new Dictionary { ["user"] = 0x7800, ["ISP"] = 0x4700, ["MSP"] = 0x7400 }; + stacks[M68040StackFixture.PhysicalBank(bank)] = originalA7; + m.Core.State.SetUserStackPointer(stacks["user"]); m.Core.State.SetInterruptStackPointer(stacks["ISP"]); + m.Core.State.SetMasterStackPointer(stacks["MSP"]); + m.Core.State.StatusRegister = (ushort)(0x0700 | M68040StackFixture.Status(bank, trace, ccr)); + m.Core.State.DestinationFunctionCode = 3; m.Core.State.SourceFunctionCode = 6; + var program = new SyntheticM68040WritebackProgram(Handler); program.Initialize(m); + m.InitializePhysical(8, Handler, 4); m.InitializePhysical(36, TraceHandler, 4); + m.InitializePhysical(TraceHandler, 0x4e73, 2); m.InitializePhysical(TraceHandler + 2, 0x4e71, 2); + m.InitializePhysical(fixture.NextPc, 0x7e55, 2); m.InitializePhysical(fixture.NextPc + 2, 0x4e71, 2); + var e = ArchitecturalExpectation.Capture(m); + Array.Copy(fixture.D, e.D, 8); Array.Copy(fixture.A, e.A, 8); + var savedSr = (ushort)((fixture.ExpectedSr & 0x071f) | M68040StackFixture.Status(bank, trace, 0)); + stacks[M68040StackFixture.PhysicalBank(bank)] = fixture.A[7]; + var exceptionBank = M68040StackFixture.ExceptionBank(savedSr); + var frame = stacks[exceptionBank] - 60; stacks[exceptionBank] = frame; + var sequence = m.Core.State.ExceptionSequence; + M68040StackFixture.SetStacks(e, stacks, (ushort)((savedSr | 0x2000) & 0x3fff)); + e.Pc = Handler; e.ExceptionVector = 2; + e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence + 1); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, savedSr); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, fixture.NextPc); + e.ControlChecks["DFC"] = (s => s.DestinationFunctionCode, 3); + e.ControlChecks["SFC"] = (s => s.SourceFunctionCode, 6); + e.Write(frame, savedSr, 2, m.Model); e.Write(frame + 2, fixture.NextPc, 4, m.Model); e.Write(frame + 6, 0x7008, 2, m.Model); + for (uint n = 8; n < 60; n++) e.MemoryMasks[frame + n] = 0; + var size = fixture.Width == 1 ? 0x20 : fixture.Width == 2 ? 0x40 : 0; + var modifier = bank.StartsWith("user") ? 1 : 5; + var ssw = (ushort)(size | modifier | (trace == 0x8000 ? 0x2000 : 0)); + ushort[] statuses = [(ushort)(size | modifier | 0x80), 0, 0]; + uint[] addresses = [fixture.DestinationAddress, 0, 0]; + WriteDefined(12, ssw, 2); e.MemoryMasks[frame + 13] = 0x7f; + foreach (uint n in new uint[] { 14, 16 }) + { WriteDefined(n, 0, 2); e.MemoryMasks[frame + n + 1] = 0x80; } + WriteDefined(18, statuses[0], 2); WriteDefined(20, addresses[0], 4); WriteDefined(40, addresses[0], 4); + if (trace == 0x8000) WriteDefined(8, SyntheticMachine.Code, 4); + for (var n = 0; n < fixture.Width; n++) + { + var at = frame + 44 + ((addresses[0] + (uint)n) & 3); + e.Memory[at] = (byte)(fixture.OperandValue >> (8 * (fixture.Width - n - 1))); e.MemoryMasks[at] = 255; + } + bus.Arm(addresses[0], M68kBusAccessKind.CpuDataWrite); + ExecuteCore(m, batch); + if (bus.Rejected.Count != 1 || bus.Rejected[0].Address != addresses[0] || + bus.Rejected[0].Width != fixture.Width && !(fixture.Width == 4 && bus.Rejected[0].Width == 2)) + throw new InvalidOperationException("Rejected operand width/address changed"); + // Some existing fallback routes retain descending word transfers. + // A completed low word before rejection is an independent partial + // store, not a whole-instruction retry or an atomic bus guarantee. + foreach (var access in bus.Accesses.Take(bus.Rejected[0].AccessCount).Where(a => a.Write)) + { + var offset = unchecked(access.Address - addresses[0]); + if (fixture.Width != 4 || access.Width != 2 || offset != 2 || access.Value != (fixture.OperandValue & 0xffff)) + throw new InvalidOperationException("Unexpected store before operand rejection"); + if (access.Address < frame || access.Address >= frame + 60) + e.Write(access.Address, fixture.OperandValue & 0xffff, 2, m.Model); + } + mismatch = e.Verify(m); + if (mismatch == null && bus.Accesses.Skip(bus.Rejected[0].AccessCount).Any(a => a.Write && + (a.Address < frame || a.Address >= frame + 60))) mismatch = "Rejected operand was repeated during entry"; + if (mismatch != null) throw new InvalidOperationException("fault entry: " + mismatch); + // Undefined frame bytes are opaque inputs to the software handler. + // Their values qualify neither the CPU's defined lanes nor the final + // intended store, which are checked independently from OperandValue. + for (uint n = 8; n < 60; n++) e.Memory[frame + n] = bus.Peek(frame + n); + e.MemoryMasks.Clear(); + uint[] encoded = [m.PeekPhysical(frame + 44, 4), 0, 0]; + var originalD = (uint[])e.D.Clone(); var originalA = (uint[])e.A.Clone(); + var stores = new List<(uint Address, int Width, uint Value)>(); + var steps = 0; + while (e.Pc != fixture.NextPc && e.Pc != TraceHandler) + { + if (++steps > 120) throw new InvalidOperationException("Handler failed to terminate"); + var instruction = program.Instructions.Single(i => i.Pc == e.Pc); + program.Expect(instruction, m, e, frame, statuses, addresses, encoded, originalD, originalA, 3, stacks, stores); + if (instruction.Operation == "return") + { + if (trace != 0x8000) + { stacks[exceptionBank] = frame + 60; M68040StackFixture.SetStacks(e, stacks, savedSr); e.Pc = fixture.NextPc; } + else + { + stacks[exceptionBank] = frame + 48; M68040StackFixture.SetStacks(e, stacks, (ushort)((savedSr | 0x2000) & 0x3fff)); + e.Write(frame + 48, savedSr, 2, m.Model); e.Write(frame + 50, fixture.NextPc, 4, m.Model); + e.Write(frame + 54, 0x2024, 2, m.Model); e.Write(frame + 56, SyntheticMachine.Code, 4, m.Model); + e.Pc = TraceHandler; e.ExceptionVector = 9; + e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence + 2); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, fixture.NextPc); + } + } + mismatch = Execute(m, e, batch); + if (mismatch != null) throw new InvalidOperationException($"handler {instruction.Operation}/WB{instruction.Slot}: {mismatch}"); + } + if (!stores.SequenceEqual(new[] { (addresses[0], fixture.Width, fixture.OperandValue) })) + throw new InvalidOperationException("Handler store differs from independently intended operand"); + if (trace == 0x8000) + { + stacks[exceptionBank] = frame + 60; M68040StackFixture.SetStacks(e, stacks, savedSr); e.Pc = fixture.NextPc; + mismatch = Execute(m, e, batch); if (mismatch != null) throw new InvalidOperationException("trace RTE: " + mismatch); + } + e.D[7] = 0x55; e.Sr = (ushort)(savedSr & 0xfff0); e.Pc = fixture.NextPc + 2; + if (trace == 0x8000) + { + stacks[exceptionBank] -= 12; var sp = stacks[exceptionBank]; + var tracedSr = e.Sr; + M68040StackFixture.SetStacks(e, stacks, (ushort)((tracedSr | 0x2000) & 0x3fff)); + e.Write(sp, tracedSr, 2, m.Model); e.Write(sp + 2, fixture.NextPc + 2, 4, m.Model); + e.Write(sp + 6, 0x2024, 2, m.Model); e.Write(sp + 8, fixture.NextPc, 4, m.Model); + e.Pc = TraceHandler; e.ExceptionVector = 9; + e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence + 3); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, tracedSr); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, fixture.NextPc + 2); + } + mismatch = Execute(m, e, batch); if (mismatch != null) throw new InvalidOperationException("following MOVEQ: " + mismatch); + + void WriteDefined(uint offset, uint data, int width) + { e.Write(frame + offset, data, width, m.Model); for (var n = 0; n < width; n++) e.MemoryMasks[frame + offset + (uint)n] = 255; } + } + catch (Exception ex) + { mismatch = ex.ToString(); status = ex is UnsupportedM68040InstructionException or UnsupportedM68kOpcodeException or UnsupportedM68kTimingException ? "unsupported" : "mismatching"; } + report.Record(id, status, mismatch); + return (status, mismatch); + } + + private static string? Execute(SyntheticMachine m, ArchitecturalExpectation e, bool batch) + { + ExecuteCore(m, batch); + return e.Verify(m); + } + private static void ExecuteCore(SyntheticMachine m, bool batch) + { + if (!batch) m.Core.ExecuteInstruction(); + else if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, m.Core.State.Cycles + 1000, new Boundary()) != 1) + throw new InvalidOperationException("Expected exactly one batch instruction"); + } + private sealed class Boundary : IM68kInstructionBoundary + { public bool BeforeInstruction() => true; public void AfterInstruction(long previousCycle, long currentCycle) { } } + + [Theory, Trait("Suite", "Synthetic")] + [InlineData(1, 0x10c0)] + [InlineData(2, 0x30c0)] + [InlineData(4, 0x20c0)] + public void MovePostincrementWriteFaultCreatesPendingWriteback(int width, int opcode) + { + var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + m.Reset(31); + m.Core.State.D[0] = 0x12345678; + m.Core.State.A[0] = 0x4201; + m.InitializePhysical(0x1000, (uint)opcode, 2); + m.InitializePhysical(0x1002, 0x7e55, 2); + m.InitializePhysical(0x9020, 0x4e73, 2); + m.InitializePhysical(0x9022, 0x4e71, 2); + m.InitializePhysical(0x4200, 0xaabbccdd, 4); + m.Start(); + var sequence = m.Core.State.ExceptionSequence; + bus.Arm(0x4201, M68kBusAccessKind.CpuDataWrite); + m.Core.ExecuteInstruction(); + Assert.Equal(0x9020u, m.Core.State.ProgramCounter); + Assert.Equal(sequence + 1, m.Core.State.ExceptionSequence); + Assert.False(m.Core.State.Halted); + Assert.Equal(0x4700u - 60, m.Core.State.A[7]); + var frame = m.Core.State.A[7]; + Assert.Equal(0x7008u, m.PeekPhysical(frame + 6, 2)); + Assert.Equal(0x1002u, m.PeekPhysical(frame + 2, 4)); + Assert.Equal(0x4201u + (uint)width, m.Core.State.A[0]); + Assert.Equal(0x2710u, m.PeekPhysical(frame, 2)); + var size = width == 1 ? 0x20u : width == 2 ? 0x40u : 0; + Assert.Equal(size | 5, m.PeekPhysical(frame + 12, 2) & 0xff7f); + Assert.Equal(size | 0x85, m.PeekPhysical(frame + 18, 2)); + Assert.Equal(0u, m.PeekPhysical(frame + 14, 2)); + Assert.Equal(0u, m.PeekPhysical(frame + 16, 2)); + Assert.Equal(0x4201u, m.PeekPhysical(frame + 20, 4)); + Assert.Equal(0x4201u, m.PeekPhysical(frame + 40, 4)); + // Check only the defined WB1 bus lanes, independently from CPU alignment. + for (var n = 0; n < width; n++) + Assert.Equal((byte)(0x12345678u >> (8 * (width - n - 1))), + bus.Peek(frame + 44 + (uint)((1 + n) & 3))); + Assert.Equal(0xaabbccddu, m.PeekPhysical(0x4200, 4)); + Assert.Single(bus.Rejected); + Assert.DoesNotContain(bus.Accesses, a => a.Write && a.Address == 0x4201); + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040WritebackProgram.cs b/Copper68k.Tests/Synthetic/SyntheticM68040WritebackProgram.cs index 029fa375..835974af 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040WritebackProgram.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040WritebackProgram.cs @@ -9,13 +9,14 @@ internal sealed record Instruction(uint Pc, ushort[] Words, string Operation, in internal const uint Entry = SyntheticMachine.Code; internal readonly List Instructions = []; internal readonly Dictionary Labels = []; - private uint pc = Entry; + private uint pc; internal static int StatusOffset(int slot) => 20 - 2 * slot; internal static int AddressOffset(int slot) => 48 - 8 * slot; internal static int DataOffset(int slot) => AddressOffset(slot) + 4; - internal SyntheticM68040WritebackProgram() + internal SyntheticM68040WritebackProgram(uint entry = Entry) { + pc = entry; Emit("save", 0, [0x48e7, 0xf0c0]); // MOVEM.L D0-D3/A0-A1,-(A7) Emit("frame", 0, [0x43ef, 24]); // LEA 24(A7),A1 Emit("save-DFC", 0, [0x4e7a, 0x3001]); diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index e17a1cb8..9871ca0f 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -1861,7 +1861,9 @@ internal readonly record struct M68040MmuFault( uint Status, uint? StackedProgramCounter = null, int ByteCount = 0, - bool RteFrameValidation = false); + bool RteFrameValidation = false, + uint? WriteValue = null, + bool CompletedMoveWrite = false); internal sealed class UnsupportedM68040InstructionException : M68kEmulationException { @@ -1918,6 +1920,10 @@ internal sealed class M68040LogicalBus : IM68kBus, IM68kCodeReader, IM68kFastMem private PhysicalMapPageCache _instructionMapCache; private PhysicalMapPageCache _dataReadMapCache; private PhysicalMapPageCache _dataWriteMapCache; + private (uint Address, uint Value)? _descendingLongWrite; + + internal void BeginDescendingLongWrite(uint address, uint value) => _descendingLongWrite = (address, value); + internal void EndDescendingLongWrite() => _descendingLongWrite = null; public M68040LogicalBus(IM68kBus physicalBus, M68kCpuState state) { @@ -1972,15 +1978,15 @@ public uint ReadLong(uint address, ref long cycle, M68kBusAccessKind accessKind) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteByte(uint address, byte value, ref long cycle, M68kBusAccessKind accessKind) - => _physicalBus.WriteByte(Translate(address, accessKind, write: true, byteCount: 1), value, ref cycle, accessKind); + => _physicalBus.WriteByte(Translate(address, accessKind, write: true, byteCount: 1, writeValue: value), value, ref cycle, accessKind); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteWord(uint address, ushort value, ref long cycle, M68kBusAccessKind accessKind) - => _physicalBus.WriteWord(Translate(address, accessKind, write: true, byteCount: 2), value, ref cycle, accessKind); + => _physicalBus.WriteWord(Translate(address, accessKind, write: true, byteCount: 2, writeValue: value), value, ref cycle, accessKind); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteLong(uint address, uint value, ref long cycle, M68kBusAccessKind accessKind) - => _physicalBus.WriteLong(Translate(address, accessKind, write: true, byteCount: 4), value, ref cycle, accessKind); + => _physicalBus.WriteLong(Translate(address, accessKind, write: true, byteCount: 4, writeValue: value), value, ref cycle, accessKind); public bool HasHostGateway(uint address) { @@ -2069,7 +2075,7 @@ public bool TryReadFastLong(uint address, M68kBusAccessKind accessKind, out uint public bool TryWriteFastByte(uint address, byte value, M68kBusAccessKind accessKind) { - var physical = Translate(address, accessKind, write: true, byteCount: 1); + var physical = Translate(address, accessKind, write: true, byteCount: 1, writeValue: value); if (_fastMemoryBus is null || !CanFastWritePhysical(physical)) { return false; @@ -2080,7 +2086,7 @@ public bool TryWriteFastByte(uint address, byte value, M68kBusAccessKind accessK public bool TryWriteFastWord(uint address, ushort value, M68kBusAccessKind accessKind) { - var physical = Translate(address, accessKind, write: true, byteCount: 2); + var physical = Translate(address, accessKind, write: true, byteCount: 2, writeValue: value); if (_fastMemoryBus is null || !CanFastWritePhysical(physical)) { return false; @@ -2091,7 +2097,7 @@ public bool TryWriteFastWord(uint address, ushort value, M68kBusAccessKind acces public bool TryWriteFastLong(uint address, uint value, M68kBusAccessKind accessKind) { - var physical = Translate(address, accessKind, write: true, byteCount: 4); + var physical = Translate(address, accessKind, write: true, byteCount: 4, writeValue: value); if (_fastMemoryBus is null || !CanFastWritePhysical(physical)) { return false; @@ -2115,21 +2121,21 @@ M68020MemoryTarget.ExpansionRam or [HotPath] [MethodImpl(MethodImplOptions.AggressiveInlining)] - private uint Translate(uint address, M68kBusAccessKind accessKind, bool write, int byteCount) + private uint Translate(uint address, M68kBusAccessKind accessKind, bool write, int byteCount, uint? writeValue = null) { var mmu = _mmu; if (mmu.BypassTranslation) { return _allPhysicalAddressesMapped ? address - : AcceptPhysicalAddress(address, byteCount, accessKind, write); + : AcceptPhysicalAddress(address, byteCount, accessKind, write, writeValue); } if (!mmu.Enabled) { return _allPhysicalAddressesMapped ? address - : AcceptPhysicalAddress(address, byteCount, accessKind, write); + : AcceptPhysicalAddress(address, byteCount, accessKind, write, writeValue); } return TranslateEnabled(address, accessKind, write, byteCount); @@ -2140,14 +2146,15 @@ private uint AcceptPhysicalAddress( uint address, int byteCount, M68kBusAccessKind accessKind, - bool write) + bool write, + uint? writeValue) { if (IsPhysicalAddressMapped(address, byteCount, accessKind)) { return address; } - return ThrowPhysicalAddressFault(address, accessKind, write, byteCount); + return ThrowPhysicalAddressFault(address, accessKind, write, byteCount, writeValue); } [MethodImpl(MethodImplOptions.NoInlining)] @@ -2184,12 +2191,20 @@ private uint ThrowPhysicalAddressFault( uint address, M68kBusAccessKind accessKind, bool write, - int byteCount) + int byteCount, + uint? writeValue) { var supervisor = (_state.StatusRegister & M68kCpuState.Supervisor) != 0; + var fault = CreatePhysicalAddressFault(address, accessKind, write, supervisor) with { ByteCount = byteCount, WriteValue = writeValue }; + // The existing integer fallback writes predecrement longs as two + // descending words. Preserve its transfers, while describing the + // original operand in the 040 fault rather than one bus fragment. + if (write && accessKind == M68kBusAccessKind.CpuDataWrite && _descendingLongWrite is { } operand && + unchecked(address - operand.Address) < 4) + fault = fault with { LogicalAddress = operand.Address, ByteCount = 4, WriteValue = operand.Value }; throw new M68040MmuFaultException( WithStackedProgramCounter( - CreatePhysicalAddressFault(address, accessKind, write, supervisor) with { ByteCount = byteCount }, + fault, address, accessKind)); } @@ -2545,10 +2560,46 @@ protected override bool TryHandleM68040ExecutionFault(M68040MmuFault fault) // half of an aligned long. Restart the executing instruction. if (!State.M68040Mmu.Enabled && fault.AccessKind == M68kBusAccessKind.CpuInstructionFetch) fault = fault with { StackedProgramCounter = ExecutionBoundaryProgramCounter }; + else if (TryCompleteFaultedMoveWrite(fault)) + fault = fault with { StackedProgramCounter = State.ProgramCounter, CompletedMoveWrite = true }; RaiseMmuFault(fault); return true; } + private bool TryCompleteFaultedMoveWrite(M68040MmuFault fault) + { + // A normal MOVE has one destination store. At this point its source + // and extensions have already been consumed; latch the actual store + // data/address rather than decoding or executing any operand again. + // This is not a MOVEM/MOVE16/MMU or arbitrary write restart policy. + var opcode = State.LastOpcode; + var top = opcode >> 12; + var mode = (opcode >> 6) & 7; + var register = (opcode >> 9) & 7; + var width = top == 1 ? 1 : top == 3 ? 2 : 4; + var sourceMode = (opcode >> 3) & 7; + if (State.ExceptionSequence != ExecutionBoundaryExceptionSequence || + State.LastInstructionProgramCounter != ExecutionBoundaryProgramCounter || + State.M68040Mmu.Enabled || !fault.Write || fault.WriteValue is not uint value || + fault.AccessKind != M68kBusAccessKind.CpuDataWrite || top is < 1 or > 3 || + mode < 2 || mode == 7 && register > 1 || fault.ByteCount != width || + top == 1 && sourceMode == 1 || sourceMode == 7 && (opcode & 7) > 4) + return false; + if (mode is 3 or 4) + { + var next = mode == 4 ? fault.LogicalAddress : + unchecked(fault.LogicalAddress + (uint)(width == 1 && register == 7 ? 2 : width)); + if (register == 7) State.SetActiveStackPointer(next); + else State.A[register] = next; + } + var mask = width == 1 ? 0xffu : width == 2 ? 0xffffu : uint.MaxValue; + var sign = width == 1 ? 0x80u : width == 2 ? 0x8000u : 0x80000000u; + value &= mask; + State.StatusRegister = (ushort)((State.StatusRegister & 0xfff0) | + (value == 0 ? 4 : 0) | ((value & sign) != 0 ? 8 : 0)); + return true; + } + private void InvalidateInstructionCacheForHostCodeVisibilityChange() { var stableMap = _stablePhysicalAddressMap; @@ -4274,6 +4325,8 @@ private void RaiseMmuFault(M68040MmuFault fault) RaiseRteValidationAccessFault(fault, stackedProgramCounter); else if (!State.M68040Mmu.Enabled && fault.AccessKind == M68kBusAccessKind.CpuInstructionFetch) RaiseUnbufferedReadAccessFault(fault, stackedProgramCounter, instruction: true); + else if (fault.CompletedMoveWrite) + RaiseCompletedMoveWriteAccessFault(fault, stackedProgramCounter); else RaiseFormat0Exception(VectorBusError, stackedProgramCounter, M68kInstructionTimingKey.IllegalInstruction); } @@ -4291,6 +4344,32 @@ private void RaiseMmuFault(M68040MmuFault fault) private void RaiseRteValidationAccessFault(M68040MmuFault fault, uint instructionPc) => RaiseUnbufferedReadAccessFault(fault, instructionPc, instruction: false); + private void RaiseCompletedMoveWriteAccessFault(M68040MmuFault fault, uint nextPc) + { + // MC68040UM 8.4.6.5/7: physical normal write -> valid WB1, FA=WB1A, + // memory-aligned data. RTE resumes at the following instruction. + var savedSr = State.StatusRegister; + var size = fault.ByteCount == 1 ? 0x20 : fault.ByteCount == 2 ? 0x40 : 0; + var modifier = (savedSr & M68kCpuState.Supervisor) != 0 ? 5 : 1; + var trace = (savedSr & 0x8000) != 0; // MOVE does not trigger T0. + uint data = fault.WriteValue!.Value; + if (fault.ByteCount == 1) data = (data & 0xff) << 24; + else if (fault.ByteCount == 2) data = (data & 0xffff) << 16; + var shift = (int)(fault.LogicalAddress & 3) * 8; + if (shift != 0) data = (data >> shift) | (data << (32 - shift)); + State.RecordException(VectorBusError, nextPc, savedSr); + State.StatusRegister = (ushort)((savedSr | M68kCpuState.Supervisor) & ~0xc000); + for (var n = 0; n < 3; n++) PushLong(0); // Undefined push data. + PushLong(data); PushLong(fault.LogicalAddress); + for (var n = 0; n < 4; n++) PushLong(0); // No WB2/WB3 in this synchronous path. + PushLong(fault.LogicalAddress); + PushWord((ushort)(0x80 | size | modifier)); PushWord(0); PushWord(0); + PushWord((ushort)((trace ? 0x2000 : 0) | size | modifier)); + PushLong(trace ? State.LastInstructionProgramCounter : 0); + PushWord(0x7008); PushLong(nextPc); PushWord(savedSr); + EnterAccessErrorHandler(); + } + private void RaiseUnbufferedReadAccessFault(M68040MmuFault fault, uint instructionPc, bool instruction) { // MC68040UM 8.4.6/7: instruction-prefetch faults have no pending @@ -4313,6 +4392,11 @@ private void RaiseUnbufferedReadAccessFault(M68040MmuFault fault, uint instructi PushWord(0x7008); PushLong(instructionPc); PushWord(savedSr); + EnterAccessErrorHandler(); + } + + private void EnterAccessErrorHandler() + { State.ProgramCounter = ReadLong(State.VectorBaseRegister + 8); DiscardInstructionPrefetch(); if ((State.ProgramCounter & 1) != 0) diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index f007ccbd..5ecc59ba 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -4505,10 +4505,12 @@ public virtual int ExecuteInstruction() } protected uint ExecutionBoundaryProgramCounter { get; private set; } + protected uint ExecutionBoundaryExceptionSequence { get; private set; } private void ExecuteInstructionCore() { ExecutionBoundaryProgramCounter = State.ProgramCounter; + ExecutionBoundaryExceptionSequence = State.ExceptionSequence; try { ExecuteInstructionWithTrace(); @@ -4710,6 +4712,7 @@ private bool TryExecuteHotBlock( var previousCycle = State.Cycles; var exitBlock = false; ExecutionBoundaryProgramCounter = State.ProgramCounter; + ExecutionBoundaryExceptionSequence = State.ExceptionSequence; try { if (!TryFetchHotOpcode(in hotInstruction, out var opcode) || @@ -4804,6 +4807,8 @@ private bool TryExecuteModelSpecificHotBlock( var previousCycle = State.Cycles; var exitBlock = false; + ExecutionBoundaryProgramCounter = State.ProgramCounter; + ExecutionBoundaryExceptionSequence = State.ExceptionSequence; try { if (!TryFetchHotOpcode(in hotInstruction, out var opcode) || @@ -4941,6 +4946,7 @@ private bool TryExecuteShortSelfBranchBlock( var previousCycle = State.Cycles; ushort opcode; ExecutionBoundaryProgramCounter = State.ProgramCounter; + ExecutionBoundaryExceptionSequence = State.ExceptionSequence; bool cacheHit, requiresSynchronization; long completedMachineCycle; try diff --git a/Copper68k/M68kCore.cs b/Copper68k/M68kCore.cs index f984db05..373d2bf8 100644 --- a/Copper68k/M68kCore.cs +++ b/Copper68k/M68kCore.cs @@ -14262,9 +14262,15 @@ private void WriteLong(uint address, uint value) private void WriteLongDescending(uint address, uint value) { - if ((address & 1) != 0) PrepareM68010LongMoveFault(address, value, 4); - WriteWord(address + 2, (ushort)value); - WriteWord(address, (ushort)(value >> 16)); + var faultContext = _bus as M68040LogicalBus; + faultContext?.BeginDescendingLongWrite(address, value); + try + { + if ((address & 1) != 0) PrepareM68010LongMoveFault(address, value, 4); + WriteWord(address + 2, (ushort)value); + WriteWord(address, (ushort)(value >> 16)); + } + finally { faultContext?.EndDescendingLongWrite(); } } [DoesNotReturn] diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 0b368f78..9d1f51bb 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -6636,3 +6636,147 @@ full handler/reference/hardware qualification remain required. Other advanced restoration and software disagreements are unchanged. No production fix, regression retirement, new consumer replay or package publication is included. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +## 040 actual MOVE write-fault qualification — 2026-10-06 + +The 040 interpreter previously stacked a short generic bus-error frame when a +normal MOVE destination write failed. Its source/extensions were already consumed, +but the frame lacked the pending data needed to finish the store. Production now +captures the actual store address/value/width, completes the destination base and +MOVE flags, and stacks the consumed following PC in a 60-byte format-7 frame. +WB1 contains valid SIZE/TM, FA=WB1A and memory-aligned WB1D. WB2/WB3 are invalid; +undefined fields use the existing zero convention. Incoming T1 sets CT and the +instruction EA; MOVE does not trigger T0. Expectations follow +[MC68040UM](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf), 8.4.6, +8.4.6.2, 8.4.6.5/table 8-5 and 8.4.6.7. Retained local manual SHA256: +`93741393f70656941e413beb232c060a5e4e0218ea2adc2f5b392f9165454a7f`. + +Eligibility is limited to legal ordinary MOVE operand writes rejected by the +physical address map, with translation disabled and the same instruction and +exception boundary. The fallback's descending word transfers retain their +existing order/widths; an internal context preserves the full original long +operand in the fault. No operand is decoded or executed again. The boundary +guard prevents a trace-frame store fault from completing an already-retired +MOVE a second time. Handler entry, double-fault routing and existing timing +policy are retained. No public API changes are made. + +Independent MOVE fixtures choose final registers, operand address/value and +following PC. Actual physical-map rejection enters the handler; no supplied +fault exception substitutes for the instruction. The shared integer handler +normalizes WB1, executes MOVES, clears validity, restores registers/DFC and RTEs. +Every instruction checks state and memory. The final store is compared with the +independently intended operand, not with production frame data. Defined frame +fields/data lanes are independently checked first; undefined bytes then become +opaque handler inputs. A completed fallback low-word store is checked separately +and preserved. T1 cases exercise format-2 trace conversion, trace-handler RTE and +the following traced sentinel; T0 cases verify normal MOVE continuation. + +| Group, each scalar/batch route | Programs | Combinations | Weight | +| --- | ---: | ---: | ---: | +| All legal memory-destination MOVE opcodes | 7,350 | 7,350 | 1 | +| Lanes/sizes/values/CCR/stack banks | 9,216 | 288 | 32 CCRs | +| Full-index structures/aliases/trace | 14,256 | 14,256 | 1 | +| Both routes | **61,644** | **43,788** | | + +Boundary cases cover B/W/L, four lanes, six values, all CCRs, user/user-M/ISP/MSP +and A7 byte stride. Indexed cases cover all 66 legal full-format structures, +sizes, register 0/7, four banks, trace 0/T1/T0 and source/destination/dual forms. +Opcode cases include source/destination register aliases. Six bounded executions +protect postincrement widths, aliased predecrement, indexed CT and trace-frame +fault routing. The trace-frame witness qualifies preservation/routing only; +its full nested exception-frame/recovery protocol remains open. + +Maintained commands: + +```powershell +./scripts/test-copper68k-040-operand-writes.ps1 -OutputDirectory artifacts/040-operand-writes +./scripts/test-copper68k-040-operand-writes.ps1 -ValidateReportsOnly -OutputDirectory artifacts/040-operand-writes +./scripts/test-copper68k-040-operand-write-mutations.ps1 -OutputDirectory artifacts/040-operand-write-mutations +./scripts/test-copper68k-040-operand-write-mutations.ps1 -ValidateReportsOnly -OutputDirectory artifacts/040-operand-write-mutations +``` + +Fresh/frozen qualification at `artifacts/m6-real-write-qualified-final-v3/` passes +all twelve exact executions without skips. Inputs are captured before execution; +source, binary, evidence, exact selection and independent keys/weights must match. +Seven maintained CPU mutations at `artifacts/m6-real-write-mutations-final/` detect +lost original width, omitted predecrement, wrong bus lanes, lost data, missing CT, +missing N and missing exception-boundary qualification. The six-test baseline +and seven six-test mutants produce 48 exact executions; expected methods/reasons +are checked. Original source bytes and normal assemblies are restored/preserved. +No old regression is retired. + +Eight positive-evidence and seven mutation-evidence controls are recorded in +`artifacts/m6-real-write-controls-final/controls.json` and +`artifacts/m6-real-write-mutation-controls-final/controls.json`. They reject +wrong profiles, empty/duplicate inputs, missing reports, foreign executions, +substituted keys, redistributed weights, hidden unsupported results and altered +mutation source/method/reason/status. Evidence hashes are refreshed before +semantic rejection. The isolated `concurrency-guard.json` verifies restoration +and refusal to overwrite a concurrent edit. + +The fresh full run at `artifacts/m6-real-write-full-qualified/` passes **5,166 +tests / zero failures / nineteen explicitly unavailable tests**, total 5,185, +in 48 minutes 37 seconds. Independent verification matches the 5,153 discovered +methods, the retained 33-row theory expansion, exact nineteen skip names and all +nine pinned WinUAE presets. The ordinary gate validates **85,834,306 scenarios / +703 reports**. Source/fixture/script identities are captured before execution and +checked afterward; normal CPU/test binaries remain unchanged. `before.json`, +`after.json`, `full-identity.json` and `summary.json` preserve the evidence. +Final isolated CPU SHA256: +`00dece901073a2918cb370410bd049f8f39bddbbb27890a133fd2d3f72a14604`; +test adapter: +`ac030d21cbf4fd2820723ff7b81f9c43f1beb734e5bf08ca369abe8b3d2ad941`. +WinUAE generator pin `025b999239800357e95065fe5b9a15ea5b300fa7` and runner pin +`7a83745d6c6159bc74ab0471578ffc8bc244e66e`, native/input/source fingerprints and +callback/frame counts match the retained qualification presets. These selected +software comparisons do not qualify a physical write-fault pipeline. The records +below are documentation added after that verified execution; CPU/test/script +source is unchanged. + +Final software-semantic audits at `artifacts/m6-real-write-external-final/` use +the same CPU/test assemblies as the final full run. SingleStepTests pin +`64b253116a3de04aaac4346c43680960dc9b67e5` passes 312,500 cases / 125 files. +Musashi pin `72c1d74800f3087b45a0c1a7342601bbed898881` passes 536 combinations, +with 88 explicitly excluded from 624 rows. These references do not provide an +external physical 040 write-fault/frame or timing oracle. + +The immutable unpublished `1.5.2-synthetic-dev.66` package has SHA256 +`d33a5b2c55a44acfa3370666351f6eb5ec6a5b76e04f29d5b1ceec8e4e22d24f`. +The isolated CopperScreen consumer builds Release with zero warnings/errors; +host tests pass 149 with six optional unavailable tests, disk 74, separate engine +diagnostics 1,080, and native boot/persistence replays all three without skips. +Four restore assets select `.66`; all four loaded CPU DLLs match packed CPU SHA256 +`1ded1424442ebc6f58145ddfc86440d6949bd97e2872731505c630aee177d33e`. +`artifacts/m6-real-write-consumer-final-identity.json` records package/assets/DLLs, +TRX identities and native inputs. Pinned native SHA256 values: + +- Kickstart 3.1 A500: `8c8a0cf04f91b88eaf0c4f1126041987067e2286a8ee590bdbae447a8000c5ee`. +- Workbench 3.1 ADF: `a8f167bad2897e8c7f2cfe73de7a9f8dad10c7a5b1f78ca17f818ab17278b985`. +- Kickstart 3.0 A1200 ZIP: `eb63ba9ceff1ac12bb025389434946101ae71dd064b8a830e8ae8fe111c1be39`. +- Pristine AGA probe HDF: `3bbab58c135844aad802ce937d49ec5da26d6407303c9d9fc543fc6cc63e9c85`. + +The two Workbench rows use 0/2 MiB Fast RAM; the A1200 row checks eight-plane +display and persistence through desktop reopen. Media stays local. Earlier +private `.64`/`.65` packages remain immutable. Root CopperScreen changes and its +published dependency pin are untouched; no public release is performed. + +Ordinary requirements are **85,834,306 scenarios / 703 reports**. Complete-040 +requirements are **67,856,332 scenarios / 92 reports and 30 fixed witnesses**, +with exactly 122 discovered executions. Static requirements and new key-branch +checks are recorded in `artifacts/m6-real-write-complete-audit-requirements.json`, +`artifacts/m6-real-write-complete-audit-key-check.json` and +`artifacts/m6-real-write-complete-discovery.json`. No passing complete-040 run is +claimed. Fresh remaining-inventory execution intentionally fails one exact +method for **480 untested cases / fifteen combinations**, retaining every prior +category (`artifacts/m6-real-write-remaining-inventory/verified-inventory.json`). +Actual normal MOVE writes are now partial coverage within that inventory; +other reads/integer/MOVEM/MOVE16, nested writebacks, mixed-epoch repair, CP/CM and +trace-entry recovery remain required. Enabled MMU, FPU arithmetic, physical +pipeline/cache timing and OS compatibility remain outside this roadmap. + +Initial bounded failures, incomplete matrix drafts, failed mutation expectations +and two stopped full attempts remain separate historical evidence under +`artifacts/m6-real-write-*`. One stopped full attempt exposed the obsolete short +MOVE-frame expectation; the next was superseded by the trace-entry boundary fix. +Neither is a complete-suite success. Final manifests are not applied retroactively +to them. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 87f33ac4..663afbff 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2488,3 +2488,48 @@ frames do not qualify enabled-MMU operation, physical function-code spaces, hardware or timing. See the [qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040-supplied-write-back-handler-qualification--2026-10-06). Milestone 6 stays **in progress**, `roadmapComplete=false`; no package publication. + +### Milestone 6 actual 040 MOVE write faults — 2026-10-06 + +The production 040 path now constructs a documented format-7 pending-writeback +frame for ordinary MOVE destination writes rejected by the host physical map, +with translation disabled. It preserves consumed source/extensions, latched +store address/data, completed destination base/flags and following PC. The +integer handler completes WB1 and returns without replaying the MOVE. Retained +descending long transfers describe the original operand in the fault while +preserving successful transfer order/widths and existing timing policy. An +instruction/exception boundary guard prevents a rejected trace-frame store from +being mistaken for another MOVE operand. No public API changes are made. + +Six ordinary scalar/batch matrices pass **61,644 whole-program scenarios / +43,788 combinations**: every legal memory-destination MOVE opcode, independent +lane/size/value/CCR/stack boundaries, and full-index structures/aliases/trace. +All twelve exact executions pass without skips, including six bounded witnesses. +Every handler instruction, defined frame field/data lane, preserved state, +ordered store, RTE and following instruction is checked. Seven maintained CPU +mutations detect operand width, predecrement, lanes, data, CT, N and exception +boundary defects. Baseline plus mutants execute 48 tests; source restores +byte-for-byte. Eight report and seven mutation integrity controls reject their +intended errors, and the isolated restoration guard preserves concurrent edits. + +Ordinary requirements become **85,834,306 scenarios / 703 reports**. Complete-040 +requirements become **67,856,332 scenarios / 92 reports and 30 fixed witnesses**, +with exactly 122 discovered executions. Static requirements/key validation is +separate from a passing complete audit. Its remaining inventory still fails +exactly **480 untested cases / fifteen combinations**, retaining all five +protocol categories. Read/other integer/MOVEM/MOVE16 fault construction, +trace-entry recovery, nested writebacks and advanced restoration remain open. + +A fresh full CPU run passes **5,166 tests / zero failures / nineteen explicitly +unavailable tests**, total 5,185, with the exact roster/theory expansion/skips and +nine pinned WinUAE selections verified. Its ordinary semantic gate validates all +85,834,306 scenarios / 703 reports. Source and normal assemblies are checked +before/after; documentation follows that execution. + +The final pinned SingleStepTests and Musashi audits pass their selected semantic +cases. The isolated unpublished `.66` package passes the CopperScreen Release +build, host/disk/engine checks and all three native replays. No regression is +retired and no public package is published. See the +[qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040-actual-move-write-fault-qualification--2026-10-06) +for exact identities and reference limitations. Milestone 6 remains +**in progress**, `roadmapComplete=false`. diff --git a/scripts/copper68k-040-operand-write-keys.ps1 b/scripts/copper68k-040-operand-write-keys.ps1 new file mode 100644 index 00000000..1490754c --- /dev/null +++ b/scripts/copper68k-040-operand-write-keys.ps1 @@ -0,0 +1,48 @@ +# Test-independent inventory from the MOVE encoding and full-EA tables. +function Get-Copper68k040OperandWriteKeys([string]$Matrix) { + if ($Matrix -cnotin @('opcodes','boundaries','indexed')) { throw 'Unknown operand-write matrix' } + $keys = [Collections.Generic.Dictionary[string,int]]::new([StringComparer]::Ordinal) + function Operand-Name([int]$Mode,[int]$Register) { + switch ($Mode) { + 0 { "D$Register" } 1 { "A$Register" } 2 { "(A$Register)" } 3 { "(A$Register)+" } + 4 { "-(A$Register)" } 5 { "d16(A$Register)" } 6 { "index(A$Register)" } + 7 { @('abs.w','abs.l','d16(PC)','index(PC)','immediate')[$Register] } + } + } + function Add-Key([string]$Scenario,[int]$Width,[int]$SourceMode,[int]$SourceRegister, + [int]$DestinationMode,[int]$DestinationRegister,[string]$Bank,[string]$Trace,[int]$Weight) { + $source = Operand-Name $SourceMode $SourceRegister + $destination = Operand-Name $DestinationMode $DestinationRegister + $key = "68040/MOVE/write-fault/$Scenario/size=$Width/source=$source/destination=$destination/bank=$Bank/T=$Trace" + if (-not $keys.TryAdd($key,$Weight)) { throw "Duplicate operand-write combination: $key" } + } + if ($Matrix -ceq 'opcodes') { + for ($opcode=0x1000; $opcode -lt 0x4000; $opcode++) { + $top=$opcode -shr 12; $sm=($opcode -shr 3) -band 7; $sr=$opcode -band 7 + $dm=($opcode -shr 6) -band 7; $dr=($opcode -shr 9) -band 7 + if ($dm -lt 2 -or ($top -eq 1 -and $sm -eq 1) -or + ($sm -eq 7 -and $sr -gt 4) -or ($dm -eq 7 -and $dr -gt 1)) { continue } + $width=if($top -eq 1){1}elseif($top -eq 3){2}else{4} + Add-Key 'opcode' $width $sm $sr $dm $dr 'ISP' '0000' 1 + } + } elseif ($Matrix -ceq 'boundaries') { + foreach($width in @(1,2,4)){foreach($bank in @('user','user-M','ISP','MSP')){ + foreach($lane in 0..3){foreach($pair in 0..5){ + Add-Key "boundary/lane=$lane/pair=$pair" $width 0 0 3 7 $bank '0000' 32 + }}}} + } else { + foreach($bs in @('False','True')){foreach($is in @('False','True')){foreach($bd in 1..3){ + foreach($iis in @(0,1,2,3,5,6,7)){ + if($is -ceq 'True' -and $iis -ge 5){continue} + $spec="full/bs=$bs/is=$is/bd=$bd/iis=$iis" + foreach($width in @(1,2,4)){foreach($register in @(0,7)){foreach($bank in @('user','user-M','ISP','MSP')){ + foreach($trace in @('0000','8000','4000')){foreach($form in @('source','destination','dual')){ + $sm=if($form -ceq 'destination'){3}else{6};$dm=if($form -ceq 'source'){3}else{6} + Add-Key "indexed/$spec/reg=$register/form=$form" $width $sm $register $dm $register $bank $trace 1 + }}}}} + }}}} + } + $required=if($Matrix -ceq 'opcodes'){7350}elseif($Matrix -ceq 'boundaries'){288}else{14256} + if($keys.Count -ne $required){throw 'Operand-write reference inventory changed'} + return ,$keys +} diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index 31c34b33..f50db332 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -6,6 +6,7 @@ param( ) $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot +. (Join-Path $PSScriptRoot 'copper68k-040-operand-write-keys.ps1') $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) $expected = [ordered]@{ 'rte-access-controls' = @{cases=6912; combinations=108} @@ -40,7 +41,7 @@ $expected = [ordered]@{ 'rte-mixed-fault-structure-batch' = @{cases=3556800; combinations=711360} 'rte-access-entry-double-fault' = @{cases=98304; combinations=3072} 'rte-access-handler-refault' = @{cases=3840; combinations=120} - 'access-double-fault-dispatch-accurate' = @{cases=1088; combinations=544} + 'access-double-fault-dispatch-accurate' = @{cases=2336; combinations=1168} 'access-double-fault-dispatch-v1' = @{cases=1088; combinations=544} 'access-double-fault-dispatch-v2' = @{cases=1088; combinations=544} 'rte-validation-batch' = @{cases=129024; combinations=4032} @@ -93,6 +94,12 @@ $expected = [ordered]@{ 'rte-writeback-canonical-batch' = @{cases=36864; combinations=1152} 'rte-writeback-structure-scalar' = @{cases=36864; combinations=18432} 'rte-writeback-structure-batch' = @{cases=36864; combinations=18432} + 'move-write-fault-opcodes-scalar' = @{cases=7350; combinations=7350} + 'move-write-fault-opcodes-batch' = @{cases=7350; combinations=7350} + 'move-write-fault-boundaries-scalar' = @{cases=9216; combinations=288} + 'move-write-fault-boundaries-batch' = @{cases=9216; combinations=288} + 'move-write-fault-indexed-scalar' = @{cases=14256; combinations=14256} + 'move-write-fault-indexed-batch' = @{cases=14256; combinations=14256} 'rte-access-remaining-protocols' = @{cases=480; combinations=15} } $testExit = 0 @@ -106,7 +113,7 @@ if (-not $ValidateReportsOnly) { $saved[$name] = [Environment]::GetEnvironmentVariable($name) [Environment]::SetEnvironmentVariable($name, $settings[$name]) } - & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests|FullyQualifiedName~SyntheticM68040ChainedOddReturnTests|FullyQualifiedName~SyntheticM68040RteValidationFaultTests|FullyQualifiedName~SyntheticM68040AccessDoubleFaultTests|FullyQualifiedName~SyntheticM68040BatchFaultTests|FullyQualifiedName~SyntheticM68040RteRepairTests|FullyQualifiedName~SyntheticM68040InstructionFaultTests|FullyQualifiedName~SyntheticM68040HandlerPrefetchTests|FullyQualifiedName~SyntheticM68040UserRteFaultTests|FullyQualifiedName~SyntheticM68040WritebackTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~SyntheticM68040AccessFrameAuditTests|FullyQualifiedName~M68040AccessFrameFixtureTests|FullyQualifiedName~SyntheticM68040MovemContinuationTests|FullyQualifiedName~SyntheticM68040FpuContinuationTests|FullyQualifiedName~SyntheticM68040ThrowawayTests|FullyQualifiedName~SyntheticM68040OddReturnTests|FullyQualifiedName~SyntheticM68040ChainedOddReturnTests|FullyQualifiedName~SyntheticM68040RteValidationFaultTests|FullyQualifiedName~SyntheticM68040AccessDoubleFaultTests|FullyQualifiedName~SyntheticM68040BatchFaultTests|FullyQualifiedName~SyntheticM68040RteRepairTests|FullyQualifiedName~SyntheticM68040InstructionFaultTests|FullyQualifiedName~SyntheticM68040HandlerPrefetchTests|FullyQualifiedName~SyntheticM68040UserRteFaultTests|FullyQualifiedName~SyntheticM68040WritebackTests|FullyQualifiedName~SyntheticM68040OperandWriteFaultTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output $testExit = $LASTEXITCODE } finally { foreach ($name in $saved.Keys) { [Environment]::SetEnvironmentVariable($name, $saved[$name]) } @@ -118,7 +125,7 @@ if (-not $ValidateReportsOnly) { cpuSourceFiles=@(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } - inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ChainedOddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040SoftwareTraceProgram.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040WritebackTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040WritebackProgram.cs') | ForEach-Object { + inputs=@('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ChainedOddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040SoftwareTraceProgram.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040WritebackTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040WritebackProgram.cs','Copper68k.Tests/Synthetic/SyntheticM68040OperandWriteFaultTests.cs','Copper68k.Tests/Synthetic/MoveSpecification.cs','Copper68k.Tests/Synthetic/AddressingFixtures.cs','scripts/copper68k-040-operand-write-keys.ps1') | ForEach-Object { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } assemblies=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | ForEach-Object { @@ -145,7 +152,7 @@ if ($identity.schema -ne 1 -or $identity.reference -cne 'MC68040UM' -or $identit @($identity.assemblies).Count -ne 2) { throw '040 access-frame input identity is missing or incomplete' } foreach ($input in $identity.inputs) { $path = Join-Path $repo $input.file - if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ChainedOddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040SoftwareTraceProgram.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040WritebackTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040WritebackProgram.cs') -or + if ($input.file -notin @('Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs','scripts/test-copper68k-040-access-frames.ps1','Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040ChainedOddReturnTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040SoftwareTraceProgram.cs','Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040WritebackTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040WritebackProgram.cs','Copper68k.Tests/Synthetic/SyntheticM68040OperandWriteFaultTests.cs','Copper68k.Tests/Synthetic/MoveSpecification.cs','Copper68k.Tests/Synthetic/AddressingFixtures.cs','scripts/copper68k-040-operand-write-keys.ps1') -or $input.sha256 -cne (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()) { throw '040 access-frame fixture/command identity differs' } } $cpuFiles = @(& git -C $repo ls-files --cached --others --exclude-standard 'Copper68k/*') @@ -159,8 +166,8 @@ foreach ($assembly in $identity.assemblies) { } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counters = $trx.TestRun.ResultSummary.Counters -if ([int]$counters.executed -ne 110 -or [int]$counters.total -ne 110 -or [int]$counters.notExecuted -ne 0) { - throw '040 access-frame audit did not execute its complete selection (86 batches, 24 fixed examples/witnesses)' +if ([int]$counters.executed -ne 122 -or [int]$counters.total -ne 122 -or [int]$counters.notExecuted -ne 0) { + throw '040 access-frame audit did not execute its complete selection (92 batches, 30 fixed examples/witnesses)' } $totals = [ordered]@{passing=0; mismatching=0; unsupported=0; untested=0} foreach ($group in $expected.Keys) { @@ -183,6 +190,10 @@ foreach ($group in $expected.Keys) { $expectedCombinations["68040/RTE/format7/$form/bank=$bank"] = 32 } } + } elseif ($group.StartsWith('move-write-fault-', [StringComparison]::Ordinal)) { + $matrix=$group -replace '^move-write-fault-','' -replace '-(scalar|batch)$','' + $keys=Get-Copper68k040OperandWriteKeys $matrix + foreach($key in $keys.Keys){$expectedCombinations[$key]=$keys[$key]} } elseif ($group.StartsWith('rte-writeback-', [StringComparison]::Ordinal)) { $structure=$group.Contains('structure',[StringComparison]::Ordinal) $matrix=if($structure){'structure'}else{'canonical'} @@ -440,7 +451,7 @@ foreach ($group in $expected.Keys) { $engine = $group.Substring('access-double-fault-dispatch-'.Length) foreach ($form in @('RTE','MOVE.B.read','MOVE.W.read','MOVE.L.read','MOVE.B.write','MOVE.W.write','MOVE.L.write')) { foreach ($bank in @('ISP','MSP')) { foreach ($alignment in @(0,1)) { foreach ($stage in @('stack','vector')) { - $width = if ($stage -eq 'vector') {4} elseif ($form -eq 'RTE') {60} else {8} + $width = if ($stage -eq 'vector') {4} elseif ($form -eq 'RTE' -or $engine -eq 'accurate' -and $form.EndsWith('write')) {60} else {8} for ($byte=0; $byte -lt $width; $byte++) { $key = '68040/access-double-fault/dispatch={0}/form={1}/bank={2}/align={3}/stage={4}/byte={5}' -f $engine,$form,$bank,$alignment,$stage,$byte $expectedCombinations[$key] = 2 @@ -617,8 +628,8 @@ foreach ($group in $expected.Keys) { if ($combinationTotals[$status] -ne $report.counts.$status) { throw "$group combination totals differ" } } } -$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 110 -and +$passed = $testExit -eq 0 -and [int]$counters.failed -eq 0 -and [int]$counters.passed -eq 122 -and ($totals.mismatching + $totals.unsupported + $totals.untested) -eq 0 -@{schema=1; model='68040'; logicalCases=67793440; xunitBatches=86; fixedExamples=24; counts=$totals; passed=$passed; roadmapComplete=$false} | +@{schema=1; model='68040'; logicalCases=67856332; xunitBatches=92; fixedExamples=30; counts=$totals; passed=$passed; roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'audit-summary.json') if (-not $passed) { throw "040 access-frame audit incomplete: $($totals | ConvertTo-Json -Compress)" } diff --git a/scripts/test-copper68k-040-operand-write-mutations.ps1 b/scripts/test-copper68k-040-operand-write-mutations.ps1 new file mode 100644 index 00000000..fcabd25e --- /dev/null +++ b/scripts/test-copper68k-040-operand-write-mutations.ps1 @@ -0,0 +1,118 @@ +[CmdletBinding()] +param([switch]$ValidateReportsOnly,[string]$OutputDirectory='artifacts/040-operand-write-mutations') +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +$programPath='Copper68k/M68040Support.cs' +$source=Join-Path $repo $programPath +$prefix='Copper68k.Tests.Synthetic.SyntheticM68040OperandWriteFaultTests.' +$names=@(($prefix+'MovePostincrementWriteFaultCreatesPendingWriteback(width: 1, opcode: 4288)'), + ($prefix+'MovePostincrementWriteFaultCreatesPendingWriteback(width: 2, opcode: 12480)'), + ($prefix+'MovePostincrementWriteFaultCreatesPendingWriteback(width: 4, opcode: 8384)'), + ($prefix+'PredecrementFaultCommitsAliasedBaseOnce'),($prefix+'FullIndexedFaultKeepsLatchedOperandAndPendingTrace'),($prefix+'TraceFrameWriteFaultCannotChangeCompletedMoveDestination')) +$mutations=@( + @{name='lost-original-operand-width';anchor='LogicalAddress = operand.Address, ByteCount = 4, WriteValue = operand.Value';replacement='LogicalAddress = operand.Address, ByteCount = 2, WriteValue = operand.Value';failed=@($names[3]);reason='fault entry: A0 expected'}, + @{name='missing-predecrement-completion';anchor='if (mode is 3 or 4)';replacement='if (mode == 3)';failed=@($names[3]);reason='fault entry: A0 expected'}, + @{name='wrong-bus-lanes';anchor='var shift = (int)(fault.LogicalAddress & 3) * 8;';replacement='var shift = 0;';failed=@($names[0],$names[1],$names[2]);reason='Expected'}, + @{name='lost-latched-data';anchor='uint data = fault.WriteValue!.Value;';replacement='uint data = 0;';failed=@($names[0],$names[1],$names[2],$names[3],$names[4]);reason='expected'}, + @{name='missing-trace-continuation';anchor='var trace = (savedSr & 0x8000) != 0;';replacement='var trace = false;';failed=@($names[4]);reason='fault entry: Memory'}, + @{name='missing-negative-flag';anchor='var sign = width == 1 ? 0x80u : width == 2 ? 0x8000u : 0x80000000u;';replacement='var sign = 0u;';failed=@($names[3],$names[4]);reason='fault entry: SR expected'}, + @{name='missing-exception-boundary';anchor='State.ExceptionSequence != ExecutionBoundaryExceptionSequence ||';replacement='';failed=@($names[5]);reason='Expected'} +) +function Identity([string]$Path){ + $file=Join-Path $repo $Path + if(-not(Test-Path -LiteralPath $file -PathType Leaf)){throw "Missing writeback mutation input: $Path"} + return @{path=$Path;sha256=(Get-FileHash -LiteralPath $file).Hash} +} +function Validate-Selection($Actual,[string[]]$Required){ + $paths=@($Actual.path) + if($paths.Count -ne $Required.Count -or @($paths|Select-Object -Unique).Count -ne $paths.Count -or + ($paths.Count -gt 0 -and @(Compare-Object ($paths|Sort-Object) ($Required|Sort-Object)).Count)){throw 'Incomplete/duplicate writeback mutation identity selection'} + foreach($entry in $Actual){if($entry.sha256 -cnotmatch '^[A-F0-9]{64}$' -or (Identity $entry.path).sha256 -cne $entry.sha256){throw "Changed writeback mutation input: $($entry.path)"}} +} +function Mutated-Source([string]$Text,$Mutation){ + if(($Text.Split($Mutation.anchor).Length-1) -ne 1){throw 'Nonunique writeback mutation anchor'} + return $Text.Replace($Mutation.anchor,$Mutation.replacement) +} +function Restore-Source([byte[]]$Bytes,[string]$ExpectedHash,[string]$OriginalHash){ + $current=(Get-FileHash -LiteralPath $source).Hash + if($current -ceq $OriginalHash){return} + if($current -cne $ExpectedHash){throw 'CPU source changed concurrently; current file left intact'} + [IO.File]::WriteAllBytes($source,$Bytes) +} +function Evidence-Paths([string]$Directory,[bool]$Mutation){ + $leaves=@('witnesses.trx','run.log','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll') + if($Mutation){$leaves+='mutated-cpu.cs'} + return @($leaves|ForEach-Object{[IO.Path]::GetRelativePath($repo,(Join-Path $Directory $_))}) +} +function Check-Execution([string]$Directory,[string[]]$Failed,[string]$Reason=''){ + [xml]$trx=Get-Content (Join-Path $Directory 'witnesses.trx') -Raw + $c=$trx.TestRun.ResultSummary.Counters;$results=@($trx.TestRun.Results.UnitTestResult) + $actualFailed=@($results|Where-Object outcome -CEQ Failed|ForEach-Object testName|Sort-Object) + if($c.total -ne 6 -or $c.executed -ne 6 -or $c.failed -ne $Failed.Count -or $c.passed -ne (6-$Failed.Count) -or + $results.Count -ne 6 -or @($results|Where-Object{$_.outcome -cnotin @('Passed','Failed')}).Count -ne 0 -or + @(Compare-Object ($results.testName|Sort-Object) ($names|Sort-Object)).Count -ne 0 -or + $actualFailed.Count -ne $Failed.Count -or + ($Failed.Count -gt 0 -and @(Compare-Object $actualFailed @($Failed|Sort-Object)).Count -ne 0)){throw 'Incomplete writeback mutation execution'} + foreach($result in @($results|Where-Object outcome -CEQ Failed)){ + if($result.Output.ErrorInfo.Message -notmatch [regex]::Escape($Reason)){throw 'Writeback mutation failed for another reason'} + } +} +Push-Location $repo +try { + $inputs=@(@(& git ls-files Copper68k)+@('Copper68k.Tests/Copper68k.Tests.csproj',$programPath, + 'Copper68k.Tests/Synthetic/SyntheticM68040OperandWriteFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticMachine.cs', + 'Copper68k.Tests/Synthetic/ArchitecturalExpectation.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs', + 'Copper68k.Tests/Synthetic/SyntheticMoveTests.cs','scripts/test-copper68k-040-operand-write-mutations.ps1')|Sort-Object -Unique) + $normal=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll'|Where-Object{Test-Path -LiteralPath (Join-Path $repo $_)}) + $filter='FullyQualifiedName~SyntheticM68040OperandWriteFaultTests.MovePostincrement|FullyQualifiedName~SyntheticM68040OperandWriteFaultTests.PredecrementFault|FullyQualifiedName~SyntheticM68040OperandWriteFaultTests.FullIndexedFault|FullyQualifiedName~SyntheticM68040OperandWriteFaultTests.TraceFrameWriteFault' + if(-not $ValidateReportsOnly){ + if(Test-Path -LiteralPath $output){throw 'Use a fresh writeback mutation directory'} + New-Item -ItemType Directory -Path $output|Out-Null + $original=@($inputs|ForEach-Object{Identity $_});$originalNormal=@($normal|ForEach-Object{Identity $_}) + $bytes=[IO.File]::ReadAllBytes($source);$text=[IO.File]::ReadAllText($source) + $sourceHash=(Get-FileHash -LiteralPath $source).Hash;$expectedHash=$sourceHash + $baseline=Join-Path $output 'baseline';New-Item -ItemType Directory -Path $baseline|Out-Null + & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path (Join-Path $baseline 'build') --filter $filter --logger 'trx;LogFileName=witnesses.trx' --results-directory $baseline *> (Join-Path $baseline 'run.log') + if($LASTEXITCODE -ne 0){throw 'Writeback mutation baseline failed'} + Check-Execution $baseline @() + $rows=@() + try { + foreach($mutation in $mutations){ + $directory=Join-Path $output $mutation.name;New-Item -ItemType Directory -Path $directory|Out-Null + $mutated=Mutated-Source $text $mutation + [IO.File]::WriteAllText((Join-Path $directory 'mutated-cpu.cs'),$mutated) + if((Get-FileHash -LiteralPath $source).Hash -cne $sourceHash){throw 'CPU source changed before mutation'} + [IO.File]::WriteAllText($source,$mutated);$expectedHash=(Get-FileHash -LiteralPath $source).Hash + & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path (Join-Path $directory 'build') --filter $filter --logger 'trx;LogFileName=witnesses.trx' --results-directory $directory *> (Join-Path $directory 'run.log') + $code=$LASTEXITCODE + Restore-Source $bytes $expectedHash $sourceHash + if($code -ne 1){throw "Writeback mutation did not fail: $($mutation.name)"} + Check-Execution $directory @($mutation.failed) $mutation.reason + $rows+=@{name=$mutation.name;exit=$code;evidence=@((Evidence-Paths $directory $true)|ForEach-Object{Identity $_})} + } + } finally {Restore-Source $bytes $expectedHash $sourceHash} + foreach($entry in @($original)+@($originalNormal)){if((Identity $entry.path).sha256 -cne $entry.sha256){throw 'Writeback mutations changed original source/binaries'}} + @{schema=1;profile='m68040-operand-write-defect-proofs';sourceRevision=(& git rev-parse HEAD);sourceRestored=$true; + inputs=$original;normalBinaries=$originalNormal;baseline=@((Evidence-Paths $baseline $false)|ForEach-Object{Identity $_});mutations=$rows}| + ConvertTo-Json -Depth 10|Set-Content (Join-Path $output 'identity.json') + } + $id=Get-Content (Join-Path $output 'identity.json') -Raw|ConvertFrom-Json + if($id.schema -ne 1 -or $id.profile -cne 'm68040-operand-write-defect-proofs' -or $id.sourceRestored -ne $true -or $id.sourceRevision -cnotmatch '^[a-f0-9]{40}$'){throw 'Incomplete writeback mutation profile'} + Validate-Selection @($id.inputs) $inputs;Validate-Selection @($id.normalBinaries) $normal + $baseline=Join-Path $output 'baseline';Validate-Selection @($id.baseline) (Evidence-Paths $baseline $false);Check-Execution $baseline @() + if($id.mutations.Count -ne 7 -or @($id.mutations.name|Select-Object -Unique).Count -ne 7){throw 'Incomplete/duplicate writeback mutations'} + $text=[IO.File]::ReadAllText($source) + foreach($mutation in $mutations){ + $row=@($id.mutations|Where-Object name -CEQ $mutation.name) + if($row.Count -ne 1 -or $row[0].exit -ne 1){throw 'Missing executed writeback mutation'} + $directory=Join-Path $output $mutation.name;Validate-Selection @($row[0].evidence) (Evidence-Paths $directory $true) + if([IO.File]::ReadAllText((Join-Path $directory 'mutated-cpu.cs')) -cne (Mutated-Source $text $mutation)){throw 'Wrong writeback mutation source scope'} + Check-Execution $directory @($mutation.failed) $mutation.reason + } + @{schema=1;mutationProofs=7;sourceRestored=$true;productionCpuMutation=$true;roadmapComplete=$false; + scope='CPU defects: original split-operand width, predecrement completion, bus lanes, latched data, CT, N and exception boundary; no regression retired'}| + ConvertTo-Json -Depth 6|Set-Content (Join-Path $output 'summary.json') + Write-Host 'Six bounded witnesses detect all seven targeted CPU defects; original CPU source and normal binaries preserved.' +} finally {Pop-Location} +$global:LASTEXITCODE=0 diff --git a/scripts/test-copper68k-040-operand-writes.ps1 b/scripts/test-copper68k-040-operand-writes.ps1 new file mode 100644 index 00000000..8be276cc --- /dev/null +++ b/scripts/test-copper68k-040-operand-writes.ps1 @@ -0,0 +1,79 @@ +[CmdletBinding()] +param([switch]$ValidateReportsOnly,[string]$OutputDirectory='artifacts/040-operand-writes') +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +. (Join-Path $PSScriptRoot 'copper68k-040-operand-write-keys.ps1') +$groups=@(foreach($matrix in @('opcodes','boundaries','indexed')){foreach($route in @('scalar','batch')){"move-write-fault-$matrix-$route"}}) +function Identity([string]$Path){ + $file=Join-Path $repo $Path + if(-not(Test-Path -LiteralPath $file -PathType Leaf)){throw "Missing operand-write input: $Path"} + return @{path=$Path;sha256=(Get-FileHash -LiteralPath $file).Hash} +} +function Validate-Selection($Actual,[string[]]$Required){ + $paths=@($Actual.path) + if($paths.Count -ne $Required.Count -or @($paths|Select-Object -Unique).Count -ne $paths.Count -or + @(Compare-Object ($paths|Sort-Object) ($Required|Sort-Object)).Count){throw 'Incomplete/duplicate operand-write identity selection'} + foreach($entry in $Actual){if($entry.sha256 -cnotmatch '^[A-F0-9]{64}$' -or (Identity $entry.path).sha256 -cne $entry.sha256){throw "Changed operand-write input: $($entry.path)"}} +} +$saved=[Environment]::GetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR') +Push-Location $repo +try{ + $inputs=@(@(& git ls-files Copper68k)+@('Copper68k.Tests/Copper68k.Tests.csproj', + 'Copper68k.Tests/Synthetic/SyntheticM68040OperandWriteFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticM68040WritebackProgram.cs', + 'Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs','Copper68k.Tests/Synthetic/SyntheticMachine.cs', + 'Copper68k.Tests/Synthetic/ArchitecturalExpectation.cs','Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs', + 'Copper68k.Tests/Synthetic/SyntheticMoveTests.cs','Copper68k.Tests/Synthetic/MoveSpecification.cs', + 'Copper68k.Tests/Synthetic/AddressingFixtures.cs','scripts/copper68k-040-operand-write-keys.ps1', + 'scripts/test-copper68k-040-operand-writes.ps1')|Sort-Object -Unique) + $binaries=@('build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'| + ForEach-Object{[IO.Path]::GetRelativePath($repo,(Join-Path $output $_))}) + $evidence=@(@($groups|ForEach-Object{"68040-$_.json"})+@('operand-writes.trx','run.log')| + ForEach-Object{[IO.Path]::GetRelativePath($repo,(Join-Path $output $_))}) + if(-not $ValidateReportsOnly){ + if(Test-Path -LiteralPath $output){throw 'Use a fresh operand-write output directory'} + New-Item -ItemType Directory -Path $output|Out-Null + $original=@($inputs|ForEach-Object{Identity $_}) + [Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$output) + & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path (Join-Path $output 'build') --filter 'FullyQualifiedName~SyntheticM68040OperandWriteFaultTests' --logger 'trx;LogFileName=operand-writes.trx' --results-directory $output *> (Join-Path $output 'run.log') + if($LASTEXITCODE -ne 0){throw "Operand-write execution failed: $output/run.log"} + foreach($entry in $original){if((Identity $entry.path).sha256 -cne $entry.sha256){throw 'Operand-write source changed during execution'}} + @{schema=1;profile='m68040-actual-move-write-faults';sourceRevision=(& git rev-parse HEAD);inputs=$original; + binaries=@($binaries|ForEach-Object{Identity $_});evidence=@($evidence|ForEach-Object{Identity $_})}| + ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'identity.json') + } + $id=Get-Content (Join-Path $output 'identity.json') -Raw|ConvertFrom-Json + if($id.schema -ne 1 -or $id.profile -cne 'm68040-actual-move-write-faults' -or $id.sourceRevision -cnotmatch '^[a-f0-9]{40}$'){throw 'Incomplete operand-write identity profile'} + Validate-Selection @($id.inputs) $inputs;Validate-Selection @($id.binaries) $binaries;Validate-Selection @($id.evidence) $evidence + $prefix='Copper68k.Tests.Synthetic.SyntheticM68040OperandWriteFaultTests.' + $names=@(foreach($method in @('EveryMemoryDestinationMoveOpcodeFaultsAndReturns','EveryLaneSizeValueCcrAndStackBank','FullIndexedAliasesAndTraceContinuation')){ + foreach($batch in @('False','True')){"$prefix${method}(batch: $batch)"} + })+@(($prefix+'PredecrementFaultCommitsAliasedBaseOnce'),($prefix+'FullIndexedFaultKeepsLatchedOperandAndPendingTrace'), + ($prefix+'TraceFrameWriteFaultCannotChangeCompletedMoveDestination'), + ($prefix+'MovePostincrementWriteFaultCreatesPendingWriteback(width: 1, opcode: 4288)'), + ($prefix+'MovePostincrementWriteFaultCreatesPendingWriteback(width: 2, opcode: 12480)'), + ($prefix+'MovePostincrementWriteFaultCreatesPendingWriteback(width: 4, opcode: 8384)')) + [xml]$trx=Get-Content (Join-Path $output 'operand-writes.trx') -Raw + $c=$trx.TestRun.ResultSummary.Counters;$results=@($trx.TestRun.Results.UnitTestResult) + if($c.total -ne 12 -or $c.executed -ne 12 -or $c.passed -ne 12 -or $c.failed -ne 0 -or + $results.Count -ne 12 -or @($results|Where-Object outcome -CNE Passed).Count -ne 0 -or + @(Compare-Object ($results.testName|Sort-Object) ($names|Sort-Object)).Count){throw 'Incomplete operand-write execution selection'} + $coverage=@() + foreach($group in $groups){ + $matrix=$group -replace '^move-write-fault-','' -replace '-(scalar|batch)$','' + $keys=Get-Copper68k040OperandWriteKeys $matrix + $cases=if($matrix -ceq 'opcodes'){7350}elseif($matrix -ceq 'boundaries'){9216}else{14256} + $r=Get-Content (Join-Path $output "68040-$group.json") -Raw|ConvertFrom-Json -AsHashtable + if($r.schema -ne 1 -or $r.model -cne '68040' -or $r.group -cne $group -or $r.xunitBatches -ne 1 -or + $r.logicalCases -ne $cases -or $r.counts.passing -ne $cases -or $r.counts.mismatching -ne 0 -or + $r.counts.unsupported -ne 0 -or $r.counts.untested -ne 0 -or $r.failures.Count -ne 0 -or $r.combinations.Count -ne $keys.Count){throw "Incomplete operand-write coverage: $group"} + foreach($key in $keys.Keys){if(-not $r.combinations.Contains($key) -or $r.combinations[$key].Count -ne 1 -or $r.combinations[$key].passing -ne $keys[$key]){throw "Missing/misweighted operand-write combination: $key"}} + $coverage+=@{group=$group;scenarios=$cases;combinations=$keys.Count} + } + @{schema=1;logicalCases=61644;xunitBatches=6;boundedWitnesses=6;coverage=$coverage;roadmapComplete=$false; + reference='MC68040UM 8.4.6, table 8-5, 8.4.6.7';scope='Actual rejected MOVE destination writes, format-7 WB1, handler, RTE and following instruction; scalar/batch public accurate CPU'; + remaining='Other integer/read/MOVEM/MOVE16 faults, compiled/cache/MMU fault pipelines, nested WB2/3, physical function-code spaces and hardware timing'}| + ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'summary.json') + Write-Host "040 actual MOVE writes: 61644 passing scenarios / 43788 combinations; $output" +}finally{[Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$saved);Pop-Location} +$global:LASTEXITCODE=0 diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 56843a48..57d50ad6 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -162,6 +162,11 @@ try { $expected['system-move-long-restart-invalid'] = 640 } if ($model -eq '68040') { + foreach ($route in @('scalar','batch')) { + $expected["move-write-fault-opcodes-$route"] = 7350 + $expected["move-write-fault-boundaries-$route"] = 9216 + $expected["move-write-fault-indexed-$route"] = 14256 + } foreach ($matrix in @('canonical','structure')) { foreach ($route in @('scalar','batch')) { $expected["rte-writeback-$matrix-$route"] = 36864 } } @@ -193,7 +198,7 @@ try { $expected['access-fault-batch-dispatch'] = 10368 $expected['rte-access-entry-double-fault'] = 98304 $expected['rte-access-handler-refault'] = 3840 - $expected['access-double-fault-dispatch-accurate'] = 1088 + $expected['access-double-fault-dispatch-accurate'] = 2336 $expected['access-double-fault-dispatch-v1'] = 1088 $expected['access-double-fault-dispatch-v2'] = 1088 $expected['rte-validation-physical-direct'] = 162816 From 0bea1be1456d0793b635456136ef1e73bf99276c Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 22:13:44 +0300 Subject: [PATCH 073/211] Fix and qualify 68030 PMMU privilege priority --- Copper68k.Tests/Synthetic/README.md | 25 +++ .../Synthetic/SyntheticLineFEncodingTests.cs | 6 +- .../SyntheticM68030PmmuPrivilegeTests.cs | 179 ++++++++++++++++++ .../Synthetic/SyntheticSystemTests.cs | 10 +- Copper68k/M68kAdvancedTimingInterpreter.cs | 10 + docs/COPPER68K_REFERENCE_QUALIFICATION.md | 114 +++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 40 ++++ ...copper68k-030-pmmu-privilege-mutations.ps1 | 115 +++++++++++ scripts/test-copper68k-030-pmmu-privilege.ps1 | 81 ++++++++ scripts/test-copper68k-synthetic.ps1 | 6 + 10 files changed, 582 insertions(+), 4 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68030PmmuPrivilegeTests.cs create mode 100644 scripts/test-copper68k-030-pmmu-privilege-mutations.ps1 create mode 100644 scripts/test-copper68k-030-pmmu-privilege.ps1 diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 71851ee6..e993b6f1 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1142,3 +1142,28 @@ the faulted instruction is never retried. This qualifies normal MOVE writes, not other integer/read/MOVEM/MOVE16 faults, nested writeback recovery, enabled MMU or physical timing. The complete-040 gate retains its 480 untested scenarios; milestone 6 remains incomplete. See `docs/COPPER68K_REFERENCE_QUALIFICATION.md`. + +## 030 integrated-MMU user privilege + +The self-contained scalar/batch suite checks privilege priority for all CpID-0 +primary words and separate defined user SR / secondary-word boundaries. It +verifies complete exception frames, state/stack preservation, no operand reads, +a software handler/RTE and the following sentinel/trace. Legal supervisor PMMU +semantics and enabled translation are separate requirements. + +```powershell +./scripts/test-copper68k-030-pmmu-privilege.ps1 -OutputDirectory artifacts/030-pmmu-privilege +./scripts/test-copper68k-030-pmmu-privilege.ps1 -ValidateReportsOnly -OutputDirectory artifacts/030-pmmu-privilege +./scripts/test-copper68k-030-pmmu-privilege-mutations.ps1 -OutputDirectory artifacts/030-pmmu-privilege-mutations +./scripts/test-copper68k-030-pmmu-privilege-mutations.ps1 -ValidateReportsOnly -OutputDirectory artifacts/030-pmmu-privilege-mutations +``` + +The focused gate requires 263,168 scenarios, four reports and 47 fixed controls, +with independently enumerated keys/weights and exact source/binary/evidence +identities. Four production mutations run 47 controls each after a baseline, +with precise failed methods/reasons and guarded byte restoration. The general +system and retained Line-F expectations include documented 030 CpID-0 priority. +Ordinary requirements are 86,097,474 scenarios / 707 reports. Missing external +fixtures fail requested audits and remain unavailable coverage. Milestone 6 is +incomplete; see the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#68030-integrated-mmu-user-privilege-qualification--2026-10-06), +including the concurrent-cleanup limitation on the latest full execution. diff --git a/Copper68k.Tests/Synthetic/SyntheticLineFEncodingTests.cs b/Copper68k.Tests/Synthetic/SyntheticLineFEncodingTests.cs index fa2c01f9..10179d3c 100644 --- a/Copper68k.Tests/Synthetic/SyntheticLineFEncodingTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticLineFEncodingTests.cs @@ -14,7 +14,9 @@ public void SaveRestoreFirstWordsSelectLineFOrPrivilegeBeforeOperandEffects(stri // MC68020UM 7.2.3.3/4, 7.5.2.2/3; MC68030UM 10.2.3.3/4, // 10.5.2.2/3 (CpID 0 belongs to the internal MMU, not external CIRs). // M68000PM 6-13/16; MC68060UM D-15/18 and 8.3: - // invalid first words are line-F, not privileged state transfers. + // Invalid external state-transfer words are line-F. MC68030UM + // 8.1.5/6 gives user-mode CpID 0 privilege priority even for undefined + // integrated-MMU patterns, independently of the external EA rules. var opcodes = (from cpId in Enumerable.Range(0, 8) from restore in new[] { false, true } from ea in Enumerable.Range(0, 64) @@ -43,7 +45,7 @@ from ea in Enumerable.Range(0, 64) // supervisor coprocessor/FPU state execution is a separate protocol, // not counted as passing exception coverage or silently simulated. if (supervisor && recognized && legalEa) continue; - var vector = recognized && legalEa ? 8 : 11; + var vector = modelId == "68030" && item.CpId == 0 && !supervisor || recognized && legalEa ? 8 : 11; var form = item.Mode == 7 && item.Register > 4 ? $"unassigned-EA(7,{item.Register})" : $"EA({item.Mode},{item.Register})"; for (var ccr = 0; ccr < 32; ccr++) InvalidOperandScenario.Run(machine, report, item.Opcode, diff --git a/Copper68k.Tests/Synthetic/SyntheticM68030PmmuPrivilegeTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68030PmmuPrivilegeTests.cs new file mode 100644 index 00000000..574640d6 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68030PmmuPrivilegeTests.cs @@ -0,0 +1,179 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68030UM 8.1.5/6 and 9.8: CpID-0 F-line words are privileged even +// when their subsequent encoding is undefined. This is not MMU translation +// or successful supervisor PMMU execution qualification. +public sealed class SyntheticM68030PmmuPrivilegeTests(ITestOutputHelper output) +{ + private static readonly ushort[] Payloads = [0, 1, 0x00be, 0x2000, 0x4000, 0x8000, 0xa000, 0xffff]; + + public static IEnumerable ModelControls() => + from model in ModelSpec.All from supervisor in new[] { false, true } from batch in new[] { false, true } + select new object[] { model.Id, supervisor, batch }; + + public static IEnumerable ExternalControls() => + from id in Enumerable.Range(1, 7) from batch in new[] { false, true } + select new object[] { id, batch }; + + [Theory, MemberData(nameof(ModelControls)), Trait("Suite", "Synthetic")] + public void RuleIsSpecificToUserModeOn68030(string modelId, bool supervisor, bool batch) + => Control(new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)), 0xf000, + supervisor, batch, modelId == "68030" && !supervisor ? 8 : 11); + + [Theory, MemberData(nameof(ExternalControls)), Trait("Suite", "Synthetic")] + public void NonzeroCpIdsKeepTheAbsentCoprocessorLineFRoute(int id, bool batch) + => Control(new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68030")), + (ushort)(0xf000 | id << 9), false, batch, 11); + + private static void Control(SyntheticMachine m, ushort opcode, bool supervisor, bool batch, int vector) + { + m.Reset(31, supervisor); + var e = SyntheticExecution.Prepare(m, [opcode, 0x00be, 0x4e71]); + SyntheticExecution.ExpectException(m, e, vector); + var sequence = m.Core.State.ExceptionSequence; + e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence + 1); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, SyntheticMachine.Code); + Assert.Null(Step(m, e, batch)); + } + + [Fact, Trait("Suite", "Synthetic")] + public void UndefinedPmmuWordInUserModeRaisesPrivilegeBeforeLineF() + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68030")); + var result = Case(m, false, 0xf000, 0x00be, 0); + Assert.True(result is null, result); + } + + [Theory, Trait("Suite", "Synthetic")] + [InlineData(false)] + [InlineData(true)] + public void EveryCpIdZeroPrimaryWordInUserMode(bool batch) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68030")); + var report = new CoverageBatch("68030", "system-pmmu-privilege-opcodes-" + (batch ? "batch" : "scalar")); + for (var opcode = 0xf000; opcode <= 0xf1ff; opcode++) + Record(report, m, batch, (ushort)opcode, 0x00be, 0x071f); + report.Complete(output); + } + + [Theory, Trait("Suite", "Synthetic")] + [InlineData(false)] + [InlineData(true)] + public void EveryDefinedUserStatusAndSecondaryWordBoundaries(bool batch) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68030")); + var report = new CoverageBatch("68030", "system-pmmu-privilege-status-" + (batch ? "batch" : "scalar")); + for (var form = 0; form < 8; form++) + foreach (var payload in Payloads) + for (var trace = 0; trace < 4; trace++) + for (var master = 0; master < 2; master++) + for (var ipl = 0; ipl < 8; ipl++) + for (var ccr = 0; ccr < 32; ccr++) + Record(report, m, batch, (ushort)(0xf000 | form << 6), payload, + (ushort)(trace << 14 | master << 12 | ipl << 8 | ccr)); + report.Complete(output); + } + + private static void Record(CoverageBatch report, SyntheticMachine m, bool batch, ushort opcode, ushort payload, ushort sr) + { + var id = $"68030/PMMU/privilege/primary={opcode:X4}/extension={payload:X4}/status={sr & 0xffe0:X4}/op={opcode:X4}/ccr={sr & 31:X2}"; + try + { + var mismatch = Case(m, batch, opcode, payload, sr); + report.Record(id, mismatch is null ? "passing" : "mismatching", mismatch); + } + catch (Exception ex) + { + report.Record(id, ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException or UnsupportedM68040InstructionException + ? "unsupported" : "mismatching", ex.ToString()); + } + } + + private static string? Case(SyntheticMachine m, bool batch, ushort opcode, ushort payload, ushort sr) + { + const uint handler = 0x9080, traceHandler = 0x9090; + m.Reset(sr & 31, false); + m.Core.State.StatusRegister = sr; + m.InitializePhysical(0x420, handler, 4); // VBR + privilege vector. + m.InitializePhysical(0x42c, 0x90b0, 4); // Distinct Line-F route for failure diagnostics. + m.InitializePhysical(0x90b0, 0x4e71, 2); + m.InitializePhysical(SyntheticMachine.Code, opcode, 2); + m.InitializePhysical(SyntheticMachine.Code + 2, payload, 2); + m.InitializePhysical(SyntheticMachine.Code + 4, 0x7e55, 2); // MOVEQ #$55,D7 sentinel. + m.InitializePhysical(SyntheticMachine.Code + 6, 0x4e71, 2); + // Software deliberately skips both fixture words; this is not a PMMU + // instruction-length expectation or retry of the privileged operation. + m.InitializePhysical(handler, 0x58af, 2); // ADDQ.L #4,2(A7) + m.InitializePhysical(handler + 2, 2, 2); + m.InitializePhysical(handler + 4, 0x4e73, 2); + m.InitializePhysical(handler + 6, 0x4e71, 2); + m.InitializePhysical(0x424, traceHandler, 4); + m.InitializePhysical(traceHandler, 0x4e73, 2); + m.Start(); + m.Core.State.VectorBaseRegister = 0x400; + m.Core.State.SetUserStackPointer(0x7800); + m.Core.State.SetInterruptStackPointer(0x4700); + m.Core.State.SetMasterStackPointer(0x7400); + m.Core.State.SourceFunctionCode = 1; + m.Core.State.DestinationFunctionCode = 5; + var sequence = m.Core.State.ExceptionSequence; + var e = ArchitecturalExpectation.Capture(m); + var master = (sr & 0x1000) != 0; + var top = master ? 0x7400u : 0x4700u; + var frame = top - 8; + e.Pc = handler; e.Sr = (ushort)((sr | 0x2000) & 0x3fff); e.ExceptionVector = 8; + e.A[7] = frame; e.InactiveStackPointer = 0x7800; + e.MasterStackPointer = master ? frame : 0x7400; + e.ControlChecks["ISP"] = (s => s.InterruptStackPointer, master ? 0x4700u : frame); + e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence + 1); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, SyntheticMachine.Code); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, sr); + e.ControlChecks["VBR"] = (s => s.VectorBaseRegister, 0x400); + e.ControlChecks["SFC"] = (s => s.SourceFunctionCode, 1); + e.ControlChecks["DFC"] = (s => s.DestinationFunctionCode, 5); + e.Write(frame, sr, 2, m.Model); e.Write(frame + 2, SyntheticMachine.Code, 4, m.Model); + e.Write(frame + 6, 0x20, 2, m.Model); + var mismatch = Step(m, e, batch); + if (m.Core.State.LastExceptionVector != 8) + return $"entry: expected privilege vector 8, actual {m.Core.State.LastExceptionVector}"; + if (m.Bus.Accesses.Any(a => a.Kind == M68kBusAccessKind.CpuDataRead && + Enumerable.Range(0, a.Width).Any(n => a.Address + (uint)n < 0x420 || a.Address + (uint)n >= 0x424))) + return "entry: privileged operation read operand memory before trapping"; + if (mismatch != null) return "entry: " + mismatch; + e.Write(frame + 2, SyntheticMachine.Code + 4, 4, m.Model); + e.Pc = handler + 4; e.Sr &= 0xffe0; // Positive nonzero ADDQ result, no carry/overflow. + mismatch = Step(m, e, batch); if (mismatch != null) return "software skip: " + mismatch; + e.Pc = SyntheticMachine.Code + 4; e.Sr = sr; e.A[7] = 0x7800; + e.InactiveStackPointer = 0x4700; e.MasterStackPointer = 0x7400; + e.ControlChecks["ISP"] = (s => s.InterruptStackPointer, 0x4700); + mismatch = Step(m, e, batch); if (mismatch != null) return "RTE: " + mismatch; + e.D[7] = 0x55; e.Sr = (ushort)(sr & 0xfff0); e.Pc = SyntheticMachine.Code + 6; + if ((sr & 0x8000) != 0) + { + var tracedSr = e.Sr; var traceFrame = top - 12; + e.A[7] = traceFrame; e.InactiveStackPointer = 0x7800; + e.MasterStackPointer = master ? traceFrame : 0x7400; + e.ControlChecks["ISP"] = (s => s.InterruptStackPointer, master ? 0x4700u : traceFrame); + e.Sr = (ushort)((tracedSr | 0x2000) & 0x3fff); e.Pc = traceHandler; e.ExceptionVector = 9; + e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence + 2); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, SyntheticMachine.Code + 6); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, tracedSr); + e.Write(traceFrame, tracedSr, 2, m.Model); e.Write(traceFrame + 2, SyntheticMachine.Code + 6, 4, m.Model); + e.Write(traceFrame + 6, 0x2024, 2, m.Model); e.Write(traceFrame + 8, SyntheticMachine.Code + 4, 4, m.Model); + } + return Step(m, e, batch); + } + + private static string? Step(SyntheticMachine m, ArchitecturalExpectation e, bool batch) + { + if (!batch) m.Core.ExecuteInstruction(); + else if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, m.Core.State.Cycles + 1000, new Boundary()) != 1) + return "Expected exactly one batch instruction"; + return e.Verify(m); + } + private sealed class Boundary : IM68kInstructionBoundary + { public bool BeforeInstruction() => true; public void AfterInstruction(long previousCycle, long currentCycle) { } } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticSystemTests.cs b/Copper68k.Tests/Synthetic/SyntheticSystemTests.cs index a5ee4466..c15122c4 100644 --- a/Copper68k.Tests/Synthetic/SyntheticSystemTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticSystemTests.cs @@ -15,7 +15,8 @@ public void TrapsPrivilegeStopsAndResetPreserveArchitecturalState(string modelId foreach (var supervisor in new[] { false, true }) { for (var trap = 0; trap < 16; trap++) Basic(machine, report, "TRAP", (ushort)(0x4e40 | trap), ccr, supervisor); - // Type 110 is an unassigned F-line word on every selected model. + // Type 110 is unassigned, but MC68030UM 8.1.5/6 still gives + // user-mode integrated-MMU CpID 0 privilege exception priority. // F123 is instead a legal privileged cpSAVE on EC020/020. foreach (var (family, opcode) in new[] { ("NOP", 0x4e71), ("RESET", 0x4e70), ("ILLEGAL", 0x4afc), ("LineA", 0xa123), ("LineF", 0xf1c0), ("TRAPV", 0x4e76) }) Basic(machine, report, family, (ushort)opcode, ccr, supervisor); @@ -137,7 +138,12 @@ internal static void Basic(SyntheticMachine machine, CoverageBatch report, strin else if (family == "RESET") expected.ExpectedDeviceResets = 1; else if (family == "TRAP") SyntheticExecution.ExpectException(machine, expected, 32 + (opcode & 15), expected.Pc); else if (family == "TRAPV" && (ccr & 2) != 0) SyntheticExecution.ExpectException(machine, expected, 7, expected.Pc); - else if (family is "ILLEGAL" or "LineA" or "LineF") SyntheticExecution.ExpectException(machine, expected, family == "ILLEGAL" ? 4 : family == "LineA" ? 10 : 11); + else if (family is "ILLEGAL" or "LineA" or "LineF") + { + var vector = family == "ILLEGAL" ? 4 : family == "LineA" ? 10 + : machine.Model.Id == "68030" && !supervisor && (opcode & 0x0e00) == 0 ? 8 : 11; + SyntheticExecution.ExpectException(machine, expected, vector); + } SyntheticExecution.Run(machine, expected, report, $"{machine.Model.Id}/{family}/none/super={supervisor}/op={opcode:X4}/imm={immediate:X4}/ccr={ccr:X2}", !expected.Stopped); } diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index 5ecc59ba..2c2b20db 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -5762,6 +5762,16 @@ private bool ExecuteDispatchedInstruction(ushort opcode, M68020OpcodeKind kind) case M68020OpcodeKind.LineFException: BeginInstruction(opcode); _ = FetchWord(); + // MC68030UM 8.1.5/6 and 9.8: the integrated MMU's CpID 0 + // is privileged, including undefined subsequent encodings. + // Check before extension/EA handling or Line-F delivery. + if (_profile.Model == M68kAcceleratorModel.M68030 && + (opcode & 0x0E00) == 0 && + (State.StatusRegister & M68kCpuState.Supervisor) == 0) + { + RaiseFormat0Exception(8, State.LastInstructionProgramCounter, M68kInstructionTimingKey.PrivilegeViolation); + return true; + } // MC68020UM 7.5.2.2/3; MC68030UM 10.1, 10.5.2.2/3: // legal cpSAVE/cpRESTORE checks privilege before accessing // an absent coprocessor. 030 CpID 0 is the internal MMU. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 9d1f51bb..461c0beb 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -6780,3 +6780,117 @@ and two stopped full attempts remain separate historical evidence under MOVE-frame expectation; the next was superseded by the trace-entry boundary fix. Neither is a complete-suite success. Final manifests are not applied retroactively to them. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## 68030 integrated-MMU user privilege qualification — 2026-10-06 + +The unchanged pinned WinUAE Basic corpus exposed a real 68030 exception-priority +defect: user-mode CpID-0 words raised Line-F instead of privilege violation. +[MC68030UM part 2](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P2.pdf), +8.1.5/6 and 9.8, requires privilege violation in user mode even for undefined +integrated-MMU patterns. The CPU now checks this before extension/operand handling +and Line-F delivery. Other models, supervisor-mode invalid words and external +coprocessor handling retain their existing behavior and timing policy. + +`SyntheticM68030PmmuPrivilegeTests` passes **263,168 whole-program scenarios / +9,216 independently checked report keys** in four scalar/batch reports, with **47 fixed +controls**, all 51 exact executions passing without skips. The opcode group covers +every first word F000–F1FF. Separate status groups combine eight primary formats, +eight secondary-word boundaries, every defined user SR image (trace, M, IPL and +CCR) and both execution routes. The fixture checks complete format-0 frames, +vector provenance, registers, SFC/DFC, stack banks and surrounding memory. Entry +must not read operand data. A literal integer handler edits the stacked PC to +skip both fixture words, then RTE restores state and a MOVEQ sentinel verifies +continuation and applicable trace delivery. This software skip does not define +PMMU instruction length or retry a partially executed instruction. + +The retained Line-F matrix had 4,096 obsolete 030 expectations; its expectation +now includes the documented integrated-MMU priority. All 25 retained tests pass. The general system matrix also corrects 32 user-mode F1C0 expectations; its eight-profile selection passes. +No old regression is retired. Four maintained production mutations prove that +the controls detect missing priority and incorrect CpID/model/mode boundaries; +baseline plus mutations execute **235 tests** with exact failing names/reasons. +Original CPU bytes and protected normal assemblies are preserved. Eight report +integrity controls reject profile/input/report/key/weight/status corruption. Seven mutation-evidence controls reject their intended errors; an isolated restoration guard preserves concurrent edits. + +```powershell +./scripts/test-copper68k-030-pmmu-privilege.ps1 -OutputDirectory artifacts/030-pmmu-privilege +./scripts/test-copper68k-030-pmmu-privilege.ps1 -ValidateReportsOnly -OutputDirectory artifacts/030-pmmu-privilege +./scripts/test-copper68k-030-pmmu-privilege-mutations.ps1 -OutputDirectory artifacts/030-pmmu-privilege-mutations +./scripts/test-copper68k-030-pmmu-privilege-mutations.ps1 -ValidateReportsOnly -OutputDirectory artifacts/030-pmmu-privilege-mutations +``` + +The commands require exact source/binary/evidence selections and independently +enumerated architectural keys and weights. Original focused evidence was written under +`artifacts/m6-pmmu-privilege-qualified-final`, +`artifacts/m6-pmmu-privilege-mutations-final-v2` and +`artifacts/m6-pmmu-privilege-controls` and `artifacts/m6-pmmu-mutation-controls-v3`. See the final validation note for the subsequent cleanup and regenerated records. Earlier incomplete matrix identifiers and +incorrect mutation expectations remain excluded historical evidence. + +The unchanged broad Basic audit improves from **1,326 passing / 47 mismatching / +8 unsupported directories** to **1,327 / 46 / 8**, with no untested directories; +the 030 MMUOP030 directory passes 4,178 callbacks. Both broad runs intentionally +fail their remaining selections. Total callbacks (11,474,194 before and +11,478,371 after) include partially executed failing directories and are not +all-passing architectural coverage. The before/after records are +`artifacts/m6-basic-current-audit` and `artifacts/m6-basic-pmmu-after-audit`. +Both use generator `025b999239800357e95065fe5b9a15ea5b300fa7`, runner +`7a83745d6c6159bc74ab0471578ffc8bc244e66e`, manifest SHA256 +`37cd8ddae8b61ac60e3a49ba835362fbf80d418f48c2539338e6541c9d85e31f` +and native library SHA256 +`75f0c11352d4a8c1e84f32a49a5347ae4cb5bd9128f1501a33d843e6f99cb057`. +No failing Basic family is silently excluded to make this audit pass. + +Ordinary requirements become **86,097,474 scenarios / 707 reports**. +Complete-040 requirements remain 67,856,332 scenarios / 92 reports plus 30 fixed +witnesses, with the required 480 untested cases / fifteen combinations retained. +No passing complete-040 audit is claimed. Legal supervisor PMMU semantics, +advanced exception restoration and remaining milestone-6 protocols stay open; +enabled MMU translation, FPU arithmetic and physical timing remain outside the +roadmap. Nonzero CpID controls here cover general coprocessor words, not every +external state-transfer privilege rule. Milestone 6 remains **in progress**, +`roadmapComplete=false`. + + +#### Final validation and concurrent artifact cleanup + +A concurrent cleanup removed ignored reports and pinned external inputs during +the full run. The retained TRX records **5,208 passing / nine failed / nineteen +optional skipped tests**, total 5,236. All nine failures are +`DirectoryNotFoundException` for deleted WinUAE preset manifests, not semantic +mismatches. This is **not a green full run** and those audits are unavailable +until their pinned inputs are restored. No missing audit is relabeled passing. + +The actual retained TRX contains all **707 deterministic batch summaries / +86,097,474 scenarios**. Independent verification matches every model/group, +expected case count, passing outcome and zero mismatch/unsupported/untested +counts against the maintained gate requirements. This recovers execution/count +evidence; it does not recreate deleted JSON key details or claim the original +JSON report gate completed. The preserved record is +`C:/Users/ilkle/AppData/Local/Temp/copper68k-030-pmmu-20261006/full-verification.json`, +with the original TRX and extracted summaries beside it. + +After cleanup stopped, both maintained PMMU commands were rerun under that +temporary root: `qualification` passes all 51 executions / 263,168 scenarios / +9,216 independently checked report keys; `mutations` proves all four defects in +235 executions, preserving original CPU bytes and normal assemblies. Their +complete fresh source/binary/evidence manifests supersede the deleted local +focused records. Earlier report/mutation integrity controls and the independent +Basic audit were observed before cleanup; their original local records are no +longer available and are not retrospectively recreated. + +Before cleanup, pinned SingleStepTests passed 312,500 cases / 125 files and +Musashi 536 combinations with 88 explicit exclusions against final full-build +CPU SHA256 `43fffbd564c89c1ec98d4d2e1e1793c47a2435a157a4a0809a9bd6a9f8369514` +and test SHA256 `2a9af80b3c770db7fb93d53d228dc4a66e55a0bd65dbccbab66f55f6a2ef8a92`. +These remain observed execution results; their deleted local manifests are not +claimed to be present. + +The immutable private `1.5.2-synthetic-dev.67` package remains present with SHA256 +`fed1a90ced4a7a11429bbfc1a0ddfe33fd38cf4aded33332c9d1dc31e759e6ea`. +Before cleanup, its CopperScreen Release build completed without warnings/errors; +host 149 passing / six optional skips, disk 74, separate engine diagnostics 1,080 +and all three native replays passed. Four restore assets and loaded DLLs matched +packed CPU SHA256 `ebc62e354bd95cb85ad7d8809775bd7db8dba66739b371b0eb6cb51e8d776006`. +Cleanup subsequently removed the consumer checkout and local replay inputs; +these results are not described as fresh post-cleanup replays. Source and the +published dependency pin remain untouched. No public package is published. +Milestone 6 stays **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 663afbff..0d74819c 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2533,3 +2533,43 @@ retired and no public package is published. See the [qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040-actual-move-write-fault-qualification--2026-10-06) for exact identities and reference limitations. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +### 68030 integrated-MMU user privilege qualification — 2026-10-06 + +The pinned unchanged WinUAE Basic audit exposed user-mode CpID-0 words raising +Line-F instead of privilege violation. The documented 030 priority now precedes +extension/operand handling, including undefined MMU patterns. Other models and +external coprocessor rules are preserved. A shared scalar/batch fixture checks +all primary words and separate status/secondary-word boundaries, full exception +frames, preserved registers/memory/stacks/SFC/DFC, software frame edit/RTE and +following sentinel/trace behavior without replaying the privileged instruction. + +The focused gate passes **263,168 programs / 9,216 report keys / four reports**, +with 47 fixed controls and all 51 exact executions passing without skips. Four +maintained production mutations detect missing priority and incorrect +CpID/model/mode boundaries in 235 executions. Eight report and seven mutation +integrity controls reject corruption; an isolated restoration guard preserves +concurrent edits. The retained Line-F matrix corrects 4,096 obsolete expectations +and passes all 25 tests; the general system matrix corrects 32 F1C0 expectations +and passes its eight-profile selection. No old regression is retired. + +Ordinary requirements become **86,097,474 scenarios / 707 reports**. Complete-040 +requirements and its required 480 untested cases / fifteen combinations remain. +The unchanged broad Basic audit still fails 46 mismatching and eight unsupported +directories; the 030 MMUOP030 directory now passes 4,178 callbacks. These findings +are recorded separately from passing scoped reference presets. Legal supervisor +PMMU semantics and other milestone-6 requirements remain open; enabled MMU +translation and physical timing remain outside this roadmap. See the +[qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#68030-integrated-mmu-user-privilege-qualification--2026-10-06). +Milestone 6 remains **in progress**, `roadmapComplete=false`. + +The final full execution records 5,208 passing tests, nineteen optional skips and +nine WinUAE failures for pinned input manifests removed by concurrent cleanup; +it is not a green full run. The preserved TRX independently verifies all 707 +ordinary batch/count summaries and 86,097,474 scenarios, while deleted JSON +report details remain unavailable. After cleanup stopped, the focused PMMU and +mutation commands pass again with complete fresh identities outside the cleaned +directory. The private `.67` consumer and external reference checks passed before +cleanup; their removed local records and replay inputs are not described as +present or rerun. See the qualification record for this distinction. No public +package release is performed. diff --git a/scripts/test-copper68k-030-pmmu-privilege-mutations.ps1 b/scripts/test-copper68k-030-pmmu-privilege-mutations.ps1 new file mode 100644 index 00000000..20b69350 --- /dev/null +++ b/scripts/test-copper68k-030-pmmu-privilege-mutations.ps1 @@ -0,0 +1,115 @@ +[CmdletBinding()] +param([switch]$ValidateReportsOnly,[string]$OutputDirectory='artifacts/030-pmmu-privilege-mutations') +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +$programPath='Copper68k/M68kAdvancedTimingInterpreter.cs';$source=Join-Path $repo $programPath +$prefix='Copper68k.Tests.Synthetic.SyntheticM68030PmmuPrivilegeTests.' +function Model-Name([string]$Model,[string]$Supervisor,[string]$Batch){"${prefix}RuleIsSpecificToUserModeOn68030(modelId: `"$Model`", supervisor: $Supervisor, batch: $Batch)"} +$names=@(($prefix+'UndefinedPmmuWordInUserModeRaisesPrivilegeBeforeLineF'))+ + @(foreach($model in @('68000','68010','68EC020','68020','68030','68040','68060','A1200')){foreach($super in @('False','True')){foreach($batch in @('False','True')){Model-Name $model $super $batch}}})+ + @(foreach($id in 1..7){foreach($batch in @('False','True')){"${prefix}NonzeroCpIdsKeepTheAbsentCoprocessorLineFRoute(id: $id, batch: $batch)"}}) +$modeAnchor="(opcode & 0x0E00) == 0 &&`n (State.StatusRegister & M68kCpuState.Supervisor) == 0)" +$mutations=@( + @{name='missing-priority';failed=@($names[0]) + @(foreach($batch in @('False','True')){Model-Name '68030' 'False' $batch});reason='expected privilege vector 8, actual 11'}, + @{name='wrong-cpid';anchor='(opcode & 0x0E00) == 0 &&';replacement='';failed=@($names|Where-Object{$_ -match 'NonzeroCpIds.*id: (1|2|5|6|7),'});reason='PC expected'}, + @{name='wrong-model';anchor='_profile.Model == M68kAcceleratorModel.M68030 &&';replacement='';failed=@(foreach($model in @('68EC020','68020','68040','68060','A1200')){foreach($batch in @('False','True')){Model-Name $model 'False' $batch}});reason='PC expected'}, + @{name='wrong-mode';anchor=$modeAnchor;replacement=$modeAnchor.Replace('== 0)','!= 0)');failed=@($names[0]) + @(foreach($super in @('False','True')){foreach($batch in @('False','True')){Model-Name '68030' $super $batch}});reason='PC expected'} +) +function Identity([string]$Path){ + $file=Join-Path $repo $Path + if(-not(Test-Path -LiteralPath $file -PathType Leaf)){throw "Missing PMMU mutation input: $Path"} + @{path=$Path;sha256=(Get-FileHash -LiteralPath $file).Hash} +} +function Validate-Selection($Actual,[string[]]$Required){ + $paths=@($Actual.path) + if($paths.Count -ne $Required.Count -or @($paths|Select-Object -Unique).Count -ne $paths.Count -or + ($paths.Count -gt 0 -and @(Compare-Object ($paths|Sort-Object) ($Required|Sort-Object)).Count)){throw 'Incomplete/duplicate PMMU mutation identity selection'} + foreach($entry in $Actual){if($entry.sha256 -cnotmatch '^[A-F0-9]{64}$' -or (Identity $entry.path).sha256 -cne $entry.sha256){throw "Changed PMMU mutation input: $($entry.path)"}} +} +function Mutated-Source([string]$Text,$Mutation){ + if($Mutation.name -ceq 'missing-priority'){ + $start=$Text.IndexOf(' // MC68030UM 8.1.5/6',[StringComparison]::Ordinal) + $end=$Text.IndexOf(' // MC68020UM 7.5.2.2/3',$start,[StringComparison]::Ordinal) + if($start -lt 0 -or $end -le $start){throw 'Missing PMMU priority region'} + return $Text.Remove($start,$end-$start) + } + if(($Text.Split($Mutation.anchor).Length-1) -ne 1){throw 'Nonunique PMMU mutation anchor'} + return $Text.Replace($Mutation.anchor,$Mutation.replacement) +} +function Restore-Source([byte[]]$Bytes,[string]$ExpectedHash,[string]$OriginalHash){ + $current=(Get-FileHash -LiteralPath $source).Hash + if($current -ceq $OriginalHash){return} + if($current -cne $ExpectedHash){throw 'CPU source changed concurrently; current file left intact'} + [IO.File]::WriteAllBytes($source,$Bytes) +} +function Evidence-Paths([string]$Directory,[bool]$Mutation){ + $leaves=@('controls.trx','run.log','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll') + if($Mutation){$leaves+='mutated-cpu.cs'} + @($leaves|ForEach-Object{[IO.Path]::GetRelativePath($repo,(Join-Path $Directory $_))}) +} +function Check-Execution([string]$Directory,[string[]]$Failed,[string]$Reason=''){ + [xml]$trx=Get-Content (Join-Path $Directory 'controls.trx') -Raw + $c=$trx.TestRun.ResultSummary.Counters;$results=@($trx.TestRun.Results.UnitTestResult) + if($names.Count -ne 47 -or $c.total -ne 47 -or $c.executed -ne 47 -or $c.failed -ne $Failed.Count -or $c.passed -ne 47-$Failed.Count -or + $results.Count -ne 47 -or @($results|Where-Object {$_.outcome -notin @('Passed','Failed')}).Count -ne 0 -or + @(Compare-Object ($results.testName|Sort-Object) ($names|Sort-Object)).Count){throw 'Incomplete PMMU mutation execution selection'} + $failedResults=@($results|Where-Object outcome -CEQ Failed) + if($Failed.Count -gt 0 -and @(Compare-Object ($failedResults.testName|Sort-Object) ($Failed|Sort-Object)).Count){throw 'Wrong PMMU mutation failed methods'} + if($Failed.Count -eq 0 -and $failedResults.Count -ne 0){throw 'Failed PMMU baseline'} + if($Reason -and -not @($failedResults|Where-Object {$_.Output.ErrorInfo.Message.Contains($Reason,[StringComparison]::Ordinal)}).Count){throw 'Wrong PMMU mutation failure reason'} +} +Push-Location $repo +try{ + $inputs=@(@(& git ls-files Copper68k)+@('Copper68k.Tests/Copper68k.Tests.csproj', + 'Copper68k.Tests/Synthetic/SyntheticM68030PmmuPrivilegeTests.cs','Copper68k.Tests/Synthetic/SyntheticMachine.cs', + 'Copper68k.Tests/Synthetic/ArchitecturalExpectation.cs','Copper68k.Tests/Synthetic/SyntheticExecution.cs', + 'scripts/test-copper68k-030-pmmu-privilege-mutations.ps1')|Sort-Object -Unique) + $normal=@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') + $filter='FullyQualifiedName~SyntheticM68030PmmuPrivilegeTests.UndefinedPmmuWord|FullyQualifiedName~SyntheticM68030PmmuPrivilegeTests.RuleIsSpecific|FullyQualifiedName~SyntheticM68030PmmuPrivilegeTests.NonzeroCpIds' + if(-not $ValidateReportsOnly){ + if(Test-Path -LiteralPath $output){throw 'Use a fresh PMMU mutation directory'} + New-Item -ItemType Directory -Path $output|Out-Null + $original=@($inputs|ForEach-Object{Identity $_});$originalNormal=@($normal|ForEach-Object{Identity $_}) + $bytes=[IO.File]::ReadAllBytes($source);$text=[IO.File]::ReadAllText($source).Replace("`r`n","`n") + $sourceHash=(Get-FileHash -LiteralPath $source).Hash;$expectedHash=$sourceHash + $baseline=Join-Path $output 'baseline';New-Item -ItemType Directory -Path $baseline|Out-Null + & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path (Join-Path $baseline 'build') --filter $filter --logger 'trx;LogFileName=controls.trx' --results-directory $baseline *> (Join-Path $baseline 'run.log') + if($LASTEXITCODE -ne 0){throw 'PMMU mutation baseline failed'} + Check-Execution $baseline @();$rows=@() + try{ + foreach($mutation in $mutations){ + $directory=Join-Path $output $mutation.name;New-Item -ItemType Directory -Path $directory|Out-Null + $mutated=Mutated-Source $text $mutation + [IO.File]::WriteAllText((Join-Path $directory 'mutated-cpu.cs'),$mutated) + if((Get-FileHash -LiteralPath $source).Hash -cne $sourceHash){throw 'CPU source changed before mutation'} + [IO.File]::WriteAllText($source,$mutated);$expectedHash=(Get-FileHash -LiteralPath $source).Hash + & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path (Join-Path $directory 'build') --filter $filter --logger 'trx;LogFileName=controls.trx' --results-directory $directory *> (Join-Path $directory 'run.log') + $code=$LASTEXITCODE;Restore-Source $bytes $expectedHash $sourceHash + if($code -ne 1){throw "PMMU mutation did not fail: $($mutation.name)"} + Check-Execution $directory @($mutation.failed) $mutation.reason + $rows+=@{name=$mutation.name;exit=$code;evidence=@((Evidence-Paths $directory $true)|ForEach-Object{Identity $_})} + } + }finally{Restore-Source $bytes $expectedHash $sourceHash} + foreach($entry in @($original)+@($originalNormal)){if((Identity $entry.path).sha256 -cne $entry.sha256){throw 'PMMU mutations changed original source/binaries'}} + @{schema=1;profile='m68030-pmmu-privilege-defect-proofs';sourceRevision=(& git rev-parse HEAD);sourceRestored=$true; + inputs=$original;normalBinaries=$originalNormal;baseline=@((Evidence-Paths $baseline $false)|ForEach-Object{Identity $_});mutations=$rows}|ConvertTo-Json -Depth 10|Set-Content (Join-Path $output 'identity.json') + } + $identity=Get-Content (Join-Path $output 'identity.json') -Raw|ConvertFrom-Json + if($identity.schema -ne 1 -or $identity.profile -cne 'm68030-pmmu-privilege-defect-proofs' -or $identity.sourceRestored -ne $true -or $identity.sourceRevision -cnotmatch '^[a-f0-9]{40}$'){throw 'Incomplete PMMU mutation profile'} + Validate-Selection @($identity.inputs) $inputs;Validate-Selection @($identity.normalBinaries) $normal + $baseline=Join-Path $output 'baseline';Validate-Selection @($identity.baseline) (Evidence-Paths $baseline $false);Check-Execution $baseline @() + if($identity.mutations.Count -ne 4 -or @($identity.mutations.name|Select-Object -Unique).Count -ne 4){throw 'Incomplete/duplicate PMMU mutations'} + $text=[IO.File]::ReadAllText($source).Replace("`r`n","`n") + foreach($mutation in $mutations){ + $row=@($identity.mutations|Where-Object name -CEQ $mutation.name) + if($row.Count -ne 1 -or $row[0].exit -ne 1){throw 'Incorrect PMMU mutation result selection/status'} + $directory=Join-Path $output $mutation.name + Validate-Selection @($row[0].evidence) (Evidence-Paths $directory $true) + if([IO.File]::ReadAllText((Join-Path $directory 'mutated-cpu.cs')) -cne (Mutated-Source $text $mutation)){throw 'Recorded PMMU mutation source differs'} + Check-Execution $directory @($mutation.failed) $mutation.reason + } + @{schema=1;sourceRestored=$true;productionCpuMutation=$true;mutationProofs=4;executions=235;roadmapComplete=$false;scope='CpID-0 priority, CpID/model/mode boundaries; no regression retired'}|ConvertTo-Json|Set-Content (Join-Path $output 'summary.json') + Write-Host '47 controls detect all four PMMU privilege CPU defects; original CPU bytes and normal assemblies preserved.' +}finally{Pop-Location} +$global:LASTEXITCODE=0 diff --git a/scripts/test-copper68k-030-pmmu-privilege.ps1 b/scripts/test-copper68k-030-pmmu-privilege.ps1 new file mode 100644 index 00000000..cb0fb7e8 --- /dev/null +++ b/scripts/test-copper68k-030-pmmu-privilege.ps1 @@ -0,0 +1,81 @@ +[CmdletBinding()] +param([switch]$ValidateReportsOnly,[string]$OutputDirectory='artifacts/030-pmmu-privilege') +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +$groups=@(foreach($matrix in @('opcodes','status')){foreach($route in @('scalar','batch')){"system-pmmu-privilege-$matrix-$route"}}) +function Identity([string]$Path){ + $file=Join-Path $repo $Path + if(-not(Test-Path -LiteralPath $file -PathType Leaf)){throw "Missing PMMU privilege input: $Path"} + @{path=$Path;sha256=(Get-FileHash -LiteralPath $file).Hash} +} +function Validate-Selection($Actual,[string[]]$Required){ + $paths=@($Actual.path) + if($paths.Count -ne $Required.Count -or @($paths|Select-Object -Unique).Count -ne $paths.Count -or + @(Compare-Object ($paths|Sort-Object) ($Required|Sort-Object)).Count){throw 'Incomplete/duplicate PMMU privilege identity selection'} + foreach($entry in $Actual){if($entry.sha256 -cnotmatch '^[A-F0-9]{64}$' -or (Identity $entry.path).sha256 -cne $entry.sha256){throw "Changed PMMU privilege input: $($entry.path)"}} +} +function Expected-Keys([string]$Matrix){ + $keys=[Collections.Generic.Dictionary[string,int]]::new([StringComparer]::Ordinal) + if($Matrix -ceq 'opcodes'){ + for($word=0xf000;$word -le 0xf1ff;$word++){$keys[('68030/PMMU/privilege/primary={0:X4}/extension=00BE/status=0700' -f $word)]=1} + }elseif($Matrix -ceq 'status'){ + for($format=0;$format -lt 8;$format++){foreach($extension in @(0,1,0xbe,0x2000,0x4000,0x8000,0xa000,0xffff)){ + for($trace=0;$trace -lt 4;$trace++){for($master=0;$master -lt 2;$master++){for($ipl=0;$ipl -lt 8;$ipl++){ + $word=0xf000+64*$format;$status=16384*$trace+4096*$master+256*$ipl + $keys[('68030/PMMU/privilege/primary={0:X4}/extension={1:X4}/status={2:X4}' -f $word,$extension,$status)]=32 + }}} + }} + }else{throw 'Unknown PMMU privilege matrix'} + return $keys +} +$prefix='Copper68k.Tests.Synthetic.SyntheticM68030PmmuPrivilegeTests.' +$names=@(($prefix+'UndefinedPmmuWordInUserModeRaisesPrivilegeBeforeLineF'))+ + @(foreach($method in @('EveryCpIdZeroPrimaryWordInUserMode','EveryDefinedUserStatusAndSecondaryWordBoundaries')){foreach($batch in @('False','True')){"$prefix${method}(batch: $batch)"}})+ + @(foreach($model in @('68000','68010','68EC020','68020','68030','68040','68060','A1200')){foreach($super in @('False','True')){foreach($batch in @('False','True')){"${prefix}RuleIsSpecificToUserModeOn68030(modelId: `"$model`", supervisor: $super, batch: $batch)"}}})+ + @(foreach($id in 1..7){foreach($batch in @('False','True')){"${prefix}NonzeroCpIdsKeepTheAbsentCoprocessorLineFRoute(id: $id, batch: $batch)"}}) +$saved=[Environment]::GetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR') +Push-Location $repo +try{ + $inputs=@(@(& git ls-files Copper68k)+@('Copper68k.Tests/Copper68k.Tests.csproj', + 'Copper68k.Tests/Synthetic/SyntheticM68030PmmuPrivilegeTests.cs','Copper68k.Tests/Synthetic/SyntheticMachine.cs', + 'Copper68k.Tests/Synthetic/ArchitecturalExpectation.cs','Copper68k.Tests/Synthetic/SyntheticExecution.cs', + 'Copper68k.Tests/Synthetic/SyntheticMoveTests.cs','scripts/test-copper68k-030-pmmu-privilege.ps1')|Sort-Object -Unique) + $binaries=@('build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'|ForEach-Object{[IO.Path]::GetRelativePath($repo,(Join-Path $output $_))}) + $evidence=@(@($groups|ForEach-Object{"68030-$_.json"})+@('privilege.trx','run.log')|ForEach-Object{[IO.Path]::GetRelativePath($repo,(Join-Path $output $_))}) + if(-not $ValidateReportsOnly){ + if(Test-Path -LiteralPath $output){throw 'Use a fresh PMMU privilege output directory'} + New-Item -ItemType Directory -Path $output|Out-Null + $original=@($inputs|ForEach-Object{Identity $_}) + [Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$output) + & dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path (Join-Path $output 'build') --filter 'FullyQualifiedName~SyntheticM68030PmmuPrivilegeTests' --logger 'trx;LogFileName=privilege.trx' --results-directory $output *> (Join-Path $output 'run.log') + if($LASTEXITCODE -ne 0){throw "PMMU privilege execution failed: $output/run.log"} + foreach($entry in $original){if((Identity $entry.path).sha256 -cne $entry.sha256){throw 'PMMU privilege source changed during execution'}} + @{schema=1;profile='m68030-cpid0-user-privilege';sourceRevision=(& git rev-parse HEAD);inputs=$original; + binaries=@($binaries|ForEach-Object{Identity $_});evidence=@($evidence|ForEach-Object{Identity $_})}|ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'identity.json') + } + $identity=Get-Content (Join-Path $output 'identity.json') -Raw|ConvertFrom-Json + if($identity.schema -ne 1 -or $identity.profile -cne 'm68030-cpid0-user-privilege' -or $identity.sourceRevision -cnotmatch '^[a-f0-9]{40}$'){throw 'Incomplete PMMU privilege identity profile'} + Validate-Selection @($identity.inputs) $inputs;Validate-Selection @($identity.binaries) $binaries;Validate-Selection @($identity.evidence) $evidence + [xml]$trx=Get-Content (Join-Path $output 'privilege.trx') -Raw + $c=$trx.TestRun.ResultSummary.Counters;$results=@($trx.TestRun.Results.UnitTestResult) + if($names.Count -ne 51 -or $c.total -ne 51 -or $c.executed -ne 51 -or $c.passed -ne 51 -or $c.failed -ne 0 -or + $results.Count -ne 51 -or @($results|Where-Object outcome -CNE Passed).Count -ne 0 -or + @(Compare-Object ($results.testName|Sort-Object) ($names|Sort-Object)).Count){throw 'Incomplete PMMU privilege execution selection'} + $coverage=@() + foreach($group in $groups){ + $matrix=$group -replace '^system-pmmu-privilege-','' -replace '-(scalar|batch)$','' + $keys=Expected-Keys $matrix;$cases=if($matrix -ceq 'opcodes'){512}else{131072} + $r=Get-Content (Join-Path $output "68030-$group.json") -Raw|ConvertFrom-Json -AsHashtable + if($r.schema -ne 1 -or $r.model -cne '68030' -or $r.group -cne $group -or $r.xunitBatches -ne 1 -or + $r.logicalCases -ne $cases -or $r.counts.passing -ne $cases -or $r.counts.mismatching -ne 0 -or + $r.counts.unsupported -ne 0 -or $r.counts.untested -ne 0 -or $r.failures.Count -ne 0 -or $r.combinations.Count -ne $keys.Count){throw "Incomplete PMMU privilege coverage: $group"} + foreach($key in $keys.Keys){if(-not $r.combinations.Contains($key) -or $r.combinations[$key].Count -ne 1 -or $r.combinations[$key].passing -ne $keys[$key]){throw "Missing/misweighted PMMU privilege combination: $key"}} + $coverage+=@{group=$group;scenarios=$cases;combinations=$keys.Count} + } + @{schema=1;logicalCases=263168;xunitBatches=4;fixedExecutions=47;coverage=$coverage;roadmapComplete=$false; + reference='MC68030UM 8.1.5/6, 9.8';scope='CpID-0 user privilege abort, preserved state/stacks, software frame skip/RTE and following sentinel; scalar/batch'; + remaining='Legal supervisor PMMU instruction semantics, enabled translation, physical coprocessor/bus timing and all remaining milestone-6 protocols'}|ConvertTo-Json -Depth 8|Set-Content (Join-Path $output 'summary.json') + Write-Host "030 PMMU user privilege: 263168 passing scenarios / 9216 combinations; $output" +}finally{[Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$saved);Pop-Location} +$global:LASTEXITCODE=0 diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 57d50ad6..6f0b8a32 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -142,6 +142,12 @@ try { $expected['system-pcr-reserved-policy'] = 6144 $expected['system-pcr-reset'] = 24576 } + if ($model -eq '68030') { + foreach ($route in @('scalar','batch')) { + $expected["system-pmmu-privilege-opcodes-$route"] = 512 + $expected["system-pmmu-privilege-status-$route"] = 131072 + } + } if ($model -eq '68010') { $expected['system-010-movec-pairs'] = 688128 $expected['system-010-movec-masks'] = 1007616 From 71605276ffd1d70a0378dece108604b29a6a8347 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 22:25:54 +0300 Subject: [PATCH 074/211] Isolate WinUAE audit builds and restore qualification evidence --- Copper68k.Tests/Synthetic/README.md | 8 +++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 54 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 11 ++++ .../test-copper68k-winuae-long-arithmetic.ps1 | 6 ++- ...-copper68k-winuae-qualified-exceptions.ps1 | 6 ++- 5 files changed, 81 insertions(+), 4 deletions(-) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index e993b6f1..ada12860 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1167,3 +1167,11 @@ Ordinary requirements are 86,097,474 scenarios / 707 reports. Missing external fixtures fail requested audits and remain unavailable coverage. Milestone 6 is incomplete; see the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#68030-integrated-mmu-user-privilege-qualification--2026-10-06), including the concurrent-cleanup limitation on the latest full execution. + +The maintained `test-copper68k-winuae-qualified-exceptions.ps1` and +`test-copper68k-winuae-long-arithmetic.ps1` commands build into +`/build` by default. Use `-ArtifactsPath` to select another +isolated build directory. Both require fresh report output directories and +preserve normal CPU/test assembly outputs. All nine scoped presets passed fresh +audits after pinned input regeneration; see the +[restoration record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#restored-scoped-winuae-qualification--2026-10-06). diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 461c0beb..d79be83e 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -6894,3 +6894,57 @@ Cleanup subsequently removed the consumer checkout and local replay inputs; these results are not described as fresh post-cleanup replays. Source and the published dependency pin remain untouched. No public package is published. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +#### Restored scoped WinUAE qualification — 2026-10-06 + +After cleanup stopped, fresh source checkouts at generator +`025b999239800357e95065fe5b9a15ea5b300fa7` and runner +`7a83745d6c6159bc74ab0471578ffc8bc244e66e` regenerated all nine deleted +qualified presets through `prepare-copper68k-winuae.ps1`. Tracked reference +sources remain unchanged. The maintained audit commands pass their complete +selections, including required corruption controls and architectural form +distributions. Their existing Motorola-qualified patches and reference caveats +remain applicable; these are software reference results, not silicon timing +qualification or an unchanged upstream oracle. + +| Preset | Passing xUnit executions | Model/family directories | Callbacks | Exception frames | +| --- | ---: | ---: | ---: | ---: | +| TrapBounds | 1 | 21 | 951,522 | 476,339 | +| Breakpoints | 1 | 7 | 224 | 224 | +| LongArithmetic | 19 | 12 | 28,418 | 4,992 | +| WordDivision | 15 | 16 | 134,928 | 37,804 | +| LowPowerStop | 11 | 1 | 245,760 | 245,760 | +| Moves | 17 | 21 | 110,492 | 86,072 | +| Cas | 16 | 18 | 49,284 | 2,466 | +| Cas2 | 12 | 12 | 3,180 | 2,296 | +| CacheEncodings | 13 | 8 | 2,048 | 2,048 | +| Total | 105 | 116 | 1,525,856 | 858,001 | + +All nine commands have zero failures or skips; all reports have zero +mismatching, unsupported or untested selected directories. The 105 executions +include nine native audit facts and 96 encoding controls. Callbacks include +distinct model/profile executions and are not a count of unique instruction +encodings. The fresh evidence is under +`C:/Users/ilkle/AppData/Local/Temp/copper68k-reference-restoration-20261006`: +`inputs/` contains each pinned input manifest, native bridge and source +identities; `audits/` contains TRX, report and isolated assemblies. +`verify-restoration.ps1` independently checks exact totals, passing outcomes, +pins, input bytes/hashes, report/manifest/native/assembly identities, row controls +and unchanged normal assemblies. `restoration-verification.json` records those +identities. New native binaries and manifests have their own hashes; no deleted +evidence is relabeled or reconstructed as identical. + +Both maintained audit wrappers now pass `--artifacts-path`, defaulting to +`/build`; optional `-ArtifactsPath` selects another isolated +build directory. CPU/test normal assembly hashes remain +`0145566d23551d1dd8793784d2d83b5fdbe9a9ad2db6c9199012b6f9af80308a` and +`180db7219a99097c9c24d7cae0cff17f5b9aa27798d241861ece75d59f1e8367`. +This slice changes audit output placement and documentation only, with no +production CPU or package changes. + +These fresh runs resolve the nine missing-input failures independently. The +earlier full run remains failed, its deleted deterministic JSON details remain +unavailable, and no new green full-suite run is claimed. SingleStepTests, +Musashi, broad Basic and consumer/native evidence retain the preceding cleanup +limitations. Remaining architectural gaps and consolidation work are unchanged; +milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 0d74819c..cf5b6e6d 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2573,3 +2573,14 @@ directory. The private `.67` consumer and external reference checks passed befor cleanup; their removed local records and replay inputs are not described as present or rerun. See the qualification record for this distinction. No public package release is performed. + +After cleanup stopped, all nine scoped WinUAE presets were regenerated from +their pinned sources and passed fresh maintained audits: 105 executions without +failures/skips, 116 model/family directories, 1,525,856 callbacks and 858,001 +exception frames. Exact input/report/binary identities and unchanged normal +assemblies are independently verified. Audit wrappers now isolate build outputs. +These runs resolve the missing-input failures separately; the cleanup-affected +full run is not reclassified green. See the +[restoration record](COPPER68K_REFERENCE_QUALIFICATION.md#restored-scoped-winuae-qualification--2026-10-06). +Other unavailable external evidence and all remaining milestone-6 gaps stay +explicit. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-winuae-long-arithmetic.ps1 b/scripts/test-copper68k-winuae-long-arithmetic.ps1 index f25ffa80..caf4a3d3 100644 --- a/scripts/test-copper68k-winuae-long-arithmetic.ps1 +++ b/scripts/test-copper68k-winuae-long-arithmetic.ps1 @@ -3,7 +3,8 @@ param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $InputDirectory, [string] $NativeLibrary, - [string] $OutputDirectory = 'artifacts/winuae-long-arithmetic-audit' + [string] $OutputDirectory = 'artifacts/winuae-long-arithmetic-audit', + [string] $ArtifactsPath ) $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot @@ -13,6 +14,7 @@ $library = (Resolve-Path -LiteralPath $NativeLibrary).Path $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } New-Item -ItemType Directory -Path $output | Out-Null +$build = if ($ArtifactsPath) { [IO.Path]::GetFullPath($ArtifactsPath, $repo) } else { Join-Path $output 'build' } $settings = @{ COPPER68K_RUN_WINUAE_LONG_ARITHMETIC_AUDIT = '1' COPPER68K_WINUAE_LONG_ARITHMETIC_PATH = $root @@ -25,7 +27,7 @@ try { $saved[$name] = [Environment]::GetEnvironmentVariable($name) [Environment]::SetEnvironmentVariable($name, $settings[$name]) } - & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter 'FullyQualifiedName~WinUaeLongArithmeticAcrossAdvancedModelsWhenEnabled|FullyQualifiedName~M68kWinUaeLongArithmeticEncodingTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --artifacts-path $build --filter 'FullyQualifiedName~WinUaeLongArithmeticAcrossAdvancedModelsWhenEnabled|FullyQualifiedName~M68kWinUaeLongArithmeticEncodingTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output if ($LASTEXITCODE -ne 0) { throw 'Qualified LongArithmetic audit failed' } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counts = $trx.TestRun.ResultSummary.Counters diff --git a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 index 2dda9539..41d14f2e 100644 --- a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 +++ b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 @@ -4,7 +4,8 @@ param( [ValidateSet('TrapBounds','Breakpoints','WordDivision','LowPowerStop','Moves','Cas','Cas2','CacheEncodings')] [string] $Preset = 'TrapBounds', [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $InputDirectory, [string] $NativeLibrary, - [string] $OutputDirectory = 'artifacts/winuae-trap-bounds-audit' + [string] $OutputDirectory = 'artifacts/winuae-trap-bounds-audit', + [string] $ArtifactsPath ) $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot @@ -14,6 +15,7 @@ $library = (Resolve-Path -LiteralPath $NativeLibrary).Path $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } New-Item -ItemType Directory -Path $output | Out-Null +$build = if ($ArtifactsPath) { [IO.Path]::GetFullPath($ArtifactsPath, $repo) } else { Join-Path $output 'build' } $key = switch ($Preset) {'TrapBounds' {'TRAP_BOUNDS'} 'Breakpoints' {'BREAKPOINT'} 'WordDivision' {'WORD_DIVISION'} 'LowPowerStop' {'LPSTOP'} 'Moves' {'MOVES'} 'Cas' {'CAS'} 'Cas2' {'CAS2'} 'CacheEncodings' {'CACHE_ENCODINGS'}} $testName = switch ($Preset) {'TrapBounds' {'WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled'} 'Breakpoints' {'WinUaeBreakpointExceptionsAcrossSelectedModelsWhenEnabled'} 'WordDivision' {'WinUaeWordDivisionAcrossSelectedModelsWhenEnabled'} 'LowPowerStop' {'WinUaeLowPowerStopExceptionsWhenEnabled'} 'Moves' {'WinUaeMovesAcrossSelectedModelsWhenEnabled'} 'Cas' {'WinUaeCasAcrossAdvancedModelsWhenEnabled'} 'Cas2' {'WinUaeCas2AcrossAdvancedModelsWhenEnabled'} 'CacheEncodings' {'WinUaeCacheScopeZeroAcrossSelectedModelsWhenEnabled'}} $reportName = switch ($Preset) {'TrapBounds' {'winuae-trap-bounds-audit.json'} 'Breakpoints' {'winuae-breakpoint-audit.json'} 'WordDivision' {'winuae-word-division-audit.json'} 'LowPowerStop' {'winuae-lpstop-audit.json'} 'Moves' {'winuae-moves-audit.json'} 'Cas' {'winuae-cas-audit.json'} 'Cas2' {'winuae-cas2-audit.json'} 'CacheEncodings' {'winuae-cache-encodings-audit.json'}} @@ -55,7 +57,7 @@ try { $saved[$name] = [Environment]::GetEnvironmentVariable($name) [Environment]::SetEnvironmentVariable($name, $settings[$name]) } - & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --filter $filter --logger 'trx;LogFileName=audit.trx' --results-directory $output + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --artifacts-path $build --filter $filter --logger 'trx;LogFileName=audit.trx' --results-directory $output if ($LASTEXITCODE -ne 0) { throw "Qualified $Preset audit failed" } [xml]$trx = Get-Content -LiteralPath (Join-Path $output 'audit.trx') -Raw $counts = $trx.TestRun.ResultSummary.Counters From d5d0b3fd34abef5a3c7ef4e75aa5ba47bac0b803 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 22:35:43 +0300 Subject: [PATCH 075/211] Restore broad CPU reference evidence and add isolated audit command --- Copper68k.Tests/Synthetic/README.md | 11 ++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 97 +++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 12 ++ scripts/test-copper68k-reference-audits.ps1 | 137 ++++++++++++++++++ 4 files changed, 257 insertions(+) create mode 100644 scripts/test-copper68k-reference-audits.ps1 diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index ada12860..e0cd6782 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1175,3 +1175,14 @@ isolated build directory. Both require fresh report output directories and preserve normal CPU/test assembly outputs. All nine scoped presets passed fresh audits after pinned input regeneration; see the [restoration record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#restored-scoped-winuae-qualification--2026-10-06). + +Run independent references without the deterministic matrix through +`./scripts/test-copper68k-reference-audits.ps1 -SingleStepPath +-MusashiPath -OutputDirectory `. Both selections +are optional individually; at least one reference is required. Builds are +isolated, complete selections and exact input identities are checked, and +requested failures remain failures. The broad Basic WinUAE selection requires +`-WinUaePath`, `-WinUaeGeneratorSource` and `-WinUaeRunnerSource`; its unresolved +discrepancies intentionally fail the command. See the +[reference-only command](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#reference-only-audit-command) +for reproducibility and caveats. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index d79be83e..c3512f5e 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -6948,3 +6948,100 @@ unavailable, and no new green full-suite run is claimed. SingleStepTests, Musashi, broad Basic and consumer/native evidence retain the preceding cleanup limitations. Remaining architectural gaps and consolidation work are unchanged; milestone 6 remains **in progress**, `roadmapComplete=false`. + +## Reference-only audit command + +`scripts/test-copper68k-reference-audits.ps1` runs the existing independent +adapters without rerunning the deterministic synthetic matrix. It requires at +least one requested reference, complete pinned inputs and a fresh output +directory; every requested test must execute and pass. Builds default to +`/build`, with optional `-ArtifactsPath` for another isolated +directory. Source revisions, input bytes/SHA256, command identity, report/TRX +and isolated CPU/test assembly hashes are retained. The command checks complete +per-file SingleStepTests coverage and per-model/program Musashi coverage, +including their documented exclusions, then verifies inputs stayed unchanged. +It rejects empty selections, wrong pins, missing inputs and orphan WinUAE +source arguments. It never publishes a package or alters comparator masks. + +```powershell +./scripts/test-copper68k-reference-audits.ps1 ` + -SingleStepPath ` + -MusashiPath ` + -OutputDirectory + +./scripts/test-copper68k-reference-audits.ps1 ` + -WinUaePath ` + -WinUaeGeneratorSource ` + -WinUaeRunnerSource ` + -OutputDirectory +``` + +The second command deliberately fails while the Basic discrepancies remain. +Its full diagnostics are retained; no passing verification manifest is emitted +for a failed selection. The native adapter still performs its complete profile, +directory, binary header, input identity and corruption-probe checks. Basic +requires the explicit `Basic` preset manifest from the maintained preparation +command; other scoped presets cannot substitute for broad qualification. + +### Restored broad reference evidence — 2026-10-06 + +Fresh pinned SingleStepTests and Musashi checkouts pass the reference-only +command's complete selections: **312,500 cases / 125 files** and **536 passing +model/program combinations / 88 explicit exclusions**, respectively. Both +facts execute without skips. `audits/IndependentFinal/reference-inputs.json` +and `reference-verification.json` under the restoration temporary root above +record all input bytes/hashes, pins, command and report/TRX/assembly identities. +Each SingleStepTests file passes exactly 2,500 cases; every Musashi profile has +the documented passing/excluded program distribution. Existing trace-boundary, +invalid-input, unavailable-instruction and software-reference caveats remain. +Four preflight controls reject empty selection, wrong source revision, absent +inputs and orphan WinUAE source arguments in +`reference-command-controls-final.json`. + +The broad Basic corpus is also freshly regenerated at the same generator/runner +pins. Its manifest SHA256 is +`b5741ca90fa75d3510ef25bac33d2d36d6a92ab2283f0666e762a5f1853a38ed`, +native bridge SHA256 +`135d8c7d01137bbef2467e34207444984fd47a9076041a0c251848d2518b32ce`. +The audit reproduces **1,327 passing / 46 mismatching / eight unsupported / +zero untested directories**, 11,478,371 callbacks and 1,668,069 exception-frame +checks. These include partial failing directories, not all-passing coverage. +All eight profiles execute their complete directory selection and register, +defined-SR, exception-frame and ignored-undefined-SR controls. The requested +command fails, retaining original diagnostics. No comparator mask, CPU behavior, +Basic input or family selection is changed to obtain agreement. + +The 54 non-passing model/family rows remain explicit: + +| Family | Non-passing rows | Current disposition | +| --- | ---: | --- | +| BKPT | 7 | Saved-PC software disagreement; separate qualified Breakpoints preset passes. | +| CHK2 B/W/L | 15 | Saved-PC software disagreement; separate qualified TrapBounds preset passes. | +| TRAPcc | 6 | Saved-PC software disagreement; separate qualified TrapBounds preset passes. | +| DIVL.L / MULL.L | 10 | Eight emulator-unsupported Basic inputs plus two 060 saved-PC disagreements; canonical LongArithmetic qualification passes separately. | +| DIVU.W | 4 | Overflow flag software disagreement; WordDivision qualification passes separately. | +| MOVES.B / MOVES.L | 3 | Reserved extension fields/undefined same-An stores; legal-input Moves qualification passes separately. | +| CAS.W / CAS.L | 2 | 060 saved-PC disagreement; separate Cas qualification passes. | +| CAS2.W | 1 | 040 compare alias software disagreement; separate Cas2 qualification passes. | +| LPSTOP | 1 | Privilege-fetch/saved-PC software disagreement; separate LowPowerStop qualification passes. | +| ILLEGAL | 2 | 040/060 F400 cache encoding software disagreement; separate CacheEncodings qualification passes. | +| RTE | 1 | 010 format-error CCR disagreement remains required. | +| STOP | 1 | 060 supervisor-bit-cleared disagreement remains required. | +| MOVE16 | 1 | 060 Basic failure uses noncanonical extension 6000; legal-input independent qualification remains required. | + +The recovered MOVE16 diagnostic is `F626 6000`, user SR 0000, source/destination +A6 `0087FE9F`. The reference increments A6 while Copper68k raises vector 4. +[M68000PRM MOVE16](https://www.nxp.com/docs/en/reference-manual/M68000PRM.pdf) +4-127 requires extension bit 15 set and bits 11..0 clear; the canonical same-A6 +form is `F626 E000`. The pinned generator instead reads only extension bits +14..12 and does not enforce bit 15. This establishes a noncanonical input, +not authority for a CPU change or a hardware-defined outcome for every reserved +extension. A canonical-input MOVE16 audit is the next reference qualification +slice; original Basic failure remains retained. + +This closes the local availability gap for SingleStepTests, Musashi and Basic +diagnostics with fresh identities. It does not recreate deleted deterministic +JSON details or consumer/media inputs, reclassify the earlier failed full run, +or close advanced frame/recovery and consolidation requirements. Production CPU, +normal assemblies and packages are unchanged. Milestone 6 remains **in +progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index cf5b6e6d..c107ee92 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2584,3 +2584,15 @@ full run is not reclassified green. See the [restoration record](COPPER68K_REFERENCE_QUALIFICATION.md#restored-scoped-winuae-qualification--2026-10-06). Other unavailable external evidence and all remaining milestone-6 gaps stay explicit. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +Pinned SingleStepTests, Musashi and the broad Basic corpus are now restored and +rerun through a maintained isolated reference-only command. SingleStepTests +passes 312,500 cases / 125 files; Musashi passes 536 model/program combinations +with 88 documented exclusions. Basic intentionally fails its preserved 46 +mismatching and eight unsupported rows; fresh diagnostics reproduce the prior +counts and expose a noncanonical 060 MOVE16 extension for the next legal-input +qualification. Four command preflight controls reject invalid requests. See the +[fresh evidence and explicit gap table](COPPER68K_REFERENCE_QUALIFICATION.md#restored-broad-reference-evidence--2026-10-06). +No production CPU change, regression retirement or package release is included. +Advanced restoration and remaining consolidation requirements remain open; +milestone 6 is **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-reference-audits.ps1 b/scripts/test-copper68k-reference-audits.ps1 new file mode 100644 index 00000000..8356ebe9 --- /dev/null +++ b/scripts/test-copper68k-reference-audits.ps1 @@ -0,0 +1,137 @@ +#requires -Version 7.0 +[CmdletBinding()] +param( + [string] $SingleStepPath, + [string] $MusashiPath, + [string] $WinUaePath, + [string] $WinUaeGeneratorSource, + [string] $WinUaeRunnerSource, + [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $OutputDirectory, + [string] $ArtifactsPath +) +$ErrorActionPreference = 'Stop' +$repo = Split-Path -Parent $PSScriptRoot +if (-not $SingleStepPath -and -not $MusashiPath -and -not $WinUaePath) { throw 'Select at least one reference audit' } +if (-not $WinUaePath -and ($WinUaeGeneratorSource -or $WinUaeRunnerSource)) { throw 'WinUAE source arguments require a Basic input directory' } +$output = [IO.Path]::GetFullPath($OutputDirectory, $repo) +if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } +$build = if ($ArtifactsPath) { [IO.Path]::GetFullPath($ArtifactsPath, $repo) } else { Join-Path $output 'build' } +$models = @('68000','68010','68EC020','68020','68030','68040','68060','A1200') +function Hash([string]$Path) { (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() } +function PinnedSource([string]$Path, [string]$Pin) { + if (-not $Path) { throw "Source checkout required for pin $Pin" } + $resolved = (Resolve-Path -LiteralPath $Path).Path + $head = & git -C $resolved rev-parse HEAD + if ($LASTEXITCODE -ne 0 -or $head -ne $Pin) { throw "Wrong reference source pin: $resolved" } + & git -C $resolved diff --quiet HEAD -- + if ($LASTEXITCODE -ne 0) { throw "Modified reference source: $resolved" } + return $resolved +} +function InputFiles([string]$Root, [string]$Pattern) { + $files = @(Get-ChildItem -LiteralPath $Root -Recurse -File -Filter $Pattern | Sort-Object FullName) + if ($files.Count -eq 0) { throw "Empty input selection: $Root" } + @($files | ForEach-Object { [ordered]@{ path=[IO.Path]::GetRelativePath($Root,$_.FullName).Replace('\','/'); bytes=$_.Length; sha256=(Hash $_.FullName) } }) +} +$jobs = @() +$identities = @() +if ($SingleStepPath) { + $pin = '64b253116a3de04aaac4346c43680960dc9b67e5' + $root = PinnedSource $SingleStepPath $pin + $files = @(InputFiles $root '*.json.bin') + if ($files.Count -ne 127) { throw 'Incomplete SingleStepTests input selection; expected 127 files' } + $identities += [ordered]@{ reference='SingleStepTests'; source=$root; pin=$pin; inputs=$files } + $jobs += @{ name='singlestep'; test='OfficialSingleStepCorpusMatchesInterpreterWhenEnabled'; settings=@{ + COPPER68K_RUN_M68000_SINGLESTEP='1'; COPPER68K_M68000_SINGLESTEP_PATH=$root + COPPER68K_M68000_SINGLESTEP_FILTER=''; COPPER68K_M68000_SINGLESTEP_LIMIT='0' + COPPER68K_M68000_SINGLESTEP_BACKEND='interpreter'; COPPER68K_M68000_SINGLESTEP_VALIDATE_CYCLES='0' + COPPER68K_M68000_SINGLESTEP_INCLUDE_UNVERIFIED='0'; COPPER68K_M68000_SINGLESTEP_AUDIT='1' + COPPER68K_M68000_SINGLESTEP_SOURCE_REVISION=$pin + } } +} +if ($MusashiPath) { + $pin = '72c1d74800f3087b45a0c1a7342601bbed898881' + $root = PinnedSource $MusashiPath $pin + $files = @(InputFiles (Join-Path $root 'test') '*.bin') + foreach ($spec in @(@('mc68000',60),@('mc68040',18))) { + if (@($files | Where-Object { $_.path.StartsWith("$($spec[0])/",[StringComparison]::Ordinal) }).Count -ne $spec[1]) { throw "Incomplete Musashi selection: $($spec[0])" } + } + if ($files.Count -ne 78) { throw 'Unexpected Musashi input selection' } + $identities += [ordered]@{ reference='Musashi'; source=$root; pin=$pin; inputs=$files } + $jobs += @{ name='musashi'; test='MusashiIntegerProgramsAcrossSelectedModelsWhenEnabled'; settings=@{ + COPPER68K_RUN_MUSASHI_MODEL_AUDIT='1'; COPPER68K_MUSASHI_MODEL_AUDIT_PATH=$root + } } +} +if ($WinUaePath) { + $generatorPin='025b999239800357e95065fe5b9a15ea5b300fa7' + $runnerPin='7a83745d6c6159bc74ab0471578ffc8bc244e66e' + $generator=PinnedSource $WinUaeGeneratorSource $generatorPin + $runner=PinnedSource $WinUaeRunnerSource $runnerPin + $root=(Resolve-Path -LiteralPath $WinUaePath).Path + $manifest=Get-Content -LiteralPath (Join-Path $root 'manifest.json') -Raw | ConvertFrom-Json + $native=Join-Path $root 'm68k_cpu_tester.dll' + if ($manifest.Schema -ne 1 -or $manifest.Preset -ne 'Basic' -or $manifest.GeneratorCommit -ne $generatorPin -or $manifest.RunnerCommit -ne $runnerPin -or $manifest.NativeLibrarySha256 -ne (Hash $native)) { throw 'Incompatible Basic WinUAE input manifest/native bridge' } + $identities += [ordered]@{ reference='WinUAE Basic'; generator=$generator; generatorPin=$generatorPin; runner=$runner; runnerPin=$runnerPin; source=$root; manifestSha256=(Hash (Join-Path $root 'manifest.json')); nativeSha256=(Hash $native); inputs=@(InputFiles $root '*.dat') } + $jobs += @{ name='winuae-basic'; test='WinUaeIntegerFixturesAcrossSelectedModelsWhenEnabled'; settings=@{ + COPPER68K_RUN_WINUAE_MODEL_AUDIT='1'; COPPER68K_WINUAE_MODEL_PATH=$root; COPPER68K_WINUAE_CPUTEST_LIBRARY=$native + } } +} +New-Item -ItemType Directory -Path $output | Out-Null +[ordered]@{ schema=1; sourceCommit=(& git -C $repo rev-parse HEAD); commandSha256=(Hash $PSCommandPath); inputs=$identities } | + ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'reference-inputs.json') -Encoding utf8 +$records=@() +foreach ($job in $jobs) { + $reportDir=Join-Path $output $job.name + New-Item -ItemType Directory -Path $reportDir | Out-Null + $job.settings.COPPER68K_SYNTHETIC_REPORT_DIR=$reportDir + $job.settings.COPPER68K_SYNTHETIC_MODELS=$models -join ',' + $saved=@{} + try { + foreach ($key in $job.settings.Keys) { $saved[$key]=[Environment]::GetEnvironmentVariable($key); [Environment]::SetEnvironmentVariable($key,$job.settings[$key]) } + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --artifacts-path $build --filter "FullyQualifiedName~$($job.test)" --logger 'trx;LogFileName=audit.trx' --results-directory $reportDir + $exitCode=$LASTEXITCODE + [xml]$trx=Get-Content -LiteralPath (Join-Path $reportDir 'audit.trx') -Raw + $counts=$trx.TestRun.ResultSummary.Counters + if ($exitCode -ne 0) { throw "$($job.name) failed; retained diagnostics: $reportDir" } + if ([int]$counts.total -ne 1 -or [int]$counts.executed -ne 1 -or [int]$counts.passed -ne 1 -or $trx.TestRun.Results.UnitTestResult.outcome -ne 'Passed') { throw "$($job.name) audit did not execute exactly one passing test" } + $reportName=switch ($job.name) { 'singlestep' {'singlestep-model-audit.json'} 'musashi' {'musashi-model-audit.json'} 'winuae-basic' {'winuae-model-audit.json'} } + $reportPath=Join-Path $reportDir $reportName + $audit=Get-Content -LiteralPath $reportPath -Raw | ConvertFrom-Json + if ($audit.mismatching -ne 0) { throw "$($job.name) report has mismatches" } + switch ($job.name) { + 'singlestep' { + $inputs=($identities | Where-Object reference -eq SingleStepTests).inputs + $expected=@($inputs | Where-Object { [IO.Path]::GetFileName($_.path) -notin @('TAS.json.bin','TRAPV.json.bin') }) + if ($audit.schema -ne 1 -or $audit.sourceRevision -ne '64b253116a3de04aaac4346c43680960dc9b67e5' -or $audit.model -ne '68000' -or $audit.backend -ne 'Interpreter' -or $audit.validateCycles -or $audit.filter -or $audit.executed -ne 312500 -or $audit.passing -ne 312500 -or $audit.rows.Count -ne 125 -or @($audit.excluded | Sort-Object -Unique).Count -ne 2 -or @($audit.excluded | Where-Object { $_ -notin @('TAS.json.bin','TRAPV.json.bin') }).Count -ne 0) { throw 'Incomplete SingleStepTests audit' } + foreach ($file in $expected) { + $row=@($audit.rows | Where-Object File -CEQ ([IO.Path]::GetFileName($file.path))) + if ($row.Count -ne 1 -or $row[0].Sha256 -ne $file.sha256 -or $row[0].Passing -ne 2500 -or $row[0].Mismatching -ne 0) { throw "SingleStepTests coverage/identity: $($file.path)" } + } + } + 'musashi' { + $inputs=($identities | Where-Object reference -eq Musashi).inputs + if ($audit.schema -ne 1 -or ($audit.selectedModels -join ',') -ne ($models -join ',') -or $audit.rows.Count -ne 624 -or $audit.passing -ne 536 -or $audit.excluded -ne 88) { throw 'Incomplete Musashi audit' } + foreach ($model in $models) { + $requiredPassing=switch ($model) { '68000' {55} '68010' {55} '68060' {66} default {72} } + if (@($audit.rows | Where-Object { $_.Model -ceq $model -and $_.Status -eq 'passing' }).Count -ne $requiredPassing) { throw "Musashi model coverage: $model" } + foreach ($file in $inputs) { + $row=@($audit.rows | Where-Object { $_.Model -ceq $model -and $_.Program -ceq $file.path }) + if ($row.Count -ne 1 -or $row[0].Sha256 -ne $file.sha256 -or $row[0].Status -notin @('passing','excluded') -or ($row[0].Status -eq 'passing' -and $row[0].RetiredInstructions -le 0) -or ($row[0].Status -eq 'excluded' -and -not $row[0].Detail)) { throw "Musashi coverage/identity: $model/$($file.path)" } + } + } + } + 'winuae-basic' { + if ($audit.schema -ne 2 -or ($audit.selectedModels -join ',') -ne ($models -join ',') -or $audit.rows.Count -ne 1381 -or $audit.passing -ne 1381 -or $audit.unsupported -ne 0 -or $audit.untested -ne 0 -or $audit.executedCases -le 0 -or @($audit.rows | Where-Object { $_.Status -ne 'passing' -or $_.ExecutedCases -le 0 }).Count -ne 0) { throw 'Incomplete WinUAE Basic audit' } + } + } + foreach ($identity in $identities) { + $inputRoot=if ($identity.reference -eq 'Musashi') { Join-Path $identity.source 'test' } else { $identity.source } + foreach ($file in $identity.inputs) { + $path=Join-Path $inputRoot $file.path + if ((Get-Item -LiteralPath $path).Length -ne $file.bytes -or (Hash $path) -ne $file.sha256) { throw "Reference input changed during audit: $path" } + } + } + $records += [ordered]@{ reference=$job.name; reportSha256=(Hash $reportPath); trxSha256=(Hash (Join-Path $reportDir 'audit.trx')); cpuSha256=(Hash (Join-Path $build 'bin/Copper68k/release/Copper68k.dll')); testSha256=(Hash (Join-Path $build 'bin/Copper68k.Tests/release/Copper68k.Tests.dll')) } + } finally { foreach ($key in $saved.Keys) { [Environment]::SetEnvironmentVariable($key,$saved[$key]) } } +} +[ordered]@{ schema=1; inputsSha256=(Hash (Join-Path $output 'reference-inputs.json')); audits=$records; roadmapComplete=$false } | + ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'reference-verification.json') -Encoding utf8 From cab99ec3570ff5bfe4bc8996e6293955ce2e9a66 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 23:07:26 +0300 Subject: [PATCH 076/211] Qualify canonical MOVE16 reference inputs and coverage --- .../M68kWinUaeCpuTesterConformanceTests.cs | 16 +- Copper68k.Tests/M68kWinUaeMove16AuditTests.cs | 148 ++++++++++++++++++ .../M68kWinUaeTrapBoundsAuditTests.cs | 31 +++- Copper68k.Tests/Synthetic/README.md | 17 ++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 68 ++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 12 ++ scripts/prepare-copper68k-winuae.ps1 | 31 ++-- ...-copper68k-winuae-qualified-exceptions.ps1 | 11 +- scripts/winuae/move16-encodings.patch | 36 +++++ 9 files changed, 353 insertions(+), 17 deletions(-) create mode 100644 Copper68k.Tests/M68kWinUaeMove16AuditTests.cs create mode 100644 scripts/winuae/move16-encodings.patch diff --git a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs index 94b36847..31f6c93e 100644 --- a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs +++ b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs @@ -310,6 +310,7 @@ private sealed class NativeTester : IDisposable private bool _corruptFrame; private bool _corruptSavedPc; private bool _corruptCas2CompareAlias; + private bool _corruptMove16Memory; private ushort _corruptSr; private bool _corruptIgnoredSr; private string _integerFamily = ""; @@ -368,7 +369,8 @@ public RunResult Run( Func? fixtureWordsClassifier = null, bool corruptSavedPc = false, Func, string>? fixtureRegisterClassifier = null, - bool corruptCas2CompareAlias = false) + bool corruptCas2CompareAlias = false, + bool corruptMove16Memory = false) { _callbackException = null; _executedCases = 0; @@ -383,6 +385,7 @@ public RunResult Run( _corruptFrame = corruptFrame; _corruptSavedPc = corruptSavedPc; _corruptCas2CompareAlias = corruptCas2CompareAlias; + _corruptMove16Memory = corruptMove16Memory; _corruptSr = corruptSr; _corruptIgnoredSr = corruptIgnoredSr; _integerFamily = opcode; @@ -527,6 +530,15 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt _fixtureForms[form] = _fixtureForms.GetValueOrDefault(form) + 1; } bus.CopyStackImage(registers.Regs[15], registers.Ssp, 0x20); + uint? corruptDestination = null; + if (_corruptMove16Memory) + { + var inputRegisters = (uint[])registers.Regs.Clone(); + if ((registers.Sr & 0x2000) != 0) inputRegisters[15] = registers.Ssp; + corruptDestination = Move16Operands(_integerProfile!, bus.ReadHostWord(registers.Pc), + bus.ReadHostWord(registers.Pc + 2), bus.ReadHostWord(registers.Pc + 4), + (ushort)registers.Sr, inputRegisters, _integerFamily).Destination & ~15u; + } if (_fixtureRegisterClassifier is not null) { // The native input stores USP in slot A7 even when S is set. @@ -648,6 +660,8 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt if (_corruptIgnoredSr) registers.Sr ^= (uint)(~mask & 31); } if (_corruptResult) registers.Regs[0] ^= 1; + if (corruptDestination.HasValue && cpu.State.LastExceptionVector < 0) + bus.WriteHostWord(corruptDestination.Value, (ushort)(bus.ReadHostWord(corruptDestination.Value) ^ 1)); registers.Sr ^= _corruptSr; if (_corruptFrame && cpu.State.LastExceptionVector >= 0) { diff --git a/Copper68k.Tests/M68kWinUaeMove16AuditTests.cs b/Copper68k.Tests/M68kWinUaeMove16AuditTests.cs new file mode 100644 index 00000000..17296b5f --- /dev/null +++ b/Copper68k.Tests/M68kWinUaeMove16AuditTests.cs @@ -0,0 +1,148 @@ +using Copper68k.Tests.Synthetic; +using Xunit.Sdk; + +namespace Copper68k.Tests; + +public sealed partial class M68kWinUaeCpuTesterConformanceTests +{ + [EnvironmentFact("COPPER68K_RUN_WINUAE_MOVE16_AUDIT", "audit canonical WinUAE MOVE16 encodings and line transfers")] + public void WinUaeMove16AcrossSupportedModelsWhenEnabled() => RunQualifiedExceptionPreset(new( + "Move16", "MOVE16", "cputest-move16.cpp", "move16-encodings.patch", "Move16", + "b201915e70fa2c7cc7c87cbfef588eb910bf3e655ea02bbff0955e42bc4e2e55", "c23761b715a1eb8abc01f4293710ffda73bb48f9a4867f1d0087c462eaa7ed12", + "winuae-move16-audit.json", ["68040", "68060"], ["68040", "68060"], + _ => ["MOVE16"], (_, _) => (41856, 0), + "M68000PRM 4-125..127: five canonical forms, aligned 16-byte line copy, low address bits retained in postincrements, same register increments once, CCR unchanged. Copied input generator fixes extension bits before EA construction, serializes explicit valid An addresses, and enumerates every Ay in sixteen low-nibble rounds. Original Basic inputs/failures remain unchanged. CCR 0/31; all 64 post-post register pairs in user mode and 49 non-A7 pairs in supervisor mode are independently executed. Thirty supervisor A7 post-post pair/CCR and eight absolute-form A7/CCR combinations per model are absent from this reference selection and remain required reference follow-up; their ordinary synthetic semantics are retained. Four absolute forms include user/supervisor. No incoming trace/faults, cache allocation, physical burst/bus order or timing qualification. Unavailable models retain synthetic Line-F coverage.", + FormCounts: (_, _) => 25260, ClassifyRegisters: QualifyMove16Encoding, + ExpectedFormHash: (_, _) => "143e6c025a7451664853b10bfe94651dae28860263dad370a98ad6a33c77d236", + AllowFrameFreeFamilies: true, MemoryControls: true, ReferenceGaps: Move16ReferenceGaps)); + + internal static IReadOnlyList Move16ReferenceGaps(IReadOnlyDictionary forms) + { + var seen = forms.Keys.Select(x => { var fields = x.Split('/'); return fields[1] + "/" + fields[2] + "/" + fields[^1]; }) + .ToHashSet(StringComparer.Ordinal); + var missing = new List(); + for (var source = 0; source < 8; source++) + for (var destination = 0; destination < 8; destination++) + foreach (var sr in new[] { 0, 0x1f, 0x2000, 0x201f }) + { + var key = $"form=post-post/A={source}:{destination}/sr={sr:X4}"; + if (!seen.Contains(key)) missing.Add("MOVE16/" + key); + } + foreach (var form in new[] { "post-abs", "abs-post", "indirect-abs", "abs-indirect" }) + for (var register = 0; register < 8; register++) + foreach (var sr in new[] { 0, 0x1f, 0x2000, 0x201f }) + { + var key = $"form={form}/A={register}:abs/sr={sr:X4}"; + if (!seen.Contains(key)) missing.Add("MOVE16/" + key); + } + return missing; + } + + // Decode only documented immutable fields and input registers; never call + // production instruction decoding or effective-address helpers. + internal static (uint Source, uint Destination) Move16Operands(ModelSpec model, ushort opcode, + ushort first, ushort second, ushort sr, IReadOnlyList registers, string family) + { + var form = (opcode >> 3) & 7; + if (model.Id is not ("68040" or "68060") || registers.Count != 16 || family != "MOVE16" || + (opcode & 0xffc0) != 0xf600 || form > 4 || sr is not (0 or 0x1f or 0x2000 or 0x201f) || + (form == 4 && (first & 0x8fff) != 0x8000)) + throw new XunitException($"Unqualified MOVE16 encoding/profile: {model.Id}/{family}/{opcode:X4}/{first:X4}/{second:X4}/{sr:X4}"); + var register = registers[8 + (opcode & 7)]; + var absolute = ((uint)first << 16) | second; + return form switch + { + 0 or 2 => (register, absolute), + 1 or 3 => (absolute, register), + _ => (register, registers[8 + ((first >> 12) & 7)]) + }; + } + + internal static string QualifyMove16Encoding(ModelSpec model, ushort opcode, ushort first, ushort second, + ushort sr, IReadOnlyList registers, string family) + { + var (source, destination) = Move16Operands(model, opcode, first, second, sr, registers, family); + var form = (opcode >> 3) & 7; + var name = form switch { 0 => "post-abs", 1 => "abs-post", 2 => "indirect-abs", 3 => "abs-indirect", _ => "post-post" }; + var other = form == 4 ? ((first >> 12) & 7).ToString() : "abs"; + return $"MOVE16/form={name}/A={opcode & 7}:{other}/sameRegister={form == 4 && (opcode & 7) == ((first >> 12) & 7)}/offset={source & 15}:{destination & 15}/sameLine={(source & ~15u) == (destination & ~15u)}/sr={sr:X4}"; + } +} + +public sealed class M68kWinUaeMove16EncodingTests +{ + private static uint[] Registers() => Enumerable.Range(0, 16).Select(x => 0x1000u + (uint)(x * 0x101)).ToArray(); + + [Theory] + [InlineData("68040", 0xf600, 0, 0x221f, 0, "MOVE16/form=post-abs/A=0:abs/sameRegister=False/offset=8:15/sameLine=False/sr=0000")] + [InlineData("68060", 0xf609, 0, 0x221f, 0x201f, "MOVE16/form=abs-post/A=1:abs/sameRegister=False/offset=15:9/sameLine=False/sr=201F")] + [InlineData("68040", 0xf612, 0, 0x221f, 0x1f, "MOVE16/form=indirect-abs/A=2:abs/sameRegister=False/offset=10:15/sameLine=False/sr=001F")] + [InlineData("68060", 0xf61b, 0, 0x221f, 0x2000, "MOVE16/form=abs-indirect/A=3:abs/sameRegister=False/offset=15:11/sameLine=False/sr=2000")] + [InlineData("68040", 0xf626, 0xe000, 0x4e71, 0, "MOVE16/form=post-post/A=6:6/sameRegister=True/offset=14:14/sameLine=True/sr=0000")] + [InlineData("68060", 0xf627, 0x8000, 0x4e71, 0x201f, "MOVE16/form=post-post/A=7:0/sameRegister=False/offset=15:8/sameLine=False/sr=201F")] + public void FixedManualExamplesDistinguishFormsAliasesAndOffsets(string model, int opcode, int first, int second, int sr, string expected) => + Assert.Equal(expected, M68kWinUaeCpuTesterConformanceTests.QualifyMove16Encoding(ModelSpec.All.Single(x => x.Id == model), + (ushort)opcode, (ushort)first, (ushort)second, (ushort)sr, Registers(), "MOVE16")); + + [Theory] + [InlineData("68000", 0xf600, 0, 0x221f, 0, "MOVE16")] + [InlineData("68010", 0xf600, 0, 0x221f, 0, "MOVE16")] + [InlineData("68EC020", 0xf600, 0, 0x221f, 0, "MOVE16")] + [InlineData("68020", 0xf600, 0, 0x221f, 0, "MOVE16")] + [InlineData("68030", 0xf600, 0, 0x221f, 0, "MOVE16")] + [InlineData("A1200", 0xf600, 0, 0x221f, 0, "MOVE16")] + [InlineData("68060", 0xf626, 0x6000, 0x4e71, 0, "MOVE16")] + [InlineData("68040", 0xf626, 0xe001, 0x4e71, 0, "MOVE16")] + [InlineData("68060", 0xf626, 0xe800, 0x4e71, 0, "MOVE16")] + [InlineData("68040", 0xf628, 0, 0, 0, "MOVE16")] + [InlineData("68040", 0xf600, 0, 0x221f, 0xa000, "MOVE16")] + [InlineData("68060", 0xf600, 0, 0x221f, 0, "MOVE")] + public void ReservedUnavailableAndForeignFormsAreRejected(string model, int opcode, int first, int second, int sr, string family) => + Assert.Throws(() => M68kWinUaeCpuTesterConformanceTests.QualifyMove16Encoding(ModelSpec.All.Single(x => x.Id == model), + (ushort)opcode, (ushort)first, (ushort)second, (ushort)sr, Registers(), family)); + + [Fact] + public void AddressWordsAreDataForAbsoluteFormsAndRegistersMustBeComplete() + { + var model = ModelSpec.All.Single(x => x.Id == "68060"); + Assert.Equal((0x1808u, 0x6000e001u), M68kWinUaeCpuTesterConformanceTests.Move16Operands(model, 0xf600, 0x6000, 0xe001, 0, Registers(), "MOVE16")); + Assert.Throws(() => M68kWinUaeCpuTesterConformanceTests.Move16Operands(model, 0xf600, 0, 0, 0, [], "MOVE16")); + } + + [Fact] + public void CoverageFingerprintUsesOrdinalKeysAndPreservesWeights() + { + var forms = new Dictionary { ["b"] = 1, ["a"] = 2 }; + Assert.Equal("b31f1b9cb2e96c4635034c21830f25be40592cee8c1ddc5c9cf83430501ecce0", + M68kWinUaeCpuTesterConformanceTests.WinUaeFormDistributionSha256(forms)); + var redistributed = new Dictionary { ["a"] = 1, ["b"] = 2 }; + Assert.NotEqual(M68kWinUaeCpuTesterConformanceTests.WinUaeFormDistributionSha256(forms), + M68kWinUaeCpuTesterConformanceTests.WinUaeFormDistributionSha256(redistributed)); + } + + [Fact] + public void MissingReferencePairsRemainExplicitEvenWhenSelectedCallbacksPass() + { + var forms = new Dictionary(); + foreach (var form in new[] { "post-abs", "abs-post", "indirect-abs", "abs-indirect" }) + for (var register = 0; register < 8; register++) + foreach (var sr in new[] { 0, 0x1f, 0x2000, 0x201f }) + { + if (sr >= 0x2000 && register == 7) continue; + forms[$"MOVE16/form={form}/A={register}:abs/sameRegister=False/offset=0:0/sameLine=False/sr={sr:X4}"] = 1; + } + for (var source = 0; source < 8; source++) + for (var destination = 0; destination < 8; destination++) + foreach (var sr in new[] { 0, 0x1f, 0x2000, 0x201f }) + { + if (sr >= 0x2000 && (source == 7 || destination == 7)) continue; + forms[$"MOVE16/form=post-post/A={source}:{destination}/sameRegister=False/offset=0:0/sameLine=False/sr={sr:X4}"] = 1; + } + var gaps = M68kWinUaeCpuTesterConformanceTests.Move16ReferenceGaps(forms); + Assert.Equal(38, gaps.Count); + Assert.Contains("MOVE16/form=post-post/A=7:7/sr=201F", gaps); + Assert.DoesNotContain("MOVE16/form=post-post/A=0:1/sr=2000", gaps); + Assert.Contains("MOVE16/form=post-abs/A=7:abs/sr=2000", gaps); + Assert.Equal(384, M68kWinUaeCpuTesterConformanceTests.Move16ReferenceGaps(new Dictionary()).Count); + } +} diff --git a/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs b/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs index 1e906ee1..55d08e2d 100644 --- a/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs +++ b/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs @@ -100,11 +100,16 @@ private void RunQualifiedExceptionPreset(QualifiedExceptionPreset preset) var ignored = preset.ArithmeticFlagControls ? tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptIgnoredSr: true) : default; var ignoredAccepted = !preset.ArithmeticFlagControls || (ignored.Passed && ignored.ExecutedCases > 0 && tester.MaskedCases > 0); detected &= carryDetected && ignoredAccepted; + var memory = preset.MemoryControls ? tester.Run(path, family, fixture.CpuLevel, false, false, model, corruptMove16Memory: true) : default; + var memoryDetected = !preset.MemoryControls || (!memory.Passed && memory.ExecutedCases > 0 && + memory.Detail.Contains("Memory", StringComparison.Ordinal)); + detected &= memoryDetected; probes.Add(new(model.Id, family, detected, register.ExecutedCases, sr.ExecutedCases, frame.ExecutedCases, preset.ArithmeticFlagControls, carryDetected, carry.ExecutedCases, ignoredAccepted, ignored.ExecutedCases, frameRequired, frameDetected, pcRequired, pcDetected, pc.ExecutedCases, pcRequired ? pc.Detail : null, aliasRequired, aliasDetected, alias.ExecutedCases, - aliasRequired ? alias.Detail : null)); + aliasRequired ? alias.Detail : null, preset.MemoryControls, memoryDetected, memory.ExecutedCases, + preset.MemoryControls ? memory.Detail : null)); var result = tester.Run(path, family, fixture.CpuLevel, false, false, model, fixtureClassifier: preset.ClassifyForm is null ? null : (opcode, inputSr) => preset.ClassifyForm(opcode, inputSr, family), fixtureWordsClassifier: preset.ClassifyWords is null ? null : (opcode, extension, followingWord, inputSr) => preset.ClassifyWords(model, opcode, extension, followingWord, inputSr, family), @@ -112,13 +117,17 @@ private void RunQualifiedExceptionPreset(QualifiedExceptionPreset preset) var forms = new SortedDictionary(tester.FixtureForms.ToDictionary(x => x.Key, x => x.Value), StringComparer.Ordinal); var expectedForms = preset.FormCounts?.Invoke(model, family) ?? 0; var formDistributionMatches = preset.ExpectedForms is null || forms.SequenceEqual(preset.ExpectedForms(model, family).OrderBy(x => x.Key, StringComparer.Ordinal)); + var formDistributionSha256 = WinUaeFormDistributionSha256(forms); + if (preset.ExpectedFormHash is not null) + formDistributionMatches &= formDistributionSha256 == preset.ExpectedFormHash(model, family); var expectedMasked = preset.MaskedCounts?.Invoke(model, family) ?? (family.StartsWith("CHK2.", StringComparison.Ordinal) ? (uint)expected.Cases : 0); var passing = detected && result.Passed && result.ExecutedCases == expected.Cases && tester.FrameChecks == expected.Frames && tester.MaskedCases == expectedMasked && forms.Count == expectedForms && formDistributionMatches && ((preset.ClassifyForm is null && preset.ClassifyWords is null && preset.ClassifyRegisters is null) || forms.Values.Sum() == result.ExecutedCases); var detail = $"{result.Detail} Expected/actual callbacks={expected.Cases}/{result.ExecutedCases}, frames={expected.Frames}/{tester.FrameChecks}, masked={expectedMasked}/{tester.MaskedCases}, forms={expectedForms}/{forms.Count}, formDistributionMatches={formDistributionMatches}."; + var referenceGaps = preset.ReferenceGaps?.Invoke(forms) ?? []; rows.Add(new(model.Id, family, passing ? "passing" : tester.UnsupportedExecution ? "unsupported" : result.ExecutedCases == 0 ? "untested" : "mismatching", - result.ExecutedCases, tester.FrameChecks, tester.MaskedCases, detected, forms, detail)); + result.ExecutedCases, tester.FrameChecks, tester.MaskedCases, detected, forms, detail, formDistributionSha256, referenceGaps)); _output.WriteLine($"{model.Id}/{family}: {result.ExecutedCases} callbacks, {tester.FrameChecks} frames; controls={detected}."); } } @@ -138,6 +147,7 @@ private void RunQualifiedExceptionPreset(QualifiedExceptionPreset preset) untested = rows.Count(x => x.Status == "untested"), exceptionFrames = rows.Sum(x => (long)x.ExceptionFrames), maskedSrCases = rows.Sum(x => (long)x.MaskedSrCases), architecturalForms = rows.Sum(x => x.Forms.Count), + unavailableReferenceCombinations = rows.Sum(x => x.ReferenceGaps.Count), probes, rows }, new JsonSerializerOptions { WriteIndented = true })); Assert.True(rows.All(x => x.Status == "passing"), string.Join(Environment.NewLine, @@ -167,14 +177,25 @@ private sealed record QualifiedExceptionPreset(string Name, string EnvironmentKe Func>? ExpectedForms = null, QualifiedInputIdentity? InputIdentity = null, bool AllowFrameFreeFamilies = false, bool SavedPcControls = false, Func, string, string>? ClassifyRegisters = null, - Func? CompareAliasControls = null); + Func? CompareAliasControls = null, bool MemoryControls = false, + Func? ExpectedFormHash = null, + Func, IReadOnlyList>? ReferenceGaps = null); private sealed record QualifiedInputIdentity(string SourceName, string PatchName, string SourceHash, string PatchHash); private sealed record WinUaeQualifiedExceptionRow(string Model, string Family, string Status, int ExecutedCases, - uint ExceptionFrames, uint MaskedSrCases, bool Controls, SortedDictionary Forms, string Detail); + uint ExceptionFrames, uint MaskedSrCases, bool Controls, SortedDictionary Forms, string Detail, + string FormDistributionSha256, IReadOnlyList ReferenceGaps); private sealed record WinUaeQualifiedExceptionProbe(string Model, string Family, bool Detected, int RegisterCases, int SrCases, int FrameCases, bool ArithmeticFlagControls, bool CarryDetected, int CarryCases, bool IgnoredAccepted, int IgnoredCases, bool FrameControlRequired, bool FrameDetected, bool SavedPcControlRequired, bool SavedPcDetected, int SavedPcCases, string? SavedPcDetail, - bool CompareAliasControlRequired, bool CompareAliasDetected, int CompareAliasCases, string? CompareAliasDetail); + bool CompareAliasControlRequired, bool CompareAliasDetected, int CompareAliasCases, string? CompareAliasDetail, + bool MemoryControlRequired, bool MemoryDetected, int MemoryCases, string? MemoryDetail); + + internal static string WinUaeFormDistributionSha256(IReadOnlyDictionary forms) + { + var text = string.Concat(forms.OrderBy(x => x.Key, StringComparer.Ordinal) + .Select(x => $"{x.Key}={x.Value.ToString(System.Globalization.CultureInfo.InvariantCulture)}\n")); + return Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(text))).ToLowerInvariant(); + } } diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index e0cd6782..6913bb25 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1186,3 +1186,20 @@ requested failures remain failures. The broad Basic WinUAE selection requires discrepancies intentionally fail the command. See the [reference-only command](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#reference-only-audit-command) for reproducibility and caveats. + +## Canonical MOVE16 independent reference + +Prepare pinned WinUAE inputs with `prepare-copper68k-winuae.ps1 -Preset Move16`, +then run `test-copper68k-winuae-qualified-exceptions.ps1 -Preset Move16 +-InputDirectory -OutputDirectory `. The maintained gate +requires 22 executions without skips, 41,856 callbacks and 25,260 weighted form +keys on each of 040/060, with exact source/input identities and distribution +hashes. Native controls prove register, SR and destination-memory comparison. +Fixed encoding, fingerprint and gap controls run without native fixtures. + +Selected native cases pass, while `ReferenceGaps` explicitly retains 38 missing +supervisor-A7 combinations per model / 76 total. Ordinary synthetic MOVE16 +semantics include both stack modes; missing independent evidence remains +required. The original Basic failures stay visible and milestone 6 remains +incomplete. See the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#canonical-move16-reference-qualification--2026-10-06) +for the exact selection, integrity controls and reproduction commands. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index c3512f5e..0a44015b 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -7045,3 +7045,71 @@ JSON details or consumer/media inputs, reclassify the earlier failed full run, or close advanced frame/recovery and consolidation requirements. Production CPU, normal assemblies and packages are unchanged. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +### Canonical MOVE16 reference qualification — 2026-10-06 + +The maintained `Move16` preset independently executes all five documented +MOVE16 forms on 68040 and 68060. Preparation patches a copy of the pinned +WinUAE generator: set the canonical post-post extension before effective-address +construction, serialize valid address-register inputs before reference execution, +and enumerate all eight destination registers in sixteen low-nibble rounds. +Production decoding/execution and the original Basic corpus remain unchanged. +The architectural basis is [M68000PRM 4-125..127](https://www.nxp.com/docs/en/reference-manual/M68000PRM.pdf). + +```powershell +./scripts/prepare-copper68k-winuae.ps1 -Preset Move16 -GeneratorSource -RunnerSource -VcVars64 -OutputDirectory +./scripts/test-copper68k-winuae-qualified-exceptions.ps1 -Preset Move16 -InputDirectory -OutputDirectory +``` + +The pins remain generator `025b999239800357e95065fe5b9a15ea5b300fa7` and runner +`7a83745d6c6159bc74ab0471578ffc8bc244e66e`. Normalized copied generator SHA256 is +`b201915e70fa2c7cc7c87cbfef588eb910bf3e655ea02bbff0955e42bc4e2e55`; +patch SHA256 `c23761b715a1eb8abc01f4293710ffda73bb48f9a4867f1d0087c462eaa7ed12`. +The final input manifest SHA256 is +`244611bbd2dd6afcc81c192e8c9478211651a251da106887c73c2692da5b5846` and native bridge +SHA256 `819bbafbeba54f065a8eed396502dd98094cd90e7ea5aac6672cd5ae2a7bc1e0`. + +Fresh acceptance passes **22 executions / zero skips**, with **83,712 callbacks** +(41,856 per model), no exception frames or masked SR bits, and zero selected +mismatches/unsupported/untested rows. There are **25,260 weighted form keys per +model**, 50,520 summed across profiles. Each model's ordinal `key=weight\n` +distribution hashes to +`143e6c025a7451664853b10bfe94651dae28860263dad370a98ad6a33c77d236`. +Keys distinguish five forms, registers, aliases, line overlap, low address bits +and initial SR. Register, defined-SR and actual destination-memory corruption +are detected independently on both models. Fixed manual encoding examples, +rejected foreign/reserved encodings, weight redistribution and missing-reference +classification execute without optional inputs. + +The native selection covers all 64 post-post register pairs in user mode and +49 non-A7 pairs in supervisor mode, with CCR 0/31. Absolute forms cover all eight +address registers in user mode and seven non-A7 registers in supervisor mode. +An independent 384-combination form/register/SR inventory reports **38 absent +supervisor-A7 combinations per model / 76 total**: thirty post-post and eight +absolute combinations per model. Full missing keys remain in `ReferenceGaps`; +`unavailableReferenceCombinations` is distinct from passing selected callbacks. +These combinations require independent reference follow-up. Ordinary synthetic +MOVE16 coverage retains both stacks; it does not substitute for missing native +reference evidence. No incoming trace, faults, physical burst/bus ordering, +cache allocation or physical timing qualification is claimed. + +Evidence is under the restoration temporary root in +`audits/Move16GapQualifiedFinal` and `move16-cohort-verification.json`. Copied-input +controls in `Move16IntegrityFinal/controls.json` reject missing profiles, changed +source, foreign families and changed fixture bytes. An isolated copied test +changes only the expected weighted-map hash: all 83,712 callbacks and their +counts pass, while both profile gates fail `formDistributionMatches=False`. +All five controls execute exactly one failing native fact without skips and +retain the intended diagnostic. Earlier exploratory preparations and the first +incorrect integrity invocation are preserved separately and excluded from this +acceptance. + +The shared adapter also passes all nine existing qualified presets again: +105 executions, 116 directories, 1,525,856 callbacks and 858,001 frames, with no +skips. Synthetic MOVE16/breakpoint, input-validation and architectural-flag +controls pass 28 executions. Normal CPU/test assemblies retain their protected +identities. This test-only slice requires no CPU fix, consumer replay, package +release or old-test retirement. The original Basic MOVE16 failure and its full +54 non-passing rows remain retained. The supervisor-A7 reference gaps, complete +480-case advanced-040 inventory and all other retained milestone-6 requirements +remain open; milestone 6 stays **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index c107ee92..e3cd0e58 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2596,3 +2596,15 @@ qualification. Four command preflight controls reject invalid requests. See the No production CPU change, regression retirement or package release is included. Advanced restoration and remaining consolidation requirements remain open; milestone 6 is **in progress**, `roadmapComplete=false`. + +Canonical MOVE16 reference qualification now passes 83,712 callbacks on 040/060 +in 22 executions without skips. All five forms, all user-mode register pairs, +sixteen low-nibble rounds and explicit weighted architectural keys are checked. +Native register/SR/memory corruption and five input/weighted-map controls detect +their intended errors. The native reference explicitly lacks 76 supervisor-A7 +form/register/SR combinations; those remain required independent qualification. +The existing nine presets and 28 shared/synthetic controls pass again. Original +Basic failures, advanced restoration and all consolidation requirements remain +open. No production CPU change, regression retirement or package release is +included. See the [MOVE16 qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#canonical-move16-reference-qualification--2026-10-06). +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/prepare-copper68k-winuae.ps1 b/scripts/prepare-copper68k-winuae.ps1 index 7116c94d..1594203c 100644 --- a/scripts/prepare-copper68k-winuae.ps1 +++ b/scripts/prepare-copper68k-winuae.ps1 @@ -4,7 +4,7 @@ param( [Parameter(Mandatory)] [string] $GeneratorSource, [Parameter(Mandatory)] [string] $RunnerSource, [Parameter(Mandatory)] [string] $VcVars64, - [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop','Moves','Cas','Cas2','CacheEncodings')] [string] $Preset = 'Basic', + [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop','Moves','Cas','Cas2','CacheEncodings','Move16')] [string] $Preset = 'Basic', [string] $OutputDirectory = 'artifacts/winuae-model-inputs' ) $ErrorActionPreference = 'Stop' @@ -105,23 +105,27 @@ try { [IO.File]::WriteAllText($testerSource, (Patch-Once ([IO.File]::ReadAllText((Join-Path $generator 'cputest.cpp'))) $before $after)) [IO.File]::WriteAllText((Join-Path $output 'trace-priority.patch'), ([IO.File]::ReadAllText($patchPath)).Replace("`r`n", "`n")) } - if ($Preset -in @('LongArithmetic','Moves','Cas','Cas2')) { + if ($Preset -in @('LongArithmetic','Moves','Cas','Cas2','Move16')) { # M68000PM 4-94/98/136/140: reserved extension fields are zero; # Dh == Dl with a 64-bit multiply has undefined results. MOVES # 6-24/25/26 similarly fixes reserved fields and excludes undefined # same-An postincrement/predecrement store values. CAS 4-67 allows # only Du/Dc extension fields. Select # legal inputs before reference execution, never in the CPU bridge. - $patchName = switch ($Preset) {'Moves' {'moves-encodings.patch'} 'Cas' {'cas-encodings.patch'} 'Cas2' {'cas2-overlap-inputs.patch'} default {'long-arithmetic-encodings.patch'}} + $patchName = switch ($Preset) {'Moves' {'moves-encodings.patch'} 'Cas' {'cas-encodings.patch'} 'Cas2' {'cas2-overlap-inputs.patch'} 'Move16' {'move16-encodings.patch'} default {'long-arithmetic-encodings.patch'}} $patch = [IO.File]::ReadAllText((Join-Path $PSScriptRoot ('winuae/' + $patchName))).Replace("`r`n", "`n") $source = [IO.File]::ReadAllText((Join-Path $generator 'cputest.cpp')).Replace("`r`n", "`n") $hunks = @($patch -split "(?m)^@@`n" | Select-Object -Skip 1) - if ($hunks.Count -ne 1) { throw "Incorrect $Preset input patch hunk count" } - $lines = $hunks[0].TrimEnd("`n").Split("`n") - $before = (($lines | Where-Object { $_.StartsWith('-') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" - $after = (($lines | Where-Object { $_.StartsWith('+') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" - $testerSource = Join-Path $output $(switch ($Preset) {'Moves' {'cputest-moves.cpp'} 'Cas' {'cputest-cas.cpp'} 'Cas2' {'cputest-cas2.cpp'} default {'cputest-long-arithmetic.cpp'}}) - [IO.File]::WriteAllText($testerSource, (Patch-Once $source $before $after)) + $requiredInputHunks = if ($Preset -eq 'Move16') { 3 } else { 1 } + if ($hunks.Count -ne $requiredInputHunks) { throw "Incorrect $Preset input patch hunk count" } + foreach ($hunk in $hunks) { + $lines = $hunk.TrimEnd("`n").Split("`n") + $before = (($lines | Where-Object { $_.StartsWith('-') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" + $after = (($lines | Where-Object { $_.StartsWith('+') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" + $source = Patch-Once $source $before $after + } + $testerSource = Join-Path $output $(switch ($Preset) {'Moves' {'cputest-moves.cpp'} 'Cas' {'cputest-cas.cpp'} 'Cas2' {'cputest-cas2.cpp'} 'Move16' {'cputest-move16.cpp'} default {'cputest-long-arithmetic.cpp'}}) + [IO.File]::WriteAllText($testerSource, $source) [IO.File]::WriteAllText((Join-Path $output $patchName), $patch) } $cpp = @('cpudefs.cpp','cpuemu_90_test.cpp','cpuemu_91_test.cpp','cpuemu_92_test.cpp','cpuemu_93_test.cpp','cpuemu_94_test.cpp','cpuemu_95_test.cpp','cputbl_test.cpp',$testerSource,'cputest_support.cpp','disasm.cpp','fpp.cpp','fpp_softfloat.cpp','ini.cpp','newcpu_common.cpp','readcpu.cpp','softfloat/softfloat.cpp','softfloat/softfloat_decimal.cpp','softfloat/softfloat_fpsp.cpp') @@ -200,6 +204,10 @@ void M68KTester_destroy(M68KTesterContext* context) { if ($Preset -eq 'CacheEncodings') { $baseIni = $baseIni.Replace('[test=Basic]', '[test=CacheEncodings]').Replace('mode=all', 'mode=ILLEGAL').Replace('feature_sr_mask=0x0000', 'feature_sr_mask=0x2000') } + if ($Preset -eq 'Move16') { + # The pinned generator runs an initial round plus test_rounds repeats. + $baseIni = $baseIni.Replace('[test=Basic]', '[test=Move16]').Replace('mode=all', 'mode=MOVE16').Replace('feature_sr_mask=0x0000', 'feature_sr_mask=0x2000').Replace('test_rounds=1','test_rounds=15') + } foreach ($model in @( @{id='68000'; cpu='68000'; width=24}, @{id='68010'; cpu='68010'; width=24}, @{id='68EC020'; cpu='68020'; width=24}, @{id='68020'; cpu='68020'; width=32}, @@ -212,6 +220,7 @@ void M68KTester_destroy(M68KTesterContext* context) { if ($Preset -eq 'LowPowerStop' -and $model.id -ne '68060') { continue } if ($Preset -eq 'Moves' -and $model.id -eq '68000') { continue } if ($Preset -in @('Cas','Cas2') -and $model.id -in @('68000','68010')) { continue } + if ($Preset -eq 'Move16' -and $model.id -notin @('68040','68060')) { continue } $profileRoot = Join-Path $output $model.id New-Item -ItemType Directory -Path $profileRoot | Out-Null # Every invocation uses a fresh generator output path; stale data cannot fill a gap. @@ -258,6 +267,8 @@ void M68KTester_destroy(M68KTesterContext* context) { LowPowerStopPatchSha256=$(if ($Preset -eq 'LowPowerStop') {(Get-FileHash -LiteralPath (Join-Path $output 'lpstop-fetch-pc.patch')).Hash.ToLowerInvariant()} else {$null}) MovesSourceSha256=$(if ($Preset -eq 'Moves') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-moves.cpp')).Hash.ToLowerInvariant()} else {$null}) MovesPatchSha256=$(if ($Preset -eq 'Moves') {(Get-FileHash -LiteralPath (Join-Path $output 'moves-encodings.patch')).Hash.ToLowerInvariant()} else {$null}) + Move16SourceSha256=$(if ($Preset -eq 'Move16') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-move16.cpp')).Hash.ToLowerInvariant()} else {$null}) + Move16PatchSha256=$(if ($Preset -eq 'Move16') {(Get-FileHash -LiteralPath (Join-Path $output 'move16-encodings.patch')).Hash.ToLowerInvariant()} else {$null}) Cas2InputSourceSha256=$(if ($Preset -eq 'Cas2') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-cas2.cpp')).Hash.ToLowerInvariant()} else {$null}) CacheEncodingsSourceSha256=$(if ($Preset -eq 'CacheEncodings') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-cache-encodings.cpp')).Hash.ToLowerInvariant()} else {$null}) CacheEncodingsPatchSha256=$(if ($Preset -eq 'CacheEncodings') {(Get-FileHash -LiteralPath (Join-Path $output 'cache-encodings.patch')).Hash.ToLowerInvariant()} else {$null}) @@ -272,7 +283,7 @@ void M68KTester_destroy(M68KTesterContext* context) { Compiler=$compiler; GeneratorExecutableSha256=(Get-FileHash -LiteralPath (Join-Path $generator 'cputester.exe') -Algorithm SHA256).Hash.ToLowerInvariant() NativeSourceSha256=(Get-FileHash -LiteralPath (Join-Path $output 'winuae-native.c') -Algorithm SHA256).Hash.ToLowerInvariant() IntegerValidationSha256=(Get-FileHash -LiteralPath (Join-Path $output 'integer_validation.h') -Algorithm SHA256).Hash.ToLowerInvariant() - Seed="Pinned generator xorshift state initialized to 1 per test set; one $Preset round" + Seed=$(if ($Preset -eq 'Move16') {'Pinned generator xorshift state initialized to 1 per test set; sixteen explicit valid-address rounds'} else {"Pinned generator xorshift state initialized to 1 per test set; one $Preset round"}) } | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'manifest.json') -Encoding utf8 Write-Host "Prepared input manifest and native bridge: $output" } finally { diff --git a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 index 41d14f2e..9afaca9a 100644 --- a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 +++ b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 @@ -1,7 +1,7 @@ #requires -Version 7.0 [CmdletBinding()] param( - [ValidateSet('TrapBounds','Breakpoints','WordDivision','LowPowerStop','Moves','Cas','Cas2','CacheEncodings')] [string] $Preset = 'TrapBounds', + [ValidateSet('TrapBounds','Breakpoints','WordDivision','LowPowerStop','Moves','Cas','Cas2','CacheEncodings','Move16')] [string] $Preset = 'TrapBounds', [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $InputDirectory, [string] $NativeLibrary, [string] $OutputDirectory = 'artifacts/winuae-trap-bounds-audit', @@ -19,8 +19,17 @@ $build = if ($ArtifactsPath) { [IO.Path]::GetFullPath($ArtifactsPath, $repo) } e $key = switch ($Preset) {'TrapBounds' {'TRAP_BOUNDS'} 'Breakpoints' {'BREAKPOINT'} 'WordDivision' {'WORD_DIVISION'} 'LowPowerStop' {'LPSTOP'} 'Moves' {'MOVES'} 'Cas' {'CAS'} 'Cas2' {'CAS2'} 'CacheEncodings' {'CACHE_ENCODINGS'}} $testName = switch ($Preset) {'TrapBounds' {'WinUaeTrapAndBoundsAcrossAdvancedModelsWhenEnabled'} 'Breakpoints' {'WinUaeBreakpointExceptionsAcrossSelectedModelsWhenEnabled'} 'WordDivision' {'WinUaeWordDivisionAcrossSelectedModelsWhenEnabled'} 'LowPowerStop' {'WinUaeLowPowerStopExceptionsWhenEnabled'} 'Moves' {'WinUaeMovesAcrossSelectedModelsWhenEnabled'} 'Cas' {'WinUaeCasAcrossAdvancedModelsWhenEnabled'} 'Cas2' {'WinUaeCas2AcrossAdvancedModelsWhenEnabled'} 'CacheEncodings' {'WinUaeCacheScopeZeroAcrossSelectedModelsWhenEnabled'}} $reportName = switch ($Preset) {'TrapBounds' {'winuae-trap-bounds-audit.json'} 'Breakpoints' {'winuae-breakpoint-audit.json'} 'WordDivision' {'winuae-word-division-audit.json'} 'LowPowerStop' {'winuae-lpstop-audit.json'} 'Moves' {'winuae-moves-audit.json'} 'Cas' {'winuae-cas-audit.json'} 'Cas2' {'winuae-cas2-audit.json'} 'CacheEncodings' {'winuae-cache-encodings-audit.json'}} +if ($Preset -eq 'Move16') { + $key = 'MOVE16' + $testName = 'WinUaeMove16AcrossSupportedModelsWhenEnabled' + $reportName = 'winuae-move16-audit.json' +} $filter = "FullyQualifiedName~$testName" $expectedTests = 1 +if ($Preset -eq 'Move16') { + $filter += '|FullyQualifiedName~M68kWinUaeMove16EncodingTests' + $expectedTests = 22 +} if ($Preset -eq 'WordDivision') { $filter += '|FullyQualifiedName~M68kWinUaeWordDivisionEncodingTests' $expectedTests = 15 diff --git a/scripts/winuae/move16-encodings.patch b/scripts/winuae/move16-encodings.patch new file mode 100644 index 00000000..a6c52e53 --- /dev/null +++ b/scripts/winuae/move16-encodings.patch @@ -0,0 +1,36 @@ +--- a/gencpu/cputest.cpp ++++ b/gencpu/cputest.cpp +@@ + if (dp->mnemo == i_MOVE16) { + if (opcode & 0x20) { +- uae_u16 v = 0; +- v |= imm_special << 12; ++ // M68000PRM 4-127: bit 15 fixed one, bits 11..0 zero. ++ // Canonicalize the generated candidate before EA construction ++ // and reference execution, preserving register/random selection. ++ uae_u16 v = 0x8000; ++ v |= (imm_special & 7) << 12; + put_word_test(pc, v); +@@ + for (int i = 0; i < MAX_REGISTERS; i++) { ++ // Set explicit valid MOVE16 inputs before serializing initial ++ // registers. Sixteen rounds cover each low address nibble; ++ // paired An share a line, and post-post selects every Ay. ++ if (lookup->mnemo == i_MOVE16 && i >= 8 && i < 16) { ++ int a = i - 8; ++ cur_registers[i] = test_memory_start + 0x1000 + (a / 2) * 0x40 + ((feature_test_rounds - rounds + a) & 15); ++ } + dst = store_reg(dst, CT_DREG + i, 0, cur_registers[i], -1); + regs.regs[i] = cur_registers[i]; +@@ + uae_u32 instructionendpc = pc; ++ // The post-post destination is an instruction extension, ++ // not a dp effective address. Enumerate all eight Ay values ++ // for every Ax in each serialized valid-address round. ++ if (dp->mnemo == i_MOVE16 && (opc & 0xfff8) == 0xf620) { ++ isconstant_src = 8; ++ isconstant_dst = 0; ++ constant_loops = 8 - imm_special; ++ } + + if (isconstant_src < 0 || isconstant_dst < 0) { From 261fbab7d1078faa0f7723188238dbfc0d38feea Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 23:15:45 +0300 Subject: [PATCH 077/211] Complete supervisor A7 MOVE16 reference qualification --- .../M68kWinUaeCpuTesterConformanceTests.cs | 13 ++++ Copper68k.Tests/M68kWinUaeMove16AuditTests.cs | 51 +++++++++++++-- .../M68kWinUaeTrapBoundsAuditTests.cs | 7 ++- Copper68k.Tests/Synthetic/README.md | 25 ++++---- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 63 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 12 ++++ scripts/prepare-copper68k-winuae.ps1 | 2 +- ...-copper68k-winuae-qualified-exceptions.ps1 | 2 +- scripts/winuae/move16-encodings.patch | 14 +++++ 9 files changed, 167 insertions(+), 22 deletions(-) diff --git a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs index 31f6c93e..7771b29c 100644 --- a/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs +++ b/Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs @@ -552,6 +552,17 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt } ApplyRegisters(cpu, registers, _cpuLevel); + // Native corpus serialization restores USP into slot A7 and + // cannot independently compare the completed active SSP value. + // Keep an architectural assertion before copying output registers. + uint[]? move16ExpectedAddresses = null; + if (_integerFamily == "MOVE16" && _fixtureRegisterClassifier is not null) + { + var input = (uint[])registers.Regs.Clone(); + input[15] = cpu.State.A[7]; + move16ExpectedAddresses = Move16ExpectedAddressRegisters(bus.ReadHostWord(registers.Pc), + bus.ReadHostWord(registers.Pc + 2), input); + } uint? corruptAliasValue = null; var corruptAliasRegister = 0; var corruptAliasWidth = 0; @@ -648,6 +659,8 @@ private void RunCopper68k(IntPtr userData, IntPtr contextPtr, IntPtr registersPt RestoreUnobservableFpuRead(cpu.State, bus, registers); } + if (move16ExpectedAddresses is not null) + Assert.Equal(move16ExpectedAddresses, cpu.State.A); CopyRegisters(cpu.State, bus, _cpuLevel, ref registers); if (corruptAliasValue.HasValue && cpu.State.LastExceptionVector < 0 && (registers.Sr & 4) == 0) registers.Regs[corruptAliasRegister] = corruptAliasWidth == 2 diff --git a/Copper68k.Tests/M68kWinUaeMove16AuditTests.cs b/Copper68k.Tests/M68kWinUaeMove16AuditTests.cs index 17296b5f..2281680e 100644 --- a/Copper68k.Tests/M68kWinUaeMove16AuditTests.cs +++ b/Copper68k.Tests/M68kWinUaeMove16AuditTests.cs @@ -8,13 +8,13 @@ public sealed partial class M68kWinUaeCpuTesterConformanceTests [EnvironmentFact("COPPER68K_RUN_WINUAE_MOVE16_AUDIT", "audit canonical WinUAE MOVE16 encodings and line transfers")] public void WinUaeMove16AcrossSupportedModelsWhenEnabled() => RunQualifiedExceptionPreset(new( "Move16", "MOVE16", "cputest-move16.cpp", "move16-encodings.patch", "Move16", - "b201915e70fa2c7cc7c87cbfef588eb910bf3e655ea02bbff0955e42bc4e2e55", "c23761b715a1eb8abc01f4293710ffda73bb48f9a4867f1d0087c462eaa7ed12", + "04ecd41c994ccdcb06a6dc08607f54d2fa5b219e8b821be8760c2022aea990a4", "fe407db40dca22a0258a0a689133fa581588117d22ca631cb2c8e52a15932caa", "winuae-move16-audit.json", ["68040", "68060"], ["68040", "68060"], - _ => ["MOVE16"], (_, _) => (41856, 0), - "M68000PRM 4-125..127: five canonical forms, aligned 16-byte line copy, low address bits retained in postincrements, same register increments once, CCR unchanged. Copied input generator fixes extension bits before EA construction, serializes explicit valid An addresses, and enumerates every Ay in sixteen low-nibble rounds. Original Basic inputs/failures remain unchanged. CCR 0/31; all 64 post-post register pairs in user mode and 49 non-A7 pairs in supervisor mode are independently executed. Thirty supervisor A7 post-post pair/CCR and eight absolute-form A7/CCR combinations per model are absent from this reference selection and remain required reference follow-up; their ordinary synthetic semantics are retained. Four absolute forms include user/supervisor. No incoming trace/faults, cache allocation, physical burst/bus order or timing qualification. Unavailable models retain synthetic Line-F coverage.", - FormCounts: (_, _) => 25260, ClassifyRegisters: QualifyMove16Encoding, - ExpectedFormHash: (_, _) => "143e6c025a7451664853b10bfe94651dae28860263dad370a98ad6a33c77d236", - AllowFrameFreeFamilies: true, MemoryControls: true, ReferenceGaps: Move16ReferenceGaps)); + _ => ["MOVE16"], (_, _) => (43856, 0), + "M68000PRM 4-125..127: five canonical forms, aligned 16-byte line copy, low address bits retained in postincrements, same register increments once, CCR unchanged. Copied input generator fixes extension bits before EA construction, serializes explicit valid An addresses, and enumerates every Ay in sixteen low-nibble rounds. Original Basic inputs/failures remain unchanged. CCR 0/31; all 64 post-post register pairs and all eight registers in four absolute forms execute in user/supervisor mode. Copied generator allows selected MOVE16 reads/writes of its supervisor-stack fixture and retains instructions that modify supervisor A7. All 384 form/register/SR combinations are mandatory per model. The native format hides final active SSP, so an independent architectural A-register check precedes output conversion. Supervisor A7 starts aligned at the fixed native SSP; all sixteen user A7 low bits remain covered. No incoming trace/faults, cache allocation, physical burst/bus order or timing qualification. Unavailable models retain synthetic Line-F coverage.", + FormCounts: (_, _) => 25674, ClassifyRegisters: QualifyMove16Encoding, + ExpectedFormHash: (_, _) => "560bf286786ac671980a481728c812f977cd08c376ced187da599fd1238487c3", + AllowFrameFreeFamilies: true, MemoryControls: true, ReferenceGaps: Move16ReferenceGaps, RequireCompleteReferenceCoverage: true)); internal static IReadOnlyList Move16ReferenceGaps(IReadOnlyDictionary forms) { @@ -38,6 +38,20 @@ internal static IReadOnlyList Move16ReferenceGaps(IReadOnlyDictionary input) + { + var result = input.Skip(8).ToArray(); + var form = (opcode >> 3) & 7; + var primary = opcode & 7; + if (form is 0 or 1 or 4) result[primary] = unchecked(result[primary] + 16); + if (form == 4) + { + var secondary = (extension >> 12) & 7; + if (secondary != primary) result[secondary] = unchecked(result[secondary] + 16); + } + return result; + } + // Decode only documented immutable fields and input registers; never call // production instruction decoding or effective-address helpers. internal static (uint Source, uint Destination) Move16Operands(ModelSpec model, ushort opcode, @@ -73,6 +87,31 @@ public sealed class M68kWinUaeMove16EncodingTests { private static uint[] Registers() => Enumerable.Range(0, 16).Select(x => 0x1000u + (uint)(x * 0x101)).ToArray(); + [Fact] + public void SupervisorA7PostincrementPreservesLowBitsAndIncrementsOnceForAlias() + { + var input = Registers(); + input[15] = 0xfffffff7; + var expected = input.Skip(8).ToArray(); + expected[7] = 7; + Assert.Equal(expected, M68kWinUaeCpuTesterConformanceTests.Move16ExpectedAddressRegisters(0xf627, 0xf000, input)); + expected[0] += 16; + Assert.Equal(expected, M68kWinUaeCpuTesterConformanceTests.Move16ExpectedAddressRegisters(0xf620, 0xf000, input)); + } + + [Fact] + public void AbsoluteFormsIncrementOnlyTheirDocumentedRegister() + { + var input = Registers(); + var preserved = input.Skip(8).ToArray(); + Assert.Equal(preserved, M68kWinUaeCpuTesterConformanceTests.Move16ExpectedAddressRegisters(0xf617, 0, input)); + Assert.Equal(preserved, M68kWinUaeCpuTesterConformanceTests.Move16ExpectedAddressRegisters(0xf61f, 0, input)); + var changed = (uint[])preserved.Clone(); + changed[7] += 16; + Assert.Equal(changed, M68kWinUaeCpuTesterConformanceTests.Move16ExpectedAddressRegisters(0xf607, 0, input)); + Assert.Equal(changed, M68kWinUaeCpuTesterConformanceTests.Move16ExpectedAddressRegisters(0xf60f, 0, input)); + } + [Theory] [InlineData("68040", 0xf600, 0, 0x221f, 0, "MOVE16/form=post-abs/A=0:abs/sameRegister=False/offset=8:15/sameLine=False/sr=0000")] [InlineData("68060", 0xf609, 0, 0x221f, 0x201f, "MOVE16/form=abs-post/A=1:abs/sameRegister=False/offset=15:9/sameLine=False/sr=201F")] diff --git a/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs b/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs index 55d08e2d..5cfb0936 100644 --- a/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs +++ b/Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs @@ -121,11 +121,11 @@ private void RunQualifiedExceptionPreset(QualifiedExceptionPreset preset) if (preset.ExpectedFormHash is not null) formDistributionMatches &= formDistributionSha256 == preset.ExpectedFormHash(model, family); var expectedMasked = preset.MaskedCounts?.Invoke(model, family) ?? (family.StartsWith("CHK2.", StringComparison.Ordinal) ? (uint)expected.Cases : 0); + var referenceGaps = preset.ReferenceGaps?.Invoke(forms) ?? []; var passing = detected && result.Passed && result.ExecutedCases == expected.Cases && tester.FrameChecks == expected.Frames && tester.MaskedCases == expectedMasked && - forms.Count == expectedForms && formDistributionMatches && ((preset.ClassifyForm is null && preset.ClassifyWords is null && preset.ClassifyRegisters is null) || forms.Values.Sum() == result.ExecutedCases); + forms.Count == expectedForms && formDistributionMatches && (!preset.RequireCompleteReferenceCoverage || referenceGaps.Count == 0) && ((preset.ClassifyForm is null && preset.ClassifyWords is null && preset.ClassifyRegisters is null) || forms.Values.Sum() == result.ExecutedCases); var detail = $"{result.Detail} Expected/actual callbacks={expected.Cases}/{result.ExecutedCases}, frames={expected.Frames}/{tester.FrameChecks}, masked={expectedMasked}/{tester.MaskedCases}, forms={expectedForms}/{forms.Count}, formDistributionMatches={formDistributionMatches}."; - var referenceGaps = preset.ReferenceGaps?.Invoke(forms) ?? []; rows.Add(new(model.Id, family, passing ? "passing" : tester.UnsupportedExecution ? "unsupported" : result.ExecutedCases == 0 ? "untested" : "mismatching", result.ExecutedCases, tester.FrameChecks, tester.MaskedCases, detected, forms, detail, formDistributionSha256, referenceGaps)); _output.WriteLine($"{model.Id}/{family}: {result.ExecutedCases} callbacks, {tester.FrameChecks} frames; controls={detected}."); @@ -179,7 +179,8 @@ private sealed record QualifiedExceptionPreset(string Name, string EnvironmentKe Func, string, string>? ClassifyRegisters = null, Func? CompareAliasControls = null, bool MemoryControls = false, Func? ExpectedFormHash = null, - Func, IReadOnlyList>? ReferenceGaps = null); + Func, IReadOnlyList>? ReferenceGaps = null, + bool RequireCompleteReferenceCoverage = false); private sealed record QualifiedInputIdentity(string SourceName, string PatchName, string SourceHash, string PatchHash); private sealed record WinUaeQualifiedExceptionRow(string Model, string Family, string Status, int ExecutedCases, uint ExceptionFrames, uint MaskedSrCases, bool Controls, SortedDictionary Forms, string Detail, diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 6913bb25..515700d2 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1191,15 +1191,18 @@ for reproducibility and caveats. Prepare pinned WinUAE inputs with `prepare-copper68k-winuae.ps1 -Preset Move16`, then run `test-copper68k-winuae-qualified-exceptions.ps1 -Preset Move16 --InputDirectory -OutputDirectory `. The maintained gate -requires 22 executions without skips, 41,856 callbacks and 25,260 weighted form -keys on each of 040/060, with exact source/input identities and distribution -hashes. Native controls prove register, SR and destination-memory comparison. -Fixed encoding, fingerprint and gap controls run without native fixtures. - -Selected native cases pass, while `ReferenceGaps` explicitly retains 38 missing -supervisor-A7 combinations per model / 76 total. Ordinary synthetic MOVE16 -semantics include both stack modes; missing independent evidence remains -required. The original Basic failures stay visible and milestone 6 remains -incomplete. See the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#canonical-move16-reference-qualification--2026-10-06) +-InputDirectory -OutputDirectory `. The gate requires +24 executions without skips, 43,856 callbacks / 25,674 weighted form keys per +040/060 profile, exact source/input/distribution identities and all 384 +form/register/SR combinations per profile with zero gaps. Native controls prove +register, SR and destination-memory comparison. Fixed encoding, fingerprint, +active-A7 effects and gap controls run without native fixtures. + +Copied supervisor-stack generator restrictions are corrected. An independent +architectural address-register assertion checks final active SSP before native +output conversion; all 76 prior missing combinations now execute. Supervisor +A7 starts at the fixed aligned native SSP; synthetic low-bit/stack coverage and +fault/cache/trace requirements remain separate. Original Basic failures remain +visible and milestone 6 remains incomplete. See the +[supervisor-A7 qualification](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#supervisor-a7-move16-reference-completion--2026-10-06) for the exact selection, integrity controls and reproduction commands. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 0a44015b..4a761680 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -7113,3 +7113,66 @@ release or old-test retirement. The original Basic MOVE16 failure and its full 54 non-passing rows remain retained. The supervisor-A7 reference gaps, complete 480-case advanced-040 inventory and all other retained milestone-6 requirements remain open; milestone 6 stays **in progress**, `roadmapComplete=false`. + +### Supervisor-A7 MOVE16 reference completion — 2026-10-06 + +The previous 76 missing form/register/SR combinations are now independently +executed. The pinned generator excluded writes to its reserved supervisor-stack +area and discarded instructions that changed supervisor A7. Two additional +input-generator patch hunks permit selected MOVE16 stack-line accesses, +including subsequent write-history validation, and retain MOVE16 A7 changes. +No production CPU or original Basic/reference source is modified. + +The native output format restores USP into register slot A7 and cannot compare +the completed active SSP. The adapter therefore independently derives all eight +expected architectural address registers from documented MOVE16 fields and the +initial active stack, then asserts them before converting output registers. +Same-register post-post increments once; non-postincrement forms preserve their +address registers. Fixed controls cover A7 aliasing, wraparound and low-bit +preservation. A copied adapter deliberately flips active supervisor A7 before +conversion; both models fail the new assertion at position seven. No reference +expected result is changed or masked to obtain agreement. + +The maintained `Move16` command now requires **24 executions without skips**, +**43,856 callbacks / 25,674 weighted form keys per model**: **87,712 callbacks / +51,348 summed profile keys**. All five forms, all 64 post-post register pairs, +all eight registers in the four absolute forms, CCR 0/31 and both user/supervisor +modes execute. An independent inventory requires **all 384 form/register/SR +combinations per model with zero missing combinations**; missing required +reference coverage now fails the selected gate. Sixteen user-register low-bit +rounds remain; the native supervisor A7 starts at its fixed aligned SSP. This +closes the recorded form/register/SR gap, without claiming every supervisor +low-bit pairing, trace/fault, cache or physical timing behavior is qualified. + +The final normalized copied source SHA256 is +`04ecd41c994ccdcb06a6dc08607f54d2fa5b219e8b821be8760c2022aea990a4`; +patch SHA256 `fe407db40dca22a0258a0a689133fa581588117d22ca631cb2c8e52a15932caa`. +Input manifest SHA256 is +`7797be5f1621a24e1e8e27cd655bfc44c27f78ad78c7360f6366ad34610f3eac`; +native SHA256 `1877ea62f605efd2701f13653bfc862d84632e1009849507ee9b857958164eae`. +Each ordinal weighted form distribution hashes to +`560bf286786ac671980a481728c812f977cd08c376ced187da599fd1238487c3`. +Generator/runner pins and the existing preparation/audit commands are unchanged. +Native register, SR and actual destination-memory corruption are detected on +both models. Five copied-input/weighted-map integrity controls and the active +supervisor-stack corruption control reject their precise intended errors. + +Fresh evidence under the restoration temporary root is +`inputs/Move16SupervisorSerialization`, `audits/Move16SupervisorQualifiedFinal`, +`Move16SupervisorIntegrity/controls.json`, +`Move16SupervisorStackControl/control.json` and +`move16-supervisor-cohort-verification.json`. The initial stack-generator +attempt failed because inactive write-history validation still rejected its +stack region; its outputs remain excluded. The exploratory successful execution +retained the old count/hash expectations and correctly failed the gate before +final coverage identities were fixed. Neither failed run is reclassified green. +All nine existing qualified presets pass again (105 executions / 116 directories / +1,525,856 callbacks / 858,001 frames), together with 28 shared/synthetic controls. +Normal CPU/test outputs remain unchanged. No consumer replay, package release +or old-regression retirement is performed in this test-only slice. + +The original Basic corpus and its 54 non-passing rows remain unchanged; its +noncanonical MOVE16 stop is preserved separately from this legal-input audit. +The advanced 480-case 040 inventory, other-model restoration, software reference +disagreements and all remaining consolidation requirements stay open. +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index e3cd0e58..f0146c26 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2608,3 +2608,15 @@ Basic failures, advanced restoration and all consolidation requirements remain open. No production CPU change, regression retirement or package release is included. See the [MOVE16 qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#canonical-move16-reference-qualification--2026-10-06). Milestone 6 remains **in progress**, `roadmapComplete=false`. + +The subsequent supervisor-A7 MOVE16 qualification closes all 76 previously +missing form/register/SR combinations. Copied generator restrictions are fixed; +independent active A-register assertions cover the native format's final SSP +blind spot. The maintained gate now passes 24 executions, 87,712 callbacks and +51,348 summed profile form keys, requiring all 384 form/register/SR combinations +on each of 040/060 with zero gaps. Six corruption/integrity controls reject their +intended errors. All nine existing presets and 28 shared controls pass again. +No production CPU fix, regression retirement or package release is included. +Original Basic failures and the full advanced restoration/consolidation scope +remain required. See the [supervisor-A7 record](COPPER68K_REFERENCE_QUALIFICATION.md#supervisor-a7-move16-reference-completion--2026-10-06). +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/prepare-copper68k-winuae.ps1 b/scripts/prepare-copper68k-winuae.ps1 index 1594203c..c4a03d9c 100644 --- a/scripts/prepare-copper68k-winuae.ps1 +++ b/scripts/prepare-copper68k-winuae.ps1 @@ -116,7 +116,7 @@ try { $patch = [IO.File]::ReadAllText((Join-Path $PSScriptRoot ('winuae/' + $patchName))).Replace("`r`n", "`n") $source = [IO.File]::ReadAllText((Join-Path $generator 'cputest.cpp')).Replace("`r`n", "`n") $hunks = @($patch -split "(?m)^@@`n" | Select-Object -Skip 1) - $requiredInputHunks = if ($Preset -eq 'Move16') { 3 } else { 1 } + $requiredInputHunks = if ($Preset -eq 'Move16') { 5 } else { 1 } if ($hunks.Count -ne $requiredInputHunks) { throw "Incorrect $Preset input patch hunk count" } foreach ($hunk in $hunks) { $lines = $hunk.TrimEnd("`n").Split("`n") diff --git a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 index 9afaca9a..b44f6fd8 100644 --- a/scripts/test-copper68k-winuae-qualified-exceptions.ps1 +++ b/scripts/test-copper68k-winuae-qualified-exceptions.ps1 @@ -28,7 +28,7 @@ $filter = "FullyQualifiedName~$testName" $expectedTests = 1 if ($Preset -eq 'Move16') { $filter += '|FullyQualifiedName~M68kWinUaeMove16EncodingTests' - $expectedTests = 22 + $expectedTests = 24 } if ($Preset -eq 'WordDivision') { $filter += '|FullyQualifiedName~M68kWinUaeWordDivisionEncodingTests' diff --git a/scripts/winuae/move16-encodings.patch b/scripts/winuae/move16-encodings.patch index a6c52e53..c0805b6f 100644 --- a/scripts/winuae/move16-encodings.patch +++ b/scripts/winuae/move16-encodings.patch @@ -34,3 +34,17 @@ + } if (isconstant_src < 0 || isconstant_dst < 0) { +@@ + if (addr >= super_stack_memory - RESERVED_SUPERSTACK && addr + size < super_stack_memory) { ++ // MOVE16 explicitly tests supervisor A7 line reads and writes. ++ // The selected reference has no trace/exception stack traffic. ++ if ((testing_active_opcode & 0xffc0) == 0xf600) { ++ test_memory_accessed = 1; ++ return 1; ++ } + // allow only instructions that have to access super stack, for example RTE +@@ + // Supervisor mode and A7 was modified and not RTE+stack mode: skip this test round. +- if (s && regs.regs[15] != a7 && (dp->mnemo != i_RTE || feature_usp < 3)) { ++ if (s && regs.regs[15] != a7 && dp->mnemo != i_MOVE16 && (dp->mnemo != i_RTE || feature_usp < 3)) { + // but not if RTE From e106e6a30d49f39c7d4d7915303740fc430ae4c8 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Tue, 6 Oct 2026 23:31:25 +0300 Subject: [PATCH 078/211] Expose 68040 operand read fault frame qualification gap --- Copper68k.Tests/Synthetic/README.md | 16 ++ ...ticM68040OperandReadFaultDiscoveryTests.cs | 139 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 75 ++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 13 ++ ...t-copper68k-040-operand-read-discovery.ps1 | 70 +++++++++ 5 files changed, 313 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040OperandReadFaultDiscoveryTests.cs create mode 100644 scripts/test-copper68k-040-operand-read-discovery.ps1 diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 515700d2..0e4eb92a 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1206,3 +1206,19 @@ fault/cache/trace requirements remain separate. Original Basic failures remain visible and milestone 6 remains incomplete. See the [supervisor-A7 qualification](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#supervisor-a7-move16-reference-completion--2026-10-06) for the exact selection, integrity controls and reproduction commands. + +## Actual 040 operand-read fault discovery + +Run `test-copper68k-040-operand-read-discovery.ps1 -OutputDirectory ` +for the self-contained legal read-instruction witnesses and scalar/batch fault +entry. Each route requires 61,440 cases / 1,920 combinations / all 32 CCRs, with +exact source/report/assembly identities and independent key/weight expansion. +`-ValidateReportsOnly` checks retained frozen evidence without CPU execution. +Both modes currently fail 122,880 architectural frame mismatches; the sixteen +literal normal-execution witnesses pass. Missing/empty/changed evidence fails. + +The processor currently delivers format 0 for operand reads where the manual +requires format 7. Actual recovery, broader addressing/MOVEM phases and all +remaining fault protocols are required work. The discovery remains outside +promoted ordinary-CI batch counts; no inventory gap is removed. See the +[qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#actual-040-operand-read-fault-discovery--2026-10-06). diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadFaultDiscoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadFaultDiscoveryTests.cs new file mode 100644 index 00000000..839e7e53 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadFaultDiscoveryTests.cs @@ -0,0 +1,139 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 8.4.6/8.4.6.2/8.4.6.7, not scalar/batch agreement. +// This failing discovery remains outside promoted ordinary-CI coverage. +public sealed class SyntheticM68040OperandReadFaultDiscoveryTests(ITestOutputHelper output) +{ + internal const string Enable = "COPPER68K_RUN_040_OPERAND_READ_DISCOVERY"; + private const uint Operand = 0x4200, Handler = 0xa000; + private static readonly (string Family, int Width, ushort Opcode, bool Movem)[] Instructions = + [ + ("MOVE", 1, 0x1010, false), ("MOVE", 2, 0x3010, false), ("MOVE", 4, 0x2010, false), + ("MOVEA", 2, 0x3050, false), ("MOVEA", 4, 0x2050, false), + ("ADD", 1, 0xd010, false), ("ADD", 2, 0xd050, false), ("ADD", 4, 0xd090, false), + ("CMP", 1, 0xb010, false), ("CMP", 2, 0xb050, false), ("CMP", 4, 0xb090, false), + ("TST", 1, 0x4a10, false), ("TST", 2, 0x4a50, false), ("TST", 4, 0x4a90, false), + ("MOVEM", 2, 0x4c90, true), ("MOVEM", 4, 0x4cd0, true) + ]; + + [EnvironmentFact(Enable, "qualify actual 040 operand-read access frames"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarReadFaultsRequireFormat7() => Audit(false); + + [EnvironmentFact(Enable, "qualify actual 040 operand-read access frames"), Trait("Suite", "ReferenceDiscovery")] + public void BatchReadFaultsRequireFormat7() => Audit(true); + + [Fact, Trait("Suite", "Synthetic")] + public void FixedManualReadInstructionWitnessesValidateEncodings() + { + // Literal results for source bytes 80 81 82 83 and initial D0 A55A0022, + // CCR 1F. These are independent of production decoder/arithmetic helpers. + uint[] expectedData = [0xa55a0080, 0xa55a8081, 0x80818283, 0xa55a0022, 0xa55a0022, + 0xa55a00a2, 0xa55a80a3, 0x25db82a5, 0xa55a0022, 0xa55a0022, 0xa55a0022, + 0xa55a0022, 0xa55a0022, 0xa55a0022, 0xffff8081, 0x80818283]; + ushort[] expectedStatus = [0x2718, 0x2718, 0x2718, 0x271f, 0x271f, + 0x2708, 0x2708, 0x2713, 0x271b, 0x2711, 0x2710, 0x2718, 0x2718, 0x2718, 0x271f, 0x271f]; + for (var index = 0; index < Instructions.Length; index++) + { + var instruction = Instructions[index]; + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); + m.Reset(31); m.Core.State.A[0] = Operand; + m.InitializePhysical(Operand, 0x80818283, 4); + m.InitializePhysical(SyntheticMachine.Code, instruction.Opcode, 2); + m.InitializePhysical(SyntheticMachine.Code + 2, instruction.Movem ? 1u : 0x4e71u, 2); + m.InitializePhysical(SyntheticMachine.Code + 4, 0x4e71, 2); + m.Start(); + var e = ArchitecturalExpectation.Capture(m); + e.D[0] = expectedData[index]; e.Sr = expectedStatus[index]; + e.Pc += instruction.Movem ? 4u : 2u; + if (instruction.Family == "MOVEA") e.A[0] = instruction.Width == 2 ? 0xffff8081u : 0x80818283u; + m.Core.ExecuteInstruction(); + Assert.True(e.Verify(m) is null, $"Manual read encoding {instruction.Opcode:X4}: {e.Verify(m)}"); + } + } + + private void Audit(bool batch) + { + var report = new CoverageBatch("68040", "operand-read-fault-discovery-" + (batch ? "batch" : "scalar")); + foreach (var instruction in Instructions) + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var trace in new ushort[] { 0, 0x8000, 0x4000 }) + for (var ccr = 0; ccr < 32; ccr++) + for (var lane = 0; lane < 4; lane++) + for (var faultByte = 0; faultByte < instruction.Width; faultByte++) + Case(report, batch, instruction, bank, trace, ccr, lane, faultByte); + report.Complete(output); + } + + private static void Case(CoverageBatch report, bool batch, + (string Family, int Width, ushort Opcode, bool Movem) instruction, + string bank, ushort trace, int ccr, int lane, int faultByte) + { + var id = $"68040/{instruction.Family}/operand-read-fault/size={instruction.Width}/bank={bank}/T={trace:X4}/lane={lane}/byte={faultByte}/op={instruction.Opcode:X4}/ccr={ccr:X2}"; + try + { + var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + m.Reset(ccr); + m.Core.State.A[0] = Operand + (uint)lane; + m.InitializePhysical(Operand - 4, 0xa55aa55a, 4); + for (uint n = 0; n < 12; n++) m.InitializePhysical(Operand + n, 0x80 + n, 1); + m.InitializePhysical(SyntheticMachine.Code, instruction.Opcode, 2); + m.InitializePhysical(SyntheticMachine.Code + 2, instruction.Movem ? 0x0001u : 0x4e71u, 2); + m.InitializePhysical(SyntheticMachine.Code + 4, 0x4e71, 2); + m.InitializePhysical(8, Handler, 4); + m.InitializePhysical(Handler, 0x4e73, 2); + m.InitializePhysical(Handler + 2, 0x4e71, 2); + foreach (var top in new uint[] { 0x4700, 0x7400 }) + for (var n = -64; n < 4; n++) m.InitializePhysical(unchecked(top + (uint)n), (uint)(n ^ 0x5a), 1); + m.Start(); + var sr = (ushort)(0x0700 | M68040StackFixture.Status(bank, trace, ccr)); + m.Core.State.SetUserStackPointer(0x7800); + m.Core.State.SetInterruptStackPointer(0x4700); + m.Core.State.SetMasterStackPointer(0x7400); + m.Core.State.StatusRegister = sr; + var e = ArchitecturalExpectation.Capture(m); + var pointers = new Dictionary { ["user"] = 0x7800, ["ISP"] = 0x4700, ["MSP"] = 0x7400 }; + var exceptionBank = M68040StackFixture.ExceptionBank(sr); + var frame = pointers[exceptionBank] - 60; + pointers[exceptionBank] = frame; + M68040StackFixture.SetStacks(e, pointers, (ushort)((sr | 0x2000) & ~0xc000)); + e.Pc = Handler; e.ExceptionVector = 2; + e.ControlChecks["one access exception"] = (s => s.ExceptionSequence, m.Core.State.ExceptionSequence + 1); + e.ControlChecks["original instruction PC"] = (s => s.LastExceptionStackedProgramCounter, SyntheticMachine.Code); + e.ControlChecks["original SR"] = (s => s.LastExceptionStatusRegister, sr); + e.ControlChecks["single original-width rejected read"] = (_ => bus.Rejected.Count == 1 && + bus.Rejected[0].Address == Operand + (uint)lane && bus.Rejected[0].Width == instruction.Width && + bus.Rejected[0].Kind == M68kBusAccessKind.CpuDataRead ? 1u : 0u, 1); + e.ControlChecks["no translation bypass leak"] = (s => s.M68040Mmu.BypassTranslation ? 1u : 0u, 0); + e.Write(frame, sr, 2, m.Model); e.Write(frame + 2, SyntheticMachine.Code, 4, m.Model); + e.Write(frame + 6, 0x7008, 2, m.Model); + // Undefined EA/push/writeback data are not assigned architectural values. + for (uint n = 8; n < 60; n++) e.MemoryMasks[frame + n] = 0; + var size = instruction.Width == 1 ? 0x20 : instruction.Width == 2 ? 0x40 : 0; + var modifier = (sr & 0x2000) != 0 ? 5 : 1; + var ssw = (uint)(0x0100 | size | modifier | (instruction.Movem ? 0x1000 : 0)); + e.Write(frame + 12, ssw, 2, m.Model); + e.MemoryMasks[frame + 12] = 255; e.MemoryMasks[frame + 13] = 0x7f; // X undefined. + if (instruction.Movem) + { + e.Write(frame + 8, Operand + (uint)lane, 4, m.Model); + for (uint n = 8; n < 12; n++) e.MemoryMasks[frame + n] = 255; + } + foreach (var offset in new uint[] { 14, 16, 18 }) + { + e.Write(frame + offset, 0, 2, m.Model); + e.MemoryMasks[frame + offset + 1] = 0x80; // No pending writes before first operand read. + } + e.Write(frame + 20, Operand + (uint)lane, 4, m.Model); + for (uint n = 20; n < 24; n++) e.MemoryMasks[frame + n] = 255; + bus.Arm(Operand + (uint)(lane + faultByte), M68kBusAccessKind.CpuDataRead); + // Exactly one faulting attempt. Do not retry partially executed instructions. + M68040StackFixture.Step(m, e, report, id, batch); + } + catch (NotSupportedException ex) { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 4a761680..3d213c95 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -7176,3 +7176,78 @@ noncanonical MOVE16 stop is preserved separately from this legal-input audit. The advanced 480-case 040 inventory, other-model restoration, software reference disagreements and all remaining consolidation requirements stay open. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +### Actual 040 operand-read fault discovery — 2026-10-06 + +The remaining real data-fault requirement now has a self-contained, strict +failing gate. `SyntheticM68040OperandReadFaultDiscoveryTests` executes sixteen +fixed legal read encodings: MOVE B/W/L, MOVEA W/L, ADD/CMP/TST B/W/L and MOVEM W/L. +The memory operand is `(A0)`; MOVEM selects D0 and rejects its first transfer. +The expectation source is [MC68040UM](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +8.4.6, 8.4.6.2 and 8.4.6.7: format-7 data access frames, fault address, read/size/ +modifier fields, MOVEM CM/calculated EA and absent pending writes before the +first operand read. Undefined EA/push/writeback data and SSW X are masked only +where the architecture leaves them undefined. No CPU decoding/arithmetic/EA +helper supplies expected results. + +Each scalar/batch route executes **61,440 cases / 1,920 combinations** covering +all 32 initial CCR values, user/user-M/ISP/MSP banks, no/T1/T0 incoming trace, +four operand address lanes and every byte of the rejected original-width read. +The gate checks original PC/SR, complete defined frame fields, one exception, +stack selection/consumption, registers, surrounding memory and translation +bypass restoration. Each case attempts its faulting instruction exactly once; +there is no retry after a mismatch or partial effects. No hardware timing, +enabled cache/MMU, arbitrary addressing, later MOVEM transfers, handler/RTE +recovery or physical split-transfer ordering is qualified by this entry fixture. + +Fresh execution passes the single fixed witness fact's **sixteen literal +normal-execution examples**, but both fault-entry batches fail without skips: +**zero passing / 122,880 mismatching / zero unsupported / zero untested** +selected cases. Captured detailed failures first differ at A7: an eight-byte +frame is consumed instead of sixty, leaving the stack 52 bytes above the required +boundary. Current `RaiseMmuFault` explicitly falls back to format 0 for these +operand reads. Further defined frame checks remain unmet prerequisites; these +failed cases are not described as qualified passing coverage. + +```powershell +./scripts/test-copper68k-040-operand-read-discovery.ps1 -OutputDirectory +./scripts/test-copper68k-040-operand-read-discovery.ps1 -ValidateReportsOnly -OutputDirectory +``` + +The maintained command requires the three exact executions (one passing witness, +two complete entry selections), with no skips, complete CPU/test source identities, +report/TRX/isolated-assembly hashes and an independently expanded 1,920-key / +32-weight inventory for each route. It records all classifications before failing +for any mismatch/unsupported/untested case. Frozen validation checks identities +and complete report/TRX content; it does not rerun the CPU. Missing fixtures, +empty selections and changed evidence cannot become passing audit coverage. +Six copied-evidence controls reject missing source/report identities, changed +weights, foreign keys, empty combinations and empty execution. Content controls +refresh altered report hashes so they exercise semantic checks as well as hashing. + +Fresh evidence is under the restoration temporary root in +`audits/OperandReadDiscoveryFinal` (three executions: one pass, two fail), +`OperandReadIntegrityFinal/controls.json` and `operand-read-discovery-final.log`. +Input manifest SHA256: +`5102bcba25b93795b8266de1c0c6efa37619edb795d4c6ac3b126cce01a7b6a1`. +Preliminary wrapper output failed an incorrect PowerShell property-count check; +its captured CPU failures remain separate from final verified acceptance. +The first integrity invocation used an overly specific missing-file diagnostic +string; final six controls require the observed intended errors. These attempts +are preserved and excluded from final validation claims. + +A separate documentary recheck finds no ordinary STOP S-clear clarification in +[NXP M68000PRMER revision 1](https://www.nxp.com/docs/en/reference-manual/M68000PRMER.pdf) +or [MC68060UMAD revision 0.1](https://www.nxp.com/docs/en/reference-manual/MC68060UMAD.pdf). +The existing 060 STOP software-reference disagreement remains unresolved; no +CPU restriction is inferred from absence of an erratum. + +No production CPU edit, regression retirement, consumer replay or package release +is included. Ordinary scenario/report requirements remain 86,097,474 / 707; +the sixteen-example fixed witness is separate from those batch totals. The +480-case advanced-040 inventory remains intact: this is a failing concrete +qualification prerequisite, not closure of its broad real-access-fault row. +The next fix must correct architectural operand-read frame delivery and preserve +actual latched MOVEM EA/continuation rules, then qualify executed recovery and +affected consumers. All other model/reference/consolidation requirements remain +required. Milestone 6 stays **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index f0146c26..4228a801 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2620,3 +2620,16 @@ No production CPU fix, regression retirement or package release is included. Original Basic failures and the full advanced restoration/consolidation scope remain required. See the [supervisor-A7 record](COPPER68K_REFERENCE_QUALIFICATION.md#supervisor-a7-move16-reference-completion--2026-10-06). Milestone 6 remains **in progress**, `roadmapComplete=false`. + +The real 040 operand-read gap now has a concrete strict discovery gate: sixteen +legal MOVE/MOVEA/ADD/CMP/TST/MOVEM encodings, all CCRs, four stack banks, three +trace states, lanes and rejected read bytes, independently verified in scalar +and batch execution. Sixteen literal normal-execution witnesses pass. All +122,880 selected fault-entry cases fail the required format-7 stack boundary; +current delivery uses an eight-byte format-0 frame. Six evidence-integrity +controls reject intended corruption. These failing cases remain discovery, +without promotion or reducing the 480-case broad inventory. The next required +work is architectural read-fault delivery/latched MOVEM continuation, executed +recovery and affected-consumer qualification. No production edit, package release +or regression retirement is included. See the [operand-read discovery](COPPER68K_REFERENCE_QUALIFICATION.md#actual-040-operand-read-fault-discovery--2026-10-06). +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-040-operand-read-discovery.ps1 b/scripts/test-copper68k-040-operand-read-discovery.ps1 new file mode 100644 index 00000000..8c027eef --- /dev/null +++ b/scripts/test-copper68k-040-operand-read-discovery.ps1 @@ -0,0 +1,70 @@ +#requires -Version 7.0 +[CmdletBinding()] +param( + [string] $OutputDirectory = 'artifacts/040-operand-read-discovery', + [switch] $ValidateReportsOnly +) +$ErrorActionPreference = 'Stop' +$repo = Split-Path -Parent $PSScriptRoot +$output = [IO.Path]::GetFullPath($OutputDirectory, $repo) +function Hash([string] $Path) { (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() } +$manifestPath = Join-Path $output 'inputs.json' +$sources = @(@(& git -C $repo ls-files 'Copper68k/*.cs' 'Copper68k/*.csproj' 'Copper68k.Tests/*.cs' 'Copper68k.Tests/*.csproj') + @('scripts/test-copper68k-040-operand-read-discovery.ps1','Copper68k.Tests/Synthetic/SyntheticM68040OperandReadFaultDiscoveryTests.cs') | Sort-Object -Unique) +$evidence = @('audit.trx','68040-operand-read-fault-discovery-scalar.json','68040-operand-read-fault-discovery-batch.json','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll') +if (-not $ValidateReportsOnly) { + if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } + New-Item -ItemType Directory -Path $output | Out-Null + $identities = @($sources | ForEach-Object { @{ path=$_; sha256=(Hash (Join-Path $repo $_)) } }) + $settings = @{ COPPER68K_RUN_040_OPERAND_READ_DISCOVERY='1'; COPPER68K_SYNTHETIC_REPORT_DIR=$output } + $saved = @{} + try { + foreach ($key in $settings.Keys) { $saved[$key]=[Environment]::GetEnvironmentVariable($key); [Environment]::SetEnvironmentVariable($key,$settings[$key]) } + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --artifacts-path (Join-Path $output 'build') --filter 'FullyQualifiedName~SyntheticM68040OperandReadFaultDiscoveryTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output *> (Join-Path $output 'execution.log') + $testExit=$LASTEXITCODE + } finally { foreach ($key in $saved.Keys) { [Environment]::SetEnvironmentVariable($key,$saved[$key]) } } + @{ schema=1; reference='MC68040UM 8.4.6/8.4.6.2/8.4.6.7'; referenceUrl='https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf'; sourceCommit=(& git -C $repo rev-parse HEAD); testExit=$testExit; sources=$identities; evidence=@($evidence | ForEach-Object { @{path=$_;sha256=(Hash (Join-Path $output $_))} }) } | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $manifestPath +} +$manifest=Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json +if ($manifest.schema -ne 1 -or $manifest.sources.Count -ne $sources.Count -or + @(Compare-Object ($manifest.sources.path | Sort-Object) $sources).Count -ne 0 -or + $manifest.evidence.Count -ne 5 -or @(Compare-Object ($manifest.evidence.path | Sort-Object) ($evidence | Sort-Object)).Count -ne 0) { throw 'Missing discovery identities' } +foreach ($source in $manifest.sources) { if ((Hash (Join-Path $repo $source.path)) -ne $source.sha256) { throw "Changed discovery source: $($source.path)" } } +foreach ($item in $manifest.evidence) { if ((Hash (Join-Path $output $item.path)) -ne $item.sha256) { throw "Changed discovery evidence: $($item.path)" } } +[xml]$trx=Get-Content (Join-Path $output 'audit.trx') -Raw +$c=$trx.TestRun.ResultSummary.Counters +$tests=@($trx.TestRun.Results.UnitTestResult) +if ($c.executed -ne 3 -or $c.notExecuted -ne 0 -or $tests.Count -ne 3 -or @($tests.testId | Sort-Object -Unique).Count -ne 3) { throw 'Incomplete discovery execution' } +foreach ($name in @('ScalarReadFaultsRequireFormat7','BatchReadFaultsRequireFormat7','FixedManualReadInstructionWitnessesValidateEncodings')) { + $selected=@($tests | Where-Object { $_.testName.EndsWith('.'+$name) }) + if ($selected.Count -ne 1 -or ($name.StartsWith('Fixed') -and $selected[0].outcome -ne 'Passed')) { throw "Missing/passing fixture witness: $name" } +} +# Independent architectural inventory; never calculate expectations with CPU helpers. +$forms=@(@('MOVE',1,'1010'),@('MOVE',2,'3010'),@('MOVE',4,'2010'),@('MOVEA',2,'3050'),@('MOVEA',4,'2050'),@('ADD',1,'D010'),@('ADD',2,'D050'),@('ADD',4,'D090'),@('CMP',1,'B010'),@('CMP',2,'B050'),@('CMP',4,'B090'),@('TST',1,'4A10'),@('TST',2,'4A50'),@('TST',4,'4A90'),@('MOVEM',2,'4C90'),@('MOVEM',4,'4CD0')) +$required=@{} +foreach ($form in $forms) { foreach ($bank in @('user','user-M','ISP','MSP')) { foreach ($trace in @('0000','8000','4000')) { foreach ($lane in 0..3) { for ($faultByte=0; $faultByte -lt $form[1]; $faultByte++) { + $key="68040/$($form[0])/operand-read-fault/size=$($form[1])/bank=$bank/T=$trace/lane=$lane/byte=$faultByte" + $required[$key]=32 +} } } } } +$rows=@() +foreach ($route in @('scalar','batch')) { + $report=Get-Content (Join-Path $output "68040-operand-read-fault-discovery-$route.json") -Raw | ConvertFrom-Json + if ($report.model -ne '68040' -or $report.group -ne "operand-read-fault-discovery-$route" -or $report.logicalCases -ne 61440 -or $report.xunitBatches -ne 1 -or @($report.combinations.psobject.Properties).Count -ne 1920) { throw "Incomplete discovery coverage: $route" } + $sum=@{passing=0;mismatching=0;unsupported=0;untested=0} + foreach ($combination in $report.combinations.psobject.Properties) { + if (-not $required.ContainsKey($combination.Name)) { throw "Foreign discovery combination: $($combination.Name)" } + $weight=0 + foreach ($status in $combination.Value.psobject.Properties) { + if (-not $sum.ContainsKey($status.Name) -or $status.Value -le 0) { throw 'Invalid discovery status/weight' } + $sum[$status.Name]+=$status.Value; $weight+=$status.Value + } + if ($weight -ne $required[$combination.Name]) { throw "Changed discovery combination weight: $($combination.Name)" } + } + foreach ($status in $sum.Keys) { if ($sum[$status] -ne $report.counts.$status) { throw "Discovery totals disagree: $route/$status" } } + $rows+=@{route=$route;logicalCases=61440;combinations=1920;counts=$sum} +} +$mismatches=($rows | ForEach-Object {$_.counts.mismatching} | Measure-Object -Sum).Sum +$unsupported=($rows | ForEach-Object {$_.counts.unsupported} | Measure-Object -Sum).Sum +$untested=($rows | ForEach-Object {$_.counts.untested} | Measure-Object -Sum).Sum +@{schema=1;rows=$rows;logicalCases=122880;fixtureWitnesses=16;roadmapComplete=$false;inputManifestSha256=(Hash $manifestPath)} | ConvertTo-Json -Depth 10 | Set-Content (Join-Path $output 'verification.json') +if ($mismatches+$unsupported+$untested -gt 0 -or $c.failed -ne 0 -or $manifest.testExit -ne 0) { throw "040 operand-read qualification fails: $mismatches mismatching, $unsupported unsupported, $untested untested cases. Retained reports: $output" } +Write-Host '040 operand-read qualification passes its complete selected inventory.' From ae7ee8ebf9a872d3a725cdb99a6659f59c075fe7 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 00:41:02 +0300 Subject: [PATCH 079/211] Fix 68040 physical operand-read access frames --- .../SyntheticM68040AccessDoubleFaultTests.cs | 10 +-- .../SyntheticM68040BatchFaultTests.cs | 11 +--- ...ticM68040OperandReadFaultDiscoveryTests.cs | 50 +++++++++++++- Copper68k/M68040Support.cs | 24 +++++-- .../M68kAdvancedTimingInterpreter.Movem.cs | 14 +++- Copper68k/M68kAdvancedTimingInterpreter.cs | 28 ++++---- Copper68k/M68kCore.cs | 17 ++++- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 66 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 18 ++++- ...t-copper68k-040-operand-read-discovery.ps1 | 6 +- scripts/test-copper68k-synthetic.ps1 | 6 +- 11 files changed, 208 insertions(+), 42 deletions(-) diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs index 39082d97..7b78a36d 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs @@ -23,7 +23,7 @@ public void WarmDispatchHaltsBothRteFallbackAndOperandFaultEntry(string engine) foreach (var ccr in new[] { 0, 31 }) foreach (var alignment in new uint[] { 0, 1 }) foreach (var stage in new[] { "stack", "vector" }) - for (var faultByte = 0; faultByte < (stage == "vector" ? 4 : form == "RTE" || engine == "accurate" && form.EndsWith("write") ? 60 : 8); faultByte++) + for (var faultByte = 0; faultByte < (stage == "vector" ? 4 : form == "RTE" || engine == "accurate" ? 60 : 8); faultByte++) { var opcode = form switch { "RTE" => 0x4e73, "MOVE.B.read" => 0x1010, "MOVE.W.read" => 0x3010, "MOVE.L.read" => 0x2010, "MOVE.B.write" => 0x1080, "MOVE.W.write" => 0x3080, _ => 0x2080 }; @@ -60,10 +60,10 @@ public void WarmDispatchHaltsBothRteFallbackAndOperandFaultEntry(string engine) var sideExits = core is M68kJitCore j2 ? j2.Counters.SideExits : 0; bus.Arm(form == "RTE" ? frame : 0x4200 + alignment, write ? M68kBusAccessKind.CpuDataWrite : M68kBusAccessKind.CpuDataRead); - // Accurate MOVE writes now create format 7. Read and compiled - // operand paths retain their separate short-frame gap; only - // fatal entry is qualified for those routes here. - var stackStart = frame - (form == "RTE" || engine == "accurate" && write ? 60u : 8u); + // Accurate physical reads and MOVE writes create format 7. + // Compiled operand delivery remains separately qualified here + // only for fatal entry, with its existing short-frame policy. + var stackStart = frame - (form == "RTE" || engine == "accurate" ? 60u : 8u); bus.Arm(stage == "vector" ? 8 + (uint)faultByte : stackStart + (uint)faultByte, stage == "vector" ? M68kBusAccessKind.CpuDataRead : M68kBusAccessKind.CpuDataWrite); Execute(core); diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs index 8c7763e0..b4343e05 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs @@ -6,7 +6,7 @@ namespace Copper68k.Tests.Synthetic; // Architectural validation frames: MC68040UM 8.4.6.7. Cached-block batching, // callbacks and cycle equality are emulator execution/timing-policy contracts. // MOVE writes and instruction fetch/validation use architectural format 7. -// Operand reads retain a separate approximate short-frame gap. +// Normal physical operand reads also use format 7; restart phases are separate. public sealed class SyntheticM68040BatchFaultTests(ITestOutputHelper output) { private const uint Handler = 0x9020; @@ -182,9 +182,8 @@ private static Fixture Prepare(SyntheticMachine m, CodeBus bus, string form, boo e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, savedSr); e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, stackedPc); e.ControlChecks["bypass cleared"] = (s => s.M68040Mmu.BypassTranslation ? 1u : 0u, 0); - var accessFrame = frame - (write || form is "RTE" or "self-fetch" ? 60u : 8u); + var accessFrame = frame - 60u; e.A[7] = accessFrame; if (bank == "MSP") e.MasterStackPointer = accessFrame; - if (write || form is "RTE" or "self-fetch") { SyntheticM68040RteValidationFaultTests.ExpectAccessFrame(m, e, accessFrame, savedSr, stackedPc, faultAddress, width); if (form == "self-fetch") e.Write(accessFrame + 12, 0x0106, 2, m.Model); @@ -200,12 +199,6 @@ private static Fixture Prepare(SyntheticMachine m, CodeBus bus, string form, boo e.Memory[accessFrame + 44 + ((faultAddress + (uint)n) & 3)] = (byte)(storeValue >> (8 * (3 - n))); } } - else - { - // Existing generic delivery policy, not architectural frame qualification. - e.Write(accessFrame, savedSr, 2, m.Model); e.Write(accessFrame + 2, stackedPc, 4, m.Model); - e.Write(accessFrame + 6, 8, 2, m.Model); - } var boundary = new FaultBoundary(m.Core, bus, prefix, incomingSr, faultAddress + (uint)faultByte, kind, outcome == "fatal-stack" ? accessFrame : outcome == "fatal-vector" ? vbr + 8 : null, outcome == "fatal-stack" ? M68kBusAccessKind.CpuDataWrite : M68kBusAccessKind.CpuDataRead); diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadFaultDiscoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadFaultDiscoveryTests.cs index 839e7e53..b9dbed8e 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadFaultDiscoveryTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadFaultDiscoveryTests.cs @@ -4,7 +4,7 @@ namespace Copper68k.Tests.Synthetic; // MC68040UM 8.4.6/8.4.6.2/8.4.6.7, not scalar/batch agreement. -// This failing discovery remains outside promoted ordinary-CI coverage. +// This focused entry gate remains separate from broader restoration coverage. public sealed class SyntheticM68040OperandReadFaultDiscoveryTests(ITestOutputHelper output) { internal const string Enable = "COPPER68K_RUN_040_OPERAND_READ_DISCOVERY"; @@ -25,6 +25,54 @@ private static readonly (string Family, int Width, ushort Opcode, bool Movem)[] [EnvironmentFact(Enable, "qualify actual 040 operand-read access frames"), Trait("Suite", "ReferenceDiscovery")] public void BatchReadFaultsRequireFormat7() => Audit(true); + [Theory, Trait("Suite", "Synthetic")] + [InlineData(false)] + [InlineData(true)] + public void ActualMovemFaultRetainsCalculatedEaAcrossLoadedBaseAndIndex(bool full) + { + var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + m.Reset(31); m.Core.State.A[0] = Operand; m.Core.State.D[1] = 0x20; + m.InitializePhysical(0x1000, 0x4cf0, 2); // MOVEM.L indexed(A0),D0/D1/A0/A1 + m.InitializePhysical(0x1002, 0x0303, 2); + m.InitializePhysical(0x1004, full ? 0x1922u : 0x1800u, 2); + if (full) + { + m.InitializePhysical(0x1006, 0x0010, 2); m.InitializePhysical(0x1008, 0x0006, 2); + m.InitializePhysical(0x4230, 0x5000, 4); // A0 + D1.L + BD, then outer displacement. + } + var ea = full ? 0x5006u : 0x4220u; + uint[] values = [0x11112222, 0x00000040, 0x66667777, 0x88889999]; + for (var n = 0; n < 4; n++) m.InitializePhysical(ea + (uint)n * 4, values[n], 4); + var next = full ? 0x100au : 0x1006u; + m.InitializePhysical(next, 0x7e55, 2); m.InitializePhysical(next + 2, 0x4e71, 2); + m.InitializePhysical(8, Handler, 4); m.InitializePhysical(Handler, 0x4e73, 2); + m.InitializePhysical(Handler + 2, 0x4e71, 2); m.Start(); + var sequence = m.Core.State.ExceptionSequence; + bus.Accesses.Clear(); bus.Arm(ea + 12, M68kBusAccessKind.CpuDataRead); + m.Core.ExecuteInstruction(); + var frame = 0x4700u - 60; + Assert.Equal(frame, m.Core.State.A[7]); Assert.Equal(Handler, m.Core.State.ProgramCounter); + Assert.Equal(0x7008u, m.PeekPhysical(frame + 6, 2)); + Assert.Equal(0x1105u, m.PeekPhysical(frame + 12, 2) & 0xff7f); + Assert.Equal(ea, m.PeekPhysical(frame + 8, 4)); Assert.Equal(ea + 12, m.PeekPhysical(frame + 20, 4)); + Assert.Equal(0x1000u, m.PeekPhysical(frame + 2, 4)); + Assert.Equal(values[0], m.Core.State.D[0]); Assert.Equal(values[1], m.Core.State.D[1]); + Assert.Equal(values[2], m.Core.State.A[0]); Assert.Equal(0x4100u, m.Core.State.A[1]); + Assert.Single(bus.Rejected); + m.Core.ExecuteInstruction(); // Software handler explicitly returns via RTE. + Assert.Equal(0x1000u, m.Core.State.ProgramCounter); Assert.Equal(0x4700u, m.Core.State.A[7]); + m.Core.ExecuteInstruction(); // CM resumes after EA calculation; preceding MOVEM reads repeat by architecture. + Assert.Equal(next, m.Core.State.ProgramCounter); Assert.Equal(values[3], m.Core.State.A[1]); + Assert.Equal(values[0], m.Core.State.D[0]); Assert.Equal(values[1], m.Core.State.D[1]); + Assert.Equal(values[2], m.Core.State.A[0]); Assert.Equal(0x271f, m.Core.State.StatusRegister); + for (var n = 0; n < 3; n++) Assert.Equal(2, bus.Accesses.Count(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address == ea + (uint)n * 4)); + Assert.Single(bus.Accesses.Where(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address == ea + 12)); + if (full) Assert.Single(bus.Accesses.Where(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address == 0x4230)); + Assert.Single(bus.Rejected); Assert.Equal(sequence + 1, m.Core.State.ExceptionSequence); + m.Core.ExecuteInstruction(); Assert.Equal(0x55u, m.Core.State.D[7]); + } + [Fact, Trait("Suite", "Synthetic")] public void FixedManualReadInstructionWitnessesValidateEncodings() { diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index 9871ca0f..d60ee9f9 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -1863,7 +1863,9 @@ internal readonly record struct M68040MmuFault( int ByteCount = 0, bool RteFrameValidation = false, uint? WriteValue = null, - bool CompletedMoveWrite = false); + bool CompletedMoveWrite = false, + bool UnbufferedOperandRead = false, + uint? MovemEffectiveAddress = null); internal sealed class UnsupportedM68040InstructionException : M68kEmulationException { @@ -2562,6 +2564,11 @@ protected override bool TryHandleM68040ExecutionFault(M68040MmuFault fault) fault = fault with { StackedProgramCounter = ExecutionBoundaryProgramCounter }; else if (TryCompleteFaultedMoveWrite(fault)) fault = fault with { StackedProgramCounter = State.ProgramCounter, CompletedMoveWrite = true }; + else if (!State.M68040Mmu.Enabled && !fault.Write && + fault.AccessKind == M68kBusAccessKind.CpuDataRead && fault.ByteCount is 1 or 2 or 4 && + State.ExceptionSequence == ExecutionBoundaryExceptionSequence && + State.LastInstructionProgramCounter == ExecutionBoundaryProgramCounter) + fault = fault with { StackedProgramCounter = ExecutionBoundaryProgramCounter, UnbufferedOperandRead = true }; RaiseMmuFault(fault); return true; } @@ -4327,6 +4334,8 @@ private void RaiseMmuFault(M68040MmuFault fault) RaiseUnbufferedReadAccessFault(fault, stackedProgramCounter, instruction: true); else if (fault.CompletedMoveWrite) RaiseCompletedMoveWriteAccessFault(fault, stackedProgramCounter); + else if (fault.UnbufferedOperandRead) + RaiseUnbufferedReadAccessFault(fault, stackedProgramCounter, instruction: false); else RaiseFormat0Exception(VectorBusError, stackedProgramCounter, M68kInstructionTimingKey.IllegalInstruction); } @@ -4372,10 +4381,10 @@ private void RaiseCompletedMoveWriteAccessFault(M68040MmuFault fault, uint nextP private void RaiseUnbufferedReadAccessFault(M68040MmuFault fault, uint instructionPc, bool instruction) { - // MC68040UM 8.4.6/7: instruction-prefetch faults have no pending - // writebacks. Pre-commit RTE validation reads use the same layout, - // preserving the original frame; data operand/write restart is a - // separate path and must not be implemented by replaying effects. + // MC68040UM 8.4.6/7: synchronous reads have no pending writebacks. + // Pre-commit RTE validation preserves the original frame. MOVEM + // carries its calculated EA for explicit software RTE continuation; + // exception delivery itself must never replay operand effects. var savedSr = State.StatusRegister; State.RecordException(VectorBusError, instructionPc, savedSr); State.StatusRegister = (ushort)((savedSr | M68kCpuState.Supervisor) & ~0xc000); @@ -4387,8 +4396,9 @@ private void RaiseUnbufferedReadAccessFault(M68040MmuFault fault, uint instructi PushLong(fault.LogicalAddress); PushWord(0); PushWord(0); PushWord(0); var modifier = ((savedSr & M68kCpuState.Supervisor) != 0 ? 4 : 0) | (instruction ? 2 : 1); - PushWord((ushort)(0x0100 | (fault.ByteCount == 2 ? 0x0040 : 0) | modifier)); - PushLong(0); + var size = fault.ByteCount == 1 ? 0x20 : fault.ByteCount == 2 ? 0x40 : 0; + PushWord((ushort)(0x0100 | size | modifier | (fault.MovemEffectiveAddress.HasValue ? 0x1000 : 0))); + PushLong(fault.MovemEffectiveAddress ?? 0); PushWord(0x7008); PushLong(instructionPc); PushWord(savedSr); diff --git a/Copper68k/M68kAdvancedTimingInterpreter.Movem.cs b/Copper68k/M68kAdvancedTimingInterpreter.Movem.cs index a88cf4db..68627e60 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.Movem.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.Movem.cs @@ -5,6 +5,17 @@ namespace Copper68k; internal partial class M68kAdvancedTimingInterpreter { + private uint ReadMovemSized(uint address, M68kOperandSize size, uint calculatedAddress) + { + try { return ReadSized(address, size); } + catch (M68040MmuFaultException ex) when (_profile.Model == M68kAcceleratorModel.M68040 && + !State.M68040Mmu.Enabled && !ex.Fault.Write && ex.Fault.AccessKind == M68kBusAccessKind.CpuDataRead) + { + // The address is already calculated. A base/index may have been + // loaded before this fault; never decode or read its pointer again. + throw new M68040MmuFaultException(ex.Fault with { MovemEffectiveAddress = calculatedAddress }); + } + } private bool TryExecuteGeneralMovem(ushort opcode, uint? continuationAddress = null) { if ((opcode & 0xfb80) != 0x4880) return false; @@ -33,6 +44,7 @@ private bool TryExecuteGeneralMovem(ushort opcode, uint? continuationAddress = n address = continuationAddress.Value; } else address = mode is 3 or 4 ? State.A[baseRegister] : ResolveMoveAddress(mode, baseRegister, size, opcode); + var calculatedAddress = address; for (var bit = 0; bit < 16; bit++) { if ((mask & (1 << bit)) == 0) continue; @@ -40,7 +52,7 @@ private bool TryExecuteGeneralMovem(ushort opcode, uint? continuationAddress = n if (mode == 4) address = unchecked(address - (uint)size); if (load) { - var value = ReadSized(address, size); + var value = ReadMovemSized(address, size, calculatedAddress); if (size == M68kOperandSize.Word) value = unchecked((uint)(int)(short)value); WriteGeneralRegister(register >= 8, register & 7, value); } diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index 2c2b20db..8fa83a65 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -9401,6 +9401,7 @@ private void ExecuteMovemWordPostIncrementToRegisters(ushort opcode) var mask = FetchWord(); var addressRegister = opcode & 7; var address = State.A[addressRegister]; + var calculatedAddress = address; for (var register = 0; register < 8; register++) { if ((mask & (1 << register)) == 0) @@ -9408,7 +9409,7 @@ private void ExecuteMovemWordPostIncrementToRegisters(ushort opcode) continue; } - State.D[register] = unchecked((uint)(int)(short)ReadWord(address)); + State.D[register] = unchecked((uint)(int)(short)ReadMovemSized(address, M68kOperandSize.Word, calculatedAddress)); address += 2; } @@ -9419,7 +9420,7 @@ private void ExecuteMovemWordPostIncrementToRegisters(ushort opcode) continue; } - WriteGeneralRegister(true, register, unchecked((uint)(int)(short)ReadWord(address))); + WriteGeneralRegister(true, register, unchecked((uint)(int)(short)ReadMovemSized(address, M68kOperandSize.Word, calculatedAddress))); address += 2; } @@ -9438,6 +9439,7 @@ private void ExecuteMovemLongPostIncrementToRegisters(ushort opcode) var mask = FetchWord(); var addressRegister = opcode & 7; var address = State.A[addressRegister]; + var calculatedAddress = address; for (var register = 0; register < 8; register++) { if ((mask & (1 << register)) == 0) @@ -9445,7 +9447,7 @@ private void ExecuteMovemLongPostIncrementToRegisters(ushort opcode) continue; } - State.D[register] = ReadLong(address); + State.D[register] = ReadMovemSized(address, M68kOperandSize.Long, calculatedAddress); address += 4; } @@ -9456,7 +9458,7 @@ private void ExecuteMovemLongPostIncrementToRegisters(ushort opcode) continue; } - WriteGeneralRegister(true, register, ReadLong(address)); + WriteGeneralRegister(true, register, ReadMovemSized(address, M68kOperandSize.Long, calculatedAddress)); address += 4; } @@ -9474,6 +9476,7 @@ private void ExecuteMovemLongAddressIndirectToRegisters(ushort opcode) _ = FetchWord(); var mask = FetchWord(); var address = State.A[opcode & 7]; + var calculatedAddress = address; for (var register = 0; register < 8; register++) { if ((mask & (1 << register)) == 0) @@ -9481,7 +9484,7 @@ private void ExecuteMovemLongAddressIndirectToRegisters(ushort opcode) continue; } - State.D[register] = ReadLong(address); + State.D[register] = ReadMovemSized(address, M68kOperandSize.Long, calculatedAddress); address += 4; } @@ -9492,7 +9495,7 @@ private void ExecuteMovemLongAddressIndirectToRegisters(ushort opcode) continue; } - WriteGeneralRegister(true, register, ReadLong(address)); + WriteGeneralRegister(true, register, ReadMovemSized(address, M68kOperandSize.Long, calculatedAddress)); address += 4; } @@ -9514,10 +9517,11 @@ private void ExecuteMovemIndexedToRegisters(ushort opcode) var address = CalculateIndexedOperandAddress(sourceBase, extension, opcode); var size = (opcode & 0x40) == 0 ? M68kOperandSize.Word : M68kOperandSize.Long; // Latch the EA before loading any base/index register in the mask. + var calculatedAddress = address; for (var register = 0; register < 16; register++) { if ((mask & (1 << register)) == 0) continue; - var value = ReadSized(address, size); + var value = ReadMovemSized(address, size, calculatedAddress); if (size == M68kOperandSize.Word) value = unchecked((uint)(int)(short)value); WriteGeneralRegister(register >= 8, register & 7, value); address = unchecked(address + (uint)(size == M68kOperandSize.Word ? 2 : 4)); @@ -9539,6 +9543,7 @@ private void ExecuteMovemLongDisplacementToRegisters(ushort opcode) var baseAddress = pcRelative ? State.ProgramCounter : State.A[opcode & 7]; var displacement = unchecked((int)(short)FetchWord()); var address = unchecked((uint)(baseAddress + displacement)); + var calculatedAddress = address; for (var register = 0; register < 8; register++) { if ((mask & (1 << register)) == 0) @@ -9546,7 +9551,7 @@ private void ExecuteMovemLongDisplacementToRegisters(ushort opcode) continue; } - State.D[register] = ReadLong(address); + State.D[register] = ReadMovemSized(address, M68kOperandSize.Long, calculatedAddress); address += 4; } @@ -9557,7 +9562,7 @@ private void ExecuteMovemLongDisplacementToRegisters(ushort opcode) continue; } - WriteGeneralRegister(true, register, ReadLong(address)); + WriteGeneralRegister(true, register, ReadMovemSized(address, M68kOperandSize.Long, calculatedAddress)); address += 4; } @@ -9579,11 +9584,12 @@ private void ExecuteMovemWordAddressDisplacementToRegisters(ushort opcode) var displacement = indirect ? 0 : unchecked((int)(short)FetchWord()); var address = unchecked((uint)(baseAddress + displacement)); + var calculatedAddress = address; for (var register = 0; register < 8; register++) { if ((mask & (1 << register)) != 0) { - State.D[register] = unchecked((uint)(int)(short)ReadWord(address)); + State.D[register] = unchecked((uint)(int)(short)ReadMovemSized(address, M68kOperandSize.Word, calculatedAddress)); address += 2; } } @@ -9592,7 +9598,7 @@ private void ExecuteMovemWordAddressDisplacementToRegisters(ushort opcode) { if ((mask & (1 << (8 + register))) != 0) { - WriteGeneralRegister(true, register, unchecked((uint)(int)(short)ReadWord(address))); + WriteGeneralRegister(true, register, unchecked((uint)(int)(short)ReadMovemSized(address, M68kOperandSize.Word, calculatedAddress))); address += 2; } } diff --git a/Copper68k/M68kCore.cs b/Copper68k/M68kCore.cs index 373d2bf8..f2cb9355 100644 --- a/Copper68k/M68kCore.cs +++ b/Copper68k/M68kCore.cs @@ -12374,9 +12374,20 @@ private void DecodeMovem(ushort opcode) { var readFaultBaseCycles = size == M68kOperandSize.Word ? 8 : 4; AddInstructionCycles(readFaultBaseCycles + ea.EaCycles + (transferred * transferCycles)); - var value = size == M68kOperandSize.Word - ? M68kCpuState.SignExtend(ReadWord(current), M68kOperandSize.Word) - : ReadLong(current); + uint value; + try + { + value = size == M68kOperandSize.Word + ? M68kCpuState.SignExtend(ReadWord(current), M68kOperandSize.Word) + : ReadLong(current); + } + catch (M68040MmuFaultException ex) when (!State.M68040Mmu.Enabled && + !ex.Fault.Write && ex.Fault.AccessKind == M68kBusAccessKind.CpuDataRead) + { + // The 040 integer fallback shares this decoder. Keep + // the original EA even after loading its base/index. + throw new M68040MmuFaultException(ex.Fault with { MovemEffectiveAddress = ea.Address }); + } if (register < 8) { State.D[register] = value; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 3d213c95..1229f998 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -7251,3 +7251,69 @@ The next fix must correct architectural operand-read frame delivery and preserve actual latched MOVEM EA/continuation rules, then qualify executed recovery and affected consumers. All other model/reference/consolidation requirements remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +### Physical 040 operand-read frame correction — 2026-10-06 + +The failing operand-read discovery above is retained as before-fix evidence. +Normal, translation-disabled 040 operand reads now enter the architectural +60-byte format-7 frame, preserving the instruction PC and live SR and recording +the actual read size, data modifier and fault address. Exception-entry reads +are excluded by the instruction boundary/exception sequence checks. Enabled +MMU delivery and compiled operand side exits retain their separate scope. + +MOVEM read faults carry the EA calculated before any transfer. Both advanced +paths and the shared integer fallback attach it at the failing read; neither +recalculates an overwritten base/index nor rereads an indirect pointer during +exception delivery. The frame sets CM and saves this EA. An explicit software +RTE then resumes through the existing CM path, repeating preceding MOVEM reads +as required by MC68040UM 8.4.6.7. There is no automatic instruction retry. + +The focused command passes **122,880 selected entry cases**, with zero +mismatches, unsupported cases or missing selected combinations. Its five +executions include sixteen literal encoding witnesses and two new brief/full +indexed MOVEM recovery controls. These fault the fourth transfer after D1 and +A0 have been overwritten, verify the saved EA/CM/PC, execute RTE and complete +the transfer, and check that the full-format pointer was read exactly once. +Dropping calculated-EA metadata in an isolated source copy makes both controls +fail at the CM assertion. Six copied-evidence corruption controls still reject +their intended errors; exact theory inputs are required by the command. + +Cached-batch expectations now require the architectural read frame while +retaining count, callback, partial-effect and timing-policy checks. Accurate +fatal-entry coverage expands from eight to all sixty frame bytes for B/W/L +reads: **1,248 additional scenarios**, making the ordinary required inventory +**86,098,722 scenarios / 707 reports**. This does not promote the focused entry +matrix into those ordinary totals or reduce the broad 480-case remaining gate. + +Consumer validation uses a clean archive of CopperScreen commit +`aa1dad5dcc0fb7c970e8e3443a160487add846af` and the isolated, unpublished +`1.5.2-synthetic-dev.68` NuGet package. Release production build, 171 host tests, +74 disk tests and 1,080 separately built engine tests pass. Six optional host +replays are unavailable in that ordinary run. Two explicit native Workbench +3.1 floppy boots (zero/2 MiB Fast RAM) pass their existing ROM/media, PC, cycle +and framebuffer fingerprints. The Workbench input was restored from its local +archive; the previously deleted HDF was not recreated or claimed as replayed. +Pinned SingleStepTests and Musashi audits also pass independently. + +Evidence is retained under the restoration temporary root in +`audits/OperandReadAcceptance`, `OperandReadAcceptanceIntegrity`, +`mutations/DropMovemEa`, `audits/OperandReadIndependent`, `consumer-68` and +`native-68`. The private package is immutable and has SHA256 +`c4d2e75502d9304d249760e9c57356b20b6ad569c45f4fe2786ecdbd35022e28`. + +The fresh full CPU run completed on **2026-10-07**: **5,244 passing / zero +failed / 21 optional or discovery skips / 5,265 total**. All ten enabled +qualified WinUAE presets pass in this run. The maintained report validator +accepts all **707 required batches / 86,098,722 scenarios**. Actual passing TRX +batch summaries are cross-checked against the report contents; the complete +result roster and report/assembly hashes are retained in +`audits/OperandReadFull/full-verification.json`. The older cleanup-affected full +run remains failed historical evidence. These fresh results do not turn skipped +discovery gates or the original Basic reference disagreements into coverage. + +General operand addressing/recovery/trace deferral, MOVEM writes, enabled +MMU/cache and physical transfer timing remain outside this selected fix. +Other models' restoration gaps, unresolved software-reference disagreements, +the original Basic non-passing rows and regression consolidation remain +required. No public release or regression retirement is included. Milestone 6 +remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 4228a801..ba85d703 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2621,7 +2621,7 @@ Original Basic failures and the full advanced restoration/consolidation scope remain required. See the [supervisor-A7 record](COPPER68K_REFERENCE_QUALIFICATION.md#supervisor-a7-move16-reference-completion--2026-10-06). Milestone 6 remains **in progress**, `roadmapComplete=false`. -The real 040 operand-read gap now has a concrete strict discovery gate: sixteen +The real 040 operand-read gap gained a concrete strict discovery gate: sixteen legal MOVE/MOVEA/ADD/CMP/TST/MOVEM encodings, all CCRs, four stack banks, three trace states, lanes and rejected read bytes, independently verified in scalar and batch execution. Sixteen literal normal-execution witnesses pass. All @@ -2633,3 +2633,19 @@ work is architectural read-fault delivery/latched MOVEM continuation, executed recovery and affected-consumer qualification. No production edit, package release or regression retirement is included. See the [operand-read discovery](COPPER68K_REFERENCE_QUALIFICATION.md#actual-040-operand-read-fault-discovery--2026-10-06). Milestone 6 remains **in progress**, `roadmapComplete=false`. + +The subsequent physical-read fix passes that complete selected entry inventory +and two brief/full indexed MOVEM fourth-transfer recovery controls. Calculated +EA metadata survives overwritten base/index registers, including the shared +integer fallback, without repeating pointer resolution. An isolated mutation +that drops the metadata fails both controls. The strict five-execution gate and +six evidence-integrity controls pass. Accurate fatal-entry checks now cover all +60 frame bytes for B/W/L reads, adding 1,248 cases; ordinary requirements become +86,098,722 scenarios / 707 reports. The fresh full CPU run passes 5,244 tests +with zero failures and 21 explicit optional/discovery skips; report validation +accepts every required ordinary batch. Clean CopperScreen consumer build, host/disk/ +engine tests and two native Workbench floppy replays pass via a private NuGet +package, as do independent pinned SingleStepTests/Musashi audits. Broader +addressing/recovery/trace and remaining model/reference/consolidation coverage +stay required. See the [read-frame correction](COPPER68K_REFERENCE_QUALIFICATION.md#physical-040-operand-read-frame-correction--2026-10-06). +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-040-operand-read-discovery.ps1 b/scripts/test-copper68k-040-operand-read-discovery.ps1 index 8c027eef..a1254f43 100644 --- a/scripts/test-copper68k-040-operand-read-discovery.ps1 +++ b/scripts/test-copper68k-040-operand-read-discovery.ps1 @@ -33,11 +33,15 @@ foreach ($item in $manifest.evidence) { if ((Hash (Join-Path $output $item.path) [xml]$trx=Get-Content (Join-Path $output 'audit.trx') -Raw $c=$trx.TestRun.ResultSummary.Counters $tests=@($trx.TestRun.Results.UnitTestResult) -if ($c.executed -ne 3 -or $c.notExecuted -ne 0 -or $tests.Count -ne 3 -or @($tests.testId | Sort-Object -Unique).Count -ne 3) { throw 'Incomplete discovery execution' } +if ($c.executed -ne 5 -or $c.notExecuted -ne 0 -or $tests.Count -ne 5 -or @($tests.testId | Sort-Object -Unique).Count -ne 5) { throw 'Incomplete discovery execution' } foreach ($name in @('ScalarReadFaultsRequireFormat7','BatchReadFaultsRequireFormat7','FixedManualReadInstructionWitnessesValidateEncodings')) { $selected=@($tests | Where-Object { $_.testName.EndsWith('.'+$name) }) if ($selected.Count -ne 1 -or ($name.StartsWith('Fixed') -and $selected[0].outcome -ne 'Passed')) { throw "Missing/passing fixture witness: $name" } } +$continuations=@($tests | Where-Object { $_.testName.Contains('.ActualMovemFaultRetainsCalculatedEaAcrossLoadedBaseAndIndex(') }) +if ($continuations.Count -ne 2 -or @($continuations | Where-Object outcome -ne 'Passed').Count -ne 0 -or + @($continuations | Where-Object { $_.testName.EndsWith('(full: False)') }).Count -ne 1 -or + @($continuations | Where-Object { $_.testName.EndsWith('(full: True)') }).Count -ne 1) { throw 'Incomplete MOVEM continuation controls' } # Independent architectural inventory; never calculate expectations with CPU helpers. $forms=@(@('MOVE',1,'1010'),@('MOVE',2,'3010'),@('MOVE',4,'2010'),@('MOVEA',2,'3050'),@('MOVEA',4,'2050'),@('ADD',1,'D010'),@('ADD',2,'D050'),@('ADD',4,'D090'),@('CMP',1,'B010'),@('CMP',2,'B050'),@('CMP',4,'B090'),@('TST',1,'4A10'),@('TST',2,'4A50'),@('TST',4,'4A90'),@('MOVEM',2,'4C90'),@('MOVEM',4,'4CD0')) $required=@{} diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 6f0b8a32..8a3d6c55 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -204,7 +204,7 @@ try { $expected['access-fault-batch-dispatch'] = 10368 $expected['rte-access-entry-double-fault'] = 98304 $expected['rte-access-handler-refault'] = 3840 - $expected['access-double-fault-dispatch-accurate'] = 2336 + $expected['access-double-fault-dispatch-accurate'] = 3584 $expected['access-double-fault-dispatch-v1'] = 1088 $expected['access-double-fault-dispatch-v2'] = 1088 $expected['rte-validation-physical-direct'] = 162816 @@ -369,8 +369,8 @@ try { m68040RteRepairScope=@{logicalCases=912384; batches=2; translationEnabled=$false; faultTailBanks=@('ISP','MSP'); restoredBanks=@('user','ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); incomingTrace='All three direct; T1 for each chained read byte'; restoredTrace=@(0,0x8000,0x4000); phases=@('fault entry','three original-frame repair stores','explicit incoming-trace clear','handler return','original RTE retry','optional pending return','following instruction'); forms='0/2/3, invalid 4/15 repaired to 0, normal/CM/CT/CU/CP49'; remaining='Internal restoration, mixed-epoch trace provenance and other real access faults; normal/CM preserved-trace and user-tail repair are qualified separately'}; m68040InstructionFaultScope=@{logicalCases=116736; batches=2; translationEnabled=$false; instructionCacheEnabled=$false; engine='accurate public factory plus cached self-branch batch'; forms=@('opcode','extension-low','next-opcode','self-branch'); banks=@('user','ISP','MSP'); ccr='all 32'; trace='All defined incoming states for frame/return; restart trace zero'; fields='Format7 SR, executing PC distinct from prefetch FA, RW/SIZE/TM and invalid writebacks'; phases=@('fault entry','handler return','restart','following'); remaining='Data writebacks/restart, speculative prefetch deferral, compiled instruction-fault PC provenance, cache-enabled fetches and physical timing'}; m68040RtePhysicalValidationScope=@{logicalCases=223872; batches=2; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; chainedCcr=@(0,31); trace=@(0,0x8000,0x4000); alignment=@(0,1); vbr=@(0,0x10000); faults='Each byte of SR/PC/format/SSW/continuation-EA reads; 14 direct/one/two-throwaway supervisor paths'; phases=@('fault entry','handler return'); undefined='EA and invalid writeback/push data; SSW X'; remaining='Internal-restoration/double faults, other instruction faults and software writeback handlers; user-tail validation is qualified separately'}; - m68040AccessDoubleFaultScope=@{logicalCases=105408; batches=5; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; dispatchCcr=@(0,31); engines=@('accurate','classic','V2'); faults='Every byte of format-7 stack/vector; later handler validation refaults; warmed RTE fallback and compiled B/W/L operand read/write side exits'; recovery='External reset, including restored host entry; interrupt, task/subroutine entry and halted batches remain inactive'; undefined='Partial stack write order, contents and exact SP'; remaining='Internal restoration, other handler-entry paths and general architectural access-fault restart'}; - m68040BatchFaultScope=@{logicalCases=139392; batches=2; translationEnabled=$false; paths=@('cold','cached','model-specific cached','self-branch cached'); limits=@('instruction cap','boundary denial','cycle deadline'); architectural='Supervisor RTE validation frames, unbuffered self-branch instruction-fault frame and fatal second-fault halt'; policy='Generic short operand frames, scalar/batch bus order and machine/native cycle equality'; qualification='Dedicated discovery enumerates independent combinations; BatchFault scope proves four delivery paths, count, callback and retry defects'; remaining='General data format-7 restart, other entry paths and internal-restoration faults'}; + m68040AccessDoubleFaultScope=@{logicalCases=106656; batches=5; translationEnabled=$false; banks=@('ISP','MSP'); directCcr='all 32'; dispatchCcr=@(0,31); engines=@('accurate','classic','V2'); faults='Every byte of format-7 stack/vector; later handler validation refaults; warmed RTE fallback and compiled B/W/L operand read/write side exits'; recovery='External reset, including restored host entry; interrupt, task/subroutine entry and halted batches remain inactive'; undefined='Partial stack write order, contents and exact SP'; remaining='Internal restoration, other handler-entry paths and general architectural access-fault restart'}; + m68040BatchFaultScope=@{logicalCases=139392; batches=2; translationEnabled=$false; paths=@('cold','cached','model-specific cached','self-branch cached'); limits=@('instruction cap','boundary denial','cycle deadline'); architectural='Supervisor RTE validation, physical operand-read and unbuffered self-branch format-7 frames, and fatal second-fault halt'; policy='Scalar/batch bus order and machine/native cycle equality'; qualification='Dedicated discovery enumerates independent combinations; BatchFault scope proves four delivery paths, count, callback and retry defects'; remaining='General data format-7 restart, other entry paths and internal-restoration faults'}; m68060LowPowerStopScope='Every unrecognized second opcode word in both privilege modes; fixed encoding, status, CCR and incoming trace cases across all profiles. Opcode-PC exception frames follow MC68060UM 8.2.4/5; physical broadcast, pins and ordinary STOP S-clear software disagreement remain unqualified'; cacheInstructionEncodingScope='Every F4xx opcode, all CCRs and both privilege states across eight profiles; scope zero is illegal on 040/060, including neither-cache forms, before privilege effects. Empty fixture caches qualify architectural state and exception outcomes, not physical invalidation, writeback, bus faults or timing'; m68010Format8Scope='58-byte address-error frame, reserved holes, version validation and tail probe, alignment double-fault halt/reset; marked private word/long-MOVE/MOVEA images resume pending word cycles without replay; non-MOVE/foreign restart/input state, external BERR, RMW and physical timing remain unqualified'; From bee48169832d95e50cda367a1dbae6697a9c7102 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 01:01:08 +0300 Subject: [PATCH 080/211] Qualify 68040 MOVEM operand-read recovery --- .../Synthetic/AddressingFixtures.cs | 6 +- .../SyntheticM68040MovemReadRecoveryTests.cs | 271 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 62 ++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 11 + ...test-copper68k-040-movem-read-recovery.ps1 | 87 ++++++ 5 files changed, 435 insertions(+), 2 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040MovemReadRecoveryTests.cs create mode 100644 scripts/test-copper68k-040-movem-read-recovery.ps1 diff --git a/Copper68k.Tests/Synthetic/AddressingFixtures.cs b/Copper68k.Tests/Synthetic/AddressingFixtures.cs index e8f745f7..8f90e206 100644 --- a/Copper68k.Tests/Synthetic/AddressingFixtures.cs +++ b/Copper68k.Tests/Synthetic/AddressingFixtures.cs @@ -45,6 +45,8 @@ internal sealed class AddressingFixture(SyntheticMachine machine, int width, Lis { public Func? SourcePointerTarget { get; private set; } public Func? DestinationPointerTarget { get; private set; } + public uint? SourcePointerAddress { get; private set; } + public uint? DestinationPointerAddress { get; private set; } public uint NextPc => SyntheticMachine.Code + (uint)words.Count * 2; public uint Resolve(OperandForm form, bool source) { @@ -96,8 +98,8 @@ private uint ResolveIndex(uint baseAddress, bool source) var pointer = source ? 0x200000u : 0x300000u; machine.InitializePhysical(pointerLocation, pointer, 4); Func finalTarget = () => unchecked(machine.PeekPhysical(pointerLocation, 4) + od + (spec.Indirect >= 5 ? index : 0u)); - if (source) SourcePointerTarget = finalTarget; - else DestinationPointerTarget = finalTarget; + if (source) { SourcePointerTarget = finalTarget; SourcePointerAddress = pointerLocation; } + else { DestinationPointerTarget = finalTarget; DestinationPointerAddress = pointerLocation; } return unchecked(pointer + od + (spec.Indirect >= 5 ? index : 0u)); } diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040MovemReadRecoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040MovemReadRecoveryTests.cs new file mode 100644 index 00000000..1bf6a6d3 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040MovemReadRecoveryTests.cs @@ -0,0 +1,271 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 8.2.6, 8.4.6.2/7: CM saves the original EA, RTE repeats +// preceding operand transfers, and a suspended instruction traces on completion. +// This is software architectural qualification, not a physical pipeline model. +public sealed class SyntheticM68040MovemReadRecoveryTests(ITestOutputHelper output) +{ + internal const string Enable = "COPPER68K_RUN_040_MOVEM_READ_RECOVERY"; + private const uint Handler = 0xa000, TraceHandler = 0xa100, Vbr = 0x900000; + private static readonly uint[] LongValues = [0x11112222, 0x00000040, 0x66667777, 0x88889999]; + private static readonly uint[] WordValues = [0xffff8001, 0x00000040, 0x00007ffe, 0xffff8888]; + + [Fact, Trait("Suite", "ReferenceDiscovery")] + public void FixedPcSelfReferenceEncodings() + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); + m.Reset(31); m.Core.State.D[1] = 0xffffffe0; + var words = new List { 0x4cfb, 0x0303 }; + var direct = new AddressingFixture(m, 4, words, m.Core.State.D, m.Core.State.A, 0, + new(SourceIndex: new(Full: true, IndexRegister: 1, SuppressIndex: true))); + Assert.Equal(0x1004u, direct.Resolve(new(7, 3), true)); + Assert.Equal(new ushort[] { 0x4cfb, 0x0303, 0x1150 }, words); + Assert.Equal(0x1006u, direct.NextPc); + words = [0x4cfb, 0x0303]; + var indirect = new AddressingFixture(m, 4, words, m.Core.State.D, m.Core.State.A, 0, + new(SourceIndex: new(Full: true, IndexRegister: 1, Indirect: 5))); + Assert.Equal(0x001fffe0u, indirect.Resolve(new(7, 3), true)); + Assert.Equal(new ushort[] { 0x4cfb, 0x0303, 0x1115 }, words); + Assert.Equal(0x1004u, indirect.SourcePointerAddress); + m.InitializePhysical(0x1004, 0x11157e55, 4); + Assert.Equal(0x11157e35u, indirect.SourcePointerTarget!()); + Assert.Equal(0x1006u, indirect.NextPc); + } + + [Theory, Trait("Suite", "ReferenceDiscovery")] + [InlineData(false)] + [InlineData(true)] + public void FixedLegalSourceRecoveryWitnesses(bool batch) + { + var report = new CoverageBatch("68040", "movem-read-recovery-witness-" + Route(batch)); + foreach (var source in Sources()) + foreach (var width in new[] { 2, 4 }) + Case(report, batch, "witness", source, width, 3, width - 1, "ISP", 0, 31); + report.Complete(output); + } + + [EnvironmentFact(Enable, "qualify actual MOVEM read faults and software RTE recovery"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarRecoveryMatrix() => Audit(false); + + [EnvironmentFact(Enable, "qualify actual MOVEM read faults and software RTE recovery"), Trait("Suite", "ReferenceDiscovery")] + public void BatchRecoveryMatrix() => Audit(true); + + private void Audit(bool batch) + { + var report = new CoverageBatch("68040", "movem-read-recovery-discovery-" + Route(batch)); + foreach (var source in Sources()) + foreach (var width in new[] { 2, 4 }) + for (var transfer = 0; transfer < 4; transfer++) + for (var faultByte = 0; faultByte < width; faultByte++) + Case(report, batch, "opcode", source, width, transfer, faultByte, "ISP", 0, 31); + + // Full structures are separate from the CCR/bank/trace product. + foreach (var spec in IndexFixture.FullStructures()) + foreach (var index in new[] { + spec with { IndexRegister = 1, LongIndex = false, Scale = 0 }, + spec with { IndexRegister = 1, LongIndex = true, Scale = 0 }, + spec with { IndexRegister = 1, LongIndex = false, Scale = 3 }, + spec with { AddressIndex = true, IndexRegister = 0, LongIndex = false, Scale = 2 } }) + foreach (var form in new[] { new OperandForm(6, 1), new OperandForm(7, 3) }) + foreach (var width in new[] { 2, 4 }) + for (var transfer = 0; transfer < 4; transfer++) + for (var faultByte = 0; faultByte < width; faultByte++) + Case(report, batch, "structure", new(form, index), width, transfer, faultByte, "ISP", 0, 31); + + Source[] canonical = [new(new(2, 0)), new(new(3, 0)), new(new(3, 7)), + new(new(5, 0)), new(new(6, 0)), new(new(7, 0)), new(new(7, 1)), + new(new(7, 2)), new(new(7, 3)), + new(new(6, 0), new(Full: true, IndexRegister: 1, LongIndex: true, BaseSize: 2, Indirect: 2)), + new(new(7, 3), new(Full: true, IndexRegister: 1, LongIndex: true, BaseSize: 3, Indirect: 7))]; + foreach (var source in canonical) + foreach (var width in new[] { 2, 4 }) + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var trace in new ushort[] { 0, 0x8000, 0x4000 }) + for (var ccr = 0; ccr < 32; ccr++) + for (var transfer = 0; transfer < 4; transfer++) + for (var faultByte = 0; faultByte < width; faultByte++) + Case(report, batch, "status", source, width, transfer, faultByte, bank, trace, ccr); + report.Complete(output); + } + + private static string Route(bool batch) => batch ? "batch" : "scalar"; + private sealed record Source(OperandForm Form, IndexFixture? Index = null) + { + public string Id => $"mode={Form.Mode}/reg={Form.Register}/index={Index?.Id ?? "brief-default"}" + + (Index is null ? "" : $"/ix={(Index.AddressIndex ? "A" : "D")}{Index.IndexRegister}/{(Index.LongIndex ? "L" : "W")}/scale={1 << Index.Scale}"); + } + private static IEnumerable Sources() + { + foreach (var mode in new[] { 2, 3, 5, 6 }) + for (var register = 0; register < 8; register++) yield return new(new(mode, register)); + for (var register = 0; register < 4; register++) yield return new(new(7, register)); + } + + private static void Case(CoverageBatch report, bool batch, string cohort, Source source, + int width, int transfer, int faultByte, string bank, ushort trace, int ccr) + { + var opcode = (ushort)(0x4c80 | (width == 4 ? 0x40 : 0) | source.Form.Mode << 3 | source.Form.Register); + var id = $"68040/MOVEM/read-recovery/{cohort}/size={width}/{source.Id}/bank={bank}/T={trace:X4}/transfer={transfer}/byte={faultByte}/op={opcode:X4}/ccr={ccr:X2}"; + var phase = "fixture"; + try + { + var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + m.Reset(ccr); + var sr = (ushort)(0x0700 | M68040StackFixture.Status(bank, trace, ccr)); + var pointers = new Dictionary { ["user"] = 0x7800, ["ISP"] = 0x4700, ["MSP"] = 0x7400 }; + SetInitialStacks(m, pointers, sr); + m.Core.State.D[1] = 0xffffffe0; // Signed/scaled index, overwritten by transfer 1. + if (source.Index?.AddressIndex == true) m.Core.State.A[0] = 0xffffffe0; + foreach (var top in pointers.Values) + for (var n = -64; n < 32; n++) m.InitializePhysical(unchecked(top + (uint)n), (uint)(n ^ 0x5a), 1); + var words = new List { opcode, 0x0303 }; // D0/D1/A0/A1, including bases/indexes. + var index = source.Index ?? new(IndexRegister: 1, LongIndex: true, BriefDisplacement: 64); + var fixture = new AddressingFixture(m, width, words, (uint[])m.Core.State.D.Clone(), + (uint[])m.Core.State.A.Clone(), 0, new(SourceIndex: index, + SourceAbsoluteWord: 0xf000, SourceAbsoluteLong: 0x12346200)); + var ea = fixture.Resolve(source.Form, true); + for (var n = 0; n < words.Count; n++) m.InitializePhysical(0x1000 + (uint)n * 2, words[n], 2); + var next = fixture.NextPc; + m.InitializePhysical(next, 0x7e55, 2); // MOVEQ #55,D7, no flow trace. + m.InitializePhysical(next + 2, 0x6002, 2); // BRA.s next+6, must flow trace under T0. + m.InitializePhysical(next + 4, 0x4afc, 2); m.InitializePhysical(next + 6, 0x4e71, 2); + // With a null PC displacement, legal operands/pointers can be the + // extension words themselves. Preserve the instruction bytes and + // derive those operand values from the test-owned encoded stream. + if (fixture.SourcePointerTarget is { } pointerTarget) ea = pointerTarget(); + var values = (uint[])(width == 2 ? WordValues : LongValues).Clone(); + for (var n = 0; n < 4; n++) + { + var at = unchecked(ea + (uint)(n * width)); + for (var lane = 0; lane < width; lane++) + { + var address = unchecked(at + (uint)lane); + if (address >= 0x1000 && address < next + 8) continue; + m.InitializePhysical(address, (values[n] >> ((width - lane - 1) * 8)) & 255, 1); + } + var raw = m.PeekPhysical(at, width); + values[n] = width == 2 ? unchecked((uint)(int)(short)raw) : raw; + } + m.InitializePhysical(Vbr + 8, Handler, 4); m.InitializePhysical(Vbr + 36, TraceHandler, 4); + m.InitializePhysical(Handler, 0x4e73, 2); m.InitializePhysical(Handler + 2, 0x4e71, 2); + // Real trace handler clears stacked T1/T0 and returns to the saved PC. + m.InitializePhysical(TraceHandler, 0x0257, 2); m.InitializePhysical(TraceHandler + 2, 0x3fff, 2); + m.InitializePhysical(TraceHandler + 4, 0x4e73, 2); m.InitializePhysical(TraceHandler + 6, 0x4e71, 2); + m.Start(); SetInitialStacks(m, pointers, sr); m.Core.State.VectorBaseRegister = Vbr; + var sequence = m.Core.State.ExceptionSequence; + var e = ArchitecturalExpectation.Capture(m); + for (var n = 0; n < transfer; n++) SetLoaded(e, n, values[n]); + var exceptionBank = M68040StackFixture.ExceptionBank(sr); + var frame = pointers[exceptionBank] - 60; + pointers[exceptionBank] = frame; + M68040StackFixture.SetStacks(e, pointers, (ushort)((sr | 0x2000) & ~0xc000)); + e.Pc = Handler; e.ExceptionVector = 2; + e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence + 1); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, 0x1000); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, sr); + e.ControlChecks["bypass cleared"] = (s => s.M68040Mmu.BypassTranslation ? 1u : 0u, 0); + for (uint n = 8; n < 60; n++) e.MemoryMasks[frame + n] = 0; + Define(m, e, frame, sr, 2); Define(m, e, frame + 2, 0x1000, 4); + Define(m, e, frame + 6, 0x7008, 2); Define(m, e, frame + 8, ea, 4); + Define(m, e, frame + 12, (uint)(0x1100 | (width == 2 ? 0x40 : 0) | ((sr & 0x2000) == 0 ? 1 : 5)), 2); + e.MemoryMasks[frame + 13] = 0x7f; + foreach (var offset in new uint[] { 14, 16, 18 }) + { + e.Write(frame + offset, 0, 2, m.Model); + e.MemoryMasks[frame + offset + 1] = 0x80; + } + Define(m, e, frame + 20, unchecked(ea + (uint)(transfer * width)), 4); + bus.Accesses.Clear(); bus.Arm(unchecked(ea + (uint)(transfer * width + faultByte)), M68kBusAccessKind.CpuDataRead); + phase = "entry"; Step(m, e, batch); + if (bus.Rejected.Count != 1 || bus.Rejected[0].Width != width) throw new InvalidOperationException("Wrong original-width read rejection"); + pointers[exceptionBank] += 60; M68040StackFixture.SetStacks(e, pointers, sr); + e.Pc = 0x1000; + phase = "handler-RTE"; Step(m, e, batch); + for (var n = 0; n < 4; n++) SetLoaded(e, n, values[n]); + if (source.Form.Mode == 3) + { + var end = unchecked(ea + (uint)(4 * width)); e.A[source.Form.Register] = end; + if (source.Form.Register == 7) pointers[M68040StackFixture.PhysicalBank(bank)] = end; + } + M68040StackFixture.SetStacks(e, pointers, sr); e.Pc = next; + var resumedSr = sr; + if (trace == 0x8000) ExpectTrace(m, e, pointers, next, 0x1000, sr, sequence + 2); + phase = "resumed-MOVEM"; Step(m, e, batch); + for (var n = 0; n < 4; n++) + { + var address = unchecked(ea + (uint)(n * width)); + var count = bus.Accesses.Count(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address == address && a.Width == width); + if (count != (n < transfer ? 2 : 1)) throw new InvalidOperationException($"Transfer {n}: repeated-read count {count}"); + } + if (fixture.SourcePointerAddress is { } pointer && + bus.Accesses.Count(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address == pointer && a.Width == 4) != 1) + throw new InvalidOperationException("Indirect pointer repeated on CM recovery"); + if (bus.Rejected.Count != 1) throw new InvalidOperationException("Faulting read automatically retried"); + if (trace == 0x8000) { phase = "trace-handler"; ClearTraceAndReturn(m, e, pointers, sr, next, batch); resumedSr &= 0x3fff; } + e.D[7] = 0x55; e.Sr = (ushort)(resumedSr & 0xfff0); e.Pc = next + 2; + phase = "following-MOVEQ"; Step(m, e, batch); + if (trace == 0x4000) + { + var branchSr = e.Sr; ExpectTrace(m, e, pointers, next + 6, next + 2, branchSr, sequence + 2); + phase = "following-flow-trace"; Step(m, e, batch); + phase = "flow-trace-handler"; ClearTraceAndReturn(m, e, pointers, branchSr, next + 6, batch); + e.Pc = next + 8; phase = "following-NOP"; Step(m, e, batch); + } + report.Record(id, "passing", null); + } + catch (NotSupportedException ex) { report.Record(id, "unsupported", phase + ": " + ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", phase + ": " + ex.Message); } + } + + private static void SetLoaded(ArchitecturalExpectation e, int transfer, uint value) + { if (transfer < 2) e.D[transfer] = value; else e.A[transfer - 2] = value; } + private static void SetInitialStacks(SyntheticMachine m, Dictionary pointers, ushort sr) + { + m.Core.State.SetUserStackPointer(pointers["user"]); m.Core.State.SetInterruptStackPointer(pointers["ISP"]); + m.Core.State.SetMasterStackPointer(pointers["MSP"]); m.Core.State.StatusRegister = sr; + } + private static void Define(SyntheticMachine m, ArchitecturalExpectation e, uint at, uint value, int width) + { e.Write(at, value, width, m.Model); for (uint n = 0; n < width; n++) e.MemoryMasks[at + n] = 255; } + private static void ExpectTrace(SyntheticMachine m, ArchitecturalExpectation e, Dictionary pointers, + uint next, uint origin, ushort sr, uint sequence) + { + var bank = M68040StackFixture.ExceptionBank(sr); var frame = pointers[bank] - 12; pointers[bank] = frame; + M68040StackFixture.SetStacks(e, pointers, (ushort)((sr | 0x2000) & ~0xc000)); + e.Pc = TraceHandler; e.ExceptionVector = 9; + e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, next); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, sr); + Define(m, e, frame, sr, 2); Define(m, e, frame + 2, next, 4); + Define(m, e, frame + 6, 0x2024, 2); Define(m, e, frame + 8, origin, 4); + } + private static void ClearTraceAndReturn(SyntheticMachine m, ArchitecturalExpectation e, + Dictionary pointers, ushort sr, uint pc, bool batch) + { + var bank = M68040StackFixture.ExceptionBank(sr); var frame = pointers[bank]; + Define(m, e, frame, (uint)(sr & 0x3fff), 2); e.Sr &= 0xfff0; e.Pc = TraceHandler + 4; + Step(m, e, batch); + pointers[bank] += 12; M68040StackFixture.SetStacks(e, pointers, (ushort)(sr & 0x3fff)); e.Pc = pc; + Step(m, e, batch); + } + private static void Step(SyntheticMachine m, ArchitecturalExpectation e, bool batch) + { + if (batch) + { + var boundary = new Boundary(); + var count = ((IM68kBatchCore)m.Core).ExecuteInstructions(1, m.Core.State.Cycles + 10000, boundary); + if (count != 1 || boundary.Before != 1 || boundary.After != 1) throw new InvalidOperationException("Batch count/callbacks differ"); + } + else m.Core.ExecuteInstruction(); + if (e.Verify(m) is { } mismatch) throw new InvalidOperationException(mismatch); + } + private sealed class Boundary : IM68kInstructionBoundary + { + public int Before, After; + public bool BeforeInstruction() { Before++; return true; } + public void AfterInstruction(long previousCycle, long currentCycle) => After++; + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 1229f998..6ace1567 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -7317,3 +7317,65 @@ Other models' restoration gaps, unresolved software-reference disagreements, the original Basic non-passing rows and regression consolidation remain required. No public release or regression retirement is included. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## Executed 040 MOVEM read recovery — 2026-10-07 + +The selected software recovery qualification now executes a complete fault, +handler RTE, resumed MOVEM and following-instruction program. Expectations use +MC68040UM 8.2.6 and 8.4.6.2/7: CM retains the calculated EA; resumed MOVEM repeats +preceding operand transfers without recalculating that EA; an aborted +instruction's trace is deferred until completion. This is architectural software +qualification with translation and instruction caching disabled, not physical +pipeline or cache timing qualification. + +Run the complete selected inventory with: + +```powershell +./scripts/test-copper68k-040-movem-read-recovery.ps1 -OutputDirectory artifacts/040-movem-recovery-audit +``` + +`-ValidateReportsOnly` verifies frozen source/report/assembly/TRX identities, +the exact five-execution roster and independently expanded combination keys and +weights. Both routes pass **114,912 matrix programs plus 72 fixed source programs**: +**229,968 whole programs**, zero mismatches, unsupported executions or missing +selected combinations. Logical counts represent complete programs, not individual +instructions or xUnit tests. Per route the matrix comprises: + +- 864 source-encoding programs: all 36 legal memory-source encodings, W/L, + four rejected transfer positions and every byte of the rejected access. +- 12,672 structure programs: 66 legal full-format structures, four signed/scaled + data/address-index variants, address-register/PC bases, W/L and every rejection + position/byte. +- 101,376 status programs: eleven canonical source forms, four stack/SR profiles, + T0/T1/off, all 32 CCR states, W/L and every rejection position/byte. + +The transfer list is deliberately D0/D1/A0/A1, exercising overwritten bases and +indexes. Registers, stack banks, saved SR/PC, defined format-7 fields, partial +effects, exact next PC, surrounding memory, operand-read repetition, pointer-read +count and following MOVEQ/branch trace sentinels are checked. Two literal fixture +examples additionally verify PC self-reference encodings. Null PC displacement +can place an operand or pointer in the instruction stream; fixture setup preserves +those bytes and derives their values from the test-owned encoded stream. + +Evidence is retained in `audits/MovemReadRecoveryAcceptance` under the restoration +temporary root. Six copied-evidence controls reject missing source/report, +changed weight, foreign key and empty selection/execution. Discarding calculated +EA metadata in an isolated shared-fallback source copy produces **31 expected +frame-EA mismatches per route**, while 41 programs per route still pass through +unmutated execution paths. The earlier selected 122,880-case read-entry gate also +passes again against the current sources. Failed provisional fixture runs remain +separate historical evidence. + +The existing MOVE extension/alias, MOVEM mask/base-alias and LEA/PEA fixture +checks pass across all eight model/profile selections: 25 executions, 24 reports +and 145,816 logical scenarios, retained in `audits/MovemRecoverySharedFixtures`. + +No production CPU code changes or new package are part of this extension. +The prior full CPU/consumer results above qualify the unchanged production +implementation; they are not claimed as a fresh full-suite run of these new tests. +The ordinary required 707-report inventory remains separate from this focused +reference command. Other MOVEM masks, MOVEM writes, pointer-fetch faults, nested +recovery, enabled MMU/cache and physical timing still need their applicable +qualification. The broad 480-case remaining gate and the other model/reference/ +consolidation work remain open. Milestone 6 remains **in progress**, +`roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index ba85d703..6e7b91d8 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1922,6 +1922,17 @@ provenance, internal restoration, data/writeback/context transfer, other models' restoration and broader independent qualification/consolidation remain required. Milestone 6 remains **in progress**, `roadmapComplete=false`. +On 2026-10-07 the executed 040 MOVEM read-recovery gate passes 229,968 complete +fault/RTE/resumption/sentinel programs across scalar and batch execution. It +covers every legal memory-source encoding, all legal full-format structures in +selected index variants, transfer-byte faults and canonical stack/CCR/trace +profiles, using the D0/D1/A0/A1 list to expose base/index aliases. Six evidence +integrity controls reject their intended corruptions; dropping saved-EA metadata +in an isolated shared fallback causes 62 expected frame-EA mismatches. The prior +read-entry gate passes again with current test sources. This test-only extension +does not close the broad remaining gate, other models or consolidation and does +not authorize a release. See [executed MOVEM recovery](COPPER68K_REFERENCE_QUALIFICATION.md#executed-040-movem-read-recovery--2026-10-07). + The three maintained software-trace mutations are detected in each complete scalar/batch canonical/chained report with their intended semantic diagnostic. Production bytes are restored exactly and the clean implementation rebuilds diff --git a/scripts/test-copper68k-040-movem-read-recovery.ps1 b/scripts/test-copper68k-040-movem-read-recovery.ps1 new file mode 100644 index 00000000..7434a429 --- /dev/null +++ b/scripts/test-copper68k-040-movem-read-recovery.ps1 @@ -0,0 +1,87 @@ +#requires -Version 7.0 +[CmdletBinding()] +param([string] $OutputDirectory = 'artifacts/040-movem-read-recovery', [switch] $ValidateReportsOnly) +$ErrorActionPreference = 'Stop' +$repo = Split-Path -Parent $PSScriptRoot +$output = [IO.Path]::GetFullPath($OutputDirectory, $repo) +function Hash([string] $Path) { (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() } +$manifestPath = Join-Path $output 'inputs.json' +$sources = @(@(& git -C $repo ls-files 'Copper68k/*.cs' 'Copper68k/*.csproj' 'Copper68k.Tests/*.cs' 'Copper68k.Tests/*.csproj') + @('scripts/test-copper68k-040-movem-read-recovery.ps1','Copper68k.Tests/Synthetic/SyntheticM68040MovemReadRecoveryTests.cs') | Sort-Object -Unique) +$reports = @('witness-scalar','witness-batch','discovery-scalar','discovery-batch') | ForEach-Object { "68040-movem-read-recovery-$_.json" } +$evidence = @('audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll') + $reports +if (-not $ValidateReportsOnly) { + if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } + New-Item -ItemType Directory -Path $output | Out-Null + $identities = @($sources | ForEach-Object { @{path=$_;sha256=(Hash (Join-Path $repo $_))} }) + $settings = @{ COPPER68K_RUN_040_MOVEM_READ_RECOVERY='1'; COPPER68K_SYNTHETIC_REPORT_DIR=$output } + $saved = @{} + try { + foreach ($key in $settings.Keys) { $saved[$key]=[Environment]::GetEnvironmentVariable($key); [Environment]::SetEnvironmentVariable($key,$settings[$key]) } + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --artifacts-path (Join-Path $output 'build') --filter 'FullyQualifiedName~SyntheticM68040MovemReadRecoveryTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output *> (Join-Path $output 'execution.log') + $testExit=$LASTEXITCODE + } finally { foreach ($key in $saved.Keys) { [Environment]::SetEnvironmentVariable($key,$saved[$key]) } } + @{ schema=1; reference='MC68040UM 8.2.6, 8.4.6.2/7'; referenceUrl='https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf'; sourceCommit=(& git -C $repo rev-parse HEAD); testExit=$testExit; sources=$identities; evidence=@($evidence | ForEach-Object { @{path=$_;sha256=(Hash (Join-Path $output $_))} }) } | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $manifestPath +} +$manifest=Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json +if ($manifest.schema -ne 1 -or $manifest.sources.Count -ne $sources.Count -or + @(Compare-Object ($manifest.sources.path | Sort-Object) $sources).Count -ne 0 -or + $manifest.evidence.Count -ne $evidence.Count -or @(Compare-Object ($manifest.evidence.path | Sort-Object) ($evidence | Sort-Object)).Count -ne 0) { throw 'Missing recovery identities' } +foreach ($source in $manifest.sources) { if ((Hash (Join-Path $repo $source.path)) -ne $source.sha256) { throw "Changed recovery source: $($source.path)" } } +foreach ($item in $manifest.evidence) { if ((Hash (Join-Path $output $item.path)) -ne $item.sha256) { throw "Changed recovery evidence: $($item.path)" } } +[xml]$trx=Get-Content (Join-Path $output 'audit.trx') -Raw +$tests=@($trx.TestRun.Results.UnitTestResult) +$prefix='Copper68k.Tests.Synthetic.SyntheticM68040MovemReadRecoveryTests.' +$names=@('ScalarRecoveryMatrix','BatchRecoveryMatrix','FixedLegalSourceRecoveryWitnesses(batch: False)','FixedLegalSourceRecoveryWitnesses(batch: True)','FixedPcSelfReferenceEncodings') | ForEach-Object { $prefix+$_ } +if ($tests.Count -ne 5 -or @(Compare-Object ($tests.testName | Sort-Object) ($names | Sort-Object)).Count -ne 0 -or @($tests.testId | Sort-Object -Unique).Count -ne 5) { throw 'Incomplete recovery execution roster' } +# Expand the architectural inventory independently from the C# generator. +function SourceId([int] $Mode, [int] $Register, [string] $Index='brief-default') { "mode=$Mode/reg=$Register/index=$Index" } +$forms=@(foreach ($mode in @(2,3,5,6)) { foreach ($reg in 0..7) { SourceId $mode $reg } }; foreach ($reg in 0..3) { SourceId 7 $reg }) +$full=@(foreach ($bs in @('False','True')) { foreach ($is in @('False','True')) { foreach ($bd in 1..3) { foreach ($iis in @(0,1,2,3,5,6,7)) { + if ($is -eq 'True' -and $iis -ge 5) { continue } + "full/bs=$bs/is=$is/bd=$bd/iis=$iis" +} } } }) +if ($forms.Count -ne 36 -or $full.Count -ne 66) { throw 'Wrong independent source inventory' } +$canonical=@(SourceId 2 0; SourceId 3 0; SourceId 3 7; SourceId 5 0; SourceId 6 0; SourceId 7 0; SourceId 7 1; SourceId 7 2; SourceId 7 3 + SourceId 6 0 'full/bs=False/is=False/bd=2/iis=2/ix=D1/L/scale=1' + SourceId 7 3 'full/bs=False/is=False/bd=3/iis=7/ix=D1/L/scale=1') +function AddKeys($Map, [string] $Cohort, [string[]] $Forms, [string[]] $Banks, [string[]] $Traces, [bool] $Witness) { + foreach ($form in $Forms) { foreach ($width in @(2,4)) { foreach ($bank in $Banks) { foreach ($trace in $Traces) { + $transfers=if ($Witness) { @(3) } else { @(0,1,2,3) } + $bytes=if ($Witness) { @($width-1) } else { @(0..($width-1)) } + foreach ($transfer in $transfers) { foreach ($byte in $bytes) { + $key="68040/MOVEM/read-recovery/$Cohort/size=$width/$form/bank=$bank/T=$trace/transfer=$transfer/byte=$byte" + if ($Map.ContainsKey($key)) { throw "Duplicate independent combination: $key" } + $Map[$key]=if ($Cohort -eq 'status') { 32 } else { 1 } + } } + } } } } +} +$witness=@{}; $required=@{} +AddKeys $witness 'witness' $forms @('ISP') @('0000') $true +AddKeys $required 'opcode' $forms @('ISP') @('0000') $false +$structures=@(foreach ($spec in $full) { foreach ($ix in @('D1/W/scale=1','D1/L/scale=1','D1/W/scale=8','A0/W/scale=4')) { + SourceId 6 1 "$spec/ix=$ix"; SourceId 7 3 "$spec/ix=$ix" +} }) +AddKeys $required 'structure' $structures @('ISP') @('0000') $false +AddKeys $required 'status' $canonical @('user','user-M','ISP','MSP') @('0000','8000','4000') $false +$rows=@() +foreach ($name in @('witness-scalar','witness-batch','discovery-scalar','discovery-batch')) { + $report=Get-Content (Join-Path $output "68040-movem-read-recovery-$name.json") -Raw | ConvertFrom-Json + $inventory=if ($name.StartsWith('witness')) { $witness } else { $required } + $expected=if ($name.StartsWith('witness')) { 72 } else { 114912 } + if ($report.schema -ne 1 -or $report.model -ne '68040' -or $report.group -ne "movem-read-recovery-$name" -or $report.logicalCases -ne $expected -or $report.xunitBatches -ne 1 -or @($report.combinations.psobject.Properties).Count -ne $inventory.Count) { throw "Incomplete recovery report: $name" } + $sum=@{passing=0;mismatching=0;unsupported=0;untested=0} + foreach ($combination in $report.combinations.psobject.Properties) { + if (-not $inventory.ContainsKey($combination.Name)) { throw "Foreign recovery combination: $($combination.Name)" } + $weight=0 + foreach ($status in $combination.Value.psobject.Properties) { + if (-not $sum.ContainsKey($status.Name) -or $status.Value -le 0) { throw 'Invalid recovery status/weight' } + $sum[$status.Name]+=$status.Value; $weight+=$status.Value + } + if ($weight -ne $inventory[$combination.Name]) { throw "Changed recovery combination weight: $($combination.Name)" } + } + foreach ($status in $sum.Keys) { if ($sum[$status] -ne $report.counts.$status) { throw "Recovery totals disagree: $name/$status" } } + $rows+=@{group=$name;wholePrograms=$expected;combinations=$inventory.Count;counts=$sum} +} +@{schema=1;rows=$rows;wholePrograms=229968;roadmapComplete=$false;inputManifestSha256=(Hash $manifestPath)} | ConvertTo-Json -Depth 10 | Set-Content (Join-Path $output 'verification.json') +if (@($tests | Where-Object outcome -ne 'Passed').Count -gt 0 -or $manifest.testExit -ne 0 -or @($rows | Where-Object { $_.counts.mismatching + $_.counts.unsupported + $_.counts.untested -gt 0 }).Count -gt 0) { throw "040 MOVEM recovery fails. Retained evidence: $output" } +Write-Host '040 MOVEM recovery passes 229,968 whole programs across scalar/batch execution; milestone 6 remains open.' From 4863f449816115b536b21f9faa7c73539ecaabcb Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 01:08:53 +0300 Subject: [PATCH 081/211] Qualify MOVEM indirect-pointer fault restart --- .../SyntheticM68040MovemReadRecoveryTests.cs | 71 +++++++++++++++++-- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 47 ++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 10 +++ ...test-copper68k-040-movem-read-recovery.ps1 | 46 +++++++++--- 4 files changed, 157 insertions(+), 17 deletions(-) diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040MovemReadRecoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040MovemReadRecoveryTests.cs index 1bf6a6d3..4384a824 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040MovemReadRecoveryTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040MovemReadRecoveryTests.cs @@ -53,6 +53,54 @@ public void FixedLegalSourceRecoveryWitnesses(bool batch) [EnvironmentFact(Enable, "qualify actual MOVEM read faults and software RTE recovery"), Trait("Suite", "ReferenceDiscovery")] public void BatchRecoveryMatrix() => Audit(true); + [Theory, Trait("Suite", "ReferenceDiscovery")] + [InlineData(false)] + [InlineData(true)] + public void FixedIndirectPointerFaultWitnesses(bool batch) + { + var report = new CoverageBatch("68040", "movem-pointer-recovery-witness-" + Route(batch)); + foreach (var source in PointerSources()) + foreach (var width in new[] { 2, 4 }) + Case(report, batch, "pointer-witness", source, width, 0, 3, "ISP", 0, 31, pointerFault: true); + report.Complete(output); + } + + [EnvironmentFact(Enable, "qualify pre-EA indirect-pointer faults and normal RTE restart"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarPointerRecoveryMatrix() => AuditPointers(false); + + [EnvironmentFact(Enable, "qualify pre-EA indirect-pointer faults and normal RTE restart"), Trait("Suite", "ReferenceDiscovery")] + public void BatchPointerRecoveryMatrix() => AuditPointers(true); + + private static IEnumerable PointerSources() + { + foreach (var form in new[] { new OperandForm(6, 0), new OperandForm(7, 3) }) + foreach (var indirect in new[] { 2, 7 }) + yield return new(form, new(Full: true, IndexRegister: 1, LongIndex: true, BaseSize: 2, Indirect: indirect)); + } + + private void AuditPointers(bool batch) + { + var report = new CoverageBatch("68040", "movem-pointer-recovery-discovery-" + Route(batch)); + foreach (var spec in IndexFixture.FullStructures().Where(x => x.Indirect != 0)) + foreach (var index in new[] { + spec with { IndexRegister = 1, LongIndex = false, Scale = 0 }, + spec with { IndexRegister = 1, LongIndex = true, Scale = 0 }, + spec with { IndexRegister = 1, LongIndex = false, Scale = 3 }, + spec with { AddressIndex = true, IndexRegister = 0, LongIndex = false, Scale = 2 } }) + foreach (var form in new[] { new OperandForm(6, 1), new OperandForm(7, 3) }) + foreach (var width in new[] { 2, 4 }) + for (var faultByte = 0; faultByte < 4; faultByte++) + Case(report, batch, "pointer-structure", new(form, index), width, 0, faultByte, "ISP", 0, 31, pointerFault: true); + foreach (var source in PointerSources()) + foreach (var width in new[] { 2, 4 }) + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var trace in new ushort[] { 0, 0x8000, 0x4000 }) + for (var ccr = 0; ccr < 32; ccr++) + for (var faultByte = 0; faultByte < 4; faultByte++) + Case(report, batch, "pointer-status", source, width, 0, faultByte, bank, trace, ccr, pointerFault: true); + report.Complete(output); + } + private void Audit(bool batch) { var report = new CoverageBatch("68040", "movem-read-recovery-discovery-" + Route(batch)); @@ -105,7 +153,7 @@ private static IEnumerable Sources() } private static void Case(CoverageBatch report, bool batch, string cohort, Source source, - int width, int transfer, int faultByte, string bank, ushort trace, int ccr) + int width, int transfer, int faultByte, string bank, ushort trace, int ccr, bool pointerFault = false) { var opcode = (ushort)(0x4c80 | (width == 4 ? 0x40 : 0) | source.Form.Mode << 3 | source.Form.Register); var id = $"68040/MOVEM/read-recovery/{cohort}/size={width}/{source.Id}/bank={bank}/T={trace:X4}/transfer={transfer}/byte={faultByte}/op={opcode:X4}/ccr={ccr:X2}"; @@ -170,18 +218,27 @@ private static void Case(CoverageBatch report, bool batch, string cohort, Source e.ControlChecks["bypass cleared"] = (s => s.M68040Mmu.BypassTranslation ? 1u : 0u, 0); for (uint n = 8; n < 60; n++) e.MemoryMasks[frame + n] = 0; Define(m, e, frame, sr, 2); Define(m, e, frame + 2, 0x1000, 4); - Define(m, e, frame + 6, 0x7008, 2); Define(m, e, frame + 8, ea, 4); - Define(m, e, frame + 12, (uint)(0x1100 | (width == 2 ? 0x40 : 0) | ((sr & 0x2000) == 0 ? 1 : 5)), 2); + Define(m, e, frame + 6, 0x7008, 2); + if (!pointerFault) Define(m, e, frame + 8, ea, 4); + var accessWidth = pointerFault ? 4 : width; + var faultAddress = pointerFault + ? fixture.SourcePointerAddress ?? throw new InvalidOperationException("Missing indirect pointer fixture") + : unchecked(ea + (uint)(transfer * width)); + Define(m, e, frame + 12, (uint)(0x0100 | (pointerFault ? 0 : 0x1000) | + (accessWidth == 2 ? 0x40 : 0) | ((sr & 0x2000) == 0 ? 1 : 5)), 2); e.MemoryMasks[frame + 13] = 0x7f; foreach (var offset in new uint[] { 14, 16, 18 }) { e.Write(frame + offset, 0, 2, m.Model); e.MemoryMasks[frame + offset + 1] = 0x80; } - Define(m, e, frame + 20, unchecked(ea + (uint)(transfer * width)), 4); - bus.Accesses.Clear(); bus.Arm(unchecked(ea + (uint)(transfer * width + faultByte)), M68kBusAccessKind.CpuDataRead); + Define(m, e, frame + 20, faultAddress, 4); + bus.Accesses.Clear(); bus.Arm(unchecked(faultAddress + (uint)faultByte), M68kBusAccessKind.CpuDataRead); phase = "entry"; Step(m, e, batch); - if (bus.Rejected.Count != 1 || bus.Rejected[0].Width != width) throw new InvalidOperationException("Wrong original-width read rejection"); + if (bus.Rejected.Count != 1 || bus.Rejected[0].Width != accessWidth) throw new InvalidOperationException("Wrong original-width read rejection"); + if (pointerFault && bus.Accesses.Any(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && + Enumerable.Range(0, 4).Any(n => a.Address == unchecked(ea + (uint)(n * width))))) + throw new InvalidOperationException("MOVEM operand accessed before pointer recovery"); pointers[exceptionBank] += 60; M68040StackFixture.SetStacks(e, pointers, sr); e.Pc = 0x1000; phase = "handler-RTE"; Step(m, e, batch); @@ -203,7 +260,7 @@ private static void Case(CoverageBatch report, bool batch, string cohort, Source } if (fixture.SourcePointerAddress is { } pointer && bus.Accesses.Count(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address == pointer && a.Width == 4) != 1) - throw new InvalidOperationException("Indirect pointer repeated on CM recovery"); + throw new InvalidOperationException(pointerFault ? "Indirect pointer not resolved once after RTE restart" : "Indirect pointer repeated on CM recovery"); if (bus.Rejected.Count != 1) throw new InvalidOperationException("Faulting read automatically retried"); if (trace == 0x8000) { phase = "trace-handler"; ClearTraceAndReturn(m, e, pointers, sr, next, batch); resumedSr &= 0x3fff; } e.D[7] = 0x55; e.Sr = (ushort)(resumedSr & 0xfff0); e.Pc = next + 2; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 6ace1567..7aa4c66b 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -7379,3 +7379,50 @@ recovery, enabled MMU/cache and physical timing still need their applicable qualification. The broad 480-case remaining gate and the other model/reference/ consolidation work remain open. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +### Pre-EA MOVEM indirect-pointer faults — 2026-10-07 + +The same maintained recovery command now also requires the pre-EA fault path. +A full-format indirect-pointer read faults before any MOVEM operand transfer. +The selected expectation is format 7 with CM clear, a long read's attributes, +the pointer address in FA, no valid pending writebacks and the original PC/SR. +EA is undefined with no continuation bits and is deliberately not compared. +RTE returns to the original instruction; address calculation resolves the pointer +once successfully, then all four operand transfers complete. T1 is deferred to +resumed completion; T0 is checked by the following branch sentinel. Partial +register/stack/memory state, exact instruction length, original-width rejection +and absence of operand reads before pointer recovery are verified. + +This distinguishes a fault during address calculation from a fault after the +calculated EA has been retained for MOVEM. The expectation derives from +MC68040UM 8.4.6.2/7. Source-level corroboration uses pinned generator +`025b999239800357e95065fe5b9a15ea5b300fa7`: `genmovemel` emits `genamode` before +`movem_mmu040`, which emits the runtime continuation flag and saved EA after +address calculation. This inspection is not an executed native fault oracle or +hardware observation; its source hash and caveat are recorded separately. + +Each route adds **15,744 matrix programs plus eight fixed programs**: + +- 3,456 structure programs: 54 legal indirect full-format structures, four + signed/scaled index variants, address-register/PC bases, W/L MOVEM and each + byte of the four-byte pointer read. +- 12,288 status programs: pre/post indirection with address-register/PC bases, + W/L, four stack/SR profiles, T0/T1/off, all 32 CCR states and every pointer byte. + +Together with the unchanged selected operand-fault coverage, the command requires +**261,472 passing whole programs / eight reports / nine executions**. It rejects +missing pointer reports or test selections rather than silently omitting them. +Evidence is retained in `audits/MovemPointerRecoveryAcceptance`; six corruption +controls pass in `MovemPointerRecoveryIntegrity`. An isolated mutation which sets +CM before EA calculation makes all **16 fixed pointer programs** fail at the SSW +continuation assertion, retained in `mutations/PointerFaultPrematureCm`. + +These are transient physical read failures with the existing one-shot bus fixture; +the handler executes RTE, without editing the pointer or completing a writeback. +The transfer list remains D0/D1/A0/A1. Other transfer masks, pointer-editing handlers, +nested recovery, MOVEM writes, enabled MMU/cache and physical timing are not +qualified by this slice. Production CPU/package bytes are unchanged. The previous +full-suite and consumer results remain historical evidence for that production +implementation, not a fresh run of these new tests. The broad remaining gate, +other model/reference disagreements and consolidation remain open. Milestone 6 +remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 6e7b91d8..81e1c014 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1933,6 +1933,16 @@ read-entry gate passes again with current test sources. This test-only extension does not close the broad remaining gate, other models or consolidation and does not authorize a release. See [executed MOVEM recovery](COPPER68K_REFERENCE_QUALIFICATION.md#executed-040-movem-read-recovery--2026-10-07). +The same gate subsequently adds pre-EA full-format pointer faults: 31,504 new +whole programs verify CM-clear format-7 entry, no preceding operand transfers, +normal RTE restart, pointer resolution and restored trace behavior. All 54 legal +indirect structures and canonical stack/CCR/trace profiles are covered in the +selected variants. The combined command requires 261,472 passing programs, +eight reports and nine executions. Six integrity controls pass; premature CM +in an isolated source copy makes all sixteen fixed pointer programs fail. +No production or package change is needed. The full milestone remains open; +see [pre-EA pointer faults](COPPER68K_REFERENCE_QUALIFICATION.md#pre-ea-movem-indirect-pointer-faults--2026-10-07). + The three maintained software-trace mutations are detected in each complete scalar/batch canonical/chained report with their intended semantic diagnostic. Production bytes are restored exactly and the clean implementation rebuilds diff --git a/scripts/test-copper68k-040-movem-read-recovery.ps1 b/scripts/test-copper68k-040-movem-read-recovery.ps1 index 7434a429..f71c50a0 100644 --- a/scripts/test-copper68k-040-movem-read-recovery.ps1 +++ b/scripts/test-copper68k-040-movem-read-recovery.ps1 @@ -7,7 +7,7 @@ $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) function Hash([string] $Path) { (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() } $manifestPath = Join-Path $output 'inputs.json' $sources = @(@(& git -C $repo ls-files 'Copper68k/*.cs' 'Copper68k/*.csproj' 'Copper68k.Tests/*.cs' 'Copper68k.Tests/*.csproj') + @('scripts/test-copper68k-040-movem-read-recovery.ps1','Copper68k.Tests/Synthetic/SyntheticM68040MovemReadRecoveryTests.cs') | Sort-Object -Unique) -$reports = @('witness-scalar','witness-batch','discovery-scalar','discovery-batch') | ForEach-Object { "68040-movem-read-recovery-$_.json" } +$reports = @(foreach ($kind in @('read','pointer')) { foreach ($name in @('witness-scalar','witness-batch','discovery-scalar','discovery-batch')) { "68040-movem-$kind-recovery-$name.json" } }) $evidence = @('audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll') + $reports if (-not $ValidateReportsOnly) { if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } @@ -31,8 +31,9 @@ foreach ($item in $manifest.evidence) { if ((Hash (Join-Path $output $item.path) [xml]$trx=Get-Content (Join-Path $output 'audit.trx') -Raw $tests=@($trx.TestRun.Results.UnitTestResult) $prefix='Copper68k.Tests.Synthetic.SyntheticM68040MovemReadRecoveryTests.' -$names=@('ScalarRecoveryMatrix','BatchRecoveryMatrix','FixedLegalSourceRecoveryWitnesses(batch: False)','FixedLegalSourceRecoveryWitnesses(batch: True)','FixedPcSelfReferenceEncodings') | ForEach-Object { $prefix+$_ } -if ($tests.Count -ne 5 -or @(Compare-Object ($tests.testName | Sort-Object) ($names | Sort-Object)).Count -ne 0 -or @($tests.testId | Sort-Object -Unique).Count -ne 5) { throw 'Incomplete recovery execution roster' } +$names=@('ScalarRecoveryMatrix','BatchRecoveryMatrix','FixedLegalSourceRecoveryWitnesses(batch: False)','FixedLegalSourceRecoveryWitnesses(batch: True)','FixedPcSelfReferenceEncodings', + 'ScalarPointerRecoveryMatrix','BatchPointerRecoveryMatrix','FixedIndirectPointerFaultWitnesses(batch: False)','FixedIndirectPointerFaultWitnesses(batch: True)') | ForEach-Object { $prefix+$_ } +if ($tests.Count -ne 9 -or @(Compare-Object ($tests.testName | Sort-Object) ($names | Sort-Object)).Count -ne 0 -or @($tests.testId | Sort-Object -Unique).Count -ne 9) { throw 'Incomplete recovery execution roster' } # Expand the architectural inventory independently from the C# generator. function SourceId([int] $Mode, [int] $Register, [string] $Index='brief-default') { "mode=$Mode/reg=$Register/index=$Index" } $forms=@(foreach ($mode in @(2,3,5,6)) { foreach ($reg in 0..7) { SourceId $mode $reg } }; foreach ($reg in 0..3) { SourceId 7 $reg }) @@ -63,12 +64,36 @@ $structures=@(foreach ($spec in $full) { foreach ($ix in @('D1/W/scale=1','D1/L/ } }) AddKeys $required 'structure' $structures @('ISP') @('0000') $false AddKeys $required 'status' $canonical @('user','user-M','ISP','MSP') @('0000','8000','4000') $false +$pointerCanonical=@(foreach ($form in @(@(6,0),@(7,3))) { foreach ($iis in @(2,7)) { SourceId $form[0] $form[1] "full/bs=False/is=False/bd=2/iis=$iis/ix=D1/L/scale=1" } }) +$pointerStructures=@(foreach ($spec in $full | Where-Object { -not $_.EndsWith('/iis=0') }) { foreach ($ix in @('D1/W/scale=1','D1/L/scale=1','D1/W/scale=8','A0/W/scale=4')) { + SourceId 6 1 "$spec/ix=$ix"; SourceId 7 3 "$spec/ix=$ix" +} }) +function AddPointerKeys($Map, [string] $Cohort, [string[]] $Forms, [string[]] $Banks, [string[]] $Traces, [bool] $Witness) { + foreach ($form in $Forms) { foreach ($width in @(2,4)) { foreach ($bank in $Banks) { foreach ($trace in $Traces) { + $bytes=if ($Witness) { @(3) } else { @(0,1,2,3) } + foreach ($byte in $bytes) { + $key="68040/MOVEM/read-recovery/$Cohort/size=$width/$form/bank=$bank/T=$trace/transfer=0/byte=$byte" + if ($Map.ContainsKey($key)) { throw "Duplicate independent combination: $key" } + $Map[$key]=if ($Cohort -eq 'pointer-status') { 32 } else { 1 } + } + } } } } +} +$pointerWitness=@{}; $pointerRequired=@{} +AddPointerKeys $pointerWitness 'pointer-witness' $pointerCanonical @('ISP') @('0000') $true +AddPointerKeys $pointerRequired 'pointer-structure' $pointerStructures @('ISP') @('0000') $false +AddPointerKeys $pointerRequired 'pointer-status' $pointerCanonical @('user','user-M','ISP','MSP') @('0000','8000','4000') $false $rows=@() +foreach ($kind in @('read','pointer')) { foreach ($name in @('witness-scalar','witness-batch','discovery-scalar','discovery-batch')) { - $report=Get-Content (Join-Path $output "68040-movem-read-recovery-$name.json") -Raw | ConvertFrom-Json - $inventory=if ($name.StartsWith('witness')) { $witness } else { $required } - $expected=if ($name.StartsWith('witness')) { 72 } else { 114912 } - if ($report.schema -ne 1 -or $report.model -ne '68040' -or $report.group -ne "movem-read-recovery-$name" -or $report.logicalCases -ne $expected -or $report.xunitBatches -ne 1 -or @($report.combinations.psobject.Properties).Count -ne $inventory.Count) { throw "Incomplete recovery report: $name" } + $report=Get-Content (Join-Path $output "68040-movem-$kind-recovery-$name.json") -Raw | ConvertFrom-Json + if ($kind -eq 'pointer') { + $inventory=if ($name.StartsWith('witness')) { $pointerWitness } else { $pointerRequired } + $expected=if ($name.StartsWith('witness')) { 8 } else { 15744 } + } else { + $inventory=if ($name.StartsWith('witness')) { $witness } else { $required } + $expected=if ($name.StartsWith('witness')) { 72 } else { 114912 } + } + if ($report.schema -ne 1 -or $report.model -ne '68040' -or $report.group -ne "movem-$kind-recovery-$name" -or $report.logicalCases -ne $expected -or $report.xunitBatches -ne 1 -or @($report.combinations.psobject.Properties).Count -ne $inventory.Count) { throw "Incomplete recovery report: $kind/$name" } $sum=@{passing=0;mismatching=0;unsupported=0;untested=0} foreach ($combination in $report.combinations.psobject.Properties) { if (-not $inventory.ContainsKey($combination.Name)) { throw "Foreign recovery combination: $($combination.Name)" } @@ -80,8 +105,9 @@ foreach ($name in @('witness-scalar','witness-batch','discovery-scalar','discove if ($weight -ne $inventory[$combination.Name]) { throw "Changed recovery combination weight: $($combination.Name)" } } foreach ($status in $sum.Keys) { if ($sum[$status] -ne $report.counts.$status) { throw "Recovery totals disagree: $name/$status" } } - $rows+=@{group=$name;wholePrograms=$expected;combinations=$inventory.Count;counts=$sum} + $rows+=@{group="$kind/$name";wholePrograms=$expected;combinations=$inventory.Count;counts=$sum} +} } -@{schema=1;rows=$rows;wholePrograms=229968;roadmapComplete=$false;inputManifestSha256=(Hash $manifestPath)} | ConvertTo-Json -Depth 10 | Set-Content (Join-Path $output 'verification.json') +@{schema=1;rows=$rows;wholePrograms=261472;roadmapComplete=$false;inputManifestSha256=(Hash $manifestPath)} | ConvertTo-Json -Depth 10 | Set-Content (Join-Path $output 'verification.json') if (@($tests | Where-Object outcome -ne 'Passed').Count -gt 0 -or $manifest.testExit -ne 0 -or @($rows | Where-Object { $_.counts.mismatching + $_.counts.unsupported + $_.counts.untested -gt 0 }).Count -gt 0) { throw "040 MOVEM recovery fails. Retained evidence: $output" } -Write-Host '040 MOVEM recovery passes 229,968 whole programs across scalar/batch execution; milestone 6 remains open.' +Write-Host '040 MOVEM operand/pointer recovery passes 261,472 whole programs across scalar/batch execution; milestone 6 remains open.' From 79b8841599692e0c115fde02bab0c02fee811e39 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 01:18:37 +0300 Subject: [PATCH 082/211] Prove and consolidate duplicate EXTB regression --- Copper68k.Tests/M68020InterpreterTests.cs | 16 --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 58 +++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 12 ++ scripts/test-copper68k-extb-consolidation.ps1 | 118 ++++++++++++++++++ 4 files changed, 188 insertions(+), 16 deletions(-) create mode 100644 scripts/test-copper68k-extb-consolidation.ps1 diff --git a/Copper68k.Tests/M68020InterpreterTests.cs b/Copper68k.Tests/M68020InterpreterTests.cs index 1d1a8ef7..5397df0e 100644 --- a/Copper68k.Tests/M68020InterpreterTests.cs +++ b/Copper68k.Tests/M68020InterpreterTests.cs @@ -39,22 +39,6 @@ public void MovecTransfersVectorBaseRegister() Assert.Equal(12, cpu.State.Cycles); } [Fact] - public void M68020ExecutesM68020OnlyExtbLong() - { - var bus = new ZeroWaitCodeBus(); - WriteWords(bus, CodeBase, 0x49C0); // EXTB.L D0 - var cpu = new M68020Interpreter(bus, M68020CpuProfile.OcsAccelerator14Mhz); - cpu.Reset(CodeBase, 0x3000); - cpu.State.D[0] = 0x0000_0080; - cpu.ExecuteInstruction(); - Assert.Equal(CodeBase + 2u, cpu.State.ProgramCounter); - Assert.Equal(0xFFFF_FF80u, cpu.State.D[0]); - Assert.True(cpu.State.GetFlag(M68kCpuState.Negative)); - Assert.False(cpu.State.GetFlag(M68kCpuState.Zero)); - Assert.False(cpu.State.GetFlag(M68kCpuState.Overflow)); - Assert.False(cpu.State.GetFlag(M68kCpuState.Carry)); - } - [Fact] public void MovecUnsupportedControlRegisterRaisesIllegalWithoutClobberingDestination() { var bus = new ZeroWaitCodeBus(); diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 7aa4c66b..2591e094 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -7426,3 +7426,61 @@ full-suite and consumer results remain historical evidence for that production implementation, not a fresh run of these new tests. The broad remaining gate, other model/reference disagreements and consolidation remain open. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## EXTB semantic regression consolidation — 2026-10-07 + +`M68020InterpreterTests.M68020ExecutesM68020OnlyExtbLong` is retired only after +proving replacement coverage. Its critical inputs are opcode `49C0`, D0 `$80`, +CCR zero and supervisor SR `$2700`. It checks the sign-extended D0, next PC and +N/Z/V/C, with no timing, bus-order, prefetch, cache or integration assertions. +The exact existing replacement is +`68020/EXTB.L/D0/boundary-ccr/op=49C0/v=00000080/ccr=00` in +`SyntheticTransferTests.RegisterTransferFamilies`. Shared expectations also +check X/other SR bits, all untouched registers/stacks/memory, execution state +and a following NOP sentinel. Other CCR/value/register selections and all eight +profiles remain covered by that matrix, including the unavailable instruction's +architectural outcome on 000/010. + +[M68000PRM EXT/EXTB, 4-106](https://www.nxp.com/docs/en/reference-manual/M68000PRM.pdf) +defines EXTB as copying bit 7 into bits 31..8. The targeted production mutation +replaces that sign extension with zero extension in an isolated source copy. +Before retirement, both the original fact and the replacement fail in the same +run. The original reports expected `$FFFFFF80`, actual `$80`; the exact +replacement reports expected SR `$2708`, actual `$2700`, catching the resulting +incorrect N flag first. There are **1,024 intended EXTB mismatches on each of +six implementing profiles / 6,144 total**. The 000/010 unavailable-instruction +checks and every other transfer combination still pass. + +The maintained command makes this proof repeatable after retirement: + +```powershell +./scripts/test-copper68k-extb-consolidation.ps1 -OutputDirectory artifacts/extb-consolidation-proof +``` + +It requires the exact original fact from pinned commit +`4863f449816115b536b21f9faa7c73539ecaabcb`, inserts it only into owned source +copies, and runs clean/mutated comparisons. Missing historical fixtures or empty +selections fail. The gate checks source, fixture, assembly, report and TRX +identities, the exact nine-test roster in each run, the original diagnostic, +the exact replacement case, and all **232 independently enumerated weighted +combination keys per model**. Both pre- and post-retirement clean comparisons +pass **140,992 logical scenarios / eight reports plus the historical fact**. +The mutated run has precisely seven failed executions and two passing ones. +`-ValidateReportsOnly` verifies the frozen proof without rebuilding. + +Evidence is retained under the restoration temporary root in +`audits/ExtbConsolidationBeforeFinal`, `audits/ExtbConsolidationAfter` and +`ExtbConsolidationIntegrity`. Six copied-evidence controls reject missing source, +altered pinned fixture, empty execution, wrong weight, lost exact replacement +case and wrong historical diagnostic. Initial verifier defects remain retained +as provisional evidence, not accepted proof. + +All **462 remaining M68020InterpreterTests executions** pass with no failures or +skips, retained in `audits/ExtbRetained020Tests`. Nearby LEA/MOVEQ and other +timing-policy tests remain: their cycle assertions are not replaced by the +semantic matrix. Specialized exception, bus, cache/prefetch, JIT and native +regressions are not retired. The ordinary 707-report scenario inventory does not +change. Production source, package bytes and protected normal assemblies remain +unchanged; no new full CPU/consumer run or package release is claimed. Other +consolidation/reference/restoration requirements remain open. Milestone 6 is +still **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 81e1c014..d7488ae5 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1943,6 +1943,18 @@ in an isolated source copy makes all sixteen fixed pointer programs fail. No production or package change is needed. The full milestone remains open; see [pre-EA pointer faults](COPPER68K_REFERENCE_QUALIFICATION.md#pre-ea-movem-indirect-pointer-faults--2026-10-07). +EXTB consolidation on 2026-10-07 retires the pure semantic +`M68020ExecutesM68020OnlyExtbLong` fact after the existing exact synthetic case +and original both detect a targeted zero-extension mutation. A maintained command +restores only the pinned historical fact in isolated source copies and verifies +the same proof after retirement: 140,992 clean scenarios across eight profiles, +6,144 intended EXTB mismatches across six implementing profiles, complete weighted +keys and exact diagnostics. Six corrupted-evidence controls are rejected. All +462 retained 68020 class tests pass; timing and specialized regressions remain. +The ordinary scenario inventory and production package are unchanged. See the +[EXTB retirement proof](COPPER68K_REFERENCE_QUALIFICATION.md#extb-semantic-regression-consolidation--2026-10-07). +Milestone 6 remains **in progress**, `roadmapComplete=false`. + The three maintained software-trace mutations are detected in each complete scalar/batch canonical/chained report with their intended semantic diagnostic. Production bytes are restored exactly and the clean implementation rebuilds diff --git a/scripts/test-copper68k-extb-consolidation.ps1 b/scripts/test-copper68k-extb-consolidation.ps1 new file mode 100644 index 00000000..6254f59b --- /dev/null +++ b/scripts/test-copper68k-extb-consolidation.ps1 @@ -0,0 +1,118 @@ +#requires -Version 7.0 +[CmdletBinding()] +param([string] $OutputDirectory='artifacts/extb-consolidation', [switch] $ValidateReportsOnly) +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +$pin='4863f449816115b536b21f9faa7c73539ecaabcb' +$legacyFile='Copper68k.Tests/M68020InterpreterTests.cs' +$legacyName='M68020ExecutesM68020OnlyExtbLong' +$advanced='Copper68k/M68kAdvancedTimingInterpreter.cs' +$before='var value = unchecked((uint)(int)(sbyte)(State.D[register] & 0xFF));' +$after='var value = State.D[register] & 0xFF; // Mutation: EXTB zero extension.' +$models=@('68000','68010','68EC020','68020','68030','68040','68060','A1200') +function Hash([string] $Path) { (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() } +$files=@(& git -C $repo ls-files Copper68k Copper68k.Tests) +$identities=@($files + 'scripts/test-copper68k-extb-consolidation.ps1' | Sort-Object -Unique) +$historical=@(& git -C $repo show "${pin}:$legacyFile") -join "`n" +if($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($historical)){throw 'Missing pinned historical regression fixture'} +$pattern='(?ms)^\t\[Fact\]\n\tpublic void '+$legacyName+'\(\)\n\t\{.*?^\t\}\n' +$matches=[regex]::Matches($historical,$pattern) +if($matches.Count -ne 1){throw 'Historical regression fixture is not unique'} +$fixture=$matches[0].Value +$manifestPath=Join-Path $output 'inputs.json' +if(-not $ValidateReportsOnly){ + if(Test-Path -LiteralPath $output){throw "Use a fresh output directory: $output"} + New-Item -ItemType Directory -Path $output | Out-Null + $sourceIds=@($identities | ForEach-Object {@{path=$_;sha256=(Hash (Join-Path $repo $_))}}) + $normal=@(foreach($file in @('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll')){ + if(Test-Path -LiteralPath (Join-Path $repo $file)){@{path=$file;sha256=(Hash (Join-Path $repo $file))}} + }) + $fixture | Set-Content -LiteralPath (Join-Path $output 'historical-regression.cs.txt') + $records=@() + foreach($kind in @('clean','zero-extension')){ + $owned=Join-Path $output $kind + New-Item -ItemType Directory -Path $owned | Out-Null + foreach($file in $files){$dest=Join-Path $owned $file;New-Item -ItemType Directory -Path (Split-Path -Parent $dest) -Force | Out-Null;Copy-Item -LiteralPath (Join-Path $repo $file) -Destination $dest} + $legacyPath=Join-Path $owned $legacyFile + $text=[IO.File]::ReadAllText($legacyPath).Replace("`r`n","`n") + # After retirement, inject only the pinned original fact into this owned copy. + if(-not $text.Contains("public void $legacyName()")){ + $anchor="public sealed class M68020InterpreterTests`n{`n" + if($text.Split([string[]]@($anchor),[StringSplitOptions]::None).Count -ne 2){throw 'Historical fixture insertion anchor changed'} + $text=$text.Replace($anchor,$anchor+$fixture) + } elseif([regex]::Match($text,$pattern).Value -cne $fixture){throw 'Existing legacy regression differs from pinned fixture'} + [IO.File]::WriteAllText($legacyPath,$text) + $production=Join-Path $owned $advanced + $code=[IO.File]::ReadAllText($production) + if($code.Split([string[]]@($before),[StringSplitOptions]::None).Count -ne 2){throw 'EXTB mutation target is not unique'} + if($kind -eq 'zero-extension'){[IO.File]::WriteAllText($production,$code.Replace($before,$after))} + $saved=[Environment]::GetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR') + try{ + [Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$owned) + & dotnet test (Join-Path $owned 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --artifacts-path (Join-Path $owned 'build') --filter "FullyQualifiedName~$legacyName|FullyQualifiedName~RegisterTransferFamilies" --logger 'trx;LogFileName=audit.trx' --results-directory $owned *> (Join-Path $owned 'execution.log') + $code=$LASTEXITCODE + }finally{[Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$saved)} + $paths=@('audit.trx','execution.log',$legacyFile,$advanced,'build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll')+@($models | ForEach-Object{"$_-transfer-registers.json"}) + $records+=@{kind=$kind;exit=$code;evidence=@($paths | ForEach-Object{@{path=$_;sha256=(Hash (Join-Path $owned $_))}})} + } + foreach($item in $normal){if((Hash (Join-Path $repo $item.path)) -ne $item.sha256){throw 'Normal assembly modified'}} + @{schema=1;historicalPin=$pin;legacyMethod=$legacyName;sourceCommit=(& git -C $repo rev-parse HEAD);sources=$sourceIds;normalBinaries=$normal;fixtureSha256=(Hash (Join-Path $output 'historical-regression.cs.txt'));runs=$records} | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $manifestPath +} +$manifest=Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json +if($manifest.schema -ne 1 -or $manifest.historicalPin -ne $pin -or $manifest.legacyMethod -ne $legacyName -or + $manifest.sources.Count -ne $identities.Count -or @(Compare-Object ($manifest.sources.path | Sort-Object) $identities).Count -or + $manifest.runs.Count -ne 2 -or @(Compare-Object ($manifest.runs.kind | Sort-Object) @('clean','zero-extension')).Count){throw 'Incomplete consolidation identities'} +foreach($item in $manifest.sources){if((Hash (Join-Path $repo $item.path)) -ne $item.sha256){throw "Changed consolidation source: $($item.path)"}} +if((Hash (Join-Path $output 'historical-regression.cs.txt')) -ne $manifest.fixtureSha256 -or + (Get-Content (Join-Path $output 'historical-regression.cs.txt') -Raw).TrimEnd() -cne $fixture.TrimEnd()){throw 'Changed historical fixture'} +$legacy='Copper68k.Tests.M68020InterpreterTests.'+$legacyName +$names=@($legacy)+@($models | ForEach-Object{'Copper68k.Tests.Synthetic.SyntheticTransferTests.RegisterTransferFamilies(modelId: "'+$_+'")'}) +foreach($run in $manifest.runs){ + $owned=Join-Path $output $run.kind + $paths=@('audit.trx','execution.log',$legacyFile,$advanced,'build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll')+@($models | ForEach-Object{"$_-transfer-registers.json"}) + if($run.evidence.Count -ne $paths.Count -or @(Compare-Object ($run.evidence.path | Sort-Object) ($paths | Sort-Object)).Count){throw 'Incomplete consolidation evidence'} + foreach($item in $run.evidence){if((Hash (Join-Path $owned $item.path)) -ne $item.sha256){throw "Changed consolidation evidence: $($item.path)"}} + $expectedCode=[IO.File]::ReadAllText((Join-Path $repo $advanced)) + if($run.kind -eq 'zero-extension'){$expectedCode=$expectedCode.Replace($before,$after)} + if([IO.File]::ReadAllText((Join-Path $owned $advanced)) -cne $expectedCode){throw 'Wrong production mutation scope'} + [xml]$trx=Get-Content (Join-Path $owned 'audit.trx') -Raw + $tests=@($trx.TestRun.Results.UnitTestResult) + if($tests.Count -ne 9 -or @(Compare-Object ($tests.testName | Sort-Object) ($names | Sort-Object)).Count -or + @($tests.testId | Sort-Object -Unique).Count -ne 9){throw 'Incomplete consolidation execution roster'} + $failed=if($run.kind -eq 'clean'){@()}else{@($names | Where-Object{$_ -eq $legacy -or ($_ -notmatch 'modelId: "68000"|modelId: "68010"')})} + $actualFailed=@($tests | Where-Object outcome -eq Failed | ForEach-Object{$_.testName}) + if((($actualFailed | Sort-Object) -join "`n") -cne (($failed | Sort-Object) -join "`n") -or + @($tests | Where-Object outcome -notin @('Passed','Failed')).Count -or $run.exit -ne [int]($run.kind -eq 'zero-extension')){throw 'Wrong consolidation failure roster'} + if($run.kind -eq 'zero-extension'){ + $message=($tests | Where-Object testName -eq $legacy).Output.ErrorInfo.Message + if($message -notmatch 'Expected: 4294967168' -or $message -notmatch 'Actual:\s+128'){throw 'Wrong historical EXTB mutation diagnostic'} + } + foreach($model in $models){ + $r=Get-Content (Join-Path $owned "$model-transfer-registers.json") -Raw | ConvertFrom-Json + $mismatch=if($run.kind -eq 'zero-extension' -and $model -notin @('68000','68010')){1024}else{0} + if($r.model -ne $model -or $r.group -ne 'transfer-registers' -or $r.logicalCases -ne 17624 -or $r.xunitBatches -ne 1 -or + $r.counts.passing -ne 17624-$mismatch -or $r.counts.mismatching -ne $mismatch -or $r.counts.unsupported -ne 0 -or $r.counts.untested -ne 0 -or $r.failures.Count -ne $mismatch){throw "Wrong transfer proof counts: $model/$($run.kind)"} + $keys=@{} + foreach($reg in 0..7){ + $keys["$model/MOVEQ/L/imm8->D$reg/all-encodings"] = 411 + foreach($family in @('EXT.W','EXT.L','EXTB.L','SWAP')){$keys["$model/$family/D$reg/boundary-ccr"] = 256} + } + foreach($kind in @('40','48','88')){foreach($source in 0..7){foreach($destination in 0..7){$keys["$model/EXG/L/kind=$kind/r$source->r$destination/preserve-flags"] = 32}}} + if(@($r.combinations.psobject.Properties).Count -ne 232){throw 'Incomplete transfer combination inventory'} + foreach($combination in $r.combinations.psobject.Properties){ + if(-not $keys.ContainsKey($combination.Name)){throw 'Foreign transfer combination'} + $bad=if($mismatch -and $combination.Name.Contains('/EXTB.L/')){128}else{0} + $states=@($combination.Value.psobject.Properties) + if($states.Count -ne (1+[int]($bad -gt 0)) -or $combination.Value.passing -ne $keys[$combination.Name]-$bad -or + ($bad -gt 0 -and $combination.Value.mismatching -ne $bad)){throw 'Wrong transfer combination weight/status'} + } + if($mismatch){ + if(@($r.failures | Where-Object{$_.status -ne 'mismatching' -or $_.id -notmatch '/EXTB.L/' -or $_.reason -notin @('SR expected 2708, actual 2700, mask=FFFF','SR expected 2718, actual 2710, mask=FFFF')}).Count){throw 'Wrong EXTB mutation diagnostics'} + $case="$model/EXTB.L/D0/boundary-ccr/op=49C0/v=00000080/ccr=00" + if(@($r.failures | Where-Object{$_.id -eq $case -and $_.reason -eq 'SR expected 2708, actual 2700, mask=FFFF'}).Count -ne 1){throw 'Missing exact legacy replacement failure'} + } + } +} +@{schema=1;historicalRegressionDetected=$true;replacementDetected=$true;cleanExecutions=9;mutationExecutions=9;passingCleanScenarios=140992;mutatedMismatches=6144;retiredMethod=$legacyName;replacementCase='68020/EXTB.L/D0/boundary-ccr/op=49C0/v=00000080/ccr=00';inputManifestSha256=(Hash $manifestPath);roadmapComplete=$false} | ConvertTo-Json -Depth 8 | Set-Content (Join-Path $output 'verification.json') +Write-Host 'EXTB consolidation proven: original and exact replacement detect zero extension; all eight clean profiles pass.' From 72800d7593da201084af0635d3352269365c142e Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 01:31:21 +0300 Subject: [PATCH 083/211] Expose actual 68040 MOVEM write-fault frame gap --- ...eticM68040MovemWriteFaultDiscoveryTests.cs | 191 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 78 +++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 ++ ...st-copper68k-040-movem-write-discovery.ps1 | 100 +++++++++ 4 files changed, 384 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteFaultDiscoveryTests.cs create mode 100644 scripts/test-copper68k-040-movem-write-discovery.ps1 diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteFaultDiscoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteFaultDiscoveryTests.cs new file mode 100644 index 00000000..311fba87 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteFaultDiscoveryTests.cs @@ -0,0 +1,191 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 8.4.6.2, 8.4.6.5 and 8.4.6.7: normal physical +// MOVEM writes require format 7, CM/original EA and memory-aligned WB1. +// This discovery does not qualify handler/RTE recovery or pipeline timing. +public sealed class SyntheticM68040MovemWriteFaultDiscoveryTests(ITestOutputHelper output) +{ + internal const string Enable = "COPPER68K_RUN_040_MOVEM_WRITE_DISCOVERY"; + private const uint Handler = 0xa000, Vbr = 0x900000; + + [Fact, Trait("Suite", "ReferenceDiscovery")] + public void FixedManualStoreEncodings() + { + Assert.Equal(0x4890, Opcode(new(2, 0), 2)); + Assert.Equal(0x48d0, Opcode(new(2, 0), 4)); + Assert.Equal(0x48a7, Opcode(new(4, 7), 2)); + Assert.Equal(0x48e7, Opcode(new(4, 7), 4)); + Assert.Equal(0x48b0, Opcode(new(6, 0), 2)); + Assert.Equal(0x48f9, Opcode(new(7, 1), 4)); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); + m.Reset(); m.Core.State.D[1] = 0xffffffe0; + var words = new List { 0x48f0, 0x0303 }; + var fixture = new AddressingFixture(m, 4, words, m.Core.State.D, m.Core.State.A, 0, + new(DestinationIndex: new(IndexRegister: 1, LongIndex: true, BriefDisplacement: 64))); + Assert.Equal(0x4020u, fixture.Resolve(new(6, 0), false)); + Assert.Equal(new ushort[] { 0x48f0, 0x0303, 0x1840 }, words); + Assert.Equal(0x1006u, fixture.NextPc); + } + + [EnvironmentFact(Enable, "discover actual MOVEM write-fault frame construction"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarStoresRequireFormat7() => Audit(false); + + [EnvironmentFact(Enable, "discover actual MOVEM write-fault frame construction"), Trait("Suite", "ReferenceDiscovery")] + public void BatchStoresRequireFormat7() => Audit(true); + + [Theory, Trait("Suite", "ReferenceDiscovery")] + [InlineData(false)] + [InlineData(true)] + public void CanonicalStoreFixturesExecuteWithoutFault(bool batch) + { + var report = new CoverageBatch("68040", "movem-write-fixture-control-" + (batch ? "batch" : "scalar")); + foreach (var mode in new[] { 2, 4, 5, 6, 7 }) + for (var register = 0; register < (mode == 7 ? 2 : 8); register++) + foreach (var width in new[] { 2, 4 }) + Case(report, batch, "fixture", new(mode, register), width, 3, 0, "ISP", 0, 31, reject: false); + report.Complete(output); + } + + private static ushort Opcode(OperandForm form, int width) + => (ushort)(0x4880 | (width == 4 ? 0x40 : 0) | form.Mode << 3 | form.Register); + + private void Audit(bool batch) + { + var report = new CoverageBatch("68040", "movem-write-fault-discovery-" + (batch ? "batch" : "scalar")); + foreach (var mode in new[] { 2, 4, 5, 6, 7 }) + for (var register = 0; register < (mode == 7 ? 2 : 8); register++) + Cases("opcode", new(mode, register), "ISP", 0, 31); + foreach (var form in new[] { new OperandForm(2, 0), new(4, 0), new(5, 0), new(6, 0), new(7, 0), new(7, 1) }) + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var trace in new ushort[] { 0, 0x8000, 0x4000 }) + for (var ccr = 0; ccr < 32; ccr++) Cases("status", form, bank, trace, ccr); + report.Complete(output); + + void Cases(string cohort, OperandForm form, string bank, ushort trace, int ccr) + { + foreach (var width in new[] { 2, 4 }) + for (var transfer = 0; transfer < 4; transfer++) + for (var faultByte = 0; faultByte < width; faultByte++) + Case(report, batch, cohort, form, width, transfer, faultByte, bank, trace, ccr); + } + } + + private static void Case(CoverageBatch report, bool batch, string cohort, OperandForm form, + int width, int transfer, int faultByte, string bank, ushort trace, int ccr, bool reject = true) + { + var opcode = Opcode(form, width); + var id = $"68040/MOVEM/write-fault/{cohort}/size={width}/mode={form.Mode}/reg={form.Register}/bank={bank}/T={trace:X4}/transfer={transfer}/byte={faultByte}/op={opcode:X4}/ccr={ccr:X2}"; + try + { + var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + m.Reset(ccr); m.Core.State.D[0] = 0x11112222; m.Core.State.D[1] = 0xffffffe0; + var sr = (ushort)(0x0700 | M68040StackFixture.Status(bank, trace, ccr)); + var stacks = new Dictionary { ["user"] = 0x7800, ["ISP"] = 0x4700, ["MSP"] = 0x7400 }; + SetStacks(); + var registers = m.Core.State.D.Concat(m.Core.State.A).ToArray(); + var words = new List { opcode, (ushort)(form.Mode == 4 ? 0xc0c0 : 0x0303) }; + var fixture = new AddressingFixture(m, width, words, (uint[])m.Core.State.D.Clone(), + (uint[])m.Core.State.A.Clone(), 0, new(DestinationIndex: + new(IndexRegister: 1, LongIndex: true, BriefDisplacement: 64), + DestinationAbsoluteWord: 0xf000, DestinationAbsoluteLong: 0x12346200)); + var first = fixture.Resolve(form, false); + var ea = form.Mode == 4 ? registers[8 + form.Register] : first; + var addresses = Enumerable.Range(0, 4).Select(n => unchecked(first + (uint)(form.Mode == 4 ? -n * width : n * width))).ToArray(); + int[] order = form.Mode == 4 ? [9, 8, 1, 0] : [0, 1, 8, 9]; + var values = order.Select(r => registers[r] - (form.Mode == 4 && r == 8 + form.Register ? (uint)width : 0u)) + .Select(v => width == 2 ? v & 0xffff : v).ToArray(); + foreach (var at in addresses) m.InitializePhysical(at, 0x5a5aa5a5, width); + for (var n = 0; n < words.Count; n++) m.InitializePhysical(0x1000 + (uint)n * 2, words[n], 2); + m.InitializePhysical(fixture.NextPc, 0x7e55, 2); + m.InitializePhysical(Vbr + 8, Handler, 4); m.InitializePhysical(Handler, 0x4e71, 2); m.InitializePhysical(Handler + 2, 0x4e71, 2); + m.Start(); SetStacks(); m.Core.State.VectorBaseRegister = Vbr; + var e = ArchitecturalExpectation.Capture(m); var sequence = m.Core.State.ExceptionSequence; + if (!reject) + { + for (var n = 0; n < 4; n++) e.Write(addresses[n], values[n], width, m.Model); + if (form.Mode == 4) + { + e.A[form.Register] = unchecked(ea - (uint)(4 * width)); + if (form.Register == 7) stacks[M68040StackFixture.PhysicalBank(bank)] = e.A[7]; + } + M68040StackFixture.SetStacks(e, stacks, sr); e.Pc = fixture.NextPc; + e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence); + Execute(); + if (e.Verify(m) is { } controlMismatch) throw new InvalidOperationException("fault-free MOVEM: " + controlMismatch); + e.Pc += 2; e.D[7] = 0x55; e.Sr &= 0xfff0; + Execute(); + if (e.Verify(m) is { } sentinelMismatch) throw new InvalidOperationException("following MOVEQ: " + sentinelMismatch); + report.Record(id, "passing", null); return; + } + for (var n = 0; n < transfer; n++) e.Write(addresses[n], values[n], width, m.Model); + var exceptionBank = M68040StackFixture.ExceptionBank(sr); var frame = stacks[exceptionBank] - 60; + stacks[exceptionBank] = frame; + M68040StackFixture.SetStacks(e, stacks, (ushort)((sr | 0x2000) & ~0xc000)); + e.Pc = Handler; e.ExceptionVector = 2; + e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence + 1); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, 0x1000); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, sr); + e.ControlChecks["bypass cleared"] = (s => s.M68040Mmu.BypassTranslation ? 1u : 0u, 0); + for (uint n = 8; n < 60; n++) e.MemoryMasks[frame + n] = 0; + Define(0, sr, 2); Define(2, 0x1000, 4); Define(6, 0x7008, 2); Define(8, ea, 4); + var size = width == 2 ? 0x40u : 0u; var tm = bank.StartsWith("user") ? 1u : 5u; + Define(12, 0x1000 | size | tm, 2); e.MemoryMasks[frame + 13] = 0x7f; // X undefined. + foreach (uint offset in new uint[] { 14, 16 }) { Define(offset, 0, 2); e.MemoryMasks[frame + offset + 1] = 0x80; } + Define(18, 0x80 | size | tm, 2); Define(20, addresses[transfer], 4); Define(40, addresses[transfer], 4); + for (var n = 0; n < width; n++) + { + var at = frame + 44 + ((addresses[transfer] + (uint)n) & 3); + e.Memory[at] = (byte)(values[transfer] >> (8 * (width - n - 1))); e.MemoryMasks[at] = 255; + } + bus.Accesses.Clear(); bus.Arm(addresses[transfer] + (uint)faultByte, M68kBusAccessKind.CpuDataWrite); + Execute(); + if (bus.Rejected.Count != 1) throw new InvalidOperationException("Missing original physical write rejection"); + var rejected = bus.Rejected[0]; + if (rejected.Kind != M68kBusAccessKind.CpuDataWrite || unchecked(addresses[transfer] + (uint)faultByte - rejected.Address) >= rejected.Width) + throw new InvalidOperationException("Wrong physical write rejected"); + var actualFormat = m.PeekPhysical(m.Core.State.A[7] + 6, 2); + if (actualFormat != 0x7008) throw new InvalidOperationException($"entry: format/vector expected 7008, actual {actualFormat:X4}; A7 expected {frame:X8}, actual {m.Core.State.A[7]:X8}"); + // Preserve the existing fallback's descending split-word transport. + // A completed low half is permissible; a whole operand retry is not. + var preceding = bus.Accesses.Take(rejected.AccessCount).Where(a => a.Write && a.Address >= addresses[transfer] && a.Address < addresses[transfer] + (uint)width).ToArray(); + foreach (var access in preceding) + { + if (width != 4 || access.Address != addresses[transfer] + 2 || access.Width != 2 || access.Value != (values[transfer] & 0xffff)) + throw new InvalidOperationException("Unexpected partial faulting store"); + if (access.Address < frame || access.Address >= frame + 60) e.Write(access.Address, access.Value, 2, m.Model); + } + if (bus.Accesses.Skip(rejected.AccessCount).Any(a => a.Write && (a.Address < frame || a.Address >= frame + 60))) + throw new InvalidOperationException("Operand store repeated during exception entry"); + if (e.Verify(m) is { } mismatch) throw new InvalidOperationException("entry: " + mismatch); + report.Record(id, "passing", null); + + void Define(uint offset, uint value, int count) + { e.Write(frame + offset, value, count, m.Model); for (uint n = 0; n < count; n++) e.MemoryMasks[frame + offset + n] = 255; } + void SetStacks() + { m.Core.State.SetUserStackPointer(stacks["user"]); m.Core.State.SetInterruptStackPointer(stacks["ISP"]); m.Core.State.SetMasterStackPointer(stacks["MSP"]); m.Core.State.StatusRegister = sr; } + void Execute() + { + if (!batch) { m.Core.ExecuteInstruction(); return; } + var boundary = new Boundary(); + var count = ((IM68kBatchCore)m.Core).ExecuteInstructions(1, m.Core.State.Cycles + 10000, boundary); + if (count != 1 || boundary.Before != 1 || boundary.After != 1) throw new InvalidOperationException("Batch count/callbacks differ"); + } + } + catch (Exception ex) + { + var unsupported = ex is NotSupportedException or UnsupportedM68040InstructionException or + UnsupportedM68kOpcodeException or UnsupportedM68kTimingException; + report.Record(id, unsupported ? "unsupported" : "mismatching", ex.Message); + } + } + private sealed class Boundary : IM68kInstructionBoundary + { + public int Before, After; + public bool BeforeInstruction() { Before++; return true; } + public void AfterInstruction(long previousCycle, long currentCycle) => After++; + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 2591e094..e3f13588 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -7484,3 +7484,81 @@ change. Production source, package bytes and protected normal assemblies remain unchanged; no new full CPU/consumer run or package release is claimed. Other consolidation/reference/restoration requirements remain open. Milestone 6 is still **in progress**, `roadmapComplete=false`. + +## Actual 040 MOVEM write-fault discovery — 2026-10-07 + +This slice exposes a production gap; it does **not** close it. MC68040UM +8.4.6.2/5/7 requires a normal physical write's format-7 frame, valid +memory-aligned WB1, FA/WB1A and the original MOVEM calculated EA with CM set. +The saved PC identifies the interrupted MOVEM. A handler completes WB1 before +RTE; CM continuation repeats preceding MOVEM operand accesses. Exception entry +must not automatically retry a partly executed instruction. These are software +architectural requirements, separate from physical pipeline/cache qualification. +Primary source: [MC68040UM](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf). + +`SyntheticM68040MovemWriteFaultDiscoveryTests` uses the public CPU factory, +sparse recording memory, existing independent addressing/stack fixtures and a +one-shot physical-map rejection. Selected instructions store D0/D1/A0/A1 with +mask `0303`, or reverse mask `C0C0` for predecrement. It separately executes: + +- All 34 legal store EA/register encodings, word/long sizes, each of four + transfers and every rejected byte: **816 cases per execution route**. +- Six canonical forms across four stack states, T1/T0/clear trace, all 32 CCRs, + both sizes and every transfer/rejected byte: **55,296 cases per route**. + +Scalar and batch execution each contain **56,112 cases / 2,544 weighted keys**. +All **112,224 fault-entry cases mismatch**, with zero unsupported or untested +cases in this *selected* inventory. Retained failure diagnostics show +`format/vector expected 7008, actual 0008`, with an eight-byte frame rather than +60 bytes (ISP example `A7 expected 000046C4, actual 000046F8`). Production routing +handles completed normal MOVE writes specially, but MOVEM writes fall through +to the generic format-0 bus exception. MOVEM store paths also lack calculated-EA +fault metadata. This identifies the required correction without claiming a +validated fix. + +The literal opcode/brief-extension witness passes. Independent fault-free +controls execute all 34 forms at both sizes in scalar and batch routes: +**136 passing MOVEM/following-MOVEQ programs**, checking register preservation, +flags, memory, exact next PC, predecrement bases (including base/list aliases), +A7 and stack state. The discovery run has **three passing executions and two +failing executions**, no skips. Successful controls establish the selected +fixture encodings; they do not validate fault frames or recovery. + +Reproduce with the explicit strict command: + +```powershell +./scripts/test-copper68k-040-movem-write-discovery.ps1 -OutputDirectory artifacts/movem-write-discovery +``` + +It currently exits nonzero for the genuine frame mismatches. Use a fresh output +directory. `-ValidateReportsOnly` checks the frozen evidence without rebuilding +and must also reject this failing acceptance cohort. The verifier pins source, +script, assembly, report and TRX identities; requires the exact five-execution +roster, both independently enumerated weighted fault rosters and both 68-key +passing fixture rosters; and checks protected normal DLL hashes. Missing inputs, +empty selections and mismatches cannot be accepted. Seven copied-evidence +controls reject missing/changed source, missing report, empty execution, changed +weight, missing key and a false fixture-control weight at the intended checks. +An initial control harness expected different missing-file wording; that +provisional run is retained separately from the completed seven-control proof. + +Evidence under the restoration temporary root: +`audits/MovemWriteDiscoveryFinalV2` (manifest SHA-256 +`dcf627ceea945aed7326637427a88f96a843aae43ac54baaedd28744e4f5df8e`), +`MovemWriteDiscoveryIntegrityFinal`, and `audits/MovemWriteOrdinaryControlsFinal`. +The latter runs the new controls and existing fixed MOVEM operand/pointer-read +recovery witnesses: **eight passing executions, zero failures, six explicit +opt-in matrix skips**. Those skips are unavailable coverage in this invocation; +they do not override the explicit write-discovery failures. Earlier discovery +iterations remain historical, with their own frozen source identities. + +Next required work is latched MOVEM write metadata across fast/general/shared +fallback routes, correct normal-write WB1/CM entry, and executed handler/RTE/ +resumption/sentinel verification, including trace and partial-write ordering. +Full indexed structures, arbitrary masks, alignment, nested write faults and +other physical write families remain untested by this slice. No promoted gate, +480-case broad inventory or ordinary 707-report requirement is reduced. No +production source change, regression retirement, new full CPU/consumer run or +package publication is claimed. Normal CPU/test assemblies and unrelated +CopperScreen edits remain untouched. Milestone 6 remains **in progress**, +`roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index d7488ae5..410b0e75 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2682,3 +2682,18 @@ package, as do independent pinned SingleStepTests/Musashi audits. Broader addressing/recovery/trace and remaining model/reference/consolidation coverage stay required. See the [read-frame correction](COPPER68K_REFERENCE_QUALIFICATION.md#physical-040-operand-read-frame-correction--2026-10-06). Milestone 6 remains **in progress**, `roadmapComplete=false`. + +On 2026-10-07, actual 040 MOVEM write-fault discovery executes every legal +store EA opcode with a selected D0/D1/A0/A1 list, word/long sizes, every rejected +transfer/byte, and separate canonical CCR/stack-bank/trace combinations. All +112,224 selected entries fail the required format-7 gate; retained diagnostics +show the current short format-0 delivery. The independent literal encoding +witness and 136 fault-free MOVEM/sentinel programs pass. Seven evidence-integrity +controls reject their intended corruptions. This is preserved failing discovery, +not promoted coverage or a production correction. Calculated-EA/CM write metadata, +normal WB1 construction and executed handler/RTE/resumption remain required; +full indexed/mask, alignment and nested write-fault protocols also remain open. +The broad 480-case inventory is not reduced, no regression is retired, and no +new package or consumer qualification is claimed. See the +[MOVEM write discovery](COPPER68K_REFERENCE_QUALIFICATION.md#actual-040-movem-write-fault-discovery--2026-10-07). +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-040-movem-write-discovery.ps1 b/scripts/test-copper68k-040-movem-write-discovery.ps1 new file mode 100644 index 00000000..cf6f6c5c --- /dev/null +++ b/scripts/test-copper68k-040-movem-write-discovery.ps1 @@ -0,0 +1,100 @@ +#requires -Version 7.0 +[CmdletBinding()] +param( + [string] $OutputDirectory = 'artifacts/040-movem-write-discovery', + [switch] $ValidateReportsOnly +) +$ErrorActionPreference = 'Stop' +$repo = Split-Path -Parent $PSScriptRoot +$output = [IO.Path]::GetFullPath($OutputDirectory, $repo) +function Hash([string] $Path) { (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() } +$manifestPath = Join-Path $output 'inputs.json' +$sources = @(@(& git -C $repo ls-files 'Copper68k/*.cs' 'Copper68k/*.csproj' 'Copper68k.Tests/*.cs' 'Copper68k.Tests/*.csproj') + @( + 'Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteFaultDiscoveryTests.cs', + 'scripts/test-copper68k-040-movem-write-discovery.ps1') | Sort-Object -Unique) +$evidence = @('audit.trx','68040-movem-write-fault-discovery-scalar.json','68040-movem-write-fault-discovery-batch.json', + '68040-movem-write-fixture-control-scalar.json','68040-movem-write-fixture-control-batch.json', + 'build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll') +if (-not $ValidateReportsOnly) { + if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } + New-Item -ItemType Directory -Path $output | Out-Null + $identities = @($sources | ForEach-Object { @{path=$_;sha256=(Hash (Join-Path $repo $_))} }) + $protected = @(@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | + Where-Object { Test-Path -LiteralPath (Join-Path $repo $_) } | ForEach-Object { @{path=$_;sha256=(Hash (Join-Path $repo $_))} }) + $settings = @{COPPER68K_RUN_040_MOVEM_WRITE_DISCOVERY='1';COPPER68K_SYNTHETIC_REPORT_DIR=$output} + $saved = @{} + try { + foreach ($key in $settings.Keys) { $saved[$key]=[Environment]::GetEnvironmentVariable($key); [Environment]::SetEnvironmentVariable($key,$settings[$key]) } + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --artifacts-path (Join-Path $output 'build') --filter 'FullyQualifiedName~SyntheticM68040MovemWriteFaultDiscoveryTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output *> (Join-Path $output 'execution.log') + $testExit=$LASTEXITCODE + } finally { foreach ($key in $saved.Keys) { [Environment]::SetEnvironmentVariable($key,$saved[$key]) } } + @{schema=1;reference='MC68040UM 8.4.6.2/5/7';referenceUrl='https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf'; + sourceCommit=(& git -C $repo rev-parse HEAD);testExit=$testExit;sources=$identities;protected=$protected; + evidence=@($evidence | ForEach-Object { @{path=$_;sha256=(Hash (Join-Path $output $_))} })} | + ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $manifestPath +} +$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json +if ($manifest.schema -ne 1 -or $manifest.sources.Count -ne $sources.Count -or + @(Compare-Object ($manifest.sources.path | Sort-Object) $sources).Count -ne 0 -or + $manifest.evidence.Count -ne $evidence.Count -or + @(Compare-Object ($manifest.evidence.path | Sort-Object) ($evidence | Sort-Object)).Count -ne 0) { throw 'Missing discovery identities' } +foreach ($source in $manifest.sources) { if ((Hash (Join-Path $repo $source.path)) -ne $source.sha256) { throw "Changed source: $($source.path)" } } +foreach ($item in $manifest.evidence) { if ((Hash (Join-Path $output $item.path)) -ne $item.sha256) { throw "Changed evidence: $($item.path)" } } +foreach ($item in $manifest.protected) { if ((Hash (Join-Path $repo $item.path)) -ne $item.sha256) { throw "Normal DLL changed: $($item.path)" } } +[xml]$trx = Get-Content (Join-Path $output 'audit.trx') -Raw +$tests = @($trx.TestRun.Results.UnitTestResult) +if ($tests.Count -ne 5 -or $trx.TestRun.ResultSummary.Counters.executed -ne 5 -or + @($tests.testId | Sort-Object -Unique).Count -ne 5) { throw 'Incomplete discovery execution' } +foreach ($name in @('FixedManualStoreEncodings','ScalarStoresRequireFormat7','BatchStoresRequireFormat7')) { + $selection = @($tests | Where-Object { $_.testName.EndsWith('.'+$name) }) + if ($selection.Count -ne 1 -or $selection[0].outcome -notin @('Passed','Failed') -or + ($name.StartsWith('Fixed') -and $selection[0].outcome -ne 'Passed')) { throw "Missing/passing fixture witness: $name" } +} +# Independent opcode/form roster. Each opcode key has one case; status keys +# contain all 32 CCRs. Never accept a reduced, empty or foreign selection. +$required = @{} +function AddKeys([string]$Cohort, [int]$Mode, [int]$Register, [string]$Bank, [string]$Trace, [int]$Weight) { + foreach ($width in @(2,4)) { foreach ($transfer in 0..3) { for ($byte=0; $byte -lt $width; $byte++) { + $required["68040/MOVEM/write-fault/$Cohort/size=$width/mode=$Mode/reg=$Register/bank=$Bank/T=$Trace/transfer=$transfer/byte=$byte"]=$Weight + } } } +} +foreach ($mode in @(2,4,5,6,7)) { for ($register=0; $register -lt $(if ($mode -eq 7) {2} else {8}); $register++) { AddKeys opcode $mode $register ISP 0000 1 } } +foreach ($mode in @(2,4,5,6,7)) { foreach ($register in $(if ($mode -eq 7) {@(0,1)} else {@(0)})) { + foreach ($bank in @('user','user-M','ISP','MSP')) { foreach ($trace in @('0000','8000','4000')) { AddKeys status $mode $register $bank $trace 32 } } +} } +$rows = @() +foreach ($route in @('scalar','batch')) { + $control = Get-Content (Join-Path $output "68040-movem-write-fixture-control-$route.json") -Raw | ConvertFrom-Json + $controlTest = @($tests | Where-Object { $_.testName.EndsWith('.CanonicalStoreFixturesExecuteWithoutFault(batch: '+ $(if ($route -eq 'scalar') {'False'} else {'True'}) +')') }) + if ($controlTest.Count -ne 1 -or $controlTest[0].outcome -ne 'Passed' -or $control.schema -ne 1 -or + $control.model -ne '68040' -or $control.group -ne "movem-write-fixture-control-$route" -or + $control.logicalCases -ne 68 -or $control.xunitBatches -ne 1 -or $control.counts.passing -ne 68 -or + $control.counts.mismatching+$control.counts.unsupported+$control.counts.untested -ne 0 -or + @($control.combinations.psobject.Properties).Count -ne 68) { throw "Missing/passing fixture controls: $route" } + foreach ($mode in @(2,4,5,6,7)) { for ($register=0; $register -lt $(if ($mode -eq 7) {2} else {8}); $register++) { foreach ($width in @(2,4)) { + $key="68040/MOVEM/write-fault/fixture/size=$width/mode=$mode/reg=$register/bank=ISP/T=0000/transfer=3/byte=0" + if ($control.combinations.$key.passing -ne 1 -or @($control.combinations.$key.psobject.Properties).Count -ne 1) { throw "Wrong fixture control key/weight: $key" } + } } } + $report = Get-Content (Join-Path $output "68040-movem-write-fault-discovery-$route.json") -Raw | ConvertFrom-Json + if ($report.schema -ne 1 -or $report.model -ne '68040' -or $report.group -ne "movem-write-fault-discovery-$route" -or + $report.logicalCases -ne 56112 -or $report.xunitBatches -ne 1 -or @($report.combinations.psobject.Properties).Count -ne 2544) { throw "Incomplete coverage: $route" } + $sum = @{passing=0;mismatching=0;unsupported=0;untested=0} + foreach ($combination in $report.combinations.psobject.Properties) { + if (-not $required.ContainsKey($combination.Name)) { throw "Foreign combination: $($combination.Name)" } + $weight=0 + foreach ($status in $combination.Value.psobject.Properties) { + if (-not $sum.ContainsKey($status.Name) -or $status.Value -le 0) { throw 'Invalid status/weight' } + $sum[$status.Name]+=$status.Value; $weight+=$status.Value + } + if ($weight -ne $required[$combination.Name]) { throw "Changed combination weight: $($combination.Name)" } + } + foreach ($status in $sum.Keys) { if ($sum[$status] -ne $report.counts.$status) { throw 'Report totals differ from combinations' } } + $test = @($tests | Where-Object { $_.testName.EndsWith('.'+ $(if ($route -eq 'scalar') {'ScalarStoresRequireFormat7'} else {'BatchStoresRequireFormat7'})) })[0] + $nonpassing=$sum.mismatching+$sum.unsupported+$sum.untested + if (($nonpassing -eq 0) -ne ($test.outcome -eq 'Passed')) { throw 'TRX/report outcome disagreement' } + $rows += [pscustomobject]@{route=$route;logicalCases=$report.logicalCases;passing=$sum.passing;mismatching=$sum.mismatching;unsupported=$sum.unsupported;untested=$sum.untested} +} +$rows | Format-Table | Out-Host +if (@($rows | Where-Object { $_.passing -ne 56112 }).Count -ne 0 -or $manifest.testExit -ne 0) { + throw 'MOVEM write-fault acceptance failed; retain the original failing evidence. No gap is closed.' +} From 902b99cbca0ecee0a79ea1995be91ba8586ade10 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 02:50:37 +0300 Subject: [PATCH 084/211] Fix 68040 MOVEM write-fault frames and qualify recovery --- ...eticM68040MovemWriteFaultDiscoveryTests.cs | 108 +++++++++++++- .../SyntheticM68040MovemWriteRecoveryTests.cs | 77 ++++++++++ Copper68k/M68040Support.cs | 26 ++-- .../M68kAdvancedTimingInterpreter.Movem.cs | 14 +- Copper68k/M68kAdvancedTimingInterpreter.cs | 31 ++-- Copper68k/M68kCore.cs | 37 ++--- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 134 ++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 25 ++++ ...st-copper68k-040-movem-write-discovery.ps1 | 1 + ...est-copper68k-040-movem-write-recovery.ps1 | 103 ++++++++++++++ 10 files changed, 513 insertions(+), 43 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteRecoveryTests.cs create mode 100644 scripts/test-copper68k-040-movem-write-recovery.ps1 diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteFaultDiscoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteFaultDiscoveryTests.cs index 311fba87..4d5f2bd0 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteFaultDiscoveryTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteFaultDiscoveryTests.cs @@ -9,7 +9,7 @@ namespace Copper68k.Tests.Synthetic; public sealed class SyntheticM68040MovemWriteFaultDiscoveryTests(ITestOutputHelper output) { internal const string Enable = "COPPER68K_RUN_040_MOVEM_WRITE_DISCOVERY"; - private const uint Handler = 0xa000, Vbr = 0x900000; + private const uint Handler = 0xa000, TraceHandler = 0xb000, Vbr = 0x900000; [Fact, Trait("Suite", "ReferenceDiscovery")] public void FixedManualStoreEncodings() @@ -73,16 +73,19 @@ void Cases(string cohort, OperandForm form, string bank, ushort trace, int ccr) } } - private static void Case(CoverageBatch report, bool batch, string cohort, OperandForm form, - int width, int transfer, int faultByte, string bank, ushort trace, int ccr, bool reject = true) + internal static void Case(CoverageBatch report, bool batch, string cohort, OperandForm form, + int width, int transfer, int faultByte, string bank, ushort trace, int ccr, bool reject = true, + bool recover = false, IndexFixture? index = null, bool overwritePointer = false) { var opcode = Opcode(form, width); - var id = $"68040/MOVEM/write-fault/{cohort}/size={width}/mode={form.Mode}/reg={form.Register}/bank={bank}/T={trace:X4}/transfer={transfer}/byte={faultByte}/op={opcode:X4}/ccr={ccr:X2}"; + var indexedId = index is null ? "" : $"/index={index.Id}/ix={(index.AddressIndex ? "A" : "D")}{index.IndexRegister}/{(index.LongIndex ? "L" : "W")}/scale={1 << index.Scale}/pointer-alias={overwritePointer}"; + var id = $"68040/MOVEM/write-fault/{cohort}/size={width}/mode={form.Mode}/reg={form.Register}{indexedId}/bank={bank}/T={trace:X4}/transfer={transfer}/byte={faultByte}/op={opcode:X4}/ccr={ccr:X2}"; try { var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); m.Reset(ccr); m.Core.State.D[0] = 0x11112222; m.Core.State.D[1] = 0xffffffe0; + if (index?.AddressIndex == true) m.Core.State.A[0] = 0xffffffe0; var sr = (ushort)(0x0700 | M68040StackFixture.Status(bank, trace, ccr)); var stacks = new Dictionary { ["user"] = 0x7800, ["ISP"] = 0x4700, ["MSP"] = 0x7400 }; SetStacks(); @@ -90,20 +93,37 @@ private static void Case(CoverageBatch report, bool batch, string cohort, Operan var words = new List { opcode, (ushort)(form.Mode == 4 ? 0xc0c0 : 0x0303) }; var fixture = new AddressingFixture(m, width, words, (uint[])m.Core.State.D.Clone(), (uint[])m.Core.State.A.Clone(), 0, new(DestinationIndex: - new(IndexRegister: 1, LongIndex: true, BriefDisplacement: 64), + index ?? new(IndexRegister: 1, LongIndex: true, BriefDisplacement: 64), DestinationAbsoluteWord: 0xf000, DestinationAbsoluteLong: 0x12346200)); var first = fixture.Resolve(form, false); + if (overwritePointer) + { + if (index is not { Full: true, BaseSize: 2, Indirect: 1, SuppressBase: false, SuppressIndex: false } || form != new OperandForm(6, 0)) + throw new InvalidOperationException("Unsupported self-overwriting pointer fixture"); + first = unchecked(registers[8] - 32 + registers[1]); + if (fixture.DestinationPointerAddress != first) throw new InvalidOperationException("Wrong independently intended pointer location"); + } var ea = form.Mode == 4 ? registers[8 + form.Register] : first; var addresses = Enumerable.Range(0, 4).Select(n => unchecked(first + (uint)(form.Mode == 4 ? -n * width : n * width))).ToArray(); int[] order = form.Mode == 4 ? [9, 8, 1, 0] : [0, 1, 8, 9]; var values = order.Select(r => registers[r] - (form.Mode == 4 && r == 8 + form.Register ? (uint)width : 0u)) .Select(v => width == 2 ? v & 0xffff : v).ToArray(); foreach (var at in addresses) m.InitializePhysical(at, 0x5a5aa5a5, width); + if (overwritePointer) m.InitializePhysical(first, first, 4); for (var n = 0; n < words.Count; n++) m.InitializePhysical(0x1000 + (uint)n * 2, words[n], 2); m.InitializePhysical(fixture.NextPc, 0x7e55, 2); + m.InitializePhysical(fixture.NextPc + 2, 0x6002, 2); m.InitializePhysical(fixture.NextPc + 6, 0x4e71, 2); m.InitializePhysical(Vbr + 8, Handler, 4); m.InitializePhysical(Handler, 0x4e71, 2); m.InitializePhysical(Handler + 2, 0x4e71, 2); + var program = recover ? new SyntheticM68040WritebackProgram(Handler) : null; + program?.Initialize(m); + m.InitializePhysical(Vbr + 36, TraceHandler, 4); + m.InitializePhysical(TraceHandler, 0x0257, 2); m.InitializePhysical(TraceHandler + 2, 0x3fff, 2); + m.InitializePhysical(TraceHandler + 4, 0x4e73, 2); m.Start(); SetStacks(); m.Core.State.VectorBaseRegister = Vbr; + m.Core.State.DestinationFunctionCode = 3; m.Core.State.SourceFunctionCode = 6; var e = ArchitecturalExpectation.Capture(m); var sequence = m.Core.State.ExceptionSequence; + e.ControlChecks["DFC"] = (s => s.DestinationFunctionCode, 3); + e.ControlChecks["SFC"] = (s => s.SourceFunctionCode, 6); if (!reject) { for (var n = 0; n < 4; n++) e.Write(addresses[n], values[n], width, m.Model); @@ -161,6 +181,64 @@ private static void Case(CoverageBatch report, bool batch, string cohort, Operan if (bus.Accesses.Skip(rejected.AccessCount).Any(a => a.Write && (a.Address < frame || a.Address >= frame + 60))) throw new InvalidOperationException("Operand store repeated during exception entry"); if (e.Verify(m) is { } mismatch) throw new InvalidOperationException("entry: " + mismatch); + if (recover) + { + // Undefined lanes/slots are opaque handler inputs, not expected + // hardware values. Every defined WB1 lane was checked above. + for (uint n = 8; n < 60; n++) e.Memory[frame + n] = bus.Peek(frame + n); + e.MemoryMasks.Clear(); + ushort[] statuses = [(ushort)(0x80 | size | tm), 0, 0]; + uint[] wbAddresses = [addresses[transfer], 0, 0]; + uint[] encoded = [m.PeekPhysical(frame + 44, 4), 0, 0]; + var originalD = (uint[])e.D.Clone(); var originalA = (uint[])e.A.Clone(); + var handlerStores = new List<(uint Address, int Width, uint Value)>(); + var steps = 0; + while (e.Pc != 0x1000) + { + if (++steps > 120) throw new InvalidOperationException("Handler failed to terminate"); + var instruction = program!.Instructions.Single(i => i.Pc == e.Pc); + program.Expect(instruction, m, e, frame, statuses, wbAddresses, encoded, originalD, originalA, 3, stacks, handlerStores); + if (instruction.Operation == "return") + { stacks[exceptionBank] = frame + 60; M68040StackFixture.SetStacks(e, stacks, sr); e.Pc = 0x1000; } + Execute(); + if (e.Verify(m) is { } handlerMismatch) throw new InvalidOperationException($"handler {instruction.Operation}/WB{instruction.Slot}: {handlerMismatch}"); + } + if (!handlerStores.SequenceEqual(new[] { (addresses[transfer], width, values[transfer]) })) + throw new InvalidOperationException("WB1 handler did not complete the independently intended store"); + for (var n = 0; n < 4; n++) e.Write(addresses[n], values[n], width, m.Model); + if (form.Mode == 4) + { + e.A[form.Register] = unchecked(ea - (uint)(4 * width)); + if (form.Register == 7) stacks[M68040StackFixture.PhysicalBank(bank)] = e.A[7]; + } + M68040StackFixture.SetStacks(e, stacks, sr); e.Pc = fixture.NextPc; + if (trace == 0x8000) ExpectTrace(fixture.NextPc, 0x1000, sr); + var resumedStart = bus.Accesses.Count; + Execute(); + if (e.Verify(m) is { } resumedMismatch) throw new InvalidOperationException("resumed MOVEM: " + resumedMismatch); + var resumedWrites = bus.Accesses.Skip(resumedStart).Where(a => a.Write).Take(4) + .Select(a => (a.Address, a.Width, a.Value)); + if (!resumedWrites.SequenceEqual(addresses.Select((at, n) => (at, width, values[n])))) + throw new InvalidOperationException("CM did not repeat the original four operand writes in order"); + if (fixture.DestinationPointerAddress is { } pointer && + bus.Accesses.Count(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address == pointer && a.Width == 4) != 1) + throw new InvalidOperationException("CM repeated indirect-pointer resolution"); + if (bus.Rejected.Count != 1) throw new InvalidOperationException("Unexpected repeated physical rejection"); + var resumedSr = sr; + if (trace == 0x8000) { ClearTrace(sr, fixture.NextPc); resumedSr &= 0x3fff; } + e.D[7] = 0x55; e.Sr = (ushort)(resumedSr & 0xfff0); e.Pc = fixture.NextPc + 2; + Execute(); + if (e.Verify(m) is { } sentinelMismatch) throw new InvalidOperationException("following MOVEQ: " + sentinelMismatch); + if (trace == 0x4000) + { + var branchSr = e.Sr; ExpectTrace(fixture.NextPc + 6, fixture.NextPc + 2, branchSr); + Execute(); + if (e.Verify(m) is { } branchMismatch) throw new InvalidOperationException("following flow trace: " + branchMismatch); + ClearTrace(branchSr, fixture.NextPc + 6); + e.Pc = fixture.NextPc + 8; Execute(); + if (e.Verify(m) is { } nopMismatch) throw new InvalidOperationException("following NOP: " + nopMismatch); + } + } report.Record(id, "passing", null); void Define(uint offset, uint value, int count) @@ -174,6 +252,26 @@ void Execute() var count = ((IM68kBatchCore)m.Core).ExecuteInstructions(1, m.Core.State.Cycles + 10000, boundary); if (count != 1 || boundary.Before != 1 || boundary.After != 1) throw new InvalidOperationException("Batch count/callbacks differ"); } + void ExpectTrace(uint next, uint origin, ushort savedSr) + { + var traceBank = M68040StackFixture.ExceptionBank(savedSr); var traceFrame = stacks[traceBank] - 12; + stacks[traceBank] = traceFrame; + M68040StackFixture.SetStacks(e, stacks, (ushort)((savedSr | 0x2000) & ~0xc000)); + e.Pc = TraceHandler; e.ExceptionVector = 9; + e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence + 2); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, next); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, savedSr); + e.Write(traceFrame, savedSr, 2, m.Model); e.Write(traceFrame + 2, next, 4, m.Model); + e.Write(traceFrame + 6, 0x2024, 2, m.Model); e.Write(traceFrame + 8, origin, 4, m.Model); + } + void ClearTrace(ushort savedSr, uint next) + { + var traceBank = M68040StackFixture.ExceptionBank(savedSr); var traceFrame = stacks[traceBank]; + e.Write(traceFrame, (uint)(savedSr & 0x3fff), 2, m.Model); e.Sr &= 0xfff0; e.Pc = TraceHandler + 4; + Execute(); if (e.Verify(m) is { } clearMismatch) throw new InvalidOperationException("trace-handler ANDI: " + clearMismatch); + stacks[traceBank] += 12; M68040StackFixture.SetStacks(e, stacks, (ushort)(savedSr & 0x3fff)); e.Pc = next; + Execute(); if (e.Verify(m) is { } rteMismatch) throw new InvalidOperationException("trace-handler RTE: " + rteMismatch); + } } catch (Exception ex) { diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteRecoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteRecoveryTests.cs new file mode 100644 index 00000000..839016b1 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteRecoveryTests.cs @@ -0,0 +1,77 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticM68040MovemWriteRecoveryTests(ITestOutputHelper output) +{ + internal const string Enable = "COPPER68K_RUN_040_MOVEM_WRITE_RECOVERY"; + + [Theory, Trait("Suite", "ReferenceDiscovery")] + [InlineData(false)] + [InlineData(true)] + public void EveryCanonicalStoreFormRunsHandlerAndResumes(bool batch) + { + var report = new CoverageBatch("68040", "movem-write-recovery-witness-" + Route(batch)); + foreach (var mode in new[] { 2, 4, 5, 6, 7 }) + for (var register = 0; register < (mode == 7 ? 2 : 8); register++) + foreach (var width in new[] { 2, 4 }) + SyntheticM68040MovemWriteFaultDiscoveryTests.Case(report, batch, "recovery-witness", new(mode, register), + width, 3, width - 1, "ISP", 0, 31, recover: true); + report.Complete(output); + } + + [Theory, Trait("Suite", "ReferenceDiscovery")] + [InlineData(false)] + [InlineData(true)] + public void SelfOverwrittenIndirectPointerIsNotResolvedAgain(bool batch) + { + var report = new CoverageBatch("68040", "movem-write-pointer-alias-" + Route(batch)); + foreach (var width in new[] { 2, 4 }) + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var trace in new ushort[] { 0, 0x8000, 0x4000 }) + SyntheticM68040MovemWriteFaultDiscoveryTests.Case(report, batch, "pointer-alias", new(6, 0), + width, 3, width - 1, bank, trace, 31, recover: true, + index: new(Full: true, IndexRegister: 1, LongIndex: true, BaseSize: 2, Indirect: 1), overwritePointer: true); + report.Complete(output); + } + + [EnvironmentFact(Enable, "qualify actual MOVEM write faults, WB1 handler and RTE resumption"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarRecoveryMatrix() => Audit(false); + + [EnvironmentFact(Enable, "qualify actual MOVEM write faults, WB1 handler and RTE resumption"), Trait("Suite", "ReferenceDiscovery")] + public void BatchRecoveryMatrix() => Audit(true); + + private static string Route(bool batch) => batch ? "batch" : "scalar"; + private void Audit(bool batch) + { + var report = new CoverageBatch("68040", "movem-write-recovery-" + Route(batch)); + foreach (var mode in new[] { 2, 4, 5, 6, 7 }) + for (var register = 0; register < (mode == 7 ? 2 : 8); register++) + Cases("recovery-opcode", new(mode, register), "ISP", 0, 31); + foreach (var form in new[] { new OperandForm(2, 0), new(4, 0), new(4, 7), new(5, 0), new(6, 0), new(7, 0), new(7, 1) }) + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var trace in new ushort[] { 0, 0x8000, 0x4000 }) + for (var ccr = 0; ccr < 32; ccr++) Cases("recovery-status", form, bank, trace, ccr); + foreach (var spec in IndexFixture.FullStructures()) + foreach (var index in new[] { + spec with { IndexRegister = 1, LongIndex = false, Scale = 0 }, + spec with { IndexRegister = 1, LongIndex = true, Scale = 0 }, + spec with { IndexRegister = 1, LongIndex = false, Scale = 3 }, + spec with { AddressIndex = true, IndexRegister = 0, LongIndex = false, Scale = 2 } }) + foreach (var width in new[] { 2, 4 }) + for (var transfer = 0; transfer < 4; transfer++) + for (var faultByte = 0; faultByte < width; faultByte++) + SyntheticM68040MovemWriteFaultDiscoveryTests.Case(report, batch, "recovery-structure", new(6, 1), + width, transfer, faultByte, "ISP", 0, 31, recover: true, index: index); + report.Complete(output); + + void Cases(string cohort, OperandForm form, string bank, ushort trace, int ccr) + { + foreach (var width in new[] { 2, 4 }) + for (var transfer = 0; transfer < 4; transfer++) + for (var faultByte = 0; faultByte < width; faultByte++) + SyntheticM68040MovemWriteFaultDiscoveryTests.Case(report, batch, cohort, form, + width, transfer, faultByte, bank, trace, ccr, recover: true); + } + } +} diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index d60ee9f9..4db8842b 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -2564,6 +2564,11 @@ protected override bool TryHandleM68040ExecutionFault(M68040MmuFault fault) fault = fault with { StackedProgramCounter = ExecutionBoundaryProgramCounter }; else if (TryCompleteFaultedMoveWrite(fault)) fault = fault with { StackedProgramCounter = State.ProgramCounter, CompletedMoveWrite = true }; + else if (!State.M68040Mmu.Enabled && fault.Write && fault.MovemEffectiveAddress.HasValue && + fault.AccessKind == M68kBusAccessKind.CpuDataWrite && + State.ExceptionSequence == ExecutionBoundaryExceptionSequence && + State.LastInstructionProgramCounter == ExecutionBoundaryProgramCounter) + fault = fault with { StackedProgramCounter = ExecutionBoundaryProgramCounter }; else if (!State.M68040Mmu.Enabled && !fault.Write && fault.AccessKind == M68kBusAccessKind.CpuDataRead && fault.ByteCount is 1 or 2 or 4 && State.ExceptionSequence == ExecutionBoundaryExceptionSequence && @@ -4332,8 +4337,9 @@ private void RaiseMmuFault(M68040MmuFault fault) RaiseRteValidationAccessFault(fault, stackedProgramCounter); else if (!State.M68040Mmu.Enabled && fault.AccessKind == M68kBusAccessKind.CpuInstructionFetch) RaiseUnbufferedReadAccessFault(fault, stackedProgramCounter, instruction: true); - else if (fault.CompletedMoveWrite) - RaiseCompletedMoveWriteAccessFault(fault, stackedProgramCounter); + else if (fault.CompletedMoveWrite || (!State.M68040Mmu.Enabled && fault.Write && + fault.MovemEffectiveAddress.HasValue && fault.AccessKind == M68kBusAccessKind.CpuDataWrite)) + RaiseNormalWriteAccessFault(fault, stackedProgramCounter); else if (fault.UnbufferedOperandRead) RaiseUnbufferedReadAccessFault(fault, stackedProgramCounter, instruction: false); else @@ -4353,29 +4359,31 @@ private void RaiseMmuFault(M68040MmuFault fault) private void RaiseRteValidationAccessFault(M68040MmuFault fault, uint instructionPc) => RaiseUnbufferedReadAccessFault(fault, instructionPc, instruction: false); - private void RaiseCompletedMoveWriteAccessFault(M68040MmuFault fault, uint nextPc) + private void RaiseNormalWriteAccessFault(M68040MmuFault fault, uint stackedPc) { // MC68040UM 8.4.6.5/7: physical normal write -> valid WB1, FA=WB1A, - // memory-aligned data. RTE resumes at the following instruction. + // memory-aligned data. A completed MOVE resumes at its following + // instruction. MOVEM remains suspended with CM and its original EA; + // software completes WB1 before RTE repeats operand transfers. var savedSr = State.StatusRegister; var size = fault.ByteCount == 1 ? 0x20 : fault.ByteCount == 2 ? 0x40 : 0; var modifier = (savedSr & M68kCpuState.Supervisor) != 0 ? 5 : 1; - var trace = (savedSr & 0x8000) != 0; // MOVE does not trigger T0. + var trace = fault.CompletedMoveWrite && (savedSr & 0x8000) != 0; // MOVE does not trigger T0. uint data = fault.WriteValue!.Value; if (fault.ByteCount == 1) data = (data & 0xff) << 24; else if (fault.ByteCount == 2) data = (data & 0xffff) << 16; var shift = (int)(fault.LogicalAddress & 3) * 8; if (shift != 0) data = (data >> shift) | (data << (32 - shift)); - State.RecordException(VectorBusError, nextPc, savedSr); + State.RecordException(VectorBusError, stackedPc, savedSr); State.StatusRegister = (ushort)((savedSr | M68kCpuState.Supervisor) & ~0xc000); for (var n = 0; n < 3; n++) PushLong(0); // Undefined push data. PushLong(data); PushLong(fault.LogicalAddress); for (var n = 0; n < 4; n++) PushLong(0); // No WB2/WB3 in this synchronous path. PushLong(fault.LogicalAddress); PushWord((ushort)(0x80 | size | modifier)); PushWord(0); PushWord(0); - PushWord((ushort)((trace ? 0x2000 : 0) | size | modifier)); - PushLong(trace ? State.LastInstructionProgramCounter : 0); - PushWord(0x7008); PushLong(nextPc); PushWord(savedSr); + PushWord((ushort)((trace ? 0x2000 : fault.MovemEffectiveAddress.HasValue ? 0x1000 : 0) | size | modifier)); + PushLong(fault.MovemEffectiveAddress ?? (trace ? State.LastInstructionProgramCounter : 0)); + PushWord(0x7008); PushLong(stackedPc); PushWord(savedSr); EnterAccessErrorHandler(); } diff --git a/Copper68k/M68kAdvancedTimingInterpreter.Movem.cs b/Copper68k/M68kAdvancedTimingInterpreter.Movem.cs index 68627e60..209263c5 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.Movem.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.Movem.cs @@ -16,6 +16,18 @@ private uint ReadMovemSized(uint address, M68kOperandSize size, uint calculatedA throw new M68040MmuFaultException(ex.Fault with { MovemEffectiveAddress = calculatedAddress }); } } + private void WriteMovemSized(uint address, uint value, M68kOperandSize size, uint calculatedAddress) + { + try { WriteSized(address, value, size); } + catch (M68040MmuFaultException ex) when (_profile.Model == M68kAcceleratorModel.M68040 && + !State.M68040Mmu.Enabled && ex.Fault.Write && ex.Fault.AccessKind == M68kBusAccessKind.CpuDataWrite) + { + // CM belongs to a MOVEM operand fault, not address calculation or + // exception-frame writes. Keep the EA before any store can alter + // memory used by indexed addressing. No instruction replay here. + throw new M68040MmuFaultException(ex.Fault with { MovemEffectiveAddress = calculatedAddress }); + } + } private bool TryExecuteGeneralMovem(ushort opcode, uint? continuationAddress = null) { if ((opcode & 0xfb80) != 0x4880) return false; @@ -62,7 +74,7 @@ private bool TryExecuteGeneralMovem(ushort opcode, uint? continuationAddress = n // M68000PM MOVEM: 020+ stores the initial base minus ONE // operand size when the predecrement base is in the list. if (mode == 4 && register == baseRegister + 8) value = unchecked(value - (uint)size); - WriteSized(address, value, size); + WriteMovemSized(address, value, size, calculatedAddress); } if (mode != 4) address = unchecked(address + (uint)size); } diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index 8fa83a65..25ca2e25 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -9140,6 +9140,7 @@ private void ExecuteMovemLongRegistersToPredecrement(ushort opcode) var mask = FetchWord(); var addressRegister = opcode & 7; var address = State.A[addressRegister]; + var calculatedAddress = address; var dataSnapshot = new uint[8]; var addressSnapshot = new uint[8]; Array.Copy(State.D, dataSnapshot, dataSnapshot.Length); @@ -9158,7 +9159,7 @@ private void ExecuteMovemLongRegistersToPredecrement(ushort opcode) ? addressSnapshot[7 - bit] : dataSnapshot[15 - bit]; address -= 4; - WriteLong(address, value); + WriteMovemSized(address, value, M68kOperandSize.Long, calculatedAddress); } WriteGeneralRegister(true, addressRegister, address); @@ -9176,6 +9177,7 @@ private void ExecuteMovemWordRegistersToPredecrement(ushort opcode) var mask = FetchWord(); var addressRegister = opcode & 7; var address = State.A[addressRegister]; + var calculatedAddress = address; var dataSnapshot = new uint[8]; var addressSnapshot = new uint[8]; Array.Copy(State.D, dataSnapshot, dataSnapshot.Length); @@ -9194,7 +9196,7 @@ private void ExecuteMovemWordRegistersToPredecrement(ushort opcode) ? addressSnapshot[7 - bit] : dataSnapshot[15 - bit]; address = unchecked(address - 2u); - WriteWord(address, (ushort)value); + WriteMovemSized(address, value, M68kOperandSize.Word, calculatedAddress); } WriteGeneralRegister(true, addressRegister, address); @@ -9212,6 +9214,7 @@ private void ExecuteMovemWordRegistersToAddressDisplacement(ushort opcode) var addressRegister = opcode & 7; var displacement = unchecked((int)(short)FetchWord()); var address = unchecked((uint)(State.A[addressRegister] + displacement)); + var calculatedAddress = address; var dataSnapshot = new uint[8]; var addressSnapshot = new uint[8]; Array.Copy(State.D, dataSnapshot, dataSnapshot.Length); @@ -9221,7 +9224,7 @@ private void ExecuteMovemWordRegistersToAddressDisplacement(ushort opcode) { if ((mask & (1 << register)) != 0) { - WriteWord(address, (ushort)dataSnapshot[register]); + WriteMovemSized(address, dataSnapshot[register], M68kOperandSize.Word, calculatedAddress); address += 2; } } @@ -9230,7 +9233,7 @@ private void ExecuteMovemWordRegistersToAddressDisplacement(ushort opcode) { if ((mask & (1 << (8 + register))) != 0) { - WriteWord(address, (ushort)addressSnapshot[register]); + WriteMovemSized(address, addressSnapshot[register], M68kOperandSize.Word, calculatedAddress); address += 2; } } @@ -9249,6 +9252,7 @@ private void ExecuteMovemLongRegistersToAddressIndirect(ushort opcode) var mask = FetchWord(); var addressRegister = opcode & 7; var address = State.A[addressRegister]; + var calculatedAddress = address; var dataSnapshot = new uint[8]; var addressSnapshot = new uint[8]; Array.Copy(State.D, dataSnapshot, dataSnapshot.Length); @@ -9258,7 +9262,7 @@ private void ExecuteMovemLongRegistersToAddressIndirect(ushort opcode) { if ((mask & (1 << register)) != 0) { - WriteLong(address, dataSnapshot[register]); + WriteMovemSized(address, dataSnapshot[register], M68kOperandSize.Long, calculatedAddress); address += 4; } } @@ -9267,7 +9271,7 @@ private void ExecuteMovemLongRegistersToAddressIndirect(ushort opcode) { if ((mask & (1 << (8 + register))) != 0) { - WriteLong(address, addressSnapshot[register]); + WriteMovemSized(address, addressSnapshot[register], M68kOperandSize.Long, calculatedAddress); address += 4; } } @@ -9287,6 +9291,7 @@ private void ExecuteMovemLongRegistersToAddressDisplacement(ushort opcode) var addressRegister = opcode & 7; var displacement = unchecked((int)(short)FetchWord()); var address = unchecked((uint)(State.A[addressRegister] + displacement)); + var calculatedAddress = address; var dataSnapshot = new uint[8]; var addressSnapshot = new uint[8]; Array.Copy(State.D, dataSnapshot, dataSnapshot.Length); @@ -9296,7 +9301,7 @@ private void ExecuteMovemLongRegistersToAddressDisplacement(ushort opcode) { if ((mask & (1 << register)) != 0) { - WriteLong(address, dataSnapshot[register]); + WriteMovemSized(address, dataSnapshot[register], M68kOperandSize.Long, calculatedAddress); address += 4; } } @@ -9305,7 +9310,7 @@ private void ExecuteMovemLongRegistersToAddressDisplacement(ushort opcode) { if ((mask & (1 << (8 + register))) != 0) { - WriteLong(address, addressSnapshot[register]); + WriteMovemSized(address, addressSnapshot[register], M68kOperandSize.Long, calculatedAddress); address += 4; } } @@ -9325,6 +9330,7 @@ private void ExecuteMovemLongRegistersToBriefIndexed(ushort opcode) var addressRegister = opcode & 7; var extension = FetchWord(); var address = CalculateIndexedOperandAddress(addressRegister, extension, opcode); + var calculatedAddress = address; var dataSnapshot = new uint[8]; var addressSnapshot = new uint[8]; Array.Copy(State.D, dataSnapshot, dataSnapshot.Length); @@ -9334,7 +9340,7 @@ private void ExecuteMovemLongRegistersToBriefIndexed(ushort opcode) { if ((mask & (1 << register)) != 0) { - WriteLong(address, dataSnapshot[register]); + WriteMovemSized(address, dataSnapshot[register], M68kOperandSize.Long, calculatedAddress); address += 4; } } @@ -9343,7 +9349,7 @@ private void ExecuteMovemLongRegistersToBriefIndexed(ushort opcode) { if ((mask & (1 << (8 + register))) != 0) { - WriteLong(address, addressSnapshot[register]); + WriteMovemSized(address, addressSnapshot[register], M68kOperandSize.Long, calculatedAddress); address += 4; } } @@ -9361,6 +9367,7 @@ private void ExecuteMovemLongRegistersToAbsolute(ushort opcode, bool absoluteWor _ = FetchWord(); var mask = FetchWord(); var address = absoluteWord ? unchecked((uint)(int)(short)FetchWord()) : FetchLong(); + var calculatedAddress = address; var dataSnapshot = new uint[8]; var addressSnapshot = new uint[8]; Array.Copy(State.D, dataSnapshot, dataSnapshot.Length); @@ -9370,7 +9377,7 @@ private void ExecuteMovemLongRegistersToAbsolute(ushort opcode, bool absoluteWor { if ((mask & (1 << register)) != 0) { - WriteLong(address, dataSnapshot[register]); + WriteMovemSized(address, dataSnapshot[register], M68kOperandSize.Long, calculatedAddress); address += 4; } } @@ -9379,7 +9386,7 @@ private void ExecuteMovemLongRegistersToAbsolute(ushort opcode, bool absoluteWor { if ((mask & (1 << (8 + register))) != 0) { - WriteLong(address, addressSnapshot[register]); + WriteMovemSized(address, addressSnapshot[register], M68kOperandSize.Long, calculatedAddress); address += 4; } } diff --git a/Copper68k/M68kCore.cs b/Copper68k/M68kCore.cs index f2cb9355..59e6f09e 100644 --- a/Copper68k/M68kCore.cs +++ b/Copper68k/M68kCore.cs @@ -12312,6 +12312,24 @@ private bool DecodeShiftRotate(ushort opcode) return true; } + private void WriteMovemOperand(uint address, uint value, M68kOperandSize size, uint calculatedAddress, bool descending) + { + try + { + if (size == M68kOperandSize.Word) WriteWord(address, (ushort)value); + else if (descending) WriteLongDescending(address, value); + else WriteLong(address, value); + } + catch (M68040MmuFaultException ex) when (!State.M68040Mmu.Enabled && + ex.Fault.Write && ex.Fault.AccessKind == M68kBusAccessKind.CpuDataWrite) + { + // The 040 fallback shares this decoder. Preserve its existing + // split-store order and the original operand size/data latched + // by the logical bus; add only MOVEM's calculated EA for CM. + throw new M68040MmuFaultException(ex.Fault with { MovemEffectiveAddress = calculatedAddress }); + } + } + private void DecodeMovem(ushort opcode) { var size = (opcode & 0x0040) == 0 ? M68kOperandSize.Word : M68kOperandSize.Long; @@ -12323,6 +12341,7 @@ private void DecodeMovem(ushort opcode) if (!directionMemoryToRegisters && mode == 4) { var address = State.A[reg]; + var calculatedAddress = address; _dataAccessStackedProgramCounter = State.ProgramCounter + 2; var predecrementTransferCycles = size == M68kOperandSize.Long ? 8 : 4; var predecrementTransferred = 0; @@ -12339,14 +12358,7 @@ private void DecodeMovem(ushort opcode) if (_useM68020BriefIndexedAddressing && register == reg + 8) value = unchecked(value - (uint)size); AddInstructionCycles(12 + (predecrementTransferred * predecrementTransferCycles)); - if (size == M68kOperandSize.Word) - { - WriteWord(address, (ushort)value); - } - else - { - WriteLongDescending(address, value); - } + WriteMovemOperand(address, value, size, calculatedAddress, descending: true); predecrementTransferred++; } @@ -12402,14 +12414,7 @@ private void DecodeMovem(ushort opcode) var writeFaultBaseCycles = size == M68kOperandSize.Word ? 8 : 4; AddInstructionCycles(writeFaultBaseCycles + ea.EaCycles + (transferred * transferCycles)); var value = register < 8 ? State.D[register] : State.A[register - 8]; - if (size == M68kOperandSize.Word) - { - WriteWord(current, (ushort)value); - } - else - { - WriteLong(current, value); - } + WriteMovemOperand(current, value, size, ea.Address, descending: false); } current += (uint)size; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index e3f13588..c0988ffb 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -7562,3 +7562,137 @@ production source change, regression retirement, new full CPU/consumer run or package publication is claimed. Normal CPU/test assemblies and unrelated CopperScreen edits remain untouched. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## Physical 040 MOVEM write-frame correction — 2026-10-07 + +The working correction addresses the preceding actual-write discovery. Fast, +general and shared-fallback MOVEM store paths retain the original calculated EA +only when an operand write faults. Pre-EA pointer reads and exception-frame +writes cannot acquire that MOVEM write metadata. Existing operand ordering, +predecrement register-list semantics and timing-plan placement remain unchanged; +the shared fallback keeps its descending split-word long stores and the logical +bus's original operand address/size/data latch. + +Normal physical MOVEM write faults now use format 7, original instruction PC, +CM/original EA and a valid memory-aligned WB1. The shared normal-write builder +retains completed MOVE's distinct following-PC/CT behavior. MOVEM remains +suspended: exception delivery never retries it, software completes WB1, then +RTE explicitly resumes the MOVEM. Enabled MMU/cache/pipeline behavior and other +physical-write families are not promoted by this correction. + +Fresh focused evidence: + +- The complete original selected entry gate passes **112,224 cases**, zero + mismatches/unsupported/untested selections, plus its literal witness and + **136 fault-free controls**. Five executions pass with no skips. Manifest + `audits/MovemWriteEntryAcceptance/inputs.json` SHA-256: + `6bef57d174ba6e083f3fc32b056ee427d2ec62c8d5c40c959fb5dd985f9d494f`. +- Executed writeback-handler/RTE/resumption qualification passes **143,512 + whole programs / six reports / six executions**, zero selected gaps. Each + route has 816 store-opcode cases, 64,512 canonical stack/CCR/trace cases and + 6,336 full-format structural cases, plus 68 canonical witnesses and 24 + self-overwriting-pointer witnesses. The full-format cohort independently + covers all 66 legal structures with signed W/L D1 and signed/scaled A0/D1 + index variants. Status cases include predecrement A7 in all four stack states. +- Actual handler instructions preserve/restore scratch registers and DFC, + complete WB1 with MOVES, clear its valid flag, and execute RTE. The resumed + MOVEM repeats all four operand writes in order. A pointer overwritten by an + earlier store is not resolved again. Exact following-instruction boundaries, + T1 completion trace, deferred T0 flow trace, registers, defined frame/CCR + fields, surrounding memory and partial pre-fault stores are checked. +- Discarding shared-fallback write-EA metadata in an owned copy produces **40 + intended frame mismatches / 96 unchanged passing witnesses**. Recalculating + the address instead of consuming CM's saved EA produces **48 intended + resumed-MOVEM mismatches**, exposing the overwritten pointer. Both routes + fail, with exact phase diagnostics. Proof: `mutations/MovemWriteCorrection`. +- Seven copied-evidence corruption controls pass for each current entry and + recovery gate: missing/changed source, missing report, empty execution, + changed weight, lost key and false witness weight. Source, binary, report, + exact TRX roster and independent weighted combination inventories are frozen. + +Reproduce whole-program recovery with: + +```powershell +./scripts/test-copper68k-040-movem-write-recovery.ps1 -OutputDirectory artifacts/movem-write-recovery +``` + +A fresh output is required. `-ValidateReportsOnly` checks frozen identities and +coverage without rebuilding. Missing fixtures, empty selections, mismatches or +unsupported execution fail. The entry command from the preceding section now +passes its unchanged architectural requirements. Recovery evidence is retained +in `audits/MovemWriteRecoveryAcceptance` (manifest SHA-256 +`2a5717f542d817e2b39d59520335252046266af575bcc90abe6d4dad61f518af`), +`MovemWriteRecoveryIntegrity` and `MovemWriteEntryAcceptanceIntegrity`. +The first recovery witness run revealed a fixture initialization error: reset +cleared DFC before handler execution. DFC/SFC are now set after reset and checked +explicitly. That provisional failing run is retained separately. + +Fresh independent audits under `audits/MovemWriteIndependent` pass **312,500 +SingleStepTests cases / 125 files**, with the established TAS/TRAPV exclusions, +and **536 Musashi model/program combinations / 88 explicit exclusions**. Pinned +sources and caveats remain unchanged. These software references do not qualify +physical bus timing or the new fault protocol by themselves. + +Clean CopperScreen commit `aa1dad5dcc0fb7c970e8e3443a160487add846af` builds with +zero warnings/errors using private **1.5.2-synthetic-dev.69** through its existing +NuGet boundary. **171 host**, **74 disk**, and separate **1,080 engine** tests pass; +six optional host replays remain unavailable in that ordinary invocation. +Two explicitly supplied native Workbench 3.1 floppy replays, zero/2 MiB Fast RAM, +pass the retained ROM/media, PC, cycle and framebuffer fingerprints. Actual app, +host-test and engine-test CPU DLLs match the exact package. Evidence: +`consumer-69-final/consumer-verification.json`; package SHA-256 +`eed6baf7d1c2dfae99f83825904cc1e729ee2f4ad985a45fd43c3ef65f9c70e1`; +packaged CPU DLL SHA-256 +`5528cb66670a6afc92fe4ddadb2c20425146995a4de63098b27845b174dc1c65`. +The package was created once and is immutable. A lock-file property error and +an external helper's command-name collision affected provisional owned validation +attempts; the completed consumer uses a fresh clean archive and the original +package. No source repository or previously published package was overwritten. +No HD replay is claimed and no public package is published. + +The fresh full CPU run in `audits/MovemWriteFull` passes **5,255 tests**, zero +failures, with **29 explicit optional/discovery skips / 5,284 total** in +**56 minutes 43 seconds**. All ten previously qualified WinUAE presets execute +and pass. The maintained ordinary inventory and a separate actual-TRX audit +verify **707 required reports / 86,098,722 passing deterministic scenarios**, +including complete combination weights and passing execution identities. +The full run's opt-in write-entry/recovery skips are qualified separately by +the fresh focused commands above; remaining skipped coverage is not promoted. +Exact skip names are retained in `full-verification.json`. Source manifest +SHA-256: `7acedd9c5c632e10b9e7ee7ba6130a18d88b01106147d5bff3f785d56df840ce`; +TRX SHA-256: `c596190315220d8cb7ebeea51b93505b8f82b88c3cb2d8ee2c3b8ef92d541078`. +`ordinary-trx-verification.json` records every required report's hash, actual +test identity, scenario count and combination count. This is fresh passing +evidence for the correction, distinct from the prior read-only full run. +Normal CPU/test assemblies and unrelated CopperScreen changes are preserved. +The ordinary 707-report inventory is unchanged. Arbitrary MOVEM masks, broader +write-fault/nested/writeback protocols, the 480-case broad advanced inventory, +original Basic failures, other-model restoration and remaining consolidation +requirements stay open. Milestone 6 remains **in progress**, +`roadmapComplete=false`. + +## Transfer timing-regression retention audit — 2026-10-07 + +Six neighboring `M68020InterpreterTests` transfer facts were reviewed after the +pure EXTB semantic duplicate was retired. They remain required. The shared +`SyntheticTransferTests.RegisterTransferFamilies` matrix checks EXT.W/EXT.L, +SWAP and all EXG register forms semantically, but does not assert the named +accelerator profiles' native/machine-cycle policy. Semantic overlap alone is +insufficient replacement coverage. + +| Retained fact | Selected profile | Additional timing assertion | +| --- | --- | --- | +| `ExtLongDataRegisterSignExtendsWordToLong` | `OcsAccelerator14Mhz` | 4 native / 2 machine cycles | +| `ExtWordDataRegisterSignExtendsByteToWord` | `A1200Ec02014Mhz` | 2 native / 1 machine cycle | +| `SwapDataRegisterSwapsWordsAndSetsFlags` | `OcsAccelerator14Mhz` | 4 native / 2 machine cycles | +| `ExgDataAddressSwapsRegistersAndPreservesFlags` | `OcsAccelerator14Mhz` | 4 native / 2 machine cycles | +| `ExgDataDataSwapsRegistersAndPreservesFlags` | `A1200Ec02014Mhz` | 4 native / 2 machine cycles | +| `ExgAddressAddressSwapsRegistersAndPreservesFlags` | `A1200Ec02014Mhz` | 4 native / 2 machine cycles | + +The EXT/SWAP facts also use specific boundary/upper-register values, and the EXG +facts use specific register values. No exact-case replacement or original-defect +mutation proof is claimed for these six facts. They are retained on the concrete +cycle-coverage difference, without weakening or moving their assertions. Those +cycle counts describe the existing execution policy; they do not establish +physical MC68020 bus/pipeline timing. This audit changes documentation only, +retires no further regression and leaves the wider consolidation scope open. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 410b0e75..0b900169 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2697,3 +2697,28 @@ The broad 480-case inventory is not reduced, no regression is retired, and no new package or consumer qualification is claimed. See the [MOVEM write discovery](COPPER68K_REFERENCE_QUALIFICATION.md#actual-040-movem-write-fault-discovery--2026-10-07). Milestone 6 remains **in progress**, `roadmapComplete=false`. + +The 2026-10-07 MOVEM physical-write correction now passes the selected 112,224 +entry cases and 143,512 executed handler/RTE/resumption programs, including all +66 full-format structures, signed/scaled indexes, four stack states, CCRs, +predecrement A7, trace and self-overwritten pointers. Two owned mutations detect +lost shared write metadata and inappropriate EA recalculation. Seven integrity +controls pass for each current gate. Fresh pinned SingleStepTests/Musashi audits +and clean CopperScreen private-package .69 build, host/disk/engine tests and two +native floppy replays pass. Fresh full CPU validation passes 5,255 tests with +zero failures and 29 explicit optional/discovery skips (5,284 total), including +all ten qualified WinUAE presets. All 707 required ordinary reports and actual +passing TRX summaries verify 86,098,722 deterministic scenarios and complete +combination weights. Opt-in write-entry/recovery gates pass separately; other +skipped coverage remains unpromoted. Broader masks/fault/restoration/reference/consolidation requirements and +the broad 480-case inventory remain required. See the +[write-frame correction](COPPER68K_REFERENCE_QUALIFICATION.md#physical-040-movem-write-frame-correction--2026-10-07). +Milestone 6 remains **in progress**, `roadmapComplete=false`. + +A follow-up transfer retention audit reviews six EXT.W/EXT.L/SWAP/EXG facts in +`M68020InterpreterTests`. All retain native/machine-cycle assertions for named +OCS/A1200 profiles that the semantic matrix does not replace. Their retention +reasons and exact asserted counts are recorded; no further regression is retired +and no replacement/mutation proof is claimed. See the +[retention audit](COPPER68K_REFERENCE_QUALIFICATION.md#transfer-timing-regression-retention-audit--2026-10-07). +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-040-movem-write-discovery.ps1 b/scripts/test-copper68k-040-movem-write-discovery.ps1 index cf6f6c5c..da67329a 100644 --- a/scripts/test-copper68k-040-movem-write-discovery.ps1 +++ b/scripts/test-copper68k-040-movem-write-discovery.ps1 @@ -11,6 +11,7 @@ function Hash([string] $Path) { (Get-FileHash -LiteralPath $Path -Algorithm SHA2 $manifestPath = Join-Path $output 'inputs.json' $sources = @(@(& git -C $repo ls-files 'Copper68k/*.cs' 'Copper68k/*.csproj' 'Copper68k.Tests/*.cs' 'Copper68k.Tests/*.csproj') + @( 'Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteFaultDiscoveryTests.cs', + 'Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteRecoveryTests.cs', 'scripts/test-copper68k-040-movem-write-discovery.ps1') | Sort-Object -Unique) $evidence = @('audit.trx','68040-movem-write-fault-discovery-scalar.json','68040-movem-write-fault-discovery-batch.json', '68040-movem-write-fixture-control-scalar.json','68040-movem-write-fixture-control-batch.json', diff --git a/scripts/test-copper68k-040-movem-write-recovery.ps1 b/scripts/test-copper68k-040-movem-write-recovery.ps1 new file mode 100644 index 00000000..c181bea0 --- /dev/null +++ b/scripts/test-copper68k-040-movem-write-recovery.ps1 @@ -0,0 +1,103 @@ +#requires -Version 7.0 +[CmdletBinding()] +param([string]$OutputDirectory='artifacts/040-movem-write-recovery', [switch]$ValidateReportsOnly) +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +function Hash([string]$Path) { (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() } +$manifestPath=Join-Path $output 'inputs.json' +$sources=@(@(& git -C $repo ls-files 'Copper68k/*.cs' 'Copper68k/*.csproj' 'Copper68k.Tests/*.cs' 'Copper68k.Tests/*.csproj') + @( + 'Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteRecoveryTests.cs','scripts/test-copper68k-040-movem-write-recovery.ps1') | Sort-Object -Unique) +$reports=@(foreach ($group in @('movem-write-recovery','movem-write-recovery-witness','movem-write-pointer-alias')) { foreach ($route in @('scalar','batch')) { "68040-$group-$route.json" } }) +$evidence=@('audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll')+$reports +if (-not $ValidateReportsOnly) { + if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } + New-Item -ItemType Directory -Path $output | Out-Null + $identities=@($sources | ForEach-Object { @{path=$_;sha256=(Hash (Join-Path $repo $_))} }) + $protected=@(@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | + Where-Object { Test-Path -LiteralPath (Join-Path $repo $_) } | ForEach-Object { @{path=$_;sha256=(Hash (Join-Path $repo $_))} }) + $settings=@{COPPER68K_RUN_040_MOVEM_WRITE_RECOVERY='1';COPPER68K_SYNTHETIC_REPORT_DIR=$output}; $saved=@{} + try { + foreach ($key in $settings.Keys) { $saved[$key]=[Environment]::GetEnvironmentVariable($key); [Environment]::SetEnvironmentVariable($key,$settings[$key]) } + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --artifacts-path (Join-Path $output 'build') --filter 'FullyQualifiedName~SyntheticM68040MovemWriteRecoveryTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output *> (Join-Path $output 'execution.log') + $testExit=$LASTEXITCODE + } finally { foreach ($key in $saved.Keys) { [Environment]::SetEnvironmentVariable($key,$saved[$key]) } } + @{schema=1;reference='MC68040UM 8.4.6.2/5/7';referenceUrl='https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf'; + sourceCommit=(& git -C $repo rev-parse HEAD);testExit=$testExit;sources=$identities;protected=$protected; + evidence=@($evidence | ForEach-Object { @{path=$_;sha256=(Hash (Join-Path $output $_))} })} | + ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $manifestPath +} +$manifest=Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json +if ($manifest.schema -ne 1 -or $manifest.sources.Count -ne $sources.Count -or + @(Compare-Object ($manifest.sources.path | Sort-Object) $sources).Count -ne 0 -or $manifest.evidence.Count -ne $evidence.Count -or + @(Compare-Object ($manifest.evidence.path | Sort-Object) ($evidence | Sort-Object)).Count -ne 0) { throw 'Missing recovery identities' } +foreach ($item in $manifest.sources) { if ((Hash (Join-Path $repo $item.path)) -ne $item.sha256) { throw "Changed recovery source: $($item.path)" } } +foreach ($item in $manifest.evidence) { if ((Hash (Join-Path $output $item.path)) -ne $item.sha256) { throw "Changed recovery evidence: $($item.path)" } } +foreach ($item in $manifest.protected) { if ((Hash (Join-Path $repo $item.path)) -ne $item.sha256) { throw "Normal DLL changed: $($item.path)" } } +[xml]$trx=Get-Content (Join-Path $output 'audit.trx') -Raw +$tests=@($trx.TestRun.Results.UnitTestResult) +$prefix='Copper68k.Tests.Synthetic.SyntheticM68040MovemWriteRecoveryTests.' +$names=@('ScalarRecoveryMatrix','BatchRecoveryMatrix','EveryCanonicalStoreFormRunsHandlerAndResumes(batch: False)', + 'EveryCanonicalStoreFormRunsHandlerAndResumes(batch: True)','SelfOverwrittenIndirectPointerIsNotResolvedAgain(batch: False)', + 'SelfOverwrittenIndirectPointerIsNotResolvedAgain(batch: True)') | ForEach-Object { $prefix+$_ } +if ($tests.Count -ne 6 -or $trx.TestRun.ResultSummary.Counters.executed -ne 6 -or + @($tests.testId | Sort-Object -Unique).Count -ne 6 -or + @(Compare-Object ($tests.testName | Sort-Object) ($names | Sort-Object)).Count -ne 0) { throw 'Incomplete recovery execution roster' } +if (@($tests | Where-Object { $_.outcome -notin @('Passed','Failed') }).Count -ne 0) { throw 'Unexecuted recovery test' } +function AddKeys([hashtable]$Map,[string]$Cohort,[string[]]$Forms,[string[]]$Banks,[string[]]$Traces,[int]$Weight,[bool]$Witness) { + foreach ($form in $Forms) { foreach ($bank in $Banks) { foreach ($trace in $Traces) { foreach ($width in @(2,4)) { + $transfers=if ($Witness) {@(3)} else {@(0,1,2,3)} + $bytes=if ($Witness) {@($width-1)} else {@(0..($width-1))} + foreach ($transfer in $transfers) { foreach ($byte in $bytes) { + $key="68040/MOVEM/write-fault/$Cohort/size=$width/$form/bank=$bank/T=$trace/transfer=$transfer/byte=$byte" + if ($Map.ContainsKey($key)) { throw "Duplicate independent key: $key" }; $Map[$key]=$Weight + } } + } } } } +} +# Independently enumerate the architectural store forms and full-format fields. +$forms=@(foreach ($mode in @(2,4,5,6,7)) { for ($register=0; $register -lt $(if ($mode -eq 7) {2} else {8}); $register++) { "mode=$mode/reg=$register" } }) +$canonical=@('mode=2/reg=0','mode=4/reg=0','mode=4/reg=7','mode=5/reg=0','mode=6/reg=0','mode=7/reg=0','mode=7/reg=1') +$fullForms=@(foreach ($bs in @('False','True')) { foreach ($is in @('False','True')) { foreach ($bd in 1..3) { foreach ($iis in @(0,1,2,3,5,6,7)) { + if ($is -eq 'True' -and $iis -ge 5) { continue } + foreach ($ix in @('D1/W/scale=1','D1/L/scale=1','D1/W/scale=8','A0/W/scale=4')) { + "mode=6/reg=1/index=full/bs=$bs/is=$is/bd=$bd/iis=$iis/ix=$ix/pointer-alias=False" + } +} } } }) +if ($forms.Count -ne 34 -or $fullForms.Count -ne 264) { throw 'Invalid independent form inventory' } +$required=@{}; $witness=@{}; $alias=@{} +AddKeys $required recovery-opcode $forms @('ISP') @('0000') 1 $false +AddKeys $required recovery-status $canonical @('user','user-M','ISP','MSP') @('0000','8000','4000') 32 $false +AddKeys $required recovery-structure $fullForms @('ISP') @('0000') 1 $false +AddKeys $witness recovery-witness $forms @('ISP') @('0000') 1 $true +AddKeys $alias pointer-alias @('mode=6/reg=0/index=full/bs=False/is=False/bd=2/iis=1/ix=D1/L/scale=1/pointer-alias=True') @('user','user-M','ISP','MSP') @('0000','8000','4000') 1 $true +$rows=@() +foreach ($group in @('movem-write-recovery','movem-write-recovery-witness','movem-write-pointer-alias')) { foreach ($route in @('scalar','batch')) { + $inventory=if ($group -eq 'movem-write-recovery') {$required} elseif ($group -eq 'movem-write-recovery-witness') {$witness} else {$alias} + $expected=if ($group -eq 'movem-write-recovery') {71664} elseif ($group -eq 'movem-write-recovery-witness') {68} else {24} + $report=Get-Content (Join-Path $output "68040-$group-$route.json") -Raw | ConvertFrom-Json + if ($report.schema -ne 1 -or $report.model -ne '68040' -or $report.group -ne "$group-$route" -or + $report.logicalCases -ne $expected -or $report.xunitBatches -ne 1 -or @($report.combinations.psobject.Properties).Count -ne $inventory.Count) { throw "Incomplete recovery report: $group/$route" } + $sum=@{passing=0;mismatching=0;unsupported=0;untested=0} + foreach ($combination in $report.combinations.psobject.Properties) { + if (-not $inventory.ContainsKey($combination.Name)) { throw "Foreign recovery combination: $($combination.Name)" } + $weight=0 + foreach ($status in $combination.Value.psobject.Properties) { + if (-not $sum.ContainsKey($status.Name) -or $status.Value -le 0) { throw 'Invalid recovery status/weight' } + $sum[$status.Name]+=$status.Value; $weight+=$status.Value + } + if ($weight -ne $inventory[$combination.Name]) { throw "Changed recovery combination weight: $($combination.Name)" } + } + foreach ($status in $sum.Keys) { if ($sum[$status] -ne $report.counts.$status) { throw "Recovery totals disagree: $group/$route/$status" } } + $testName=if ($group -eq 'movem-write-recovery') {if ($route -eq 'scalar') {'ScalarRecoveryMatrix'} else {'BatchRecoveryMatrix'}} else { + $(if ($group -eq 'movem-write-recovery-witness') {'EveryCanonicalStoreFormRunsHandlerAndResumes'} else {'SelfOverwrittenIndirectPointerIsNotResolvedAgain'})+ + '(batch: '+$(if ($route -eq 'scalar') {'False'} else {'True'})+')' + } + $test=@($tests | Where-Object testName -eq ($prefix+$testName))[0] + if (($sum.mismatching+$sum.unsupported+$sum.untested -eq 0) -ne ($test.outcome -eq 'Passed')) { throw 'Recovery TRX/report outcome disagreement' } + $rows += [pscustomobject]@{group=$group;route=$route;logicalCases=$report.logicalCases;counts=$sum;keys=$inventory.Count} +} } +@{schema=1;rows=$rows;wholePrograms=143512;roadmapComplete=$false;inputManifestSha256=(Hash $manifestPath)} | + ConvertTo-Json -Depth 10 | Set-Content (Join-Path $output 'verification.json') +$rows | Format-Table | Out-Host +if (@($tests | Where-Object outcome -ne 'Passed').Count -ne 0 -or $manifest.testExit -ne 0 -or + @($rows | Where-Object {$_.counts.mismatching+$_.counts.unsupported+$_.counts.untested -gt 0}).Count -ne 0) { throw "040 MOVEM write recovery fails; retain evidence: $output" } From 162d5ce3e551b90b7eddad25b52fdf32e67752e6 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 02:59:44 +0300 Subject: [PATCH 085/211] Expose 020 and 030 address-error frame restoration gap --- ...ntheticM68020AddressFrameDiscoveryTests.cs | 111 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 67 +++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 13 ++ ...-copper68k-020-address-frame-discovery.ps1 | 89 ++++++++++++++ 4 files changed, 280 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020AddressFrameDiscoveryTests.cs create mode 100644 scripts/test-copper68k-020-address-frame-discovery.ps1 diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020AddressFrameDiscoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020AddressFrameDiscoveryTests.cs new file mode 100644 index 00000000..970c52c1 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020AddressFrameDiscoveryTests.cs @@ -0,0 +1,111 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68020UM 6.1.3 and MC68030UM 8.1.3 permit A or B for an odd +// instruction prefetch. Neither permits the normal format-0 frame. +// This discovers real exception entry; it does not fabricate opaque RTE state. +public sealed class SyntheticM68020AddressFrameDiscoveryTests(ITestOutputHelper output) +{ + public static IEnumerable Models => ModelSpec.All + .Where(m => m.Id is "68EC020" or "A1200" or "68020" or "68030") + .Select(m => new object[] { m.Id }); + + [Theory, MemberData(nameof(Models)), Trait("Suite", "ReferenceDiscovery")] + public void FaultFreeFixturesPreserveBanksRegistersAndMemory(string modelId) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); + var report = new CoverageBatch(modelId, "address-frame-controls"); + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + for (var ccr = 0; ccr < 32; ccr++) + { + Initialize(m, bank, ccr, 0); + var expected = ArchitecturalExpectation.Capture(m); + expected.Pc += 2; + SyntheticExecution.Run(m, expected, report, + $"{modelId}/NOP/address-frame-control/bank={bank}/op=4E71/ccr={ccr:X2}", false); + } + report.Complete(output); + } + + [EnvironmentFact("COPPER68K_RUN_020_ADDRESS_FRAME_DISCOVERY", "require real 020/030 address-error A/B frames"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarOddInstructionFetchRequiresBusFaultFrame() => Audit(false); + + [EnvironmentFact("COPPER68K_RUN_020_ADDRESS_FRAME_DISCOVERY", "require real 020/030 address-error A/B frames"), Trait("Suite", "ReferenceDiscovery")] + public void BatchOddInstructionFetchRequiresBusFaultFrame() => Audit(true); + + private void Audit(bool batch) + { + // Complete all models before the aggregate assertion, retaining each + // report even when an earlier model exposes the implementation gap. + var failures = new List(); + foreach (var row in Models) + try { Audit((string)row[0], batch); } catch (Exception ex) { failures.Add(ex); } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(e => e.Message))); + } + + private void Audit(string modelId, bool batch) + { + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == modelId)); + var report = new CoverageBatch(modelId, "address-frame-discovery-" + (batch ? "batch" : "scalar")); + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var trace in new[] { 0, 0x8000, 0x4000 }) + foreach (var target in new[] { 0x6001u, 0x10006001u }) + for (var ccr = 0; ccr < 32; ccr++) + { + Initialize(m, bank, ccr, trace); + m.Core.State.ProgramCounter = target; + var before = ArchitecturalExpectation.Capture(m); + var id = $"{modelId}/address-error/odd-prefetch/bank={bank}/T={trace:X4}/target={target:X8}/op=0000/ccr={ccr:X2}"; + string? mismatch = null; + try + { + if (batch) + { + var boundary = new Boundary(); + var count = ((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, boundary); + if (count != 1 || boundary.Before != 1 || boundary.After != 1) + mismatch = $"boundary count expected 1/1/1 actual {count}/{boundary.Before}/{boundary.After}"; + } + else m.Core.ExecuteInstruction(); + var state = m.Core.State; + var frame = m.PeekPhysical(state.A[7] + 6, 2); + if (mismatch == null && frame >> 12 is not (10 or 11)) + mismatch = $"address-error format expected A or B actual {frame:X4}; PC={state.ProgramCounter:X8}; SP={state.A[7]:X8}"; + if (mismatch == null && (frame & 0xfff) != 12) mismatch = "address-error vector offset differs"; + if (mismatch == null && (state.LastExceptionVector != 3 || state.ProgramCounter != 0x9030 || + m.PeekPhysical(state.A[7], 2) != before.Sr || m.PeekPhysical(state.A[7] + 2, 4) != target)) + mismatch = "address-error common header/vector differs"; + if (mismatch == null && m.Bus.Accesses.Any(a => !a.Write && + a.Kind == M68kBusAccessKind.CpuInstructionFetch && a.Address == m.Model.Physical(target))) + mismatch = "odd instruction address was accessed on the bus"; + report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch); + } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException or UnsupportedM68040InstructionException) + { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); } + } + report.Complete(output); + } + + private static void Initialize(SyntheticMachine m, string bank, int ccr, int trace) + { + m.Reset(ccr); + _ = SyntheticExecution.Prepare(m, [0x4e71]); + m.Core.State.SetUserStackPointer(0x7800); + m.Core.State.SetInterruptStackPointer(0x4700); + m.Core.State.SetMasterStackPointer(0x7400); + m.Core.State.StatusRegister = (ushort)(trace | 0x700 | ccr | (bank is "ISP" or "MSP" ? 0x2000 : 0) | + (bank is "user-M" or "MSP" ? 0x1000 : 0)); + m.Core.State.SetActiveStackPointer(bank == "MSP" ? 0x7400u : bank == "ISP" ? 0x4700u : 0x7800u); + m.Bus.Accesses.Clear(); + } + + private sealed class Boundary : IM68kInstructionBoundary + { + public int Before, After; + public bool BeforeInstruction() { Before++; return true; } + public void AfterInstruction(long previousCycle, long currentCycle) => After++; + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index c0988ffb..779a8dbb 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -7696,3 +7696,70 @@ cycle-coverage difference, without weakening or moving their assertions. Those cycle counts describe the existing execution policy; they do not establish physical MC68020 bus/pipeline timing. This audit changes documentation only, retires no further regression and leaves the wider consolidation scope open. + +## 020/030 address-frame entry discovery — 2026-10-07 + +The next restoration audit starts with real exception entry rather than invented +internal-state images. MC68020UM 6.1.3 and MC68030UM 8.1.3 require a short or long +bus-fault frame for an odd instruction prefetch, with vector offset 12 and no bus +access to the odd instruction address. The short/long choice depends on saved +execution context; the discovery therefore accepts either format A or B, without +forcing one physical pipeline implementation. + +`SyntheticM68020AddressFrameDiscoveryTests` uses the public factory and sparse +recording bus for EC020, A1200, 020 and 030. Both execution routes cover four +stack states, all 32 CCRs, incoming T0/T1/no trace and low/high odd addresses. +The strict command executes **6,144 cases / eight discovery reports**, with +**zero passing / 6,144 mismatching / zero unsupported / zero untested** in this +selected entry matrix. Every case currently delivers format `000C`, a normal +eight-byte frame. The first EC020 user witness reaches handler `9030` with +SP `46F8`, instead of delivering a bus-fault frame. Accepted cases must also +pass the common header/vector and no-odd-fetch checks; failure at the format +check does not verify those subsequent requirements. + +All **512 fault-free controls / four reports** pass shared register, stack, +PC and surrounding-memory verification. The complete strict roster has four +passing control executions and two failing matrix executions, zero skips. +The ordinary invocation passes the four controls and explicitly skips the two +opt-in discovery matrices. They are not promoted architectural coverage. + +```powershell +./scripts/test-copper68k-020-address-frame-discovery.ps1 -OutputDirectory artifacts/020-address-frames +``` + +A fresh directory is required. The command currently fails acceptance. +`-ValidateReportsOnly` freezes source/binary/report identities and verifies the +exact roster, all twelve actual TRX summaries and independently enumerated +combination weights. Four copied-evidence controls reject a missing report, +changed weight, empty execution and changed source identity at their intended +checks. Evidence: `audits/AddressFrameDiscoveryFinalV2` under the restoration +temporary root; manifest SHA-256 +`c308c348ccd55a98196f8b9a0b1a8a69d355651c4de7bd91349c97e490d1b68e`. +Integrity evidence is in `AddressFrameDiscoveryIntegrityFinal`. Provisional +compiler/helper failures remain separate from this verified discovery. + +The pinned generator `025b999239800357e95065fe5b9a15ea5b300fa7` also exposes a +reference limitation: its non-MMU RTE branch only pops 20/32/92 bytes for +formats 9/A/B. Its 030 MMU branch calls separate restoration helpers. Agreement +with the simple pop branch cannot qualify opaque saved state or recovery. +Inspected `gencpu/gencpu.cpp` SHA-256: +`8215f4fee33b1410a7476dec2bce1fc40c4272fdcd2ef5dbca3262ba706e011d`. + +The required implementation/qualification sequence remains: + +1. Produce real A/B frames with defined SSW, saved PC/SR and resumable integer + context; retain partial operand effects without implicit retries. +2. Validate both ends before loading state, including long-frame version at + SP+36 hex. Distinguish validation exceptions from fatal state-load faults. +3. Execute repair handlers and RTE for software-completed and rerun data cycles, + instruction stages and read-modify-write operations, preserving stack, + trace, boundary and existing timing policy. Check exact following execution. +4. Qualify format-9 coprocessor context transport separately; frame deallocation + alone does not restore it. Enabled MMU and physical pipeline qualification + remain outside the roadmap. + +The existing legal 9/A/B exclusions are not relabeled invalid or removed. +This slice adds tests, a strict command and documentation only. It fixes no +production behavior, retires no regression and creates no package. The preceding +full CPU/consumer evidence applies to the MOVEM correction, not a fresh full run +of this new fixture. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 0b900169..25380003 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2722,3 +2722,16 @@ reasons and exact asserted counts are recorded; no further regression is retired and no replacement/mutation proof is claimed. See the [retention audit](COPPER68K_REFERENCE_QUALIFICATION.md#transfer-timing-regression-retention-audit--2026-10-07). Milestone 6 remains **in progress**, `roadmapComplete=false`. + +The subsequent 020/030 restoration audit establishes a real exception-entry +defect: all 6,144 selected odd-prefetch cases on EC020/A1200/020/030 deliver +normal format 0 rather than the documented A/B bus-fault frame. All 512 +fault-free controls pass; four evidence-integrity controls reject intended +corruption. The strict maintained command fails acceptance and preserves all +model/stack/CCR/trace/address combinations. Pinned reference inspection shows +that its non-MMU RTE path merely pops advanced frame sizes and cannot establish +internal recovery correctness. Real fault-context generation, validation, +executed repair/RTE continuation and separate format-9 transport remain required. +No CPU fix, package or regression retirement is claimed. See the +[address-frame discovery](COPPER68K_REFERENCE_QUALIFICATION.md#020030-address-frame-entry-discovery--2026-10-07). +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-020-address-frame-discovery.ps1 b/scripts/test-copper68k-020-address-frame-discovery.ps1 new file mode 100644 index 00000000..4bbec609 --- /dev/null +++ b/scripts/test-copper68k-020-address-frame-discovery.ps1 @@ -0,0 +1,89 @@ +#requires -Version 7.0 +[CmdletBinding()] +param([string]$OutputDirectory='artifacts/020-address-frame-discovery', [switch]$ValidateReportsOnly) +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +function Hash([string]$Path) { (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() } +$sourceNames=@(@(& git -C $repo ls-files 'Copper68k/*.cs' 'Copper68k/*.csproj' 'Copper68k.Tests/*.cs' 'Copper68k.Tests/*.csproj')+@( + 'Copper68k.Tests/Synthetic/SyntheticM68020AddressFrameDiscoveryTests.cs','scripts/test-copper68k-020-address-frame-discovery.ps1') | Sort-Object -Unique) +$models=@('68EC020','A1200','68020','68030') +$groups=@('address-frame-controls','address-frame-discovery-scalar','address-frame-discovery-batch') +$reportNames=@(foreach ($model in $models) { foreach ($group in $groups) { "$model-$group.json" } }) +$evidenceNames=@('audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll')+$reportNames +$manifestPath=Join-Path $output 'inputs.json' +if (-not $ValidateReportsOnly) { + if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } + New-Item -ItemType Directory -Path $output | Out-Null + $sources=@($sourceNames | ForEach-Object { @{path=$_;sha256=(Hash (Join-Path $repo $_))} }) + $protected=@(@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | + Where-Object { Test-Path (Join-Path $repo $_) } | ForEach-Object { @{path=$_;sha256=(Hash (Join-Path $repo $_))} }) + $settings=@{COPPER68K_RUN_020_ADDRESS_FRAME_DISCOVERY='1';COPPER68K_SYNTHETIC_REPORT_DIR=$output}; $saved=@{} + try { + foreach ($key in $settings.Keys) { $saved[$key]=[Environment]::GetEnvironmentVariable($key); [Environment]::SetEnvironmentVariable($key,$settings[$key]) } + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --artifacts-path (Join-Path $output 'build') --filter 'FullyQualifiedName~SyntheticM68020AddressFrameDiscoveryTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output *> (Join-Path $output 'execution.log') + $testExit=$LASTEXITCODE + } finally { foreach ($key in $saved.Keys) { [Environment]::SetEnvironmentVariable($key,$saved[$key]) } } + @{schema=1;reference='MC68020UM 6.1.3; MC68030UM 8.1.3';sourceCommit=(& git -C $repo rev-parse HEAD);testExit=$testExit; + sources=$sources;protected=$protected;evidence=@($evidenceNames | ForEach-Object { @{path=$_;sha256=(Hash (Join-Path $output $_))} })} | + ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $manifestPath +} +$manifest=Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json +if ($manifest.schema -ne 1 -or $manifest.sources.Count -ne $sourceNames.Count -or + (($manifest.sources.path | Sort-Object) -join '|') -cne (($sourceNames | Sort-Object) -join '|') -or + $manifest.evidence.Count -ne $evidenceNames.Count -or + (($manifest.evidence.path | Sort-Object) -join '|') -cne (($evidenceNames | Sort-Object) -join '|')) { throw 'Missing frozen address-frame identities' } +foreach ($item in $manifest.sources+$manifest.protected) { if ((Hash (Join-Path $repo $item.path)) -ne $item.sha256) { throw "Changed source/protected DLL: $($item.path)" } } +foreach ($item in $manifest.evidence) { if ((Hash (Join-Path $output $item.path)) -ne $item.sha256) { throw "Changed evidence: $($item.path)" } } +[xml]$trx=Get-Content (Join-Path $output 'audit.trx') -Raw +$tests=@($trx.TestRun.Results.UnitTestResult) +$prefix='Copper68k.Tests.Synthetic.SyntheticM68020AddressFrameDiscoveryTests.' +$names=@($models | ForEach-Object { $prefix+'FaultFreeFixturesPreserveBanksRegistersAndMemory(modelId: "'+$_+'")' })+@( + ($prefix+'ScalarOddInstructionFetchRequiresBusFaultFrame'),($prefix+'BatchOddInstructionFetchRequiresBusFaultFrame')) +if ($tests.Count -ne 6 -or @($tests.testId | Sort-Object -Unique).Count -ne 6 -or + (($tests.testName | Sort-Object) -join '|') -cne (($names | Sort-Object) -join '|') -or + @($tests | Where-Object { $_.outcome -notin @('Passed','Failed') }).Count -ne 0) { throw 'Incomplete address-frame execution roster' } +$allPassing=$true; $rows=@() +foreach ($model in $models) { foreach ($group in $groups) { + $report=Get-Content (Join-Path $output "$model-$group.json") -Raw | ConvertFrom-Json + $control=$group -eq 'address-frame-controls'; $expected=if ($control) {128} else {768}; $keys=@{} + foreach ($bank in @('user','user-M','ISP','MSP')) { + if ($control) { $keys["$model/NOP/address-frame-control/bank=$bank"]=32 } + else { foreach ($trace in @(0,0x8000,0x4000)) { foreach ($target in @(0x6001,0x10006001)) { + $keys[('{0}/address-error/odd-prefetch/bank={1}/T={2:X4}/target={3:X8}' -f $model,$bank,$trace,$target)]=32 + } } } + } + if ($report.schema -ne 1 -or $report.model -ne $model -or $report.group -ne $group -or + $report.logicalCases -ne $expected -or $report.xunitBatches -ne 1 -or + @($report.combinations.psobject.Properties).Count -ne $keys.Count) { throw "Incomplete address-frame report: $model/$group" } + $sum=@{passing=0;mismatching=0;unsupported=0;untested=0} + foreach ($combination in $report.combinations.psobject.Properties) { + if (-not $keys.ContainsKey($combination.Name)) { throw 'Foreign address-frame combination' } + $weight=0 + foreach ($status in $combination.Value.psobject.Properties) { + if (-not $sum.ContainsKey($status.Name) -or $status.Value -le 0) { throw 'Invalid address-frame weight/status' } + $weight+=$status.Value; $sum[$status.Name]+=$status.Value + } + if ($weight -ne $keys[$combination.Name]) { throw 'Changed address-frame weight' } + } + foreach ($status in $sum.Keys) { if ($sum[$status] -ne $report.counts.$status) { throw 'Address-frame totals differ' } } + $testName=if ($control) { $prefix+'FaultFreeFixturesPreserveBanksRegistersAndMemory(modelId: "'+$model+'")' } + elseif ($group.EndsWith('scalar')) { $prefix+'ScalarOddInstructionFetchRequiresBusFaultFrame' } + else { $prefix+'BatchOddInstructionFetchRequiresBusFaultFrame' } + $test=@($tests | Where-Object testName -eq $testName)[0] + $stdout=[string]$test.Output.StdOut + $pattern=[regex]::Escape("$model/${group}: ")+'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)' + $matches=[regex]::Matches($stdout,$pattern) + if ($matches.Count -ne 1 -or [int]$matches[0].Groups[2].Value -ne $expected -or + [int]$matches[0].Groups[3].Value -ne $keys.Count) { throw 'Missing actual TRX address-frame summary' } + $actualCounts=$matches[0].Groups[1].Value | ConvertFrom-Json + foreach ($status in $sum.Keys) { if ($actualCounts.$status -ne $sum[$status]) { throw 'TRX/report address-frame counts differ' } } + if ($control -and ($sum.passing -ne 128 -or $test.outcome -ne 'Passed')) { throw 'Invalid fault-free address-frame controls' } + if ($sum.passing -ne $expected) { $allPassing=$false } + $rows+=@{model=$model;group=$group;cases=$expected;counts=$sum} +} } +$rows | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $output 'verification.json') +if (-not $allPassing -or $manifest.testExit -ne 0 -or @($tests | Where-Object outcome -ne 'Passed').Count -ne 0) { + throw '020/030 address-frame gate has required mismatches or unsupported/untested cases' +} +Write-Output '6,144 address-frame cases and 512 controls pass.' From 4837ce900acaea77467e310c50e4e725abdc6a56 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 04:27:06 +0300 Subject: [PATCH 086/211] Fix 020 and 030 instruction-prefetch recovery --- .../SyntheticM68020PrefetchRecoveryTests.cs | 231 ++++++++++++++++++ ...M68kAdvancedTimingInterpreter.Access020.cs | 84 +++++++ Copper68k/M68kAdvancedTimingInterpreter.cs | 10 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 78 ++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 20 ++ .../test-copper68k-020-prefetch-recovery.ps1 | 85 +++++++ 6 files changed, 506 insertions(+), 2 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020PrefetchRecoveryTests.cs create mode 100644 Copper68k/M68kAdvancedTimingInterpreter.Access020.cs create mode 100644 scripts/test-copper68k-020-prefetch-recovery.ps1 diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020PrefetchRecoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020PrefetchRecoveryTests.cs new file mode 100644 index 00000000..33817792 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020PrefetchRecoveryTests.cs @@ -0,0 +1,231 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticM68020PrefetchRecoveryTests(ITestOutputHelper output) +{ + [Theory, Trait("Suite", "ReferenceDiscovery")] + [InlineData(false)] + [InlineData(true)] + public void RepairedPipelineRunsWithoutOddBusFetch(bool batch) => Audit(batch, false); + + [EnvironmentFact("COPPER68K_RUN_020_PREFETCH_RECOVERY", "qualify executed 020/030 prefetch repair and RTE"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarRecoveryMatrix() => Audit(false, true); + + [EnvironmentFact("COPPER68K_RUN_020_PREFETCH_RECOVERY", "qualify executed 020/030 prefetch repair and RTE"), Trait("Suite", "ReferenceDiscovery")] + public void BatchRecoveryMatrix() => Audit(true, true); + + [Theory, Trait("Suite", "ReferenceDiscovery")] + [InlineData(false)] + [InlineData(true)] + public void IncompatibleVersionPreservesOriginalFrame(bool batch) => Refusal(batch, true); + + [Theory, Trait("Suite", "ReferenceDiscovery")] + [InlineData(false)] + [InlineData(true)] + public void UnclearedRerunReentersAddressHandlerWithoutInstructionReplay(bool batch) => Refusal(batch, false); + + private void Refusal(bool batch, bool version) + { + var failures = new List(); + foreach (var model in ModelSpec.All.Where(x => x.Id is "68020" or "68030" or "68EC020" or "A1200")) + { + var m = new SyntheticMachine(model); + var report = new CoverageBatch(model.Id, "prefetch-" + (version ? "version-" : "rerun-") + (batch ? "batch" : "scalar")); + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var revision in version ? Enumerable.Range(1, 15) : new[] { 0 }) + { + var id = $"{model.Id}/prefetch-{(version ? "version" : "rerun")}/bank={bank}/revision={revision:X}/op=4E73/ccr=1F"; + var phase = "entry"; + try + { + m.Reset(31); m.InitializePhysical(0x9030, 0x4e73, 2); + _ = SyntheticExecution.Prepare(m, [0x4e71]); + m.Core.State.SetUserStackPointer(0x7800); m.Core.State.SetInterruptStackPointer(0x4700); m.Core.State.SetMasterStackPointer(0x7400); + m.Core.State.StatusRegister = (ushort)(0x71f | (bank is "ISP" or "MSP" ? 0x2000 : 0) | (bank is "user-M" or "MSP" ? 0x1000 : 0)); + m.Core.State.SetActiveStackPointer(bank == "MSP" ? 0x7400u : bank == "ISP" ? 0x4700u : 0x7800u); + m.Core.State.ProgramCounter = 0x6001; + Step(m, batch); + var frame = m.Core.State.A[7]; + if (m.PeekPhysical(frame+6,2) != 0xb00c || m.PeekPhysical(frame+10,2) != 0x3000 || m.Core.State.ProgramCounter != 0x9030) + throw new InvalidOperationException("required prefetch frame was not delivered"); + if (version) m.InitializePhysical(frame+0x36, (uint)revision << 12, 2); + var expected = ArchitecturalExpectation.Capture(m); + var serial = m.Core.State.ExceptionSequence; + if (version) + { + SyntheticExecution.ExpectException(m, expected, 14, 0x9030); + if ((expected.Sr & 0x1000) != 0) expected.MasterStackPointer = expected.A[7]; + } + m.Bus.Accesses.Clear(); + phase = "RTE"; + Step(m, batch); Check(m, expected, phase); + if (m.Core.State.ExceptionSequence != serial+1 || m.Core.State.LastExceptionVector != (version ? 14 : 3)) + throw new InvalidOperationException("refusal exception sequence/vector differs"); + if (version) + { + var reads = m.Bus.Accesses.Where(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address >= frame && a.Address < frame+92) + .Select(a => (a.Address,a.Width)).ToArray(); + (uint,int)[] required = [(frame,2),(frame+2,4),(frame+6,2),(frame+0x36,2)]; + if (!reads.SequenceEqual(required)) throw new InvalidOperationException("version validation transport order differs"); + } + if (m.Bus.Accesses.Any(a => !a.Write && a.Kind == M68kBusAccessKind.CpuInstructionFetch && (a.Address & 1) != 0)) + throw new InvalidOperationException("refusal issued an odd instruction bus read"); + report.Record(id, "passing", null); + } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException or UnsupportedM68040InstructionException) + { report.Record(id, "unsupported", phase+": "+ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", phase+": "+ex.Message); } + } + try { report.Complete(output); } catch (Exception ex) { failures.Add(ex); } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(x => x.Message))); + } + + private void Audit(bool batch, bool matrix) + { + var failures = new List(); + foreach (var model in ModelSpec.All.Where(x => x.Id is "68020" or "68030" or "68EC020" or "A1200")) + { + var m = new SyntheticMachine(model); + var report = new CoverageBatch(model.Id, "prefetch-recovery-" + (matrix ? "matrix-" : "witness-") + (batch ? "batch" : "scalar")); + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var trace in new ushort[] { 0, 0x8000, 0x4000 }) + foreach (var target in new[] { 0x6001u, 0x10006001u }) + foreach (var ccr in matrix ? Enumerable.Range(0, 32) : new[] { 31 }) + { + Case(m, report, batch, bank, trace, target, ccr, false); + if (trace == 0) Case(m, report, batch, bank, trace, target, ccr, true); + } + try { report.Complete(output); } catch (Exception ex) { failures.Add(ex); } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(x => x.Message))); + } + + private static void Case(SyntheticMachine m, CoverageBatch report, bool batch, string bank, ushort trace, uint target, int ccr, bool twoStage) + { + var phase = "entry"; + var id = $"{m.Model.Id}/prefetch-recovery/bank={bank}/T={trace:X4}/target={target:X8}/sequence={(twoStage ? "nop-branch" : "single-branch")}/op=4E73/ccr={ccr:X2}"; + try + { + m.Reset(ccr); + // MOVE.W #BRA,12(A7); MOVE.W #NOP,14(A7); ANDI.W #CFFF,10(A7); RTE. + ushort[] handler = [0x3f7c,0x6001,0x000c,0x3f7c,0x4e71,0x000e,0x026f,0xcfff,0x000a,0x4e73]; + if (twoStage) { handler[1] = 0x4e71; handler[4] = 0x6001; } + for (var n = 0; n < handler.Length; n++) m.InitializePhysical(0x9030u + (uint)n * 2, handler[n], 2); + // Clear trace in the saved SR, then return from the real trace frame. + m.InitializePhysical(0x9090, 0x0257, 2); m.InitializePhysical(0x9092, 0x3fff, 2); m.InitializePhysical(0x9094, 0x4e73, 2); + var branchTarget = unchecked(target + (twoStage ? 5u : 3u)); + m.InitializePhysical(branchTarget, 0x7c2a, 2); m.InitializePhysical(branchTarget + 2, 0x4e71, 2); + _ = SyntheticExecution.Prepare(m, [0x4e71]); + m.Core.State.SetUserStackPointer(0x7800); m.Core.State.SetInterruptStackPointer(0x4700); m.Core.State.SetMasterStackPointer(0x7400); + m.Core.State.StatusRegister = (ushort)(trace | 0x700 | ccr | (bank is "ISP" or "MSP" ? 0x2000 : 0) | + (bank is "user-M" or "MSP" ? 0x1000 : 0)); + m.Core.State.SetActiveStackPointer(bank == "MSP" ? 0x7400u : bank == "ISP" ? 0x4700u : 0x7800u); + m.Core.State.ProgramCounter = target; + m.Bus.Accesses.Clear(); + var expected = ArchitecturalExpectation.Capture(m); + var initialSr = expected.Sr; + var master = (initialSr & 0x1000) != 0; + var frame = (master ? 0x7400u : 0x4700u) - 92; + SyntheticSystemTests.ApplyStatus(m, expected, (ushort)((initialSr | 0x2000) & ~0xc000)); + expected.A[7] = frame; + if (master) expected.MasterStackPointer = frame; + expected.Pc = 0x9030; + // Opaque internal values are not silicon expectations. Defined + // fields are checked independently, then opaque bytes are frozen + // solely to detect unintended handler/RTE memory modifications. + for (uint offset = 0; offset < 92; offset++) expected.MemoryMasks[m.Model.Physical(frame + offset)] = 0; + foreach (var (offset, value, width) in new[] { + (0u,(uint)initialSr,2),(2u,target,4),(6u,0xb00cu,2),(10u,0x3000u,2),(0x24u,unchecked(target+2),4) }) + { + expected.Write(frame + offset, value, width, m.Model); + for (uint n = 0; n < width; n++) expected.MemoryMasks[m.Model.Physical(frame + offset + n)] = 255; + } + Step(m, batch); Check(m, expected, phase); + for (uint offset = 0; offset < 92; offset++) + { + var address = m.Model.Physical(frame + offset); + expected.Memory[address] = m.Bus.Peek(address); + expected.MemoryMasks.Remove(address); + } + phase = "repair C"; + expected.Write(frame + 12, twoStage ? 0x4e71u : 0x6001u, 2, m.Model); expected.Pc += 6; + expected.Sr = (ushort)(expected.Sr & 0xfff0); + Step(m, batch); Check(m, expected, phase); + phase = "repair B"; + expected.Write(frame + 14, twoStage ? 0x6001u : 0x4e71u, 2, m.Model); expected.Pc += 6; + Step(m, batch); Check(m, expected, phase); + phase = "clear rerun"; + expected.Write(frame + 10, 0, 2, m.Model); expected.Pc += 6; expected.Sr |= 4; + Step(m, batch); Check(m, expected, phase); + phase = "RTE"; + expected.A[7] = frame + 92; + if (master) expected.MasterStackPointer = frame + 92; + SyntheticSystemTests.ApplyStatus(m, expected, initialSr); expected.Pc = target; + if ((initialSr & 0x2000) == 0) expected.InactiveStackPointer = 0x4700; + Step(m, batch); Check(m, expected, phase); + if (twoStage) + { + phase = "supplied NOP"; + expected.Pc += 2; + Step(m, batch); Check(m, expected, phase); + } + phase = "supplied BRA"; + expected.Pc = branchTarget; + if (trace != 0) + { + SyntheticSystemTests.ApplyStatus(m, expected, (ushort)((initialSr | 0x2000) & ~0xc000)); + expected.A[7] -= 12; + if (master) expected.MasterStackPointer = expected.A[7]; + expected.Write(expected.A[7], initialSr, 2, m.Model); + expected.Write(expected.A[7]+2, branchTarget, 4, m.Model); + expected.Write(expected.A[7]+6, 0x2024, 2, m.Model); + expected.Write(expected.A[7]+8, target, 4, m.Model); + expected.Pc = 0x9090; + } + Step(m, batch); Check(m, expected, phase); + if (trace != 0) + { + phase = "trace repair"; + expected.Write(expected.A[7], (uint)(initialSr & ~0xc000), 2, m.Model); expected.Pc += 4; + expected.Sr = (ushort)((expected.Sr & 0xfff0) | ((initialSr & 0x3fff) == 0 ? 4 : 0)); + Step(m, batch); Check(m, expected, phase); + phase = "trace RTE"; + expected.A[7] += 12; if (master) expected.MasterStackPointer = expected.A[7]; + SyntheticSystemTests.ApplyStatus(m, expected, (ushort)(initialSr & ~0xc000)); expected.Pc = branchTarget; + if ((initialSr & 0x2000) == 0) expected.InactiveStackPointer = 0x4700; + Step(m, batch); Check(m, expected, phase); + } + phase = "sentinel"; + expected.D[6] = 42; expected.Pc += 2; expected.Sr = (ushort)(expected.Sr & 0xfff0); + Step(m, batch); Check(m, expected, phase); + if (m.Bus.Accesses.Any(a => !a.Write && a.Kind == M68kBusAccessKind.CpuInstructionFetch && (a.Address & 1) != 0)) + throw new InvalidOperationException("repaired instruction stream was fetched at an odd bus address"); + report.Record(id, "passing", null); + } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException or UnsupportedM68040InstructionException) + { report.Record(id, "unsupported", phase + ": " + ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", phase + ": " + ex.Message); } + } + + private static void Check(SyntheticMachine m, ArchitecturalExpectation expected, string phase) + { + var mismatch = expected.Verify(m); + if (mismatch != null) throw new InvalidOperationException(phase + ": " + mismatch); + } + private static void Step(SyntheticMachine m, bool batch) + { + if (!batch) { m.Core.ExecuteInstruction(); return; } + var boundary = new Boundary(); + var count = ((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, boundary); + if (count != 1 || boundary.Before != 1 || boundary.After != 1) throw new InvalidOperationException("instruction boundary count differs"); + } + private sealed class Boundary : IM68kInstructionBoundary + { + public int Before, After; + public bool BeforeInstruction() { Before++; return true; } + public void AfterInstruction(long previousCycle, long currentCycle) => After++; + } +} diff --git a/Copper68k/M68kAdvancedTimingInterpreter.Access020.cs b/Copper68k/M68kAdvancedTimingInterpreter.Access020.cs new file mode 100644 index 00000000..beda8290 --- /dev/null +++ b/Copper68k/M68kAdvancedTimingInterpreter.Access020.cs @@ -0,0 +1,84 @@ +/* Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT */ +namespace Copper68k; + +internal partial class M68kAdvancedTimingInterpreter +{ + // Internal register images are processor-specific. This identifies the + // synchronous interpreter's pending instruction-prefetch context, not + // an external silicon/MMU/pipeline image. No out-of-frame state is needed. + private const ushort InstructionPrefetchContext020 = 0xc020; + private bool Has020AccessFrames => _profile.Model is M68kAcceleratorModel.M68020 or M68kAcceleratorModel.M68030; + + private void RaiseM68020InstructionAddressError(uint address) + { + var sr = State.StatusRegister; + State.RecordException(3, address, sr); + State.StatusRegister = (ushort)((sr | M68kCpuState.Supervisor) & ~0xc000); + // MC68020UM 6.1.3/6.4, MC68030UM 8.1.3/8.4: format B retains + // instruction continuation. Stage C is the failed opcode prefetch; + // stage B's explicit address is C+2. No bus read is made at either + // odd address. FB/FC/DF stay clear; RC/RB request software repair. + for (var n = 0; n < 18; n++) PushWord(0); // Internal state. + PushWord(0); // Version 0 / internal state. + for (var n = 0; n < 3; n++) PushWord(0); + PushLong(0); // No data input buffer. + PushLong(0); // Internal state. + PushLong(unchecked(address + 2)); // Stage B address. + PushLong(0); PushLong(0); // Internal state. + PushLong(0); // No data output buffer. + PushLong(address); // Internal pending instruction address. + PushLong(0); // No data-cycle fault. + PushWord(0); PushWord(0); // Stage B/C images, to be repaired. + PushWord(0x3000); // RC/RB, address error has no bus fault bits. + PushWord(InstructionPrefetchContext020); + PushWord(0xb00c); PushLong(address); PushWord(sr); + _instructionPipe.Reset(); + State.ProgramCounter = ReadLong(State.VectorBaseRegister + 12); + CompleteTiming(M68kInstructionTimingKey.IllegalInstruction); + } + + private bool TryRestoreM68020AccessFrame(uint frame, ushort format, ushort sr, uint pc) + { + if (!Has020AccessFrames || format is not (10 or 11)) return false; + var size = format == 10 ? 32u : 92u; + // Check the far end and long-frame version before loading state. + if (format == 11 && (ReadRteFrameWord(frame + 0x36) >> 12) != 0) + { + RaiseFormat0Exception(14, State.LastInstructionProgramCounter, M68kInstructionTimingKey.FormatError); + return true; + } + _ = ReadRteFrameWord(unchecked(frame + size - 2)); + var context = ReadRteFrameWord(frame + 8); + var ssw = ReadRteFrameWord(frame + 10); + // Data faults and foreign opaque images remain implementation gaps. + // Never silently consume them as ordinary frame-size pops. + if (format != 11 || context != InstructionPrefetchContext020 || (ssw & 0xcf00) != 0) + throw new UnsupportedM68kTimingException(0x4e73, State.LastInstructionProgramCounter, _profile); + var instructionAddress = ReadRteFrameLong(frame + 0x14); + var stageBAddress = ReadRteFrameLong(frame + 0x24); + if (pc != instructionAddress || stageBAddress != unchecked(instructionAddress + 2)) + throw new UnsupportedM68kTimingException(0x4e73, State.LastInstructionProgramCounter, _profile); + var stageC = ReadRteFrameWord(frame + 12); + var stageB = ReadRteFrameWord(frame + 14); + // Read the complete opaque state before committing the stack/status. + for (var offset = 0x10u; offset < size; offset += 2) _ = ReadRteFrameWord(frame + offset); + State.SetActiveStackPointer(unchecked(frame + size)); + State.StatusRegister = sr; + State.ProgramCounter = pc; + _instructionPipe.Reset(pc); + if ((ssw & 0x3000) != 0) + { + // An odd prefetch cannot be rerun on the bus. Software must repair + // the pipeline words and clear RC/RB. This is explicit RTE work, + // never an automatic retry during exception delivery. + RaiseM68020InstructionAddressError(instructionAddress); + return true; + } + CompleteTiming(M68kInstructionTimingKey.Rte); + // RTE's existing timing barrier flushes the handler's pipe first. + // Restore the architectural images after that barrier, not before it. + _instructionPipe.Append(instructionAddress, stageC, default); + _instructionPipe.Append(stageBAddress, stageB, default); + return true; + } +} diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index 25ca2e25..e3baa485 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -4572,12 +4572,13 @@ private void ExecuteInstructionBody() return; } - if ((State.ProgramCounter & 1) != 0) + if ((State.ProgramCounter & 1) != 0 && !_instructionPipe.Contains(State.ProgramCounter)) { var instructionPc = State.ProgramCounter; BeginInstruction(0); if (_profile.Model == M68kAcceleratorModel.M68040) RaiseM68040AddressError(instructionPc, instructionPc, State.StatusRegister); + else if (Has020AccessFrames) RaiseM68020InstructionAddressError(instructionPc); else RaiseFormat0Exception(3, instructionPc, M68kInstructionTimingKey.IllegalInstruction); return; } @@ -8716,6 +8717,7 @@ private void ExecuteRte() var format = ReadRteFrameWord(framePointer + 6) >> 12; if (format == 7 && _profile.Model == M68kAcceleratorModel.M68040 && TryRestoreM68040AccessFrame(framePointer, restoredStatus, restoredPc)) return; + if (TryRestoreM68020AccessFrame(framePointer, (ushort)format, restoredStatus, restoredPc)) return; var size = format switch { 0 or 1 => 8u, 2 => 12u, 3 when _profile.Model == M68kAcceleratorModel.M68040 => 12u, _ => 0u }; if (size == 0) { RaiseFormat0Exception(14, instructionPc, M68kInstructionTimingKey.FormatError); return; } @@ -18776,7 +18778,10 @@ private void RefillInstructionPipe() { if ((State.ProgramCounter & 1) != 0) { - _instructionPipe.Reset(State.ProgramCounter); + // RTE can supply repaired instruction words without a bus + // prefetch. Keep the remaining restored head; never read ahead + // at an odd address after one of those words retires. + if (!_instructionPipe.Contains(State.ProgramCounter)) _instructionPipe.Reset(State.ProgramCounter); return; } @@ -19380,6 +19385,7 @@ private void SynchronizeNativeToMachine() private bool TryPeekOpcode(uint address, out ushort opcode) { opcode = 0; + if (_instructionPipe.TryPeek(address, out opcode)) return true; if ((address & 1) != 0) { return false; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 779a8dbb..2bf28684 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -7763,3 +7763,81 @@ This slice adds tests, a strict command and documentation only. It fixes no production behavior, retires no regression and creates no package. The preceding full CPU/consumer evidence applies to the MOVEM correction, not a fresh full run of this new fixture. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## 020/030 instruction-prefetch recovery correction — 2026-10-07 + +The subsequent correction delivers a 92-byte format-B frame for the synchronous +interpreter's initial odd instruction prefetch on EC020/A1200/020/030. The +defined header, SSW RC/RB bits and stage-B address follow MC68020UM 6.1.3/6.2/6.4 +and MC68030UM 8.1.3/8.2/8.4. FB/FC/DF remain clear because an odd prefetch does +not issue a bus cycle. The opaque internal image identifies this interpreter's +pending instruction context; it is not a silicon, MMU or physical-pipeline +serialization format. No address-keyed side table or automatic instruction +retry is introduced. + +RTE checks the long-frame version before loading state, preserves the original +frame on version rejection, and restores software-repaired C/B instruction +words after the existing RTE timing barrier. Both supplied words survive until +consumed, including a supplied NOP followed by a supplied branch. Uncleared +RC/RB bits cause explicit RTE to enter the address-error handler again without +fetching an odd address. Foreign opaque images, format-A data continuation and +format-9 context transport remain unsupported required work; they are not +relabeled architectural format errors or promoted by this correction. + +The original discovery command now passes **6,144 entry cases and 512 controls** +in six executions, zero skips. Evidence: `audits/AddressFrameEntryAcceptance`, +manifest SHA-256 +`5ba39907279db53982dbd61162df2849b457bce9f8621cea5c40ef0b278942d2`. +The retained failing discovery remains historical evidence of the original defect. + +`SyntheticM68020PrefetchRecoveryTests` executes real repair handlers and RTE, +with shared register/PC/status/stack/memory checks at instruction boundaries, +two pipeline-word sequences, low/high addresses, four stack states, all CCRs, +T0/T1 trace returns, fifteen incompatible versions and uncleared rerun bits. +The strict eight-execution gate passes **8,960 complete programs / 32 reports**, +with zero mismatches, unsupported or untested cases in this selected matrix. + +```powershell +./scripts/test-copper68k-020-prefetch-recovery.ps1 -OutputDirectory artifacts/020-prefetch-recovery +``` + +Fresh output is required. `-ValidateReportsOnly` checks frozen source/binary/ +report identities, exact execution roster, all actual TRX summaries and +independently enumerated keys and weights. Evidence: +`audits/PrefetchRecoveryAcceptanceFinal`, manifest SHA-256 +`491ad179157b10a3b6c9ccb1f7d6bd8aea5f1267d5804eaacdb974739e26925a`. +Four copied-evidence integrity controls reject missing reports, changed weights, +empty execution and changed source identity. Two isolated production mutations +detect loss of the restored pipe (256 mismatches) and loss of its second word +(64 mismatches, 192 unchanged passing witnesses). Evidence is retained under +`PrefetchRecoveryIntegrity` and `mutations/PrefetchRecovery`. The 486 selected +existing 020/030 interpreter and synthetic exception controls also pass. + +Fresh pinned SingleStepTests and Musashi audits pass: 312,500 cases / 125 files +with documented TAS/TRAPV exclusions, and 536 model/program combinations / 88 +explicit exclusions respectively. A clean CopperScreen archive at +`aa1dad5dcc0fb7c970e8e3443a160487add846af` builds with zero warnings/errors through +isolated immutable private package `1.5.2-synthetic-dev.70`. Host 171, disk 74 and +separate engine 1,080 tests pass, with six unavailable optional host replays. +Two explicit native Workbench 3.1 floppy boots pass; runtime app and test CPU +assemblies match the package DLL. No HD replay or public release is claimed. +Evidence: `audits/PrefetchIndependent` and `consumer-70-final`. + +Fresh full CPU validation passes **5,265 tests, zero failures and 33 explicit +optional/discovery skips (5,298 total)**, including all ten qualified WinUAE +presets. The ordinary gate and separate actual-TRX audit verify all **707 required +reports / 86,098,722 scenarios**, with complete combination weights. The opt-in +entry/recovery matrices pass separately above; remaining skipped coverage stays +unpromoted. Evidence: `audits/PrefetchFull`; source manifest SHA-256 +`3a03e18a3cd8b7210e078b26a56100cd13df6f5ac9b5ff18b407ebb3256db2ab`, TRX SHA-256 +`a131a12c38502a8347f9578df463d1c63ba45c7c03e71c41e2798d9f4b611e2f`. +Normal CPU/test assemblies and unrelated CopperScreen changes are preserved. +The immutable .70 package SHA-256 is +`15d6af210c9487b66b1bfd1c25ff92c694fda296b7eb0d703fae35e6a38aec84`; +validated app/test CPU DLL SHA-256 is +`cddef7efa5803a2f2b32345cb7330e4b4d10192da92a55600da5f4e86bed18f4`. + +Broader A/B data recovery, foreign +images, format-9 transport, nested fault/load-failure qualification and remaining +reference/consolidation work stay required. Milestone 6 remains **in progress**, +`roadmapComplete=false`. No regression is retired or public package published. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 25380003..8431b154 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2735,3 +2735,23 @@ executed repair/RTE continuation and separate format-9 transport remain required No CPU fix, package or regression retirement is claimed. See the [address-frame discovery](COPPER68K_REFERENCE_QUALIFICATION.md#020030-address-frame-entry-discovery--2026-10-07). Milestone 6 remains **in progress**, `roadmapComplete=false`. + +The subsequent scoped 020/030 correction passes all 6,144 selected frame-entry +cases and 512 controls, plus 8,960 executed instruction-prefetch recovery/refusal +programs. Real handlers repair C/B words and clear rerun bits before RTE; +two-word retention, CCRs, stack states, trace, incompatible versions and explicit +uncleared-bit reentry are checked without odd instruction bus accesses. The +interpreter-specific opaque image is not claimed as a physical pipeline oracle. +Two isolated mutations detect lost restored words and four integrity controls +reject corrupted evidence. The 486 retained affected controls and fresh pinned +SingleStepTests/Musashi audits pass. Clean CopperScreen private-package .70 +production build, host/disk/engine tests and two native floppy boots pass. +Fresh full CPU validation passes 5,265 tests, zero failures and 33 explicit +optional/discovery skips (5,298 total), including all ten qualified WinUAE +presets. All 707 ordinary reports and actual passing TRX summaries verify +86,098,722 deterministic scenarios and complete combination weights. Opt-in +entry/recovery matrices pass separately; remaining skipped coverage is unpromoted. +A/B data continuation, foreign images, format-9 +transport and wider restoration/consolidation remain required. See the +[prefetch correction](COPPER68K_REFERENCE_QUALIFICATION.md#020030-instruction-prefetch-recovery-correction--2026-10-07). +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-020-prefetch-recovery.ps1 b/scripts/test-copper68k-020-prefetch-recovery.ps1 new file mode 100644 index 00000000..8c8f2022 --- /dev/null +++ b/scripts/test-copper68k-020-prefetch-recovery.ps1 @@ -0,0 +1,85 @@ +#requires -Version 7.0 +[CmdletBinding()] +param([string]$OutputDirectory='artifacts/020-prefetch-recovery', [switch]$ValidateReportsOnly) +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +function Hash([string]$Path) { (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() } +$sources=@(@(& git -C $repo ls-files 'Copper68k/*.cs' 'Copper68k/*.csproj' 'Copper68k.Tests/*.cs' 'Copper68k.Tests/*.csproj')+@( + 'Copper68k/M68kAdvancedTimingInterpreter.Access020.cs','Copper68k.Tests/Synthetic/SyntheticM68020PrefetchRecoveryTests.cs', + 'scripts/test-copper68k-020-prefetch-recovery.ps1') | Sort-Object -Unique) +$models=@('68EC020','A1200','68020','68030') +$groups=@('prefetch-recovery-witness','prefetch-recovery-matrix','prefetch-version','prefetch-rerun') +$reports=@(foreach ($model in $models) { foreach ($group in $groups) { foreach ($route in @('scalar','batch')) { "$model-$group-$route.json" } } }) +$evidence=@('audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll')+$reports +$manifestPath=Join-Path $output 'inputs.json' +if (-not $ValidateReportsOnly) { + if (Test-Path $output) { throw "Use a fresh output directory: $output" } + New-Item -ItemType Directory -Path $output | Out-Null + $identities=@($sources | ForEach-Object { @{path=$_;sha256=(Hash (Join-Path $repo $_))} }) + $protected=@(@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | + Where-Object { Test-Path (Join-Path $repo $_) } | ForEach-Object { @{path=$_;sha256=(Hash (Join-Path $repo $_))} }) + $settings=@{COPPER68K_RUN_020_PREFETCH_RECOVERY='1';COPPER68K_SYNTHETIC_REPORT_DIR=$output}; $saved=@{} + try { + foreach ($key in $settings.Keys) { $saved[$key]=[Environment]::GetEnvironmentVariable($key); [Environment]::SetEnvironmentVariable($key,$settings[$key]) } + & dotnet test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --artifacts-path (Join-Path $output 'build') --filter 'FullyQualifiedName~SyntheticM68020PrefetchRecoveryTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output *> (Join-Path $output 'execution.log') + $testExit=$LASTEXITCODE + } finally { foreach ($key in $saved.Keys) { [Environment]::SetEnvironmentVariable($key,$saved[$key]) } } + @{schema=1;sourceCommit=(& git -C $repo rev-parse HEAD);testExit=$testExit;sources=$identities;protected=$protected; + evidence=@($evidence | ForEach-Object { @{path=$_;sha256=(Hash (Join-Path $output $_))} })} | ConvertTo-Json -Depth 10 | Set-Content $manifestPath +} +$manifest=Get-Content $manifestPath -Raw | ConvertFrom-Json +if ($manifest.schema -ne 1 -or $manifest.sources.Count -ne $sources.Count -or + (($manifest.sources.path | Sort-Object) -join '|') -cne (($sources | Sort-Object) -join '|') -or + $manifest.evidence.Count -ne $evidence.Count -or + (($manifest.evidence.path | Sort-Object) -join '|') -cne (($evidence | Sort-Object) -join '|')) { throw 'Missing prefetch recovery identities' } +foreach ($item in $manifest.sources+$manifest.protected) { if ((Hash (Join-Path $repo $item.path)) -ne $item.sha256) { throw "Changed source/protected DLL: $($item.path)" } } +foreach ($item in $manifest.evidence) { if ((Hash (Join-Path $output $item.path)) -ne $item.sha256) { throw "Changed recovery evidence: $($item.path)" } } +[xml]$trx=Get-Content (Join-Path $output 'audit.trx') -Raw +$tests=@($trx.TestRun.Results.UnitTestResult) +$prefix='Copper68k.Tests.Synthetic.SyntheticM68020PrefetchRecoveryTests.' +$names=@(($prefix+'ScalarRecoveryMatrix'), ($prefix+'BatchRecoveryMatrix')) +foreach ($method in @('RepairedPipelineRunsWithoutOddBusFetch','IncompatibleVersionPreservesOriginalFrame','UnclearedRerunReentersAddressHandlerWithoutInstructionReplay')) { + foreach ($route in @('False','True')) { $names+=($prefix+$method+'(batch: '+$route+')') } +} +if ($tests.Count -ne 8 -or @($tests.testId | Sort-Object -Unique).Count -ne 8 -or + (($tests.testName | Sort-Object) -join '|') -cne (($names | Sort-Object) -join '|') -or + @($tests | Where-Object outcome -ne 'Passed').Count -ne 0 -or $manifest.testExit -ne 0) { throw 'Incomplete/failed recovery execution roster' } +$rows=@(); $total=0 +foreach ($model in $models) { foreach ($group in $groups) { foreach ($route in @('scalar','batch')) { + $keys=@{}; $weight=if ($group -eq 'prefetch-recovery-matrix') {32} else {1} + foreach ($bank in @('user','user-M','ISP','MSP')) { + if ($group -eq 'prefetch-version') { foreach ($revision in 1..15) { $keys[('{0}/prefetch-version/bank={1}/revision={2:X}' -f $model,$bank,$revision)]=1 } } + elseif ($group -eq 'prefetch-rerun') { $keys["$model/prefetch-rerun/bank=$bank/revision=0"]=1 } + else { foreach ($trace in @(0,0x8000,0x4000)) { foreach ($target in @(0x6001,0x10006001)) { + foreach ($sequence in $(if ($trace -eq 0) {@('single-branch','nop-branch')} else {@('single-branch')})) { + $keys[('{0}/prefetch-recovery/bank={1}/T={2:X4}/target={3:X8}/sequence={4}' -f $model,$bank,$trace,$target,$sequence)]=$weight + } + } } } + } + $expected=$keys.Count*$weight + $report=Get-Content (Join-Path $output "$model-$group-$route.json") -Raw | ConvertFrom-Json + if ($report.schema -ne 1 -or $report.model -ne $model -or $report.group -ne "$group-$route" -or + $report.logicalCases -ne $expected -or $report.xunitBatches -ne 1 -or @($report.combinations.psobject.Properties).Count -ne $keys.Count -or + $report.counts.passing -ne $expected -or $report.counts.mismatching+$report.counts.unsupported+$report.counts.untested -ne 0) { throw 'Incomplete recovery report' } + foreach ($combination in $report.combinations.psobject.Properties) { + if (-not $keys.ContainsKey($combination.Name) -or @($combination.Value.psobject.Properties).Count -ne 1 -or + $combination.Value.passing -ne $keys[$combination.Name]) { throw 'Changed recovery combination/weight' } + } + $method=switch ($group) { + prefetch-recovery-matrix { if ($route -eq 'scalar') {'ScalarRecoveryMatrix'} else {'BatchRecoveryMatrix'} } + prefetch-recovery-witness { 'RepairedPipelineRunsWithoutOddBusFetch' } + prefetch-version { 'IncompatibleVersionPreservesOriginalFrame' } + prefetch-rerun { 'UnclearedRerunReentersAddressHandlerWithoutInstructionReplay' } + } + $name=$prefix+$method+$(if ($group -ne 'prefetch-recovery-matrix') {'(batch: '+$(if ($route -eq 'scalar') {'False'} else {'True'})+')'} else {''}) + $test=@($tests | Where-Object testName -eq $name)[0] + $matches=[regex]::Matches([string]$test.Output.StdOut,[regex]::Escape("$model/${group}-${route}: ")+'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)') + if ($matches.Count -ne 1 -or [int]$matches[0].Groups[2].Value -ne $expected -or [int]$matches[0].Groups[3].Value -ne $keys.Count) { throw 'Missing actual recovery TRX summary' } + $counts=$matches[0].Groups[1].Value | ConvertFrom-Json + foreach ($status in @('passing','mismatching','unsupported','untested')) { if ($counts.$status -ne $report.counts.$status) { throw 'Recovery TRX/report disagreement' } } + $total+=$expected; $rows+=@{model=$model;group="$group-$route";cases=$expected;keys=$keys.Count} +} } } +if ($total -ne 8960 -or $rows.Count -ne 32) { throw 'Recovery inventory changed' } +@{cases=$total;reports=$rows;roadmapComplete=$false} | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $output 'verification.json') +Write-Output '8,960 passing prefetch repair/refusal programs / 32 reports / eight executions.' From 8c471e241f8d42c2016d3b43bf1caf5cbc1ecea4 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 06:26:59 +0300 Subject: [PATCH 087/211] Fix 020/030 RTE validation faults and EC020 map wrapping --- ...SyntheticM68020RteAddressTransportTests.cs | 102 +++++ .../Synthetic/SyntheticM68020RteFaultTests.cs | 368 ++++++++++++++++++ Copper68k/M68EC020Interpreter.cs | 21 +- ...M68kAdvancedTimingInterpreter.Access020.cs | 8 +- .../M68kAdvancedTimingInterpreter.Rte.cs | 2 + .../M68kAdvancedTimingInterpreter.Rte020.cs | 159 ++++++++ Copper68k/M68kAdvancedTimingInterpreter.cs | 2 + docs/COPPER68K_REFERENCE_QUALIFICATION.md | 266 +++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 51 +++ ...st-copper68k-020-rte-address-transport.ps1 | 74 ++++ scripts/test-copper68k-020-rte-faults.ps1 | 113 ++++++ 11 files changed, 1162 insertions(+), 4 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020RteAddressTransportTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020RteFaultTests.cs create mode 100644 Copper68k/M68kAdvancedTimingInterpreter.Rte020.cs create mode 100644 scripts/test-copper68k-020-rte-address-transport.ps1 create mode 100644 scripts/test-copper68k-020-rte-faults.ps1 diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020RteAddressTransportTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020RteAddressTransportTests.cs new file mode 100644 index 00000000..23c436d3 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020RteAddressTransportTests.cs @@ -0,0 +1,102 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// The host map describes physical byte ranges. These are complete RTE +// programs using defined normal frames, not invented opaque source state. +// Logical rejection is qualified; physical port widths/BERR cycles are not. +public sealed class SyntheticM68020RteAddressTransportTests(ITestOutputHelper output) +{ + private static readonly ModelSpec[] Models=ModelSpec.All.Where(x=>x.Id is "68EC020" or "A1200" or "68020" or "68030").ToArray(); + private static readonly uint[] Frames=[0x4700,0x10004700,0xfffffb,0xffffff,0xfffffffb,0xffffffff,0x1000001]; + private static readonly (uint Offset,int Width)[] Reads=[(0,2),(2,4),(6,2)]; + + [Theory,Trait("Suite","ReferenceDiscovery")] + [InlineData(false)] + [InlineData(true)] + public void FaultFreeHighAndWrappedFrames(bool batch)=>Audit(batch,false); + + [EnvironmentFact("COPPER68K_RUN_020_RTE_ADDRESS_TRANSPORT","require high/wrapped RTE validation-fault transport"),Trait("Suite","ReferenceDiscovery")] + public void ScalarHighAndWrappedValidationFaults()=>Audit(false,true); + [EnvironmentFact("COPPER68K_RUN_020_RTE_ADDRESS_TRANSPORT","require high/wrapped RTE validation-fault transport"),Trait("Suite","ReferenceDiscovery")] + public void BatchHighAndWrappedValidationFaults()=>Audit(true,true); + + private void Audit(bool batch,bool fault) + { + var failures=new List(); + foreach(var model in Models) + { + var bus=new FaultBus();var m=new SyntheticMachine(model,bus); + var report=new CoverageBatch(model.Id,$"rte-address-{(fault?"fault":"control")}-{(batch?"batch":"scalar")}"); + foreach(var frame in Frames) + foreach(var bank in new[]{"ISP","MSP"}) + for(var ccr=0;ccr<32;ccr++) + foreach(var read in fault?Reads:new[]{(0u,0)}) + foreach(var lane in fault?Enumerable.Range(0,read.Item2):new[]{0}) + { + var id=$"{model.Id}/RTE/address/bank={bank}/frame={frame:X8}/read={read.Item1:X}:{read.Item2}/byte={lane}/ccr={ccr:X2}"; + try + { + bus.Disarm();m.Reset(ccr);_=SyntheticExecution.Prepare(m,[0x4e73]); + m.InitializePhysical(0x9020,0x4e73,2);m.InitializePhysical(0x9022,0x4e71,2);m.InitializePhysical(0x6000,0x7c2a,2);m.InitializePhysical(0x6002,0x4e71,2); + var sr=(ushort)(0x2700|ccr|(bank=="MSP"?0x1000:0)); + m.Core.State.SetUserStackPointer(0x30007800);m.Core.State.SetInterruptStackPointer(bank=="ISP"?frame:0x20007400);m.Core.State.SetMasterStackPointer(bank=="MSP"?frame:0x20007400); + m.Core.State.StatusRegister=sr;m.Core.State.SetActiveStackPointer(frame);m.Core.State.VectorBaseRegister=0x10009000; + m.InitializePhysical(0x10009008,0x9020,4); + m.InitializePhysical(frame,sr,2);m.InitializePhysical(unchecked(frame+2),0x6000,4);m.InitializePhysical(unchecked(frame+6),0x24,2); + var e=ArchitecturalExpectation.Capture(m); + e.ControlChecks["USP"]=(s=>s.UserStackPointer,0x30007800); + e.ControlChecks["ISP"]=(s=>s.InterruptStackPointer,bank=="ISP"?frame:0x20007400); + e.ControlChecks["VBR"]=(s=>s.VectorBaseRegister,0x10009000); + var serial=m.Core.State.ExceptionSequence;bus.Accesses.Clear(); + if(fault)bus.Arm(model.Physical(unchecked(frame+read.Item1+(uint)lane))); + Step(m,batch); + if(fault) + { + if(bus.Rejected.Count!=1)throw new InvalidOperationException("wrapped validation request did not reject its physical byte exactly once"); + var nested=unchecked(frame-92); + if(m.Core.State.Halted||m.Core.State.ExceptionSequence!=serial+1||m.Core.State.LastExceptionVector!=2||m.Core.State.A[7]!=nested||m.Core.State.ProgramCounter!=0x9020|| + m.PeekPhysical(nested,2)!=sr||m.PeekPhysical(unchecked(nested+2),4)!=0x1000||m.PeekPhysical(unchecked(nested+6),2)!=0xb008|| + m.PeekPhysical(unchecked(nested+10),2)!=(read.Item2==2?0x165u:0x145u)||m.PeekPhysical(unchecked(nested+16),4)!=unchecked(frame+read.Item1)) + throw new InvalidOperationException("high/wrapped validation frame/vector differs"); + for(uint n=0;n<92;n++)e.Memory[model.Physical(unchecked(nested+n))]=bus.Peek(model.Physical(unchecked(nested+n))); + e.A[7]=nested;if(bank=="MSP")e.MasterStackPointer=nested;else e.ControlChecks["ISP"]=(s=>s.InterruptStackPointer,nested); + e.Pc=0x9020;e.ExceptionVector=2;Check(m,e); + Step(m,batch); + } + else if(m.Core.State.ExceptionSequence!=serial)throw new InvalidOperationException("fault-free address fixture raised an exception"); + e.A[7]=unchecked(frame+8);if(bank=="MSP")e.MasterStackPointer=e.A[7];else e.ControlChecks["ISP"]=(s=>s.InterruptStackPointer,e.A[7]); + e.Pc=0x6000;Check(m,e); + e.D[6]=42;e.Pc+=2;e.Sr=(ushort)(e.Sr&0xfff0);Step(m,batch);Check(m,e); + report.Record(id,"passing",null); + } + catch(Exception ex)when(ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException){report.Record(id,"unsupported",ex.Message);} + catch(Exception ex){report.Record(id,"mismatching",ex.Message);} + } + try{report.Complete(output);}catch(Exception ex){failures.Add(ex);} + } + Assert.True(failures.Count==0,string.Join("\n",failures.Select(x=>x.Message))); + } + private static void Check(SyntheticMachine m,ArchitecturalExpectation e){var error=e.Verify(m);if(error!=null)throw new InvalidOperationException(error);} + private static void Step(SyntheticMachine m,bool batch) + { + if(!batch){m.Core.ExecuteInstruction();return;} + var boundary=new Boundary();if(((IM68kBatchCore)m.Core).ExecuteInstructions(1,null,boundary)!=1||boundary.Before!=1||boundary.After!=1) + throw new InvalidOperationException("batch boundary count differs"); + } + private sealed class Boundary:IM68kInstructionBoundary + {public int Before,After;public bool BeforeInstruction(){Before++;return true;}public void AfterInstruction(long a,long b)=>After++;} + private sealed class FaultBus:SparseRecordingBus,IM68kPhysicalAddressMap + { + private uint? denied; + internal List<(uint Address,int Width)> Rejected{get;}=[]; + internal void Disarm(){denied=null;Rejected.Clear();} + internal void Arm(uint address)=>denied=address; + public bool IsCpuPhysicalAddressMapped(uint address,int size,M68kBusAccessKind kind) + { + if(denied is not{} at||kind!=M68kBusAccessKind.CpuDataRead||unchecked(at-address)>=size)return true; + denied=null;Rejected.Add((address,size));return false; + } + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020RteFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020RteFaultTests.cs new file mode 100644 index 00000000..810383e4 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020RteFaultTests.cs @@ -0,0 +1,368 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticM68020RteFaultTests(ITestOutputHelper output) +{ + [Theory, Trait("Suite", "ReferenceDiscovery")] + [InlineData(false)] + [InlineData(true)] + public void ValidationFaultPreservesFrameAndExplicitRteResumes(bool batch) => Witness(batch, false); + + [Theory, Trait("Suite", "ReferenceDiscovery")] + [InlineData(false)] + [InlineData(true)] + public void StateLoadFaultHaltsWithoutWritingMemory(bool batch) => Witness(batch, true); + + [EnvironmentFact("COPPER68K_RUN_020_RTE_FAULTS", "qualify 020/030 validation and state-load faults"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarFrameFaultMatrix() => Matrix(false, false); + [EnvironmentFact("COPPER68K_RUN_020_RTE_FAULTS", "qualify 020/030 validation and state-load faults"), Trait("Suite", "ReferenceDiscovery")] + public void BatchFrameFaultMatrix() => Matrix(true, false); + [EnvironmentFact("COPPER68K_RUN_020_RTE_FAULTS", "qualify 020/030 RTE fault CCR boundaries"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarFrameFaultCcrBoundaries() => Matrix(false, true); + [EnvironmentFact("COPPER68K_RUN_020_RTE_FAULTS", "qualify 020/030 RTE fault CCR boundaries"), Trait("Suite", "ReferenceDiscovery")] + public void BatchFrameFaultCcrBoundaries() => Matrix(true, true); + + [Theory, Trait("Suite", "ReferenceDiscovery")] + [InlineData(false, "buffer")] + [InlineData(true, "buffer")] + [InlineData(false, "alias")] + [InlineData(true, "alias")] + [InlineData(false, "refault")] + [InlineData(true, "refault")] + [InlineData(false, "version")] + [InlineData(true, "version")] + public void HandlerControls(bool batch, string mode) + { + var failures=new List(); + foreach(var model in Models) + { + var bus=new FaultBus();var m=new SyntheticMachine(model,bus); + var report=new CoverageBatch(model.Id,$"rte-handler-{mode}-{(batch?"batch":"scalar")}"); + foreach(var bank in Banks) + foreach(var read in mode=="buffer" ? ValidationReads.Take(3) : new[]{(mode=="version"?0x36u:0x5au,2,1)}) + foreach(var lane in Enumerable.Range(0,read.Item2)) + { + var id=$"{model.Id}/RTE/handler={mode}/bank={bank}/read={read.Item1:X}:{read.Item2}:{read.Item3}/byte={lane}/ccr=1F"; + Record(report,id,()=>Run(m,bus,batch,bank,31,0,false,read,lane,mode)); + } + try{report.Complete(output);}catch(Exception ex){failures.Add(ex);} + } + Assert.True(failures.Count==0,string.Join("\n",failures.Select(x=>x.Message))); + } + + private static readonly ModelSpec[] Models=ModelSpec.All.Where(x=>x.Id is "68020" or "68030" or "68EC020" or "A1200").ToArray(); + private static readonly string[] Banks=["user","user-M","ISP","MSP"]; + private static readonly (uint,int,int)[] ValidationReads=[(0,2,1),(2,4,1),(6,2,1),(0x36,2,1),(0x5a,2,1)]; + private static IEnumerable<(uint,int,int)> LoadReads() + { + yield return (8,2,1);yield return (10,2,1);yield return (0x14,4,1);yield return (0x24,4,1); + yield return (12,2,1);yield return (14,2,1); + for(uint offset=0x10;offset<92;offset+=2)yield return(offset,2,offset is 0x36 or 0x5a?2:1); + } + private void Matrix(bool batch,bool ccrs) + { + var failures=new List(); + foreach(var model in Models) + foreach(var loading in new[]{false,true}) + { + var bus=new FaultBus();var m=new SyntheticMachine(model,bus); + var group=$"rte-{(loading?"load":"validation")}-{(ccrs?"ccr":"matrix")}-{(batch?"batch":"scalar")}"; + var report=new CoverageBatch(model.Id,group); + foreach(var bank in Banks) + foreach(var trace in new ushort[]{0,0x8000,0x4000}) + foreach(var ccr in ccrs?Enumerable.Range(0,32):new[]{31}) + foreach(var read in ccrs ? new[]{(loading?8u:0u,2,1)} : loading?LoadReads():ValidationReads) + foreach(var lane in Enumerable.Range(0,read.Item2)) + { + var id=$"{model.Id}/RTE/{(loading?"loading":"validation")}/bank={bank}/T={trace:X4}/read={read.Item1:X}:{read.Item2}:{read.Item3}/byte={lane}/ccr={ccr:X2}"; + Record(report,id,()=>Run(m,bus,batch,bank,ccr,trace,loading,read,lane,"rerun")); + } + try{report.Complete(output);}catch(Exception ex){failures.Add(ex);} + } + Assert.True(failures.Count==0,string.Join("\n",failures.Select(x=>x.Message))); + } + private static void Record(CoverageBatch report,string id,Action run) + { + try{run();report.Record(id,"passing",null);} + catch(Exception ex)when(ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException){report.Record(id,"unsupported",ex.Message);} + catch(Exception ex){report.Record(id,"mismatching",ex.Message);} + } + + [Theory,Trait("Suite","ReferenceDiscovery")] + [InlineData(false)] + [InlineData(true)] + public void FaultDuringValidationExceptionEntryHalts(bool batch) + { + var failures=new List(); + foreach(var model in Models) + { + var bus=new FaultBus();var m=new SyntheticMachine(model,bus); + var report=new CoverageBatch(model.Id,$"rte-entry-halt-{(batch?"batch":"scalar")}"); + foreach(var bank in Banks) + foreach(var vector in new[]{false,true}) + foreach(var offset in vector?new[]{0u}:Enumerable.Range(0,46).Select(n=>(uint)n*2)) + foreach(var lane in Enumerable.Range(0,vector?4:2)) + { + var id=$"{model.Id}/RTE/entry-halt/bank={bank}/kind={(vector?"vector":"write")}/offset={offset:X}/byte={lane}/ccr=1F"; + Record(report,id,()=> + { + var frame=Prepare(m,bank,31,batch);var nested=frame-92; + var e=ArchitecturalExpectation.Capture(m);var serial=m.Core.State.ExceptionSequence; + bus.Arm(model.Physical(frame+90),2,1); + bus.AlsoArm(model.Physical(vector?8u+(uint)lane:nested+offset+(uint)lane),vector?4:2,1, + vector?M68kBusAccessKind.CpuDataRead:M68kBusAccessKind.CpuDataWrite); + Step(m,batch); + e.Halted=true;e.Pc+=2;e.ExceptionVector=2;e.A[7]=vector?nested:nested+offset; + if((e.Sr&0x1000)!=0)e.MasterStackPointer=e.A[7]; + // Successful writes preceding the rejected cycle may have + // committed opaque state. Check their footprint, retaining + // all original-frame and surrounding-memory expectations. + foreach(var access in bus.Accesses.Where(a=>a.Write)) + { + if(access.Address=frame || !vector && access.Address<=nested+offset) + throw new InvalidOperationException("fatal entry wrote beyond its completed prefix"); + for(uint n=0;nx.Message))); + } + + [Theory,Trait("Suite","ReferenceDiscovery")] + [InlineData(false,false)] + [InlineData(true,false)] + [InlineData(false,true)] + [InlineData(true,true)] + public void HeaderFaultsInNormalFramesAndThrowawayChains(bool batch,bool chained) + { + var failures=new List(); + foreach(var model in Models) + { + var bus=new FaultBus();var m=new SyntheticMachine(model,bus); + var report=new CoverageBatch(model.Id,$"rte-header-{(chained?"chained":"normal")}-{(batch?"batch":"scalar")}"); + foreach(var start in new[]{"ISP","MSP"}) + foreach(var tail in chained?Banks:new[]{start}) + foreach(var restored in Banks) + foreach(var format in new[]{0,2}) + foreach(var ccr in new[]{0,31}) + foreach(var read in ValidationReads.Take(3)) + foreach(var lane in Enumerable.Range(0,read.Item2)) + { + var id=$"{model.Id}/RTE/header/start={start}/tail={tail}/restore={restored}/format={format}/read={read.Item1:X}:{read.Item2}/byte={lane}/ccr={ccr:X2}"; + Record(report,id,()=> + { + bus.Disarm();m.Reset(ccr);m.InitializePhysical(0x9020,0x4e73,2);m.InitializePhysical(0x6000,0x7c2a,2); + _=SyntheticExecution.Prepare(m,[0x4e73]); + var pointers=new Dictionary{{"ISP",0x4700},{"MSP",0x7400},{"user",0x7800}}; + m.Core.State.SetUserStackPointer(pointers["user"]);m.Core.State.SetInterruptStackPointer(pointers["ISP"]);m.Core.State.SetMasterStackPointer(pointers["MSP"]); + var initialSr=(ushort)(0x700|M68040StackFixture.Status(start,0,ccr)); + m.Core.State.StatusRegister=initialSr;m.Core.State.SetActiveStackPointer(pointers[start]); + var liveSr=initialSr; + if(chained) + { + liveSr=(ushort)(0x700|M68040StackFixture.Status(tail,0,ccr^31)); + m.InitializePhysical(pointers[start],liveSr,2);m.InitializePhysical(pointers[start]+2,0xdead0001,4);m.InitializePhysical(pointers[start]+6,0x1024,2); + pointers[start]+=8; + } + var sourceBank=M68040StackFixture.PhysicalBank(tail);var frame=pointers[sourceBank]; + var savedSr=(ushort)(0x700|M68040StackFixture.Status(restored,0,ccr)); + m.InitializePhysical(frame,savedSr,2);m.InitializePhysical(frame+2,0x6000,4);m.InitializePhysical(frame+6,(uint)(format<<12)|0x24,2); + if(format==2)m.InitializePhysical(frame+8,0x1000,4); + var e=ArchitecturalExpectation.Capture(m);m.Bus.Accesses.Clear(); + bus.Arm(model.Physical(frame+read.Item1+(uint)lane),read.Item2,1); + Step(m,batch); + var exceptionBank=M68040StackFixture.ExceptionBank(liveSr);pointers[exceptionBank]-=92;var nested=pointers[exceptionBank]; + if(bus.Rejected.Count!=1 || m.PeekPhysical(nested,2)!=liveSr || m.PeekPhysical(nested+2,4)!=0x1000 || + m.PeekPhysical(nested+6,2)!=0xb008 || m.PeekPhysical(nested+10,2)!=((read.Item2==2?0x160u:0x140u)|((liveSr&0x2000)!=0?5u:1u)) || + m.PeekPhysical(nested+16,4)!=frame+read.Item1) + throw new InvalidOperationException("normal/chained validation frame differs"); + for(uint n=0;n<92;n++)e.Memory[model.Physical(nested+n)]=bus.Peek(model.Physical(nested+n)); + M68040StackFixture.SetStacks(e,pointers,(ushort)((liveSr|0x2000)&~0xc000));e.Pc=0x9020;e.ExceptionVector=2;Check(m,e); + pointers[exceptionBank]+=92;pointers[sourceBank]+=(uint)(format==0?8:12); + M68040StackFixture.SetStacks(e,pointers,savedSr);e.Pc=0x6000; + Step(m,batch);Check(m,e); + e.D[6]=42;e.Pc+=2;e.Sr=(ushort)(e.Sr&0xfff0);Step(m,batch);Check(m,e); + }); + } + try{report.Complete(output);}catch(Exception ex){failures.Add(ex);} + } + Assert.True(failures.Count==0,string.Join("\n",failures.Select(x=>x.Message))); + } + + private void Witness(bool batch, bool loading) + { + var failures = new List(); + foreach (var model in Models) + { + var bus = new FaultBus(); var m = new SyntheticMachine(model, bus); + var report = new CoverageBatch(model.Id, "rte-" + (loading ? "load-fault" : "validation-fault") + (batch ? "-batch" : "-scalar")); + foreach (var bank in Banks) + { + var id = $"{model.Id}/RTE/{(loading ? "loading" : "validation")}/bank={bank}/read={(loading ? 8 : 0x5a):X}/ccr=1F"; + Record(report, id, () => Run(m, bus, batch, bank, 31, 0, loading, (loading ? 8u : 0x5au, 2, 1), 0, "rerun")); + } + try { report.Complete(output); } catch (Exception ex) { failures.Add(ex); } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(x => x.Message))); + } + + private static void Run(SyntheticMachine m,FaultBus bus,bool batch,string bank,int ccr,ushort trace, + bool loading,(uint Offset,int Width,int Nth) read,int lane,string mode) + { + var frame=Prepare(m,bank,ccr,batch,trace); + var savedSr=(ushort)m.PeekPhysical(frame,2); + var supplied=mode=="version"?0x1000u:m.PeekPhysical(frame+read.Offset,read.Width); + var faultSr=m.Core.State.StatusRegister; + ushort[] code=mode switch + { + "buffer" or "version"=>[0x2f7c,(ushort)(supplied>>16),(ushort)supplied,0x002c,0x026f,0xfeff,0x000a,0x4e73], + "alias"=>[0x33fc,0x2700,(ushort)(frame>>16),(ushort)frame,0x4e73], + _=>[0x4e73] + }; + for(var n=0;nm.PeekPhysical(frame+(uint)n,1)).ToArray(); + bus.Arm(m.Model.Physical(frame+read.Offset+(uint)lane),read.Width,read.Nth,mode is "buffer" or "refault" or "version"); + var serial=m.Core.State.ExceptionSequence; + Step(m,batch); + if(bus.Rejected.Count!=1)throw new InvalidOperationException("RTE did not reject the selected read exactly once"); + if(loading) + { + e.Pc+=2;e.Halted=true;Check(m,e); + if(bus.Accesses.Any(x=>x.Write))throw new InvalidOperationException("state-load fault wrote memory"); + if(m.Core.State.ExceptionSequence!=serial)throw new InvalidOperationException("state-load fault delivered another exception"); + var count=bus.Accesses.Count;m.Core.ExecuteInstruction();m.Core.ExecuteInstruction(); + if(bus.Accesses.Count!=count)throw new InvalidOperationException("halted CPU accessed the bus"); + return; + } + if(m.Core.State.Halted || m.Core.State.LastExceptionVector!=2 || m.Core.State.ExceptionSequence!=serial+1) + throw new InvalidOperationException("validation read did not deliver exactly one bus fault"); + var nested=m.Core.State.A[7]; + if(nested!=frame-92 || m.Core.State.ProgramCounter!=0x9020 || m.PeekPhysical(nested,2)!=e.Sr || + m.PeekPhysical(nested+2,4)!=0x9042 || m.PeekPhysical(nested+6,2)!=0xb008 || + m.PeekPhysical(nested+10,2)!=(read.Width==2?0x165u:0x145u) || m.PeekPhysical(nested+16,4)!=frame+read.Offset) + throw new InvalidOperationException("defined validation-fault frame fields differ"); + if(!original.SequenceEqual(Enumerable.Range(0,92).Select(n=>m.PeekPhysical(frame+(uint)n,1)))) + throw new InvalidOperationException("validation fault damaged the original frame"); + // Opaque bytes are captured for preservation only, never treated as + // independently known silicon state. + for(uint n=0;n<92;n++)e.Memory[m.Model.Physical(nested+n)]=bus.Peek(m.Model.Physical(nested+n)); + e.A[7]=nested;if((e.Sr&0x1000)!=0)e.MasterStackPointer=nested; + e.Pc=0x9020;e.ExceptionVector=2;Check(m,e); + var postFaultReads=bus.Accesses.Count; + if(mode is "buffer" or "version") + { + e.Write(nested+44,supplied,4,m.Model);e.Pc+=8;e.Sr=(ushort)((e.Sr&0xfff0)|(supplied==0?4:0)|(supplied>=0x80000000?8:0)); + Step(m,batch);Check(m,e); + e.Write(nested+10,read.Width==2?0x65u:0x45u,2,m.Model);e.Pc+=6;e.Sr=(ushort)(e.Sr&0xfff0); + Step(m,batch);Check(m,e); + } + else if(mode=="alias") + { + e.Write(frame,0x2700,2,m.Model);e.Pc+=8;e.Sr=(ushort)(e.Sr&0xfff0); + Step(m,batch);Check(m,e); + } + else if(mode=="refault") + { + Step(m,batch);Check(m,e); + if(bus.Rejected.Count!=2 || m.Core.State.ExceptionSequence!=serial+2) + throw new InvalidOperationException("explicit retry did not refault exactly once"); + bus.Disarm(); + } + if(mode=="version") + { + e.Sr=faultSr;e.A[7]=frame-8;if((e.Sr&0x1000)!=0)e.MasterStackPointer=e.A[7]; + e.Write(e.A[7],faultSr,2,m.Model);e.Write(e.A[7]+2,0x9042,4,m.Model);e.Write(e.A[7]+6,0x38,2,m.Model); + e.Pc=0x90e0;e.ExceptionVector=14;Step(m,batch);Check(m,e); + if(bus.Rejected.Count!=1 || bus.Accesses.Skip(postFaultReads).Any(x=>!x.Write&&x.Kind==M68kBusAccessKind.CpuDataRead&&x.Address>=frame&&x.Address!x.Write&&x.Kind==M68kBusAccessKind.CpuDataRead&&x.Address==m.Model.Physical(frame+read.Offset)&&x.Width==read.Width))) + throw new InvalidOperationException("software-supplied validation value was reread"); + if(mode=="alias" && bus.Accesses.Skip(postFaultReads).Any(x=>!x.Write&&x.Kind==M68kBusAccessKind.CpuDataRead&&x.Address==m.Model.Physical(frame))) + throw new InvalidOperationException("completed SR read was replayed"); + e.Pc=0x6004; + if(trace!=0) + { + SyntheticSystemTests.ApplyStatus(m,e,(ushort)((savedSr|0x2000)&~0xc000));e.A[7]-=12; + if((e.Sr&0x1000)!=0)e.MasterStackPointer=e.A[7]; + e.Write(e.A[7],savedSr,2,m.Model);e.Write(e.A[7]+2,0x6004,4,m.Model);e.Write(e.A[7]+6,0x2024,2,m.Model);e.Write(e.A[7]+8,0x6001,4,m.Model); + e.Pc=0x9090;e.ExceptionVector=9; + } + Step(m,batch);Check(m,e); + if(trace!=0) + { + e.Write(e.A[7],(uint)(savedSr&~0xc000),2,m.Model);e.Pc+=4;e.Sr=(ushort)(e.Sr&0xfff0); + Step(m,batch);Check(m,e); + e.A[7]+=12;if((e.Sr&0x1000)!=0)e.MasterStackPointer=e.A[7]; + SyntheticSystemTests.ApplyStatus(m,e,(ushort)(savedSr&~0xc000));if(bank is "user" or "user-M")e.InactiveStackPointer=0x4700; + e.Pc=0x6004;Step(m,batch);Check(m,e); + } + e.D[6]=42;e.Pc+=2;e.Sr=(ushort)(e.Sr&0xfff0);Step(m,batch);Check(m,e); + if(bus.Accesses.Any(x=>!x.Write&&x.Kind==M68kBusAccessKind.CpuInstructionFetch&&(x.Address&1)!=0)) + throw new InvalidOperationException("recovery issued an odd instruction fetch"); + } + + private static uint Prepare(SyntheticMachine m, string bank, int ccr, bool batch,ushort trace=0) + { + ((FaultBus)m.Bus).Disarm(); m.Reset(ccr); + ushort[] handler=[0x3f7c,0x6001,0x000c,0x3f7c,0x4e71,0x000e,0x026f,0xcfff,0x000a,0x4e73]; + for(var n=0;nAfter++; } + private sealed class FaultBus:SparseRecordingBus,IM68kPhysicalAddressMap + { + private sealed class Request(uint at,int size,int nth,M68kBusAccessKind kind,bool repeat) + {public uint Address=at;public int Width=size,Occurrence=nth;public M68kBusAccessKind Kind=kind;public bool Persistent=repeat;} + private readonly List requests=[]; + public List<(uint Address,int Width)> Rejected {get;}=[]; + public void Disarm(){requests.Clear();Rejected.Clear();} + public void Arm(uint at,int size,int nth,bool repeat=false){Disarm();requests.Add(new(at,size,nth,M68kBusAccessKind.CpuDataRead,repeat));} + public void AlsoArm(uint at,int size,int nth,M68kBusAccessKind kind)=>requests.Add(new(at,size,nth,kind,false)); + public bool IsCpuPhysicalAddressMapped(uint at,int size,M68kBusAccessKind kind) + { + for(var n=0;n=size||request.Width!=size)continue; + if(--request.Occurrence!=0)return true; + if(request.Persistent)request.Occurrence=1;else requests.RemoveAt(n); + Rejected.Add((at,size));return false; + } + return true; + } + } +} diff --git a/Copper68k/M68EC020Interpreter.cs b/Copper68k/M68EC020Interpreter.cs index 6d575e81..0bbfc877 100644 --- a/Copper68k/M68EC020Interpreter.cs +++ b/Copper68k/M68EC020Interpreter.cs @@ -18,6 +18,7 @@ internal class M68EC020AddressMaskedBus : IM68kModuleAccessBus, private readonly IM68kCodeReader? _codeReader; private readonly IM68kFastMemoryBus? _fastMemoryBus; private readonly IM68kPhysicalAddressMap? _physicalAddressMap; + internal bool HasPhysicalAddressMap => _physicalAddressMap is not null; internal M68EC020AddressMaskedBus(IM68kBus bus) { @@ -118,7 +119,25 @@ public bool TryWriteFastLong(uint address, uint value, M68kBusAccessKind accessK => Mask(address) <= AddressMask - 3 && (_fastMemoryBus?.TryWriteFastLong(Mask(address), value, accessKind) ?? false); public bool IsCpuPhysicalAddressMapped(uint address, int byteCount, M68kBusAccessKind accessKind) - => _physicalAddressMap?.IsCpuPhysicalAddressMapped(Mask(address), byteCount, accessKind) ?? false; + { + if (_physicalAddressMap is not { } map) return false; + var physical = Mask(address); + var firstSpan = (int)(AddressMask - physical + 1); + if (byteCount <= firstSpan) + return map.IsCpuPhysicalAddressMapped(physical, byteCount, accessKind); + // The host map uses full physical addresses. Match the existing + // timed-bus wrapping without asking about fictitious bytes above + // the EC020's address space. These are predicates, not bus cycles. + var remaining = byteCount; + while (remaining > 0) + { + var count = Math.Min(remaining, (int)(AddressMask - physical + 1)); + if (!map.IsCpuPhysicalAddressMapped(physical, count, accessKind)) return false; + remaining -= count; + physical = 0; + } + return true; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint Mask(uint address) => address & AddressMask; diff --git a/Copper68k/M68kAdvancedTimingInterpreter.Access020.cs b/Copper68k/M68kAdvancedTimingInterpreter.Access020.cs index beda8290..ece04dc7 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.Access020.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.Access020.cs @@ -37,19 +37,21 @@ private void RaiseM68020InstructionAddressError(uint address) CompleteTiming(M68kInstructionTimingKey.IllegalInstruction); } - private bool TryRestoreM68020AccessFrame(uint frame, ushort format, ushort sr, uint pc) + private bool TryRestoreM68020AccessFrame(uint frame, ushort format, ushort sr, uint pc, bool validated = false) { if (!Has020AccessFrames || format is not (10 or 11)) return false; var size = format == 10 ? 32u : 92u; // Check the far end and long-frame version before loading state. - if (format == 11 && (ReadRteFrameWord(frame + 0x36) >> 12) != 0) + if (!validated && format == 11 && (ReadRteFrameWord(frame + 0x36) >> 12) != 0) { RaiseFormat0Exception(14, State.LastInstructionProgramCounter, M68kInstructionTimingKey.FormatError); return true; } - _ = ReadRteFrameWord(unchecked(frame + size - 2)); + if (!validated) _ = ReadRteFrameWord(unchecked(frame + size - 2)); var context = ReadRteFrameWord(frame + 8); var ssw = ReadRteFrameWord(frame + 10); + if (format == 11 && context == RteValidationContext020) + return RestoreRteValidation020(frame, sr, pc, ssw); // Data faults and foreign opaque images remain implementation gaps. // Never silently consume them as ordinary frame-size pops. if (format != 11 || context != InstructionPrefetchContext020 || (ssw & 0xcf00) != 0) diff --git a/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs b/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs index b7d52ff8..809db969 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.Rte.cs @@ -8,6 +8,7 @@ internal partial class M68kAdvancedTimingInterpreter private ushort ReadRteFrameWord(uint address) { + if (Has020AccessFrames) return (ushort)ReadRteState020(address, 2); try { return ReadWord(address); } catch (M68040MmuFaultException ex) when (IsM68040PhysicalRteValidationFault(ex.Fault)) { @@ -17,6 +18,7 @@ private ushort ReadRteFrameWord(uint address) private uint ReadRteFrameLong(uint address) { + if (Has020AccessFrames) return ReadRteState020(address, 4); try { return ReadLong(address); } catch (M68040MmuFaultException ex) when (IsM68040PhysicalRteValidationFault(ex.Fault)) { diff --git a/Copper68k/M68kAdvancedTimingInterpreter.Rte020.cs b/Copper68k/M68kAdvancedTimingInterpreter.Rte020.cs new file mode 100644 index 00000000..cb56b2a4 --- /dev/null +++ b/Copper68k/M68kAdvancedTimingInterpreter.Rte020.cs @@ -0,0 +1,159 @@ +/* Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT */ +namespace Copper68k; + +internal partial class M68kAdvancedTimingInterpreter +{ + private const ushort RteValidationContext020 = 0xc021; + + // Transient during one RTE. A suspended context lives entirely in its + // private format-B image; no frame-address lookup table is retained. + private sealed class RteValidation020(uint frame, uint instructionPc) + { + internal uint Frame = frame, InstructionPc = instructionPc, Pc; + internal ushort Sr, Format, Version; + internal int Phase; + internal int Width => Phase == 1 ? 4 : 2; + internal uint Address => unchecked(Frame + (Phase switch + { 0 => 0u, 1 => 2u, 2 => 6u, 3 => 0x36u, 4 => Format >> 12 == 10 ? 30u : 90u, _ => 0u })); + } + + private sealed class RteReadFault020(RteValidation020? context, uint address, int width) : + M68kEmulationException($"020/030 RTE {(context is null ? "state-load" : "validation")} read fault at {address:X8}.") + { + internal RteValidation020? Context = context; + internal uint Address = address; + internal int Width = width; + } + + private bool IsRte020Mapped(uint address, int width, M68kBusAccessKind kind) => + _physicalAddressMap is null || + _physicalAddressMap is M68EC020AddressMaskedBus { HasPhysicalAddressMap: false } || + _physicalAddressMap.IsCpuPhysicalAddressMapped(address, width, kind); + + private uint ReadRteState020(uint address, int width) + { + if (!IsRte020Mapped(address, width, M68kBusAccessKind.CpuDataRead)) + throw new RteReadFault020(null, address, width); + return width == 2 ? ReadWord(address) : ReadLong(address); + } + + private uint ReadRteValidation020(RteValidation020 context, ref uint? supplied) + { + uint value; + if (supplied is { } repaired) { value = repaired; supplied = null; } + else + { + if (!IsRte020Mapped(context.Address, context.Width, M68kBusAccessKind.CpuDataRead)) + throw new RteReadFault020(context, context.Address, context.Width); + value = context.Width == 2 ? ReadWord(context.Address) : ReadLong(context.Address); + } + return context.Width == 2 ? value & 0xffff : value; + } + + private void ExecuteRte020(RteValidation020? resume = null, uint? supplied = null) + { + try + { + while (true) + { + var context = resume ?? new RteValidation020(State.A[7], State.LastInstructionProgramCounter); + resume = null; + if (context.Phase == 0) { context.Sr = (ushort)ReadRteValidation020(context, ref supplied); context.Phase = 1; } + if (context.Phase == 1) { context.Pc = ReadRteValidation020(context, ref supplied); context.Phase = 2; } + if (context.Phase == 2) { context.Format = (ushort)ReadRteValidation020(context, ref supplied); context.Phase = 3; } + var format = context.Format >> 12; + var size = format switch { 0 or 1 => 8u, 2 => 12u, 10 => 32u, 11 => 92u, _ => 0u }; + if (size == 0) + { RaiseFormat0Exception(14, context.InstructionPc, M68kInstructionTimingKey.FormatError); return; } + if (format is 10 or 11) + { + if (context.Phase == 3) + { + if (format == 11) context.Version = (ushort)ReadRteValidation020(context, ref supplied); + context.Phase = 4; + } + if (format == 11 && (context.Version >> 12) != 0) + { RaiseFormat0Exception(14, context.InstructionPc, M68kInstructionTimingKey.FormatError); return; } + if (context.Phase == 4) { _ = ReadRteValidation020(context, ref supplied); context.Phase = 5; } + if (TryRestoreM68020AccessFrame(context.Frame, (ushort)format, context.Sr, context.Pc, validated: true)) return; + } + State.SetActiveStackPointer(unchecked(context.Frame + size)); + State.StatusRegister = context.Sr; + if (format == 1) continue; + State.ProgramCounter = context.Pc; + CompleteTiming(M68kInstructionTimingKey.Rte); + return; + } + } + catch (RteReadFault020 fault) + { + if (fault.Context is null) + { + // MC68020UM 6.1.12 / MC68030UM 8.1.13: BERR during + // internal-state loading halts, without another stack image. + State.Halted = true; + _instructionPipe.Reset(); + } + else RaiseRteValidationFault020(fault.Context, fault.Address, fault.Width); + } + } + + private void RaiseRteValidationFault020(RteValidation020 context, uint address, int width) + { + var sr = State.StatusRegister; + State.RecordException(2, context.InstructionPc, sr); + State.StatusRegister = (ushort)((sr | M68kCpuState.Supervisor) & ~0xc000); + var words = new ushort[46]; + void Long(int offset, uint value) { words[offset / 2] = (ushort)(value >> 16); words[offset / 2 + 1] = (ushort)value; } + words[0] = sr; Long(2, context.InstructionPc); words[3] = 0xb008; + words[4] = RteValidationContext020; + words[5] = (ushort)(0x140 | (width == 2 ? 0x20 : 0) | ((sr & 0x2000) != 0 ? 5 : 1)); + Long(0x10, address); Long(0x14, context.Frame); + Long(0x1c, ((uint)context.Sr << 16) | context.Format); Long(0x20, context.Pc); + words[0x28 / 2] = (ushort)context.Phase; words[0x30 / 2] = context.Version; + // Right-justified input buffer at +2C is initially empty. Version 0 + // describes this interpreter's opaque continuation, not silicon state. + try + { + for (var n = words.Length - 1; n >= 0; n--) + { + State.SetActiveStackPointer(unchecked(State.A[7] - 2)); + if (!IsRte020Mapped(State.A[7], 2, M68kBusAccessKind.CpuDataWrite)) + throw new RteReadFault020(null, State.A[7], 2); + WriteWord(State.A[7], words[n]); + } + _instructionPipe.Reset(); + State.ProgramCounter = ReadRteState020(unchecked(State.VectorBaseRegister + 8), 4); + CompleteTiming(M68kInstructionTimingKey.IllegalInstruction); + } + catch (RteReadFault020) { State.Halted = true; _instructionPipe.Reset(); } + } + + private bool RestoreRteValidation020(uint frame, ushort sr, uint pc, ushort ssw) + { + var context = new RteValidation020(ReadRteFrameLong(frame + 0x14), pc) + { + Sr = ReadRteFrameWord(frame + 0x1c), Format = ReadRteFrameWord(frame + 0x1e), + Pc = ReadRteFrameLong(frame + 0x20), Phase = ReadRteFrameWord(frame + 0x28), Version = ReadRteFrameWord(frame + 0x30) + }; + var faultAddress = ReadRteFrameLong(frame + 0x10); + var input = ReadRteFrameLong(frame + 0x2c); + var format = context.Format >> 12; + var expectedSsw = 0x40 | (context.Width == 2 ? 0x20 : 0) | ((sr & 0x2000) != 0 ? 5 : 1); + if (context.Phase is < 0 or > 4 || context.Phase == 3 && format != 11 || context.Phase == 4 && format is not (10 or 11) || + faultAddress != context.Address || (ssw & ~0x100) != expectedSsw) + throw new UnsupportedM68kTimingException(0x4e73, State.LastInstructionProgramCounter, _profile); + var returnedStack = (sr & 0x2000) == 0 ? State.UserStackPointer : + (sr & 0x1000) == (State.StatusRegister & 0x1000) ? unchecked(frame + 92) : + (sr & 0x1000) != 0 ? State.MasterStackPointer : State.InterruptStackPointer; + if (returnedStack != context.Frame) + throw new UnsupportedM68kTimingException(0x4e73, State.LastInstructionProgramCounter, _profile); + for (var offset = 0x10u; offset < 92; offset += 2) _ = ReadRteFrameWord(frame + offset); + State.SetActiveStackPointer(unchecked(frame + 92)); State.StatusRegister = sr; + State.ProgramCounter = unchecked(pc + 2); _instructionPipe.Reset(); + // Explicit handler RTE resumes at the rejected validation read. Earlier + // reads use their serialized values, never replaying completed phases. + ExecuteRte020(context, (ssw & 0x100) == 0 ? input : null); + return true; + } +} diff --git a/Copper68k/M68kAdvancedTimingInterpreter.cs b/Copper68k/M68kAdvancedTimingInterpreter.cs index e3baa485..f683d215 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.cs @@ -8708,6 +8708,8 @@ private void ExecuteRte() return; } + if (Has020AccessFrames) { ExecuteRte020(); return; } + while (true) { var framePointer = State.A[7]; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 2bf28684..ced818de 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -7841,3 +7841,269 @@ Broader A/B data recovery, foreign images, format-9 transport, nested fault/load-failure qualification and remaining reference/consolidation work stay required. Milestone 6 remains **in progress**, `roadmapComplete=false`. No regression is retired or public package published. + +## 020/030 RTE validation and state-load faults — 2026-10-07 + +The next correction distinguishes a rejected frame-validation read from a +rejected internal-state load on EC020, A1200, 020 and 030. The architectural +expectations come from [MC68020UM](https://www.nxp.com/docs/en/data-sheet/MC68020UM.pdf) +6.1.12 and [MC68030UM](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P2.pdf) +8.1.13. A validation fault creates a format-B bus-error frame below the intact +original frame. A failure once state loading starts halts without writing a +second exception image. An inaccessible write or vector read during validation +exception entry also halts after the already-completed prefix. + +The interpreter serializes its pending validation phase and previously read +values in a private opaque format-B continuation. An explicit handler RTE +resumes the rejected phase, retaining completed reads. Clearing DF selects the +right-justified software input buffer; leaving DF set retries only the pending +read. Repeated rejection delivers a fresh bus-error image after deallocating +the old one. An incompatible software-supplied version produces format error +before further state loading. No implicit replay or frame-address side table is +used, and no public API changes are introduced. + +`SyntheticM68020RteFaultTests` passes **36,096 complete programs / 104 reports / +22 executions**, zero mismatches, unsupported forms or skips in this selected +matrix. Tests cover scalar/batch dispatch, each selected validation/load request +and byte lane, all CCRs, four stack states, T0/T1 returns, software-supplied +header/version reads, preservation of an already-read SR after handler writes, +explicit repeated faults, exception-entry failures, and normal/throwaway-chain +headers. All registers, PC, status, stacks and surrounding memory are checked +at instruction boundaries. Opaque bytes are checked for preservation, never +presented as independently known silicon state. + +```powershell +./scripts/test-copper68k-020-rte-faults.ps1 -OutputDirectory artifacts/020-rte-faults +``` + +The command requires fresh output and rejects missing fixtures, empty/partial +execution and changed source/binary/report identities. `-ValidateReportsOnly` +checks the exact 22-test roster, all 104 actual TRX summaries and independently +enumerated case identifiers and weights. Evidence: `audits/Rte020FaultAcceptanceV3`, +source/evidence manifest SHA-256 +`8a5f585c884dff3783add8f1c3a65d8e87e2c0a5b1b3d04ccfaa52bf46906ad1`. +Earlier verifier attempts are retained as failed provisional evidence, not +relabeled accepted runs. Four copied-evidence integrity controls reject missing +reports, changed weights, empty execution and changed source identity under +`Rte020FaultIntegrity`. + +The pinned pre-fix archive at `4837ce900acaea77467e310c50e4e725abdc6a56` +fails all **64 original witnesses / 16 reports / four executions** at the +intended missing-read-rejection check. The baseline's source, binaries, exact +case roster, diagnostics and actual TRX summaries are independently recorded +under `audits/Rte020BaselineReplay`. The current strict prefetch gate still +passes 8,960 programs and frame-entry qualification passes 6,144 cases plus +512 controls (`audits/Rte020Prefetch`, `audits/Rte020Entry`). Fresh pinned +SingleStepTests and Musashi audits also pass (`audits/Rte020Independent`). + +Three isolated production mutations detect replayed validation phases (448 +mismatches), discarded software input (320 mismatches, 128 unaffected passing +cases), and ignored state-load rejection (32 mismatches). The proof checks +source snapshots, binaries, exact passing/failing execution rosters, case +identities/weights, diagnostic phases and actual TRX summaries. Evidence: +`mutations/Rte020FaultsVerified`, verification SHA-256 +`dec7b05946c9206042c3b605abe13447a52167ae30a31578ec714e06aa76df41`. +Earlier failed proof-verifier attempts remain provisional evidence. + +A clean CopperScreen archive at `aa1dad5dcc0fb7c970e8e3443a160487add846af` +builds with zero warnings/errors through isolated private package +`1.5.2-synthetic-dev.71`. Host 171, disk 74 and separate engine 1,080 tests pass, +with six unavailable optional host replays. Two explicit native Workbench 3.1 +floppy boots pass; the app and test CPU assemblies match the exact package DLL. +No HD replay or public release is claimed. Evidence: `consumer-71-final`; +immutable package SHA-256 +`ca953178b2a5b29f3a5fbf933db368765079723ce4fe63bc530f45a7f1aa99a8`. + +The initial snapshot's unfinished full run was cancelled when the subsequent +wrapping correction was identified. Final corrected-source full-suite and +actual-TRX qualification is recorded below; earlier full-suite evidence is not +substituted. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +The host's internal physical-address map rejects logical requests before the +ordinary bus access. This is not a physical port-width/BERR-cycle oracle. +Physical pipeline timing and halted internal register images are not qualified +here. High-address/wrapping validation-fault transport, migrated/foreign private +images, general A/B operand fault generation and data continuation, format-9 +coprocessor context transport, wider nested-fault protocols and remaining +reference/consolidation work stay required. Existing timing policy is retained; +no regression is retired or public package published. + +### High and wrapped RTE validation transport + +A subsequent isolated discovery checks seven low/high/24-bit/32-bit boundary +stack addresses, ISP/MSP, all CCRs, scalar/batch dispatch, each normal-frame +header read and byte lane, and a high VBR. Fault-free frames and a real handler +RTE followed by MOVEQ are verified against independent register/status/stack/PC +and surrounding-memory expectations. On EC020/A1200, 1,024 selected cases +initially failed because the wrapper masked only the start of a map query and +missed a denied physical byte after the 24-bit wrap. The baseline passes the +other 31,232 programs, including all selected 020/030 cases. Evidence: +`audits/Rte020AddressDiscovery`; exact failures and source/TRX/report identities +are independently verified. + +The correction queries each physical span at the existing wrap, retaining +optional-interface fallbacks and the original timed byte/word/long accesses. +The isolated corrected tree passes all **32,256 programs / 16 reports / four +executions**, plus 25 retained address-map, optional-interface and cache tests. +Only `M68EC020Interpreter.cs` differs between those production snapshots; +all fixture and other CPU source identities match. Evidence: +`audits/Rte020AddressFix/proof.json`, SHA-256 +`c81fc9d0dba3a04fe3b8a54950ec99b345e3567a6c2fdfee1c9056ba274b4c92`. + +The correction and fixture are now imported. The maintained mainline command +passes all 32,256 programs with exact source/binary/report identities, execution +roster, independently enumerated cases/weights and actual TRX summaries: + +```powershell +./scripts/test-copper68k-020-rte-address-transport.ps1 -OutputDirectory artifacts/020-rte-address-transport +``` + +Evidence: `audits/Rte020AddressAcceptance`; manifest SHA-256 +`b3e02251d176e67bbea201f76536bcb96a1f8eaffcc194948540bb58d65103f7`. +Four copied-evidence controls reject missing reports, changed weights, empty +execution and changed source identity (`Rte020AddressIntegrity`). The original +36,096-case RTE gate also passes against this corrected source, including its +four fresh integrity controls (`audits/Rte020FinalFaultAcceptance`, +`Rte020FinalFaultIntegrity`); manifest SHA-256 +`c2912e8877678ca52d883a4d862e13419bea1f2db4fc97db55eeb46568661b81`. + +The older `audits/Rte020Full` run was deliberately stopped after this necessary +production correction was identified. Its partial outputs and cancellation +reason remain preserved; it is not accepted full-suite evidence. Fresh final +CPU validation under `audits/Rte020FullFinal` passes **5,291 tests**, zero +failures and **33 explicit optional/discovery skips** (5,324 total), in +46 minutes 19 seconds. Both new RTE matrices and all ten qualified WinUAE +presets are explicitly enabled. The maintained inventory and separate actual-TRX +audit verify all **707 ordinary reports / 86,098,722 deterministic scenarios** +and complete combination weights. Actual-TRX verification SHA-256: +`85f48a39cfc90485fb85876f44d6dfaf006cce4d41a4e54ee2dc64ee1e5eb279`. +Source manifest SHA-256: +`2db5aa39d79ac190a21182238c10744468f40c2056187acd9d8fd797e3d458f1`; +TRX SHA-256: +`c73a6e4e1e54de0a3ea5ed4c8c8fe449278fff74326c6dbe89285ccc8fc9c7e2`. +Clean private .72 consumer qualification passes the +production build with zero warnings/errors, 171 host, 74 disk and 1,080 separate +engine tests, and two explicit native Workbench 3.1 floppy boots. Six optional +host replays remain unavailable, with no HD replay claimed. Runtime app/test +CPU DLLs match the exact package. Evidence: `consumer-72-final`; immutable +package SHA-256 +`aab23ec350f487e7baaaa93b943f16c98457e1af815a80a4f10a1b923d12a8f3`; +validated CPU DLL SHA-256 +`8ef14985fe1393d4e42ae7faebc1b45ab218533674aa4ebef22050643b439e43`. +.71 is not repacked or substituted for the corrected source. Fresh final +prefetch (8,960 programs), entry (6,144 cases plus 512 controls), SingleStepTests +and Musashi gates also pass against this source (`audits/Rte020FinalPrefetch`, +`audits/Rte020FinalEntry`, `audits/Rte020FinalIndependent`). All three original +RTE production mutations are re-proved after the mapping correction: 448, +320 and 32 intended mismatches respectively, with the same 128 unaffected +software-input controls. Evidence: `mutations/Rte020FinalFaults`, verification +SHA-256 `7bb9c19b6537053e80fe3002ab8d84292e7821bd8e68838d4d1a3db9733d5fd4`. +The final shipping validation commands are cleaned before commit: the fault +command's extra trailing blank line is removed and the address command's +baseline-only branches are removed. Both commands rerun successfully against +the unchanged CPU source, with four fresh corruption controls each. Current +command evidence: `audits/Rte020FinalFaultCleanCommand`, manifest SHA-256 +`0641c7d138b165e47edbee3bf87a0c9397403ba39abbeebe410770fdcd65e88b`; +`audits/Rte020AddressCleanCommand`, manifest SHA-256 +`1f470977dd122d647297470d8185bd705d3bc3cb8b90b46c6e05ab3a3dedd849`. +Their integrity controls are retained under `Rte020FinalFaultCleanCommandIntegrity` +and `Rte020AddressCleanCommandIntegrity`. Earlier command runs retain their own +producer identities; they are not substituted for these final command gates. +No public package is published. Wider high-address private-frame load/entry faults, +transferred/foreign images and general A/B continuation remain required; +milestone 6 remains **in progress**, `roadmapComplete=false`. + +### Next required data-read discovery (isolated source) + +While the RTE correction's full suite runs against frozen source, an isolated +archive plus that exact correction reproduces the remaining ordinary operand +read gap. Six literal MOVE source forms (indirect, postincrement, predecrement, +displacement, absolute word and absolute long), byte/word/long sizes, all CCRs, +four stack states, scalar/batch routes and all four 020/030 profiles pass +**18,432 fault-free control programs**, including exact next PC, a following +MOVEQ sentinel, all registers/status/stacks and surrounding memory. +The same fixtures produce **43,008 intended mismatches** when the internal map +rejects an operand request: the ordinary read route never queries/rejects it. +No case is labeled passing or an architectural exception merely because the +instruction silently completes. The selected data-read expectation is format B, +with DF/read/size/function-code information and the operand fault address, +per MC68020UM 6.2 and MC68030UM 8.2. + +Evidence: `audits/Operand020Discovery` (16 reports, four executions: two passing +controls and two failing discovery batches). Its independent verifier checks +frozen source/binary identities, exact case keys and weights, all diagnostic +reasons and actual TRX summaries. The fixture remains in that isolated archive +until the current full-run source freeze ends; this is discovery evidence, not +a promoted mainline gate or a completed data-continuation implementation. +The next correction must capture pending operation state and evaluated EAs, +preserve partial effects, and resume only the rejected operation through +explicit RTE; replaying the instruction is not an acceptable replacement. +Full-format pointer stages, writes, other integer families and broader A/B +transport remain required coverage. Milestone 6 remains **in progress**. + +#### Isolated read-entry and RTE-rerun development + +The next isolated source prototype now rejects the selected operand requests +before ordinary bus access and serializes the pending logical address, consumed +extension PC, opcode, stack and instruction-pipe words in an interpreter-private +format-B context. Both shared scalar reads and the direct fast long +postincrement read are covered. The initial prototype's missed fast route is +retained as failed provisional evidence (`audits/Operand020ReadFixV2`). + +Read-entry qualification passes all **43,008 selected fault cases** plus +**18,432 fault-free controls**, with exact sources, four executions, all 16 +reports, independently enumerated keys/weights and actual TRX summaries +(`audits/Operand020ReadFixV3/verification.json`, SHA-256 +`a7a9bfaa855c9db5ee62a82af6fb41acd3f2dcaccaa65795f171ddd490c268f6`). + +A further isolated prototype implements explicit RTE rerun for these simple +memory-source/register-destination MOVE forms. It restores the pending pipe, +reads the saved operand address and finishes the register/flag suffix without +beginning the instruction, consuming extensions or calculating the source EA +again. All **43,008 selected whole recovery programs** and **18,432 controls** +pass, checking the final architectural state, surrounding memory and a +following MOVEQ sentinel. Strict evidence: +`audits/Operand020ReadFixV4/verification.json`, SHA-256 +`4c57ba682ae147a5b06bdb68c7115c2ca6da4eda2123619e43c08f0e0075c795`. + +Neither prototype is imported or a promoted mainline gate. Software-buffer +repair, persistent refaults, handler-modified operands/extensions, saved-pipe +mutations, A7/alias/index combinations and retained controls still require +qualification before promotion. Full-format pointer stages, memory destinations, +other instruction families and wider A/B transport remain required. The +existing full-suite source remains frozen while this development proceeds; +milestone 6 remains **in progress**, `roadmapComplete=false`. + +The next frozen prototype (`audits/Operand020ReadFixV5`) passes **129,024 +whole handler-recovery programs / 24 reports / six executions**. Real handlers +supply a different value through DIB and clear DF, persistently reject the +pending read before allowing it, or modify the source base, original opcode and +consumed extension words. Every CCR, selected size/form, stack state and byte +lane runs through scalar/batch dispatch. Complete architectural state and +surrounding memory are checked after each handler instruction, resumption and +following sentinel. Software input causes no ordinary source read; explicit +rerun completes exactly one successful read. Strict verification SHA-256: +`439c7e3dd73b4b877e066af7a37ab23afdff2ae156853a9d46c6b007b4a41913`. + +Three isolated mutations produce exactly **43,008** software-input mismatches, +**50,176** saved-address mismatches and **21,504** repeated-predecrement +mismatches, with the remaining 86,016, 78,848 and 107,520 programs respectively +still passing. The proof checks the sole mutated CPU source, unchanged fixtures, +exact execution outcomes, every failing CCR identifier and diagnostic phase, +coverage weights and actual TRX summaries. Evidence: +`mutations/Operand020ReadHandlers/verification.json`, SHA-256 +`71f5d52b125c3df5bddc2312f678035791beb373045d3296ac80d34cc253d250`. +All **51 retained executions** pass: the 68,352 previously qualified RTE +programs produce byte-identical reports with matching actual TRX summaries, +and all 25 mapping/cache/optional-interface controls pass. Evidence: +`audits/Operand020ReadFixV5/retained/verification.json`, SHA-256 +`6eb70887a60d448fde5d20966ea5a412693fc24a87cf0f9eaeaba99d682f2ad0`. + +The prototype remains unimported and unpromoted. Before promotion, pending-read +origin must be distinguished from a later exception/vector read rather than +inferred solely from the retained opcode; that distinction is not yet qualified. +Saved-pipe mutations, A7/alias/index combinations, trace/interrupt and changed +return-SR/stack behavior, high-address transport and evidence-integrity controls +also remain required. Broader pointer, memory-destination and other-family +continuation stays open. No private consumer package is built for this prototype; +the next such version remains .73. Milestone 6 remains **in progress**. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 8431b154..749338bd 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1922,6 +1922,8 @@ provenance, internal restoration, data/writeback/context transfer, other models' restoration and broader independent qualification/consolidation remain required. Milestone 6 remains **in progress**, `roadmapComplete=false`. + + On 2026-10-07 the executed 040 MOVEM read-recovery gate passes 229,968 complete fault/RTE/resumption/sentinel programs across scalar and batch execution. It covers every legal memory-source encoding, all legal full-format structures in @@ -2755,3 +2757,52 @@ A/B data continuation, foreign images, format-9 transport and wider restoration/consolidation remain required. See the [prefetch correction](COPPER68K_REFERENCE_QUALIFICATION.md#020030-instruction-prefetch-recovery-correction--2026-10-07). Milestone 6 remains **in progress**, `roadmapComplete=false`. + +The subsequent RTE validation/load-fault slice passes 36,096 selected complete +programs across EC020/A1200/020/030. It preserves completed validation reads, +resumes only through explicit handler RTE, supports DF-cleared software input, +and halts on state-load or exception-entry failures. The maintained command +checks all 104 reports against their actual 22 executions and independently +enumerated cases. Mutation proofs, the archived failing baseline, independent +audits and clean private-package .71 consumer/native floppy checks pass. That +snapshot's unfinished full run was cancelled when the subsequent wrapping +correction was identified; final corrected-source results are recorded below. +Broader data continuation, foreign images, format-9 transport +and consolidation remain required. See the +[RTE fault correction](COPPER68K_REFERENCE_QUALIFICATION.md#020030-rte-validation-and-state-load-faults--2026-10-07). +Milestone 6 remains **in progress**, `roadmapComplete=false`. + +The next address-transport qualification catches and fixes an EC020 wrapper +map-query bug at the 24-bit wrap. The maintained gate passes 32,256 normal-frame +RTE programs at seven low/high/boundary stack addresses, including real +validation-fault handlers and high VBR, with independently verified coverage +and four integrity controls. The original 36,096-case RTE gate also passes +against the corrected source. The isolated baseline retains 1,024 intended +wrap failures; 25 affected mapping/cache tests pass with the correction. The +older unfinished full run is preserved as cancelled evidence. Private .72 clean +consumer/native floppy qualification passes; fresh final full CPU validation +passes 5,291 tests with zero failures and 33 optional/discovery skips (5,324 +total). The maintained inventory and separate actual-TRX audit verify all 707 +ordinary reports, 86,098,722 deterministic scenarios and complete combination +weights. Wider private +load/entry fault transport, foreign images, data continuation and consolidation +remain required. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +Isolated development now passes 43,008 selected ordinary MOVE read-entry +cases and, in a subsequent snapshot, 43,008 explicit RTE-rerun programs; +each also passes 18,432 fault-free controls. Saved addresses, extension PC and +pipe words allow the selected register-destination suffix to finish without +instruction replay. These prototypes remain unimported and unpromoted pending +software-input/refault, handler-modification, alias/A7/index, mutation and +retained-control qualification. Broader instruction/operand continuations and +consolidation remain required. Milestone 6 remains **in progress**, +`roadmapComplete=false`. + +The subsequent frozen prototype passes 129,024 software-input, persistent +refault and handler-modification recovery programs. Three source mutations +detect 43,008, 50,176 and 21,504 intended mismatches, preserving the unaffected +passing cohorts. All 68,352 retained RTE programs and 25 mapping/cache/optional +controls pass. This remains unimported and unpromoted pending operation-origin +discrimination, saved-pipe, A7/alias/index, trace/interrupt, return-SR/stack, +high-address and integrity qualification. Broader continuation and consolidation +remain required. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-020-rte-address-transport.ps1 b/scripts/test-copper68k-020-rte-address-transport.ps1 new file mode 100644 index 00000000..05b31bf5 --- /dev/null +++ b/scripts/test-copper68k-020-rte-address-transport.ps1 @@ -0,0 +1,74 @@ +#requires -Version 7.0 +[CmdletBinding()] +param([string]$OutputDirectory='artifacts/020-rte-address-transport',[switch]$ValidateReportsOnly) +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$Root=[IO.Path]::GetFullPath($OutputDirectory,$repo) +function Hash([string]$Path){(Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()} +$tracked=@(& git -C $repo ls-files 'Copper68k/*.cs' 'Copper68k/*.csproj' 'Copper68k.Tests/*.cs' 'Copper68k.Tests/*.csproj') +if($LASTEXITCODE -ne 0 -or $tracked.Count -eq 0){throw 'CPU/test source inventory unavailable'} +$sources=@($tracked+@('Copper68k.Tests/Synthetic/SyntheticM68020RteAddressTransportTests.cs','scripts/test-copper68k-020-rte-address-transport.ps1')|Sort-Object -Unique) +$reports=@(foreach($model in @('68EC020','A1200','68020','68030')){foreach($phase in @('control','fault')){foreach($route in @('scalar','batch')){"$model-rte-address-$phase-$route.json"}}}) +$evidence=@('audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll')+$reports +$manifestPath=Join-Path $Root 'inputs.json' +if(-not $ValidateReportsOnly){ + if(Test-Path $Root){throw "Use a fresh output directory: $Root"} + New-Item -ItemType Directory -Path $Root|Out-Null + $identities=@($sources|ForEach-Object {@{path=$_;sha256=(Hash (Join-Path $repo $_))}}) + $protected=@(@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll')| + Where-Object {Test-Path (Join-Path $repo $_)}|ForEach-Object {@{path=$_;sha256=(Hash (Join-Path $repo $_))}}) + $settings=@{COPPER68K_RUN_020_RTE_ADDRESS_TRANSPORT='1';COPPER68K_SYNTHETIC_REPORT_DIR=$Root};$saved=@{} + try{ + foreach($key in $settings.Keys){$saved[$key]=[Environment]::GetEnvironmentVariable($key);[Environment]::SetEnvironmentVariable($key,$settings[$key])} + & dotnet.exe test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --artifacts-path (Join-Path $Root 'build') --filter 'FullyQualifiedName~SyntheticM68020RteAddressTransportTests' --logger 'trx;LogFileName=audit.trx' --results-directory $Root *> (Join-Path $Root 'execution.log') + $testExit=$LASTEXITCODE + }finally{foreach($key in $saved.Keys){[Environment]::SetEnvironmentVariable($key,$saved[$key])}} + @{schema=1;sourceCommit=(& git -C $repo rev-parse HEAD);testExit=$testExit;sources=$identities;protected=$protected; + evidence=@($evidence|ForEach-Object {@{path=$_;sha256=(Hash (Join-Path $Root $_))}})}|ConvertTo-Json -Depth 10|Set-Content $manifestPath +} +$manifest=Get-Content $manifestPath -Raw|ConvertFrom-Json +if($manifest.schema -ne 1 -or $manifest.testExit -ne 0 -or $manifest.sources.Count -ne $sources.Count -or + (($manifest.sources.path|Sort-Object)-join '|') -cne (($sources|Sort-Object)-join '|') -or + $manifest.evidence.Count -ne $evidence.Count -or (($manifest.evidence.path|Sort-Object)-join '|') -cne (($evidence|Sort-Object)-join '|')){throw 'Missing/failed address-transport identities'} +foreach($item in $manifest.sources+$manifest.protected){if((Hash (Join-Path $repo $item.path)) -ne $item.sha256){throw 'Changed address-transport source/protected DLL'}} +foreach($item in $manifest.evidence){if((Hash (Join-Path $Root $item.path)) -ne $item.sha256){throw 'Changed address-transport evidence'}} +[xml]$trx=Get-Content (Join-Path $Root 'audit.trx') -Raw;$tests=@($trx.TestRun.Results.UnitTestResult) +$prefix='Copper68k.Tests.Synthetic.SyntheticM68020RteAddressTransportTests.' +$names=@(($prefix+'FaultFreeHighAndWrappedFrames(batch: False)'),($prefix+'FaultFreeHighAndWrappedFrames(batch: True)'), + ($prefix+'ScalarHighAndWrappedValidationFaults'),($prefix+'BatchHighAndWrappedValidationFaults')) +if($tests.Count -ne 4 -or (($tests.testName|Sort-Object)-join '|') -cne (($names|Sort-Object)-join '|')){throw 'Wrong address-transport execution roster'} +foreach($test in $tests){if($test.outcome -ne 'Passed'){throw 'Wrong address-transport execution outcome'}} +$rows=@();$passing=0;$mismatching=0 +foreach($model in @('68EC020','A1200','68020','68030')){foreach($phase in @('control','fault')){foreach($route in @('scalar','batch')){ + $group="rte-address-$phase-$route";$name="$model-$group.json";$report=Get-Content (Join-Path $Root $name) -Raw|ConvertFrom-Json + $keys=@{} + foreach($frame in @(0x4700L,0x10004700L,0xfffffbL,0xffffffL,0xfffffffbL,0xffffffffL,0x1000001L)){ + foreach($bank in @('ISP','MSP')){foreach($ccr in 0..31){ + $reads=@(if($phase -eq 'control'){,@(0,0)}else{@(0,2),@(2,4),@(6,2)}) + foreach($read in $reads){$lanes=if($phase -eq 'control'){@(0)}else{@(0..($read[1]-1))};foreach($lane in $lanes){ + $key='{0}/RTE/address/bank={1}/frame={2:X8}/read={3:X}:{4}/byte={5}/ccr={6:X2}' -f $model,$bank,$frame,$read[0],$read[1],$lane,$ccr + $keys[$key]='passing' + }} + }} + } + $expected=$keys.Count;$failures=0 + if($report.schema -ne 1 -or $report.model -ne $model -or $report.group -ne $group -or $report.logicalCases -ne $expected -or + $report.xunitBatches -ne 1 -or $report.counts.mismatching -ne $failures -or $report.counts.passing -ne $expected-$failures -or + $report.counts.unsupported+$report.counts.untested -ne 0 -or + (($report.combinations.psobject.Properties.Name|Sort-Object)-join '|') -cne (($keys.Keys|Sort-Object)-join '|')){throw "Incomplete address-transport report: $name"} + foreach($key in $report.combinations.psobject.Properties){$status=$keys[$key.Name];if(@($key.Value.psobject.Properties).Count -ne 1 -or $key.Value.$status -ne 1){throw 'Wrong address-transport combination weight/outcome'}} + if($report.failures.Count -ne $failures){throw 'Missing address-transport diagnostics'} + foreach($failure in $report.failures){if($failure.status -ne 'mismatching' -or $failure.reason -ne 'wrapped validation request did not reject its physical byte exactly once'){throw 'Unrelated address-transport failure'}} + $actual=[regex]::Matches(($tests.Output.StdOut -join "`n"),[regex]::Escape("$model/${group}: ")+'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)') + if($actual.Count -ne 1 -or [int]$actual[0].Groups[2].Value -ne $expected -or [int]$actual[0].Groups[3].Value -ne $keys.Count){throw 'Missing actual address-transport TRX summary'} + $counts=$actual[0].Groups[1].Value|ConvertFrom-Json + foreach($status in @('passing','mismatching','unsupported','untested')){if($counts.$status -ne $report.counts.$status){throw 'Address-transport TRX/report disagreement'}} + $passing+=$report.counts.passing;$mismatching+=$report.counts.mismatching + $rows+=@{path=$name;sha256=(Hash (Join-Path $Root $name));cases=$expected;keys=$keys.Count;passing=$expected-$failures;mismatching=$failures} +}}} +if($passing -ne 32256 -or $mismatching -ne 0 -or $rows.Count -ne 16){throw 'Address-transport inventory changed'} +@{schema=1;fixed=$true;passing=$passing;mismatching=$mismatching;executions=4;reports=$rows; + inputsSha256=(Hash (Join-Path $Root 'inputs.json'));helperSha256=(Hash $PSCommandPath); + cpuSha256=(Hash (Join-Path $Root 'build/bin/Copper68k/release/Copper68k.dll'));testSha256=(Hash (Join-Path $Root 'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll')); + trxSha256=(Hash (Join-Path $Root 'audit.trx'));roadmapComplete=$false}|ConvertTo-Json -Depth 9|Set-Content (Join-Path $Root 'verification.json') +Write-Output "$passing passing / $mismatching mismatches / 16 reports / four executions." diff --git a/scripts/test-copper68k-020-rte-faults.ps1 b/scripts/test-copper68k-020-rte-faults.ps1 new file mode 100644 index 00000000..1d02d4d9 --- /dev/null +++ b/scripts/test-copper68k-020-rte-faults.ps1 @@ -0,0 +1,113 @@ +#requires -Version 7.0 +[CmdletBinding()] +param([string]$OutputDirectory='artifacts/020-rte-faults', [switch]$ValidateReportsOnly) +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +function Hash([string]$Path) { (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() } +$sources=@(@(& git -C $repo ls-files 'Copper68k/*.cs' 'Copper68k/*.csproj' 'Copper68k.Tests/*.cs' 'Copper68k.Tests/*.csproj')+ + @('Copper68k/M68kAdvancedTimingInterpreter.Rte020.cs','Copper68k.Tests/Synthetic/SyntheticM68020RteFaultTests.cs', + 'scripts/test-copper68k-020-rte-faults.ps1') | Sort-Object -Unique) +$models=@('68EC020','A1200','68020','68030'); $banks=@('user','user-M','ISP','MSP') +$groups=@('rte-validation-fault','rte-load-fault','rte-validation-matrix','rte-load-matrix','rte-validation-ccr','rte-load-ccr', + 'rte-handler-buffer','rte-handler-alias','rte-handler-refault','rte-handler-version','rte-entry-halt','rte-header-normal','rte-header-chained') +$reports=@(foreach($model in $models){foreach($group in $groups){foreach($route in @('scalar','batch')){"$model-$group-$route.json"}}}) +$evidence=@('audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll')+$reports +$manifestPath=Join-Path $output 'inputs.json' +if(-not $ValidateReportsOnly){ + if(Test-Path $output){throw "Use a fresh output directory: $output"} + New-Item -ItemType Directory -Path $output | Out-Null + $identities=@($sources | ForEach-Object { @{path=$_;sha256=(Hash (Join-Path $repo $_))} }) + $protected=@(@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | + Where-Object {Test-Path (Join-Path $repo $_)} | ForEach-Object {@{path=$_;sha256=(Hash (Join-Path $repo $_))}}) + $settings=@{COPPER68K_RUN_020_RTE_FAULTS='1';COPPER68K_SYNTHETIC_REPORT_DIR=$output};$saved=@{} + try{ + foreach($key in $settings.Keys){$saved[$key]=[Environment]::GetEnvironmentVariable($key);[Environment]::SetEnvironmentVariable($key,$settings[$key])} + & dotnet.exe test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --artifacts-path (Join-Path $output 'build') --filter 'FullyQualifiedName~SyntheticM68020RteFaultTests' --logger 'trx;LogFileName=audit.trx' --results-directory $output *> (Join-Path $output 'execution.log') + $testExit=$LASTEXITCODE + }finally{foreach($key in $saved.Keys){[Environment]::SetEnvironmentVariable($key,$saved[$key])}} + @{schema=1;sourceCommit=(& git -C $repo rev-parse HEAD);testExit=$testExit;sources=$identities;protected=$protected; + evidence=@($evidence | ForEach-Object {@{path=$_;sha256=(Hash (Join-Path $output $_))}})} | ConvertTo-Json -Depth 10 | Set-Content $manifestPath +} +$manifest=Get-Content $manifestPath -Raw | ConvertFrom-Json +if($manifest.schema -ne 1 -or $manifest.sources.Count -ne $sources.Count -or + (($manifest.sources.path|Sort-Object)-join '|') -cne (($sources|Sort-Object)-join '|') -or + $manifest.evidence.Count -ne $evidence.Count -or + (($manifest.evidence.path|Sort-Object)-join '|') -cne (($evidence|Sort-Object)-join '|')){throw 'Missing RTE fault identities'} +foreach($item in $manifest.sources+$manifest.protected){if((Hash (Join-Path $repo $item.path)) -ne $item.sha256){throw "Changed source/protected DLL: $($item.path)"}} +foreach($item in $manifest.evidence){if((Hash (Join-Path $output $item.path)) -ne $item.sha256){throw "Changed RTE evidence: $($item.path)"}} +[xml]$trx=Get-Content (Join-Path $output 'audit.trx') -Raw +$tests=@($trx.TestRun.Results.UnitTestResult) +$prefix='Copper68k.Tests.Synthetic.SyntheticM68020RteFaultTests.' +$names=@(foreach($method in @('ScalarFrameFaultMatrix','BatchFrameFaultMatrix','ScalarFrameFaultCcrBoundaries','BatchFrameFaultCcrBoundaries')){$prefix+$method}) +foreach($route in @('False','True')){ + foreach($method in @('ValidationFaultPreservesFrameAndExplicitRteResumes','StateLoadFaultHaltsWithoutWritingMemory','FaultDuringValidationExceptionEntryHalts')){$names+=($prefix+$method+'(batch: '+$route+')')} + foreach($mode in @('buffer','alias','refault','version')){$names+=($prefix+'HandlerControls(batch: '+$route+', mode: "'+$mode+'")')} + foreach($chained in @('False','True')){$names+=($prefix+'HeaderFaultsInNormalFramesAndThrowawayChains(batch: '+$route+', chained: '+$chained+')')} +} +if($tests.Count -ne 22 -or @($tests.testId|Sort-Object -Unique).Count -ne 22 -or + (($tests.testName|Sort-Object)-join '|') -cne (($names|Sort-Object)-join '|') -or + @($tests|Where-Object outcome -ne 'Passed').Count -ne 0 -or $manifest.testExit -ne 0){throw 'Incomplete/failed RTE execution roster'} +# Requests are enumerated independently of the test/production read helpers. +$validation=@(@(0,2,1),@(2,4,1),@(6,2,1),@(0x36,2,1),@(0x5a,2,1)) +$loading=@(@(8,2,1),@(10,2,1),@(0x14,4,1),@(0x24,4,1),@(12,2,1),@(14,2,1)) +for($offset=0x10;$offset -lt 92;$offset+=2){$loading+=,@($offset,2,$(if($offset -in @(0x36,0x5a)){2}else{1}))} +$rows=@();$total=0 +foreach($model in $models){foreach($group in $groups){foreach($route in @('scalar','batch')){ + $keys=@{};$weight=1;$load=$group.StartsWith('rte-load-');$phase=if($load){'loading'}else{'validation'} + if($group.EndsWith('-fault')){foreach($bank in $banks){$keys[('{0}/RTE/{1}/bank={2}/read={3:X}' -f $model,$phase,$bank,$(if($load){8}else{0x5a}))]=1}} + elseif($group.EndsWith('-matrix') -or $group.EndsWith('-ccr')){ + $ccrs=$group.EndsWith('-ccr') + $requests=@(if($ccrs){,@($(if($load){8}else{0}),2,1)}elseif($load){$loading}else{$validation}) + foreach($bank in $banks){foreach($trace in @(0,0x8000,0x4000)){foreach($read in $requests){for($lane=0;$lane -lt $read[1];$lane++){ + $keys[('{0}/RTE/{1}/bank={2}/T={3:X4}/read={4:X}:{5}:{6}/byte={7}' -f $model,$phase,$bank,$trace,$read[0],$read[1],$read[2],$lane)]=$weight + }}}} + }elseif($group.StartsWith('rte-handler-')){ + $mode=$group.Substring(12);$requests=@(if($mode -eq 'buffer'){$validation[0..2]}else{,@($(if($mode -eq 'version'){0x36}else{0x5a}),2,1)}) + foreach($bank in $banks){foreach($read in $requests){for($lane=0;$lane -lt $read[1];$lane++){ + $keys[('{0}/RTE/handler={1}/bank={2}/read={3:X}:{4}:{5}/byte={6}' -f $model,$mode,$bank,$read[0],$read[1],$read[2],$lane)]=1 + }}} + }elseif($group -eq 'rte-entry-halt'){ + foreach($bank in $banks){foreach($kind in @('write','vector')){ + $offsets=if($kind -eq 'vector'){@(0)}else{@(0..45|ForEach-Object {$_*2})};$width=if($kind -eq 'vector'){4}else{2} + foreach($offset in $offsets){for($lane=0;$lane -lt $width;$lane++){$keys[('{0}/RTE/entry-halt/bank={1}/kind={2}/offset={3:X}/byte={4}' -f $model,$bank,$kind,$offset,$lane)]=1}} + }} + }else{ + $weight=2 + foreach($start in @('ISP','MSP')){$tails=if($group -eq 'rte-header-chained'){$banks}else{@($start)} + foreach($tail in $tails){foreach($restored in $banks){foreach($format in @(0,2)){foreach($read in $validation[0..2]){for($lane=0;$lane -lt $read[1];$lane++){ + $keys[('{0}/RTE/header/start={1}/tail={2}/restore={3}/format={4}/read={5:X}:{6}/byte={7}' -f $model,$start,$tail,$restored,$format,$read[0],$read[1],$lane)]=$weight + }}}}} + } + } + $ccrValues=if($group.EndsWith('-ccr')){0..31}elseif($group.StartsWith('rte-header-')){@(0,31)}else{@(31)} + $expanded=@{} + foreach($key in $keys.Keys){foreach($ccr in $ccrValues){$expanded[('{0}/ccr={1:X2}' -f $key,$ccr)]=1}} + $keys=$expanded;$weight=1 + $expected=$keys.Count + $report=Get-Content (Join-Path $output "$model-$group-$route.json") -Raw | ConvertFrom-Json + if($report.schema -ne 1 -or $report.model -ne $model -or $report.group -ne "$group-$route" -or + $report.logicalCases -ne $expected -or $report.xunitBatches -ne 1 -or @($report.combinations.psobject.Properties).Count -ne $keys.Count -or + $report.counts.passing -ne $expected -or $report.counts.mismatching+$report.counts.unsupported+$report.counts.untested -ne 0){throw "Incomplete RTE report: $model/$group/$route"} + foreach($combination in $report.combinations.psobject.Properties){if(-not $keys.ContainsKey($combination.Name) -or + @($combination.Value.psobject.Properties).Count -ne 1 -or $combination.Value.passing -ne $keys[$combination.Name]){throw 'Changed RTE combination/weight'}} + $flag=if($route -eq 'scalar'){'False'}else{'True'} + $method=switch -Regex ($group){ + '-matrix$' {$(if($route -eq 'scalar'){'Scalar'}else{'Batch'})+'FrameFaultMatrix';break} + '-ccr$' {$(if($route -eq 'scalar'){'Scalar'}else{'Batch'})+'FrameFaultCcrBoundaries';break} + '^rte-validation-fault$' {'ValidationFaultPreservesFrameAndExplicitRteResumes(batch: '+$flag+')';break} + '^rte-load-fault$' {'StateLoadFaultHaltsWithoutWritingMemory(batch: '+$flag+')';break} + '^rte-handler-' {'HandlerControls(batch: '+$flag+', mode: "'+$mode+'")';break} + '^rte-entry-halt$' {'FaultDuringValidationExceptionEntryHalts(batch: '+$flag+')';break} + default {'HeaderFaultsInNormalFramesAndThrowawayChains(batch: '+$flag+', chained: '+$(if($group -eq 'rte-header-chained'){'True'}else{'False'})+')'} + } + $test=@($tests|Where-Object testName -eq ($prefix+$method))[0] + $matches=[regex]::Matches([string]$test.Output.StdOut,[regex]::Escape("$model/${group}-${route}: ")+'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)') + if($matches.Count -ne 1 -or [int]$matches[0].Groups[2].Value -ne $expected -or [int]$matches[0].Groups[3].Value -ne $keys.Count){throw 'Missing actual RTE TRX summary'} + $counts=$matches[0].Groups[1].Value|ConvertFrom-Json + foreach($status in @('passing','mismatching','unsupported','untested')){if($counts.$status -ne $report.counts.$status){throw 'RTE TRX/report disagreement'}} + $total+=$expected;$rows+=@{model=$model;group="$group-$route";cases=$expected;keys=$keys.Count} +}}} +if($total -ne 36096 -or $rows.Count -ne 104){throw 'RTE inventory changed'} +@{cases=$total;reports=$rows;roadmapComplete=$false}|ConvertTo-Json -Depth 5|Set-Content (Join-Path $output 'verification.json') +Write-Output '36,096 passing RTE fault programs / 104 reports / 22 executions.' From 020629b5970bcd0d06b4a651d1f73f05d0c18f8c Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 07:02:19 +0300 Subject: [PATCH 088/211] Record typed trace-vector recovery qualification --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 62 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 16 +++++ 2 files changed, 78 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index ced818de..63b45104 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8107,3 +8107,65 @@ return-SR/stack behavior, high-address transport and evidence-integrity controls also remain required. Broader pointer, memory-destination and other-family continuation stays open. No private consumer package is built for this prototype; the next such version remains .73. Milestone 6 remains **in progress**. + +#### Typed trace-vector read continuation — 2026-10-07 + +The next isolated discovery distinguishes a pending exception read from a +completed MOVE source read. With T1 set, a repaired MOVE completes its register +and flag effects, creates the format-2 trace frame, then encounters a rejected +vector-9 read. The V5 opcode-only recovery path incorrectly treats that vector +as another source operand. The independent fixture retains **18,432 passing +controls**, **24,576 long-size mismatches** and **49,152 byte/word unsupported +programs**; these failed discovery batches are not promoted coverage. +The documented ordering is in [MC68020UM](https://www.nxp.com/docs/en/data-sheet/MC68020UM.pdf) +6.1.11 and [MC68030UM](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P2.pdf) +8.1.12: a bus fault during trace processing must be handled before that +processing finishes. + +The frozen corrected prototype serializes a typed pending-read kind and the +pending indexed timing-policy state. A trace-vector continuation retries only +its saved read (or accepts DF-cleared DIB input), then finishes the existing +exception timing policy. It neither reexecutes MOVE nor pushes another trace +frame. Unknown read origins remain explicitly unsupported on return. All +**92,160 selected programs / 16 reports / four executions** pass across +EC020/A1200/020/030, scalar/batch routes, six source forms, three sizes, four +stack states, every CCR and all vector-read byte lanes. Removing the trace +continuation produces exactly **73,728 mismatches**, with **18,432 unchanged +controls** passing. The strict proof checks both changed CPU files, unchanged +fixtures, precise failure identifiers/reasons, combination weights and actual +TRX summaries. Evidence: +`audits/Operand020TraceVectorFixV2/proof.json`, SHA-256 `c5168b47442dcd246c206b0d784596d603c463e9504a0fd3bd109d4d67f6e5d2`; +fixed verification SHA-256 `f4cd4ac39c04d200a9d73137d2e1ac2183f15e1332199e87ba6c1728c17392d8`. + +An additional canonical-indirect fixture passes **73,728 complete programs / +48 reports / 12 executions**, with and without the earlier source fault. +Real handlers supply a different trace target through DIB, persistently refault +before allowing the pending read, or change VBR through MOVEC. All code is +prepared before execution; the source handler installs the subsequent bus-error +handler with a real memory write. Per-instruction architectural state, all +stacks, trace-frame fields, surrounding memory, exception counts, source/vector +read counts and the following sentinel are checked. A VBR change preserves +the already-evaluated pending vector address. Ignoring DIB and recalculating +that address from VBR each produce **24,576 precise mismatches**, with **49,152 +unaffected programs** passing. Evidence: +`audits/Operand020TraceVectorHandlersV2/verification.json`, SHA-256 `6277bbd11424c486d6e0c2e1ad12d451fa196f41e7cb72e3d6a3840175931bf6`; +mutation verification SHA-256 `dc882d0f264639ffa4049c0546f1f48e46fe6fa2eedaff2a7729aa5aa18b283d` and `85982c02016c89b78c3da8cc300ffd6cb6fa5741a15d716967553ffb131c82b0`. +The strict verifier also rejects a missing report, an empty execution selection +and an incorrect per-combination weight; its separate controls do not count as +CPU programs. + +Fresh corrected-source retention passes all **129,024 earlier handler programs** +and all **51 retained executions**: **68,352 RTE programs** have byte-identical +reports and matching actual TRX summaries, plus **25 mapping/cache/optional +interface controls**. These source-specific gates validate the typed prototype; +the delivered production full-suite/private .72 results apply to the earlier +RTE/map correction, not this prototype. The failed initial handler fixture +compile attempt is retained separately and supplies no passing coverage. + +This development remains isolated, unimported and unpromoted. Independent +nested vector-frame PC provenance, T0/interrupt interactions, other exception +read origins, saved-pipe mutation and warm dispatch, A7/aliases/indexing, +changed return SR/stacks, wider fault transport, memory destinations and +other-family continuation remain required. Existing reference and consolidation +gaps remain open. No package is built or published; the next private consumer +version remains .73. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 749338bd..45a5b699 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2806,3 +2806,19 @@ controls pass. This remains unimported and unpromoted pending operation-origin discrimination, saved-pipe, A7/alias/index, trace/interrupt, return-SR/stack, high-address and integrity qualification. Broader continuation and consolidation remain required. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +Typed pending-read development now distinguishes trace-vector suspension from +an already-completed MOVE source. The isolated corrected matrix passes 92,160 +trace/control programs; removing its trace continuation causes exactly 73,728 +failures while preserving 18,432 controls. Another 73,728 canonical-indirect +programs cover software-supplied vector targets, persistent refault and changed +VBR, both with and without an earlier source fault. Two mutations each detect +24,576 intended mismatches with 49,152 unaffected passes. Three evidence-integrity +controls reject missing reports, empty executions and incorrect weights. +All 129,024 previous handler programs and 68,352 RTE programs plus 25 retained +controls pass against the typed source. The prototype remains isolated and +unpromoted pending nested-PC provenance, other read origins, pipeline/A7/index, +trace/interrupt, changed return-SR/stack and wider transport qualification. +Broader continuation, reference gaps and consolidation remain required. +Milestone 6 remains **in progress**, `roadmapComplete=false`. See the +[typed trace-vector continuation](COPPER68K_REFERENCE_QUALIFICATION.md#typed-trace-vector-read-continuation--2026-10-07). From 4f9024fbe8964f40b40396be8567f8174d477708 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 07:25:20 +0300 Subject: [PATCH 089/211] Record A7 and saved instruction-pipe qualification --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 59 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 +++++ 2 files changed, 74 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 63b45104..bb7f1440 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8169,3 +8169,62 @@ changed return SR/stacks, wider fault transport, memory destinations and other-family continuation remain required. Existing reference and consolidation gaps remain open. No package is built or published; the next private consumer version remains .73. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +#### A7/MOVEA and captured private-pipe qualification — 2026-10-07 + +Two new isolated fixtures qualify the unchanged typed-read CPU source above. +The A7 fixture passes **196,608 programs / 32 reports / eight executions**: +**21,504 fault-free controls** and **175,104 explicit repair, software-input +and persistent-refault programs**. EC020/A1200/020/030 run scalar/batch routes, +indirect/postincrement/predecrement sources, MOVE byte/word/long, MOVEA word/long +to A0 and aliased A7, four stack states, all CCRs and all operand fault byte +lanes. Literal expected strides include two bytes for A7 byte operations; +MOVEA.W sign extension, flag preservation and destination-after-source ordering +are independent of production helpers. All architectural registers/status, +active and inactive stacks, defined bus-error fields, surrounding memory, +exception/read counts and a following MOVEQ sentinel are checked after each +handler and recovery instruction. Changing the resumed byte stride to one +causes exactly **3,072 mismatches / 193,536 unaffected passes**. Moving the +source update after an aliased MOVEA destination write causes exactly +**18,432 mismatches / 178,176 unaffected passes**. + +The captured-pipe fixture passes **86,016 programs / 32 reports / eight +executions**. A real instruction-prefetch error and handler/RTE repair supply +the opcode and following word; the subsequent ordinary MOVE fault genuinely +captures that remaining word. Real handlers write a different following opcode +to memory, and separately replace the saved private pipe word. This covers +three extension-free source forms, three operand sizes, four stack states, +all CCRs/operand byte lanes, both routes and cold/warmed dispatch on all four +profiles. Warming uses batch dispatch and requires host-code-reader use; +recovery then uses the selected scalar or batch route. The original saved word +and software-edited word both override different memory/cached candidates. +Discarding the pipe causes **86,016 mismatches**; ignoring the edited pipe word +causes **43,008 mismatches / 43,008 unaffected passes**. These checks qualify +interpreter-private continuation and existing execution policy, not physical +cache/pipeline contents or timing. + +Each strict case verifier checks exact executions, every combination/CCR weight, +precise mutation failure identifiers/reasons and matching actual TRX summaries. +A separate complete-input audit independently enumerates all CPU/test C# and +project sources, checks every file hash, pins the execution helper/base inputs +and links the selected-case proof to its TRX. **Ten separate integrity controls** +(five per gate) reject omitted fixtures, changed source, altered producer +identities, empty execution selections and wrong combination weights. Mutation +fixtures are byte-identical to their accepted parent; only the intended private +operand-continuation CPU file changes. + +Evidence: `audits/Operand020StackRead`, `audits/Operand020PipeReadV4`, +`mutations/Operand020StackRead`, `mutations/Operand020PipeRead`; aggregate proof +`operand020-stack-pipe-proof.json`, SHA-256 `2d6b57c4a244c8efea96902886e2889d10b2e5c586b08351dc631da706d7b432`. Its six entries pin +all case and input-integrity verifiers; it also pins the ten corruption controls. +The earlier pipe fixture's assumption of an initially non-empty pipe, a tuple +compile error and incorrect scalar cache priming remain separate failed +fixture evidence. None is relabeled as a production CPU defect or passing run. + +No production CPU source, timing policy, package or regression retirement changes. +The isolated prototype remains unimported and unpromoted. Broader register/index +aliases, extension-bearing and multiword captured pipes, nested-PC provenance, +other read origins, trace/interrupt and changed return-SR/stack combinations, +wider fault transport, memory destinations and other-family continuation remain +required alongside all existing reference/consolidation gaps. Milestone 6 stays +**in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 45a5b699..60fb9aa0 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2822,3 +2822,18 @@ trace/interrupt, changed return-SR/stack and wider transport qualification. Broader continuation, reference gaps and consolidation remain required. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [typed trace-vector continuation](COPPER68K_REFERENCE_QUALIFICATION.md#typed-trace-vector-read-continuation--2026-10-07). + +The unchanged isolated typed-read source now passes 196,608 A7/MOVEA programs +(21,504 controls; 175,104 repair/software-input/refault programs) and 86,016 +captured private-pipe programs. A7 byte stride and aliased MOVEA ordering mutations +detect exactly 3,072 and 18,432 failures, preserving unaffected cases. A real +prefetch-handler/RTE sequence supplies the pipe words; cold and explicitly +batch-warmed dispatch respect saved/software-edited words. Pipe-loss and +edited-word mutations detect 86,016 and 43,008 failures respectively. Complete +source/producer/input inventories and ten deliberate integrity controls pass. +This remains unimported and unpromoted. Wider aliases/indexing, extension-bearing +and multiword pipes, other read origins, nested-PC provenance, trace/interrupt, +changed return-SR/stack, transport and general continuation stay required. +No CPU source/package change or regression retirement is included; milestone 6 +stays **in progress**, `roadmapComplete=false`. See the +[A7 and private-pipe qualification](COPPER68K_REFERENCE_QUALIFICATION.md#a7movea-and-captured-private-pipe-qualification--2026-10-07). From eeb0c4dbedb6defa541c507b664e95c0c60ac4f6 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 07:49:04 +0300 Subject: [PATCH 090/211] Record PC-relative operand function-code qualification --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 61 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 16 +++++ 2 files changed, 77 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index bb7f1440..de8afdfd 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8228,3 +8228,64 @@ other read origins, trace/interrupt and changed return-SR/stack combinations, wider fault transport, memory destinations and other-family continuation remain required alongside all existing reference/consolidation gaps. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +#### PC-relative operand function codes — 2026-10-07 + +The next literal fixture demonstrates a function-code defect in the isolated +read prototype. [M68000PM section 2](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) +and [MC68030UM-P1 section 2/Table 4-1](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P1.pdf) +classify PC-relative operands as program references: FC 2 in user mode and +FC 6 in supervisor mode. Part 1 section 6.1.2 nevertheless places their operand +data in the data cache, and section 4.3 places non-reset exception vectors in +supervisor data space. Program function codes therefore do not justify changing +the existing operand bus access kind to instruction fetch. + +The frozen discovery passes **9,216 fault-free controls** and retains **64,512 +precise SSW mismatches**: source faults incorrectly carry FC 1/5 instead of 2/6. +The fixtures use independently selected positive, negative and unaligned operand +locations, literal displacements, byte/word/long MOVE, every CCR and fault byte +lane, four stack states, EC020/A1200/020/030 and scalar/batch dispatch with a +host code-reader adapter. These discovery failures remain failed evidence. + +The isolated correction captures the function code for the typed pending read +and validates it on explicit RTE. A MOVE PC-relative source uses program space; +a trace-vector read uses supervisor data space even when the retained opcode +is PC-relative. Existing bus access kinds, operand evaluation, extension order, +partial effects and timing policy are unchanged. All **73,728 selected programs / +32 reports / eight executions** pass, including controls, different DIB input, +persistent refault and handlers that modify the source opcode/displacement. +Per-instruction architectural state, stacks, defined frame fields, surrounding +memory, exception/read counts and a following sentinel are checked. + +A separate PC-relative trace fixture passes **73,728 programs / 48 reports / +12 executions**, with and without an earlier source fault, including software +vector input, refault and changed VBR. It independently requires vector FC 5; +the operand's program-space classification cannot leak into this later read. +Discarding program-space capture produces exactly **64,512 SSW mismatches / +9,216 unchanged control passes**. Deriving the vector code from the stale opcode +produces exactly **73,728 mismatches**, each FC 6 where FC 5 is required. + +Fresh corrected-source retention passes **645,888 previously qualified programs**, +with **272 byte-identical reports** and matching actual TRX summaries, plus all +**25 mapping/cache/optional-interface controls** (**89 executions total**). +These include the previous handler, A7/MOVEA, captured-pipe, trace, RTE fault +and RTE transport gates. Strict verifiers check exact executions, independently +enumerated keys/CCR weights, precise mutation identifiers/reasons, all source +and producer/base-input identities and actual TRX summaries. Ten separate +corruption controls reject omitted fixtures, changed source, altered producers, +empty execution selections and wrong weights. + +Evidence: `audits/Operand020PcRelativeDiscovery`, `audits/Operand020PcRelativeFix`, +`audits/Operand020PcRelativeTrace`, `audits/Operand020PcRelativeRetained` and +`mutations/Operand020PcRelative`. Aggregate proof `operand020-pcrel-proof.json`, +SHA-256 `c19704ef4687311f61f14aa19e204e93509303453be7a26cf8f28edc8a40f527`, pins all six case/input-integrity proofs and corruption +controls. It confirms that the correction changes only the private operand +continuation CPU file, with unchanged witness fixtures; each mutation likewise +changes only that file. The production CPU source and public API remain unchanged. + +This correction remains isolated and unpromoted. Indexed PC-relative/pointer +stages, other integer-family address-space handling, wider fault transport, +more pipe/alias combinations, nested-PC provenance, trace/interrupt and changed +return-SR/stack behavior remain required with all earlier reference/consolidation +gaps. No package is built or published. Milestone 6 remains **in progress**, +`roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 60fb9aa0..a1a86c4d 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2837,3 +2837,19 @@ changed return-SR/stack, transport and general continuation stay required. No CPU source/package change or regression retirement is included; milestone 6 stays **in progress**, `roadmapComplete=false`. See the [A7 and private-pipe qualification](COPPER68K_REFERENCE_QUALIFICATION.md#a7movea-and-captured-private-pipe-qualification--2026-10-07). + +PC-relative function-code qualification reproduces 64,512 precise SSW failures +with 9,216 fault-free controls in the isolated prototype. A typed-read correction +now passes all 73,728 selected operand/control programs and another 73,728 +PC-relative trace-vector programs. Program-space operand codes remain separate +from supervisor data-space vector codes, without changing bus access kinds or +timing policy. Two mutations detect exactly 64,512 and 73,728 failures. Fresh +corrected-source retention passes 645,888 programs with 272 byte-identical +reports, plus 25 map/cache controls (89 executions total); ten integrity controls +and complete source/producer/input inventories pass. The correction remains +isolated and unpromoted. Indexed/pointer stages, other-family function codes, +wider transport, pipeline/aliases, nested-PC provenance, trace/interrupt, +changed return-SR/stack and all remaining reference/consolidation work stay +required. No package is built or published; milestone 6 stays **in progress**, +`roadmapComplete=false`. See the +[PC-relative function-code correction](COPPER68K_REFERENCE_QUALIFICATION.md#pc-relative-operand-function-codes--2026-10-07). From 9137d0982e850c26d57e648aba5458592d27d5ed Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 08:14:31 +0300 Subject: [PATCH 091/211] Record indexed MOVE read-stage qualification --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 69 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 18 +++++ 2 files changed, 87 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index de8afdfd..310ad543 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8289,3 +8289,72 @@ more pipe/alias combinations, nested-PC provenance, trace/interrupt and changed return-SR/stack behavior remain required with all earlier reference/consolidation gaps. No package is built or published. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +#### Indexed MOVE source and pointer stages — 2026-10-07 + +The next frozen discovery passes **11,520 fault-free literal controls** and +retains **105,984 PC-indexed SSW mismatches / 105,984 unsupported An-indexed +RTE continuations**. Its 18 independent witnesses cover brief signed/scaled +word and long indexes, an aliased address-register index, full null/word/long +base displacements, base/index suppression, unaligned and high/negative +addresses, and pre/post memory indirection with null/word/long outer +displacements. An and PC sources, MOVE B/W/L to an aliased index register, +MOVEA W/L to the source base, four stack states, CCR 00/1F, every rejected byte +lane, EC020/A1200/020/030 and scalar/batch routes are selected. This is a bounded +extension/stage matrix; it does not claim every full-format structural or +register combination. The earlier all-CCR gates remain separately retained. + +An isolated correction explicitly distinguishes the indirect-pointer read +from the final operand read. It serializes the consumed extension, selected +timing policy and already-evaluated outer/post-index component in versioned +interpreter-private context. RTE performs only the suspended read and its +remaining suffix. A pointer DIB supplies a different pointer and therefore +selects a different operand address; an operand DIB supplies a different value +without rereading memory. Persistent faults remain observable. Real handlers +alter base/index registers and consumed instruction words, and change already +read pointers. Recovery preserves the saved calculation and never retries the +instruction or repeats completed pointer reads. + +[M68000PM 2.2.7–2.2.15](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) +provides the addressing rules. [MC68030UM-P1 section 2/Table 4-1](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P1.pdf) +provides program-space classification for PC-relative references, including +suppressed-PC forms. [Part 2 section 8.2.2](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P2.pdf) +defines DF/DIB software completion. Operand bus access kinds remain unchanged; +the private context is not a claim about physical pipeline/cache contents. + +All **223,488 selected programs / 40 reports / ten executions** pass, with +independent architectural register/SR/stack/memory/frame expectations, exact +next PC, ordered pointer/operand read widths and counts, exception counts and +a following sentinel. A separate **223,488-program** policy gate checks literal +retained timing keys/cycle policies and full-indexed 030 head/tail shapes after +both ordinary and recovered execution. These are existing approximate policies, +not physical timing qualification. Fresh retention passes **793,344 previously +qualified programs / 352 byte-identical reports / 109 executions**, including +all **25 mapping/cache/optional-interface controls**. + +Discarding the saved pointer offset produces exactly **81,920 mismatches / +141,568 unaffected passes**. Repeating the recovered operand read produces +exactly **89,856 mismatches / 133,632 unaffected passes**. Replacing the captured +indexed timing policy with the general MOVE policy produces exactly **211,968 +policy mismatches / 11,520 unchanged control passes**. Each mutation changes +only the private continuation CPU file; its fixtures are unchanged. Strict +verifiers independently enumerate all keys/weights and precise failure +identifiers/reasons, match actual TRX execution summaries, and verify complete +source and producer/base-input inventories. Ten deliberate corruption controls +reject omitted/changed fixtures, wrong producers, empty executions and wrong +weights for the read and policy gates. + +Evidence: `audits/Operand020IndexedDiscovery`, `audits/Operand020IndexedFixV2`, +`audits/Operand020IndexedPolicy`, `audits/Operand020IndexedRetained`, +`mutations/Operand020Indexed`; aggregate proof `operand020-indexed-proof.json`, +SHA-256 `55c6d6caccb956d708a48d78f3c2f78cb4e4169329f0d904bbae4026db771d99`. The first correction snapshot's missing +narrowing casts remain a distinct failed compile, not passing qualification. +The accepted correction changes three isolated CPU files; production source, +public API, packages and regression retirement remain unchanged. + +The prototype stays isolated and unpromoted. All-structural indexed/address +alias coverage, chained pointer/operand faults, extension-bearing/multiword +captured pipes, nested-PC provenance, other read origins, trace/interrupt, +changed return-SR/stack, wider transport, memory destinations and other-family +continuation remain required with the earlier reference/consolidation gaps. +Milestone 6 stays **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index a1a86c4d..d5f5b943 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2853,3 +2853,21 @@ changed return-SR/stack and all remaining reference/consolidation work stay required. No package is built or published; milestone 6 stays **in progress**, `roadmapComplete=false`. See the [PC-relative function-code correction](COPPER68K_REFERENCE_QUALIFICATION.md#pc-relative-operand-function-codes--2026-10-07). + +Indexed source/pointer qualification now distinguishes the suspended read stage +and preserves consumed extensions, evaluated index/outer components and the +selected timing policy. Eighteen literal witnesses expose 105,984 PC-indexed +SSW failures and 105,984 unsupported An-indexed RTE continuations, with 11,520 +controls. The isolated correction passes all 223,488 selected programs and a +separate 223,488-program retained timing-policy gate. Fresh retention passes +793,344 prior programs with 352 byte-identical reports, plus all 25 map/cache +controls (109 executions total). Saved-offset, repeated-read and timing-policy +mutations detect exactly 81,920, 89,856 and 211,968 intended failures, preserving +unaffected cohorts. Complete source/producer inventories and ten deliberate +integrity controls pass. Production CPU source, packages and regression +retirement remain unchanged. The prototype remains isolated and unpromoted; +all-structural indexed/alias and chained-fault coverage, wider pipe/provenance, +transport, other read origins, trace/interrupt, changed return-SR/stack and +broader continuation/reference/consolidation work stay required. Milestone 6 +stays **in progress**, `roadmapComplete=false`. See the +[indexed source/pointer qualification](COPPER68K_REFERENCE_QUALIFICATION.md#indexed-move-source-and-pointer-stages--2026-10-07). From e81ddd5f06d77c71a0546686b7a94ecc7c14f569 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 08:36:38 +0300 Subject: [PATCH 092/211] Record chained indexed read-fault qualification --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 61 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 18 ++++++ 2 files changed, 79 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 310ad543..71932994 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8358,3 +8358,64 @@ captured pipes, nested-PC provenance, other read origins, trace/interrupt, changed return-SR/stack, wider transport, memory destinations and other-family continuation remain required with the earlier reference/consolidation gaps. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +#### Chained indexed pointer and operand faults — 2026-10-07 + +The unchanged isolated indexed prototype now passes **365,184 programs / 56 +reports / 14 executions**: **5,760 controls** and **359,424 chained-fault +programs**. Nine literal full-format witnesses select pre/post indirection, +null/word/long displacements and suppression, An/PC sources, MOVE B/W/L and +MOVEA W/L, four stack states, CCR 00/1F, every pointer/operand fault byte lane, +EC020/A1200/020/030 and scalar/batch dispatch. Both faults are armed before +execution; guest state changes occur through real handler instructions. + +The first handler redirects the bus-error vector to a separate second handler. +Its RTE completes the pointer stage and then faults on the operand. Each frame +must retain the original resumed MOVE's PC/SR, consumed instruction context, +correct access address/width/function code and stack bank. The second frame +must identify the operand stage, without restoring a stale pointer stage. +Checks cover different pointer DIB input (and therefore a different operand +address), different operand DIB input, both inputs together, an additional +persistent operand refault, and handlers changing base/index registers, +consumed opcode/extensions and an already-read pointer. Architectural state, +all stacks, surrounding memory, exact next PC, ordered rejected/completed +access widths/counts, exception counts and a following sentinel are checked. +Literal retained full-indexed timing policies and 030 head/tail shapes also pass. + +[MC68030UM-P2 sections 8.1.2 and 8.2.2–8.2.3](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P2.pdf) +provide bus-error status capture and DF/DIB/RTE completion rules. The saved +stage and resumed-instruction provenance checks qualify the interpreter's +serialized continuation model. They do not qualify physical overlapped +instruction/pipeline state or all exception-PC origins. + +Three isolated CPU mutations detect exactly **359,424 stale-stage failures**, +**359,424 RTE-PC provenance failures**, and **239,616 repeated-pointer-read +failures**, with **5,760**, **5,760**, and **125,568** unaffected passes respectively. +Each changes only the private operand-continuation file; witness fixtures are +byte-identical to their accepted parent. Strict verifiers independently +enumerate the full selection and weights, verify every precise mutation +identifier/reason, match actual TRX summaries and pin complete source, +producer and base-input identities. Five deliberate corruption controls reject +omitted/changed fixtures, wrong producers, empty executions and wrong weights. + +All CPU/project and preexisting fixture sources remain byte-identical to the +previous indexed correction. Its accepted retention evidence (**793,344 +programs / 352 reports / 109 executions**, including 25 map/cache controls) +is reused after verifying unchanged CPU identities; no fresh retention replay +is claimed. The first fixture placed its longest alias handler over the second +handler: **26,624 failures** remain distinct invalid-fixture evidence, with +338,560 other cases passing. A fresh fixture moves the second handler to a +disjoint region and checks their extents. The repair changes only that fixture; +no CPU defect is inferred from the overlap. + +Evidence: `audits/Operand020IndexedChain`, `audits/Operand020IndexedChainV2`, +`mutations/Operand020IndexedChain`; aggregate `operand020-indexed-chain-proof.json`, +SHA-256 `7531f558fe7391aa96b0c69fd557105fb6e740d7b64841172ac4f51bacf3825a`. It links all four selected/input proofs, +three mutations, five corruption controls, invalid-fixture repair and the +previous unchanged-source retention proof. Production source, public API, +packages and regression retirement remain unchanged. The prototype remains +isolated and unpromoted. All-structural indexed/register aliases, broader +chained faults, captured multiword pipes, other exception-PC origins, +trace/interrupt, changed return-SR/stack, wider transport, memory destinations, +other families and all existing reference/consolidation gaps remain required. +Milestone 6 stays **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index d5f5b943..78bdc73d 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2871,3 +2871,21 @@ transport, other read origins, trace/interrupt, changed return-SR/stack and broader continuation/reference/consolidation work stay required. Milestone 6 stays **in progress**, `roadmapComplete=false`. See the [indexed source/pointer qualification](COPPER68K_REFERENCE_QUALIFICATION.md#indexed-move-source-and-pointer-stages--2026-10-07). + +The unchanged indexed prototype now passes 365,184 chained pointer/operand +programs (5,760 controls; 359,424 fault programs). Real handlers redirect the +second fault, supply different pointer/operand input, refault and alter +registers, consumed code and completed pointers. Stage advance, original +resumed-instruction PC/SR, ordered accesses and retained timing policies pass. +Three mutations detect exactly 359,424 stale-stage failures, 359,424 wrong-PC +failures and 239,616 repeated-pointer failures, preserving unaffected cases. +Complete source/producer inventories and five corruption controls pass. The +first fixture's 26,624 overlap failures remain invalid evidence; only the +fixture changes in its repair. Existing retained evidence is reused after +verifying unchanged CPU sources, with no new retention replay claimed. +Production source/packages remain unchanged and the prototype stays isolated +and unpromoted. All-structural indexed/register and wider chained/pipe, +provenance/transport, trace/interrupt, return-SR/stack, other-family and +remaining reference/consolidation work stay required. Milestone 6 remains +**in progress**, `roadmapComplete=false`. See the +[chained indexed fault qualification](COPPER68K_REFERENCE_QUALIFICATION.md#chained-indexed-pointer-and-operand-faults--2026-10-07). From 6a5e47806478ffdcb1242b2d49e8fb63ebb06aad Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 08:51:49 +0300 Subject: [PATCH 093/211] Record full-indexed read-structure qualification --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 53 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 ++++++ 2 files changed, 68 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 71932994..6b24ebd1 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8419,3 +8419,56 @@ chained faults, captured multiword pipes, other exception-PC origins, trace/interrupt, changed return-SR/stack, wider transport, memory destinations, other families and all existing reference/consolidation gaps remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +#### All legal full-indexed read structures — 2026-10-07 + +The unchanged isolated CPU prototype passes **786,432 logical programs / 32 +reports / nine executions**, with zero mismatching, unsupported or untested +cases in this selection. The ninth execution checks **20 fixed An/PC fixture +examples**. Shared test-internal `IndexFixture.FullStructures()` and +`AddressingFixture` prepare all **66 legal full-format structures**: both +suppression bits, null/word/long base displacements, direct addressing and +all legal pre/post-indirect outer-displacement forms. Reserved combinations +remain excluded according to [M68000PM table 2-2](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf). + +The separate deterministic matrix crosses An/PC bases, MOVE byte/word/long, +MOVEA word/long, four 020/030 models/profiles, scalar/batch routes, four stack +states, CCR 00/1F, and every byte lane of pointer/operand reads. It includes +**42,240 fault-free controls** and **744,192 recovery programs** with repair, +data-input-buffer and persistent refault handlers. Index D7.W=-8 and +register destinations D2/A0 are canonical; this does not qualify all register, +index-width, scale or alias combinations. The generator never calls production +EA helpers. Fixed literal encodings and addresses anchor the shared fixture. + +Legal operands/pointers overlapping instruction extensions, including +suppressed PC bases and address zero, stay in the matrix. Bytewise setup +preserves code, expectations capture its real operand/pointer bytes, and the +evaluated outer/index offset is frozen before handlers execute. Checks cover +architectural registers, defined SR, PC, stack selection, memory guards, +exception delivery, ordered read widths/counts and a following sentinel. +This matrix does not add physical timing qualification or an all-structure +timing-policy gate; the preceding literal timing-policy evidence is retained. + +A sole CPU mutation duplicates resumed **word** operand reads. With all +fixtures unchanged it causes exactly **67,584 identified mismatches**, each +for pointer/operand access ordering or count, while **718,848 unaffected +programs pass**. Five deliberate integrity controls reject omitted/changed +fixtures, wrong producer identity, empty execution selection and incorrect +combination weights. Independent verifiers enumerate the complete source +inventory and architectural keys, check exact logical weights against actual +TRX output, and match every intended mutation failure identifier and reason. + +Evidence: `audits/Operand020IndexedStructures`, +`mutations/Operand020IndexedStructures/RepeatWordRead`; aggregate +`operand020-indexed-structures-proof.json`, SHA-256 +`e62a7da9ca29637cc5ce46ad943784051a28cebdc113c5eab7fd81bcfe9f353f`. +It pins selected inputs/TRX, verifier identities, the mutation, five integrity +controls, and the preceding chained proof. All CPU/project and preexisting +fixture sources remain byte-identical; accepted retention is reused, with no +fresh retention replay claimed. Protected normal assemblies remain unchanged. +No production import, API/package change, publication or regression retirement +is included. Register/index/alias permutations, all-structure chained faults, +wider captured pipes and provenance/transport, other read origins, +trace/interrupt, return-SR/stack, memory destinations, other families and +remaining reference/consolidation gates stay required. Milestone 6 remains +**in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 78bdc73d..cae106e8 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2889,3 +2889,18 @@ provenance/transport, trace/interrupt, return-SR/stack, other-family and remaining reference/consolidation work stay required. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [chained indexed fault qualification](COPPER68K_REFERENCE_QUALIFICATION.md#chained-indexed-pointer-and-operand-faults--2026-10-07). + +All 66 legal full-indexed structures now pass a separate 786,432-program +read-recovery matrix on the unchanged isolated CPU prototype. Twenty fixed +An/PC examples anchor the shared addressing fixtures; self-code operands, +suppression, displacement lengths, indirection, stack states, CCR 00/1F and +all fault lanes remain represented. A repeated-word-read mutation detects +exactly 67,584 intended failures while preserving 718,848 unaffected programs; +complete source/key/weight checks and five integrity controls pass. Canonical +register/index selections do not replace the remaining permutation/alias +matrix. Existing retention/timing evidence is reused, without claiming a fresh +replay or all-structure timing gate. The prototype remains unimported and +unpromoted, with no package change or test retirement. Register/index aliases, +all-structure chained faults and broader continuation/reference/consolidation +gates stay required. Milestone 6 remains **in progress**, `roadmapComplete=false`. +See the [all-structure read qualification](COPPER68K_REFERENCE_QUALIFICATION.md#all-legal-full-indexed-read-structures--2026-10-07). From e7e10b23e8490d1290c29852c7627d8f9361de2b Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 09:08:05 +0300 Subject: [PATCH 094/211] Record indexed register and alias qualification --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 67 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 16 +++++ 2 files changed, 83 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 6b24ebd1..df4e15f1 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8472,3 +8472,70 @@ wider captured pipes and provenance/transport, other read origins, trace/interrupt, return-SR/stack, memory destinations, other families and remaining reference/consolidation gates stay required. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +#### Indexed register, index-size, scale and alias groups — 2026-10-07 + +The unchanged isolated prototype passes **768,000 programs / 256 reports / 65 +executions** with zero mismatching, unsupported or untested cases in this +selection. Six fixed opcode/extension examples validate the shared +`MoveSpecification`/`IndexFixture` encodings. Two separate deterministic groups +complement the preceding 66-structure matrix: + +- **460,800 register programs:** all eight An bases plus PC, all eight + destinations, all eight D/A indexes, at canonical word-index/scale-one + settings. This includes source/destination/index register aliases. +- **307,200 index programs:** word/long indexes and scales 1/2/4/8 across + distinct, base/destination, destination/index, base/index and all-register + aliases, plus PC-relative destination/index aliases, rotated through all + register numbers. + +Both groups cover brief addressing, full direct, full preindexed and full +postindexed canonical structures, MOVE byte/word/long and MOVEA word/long, +four 020/030 models/profiles and scalar/batch routes. The selected state is ISP, +CCR 1F, with one canonical fault lane; other stack/CCR/read-lane combinations +remain covered by their preceding matrices. There are **307,200 controls** and +**460,800 real alias-handler programs**. D-index input `1000FFF8` distinguishes +negative word indexes from high long indexes; An inputs retain the fixture's +positive address/stack values. Negative address-index values and wider numeric +boundaries remain a separate required group. + +Real handlers change participating D and A0-A6 registers, rewrite consumed +opcode/extensions, and change already-completed pointers. They preserve A7 +while it addresses the active handler frame. A7 source/index cases and MOVEA +A7 destinations remain included; architectural checks verify ISP changes along +with the resulting A7 value. Expected EAs/index offsets are established before +CPU execution without production helpers. A resumed source reads current +expected memory after real handler writes, including a self-code operand. +All registers, defined flags, PC, stacks, memory guards, exception fields, +ordered read widths/counts and the following sentinel are checked. + +A sole CPU mutation drops the saved outer/post-index offset during pointer +recovery. Every one of the **153,600 intended failures** has its exact +identifier and register/flag mismatch verified; **614,400 unaffected programs +pass**. Five corruption controls reject omitted/changed fixtures, wrong +producers, empty executed selections and wrong weights. Independent source, +execution-roster, architectural-key and logical-weight checks pass. + +The first two snapshots remain distinct invalid-fixture evidence: the first +has **744,656 passes / 23,344 expectation failures** from self-code data changes +and stale ISP expectations; the second has **744,960 passes / 23,040 stale-ISP +expectation failures**. Their complete failure identifiers/reasons are audited. +Fresh repairs modify only the fixture, never the CPU or an executed snapshot. +No CPU defect is inferred from those failures. + +Evidence: `audits/Operand020IndexedRegisters`, +`audits/Operand020IndexedRegistersV2`, `audits/Operand020IndexedRegistersV3`, +`mutations/Operand020IndexedRegisters/DiscardOffset`; aggregate +`operand020-indexed-registers-proof.json`, SHA-256 +`ba6093c39caf7695c6933f7c3de537304c3fbf781b5a2d086b26b4c07ca04fd5`. +It pins all four selected executions, complete 217-file source inventories, +verifier/binary identities, fixture-only repairs, the sole CPU mutation, five +corruption controls and the previous unchanged-source proof. Existing +source-specific retention/timing evidence is reused; no fresh retention or +all-structure timing gate is claimed. Protected normal assemblies, production +source, API/packages and regression retirement remain unchanged; the prototype +stays isolated and unpromoted. Signed address-index/value boundaries, +all-structure chained faults, broader pipes/provenance/transport, other read +origins, trace/interrupt, return-SR/stack, memory destinations, other families +and existing reference/consolidation work remain required. Milestone 6 stays +**in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index cae106e8..2d2b6d60 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2904,3 +2904,19 @@ unpromoted, with no package change or test retirement. Register/index aliases, all-structure chained faults and broader continuation/reference/consolidation gates stay required. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [all-structure read qualification](COPPER68K_REFERENCE_QUALIFICATION.md#all-legal-full-indexed-read-structures--2026-10-07). + +Separate indexed register and index-size/scale/alias groups now pass 768,000 +programs on the unchanged isolated prototype. All An/PC base, destination and +D/A-index register selections use canonical index settings; rotated alias +layouts additionally cover word/long indexes and all four scales. A7 sources, +indexes and MOVEA destinations remain included, with ISP updates verified. +Real handlers alter participating registers, consumed code and completed +pointers. A saved-offset mutation detects exactly 153,600 intended failures +while preserving 614,400 unaffected programs; six fixed encoding examples, +complete source/key/weight/TRX checks and five corruption controls pass. Two +invalid-fixture snapshots remain audited separately; their repairs change only +the test file. Negative address-index/value boundaries, all-structure chained +faults and broader continuation/reference/consolidation remain required. The +prototype stays unimported and unpromoted, with no package change or test +retirement; milestone 6 stays **in progress**, `roadmapComplete=false`. +See the [indexed register qualification](COPPER68K_REFERENCE_QUALIFICATION.md#indexed-register-index-size-scale-and-alias-groups--2026-10-07). From 16f0940774e4fddf9d8307c8f6db4b83ff6aded7 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 09:26:40 +0300 Subject: [PATCH 095/211] Record signed indexed read-boundary qualification --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 67 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 16 +++++ 2 files changed, 83 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index df4e15f1..8a711986 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8539,3 +8539,70 @@ all-structure chained faults, broader pipes/provenance/transport, other read origins, trace/interrupt, return-SR/stack, memory destinations, other families and existing reference/consolidation work remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +#### Signed index boundaries and user-stack A7 — 2026-10-07 + +The unchanged isolated prototype passes **1,638,400 programs / 1,024 reports / +257 executions**, with zero mismatching, unsupported or untested cases in the +selected matrix. **64 fixed address examples** independently anchor D/A word +and long indexes at scales one/eight; execution covers all four scales. Word +sign extension and scale encodings follow [M68000PM table 2-1](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf). +The eight fixed raw register values are `80000000`, `80007FFF`, `80008000`, +`8000FFFF`, `7FFFFFFF`, `FFFFFFFF`, `1000FFF8` and zero. They distinguish low-word +sign boundaries from complete long values and exercise scaling/32-bit wrapping. + +The separate deterministic group rotates destination/index, base/index, +all-register and PC-relative aliases through every D/A index register. It +crosses word/long indexes, scales 1/2/4/8, brief/full-direct/pre/post canonical +structures, MOVE byte/word/long and MOVEA word/long, four 020/030 models/profiles +and both execution routes. There are **655,360 controls** and **983,040 real +alias-handler programs**, at user CCR 1F and a canonical fault lane. Other +stack/CCR/lane combinations retain their preceding evidence; this is not an +all-structure/value Cartesian product. + +A7 address-index inputs use the selected raw value as the user stack pointer, +including zero, high/negative and odd values, with a separate supervisor handler +frame. MOVEA A7 updates USP and the architectural register together. Handlers +alter participating D and A0-A6 registers, consumed opcode/extensions and +completed pointers while preserving their active frame pointer. Operand/pointer +access spans are asserted disjoint from handler and exception-frame regions; +setup protects code through the model's physical address mapping. Expected +addresses and evaluated offsets use the shared test fixture, never production +EA helpers. Architectural state, logical fault addresses/function codes, +ordered read widths/counts, memory guards and the following sentinel pass. + +A full preindexed PC case deliberately reads its own extension words as a +pointer. Recovery reads the real pointer bytes after handler writes at the +**saved pointer address**, retaining the already-evaluated outer/index offset. +Final-operand recovery similarly retains its saved EA when the handler changes +a completed pointer. The first snapshot incorrectly expected an unchanged +pointer value: **1,637,760 other programs pass / 640 fixture-expectation failures** +remain distinct audited invalid evidence. A fresh repair changes only that +fixture, not CPU source or an executed snapshot. + +A sole CPU mutation discards the saved offset at pointer recovery. Exact +identifiers/reasons verify all **327,680 intended failures**, with **1,310,720 +unaffected passes**. Of those failures, **640** still produce the same zero +register/flag result and are detected by the access-order/count checks; the +other **327,040** produce the expected register/flag mismatches. Five deliberate +integrity controls reject omitted/changed fixtures, wrong producer identities, +empty executed selections and incorrect weights. Complete source inventories, +actual test rosters/TRX summaries and independently enumerated architectural +keys/logical weights pass. + +Evidence: `audits/Operand020IndexedBoundaries`, +`audits/Operand020IndexedBoundariesV2`, +`mutations/Operand020IndexedBoundaries/DiscardOffset`; aggregate +`operand020-indexed-boundaries-proof.json`, SHA-256 +`9252f8f0c37250b07224ad2e2ef90defedd19e238fbb4faba77c293acad982a7`. +It pins three complete selected executions, 218-file source inventories, +verifier/binary identities, fixture-only repair, the sole CPU mutation, five +corruption controls and the preceding unchanged-source proof. Accepted +source-specific retention/timing evidence is reused; no fresh retention replay +or physical/all-structure timing qualification is claimed. Protected normal +assemblies, production CPU source, API/packages and regression retirement +remain unchanged. The prototype stays isolated and unpromoted. All-structure +chained faults, broader captured pipes/provenance/transport, other read origins, +trace/interrupt, changed return-SR/stack, memory destinations, other families +and remaining reference/consolidation work stay required. Milestone 6 remains +**in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 2d2b6d60..33af5638 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2920,3 +2920,19 @@ faults and broader continuation/reference/consolidation remain required. The prototype stays unimported and unpromoted, with no package change or test retirement; milestone 6 stays **in progress**, `roadmapComplete=false`. See the [indexed register qualification](COPPER68K_REFERENCE_QUALIFICATION.md#indexed-register-index-size-scale-and-alias-groups--2026-10-07). + +Signed D/A-index boundary qualification now passes 1,638,400 programs on the +unchanged isolated prototype. Eight fixed raw values, all register numbers, +word/long indexes, all scales and four An/PC alias layouts complement the prior +structure/register matrices. A7 uses user-stack values with a separate handler +frame; MOVEA A7 updates USP and its register expectation together. Sixty-four +fixed address examples anchor the shared fixture. A suspended self-code pointer +reads handler-updated bytes at its saved pointer address with its saved offset; +a fixture-only repair preserves 640 earlier expectation failures as invalid +evidence. The offset mutation detects exactly 327,680 intended failures, +including 640 same-value cases exposed by access checks, while 1,310,720 cases +remain passing. Complete source/key/weight/TRX audits and five integrity controls +pass. No production import, package change or regression retirement occurs. +All-structure chained faults and wider continuation/reference/consolidation +remain required; milestone 6 stays **in progress**, `roadmapComplete=false`. +See the [signed index boundary qualification](COPPER68K_REFERENCE_QUALIFICATION.md#signed-index-boundaries-and-user-stack-a7--2026-10-07). From 9cd8a646e5698bbfb99d65d9ab62edabaf270db1 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 10:04:34 +0300 Subject: [PATCH 096/211] Record all-structure chained indexed fault qualification --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 62 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 +++++ 2 files changed, 77 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 8a711986..4018dce7 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8606,3 +8606,65 @@ chained faults, broader captured pipes/provenance/transport, other read origins, trace/interrupt, changed return-SR/stack, memory destinations, other families and remaining reference/consolidation work stay required. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +#### All-structure chained indexed faults — 2026-10-07 + +The unchanged isolated prototype passes **2,198,784 programs / 336 reports / +85 executions**, with zero mismatching, unsupported or untested cases in the +selected matrix. All **66 legal full-format structures** retain **42,240 +controls**. The **54 pointer-bearing structures** execute **2,156,544 chained +programs**; the **12 direct structures** have no pointer access and are +explicitly excluded from chains with that architectural reason. Four fixed +encoding/pointer examples anchor the shared fixture. + +The matrix crosses An/PC, MOVE byte/word/long and MOVEA word/long, four stack +states, CCR 00/1F, every pointer and operand fault lane, scalar/batch execution +and EC020/A1200/020/030 profiles. Canonical D2.W is -8; previous register, +size/scale and signed-boundary groups retain their separate evidence. Groups +partition displacement length and base suppression to preserve every failure +identifier without report truncation. + +Real handlers repair accesses, supply pointer/operand/both input buffers, +refault, redirect the second exception vector and alter registers, consumed +code and completed pointers. Checks cover original logical instruction PC/SR, +private stage advance, saved addresses/function codes, architectural registers +and stacks, ordered access widths/counts, memory guards and the following +sentinel. Full-index native cycles, head/tail and barrier expectations pass +across selected structures. These are the existing approximate timing policy, +not physical pipeline/bus qualification; private frame fields are not claims +about silicon frame contents. + +The compile-only initial snapshot has a constant-to-ushort narrowing error +and no executed coverage. Its fresh fixture-only repair masks the low vector +address word. The next snapshot has **2,192,128 passes / 6,656 invalid-fixture +failures**: a completed-pointer write overlapping PC extension storage replaces +the following sentinel with BRA.W, which the CPU correctly executes. A fresh +fixture-only repair changes the pointer while preserving that sentinel's low +word. Neither invalid snapshot counts toward the passing gate; no CPU bug is +inferred from either failure. + +A sole CPU mutation leaves recovery at the pointer stage instead of advancing +to the operand stage. Against the repaired fixture it produces exactly +**2,156,544 identified stage mismatches**, preserving **42,240 controls**. +An earlier mutation against the invalid-sentinel fixture remains a separate +prefix probe. Complete 219-file source inventories, actual TRX rosters, +independently enumerated keys/weights and five corruption controls pass. The +controls reject omitted/changed fixtures, wrong producer identity, empty +execution selections and incorrect weights. + +Evidence: `audits/Operand020IndexedFullChain`, +`audits/Operand020IndexedFullChainV2`, `audits/Operand020IndexedFullChainV3`, +`mutations/Operand020IndexedFullChain/KeepPointerStage` and +`mutations/Operand020IndexedFullChain/KeepPointerStageV3`; aggregate +`operand020-indexed-full-chain-proof.json`, SHA-256 +`eb64a95e6b2ac90d073bb073f7446721b23d7d4662f9acd66001744b303cf52d`. +It pins complete inventories, producer/verifier/binary identities, exact +fixture-only repairs, the sole CPU mutation, integrity rejections and the +preceding signed-boundary proof. Existing source-specific retention is reused; +no fresh retention replay is claimed. Protected normal assemblies, production +source/packages and regression retirement remain unchanged. The prototype +stays isolated, unimported and unpromoted. Wider captured multiword/extension +pipes, provenance/transport, other read origins, trace/interrupt, changed +return-SR/stack, memory destinations, other families and remaining reference/ +consolidation gates stay required. Milestone 6 remains **in progress**, +`roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 33af5638..0b7ea672 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2936,3 +2936,18 @@ pass. No production import, package change or regression retirement occurs. All-structure chained faults and wider continuation/reference/consolidation remain required; milestone 6 stays **in progress**, `roadmapComplete=false`. See the [signed index boundary qualification](COPPER68K_REFERENCE_QUALIFICATION.md#signed-index-boundaries-and-user-stack-a7--2026-10-07). + +All-structure chained indexed faults now pass 2,198,784 programs on the unchanged +isolated prototype. All 66 full-format structures retain 42,240 controls; 54 +pointer-bearing structures execute 2,156,544 chained programs, with 12 direct +structures explicitly excluded from pointer chains. Real handlers cover repair, +input, refault and aliases; original PC/SR, ordered effects, private stage +advance, sentinel and existing timing policy pass. A mutation against the +repaired fixture detects exactly 2,156,544 intended failures while preserving +all controls. Complete source/key/weight/TRX audits and five integrity controls +pass. A compile-only snapshot and 6,656 earlier sentinel fixture failures remain +invalid evidence, with fixture-only repairs. No production import, package +change or regression retirement occurs. Wider pipes, provenance/transport and +remaining continuation/reference/consolidation gates remain required; +milestone 6 stays **in progress**, `roadmapComplete=false`. +See the [all-structure chained qualification](COPPER68K_REFERENCE_QUALIFICATION.md#all-structure-chained-indexed-faults--2026-10-07). From d59822fa47396507a274723be656aefe17135352 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 10:28:00 +0300 Subject: [PATCH 097/211] Record three-word private pipe qualification --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 57 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 16 ++++++ 2 files changed, 73 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 4018dce7..646a5207 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8668,3 +8668,60 @@ pipes, provenance/transport, other read origins, trace/interrupt, changed return-SR/stack, memory destinations, other families and remaining reference/ consolidation gates stay required. Milestone 6 remains **in progress**, `roadmapComplete=false`. + + +#### Three-word private pipe transport — 2026-10-07 + +The unchanged isolated prototype passes **1,003,520 programs / 160 reports / +40 executions** for a following three-word MOVE.L. Real handlers replace its +backing opcode and both immediate words, then supply a private saved pipe of +length **0, 1, 2 or 3**, with unchanged words or an edit at each retained +position. The expected instruction combines the supplied prefix with the +changed memory suffix; its destination, complete long value, flags, next PC +and following NOP distinguish each position and fetch boundary. MOVE encoding +and flag expectations use the test-internal specification and literal cases; +architectural semantics follow the [programmer reference's MOVE description](https://www.nxp.com/docs/en/reference-manual/M68000PRM.pdf). + +The matrix crosses indirect/postincrement/predecrement, displacement, +absolute-word/long and PC-displacement sources; byte/word/long operand reads; +all four stack states, all 32 initial CCR states, every read lane, scalar/batch +execution, cold/warm host-reader preparation and EC020/A1200/020/030 profiles. +An actual odd-prefetch exception and software RTE prepare the stream before +the operand fault. Originally captured entries are checked for address/content; +the handler-supplied pipe is checked through subsequent execution. Completed +operand effects, stack selection, original PC/SR, operand read count/width, +architectural registers and memory remain verified. This qualifies interpreter- +private software frame transport, not three-word silicon capture or physical +pipeline/cache timing. Wider indexed/chained pipes remain required. + +The first three-source-form matrix separately passes **430,080 programs**. +Its extended fixture incorrectly expected a zero extension-word write to clear +CCR.Z. The original extended snapshot retains **860,160 passing programs / +143,360 precisely identified invalid-fixture failures**; a fresh repair changes +only that expectation, preserving the failed snapshot and unchanged CPU source. +No production defect is inferred from that fixture error. + +Two sole CPU mutations corrupt the saved middle and final words during RTE. +The repaired fixture detects exactly **702,464 middle-word failures / 301,056 +unaffected passes** and **401,408 final-word failures / 602,112 unaffected +passes**. Every failure identifier and wrong-register-value reason is audited; +the mutations do not change fixtures. Complete 220-file source inventories, +actual TRX rosters/summaries, independently enumerated architectural keys and +weights, and five integrity controls pass. Those controls reject omitted or +changed fixtures, wrong producers, empty executions and incorrect weights. + +Evidence: `audits/Operand020MultiwordPipe`, +`audits/Operand020MultiwordPipeExtended`, +`audits/Operand020MultiwordPipeExtendedV2`, +`mutations/Operand020MultiwordPipeExtended/Middle` and +`mutations/Operand020MultiwordPipeExtended/Last`; aggregate +`operand020-multiword-pipe-proof.json`, SHA-256 `43f3034390925f06d8cd2eceefb18c74ee29187ee82ef3218a08493dc70b0a0f`. +The proof pins all five executions, sources, binaries, producer/verifier +identities, fixture-only repair, sole mutations, integrity rejections and the +preceding chained-structure proof. Existing source-specific retention is reused; +no fresh replay is claimed. Protected normal assemblies, production source, +API/packages and regression retirement remain unchanged. The prototype stays +isolated, unimported and unpromoted. Wider pipes, nested PC/SR provenance, +transport, other read origins, trace/interrupt, changed return-SR/stack, memory +destinations, other families and remaining reference/consolidation gates stay +required. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 0b7ea672..f42031b0 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2951,3 +2951,19 @@ change or regression retirement occurs. Wider pipes, provenance/transport and remaining continuation/reference/consolidation gates remain required; milestone 6 stays **in progress**, `roadmapComplete=false`. See the [all-structure chained qualification](COPPER68K_REFERENCE_QUALIFICATION.md#all-structure-chained-indexed-faults--2026-10-07). + + +Three-word private pipe transport now passes 1,003,520 programs across seven +source forms, operand sizes, stack states, all CCR/read lanes, scalar/batch and +cold/warm host-reader preparation. Real handlers supply pipe lengths 0..3 and +edit each retained position against different backing opcode/extensions; the +following MOVE.L and NOP verify values and fetch boundaries. Middle/final word +mutations detect exactly 702,464 / 401,408 intended failures while unaffected +cases pass. Complete source/key/weight/TRX audits and five integrity controls +pass. The earlier extended fixture's 143,360 zero-word CCR expectation failures +remain separate invalid evidence, with a fixture-only repair. This qualifies +private software frame transport; broader indexed/chained pipes, provenance, +transport and remaining continuation/reference/consolidation stay required. +No production import, package change or regression retirement occurs; +milestone 6 stays **in progress**, `roadmapComplete=false`. +See the [three-word pipe qualification](COPPER68K_REFERENCE_QUALIFICATION.md#three-word-private-pipe-transport--2026-10-07). From 795dc75cd670cd51bf41a94745a57d95576e140f Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 10:39:03 +0300 Subject: [PATCH 098/211] Record nested operand and trace-vector provenance --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 55 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 16 ++++++ 2 files changed, 71 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 646a5207..05f042f8 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8725,3 +8725,58 @@ isolated, unimported and unpromoted. Wider pipes, nested PC/SR provenance, transport, other read origins, trace/interrupt, changed return-SR/stack, memory destinations, other families and remaining reference/consolidation gates stay required. Milestone 6 remains **in progress**, `roadmapComplete=false`. + + +#### Nested operand and trace-vector provenance — 2026-10-07 + +The unchanged isolated prototype passes **516,096 programs / 336 reports / +84 executions** for direct traces and traces after operand-read recovery. +Seven MOVE source forms cover indirect/postincrement/predecrement, +displacement, absolute-word/long and PC displacement, byte/word/long sizes, +four stack states, all 32 initial CCR states, all four vector fault lanes, +scalar/batch execution and EC020/A1200/020/030 profiles. The operand fault uses +a canonical lane; earlier lane matrices retain their separate evidence. + +Real operand handlers replace consumed opcode/extensions with NOPs and install +the later vector handler. Trace-vector handlers supply different vector input, +refault or change VBR. Both initial and repeated vector faults retain the +original instruction PC/opcode, consumed next PC, long vector-read width and +typed pending-vector state. Trace frames preserve the post-MOVE SR and next +instruction PC, together with the serialized original-instruction address. +Register/stack/memory checks at each handler instruction, exact exception +counts, completed operand-read counts and the following sentinel pass. +[MC68030UM section 8.1.7](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P2.pdf) +defers tracing until a suspended instruction completes and saves its resulting +SR and next PC. The private nested vector-fault fields qualify the interpreter's +serialized policy; they do not establish physical pipeline-overlap provenance +or silicon-private frame contents. T0/interrupt and wider origins remain open. + +The first fixture left relocated bus-error/trace handlers' initial vectors at +the old addresses. Its **86,016 passes / 430,080 precisely identified invalid- +fixture failures** remain separate evidence. A fresh repair changes only the +fixture's two vector initializations; no CPU defect is inferred. + +A sole PC mutation loses the original instruction address after operand RTE: +**258,048 intended failures / 258,048 unaffected passes**. A sole SR mutation +discards completed MOVE flags: **241,920 intended failures / 274,176 unaffected +passes**. The latter preserves cases whose initial flags already equal the +MOVE result. Every failure identifier/reason is verified; both mutations leave +fixtures unchanged. Complete 221-file source inventories, exact TRX rosters, +independently enumerated keys/weights and five corruption controls pass. + +Evidence: `audits/Operand020TraceProvenance`, +`audits/Operand020TraceProvenanceV2`, +`mutations/Operand020TraceProvenance/PC` and +`mutations/Operand020TraceProvenance/SR`; aggregate +`operand020-trace-provenance-proof.json`, SHA-256 +`1bc7701a3525f6d181f011ada153a000e37c1fe87374e282852a4a7adb357d61`. +The proof pins four executions, complete source/producer/verifier/binary +identities, the fixture-only repair, sole mutations, integrity rejections and +the preceding multiword-pipe proof. Existing source-specific retention is +reused; no fresh replay is claimed. Protected normal assemblies, production +CPU source, API/packages and regression retirement remain unchanged. The +prototype stays isolated, unimported and unpromoted. Wider indexed/chained +pipes and provenance, frame transport, other read origins, trace/interrupt, +changed return-SR/stack, memory destinations, other families and remaining +reference/consolidation gates stay required. Milestone 6 remains **in progress**, +`roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index f42031b0..5ceef73a 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2967,3 +2967,19 @@ transport and remaining continuation/reference/consolidation stay required. No production import, package change or regression retirement occurs; milestone 6 stays **in progress**, `roadmapComplete=false`. See the [three-word pipe qualification](COPPER68K_REFERENCE_QUALIFICATION.md#three-word-private-pipe-transport--2026-10-07). + + +Nested operand/trace-vector provenance now passes 516,096 programs across seven +MOVE source forms, operand sizes, stack states, all CCR/vector lanes and both +execution routes. Real handlers overwrite consumed code, supply vector input, +refault and change VBR. Original instruction PC/opcode, next PC, post-MOVE trace +SR, typed pending state and completed effects pass. Sole PC/SR mutations detect +exactly 258,048 / 241,920 intended failures, preserving unaffected cases. +Complete source/key/weight/TRX audits and five integrity controls pass. Earlier +vector-initialization failures remain invalid fixture evidence with a fixture- +only repair. This qualifies serialized private provenance, not physical overlap. +Wider indexed/chained pipes/provenance, transport and remaining continuation/ +reference/consolidation gates remain required. No production import, package +change or regression retirement occurs; milestone 6 stays **in progress**, +`roadmapComplete=false`. +See the [nested trace qualification](COPPER68K_REFERENCE_QUALIFICATION.md#nested-operand-and-trace-vector-provenance--2026-10-07). From dd072e70a365299cf31ab56e06db42ed2d1b3750 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 10:52:59 +0300 Subject: [PATCH 099/211] Record high and wrapped private operand-frame transport --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 51 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 13 +++++ 2 files changed, 64 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 05f042f8..bd7f5c70 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8780,3 +8780,54 @@ pipes and provenance, frame transport, other read origins, trace/interrupt, changed return-SR/stack, memory destinations, other families and remaining reference/consolidation gates stay required. Milestone 6 remains **in progress**, `roadmapComplete=false`. + + +#### High and wrapped private operand frames — 2026-10-07 + +The unchanged isolated prototype passes **94,080 programs / 1,680 reports / +420 executions** for nominal private operand-frame entry and RTE transport. +Frame starts are `00004700`, `10004700`, `00FFFFA4`, `00FFFFD0`, `FFFFFFA4`, +`FFFFFFD0`, `FFFFFFFF`, `01000001`, `00100001` and `10004701`. They include +high/odd addresses, exact boundary ends and crossings of the 24-/32-bit address +boundaries. Seven source forms, MOVE byte/word/long, four stack states, all +operand fault lanes, CCR 00/1F, input/refault/alias handlers, both routes and +EC020/A1200/020/030 profiles remain represented. Earlier canonical groups +retain all-CCR evidence; this address group avoids repeating that Cartesian +product. [MC68020UM](https://www.nxp.com/docs/en/data-sheet/MC68020UM.pdf) +distinguishes the MC68020's 32-bit and EC020's 24-bit external address spaces; +logical register addresses retain their full width. + +A relocated VBR keeps vector entries clear of wrapping frames. Full-address +models poison the low 24-bit mirror wherever it is disjoint from the real +frame/guards. Defined/private fields independently verify original PC/SR, +consumed next PC, opcode, operand width, source address, guest stack and pending +kind. Entry writes match the descending logical word sequence mapped through +the selected model, including EC020 split bytes at its address-space boundary. +Map predicates reject requests outside that physical domain. Guards, untouched +mirrors, architectural registers/stacks and memory, exact exception/read counts, +real handler changes and the following sentinel pass. + +A sole mutation truncates the logical stack on RTE: **47,040 intended failures / +47,040 unaffected passes**. Repeated faults additionally expose the resulting +wrong logical frame, with precise A7 failure reasons. A sole mutation truncates +frame-entry write addresses: **32,928 intended failures / 61,152 unaffected +passes**; the EC020 profiles already apply that external mapping and remain +unaffected. Both mutations preserve fixtures. Complete 222-file inventories, +actual TRX rosters, independently enumerated case keys/weights and five +corruption controls pass. + +Evidence: `audits/Operand020FrameTransport`, +`mutations/Operand020FrameTransport/StackMask` and +`mutations/Operand020FrameTransport/EntryMask`; aggregate +`operand020-frame-transport-proof.json`, SHA-256 +`72fa89495653d9f02af0babffb958bd27388e79db19cbdde62003b253ba93c74`. +The proof pins three executions, complete source/producer/verifier/binary +identities, sole mutations, integrity rejections and the preceding trace proof. +Existing source-specific retention is reused; no fresh replay is claimed. +This qualifies nominal private transport, not physical port widths/timing or +silicon-private frame contents. Faults during frame entry/state loading, +wider indexed/chained pipes/provenance, other origins, changed return-SR/stack, +memory destinations, other families and remaining reference/consolidation stay +required. Production source/packages and protected normal assemblies remain +unchanged; no regression is retired and the prototype stays isolated/unpromoted. +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 5ceef73a..69b20798 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2983,3 +2983,16 @@ reference/consolidation gates remain required. No production import, package change or regression retirement occurs; milestone 6 stays **in progress**, `roadmapComplete=false`. See the [nested trace qualification](COPPER68K_REFERENCE_QUALIFICATION.md#nested-operand-and-trace-vector-provenance--2026-10-07). + + +Nominal high/wrapped private operand frames now pass 94,080 programs at ten +frame addresses, including odd and 24-/32-bit crossings. Seven source forms, +sizes, stack states, all operand lanes, CCR 00/1F and real input/refault/alias +handlers verify full logical stacks, physical entry ordering/domain, guards, +poisoned mirrors and continued effects. Stack/entry truncation mutations detect +exactly 47,040 / 32,928 intended failures, preserving unaffected cases. +Complete source/key/weight/TRX audits and five integrity controls pass. Frame +entry/state-load faults and remaining wider continuation/reference/consolidation +gates remain required; no production import, package change or test retirement +occurs. Milestone 6 stays **in progress**, `roadmapComplete=false`. +See the [private frame transport qualification](COPPER68K_REFERENCE_QUALIFICATION.md#high-and-wrapped-private-operand-frames--2026-10-07). From 1e1ab44489b68982a1bbc981c45cddf01fec09c5 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 11:15:36 +0300 Subject: [PATCH 100/211] Record private frame entry and state-load HALT qualification --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 67 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 17 +++++ 2 files changed, 84 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index bd7f5c70..6944ae8f 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8831,3 +8831,70 @@ memory destinations, other families and remaining reference/consolidation stay required. Production source/packages and protected normal assemblies remain unchanged; no regression is retired and the prototype stays isolated/unpromoted. Milestone 6 remains **in progress**, `roadmapComplete=false`. + + +#### Private frame-entry and internal state-load HALT — 2026-10-07 + +The unchanged isolated prototype passes **138,240 programs / 160 reports / +40 executions** for private frame-entry and internal RTE state-load faults. +The ten preceding high/wrapped/odd frame starts remain selected. MOVE.L +`(A0),D0` provides a canonical source fault; four stack states, CCR 00/1F, +scalar/batch routes and EC020/A1200/020/030 profiles are covered. Entry faults +exercise both bytes of all 46 descending word writes: **58,880 programs**. +State-load faults exercise every byte of all 54 read occurrences, including +repeated reads: **78,080 programs**. The remaining **1,280 controls** complete +RTE, the pending operand read and the following MOVEQ sentinel. + +[MC68020UM section 6.1.2](https://www.nxp.com/docs/en/data-sheet/MC68020UM.pdf) +and [MC68030UM section 8.1.2](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P2.pdf) +specify HALT for a bus error during bus/address/reset exception processing or +internal RTE state loading, with no further attempt to alter memory. This gate +checks selected bus-error entry and internal state-load paths; it does not +claim reset/address-error entry coverage. Earlier recoverable RTE validation +faults remain distinct from internal state-load faults. + +The complete 92-byte private image is independently constructed from literals, +including the selected cold pipe state. Guards and disjoint poisoned low +mirrors verify memory outside completed writes. Entry failures preserve exactly +the completed descending prefix and the failed logical stack position, without +fetching the handler or building another image. Load failures preserve the +completed validation/load read prefix, uncommitted original frame and live +handler state, without an operand retry or another exception. Exact PC/SR, +registers/stacks, exception counts, access widths/order and physical map domain +pass, including EC020 boundary splits. Subsequent HALT calls perform no memory +access or architectural work. Scalar calls retain the existing two-native-cycle +Idle policy with a bus-synchronization barrier; batch calls retire zero +instructions without advancing those counters. This is approximate policy +preservation, not physical timing qualification. + +The first fixture did not compile because two batch calls omitted required +arguments. It remains compile-only evidence with no TRX or executed coverage. +An API-only fixture repair executed the matrix but incorrectly required scalar +HALT calls to freeze timing counters: **69,760 passes / 68,480 precisely +identified invalid-fixture failures**. A second fixture-only repair checks the +existing Idle policy while preserving architectural and bus invariants. No CPU +defect is inferred from either fixture error. + +A sole entry mutation continues after a failed frame write: **58,880 intended +failures / 79,360 unaffected passes**. A sole state-load mutation fails to set +HALT: **78,080 intended failures / 60,160 unaffected passes**. Every failure +identifier/reason is checked; both mutations retain the repaired fixture. +Complete 223-file source inventories, exact TRX rosters, independently +enumerated case keys/weights and five evidence-corruption controls pass. + +Evidence: `audits/Operand020FrameFaults`, `audits/Operand020FrameFaultsV2`, +`audits/Operand020FrameFaultsV3`, +`mutations/Operand020FrameFaults/ContinueEntry` and +`mutations/Operand020FrameFaults/ContinueLoad`; aggregate +`operand020-frame-faults-proof.json`, SHA-256 +`69b5baebce61d0284fc132a3840c2c18c05d1ca422d1fb760e1337fbc2d823b3`. +The proof pins four executed snapshots plus the compile-only attempt, complete +source/producer/verifier/binary identities, exact fixture-only repairs, sole +mutations, integrity rejections and the preceding nominal transport proof. +Existing source-specific retention is reused; no fresh full replay is claimed. +Production CPU source/packages and protected normal assemblies stay unchanged; +the prototype remains isolated, unimported and unpromoted. Wider indexed/chained +pipes/provenance, foreign frames, other origins, reset/address-error entry, +trace/interrupt, changed return-SR/stack, memory destinations, other families +and remaining reference/consolidation gates stay required. No regression is +retired. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 69b20798..5bb4fa16 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2996,3 +2996,20 @@ entry/state-load faults and remaining wider continuation/reference/consolidation gates remain required; no production import, package change or test retirement occurs. Milestone 6 stays **in progress**, `roadmapComplete=false`. See the [private frame transport qualification](COPPER68K_REFERENCE_QUALIFICATION.md#high-and-wrapped-private-operand-frames--2026-10-07). + +Private frame-entry and internal state-load faults now pass 138,240 programs +on the unchanged isolated prototype: 58,880 entry faults, 78,080 load faults +and 1,280 recovery controls. Ten high/wrapped/odd frame starts, all 92 entry +byte positions and 122 load-byte occurrences, stack states, CCR 00/1F and both +routes verify the complete literal image, completed access prefixes, exact +HALT state and absence of subsequent architectural/bus work. Scalar Idle timing +and zero-retirement batch policy remain intact. Sole entry/load mutations detect +exactly 58,880 / 78,080 intended failures, preserving unaffected cases. +Complete source/key/weight/TRX audits and five integrity controls pass. The +compile-only attempt and 68,480 earlier scalar timing-expectation failures stay +invalid fixture evidence; both repairs change only the fixture. No production +import, package change or regression retirement occurs. Wider indexed/chained +pipes/provenance, foreign frames, other origins, reset/address-error entry and +remaining continuation/reference/consolidation gates stay required. Milestone +6 remains **in progress**, `roadmapComplete=false`. +See the [frame fault HALT qualification](COPPER68K_REFERENCE_QUALIFICATION.md#private-frame-entry-and-internal-state-load-halt--2026-10-07). From 72d23ed2c5ada28dec6037a7c9d2c4ae573af46b Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 12:01:07 +0300 Subject: [PATCH 101/211] Prove and consolidate CMPM alias and stack regressions --- Copper68k.Tests/M68020CmpmTests.cs | 29 ------- Copper68k.Tests/Synthetic/README.md | 21 +++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 58 ++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 ++++ ...-copper68k-cmpm-consolidation-integrity.py | 59 ++++++++++++++ scripts/run-copper68k-cmpm-consolidation.ps1 | 15 ++++ scripts/test-copper68k-cmpm-consolidation.ps1 | 76 +++++++++++++++++++ .../verify-copper68k-cmpm-consolidation.py | 56 ++++++++++++++ 8 files changed, 300 insertions(+), 29 deletions(-) create mode 100644 scripts/prove-copper68k-cmpm-consolidation-integrity.py create mode 100644 scripts/run-copper68k-cmpm-consolidation.ps1 create mode 100644 scripts/test-copper68k-cmpm-consolidation.ps1 create mode 100644 scripts/verify-copper68k-cmpm-consolidation.py diff --git a/Copper68k.Tests/M68020CmpmTests.cs b/Copper68k.Tests/M68020CmpmTests.cs index 5471492b..61c7bcbb 100644 --- a/Copper68k.Tests/M68020CmpmTests.cs +++ b/Copper68k.Tests/M68020CmpmTests.cs @@ -44,35 +44,6 @@ public void CmpmComparesSelectedWidthPreservesExtendAndDoesNotWriteMemory(int pr Assert.Equal(5, cpu.State.Cycles); } - [Theory] - [InlineData(0)] [InlineData(1)] [InlineData(2)] - public void AliasedAddressRegisterUsesTheNextOperandAfterSourceIncrement(int size) - { - var bus = new ZeroWaitCodeBus(); - WriteWords(bus, CodeBase, (ushort)(0xB108 | size << 6)); - Store(bus, 0x2000, size, 0); - Store(bus, 0x2000 + (1u << size), size, 1); - using var cpu = Create(bus, 1); - cpu.Reset(CodeBase, 0x4000); cpu.State.A[0] = 0x2000; cpu.State.StatusRegister = 0x201F; - - cpu.ExecuteInstruction(); - - Assert.Equal(0x2000u + (2u << size), cpu.State.A[0]); - Assert.Equal(0x10, cpu.State.StatusRegister & 31); - } - - [Fact] - public void ByteStackRegisterUsesTwoByteStrideForBothAliasedOperands() - { - var bus = new ZeroWaitCodeBus(); WriteWords(bus, CodeBase, 0xBF0F); - WriteWords(bus, 0x4000, 0x0080, 0x0180); - using var cpu = Create(bus, 1); - cpu.Reset(CodeBase, 0x4000); cpu.State.StatusRegister = 0x201F; - cpu.ExecuteInstruction(); - Assert.Equal(0x4004u, cpu.State.A[7]); - Assert.Equal(0x10, cpu.State.StatusRegister & 31); - } - private static IM68kCore Create(ZeroWaitCodeBus bus, int profile) => profile switch { 0 => M68kCoreFactory.Default.Create(M68kCpuModel.M68020, bus), diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 0e4eb92a..a01d13d2 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1222,3 +1222,24 @@ requires format 7. Actual recovery, broader addressing/MOVEM phases and all remaining fault protocols are required work. The discovery remains outside promoted ordinary-CI batch counts; no inventory gap is removed. See the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#actual-040-operand-read-fault-discovery--2026-10-06). + +## CMPM semantic consolidation + +Run `./scripts/test-copper68k-cmpm-consolidation.ps1 -PythonPath +-OutputDirectory ` to reproduce the original CMPM alias/stack +witnesses and their shared synthetic replacements. Python 3 uses only its +standard library for this optional audit; ordinary xUnit coverage is unchanged. +The command reinstates the exact class pinned at commit +`1e1ab44489b68982a1bbc981c45cddf01fec09c5` in owned source copies, including +after retirement. Production source and normal build outputs are preserved. + +All eight shared `arithmetic-extend` matrices require 203,520 clean scenarios. +The alias-order and A7 byte-stride mutations must fail both original and +replacement witnesses, with exactly 288 / 180 synthetic failures and the +unaffected cases passing. All 36 specialized timing/width/flags/memory cases +remain in `M68020CmpmTests` and pass under both mutations. Eight corruption +controls check input/source linkage, pinned witnesses, actual execution and +report integrity. `-ValidateReportsOnly` rechecks unchanged retained evidence; +missing, empty, changed or unrelated evidence fails. + +See the [replacement mapping and proof](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#cmpm-alias-and-a7-semantic-consolidation--2026-10-07). diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 6944ae8f..e40c5c20 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8898,3 +8898,61 @@ pipes/provenance, foreign frames, other origins, reset/address-error entry, trace/interrupt, changed return-SR/stack, memory destinations, other families and remaining reference/consolidation gates stay required. No regression is retired. Milestone 6 remains **in progress**, `roadmapComplete=false`. + + +#### CMPM alias and A7 semantic consolidation — 2026-10-07 + +Two pure semantic methods in `M68020CmpmTests` are retired after proving their +shared replacements: three aliased-A0 size cases and one aliased-A7 byte case. +The existing 36 timing/width/flags/memory cases remain. The shared +`SyntheticExtendDecimalTests.ExtendComparisonAndAliases` matrix checks CMPM +source updates before destination reads, all register pairs, sizes, both stack +privilege modes and complete architectural/memory state. Its A7 byte fixtures +retain the two-byte stride specified by +[M68000PM section 2.2.4](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf). + +| Retired original witness | Representative shared replacement | +| --- | --- | +| `AliasedAddressRegisterUsesTheNextOperandAfterSourceIncrement`, B/W/L | `A1200/CMPM/{1,2,4}/r0-r0/memory=True/super=True`, opcodes `B108/B148/B188`, source `1`, destination all-ones, CCR `14` | +| `ByteStackRegisterUsesTwoByteStrideForBothAliasedOperands` | `A1200/CMPM/1/r7-r7/memory=True/super=True/op=BF0F/s=00000001/d=000000FF/ccr=14` | + +The representative replacement inputs differ from the originals' `0/1`, CCR +`1F`; both inputs distinguish the same ordering/stride defect. The shared group +also covers all other registers and user stacks. No equivalence is inferred +from passing tests alone: the original class is pinned to commit +`1e1ab44489b68982a1bbc981c45cddf01fec09c5` and reinstated unchanged in owned +copies for co-execution under each targeted mutation. + +The clean command passes **203,520 shared programs / eight reports**, plus all +40 pinned original cases. A sole CMPM-function mutation samples the destination +base before source increment: **288 precisely identified synthetic failures** +and all four original alias/stack witnesses fail, while 203,232 shared programs +and all 36 timing cases pass. A second sole-function mutation gives byte A7 a +one-byte stride: **180 precisely identified synthetic failures** and the original +A7 witness fail, while 203,340 shared programs and all 36 timing cases pass. +The untouched 000/010 implementations remain controls. Fixtures are unchanged +across mutations; defined flags, register updates and failure reasons are +independently enumerated. + +The maintained command audits complete source/helper identities, links owned +baseline sources to the current repository and pinned original class, verifies +the exact 48-test TRX roster, all 1,920 weighted keys per profile and every +failure identifier/reason. Eight corruption controls reject missing fixtures, +wrong producers, empty selections, wrong weights, changed pins/witnesses, +unrelated failure reasons and a copied CPU alteration even when its metadata is +re-signed. A fresh direct run against the reduced worktree passes **44 tests**: +all 36 retained timing cases and eight shared batches / 203,520 programs, with +zero failures or skips. + +Reproduce with `scripts/test-copper68k-cmpm-consolidation.ps1 -PythonPath + -OutputDirectory `; Python 3 requires only +its standard library. `-ValidateReportsOnly` checks retained evidence without +executing the CPU. Ordinary CI coverage and integer inventory counts are +unchanged. Evidence: `audits/CmpmConsolidationStrict/proof.json`, SHA-256 +`625fb7cceb164f0b76fd8d4903a992627378e680de6aeff74b95ceadc0815c7d`, +and `audits/CmpmRetainedActual/verification.json`. Earlier probe/command +snapshots remain distinct. Production CPU source/packages and protected normal +assemblies are unchanged; no fresh full CPU/consumer replay is claimed. +Indexed/chained private pipe execution remains a separate pending gate. +All remaining reference/continuation/consolidation requirements stay open; +milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 5bb4fa16..1b0617c1 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3013,3 +3013,18 @@ pipes/provenance, foreign frames, other origins, reset/address-error entry and remaining continuation/reference/consolidation gates stay required. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [frame fault HALT qualification](COPPER68K_REFERENCE_QUALIFICATION.md#private-frame-entry-and-internal-state-load-halt--2026-10-07). + +Two pure CMPM alias/A7 methods (four original cases) are now retired with a +pinned-original/shared-replacement proof. The maintained command passes 203,520 +shared programs; alias-order and byte-stack-stride mutations detect exactly +288 / 180 intended failures and the corresponding original witnesses. All +36 specialized timing/width/flags/memory cases remain and pass under both +mutations. Complete source linkage, exact TRX/key/weight/reason audits and eight +integrity controls pass. A direct reduced-worktree run passes all 44 selected +tests, including the 36 retained cases and eight shared batches. Production CPU +source, packages and protected normal assemblies are unchanged; no fresh full +CPU/consumer replay is claimed. The indexed/chained pipe matrix remains a +separate pending gate. All remaining reference/continuation/consolidation +requirements stay open; milestone 6 remains **in progress**, +`roadmapComplete=false`. +See the [CMPM replacement mapping](COPPER68K_REFERENCE_QUALIFICATION.md#cmpm-alias-and-a7-semantic-consolidation--2026-10-07). diff --git a/scripts/prove-copper68k-cmpm-consolidation-integrity.py b/scripts/prove-copper68k-cmpm-consolidation-integrity.py new file mode 100644 index 00000000..cb5dff94 --- /dev/null +++ b/scripts/prove-copper68k-cmpm-consolidation-integrity.py @@ -0,0 +1,59 @@ +from pathlib import Path +import json,hashlib,shutil,subprocess,sys,xml.etree.ElementTree as ET +root=Path(sys.argv[1]);repo=Path(sys.argv[2]);mode=sys.argv[3];assert mode in ['create','verify'];scripts=Path(__file__).parent +def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest() +def load(p):return json.loads(p.read_text(encoding='utf-8-sig')) +def save(p,obj):p.write_text(json.dumps(obj,indent=2),encoding='utf-8') +definitions=[('OmittedFixture','Clean','Incomplete/changed source inventory'),('WrongProducer','Clean','Wrong producer'),('EmptySelection','Clean','Wrong/empty execution selection'),('WrongWeight','Clean','Missing/extra keys or weights'),('WrongHistoricalPin','Clean','Wrong historical/model pin'),('ChangedHistoricalFixture','Clean','Changed historical regression fixture'),('WrongFailureReason','AliasOrder','Unrelated failure identifier/reason'),('WrongBaselineCpu','Clean','Wrong baseline source identity')] +if mode=='create': + controls=root/'integrity';assert not controls.exists();rows=[] + for name,selection,expected in definitions: + parent=root/selection;r=controls/name;r.mkdir(parents=True);shutil.copytree(parent/'source',r/'source',ignore=shutil.ignore_patterns('bin','obj')) + for p in parent.iterdir(): + if p.name in ['inputs.json','base-inputs.json','audit.trx'] or p.name.endswith('-arithmetic-extend.json'):shutil.copy2(p,r/p.name) + if name=='OmittedFixture':(r/'source/Copper68k.Tests/Synthetic/SyntheticExtendDecimalTests.cs').unlink() + if name=='WrongProducer': + p=r/'inputs.json';d=load(p);d['helperSha256']='0'*64;save(p,d) + if name=='EmptySelection': + p=r/'audit.trx';tree=ET.parse(p);ns={'t':'http://microsoft.com/schemas/VisualStudio/TeamTest/2010'};results=tree.find('.//t:Results',ns) + for child in list(results):results.remove(child) + tree.write(p,encoding='utf-8',xml_declaration=True) + if name=='WrongWeight': + p=r/'68020-arithmetic-extend.json';d=load(p);d['combinations'][next(iter(d['combinations']))]['passing']+=1;save(p,d) + if name in ['WrongHistoricalPin','ChangedHistoricalFixture']: + p=r/'base-inputs.json';d=load(p) + if name=='WrongHistoricalPin':d['historicalPin']='0'*40 + else: + fixture=r/'source/Copper68k.Tests/M68020CmpmTests.cs';fixture.write_text(fixture.read_text(encoding='utf-8-sig')+'\n// altered historical witness\n',encoding='utf-8');h=sha(fixture) + for item in d['sourceIds']: + if item['path'].replace('\\','/')=='Copper68k.Tests/M68020CmpmTests.cs':item['sha256']=h + inputs=load(r/'inputs.json') + for item in inputs['sources']: + if item['path'].replace('\\','/')=='Copper68k.Tests/M68020CmpmTests.cs':item['sha256']=h + save(r/'inputs.json',inputs) + save(p,d);inputs=load(r/'inputs.json');inputs['baseInputsSha256']=sha(p);save(r/'inputs.json',inputs) + if name=='WrongFailureReason': + p=r/'A1200-arithmetic-extend.json';d=load(p);d['failures'][0]['reason']='unrelated failure';save(p,d) + if name=='WrongBaselineCpu': + cpu=r/'source/Copper68k/M68kAdvancedTimingInterpreter.cs';cpu.write_text(cpu.read_text(encoding='utf-8-sig')+'\n// unrelated copied CPU source\n',encoding='utf-8');h=sha(cpu);base=load(r/'base-inputs.json');inputs=load(r/'inputs.json') + for collection in [base['sourceIds'],inputs['sources']]: + for item in collection: + if item['path'].replace('\\','/')=='Copper68k/M68kAdvancedTimingInterpreter.cs':item['sha256']=h + save(r/'base-inputs.json',base);inputs['baseInputsSha256']=sha(r/'base-inputs.json');save(r/'inputs.json',inputs) + result=subprocess.run([sys.executable,str(scripts/'verify-copper68k-cmpm-consolidation.py'),str(r),selection,str(repo),str(root)],capture_output=True,text=True) + (r/'rejection.log').write_text(result.stdout+result.stderr,encoding='utf-8') + if result.returncode==0 or expected not in result.stderr:raise ValueError('Wrong integrity rejection: '+name+' '+result.stderr) + rows.append(dict(name=name,mode=selection,exit=result.returncode,reason=expected,rejectionSha256=sha(r/'rejection.log')));print(name+': rejected precisely',flush=True) + save(root/'integrity.json',dict(schema=1,controls=rows,helperSha256=sha(Path(__file__)),cleanVerificationSha256=sha(root/'Clean/verification.json'))) +integrity=load(root/'integrity.json');assert integrity['helperSha256']==sha(Path(__file__)) and integrity['cleanVerificationSha256']==sha(root/'Clean/verification.json');assert len(integrity['controls'])==len(definitions) +for row,(name,selection,expected) in zip(integrity['controls'],definitions):assert row['name']==name and row['mode']==selection and row['exit']!=0 and row['reason']==expected and row['rejectionSha256']==sha(root/'integrity'/name/'rejection.log') +entries=[] +for selection,expected in [('Clean',(203520,0)),('AliasOrder',(203232,288)),('ByteStackStride',(203340,180))]: + r=root/selection;v=load(r/'verification.json');i=load(r/'inputs.json');base=load(r/'base-inputs.json');assert (v['passing'],v['mismatching'])==expected and v['executions']==48 and v['retainedTimingExecutions']==36 and len(v['reports'])==8 + assert v['inputsSha256']==sha(r/'inputs.json') and v['trxSha256']==sha(r/'audit.trx') and v['helperSha256']==sha(scripts/'verify-copper68k-cmpm-consolidation.py') and i['helperSha256']==sha(scripts/'run-copper68k-cmpm-consolidation.ps1') and i['baseInputsSha256']==sha(r/'base-inputs.json') + for row in v['reports']:assert row['sha256']==sha(r/row['path']) + for file,h in base['normalAssemblies'].items():assert sha(repo/file)==h + for row in base['repositoryInputs']:assert sha(repo/row['path'])==row['sha256'] + entries.append(dict(mode=selection,passing=expected[0],mismatching=expected[1],verificationSha256=sha(r/'verification.json'),inputsSha256=sha(r/'inputs.json'),trxSha256=sha(r/'audit.trx'),cpuSha256=sha(r/'build/bin/Copper68k/release/Copper68k.dll'),testSha256=sha(r/'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'),replacementAnchors=v['replacementAnchors'])) +proof=dict(schema=1,scope='CMPM alias ordering and A7 byte stride semantic replacement; entire shared arithmetic-extend matrix across eight profiles plus pinned original witnesses. Existing 36 timing/width/flags/memory cases are co-executed and retained.',historicalPin='1e1ab44489b68982a1bbc981c45cddf01fec09c5',entries=entries,passingCleanScenarios=203520,aliasMutationMismatches=288,stackMutationMismatches=180,originalAliasWitnessFailures=4,originalStackWitnessFailures=1,retainedTimingExecutions=36,integrityControls=8,integritySha256=sha(root/'integrity.json'),historicalMethods=['AliasedAddressRegisterUsesTheNextOperandAfterSourceIncrement','ByteStackRegisterUsesTwoByteStrideForBothAliasedOperands'],originalAndReplacementDetected=True,mutantFixturesUnchanged=True,productionCpuChanged=False,publication=False,roadmapComplete=False,helperSha256=sha(Path(__file__))) +save(root/'proof.json',proof);print('CMPM consolidation proof SHA-256: '+sha(root/'proof.json')) diff --git a/scripts/run-copper68k-cmpm-consolidation.ps1 b/scripts/run-copper68k-cmpm-consolidation.ps1 new file mode 100644 index 00000000..a935bb69 --- /dev/null +++ b/scripts/run-copper68k-cmpm-consolidation.ps1 @@ -0,0 +1,15 @@ +param([Parameter(Mandatory)][string]$Root) +$ErrorActionPreference='Stop' +function Hash([string]$Path){(Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()} +$repo=Join-Path $Root 'source' +$files=@(Get-ChildItem (Join-Path $repo 'Copper68k'),(Join-Path $repo 'Copper68k.Tests') -Recurse -File | Where-Object {$_.Extension -in @('.cs','.csproj') -and $_.FullName -notmatch '[\\/](bin|obj)[\\/]'}) +$ids=@($files|ForEach-Object{@{path=[IO.Path]::GetRelativePath($repo,$_.FullName);sha256=(Hash $_.FullName)}}) +$saved=[Environment]::GetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR') +try { + [Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$Root) + & dotnet.exe test (Join-Path $repo 'Copper68k.Tests/Copper68k.Tests.csproj') -c Release --artifacts-path (Join-Path $Root 'build') --filter 'FullyQualifiedName~M68020CmpmTests|FullyQualifiedName~ExtendComparisonAndAliases' --logger 'trx;LogFileName=audit.trx' --results-directory $Root *> (Join-Path $Root 'execution.log') + $testExit=$LASTEXITCODE +}finally{[Environment]::SetEnvironmentVariable('COPPER68K_SYNTHETIC_REPORT_DIR',$saved)} +foreach($i in $ids){if((Hash (Join-Path $repo $i.path)) -ne $i.sha256){throw 'Source changed during consolidation execution'}} +@{schema=1;testExit=$testExit;sources=$ids;helperSha256=(Hash $PSCommandPath);baseInputsSha256=(Hash (Join-Path $Root 'base-inputs.json'));roadmapComplete=$false}|ConvertTo-Json -Depth 10|Set-Content (Join-Path $Root 'inputs.json') +Write-Output "CMPM consolidation execution exit $testExit; strict audit pending." diff --git a/scripts/test-copper68k-cmpm-consolidation.ps1 b/scripts/test-copper68k-cmpm-consolidation.ps1 new file mode 100644 index 00000000..c5610031 --- /dev/null +++ b/scripts/test-copper68k-cmpm-consolidation.ps1 @@ -0,0 +1,76 @@ +#requires -Version 7.0 +[CmdletBinding()] +param( + [Parameter(Mandatory)][string]$PythonPath, + [string]$OutputDirectory='artifacts/cmpm-consolidation', + [switch]$ValidateReportsOnly +) +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +$pin='1e1ab44489b68982a1bbc981c45cddf01fec09c5' +$legacy='Copper68k.Tests/M68020CmpmTests.cs' +$cpu='Copper68k/M68kAdvancedTimingInterpreter.cs' +$helpers=@('scripts/test-copper68k-cmpm-consolidation.ps1','scripts/run-copper68k-cmpm-consolidation.ps1','scripts/verify-copper68k-cmpm-consolidation.py','scripts/prove-copper68k-cmpm-consolidation-integrity.py') +$files=@(& git -C $repo ls-files Copper68k Copper68k.Tests | Where-Object {[IO.Path]::GetExtension($_) -in @('.cs','.csproj')}) +if($LASTEXITCODE -ne 0 -or $files.Count -eq 0){throw 'Missing source input selection'} +function Hash([string]$Path){(Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()} +$fixture=(@(& git -C $repo show "${pin}:$legacy") -join "`n")+"`n" +if($LASTEXITCODE -ne 0 -or $fixture -notmatch 'AliasedAddressRegisterUsesTheNextOperandAfterSourceIncrement' -or $fixture -notmatch 'ByteStackRegisterUsesTwoByteStrideForBothAliasedOperands'){throw 'Missing pinned historical CMPM witnesses'} +if(-not $ValidateReportsOnly){ + if(Test-Path -LiteralPath $output){throw "Use a fresh output directory: $output"} + New-Item -ItemType Directory -Path $output | Out-Null + $repositoryInputs=@($files+$helpers | Sort-Object -Unique | ForEach-Object {@{path=$_;sha256=(Hash (Join-Path $repo $_))}}) + $normal=@{} + foreach($file in @('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll')){ + if(Test-Path -LiteralPath (Join-Path $repo $file)){$normal[$file]=Hash (Join-Path $repo $file)} + } + foreach($mode in @('Clean','AliasOrder','ByteStackStride')){ + $owned=Join-Path $output $mode + foreach($file in $files){ + $target=Join-Path $owned (Join-Path 'source' $file) + New-Item -ItemType Directory -Path (Split-Path -Parent $target) -Force | Out-Null + Copy-Item -LiteralPath (Join-Path $repo $file) -Destination $target + } + # Reinstate the exact pinned original class only in the owned audit copy. + $legacyPath=Join-Path $owned (Join-Path 'source' $legacy) + [IO.File]::WriteAllText($legacyPath,$fixture) + $source=Join-Path $owned 'source' + $ids=@(Get-ChildItem (Join-Path $source 'Copper68k'),(Join-Path $source 'Copper68k.Tests') -Recurse -File | + Where-Object {$_.Extension -in @('.cs','.csproj')} | + ForEach-Object {@{path=[IO.Path]::GetRelativePath($source,$_.FullName).Replace('\','/');sha256=(Hash $_.FullName)}}) + $path=Join-Path $source $cpu + $text=[IO.File]::ReadAllText($path).Replace("`r`n","`n") + $start=$text.IndexOf(' private void ExecuteCmpmPostIncrement(') + if($start -lt 0){throw 'Missing unique CMPM mutation target'} + $end=$text.IndexOf(' private void ExecuteWordBranch(',$start) + if($end -le $start){throw 'Missing CMPM function boundary'} + $block=$text.Substring($start,$end-$start) + if($mode -eq 'AliasOrder'){ + $old='var source = ReadSized(State.A[sourceRegister], size);' + if($block.Split([string[]]@($old),[StringSplitOptions]::None).Count -ne 2){throw 'Ambiguous source mutation'} + $block=$block.Replace($old,"var capturedDestination = State.A[destinationRegister]; // intentional old-base alias sampling`n $old") + $old='var destination = ReadSized(State.A[destinationRegister], size);' + if($block.Split([string[]]@($old),[StringSplitOptions]::None).Count -ne 2){throw 'Ambiguous destination mutation'} + $block=$block.Replace($old,'var destination = ReadSized(capturedDestination, size);') + } + if($mode -eq 'ByteStackStride'){ + foreach($register in @('sourceRegister','destinationRegister')){ + $old="size == M68kOperandSize.Byte && $register == 7 ? 2u : (uint)size" + if($block.Split([string[]]@($old),[StringSplitOptions]::None).Count -ne 2){throw 'Ambiguous stack-stride mutation'} + $block=$block.Replace($old,"size == M68kOperandSize.Byte && $register == 7 ? 1u : (uint)size") + } + } + if($mode -ne 'Clean'){[IO.File]::WriteAllText($path,$text.Substring(0,$start)+$block+$text.Substring($end))} + $sha= [Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($fixture)) + @{schema=1;sourceCommit=(& git -C $repo rev-parse HEAD);historicalPin=$pin;mode=$mode;sourceIds=$ids;repositoryInputs=$repositoryInputs;normalAssemblies=$normal;historicalFile=$legacy;historicalSha256=[Convert]::ToHexString($sha).ToLowerInvariant();roadmapComplete=$false}|ConvertTo-Json -Depth 10|Set-Content (Join-Path $owned 'base-inputs.json') + & (Join-Path $PSScriptRoot 'run-copper68k-cmpm-consolidation.ps1') -Root $owned + } +} +foreach($mode in @('Clean','AliasOrder','ByteStackStride')){ + & $PythonPath (Join-Path $PSScriptRoot 'verify-copper68k-cmpm-consolidation.py') (Join-Path $output $mode) $mode $repo $output + if($LASTEXITCODE -ne 0){throw "CMPM consolidation verification failed: $mode"} +} +& $PythonPath (Join-Path $PSScriptRoot 'prove-copper68k-cmpm-consolidation-integrity.py') $output $repo $(if($ValidateReportsOnly){'verify'}else{'create'}) +if($LASTEXITCODE -ne 0){throw 'CMPM consolidation integrity/proof failed'} +Write-Output 'CMPM alias/stack consolidation proven; all original and replacement witnesses audited, 36 timing cases retained.' diff --git a/scripts/verify-copper68k-cmpm-consolidation.py b/scripts/verify-copper68k-cmpm-consolidation.py new file mode 100644 index 00000000..76c2eb97 --- /dev/null +++ b/scripts/verify-copper68k-cmpm-consolidation.py @@ -0,0 +1,56 @@ +from pathlib import Path +import hashlib,json,subprocess,itertools,re,xml.etree.ElementTree as ET,sys +b=Path(__file__).parent;root=Path(sys.argv[1]);mode=sys.argv[2];assert mode in ['Clean','AliasOrder','ByteStackStride'];repo=Path(sys.argv[3]);evidenceRoot=Path(sys.argv[4]) +def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest() +def load(p):return json.loads(p.read_text(encoding='utf-8-sig')) +def check(x,msg): + if not x:raise ValueError(msg) +i=load(root/'inputs.json');base=load(root/'base-inputs.json');check(i['testExit']==(0 if mode=='Clean' else 1),'Wrong execution exit');check(i['helperSha256']==sha(b/'run-copper68k-cmpm-consolidation.ps1'),'Wrong producer');check(i['baseInputsSha256']==sha(root/'base-inputs.json'),'Changed base inputs') +source=root/'source';actual={str(p.relative_to(source)).replace('\\','/'):sha(p) for p in source.rglob('*') if p.suffix in ['.cs','.csproj'] and 'bin' not in p.parts and 'obj' not in p.parts};check(actual=={x['path'].replace('\\','/'):x['sha256'] for x in i['sources']},'Incomplete/changed source inventory');expected={x['path']:x['sha256'] for x in base['sourceIds']};cpu='Copper68k/M68kAdvancedTimingInterpreter.cs';check(set(actual)==set(expected),'Wrong source inventory selection');check([p for p in expected if actual[p]!=expected[p]]==([] if mode=='Clean' else [cpu]),'Wrong mutation source scope') +for p,h in base['normalAssemblies'].items():check(sha(repo/p)==h,'Changed protected assembly') +normalFiles=['Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll'];check(set(base['normalAssemblies'])=={p for p in normalFiles if (repo/p).exists()},'Incomplete protected assembly identities') +currentFiles=[p for p in subprocess.check_output(['git','ls-files','Copper68k','Copper68k.Tests'],cwd=repo,text=True).splitlines() if Path(p).suffix in ['.cs','.csproj']] +helpers=['scripts/test-copper68k-cmpm-consolidation.ps1','scripts/run-copper68k-cmpm-consolidation.ps1','scripts/verify-copper68k-cmpm-consolidation.py','scripts/prove-copper68k-cmpm-consolidation-integrity.py'] +check(len(base['repositoryInputs'])==len(set(currentFiles+helpers)) and {x['path'] for x in base['repositoryInputs']}==set(currentFiles+helpers),'Incomplete repository input identities') +for row in base['repositoryInputs']:check(sha(repo/row['path'])==row['sha256'],'Changed repository source/helper') +pin='1e1ab44489b68982a1bbc981c45cddf01fec09c5';legacy='Copper68k.Tests/M68020CmpmTests.cs';check(base['historicalPin']==pin and base['mode']==mode,'Wrong historical/model pin');fixture=subprocess.check_output(['git','show',pin+':'+legacy],cwd=repo).decode('utf-8-sig');check((source/legacy).read_text(encoding='utf-8-sig')==fixture.replace('\r\n','\n'),'Changed historical regression fixture');check(base['historicalSha256']==hashlib.sha256(fixture.encode()).hexdigest(),'Wrong historical fixture identity') +baseline={p:sha(repo/p) for p in currentFiles};baseline[legacy]=hashlib.sha256(fixture.replace('\r\n','\n').encode()).hexdigest();check(expected==baseline,'Wrong baseline source identity') +if mode!='Clean': + clean=evidenceRoot/'Clean';t=(clean/'source'/cpu).read_text(encoding='utf-8-sig');start=t.index(' private void ExecuteCmpmPostIncrement(');end=t.index(' private void ExecuteWordBranch(',start);block=t[start:end] + if mode=='AliasOrder': + old='var source = ReadSized(State.A[sourceRegister], size);';check(block.count(old)==1,'Missing unique alias target');block=block.replace(old,'var capturedDestination = State.A[destinationRegister]; // intentional old-base alias sampling\n '+old);old='var destination = ReadSized(State.A[destinationRegister], size);';check(block.count(old)==1,'Missing destination target');block=block.replace(old,'var destination = ReadSized(capturedDestination, size);') + else: + for reg in ['sourceRegister','destinationRegister']: + old=f'size == M68kOperandSize.Byte && {reg} == 7 ? 2u : (uint)size';check(block.count(old)==1,'Missing byte stride target');block=block.replace(old,f'size == M68kOperandSize.Byte && {reg} == 7 ? 1u : (uint)size') + check((source/cpu).read_text(encoding='utf-8-sig')==t[:start]+block+t[end:],'Wrong intentional mutation') +models=['68000','68010','68EC020','68020','68030','68040','68060','A1200'];timing='Copper68k.Tests.M68020CmpmTests.CmpmComparesSelectedWidthPreservesExtendAndDoesNotWriteMemory';alias='Copper68k.Tests.M68020CmpmTests.AliasedAddressRegisterUsesTheNextOperandAfterSourceIncrement';stack='Copper68k.Tests.M68020CmpmTests.ByteStackRegisterUsesTwoByteStrideForBothAliasedOperands';names={f'{timing}(profile: {p}, size: {s}, flags: {f})':'Passed' for p,s,f in itertools.product(range(3),range(3),[20,25,18,27])} +for size in range(3):names[f'{alias}(size: {size})']='Failed' if mode=='AliasOrder' else 'Passed' +names[stack]='Passed' if mode=='Clean' else 'Failed' +for model in models:names[f'Copper68k.Tests.Synthetic.SyntheticExtendDecimalTests.ExtendComparisonAndAliases(modelId: "{model}")']='Failed' if mode!='Clean' and model not in ['68000','68010'] else 'Passed' +ns={'t':'http://microsoft.com/schemas/VisualStudio/TeamTest/2010'};tree=ET.parse(root/'audit.trx');tests=tree.findall('.//t:UnitTestResult',ns);check(len(tests)==48 and {t.get('testName') for t in tests}==set(names),'Wrong/empty execution selection') +for t in tests: + check(t.get('outcome')==names[t.get('testName')],'Wrong executed test outcome') + if t.get('outcome')=='Failed' and (t.get('testName').startswith(alias) or t.get('testName')==stack): + message=t.find('t:Output/t:ErrorInfo/t:Message',ns).text;exp,act=(16388,16386) if mode=='ByteStackStride' else (16,20);check(re.search(rf'Expected:\s*{exp}\b',message) and re.search(rf'Actual:\s*{act}\b',message),'Wrong historical mutation reason') +stdout='\n'.join(x.text or '' for x in tree.findall('.//t:UnitTestResult/t:Output/t:StdOut',ns));rows=[];passing=0;misses=0;anchors=[] +for model in models: + p=root/f'{model}-arithmetic-extend.json';d=load(p);keys={};failures={} + for width,family,src,dst,supervisor in itertools.product([1,2,4],['ADDX','SUBX','CMPM'],range(8),range(8),[False,True]): + for memory in [True] if family=='CMPM' else [False,True]: + key=f'{model}/{family}/{width}/r{src}-r{dst}/memory={memory}/super={supervisor}';weight=1569 if src==0 and dst==1 and supervisor else 1;failed=model not in ['68000','68010'] and family=='CMPM' and ((mode=='AliasOrder' and src==dst) or (mode=='ByteStackStride' and width==1 and (src==7 or dst==7))) + keys[key]={'passing':weight} if not failed else {'mismatching':1} + if failed: + sr=0x2700 if supervisor else 0x700 + if mode=='AliasOrder':reason=f'SR expected {sr|24:04X}, actual {sr|20:04X}, mask=FFFF' + elif src==7 and dst==7:reason=f'SR expected {sr|24:04X}, actual {sr|25:04X}, mask=FFFF' + else:address=0x4700 if supervisor else 0x7800;reason=f'A7 expected {address+2:08X}, actual {address+1:08X}' + op=(0xb108|(dst<<9)|({1:0,2:1,4:2}[width]<<6)|src);mask=(1<<(width*8))-1;failures[f'{key}/op={op:04X}/s=00000001/d={mask:08X}/ccr=14']=reason + m=len(failures);counts=dict(passing=25440-m,mismatching=m,unsupported=0,untested=0);check(d['schema']==1 and d['model']==model and d['group']=='arithmetic-extend' and d['logicalCases']==25440 and d['xunitBatches']==1,'Wrong report identity/weights');check(d['counts']==counts,'Wrong status counts');check(d['combinations']==keys,'Missing/extra keys or weights');check(len(d['failures'])==m,'Incomplete failure selection') + for f in d['failures']:check(f['status']=='mismatching' and f['id'] in failures and f['reason']==failures.pop(f['id']),'Unrelated failure identifier/reason') + check(not failures,'Missing precise failures');matches=re.findall(re.escape(f'{model}/arithmetic-extend: ')+r'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)',stdout);check(len(matches)==1 and json.loads(matches[0][0])==counts and int(matches[0][1])==25440 and int(matches[0][2])==1920,'Actual TRX/report disagreement');rows.append(dict(path=p.name,sha256=sha(p),passing=25440-m,mismatching=m));passing+=25440-m;misses+=m + if model=='A1200' and mode!='Clean': + anchors=[f'{model}/CMPM/{width}/r{reg}-r{reg}/memory=True/super=True/op={0xb108|(reg<<9)|({1:0,2:1,4:2}[width]<<6)|reg:04X}/s=00000001/d={(1<<(width*8))-1:08X}/ccr=14' for reg,width in ([(0,1),(0,2),(0,4),(7,1)] if mode=='AliasOrder' else [(7,1)])] + check(all(any(f['id']==a for f in d['failures']) for a in anchors),'Missing direct replacement witnesses') +check(passing+misses==203520 and misses=={'Clean':0,'AliasOrder':288,'ByteStackStride':180}[mode],'Wrong complete consolidation counts') +v=dict(schema=1,mode=mode,passing=passing,mismatching=misses,unsupported=0,untested=0,executions=48,retainedTimingExecutions=36,reports=rows,sources=len(actual),historicalPin=pin,historicalFixtureSha256=base['historicalSha256'],replacementAnchors=anchors,inputsSha256=sha(root/'inputs.json'),trxSha256=sha(root/'audit.trx'),helperSha256=sha(Path(__file__)),productionCpuChanged=False,retirementApplied=False,roadmapComplete=False);(root/'verification.json').write_text(json.dumps(v,indent=2));print(f'{mode}: {passing} passing / {misses} precisely identified synthetic failures; original witnesses and 36 timing cases verified') + From b03f1b4edd141987675a520a7afa5ee8b6a5e7aa Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 12:05:59 +0300 Subject: [PATCH 102/211] Normalize CMPM verifier and record final audit identity --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 4 ++-- scripts/verify-copper68k-cmpm-consolidation.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index e40c5c20..8fac60ce 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8948,8 +8948,8 @@ Reproduce with `scripts/test-copper68k-cmpm-consolidation.ps1 -PythonPath -OutputDirectory `; Python 3 requires only its standard library. `-ValidateReportsOnly` checks retained evidence without executing the CPU. Ordinary CI coverage and integer inventory counts are -unchanged. Evidence: `audits/CmpmConsolidationStrict/proof.json`, SHA-256 -`625fb7cceb164f0b76fd8d4903a992627378e680de6aeff74b95ceadc0815c7d`, +unchanged. Evidence: `audits/CmpmConsolidationFinal/proof.json`, SHA-256 +`e6360daec2c2de102da162e448c347bf63ecbc84f8378b28cd902f686ae27f9a`, and `audits/CmpmRetainedActual/verification.json`. Earlier probe/command snapshots remain distinct. Production CPU source/packages and protected normal assemblies are unchanged; no fresh full CPU/consumer replay is claimed. diff --git a/scripts/verify-copper68k-cmpm-consolidation.py b/scripts/verify-copper68k-cmpm-consolidation.py index 76c2eb97..c0d8dc83 100644 --- a/scripts/verify-copper68k-cmpm-consolidation.py +++ b/scripts/verify-copper68k-cmpm-consolidation.py @@ -53,4 +53,3 @@ def check(x,msg): check(all(any(f['id']==a for f in d['failures']) for a in anchors),'Missing direct replacement witnesses') check(passing+misses==203520 and misses=={'Clean':0,'AliasOrder':288,'ByteStackStride':180}[mode],'Wrong complete consolidation counts') v=dict(schema=1,mode=mode,passing=passing,mismatching=misses,unsupported=0,untested=0,executions=48,retainedTimingExecutions=36,reports=rows,sources=len(actual),historicalPin=pin,historicalFixtureSha256=base['historicalSha256'],replacementAnchors=anchors,inputsSha256=sha(root/'inputs.json'),trxSha256=sha(root/'audit.trx'),helperSha256=sha(Path(__file__)),productionCpuChanged=False,retirementApplied=False,roadmapComplete=False);(root/'verification.json').write_text(json.dumps(v,indent=2));print(f'{mode}: {passing} passing / {misses} precisely identified synthetic failures; original witnesses and 36 timing cases verified') - From 17915799f9c6bb14513dd92d63721bd69a94f6df Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 12:16:13 +0300 Subject: [PATCH 103/211] Record full indexed and chained private pipe qualification --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 50 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 ++++++ 2 files changed, 65 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 8fac60ce..954f6b1c 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8956,3 +8956,53 @@ assemblies are unchanged; no fresh full CPU/consumer replay is claimed. Indexed/chained private pipe execution remains a separate pending gate. All remaining reference/continuation/consolidation requirements stay open; milestone 6 remains **in progress**, `roadmapComplete=false`. + +#### Full-format indexed and chained private pipe transport — 2026-10-07 + +The isolated operand-read continuation prototype now passes **4,692,480 +programs / 1,920 reports / 480 executions**: 1,098,240 source-fault programs +across all 66 full-format structural combinations, and 3,594,240 +pointer-to-operand chains across the 54 pointer-bearing combinations. The 12 +direct forms are explicitly excluded from pointer chains and remain covered +by source faults. An/PC bases, MOVE byte/word/long and MOVEA word/long, four +stack states, CCR `00/1F`, every selected pointer/operand byte lane and +scalar/batch execution are included. Canonical indexes use signed `D2.W=-8`; +the separate retained register and signed-boundary gates provide their own +broader index coverage. + +Real handlers supply software pipe prefixes of zero through three words, +change all three backing words and optionally edit each retained word. Chained +faults must preserve the prefix through pointer recovery and the subsequent +operand fault. Independent literal checks verify each saved word, original +instruction provenance, completed access order/width and the following +MOVE.L/NOP result and exact next PC. Existing full-indexed timing policy is +checked separately. This qualifies interpreter-private software transport; +it does not establish physical three-word capture or cache/pipeline timing. + +Two sole CPU-file mutations retain every fixture unchanged. Corrupting the +third word during capture detects exactly **1,437,696 failures**, leaving +3,254,784 programs passing. Corrupting it during restoration detects exactly +**1,876,992 failures**, leaving 2,815,488 passing. Every failure identifier and +reason is independently enumerated, including chained saved-word failures +and source-only following-register differences. No unrelated failures count +as mutation detection. + +The strict verifier checks all 224 source/project inputs against the frozen +parent, exact mutation text, all 480 actual TRX results and their per-test +output, every weighted report key, logical totals and all failure reasons. +Five integrity controls precisely reject omitted or changed fixtures, a wrong +producer, an empty selection and a changed logical weight. Aggregate proof: +`operand020-indexed-pipe-proof.json`, SHA-256 +`0cf31520cc9b5e21cb4093f42baa42a5a19e7bcb689b63f82dd805dc172aafe0`. +Evidence roots are `audits/Operand020IndexedPipe` and +`mutations/Operand020IndexedPipe/{CaptureThird,RestoreThird}` under the +retained reference-restoration evidence directory. The parent frame-fault +proof and all 223 preexisting inputs remain unchanged. + +Production CPU source/packages and protected normal assemblies are unchanged; +the prototype remains isolated and unimported. No fresh full CPU/consumer +replay or regression retirement is claimed. Future import must preserve the +already-delivered CMPM retirements instead of replacing tests from an older +snapshot. Indexed/chained nested trace-vector provenance, foreign frames, +other origins and the remaining reference/continuation/consolidation gates +remain required. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 1b0617c1..69cb1161 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3028,3 +3028,18 @@ separate pending gate. All remaining reference/continuation/consolidation requirements stay open; milestone 6 remains **in progress**, `roadmapComplete=false`. See the [CMPM replacement mapping](COPPER68K_REFERENCE_QUALIFICATION.md#cmpm-alias-and-a7-semantic-consolidation--2026-10-07). + +The full-format indexed/chained private pipe gate now passes 4,692,480 programs +across 1,920 reports and 480 executions. All 66 structures are covered by source +faults and all 54 pointer-bearing structures by pointer-to-operand chains; +the 12 direct forms remain explicit chain exclusions. Software prefixes of +zero through three words and each retained-word edit survive real handler +changes and selected scalar/batch recovery. Sole capture/restore mutations +detect exactly 1,437,696 / 1,876,992 intended failures, with unchanged fixtures. +Complete source/key/weight/TRX/reason audits and five integrity controls pass. +This is private transport qualification, not physical pipe/cache timing. +Production source/packages and CMPM retirements remain unchanged; the prototype +is unimported. Wider indexed/chained nested provenance, foreign frames, other +origins and remaining roadmap gates stay required. Milestone 6 remains +**in progress**, `roadmapComplete=false`. +See the [indexed/chained pipe qualification](COPPER68K_REFERENCE_QUALIFICATION.md#full-format-indexed-and-chained-private-pipe-transport--2026-10-07). From 032a4667f69532c3f57199bd193b0e2066ca325c Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 12:41:57 +0300 Subject: [PATCH 104/211] Record private fault frame relocation defect and isolated correction --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 44 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 12 +++++ 2 files changed, 56 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 954f6b1c..2fd7d58e 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9006,3 +9006,47 @@ already-delivered CMPM retirements instead of replacing tests from an older snapshot. Indexed/chained nested trace-vector provenance, foreign frames, other origins and the remaining reference/continuation/consolidation gates remain required. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +#### Copied private frame and selected-stack relocation — 2026-10-07 + +A bounded discovery exposes an original-stack-address restriction in the +isolated MOVE operand-read prototype. A real handler copies every byte of the +92-byte private frame using 23 MOVE.L instructions, selects the relocated +frame with LEA A7, and executes RTE. Across EC020/A1200/020/030, MOVE B/W/L, +four stack states, CCR `00/1F`, all operand byte lanes and scalar/batch, +**672 programs pass and 224 supervisor-stack programs are unsupported**. +All unmoved controls and relocated user-stack cases pass. Every unsupported +identifier/reason points to the original RTE stack-equality guard. + +[MC68030UM section 8.1.13](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P2.pdf) +describes RTE validating and consuming the frame on the selected active +supervisor stack. For this interpreter-private image, fault-time A7 provenance +does not impose an original-address restriction: the completed source address +is serialized independently. A separate isolated correction removes that +restriction while retaining the provenance read, all other frame reads, +validation, saved effective address and suffix execution. The unchanged +fixture then passes **896 programs / 16 reports / four executions**, with zero +mismatches or unsupported cases and exactly one completed operand read. +The original guard remains a retained discriminating regression witness. + +Fresh affected retention passes **138,240 frame-entry/internal-load fault +programs / 160 reports / 40 executions**. Complete keys and weights match the +previous independently qualified frame-fault gate; actual per-test TRX output +and complete source identities agree. Defined HALT behavior, completed effects +and existing scalar/batch policy remain intact. Six integrity controls reject +omitted/changed fixtures, a wrong producer, an empty selection, changed weights +and unrelated failure reasons. Aggregate proof: +`operand020-moved-frame-proof.json`, SHA-256 +`719240a5ab6fa273f1402062249ddeb613e40a7b7fc826f81964a111ca7be4b3`. +Evidence roots are `audits/Operand020MovedFrameDiscovery` and +`audits/Operand020MovedFrameFix`; the sole CPU-file change and unchanged fixture +are independently verified against the frozen indexed-pipe parent. + +This qualifies selected private-image relocation within each interpreter +profile. It does not qualify foreign silicon images, cross-profile migration, +changed return SR/banks, all A7 aliases or general A/B continuations. The +indexed/chained nested-trace matrix continues separately on unchanged parent +CPU source. Broader qualification, fresh full CPU validation and affected +consumers remain required before importing the correction. Production CPU +source/packages, protected normal assemblies and CMPM retirements are +unchanged. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 69cb1161..828bd354 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3043,3 +3043,15 @@ is unimported. Wider indexed/chained nested provenance, foreign frames, other origins and remaining roadmap gates stay required. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [indexed/chained pipe qualification](COPPER68K_REFERENCE_QUALIFICATION.md#full-format-indexed-and-chained-private-pipe-transport--2026-10-07). + +Selected private-frame relocation now has a retained failing witness: 224 +supervisor-stack cases are unsupported while 672 controls/user-stack cases +pass. A separate isolated correction removes the original-SP restriction and +passes all 896 programs, retaining every frame read and the saved operand +address. Fresh affected retention passes 138,240 frame-entry/internal-load +fault programs; complete TRX/key/weight/source audits and six integrity +controls pass. The correction remains unimported. Indexed/chained nested trace +continues separately; foreign/cross-profile frames, broader return-SR/stack +and alias behavior, full CPU/consumer validation and remaining roadmap gates +stay required. Milestone 6 remains **in progress**, `roadmapComplete=false`. +See the [private-frame relocation evidence](COPPER68K_REFERENCE_QUALIFICATION.md#copied-private-frame-and-selected-stack-relocation--2026-10-07). From d64a87387f784226da3f97a037c823fbda971582 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 13:06:54 +0300 Subject: [PATCH 105/211] Qualify private recovery A7 aliases and returned CCR --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 55 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 14 +++++ 2 files changed, 69 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 2fd7d58e..9109bfe4 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9050,3 +9050,58 @@ CPU source. Broader qualification, fresh full CPU validation and affected consumers remain required before importing the correction. Production CPU source/packages, protected normal assemblies and CMPM retirements are unchanged. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +#### Private recovery with A7 aliases and edited CCR — 2026-10-07 + +The separately corrected private-frame relocation prototype passes +**139,776 programs / 192 reports / 48 executions** for A0/A7 indirect, +postincrement and predecrement sources, MOVE byte/word/long to D0/D7 and +MOVEA word/long to A0/A7. Four stack states, unmoved/copied frames, real +keep/CCR-edit handlers, every operand byte lane and scalar/batch are included. +CCR `00/1F` covers all forms; the canonical A7 postincrement selection covers +all 32 CCR values separately. The handlers copy the complete private image, +select A7 with LEA and optionally invert all five saved CCR bits. Privilege, +bank and trace bits remain unchanged in this gate. + +Independently chosen operand locations remain fixed through relocation and +source/destination aliasing. The test checks completed predecrement effects, +the pending postincrement suffix, destination A7 overrides, word MOVEA sign +extension, partial data registers, untouched state/memory, returned CCR +preservation and the following MOVEQ sentinel. A7 byte strides and MOVE/MOVEA +flag expectations follow +[M68000PM sections 2.2.4/2.2.5 and the instruction definitions](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf). +The exact literal RTE validation/load prefix already qualified by the frame +fault gate precedes exactly one pending operand read. + +An initial fixture records **139,520 passing / 256 mismatching cases**. The +relocated MSP frame overlaps the user-M predecrement word operand: legitimate +frame validation/load word reads share its address and width. Counting all +address matches as operand reads incorrectly labels those cases as replay. +A fresh fixture-only repair compares the entire independently qualified +59-read frame prefix followed by one operand read. Every original failure +identifier/reason and the exact fixture repair remain pinned; no CPU change +or acceptance-scope reduction resolves the invalid counting assumption. + +Sole CPU-file mutations retain the repaired fixture unchanged. Giving byte A7 +a one-byte postincrement detects exactly **8,192 failures**, retaining 131,584 +passing programs. Clearing the returned X flag detects exactly **69,888 +failures**, retaining 69,888 passing programs. Independent expectations check +each failing A7 or SR value, including overlapping-frame zero operands and +MOVEA's unchanged CCR. All 226 source/project inputs, actual TRX results/output, +weighted keys and precise reasons are audited. Six corruption controls reject +missing/changed fixtures, wrong producers, empty selections, changed weights +and unrelated failure reasons. Aggregate proof: +`operand020-stack-alias-proof.json`, SHA-256 +`1b1888711fe5f23ab9c8cd47b38da00093f5ada1dade7fad4de7a7b2e1686716`. + +Evidence: `audits/Operand020StackAliasV2` and +`mutations/Operand020StackAlias/{Stride,Extend}`. The earlier invalid fixture +remains `audits/Operand020StackAlias`. CPU and all preexisting sources are +unchanged from the separately qualified relocation correction; its fresh +frame-fault retention remains applicable without claiming a new full replay. +The indexed/chained nested-trace run remains separate on unchanged older CPU +source. Returned privilege/bank/trace changes, foreign/cross-profile frames, +other origins and remaining roadmap gates still require qualification before +production import and fresh full CPU/consumer validation. Production CPU +source/packages, protected normal assemblies and CMPM retirements remain +unchanged. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 828bd354..96434f03 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3055,3 +3055,17 @@ continues separately; foreign/cross-profile frames, broader return-SR/stack and alias behavior, full CPU/consumer validation and remaining roadmap gates stay required. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [private-frame relocation evidence](COPPER68K_REFERENCE_QUALIFICATION.md#copied-private-frame-and-selected-stack-relocation--2026-10-07). + +The isolated relocation correction now passes 139,776 stack/CCR alias programs +across 192 reports and 48 executions: A0/A7 indirect/postincrement/predecrement, +MOVE to D0/D7, MOVEA to A0/A7, four stack states, copied/unmoved frames, real +CCR edits, all operand lanes and scalar/batch. Canonical A7 postincrement +covers all 32 CCR values. Stride/X mutations detect exactly 8,192 / 69,888 +intended failures; complete source/TRX/key/weight/reason and six corruption +controls pass. The 256 earlier overlap/read-count fixture failures and exact +fixture-only repair remain retained. CPU source is unchanged from the isolated +relocation correction and is unimported. Nested indexed trace continues on its +separate parent; privilege/bank/trace edits, foreign frames, other origins, +full CPU/consumers and remaining roadmap gates stay required. Milestone 6 +remains **in progress**, `roadmapComplete=false`. +See the [A7 alias and CCR record](COPPER68K_REFERENCE_QUALIFICATION.md#private-recovery-with-a7-aliases-and-edited-ccr--2026-10-07). From 196c7de00a93f181488046cc9b79e85b04fbbef3 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 17:29:08 +0300 Subject: [PATCH 106/211] Qualify full indexed and chained nested trace provenance --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 54 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 ++++++ 2 files changed, 69 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 9109bfe4..8072be28 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9105,3 +9105,57 @@ other origins and remaining roadmap gates still require qualification before production import and fresh full CPU/consumer validation. Production CPU source/packages, protected normal assemblies and CMPM retirements remain unchanged. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +#### Full-format indexed and chained nested trace provenance — 2026-10-07 + +The isolated unchanged-CPU parent passes **6,137,856 programs / 3,456 reports / +864 executions** with zero mismatches, unsupported cases or skips in the +selected nested-trace matrix. Direct completion contributes 506,880 programs, +source-fault recovery 1,317,888 and pointer-to-operand chains 4,313,088. All 66 +full-format structural forms have direct/source coverage; all 54 +pointer-bearing forms have chained coverage, with 12 direct chain exclusions. +An/PC bases, MOVE byte/word/long and MOVEA word/long, four stack states, CCR +`00/1F`, all selected pointer/operand/vector byte lanes and scalar/batch are +included. Canonical indexes are signed `D2.W=-8`; retained register/boundary +and simple-address trace gates provide their separately qualified breadth. + +Real source handlers overwrite the original opcode before return. The resumed +instruction completes its saved operand and register/flag suffix once, then +enters trace processing. Vector handlers exercise a software input buffer, +persistent refault and changed VBR. The test independently verifies the +original instruction PC/opcode and exact next PC, post-instruction SR, format-2 +trace frame, nested private bus-error context, stack selection, completed +pointer/operand reads, the saved original vector address and following +instruction. The architectural trace expectations follow +[MC68030UM section 8.1.7](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P2.pdf). +Private serialized provenance is distinct from physical pipeline overlap. + +Two sole CPU-file mutations retain every fixture unchanged. Losing original +PC provenance during resumed completion produces exactly **5,630,976 +failures**, retaining 506,880 direct controls. Corrupting the saved vector-read +width produces exactly **6,137,856 failures**. Every identifier and precise +frame-field reason is independently enumerated. Complete source inventories, +all 864 actual TRX results and per-test output, weighted report keys and logical +totals agree. Six corruption controls precisely reject missing/changed fixtures, +wrong producers, empty selections, changed weights and unrelated failure +reasons. Aggregate proof: `operand020-indexed-trace-proof.json`, SHA-256 +`3d67385d5fb57b1f0a702bf1ec9ed887262668ba3b1a70b86c3d16ba0fbc06ca`. + +Evidence: `audits/Operand020IndexedTraceV2` and +`mutations/Operand020IndexedTrace/{PC,Width}`. The original CS0136 local-name +collision is compile-only evidence; its repair changes only the fixture local +name. An initial planned aggregate omitted MOVEA widths when adding the five +sizes. Per-case catalogs/weights already included them; the aggregate was +corrected to 6,137,856 before any complete gate was accepted, without changing +fixtures, selections or acceptance scope. All 224 preexisting parent inputs +remain unchanged; the new fixture is the 225th source/project input. + +This proof applies to the unchanged parent CPU, separately from the relocated +frame correction and its stack-alias gate. They must be combined in a fresh +source snapshot and checked against their complete qualified selections. +Foreign/cross-profile frames, returned privilege/bank/trace changes, other +origins and remaining reference/continuation/consolidation gates remain open. +Production CPU source/packages, protected normal assemblies and CMPM retirements +are unchanged. No fresh full CPU/consumer replay or physical timing +qualification is claimed. Milestone 6 remains **in progress**, +`roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 96434f03..a851d301 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3069,3 +3069,18 @@ separate parent; privilege/bank/trace edits, foreign frames, other origins, full CPU/consumers and remaining roadmap gates stay required. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [A7 alias and CCR record](COPPER68K_REFERENCE_QUALIFICATION.md#private-recovery-with-a7-aliases-and-edited-ccr--2026-10-07). + +Full-format indexed/chained nested-trace qualification now passes 6,137,856 +programs, 3,456 reports and 864 executions. All 66 direct/source structures +and 54 pointer chains are covered; the 12 direct chain exclusions remain +explicit. Real opcode-overwrite and buffer/refault/VBR handlers preserve +original provenance, post-instruction SR, completed reads and trace frames. +PC/width mutations detect exactly 5,630,976 / 6,137,856 intended failures; +complete source/TRX/key/weight/reason audits and six corruption controls pass. +Compile-only fixture repair and the corrected pre-acceptance planned total are +retained. This unchanged-CPU parent remains separate from the qualified +relocation/stack-alias candidate until a fresh combined snapshot and complete +selected replay pass. Foreign frames, privilege/bank/trace edits, other origins, +full CPU/consumer validation and remaining gates stay required. Milestone 6 +remains **in progress**, `roadmapComplete=false`. +See the [indexed nested-trace record](COPPER68K_REFERENCE_QUALIFICATION.md#full-format-indexed-and-chained-nested-trace-provenance--2026-10-07). From 44637f315bf0df4fab0ec26f60a35e31f9c391e5 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 17:58:21 +0300 Subject: [PATCH 107/211] Record private returned privilege and stack-mode qualification --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 43 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 13 ++++++ 2 files changed, 56 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 8072be28..d30aa7c1 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9159,3 +9159,46 @@ Production CPU source/packages, protected normal assemblies and CMPM retirements are unchanged. No fresh full CPU/consumer replay or physical timing qualification is claimed. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +#### Private recovery with returned privilege and stack modes — 2026-10-07 + +A new isolated discovery executes all sixteen original/returned S/M bank +pairs, with real handlers editing the saved SR. MOVE byte/word/long to D0 and +MOVEA word/long to A0 use independently chosen `(A0)` operands. CCR `00/1F`, +copied/control private frames, every operand fault lane, scalar/batch and +EC020/A1200/020/030 are included: **6,656 programs / 64 reports / 16 executions**. +It checks the literal frame-read prefix, one pending operand read, exact +registers/flags/PC, all three stack pointers, untouched memory and a following +MOVEQ sentinel. Changing only M already works; changing S is rejected by the +original private validator: **3,328 passing / 3,328 unsupported / zero +mismatches**. Every rejected opcode/PC/profile and case identifier is audited. + +The validator incorrectly derives the faulted data-cycle supervisor bit from +the returned SR. The saved SSW describes the suspended cycle independently +of that SR. An isolated one-file correction preserves direction, size and +reference-kind checks, validates the SSW's own supervisor bit and requires +supervisor data space for trace-vector reads. The discovery fixture is +unchanged; all **6,656 programs pass**. This follows the distinction between +RTE's restored context and the saved fault-cycle address space in +[MC68030UM-P2 sections 8.1.13 and 8.2.1](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P2.pdf). +The serialized `C022` frame remains an interpreter-private policy; this gate +does not qualify undocumented silicon internal-state behavior. + +Complete inventories of 228 source/project inputs, actual TRX selections and +output, all weighted combinations and exact original rejection reasons are +verified. Six corruption controls reject missing/changed fixtures, wrong +producers, empty selections, altered weights and unrelated failure reasons. +Evidence: `audits/Operand020ReturnBankDiscovery`, +`audits/Operand020ReturnBankFix` and `integrity/Operand020ReturnBank`. +Aggregate proof: `operand020-return-bank-proof.json`, SHA-256 +`5e39f0ca0429646b0b26dbea310b8cc40784b677ee1e33444a4c05616dc2b6e5`. + +The earlier combined recovery run continues on its unchanged relocation-only +CPU. It cannot qualify this additional validator correction. Persistent +refaults still need independent tests for preservation of the original saved +cycle FC; returned trace edits, wider source/destination aliases and indexed +chains, foreign frames and other origins remain required. A fresh affected +replay and full CPU/consumer qualification are still needed before import. +Production CPU/packages, normal assemblies and CMPM retirements remain +unchanged. No publication is authorized or performed. Milestone 6 remains +**in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index a851d301..b24f76be 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3084,3 +3084,16 @@ selected replay pass. Foreign frames, privilege/bank/trace edits, other origins, full CPU/consumer validation and remaining gates stay required. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [indexed nested-trace record](COPPER68K_REFERENCE_QUALIFICATION.md#full-format-indexed-and-chained-nested-trace-provenance--2026-10-07). + +Returned privilege/stack-mode discovery now records 3,328 passing controls and +3,328 precise unsupported S-change cases. A sole private validator correction +separates saved cycle FC from returned SR and passes all 6,656 programs across +64 reports and 16 executions, with the fixture unchanged and six corruption +controls passed. All sixteen S/M bank transitions, copied/control frames, +MOVE/MOVEA `(A0)`, CCR00/1F, operand lanes and scalar/batch are covered. +Persistent refault FC, returned trace edits, wider aliases/indexed cases and +foreign origins remain required. The combined run uses the preceding CPU +snapshot and cannot qualify this additional correction. No production import, +publication or full-replay claim is made. Milestone 6 remains **in progress**, +`roadmapComplete=false`. See the +[returned privilege and stack-mode record](COPPER68K_REFERENCE_QUALIFICATION.md#private-recovery-with-returned-privilege-and-stack-modes--2026-10-07). From 7fe695fd6a5c3785d6399fa34b5216842047818a Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 18:15:37 +0300 Subject: [PATCH 108/211] Qualify private refault cycle provenance and scope restoration --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 55 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 14 +++++ 2 files changed, 69 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index d30aa7c1..761c32df 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9202,3 +9202,58 @@ replay and full CPU/consumer qualification are still needed before import. Production CPU/packages, normal assemblies and CMPM retirements remain unchanged. No publication is authorized or performed. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +#### Private recovery with changed privilege and repeated faults — 2026-10-07 + +The returned-bank validator candidate now has a separate repeated-fault +discovery: **13,312 programs / 128 reports / 32 executions**. All sixteen +original/returned S/M pairs, copied/control frames, MOVE byte/word/long and +MOVEA word/long `(A0)`, CCR `00/1F`, operand fault lanes and scalar/batch are +covered. A real first handler edits the saved SR and changes the bus-error +vector to a second real handler. One or two further failed pending reads +precede successful recovery. Each retry checks an independently constructed +92-byte private image, original opcode/PC/address/width/FC, all stacks and +registers, exact frame-read/write order, exception sequence and eventual +operand/flags/PC plus a following sentinel. + +The discovery retains **6,656 passing / 6,656 mismatching / zero unsupported** +programs. Every S-changing case incorrectly stores the returned privilege's +FC in the new frame. The expected original FC and actual value are checked +for every failure. A sole CPU-file correction scopes the saved SSW FC around +pending data/pointer reads and restores the previous context in `finally`, +including exceptional exits; trace-vector accesses select supervisor data +space independently. It preserves the completed read prefix and pending +suffix without instruction replay. The unchanged fixture passes all +**13,312 programs**. This distinguishes the restored SR from the faulted +data cycle, and follows deallocation/new-frame refault behavior in +[MC68030UM-P2 sections 8.1.13, 8.2.1 and 8.2.3](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P2.pdf). + +A sole scope-leak mutation removes the context restoration. It detects +exactly **6,656 failures**, retaining 6,656 passing cases; every original +supervisor-source case fails the next independently checked initial cycle +provenance. Fixtures remain unchanged. Complete source inventories (229 +inputs), actual TRX selections/output, weighted combinations and exact +baseline/mutation identifiers/reasons pass their audits. Six refault +corruption controls reject missing/changed fixtures, wrong producers, empty +selections, changed weights and unrelated failure reasons. + +Fresh affected retention on this corrected CPU passes **800,768 programs / +752 reports / 188 executions**: returned-bank edits, A7/CCR aliases, frame +entry/internal-load faults and the preceding simple nested-trace matrix. +All four source-specific parent proofs, unchanged fixtures, actual executions +and complete report dictionaries are checked. Six additional integrity +controls reject producer/source-identity changes, empty selections, omitted +executed-source inventory, missing reports and altered weights. +Evidence: `audits/Operand020ReturnRefault{Discovery,Fix}`, +`mutations/Operand020ReturnRefault/ScopeLeak` and the fix's `retained` directory. +Aggregate proof: `operand020-return-refault-proof.json`, SHA-256 +`ef429454056ae53132f6b49324a5544b3997ba6b9f4bc3d003255e73fba11046`. + +This gate qualifies serialized interpreter-private cycle provenance, not +physical function-code signals or enabled-MMU behavior. The large combined +replay uses the preceding CPU and cannot qualify these additional changes. +Changed-return trace, wider indexed/chained/alias cases, foreign frames, +other origins and remaining roadmap gates stay required, as does fresh full +CPU/consumer validation before import. Production CPU/packages, normal +assemblies and CMPM retirements remain unchanged. No package is published. +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index b24f76be..2a8bf98f 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3097,3 +3097,17 @@ snapshot and cannot qualify this additional correction. No production import, publication or full-replay claim is made. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [returned privilege and stack-mode record](COPPER68K_REFERENCE_QUALIFICATION.md#private-recovery-with-returned-privilege-and-stack-modes--2026-10-07). + +Repeated-fault coverage after saved privilege edits now passes 13,312 programs +(128 reports / 32 executions). It retains 6,656 exact original FC mismatches +and detects 6,656 scope-leak mutation failures with the fixture unchanged. +All sixteen S/M pairs, one/two refaults, copied/control frames, MOVE/MOVEA +`(A0)`, CCR00/1F, operand lanes and scalar/batch are covered. A scoped private +saved-cycle FC correction also passes fresh affected retention: 800,768 +programs / 752 reports / 188 executions against four pinned parent proofs. +Both complete audits and twelve evidence corruption controls pass. The older +large combined run does not qualify this newer CPU. Changed-return trace, +wider aliases/indexed chains, foreign origins and full CPU/consumer gates +remain required; production source/packages remain unchanged. Milestone 6 +remains **in progress**, `roadmapComplete=false`. See the +[changed-privilege repeated-fault record](COPPER68K_REFERENCE_QUALIFICATION.md#private-recovery-with-changed-privilege-and-repeated-faults--2026-10-07). From 34b4e8b86e26c2dfb83536cf4a87a0fcde114b67 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 18:35:07 +0300 Subject: [PATCH 109/211] Record private returned trace policy and unresolved reference outcomes --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 63 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 16 +++++ 2 files changed, 79 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 761c32df..091251b2 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9257,3 +9257,66 @@ other origins and remaining roadmap gates stay required, as does fresh full CPU/consumer validation before import. Production CPU/packages, normal assemblies and CMPM retirements remain unchanged. No package is published. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +#### Private returned-trace policy and unresolved architectural outcomes — 2026-10-07 + +On the unchanged refault-corrected private CPU, **188,416 programs / 512 +reports / 128 executions** pass the prototype's resumed-suffix trace policy. +The selection combines all sixteen original/returned S/M pairs and all four +initial/returned T1 pairs with copied/control private frames, MOVE byte/word/ +long and MOVEA word/long `(A0)`, source/vector fault lanes and scalar/batch. +CCR `00/1F` covers all forms; a separate canonical MOVE.L selection covers +all 32 CCR values. Real source handlers install the nested vector handler, +overwrite the original opcode, optionally copy the complete frame and edit +its saved SR. Returned trace-off cases finish without a trace-vector read. +Trace-on cases use real buffer/refault/VBR handlers, check post-suffix saved +SR/next PC/original instruction address, complete source reads exactly once, +recover the saved vector access and run a following sentinel. + +Sole CPU-file mutations suppress tracing or give the trace-vector fault user +data FC. Each detects exactly **175,104 mismatches**, retaining **13,312 +trace-off controls**. All fixtures are unchanged; complete 230-source +inventories, actual execution rosters/output, weighted keys and every precise +failure identifier/reason are verified. Six corruption controls reject +missing/changed fixtures, wrong producers, empty selections, altered weights +and unrelated reasons. An initial control runner correctly rejected the +wrong producer but expected an inherited diagnostic spelling; the fresh V2 +changes only runner expectations/output location and retains that rejection. +The report label `T0` means T1=false, not hardware T0. The producer's scope +text is inherited from its refault predecessor; its pinned actual filter, +fixture, complete TRX roster and case keys define this trace selection. + +**This is not completion of changed-T1 architectural qualification.** +[MC68030UM-P2 section 8.1.7](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P2.pdf) +selects tracing at instruction start and defers a pending trace across a bus +fault until the suspended instruction completes. The prototype instead uses +the returned SR for its resumed suffix. Whether a handler's edited T1 should +change tracing of that suspended instruction requires a targeted independent +reference; ordinary RTE/SR code inspection does not settle that distinction. +Exactly **86,528** selected programs leave T1 unchanged and **101,888** change +it. The latter remain explicitly **reference-unresolved** despite passing +their private expectations. The verifiers and aggregate proof require +`editedT1HardwareQualified=false` and +`architecturalChangedTraceGatePassed=false`; these rows cannot count as a +completed architectural gate. + +The next discriminating reference must cause a genuine operand bus fault, +preserve the reference processor's own internal frame, edit only stacked T1 +before RTE and observe trace before the following instruction for all four +initial/returned T1 combinations. Keep privilege, operands, fault repair and +other frame fields fixed first. Neither injecting the private `C022` image +into another CPU nor replaying the prototype's expected results qualifies +this behavior. Broader bank/frame/trace-vector cases follow the resolved +canonical result; hardware T0 and physical pipeline behavior remain separate. + +Evidence: `audits/Operand020ReturnTrace`, +`mutations/Operand020ReturnTrace/{Suppress,VectorFc}` and +`integrity/Operand020ReturnTraceV2`. Private-policy proof: +`operand020-return-trace-proof.json`, SHA-256 +`febb36a5e0c9c119b44be4f232c1952bbba5176ee1ca2d397799a91042d58544`. +CPU/preexisting sources are unchanged from the separately qualified refault +candidate; its affected retention remains separately identified. No new full +CPU/consumer replay, production import or publication is claimed. The earlier +large combined run uses its preceding source snapshot. Indexed/alias/foreign +origins and all remaining roadmap gates stay required. Milestone 6 remains +**in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 2a8bf98f..c4276c81 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3111,3 +3111,19 @@ wider aliases/indexed chains, foreign origins and full CPU/consumer gates remain required; production source/packages remain unchanged. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [changed-privilege repeated-fault record](COPPER68K_REFERENCE_QUALIFICATION.md#private-recovery-with-changed-privilege-and-repeated-faults--2026-10-07). + +Returned-trace private-policy coverage passes 188,416 programs across 512 +reports and 128 executions on unchanged refault-corrected CPU source. +Suppression/user-vector-FC mutations each detect 175,104 failures, retaining +13,312 trace-off controls; complete audits and six corruption controls pass. +This does **not** complete architectural changed-T1 qualification: 86,528 +cases preserve T1 and 101,888 edit it, with the latter explicitly reference- +unresolved. The manual selects tracing at instruction start; the prototype +uses the returned SR for its resumed suffix. A genuine reference-generated +fault/frame with only stacked T1 edited must settle the four canonical trace +combinations before promoting that behavior. The proof requires the hardware +qualification flag to remain false. Hardware T0, wider indexed/alias/foreign +cases, full CPU/consumers and remaining gates stay required. No production +import or publication is claimed. Milestone 6 remains **in progress**, +`roadmapComplete=false`. See the +[returned-trace policy and reference-gap record](COPPER68K_REFERENCE_QUALIFICATION.md#private-returned-trace-policy-and-unresolved-architectural-outcomes--2026-10-07). From 6b643201f59a1dc02a2521da5b1f2721d08e530d Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 18:47:06 +0300 Subject: [PATCH 110/211] Record complete combined private recovery qualification --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 30 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 11 +++++++ 2 files changed, 41 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 091251b2..ce10c2da 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9320,3 +9320,33 @@ CPU/consumer replay, production import or publication is claimed. The earlier large combined run uses its preceding source snapshot. Indexed/alias/foreign origins and all remaining roadmap gates stay required. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +#### Complete combined private recovery replay — 2026-10-07 + +A fresh combined snapshot passes **11,109,248 programs / 5,744 reports / +1,436 executions**, with zero mismatching, unsupported or untested selected +cases. Its 227 source inputs combine the relocation-corrected private CPU +and complete frozen selections for indexed software pipes (4,692,480), +indexed/chained nested trace (6,137,856), stack aliases and returned CCR +(139,776), moved frames (896), and entry/internal-load faults (138,240). +Every parent proof, source identity, report key/weight/result and actual TRX +execution roster is checked. Seven corruption controls reject omitted or +changed fixtures, wrong parent identities, changed weights, wrong producers, +empty executions and wrong selections. Protected normal assemblies remain +unchanged. Prior mutation proofs remain separately identified; mutations +were not repeated by this combined passing replay. + +Evidence: `audits/Operand020CombinedRecovery` and +`integrity/Operand020CombinedRecovery/Full`. Aggregate proof: +`operand020-combined-recovery-proof.json`, SHA-256 `002270baccebfff53ca623f20eadccdc70200254e3585e067cb86a129ffa56e5`. + +This replay qualifies only its frozen relocation-corrected private source. +It does not qualify the later returned-bank validator or scoped refault-FC +corrections, which have their own source-specific evidence. The private +returned-trace policy still has 101,888 reference-unresolved changed-T1 +cases. Interpreter frames/software pipes are distinct from physical pipeline, +cache or silicon timing qualification. Wider origins, foreign frames, full +CPU/consumer validation and the remaining roadmap gates stay required. +Production CPU source and packages are unchanged; preserve the production +CMPM retirements on any future import. Milestone 6 remains **in progress**, +`roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index c4276c81..9d437c13 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3127,3 +3127,14 @@ cases, full CPU/consumers and remaining gates stay required. No production import or publication is claimed. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [returned-trace policy and reference-gap record](COPPER68K_REFERENCE_QUALIFICATION.md#private-returned-trace-policy-and-unresolved-architectural-outcomes--2026-10-07). + +The fresh combined private recovery replay passes 11,109,248 programs across +5,744 reports and 1,436 executions, with exact complete parent selections, +227 source identities and seven corruption controls verified. This closes +the combined replay gate for the relocation-corrected snapshot only; later +returned-bank/refault corrections and changed-T1 architectural uncertainty +remain separately identified. Full CPU/consumer validation, wider origins, +foreign frames and remaining roadmap gates are still required. No production +CPU import or publication occurs. Milestone 6 remains **in progress**, +`roadmapComplete=false`. See the +[combined recovery record](COPPER68K_REFERENCE_QUALIFICATION.md#complete-combined-private-recovery-replay--2026-10-07). From 236675447efa9edb38b1003dec674a3f01ecce33 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 19:00:12 +0300 Subject: [PATCH 111/211] Execute canonical 030 fault recovery trace reference --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 66 ++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 12 ++ scripts/reference/m68030-return-trace.cpp | 143 ++++++++++++++++++ ...t-copper68k-030-return-trace-reference.ps1 | 110 ++++++++++++++ 4 files changed, 331 insertions(+) create mode 100644 scripts/reference/m68030-return-trace.cpp create mode 100644 scripts/test-copper68k-030-return-trace-reference.ps1 diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index ce10c2da..6996c473 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9350,3 +9350,69 @@ CPU/consumer validation and the remaining roadmap gates stay required. Production CPU source and packages are unchanged; preserve the production CMPM retirements on any future import. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +#### Executed 030 returned-trace reference disagreement — 2026-10-07 + +The maintained `scripts/test-copper68k-030-return-trace-reference.ps1` and +`scripts/reference/m68030-return-trace.cpp` now execute four canonical +initial/returned T1 combinations plus two direct controls against pinned +WinUAE source `5d22d33632646efc3f747f03e82d28353e52722e`. They use unchanged +hardware-bus-error/page-fault helpers, format-B frame construction, SR/trace +helpers, generated `_32` MOVE/RTE/MOVEQ instructions, access-history macros +and the complete previous MMU030 retry loop. Physical transport faults once +on the aligned MOVE.L `(A0)` read. A guest MOVE.W changes only stacked T1; +every other byte of the reference-generated 92-byte frame is checked unchanged. +RTE repairs the access, reuses its completed value and completes MOVE with +exactly two attempted reads and one successful read. No Copper68k private +frame is injected. A following MOVEQ writes D7=`2A` as the trace-order marker. + +| Initial T1 | Returned T1 | Executed software-reference observation | +| --- | --- | --- | +| 0 | 0 | No trace before the bounded stopping point | +| 0 | 1 | Trace at PC `1004`, after following MOVEQ; D7=`2A` | +| 1 | 0 | No trace before the bounded stopping point | +| 1 | 1 | Trace at PC `1004`, after following MOVEQ; D7=`2A` | + +The initial trace-on boundary is already scheduled (`DOTRACE`). Its direct +no-fault control traces at PC `1002`, immediately after MOVE, with D7 still +zero; the trace-off direct control executes both sentinels without tracing. +These controls distinguish fault-recovery scheduling from a general observer +delay. Fresh execution and a native replay reproduce all six exact outcomes. +Seven corruption controls reject missing fixtures, altered extracted functions, +incomplete identities, wrong producers, empty results, altered outcomes and +false architectural-qualification flags. An initial control runner expected +PowerShell's wrong missing-file diagnostic spelling; its genuine rejection is +retained, and V2 corrects only that runner expectation/output location. + +This is **software-reference disagreement, not architectural qualification**. +Returned trace-on recovery differs from the prototype's immediate post-suffix +trace, including unchanged T1=1. Therefore agreement for unchanged T1 cannot +be inferred from this reference either. The earlier private-policy counts +remain recorded; their passing outcomes must not be promoted as a trace +architecture gate. The manual's instruction-start/deferred-trace rule and +handler-edited T1 still need a reconciled independent reference or hardware +observation. Do not change the CPU to imitate this software delay. + +The observer is explicitly limited to supervisor ISP, CCR=0, aligned MOVE.L +`(A0)`, one repaired physical read and this previous MMU030 loop. It executes +the exact trace portion of `do_specialties`, not the complete host/IRQ routine. +Unavailable transport paths fail; translation, caches, physical pipeline/timing, +interrupts and hardware are unqualified. Compiler warnings and initial build +repairs remain in the exploratory outputs. Production normal assemblies retain +their protected hashes; no production CPU edit or full CPU/consumer claim occurs. + +Run with a fresh output directory: + +```powershell +./scripts/test-copper68k-030-return-trace-reference.ps1 -OutputDirectory +./scripts/test-copper68k-030-return-trace-reference.ps1 -OutputDirectory -ValidateReportsOnly +``` + +The required pinned checkout and MSVC toolchain must be present; missing +inputs, empty selections and changed outcomes fail. Evidence: +`reference/ReturnedTrace030MaintainedV2`, +`integrity/ReturnedTrace030ReferenceV2`, proof +`returned-trace-reference-proof.json`, SHA-256 `401a50583f96d5253c3f5304319f143ae546975052c97eda17058150abbe6f32`. +The next gate must reconcile the trace point before widening the canonical +case to other banks, operands and frame origins. No publication or production +import occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 9d437c13..679a190f 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3138,3 +3138,15 @@ foreign frames and remaining roadmap gates are still required. No production CPU import or publication occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [combined recovery record](COPPER68K_REFERENCE_QUALIFICATION.md#complete-combined-private-recovery-replay--2026-10-07). + +A maintained native 030 observer now executes reference-generated operand +fault/frame/RTE recovery for all four initial/returned T1 pairs, with two +direct controls, native replay and seven corruption controls. Returned T1=1 +traces after the following MOVEQ in the previous WinUAE MMU030 loop, while +the direct trace-on control traces immediately after MOVE. This disagrees +with immediate resumed-suffix tracing, including unchanged T1=1; it does not +qualify that private policy or establish hardware behavior. Reconcile the +software/manual trace point before promotion or wider trace qualification. +No production CPU import/publication occurs. Milestone 6 remains **in progress**, +`roadmapComplete=false`. See the +[executed reference disagreement](COPPER68K_REFERENCE_QUALIFICATION.md#executed-030-returned-trace-reference-disagreement--2026-10-07). diff --git a/scripts/reference/m68030-return-trace.cpp b/scripts/reference/m68030-return-trace.cpp new file mode 100644 index 00000000..c9315172 --- /dev/null +++ b/scripts/reference/m68030-return-trace.cpp @@ -0,0 +1,143 @@ +// Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT +// Canonical 030 reference-generated bus error, guest saved-T1 edit and RTE. +// Untouched extracted fault/frame/SR/opcode/retry-loop functions; physical +// memory transport and exact trace-only do_specialties fragment. No enabled +// translation, IRQ, cache/pipeline timing, or hardware qualification. +#include +#include +#include +#include +#include +using uae_u8=uint8_t;using uae_u16=uint16_t;using uae_u32=uint32_t; +using uae_s8=int8_t;using uae_s16=int16_t;using uae_s32=int32_t;using uaecptr=uint32_t; +#define REGPARAM2 +#define STATIC_INLINE static inline +#define CYCLE_UNIT 2 +#define _T(x) x +#define MMUDEBUG 0 +#define TRY(x) try +#define CATCH(x) catch(int x) +#define ENDTRY +#define STOPTRY +#define THROW(x) throw int(x) +#define SPCFLAG_TRACE 0x40 +#define SPCFLAG_DOTRACE 0x80 +#define SPCFLAG_TRAP 0x400 +#define SPCFLAG_INT 8 +#define SPCFLAG_DOINT 0x100 +#define CPU_HALT_CPU_STUCK 1 +#define CPU_HALT_DOUBLE_FAULT 2 +#define sz_byte 0 +#define sz_word 1 +#define sz_long 2 +#define m68k_areg(r,n) ((r).a[n]) +#define m68k_dreg(r,n) ((r).d[n]) +struct Regs { + uae_u32 a[8]{},d[8]{},usp{},isp{},msp{},pc{},instruction_pc{},trace_pc{},vbr{},mmu_ssw{},mmu_fault_addr{},sfc{},dfc{},wb3_data{},wb2_address{},fp_ea{},fp_opword{},mmu_effective_addr{},pcr{},fp_unimp_ins{},ir{}; + uae_u16 sr{};int m{},s{},t0{},t1{},intmask{},ipl[1]{},ipl_pin{},stopped{},spcflags{},opcode{},irc{},instruction_cnt{},wb2_status{},wb3_status{},read_buffer{},write_buffer{},fpu_exp_pre{},fp_unimp_pend{}; + int prefetch020_valid[3]{},prefetch020[3]{},pipeline_r8[2]{},pipeline_pos{},pipeline_stop{}; +}regs; +struct Prefs{int cpu_model=68030,mmu_model=68030,cachesize{},cpu_compatible{},cpu_cycle_exact{},fpu_model{};}currprefs; +struct flag_struct{int cznv{},x{};}regflags; +int xflag,nflag,zflag,vflag,cflag; +#define SET_XFLG(x) (xflag=(x)) +#define SET_NFLG(x) (nflag=(x)) +#define SET_ZFLG(x) (zflag=(x)) +#define SET_VFLG(x) (vflag=(x)) +#define SET_CFLG(x) (cflag=(x)) +#define GET_XFLG() xflag +#define GET_NFLG() nflag +#define GET_ZFLG() zflag +#define GET_VFLG() vflag +#define GET_CFLG() cflag +#define CLEAR_CZNV() (cflag=zflag=nflag=vflag=0) +bool m68k_accurate_ipl=false,mmu_debugger=false,ismoves030=false,islrmw030=false,bBusErrorReadWrite=false,mmu030_retry=false; +int mmu030_idx,mmu030_idx_done,mmu030_opcode,mmu030_opcode_stageb,mmu030_fake_prefetch,cpu_cycles; +uaecptr mmu030_fake_prefetch_addr,mm030_stageb_address,last_fault_for_exception_3; +uae_u16 mmu030_state[3];uae_u32 mmu030_data_buffer_out,mmu030_disp_store[2],mmu030_fmovem_store[2],mmu040_move16[4]; +struct mmu030_access{uae_u32 val;};mmu030_access mmu030_ad[10]; +struct{int reg=-1;}mmufixup[2]; +std::unordered_map mem; +std::vector events;std::vector originalFrame,editedFrame; +bool faultOnce;int attempts,reads,steps,traceCount;uae_u32 tracePc,traceD7;int returnT1; +[[noreturn]] void unavailable(){throw std::runtime_error("Out-of-scope path");} +void set_special(int x){regs.spcflags|=x;}void unset_special(int x){regs.spcflags&=~x;} +void mmu_set_super(int){};int mmu030fixupreg(int){return 0;}void mmu030fixupmod(int x,int,int){if(x)unavailable();} +uaecptr m68k_getpc(){return regs.pc;}uaecptr m68k_getpci(){return regs.pc;} +void m68k_setpci(uaecptr x){regs.pc=x;}void m68k_incpci(int n){regs.pc+=n;} +void mmu030_hardware_bus_error(uaecptr,uae_u32,bool,bool,int); +uae_u32 read(uaecptr a,int w){ + if(a==0x4200&&w==4){attempts++;if(faultOnce){faultOnce=false;events.push_back(1);mmu030_hardware_bus_error(a,0,true,false,sz_long);}reads++;events.push_back(4);} + uae_u32 v=0;for(int n=0;nsecond;}return v; +} +void put(uaecptr a,uae_u32 v,int w){for(int n=0;n>(8*(w-1-n)));} +uae_u32 uae_mmu030_get_long(uaecptr a){return read(a,4);}uae_u32 uae_mmu030_get_word(uaecptr a){return read(a,2);}uae_u32 uae_mmu030_get_iword(uaecptr a){return read(a,2);} +void uae_mmu030_put_word(uaecptr a,uae_u32 v){put(a,v,2);} +uae_u32 get_word_mmu030(uaecptr a){return read(a,2);}uae_u32 get_long_mmu030(uaecptr a){return read(a,4);} +uae_u32 x_get_long(uaecptr a){return read(a,4);}void x_put_long(uaecptr a,uae_u32 v){put(a,v,4);}void x_put_word(uaecptr a,uae_u32 v){put(a,v,2);} +uae_u32 uae_mmu030_get_long_fcx(uaecptr a,int fc){if(fc!=5)unavailable();return read(a,4);} +uae_u32 uae_mmu030_get_word_fcx(uaecptr,int){unavailable();}uae_u32 uae_mmu030_get_byte_fcx(uaecptr,int){unavailable();} +void uae_mmu030_put_long_fcx(uaecptr,uae_u32,int){unavailable();}void uae_mmu030_put_word_fcx(uaecptr,uae_u32,int){unavailable();}void uae_mmu030_put_byte_fcx(uaecptr,uae_u32,int){unavailable();} +uae_u32 mmu030_get_generic(uaecptr,uae_u32,int,int){unavailable();}void mmu030_put_generic(uaecptr,uae_u32,uae_u32,int,int){unavailable();} +templatevoid mmu030_unaligned_read_continue(uaecptr,int,F){unavailable();}templatevoid mmu030_unaligned_write_continue(uaecptr,int,F){unavailable();} +void unalign_clear(){unavailable();}void exception3_read_prefetch(uae_u32,uaecptr){unavailable();} +void exception3_read_special(uae_u32,uaecptr,int,int){unavailable();} +void write_log(const char*,...){unavailable();}void cpu_halt(int){unavailable();}int iack_cycle(int){unavailable();} +void exception_debug(int){}void fill_prefetch(){} // compatible/cache transport disabled. +void Exception(int);void Exception_cpu(int){unavailable();} +#include "constants.inc" +#include "sr.inc" +#include "access.inc" +#include "fault.inc" +#include "frame.inc" +#include "rte.inc" +#include "ops.inc" +void Exception(int nr){ + if(nr!=2&&nr!=9)unavailable(); + if(nr==9){traceCount++;tracePc=regs.pc;traceD7=regs.d[7];events.push_back(7);} + Exception_mmu030(nr,regs.instruction_pc); + if(nr==2){events.push_back(2);originalFrame.clear();for(int n=0;n<92;n++)originalFrame.push_back(mem.at(regs.a[7]+n));} +} +uae_u32 (*cpufunctbl[65536])(uae_u32);void (*cpufunctbl_noret[65536])(uae_u32); +void check_halt(){}void check_debugger(){}void count_instr(int){}void do_cycles(int){}int adjust_cycles(int x){return x;}void wait_memory_cycles(){unavailable();}bool time_for_interrupt(){unavailable();} +uaecptr mmu030_translate(uaecptr,bool,bool,bool){unavailable();}void cpu_restore_fixup(){if(mmufixup[0].reg!=-1||mmufixup[1].reg!=-1)unavailable();} +struct Done{}; +uae_u32 x_prefetch(int n){if(regs.pc==0x1006)throw Done{};if(++steps>32)throw std::runtime_error("Bound exceeded");return read(regs.pc+n,2);} +int do_specialties(int){ + const int spcflags=regs.spcflags; + if(spcflags&~(SPCFLAG_TRACE|SPCFLAG_DOTRACE))unavailable(); + // Exact trace fragment of reference do_specialties; IRQ/host branches excluded. +#include "special-trace.inc" + return regs.pc==0x6000||regs.pc==0x1006; +} +uae_u32 dispatch(uae_u32 op){ + if(op==0x3ebc){events.push_back(3);auto v=op_30bc_32_ff(op);editedFrame.clear();for(int n=0;n<92;n++)editedFrame.push_back(mem.at(regs.a[7]+n));return v;} + if(op==0x4e73){events.push_back(5);return op_4e73_32_ff(op);} + if(op==0x2010)return op_2010_32_ff(op); + if(op==0x7e2a){events.push_back(6);return op_7000_32_ff(op);} + if(op==0x4e71)return op_4e71_32_ff(op); + unavailable(); +} +#include "loop.inc" +int main(){try{ + for(int id=0;id<6;id++){ + const bool fault=id<4;const int initial=fault?id/2:id-4,returned=fault?id%2:initial; + regs={};mem.clear();events.clear();originalFrame.clear();editedFrame.clear();faultOnce=fault;attempts=reads=steps=traceCount=0;tracePc=traceD7=0;regflags={};xflag=nflag=zflag=vflag=cflag=0; + mmu030_state[0]=mmu030_state[1]=mmu030_state[2]=0;mmu030_idx=mmu030_idx_done=0;mmu030_retry=false;mmu030_data_buffer_out=0; + // DOTRACE represents tracing already scheduled for the instruction at the + // entry boundary. Direct controls prove the observer traces this MOVE. + regs.s=1;regs.t1=initial;regs.a[7]=regs.isp=0x8000;regs.a[0]=0x4200;regs.pc=regs.trace_pc=0x1000;regs.spcflags=initial?SPCFLAG_DOTRACE:0; + put(0x1000,0x2010,2);put(0x1002,0x7e2a,2);put(0x1004,0x4e71,2);put(0x4200,0x80818283,4);put(8,0x5000,4);put(36,0x6000,4); + put(0x5000,0x3ebc,2);put(0x5002,0x2000|(returned?0x8000:0),2);put(0x5004,0x4e73,2); + for(auto& p:cpufunctbl)p=dispatch; + try{m68k_run_mmu030();}catch(const Done&){} + if(attempts!=(fault?2:1)||reads!=1||regs.d[0]!=0x80818283)throw std::runtime_error("Missing actual recovery/control"); + if(fault){ + if(originalFrame.size()!=92||editedFrame.size()!=92||originalFrame[6]!=0xb0||originalFrame[7]!=8)throw std::runtime_error("Missing reference format-B frame"); + for(int n=0;n<92;n++)if((originalFrame[n]^editedFrame[n])!=(n==0&&initial!=returned?0x80:0))throw std::runtime_error("Handler changed non-T1 frame fields"); + }else if(!originalFrame.empty()||!editedFrame.empty())throw std::runtime_error("Unexpected control bus fault"); + std::cout<<"fault="<nul && cl /nologo /O2 /EHsc /W4 observer.cpp /Fe:observer.exe > build.log 2>&1" + if($LASTEXITCODE -ne 0){throw 'Reference observer build failed'} + & ./observer.exe > run.log 2> errors.log + if($LASTEXITCODE -ne 0){throw 'Reference observer execution failed'} + }finally{Pop-Location} + foreach($row in $before){if((Hash (Join-Path $source $row.file)) -cne $row.sha256){throw 'Reference changed during execution'}} + @{schema=1;referenceCommit=$pin;producerSha256=(Hash $PSCommandPath);fixtureSha256=(Hash $fixture); + referenceInputs=$before; + outputs=@($files|ForEach-Object {@{file=$_;sha256=(Hash (Join-Path $output $_))}}); + physicalTransport=$true;fullRunLoop=$true;traceSpecialtiesFragment=$true;fullSpecialties=$false; + enabledMmu=$false;hardwareQualified=$false;architecturalChangedTraceGatePassed=$false;roadmapComplete=$false}| + ConvertTo-Json -Depth 6|Set-Content (Join-Path $output 'identities.json') +} +$identity=Get-Content (Join-Path $output 'identities.json') -Raw|ConvertFrom-Json +if($identity.schema -ne 1 -or $identity.referenceCommit -cne $pin -or $identity.producerSha256 -cne (Hash $PSCommandPath) -or + $identity.fixtureSha256 -cne (Hash $fixture) -or $identity.hardwareQualified -ne $false -or + $identity.architecturalChangedTraceGatePassed -ne $false -or $identity.fullSpecialties -ne $false -or + $identity.physicalTransport -ne $true -or $identity.fullRunLoop -ne $true -or + $identity.traceSpecialtiesFragment -ne $true -or $identity.enabledMmu -ne $false -or + $identity.roadmapComplete -ne $false){throw 'Wrong reference scope/producer'} +if(@($identity.outputs).Count -ne $files.Count -or @($identity.referenceInputs).Count -ne $inputs.Count -or + @(Compare-Object ($files|Sort-Object) ($identity.outputs.file|Sort-Object)).Count -ne 0 -or + @(Compare-Object ($inputs|Sort-Object) ($identity.referenceInputs.file|Sort-Object)).Count -ne 0){throw 'Incomplete reference identity inventory'} +foreach($row in $identity.referenceInputs){if((Hash (Join-Path $source $row.file)) -cne $row.sha256){throw 'Changed reference input'}} +foreach($row in $identity.outputs){if((Hash (Join-Path $output $row.file)) -cne $row.sha256){throw 'Changed reference output'}} +foreach($name in $expected.Keys){if([IO.File]::ReadAllText((Join-Path $output $name)) -cne $expected[$name]){throw 'Changed extracted function'}} +if((Hash (Join-Path $output 'observer.cpp')) -cne (Hash $fixture)){throw 'Changed observer'} +if((& git -C $source status --porcelain)){throw 'Reference checkout changed'} +if($ValidateReportsOnly){ + $replay=Join-Path $output ('replay-'+[guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Path $replay|Out-Null + Push-Location $replay + try{ + & (Join-Path $output 'observer.exe') > run.log 2> errors.log + if($LASTEXITCODE -ne 0 -or (Hash 'run.log') -cne (Hash (Join-Path $output 'run.log')) -or + (Get-Item errors.log).Length -ne 0){throw 'Executed reference replay differs'} + }finally{Pop-Location} +} +$rows=[IO.File]::ReadAllLines((Join-Path $output 'run.log')) +$required=@('fault=1 0 0 2 1 0 0 0 1006 events=123546','fault=1 0 1 2 1 1 1004 2a 6000 events=1235467','fault=1 1 0 2 1 0 0 0 1006 events=123546','fault=1 1 1 2 1 1 1004 2a 6000 events=1235467','fault=0 0 0 1 1 0 0 0 1006 events=46','fault=0 1 1 1 1 1 1002 0 6000 events=47') +if($rows.Count -ne 6 -or ($rows -join "`n") -cne ($required -join "`n") -or (Get-Item (Join-Path $output 'errors.log')).Length -ne 0){throw 'Missing/changed canonical reference outcomes'} +@{schema=1;canonicalPrograms=4;directControls=2;successfulRecoveries=4;traceAfterFollowingInstruction=2;traceSuppressed=2; + referenceCommit=$pin;identitySha256=(Hash (Join-Path $output 'identities.json'));hardwareQualified=$false; + architecturalChangedTraceGatePassed=$false;roadmapComplete=$false}| + ConvertTo-Json|Set-Content (Join-Path $output 'verification.json') +Write-Host 'Verified four reference-generated fault/frame/RTE recoveries; returned T1 traces after following MOVEQ. Architectural gate remains open.' From 1d4339e686a56abd5260a49e8cdbd72ed4eec512 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 19:19:22 +0300 Subject: [PATCH 112/211] Audit compatible 030 trace recovery and reference disagreement --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 62 +++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 13 ++ .../m68030-compatible-return-trace.cpp | 174 ++++++++++++++++++ ...t-copper68k-030-return-trace-reference.ps1 | 37 +++- 4 files changed, 280 insertions(+), 6 deletions(-) create mode 100644 scripts/reference/m68030-compatible-return-trace.cpp diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 6996c473..307d8fbb 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9416,3 +9416,65 @@ inputs, empty selections and changed outcomes fail. Evidence: The next gate must reconcile the trace point before widening the canonical case to other banks, operands and frame origins. No publication or production import occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +#### Compatible 030 pipeline reference and failing agreement gate — 2026-10-07 + +The reference checkout is verified clean upstream `tonioni/WinUAE` at +`5d22d33632646efc3f747f03e82d28353e52722e`. A second maintained observer, +`scripts/reference/m68030-compatible-return-trace.cpp`, executes generated +`_35` instructions, the compatible RTE helper, unchanged pipeline/prefetch +functions and official `cpustbl.cpp` metadata. The reference's +`MORE_ACCURATE_68020_PIPELINE=1` is retained. Physical transport supplies +cacheholding data; IRQ, enabled translation and physical timing remain excluded. +The same complete retry loop and exact trace-specialties fragment execute. + +Fresh previous and compatible selections each execute **four fault/frame/RTE +recoveries and two direct controls**, with native replay: **eight recoveries +and four controls** across the two profiles. Both retain the earlier six +exact observed outcomes. Thus executing the compatible software pipeline +does not remove the delay: returned T1=1 traces after following MOVEQ at +PC `1004`, while the direct trace-on control traces after MOVE at PC `1002`. +This is agreement between two paths sharing a retry loop, not independent +hardware confirmation. + +The compatible exploratory adapter first failed compilation for a missing +forward declaration, then crashed because prefetch words were wider than +the reference's `uae_u16` fields. The restored stage-B concatenation relies +on 16-bit truncation. Matching word/validity-array types and adding static +width/capacity assertions correct the observer, without changing extracted +reference functions or accepting the failed outputs. A width mutant now +fails compilation at the assertion; a sole access-history predicate mutant +compiles and fails the successful-read-count guard. All other native inputs +remain byte-identical. An initial guard runner expected the wrong compiler +diagnostic spelling; its actual rejection is retained and fresh V2 corrects +only that runner expectation/output directory. + +Nine evidence corruption controls pass, including incorrect profile and +false pipeline claims. The new `-RequireArchitecturalAgreement` option +**fails for both profiles** on the documented unchanged-T1=1 case. The +[MC68030 manual, section 8.1.7](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P2.pdf) +places tracing of a completed suspended instruction before the next +instruction. With T1 unchanged, the trace belongs at PC `1002` with D7 +unchanged; the software references instead observe PC `1004` and D7=`2A`. +The observer records the discrepancy rather than making it a passing +architectural expectation. Manual text was verified through the browser; +the direct PDF capture was denied, so no local PDF identity is claimed. + +Run the additional profile and the explicitly failing agreement gate: + +```powershell +./scripts/test-copper68k-030-return-trace-reference.ps1 -Compatible030 -OutputDirectory +./scripts/test-copper68k-030-return-trace-reference.ps1 -Compatible030 -OutputDirectory -ValidateReportsOnly +./scripts/test-copper68k-030-return-trace-reference.ps1 -Compatible030 -OutputDirectory -ValidateReportsOnly -RequireArchitecturalAgreement +``` + +Evidence: `reference/ReturnedTrace030CompatibleMaintainedV1`, +`reference/ReturnedTrace030PreviousMaintainedV3`, +`integrity/ReturnedTrace030Compatible`, +`mutations/ReturnedTrace030CompatibleV2`. Proof: +`compatible-return-trace-reference-proof.json`, SHA-256 `a38cbfff0602b46ca7701f4d70b14faf272e1ac4ce978237a7da391b3563ab61`. +The earlier exploratory failures and prior source-specific proofs remain +separate. Production assemblies retain protected hashes; no CPU fix, package +publication or full CPU/consumer claim occurs. Handler-edited T1 remains +unresolved; neither software delay is a replacement architecture rule. +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 679a190f..649dfd1c 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3150,3 +3150,16 @@ software/manual trace point before promotion or wider trace qualification. No production CPU import/publication occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [executed reference disagreement](COPPER68K_REFERENCE_QUALIFICATION.md#executed-030-returned-trace-reference-disagreement--2026-10-07). + +Compatible 030 reference qualification now executes the intact software +pipeline, generated `_35` instructions and compatible RTE helper. Fresh +previous/compatible profiles reproduce eight recoveries and four direct +controls, with nine evidence corruption checks and two native observer +guards. Both fail the explicitly requested architectural-agreement gate +for unchanged T1=1: their trace occurs after following MOVEQ rather than +before it. The manual-defined unchanged-T1 requirement is retained; +software reference agreement cannot override it. Handler-edited T1 remains +unresolved, and no production CPU behavior is changed. Observer word-width +repairs and failed exploratory runs are recorded separately. Milestone 6 +remains **in progress**, `roadmapComplete=false`. See the +[compatible pipeline/reference gate record](COPPER68K_REFERENCE_QUALIFICATION.md#compatible-030-pipeline-reference-and-failing-agreement-gate--2026-10-07). diff --git a/scripts/reference/m68030-compatible-return-trace.cpp b/scripts/reference/m68030-compatible-return-trace.cpp new file mode 100644 index 00000000..977722f9 --- /dev/null +++ b/scripts/reference/m68030-compatible-return-trace.cpp @@ -0,0 +1,174 @@ +// Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT +// Canonical 030 reference-generated bus error, guest saved-T1 edit and RTE. +// Untouched compatible _35 operations/RTE helper, software pipeline and full +// retry loop; physical memory transport and exact trace-only specialties. +// No enabled translation, IRQ, cache/pipeline timing or hardware qualification. +#include +#include +#include +#include +#include +#include +using uae_u8=uint8_t;using uae_u16=uint16_t;using uae_u32=uint32_t; +using uae_s8=int8_t;using uae_s16=int16_t;using uae_s32=int32_t;using uaecptr=uint32_t; +#define MORE_ACCURATE_68020_PIPELINE 1 +#define PIPELINE_DEBUG 0 +#define REGPARAM2 +#define STATIC_INLINE static inline +#define CYCLE_UNIT 2 +#define _T(x) x +#define MMUDEBUG 0 +#define TRY(x) try +#define CATCH(x) catch(int x) +#define ENDTRY +#define STOPTRY +#define THROW(x) throw int(x) +#define SPCFLAG_TRACE 0x40 +#define SPCFLAG_DOTRACE 0x80 +#define SPCFLAG_TRAP 0x400 +#define SPCFLAG_INT 8 +#define SPCFLAG_DOINT 0x100 +#define CPU_HALT_CPU_STUCK 1 +#define CPU_HALT_DOUBLE_FAULT 2 +#define sz_byte 0 +#define sz_word 1 +#define sz_long 2 +#define m68k_areg(r,n) ((r).a[n]) +#define m68k_dreg(r,n) ((r).d[n]) +struct Regs { + uae_u32 a[8]{},d[8]{},usp{},isp{},msp{},pc{},instruction_pc{},trace_pc{},vbr{},mmu_ssw{},mmu_fault_addr{},sfc{},dfc{},wb3_data{},wb2_address{},fp_ea{},fp_opword{},mmu_effective_addr{},pcr{},fp_unimp_ins{},ir{},cacheholdingaddr020{},cacheholdingdata020{},db{}; + uae_u16 sr{},opcode{},irc{},ird{};int m{},s{},t0{},t1{},intmask{},ipl[1]{},ipl_pin{},stopped{},spcflags{},instruction_cnt{},wb2_status{},wb3_status{},read_buffer{},write_buffer{},fpu_exp_pre{},fp_unimp_pend{}; + uae_u16 prefetch020[4]{};uae_u8 prefetch020_valid[4]{},cacheholdingdata_valid{};int pipeline_r8[2]{},pipeline_pos{},pipeline_stop{}; +}regs; +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(sizeof(regs.prefetch020)/sizeof(regs.prefetch020[0])==4); +struct Prefs{int cpu_model=68030,mmu_model=68030,cachesize{},cpu_compatible=1,cpu_cycle_exact{},fpu_model{},cpu_memory_cycle_exact{};}currprefs; +struct flag_struct{int cznv{},x{};}regflags; +int xflag,nflag,zflag,vflag,cflag; +#define SET_XFLG(x) (xflag=(x)) +#define SET_NFLG(x) (nflag=(x)) +#define SET_ZFLG(x) (zflag=(x)) +#define SET_VFLG(x) (vflag=(x)) +#define SET_CFLG(x) (cflag=(x)) +#define GET_XFLG() xflag +#define GET_NFLG() nflag +#define GET_ZFLG() zflag +#define GET_VFLG() vflag +#define GET_CFLG() cflag +#define CLEAR_CZNV() (cflag=zflag=nflag=vflag=0) +bool m68k_accurate_ipl=false,mmu_debugger=false,ismoves030=false,islrmw030=false,bBusErrorReadWrite=false,mmu030_retry=false; +int mmu030_idx,mmu030_idx_done,mmu030_opcode,mmu030_opcode_stageb,mmu030_fake_prefetch,cpu_cycles; +uaecptr mmu030_fake_prefetch_addr,mm030_stageb_address,last_fault_for_exception_3; +uae_u16 mmu030_state[3];uae_u32 mmu030_data_buffer_out,mmu030_disp_store[2],mmu030_fmovem_store[2],mmu040_move16[4]; +struct mmu030_access{uae_u32 val;};mmu030_access mmu030_ad[10]; +struct{int reg=-1;}mmufixup[2]; +std::unordered_map mem; +std::vector events;std::vector originalFrame,editedFrame; +bool faultOnce;int attempts,reads,steps,traceCount;uae_u32 tracePc,traceD7;int returnT1; +[[noreturn]] void unavailable(){throw std::runtime_error("Out-of-scope path");} +void set_special(int x){regs.spcflags|=x;}void unset_special(int x){regs.spcflags&=~x;} +void mmu_set_super(int){};int mmu030fixupreg(int){return 0;}void mmu030fixupmod(int x,int,int){if(x)unavailable();} +uaecptr m68k_getpc(){return regs.pc;}uaecptr m68k_getpci(){return regs.pc;} +void m68k_setpci(uaecptr x){regs.pc=x;}void m68k_incpci(int n){regs.pc+=n;} +void mmu030_hardware_bus_error(uaecptr,uae_u32,bool,bool,int); +void mmu030_page_fault(uaecptr,bool,int,uae_u32); +uae_u32 read(uaecptr a,int w){ + if(a==0x4200&&w==4){attempts++;if(faultOnce){faultOnce=false;events.push_back(1);mmu030_hardware_bus_error(a,0,true,false,sz_long);}reads++;events.push_back(4);} + uae_u32 v=0;for(int n=0;nsecond;}return v; +} +void put(uaecptr a,uae_u32 v,int w){for(int n=0;n>(8*(w-1-n)));} +uae_u32 uae_mmu030_get_long(uaecptr a){return read(a,4);}uae_u32 uae_mmu030_get_word(uaecptr a){return read(a,2);}uae_u32 uae_mmu030_get_iword(uaecptr a){return read(a,2);} +void uae_mmu030_put_word(uaecptr a,uae_u32 v){put(a,v,2);} +uae_u32 get_word_mmu030(uaecptr a){return read(a,2);}uae_u32 get_long_mmu030(uaecptr a){return read(a,4);} +uae_u32 x_get_long(uaecptr a){return read(a,4);}void x_put_long(uaecptr a,uae_u32 v){put(a,v,4);}void x_put_word(uaecptr a,uae_u32 v){put(a,v,2);} +uae_u32 uae_mmu030_get_long_fcx(uaecptr a,int fc){if(fc!=5)unavailable();return read(a,4);} +uae_u32 uae_mmu030_get_word_fcx(uaecptr,int){unavailable();}uae_u32 uae_mmu030_get_byte_fcx(uaecptr,int){unavailable();} +void uae_mmu030_put_long_fcx(uaecptr,uae_u32,int){unavailable();}void uae_mmu030_put_word_fcx(uaecptr,uae_u32,int){unavailable();}void uae_mmu030_put_byte_fcx(uaecptr,uae_u32,int){unavailable();} +uae_u32 mmu030_get_generic(uaecptr,uae_u32,int,int){unavailable();}void mmu030_put_generic(uaecptr,uae_u32,uae_u32,int,int){unavailable();} +templatevoid mmu030_unaligned_read_continue(uaecptr,int,F){unavailable();}templatevoid mmu030_unaligned_write_continue(uaecptr,int,F){unavailable();} +void unalign_clear(){unavailable();}void exception3_read_prefetch(uae_u32,uaecptr){unavailable();} +void exception3_read_special(uae_u32,uaecptr,int,int){unavailable();} +void write_log(const char*,...){unavailable();}void cpu_halt(int){unavailable();}int iack_cycle(int){unavailable();} +void exception_debug(int){} +struct CpuData{int length;int disp020[2];int branch;};CpuData cpudatatbl[65536]; +void fill_icache030(uae_u32 a){a&=~3u;regs.cacheholdingaddr020=a;regs.cacheholdingdata020=read(a,4);regs.cacheholdingdata_valid=1;} +bool maybe_icache030(uae_u32 a){fill_icache030(a);return true;} +uae_u16 icache_fetch_word(uaecptr a){return read(a,2);} +uae_u32 get_word_ce030_prefetch_opcode(int){unavailable();} +void fill_icache040(uae_u32){unavailable();}void fill_prefetch_020(){unavailable();} +uae_u32 x_get_word(uaecptr a){return read(a,2);} +void ipl_fetch_now(){}void intlev_load(){} +void fill_prefetch();void fill_prefetch_030_ntx();void fill_prefetch_030_ntx_continue(); +uae_u32 read_data_030_wget(uaecptr a){return read(a,2);}uae_u32 read_data_030_lget(uaecptr a){return read(a,4);} +void write_data_030_wput(uaecptr a,uae_u32 v){put(a,v,2);} +uae_u32 get_word_mmu030c(uaecptr a){return read(a,2);}uae_u32 get_long_mmu030c(uaecptr a){return read(a,4);} +uae_u32 read_data_030_fc_lget(uaecptr a,int fc){return uae_mmu030_get_long_fcx(a,fc);} +uae_u32 read_data_030_fc_wget(uaecptr,int){unavailable();}uae_u32 read_data_030_fc_bget(uaecptr,int){unavailable();} +void write_data_030_fc_lput(uaecptr,uae_u32,int){unavailable();}void write_data_030_fc_wput(uaecptr,uae_u32,int){unavailable();}void write_data_030_fc_bput(uaecptr,uae_u32,int){unavailable();} +uae_u32 read_dcache030_retry(uaecptr,uae_u32,int,int){unavailable();}void write_dcache030_retry(uaecptr,uae_u32,uae_u32,int,int){unavailable();} + +void Exception(int);void Exception_cpu(int){unavailable();} +#include "constants.inc" +#include "sr.inc" +#include "prefetch.inc" +#include "access.inc" +#include "fault.inc" +#include "frame.inc" +#include "rte.inc" +#include "ops.inc" +void Exception(int nr){ + if(nr!=2&&nr!=9)unavailable(); + if(nr==9){traceCount++;tracePc=regs.pc;traceD7=regs.d[7];events.push_back(7);} + Exception_mmu030(nr,regs.instruction_pc); + if(nr==2){events.push_back(2);originalFrame.clear();for(int n=0;n<92;n++)originalFrame.push_back(mem.at(regs.a[7]+n));} +} +uae_u32 (*cpufunctbl[65536])(uae_u32);void (*cpufunctbl_noret[65536])(uae_u32); +void check_halt(){}void check_debugger(){}void count_instr(int){}void do_cycles(int){}int adjust_cycles(int x){return x;}void wait_memory_cycles(){unavailable();}bool time_for_interrupt(){unavailable();} +uaecptr mmu030_translate(uaecptr,bool,bool,bool){unavailable();}void cpu_restore_fixup(){if(mmufixup[0].reg!=-1||mmufixup[1].reg!=-1)unavailable();} +struct Done{}; +uae_u32 x_prefetch(int n){if(regs.pc==0x1006)throw Done{};if(++steps>32)throw std::runtime_error("Bound exceeded");return read(regs.pc+n,2);} +int do_specialties(int){ + const int spcflags=regs.spcflags; + if(spcflags&~(SPCFLAG_TRACE|SPCFLAG_DOTRACE))unavailable(); + // Exact trace fragment of reference do_specialties; IRQ/host branches excluded. +#include "special-trace.inc" + return regs.pc==0x6000||regs.pc==0x1006; +} +uae_u32 dispatch(uae_u32 op){ + if(regs.pc==0x1006)throw Done{};if(++steps>32)throw std::runtime_error("Dispatch bound exceeded"); + if(op==0x3ebc){events.push_back(3);op_30bc_35_ff(op);editedFrame.clear();for(int n=0;n<92;n++)editedFrame.push_back(mem.at(regs.a[7]+n));return 0;} + if(op==0x4e73){events.push_back(5);op_4e73_35_ff(op);return 0;} + if(op==0x2010){op_2010_35_ff(op);return 0;} + if(op==0x7e2a){events.push_back(6);op_7000_35_ff(op);return 0;} + if(op==0x4e71){op_4e71_35_ff(op);return 0;} + unavailable(); +} +#include "loop.inc" +int main(){try{ + for(int id=0;id<6;id++){ + const bool fault=id<4;const int initial=fault?id/2:id-4,returned=fault?id%2:initial; + regs={};mem.clear();events.clear();originalFrame.clear();editedFrame.clear();faultOnce=fault;attempts=reads=steps=traceCount=0;tracePc=traceD7=0;regflags={};xflag=nflag=zflag=vflag=cflag=0; + mmu030_state[0]=mmu030_state[1]=mmu030_state[2]=0;mmu030_idx=mmu030_idx_done=0;mmu030_retry=false;mmu030_data_buffer_out=0; + // DOTRACE represents tracing already scheduled for the instruction at the + // entry boundary. Direct controls prove the observer traces this MOVE. + regs.s=1;regs.t1=initial;regs.a[7]=regs.isp=0x8000;regs.a[0]=0x4200;regs.pc=regs.trace_pc=0x1000;regs.spcflags=initial?SPCFLAG_DOTRACE:0; + put(0x1000,0x2010,2);put(0x1002,0x7e2a,2);put(0x1004,0x4e71,2);put(0x4200,0x80818283,4);put(8,0x5000,4);put(36,0x6000,4); + put(0x5000,0x3ebc,2);put(0x5002,0x2000|(returned?0x8000:0),2);put(0x5004,0x4e73,2); + for(uaecptr a=0x1006;a<=0x1010;a+=2)put(a,0x4e71,2); + for(uaecptr a=0x5006;a<=0x5010;a+=2)put(a,0x4e71,2); + for(uaecptr a=0x6000;a<=0x6010;a+=2)put(a,0x4e71,2); +#include "metadata.inc" + fill_prefetch_030_ntx(); + for(auto& p:cpufunctbl)p=dispatch; + try{m68k_run_mmu030();}catch(const Done&){} + if(attempts!=(fault?2:1)||reads!=1||regs.d[0]!=0x80818283)throw std::runtime_error("Missing actual recovery/control"); + if(fault){ + if(originalFrame.size()!=92||editedFrame.size()!=92||originalFrame[6]!=0xb0||originalFrame[7]!=8)throw std::runtime_error("Missing reference format-B frame"); + for(int n=0;n<92;n++)if((originalFrame[n]^editedFrame[n])!=(n==0&&initial!=returned?0x80:0))throw std::runtime_error("Handler changed non-T1 frame fields"); + }else if(!originalFrame.empty()||!editedFrame.empty())throw std::runtime_error("Unexpected control bus fault"); + std::cout<<"fault="< Date: Wed, 7 Oct 2026 19:32:38 +0300 Subject: [PATCH 113/211] Prove and consolidate 68010 EXTB availability coverage --- Copper68k.Tests/M68010InterpreterTests.cs | 17 -- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 52 ++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 12 + scripts/test-copper68k-extb-consolidation.py | 237 ++++++++++++++++++ 4 files changed, 301 insertions(+), 17 deletions(-) create mode 100644 scripts/test-copper68k-extb-consolidation.py diff --git a/Copper68k.Tests/M68010InterpreterTests.cs b/Copper68k.Tests/M68010InterpreterTests.cs index 0c30cbb4..bd0ef32d 100644 --- a/Copper68k.Tests/M68010InterpreterTests.cs +++ b/Copper68k.Tests/M68010InterpreterTests.cs @@ -156,22 +156,5 @@ public void OddWordReadRaisesFormat8AddressErrorFrame() Assert.Equal(0x800Cu, bus.ReadWord(0x2FCC)); } - [Fact] - public void RejectsM68020OnlyExtbLong() - { - var bus = new ZeroWaitCodeBus(); - WriteWords(bus, CodeBase, 0x49C0); // EXTB.L D0 - bus.WriteLong(4u * 4u, 0x0000_2000); - var cpu = new M68010Interpreter(bus); - cpu.Reset(CodeBase, 0x3000); - cpu.State.D[0] = 0x0000_0080; - cpu.ExecuteInstruction(); - Assert.Equal(0x2000u, cpu.State.ProgramCounter); - Assert.Equal(0x0000_0080u, cpu.State.D[0]); - Assert.Equal(0x2FF8u, cpu.State.A[7]); - Assert.Equal(CodeBase, bus.ReadLong(0x2FFA)); - Assert.Equal(4 * 4, bus.ReadWord(0x2FFE)); - } - private const uint CodeBase = 0x1000; } diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 307d8fbb..8fe7b8c3 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9478,3 +9478,55 @@ separate. Production assemblies retain protected hashes; no CPU fix, package publication or full CPU/consumer claim occurs. Handler-edited T1 remains unresolved; neither software delay is a replacement architecture rule. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## EXTB availability regression consolidation — 2026-10-07 + +Retire only `M68010InterpreterTests.RejectsM68020OnlyExtbLong` in favor of +`SyntheticTransferTests.RegisterTransferFamilies`. The exact replacement +anchor is `68010/EXTB.L/D0/boundary-ccr/op=49C0/v=00000080/ccr=00`. +The shared family verifies the documented illegal-instruction outcome on +68000/68010 and legal execution on the later models, including A1200. +It checks registers, saved PC/SR, format/vector word, stack and surrounding +memory; all eight registers, eight boundary values and 32 CCR states cover +2,048 EXTB scenarios per profile. This retirement concerns semantics only. +The seven other 68010 tests and all timing, prefetch, cache, JIT, bus-ordering +and native regressions remain. + +The isolated audit restores the exact original class from commit +`1d4339e686a56abd5260a49e8cdbd72ed4eec512` and copies 204 source/project +inputs. Its clean selection passes 16 executions, including the original +witness, seven retained 68010 tests and eight complete shared batches. +The shared batches pass **140,992 logical scenarios**, with zero mismatches, +unsupported or untested selected cases. A targeted mutation accepts EXTB +as an empty operation only in the 68010 model-specific dispatch. Both the +original witness and precisely **2,048 shared EXTB scenarios** detect it; +the other 138,944 shared scenarios and seven retained 68010 tests pass. +The mutated CPU file is the only changed audit input; no production CPU +change is made. The reduced repository selection separately passes all +15 executions, with no skips. + +Six corruption checks reject a missing fixture, wrong producer, empty test +selection, incorrect weights, unrelated failure reason and a changed CPU +baseline even when its manifest hash is updated. Validation checks exact +source inventories, pinned original fixture, test rosters/outcomes, complete +report keys/weights, all failure identifiers/reasons and TRX agreement. +Normal assemblies retain their protected identities. + +Run the maintained audit with Python 3 and dotnet available: + +```powershell +python ./scripts/test-copper68k-extb-consolidation.py --output +python ./scripts/test-copper68k-extb-consolidation.py --output --validate-only +``` + +Evidence: `audits/ExtbConsolidationV2/proof.json`, SHA-256 +`f0a5eec4601d67e973fdccde2182f56a1c84abbdec6da2ce5659f8383fdfb044`; +`audits/ExtbConsolidationCurrent/current.trx`, SHA-256 +`f3d5846051cc40e1f35f66487c92d3f0135443b82d4932a5ab35eb7ecd5e3e9b`, +records the reduced selection. V1 passed its clean execution but failed +fixture-identity validation because Python's Windows newline conversion +changed the frozen file bytes. V2 fixes only the audit writer and reruns +into a fresh directory; V1 is not qualified evidence. No new full CPU, +consumer or physical timing qualification is claimed. The 030 trace +disagreement remains open. Milestone 6 remains **in progress**, +`roadmapComplete=false`; no package publication occurs. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 649dfd1c..4735d7b3 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3163,3 +3163,15 @@ unresolved, and no production CPU behavior is changed. Observer word-width repairs and failed exploratory runs are recorded separately. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [compatible pipeline/reference gate record](COPPER68K_REFERENCE_QUALIFICATION.md#compatible-030-pipeline-reference-and-failing-agreement-gate--2026-10-07). + +The pure semantic 68010 EXTB rejection regression is now replaced by the +shared register-transfer suite after an isolated proof restores its exact +original fixture. All 140,992 shared scenarios pass across eight profiles; +an intentional 68010 acceptance defect fails both the original witness and +exactly 2,048 shared EXTB scenarios. Seven other 68010 tests pass unchanged, +six evidence corruption checks reject altered inputs, and all 15 executions +in the reduced repository selection pass without skips. Timing and hardware +regressions remain. This completes only the EXTB consolidation slice; +remaining reference gaps, including the 030 trace disagreement, stay open. +Milestone 6 remains **in progress**, `roadmapComplete=false`. See the +[EXTB consolidation record](COPPER68K_REFERENCE_QUALIFICATION.md#extb-availability-regression-consolidation--2026-10-07). diff --git a/scripts/test-copper68k-extb-consolidation.py b/scripts/test-copper68k-extb-consolidation.py new file mode 100644 index 00000000..700b6b35 --- /dev/null +++ b/scripts/test-copper68k-extb-consolidation.py @@ -0,0 +1,237 @@ +"""Isolated, reproducible EXTB availability consolidation and mutation audit. + +Run with Python 3 and dotnet on PATH. Use a fresh --output directory; replay +the complete evidence with --validate-only. No normal build outputs are used. +""" +import argparse +import hashlib +import itertools +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import xml.etree.ElementTree as ET + +REPO = Path(__file__).resolve().parent.parent +PIN = "1d4339e686a56abd5260a49e8cdbd72ed4eec512" +LEGACY = "Copper68k.Tests/M68010InterpreterTests.cs" +CPU = "Copper68k/M68010Interpreter.cs" +HELPER = "scripts/test-copper68k-extb-consolidation.py" +MODELS = ["68000", "68010", "68EC020", "68020", "68030", "68040", "68060", "A1200"] +VALUES = [0, 1, 0x7f, 0x80, 0x7fff, 0x8000, 0x89abcdef, 0xffffffff] +OLD = " if (TryExecuteM68010StatusAndReturn(opcode, instructionPc)) return true;" +NEW = " if ((opcode & 0xFFF8) == 0x49C0) return true; // intentional unavailable-instruction acceptance\n" + OLD +NS = {"t": "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"} + + +def check(condition, reason): + if not condition: + raise ValueError(reason) + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def load(path): + return json.loads(path.read_text(encoding="utf-8-sig")) + + +def save(path, value): + path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8", newline="\n") + + +def tracked(): + return [p for p in subprocess.check_output( + ["git", "ls-files", "Copper68k", "Copper68k.Tests"], cwd=REPO, text=True + ).splitlines() if Path(p).suffix in (".cs", ".csproj")] + + +def original(): + text = subprocess.check_output(["git", "show", PIN + ":" + LEGACY], cwd=REPO).decode("utf-8-sig") + check(text.count("public void RejectsM68020OnlyExtbLong()") == 1, "Missing pinned witness") + return text.replace("\r\n", "\n") + + +def mutation(text): + check(text.count(OLD) == 1, "Missing unique mutation target") + return text.replace(OLD, NEW) + + +def inventory(source): + return {p.relative_to(source).as_posix(): sha(p) for p in source.rglob("*") + if p.suffix in (".cs", ".csproj") and not {"bin", "obj"}.intersection(p.parts)} + + +def expected_combinations(model, mode): + rows, failures = {}, [] + for reg in range(8): + rows[f"{model}/MOVEQ/L/imm8->D{reg}/all-encodings"] = {"passing": 411} + for family in ["EXT.W", "EXT.L", "EXTB.L", "SWAP"]: + failed = mode == "AcceptUnavailable" and model == "68010" and family == "EXTB.L" + rows[f"{model}/{family}/D{reg}/boundary-ccr"] = {"mismatching" if failed else "passing": 256} + if failed: + for value, ccr in itertools.product(VALUES, range(32)): + failures.append(dict(id=f"{model}/{family}/D{reg}/boundary-ccr/op={0x49c0 | reg:04X}/v={value:08X}/ccr={ccr:02X}", + status="mismatching", reason="PC expected 00009040, actual 00001002")) + for kind, src, dst in itertools.product([0x40, 0x48, 0x88], range(8), range(8)): + rows[f"{model}/EXG/L/kind={kind:02X}/r{src}->r{dst}/preserve-flags"] = {"passing": 32} + return rows, failures + + +def verify(root, mode): + data = load(root / "inputs.json") + check(data["mode"] == mode and data["pin"] == PIN, "Wrong selection pin") + check(data["producer"] == sha(REPO / HELPER), "Wrong producer") + pinned = original() + start = pinned.index("\t[Fact]\n\tpublic void RejectsM68020OnlyExtbLong()") + end = pinned.index("\tprivate const uint CodeBase", start) + check((REPO / LEGACY).read_text(encoding="utf-8-sig") == pinned[:start] + pinned[end:], "Wrong legacy retirement scope") + expected = {p: sha(REPO / p) for p in tracked()} + expected[LEGACY] = hashlib.sha256(original().encode()).hexdigest() + if mode == "AcceptUnavailable": + expected[CPU] = hashlib.sha256(mutation((REPO / CPU).read_text(encoding="utf-8-sig")).encode()).hexdigest() + check(data["sources"] == expected, "Wrong baseline or mutation identity") + check(inventory(root / "source") == expected, "Incomplete or changed source inventory") + for p, h in data["protected"].items(): + check(sha(REPO / p) == h, "Changed protected assembly") + check(set(data["protected"]) == {p for p in normal_paths() if (REPO / p).exists()}, "Incomplete protected identities") + check(data["testExit"] == (0 if mode == "Clean" else 1), "Wrong test exit") + check(data["logSha256"] == sha(root / "execution.log"), "Changed execution log") + tree = ET.parse(root / "audit.trx") + tests = tree.findall(".//t:UnitTestResult", NS) + legacy_names = re.findall(r"public void (\w+)\(\)", original()) + names = {"Copper68k.Tests.M68010InterpreterTests." + n: "Passed" for n in legacy_names} + old_name = "Copper68k.Tests.M68010InterpreterTests.RejectsM68020OnlyExtbLong" + if mode != "Clean": + names[old_name] = "Failed" + for model in MODELS: + names[f'Copper68k.Tests.Synthetic.SyntheticTransferTests.RegisterTransferFamilies(modelId: "{model}")'] = ( + "Failed" if mode != "Clean" and model == "68010" else "Passed") + check(len(tests) == len(names) and {t.get("testName") for t in tests} == set(names), "Wrong or empty execution selection") + for test in tests: + check(test.get("outcome") == names[test.get("testName")], "Wrong executed outcome") + if mode != "Clean" and test.get("testName") == old_name: + message = test.find("t:Output/t:ErrorInfo/t:Message", NS).text + check(re.search(r"Expected:\s*8192\b", message) and re.search(r"Actual:\s*4098\b", message), "Wrong historical failure reason") + stdout = "\n".join(t.text or "" for t in tree.findall(".//t:UnitTestResult/t:Output/t:StdOut", NS)) + reports = [] + for model in MODELS: + path = root / f"{model}-transfer-registers.json" + report = load(path) + rows, failures = expected_combinations(model, mode) + missed = len(failures) + counts = dict(passing=17624 - missed, mismatching=missed, unsupported=0, untested=0) + check(report["schema"] == 1 and report["model"] == model and report["group"] == "transfer-registers" + and report["logicalCases"] == 17624 and report["xunitBatches"] == 1, "Wrong report identity") + check(report["counts"] == counts and report["combinations"] == rows, "Wrong complete counts or weights") + check(sorted(report["failures"], key=lambda r: r["id"]) == sorted(failures, key=lambda r: r["id"]), "Wrong failure roster or reason") + matches = re.findall(re.escape(f"{model}/transfer-registers: ") + r"(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)", stdout) + check(len(matches) == 1 and json.loads(matches[0][0]) == counts and int(matches[0][1]) == 17624 + and int(matches[0][2]) == len(rows), "TRX report disagreement") + reports.append(dict(path=path.name, sha256=sha(path))) + return dict(mode=mode, passing=140992 - (2048 if mode != "Clean" else 0), mismatching=2048 if mode != "Clean" else 0, + executions=len(tests), retainedLegacyExecutions=len(legacy_names) - 1, + sources=len(expected), reports=reports, trxSha256=sha(root / "audit.trx"), inputsSha256=sha(root / "inputs.json"), + replacementAnchor="68010/EXTB.L/D0/boundary-ccr/op=49C0/v=00000080/ccr=00") + + +def normal_paths(): + return ["Copper68k/bin/Release/net10.0/Copper68k.dll", "Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll"] + + +def execute(root, mode): + source = root / "source" + for p in tracked(): + target = source / p + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(REPO / p, target) + (source / LEGACY).write_text(original(), encoding="utf-8", newline="\n") + if mode != "Clean": + (source / CPU).write_text(mutation((source / CPU).read_text(encoding="utf-8-sig")), encoding="utf-8", newline="\n") + data = dict(mode=mode, pin=PIN, producer=sha(REPO / HELPER), sources=inventory(source), + protected={p: sha(REPO / p) for p in normal_paths() if (REPO / p).exists()}) + env = dict(os.environ, COPPER68K_SYNTHETIC_REPORT_DIR=str(root)) + command = ["dotnet", "test", str(source / "Copper68k.Tests/Copper68k.Tests.csproj"), "-c", "Release", + "--artifacts-path", str(root / "build"), "--filter", + "FullyQualifiedName~M68010InterpreterTests|FullyQualifiedName~RegisterTransferFamilies", + "--logger", "trx;LogFileName=audit.trx", "--results-directory", str(root)] + print(f"{mode}: running original witnesses and all eight shared register matrices", flush=True) + with (root / "execution.log").open("w", encoding="utf-8") as log: + data["testExit"] = subprocess.run(command, stdout=log, stderr=subprocess.STDOUT, env=env, cwd=REPO).returncode + data["command"] = command + data["logSha256"] = sha(root / "execution.log") + save(root / "inputs.json", data) + + +def integrity(output, validate_only): + definitions = [("MissingFixture", "Incomplete or changed source inventory"), + ("WrongProducer", "Wrong producer"), ("EmptySelection", "Wrong or empty execution selection"), + ("WrongWeight", "Wrong complete counts or weights"), ("WrongFailureReason", "Wrong failure roster or reason"), + ("WrongBaseline", "Wrong baseline or mutation identity")] + records = [] + for name, reason in definitions: + root = output / "integrity" / name + if not validate_only: + base = output / "AcceptUnavailable" + shutil.copytree(base / "source", root / "source") + for path in [base / "inputs.json", base / "audit.trx", base / "execution.log", *base.glob("*-transfer-registers.json")]: + shutil.copyfile(path, root / path.name) + if name == "MissingFixture": + (root / "source" / "Copper68k.Tests/Synthetic/SyntheticTransferTests.cs").unlink() + elif name == "WrongProducer": + data = load(root / "inputs.json"); data["producer"] = "0" * 64; save(root / "inputs.json", data) + elif name == "EmptySelection": + tree = ET.parse(root / "audit.trx") + parent = tree.find("t:Results", NS) + for child in list(parent): parent.remove(child) + tree.write(root / "audit.trx") + elif name == "WrongBaseline": + path = root / "source" / CPU + path.write_text(path.read_text(encoding="utf-8-sig") + "\n// unrelated CPU change\n", encoding="utf-8", newline="\n") + data = load(root / "inputs.json"); data["sources"][CPU] = sha(path); save(root / "inputs.json", data) + else: + path = root / "68010-transfer-registers.json"; data = load(path) + if name == "WrongWeight": data["counts"]["passing"] += 1 + else: data["failures"][0]["reason"] = "unrelated failure" + save(path, data) + try: + verify(root, "AcceptUnavailable") + except ValueError as error: + check(str(error) == reason, "Wrong integrity rejection: " + name + ": " + str(error)) + records.append(dict(name=name, reason=reason)) + else: + raise ValueError("Integrity corruption accepted: " + name) + return records + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--validate-only", action="store_true") + args = parser.parse_args() + output = args.output.resolve() + if not args.validate_only: + check(not output.exists(), "Use a fresh output directory") + output.mkdir(parents=True) + entries = [] + for mode in ["Clean", "AcceptUnavailable"]: + root = output / mode + if not args.validate_only: + root.mkdir(); execute(root, mode) + entries.append(verify(root, mode)) + print(f"{mode}: {entries[-1]['passing']} passing / {entries[-1]['mismatching']} precise mismatches", flush=True) + proof = dict(schema=1, scope="EXTB.L unavailable on 68010: historical witness and complete shared register-transfer replacement", + pin=PIN, producerSha256=sha(REPO / HELPER), entries=entries, integrity=integrity(output, args.validate_only), + originalAndReplacementDetected=True, retirementApplied=True, productionCpuChanged=False, publication=False, roadmapComplete=False) + if args.validate_only: + check(load(output / "proof.json") == proof, "Changed aggregate proof") + else: + save(output / "proof.json", proof) + print("EXTB consolidation proof SHA-256: " + sha(output / "proof.json"), flush=True) + + +if __name__ == "__main__": + main() From 6bb7ef83d9808ce39456a7e166e81d2ed5244802 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 19:47:27 +0300 Subject: [PATCH 114/211] Record indexed returned-bank control and pending complete gate --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 55 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 13 +++++ 2 files changed, 68 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 8fe7b8c3..e245ad74 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9530,3 +9530,58 @@ into a fresh directory; V1 is not qualified evidence. No new full CPU, consumer or physical timing qualification is claimed. The 030 trace disagreement remains open. Milestone 6 remains **in progress**, `roadmapComplete=false`; no package publication occurs. + +## Indexed returned-bank recovery qualification in progress — 2026-10-07 + +The canonical returned-bank/refault proof did not qualify full indexed or +pointer-to-source recovery after saved S/M changes. A new isolated fixture +copies the 229 exact inputs from `Operand020ReturnRefaultFix` and adds one +test file, leaving its CPU source unchanged. It extends the independently +qualified indexed-pipe fixture with real writes to the saved SR. The pending +source/pointer locations are selected before CPU execution; expectations +account for deallocation of the old frame, the new exception stack, retained +cycle FC, saved instruction provenance, registers, flags, memory, following +instruction and successful-read ordering. + +The [MC68030 manual, sections 8.2.1–8.2.3](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P2.pdf) +defines the data cycle's address space through the SSW FC field, permits +handler changes to the saved SR and describes creating a new frame after +deallocating the previous frame when a restarted cycle faults. These defined +rules guide the assertions. The prototype's serialized C022 internal state, +saved three-word pipe and continuation suffix remain a selected private +transport policy; this does not qualify silicon pipeline state, physical +function-code spaces or arbitrary foreign frames. Incoming/returned trace +is disabled, leaving the independent trace disagreement open. + +The **completed focused control** passes **51,584 programs / 16 reports / +four executions**. It covers null base displacement, unsuppressed base/index, +all seven legal IIS structures (six indirect), An/PC sources, MOVE B/W/L and +MOVEA W/L, all four initial S/M images and user/ISP returns (eight bank pairs), +CCR 00/1F and each pointer/operand fault byte. Actual handlers retain all +three following-instruction words and edit the third. Source-only and +pointer-to-source paths execute on EC020, A1200, 020 and 030. + +With this focused fixture unchanged, removing only the scoped restoration +of the original data-cycle FC produces **19,968 precisely identified +mismatches** in chained S-changing cases; **31,616 controls pass**, with +zero unsupported/untested selected cases. The exact expected failure is +`Indexed pipe fault provenance differs`. Six copied-evidence controls +reject missing fixture, wrong producer, empty execution, wrong weights, +wrong parent identity and a missing mutation failure. Normal DLL identities +are unchanged. Focused proof: `indexed-return-bank-focus-proof.json`, SHA-256 +`f20baad77d5e7251dae1ddd4f7d5d0e4f8d18f35019eb195f83dfa403cc30d33`; +evidence under `mutations/Operand020IndexedReturnBank` and +`integrity/Operand020IndexedReturnBankFocus` in the restoration temporary root. + +The separate **full gate is still running**, not qualified: its required +selection is all sixteen bank pairs, all 66 full-index source structures +and 54 indirect structures, An/PC, all five MOVE/MOVEA forms, CCR 00/1F, +every fault byte, scalar/batch and retained third-word editing. Its planned +complete totals are **1,876,992 programs / 768 reports / 192 executions** +at `audits/Operand020IndexedReturnBankV1`. The focused evidence cannot +replace that full selection. Audit all source identities, reports, weights, +failure identifiers and actual TRX selection after terminal execution, +then perform full-gate integrity checks. No production CPU import, full CPU +or consumer claim, inventory reduction or publication occurs. Remaining +reference/foreign-frame/general fault gates remain required. Milestone 6 +stays **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 4735d7b3..06d9dc27 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3175,3 +3175,16 @@ regressions remain. This completes only the EXTB consolidation slice; remaining reference gaps, including the 030 trace disagreement, stay open. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [EXTB consolidation record](COPPER68K_REFERENCE_QUALIFICATION.md#extb-availability-regression-consolidation--2026-10-07). + +Indexed returned-bank qualification now has a completed focused control of +51,584 programs and an unchanged-fixture cycle-FC mutation detecting 19,968 +precise failures, plus six evidence corruption checks. The CPU source is +unchanged from the refault-corrected private snapshot. Its separate full +all-sixteen-bank / full-index source-and-chain selection is still running: +1,876,992 programs, 768 reports and 192 executions are required before its +gate can pass. Focused evidence cannot replace the complete selection. +Trace remains disabled in this slice; its disagreement and other required +reference/foreign-frame/general fault gates stay open. No production import +or publication occurs. Milestone 6 remains **in progress**, +`roadmapComplete=false`. See the +[indexed returned-bank progress record](COPPER68K_REFERENCE_QUALIFICATION.md#indexed-returned-bank-recovery-qualification-in-progress--2026-10-07). From b4a60c601725455866c2cc8fb693dce7dc4eff23 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 20:34:54 +0300 Subject: [PATCH 115/211] Complete indexed return-bank audit and consolidate address arithmetic --- Copper68k.Tests/M68020HdfBootTests.cs | 26 ----- .../Synthetic/SyntheticArithmeticTests.cs | 14 +++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 97 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 22 +++++ scripts/test-copper68k-synthetic.ps1 | 2 +- 5 files changed, 134 insertions(+), 27 deletions(-) diff --git a/Copper68k.Tests/M68020HdfBootTests.cs b/Copper68k.Tests/M68020HdfBootTests.cs index d8126c2b..8ea984d6 100644 --- a/Copper68k.Tests/M68020HdfBootTests.cs +++ b/Copper68k.Tests/M68020HdfBootTests.cs @@ -226,32 +226,6 @@ public void NotLongDisplacementUpdatesOnlyTheLongAndPreservesExtend(M68kCpuModel } } - [Theory] - [InlineData(M68kCpuModel.M68020)] - [InlineData(M68kCpuModel.M68EC020)] - [InlineData(M68kCpuModel.M68030)] - [InlineData(M68kCpuModel.M68040)] - public void AddaWordAddressRegisterSignExtendsAndAllowsAliasing(M68kCpuModel model) - { - foreach (var source in new uint[] { 0x12348000, 0xFFFF7FFF }) - foreach (var alias in new[] { false, true }) - { - var bus = new Copper68kTestBus(0x10000); - bus.WriteWord(0x1000, (ushort)(alias ? 0xD4CA : 0xD4CD)); - using var cpu = M68kCoreFactory.Default.Create(model, bus); - cpu.Reset(0x1000, 0x7000); - cpu.State.A[2] = alias ? source : 0x3000; - cpu.State.A[5] = source; - cpu.State.StatusRegister = 0x271F; - cpu.ExecuteInstruction(); - var increment = source == 0x12348000 ? -32768 : 32767; - Assert.Equal(unchecked((uint)((alias ? source : 0x3000) + increment)), cpu.State.A[2]); - Assert.Equal(source, cpu.State.A[5]); - Assert.Equal(0x271F, cpu.State.StatusRegister); - Assert.Equal(0x1002u, cpu.State.ProgramCounter); - } - } - [Theory] [InlineData(M68kCpuModel.M68020)] [InlineData(M68kCpuModel.M68EC020)] diff --git a/Copper68k.Tests/Synthetic/SyntheticArithmeticTests.cs b/Copper68k.Tests/Synthetic/SyntheticArithmeticTests.cs index 545e02b5..b1eae4b4 100644 --- a/Copper68k.Tests/Synthetic/SyntheticArithmeticTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticArithmeticTests.cs @@ -74,6 +74,20 @@ public void ArithmeticAddressingModesAndFullExtensions(string modelId) for (var dst = 0; dst < 8; dst++) Case(machine, report, family, width, new(0, src), 0x8002, 0x7ffffffe, 31, reg: dst, scenario: "all-register-fields"); } + // Address-register sources have separate encodings. Include every + // source/destination field and A7 bank, with alias CCR preservation + // and nonzero upper source words that expose missing sign extension. + foreach (var width in new[] { 2, 4 }) + foreach (var family in new[] { "ADDA", "SUBA", "CMPA" }) + foreach (var source in width == 2 + ? new[] { 0u, 1u, 0x12348000u, 0xffff7fffu } + : new[] { 0u, 1u, 0x80000000u, uint.MaxValue }) + for (var src = 0; src < 8; src++) + for (var dst = 0; dst < 8; dst++) + foreach (var supervisor in new[] { false, true }) + foreach (var ccr in src == dst ? Enumerable.Range(0, 32) : new[] { 0, 31 }) + Case(machine, report, family, width, new(1, src), source, 0x7ffffffe, ccr, + reg: dst, supervisor: supervisor, scenario: "all-address-register-fields"); report.Complete(output); } diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index e245ad74..9422827a 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9585,3 +9585,100 @@ then perform full-gate integrity checks. No production CPU import, full CPU or consumer claim, inventory reduction or publication occurs. Remaining reference/foreign-frame/general fault gates remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +## Complete indexed returned-bank recovery gate — 2026-10-07 + +The separately required full selection above has now finished and passed: +**1,876,992 programs / 768 reports / 192 executions**, with zero mismatching, +unsupported or untested selected cases and no skips. This comprises 439,296 +source-fault and 1,437,696 pointer-to-source programs across EC020, A1200, +020 and 030. All sixteen initial/returned S/M pairs, 66 full-index source +structures and 54 indirect structures execute with An/PC sources, MOVE +B/W/L and MOVEA W/L, CCR 00/1F, all fault bytes and scalar/batch boundaries. +The three-word saved pipe and its third-word handler edit are preserved. +The twelve direct structures have no pointer phase; their source-fault +coverage remains included rather than disappearing from the catalog. + +The audit independently expands every key and weight, verifies the exact +192-case TRX roster and its report summaries, and binds all 230 source/project +inputs to the unchanged 229-input refault-corrected parent plus the single +new fixture. Seven corruption checks reject a missing fixture, wrong producer, +empty selection, incorrect count, wrong parent, foreign returned-bank key +and changed fixture even when both manifests are refreshed. The frozen new +fixture identity is required independently of those mutable manifests. +Normal assemblies retain their protected identities. + +Complete proof: `indexed-return-bank-full-proof.json`, SHA-256 +`414d299db0e1bd6affcdeedfeff140a8880198134d3a59d588e03539d397c0a5`. +Evidence: `audits/Operand020IndexedReturnBankV1` and +`integrity/Operand020IndexedReturnBankFull` in the restoration temporary root. +The separate focused 51,584-program control, 19,968-mismatch FC mutation +and six focused corruption checks retain their own identities and scope; +they were not substituted for this complete run. + +This closes the selected indexed returned-bank gate on the private snapshot. +It does not qualify trace, other register/index/scaling choices, relocated or +foreign frames, broader fault origins, enabled MMU/cache operation or physical +pipeline/function-code behavior. Existing timing policy checks pass; silicon +timing is not claimed. Production CPU import, combined validation on this +snapshot, full CPU/consumers and other remaining reference gates stay required. +No package publication occurs. Milestone 6 remains **in progress**, +`roadmapComplete=false`. + +## Address-register arithmetic matrix and ADDA consolidation — 2026-10-07 + +The earlier exhaustive register-field loop used Dn sources; An sources sampled +only destinations A0/A1/A7. `ArithmeticAddressingModesAndFullExtensions` now +also covers every An source/destination field for ADDA, SUBA and CMPA W/L +on all eight profiles. Four source boundaries per size, both user/supervisor +stack selections, CCR 00/1F on distinct registers and all 32 CCR states on +aliases add **17,664 scenarios per profile / 141,312 total**. Nonzero upper +source words expose missing word sign extension, and all A7 aliases execute. +These cases reuse the shared operand and independent mathematical expectations. + +The expanded addressing selection passes **200,068 scenarios in eight batches**, +and its clean audit co-executes all 79 original HDF-boot regression cases: +**87 executions pass, no skips**. Independent verification expands all 768 +new keys per profile and their value/CCR weights, while requiring the exact +unchanged earlier keys from the pinned 707-report audit. No older evidence +is relabeled. Ordinary required arithmetic-addressing counts rise from +4,667/8,237 to **22,331/25,901** (early/later models); the ordinary inventory +now requires **86,240,034 cases / 707 reports**. This is its updated requirement, +not a claim of a fresh complete-suite execution. + +A targeted ADDA word-An zero-extension mutation covers both the advanced +handler and base-core path, with fixtures unchanged. It produces exactly +**6,080 mismatches across all eight profiles** (760 each), while 193,988 +selected scenarios pass. All four original +`AddaWordAddressRegisterSignExtendsAndAllowsAliasing` witnesses also fail +with the intended result difference. An earlier generic-arithmetic probe +only affected SUBA on five profiles, producing 3,800 precise failures while +the original ADDA witnesses passed. That probe is retained separately and +was not accepted as ADDA replacement proof. The verifier's preliminary +short roster omitted parameterized PC tests; its rejection led to an explicit +complete 79-case catalog before acceptance, without changing test outcomes. + +Six corruption checks reject missing register coverage, incorrect alias CCR +weight, empty execution, missing original witness, wrong mutation producer +and a changed mutation even when its manifests are refreshed. Retire only +the pure semantic ADDA method's four model executions after this proof. +Its exact replacement anchor is +`68020/ADDA/2/A2/r2/all-address-register-fields/brief/super=True/op=D4CA/s=12348000/d=7FFFFFFE/ccr=1F`. +All 75 other HDF-boot executions, including their timing assertions, remain. +The reduced repository selection passes **83 executions without skips**; +its eight shared batches again report all 200,068 passing scenarios. + +Semantic proof: `arithmetic-address-register-proof.json`, SHA-256 +`a58eecd4412ae570b4619de32b70bdde84e90a0307b632dde2e233a4f17fa2b7`. +Integrity proof: `arithmetic-address-register-integrity-proof.json`, SHA-256 +`9578db78c237a5865f90e74bb44cae8e05fd14fb0c7a573bf15ddca62940a266`. +Final retirement proof: `address-arithmetic-retirement-proof.json`, SHA-256 +`960bbe6eaa07d9441e53a0642337954078d4252c9cc0ddcdcbcb42bfc46a21c0`. +Evidence is under `audits/ArithmeticAddressRegisters`, +`audits/ArithmeticAddressRegistersCurrent`, `mutations/AddaAddressRegisters`, +`mutations/ArithmeticAddressRegisters` and `integrity/ArithmeticAddressRegisters`. +Current reduced TRX SHA-256: +`093db48e491a48ab0a0cf2a254aa957cb1b1543b9a27320cafcd327171b72c88`. +No production CPU change, package/consumer requalification or physical timing +claim occurs. Protected normal DLLs are unchanged. Broader milestone-6 gates, +including the 030 trace disagreement, remain open; `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 06d9dc27..3617b97f 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3188,3 +3188,25 @@ reference/foreign-frame/general fault gates stay open. No production import or publication occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [indexed returned-bank progress record](COPPER68K_REFERENCE_QUALIFICATION.md#indexed-returned-bank-recovery-qualification-in-progress--2026-10-07). + +The complete indexed returned-bank run above now passes all 1,876,992 programs, +768 reports and 192 executions, with seven complete-gate corruption controls. +All sixteen bank pairs and all 66 source/54 indirect structures are audited +on the unchanged private refault-corrected CPU snapshot. The focused mutation +retains its separate scope. This closes that selected private gate only; +trace, broader/foreign origins, combined/full CPU/consumer validation and +remaining qualification requirements stay open. See the +[complete indexed returned-bank record](COPPER68K_REFERENCE_QUALIFICATION.md#complete-indexed-returned-bank-recovery-gate--2026-10-07). + +The shared arithmetic matrix now covers every An source/destination field +for ADDA/SUBA/CMPA W/L, both stack modes and all alias CCRs, adding 141,312 +scenarios. Its 200,068-scenario addressing selection and 79 original HDF-boot +regressions pass. A two-path ADDA sign-extension mutation fails all eight +profiles and all four original ADDA witnesses; six corruption checks pass. +Only that pure semantic method is retired, leaving 75 other HDF-boot +executions, including timing checks. The reduced 83-execution selection passes +without skips. The ordinary requirement becomes 86,240,034 scenarios in +707 reports; a fresh complete-suite run is not claimed by this test-only +change. No production CPU import or publication occurs. Milestone 6 remains +**in progress**, `roadmapComplete=false`. See the +[address-register matrix and consolidation record](COPPER68K_REFERENCE_QUALIFICATION.md#address-register-arithmetic-matrix-and-adda-consolidation--2026-10-07). diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 8a3d6c55..b11b906c 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -69,7 +69,7 @@ try { 'transfer-moveq-unassigned-words'=131072; 'transfer-movep'=3708; 'transfer-movem'=$(if ($model -in @('68000','68010')) {3196} else {3592}); 'transfer-movem-invalid-operands'=6400; - 'arithmetic-boundaries'=56448; 'arithmetic-addressing'=$(if ($model -in @('68000','68010')) {4667} else {8237}); + 'arithmetic-boundaries'=56448; 'arithmetic-addressing'=$(if ($model -in @('68000','68010')) {22331} else {25901}); 'arithmetic-scenarios'=21072; 'arithmetic-extend'=25440; 'arithmetic-invalid-operands'=$(if ($model -in @('68000','68010')) {6336} else {5952}); 'arithmetic-quick-invalid-operands'=26624; From 35e2693bea2cd39cb368a93c75573d7d65b9fcc9 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 20:40:35 +0300 Subject: [PATCH 116/211] Record latest corrected combined recovery replay and provisional audit --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 28 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 9 ++++++ 2 files changed, 37 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 9422827a..96753b49 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9682,3 +9682,31 @@ Current reduced TRX SHA-256: No production CPU change, package/consumer requalification or physical timing claim occurs. Protected normal DLLs are unchanged. Broader milestone-6 gates, including the 030 trace disagreement, remain open; `roadmapComplete=false`. + +## Latest refault-corrected combined replay in progress — 2026-10-07 + +The preceding 11,109,248-program combined proof qualifies the relocation-only +CPU, not the later returned-bank and scoped original-FC corrections. A fresh +isolated replay now executes all five unchanged complete selections on the +latest indexed returned-bank parent: indexed pipe, indexed/chained trace, +A7/CCR aliases, moved frames and entry/internal-load faults. Required coverage +remains **11,109,248 programs / 5,744 reports / 1,436 executions**. Its 230 +source/project inputs match that qualified parent exactly; no CPU or fixture +edit, package or production import is made. + +A provisional independent comparison verifies **721,984 passing scenarios +in 599 reports**, with no changed keys, weights or results relative to +the five pinned original proofs. Four controls precisely reject omitted and +changed fixtures, wrong selection-parent evidence and altered report weights. +This is an observation of completed reports while execution continues, not +proof of the complete TRX roster or a passing complete gate. The full replay, +exact terminal execution/producer/source audit and full integrity controls +remain required. Existing trace results are software-policy retention; +the unresolved 030 manual/reference disagreement is not promoted by replay. + +Evidence: `audits/Operand020CombinedRefaultV1`, +`combined-refault-progress.json` and +`integrity/Operand020CombinedRefaultV1/Partial` in the restoration temporary +root. Progress record SHA-256: `5697e88d14180dcb990ed43a668bc2baab0d3ce3899fc2d319c6056eee213747`. +Milestone 6 remains **in progress**, `roadmapComplete=false`; broader recovery, +foreign origins, full CPU/consumers and other reference requirements remain. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 3617b97f..261a789b 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3210,3 +3210,12 @@ without skips. The ordinary requirement becomes 86,240,034 scenarios in change. No production CPU import or publication occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [address-register matrix and consolidation record](COPPER68K_REFERENCE_QUALIFICATION.md#address-register-arithmetic-matrix-and-adda-consolidation--2026-10-07). + +A fresh complete five-selection replay is now running on the latest scoped-FC +and returned-bank corrected private CPU, with all 230 parent inputs unchanged. +The required 11,109,248 programs / 5,744 reports / 1,436 executions remain +pending. A provisional 721,984-scenario / 599-report comparison and +four integrity controls pass; they do not replace the terminal full gate. +No production import/publication or architectural trace qualification occurs. +Milestone 6 remains **in progress**, `roadmapComplete=false`. See the +[latest combined replay progress record](COPPER68K_REFERENCE_QUALIFICATION.md#latest-refault-corrected-combined-replay-in-progress--2026-10-07). From 125f5a7a3218943be58ea28f9a68da73acbe02aa Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 20:55:09 +0300 Subject: [PATCH 117/211] Make address arithmetic consolidation independently reproducible --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 42 +++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 11 + ...per68k-address-arithmetic-consolidation.py | 276 ++++++++++++++++++ 3 files changed, 329 insertions(+) create mode 100644 scripts/test-copper68k-address-arithmetic-consolidation.py diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 96753b49..d07a2cc5 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9710,3 +9710,45 @@ Evidence: `audits/Operand020CombinedRefaultV1`, root. Progress record SHA-256: `5697e88d14180dcb990ed43a668bc2baab0d3ce3899fc2d319c6056eee213747`. Milestone 6 remains **in progress**, `roadmapComplete=false`; broader recovery, foreign origins, full CPU/consumers and other reference requirements remain. + +## Maintained address-arithmetic consolidation audit — 2026-10-07 + +The ADDA retirement above now has a repository-owned reproduction command, +[test-copper68k-address-arithmetic-consolidation.py](../scripts/test-copper68k-address-arithmetic-consolidation.py). +It requires Python 3, Git and dotnet, a fresh output directory, and access to +the pinned historical commit `6bb7ef83d9808ce39456a7e166e81d2ed5244802`. +It regenerates historical addressing reports from that commit's test source; +no earlier temporary reports, ROMs, media or external test binaries are needed. +All compilation and intentional CPU defects stay in isolated output copies. + +```text +python scripts/test-copper68k-address-arithmetic-consolidation.py --output artifacts/address-arithmetic-audit +python scripts/test-copper68k-address-arithmetic-consolidation.py --output artifacts/address-arithmetic-audit --validate-only +``` + +The fresh complete audit and report-only replay both pass. Historical coverage +is **58,756 scenarios / 87 executions**, including all 79 original HDF-boot +regressions. The expanded clean selection is **200,068 / 87**, and the current +selection after the documented retirement is **200,068 / 83**, retaining all +75 other HDF-boot executions. Each passing selection has zero skips. +The sole two-path word-An ADDA zero-extension mutation produces exactly +**6,080 shared mismatches / 193,988 passing controls**; all eight shared +batches and all four original witnesses fail, with precise result reasons. +Independent formulas expand the 768 added combinations and their value/CCR +weights per profile, while preserving the regenerated historical keys. + +Seven corruption controls reject missing fixtures, wrong producer, empty +selection, altered weights, unrelated failure reasons, changed CPU mutation +even with its manifest refreshed, and omitted original witnesses. Exact +source inventories, historical pin, selected TRX names/outcomes, report +summaries, protected normal DLLs and aggregate proof are checked on replay. +Only the original pure semantic method is absent from the current source; +its exact removal is verified against Git. This adds no further retirement, +production CPU change or architectural/hardware reference claim. + +Fresh evidence: `audits/AddressArithmeticMaintainedV1` in the restoration +temporary root; `proof.json` SHA-256 +`22178eb139ff644d72fb76c1e131bf6113e09493a73c1aed8f1bc9fdfaa3b3ab`. +The earlier evidence keeps its separate identity. The latest private combined +recovery replay is still pending; no full CPU/consumer claim, package +publication or roadmap completion occurs. Milestone 6 remains **in progress**. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 261a789b..4f28a74e 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3219,3 +3219,14 @@ four integrity controls pass; they do not replace the terminal full gate. No production import/publication or architectural trace qualification occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [latest combined replay progress record](COPPER68K_REFERENCE_QUALIFICATION.md#latest-refault-corrected-combined-replay-in-progress--2026-10-07). + +ADDA consolidation now also has a maintained, isolated reproduction command: +`python scripts/test-copper68k-address-arithmetic-consolidation.py --output artifacts/address-arithmetic-audit` +and the same command with `--validate-only` replays the complete proof. +Fresh pinned historical, expanded clean, two-path mutation and current reduced +selections pass their required gates, together with seven integrity controls. +The historical reports are regenerated without temporary reference inputs; +all eight profiles and four original witnesses detect the intended defect. +No further test retirement, CPU change or publication occurs. The latest +combined private replay and broader milestone-6 requirements stay pending. +See the [maintained consolidation record](COPPER68K_REFERENCE_QUALIFICATION.md#maintained-address-arithmetic-consolidation-audit--2026-10-07). diff --git a/scripts/test-copper68k-address-arithmetic-consolidation.py b/scripts/test-copper68k-address-arithmetic-consolidation.py new file mode 100644 index 00000000..e6c963a0 --- /dev/null +++ b/scripts/test-copper68k-address-arithmetic-consolidation.py @@ -0,0 +1,276 @@ +"""Reproduce address-register arithmetic coverage and ADDA retirement proof. + +Requires Python 3, Git and dotnet. Use a fresh --output; --validate-only +rechecks existing evidence. All builds and intentional defects stay isolated. +Historical reports are regenerated from pinned test source, not temporary data. +""" +import argparse +import hashlib +import itertools +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import xml.etree.ElementTree as ET + +REPO = Path(__file__).resolve().parent.parent +HELPER = "scripts/test-copper68k-address-arithmetic-consolidation.py" +PIN = "6bb7ef83d9808ce39456a7e166e81d2ed5244802" +LEGACY = "Copper68k.Tests/M68020HdfBootTests.cs" +MATRIX = "Copper68k.Tests/Synthetic/SyntheticArithmeticTests.cs" +MODELS = ["68000", "68010", "68EC020", "68020", "68030", "68040", "68060", "A1200"] +WITNESS = "AddaWordAddressRegisterSignExtendsAndAllowsAliasing" +NS = {"t": "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"} + + +def check(condition, reason): + if not condition: + raise ValueError(reason) + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def load(path): + return json.loads(path.read_text(encoding="utf-8-sig")) + + +def save(path, data): + path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8", newline="\n") + + +def pinned(path): + return subprocess.check_output(["git", "show", PIN + ":" + path], cwd=REPO).decode("utf-8-sig").replace("\r\n", "\n") + + +def tracked(): + return [p for p in subprocess.check_output(["git", "ls-files", "Copper68k", "Copper68k.Tests"], cwd=REPO, text=True).splitlines() + if Path(p).suffix in (".cs", ".csproj")] + + +def inventory(source): + return {p.relative_to(source).as_posix(): sha(p) for p in source.rglob("*") + if p.suffix in (".cs", ".csproj") and not {"bin", "obj"}.intersection(p.parts)} + + +def normal_paths(): + return ["Copper68k/bin/Release/net10.0/Copper68k.dll", "Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll"] + + +def mutate(path, text): + if path == "Copper68k/M68kAdvancedTimingInterpreter.cs": + start = text.index(" private void ExecuteAddaWordAddressToAddress(") + end = text.index(" private void ExecuteAddaWordDataToAddress(", start) + old = "var source = unchecked((short)State.A[opcode & 7]);" + block = text[start:end] + check(block.count(old) == 1, "Missing advanced mutation target") + return text[:start] + block.replace(old, "var source = unchecked((ushort)State.A[opcode & 7]); // intentional zero extension") + text[end:] + old = "SetAddressRegister(reg, State.A[reg] + M68kCpuState.SignExtend(value, size));" + check(text.count(old) == 1, "Missing base mutation target") + return text.replace(old, "SetAddressRegister(reg, State.A[reg] + (mode == 1 && size == M68kOperandSize.Word ? value & 0xffffu : M68kCpuState.SignExtend(value, size))); // intentional zero extension") + + +def expected_sources(mode): + sources = {p: (REPO / p).read_bytes() for p in tracked()} + if mode != "Current": + sources[LEGACY] = pinned(LEGACY).encode() + if mode == "Historical": + sources[MATRIX] = pinned(MATRIX).encode() + if mode == "AddaZeroExtend": + for p in ["Copper68k/M68kAdvancedTimingInterpreter.cs", "Copper68k/M68kCore.cs"]: + sources[p] = mutate(p, (REPO / p).read_text(encoding="utf-8-sig")).encode() + return sources + + +def legacy_names(): + names = {} + for attributes, name in re.findall(r"(\[Theory\][\s\S]*?)public void (\w+)\(M68kCpuModel model\)", pinned(LEGACY)): + for model in re.findall(r"\[InlineData\(M68kCpuModel\.(\w+)\)\]", attributes): + names[f"Copper68k.Tests.M68020HdfBootTests.{name}(model: {model})"] = "Passed" + check(len(names) == 43, "Wrong simple historical roster") + for model, displacement in [("M68020", -128), ("M68EC020", 128), ("M68030", -128), ("M68040", 128)]: + names[f"Copper68k.Tests.M68020HdfBootTests.SubaLongPcDisplacementUsesFullLongAndPreservesFlags(model: {model}, displacement: {displacement})"] = "Passed" + for model, displacement, values in itertools.product(["M68020", "M68EC020", "M68030", "M68040"], [-128, 128], + [(0, 1, 1, 0), (0xffffffff, 1, 0, 21), (0x7fffffff, 1, 0x80000000, 10), (0x80000000, 0x80000000, 0, 23)]): + destination, source, result, flags = values + names[f"Copper68k.Tests.M68020HdfBootTests.AddLongPcDisplacementReadsMemoryFromExtensionPcAndSetsArithmeticFlags(model: {model}, displacement: {displacement}, destination: {destination}, source: {source}, result: {result}, flags: {flags})"] = "Passed" + check(len(names) == 79, "Wrong complete historical roster") + return names + + +def execute(root, mode): + for p, contents in expected_sources(mode).items(): + target = root / "source" / p + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(contents) + selection = WITNESS if mode == "AddaZeroExtend" else "M68020HdfBootTests" + command = ["dotnet", "test", str(root / "source/Copper68k.Tests/Copper68k.Tests.csproj"), "-c", "Release", + "--artifacts-path", str(root / "build"), "--filter", f"FullyQualifiedName~ArithmeticAddressingModesAndFullExtensions|FullyQualifiedName~{selection}", + "--logger", "trx;LogFileName=audit.trx", "--results-directory", str(root)] + data = dict(mode=mode, pin=PIN, producer=sha(REPO / HELPER), sources=inventory(root / "source"), + protected={p: sha(REPO / p) for p in normal_paths() if (REPO / p).exists()}, command=command) + env = dict(os.environ, COPPER68K_SYNTHETIC_REPORT_DIR=str(root), COPPER68K_SYNTHETIC_MODELS=",".join(MODELS)) + print(mode + ": executing all eight matrices and selected historical witnesses", flush=True) + with (root / "execution.log").open("w", encoding="utf-8") as log: + data["testExit"] = subprocess.run(command, cwd=REPO, env=env, stdout=log, stderr=subprocess.STDOUT).returncode + data["logSha256"] = sha(root / "execution.log") + save(root / "inputs.json", data) + + +def verify(root, mode, historical_root): + data = load(root / "inputs.json") + check(data["mode"] == mode and data["pin"] == PIN, "Wrong selection pin") + check(data["producer"] == sha(REPO / HELPER), "Wrong producer") + expected_ids = {p: hashlib.sha256(contents).hexdigest() for p, contents in expected_sources(mode).items()} + check(data["sources"] == expected_ids, "Wrong baseline or mutation identity") + check(inventory(root / "source") == expected_ids, "Incomplete or changed source inventory") + check(set(data["protected"]) == {p for p in normal_paths() if (REPO / p).exists()}, "Incomplete protected identities") + for p, h in data["protected"].items(): + check(sha(REPO / p) == h, "Changed protected assembly") + check(data["testExit"] == (1 if mode == "AddaZeroExtend" else 0), "Wrong test exit") + check(data["logSha256"] == sha(root / "execution.log"), "Changed execution log") + names = legacy_names() + if mode == "Current": + names = {p: status for p, status in names.items() if WITNESS not in p} + if mode == "AddaZeroExtend": + names = {p: "Failed" for p in names if WITNESS in p} + for model in MODELS: + names[f'Copper68k.Tests.Synthetic.SyntheticArithmeticTests.ArithmeticAddressingModesAndFullExtensions(modelId: "{model}")'] = "Failed" if mode == "AddaZeroExtend" else "Passed" + tree = ET.parse(root / "audit.trx") + tests = tree.findall(".//t:UnitTestResult", NS) + check(len(tests) == len(names) and {t.get("testName") for t in tests} == set(names), "Wrong or empty execution selection") + for test in tests: + check(test.get("outcome") == names[test.get("testName")], "Wrong execution outcome") + if mode == "AddaZeroExtend" and WITNESS in test.get("testName"): + message = test.find("t:Output/t:ErrorInfo/t:Message", NS).text + check(re.search(r"Expected:\s*4294946816\b", message) and re.search(r"Actual:\s*45056\b", message), "Wrong historical failure reason") + stdout = "\n".join(t.text or "" for t in tree.findall(".//t:UnitTestResult/t:Output/t:StdOut", NS)) + total = misses = 0 + report_ids = [] + for model in MODELS: + path = root / f"{model}-arithmetic-addressing.json" + report = load(path) + old = load(historical_root / path.name) + expected = dict(old["combinations"]) + old_count = 4667 if model in ["68000", "68010"] else 8237 + check(old["logicalCases"] == old_count and old["counts"] == dict(passing=old_count, mismatching=0, unsupported=0, untested=0) + and sum(sum(row.values()) for row in expected.values()) == old_count and all(set(row) == {"passing"} for row in expected.values()), "Wrong historical complete matrix") + failures = {} + if mode == "AddaZeroExtend": + for src, dst in itertools.product(range(8), [0, 1, 7]): + key = f"{model}/ADDA/2/A{src}/r{dst}/canonical/brief/super=True" + check(expected[key] == {"passing": 1}, "Missing historical canonical key") + expected[key] = {"mismatching": 1} + lhs = 0x8002 if src == dst else 0x7ffffffe + failures[f"{key}/op={0xd0c8 | (dst << 9) | src:04X}/s=00008002/d=7FFFFFFE/ccr=1F"] = f"A{dst} expected {(lhs + 0xffff8002) & 0xffffffff:08X}, actual {(lhs + 0x8002) & 0xffffffff:08X}" + if mode != "Historical": + for family, width, src, dst, supervisor in itertools.product(["ADDA", "SUBA", "CMPA"], [2, 4], range(8), range(8), [False, True]): + key = f"{model}/{family}/{width}/A{src}/r{dst}/all-address-register-fields/brief/super={supervisor}" + check(key not in expected, "Duplicate added combination") + ccrs = range(32) if src == dst else [0, 31] + count = 4 * len(ccrs) + bad = mode == "AddaZeroExtend" and family == "ADDA" and width == 2 + expected[key] = {"passing": count - len(ccrs), "mismatching": len(ccrs)} if bad else {"passing": count} + if bad: + lhs = 0x12348000 if src == dst else 0x7ffffffe + for ccr in ccrs: + failures[f"{key}/op={0xd0c8 | (dst << 9) | src:04X}/s=12348000/d=7FFFFFFE/ccr={ccr:02X}"] = f"A{dst} expected {(lhs + 0xffff8000) & 0xffffffff:08X}, actual {(lhs + 0x8000) & 0xffffffff:08X}" + count = old_count + (0 if mode == "Historical" else 17664) + missed = len(failures) + counts = dict(passing=count - missed, mismatching=missed, unsupported=0, untested=0) + check(report["schema"] == 1 and report["model"] == model and report["group"] == "arithmetic-addressing" + and report["logicalCases"] == count and report["xunitBatches"] == 1, "Wrong report identity") + check(report["counts"] == counts and report["combinations"] == expected, "Wrong complete keys or weights") + check(len(report["failures"]) == missed, "Incomplete failure roster") + for f in report["failures"]: + check(f["status"] == "mismatching" and f["id"] in failures and f["reason"] == failures.pop(f["id"]), "Wrong failure reason") + check(not failures, "Missing precise failures") + matches = re.findall(re.escape(f"{model}/arithmetic-addressing: ") + r"(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)", stdout) + check(len(matches) == 1 and json.loads(matches[0][0]) == counts and int(matches[0][1]) == count and int(matches[0][2]) == len(expected), "TRX report disagreement") + total += count; misses += missed + report_ids.append(dict(path=path.name, sha256=sha(path))) + check(total == (58756 if mode == "Historical" else 200068) and misses == (6080 if mode == "AddaZeroExtend" else 0), "Wrong complete totals") + check({p.name for p in root.glob("*-arithmetic-addressing.json")} == {row["path"] for row in report_ids}, "Foreign report selection") + return dict(mode=mode, passing=total - misses, mismatching=misses, executions=len(tests), sources=len(expected_ids), + reports=report_ids, inputsSha256=sha(root / "inputs.json"), trxSha256=sha(root / "audit.trx")) + + +def integrity(output, validate_only): + definitions = [("MissingFixture", "Incomplete or changed source inventory"), ("WrongProducer", "Wrong producer"), + ("EmptySelection", "Wrong or empty execution selection"), ("WrongWeight", "Wrong complete keys or weights"), + ("WrongFailureReason", "Wrong failure reason"), ("ChangedMutationAndManifest", "Wrong baseline or mutation identity"), + ("MissingHistoricalWitness", "Wrong or empty execution selection")] + rows = [] + for name, reason in definitions: + root = output / "integrity" / name + if not validate_only: + base = output / "AddaZeroExtend" + shutil.copytree(base / "source", root / "source", ignore=shutil.ignore_patterns("bin", "obj")) + for p in [base / "inputs.json", base / "audit.trx", base / "execution.log", *base.glob("*-arithmetic-addressing.json")]: + shutil.copyfile(p, root / p.name) + if name == "MissingFixture": + (root / "source" / MATRIX).unlink() + elif name == "WrongProducer": + data = load(root / "inputs.json"); data["producer"] = "0" * 64; save(root / "inputs.json", data) + elif name in ["EmptySelection", "MissingHistoricalWitness"]: + tree = ET.parse(root / "audit.trx"); parent = tree.find("t:Results", NS) + for child in list(parent): + if name == "EmptySelection" or WITNESS in child.get("testName", ""): + parent.remove(child) + tree.write(root / "audit.trx") + elif name == "ChangedMutationAndManifest": + p = "Copper68k/M68kCore.cs"; target = root / "source" / p + target.write_text(target.read_text(encoding="utf-8-sig") + "\n// unrelated CPU mutation\n", encoding="utf-8") + data = load(root / "inputs.json"); data["sources"][p] = sha(target); save(root / "inputs.json", data) + else: + p = root / "68020-arithmetic-addressing.json"; data = load(p) + if name == "WrongWeight": + key = next(k for k in data["combinations"] if "all-address-register-fields" in k) + row = data["combinations"][key]; row[next(iter(row))] += 1 + else: + data["failures"][0]["reason"] = "unrelated failure" + save(p, data) + try: + verify(root, "AddaZeroExtend", output / "Historical") + except ValueError as error: + check(str(error) == reason, "Wrong integrity rejection: " + name + ": " + str(error)) + rows.append(dict(name=name, reason=reason)) + else: + raise ValueError("Corrupt evidence accepted: " + name) + return rows + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--validate-only", action="store_true") + args = parser.parse_args(); output = args.output.resolve() + historical = pinned(LEGACY) + start = historical.rfind(" [Theory]\n", 0, historical.index(" public void " + WITNESS + "(")) + end = historical.index(" [Theory]\n", historical.index(" public void " + WITNESS + "(")) + check(start >= 0 and (REPO / LEGACY).read_text(encoding="utf-8-sig") == historical[:start] + historical[end:], "Wrong legacy retirement scope") + if not args.validate_only: + check(not output.exists(), "Use a fresh output directory"); output.mkdir(parents=True) + entries = [] + for mode in ["Historical", "Clean", "AddaZeroExtend", "Current"]: + root = output / mode + if not args.validate_only: + root.mkdir(); execute(root, mode) + entries.append(verify(root, mode, output / "Historical")) + print(f"{mode}: {entries[-1]['passing']} passing / {entries[-1]['mismatching']} precise mismatches", flush=True) + proof = dict(schema=1, scope="Every An source/destination ADDA/SUBA/CMPA W/L field and proven semantic ADDA retirement; no physical timing claim", + pin=PIN, producerSha256=sha(REPO / HELPER), entries=entries, integrity=integrity(output, args.validate_only), + addedScenarios=141312, originalWitnessesDetected=4, retainedLegacyExecutions=75, retirementApplied=True, + productionCpuChanged=False, publication=False, roadmapComplete=False) + if args.validate_only: + check(load(output / "proof.json") == proof, "Changed aggregate proof") + else: + save(output / "proof.json", proof) + print("Address arithmetic consolidation proof SHA-256: " + sha(output / "proof.json"), flush=True) + + +if __name__ == "__main__": + main() From 1fcc8c0d8e8b3a31c0b2cc7631ce70c4f58cbedf Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 21:18:21 +0300 Subject: [PATCH 118/211] Start current CPU-suite qualification on latest private recovery source --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 39 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 11 ++++++ 2 files changed, 50 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index d07a2cc5..c40627dc 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9752,3 +9752,42 @@ temporary root; `proof.json` SHA-256 The earlier evidence keeps its separate identity. The latest private combined recovery replay is still pending; no full CPU/consumer claim, package publication or roadmap completion occurs. Milestone 6 remains **in progress**. + +## Current tests on latest private CPU — full replay started, 2026-10-07 + +The remaining full-suite integration gate now executes an isolated snapshot +containing the latest qualified private CPU and current mainline tests. +All **38 CPU source/project inputs** match the indexed returned-bank parent +exactly. All **167 test/project inputs** match current mainline at +`125f5a7a3218943be58ea28f9a68da73acbe02aa`, preserving the expanded arithmetic +matrix and the CMPM, EXTB and ADDA retirements. Four existing CPU files differ +from production and the private operand-continuation file is added only to +the frozen copy. The 26 private recovery fixture additions are not imported +or credited as full-suite executions; their complete combined replay remains +separate and uses the identical CPU source. + +The preflight audit verifies all **205 source/project identities**, ten +unchanged pinned native presets and both protected normal assemblies. +Eight corruption checks reject wrong snapshot/producer, absent source or +source identity, changed CPU with refreshed manifests, changed tests, +incorrect native selection and missing protected-assembly records. +Preflight proof: `latest-private-full-preflight-proof.json`, SHA-256 +`3ee757123db85d5d35d85d1a6578aeda3c816d98026489b63879d73cb004efad`. This verifies inputs only, not completed tests. + +The required complete roster is derived from the prior 5,324-case TRX minus +the nine proven retirements: **5,315 executions**, expecting **5,282 passing +and the same 33 explicitly unavailable optional/discovery cases**. The strict +terminal audit must verify exact names/outcomes, **865 profile reports**, +the updated **707 ordinary reports / 86,240,034 scenarios**, all ten native +presets and their controls. Historical keys/results remain pinned; the eight +arithmetic reports use the separately pinned maintained audit. Only native +CPU/test assembly identity fields may differ, and those must bind to the +actual frozen build. Skips cannot be counted as successful optional coverage. + +Both this run at `audits/LatestPrivateFullCpuV1` and the 11,109,248-program +combined run remain pending. Start record: `latest-private-full-start.json`, +SHA-256 `1c2c1fbdb335608a3500646e0d57b191a680e4e1b712b40200186058aaf9b6b6` in the restoration temporary root. +Full execution/integrity, wider recovery/reference gates and consumers stay +required. No production import, package publication, architectural trace or +physical timing qualification occurs. Milestone 6 remains **in progress**, +`roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 4f28a74e..84eb9b3a 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3230,3 +3230,14 @@ all eight profiles and four original witnesses detect the intended defect. No further test retirement, CPU change or publication occurs. The latest combined private replay and broader milestone-6 requirements stay pending. See the [maintained consolidation record](COPPER68K_REFERENCE_QUALIFICATION.md#maintained-address-arithmetic-consolidation-audit--2026-10-07). + +The full CPU integration replay now runs on an isolated 205-input snapshot: +38 exact latest private CPU inputs and 167 current test/project inputs, with +all three documented retirements and expanded arithmetic coverage preserved. +Its positive preflight and eight corruption controls pass. Required terminal +coverage remains 5,315 executions (5,282 passing / 33 unavailable), 865 profile +reports, 707 ordinary reports / 86,240,034 scenarios and ten pinned native +presets. Both full CPU and combined private recovery executions are pending; +input qualification is not execution completion. No production import or +publication occurs. Milestone 6 stays **in progress**, `roadmapComplete=false`. +See the [current-tests/private-CPU full replay record](COPPER68K_REFERENCE_QUALIFICATION.md#current-tests-on-latest-private-cpu--full-replay-started-2026-10-07). From 77d5f001eb7858d1ba362f081617cdfbd1fc2573 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 21:56:41 +0300 Subject: [PATCH 119/211] Qualify complete recovery replay on latest private CPU --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 35 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 10 ++++++ 2 files changed, 45 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index c40627dc..b1ed9165 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9791,3 +9791,38 @@ Full execution/integrity, wider recovery/reference gates and consumers stay required. No production import, package publication, architectural trace or physical timing qualification occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## Latest refault-corrected combined replay complete — 2026-10-07 + +The complete five-selection integration gate now passes on the latest private +returned-bank and scoped-original-FC CPU: **11,109,248 scenarios / 5,744 reports / +1,436 executions**, with zero mismatches, unsupported forms or untested selected +combinations and no skipped executions. All 230 source/project inputs remain +identical to the qualified indexed returned-bank parent. Exact report keys, +weights and outcomes match the five original independently pinned selections; +TRX names, outcomes and per-report summaries agree with those reports. This +supersedes the provisional observations only for this complete selected gate. + +Seven full evidence controls reject omitted/changed fixtures, wrong parent +proof, altered weights, wrong producer, empty execution selection and changed +requested selection. Both protected normal assemblies remain unchanged. +Evidence is `audits/Operand020CombinedRefaultV1` and +`integrity/Operand020CombinedRefaultV1/Full` in the restoration temporary root. +Aggregate `combined-refault-complete-proof.json` SHA-256: +`8e0f69acef6ee982175952a438c17bd0b097888cbf1ddba53b1c64117bf09a21`. + +Trace cases here retain the private software policy; passing them does not +resolve the separate 030 manual/reference disagreement. The current-tests +full CPU replay remains pending, as do consumer qualification, broader fault +origins/recovery and the other reference/consolidation requirements. No +production import, package publication or physical timing qualification occurs. +Milestone 6 remains **in progress**, `roadmapComplete=false`. + +Hardware availability was explicitly confirmed unavailable by the user. +A source-only inspection of [Moira](https://github.com/dirkwhoffmann/Moira/tree/ce28239b0507ebfe1bf2ad2b9c9252dc2f2cc031) +at commit `ce28239b0507ebfe1bf2ad2b9c9252dc2f2cc031` excludes it as a 030/040 +fault-recovery trace oracle: `MoiraTypes.h` labels those models disassembler-only, +and the 020 format-B RTE path in `MoiraExec_cpp.h` discards saved recovery words +before restoring SR/PC and refilling prefetch. No reference cases were executed +or credited. The source hashes and inspection scope are retained separately in +`moira-capability-v1.json`; the architectural trace question stays unresolved. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 84eb9b3a..af23629d 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3241,3 +3241,13 @@ presets. Both full CPU and combined private recovery executions are pending; input qualification is not execution completion. No production import or publication occurs. Milestone 6 stays **in progress**, `roadmapComplete=false`. See the [current-tests/private-CPU full replay record](COPPER68K_REFERENCE_QUALIFICATION.md#current-tests-on-latest-private-cpu--full-replay-started-2026-10-07). + +The latest returned-bank/scoped-FC private CPU now completes the unchanged +five-selection combined gate: 11,109,248 scenarios / 5,744 reports / 1,436 +passing executions, zero skips, with exact original coverage and seven full +integrity controls. This supersedes provisional evidence for that selection. +Full current CPU/consumer qualification and broader roadmap gates remain +required. Hardware is unavailable and the trace boundary stays unresolved; +no production import/publication or architectural trace promotion occurs. +Milestone 6 stays **in progress**, `roadmapComplete=false`. See the +[complete corrected combined record](COPPER68K_REFERENCE_QUALIFICATION.md#latest-refault-corrected-combined-replay-complete--2026-10-07). From 3439d71f6b89b3b48d2c8b0070af7f1b4188ec0f Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 22:06:45 +0300 Subject: [PATCH 120/211] Qualify current full CPU suite on latest private recovery source --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 36 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 10 ++++++ 2 files changed, 46 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index b1ed9165..bebc7be7 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9826,3 +9826,39 @@ and the 020 format-B RTE path in `MoiraExec_cpp.h` discards saved recovery words before restoring SR/PC and refilling prefetch. No reference cases were executed or credited. The source hashes and inspection scope are retained separately in `moira-capability-v1.json`; the architectural trace question stays unresolved. + +## Current tests on latest private CPU — full replay complete, 2026-10-07 + +The current CPU suite on the latest private returned-bank/scoped-FC source +now passes its complete integration gate: **5,282 passing / 33 unavailable / +5,315 exact executions**, zero failures. The roster is exactly the historical +full suite minus nine independently proven retired semantic rows. All 205 +source/project identities remain frozen: 38 qualified private CPU inputs and +167 current test/project inputs. The 26 private recovery fixture additions +remain separate; they are not credited as executions in this full project. + +The strict terminal audit verifies all **865 profile reports**, including +**707 ordinary reports / 86,240,034 scenarios**, with exact keys, weights, +outcomes and actual TRX summaries. All ten pinned native preset audits pass +unchanged coverage and controls. Their CPU/test assembly identities bind to +the actual isolated build. The 33 optional/discovery skips remain unavailable +coverage; they are not successful optional executions. Protected normal +assemblies and source inputs remain unchanged. + +Eight complete-evidence controls reject a missing report, altered arithmetic +weight, empty test roster, an unexpected skip, missing protected identity, +wrong native assembly, altered native control and an incorrect aggregate +execution record. The original preflight verifier remains byte-identical; +terminal V2 adds an explicit missing-report diagnostic without changing any +coverage or expectation. Evidence: `audits/LatestPrivateFullCpuV1` and +`integrity/LatestPrivateFullCpuCompleteV1` in the restoration temporary root. +Aggregate `latest-private-full-complete-proof.json` SHA-256: +`d9fa5faee85a7ab8ee075371a22d8af58697ad6f2cc4f01ba5dc2e4f1a2df23e`. + +The separate complete five-selection recovery gate uses identical CPU source +and retains its own 11,109,248-scenario proof. These completed software gates +do not resolve the architectural trace disagreement or the broader fault, +foreign-frame and reference requirements. Consumers still need qualification +before any production import. No CPU import, package publication or physical +timing qualification occurs. Milestone 6 remains **in progress**, +`roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index af23629d..201e1daa 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3251,3 +3251,13 @@ required. Hardware is unavailable and the trace boundary stays unresolved; no production import/publication or architectural trace promotion occurs. Milestone 6 stays **in progress**, `roadmapComplete=false`. See the [complete corrected combined record](COPPER68K_REFERENCE_QUALIFICATION.md#latest-refault-corrected-combined-replay-complete--2026-10-07). + +The current full CPU project on the latest private source completes its exact +integration gate: 5,282 passes / 33 unavailable / 5,315 executions, zero failures; +865 profile reports, 707 ordinary reports / 86,240,034 scenarios, and ten pinned +native preset audits. Eight complete-evidence corruption controls pass. The +separate combined recovery gate remains qualified on identical CPU source. +Consumer qualification and broader reference/fault requirements stay open; +architectural trace remains unresolved. No production import/publication occurs. +Milestone 6 stays **in progress**, `roadmapComplete=false`. See the +[full current-tests/private-CPU record](COPPER68K_REFERENCE_QUALIFICATION.md#current-tests-on-latest-private-cpu--full-replay-complete-2026-10-07). From 8e020e9197c130acc5ddbc7048ced1c1c041f036 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 22:13:15 +0300 Subject: [PATCH 121/211] Discriminate returned A7 postincrement bank policies --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 40 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 9 +++++ 2 files changed, 49 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index bebc7be7..d25cd1b1 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9862,3 +9862,43 @@ foreign-frame and reference requirements. Consumers still need qualification before any production import. No CPU import, package publication or physical timing qualification occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## Returned A7 bank-selection discovery — 2026-10-07 + +A new isolated discovery compares two explicit postincrement hypotheses for +MOVE B/W/L from A7 when a real fault handler edits saved S/M before RTE. +Each hypothesis executes **60,928 scenarios / 128 reports / 32 tests**, covering +all sixteen original/returned bank pairs, postincrement and predecrement, +relocated/control frames, scalar/batch execution, every operand fault byte, +all 32 postincrement CCRs and predecrement CCR 0/31. T1 is disabled. All 230 +qualified parent inputs remain byte-identical; only a new discovery fixture +is added in each 231-input snapshot. Production CPU source is untouched. + +The original-bank update hypothesis has **25,088 passing / 35,840 mismatching** +scenarios. Its 16 postincrement tests fail; all 16 predecrement tests pass. +Every mismatch is an exact A7-value disagreement for a changed physical bank. +Same-physical-bank postincrement controls account for 21,504 passing cases; +all 3,584 predecrement cases pass. The second fixture differs only in expected +postincrement bank (plus class/report names and explanatory comment): updating +the currently selected bank passes all **60,928** cases. Scenario setup, +instruction bytes, CPU code, other register/memory expectations and bus checks +remain unchanged. This is a comparison of unqualified expectations, not a +mutation proving a CPU defect or a promoted architectural gate. The original +failed execution and diagnostics are retained. + +The independent comparison enumerates every report identity, all 28 combination +keys per report, exact CCR weights, every failure ID/reason, actual TRX summaries, +32 test names/outcomes and both complete source inventories. Unsupported and +untested counts are zero in both selected discovery matrices. Evidence: +`audits/Operand020ReturnedA7DiscoveryV1` and +`audits/Operand020ReturnedA7SelectedDiscoveryV1` in the restoration temporary +root. `returned-a7-discovery-comparison-v1.json` SHA-256: +`ec904d3095b1ad025083093dc40e4e0100a925ff188a675bba2765cbd813c706`. These source-only evidence checks do not qualify physical function-code +spaces, other instructions, interrupted writebacks or handler-edited trace. + +The result distinguishes register-bank update policy from pending operand +address retention. An independent architectural expectation is still needed +before promoting changed-bank A7 postincrement behavior. Neither successful +software agreement nor changing the expected bank closes that requirement. +No CPU fix/import, package publication or regression retirement occurs. +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 201e1daa..e9fb2bdf 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3261,3 +3261,12 @@ Consumer qualification and broader reference/fault requirements stay open; architectural trace remains unresolved. No production import/publication occurs. Milestone 6 stays **in progress**, `roadmapComplete=false`. See the [full current-tests/private-CPU record](COPPER68K_REFERENCE_QUALIFICATION.md#current-tests-on-latest-private-cpu--full-replay-complete-2026-10-07). + +Changed-bank A7 discovery now distinguishes two postincrement hypotheses in +60,928 scenarios each. Updating the original physical bank yields 35,840 A7 +disagreements; updating the selected returned bank passes all cases on identical +CPU source. All predecrement and same-bank controls pass. Exact identities, +weights, failure IDs and test outcomes are verified. This remains unqualified +architectural behavior; changing an expectation is not a CPU fix or completion. +Milestone 6 stays **in progress**. See the +[returned A7 discovery record](COPPER68K_REFERENCE_QUALIFICATION.md#returned-a7-bank-selection-discovery--2026-10-07). From d510c54bd04834086afe93aa49c4b9a01b9f7532 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 22:24:51 +0300 Subject: [PATCH 122/211] Investigate native 030 A7 fault-recovery fixup behavior --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 50 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 8 +++ 2 files changed, 58 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index d25cd1b1..66f8de5f 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9902,3 +9902,53 @@ before promoting changed-bank A7 postincrement behavior. Neither successful software agreement nor changing the expected bank closes that requirement. No CPU fix/import, package publication or regression retirement occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## Native 030 returned-A7 observer discovery — 2026-10-07 + +The pinned generic WinUAE 030 observer now executes the actual `(A7)+` MOVE.L +operation, real postincrement fixup serialization/restoration, generated +format-B exception entry, guest saved-S/M edit and RTE in the existing complete +MMU retry loop. The extracted `op_2018_32_ff`, `mmu030fixupreg`, +`mmu030fixupmod` and `cpu_restore_fixup` functions are byte-equivalent text +from reference commit `5d22d33632646efc3f747f03e82d28353e52722e`. +Previously qualified frame/RTE/access/run-loop fragments remain unchanged. +Transport is flat physical memory; enabled translation, cache, IRQ, timing +and hardware behavior are not qualified. Trace is disabled. + +All **16 fault/frame/RTE bank pairs and four direct controls** execute. Each +fault has exactly two operand attempts and one successful source read; each +direct control has one attempt/read. Source address, destination value, +following MOVEQ, final PC and exception/handler events are checked. The +observer verifies that only saved-SR bytes change in the 92-byte native frame. +Raw USP/ISP/MSP fields are recorded as software storage fields; the active +A7 alias is not silently normalized into them. + +The reference does **not** resolve the architectural A7 policy: its two +unchanged-bank user-mode fault cases leave A7=`4208`, while all four direct +controls leave A7=`4204`. Other fault cases show different bank adjustments. +These are recorded software observations, not expected hardware behavior. +The ordinary postincrement rule in +[MC68030UM-P1 section 2.4.4](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P1.pdf) +does not justify selecting that eight-byte result. Reviewing the restoration +and recovery sections in +[MC68030UM-P2 sections 8.1.10 and 8.2](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P2.pdf) +also did not establish the changed-S/M physical-bank update in this experiment. +The observer or the reference recovery/fixup interaction requires further +investigation before it can be an oracle for that case. + +Two sole observer mutations are rejected precisely: an extra actual source +read fails the attempt/successful-read guard, and leaked operand context fails +when an RTE stack read is mistaken for an operand read. Native functions stay +unchanged in both. Failed V1..V7 exploratory adapters remain retained: line +comment/header build errors, unsupported user FC transport, and address-only +read counters that confused overlapping RTE stack and operand accesses. +V8 distinguishes transport origin and resets context on every dispatch exit; +no count expectation or native source function is weakened to obtain completion. + +Evidence: `reference/ReturnedA7Native030V8` and +`mutations/ReturnedA7ObserverV1` in the restoration temporary root. +`native-returned-a7-discovery-proof-v8.json` SHA-256: +`386737febabb5a4b9ac81108b01c2653d273a546e28db6f3f39b45e548d2189d`. This is observer/software discovery, not a passing architectural +gate or a replacement for either retained A7 hypothesis. No production CPU +change, import, package publication or regression retirement occurs. +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index e9fb2bdf..467d69e6 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3270,3 +3270,11 @@ weights, failure IDs and test outcomes are verified. This remains unqualified architectural behavior; changing an expectation is not a CPU fix or completion. Milestone 6 stays **in progress**. See the [returned A7 discovery record](COPPER68K_REFERENCE_QUALIFICATION.md#returned-a7-bank-selection-discovery--2026-10-07). + +The native 030 A7 observer now executes sixteen fault bank pairs and four +direct controls using unchanged postincrement/fixup/frame/RTE/run-loop source. +Its same-bank user fault cases disagree with the ordinary four-byte update; +the architectural question remains open. Two observer provenance/replay guards +pass, and failed exploratory adapters remain retained. No CPU change or +architectural promotion occurs. See the +[native A7 discovery record](COPPER68K_REFERENCE_QUALIFICATION.md#native-030-returned-a7-observer-discovery--2026-10-07). From b0a930cc8cb04c37bc69bf2de6c3d2e2dd49415c Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 22:31:05 +0300 Subject: [PATCH 123/211] Isolate native A7 fault-recovery fixup ordering cause --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 38 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 8 ++++ 2 files changed, 46 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 66f8de5f..e60ca1bc 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9952,3 +9952,41 @@ Evidence: `reference/ReturnedA7Native030V8` and gate or a replacement for either retained A7 hypothesis. No production CPU change, import, package publication or regression retirement occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## Native A7 fixup-ordering cause isolated — 2026-10-07 + +Passive state snapshots and capture of existing native debug logs preserve +all twenty V8 observer outcomes byte-for-byte. They expose the unchanged-bank +user case: the fault path advances original A7 from `4200` to `4204` and saves +USP=`4204`; RTE then applies the inverse adjustment to the currently active +handler stack (`7FA4` to `7FA0`) before restoring SR. After stack deallocation +and SR restoration, USP remains `4204`, ISP is left at `7FFC`, and the resumed +MOVE advances user A7 again to `4208`. Thus the inverse fixup targets the +handler bank in this reference path. No passive observer change alters native +functions, memory, register state or expected results. + +A sole ordering experiment moves both format-B inverse-fixup calls from before +SR restoration to immediately after it. Only `rte.inc` differs; the observer +and all other native fragments are byte-identical. The trace now deallocates +the complete handler frame back to ISP=`8000`, restores user mode, then undoes +the increment on user A7 (`4204` to `4200`). The resumed MOVE completes at +`4204`. The two original unchanged-bank user discrepancies disappear; all +four unchanged-bank fault cases and all four direct controls have the ordinary +four-byte final increment. All sixteen bank-pair recoveries still execute the +exact two operand attempts / one successful read, original data result and +following sentinel. Direct control output is byte-identical to the baseline. + +This isolates a software recovery/fixup ordering cause; the changed native +helper is an experiment, not an independently validated reference correction +or a Copper68k candidate. Changed-S/M architectural bank selection, other +registers/instructions, nested refaults and full host/pipeline behavior remain +unqualified. The reference checkout and the failed original observations are +preserved. No CPU source, published package or acceptance expectation changes. + +Evidence: `reference/ReturnedA7Native030V9` and +`mutations/ReturnedA7FixupOrderingV1` in the restoration temporary root. +`native-a7-fixup-ordering-proof-v1.json` SHA-256: +`b1c40ddf7b3c6d50add4287cf410526b2f36ced26d10bbc2aa739695b1efaef8`. The verifier checks the exact original/changed helper relationship, +all untouched fragments, complete case roster/results, identical passive +observations, direct controls and both causal state sequences. Milestone 6 +remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 467d69e6..42fcab24 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3278,3 +3278,11 @@ the architectural question remains open. Two observer provenance/replay guards pass, and failed exploratory adapters remain retained. No CPU change or architectural promotion occurs. See the [native A7 discovery record](COPPER68K_REFERENCE_QUALIFICATION.md#native-030-returned-a7-observer-discovery--2026-10-07). + +The native A7 discrepancy is isolated to inverse-fixup ordering: the unmodified +reference undoes the original increment on the handler's stack before restoring +SR. A sole native-helper ordering experiment removes both unchanged-bank user +discrepancies and retains all direct controls. This is causal software evidence, +not a qualified reference correction or changed-S/M architectural expectation. +No CPU change or promotion occurs. See the +[native fixup-ordering record](COPPER68K_REFERENCE_QUALIFICATION.md#native-a7-fixup-ordering-cause-isolated--2026-10-07). From 714ee99c576f978b6c7415910fe87d3dca12cbe5 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 22:39:45 +0300 Subject: [PATCH 124/211] Maintain reproducible native 030 A7 recovery discovery audit --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 47 ++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 11 + scripts/reference/m68030-a7-recovery.cpp | 157 +++++++++++++ ...est-copper68k-030-a7-recovery-reference.py | 215 ++++++++++++++++++ 4 files changed, 430 insertions(+) create mode 100644 scripts/reference/m68030-a7-recovery.cpp create mode 100644 scripts/test-copper68k-030-a7-recovery-reference.py diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index e60ca1bc..1a5b1cd2 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -9990,3 +9990,50 @@ Evidence: `reference/ReturnedA7Native030V9` and all untouched fragments, complete case roster/results, identical passive observations, direct controls and both causal state sequences. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## Maintained native A7 discovery command — 2026-10-07 + +The original passive observer is now repository-owned in +`scripts/reference/m68030-a7-recovery.cpp`, driven by +`scripts/test-copper68k-030-a7-recovery-reference.py`. It regenerates the exact +native fragments from WinUAE commit `5d22d33632646efc3f747f03e82d28353e52722e`, +using the maintained return/trace helper's six canonical baseline cases. It +requires Windows, Python 3, PowerShell 7 and Visual Studio 18 Community MSVC; +the default reference checkout is `artifacts/reference-winuae-rte-modern`. +No temporary producer scripts or Copper68k build are required. + +```powershell +python scripts/test-copper68k-030-a7-recovery-reference.py --output artifacts/a7-reference-fresh --discovery-only +python scripts/test-copper68k-030-a7-recovery-reference.py --output artifacts/a7-reference-fresh --validate-only --discovery-only +python scripts/test-copper68k-030-a7-recovery-reference.py --output artifacts/a7-reference-fresh --validate-only +``` + +Use a fresh output for generation. The first two commands verify discovery; +the third deliberately fails the architectural gate on the two unchanged-bank +user recoveries ending at A7=`4208`. Twelve changed-bank architectural outcomes +remain explicitly untested. All sixteen fault cases and four direct controls +are executed, with exact source-read counts, result/PC/sentinel checks, native +fragment identities and causal snapshots. Validate-only rechecks the pinned +inputs and reexecutes the frozen observer, comparing output and trace byte for +byte. This command preserves original native fixup ordering; it does not import +the earlier ordering experiment. Flat physical FC1/5 transport, disabled +translation/cache/IRQ/trace and raw inactive stack storage remain caveats. + +Fresh generation and discovery replay passed; ordinary architectural validation +returned exit 1 with the expected A7 disagreement. Eight corruption controls +were precisely rejected: missing fixture, wrong producer, wrong scope, missing +output identity, changed fixture with updated manifest, changed native function +with updated manifest, empty executable selection with updated manifest, and +changed verification. These prove evidence handling, not hardware semantics. + +Evidence is `reference/ReturnedA7MaintainedV1` and +`integrity/ReturnedA7MaintainedV1` in the restoration temporary root. +`identities.json` SHA-256: +`874cbf36b0df0cb2ffc6add944721635d7bb3dbd4423bf01d2cd4d97bdf261e5`; +`verification.json` SHA-256: +`71192189d4f688a903a8b4f05fa1b219208d15cc67e5ad47aabb30e49e3b5f95`; +`maintained-a7-controls-v1.json` SHA-256: +`c06f7ec04cdfa2ef641c097c21e7fb0ef5610b235887e6b019f781c2c79fd8d8`. +Hardware is unavailable. No production CPU change, import, publication, +regression retirement or architectural promotion occurs. Milestone 6 remains +**in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 42fcab24..8b7629f9 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3286,3 +3286,14 @@ discrepancies and retains all direct controls. This is causal software evidence, not a qualified reference correction or changed-S/M architectural expectation. No CPU change or promotion occurs. See the [native fixup-ordering record](COPPER68K_REFERENCE_QUALIFICATION.md#native-a7-fixup-ordering-cause-isolated--2026-10-07). + +The passive native A7 observer now has a maintained repository command and +fixture, regenerating pinned reference fragments without temporary producers. +Fresh generation and deterministic replay reproduce sixteen fault cases and +four direct controls. Eight corrupted-evidence controls fail precisely. +Discovery-only mode records unqualified observations; ordinary validation fails +the two unchanged-bank user recovery disagreements. Twelve changed-bank +architectural outcomes remain explicitly untested. Hardware is unavailable; +no CPU change, promotion or publication occurs. Milestone 6 remains +**in progress**, `roadmapComplete=false`. See the +[maintained A7 command and evidence](COPPER68K_REFERENCE_QUALIFICATION.md#maintained-native-a7-discovery-command--2026-10-07). diff --git a/scripts/reference/m68030-a7-recovery.cpp b/scripts/reference/m68030-a7-recovery.cpp new file mode 100644 index 00000000..4f1afc90 --- /dev/null +++ b/scripts/reference/m68030-a7-recovery.cpp @@ -0,0 +1,157 @@ +// Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT +// 030 software discovery: MOVE.L (A7)+ faults, guest saved-S/M edits and RTE. +// Unchanged generated operations, fixup helpers and retry loop; flat physical +// transport. Passive snapshots expose the inverse-fixup ordering discrepancy. +// Trace is disabled. No enabled translation, IRQ, cache/pipeline timing, +// physical function-code spaces or architectural changed-bank qualification. +#include +#include +#include +#include +#include +#include +#include +#include +#include +using uae_u8=uint8_t;using uae_u16=uint16_t;using uae_u32=uint32_t; +using uae_s8=int8_t;using uae_s16=int16_t;using uae_s32=int32_t;using uaecptr=uint32_t; +#define REGPARAM2 +#define STATIC_INLINE static inline +#define CYCLE_UNIT 2 +#define _T(x) x +#define MMUDEBUG 0 +#define TRY(x) try +#define CATCH(x) catch(int x) +#define ENDTRY +#define STOPTRY +#define THROW(x) throw int(x) +#define SPCFLAG_TRACE 0x40 +#define SPCFLAG_DOTRACE 0x80 +#define SPCFLAG_TRAP 0x400 +#define SPCFLAG_INT 8 +#define SPCFLAG_DOINT 0x100 +#define CPU_HALT_CPU_STUCK 1 +#define CPU_HALT_DOUBLE_FAULT 2 +#define sz_byte 0 +#define sz_word 1 +#define sz_long 2 +#define m68k_areg(r,n) ((r).a[n]) +#define m68k_dreg(r,n) ((r).d[n]) +struct Regs { + uae_u32 a[8]{},d[8]{},usp{},isp{},msp{},pc{},instruction_pc{},trace_pc{},vbr{},mmu_ssw{},mmu_fault_addr{},sfc{},dfc{},wb3_data{},wb2_address{},fp_ea{},fp_opword{},mmu_effective_addr{},pcr{},fp_unimp_ins{},ir{}; + uae_u16 sr{};int m{},s{},t0{},t1{},intmask{},ipl[1]{},ipl_pin{},stopped{},spcflags{},opcode{},irc{},instruction_cnt{},wb2_status{},wb3_status{},read_buffer{},write_buffer{},fpu_exp_pre{},fp_unimp_pend{}; + int prefetch020_valid[3]{},prefetch020[3]{},pipeline_r8[2]{},pipeline_pos{},pipeline_stop{}; +}regs; +struct Prefs{int cpu_model=68030,mmu_model=68030,cachesize{},cpu_compatible{},cpu_cycle_exact{},fpu_model{};}currprefs; +struct flag_struct{int cznv{},x{};}regflags; +int xflag,nflag,zflag,vflag,cflag; +#define SET_XFLG(x) (xflag=(x)) +#define SET_NFLG(x) (nflag=(x)) +#define SET_ZFLG(x) (zflag=(x)) +#define SET_VFLG(x) (vflag=(x)) +#define SET_CFLG(x) (cflag=(x)) +#define GET_XFLG() xflag +#define GET_NFLG() nflag +#define GET_ZFLG() zflag +#define GET_VFLG() vflag +#define GET_CFLG() cflag +#define CLEAR_CZNV() (cflag=zflag=nflag=vflag=0) +bool m68k_accurate_ipl=false,mmu_debugger=false,ismoves030=false,islrmw030=false,bBusErrorReadWrite=false,mmu030_retry=false; +int mmu030_idx,mmu030_idx_done,mmu030_opcode,mmu030_opcode_stageb,mmu030_fake_prefetch,cpu_cycles; +uaecptr mmu030_fake_prefetch_addr,mm030_stageb_address,last_fault_for_exception_3; +uae_u16 mmu030_state[3];uae_u32 mmu030_data_buffer_out,mmu030_disp_store[2],mmu030_fmovem_store[2],mmu040_move16[4]; +struct mmu030_access{uae_u32 val;};mmu030_access mmu030_ad[10]; +struct mmufixup{int reg=-1;uae_u32 value{};};mmufixup mmufixup[2]; +std::unordered_map mem; +std::vector events;std::vector originalFrame,editedFrame; +bool operandContext=false;bool faultOnce;int attempts,reads,steps,traceCount;uae_u32 tracePc,traceD7;int returnT1; +int activeCase=-1; +std::ofstream diagnostics("trace.log"); +void snapshot(const char* phase){diagnostics<second;}return v; +} +void put(uaecptr a,uae_u32 v,int w){for(int n=0;n>(8*(w-1-n)));} +uae_u32 operand_read(uaecptr a){if(a!=0x4200)unavailable();attempts++;if(faultOnce){faultOnce=false;events.push_back(1);mmu030_hardware_bus_error(a,0,true,false,sz_long);}reads++;events.push_back(4);return read(a,4);} +uae_u32 uae_mmu030_get_long(uaecptr a){return operandContext?operand_read(a):read(a,4);}uae_u32 uae_mmu030_get_word(uaecptr a){return read(a,2);}uae_u32 uae_mmu030_get_iword(uaecptr a){return read(a,2);} +void uae_mmu030_put_word(uaecptr a,uae_u32 v){put(a,v,2);} +uae_u32 get_word_mmu030(uaecptr a){return read(a,2);}uae_u32 get_long_mmu030(uaecptr a){return read(a,4);} +uae_u32 x_get_long(uaecptr a){return read(a,4);}void x_put_long(uaecptr a,uae_u32 v){put(a,v,4);}void x_put_word(uaecptr a,uae_u32 v){put(a,v,2);} +uae_u32 uae_mmu030_get_long_fcx(uaecptr a,int fc){if(fc!=1&&fc!=5)unavailable();return a==0x4200?operand_read(a):read(a,4);} +uae_u32 uae_mmu030_get_word_fcx(uaecptr,int){unavailable();}uae_u32 uae_mmu030_get_byte_fcx(uaecptr,int){unavailable();} +void uae_mmu030_put_long_fcx(uaecptr,uae_u32,int){unavailable();}void uae_mmu030_put_word_fcx(uaecptr,uae_u32,int){unavailable();}void uae_mmu030_put_byte_fcx(uaecptr,uae_u32,int){unavailable();} +uae_u32 mmu030_get_generic(uaecptr,uae_u32,int,int){unavailable();}void mmu030_put_generic(uaecptr,uae_u32,uae_u32,int,int){unavailable();} +templatevoid mmu030_unaligned_read_continue(uaecptr,int,F){unavailable();}templatevoid mmu030_unaligned_write_continue(uaecptr,int,F){unavailable();} +void unalign_clear(){unavailable();}void exception3_read_prefetch(uae_u32,uaecptr){unavailable();} +void exception3_read_special(uae_u32,uaecptr,int,int){unavailable();} +void write_log(const char* format,...){char buffer[512];va_list args;va_start(args,format);vsnprintf(buffer,sizeof(buffer),format,args);va_end(args);diagnostics<32)throw std::runtime_error("Bound exceeded");return read(regs.pc+n,2);} +int do_specialties(int){ + const int spcflags=regs.spcflags; + if(spcflags&~(SPCFLAG_TRACE|SPCFLAG_DOTRACE))unavailable(); + // Exact trace fragment of reference do_specialties; IRQ/host branches excluded. +#include "special-trace.inc" + return regs.pc==0x6000||regs.pc==0x1006; +} +uae_u32 dispatch(uae_u32 op){ + if(op==0x3ebc){events.push_back(3);auto v=op_30bc_32_ff(op);editedFrame.clear();for(int n=0;n<92;n++)editedFrame.push_back(mem.at(regs.a[7]+n));return v;} + if(op==0x4e73){events.push_back(5);snapshot("before-RTE");auto value=op_4e73_32_ff(op);snapshot("after-RTE");return value;} + if(op==0x201f){snapshot("before-MOVE");operandContext=true;try{auto value=op_2018_32_ff(op);operandContext=false;snapshot("after-MOVE");return value;}catch(...){operandContext=false;throw;}} + if(op==0x7e2a){events.push_back(6);return op_7000_32_ff(op);} + if(op==0x4e71)return op_4e71_32_ff(op); + unavailable(); +} +#include "loop.inc" + +int main(){try{ + for(int id=0;id<20;id++){activeCase=id; + const bool fault=id<16;const int initial=fault?id/4:id-16,returned=fault?id%4:initial; + regs={};mem.clear();events.clear();originalFrame.clear();editedFrame.clear();faultOnce=fault;attempts=reads=steps=traceCount=0;tracePc=traceD7=0;regflags={};xflag=nflag=zflag=vflag=cflag=0; + mmu030_state[0]=mmu030_state[1]=mmu030_state[2]=0;mmu030_idx=mmu030_idx_done=0;mmu030_retry=false;mmu030_data_buffer_out=0;mmufixup[0].reg=mmufixup[1].reg=-1; + regs.usp=0x7000;regs.isp=0x8000;regs.msp=0x9000; + if(initial<2)regs.usp=0x4200;else if(initial==2)regs.isp=0x4200;else regs.msp=0x4200; + regs.s=initial>=2;regs.m=initial==1||initial==3;regs.intmask=7;regs.a[7]=0x4200;regs.pc=regs.instruction_pc=0x1000; + put(0x1000,0x201f,2);put(0x1002,0x7e2a,2);put(0x1004,0x4e71,2); + put(0x4200,0x80818283,4);put(0x7000,0x70717273,4);put(0x8000,0x40414243,4);put(0x9000,0x90919293,4);put(8,0x5000,4);put(36,0x6000,4); + put(0x5000,0x3ebc,2);put(0x5002,0x700|(returned>=2?0x2000:0)|((returned==1||returned==3)?0x1000:0),2);put(0x5004,0x4e73,2); + for(auto& p:cpufunctbl)p=dispatch; + try{m68k_run_mmu030();}catch(const Done&){} + if(reads!=1||attempts!=(fault?2:1)||traceCount!=0||regs.pc!=0x1006||regs.d[7]!=42)throw std::runtime_error("Missing actual recovery/control: id="+std::to_string(id)+" attempts="+std::to_string(attempts)+" reads="+std::to_string(reads)+" PC="+std::to_string(regs.pc)+" D0="+std::to_string(regs.d[0])); + if(fault){ + if(originalFrame.size()!=92||editedFrame.size()!=92||originalFrame[6]!=0xb0||originalFrame[7]!=8)throw std::runtime_error("Missing reference format-B frame"); + for(int n=2;n<92;n++)if(originalFrame[n]!=editedFrame[n])throw std::runtime_error("Handler changed non-SR frame fields"); + }else if(!originalFrame.empty()||!editedFrame.empty())throw std::runtime_error("Unexpected control bus fault"); + std::cout<<"fault="<nul\r\n' + 'if errorlevel 1 exit /b %errorlevel%\r\n' + 'cl /nologo /O2 /EHsc /W4 observer.cpp /Fe:observer.exe\r\nexit /b %errorlevel%\r\n') + (native / "build.cmd").write_text(command, encoding="utf-8") + run([os.environ.get("COMSPEC", "cmd.exe"), "/d", "/c", "build.cmd"], native, native / "build.log") + with (native / "run.log").open("w", encoding="utf-8") as stdout, (native / "errors.log").open("w", encoding="utf-8") as stderr: + result = subprocess.run([str(native / "observer.exe")], cwd=native, stdout=stdout, stderr=stderr) + check(result.returncode == 0, "Native observer failed; inspect errors.log") + files = [*sorted(fragments), "observer.cpp", "observer.exe", "build.cmd", "build.log", + "run.log", "errors.log", "trace.log"] + identity = dict(schema=1, producerSha256=sha(Path(__file__)), fixtureSha256=sha(FIXTURE), + referenceCommit=PIN, referenceInputs=reference_inputs, + outputs={name: sha(native / name) for name in files}, + enabledTranslation=False, hardwareQualified=False, architecturalQualified=False, + productionImport=False, roadmapComplete=False) + save(output / "identities.json", identity) + identity = load(output / "identities.json") + check(identity["schema"] == 1 and identity["producerSha256"] == sha(Path(__file__)) and + identity["fixtureSha256"] == sha(FIXTURE) and identity["referenceCommit"] == PIN and + identity["referenceInputs"] == reference_inputs and all(identity.get(k) is False for k in + ["enabledTranslation", "hardwareQualified", "architecturalQualified", "productionImport", "roadmapComplete"]), + "Wrong A7 scope/producer/input identity") + files = {*fragments, "observer.cpp", "observer.exe", "build.cmd", "build.log", "run.log", "errors.log", "trace.log"} + check(set(identity["outputs"]) == files, "Missing native output identity") + for name, digest in identity["outputs"].items(): + check(sha(native / name) == digest, "Changed native output: " + name) + check(sha(native / "observer.cpp") == sha(FIXTURE), "Changed observer fixture") + for name, text in fragments.items(): + check((native / name).read_text(encoding="utf-8-sig") == text, "Changed extracted native function: " + name) + if validate_only: + replay = output / ("replay-" + uuid.uuid4().hex) + replay.mkdir() + with (replay / "run.log").open("w", encoding="utf-8") as stdout, (replay / "errors.log").open("w", encoding="utf-8") as stderr: + result = subprocess.run([str(native / "observer.exe")], cwd=replay, stdout=stdout, stderr=stderr) + check(result.returncode == 0 and (replay / "errors.log").stat().st_size == 0 and + all(sha(replay / name) == sha(native / name) for name in ["run.log", "trace.log"]), + "Executed A7 replay differs") + rows, mismatches = observations(native) + verification = dict(schema=1, nativeFaultCases=16, directControls=4, observations=rows, + sameBankDisagreements=mismatches, changedBankArchitecturalUntested=12, + identitySha256=sha(output / "identities.json"), architecturalQualified=False, + productionImport=False, publication=False, roadmapComplete=False) + if validate_only: + check(load(output / "verification.json") == verification, "Changed verification record") + else: + save(output / "verification.json", verification) + return mismatches + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument("--reference-directory", type=Path, default=REPO / "artifacts/reference-winuae-rte-modern") + parser.add_argument("--validate-only", action="store_true") + parser.add_argument("--discovery-only", action="store_true", help="record unqualified observations; do not require architectural agreement") + args = parser.parse_args() + mismatches = audit(args.output.resolve(), args.reference_directory.resolve(), args.validate_only) + print("16 native recoveries / four direct controls; two same-bank disagreements; twelve changed-bank cases architecturally untested.") + if mismatches and not args.discovery_only: + raise ValueError("Architectural A7 gate failed: unchanged-bank user recovery advances A7 by eight bytes") + print("Discovery identities and execution verified; no architectural promotion.") + + +if __name__ == "__main__": + try: + main() + except (ValueError, OSError, KeyError, subprocess.SubprocessError) as error: + print(str(error), file=sys.stderr) + sys.exit(1) From 894831302403c82fe947408d0655664e7bba780a Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 22:41:01 +0300 Subject: [PATCH 125/211] Refresh native A7 audit evidence after fixture whitespace cleanup --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 1a5b1cd2..2bc91285 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -10026,14 +10026,14 @@ output identity, changed fixture with updated manifest, changed native function with updated manifest, empty executable selection with updated manifest, and changed verification. These prove evidence handling, not hardware semantics. -Evidence is `reference/ReturnedA7MaintainedV1` and -`integrity/ReturnedA7MaintainedV1` in the restoration temporary root. +Evidence is `reference/ReturnedA7MaintainedV2` and +`integrity/ReturnedA7MaintainedV2` in the restoration temporary root. `identities.json` SHA-256: -`874cbf36b0df0cb2ffc6add944721635d7bb3dbd4423bf01d2cd4d97bdf261e5`; +`aaf97a3c23ec679e174ca07a0119f52f440dafe3955fa6a47a45821491f340cf`; `verification.json` SHA-256: -`71192189d4f688a903a8b4f05fa1b219208d15cc67e5ad47aabb30e49e3b5f95`; -`maintained-a7-controls-v1.json` SHA-256: -`c06f7ec04cdfa2ef641c097c21e7fb0ef5610b235887e6b019f781c2c79fd8d8`. +`3571738cfecfcd018930c40533159464ef7b0f32937e20166980c7bf6ead539a`; +`maintained-a7-controls-v2.json` SHA-256: +`72d0a3abb4b0b3d6f2d0d4df22ea6aaced1aabad2db734c30ca9b95140eaebe5`. Hardware is unavailable. No production CPU change, import, publication, regression retirement or architectural promotion occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. From 073d1f4ea604f8f4db1b5b42a647c1fb942f3fd7 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 22:42:13 +0300 Subject: [PATCH 126/211] Remove fixture blank-line whitespace and pin fresh A7 audit evidence --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 12 ++++++------ scripts/reference/m68030-a7-recovery.cpp | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 2bc91285..4ea6c9db 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -10026,14 +10026,14 @@ output identity, changed fixture with updated manifest, changed native function with updated manifest, empty executable selection with updated manifest, and changed verification. These prove evidence handling, not hardware semantics. -Evidence is `reference/ReturnedA7MaintainedV2` and -`integrity/ReturnedA7MaintainedV2` in the restoration temporary root. +Evidence is `reference/ReturnedA7MaintainedV3` and +`integrity/ReturnedA7MaintainedV3` in the restoration temporary root. `identities.json` SHA-256: -`aaf97a3c23ec679e174ca07a0119f52f440dafe3955fa6a47a45821491f340cf`; +`e2f767eaec99feb3ce8a3458619b52f29a8cdc57252a0913eb3d3649610cbdf3`; `verification.json` SHA-256: -`3571738cfecfcd018930c40533159464ef7b0f32937e20166980c7bf6ead539a`; -`maintained-a7-controls-v2.json` SHA-256: -`72d0a3abb4b0b3d6f2d0d4df22ea6aaced1aabad2db734c30ca9b95140eaebe5`. +`df40005c38bbb496648071a7f667c71da0062658bd4a870041236911a2da4840`; +`maintained-a7-controls-v3.json` SHA-256: +`4abe8726607a21271a52752878eb615136fa1c70480126f02fda8abe4ccd952d`. Hardware is unavailable. No production CPU change, import, publication, regression retirement or architectural promotion occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/reference/m68030-a7-recovery.cpp b/scripts/reference/m68030-a7-recovery.cpp index 4f1afc90..0c643775 100644 --- a/scripts/reference/m68030-a7-recovery.cpp +++ b/scripts/reference/m68030-a7-recovery.cpp @@ -76,7 +76,7 @@ uaecptr m68k_getpc(){return regs.pc;}uaecptr m68k_getpci(){return regs.pc;} void m68k_setpci(uaecptr x){regs.pc=x;}void m68k_incpci(int n){regs.pc+=n;} void mmu030_hardware_bus_error(uaecptr,uae_u32,bool,bool,int); uae_u32 read(uaecptr a,int w){ - + uae_u32 v=0;for(int n=0;nsecond;}return v; } void put(uaecptr a,uae_u32 v,int w){for(int n=0;n>(8*(w-1-n)));} From 8b6418fc84fa5e99de81434a30e1c2e9f05be0b4 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 22:48:22 +0300 Subject: [PATCH 127/211] Prove shared SUBA indirect sign-extension replacement coverage --- .../Synthetic/SyntheticSubaIndirectTests.cs | 26 +++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 52 +++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 11 ++ ...t-copper68k-suba-indirect-consolidation.py | 179 ++++++++++++++++++ 4 files changed, 268 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticSubaIndirectTests.cs create mode 100644 scripts/test-copper68k-suba-indirect-consolidation.py diff --git a/Copper68k.Tests/Synthetic/SyntheticSubaIndirectTests.cs b/Copper68k.Tests/Synthetic/SyntheticSubaIndirectTests.cs new file mode 100644 index 00000000..76bee913 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticSubaIndirectTests.cs @@ -0,0 +1,26 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticSubaIndirectTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void IndirectSubaSignExtensionAliasesAndPreservedFlags(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "suba-indirect-consolidation"); + foreach (var width in new[] { 2, 4 }) + foreach (var destination in new[] { 0, 3, 7 }) + foreach (var supervisor in new[] { false, true }) + foreach (var source in width == 2 + ? new uint[] { 0xfffe, 0x8000, 0x7fff, 0 } + : new uint[] { 2, 0x80000000, 0x7fffffff, 0 }) + for (var ccr = 0; ccr < 32; ccr++) + SyntheticArithmeticTests.Case(machine, report, "SUBA", width, new(2, 3), source, + destination == 3 ? 0x3000u : 0x1000u, ccr, reg: destination, + supervisor: supervisor, scenario: "indirect-sign-alias-all-CCR"); + report.Complete(output); + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 4ea6c9db..8d5ed4eb 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -10037,3 +10037,55 @@ Evidence is `reference/ReturnedA7MaintainedV3` and Hardware is unavailable. No production CPU change, import, publication, regression retirement or architectural promotion occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## SUBA indirect shared replacement proof, retirement pending — 2026-10-07 + +`SyntheticSubaIndirectTests.IndirectSubaSignExtensionAliasesAndPreservedFlags` +adds 12,288 ordinary-CI scenarios: all eight model/profiles, word/long sources +through A3, destinations A0/A3/A7, both user/supervisor stacks, four source +boundaries per width and all 32 CCR states. It reuses the independent operand +fixture and common architectural/memory verifier. The A3 destination aliases +the source base; A7 checks the selected stack bank. Word inputs include `FFFE` +and `8000`; long inputs include `2`, `80000000` and `7FFFFFFF`. + +The maintained command builds isolated copies and retains both historical +`SubaIndirectSignExtendsWordsAndLeavesCcrUntouched` cases from source pin +`073d1f4ea604f8f4db1b5b42a647c1fb942f3fd7`. It checks complete source identities, +all twelve coverage keys per profile and their exact 128-case weights, +register/CCR/PC/memory results, exact xUnit selection/outcomes and report/stdout +agreement. Normal assemblies are protected. No production CPU is edited. + +```powershell +python scripts/test-copper68k-suba-indirect-consolidation.py --output artifacts/suba-indirect-fresh +python scripts/test-copper68k-suba-indirect-consolidation.py --output artifacts/suba-indirect-fresh --validate-only +``` + +Clean execution passes **12,288 scenarios / eight profile reports** and both +historical cases: ten xUnit executions, zero failures/skips. A sole semantic +mutation zero-extends word indirect operands in both the base and advanced +execution paths. It produces exactly **3,072 shared mismatches**, with the +expected IDs and address-register result differences, and retains **9,216 +passing controls**. All eight shared profile batches fail; the original word +case fails with expected `4098` / actual `4294905858`. The original long case +passes. Validate-only independently rechecks this complete evidence. + +Eight corruption controls are precisely rejected: missing fixture, wrong +producer, empty selection, missing historical witnesses, wrong coverage weight, +wrong failure reason, changed CPU mutation with updated manifest and missing +protected assembly identity. They verify evidence handling, not hardware timing. + +Evidence: `audits/SubaIndirectConsolidationV1` and +`integrity/SubaIndirectConsolidationV1` in the restoration temporary root. +`proof.json` SHA-256: +`3dec11fa433d2bdd11f483f4e871f939f0da57daa61db11e3019c0373e4cee59`; +`suba-indirect-controls-v1.json` SHA-256: +`acea27e4912a154dc9bd718ace2b324bbb9d821fa7aed7f20cc55815852d5f3f`. +The replacement anchor is +`68EC020/SUBA/2/(A3)/r0/indirect-sign-alias-all-CCR/brief/super=True/op=90D3/s=0000FFFE/d=00001000/ccr=1F`. + +**No retirement yet:** the original method contains a separate long-width +witness. A discriminating long-width mutation and unchanged remainder-of-class +retention are still required before removing that method. Earlier full CPU +results do not include this new group. Timing, prefetch, native media and other +specialized regressions remain retained. No CPU import/publication occurs; +milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 8b7629f9..0fa979ee 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3297,3 +3297,14 @@ architectural outcomes remain explicitly untested. Hardware is unavailable; no CPU change, promotion or publication occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [maintained A7 command and evidence](COPPER68K_REFERENCE_QUALIFICATION.md#maintained-native-a7-discovery-command--2026-10-07). + +Shared SUBA indirect coverage now passes 12,288 word/long, alias, stack and +all-CCR scenarios across all eight profiles. An isolated two-path word +zero-extension mutation produces exactly 3,072 shared mismatches and detects +the original word witness, retaining 9,216 controls and the original long case. +Eight evidence-corruption controls are precisely rejected; the maintained +command supports fresh generation and strict validate-only replay. The old +method remains: its separate long-width witness and remaining class need +replacement/retention proof before retirement. No production CPU change or +publication occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. +See the [SUBA indirect proof and remaining gate](COPPER68K_REFERENCE_QUALIFICATION.md#suba-indirect-shared-replacement-proof-retirement-pending--2026-10-07). diff --git a/scripts/test-copper68k-suba-indirect-consolidation.py b/scripts/test-copper68k-suba-indirect-consolidation.py new file mode 100644 index 00000000..af6f5a05 --- /dev/null +++ b/scripts/test-copper68k-suba-indirect-consolidation.py @@ -0,0 +1,179 @@ +"""Prove shared SUBA indirect coverage against an isolated sign-extension defect. + +Python 3, Git and dotnet are required. --output must be fresh unless using +--validate-only. Historical witnesses remain present until retirement is proven. +""" +import argparse +import hashlib +import itertools +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import xml.etree.ElementTree as ET + +REPO = Path(__file__).resolve().parent.parent +PIN = "073d1f4ea604f8f4db1b5b42a647c1fb942f3fd7" +LEGACY = "Copper68k.Tests/M68020AddressSourceTests.cs" +FIXTURE = "Copper68k.Tests/Synthetic/SyntheticSubaIndirectTests.cs" +WITNESS = "SubaIndirectSignExtendsWordsAndLeavesCcrUntouched" +GROUP = "suba-indirect-consolidation" +METHOD = "IndirectSubaSignExtensionAliasesAndPreservedFlags" +MODELS = ["68000", "68010", "68EC020", "68020", "68030", "68040", "68060", "A1200"] +NS = {"t": "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"} + + +def check(ok, reason): + if not ok: + raise ValueError(reason) + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def load(path): + return json.loads(path.read_text(encoding="utf-8-sig")) + + +def save(path, value): + path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8") + + +def mutation(path, text): + if path == "Copper68k/M68kAdvancedTimingInterpreter.cs": + start = text.index(" private void ExecuteSubaAddressIndirectToAddress(") + end = text.index(" private void ExecuteSubaPostIncrementToAddress(", start) + old = "unchecked((uint)(int)(short)ReadWord(State.A[opcode & 7]))" + block = text[start:end] + check(block.count(old) == 1, "Missing advanced mutation target") + return text[:start] + block.replace(old, "unchecked((uint)ReadWord(State.A[opcode & 7]))") + text[end:] + old = "SetAddressRegister(reg, State.A[reg] - M68kCpuState.SignExtend(value, size));" + check(text.count(old) == 1, "Missing base mutation target") + return text.replace(old, "SetAddressRegister(reg, State.A[reg] - (mode == 2 && size == M68kOperandSize.Word ? value & 0xffffu : M68kCpuState.SignExtend(value, size)));") + + +def sources(mode): + paths = subprocess.check_output(["git", "ls-files", "Copper68k", "Copper68k.Tests"], cwd=REPO, text=True).splitlines() + paths = sorted(set(paths + [FIXTURE])) + result = {p: (REPO / p).read_bytes() for p in paths if Path(p).suffix in [".cs", ".csproj"]} + original = subprocess.check_output(["git", "show", PIN + ":" + LEGACY], cwd=REPO) + check(result[LEGACY].decode("utf-8-sig").replace("\r\n", "\n") == original.decode("utf-8-sig").replace("\r\n", "\n"), "Changed historical witness source") + if mode == "ZeroExtend": + for p in ["Copper68k/M68kCore.cs", "Copper68k/M68kAdvancedTimingInterpreter.cs"]: + result[p] = mutation(p, result[p].decode("utf-8-sig")).encode() + return result + + +def inventory(source): + return {p.relative_to(source).as_posix(): sha(p) for p in source.rglob("*") + if p.suffix in [".cs", ".csproj"] and not {"bin", "obj"}.intersection(p.parts)} + + +def protected(): + paths = ["Copper68k/bin/Release/net10.0/Copper68k.dll", "Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll"] + return {p: sha(REPO / p) for p in paths if (REPO / p).exists()} + + +def execute(root, mode): + for p, content in sources(mode).items(): + target = root / "source" / p + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(content) + command = ["dotnet", "test", str(root / "source/Copper68k.Tests/Copper68k.Tests.csproj"), "-c", "Release", + "--artifacts-path", str(root / "build"), "--filter", f"FullyQualifiedName~{METHOD}|FullyQualifiedName~{WITNESS}", + "--logger", "trx;LogFileName=audit.trx", "--results-directory", str(root)] + record = dict(mode=mode, pin=PIN, producer=sha(Path(__file__)), sources=inventory(root / "source"), protected=protected(), command=command) + env = dict(os.environ, COPPER68K_SYNTHETIC_MODELS=",".join(MODELS), COPPER68K_SYNTHETIC_REPORT_DIR=str(root)) + print(mode + ": executing eight shared profiles and two historical witnesses", flush=True) + with (root / "execution.log").open("w", encoding="utf-8") as log: + record["testExit"] = subprocess.run(command, env=env, cwd=REPO, stdout=log, stderr=subprocess.STDOUT).returncode + record["logSha256"] = sha(root / "execution.log") + save(root / "inputs.json", record) + + +def expected(model, mode): + combinations, failures = {}, [] + for width, reg, supervisor in itertools.product([2, 4], [0, 3, 7], [False, True]): + key = f"{model}/SUBA/{width}/(A3)/r{reg}/indirect-sign-alias-all-CCR/brief/super={supervisor}" + bad = mode == "ZeroExtend" and width == 2 + combinations[key] = dict(passing=64, mismatching=64) if bad else dict(passing=128) + if bad: + destination = 0x3000 if reg == 3 else 0x1000 + for value, ccr in itertools.product([0xfffe, 0x8000], range(32)): + wanted = (destination - (value - 0x10000)) & 0xffffffff + actual = (destination - value) & 0xffffffff + failures.append(dict(id=f"{key}/op={0x90d3 | reg << 9:04X}/s={value:08X}/d={destination:08X}/ccr={ccr:02X}", + status="mismatching", reason=f"A{reg} expected {wanted:08X}, actual {actual:08X}")) + return combinations, failures + + +def verify(root, mode): + record = load(root / "inputs.json") + check(record["mode"] == mode and record["pin"] == PIN and record["producer"] == sha(Path(__file__)), "Wrong producer or scope") + wanted = {p: hashlib.sha256(v).hexdigest() for p, v in sources(mode).items()} + check(record["sources"] == wanted, "Wrong baseline or mutation identity") + check(inventory(root / "source") == wanted, "Incomplete or changed source inventory") + check(record["protected"] == protected(), "Changed protected assemblies") + check(record["testExit"] == (1 if mode == "ZeroExtend" else 0) and record["logSha256"] == sha(root / "execution.log"), "Wrong execution or log") + tree = ET.parse(root / "audit.trx") + tests = tree.findall(".//t:UnitTestResult", NS) + names = {f'Copper68k.Tests.Synthetic.SyntheticSubaIndirectTests.{METHOD}(modelId: "{m}")': "Failed" if mode == "ZeroExtend" else "Passed" for m in MODELS} + prefix = "Copper68k.Tests.M68020AddressSourceTests." + WITNESS + word = prefix + "(opcode: 37075, hi: 65534, lo: 0, expected: 4098)" + names[word] = "Failed" if mode == "ZeroExtend" else "Passed" + names[prefix + "(opcode: 37331, hi: 0, lo: 2, expected: 4094)"] = "Passed" + check(len(tests) == 10 and {t.get("testName") for t in tests} == set(names), "Wrong or empty execution selection") + check(all(t.get("outcome") == names[t.get("testName")] for t in tests), "Wrong execution outcomes") + if mode == "ZeroExtend": + message = next(t for t in tests if t.get("testName") == word).find("t:Output/t:ErrorInfo/t:Message", NS).text + check(re.search(r"Expected:\s*4098\b", message) and re.search(r"Actual:\s*4294905858\b", message), "Wrong historical failure reason") + stdout = "\n".join(t.text or "" for t in tree.findall(".//t:UnitTestResult/t:Output/t:StdOut", NS)) + reports = [] + for model in MODELS: + path = root / f"{model}-{GROUP}.json" + report = load(path) + combinations, failures = expected(model, mode) + counts = dict(passing=1536 - len(failures), mismatching=len(failures), unsupported=0, untested=0) + check(report["schema"] == 1 and report["model"] == model and report["group"] == GROUP and report["logicalCases"] == 1536 and report["xunitBatches"] == 1, "Wrong report scope") + check(report["counts"] == counts and report["combinations"] == combinations, "Wrong coverage keys or weights") + check(sorted(report["failures"], key=lambda f: f["id"]) == sorted(failures, key=lambda f: f["id"]), "Wrong failure IDs or reasons") + matches = re.findall(re.escape(f"{model}/{GROUP}: ") + r"(\{[^\r\n]+\}); logical cases=1536, xUnit batches=1, combinations=12", stdout) + check(len(matches) == 1 and json.loads(matches[0]) == counts, "TRX report disagreement") + reports.append(dict(path=path.name, sha256=sha(path))) + check({p.name for p in root.glob(f"*-{GROUP}.json")} == {r["path"] for r in reports}, "Foreign report selection") + return dict(mode=mode, passing=12288 - (3072 if mode == "ZeroExtend" else 0), mismatching=3072 if mode == "ZeroExtend" else 0, + executions=10, reports=reports, inputsSha256=sha(root / "inputs.json"), trxSha256=sha(root / "audit.trx")) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--validate-only", action="store_true") + args = parser.parse_args() + output = args.output.resolve() + if not args.validate_only: + check(not output.exists(), "Use fresh audit output") + output.mkdir(parents=True) + entries = [] + for mode in ["Clean", "ZeroExtend"]: + root = output / mode + if not args.validate_only: + root.mkdir() + execute(root, mode) + entries.append(verify(root, mode)) + print(f"{mode}: {entries[-1]['passing']} passing / {entries[-1]['mismatching']} precise mismatches", flush=True) + proof = dict(schema=1, producerSha256=sha(Path(__file__)), pin=PIN, entries=entries, + originalWordWitnessDetected=True, originalLongControlPassed=True, retirementApplied=False, + productionCpuChanged=False, publication=False, roadmapComplete=False) + if args.validate_only: + check(load(output / "proof.json") == proof, "Changed aggregate proof") + else: + save(output / "proof.json", proof) + print("SUBA indirect proof SHA-256: " + sha(output / "proof.json")) + + +if __name__ == "__main__": + main() From 4ee523df69688c2ded0b4d26567399dc96830ad7 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 22:58:12 +0300 Subject: [PATCH 128/211] Retire proven redundant SUBA indirect word and long regressions --- Copper68k.Tests/M68020AddressSourceTests.cs | 12 - docs/COPPER68K_REFERENCE_QUALIFICATION.md | 64 ++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 12 + ...test-copper68k-suba-indirect-retirement.py | 324 ++++++++++++++++++ 4 files changed, 400 insertions(+), 12 deletions(-) create mode 100644 scripts/test-copper68k-suba-indirect-retirement.py diff --git a/Copper68k.Tests/M68020AddressSourceTests.cs b/Copper68k.Tests/M68020AddressSourceTests.cs index 816dc2ba..4447e8a3 100644 --- a/Copper68k.Tests/M68020AddressSourceTests.cs +++ b/Copper68k.Tests/M68020AddressSourceTests.cs @@ -102,18 +102,6 @@ public void ImmediateIndexedBitOperationsReadTheOriginalBitAndKeepOtherFlags(ush Assert.Equal(CodeBase + 6, cpu.State.ProgramCounter); } - [Theory] - [InlineData(0x90D3, 0xFFFE, 0, 0x1002u)] - [InlineData(0x91D3, 0, 2, 0xFFEu)] - public void SubaIndirectSignExtendsWordsAndLeavesCcrUntouched(ushort opcode, ushort hi, ushort lo, uint expected) - { - var bus = new ZeroWaitCodeBus(); WriteWords(bus, CodeBase, opcode); WriteWords(bus, 0x2000, hi, lo); - using var cpu = M68kCoreFactory.Default.Create(M68kCpuModel.M68EC020, bus); - cpu.Reset(CodeBase, 0x3000); cpu.State.A[3] = 0x2000; cpu.State.A[0] = 0x1000; cpu.State.StatusRegister = 0x201F; - cpu.ExecuteInstruction(); Assert.Equal(expected, cpu.State.A[0]); Assert.Equal(0x2000u, cpu.State.A[3]); - Assert.Equal(0x201F, cpu.State.StatusRegister); Assert.Equal(CodeBase + 2, cpu.State.ProgramCounter); - } - [Theory] [InlineData(0x2060, 0)] [InlineData(0x2260, 1)] public void MoveaPredecrementLatchesSourceBeforeDestinationAndKeepsFlags(ushort opcode, int destination) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 8d5ed4eb..46ad66b2 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -10089,3 +10089,67 @@ retention are still required before removing that method. Earlier full CPU results do not include this new group. Timing, prefetch, native media and other specialized regressions remain retained. No CPU import/publication occurs; milestone 6 remains **in progress**, `roadmapComplete=false`. + +## SUBA indirect word/long retirement completed — 2026-10-07 + +The preceding SUBA retirement-pending gate is now closed for the one pure +semantic method `SubaIndirectSignExtendsWordsAndLeavesCcrUntouched` (two rows). +Only its twelve-line attribute/method block is removed. The remaining class is +byte-equivalent to the pinned original after that exact removal; its timing, +factory-profile, memory, extension-order and continuation checks remain. +Milestone 6 as a whole is still **in progress**. + +The maintained successor `scripts/test-copper68k-suba-indirect-retirement.py` +restores original witness source from pin +`073d1f4ea604f8f4db1b5b42a647c1fb942f3fd7` in isolated copies. It checks all +source identities, exact original/reduced class rosters, the actual command, +TRX outcomes/counters/stdout, all profile keys/weights and precise failure IDs +and reasons. No normal build outputs are used. The old sign-extension-only +command is historical and requires the pre-retirement `8b6418f` source checkout; +the successor below is the current reproduction command. + +```powershell +python scripts/test-copper68k-suba-indirect-retirement.py --output artifacts/suba-retirement-fresh +python scripts/test-copper68k-suba-indirect-retirement.py --output artifacts/suba-retirement-fresh --validate-only +``` + +| Isolated selection | Shared passing | Shared mismatching | xUnit executions | +| --- | ---: | ---: | ---: | +| Original class and shared matrix | 12,288 | 0 | 55 | +| Word zero-extension defect | 9,216 | 3,072 | 10 | +| Long operand read as one word | 7,680 | 4,608 | 10 | +| Reduced class and shared matrix | 12,288 | 0 | 53 | + +Original class coverage is **47 passing cases**, reduced coverage **45 passing +cases**; eight shared batches run in each clean selection. Neither clean +selection skips a case. Each isolated mutation changes only the same semantic +operation in the base/advanced paths and fails all eight shared batches plus +its corresponding historical witness. The other historical width passes. +The long defect reads the operand's high word in place of its complete long; +the original long witness fails with expected `4094` / actual `4096`. Thus +both removed rows have separate discriminating proofs, rather than relying on +word failures to justify retiring long coverage. + +Replacement anchors (all other CCRs, aliases, stack modes and profiles retained): + +- Word: `68EC020/SUBA/2/(A3)/r0/indirect-sign-alias-all-CCR/brief/super=True/op=90D3/s=0000FFFE/d=00001000/ccr=1F`. +- Long: `68EC020/SUBA/4/(A3)/r0/indirect-sign-alias-all-CCR/brief/super=True/op=91D3/s=00000002/d=00001000/ccr=1F`. + +Ten evidence controls reject missing fixture, wrong producer, empty selection, +missing historical witnesses, wrong coverage weights, unrelated failure reasons, +changed CPU mutation with updated manifest, wrong protected-output identity, +wrong command and wrong TRX counters. Fresh end-to-end generation from the +retired checkout and strict validate-only replay both pass. Evidence: +`audits/SubaIndirectRetirementV4/proof.json` in the restoration temporary root, +SHA-256 `b0bb373794722f9f9f16ccefcb0b972a988c902e79aa90d9f57d8dfe74d60050`. + +A failed V2 preparation printed the wrong proof filename after writing its +verified preparation record; that terminal exit-1 observation and frozen +producer are retained. The reporter was corrected before fresh V3 preparation, +retirement and complete V4 reproduction. No CPU/test expectation was weakened. +The restored checkout has no prior normal assemblies: old protection evidence +predates the external deletion and is not relabeled as a post-deletion result. +Earlier full CPU/private-consumer evidence retains its exact original source +scope; it does not include this new group or retirement. No production CPU +change, package import/publication or physical timing qualification occurs. +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 0fa979ee..53869234 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3308,3 +3308,15 @@ method remains: its separate long-width witness and remaining class need replacement/retention proof before retirement. No production CPU change or publication occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [SUBA indirect proof and remaining gate](COPPER68K_REFERENCE_QUALIFICATION.md#suba-indirect-shared-replacement-proof-retirement-pending--2026-10-07). + +The pending SUBA indirect retirement gate is complete: independent word +zero-extension and long-as-word mutations fail their respective original rows +and produce exactly 3,072 / 4,608 shared failures, with the other width retained +as a control. Original/reduced class runs pass 47 / 45 cases plus eight shared +batches, each covering 12,288 scenarios. Only the one two-row pure semantic +method is removed; exact source-removal scope is checked. Fresh end-to-end +reproduction and validate-only replay pass, including ten evidence controls. +All specialized timing, factory, continuation and memory regressions remain. +No production CPU change/import/publication occurs. Milestone 6 remains +**in progress**, `roadmapComplete=false`. See the +[completed SUBA word/long replacement proof](COPPER68K_REFERENCE_QUALIFICATION.md#suba-indirect-wordlong-retirement-completed--2026-10-07). diff --git a/scripts/test-copper68k-suba-indirect-retirement.py b/scripts/test-copper68k-suba-indirect-retirement.py new file mode 100644 index 00000000..31376e26 --- /dev/null +++ b/scripts/test-copper68k-suba-indirect-retirement.py @@ -0,0 +1,324 @@ +"""Prove SUBA indirect retirement with separate word and long defects. + +Python 3, Git and dotnet are required. --output must be fresh unless using +--validate-only. --prepare proves original witnesses before retirement; +--finish-retirement completes that evidence after the exact scoped removal. +""" +import argparse +import hashlib +import itertools +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import xml.etree.ElementTree as ET + +REPO = Path(__file__).resolve().parent.parent +PIN = "073d1f4ea604f8f4db1b5b42a647c1fb942f3fd7" +LEGACY = "Copper68k.Tests/M68020AddressSourceTests.cs" +FIXTURE = "Copper68k.Tests/Synthetic/SyntheticSubaIndirectTests.cs" +WITNESS = "SubaIndirectSignExtendsWordsAndLeavesCcrUntouched" +GROUP = "suba-indirect-consolidation" +METHOD = "IndirectSubaSignExtensionAliasesAndPreservedFlags" +MODELS = ["68000", "68010", "68EC020", "68020", "68030", "68040", "68060", "A1200"] +NS = {"t": "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"} + + +def check(ok, reason): + if not ok: + raise ValueError(reason) + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def load(path): + return json.loads(path.read_text(encoding="utf-8-sig")) + + +def save(path, value): + path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8") + + +def original(): + return subprocess.check_output(["git", "show", PIN + ":" + LEGACY], cwd=REPO).decode("utf-8-sig").replace("\r\n", "\n") + + +def reduced(): + text = original() + method = text.index(" public void " + WITNESS) + start = text.rfind(" [Theory]\n", 0, method) + end = text.index(" [Theory]\n", method) + check(start >= 0, "Missing retirement boundary") + return text[:start] + text[end:] + + +def mutation(path, text, mode): + if path == "Copper68k/M68kAdvancedTimingInterpreter.cs": + start = text.index(" private void ExecuteSubaAddressIndirectToAddress(") + end = text.index(" private void ExecuteSubaPostIncrementToAddress(", start) + old = "unchecked((uint)(int)(short)ReadWord(State.A[opcode & 7]))" if mode == "ZeroExtend" else "isLong ? ReadLong(State.A[opcode & 7])" + block = text[start:end] + check(block.count(old) == 1, "Missing advanced mutation target") + new = "unchecked((uint)ReadWord(State.A[opcode & 7]))" if mode == "ZeroExtend" else "isLong ? (uint)ReadWord(State.A[opcode & 7])" + return text[:start] + block.replace(old, new) + text[end:] + old = "SetAddressRegister(reg, State.A[reg] - M68kCpuState.SignExtend(value, size));" + check(text.count(old) == 1, "Missing base mutation target") + new = "mode == 2 && size == M68kOperandSize.Word ? value & 0xffffu : M68kCpuState.SignExtend(value, size)" if mode == "ZeroExtend" else "mode == 2 && size == M68kOperandSize.Long ? value >> 16 : M68kCpuState.SignExtend(value, size)" + return text.replace(old, "SetAddressRegister(reg, State.A[reg] - (" + new + "));") + + +def sources(mode): + paths = subprocess.check_output(["git", "ls-files", "Copper68k", "Copper68k.Tests"], cwd=REPO, text=True).splitlines() + paths = sorted(set(paths + [FIXTURE])) + result = {p: (REPO / p).read_bytes() for p in paths if Path(p).suffix in [".cs", ".csproj"]} + current = result[LEGACY].decode("utf-8-sig").replace("\r\n", "\n") + check(current in [original(), reduced()], "Wrong legacy retirement scope") + if mode == "Current": + check(current == reduced(), "Retirement not applied") + else: + result[LEGACY] = original().encode() + if mode in ["ZeroExtend", "LongWordRead"]: + for p in ["Copper68k/M68kCore.cs", "Copper68k/M68kAdvancedTimingInterpreter.cs"]: + result[p] = mutation(p, result[p].decode("utf-8-sig"), mode).encode() + return result + + +def inventory(source): + return {p.relative_to(source).as_posix(): sha(p) for p in source.rglob("*") + if p.suffix in [".cs", ".csproj"] and not {"bin", "obj"}.intersection(p.parts)} + + +def protected(): + paths = ["Copper68k/bin/Release/net10.0/Copper68k.dll", "Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll"] + return {p: sha(REPO / p) for p in paths if (REPO / p).exists()} + + +def command(root, mode): + selection = "M68020AddressSourceTests" if mode in ["Clean", "Current"] else WITNESS + return ["dotnet", "test", str(root / "source/Copper68k.Tests/Copper68k.Tests.csproj"), "-c", "Release", + "--artifacts-path", str(root / "build"), "--filter", f"FullyQualifiedName~{METHOD}|FullyQualifiedName~{selection}", + "--logger", "trx;LogFileName=audit.trx", "--results-directory", str(root)] + + +def execute(root, mode): + for p, content in sources(mode).items(): + target = root / "source" / p + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(content) + args = command(root, mode) + record = dict(mode=mode, pin=PIN, producer=sha(Path(__file__)), sources=inventory(root / "source"), protected=protected(), command=args) + env = dict(os.environ, COPPER68K_SYNTHETIC_MODELS=",".join(MODELS), COPPER68K_SYNTHETIC_REPORT_DIR=str(root)) + selection = "the original class" if mode == "Clean" else "the reduced class" if mode == "Current" else "two historical witnesses" + print(mode + ": executing eight shared profiles and " + selection, flush=True) + with (root / "execution.log").open("w", encoding="utf-8") as log: + record["testExit"] = subprocess.run(args, env=env, cwd=REPO, stdout=log, stderr=subprocess.STDOUT).returncode + record["logSha256"] = sha(root / "execution.log") + save(root / "inputs.json", record) + + +def expected(model, mode): + combinations, failures = {}, [] + for width, reg, supervisor in itertools.product([2, 4], [0, 3, 7], [False, True]): + key = f"{model}/SUBA/{width}/(A3)/r{reg}/indirect-sign-alias-all-CCR/brief/super={supervisor}" + bad = mode == "ZeroExtend" and width == 2 or mode == "LongWordRead" and width == 4 + missed = 64 if mode == "ZeroExtend" else 96 + combinations[key] = dict(passing=128 - missed, mismatching=missed) if bad else dict(passing=128) + if bad: + destination = 0x3000 if reg == 3 else 0x1000 + values = [0xfffe, 0x8000] if mode == "ZeroExtend" else [2, 0x80000000, 0x7fffffff] + for value, ccr in itertools.product(values, range(32)): + rhs = value - 0x10000 if mode == "ZeroExtend" else value + wanted = (destination - rhs) & 0xffffffff + actual = (destination - (value if mode == "ZeroExtend" else value >> 16)) & 0xffffffff + opcode = (0x90d3 if width == 2 else 0x91d3) | reg << 9 + failures.append(dict(id=f"{key}/op={opcode:04X}/s={value:08X}/d={destination:08X}/ccr={ccr:02X}", + status="mismatching", reason=f"A{reg} expected {wanted:08X}, actual {actual:08X}")) + return combinations, failures + + +def legacy_names(): + text = original() + names = {} + for match in re.finditer(r"public void (\w+)\(([^)]*)\)", text): + method, signature = match.groups() + start = max(text.rfind("[Theory]", 0, match.start()), text.rfind("[Fact]", 0, match.start())) + attributes = text[start:match.start()] + parameters = [p.strip().split()[-1] for p in signature.split(",")] if signature else [] + rows = re.findall(r"\[InlineData\(([^)]*)\)\]", attributes) if parameters else [""] + check(bool(rows), "Missing historical rows") + for row in rows: + values = [] + for value in row.split(",") if row else []: + value = value.strip() + if value in ["true", "false"]: + values.append(value.title()) + elif value.startswith("M68kCpuModel."): + values.append(value.split(".")[-1]) + else: + values.append(str(int(value.rstrip("uU"), 0))) + check(len(values) == len(parameters), "Wrong historical parameters") + suffix = "(" + ", ".join(f"{p}: {v}" for p, v in zip(parameters, values)) + ")" if parameters else "" + key = "Copper68k.Tests.M68020AddressSourceTests." + method + suffix + check(key not in names, "Duplicate historical row") + names[key] = "Passed" + check(len(names) > 30, "Incomplete historical class") + return names + + +def verify(root, mode): + record = load(root / "inputs.json") + check(record["mode"] == mode and record["pin"] == PIN and record["producer"] == sha(Path(__file__)), "Wrong producer or scope") + wanted = {p: hashlib.sha256(v).hexdigest() for p, v in sources(mode).items()} + check(record["sources"] == wanted, "Wrong baseline or mutation identity") + check(inventory(root / "source") == wanted, "Incomplete or changed source inventory") + check(record["protected"] == protected(), "Changed protected assemblies") + check(record["command"] == command(root, mode), "Wrong execution command") + mutated = mode in ["ZeroExtend", "LongWordRead"] + check(record["testExit"] == (1 if mutated else 0) and record["logSha256"] == sha(root / "execution.log"), "Wrong execution or log") + tree = ET.parse(root / "audit.trx") + tests = tree.findall(".//t:UnitTestResult", NS) + names = {f'Copper68k.Tests.Synthetic.SyntheticSubaIndirectTests.{METHOD}(modelId: "{m}")': "Failed" if mutated else "Passed" for m in MODELS} + prefix = "Copper68k.Tests.M68020AddressSourceTests." + WITNESS + word = prefix + "(opcode: 37075, hi: 65534, lo: 0, expected: 4098)" + names[word] = "Failed" if mode == "ZeroExtend" else "Passed" + long = prefix + "(opcode: 37331, hi: 0, lo: 2, expected: 4094)" + names[long] = "Failed" if mode == "LongWordRead" else "Passed" + if mode in ["Clean", "Current"]: + names.update(legacy_names()) + if mode == "Current": + names.pop(word) + names.pop(long) + check(len(tests) == len(names) and {t.get("testName") for t in tests} == set(names), "Wrong or empty execution selection") + check(all(t.get("outcome") == names[t.get("testName")] for t in tests), "Wrong execution outcomes") + if mode == "ZeroExtend": + message = next(t for t in tests if t.get("testName") == word).find("t:Output/t:ErrorInfo/t:Message", NS).text + check(re.search(r"Expected:\s*4098\b", message) and re.search(r"Actual:\s*4294905858\b", message), "Wrong historical failure reason") + if mode == "LongWordRead": + message = next(t for t in tests if t.get("testName") == long).find("t:Output/t:ErrorInfo/t:Message", NS).text + check(re.search(r"Expected:\s*4094\b", message) and re.search(r"Actual:\s*4096\b", message), "Wrong historical failure reason") + counters = tree.find("t:ResultSummary/t:Counters", NS) + failed = sum(v == "Failed" for v in names.values()) + check(counters is not None and all(int(counters.get(k, -1)) == v for k, v in dict(total=len(names), executed=len(names), passed=len(names)-failed, failed=failed, notExecuted=0).items()), "Wrong TRX counters") + stdout = "\n".join(t.text or "" for t in tree.findall(".//t:UnitTestResult/t:Output/t:StdOut", NS)) + reports = [] + for model in MODELS: + path = root / f"{model}-{GROUP}.json" + report = load(path) + combinations, failures = expected(model, mode) + counts = dict(passing=1536 - len(failures), mismatching=len(failures), unsupported=0, untested=0) + check(report["schema"] == 1 and report["model"] == model and report["group"] == GROUP and report["logicalCases"] == 1536 and report["xunitBatches"] == 1, "Wrong report scope") + check(report["counts"] == counts and report["combinations"] == combinations, "Wrong coverage keys or weights") + check(sorted(report["failures"], key=lambda f: f["id"]) == sorted(failures, key=lambda f: f["id"]), "Wrong failure IDs or reasons") + matches = re.findall(re.escape(f"{model}/{GROUP}: ") + r"(\{[^\r\n]+\}); logical cases=1536, xUnit batches=1, combinations=12", stdout) + check(len(matches) == 1 and json.loads(matches[0]) == counts, "TRX report disagreement") + reports.append(dict(path=path.name, sha256=sha(path))) + check({p.name for p in root.glob(f"*-{GROUP}.json")} == {r["path"] for r in reports}, "Foreign report selection") + missed = 3072 if mode == "ZeroExtend" else 4608 if mode == "LongWordRead" else 0 + return dict(mode=mode, passing=12288 - missed, mismatching=missed, + executions=len(names), reports=reports, inputsSha256=sha(root / "inputs.json"), trxSha256=sha(root / "audit.trx")) + + +def integrity(output, validate_only): + definitions = [("MissingFixture", "Incomplete or changed source inventory"), + ("WrongProducer", "Wrong producer or scope"), + ("EmptySelection", "Wrong or empty execution selection"), + ("MissingHistoricalWitness", "Wrong or empty execution selection"), + ("WrongWeight", "Wrong coverage keys or weights"), + ("WrongFailureReason", "Wrong failure IDs or reasons"), + ("ChangedMutationAndManifest", "Wrong baseline or mutation identity"), + ("MissingProtection", "Changed protected assemblies"), + ("WrongCommand", "Wrong execution command"), + ("WrongCounters", "Wrong TRX counters")] + records = [] + for name, reason in definitions: + target = output / "integrity" / name + if not validate_only: + parent = output / "LongWordRead" + shutil.copytree(parent / "source", target / "source", ignore=shutil.ignore_patterns("bin", "obj")) + for path in [parent / "inputs.json", parent / "execution.log", parent / "audit.trx", *parent.glob(f"*-{GROUP}.json")]: + shutil.copyfile(path, target / path.name) + data = load(target / "inputs.json") + data["command"] = command(target, "LongWordRead") + if name == "MissingFixture": + path = target / "source" / FIXTURE + check(path.resolve().is_relative_to(target.resolve()), "Unsafe integrity path") + path.unlink() + elif name == "WrongProducer": + data["producer"] = "0" * 64 + elif name == "MissingProtection": + data["protected"]["absent-protected-assembly"] = "0" * 64 + elif name == "WrongCommand": + data["command"][data["command"].index("--filter") + 1] = "FullyQualifiedName~Nothing" + elif name == "ChangedMutationAndManifest": + path = target / "source/Copper68k/M68kCore.cs" + path.write_text(path.read_text(encoding="utf-8-sig") + "\n// sole evidence defect\n", encoding="utf-8") + data["sources"]["Copper68k/M68kCore.cs"] = sha(path) + elif name in ["EmptySelection", "MissingHistoricalWitness", "WrongCounters"]: + tree = ET.parse(target / "audit.trx") + if name == "WrongCounters": + tree.find("t:ResultSummary/t:Counters", NS).set("failed", "0") + else: + results = tree.find("t:Results", NS) + for test in list(results): + if name == "EmptySelection" or WITNESS in test.get("testName", ""): + results.remove(test) + tree.write(target / "audit.trx") + else: + path = target / f"68020-{GROUP}.json" + report = load(path) + if name == "WrongWeight": + report["combinations"][next(iter(report["combinations"]))]["passing"] += 1 + else: + report["failures"][0]["reason"] = "unrelated failure" + save(path, report) + save(target / "inputs.json", data) + try: + verify(target, "LongWordRead") + except ValueError as error: + check(str(error) == reason, "Wrong integrity rejection: " + name + ": " + str(error)) + records.append(dict(name=name, reason=reason)) + else: + raise ValueError("Corrupted evidence accepted: " + name) + return records + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--validate-only", action="store_true") + parser.add_argument("--prepare", action="store_true") + parser.add_argument("--finish-retirement", action="store_true") + args = parser.parse_args() + output = args.output.resolve() + check(sum([args.validate_only, args.prepare, args.finish_retirement]) <= 1, "Conflicting audit modes") + if not args.prepare: + check((REPO / LEGACY).read_text(encoding="utf-8-sig") == reduced(), "Retirement not applied exactly") + if not args.validate_only and not args.finish_retirement: + check(not output.exists(), "Use fresh audit output") + output.mkdir(parents=True) + entries = [] + for mode in ["Clean", "ZeroExtend", "LongWordRead"] + ([] if args.prepare else ["Current"]): + root = output / mode + if not args.validate_only and (not args.finish_retirement or mode == "Current"): + root.mkdir() + execute(root, mode) + entries.append(verify(root, mode)) + print(f"{mode}: {entries[-1]['passing']} passing / {entries[-1]['mismatching']} precise mismatches", flush=True) + proof = dict(schema=2, producerSha256=sha(Path(__file__)), pin=PIN, entries=entries, + originalWordWitnessDetected=True, originalLongWitnessDetected=True, + integrity=integrity(output, args.validate_only or args.finish_retirement), retirementApplied=not args.prepare, + productionCpuChanged=False, publication=False, roadmapComplete=False) + if args.validate_only: + check(load(output / "proof.json") == proof, "Changed aggregate proof") + else: + save(output / ("preparation-proof.json" if args.prepare else "proof.json"), proof) + proof_path = output / ("preparation-proof.json" if args.prepare else "proof.json") + print("SUBA indirect proof SHA-256: " + sha(proof_path)) + + +if __name__ == "__main__": + main() From 2d4749525144d593b869cc642a156a482c7d2476 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 23:14:21 +0300 Subject: [PATCH 129/211] Consolidate CMPA displacement regression with shared audit infrastructure --- Copper68k.Tests/M68020AddressSourceTests.cs | 12 - .../SyntheticCmpaDisplacementTests.cs | 24 ++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 76 ++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 14 + scripts/copper68k_consolidation.py | 242 ++++++++++++++++++ ...-copper68k-cmpa-displacement-retirement.py | 78 ++++++ 6 files changed, 434 insertions(+), 12 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticCmpaDisplacementTests.cs create mode 100644 scripts/copper68k_consolidation.py create mode 100644 scripts/test-copper68k-cmpa-displacement-retirement.py diff --git a/Copper68k.Tests/M68020AddressSourceTests.cs b/Copper68k.Tests/M68020AddressSourceTests.cs index 4447e8a3..a216452c 100644 --- a/Copper68k.Tests/M68020AddressSourceTests.cs +++ b/Copper68k.Tests/M68020AddressSourceTests.cs @@ -46,18 +46,6 @@ public void AddToIndirectMemoryWritesSelectedWidthAndPreservesSource(ushort opco Assert.Equal(source, cpu.State.D[0]); Assert.Equal(flags, cpu.State.StatusRegister & 31); Assert.Equal(CodeBase + 2, cpu.State.ProgramCounter); } - [Theory] - [InlineData(0xFFFFu, 0xFFFF, 0x11)] [InlineData(0xFFFFFFFFu, 0xFFFF, 0x14)] - [InlineData(0u, 1, 0x19)] - public void CmpaWordDisplacementComparesSignExtendedSourceAgainstFullAddress(uint destination, ushort source, int flags) - { - var bus = new ZeroWaitCodeBus(); WriteWords(bus, CodeBase, 0xB2E8, 0xFFFE); WriteWords(bus, 0x2000, source); - using var cpu = M68kCoreFactory.Default.CreateA1200Ec020(bus); - cpu.Reset(CodeBase, 0x3000); cpu.State.A[0] = 0x2002; cpu.State.A[1] = destination; cpu.State.StatusRegister = 0x201F; - cpu.ExecuteInstruction(); Assert.Equal(flags, cpu.State.StatusRegister & 31); - Assert.Equal(destination, cpu.State.A[1]); Assert.Equal(CodeBase + 4, cpu.State.ProgramCounter); - } - [Theory] [InlineData(0x5270, 0xFFFF, 0, 0x15)] [InlineData(0x5370, 0, 0xFFFF, 0x19)] [InlineData(0x5070, 0x7FF8, 0x8000, 10)] diff --git a/Copper68k.Tests/Synthetic/SyntheticCmpaDisplacementTests.cs b/Copper68k.Tests/Synthetic/SyntheticCmpaDisplacementTests.cs new file mode 100644 index 00000000..1bfe4831 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticCmpaDisplacementTests.cs @@ -0,0 +1,24 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticCmpaDisplacementTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void WordSourceComparesAgainstFullAddressAndPreservesExtend(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "cmpa-displacement-consolidation"); + foreach (var supervisor in new[] { false, true }) + foreach (var destination in new uint[] { 0xffff, uint.MaxValue, 0, 0x7fffffff, 0x80000000 }) + foreach (var source in new uint[] { 0xffff, 1, 0x8000, 0x7fff }) + for (var ccr = 0; ccr < 32; ccr++) + SyntheticArithmeticTests.Case(machine, report, "CMPA", 2, new(5, 0), source, + destination, ccr, reg: 1, supervisor: supervisor, + scenario: "negative-d16-word-source-full-destination-all-CCR", + options: new(SourceDisplacement: -2)); + report.Complete(output); + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 46ad66b2..77ddbfdb 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -10153,3 +10153,79 @@ Earlier full CPU/private-consumer evidence retains its exact original source scope; it does not include this new group or retirement. No production CPU change, package import/publication or physical timing qualification occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## CMPA displacement sign-extension consolidation — 2026-10-07 + +`SyntheticCmpaDisplacementTests.WordSourceComparesAgainstFullAddressAndPreservesExtend` +adds **10,240 ordinary-CI scenarios** across all eight profiles: five full +32-bit destination boundaries, four word source values, every CCR and both +user/supervisor stacks. It uses the common independent operand fixture and +architectural/memory verifier. Source extension is explicitly `FFFE` (-2), +preserving the negative displacement exercised by the historical regression. +All data/address registers, selected/inactive stacks, PC, defined SR, execution +state and surrounding memory are checked through the shared verifier. + +The pure semantic method +`CmpaWordDisplacementComparesSignExtendedSourceAgainstFullAddress` is retired: +three rows, one twelve-line attribute/method block. Its original source is +restored from Git pin `4ee523df69688c2ded0b4d26567399dc96830ad7` only in isolated +audit copies. The rest of the class must equal the original after exactly that +removal. No production CPU source changes. The original 45 class cases and +reduced 42 class cases all pass, including their timing, factory-profile, +extension, memory and continuation checks. + +The maintained family driver shares `scripts/copper68k_consolidation.py` for +source inventories, pinned original/removal scope, isolated execution, exact +xUnit rosters/outcomes, TRX counters/stdout, producer/command/protection identities +and corrupted-evidence controls. Family-specific expectations remain in +`scripts/test-copper68k-cmpa-displacement-retirement.py`: independent integer +comparison arithmetic and explicit scenario/failure enumeration, without +production decoder, flag or effective-address helpers. Both Python producer +identities are bound. Older qualified commands remain source-specific: the +preceding complete SUBA command requires its qualified `4ee523d` checkout; +the earlier sign-extension-only SUBA command requires `8b6418f`. + +```powershell +python scripts/test-copper68k-cmpa-displacement-retirement.py --output artifacts/cmpa-retirement-fresh +python scripts/test-copper68k-cmpa-displacement-retirement.py --output artifacts/cmpa-retirement-fresh --validate-only +``` + +| Isolated selection | Shared passing | Shared mismatching | xUnit executions | +| --- | ---: | ---: | ---: | +| Original class plus shared matrix | 10,240 | 0 | 53 | +| Word source zero-extension defect | 5,120 | 5,120 | 11 | +| Reduced class plus shared matrix | 10,240 | 0 | 50 | + +The sole semantic defect zero-extends word displacement sources in the base +arithmetic decoder and advanced CMPA path. All eight shared batches and the two +negative-source historical rows fail precisely; the original positive-source +row passes as a control. Expected/actual flags in the historical failures are +`17/20` and `20/24`. The complete failed scenario IDs and full-SR differences +are independently enumerated. All 5,120 positive-source controls pass; no +unsupported/untested case or skipped clean execution is credited as coverage. + +Replacement anchors, with all other CCRs/profiles and boundaries retained: + +- `A1200/CMPA/2/d16(A0)/r1/negative-d16-word-source-full-destination-all-CCR/brief/super=True/op=B2E8/s=0000FFFF/d=0000FFFF/ccr=1F`. +- `A1200/CMPA/2/d16(A0)/r1/negative-d16-word-source-full-destination-all-CCR/brief/super=True/op=B2E8/s=0000FFFF/d=FFFFFFFF/ccr=1F`. +- `A1200/CMPA/2/d16(A0)/r1/negative-d16-word-source-full-destination-all-CCR/brief/super=True/op=B2E8/s=00000001/d=00000000/ccr=1F`. + +Nine controls reject missing fixture, wrong producer, empty selection, missing +historical witnesses, wrong weights, unrelated failure reasons, changed CPU +mutation with updated manifest, wrong command and wrong TRX counters. Fresh +end-to-end generation from retired source and strict replay both pass. +Evidence: `audits/CmpaDisplacementRetirementV4/proof.json` in the restoration +temporary root, SHA-256 +`44bf90ce38d0efcc21c98c0270484d2905566d188a5c435e1a5bba00948a2931`. +V1 clean execution passed but its mutation target guard rejected a nonunique +expression before mutation execution. The target was narrowed to the intended +arithmetic function. V2 proved source semantics at a positive offset; before +retirement V3/V4 explicitly preserved the old negative displacement. These +older attempts are retained separately and do not substitute for V4 acceptance. + +This consolidates one method; it does not establish physical timing or replace +specialized cache/prefetch/JIT/fault/native-media coverage. Earlier full CPU and +consumer proofs retain their original source scope and do not include this +new group/retirement. The hardware-dependent disagreements and broader frame/ +reference requirements remain open. No CPU import or package publication occurs. +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 53869234..78fc8cda 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3320,3 +3320,17 @@ All specialized timing, factory, continuation and memory regressions remain. No production CPU change/import/publication occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [completed SUBA word/long replacement proof](COPPER68K_REFERENCE_QUALIFICATION.md#suba-indirect-wordlong-retirement-completed--2026-10-07). + +CMPA's three-row pure displacement/sign-extension method is retired after +10,240 shared scenarios across eight profiles pass with the original negative +displacement, full 32-bit destinations, all CCRs and both stack modes. A scoped +base/advanced source-sign mutation produces exactly 5,120 shared failures and +detects both original negative-source witnesses, retaining 5,120 controls and +the original positive-source case. Original/reduced class runs pass 45 / 42 +cases plus eight shared batches. Exact removal scope and all rosters/results +are verified; timing and integration checks remain. A reusable internal audit +runner checks producer/source/command identities and nine corruption controls. +Fresh end-to-end reproduction and strict replay pass. No production CPU change +or publication occurs. Milestone 6 remains **in progress**, +`roadmapComplete=false`. See the +[CMPA replacement proof](COPPER68K_REFERENCE_QUALIFICATION.md#cmpa-displacement-sign-extension-consolidation--2026-10-07). diff --git a/scripts/copper68k_consolidation.py b/scripts/copper68k_consolidation.py new file mode 100644 index 00000000..9f36e13e --- /dev/null +++ b/scripts/copper68k_consolidation.py @@ -0,0 +1,242 @@ +"""Internal shared runner for pinned instruction-regression retirement proofs.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import xml.etree.ElementTree as ET + +MODELS = ["68000", "68010", "68EC020", "68020", "68030", "68040", "68060", "A1200"] +NS = {"t": "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"} + + +def check(ok, reason): + if not ok: + raise ValueError(reason) + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def load(path): + return json.loads(path.read_text(encoding="utf-8-sig")) + + +def save(path, value): + path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8") + + +class Audit: + def __init__(self, spec): + self.s = spec + self.repo = spec["repo"] + self.original = subprocess.check_output(["git", "show", spec["pin"] + ":" + spec["legacy"]], cwd=self.repo).decode("utf-8-sig").replace("\r\n", "\n") + method = self.original.index(" public void " + spec["witness"]) + start = self.original.rfind(" [Theory]\n", 0, method) + end = self.original.index(" [Theory]\n", method) + check(start >= 0, "Missing retirement boundary") + self.reduced = self.original[:start] + self.original[end:] + + def producers(self): + return {str(p.relative_to(self.repo)): sha(p) for p in [Path(__file__).resolve(), self.s["producer"]]} + + def sources(self, mode): + paths = subprocess.check_output(["git", "ls-files", "Copper68k", "Copper68k.Tests"], cwd=self.repo, text=True).splitlines() + paths = sorted(set(paths + [self.s["fixture"]])) + result = {p: (self.repo / p).read_bytes() for p in paths if Path(p).suffix in [".cs", ".csproj"]} + current = result[self.s["legacy"]].decode("utf-8-sig").replace("\r\n", "\n") + check(current in [self.original, self.reduced], "Wrong legacy retirement scope") + if mode == "Current": + check(current == self.reduced, "Retirement not applied") + else: + result[self.s["legacy"]] = self.original.encode() + if mode == "Mutation": + for p in self.s["mutation_files"]: + result[p] = self.s["mutate"](p, result[p].decode("utf-8-sig")).encode() + return result + + def protected(self): + paths = ["Copper68k/bin/Release/net10.0/Copper68k.dll", "Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll"] + return {p: sha(self.repo / p) for p in paths if (self.repo / p).exists()} + + @staticmethod + def inventory(source): + return {p.relative_to(source).as_posix(): sha(p) for p in source.rglob("*") + if p.suffix in [".cs", ".csproj"] and not {"bin", "obj"}.intersection(p.parts)} + + def command(self, root, mode): + selection = self.s["witness"] if mode == "Mutation" else self.s["legacy_class"] + return ["dotnet", "test", str(root / "source/Copper68k.Tests/Copper68k.Tests.csproj"), "-c", "Release", + "--artifacts-path", str(root / "build"), "--filter", f"FullyQualifiedName~{self.s['method']}|FullyQualifiedName~{selection}", + "--logger", "trx;LogFileName=audit.trx", "--results-directory", str(root)] + + def execute(self, root, mode): + for p, contents in self.sources(mode).items(): + target = root / "source" / p + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(contents) + args = self.command(root, mode) + data = dict(schema=1, mode=mode, pin=self.s["pin"], producers=self.producers(), sources=self.inventory(root / "source"), protected=self.protected(), command=args) + env = dict(os.environ, COPPER68K_SYNTHETIC_MODELS=",".join(MODELS), COPPER68K_SYNTHETIC_REPORT_DIR=str(root)) + print(mode + ": executing shared coverage and pinned legacy selection", flush=True) + with (root / "execution.log").open("w", encoding="utf-8") as log: + data["testExit"] = subprocess.run(args, cwd=self.repo, env=env, stdout=log, stderr=subprocess.STDOUT).returncode + data["logSha256"] = sha(root / "execution.log") + save(root / "inputs.json", data) + + def legacy_names(self): + names = {} + for match in re.finditer(r"public void (\w+)\(([^)]*)\)", self.original): + method, signature = match.groups() + start = max(self.original.rfind("[Theory]", 0, match.start()), self.original.rfind("[Fact]", 0, match.start())) + parameters = [p.strip().split()[-1] for p in signature.split(",")] if signature else [] + rows = re.findall(r"\[InlineData\(([^)]*)\)\]", self.original[start:match.start()]) if parameters else [""] + check(bool(rows), "Missing historical rows") + for row in rows: + values = [] + for v in row.split(",") if row else []: + v = v.strip() + values.append(v.title() if v in ["true", "false"] else v.split(".")[-1] if v.startswith("M68kCpuModel.") else str(int(v.rstrip("uU"), 0))) + check(len(values) == len(parameters), "Wrong historical parameters") + suffix = "(" + ", ".join(f"{p}: {v}" for p, v in zip(parameters, values)) + ")" if parameters else "" + key = self.s["legacy_class"] + "." + method + suffix + check(key not in names, "Duplicate historical row") + names[key] = "Passed" + check(len(names) == self.s["legacy_cases"], "Incomplete original class") + return names + + def verify(self, root, mode): + data = load(root / "inputs.json") + check(data["schema"] == 1 and data["mode"] == mode and data["pin"] == self.s["pin"] and data["producers"] == self.producers(), "Wrong producer or scope") + ids = {p: hashlib.sha256(v).hexdigest() for p, v in self.sources(mode).items()} + check(data["sources"] == ids, "Wrong baseline or mutation identity") + check(self.inventory(root / "source") == ids, "Incomplete or changed source inventory") + check(data["protected"] == self.protected(), "Wrong protected identities") + check(data["command"] == self.command(root, mode), "Wrong execution command") + check(data["testExit"] == (1 if mode == "Mutation" else 0) and data["logSha256"] == sha(root / "execution.log"), "Wrong execution or log") + names = self.legacy_names() + witnesses = {n for n in names if self.s["witness"] in n} + check(len(witnesses) == self.s["witness_cases"], "Wrong witness roster") + if mode == "Mutation": + names = {n: self.s["witness_outcome"](n) for n in witnesses} + elif mode == "Current": + names = {n: v for n, v in names.items() if n not in witnesses} + names.update({f'{self.s["shared_class"]}.{self.s["method"]}(modelId: "{m}")': "Failed" if mode == "Mutation" else "Passed" for m in MODELS}) + tree = ET.parse(root / "audit.trx") + tests = tree.findall(".//t:UnitTestResult", NS) + check(len(tests) == len(names) and {t.get("testName") for t in tests} == set(names), "Wrong or empty execution selection") + check(all(t.get("outcome") == names[t.get("testName")] for t in tests), "Wrong execution outcomes") + for t in tests: + name = t.get("testName") + if mode == "Mutation" and name in witnesses and names[name] == "Failed": + self.s["witness_error"](name, t.find("t:Output/t:ErrorInfo/t:Message", NS).text) + counters = tree.find("t:ResultSummary/t:Counters", NS) + failed = sum(v == "Failed" for v in names.values()) + check(counters is not None and all(int(counters.get(k, -1)) == v for k, v in dict(total=len(names), executed=len(names), passed=len(names)-failed, failed=failed, notExecuted=0).items()), "Wrong TRX counters") + stdout = "\n".join(t.text or "" for t in tree.findall(".//t:UnitTestResult/t:Output/t:StdOut", NS)) + reports, passed, missed = [], 0, 0 + for model in MODELS: + path = root / f"{model}-{self.s['group']}.json" + report = load(path) + expected = self.s["report"](model, mode) + check(report == expected, "Wrong report keys weights or failures") + matches = re.findall(re.escape(f"{model}/{self.s['group']}: ") + r"(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)", stdout) + check(len(matches) == 1 and json.loads(matches[0][0]) == expected["counts"] and int(matches[0][1]) == expected["logicalCases"] and int(matches[0][2]) == len(expected["combinations"]), "TRX report disagreement") + reports.append(dict(path=path.name, sha256=sha(path))) + passed += expected["counts"]["passing"] + missed += expected["counts"]["mismatching"] + check({p.name for p in root.glob(f"*-{self.s['group']}.json")} == {r["path"] for r in reports}, "Foreign report selection") + return dict(mode=mode, passing=passed, mismatching=missed, executions=len(names), reports=reports, inputsSha256=sha(root / "inputs.json"), trxSha256=sha(root / "audit.trx")) + + def integrity(self, output, validate_only): + definitions = [("MissingFixture", "Incomplete or changed source inventory"), ("WrongProducer", "Wrong producer or scope"), + ("EmptySelection", "Wrong or empty execution selection"), ("MissingWitness", "Wrong or empty execution selection"), + ("WrongWeight", "Wrong report keys weights or failures"), ("WrongFailure", "Wrong report keys weights or failures"), + ("ChangedMutationAndManifest", "Wrong baseline or mutation identity"), ("WrongCommand", "Wrong execution command"), + ("WrongCounters", "Wrong TRX counters")] + records = [] + for name, reason in definitions: + target = output / "integrity" / name + if not validate_only: + parent = output / "Mutation" + shutil.copytree(parent / "source", target / "source", ignore=shutil.ignore_patterns("bin", "obj")) + for path in [parent / "inputs.json", parent / "execution.log", parent / "audit.trx", *parent.glob(f"*-{self.s['group']}.json")]: + shutil.copyfile(path, target / path.name) + data = load(target / "inputs.json") + data["command"] = self.command(target, "Mutation") + if name == "MissingFixture": + path = target / "source" / self.s["fixture"] + check(path.resolve().is_relative_to(target.resolve()), "Unsafe integrity path") + path.unlink() + elif name == "WrongProducer": + data["producers"] = {} + elif name == "ChangedMutationAndManifest": + key = self.s["mutation_files"][0] + path = target / "source" / key + path.write_text(path.read_text(encoding="utf-8-sig") + "\n// unrelated defect\n", encoding="utf-8") + data["sources"][key] = sha(path) + elif name == "WrongCommand": + data["command"][data["command"].index("--filter") + 1] = "FullyQualifiedName~Nothing" + elif name in ["EmptySelection", "MissingWitness", "WrongCounters"]: + tree = ET.parse(target / "audit.trx") + if name == "WrongCounters": + tree.find("t:ResultSummary/t:Counters", NS).set("failed", "0") + else: + results = tree.find("t:Results", NS) + for test in list(results): + if name == "EmptySelection" or self.s["witness"] in test.get("testName", ""): + results.remove(test) + tree.write(target / "audit.trx") + else: + path = target / f"68020-{self.s['group']}.json" + report = load(path) + if name == "WrongWeight": + report["logicalCases"] += 1 + else: + report["failures"][0]["reason"] = "unrelated failure" + save(path, report) + save(target / "inputs.json", data) + try: + self.verify(target, "Mutation") + except ValueError as error: + check(str(error) == reason, "Wrong corruption rejection: " + name + ": " + str(error)) + records.append(dict(name=name, reason=reason)) + else: + raise ValueError("Corrupted evidence accepted: " + name) + return records + + def main(self): + parser = argparse.ArgumentParser(description=self.s["description"]) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--validate-only", action="store_true") + parser.add_argument("--prepare", action="store_true") + parser.add_argument("--finish-retirement", action="store_true") + args = parser.parse_args() + check(sum([args.validate_only, args.prepare, args.finish_retirement]) <= 1, "Conflicting audit modes") + if not args.prepare: + check((self.repo / self.s["legacy"]).read_text(encoding="utf-8-sig") == self.reduced, "Retirement not applied exactly") + output = args.output.resolve() + if not args.validate_only and not args.finish_retirement: + check(not output.exists(), "Use fresh audit output") + output.mkdir(parents=True) + entries = [] + for mode in ["Clean", "Mutation"] + ([] if args.prepare else ["Current"]): + root = output / mode + if not args.validate_only and (not args.finish_retirement or mode == "Current"): + root.mkdir() + self.execute(root, mode) + entries.append(self.verify(root, mode)) + print(f"{mode}: {entries[-1]['passing']} passing / {entries[-1]['mismatching']} precise mismatches", flush=True) + proof = dict(schema=1, producers=self.producers(), pin=self.s["pin"], entries=entries, + integrity=self.integrity(output, args.validate_only or args.finish_retirement), retiredRows=self.s["witness_cases"] if not args.prepare else 0, + productionCpuChanged=False, publication=False, roadmapComplete=False) + path = output / ("preparation-proof.json" if args.prepare else "proof.json") + if args.validate_only: + check(load(path) == proof, "Changed aggregate proof") + else: + save(path, proof) + print("Consolidation proof SHA-256: " + sha(path)) diff --git a/scripts/test-copper68k-cmpa-displacement-retirement.py b/scripts/test-copper68k-cmpa-displacement-retirement.py new file mode 100644 index 00000000..09d56966 --- /dev/null +++ b/scripts/test-copper68k-cmpa-displacement-retirement.py @@ -0,0 +1,78 @@ +"""Isolated CMPA word/full-address comparison replacement and retirement audit.""" +from pathlib import Path +import re +from copper68k_consolidation import Audit, check + +REPO = Path(__file__).resolve().parent.parent +GROUP = "cmpa-displacement-consolidation" + + +def mutate(path, text): + if path.endswith("M68kAdvancedTimingInterpreter.cs"): + start = text.index(" private void ExecuteCmpaWordAddressDisplacementToAddress(") + end = text.index(" private void ExecuteCmpaLongAddressDisplacementToAddress(", start) + block = text[start:end] + check(block.count("(int)(short)ReadWord") == 1, "Missing advanced mutation target") + return text[:start] + block.replace("(int)(short)ReadWord", "ReadWord") + text[end:] + old = "? M68kCpuState.SignExtend(value, M68kOperandSize.Word)" + start = text.index(" private bool DecodeArithmetic(ushort opcode)") + end = text.index("\n private ", start + 1) + block = text[start:end] + check(block.count(old) == 1, "Missing base mutation target") + return text[:start] + block.replace(old, "? (mode == 5 ? value & 0xffffu : M68kCpuState.SignExtend(value, M68kOperandSize.Word))") + text[end:] + + +def flags(destination, source, ccr): + result = (destination - source) & 0xffffffff + signed_destination = destination - 0x100000000 if destination & 0x80000000 else destination + signed_source = source - 0x100000000 if source & 0x80000000 else source + difference = signed_destination - signed_source + overflow = difference < -0x80000000 or difference > 0x7fffffff + return (ccr & 16) | (8 if result & 0x80000000 else 0) | (4 if result == 0 else 0) | (2 if overflow else 0) | (1 if destination < source else 0) + + +def report(model, mode): + combinations, failures = {}, [] + for supervisor in [False, True]: + key = f"{model}/CMPA/2/d16(A0)/r1/negative-d16-word-source-full-destination-all-CCR/brief/super={supervisor}" + passing = mismatching = 0 + for destination in [0xffff, 0xffffffff, 0, 0x7fffffff, 0x80000000]: + for source in [0xffff, 1, 0x8000, 0x7fff]: + for ccr in range(32): + rhs = source | 0xffff0000 if source & 0x8000 else source + expected = (0x2700 if supervisor else 0x700) | flags(destination, rhs, ccr) + actual = (0x2700 if supervisor else 0x700) | flags(destination, source if mode == "Mutation" else rhs, ccr) + if actual != expected: + mismatching += 1 + failures.append(dict(id=f"{key}/op=B2E8/s={source:08X}/d={destination:08X}/ccr={ccr:02X}", status="mismatching", + reason=f"SR expected {expected:04X}, actual {actual:04X}, mask=FFFF")) + else: + passing += 1 + combinations[key] = dict(passing=passing, mismatching=mismatching) if mismatching else dict(passing=passing) + return dict(schema=1, model=model, group=GROUP, + counts=dict(passing=1280 - len(failures), mismatching=len(failures), unsupported=0, untested=0), + logicalCases=1280, xunitBatches=1, combinations=combinations, failures=failures) + + +def witness_outcome(name): + return "Passed" if "destination: 0," in name else "Failed" + + +def witness_error(name, message): + expected, actual = (17, 20) if "destination: 65535," in name else (20, 24) + check(re.search(r"Expected:\s*" + str(expected) + r"\b", message) and re.search(r"Actual:\s*" + str(actual) + r"\b", message), "Wrong historical failure reason") + + +def make_audit(): + return Audit(dict(repo=REPO, producer=Path(__file__).resolve(), pin="4ee523df69688c2ded0b4d26567399dc96830ad7", + legacy="Copper68k.Tests/M68020AddressSourceTests.cs", legacy_class="Copper68k.Tests.M68020AddressSourceTests", legacy_cases=45, + witness="CmpaWordDisplacementComparesSignExtendedSourceAgainstFullAddress", witness_cases=3, + fixture="Copper68k.Tests/Synthetic/SyntheticCmpaDisplacementTests.cs", shared_class="Copper68k.Tests.Synthetic.SyntheticCmpaDisplacementTests", + method="WordSourceComparesAgainstFullAddressAndPreservesExtend", group=GROUP, + mutation_files=["Copper68k/M68kCore.cs", "Copper68k/M68kAdvancedTimingInterpreter.cs"], mutate=mutate, report=report, + witness_outcome=witness_outcome, witness_error=witness_error, + description="Prove CMPA displacement/full-address shared coverage; fresh output or strict replay.")) + + +if __name__ == "__main__": + make_audit().main() From b403a8ca121ef985e84d011e0195ac0ed932e437 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 23:28:41 +0300 Subject: [PATCH 130/211] Observe native 030 source-fault recovery before MOVE memory destinations --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 76 ++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 14 ++ .../m68030-memory-destination-recovery.cpp | 184 ++++++++++++++++++ ...per68k-030-memory-destination-reference.py | 135 +++++++++++++ 4 files changed, 409 insertions(+) create mode 100644 scripts/reference/m68030-memory-destination-recovery.cpp create mode 100644 scripts/test-copper68k-030-memory-destination-reference.py diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 77ddbfdb..dd6c3b1f 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -10229,3 +10229,79 @@ consumer proofs retain their original source scope and do not include this new group/retirement. The hardware-dependent disagreements and broader frame/ reference requirements remain open. No CPU import or package publication occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## Native source-read recovery before a memory destination — 2026-10-07 + +Source inspection of the latest private 020/030 operand-read candidate still +restricts simple MOVE continuation to register destinations. A memory-to-memory +source fault is therefore an open recovery requirement. The new maintained +native observer establishes the next software-reference fixture before changing +that private continuation path; no production CPU fix is claimed here. + +`scripts/test-copper68k-030-memory-destination-reference.py` and +`scripts/reference/m68030-memory-destination-recovery.cpp` execute unchanged +WinUAE 030 generated operations `op_2090_32_ff` / `op_2098_32_ff`, native fixup, +format-B construction/restoration and MMU retry-loop fragments at pin +`5d22d33632646efc3f747f03e82d28353e52722e`. Canonical opcodes select +`MOVE.L (A0),(A1)`, `(A0)+,(A1)`, `(A0),(A0)` and `(A0)+,(A0)`. +A source physical request at `4200` is rejected once before its successful read; +the real format-B handler/RTE path resumes execution. Direct controls omit that +fault. The handler preserves saved SR and every frame byte. + +**4,096 native programs pass**: 2,048 fault recoveries and 2,048 direct controls, +with four stack states, both source forms, separate/aliased destinations, four +source values and all 32 initial CCRs. Every fault has two source attempts and +one successful read; every control has one attempt/read. All have exactly one +destination write. The aliased postincrement destination is `4204`, evaluated +after A0 advances once; the ordinary alias writes `4200`, separate A1 writes +`4400`. MOVE's X preservation and N/Z/V/C result are captured before the +following MOVEQ changes flags. The next PC/following sentinel, active A7, +untouched registers, original source data and surrounding memory are checked. +The reference's successful data refill is not physically replayed by its +instruction retry. This does not authorize replay after partial side effects +in Copper68k. + +```powershell +python scripts/test-copper68k-030-memory-destination-reference.py --reference-directory --output artifacts/memory-destination-native-fresh +python scripts/test-copper68k-030-memory-destination-reference.py --reference-directory --output artifacts/memory-destination-native-fresh --validate-only +``` + +The command requires Windows/MSVC (Visual Studio 18 Community), Python 3 and +PowerShell 7. It reuses the maintained baseline's six canonical cases and exact +function extraction. Inputs, all four producer identities, extracted native +fragments, fixture, executable, logs and trace are bound. Strict replay +reexecutes the frozen observer and requires byte-identical rows and trace. +Missing/changed reference inputs, empty/incomplete selection, wrong read/write +provenance, outcomes or saved evidence fail. The restored pristine source is +`reference/WinUaeSourceV2` in the restoration temporary root, outside the deleted +artifacts checkout. + +Three sole observer mutations compile and fail precisely without any native +fragment change: an extra actual source read, a duplicate physical destination +write and writing an aliased postincrement result to the original address. +The first two fail case 0; the alias defect retains 768 earlier controls then +fails case 768. Seven evidence controls reject missing fixture, wrong producer, +missing output identity, altered fixture with updated manifest, altered native +function with updated manifest, empty output with updated manifest and altered +verification. These test observers/evidence handling, not processor timing. + +Evidence: `reference/MemoryDestination030V2`, +`mutations/MemoryDestinationNativeV2` and `integrity/MemoryDestinationNativeV2` +in the restoration temporary root. `verification.json` SHA-256: +`6a5cd149439268a4fe1b3a2158cc0b35e64e41f470b89d4fc8b8c496b7f68b87`; +`memory-destination-native-proof-v2.json` SHA-256: +`f28256faa078c5b5fb204da366004750692dee9cfef32387bacb2a81ecf186b6`. +V1 passed the same cases with adapter signedness warnings; V2 corrects those +adapter types and passes fresh generation/replay. Warnings in untouched +extracted native code remain separately recorded. + +Scope is **030 software observation**, with flat physical FC1/5 transport, +aligned long accesses, unchanged S/M and no trace/IRQ/cache/translation. It is +not a full exception/pipeline/hardware oracle or architectural promotion. +Byte/word destinations, other destination EAs, destination-write faults, +handler-edited registers/S/M, indexed sources and other processor models remain +required and unqualified by this observer. The private candidate still needs a +bounded failing test and suffix-only implementation for the newly observed +memory destination, followed by retention/full integration and consumers before +any import. No CPU source/package/regression retirement changes. Milestone 6 +remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 78fc8cda..1222b08c 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3334,3 +3334,17 @@ Fresh end-to-end reproduction and strict replay pass. No production CPU change or publication occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [CMPA replacement proof](COPPER68K_REFERENCE_QUALIFICATION.md#cmpa-displacement-sign-extension-consolidation--2026-10-07). + +Independent native 030 source-fault recovery now covers a memory destination: +4,096 programs pass (2,048 recoveries / 2,048 controls), indirect/postincrement +A0 source, separate A1 or aliased A0 destination, four stack states, four values +and all CCRs. Exact source attempts, one successful read/write, postincrement +alias address, pre-sentinel MOVE flags, untouched state/memory and frame +preservation are checked. Frozen native replay matches; three sole observer +mutations and seven corrupted-evidence controls are rejected precisely. +This is scoped software reference evidence, not hardware/trace/translation or +architectural promotion. The private 020/030 candidate still excludes memory +destinations; implementation and broader model/EA/fault coverage stay required. +No production CPU/package change occurs. Milestone 6 remains **in progress**, +`roadmapComplete=false`. See the +[native memory-destination evidence and scope](COPPER68K_REFERENCE_QUALIFICATION.md#native-source-read-recovery-before-a-memory-destination--2026-10-07). diff --git a/scripts/reference/m68030-memory-destination-recovery.cpp b/scripts/reference/m68030-memory-destination-recovery.cpp new file mode 100644 index 00000000..72dfa31e --- /dev/null +++ b/scripts/reference/m68030-memory-destination-recovery.cpp @@ -0,0 +1,184 @@ +// Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT +// 030 software discovery: a source-read fault before MOVE.L to memory. +// Unchanged generated operations, fixup helpers and retry loop; flat physical +// transport. Observe indirect/postincrement source and aliased destinations. +// Trace is disabled. No enabled translation, IRQ, cache/pipeline timing, +// physical function-code spaces or architectural changed-bank qualification. +#include +#include +#include +#include +#include +#include +#include +#include +#include +using uae_u8=uint8_t;using uae_u16=uint16_t;using uae_u32=uint32_t; +using uae_s8=int8_t;using uae_s16=int16_t;using uae_s32=int32_t;using uaecptr=uint32_t; +#define REGPARAM2 +#define STATIC_INLINE static inline +#define CYCLE_UNIT 2 +#define _T(x) x +#define MMUDEBUG 0 +#define TRY(x) try +#define CATCH(x) catch(int x) +#define ENDTRY +#define STOPTRY +#define THROW(x) throw int(x) +#define SPCFLAG_TRACE 0x40 +#define SPCFLAG_DOTRACE 0x80 +#define SPCFLAG_TRAP 0x400 +#define SPCFLAG_INT 8 +#define SPCFLAG_DOINT 0x100 +#define CPU_HALT_CPU_STUCK 1 +#define CPU_HALT_DOUBLE_FAULT 2 +#define sz_byte 0 +#define sz_word 1 +#define sz_long 2 +#define m68k_areg(r,n) ((r).a[n]) +#define m68k_dreg(r,n) ((r).d[n]) +struct Regs { + uae_u32 a[8]{},d[8]{},usp{},isp{},msp{},pc{},instruction_pc{},trace_pc{},vbr{},mmu_ssw{},mmu_fault_addr{},sfc{},dfc{},wb3_data{},wb2_address{},fp_ea{},fp_opword{},mmu_effective_addr{},pcr{},fp_unimp_ins{},ir{}; + uae_u16 sr{};int m{},s{},t0{},t1{},intmask{},ipl[1]{},ipl_pin{},stopped{},spcflags{},opcode{},irc{},instruction_cnt{},wb2_status{},wb3_status{},read_buffer{},write_buffer{},fpu_exp_pre{},fp_unimp_pend{}; + int prefetch020_valid[3]{},prefetch020[3]{},pipeline_r8[2]{},pipeline_pos{},pipeline_stop{}; +}regs; +struct Prefs{int cpu_model=68030,mmu_model=68030,cachesize{},cpu_compatible{},cpu_cycle_exact{},fpu_model{};}currprefs; +struct flag_struct{int cznv{},x{};}regflags; +int xflag,nflag,zflag,vflag,cflag; +#define SET_XFLG(x) (xflag=(x)) +#define SET_NFLG(x) (nflag=(x)) +#define SET_ZFLG(x) (zflag=(x)) +#define SET_VFLG(x) (vflag=(x)) +#define SET_CFLG(x) (cflag=(x)) +#define GET_XFLG() xflag +#define GET_NFLG() nflag +#define GET_ZFLG() zflag +#define GET_VFLG() vflag +#define GET_CFLG() cflag +#define CLEAR_CZNV() (cflag=zflag=nflag=vflag=0) +bool m68k_accurate_ipl=false,mmu_debugger=false,ismoves030=false,islrmw030=false,bBusErrorReadWrite=false,mmu030_retry=false; +int mmu030_idx,mmu030_idx_done,mmu030_opcode,mmu030_opcode_stageb,mmu030_fake_prefetch,cpu_cycles; +uaecptr mmu030_fake_prefetch_addr,mm030_stageb_address,last_fault_for_exception_3; +uae_u16 mmu030_state[3];uae_u32 mmu030_data_buffer_out,mmu030_disp_store[2],mmu030_fmovem_store[2],mmu040_move16[4]; +struct mmu030_access{uae_u32 val;};mmu030_access mmu030_ad[10]; +struct mmufixup{int reg=-1;uae_u32 value{};};mmufixup mmufixup[2]; +std::unordered_map mem; +std::vector events;std::vector originalFrame,editedFrame; +bool operandContext=false;bool faultOnce;int attempts,reads,steps,traceCount;uae_u32 tracePc,traceD7;int returnT1; +int activeCase=-1; +int writes,moveCcr;uaecptr writeAddress;uae_u32 writeValue; +std::ofstream diagnostics("trace.log"); +void snapshot(const char* phase){diagnostics<second;}return v; +} +void put(uaecptr a,uae_u32 v,int w){for(int n=0;n>(8*(w-1-n)));} +uae_u32 operand_read(uaecptr a){if(a!=0x4200)unavailable();attempts++;if(faultOnce){faultOnce=false;events.push_back(1);mmu030_hardware_bus_error(a,0,true,false,sz_long);}reads++;events.push_back(4);return read(a,4);} +uae_u32 uae_mmu030_get_long(uaecptr a){return operandContext?operand_read(a):read(a,4);}uae_u32 uae_mmu030_get_word(uaecptr a){return read(a,2);}uae_u32 uae_mmu030_get_iword(uaecptr a){return read(a,2);} +void uae_mmu030_put_word(uaecptr a,uae_u32 v){put(a,v,2);} +void uae_mmu030_put_long(uaecptr a,uae_u32 v){ + if(!operandContext||++writes!=1)unavailable(); + writeAddress=a;writeValue=v;events.push_back(8);put(a,v,4); +} +uae_u32 get_word_mmu030(uaecptr a){return read(a,2);}uae_u32 get_long_mmu030(uaecptr a){return read(a,4);} +uae_u32 x_get_long(uaecptr a){return read(a,4);}void x_put_long(uaecptr a,uae_u32 v){put(a,v,4);}void x_put_word(uaecptr a,uae_u32 v){put(a,v,2);} +uae_u32 uae_mmu030_get_long_fcx(uaecptr a,int fc){if(fc!=1&&fc!=5)unavailable();return a==0x4200?operand_read(a):read(a,4);} +uae_u32 uae_mmu030_get_word_fcx(uaecptr,int){unavailable();}uae_u32 uae_mmu030_get_byte_fcx(uaecptr,int){unavailable();} +void uae_mmu030_put_long_fcx(uaecptr,uae_u32,int){unavailable();}void uae_mmu030_put_word_fcx(uaecptr,uae_u32,int){unavailable();}void uae_mmu030_put_byte_fcx(uaecptr,uae_u32,int){unavailable();} +uae_u32 mmu030_get_generic(uaecptr,uae_u32,int,int){unavailable();}void mmu030_put_generic(uaecptr,uae_u32,uae_u32,int,int){unavailable();} +templatevoid mmu030_unaligned_read_continue(uaecptr,int,F){unavailable();}templatevoid mmu030_unaligned_write_continue(uaecptr,int,F){unavailable();} +void unalign_clear(){unavailable();}void exception3_read_prefetch(uae_u32,uaecptr){unavailable();} +void exception3_read_special(uae_u32,uaecptr,int,int){unavailable();} +void write_log(const char* format,...){char buffer[512];va_list args;va_start(args,format);vsnprintf(buffer,sizeof(buffer),format,args);va_end(args);diagnostics<32)throw std::runtime_error("Bound exceeded");return read(regs.pc+n,2);} +int do_specialties(int){ + const int spcflags=regs.spcflags; + if(spcflags&~(SPCFLAG_TRACE|SPCFLAG_DOTRACE))unavailable(); + // Exact trace fragment of reference do_specialties; IRQ/host branches excluded. +#include "special-trace.inc" + return regs.pc==0x6000||regs.pc==0x1006; +} +uae_u32 dispatch(uae_u32 op){ + if(op==0x3ebc){events.push_back(3);auto v=op_30bc_32_ff(op);editedFrame.clear();for(int n=0;n<92;n++)editedFrame.push_back(mem.at(regs.a[7]+n));return v;} + if(op==0x4e73){events.push_back(5);snapshot("before-RTE");auto value=op_4e73_32_ff(op);snapshot("after-RTE");return value;} + if((op&0xfdff)==0x2090||(op&0xfdff)==0x2098){ + snapshot("before-MOVE");operandContext=true; + try{auto value=(op&8)?op_2098_32_ff(op):op_2090_32_ff(op);operandContext=false; + moveCcr=(GET_XFLG()?16:0)|(GET_NFLG()?8:0)|(GET_ZFLG()?4:0)|(GET_VFLG()?2:0)|(GET_CFLG()?1:0); + snapshot("after-MOVE");return value; + }catch(...){operandContext=false;throw;} + } + if(op==0x7e2a){events.push_back(6);return op_7000_32_ff(op);} + if(op==0x4e71)return op_4e71_32_ff(op); + unavailable(); +} +#include "loop.inc" + +int main(){try{ + int id=0;const uae_u32 values[]={0,0x80818283,0x7fffffff,0xffffffff}; + for(int bank=0;bank<4;bank++)for(int post=0;post<2;post++)for(int alias=0;alias<2;alias++) + for(int fault=0;fault<2;fault++)for(auto value:values)for(int ccr=0;ccr<32;ccr++,id++){activeCase=id; + const int initial=bank,returned=bank; + regs={};mem.clear();events.clear();originalFrame.clear();editedFrame.clear();faultOnce=fault;attempts=reads=steps=traceCount=0;tracePc=traceD7=0;regflags={};xflag=nflag=zflag=vflag=cflag=0; + mmu030_state[0]=mmu030_state[1]=mmu030_state[2]=0;mmu030_idx=mmu030_idx_done=0;mmu030_retry=false;mmu030_data_buffer_out=0;mmufixup[0].reg=mmufixup[1].reg=-1; + writes=moveCcr=0;writeAddress=writeValue=0; + xflag=(ccr>>4)&1;nflag=(ccr>>3)&1;zflag=(ccr>>2)&1;vflag=(ccr>>1)&1;cflag=ccr&1; + regs.usp=0x7000;regs.isp=0x8000;regs.msp=0x9000; + regs.s=initial>=2;regs.m=initial==1||initial==3;regs.intmask=7; + regs.a[7]=initial<2?0x7000:initial==2?0x8000:0x9000; + regs.a[0]=0x4200;regs.a[1]=0x4400;regs.d[0]=0x12345678;regs.pc=regs.instruction_pc=0x1000; + const auto opcode=0x2090|(post?8:0)|(alias?0:0x200); + put(0x1000,opcode,2);put(0x1002,0x7e2a,2);put(0x1004,0x4e71,2); + for(int n=-8;n<16;n++){put(0x4200+n,0xa5,1);put(0x4400+n,0xa5,1);} + put(0x4200,value,4);put(8,0x5000,4);put(36,0x6000,4); + put(0x5000,0x3ebc,2);put(0x5002,0x700|(returned>=2?0x2000:0)|((returned==1||returned==3)?0x1000:0)|ccr,2);put(0x5004,0x4e73,2); + for(auto& p:cpufunctbl)p=dispatch; + try{m68k_run_mmu030();}catch(const Done&){} + const uaecptr destination=alias?0x4200u+(post?4u:0u):0x4400u; + const int expectedCcr=(ccr&16)|(value==0?4:0)|(value&0x80000000?8:0); + if(reads!=1||attempts!=(fault?2:1)||writes!=1||writeAddress!=destination||writeValue!=value||read(destination,4)!=value||moveCcr!=expectedCcr||traceCount!=0||regs.pc!=0x1006||regs.d[7]!=42||regs.d[0]!=0x12345678||regs.a[0]!=(post?0x4204u:0x4200u)||regs.a[1]!=0x4400||regs.a[7]!=(bank<2?0x7000u:bank==2?0x8000u:0x9000u))throw std::runtime_error("Missing source-read/memory-suffix recovery: id="+std::to_string(id)); + if(read(0x4200,4)!=value)throw std::runtime_error("Source data changed"); + for(int n=1;n<7;n++)if(regs.d[n]!=0)throw std::runtime_error("Untouched data register changed"); + for(int n=2;n<7;n++)if(regs.a[n]!=0)throw std::runtime_error("Untouched address register changed"); + for(int n=-8;n<16;n++)for(auto base:{0x4200u,0x4400u}){ + uaecptr a=base+static_cast(n);if(a>=destination&&a=0x4200&&a<0x4204)continue; + if(read(a,1)!=0xa5)throw std::runtime_error("Surrounding memory changed"); + } + if(fault){ + if(originalFrame.size()!=92||editedFrame.size()!=92||originalFrame[6]!=0xb0||originalFrame[7]!=8)throw std::runtime_error("Missing reference format-B frame"); + for(int n=0;n<92;n++)if(originalFrame[n]!=editedFrame[n])throw std::runtime_error("Handler changed source-fault frame"); + }else if(!originalFrame.empty()||!editedFrame.empty())throw std::runtime_error("Unexpected control bus fault"); + std::cout<<"id="<nul\r\nif errorlevel 1 exit /b %errorlevel%\r\ncl /nologo /O2 /EHsc /W4 observer.cpp /Fe:observer.exe\r\nexit /b %errorlevel%\r\n' + (native / "build.cmd").write_text(command, encoding="utf-8") + run([os.environ.get("COMSPEC", "cmd.exe"), "/d", "/c", "build.cmd"], native, native / "build.log") + with (native / "run.log").open("w", encoding="utf-8") as stdout, (native / "errors.log").open("w", encoding="utf-8") as stderr: + result = subprocess.run([str(native / "observer.exe")], cwd=native, stdout=stdout, stderr=stderr) + check(result.returncode == 0, "Native source/destination observer failed; inspect errors.log") + save(output / "identities.json", dict(schema=1, referenceCommit=PIN, producers=producers, referenceInputs=inputs, + outputs={name: sha(native / name) for name in sorted(files)})) + identity = load(output / "identities.json") + check(identity["schema"] == 1 and identity["referenceCommit"] == PIN and identity["producers"] == producers and identity["referenceInputs"] == inputs, "Wrong producer/input identity") + check(set(identity["outputs"]) == files, "Missing native output identity") + for name, digest in identity["outputs"].items(): + check(sha(native / name) == digest, "Changed native output: " + name) + check(sha(native / "observer.cpp") == sha(FIXTURE), "Changed observer fixture") + for name, text in expected.items(): + check((native / name).read_text(encoding="utf-8-sig") == text, "Changed extracted native function: " + name) + if validate_only: + replay = output / ("replay-" + uuid.uuid4().hex) + replay.mkdir() + with (replay / "run.log").open("w", encoding="utf-8") as stdout, (replay / "errors.log").open("w", encoding="utf-8") as stderr: + result = subprocess.run([str(native / "observer.exe")], cwd=replay, stdout=stdout, stderr=stderr) + check(result.returncode == 0 and (replay / "errors.log").stat().st_size == 0 and + all(sha(replay / name) == sha(native / name) for name in ["run.log", "trace.log"]), "Executed native replay differs") + result = observations(native) + result["identitySha256"] = sha(output / "identities.json") + if validate_only: + check(load(output / "verification.json") == result, "Changed verification record") + else: + save(output / "verification.json", result) + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--reference-directory", type=Path, required=True) + parser.add_argument("--validate-only", action="store_true") + args = parser.parse_args() + audit(args.output.resolve(), args.reference_directory.resolve(), args.validate_only) + print("4,096 native memory-destination cases; 2,048 recoveries / 2,048 controls, no source replay or duplicate writes. No architectural promotion.") + + +if __name__ == "__main__": + try: + main() + except (ValueError, OSError, KeyError, subprocess.SubprocessError) as error: + print(str(error)) + raise SystemExit(1) From b8e85ae77eada9a3dad253e88690af56d6c41923 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Wed, 7 Oct 2026 23:47:38 +0300 Subject: [PATCH 131/211] Qualify private MOVE memory-destination read recovery --- ...ticM68020MemoryDestinationRecoveryTests.cs | 135 ++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 85 ++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 ++ ...t-copper68k-memory-destination-recovery.py | 244 ++++++++++++++++++ 4 files changed, 479 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020MemoryDestinationRecoveryTests.cs create mode 100644 scripts/test-copper68k-memory-destination-recovery.py diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020MemoryDestinationRecoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020MemoryDestinationRecoveryTests.cs new file mode 100644 index 00000000..dbdd5053 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020MemoryDestinationRecoveryTests.cs @@ -0,0 +1,135 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Literal MOVE.L encodings, independently chosen addresses and CCR expectations. +// Native 030 observer: scripts/reference/m68030-memory-destination-recovery.cpp. +// The physical map rejects a whole logical request, not a partial silicon cycle. +public sealed class SyntheticM68020MemoryDestinationRecoveryTests(ITestOutputHelper output) +{ + private static readonly ModelSpec[] Models = ModelSpec.All.Where(x => x.Id is "68EC020" or "A1200" or "68020" or "68030").ToArray(); + private static readonly string[] Banks = ["user", "user-M", "ISP", "MSP"]; + private static readonly uint[] Values = [0, 0x80818283, 0x7fffffff, 0xffffffff]; + private static readonly (ushort Opcode, bool Post, bool Alias)[] Forms = + [(0x2290, false, false), (0x2298, true, false), (0x2090, false, true), (0x2098, true, true)]; + + [Theory, Trait("Suite", "ReferenceDiscovery")] + [InlineData(false)] + [InlineData(true)] + public void DirectMemoryDestinationControls(bool batch) => Audit(batch, false); + + [EnvironmentFact("COPPER68K_RUN_020_MEMORY_DESTINATION_RECOVERY", "require isolated source-read recovery candidate"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarMemoryDestinationRecovery() => Audit(false, true); + + [EnvironmentFact("COPPER68K_RUN_020_MEMORY_DESTINATION_RECOVERY", "require isolated source-read recovery candidate"), Trait("Suite", "ReferenceDiscovery")] + public void BatchMemoryDestinationRecovery() => Audit(true, true); + + private void Audit(bool batch, bool fault) + { + var failures = new List(); + foreach (var model in Models) + { + var bus = new FaultBus(); + var m = new SyntheticMachine(model, bus); + var report = new CoverageBatch(model.Id, $"memory-destination-{(fault ? "recovery" : "controls")}-{(batch ? "batch" : "scalar")}"); + foreach (var form in Forms) + foreach (var bank in Banks) + foreach (var value in Values) + for (var ccr = 0; ccr < 32; ccr++) + foreach (var lane in fault ? Enumerable.Range(0, 4) : new[] { 0 }) + { + var id = $"{model.Id}/MOVE.L/source={(form.Post ? "post" : "indirect")}/dest={(form.Alias ? "alias-A0" : "A1")}/bank={bank}/value={value:X8}/ccr={ccr:X2}/lane={lane}"; + try + { + bus.Disarm(); m.Reset(ccr); + _ = SyntheticExecution.Prepare(m, [form.Opcode, 0x7e2a, 0x4e71]); + // Guards include overlapping source/destination bytes. + for (uint address = 0x41f8; address < 0x4210; address++) m.InitializePhysical(address, 0xa5, 1); + for (uint address = 0x43f8; address < 0x4410; address++) m.InitializePhysical(address, 0xa5, 1); + m.InitializePhysical(0x4200, value, 4); + m.InitializePhysical(0x9020, 0x4e73, 2); m.InitializePhysical(0x9022, 0x4e71, 2); + m.Core.State.A[0] = 0x4200; m.Core.State.A[1] = 0x4400; + m.Core.State.SetUserStackPointer(0x7800); m.Core.State.SetInterruptStackPointer(0x4700); m.Core.State.SetMasterStackPointer(0x7400); + var sr = (ushort)(0x700 | ccr | (bank is "ISP" or "MSP" ? 0x2000 : 0) | (bank is "user-M" or "MSP" ? 0x1000 : 0)); + m.Core.State.StatusRegister = sr; + m.Core.State.SetActiveStackPointer(bank == "MSP" ? 0x7400u : bank == "ISP" ? 0x4700u : 0x7800u); + var e = ArchitecturalExpectation.Capture(m); + var serial = m.Core.State.ExceptionSequence; + bus.Accesses.Clear(); + if (fault) bus.Arm(0x4200u + (uint)lane); + Step(m, batch); + if (fault) + { + var frame = (bank is "user-M" or "MSP" ? 0x7400u : 0x4700u) - 92; + var ssw = 0x140u | ((sr & 0x2000) != 0 ? 5u : 1u); + if (bus.Rejected != 1 || m.Core.State.ExceptionSequence != serial + 1 || m.Core.State.LastExceptionVector != 2 || + m.Core.State.ProgramCounter != 0x9020 || m.Core.State.A[7] != frame || m.Core.State.Halted || m.Core.State.Stopped || + m.PeekPhysical(frame + 6, 2) != 0xb008 || (m.PeekPhysical(frame + 10, 2) & 0x1f7) != ssw || m.PeekPhysical(frame + 16, 4) != 0x4200) + throw new InvalidOperationException("Source-fault frame/SSW/vector differs"); + if (bus.Accesses.Any(a => a.Address is >= 0x41f8 and < 0x4410 && (a.Write || a.Kind == M68kBusAccessKind.CpuDataRead)) || + m.Core.State.A[0] != 0x4200 || m.Core.State.A[1] != 0x4400 || m.Core.State.D[7] != e.D[7]) + throw new InvalidOperationException("Source fault prematurely completed an operand effect"); + // Opaque private frame preservation; no invented silicon fields. + for (uint n = 0; n < 92; n++) e.Memory[model.Physical(frame + n)] = bus.Peek(model.Physical(frame + n)); + e.ExceptionVector = 2; + Step(m, batch); + } + var destination = form.Alias ? (form.Post ? 0x4204u : 0x4200u) : 0x4400u; + if (form.Post) e.A[0] = 0x4204; + e.Write(destination, value, 4, model); + e.Pc = 0x1002; + e.Sr = (ushort)((sr & 0xfff0) | (value == 0 ? 4 : (value & 0x80000000) != 0 ? 8 : 0)); + Check(m, e); + // Existing approximate execution policy, not physical cycle accuracy. + var plan = ((M68kAdvancedTimingInterpreter)m.Core).Timing.LastInstructionTiming.Plan; + var key = form.Post ? M68kInstructionTimingKey.MoveLongPostIncrementToAddressIndirect : M68kInstructionTimingKey.MoveLongAddressIndirectToAddressIndirect; + if (plan.Key != key || plan.NativeCycles != 8 || + plan.Barriers != M68kTimingBarrier.None || plan.UsesHeadTail != (model.Id == "68030") || + plan.HeadCycles != (model.Id == "68030" ? 1 : 0) || plan.TailCycles != (model.Id == "68030" ? 1 : 0)) + throw new InvalidOperationException($"Memory destination changed the existing MOVE timing policy: {plan.Key}/{plan.NativeCycles}/{plan.UsesHeadTail}/{plan.HeadCycles}/{plan.TailCycles}/{plan.Barriers}"); + if (bus.Rejected != (fault ? 1 : 0) || m.Core.State.ExceptionSequence != serial + (fault ? 1 : 0)) + throw new InvalidOperationException("Source was rejected again during recovery"); + var transfers = bus.Accesses.Where(a => a.Address is >= 0x41f8 and < 0x4410 && + a.Kind is M68kBusAccessKind.CpuDataRead or M68kBusAccessKind.CpuDataWrite).ToArray(); + if (transfers.Length != 2 || transfers[0].Write || transfers[0].Address != 0x4200 || transfers[0].Width != 4 || + !transfers[1].Write || transfers[1].Address != destination || transfers[1].Width != 4 || transfers[1].Value != value) + throw new InvalidOperationException("Source read/destination write address, order, width or count differs"); + if (bus.Accesses.Count(a => !a.Write && a.Kind == M68kBusAccessKind.CpuInstructionFetch && a.Address == 0x1000) > 1) + throw new InvalidOperationException("Recovery replayed the MOVE opcode fetch"); + e.D[7] = 42; e.Pc = 0x1004; e.Sr = (ushort)(e.Sr & 0xfff0); + Step(m, batch); Check(m, e); + report.Record(id, "passing", null); + } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException) { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + try { report.Complete(output); } catch (Exception ex) { failures.Add(ex); } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(x => x.Message))); + } + + private static void Check(SyntheticMachine m, ArchitecturalExpectation e) + { var error = e.Verify(m); if (error != null) throw new InvalidOperationException(error); } + private static void Step(SyntheticMachine m, bool batch) + { + if (!batch) { m.Core.ExecuteInstruction(); return; } + var boundary = new Boundary(); + if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, boundary) != 1 || boundary.Before != 1 || boundary.After != 1) + throw new InvalidOperationException("Batch boundary count differs"); + } + private sealed class Boundary : IM68kInstructionBoundary + { public int Before, After; public bool BeforeInstruction() { Before++; return true; } public void AfterInstruction(long a, long b) => After++; } + private sealed class FaultBus : SparseRecordingBus, IM68kPhysicalAddressMap + { + private uint? denied; + internal int Rejected { get; private set; } + internal void Disarm() { denied = null; Rejected = 0; } + internal void Arm(uint address) => denied = address; + public bool IsCpuPhysicalAddressMapped(uint address, int size, M68kBusAccessKind kind) + { + if (denied is not { } at || kind != M68kBusAccessKind.CpuDataRead || size != 4 || unchecked(at - address) >= 4) return true; + denied = null; Rejected++; return false; + } + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index dd6c3b1f..fcdd039b 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -10305,3 +10305,88 @@ bounded failing test and suffix-only implementation for the newly observed memory destination, followed by retention/full integration and consumers before any import. No CPU source/package/regression retirement changes. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +### Private memory-destination read-recovery suffix — 2026-10-07 + +The bounded continuation following the native observation is implemented only +in a fresh private snapshot. Production CPU source and packages remain unchanged. +`SyntheticM68020MemoryDestinationRecoveryTests` uses literal `2290`, `2298`, +`2090`, `2098` encodings: MOVE.L from indirect/postincrement A0 to indirect A1 +or A0. Independent addresses are source `4200`, separate destination `4400`, +postincrement alias destination `4204`, and indirect alias destination `4200`. +Four values, every initial CCR, four unchanged stack states, scalar/batch +execution and all four rejected logical-request lanes are selected. + +The original private CPU produces exactly **65,536 unsupported recoveries** +and retains **16,384 passing direct controls**. The candidate completes all +**81,920 scenarios**, zero mismatches/unsupported/untested selected cases, plus +**61,440 existing register-destination continuation/control scenarios**. +Each recovery completes only its pending read, source update, destination write +and flags. It never restarts the instruction or recomputes the source EA. +The test verifies full registers/defined SR, stack banks, next PC, one successful +source read, one destination write in order, no opcode replay, untouched memory +and guards, opaque frame retention and a following MOVEQ sentinel. + +Existing timing policies are kept separately: both selected forms have eight +native policy cycles, with their original specialized timing keys; 030 uses +head/tail 1/1 and 020/EC020 flat timing. These are execution-policy checks, not +physical timing qualification. The first two exploratory snapshots had an +incorrect general-MOVE timing expectation. V3 corrected the expectation but +its verifier also counted aggregate TRX stdout, rejecting duplicate summaries. +V4 reads only per-test stdout and passes the exact deterministic audit. Those +failed explorations remain recorded, not credited as passing qualification. + +Four sole private CPU mutations are detected: + +| Defect | Mismatching new cases | Retained new controls | +| --- | ---: | ---: | +| Repeat completed source read | 65,536 | 16,384 | +| Write postincrement alias at the old source address | 16,384 | 65,536 | +| Preserve old flags instead of completing MOVE flags | 61,440 | 20,480 | +| Substitute general six-cycle flat suffix timing | 65,536 | 16,384 | + +The maintained command requires the frozen 230-file private parent, pins its +manifest and CPU file, copies all inputs, and applies the bounded patch only +inside the new output directory. It checks exact complete source inventories, +both producer identities, command selections, TRX test rosters/counters/stdout, +every coverage key and weight, failures and output identities. Empty/missing +inputs and mismatches fail the audit. No pack/publication/import occurs. + +```powershell +python scripts/test-copper68k-memory-destination-recovery.py ` + --parent-directory /audits/Operand020CombinedRefaultV1 ` + --output +python scripts/test-copper68k-memory-destination-recovery.py ` + --parent-directory /audits/Operand020CombinedRefaultV1 ` + --output --validate-only +``` + +The private parent manifest SHA-256 is +`943288fe579f746468f9df7ecdfd11833e2862e9db058039476aa8768aa61a9a`; +its operand continuation CPU file is +`b93d2ef4b2801b544313c1efad29d4cc47254f7a38897ee1a7b61a3f7e556a0d`. +Evidence `audits/MemoryDestinationPrivateV4/proof.json` SHA-256: +`003bb1a07053f298c395e143afc93d725926bf11712cfb19f2bfac99ac3488fc`. +Eleven independent copied-evidence controls reject wrong producer, missing +fixture/output identity, empty selection, wrong weight, missing witness, wrong +mutation cause/command/TRX counters, changed mutation with an updated manifest, +and changed aggregate verification. Original evidence then revalidates. +`memory-destination-private-controls-v4.json` SHA-256: +`1c7517d6226964d4aac645ad1d56cd9485ac74d61b42e59f5a8ffe3e1ef99335`. +Separate production-source direct coverage passes 16,384 scenarios / two +batches, with two private recovery batches explicitly unavailable; its isolated +verification SHA-256 is +`5a429f1c5bea80f25b0231adcaedd666088fbe870035a6d97241d6f2bc9e28d3`. + +Native agreement is scoped to the prior **030 software observation**. The +020/EC020/A1200 runs establish private software behavior and retention; they do +not add an independent processor reference. This addition is aligned long +source-read recovery with unchanged handler registers/S/M, no trace/interrupt, +and a mapped indirect memory destination. Byte/word, other source/destination +EAs, A7/changed-bank cases, indexed/pointer sources, destination-write faults, +handler modifications and general fault origins remain required. The earlier +complete CPU/combined/consumer evidence has its original source identity and +does not qualify this new candidate. Full integration and consumers are still +required before import. Hardware is unavailable; the trace disagreements remain +open. No regression retirement or public release occurs. Milestone 6 remains +**in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 1222b08c..192e7d2f 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3348,3 +3348,18 @@ destinations; implementation and broader model/EA/fault coverage stay required. No production CPU/package change occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [native memory-destination evidence and scope](COPPER68K_REFERENCE_QUALIFICATION.md#native-source-read-recovery-before-a-memory-destination--2026-10-07). + +The observed indirect-memory MOVE.L destination now has a bounded private +source-read recovery implementation. Original private source rejects 65,536 +recoveries while retaining 16,384 direct controls; the isolated suffix passes +all 81,920 new scenarios and 61,440 existing register-destination scenarios. +Read replay, alias address, flag and timing mutations fail precisely; eleven +evidence-corruption controls are rejected and original evidence revalidates. Production +direct coverage passes separately; two optional private recovery batches remain +unavailable there. The maintained command pins the complete private parent and +verifies exact inputs, rosters, coverage keys/weights and outcomes. Existing +specialized timing policies and operand order are preserved. Broader fault/EA, +trace/hardware, full integration and consumer gates remain open before any CPU +import. No production CPU/package change occurs. Milestone 6 remains **in +progress**, `roadmapComplete=false`. See the +[private memory-destination suffix and evidence](COPPER68K_REFERENCE_QUALIFICATION.md#private-memory-destination-read-recovery-suffix--2026-10-07). diff --git a/scripts/test-copper68k-memory-destination-recovery.py b/scripts/test-copper68k-memory-destination-recovery.py new file mode 100644 index 00000000..d1bb6e93 --- /dev/null +++ b/scripts/test-copper68k-memory-destination-recovery.py @@ -0,0 +1,244 @@ +"""Isolated MOVE.L source-fault suffix audit; never edits or packs production CPU. + +Requires the frozen 230-file Operand020CombinedRefaultV1 private parent. +The candidate patch below is development evidence, not a production import. +""" +from pathlib import Path +import argparse +import hashlib +import itertools +import json +import os +import re +import shutil +import subprocess +import xml.etree.ElementTree as ET + +REPO = Path(__file__).resolve().parents[1] +FIXTURE = Path("Copper68k.Tests/Synthetic/SyntheticM68020MemoryDestinationRecoveryTests.cs") +CPU = Path("Copper68k/M68kAdvancedTimingInterpreter.Operand020.cs") +PARENT_MANIFEST = "943288fe579f746468f9df7ecdfd11833e2862e9db058039476aa8768aa61a9a" +PARENT_CPU = "b93d2ef4b2801b544313c1efad29d4cc47254f7a38897ee1a7b61a3f7e556a0d" +MODELS = ["68EC020", "68020", "68030", "A1200"] +NS = {"t": "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"} +FILTER = "FullyQualifiedName~SyntheticM68020MemoryDestinationRecoveryTests" +RETENTION = "FullyQualifiedName~SyntheticM68020OperandReadContinuationTests" +VARIANTS = ["baseline", "candidate", "replay", "alias", "flags", "timing"] + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def load(path): + return json.loads(path.read_text(encoding="utf-8-sig")) + + +def save(path, value): + path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8") + + +def require(condition, message): + if not condition: + raise ValueError(message) + + +def inventory(root): + return {p.relative_to(root).as_posix(): sha(p) for p in sorted(root.rglob("*")) + if p.suffix in (".cs", ".csproj") and not {"bin", "obj"}.intersection(p.parts)} + + +def replace_once(text, old, new): + require(text.count(old) == 1, "Nonunique candidate patch witness: " + old[:70]) + return text.replace(old, new) + + +def candidate(original, variant): + if variant == "baseline": + return original + text = replace_once(original, + "var moveSource = top is >= 1 and <= 3 && destination <= 1 && !(top == 1 && destination == 1) &&", + "var memoryDestination = top == 2 && destination == 2 && mode is 2 or 3;\n" + " var moveSource = top is >= 1 and <= 3 && (destination <= 1 || memoryDestination) && !(top == 1 && destination == 1) &&") + text = replace_once(text, + "kind == PendingReadKind020.MoveSource && (top is < 1 or > 3 || destinationMode > 1 || top == 1 && destinationMode == 1 ||", + "kind == PendingReadKind020.MoveSource && (top is < 1 or > 3 ||\n" + " destinationMode > 1 && !(top == 2 && destinationMode == 2 && mode is 2 or 3) || top == 1 && destinationMode == 1 ||") + text = replace_once(text, + "else WriteGeneralRegister(true, destination, expectedWidth == 2 ? unchecked((uint)(int)(short)value) : value);", + "else if (destinationMode == 2) { WriteSized(State.A[destination], value, size); SetMoveFlags(value, size); }\n" + " else WriteGeneralRegister(true, destination, expectedWidth == 2 ? unchecked((uint)(int)(short)value) : value);") + timing = "CompleteTiming(mode == 2 ? M68kInstructionTimingKey.MoveLongAddressIndirectToAddressIndirect : M68kInstructionTimingKey.MoveLongPostIncrementToAddressIndirect);" + text = replace_once(text, + "if (indexed && indexedTimingKey != M68kInstructionTimingKey.GeneralMove)", + "if (destinationMode == 2)\n " + timing + "\n else if (indexed && indexedTimingKey != M68kInstructionTimingKey.GeneralMove)") + if variant == "replay": + text = replace_once(text, "var destination = (opcode >> 9) & 7;", + "if (destinationMode == 2) _ = ReadPendingMoveSource020(address, size);\n var destination = (opcode >> 9) & 7;") + elif variant == "alias": + text = replace_once(text, "WriteSized(State.A[destination], value, size);", + "WriteSized(destination == register && mode == 3 ? address : State.A[destination], value, size);") + elif variant == "flags": + text = replace_once(text, + "else if (destinationMode == 2) { WriteSized(State.A[destination], value, size); SetMoveFlags(value, size); }", + "else if (destinationMode == 2) { WriteSized(State.A[destination], value, size); }") + elif variant == "timing": + text = replace_once(text, timing, + 'CompleteTimingPlan(M68kInstructionPlan.CreateFlat(M68kInstructionTimingKey.GeneralMove, "mutated suffix", 6));') + return text + + +def expected_memory(model, group, variant): + fault = "recovery" in group + expected = {} + for post, alias, bank, value, ccr, lane in itertools.product( + [False, True], [False, True], ["user", "user-M", "ISP", "MSP"], + [0, 0x80818283, 0x7fffffff, 0xffffffff], range(32), range(4) if fault else [0]): + key = f"{model}/MOVE.L/source={'post' if post else 'indirect'}/dest={'alias-A0' if alias else 'A1'}/bank={bank}/value={value:08X}/ccr={ccr:02X}/lane={lane}" + status = "passing" + if fault and variant == "baseline": + status = "unsupported" + elif fault and (variant in ("replay", "timing") or variant == "alias" and alias and post): + status = "mismatching" + elif fault and variant == "flags" and (ccr & 15) != (4 if value == 0 else 8 if value & 0x80000000 else 0): + status = "mismatching" + expected[key] = {status: 1} + return expected + + +def command(root, variant): + selected = FILTER + ("|" + RETENTION if variant == "candidate" else "") + return ["dotnet", "test", str(root / "source/Copper68k.Tests/Copper68k.Tests.csproj"), "-c", "Release", + "--artifacts-path", str(root / "build"), "--filter", selected, + "--logger", "trx;LogFileName=audit.trx", "--results-directory", str(root)] + + +def verify_run(root, variant, expected_sources): + record = load(root / "execution.json") + require(record["command"] == command(root, variant), "Wrong exact command") + require(record["sources"] == expected_sources == inventory(root / "source"), "Changed complete source inventory") + require(record["exit"] == (0 if variant == "candidate" else 1), "Wrong expected execution exit") + outputs = ["execution.log", "audit.trx", "build/bin/Copper68k/release/Copper68k.dll", "build/bin/Copper68k.Tests/release/Copper68k.Tests.dll"] + require(set(record["outputs"]) == set(outputs) and record["outputs"] == {p: sha(root / p) for p in outputs}, "Changed execution output identity") + tree = ET.parse(root / "audit.trx") + tests = tree.findall(".//t:UnitTestResult", NS) + names = {"DirectMemoryDestinationControls(batch: False)", "DirectMemoryDestinationControls(batch: True)", + "ScalarMemoryDestinationRecovery", "BatchMemoryDestinationRecovery"} + if variant == "candidate": + names |= {"LiteralMoveFixturesAndSentinels(batch: False)", "LiteralMoveFixturesAndSentinels(batch: True)", + "ScalarOperandReadFaultsRequireFormatB", "BatchOperandReadFaultsRequireFormatB"} + actual_names = {t.get("testName").split(".")[-1] for t in tests} + require(len(tests) == len(names) and actual_names == names, "Wrong or empty actual test selection") + expected_failures = 0 if variant == "candidate" else 2 + require(sum(t.get("outcome") == "Failed" for t in tests) == expected_failures and + all(t.get("outcome") in ("Passed", "Failed") for t in tests), "Wrong actual test outcomes") + counters = tree.find(".//t:Counters", NS) + require(int(counters.get("total")) == len(names) and int(counters.get("failed")) == expected_failures and + int(counters.get("passed")) == len(names) - expected_failures, "Wrong TRX counters") + stdout = "\n".join(t.text or "" for t in tree.findall(".//t:UnitTestResult/t:Output/t:StdOut", NS)) + totals = dict(passing=0, mismatching=0, unsupported=0, untested=0) + reports = [] + for model, kind, mode in itertools.product(MODELS, ["controls", "recovery"], ["scalar", "batch"]): + group = f"memory-destination-{kind}-{mode}" + p = root / f"{model}-{group}.json" + d = load(p) + expected = expected_memory(model, group, variant) + counts = {s: sum(row.get(s, 0) for row in expected.values()) for s in totals} + require(d["model"] == model and d["group"] == group and d["combinations"] == expected and + d["counts"] == counts and d["logicalCases"] == len(expected) and d["xunitBatches"] == 1, + "Wrong keys, weights or results: " + p.name) + bad = {k for k, row in expected.items() if "passing" not in row} + require(len(d["failures"]) == len(bad) and {f["id"] for f in d["failures"]} == bad and + all(f["status"] in expected[f["id"]] and f["reason"] for f in d["failures"]), "Missing failure witnesses") + reason = {"replay": "Source read/destination write address, order, width or count differs", + "timing": "Memory destination changed the existing MOVE timing policy"}.get(variant) + if reason: + require(all(f["reason"].startswith(reason) for f in d["failures"]), "Wrong mutation failure cause") + summary = re.findall(re.escape(model + "/" + group + ": ") + r'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)', stdout) + require(len(summary) == 1 and json.loads(summary[0][0]) == counts and + int(summary[0][1]) == len(expected) and int(summary[0][2]) == len(expected), "TRX/report disagreement") + for key in totals: + totals[key] += counts[key] + reports.append({"path": p.name, "sha256": sha(p)}) + if variant == "candidate": + for p in root.glob("*-operand-read-resume-*.json"): + d = load(p) + count = 5376 if "discovery" in d["group"] else 2304 + fault = "discovery" in d["group"] + expected = {} + for form, width, bank in itertools.product(["indirect", "postincrement", "predecrement", "displacement", "absolute-word", "absolute-long"], [1, 2, 4], ["user", "user-M", "ISP", "MSP"]): + for lane in range(width) if fault else [0]: + key = f"{d['model']}/MOVE/read-{'fault' if fault else 'control'}/form={form}/size={width}/bank={bank}/byte={lane}" + expected[key] = {"passing": 32} + require(d["logicalCases"] == count and d["counts"] == dict(passing=count, mismatching=0, unsupported=0, untested=0) + and d["combinations"] == expected and not d["failures"], "Register-destination retention failed") + summary = re.findall(re.escape(d["model"] + "/" + d["group"] + ": ") + r'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)', stdout) + require(len(summary) == 1 and json.loads(summary[0][0]) == d["counts"] and int(summary[0][1]) == count and int(summary[0][2]) == len(expected), "Retention TRX/report disagreement") + reports.append({"path": p.name, "sha256": sha(p)}) + require(len(reports) == 32, "Missing register-destination retention report") + require(set(p.name for p in root.glob("*.json")) == {r["path"] for r in reports} | {"execution.json"}, "Foreign or missing report") + return dict(variant=variant, counts=totals, reports=reports, executions=len(names), + retentionCases=61440 if variant == "candidate" else 0, executionSha256=sha(root / "execution.json")) + + +def audit(parent, output, validate): + require(sha(parent / "base-inputs.json") == PARENT_MANIFEST, "Wrong frozen parent manifest") + base = load(parent / "base-inputs.json") + parent_sources = {row["path"].replace("\\", "/"): row["sha256"] for row in base["sources"]} + require(len(parent_sources) == len(base["sources"]) == 230 and inventory(parent / "source") == parent_sources, + "Incomplete or changed frozen parent source") + require(sha(parent / "source" / CPU) == PARENT_CPU, "Wrong private CPU parent") + original = (parent / "source" / CPU).read_text(encoding="utf-8") + producers = {str(Path(__file__).relative_to(REPO).as_posix()): sha(Path(__file__)), FIXTURE.as_posix(): sha(REPO / FIXTURE)} + if validate: + manifest = load(output / "inputs.json") + require(manifest["producers"] == producers and manifest["parentManifestSha256"] == PARENT_MANIFEST, + "Wrong producer or parent identity") + else: + output.mkdir(parents=True, exist_ok=False) + save(output / "inputs.json", dict(schema=1, parentManifestSha256=PARENT_MANIFEST, producers=producers, variants=VARIANTS)) + require(load(output / "inputs.json")["variants"] == VARIANTS, "Changed requested selection") + entries = [] + for variant in VARIANTS: + root = output / variant + expected_sources = dict(parent_sources) + expected_sources[FIXTURE.as_posix()] = sha(REPO / FIXTURE) + patch = candidate(original, variant) + patch_bytes = patch.encode("utf-8") + # Normalize only the one explicitly patched private file; all others byte-identical. + expected_sources[CPU.as_posix()] = hashlib.sha256(patch_bytes).hexdigest() if variant != "baseline" else PARENT_CPU + if not validate: + (root / "source").mkdir(parents=True) + for filename in parent_sources: + target = root / "source" / filename + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(parent / "source" / filename, target) + shutil.copyfile(REPO / FIXTURE, root / "source" / FIXTURE) + if variant != "baseline": + (root / "source" / CPU).write_bytes(patch_bytes) + env = dict(os.environ, COPPER68K_RUN_020_MEMORY_DESTINATION_RECOVERY="1", + COPPER68K_RUN_020_OPERAND_READ_DISCOVERY="1", COPPER68K_SYNTHETIC_REPORT_DIR=str(root)) + with (root / "execution.log").open("w", encoding="utf-8") as log: + result = subprocess.run(command(root, variant), env=env, stdout=log, stderr=subprocess.STDOUT, check=False) + outputs = ["execution.log", "audit.trx", "build/bin/Copper68k/release/Copper68k.dll", "build/bin/Copper68k.Tests/release/Copper68k.Tests.dll"] + save(root / "execution.json", dict(command=command(root, variant), exit=result.returncode, + sources=inventory(root / "source"), outputs={p: sha(root / p) for p in outputs})) + entry = verify_run(root, variant, expected_sources) + entries.append(entry) + print(variant, entry["counts"], "retention", entry["retentionCases"], flush=True) + proof = dict(schema=1, inputsSha256=sha(output / "inputs.json"), entries=entries, + productionImport=False, publication=False, hardwareQualified=False, roadmapComplete=False) + if validate: + require(load(output / "proof.json") == proof, "Changed aggregate verification") + else: + save(output / "proof.json", proof) + print("Strict isolated recovery audit passed", sha(output / "proof.json"), flush=True) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--parent-directory", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--validate-only", action="store_true") + args = parser.parse_args() + audit(args.parent_directory.resolve(), args.output.resolve(), args.validate_only) From 23f9e8dc8ab12410a952114a2d5e793c996b6c0a Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 00:14:00 +0300 Subject: [PATCH 132/211] Qualify native MOVE transfer faults and expose private write mapping gap --- .../SyntheticM68020MoveWriteFaultTests.cs | 132 ++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 110 +++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 ++ .../reference/m68030-move-transfer-faults.cpp | 231 ++++++++++++++++++ ...test-copper68k-030-move-transfer-faults.py | 146 +++++++++++ .../test-copper68k-move-write-discovery.py | 114 +++++++++ 6 files changed, 748 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFaultTests.cs create mode 100644 scripts/reference/m68030-move-transfer-faults.cpp create mode 100644 scripts/test-copper68k-030-move-transfer-faults.py create mode 100644 scripts/test-copper68k-move-write-discovery.py diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFaultTests.cs new file mode 100644 index 00000000..226a2774 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFaultTests.cs @@ -0,0 +1,132 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Native 030 final-write observation: short format A, completed source/flags, +// following PC, one pending write. Other models remain software discovery. +public sealed class SyntheticM68020MoveWriteFaultTests(ITestOutputHelper output) +{ + private static readonly ModelSpec[] Models = ModelSpec.All.Where(x => x.Id is "68EC020" or "68020" or "68030" or "A1200").ToArray(); + [Theory, Trait("Suite", "ReferenceDiscovery")] + [InlineData(false)] [InlineData(true)] + public void DirectFinalWriteControls(bool batch) => Audit(batch, false); + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_DISCOVERY", "require actual final-write fault recovery"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarFinalWriteFaults() => Audit(false, true); + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_DISCOVERY", "require actual final-write fault recovery"), Trait("Suite", "ReferenceDiscovery")] + public void BatchFinalWriteFaults() => Audit(true, true); + + private void Audit(bool batch, bool fault) + { + var failures = new List(); + foreach (var model in Models) + { + var bus = new FaultBus(); var m = new SyntheticMachine(model, bus); + var report = new CoverageBatch(model.Id, $"move-final-write-{(fault ? "fault" : "controls")}-{(batch ? "batch" : "scalar")}"); + foreach (var width in new[] { 1, 2, 4 }) + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var source in new[] { 0, 7 }) + foreach (var post in new[] { false, true }) + foreach (var alias in new[] { false, true }) + foreach (var vi in Enumerable.Range(0, 4)) + foreach (var ccr in new[] { 0, 31 }) + { + var id = $"{model.Id}/MOVE/write/width={width}/bank={bank}/source={source}/post={post}/alias={alias}/value={vi}/ccr={ccr}"; + try + { + var mask = width == 1 ? 0xffu : width == 2 ? 0xffffu : uint.MaxValue; + var value = vi switch { 0 => 0u, 1 => 0x80818283u & mask, 2 => mask >> 1, _ => mask }; + var stride = width == 1 && source == 7 ? 2u : (uint)width; + var destination = alias ? 0x4200u + (post ? stride : 0) : 0x4400u; + var opcode = (ushort)((width == 1 ? 0x1090 : width == 2 ? 0x3090 : 0x2090) | source | (post ? 8 : 0) | ((alias ? source : 1) << 9)); + bus.Disarm(); m.Reset(ccr); _ = SyntheticExecution.Prepare(m, [opcode, 0x7e2a, 0x4e71]); + for (uint address = 0x41f8; address < 0x4210; address++) m.InitializePhysical(address, 0xa5, 1); + for (uint address = 0x43f8; address < 0x4410; address++) m.InitializePhysical(address, 0xa5, 1); + m.InitializePhysical(0x4200, value, width); m.InitializePhysical(0x9020, 0x4e73, 2); m.InitializePhysical(0x9022, 0x4e71, 2); + m.Core.State.A[0] = source == 0 ? 0x4200u : 0x4000u; m.Core.State.A[1] = 0x4400; + m.Core.State.SetUserStackPointer(source == 7 && bank is "user" or "user-M" ? 0x4200u : 0x7800u); + m.Core.State.SetInterruptStackPointer(source == 7 && bank == "ISP" ? 0x4200u : 0x4700u); + m.Core.State.SetMasterStackPointer(source == 7 && bank == "MSP" ? 0x4200u : 0x7400u); + var sr = (ushort)(0x700 | ccr | (bank is "ISP" or "MSP" ? 0x2000 : 0) | (bank is "user-M" or "MSP" ? 0x1000 : 0)); + m.Core.State.StatusRegister = sr; m.Core.State.SetActiveStackPointer(source == 7 ? 0x4200u : bank == "MSP" ? 0x7400u : bank == "ISP" ? 0x4700u : 0x7800u); + var e = ArchitecturalExpectation.Capture(m); var serial = m.Core.State.ExceptionSequence; + e.Sr = (ushort)((sr & 0xfff0) | (value == 0 ? 4 : (value & (1u << (width * 8 - 1))) != 0 ? 8 : 0)); + if (post) { e.A[source] += stride; if (source == 7 && bank == "MSP") e.MasterStackPointer = e.A[7]; } + bus.Accesses.Clear(); bus.Observe(destination, width); if (fault) bus.Arm(destination, width); Step(m, batch); + if (fault) + { + if (bus.Rejected != 1 || m.Core.State.LastExceptionVector != 2 || m.Core.State.ExceptionSequence != serial + 1 || + m.Core.State.ProgramCounter != 0x9020 || m.Core.State.Halted || m.Core.State.Stopped) + throw new InvalidOperationException("Final-write request did not enter vector 2 exactly once"); + var frame = m.Core.State.A[7]; + var expectedFrame = (bank is "ISP" or "MSP" ? e.A[7] : bank == "user-M" ? 0x7400u : 0x4700u) - 32; + if (frame != expectedFrame || bus.SuccessfulSourceReads != 1 || bus.SuccessfulDestinationWrites != 0 || + m.Core.State.D[7] != e.D[7] || m.PeekPhysical(frame + 6, 2) != 0xa008 || m.PeekPhysical(frame, 2) != e.Sr || m.PeekPhysical(frame + 2, 4) != 0x1002 || + (m.PeekPhysical(frame + 10, 2) & 0x1f7) != (0x100u | (width == 1 ? 0x10u : width == 2 ? 0x20u : 0) | ((sr & 0x2000) != 0 ? 5u : 1u)) || + m.PeekPhysical(frame + 16, 4) != destination || (m.PeekPhysical(frame + 24, 4) & mask) != value) + throw new InvalidOperationException("Final-write format-A PC/SR/SSW/address/output differs"); + if (bus.Accesses.Any(a => a.Write && (a.Address < frame || a.Address >= frame + 32))) + throw new InvalidOperationException("Rejected final write reached ordinary memory"); + for (uint n = 0; n < 32; n++) e.Memory[model.Physical(frame + n)] = bus.Peek(model.Physical(frame + n)); + bus.ExcludeFrameReads(frame); e.ExceptionVector = 2; Step(m, batch); + } + e.Write(destination, value, width, model); e.Pc = 0x1002; Check(m, e); + // Frame traffic can overlap A7 source guards; distinguish the + // intended operand widths/value from frame word transfers. + if (bus.SuccessfulSourceReads != 1 || bus.SuccessfulDestinationWrites != 1 || bus.Rejected != (fault ? 1 : 0)) + throw new InvalidOperationException("Final-write recovery repeated a completed source/update or write"); + e.D[7] = 42; e.Pc = 0x1004; e.Sr = (ushort)(e.Sr & 0xfff0); Step(m, batch); Check(m, e); + report.Record(id, "passing", null); + } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException) { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + try { report.Complete(output); } catch (Exception ex) { failures.Add(ex); } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(x => x.Message))); + } + private static void Check(SyntheticMachine m, ArchitecturalExpectation e) + { var error = e.Verify(m); if (error != null) throw new InvalidOperationException(error); } + private static void Step(SyntheticMachine m, bool batch) + { + if (!batch) { m.Core.ExecuteInstruction(); return; } + var boundary = new Boundary(); if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, boundary) != 1 || boundary.Before != 1 || boundary.After != 1) + throw new InvalidOperationException("Batch boundary differs"); + } + private sealed class Boundary : IM68kInstructionBoundary + { public int Before, After; public bool BeforeInstruction() { Before++; return true; } public void AfterInstruction(long a, long b) => After++; } + private sealed class FaultBus : SparseRecordingBus, IM68kPhysicalAddressMap, IM68kBus + { + private uint? denied; private int width; + internal int Rejected, SuccessfulSourceReads, SuccessfulDestinationWrites; + private uint destination; private int operandWidth; private uint? frameReadStart; + internal void Disarm() { denied = frameReadStart = null; Rejected = SuccessfulSourceReads = SuccessfulDestinationWrites = 0; } + internal void Arm(uint address, int size) { denied = address; width = size; } + public bool IsCpuPhysicalAddressMapped(uint address, int size, M68kBusAccessKind kind) + { + if (denied is not { } at || kind != M68kBusAccessKind.CpuDataWrite || size != width || address != at) return true; + denied = null; Rejected++; return false; + } + // Record actual transport, independently of the mapping query. A denied + // destination must never be dispatched to the ordinary bus. + private void BeforeWrite(uint address, int size, M68kBusAccessKind kind) + { + if (kind != M68kBusAccessKind.CpuDataWrite) return; + if (denied == address && size == width) throw new InvalidOperationException("Denied destination write bypassed physical map"); + if (address == destination && size == operandWidth) SuccessfulDestinationWrites++; + } + internal void Observe(uint address, int size) { destination = address; operandWidth = size; } + internal void ExcludeFrameReads(uint frame) => frameReadStart = frame; + private void BeforeRead(uint address, int size, M68kBusAccessKind kind) + { + if (address == 0x4200 && size == operandWidth && kind == M68kBusAccessKind.CpuDataRead && + !(frameReadStart is { } frame && address >= frame && address + (uint)size <= frame + 32)) SuccessfulSourceReads++; + } + byte IM68kBus.ReadByte(uint a, ref long c, M68kBusAccessKind k) { BeforeRead(a, 1, k); return base.ReadByte(a, ref c, k); } + ushort IM68kBus.ReadWord(uint a, ref long c, M68kBusAccessKind k) { BeforeRead(a, 2, k); return base.ReadWord(a, ref c, k); } + uint IM68kBus.ReadLong(uint a, ref long c, M68kBusAccessKind k) { BeforeRead(a, 4, k); return base.ReadLong(a, ref c, k); } + void IM68kBus.WriteByte(uint a, byte v, ref long c, M68kBusAccessKind k) { BeforeWrite(a, 1, k); base.WriteByte(a, v, ref c, k); } + void IM68kBus.WriteWord(uint a, ushort v, ref long c, M68kBusAccessKind k) { BeforeWrite(a, 2, k); base.WriteWord(a, v, ref c, k); } + void IM68kBus.WriteLong(uint a, uint v, ref long c, M68kBusAccessKind k) { BeforeWrite(a, 4, k); base.WriteLong(a, v, ref c, k); } + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index fcdd039b..27b696e3 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -10390,3 +10390,113 @@ does not qualify this new candidate. Full integration and consumers are still required before import. Hardware is unavailable; the trace disagreements remain open. No regression retirement or public release occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +### Native MOVE widths and final-write fault discovery — 2026-10-08 + +The maintained native successor now executes byte, word and long indirect/ +postincrement MOVE to an indirect separate or aliased memory destination, with +A0 or A7 sources, four unchanged stack states, four width-specific values and +all 32 CCR states. Pinned WinUAE generated functions, MMU access/retry, frame, +RTE and fixup fragments are unchanged; the byte-stride table is copied exactly +from pinned `newcpu.cpp`. Reference pin remains +`5d22d33632646efc3f747f03e82d28353e52722e`. + +**33,792 programs pass:** 9,216 source-read recoveries, 12,288 final-write +recoveries and 12,288 direct controls. The 3,072 A7-postincrement source-read +fault combinations remain explicitly **untested by this command**, because the +separate native A7 investigation retains its unresolved restoration disagreement. +They are not silently counted as successful, invalid or excluded architectural +instructions. Direct and final-write A7 cases do run, including its byte stride +of two and supervisor-stack/operand overlap. + +Source faults produce format B / 92 bytes, saved PC `1000`, initial CCR and a +read SSW. Final-write faults produce format A / 32 bytes, saved PC `1002`, +completed MOVE flags, write SSW, pending destination address and data output. +Native RTE completes the pending final write without another source read or +source update. Every selected case checks exact source/write attempts and +successful transfers, destination address/value/width, source/alias/A7 results, +pre-sentinel flags, returned S/M, inactive banks, untouched registers, next PC +and exact event order. Full memory is compared with only the intended operand +write and observed opaque native frame allowed. A frame overlapping completed +A7 source memory is accounted for explicitly. No frame/internal-word guess is +used to infer architectural behavior. Data-output checks cover the defined +operand-width bits; upper unused transport bits are not qualified. + +Four independent negative runs preserve all generated native functions: +source replay and duplicate write fail at case 0, a wrong final-write retry +address fails at case 256 after those controls pass, and a sole byte-stride +table mutation fails at case 2304 after preceding controls pass. The last +mutation deliberately changes the copied data table; it is not a pristine +reference run. Nine corrupted-evidence controls reject missing fixture, wrong +producer, missing output identity, changed fixture/function/stride fragment +with updated manifest, empty output, wrong saved PC with updated manifest, and +changed verification. The original executable then replays rows/trace exactly. + +```powershell +python scripts/test-copper68k-030-move-transfer-faults.py ` + --reference-directory ` + --output +python scripts/test-copper68k-030-move-transfer-faults.py ` + --reference-directory --output --validate-only +``` + +Evidence `reference/MoveTransfer030V5/verification.json` SHA-256: +`4e7ff2240652bed80ada5ffb716c7be745ad404a2b92b0c95888fb710cf76f08`. +Combined `move-transfer-native-proof-v5.json` SHA-256: +`5be3cce1e2f05d56413c85571c2279eb02f19f1f36cbe77d2b362b9b169315ad`. +V1 rejected an incorrect native extraction signature; V2 rejected duplicate +inherited definitions at compilation. V3 passed transfer results; V4 added +frame fields/whole-memory/bank checks; V5 pins the actual byte-stride table. +Only V5 is the final current proof. Compiler warnings in unchanged extracted +native code remain recorded. Production CPU source is untouched. + +The current private CPU fails a new independent write-fault fixture: +**6,144 denied destination writes bypass its physical-address map**, while all +**6,144 direct controls pass**. `SyntheticM68020MoveWriteFaultTests` covers all +three widths, A0/A7, indirect/postincrement, separate/aliased destinations, +four stack states/values, CCR 0/31 and scalar/batch execution. The transport +throws before a denied ordinary write can modify memory. Frame-A, completed +prefix and pending-write expectations are encoded for the future correction, +but those recovery checks are **not yet reached or qualified**. For an A7 +source overlapping a future short frame, its ordinary frame reads are excluded +from the coarse source counter; detailed overlapping frame/read sequencing +will need its own proof, not credit from this failing probe. + +The discovery command pins the complete previous private read-recovery proof, +copies all 231 parent sources plus this fixture, and checks exact producers, +commands, outputs, TRX names/outcomes/counters/stdout and every report key, +weight and mapping-bypass cause. Default execution and validate-only return +failure for the unqualified recovery gate. Explicit discovery-only mode +records the same failures with `recoveryGatePassed=false`; it does not qualify +or promote the CPU. + +```powershell +python scripts/test-copper68k-move-write-discovery.py ` + --qualified-parent-directory /audits/MemoryDestinationPrivateV4 ` + --output +# To retain the failing observations explicitly, add --discovery-only. +# For an existing output, add --validate-only; the ordinary gate still fails. +``` + +Evidence `audits/MoveFinalWriteDiscoveryV1/verification.json` SHA-256: +`79a07c829bb43d0deba60bebc9a0d69a475738b65677dadb4609fe0fa345766c`. +All four requested batches ran: two pass / two fail, zero unavailable or +unsupported selected cases. These are recorded mapping failures, not passing +bus-error recovery. The next implementation must finish only the pending +write and preserve the already-completed source/flags, saved pipe and existing +timing policy. Byte/word memory-destination source-read continuation also +remains absent from the previous private CPU candidate. + +Separate production-source runs retain both fixtures' direct coverage: +22,528 scenarios / four passing batches, with four optional private recovery +batches explicitly unavailable. No normal build output is used or replaced. +`audits/MoveTransferProductionControlsV1/verification.json` SHA-256: +`c11b4d5273fda6849c4b345c287a783b3401b4a745114e0d7d98e4256fcb1831`. + +Native agreement remains **030 software observation**, not hardware or an +independent 020/EC020/040/060 exception oracle. Enabled translation, trace, +interrupts, physical timing/cache behavior, changed handler registers/S/M, +other EAs, unaligned/partial writes, nested/frame faults and broader origins +remain open. Earlier complete-suite/combined/consumer gates retain their old +source scope. No CPU import, package publication or regression retirement +occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 192e7d2f..1a601ecb 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3363,3 +3363,18 @@ trace/hardware, full integration and consumer gates remain open before any CPU import. No production CPU/package change occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [private memory-destination suffix and evidence](COPPER68K_REFERENCE_QUALIFICATION.md#private-memory-destination-read-recovery-suffix--2026-10-07). + +The native MOVE fault reference now covers byte/word/long source-read and final +destination-write recovery with A0/A7, aliases, four stack states/values and all +CCRs: 33,792 programs pass, with 3,072 A7-postincrement source-read combinations +explicitly untested by this command. Final writes use short format A with the +following PC, completed flags and pending output; source reads use format B. +Four sole observer/input defects and nine evidence corruptions are detected; +frozen native replay matches. The current private CPU separately reproduces +6,144 denied-write mapping bypasses while 6,144 direct controls pass. Its +maintained default audit correctly fails the recovery gate; discovery-only +records the failures without promotion. Write recovery and byte/word source +continuation still require implementation, mutation/retention and integration +proof. No production CPU/package change occurs. Milestone 6 remains **in +progress**, `roadmapComplete=false`. See the +[native widths/final-write evidence and failing private probe](COPPER68K_REFERENCE_QUALIFICATION.md#native-move-widths-and-final-write-fault-discovery--2026-10-08). diff --git a/scripts/reference/m68030-move-transfer-faults.cpp b/scripts/reference/m68030-move-transfer-faults.cpp new file mode 100644 index 00000000..6231bca4 --- /dev/null +++ b/scripts/reference/m68030-move-transfer-faults.cpp @@ -0,0 +1,231 @@ +// Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT +// 030 software discovery: byte/word/long MOVE source-read and final-write faults. +// Unchanged generated operations, fixup helpers and retry loop; flat physical +// transport. Observe indirect/postincrement source and aliased destinations. +// Trace is disabled. No enabled translation, IRQ, cache/pipeline timing, +// physical function-code spaces or architectural changed-bank qualification. +#include +#include +#include +#include +#include +#include +#include +#include +#include +using uae_u8=uint8_t;using uae_u16=uint16_t;using uae_u32=uint32_t; +using uae_s8=int8_t;using uae_s16=int16_t;using uae_s32=int32_t;using uaecptr=uint32_t; +#define REGPARAM2 +#define STATIC_INLINE static inline +#define CYCLE_UNIT 2 +#define _T(x) x +#define MMUDEBUG 0 +#define TRY(x) try +#define CATCH(x) catch(int x) +#define ENDTRY +#define STOPTRY +#define THROW(x) throw int(x) +#define SPCFLAG_TRACE 0x40 +#define SPCFLAG_DOTRACE 0x80 +#define SPCFLAG_TRAP 0x400 +#define SPCFLAG_INT 8 +#define SPCFLAG_DOINT 0x100 +#define CPU_HALT_CPU_STUCK 1 +#define CPU_HALT_DOUBLE_FAULT 2 +#define sz_byte 0 +#define sz_word 1 +#define sz_long 2 +#define m68k_areg(r,n) ((r).a[n]) +#define m68k_dreg(r,n) ((r).d[n]) +struct Regs { + uae_u32 a[8]{},d[8]{},usp{},isp{},msp{},pc{},instruction_pc{},trace_pc{},vbr{},mmu_ssw{},mmu_fault_addr{},sfc{},dfc{},wb3_data{},wb2_address{},fp_ea{},fp_opword{},mmu_effective_addr{},pcr{},fp_unimp_ins{},ir{}; + uae_u16 sr{};int m{},s{},t0{},t1{},intmask{},ipl[1]{},ipl_pin{},stopped{},spcflags{},opcode{},irc{},instruction_cnt{},wb2_status{},wb3_status{},read_buffer{},write_buffer{},fpu_exp_pre{},fp_unimp_pend{}; + int prefetch020_valid[3]{},prefetch020[3]{},pipeline_r8[2]{},pipeline_pos{},pipeline_stop{}; +}regs; +struct Prefs{int cpu_model=68030,mmu_model=68030,cachesize{},cpu_compatible{},cpu_cycle_exact{},fpu_model{};}currprefs; +struct flag_struct{int cznv{},x{};}regflags; +int xflag,nflag,zflag,vflag,cflag; +#define SET_XFLG(x) (xflag=(x)) +#define SET_NFLG(x) (nflag=(x)) +#define SET_ZFLG(x) (zflag=(x)) +#define SET_VFLG(x) (vflag=(x)) +#define SET_CFLG(x) (cflag=(x)) +#define GET_XFLG() xflag +#define GET_NFLG() nflag +#define GET_ZFLG() zflag +#define GET_VFLG() vflag +#define GET_CFLG() cflag +#define CLEAR_CZNV() (cflag=zflag=nflag=vflag=0) +bool m68k_accurate_ipl=false,mmu_debugger=false,ismoves030=false,islrmw030=false,bBusErrorReadWrite=false,mmu030_retry=false; +int mmu030_idx,mmu030_idx_done,mmu030_opcode,mmu030_opcode_stageb,mmu030_fake_prefetch,cpu_cycles; +uaecptr mmu030_fake_prefetch_addr,mm030_stageb_address,last_fault_for_exception_3; +uae_u16 mmu030_state[3];uae_u32 mmu030_data_buffer_out,mmu030_disp_store[2],mmu030_fmovem_store[2],mmu040_move16[4]; +struct mmu030_access{uae_u32 val;};mmu030_access mmu030_ad[10]; +struct mmufixup{int reg=-1;uae_u32 value{};};mmufixup mmufixup[2]; +std::unordered_map mem; +std::vector events;std::vector originalFrame,editedFrame; +bool operandContext=false;bool faultOnce;int attempts,reads,steps,traceCount;uae_u32 tracePc,traceD7;int returnT1; +int activeCase=-1; +int writes,moveCcr,writeAttempts,operandWidth,faultOrigin;uaecptr writeAddress,faultFrame;uae_u32 writeValue; +#include "stride.inc" +std::ofstream diagnostics("trace.log"); +void snapshot(const char* phase){diagnostics<second;}return v; +} +void put(uaecptr a,uae_u32 v,int w){for(int n=0;n>(8*(w-1-n)));} +uae_u32 operand_read(uaecptr a,int width){ + if(a!=0x4200||width!=operandWidth)unavailable();attempts++; + if(faultOnce&&faultOrigin==1){faultOnce=false;events.push_back(1);mmu030_hardware_bus_error(a,0,true,false,width==1?sz_byte:width==2?sz_word:sz_long);} + reads++;events.push_back(4);return read(a,width); +} +void operand_write(uaecptr a,uae_u32 value,int width){ + if(width!=operandWidth)unavailable();writeAttempts++; + const uae_u32 mask=width==1?0xffu:width==2?0xffffu:0xffffffffu;value&=mask; + if(faultOnce&&faultOrigin==2){faultOnce=false;events.push_back(9);mmu030_hardware_bus_error(a,value,false,false,width==1?sz_byte:width==2?sz_word:sz_long);} + if(++writes!=1)unavailable();writeAddress=a;writeValue=value;events.push_back(8);put(a,value,width); +} +uae_u32 uae_mmu030_get_long(uaecptr a){return operandContext?operand_read(a,4):read(a,4);} +uae_u32 uae_mmu030_get_word(uaecptr a){return operandContext?operand_read(a,2):read(a,2);} +uae_u32 uae_mmu030_get_byte(uaecptr a){return operandContext?operand_read(a,1):read(a,1);} +uae_u32 uae_mmu030_get_iword(uaecptr a){return read(a,2);} +void uae_mmu030_put_word(uaecptr a,uae_u32 v){if(operandContext)operand_write(a,v,2);else put(a,v,2);} +void uae_mmu030_put_long(uaecptr a,uae_u32 v){if(!operandContext)unavailable();operand_write(a,v,4);} +void uae_mmu030_put_byte(uaecptr a,uae_u32 v){if(!operandContext)unavailable();operand_write(a,v,1);} +uae_u32 get_word_mmu030(uaecptr a){return read(a,2);}uae_u32 get_long_mmu030(uaecptr a){return read(a,4);} +uae_u32 x_get_long(uaecptr a){return read(a,4);}void x_put_long(uaecptr a,uae_u32 v){put(a,v,4);}void x_put_word(uaecptr a,uae_u32 v){put(a,v,2);} +void fc_check(int fc){if(fc!=1&&fc!=5)unavailable();} +uae_u32 uae_mmu030_get_long_fcx(uaecptr a,int fc){fc_check(fc);return operand_read(a,4);} +uae_u32 uae_mmu030_get_word_fcx(uaecptr a,int fc){fc_check(fc);return operand_read(a,2);} +uae_u32 uae_mmu030_get_byte_fcx(uaecptr a,int fc){fc_check(fc);return operand_read(a,1);} +void uae_mmu030_put_long_fcx(uaecptr a,uae_u32 v,int fc){fc_check(fc);operand_write(a,v,4);} +void uae_mmu030_put_word_fcx(uaecptr a,uae_u32 v,int fc){fc_check(fc);operand_write(a,v,2);} +void uae_mmu030_put_byte_fcx(uaecptr a,uae_u32 v,int fc){fc_check(fc);operand_write(a,v,1);} +uae_u32 mmu030_get_generic(uaecptr,uae_u32,int,int){unavailable();}void mmu030_put_generic(uaecptr,uae_u32,uae_u32,int,int){unavailable();} +templatevoid mmu030_unaligned_read_continue(uaecptr,int,F){unavailable();}templatevoid mmu030_unaligned_write_continue(uaecptr,int,F){unavailable();} +void unalign_clear(){unavailable();}void exception3_read_prefetch(uae_u32,uaecptr){unavailable();} +void exception3_read_special(uae_u32,uaecptr,int,int){unavailable();} +void write_log(const char* format,...){char buffer[512];va_list args;va_start(args,format);vsnprintf(buffer,sizeof(buffer),format,args);va_end(args);diagnostics<>12)==10?32:92;for(int n=0;n32)throw std::runtime_error("Bound exceeded");return read(regs.pc+n,2);} +int do_specialties(int){ + const int spcflags=regs.spcflags; + if(spcflags&~(SPCFLAG_TRACE|SPCFLAG_DOTRACE))unavailable(); + // Exact trace fragment of reference do_specialties; IRQ/host branches excluded. +#include "special-trace.inc" + return regs.pc==0x6000||regs.pc==0x1006; +} +uae_u32 dispatch(uae_u32 op){ + if(op==0x3ebc){events.push_back(3);auto v=op_30bc_32_ff(op);editedFrame.clear();for(size_t n=0;n(n)));return v;} + if(op==0x4e73){events.push_back(5);snapshot("before-RTE");auto value=op_4e73_32_ff(op);snapshot("after-RTE");return value;} + if((op&0xf1f0)==0x1090||(op&0xf1f0)==0x2090||(op&0xf1f0)==0x3090){ + snapshot("before-MOVE");operandContext=true; + try{uae_u32 value; + if((op>>12)==1)value=(op&8)?op_1098_32_ff(op):op_1090_32_ff(op); + else if((op>>12)==3)value=(op&8)?op_3098_32_ff(op):op_3090_32_ff(op); + else value=(op&8)?op_2098_32_ff(op):op_2090_32_ff(op); + operandContext=false; + moveCcr=(GET_XFLG()?16:0)|(GET_NFLG()?8:0)|(GET_ZFLG()?4:0)|(GET_VFLG()?2:0)|(GET_CFLG()?1:0); + snapshot("after-MOVE");return value; + }catch(...){operandContext=false;throw;} + } + if(op==0x7e2a){ + if(faultOrigin==2)moveCcr=(GET_XFLG()?16:0)|(GET_NFLG()?8:0)|(GET_ZFLG()?4:0)|(GET_VFLG()?2:0)|(GET_CFLG()?1:0); + events.push_back(6);return op_7000_32_ff(op); + } + if(op==0x4e71)return op_4e71_32_ff(op); + unavailable(); +} +#include "loop.inc" + +int main(){try{ + int id=0; + for(int width:{1,2,4})for(int bank=0;bank<4;bank++)for(int source:{0,7})for(int post=0;post<2;post++)for(int alias=0;alias<2;alias++) + for(int origin=0;origin<3;origin++)for(int vi=0;vi<4;vi++)for(int ccr=0;ccr<32;ccr++){ + // Known unqualified native A7 postincrement read-fault restoration remains + // in the separate A7 discovery command; excluded combinations are counted. + if(source==7&&post&&origin==1)continue; + activeCase=id;operandWidth=width;faultOrigin=origin; + const uae_u32 mask=width==1?0xffu:width==2?0xffffu:0xffffffffu; + const uae_u32 value=vi==0?0:vi==1?(0x80818283&mask):vi==2?(mask>>1):mask; + const uae_u32 stride=width==1&&source==7?2:static_cast(width); + const int expectedCcr=(ccr&16)|(value==0?4:0)|(value&(1u<<(width*8-1))?8:0); + regs={};mem.clear();events.clear();originalFrame.clear();editedFrame.clear();faultOnce=origin!=0;attempts=reads=steps=traceCount=0;tracePc=traceD7=0;regflags={};xflag=nflag=zflag=vflag=cflag=0; + mmu030_state[0]=mmu030_state[1]=mmu030_state[2]=0;mmu030_idx=mmu030_idx_done=0;mmu030_retry=false;mmu030_data_buffer_out=0;mmufixup[0].reg=mmufixup[1].reg=-1; + writes=moveCcr=writeAttempts=0;writeAddress=writeValue=0; + xflag=(ccr>>4)&1;nflag=(ccr>>3)&1;zflag=(ccr>>2)&1;vflag=(ccr>>1)&1;cflag=ccr&1; + regs.usp=0x7000;regs.isp=0x8000;regs.msp=0x9000; + if(source==7){if(bank<2)regs.usp=0x4200;else if(bank==2)regs.isp=0x4200;else regs.msp=0x4200;} + regs.s=bank>=2;regs.m=bank==1||bank==3;regs.intmask=7; + regs.a[7]=source==7?0x4200:bank<2?0x7000:bank==2?0x8000:0x9000; + regs.a[0]=source==0?0x4200:0x4000;regs.a[1]=0x4400;regs.d[0]=0x12345678;regs.pc=regs.instruction_pc=0x1000; + const auto opcode=(width==1?0x1090:width==2?0x3090:0x2090)|(post?8:0)|source|((alias?source:1)<<9); + put(0x1000,opcode,2);put(0x1002,0x7e2a,2);put(0x1004,0x4e71,2); + for(int n=-8;n<16;n++){put(0x4200+n,0xa5,1);put(0x4400+n,0xa5,1);} + put(0x4200,value,width);put(8,0x5000,4);put(36,0x6000,4); + put(0x5000,0x3ebc,2);put(0x5002,0x700|(bank>=2?0x2000:0)|((bank==1||bank==3)?0x1000:0)|(origin==2?expectedCcr:ccr),2);put(0x5004,0x4e73,2); + auto expectedMemory=mem; + for(auto& p:cpufunctbl)p=dispatch; + try{m68k_run_mmu030();}catch(const Done&){} + const uaecptr destination=alias?0x4200u+(post?stride:0u):0x4400u; + const uae_u32 sourceExpected=0x4200u+(post?stride:0u); + const uae_u32 stackExpected=source==7?sourceExpected:bank<2?0x7000u:bank==2?0x8000u:0x9000u; + if(reads!=1||attempts!=(origin==1?2:1)||writes!=1||writeAttempts!=(origin==2?2:1)||writeAddress!=destination||writeValue!=value||read(destination,width)!=value||traceCount!=0||regs.pc!=0x1006||regs.d[7]!=42||regs.d[0]!=0x12345678||regs.a[source]!=sourceExpected||regs.a[1]!=0x4400||regs.a[7]!=stackExpected)throw std::runtime_error("Missing width/source/final-write recovery: id="+std::to_string(id)); + // A final-write short frame completes in RTE, so no MOVE dispatch occurs on + // return; capture CCR after RTE from the native flags directly. + const int finalCcr=(GET_XFLG()?16:0)|(GET_NFLG()?8:0)|(GET_ZFLG()?4:0)|(GET_VFLG()?2:0)|(GET_CFLG()?1:0); + if(finalCcr!=(ccr&16))throw std::runtime_error("Following sentinel flags differ"); + if(moveCcr!=expectedCcr)throw std::runtime_error("MOVE flags differ"); + if(regs.s!=(bank>=2)||regs.m!=(bank==1||bank==3)||regs.t0||regs.t1)throw std::runtime_error("Returned SR mode differs"); + if((bank>=2&®s.usp!=0x7000)||(bank!=2&®s.isp!=0x8000)||(bank!=3&®s.msp!=0x9000))throw std::runtime_error("Inactive stack bank changed"); + for(int n=1;n<7;n++)if(regs.d[n]!=0)throw std::runtime_error("Untouched data register changed"); + for(int n=2;n<7;n++)if(regs.a[n]!=0)throw std::runtime_error("Untouched address register changed"); + if(origin){ + if(originalFrame.size()!=(origin==2?32u:92u)||editedFrame.size()!=originalFrame.size()||originalFrame[6]!=(origin==2?0xa0:0xb0)||originalFrame[7]!=8)throw std::runtime_error("Missing reference fault frame"); + if(originalFrame!=editedFrame)throw std::runtime_error("Handler changed fault frame"); + }else if(!originalFrame.empty()||!editedFrame.empty())throw std::runtime_error("Unexpected control bus fault"); + auto frameRead=[&](size_t offset,int count){uae_u32 result=0;for(int n=0;n(n));return result;}; + const uae_u32 framePc=origin?frameRead(2,4):0; + const uae_u32 frameSr=origin?frameRead(0,2):0; + const uae_u32 frameSsw=origin?frameRead(10,2):0; + const uae_u32 frameAddress=origin?frameRead(16,4):0; + const uae_u32 frameOutput=origin==2?frameRead(24,4)&mask:0; + const uae_u32 expectedSr=0x700u|(bank>=2?0x2000u:0u)|((bank==1||bank==3)?0x1000u:0u)|static_cast(origin==2?expectedCcr:ccr); + const uae_u32 expectedSsw=0x100u|(origin==1?0x40u:0u)|(width==1?0x10u:width==2?0x20u:0u)|(bank>=2?5u:1u); + if(origin&&(framePc!=(origin==2?0x1002u:0x1000u)||frameSr!=expectedSr||(frameSsw&0x1f7)!=expectedSsw||frameAddress!=(origin==2?destination:0x4200u)||origin==2&&frameOutput!=value))throw std::runtime_error("Saved PC/SR/SSW/fault-address/data-output differs"); + for(size_t n=0;n(n)]=originalFrame[n]; + for(int n=0;n(n)]=uae_u8(value>>(8*(width-1-n))); + if(mem!=expectedMemory)throw std::runtime_error("Unexpected operand, frame or surrounding-memory change"); + std::cout<<"id="<> 1, mask][vi] + stride = 2 if width == 1 and source == 7 else width + destination = 0x4200 + post * stride if alias else 0x4400 + moveccr = (ccr & 16) | (4 if value == 0 else 0) | (8 if value & (1 << (8 * width - 1)) else 0) + frame_sr = 0x700 | (0x2000 if bank >= 2 else 0) | (0x1000 if bank in (1,3) else 0) | (moveccr if origin == 2 else ccr) + frame_ssw = 0x100 | (0x40 if origin == 1 else 0) | (0x10 if width == 1 else 0x20 if width == 2 else 0) | (5 if bank >= 2 else 1) + expected = [id, width, bank, source, post, alias, origin, vi, ccr, 2 if origin == 1 else 1, 1, 2 if origin == 2 else 1, 1, + value, destination, 0x4200 + post * stride, 0x4200 + post * stride if source == 7 else [0x7000, 0x7000, 0x8000, 0x9000][bank], 0x1006, + 0x1002 if origin == 2 else 0x1000 if origin == 1 else 0, + frame_sr if origin else 0, frame_ssw if origin else 0, destination if origin == 2 else 0x4200 if origin == 1 else 0, value if origin == 2 else 0, + moveccr, 32 if origin == 2 else 92 if origin == 1 else 0, + "4923586" if origin == 2 else "1235486" if origin == 1 else "486"] + check(actual == expected, "Wrong transfer fault recovery: id=" + str(id) + " actual=" + str(actual) + " expected=" + str(expected)) + return dict(nativeCases=33792, sourceReadRecoveries=9216, finalWriteRecoveries=12288, directControls=12288, + untestedA7PostReadFaultCases=3072, sourceForms=["indirect", "postincrement"], widths=[1,2,4], sourceRegisters=[0,7], + softwareReferenceExecuted=True, hardwareQualified=False, architecturalPromotion=False, + enabledTranslation=False, traceQualified=False, physicalTimingQualified=False, + productionImport=False, publication=False, roadmapComplete=False) + + +def audit(output, reference, validate_only): + check(os.name == "nt", "Windows/MSVC required") + check(subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=reference, text=True).strip() == PIN and + not subprocess.check_output(["git", "status", "--porcelain"], cwd=reference, text=True).strip(), "Wrong or changed pinned reference") + if not validate_only: + check(not output.exists(), "Use fresh audit output") + check(VCVARS.is_file(), "Missing MSVC fixture") + output.mkdir(parents=True) + baseline, native = output / "baseline", output / "native" + args = ["pwsh", "-NoProfile", "-File", str(BASE), "-ReferenceDirectory", str(reference), "-OutputDirectory", str(baseline)] + if validate_only: + args.append("-ValidateReportsOnly") + run(args, REPO, output / ("baseline-replay.log" if validate_only else "baseline.log")) + expected = fragments(reference, baseline) + reference_files = sorted({x["file"] for x in load(baseline / "identities.json")["referenceInputs"]}) + producers = {p.relative_to(REPO).as_posix(): sha(p) for p in [Path(__file__), FIXTURE, BASE, EXTRACTOR]} + inputs = {name: sha(reference / name) for name in reference_files} + files = {*expected, "observer.cpp", "observer.exe", "build.cmd", "build.log", "run.log", "errors.log", "trace.log"} + if not validate_only: + native.mkdir() + for name, text in expected.items(): + (native / name).write_text(text, encoding="utf-8", newline="\n") + shutil.copyfile(FIXTURE, native / "observer.cpp") + command = '@echo off\r\ncall "' + str(VCVARS) + '" >nul\r\nif errorlevel 1 exit /b %errorlevel%\r\ncl /nologo /O2 /EHsc /W4 observer.cpp /Fe:observer.exe\r\nexit /b %errorlevel%\r\n' + (native / "build.cmd").write_text(command, encoding="utf-8") + run([os.environ.get("COMSPEC", "cmd.exe"), "/d", "/c", "build.cmd"], native, native / "build.log") + with (native / "run.log").open("w", encoding="utf-8") as stdout, (native / "errors.log").open("w", encoding="utf-8") as stderr: + result = subprocess.run([str(native / "observer.exe")], cwd=native, stdout=stdout, stderr=stderr) + check(result.returncode == 0, "Native source/destination observer failed; inspect errors.log") + save(output / "identities.json", dict(schema=1, referenceCommit=PIN, producers=producers, referenceInputs=inputs, + outputs={name: sha(native / name) for name in sorted(files)})) + identity = load(output / "identities.json") + check(identity["schema"] == 1 and identity["referenceCommit"] == PIN and identity["producers"] == producers and identity["referenceInputs"] == inputs, "Wrong producer/input identity") + check(set(identity["outputs"]) == files, "Missing native output identity") + for name, digest in identity["outputs"].items(): + check(sha(native / name) == digest, "Changed native output: " + name) + check(sha(native / "observer.cpp") == sha(FIXTURE), "Changed observer fixture") + for name, text in expected.items(): + check((native / name).read_text(encoding="utf-8-sig") == text, "Changed extracted native function: " + name) + if validate_only: + replay = output / ("replay-" + uuid.uuid4().hex) + replay.mkdir() + with (replay / "run.log").open("w", encoding="utf-8") as stdout, (replay / "errors.log").open("w", encoding="utf-8") as stderr: + result = subprocess.run([str(native / "observer.exe")], cwd=replay, stdout=stdout, stderr=stderr) + check(result.returncode == 0 and (replay / "errors.log").stat().st_size == 0 and + all(sha(replay / name) == sha(native / name) for name in ["run.log", "trace.log"]), "Executed native replay differs") + result = observations(native) + result["identitySha256"] = sha(output / "identities.json") + if validate_only: + check(load(output / "verification.json") == result, "Changed verification record") + else: + save(output / "verification.json", result) + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--reference-directory", type=Path, required=True) + parser.add_argument("--validate-only", action="store_true") + args = parser.parse_args() + audit(args.output.resolve(), args.reference_directory.resolve(), args.validate_only) + print("33,792 native MOVE transfer cases; 9,216 source recoveries / 12,288 final-write recoveries / 12,288 controls. A7 post-read fault combinations remain untested; no architectural promotion.") + + +if __name__ == "__main__": + try: + main() + except (ValueError, OSError, KeyError, subprocess.SubprocessError) as error: + print(str(error)) + raise SystemExit(1) diff --git a/scripts/test-copper68k-move-write-discovery.py b/scripts/test-copper68k-move-write-discovery.py new file mode 100644 index 00000000..7578133d --- /dev/null +++ b/scripts/test-copper68k-move-write-discovery.py @@ -0,0 +1,114 @@ +"""Reproduce final-write mapping gaps on the frozen private read-recovery CPU. + +Default and validate-only fail the unqualified recovery gate. --discovery-only +records the known failures explicitly; it never promotes this CPU or publishes. +All builds stay in a fresh output directory. The qualified parent is the complete +MemoryDestinationPrivateV4 audit, not a production checkout. +""" +import argparse +import contextlib +import hashlib +import importlib.util +import io +import itertools +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import xml.etree.ElementTree as ET + +REPO = Path(__file__).resolve().parents[1] +FIXTURE = Path("Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFaultTests.cs") +PARENT_PROOF = "003bb1a07053f298c395e143afc93d725926bf11712cfb19f2bfac99ac3488fc" +READ_HELPER = REPO / "scripts/test-copper68k-memory-destination-recovery.py" +spec = importlib.util.spec_from_file_location("read_recovery", READ_HELPER) +read = importlib.util.module_from_spec(spec) +spec.loader.exec_module(read) +sha, load, save, require, inventory = read.sha, read.load, read.save, read.require, read.inventory +NS = read.NS +MODELS = read.MODELS +FILTER = "FullyQualifiedName~SyntheticM68020MoveWriteFaultTests" + + +def expected(model): + return {f"{model}/MOVE/write/width={width}/bank={bank}/source={source}/post={post}/alias={alias}/value={vi}/ccr={ccr}": 1 + for width, bank, source, post, alias, vi, ccr in itertools.product([1,2,4], ["user","user-M","ISP","MSP"], [0,7], [False,True], [False,True], range(4), [0,31])} + + +def audit(parent, output, validate): + require(sha(parent / "proof.json") == PARENT_PROOF, "Wrong qualified private parent proof") + with contextlib.redirect_stdout(io.StringIO()): + read.audit(parent.parent / "Operand020CombinedRefaultV1", parent, True) + parent_source = parent / "candidate/source" + inputs = inventory(parent_source) + require(len(inputs) == 231, "Incomplete private parent inventory") + inputs[FIXTURE.as_posix()] = sha(REPO / FIXTURE) + producers = {p.relative_to(REPO).as_posix(): sha(p) for p in [Path(__file__), REPO / FIXTURE, READ_HELPER]} + cmd = ["dotnet", "test", str(output / "source/Copper68k.Tests/Copper68k.Tests.csproj"), "-c", "Release", + "--artifacts-path", str(output / "build"), "--filter", FILTER, "--logger", "trx;LogFileName=audit.trx", "--results-directory", str(output)] + outputs = ["execution.log", "audit.trx", "build/bin/Copper68k/release/Copper68k.dll", "build/bin/Copper68k.Tests/release/Copper68k.Tests.dll"] + if not validate: + (output / "source").mkdir(parents=True, exist_ok=False) + for name in inventory(parent_source): + target = output / "source" / name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(parent_source / name, target) + shutil.copyfile(REPO / FIXTURE, output / "source" / FIXTURE) + env = dict(os.environ, COPPER68K_RUN_020_MOVE_WRITE_DISCOVERY="1", COPPER68K_SYNTHETIC_REPORT_DIR=str(output)) + with (output / "execution.log").open("w", encoding="utf-8") as log: + result = subprocess.run(cmd, env=env, stdout=log, stderr=subprocess.STDOUT) + save(output / "inputs.json", dict(schema=1, sources=inputs, producers=producers, parentProofSha256=PARENT_PROOF, + command=cmd, exit=result.returncode, outputs={p: sha(output / p) for p in outputs})) + record = load(output / "inputs.json") + require(record["producers"] == producers and record["parentProofSha256"] == PARENT_PROOF, "Wrong producer or parent") + require(record["sources"] == inputs == inventory(output / "source"), "Changed source inventory") + require(record["command"] == cmd and record["exit"] == 1, "Wrong discovery selection or exit") + require(set(record["outputs"]) == set(outputs) and record["outputs"] == {p: sha(output / p) for p in outputs}, "Changed execution output") + tree = ET.parse(output / "audit.trx") + tests = tree.findall(".//t:UnitTestResult", NS) + names = {"DirectFinalWriteControls(batch: False)": "Passed", "DirectFinalWriteControls(batch: True)": "Passed", + "ScalarFinalWriteFaults": "Failed", "BatchFinalWriteFaults": "Failed"} + require(len(tests) == 4 and {t.get("testName").split(".")[-1]:t.get("outcome") for t in tests} == names, "Wrong actual discovery outcomes") + counters = tree.find(".//t:Counters", NS) + require(int(counters.get("total")) == 4 and int(counters.get("passed")) == 2 and int(counters.get("failed")) == 2, "Wrong discovery counters") + stdout = "\n".join(t.text or "" for t in tree.findall(".//t:UnitTestResult/t:Output/t:StdOut", NS)) + reports = [] + for model, kind, mode in itertools.product(MODELS, ["controls","fault"], ["scalar","batch"]): + group = f"move-final-write-{kind}-{mode}" + p = output / f"{model}-{group}.json"; d = load(p) + status = "mismatching" if kind == "fault" else "passing" + keys = expected(model) + counts = {s: 768 if s == status else 0 for s in ["passing","mismatching","unsupported","untested"]} + require(d["model"] == model and d["group"] == group and d["logicalCases"] == 768 and d["counts"] == counts and + d["combinations"] == {k:{status:n} for k,n in keys.items()}, "Wrong discovery keys, weights or results") + failures = d["failures"] + require(len(failures) == (768 if kind == "fault" else 0) and (kind != "fault" or + {f["id"] for f in failures} == set(keys) and all(f["status"] == "mismatching" and + f["reason"] == "Denied destination write bypassed physical map" for f in failures)), "Wrong mapping-gap cause") + summary = re.findall(re.escape(model + "/" + group + ": ") + r'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)', stdout) + require(len(summary) == 1 and json.loads(summary[0][0]) == counts and int(summary[0][1]) == 768 and int(summary[0][2]) == 768, "Actual discovery/report disagreement") + reports.append(dict(path=p.name, sha256=sha(p))) + allowed = {r["path"] for r in reports} | {"inputs.json", "verification.json"} + require({p.name for p in output.glob("*.json")} <= allowed, "Foreign discovery report") + verification = dict(schema=1, sources=232, passingControls=6144, mismatchingFaults=6144, unsupported=0, untestedSelected=0, + executions=4, passed=2, failed=2, unavailable=0, reports=reports, inputsSha256=sha(output / "inputs.json"), + recoveryGatePassed=False, architecturalPromotion=False, productionImport=False, publication=False, roadmapComplete=False) + if validate: + require(load(output / "verification.json") == verification, "Changed discovery verification") + else: + save(output / "verification.json", verification) + return verification + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--qualified-parent-directory", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--validate-only", action="store_true") + parser.add_argument("--discovery-only", action="store_true") + args = parser.parse_args() + result = audit(args.qualified_parent_directory.resolve(), args.output.resolve(), args.validate_only) + print("Recorded 6144 denied-write mapping mismatches and 6144 passing controls; recovery gate FAILED. No promotion.") + raise SystemExit(0 if args.discovery_only else 1) From 57b9e919d349319f453a25709c31d75c0148f1cf Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 00:45:53 +0300 Subject: [PATCH 133/211] Qualify private pending MOVE write recovery without source replay --- .../SyntheticM68020MoveWriteFaultTests.cs | 16 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 70 +++++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 ++ .../reference/m68020-final-write-candidate.cs | 134 ++++++++++++++ ...st-copper68k-final-move-write-candidate.py | 166 ++++++++++++++++++ 5 files changed, 399 insertions(+), 2 deletions(-) create mode 100644 scripts/reference/m68020-final-write-candidate.cs create mode 100644 scripts/test-copper68k-final-move-write-candidate.py diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFaultTests.cs index 226a2774..88ec4ddc 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFaultTests.cs @@ -71,6 +71,20 @@ private void Audit(bool batch, bool fault) bus.ExcludeFrameReads(frame); e.ExceptionVector = 2; Step(m, batch); } e.Write(destination, value, width, model); e.Pc = 0x1002; Check(m, e); + // Existing approximate execution policy, not physical cycles. + var key = (post, width) switch + { + (false, 1) => M68kInstructionTimingKey.MoveByteAddressIndirectToAddressIndirect, + (false, 2) => M68kInstructionTimingKey.MoveWordAddressIndirectToAddressIndirect, + (false, _) => M68kInstructionTimingKey.MoveLongAddressIndirectToAddressIndirect, + (true, 1) => M68kInstructionTimingKey.MoveBytePostIncrementToAddressIndirect, + (true, 2) => M68kInstructionTimingKey.MoveWordPostIncrementToAddressIndirect, + _ => M68kInstructionTimingKey.MoveLongPostIncrementToAddressIndirect + }; + var plan = ((M68kAdvancedTimingInterpreter)m.Core).Timing.LastInstructionTiming.Plan; + if (plan.Key != key || plan.NativeCycles != 8 || plan.Barriers != M68kTimingBarrier.None || + plan.UsesHeadTail != (model.Id == "68030") || plan.HeadCycles != (model.Id == "68030" ? 1 : 0) || plan.TailCycles != (model.Id == "68030" ? 1 : 0)) + throw new InvalidOperationException($"Final-write timing policy differs: {plan.Key}/{plan.NativeCycles}/{plan.HeadCycles}/{plan.TailCycles}"); // Frame traffic can overlap A7 source guards; distinguish the // intended operand widths/value from frame word transfers. if (bus.SuccessfulSourceReads != 1 || bus.SuccessfulDestinationWrites != 1 || bus.Rejected != (fault ? 1 : 0)) @@ -119,7 +133,7 @@ private void BeforeWrite(uint address, int size, M68kBusAccessKind kind) internal void ExcludeFrameReads(uint frame) => frameReadStart = frame; private void BeforeRead(uint address, int size, M68kBusAccessKind kind) { - if (address == 0x4200 && size == operandWidth && kind == M68kBusAccessKind.CpuDataRead && + if (address is >= 0x41f8 and < 0x4210 && size == operandWidth && kind == M68kBusAccessKind.CpuDataRead && !(frameReadStart is { } frame && address >= frame && address + (uint)size <= frame + 32)) SuccessfulSourceReads++; } byte IM68kBus.ReadByte(uint a, ref long c, M68kBusAccessKind k) { BeforeRead(a, 1, k); return base.ReadByte(a, ref c, k); } diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 27b696e3..5bbe50fa 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -10450,7 +10450,7 @@ frame fields/whole-memory/bank checks; V5 pins the actual byte-stride table. Only V5 is the final current proof. Compiler warnings in unchanged extracted native code remain recorded. Production CPU source is untouched. -The current private CPU fails a new independent write-fault fixture: +At the initial discovery, the private CPU fails a new independent write-fault fixture: **6,144 denied destination writes bypass its physical-address map**, while all **6,144 direct controls pass**. `SyntheticM68020MoveWriteFaultTests` covers all three widths, A0/A7, indirect/postincrement, separate/aliased destinations, @@ -10500,3 +10500,71 @@ other EAs, unaligned/partial writes, nested/frame faults and broader origins remain open. Earlier complete-suite/combined/consumer gates retain their old source scope. No CPU import, package publication or regression retirement occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +### Private final MOVE write continuation — 2026-10-08 + +An isolated candidate now corrects the selected final-write mapping bypass and +implements the pending write after RTE. The production interpreter is unchanged: +`scripts/reference/m68020-final-write-candidate.cs` is copied into an audit +snapshot only. Its distinct private marker `C023` uses a 32-byte format-A image +with next PC, completed MOVE flags, write SSW/address/output, original opcode +and saved instruction words. It resumes only the pending write, without fetching +or executing the original instruction again. Normal mapped execution retains +its existing flag/write order; a denied final write saves the completed flags. +Original function code and the specialized execution timing policy are retained. + +The fixture now counts reads at the source and its surrounding guards, exposing +replays at a postincremented address, and independently checks the existing +six specialized timing keys, native-cycle policy and 030 head/tail policy. +These are execution-policy checks, not physical timing qualification. The fresh +unchanged-parent discovery still records 6,144 mapping mismatches and 6,144 +passing controls. Its V3 verification SHA-256 is +`ba86f8dae54eab0037eda191195e4c79e714ce2fb4b118929c8b69eeb8326594`. +The older V1 discovery and production-control hashes above are historical, +source-specific evidence from commit `23f9e8d`; current fixture producers must +use fresh outputs rather than relabel those records. + +The candidate passes **12,288 selected scenarios**, including 6,144 final-write +recoveries, and retains the exact **143,360** previous register/memory-destination +read-recovery results and coverage keys: 155,648 logical cases / 12 xUnit +executions / 48 reports, with zero selected mismatches, unsupported cases or +unavailable batches. Five isolated defects produce the expected mismatches: +source replay 6,144; repeated postincrement 3,072; address substituted for output +value 5,760; omitted completed flags 5,376; changed suffix timing 6,144. Each +retains all 6,144 direct controls; unaffected recovery cases also remain passing. +The maintained audit verifies complete input inventories, producer/parent +identities, exact commands and output hashes, actual TRX rosters/counters/stdout, +every case key/weight, mutation witnesses and all retained report contents. + +```powershell +python scripts/test-copper68k-final-move-write-candidate.py ` + --qualified-parent-directory /audits/MemoryDestinationPrivateV4 ` + --discovery-directory ` + --output +# Recheck the same evidence with --validate-only. +``` + +Evidence `audits/MoveFinalWriteCandidateV2/proof.json` SHA-256: +`c1a5a156f5922a8d33fe6260eebd6edb61ddba3b309a392337e3772d537445a7`. +Twelve independent copied-evidence controls reject changed producers, missing +fixtures/output identities, empty or reweighted selections, missing witnesses, +wrong causal reasons/commands/counters, changed mutation source plus manifest, +altered retained results and a false completion flag. The original proof then +revalidates. Controls are recorded in `final-write-candidate-controls-v2.json`. +Separate current-production direct controls pass 22,528 scenarios / four batches; +four optional private recovery batches remain unavailable. Their V2 verification +SHA-256 is `c5da8e56e568ab47fea88313215ce8e49429e3e2f76f0dc8069ad6668b945d78`. +Existing private-test compiler warnings remain recorded. + +This gate covers byte/word/long simple indirect and postincrement source forms +with indirect destinations, A0/A7, aliases, four stack states/values, CCR 0/31 +and scalar/batch execution on EC020, 020, 030 and A1200 profiles. It does not +qualify the whole short-frame protocol: saved-pipe lengths 0–3, software DF-clear +or edited output, repeated pending-write faults, changed return SR/stacks, +foreign/moved/invalid images, entry/load faults and trace still require their +own checks. Returned T1/T0 is explicitly unsupported here. Byte/word +memory-destination source-read continuation, read-to-write fault chains, broader +EAs/origins and full integration/isolated consumers remain required before import. +Hardware is unavailable; the earlier trace and A7 software disagreements remain +open. No production CPU import, package publication or regression retirement +occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 1a601ecb..0df7cc96 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3378,3 +3378,18 @@ continuation still require implementation, mutation/retention and integration proof. No production CPU/package change occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [native widths/final-write evidence and failing private probe](COPPER68K_REFERENCE_QUALIFICATION.md#native-move-widths-and-final-write-fault-discovery--2026-10-08). + +The selected pending final-write path now has an isolated private implementation: +all 12,288 new scenarios pass, including 6,144 formerly failing recoveries, with +143,360 exact previous read-recovery results retained. Five deliberate defects +are detected while direct controls remain passing. The strengthened fixture +checks source guard reads and existing specialized timing policy; fresh unchanged +parent discovery preserves the original 6,144 mapping failures. Production +direct controls pass separately; its private recovery batches remain unavailable. +Short-frame software edits/refaults, saved pipe variants, foreign/frame faults, +trace/changed stacks, byte/word source-read memory destinations and read-to-write +chains remain unqualified. Full integration and consumer gates are still required +before importing this private candidate. Hardware is unavailable, so unresolved +reference disagreements remain open. No production CPU/package change or test +retirement occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. +See the [private final-write implementation and bounded evidence](COPPER68K_REFERENCE_QUALIFICATION.md#private-final-move-write-continuation--2026-10-08). diff --git a/scripts/reference/m68020-final-write-candidate.cs b/scripts/reference/m68020-final-write-candidate.cs new file mode 100644 index 00000000..a4ccc339 --- /dev/null +++ b/scripts/reference/m68020-final-write-candidate.cs @@ -0,0 +1,134 @@ +/* Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT */ +// Isolated candidate only; copied into an audit snapshot by the test command. +namespace Copper68k; + +internal partial class M68kAdvancedTimingInterpreter +{ + private const ushort FinalMoveWriteContext020 = 0xc023; + private bool _enteringFinalMoveWrite020, _resumingFinalMoveWrite020; + private ushort? _restoredWriteFunctionCode020; + + private sealed class FinalMoveWriteFault020(uint address, uint value, ushort opcode, + uint nextPc, ushort functionCode, M68kAdvancedInstructionPipe pipe) : + M68kEmulationException($"020/030 final MOVE write fault at {address:X8}.") + { + internal readonly uint Address = address, Value = value, NextPc = nextPc; + internal readonly ushort Opcode = opcode, FunctionCode = functionCode; + internal readonly M68kAdvancedInstructionPipe Pipe = pipe; + } + + private static bool IsSimpleFinalMove020(ushort opcode) => opcode >> 12 is >= 1 and <= 3 && + ((opcode >> 6) & 7) == 2 && ((opcode >> 3) & 7) is 2 or 3; + + private static M68kOperandSize FinalMoveSize020(ushort opcode) => (opcode >> 12) switch + { 1 => M68kOperandSize.Byte, 3 => M68kOperandSize.Word, _ => M68kOperandSize.Long }; + + private static M68kInstructionTimingKey FinalMoveTiming020(ushort opcode) => + (((opcode >> 3) & 7) == 3, opcode >> 12) switch + { + (false, 1) => M68kInstructionTimingKey.MoveByteAddressIndirectToAddressIndirect, + (false, 3) => M68kInstructionTimingKey.MoveWordAddressIndirectToAddressIndirect, + (false, _) => M68kInstructionTimingKey.MoveLongAddressIndirectToAddressIndirect, + (true, 1) => M68kInstructionTimingKey.MoveBytePostIncrementToAddressIndirect, + (true, 3) => M68kInstructionTimingKey.MoveWordPostIncrementToAddressIndirect, + _ => M68kInstructionTimingKey.MoveLongPostIncrementToAddressIndirect + }; + + private void WriteFinalMoveDestination020(uint address, uint value, M68kOperandSize size) + { + var width = size == M68kOperandSize.Byte ? 1 : size == M68kOperandSize.Word ? 2 : 4; + if (_has020PhysicalMap && !_physicalAddressMap!.IsCpuPhysicalAddressMapped(address, width, M68kBusAccessKind.CpuDataWrite)) + { + if (!IsSimpleFinalMove020(State.LastOpcode) || size != FinalMoveSize020(State.LastOpcode) || + State.ProgramCounter != unchecked(State.LastInstructionProgramCounter + 2)) + throw new UnsupportedM68kTimingException(State.LastOpcode, State.LastInstructionProgramCounter, _profile); + // Normal mapped execution keeps its existing ordering. On this + // final-write fault the completed MOVE flags belong in saved SR. + // A repeated pending write must retain the returned SR verbatim. + if (!_resumingFinalMoveWrite020) SetMoveFlags(value, size); + var fc = _restoredWriteFunctionCode020 ?? (ushort)((State.StatusRegister & 0x2000) != 0 ? 5 : 1); + throw new FinalMoveWriteFault020(address, value, State.LastOpcode, State.ProgramCounter, fc, _instructionPipe); + } + WriteSized(address, value, size); + } + + private void RaiseFinalMoveWriteFault020(FinalMoveWriteFault020 fault) + { + if (_enteringFinalMoveWrite020) { State.Halted = true; _instructionPipe.Reset(); return; } + var sr = State.StatusRegister; + var size = FinalMoveSize020(fault.Opcode); + var words = new ushort[16]; + void Long(int offset, uint value) { words[offset / 2] = (ushort)(value >> 16); words[offset / 2 + 1] = (ushort)value; } + words[0] = sr; Long(2, fault.NextPc); words[3] = 0xa008; + words[4] = FinalMoveWriteContext020; + words[5] = (ushort)(0x100 | (size == M68kOperandSize.Byte ? 0x10 : size == M68kOperandSize.Word ? 0x20 : 0) | fault.FunctionCode); + Long(16, fault.Address); words[10] = fault.Opcode; + words[11] = (ushort)(1 | (fault.Pipe.Count << 8)); Long(24, fault.Value); + var pipe = fault.Pipe; + for (var n = 0; n < fault.Pipe.Count; n++) + { + var address = pipe.HeadAddress; + _ = pipe.TryConsumeKnownHead(out var word, out _); + if (address != unchecked(fault.NextPc + (uint)n * 2)) + throw new UnsupportedM68kTimingException(fault.Opcode, unchecked(fault.NextPc - 2), _profile); + words[n == 0 ? 6 : n == 1 ? 7 : 14] = word; + } + if (fault.Pipe.Count != 0 && fault.Pipe.NextAddress != unchecked(fault.NextPc + (uint)fault.Pipe.Count * 2)) + throw new UnsupportedM68kTimingException(fault.Opcode, unchecked(fault.NextPc - 2), _profile); + State.RecordException(2, fault.NextPc, sr); + State.StatusRegister = (ushort)((sr | M68kCpuState.Supervisor) & ~0xc000); + _enteringFinalMoveWrite020 = true; + try + { + for (var n = words.Length - 1; n >= 0; n--) + { + State.SetActiveStackPointer(unchecked(State.A[7] - 2)); + if (!_physicalAddressMap!.IsCpuPhysicalAddressMapped(State.A[7], 2, M68kBusAccessKind.CpuDataWrite)) + { State.Halted = true; _instructionPipe.Reset(); return; } + WriteWord(State.A[7], words[n]); + } + _instructionPipe.Reset(); + State.ProgramCounter = ReadLong(unchecked(State.VectorBaseRegister + 8)); + CompleteTiming(M68kInstructionTimingKey.IllegalInstruction); + } + catch (OperandReadFault020) { State.Halted = true; _instructionPipe.Reset(); } + finally { _enteringFinalMoveWrite020 = false; } + } + + private bool RestoreFinalMoveWrite020(uint frame, ushort sr, uint pc, ushort ssw) + { + var address = ReadRteFrameLong(unchecked(frame + 16)); + var opcode = ReadRteFrameWord(unchecked(frame + 20)); + var metadata = ReadRteFrameWord(unchecked(frame + 22)); + var value = ReadRteFrameLong(unchecked(frame + 24)); + var reserved = ReadRteFrameWord(unchecked(frame + 30)); + var count = metadata >> 8; + var size = FinalMoveSize020(opcode); + var expectedSsw = (size == M68kOperandSize.Byte ? 0x10 : size == M68kOperandSize.Word ? 0x20 : 0) | (ssw & 4) | 1; + if (!IsSimpleFinalMove020(opcode) || (metadata & 0xff) != 1 || count > M68kAdvancedInstructionPipe.Capacity || reserved != 0 || + (ssw & ~0x100) != expectedSsw || (sr & 0xc000) != 0 || (pc & 1) != 0) + throw new UnsupportedM68kTimingException(0x4e73, State.LastInstructionProgramCounter, _profile); + M68kAdvancedInstructionPipe pipe = default; pipe.Reset(pc); + for (var n = 0; n < count; n++) + { + var offset = n == 0 ? 12u : n == 1 ? 14u : 28u; + pipe.Append(unchecked(pc + (uint)n * 2), ReadRteFrameWord(unchecked(frame + offset)), default); + } + // Read the full private state before committing stack, SR or the write. + for (var offset = 12u; offset < 32; offset += 2) _ = ReadRteFrameWord(unchecked(frame + offset)); + State.SetActiveStackPointer(unchecked(frame + 32)); State.StatusRegister = sr; State.ProgramCounter = pc; + CompleteTiming(M68kInstructionTimingKey.Rte); _instructionPipe = pipe; + State.LastOpcode = opcode; State.LastInstructionProgramCounter = unchecked(pc - 2); + var previous = _resumingFinalMoveWrite020; var previousFc = _restoredWriteFunctionCode020; + _resumingFinalMoveWrite020 = true; _restoredWriteFunctionCode020 = (ushort)(ssw & 7); + try + { + // DF clear means software completed the cycle. No opcode fetch, + // source read, EA calculation, source update or flag recomputation. + if ((ssw & 0x100) != 0) WriteFinalMoveDestination020(address, value, size); + } + finally { _resumingFinalMoveWrite020 = previous; _restoredWriteFunctionCode020 = previousFc; } + CompleteTiming(FinalMoveTiming020(opcode)); + return true; + } +} diff --git a/scripts/test-copper68k-final-move-write-candidate.py b/scripts/test-copper68k-final-move-write-candidate.py new file mode 100644 index 00000000..73257594 --- /dev/null +++ b/scripts/test-copper68k-final-move-write-candidate.py @@ -0,0 +1,166 @@ +"""Reproduce the isolated short-format-A MOVE write candidate and sole defects. + +Requires the qualified MemoryDestinationPrivateV4 parent and a current failing +write-discovery audit. Production CPU and normal outputs are never edited. +""" +import argparse +import importlib.util +import itertools +import os +from pathlib import Path +import re +import shutil +import subprocess +import xml.etree.ElementTree as ET + +REPO = Path(__file__).resolve().parents[1] +DISCOVERY = REPO / "scripts/test-copper68k-move-write-discovery.py" +CANDIDATE = REPO / "scripts/reference/m68020-final-write-candidate.cs" +spec = importlib.util.spec_from_file_location("write_discovery", DISCOVERY) +m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m) +r = m.read +sha, load, save, require, inventory = r.sha, r.load, r.save, r.require, r.inventory +CORE = "Copper68k/M68kAdvancedTimingInterpreter.cs" +ACCESS = "Copper68k/M68kAdvancedTimingInterpreter.Access020.cs" +OPERAND = "Copper68k/M68kAdvancedTimingInterpreter.Operand020.cs" +NEW = "Copper68k/M68kAdvancedTimingInterpreter.FinalWrite020.cs" +VARIANTS = ["candidate", "source-replay", "source-update", "output-address", "flags", "timing"] +RETENTION = ["FullyQualifiedName~SyntheticM68020OperandReadContinuationTests", "FullyQualifiedName~SyntheticM68020MemoryDestinationRecoveryTests"] + + +def once(text, old, new): + require(text.count(old) == 1, "Nonunique candidate witness: " + old[:60]) + return text.replace(old, new) + + +def sources(parent, variant): + data = {name: (parent / "source" / name).read_bytes() for name in inventory(parent / "source")} + text = data[CORE].decode("utf-8").replace("\r\n", "\n") + for indent, count in [(" ", 1), (" ", 2)]: + old = indent + "catch (OperandReadFault020 fault)\n" + indent + "{\n" + indent + " RaiseOperandReadFault020(fault);\n" + (indent + " exitBlock = true;\n" if count == 2 else "") + indent + "}\n" + new = old + indent + "catch (FinalMoveWriteFault020 fault)\n" + indent + "{\n" + indent + " RaiseFinalMoveWriteFault020(fault);\n" + (indent + " exitBlock = true;\n" if count == 2 else "") + indent + "}\n" + require(text.count(old) == count, "Wrong scalar/batch catch witnesses"); text = text.replace(old, new) + methods = [ + ("ExecuteMoveLongAddressIndirectToAddressIndirect", "WriteLong(State.A[destination], value);", "WriteFinalMoveDestination020(State.A[destination], value, M68kOperandSize.Long);"), + ("ExecuteMoveByteAddressIndirectToAddressIndirect", "WriteByte(State.A[destination], value);", "WriteFinalMoveDestination020(State.A[destination], value, M68kOperandSize.Byte);"), + ("ExecuteMoveWordAddressIndirectToAddressIndirect", "WriteWord(State.A[(opcode >> 9) & 7], value);", "WriteFinalMoveDestination020(State.A[(opcode >> 9) & 7], value, M68kOperandSize.Word);"), + ("ExecuteMovePostIncrementToAddressIndirect", "WriteSized(State.A[(opcode >> 9) & 7], value, size);", "WriteFinalMoveDestination020(State.A[(opcode >> 9) & 7], value, size);")] + for name, old, new in methods: + start = text.index("private void " + name + "("); end = text.index("\n }", start) + 10 + text = text[:start] + once(text[start:end], old, new) + text[end:] + data[CORE] = text.encode("utf-8") + text = data[ACCESS].decode("utf-8").replace("\r\n", "\n") + old = "if (format == 11 && context == OperandReadContext020)\n return RestoreOperandRead020(frame, sr, pc, ssw);" + data[ACCESS] = once(text, old, old + "\n if (format == 10 && context == FinalMoveWriteContext020)\n return RestoreFinalMoveWrite020(frame, sr, pc, ssw);").encode("utf-8") + text = data[OPERAND].decode("utf-8").replace("\r\n", "\n") + old = "else if (destinationMode == 2) { WriteSized(State.A[destination], value, size); SetMoveFlags(value, size); }" + data[OPERAND] = once(text, old, "else if (destinationMode == 2) { WriteFinalMoveDestination020(State.A[destination], value, size); SetMoveFlags(value, size); }").encode("utf-8") + text = CANDIDATE.read_text(encoding="utf-8") + pending = "if ((ssw & 0x100) != 0) WriteFinalMoveDestination020(address, value, size);" + if variant == "source-replay": + text = once(text, pending, "if ((ssw & 0x100) != 0) { _ = ReadSized(State.A[opcode & 7], size); WriteFinalMoveDestination020(address, value, size); }") + elif variant == "source-update": + text = once(text, pending, "if (((opcode >> 3) & 7) == 3) WriteGeneralRegister(true, opcode & 7, unchecked(State.A[opcode & 7] + M68kIntegerSemantics.AddressIncrement(opcode & 7, size)));\n " + pending) + elif variant == "output-address": + text = once(text, "var value = ReadRteFrameLong(unchecked(frame + 24));", "var value = ReadRteFrameLong(unchecked(frame + 16));") + elif variant == "flags": + text = once(text, "if (!_resumingFinalMoveWrite020) SetMoveFlags(value, size);", "// Sole defect: omit completed flags on the original final-write fault.") + elif variant == "timing": + text = once(text, "CompleteTiming(FinalMoveTiming020(opcode));", 'CompleteTimingPlan(M68kInstructionPlan.CreateFlat(M68kInstructionTimingKey.GeneralMove, "mutated suffix", 6));') + data[NEW] = text.encode("utf-8") + return data + + +def status(key, variant, fault): + if not fault or variant == "candidate": return "passing" + fields = dict(row.split("=", 1) for row in key.split("/")[3:]) + width, vi, ccr, source = [int(fields[x]) for x in ["width", "value", "ccr", "source"]] + post, alias = fields["post"] == "True", fields["alias"] == "True" + mask = (1 << (8 * width)) - 1 + value = [0, 0x80818283 & mask, mask >> 1, mask][vi] + destination = 0x4200 + (2 if width == 1 and source == 7 else width) * post if alias else 0x4400 + flags = 4 if value == 0 else 8 if value & (1 << (width * 8 - 1)) else 0 + mismatch = variant in ["source-replay", "timing"] or variant == "source-update" and post or variant == "output-address" and value != destination & mask or variant == "flags" and (ccr & 15) != flags + return "mismatching" if mismatch else "passing" + + +def command(root, variant): + selected = "|".join([m.FILTER] + (RETENTION if variant == "candidate" else [])) + return ["dotnet", "test", str(root / "source/Copper68k.Tests/Copper68k.Tests.csproj"), "-c", "Release", "--artifacts-path", str(root / "build"), + "--filter", selected, "--logger", "trx;LogFileName=audit.trx", "--results-directory", str(root)] + + +def verify(root, variant, data, retained): + ids = {name: r.hashlib.sha256(value).hexdigest() for name, value in data.items()} + record = load(root / "execution.json") + require(record["sources"] == ids == inventory(root / "source"), "Changed complete candidate sources") + require(record["command"] == command(root, variant) and record["exit"] == (0 if variant == "candidate" else 1), "Wrong command or execution exit") + outputs = ["execution.log", "audit.trx", "build/bin/Copper68k/release/Copper68k.dll", "build/bin/Copper68k.Tests/release/Copper68k.Tests.dll"] + require(set(record["outputs"]) == set(outputs) and record["outputs"] == {p: sha(root / p) for p in outputs}, "Changed execution outputs") + tree = ET.parse(root / "audit.trx"); tests = tree.findall(".//t:UnitTestResult", r.NS) + names = {"DirectFinalWriteControls(batch: False)": "Passed", "DirectFinalWriteControls(batch: True)": "Passed", "ScalarFinalWriteFaults": "Passed" if variant == "candidate" else "Failed", "BatchFinalWriteFaults": "Passed" if variant == "candidate" else "Failed"} + if variant == "candidate": + for name in ["DirectMemoryDestinationControls(batch: False)", "DirectMemoryDestinationControls(batch: True)", "ScalarMemoryDestinationRecovery", "BatchMemoryDestinationRecovery", "LiteralMoveFixturesAndSentinels(batch: False)", "LiteralMoveFixturesAndSentinels(batch: True)", "ScalarOperandReadFaultsRequireFormatB", "BatchOperandReadFaultsRequireFormatB"]: names[name] = "Passed" + require(len(tests) == len(names) and {t.get("testName").split(".")[-1]:t.get("outcome") for t in tests} == names, "Wrong actual candidate outcomes") + counters = tree.find(".//t:Counters", r.NS); failed = sum(outcome == "Failed" for outcome in names.values()) + require(int(counters.get("total")) == len(names) and int(counters.get("failed")) == failed and int(counters.get("passed")) == len(names) - failed, "Wrong candidate counters") + stdout = "\n".join(t.text or "" for t in tree.findall(".//t:UnitTestResult/t:Output/t:StdOut", r.NS)) + totals = dict(passing=0, mismatching=0, unsupported=0, untested=0); rows = [] + for model, kind, mode in itertools.product(r.MODELS, ["controls", "fault"], ["scalar", "batch"]): + group = f"move-final-write-{kind}-{mode}"; p = root / f"{model}-{group}.json"; d = load(p) + expected = {key: {status(key, variant, kind == "fault"):weight} for key, weight in m.expected(model).items()} + counts = {s:sum(row.get(s, 0) for row in expected.values()) for s in totals} + require(d["model"] == model and d["group"] == group and d["logicalCases"] == 768 and d["counts"] == counts and d["combinations"] == expected, "Wrong candidate keys, weights or results") + bad = {key for key, row in expected.items() if "mismatching" in row} + require(len(d["failures"]) == len(bad) and {f["id"] for f in d["failures"]} == bad and all(f["status"] == "mismatching" and f["reason"] for f in d["failures"]), "Wrong mutation witnesses") + reason = {"source-replay": "Final-write recovery repeated a completed source/update or write", "flags": "Final-write format-A PC/SR/SSW/address/output differs", "timing": "Final-write timing policy differs"}.get(variant) + if reason: require(all(f["reason"].startswith(reason) for f in d["failures"]), "Wrong mutation cause") + summary = re.findall(re.escape(model + "/" + group + ": ") + r'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)', stdout) + require(len(summary) == 1 and r.json.loads(summary[0][0]) == counts and int(summary[0][1]) == 768 and int(summary[0][2]) == 768, "Candidate TRX/report disagreement") + for key in totals: totals[key] += counts[key] + rows.append(dict(path=p.name, sha256=sha(p))) + if variant == "candidate": + require(len(retained) == 32, "Missing complete retention selection") + for name, expected in retained.items(): + p = root / name; d = load(p); require(d == expected, "Changed retained results/keys: " + name) + summary = re.findall(re.escape(d["model"] + "/" + d["group"] + ": ") + r'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)', stdout) + require(len(summary) == 1 and r.json.loads(summary[0][0]) == d["counts"] and int(summary[0][1]) == d["logicalCases"] and int(summary[0][2]) == len(d["combinations"]), "Retained TRX/report disagreement") + rows.append(dict(path=name, sha256=sha(p))) + require({p.name for p in root.glob("*.json")} == {row["path"] for row in rows} | {"execution.json"}, "Foreign or missing selected report") + return dict(variant=variant, counts=totals, retentionCases=143360 if variant == "candidate" else 0, executions=len(names), reports=rows, executionSha256=sha(root / "execution.json")) + + +def audit(qualified, discovery, output, validate): + m.audit(qualified, discovery, True) + producers = {p.relative_to(REPO).as_posix(): sha(p) for p in [Path(__file__), DISCOVERY, m.READ_HELPER, REPO / m.FIXTURE, CANDIDATE]} + linkage = dict(producers=producers, discoveryVerificationSha256=sha(discovery / "verification.json"), qualifiedParentProofSha256=sha(qualified / "proof.json"), variants=VARIANTS) + if not validate: + output.mkdir(parents=True, exist_ok=False); save(output / "inputs.json", linkage) + require(load(output / "inputs.json") == linkage, "Wrong producer, parent or complete selection") + retained = {p.name:load(p) for p in (qualified / "candidate").glob("*.json") if p.name.startswith(tuple(model + "-" for model in r.MODELS))} + entries = [] + for variant in VARIANTS: + root = output / variant; data = sources(discovery, variant) + if not validate: + (root / "source").mkdir(parents=True) + for name, value in data.items(): + p = root / "source" / name; p.parent.mkdir(parents=True, exist_ok=True); p.write_bytes(value) + env = dict(os.environ, COPPER68K_RUN_020_MOVE_WRITE_DISCOVERY="1", COPPER68K_RUN_020_OPERAND_READ_DISCOVERY="1", COPPER68K_RUN_020_MEMORY_DESTINATION_RECOVERY="1", COPPER68K_SYNTHETIC_REPORT_DIR=str(root)) + with (root / "execution.log").open("w", encoding="utf-8") as log: result = subprocess.run(command(root, variant), env=env, stdout=log, stderr=subprocess.STDOUT) + outputs = ["execution.log", "audit.trx", "build/bin/Copper68k/release/Copper68k.dll", "build/bin/Copper68k.Tests/release/Copper68k.Tests.dll"] + save(root / "execution.json", dict(command=command(root, variant), exit=result.returncode, sources={name:r.hashlib.sha256(value).hexdigest() for name,value in data.items()}, outputs={p:sha(root/p) for p in outputs})) + entry = verify(root, variant, data, retained); entries.append(entry); print(variant, entry["counts"], "retention", entry["retentionCases"], flush=True) + proof = dict(schema=1, inputsSha256=sha(output / "inputs.json"), entries=entries, sourceCount=233, productionImport=False, publication=False, hardwareQualified=False, roadmapComplete=False) + if validate: require(load(output / "proof.json") == proof, "Changed complete candidate verification") + else: save(output / "proof.json", proof) + print("Complete isolated final-write audit", sha(output / "proof.json"), flush=True) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--qualified-parent-directory", type=Path, required=True) + parser.add_argument("--discovery-directory", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--validate-only", action="store_true") + args = parser.parse_args() + audit(args.qualified_parent_directory.resolve(), args.discovery_directory.resolve(), args.output.resolve(), args.validate_only) From e0cdd4efba163972e1121ed660bdb527c00bcc79 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 01:02:33 +0300 Subject: [PATCH 134/211] Qualify private pending-write software edits and refault protocols --- .../SyntheticM68020MoveWriteHandlerTests.cs | 192 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 74 +++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 ++ .../test-copper68k-final-write-handlers.py | 130 ++++++++++++ 4 files changed, 411 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteHandlerTests.cs create mode 100644 scripts/test-copper68k-final-write-handlers.py diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteHandlerTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteHandlerTests.cs new file mode 100644 index 00000000..aad9ed5d --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteHandlerTests.cs @@ -0,0 +1,192 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Native 030 final-write observation: short format A, completed source/flags, +// following PC, one pending write. Other models remain software discovery. +public sealed class SyntheticM68020MoveWriteHandlerTests(ITestOutputHelper output) +{ + private static readonly ModelSpec[] Models = ModelSpec.All.Where(x => x.Id is "68EC020" or "68020" or "68030" or "A1200").ToArray(); + // Private frame software protocol. This does not qualify hardware trace, + // partial bus cycles or arbitrary handler stack/model changes. + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_HANDLERS", "require private pending-write handler protocols"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarPendingWriteHandlers() => Audit(false); + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_HANDLERS", "require private pending-write handler protocols"), Trait("Suite", "ReferenceDiscovery")] + public void BatchPendingWriteHandlers() => Audit(true); + + private void Audit(bool batch) + { + var failures = new List(); + foreach (var model in Models) + { + var bus = new FaultBus(); var m = new SyntheticMachine(model, bus); + foreach (var width in new[] { 1, 2, 4 }) + foreach (var lane in new[] { "original", "output", "software", "refault", "edited-refault" }) + { + // Keep every possible failure below the shared report's witness cap. + var report = new CoverageBatch(model.Id, $"move-write-handlers-{width}-{lane}-{(batch ? "batch" : "scalar")}"); + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var source in new[] { 0, 7 }) + foreach (var post in new[] { false, true }) + foreach (var alias in new[] { false, true }) + foreach (var vi in Enumerable.Range(0, 4)) + foreach (var ccr in Enumerable.Range(0, 32)) + { + var id = $"{model.Id}/MOVE/write-handler/width={width}/bank={bank}/source={source}/post={post}/alias={alias}/value={vi}/ccr={ccr}/lane={lane}"; + try + { + var mask = width == 1 ? 0xffu : width == 2 ? 0xffffu : uint.MaxValue; + var value = vi switch { 0 => 0u, 1 => 0x80818283u & mask, 2 => mask >> 1, _ => mask }; + var stride = width == 1 && source == 7 ? 2u : (uint)width; + var destination = alias ? 0x4200u + (post ? stride : 0) : 0x4400u; + var opcode = (ushort)((width == 1 ? 0x1090 : width == 2 ? 0x3090 : 0x2090) | source | (post ? 8 : 0) | ((alias ? source : 1) << 9)); + bus.Disarm(); m.Reset(ccr); _ = SyntheticExecution.Prepare(m, [opcode, 0x7e2a, 0x4e71]); + for (uint address = 0x41f8; address < 0x4210; address++) m.InitializePhysical(address, 0xa5, 1); + for (uint address = 0x43f8; address < 0x4410; address++) m.InitializePhysical(address, 0xa5, 1); + m.InitializePhysical(0x4200, value, width); m.InitializePhysical(0x9020, 0x4e73, 2); m.InitializePhysical(0x9022, 0x4e71, 2); + m.Core.State.A[0] = source == 0 ? 0x4200u : 0x4000u; m.Core.State.A[1] = 0x4400; + m.Core.State.SetUserStackPointer(source == 7 && bank is "user" or "user-M" ? 0x4200u : 0x7800u); + m.Core.State.SetInterruptStackPointer(source == 7 && bank == "ISP" ? 0x4200u : 0x4700u); + m.Core.State.SetMasterStackPointer(source == 7 && bank == "MSP" ? 0x4200u : 0x7400u); + var sr = (ushort)(0x700 | ccr | (bank is "ISP" or "MSP" ? 0x2000 : 0) | (bank is "user-M" or "MSP" ? 0x1000 : 0)); + m.Core.State.StatusRegister = sr; m.Core.State.SetActiveStackPointer(source == 7 ? 0x4200u : bank == "MSP" ? 0x7400u : bank == "ISP" ? 0x4700u : 0x7800u); + var e = ArchitecturalExpectation.Capture(m); var serial = m.Core.State.ExceptionSequence; + e.Sr = (ushort)((sr & 0xfff0) | (value == 0 ? 4 : (value & (1u << (width * 8 - 1))) != 0 ? 8 : 0)); + if (post) { e.A[source] += stride; if (source == 7 && bank == "MSP") e.MasterStackPointer = e.A[7]; } + bus.Accesses.Clear(); bus.Observe(destination, width); bus.Arm(destination, width); Step(m, batch); + { + if (bus.Rejected != 1 || m.Core.State.LastExceptionVector != 2 || m.Core.State.ExceptionSequence != serial + 1 || + m.Core.State.ProgramCounter != 0x9020 || m.Core.State.Halted || m.Core.State.Stopped) + throw new InvalidOperationException("Final-write request did not enter vector 2 exactly once"); + var frame = m.Core.State.A[7]; + var expectedFrame = (bank is "ISP" or "MSP" ? e.A[7] : bank == "user-M" ? 0x7400u : 0x4700u) - 32; + if (frame != expectedFrame || bus.SuccessfulSourceReads != 1 || bus.SuccessfulDestinationWrites != 0 || + m.Core.State.D[7] != e.D[7] || m.PeekPhysical(frame + 6, 2) != 0xa008 || m.PeekPhysical(frame, 2) != e.Sr || m.PeekPhysical(frame + 2, 4) != 0x1002 || + (m.PeekPhysical(frame + 10, 2) & 0x1f7) != (0x100u | (width == 1 ? 0x10u : width == 2 ? 0x20u : 0) | ((sr & 0x2000) != 0 ? 5u : 1u)) || + m.PeekPhysical(frame + 16, 4) != destination || (m.PeekPhysical(frame + 24, 4) & mask) != value) + throw new InvalidOperationException("Final-write format-A PC/SR/SSW/address/output differs"); + if (bus.Accesses.Any(a => a.Write && (a.Address < frame || a.Address >= frame + 32))) + throw new InvalidOperationException("Rejected final write reached ordinary memory"); + for (uint n = 0; n < 32; n++) e.Memory[model.Physical(frame + n)] = bus.Peek(model.Physical(frame + n)); + bus.ExcludeFrameReads(frame); e.ExceptionVector = 2; + var pending = value; + if (lane == "output") { pending = value ^ mask; m.InitializePhysical(frame + 24, pending, 4); } + if (lane == "software") + { + // Software completed the write; fixture initialization + // is deliberately separate from CPU bus transactions. + m.InitializePhysical(destination, value, width); + m.InitializePhysical(frame + 10, m.PeekPhysical(frame + 10, 2) & ~0x100u, 2); + } + if (lane == "edited-refault") + { + e.Sr = (ushort)((e.Sr & 0xffe0) | ((ccr + 7) & 31)); + m.InitializePhysical(frame, e.Sr, 2); + // Original SSW FC, independently of returned SR. + m.InitializePhysical(frame + 10, m.PeekPhysical(frame + 10, 2) ^ 4, 2); + } + var pendingSsw = m.PeekPhysical(frame + 10, 2); + if (lane is "refault" or "edited-refault") + { + bus.Arm(destination, width); Step(m, batch); + if (m.Core.State.ExceptionSequence != serial + 2 || m.Core.State.LastExceptionVector != 2 || + m.Core.State.ProgramCounter != 0x9020 || m.Core.State.A[7] != frame || m.Core.State.Halted || m.Core.State.Stopped || + m.PeekPhysical(frame, 2) != e.Sr || m.PeekPhysical(frame + 2, 4) != 0x1002 || + m.PeekPhysical(frame + 10, 2) != pendingSsw || m.PeekPhysical(frame + 16, 4) != destination || + (m.PeekPhysical(frame + 24, 4) & mask) != pending || bus.SuccessfulSourceReads != 1 || bus.SuccessfulDestinationWrites != 0) + throw new InvalidOperationException("Pending-write refault lost returned SR/FC/output or repeated source effects"); + // Check every register at the second fault boundary, + // including source updates and inactive stack banks. + for (var n = 0; n < 8; n++) + if (m.Core.State.D[n] != e.D[n] || (n != 7 && m.Core.State.A[n] != e.A[n])) + throw new InvalidOperationException("Pending-write refault changed completed registers"); + if (m.Core.State.UserStackPointer != (bank is "user" or "user-M" ? e.A[7] : 0x7800u) || + m.Core.State.InterruptStackPointer != (bank is "user" or "ISP" ? frame : 0x4700u) || + m.Core.State.MasterStackPointer != (bank is "user-M" or "MSP" ? frame : 0x7400u) || + m.Core.State.StatusRegister != (ushort)((e.Sr | 0x2000) & ~0xc000)) + throw new InvalidOperationException("Pending-write refault changed stack banks or entry SR"); + } + // Only opaque/private frame bytes may differ. Handler edits + // are included explicitly; unrelated memory stays guarded. + for (uint n = 0; n < 32; n++) e.Memory[model.Physical(frame + n)] = bus.Peek(model.Physical(frame + n)); + Step(m, batch); + if (m.Core.State.ExceptionSequence != serial + (lane is "refault" or "edited-refault" ? 2 : 1)) + throw new InvalidOperationException("Pending-write recovery created an extra exception"); + value = pending; + } + e.Write(destination, value, width, model); e.Pc = 0x1002; Check(m, e); + // Existing approximate execution policy, not physical cycles. + var key = (post, width) switch + { + (false, 1) => M68kInstructionTimingKey.MoveByteAddressIndirectToAddressIndirect, + (false, 2) => M68kInstructionTimingKey.MoveWordAddressIndirectToAddressIndirect, + (false, _) => M68kInstructionTimingKey.MoveLongAddressIndirectToAddressIndirect, + (true, 1) => M68kInstructionTimingKey.MoveBytePostIncrementToAddressIndirect, + (true, 2) => M68kInstructionTimingKey.MoveWordPostIncrementToAddressIndirect, + _ => M68kInstructionTimingKey.MoveLongPostIncrementToAddressIndirect + }; + var plan = ((M68kAdvancedTimingInterpreter)m.Core).Timing.LastInstructionTiming.Plan; + if (plan.Key != key || plan.NativeCycles != 8 || plan.Barriers != M68kTimingBarrier.None || + plan.UsesHeadTail != (model.Id == "68030") || plan.HeadCycles != (model.Id == "68030" ? 1 : 0) || plan.TailCycles != (model.Id == "68030" ? 1 : 0)) + throw new InvalidOperationException($"Final-write timing policy differs: {plan.Key}/{plan.NativeCycles}/{plan.HeadCycles}/{plan.TailCycles}"); + // Frame traffic can overlap A7 source guards; distinguish the + // intended operand widths/value from frame word transfers. + if (bus.SuccessfulSourceReads != 1 || bus.SuccessfulDestinationWrites != (lane == "software" ? 0 : 1) || bus.Rejected != (lane is "refault" or "edited-refault" ? 2 : 1)) + throw new InvalidOperationException("Pending-write handler repeated a completed source/update or write"); + e.D[7] = 42; e.Pc = 0x1004; e.Sr = (ushort)(e.Sr & 0xfff0); Step(m, batch); Check(m, e); + report.Record(id, "passing", null); + } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException) { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + try { report.Complete(output); } catch (Exception ex) { failures.Add(ex); } + } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(x => x.Message))); + } + private static void Check(SyntheticMachine m, ArchitecturalExpectation e) + { var error = e.Verify(m); if (error != null) throw new InvalidOperationException(error); } + private static void Step(SyntheticMachine m, bool batch) + { + if (!batch) { m.Core.ExecuteInstruction(); return; } + var boundary = new Boundary(); if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, boundary) != 1 || boundary.Before != 1 || boundary.After != 1) + throw new InvalidOperationException("Batch boundary differs"); + } + private sealed class Boundary : IM68kInstructionBoundary + { public int Before, After; public bool BeforeInstruction() { Before++; return true; } public void AfterInstruction(long a, long b) => After++; } + private sealed class FaultBus : SparseRecordingBus, IM68kPhysicalAddressMap, IM68kBus + { + private uint? denied; private int width; + internal int Rejected, SuccessfulSourceReads, SuccessfulDestinationWrites; + private uint destination; private int operandWidth; private uint? frameReadStart; + internal void Disarm() { denied = frameReadStart = null; Rejected = SuccessfulSourceReads = SuccessfulDestinationWrites = 0; } + internal void Arm(uint address, int size) { denied = address; width = size; } + public bool IsCpuPhysicalAddressMapped(uint address, int size, M68kBusAccessKind kind) + { + if (denied is not { } at || kind != M68kBusAccessKind.CpuDataWrite || size != width || address != at) return true; + denied = null; Rejected++; return false; + } + // Record actual transport, independently of the mapping query. A denied + // destination must never be dispatched to the ordinary bus. + private void BeforeWrite(uint address, int size, M68kBusAccessKind kind) + { + if (kind != M68kBusAccessKind.CpuDataWrite) return; + if (denied == address && size == width) throw new InvalidOperationException("Denied destination write bypassed physical map"); + if (address == destination && size == operandWidth) SuccessfulDestinationWrites++; + } + internal void Observe(uint address, int size) { destination = address; operandWidth = size; } + internal void ExcludeFrameReads(uint frame) => frameReadStart = frame; + private void BeforeRead(uint address, int size, M68kBusAccessKind kind) + { + if (address is >= 0x41f8 and < 0x4210 && size == operandWidth && kind == M68kBusAccessKind.CpuDataRead && + !(frameReadStart is { } frame && address >= frame && address + (uint)size <= frame + 32)) SuccessfulSourceReads++; + } + byte IM68kBus.ReadByte(uint a, ref long c, M68kBusAccessKind k) { BeforeRead(a, 1, k); return base.ReadByte(a, ref c, k); } + ushort IM68kBus.ReadWord(uint a, ref long c, M68kBusAccessKind k) { BeforeRead(a, 2, k); return base.ReadWord(a, ref c, k); } + uint IM68kBus.ReadLong(uint a, ref long c, M68kBusAccessKind k) { BeforeRead(a, 4, k); return base.ReadLong(a, ref c, k); } + void IM68kBus.WriteByte(uint a, byte v, ref long c, M68kBusAccessKind k) { BeforeWrite(a, 1, k); base.WriteByte(a, v, ref c, k); } + void IM68kBus.WriteWord(uint a, ushort v, ref long c, M68kBusAccessKind k) { BeforeWrite(a, 2, k); base.WriteWord(a, v, ref c, k); } + void IM68kBus.WriteLong(uint a, uint v, ref long c, M68kBusAccessKind k) { BeforeWrite(a, 4, k); base.WriteLong(a, v, ref c, k); } + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 5bbe50fa..2fbcf6b7 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -10568,3 +10568,77 @@ EAs/origins and full integration/isolated consumers remain required before impor Hardware is unavailable; the earlier trace and A7 software disagreements remain open. No production CPU import, package publication or regression retirement occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +### Private pending-write handler protocols — 2026-10-08 + +`SyntheticM68020MoveWriteHandlerTests` adds five deterministic handler operations: +unchanged frame, edited output value, software-completed write with DF clear, +one further denied write, and a refault after editing CCR and the saved function +code independently of returned S/M. The source prefix must execute exactly +once, even with postincrement/A7/aliases. Output editing does not recompute MOVE +flags. Software completion performs no CPU destination write. Both refault +operations must retain following PC, returned SR, saved FC, output/address, +source updates and stack banks at the second exception boundary, then finish +only the pending write. Whole memory, untouched registers, timing policy, +exception counts and the following MOVEQ sentinel are checked as well. + +All **491,520** new cases pass across byte/word/long, A0/A7, +indirect/postincrement, separate/aliased destinations, four stack states/values, +all 32 initial CCR states, scalar/batch and EC020/020/030/A1200 profiles. The +previous **155,648** final-write/read-recovery cases retain exactly their full +report contents: **647,168 logical cases / 14 xUnit executions / 168 reports**, +zero selected mismatches, unsupported cases or unavailable batches. Four sole +defects are detected with exact case identities and full failure details: + +| Defect | Mismatching | Unaffected passing | +| --- | ---: | ---: | +| Ignore DF clear | 98,304 | 393,216 | +| Repeat completed postincrement | 245,760 | 245,760 | +| Recompute returned flags from output | 190,464 | 301,056 | +| Discard saved function code on refault | 98,304 | 393,216 | + +V1 passed its candidate execution, but its first mutation audit was correctly +rejected because shared reports cap failure details at 10,000 witnesses. +V2 splits reports by width and handler operation into 4,096-case groups, +preserving every expected failure detail without changing that shared cap or +weakening verification. V2 also adds explicit stack-bank/entry-SR checks at the +second fault boundary. Only V2 is the current complete proof. + +```powershell +python scripts/test-copper68k-final-write-handlers.py ` + --qualified-parent-directory /audits/MoveFinalWriteCandidateV2 ` + --output +# Recheck the same complete evidence with --validate-only. +``` + +The maintained audit pins and revalidates the complete qualified parent, +copies all 233 parent sources plus the fixture, and verifies complete source +and producer identities, exact commands/outputs, actual TRX rosters/counters/ +stdout, every case key/weight, every mutation witness and all 48 retained +reports. Missing fixtures, empty reports, missing selections, skipped requested +tests and any unexpected outcome fail its gate. +Evidence `audits/MoveWriteHandlersV2/proof.json` SHA-256: +`f63d2ecf21777c2bc8fc259039aa2361bb72165b63b51caefa085b751a9f0674`. +Seven independent copied-variant evidence controls reject missing fixtures, +empty combinations, wrong TRX counters, removed stdout summaries, altered +retained results, unrelated FC failure causes and missing output identities. +The complete original proof then revalidates. Their scoped control record is +`write-handlers-controls-v2.json`. +Separate production-source controls pass 22,528 scenarios / four batches; +six optional private-recovery batches, including the two new methods, remain +explicitly unavailable there. Existing private-fixture compiler warnings are +retained in logs. The candidate CPU is byte-identical to the qualified parent; +this slice adds qualification, not an interpreter correction. +`audits/MoveTransferProductionControlsV4/verification.json` SHA-256: +`ca4445e6735e32f8ab76d3f8343b1b8a40a7c0913fbbcbe9871fb13b300ca43f`. + +These are **private frame-software protocol checks**, not independent native +observations of the new handler edits/refaults or hardware qualification. The +earlier native 030 baseline frame/transfer evidence keeps its original scope. +Saved-pipe lengths/edits, invalid/foreign/moved short frames, entry/load faults, +changed S/M/stacks, trace, broader addressing modes and partial bus cycles +remain unqualified. Independent handler-protocol reference checks, +byte/word memory-destination source-read continuation, read-to-write fault +chains, complete integration and isolated consumers remain required before +import. No production CPU change, publication or regression retirement occurs. +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 0df7cc96..290ed040 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3393,3 +3393,18 @@ before importing this private candidate. Hardware is unavailable, so unresolved reference disagreements remain open. No production CPU/package change or test retirement occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [private final-write implementation and bounded evidence](COPPER68K_REFERENCE_QUALIFICATION.md#private-final-move-write-continuation--2026-10-08). + +Pending-write handler protocols now pass 491,520 all-CCR cases for unchanged/ +edited output, software DF-clear completion, another write denial and refault +after CCR/FC edits, with 155,648 previous recovery cases exactly retained. +Four sole defects are detected, including repeated postincrement, flag +recomputation and lost refault FC. Reports are split below the shared failure +witness cap; a capped V1 mutation audit was rejected rather than accepted with +incomplete detail. Second-fault stack-bank/SR, register/memory, timing, exception +count and sentinel checks pass. Production controls remain passing and optional +private recovery tests remain unavailable there. These checks qualify the +private software protocol; new handler edits/refaults still need independent +reference evidence, alongside the remaining short-frame, source-read/chained, +trace and integration/consumer gates. No production CPU/package change or test +retirement occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. +See [pending-write handler scope and evidence](COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-handler-protocols--2026-10-08). diff --git a/scripts/test-copper68k-final-write-handlers.py b/scripts/test-copper68k-final-write-handlers.py new file mode 100644 index 00000000..8c146388 --- /dev/null +++ b/scripts/test-copper68k-final-write-handlers.py @@ -0,0 +1,130 @@ +"""Audit private pending MOVE writes after software frame edits and refaults. + +Requires a complete final-write candidate audit. Uses only isolated source/build +snapshots; no production import, publication or hardware promotion occurs. +""" +import argparse +import contextlib +import hashlib +import importlib.util +import io +import itertools +import json +import os +from pathlib import Path +import re +import subprocess +import xml.etree.ElementTree as ET + +REPO = Path(__file__).resolve().parents[1] +PARENT_HELPER = REPO / 'scripts/test-copper68k-final-move-write-candidate.py' +FIXTURE = 'Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteHandlerTests.cs' +spec = importlib.util.spec_from_file_location('final_write', PARENT_HELPER) +m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m) +sha, load, save, require, inventory = m.sha, m.load, m.save, m.require, m.inventory +PARENT_PROOF = 'c1a5a156f5922a8d33fe6260eebd6edb61ddba3b309a392337e3772d537445a7' +FILTER = 'FullyQualifiedName~SyntheticM68020MoveWriteHandlerTests' +VARIANTS = ['candidate', 'ignore-df', 'source-update', 'recompute-flags', 'discard-fc'] +LANES = ['original', 'output', 'software', 'refault', 'edited-refault'] + + +def expected(model, variant, width, lane): + rows = {} + for bank, source, post, alias, vi, ccr in itertools.product(['user','user-M','ISP','MSP'], [0,7], [False,True], [False,True], range(4), range(32)): + key = f'{model}/MOVE/write-handler/width={width}/bank={bank}/source={source}/post={post}/alias={alias}/value={vi}/ccr={ccr}/lane={lane}' + original_flags = [4,8,0,8][vi] + mismatch = variant == 'ignore-df' and lane == 'software' or variant == 'source-update' and post or variant == 'discard-fc' and lane == 'edited-refault' or variant == 'recompute-flags' and (lane == 'output' or lane == 'edited-refault' and (ccr + 7) % 16 != original_flags) + rows[key] = {'mismatching' if mismatch else 'passing':1} + return rows + + +def sources(parent, variant): + data = {name:(parent/'candidate/source'/name).read_bytes() for name in inventory(parent/'candidate/source')} + require(len(data) == 233, 'Incomplete final-write parent sources') + data[FIXTURE] = (REPO/FIXTURE).read_bytes() + text = data[m.NEW].decode('utf-8').replace('\r\n','\n') + pending = 'if ((ssw & 0x100) != 0) WriteFinalMoveDestination020(address, value, size);' + if variant == 'ignore-df': text = m.once(text, pending, 'WriteFinalMoveDestination020(address, value, size);') + elif variant == 'source-update': text = m.once(text, pending, 'if (((opcode >> 3) & 7) == 3) WriteGeneralRegister(true, opcode & 7, unchecked(State.A[opcode & 7] + M68kIntegerSemantics.AddressIncrement(opcode & 7, size)));\n '+pending) + elif variant == 'recompute-flags': text = m.once(text, pending, 'SetMoveFlags(value, size);\n '+pending) + elif variant == 'discard-fc': text = m.once(text, '_restoredWriteFunctionCode020 = (ushort)(ssw & 7);', '_restoredWriteFunctionCode020 = null;') + data[m.NEW] = text.encode('utf-8') + return data + + +def command(root, variant): + selected = FILTER + ('|'+'|'.join([m.m.FILTER]+m.RETENTION) if variant == 'candidate' else '') + return ['dotnet','test',str(root/'source/Copper68k.Tests/Copper68k.Tests.csproj'),'-c','Release','--artifacts-path',str(root/'build'),'--filter',selected,'--logger','trx;LogFileName=audit.trx','--results-directory',str(root)] + + +def verify(root, variant, data, retained): + ids={name:hashlib.sha256(value).hexdigest() for name,value in data.items()} + record=load(root/'execution.json') + require(record['sources'] == ids == inventory(root/'source'), 'Changed complete handler sources') + require(record['command'] == command(root,variant) and record['exit'] == (0 if variant == 'candidate' else 1), 'Wrong handler command or exit') + outputs=['execution.log','audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + require(set(record['outputs']) == set(outputs) and record['outputs'] == {p:sha(root/p) for p in outputs}, 'Changed handler execution outputs') + tree=ET.parse(root/'audit.trx');tests=tree.findall('.//t:UnitTestResult',m.r.NS) + names={'ScalarPendingWriteHandlers':'Passed' if variant == 'candidate' else 'Failed', 'BatchPendingWriteHandlers':'Passed' if variant == 'candidate' else 'Failed'} + if variant == 'candidate': + for name in ['DirectFinalWriteControls(batch: False)','DirectFinalWriteControls(batch: True)','ScalarFinalWriteFaults','BatchFinalWriteFaults','DirectMemoryDestinationControls(batch: False)','DirectMemoryDestinationControls(batch: True)','ScalarMemoryDestinationRecovery','BatchMemoryDestinationRecovery','LiteralMoveFixturesAndSentinels(batch: False)','LiteralMoveFixturesAndSentinels(batch: True)','ScalarOperandReadFaultsRequireFormatB','BatchOperandReadFaultsRequireFormatB']: names[name]='Passed' + require(len(tests) == len(names) and {t.get('testName').split('.')[-1]:t.get('outcome') for t in tests} == names, 'Wrong actual handler roster/outcomes') + counters=tree.find('.//t:Counters',m.r.NS);failed=sum(outcome == 'Failed' for outcome in names.values()) + require(int(counters.get('total')) == len(names) and int(counters.get('passed')) == len(names)-failed and int(counters.get('failed')) == failed, 'Wrong handler TRX counters') + stdout='\n'.join(t.text or '' for t in tree.findall('.//t:UnitTestResult/t:Output/t:StdOut',m.r.NS)) + rows=[];totals=dict(passing=0,mismatching=0,unsupported=0,untested=0) + def summary(d): + matches=re.findall(re.escape(d['model']+'/'+d['group']+': ')+r'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)',stdout) + require(len(matches) == 1 and json.loads(matches[0][0]) == d['counts'] and int(matches[0][1]) == d['logicalCases'] and int(matches[0][2]) == len(d['combinations']), 'Handler TRX/report disagreement') + for model,width,lane,mode in itertools.product(m.r.MODELS,[1,2,4],LANES,['scalar','batch']): + group=f'move-write-handlers-{width}-{lane}-{mode}' + p=root/f'{model}-{group}.json';d=load(p);wanted=expected(model,variant,width,lane) + counts={s:sum(row.get(s,0) for row in wanted.values()) for s in totals} + require(d['model'] == model and d['group'] == group and d['logicalCases'] == 4096 and d['counts'] == counts and d['combinations'] == wanted, 'Wrong handler keys, weights or results') + bad={key for key,row in wanted.items() if 'mismatching' in row} + require(len(d['failures']) == len(bad) and {f['id'] for f in d['failures']} == bad and all(f['status'] == 'mismatching' and f['reason'] for f in d['failures']), 'Wrong handler mutation witnesses') + if variant == 'discard-fc': require(all(f['reason'].startswith('Pending-write refault lost returned SR/FC/output or repeated source effects') for f in d['failures']), 'Wrong FC mutation cause') + if variant == 'ignore-df': require(all(f['reason'].startswith('Pending-write handler repeated a completed source/update or write') for f in d['failures']), 'Wrong DF mutation cause') + summary(d) + for s in totals:totals[s]+=counts[s] + rows.append(dict(path=p.name,sha256=sha(p))) + if variant == 'candidate': + require(len(retained) == 48, 'Incomplete retained handler selection') + for name,wanted in retained.items(): + p=root/name;d=load(p);require(d == wanted, 'Changed complete retained handler results: '+name);summary(d);rows.append(dict(path=name,sha256=sha(p))) + require({p.name for p in root.glob('*.json')} == {row['path'] for row in rows}|{'execution.json'}, 'Missing or foreign handler reports') + return dict(variant=variant,counts=totals,retentionCases=155648 if variant == 'candidate' else 0,executions=len(names),reports=rows,executionSha256=sha(root/'execution.json')) + + +def audit(parent,output,validate): + require(sha(parent/'proof.json') == PARENT_PROOF, 'Wrong qualified final-write proof') + inputs=load(parent/'inputs.json') + # The discovery and its qualified parent must accompany the parent evidence. + discovery=parent.parent/'MoveFinalWriteDiscoveryV3';qualified=parent.parent/'MemoryDestinationPrivateV4' + with contextlib.redirect_stdout(io.StringIO()):m.audit(qualified,discovery,parent,True) + producers={p.relative_to(REPO).as_posix():sha(p) for p in [Path(__file__),PARENT_HELPER,REPO/FIXTURE]} + linkage=dict(producers=producers,parentProofSha256=PARENT_PROOF,parentInputsSha256=sha(parent/'inputs.json'),variants=VARIANTS) + if not validate:output.mkdir(parents=True,exist_ok=False);save(output/'inputs.json',linkage) + require(load(output/'inputs.json') == linkage, 'Wrong handler producer, parent or selection') + retained={p.name:load(p) for p in (parent/'candidate').glob('*.json') if p.name.startswith(tuple(model+'-' for model in m.r.MODELS))} + entries=[] + for variant in VARIANTS: + root=output/variant;data=sources(parent,variant) + if not validate: + (root/'source').mkdir(parents=True) + for name,value in data.items(): + p=root/'source'/name;p.parent.mkdir(parents=True,exist_ok=True);p.write_bytes(value) + env=dict(os.environ,COPPER68K_RUN_020_MOVE_WRITE_HANDLERS='1',COPPER68K_RUN_020_MOVE_WRITE_DISCOVERY='1',COPPER68K_RUN_020_OPERAND_READ_DISCOVERY='1',COPPER68K_RUN_020_MEMORY_DESTINATION_RECOVERY='1',COPPER68K_SYNTHETIC_REPORT_DIR=str(root)) + with (root/'execution.log').open('w',encoding='utf-8') as log:result=subprocess.run(command(root,variant),env=env,stdout=log,stderr=subprocess.STDOUT) + outputs=['execution.log','audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + save(root/'execution.json',dict(command=command(root,variant),exit=result.returncode,sources={name:hashlib.sha256(value).hexdigest() for name,value in data.items()},outputs={p:sha(root/p) for p in outputs})) + entry=verify(root,variant,data,retained);entries.append(entry);print(variant,entry['counts'],'retention',entry['retentionCases'],flush=True) + proof=dict(schema=1,inputsSha256=sha(output/'inputs.json'),entries=entries,sourceCount=234,productionImport=False,publication=False,hardwareQualified=False,roadmapComplete=False) + if validate:require(load(output/'proof.json') == proof,'Changed complete handler verification') + else:save(output/'proof.json',proof) + print('Complete isolated pending-write handler audit',sha(output/'proof.json'),flush=True) + + +if __name__ == '__main__': + parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--qualified-parent-directory',type=Path,required=True);parser.add_argument('--output',type=Path,required=True);parser.add_argument('--validate-only',action='store_true');args=parser.parse_args() + audit(args.qualified_parent_directory.resolve(),args.output.resolve(),args.validate_only) From 361ea5a4feae07f9476e71fcd685225a112cf6f8 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 01:24:43 +0300 Subject: [PATCH 135/211] Audit native pending-write handlers and isolate refault PC disagreement --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 88 +++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 18 ++ scripts/reference/m68030-write-handlers.cpp | 244 ++++++++++++++++++ scripts/test-copper68k-030-write-handlers.py | 68 +++++ 4 files changed, 418 insertions(+) create mode 100644 scripts/reference/m68030-write-handlers.cpp create mode 100644 scripts/test-copper68k-030-write-handlers.py diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 2fbcf6b7..ca0b1c92 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -10642,3 +10642,91 @@ byte/word memory-destination source-read continuation, read-to-write fault chains, complete integration and isolated consumers remain required before import. No production CPU change, publication or regression retirement occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +### Native pending-write handler comparison — 2026-10-08 + +`scripts/test-copper68k-030-write-handlers.py` executes **61,440** native 030 +programs using the same pristine WinUAE commit +`5d22d33632646efc3f747f03e82d28353e52722e`. Generated MOVE/RTE/MOVEQ, access, +frame construction, fixup and retry-loop fragments remain unchanged. The native +`unalign_clear` helper is now extracted unchanged for software DF-clear handling; +it is not replaced by a no-op. Width constants are independently checked against +the pinned definitions. Host handler edits are explicit fixture writes; they +do not count as CPU transfers. For an FC-bearing logical denial during native +RTE, the adapter supplies the received FC to native `mmu030_page_fault`. +This tests software state/recovery, without enabling address translation or +emulating physical function-code spaces. + +The three non-refault operations agree with the private protocol: +**12,288 unchanged-frame / 12,288 output-edit / 12,288 DF-clear programs pass**. +Each covers all widths, A0/A7, indirect/postincrement, separate/aliased +destinations, four stack states/values and all initial CCRs. Checks include +original frame fields, exact reads/write attempts/writes, final registers/banks, +flags before and after the sentinel, whole memory and event order. Native output +edits change the pending data without recomputing returned flags. DF-clear +software completion emits no CPU destination write. + +Both repeated-write-fault operations disagree: **24,576 comparisons mismatch**. +The second short frame saves the RTE handler PC `5000`, whereas the private +protocol expects restored continuation PC `1002`. Native code preserves the +pending address/output/returned SR/FC, completes the write once on the next RTE, +then returns to `5000` and attempts another RTE. The original MOVEQ sentinel is +not reached. The observer records the subsequent unsupported exception path or +uninitialized frame read; it does not pretend that downstream execution completed. +The primary second-frame PC mismatch is observed before that downstream limitation. +These are private-protocol disagreements, not an architectural verdict that either +implementation is correct. No affected case is excluded or relabelled passing. + +The retained trace exposes the cause: normal generated MOVE sets +`regs.instruction_pc` to the following PC before its final write; native RTE +restores `regs.pc` before the pending transfer but leaves the fault-origin PC at +the handler address. The retry-loop fault catch then restores that handler PC, +which native exception construction saves. One isolated **adapter-state +intervention**, setting the fault-origin PC from the stacked continuation before +calling native RTE, makes all 61,440 comparisons pass, retaining all 36,864 +previously passing cases. Every extracted native fragment stays byte-identical. +This is causal diagnostic evidence, not a patched reference oracle, hardware +qualification or a proposed production correction. Two negative adapter defects +independently make all 12,288 DF-clear or all 12,288 output-edit cases fail, +retaining 24,576 passing cases and the original 24,576 refault disagreements. + +The [MC68030 manual, sections 8.1.2, 8.1.13 and 8.2.3](https://www.nxp.com/docs/en/reference-manual/MC68030UM-P2.pdf) +distinguishes faults while reading a restoration image from faults while +rerunning its pending cycle. For the latter it requires deallocation of the +old frame followed by a new exception frame. This supports the tested stack +transition, but the cited description alone does not resolve the precise saved-PC +and internal continuation-state disagreement. Hardware remains unavailable; +further independent evidence is required before architectural promotion. + +```powershell +python scripts/test-copper68k-030-write-handlers.py ` + --reference-directory ` + --output +# Ordinary execution and --validate-only fail the disagreement gate. +# Add --discovery-only to explicitly record the same failing observations. +``` + +The audit checks every deterministic case/field and separately records each +difference. It pins producers, reference inputs, all extracted native fragments, +source/binary/build/log identities and verification contents. Validation executes +the frozen observer again and requires identical rows and trace. Eight independent +corrupted-evidence controls reject missing fixtures/producers/output identities, +changed observer/native helper with updated manifests, empty rows, altered +refault PC with an updated manifest, and a false agreement flag. Original frozen +replay matches; the ordinary agreement gate correctly returns failure. + +Evidence `reference/WriteHandlers030V1/verification.json` SHA-256: +`f1fc9bf9408eff4ab916749546463d4ee0b7a83913c197a9f66af4124202c1c0`. +Diagnostics `native-write-handlers-diagnostics-v1.json` SHA-256: +`dba067fb90b1e0dfcea123d4ef9c9e504d599ce0d3dcdc1fabfa441444f24e13`. +Combined `native-write-handlers-proof-v1.json` SHA-256: +`99e875fa065af5f9a63fe37362f5019d3d72b3086196c968e7399e871ee88ff1`. +Existing native compiler warnings are retained in logs. These observations do +not qualify other models, changed S/M/stacks, trace/interrupts, physical timing, +enabled MMU, partial transfers, other origins or broader short-frame protocols. +The private 647,168-case proof retains its software-contract scope; refaults are +not independently architecturally qualified. Byte/word source-read destination +continuation, read-to-write chains, saved-pipe/frame/fault variants and complete +integration/isolated consumers remain required. No production or private CPU +source correction, package publication or regression retirement occurs. +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 290ed040..6a0a8451 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3408,3 +3408,21 @@ reference evidence, alongside the remaining short-frame, source-read/chained, trace and integration/consumer gates. No production CPU/package change or test retirement occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See [pending-write handler scope and evidence](COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-handler-protocols--2026-10-08). + +The pinned native 030 observer now independently agrees with unchanged frames, +edited output and DF-clear completion: 36,864 programs pass. All 24,576 refault +comparisons remain mismatching because the native short frame saves the RTE +handler PC and subsequent recovery does not reach the original continuation. +One adapter-state intervention isolates the fault-PC origin and makes all 61,440 +software comparisons pass without changing native fragments; this diagnostic +does not establish hardware behavior or an authoritative CPU correction. Negative +DF/output defects are detected, eight evidence corruptions are rejected, and +frozen rows/trace replay exactly. The ordinary agreement gate correctly fails; +discovery-only records the failures without promotion. Manual recovery/deallocation +rules do not by themselves settle the saved-PC/internal-state distinction. +Hardware is unavailable. New external evidence qualifies only the stated 030 +non-refault software scope; the private refault protocol remains unqualified +architecturally. Broader source-read/chained, short-frame, trace, integration and +consumer requirements remain open. No CPU/package change or test retirement +occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the +[native handler agreement, disagreement and causal diagnostic](COPPER68K_REFERENCE_QUALIFICATION.md#native-pending-write-handler-comparison--2026-10-08). diff --git a/scripts/reference/m68030-write-handlers.cpp b/scripts/reference/m68030-write-handlers.cpp new file mode 100644 index 00000000..34d02404 --- /dev/null +++ b/scripts/reference/m68030-write-handlers.cpp @@ -0,0 +1,244 @@ +// Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT +// 030 software discovery: private pending-write handler protocol comparison. +// Unchanged generated operations, fixup helpers and retry loop; flat physical +// transport. Observe indirect/postincrement source and aliased destinations. +// Trace is disabled. No enabled translation, IRQ, cache/pipeline timing, +// physical function-code spaces or architectural changed-bank qualification. +#include +#include +#include +#include +#include +#include +#include +#include +#include +using uae_u8=uint8_t;using uae_u16=uint16_t;using uae_u32=uint32_t; +using uae_s8=int8_t;using uae_s16=int16_t;using uae_s32=int32_t;using uaecptr=uint32_t; +#define REGPARAM2 +#define STATIC_INLINE static inline +#define CYCLE_UNIT 2 +#define _T(x) x +#define MMUDEBUG 0 +#define TRY(x) try +#define CATCH(x) catch(int x) +#define ENDTRY +#define STOPTRY +#define THROW(x) throw int(x) +#define SPCFLAG_TRACE 0x40 +#define SPCFLAG_DOTRACE 0x80 +#define SPCFLAG_TRAP 0x400 +#define SPCFLAG_INT 8 +#define SPCFLAG_DOINT 0x100 +#define CPU_HALT_CPU_STUCK 1 +#define CPU_HALT_DOUBLE_FAULT 2 +#define sz_byte 0 +#define sz_word 1 +#define sz_long 2 +#define m68k_areg(r,n) ((r).a[n]) +#define m68k_dreg(r,n) ((r).d[n]) +struct Regs { + uae_u32 a[8]{},d[8]{},usp{},isp{},msp{},pc{},instruction_pc{},trace_pc{},vbr{},mmu_ssw{},mmu_fault_addr{},sfc{},dfc{},wb3_data{},wb2_address{},fp_ea{},fp_opword{},mmu_effective_addr{},pcr{},fp_unimp_ins{},ir{}; + uae_u16 sr{};int m{},s{},t0{},t1{},intmask{},ipl[1]{},ipl_pin{},stopped{},spcflags{},opcode{},irc{},instruction_cnt{},wb2_status{},wb3_status{},read_buffer{},write_buffer{},fpu_exp_pre{},fp_unimp_pend{}; + int prefetch020_valid[3]{},prefetch020[3]{},pipeline_r8[2]{},pipeline_pos{},pipeline_stop{}; +}regs; +struct Prefs{int cpu_model=68030,mmu_model=68030,cachesize{},cpu_compatible{},cpu_cycle_exact{},fpu_model{};}currprefs; +struct flag_struct{int cznv{},x{};}regflags; +int xflag,nflag,zflag,vflag,cflag; +#define SET_XFLG(x) (xflag=(x)) +#define SET_NFLG(x) (nflag=(x)) +#define SET_ZFLG(x) (zflag=(x)) +#define SET_VFLG(x) (vflag=(x)) +#define SET_CFLG(x) (cflag=(x)) +#define GET_XFLG() xflag +#define GET_NFLG() nflag +#define GET_ZFLG() zflag +#define GET_VFLG() vflag +#define GET_CFLG() cflag +#define CLEAR_CZNV() (cflag=zflag=nflag=vflag=0) +bool m68k_accurate_ipl=false,mmu_debugger=false,ismoves030=false,islrmw030=false,bBusErrorReadWrite=false,mmu030_retry=false; +int mmu030_idx,mmu030_idx_done,mmu030_opcode,mmu030_opcode_stageb,mmu030_fake_prefetch,cpu_cycles; +uaecptr mmu030_fake_prefetch_addr,mm030_stageb_address,last_fault_for_exception_3; +uae_u16 mmu030_state[3];uae_u32 mmu030_data_buffer_out,mmu030_disp_store[2],mmu030_fmovem_store[2],mmu040_move16[4]; +struct mmu030_access{uae_u32 val;};mmu030_access mmu030_ad[10]; +struct mmufixup{int reg=-1;uae_u32 value{};};mmufixup mmufixup[2]; +std::unordered_map mem; +std::vector events;std::vector originalFrame,editedFrame; +bool operandContext=false;bool faultOnce;int attempts,reads,steps,traceCount;uae_u32 tracePc,traceD7;int returnT1; +int activeCase=-1;int handlerLane,exceptions,retryFc;bool handlerEdited;std::vector> frames;std::vector frameAddresses; +int writes,moveCcr,writeAttempts,operandWidth,faultOrigin;uaecptr writeAddress,faultFrame;uae_u32 writeValue; +#include "stride.inc" +std::ofstream diagnostics("trace.log"); +void snapshot(const char* phase){diagnostics<second;}return v; +} +void put(uaecptr a,uae_u32 v,int w){for(int n=0;n>(8*(w-1-n)));} +uae_u32 operand_read(uaecptr a,int width){ + if(a!=0x4200||width!=operandWidth)unavailable();attempts++; + if(faultOnce&&faultOrigin==1){faultOnce=false;events.push_back(1);mmu030_hardware_bus_error(a,0,true,false,width==1?sz_byte:width==2?sz_word:sz_long);} + reads++;events.push_back(4);return read(a,width); +} +void operand_write(uaecptr a,uae_u32 value,int width,int explicitFc=-1){ + if(width!=operandWidth)unavailable();writeAttempts++; + const uae_u32 mask=width==1?0xffu:width==2?0xffffu:0xffffffffu;value&=mask; + if(faultOnce&&faultOrigin==2){ + faultOnce=false;events.push_back(9); + if(explicitFc>=0){ + // Logical FC-bearing denial. Execute the reference's native page-fault + // helper with the FC passed by native RTE; no enabled translation or + // physical address-space emulation is claimed by this adapter. + mmu030_page_fault(a,false,width==1?0x10:width==2?0x20:0,explicitFc); + } + mmu030_hardware_bus_error(a,value,false,false,width==1?sz_byte:width==2?sz_word:sz_long); + } + if(++writes!=1)unavailable();writeAddress=a;writeValue=value;events.push_back(8);put(a,value,width); +} +uae_u32 uae_mmu030_get_long(uaecptr a){return operandContext?operand_read(a,4):read(a,4);} +uae_u32 uae_mmu030_get_word(uaecptr a){return operandContext?operand_read(a,2):read(a,2);} +uae_u32 uae_mmu030_get_byte(uaecptr a){return operandContext?operand_read(a,1):read(a,1);} +uae_u32 uae_mmu030_get_iword(uaecptr a){return read(a,2);} +void uae_mmu030_put_word(uaecptr a,uae_u32 v){if(operandContext)operand_write(a,v,2);else put(a,v,2);} +void uae_mmu030_put_long(uaecptr a,uae_u32 v){if(!operandContext)unavailable();operand_write(a,v,4);} +void uae_mmu030_put_byte(uaecptr a,uae_u32 v){if(!operandContext)unavailable();operand_write(a,v,1);} +uae_u32 get_word_mmu030(uaecptr a){return read(a,2);}uae_u32 get_long_mmu030(uaecptr a){return read(a,4);} +uae_u32 x_get_long(uaecptr a){return read(a,4);}void x_put_long(uaecptr a,uae_u32 v){put(a,v,4);}void x_put_word(uaecptr a,uae_u32 v){put(a,v,2);} +void fc_check(int fc){if(fc!=1&&fc!=5)unavailable();} +uae_u32 uae_mmu030_get_long_fcx(uaecptr a,int fc){fc_check(fc);return operand_read(a,4);} +uae_u32 uae_mmu030_get_word_fcx(uaecptr a,int fc){fc_check(fc);return operand_read(a,2);} +uae_u32 uae_mmu030_get_byte_fcx(uaecptr a,int fc){fc_check(fc);return operand_read(a,1);} +void uae_mmu030_put_long_fcx(uaecptr a,uae_u32 v,int fc){fc_check(fc);retryFc=fc;operand_write(a,v,4,fc);} +void uae_mmu030_put_word_fcx(uaecptr a,uae_u32 v,int fc){fc_check(fc);retryFc=fc;operand_write(a,v,2,fc);} +void uae_mmu030_put_byte_fcx(uaecptr a,uae_u32 v,int fc){fc_check(fc);retryFc=fc;operand_write(a,v,1,fc);} +uae_u32 mmu030_get_generic(uaecptr,uae_u32,int,int){unavailable();}void mmu030_put_generic(uaecptr,uae_u32,uae_u32,int,int){unavailable();} +templatevoid mmu030_unaligned_read_continue(uaecptr,int,F){unavailable();}templatevoid mmu030_unaligned_write_continue(uaecptr,int,F){unavailable();} +static void unalign_clear(void);void exception3_read_prefetch(uae_u32,uaecptr){unavailable();} +void exception3_read_special(uae_u32,uaecptr,int,int){unavailable();} +void write_log(const char* format,...){char buffer[512];va_list args;va_start(args,format);vsnprintf(buffer,sizeof(buffer),format,args);va_end(args);diagnostics<>12)==10?32:92;for(int n=0;n32)throw std::runtime_error("Bound exceeded");return read(regs.pc+n,2);} +int do_specialties(int){ + const int spcflags=regs.spcflags; + if(spcflags&~(SPCFLAG_TRACE|SPCFLAG_DOTRACE))unavailable(); + // Exact trace fragment of reference do_specialties; IRQ/host branches excluded. +#include "special-trace.inc" + return regs.pc==0x6000||regs.pc==0x1006; +} +uae_u32 dispatch(uae_u32 op){ + if(op==0x3ebc){events.push_back(3);auto v=op_30bc_32_ff(op);editedFrame.clear();for(size_t n=0;n(n)));return v;} + if(op==0x4e73){ + if(!handlerEdited){ + handlerEdited=true;events.push_back(3);const uaecptr frame=regs.a[7]; + const auto mask=operandWidth==1?0xffu:operandWidth==2?0xffffu:0xffffffffu; + if(handlerLane==1)put(frame+24,read(frame+24,4)^mask,4); + if(handlerLane==2){put(read(frame+16,4),read(frame+24,4),operandWidth);put(frame+10,read(frame+10,2)&~0x100u,2);} + if(handlerLane==4){put(frame,(read(frame,2)&0xffe0u)|((returnT1+7)&31),2);put(frame+10,read(frame+10,2)^4,2);} + if(handlerLane==3||handlerLane==4)faultOnce=true; + editedFrame.clear();for(int n=0;n<32;n++)editedFrame.push_back(mem.at(frame+n)); + } + events.push_back(5);snapshot("before-RTE");auto value=op_4e73_32_ff(op);snapshot("after-RTE");return value; + } + if((op&0xf1f0)==0x1090||(op&0xf1f0)==0x2090||(op&0xf1f0)==0x3090){ + snapshot("before-MOVE");operandContext=true; + try{uae_u32 value; + if((op>>12)==1)value=(op&8)?op_1098_32_ff(op):op_1090_32_ff(op); + else if((op>>12)==3)value=(op&8)?op_3098_32_ff(op):op_3090_32_ff(op); + else value=(op&8)?op_2098_32_ff(op):op_2090_32_ff(op); + operandContext=false; + moveCcr=(GET_XFLG()?16:0)|(GET_NFLG()?8:0)|(GET_ZFLG()?4:0)|(GET_VFLG()?2:0)|(GET_CFLG()?1:0); + snapshot("after-MOVE");return value; + }catch(...){operandContext=false;throw;} + } + if(op==0x7e2a){ + if(faultOrigin==2)moveCcr=(GET_XFLG()?16:0)|(GET_NFLG()?8:0)|(GET_ZFLG()?4:0)|(GET_VFLG()?2:0)|(GET_CFLG()?1:0); + events.push_back(6);return op_7000_32_ff(op); + } + if(op==0x4e71)return op_4e71_32_ff(op); + unavailable(); +} +#include "loop.inc" + + +int main(){try{ + int id=0; + for(int width:{1,2,4})for(int bank=0;bank<4;bank++)for(int source:{0,7})for(int post=0;post<2;post++)for(int alias=0;alias<2;alias++) + for(int vi=0;vi<4;vi++)for(int ccr=0;ccr<32;ccr++)for(int lane=0;lane<5;lane++){ + activeCase=id;operandWidth=width;faultOrigin=2;handlerLane=lane;handlerEdited=false;exceptions=0;retryFc=-1;returnT1=ccr; + const uae_u32 mask=width==1?0xffu:width==2?0xffffu:0xffffffffu; + const uae_u32 value=vi==0?0:vi==1?(0x80818283&mask):vi==2?(mask>>1):mask; + const uae_u32 pending=lane==1?value^mask:value; + const uae_u32 stride=width==1&&source==7?2:static_cast(width); + const int originalCcr=(ccr&16)|(value==0?4:0)|(value&(1u<<(width*8-1))?8:0); + const int returnedCcr=lane==4?(ccr+7)&31:originalCcr; + regs={};mem.clear();events.clear();originalFrame.clear();editedFrame.clear();frames.clear();frameAddresses.clear();faultOnce=true;attempts=reads=steps=traceCount=0;tracePc=traceD7=0;regflags={};xflag=nflag=zflag=vflag=cflag=0; + mmu030_state[0]=mmu030_state[1]=mmu030_state[2]=0;mmu030_idx=mmu030_idx_done=0;mmu030_retry=false;mmu030_data_buffer_out=0;mmufixup[0].reg=mmufixup[1].reg=-1; + writes=moveCcr=writeAttempts=0;writeAddress=writeValue=0; + xflag=(ccr>>4)&1;nflag=(ccr>>3)&1;zflag=(ccr>>2)&1;vflag=(ccr>>1)&1;cflag=ccr&1; + regs.usp=0x7000;regs.isp=0x8000;regs.msp=0x9000; + if(source==7){if(bank<2)regs.usp=0x4200;else if(bank==2)regs.isp=0x4200;else regs.msp=0x4200;} + regs.s=bank>=2;regs.m=bank==1||bank==3;regs.intmask=7; + regs.a[7]=source==7?0x4200:bank<2?0x7000:bank==2?0x8000:0x9000; + regs.a[0]=source==0?0x4200:0x4000;regs.a[1]=0x4400;regs.d[0]=0x12345678;regs.pc=regs.instruction_pc=0x1000; + const auto opcode=(width==1?0x1090:width==2?0x3090:0x2090)|(post?8:0)|source|((alias?source:1)<<9); + put(0x1000,opcode,2);put(0x1002,0x7e2a,2);put(0x1004,0x4e71,2); + for(int n=-8;n<16;n++){put(0x4200+n,0xa5,1);put(0x4400+n,0xa5,1);} + put(0x4200,value,width);put(8,0x5000,4);put(36,0x6000,4);put(0x5000,0x4e73,2); + auto expectedMemory=mem;for(auto& p:cpufunctbl)p=dispatch; + bool completed=false;std::string error="none"; + try{m68k_run_mmu030();completed=regs.pc==0x1006;}catch(const Done&){completed=true;}catch(const std::exception&e){error=e.what();}catch(int x){error="uncaught-native-"+std::to_string(x);} + for(char&c:error)if(c==' '||c=='\n'||c=='\r')c='_'; + const uaecptr destination=alias?0x4200u+(post?stride:0u):0x4400u; + const uae_u32 sourceExpected=0x4200u+(post?stride:0u); + const uae_u32 stackExpected=source==7?sourceExpected:bank<2?0x7000u:bank==2?0x8000u:0x9000u; + const uae_u32 bankBits=0x700u|(bank>=2?0x2000u:0u)|((bank==1||bank==3)?0x1000u:0u); + const uae_u32 originalSsw=0x100u|(width==1?0x10u:width==2?0x20u:0u)|(bank>=2?5u:1u); + auto field=[&](const std::vector& f,int at,int w){uae_u32 v=0;if(f.size()(at+w))return 0xffffffffu;for(int n=0;n=1&&frames[0].size()==32&&field(frames[0],0,2)==(bankBits|originalCcr)&&field(frames[0],2,4)==0x1002&&field(frames[0],6,2)==0xa008&&(field(frames[0],10,2)&0x1f7)==originalSsw&&field(frames[0],16,4)==destination&&(field(frames[0],24,4)&mask)==value; + bool second=lane<3?frames.size()==1:frames.size()==2&&frames[1].size()==32&&field(frames[1],0,2)==(bankBits|returnedCcr)&&field(frames[1],2,4)==0x1002&&field(frames[1],6,2)==0xa008&&(field(frames[1],10,2)&0x1f7)==(originalSsw^(lane==4?4u:0u))&&field(frames[1],16,4)==destination&&(field(frames[1],24,4)&mask)==pending&&frameAddresses[0]==frameAddresses[1]; + const int finalCcr=(GET_XFLG()?16:0)|(GET_NFLG()?8:0)|(GET_ZFLG()?4:0)|(GET_VFLG()?2:0)|(GET_CFLG()?1:0); + bool registers=regs.d[0]==0x12345678&®s.d[7]==42&®s.a[source]==sourceExpected&®s.a[1]==0x4400&®s.a[7]==stackExpected&®s.s==(bank>=2)&®s.m==(bank==1||bank==3)&&!regs.t0&&!regs.t1; + if((bank>=2&®s.usp!=0x7000)||(bank!=2&®s.isp!=0x8000)||(bank!=3&®s.msp!=0x9000))registers=false; + for(int n=1;n<7;n++)if(regs.d[n]!=0)registers=false;for(int n=2;n<7;n++)if(regs.a[n]!=0)registers=false; + // Capture only actual allowed frame bytes plus the explicitly edited output. + for(size_t i=0;i(n)]=mem.at(frameAddresses[i]+static_cast(n)); + for(int n=0;n(n)]=uae_u8(pending>>(8*(width-1-n))); + bool memory=mem==expectedMemory; + bool transfers=reads==1&&attempts==1&&writes==(lane==2?0:1)&&writeAttempts==(lane==2?1:lane>=3?3:2)&&exceptions==(lane>=3?2:1)&&read(destination,width)==pending; + bool flags=moveCcr==returnedCcr&&finalCcr==(returnedCcr&16); + bool agreement=completed&&error=="none"&&first&&second&®isters&&memory&&transfers&&flags&&traceCount==0; + const auto last=frames.empty()?std::vector{}:frames.back(); + std::cout<<"id="<>1,mask][vi];pending=value^mask if lane==1 else value + stride=2 if width==1 and source==7 else width;address=0x4200+post*stride if alias else 0x4400 + original=(ccr&16)|[4,8,0,8][vi];returned=(ccr+7)&31 if lane==4 else original + sr=0x700|(0x2000 if bank>=2 else 0)|(0x1000 if bank in [1,3] else 0)| (returned if lane>=3 else original) + fc=(5 if bank>=2 else 1)^(4 if lane==4 else 0) + ssw=0x100|(0x10 if width==1 else 0x20 if width==2 else 0)|fc + wanted={'complete':1,'first':1,'second':1,'registers':1,'memory':1,'transfers':1,'flags':1,'exceptions':2 if lane>=3 else 1,'attempts':1,'reads':1,'wattempts':1 if lane==2 else 3 if lane>=3 else 2,'writes':0 if lane==2 else 1,'retryFC':-1 if lane==2 else fc,'PC':0x1006,'sourceA':0x4200+post*stride,'A7':0x4200+post*stride if source==7 else [0x7000,0x7000,0x8000,0x9000][bank],'framePC':0x1002,'frameSR':sr,'frameSSW':ssw,'output':value,'address':0 if lane==2 else address,'value':0 if lane==2 else pending,'D7':42,'moveccr':returned,'finalccr':returned&16,'error':'none','events':'49235'+('92586' if lane>=3 else '6' if lane==2 else '86')} + mismatches={key:dict(expected=value,actual=d[key]) for key,value in wanted.items() if d[key]!=value} + passing=not mismatches + m.check(d['agreement']==int(passing),'Native observer and independent comparison disagree: '+str(id)+' '+str(mismatches)) + status='passing' if passing else 'mismatching';counts[status]+=1;lanes[str(lane)][status]+=1 + rows.append(dict(id=id,case=list(case),status=status,differences=mismatches)) + return dict(schema=1,nativeCases=61440,counts=counts,lanes=lanes,cases=rows,producerSha256=m.sha(Path(__file__)),transferProducerSha256=m.sha(BASE),softwareReferenceExecuted=True,privateProtocolAgreementPassed=counts['mismatching']==0,architecturalPromotion=False,hardwareQualified=False,enabledTranslation=False,physicalFunctionCodeSpaces=False,traceQualified=False,productionImport=False,publication=False,roadmapComplete=False) + + +m.fragments=fragments;m.observations=observations +if __name__=='__main__': + parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--reference-directory',type=Path,required=True);parser.add_argument('--output',type=Path,required=True);parser.add_argument('--validate-only',action='store_true');parser.add_argument('--discovery-only',action='store_true');args=parser.parse_args() + result=m.audit(args.output.resolve(),args.reference_directory.resolve(),args.validate_only) + print('Native handler comparison',result['counts'],'lanes',result['lanes']) + if not args.discovery_only and not result['privateProtocolAgreementPassed']:raise SystemExit(1) From 8aa90834c49f9dc2270b9349f7505387bb3d1fa9 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 01:47:06 +0300 Subject: [PATCH 136/211] Qualify private all-width source recovery and chained MOVE writes --- .../SyntheticM68020MemoryReadWriteTests.cs | 173 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 63 +++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 14 ++ scripts/test-copper68k-memory-read-write.py | 134 ++++++++++++++ 4 files changed, 384 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020MemoryReadWriteTests.cs create mode 100644 scripts/test-copper68k-memory-read-write.py diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020MemoryReadWriteTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020MemoryReadWriteTests.cs new file mode 100644 index 00000000..01d48147 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020MemoryReadWriteTests.cs @@ -0,0 +1,173 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Whole logical requests; no physical partial-cycle or trace qualification. +public sealed class SyntheticM68020MemoryReadWriteTests(ITestOutputHelper output) +{ + private static readonly ModelSpec[] Models = ModelSpec.All.Where(x => x.Id is "68EC020" or "68020" or "68030" or "A1200").ToArray(); + [Theory, Trait("Suite", "ReferenceDiscovery")] + [InlineData(false)] [InlineData(true)] + public void DirectWidthControls(bool batch) => Audit(batch, false); + [EnvironmentFact("COPPER68K_RUN_020_MEMORY_READ_WRITE", "require private all-width read/write continuations"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarReadWriteContinuations() => Audit(false, true); + [EnvironmentFact("COPPER68K_RUN_020_MEMORY_READ_WRITE", "require private all-width read/write continuations"), Trait("Suite", "ReferenceDiscovery")] + public void BatchReadWriteContinuations() => Audit(true, true); + + private void Audit(bool batch, bool fault) + { + var failures = new List(); + foreach (var model in Models) + { + var bus = new FaultBus(); var m = new SyntheticMachine(model, bus); + foreach (var width in new[] { 1, 2, 4 }) + foreach (var lane in fault ? new[] { "read", "read-write", "read-write-refault" } : new[] { "control" }) + foreach (var offset in fault ? Enumerable.Range(0, width) : new[] { 0 }) + { + var report = new CoverageBatch(model.Id, $"memory-read-write-{width}-{lane}-{offset}-{(batch ? "batch" : "scalar")}"); + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var source in new[] { 0, 7 }) + foreach (var post in new[] { false, true }) + foreach (var alias in new[] { false, true }) + foreach (var vi in Enumerable.Range(0, 4)) + foreach (var ccr in Enumerable.Range(0, 32)) + { + var id = $"{model.Id}/MOVE/read-write/width={width}/lane={lane}/offset={offset}/bank={bank}/source={source}/post={post}/alias={alias}/value={vi}/ccr={ccr}"; + try + { + var mask = width == 1 ? 0xffu : width == 2 ? 0xffffu : uint.MaxValue; + var value = vi switch { 0 => 0u, 1 => 0x80818283u & mask, 2 => mask >> 1, _ => mask }; + var stride = width == 1 && source == 7 ? 2u : (uint)width; + var destination = alias ? 0x4200u + (post ? stride : 0) : 0x4400u; + var opcode = (ushort)((width == 1 ? 0x1090 : width == 2 ? 0x3090 : 0x2090) | source | (post ? 8 : 0) | ((alias ? source : 1) << 9)); + bus.ResetObservation(); m.Reset(ccr); _ = SyntheticExecution.Prepare(m, [opcode, 0x7e2a, 0x4e71]); + for (uint a = 0x41f8; a < 0x4210; a++) m.InitializePhysical(a, 0xa5, 1); + for (uint a = 0x43f8; a < 0x4410; a++) m.InitializePhysical(a, 0xa5, 1); + m.InitializePhysical(0x4200, value, width); m.InitializePhysical(0x9020, 0x4e73, 2); m.InitializePhysical(0x9022, 0x4e71, 2); + m.Core.State.A[0] = source == 0 ? 0x4200u : 0x4000u; m.Core.State.A[1] = 0x4400; + m.Core.State.SetUserStackPointer(source == 7 && bank is "user" or "user-M" ? 0x4200u : 0x7800u); + m.Core.State.SetInterruptStackPointer(source == 7 && bank == "ISP" ? 0x4200u : 0x4700u); + m.Core.State.SetMasterStackPointer(source == 7 && bank == "MSP" ? 0x4200u : 0x7400u); + var sr = (ushort)(0x700 | ccr | (bank is "ISP" or "MSP" ? 0x2000 : 0) | (bank is "user-M" or "MSP" ? 0x1000 : 0)); + m.Core.State.StatusRegister = sr; m.Core.State.SetActiveStackPointer(source == 7 ? 0x4200u : bank == "MSP" ? 0x7400u : bank == "ISP" ? 0x4700u : 0x7800u); + var e = ArchitecturalExpectation.Capture(m); var serial = m.Core.State.ExceptionSequence; + var readFrame = (bank is "ISP" or "MSP" ? e.A[7] : bank == "user-M" ? 0x7400u : 0x4700u) - 92; + bus.Accesses.Clear(); bus.Observe(destination, width); + if (fault) bus.ArmRead(0x4200u + (uint)offset); + if (lane != "read" && fault) bus.ArmWrite(destination); + Step(m, batch); + if (fault) + { + if (m.Core.State.A[7] != readFrame || m.Core.State.ProgramCounter != 0x9020 || m.Core.State.ExceptionSequence != serial + 1 || + m.Core.State.LastExceptionVector != 2 || m.Core.State.Halted || m.Core.State.Stopped || + m.PeekPhysical(readFrame, 2) != sr || m.PeekPhysical(readFrame + 2, 4) != 0x1000 || m.PeekPhysical(readFrame + 6, 2) != 0xb008 || + (m.PeekPhysical(readFrame + 10, 2) & 0x1f7) != (0x140u | (width == 1 ? 0x10u : width == 2 ? 0x20u : 0) | ((sr & 0x2000) != 0 ? 5u : 1u)) || + m.PeekPhysical(readFrame + 16, 4) != 0x4200 || bus.Reads != 0 || bus.Writes != 0 || bus.ReadDenied != 1 || bus.WriteDenied != 0) + throw new InvalidOperationException("All-width read fault changed prefix/frame/PC/SR/SSW"); + for (var n = 0; n < 8; n++) if (m.Core.State.D[n] != e.D[n] || n != 7 && m.Core.State.A[n] != e.A[n]) + throw new InvalidOperationException("Read denial performed an early register effect"); + CaptureFrame(m, e, bus, readFrame, 92); e.ExceptionVector = 2; + Step(m, batch); + } + e.Sr = (ushort)((sr & 0xfff0) | (value == 0 ? 4 : (value & (1u << (width * 8 - 1))) != 0 ? 8 : 0)); + if (post) { e.A[source] += stride; if (source == 7 && bank == "MSP") e.MasterStackPointer = e.A[7]; } + if (fault && lane != "read") + { + var frame = (bank is "ISP" or "MSP" ? e.A[7] : bank == "user-M" ? 0x7400u : 0x4700u) - 32; + var count = 2; + CheckWriteBoundary(m, e, bus, frame, destination, value, mask, width, serial + count); + CaptureFrame(m, e, bus, frame, 32); + if (lane == "read-write-refault") + { + bus.ArmWrite(destination); Step(m, batch); count++; + CheckWriteBoundary(m, e, bus, frame, destination, value, mask, width, serial + count); + CaptureFrame(m, e, bus, frame, 32); + } + Step(m, batch); + } + e.Write(destination, value, width, model); e.Pc = 0x1002; Check(m, e); + var key = (post, width) switch + { + (false, 1) => M68kInstructionTimingKey.MoveByteAddressIndirectToAddressIndirect, + (false, 2) => M68kInstructionTimingKey.MoveWordAddressIndirectToAddressIndirect, + (false, _) => M68kInstructionTimingKey.MoveLongAddressIndirectToAddressIndirect, + (true, 1) => M68kInstructionTimingKey.MoveBytePostIncrementToAddressIndirect, + (true, 2) => M68kInstructionTimingKey.MoveWordPostIncrementToAddressIndirect, + _ => M68kInstructionTimingKey.MoveLongPostIncrementToAddressIndirect + }; + var plan = ((M68kAdvancedTimingInterpreter)m.Core).Timing.LastInstructionTiming.Plan; + if (plan.Key != key || plan.NativeCycles != 8 || plan.Barriers != M68kTimingBarrier.None || + plan.UsesHeadTail != (model.Id == "68030") || plan.HeadCycles != (model.Id == "68030" ? 1 : 0) || plan.TailCycles != (model.Id == "68030" ? 1 : 0)) + throw new InvalidOperationException("All-width continuation changed timing policy"); + var exceptions = fault ? lane == "read" ? 1 : lane == "read-write" ? 2 : 3 : 0; + if (bus.Reads != 1 || bus.Writes != 1 || bus.ReadDenied != (fault ? 1 : 0) || bus.WriteDenied != (fault ? exceptions - 1 : 0) || + m.Core.State.ExceptionSequence != serial + exceptions || + bus.Accesses.Count(a => !a.Write && a.Kind == M68kBusAccessKind.CpuInstructionFetch && a.Address == 0x1000) > 1) + throw new InvalidOperationException("All-width continuation repeated a source/update/write or opcode fetch"); + e.D[7] = 42; e.Pc = 0x1004; e.Sr = (ushort)(e.Sr & 0xfff0); Step(m, batch); Check(m, e); + report.Record(id, "passing", null); + } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException) { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + try { report.Complete(output); } catch (Exception ex) { failures.Add(ex); } + } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(x => x.Message))); + } + private static void CaptureFrame(SyntheticMachine m, ArchitecturalExpectation e, FaultBus bus, uint frame, int length) + { for (uint n = 0; n < length; n++) e.Memory[m.Model.Physical(frame + n)] = bus.Peek(m.Model.Physical(frame + n)); bus.Frames.Add((frame, length)); } + private static void CheckWriteBoundary(SyntheticMachine m, ArchitecturalExpectation e, FaultBus bus, uint frame, uint address, uint value, uint mask, int width, long serial) + { + var sr = e.Sr; + if (m.Core.State.ProgramCounter != 0x9020 || m.Core.State.A[7] != frame || m.Core.State.ExceptionSequence != serial || m.Core.State.LastExceptionVector != 2 || m.Core.State.Halted || m.Core.State.Stopped || + m.PeekPhysical(frame, 2) != sr || m.PeekPhysical(frame + 2, 4) != 0x1002 || m.PeekPhysical(frame + 6, 2) != 0xa008 || + (m.PeekPhysical(frame + 10, 2) & 0x1f7) != (0x100u | (width == 1 ? 0x10u : width == 2 ? 0x20u : 0) | ((sr & 0x2000) != 0 ? 5u : 1u)) || + m.PeekPhysical(frame + 16, 4) != address || (m.PeekPhysical(frame + 24, 4) & mask) != value || bus.Reads != 1 || bus.Writes != 0) + throw new InvalidOperationException("Read-to-write fault lost completed source/flags or pending write/frame"); + for (var n = 0; n < 8; n++) if (m.Core.State.D[n] != e.D[n] || n != 7 && m.Core.State.A[n] != e.A[n]) + throw new InvalidOperationException("Read-to-write fault repeated a register update"); + } + private static void Check(SyntheticMachine m, ArchitecturalExpectation e) { var error = e.Verify(m); if (error != null) throw new InvalidOperationException(error); } + private static void Step(SyntheticMachine m, bool batch) + { + if (!batch) { m.Core.ExecuteInstruction(); return; } + var b = new Boundary(); if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, b) != 1 || b.Before != 1 || b.After != 1) throw new InvalidOperationException("Batch boundaries differ"); + } + private sealed class Boundary : IM68kInstructionBoundary { public int Before, After; public bool BeforeInstruction() { Before++; return true; } public void AfterInstruction(long a, long b) => After++; } + private sealed class FaultBus : SparseRecordingBus, IM68kPhysicalAddressMap, IM68kBus + { + private uint? deniedRead, deniedWrite; private uint destination; private int width; + internal int Reads, Writes, ReadDenied, WriteDenied; + internal readonly List<(uint Start, int Length)> Frames = []; + internal void ResetObservation() { deniedRead = deniedWrite = null; Reads = Writes = ReadDenied = WriteDenied = 0; Frames.Clear(); } + internal void Observe(uint target, int size) { destination = target; width = size; } + internal void ArmRead(uint at) => deniedRead = at; + internal void ArmWrite(uint at) => deniedWrite = at; + public bool IsCpuPhysicalAddressMapped(uint a, int size, M68kBusAccessKind kind) + { + if (kind == M68kBusAccessKind.CpuDataRead && size == width && deniedRead is { } r && unchecked(r - a) < size) { deniedRead = null; ReadDenied++; return false; } + if (kind == M68kBusAccessKind.CpuDataWrite && size == width && deniedWrite == a) { deniedWrite = null; WriteDenied++; return false; } + return true; + } + private void Read(uint a, int size, M68kBusAccessKind kind) + { + if (kind != M68kBusAccessKind.CpuDataRead) return; + if (deniedRead is { } r && size == width && unchecked(r - a) < size) throw new InvalidOperationException("Source read bypassed physical map"); + if (size == width && a is >= 0x41f8 and < 0x4210 && !Frames.Any(f => a >= f.Start && a + (uint)size <= f.Start + f.Length)) Reads++; + } + private void Write(uint a, int size, M68kBusAccessKind kind) + { + if (kind != M68kBusAccessKind.CpuDataWrite) return; + if (deniedWrite == a && size == width) throw new InvalidOperationException("Destination write bypassed physical map"); + if (a == destination && size == width) Writes++; + } + byte IM68kBus.ReadByte(uint a, ref long c, M68kBusAccessKind k) { Read(a, 1, k); return base.ReadByte(a, ref c, k); } + ushort IM68kBus.ReadWord(uint a, ref long c, M68kBusAccessKind k) { Read(a, 2, k); return base.ReadWord(a, ref c, k); } + uint IM68kBus.ReadLong(uint a, ref long c, M68kBusAccessKind k) { Read(a, 4, k); return base.ReadLong(a, ref c, k); } + void IM68kBus.WriteByte(uint a, byte v, ref long c, M68kBusAccessKind k) { Write(a, 1, k); base.WriteByte(a, v, ref c, k); } + void IM68kBus.WriteWord(uint a, ushort v, ref long c, M68kBusAccessKind k) { Write(a, 2, k); base.WriteWord(a, v, ref c, k); } + void IM68kBus.WriteLong(uint a, uint v, ref long c, M68kBusAccessKind k) { Write(a, 4, k); base.WriteLong(a, v, ref c, k); } + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index ca0b1c92..04a0c581 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -10730,3 +10730,66 @@ continuation, read-to-write chains, saved-pipe/frame/fault variants and complete integration/isolated consumers remain required. No production or private CPU source correction, package publication or regression retirement occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +### Private all-width source-read and chained-write continuation — 2026-10-08 + +`scripts/test-copper68k-memory-read-write.py` extends the frozen private +handler-qualified candidate to byte/word memory-destination source-read suffixes. +Its only CPU changes are in the isolated `Operand020.cs`: admit all three MOVE +size encodings for indirect/postincrement sources and indirect destinations, +validate the same forms on restoration, and select the existing size-specific +timing key. The long path, completed source effects and pending-write protocol +are retained. Production CPU source is unchanged. + +The new fixture independently checks **786,432 cases** across EC020, 020, 030 +and A1200: byte/word/long, A0/A7, indirect/postincrement, separate/aliased +addresses, four stack states and boundary values, every CCR, scalar/batch and +each byte offset of a denied whole logical source request. Direct controls, +source-read recovery, source-to-write denial and another write denial are +separate deterministic lanes. Checks include initial B92 and subsequent A32 +frames, exact PC/SR, registers/stacks, surrounding memory, one source read and +one final write, exception counts, the following sentinel and existing timing +policy. Source rereads and repeated postincrement are forbidden. + +The unchanged private baseline passes 491,520 cases and reports 294,912 +byte/word recoveries unsupported. The corrected private candidate passes all +786,432 new cases and exactly retains 647,168 previous cases: **1,433,600 logical +cases / 18 xUnit executions / 360 reports**. Four isolated defects are detected: +source replay (688,128 mismatches), repeated update (344,064), stale aliased +postincrement destination (172,032), and changed source-suffix timing (229,376). +Each keeps its unaffected direct and recovery cases passing. Reports contain +4,096 cases each, below the shared failure-witness cap. Every failure identifier, +status and cause is checked; timing failures must name the timing-policy cause. + +Seven independent corrupted-evidence controls reject missing fixture sources, +empty combinations, false TRX counters, missing per-test summaries, changed +retained reports, an unrelated mutation cause and missing output identities. +The complete original proof is revalidated. The unchanged production CPU passes +**120,832 direct cases / 6 batches**; its eight optional private recovery batches +remain unavailable, separately reported. + +```powershell +python scripts/test-copper68k-memory-read-write.py ` + --qualified-parent-directory ` + --output +# --validate-only requires the full frozen parent, sources and every witness. +``` + +The pinned native all-width transfer evidence remains the external software +comparison for its recorded non-chained scope. The native A7-postincrement +source-read difference and repeated-write-fault saved-PC difference remain +open; this new chain matrix checks the private software contract and does not +settle those architectural questions. Trace, saved-pipe variations, foreign/ +moved/invalid frames, changed return S/M/banks, entry/load faults, broader +addressing/origins and physical partial transfers remain outside this gate. +Complete private CPU integration and isolated package consumers are still +required before import. Hardware is unavailable. No production CPU import, +package publication or regression retirement occurs. Milestone 6 remains +**in progress**, `roadmapComplete=false`. + +Evidence `audits/MemoryReadWriteV1/proof.json` SHA-256: +`d6f501a8605e78626bffc7ce26c04379d6fd2401a2e4fbf4c80508e152db2ecb`. +Unchanged production `audits/MoveTransferProductionControlsV5/verification.json` +SHA-256: `32d2a464c7e7cb78e8d38154f1284074a4e1ea92dc3be26e2f02cbdde6f12ded`. +Integrity `memory-read-write-controls-v1.json` SHA-256: +`077926031cd65ecf74a7fcd695ec602c39e396986dcbeee4d4846ce6535052d4`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 6a0a8451..ed969e47 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3426,3 +3426,17 @@ architecturally. Broader source-read/chained, short-frame, trace, integration an consumer requirements remain open. No CPU/package change or test retirement occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [native handler agreement, disagreement and causal diagnostic](COPPER68K_REFERENCE_QUALIFICATION.md#native-pending-write-handler-comparison--2026-10-08). + +The private all-width source-read suffix and read-to-write/refault chain gate +now passes 786,432 new cases, exactly retaining 647,168 earlier cases. Byte/word +recovery gaps in the old private baseline are reproduced; four sole defects +are detected with complete failure witnesses, and evidence integrity controls +reject incomplete or altered results. Current production direct controls pass +120,832 cases, with eight private recovery batches unavailable. The pinned +33,792-case native transfer evidence replays exactly in its stated scope. +A7 source-postincrement and repeated-fault saved-PC disagreements remain open; +the new chains qualify the private software contract only. Broader short-frame, +trace, frame/fault/origin and full integration/isolated-consumer gates remain +required. Hardware is unavailable. No production CPU import, package publication +or regression retirement occurs. Milestone 6 remains **in progress**, +`roadmapComplete=false`. See [all-width source and chained-write scope](COPPER68K_REFERENCE_QUALIFICATION.md#private-all-width-source-read-and-chained-write-continuation--2026-10-08). diff --git a/scripts/test-copper68k-memory-read-write.py b/scripts/test-copper68k-memory-read-write.py new file mode 100644 index 00000000..8a94d353 --- /dev/null +++ b/scripts/test-copper68k-memory-read-write.py @@ -0,0 +1,134 @@ +"""Audit all-width memory-destination source recovery and read/write fault chains. + +Uses a frozen private handler-qualified parent, fresh isolated sources/builds, +exact case/witness verification and retained reports. No production CPU import +or architectural promotion of the known native A7/trace/refault gaps occurs. +""" +import argparse +import contextlib +import hashlib +import importlib.util +import io +import itertools +import json +import os +from pathlib import Path +import re +import subprocess +import xml.etree.ElementTree as ET + +REPO=Path(__file__).resolve().parents[1] +HELPER=REPO/'scripts/test-copper68k-final-write-handlers.py' +FIXTURE='Copper68k.Tests/Synthetic/SyntheticM68020MemoryReadWriteTests.cs' +spec=importlib.util.spec_from_file_location('write_handlers',HELPER) +m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m) +sha,load,save,require,inventory=m.sha,m.load,m.save,m.require,m.inventory +PARENT_PROOF='f63d2ecf21777c2bc8fc259039aa2361bb72165b63b51caefa085b751a9f0674' +CPU=m.m.OPERAND +FILTER='FullyQualifiedName~SyntheticM68020MemoryReadWriteTests' +VARIANTS=['baseline','candidate','source-replay','source-update','stale-destination','timing'] +MODELS=m.m.r.MODELS + + +def expected(model,width,lane,offset,variant): + rows={} + for bank,source,post,alias,vi,ccr in itertools.product(['user','user-M','ISP','MSP'],[0,7],[False,True],[False,True],range(4),range(32)): + key=f'{model}/MOVE/read-write/width={width}/lane={lane}/offset={offset}/bank={bank}/source={source}/post={post}/alias={alias}/value={vi}/ccr={ccr}' + status='passing' + if lane!='control': + if variant=='baseline' and width!=4:status='unsupported' + elif variant=='source-replay' or variant=='source-update' and post or variant=='stale-destination' and post and alias or variant=='timing' and lane=='read':status='mismatching' + rows[key]={status:1} + return rows + + +def sources(parent,variant): + data={name:(parent/'candidate/source'/name).read_bytes() for name in inventory(parent/'candidate/source')} + require(len(data)==234,'Incomplete handler-qualified sources') + data[FIXTURE]=(REPO/FIXTURE).read_bytes() + if variant=='baseline':return data + t=data[CPU].decode('utf-8').replace('\r\n','\n') + t=m.m.once(t,'var memoryDestination = top == 2 && destination == 2 && mode is 2 or 3;','var memoryDestination = top is >= 1 and <= 3 && destination == 2 && mode is 2 or 3;') + t=m.m.once(t,'destinationMode > 1 && !(top == 2 && destinationMode == 2 && mode is 2 or 3)','destinationMode > 1 && !(top is >= 1 and <= 3 && destinationMode == 2 && mode is 2 or 3)') + t=m.m.once(t,'CompleteTiming(mode == 2 ? M68kInstructionTimingKey.MoveLongAddressIndirectToAddressIndirect : M68kInstructionTimingKey.MoveLongPostIncrementToAddressIndirect);','CompleteTiming(FinalMoveTiming020(opcode));') + update='if (mode == 3) WriteGeneralRegister(true, register, unchecked(address + M68kIntegerSemantics.AddressIncrement(register, size)));' + write='else if (destinationMode == 2) { WriteFinalMoveDestination020(State.A[destination], value, size); SetMoveFlags(value, size); }' + if variant=='source-replay':t=m.m.once(t,update,update+'\n if (destinationMode == 2) _ = ReadSized(State.A[register], size);') + elif variant=='source-update':t=m.m.once(t,update,'if (mode == 3) WriteGeneralRegister(true, register, unchecked(address + (destinationMode == 2 ? 2u : 1u) * M68kIntegerSemantics.AddressIncrement(register, size)));') + elif variant=='stale-destination':t=m.m.once(t,write,'else if (destinationMode == 2) { WriteFinalMoveDestination020(unchecked(State.A[destination] - (mode == 3 && destination == register ? M68kIntegerSemantics.AddressIncrement(register, size) : 0)), value, size); SetMoveFlags(value, size); }') + elif variant=='timing':t=m.m.once(t,'CompleteTiming(FinalMoveTiming020(opcode));','CompleteTimingPlan(M68kInstructionPlan.CreateFlat(M68kInstructionTimingKey.GeneralMove, "mutated read suffix", 6));') + data[CPU]=t.encode('utf-8');return data + + +def command(root,variant): + selection=FILTER+('|'+'|'.join([m.FILTER,m.m.m.FILTER]+m.m.RETENTION) if variant=='candidate' else '') + return ['dotnet','test',str(root/'source/Copper68k.Tests/Copper68k.Tests.csproj'),'-c','Release','--artifacts-path',str(root/'build'),'--filter',selection,'--logger','trx;LogFileName=audit.trx','--results-directory',str(root)] + + +def verify(root,variant,data,retained,parent_names): + record=load(root/'execution.json');ids={name:hashlib.sha256(value).hexdigest() for name,value in data.items()} + require(record['sources']==ids==inventory(root/'source'),'Changed all-width complete sources') + require(record['command']==command(root,variant) and record['exit']==(0 if variant=='candidate' else 1),'Wrong all-width command/exit') + outputs=['execution.log','audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + require(set(record['outputs'])==set(outputs) and record['outputs']=={p:sha(root/p) for p in outputs},'Changed all-width outputs') + tree=ET.parse(root/'audit.trx');tests=tree.findall('.//t:UnitTestResult',m.m.r.NS) + names={'DirectWidthControls(batch: False)':'Passed','DirectWidthControls(batch: True)':'Passed','ScalarReadWriteContinuations':'Passed' if variant=='candidate' else 'Failed','BatchReadWriteContinuations':'Passed' if variant=='candidate' else 'Failed'} + if variant=='candidate':names.update(parent_names) + require(len(tests)==len(names) and {t.get('testName').split('.')[-1]:t.get('outcome') for t in tests}==names,'Wrong actual all-width outcomes/roster') + counters=tree.find('.//t:Counters',m.m.r.NS);failed=sum(s=='Failed' for s in names.values()) + require(int(counters.get('total'))==len(names) and int(counters.get('failed'))==failed and int(counters.get('passed'))==len(names)-failed,'Wrong all-width TRX counters') + stdout='\n'.join(t.text or '' for t in tree.findall('.//t:UnitTestResult/t:Output/t:StdOut',m.m.r.NS)) + totals=dict(passing=0,mismatching=0,unsupported=0,untested=0);rows=[] + def summary(d): + matches=re.findall(re.escape(d['model']+'/'+d['group']+': ')+r'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)',stdout) + require(len(matches)==1 and json.loads(matches[0][0])==d['counts'] and int(matches[0][1])==d['logicalCases'] and int(matches[0][2])==len(d['combinations']),'All-width TRX/report disagreement') + for model,width,mode in itertools.product(MODELS,[1,2,4],['scalar','batch']): + for lane in ['control','read','read-write','read-write-refault']: + for offset in range(1 if lane=='control' else width): + group=f'memory-read-write-{width}-{lane}-{offset}-{mode}';p=root/f'{model}-{group}.json';d=load(p);wanted=expected(model,width,lane,offset,variant) + counts={s:sum(row.get(s,0) for row in wanted.values()) for s in totals} + require(d['model']==model and d['group']==group and d['logicalCases']==4096 and d['combinations']==wanted and d['counts']==counts,'Wrong all-width keys, weights or results') + bad={key for key,row in wanted.items() if 'passing' not in row} + require(len(d['failures'])==len(bad) and {f['id'] for f in d['failures']}==bad and all(f['status']==next(iter(wanted[f['id']])) and f['reason'] for f in d['failures']),'Wrong complete all-width failure witnesses') + if variant=='timing':require(all(f['reason']=='All-width continuation changed timing policy' for f in d['failures']),'Wrong all-width timing cause') + summary(d) + for s in totals:totals[s]+=counts[s] + rows.append(dict(path=p.name,sha256=sha(p))) + if variant=='candidate': + require(len(retained)==168,'Missing complete all-width retention selection') + for name,wanted in retained.items(): + p=root/name;d=load(p);require(d==wanted,'Changed complete all-width retained reports: '+name);summary(d);rows.append(dict(path=name,sha256=sha(p))) + require({p.name for p in root.glob('*.json')}=={row['path'] for row in rows}|{'execution.json'},'Missing/foreign all-width reports') + return dict(variant=variant,counts=totals,retentionCases=647168 if variant=='candidate' else 0,executions=len(names),reports=rows,executionSha256=sha(root/'execution.json')) + + +def audit(parent,output,validate): + require(sha(parent/'proof.json')==PARENT_PROOF,'Wrong all-width qualified parent') + with contextlib.redirect_stdout(io.StringIO()):m.audit(parent.parent/'MoveFinalWriteCandidateV2',parent,True) + producers={p.relative_to(REPO).as_posix():sha(p) for p in [Path(__file__),HELPER,REPO/FIXTURE]} + linkage=dict(producers=producers,parentProofSha256=PARENT_PROOF,variants=VARIANTS) + if not validate:output.mkdir(parents=True,exist_ok=False);save(output/'inputs.json',linkage) + require(load(output/'inputs.json')==linkage,'Wrong all-width producer/parent/selection') + retained={p.name:load(p) for p in (parent/'candidate').glob('*.json') if p.name.startswith(tuple(x+'-' for x in MODELS))} + tree=ET.parse(parent/'candidate/audit.trx');parent_names={t.get('testName').split('.')[-1]:t.get('outcome') for t in tree.findall('.//t:UnitTestResult',m.m.r.NS)} + require(len(parent_names)==14 and set(parent_names.values())=={'Passed'},'Incomplete qualified test roster') + entries=[] + for variant in VARIANTS: + root=output/variant;data=sources(parent,variant) + if not validate: + (root/'source').mkdir(parents=True) + for name,value in data.items(): + p=root/'source'/name;p.parent.mkdir(parents=True,exist_ok=True);p.write_bytes(value) + env=dict(os.environ,COPPER68K_RUN_020_MEMORY_READ_WRITE='1',COPPER68K_RUN_020_MOVE_WRITE_HANDLERS='1',COPPER68K_RUN_020_MOVE_WRITE_DISCOVERY='1',COPPER68K_RUN_020_OPERAND_READ_DISCOVERY='1',COPPER68K_RUN_020_MEMORY_DESTINATION_RECOVERY='1',COPPER68K_SYNTHETIC_REPORT_DIR=str(root)) + with (root/'execution.log').open('w',encoding='utf-8') as log:result=subprocess.run(command(root,variant),env=env,stdout=log,stderr=subprocess.STDOUT) + outputs=['execution.log','audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + save(root/'execution.json',dict(command=command(root,variant),exit=result.returncode,sources={name:hashlib.sha256(value).hexdigest() for name,value in data.items()},outputs={p:sha(root/p) for p in outputs})) + entry=verify(root,variant,data,retained,parent_names);entries.append(entry);print(variant,entry['counts'],'retention',entry['retentionCases'],flush=True) + proof=dict(schema=1,inputsSha256=sha(output/'inputs.json'),entries=entries,sourceCount=235,productionImport=False,publication=False,hardwareQualified=False,architecturalPromotion=False,roadmapComplete=False) + if validate:require(load(output/'proof.json')==proof,'Changed complete all-width proof') + else:save(output/'proof.json',proof) + print('Complete isolated memory read/write audit',sha(output/'proof.json'),flush=True) + + +if __name__=='__main__': + p=argparse.ArgumentParser(description=__doc__);p.add_argument('--qualified-parent-directory',type=Path,required=True);p.add_argument('--output',type=Path,required=True);p.add_argument('--validate-only',action='store_true');a=p.parse_args();audit(a.qualified_parent_directory.resolve(),a.output.resolve(),a.validate_only) From e8168c43e6080a17f333d830b3ab682f0b75a03e Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 02:33:03 +0300 Subject: [PATCH 137/211] Qualify private pending-write saved pipes and retained word edits --- .../SyntheticM68020MoveWritePipeTests.cs | 160 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 77 +++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 14 ++ scripts/test-copper68k-move-write-pipe.py | 108 ++++++++++++ 4 files changed, 359 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020MoveWritePipeTests.cs create mode 100644 scripts/test-copper68k-move-write-pipe.py diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020MoveWritePipeTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWritePipeTests.cs new file mode 100644 index 00000000..0ec54607 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWritePipeTests.cs @@ -0,0 +1,160 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Private serialized pipe protocol; not physical prefetch or silicon frame state. +public sealed class SyntheticM68020MoveWritePipeTests(ITestOutputHelper output) +{ + private static readonly ModelSpec[] Models = ModelSpec.All.Where(x => x.Id is "68EC020" or "68020" or "68030" or "A1200").ToArray(); + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_PIPE", "require private pending-write saved pipes"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarPendingWritePipes() => Audit(false); + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_PIPE", "require private pending-write saved pipes"), Trait("Suite", "ReferenceDiscovery")] + public void BatchPendingWritePipes() => Audit(true); + + private void Audit(bool batch) + { + var failures = new List(); + foreach (var model in Models) + { + var bus = new FaultBus(); var m = new SyntheticMachine(model, bus); + foreach (var width in new[] { 1, 2, 4 }) + foreach (var lane in new[] { "write", "software", "refault" }) + foreach (var count in Enumerable.Range(0, 4)) + foreach (var edit in Enumerable.Range(-1, count + 1)) + { + var report = new CoverageBatch(model.Id, $"move-write-pipe-{width}-{lane}-count{count}-edit{edit}-{(batch ? "batch" : "scalar")}"); + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var source in new[] { 0, 7 }) + foreach (var post in new[] { false, true }) + foreach (var alias in new[] { false, true }) + foreach (var vi in Enumerable.Range(0, 4)) + foreach (var ccr in new[] { 0, 31 }) + { + var id = $"{model.Id}/MOVE/write-pipe/width={width}/lane={lane}/count={count}/edit={edit}/bank={bank}/source={source}/post={post}/alias={alias}/value={vi}/ccr={ccr}"; + try + { + var mask = width == 1 ? 0xffu : width == 2 ? 0xffffu : uint.MaxValue; + var value = vi switch { 0 => 0u, 1 => 0x80818283u & mask, 2 => mask >> 1, _ => mask }; + var stride = width == 1 && source == 7 ? 2u : (uint)width; + var destination = alias ? 0x4200u + (post ? stride : 0) : 0x4400u; + var opcode = (ushort)((width == 1 ? 0x1090 : width == 2 ? 0x3090 : 0x2090) | source | (post ? 8 : 0) | ((alias ? source : 1) << 9)); + bus.ResetObservation(); m.Reset(ccr); _ = SyntheticExecution.Prepare(m, [opcode, 0x2c3c, 0x9abc, 0xdef0, 0x4e71]); + for (uint a = 0x41f8; a < 0x4210; a++) m.InitializePhysical(a, 0xa5, 1); + for (uint a = 0x43f8; a < 0x4410; a++) m.InitializePhysical(a, 0xa5, 1); + m.InitializePhysical(0x4200, value, width); m.InitializePhysical(0x9020, 0x4e73, 2); m.InitializePhysical(0x9022, 0x4e71, 2); + m.Core.State.A[0] = source == 0 ? 0x4200u : 0x4000u; m.Core.State.A[1] = 0x4400; + m.Core.State.SetUserStackPointer(source == 7 && bank is "user" or "user-M" ? 0x4200u : 0x7800u); + m.Core.State.SetInterruptStackPointer(source == 7 && bank == "ISP" ? 0x4200u : 0x4700u); + m.Core.State.SetMasterStackPointer(source == 7 && bank == "MSP" ? 0x4200u : 0x7400u); + var sr = (ushort)(0x700 | ccr | (bank is "ISP" or "MSP" ? 0x2000 : 0) | (bank is "user-M" or "MSP" ? 0x1000 : 0)); + m.Core.State.StatusRegister = sr; m.Core.State.SetActiveStackPointer(source == 7 ? 0x4200u : bank == "MSP" ? 0x7400u : bank == "ISP" ? 0x4700u : 0x7800u); + var e = ArchitecturalExpectation.Capture(m); var serial = m.Core.State.ExceptionSequence; + e.Sr = (ushort)((sr & 0xfff0) | (value == 0 ? 4 : (value & (1u << (width * 8 - 1))) != 0 ? 8 : 0)); + if (post) { e.A[source] += stride; if (source == 7 && bank == "MSP") e.MasterStackPointer = e.A[7]; } + var frame = (bank is "ISP" or "MSP" ? e.A[7] : bank == "user-M" ? 0x7400u : 0x4700u) - 32; + bus.Accesses.Clear(); bus.Observe(destination, width); bus.Arm(destination); Step(m, batch); + CheckFault(m, e, bus, frame, destination, value, width, serial + 1, 0, opcode); + bus.Frames.Add(frame); e.ExceptionVector = 2; + ushort[] words = [0x2e3c, 0x1234, 0x5678]; + if (edit >= 0) words[edit] = edit == 0 ? (ushort)0x2a3c : edit == 1 ? (ushort)0x8765 : (ushort)0x4321; + m.InitializePhysical(frame + 22, (uint)(1 | count << 8), 2); + for (var n = 0; n < 3; n++) m.InitializePhysical(frame + (n == 0 ? 12u : n == 1 ? 14u : 28u), words[n], 2); + if (lane == "software") + { m.InitializePhysical(frame + 10, m.PeekPhysical(frame + 10, 2) & ~0x100u, 2); m.InitializePhysical(destination, value, width); } + CaptureFrame(m, e, bus, frame); + if (lane == "refault") + { + bus.Arm(destination); var start = bus.Accesses.Count; Step(m, batch); CheckFrameReads(bus, frame, count, start); + CheckFault(m, e, bus, frame, destination, value, width, serial + 2, count, opcode); + for (var n = 0; n < count; n++) if (m.PeekPhysical(frame + (n == 0 ? 12u : n == 1 ? 14u : 28u), 2) != words[n]) + throw new InvalidOperationException("Refault changed retained pending-write words"); + CaptureFrame(m, e, bus, frame); + } + var before = bus.Accesses.Count; Step(m, batch); CheckFrameReads(bus, frame, count, before); + e.Write(destination, value, width, model); e.Pc = 0x1002; Check(m, e); + var plan = ((M68kAdvancedTimingInterpreter)m.Core).Timing.LastInstructionTiming.Plan; + var key = (post, width) switch + { + (false, 1) => M68kInstructionTimingKey.MoveByteAddressIndirectToAddressIndirect, + (false, 2) => M68kInstructionTimingKey.MoveWordAddressIndirectToAddressIndirect, + (false, _) => M68kInstructionTimingKey.MoveLongAddressIndirectToAddressIndirect, + (true, 1) => M68kInstructionTimingKey.MoveBytePostIncrementToAddressIndirect, + (true, 2) => M68kInstructionTimingKey.MoveWordPostIncrementToAddressIndirect, + _ => M68kInstructionTimingKey.MoveLongPostIncrementToAddressIndirect + }; + if (plan.Key != key || plan.NativeCycles != 8 || plan.Barriers != M68kTimingBarrier.None || plan.UsesHeadTail != (model.Id == "68030") || + plan.HeadCycles != (model.Id == "68030" ? 1 : 0) || plan.TailCycles != (model.Id == "68030" ? 1 : 0)) + throw new InvalidOperationException("Pending-write pipe changed timing policy"); + var writes = lane == "software" ? 0 : 1; + if (bus.Reads != 1 || bus.Writes != writes || bus.Rejected != (lane == "refault" ? 2 : 1) || + m.Core.State.ExceptionSequence != serial + (lane == "refault" ? 2 : 1)) + throw new InvalidOperationException("Pending-write pipe repeated source or write effects"); + var first = count > 0 ? words[0] : (ushort)0x2c3c; + var immediate = ((uint)(count > 1 ? words[1] : (ushort)0x9abc) << 16) | (count > 2 ? words[2] : (ushort)0xdef0); + e.D[(first >> 9) & 7] = immediate; e.Pc = 0x1008; + e.Sr = (ushort)((e.Sr & 0xfff0) | (immediate == 0 ? 4 : (immediate & 0x80000000) != 0 ? 8 : 0)); + before = bus.Accesses.Count; Step(m, batch); Check(m, e); + for (var n = 0; n < 3; n++) + if (bus.Accesses.Skip(before).Count(a => !a.Write && a.Kind == M68kBusAccessKind.CpuInstructionFetch && a.Address == 0x1002u + (uint)n * 2) != (n < count ? 0 : 1)) + throw new InvalidOperationException("Pending-write pipe refetched a retained word or missed backing data"); + report.Record(id, "passing", null); + } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException) { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + try { report.Complete(output); } catch (Exception ex) { failures.Add(ex); } + } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(x => x.Message))); + } + private static void CheckFault(SyntheticMachine m, ArchitecturalExpectation e, FaultBus bus, uint frame, uint address, uint value, int width, long serial, int count, ushort opcode) + { + if (m.Core.State.ProgramCounter != 0x9020 || m.Core.State.A[7] != frame || m.Core.State.ExceptionSequence != serial || m.Core.State.LastExceptionVector != 2 || m.Core.State.Halted || m.Core.State.Stopped || + m.PeekPhysical(frame, 2) != e.Sr || m.PeekPhysical(frame + 2, 4) != 0x1002 || m.PeekPhysical(frame + 6, 2) != 0xa008 || m.PeekPhysical(frame + 8, 2) != 0xc023 || + m.PeekPhysical(frame + 20, 2) != opcode || m.PeekPhysical(frame + 22, 2) != (uint)(1 | count << 8) || m.PeekPhysical(frame + 30, 2) != 0 || + (m.PeekPhysical(frame + 10, 2) & 0x1f7) != (0x100u | (width == 1 ? 0x10u : width == 2 ? 0x20u : 0) | ((e.Sr & 0x2000) != 0 ? 5u : 1u)) || + m.PeekPhysical(frame + 16, 4) != address || m.PeekPhysical(frame + 24, 4) != value || bus.Reads != 1 || bus.Writes != 0) + throw new InvalidOperationException("Pending-write pipe fault changed completed effects or frame state"); + for (var n = 0; n < 8; n++) if (m.Core.State.D[n] != e.D[n] || n != 7 && m.Core.State.A[n] != e.A[n]) + throw new InvalidOperationException("Pending-write pipe fault repeated register effects"); + } + private static void CaptureFrame(SyntheticMachine m, ArchitecturalExpectation e, FaultBus bus, uint frame) + { for (uint n = 0; n < 32; n++) e.Memory[m.Model.Physical(frame + n)] = bus.Peek(m.Model.Physical(frame + n)); } + private static void CheckFrameReads(FaultBus bus, uint frame, int count, int start) + { + // Fixed private-frame request protocol. Counting these requests also + // detects extra source reads when A7 postincrement overlaps the frame. + (uint Offset, int Width)[] header = [(0, 2), (2, 4), (6, 2), (30, 2), (8, 2), (10, 2), (16, 4), (20, 2), (22, 2), (24, 4), (30, 2)]; + var expected = header.Select(x => (frame + x.Offset, x.Width)) + .Concat(Enumerable.Range(0, count).Select(n => (frame + (n == 0 ? 12u : n == 1 ? 14u : 28u), 2))) + .Concat(Enumerable.Range(0, 10).Select(n => (frame + 12u + (uint)n * 2, 2))); + var actual = bus.Accesses.Skip(start).Where(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address >= frame && a.Address < frame + 32).Select(a => (a.Address, a.Width)); + if (!actual.SequenceEqual(expected)) throw new InvalidOperationException("Pending-write pipe frame reads differ from fixed transport protocol"); + } + private static void Check(SyntheticMachine m, ArchitecturalExpectation e) { var error = e.Verify(m); if (error != null) throw new InvalidOperationException(error); } + private static void Step(SyntheticMachine m, bool batch) + { if (!batch) { m.Core.ExecuteInstruction(); return; } var b = new Boundary(); if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, b) != 1 || b.Before != 1 || b.After != 1) throw new InvalidOperationException("Batch boundaries differ"); } + private sealed class Boundary : IM68kInstructionBoundary { public int Before, After; public bool BeforeInstruction() { Before++; return true; } public void AfterInstruction(long a, long b) => After++; } + private sealed class FaultBus : SparseRecordingBus, IM68kPhysicalAddressMap, IM68kBus + { + private uint? denied; private uint destination; private int width; + internal int Reads, Writes, Rejected; + internal readonly List Frames = []; + internal void ResetObservation() { denied = null; Reads = Writes = Rejected = 0; Frames.Clear(); } + internal void Observe(uint target, int size) { destination = target; width = size; } + internal void Arm(uint at) => denied = at; + public bool IsCpuPhysicalAddressMapped(uint a, int size, M68kBusAccessKind kind) + { if (kind == M68kBusAccessKind.CpuDataWrite && size == width && denied == a) { denied = null; Rejected++; return false; } return true; } + private void Read(uint a, int size, M68kBusAccessKind k) + { if (k == M68kBusAccessKind.CpuDataRead && size == width && a is >= 0x41f8 and < 0x4210 && !Frames.Any(f => a >= f && a + (uint)size <= f + 32)) Reads++; } + private void Write(uint a, int size, M68kBusAccessKind k) + { if (k != M68kBusAccessKind.CpuDataWrite) return; if (denied == a && size == width) throw new InvalidOperationException("Pending write bypassed physical map"); if (a == destination && size == width) Writes++; } + byte IM68kBus.ReadByte(uint a, ref long c, M68kBusAccessKind k) { Read(a, 1, k); return base.ReadByte(a, ref c, k); } + ushort IM68kBus.ReadWord(uint a, ref long c, M68kBusAccessKind k) { Read(a, 2, k); return base.ReadWord(a, ref c, k); } + uint IM68kBus.ReadLong(uint a, ref long c, M68kBusAccessKind k) { Read(a, 4, k); return base.ReadLong(a, ref c, k); } + void IM68kBus.WriteByte(uint a, byte v, ref long c, M68kBusAccessKind k) { Write(a, 1, k); base.WriteByte(a, v, ref c, k); } + void IM68kBus.WriteWord(uint a, ushort v, ref long c, M68kBusAccessKind k) { Write(a, 2, k); base.WriteWord(a, v, ref c, k); } + void IM68kBus.WriteLong(uint a, uint v, ref long c, M68kBusAccessKind k) { Write(a, 4, k); base.WriteLong(a, v, ref c, k); } + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 04a0c581..f1ef937f 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -10793,3 +10793,80 @@ Unchanged production `audits/MoveTransferProductionControlsV5/verification.json` SHA-256: `32d2a464c7e7cb78e8d38154f1284074a4e1ea92dc3be26e2f02cbdde6f12ded`. Integrity `memory-read-write-controls-v1.json` SHA-256: `077926031cd65ecf74a7fcd695ec602c39e396986dcbeee4d4846ce6535052d4`. + +### Private pending-write saved pipes — 2026-10-08 + +`scripts/test-copper68k-move-write-pipe.py` executes **184,320** new cases for +zero through three serialized instruction words, every retained-word edit, +normal pending-write completion, DF-clear software completion and another write +fault. EC020, 020, 030 and A1200 cover byte/word/long, A0/A7, indirect/ +postincrement, separate/aliased destinations, four stack states/values and +CCR 0/31 in scalar/batch mode. The earlier all-32-CCR handler and read/write +matrices are retained rather than multiplying that full domain by every pipe +edit. Each new report contains 256 cases, below the failure-witness cap. + +The following literal MOVE.L instruction distinguishes retained words from +changed backing memory and exposes lost/reordered words. Checks include exact +next PC, registers/stacks, defined flags, surrounding memory, original opcode, +private metadata and active words across refault, exact exception counts, +one source read/one pending write (zero CPU writes for DF-clear), selected +existing last-instruction timing policy and instruction-fetch addresses/counts. +The fixed RTE frame-request sequence also catches extra source reads when A7 +postincrement overlaps the frame. These are repository transport and serialized +software-state checks, not physical bus/prefetch or silicon internal-state +qualification. + +The candidate CPU is byte-identical to the all-width read/write parent. All +184,320 new cases and **1,433,600 exactly retained cases** pass: **1,617,920 +logical cases / 20 xUnit executions / 1,080 reports**. Four sole defects fail +precise affected cases while retaining unaffected cases: discard the restored +pipe (165,888 mismatches), swap the second/third saved data words while preserving +all frame reads (73,728), lose refault pipe count (55,296), and clear the pipe +on DF-clear completion (55,296). Every failure identifier/status/cause is +recorded and checked. The count mutation requires the specific frame-state +failure reason. + +The first V1 fixture omitted the short frame's far-end validation read. Its +184,320 failing cases and failed command remain preserved. A separate one-case +trace, using byte-identical CPU sources, proves that the sole request-sequence +difference is the offset-30 word read after the frame header and before context/ +SSW loading. The diagnostic's first build failed from a tuple-name mistake; +its fresh V2 successor executes the one case and records the expected failed +comparison. No diagnostic is relabelled successful qualification. V2 corrects +the fixture; final V3 additionally verifies opcode provenance and exact final +exception counts, and makes the word-order mutation change data without changing +reads. These are test/verifier changes; no CPU correction occurs. + +Seven independent corrupted-evidence controls reject missing fixture sources, +empty combinations, false counters, missing per-test summaries, altered retained +reports, an unrelated mutation cause and missing output identities. The complete +original proof revalidates. The audit binds all 236 source/project inputs, +producer/parent identities, commands/exits, assemblies, logs, actual test rosters, +complete report keys/weights and every failure witness. Missing fixtures or empty +selections cannot become successful coverage. + +```powershell +python scripts/test-copper68k-move-write-pipe.py ` + --qualified-parent-directory ` + --output +# --validate-only requires every frozen input and report. +``` + +Foreign/moved/invalid short frames, changed return S/M/banks, entry/load faults, +trace/interrupts, wider addressing/origins and partial physical transfers remain +open. The native A7 source-restoration and repeated-fault saved-PC disagreements +are unchanged and remain architecturally unqualified. Hardware is unavailable. +A fresh complete production/private CPU integration run is active on frozen +`8aa9083` snapshots; it does not include this later test-only addition, and final +integration results are not yet available. The private CPU sources are unchanged +between that run and this focused proof. Package/consumer validation still waits +for its completed gate. No production CPU import, package publication or +regression retirement occurs. Milestone 6 remains **in progress**, +`roadmapComplete=false`. + +Evidence `audits/MoveWritePipeV3/proof.json` SHA-256: +`dcb7ea91da3d88b0b35d84c01f159e16191c0eed4c78707d65e4e3239c8fdca7`. +Integrity `move-write-pipe-controls-v3.json` SHA-256: +`581babd34c31c5ba0abd2a6ed5a7e65329aa48f1ec13c556cec07908554820be`. +Diagnostic `move-write-pipe-read-diagnostic-v2.json` SHA-256: +`2c0fabc565c2b034f1a249cabe661006f00aecf44d1efcafdb3f0ffa36d4250e`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index ed969e47..7a7b8cf6 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3440,3 +3440,17 @@ trace, frame/fault/origin and full integration/isolated-consumer gates remain required. Hardware is unavailable. No production CPU import, package publication or regression retirement occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See [all-width source and chained-write scope](COPPER68K_REFERENCE_QUALIFICATION.md#private-all-width-source-read-and-chained-write-continuation--2026-10-08). + +The private short-frame saved-pipe gate now passes 184,320 cases for every +zero-to-three word length/edit, normal/DF-clear/refault completion, all transfer +widths, A0/A7 and aliases/banks, while retaining all 1,433,600 previous cases. +Four sole defects are detected, including data-word reordering without altered +reads. Seven evidence corruptions are rejected; the initial fixture's omitted +far-end validation request is independently traced and its failed evidence +preserved. No CPU correction occurs. The complete frozen production/private CPU +run remains active and its final result is unavailable; the new focused fixture +is a later test-only addition with byte-identical private CPU inputs. Integration, +isolated consumers, broader frame/provenance/fault and architectural A7/trace/ +native refault-PC gates remain required. Hardware is unavailable. No production +import, publication or test retirement occurs. Milestone 6 remains **in progress**, +`roadmapComplete=false`. See [saved-pipe scope and evidence](COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-saved-pipes--2026-10-08). diff --git a/scripts/test-copper68k-move-write-pipe.py b/scripts/test-copper68k-move-write-pipe.py new file mode 100644 index 00000000..6ed51885 --- /dev/null +++ b/scripts/test-copper68k-move-write-pipe.py @@ -0,0 +1,108 @@ +"""Audit private pending-write saved pipes with complete retained transfer evidence. + +No production CPU import, physical pipeline qualification or architectural +promotion of the known native A7/trace/refault disagreements occurs. +""" +import argparse,contextlib,hashlib,importlib.util,io,itertools,json,os,re,subprocess +from pathlib import Path +import xml.etree.ElementTree as ET +REPO=Path(__file__).resolve().parents[1] +HELPER=REPO/'scripts/test-copper68k-memory-read-write.py' +FIXTURE='Copper68k.Tests/Synthetic/SyntheticM68020MoveWritePipeTests.cs' +spec=importlib.util.spec_from_file_location('read_write',HELPER);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m) +sha,load,save,require,inventory=m.sha,m.load,m.save,m.require,m.inventory +PARENT_PROOF='d6f501a8605e78626bffc7ce26c04379d6fd2401a2e4fbf4c80508e152db2ecb' +CPU='Copper68k/M68kAdvancedTimingInterpreter.FinalWrite020.cs' +FILTER='FullyQualifiedName~SyntheticM68020MoveWritePipeTests' +VARIANTS=['candidate','discard-pipe','swap-pipe','refault-count','dfclear-pipe'] +MODELS=m.MODELS +NS={'t':'http://microsoft.com/schemas/VisualStudio/TeamTest/2010'} +def once(text,old,new): + require(text.count(old)==1,'Missing/ambiguous private pipe mutation target');return text.replace(old,new) +def expected(model,width,lane,count,edit,variant): + bad=variant=='discard-pipe' and count>0 or variant=='swap-pipe' and count==3 or variant=='refault-count' and lane=='refault' and count>0 or variant=='dfclear-pipe' and lane=='software' and count>0 + status='mismatching' if bad else 'passing' + return {f'{model}/MOVE/write-pipe/width={width}/lane={lane}/count={count}/edit={edit}/bank={bank}/source={source}/post={post}/alias={alias}/value={vi}/ccr={ccr}':{status:1} for bank,source,post,alias,vi,ccr in itertools.product(['user','user-M','ISP','MSP'],[0,7],[False,True],[False,True],range(4),[0,31])} +def sources(parent,variant): + data={name:(parent/'candidate/source'/name).read_bytes() for name in inventory(parent/'candidate/source')};require(len(data)==235,'Missing complete private pipe parent sources');data[FIXTURE]=(REPO/FIXTURE).read_bytes() + t=data[CPU].decode('utf-8').replace('\r\n','\n') + if variant=='discard-pipe':t=once(t,'CompleteTiming(M68kInstructionTimingKey.Rte); _instructionPipe = pipe;','CompleteTiming(M68kInstructionTimingKey.Rte); _instructionPipe.Reset(pc);') + elif variant=='swap-pipe': + marker=' // Read the full private state before committing stack, SR or the write.' + swapped=''' if (count == 3) + { + var copy = pipe; + _ = copy.TryConsumeKnownHead(out var first, out _); + _ = copy.TryConsumeKnownHead(out var second, out _); + _ = copy.TryConsumeKnownHead(out var third, out _); + pipe.Reset(pc); + pipe.Append(pc, first, default); + pipe.Append(unchecked(pc + 2), third, default); + pipe.Append(unchecked(pc + 4), second, default); + } +''' + t=once(t,marker,swapped+marker) + elif variant=='refault-count':t=once(t,'words[11] = (ushort)(1 | (fault.Pipe.Count << 8));','words[11] = 1;') + elif variant=='dfclear-pipe':t=once(t,'if ((ssw & 0x100) != 0) WriteFinalMoveDestination020(address, value, size);','if ((ssw & 0x100) != 0) WriteFinalMoveDestination020(address, value, size); else _instructionPipe.Reset(pc);') + if variant!='candidate':data[CPU]=t.encode('utf-8') + return data +def command(root,variant): + selection=FILTER+('|' + m.command(root,'candidate')[m.command(root,'candidate').index('--filter')+1] if variant=='candidate' else '') + return ['dotnet','test',str(root/'source/Copper68k.Tests/Copper68k.Tests.csproj'),'-c','Release','--artifacts-path',str(root/'build'),'--filter',selection,'--logger','trx;LogFileName=audit.trx','--results-directory',str(root)] +def verify(root,variant,data,retained,parent_names): + record=load(root/'execution.json');ids={n:hashlib.sha256(v).hexdigest() for n,v in data.items()} + require(record['sources']==ids==inventory(root/'source'),'Changed complete private pipe sources') + require(record['command']==command(root,variant) and record['exit']==(0 if variant=='candidate' else 1),'Wrong private pipe command/exit') + outputs=['execution.log','audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + require(set(record['outputs'])==set(outputs) and record['outputs']=={p:sha(root/p) for p in outputs},'Changed private pipe outputs') + tree=ET.parse(root/'audit.trx');tests=tree.findall('.//t:UnitTestResult',NS);names={x:'Passed' if variant=='candidate' else 'Failed' for x in ['ScalarPendingWritePipes','BatchPendingWritePipes']} + if variant=='candidate':names.update(parent_names) + require(len(tests)==len(names) and {t.get('testName').split('.')[-1]:t.get('outcome') for t in tests}==names,'Wrong private pipe outcomes/roster') + counters=tree.find('.//t:Counters',NS);failed=sum(x=='Failed' for x in names.values()) + require(int(counters.get('total'))==len(names) and int(counters.get('failed'))==failed and int(counters.get('passed'))==len(names)-failed,'Wrong private pipe counters') + stdout='\n'.join(t.text or '' for t in tree.findall('.//t:UnitTestResult/t:Output/t:StdOut',NS));totals=dict(passing=0,mismatching=0,unsupported=0,untested=0);rows=[] + def summary(d): + matches=re.findall(re.escape(d['model']+'/'+d['group']+': ')+r'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)',stdout) + require(len(matches)==1 and json.loads(matches[0][0])==d['counts'] and int(matches[0][1])==d['logicalCases'] and int(matches[0][2])==len(d['combinations']),'Private pipe TRX/report disagreement') + for model,width,lane,count,mode in itertools.product(MODELS,[1,2,4],['write','software','refault'],range(4),['scalar','batch']): + for edit in range(-1,count): + p=root/f'{model}-move-write-pipe-{width}-{lane}-count{count}-edit{edit}-{mode}.json';d=load(p);wanted=expected(model,width,lane,count,edit,variant);counts={s:sum(row.get(s,0) for row in wanted.values()) for s in totals} + require(d['schema']==1 and d['model']==model and d['group']==f'move-write-pipe-{width}-{lane}-count{count}-edit{edit}-{mode}' and d['xunitBatches']==1 and d['logicalCases']==256 and d['combinations']==wanted and d['counts']==counts,'Wrong private pipe keys, weights or results') + bad={key for key,row in wanted.items() if 'passing' not in row} + require(len(d['failures'])==len(bad) and {f['id'] for f in d['failures']}==bad and all(f['status']=='mismatching' and f['reason'] for f in d['failures']),'Wrong complete private pipe failure witnesses') + if variant=='refault-count':require(all(f['reason']=='Pending-write pipe fault changed completed effects or frame state' for f in d['failures']),'Wrong private pipe refault cause') + summary(d) + for s in totals:totals[s]+=counts[s] + rows.append(dict(path=p.name,sha256=sha(p))) + if variant=='candidate': + require(len(retained)==360,'Missing private pipe retention inventory') + for name,wanted in retained.items(): + p=root/name;d=load(p);require(d==wanted,'Changed private pipe retained report: '+name);summary(d);rows.append(dict(path=name,sha256=sha(p))) + require({p.name for p in root.glob('*.json')}=={row['path'] for row in rows}|{'execution.json'},'Missing/foreign private pipe reports') + return dict(variant=variant,counts=totals,retentionCases=1433600 if variant=='candidate' else 0,executions=len(names),reports=rows,executionSha256=sha(root/'execution.json')) +def audit(parent,output,validate): + require(sha(parent/'proof.json')==PARENT_PROOF,'Wrong qualified private pipe parent') + with contextlib.redirect_stdout(io.StringIO()):m.audit(parent.parent/'MoveWriteHandlersV2',parent,True) + linkage=dict(producers={p.relative_to(REPO).as_posix():sha(p) for p in [Path(__file__),HELPER,REPO/FIXTURE]},parentProofSha256=PARENT_PROOF,variants=VARIANTS) + if not validate:output.mkdir(parents=True,exist_ok=False);save(output/'inputs.json',linkage) + require(load(output/'inputs.json')==linkage,'Wrong private pipe producer/parent/selection') + retained={p.name:load(p) for p in (parent/'candidate').glob('*.json') if p.name.startswith(tuple(x+'-' for x in MODELS))} + tree=ET.parse(parent/'candidate/audit.trx');parent_names={t.get('testName').split('.')[-1]:t.get('outcome') for t in tree.findall('.//t:UnitTestResult',NS)};require(len(parent_names)==18 and set(parent_names.values())=={'Passed'},'Incomplete private pipe parent roster') + entries=[] + for variant in VARIANTS: + root=output/variant;data=sources(parent,variant) + if not validate: + (root/'source').mkdir(parents=True) + for name,value in data.items(): + p=root/'source'/name;p.parent.mkdir(parents=True,exist_ok=True);p.write_bytes(value) + env=dict(os.environ,COPPER68K_RUN_020_MOVE_WRITE_PIPE='1',COPPER68K_RUN_020_MEMORY_READ_WRITE='1',COPPER68K_RUN_020_MOVE_WRITE_HANDLERS='1',COPPER68K_RUN_020_MOVE_WRITE_DISCOVERY='1',COPPER68K_RUN_020_OPERAND_READ_DISCOVERY='1',COPPER68K_RUN_020_MEMORY_DESTINATION_RECOVERY='1',COPPER68K_SYNTHETIC_REPORT_DIR=str(root)) + with (root/'execution.log').open('w',encoding='utf-8') as log:result=subprocess.run(command(root,variant),env=env,stdout=log,stderr=subprocess.STDOUT) + outputs=['execution.log','audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + save(root/'execution.json',dict(command=command(root,variant),exit=result.returncode,sources={n:hashlib.sha256(v).hexdigest() for n,v in data.items()},outputs={p:sha(root/p) for p in outputs})) + entry=verify(root,variant,data,retained,parent_names);entries.append(entry);print(variant,entry['counts'],'retention',entry['retentionCases'],flush=True) + proof=dict(schema=1,inputsSha256=sha(output/'inputs.json'),entries=entries,sourceCount=236,productionImport=False,publication=False,hardwareQualified=False,architecturalPromotion=False,roadmapComplete=False) + if validate:require(load(output/'proof.json')==proof,'Changed complete private pipe proof') + else:save(output/'proof.json',proof) + print('Complete private pending-write pipe audit',sha(output/'proof.json'),flush=True) +if __name__=='__main__': + p=argparse.ArgumentParser(description=__doc__);p.add_argument('--qualified-parent-directory',type=Path,required=True);p.add_argument('--output',type=Path,required=True);p.add_argument('--validate-only',action='store_true');a=p.parse_args();audit(a.qualified_parent_directory.resolve(),a.output.resolve(),a.validate_only) From 1ac67e22ea97a8d8cbc544d80046362bd6c714e7 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 03:07:22 +0300 Subject: [PATCH 138/211] Record full production baseline and exact private discovery gate --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 40 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 10 +++++ 2 files changed, 50 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index f1ef937f..12a1ce1a 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -10870,3 +10870,43 @@ Integrity `move-write-pipe-controls-v3.json` SHA-256: `581babd34c31c5ba0abd2a6ed5a7e65329aa48f1ec13c556cec07908554820be`. Diagnostic `move-write-pipe-read-diagnostic-v2.json` SHA-256: `2c0fabc565c2b034f1a249cabe661006f00aecf44d1efcafdb3f0ffa36d4250e`. + +## Current full production baseline — 2026-10-08 + +The production half of the frozen `8aa9083` integration snapshot completed: +**5,340 executions / 5,299 passing / 41 explicitly unavailable / zero failures**. +The complete strict report audit verifies **86,453,506 logical cases / 921 +profile reports**, plus all ten selected native presets. This aggregate includes +additional promoted/consolidation and direct-control reports beyond the ordinary +milestone helper's 707-report aggregate; those distinct counts are not substituted +for each other. The production CPU source is unchanged. + +The first strict verifier incorrectly expected xUnit's TRX `notExecuted` counter +to equal the unavailable result rows. This adapter emits 41 `NotExecuted` rows +but zero in that counter. The failed verifier evidence is preserved; its successor +checks the actual row outcomes, complete counters and `Completed` summary. +No CPU failure or missing coverage is relabelled as success. + +The latest verifier also requires the exact selected environment settings. +Two controls disable a required RTE gate or add a foreign selection; both fail +with the intended selection reason before acceptance. Independent test discovery +from the pinned private assembly lists **8,022 test cases**. The completed private +suite must contain exactly that roster, in addition to preserved source fixtures, +current test outcomes, complete report keys/weights and native preset identities. +Discovery is not execution evidence. The earlier verifier's weaker extra-test +presence check cannot substitute for this roster gate. + +The private half is still running. The frozen full snapshots precede the later +saved-pipe fixture, which has separate focused proof and byte-identical private +CPU inputs. Full private verification and isolated NuGet consumer validation +remain required before importing any candidate. Hardware is unavailable; the +architectural A7, trace and native refault-PC disagreements remain open. +No production CPU import, package publication or regression retirement occurs. +Milestone 6 remains **in progress**, `roadmapComplete=false`. + +Production proof `audits/LatestPrivateFullCpuV2/production-verification-v5.json` SHA-256: +`79556e7128f0e84e1790521966a7296b7c5ff5368c8585edba0e340321115125`. +Candidate discovery `full-private-discovery-v1/discovery.json` SHA-256: +`cb09bd39466df3a7355c9a9db40b4a4672e79f05fab38268f7bbec4bb2911471`. +Selection controls `full-selection-controls-v1/verification.json` SHA-256: +`a3a36a048b226077795391498351e5e36872f90bc6cd5994d91377db67b69603`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 7a7b8cf6..5fe431db 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3454,3 +3454,13 @@ isolated consumers, broader frame/provenance/fault and architectural A7/trace/ native refault-PC gates remain required. Hardware is unavailable. No production import, publication or test retirement occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See [saved-pipe scope and evidence](COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-saved-pipes--2026-10-08). + +The frozen current production baseline now passes 5,299 tests with 41 explicitly +unavailable (5,340 total). Strict verification binds 86,453,506 logical cases, +921 complete profile reports and ten native presets. An independent discovery +requires the private half to execute its entire 8,022-case roster; discovery alone +is not qualification. Two altered-selection controls are rejected. The private +run remains active; complete private, isolated-consumer and broader architectural +requirements remain open. No candidate import, publication or test retirement +occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the +[current baseline evidence](COPPER68K_REFERENCE_QUALIFICATION.md#current-full-production-baseline--2026-10-08). From 2b55c6dde887c455cc8672d97c004b69312c8321 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 04:42:56 +0300 Subject: [PATCH 139/211] Require consolidation reports and record complete private validation --- Copper68k.Tests/Synthetic/README.md | 13 +++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 90 +++++++++++++++---- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 36 ++++++-- scripts/test-copper68k-synthetic.ps1 | 1 + 4 files changed, 116 insertions(+), 24 deletions(-) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index a01d13d2..5c40e121 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1243,3 +1243,16 @@ report integrity. `-ValidateReportsOnly` rechecks unchanged retained evidence; missing, empty, changed or unrelated evidence fails. See the [replacement mapping and proof](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#cmpm-alias-and-a7-semantic-consolidation--2026-10-07). + +## Mandatory SUBA and CMPA replacement reports + +The ordinary `scripts/test-copper68k-synthetic.ps1` gate requires +`suba-indirect-consolidation` (1,536 cases) and +`cmpa-displacement-consolidation` (1,280 cases) for each of the eight profiles. +These already execute in the default synthetic selection. Missing or incomplete +replacement reports fail acceptance, including `-ValidateReportsOnly` runs. +Keep the required `integer-inventory.json` alongside frozen reports. + +The complete ordinary semantic gate totals 86,262,562 cases in 723 batches; +full-suite and optional private audit counts have separate scopes. See the +[gate evidence and negative controls](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#mandatory-arithmetic-consolidation-reports--2026-10-08). diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 12a1ce1a..d9ba1e99 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -10887,26 +10887,86 @@ but zero in that counter. The failed verifier evidence is preserved; its success checks the actual row outcomes, complete counters and `Completed` summary. No CPU failure or missing coverage is relabelled as success. -The latest verifier also requires the exact selected environment settings. -Two controls disable a required RTE gate or add a foreign selection; both fail -with the intended selection reason before acceptance. Independent test discovery -from the pinned private assembly lists **8,022 test cases**. The completed private -suite must contain exactly that roster, in addition to preserved source fixtures, -current test outcomes, complete report keys/weights and native preset identities. -Discovery is not execution evidence. The earlier verifier's weaker extra-test -presence check cannot substitute for this roster gate. - -The private half is still running. The frozen full snapshots precede the later -saved-pipe fixture, which has separate focused proof and byte-identical private -CPU inputs. Full private verification and isolated NuGet consumer validation -remain required before importing any candidate. Hardware is unavailable; the -architectural A7, trace and native refault-PC disagreements remain open. +The verifier requires exact selected environment settings. Two controls disable +a required RTE gate or add a foreign selection; both fail for the intended reason. +The initial discovery roster had 8,022 names, including one deferred theory +placeholder. Execution expands that placeholder into 33 declared matrix rows, +giving **8,054 cases**. The initial discovery also decoded UTF-8 output as Windows +1252, corrupting thirteen display names. The V5 verifier correctly rejected that +roster; its failed evidence is preserved in +`full-private-verifier-v5-discovery-failure.json`. This was a discovery/verifier +defect, not a CPU failure. + +Fresh explicit UTF-8 discovery and independent enumeration of the frozen test +fixture's declared data reconstruct exactly all 8,054 execution names. The data +reader does not execute CPU instructions or call decoder/EA/arithmetic helpers. +V6 binds its sources, assembly, command and output to the frozen inputs, checks +the complete roster and outcomes, and rejects missing names or changed data in +two separate controls. No foreign or missing case is ignored. + +The private half completed **8,054 executions / 8,021 passing / 33 explicitly +unavailable / zero failures**. Strict verification binds **109,079,170 logical +cases / 12,065 profile reports** and ten native presets, exact source and assembly +identities, selection settings, retained reports, complete keys and weights. +The frozen full snapshots precede the later saved-pipe fixture, which has its +separate 1,617,920-case proof and byte-identical private CPU inputs. + +The immutable local package **1.5.2-synthetic-dev.73** passed an isolated Release +app build and clean CopperScreen `aa1dad5` consumer checks: **171 host tests / +6 unavailable**, **74 disk tests**, **1,080 engine diagnostic tests**, and **two +Workbench 3.1 floppy replays**. The clean archive excludes unrelated working +changes. Package CPU bytes match the frozen candidate and consumer assemblies; +archive, source, commands, results and native inputs are bound by independent +verification. Three preflight controls reject changed archive, wrong commit or +publication status before extraction. This is local package validation, with no +publication or HD-boot claim. + +Full integration and consumers qualify these private software contracts only. +Hardware is unavailable; architectural A7, trace and native refault-PC +disagreements, broader frame provenance, fault origins and other milestone-6 +requirements remain open. No production CPU import, package publication or regression retirement occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. Production proof `audits/LatestPrivateFullCpuV2/production-verification-v5.json` SHA-256: `79556e7128f0e84e1790521966a7296b7c5ff5368c8585edba0e340321115125`. -Candidate discovery `full-private-discovery-v1/discovery.json` SHA-256: +Initial rejected discovery `full-private-discovery-v1/discovery.json` SHA-256: `cb09bd39466df3a7355c9a9db40b4a4672e79f05fab38268f7bbec4bb2911471`. Selection controls `full-selection-controls-v1/verification.json` SHA-256: `a3a36a048b226077795391498351e5e36872f90bc6cd5994d91377db67b69603`. + +Complete V6 proof `audits/LatestPrivateFullCpuV2/proof.json` SHA-256: +`a17fb76bcca015c9cebfc86072bc7c6ff977a69070295f79bc765589bdd43c4b`. +Corrected discovery `full-private-discovery-v2/expanded-discovery.json` SHA-256: +`78c47e1e6701876792f0999dc77c2d6addac7de91597d613999978f9c20f2525`. +Discovery controls `full-discovery-controls-v6/verification.json` SHA-256: +`280d235cb7e6bc8ce748c3e9dd258614ec3b79a5c2784924a8e7ea30f83d4b17`. +Consumer proof `consumer-73-complete-proof.json` SHA-256: +`c7f97a733ab89a556ded142649c829ed3fb5214484c8e1575b6f3655d4351099`. +Package SHA-256: +`4d9dc8c6d6298b533f100513b9e339cd57ea04c4210afad16a96d0c52955258a`. + +## Mandatory arithmetic consolidation reports — 2026-10-08 + +The ordinary synthetic selection already executes the shared SUBA indirect and +CMPA displacement fixtures, but its report validator omitted their mandatory +membership. Both groups are now required on every model/profile: 1,536 SUBA and +1,280 CMPA cases each, **22,528 cases / 16 reports** in total. This closes the +acceptance omission after their proven semantic-regression retirements; it adds +no CPU behavior or new retirement. + +The ordinary gate now verifies **86,262,562 logical cases / 723 batches**, distinct +from the broader full-production aggregate above. Report-only validation uses +the unchanged frozen production reports and required integer inventory. All +921 input reports and the inventory retain their hashes. Removing a SUBA report +or marking a CMPA case untested makes the maintained validator fail with the +specific group reason. The first copied positive fixture omitted the integer +inventory and correctly failed; that evidence remains preserved, and a fresh +complete fixture passes. Earlier 707-report results retain their original scope. + +Gate proof `ordinary-consolidation-gate-proof-v2.json` SHA-256: +`bb37e23e2d229c7961aa67a66ad704318f551c770a3787a45f1b3dc9f281ebfc`. +Controls `ordinary-consolidation-controls-v1/verification.json` SHA-256: +`540a76d28d66bb81ccdd79c6b45172850254bbf0c0dfcd4a26434adbcdf07508`. +Milestone 6 remains **in progress**, `roadmapComplete=false`; no production +candidate import or package publication is authorized by these results. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 5fe431db..a9079e44 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3455,12 +3455,30 @@ native refault-PC gates remain required. Hardware is unavailable. No production import, publication or test retirement occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See [saved-pipe scope and evidence](COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-saved-pipes--2026-10-08). -The frozen current production baseline now passes 5,299 tests with 41 explicitly -unavailable (5,340 total). Strict verification binds 86,453,506 logical cases, -921 complete profile reports and ten native presets. An independent discovery -requires the private half to execute its entire 8,022-case roster; discovery alone -is not qualification. Two altered-selection controls are rejected. The private -run remains active; complete private, isolated-consumer and broader architectural -requirements remain open. No candidate import, publication or test retirement -occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the -[current baseline evidence](COPPER68K_REFERENCE_QUALIFICATION.md#current-full-production-baseline--2026-10-08). +The frozen complete production suite passes 5,299 tests with 41 explicitly +unavailable (5,340 total), and the private suite passes 8,021 with 33 unavailable +(8,054 total), both with zero failures. Strict verification binds respectively +86,453,506 / 109,079,170 logical cases, 921 / 12,065 complete profile reports, +ten native presets each, exact selections and source/assembly identities. +The initial 8,022-name discovery used the wrong text encoding and included a +deferred theory placeholder; its verifier rejection is preserved. Explicit UTF-8 +discovery plus independent enumeration of 33 declared fixture rows reconstructs +exactly all 8,054 execution names. Missing names and changed data are rejected. +The later focused pipe fixture remains separate, with identical private CPU bytes. + +Immutable local `.73` passes a clean committed CopperScreen archive build, +171 host tests (six unavailable), 74 disk tests, 1,080 engine tests and two +Workbench 3.1 floppy replays. These validate the private software contract, +not the outstanding architectural disagreements. No CPU candidate import or +package publication occurs. See the [full integration and consumer evidence](COPPER68K_REFERENCE_QUALIFICATION.md#current-full-production-baseline--2026-10-08). + +The ordinary validator now requires the already executed SUBA/CMPA replacement +groups on every profile: 22,528 cases / 16 reports. Its total is 86,262,562 cases +in 723 batches. Missing SUBA and incomplete CMPA controls fail for their precise +reasons; unchanged frozen reports and the integer inventory pass. A first copied +fixture missing that inventory correctly failed and remains preserved. This +changes acceptance membership only, with no CPU fix or further test retirement. +See the [mandatory consolidation gate](COPPER68K_REFERENCE_QUALIFICATION.md#mandatory-arithmetic-consolidation-reports--2026-10-08). +Hardware is unavailable. Broader frame provenance, fault origins, trace and +reference requirements remain open. Milestone 6 remains **in progress**, +`roadmapComplete=false`. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index b11b906c..d10dab0e 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -71,6 +71,7 @@ try { 'transfer-movem-invalid-operands'=6400; 'arithmetic-boundaries'=56448; 'arithmetic-addressing'=$(if ($model -in @('68000','68010')) {22331} else {25901}); 'arithmetic-scenarios'=21072; 'arithmetic-extend'=25440; + 'suba-indirect-consolidation'=1536; 'cmpa-displacement-consolidation'=1280; 'arithmetic-invalid-operands'=$(if ($model -in @('68000','68010')) {6336} else {5952}); 'arithmetic-quick-invalid-operands'=26624; 'integer-binary-invalid-operands'=121344; From bbce629ac43f2ca0a18c0a8efc61b58878d7df93 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 04:59:36 +0300 Subject: [PATCH 140/211] Qualify cold pending-write frame restoration and rejection --- Copper68k.Tests/Synthetic/README.md | 15 ++ .../SyntheticM68020MoveWriteFrameTests.cs | 141 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 83 +++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 20 +++ scripts/test-copper68k-move-write-frame.py | 119 +++++++++++++++ 5 files changed, 378 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFrameTests.cs create mode 100644 scripts/test-copper68k-move-write-frame.py diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 5c40e121..783b924b 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1256,3 +1256,18 @@ Keep the required `integer-inventory.json` alongside frozen reports. The complete ordinary semantic gate totals 86,262,562 cases in 723 batches; full-suite and optional private audit counts have separate scopes. See the [gate evidence and negative controls](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#mandatory-arithmetic-consolidation-reports--2026-10-08). + +## Private cold pending-write frame audit + +`scripts/test-copper68k-move-write-frame.py --qualified-parent-directory + --output ` runs the four optional +`SyntheticM68020MoveWriteFrameTests` batches on an isolated, unchanged private +CPU snapshot, plus three targeted mutations. The parent and its pinned upstream +directories must remain available. `--validate-only` rejects missing, empty, +changed or unrelated inputs/results. + +The 163,584 cases qualify literal cold/relocated C023 images, returned stack banks, +preserved completed state and rejection atomicity under the private software +contract. They do not qualify silicon internal images or promote architectural +unsupported forms. Existing ordinary CI counts and full-suite checkpoint scopes +remain unchanged. See the [scope and evidence](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#private-cold-and-relocated-pending-write-frames--2026-10-08). diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFrameTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFrameTests.cs new file mode 100644 index 00000000..ad17fa22 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFrameTests.cs @@ -0,0 +1,141 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Cold, relocated private C023 images. This is a serialized software contract, +// not a claim about silicon internal register images or physical fault timing. +public sealed class SyntheticM68020MoveWriteFrameTests(ITestOutputHelper output) +{ + private static readonly ModelSpec[] Models = ModelSpec.All.Where(x => x.Id is "68EC020" or "68020" or "68030" or "A1200").ToArray(); + private static readonly string[] Banks = ["user", "user-M", "ISP", "MSP"]; + private static readonly uint[] Frames = [0x6000, 0x7000, 0x12346000]; + private static readonly string[] Invalid = ["foreign-zero", "foreign-read", "version-zero", "version-two", "count-four", "reserved", "read-cycle", "wrong-size", "bad-fc", "trace-t1", "trace-t0", "odd-pc", "bad-source", "bad-destination"]; + + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_FRAME", "require private pending-write frame provenance"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarColdWriteFrames() => Audit(false, false); + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_FRAME", "require private pending-write frame provenance"), Trait("Suite", "ReferenceDiscovery")] + public void BatchColdWriteFrames() => Audit(true, false); + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_FRAME", "require private pending-write frame provenance"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarInvalidWriteFrames() => Audit(false, true); + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_FRAME", "require private pending-write frame provenance"), Trait("Suite", "ReferenceDiscovery")] + public void BatchInvalidWriteFrames() => Audit(true, true); + + private void Audit(bool batch, bool invalid) + { + var failures = new List(); + foreach (var model in Models) + foreach (var width in new[] { 1, 2, 4 }) + foreach (var current in new[] { "ISP", "MSP" }) + foreach (var lane in invalid ? Invalid : new[] { "pending", "completed" }) + { + var report = new CoverageBatch(model.Id, $"move-write-frame-{width}-{current}-{lane}-{(batch ? "batch" : "scalar")}"); + var m = new SyntheticMachine(model, new FrameBus()); + foreach (var returned in Banks) + foreach (var location in Enumerable.Range(0, Frames.Length)) + foreach (var count in invalid ? new[] { 3 } : Enumerable.Range(0, 4)) + foreach (var ccr in invalid ? new[] { 0, 31 } : Enumerable.Range(0, 32)) + { + var id = $"{model.Id}/MOVE/write-frame/width={width}/current={current}/returned={returned}/location={location}/count={count}/ccr={ccr}/lane={lane}"; + try + { + m.Reset(); _ = SyntheticExecution.Prepare(m, [0x4e73, 0x4e71]); + var frame = Frames[location]; + var savedSr = (ushort)(0x700 | ccr | (returned is "ISP" or "MSP" ? 0x2000 : 0) | (returned is "user-M" or "MSP" ? 0x1000 : 0)); + var value = 0x80a1b2c3u & (width == 1 ? 0xffu : width == 2 ? 0xffffu : uint.MaxValue); + // MOVE.B/W/L (A7)+,(A1), with completed source registers deliberately + // unrelated to the saved destination. No original MOVE is executed. + var opcode = (ushort)(width == 1 ? 0x129f : width == 2 ? 0x329f : 0x229f); + var ssw = (ushort)((lane == "completed" ? 0 : 0x100) | (width == 1 ? 0x10 : width == 2 ? 0x20 : 0) | (current == "ISP" ? 1 : 5)); + for (uint a = frame - 8; a < frame + 40; a++) m.InitializePhysical(a, 0xa5, 1); + for (uint a = 0x43f8; a < 0x4510; a++) m.InitializePhysical(a, 0xa5, 1); + m.InitializePhysical(0x2000, 0x7e2a, 2); m.InitializePhysical(0x2002, 0x7c2b, 2); m.InitializePhysical(0x2004, 0x7a2c, 2); m.InitializePhysical(0x2006, 0x4e71, 2); + // Literal documented private layout; no production frame/EA helper. + m.InitializePhysical(frame, savedSr, 2); m.InitializePhysical(frame + 2, 0x2000, 4); + m.InitializePhysical(frame + 6, 0xa008, 2); m.InitializePhysical(frame + 8, 0xc023, 2); + m.InitializePhysical(frame + 10, ssw, 2); m.InitializePhysical(frame + 12, 0x7e11, 2); m.InitializePhysical(frame + 14, 0x7c22, 2); + m.InitializePhysical(frame + 16, 0x4400, 4); m.InitializePhysical(frame + 20, opcode, 2); + m.InitializePhysical(frame + 22, (uint)(1 | count << 8), 2); m.InitializePhysical(frame + 24, value, 4); + m.InitializePhysical(frame + 28, 0x7a33, 2); m.InitializePhysical(frame + 30, 0, 2); + if (lane == "completed") m.InitializePhysical(0x4400, value, width); + switch (lane) + { + case "foreign-zero": m.InitializePhysical(frame + 8, 0, 2); break; + case "foreign-read": m.InitializePhysical(frame + 8, 0xc022, 2); break; + case "version-zero": m.InitializePhysical(frame + 22, 0x300, 2); break; + case "version-two": m.InitializePhysical(frame + 22, 0x302, 2); break; + case "count-four": m.InitializePhysical(frame + 22, 0x401, 2); break; + case "reserved": m.InitializePhysical(frame + 30, 1, 2); break; + case "read-cycle": m.InitializePhysical(frame + 10, (uint)(ssw | 0x40), 2); break; + case "wrong-size": m.InitializePhysical(frame + 10, (uint)(ssw ^ 0x20), 2); break; + case "bad-fc": m.InitializePhysical(frame + 10, (uint)(ssw & ~7), 2); break; + case "trace-t1": m.InitializePhysical(frame, (uint)(savedSr | 0x8000), 2); break; + case "trace-t0": m.InitializePhysical(frame, (uint)(savedSr | 0x4000), 2); break; + case "odd-pc": m.InitializePhysical(frame + 2, 0x2001, 4); break; + case "bad-source": m.InitializePhysical(frame + 20, (uint)(opcode & ~0x38), 2); break; + case "bad-destination": m.InitializePhysical(frame + 20, (uint)(opcode & ~0x1c0), 2); break; + } + m.Core.State.A[0] = 0x4204; m.Core.State.A[1] = 0x4500; + m.Core.State.SetUserStackPointer(0x7800); + m.Core.State.SetInterruptStackPointer(current == "ISP" ? frame : 0x4700); + m.Core.State.SetMasterStackPointer(current == "MSP" ? frame : 0x7400); + m.Core.State.StatusRegister = current == "ISP" ? (ushort)0x2700 : (ushort)0x3700; + m.Core.State.SetActiveStackPointer(frame); + var e = ArchitecturalExpectation.Capture(m); var serial = m.Core.State.ExceptionSequence; + m.Bus.Accesses.Clear(); + if (invalid) + { + var rejected = false; + try { Step(m, batch); } catch (UnsupportedM68kTimingException) { rejected = true; } + if (!rejected) throw new InvalidOperationException("Malformed or foreign private write frame was accepted"); + e.Pc = 0x1002; Check(m, e); + CheckBanks(m, 0x7800, current == "ISP" ? frame : 0x4700, current == "MSP" ? frame : 0x7400); + if (m.Bus.Accesses.Any(a => a.Write)) throw new InvalidOperationException("Rejected write frame committed a memory effect"); + } + else + { + var isp = current == "ISP" ? frame + 32 : 0x4700; + var msp = current == "MSP" ? frame + 32 : 0x7400; + e.Sr = savedSr; e.Pc = 0x2000; + e.A[7] = returned == "ISP" ? isp : returned == "MSP" ? msp : 0x7800; + e.InactiveStackPointer = returned is "ISP" or "MSP" ? 0x7800 : isp; e.MasterStackPointer = msp; + e.Write(0x4400, value, width, model); + Step(m, batch); Check(m, e); CheckBanks(m, 0x7800, isp, msp); + if (!m.Bus.Accesses.Where(a => a.Write).Select(a => (a.Address, a.Width)).SequenceEqual(lane == "completed" ? Array.Empty<(uint, int)>() : new[] { (0x4400u, width) })) + throw new InvalidOperationException("Cold write frame repeated or misplaced the pending write"); + for (var n = 0; n < 3; n++) + { + e.D[7 - n] = (uint)(n < count ? new[] { 0x11, 0x22, 0x33 }[n] : 0x2a + n); + e.Sr = (ushort)(e.Sr & 0xfff0); e.Pc += 2; + var start = m.Bus.Accesses.Count; Step(m, batch); Check(m, e); CheckBanks(m, 0x7800, isp, msp); + if (m.Bus.Accesses.Skip(start).Count(a => !a.Write && a.Kind == M68kBusAccessKind.CpuInstructionFetch && a.Address == 0x2000u + (uint)n * 2) != (n < count ? 0 : 1)) + throw new InvalidOperationException("Cold write frame discarded or refetched a saved instruction word"); + } + } + if (m.Core.State.ExceptionSequence != serial || m.Bus.Accesses.Any(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && !(a.Address >= model.Physical(frame) && a.Address + (uint)a.Width <= model.Physical(frame) + 32))) + throw new InvalidOperationException("Cold write frame replayed source or changed exception provenance"); + report.Record(id, "passing", null); + } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException) { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + try { report.Complete(output); } catch (Exception ex) { failures.Add(ex); } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(x => x.Message))); + } + + private static void CheckBanks(SyntheticMachine m, uint usp, uint isp, uint msp) + { + if (m.Core.State.UserStackPointer != usp || m.Core.State.InterruptStackPointer != isp || m.Core.State.MasterStackPointer != msp) + throw new InvalidOperationException("Cold write frame changed a completed or inactive stack bank"); + } + private static void Check(SyntheticMachine m, ArchitecturalExpectation e) { var error = e.Verify(m); if (error != null) throw new InvalidOperationException(error); } + private static void Step(SyntheticMachine m, bool batch) + { + if (!batch) { m.Core.ExecuteInstruction(); return; } + var b = new Boundary(); if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, b) != 1 || b.Before != 1 || b.After != 1) throw new InvalidOperationException("Cold write frame batch boundaries differ"); + } + private sealed class Boundary : IM68kInstructionBoundary { public int Before, After; public bool BeforeInstruction() { Before++; return true; } public void AfterInstruction(long a, long b) => After++; } + private sealed class FrameBus : SparseRecordingBus, IM68kPhysicalAddressMap + { public bool IsCpuPhysicalAddressMapped(uint address, int size, M68kBusAccessKind kind) => true; } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index d9ba1e99..094fc83c 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -10970,3 +10970,86 @@ Controls `ordinary-consolidation-controls-v1/verification.json` SHA-256: `540a76d28d66bb81ccdd79c6b45172850254bbf0c0dfcd4a26434adbcdf07508`. Milestone 6 remains **in progress**, `roadmapComplete=false`; no production candidate import or package publication is authorized by these results. + +## Private cold and relocated pending-write frames — 2026-10-08 + +`SyntheticM68020MoveWriteFrameTests` adds **163,584 passing cases / four +executions / 768 reports** on EC020, A1200, 020 and 030. The audit starts from +literal C023 short-frame bytes after reset, without executing an original MOVE +or relying on a generated-frame address. Frames sit at `0x6000`, `0x7000` and +`0x12346000`; EC020 transport and full-width models retain their distinct address +widths. This qualifies these selected private serialized images at relocated +addresses, not arbitrary silicon images or cross-model hardware state migration. + +The **147,456 completion cases** cover B/W/L pending writes and software-completed +DF-clear cycles, current ISP/MSP, restored user/user-M/ISP/MSP, all 32 CCRs and +zero-to-three retained instruction words in scalar and one-instruction batch +execution. Literal MOVE `(A7)+,(A1)` encodings deliberately have live registers +unrelated to the saved destination: returning must consume the serialized pending +cycle without reading/recalculating the source, incrementing A7 or recomputing +completed flags. The verifier checks every architectural register, all three +stack banks, complete memory including frame/destination guards, exception +sequence, exact write count/address/width, and three following literal MOVEQ +sentinels. Saved words must avoid refetch while remaining words use backing code. + +The **16,128 rejection cases** cover fourteen private-protocol exclusions: +foreign zero/read markers, version zero/two, pipe count four, nonzero reserved +state, read-cycle/wrong-size/bad-FC fields, T1/T0, odd return PC and unsupported +source/destination opcode forms. Each must report explicit emulator unsupported +execution before stack/SR/register or memory commitment. The normal opcode-fetch +PC advance is checked separately. Passing these tests means correct rejection +under the bounded private contract; it does **not** qualify the corresponding +hardware instruction/frame outcomes or remove their architectural gaps. + +Three sole CPU mutations detect **1,152** reserved-state acceptances, +**147,456** wrong stack pops and **138,240** recomputed-flag mismatches, with +all unaffected cases passing. Each uses the same complete source inventory and +fixture. The maintained command binds exact selection settings, all four xUnit +outcomes/counters, source and assembly hashes, every report key/weight/witness, +and TRX summaries. A separate frozen `--validate-only` replay passes unchanged. +An isolated build against the frozen production CPU also compiles the fixture; +its four cases are explicitly unavailable with the private audit flag absent. +That default-selection check is not passing instruction-execution coverage. + +Eight copied evidence controls reject missing reports, empty combinations, +untested or foreign cases, changed source, changed execution roster, changed +settings and missing mutation witnesses. Control manifest paths are rebased only +for validator fixtures; no CPU execution is claimed for those copies. Original +sources, outputs and evidence remain unchanged. The first control wrapper omitted +the helper's `ValueError` exception type; it stopped after the validator correctly +rejected empty selection. Its failed wrapper evidence is preserved, and a fresh +successor catches the actual exception type and verifies all eight controls. + +Reproduce with a complete immutable `MoveWritePipeV3` parent and its pinned +upstream directories, then use a fresh output: + +```powershell +python scripts/test-copper68k-move-write-frame.py ` + --qualified-parent-directory ` + --output +# --validate-only requires unchanged complete inputs and all reports. +``` + +The only source addition to that parent is the new fixture. All **39 private CPU +inputs** remain byte-identical to the full integration and immutable `.73` +consumer checkpoint. This focused run does not reexecute retained earlier tests +or amend the scope of their results; the parent is revalidated independently. +No production CPU change, candidate import, package publication or regression +retirement occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +The selected cold-image and returned-bank success contract is now covered. +Frame validation/internal-load faults, changed-bank refaults, entry faults, +trace/interrupt continuation, wider addressing/origins and physical partial +transfers remain required. General foreign hardware frames and native A7/trace/ +repeated-fault PC disagreements remain unqualified; hardware is unavailable. + +Proof `audits/MoveWriteFrameV1/proof.json` SHA-256: +`ca12149f0542b202f764e1cb7543d928cefc7c3c79830456e5e9ef294225b1b1`. +Controls `move-write-frame-controls-v2/verification.json` SHA-256: +`cd265cfc69ab7574a40720ac4ccd85e96e8c20881693d1f0e757174a886e40c9`. +Integration linkage `move-write-frame-integration-link-v1.json` SHA-256: +`36a422fb7422dddb9514b1784af24c244d0f8c300e8ad92243b3083af3e2bd2d`. +Control provenance `move-write-frame-controls-v2/provenance.json` SHA-256: +`07b8a5b4c7ae93ce1da3d43e0407645ec298778c365775186aa6053cb76efb8b`. +Production default-selection `move-write-frame-production-default-v1/verification.json` SHA-256: +`819c957c16dabff105af53d4149062beb3253a2a016f4790d1fcfe17de12f248`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index a9079e44..910b9151 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3482,3 +3482,23 @@ See the [mandatory consolidation gate](COPPER68K_REFERENCE_QUALIFICATION.md#mand Hardware is unavailable. Broader frame provenance, fault origins, trace and reference requirements remain open. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +### Milestone 6 private cold pending-write images — 2026-10-08 + +Four optional reference-discovery batches now pass 163,584 private-contract +cases / 768 reports across EC020/A1200/020/030. Literal C023 frames after reset +exercise three locations, B/W/L, pending/DF-clear cycles, current ISP/MSP, +all four return banks, all CCRs and every zero-to-three saved instruction length. +Fourteen malformed/foreign/unsupported private image classes must reject before +stack/status/data commitment. Registers, all stack banks, guarded memory, exact +write/exception counts and three following instruction words are checked. + +Wrong reserved-state acceptance, stack pop and flag recomputation are independently +detected; eight missing/changed evidence controls fail precisely. Frozen +revalidation passes. All 39 private CPU inputs are unchanged from the full suite +and `.73` consumer checkpoint; this adds only a fixture and maintained audit helper. +No CPU import, publication or old-test retirement occurs. These are bounded +software-protocol results, not hardware frame or trace qualification. General +foreign images, validation/load/entry faults, changed-bank refaults and broader +continuations remain required. Milestone 6 remains **in progress**, +`roadmapComplete=false`. See the [scope, command and evidence](COPPER68K_REFERENCE_QUALIFICATION.md#private-cold-and-relocated-pending-write-frames--2026-10-08). diff --git a/scripts/test-copper68k-move-write-frame.py b/scripts/test-copper68k-move-write-frame.py new file mode 100644 index 00000000..dabbdea0 --- /dev/null +++ b/scripts/test-copper68k-move-write-frame.py @@ -0,0 +1,119 @@ +"""Qualify cold/relocated private pending-write images and rejection atomicity. + +Uses an immutable pipe-qualified parent and unchanged private CPU bytes. This is +a software-contract audit, not hardware frame qualification or production import. +""" +import argparse, contextlib, hashlib, importlib.util, io, itertools, json, os, re, subprocess +from pathlib import Path +import xml.etree.ElementTree as ET + +REPO = Path(__file__).resolve().parents[1] +HELPER = REPO / 'scripts/test-copper68k-move-write-pipe.py' +FIXTURE = 'Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFrameTests.cs' +spec = importlib.util.spec_from_file_location('write_pipe', HELPER) +p = importlib.util.module_from_spec(spec); spec.loader.exec_module(p) +sha, load, save, require, inventory = p.sha, p.load, p.save, p.require, p.inventory +PARENT_PROOF = 'dcb7ea91da3d88b0b35d84c01f159e16191c0eed4c78707d65e4e3239c8fdca7' +VARIANTS = ['candidate', 'reserved', 'stack-pop', 'flags'] +MODELS = ['68EC020', '68020', '68030', 'A1200'] +INVALID = ['foreign-zero', 'foreign-read', 'version-zero', 'version-two', 'count-four', 'reserved', 'read-cycle', 'wrong-size', 'bad-fc', 'trace-t1', 'trace-t0', 'odd-pc', 'bad-source', 'bad-destination'] +FILTER = 'FullyQualifiedName~SyntheticM68020MoveWriteFrameTests' +ENV = {'COPPER68K_RUN_020_MOVE_WRITE_FRAME': '1'} +NS = {'t': 'http://microsoft.com/schemas/VisualStudio/TeamTest/2010'} + + +def sources(parent, variant): + data = {n: (parent/'candidate/source'/n).read_bytes() for n in inventory(parent/'candidate/source')} + require(len(data) == 236 and FIXTURE not in data, 'Missing or changed cold-frame parent inventory') + data[FIXTURE] = (REPO/FIXTURE).read_bytes() + text = data[p.CPU].decode('utf-8').replace('\r\n', '\n') + if variant == 'reserved': + text = p.once(text, 'reserved != 0 ||', 'false ||') + elif variant == 'stack-pop': + text = p.once(text, 'State.SetActiveStackPointer(unchecked(frame + 32));', 'State.SetActiveStackPointer(unchecked(frame + 34));') + elif variant == 'flags': + text = p.once(text, 'CompleteTiming(M68kInstructionTimingKey.Rte); _instructionPipe = pipe;', 'CompleteTiming(M68kInstructionTimingKey.Rte); _instructionPipe = pipe; SetMoveFlags(value, size);') + if variant != 'candidate': data[p.CPU] = text.encode('utf-8') + return data + + +def command(root): + return ['dotnet', 'test', str(root/'source/Copper68k.Tests/Copper68k.Tests.csproj'), '-c', 'Release', '--artifacts-path', str(root/'build'), '--filter', FILTER, '--logger', 'trx;LogFileName=audit.trx', '--results-directory', str(root)] + + +def cases(model, width, current, lane, variant): + invalid = lane in INVALID + rows = {} + for returned, location, count, ccr in itertools.product(['user', 'user-M', 'ISP', 'MSP'], range(3), [3] if invalid else range(4), [0, 31] if invalid else range(32)): + bad = (variant == 'reserved' and lane == 'reserved' or + variant == 'stack-pop' and not invalid or + variant == 'flags' and not invalid and ccr & 15 != 8) + key = f'{model}/MOVE/write-frame/width={width}/current={current}/returned={returned}/location={location}/count={count}/ccr={ccr}/lane={lane}' + rows[key] = {'mismatching' if bad else 'passing': 1} + return rows + + +def verify(root, variant, data): + record = load(root/'execution.json') + ids = {n: hashlib.sha256(v).hexdigest() for n, v in data.items()} + require(record['sources'] == ids == inventory(root/'source'), 'Changed complete cold-frame sources') + require(record['command'] == command(root) and record['settings'] == ENV and record['exit'] == (0 if variant == 'candidate' else 1), 'Wrong cold-frame command, settings or exit') + outputs = ['execution.log', 'audit.trx', 'build/bin/Copper68k/release/Copper68k.dll', 'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + require(record['outputs'] == {n: sha(root/n) for n in outputs}, 'Changed cold-frame outputs') + tree = ET.parse(root/'audit.trx'); tests = tree.findall('.//t:UnitTestResult', NS) + names = {f'{route}{kind}WriteFrames': 'Failed' if (variant == 'reserved' and kind == 'Invalid' or variant in ['stack-pop', 'flags'] and kind == 'Cold') else 'Passed' for route, kind in itertools.product(['Scalar', 'Batch'], ['Cold', 'Invalid'])} + require(len(tests) == 4 and {t.get('testName').split('.')[-1]: t.get('outcome') for t in tests} == names, 'Wrong cold-frame execution roster or outcomes') + c = tree.find('.//t:Counters', NS); failed = sum(x == 'Failed' for x in names.values()) + require(int(c.get('total')) == 4 and int(c.get('executed')) == 4 and int(c.get('passed')) == 4-failed and int(c.get('failed')) == failed and int(c.get('notExecuted')) == 0, 'Wrong cold-frame counters') + require(tree.find('.//t:ResultSummary', NS).get('outcome') == ('Completed' if variant == 'candidate' else 'Failed'), 'Wrong cold-frame run summary') + stdout = '\n'.join(t.text or '' for t in tree.findall('.//t:UnitTestResult/t:Output/t:StdOut', NS)) + totals = dict(passing=0, mismatching=0, unsupported=0, untested=0); reports = [] + for model, width, current, lane, mode in itertools.product(MODELS, [1, 2, 4], ['ISP', 'MSP'], ['pending', 'completed']+INVALID, ['scalar', 'batch']): + group = f'move-write-frame-{width}-{current}-{lane}-{mode}' + path = root/f'{model}-{group}.json'; d = load(path); wanted = cases(model, width, current, lane, variant) + counts = {s: sum(row.get(s, 0) for row in wanted.values()) for s in totals} + require(d['schema'] == 1 and d['model'] == model and d['group'] == group and d['xunitBatches'] == 1 and d['logicalCases'] == len(wanted) and d['combinations'] == wanted and d['counts'] == counts, 'Wrong cold-frame keys, weights or results: '+path.name) + bad = {k for k, v in wanted.items() if 'mismatching' in v} + require(len(d['failures']) == len(bad) and {f['id'] for f in d['failures']} == bad and all(f['status'] == 'mismatching' and f['reason'] for f in d['failures']), 'Wrong complete cold-frame failure witnesses') + if variant == 'reserved' and lane == 'reserved': + require(all(f['reason'] == 'Malformed or foreign private write frame was accepted' for f in d['failures']), 'Wrong reserved-frame mutation cause') + matches = re.findall(re.escape(model+'/'+group+': ')+r'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)', stdout) + require(len(matches) == 1 and json.loads(matches[0][0]) == counts and int(matches[0][1]) == len(wanted) and int(matches[0][2]) == len(wanted), 'Cold-frame report/TRX disagreement') + for status in totals: totals[status] += counts[status] + reports.append(dict(path=path.name, sha256=sha(path))) + require({f.name for f in root.glob('*.json')} == {f['path'] for f in reports}|{'execution.json'}, 'Missing or foreign cold-frame reports') + return dict(variant=variant, counts=totals, executions=4, reports=reports, executionSha256=sha(root/'execution.json')) + + +def audit(parent, output, validate): + require(sha(parent/'proof.json') == PARENT_PROOF, 'Wrong qualified cold-frame parent proof') + with contextlib.redirect_stdout(io.StringIO()): p.audit(parent.parent/'MemoryReadWriteV1', parent, True) + linkage = dict(producers={f.relative_to(REPO).as_posix(): sha(f) for f in [Path(__file__), HELPER, REPO/FIXTURE]}, parentProofSha256=PARENT_PROOF, variants=VARIANTS, settings=ENV) + if not validate: output.mkdir(parents=True, exist_ok=False); save(output/'inputs.json', linkage) + require(load(output/'inputs.json') == linkage, 'Changed cold-frame producers, parent or selection') + entries = [] + for variant in VARIANTS: + root = output/variant; data = sources(parent, variant) + if not validate: + (root/'source').mkdir(parents=True) + for n, value in data.items(): + target = root/'source'/n; target.parent.mkdir(parents=True, exist_ok=True); target.write_bytes(value) + env = {k: v for k, v in os.environ.items() if not k.startswith('COPPER68K_RUN_')} + env.update(ENV); env['COPPER68K_SYNTHETIC_REPORT_DIR'] = str(root) + with (root/'execution.log').open('w', encoding='utf-8') as log: + result = subprocess.run(command(root), env=env, stdout=log, stderr=subprocess.STDOUT) + outputs = ['execution.log', 'audit.trx', 'build/bin/Copper68k/release/Copper68k.dll', 'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + save(root/'execution.json', dict(command=command(root), settings=ENV, exit=result.returncode, sources={n: hashlib.sha256(v).hexdigest() for n, v in data.items()}, outputs={n: sha(root/n) for n in outputs})) + entry = verify(root, variant, data); entries.append(entry); print(variant, entry['counts'], flush=True) + proof = dict(schema=1, inputsSha256=sha(output/'inputs.json'), entries=entries, sourceCount=237, retainedEvidenceParentSha256=PARENT_PROOF, retentionReexecuted=False, productionImport=False, publication=False, hardwareQualified=False, architecturalPromotion=False, roadmapComplete=False) + if validate: require(load(output/'proof.json') == proof, 'Changed complete cold-frame proof') + else: save(output/'proof.json', proof) + print('Complete private cold-frame audit', sha(output/'proof.json'), flush=True) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--qualified-parent-directory', type=Path, required=True) + parser.add_argument('--output', type=Path, required=True) + parser.add_argument('--validate-only', action='store_true') + args = parser.parse_args(); audit(args.qualified_parent_directory.resolve(), args.output.resolve(), args.validate_only) From ab1d1cc4f54a81bc649d9e76f58770f143ab5627 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 05:19:55 +0300 Subject: [PATCH 141/211] Qualify pending-write frame validation and load faults --- Copper68k.Tests/Synthetic/README.md | 15 ++ ...SyntheticM68020MoveWriteFrameFaultTests.cs | 151 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 79 ++++++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 21 +++ .../test-copper68k-move-write-frame-fault.py | 118 ++++++++++++++ 5 files changed, 383 insertions(+), 1 deletion(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFrameFaultTests.cs create mode 100644 scripts/test-copper68k-move-write-frame-fault.py diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 783b924b..23ebed40 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1271,3 +1271,18 @@ preserved completed state and rejection atomicity under the private software contract. They do not qualify silicon internal images or promote architectural unsupported forms. Existing ordinary CI counts and full-suite checkpoint scopes remain unchanged. See the [scope and evidence](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#private-cold-and-relocated-pending-write-frames--2026-10-08). + +## Private pending-write frame faults + +`scripts/test-copper68k-move-write-frame-fault.py --qualified-parent-directory + --output ` executes the two optional +`SyntheticM68020MoveWriteFrameFaultTests` batches, retains the four cold-frame +batches, and runs four targeted mutations on isolated sources. The parent and +its complete pinned upstream chain must remain available. `--validate-only` +rejects missing or changed inputs, selections, reports and retained evidence. + +The candidate totals 476,928 cases: 313,344 new validation/load-fault cases plus +163,584 exactly retained cold-frame cases. Validation recovery and internal-load +halt remain distinct; these qualify selected private images and existing software +ordering. Physical BERR/partial transfers and general silicon images remain +unqualified. See the [scope and evidence](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-frame-validation-and-load-faults--2026-10-08). diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFrameFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFrameFaultTests.cs new file mode 100644 index 00000000..dd5ef5bb --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFrameFaultTests.cs @@ -0,0 +1,151 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Faults in the bounded private C023 software image. Internal-load halt and +// validation entry have distinct outcomes; neither retries the completed MOVE. +public sealed class SyntheticM68020MoveWriteFrameFaultTests(ITestOutputHelper output) +{ + private static readonly ModelSpec[] Models = ModelSpec.All.Where(x => x.Id is "68EC020" or "68020" or "68030" or "A1200").ToArray(); + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_FRAME_FAULT", "require private pending-write frame faults"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarWriteFrameFaults() => Audit(false); + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_FRAME_FAULT", "require private pending-write frame faults"), Trait("Suite", "ReferenceDiscovery")] + public void BatchWriteFrameFaults() => Audit(true); + + private static (uint Offset, int Width)[] Requests(int count) => + new (uint, int)[] { (0, 2), (2, 4), (6, 2), (30, 2), (8, 2), (10, 2), (16, 4), (20, 2), (22, 2), (24, 4), (30, 2) } + .Concat(Enumerable.Range(0, count).Select(n => (n == 0 ? 12u : n == 1 ? 14u : 28u, 2))) + .Concat(Enumerable.Range(0, 10).Select(n => (12u + (uint)n * 2, 2))).ToArray(); + + private void Audit(bool batch) + { + var failures = new List(); + foreach (var model in Models) + foreach (var width in new[] { 1, 2, 4 }) + foreach (var current in new[] { "ISP", "MSP" }) + foreach (var count in Enumerable.Range(0, 4)) + foreach (var loading in new[] { false, true }) + { + var report = new CoverageBatch(model.Id, $"move-write-frame-fault-{width}-{current}-count{count}-{(loading ? "load" : "validation")}-{(batch ? "batch" : "scalar")}"); + var bus = new FaultBus(); var m = new SyntheticMachine(model, bus); var requests = Requests(count); + foreach (var returned in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var location in Enumerable.Range(0, 2)) + foreach (var ccr in new[] { 0, 31 }) + foreach (var software in new[] { false, true }) + foreach (var ri in loading ? Enumerable.Range(4, requests.Length - 4) : Enumerable.Range(0, 4)) + foreach (var lane in Enumerable.Range(0, requests[ri].Width)) + { + var id = $"{model.Id}/MOVE/write-frame-fault/width={width}/current={current}/returned={returned}/location={location}/count={count}/ccr={ccr}/software={software}/request={ri}/byte={lane}"; + try { Run(m, bus, batch, width, current, returned, location, count, ccr, software, requests, ri, lane); report.Record(id, "passing", null); } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException) { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + try { report.Complete(output); } catch (Exception ex) { failures.Add(ex); } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(x => x.Message))); + } + + private static void Run(SyntheticMachine m, FaultBus bus, bool batch, int width, string current, string returned, int location, int count, int ccr, bool software, (uint Offset, int Width)[] requests, int ri, int lane) + { + bus.Disarm(); m.Reset(); _ = SyntheticExecution.Prepare(m, [0x4e73, 0x4e71]); + var frame = location == 0 ? 0x6000u : 0x12346000u; + var saved = (ushort)(0x700 | ccr | (returned is "ISP" or "MSP" ? 0x2000 : 0) | (returned is "user-M" or "MSP" ? 0x1000 : 0)); + var currentSr = (ushort)((current == "ISP" ? 0x2700 : 0x3700) | ccr); + var value = 0x80a1b2c3u & (width == 1 ? 0xffu : width == 2 ? 0xffffu : uint.MaxValue); + for (uint a = frame - 100; a < frame + 40; a++) m.InitializePhysical(a, 0xa5, 1); + for (uint a = 0x43f8; a < 0x4510; a++) m.InitializePhysical(a, 0xa5, 1); + m.InitializePhysical(0x9020, 0x4e73, 2); m.InitializePhysical(0x9022, 0x4e71, 2); + m.InitializePhysical(0x2000, 0x7e2a, 2); m.InitializePhysical(0x2002, 0x7c2b, 2); m.InitializePhysical(0x2004, 0x7a2c, 2); m.InitializePhysical(0x2006, 0x4e71, 2); + m.InitializePhysical(frame, saved, 2); m.InitializePhysical(frame + 2, 0x2000, 4); m.InitializePhysical(frame + 6, 0xa008, 2); m.InitializePhysical(frame + 8, 0xc023, 2); + m.InitializePhysical(frame + 10, (uint)((software ? 0 : 0x100) | (width == 1 ? 0x10 : width == 2 ? 0x20 : 0) | 5), 2); + m.InitializePhysical(frame + 12, 0x7e11, 2); m.InitializePhysical(frame + 14, 0x7c22, 2); m.InitializePhysical(frame + 16, 0x4400, 4); + m.InitializePhysical(frame + 20, (uint)(width == 1 ? 0x129f : width == 2 ? 0x329f : 0x229f), 2); + m.InitializePhysical(frame + 22, (uint)(1 | count << 8), 2); m.InitializePhysical(frame + 24, value, 4); m.InitializePhysical(frame + 28, 0x7a33, 2); m.InitializePhysical(frame + 30, 0, 2); + if (software) m.InitializePhysical(0x4400, value, width); + m.Core.State.A[0] = 0x4204; m.Core.State.A[1] = 0x4500; + m.Core.State.SetUserStackPointer(0x7800); m.Core.State.SetInterruptStackPointer(current == "ISP" ? frame : 0x4700); m.Core.State.SetMasterStackPointer(current == "MSP" ? frame : 0x7400); + m.Core.State.StatusRegister = currentSr; m.Core.State.SetActiveStackPointer(frame); + var e = ArchitecturalExpectation.Capture(m); var serial = m.Core.State.ExceptionSequence; + var request = requests[ri]; var byteOffset = request.Offset + (uint)lane; + var occurrence = requests.Take(ri + 1).Count(x => x.Width == request.Width && byteOffset >= x.Offset && byteOffset < x.Offset + (uint)x.Width); + m.Bus.Accesses.Clear(); bus.Arm(m.Model.Physical(frame + byteOffset), request.Width, occurrence); + Step(m, batch); + if (bus.Rejected.Count != 1 || bus.Rejected[0] != (m.Model.Physical(frame + request.Offset), request.Width)) throw new InvalidOperationException("Wrong selected frame fault request"); + CheckReads(m, frame, requests.Take(ri)); + if (ri >= 4) + { + e.Pc = 0x1002; e.Halted = true; Check(m, e); CheckBanks(m, current, frame); + if (m.Core.State.ExceptionSequence != serial || m.Bus.Accesses.Any(a => a.Write)) throw new InvalidOperationException("Internal frame-load fault committed effects or stacked another image"); + var accesses = m.Bus.Accesses.Count; + if (batch) { if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, new Boundary()) != 0) throw new InvalidOperationException("Halted frame load executed a batch instruction"); } + else m.Core.ExecuteInstruction(); + Check(m, e); + if (m.Bus.Accesses.Count != accesses) throw new InvalidOperationException("Halted frame load retried a memory access"); + return; + } + + var outer = frame - 92; + for (uint n = 0; n < 92; n += 2) e.Write(outer + n, 0, 2, m.Model); + e.Write(outer, currentSr, 2, m.Model); e.Write(outer + 2, 0x1000, 4, m.Model); e.Write(outer + 6, 0xb008, 2, m.Model); e.Write(outer + 8, 0xc021, 2, m.Model); + e.Write(outer + 10, (uint)(0x145 | (request.Width == 2 ? 0x20 : 0)), 2, m.Model); e.Write(outer + 16, frame + request.Offset, 4, m.Model); e.Write(outer + 20, frame, 4, m.Model); + e.Write(outer + 28, (uint)((ri >= 1 ? saved : 0) << 16) | (ri >= 3 ? 0xa008u : 0), 4, m.Model); e.Write(outer + 32, ri >= 2 ? 0x2000u : 0, 4, m.Model); + e.Write(outer + 40, (uint)(ri == 3 ? 4 : ri), 2, m.Model); + e.A[7] = outer; if (current == "MSP") e.MasterStackPointer = outer; + e.Pc = 0x9020; e.ExceptionVector = 2; Check(m, e); CheckBanks(m, current, outer); + if (m.Core.State.ExceptionSequence != serial + 1 || !m.Bus.Accesses.Where(a => a.Write).Select(a => (a.Address, a.Width)).SequenceEqual(Enumerable.Range(0, 46).Select(n => (m.Model.Physical(frame - 2u - (uint)n * 2), 2)))) + throw new InvalidOperationException("Validation fault changed exception count or entry writes"); + + var start = m.Bus.Accesses.Count; + var isp = current == "ISP" ? frame + 32 : 0x4700u; var msp = current == "MSP" ? frame + 32 : 0x7400u; + e.A[7] = returned == "ISP" ? isp : returned == "MSP" ? msp : 0x7800u; + e.InactiveStackPointer = returned is "ISP" or "MSP" ? 0x7800u : isp; e.MasterStackPointer = msp; + e.Sr = saved; e.Pc = 0x2000; e.Write(0x4400, value, width, m.Model); + Step(m, batch); Check(m, e); CheckBanks(m, current, frame + 32); + CheckReads(m, frame, requests.Skip(ri), start); + if (!m.Bus.Accesses.Skip(start).Where(a => a.Write).Select(a => (a.Address, a.Width)).SequenceEqual(software ? Array.Empty<(uint, int)>() : new[] { (0x4400u, width) })) + throw new InvalidOperationException("Recovered validation fault repeated or misplaced the pending write"); + for (var n = 0; n < 3; n++) + { + e.D[7 - n] = (uint)(n < count ? new[] { 0x11, 0x22, 0x33 }[n] : 0x2a + n); e.Sr = (ushort)(e.Sr & 0xfff0); e.Pc += 2; + var before = m.Bus.Accesses.Count; Step(m, batch); Check(m, e); CheckBanks(m, current, frame + 32); + if (m.Bus.Accesses.Skip(before).Count(a => !a.Write && a.Kind == M68kBusAccessKind.CpuInstructionFetch && a.Address == 0x2000u + (uint)n * 2) != (n < count ? 0 : 1)) + throw new InvalidOperationException("Recovered frame fault changed retained instruction words"); + } + if (m.Core.State.ExceptionSequence != serial + 1 || bus.Rejected.Count != 1 || m.Bus.Accesses.Any(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && + !(a.Address >= m.Model.Physical(outer) && a.Address + (uint)a.Width <= m.Model.Physical(frame) + 32) && a.Address != 8)) + throw new InvalidOperationException("Recovered frame fault replayed source or changed exception provenance"); + } + + private static void CheckReads(SyntheticMachine m, uint frame, IEnumerable<(uint Offset, int Width)> expected, int start = 0) + { + var reads = m.Bus.Accesses.Skip(start).Where(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address >= m.Model.Physical(frame) && a.Address < m.Model.Physical(frame) + 32).Select(a => (a.Address, a.Width)); + if (!reads.SequenceEqual(expected.Select(x => (m.Model.Physical(frame + x.Offset), x.Width)))) throw new InvalidOperationException("Frame fault replayed or skipped completed validation/load reads"); + } + private static void CheckBanks(SyntheticMachine m, string current, uint stack) + { + var isp = current == "ISP" ? stack : 0x4700u; var msp = current == "MSP" ? stack : 0x7400u; + if (m.Core.State.UserStackPointer != 0x7800 || m.Core.State.InterruptStackPointer != isp || m.Core.State.MasterStackPointer != msp) + throw new InvalidOperationException("Frame fault changed an inactive or completed stack bank"); + } + private static void Check(SyntheticMachine m, ArchitecturalExpectation e) { var error = e.Verify(m); if (error != null) throw new InvalidOperationException(error); } + private static void Step(SyntheticMachine m, bool batch) + { + if (!batch) { m.Core.ExecuteInstruction(); return; } + var b = new Boundary(); if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, b) != 1 || b.Before != 1 || b.After != 1) throw new InvalidOperationException("Frame fault batch boundaries differ"); + } + private sealed class Boundary : IM68kInstructionBoundary { public int Before, After; public bool BeforeInstruction() { Before++; return true; } public void AfterInstruction(long a, long b) => After++; } + private sealed class FaultBus : SparseRecordingBus, IM68kPhysicalAddressMap + { + private uint address; private int width, occurrence; + internal readonly List<(uint Address, int Width)> Rejected = []; + internal void Disarm() { occurrence = 0; Rejected.Clear(); } + internal void Arm(uint at, int size, int nth) { Disarm(); address = at; width = size; occurrence = nth; } + public bool IsCpuPhysicalAddressMapped(uint at, int size, M68kBusAccessKind kind) + { + if (occurrence == 0 || kind != M68kBusAccessKind.CpuDataRead || size != width || unchecked(address - at) >= size) return true; + if (--occurrence != 0) return true; + Rejected.Add((at, size)); return false; + } + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 094fc83c..ae18b844 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -10987,7 +10987,7 @@ zero-to-three retained instruction words in scalar and one-instruction batch execution. Literal MOVE `(A7)+,(A1)` encodings deliberately have live registers unrelated to the saved destination: returning must consume the serialized pending cycle without reading/recalculating the source, incrementing A7 or recomputing -completed flags. The verifier checks every architectural register, all three +completed flags. The verifier checks PC/SR, D/A registers, all three stack banks, complete memory including frame/destination guards, exception sequence, exact write count/address/width, and three following literal MOVEQ sentinels. Saved words must avoid refetch while remaining words use backing code. @@ -11053,3 +11053,80 @@ Control provenance `move-write-frame-controls-v2/provenance.json` SHA-256: `07b8a5b4c7ae93ce1da3d43e0407645ec298778c365775186aa6053cb76efb8b`. Production default-selection `move-write-frame-production-default-v1/verification.json` SHA-256: `819c957c16dabff105af53d4149062beb3253a2a016f4790d1fcfe17de12f248`. + +## Private pending-write frame validation and load faults — 2026-10-08 + +`SyntheticM68020MoveWriteFrameFaultTests` adds **313,344 passing cases** and +reexecutes all **163,584 cold-frame cases** without changing their complete +reports: **476,928 cases / six executions / 1,152 reports** in the candidate +selection. All EC020/A1200/020/030 profiles use the public factory and isolated +sources/builds. The sole source addition to the cold-frame parent is this fixture; +all **39 private CPU inputs** remain byte-identical to the full integration and +`.73` consumer checkpoint. No CPU correction or new package is needed. + +The fixture denies every byte of every request in the literal private C023 +read protocol, including the far-end validation read, explicit context/output +loads, each active pipe word and every opaque tail word. Repeated reads have +separate occurrence selectors. It covers B/W/L writes, pending/DF-clear cycles, +ISP/MSP entry, all four returned banks, low/high frame locations, pipe lengths +zero-to-three, CCR 0/31, and scalar/one-instruction batch routes. These are mapped +access rejections, not physical partial-cycle or external BERR-pin qualification. + +The **61,440 validation cases** check an intact original image and a new private +format-B validation image containing the original RTE PC, fault address, phase, +previously read SR/PC/format and empty software input. The existing private word +write sequence, PC/SR, D/A registers, stack/memory state and exact exception count are +checked. Executing the handler's RTE then resumes the rejected read, skips all +completed header reads, consumes both frames, selects the supplied return bank, +and completes only the pending write. Three literal following MOVEQ sentinels +verify retained instruction words and backing-code fetches. Source/EA/flags are +not recalculated and software-completed cycles perform no pending write. + +The **251,904 internal-load cases** must halt without another exception image, +stack/status commitment or destination write; a subsequent scalar or batch call +must perform no memory access. This preserves the already qualified distinction +between validation fault recovery and internal-load halt described by MC68020UM +6.1.12 / MC68030UM 8.1.13, while extending it to the selected private pending-write +images. The opaque image and exact software request/write order are not silicon +internal-state claims. + +Four sole mutations produce **251,904** missing-halt failures, **251,904** premature +stack-commit failures, **49,152** completed-validation replay failures and +**61,440** wrong saved-PC failures. An independent audit checks every failure's +specific state/read/memory cause and all unaffected passing cases. The maintained +command verifies complete source/assembly/selection identities, exact xUnit +rosters/counters, report keys/weights/witnesses, TRX summaries, and byte-for-byte +retained report content. Frozen `--validate-only` revalidation passes unchanged. + +Ten copied evidence controls reject missing/empty/untested/foreign new cases, +changed source, roster or settings, missing mutation witnesses, and changed or +missing cold-frame retention. Their rebased manifests are validator fixtures, +with no CPU execution claim. Original evidence remains unchanged. A separate +isolated production-source build compiles the new fixture and discovers its two +optional cases as unavailable without the private flag; this is not instruction +execution coverage. + +```powershell +python scripts/test-copper68k-move-write-frame-fault.py ` + --qualified-parent-directory ` + --output +# --validate-only requires the complete unchanged parent/input/report chain. +``` + +The selected C023 validation/load-fault contract is now covered. Software-supplied +validation input, validation refaults/entry failures, returned-bank pending-write +refaults, trace/interrupts and broader origins/images remain required. General +hardware images and native A7/trace/repeated-fault-PC disagreements remain +architecturally unqualified. No production CPU import, publication or regression +retirement occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +Proof `audits/MoveWriteFrameFaultV1/proof.json` SHA-256: +`93ec2beed12410b94ce6cf448e1b5bc67e010474edf5ed7b20b2b7a765ae123e`. +Controls `move-write-frame-fault-controls-v1/verification.json` SHA-256: +`2ec5318c296b31b06c1d71c3a175d3709fd0dde0ce5abebcad682c9159b47bbd`. +Causal/source linkage `move-write-frame-fault-causal-link-v1.json` SHA-256: +`cb915cbb58d33cf25195b8b3d96ebde8522b1a48ed59e6bb95695e1de402f34d`. +Production default-selection `move-write-frame-fault-production-default-v1/verification.json` SHA-256: +`a721401813cfa32616267476dabacd2105a495971574a9a4a6421f2ad88819ff`. +Complete linkage `move-write-frame-fault-complete-link-v1.json` SHA-256: +`214e91c17a1cc6911f697939c6bb8a935be54ca32df52374b9406b910f7cbece`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 910b9151..4f92dfc9 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3502,3 +3502,24 @@ software-protocol results, not hardware frame or trace qualification. General foreign images, validation/load/entry faults, changed-bank refaults and broader continuations remain required. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [scope, command and evidence](COPPER68K_REFERENCE_QUALIFICATION.md#private-cold-and-relocated-pending-write-frames--2026-10-08). + +### Milestone 6 private pending-write frame faults — 2026-10-08 + +Two optional batches add 313,344 passing cases covering every byte and occurrence +of the selected C023 validation/load requests across all four 020/030 profiles. +They retain 163,584 cold-frame cases exactly: 476,928 cases / six executions / +1,152 reports. Validation faults preserve the original frame, serialize the +completed phase and recover through explicit RTE without repeating prior reads; +internal-load faults halt without committed stack/data effects or a second image. +Both outcomes check PC/SR, D/A registers, memory, banks, exception counts, +request order and resumed sentinels. CCR scope is 0/31 for this structural group. + +Four mutations detect missing halt, early stack commitment, completed-read replay +and wrong saved PC with precise causes; ten corrupted evidence fixtures fail. +Frozen revalidation passes. An isolated production build confirms compilation +and two optional unavailable cases, not passing instruction coverage. All 39 +private CPU inputs remain unchanged; no CPU correction, import, publication or +test retirement occurs. Supplied input, validation refaults/entry failures, +returned-bank pending-write refaults and broader architectural requirements remain +open. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the +[frame-fault scope and evidence](COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-frame-validation-and-load-faults--2026-10-08). diff --git a/scripts/test-copper68k-move-write-frame-fault.py b/scripts/test-copper68k-move-write-frame-fault.py new file mode 100644 index 00000000..ce41ad3f --- /dev/null +++ b/scripts/test-copper68k-move-write-frame-fault.py @@ -0,0 +1,118 @@ +"""Audit validation recovery and internal-load halt for private pending writes. + +Every byte of every selected request is denied, including repeated frame reads. +No production CPU import or silicon internal-image qualification occurs. +""" +import argparse, contextlib, hashlib, importlib.util, io, itertools, json, os, re, subprocess +from pathlib import Path +import xml.etree.ElementTree as ET +REPO = Path(__file__).resolve().parents[1] +HELPER = REPO/'scripts/test-copper68k-move-write-frame.py' +FIXTURE = 'Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFrameFaultTests.cs' +spec = importlib.util.spec_from_file_location('write_frame', HELPER) +m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m) +sha, load, save, require, inventory = m.sha, m.load, m.save, m.require, m.inventory +PARENT_PROOF = 'ca12149f0542b202f764e1cb7543d928cefc7c3c79830456e5e9ef294225b1b1' +CPU = 'Copper68k/M68kAdvancedTimingInterpreter.Rte020.cs' +VARIANTS = ['candidate', 'no-halt', 'early-commit', 'validation-replay', 'fault-pc'] +FILTER = 'FullyQualifiedName~SyntheticM68020MoveWriteFrameFaultTests' +ENV = {'COPPER68K_RUN_020_MOVE_WRITE_FRAME_FAULT': '1', 'COPPER68K_RUN_020_MOVE_WRITE_FRAME': '1'} +NS = m.NS + + +def requests(count): + return [(0, 2), (2, 4), (6, 2), (30, 2), (8, 2), (10, 2), (16, 4), (20, 2), (22, 2), (24, 4), (30, 2)]+[(x, 2) for x in [12, 14, 28][:count]]+[(x, 2) for x in range(12, 32, 2)] + + +def sources(parent, variant): + data = {n: (parent/'candidate/source'/n).read_bytes() for n in inventory(parent/'candidate/source')} + require(len(data) == 237 and FIXTURE not in data, 'Missing complete frame-fault parent sources') + data[FIXTURE] = (REPO/FIXTURE).read_bytes(); text = data[CPU].decode('utf-8').replace('\r\n', '\n') + if variant == 'no-halt': text = m.p.once(text, 'State.Halted = true;\n _instructionPipe.Reset();', 'State.Halted = false;\n _instructionPipe.Reset();') + elif variant == 'early-commit': text = m.p.once(text, 'State.Halted = true;\n _instructionPipe.Reset();', 'State.SetActiveStackPointer(unchecked(State.A[7] + 32));\n State.Halted = true;\n _instructionPipe.Reset();') + elif variant == 'validation-replay': text = m.p.once(text, 'ExecuteRte020(context, (ssw & 0x100) == 0 ? input : null);', 'context.Phase = 0; ExecuteRte020(context, (ssw & 0x100) == 0 ? input : null);') + elif variant == 'fault-pc': text = m.p.once(text, 'words[0] = sr; Long(2, context.InstructionPc);', 'words[0] = sr; Long(2, unchecked(context.InstructionPc + 2));') + if variant != 'candidate': data[CPU] = text.encode('utf-8') + return data + + +def command(root, variant): + selection = FILTER+('|'+m.FILTER if variant == 'candidate' else '') + return ['dotnet', 'test', str(root/'source/Copper68k.Tests/Copper68k.Tests.csproj'), '-c', 'Release', '--artifacts-path', str(root/'build'), '--filter', selection, '--logger', 'trx;LogFileName=audit.trx', '--results-directory', str(root)] + + +def expected(model, width, current, count, loading, variant): + rows = {}; reads = requests(count) + for returned, location, ccr, software, ri in itertools.product(['user', 'user-M', 'ISP', 'MSP'], range(2), [0, 31], [False, True], range(4, len(reads)) if loading else range(4)): + for lane in range(reads[ri][1]): + bad = variant in ['no-halt', 'early-commit'] and loading or variant == 'validation-replay' and not loading and ri > 0 or variant == 'fault-pc' and not loading + key = f'{model}/MOVE/write-frame-fault/width={width}/current={current}/returned={returned}/location={location}/count={count}/ccr={ccr}/software={software}/request={ri}/byte={lane}' + rows[key] = {'mismatching' if bad else 'passing': 1} + return rows + + +def verify(root, variant, data, retained, parent_names): + record = load(root/'execution.json'); ids = {n: hashlib.sha256(v).hexdigest() for n, v in data.items()} + require(record['sources'] == ids == inventory(root/'source'), 'Changed complete frame-fault sources') + require(record['command'] == command(root, variant) and record['settings'] == ENV and record['exit'] == (0 if variant == 'candidate' else 1), 'Wrong frame-fault command, settings or exit') + outputs = ['execution.log', 'audit.trx', 'build/bin/Copper68k/release/Copper68k.dll', 'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + require(record['outputs'] == {n: sha(root/n) for n in outputs}, 'Changed frame-fault outputs') + tree = ET.parse(root/'audit.trx'); tests = tree.findall('.//t:UnitTestResult', NS) + names = {f'{route}WriteFrameFaults': 'Passed' if variant == 'candidate' else 'Failed' for route in ['Scalar', 'Batch']} + if variant == 'candidate': names.update(parent_names) + require(len(tests) == len(names) and {t.get('testName').split('.')[-1]: t.get('outcome') for t in tests} == names, 'Wrong frame-fault roster or outcomes') + c = tree.find('.//t:Counters', NS); failed = sum(x == 'Failed' for x in names.values()) + require(int(c.get('total')) == len(names) and int(c.get('executed')) == len(names) and int(c.get('passed')) == len(names)-failed and int(c.get('failed')) == failed and int(c.get('notExecuted')) == 0, 'Wrong frame-fault counters') + require(tree.find('.//t:ResultSummary', NS).get('outcome') == ('Completed' if variant == 'candidate' else 'Failed'), 'Wrong frame-fault run summary') + stdout = '\n'.join(t.text or '' for t in tree.findall('.//t:UnitTestResult/t:Output/t:StdOut', NS)); reports = []; totals = dict(passing=0, mismatching=0, unsupported=0, untested=0) + def summary(d): + matches = re.findall(re.escape(d['model']+'/'+d['group']+': ')+r'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)', stdout) + require(len(matches) == 1 and json.loads(matches[0][0]) == d['counts'] and int(matches[0][1]) == d['logicalCases'] and int(matches[0][2]) == len(d['combinations']), 'Frame-fault report/TRX disagreement') + for model, width, current, count, loading, mode in itertools.product(m.MODELS, [1, 2, 4], ['ISP', 'MSP'], range(4), [False, True], ['scalar', 'batch']): + group = f'move-write-frame-fault-{width}-{current}-count{count}-'+('load' if loading else 'validation')+'-'+mode + path = root/f'{model}-{group}.json'; d = load(path); wanted = expected(model, width, current, count, loading, variant) + counts = {s: sum(v.get(s, 0) for v in wanted.values()) for s in totals} + require(d['schema'] == 1 and d['model'] == model and d['group'] == group and d['xunitBatches'] == 1 and d['logicalCases'] == len(wanted) and d['combinations'] == wanted and d['counts'] == counts, 'Wrong frame-fault keys, weights or results: '+path.name) + bad = {k for k, v in wanted.items() if 'mismatching' in v} + require(len(d['failures']) == len(bad) and {f['id'] for f in d['failures']} == bad and all(f['status'] == 'mismatching' and f['reason'] for f in d['failures']), 'Wrong complete frame-fault witnesses') + summary(d) + for s in totals: totals[s] += counts[s] + reports.append(dict(path=path.name, sha256=sha(path))) + if variant == 'candidate': + require(len(retained) == 768, 'Missing complete cold-frame retention') + for name, wanted in retained.items(): + path = root/name; d = load(path); require(d == wanted, 'Changed retained cold-frame report: '+name); summary(d); reports.append(dict(path=name, sha256=sha(path))) + require({p.name for p in root.glob('*.json')} == {p['path'] for p in reports}|{'execution.json'}, 'Missing or foreign frame-fault reports') + return dict(variant=variant, counts=totals, retentionCases=163584 if variant == 'candidate' else 0, executions=len(names), reports=reports, executionSha256=sha(root/'execution.json')) + + +def audit(parent, output, validate): + require(sha(parent/'proof.json') == PARENT_PROOF, 'Wrong frame-fault parent proof') + with contextlib.redirect_stdout(io.StringIO()): m.audit(parent.parent/'MoveWritePipeV3', parent, True) + linkage = dict(producers={p.relative_to(REPO).as_posix(): sha(p) for p in [Path(__file__), HELPER, REPO/FIXTURE]}, parentProofSha256=PARENT_PROOF, variants=VARIANTS, settings=ENV) + if not validate: output.mkdir(parents=True, exist_ok=False); save(output/'inputs.json', linkage) + require(load(output/'inputs.json') == linkage, 'Changed frame-fault producers, parent or selection') + retained = {p.name: load(p) for p in (parent/'candidate').glob('*-move-write-frame-*.json')} + tree = ET.parse(parent/'candidate/audit.trx'); parent_names = {t.get('testName').split('.')[-1]: t.get('outcome') for t in tree.findall('.//t:UnitTestResult', NS)} + require(len(parent_names) == 4 and set(parent_names.values()) == {'Passed'}, 'Incomplete cold-frame parent roster') + entries = [] + for variant in VARIANTS: + root = output/variant; data = sources(parent, variant) + if not validate: + (root/'source').mkdir(parents=True) + for n, value in data.items(): + p = root/'source'/n; p.parent.mkdir(parents=True, exist_ok=True); p.write_bytes(value) + env = {k: v for k, v in os.environ.items() if not k.startswith('COPPER68K_RUN_')}; env.update(ENV); env['COPPER68K_SYNTHETIC_REPORT_DIR'] = str(root) + with (root/'execution.log').open('w', encoding='utf-8') as log: result = subprocess.run(command(root, variant), env=env, stdout=log, stderr=subprocess.STDOUT) + outputs = ['execution.log', 'audit.trx', 'build/bin/Copper68k/release/Copper68k.dll', 'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + save(root/'execution.json', dict(command=command(root, variant), settings=ENV, exit=result.returncode, sources={n: hashlib.sha256(v).hexdigest() for n, v in data.items()}, outputs={n: sha(root/n) for n in outputs})) + entry = verify(root, variant, data, retained, parent_names); entries.append(entry); print(variant, entry['counts'], 'retention', entry['retentionCases'], flush=True) + proof = dict(schema=1, inputsSha256=sha(output/'inputs.json'), entries=entries, sourceCount=238, productionImport=False, publication=False, hardwareQualified=False, architecturalPromotion=False, roadmapComplete=False) + if validate: require(load(output/'proof.json') == proof, 'Changed complete frame-fault proof') + else: save(output/'proof.json', proof) + print('Complete private frame-fault audit', sha(output/'proof.json'), flush=True) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__); parser.add_argument('--qualified-parent-directory', type=Path, required=True); parser.add_argument('--output', type=Path, required=True); parser.add_argument('--validate-only', action='store_true') + args = parser.parse_args(); audit(args.qualified_parent_directory.resolve(), args.output.resolve(), args.validate_only) From cae17a9899ac26e94fcd78aeed5e2d921137cb3b Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 05:53:30 +0300 Subject: [PATCH 142/211] Account for broad reference first failures without promoting coverage --- Copper68k.Tests/Synthetic/README.md | 13 + ..._BASIC_REFERENCE_QUALIFICATION_LEDGER.json | 1489 +++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 83 +- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 19 + ...st-copper68k-basic-qualification-ledger.py | 94 ++ 5 files changed, 1692 insertions(+), 6 deletions(-) create mode 100644 docs/COPPER68K_BASIC_REFERENCE_QUALIFICATION_LEDGER.json create mode 100644 scripts/test-copper68k-basic-qualification-ledger.py diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 23ebed40..54a82f43 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1286,3 +1286,16 @@ The candidate totals 476,928 cases: 313,344 new validation/load-fault cases plus halt remain distinct; these qualify selected private images and existing software ordering. Physical BERR/partial transfers and general silicon images remain unqualified. See the [scope and evidence](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-frame-validation-and-load-faults--2026-10-08). + +## Broad reference first-failure review + +`scripts/test-copper68k-basic-qualification-ledger.py --basic-audit-directory + --qualified-report-directory + --output ` verifies the +54 observed non-passing model/family rows against the committed qualification +ledger and pinned reference evidence. It validates a failed-audit checkpoint; +it does not run CPU instructions or make the raw Basic audit passing. + +Every row retains its original status and marks later raw failures unobserved. +Missing fixtures, mappings, counterparts and unsupported promotion claims fail. +See the [scope, identities and remaining questions](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#basic-first-failure-qualification-ledger--2026-10-08). diff --git a/docs/COPPER68K_BASIC_REFERENCE_QUALIFICATION_LEDGER.json b/docs/COPPER68K_BASIC_REFERENCE_QUALIFICATION_LEDGER.json new file mode 100644 index 00000000..9d936b91 --- /dev/null +++ b/docs/COPPER68K_BASIC_REFERENCE_QUALIFICATION_LEDGER.json @@ -0,0 +1,1489 @@ +{ + "schema": 1, + "scope": "Observed first failures only; qualified counterpart presence never makes a raw Basic directory pass or qualifies unseen later cases.", + "sourceScope": "Frozen production CPU/test inputs used by full integration; focused private frame fixtures are outside this raw selection.", + "generatorPin": "025b999239800357e95065fe5b9a15ea5b300fa7", + "runnerPin": "7a83745d6c6159bc74ab0471578ffc8bc244e66e", + "rawManifestSha256": "b5741ca90fa75d3510ef25bac33d2d36d6a92ab2283f0666e762a5f1853a38ed", + "rawNativeSha256": "135d8c7d01137bbef2467e34207444984fd47a9076041a0c251848d2518b32ce", + "rawReportSha256": "4a08c9eac033fcf8320f9da5b8ff8c430ebb6695bcf60e86cb3705a73f507fa7", + "rawInputRecordSha256": "9386f2a388943515f85cc5182cfbf285ae086c54ce6dee96fd13437576502f88", + "rawExecutionRecordSha256": "dcbaf9a37da1edb1ad7bfa1791909a7b3fb2f25b79dce67a67d4ca4ca06cfd3f", + "fullIntegrationProofSha256": "a17fb76bcca015c9cebfc86072bc7c6ff977a69070295f79bc765589bdd43c4b", + "qualifiedReports": { + "winuae-breakpoint-audit.json": "71d97459ce449fadb386c7993cd6c53c6de9afa842a53d53561876ea1f285a4f", + "winuae-moves-audit.json": "2f85216f60919a2bb5ca880ec223d23d986d075a639292ad756c8518072087c9", + "winuae-trap-bounds-audit.json": "5560b6f7b2f42e4a99ca4e7b52359d4a0ed1e2e25f0b8cddbdd5ed941e15d9dc", + "winuae-long-arithmetic-audit.json": "983fe3820cfd053846b785874932b1addeceb3bce891f6182f03ca4f4b5a62bd", + "winuae-word-division-audit.json": "572a65f0b64a69773813b4e6d5acdc39a6301dab3bf9fc1db64b799f54a95655", + "winuae-cas2-audit.json": "69ab78d322335c3298787bf69e2406362e58e2940dfb7b64cdf5f941523c2743", + "winuae-cache-encodings-audit.json": "6b1d51c0e068907e8998ae2a9b587a3929489c649c8bec9d63259fc6906b569b", + "winuae-cas-audit.json": "4668d2566f2a26ebcb384c5f6594240bfb6f0439c65c9eaa3d8ca1a204b57656", + "winuae-lpstop-audit.json": "959afa70a7ced5d7a0be0366e26c942b91106d481cb79536d76fb7314853e1d4", + "winuae-move16-audit.json": "6810ed54ef8773a89f6280ab8ebf529f1e5270665dc19f64d1c0ee7808553d3a" + }, + "rawCounts": { + "passing": 1327, + "mismatching": 46, + "unsupported": 8, + "untested": 0, + "executedCases": 11478371, + "exceptionFrames": 1668069 + }, + "classifications": { + "qualified-reference-correction": 40, + "reserved-or-undefined-input": 12, + "unresolved-architecture": 2 + }, + "entries": [ + { + "model": "68010", + "family": "BKPT", + "diagnosticProfile": true, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "4848", + "4E71", + "4AFC", + "00FA", + "0400", + "00C7" + ], + "rawDetailSha256": "1b5856db0d26f0d44d1eea1eae7665462c0757aa204ac71d47e253bc29b4fae2", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Fallback illegal exception must save opcode PC; the reference advances its internal PC before fallback.", + "qualifiedCounterpart": { + "report": "winuae-breakpoint-audit.json", + "model": "68010", + "family": "BKPT", + "executedCases": 32, + "documentationSection": "breakpoint-fallback-qualification-and-shared-audit-runner-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68010", + "family": "MOVES.L", + "diagnosticProfile": true, + "rawStatus": "mismatching", + "rawExecutedCases": 783, + "firstWords": [ + "0E9D", + "DA5E", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "4099fb46e796a43a80a036a9266a560bdbd59af57ce1854f6efb6351e83f82bb", + "firstFailureClassification": "reserved-or-undefined-input", + "reason": "Observed extension has nonzero reserved fields; some stored same-An values are also undefined. Legal inputs have a separately qualified counterpart.", + "qualifiedCounterpart": { + "report": "winuae-moves-audit.json", + "model": "68010", + "family": "MOVES.L", + "executedCases": 4810, + "documentationSection": "moves-legal-input-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68010", + "family": "RTE", + "diagnosticProfile": true, + "rawStatus": "mismatching", + "rawExecutedCases": 12, + "firstWords": [ + "4E73", + "4AFC", + "4E71", + "00FA", + "0400", + "00C7" + ], + "rawDetailSha256": "9824f38311ee678ca512ecf3e677b589a04986ea1500655666ac198c92ce0c9b", + "firstFailureClassification": "unresolved-architecture", + "reason": "Invalid format/version RTE changes N/Z/V in the reference; manual evidence has not settled those flags. No hardware available.", + "qualifiedCounterpart": null, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68EC020", + "family": "BKPT", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "4848", + "4E71", + "4AFC", + "00FA", + "0400", + "00C7" + ], + "rawDetailSha256": "e022201a2bc6ef453bb570e31d016cad1ec9974f028ed30a40b1102a33972867", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Fallback illegal exception must save opcode PC; the reference advances its internal PC before fallback.", + "qualifiedCounterpart": { + "report": "winuae-breakpoint-audit.json", + "model": "68EC020", + "family": "BKPT", + "executedCases": 32, + "documentationSection": "breakpoint-fallback-qualification-and-shared-audit-runner-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68EC020", + "family": "CHK2.B", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "00D0", + "0800", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "429198949debe4ff5524f50f2f9cd4e10e06538d5fb357ef7ff318ac313d8134", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "68EC020", + "family": "CHK2.B", + "executedCases": 1074, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68EC020", + "family": "CHK2.L", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "04D0", + "0800", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "64f5ff0673fbe41ccb719704bba12dc8eeda500c936a68c4b36ae6fc95e1958f", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "68EC020", + "family": "CHK2.L", + "executedCases": 1130, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68EC020", + "family": "CHK2.W", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 3, + "firstWords": [ + "02D0", + "A800", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "a638b4fa8c023eb69f00cfaa7b8088ad00129632b96f1f116f343ece1d0ff5a5", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "68EC020", + "family": "CHK2.W", + "executedCases": 1048, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68EC020", + "family": "DIVL.L", + "diagnosticProfile": false, + "rawStatus": "unsupported", + "rawExecutedCases": 0, + "firstWords": [ + "4C40", + "0031", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "d9f4c8b551d185d3c2d6550f6913ceef8c11381944eba44612cb1de7fd328ce9", + "firstFailureClassification": "reserved-or-undefined-input", + "reason": "Observed extension has nonzero reserved long-arithmetic fields; this is not a legal form failing qualified execution.", + "qualifiedCounterpart": { + "report": "winuae-long-arithmetic-audit.json", + "model": "68EC020", + "family": "DIVL.L", + "executedCases": 2422, + "documentationSection": "long-arithmetic-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68EC020", + "family": "DIVU.W", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1480, + "firstWords": [ + "84C0", + "4AFC", + "4E71", + "4E71", + "4E71", + "4E71" + ], + "rawDetailSha256": "c32eecf185cf86cb03f5a694278763ae06dee3c8055f07e5738fda99341261ec", + "firstFailureClassification": "qualified-reference-correction", + "reason": "The reference retains carry in a defined divide-overflow outcome; the qualified source clears it.", + "qualifiedCounterpart": { + "report": "winuae-word-division-audit.json", + "model": "68EC020", + "family": "DIVU.W", + "executedCases": 9670, + "documentationSection": "word-division-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68EC020", + "family": "MULL.L", + "diagnosticProfile": false, + "rawStatus": "unsupported", + "rawExecutedCases": 0, + "firstWords": [ + "4C00", + "0084", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "dfe09db6939bab1cb31cdc3661baa67f65814e0a6dff3861feea28d4440eb559", + "firstFailureClassification": "reserved-or-undefined-input", + "reason": "Observed extension has nonzero reserved long-arithmetic fields; this is not a legal form failing qualified execution.", + "qualifiedCounterpart": { + "report": "winuae-long-arithmetic-audit.json", + "model": "68EC020", + "family": "MULL.L", + "executedCases": 2456, + "documentationSection": "long-arithmetic-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68EC020", + "family": "TRAPcc", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "50FA", + "0095", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "17bdbd8c4876674a450c2214ae7fee8525a4d34caf8ee82001b1a3016c3d885e", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "68EC020", + "family": "TRAPcc", + "executedCases": 156160, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68020", + "family": "BKPT", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "4848", + "4E71", + "4AFC", + "00FA", + "0400", + "00C7" + ], + "rawDetailSha256": "17f895252545f4d72e284def60c376361090cf33dda5d6912e8c3265dcc238bf", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Fallback illegal exception must save opcode PC; the reference advances its internal PC before fallback.", + "qualifiedCounterpart": { + "report": "winuae-breakpoint-audit.json", + "model": "68020", + "family": "BKPT", + "executedCases": 32, + "documentationSection": "breakpoint-fallback-qualification-and-shared-audit-runner-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68020", + "family": "CHK2.B", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "00D0", + "0800", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "bae4e7f3ebd2bc265909b314660a03c7a7efb19b894599dc9d4ea35856ef1367", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "68020", + "family": "CHK2.B", + "executedCases": 912, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68020", + "family": "CHK2.L", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "04D0", + "0800", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "abe623c8d6a146ec5515737bf6516865cacbdbfff3c7f28f5526824cc28b76ab", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "68020", + "family": "CHK2.L", + "executedCases": 874, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68020", + "family": "CHK2.W", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 5, + "firstWords": [ + "02D0", + "2800", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "2c509be6a42b92f94458ed412fa4f50aaefc131dfa687c60412f26c085765e3b", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "68020", + "family": "CHK2.W", + "executedCases": 900, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68020", + "family": "DIVL.L", + "diagnosticProfile": false, + "rawStatus": "unsupported", + "rawExecutedCases": 0, + "firstWords": [ + "4C40", + "0031", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "0a2bdbf2caca549f964b9d4baea55bf7b92bc381f434c5252aabc542f96c0bc9", + "firstFailureClassification": "reserved-or-undefined-input", + "reason": "Observed extension has nonzero reserved long-arithmetic fields; this is not a legal form failing qualified execution.", + "qualifiedCounterpart": { + "report": "winuae-long-arithmetic-audit.json", + "model": "68020", + "family": "DIVL.L", + "executedCases": 2040, + "documentationSection": "long-arithmetic-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68020", + "family": "DIVU.W", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1344, + "firstWords": [ + "84C0", + "4AFC", + "4E71", + "4E71", + "4E71", + "4AFC" + ], + "rawDetailSha256": "aad488a5adcd0a3a2a226877572b52b404f538f0baa14e07e47f307118f5a88c", + "firstFailureClassification": "qualified-reference-correction", + "reason": "The reference retains carry in a defined divide-overflow outcome; the qualified source clears it.", + "qualifiedCounterpart": { + "report": "winuae-word-division-audit.json", + "model": "68020", + "family": "DIVU.W", + "executedCases": 8870, + "documentationSection": "word-division-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68020", + "family": "MULL.L", + "diagnosticProfile": false, + "rawStatus": "unsupported", + "rawExecutedCases": 2, + "firstWords": [ + "4C00", + "5B2B", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "0cd6614750402f09bcdb23940fa9a1d5c10f376e24bd5103deb75d4bcedf6763", + "firstFailureClassification": "reserved-or-undefined-input", + "reason": "Observed extension has nonzero reserved long-arithmetic fields; this is not a legal form failing qualified execution.", + "qualifiedCounterpart": { + "report": "winuae-long-arithmetic-audit.json", + "model": "68020", + "family": "MULL.L", + "executedCases": 2046, + "documentationSection": "long-arithmetic-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68020", + "family": "TRAPcc", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "50FA", + "0038", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "009d54b9aad60448c50dca1404fa63254dee092d885c409c828a6fd501aba2c6", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "68020", + "family": "TRAPcc", + "executedCases": 156160, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68030", + "family": "BKPT", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "4848", + "4E71", + "4AFC", + "00FA", + "0400", + "00C7" + ], + "rawDetailSha256": "17f895252545f4d72e284def60c376361090cf33dda5d6912e8c3265dcc238bf", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Fallback illegal exception must save opcode PC; the reference advances its internal PC before fallback.", + "qualifiedCounterpart": { + "report": "winuae-breakpoint-audit.json", + "model": "68030", + "family": "BKPT", + "executedCases": 32, + "documentationSection": "breakpoint-fallback-qualification-and-shared-audit-runner-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68030", + "family": "CHK2.B", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "00D0", + "0800", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "bae4e7f3ebd2bc265909b314660a03c7a7efb19b894599dc9d4ea35856ef1367", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "68030", + "family": "CHK2.B", + "executedCases": 912, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68030", + "family": "CHK2.L", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "04D0", + "0800", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "abe623c8d6a146ec5515737bf6516865cacbdbfff3c7f28f5526824cc28b76ab", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "68030", + "family": "CHK2.L", + "executedCases": 874, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68030", + "family": "CHK2.W", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 5, + "firstWords": [ + "02D0", + "2800", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "2c509be6a42b92f94458ed412fa4f50aaefc131dfa687c60412f26c085765e3b", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "68030", + "family": "CHK2.W", + "executedCases": 900, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68030", + "family": "DIVL.L", + "diagnosticProfile": false, + "rawStatus": "unsupported", + "rawExecutedCases": 0, + "firstWords": [ + "4C40", + "0031", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "1b5e08f127d5d0ef108c2795225e17687673505f9c9fbc9d5ea7620db713045a", + "firstFailureClassification": "reserved-or-undefined-input", + "reason": "Observed extension has nonzero reserved long-arithmetic fields; this is not a legal form failing qualified execution.", + "qualifiedCounterpart": { + "report": "winuae-long-arithmetic-audit.json", + "model": "68030", + "family": "DIVL.L", + "executedCases": 2040, + "documentationSection": "long-arithmetic-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68030", + "family": "DIVU.W", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1344, + "firstWords": [ + "84C0", + "4AFC", + "4E71", + "4E71", + "4E71", + "4AFC" + ], + "rawDetailSha256": "aad488a5adcd0a3a2a226877572b52b404f538f0baa14e07e47f307118f5a88c", + "firstFailureClassification": "qualified-reference-correction", + "reason": "The reference retains carry in a defined divide-overflow outcome; the qualified source clears it.", + "qualifiedCounterpart": { + "report": "winuae-word-division-audit.json", + "model": "68030", + "family": "DIVU.W", + "executedCases": 8870, + "documentationSection": "word-division-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68030", + "family": "MULL.L", + "diagnosticProfile": false, + "rawStatus": "unsupported", + "rawExecutedCases": 2, + "firstWords": [ + "4C00", + "5B2B", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "4008e6b3b722aa284490c0e35bf4c42e3ad3c8a174a88f645059e2672062c0e4", + "firstFailureClassification": "reserved-or-undefined-input", + "reason": "Observed extension has nonzero reserved long-arithmetic fields; this is not a legal form failing qualified execution.", + "qualifiedCounterpart": { + "report": "winuae-long-arithmetic-audit.json", + "model": "68030", + "family": "MULL.L", + "executedCases": 2046, + "documentationSection": "long-arithmetic-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68030", + "family": "TRAPcc", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "50FA", + "0038", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "009d54b9aad60448c50dca1404fa63254dee092d885c409c828a6fd501aba2c6", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "68030", + "family": "TRAPcc", + "executedCases": 156160, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68040", + "family": "BKPT", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "4848", + "4E71", + "4AFC", + "00FA", + "0400", + "00C7" + ], + "rawDetailSha256": "17f895252545f4d72e284def60c376361090cf33dda5d6912e8c3265dcc238bf", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Fallback illegal exception must save opcode PC; the reference advances its internal PC before fallback.", + "qualifiedCounterpart": { + "report": "winuae-breakpoint-audit.json", + "model": "68040", + "family": "BKPT", + "executedCases": 32, + "documentationSection": "breakpoint-fallback-qualification-and-shared-audit-runner-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68040", + "family": "CAS2.W", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 23, + "firstWords": [ + "0CFC", + "8083", + "E043", + "4E71", + "4AFC", + "00C7" + ], + "rawDetailSha256": "3e219ae3fba0a2effd4ce9b599af03eaa800d846428e6aeba50d5d5dc195105a", + "firstFailureClassification": "qualified-reference-correction", + "reason": "The reference mishandles shared compare-register operands; the qualified source preserves both original compare values.", + "qualifiedCounterpart": { + "report": "winuae-cas2-audit.json", + "model": "68040", + "family": "CAS2.W", + "executedCases": 42, + "documentationSection": "cas2-compare-alias-and-unimplemented-frame-reference-qualification--2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68040", + "family": "CHK2.B", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "00D0", + "0800", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "e1510c242961a6117944bb5bed58213e5af97b2835bb7ea35fead26b38ba0b7d", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "68040", + "family": "CHK2.B", + "executedCases": 912, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68040", + "family": "CHK2.L", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "04D0", + "0800", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "4bc02c90b024c1dfe4258f6b0dcf1629227caf65e20fe2cdd010930e767f97a4", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "68040", + "family": "CHK2.L", + "executedCases": 874, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68040", + "family": "CHK2.W", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 5, + "firstWords": [ + "02D0", + "2800", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "2c509be6a42b92f94458ed412fa4f50aaefc131dfa687c60412f26c085765e3b", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "68040", + "family": "CHK2.W", + "executedCases": 900, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68040", + "family": "ILLEGAL", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 29331, + "firstWords": [ + "F400", + "4E71", + "4AFC", + "00FA", + "0400", + "00C7" + ], + "rawDetailSha256": "c6a594c68e2acbb4861f1ca1f4a715706bd5d1065dace267b458f7216a31b752", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Observed F400 is cache scope zero: the documented outcome is illegal instruction, not the reference line-F expectation.", + "qualifiedCounterpart": { + "report": "winuae-cache-encodings-audit.json", + "model": "68040", + "family": "ILLEGAL", + "executedCases": 256, + "documentationSection": "cache-encoding-reference-qualification-and-consolidation--2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68040", + "family": "TRAPcc", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "50FA", + "0095", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "57f567ad2b8cbb714a652ac5b1921f21b559287d54d95ed3cc2bc003f0bbd66d", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "68040", + "family": "TRAPcc", + "executedCases": 156160, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68060", + "family": "BKPT", + "diagnosticProfile": true, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "4848", + "4E71", + "4AFC", + "00FA", + "0400", + "00C7" + ], + "rawDetailSha256": "fcdb71a50a1926d4c8c4f7c96ec24e5d33f0bfd645fa91bcd7385ed35ea66759", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Fallback illegal exception must save opcode PC; the reference advances its internal PC before fallback.", + "qualifiedCounterpart": { + "report": "winuae-breakpoint-audit.json", + "model": "68060", + "family": "BKPT", + "executedCases": 32, + "documentationSection": "breakpoint-fallback-qualification-and-shared-audit-runner-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68060", + "family": "CAS.L", + "diagnosticProfile": true, + "rawStatus": "mismatching", + "rawExecutedCases": 273, + "firstWords": [ + "0EE3", + "00C1", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "90471b386709ac245e0f05dcdefd967854d3f0381796956d6b853e314a714a4b", + "firstFailureClassification": "qualified-reference-correction", + "reason": "060 unimplemented misaligned integer entry requires original opcode PC before operand effects.", + "qualifiedCounterpart": { + "report": "winuae-cas-audit.json", + "model": "68060", + "family": "CAS.L", + "executedCases": 2652, + "documentationSection": "cas-legal-input-and-unimplemented-frame-reference-qualification--2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68060", + "family": "CAS.W", + "diagnosticProfile": true, + "rawStatus": "mismatching", + "rawExecutedCases": 273, + "firstWords": [ + "0CE3", + "0043", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "c922cb23b983b72cd59d7c28ea8255c1b93fd876c4d3d1d1b97a9d8326528514", + "firstFailureClassification": "qualified-reference-correction", + "reason": "060 unimplemented misaligned integer entry requires original opcode PC before operand effects.", + "qualifiedCounterpart": { + "report": "winuae-cas-audit.json", + "model": "68060", + "family": "CAS.W", + "executedCases": 2442, + "documentationSection": "cas-legal-input-and-unimplemented-frame-reference-qualification--2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68060", + "family": "DIVL.L", + "diagnosticProfile": true, + "rawStatus": "mismatching", + "rawExecutedCases": 7, + "firstWords": [ + "4C40", + "2406", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "a5f36f5d46b59462252e8e9a04cc5ae0c82aedcf7971d39e59ccbf89612e2b95", + "firstFailureClassification": "qualified-reference-correction", + "reason": "060 unimplemented-integer entry is detected before PC/EA effects; the qualified source separates that outcome from ordinary divide-by-zero.", + "qualifiedCounterpart": { + "report": "winuae-long-arithmetic-audit.json", + "model": "68060", + "family": "DIVL.L", + "executedCases": 3178, + "documentationSection": "long-arithmetic-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68060", + "family": "ILLEGAL", + "diagnosticProfile": true, + "rawStatus": "mismatching", + "rawExecutedCases": 29331, + "firstWords": [ + "F400", + "4E71", + "4AFC", + "00FA", + "0400", + "00C7" + ], + "rawDetailSha256": "3fcf4ac0460ea40687b469b1a515bb86463b45822401afa9b11b2f71e2dad715", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Observed F400 is cache scope zero: the documented outcome is illegal instruction, not the reference line-F expectation.", + "qualifiedCounterpart": { + "report": "winuae-cache-encodings-audit.json", + "model": "68060", + "family": "ILLEGAL", + "executedCases": 256, + "documentationSection": "cache-encoding-reference-qualification-and-consolidation--2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68060", + "family": "LPSTOP", + "diagnosticProfile": true, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "F800", + "01C0", + "0000", + "4E71", + "4AFC", + "00C7" + ], + "rawDetailSha256": "03e01387ca4c514f4593358b3e2702366fd6bfac9e6b51fa18fde41692ad6bf4", + "firstFailureClassification": "qualified-reference-correction", + "reason": "The reference fixed-word helper advances its PC; the qualified source uses nonadvancing fetches and the documented opcode-PC privilege frame.", + "qualifiedCounterpart": { + "report": "winuae-lpstop-audit.json", + "model": "68060", + "family": "LPSTOP", + "executedCases": 245760, + "documentationSection": "lpstop-independent-exception-qualification-and-consolidation-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68060", + "family": "MOVE16", + "diagnosticProfile": true, + "rawStatus": "mismatching", + "rawExecutedCases": 617, + "firstWords": [ + "F626", + "6000", + "4E71", + "4AFC", + "4E71", + "00C7" + ], + "rawDetailSha256": "baa71721b3d5390a170ac120090f36545905e0417ce0c3ef9919359619204920", + "firstFailureClassification": "reserved-or-undefined-input", + "reason": "Observed post-post extension lacks its fixed bit 15; the reference also needs qualified same-register increment and supervisor-stack serialization.", + "qualifiedCounterpart": { + "report": "winuae-move16-audit.json", + "model": "68060", + "family": "MOVE16", + "executedCases": 43856, + "documentationSection": "canonical-move16-reference-qualification--2026-10-06" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68060", + "family": "MOVES.B", + "diagnosticProfile": true, + "rawStatus": "mismatching", + "rawExecutedCases": 1073, + "firstWords": [ + "0E22", + "AF26", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "73350e54aa1b80d460eb0c28373fa866da0c218cf2d9de589f86b2ae1fcf0743", + "firstFailureClassification": "reserved-or-undefined-input", + "reason": "Observed extension has nonzero reserved fields; some stored same-An values are also undefined. Legal inputs have a separately qualified counterpart.", + "qualifiedCounterpart": { + "report": "winuae-moves-audit.json", + "model": "68060", + "family": "MOVES.B", + "executedCases": 5238, + "documentationSection": "moves-legal-input-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68060", + "family": "MOVES.L", + "diagnosticProfile": true, + "rawStatus": "mismatching", + "rawExecutedCases": 483, + "firstWords": [ + "0E98", + "8BA2", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "218e9690228abd411907b6b72f3cea3c1c1a0e07e9eb0f2e4e580c654706e5a1", + "firstFailureClassification": "reserved-or-undefined-input", + "reason": "Observed extension has nonzero reserved fields; some stored same-An values are also undefined. Legal inputs have a separately qualified counterpart.", + "qualifiedCounterpart": { + "report": "winuae-moves-audit.json", + "model": "68060", + "family": "MOVES.L", + "executedCases": 5334, + "documentationSection": "moves-legal-input-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68060", + "family": "MULL.L", + "diagnosticProfile": true, + "rawStatus": "mismatching", + "rawExecutedCases": 5, + "firstWords": [ + "4C00", + "5C07", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "0e301d11a5ccb7cb396dfd9c3946abb48166656419d5927961e2b369e710d599", + "firstFailureClassification": "qualified-reference-correction", + "reason": "060 unimplemented-integer entry is detected before PC/EA effects; the qualified source separates that outcome from ordinary divide-by-zero.", + "qualifiedCounterpart": { + "report": "winuae-long-arithmetic-audit.json", + "model": "68060", + "family": "MULL.L", + "executedCases": 3226, + "documentationSection": "long-arithmetic-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68060", + "family": "STOP", + "diagnosticProfile": true, + "rawStatus": "mismatching", + "rawExecutedCases": 3, + "firstWords": [ + "4E72", + "0000", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "13a35d482d96532a469d9e8bfbadfa811dbb2e7a4d3ed9c58c115c3e6898ca07", + "firstFailureClassification": "unresolved-architecture", + "reason": "Ordinary STOP with S clear in the immediate word disagrees on returned SR and privilege outcome; no hardware evidence is available.", + "qualifiedCounterpart": null, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "68060", + "family": "TRAPcc", + "diagnosticProfile": true, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "50FA", + "0095", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "18e7014b6b4a23eb2fe8fc332067c57778147ad3987e7b7f6bc096ced6a41959", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "68060", + "family": "TRAPcc", + "executedCases": 156160, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "A1200", + "family": "BKPT", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "4848", + "4E71", + "4AFC", + "00FA", + "0400", + "00C7" + ], + "rawDetailSha256": "e022201a2bc6ef453bb570e31d016cad1ec9974f028ed30a40b1102a33972867", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Fallback illegal exception must save opcode PC; the reference advances its internal PC before fallback.", + "qualifiedCounterpart": { + "report": "winuae-breakpoint-audit.json", + "model": "A1200", + "family": "BKPT", + "executedCases": 32, + "documentationSection": "breakpoint-fallback-qualification-and-shared-audit-runner-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "A1200", + "family": "CHK2.B", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "00D0", + "0800", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "429198949debe4ff5524f50f2f9cd4e10e06538d5fb357ef7ff318ac313d8134", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "A1200", + "family": "CHK2.B", + "executedCases": 1074, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "A1200", + "family": "CHK2.L", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "04D0", + "0800", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "64f5ff0673fbe41ccb719704bba12dc8eeda500c936a68c4b36ae6fc95e1958f", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "A1200", + "family": "CHK2.L", + "executedCases": 1130, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "A1200", + "family": "CHK2.W", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 3, + "firstWords": [ + "02D0", + "A800", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "a638b4fa8c023eb69f00cfaa7b8088ad00129632b96f1f116f343ece1d0ff5a5", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "A1200", + "family": "CHK2.W", + "executedCases": 1048, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "A1200", + "family": "DIVL.L", + "diagnosticProfile": false, + "rawStatus": "unsupported", + "rawExecutedCases": 0, + "firstWords": [ + "4C40", + "0031", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "e222262e2a84424385a6678649000dc0fa06667834cfe5b105ed676323056eb2", + "firstFailureClassification": "reserved-or-undefined-input", + "reason": "Observed extension has nonzero reserved long-arithmetic fields; this is not a legal form failing qualified execution.", + "qualifiedCounterpart": { + "report": "winuae-long-arithmetic-audit.json", + "model": "A1200", + "family": "DIVL.L", + "executedCases": 2422, + "documentationSection": "long-arithmetic-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "A1200", + "family": "DIVU.W", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1480, + "firstWords": [ + "84C0", + "4AFC", + "4E71", + "4E71", + "4E71", + "4E71" + ], + "rawDetailSha256": "c32eecf185cf86cb03f5a694278763ae06dee3c8055f07e5738fda99341261ec", + "firstFailureClassification": "qualified-reference-correction", + "reason": "The reference retains carry in a defined divide-overflow outcome; the qualified source clears it.", + "qualifiedCounterpart": { + "report": "winuae-word-division-audit.json", + "model": "A1200", + "family": "DIVU.W", + "executedCases": 9670, + "documentationSection": "word-division-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "A1200", + "family": "MULL.L", + "diagnosticProfile": false, + "rawStatus": "unsupported", + "rawExecutedCases": 0, + "firstWords": [ + "4C00", + "0084", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "2f52c4f268edef8d7961345392d034c51efc8a76909acdb27d607e01c1ee097b", + "firstFailureClassification": "reserved-or-undefined-input", + "reason": "Observed extension has nonzero reserved long-arithmetic fields; this is not a legal form failing qualified execution.", + "qualifiedCounterpart": { + "report": "winuae-long-arithmetic-audit.json", + "model": "A1200", + "family": "MULL.L", + "executedCases": 2456, + "documentationSection": "long-arithmetic-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + }, + { + "model": "A1200", + "family": "TRAPcc", + "diagnosticProfile": false, + "rawStatus": "mismatching", + "rawExecutedCases": 1, + "firstWords": [ + "50FA", + "0095", + "4E71", + "4AFC", + "0400", + "00C7" + ], + "rawDetailSha256": "17bdbd8c4876674a450c2214ae7fee8525a4d34caf8ee82001b1a3016c3d885e", + "firstFailureClassification": "qualified-reference-correction", + "reason": "Saved-PC synchronization differs; the qualified source computes the documented trap/bounds continuation PC.", + "qualifiedCounterpart": { + "report": "winuae-trap-bounds-audit.json", + "model": "A1200", + "family": "TRAPcc", + "executedCases": 156160, + "documentationSection": "trapcc-and-chk2-saved-pc-reference-qualification-2026-10-05" + }, + "rawDirectoryResolved": false, + "laterRawFailuresUnobserved": true + } + ], + "rawGatePassed": false, + "architecturalPromotion": false, + "roadmapComplete": false +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index ae18b844..a15817e4 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -270,12 +270,14 @@ bus ordering, detailed fault sequencing, JIT and native ROM/media tests remain. - SingleStepTests has the pinned 312,500-case 68000 instruction-body audit. The later checkpoints below add a pinned multi-model WinUAE bridge, the unchanged failing Basic discovery audit, and separately qualified trace, - trap/bounds, breakpoint, legal long-arithmetic and word-division presets. Remaining Basic disagreements and - unsupported integer execution still require per-case qualification; passing - focused presets do not replace the failing broad audit. -- Generated 010 word-MOVE/MOVEA format-8 images now have the scoped continuation - gate below. Long transfers, other instruction families, foreign silicon images, - external bus faults, 020/030 formats 9/A/B and the remaining 040 format-7 + trap/bounds, breakpoint, legal long-arithmetic and word-division presets. The + [first-failure ledger](#basic-first-failure-qualification-ledger--2026-10-08) + maps all 54 current non-passing rows to their qualified counterparts or explicit + unresolved questions. It does not qualify later unseen failures or replace the + failing broad audit; Basic 010 RTE flags and 060 STOP remain unresolved. +- Selected generated 010 word/long MOVE/MOVEA format-8 images now have scoped + continuation gates below. Other instruction families, foreign silicon images, + external bus faults, general 020/030 formats 9/A/B and the remaining 040 format-7 CP context transfer and detailed fault protocols remain unqualified. The advanced decoder does not implement all these legal restoration protocols; this is an implementation gap, not invalid encoding. The [040 access-frame discovery gate](#040-access-frame-restoration-discovery-2026-10-05) @@ -11130,3 +11132,72 @@ Production default-selection `move-write-frame-fault-production-default-v1/verif `a721401813cfa32616267476dabacd2105a495971574a9a4a6421f2ad88819ff`. Complete linkage `move-write-frame-fault-complete-link-v1.json` SHA-256: `214e91c17a1cc6911f697939c6bb8a935be54ca32df52374b9406b910f7cbece`. + +## Basic first-failure qualification ledger — 2026-10-08 + +A fresh isolated production-source replay of the unchanged restored Basic +corpus confirms **1,327 passing directories / 46 mismatching / eight unsupported / +zero untested**, across all eight profiles and 1,381 directories. It executes +11,478,371 callbacks and 1,668,069 frame checks, including partial failing +directories; these are not all-passing architectural cases. The single xUnit +audit correctly fails. Native/input/source/assembly identities and all results +are preserved in `audits/BasicCurrentLedgerV1`. + +[The machine-readable ledger](COPPER68K_BASIC_REFERENCE_QUALIFICATION_LEDGER.json) +accounts for every observed first failure, with exact model/family, raw status, +callback count, instruction words and diagnostic hash. It identifies **40 +reference-correction observations**, **12 reserved/undefined-input observations**, +and **two unresolved architectural observations**. The 52 observations with +counterparts bind the appropriate model/family row in one of ten already +qualified presets, plus its report hash, callback count and documented source +qualification. The two unresolved observations are 010 invalid-format/version +RTE N/Z/V behavior and 060 ordinary STOP with S clear in the immediate word. +Hardware is unavailable; neither is assigned a passing counterpart. + +This is an observed-first-failure review, not resolution of 52 Basic directories. +Every ledger row retains `rawDirectoryResolved=false` and +`laterRawFailuresUnobserved=true`. A passing canonical family sample cannot prove +that the remainder of a raw failing directory is correct. The original corpus +is neither patched nor filtered, and its eight reserved-extension unsupported +rows are not disguised as successful legal integer execution. No overall Basic +gate or architectural qualification status is promoted. + +The maintained validator binds the exact failed execution/command/model roster, +source/input inventories, assemblies and report hashes, and matches every raw +first failure to exactly one ledger row. Reserved-field observations use fixed +encoding masks, without a production decoder. Counterparts require the same +CPU/adapter source snapshot, a pinned full-integration proof, passing model/family +rows and their comparator controls. Diagnostic 010/060 status remains separate +from desktop readiness. Missing, duplicate, foreign or reclassified rows, +unrelated counterparts and claims about unseen coverage fail acceptance. + +```powershell +python scripts/test-copper68k-basic-qualification-ledger.py ` + --basic-audit-directory ` + --qualified-report-directory ` + --output +``` + +The ledger describes this pinned checkpoint; future replays require a fresh +review of changed identities and first failures. Ten copied-ledger controls reject +missing/foreign rows, wrong classification, absent/unrelated counterparts, +changed words or report pin, a claimed raw pass, a claimed unseen result and +missing fixtures. A final positive recheck leaves raw and qualified evidence +unchanged. No CPU source change, package publication or regression retirement +occurs. Broader combined-reference execution, later raw failure discovery, +advanced exception protocols and outstanding architectural qualifications remain +required. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +Raw report SHA-256: +`4a08c9eac033fcf8320f9da5b8ff8c430ebb6695bcf60e86cb3705a73f507fa7`. +Original restored manifest/native hashes remain +`b5741ca90fa75d3510ef25bac33d2d36d6a92ab2283f0666e762a5f1853a38ed` / +`135d8c7d01137bbef2467e34207444984fd47a9076041a0c251848d2518b32ce`. + +Ledger SHA-256: +`b21d8dd1ff4234a4d33bdce6d0bfb250125c8ced68df406fa1c17d9ecf9d9f15`. +Successful validator / ten-control records: +`a052e793addd064da6da5c51386b1f9c560e8b699070d63f2cf7b4f17f65a19e` / +`3f330ac0e6fe4201916757defa4b5cdd7f5c075458f6860bff8f1ac1bde631e7`. +Complete evidence linkage: +`a8223397d35c31d589b91817e8f81b136a9c2b05661097b68c45078f1a998fd9`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 4f92dfc9..983c89b3 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3523,3 +3523,22 @@ test retirement occurs. Supplied input, validation refaults/entry failures, returned-bank pending-write refaults and broader architectural requirements remain open. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [frame-fault scope and evidence](COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-frame-validation-and-load-faults--2026-10-08). + +### Milestone 6 broad-reference first-failure review — 2026-10-08 + +Fresh unfiltered Basic execution confirms 1,327 passing, 46 mismatching and eight +unsupported directories, zero untested, across 1,381 model/family rows. The audit +fails as required. A maintained [qualification ledger](COPPER68K_BASIC_REFERENCE_QUALIFICATION_LEDGER.json) +maps all 54 observed first failures: 40 reference-correction observations, +12 reserved/undefined input observations and two unresolved architectural questions +(010 RTE N/Z/V and 060 STOP S-clear). Qualified counterpart rows have exact +model/family, report identity, source and comparator-control bindings. + +No raw directory is relabelled passing, and all later failures remain explicitly +unobserved. The validator rejects missing/unrelated mappings and fabricated +resolution; ten copied-ledger controls fail precisely. This completes the mapping +of the currently observed failures, not the entire independent-reference goal. +Combined reference execution, later-failure discovery, outstanding fault protocols +and architectural questions remain required. No CPU change, publication or +regression retirement occurs. Milestone 6 remains **in progress**, +`roadmapComplete=false`. See the [review scope and command](COPPER68K_REFERENCE_QUALIFICATION.md#basic-first-failure-qualification-ledger--2026-10-08). diff --git a/scripts/test-copper68k-basic-qualification-ledger.py b/scripts/test-copper68k-basic-qualification-ledger.py new file mode 100644 index 00000000..42b9e109 --- /dev/null +++ b/scripts/test-copper68k-basic-qualification-ledger.py @@ -0,0 +1,94 @@ +"""Validate the pinned first-failure review without promoting a failed Basic audit. + +This checks an audited checkpoint, not arbitrary future executions. Qualified +counterparts are family evidence; later raw failures remain unobserved. +""" +import argparse, collections, hashlib, json, re +from pathlib import Path +import xml.etree.ElementTree as ET + +REPO = Path(__file__).resolve().parents[1] +LEDGER = REPO/'docs/COPPER68K_BASIC_REFERENCE_QUALIFICATION_LEDGER.json' +FULL_PROOF = 'a17fb76bcca015c9cebfc86072bc7c6ff977a69070295f79bc765589bdd43c4b' +MODELS = ['68000', '68010', '68EC020', '68020', '68030', '68040', '68060', 'A1200'] +PINS = ('025b999239800357e95065fe5b9a15ea5b300fa7', '7a83745d6c6159bc74ab0471578ffc8bc244e66e') + + +def sha(path): return hashlib.sha256(path.read_bytes()).hexdigest() +def load(path): return json.loads(path.read_text(encoding='utf-8-sig')) +def require(ok, reason): + if not ok: raise ValueError(reason) + + +def classification(model, family, words, status): + if (model, family) in [('68010', 'RTE'), ('68060', 'STOP')]: + return 'unresolved-architecture', None + if family in ['DIVL.L', 'MULL.L'] and status == 'unsupported': + require(words[1] & 0x83f8, 'Missing reserved long-arithmetic field witness') + return 'reserved-or-undefined-input', 'long-arithmetic' + if family.startswith('MOVES'): + require(words[1] & 0x7ff, 'Missing reserved MOVES field witness') + return 'reserved-or-undefined-input', 'moves' + if family == 'MOVE16': + require(words[:2] == [0xf626, 0x6000], 'Changed malformed MOVE16 witness') + return 'reserved-or-undefined-input', 'move16' + if family == 'BKPT': preset = 'breakpoint' + elif family.startswith('CHK2') or family == 'TRAPcc': preset = 'trap-bounds' + elif family == 'DIVU.W': preset = 'word-division' + elif family in ['DIVL.L', 'MULL.L']: preset = 'long-arithmetic' + elif family.startswith('CAS2'): preset = 'cas2' + elif family.startswith('CAS'): preset = 'cas' + elif family == 'LPSTOP': preset = 'lpstop' + elif family == 'ILLEGAL': + require(words[0] == 0xf400, 'Changed cache-scope-zero witness'); preset = 'cache-encodings' + else: raise ValueError('Unreviewed Basic first failure: '+model+'/'+family) + return 'qualified-reference-correction', preset + + +def validate(raw, qualified, ledger): + l = load(ledger) + require(l['schema'] == 1 and l['rawGatePassed'] is False and l['architecturalPromotion'] is False and l['roadmapComplete'] is False, 'Ledger promotes unresolved or failing coverage') + require((l['generatorPin'], l['runnerPin']) == PINS and l['fullIntegrationProofSha256'] == FULL_PROOF == sha(qualified.parent/'proof.json'), 'Wrong pinned reference qualification') + require(l['rawReportSha256'] == sha(raw/'winuae-model-audit.json') and l['rawInputRecordSha256'] == sha(raw/'inputs.json') and l['rawExecutionRecordSha256'] == sha(raw/'execution.json'), 'Changed raw Basic evidence') + i = load(raw/'inputs.json'); e = load(raw/'execution.json'); d = load(raw/'winuae-model-audit.json') + outputs = {'execution.log', 'audit.trx', 'winuae-model-audit.json', 'build/bin/Copper68k/release/Copper68k.dll', 'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'} + command = ['dotnet', 'test', str(raw/'source/Copper68k.Tests/Copper68k.Tests.csproj'), '-c', 'Release', '--artifacts-path', str(raw/'build'), '--filter', 'FullyQualifiedName~WinUaeIntegerFixturesAcrossSelectedModelsWhenEnabled', '--logger', 'trx;LogFileName=audit.trx', '--results-directory', str(raw)] + require(i['command'] == command and e['exit'] == 1 and e['inputsSha256'] == sha(raw/'inputs.json') and set(e['outputs']) == outputs and all(sha(raw/n) == h for n, h in e['outputs'].items()), 'Wrong raw Basic execution or outputs') + expected_settings = {'COPPER68K_RUN_WINUAE_MODEL_AUDIT': '1', 'COPPER68K_WINUAE_MODEL_PATH': i['settings']['COPPER68K_WINUAE_MODEL_PATH'], 'COPPER68K_WINUAE_CPUTEST_LIBRARY': i['settings']['COPPER68K_WINUAE_CPUTEST_LIBRARY'], 'COPPER68K_SYNTHETIC_MODELS': ','.join(MODELS), 'COPPER68K_SYNTHETIC_REPORT_DIR': str(raw)} + require(i['settings'] == expected_settings, 'Changed complete Basic model selection') + reference = Path(expected_settings['COPPER68K_WINUAE_MODEL_PATH']); native = Path(expected_settings['COPPER68K_WINUAE_CPUTEST_LIBRARY']) + require(sha(reference/'manifest.json') == i['manifestSha256'] == l['rawManifestSha256'] == d['manifestSha256'] and sha(native) == i['nativeSha256'] == l['rawNativeSha256'] == d['NativeLibrarySha256'], 'Changed raw reference inputs') + require(i['inputFiles'] and set(i['inputFiles']) == {p.relative_to(reference).as_posix() for p in reference.rglob('*.dat')} and all(sha(reference/n) == h for n, h in i['inputFiles'].items()), 'Missing or changed Basic input selection') + def source_names(root): return {p.relative_to(root).as_posix() for p in root.rglob('*') if p.suffix in ['.cs', '.csproj'] and not any(x in ['obj', 'bin'] for x in p.relative_to(root).parts)} + require(len(i['sources']) == 210 and set(i['sources']) == source_names(raw/'source') == source_names(qualified/'source') and all(sha(raw/'source'/n) == h == sha(qualified/'source'/n) for n, h in i['sources'].items()), 'Changed common CPU/adapter source identity') + require(d['schema'] == 2 and d['selectedModels'] == MODELS and (d['GeneratorCommit'], d['RunnerCommit']) == PINS and d['cpuAssemblySha256'] == e['outputs']['build/bin/Copper68k/release/Copper68k.dll'] and d['adapterAssemblySha256'] == e['outputs']['build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'], 'Wrong raw model or assembly bindings') + require({k: d[k] for k in l['rawCounts']} == l['rawCounts'] and (d['passing'], d['mismatching'], d['unsupported'], d['untested'], d['executedCases'], len(d['rows'])) == (1327, 46, 8, 0, 11478371, 1381), 'Wrong complete raw coverage counts') + ns = {'t': 'http://microsoft.com/schemas/VisualStudio/TeamTest/2010'}; tree = ET.parse(raw/'audit.trx'); rows = tree.findall('.//t:UnitTestResult', ns); counters = tree.find('.//t:Counters', ns) + require(len(rows) == 1 and rows[0].get('testName') == 'Copper68k.Tests.M68kWinUaeCpuTesterConformanceTests.WinUaeIntegerFixturesAcrossSelectedModelsWhenEnabled' and rows[0].get('outcome') == 'Failed' and int(counters.get('total')) == int(counters.get('executed')) == int(counters.get('failed')) == 1 and int(counters.get('passed')) == 0, 'Raw Basic failure was lost or relabelled') + failures = {(x['Model'], x['Opcode']): x for x in d['rows'] if x['Status'] != 'passing'} + require(len(l['entries']) == len(failures) == 54 and len({(x['model'], x['family']) for x in l['entries']}) == 54 and {(x['model'], x['family']) for x in l['entries']} == set(failures), 'Missing, duplicate or foreign first-failure mapping') + reports = {n: load(qualified/n) for n in l['qualifiedReports']} + require(len(reports) == 10 and all(sha(qualified/n) == h for n, h in l['qualifiedReports'].items()), 'Changed qualified counterpart reports') + for report in reports.values(): + require((report['GeneratorCommit'], report['RunnerCommit']) == PINS and report['cpuAssemblySha256'] == sha(qualified/'build/bin/Copper68k/release/Copper68k.dll') and report['adapterAssemblySha256'] == sha(qualified/'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll') and report['mismatching'] == report['unsupported'] == report['untested'] == 0, 'Unqualified counterpart identity or outcome') + documentation = (REPO/'docs/COPPER68K_REFERENCE_QUALIFICATION.md').read_text(encoding='utf-8') + anchors = {re.sub(r'[^\w\- ]', '', line.lstrip('# ').lower()).replace(' ', '-') for line in documentation.splitlines() if line.startswith('#')} + for row in l['entries']: + original = failures[(row['model'], row['family'])]; words = [int(x, 16) for x in re.search(r'words=(.*?), PC=', original['Detail']).group(1).split(',')] + require(row['rawStatus'] == original['Status'] and row['rawExecutedCases'] == original['ExecutedCases'] and row['firstWords'] == [f'{w:04X}' for w in words] and row['rawDetailSha256'] == hashlib.sha256(original['Detail'].encode()).hexdigest(), 'Changed observed Basic first-failure witness') + category, preset = classification(row['model'], row['family'], words, original['Status']) + require(row['firstFailureClassification'] == category and row['rawDirectoryResolved'] is False and row['laterRawFailuresUnobserved'] is True and row['diagnosticProfile'] == (row['model'] in ['68010', '68060']) and row['reason'], 'Wrong first-failure classification or promotion') + q = row['qualifiedCounterpart'] + if preset is None: require(q is None, 'Unresolved architecture has a passing counterpart claim') + else: + require(q is not None and q['report'] == f'winuae-{preset}-audit.json' and q['model'] == row['model'] and q['family'] == row['family'] and q['documentationSection'] in anchors, 'Missing or unrelated qualified counterpart') + found = [x for x in reports[q['report']]['rows'] if (x['Model'], x['Family']) == (row['model'], row['family'])] + require(len(found) == 1 and found[0]['Status'] == 'passing' and found[0]['Controls'] and found[0]['ExecutedCases'] == q['executedCases'] > 0, 'Counterpart model/family coverage unavailable') + counts = dict(collections.Counter(x['firstFailureClassification'] for x in l['entries'])) + require(counts == l['classifications'] == {'qualified-reference-correction': 40, 'reserved-or-undefined-input': 12, 'unresolved-architecture': 2}, 'Wrong qualification review totals') + return dict(schema=1, ledgerSha256=sha(ledger), producerSha256=sha(Path(__file__)), rawReportSha256=sha(raw/'winuae-model-audit.json'), qualifiedReports=l['qualifiedReports'], firstFailures=54, classifications=counts, ledgerValidationPassed=True, rawGatePassed=False, laterRawFailuresUnobserved=True, architecturalPromotion=False, roadmapComplete=False) + + +if __name__ == '__main__': + p = argparse.ArgumentParser(description=__doc__); p.add_argument('--basic-audit-directory', type=Path, required=True); p.add_argument('--qualified-report-directory', type=Path, required=True); p.add_argument('--ledger', type=Path, default=LEDGER); p.add_argument('--output', type=Path, required=True) + a = p.parse_args(); result = validate(a.basic_audit_directory.resolve(), a.qualified_report_directory.resolve(), a.ledger.resolve()); a.output.mkdir(parents=True, exist_ok=False); (a.output/'verification.json').write_text(json.dumps(result, indent=2)+'\n', encoding='utf-8'); print('First-failure mapping verified; raw Basic gate remains failed:', result['classifications']) From 133f639d0f8bc80ba09fc10a9bae51d4f59e8082 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 06:07:35 +0300 Subject: [PATCH 143/211] Compose broad reference corrections and expose later failures --- Copper68k.Tests/Synthetic/README.md | 10 + docs/COPPER68K_REFERENCE_QUALIFICATION.md | 87 +++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 ++ scripts/prepare-copper68k-winuae.ps1 | 52 +++- .../test-copper68k-qualified-basic-inputs.py | 230 ++++++++++++++++++ 5 files changed, 393 insertions(+), 1 deletion(-) create mode 100644 scripts/test-copper68k-qualified-basic-inputs.py diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 54a82f43..ab3ed97c 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1299,3 +1299,13 @@ it does not run CPU instructions or make the raw Basic audit passing. Every row retains its original status and marks later raw failures unobserved. Missing fixtures, mappings, counterparts and unsupported promotion claims fail. See the [scope, identities and remaining questions](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#basic-first-failure-qualification-ledger--2026-10-08). + +Broad composition: `prepare-copper68k-winuae.ps1 -Preset QualifiedBasic` preserves +all Basic families and composes the reviewed generator corrections in separate +inputs. The [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#composed-broad-reference-execution--2026-10-08) +contains the preparation and frozen-result verification commands. The maintained +`test-copper68k-qualified-basic-inputs.py` checks exact composition and optional +execution bindings; it returns failure for the current four mismatching rows. +Input-only verification executes no CPU instructions. The original raw Basic +ledger and failed audit retain their separate scope. This broad checkpoint does +not qualify hardware timing, resolve unseen failures or complete milestone 6. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index a15817e4..0f3737c4 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -11201,3 +11201,90 @@ Successful validator / ten-control records: `3f330ac0e6fe4201916757defa4b5cdd7f5c075458f6860bff8f1ac1bde631e7`. Complete evidence linkage: `a8223397d35c31d589b91817e8f81b136a9c2b05661097b68c45078f1a998fd9`. + +## Composed broad reference execution — 2026-10-08 + +The first-failure ledger does not establish what lies behind the original 54 +failed Basic directories. A fresh `QualifiedBasic` generator preset now composes +thirteen already reviewed source corrections and legal-input patches in one +broad run. It retains all **1,202 generator directories** across seven hardware +models and all **1,381 execution rows** across the eight profiles (including +A1200). `mode=all`, address widths, rounds, privilege selection and Basic's other +settings remain intact. The cache patch applies its exception-classification +hunk only; its narrow family filter is deliberately absent. MOVE16 uses the +broad one-round inputs here; the separate sixteen-round qualifier retains its +own scope. No trace correction is introduced into a non-trace selection. + +The frozen production CPU and adapter sources are unchanged: the same 210 +source/project inputs as the current raw Basic audit and completed production +integration. The new corpus is generated from copies of pinned sources, rather +than patching the original `.dat` files or observed CPU results. Both original +Basic evidence and the individual family presets remain unchanged. This is a +new corrected corpus, not a claim that 50 original raw directories are resolved. + +Execution in `QualifiedBasicCurrentV1` reports **1,377 passing directories / four +mismatching / zero unsupported / zero untested**, with **12,649,817 callbacks** and +**2,316,298 compared exception frames**. All 32 register/SR/frame/undefined-SR +comparison controls remain present. Callbacks include partial failing directories +and are not a count of independently qualified passing architectural cases. +The audit and its strict verifier both return failure; milestone 6 stays in +progress and `roadmapComplete=false`. + +The four observed first failures are: + +| Profile / family | Observed failure | +| --- | --- | +| 010 RTE | Callback 12: invalid-format/version N/Z/V discrepancy, unchanged | +| 040 ILLEGAL | Callback 29,459: `F520`, reference privilege vector 8 versus CPU line-F vector 11 | +| 060 ILLEGAL | Callback 29,459: `F520`, reference privilege vector 8 versus CPU line-F vector 11 | +| 060 STOP | Callback 3: ordinary immediate S-clear behavior, unchanged | + +The `F520` observations were behind the earlier scope-zero `F400` discrepancy. +Their native `privileged_copro_instruction` helper accepts every ID above zero +for 040/060 cpSAVE/cpRESTORE privilege checks. `F520` has ID 2. +[MC68040UM E.1/E-2](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +requires floating-point coprocessor ID 001; its 8.2.4 and +[MC68060UM 8.2.4](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf) +distinguish unrecognized F-line words from floating-point operations. This +identifies a reference-priority question to qualify across neighboring encodings +and both privilege states, not grounds to alter the CPU from this trace alone. +Later failures in all four stopped directories remain unobserved. Existing +A7/trace/repeated-fault and advanced-frame gaps also remain open. + +`scripts/test-copper68k-qualified-basic-inputs.py` reconstructs both composed +sources from pinned original Git content and exact patch/hunk inventories. It +binds all fixture hashes, complete model/family selections, configuration and +native comparison authority. Exact source hashes are retained; only LF/CRLF +transport differences are allowed when comparing the restored comparator text. +Its optional execution verification binds the exact command/settings, unchanged +210 inputs, full-integration identity, assemblies, log/TRX/report, complete +1,381-row roster and all 32 named controls. It writes the actual failed result +and exits nonzero for mismatches, unsupported or untested rows. Input-only +verification executes no CPU instructions and is not an architectural gate. +The execution verifier is bound to this checkpoint; future CPU changes require +fresh source qualification, not replacement of these frozen records. + +```powershell +./scripts/prepare-copper68k-winuae.ps1 -GeneratorSource ` + -RunnerSource -VcVars64 ` + -Preset QualifiedBasic -OutputDirectory +python scripts/test-copper68k-qualified-basic-inputs.py ` + --input-directory --raw-basic-directory ` + --generator-source --audit-directory ` + --raw-audit-directory --output +``` + +Ten separate copied-input controls reject missing/reordered patches, the cache +family-filter hunk, a removed family/profile, unreviewed composed source, +modified comparator/configuration, empty fixtures and corrupt fixture bytes. +Controls unlink a copy before alteration and verify original evidence after +every experiment. The initial verifier exposed LF/CRLF representation differences; +source comparison was corrected without relaxing semantic or exact inventory +bindings. Failed initial checks and successive proofs remain separate. +No CPU fix, package release, candidate import or regression retirement occurs. + +Evidence SHA-256: manifest `34f41176251d1816586e70cdb384b08cacb69bb316785a34f9f24fb2b8138d34`; failed broad report +`57ce11cb65901ac34d8e6e15e7fdac2ae2f0ffd61e36397d89c6011eb07141e1`; strict verification / ten-control records +`c3037b7cd25288e82ce2a53b8f7205f75d92b81fa0dcf438a41699d09f4f850f` / +`5cd24163a0acac4cc41d6026cc8c0248f88697e617b9cc6c30d38ec414f1fd2a`; complete linkage +`b9a8133b4ed373f91339fdc7a72f5d8b86bb2153ce30a98a518bad9f8f3d86a6`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 983c89b3..396814e8 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3542,3 +3542,18 @@ Combined reference execution, later-failure discovery, outstanding fault protoco and architectural questions remain required. No CPU change, publication or regression retirement occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. See the [review scope and command](COPPER68K_REFERENCE_QUALIFICATION.md#basic-first-failure-qualification-ledger--2026-10-08). + +### Milestone 6 composed broad reference execution — 2026-10-08 + +A separate `QualifiedBasic` corpus composes reviewed reference/input corrections +while preserving all Basic families and all eight execution profiles. Its fresh +run has 1,377 passing / four mismatching / zero unsupported / zero untested +directories. New 040/060 `F520` first failures are exposed behind the earlier +`F400` discrepancy; 010 RTE and 060 STOP remain unresolved. This is a new corpus, +not resolution of the old raw directories, and the broad gate still fails. +Exact composition, source/input/result bindings and ten corruption controls are +maintained alongside [the qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#composed-broad-reference-execution--2026-10-08). +No CPU source changes, candidate import, publication or retirement occur. +Milestone 6 remains **in progress**, `roadmapComplete=false`; neighboring +encoding/priority qualification, later-failure discovery and the existing +architectural/protocol requirements remain outstanding. diff --git a/scripts/prepare-copper68k-winuae.ps1 b/scripts/prepare-copper68k-winuae.ps1 index c4a03d9c..902e6635 100644 --- a/scripts/prepare-copper68k-winuae.ps1 +++ b/scripts/prepare-copper68k-winuae.ps1 @@ -4,7 +4,7 @@ param( [Parameter(Mandatory)] [string] $GeneratorSource, [Parameter(Mandatory)] [string] $RunnerSource, [Parameter(Mandatory)] [string] $VcVars64, - [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop','Moves','Cas','Cas2','CacheEncodings','Move16')] [string] $Preset = 'Basic', + [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop','Moves','Cas','Cas2','CacheEncodings','Move16','QualifiedBasic')] [string] $Preset = 'Basic', [string] $OutputDirectory = 'artifacts/winuae-model-inputs' ) $ErrorActionPreference = 'Stop' @@ -31,6 +31,25 @@ function Patch-Once([string] $Text, [string] $Before, [string] $After) { if (($Text.Split([string[]]@($Before), [StringSplitOptions]::None).Count - 1) -ne 1) { throw "Native patch anchor changed: $Before" } return $Text.Replace($Before, $After) } +# Compose only already reviewed corrections. Each original patch and resulting +# source is recorded; Basic and the individual family presets stay unchanged. +$combinedPatches = @() +function Apply-QualifiedPatch([string] $Text, [string] $Name, [int] $RequiredHunks, [int] $SelectedHunks = 0) { + $path = Join-Path $PSScriptRoot ('winuae/' + $Name) + $patch = [IO.File]::ReadAllText($path).Replace("`r`n", "`n") + $hunks = @($patch -split "(?m)^@@`n" | Select-Object -Skip 1) + if ($hunks.Count -ne $RequiredHunks) { throw "Incorrect combined patch hunk count: $Name" } + $count = if ($SelectedHunks -eq 0) { $RequiredHunks } else { $SelectedHunks } + for ($i = 0; $i -lt $count; $i++) { + $lines = $hunks[$i].TrimEnd("`n").Split("`n") + $before = (($lines | Where-Object { $_.StartsWith('-') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" + $after = (($lines | Where-Object { $_.StartsWith('+') -or $_.StartsWith(' ') }) | ForEach-Object { $_.Substring(1) }) -join "`n" + $Text = Patch-Once $Text $before $after + } + [IO.File]::WriteAllText((Join-Path $output $Name), $patch) + $script:combinedPatches += @{Name=$Name; Sha256=(Get-FileHash -LiteralPath $path).Hash.ToLowerInvariant(); AppliedHunks=$count; AvailableHunks=$RequiredHunks} + return $Text +} try { $settings = & $env:COMSPEC /c "`"$vcvars`" >nul && set" if ($LASTEXITCODE -ne 0) { throw 'MSVC environment initialization failed' } @@ -48,6 +67,18 @@ try { & ./build68k.exe table68k | Set-Content -Encoding utf8 cpudefs.cpp if ($LASTEXITCODE -ne 0) { throw 'Opcode table generation failed' } $cpuGeneratorSource = 'gencpu.cpp' + if ($Preset -eq 'QualifiedBasic') { + $source = [IO.File]::ReadAllText((Join-Path $generator 'gencpu.cpp')).Replace("`r`n", "`n") + foreach ($item in @( + @('trap-bounds-pc.patch',2), @('breakpoint-pc.patch',1), + @('long-arithmetic-unimplemented.patch',3), @('word-division-carry.patch',1), + @('lpstop-fetch-pc.patch',2), @('cas-unimplemented-pc.patch',1), + @('cas2-compare-alias.patch',2))) { + $source = Apply-QualifiedPatch $source $item[0] $item[1] + } + $cpuGeneratorSource = Join-Path $output 'gencpu-qualified-basic.cpp' + [IO.File]::WriteAllText($cpuGeneratorSource, $source) + } if ($Preset -in @('TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop','Cas','Cas2')) { # TrapBounds: following PC (MC68020UM 6.1.4 / MC68040UM 8.2.3). # Breakpoints: opcode PC (illegal exception, UM 6.1.5 / 8.2.4). @@ -74,6 +105,20 @@ try { & ./gencpu_prog.exe . *> (Join-Path $output 'gencpu-run.log') if ($LASTEXITCODE -ne 0) { throw 'CPU source generation failed' } $testerSource = 'cputest.cpp' + if ($Preset -eq 'QualifiedBasic') { + $source = [IO.File]::ReadAllText((Join-Path $generator 'cputest.cpp')).Replace("`r`n", "`n") + # CAS's insertion anchors the original MOVES block: apply it first. + foreach ($item in @(@('cas-encodings.patch',1), @('moves-encodings.patch',1), + @('long-arithmetic-encodings.patch',1), @('cas2-overlap-inputs.patch',1), + @('move16-encodings.patch',5))) { + $source = Apply-QualifiedPatch $source $item[0] $item[1] + } + # Correct scope-00 exception classification only. The second hunk + # is the narrow preset's family filter and MUST NOT enter this run. + $source = Apply-QualifiedPatch $source 'cache-encodings.patch' 2 1 + $testerSource = Join-Path $output 'cputest-qualified-basic.cpp' + [IO.File]::WriteAllText($testerSource, $source) + } if ($Preset -eq 'CacheEncodings') { # Correct only the scope-00 architectural exception; select its # ILLEGAL inputs before execution. Upstream excludes actual cache @@ -172,6 +217,7 @@ void M68KTester_destroy(M68KTesterContext* context) { $profiles = @() $baseIni = [IO.File]::ReadAllText((Join-Path $PSScriptRoot 'winuae/cputestgen.ini')).Replace("`r`n", "`n") + if ($Preset -eq 'QualifiedBasic') { $baseIni = $baseIni.Replace('[test=Basic]', '[test=QualifiedBasic]') } if ($Preset -eq 'TraceTraps') { $baseIni = $baseIni.Replace('[test=Basic]', '[test=TraceTraps]').Replace('mode=all', 'mode=TRAP').Replace('feature_sr_mask=0x0000', 'feature_sr_mask=0xa000') } @@ -251,6 +297,10 @@ void M68KTester_destroy(M68KTesterContext* context) { @{ Schema=1; GeneratorCommit=$generatorPin; RunnerCommit=$runnerPin; Profiles=$profiles Preset=$Preset + QualifiedBasicPatches=$combinedPatches + QualifiedBasicCpuSourceSha256=$(if ($Preset -eq 'QualifiedBasic') {(Get-FileHash -LiteralPath (Join-Path $output 'gencpu-qualified-basic.cpp')).Hash.ToLowerInvariant()} else {$null}) + QualifiedBasicInputSourceSha256=$(if ($Preset -eq 'QualifiedBasic') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-qualified-basic.cpp')).Hash.ToLowerInvariant()} else {$null}) + QualifiedBasicScope=$(if ($Preset -eq 'QualifiedBasic') {'All Basic families and profiles; reviewed corrections and legal-input rules composed before execution. Ordinary STOP and RTE disagreements retained; no trace correction or narrow cache family filter. MOVE16 retains the broad one-round selection, not the separate sixteen-round qualification.'} else {$null}) TracePrioritySourceSha256=$(if ($Preset -eq 'TraceTraps') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-trace.cpp')).Hash.ToLowerInvariant()} else {$null}) TracePriorityPatchSha256=$(if ($Preset -eq 'TraceTraps') {(Get-FileHash -LiteralPath (Join-Path $output 'trace-priority.patch')).Hash.ToLowerInvariant()} else {$null}) TrapBoundsSourceSha256=$(if ($Preset -eq 'TrapBounds') {(Get-FileHash -LiteralPath (Join-Path $output 'gencpu-trap-bounds.cpp')).Hash.ToLowerInvariant()} else {$null}) diff --git a/scripts/test-copper68k-qualified-basic-inputs.py b/scripts/test-copper68k-qualified-basic-inputs.py new file mode 100644 index 00000000..8f197afe --- /dev/null +++ b/scripts/test-copper68k-qualified-basic-inputs.py @@ -0,0 +1,230 @@ +"""Verify the composed broad reference inputs; never promote a failing CPU audit.""" +import argparse +import hashlib +import json +import re +import subprocess +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + +PATCHES = {'trap-bounds-pc.patch': {'sha256': '2ab0b6724978492623faa188cb23b1e9de914924209f4fc04b8acdcb59d07e73', + 'available': 2, + 'applied': 2, + 'copySha256': 'ca94d93ec49447e853769bb93bd4e823fe313854aba59601161b35ef8f7bcca4'}, + 'breakpoint-pc.patch': {'sha256': 'cf891c3a0d0e0461dbb7acaec7d77ee180cf115da40d3eb01c28182f6d8a8fc1', + 'available': 1, + 'applied': 1, + 'copySha256': '27b0fb21a7fadbe0bf92ae42074ceaf665d7c19a83efdc841bc7730befc87a7b'}, + 'long-arithmetic-unimplemented.patch': {'sha256': '77bae53d564a288f8d1facf1393250a3438ba74f5a09648d3c071bb94ee3ed84', + 'available': 3, + 'applied': 3, + 'copySha256': '75a9e8d7ce2f534d2b16910dffe436e9d8396b3f470628bc59de9852898e0733'}, + 'word-division-carry.patch': {'sha256': 'b999782fc243fd9387bd83499b9a7b3754189505eafaad9be4e1056be0f036b2', + 'available': 1, + 'applied': 1, + 'copySha256': '7eb90f09aaad56d164d0eafb9f66ddfc31becde9951a832820d5fdcc7973a83f'}, + 'lpstop-fetch-pc.patch': {'sha256': '0ab8eb3139c1343ff52a1aa7ed0564a0970ca1b023c1bee1b28dcf94c785b900', + 'available': 2, + 'applied': 2, + 'copySha256': '64cb8ff58d5a3a4fb4f217e3327fb1748d73f85521e3c9be98af7fd08f3a47bf'}, + 'cas-unimplemented-pc.patch': {'sha256': '26533e846f04fa6573dbdbf485a471b63befcfcd36ad1a817a62779d7cce9936', + 'available': 1, + 'applied': 1, + 'copySha256': 'a27200bdbac2441b1b63651f02590d2894eadc3257df54097fd8a50a5c0d8646'}, + 'cas2-compare-alias.patch': {'sha256': '772a5da413359e5259af2fcdea735290ca0b82904499ee161bad5225c9a79ce3', + 'available': 2, + 'applied': 2, + 'copySha256': '134b61047dfafb91f38374b5151b940ada98dee32bfec251b6a6855043092888'}, + 'cas-encodings.patch': {'sha256': '62b929e03ddff6b9af55acb950254b67d646178bebf0572c82a546b54a1604ae', + 'available': 1, + 'applied': 1, + 'copySha256': 'cb45000b18f7d4a11dcb0fec7130202c7918ac7fd42c9940effa248b1991fba2'}, + 'moves-encodings.patch': {'sha256': '41460b8323d8ac905e740f5615be1b8b12eeea5f16523aba2ea18a107ef4fec8', + 'available': 1, + 'applied': 1, + 'copySha256': 'e79367079bbbbe4fda3b326cf7c6a5cf15e08f021cc3137b1fc3b3aeee756368'}, + 'long-arithmetic-encodings.patch': {'sha256': 'fab3a88d0e3183f9a57d340c7e50e762dc41d218d390365570a4d47114db66ff', + 'available': 1, + 'applied': 1, + 'copySha256': '44e8dacde5a11e9cbd2e15119362a09b35c2e109fbbdbecbefa92929e89b32ea'}, + 'cas2-overlap-inputs.patch': {'sha256': 'c0bf3115542163e61683640ee3a0a5936209f555039ec71e6496d8a656ff0602', + 'available': 1, + 'applied': 1, + 'copySha256': 'dbf0c78f5b88bf2f2ddc0d656a52bca190ec6b970b9e3c6b82cc48d5eac5fc0d'}, + 'move16-encodings.patch': {'sha256': '8903787103e428543e189cb2f7f46d33636ceeeb6c41424385d59b61588c54be', + 'available': 5, + 'applied': 5, + 'copySha256': 'fe407db40dca22a0258a0a689133fa581588117d22ca631cb2c8e52a15932caa'}, + 'cache-encodings.patch': {'sha256': '279f9596cf5d2cb802cdb4b7d93f6ff92342829cc0da5e5ca841c4d4ffad3934', + 'available': 2, + 'applied': 1, + 'copySha256': 'ef393d99b50198f1c0139de12d890f1f2c3cc0675bee19d22259de698c68e32e'}} +GENERATOR_PIN = '025b999239800357e95065fe5b9a15ea5b300fa7' +RUNNER_PIN = '7a83745d6c6159bc74ab0471578ffc8bc244e66e' +MODELS = ['68000', '68010', '68EC020', '68020', '68030', '68040', '68060'] + + +def require(condition, message): + if not condition: + raise ValueError(message) + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def load(path): + return json.loads(path.read_text(encoding='utf-8-sig')) + + +def apply(text, patch, spec): + hunks = patch.split('\n@@\n')[1:] + require(len(hunks) == spec['available'], 'Changed patch hunk count') + for hunk in hunks[:spec['applied']]: + lines = hunk.rstrip('\n').split('\n') + before = '\n'.join(s[1:] for s in lines if s.startswith(('-', ' '))) + after = '\n'.join(s[1:] for s in lines if s.startswith(('+', ' '))) + require(text.count(before) == 1, 'Changed patch anchor') + text = text.replace(before, after) + return text + + +def validate(root, basic, generator): + m = load(root / 'manifest.json') + b = load(basic / 'manifest.json') + require(sha(basic / 'manifest.json') == 'b5741ca90fa75d3510ef25bac33d2d36d6a92ab2283f0666e762a5f1853a38ed', 'Changed raw Basic identity') + require(m['Preset'] == 'QualifiedBasic' and b['Preset'] == 'Basic', 'Wrong broad preset') + require(m['GeneratorCommit'] == b['GeneratorCommit'] == GENERATOR_PIN and + m['RunnerCommit'] == b['RunnerCommit'] == RUNNER_PIN, 'Changed reference pins') + actual = m['QualifiedBasicPatches'] + require(len(actual) == len(PATCHES) and [p['Name'] for p in actual] == list(PATCHES), 'Missing or reordered patches') + sources = {} + for file in ['gencpu.cpp', 'cputest.cpp']: + sources[file] = subprocess.check_output(['git', '-C', str(generator), 'show', + GENERATOR_PIN + ':gencpu/' + file]).decode('utf-8').replace('\r\n', '\n') + for item, (name, spec) in zip(actual, PATCHES.items()): + path = root / name + require(sha(path) == spec['copySha256'] and item['Sha256'] == spec['sha256'] and item['AppliedHunks'] == spec['applied'] + and item['AvailableHunks'] == spec['available'], 'Changed patch identity or selected hunks') + file = 'gencpu.cpp' if name in list(PATCHES)[:7] else 'cputest.cpp' + sources[file] = apply(sources[file], path.read_text(encoding='utf-8').replace('\r\n', '\n'), spec) + for original, generated, field in [ + ('gencpu.cpp', 'gencpu-qualified-basic.cpp', 'QualifiedBasicCpuSourceSha256'), + ('cputest.cpp', 'cputest-qualified-basic.cpp', 'QualifiedBasicInputSourceSha256')]: + path = root / generated + require(path.read_bytes() == sources[original].encode('utf-8') and sha(path) == m[field], 'Unreviewed source composition') + require(sha(root / 'm68k_cpu_tester.dll') == m['NativeLibrarySha256'], 'Changed native assembly') + for file, field in [('winuae-native.c', 'NativeSourceSha256'), ('integer_validation.h', 'IntegerValidationSha256')]: + require(sha(root / file) == m[field] and sha(basic / file) == b[field], 'Changed comparator source binding') + # Older restored inputs use LF; this Windows checkout uses CRLF. Bind + # both exact inventories and allow only that textual transport change. + require((root / file).read_text(encoding='utf-8') == (basic / file).read_text(encoding='utf-8'), 'Changed comparison authority') + require([p['Id'] for p in m['Profiles']] == MODELS, 'Missing broad profile') + for p in m['Profiles']: + original = next(x for x in b['Profiles'] if x['Id'] == p['Id']) + require(all(p[k] == original[k] for k in ['CpuDirectory', 'CpuLevel', 'AddressBits', 'Opcodes']), 'Filtered or incompatible broad profile') + directory = root / p['Id'] + require(sorted(x.relative_to(directory).as_posix() for x in directory.rglob('*.dat')) == sorted(x['Path'] for x in p['Inputs']), 'Changed input inventory') + require(p['Inputs'] and len({x['Path'] for x in p['Inputs']}) == len(p['Inputs']), 'Empty or duplicate inputs') + for item in p['Inputs']: + path = directory / item['Path'] + require(path.stat().st_size == item['Bytes'] and sha(path) == item['Sha256'], 'Changed generated fixture') + config = (directory / 'cputestgen.ini').read_text(encoding='utf-8').replace('[test=QualifiedBasic]', '[test=Basic]') + original_config = (basic / p['Id'] / 'cputestgen.ini').read_text(encoding='utf-8') + normalize = lambda x: re.sub(r'(?m)^path=.*$', 'path=/', x) + require(normalize(config) == normalize(original_config), 'Changed broad generator selection') + return {'compositionVerified': True, 'profiles': MODELS, 'patches': len(PATCHES), + 'manifestSha256': sha(root / 'manifest.json'), 'rawBasicManifestSha256': sha(basic / 'manifest.json'), + 'directoryCount': sum(len(p['Opcodes']) for p in m['Profiles']), + 'architecturalPromotion': False, 'roadmapComplete': False} + + +def validate_execution(audit, raw_audit, root): + manifest = load(root / 'manifest.json') + inputs = load(audit / 'inputs.json') + execution = load(audit / 'execution.json') + raw_inputs = load(raw_audit / 'inputs.json') + require(sha(raw_audit / 'inputs.json') == '9386f2a388943515f85cc5182cfbf285ae086c54ce6dee96fd13437576502f88', 'Changed raw execution identity') + require(inputs['sources'] == raw_inputs['sources'] and len(inputs['sources']) == 210, + 'Changed production source selection') + require(inputs['sourceFullProofSha256'] == raw_inputs['sourceFullProofSha256'] == + 'a17fb76bcca015c9cebfc86072bc7c6ff977a69070295f79bc765589bdd43c4b', 'Changed integration identity') + actual_sources = {p.relative_to(audit / 'source').as_posix(): sha(p) for p in (audit / 'source').rglob('*') + if p.suffix in ['.cs', '.csproj'] and not any(x in ['bin', 'obj'] for x in p.parts)} + require(actual_sources == inputs['sources'], 'Changed production source bytes') + models = MODELS + ['A1200'] + settings = {'COPPER68K_RUN_WINUAE_MODEL_AUDIT': '1', 'COPPER68K_WINUAE_MODEL_PATH': str(root), + 'COPPER68K_WINUAE_CPUTEST_LIBRARY': str(root / 'm68k_cpu_tester.dll'), + 'COPPER68K_SYNTHETIC_MODELS': ','.join(models), 'COPPER68K_SYNTHETIC_REPORT_DIR': str(audit)} + require(inputs['settings'] == settings, 'Changed broad audit settings') + command = ['dotnet', 'test', str(audit / 'source/Copper68k.Tests/Copper68k.Tests.csproj'), '-c', 'Release', + '--artifacts-path', str(audit / 'build'), '--filter', + 'FullyQualifiedName~WinUaeIntegerFixturesAcrossSelectedModelsWhenEnabled', '--logger', + 'trx;LogFileName=audit.trx', '--results-directory', str(audit)] + require(inputs['command'] == command, 'Changed broad execution command') + require(inputs['manifestSha256'] == sha(root / 'manifest.json') and inputs['nativeSha256'] == manifest['NativeLibrarySha256'], 'Changed audit inputs') + require(inputs['inputFiles'] == {p.relative_to(root).as_posix(): sha(p) for p in root.rglob('*.dat')}, 'Changed executed fixtures') + outputs = ['execution.log', 'audit.trx', 'winuae-model-audit.json', + 'build/bin/Copper68k/release/Copper68k.dll', 'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + require(execution['inputsSha256'] == sha(audit / 'inputs.json') and + execution['outputs'] == {p: sha(audit / p) for p in outputs}, 'Changed execution evidence') + require(all(execution[k] is False for k in ['productionImport', 'publication', 'roadmapComplete']), 'Unexpected promotion') + report = load(audit / 'winuae-model-audit.json') + require(report['schema'] == 2 and report['selectedModels'] == models and + report['GeneratorCommit'] == GENERATOR_PIN and report['RunnerCommit'] == RUNNER_PIN and + report['NativeLibrarySha256'] == manifest['NativeLibrarySha256'] and report['manifestSha256'] == inputs['manifestSha256'], 'Changed report identity') + require(report['cpuAssemblySha256'] == execution['outputs'][outputs[3]] and + report['adapterAssemblySha256'] == execution['outputs'][outputs[4]], 'Changed report assemblies') + roster = [(m, family) for m in models for family in next(p for p in manifest['Profiles'] + if p['Id'] == ('68EC020' if m == 'A1200' else m))['Opcodes']] + rows = report['rows'] + require([(r['Model'], r['Opcode']) for r in rows] == roster and len(rows) == 1381, 'Incomplete broad execution roster') + require([(p['Model'], p['Kind']) for p in report['probes']] == [(m, k) for m in models for k in + ['register', 'defined-sr', 'exception-frame', 'undefined-sr-ignored']] and + all(p['Detected'] and p['ExecutedCases'] > 0 for p in report['probes']), 'Lost comparator controls') + counts = {status: sum(r['Status'] == status for r in rows) for status in ['passing', 'mismatching', 'unsupported', 'untested']} + require(all(report[k] == v for k, v in counts.items()) and sum(counts.values()) == len(rows), 'Changed broad counts') + for key, field in [('executedCases', 'ExecutedCases'), ('exceptionFrames', 'ExceptionFrames'), + ('maskedSrCases', 'MaskedSrCases'), ('terminalCases', 'TerminalCases')]: + require(report[key] == sum(r[field] for r in rows), 'Changed execution totals') + passing = counts['passing'] == len(rows) + require(execution['exit'] == (0 if passing else 1), 'Changed audit exit') + trx = ET.parse(audit / 'audit.trx').getroot() + ns = {'t': 'http://microsoft.com/schemas/VisualStudio/TeamTest/2010'} + results = trx.findall('.//t:UnitTestResult', ns) + require(len(results) == 1 and results[0].get('outcome') == ('Passed' if passing else 'Failed') and + 'WinUaeIntegerFixturesAcrossSelectedModelsWhenEnabled' in results[0].get('testName', ''), 'Changed executed test') + return {'counts': counts, 'callbacks': report['executedCases'], 'exceptionFrames': report['exceptionFrames'], + 'comparatorControls': 32, 'auditGatePassed': passing, 'reportSha256': sha(audit / 'winuae-model-audit.json'), + 'executionSha256': sha(audit / 'execution.json'), 'sourcesUnchanged': True, + 'firstFailures': [{'model': r['Model'], 'family': r['Opcode'], 'callbacks': r['ExecutedCases'], + 'detailSha256': hashlib.sha256(r['Detail'].encode('utf-8')).hexdigest(), + 'detail': r['Detail'], 'laterFailuresUnobserved': True} for r in rows if r['Status'] != 'passing']} + + +def main(): + a = argparse.ArgumentParser(description=__doc__) + a.add_argument('--input-directory', type=Path, required=True) + a.add_argument('--raw-basic-directory', type=Path, required=True) + a.add_argument('--generator-source', type=Path, required=True) + a.add_argument('--output', type=Path, required=True) + a.add_argument('--audit-directory', type=Path) + a.add_argument('--raw-audit-directory', type=Path) + args = a.parse_args() + require(not args.output.exists(), 'Use a fresh verification output') + result = validate(args.input_directory, args.raw_basic_directory, args.generator_source) + if args.audit_directory: + require(args.raw_audit_directory is not None, 'Missing raw source evidence') + result['execution'] = validate_execution(args.audit_directory, args.raw_audit_directory, args.input_directory) + result['validatorSha256'] = sha(Path(__file__)) + args.output.mkdir(parents=True) + (args.output / 'verification.json').write_text(json.dumps(result, indent=2) + '\n', encoding='utf-8') + print('Broad composition verified; architectural promotion remains false') + if args.audit_directory and not result['execution']['auditGatePassed']: + print('Broad CPU audit remains failed:', result['execution']['counts']) + sys.exit(1) + + +if __name__ == '__main__': + main() From d7f330c0c2e2b32e6d477d8338158736811d07c2 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 06:22:55 +0300 Subject: [PATCH 144/211] Qualify coprocessor ID priority in broad native references --- Copper68k.Tests/Synthetic/README.md | 7 ++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 84 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 14 ++++ scripts/prepare-copper68k-winuae.ps1 | 5 ++ .../test-copper68k-qualified-basic-inputs.py | 16 +++- scripts/winuae/coprocessor-id-priority.patch | 13 +++ 6 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 scripts/winuae/coprocessor-id-priority.patch diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index ab3ed97c..4220f88b 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1309,3 +1309,10 @@ execution bindings; it returns failure for the current four mismatching rows. Input-only verification executes no CPU instructions. The original raw Basic ledger and failed audit retain their separate scope. This broad checkpoint does not qualify hardware timing, resolve unseen failures or complete milestone 6. + +`QualifiedBasicRevision=2` adds the reviewed 040/060 coprocessor-ID fallback +priority correction; revision 1 remains supported and its frozen evidence is +retained. The [revision-2 record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040060-coprocessor-id-priority-qualification--2026-10-08) +binds the 445,248-case first-word matrix and twelve corruption controls. The +broad audit remains failed and now exposes `F628`; input validation is not a +passing CPU gate. No Copper68k source change or package release is included. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 0f3737c4..205145ed 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -267,6 +267,11 @@ bus ordering, detailed fault sequencing, JIT and native ROM/media tests remain. ## Remaining qualification and implementation gaps +The [revision-2 broad replay](#040060-coprocessor-id-priority-qualification--2026-10-08) +gets past `F520` and exposes reserved MOVE16 word `F628` on 040/060. The broad +gate remains failed, alongside the retained 010 RTE and 060 STOP disagreements. + + - SingleStepTests has the pinned 312,500-case 68000 instruction-body audit. The later checkpoints below add a pinned multi-model WinUAE bridge, the unchanged failing Basic discovery audit, and separately qualified trace, @@ -11288,3 +11293,82 @@ Evidence SHA-256: manifest `34f41176251d1816586e70cdb384b08cacb69bb316785a34f9f2 `c3037b7cd25288e82ce2a53b8f7205f75d92b81fa0dcf438a41699d09f4f850f` / `5cd24163a0acac4cc41d6026cc8c0248f88697e617b9cc6c30d38ec414f1fd2a`; complete linkage `b9a8133b4ed373f91339fdc7a72f5d8b86bb2153ce30a98a518bad9f8f3d86a6`. + +## 040/060 coprocessor ID priority qualification — 2026-10-08 + +The native fallback applied external cpSAVE/cpRESTORE privilege rules to every +nonzero coprocessor ID on 040/060. `F520` encodes ID 2 and predecrement A0; +recognizing it as privileged external state transfer gave vector 8 in user mode. +[MC68040UM E-2 and 8.2.4](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +requires ID 001 for floating-point instructions and distinguishes unrecognized +F-line words. [MC68060UM 8.2.4](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf) +specifies vector 11, format 0 and causing-instruction PC for unrecognized F-line +words. The recognized ID-1 FPU and integer MMU instruction paths remain separate. + +`coprocessor-id-priority.patch` confines the native fallback privilege check to +ID 1 on 040/060; the 020/030 rule is unchanged. The correction runs before +reference execution, without changing Copper68k, an observed CPU result, a +comparator mask or an input family selection. `QualifiedBasicRevision=2` adds +this fourteenth composed patch. Revision 1 keeps its thirteen-patch definition; +the maintained verifier successfully rechecks its frozen failed execution. +Both original corpora and their evidence remain unchanged. + +The existing public-factory first-word matrix was freshly executed against the +same frozen production source/assemblies. All **445,248 cases in eight batches** +pass. Exact per-model encoding/vector inventories and case multiplicities are +independently re-enumerated: both privilege states, every CCR, all IDs and EA +fields, with integer MMU overlaps explicitly owned by their existing matrices +and legal supervisor FPU protocols outside this first-word exception selection. +ID-2 coverage is **5,120 cases on 040** and **6,144 on 060**; `F520` alone has +64 cases on each. Verification includes registers, saved PC/SR/frame, canaries +and forbidden operand reads. This is documented semantic software qualification, +not hardware evidence or FPU/MMU operational qualification. + +The fresh revision-2 broad run executes every retained family/profile. It reports +**1,377 passing / four mismatching / zero unsupported / zero untested**, +**12,650,585 callbacks** and **2,317,066 exception frames**, with all 32 named +comparison controls intact. On both 040 and 060 the ILLEGAL first failure moves +from callback 29,459 (`F520`) to callback 29,843 (`F628`): 384 additional callbacks +are reached per profile. The passing-directory count does not increase; neither +failing ILLEGAL directory is resolved. The 010 RTE and 060 STOP observations are +unchanged, and later failures in all four directories remain unobserved. + +`F628` is a new reserved MOVE16 first-word discrepancy. The native diagnostic +expects vector 11 and reports no actual exception, while the CPU execution +trace records vector 4. Its architectural classification and adapter termination +handling need a bounded reproduction; this checkpoint makes no correction or +qualification claim for it. Other A7/trace/repeated-fault/advanced-frame gaps +remain open. The broad test and strict execution verifier both return failure. + +A separate data-delta comparison ignores **only** the generator's documented +start-time field (bytes 4..7 of opcode header/data records); memory images are +compared in full. It finds changed payloads only in the 040 and 060 ILLEGAL data +files. All other payloads are unchanged. This diagnostic comparison does not +weaken acceptance: both complete exact input inventories and hashes are still +bound by the strict verifier. + +Twelve copied-input controls reject the original ten corruptions plus an unknown +revision and a widened CpID mask despite an updated composed-source hash. They +unlink copies before modification and confirm originals after every experiment. +The new patch's original Windows CRLF identity and emitted LF-copy identity are +recorded separately; the initial preflight rejected their mistaken conflation +before any CPU execution. The verifier now permits only the pinned original or +exact LF/CRLF textual representations, while retaining exact emitted-source and +fixture bindings. No production CPU change, private candidate import, package +publication or regression retirement occurs. Milestone 6 remains **in progress**. + +Use `prepare-copper68k-winuae.ps1 -Preset QualifiedBasic -QualifiedBasicRevision 2` +for new inputs; `-QualifiedBasicRevision 1` preserves the earlier composition. +The unchanged frozen verification command above accepts both explicit revisions +(and the original revision-1 manifest) and fails unknown revisions or any failed +broad gate. Requested execution audits must not interpret input-only validation +as instruction execution. + +Evidence SHA-256: normalized new patch `e646382b98fb38533f6411fe13b52eecb92e78dfa169b88137b9a54da86e6461`; matrix +`898d4f7c996fe5cc90922918e3d751a4c394a5e2bac7a299934348fb2deb7dfa`; failed broad report +`44e23cf3fda3c7a3ddceb523dae87ed1f653ded9604011c3e026c58998d470f0`; strict proof / twelve controls +`95757f467a366ba775e9fdf18737c9c099c96872f35427d6d096b6f9e26a7a17` / +`5d509b365df6e0703b1b04e52ca7faed059bf8de8c0650a2714eabe0517d8ec6`; retained revision-1 proof +`26e1bd6a201fc6d46df6ce2b38c86bce806e8aaa112ab6a06c0a833a0c808dc0`; input-delta record +`c7500fae50a111c0ec60d141e295d89025083b2934876d77e051ec6154fab889`; complete linkage +`cbd5c1d9f365dfd6aeec7941431c02cd3e55ee52ea2dc0c737ace153ec6bf2fd`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 396814e8..3b2f559e 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3557,3 +3557,17 @@ No CPU source changes, candidate import, publication or retirement occur. Milestone 6 remains **in progress**, `roadmapComplete=false`; neighboring encoding/priority qualification, later-failure discovery and the existing architectural/protocol requirements remain outstanding. + +### Milestone 6 coprocessor-ID reference priority — 2026-10-08 + +The 040/060 `F520` discrepancy is qualified as a native-reference priority error: +only ID 1 identifies FPU state instructions on these models. Revision 2 limits +that reference check without changing Copper68k or excluding families; revision 1 +remains reproducible. The fresh independent first-word matrix passes 445,248 +cases in eight batches. The broad run still fails four directories, now exposing +`F628` reserved MOVE16 words behind `F520`; 010 RTE and 060 STOP remain. Twelve +corruption controls pass, and exact retained revision-1 evidence revalidates. +See [the qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040060-coprocessor-id-priority-qualification--2026-10-08). +Milestone 6 remains **in progress**, `roadmapComplete=false`. The new encoding and +adapter discrepancy, later failures and prior architectural/protocol gaps must +be addressed; no source candidate import or package publication occurs. diff --git a/scripts/prepare-copper68k-winuae.ps1 b/scripts/prepare-copper68k-winuae.ps1 index 902e6635..451de0ee 100644 --- a/scripts/prepare-copper68k-winuae.ps1 +++ b/scripts/prepare-copper68k-winuae.ps1 @@ -5,6 +5,7 @@ param( [Parameter(Mandatory)] [string] $RunnerSource, [Parameter(Mandatory)] [string] $VcVars64, [ValidateSet('Basic','TraceTraps','TrapBounds','Breakpoints','LongArithmetic','WordDivision','LowPowerStop','Moves','Cas','Cas2','CacheEncodings','Move16','QualifiedBasic')] [string] $Preset = 'Basic', + [ValidateSet(1,2)] [int] $QualifiedBasicRevision = 2, [string] $OutputDirectory = 'artifacts/winuae-model-inputs' ) $ErrorActionPreference = 'Stop' @@ -116,6 +117,9 @@ try { # Correct scope-00 exception classification only. The second hunk # is the narrow preset's family filter and MUST NOT enter this run. $source = Apply-QualifiedPatch $source 'cache-encodings.patch' 2 1 + if ($QualifiedBasicRevision -eq 2) { + $source = Apply-QualifiedPatch $source 'coprocessor-id-priority.patch' 1 + } $testerSource = Join-Path $output 'cputest-qualified-basic.cpp' [IO.File]::WriteAllText($testerSource, $source) } @@ -297,6 +301,7 @@ void M68KTester_destroy(M68KTesterContext* context) { @{ Schema=1; GeneratorCommit=$generatorPin; RunnerCommit=$runnerPin; Profiles=$profiles Preset=$Preset + QualifiedBasicRevision=$(if ($Preset -eq 'QualifiedBasic') {$QualifiedBasicRevision} else {$null}) QualifiedBasicPatches=$combinedPatches QualifiedBasicCpuSourceSha256=$(if ($Preset -eq 'QualifiedBasic') {(Get-FileHash -LiteralPath (Join-Path $output 'gencpu-qualified-basic.cpp')).Hash.ToLowerInvariant()} else {$null}) QualifiedBasicInputSourceSha256=$(if ($Preset -eq 'QualifiedBasic') {(Get-FileHash -LiteralPath (Join-Path $output 'cputest-qualified-basic.cpp')).Hash.ToLowerInvariant()} else {$null}) diff --git a/scripts/test-copper68k-qualified-basic-inputs.py b/scripts/test-copper68k-qualified-basic-inputs.py index 8f197afe..f563ea90 100644 --- a/scripts/test-copper68k-qualified-basic-inputs.py +++ b/scripts/test-copper68k-qualified-basic-inputs.py @@ -60,6 +60,8 @@ 'available': 2, 'applied': 1, 'copySha256': 'ef393d99b50198f1c0139de12d890f1f2c3cc0675bee19d22259de698c68e32e'}} +ID_PRIORITY_PATCH = {'sha256': '5400a56d5fc4f73492a26f261208b5b25f6d909add545c07b85902a9685a58a4', 'copySha256': 'e646382b98fb38533f6411fe13b52eecb92e78dfa169b88137b9a54da86e6461', 'available': 1, 'applied': 1} + GENERATOR_PIN = '025b999239800357e95065fe5b9a15ea5b300fa7' RUNNER_PIN = '7a83745d6c6159bc74ab0471578ffc8bc244e66e' MODELS = ['68000', '68010', '68EC020', '68020', '68030', '68040', '68060'] @@ -97,15 +99,21 @@ def validate(root, basic, generator): require(m['Preset'] == 'QualifiedBasic' and b['Preset'] == 'Basic', 'Wrong broad preset') require(m['GeneratorCommit'] == b['GeneratorCommit'] == GENERATOR_PIN and m['RunnerCommit'] == b['RunnerCommit'] == RUNNER_PIN, 'Changed reference pins') + revision = m.get('QualifiedBasicRevision', 1) + require(revision in [1, 2], 'Unknown broad reference revision') + patches = dict(PATCHES) + if revision == 2: + patches['coprocessor-id-priority.patch'] = ID_PRIORITY_PATCH actual = m['QualifiedBasicPatches'] - require(len(actual) == len(PATCHES) and [p['Name'] for p in actual] == list(PATCHES), 'Missing or reordered patches') + require(len(actual) == len(patches) and [p['Name'] for p in actual] == list(patches), 'Missing or reordered patches') sources = {} for file in ['gencpu.cpp', 'cputest.cpp']: sources[file] = subprocess.check_output(['git', '-C', str(generator), 'show', GENERATOR_PIN + ':gencpu/' + file]).decode('utf-8').replace('\r\n', '\n') - for item, (name, spec) in zip(actual, PATCHES.items()): + for item, (name, spec) in zip(actual, patches.items()): path = root / name - require(sha(path) == spec['copySha256'] and item['Sha256'] == spec['sha256'] and item['AppliedHunks'] == spec['applied'] + require(sha(path) == spec['copySha256'] and item['Sha256'] in + {spec['sha256'], spec['copySha256'], hashlib.sha256(path.read_text(encoding='utf-8').replace('\n', '\r\n').encode('utf-8')).hexdigest()} and item['AppliedHunks'] == spec['applied'] and item['AvailableHunks'] == spec['available'], 'Changed patch identity or selected hunks') file = 'gencpu.cpp' if name in list(PATCHES)[:7] else 'cputest.cpp' sources[file] = apply(sources[file], path.read_text(encoding='utf-8').replace('\r\n', '\n'), spec) @@ -134,7 +142,7 @@ def validate(root, basic, generator): original_config = (basic / p['Id'] / 'cputestgen.ini').read_text(encoding='utf-8') normalize = lambda x: re.sub(r'(?m)^path=.*$', 'path=/', x) require(normalize(config) == normalize(original_config), 'Changed broad generator selection') - return {'compositionVerified': True, 'profiles': MODELS, 'patches': len(PATCHES), + return {'compositionVerified': True, 'profiles': MODELS, 'patches': len(patches), 'revision': revision, 'manifestSha256': sha(root / 'manifest.json'), 'rawBasicManifestSha256': sha(basic / 'manifest.json'), 'directoryCount': sum(len(p['Opcodes']) for p in m['Profiles']), 'architecturalPromotion': False, 'roadmapComplete': False} diff --git a/scripts/winuae/coprocessor-id-priority.patch b/scripts/winuae/coprocessor-id-priority.patch new file mode 100644 index 00000000..7bac5141 --- /dev/null +++ b/scripts/winuae/coprocessor-id-priority.patch @@ -0,0 +1,13 @@ +--- cputest.cpp ++++ cputest-qualified-basic.cpp +@@ + test_exception = 11; +- if (privileged_copro_instruction(opcode)) { ++ // MC68040UM E-2 / 8.2.4 and MC68060UM 8.2.4: ++ // only CpID 1 identifies FPU state instructions on 040/060. ++ // Other IDs are unrecognized F-line words, not privileged ++ // external cpSAVE/cpRESTORE. Preserve the 020/030 CIR rules. ++ if ((cpu_lvl < 4 || (opcode & 0x0e00) == 0x0200) && ++ privileged_copro_instruction(opcode)) { + test_exception = 8; + } From 375c6d8c080d60b09099715f7b98e1a43d633b93 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 08:16:22 +0300 Subject: [PATCH 145/211] Fix reserved MOVE16 exceptions and consolidate AND coverage --- Copper68k.Tests/M68020AddressSourceTests.cs | 15 -- Copper68k.Tests/Synthetic/README.md | 19 ++ .../Synthetic/SyntheticAndIndirectTests.cs | 41 +++++ .../Synthetic/SyntheticReservedMove16Tests.cs | 32 ++++ .../M68kAdvancedTimingInterpreter.System.cs | 5 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 164 +++++++++++++++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 39 +++++ scripts/test-copper68k-synthetic.ps1 | 2 + 8 files changed, 298 insertions(+), 19 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticAndIndirectTests.cs create mode 100644 Copper68k.Tests/Synthetic/SyntheticReservedMove16Tests.cs diff --git a/Copper68k.Tests/M68020AddressSourceTests.cs b/Copper68k.Tests/M68020AddressSourceTests.cs index a216452c..170b256b 100644 --- a/Copper68k.Tests/M68020AddressSourceTests.cs +++ b/Copper68k.Tests/M68020AddressSourceTests.cs @@ -116,21 +116,6 @@ public void CmpiPostIncrementPreservesExtendAndUsesByteStackStride(ushort opcode Assert.Equal(0x2000u + stride, cpu.State.A[opcode & 7]); Assert.Equal(CodeBase + 4, cpu.State.ProgramCounter); } - [Theory] - [InlineData(0xC012, 0xFFFF00F0u)] - [InlineData(0xC052, 0xFFFFF0A5u)] - [InlineData(0xC092, 0xF0A55A0Fu)] - public void AndAddressIndirectUsesSelectedWidthAndPreservesExtend(ushort opcode, uint expected) - { - var bus = new ZeroWaitCodeBus(); WriteWords(bus, CodeBase, opcode); WriteWords(bus, 0x2000, 0xF0A5, 0x5A0F); - using var cpu = M68kCoreFactory.Default.Create(M68kCpuModel.M68EC020, bus); - cpu.Reset(CodeBase, 0x3000); cpu.State.A[2] = 0x2000; cpu.State.D[0] = 0xFFFF00FF; - if (opcode != 0xC012) cpu.State.D[0] = uint.MaxValue; - cpu.State.StatusRegister = 0x2017; cpu.ExecuteInstruction(); - Assert.Equal(expected, cpu.State.D[0]); Assert.Equal(0x18, cpu.State.StatusRegister & 31); - Assert.Equal(0x2000u, cpu.State.A[2]); Assert.Equal(CodeBase + 2, cpu.State.ProgramCounter); - } - [Theory] [InlineData(9u, 0x80, 0x1F)] [InlineData(15u, 0x80, 0x1B)] diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 4220f88b..316e8c87 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1316,3 +1316,22 @@ retained. The [revision-2 record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.m binds the 445,248-case first-word matrix and twelve corruption controls. The broad audit remains failed and now exposes `F628`; input validation is not a passing CPU gate. No Copper68k source change or package release is included. + +The subsequent [reserved MOVE16 correction](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#reserved-move16-first-word-qualification--2026-10-08) +requires 6,144 `system-move16-reserved-first-words` cases per profile in ordinary +CI. It independently checks all `F628..F63F` words, four following words, both +privilege states and every CCR. The fixed CPU passes all 49,152 cases; the before +snapshot detects every 040/060 case. The unchanged broad corpus now passes 1,379 +directories, while 010 RTE and 060 STOP remain mismatching. The frozen full CPU +suite, ordinary gate and clean `.74` consumers pass; the separate consolidated +full audit passes: 5,312 tests / 49 unavailable / zero failures. Milestone 6 and the hardware trace gap remain open. + +`SyntheticAndIndirectTests` replaces only the three captured EC020 AND-indirect +semantic rows after [five causal mutations and a sibling-retention run](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#and-indirect-semantic-regression-consolidation--2026-10-08). +It executes 12,288 `logical-and-indirect-captured` cases per profile, using all +D/A registers, CCRs and privilege states. The original captured opcode/value +combinations are preserved. Ordinary CI requires that exact per-profile count. +The accepted preconsolidation full suite has a separate source roster; the +maintained consolidated full audit passes separately with 86,600,962 cases / +937 complete reports / ten native presets; its ordinary gate passes 86,410,018 +cases / 739 batches. diff --git a/Copper68k.Tests/Synthetic/SyntheticAndIndirectTests.cs b/Copper68k.Tests/Synthetic/SyntheticAndIndirectTests.cs new file mode 100644 index 00000000..26e0ae35 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticAndIndirectTests.cs @@ -0,0 +1,41 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticAndIndirectTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void AndIndirectRetainsSelectedWidthRegistersAndFlags(string modelId) + { + // Captured C012/C052/C092 operands from the old EC020 regression, + // extended to every source/address register, CCR and stack state. + // M68000PM AND: sized bitwise conjunction, X unchanged, N/Z from the + // selected result, V/C clear. No production arithmetic/EA helpers. + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "logical-and-indirect-captured"); + foreach (var width in new[] { 1, 2, 4 }) + for (var addressReg = 0; addressReg < 8; addressReg++) + for (var dataReg = 0; dataReg < 8; dataReg++) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + machine.Reset(ccr, supervisor); + var initial = width == 1 ? 0xffff00ffu : uint.MaxValue; + machine.Core.State.D[dataReg] = initial; + var opcode = (ushort)(0xc010 | dataReg << 9 | ArithmeticSpecification.SizeField(width) << 6 | addressReg); + if (dataReg == 0 && addressReg == 2) + Assert.Equal(width == 1 ? 0xc012 : width == 2 ? 0xc052 : 0xc092, opcode); + var value = width == 1 ? 0xf0u : width == 2 ? 0xf0a5u : 0xf0a55a0fu; + var operand = new OperandFixture(machine, [opcode], new(2, addressReg), width, value); + var expected = operand.Expected; + var result = width == 1 ? 0xf0u : width == 2 ? 0xf0a5u : 0xf0a55a0fu; + expected.D[dataReg] = (initial & ~MoveSpecification.Mask(width)) | result; + expected.Sr = (ushort)((expected.Sr & 0xfff0) | 8); // X preserved; N=1, Z/V/C=0. + SyntheticExecution.Run(machine, expected, report, + $"{modelId}/AND/{width}/(A{addressReg})/D{dataReg}/super={supervisor}/ccr={ccr:X2}/op={opcode:X4}"); + } + report.Complete(output); + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticReservedMove16Tests.cs b/Copper68k.Tests/Synthetic/SyntheticReservedMove16Tests.cs new file mode 100644 index 00000000..bde22ba3 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticReservedMove16Tests.cs @@ -0,0 +1,32 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticReservedMove16Tests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void UnassignedMove16FirstWordsTakeLineFBeforeOperandEffects(string modelId) + { + // Five assigned first-word forms occupy F600..F627. F628..F63F + // are unrecognized F-line words, not a valid MOVE16 with an invalid + // extension. MC68040UM 9.6.1 / MC68060UM 8.2.4 require vector 11, + // format zero and the causing instruction PC on the advanced models. + // Earlier models also take their documented Line-F exception. + var words = Enumerable.Range(0xf628, 24).Select(x => (ushort)x).ToArray(); + Assert.Equal(0xf628, words[0]); + Assert.Equal(0xf63f, words[^1]); + Assert.All(words, word => Assert.InRange((word >> 3) & 7, 5, 7)); + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "system-move16-reserved-first-words"); + foreach (var word in words) + foreach (var followingWord in new ushort[] { 0, 0x8000, 0xffff, 0x4e71 }) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + InvalidOperandScenario.Run(machine, report, word, + $"MOVE16/unassigned-first-word/following={followingWord:X4}", + supervisor, ccr, followingWord, vector: 11); + report.Complete(output); + } +} diff --git a/Copper68k/M68kAdvancedTimingInterpreter.System.cs b/Copper68k/M68kAdvancedTimingInterpreter.System.cs index d1b0d5ec..d5276cbc 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.System.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.System.cs @@ -44,7 +44,10 @@ private bool TryExecuteExtendedSystem(ushort opcode) source = (form & 1) == 0 ? State.A[register] : absolute; destination = (form & 1) == 0 ? absolute : State.A[register]; } - else { RaiseFormat0Exception(4, pc, M68kInstructionTimingKey.IllegalInstruction); return true; } + // F628..F63F do not assign a MOVE16 first-word form. They are + // unrecognized F-line words (MC68040UM 9.6.1 / MC68060UM 8.2.4), + // unlike a recognized F620..F627 word with an invalid extension. + else { RaiseFormat0Exception(11, pc, M68kInstructionTimingKey.LineFException); return true; } source &= 0xfffffff0; destination &= 0xfffffff0; Span line = stackalloc uint[4]; for (var i = 0; i < 4; i++) line[i] = ReadLong(source + (uint)i * 4); diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 205145ed..6b37a44e 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -267,9 +267,9 @@ bus ordering, detailed fault sequencing, JIT and native ROM/media tests remain. ## Remaining qualification and implementation gaps -The [revision-2 broad replay](#040060-coprocessor-id-priority-qualification--2026-10-08) -gets past `F520` and exposes reserved MOVE16 word `F628` on 040/060. The broad -gate remains failed, alongside the retained 010 RTE and 060 STOP disagreements. +The [reserved MOVE16 correction](#reserved-move16-first-word-qualification--2026-10-08) +gets the revision-2 broad replay past `F628` on 040/060: 1,379 directories pass, +and two remain mismatching (010 RTE and 060 STOP). The broad gate remains failed. - SingleStepTests has the pinned 312,500-case 68000 instruction-body audit. @@ -11372,3 +11372,161 @@ Evidence SHA-256: normalized new patch `e646382b98fb38533f6411fe13b52eecb92e78df `26e1bd6a201fc6d46df6ce2b38c86bce806e8aaa112ab6a06c0a833a0c808dc0`; input-delta record `c7500fae50a111c0ec60d141e295d89025083b2934876d77e051ec6154fab889`; complete linkage `cbd5c1d9f365dfd6aeec7941431c02cd3e55ee52ea2dc0c737ace153ec6bf2fd`. + +## Reserved MOVE16 first-word qualification — 2026-10-08 + +The newly exposed `F628` failure is a Copper68k exception-classification defect. +Five assigned MOVE16 first-word forms occupy `F600..F627`. Unassigned `F628..F63F` +are unrecognized F-line words, requiring vector 11 and a format-zero frame on +040/060 ([MC68040UM 9.6.1](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf), +[MC68060UM 8.2.4](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf)). They are +separate from an assigned `F620..F627` first word with a malformed extension, whose +existing illegal-instruction handling is retained. + +The bounded regression enumerates all 24 unassigned words, four following words +(`0000/8000/FFFF/4E71`), both incoming privilege states and all 32 CCRs on all eight +profiles. Shared independent fixtures verify saved PC/SR, stack selection, +register and memory preservation, and forbidden operand reads. The unchanged +CPU fails all 6,144 cases on each of 040 and 060 with the vector-4 handler PC +instead of vector 11; the other six profiles pass. With the single `System.cs` +classification correction, all 49,152 cases pass in eight xUnit batches. The +ordinary CI gate now requires the exact 6,144 cases per profile under +`system-move16-reserved-first-words`. No legal MOVE16 transfer, extension decode, +operand ordering or retry path changes. + +The native adapter already reports actual exception vector 4 correctly. Its +comparator rejects expected 11 / actual 4. The wording "got no exception" refers +to its vector-4 harness sentinel convention; it does not conceal a passing case. +Neither adapter nor comparator is changed. On the public 040/060 profiles, +`M68kTimingEngine` selects the one-native-cycle fixed plan for both exception keys; +changing the exception classification retains that approximate execution policy. +This is not physical exception timing qualification. + +A fresh broad replay uses the same revision-2 manifest and all 1,381 directories. +It passes 1,379, with two mismatches, zero unsupported and zero untested rows; +12,660,659 callbacks and 2,327,142 exception frames are checked. Both 040/060 +ILLEGAL directories now pass completely. Every other directory result is +identical to the earlier broad result, including 010 RTE callback 12 and 060 STOP +callback 3. Later cases inside those two failing directories remain unobserved. +All 32 comparator controls remain detected. Eight copied-evidence corruptions +are rejected, including removed failures/probes, changed source/assembly/input +identity, and rehashed report/counter/exit changes. Originals remain unchanged. +The frozen preconsolidation full CPU suite passes 5,307 tests, with zero failures +and 49 explicitly unavailable rows (5,356 total). Independent verification binds +86,502,658 logical cases, 929 complete reports and all ten native presets. The +ordinary gate passes 86,311,714 cases / 731 batches. A fresh immutable local-only +`1.5.2-synthetic-dev.74` package is built from those 37 CPU inputs plus the package +README/icon. A clean committed CopperScreen `aa1dad5` archive passes Release +build, 171 host tests / six unavailable, 74 disk tests, 1,080 engine tests and +two supplied Workbench 3.1 floppy replays. Independent verification checks the +archive's unchanged source files, exact package/source/DLL identities, restored +NuGet assets and private cache, all result rosters and pinned ROM/ADF inputs. +The 37 CPU source inputs also match the independently verified consolidated full +snapshot below; the package and consumer evidence remain separate frozen records. +The later AND consolidation has +a separate current-source full audit; these rosters must not be conflated. +Full proof SHA-256: `bff08761a1d952109c9156dda85977f965dc1c6afd2690958fb7eaf412a77e64`; +local package SHA-256: `468689e5a430cdd5bb624f0af6479b355a859a3a0adaedae1c073fb71b834803`. +Independent consumer proof: +`d751ebee088625bfff148109f6cee0a6c15fcbf92e1f903c80370f8c66e71c08`. +These results alone do not authorize roadmap completion or package publication. + +Local evidence is frozen under +`%TEMP%/copper68k-reference-restoration-20261006/`: the before/after snapshots are +`audits/ReservedMove16BeforeV1` and `audits/ReservedMove16AfterV1`; the broad replay +is `audits/QualifiedBasicMove16FixV1`. Independent proofs hash the actual commands, +source inputs, outputs and assemblies. Producer metadata in this slice contains +a literal trailing backslash-n; verification strips only that two-byte suffix +for parsing while retaining the original byte hashes. It does not rewrite any +execution evidence or normalize source, fixture or report content. + +Focused proof SHA-256: `af6d55e63ab797dff33135bd549ac119965a2277cb4cf215db5704e7f759fb1f`. +Failed broad proof: `0726e6a76e127561e0ca5fca29f837b88e396b61dbeb28808d85ccb069cdd9fa`. +Eight rejection controls: `18976a3005d31271205ce220f9b8a47ccf318c74e75588fe0004ef01edb54d9e`. +Broad report: `49de5c92b738b4bde099f54ecb90450e83c8390bf3dd9213c41418e28e329c5e`. + +Hardware is unavailable for the separate 030/040 trace boundary disagreement. +That gap and the other architectural/protocol requirements remain open. No +private pending-write CPU candidate is imported, no regression is retired and +no package is published. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## AND indirect semantic regression consolidation — 2026-10-08 + +The three EC020 rows of +`M68020AddressSourceTests.AndAddressIndirectUsesSelectedWidthAndPreservesExtend` +are replaced by `SyntheticAndIndirectTests.AndIndirectRetainsSelectedWidthRegistersAndFlags`. +The replacement uses the shared public-factory machine, operand fixture, recording +bus and complete architectural verifier. It preserves the captured literal +opcodes `C012/C052/C092`, operands `F0/F0A5/F0A55A0F`, initial data values +`FFFF00FF/FFFFFFFF/FFFFFFFF` and incoming CCR `17`, and expands to all eight +profiles, every D/A register selection, all 32 CCRs and both stack modes. +There are **12,288 cases per profile / 98,304 total**, in eight xUnit batches. +Expectations are literal sized AND results and independent N/Z/V/C/X rules; +no production arithmetic, decoder, EA or timing helper calculates them. + +The initial snapshot's byte/word setup incorrectly selected the low bytes of the +long capture. Its failures are preserved in `audits/AndIndirectConsolidationV1`. +Corrected captured-width fixtures pass all 98,304 cases and the three original +rows. A first general-logical-path mutation did not execute for these words; +the ineffective passing experiment remains in `audits/AndIndirectConsolidationV2` +and provides no replacement qualification. + +The corrected causal mutations target the actual byte/word handler and the +separate long effective-address handler. The independent verifier enumerates +every combination and predicts its exact status, rather than accepting aggregate +failure counts: + +| Targeted defect | Replacement mismatches | Original rows failing | +| --- | ---: | --- | +| Byte/word AND changed to OR | 49,152 | Byte and word | +| Byte/word Extend cleared | 24,576 | Byte and word | +| Byte/word upper register bits discarded | 49,152 | Byte and word | +| Long indirect AND changed to OR | 24,576 | Long | +| Long indirect Extend cleared | 12,288 | Long | + +The unchanged 000/010 execution paths pass in each mutation. Unaffected sizes and +CCR-X-clear combinations pass exactly where predicted. Test identifiers, source +preimages, actual executed assemblies, commands, reports, the eleven-row old/new +roster and precise register/SR failure categories are checked independently. +Both handler mutations remain private experiments; production CPU code is unchanged +by consolidation. Specialized timing, JIT, cache, bus and native media regressions +are retained. + +Removing only the documented method's three rows in a fresh isolated snapshot +passes **47 executions**: all 39 retained sibling rows and the eight replacement +batches. Replacement reports are identical to the earlier passing snapshot. +The maintained test source now matches that qualified replacement/removal exactly. +The ordinary CI gate now requires exactly 12,288 cases per profile under +`logical-and-indirect-captured`. This requirement was added after the frozen MOVE16 +gate completed. Its source snapshot intentionally retains the original three rows +and does not contain this replacement. The separate maintained full audit passes with the qualified 215-input source +roster: **5,312 passing / 49 unavailable / zero failures (5,361 total)**. Exact +old/new roster reconstruction binds **86,600,962 logical cases / 937 complete +reports / ten native presets**. All 929 retained reports are identical to their +parent records; all eight replacement reports match the isolated retirement. +The current ordinary gate passes **86,410,018 cases / 739 batches**. These two +source rosters remain separate full-suite executions. + +A composed **report-only** fixture combines the accepted frozen full reports +with the eight independently qualified AND reports and original integer inventory. +It passes the updated ordinary gate: 86,410,018 cases / 739 batches. Missing and +one-case-short 000 AND reports are rejected for the intended group, and original +reports remain unchanged after each control. This validates the CI requirement; +it executes no instructions and does not substitute for the separate completed +current full audit. Control proof SHA-256: +`2d2bc6b92a75f617cd83875094e7c0c596e3a5f1508126fd38e51a2be3e37a45`. + +Evidence lives under `%TEMP%/copper68k-reference-restoration-20261006/`: +`audits/AndIndirectConsolidationV3`, `audits/AndIndirectConsolidationV4` and +`audits/AndIndirectRetirementV5`. Causal proof SHA-256: +`22f30360da31b4767599210dd24286c53b14692d655ae91dfde2915a7bcfa775`; +isolated retirement proof: +`35364ec980b39a4bd04a7fe7f9f6a096c5706ab2434739e05c9871eadf39e5fb`. +Current consolidated full proof SHA-256: `2f52f036cae9b0dd7b01720a451e2b3d74eb2f1a9c50750d9b2dd2dd86a90920`. +Complete linkage: `33ae2c4a4ff4870229e16327e533c876614b1664da5662948532b714a08eb05f`. The linkage checks that current maintained +CPU/test sources equal the executed frozen inputs and that the production CPU +sources equal the local package and corrected broad replay. It retains the broad +failed gate and does not rewrite earlier evidence that recorded the full run as +pending. +No private CPU candidate import, package publication, hardware qualification or +roadmap completion is claimed. Milestone 6 remains **in progress**. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 3b2f559e..a4b922f2 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3571,3 +3571,42 @@ See [the qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040060-copro Milestone 6 remains **in progress**, `roadmapComplete=false`. The new encoding and adapter discrepancy, later failures and prior architectural/protocol gaps must be addressed; no source candidate import or package publication occurs. + +### Milestone 6 reserved MOVE16 first words — 2026-10-08 + +Unassigned `F628..F63F` words incorrectly raised vector 4 on 040/060. Their +documented Line-F classification is fixed without changing assigned transfers, +extension decoding or the fixed-cycle policy. The new ordinary gate executes +49,152 cases across eight profiles; it detects 12,288 failures before the fix +and passes completely afterward. The unchanged revision-2 broad corpus now +passes 1,379 directories, retaining the 010 RTE and 060 STOP mismatches. Eight +evidence-corruption controls reject invalid acceptance records. See +[the qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#reserved-move16-first-word-qualification--2026-10-08). +The frozen preconsolidation full CPU suite passes 5,307 / 49 unavailable / zero +failures; independent verification binds 86,502,658 cases / 929 reports / ten native +presets, and the ordinary gate passes 86,311,714 cases / 731 batches. Fresh local +`.74` consumers pass Release build, 171 host / six unavailable, 74 disk, 1,080 +engine and two supplied Workbench floppy replays, with independent source/package +bindings. The later consolidated full audit passes separately: 5,312 / 49 +unavailable / zero failures, 86,600,962 cases / 937 reports / ten native presets. +Milestone 6 stays +**in progress**, `roadmapComplete=false`; hardware is unavailable for the separate +030/040 trace disagreement. No private CPU candidate import or publication occurs. + +### Milestone 6 AND indirect consolidation — 2026-10-08 + +The three captured EC020 AND-indirect semantic rows are replaced by 98,304 shared +fixture cases across all eight profiles. Five causal mutations cover both executed +handlers, logical results, Extend preservation and partial-register preservation; +each fails the corresponding original row and exact predicted replacement cases. +An isolated retirement run passes all 39 sibling rows and eight replacement batches. +See [the qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#and-indirect-semantic-regression-consolidation--2026-10-08). +The ordinary CI gate now requires 12,288 `logical-and-indirect-captured` cases per +profile; the current full audit passes separately from the accepted +preconsolidation snapshot (5,312 passing / 49 unavailable / zero failures). A complete composed report-only fixture passes 86,410,018 cases / 739 +batches; missing/incomplete AND controls reject. That check executes no instructions +and does not qualify the separate current full run. Its independent proof binds +86,600,962 cases / 937 reports / ten native presets; its current ordinary gate +passes 86,410,018 cases / 739 batches. Production CPU behavior, +specialized regressions and the +remaining reference requirements are unchanged. Milestone 6 stays **in progress**. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index d10dab0e..f9d754d0 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -94,6 +94,7 @@ try { 'logical-bitfield-values'=155136; 'logical-bits'=$(if ($model -in @('68000','68010')) {104192} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {104852}); 'logical-boundaries'=31808; + 'logical-and-indirect-captured'=12288; 'logical-invalid-operands'=13248; 'logical-unary-invalid-operands'=$(if ($model -in @('68000','68010')) {12544} elseif ($model -eq '68060') {10304} else {10432}); 'logical-bitfield-invalid-operands'=13312; @@ -114,6 +115,7 @@ try { 'system-callm'=$(if ($model -in @('68000','68010')) {10832} elseif ($model -in @('68020','68EC020','A1200')) {11000} elseif ($model -in @('68030','68040','68060')) {10964}); 'system-interrupt'=$(if ($model -in @('68000')) {224} elseif ($model -in @('68010','68060')) {226} elseif ($model -in @('68020','68030','68040','68EC020','A1200')) {418}); 'system-model'=3584; + 'system-move16-reserved-first-words'=6144; 'system-cache-encodings'=16384; 'system-lpstop-values'=17024; 'system-movec'=2432; From 4aa9db3457e450b1aca06b413a241a8527ab44d0 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 09:07:52 +0300 Subject: [PATCH 146/211] Qualify 040 MOVEM register-mask fault recovery --- Copper68k.Tests/Synthetic/README.md | 23 +++ .../SyntheticM68040MovemMaskFaultTests.cs | 61 ++++++ ...eticM68040MovemWriteFaultDiscoveryTests.cs | 32 ++-- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 88 +++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 38 ++++ .../test-copper68k-040-movem-mask-faults.ps1 | 56 ++++++ scripts/test-copper68k-synthetic.ps1 | 2 + .../verify-copper68k-040-movem-mask-faults.py | 173 ++++++++++++++++++ 8 files changed, 462 insertions(+), 11 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040MovemMaskFaultTests.cs create mode 100644 scripts/test-copper68k-040-movem-mask-faults.ps1 create mode 100644 scripts/verify-copper68k-040-movem-mask-faults.py diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 316e8c87..88e88327 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1335,3 +1335,26 @@ The accepted preconsolidation full suite has a separate source roster; the maintained consolidated full audit passes separately with 86,600,962 cases / 937 complete reports / ten native presets; its ordinary gate passes 86,410,018 cases / 739 batches. + +The new 040 MOVEM mask fault audit extends the existing physical writeback/CM +fixture. Ordinary CI requires 1,364 `movem-mask-fault-controls-scalar` and 1,364 +`movem-mask-fault-controls-batch` whole programs. It checks all selected fault +positions for 55 representative masks, both sizes and indirect/predecrement A2. +The opt-in audit adds all 65,536 mask words with a last-selected-transfer fault; +empty masks are fault-free controls. It retains the existing recovery matrix and +fixed encoding witnesses. Both the frozen deep audit and maintained command pass +670,664 programs in thirteen tests / twelve reports, with exact inventories, +unchanged retained reports and eight evidence-corruption controls. See the +[qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-movem-register-mask-access-fault-qualification--2026-10-08). + +```powershell +./scripts/test-copper68k-040-movem-mask-faults.ps1 -OutputDirectory artifacts/movem-mask-audit +``` + +Use PowerShell 7 and Python 3 (`-PythonCommand` selects an installed interpreter). +A fresh output is required; the command copies and hashes a frozen source graph, +uses isolated build outputs, and requires all thirteen tests and twelve reports. +`-ValidateReportsOnly` verifies existing frozen evidence without executing CPU +instructions. `-BaselineDirectory` additionally requires unchanged retained +recovery reports; a missing requested baseline fails. Neither mode promotes +missing inputs, empty selections or mismatching/unsupported execution. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040MovemMaskFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040MovemMaskFaultTests.cs new file mode 100644 index 00000000..32e262e0 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040MovemMaskFaultTests.cs @@ -0,0 +1,61 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 8.4.6.2/7: CM restarts after EA calculation and repeats +// all selected operand accesses. The mask/order oracle is test-owned. +public sealed class SyntheticM68040MovemMaskFaultTests(ITestOutputHelper output) +{ + internal const string Enable = "COPPER68K_RUN_040_MOVEM_MASK_FAULT_AUDIT"; + + [Theory, Trait("Suite", "Synthetic")] + [InlineData(false)] + [InlineData(true)] + public void SelectedMasksRecoverEveryTransferWithoutLosingRegisterOrder(bool batch) + { + var report = new CoverageBatch("68040", "movem-mask-fault-controls-" + Route(batch)); + var masks = new SortedSet { 0, 0xffff, 0x00ff, 0xff00, 0x5555, 0xaaaa, 0x0303, 0x8001 }; + for (var bit = 0; bit < 16; bit++) + { + masks.Add((ushort)(1 << bit)); + masks.Add((ushort)(0xffff ^ (1 << bit))); + if (bit < 15) masks.Add((ushort)(3 << bit)); + } + foreach (var mask in masks) + foreach (var mode in new[] { 2, 4 }) + foreach (var width in new[] { 2, 4 }) + { + var count = Enumerable.Range(0, 16).Count(bit => (mask & (1 << bit)) != 0); + if (count == 0) Run(report, batch, "mask-controls", mode, width, mask, 0, reject: false); + else for (var transfer = 0; transfer < count; transfer++) + Run(report, batch, "mask-controls", mode, width, mask, transfer); + } + report.Complete(output); + } + + [EnvironmentFact(Enable, "audit all 65,536 MOVEM mask words with real write faults"), Trait("Suite", "ReferenceDiscovery")] + public void EveryMaskWordScalar() => Audit(false); + + [EnvironmentFact(Enable, "audit all 65,536 MOVEM mask words with real write faults"), Trait("Suite", "ReferenceDiscovery")] + public void EveryMaskWordBatch() => Audit(true); + + private void Audit(bool batch) + { + var report = new CoverageBatch("68040", "movem-mask-fault-all-" + Route(batch)); + for (var mask = 0; mask <= 0xffff; mask++) + foreach (var mode in new[] { 2, 4 }) + foreach (var width in new[] { 2, 4 }) + { + var count = Enumerable.Range(0, 16).Count(bit => (mask & (1 << bit)) != 0); + Run(report, batch, "all-masks", mode, width, (ushort)mask, Math.Max(0, count - 1), reject: count != 0); + } + report.Complete(output); + } + + private static void Run(CoverageBatch report, bool batch, string cohort, int mode, int width, + ushort mask, int transfer, bool reject = true) + => SyntheticM68040MovemWriteFaultDiscoveryTests.Case(report, batch, cohort, new(mode, 2), + width, transfer, width - 1, "ISP", 0, 31, reject: reject, recover: reject, registerMask: mask); + + private static string Route(bool batch) => batch ? "batch" : "scalar"; +} diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteFaultDiscoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteFaultDiscoveryTests.cs index 4d5f2bd0..b81ad484 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteFaultDiscoveryTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteFaultDiscoveryTests.cs @@ -75,11 +75,12 @@ void Cases(string cohort, OperandForm form, string bank, ushort trace, int ccr) internal static void Case(CoverageBatch report, bool batch, string cohort, OperandForm form, int width, int transfer, int faultByte, string bank, ushort trace, int ccr, bool reject = true, - bool recover = false, IndexFixture? index = null, bool overwritePointer = false) + bool recover = false, IndexFixture? index = null, bool overwritePointer = false, ushort? registerMask = null) { var opcode = Opcode(form, width); var indexedId = index is null ? "" : $"/index={index.Id}/ix={(index.AddressIndex ? "A" : "D")}{index.IndexRegister}/{(index.LongIndex ? "L" : "W")}/scale={1 << index.Scale}/pointer-alias={overwritePointer}"; - var id = $"68040/MOVEM/write-fault/{cohort}/size={width}/mode={form.Mode}/reg={form.Register}{indexedId}/bank={bank}/T={trace:X4}/transfer={transfer}/byte={faultByte}/op={opcode:X4}/ccr={ccr:X2}"; + var maskId = registerMask is { } selectedMask ? $"/mask={selectedMask:X4}" : ""; + var id = $"68040/MOVEM/write-fault/{cohort}/size={width}/mode={form.Mode}/reg={form.Register}{indexedId}{maskId}/bank={bank}/T={trace:X4}/transfer={transfer}/byte={faultByte}/op={opcode:X4}/ccr={ccr:X2}"; try { var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); @@ -90,7 +91,15 @@ internal static void Case(CoverageBatch report, bool batch, string cohort, Opera var stacks = new Dictionary { ["user"] = 0x7800, ["ISP"] = 0x4700, ["MSP"] = 0x7400 }; SetStacks(); var registers = m.Core.State.D.Concat(m.Core.State.A).ToArray(); - var words = new List { opcode, (ushort)(form.Mode == 4 ? 0xc0c0 : 0x0303) }; + var logicalMask = registerMask ?? 0x0303; + var encodedMask = logicalMask; + if (form.Mode == 4) + { + encodedMask = 0; + for (var bit = 0; bit < 16; bit++) + if ((logicalMask & (1 << bit)) != 0) encodedMask |= (ushort)(1 << (15 - bit)); + } + var words = new List { opcode, encodedMask }; var fixture = new AddressingFixture(m, width, words, (uint[])m.Core.State.D.Clone(), (uint[])m.Core.State.A.Clone(), 0, new(DestinationIndex: index ?? new(IndexRegister: 1, LongIndex: true, BriefDisplacement: 64), @@ -104,8 +113,9 @@ internal static void Case(CoverageBatch report, bool batch, string cohort, Opera if (fixture.DestinationPointerAddress != first) throw new InvalidOperationException("Wrong independently intended pointer location"); } var ea = form.Mode == 4 ? registers[8 + form.Register] : first; - var addresses = Enumerable.Range(0, 4).Select(n => unchecked(first + (uint)(form.Mode == 4 ? -n * width : n * width))).ToArray(); - int[] order = form.Mode == 4 ? [9, 8, 1, 0] : [0, 1, 8, 9]; + var selected = Enumerable.Range(0, 16).Where(r => (logicalMask & (1 << r)) != 0); + int[] order = (form.Mode == 4 ? selected.Reverse() : selected).ToArray(); + var addresses = Enumerable.Range(0, order.Length).Select(n => unchecked(first + (uint)(form.Mode == 4 ? -n * width : n * width))).ToArray(); var values = order.Select(r => registers[r] - (form.Mode == 4 && r == 8 + form.Register ? (uint)width : 0u)) .Select(v => width == 2 ? v & 0xffff : v).ToArray(); foreach (var at in addresses) m.InitializePhysical(at, 0x5a5aa5a5, width); @@ -126,10 +136,10 @@ internal static void Case(CoverageBatch report, bool batch, string cohort, Opera e.ControlChecks["SFC"] = (s => s.SourceFunctionCode, 6); if (!reject) { - for (var n = 0; n < 4; n++) e.Write(addresses[n], values[n], width, m.Model); + for (var n = 0; n < order.Length; n++) e.Write(addresses[n], values[n], width, m.Model); if (form.Mode == 4) { - e.A[form.Register] = unchecked(ea - (uint)(4 * width)); + e.A[form.Register] = unchecked(ea - (uint)(order.Length * width)); if (form.Register == 7) stacks[M68040StackFixture.PhysicalBank(bank)] = e.A[7]; } M68040StackFixture.SetStacks(e, stacks, sr); e.Pc = fixture.NextPc; @@ -205,10 +215,10 @@ internal static void Case(CoverageBatch report, bool batch, string cohort, Opera } if (!handlerStores.SequenceEqual(new[] { (addresses[transfer], width, values[transfer]) })) throw new InvalidOperationException("WB1 handler did not complete the independently intended store"); - for (var n = 0; n < 4; n++) e.Write(addresses[n], values[n], width, m.Model); + for (var n = 0; n < order.Length; n++) e.Write(addresses[n], values[n], width, m.Model); if (form.Mode == 4) { - e.A[form.Register] = unchecked(ea - (uint)(4 * width)); + e.A[form.Register] = unchecked(ea - (uint)(order.Length * width)); if (form.Register == 7) stacks[M68040StackFixture.PhysicalBank(bank)] = e.A[7]; } M68040StackFixture.SetStacks(e, stacks, sr); e.Pc = fixture.NextPc; @@ -216,10 +226,10 @@ internal static void Case(CoverageBatch report, bool batch, string cohort, Opera var resumedStart = bus.Accesses.Count; Execute(); if (e.Verify(m) is { } resumedMismatch) throw new InvalidOperationException("resumed MOVEM: " + resumedMismatch); - var resumedWrites = bus.Accesses.Skip(resumedStart).Where(a => a.Write).Take(4) + var resumedWrites = bus.Accesses.Skip(resumedStart).Where(a => a.Write).Take(order.Length) .Select(a => (a.Address, a.Width, a.Value)); if (!resumedWrites.SequenceEqual(addresses.Select((at, n) => (at, width, values[n])))) - throw new InvalidOperationException("CM did not repeat the original four operand writes in order"); + throw new InvalidOperationException("CM did not repeat the selected operand writes in order"); if (fixture.DestinationPointerAddress is { } pointer && bus.Accesses.Count(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address == pointer && a.Width == 4) != 1) throw new InvalidOperationException("CM repeated indirect-pointer resolution"); diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 6b37a44e..380e2ae6 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -11530,3 +11530,91 @@ failed gate and does not rewrite earlier evidence that recorded the full run as pending. No private CPU candidate import, package publication, hardware qualification or roadmap completion is claimed. Milestone 6 remains **in progress**. + +## 040 MOVEM register-mask access-fault qualification — 2026-10-08 + +The existing physical MOVEM store frame/handler/CM recovery gate uses a fixed +logical `0303` list. The new test-internal optional mask fixture preserves that +default, its predecrement `C0C0` encoding and all existing successful report keys. +Register selection, reversed predecrement encoding, ordering and initial-base +minus one operand size are computed independently of the production decoder. +Their reference is [M68000PM MOVEM, 4-128–4-130](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf): +control-mode lists run D0 through A7, predecrement reverses the list and its mask +bits, and 040 stores the initial selected base minus one operand size. +Source and destination registers, exact saved/restored PC/SR and stack state, +defined frame fields, WB1 lanes, completed pre-fault writes, actual handler steps, +resumed operand order and following MOVEQ are verified through the public factory. +RTE explicitly repeats MOVEM operands as documented in MC68040UM 8.4.6.7; no +automatic instruction retry is added. + +The bounded controls execute 55 logical masks: empty/full, all sixteen singletons, +all sixteen one-register omissions, fifteen adjacent pairs, alternating/data-only/ +address-only lists and captured/boundary lists. Both sizes and indirect/predecrement +A2 are tested on scalar and batch routes, ISP and CCR 31. Every selected transfer +is faulted at its last byte; empty masks execute fault-free and check the following +instruction. Each route passes **1,364 programs**, totaling **2,728 new programs**. +A frozen 216-input snapshot passes nine bounded tests and **3,048 whole programs**; +the six retained witness/fault-free reports are identical to the preceding +215-input full-suite reports. Production CPU inputs are unchanged. + +Private continuation-only mutations qualify causality: + +| Defect | Mismatches per route | Unchanged passing per route | +| --- | ---: | ---: | +| Omit initial-base decrement when replaying its selected register | 524 | 840 | +| Omit A7 replay | 1,048 | 316 | +| Replay a different data register | 1,240 | 124 | + +The independent verifier predicts exact mask/mode membership and every +passing/failing combination. Omitted A7 causes a final-base mismatch for +predecrement; for indirect stores, it either leaves missing memory or reaches +the replay-order check, depending on whether the original fault occurred at A7. +The initial verifier's too-narrow diagnostic assertion fails and is preserved; +the successor distinguishes these outcomes without changing any execution. +All mutations affect only owned CPU copies. No old regression is retired. + +The complete audit additionally enumerates every one of **65,536 mask words**, +both sizes and both A2 forms, on each route: **524,288 programs**. Nonempty masks +fault their last selected transfer; zero masks are fault-free. It retains the +separate **143,512-program** earlier recovery matrix and **136 fault-free** +programs. Required total: **670,664 programs / thirteen tests / twelve reports**. +The frozen discovery audit passes all **670,664 programs / thirteen tests / twelve +reports**, with zero mismatching/unsupported/untested cases. Independent verification +binds exact source/assembly/command/TRX/combination identities and all six retained +recovery reports match the accepted baseline. Eight copied-evidence corruptions +reject for the intended reasons; originals remain unchanged. The maintained command +also passes the same thirteen tests / twelve reports / 670,664 programs in its own +frozen snapshot, including the requested baseline comparison. Every report matches +the first completed audit. The 37 production CPU inputs remain byte-identical to +the preceding full CPU and `.74` consumer qualification; only test inputs change. +No new wide full-suite execution is claimed for the 216-input test graph. + +```powershell +./scripts/test-copper68k-040-movem-mask-faults.ps1 -OutputDirectory artifacts/movem-mask-audit +``` + +The command requires PowerShell 7 and Python 3, freezes source bytes in a fresh +directory, isolates build/results, and invokes an independent architectural +combination verifier. `-PythonCommand` selects the interpreter; +`-BaselineDirectory` requires the retained recovery reports to match a supplied +accepted baseline. Requested missing inputs already reject. `-ValidateReportsOnly` +performs no CPU execution. The ordinary CI gate now requires each 1,364-program +control report. Its composed report-only fixture passes **86,412,746 cases / 741 +batches**; missing/incomplete scalar controls reject. That fixture executes no +instructions and does not substitute for the separate deep program audit. + +Local frozen evidence: `%TEMP%/copper68k-reference-restoration-20261006/`: +`audits/MovemMaskControlsV1`, `mutations/MovemMaskV1`, `audits/MovemMaskFullV1`, +`audits/MovemMaskMaintainedCommandV1` and `movem-mask-ordinary-controls-v1`. +Bounded proof SHA-256: +`2b5b36b0286bb0b0118e607590c5d33b5c8494d1a272a9409fa6973f005e5786`; +mutation proof: +`cd533a4941106c9acb39391d121139485c356225be1e4c36a7d7ef534d1bca3e`. +Frozen complete proof: `2562176b2999f2aea8bc2ba905bc327db0833017578d566fe63b8461554073eb`; +eight integrity controls: `e8abc5ecbcc67edd3dbaa139f1c5624851de2e8c5dee7c1a4d91b661548c53be`; +ordinary report-only controls: `4e23e681a0f1f39deb6988020e1dbe1f3587ca678ca6e94b5d44686d60d4d231`. +Maintained-command proof: `8160c5a78a52a4a9aeccbb752e2973067d230ce9e7a99abd07bbd3bd536e1e13`; +complete source/evidence linkage: `3f5d769639d7cc0678980fc24c0b5cf575c5f03c4d70b1280b31d34b1905e423`. +This slice does not establish every mask/fault-position Cartesian combination, +other stack/CCR/EA combinations, enabled cache/MMU, physical timing or hardware +trace behavior. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index a4b922f2..a79b989a 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3610,3 +3610,41 @@ and does not qualify the separate current full run. Its independent proof binds passes 86,410,018 cases / 739 batches. Production CPU behavior, specialized regressions and the remaining reference requirements are unchanged. Milestone 6 stays **in progress**. + +### Milestone 6 MOVEM mask access-fault qualification — 2026-10-08 + +Extend the previously qualified 040 physical MOVEM store/CM recovery fixture from +its captured four-register mask to independent register-list masks. The new +ordinary controls execute 55 masks, including empty/full masks, each singleton, +each omitted register, adjacent pairs and alternating data/address selections. +Every selected transfer is faulted for word/long indirect and predecrement stores +using A2, both scalar/batch routes, ISP and CCR 31. Empty masks execute without a +fault and must leave operands unchanged. All 2,728 new programs pass; nine bounded +tests also preserve the six earlier witness reports exactly. + +Three private mutations qualify detection after CM return: lost initial-base +decrement (524 mismatches per route), omitted A7 replay (1,048), and wrong data +register replay (1,240). Independent enumeration checks every passing/failing +combination and its diagnostic category. The initial diagnostic verifier omitted +normal-mode missing-memory cases; its failure is retained and a successor verifies +the distinct memory, final-base and transfer-order outcomes. + +The deep audit must execute all 65,536 mask words per route, both sizes and both +addressing forms. Each nonempty mask faults its last selected transfer; empty +masks are fault-free. This adds 524,288 whole programs. The unchanged earlier +143,512-program recovery matrix and 136 fault-free programs remain separate +required cohorts. The frozen complete audit passes **670,664 programs / thirteen +tests / twelve reports** with exact independently verified inventories and retained +report comparison. Eight altered-evidence controls reject; the ordinary report-only +gate passes 86,412,746 cases / 741 batches and rejects missing/incomplete controls. +The maintained command also passes the same complete gate from its separate +frozen 216-input snapshot; all twelve reports match the first audit. Complete +source/evidence linkage is recorded in the +[qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040-movem-register-mask-access-fault-qualification--2026-10-08). +The prior wide full suite remains a separate 215-input execution; production +CPU sources remain identical, and no new wide full-suite run is claimed. + +No production CPU source or package changes occur. This slice does not qualify +all mask/fault-position Cartesian combinations, other CCR/stack/EA combinations, +enabled MMU/cache, physical timing, or the unresolved hardware boundaries. +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-040-movem-mask-faults.ps1 b/scripts/test-copper68k-040-movem-mask-faults.ps1 new file mode 100644 index 00000000..eb3574d7 --- /dev/null +++ b/scripts/test-copper68k-040-movem-mask-faults.ps1 @@ -0,0 +1,56 @@ +#requires -Version 7.0 +[CmdletBinding()] +param( + [string]$OutputDirectory='artifacts/040-movem-mask-faults', + [switch]$ValidateReportsOnly, + [string]$PythonCommand='python', + [string]$BaselineDirectory +) +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +function Hash([string]$Path) { (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() } +if (-not $ValidateReportsOnly) { + if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } + $sourceRoot=Join-Path $output 'source' + New-Item -ItemType Directory -Path $sourceRoot | Out-Null + $sources=@(@(& git -C $repo ls-files 'Copper68k/*.cs' 'Copper68k/*.csproj' 'Copper68k.Tests/*.cs' 'Copper68k.Tests/*.csproj') + + @('Copper68k.Tests/Synthetic/SyntheticM68040MovemMaskFaultTests.cs') | Sort-Object -Unique) + if ($sources.Count -eq 0) { throw 'Empty source selection' } + $identities=@{} + foreach ($name in $sources) { + $from=Join-Path $repo $name; $to=Join-Path $sourceRoot $name + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $to) | Out-Null + Copy-Item -LiteralPath $from -Destination $to + $identities[$name]=Hash $to + if ((Hash $from) -ne $identities[$name]) { throw "Source changed while freezing: $name" } + } + $protected=@(@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | + Where-Object { Test-Path -LiteralPath (Join-Path $repo $_) } | + ForEach-Object { @{path=$_;sha256=(Hash (Join-Path $repo $_))} }) + $selection='FullyQualifiedName~SyntheticM68040MovemMaskFaultTests|FullyQualifiedName~SyntheticM68040MovemWriteRecoveryTests|FullyQualifiedName~FixedManualStoreEncodings|FullyQualifiedName~CanonicalStoreFixturesExecuteWithoutFault' + $command=@('dotnet','test',(Join-Path $sourceRoot 'Copper68k.Tests/Copper68k.Tests.csproj'),'-c','Release', + '--artifacts-path',(Join-Path $output 'build'),'--filter',$selection,'--logger','trx;LogFileName=audit.trx','--results-directory',$output) + $settings=@{COPPER68K_SYNTHETIC_REPORT_DIR=$output;COPPER68K_RUN_040_MOVEM_MASK_FAULT_AUDIT='1';COPPER68K_RUN_040_MOVEM_WRITE_RECOVERY='1'} + $inputs=@{sourceCommit=(& git -C $repo rev-parse HEAD);sources=$identities;command=$command;settings=$settings; + producerSha256=(Hash $PSCommandPath);protected=$protected;publication=$false;privateCandidateImport=$false} + $inputsPath=Join-Path $output 'inputs.json' + $inputs | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $inputsPath -Encoding utf8 + $saved=@{} + try { + foreach ($key in $settings.Keys) { $saved[$key]=[Environment]::GetEnvironmentVariable($key); [Environment]::SetEnvironmentVariable($key,$settings[$key]) } + $arguments=$command[1..($command.Count-1)] + & dotnet @arguments *> (Join-Path $output 'execution.log') + $testExit=$LASTEXITCODE + } finally { + foreach ($key in $saved.Keys) { [Environment]::SetEnvironmentVariable($key,$saved[$key]) } + } + @{exit=$testExit;inputsSha256=(Hash $inputsPath);logSha256=(Hash (Join-Path $output 'execution.log')); + trxSha256=(Hash (Join-Path $output 'audit.trx'));cpuSha256=(Hash (Join-Path $output 'build/bin/Copper68k/release/Copper68k.dll')); + testSha256=(Hash (Join-Path $output 'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'))} | + ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'execution.json') -Encoding utf8 +} +$verify=@((Join-Path $PSScriptRoot 'verify-copper68k-040-movem-mask-faults.py'),'--repo',$repo,'--output',$output,'--producer',$PSCommandPath) +if ($BaselineDirectory) { $verify+=@('--baseline',[IO.Path]::GetFullPath($BaselineDirectory,$repo)) } +& $PythonCommand @verify +if ($LASTEXITCODE -ne 0) { throw "040 MOVEM mask fault qualification failed; preserve evidence: $output" } diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index f9d754d0..fb1a8033 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -240,6 +240,8 @@ try { $expected['rte-access-trace'] = 13824 $expected['rte-access-movem-opcodes'] = 120960 $expected['rte-access-movem-full-index'] = 114048 + $expected['movem-mask-fault-controls-scalar'] = 1364 + $expected['movem-mask-fault-controls-batch'] = 1364 $expected['handler-prefetch-executing-scalar'] = 12288 $expected['handler-prefetch-executing-batch'] = 12288 $expected['handler-prefetch-entry-scalar'] = 196608 diff --git a/scripts/verify-copper68k-040-movem-mask-faults.py b/scripts/verify-copper68k-040-movem-mask-faults.py new file mode 100644 index 00000000..949d5785 --- /dev/null +++ b/scripts/verify-copper68k-040-movem-mask-faults.py @@ -0,0 +1,173 @@ +"""Verify frozen MOVEM mask fault programs, not xUnit counts alone.""" +from pathlib import Path +import argparse, hashlib, json, re, subprocess, xml.etree.ElementTree as ET + +parser = argparse.ArgumentParser() +parser.add_argument('--repo', type=Path, required=True) +parser.add_argument('--output', type=Path, required=True) +parser.add_argument('--baseline', type=Path) +parser.add_argument('--producer', type=Path, required=True) +args = parser.parse_args() +root = args.output.resolve() +ns = {'t': 'http://microsoft.com/schemas/VisualStudio/TeamTest/2010'} + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + +def load(path): + return json.loads(path.read_text(encoding='utf-8-sig')) + +inputs = load(root / 'inputs.json') +execution = load(root / 'execution.json') +assert execution['exit'] == 0 and execution['inputsSha256'] == sha(root / 'inputs.json') +assert not inputs['publication'] and not inputs['privateCandidateImport'] +assert inputs['producerSha256'] == sha(args.producer) +sources = inputs['sources'] +tracked = subprocess.check_output(['git', 'ls-files', 'Copper68k/*.cs', 'Copper68k/*.csproj', + 'Copper68k.Tests/*.cs', 'Copper68k.Tests/*.csproj'], + cwd=args.repo, text=True).splitlines() +expected_sources = set(tracked) | {'Copper68k.Tests/Synthetic/SyntheticM68040MovemMaskFaultTests.cs'} +assert set(sources) == expected_sources and sources +for name, digest in sources.items(): + assert sha(args.repo / name) == sha(root / 'source' / name) == digest, name +for item in inputs.get('protected', []): + assert sha(args.repo / item['path']) == item['sha256'], 'Normal output changed' +settings = {'COPPER68K_SYNTHETIC_REPORT_DIR': str(root), + 'COPPER68K_RUN_040_MOVEM_MASK_FAULT_AUDIT': '1', + 'COPPER68K_RUN_040_MOVEM_WRITE_RECOVERY': '1'} +assert inputs['settings'] == settings +selection = ('FullyQualifiedName~SyntheticM68040MovemMaskFaultTests|' + 'FullyQualifiedName~SyntheticM68040MovemWriteRecoveryTests|' + 'FullyQualifiedName~FixedManualStoreEncodings|' + 'FullyQualifiedName~CanonicalStoreFixturesExecuteWithoutFault') +assert inputs['command'] == ['dotnet', 'test', str(root / 'source/Copper68k.Tests/Copper68k.Tests.csproj'), + '-c', 'Release', '--artifacts-path', str(root / 'build'), '--filter', selection, + '--logger', 'trx;LogFileName=audit.trx', '--results-directory', str(root)] +for key, path in [('logSha256', 'execution.log'), ('trxSha256', 'audit.trx'), + ('cpuSha256', 'build/bin/Copper68k/release/Copper68k.dll'), + ('testSha256', 'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll')]: + assert execution[key] == sha(root / path), key + +prefix = 'Copper68k.Tests.Synthetic.' +names = { + prefix + 'SyntheticM68040MovemMaskFaultTests.EveryMaskWordScalar', + prefix + 'SyntheticM68040MovemMaskFaultTests.EveryMaskWordBatch', + prefix + 'SyntheticM68040MovemWriteRecoveryTests.ScalarRecoveryMatrix', + prefix + 'SyntheticM68040MovemWriteRecoveryTests.BatchRecoveryMatrix', + prefix + 'SyntheticM68040MovemWriteFaultDiscoveryTests.FixedManualStoreEncodings'} +for batch in ('False', 'True'): + for cls, method in [('SyntheticM68040MovemMaskFaultTests', 'SelectedMasksRecoverEveryTransferWithoutLosingRegisterOrder'), + ('SyntheticM68040MovemWriteRecoveryTests', 'EveryCanonicalStoreFormRunsHandlerAndResumes'), + ('SyntheticM68040MovemWriteRecoveryTests', 'SelfOverwrittenIndirectPointerIsNotResolvedAgain'), + ('SyntheticM68040MovemWriteFaultDiscoveryTests', 'CanonicalStoreFixturesExecuteWithoutFault')]: + names.add(f'{prefix}{cls}.{method}(batch: {batch})') +tree = ET.parse(root / 'audit.trx') +rows = tree.findall('.//t:UnitTestResult', ns) +assert len(rows) == len(names) == 13 and {r.get('testName') for r in rows} == names +assert len({r.get('testId') for r in rows}) == 13 and all(r.get('outcome') == 'Passed' for r in rows) +counters = tree.find('.//t:Counters', ns) +assert all(int(counters.get(k)) == 13 for k in ('total', 'executed', 'passed')) +assert int(counters.get('failed')) == 0 +assert tree.find('.//t:ResultSummary', ns).get('outcome') == 'Completed' +summaries = {} +for row in rows: + output = row.find('t:Output/t:StdOut', ns) + for match in re.finditer(r'68040/([^:\r\n]+): (\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=(\d+), combinations=(\d+)', + output.text or '' if output is not None else ''): + assert match[1] not in summaries + summaries[match[1]] = (json.loads(match[2]), int(match[3]), int(match[4]), int(match[5])) + +# This independent inventory uses integer bit-counts, not the C# fixture's +# selected-register sequence, encoder or production decoder. +controls = {0, 0xffff, 0xff, 0xff00, 0x5555, 0xaaaa, 0x303, 0x8001} +for bit in range(16): + controls.update((1 << bit, 0xffff ^ (1 << bit))) + if bit < 15: + controls.add(3 << bit) +assert len(controls) == 55 +inventories = {} +for cohort, group, masks in [('mask-controls', 'movem-mask-fault-controls', sorted(controls)), + ('all-masks', 'movem-mask-fault-all', range(65536))]: + keys = {} + for mask in masks: + count = mask.bit_count() + transfers = range(max(1, count)) if cohort == 'mask-controls' else [max(0, count - 1)] + for mode in (2, 4): + for width in (2, 4): + for transfer in transfers: + key = (f'68040/MOVEM/write-fault/{cohort}/size={width}/mode={mode}/reg=2/' + f'mask={mask:04X}/bank=ISP/T=0000/transfer={transfer}/byte={width-1}') + assert key not in keys + keys[key] = 1 + assert len(keys) == (1364 if cohort == 'mask-controls' else 262144) + for route in ('scalar', 'batch'): + inventories[f'{group}-{route}'] = keys + +forms = [f'mode={mode}/reg={reg}' for mode in (2, 4, 5, 6, 7) + for reg in range(2 if mode == 7 else 8)] +canonical = ['mode=2/reg=0', 'mode=4/reg=0', 'mode=4/reg=7', 'mode=5/reg=0', + 'mode=6/reg=0', 'mode=7/reg=0', 'mode=7/reg=1'] +full = [] +for bs in ('False', 'True'): + for ix_suppressed in ('False', 'True'): + for bd in (1, 2, 3): + for iis in (0, 1, 2, 3, 5, 6, 7): + if ix_suppressed == 'True' and iis >= 5: + continue + for index in ('D1/W/scale=1', 'D1/L/scale=1', 'D1/W/scale=8', 'A0/W/scale=4'): + full.append(f'mode=6/reg=1/index=full/bs={bs}/is={ix_suppressed}/bd={bd}/iis={iis}/ix={index}/pointer-alias=False') +assert len(forms) == 34 and len(full) == 264 + +def add(keys, cohort, operands, banks, traces, weight, witness): + for form in operands: + for bank in banks: + for trace in traces: + for width in (2, 4): + for transfer in ([3] if witness else range(4)): + for byte in ([width - 1] if witness else range(width)): + key = f'68040/MOVEM/write-fault/{cohort}/size={width}/{form}/bank={bank}/T={trace}/transfer={transfer}/byte={byte}' + assert key not in keys + keys[key] = weight + +required, witness, alias, fault_free = {}, {}, {}, {} +add(required, 'recovery-opcode', forms, ['ISP'], ['0000'], 1, False) +add(required, 'recovery-status', canonical, ['user', 'user-M', 'ISP', 'MSP'], ['0000', '8000', '4000'], 32, False) +add(required, 'recovery-structure', full, ['ISP'], ['0000'], 1, False) +add(witness, 'recovery-witness', forms, ['ISP'], ['0000'], 1, True) +add(alias, 'pointer-alias', ['mode=6/reg=0/index=full/bs=False/is=False/bd=2/iis=1/ix=D1/L/scale=1/pointer-alias=True'], + ['user', 'user-M', 'ISP', 'MSP'], ['0000', '8000', '4000'], 1, True) +add(fault_free, 'fixture', forms, ['ISP'], ['0000'], 1, True) +# Fault-free controls choose byte zero; they do not arm a rejection. +fault_free = {k.rsplit('/byte=', 1)[0] + '/byte=0': v for k, v in fault_free.items()} +for group, keys in [('movem-write-recovery', required), ('movem-write-recovery-witness', witness), + ('movem-write-pointer-alias', alias), ('movem-write-fixture-control', fault_free)]: + for route in ('scalar', 'batch'): + inventories[f'{group}-{route}'] = keys +reports = {p.name for p in root.glob('68040-*.json')} +assert reports == {f'68040-{g}.json' for g in inventories} and set(summaries) == set(inventories) +records = [] +for group, keys in inventories.items(): + path = root / f'68040-{group}.json' + report = load(path) + count = sum(keys.values()) + assert report['schema'] == 1 and report['model'] == '68040' and report['group'] == group + assert report['logicalCases'] == count and report['xunitBatches'] == 1 and not report['failures'] + expected_counts = dict(passing=count, mismatching=0, unsupported=0, untested=0) + assert report['counts'] == expected_counts and set(report['combinations']) == set(keys) + assert all(report['combinations'][k] == {'passing': weight} for k, weight in keys.items()) + assert summaries[group] == (expected_counts, count, 1, len(keys)) + if args.baseline and group.startswith(('movem-write-recovery', 'movem-write-pointer-alias')): + assert report == load(args.baseline / path.name), ('retained report changed', group) + records.append(dict(path=path.name,sha256=sha(path),logicalCases=count,combinations=len(keys))) +assert sum(r['logicalCases'] for r in records) == 670664 +proof = dict(schema=1,executions=13,logicalCases=670664,reports=records, + inputSha256=sha(root/'inputs.json'),executionSha256=sha(root/'execution.json'), + verifierSha256=sha(Path(__file__)),retainedBaseline=str(args.baseline) if args.baseline else None, + publication=False,privateCandidateImport=False,hardwareQualified=False,roadmapComplete=False) +serialized = json.dumps(proof,indent=2)+'\n' +path = root/'proof.json' +if path.exists(): + assert path.read_text(encoding='utf-8') == serialized, 'Existing proof differs; preserve it' +else: + path.write_text(serialized,encoding='utf-8') +print('MOVEM mask fault qualification verified:', sha(path), '670664 programs / 13 executions / 12 reports') From 37dbb3abd7e5cb09169b8c5a08f85d0be6f4937f Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 09:27:46 +0300 Subject: [PATCH 147/211] Reproduce 040 MOVES physical fault frame gaps --- Copper68k.Tests/Synthetic/README.md | 21 +++ ...SyntheticM68040MovesFaultDiscoveryTests.cs | 134 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 81 +++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 24 ++++ scripts/test-copper68k-040-moves-faults.ps1 | 52 +++++++ scripts/verify-copper68k-040-moves-faults.py | 118 +++++++++++++++ 6 files changed, 430 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040MovesFaultDiscoveryTests.cs create mode 100644 scripts/test-copper68k-040-moves-faults.ps1 create mode 100644 scripts/verify-copper68k-040-moves-faults.py diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 88e88327..790f5077 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1358,3 +1358,24 @@ uses isolated build outputs, and requires all thirteen tests and twelve reports. instructions. `-BaselineDirectory` additionally requires unchanged retained recovery reports; a missing requested baseline fails. Neither mode promotes missing inputs, empty selections or mismatching/unsupported execution. + +The separate 040 MOVES physical-fault discovery is currently **failing**. Its +literal byte/word/long encodings use (A0), all eight SFC/DFC choices, four address +lanes, ISP/MSP and CCR 0/31. Each operand byte can trigger a physical-map rejection +on scalar/batch public-factory routes. All 1,536 fault-free controls pass; of 3,584 +fault cases, 3,136 mismatch. Writes incorrectly stack format 0; reads record +supervisor-data attributes except where those happen to match the selected FC. +The controls run by default; the two fault tests are unavailable unless requested. +They remain a discovery gate, separate from the qualified MOVES instruction matrix. + +```powershell +./scripts/test-copper68k-040-moves-faults.ps1 -OutputDirectory artifacts/moves-fault-discovery +``` + +This command intentionally returns failure while the defects remain. It freezes +the source graph, requires all four tests and exact inventories, and records a +failed verification proof. Missing inputs, empty selections and altered evidence +also fail. `-ValidateReportsOnly` executes no instructions; `-PythonCommand` +selects Python 3. Saved-PC policy, recovery/retry, special-space writebacks, +nested handler faults, enabled MMU/cache and hardware timing remain unqualified. +See the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-moves-physical-fault-discovery--2026-10-08). diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040MovesFaultDiscoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040MovesFaultDiscoveryTests.cs new file mode 100644 index 00000000..2c4720da --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040MovesFaultDiscoveryTests.cs @@ -0,0 +1,134 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 3.2.5/Table 3-2 and 8.4.6: physical MOVES operand faults +// require format 7 and converted TT/TM. No saved-PC, instruction completion, +// special-transfer writeback, handler restart or physical timing claim is made. +public sealed class SyntheticM68040MovesFaultDiscoveryTests(ITestOutputHelper output) +{ + internal const string Enable = "COPPER68K_RUN_040_MOVES_FAULT_DISCOVERY"; + private const uint Handler = 0x9020; + // Literal table rows, independent of any production FC conversion helper. + private static readonly ushort[] Attributes = [0x0010, 0x0001, 0x0001, 0x0013, 0x0014, 0x0005, 0x0005, 0x0017]; + + [Theory, Trait("Suite", "ReferenceDiscovery")] + [InlineData(false)] + [InlineData(true)] + public void CanonicalMovesFixturesPreserveFlagsAndOtherFunctionCode(bool batch) => Audit(batch, false); + + [EnvironmentFact(Enable, "discover physical MOVES operand fault frames"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarMovesFaultsRequireFormat7AndConvertedFunctionCodes() => Audit(false, true); + + [EnvironmentFact(Enable, "discover physical MOVES operand fault frames"), Trait("Suite", "ReferenceDiscovery")] + public void BatchMovesFaultsRequireFormat7AndConvertedFunctionCodes() => Audit(true, true); + + private void Audit(bool batch, bool fault) + { + var report = new CoverageBatch("68040", "moves-physical-fault-" + (fault ? "discovery-" : "fixture-") + (batch ? "batch" : "scalar")); + foreach (var width in new[] { 1, 2, 4 }) + foreach (var lane in new uint[] { 0, 1, 2, 3 }) + for (var fc = 0; fc < 8; fc++) + foreach (var write in new[] { false, true }) + foreach (var bank in new[] { "ISP", "MSP" }) + foreach (var ccr in new[] { 0, 31 }) + for (var faultByte = 0; faultByte < (fault ? width : 1); faultByte++) + { + // Fixed reference encodings: MOVES.B/W/L D0,(A0) or (A0),D0. + ushort opcode = width switch { 1 => 0x0e10, 2 => 0x0e50, _ => 0x0e90 }; + var id = $"68040/MOVES/physical-{(fault ? "fault" : "fixture")}/route={(batch ? "batch" : "scalar")}/size={width}/lane={lane}/FC={fc}/direction={(write ? "write" : "read")}/bank={bank}/ccr={ccr:X2}/byte={faultByte}/op={opcode:X4}"; + try + { + var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + m.Reset(ccr); m.Core.State.D[0] = 0x89abcdef; m.Core.State.A[0] = 0x4200 + lane; + for (uint n = 0x41fc; n < 0x420b; n++) m.InitializePhysical(n, 0x5a, 1); + m.InitializePhysical(0x4200 + lane, 0x12345678, width); + m.InitializePhysical(Handler, 0x4e71, 2); m.InitializePhysical(Handler + 2, 0x4e71, 2); + m.InitializePhysical(8, Handler, 4); + SyntheticExecution.Prepare(m, [opcode, (ushort)(write ? 0x0800 : 0x0000), 0x7e55, 0x60fe]); + var s = m.Core.State; + s.SetInterruptStackPointer(0x4700); s.SetMasterStackPointer(0x7400); + s.StatusRegister = (ushort)((bank == "MSP" ? 0x3700 : 0x2700) | ccr); + s.SourceFunctionCode = (uint)(write ? 7 - fc : fc); + s.DestinationFunctionCode = (uint)(write ? fc : 7 - fc); + var before = ArchitecturalExpectation.Capture(m); var sequence = s.ExceptionSequence; + if (fault) bus.Arm(0x4200 + lane + (uint)faultByte, + write ? M68kBusAccessKind.CpuDataWrite : M68kBusAccessKind.CpuDataRead); + if (batch) + { + if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, s.Cycles + 1000, new Boundary()) != 1) + throw new InvalidOperationException("Expected exactly one batch instruction boundary"); + } + else m.Core.ExecuteInstruction(); + if (!fault) + { + before.Pc = 0x1004; + if (write) before.Write(0x4200 + lane, 0x89abcdef, width, m.Model); + else before.D[0] = width switch { 1 => 0x89abcd78, 2 => 0x89ab5678, _ => 0x12345678 }; + if (before.Verify(m) is { } mismatch) throw new InvalidOperationException(mismatch); + if (s.ExceptionSequence != sequence || bus.Rejected.Count != 0) + throw new InvalidOperationException("Fault-free fixture generated an exception"); + var operand = bus.Accesses.Where(a => a.Address == 0x4200 + lane && a.Kind is + M68kBusAccessKind.CpuDataRead or M68kBusAccessKind.CpuDataWrite).ToArray(); + if (operand.Length != 1 || operand[0].Write != write || operand[0].Width != width) + throw new InvalidOperationException("Fixture operand width/direction/count differs"); + } + else + { + var stack = bank == "MSP" ? 0x7400u : 0x4700u; + var frame = stack - 60; + if (s.LastExceptionVector != 2 || s.ExceptionSequence != sequence + 1 || + s.ProgramCounter != Handler || s.Halted || s.Stopped || bus.Rejected.Count != 1 || + bus.Rejected[0].Address != 0x4200 + lane || bus.Rejected[0].Width != width) + throw new InvalidOperationException("Physical operand rejection did not enter exactly one access error"); + var actualFormat = bus.Peek(s.A[7] + 6, 2); + if (s.A[7] != frame || actualFormat != 0x7008) + throw new InvalidOperationException($"format 7 required: SP={s.A[7]:X8}, frame={actualFormat:X4}"); + var size = width switch { 1 => 0x20, 2 => 0x40, _ => 0 }; + var expectedSsw = Attributes[fc] | size | (write ? 0 : 0x0100); + if ((bus.Peek(frame + 12, 2) & 0xff7f) != expectedSsw) + throw new InvalidOperationException($"SSW TT/TM/size/direction expected {expectedSsw:X4}, actual {bus.Peek(frame + 12, 2):X4}"); + if (bus.Peek(frame, 2) != before.Sr || bus.Peek(frame + 20, 4) != 0x4200 + lane) + throw new InvalidOperationException("Saved SR or initial logical fault address differs"); + if (!s.D.SequenceEqual(before.D) || !s.A.Take(7).SequenceEqual(before.A.Take(7)) || + s.StatusRegister != before.Sr || s.UserStackPointer != before.InactiveStackPointer || + (bank == "MSP" ? s.InterruptStackPointer != 0x4700 : s.MasterStackPointer != 0x7400) || + s.M68040Mmu.BypassTranslation) + throw new InvalidOperationException("Fault changed registers, flags, inactive stacks or translation state"); + // For ordinary data-space writes only, 8.4.6.7 example 3 + // requires WB1 valid, FA=WB1A. Special-space writes are not + // promoted to a writeback expectation by this discovery. + if (write && fc is 1 or 2 or 5 or 6) + { + if (bus.Peek(frame + 18, 2) != (uint)(0x80 | size | Attributes[fc]) || + bus.Peek(frame + 40, 4) != 0x4200 + lane) + throw new InvalidOperationException("Normal physical write must preserve WB1 status/address"); + for (var n = 0; n < width; n++) + if (bus.Peek(frame + 44 + ((lane + (uint)n) & 3)) != (byte)(0x89abcdefu >> (8 * (width - 1 - n)))) + throw new InvalidOperationException("Memory-aligned WB1 data lane differs"); + } + if (bus.Accesses.Any(a => a.Address == 0x4200 + lane && a.Kind is + M68kBusAccessKind.CpuDataRead or M68kBusAccessKind.CpuDataWrite)) + throw new InvalidOperationException("Rejected operand was retried or completed implicitly"); + foreach (var at in before.Memory.Keys.Concat(bus.Memory.Keys).Distinct()) + if ((at < frame || at >= stack) && before.Memory.GetValueOrDefault(at) != bus.Peek(at)) + throw new InvalidOperationException($"Unrelated memory changed at {at:X8}"); + } + if (s.SourceFunctionCode != (uint)(write ? 7 - fc : fc) || + s.DestinationFunctionCode != (uint)(write ? fc : 7 - fc)) + throw new InvalidOperationException("MOVES changed SFC/DFC registers"); + report.Record(id, "passing", null); + } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + report.Complete(output); + } + + private sealed class Boundary : IM68kInstructionBoundary + { + public bool BeforeInstruction() => true; + public void AfterInstruction(long previousCycle, long currentCycle) { } + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 380e2ae6..2f0c4288 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -11618,3 +11618,84 @@ complete source/evidence linkage: `3f5d769639d7cc0678980fc24c0b5cf575c5f03c4d70b This slice does not establish every mask/fault-position Cartesian combination, other stack/CCR/EA combinations, enabled cache/MMU, physical timing or hardware trace behavior. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## 040 MOVES physical-fault discovery — 2026-10-08 + +The user confirms that no real 68030/68040 hardware is available. The existing +manual/software disagreement at a trace boundary remains explicitly unqualified; +absence of hardware evidence does not establish either software reference as an +architectural oracle. Software work continues on separately documented behavior. + +[MC68040UM 3.2.5/Table 3-2 and 8.4.6](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +defines format 7 for physical operand access errors and the MOVES SFC/DFC to +TT/TM conversion. Function codes 2/6 become data references 1/5; 0/3/4/7 use +transfer type 2 and retain their modifier. These literal table rows are independent +test expectations. The new discovery does not infer saved-PC or instruction +completion policy from the software reference's approximate writeback slot. +Normal data-space physical writes require WB1 valid, FA=WB1A and memory-aligned +WB1 data (8.4.6.5/7); special-space writeback fields are left unqualified. + +Fixed MOVES.B/W/L D0,(A0) and (A0),D0 encodings execute through the public 040 +factory on scalar and one-instruction batch routes. The bounded inventory crosses +three widths, four address lanes, all eight SFC/DFC choices, both directions, +ISP/MSP and initial CCR 0/31. Each byte in the original operand can be rejected +by the physical address map. Expected fault fields include original SR, format, +SSW transfer attributes, logical FA and normal-write WB1; registers, inactive +stacks, surrounding memory and absence of implicit operand retry are checked. +Saved PC, unknown frame fields, trace and actual handler execution are excluded. + +| Cohort | Cases per route | Passing per route | Mismatching per route | +| --- | ---: | ---: | ---: | +| Fault-free encodings | 768 | 768 | 0 | +| Physical operand faults | 1,792 | 224 | 1,568 | + +Each write faults into an eight-byte format-zero frame, rather than the required +60-byte format-seven frame: **896 mismatches per route**. Reads do create format +7, but use supervisor-data TT/TM regardless of SFC: **672 mismatches per route**. +Only FC 5/6 reads match the converted attributes (**224 passing per route**). +These exact memberships and diagnostic categories are independently enumerated. +There are zero unsupported/untested cases in the requested bounded inventory; +the **3,136 mismatches among 5,120 total cases keep qualification failed**. +Later checks behind each first mismatch do not thereby become verified. + +A first frozen discovery and two maintained-command snapshots produce identical +four reports. Each actual CPU invocation returns exit 1, with two passing fixture +tests and two failing fault tests; the verifier also returns 1 and records status +`failed`. All 216 preceding compiler inputs are byte-identical; the sole added +input is this discovery test, yielding 217. Production CPU inputs remain unchanged. +The same frozen assembly separately passes 18 MOVES fixture/semantic/invalid-EA +tests, with the two unrequested fault discoveries unavailable. No current wide +full-suite or consumer rerun is claimed for this test-only slice. + +The maintained command requires PowerShell 7/Python 3 and a fresh output: + +```powershell +./scripts/test-copper68k-040-moves-faults.ps1 -OutputDirectory artifacts/moves-fault-discovery +``` + +Its verifier binds source, producer, command/settings, assemblies, TRX execution +identities, complete architectural keys, counts and per-case failures. Requested +missing/empty selections fail. Seven copied-evidence controls reject missing +reports, empty test selection, changed combination weight, wrong FC key, changed +source, wrong producer and changed assembly. A complete relocated fixture remains +**failed**, retaining all 3,136 mismatches. The original evidence is hash-checked +unchanged after every control. An initial relocation helper failed because TRX +storage paths were lowercased; that attempt is retained, and its successor fixes +only owned fixture relocation. No CPU result or expectation is altered. + +Local evidence under `%TEMP%/copper68k-reference-restoration-20261006/`: +`audits/MovesFaultDiscoveryV1`, `audits/MovesFaultMaintainedV1`, +`audits/MovesFaultMaintainedV2` and `moves-gap-controls-v2`. +Maintained failed-gate proof SHA-256: +`198a8a8666ff402f87a8ff2964e4d9d5c5119084be826d2d498a5ef653b0c9ad`; +exact-gap/integrity-control proof: +`87bd4b9707267142e8b82fbac1fd95216d6bd9a337da3761ce63abcf9a0aa672`. + +Next: carry the selected alternate-space attributes on the actual MOVES operand +fault, distinguish EA pointer faults from the MOVES transfer, and qualify store +completion/saved-PC/writeback return before introducing a recovery policy. +Never rerun an instruction automatically after partial operand side effects. +Auto addressing/aliases, nested WB2/WB3 handler faults, trace, enabled MMU/cache, +physical partial transfers and timing remain outside this bounded discovery. +No CPU fix, regression retirement, package publication or private candidate import +occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index a79b989a..b70ca5a7 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1922,6 +1922,30 @@ provenance, internal restoration, data/writeback/context transfer, other models' restoration and broader independent qualification/consolidation remain required. Milestone 6 remains **in progress**, `roadmapComplete=false`. +### Milestone 6 MOVES physical-fault discovery — 2026-10-08 + +No 030/040 hardware is available; the manual/software trace disagreement remains +unqualified. Continue independent software qualification of defined behavior. +The bounded 040 MOVES discovery uses fixed B/W/L D0/(A0) encodings, all eight +SFC/DFC choices and independently literal MC68040UM Table 3-2 attributes. +Scalar/batch routes, four address lanes, ISP/MSP, CCR 0/31 and rejection at each +operand byte execute **5,120 cases** including **1,536 passing fault-free controls**. +Of the **3,584 fault cases**, **3,136 mismatch**: writes produce format 0 and most +reads use supervisor-data attributes. Each route passes only FC 5/6 reads. +The discovery gate remains **failed**, not a completed fault/recovery promotion. + +Two maintained snapshots reproduce the first frozen discovery reports exactly; +an independent verifier binds the full input/execution/architectural inventories. +Seven evidence corruptions reject, while a complete copied fixture retains all +3,136 mismatches. Existing MOVES semantics and invalid-operand tests pass separately. +Production CPU sources are unchanged; no wide full-suite/consumer rerun is claimed. +See [the qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040-moves-physical-fault-discovery--2026-10-08). +Next work must qualify alternate-space operand provenance and store completion, +saved PC and actual writeback recovery; it must not retry a partially executed +instruction. EA pointer faults, alias/auto modes, trace, nested handlers, enabled +MMU/cache and physical timing remain unqualified. No package or retirement changes +occur. Milestone 6 remains **in progress**, `roadmapComplete=false`. + On 2026-10-07 the executed 040 MOVEM read-recovery gate passes 229,968 complete diff --git a/scripts/test-copper68k-040-moves-faults.ps1 b/scripts/test-copper68k-040-moves-faults.ps1 new file mode 100644 index 00000000..b1d58f64 --- /dev/null +++ b/scripts/test-copper68k-040-moves-faults.ps1 @@ -0,0 +1,52 @@ +#requires -Version 7.0 +[CmdletBinding()] +param( + [string]$OutputDirectory='artifacts/040-moves-faults', + [switch]$ValidateReportsOnly, + [string]$PythonCommand='python' +) +$ErrorActionPreference='Stop' +$repo=Split-Path -Parent $PSScriptRoot +$output=[IO.Path]::GetFullPath($OutputDirectory,$repo) +function Hash([string]$Path) { (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() } +if (-not $ValidateReportsOnly) { + if (Test-Path -LiteralPath $output) { throw "Use a fresh output directory: $output" } + $sourceRoot=Join-Path $output 'source' + New-Item -ItemType Directory -Path $sourceRoot | Out-Null + $sources=@(@(& git -C $repo ls-files 'Copper68k/*.cs' 'Copper68k/*.csproj' 'Copper68k.Tests/*.cs' 'Copper68k.Tests/*.csproj') + + @('Copper68k.Tests/Synthetic/SyntheticM68040MovesFaultDiscoveryTests.cs') | Sort-Object -Unique) + if ($sources.Count -eq 0) { throw 'Empty source selection' } + $identities=@{} + foreach ($name in $sources) { + $from=Join-Path $repo $name; $to=Join-Path $sourceRoot $name + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $to) | Out-Null + Copy-Item -LiteralPath $from -Destination $to + $identities[$name]=Hash $to + if ((Hash $from) -ne $identities[$name]) { throw "Source changed while freezing: $name" } + } + $protected=@(@('Copper68k/bin/Release/net10.0/Copper68k.dll','Copper68k.Tests/bin/Release/net10.0/Copper68k.Tests.dll') | + Where-Object { Test-Path -LiteralPath (Join-Path $repo $_) } | + ForEach-Object { @{path=$_;sha256=(Hash (Join-Path $repo $_))} }) + $command=@('dotnet','test',(Join-Path $sourceRoot 'Copper68k.Tests/Copper68k.Tests.csproj'),'-c','Release', + '--artifacts-path',(Join-Path $output 'build'),'--filter','FullyQualifiedName~SyntheticM68040MovesFaultDiscoveryTests', + '--logger','trx;LogFileName=audit.trx','--results-directory',$output) + $settings=@{COPPER68K_SYNTHETIC_REPORT_DIR=$output;COPPER68K_RUN_040_MOVES_FAULT_DISCOVERY='1'} + @{sourceCommit=(& git -C $repo rev-parse HEAD);sources=$identities;command=$command;settings=$settings; + producerSha256=(Hash $PSCommandPath);protected=$protected;publication=$false;privateCandidateImport=$false} | + ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'inputs.json') -Encoding utf8 + $saved=@{} + try { + foreach ($key in $settings.Keys) { $saved[$key]=[Environment]::GetEnvironmentVariable($key); [Environment]::SetEnvironmentVariable($key,$settings[$key]) } + $arguments=$command[1..($command.Count-1)] + & dotnet @arguments *> (Join-Path $output 'execution.log') + $testExit=$LASTEXITCODE + } finally { + foreach ($key in $saved.Keys) { [Environment]::SetEnvironmentVariable($key,$saved[$key]) } + } + @{exit=$testExit;inputsSha256=(Hash (Join-Path $output 'inputs.json'));logSha256=(Hash (Join-Path $output 'execution.log')); + trxSha256=(Hash (Join-Path $output 'audit.trx'));cpuSha256=(Hash (Join-Path $output 'build/bin/Copper68k/release/Copper68k.dll')); + testSha256=(Hash (Join-Path $output 'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'))} | + ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'execution.json') -Encoding utf8 +} +& $PythonCommand (Join-Path $PSScriptRoot 'verify-copper68k-040-moves-faults.py') --repo $repo --output $output --producer $PSCommandPath +if ($LASTEXITCODE -ne 0) { throw "040 MOVES fault qualification failed; preserve evidence: $output" } diff --git a/scripts/verify-copper68k-040-moves-faults.py b/scripts/verify-copper68k-040-moves-faults.py new file mode 100644 index 00000000..3a74aff7 --- /dev/null +++ b/scripts/verify-copper68k-040-moves-faults.py @@ -0,0 +1,118 @@ +"""Require complete, frozen MOVES fault evidence; mismatches keep the gate failed.""" +from pathlib import Path +import argparse, hashlib, json, subprocess, xml.etree.ElementTree as ET + +p = argparse.ArgumentParser() +p.add_argument('--repo', type=Path, required=True) +p.add_argument('--output', type=Path, required=True) +p.add_argument('--producer', type=Path, required=True) +a = p.parse_args() +r = a.output.resolve() + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + +def load(path): + return json.loads(path.read_text(encoding='utf-8-sig')) + +i, e = load(r / 'inputs.json'), load(r / 'execution.json') +assert not i['publication'] and not i['privateCandidateImport'] +assert i['producerSha256'] == sha(a.producer), 'Changed producer' +assert e['inputsSha256'] == sha(r / 'inputs.json'), 'Changed inputs' +names = set(subprocess.check_output(['git', 'ls-files', 'Copper68k/*.cs', 'Copper68k/*.csproj', + 'Copper68k.Tests/*.cs', 'Copper68k.Tests/*.csproj'], cwd=a.repo, text=True).splitlines()) +names.add('Copper68k.Tests/Synthetic/SyntheticM68040MovesFaultDiscoveryTests.cs') +assert names and set(i['sources']) == names, 'Incomplete source selection' +for name, digest in i['sources'].items(): + assert sha(a.repo / name) == sha(r / 'source' / name) == digest, 'Changed source: ' + name +for item in i.get('protected', []): + assert sha(a.repo / item['path']) == item['sha256'], 'Normal output changed' +assert i['settings'] == {'COPPER68K_SYNTHETIC_REPORT_DIR': str(r), 'COPPER68K_RUN_040_MOVES_FAULT_DISCOVERY': '1'} +assert i['command'] == ['dotnet', 'test', str(r / 'source/Copper68k.Tests/Copper68k.Tests.csproj'), + '-c', 'Release', '--artifacts-path', str(r / 'build'), '--filter', + 'FullyQualifiedName~SyntheticM68040MovesFaultDiscoveryTests', '--logger', + 'trx;LogFileName=audit.trx', '--results-directory', str(r)], 'Wrong execution command' +for key, name in [('logSha256', 'execution.log'), ('trxSha256', 'audit.trx'), + ('cpuSha256', 'build/bin/Copper68k/release/Copper68k.dll'), + ('testSha256', 'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll')]: + assert e[key] == sha(r / name), 'Changed ' + key + +ns = {'t': 'http://microsoft.com/schemas/VisualStudio/TeamTest/2010'} +tree = ET.parse(r / 'audit.trx') +rows = tree.findall('.//t:UnitTestResult', ns) +prefix = 'Copper68k.Tests.Synthetic.SyntheticM68040MovesFaultDiscoveryTests.' +expected_tests = {prefix + 'ScalarMovesFaultsRequireFormat7AndConvertedFunctionCodes', + prefix + 'BatchMovesFaultsRequireFormat7AndConvertedFunctionCodes'} +expected_tests.update(prefix + f'CanonicalMovesFixturesPreserveFlagsAndOtherFunctionCode(batch: {b})' for b in ('False', 'True')) +assert len(rows) == 4 and {x.attrib['testName'] for x in rows} == expected_tests, 'Missing/empty test selection' +assert len({x.attrib['testId'] for x in rows}) == 4 and len({x.attrib['executionId'] for x in rows}) == 4 +assert all(x.attrib['outcome'] in ('Passed', 'Failed') for x in rows), 'Unavailable requested test' +definitions = {x.attrib['id']: x for x in tree.findall('.//t:UnitTest', ns)} +for row in rows: + definition = definitions[row.attrib['testId']] + assert Path(definition.attrib['storage']).resolve() == (r / 'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll').resolve() + assert definition.find('t:Execution', ns).attrib['id'] == row.attrib['executionId'] +counters = tree.find('.//t:ResultSummary/t:Counters', ns).attrib +failed_tests = sum(x.attrib['outcome'] == 'Failed' for x in rows) +assert all(int(counters[k]) == v for k, v in {'total': 4, 'executed': 4, 'passed': 4-failed_tests, + 'failed': failed_tests, 'notExecuted': 0}.items()), 'TRX counters differ' +assert e['exit'] == (1 if failed_tests else 0), 'Recorded execution outcome differs' + +reports = {} +total_failures = 0 +for route in ('scalar', 'batch'): + for fault in (False, True): + group = 'moves-physical-fault-' + ('discovery-' if fault else 'fixture-') + route + path = r / ('68040-' + group + '.json') + report = load(path) + assert report['schema'] == 1 and report['model'] == '68040' and report['group'] == group + expected = set() + opcodes = {1: '0E10', 2: '0E50', 4: '0E90'} + full_ids = {} + for width in (1, 2, 4): + for lane in range(4): + for fc in range(8): + for direction in ('read', 'write'): + for bank in ('ISP', 'MSP'): + for ccr in ('00', '1F'): + for byte in range(width if fault else 1): + key = (f'68040/MOVES/physical-{"fault" if fault else "fixture"}/route={route}' + f'/size={width}/lane={lane}/FC={fc}/direction={direction}/bank={bank}/ccr={ccr}/byte={byte}') + expected.add(key); full_ids[key + '/op=' + opcodes[width]] = key + assert len(expected) == (1792 if fault else 768) + assert set(report['combinations']) == expected, 'Missing/extra architectural combination' + counts = dict.fromkeys(('passing', 'mismatching', 'unsupported', 'untested'), 0) + failing = set() + for key, statuses in report['combinations'].items(): + assert set(statuses) <= set(counts) and sum(statuses.values()) == 1, 'Wrong combination weight/status' + assert all(isinstance(v, int) and v > 0 for v in statuses.values()) + for status, value in statuses.items(): + counts[status] += value + if status != 'passing': failing.add(key) + assert counts == report['counts'] and sum(counts.values()) == report['logicalCases'] == len(expected), 'Report counts differ' + assert report['xunitBatches'] == 1 + failures = report['failures'] + assert len(failures) == len(failing) and len({x['id'] for x in failures}) == len(failing) + assert {full_ids[x['id']] for x in failures} == failing + assert all(x['reason'] and x['status'] != 'passing' and + report['combinations'][full_ids[x['id']]].get(x['status']) == 1 for x in failures) + method = (('Scalar' if route == 'scalar' else 'Batch') + 'MovesFaultsRequireFormat7AndConvertedFunctionCodes' + if fault else f'CanonicalMovesFixturesPreserveFlagsAndOtherFunctionCode(batch: {"False" if route == "scalar" else "True"})') + row = next(x for x in rows if x.attrib['testName'] == prefix + method) + assert row.attrib['outcome'] == ('Failed' if failing else 'Passed'), 'Report/TRX outcome differs' + total_failures += len(failing) + reports[path.name] = {'sha256': sha(path), 'counts': counts, 'logicalCases': len(expected)} +assert {x.name for x in r.glob('68040-moves-physical-fault-*.json')} == set(reports) +proof = {'schema': 1, 'status': 'failed' if total_failures else 'passing', + 'inputsSha256': sha(r / 'inputs.json'), 'executionSha256': sha(r / 'execution.json'), + 'verifierSha256': sha(Path(__file__)), 'reports': reports, 'logicalCases': 5120, + 'mismatchingOrUnavailable': total_failures, 'savedPcQualification': False, + 'recoveryQualification': False, 'hardwareQualification': False, + 'publication': False, 'privateCandidateImport': False, 'roadmapComplete': False} +target = r / 'verification.json' +if target.exists(): + assert load(target) == proof, 'Existing proof differs; preserve it' +else: + target.write_text(json.dumps(proof, indent=2)+'\n', encoding='utf-8') +print(f'MOVES fault gate: {proof["status"]}; 5120 cases, {total_failures} mismatching/unavailable') +raise SystemExit(1 if total_failures else 0) From d960ff2c1bbf5f859aadf0ba86d2580ab629fa82 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 10:22:50 +0300 Subject: [PATCH 148/211] Correct 040 MOVES read fault transfer attributes --- Copper68k.Tests/Synthetic/README.md | 24 +++- .../SyntheticM68040MovesReadFaultTests.cs | 123 ++++++++++++++++++ Copper68k/M68040Support.cs | 14 +- .../M68kAdvancedTimingInterpreter.System.cs | 12 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 110 ++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 30 +++++ scripts/test-copper68k-synthetic.ps1 | 2 + 7 files changed, 309 insertions(+), 6 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040MovesReadFaultTests.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 790f5077..5b0a68e0 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1363,8 +1363,9 @@ The separate 040 MOVES physical-fault discovery is currently **failing**. Its literal byte/word/long encodings use (A0), all eight SFC/DFC choices, four address lanes, ISP/MSP and CCR 0/31. Each operand byte can trigger a physical-map rejection on scalar/batch public-factory routes. All 1,536 fault-free controls pass; of 3,584 -fault cases, 3,136 mismatch. Writes incorrectly stack format 0; reads record -supervisor-data attributes except where those happen to match the selected FC. +fault cases, the original captured CPU had 3,136 mismatches. After the read +attribute fix, all read cases pass; 1,792 write cases still incorrectly stack +format 0. The original failed reports remain historical evidence. The controls run by default; the two fault tests are unavailable unless requested. They remain a discovery gate, separate from the qualified MOVES instruction matrix. @@ -1379,3 +1380,22 @@ also fail. `-ValidateReportsOnly` executes no instructions; `-PythonCommand` selects Python 3. Saved-PC policy, recovery/retry, special-space writebacks, nested handler faults, enabled MMU/cache and hardware timing remain unqualified. See the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-moves-physical-fault-discovery--2026-10-08). + +The ordinary gate now requires 46,976 `moves-read-fault-provenance-scalar` and +46,976 `moves-read-fault-provenance-batch` cases. The new matrix checks all SFC +table rows across nine captured legal EA forms, widths/lanes, ISP/MSP, selected +CCR states and each rejected operand byte. Separate all-CCR indirect cases, +pre/post indexed pointer faults and extension-fetch faults verify that only +the MOVES operand acquires alternate-space attributes. Store-side EA pointer +and extension faults retain ordinary read/instruction attributes. The unchanged +CPU fails 33,600 of these cases; the fix passes all 93,952. Three owned mutations +detect raw FC encoding, selecting DFC and inferring MOVES provenance from the +opcode at the frame builder. Saved opcode PC/auto-address effects preserve the +existing execution policy; handler return and physical pipeline behavior remain +unqualified. See [the read-fault record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-moves-read-fault-provenance--2026-10-08). +The complete 218-input CPU audit passes 5,318 tests with 53 unavailable and no +failures; independent verification binds 86,699,178 cases, 943 profile reports +and ten native presets. The current ordinary gate passes 86,506,698 cases / 743 +batches. The same CPU source inputs pass isolated local `.75` NuGet consumer +build/tests and two native floppy replays. The remaining write discovery stays +failed and Milestone 6 remains in progress. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040MovesReadFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040MovesReadFaultTests.cs new file mode 100644 index 00000000..b973fce7 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040MovesReadFaultTests.cs @@ -0,0 +1,123 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM Table 3-2, 8.4.6: only the MOVES operand uses alternate TT/TM. +// Physical-map rejection, caches/MMU disabled. Saved opcode PC and retained +// auto-address effects describe the existing synchronous execution policy; +// handler return, store completion and physical pipeline timing are not qualified. +public sealed class SyntheticM68040MovesReadFaultTests(ITestOutputHelper output) +{ + private const uint Handler = 0x9020; + private static readonly ushort[] Attributes = [0x10, 1, 1, 0x13, 0x14, 5, 5, 0x17]; + private static readonly string[] Forms = ["indirect", "post", "pre", "displacement", "brief", "absolute-word", "absolute-long", "full-pre", "full-post"]; + + [Theory, Trait("Suite", "Synthetic")] + [InlineData(false)] + [InlineData(true)] + public void PhysicalReadFaultAttributesBelongOnlyToTheMovesOperand(bool batch) + { + var report = new CoverageBatch("68040", "moves-read-fault-provenance-" + (batch ? "batch" : "scalar")); + foreach (var form in Forms) RunCohort("operand", form, "operand", [0, 31], false); + RunCohort("ccr", "indirect", "operand", Enumerable.Range(0, 32), false); + foreach (var form in new[] { "full-pre", "full-post" }) + foreach (var store in new[] { false, true }) RunCohort("pointer", form, "pointer", [0, 31], store); + foreach (var form in Forms.Skip(3)) + foreach (var store in new[] { false, true }) RunCohort("extension", form, "extension", [0, 31], store); + report.Complete(output); + + void RunCohort(string cohort, string form, string stage, IEnumerable ccrs, bool store) + { + foreach (var width in new[] { 1, 2, 4 }) + foreach (var lane in new uint[] { 0, 1, 2, 3 }) + for (var fc = 0; fc < 8; fc++) + foreach (var bank in new[] { "ISP", "MSP" }) + foreach (var ccr in ccrs) + for (var rejectedByte = 0; rejectedByte < (stage == "operand" ? width : 4); rejectedByte++) + Case(report, batch, cohort, form, stage, width, lane, fc, bank, ccr, rejectedByte, store); + } + } + + private static void Case(CoverageBatch report, bool batch, string cohort, string form, string stage, + int width, uint lane, int fc, string bank, int ccr, int rejectedByte, bool store) + { + var id = $"68040/MOVES/read-fault/{cohort}/route={(batch ? "batch" : "scalar")}/form={form}/stage={stage}/size={width}/lane={lane}/FC={fc}/bank={bank}/ccr={ccr:X2}/byte={rejectedByte}/store={store}"; + try + { + var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + m.Reset(ccr); var s = m.Core.State; + var target = (form == "absolute-long" ? 0x12344200u : 0x4200u) + lane; + s.A[0] = form switch { "pre" => target + (uint)width, "displacement" => target - 16, + "brief" => target - 32, "full-pre" or "full-post" => 0x4000 + lane, _ => target }; + s.D[1] = 32; + // Fixed legal opcode and extension shapes. Literal full-format + // pre/post indexed examples are 1921/1925 with word BD=0010. + var ea = form switch { "indirect" => 0x10, "post" => 0x18, "pre" => 0x20, + "displacement" => 0x28, "brief" or "full-pre" or "full-post" => 0x30, + "absolute-word" => 0x38, _ => 0x39 }; + var opcode = (ushort)((width == 1 ? 0x0e00 : width == 2 ? 0x0e40 : 0x0e80) | ea); + var words = new List { opcode, (ushort)(store ? 0x0800 : 0) }; + if (form == "displacement") words.Add(0x0010); + else if (form == "brief") words.Add(0x1800); + else if (form == "absolute-word") words.Add((ushort)target); + else if (form == "absolute-long") { words.Add((ushort)(target >> 16)); words.Add((ushort)target); } + else if (form is "full-pre" or "full-post") { words.Add((ushort)(form == "full-pre" ? 0x1921 : 0x1925)); words.Add(0x0010); } + var pointer = (form == "full-pre" ? 0x4030u : 0x4010u) + lane; + if (form is "full-pre" or "full-post") m.InitializePhysical(pointer, form == "full-pre" ? target : target - 32, 4); + m.InitializePhysical(target, 0x12345678, width); + m.InitializePhysical(Handler, 0x4e71, 2); m.InitializePhysical(Handler + 2, 0x4e71, 2); + m.InitializePhysical(8, Handler, 4); + _ = SyntheticExecution.Prepare(m, words); + s.SetInterruptStackPointer(0x4700); s.SetMasterStackPointer(0x7400); + s.StatusRegister = (ushort)((bank == "MSP" ? 0x3700 : 0x2700) | ccr); + s.SourceFunctionCode = (uint)fc; s.DestinationFunctionCode = (uint)(7 - fc); + var e = ArchitecturalExpectation.Capture(m); var sequence = s.ExceptionSequence; + var stack = bank == "MSP" ? 0x7400u : 0x4700u; var frame = stack - 60; + var faultAddress = stage == "extension" ? 0x1004u : stage == "pointer" ? pointer : target; + var faultWidth = stage == "operand" ? width : 4; + if (stage == "operand" && form is "post" or "pre") e.A[0] = form == "post" ? target + (uint)width : target; + e.A[7] = frame; if (bank == "MSP") e.MasterStackPointer = frame; + e.Pc = Handler; e.ExceptionVector = 2; + e.ControlChecks["exception sequence"] = (x => x.ExceptionSequence, sequence + 1); + e.ControlChecks["SFC"] = (x => x.SourceFunctionCode, (uint)fc); + e.ControlChecks["DFC"] = (x => x.DestinationFunctionCode, (uint)(7 - fc)); + e.ControlChecks["synchronous saved opcode PC"] = (x => x.LastExceptionStackedProgramCounter, 0x1000); + e.Write(frame, e.Sr, 2, m.Model); e.Write(frame + 2, 0x1000, 4, m.Model); e.Write(frame + 6, 0x7008, 2, m.Model); + for (uint n = 8; n < 60; n++) e.MemoryMasks[frame + n] = 0; + var attribute = stage == "operand" ? Attributes[fc] : stage == "extension" ? 6 : 5; + var size = faultWidth == 1 ? 0x20 : faultWidth == 2 ? 0x40 : 0; + e.Write(frame + 12, (uint)(0x0100 | size | attribute), 2, m.Model); + e.MemoryMasks[frame + 12] = 255; e.MemoryMasks[frame + 13] = 0x7f; // X undefined. + foreach (var at in new uint[] { 14, 16, 18 }) { e.Write(frame + at, 0, 2, m.Model); e.MemoryMasks[frame + at + 1] = 0x80; } + e.Write(frame + 20, faultAddress, 4, m.Model); + for (uint n = 20; n < 24; n++) e.MemoryMasks[frame + n] = 255; + var kind = stage == "extension" ? M68kBusAccessKind.CpuInstructionFetch : M68kBusAccessKind.CpuDataRead; + bus.Arm(faultAddress + (uint)rejectedByte, kind); + if (batch) + { + if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, s.Cycles + 1000, new Boundary()) != 1) + throw new InvalidOperationException("Expected one batch instruction boundary"); + } + else m.Core.ExecuteInstruction(); + if (e.Verify(m) is { } mismatch) throw new InvalidOperationException(mismatch); + if (s.M68040Mmu.BypassTranslation || bus.Rejected.Count != 1 || bus.Rejected[0].Address != faultAddress || + bus.Rejected[0].Width != faultWidth || bus.Rejected[0].Kind != kind) + throw new InvalidOperationException("Rejection provenance/width or bypass state differs"); + if (bus.Accesses.Any(x => x.Address == target && x.Kind is M68kBusAccessKind.CpuDataRead or M68kBusAccessKind.CpuDataWrite)) + throw new InvalidOperationException("MOVES operand completed or was retried after rejection"); + if (bus.Accesses.Count(x => x.Address == pointer && x.Kind == M68kBusAccessKind.CpuDataRead) != + (stage == "operand" && form is "full-pre" or "full-post" ? 1 : 0)) + throw new InvalidOperationException("Pointer was repeated or accessed before its extension fault"); + report.Record(id + $"/op={opcode:X4}", "passing", null); + } + catch (Exception ex) { report.Record(id + "/op=fixture", "mismatching", ex.Message); } + } + + private sealed class Boundary : IM68kInstructionBoundary + { + public bool BeforeInstruction() => true; + public void AfterInstruction(long previousCycle, long currentCycle) { } + } +} diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index 4db8842b..93f59a89 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -1865,7 +1865,8 @@ internal readonly record struct M68040MmuFault( uint? WriteValue = null, bool CompletedMoveWrite = false, bool UnbufferedOperandRead = false, - uint? MovemEffectiveAddress = null); + uint? MovemEffectiveAddress = null, + byte? MovesFunctionCode = null); internal sealed class UnsupportedM68040InstructionException : M68kEmulationException { @@ -4398,12 +4399,19 @@ private void RaiseUnbufferedReadAccessFault(M68040MmuFault fault, uint instructi State.StatusRegister = (ushort)((savedSr | M68kCpuState.Supervisor) & ~0xc000); // No pending pipeline writebacks in this synchronous read path. // Undefined EA/writeback/push data use zero as an implementation - // convention, not a hardware-defined result. ATC/MA/LK/TT are clear; - // SSW SIZE describes the original read, TM its data function code. + // convention, not a hardware-defined result. ATC/MA/LK are clear; + // SSW SIZE describes the original read. MOVES carries the selected + // alternate-space FC only for its operand, not EA pointer reads. for (var n = 0; n < 9; n++) PushLong(0); PushLong(fault.LogicalAddress); PushWord(0); PushWord(0); PushWord(0); var modifier = ((savedSr & M68kCpuState.Supervisor) != 0 ? 4 : 0) | (instruction ? 2 : 1); + if (!instruction && fault.MovesFunctionCode is byte fc) + { + // MC68040UM Table 3-2: instruction spaces are merged into data; + // the other alternate spaces retain TM and use special TT=2. + modifier = (fc & 3) is 0 or 3 ? 0x10 | fc : (fc & 4) | 1; + } var size = fault.ByteCount == 1 ? 0x20 : fault.ByteCount == 2 ? 0x40 : 0; PushWord((ushort)(0x0100 | size | modifier | (fault.MovemEffectiveAddress.HasValue ? 0x1000 : 0))); PushLong(fault.MovemEffectiveAddress ?? 0); diff --git a/Copper68k/M68kAdvancedTimingInterpreter.System.cs b/Copper68k/M68kAdvancedTimingInterpreter.System.cs index d5276cbc..e63cf2df 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.System.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.System.cs @@ -86,7 +86,17 @@ private bool TryExecuteGeneralSystem(ushort opcode) if (store) WriteSized(address, ReadGeneralRegister(ar, general), size); else { - var value = ReadSized(address, size); + uint value; + try { value = ReadSized(address, size); } + catch (M68040MmuFaultException ex) when (!State.M68040Mmu.Enabled && + !ex.Fault.Write && ex.Fault.AccessKind == M68kBusAccessKind.CpuDataRead) + { + // Only the actual MOVES operand uses SFC. Address calculation + // (including indirect pointers) and extension fetches precede + // this scope. Carry provenance without retrying any access. + throw new M68040MmuFaultException(ex.Fault with + { MovesFunctionCode = (byte)(State.SourceFunctionCode & 7) }); + } if (ar) WriteGeneralRegister(true, general, M68kCpuState.SignExtend(value, size)); else WriteDataRegisterSized(general, value, size); } diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 2f0c4288..b20766b3 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -11699,3 +11699,113 @@ Auto addressing/aliases, nested WB2/WB3 handler faults, trace, enabled MMU/cache physical partial transfers and timing remain outside this bounded discovery. No CPU fix, regression retirement, package publication or private candidate import occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## 040 MOVES read-fault provenance — 2026-10-08 + +The captured MOVES discovery above records the unchanged CPU at `37dbb3a`; it +remains failed historical evidence. Its read defect is now reproduced independently +and fixed at the actual operand-read boundary. A physical-map read rejection +carries the selected SFC in an internal fault record. Extension consumption and +effective-address pointer reads happen before that scope and cannot acquire SFC. +No access is retried, no public API changes, and enabled MMU behavior is unchanged. +The format-seven read builder converts that latched SFC using +[MC68040UM Table 3-2](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf): +1/2 map to normal user-data TM 1, 5/6 to supervisor-data TM 5, and 0/3/4/7 +retain TM with special transfer type 2. Ordinary read/fetch frame attributes and +existing saved-PC/auto-address effects are preserved. + +The new test encodes nine legal forms independently: indirect, postincrement, +predecrement, displacement, brief index, absolute word/long and full-format +pre/post memory indirection with word base displacement. Literal examples use +extensions `1921/1925`, independent pointer locations and values. Both public +040 scalar/batch routes cover the following separately ordered cohorts: + +| Cohort | Cases per route | Scope | +| --- | ---: | --- | +| Operand | 8,064 | Nine forms, B/W/L, four lanes, eight SFC values, ISP/MSP, CCR 0/31, every rejected operand byte | +| CCR | 14,336 | All 32 CCR values for indirect operands, remaining dimensions retained | +| Pointer | 6,144 | Full pre/post indirect pointers, load/store, every rejected long-read byte; ordinary TM 5 | +| Extension | 18,432 | Six forms with EA extensions, load/store, every rejected prefetch byte; ordinary TM 6 | + +Registers, active/inactive stacks, defined SR/SSW/frame words, memory canaries, +single fault provenance and no operand/pointer retry are checked. Saved opcode +PC and retained pre/post address effects assert the existing synchronous +execution policy, **not hardware pipeline or handler-return qualification**. +No following RTE or writeback recovery is claimed. The expected attributes are +literal manual table rows, not calls into production decoding or conversion. + +The unchanged 37-input CPU with the new test executes the identical 218-input +graph except for the two production fixes: **33,600 mismatches**, exactly operand +SFC values other than 5/6. Every pointer and extension case already passes. +The fixed CPU passes **93,952 read/provenance cases**, plus **1,536 unchanged +fault-free MOVES controls**. All 46,976 combination keys per route are independently +enumerated. The existing report limits failure diagnostics to 10,000 per batch; +the before-run has 16,800 mismatches per route, so diagnostic samples are capped +while every passing/mismatching combination remains recorded and verified. + +| Owned CPU mutation | Mismatches per route | Preserved passing per route | +| --- | ---: | ---: | +| Store raw FC without Table 3-2 conversion | 16,800 | 30,176 | +| Select DFC instead of SFC | 22,400 | 24,576 | +| Infer MOVES attributes from the opcode at frame construction | 4,608 | 42,368 | + +Each mutation changes exactly one CPU input in its owned copy. Independent +enumeration predicts all affected FC/stage memberships, counts, diagnostics +(subject to the existing cap) and unchanged combinations. The third mutation +demonstrates why a MOVES opcode alone must not relabel indirect pointer faults. +Original focused source bytes remain unchanged. No old regression is retired. + +The maintained discovery command separately executes all 5,120 original cases +with the fixed CPU. All **1,792 read faults and 1,536 fixtures pass**. Its remaining +**1,792 write-frame mismatches** keep the discovery gate **failed**. Special-space +writeback fields, saved-PC/store completion, nested handler faults and recovery +remain unqualified; those first write-frame failures do not validate later checks. + +Ordinary CI requires each new 46,976-case read report. The full 218-input CPU +audit passes **5,318 tests / 53 unavailable / zero failures** (5,371 total). +Its independently verified inventory contains **86,699,178 logical cases / 943 +reports / ten native presets**. All 937 retained reports match the preceding +215-input full audit; the six added mask/fixture/read reports match their +independently qualified snapshots. The ten native results retain identical +outcomes and pinned inputs, with current CPU/adapter assembly identities bound. +The current ordinary semantic gate passes **86,506,698 cases / 743 batches**. +The composed **report-only** fixture separately passes **86,506,698 cases / 743 +batches** across 941 reports. Missing/incomplete scalar read reports reject for +the intended reasons; each hardlinked control is unlinked before alteration and +original report hashes remain unchanged. That check executes no CPU instructions +and does not substitute for the running full audit. +A fresh isolated local `1.5.2-synthetic-dev.75` package uses these exact 37 CPU +inputs plus README/icon (39 total). Its immutable package record explicitly +retains the `fullIntegrationPending=true` recorded at creation. A separate +completed linkage proof now binds those exact 37 packaged CPU inputs to the full +execution and clears the pending qualification without rewriting that record. +The clean committed CopperScreen `aa1dad5` archive passes the Release production +build, 171 host tests (six unavailable), 74 disk tests, 1,080 engine diagnostics +and two Workbench 3.1 **floppy** replays. Independent verification binds identical +consumer source files and test rosters, NuGet-only/private-cache resolution, +packaged/loaded DLL hashes, producer exits and exact ROM/ADF identities. +No HD boot or physical throughput result is claimed. + +Local evidence under `%TEMP%/copper68k-reference-restoration-20261006/`: +`audits/MovesReadProvenanceBeforeV1`, `audits/MovesReadProvenanceAfterV1`, +`mutations/MovesReadV1`, `audits/MovesFaultReadFixV1`, `audits/MovesReadFullV1` +and `consumer-75-final`. Focused proof: +`2a5b3126d471119a2f64499372c0f7794cf4a8bc08d501470ccc01711c362e8e`; +mutation proof: +`9a762acd3048144a968d792bf3be8c27f99c57f1d152db9835ac1436df41b4dc`; +remaining failed discovery: +`2a0e0af89c0b42d51fe420957d08ed59bf170d2d3b3fb79c42548cfd3797a3ed`; +consumer proof: +`a70d4cf6802c6ec6d16deac74d6965b3d862e7469b3e64c615f40e6905c20721`. +Ordinary report-only control proof: +`a271213de74a35e51c95e1819061d3cc53b27546d65e2550bb176495b71e9818`. +Completed full audit proof: +`47ea0f96f3fb28ff5fba16ce8fe53a7bdd5bd035d7694e9c1ed480430f229fef`; +complete source/evidence linkage: +`22eb639ac1071a62f5d8338a05ccabd86c4e7b715453fe82ee3b459a7846742d`. + +This slice does not qualify every EA/full-format/alias/stack-register combination, +enabled MMU/cache, partial physical transfers, handler return, trace or physical +timing. Store faults remain unfinished and the hardware disagreement remains +open. Package `.75` is local-only; no publication or private C023 candidate +import occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index b70ca5a7..7a399cc3 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1946,6 +1946,36 @@ instruction. EA pointer faults, alias/auto modes, trace, nested handlers, enable MMU/cache and physical timing remain unqualified. No package or retirement changes occur. Milestone 6 remains **in progress**, `roadmapComplete=false`. +### Milestone 6 MOVES read-fault provenance — 2026-10-08 + +Fix the documented read attribute defect at the actual MOVES operand boundary. +Physical reads latch SFC in an internal fault record, converted by the frame +builder according to MC68040UM Table 3-2. Extension and EA pointer reads retain +ordinary attributes. No access is retried, no public API or enabled MMU behavior +changes, and saved-PC/auto-address effects preserve the existing execution policy. +The separate nine-form operand, all-CCR indirect, pointer and extension cohorts +execute **93,952 cases**, all passing. The unchanged CPU fails **33,600** exact +operand combinations; the 1,536 fault-free discovery controls remain identical. +Three private mutations qualify conversion, SFC selection and operand provenance +against exact affected memberships. Ordinary CI requires 46,976 cases per route. +The composed report-only CI fixture passes 86,506,698 cases / 743 batches; +missing/incomplete scalar read reports reject, with original evidence unchanged. +This report validation executes no instructions and is separate from CPU testing. + +The original maintained discovery now has only **1,792 write-frame mismatches**, +so its gate stays **failed**. Store completion/saved PC, actual handler recovery, +trace, additional EA/alias/register combinations and physical pipeline/timing +remain unqualified. The full 218-input CPU audit passes **5,318 tests / 53 +unavailable / zero failures**, with an independently verified **86,699,178-case / +943-report / ten-native-preset** inventory. All retained reports match the earlier +accepted full audit; the current ordinary gate passes 86,506,698 cases / 743 batches. +Local `.75` package consumers pass the clean production build, host, disk, engine +and two native Workbench 3.1 floppy replays. Completed linkage binds all 37 CPU +inputs across focused/full execution and the package; immutable earlier pending +records are preserved. The local package is not published and the private C023 CPU is +not imported. See [the read-fault record](COPPER68K_REFERENCE_QUALIFICATION.md#040-moves-read-fault-provenance--2026-10-08). +No regression is retired. Milestone 6 remains **in progress**, `roadmapComplete=false`. + On 2026-10-07 the executed 040 MOVEM read-recovery gate passes 229,968 complete diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index fb1a8033..b47b9989 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -242,6 +242,8 @@ try { $expected['rte-access-movem-full-index'] = 114048 $expected['movem-mask-fault-controls-scalar'] = 1364 $expected['movem-mask-fault-controls-batch'] = 1364 + $expected['moves-read-fault-provenance-scalar'] = 46976 + $expected['moves-read-fault-provenance-batch'] = 46976 $expected['handler-prefetch-executing-scalar'] = 12288 $expected['handler-prefetch-executing-batch'] = 12288 $expected['handler-prefetch-entry-scalar'] = 196608 From 6cddeab34b2db93b32800ef1f3cdaa9609eaf01a Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 11:41:28 +0300 Subject: [PATCH 149/211] Correct 040 MOVES normal-space write fault recovery --- Copper68k.Tests/Synthetic/README.md | 48 ++++- .../SyntheticM68040MovesWriteFaultTests.cs | 203 ++++++++++++++++++ Copper68k/M68040Support.cs | 25 ++- .../M68kAdvancedTimingInterpreter.System.cs | 14 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 83 +++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 42 ++++ scripts/test-copper68k-synthetic.ps1 | 2 + 7 files changed, 399 insertions(+), 18 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040MovesWriteFaultTests.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 5b0a68e0..52fad760 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1359,13 +1359,13 @@ instructions. `-BaselineDirectory` additionally requires unchanged retained recovery reports; a missing requested baseline fails. Neither mode promotes missing inputs, empty selections or mismatching/unsupported execution. -The separate 040 MOVES physical-fault discovery is currently **failing**. Its +The separate 040 MOVES physical-fault gate now **passes all 5,120 cases**. Its literal byte/word/long encodings use (A0), all eight SFC/DFC choices, four address lanes, ISP/MSP and CCR 0/31. Each operand byte can trigger a physical-map rejection on scalar/batch public-factory routes. All 1,536 fault-free controls pass; of 3,584 -fault cases, the original captured CPU had 3,136 mismatches. After the read -attribute fix, all read cases pass; 1,792 write cases still incorrectly stack -format 0. The original failed reports remain historical evidence. +fault cases, the original captured CPU had 3,136 mismatches. After the read attribute fix, 1,792 writes still stacked format 0. The write +correction closes those remaining frame mismatches; all 3,584 physical faults +and 1,536 controls now pass. The original failed reports remain historical evidence. The controls run by default; the two fault tests are unavailable unless requested. They remain a discovery gate, separate from the qualified MOVES instruction matrix. @@ -1373,12 +1373,13 @@ They remain a discovery gate, separate from the qualified MOVES instruction matr ./scripts/test-copper68k-040-moves-faults.ps1 -OutputDirectory artifacts/moves-fault-discovery ``` -This command intentionally returns failure while the defects remain. It freezes -the source graph, requires all four tests and exact inventories, and records a -failed verification proof. Missing inputs, empty selections and altered evidence +This command freezes the source graph, requires all four tests and exact +inventories, and records a passing verification proof only for complete evidence. Missing inputs, empty selections and altered evidence also fail. `-ValidateReportsOnly` executes no instructions; `-PythonCommand` -selects Python 3. Saved-PC policy, recovery/retry, special-space writebacks, -nested handler faults, enabled MMU/cache and hardware timing remain unqualified. +selects Python 3. This frame-discovery command alone does not qualify saved-PC +policy or handler recovery; the ordinary normal-space recovery matrix is +described below. Special-space recovery, nested handler faults, enabled MMU/cache +and hardware timing remain unqualified by these ordinary gates. See the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-moves-physical-fault-discovery--2026-10-08). The ordinary gate now requires 46,976 `moves-read-fault-provenance-scalar` and @@ -1397,5 +1398,30 @@ The complete 218-input CPU audit passes 5,318 tests with 53 unavailable and no failures; independent verification binds 86,699,178 cases, 943 profile reports and ten native presets. The current ordinary gate passes 86,506,698 cases / 743 batches. The same CPU source inputs pass isolated local `.75` NuGet consumer -build/tests and two native floppy replays. The remaining write discovery stays -failed and Milestone 6 remains in progress. +build/tests and two native floppy replays. This is the retained read checkpoint; +the following write correction has its own complete frozen audit. + + +Ordinary coverage additionally requires 4,032 `moves-write-recovery-scalar` and +4,032 `moves-write-recovery-batch` programs. These use literal B/W/L D0 stores +through (A0), (A0)+ and -(A0), four lanes, DFC 1/2/5/6, ISP/MSP, CCR 0/31, all +rejected operand bytes and 0/T1/T0. Actual integer handlers complete WB1, clear +its valid bit, restore DFC/registers and execute RTE. Defined frame fields, +every handler instruction, pending trace and following MOVEQ are checked. The +original operand is never retried and auto-address effects occur once. + +All 8,064 new programs pass; the preceding CPU fails every one. Three isolated +mutations detect raw DFC, lost T0 and opcode return PC. The synchronous following +PC and retained address effects qualify the existing execution policy, not +physical pipeline behavior. Special-space write recovery, other operand forms, +nested handler faults, enabled MMU/cache and hardware timing remain outside this +ordinary matrix. No old regression is retired. + +The complete 219-input CPU audit passes 5,320 tests with 53 unavailable and no +failures: 86,707,242 cases / 945 profile reports / ten native presets. All 943 +retained reports match the predecessor. The current ordinary semantic gate +passes 86,514,762 cases / 745 batches. A local-only `.76` NuGet package passes +the clean consumer build/tests and two native floppy replays; source, package, +loaded DLL and input/result linkage is independently verified. Nothing is +published. See [the write-recovery record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-moves-normal-space-write-recovery--2026-10-08). +Milestone 6 remains in progress. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040MovesWriteFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040MovesWriteFaultTests.cs new file mode 100644 index 00000000..f77f7238 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040MovesWriteFaultTests.cs @@ -0,0 +1,203 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM Table 3-2 and 8.4.6: actual normal-space MOVES writes, +// WB1 software completion and RTE. Following-PC and retained EA effects +// qualify the existing synchronous execution policy, not physical pipelines. +public sealed class SyntheticM68040MovesWriteFaultTests(ITestOutputHelper output) +{ + private const uint Handler = 0xa000, TraceHandler = 0xb000; + private sealed record StoreFixture(int Width, uint DestinationAddress, uint OperandValue, + uint NextPc, uint[] D, uint[] A, ushort ExpectedSr); + + [Theory, Trait("Suite", "Synthetic")] + [InlineData(false)] + [InlineData(true)] + public void NormalSpaceStoresCompleteThroughActualHandler(bool batch) + { + var report = new CoverageBatch("68040", "moves-write-recovery-" + (batch ? "batch" : "scalar")); + foreach (var mode in new[] { 2, 3, 4 }) + foreach (var width in new[] { 1, 2, 4 }) + for (uint lane = 0; lane < 4; lane++) + foreach (var fc in new[] { 1, 2, 5, 6 }) + foreach (var bank in new[] { "ISP", "MSP" }) + foreach (var ccr in new[] { 0, 31 }) + foreach (var trace in new ushort[] { 0, 0x8000, 0x4000 }) + for (var faultByte = 0; faultByte < width; faultByte++) + { + ushort opcode = (ushort)((width == 1 ? 0x0e00 : width == 2 ? 0x0e40 : 0x0e80) | mode << 3); + Case(report, batch, opcode, lane, ccr, bank, trace, $"lane={lane}/DFC={fc}/byte={faultByte}", fc, faultByte); + } + report.Complete(output); + } + + private static (string Status, string? Reason) Case(CoverageBatch report, bool batch, ushort opcode, uint value, int ccr, + string bank, ushort trace, string scenario, int fc = 5, int faultByte = 0) + { + var width = opcode >> 6 & 3; + width = 1 << width; + var mode = opcode >> 3 & 7; + var id = $"68040/MOVES/write-recovery/{scenario}/size={width}/mode={mode}/bank={bank}/T={trace:X4}/ccr={ccr:X2}/op={opcode:X4}"; + string? mismatch = null; var status = "passing"; + try + { + var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + m.Reset(ccr); + var operand = 0x4200u + (uint)(value & 3); + m.Core.State.D[0] = 0x89abcdef; + m.Core.State.A[0] = mode == 4 ? operand + (uint)width : operand; + for (uint at = 0x41fc; at < 0x420b; at++) m.InitializePhysical(at, 0x5a, 1); + SyntheticExecution.Prepare(m, [opcode, 0x0800, 0x7e55, 0x4e71]); + m.Core.State.SetInterruptStackPointer(0x4700); + m.Core.State.SetMasterStackPointer(0x7400); + m.Core.State.StatusRegister = (ushort)(0x0700 | M68040StackFixture.Status(bank, trace, ccr)); + var fixture = new StoreFixture(width, operand, 0x89abcdefu & (width == 1 ? 0xffu : width == 2 ? 0xffffu : uint.MaxValue), + 0x1004, (uint[])m.Core.State.D.Clone(), (uint[])m.Core.State.A.Clone(), (ushort)(0x0700 | ccr)); + if (mode == 3) fixture.A[0] = operand + (uint)width; + if (mode == 4) fixture.A[0] = operand; + var originalA7 = m.Core.State.A[7]; + var stacks = new Dictionary { ["user"] = 0x7800, ["ISP"] = 0x4700, ["MSP"] = 0x7400 }; + stacks[M68040StackFixture.PhysicalBank(bank)] = originalA7; + m.Core.State.SetUserStackPointer(stacks["user"]); m.Core.State.SetInterruptStackPointer(stacks["ISP"]); + m.Core.State.SetMasterStackPointer(stacks["MSP"]); + m.Core.State.StatusRegister = (ushort)(0x0700 | M68040StackFixture.Status(bank, trace, ccr)); + m.Core.State.DestinationFunctionCode = (uint)fc; m.Core.State.SourceFunctionCode = 6; + var program = new SyntheticM68040WritebackProgram(Handler); program.Initialize(m); + m.InitializePhysical(8, Handler, 4); m.InitializePhysical(36, TraceHandler, 4); + m.InitializePhysical(TraceHandler, 0x4e73, 2); m.InitializePhysical(TraceHandler + 2, 0x4e71, 2); + m.InitializePhysical(fixture.NextPc, 0x7e55, 2); m.InitializePhysical(fixture.NextPc + 2, 0x4e71, 2); + var e = ArchitecturalExpectation.Capture(m); + Array.Copy(fixture.D, e.D, 8); Array.Copy(fixture.A, e.A, 8); + var savedSr = (ushort)((fixture.ExpectedSr & 0x071f) | M68040StackFixture.Status(bank, trace, 0)); + stacks[M68040StackFixture.PhysicalBank(bank)] = fixture.A[7]; + var exceptionBank = M68040StackFixture.ExceptionBank(savedSr); + var frame = stacks[exceptionBank] - 60; stacks[exceptionBank] = frame; + var sequence = m.Core.State.ExceptionSequence; + M68040StackFixture.SetStacks(e, stacks, (ushort)((savedSr | 0x2000) & 0x3fff)); + e.Pc = Handler; e.ExceptionVector = 2; + e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence + 1); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, savedSr); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, fixture.NextPc); + e.ControlChecks["DFC"] = (s => s.DestinationFunctionCode, (uint)fc); + e.ControlChecks["SFC"] = (s => s.SourceFunctionCode, 6); + e.Write(frame, savedSr, 2, m.Model); e.Write(frame + 2, fixture.NextPc, 4, m.Model); e.Write(frame + 6, 0x7008, 2, m.Model); + for (uint n = 8; n < 60; n++) e.MemoryMasks[frame + n] = 0; + var size = fixture.Width == 1 ? 0x20 : fixture.Width == 2 ? 0x40 : 0; + var modifier = fc is 1 or 2 ? 1 : 5; + var ssw = (ushort)(size | modifier | (trace != 0 ? 0x2000 : 0)); + ushort[] statuses = [(ushort)(size | modifier | 0x80), 0, 0]; + uint[] addresses = [fixture.DestinationAddress, 0, 0]; + WriteDefined(12, ssw, 2); e.MemoryMasks[frame + 13] = 0x7f; + foreach (uint n in new uint[] { 14, 16 }) + { WriteDefined(n, 0, 2); e.MemoryMasks[frame + n + 1] = 0x80; } + WriteDefined(18, statuses[0], 2); WriteDefined(20, addresses[0], 4); WriteDefined(40, addresses[0], 4); + if (trace != 0) WriteDefined(8, SyntheticMachine.Code, 4); + for (var n = 0; n < fixture.Width; n++) + { + var at = frame + 44 + ((addresses[0] + (uint)n) & 3); + e.Memory[at] = (byte)(fixture.OperandValue >> (8 * (fixture.Width - n - 1))); e.MemoryMasks[at] = 255; + } + bus.Arm(addresses[0] + (uint)faultByte, M68kBusAccessKind.CpuDataWrite); + ExecuteCore(m, batch); + if (bus.Rejected.Count != 1 || bus.Rejected[0].Address != addresses[0] || + bus.Rejected[0].Width != fixture.Width && !(fixture.Width == 4 && bus.Rejected[0].Width == 2)) + throw new InvalidOperationException("Rejected operand width/address changed"); + // Some existing fallback routes retain descending word transfers. + // A completed low word before rejection is an independent partial + // store, not a whole-instruction retry or an atomic bus guarantee. + foreach (var access in bus.Accesses.Take(bus.Rejected[0].AccessCount).Where(a => a.Write)) + { + var offset = unchecked(access.Address - addresses[0]); + if (fixture.Width != 4 || access.Width != 2 || offset != 2 || access.Value != (fixture.OperandValue & 0xffff)) + throw new InvalidOperationException("Unexpected store before operand rejection"); + if (access.Address < frame || access.Address >= frame + 60) + e.Write(access.Address, fixture.OperandValue & 0xffff, 2, m.Model); + } + mismatch = e.Verify(m); + if (mismatch == null && bus.Accesses.Skip(bus.Rejected[0].AccessCount).Any(a => a.Write && + (a.Address < frame || a.Address >= frame + 60))) mismatch = "Rejected operand was repeated during entry"; + if (mismatch != null) throw new InvalidOperationException("fault entry: " + mismatch); + // Undefined frame bytes are opaque inputs to the software handler. + // Their values qualify neither the CPU's defined lanes nor the final + // intended store, which are checked independently from OperandValue. + for (uint n = 8; n < 60; n++) e.Memory[frame + n] = bus.Peek(frame + n); + e.MemoryMasks.Clear(); + uint[] encoded = [m.PeekPhysical(frame + 44, 4), 0, 0]; + var originalD = (uint[])e.D.Clone(); var originalA = (uint[])e.A.Clone(); + var stores = new List<(uint Address, int Width, uint Value)>(); + var steps = 0; + while (e.Pc != fixture.NextPc && e.Pc != TraceHandler) + { + if (++steps > 120) throw new InvalidOperationException("Handler failed to terminate"); + var instruction = program.Instructions.Single(i => i.Pc == e.Pc); + program.Expect(instruction, m, e, frame, statuses, addresses, encoded, originalD, originalA, (uint)fc, stacks, stores); + if (instruction.Operation == "return") + { + if (trace == 0) + { stacks[exceptionBank] = frame + 60; M68040StackFixture.SetStacks(e, stacks, savedSr); e.Pc = fixture.NextPc; } + else + { + stacks[exceptionBank] = frame + 48; M68040StackFixture.SetStacks(e, stacks, (ushort)((savedSr | 0x2000) & 0x3fff)); + e.Write(frame + 48, savedSr, 2, m.Model); e.Write(frame + 50, fixture.NextPc, 4, m.Model); + e.Write(frame + 54, 0x2024, 2, m.Model); e.Write(frame + 56, SyntheticMachine.Code, 4, m.Model); + e.Pc = TraceHandler; e.ExceptionVector = 9; + e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence + 2); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, fixture.NextPc); + } + } + mismatch = Execute(m, e, batch); + if (mismatch != null) throw new InvalidOperationException($"handler {instruction.Operation}/WB{instruction.Slot}: {mismatch}"); + } + if (!stores.SequenceEqual(new[] { (addresses[0], fixture.Width, fixture.OperandValue) })) + throw new InvalidOperationException("Handler store differs from independently intended operand"); + if (trace != 0) + { + stacks[exceptionBank] = frame + 60; M68040StackFixture.SetStacks(e, stacks, savedSr); e.Pc = fixture.NextPc; + mismatch = Execute(m, e, batch); if (mismatch != null) throw new InvalidOperationException("trace RTE: " + mismatch); + } + var completedStores = bus.Accesses.Count(a => a.Write && a.Address == fixture.DestinationAddress && a.Kind == M68kBusAccessKind.CpuDataWrite); + if (completedStores != 1 || bus.Rejected.Count != 1) throw new InvalidOperationException("Original MOVES was repeated or pending write not completed exactly once"); + e.D[7] = 0x55; e.Sr = (ushort)(savedSr & 0xfff0); e.Pc = fixture.NextPc + 2; + if (trace == 0x8000) + { + stacks[exceptionBank] -= 12; var sp = stacks[exceptionBank]; + var tracedSr = e.Sr; + M68040StackFixture.SetStacks(e, stacks, (ushort)((tracedSr | 0x2000) & 0x3fff)); + e.Write(sp, tracedSr, 2, m.Model); e.Write(sp + 2, fixture.NextPc + 2, 4, m.Model); + e.Write(sp + 6, 0x2024, 2, m.Model); e.Write(sp + 8, fixture.NextPc, 4, m.Model); + e.Pc = TraceHandler; e.ExceptionVector = 9; + e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence + 3); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, tracedSr); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, fixture.NextPc + 2); + } + mismatch = Execute(m, e, batch); if (mismatch != null) throw new InvalidOperationException("following MOVEQ: " + mismatch); + if (bus.Accesses.Count(a => a.Write && a.Address == fixture.DestinationAddress && a.Kind == M68kBusAccessKind.CpuDataWrite) != 1) + throw new InvalidOperationException("Following sentinel repeated the original store"); + + void WriteDefined(uint offset, uint data, int width) + { e.Write(frame + offset, data, width, m.Model); for (var n = 0; n < width; n++) e.MemoryMasks[frame + offset + (uint)n] = 255; } + } + catch (Exception ex) + { mismatch = ex.ToString(); status = ex is UnsupportedM68040InstructionException or UnsupportedM68kOpcodeException or UnsupportedM68kTimingException ? "unsupported" : "mismatching"; } + report.Record(id, status, mismatch); + return (status, mismatch); + } + + private static string? Execute(SyntheticMachine m, ArchitecturalExpectation e, bool batch) + { + ExecuteCore(m, batch); + return e.Verify(m); + } + private static void ExecuteCore(SyntheticMachine m, bool batch) + { + if (!batch) m.Core.ExecuteInstruction(); + else if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, m.Core.State.Cycles + 1000, new Boundary()) != 1) + throw new InvalidOperationException("Expected exactly one batch instruction"); + } + private sealed class Boundary : IM68kInstructionBoundary + { public bool BeforeInstruction() => true; public void AfterInstruction(long previousCycle, long currentCycle) { } } + +} diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index 93f59a89..0830f7c8 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -1866,7 +1866,8 @@ internal readonly record struct M68040MmuFault( bool CompletedMoveWrite = false, bool UnbufferedOperandRead = false, uint? MovemEffectiveAddress = null, - byte? MovesFunctionCode = null); + byte? MovesFunctionCode = null, + bool CompletedMovesWrite = false); internal sealed class UnsupportedM68040InstructionException : M68kEmulationException { @@ -2565,6 +2566,11 @@ protected override bool TryHandleM68040ExecutionFault(M68040MmuFault fault) fault = fault with { StackedProgramCounter = ExecutionBoundaryProgramCounter }; else if (TryCompleteFaultedMoveWrite(fault)) fault = fault with { StackedProgramCounter = State.ProgramCounter, CompletedMoveWrite = true }; + else if (!State.M68040Mmu.Enabled && fault.CompletedMovesWrite && + fault.Write && fault.WriteValue.HasValue && fault.AccessKind == M68kBusAccessKind.CpuDataWrite && + State.ExceptionSequence == ExecutionBoundaryExceptionSequence && + State.LastInstructionProgramCounter == ExecutionBoundaryProgramCounter) + fault = fault with { StackedProgramCounter = State.ProgramCounter }; else if (!State.M68040Mmu.Enabled && fault.Write && fault.MovemEffectiveAddress.HasValue && fault.AccessKind == M68kBusAccessKind.CpuDataWrite && State.ExceptionSequence == ExecutionBoundaryExceptionSequence && @@ -4338,7 +4344,7 @@ private void RaiseMmuFault(M68040MmuFault fault) RaiseRteValidationAccessFault(fault, stackedProgramCounter); else if (!State.M68040Mmu.Enabled && fault.AccessKind == M68kBusAccessKind.CpuInstructionFetch) RaiseUnbufferedReadAccessFault(fault, stackedProgramCounter, instruction: true); - else if (fault.CompletedMoveWrite || (!State.M68040Mmu.Enabled && fault.Write && + else if (fault.CompletedMoveWrite || fault.CompletedMovesWrite || (!State.M68040Mmu.Enabled && fault.Write && fault.MovemEffectiveAddress.HasValue && fault.AccessKind == M68kBusAccessKind.CpuDataWrite)) RaiseNormalWriteAccessFault(fault, stackedProgramCounter); else if (fault.UnbufferedOperandRead) @@ -4363,13 +4369,17 @@ private void RaiseRteValidationAccessFault(M68040MmuFault fault, uint instructio private void RaiseNormalWriteAccessFault(M68040MmuFault fault, uint stackedPc) { // MC68040UM 8.4.6.5/7: physical normal write -> valid WB1, FA=WB1A, - // memory-aligned data. A completed MOVE resumes at its following - // instruction. MOVEM remains suspended with CM and its original EA; + // memory-aligned data. Synchronous completed MOVE/MOVES stores + // resume at the following instruction; physical pipeline PCs remain + // unqualified. MOVEM remains suspended with CM and its original EA; // software completes WB1 before RTE repeats operand transfers. var savedSr = State.StatusRegister; var size = fault.ByteCount == 1 ? 0x20 : fault.ByteCount == 2 ? 0x40 : 0; var modifier = (savedSr & M68kCpuState.Supervisor) != 0 ? 5 : 1; - var trace = fault.CompletedMoveWrite && (savedSr & 0x8000) != 0; // MOVE does not trigger T0. + if (fault.MovesFunctionCode is byte fc) modifier = MovesTransferAttributes(fc); + // MOVE does not trigger T0; MOVES does (MC68040UM 8.2.6). + var trace = fault.CompletedMoveWrite && (savedSr & 0x8000) != 0 || + fault.CompletedMovesWrite && (savedSr & 0xc000) != 0; uint data = fault.WriteValue!.Value; if (fault.ByteCount == 1) data = (data & 0xff) << 24; else if (fault.ByteCount == 2) data = (data & 0xffff) << 16; @@ -4410,7 +4420,7 @@ private void RaiseUnbufferedReadAccessFault(M68040MmuFault fault, uint instructi { // MC68040UM Table 3-2: instruction spaces are merged into data; // the other alternate spaces retain TM and use special TT=2. - modifier = (fc & 3) is 0 or 3 ? 0x10 | fc : (fc & 4) | 1; + modifier = MovesTransferAttributes(fc); } var size = fault.ByteCount == 1 ? 0x20 : fault.ByteCount == 2 ? 0x40 : 0; PushWord((ushort)(0x0100 | size | modifier | (fault.MovemEffectiveAddress.HasValue ? 0x1000 : 0))); @@ -4421,6 +4431,9 @@ private void RaiseUnbufferedReadAccessFault(M68040MmuFault fault, uint instructi EnterAccessErrorHandler(); } + private static int MovesTransferAttributes(byte fc) + => (fc & 3) is 0 or 3 ? 0x10 | fc : (fc & 4) | 1; + private void EnterAccessErrorHandler() { State.ProgramCounter = ReadLong(State.VectorBaseRegister + 8); diff --git a/Copper68k/M68kAdvancedTimingInterpreter.System.cs b/Copper68k/M68kAdvancedTimingInterpreter.System.cs index e63cf2df..55e1210d 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.System.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.System.cs @@ -83,7 +83,19 @@ private bool TryExecuteGeneralSystem(ushort opcode) var size = (M68kOperandSize)(1 << field); var address = ResolveMoveAddress(m, r, size, opcode); if (m == 3) WriteGeneralRegister(true, r, unchecked(address + M68kIntegerSemantics.AddressIncrement(r, size))); - if (store) WriteSized(address, ReadGeneralRegister(ar, general), size); + if (store) + { + try { WriteSized(address, ReadGeneralRegister(ar, general), size); } + catch (M68040MmuFaultException ex) when (!State.M68040Mmu.Enabled && + ex.Fault.Write && ex.Fault.WriteValue.HasValue && + ex.Fault.AccessKind == M68kBusAccessKind.CpuDataWrite) + { + // The actual store is already latched by the logical bus; + // extensions and EA effects must not be performed again. + throw new M68040MmuFaultException(ex.Fault with + { MovesFunctionCode = (byte)(State.DestinationFunctionCode & 7), CompletedMovesWrite = true }); + } + } else { uint value; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index b20766b3..ae9092c2 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -11809,3 +11809,86 @@ enabled MMU/cache, partial physical transfers, handler return, trace or physical timing. Store faults remain unfinished and the hardware disagreement remains open. Package `.75` is local-only; no publication or private C023 candidate import occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + + +## 040 MOVES normal-space write recovery — 2026-10-08 + +Actual physical MOVES stores formerly fell through to format 0. The correction +attaches the selected DFC and completion provenance only when the operand +`WriteSized` rejects a physical store with latched data. EA resolution and +extension fetching precede this scope. The frame builder converts TT/TM using +[MC68040UM Table 3-2 and 8.4.6](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf), +retains the actual data/address in memory-aligned WB1, and preserves the existing +synchronous completed-store policy: software completes WB1 before RTE returns +after MOVES. Pre/postincrement effects are retained exactly once. T0/T1 pending +trace uses CT; MOVE retains its existing T1-only rule. No whole-instruction retry, +production public API or timing-policy change is introduced. Physical pipeline +PC/address timing remains unqualified. + +The independent normal-space matrix uses literal B/W/L D0 stores through (A0), +(A0)+ and -(A0), all four byte lanes, DFC 1/2/5/6, ISP/MSP, CCR 0/31, every +rejected operand byte and trace 0/T1/T0. Each of **8,064 programs** verifies +registers/flags/canaries and defined frame fields, executes the actual integer +WB1 handler with checks after every instruction, checks DFC/register restoration, +RTE and pending trace, and executes a following MOVEQ sentinel. No original store +or address update repeats. All 8,064 fail against the preceding CPU's format-zero +path and pass with the correction. All 93,952 read-fault programs and 1,536 +fault-free fixtures retain identical reports. The first fixture run incorrectly +omitted IPL from expected SR; its failure is retained, and the corrected fixture +is run identically against both CPUs. + +Three owned mutations verify exact failing inventories per route: raw DFC +conversion 2,016; lost T0 1,344; opcode return PC 4,032. Original sources remain +unchanged. No existing regression is retired. The maintained physical MOVES gate +separately passes **5,120 cases**: 3,584 actual faults and 1,536 controls, closing +its 1,792 remaining write-frame mismatches. Its required WB1 data fields still +apply only to ordinary data spaces. Seven altered-evidence controls reject +missing reports, empty selection, wrong weight/key, changed source, producer and +assembly; complete independently relocated evidence passes. + +The complete frozen **219-input / 37-CPU-input** full suite passes **5,320 tests / +53 unavailable / zero failures** (5,373 total). Independent verification binds +**86,707,242 logical cases / 945 complete reports / ten native presets**; all 943 +retained reports match the accepted read checkpoint and both new reports match +focused evidence. The ordinary semantic gate passes **86,514,762 cases / 745 +batches**. Missing/incomplete write reports reject in separate copied-evidence +controls. Those controls execute no CPU instructions and do not replace the full +audit. + +A fresh immutable local `1.5.2-synthetic-dev.76` package uses these exact 37 CPU +inputs plus README/icon. The clean committed CopperScreen `aa1dad5` archive +passes the Release build, 171 host tests (six unavailable), 74 disk tests, 1,080 +engine diagnostics and two Workbench 3.1 floppy replays. Source files and test +rosters match the accepted consumer archive; NuGet/private-cache resolution and +all three loaded CPU DLLs match the package. The package/consumer records retain +the full-pending status recorded when created. A separate complete linkage proof +binds the now-finished full suite without rewriting those records. No HD boot or +host-throughput qualification is claimed. + +Local evidence remains under `%TEMP%/copper68k-reference-restoration-20261006/`: +`audits/MovesWriteProvenanceBeforeV4`, `audits/MovesWriteProvenanceAfterV5`, +`mutations/MovesWriteV1`, `audits/MovesFaultStoreFixV1`, +`moves-write-integrity-v1`, `moves-write-ordinary-controls-v1`, +`audits/MovesWriteFullV1` and `consumer-76-final`. + +| Evidence | SHA-256 | +| --- | --- | +| focused | `6f21041e00c2aa09fde49d11cb6e8c73960b1346c6e4352004c3bd97cef0eb77` | +| mutations | `bfb94303bde01542adf509debf0d442136c99ecf45eff6cf5f520bf46ee5b6b2` | +| faultGate | `f0f55bcabb0406ef2ac59b920a0c4c1f44046e9ab6fd08c34f94ad143a1c62a1` | +| integrity | `0da6c3ce401a8bc5b947f9b292dabee874aa38712949e45b1f76538870759407` | +| ordinaryControls | `33a0fef2a7e7a0a0e5f41f76a82ebf49226a2110a81ba5deb90f9702abba4ab3` | +| consumer | `9fc1251d189a160fb8eb3553c277e6630ca7e332b8545bbcdcfa2e8d52c55367` | +| full | `f32959471ba2ea93a8c2d9444a119016b347a2fa0e4f07bf7e394abee6f93027` | +| package | `d3936d1812ea5d978970c99d8c50a894497597e65d8eda4869e1ef71b0b062e3` | +| complete source/evidence link | `8105e5c86f6bf8974b16067131972c1720f2c42ff869676b2662dd6a4ed2470c` | + + +This bounded gate does not qualify every general register, EA/full-format, +alias/A7/overlap combination, special-space recovery, nested handler fault, +enabled MMU/cache, partial physical transport or physical timing. Separately +captured private nested-handler discovery remains unpromoted and cannot expand +this ordinary gate's scope. The software/manual trace disagreements and 010/060 +broad-reference mismatches remain open; hardware is unavailable. No private C023 +production candidate is imported, no package is published and prior versions +remain immutable. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 7a399cc3..24ba2b66 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3702,3 +3702,45 @@ No production CPU source or package changes occur. This slice does not qualify all mask/fault-position Cartesian combinations, other CCR/stack/EA combinations, enabled MMU/cache, physical timing, or the unresolved hardware boundaries. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +### Milestone 6 MOVES normal-space write recovery — 2026-10-08 + +Hardware is unavailable for the disputed 030/040 trace boundary; that +qualification gap remains open. The current software slice extends the existing +synchronous completed-MOVE write policy to actual MOVES operand writes. It +retains the bus-latched data/address and selected DFC, reports format 7/WB1, +and resumes after the store once an actual integer handler completes WB1. +No implicit instruction retry is permitted. Following-PC and pre/postincrement +effects qualify that execution policy, not physical pipeline behavior. + +The bounded independent matrix executes byte/word/long D0 stores through (A0), +(A0)+ and -(A0), all four byte lanes, DFC 1/2/5/6, ISP/MSP, CCR 0/31, every +rejected operand byte and trace states 0/T1/T0. Each of **8,064 programs** checks +defined frame fields, every handler instruction, DFC/register restoration, RTE, +pending trace and a following MOVEQ sentinel. Every program fails against the +preceding CPU's format-zero path and passes with the correction. +All **93,952 read-fault programs** and **1,536 fault-free fixtures** retain their +previous complete reports. The maintained MOVES fault command separately passes +**5,120 cases**, closing its 1,792 remaining write-frame mismatches. + +Three isolated mutations fail at their independently enumerated combinations: +raw DFC conversion (2,016 per route), lost T0 (1,344), and opcode return PC +(4,032). Original sources remain unchanged. These are new qualification cases; +no existing regression has been retired. + +The complete frozen 219-input CPU audit passes **5,320 tests / 53 unavailable / +zero failures** (5,373 total), with 86,707,242 logical cases, 945 profile reports +and ten native presets. All 943 retained reports match the accepted predecessor. +The ordinary semantic gate passes 86,514,762 cases / 745 batches. Missing and +incomplete write reports reject; seven altered-source/input/assembly controls +reject, and the complete relocated fault evidence passes. A fresh local-only +`1.5.2-synthetic-dev.76` package preserves the NuGet consumer boundary. The clean +consumer archive passes the Release build, 171 host tests (six optional tests +unavailable), 74 disk tests, 1,080 engine tests and two native floppy replays; +loaded CPU DLLs match the package. It is not published. Special-space write +recovery, nested handler write faults, broader operand forms, enabled MMU/cache +and physical timing remain outside this +bounded gate. Complete source/evidence linkage binds the focused, full, mutation, +fault-frame, ordinary-control and consumer results without rewriting records that +were created while the full gate was pending. Milestone 6 remains **in progress**, +`roadmapComplete=false`. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index b47b9989..e396db75 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -244,6 +244,8 @@ try { $expected['movem-mask-fault-controls-batch'] = 1364 $expected['moves-read-fault-provenance-scalar'] = 46976 $expected['moves-read-fault-provenance-batch'] = 46976 + $expected['moves-write-recovery-scalar'] = 4032 + $expected['moves-write-recovery-batch'] = 4032 $expected['handler-prefetch-executing-scalar'] = 12288 $expected['handler-prefetch-executing-batch'] = 12288 $expected['handler-prefetch-entry-scalar'] = 196608 From 61fd6ae52f900c5de04d0994f3505be131b5d327 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 12:09:10 +0300 Subject: [PATCH 150/211] Promote 040 nested integer writeback fault coverage --- Copper68k.Tests/Synthetic/README.md | 19 +++ ...yntheticM68040NestedWritebackFaultTests.cs | 155 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 63 +++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 26 +++ scripts/test-copper68k-synthetic.ps1 | 2 + 5 files changed, 265 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 52fad760..abae9239 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1425,3 +1425,22 @@ the clean consumer build/tests and two native floppy replays; source, package, loaded DLL and input/result linkage is independently verified. Nothing is published. See [the write-recovery record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-moves-normal-space-write-recovery--2026-10-08). Milestone 6 remains in progress. + +Ordinary coverage now also requires 336 `nested-writeback-fault-scalar` and 336 +`nested-writeback-fault-batch` programs. These supply a three-slot normal outer +format-7 frame and reject an actual integer handler store in WB1/WB2/WB3. +Nested completion, register/DFC restoration, RTE, remaining writes and a following +sentinel are checked instruction by instruction. The three writes occur in order +exactly once. Coverage spans B/W/L, four lanes, TM 1/5, ISP/MSP and every rejected +operand byte; the slots share one selected width and the initial CCR is 31. + +All 672 programs pass and fail against the preceding MOVES write CPU. Three +mutations detect lost A1 restoration, wrong word data and wrong return PC. The +ordinary composed report-only gate passes 86,515,434 cases / 747 batches and +rejects missing/incomplete nested reports. This test-only promotion retains the +same 37 production CPU inputs, earlier 219-input full CPU audit and local `.76` +consumer evidence. It adds a bounded 220-input execution and does not claim a new +wide full suite or consumer run. Outer-frame construction, mixed slot widths, +trace/interrupt interruption, other transfer spaces, enabled MMU/cache and +physical timing remain unqualified. See [the nested-handler record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-nested-integer-writeback-handler-faults--2026-10-08). +Milestone 6 remains in progress; nothing is published. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs new file mode 100644 index 00000000..09d10ed0 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs @@ -0,0 +1,155 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Supplied normal format-7 outer frame; actual physical fault in the integer +// WB1/WB2/WB3 completion handler. MC68040UM 8.4.6.3/5/7. No physical pipeline, +// original outer-frame construction or enabled MMU qualification is claimed. +public sealed class SyntheticM68040NestedWritebackFaultTests(ITestOutputHelper output) +{ + private const uint Handler = 0xa000, Next = 0x8000; + + [Theory, Trait("Suite", "Synthetic")] + [InlineData(false)] + [InlineData(true)] + public void ActualHandlerStoresFaultCompleteAndResume(bool batch) + { + var report = new CoverageBatch("68040", "nested-writeback-fault-" + (batch ? "batch" : "scalar")); + foreach (var width in new[] { 1, 2, 4 }) + for (uint lane = 0; lane < 4; lane++) + foreach (var fc in new[] { 1, 5 }) + foreach (var bank in new[] { "ISP", "MSP" }) + for (var slot = 1; slot <= 3; slot++) + for (var faultByte = 0; faultByte < width; faultByte++) + { + var id = $"68040/writeback/nested-physical-fault/size={width}/lane={lane}/FC={fc}/bank={bank}/WB={slot}/byte={faultByte}"; + try { Run(batch, width, lane, fc, bank, slot, faultByte); report.Record(id, "passing", null); } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); } + } + report.Complete(output); + } + + private static void Run(bool batch, int width, uint lane, int fc, string bank, int faultSlot, int faultByte) + { + var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + m.Reset(31); + var sr = (ushort)(bank == "MSP" ? 0x371f : 0x271f); + var stacks = new Dictionary { ["user"] = 0x7800, ["ISP"] = 0x4700, ["MSP"] = 0x7400 }; + var frame = stacks[bank] - 60; stacks[bank] = frame; + var size = width == 1 ? 0x20 : width == 2 ? 0x40 : 0; + var wbStatus = (ushort)(0x80 | size | fc); + ushort[] statuses = [wbStatus, wbStatus, wbStatus]; + uint[] addresses = [0x4200 + lane, 0x4300 + lane, 0x4400 + lane]; + uint[] values = [0x89abcdef, 0x10203040, 0xfedcba98]; + var mask = width == 1 ? 0xffu : width == 2 ? 0xffffu : uint.MaxValue; + values = values.Select(x => x & mask).ToArray(); + for (uint at = frame; at < frame + 60; at++) m.InitializePhysical(at, 0, 1); + m.InitializePhysical(frame, sr, 2); m.InitializePhysical(frame + 2, Next, 4); + m.InitializePhysical(frame + 6, 0x7008, 2); + m.InitializePhysical(frame + 12, (uint)(size | fc), 2); + m.InitializePhysical(frame + 20, addresses[0], 4); + for (var n = 0; n < 3; n++) + { + m.InitializePhysical(frame + (uint)SyntheticM68040WritebackProgram.StatusOffset(n + 1), wbStatus, 2); + m.InitializePhysical(frame + (uint)SyntheticM68040WritebackProgram.AddressOffset(n + 1), addresses[n], 4); + if (n == 0) + for (var b = 0; b < width; b++) + m.InitializePhysical(frame + 44 + ((addresses[0] + (uint)b) & 3), values[0] >> (8 * (width - 1 - b)), 1); + else m.InitializePhysical(frame + (uint)SyntheticM68040WritebackProgram.DataOffset(n + 1), values[n], 4); + for (uint b = 0; b < 12; b++) m.InitializePhysical(addresses[n] - 4 + b, 0x5a, 1); + } + uint[] data = [m.PeekPhysical(frame + 44, 4), values[1], values[2]]; + var program = new SyntheticM68040WritebackProgram(Handler); program.Initialize(m); + m.InitializePhysical(8, Handler, 4); m.InitializePhysical(Next, 0x7e55, 2); + m.InitializePhysical(Next + 2, 0x4e71, 2); + m.Start(); m.Core.State.SetUserStackPointer(stacks["user"]); + m.Core.State.SetInterruptStackPointer(stacks["ISP"]); m.Core.State.SetMasterStackPointer(stacks["MSP"]); + m.Core.State.StatusRegister = sr; m.Core.State.ProgramCounter = Handler; + m.Core.State.DestinationFunctionCode = 7; m.Core.State.SourceFunctionCode = 6; + var e = ArchitecturalExpectation.Capture(m); var sequence = m.Core.State.ExceptionSequence; + e.ControlChecks["DFC"] = (s => s.DestinationFunctionCode, 7); + e.ControlChecks["SFC"] = (s => s.SourceFunctionCode, 6); + e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence); + var originalD = (uint[])e.D.Clone(); var originalA = (uint[])e.A.Clone(); + var stores = new List<(uint Address, int Width, uint Value)>(); var faulted = false; var steps = 0; + bus.Accesses.Clear(); + while (e.Pc != Next) + { + if (++steps > 240) throw new InvalidOperationException("Outer handler did not terminate"); + var i = program.Instructions.Single(x => x.Pc == e.Pc); + if (!faulted && i.Slot == faultSlot && i.Operation.StartsWith("store-")) + { + faulted = true; + var savedSr = e.Sr; var resume = i.Pc + 4; + var nested = stacks[bank] - 60; stacks[bank] = nested; + M68040StackFixture.SetStacks(e, stacks, (ushort)(savedSr & 0x3fff)); + e.Pc = Handler; e.ExceptionVector = 2; + e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence + 1); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, resume); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, savedSr); + for (uint b = 8; b < 60; b++) e.MemoryMasks[nested + b] = 0; + Define(0, savedSr, 2); Define(2, resume, 4); Define(6, 0x7008, 2); + Define(12, (uint)(size | fc), 2); e.MemoryMasks[nested + 13] = 0x7f; + foreach (uint at in new uint[] { 14, 16 }) { Define(at, 0, 2); e.MemoryMasks[nested + at + 1] = 0x80; } + Define(18, wbStatus, 2); Define(20, addresses[faultSlot - 1], 4); Define(40, addresses[faultSlot - 1], 4); + for (var b = 0; b < width; b++) + { + var at = nested + 44 + ((addresses[faultSlot - 1] + (uint)b) & 3); + e.Memory[at] = (byte)(values[faultSlot - 1] >> (8 * (width - 1 - b))); e.MemoryMasks[at] = 255; + } + bus.Arm(addresses[faultSlot - 1] + (uint)faultByte, M68kBusAccessKind.CpuDataWrite); + Execute("nested fault entry"); + if (bus.Rejected.Count != 1) throw new InvalidOperationException("Missing unique handler-store rejection"); + for (uint b = 8; b < 60; b++) e.Memory[nested + b] = bus.Peek(nested + b); + e.MemoryMasks.Clear(); + ushort[] ns = [wbStatus, 0, 0]; uint[] na = [addresses[faultSlot - 1], 0, 0]; + uint[] nd = [m.PeekPhysical(nested + 44, 4), 0, 0]; + var nestedD = (uint[])e.D.Clone(); var nestedA = (uint[])e.A.Clone(); + var nestedStores = new List<(uint Address, int Width, uint Value)>(); var nsteps = 0; + // The reentrant handler shares code addresses with its caller. + // Seeing the resume PC inside the nested handler is not a return. + var returned = false; + while (!returned) + { + if (++nsteps > 120) throw new InvalidOperationException("Nested handler did not terminate"); + var ni = program.Instructions.Single(x => x.Pc == e.Pc); + program.Expect(ni, m, e, nested, ns, na, nd, nestedD, nestedA, (uint)fc, stacks, nestedStores); + if (ni.Operation == "return") + { stacks[bank] = nested + 60; M68040StackFixture.SetStacks(e, stacks, savedSr); e.Pc = resume; returned = true; } + Execute("nested " + ni.Operation); + } + if (!nestedStores.SequenceEqual(new[] { (addresses[faultSlot - 1], width, values[faultSlot - 1]) })) + throw new InvalidOperationException("Nested handler completed wrong pending store"); + stores.Add((addresses[faultSlot - 1], width, values[faultSlot - 1])); + + void Define(uint at, uint value, int count) + { e.Write(nested + at, value, count, m.Model); for (uint b = 0; b < count; b++) e.MemoryMasks[nested + at + b] = 255; } + } + else + { + program.Expect(i, m, e, frame, statuses, addresses, data, originalD, originalA, 7, stacks, stores); + if (i.Operation == "return") + { stacks[bank] = frame + 60; M68040StackFixture.SetStacks(e, stacks, sr); e.Pc = Next; } + Execute("outer " + i.Operation); + } + } + if (!faulted || !stores.SequenceEqual(addresses.Select((a, n) => (a, width, values[n])))) + throw new InvalidOperationException("WB1/WB2/WB3 completion order differs"); + var operandWrites = bus.Accesses.Where(a => a.Write && addresses.Contains(a.Address)) + .Select(a => (a.Address, a.Width, a.Value)); + if (!operandWrites.SequenceEqual(stores)) throw new InvalidOperationException("A handler store was repeated or omitted"); + e.Pc = Next + 2; e.D[7] = 0x55; e.Sr &= 0xfff0; Execute("following MOVEQ"); + + void Execute(string stage) + { + if (batch) + { if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, m.Core.State.Cycles + 10000, new Boundary()) != 1) throw new InvalidOperationException("Empty batch"); } + else m.Core.ExecuteInstruction(); + if (e.Verify(m) is { } mismatch) throw new InvalidOperationException(stage + ": " + mismatch); + } + } + private sealed class Boundary : IM68kInstructionBoundary + { public bool BeforeInstruction() => true; public void AfterInstruction(long previousCycle, long currentCycle) { } } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index ae9092c2..c9ece347 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -11892,3 +11892,66 @@ this ordinary gate's scope. The software/manual trace disagreements and 010/060 broad-reference mismatches remain open; hardware is unavailable. No private C023 production candidate is imported, no package is published and prior versions remain immutable. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## 040 nested integer writeback handler faults — 2026-10-08 + +`SyntheticM68040NestedWritebackFaultTests` promotes the previously captured +private fixture into ordinary synthetic coverage. Only the class, trait and +report labels change; the fixture logic is identical. All **672 programs** pass +(336 on each scalar/batch route), and all 672 fail against the preceding MOVES +write implementation. The accompanying **103,552 retained MOVES programs** keep +their case inventories and architectural results. The six passing reports are +identical to the accepted full CPU checkpoint. Before-fix failure stack traces +have different frozen source paths; their case IDs, statuses and diagnostic +messages match the captured predecessor. The initial verifier's overstrict +stack-trace comparison is retained alongside the corrected verifier. + +The independent fixture supplies a normal format-7 outer frame with three valid +writeback slots. An actual integer completion handler suffers one physical MOVES +store fault in WB1, WB2 or WB3; its nested handler completes the rejected store +and returns to the outer handler. Byte/word/long stores, all four lanes, TM 1/5, +ISP/MSP and every rejected operand byte are covered. The three supplied slots use +the same selected operand width and CCR starts at 31. Each instruction checks +registers, defined flags, memory canaries, stack banks and PC. The program checks +DFC restoration, actual RTE, all three writes in order exactly once, and a +following MOVEQ sentinel. This is explicit software completion of pending writes, +not implicit operand retry. The reentrant handler shares code addresses with its +caller: observing the resume address is insufficient to prove return, so the +fixture observes execution of the actual RTE step. + +Three owned mutations fail at precisely enumerated combinations per route: +lost A1 restoration **336**; wrong word writeback data **96** (the remaining 240 +pass); wrong following PC **336**. The mutants change only the actual handler +encoding or the production return-PC assignment, leaving expectations intact. +Original frozen sources are unchanged. No regression is retired. + +The ordinary semantic gate requires both 336-case reports. Missing and incomplete +reports reject; a complete composed fixture passes **86,515,434 logical cases / +747 batches / 947 model reports**. These report-only controls execute no CPU +instructions. The new bounded execution uses **220 source inputs**, with all +**37 production CPU inputs identical** to commit `6cddeab` and the accepted local +`.76` package. The earlier **219-input** full execution remains **5,320 passing / +53 unavailable / zero failures**, 86,707,242 cases, 945 reports and ten native +presets. The same CPU's isolated consumer evidence is retained; neither a new +wide full-suite run nor new consumer execution is claimed for this test-only +promotion. + +Evidence is local under `%TEMP%/copper68k-reference-restoration-20261006/`: +`audits/NestedWritebackPromotionBeforeV1`, `audits/NestedWritebackPromotionAfterV1`, +`mutations/NestedWritebackV1` and `nested-writeback-ordinary-controls-v1`. + +| Evidence | SHA-256 | +| --- | --- | +| promotion | `bc28c2a25540af6f682f88bb4aabf93b20fa3adbebfb502e176f87f9da568303` | +| mutations | `8cf0aa5d26a8dbae6ba7d8448cb290f9832505ef32b207faa32878cd91d8e788` | +| ordinary controls | `6455a9fa164e960ddec37e4acb41441725b09ae42444a3e034cf605e4986b1ea` | +| retained full CPU proof | `f32959471ba2ea93a8c2d9444a119016b347a2fa0e4f07bf7e394abee6f93027` | +| retained consumer proof | `9fc1251d189a160fb8eb3553c277e6630ca7e332b8545bbcdcfa2e8d52c55367` | + +The outer frame is supplied; original physical construction of all three pending +slots is not qualified. This gate does not cover mixed slot widths, every CCR, +trace/interrupt interruption, other transfer spaces, deeper repeated handler +faults, enabled MMU/cache or physical pipeline/timing. MOVE16 write-fault discovery +remains failed and unpromoted. Hardware is unavailable for the trace/manual +boundary disagreements. No private C023 production candidate is imported and no +package is published. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 24ba2b66..a334ad71 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3744,3 +3744,29 @@ bounded gate. Complete source/evidence linkage binds the focused, full, mutation fault-frame, ordinary-control and consumer results without rewriting records that were created while the full gate was pending. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +### Milestone 6 nested integer writeback handler faults — 2026-10-08 + +Promote the captured normal-space nested-handler fixture with unchanged logic. +All **672 programs** pass and fail against the preceding MOVES write CPU; all +**103,552 accompanying MOVES programs** retain their architectural results. +The supplied three-slot outer format-7 frame drives actual integer WB1/WB2/WB3 +handler faults, nested completion, restoration, RTE, remaining outer writes and +a following sentinel. B/W/L, four lanes, TM 1/5, ISP/MSP and each rejected byte +are covered; every instruction is checked and the three writes occur in order +exactly once. Three mutations detect missing register restoration, wrong word +data and wrong following PC without changing expectations. + +Both new 336-case reports are mandatory in ordinary coverage. Missing/incomplete +controls reject; the composed report-only gate passes **86,515,434 cases / 747 +batches**. The new bounded 220-input execution retains all 37 production CPU +inputs. The accepted 219-input full suite and local `.76` NuGet consumer gates +remain separate unchanged evidence; this slice claims no new wide full execution, +consumer replay or package release. Evidence identities and exact limits are in +[the qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040-nested-integer-writeback-handler-faults--2026-10-08). + +Outer frame creation, mixed slot widths, all CCR states, trace/interrupt +interruption, other transfer spaces and physical pipeline behavior remain outside +this gate. MOVE16 write faults remain a failed private discovery. No old test is +retired. Hardware is unavailable at disputed boundaries. Milestone 6 remains +**in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index e396db75..180d2627 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -246,6 +246,8 @@ try { $expected['moves-read-fault-provenance-batch'] = 46976 $expected['moves-write-recovery-scalar'] = 4032 $expected['moves-write-recovery-batch'] = 4032 + $expected['nested-writeback-fault-scalar'] = 336 + $expected['nested-writeback-fault-batch'] = 336 $expected['handler-prefetch-executing-scalar'] = 12288 $expected['handler-prefetch-executing-batch'] = 12288 $expected['handler-prefetch-entry-scalar'] = 196608 From 3dd2640cd5f112a42efbb51a42847d830eb000e4 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 13:11:33 +0300 Subject: [PATCH 151/211] Correct 040 MOVE16 physical line-write recovery --- Copper68k.Tests/Synthetic/README.md | 27 ++ .../SyntheticM68040Move16FaultTests.cs | 270 ++++++++++++++++++ Copper68k/M68040Support.cs | 36 ++- .../M68kAdvancedTimingInterpreter.System.cs | 16 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 96 +++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 49 ++++ scripts/test-copper68k-synthetic.ps1 | 4 + 7 files changed, 496 insertions(+), 2 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040Move16FaultTests.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index abae9239..040c680b 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1444,3 +1444,30 @@ wide full suite or consumer run. Outer-frame construction, mixed slot widths, trace/interrupt interruption, other transfer spaces, enabled MMU/cache and physical timing remain unqualified. See [the nested-handler record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-nested-integer-writeback-handler-faults--2026-10-08). Milestone 6 remains in progress; nothing is published. + + +Ordinary coverage additionally requires 320 `move16-physical-fixture-scalar` and +320 `move16-physical-fixture-batch` cases, plus 20,480 `move16-physical-fault-scalar` +and 20,480 `move16-physical-fault-batch` programs. Five literal legal MOVE16 forms, +all 16 source alignment lanes paired with complementary destination lanes, +ISP/MSP, CCR 0/31 and every rejected line byte are checked. Write programs cover +0/T1/T0 and execute an actual integer full-line completion handler, RTE and a +following sentinel. Latched PD0..PD3, defined transfer fields, registers, canaries +and stacks are checked instruction by instruction; the source is not reread and +the instruction is not implicitly retried. Explicit software completion can +retouch destination long words accepted before the fault. + +All 41,600 programs pass; the preceding CPU fails all 30,720 write cases. Four +mutations detect lost line data, wrong transfer type, wrong following PC and lost +address updates. A fifth control proves that the final fixture detects damaged +inactive USP using its captured pre-fault expectation. The complete full CPU +audit passes 5,326 tests / 53 unavailable / zero failures: 86,749,514 cases / 951 +reports / ten native presets. The full frozen V3 fixture and final focused V4 +fixture differ by that one expectation line, with identical CPU inputs and +byte-identical focused reports. The ordinary gate passes 86,557,034 cases / 751 +batches and rejects missing/incomplete MOVE16 reports. Local-only `.77` NuGet +consumers pass the clean Release build, host/disk/engine tests and two floppy +replays. WB1 validity, read transfer type, broader aliases/user-mode entry and +physical pipelines remain unqualified. The refreshed broad Basic replay still +has two mismatching directories. See [the MOVE16 record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-move16-physical-line-write-recovery--2026-10-08). +Milestone 6 remains in progress; nothing is published. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040Move16FaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040Move16FaultTests.cs new file mode 100644 index 00000000..da6a1c1b --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040Move16FaultTests.cs @@ -0,0 +1,270 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 8.4.6/8.4.6.7: format 7; normal read or MOVE16 write +// write transfer attributes. WB1 V is deliberately unqualified: 8.4.6.3 +// contradicts example 4 and Table 8-6. Read TT also remains unqualified: +// example 1 specifies TT=0, while Table 5-2 and the software reference +// describe MOVE16 TT=1. Following-PC/address updates qualify the synchronous +// execution policy. Physical pipeline/timing remain unqualified. +public sealed class SyntheticM68040Move16FaultTests(ITestOutputHelper output) +{ + private const uint Handler = 0xa000, TraceHandler = 0xb000; + + [Theory, Trait("Suite", "Synthetic")] + [InlineData(false)] + [InlineData(true)] + public void LiteralMove16FixturesComplete(bool batch) => Audit(batch, false); + + [Theory, Trait("Suite", "Synthetic")] + [InlineData(false)] + [InlineData(true)] + public void ActualLineOperandFaultsRequireFormat7(bool batch) => Audit(batch, true); + + private void Audit(bool batch, bool fault) + { + var report = new CoverageBatch("68040", "move16-physical-" + (fault ? "fault-" : "fixture-") + (batch ? "batch" : "scalar")); + for (var form = 0; form < 5; form++) + for (uint lane = 0; lane < 16; lane++) + foreach (var bank in new[] { "ISP", "MSP" }) + foreach (var ccr in new[] { 0, 31 }) + foreach (var write in fault ? new[] { false, true } : new[] { false }) + foreach (var trace in fault && write ? new ushort[] { 0, 0x8000, 0x4000 } : new ushort[] { 0 }) + for (var byteIndex = 0; byteIndex < (fault ? 16 : 1); byteIndex++) + { + var id = $"68040/MOVE16/physical-{(fault ? "fault" : "fixture")}/form={form}/source-low={lane}/destination-low={15 - lane}/bank={bank}/ccr={ccr:X2}/direction={(write ? "write" : "read")}/T={trace:X4}/byte={byteIndex}"; + try { Run(batch, fault, form, lane, bank, ccr, write, byteIndex, trace); report.Record(id, "passing", null); } + catch (Exception ex) { report.Record(id, ex is UnsupportedM68040InstructionException or UnsupportedM68kOpcodeException or UnsupportedM68kTimingException ? "unsupported" : "mismatching", ex.Message); } + } + report.Complete(output); + } + + private static void Run(bool batch, bool fault, int form, uint lane, string bank, int ccr, bool write, int byteIndex, ushort trace) + { + var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); + m.Reset(ccr); + var source = 0x5200u + lane; var destination = 0x6200u + 15 - lane; + ushort[] words; + // Fixed manual first words and fixed destination-register extension. + // Fixture locations are chosen before encoding; no decoder/EA helper. + switch (form) + { + case 0: m.Core.State.A[0] = source; words = [0xf600, (ushort)(destination >> 16), (ushort)destination]; break; + case 1: m.Core.State.A[0] = destination; words = [0xf608, (ushort)(source >> 16), (ushort)source]; break; + case 2: m.Core.State.A[0] = source; words = [0xf610, (ushort)(destination >> 16), (ushort)destination]; break; + case 3: m.Core.State.A[0] = destination; words = [0xf618, (ushort)(source >> 16), (ushort)source]; break; + default: m.Core.State.A[0] = source; m.Core.State.A[1] = destination; words = [0xf620, 0x9000]; break; + } + uint[] line = [0x89abcdef, 0x10203040, 0xfedcba98, 0x76543210]; + foreach (var at in new uint[] { 0x5200, 0x6200 }) + for (uint n = 0; n < 24; n++) m.InitializePhysical(at - 4 + n, 0x5a, 1); + for (var n = 0; n < 4; n++) m.InitializePhysical(0x5200 + (uint)n * 4, line[n], 4); + SyntheticExecution.Prepare(m, words); + var next = 0x1000u + (uint)words.Length * 2; + m.InitializePhysical(next, 0x7e55, 2); m.InitializePhysical(next + 2, 0x4e71, 2); + m.InitializePhysical(8, Handler, 4); m.InitializePhysical(Handler, 0x4e71, 2); + m.InitializePhysical(Handler + 2, 0x4e71, 2); + m.InitializePhysical(36, TraceHandler, 4); m.InitializePhysical(TraceHandler, 0x4e73, 2); + if (fault && write) + { + uint hp = Handler; + foreach (var wordsAt in LineHandler()) + foreach (var word in wordsAt) { m.InitializePhysical(hp, word, 2); hp += 2; } + } + var s = m.Core.State; + s.SetInterruptStackPointer(0x4700); s.SetMasterStackPointer(0x7400); + s.StatusRegister = (ushort)((bank == "MSP" ? 0x3700 : 0x2700) | ccr | trace); + s.SourceFunctionCode = 2; s.DestinationFunctionCode = 6; + var e = ArchitecturalExpectation.Capture(m); var sequence = s.ExceptionSequence; + bus.Accesses.Clear(); + if (fault) bus.Arm((write ? 0x6200u : 0x5200u) + (uint)byteIndex, + write ? M68kBusAccessKind.CpuDataWrite : M68kBusAccessKind.CpuDataRead); + Execute(); + if (!fault) + { + for (var n = 0; n < 4; n++) e.Write(0x6200 + (uint)n * 4, line[n], 4, m.Model); + if (form is 0 or 1 or 4) e.A[0] += 16; + if (form == 4) e.A[1] += 16; + e.Pc = next; + if (e.Verify(m) is { } mismatch) throw new InvalidOperationException("fixture: " + mismatch); + if (s.ExceptionSequence != sequence || bus.Rejected.Count != 0) throw new InvalidOperationException("Unexpected fault-free exception"); + var accesses = bus.Accesses.Where(a => a.Address >= 0x5200 && a.Address < 0x5210 || a.Address >= 0x6200 && a.Address < 0x6210) + .Select(a => (a.Address, a.Width, a.Write)); + var expected = Enumerable.Range(0, 4).Select(n => (0x5200u + (uint)n * 4, 4, false)) + .Concat(Enumerable.Range(0, 4).Select(n => (0x6200u + (uint)n * 4, 4, true))); + if (!accesses.SequenceEqual(expected)) throw new InvalidOperationException("Synchronous line transport differs"); + e.Pc += 2; e.D[7] = 0x55; e.Sr &= 0xfff0; Execute(); + if (e.Verify(m) is { } sentinel) throw new InvalidOperationException("sentinel: " + sentinel); + } + else + { + var stack = bank == "MSP" ? 0x7400u : 0x4700u; var frame = stack - 60; + if (bus.Rejected.Count != 1 || bus.Rejected[0].Address != (write ? 0x6200u : 0x5200u) + (uint)(byteIndex / 4) * 4 || bus.Rejected[0].Width != 4) + throw new InvalidOperationException("Wrong physical operand rejection"); + if (s.ProgramCounter != Handler || s.LastExceptionVector != 2 || s.ExceptionSequence != sequence + 1 || s.Halted || s.Stopped) + throw new InvalidOperationException("Rejected transfer did not enter exactly one access error"); + var format = m.PeekPhysical(s.A[7] + 6, 2); + if (s.A[7] != frame || format != 0x7008) + throw new InvalidOperationException($"format 7 required: SP={s.A[7]:X8}, frame={format:X4}"); + if (m.PeekPhysical(frame, 2) != e.Sr || m.PeekPhysical(frame + 20, 4) != bus.Rejected[0].Address) + throw new InvalidOperationException("Saved SR or logical fault address differs"); + // Size/MA, read TT and WB1 V remain explicitly excluded. Write + // TT=1 and both normal supervisor data TM=5 are required. + // SFC/DFC do not select MOVE16. + var attributes = write ? 0x000du : 0x0105u; + if ((m.PeekPhysical(frame + 12, 2) & (write ? 0x011fu : 0x0107u)) != attributes) + throw new InvalidOperationException("MOVE16 direction/TM or write TT differ"); + // Reads must not modify D registers or CCR. Fault-time A-register + // completion/PC policy remains an explicitly separate investigation. + if (!s.D.SequenceEqual(e.D) || s.StatusRegister != (e.Sr & 0x3fff)) throw new InvalidOperationException("Fault changed data registers/flags"); + var partials = write ? byteIndex / 4 : 0; + for (var n = 0; n < partials; n++) e.Write(0x6200 + (uint)n * 4, line[n], 4, m.Model); + foreach (var at in e.Memory.Keys.Concat(bus.Memory.Keys).Distinct()) + if ((at < frame || at >= stack) && e.Memory.GetValueOrDefault(at) != bus.Peek(at)) + throw new InvalidOperationException($"Unexpected operand/canary change at {at:X8}"); + var after = bus.Accesses.Skip(bus.Rejected[0].AccessCount); + if (after.Any(a => a.Address >= 0x5200 && a.Address < 0x5210 || a.Address >= 0x6200 && a.Address < 0x6210)) + throw new InvalidOperationException("Line operand access repeated during exception delivery"); + if (write) + CompleteLine(m, bus, e, batch, frame, bank, form, next, line, partials, sequence); + } + if (s.SourceFunctionCode != 2 || s.DestinationFunctionCode != 6) throw new InvalidOperationException("Instruction changed FC registers"); + + void Execute() + { + if (batch) + { if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, s.Cycles + 10000, new Boundary()) != 1) throw new InvalidOperationException("Empty batch"); } + else m.Core.ExecuteInstruction(); + } + } + // Fixed independent program: preserve D0/A0/A1, align FA, write PD0..PD3 + // using ordinary MOVE.L, restore registers, then RTE. WB1 validity is not + // consulted, because the manual contradicts itself on that field. + private static ushort[][] LineHandler() => + [ + [0x48e7, 0x80c0], [0x43ef, 12], [0x2029, 20], + [0x0280, 0xffff, 0xfff0], [0x2040], + [0x2029, 44], [0x20c0], [0x2029, 48], [0x20c0], + [0x2029, 52], [0x20c0], [0x2029, 56], [0x20c0], + [0x4cdf, 0x0301], [0x4e73] + ]; + + private static void CompleteLine(SyntheticMachine m, + SyntheticM68040AccessDoubleFaultTests.FaultBus bus, ArchitecturalExpectation e, + bool batch, uint frame, string bank, int form, uint next, uint[] line, int partials, uint sequence) + { + var s = m.Core.State; + if (m.PeekPhysical(frame + 2, 4) != next || s.LastExceptionStackedProgramCounter != next) + throw new InvalidOperationException("Synchronous MOVE16 write must resume after consumed extensions"); + if ((m.PeekPhysical(frame + 12, 2) & 0x60) != 0x60) + throw new InvalidOperationException("MOVE16 line transfer size missing"); + var traced = (e.Sr & 0x8000) != 0; + if ((m.PeekPhysical(frame + 12, 2) & 0xf000) != (traced ? 0x2000u : 0)) + throw new InvalidOperationException("MOVE16 pending trace classification differs"); + if (traced && m.PeekPhysical(frame + 8, 4) != SyntheticMachine.Code) + throw new InvalidOperationException("Pending trace origin differs"); + for (var n = 0; n < 4; n++) + if (m.PeekPhysical(frame + 44 + (uint)n * 4, 4) != line[n]) + throw new InvalidOperationException($"Latched PD{n} data differs"); + if (form is 0 or 1 or 4) e.A[0] += 16; + if (form == 4) e.A[1] += 16; + var originalD = (uint[])e.D.Clone(); var originalA = (uint[])e.A.Clone(); + var savedSr = e.Sr; + var stacks = new Dictionary + { ["user"] = e.InactiveStackPointer!.Value, ["ISP"] = bank == "ISP" ? frame : 0x4700, + ["MSP"] = bank == "MSP" ? frame : 0x7400 }; + M68040StackFixture.SetStacks(e, stacks, (ushort)(savedSr & 0x3fff)); + e.Pc = Handler; e.ExceptionVector = 2; + e.ControlChecks["exception sequence"] = (x => x.ExceptionSequence, sequence + 1); + e.ControlChecks["saved PC"] = (x => x.LastExceptionStackedProgramCounter, next); + e.ControlChecks["saved SR"] = (x => x.LastExceptionStatusRegister, savedSr); + // Defined fields were checked independently above. Carry the remaining + // unqualified/undefined frame bytes only as preservation canaries. + for (uint at = frame; at < frame + 60; at++) e.Memory[at] = bus.Peek(at); + Verify("fault entry"); + var program = LineHandler(); + for (var n = 0; n < program.Length; n++) + { + e.Pc += (uint)program[n].Length * 2; + if (n == 0) + { + stacks[bank] = frame - 12; M68040StackFixture.SetStacks(e, stacks, e.Sr); + e.Write(frame - 12, originalD[0], 4, m.Model); + e.Write(frame - 8, originalA[0], 4, m.Model); + e.Write(frame - 4, originalA[1], 4, m.Model); + } + else if (n == 1) e.A[1] = frame; + else if (n == 2) { e.D[0] = 0x6200u + (uint)partials * 4; MoveFlags(); } + else if (n == 3) { e.D[0] &= 0xfffffff0; MoveFlags(); } + else if (n == 4) e.A[0] = e.D[0]; + else if (n < 13) + { + var index = (n - 5) / 2; + if ((n & 1) != 0) { e.D[0] = line[index]; MoveFlags(); } + else { e.Write(e.A[0], e.D[0], 4, m.Model); e.A[0] += 4; MoveFlags(); } + } + else if (n == 13) + { + e.D[0] = originalD[0]; e.A[0] = originalA[0]; e.A[1] = originalA[1]; + stacks[bank] = frame; M68040StackFixture.SetStacks(e, stacks, e.Sr); + } + else if (!traced) + { stacks[bank] = frame + 60; M68040StackFixture.SetStacks(e, stacks, savedSr); e.Pc = next; } + else + { + stacks[bank] = frame + 48; M68040StackFixture.SetStacks(e, stacks, (ushort)(savedSr & 0x3fff)); + e.Write(frame + 48, savedSr, 2, m.Model); e.Write(frame + 50, next, 4, m.Model); + e.Write(frame + 54, 0x2024, 2, m.Model); e.Write(frame + 56, SyntheticMachine.Code, 4, m.Model); + e.Pc = TraceHandler; e.ExceptionVector = 9; + e.ControlChecks["exception sequence"] = (x => x.ExceptionSequence, sequence + 2); + e.ControlChecks["saved PC"] = (x => x.LastExceptionStackedProgramCounter, next); + } + Step(); Verify($"line handler step {n}"); + } + if (traced) + { + stacks[bank] = frame + 60; M68040StackFixture.SetStacks(e, stacks, savedSr); e.Pc = next; + Step(); Verify("pending trace RTE"); + } + var reads = bus.Accesses.Where(a => !a.Write && a.Address >= 0x5200 && a.Address < 0x5210) + .Select(a => (a.Address, a.Width)); + if (!reads.SequenceEqual(Enumerable.Range(0, 4).Select(n => (0x5200u + (uint)n * 4, 4)))) + throw new InvalidOperationException("MOVE16 source was reread during recovery"); + var writes = bus.Accesses.Where(a => a.Write && a.Address >= 0x6200 && a.Address < 0x6210) + .Select(a => (a.Address, a.Width, a.Value)); + var expected = Enumerable.Range(0, partials).Concat(Enumerable.Range(0, 4)) + .Select(n => (0x6200u + (uint)n * 4, 4, line[n])); + if (!writes.SequenceEqual(expected)) + throw new InvalidOperationException("Original partial writes or explicit line completion differ"); + e.Pc = next + 2; e.D[7] = 0x55; e.Sr &= 0xfff0; + if (traced) + { + var tracedSr = e.Sr; stacks[bank] -= 12; var sp = stacks[bank]; + M68040StackFixture.SetStacks(e, stacks, (ushort)(tracedSr & 0x3fff)); + e.Write(sp, tracedSr, 2, m.Model); e.Write(sp + 2, next + 2, 4, m.Model); + e.Write(sp + 6, 0x2024, 2, m.Model); e.Write(sp + 8, next, 4, m.Model); + e.Pc = TraceHandler; e.ExceptionVector = 9; + e.ControlChecks["exception sequence"] = (x => x.ExceptionSequence, sequence + 3); + e.ControlChecks["saved PC"] = (x => x.LastExceptionStackedProgramCounter, next + 2); + e.ControlChecks["saved SR"] = (x => x.LastExceptionStatusRegister, tracedSr); + } + Step(); Verify("following MOVEQ"); + + void MoveFlags() => e.Sr = (ushort)((e.Sr & 0xfff0) | + (e.D[0] == 0 ? 4 : 0) | ((e.D[0] & 0x80000000) != 0 ? 8 : 0)); + void Verify(string stage) + { if (e.Verify(m) is { } mismatch) throw new InvalidOperationException(stage + ": " + mismatch); } + void Step() + { + if (batch) + { if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, s.Cycles + 10000, new Boundary()) != 1) throw new InvalidOperationException("Empty batch"); } + else m.Core.ExecuteInstruction(); + } + } + + private sealed class Boundary : IM68kInstructionBoundary + { public bool BeforeInstruction() => true; public void AfterInstruction(long previousCycle, long currentCycle) { } } +} diff --git a/Copper68k/M68040Support.cs b/Copper68k/M68040Support.cs index 0830f7c8..c2015f1b 100644 --- a/Copper68k/M68040Support.cs +++ b/Copper68k/M68040Support.cs @@ -1854,6 +1854,8 @@ private static ulong CreateAtcKey(uint page, M68kBusAccessKind accessKind, bool (supervisor ? 1u : 0u); } + internal readonly record struct M68040Move16Line(uint Long0, uint Long1, uint Long2, uint Long3); + internal readonly record struct M68040MmuFault( uint LogicalAddress, M68kBusAccessKind AccessKind, @@ -1867,7 +1869,8 @@ internal readonly record struct M68040MmuFault( bool UnbufferedOperandRead = false, uint? MovemEffectiveAddress = null, byte? MovesFunctionCode = null, - bool CompletedMovesWrite = false); + bool CompletedMovesWrite = false, + M68040Move16Line? CompletedMove16Line = null); internal sealed class UnsupportedM68040InstructionException : M68kEmulationException { @@ -2564,6 +2567,11 @@ protected override bool TryHandleM68040ExecutionFault(M68040MmuFault fault) // half of an aligned long. Restart the executing instruction. if (!State.M68040Mmu.Enabled && fault.AccessKind == M68kBusAccessKind.CpuInstructionFetch) fault = fault with { StackedProgramCounter = ExecutionBoundaryProgramCounter }; + else if (!State.M68040Mmu.Enabled && fault.CompletedMove16Line.HasValue && + fault.Write && fault.AccessKind == M68kBusAccessKind.CpuDataWrite && + State.ExceptionSequence == ExecutionBoundaryExceptionSequence && + State.LastInstructionProgramCounter == ExecutionBoundaryProgramCounter) + fault = fault with { StackedProgramCounter = State.ProgramCounter }; else if (TryCompleteFaultedMoveWrite(fault)) fault = fault with { StackedProgramCounter = State.ProgramCounter, CompletedMoveWrite = true }; else if (!State.M68040Mmu.Enabled && fault.CompletedMovesWrite && @@ -4344,6 +4352,8 @@ private void RaiseMmuFault(M68040MmuFault fault) RaiseRteValidationAccessFault(fault, stackedProgramCounter); else if (!State.M68040Mmu.Enabled && fault.AccessKind == M68kBusAccessKind.CpuInstructionFetch) RaiseUnbufferedReadAccessFault(fault, stackedProgramCounter, instruction: true); + else if (!State.M68040Mmu.Enabled && fault.CompletedMove16Line.HasValue && fault.Write) + RaiseMove16WriteAccessFault(fault, stackedProgramCounter); else if (fault.CompletedMoveWrite || fault.CompletedMovesWrite || (!State.M68040Mmu.Enabled && fault.Write && fault.MovemEffectiveAddress.HasValue && fault.AccessKind == M68kBusAccessKind.CpuDataWrite)) RaiseNormalWriteAccessFault(fault, stackedProgramCounter); @@ -4366,6 +4376,30 @@ private void RaiseMmuFault(M68040MmuFault fault) private void RaiseRteValidationAccessFault(M68040MmuFault fault, uint instructionPc) => RaiseUnbufferedReadAccessFault(fault, instructionPc, instruction: false); + private void RaiseMove16WriteAccessFault(M68040MmuFault fault, uint stackedPc) + { + // MC68040UM 8.4.6.7 example 4: MOVE16 TT=1; all four memory-aligned + // PD words survive for software completion. 8.4.6.3 contradicts + // example 4 on WB1 validity; zero follows 8.4.6.3 as an unqualified + // convention. This synchronous policy resumes after the instruction, + // with its address updates retained. It is not pipeline qualification. + var line = fault.CompletedMove16Line!.Value; + var savedSr = State.StatusRegister; + var modifier = (savedSr & M68kCpuState.Supervisor) != 0 ? 5 : 1; + var trace = (savedSr & 0x8000) != 0; // MOVE16 does not change flow. + State.RecordException(VectorBusError, stackedPc, savedSr); + State.StatusRegister = (ushort)((savedSr | M68kCpuState.Supervisor) & ~0xc000); + PushLong(line.Long3); PushLong(line.Long2); PushLong(line.Long1); PushLong(line.Long0); + PushLong(fault.LogicalAddress & 0xfffffff0); + for (var n = 0; n < 4; n++) PushLong(0); // No synchronous WB2/WB3. + PushLong(fault.LogicalAddress); + PushWord(0); PushWord(0); PushWord(0); + PushWord((ushort)((trace ? 0x2000 : 0) | 0x60 | 8 | modifier)); + PushLong(trace ? State.LastInstructionProgramCounter : 0); + PushWord(0x7008); PushLong(stackedPc); PushWord(savedSr); + EnterAccessErrorHandler(); + } + private void RaiseNormalWriteAccessFault(M68040MmuFault fault, uint stackedPc) { // MC68040UM 8.4.6.5/7: physical normal write -> valid WB1, FA=WB1A, diff --git a/Copper68k/M68kAdvancedTimingInterpreter.System.cs b/Copper68k/M68kAdvancedTimingInterpreter.System.cs index 55e1210d..5dd672cd 100644 --- a/Copper68k/M68kAdvancedTimingInterpreter.System.cs +++ b/Copper68k/M68kAdvancedTimingInterpreter.System.cs @@ -51,7 +51,21 @@ private bool TryExecuteExtendedSystem(ushort opcode) source &= 0xfffffff0; destination &= 0xfffffff0; Span line = stackalloc uint[4]; for (var i = 0; i < 4; i++) line[i] = ReadLong(source + (uint)i * 4); - for (var i = 0; i < 4; i++) WriteLong(destination + (uint)i * 4, line[i]); + try + { + for (var i = 0; i < 4; i++) WriteLong(destination + (uint)i * 4, line[i]); + } + catch (M68040MmuFaultException ex) when (_profile.Model == M68kAcceleratorModel.M68040 && + !State.M68040Mmu.Enabled && ex.Fault.Write && ex.Fault.ByteCount == 4 && + ex.Fault.AccessKind == M68kBusAccessKind.CpuDataWrite) + { + // All source reads completed before the first store. Keep their + // actual data; exception delivery must not reread or retry the line. + if (form == 4 || form < 2) WriteGeneralRegister(true, register, unchecked(State.A[register] + 16)); + if (other >= 0 && other != register) WriteGeneralRegister(true, other, unchecked(State.A[other] + 16)); + throw new M68040MmuFaultException(ex.Fault with + { CompletedMove16Line = new(line[0], line[1], line[2], line[3]) }); + } if (form == 4 || form < 2) WriteGeneralRegister(true, register, unchecked(State.A[register] + 16)); if (other >= 0 && other != register) WriteGeneralRegister(true, other, unchecked(State.A[other] + 16)); CompleteTiming(M68kInstructionTimingKey.Movec); diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index c9ece347..49f20050 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -11955,3 +11955,99 @@ faults, enabled MMU/cache or physical pipeline/timing. MOVE16 write-fault discov remains failed and unpromoted. Hardware is unavailable for the trace/manual boundary disagreements. No private C023 production candidate is imported and no package is published. Milestone 6 remains **in progress**, `roadmapComplete=false`. + + +## 040 MOVE16 physical line-write recovery — 2026-10-08 + +Actual normal-space MOVE16 destination faults previously escaped through the +format-zero path. The correction retains the four already-read long words, +builds format 7 with the MOVE16 write attributes and PD0..PD3, and preserves +completed synchronous address updates and the following PC. Successful execution +ordering and timing policy are unchanged. The handler explicitly writes the +saved line, restores registers and executes RTE; the original instruction is +never automatically retried and its source is never reread. Previously accepted +destination stores remain intact. Explicit software full-line completion may +write those destination long words again. + +`SyntheticM68040Move16FaultTests` uses five fixed legal opcode forms, independently +chosen operands, all 16 source alignment lanes paired with complementary +destination lanes, ISP/MSP, CCR 0/31 and every rejected byte. The paired lanes +are not a full source/destination Cartesian product. Fault-free fixtures provide +640 cases; 10,240 read-fault cases check format 7 and preserved state; 30,720 +write-fault programs additionally cover 0/T1/T0, defined transfer attributes, +latched line data, retained address effects, every integer handler instruction, +register/stack restoration, RTE, pending trace and a following MOVEQ sentinel. +All **41,600 cases pass**; all **30,720 write cases fail** against the preceding +CPU. All **104,224 accompanying MOVES/nested-handler programs** retain their +results and eight reports. Missing or incomplete MOVE16 reports reject. The +ordinary semantic gate passes **86,557,034 logical cases / 751 batches**. + +Four owned mutations detect wrong PD3 data, wrong TT, opcode return PC and lost +address updates at precisely predicted combinations. Review exposed an inactive +USP expectation read from live post-fault state. Final V4 uses the captured +pre-fault value. A fifth control demonstrates that V3 incorrectly passes the +damaged-USP implementation while V4 detects all 30,720 affected write cases. +No production change is needed for this test correction. All twelve V4 focused +reports are byte-identical to V3. Failure diagnostics have the existing 10,000 +message cap per route; every case ID/status is independently checked, and the +captured messages are checked in canonical generation order. Initial verifier +failures caused by the cap and lexical ordering are preserved; corrected +verifiers enforce the complete inventories without weakening expectations. + +The complete frozen **221-input / 37-CPU-input** full audit passes **5,326 tests / +53 unavailable / zero failures** (5,379 total): **86,749,514 logical cases / 951 +profile reports / ten native presets**. All 945 retained reports match the prior +full checkpoint and six added reports match focused evidence. This full run uses +V3. Final focused V4 differs by one captured-USP expectation line. Independent +linkage verifies both exact source snapshots, that one-line transformation, +report equality and the same 37 production inputs; it does not relabel V3 as a +full V4 execution. + +The immutable local-only `1.5.2-synthetic-dev.77` package uses these same CPU +inputs. Clean committed CopperScreen `aa1dad5` consumers pass the Release build, +171 host tests (six unavailable), 74 disk tests, 1,080 engine tests and two +Workbench 3.1 floppy replays. Exact archive/project/test/input rosters and all +three loaded CPU DLLs are independently bound to the package. Earlier records +retain their full-pending status; the separate complete-link proof records the +finished gate. No HD boot or host-throughput claim is made. + +A fresh pinned broad Basic replay with the current CPU and final V4 fixture +reproduces **1,379 passing / two mismatching / zero unsupported / zero untested +directories**, 12,660,659 callbacks, 2,327,142 exception frames and 32 comparator +probes. Every row/status/diagnostic/counter/probe matches the older broad result, +apart from verified loaded CPU/test assembly identities. The 010 invalid-format +RTE CCR mismatch and 060 ordinary S-clearing STOP mismatch remain unresolved. +The requested broad test exits 1; its gate remains failed. Later failures inside +those two failing directories remain unobserved. No Basic family is excluded. + +The pinned [MC68040 user manual](https://www.nxp.com/docs/en/reference-manual/MC68040UM.pdf) +8.4.6.3 specifies invalid WB1 for MOVE16 writes, while example 4 in 8.4.6.7 +specifies valid WB1. WB1 validity remains unqualified. The example and transfer +table also disagree about read TT; it remains outside the read gate. Saved +following PC and address effects qualify the synchronous execution policy, +not physical pipeline behavior. No real hardware is available. Broader general +register/alias/user-mode combinations, enabled MMU/cache and physical timing +remain outside this bounded gate. No regression is retired, private C023 CPU +candidate imported or public package released. Milestone 6 remains **in progress**, +`roadmapComplete=false`. + +Evidence is local under `%TEMP%/copper68k-reference-restoration-20261006/`: +`audits/Move16RecoveryBeforeV4`, `audits/Move16RecoveryAfterV4`, +`mutations/Move16RecoveryV1`, `mutations/Move16UserStackV1`, +`move16-recovery-ordinary-controls-v1`, `audits/Move16RecoveryFullV1`, +`consumer-77-final`, `audits/QualifiedBasicMove16RecoveryV1` and +`reference/Move16ManualV1`. + +| Evidence | SHA-256 | +| --- | --- | +| initialFocused | `f010b76fded5dcfb87e574d92e5c98a9066b0919b84dc024e97c59c87deabb2a` | +| finalFocused | `534c2482052f3251c52ed275ba619d5fc81f2bb4020a814789f782f73262d8d2` | +| mutations | `239925be9b75e271e60884e46fd1d181ad5110ac621aabc8e5febc3582c4d7ab` | +| userStackControl | `5c94ef61f4d4c0f03384f8c8d804bc628746cf91d64981d8e6842790bb403ddf` | +| ordinaryControls | `9bfb854c7e3569963084d0218ff55de9b96868dcc846acd1cb8ca279a3da114d` | +| consumer | `ef4e3017ebfdf7471378dff94a430bfc0998eb9bbe7bd93dee4a16336b1d75f2` | +| package | `6d8206c1e2d8d41fb584477ced38a3d30708d47a2594f12eb24becff48723fbd` | +| full | `46bddaccc8921960717c6f62a3fae9eb57eaf3dade9cf100d5294b3c617d1298` | +| manual | `c426a8429ca15f342b31c8eb5f407b442e383d1e48f3c7be4aa649ad30c8ef82` | +| complete source/evidence link | `093cec2ced250c78676790816de51aebde285f111367784da86206912dd8bf8f` | +| current broad Basic replay | `d8c4e6f6e7a94e07c6f76cb9b66f1d1ddcb1672dd2195c328171428ee1642d90` | diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index a334ad71..f15a06bb 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3770,3 +3770,52 @@ interruption, other transfer spaces and physical pipeline behavior remain outsid this gate. MOVE16 write faults remain a failed private discovery. No old test is retired. Hardware is unavailable at disputed boundaries. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +### Milestone 6 MOVE16 physical line-write recovery — 2026-10-08 + +The physical MOVE16 write path now +captures the four values already read, constructs format 7 with MOVE16 transfer +attributes and pending line data, and retains synchronous following-PC/address +updates. Actual integer software completion writes all four saved long words, +restores registers and executes RTE without rereading the source or implicitly +retrying the instruction. Already completed operand stores remain intact; the +handler's explicit full-line writes are separately checked. + +The final bounded 221-input fixture passes **41,600 MOVE16 programs** and retains +**104,224 MOVES/nested-handler programs**. The preceding CPU fails all **30,720 +write-fault combinations**. Captured diagnostic messages are limited to 10,000 +per route; all case IDs and statuses are independently verified. Four mutations +detect wrong line data, transfer type, return PC and lost address updates. +Review also exposed a test expectation sampled from live post-fault USP. The +corrected fixture uses the captured pre-fault expectation; a fifth corruption +control reproduces the former loophole and detects every affected write case. + +The composed ordinary report-only gate passes **86,557,034 cases / 751 batches** +and rejects missing/incomplete MOVE16 reports. A fresh local-only `.77` NuGet +package passes the clean consumer Release build, 171 host tests (six unavailable), +74 disk tests, 1,080 engine diagnostics and two native floppy replays. Exact +source/package/loaded-DLL and archive/input/result linkage is independently +verified. No public package is released. + +The complete frozen 221-input CPU audit passes **5,326 tests / 53 unavailable / +zero failures** (5,379 total), with **86,749,514 logical cases / 951 profile +reports / ten native presets**. All 945 retained reports match the accepted +predecessor; six added reports match focused evidence. The full audit uses V3; +the final V4 focused audit strengthens one test-only USP expectation and all +twelve focused reports remain byte-identical. Exact linkage proves the single +test-line difference and identical 37 CPU inputs across both revisions and `.77`. +This is not relabeled as a full execution of V4. + +A fresh broad Basic replay with the final V4 fixture and current CPU reproduces +**1,379 passing / two mismatching directories**, 12,660,659 callbacks and +2,327,142 exception frames. The unresolved 010 invalid-format RTE flags and 060 +S-clearing STOP behavior keep that separate gate failed. No family is excluded; +later failures within the two failing groups remain unobserved. See +[the qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#040-move16-physical-line-write-recovery--2026-10-08). + +MC68040UM 8.4.6.3 contradicts example 4 in 8.4.6.7 on WB1 validity; the bit remains +unqualified. Read transfer type also remains unqualified. Saved following PC and +address updates qualify the synchronous policy, not physical pipelines. Broader +register/alias/user-mode combinations, enabled MMU/cache and physical timing +remain outside this gate. No old regression is retired. Hardware is unavailable +at disputed boundaries. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 180d2627..3e94a66d 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -248,6 +248,10 @@ try { $expected['moves-write-recovery-batch'] = 4032 $expected['nested-writeback-fault-scalar'] = 336 $expected['nested-writeback-fault-batch'] = 336 + $expected['move16-physical-fault-scalar'] = 20480 + $expected['move16-physical-fault-batch'] = 20480 + $expected['move16-physical-fixture-scalar'] = 320 + $expected['move16-physical-fixture-batch'] = 320 $expected['handler-prefetch-executing-scalar'] = 12288 $expected['handler-prefetch-executing-batch'] = 12288 $expected['handler-prefetch-entry-scalar'] = 196608 From 0a55fa18cb0a039a4186e846f971159e400f9132 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 13:22:47 +0300 Subject: [PATCH 152/211] Consolidate NOT displacement semantic regressions --- Copper68k.Tests/M68020HdfBootTests.cs | 30 ------- Copper68k.Tests/Synthetic/README.md | 25 ++++++ .../SyntheticNotDisplacementTests.cs | 42 +++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 62 +++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 18 ++++ ...t-copper68k-not-displacement-retirement.py | 86 +++++++++++++++++++ scripts/test-copper68k-synthetic.ps1 | 1 + 7 files changed, 234 insertions(+), 30 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticNotDisplacementTests.cs create mode 100644 scripts/test-copper68k-not-displacement-retirement.py diff --git a/Copper68k.Tests/M68020HdfBootTests.cs b/Copper68k.Tests/M68020HdfBootTests.cs index 8ea984d6..b3054d20 100644 --- a/Copper68k.Tests/M68020HdfBootTests.cs +++ b/Copper68k.Tests/M68020HdfBootTests.cs @@ -196,36 +196,6 @@ public void MoveaWordDisplacementSignExtendsBeforeOverwritingAliasedBase(M68kCpu } } - [Theory] - [InlineData(M68kCpuModel.M68020)] - [InlineData(M68kCpuModel.M68EC020)] - [InlineData(M68kCpuModel.M68030)] - [InlineData(M68kCpuModel.M68040)] - public void NotLongDisplacementUpdatesOnlyTheLongAndPreservesExtend(M68kCpuModel model) - { - foreach (var value in new uint[] { 0, 0xFFFFFFFF, 0x92345678 }) - foreach (var displacement in new[] { -8, 8 }) - { - var bus = new Copper68kTestBus(0x10000); - bus.WriteWords(0x1000, 0x46AD, (ushort)displacement); - var address = (uint)(0x3000 + displacement); - bus.WriteWord(address - 2, 0xAAAA); - bus.WriteLong(address, value); - bus.WriteWord(address + 4, 0xBBBB); - using var cpu = M68kCoreFactory.Default.Create(model, bus); - cpu.Reset(0x1000, 0x7000); - cpu.State.A[5] = 0x3000; - cpu.State.StatusRegister = 0x271F; - cpu.ExecuteInstruction(); - Assert.Equal(~value, bus.ReadLong(address)); - Assert.Equal(0xAAAA, bus.ReadWord(address - 2)); - Assert.Equal(0xBBBB, bus.ReadWord(address + 4)); - Assert.Equal(value == 0 ? 0x2718 : value == 0xFFFFFFFF ? 0x2714 : 0x2710, cpu.State.StatusRegister); - Assert.Equal(0x1004u, cpu.State.ProgramCounter); - Assert.Equal(0x3000u, cpu.State.A[5]); - } - } - [Theory] [InlineData(M68kCpuModel.M68020)] [InlineData(M68kCpuModel.M68EC020)] diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 040c680b..4e77cddf 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1471,3 +1471,28 @@ replays. WB1 validity, read transfer type, broader aliases/user-mode entry and physical pipelines remain unqualified. The refreshed broad Basic replay still has two mismatching directories. See [the MOVE16 record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#040-move16-physical-line-write-recovery--2026-10-08). Milestone 6 remains in progress; nothing is published. + + +The ordinary gate requires 3,072 `logical-not-displacement-captured` cases for +each of the eight profiles. Captured long-NOT values 0 / FFFFFFFF / 92345678 and +signed displacements -8/+8 are extended across A0..A7, user/supervisor stacks and +all 32 CCRs. Shared operand fixtures and architectural verification check the +result, flags, untouched registers/memory, extension length and following NOP. +An isolated lost-X mutation fails all four old HDF rows and exactly 12,288 shared +cases. Their exact retirement leaves 71 sibling HDF rows passing alongside eight +replacement batches. Nine altered-evidence controls reject. This test-only +slice retains the same 37 CPU inputs and accepted full/.77 consumer evidence; +the composed report-only ordinary gate passes 86,581,610 cases / 759 batches. + +Reproduce before retirement with `--prepare`, apply the exact scoped removal, +then use `--finish-retirement`; an existing complete result supports strict replay: + +```powershell +python ./scripts/test-copper68k-not-displacement-retirement.py --output artifacts/not-consolidation +python ./scripts/test-copper68k-not-displacement-retirement.py --output artifacts/not-consolidation --validate-only +``` + +Use a fresh directory for execution. Missing fixtures, empty/wrong selections, +changed source or defect, producer, command, case weights, reasons and counters +fail verification. This does not qualify timing or bus ordering. See +[the NOT retirement record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#not-displacement-semantic-regression-consolidation--2026-10-08). diff --git a/Copper68k.Tests/Synthetic/SyntheticNotDisplacementTests.cs b/Copper68k.Tests/Synthetic/SyntheticNotDisplacementTests.cs new file mode 100644 index 00000000..959410ef --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticNotDisplacementTests.cs @@ -0,0 +1,42 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticNotDisplacementTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void LongNotDisplacementPreservesExtendAndSurroundingMemory(string modelId) + { + // M68000PM NOT: complement the selected operand, preserve X, + // derive N/Z from the result, and clear V/C. Captured old operands + // and signed displacements are extended across registers and CCRs. + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "logical-not-displacement-captured"); + foreach (var value in new uint[] { 0, 0xffffffff, 0x92345678 }) + foreach (short displacement in new short[] { -8, 8 }) + for (var register = 0; register < 8; register++) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + machine.Reset(ccr, supervisor); + if (register == 7) machine.Core.State.SetActiveStackPointer(0x3000); + else machine.Core.State.A[register] = 0x3000; + var opcode = (ushort)(0x46a8 | register); + if (register == 5) Assert.Equal(0x46ad, opcode); + var operand = new OperandFixture(machine, [opcode], new(5, register), 4, value, + source: false, options: new(DestinationDisplacement: displacement)); + Assert.Equal(unchecked((uint)(0x3000 + displacement)), operand.Address); + Assert.Equal(0x1004u, operand.NextPc); + var expected = operand.Expected; + var result = ~value; + operand.Write(result); + expected.Sr = (ushort)((expected.Sr & 0xfff0) | + (result == 0 ? 4 : 0) | ((result & 0x80000000) != 0 ? 8 : 0)); + SyntheticExecution.Run(machine, expected, report, + $"{modelId}/NOT/4/d16(A{register})/captured/super={supervisor}/op={opcode:X4}/v={value:X8}/disp={displacement}/ccr={ccr:X2}"); + } + report.Complete(output); + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 49f20050..96e076a5 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -12051,3 +12051,65 @@ Evidence is local under `%TEMP%/copper68k-reference-restoration-20261006/`: | manual | `c426a8429ca15f342b31c8eb5f407b442e383d1e48f3c7be4aa649ad30c8ef82` | | complete source/evidence link | `093cec2ced250c78676790816de51aebde285f111367784da86206912dd8bf8f` | | current broad Basic replay | `d8c4e6f6e7a94e07c6f76cb9b66f1d1ddcb1672dd2195c328171428ee1642d90` | + + +## NOT displacement semantic regression consolidation — 2026-10-08 + +Retire exactly four model rows of +`M68020HdfBootTests.NotLongDisplacementUpdatesOnlyTheLongAndPreservesExtend`, +pinned at `3dd2640cd5f112a42efbb51a42847d830eb000e4`. They cover NOT.L d16(A5) +with values 0, FFFFFFFF and 92345678, displacements -8/+8 and CCR 31. The shared +`SyntheticNotDisplacementTests.LongNotDisplacementPreservesExtendAndSurroundingMemory` +contains those architectural combinations and expands to all eight profiles, +A0..A7, user/supervisor stacks and all 32 CCRs: **24,576 cases / eight batches**. +The shared operand fixture chooses the address independently, validates the fixed +46AD example and exact four-byte instruction length, surrounds the operand with +canaries and checks all registers, defined flags, memory and a following NOP. +No production arithmetic, EA or timing helper computes expected results. + +Expectations follow [M68000PM NOT, printed 4-148/149](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf): +complement the long operand, preserve X, derive N/Z, clear V/C. The isolated +intentional lost-X mutation changes only the base displacement-long unary path +and advanced long-displacement NOT path. All four pinned legacy rows fail at the +same flag defect; exactly **12,288 shared cases fail**, the X-set half. The other +12,288 pass. Clean pre-retirement execution passes **83 tests** (75 old HDF rows +and eight replacement batches); exact retirement passes **79 tests** (71 retained +sibling rows and eight batches). No source CPU is changed and no other old method +is removed. Nine controls reject missing fixtures, changed producers, empty or +missing-witness selections, wrong case weights/reasons, an altered mutation and +manifest, wrong execution command and wrong TRX counters. + +The maintained ordinary gate requires all eight 3,072-case reports. +Missing/incomplete reports reject and complete composed evidence passes +**86,581,610 logical cases / 759 batches / 959 profile reports**. These copied +report controls execute no instructions. Original report hashes remain intact; +hardlinked control targets are unlinked before alteration. The first control +producer stopped on an incorrect assertion of the PowerShell declaration syntax; +that failed output is retained and the corrected producer uses a fresh directory. + +The focused retirement snapshot has **222 source inputs**, including the added +fixture and reduced legacy file. Its **37 production CPU inputs are identical** +to the accepted MOVE16 full audit and `.77` package. The retained full audit uses +the separately recorded 221-input V3 fixture (5,326 passing / 53 unavailable, +86,749,514 cases / 951 reports / ten native presets), with its final focused V4 +expectation strengthening recorded separately. It is not claimed as a new full +execution of this 222-input retirement. The accepted `.77` consumer build/tests +and two floppy replays remain unchanged evidence. No new consumer execution, +package release, timing/cache/prefetch/bus/JIT/native retirement or physical +qualification is claimed. The two broad Basic reference mismatches and other +restoration gaps remain open. Milestone 6 remains **in progress**, +`roadmapComplete=false`. + +Reproduce with `scripts/test-copper68k-not-displacement-retirement.py` and a fresh +`--output`; `--validate-only` independently reconstructs all expected case IDs, +weights, failure reasons, source/producer inventories, pinned row outcomes and +TRX counters. `--prepare` / `--finish-retirement` support proof before exact removal. +Evidence is local in `%TEMP%/copper68k-reference-restoration-20261006/` under +`not-displacement-consolidation-v1` and `not-displacement-ordinary-controls-v2`. + +| Evidence | SHA-256 | +| --- | --- | +| retirement | `33bdf414a53cd0f5eb9244e0432ad06ecc669a235ccdb0a4aa5b8d0bbc63b431` | +| ordinary controls | `229a62670d18a8cad98cc3cc7f336b70cb712f588b12616144acb4d760efe84a` | +| retained full | `46bddaccc8921960717c6f62a3fae9eb57eaf3dade9cf100d5294b3c617d1298` | +| retained consumer | `ef4e3017ebfdf7471378dff94a430bfc0998eb9bbe7bd93dee4a16336b1d75f2` | diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index f15a06bb..3c474d78 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3819,3 +3819,21 @@ address updates qualify the synchronous policy, not physical pipelines. Broader register/alias/user-mode combinations, enabled MMU/cache and physical timing remain outside this gate. No old regression is retired. Hardware is unavailable at disputed boundaries. Milestone 6 remains **in progress**, `roadmapComplete=false`. + + +### Milestone 6 NOT displacement consolidation — 2026-10-08 + +The shared captured NOT.L displacement fixture passes **24,576 cases** across +eight profiles, every address register, user/supervisor stacks and all CCRs. +The isolated lost-X mutation fails exactly **12,288 cases** and all four pinned +legacy rows. Exact removal preserves 71 sibling HDF rows and eight replacement +batches; nine altered-evidence controls reject. All eight 3,072-case reports are +mandatory. Missing/incomplete report controls reject; the complete composed +report-only gate passes **86,581,610 cases / 759 batches / 959 reports**. + +This test-only 222-input snapshot retains all 37 production CPU inputs and the +separately recorded full/.77 consumer evidence. No new wide full-suite execution, +consumer replay, package release or specialized-regression retirement is claimed. +The [qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#not-displacement-semantic-regression-consolidation--2026-10-08) +maps the exact replacement, defect and pinned evidence. Milestone 6 remains +**in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-not-displacement-retirement.py b/scripts/test-copper68k-not-displacement-retirement.py new file mode 100644 index 00000000..8391fd52 --- /dev/null +++ b/scripts/test-copper68k-not-displacement-retirement.py @@ -0,0 +1,86 @@ +"""Pinned NOT displacement replacement: fresh isolated execution or strict replay.""" +from pathlib import Path +import itertools +import re +from copper68k_consolidation import Audit, check + +REPO = Path(__file__).resolve().parent.parent +GROUP = "logical-not-displacement-captured" + + +def mutate(path, text): + if path.endswith("M68kAdvancedTimingInterpreter.cs"): + start = text.index(" private void ExecuteNotLongAddressDisplacement(") + end = text.index(" private void ExecuteNotByteAddressDisplacement(", start) + block = text[start:end] + old = "SetMoveFlags(result, M68kOperandSize.Long);" + check(block.count(old) == 1, "Missing advanced NOT target") + return text[:start] + block.replace(old, old + "\n State.StatusRegister &= 0xffef; // intentional lost X") + text[end:] + start = text.index(" private bool ExecuteLine4Unary(") + end = text.index("\n /// ", start) + block = text[start:end] + old = "SetLogicFlags(value, size);" + check(block.count(old) == 1, "Missing base NOT target") + return text[:start] + block.replace(old, old + "\n if (mode == 5 && size == M68kOperandSize.Long) State.StatusRegister &= 0xffef; // intentional lost X") + text[end:] + + +def report(model, mode): + combinations, failures = {}, [] + for value, displacement, register, supervisor, ccr in itertools.product( + [0, 0xffffffff, 0x92345678], [-8, 8], range(8), [False, True], range(32)): + key = f"{model}/NOT/4/d16(A{register})/captured/super={supervisor}" + failed = mode == "Mutation" and bool(ccr & 16) + status = "mismatching" if failed else "passing" + counts = combinations.setdefault(key, {}) + counts[status] = counts.get(status, 0) + 1 + if failed: + result = (~value) & 0xffffffff + expected = (0x2700 if supervisor else 0x700) | (ccr & 16) | (4 if result == 0 else 0) | (8 if result & 0x80000000 else 0) + failures.append(dict(id=f"{key}/op={0x46a8 | register:04X}/v={value:08X}/disp={displacement}/ccr={ccr:02X}", status=status, + reason=f"SR expected {expected:04X}, actual {expected & ~16:04X}, mask=FFFF")) + return dict(schema=1, model=model, group=GROUP, + counts=dict(passing=3072-len(failures), mismatching=len(failures), unsupported=0, untested=0), + logicalCases=3072, xunitBatches=1, combinations=combinations, failures=failures) + + +class NotAudit(Audit): + def legacy_names(self): + names = {} + for match in re.finditer(r"public void (\w+)\(\s*([^)]*)\)", self.original): + method, signature = match.groups() + start = self.original.rfind("[Theory]", 0, match.start()) + parameters = [v.strip().split()[-1] for v in signature.split(",")] + if method == "AddLongPcDisplacementReadsMemoryFromExtensionPcAndSetsArithmeticFlags": + rows = itertools.product(["M68020", "M68EC020", "M68030", "M68040"], [-128, 128], + [(0, 1, 1, 0), (0xffffffff, 1, 0, 21), (0x7fffffff, 1, 0x80000000, 10), (0x80000000, 0x80000000, 0, 23)]) + values = [[m, str(d), *map(str, arithmetic)] for m, d, arithmetic in rows] + else: + rows = re.findall(r"\[InlineData\(([^)]*)\)\]", self.original[start:match.start()]) + values = [[v.strip().split('.')[-1] if v.strip().startswith('M68kCpuModel.') else str(int(v.strip(), 0)) for v in row.split(',')] for row in rows] + check(bool(values), "Missing pinned legacy rows") + for value in values: + check(len(value) == len(parameters), "Wrong pinned parameter inventory") + name = self.s['legacy_class'] + '.' + method + '(' + ', '.join(f'{k}: {v}' for k, v in zip(parameters, value)) + ')' + check(name not in names, "Duplicate pinned row") + names[name] = 'Passed' + check(len(names) == 75, "Incomplete pinned HDF class") + return names + + +def witness_error(name, message): + check(re.search(r"Expected:\s*10008\b", message) and re.search(r"Actual:\s*9992\b", message), "Wrong historical NOT defect") + + +def make_audit(): + return NotAudit(dict(repo=REPO, producer=Path(__file__).resolve(), pin="3dd2640cd5f112a42efbb51a42847d830eb000e4", + legacy="Copper68k.Tests/M68020HdfBootTests.cs", legacy_class="Copper68k.Tests.M68020HdfBootTests", legacy_cases=75, + witness="NotLongDisplacementUpdatesOnlyTheLongAndPreservesExtend", witness_cases=4, + fixture="Copper68k.Tests/Synthetic/SyntheticNotDisplacementTests.cs", shared_class="Copper68k.Tests.Synthetic.SyntheticNotDisplacementTests", + method="LongNotDisplacementPreservesExtendAndSurroundingMemory", group=GROUP, + mutation_files=["Copper68k/M68kCore.cs", "Copper68k/M68kAdvancedTimingInterpreter.cs"], mutate=mutate, report=report, + witness_outcome=lambda name: "Failed", witness_error=witness_error, + description="Prove NOT displacement shared coverage before exact four-row retirement.")) + + +if __name__ == "__main__": + make_audit().main() diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 3e94a66d..54f538ce 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -95,6 +95,7 @@ try { 'logical-bits'=$(if ($model -in @('68000','68010')) {104192} elseif ($model -in @('68020','68030','68040','68060','68EC020','A1200')) {104852}); 'logical-boundaries'=31808; 'logical-and-indirect-captured'=12288; + 'logical-not-displacement-captured'=3072; 'logical-invalid-operands'=13248; 'logical-unary-invalid-operands'=$(if ($model -in @('68000','68010')) {12544} elseif ($model -eq '68060') {10304} else {10432}); 'logical-bitfield-invalid-operands'=13312; From b7ccd69b5540c9bdcb79f343f17f75b63758fc87 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 13:43:23 +0300 Subject: [PATCH 153/211] Qualify private pending-write validation continuations --- Copper68k.Tests/Synthetic/README.md | 22 +++ ...SyntheticM68020MoveWriteValidationTests.cs | 163 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 88 ++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 18 ++ .../test-copper68k-move-write-validation.py | 118 +++++++++++++ 5 files changed, 409 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteValidationTests.cs create mode 100644 scripts/test-copper68k-move-write-validation.py diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 4e77cddf..321187bb 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1496,3 +1496,25 @@ Use a fresh directory for execution. Missing fixtures, empty/wrong selections, changed source or defect, producer, command, case weights, reasons and counters fail verification. This does not qualify timing or bus ordering. See [the NOT retirement record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#not-displacement-semantic-regression-consolidation--2026-10-08). + + +Private pending-write validation continuation is opt-in with +`COPPER68K_RUN_020_MOVE_WRITE_VALIDATION=1`. The maintained command requires the +complete frozen `MoveWriteFrameFaultV1` parent, uses a fresh output and supports +strict `--validate-only` replay: + +```powershell +python scripts/test-copper68k-move-write-validation.py ` + --qualified-parent-directory ` + --output +``` + +The 122,880 new cases execute integer buffer-supply/refault handlers for the four +selected validation requests before pending-write completion, retaining 476,928 +parent cases. Three mutations detect ignored input, replayed phases and wrong +fault PC. The private 39-input CPU remains unchanged; the current 37-input CPU +only compiles the fixture with both tests unavailable by default. These are +distinct source graphs. Missing fixtures, empty selections or any mismatch fail +the requested audit. Changed validation values, entry faults, pending-store +refaults, trace/interrupts and hardware frames remain unqualified. See +[the private validation record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-validation-input-and-refault--2026-10-08). diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteValidationTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteValidationTests.cs new file mode 100644 index 00000000..c12abee3 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteValidationTests.cs @@ -0,0 +1,163 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Bounded private C023 validation continuations. Literal integer handlers +// supply a rejected validation value or explicitly refault before completion. +// Completed operand writes and earlier validation reads never repeat. +public sealed class SyntheticM68020MoveWriteValidationTests(ITestOutputHelper output) +{ + private static readonly ModelSpec[] Models = ModelSpec.All.Where(x => x.Id is "68EC020" or "68020" or "68030" or "A1200").ToArray(); + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_VALIDATION", "require private pending-write validation continuation"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarWriteValidationContinuation() => Audit(false); + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_VALIDATION", "require private pending-write validation continuation"), Trait("Suite", "ReferenceDiscovery")] + public void BatchWriteValidationContinuation() => Audit(true); + + private static (uint Offset, int Width)[] Requests(int count) => + new (uint, int)[] { (0, 2), (2, 4), (6, 2), (30, 2), (8, 2), (10, 2), (16, 4), (20, 2), (22, 2), (24, 4), (30, 2) } + .Concat(Enumerable.Range(0, count).Select(n => (n == 0 ? 12u : n == 1 ? 14u : 28u, 2))) + .Concat(Enumerable.Range(0, 10).Select(n => (12u + (uint)n * 2, 2))).ToArray(); + + private void Audit(bool batch) + { + var failures = new List(); + foreach (var model in Models) + foreach (var width in new[] { 1, 2, 4 }) + foreach (var current in new[] { "ISP", "MSP" }) + foreach (var count in Enumerable.Range(0, 4)) + { + var report = new CoverageBatch(model.Id, $"move-write-validation-{width}-{current}-count{count}-{(batch ? "batch" : "scalar")}"); + var bus = new FaultBus(); var m = new SyntheticMachine(model, bus); var requests = Requests(count); + foreach (var returned in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var location in Enumerable.Range(0, 2)) + foreach (var ccr in new[] { 0, 31 }) + foreach (var software in new[] { false, true }) + foreach (var repairMode in new[] { "buffer", "refault" }) + foreach (var ri in Enumerable.Range(0, 4)) + foreach (var lane in Enumerable.Range(0, requests[ri].Width)) + { + var id = $"{model.Id}/MOVE/write-frame-fault/width={width}/current={current}/returned={returned}/location={location}/count={count}/ccr={ccr}/software={software}/mode={repairMode}/request={ri}/byte={lane}"; + try { Run(m, bus, batch, width, current, returned, location, count, ccr, software, requests, ri, lane, repairMode); report.Record(id, "passing", null); } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException) { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + try { report.Complete(output); } catch (Exception ex) { failures.Add(ex); } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(x => x.Message))); + } + + private static void Run(SyntheticMachine m, FaultBus bus, bool batch, int width, string current, string returned, int location, int count, int ccr, bool software, (uint Offset, int Width)[] requests, int ri, int lane, string repairMode) + { + bus.Disarm(); m.Reset(); _ = SyntheticExecution.Prepare(m, [0x4e73, 0x4e71]); + var frame = location == 0 ? 0x6000u : 0x12346000u; + var saved = (ushort)(0x700 | ccr | (returned is "ISP" or "MSP" ? 0x2000 : 0) | (returned is "user-M" or "MSP" ? 0x1000 : 0)); + var currentSr = (ushort)((current == "ISP" ? 0x2700 : 0x3700) | ccr); + var value = 0x80a1b2c3u & (width == 1 ? 0xffu : width == 2 ? 0xffffu : uint.MaxValue); + for (uint a = frame - 100; a < frame + 40; a++) m.InitializePhysical(a, 0xa5, 1); + for (uint a = 0x43f8; a < 0x4510; a++) m.InitializePhysical(a, 0xa5, 1); + m.InitializePhysical(0x9020, 0x4e73, 2); m.InitializePhysical(0x9022, 0x4e71, 2); + m.InitializePhysical(0x2000, 0x7e2a, 2); m.InitializePhysical(0x2002, 0x7c2b, 2); m.InitializePhysical(0x2004, 0x7a2c, 2); m.InitializePhysical(0x2006, 0x4e71, 2); + m.InitializePhysical(frame, saved, 2); m.InitializePhysical(frame + 2, 0x2000, 4); m.InitializePhysical(frame + 6, 0xa008, 2); m.InitializePhysical(frame + 8, 0xc023, 2); + m.InitializePhysical(frame + 10, (uint)((software ? 0 : 0x100) | (width == 1 ? 0x10 : width == 2 ? 0x20 : 0) | 5), 2); + m.InitializePhysical(frame + 12, 0x7e11, 2); m.InitializePhysical(frame + 14, 0x7c22, 2); m.InitializePhysical(frame + 16, 0x4400, 4); + m.InitializePhysical(frame + 20, (uint)(width == 1 ? 0x129f : width == 2 ? 0x329f : 0x229f), 2); + m.InitializePhysical(frame + 22, (uint)(1 | count << 8), 2); m.InitializePhysical(frame + 24, value, 4); m.InitializePhysical(frame + 28, 0x7a33, 2); m.InitializePhysical(frame + 30, 0, 2); + if (software) m.InitializePhysical(0x4400, value, width); + m.Core.State.A[0] = 0x4204; m.Core.State.A[1] = 0x4500; + m.Core.State.SetUserStackPointer(0x7800); m.Core.State.SetInterruptStackPointer(current == "ISP" ? frame : 0x4700); m.Core.State.SetMasterStackPointer(current == "MSP" ? frame : 0x7400); + m.Core.State.StatusRegister = currentSr; m.Core.State.SetActiveStackPointer(frame); + var supplied = ri switch { 0 => (uint)saved, 1 => 0x2000u, 2 => 0xa008u, _ => 0u }; + ushort[] handler = repairMode == "buffer" + ? [0x2f7c, (ushort)(supplied >> 16), (ushort)supplied, 0x002c, 0x026f, 0xfeff, 0x000a, 0x4e73] + : [0x4e73]; + for (var n = 0; n < handler.Length; n++) m.InitializePhysical(0x9020u + (uint)n * 2, handler[n], 2); + var e = ArchitecturalExpectation.Capture(m); var serial = m.Core.State.ExceptionSequence; + var request = requests[ri]; var byteOffset = request.Offset + (uint)lane; + var occurrence = requests.Take(ri + 1).Count(x => x.Width == request.Width && byteOffset >= x.Offset && byteOffset < x.Offset + (uint)x.Width); + m.Bus.Accesses.Clear(); bus.Arm(m.Model.Physical(frame + byteOffset), request.Width, occurrence); + Step(m, batch); + if (bus.Rejected.Count != 1 || bus.Rejected[0] != (m.Model.Physical(frame + request.Offset), request.Width)) throw new InvalidOperationException("Wrong selected frame fault request"); + CheckReads(m, frame, requests.Take(ri)); + var outer = frame - 92; + for (uint n = 0; n < 92; n += 2) e.Write(outer + n, 0, 2, m.Model); + e.Write(outer, currentSr, 2, m.Model); e.Write(outer + 2, 0x1000, 4, m.Model); e.Write(outer + 6, 0xb008, 2, m.Model); e.Write(outer + 8, 0xc021, 2, m.Model); + e.Write(outer + 10, (uint)(0x145 | (request.Width == 2 ? 0x20 : 0)), 2, m.Model); e.Write(outer + 16, frame + request.Offset, 4, m.Model); e.Write(outer + 20, frame, 4, m.Model); + e.Write(outer + 28, (uint)((ri >= 1 ? saved : 0) << 16) | (ri >= 3 ? 0xa008u : 0), 4, m.Model); e.Write(outer + 32, ri >= 2 ? 0x2000u : 0, 4, m.Model); + e.Write(outer + 40, (uint)(ri == 3 ? 4 : ri), 2, m.Model); + e.A[7] = outer; if (current == "MSP") e.MasterStackPointer = outer; + e.Pc = 0x9020; e.ExceptionVector = 2; Check(m, e); CheckBanks(m, current, outer); + if (m.Core.State.ExceptionSequence != serial + 1 || !m.Bus.Accesses.Where(a => a.Write).Select(a => (a.Address, a.Width)).SequenceEqual(Enumerable.Range(0, 46).Select(n => (m.Model.Physical(frame - 2u - (uint)n * 2), 2)))) + throw new InvalidOperationException("Validation fault changed exception count or entry writes"); + + if (repairMode == "buffer") + { + e.Write(outer + 44, supplied, 4, m.Model); e.Pc += 8; + e.Sr = (ushort)((e.Sr & 0xfff0) | (supplied == 0 ? 4 : 0) | (supplied >= 0x80000000 ? 8 : 0)); + Step(m, batch); Check(m, e); CheckBanks(m, current, outer); + e.Write(outer + 10, (uint)(0x45 | (request.Width == 2 ? 0x20 : 0)), 2, m.Model); + e.Pc += 6; e.Sr = (ushort)(e.Sr & 0xfff0); + Step(m, batch); Check(m, e); CheckBanks(m, current, outer); + } + else + { + bus.Repeat(); var beforeRefault = m.Bus.Accesses.Count; + Step(m, batch); Check(m, e); CheckBanks(m, current, outer); + if (bus.Rejected.Count != 2 || m.Core.State.ExceptionSequence != serial + 2) + throw new InvalidOperationException("Explicit validation RTE did not refault exactly once"); + CheckReads(m, frame, Array.Empty<(uint Offset, int Width)>(), beforeRefault); + } + var start = m.Bus.Accesses.Count; + var isp = current == "ISP" ? frame + 32 : 0x4700u; var msp = current == "MSP" ? frame + 32 : 0x7400u; + e.A[7] = returned == "ISP" ? isp : returned == "MSP" ? msp : 0x7800u; + e.InactiveStackPointer = returned is "ISP" or "MSP" ? 0x7800u : isp; e.MasterStackPointer = msp; + e.Sr = saved; e.Pc = 0x2000; e.Write(0x4400, value, width, m.Model); + Step(m, batch); Check(m, e); CheckBanks(m, current, frame + 32); + CheckReads(m, frame, requests.Skip(ri + (repairMode == "buffer" ? 1 : 0)), start); + if (!m.Bus.Accesses.Skip(start).Where(a => a.Write).Select(a => (a.Address, a.Width)).SequenceEqual(software ? Array.Empty<(uint, int)>() : new[] { (0x4400u, width) })) + throw new InvalidOperationException("Recovered validation fault repeated or misplaced the pending write"); + for (var n = 0; n < 3; n++) + { + e.D[7 - n] = (uint)(n < count ? new[] { 0x11, 0x22, 0x33 }[n] : 0x2a + n); e.Sr = (ushort)(e.Sr & 0xfff0); e.Pc += 2; + var before = m.Bus.Accesses.Count; Step(m, batch); Check(m, e); CheckBanks(m, current, frame + 32); + if (m.Bus.Accesses.Skip(before).Count(a => !a.Write && a.Kind == M68kBusAccessKind.CpuInstructionFetch && a.Address == 0x2000u + (uint)n * 2) != (n < count ? 0 : 1)) + throw new InvalidOperationException("Recovered frame fault changed retained instruction words"); + } + if (m.Core.State.ExceptionSequence != serial + (repairMode == "refault" ? 2u : 1u) || bus.Rejected.Count != (repairMode == "refault" ? 2 : 1) || m.Bus.Accesses.Any(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && + !(a.Address >= m.Model.Physical(outer) && a.Address + (uint)a.Width <= m.Model.Physical(frame) + 32) && a.Address != 8)) + throw new InvalidOperationException("Recovered frame fault replayed source or changed exception provenance"); + } + + private static void CheckReads(SyntheticMachine m, uint frame, IEnumerable<(uint Offset, int Width)> expected, int start = 0) + { + var reads = m.Bus.Accesses.Skip(start).Where(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address >= m.Model.Physical(frame) && a.Address < m.Model.Physical(frame) + 32).Select(a => (a.Address, a.Width)); + if (!reads.SequenceEqual(expected.Select(x => (m.Model.Physical(frame + x.Offset), x.Width)))) throw new InvalidOperationException("Frame fault replayed or skipped completed validation/load reads"); + } + private static void CheckBanks(SyntheticMachine m, string current, uint stack) + { + var isp = current == "ISP" ? stack : 0x4700u; var msp = current == "MSP" ? stack : 0x7400u; + if (m.Core.State.UserStackPointer != 0x7800 || m.Core.State.InterruptStackPointer != isp || m.Core.State.MasterStackPointer != msp) + throw new InvalidOperationException("Frame fault changed an inactive or completed stack bank"); + } + private static void Check(SyntheticMachine m, ArchitecturalExpectation e) { var error = e.Verify(m); if (error != null) throw new InvalidOperationException(error); } + private static void Step(SyntheticMachine m, bool batch) + { + if (!batch) { m.Core.ExecuteInstruction(); return; } + var b = new Boundary(); if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, b) != 1 || b.Before != 1 || b.After != 1) throw new InvalidOperationException("Frame fault batch boundaries differ"); + } + private sealed class Boundary : IM68kInstructionBoundary { public int Before, After; public bool BeforeInstruction() { Before++; return true; } public void AfterInstruction(long a, long b) => After++; } + private sealed class FaultBus : SparseRecordingBus, IM68kPhysicalAddressMap + { + private uint address; private int width, occurrence; + internal readonly List<(uint Address, int Width)> Rejected = []; + internal void Disarm() { occurrence = 0; Rejected.Clear(); } + internal void Arm(uint at, int size, int nth) { Disarm(); address = at; width = size; occurrence = nth; } + internal void Repeat() => occurrence = 1; + public bool IsCpuPhysicalAddressMapped(uint at, int size, M68kBusAccessKind kind) + { + if (occurrence == 0 || kind != M68kBusAccessKind.CpuDataRead || size != width || unchecked(address - at) >= size) return true; + if (--occurrence != 0) return true; + Rejected.Add((at, size)); return false; + } + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 96e076a5..039a2310 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -12113,3 +12113,91 @@ Evidence is local in `%TEMP%/copper68k-reference-restoration-20261006/` under | ordinary controls | `229a62670d18a8cad98cc3cc7f336b70cb712f588b12616144acb4d760efe84a` | | retained full | `46bddaccc8921960717c6f62a3fae9eb57eaf3dade9cf100d5294b3c617d1298` | | retained consumer | `ef4e3017ebfdf7471378dff94a430bfc0998eb9bbe7bd93dee4a16336b1d75f2` | + + +## Private pending-write validation input and refault — 2026-10-08 + +`SyntheticM68020MoveWriteValidationTests` adds **122,880 passing programs / two +executions / 192 reports** on EC020, A1200, 020 and 030. The fixture begins with +the literal private C023 format-A pending-write image at low or high relocated +addresses. It rejects each byte of the four selected RTE validation requests: +saved SR, PC, format and private version. Actual integer handlers either write +the original validation value into the right-justified C021 input buffer and +clear DF, or execute RTE to refault that validation request once before allowing +completion. The two modes each contain **61,440 programs**. + +Coverage spans B/W/L pending stores, current ISP/MSP, returned user/user-M/ISP/MSP, +CCR 0/31, zero-to-three retained instruction words and pending/software-completed +stores, in scalar and one-instruction batch execution. Each handler instruction +checks registers, defined flags, PC, memory guards and stack banks. Successful +return checks exact validation-read ordering, one additional exception for +explicit refault, serialized pending-write completion, unchanged source/address +effects and three following MOVEQ sentinels. Supplying a validation value skips +only that rejected request; later legitimate frame reads still occur. Refault +does not replay completed validation phases. This is explicit RTE continuation, +not automatic retry of the original MOVE. + +All **476,928 parent cases / six executions / 1,152 reports** retain identical +results. The combined candidate has **599,808 cases / eight executions / 1,344 +reports**. The complete **239-input** frozen source graph differs from the +238-input parent only by this fixture; all **39 private CPU inputs remain +byte-identical**. The maintained audit revalidates the complete parent chain, +selection/settings, source and output hashes, loaded test-assembly path, exact +TRX roster/counters, every case key/weight/status and all retained reports. +A frozen `--validate-only` replay passes without changing the proof. + +Three isolated mutations fail at exactly predicted cases: ignoring supplied +input **61,440**; replaying completed phases during refault **49,152**; wrong saved +fault PC **122,880**. Unaffected cases pass. A separate independent verifier +checks all **233,472 complete diagnostic messages**, including the exact wrong-PC +byte address under each model's transport width. Eleven copied controls reject +missing reports, empty combinations, untested/foreign cases, changed source, +roster or settings, missing witnesses, changed/missing retention and wrong loaded +test-assembly identity. Validator fixture paths are explicitly rebased, including +TRX storage paths; no copied control executes instructions. Hardlinked targets +are unlinked before alteration and original evidence hashes remain intact. + +An isolated **223-input / 37-CPU-input** current production build also compiles +the fixture. With the private flag absent, the exact two new tests are unavailable +and execute no cases. The first default-selection verifier wrongly expected +xUnit skips in the TRX aggregate `notExecuted` counter; it stopped with its +failure preserved. The successor binds the compiled source/assemblies, reruns +the exact selection with validated build prerequisites, verifies both explicit +`NotExecuted` rows and records the actual aggregate counters (total 2, executed +0, passed/failed/notExecuted 0). This is unavailable coverage, not successful +production replay. + +Reproduce with the complete immutable `MoveWriteFrameFaultV1` parent and its +pinned parent chain, using a fresh output: + +```powershell +python scripts/test-copper68k-move-write-validation.py ` + --qualified-parent-directory ` + --output +# --validate-only requires unchanged complete sources, settings and reports. +``` + +This qualifies resupply of the original validation value and one explicit +validation refault under the bounded private contract. Changed input values, +validation/exception-entry failures, returned-bank pending-write refaults, +trace/interrupt continuation, wider origins/physical partial transfers and +foreign hardware frames remain required. The fixture does not qualify the +silicon representation of C021/C023 or resolve hardware trace/A7/PC disputes. +No production CPU is changed or imported, no package published or old regression +retired. No new wide CPU suite, consumer replay or physical qualification is +claimed. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +Evidence is local under `%TEMP%/copper68k-reference-restoration-20261006/`: +`audits/MoveWriteValidationDiscoveryV1`, `audits/MoveWriteValidationV1`, +`move-write-validation-controls-v1`, `write-validation-production-default-v1` +and `write-validation-production-default-v2`. + +| Evidence | SHA-256 | +| --- | --- | +| audit | `3a2d2b3c8f82f4205224819a3ba82ba2d6066958881b9649d6153f88366cbf64` | +| controls | `04184f1c3d7418eb5ad675ae2f915f298f52ca43c9ce32b94850d77563875790` | +| reasons | `c3a10e52b3ec0b8562db6d3ad93676e5aae51a4f661dd3f17e5b25360144f900` | +| default | `96449a7c17e272b8797c7f1d3855d1ba8c4197a5c203acd9d711ff0fd9d664cd` | +| discovery | `a03c2242d9c6010c4d87d8309edb9502f5881dac5f7c7c92a9f25f8a55334c0c` | +| parent | `93ec2beed12410b94ce6cf448e1b5bc67e010474edf5ed7b20b2b7a765ae123e` | +| complete source/evidence linkage | `f0a6174fcbfdfe595ea48efdb0578f40a36caaa1616a9c3ccce773e6709523ba` | diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 3c474d78..a46b6b62 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3837,3 +3837,21 @@ consumer replay, package release or specialized-regression retirement is claimed The [qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#not-displacement-semantic-regression-consolidation--2026-10-08) maps the exact replacement, defect and pinned evidence. Milestone 6 remains **in progress**, `roadmapComplete=false`. + + +### Milestone 6 private pending-write validation continuation — 2026-10-08 + +The selected C023/C021 software contract adds **122,880 passing programs** for +original-value validation input and one explicit refault, with integer handlers, +exact read ordering, pending-store completion and following sentinels. All +**476,928 parent cases** retain their reports. Three isolated defects produce +**61,440 / 49,152 / 122,880** exact mismatches; all 233,472 diagnostic reasons are +independently checked. Eleven evidence controls reject and frozen strict replay +passes. The private 239-input graph preserves all 39 CPU inputs; the current +223-input production graph compiles the fixture with both new tests unavailable. + +No production candidate is imported, package published or regression retired. +Changed validation values, entry failures, pending-write refaults, trace/interrupt +continuation and general hardware frames remain open. Exact graph/evidence limits +are in [the qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-validation-input-and-refault--2026-10-08). +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-move-write-validation.py b/scripts/test-copper68k-move-write-validation.py new file mode 100644 index 00000000..fa6c54a7 --- /dev/null +++ b/scripts/test-copper68k-move-write-validation.py @@ -0,0 +1,118 @@ +"""Qualify supplied validation input and explicit refault of private C023 writes. + +Requires the complete immutable MoveWriteFrameFaultV1 parent. No private CPU +import, silicon-frame qualification, or package publication is performed. +""" +import argparse, contextlib, hashlib, importlib.util, io, itertools, json, os, re, subprocess +from pathlib import Path +import xml.etree.ElementTree as ET +REPO = Path(__file__).resolve().parents[1] +HELPER = REPO/'scripts/test-copper68k-move-write-frame-fault.py' +spec = importlib.util.spec_from_file_location('write_fault', HELPER) +m = importlib.util.module_from_spec(spec);spec.loader.exec_module(m) +sha,load,save,require,inventory=m.sha,m.load,m.save,m.require,m.inventory +FIXTURE='Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteValidationTests.cs' +CPU='Copper68k/M68kAdvancedTimingInterpreter.Rte020.cs' +PARENT_PROOF='93ec2beed12410b94ce6cf448e1b5bc67e010474edf5ed7b20b2b7a765ae123e' +VARIANTS=['candidate','ignored-input','replay-phase','fault-pc'] +FILTER='FullyQualifiedName~SyntheticM68020MoveWriteValidationTests' +ENV={'COPPER68K_RUN_020_MOVE_WRITE_VALIDATION':'1','COPPER68K_RUN_020_MOVE_WRITE_FRAME_FAULT':'1','COPPER68K_RUN_020_MOVE_WRITE_FRAME':'1'} +NS=m.NS + + +def sources(parent,variant): + prior=load(parent/'candidate/execution.json')['sources'] + require(len(prior)==238 and FIXTURE not in prior,'Incomplete validation parent') + data={n:(parent/'candidate/source'/n).read_bytes() for n in prior} + require(all(hashlib.sha256(v).hexdigest()==prior[n] for n,v in data.items()),'Changed validation parent sources') + data[FIXTURE]=(REPO/FIXTURE).read_bytes() + text=data[CPU].decode('utf-8').replace('\r\n','\n') + old='ExecuteRte020(context, (ssw & 0x100) == 0 ? input : null);' + if variant=='ignored-input':text=m.m.p.once(text,old,'ExecuteRte020(context, null);') + elif variant=='replay-phase':text=m.m.p.once(text,old,'if ((ssw & 0x100) != 0) context.Phase = 0; '+old) + elif variant=='fault-pc':text=m.m.p.once(text,'words[0] = sr; Long(2, context.InstructionPc);','words[0] = sr; Long(2, unchecked(context.InstructionPc + 2));') + if variant!='candidate':data[CPU]=text.encode('utf-8') + return data + + +def command(root,variant): + selection=FILTER+('|'+m.FILTER+'|'+m.m.FILTER if variant=='candidate' else '') + return ['dotnet','test',str(root/'source/Copper68k.Tests/Copper68k.Tests.csproj'),'-c','Release','--artifacts-path',str(root/'build'),'--filter',selection,'--logger','trx;LogFileName=audit.trx','--results-directory',str(root)] + + +def expected(model,width,current,count,variant): + rows={} + for returned,location,ccr,software,mode,request in itertools.product(['user','user-M','ISP','MSP'],range(2),[0,31],[False,True],['buffer','refault'],range(4)): + bad=variant=='fault-pc' or variant=='ignored-input' and mode=='buffer' or variant=='replay-phase' and mode=='refault' and request>0 + for byte in range(4 if request==1 else 2): + key=f'{model}/MOVE/write-frame-fault/width={width}/current={current}/returned={returned}/location={location}/count={count}/ccr={ccr}/software={software}/mode={mode}/request={request}/byte={byte}' + rows[key]={'mismatching' if bad else 'passing':1} + require(len(rows)==640,'Wrong validation inventory') + return rows + + +def verify(root,variant,data,retained,parent_names): + e=load(root/'execution.json');ids={n:hashlib.sha256(v).hexdigest() for n,v in data.items()} + require(e['sources']==ids==inventory(root/'source'),'Changed complete validation source inventory') + require(e['command']==command(root,variant) and e['settings']==ENV and e['exit']==(0 if variant=='candidate' else 1),'Wrong validation command, settings or exit') + outputs=['execution.log','audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + require(e['outputs']=={n:sha(root/n) for n in outputs},'Changed validation outputs') + tree=ET.parse(root/'audit.trx');tests=tree.findall('.//t:UnitTestResult',NS) + names={f'{route}WriteValidationContinuation':'Passed' if variant=='candidate' else 'Failed' for route in ['Scalar','Batch']} + if variant=='candidate':names.update(parent_names) + require(len(tests)==len(names) and {x.get('testName').split('.')[-1]:x.get('outcome') for x in tests}==names,'Wrong or empty validation selection') + counter=tree.find('.//t:Counters',NS);failed=sum(v=='Failed' for v in names.values()) + require(all(int(counter.get(k,-1))==v for k,v in dict(total=len(names),executed=len(names),passed=len(names)-failed,failed=failed,notExecuted=0).items()),'Wrong validation counters') + require(tree.find('.//t:ResultSummary',NS).get('outcome')==('Completed' if variant=='candidate' else 'Failed'),'Wrong validation outcome') + definitions={x.get('id'):x for x in tree.findall('.//t:UnitTest',NS)} + require(all(Path(definitions[x.get('testId')].get('storage')).resolve()==(root/'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll').resolve() for x in tests),'Wrong loaded validation test assembly') + stdout='\n'.join(x.text or '' for x in tree.findall('.//t:UnitTestResult/t:Output/t:StdOut',NS));reports=[];totals=dict(passing=0,mismatching=0,unsupported=0,untested=0) + def summary(d): + matches=re.findall(re.escape(d['model']+'/'+d['group']+': ')+r'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)',stdout) + require(len(matches)==1 and json.loads(matches[0][0])==d['counts'] and int(matches[0][1])==d['logicalCases'] and int(matches[0][2])==len(d['combinations']),'Validation report/TRX disagreement') + for model,width,current,count,route in itertools.product(m.m.MODELS,[1,2,4],['ISP','MSP'],range(4),['scalar','batch']): + group=f'move-write-validation-{width}-{current}-count{count}-{route}';path=root/f'{model}-{group}.json';d=load(path);wanted=expected(model,width,current,count,variant) + counts={s:sum(v.get(s,0) for v in wanted.values()) for s in totals} + require(d['schema']==1 and d['model']==model and d['group']==group and d['logicalCases']==640 and d['xunitBatches']==1 and d['counts']==counts and d['combinations']==wanted,'Wrong validation combinations: '+path.name) + bad={n for n,v in wanted.items() if 'mismatching' in v} + require(len(d['failures'])==len(bad) and {x['id'] for x in d['failures']}==bad and all(x['status']=='mismatching' and x['reason'] for x in d['failures']),'Wrong complete validation failure witnesses') + summary(d);reports.append(dict(path=path.name,sha256=sha(path))) + for s in totals:totals[s]+=counts[s] + if variant=='candidate': + require(len(retained)==1152,'Incomplete validation retention') + for name,wanted in retained.items(): + d=load(root/name);require(d==wanted,'Changed retained validation-parent report: '+name);summary(d);reports.append(dict(path=name,sha256=sha(root/name))) + require({x.name for x in root.glob('*.json')}=={x['path'] for x in reports}|{'execution.json'},'Missing or foreign validation reports') + return dict(variant=variant,counts=totals,retentionCases=476928 if variant=='candidate' else 0,executions=len(names),reports=reports,executionSha256=sha(root/'execution.json')) + + +def audit(parent,output,validate): + require(sha(parent/'proof.json')==PARENT_PROOF,'Wrong validation parent proof') + with contextlib.redirect_stdout(io.StringIO()):m.audit(parent.parent/'MoveWriteFrameV1',parent,True) + linkage=dict(producers={f.relative_to(REPO).as_posix():sha(f) for f in [Path(__file__),HELPER,REPO/FIXTURE]},parentProofSha256=PARENT_PROOF,variants=VARIANTS,settings=ENV) + if not validate:output.mkdir(parents=True,exist_ok=False);save(output/'inputs.json',linkage) + require(load(output/'inputs.json')==linkage,'Changed validation producers or selection') + retained={f.name:load(f) for f in (parent/'candidate').glob('*-move-write-*.json')} + tree=ET.parse(parent/'candidate/audit.trx');parent_names={x.get('testName').split('.')[-1]:x.get('outcome') for x in tree.findall('.//t:UnitTestResult',NS)} + require(len(parent_names)==6 and set(parent_names.values())=={'Passed'},'Incomplete validation parent roster') + entries=[] + for variant in VARIANTS: + root=output/variant;data=sources(parent,variant) + if not validate: + (root/'source').mkdir(parents=True) + for n,v in data.items(): + f=root/'source'/n;f.parent.mkdir(parents=True,exist_ok=True);f.write_bytes(v) + env={k:v for k,v in os.environ.items() if not k.startswith('COPPER68K_RUN_')};env.update(ENV);env['COPPER68K_SYNTHETIC_REPORT_DIR']=str(root) + with (root/'execution.log').open('w',encoding='utf-8') as log:done=subprocess.run(command(root,variant),env=env,stdout=log,stderr=subprocess.STDOUT) + outputs=['execution.log','audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + save(root/'execution.json',dict(command=command(root,variant),settings=ENV,exit=done.returncode,sources={n:hashlib.sha256(v).hexdigest() for n,v in data.items()},outputs={n:sha(root/n) for n in outputs})) + entry=verify(root,variant,data,retained,parent_names);entries.append(entry);print(variant,entry['counts'],'retention',entry['retentionCases'],flush=True) + proof=dict(schema=1,inputsSha256=sha(output/'inputs.json'),entries=entries,sourceCount=239,productionImport=False,publication=False,hardwareQualified=False,architecturalPromotion=False,roadmapComplete=False) + if validate:require(load(output/'proof.json')==proof,'Changed complete validation proof') + else:save(output/'proof.json',proof) + print('Complete private write-validation audit',sha(output/'proof.json'),flush=True) + + +if __name__=='__main__': + parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--qualified-parent-directory',type=Path,required=True);parser.add_argument('--output',type=Path,required=True);parser.add_argument('--validate-only',action='store_true') + args=parser.parse_args();audit(args.qualified_parent_directory.resolve(),args.output.resolve(),args.validate_only) From af645623648f15a6112d0f8305c2b9881a580be2 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 14:01:02 +0300 Subject: [PATCH 154/211] Qualify private validation exception-entry faults --- Copper68k.Tests/Synthetic/README.md | 22 +++ ...SyntheticM68020MoveWriteEntryFaultTests.cs | 127 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 83 ++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 19 +++ .../test-copper68k-move-write-entry-fault.py | 125 +++++++++++++++++ 5 files changed, 376 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteEntryFaultTests.cs create mode 100644 scripts/test-copper68k-move-write-entry-fault.py diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 321187bb..5f71431b 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1518,3 +1518,25 @@ distinct source graphs. Missing fixtures, empty selections or any mismatch fail the requested audit. Changed validation values, entry faults, pending-store refaults, trace/interrupts and hardware frames remain unqualified. See [the private validation record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-validation-input-and-refault--2026-10-08). + + +Private validation exception-entry faults are opt-in with +`COPPER68K_RUN_020_MOVE_WRITE_ENTRY_FAULT=1`. Require the complete frozen +`MoveWriteValidationV1` parent, a fresh output, and use strict `--validate-only` +for unchanged frozen evidence: + +```powershell +python scripts/test-copper68k-move-write-entry-fault.py ` + --qualified-parent-directory ` + --output +``` + +The 51,200 new cases check secondary stack/vector faults after selected private +validation failures, exact completed accesses, saved state and halted behavior; +599,808 parent cases retain their reports. Three mutations and eleven corrupted +evidence controls fail as expected. This bounded software contract preserves all +39 private CPU inputs. Current production compiles the fixture with two tests +unavailable by default; it uses a distinct 37-input CPU graph. Missing fixtures, +empty selection or mismatches fail the requested audit. Wider pending forms, +changed validation values, pending-store refaults, trace/interrupts and hardware +frames remain unqualified. See [the entry-fault qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-validation-exception-entry--2026-10-08). diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteEntryFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteEntryFaultTests.cs new file mode 100644 index 00000000..0deb816a --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteEntryFaultTests.cs @@ -0,0 +1,127 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Secondary entry faults of the bounded C021/C023 software continuation. +// Enumerate all entry bytes separately from the all-CCR canonical group. +public sealed class SyntheticM68020MoveWriteEntryFaultTests(ITestOutputHelper output) +{ + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_ENTRY_FAULT", "require private validation entry-fault audit"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarWriteValidationEntryFaults() => Audit(false); + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_ENTRY_FAULT", "require private validation entry-fault audit"), Trait("Suite", "ReferenceDiscovery")] + public void BatchWriteValidationEntryFaults() => Audit(true); + + private void Audit(bool batch) + { + var failures = new List(); + foreach (var model in ModelSpec.All.Where(x => x.Id is "68EC020" or "68020" or "68030" or "A1200")) + foreach (var bank in new[] { "ISP", "MSP" }) + foreach (var allCcr in new[] { false, true }) + { + var kind = allCcr ? "ccr" : "enumeration"; + var report = new CoverageBatch(model.Id, $"move-write-entry-{kind}-{bank}-{(batch ? "batch" : "scalar")}"); + var bus = new EntryBus(); var m = new SyntheticMachine(model, bus); + foreach (var location in new[] { 0, 1 }) + foreach (var ccr in allCcr ? Enumerable.Range(0, 32) : new[] { 31 }) + for (var request = 0; request < 4; request++) + for (var validationByte = 0; validationByte < (request == 1 ? 4 : 2); validationByte++) + foreach (var entry in allCcr ? new[] { 0 } : Enumerable.Range(0, 47)) + for (var entryByte = 0; entryByte < (entry == 46 ? 4 : 2); entryByte++) + { + var id = $"{model.Id}/RTE/write-validation-entry/{kind}/bank={bank}/location={location}/ccr={ccr}/request={request}/byte={validationByte}/entry={entry}/entry-byte={entryByte}"; + try { Run(m, bus, batch, bank, location, ccr, request, validationByte, entry, entryByte); report.Record(id, "passing", null); } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException) { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + try { report.Complete(output); } catch (Exception ex) { failures.Add(ex); } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(x => x.Message))); + } + + private static void Run(SyntheticMachine m, EntryBus bus, bool batch, string bank, int location, int ccr, int request, int validationByte, int entry, int entryByte) + { + bus.Disarm(); m.Reset(); _ = SyntheticExecution.Prepare(m, [0x4e73, 0x4e71]); + var frame = location == 0 ? 0x6000u : 0x12346000u; + var vbr = location == 0 ? 0u : 0x12345000u; + var saved = (ushort)(0x700 | ccr); var current = (ushort)((bank == "ISP" ? 0x2700 : 0x3700) | ccr); + for (uint at = frame - 100; at < frame + 40; at++) m.InitializePhysical(at, 0xa5, 1); + for (uint at = 0x43f8; at < 0x4510; at++) m.InitializePhysical(at, 0xa5, 1); + m.InitializePhysical(vbr + 8, 0x9020, 4); + m.InitializePhysical(frame, saved, 2); m.InitializePhysical(frame + 2, 0x2000, 4); + m.InitializePhysical(frame + 6, 0xa008, 2); m.InitializePhysical(frame + 8, 0xc023, 2); + m.InitializePhysical(frame + 10, 0x125, 2); m.InitializePhysical(frame + 12, 0x7e11, 2); + m.InitializePhysical(frame + 14, 0x7c22, 2); m.InitializePhysical(frame + 16, 0x4400, 4); + m.InitializePhysical(frame + 20, 0x329f, 2); m.InitializePhysical(frame + 22, 0x301, 2); + m.InitializePhysical(frame + 24, 0xb2c3, 4); m.InitializePhysical(frame + 28, 0x7a33, 2); m.InitializePhysical(frame + 30, 0, 2); + var s = m.Core.State; s.A[0] = 0x4204; s.A[1] = 0x4500; s.VectorBaseRegister = vbr; + s.SetUserStackPointer(0x7800); s.SetInterruptStackPointer(bank == "ISP" ? frame : 0x4700); s.SetMasterStackPointer(bank == "MSP" ? frame : 0x7400); + s.StatusRegister = current; s.SetActiveStackPointer(frame); + var e = ArchitecturalExpectation.Capture(m); var serial = s.ExceptionSequence; + var requests = new (uint Offset, int Width)[] { (0, 2), (2, 4), (6, 2), (30, 2) }; + var rejected = requests[request]; var vectorFault = entry == 46; + var secondary = vectorFault ? vbr + 8 : frame - (uint)(entry + 1) * 2; + bus.Accesses.Clear(); bus.Arm(m.Model.Physical(frame + rejected.Offset + (uint)validationByte), rejected.Width, + m.Model.Physical(secondary + (uint)entryByte), vectorFault ? 4 : 2, + vectorFault ? M68kBusAccessKind.CpuDataRead : M68kBusAccessKind.CpuDataWrite); + Step(m, batch); + if (!bus.Rejected.SequenceEqual(new[] { (m.Model.Physical(frame + rejected.Offset), rejected.Width, M68kBusAccessKind.CpuDataRead), + (m.Model.Physical(secondary), vectorFault ? 4 : 2, vectorFault ? M68kBusAccessKind.CpuDataRead : M68kBusAccessKind.CpuDataWrite) })) + throw new InvalidOperationException("Wrong validation/entry fault requests"); + // Fixed private layout, independent of the production frame builder. + var words = new ushort[46]; + void Long(int offset, uint value) { words[offset / 2] = (ushort)(value >> 16); words[offset / 2 + 1] = (ushort)value; } + words[0] = current; Long(2, 0x1000); words[3] = 0xb008; words[4] = 0xc021; + words[5] = (ushort)(0x145 | (rejected.Width == 2 ? 0x20 : 0)); + Long(16, frame + rejected.Offset); Long(20, frame); + Long(28, (uint)((request >= 1 ? saved : 0) << 16) | (request >= 3 ? 0xa008u : 0)); + Long(32, request >= 2 ? 0x2000u : 0); words[20] = (ushort)(request == 3 ? 4 : request); + var completed = vectorFault ? 46 : entry; + for (var n = 0; n < completed; n++) e.Write(frame - (uint)(n + 1) * 2, words[45 - n], 2, m.Model); + var sp = vectorFault ? frame - 92 : secondary; + e.A[7] = sp; if (bank == "MSP") e.MasterStackPointer = sp; + e.Pc = 0x1002; e.Halted = true; e.ExceptionVector = 2; + e.ControlChecks["exception sequence"] = (x => x.ExceptionSequence, serial + 1); + e.ControlChecks["saved PC"] = (x => x.LastExceptionStackedProgramCounter, 0x1000); + e.ControlChecks["saved SR"] = (x => x.LastExceptionStatusRegister, current); + Check(m, e); + if (s.UserStackPointer != 0x7800 || s.InterruptStackPointer != (bank == "ISP" ? sp : 0x4700u) || + s.MasterStackPointer != (bank == "MSP" ? sp : 0x7400u) || s.VectorBaseRegister != vbr) + throw new InvalidOperationException("Entry fault changed inactive stacks or VBR"); + var reads = bus.Accesses.Where(x => !x.Write && x.Kind == M68kBusAccessKind.CpuDataRead).Select(x => (x.Address, x.Width)); + if (!reads.SequenceEqual(requests.Take(request).Select(x => (m.Model.Physical(frame + x.Offset), x.Width)))) + throw new InvalidOperationException("Entry fault replayed validation or read the pending operand"); + var writes = bus.Accesses.Where(x => x.Write).Select(x => (x.Address, x.Width, x.Value)); + if (!writes.SequenceEqual(Enumerable.Range(0, completed).Select(n => (m.Model.Physical(frame - (uint)(n + 1) * 2), 2, (uint)words[45 - n])))) + throw new InvalidOperationException("Entry fault lost/repeated a completed stack write"); + var accesses = bus.Accesses.Count; + if (batch) { if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, new Boundary()) != 0) throw new InvalidOperationException("Halted entry ran a batch instruction"); } + else m.Core.ExecuteInstruction(); + Check(m, e); + if (bus.Accesses.Count != accesses || bus.Rejected.Count != 2) throw new InvalidOperationException("Halted entry retried a bus access"); + } + private static void Check(SyntheticMachine m, ArchitecturalExpectation e) { if (e.Verify(m) is { } error) throw new InvalidOperationException(error); } + private static void Step(SyntheticMachine m, bool batch) + { + if (!batch) m.Core.ExecuteInstruction(); + else if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, new Boundary()) != 1) throw new InvalidOperationException("Empty entry-fault batch"); + } + private sealed class Boundary : IM68kInstructionBoundary { public bool BeforeInstruction() => true; public void AfterInstruction(long a, long b) { } } + private sealed class EntryBus : SparseRecordingBus, IM68kPhysicalAddressMap + { + private uint validation, secondary; private int validationWidth, secondaryWidth; + private M68kBusAccessKind secondaryKind; private int phase; + internal readonly List<(uint Address, int Width, M68kBusAccessKind Kind)> Rejected = []; + internal void Disarm() { phase = 0; Rejected.Clear(); } + internal void Arm(uint first, int firstWidth, uint second, int secondWidth, M68kBusAccessKind kind) + { Disarm(); validation = first; validationWidth = firstWidth; secondary = second; secondaryWidth = secondWidth; secondaryKind = kind; phase = 1; } + public bool IsCpuPhysicalAddressMapped(uint at, int size, M68kBusAccessKind kind) + { + if (phase == 1 && kind == M68kBusAccessKind.CpuDataRead && size == validationWidth && unchecked(validation - at) < size) + { Rejected.Add((at, size, kind)); phase = 2; return false; } + if (phase == 2 && kind == secondaryKind && size == secondaryWidth && unchecked(secondary - at) < size) + { Rejected.Add((at, size, kind)); phase = 0; return false; } + return true; + } + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 039a2310..507cb3fb 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -12201,3 +12201,86 @@ and `write-validation-production-default-v2`. | discovery | `a03c2242d9c6010c4d87d8309edb9502f5881dac5f7c7c92a9f25f8a55334c0c` | | parent | `93ec2beed12410b94ce6cf448e1b5bc67e010474edf5ed7b20b2b7a765ae123e` | | complete source/evidence linkage | `f0a6174fcbfdfe595ea48efdb0578f40a36caaa1616a9c3ccce773e6709523ba` | + + +## Private pending-write validation exception entry — 2026-10-08 + +`SyntheticM68020MoveWriteEntryFaultTests` adds **51,200 passing cases / two +executions / 32 reports** on EC020, A1200, 020 and 030. Starting from a literal +private C023 word pending-write image, it rejects each byte of the four selected +validation reads, then each byte of the 46 format-B stack-word writes or the +vector long read. The canonical enumeration group covers all 96 secondary byte +positions (**30,720 cases**); the separate all-32-CCR group covers both bytes of +the first stack write (**20,480 cases**). Current ISP/MSP, paired low/high frame +and VBR locations, and scalar/one-instruction batch execution are covered. The +saved return is user mode and the retained pipe has three words. Other pending +widths, return banks and pipe counts are not newly enumerated by this slice. + +An independently serialized fixed C021 layout checks every completed frame word +and surrounding memory, logical SP advancement before the rejected write, live +and saved PC/SR, exception count, all registers and inactive stack banks. Exact +accepted validation reads and completed descending stack writes must match their +prefixes. The pending operand remains untouched. A second halted scalar/batch +call must perform no access or additional fault request. Faults are rejected +mapped requests, not physical partial transfers or hardware BERR observations. + +All **599,808 parent cases / eight executions / 1,344 reports** retain identical +results. The combined candidate has **651,008 cases / ten executions / 1,376 +reports**. Its **240-input** frozen source graph adds only this fixture to the +239-input parent; all **39 private CPU inputs remain byte-identical**. The +maintained audit revalidates the complete pinned parent chain, exact source and +output inventories, command/settings, loaded test assembly, TRX roster/counters, +every case ID/weight/status and retained report. Strict frozen `--validate-only` +replay passes without changing the proof. + +Three isolated defects produce exactly predicted failures: missing halt +**51,200**, incorrect stack advancement **51,200**, and corrupted saved SR +**1,280**. The other **49,920** saved-SR mutation cases pass because they fault +before that word is committed. A separate verifier independently checks all +**103,680 complete diagnostic messages**, including logical SP and model-width +physical memory addresses. Eleven copied evidence controls reject missing +reports, empty/foreign/untested combinations, changed source/roster/settings, +missing witnesses, changed/missing retention and wrong loaded assembly. Copied +validator paths are explicitly rebased, hardlinked targets unlinked before +alteration, and original evidence hashes preserved. Controls execute no CPU. + +The isolated current **224-input / 37-CPU-input** production graph compiles the +fixture with both tests explicitly `NotExecuted` and zero executed cases when +private flags are absent. Actual TRX aggregate counters are total 2, executed +0, passed/failed/notExecuted 0; the exact result rows establish unavailability. +The first local producer had a syntax error before execution or output creation; +its successor uses a fresh output and passes. This is compile/unavailable +coverage, not a production instruction replay. + +Reproduce with the complete immutable `MoveWriteValidationV1` parent and its +pinned chain, using a fresh output: + +```powershell +python scripts/test-copper68k-move-write-entry-fault.py ` + --qualified-parent-directory ` + --output +# --validate-only requires unchanged complete sources, settings and reports. +``` + +This qualifies secondary stack-write/vector-read faults for the selected private +validation exception-entry contract. Changed validation values/protocol repairs, +returned-bank pending-write refaults, trace/interrupt continuation, wider origins, +physical partial transfers and general hardware frames remain required. The user +has no hardware available; native 030/040 trace/A7/PC disagreements remain +unqualified. No private CPU is imported, package published or regression retired. +No new full CPU suite, consumer replay or physical qualification is claimed. +Milestone 6 remains **in progress**, `roadmapComplete=false`. + +Evidence is local under `%TEMP%/copper68k-reference-restoration-20261006/`: +`audits/MoveWriteEntryDiscoveryV1`, `audits/MoveWriteEntryV1`, +`move-write-entry-controls-v1` and `write-entry-production-default-v2`. + +| Evidence | SHA-256 | +| --- | --- | +| audit | `540bc6b5f896666eb404cf9ccb6c2da1cbf7ad0e6e62dfef2dde29791c3c6478` | +| controls | `0aae5ae6a50314ad2728d62e6ab98447900d411be567e48719e2a73b875f5cec` | +| reasons | `dfd2d48bed532a49969c6b6b11c517f35f6221a5af4c55db0e9089e004e27c81` | +| default | `90cdba9f772e2338ba1878d991aafa9d7b3a8ddcab5fec6d9982b871364da45b` | +| discovery | `aaa09e983612da06d214498b73e163efc00774a6eda42e9a7a83a55b9b5e613d` | +| parent | `3a2d2b3c8f82f4205224819a3ba82ba2d6066958881b9649d6153f88366cbf64` | +| complete source/evidence linkage | `e99fa0e461c07f93451e42cb3f52251d6583654b17938462073e003b600c4853` | diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index a46b6b62..fe1190ad 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3855,3 +3855,22 @@ Changed validation values, entry failures, pending-write refaults, trace/interru continuation and general hardware frames remain open. Exact graph/evidence limits are in [the qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-validation-input-and-refault--2026-10-08). Milestone 6 remains **in progress**, `roadmapComplete=false`. + + +### Milestone 6 private validation exception entry — 2026-10-08 + +The selected C023/C021 software contract adds **51,200 passing cases** for +secondary stack/vector faults after validation failure. All **599,808 parent +cases** retain identical reports. Three isolated defects produce exactly +**51,200 / 51,200 / 1,280** mismatches; all **103,680** diagnostic messages are +independently checked. Eleven evidence controls reject and strict frozen replay +passes. The private 240-input graph preserves all 39 CPU inputs; current +production's 224-input graph compiles the fixture with two tests unavailable. + +No production candidate is imported, package published or regression retired. +Changed validation values/protocol repairs, returned-bank pending-write refaults, +trace/interrupt continuation, wider origins/physical partial transfers and general +hardware frames remain open. Hardware is unavailable, so disputed native 030/040 +boundaries remain unqualified. Exact scope and evidence are in [the qualification +record](COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-validation-exception-entry--2026-10-08). +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-move-write-entry-fault.py b/scripts/test-copper68k-move-write-entry-fault.py new file mode 100644 index 00000000..dc9e4ccb --- /dev/null +++ b/scripts/test-copper68k-move-write-entry-fault.py @@ -0,0 +1,125 @@ +"""Audit secondary exception-entry faults of the private C021/C023 continuation. + +Requires the complete immutable MoveWriteValidationV1 parent. No private CPU +import, hardware-frame qualification, or package publication is performed. +""" +import argparse, contextlib, hashlib, importlib.util, io, itertools, json, os, re, subprocess +from pathlib import Path +import xml.etree.ElementTree as ET +REPO = Path(__file__).resolve().parents[1] +HELPER = REPO/'scripts/test-copper68k-move-write-validation.py' +spec = importlib.util.spec_from_file_location('write_validation', HELPER) +m = importlib.util.module_from_spec(spec);spec.loader.exec_module(m) +sha,load,save,require,inventory=m.sha,m.load,m.save,m.require,m.inventory +FIXTURE='Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteEntryFaultTests.cs' +CPU='Copper68k/M68kAdvancedTimingInterpreter.Rte020.cs' +PARENT_PROOF='3a2d2b3c8f82f4205224819a3ba82ba2d6066958881b9649d6153f88366cbf64' +MODELS=['68EC020','68020','68030','A1200'] +VARIANTS=['candidate','no-halt','stack-advance','wrong-saved-sr'] +FILTER='FullyQualifiedName~SyntheticM68020MoveWriteEntryFaultTests' +ENV={'COPPER68K_RUN_020_MOVE_WRITE_ENTRY_FAULT':'1',**m.ENV} +NS=m.NS + + +def once(text,old,new): + require(text.count(old)==1,'Nonunique entry-fault mutation') + return text.replace(old,new) + + +def sources(parent,variant): + prior=load(parent/'candidate/execution.json')['sources'] + require(len(prior)==239 and FIXTURE not in prior,'Incomplete entry-fault parent') + data={n:(parent/'candidate/source'/n).read_bytes() for n in prior} + require(all(hashlib.sha256(v).hexdigest()==prior[n] for n,v in data.items()),'Changed entry-fault parent sources') + data[FIXTURE]=(REPO/FIXTURE).read_bytes() + text=data[CPU].decode('utf-8').replace('\r\n','\n') + old='catch (RteReadFault020) { State.Halted = true; _instructionPipe.Reset(); }' + if variant=='no-halt':text=once(text,old,old.replace('State.Halted = true','State.Halted = false')) + elif variant=='stack-advance':text=once(text,old,old.replace('State.Halted = true','State.SetActiveStackPointer(unchecked(State.A[7] + 2)); State.Halted = true')) + elif variant=='wrong-saved-sr':text=once(text,'words[0] = sr; Long(2, context.InstructionPc);','words[0] = (ushort)(sr ^ 1); Long(2, context.InstructionPc);') + if variant!='candidate':data[CPU]=text.encode('utf-8') + return data + + +def command(root,variant): + selection=FILTER+('|'+m.FILTER+'|'+m.m.FILTER+'|'+m.m.m.FILTER if variant=='candidate' else '') + return ['dotnet','test',str(root/'source/Copper68k.Tests/Copper68k.Tests.csproj'),'-c','Release','--artifacts-path',str(root/'build'),'--filter',selection,'--logger','trx;LogFileName=audit.trx','--results-directory',str(root)] + + +def expected(model,kind,bank,variant): + rows={} + for location,ccr,request in itertools.product(range(2),range(32) if kind=='ccr' else [31],range(4)): + for byte,entry in itertools.product(range(4 if request==1 else 2),[0] if kind=='ccr' else range(47)): + for entry_byte in range(4 if entry==46 else 2): + bad=variant in ['no-halt','stack-advance'] or variant=='wrong-saved-sr' and entry==46 + key=f'{model}/RTE/write-validation-entry/{kind}/bank={bank}/location={location}/ccr={ccr}/request={request}/byte={byte}/entry={entry}/entry-byte={entry_byte}' + rows[key]={'mismatching' if bad else 'passing':1} + require(len(rows)==(1280 if kind=='ccr' else 1920),'Wrong entry-fault inventory') + return rows + + +def verify(root,variant,data,retained,parent_names): + e=load(root/'execution.json');ids={n:hashlib.sha256(v).hexdigest() for n,v in data.items()} + require(e['sources']==ids==inventory(root/'source'),'Changed complete entry-fault source inventory') + require(e['command']==command(root,variant) and e['settings']==ENV and e['exit']==(0 if variant=='candidate' else 1),'Wrong entry-fault command, settings or exit') + outputs=['execution.log','audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + require(e['outputs']=={n:sha(root/n) for n in outputs},'Changed entry-fault outputs') + tree=ET.parse(root/'audit.trx');tests=tree.findall('.//t:UnitTestResult',NS) + names={f'{route}WriteValidationEntryFaults':'Passed' if variant=='candidate' else 'Failed' for route in ['Scalar','Batch']} + if variant=='candidate':names.update(parent_names) + require(len(tests)==len(names) and {x.get('testName').split('.')[-1]:x.get('outcome') for x in tests}==names,'Wrong or empty entry-fault selection') + counter=tree.find('.//t:Counters',NS);failed=sum(v=='Failed' for v in names.values()) + require(all(int(counter.get(k,-1))==v for k,v in dict(total=len(names),executed=len(names),passed=len(names)-failed,failed=failed,notExecuted=0).items()),'Wrong entry-fault counters') + require(tree.find('.//t:ResultSummary',NS).get('outcome')==('Completed' if variant=='candidate' else 'Failed'),'Wrong entry-fault outcome') + definitions={x.get('id'):x for x in tree.findall('.//t:UnitTest',NS)} + require(all(Path(definitions[x.get('testId')].get('storage')).resolve()==(root/'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll').resolve() for x in tests),'Wrong loaded entry-fault test assembly') + stdout='\n'.join(x.text or '' for x in tree.findall('.//t:UnitTestResult/t:Output/t:StdOut',NS));reports=[];totals=dict(passing=0,mismatching=0,unsupported=0,untested=0) + def summary(d): + matches=re.findall(re.escape(d['model']+'/'+d['group']+': ')+r'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)',stdout) + require(len(matches)==1 and json.loads(matches[0][0])==d['counts'] and int(matches[0][1])==d['logicalCases'] and int(matches[0][2])==len(d['combinations']),'Entry-fault report/TRX disagreement') + for model,kind,bank,route in itertools.product(MODELS,['enumeration','ccr'],['ISP','MSP'],['scalar','batch']): + group=f'move-write-entry-{kind}-{bank}-{route}';path=root/f'{model}-{group}.json';d=load(path);wanted=expected(model,kind,bank,variant) + counts={s:sum(v.get(s,0) for v in wanted.values()) for s in totals} + require(d['schema']==1 and d['model']==model and d['group']==group and d['logicalCases']==len(wanted) and d['xunitBatches']==1 and d['counts']==counts and d['combinations']==wanted,'Wrong entry-fault combinations: '+path.name) + bad={n for n,v in wanted.items() if 'mismatching' in v} + require(len(d['failures'])==len(bad) and {x['id'] for x in d['failures']}==bad and all(x['status']=='mismatching' and x['reason'] for x in d['failures']),'Wrong complete entry-fault failure witnesses') + summary(d);reports.append(dict(path=path.name,sha256=sha(path))) + for s in totals:totals[s]+=counts[s] + if variant=='candidate': + require(len(retained)==1344,'Incomplete entry-fault retention') + for name,wanted in retained.items(): + d=load(root/name);require(d==wanted,'Changed retained entry-fault-parent report: '+name);summary(d);reports.append(dict(path=name,sha256=sha(root/name))) + require({x.name for x in root.glob('*.json')}=={x['path'] for x in reports}|{'execution.json'},'Missing or foreign entry-fault reports') + return dict(variant=variant,counts=totals,retentionCases=599808 if variant=='candidate' else 0,executions=len(names),reports=reports,executionSha256=sha(root/'execution.json')) + + +def audit(parent,output,validate): + require(sha(parent/'proof.json')==PARENT_PROOF,'Wrong entry-fault parent proof') + with contextlib.redirect_stdout(io.StringIO()):m.audit(parent.parent/'MoveWriteFrameFaultV1',parent,True) + linkage=dict(producers={f.relative_to(REPO).as_posix():sha(f) for f in [Path(__file__),HELPER,REPO/FIXTURE]},parentProofSha256=PARENT_PROOF,variants=VARIANTS,settings=ENV) + if not validate:output.mkdir(parents=True,exist_ok=False);save(output/'inputs.json',linkage) + require(load(output/'inputs.json')==linkage,'Changed entry-fault producers or selection') + retained={f.name:load(f) for f in (parent/'candidate').glob('*-move-write-*.json')} + tree=ET.parse(parent/'candidate/audit.trx');parent_names={x.get('testName').split('.')[-1]:x.get('outcome') for x in tree.findall('.//t:UnitTestResult',NS)} + require(len(parent_names)==8 and set(parent_names.values())=={'Passed'},'Incomplete entry-fault parent roster') + entries=[] + for variant in VARIANTS: + root=output/variant;data=sources(parent,variant) + if not validate: + (root/'source').mkdir(parents=True) + for n,v in data.items(): + f=root/'source'/n;f.parent.mkdir(parents=True,exist_ok=True);f.write_bytes(v) + env={k:v for k,v in os.environ.items() if not k.startswith('COPPER68K_RUN_')};env.update(ENV);env['COPPER68K_SYNTHETIC_REPORT_DIR']=str(root) + with (root/'execution.log').open('w',encoding='utf-8') as log:done=subprocess.run(command(root,variant),env=env,stdout=log,stderr=subprocess.STDOUT) + outputs=['execution.log','audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + save(root/'execution.json',dict(command=command(root,variant),settings=ENV,exit=done.returncode,sources={n:hashlib.sha256(v).hexdigest() for n,v in data.items()},outputs={n:sha(root/n) for n in outputs})) + entry=verify(root,variant,data,retained,parent_names);entries.append(entry);print(variant,entry['counts'],'retention',entry['retentionCases'],flush=True) + proof=dict(schema=1,inputsSha256=sha(output/'inputs.json'),entries=entries,sourceCount=240,productionImport=False,publication=False,hardwareQualified=False,architecturalPromotion=False,roadmapComplete=False) + if validate:require(load(output/'proof.json')==proof,'Changed complete entry-fault proof') + else:save(output/'proof.json',proof) + print('Complete private write-entry audit',sha(output/'proof.json'),flush=True) + + +if __name__=='__main__': + parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--qualified-parent-directory',type=Path,required=True);parser.add_argument('--output',type=Path,required=True);parser.add_argument('--validate-only',action='store_true') + args=parser.parse_args();audit(args.qualified_parent_directory.resolve(),args.output.resolve(),args.validate_only) From b74bf3ba0b76d523841c5df343d907024d13cdc3 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 14:14:39 +0300 Subject: [PATCH 155/211] Qualify changed input to private write validation --- Copper68k.Tests/Synthetic/README.md | 20 +++ ...ntheticM68020MoveWriteChangedInputTests.cs | 101 ++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 80 +++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 17 +++ ...test-copper68k-move-write-changed-input.py | 124 ++++++++++++++++++ 5 files changed, 342 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteChangedInputTests.cs create mode 100644 scripts/test-copper68k-move-write-changed-input.py diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 5f71431b..bb8e8042 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1540,3 +1540,23 @@ unavailable by default; it uses a distinct 37-input CPU graph. Missing fixtures, empty selection or mismatches fail the requested audit. Wider pending forms, changed validation values, pending-store refaults, trace/interrupts and hardware frames remain unqualified. See [the entry-fault qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-validation-exception-entry--2026-10-08). + + +Cold private changed validation input is opt-in with +`COPPER68K_RUN_020_MOVE_WRITE_CHANGED_INPUT=1`: + +```powershell +python scripts/test-copper68k-move-write-changed-input.py ` + --qualified-parent-directory ` + --output +``` + +The 16,384 new cases execute changed SR/PC/same-format/end input handlers from +literal C021/C023 contexts, retaining 651,008 parent cases. Three mutations +detect ignored input, replayed phases and lost M state; unsupported mutant cases +fail the gate. Strict `--validate-only` requires complete unchanged evidence. +Missing fixtures, empty selection or mismatch/unsupported legal execution fail +the requested audit. All 39 private CPU inputs remain frozen; current production +uses a distinct 37-input CPU graph and both tests are unavailable by default. +Malformed protocols, wider forms, store refaults, trace/interrupts and hardware +frames remain open. See [the changed-input qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-changed-validation-input--2026-10-08). diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteChangedInputTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteChangedInputTests.cs new file mode 100644 index 00000000..2eccbbbb --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteChangedInputTests.cs @@ -0,0 +1,101 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Cold serialized C021/C023 contexts. No original MOVE or validation fault +// is executed here; the handler supplies changed validation data explicitly. +public sealed class SyntheticM68020MoveWriteChangedInputTests(ITestOutputHelper output) +{ + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_CHANGED_INPUT", "require private changed validation-input audit"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarChangedWriteValidationInput() => Audit(false); + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_CHANGED_INPUT", "require private changed validation-input audit"), Trait("Suite", "ReferenceDiscovery")] + public void BatchChangedWriteValidationInput() => Audit(true); + + private void Audit(bool batch) + { + var failures = new List(); + foreach (var model in ModelSpec.All.Where(x => x.Id is "68EC020" or "68020" or "68030" or "A1200")) + foreach (var bank in new[] { "ISP", "MSP" }) + { + var report = new CoverageBatch(model.Id, $"move-write-changed-input-{bank}-{(batch ? "batch" : "scalar")}"); + var m = new SyntheticMachine(model); + foreach (var location in new[] { 0, 1 }) + foreach (var ccr in Enumerable.Range(0, 32)) + foreach (var returned in new[] { "user", "user-M", "ISP", "MSP" }) + for (var request = 0; request < 4; request++) + { + var id = $"{model.Id}/RTE/write-changed-input/bank={bank}/location={location}/ccr={ccr}/returned={returned}/request={request}"; + try { Run(m, batch, bank, location, ccr, returned, request); report.Record(id, "passing", null); } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException) { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + try { report.Complete(output); } catch (Exception ex) { failures.Add(ex); } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(x => x.Message))); + } + + private static void Run(SyntheticMachine m, bool batch, string bank, int location, int ccr, string returned, int request) + { + m.Reset(); + var inner = location == 0 ? 0x6000u : 0x12346000u; var outer = inner - 92; + var saved = (ushort)(0x700 | ccr | (returned is "ISP" or "MSP" ? 0x2000 : 0) | (returned is "user-M" or "MSP" ? 0x1000 : 0)); + var current = (ushort)((bank == "ISP" ? 0x2700 : 0x3700) | ccr); + var supplied = request switch { 0 => 0xcafe0000u | (uint)(saved ^ 0x101f), 1 => 0x12342400u, 2 => 0xcafea024u, _ => 0xcafe9876u }; + var resultSr = request == 0 ? (ushort)(saved ^ 0x101f) : saved; + var resultPc = request == 1 ? 0x12342400u : 0x2000u; + _ = SyntheticExecution.Prepare(m, [0x2f7c, (ushort)(supplied >> 16), (ushort)supplied, 0x002c, 0x026f, 0xfeff, 0x000a, 0x4e73, 0x4e71]); + for (uint at = outer - 8; at < inner + 40; at++) m.InitializePhysical(at, 0xa5, 1); + for (uint at = 0x43f8; at < 0x4410; at++) m.InitializePhysical(at, 0xa5, 1); + for (uint at = 0; at < 92; at += 2) m.InitializePhysical(outer + at, 0, 2); + m.InitializePhysical(outer, current, 2); m.InitializePhysical(outer + 2, 0x9000, 4); + m.InitializePhysical(outer + 6, 0xb008, 2); m.InitializePhysical(outer + 8, 0xc021, 2); + m.InitializePhysical(outer + 10, (uint)(request == 1 ? 0x145 : 0x165), 2); + m.InitializePhysical(outer + 16, inner + (request == 0 ? 0u : request == 1 ? 2u : request == 2 ? 6u : 30u), 4); + m.InitializePhysical(outer + 20, inner, 4); + m.InitializePhysical(outer + 28, (uint)((request >= 1 ? saved : 0) << 16) | (request >= 3 ? 0xa008u : 0), 4); + m.InitializePhysical(outer + 32, request >= 2 ? 0x2000u : 0, 4); + m.InitializePhysical(outer + 40, (uint)(request == 3 ? 4 : request), 2); + m.InitializePhysical(inner, saved, 2); m.InitializePhysical(inner + 2, 0x2000, 4); + m.InitializePhysical(inner + 6, 0xa008, 2); m.InitializePhysical(inner + 8, 0xc023, 2); + m.InitializePhysical(inner + 10, 0x125, 2); m.InitializePhysical(inner + 12, 0x7e11, 2); m.InitializePhysical(inner + 14, 0x7c22, 2); + m.InitializePhysical(inner + 16, 0x4400, 4); m.InitializePhysical(inner + 20, 0x329f, 2); + m.InitializePhysical(inner + 22, 0x301, 2); m.InitializePhysical(inner + 24, 0xb2c3, 4); + m.InitializePhysical(inner + 28, 0x7a33, 2); m.InitializePhysical(inner + 30, 0, 2); + for (var n = 0; n < 4; n++) m.InitializePhysical(resultPc + (uint)n * 2, (uint)(n == 3 ? 0x4e71 : new[] { 0x7e2a, 0x7c2b, 0x7a2c }[n]), 2); + var s = m.Core.State; s.A[0] = 0x4204; s.A[1] = 0x4500; + s.SetUserStackPointer(0x7800); s.SetInterruptStackPointer(bank == "ISP" ? outer : 0x4700); s.SetMasterStackPointer(bank == "MSP" ? outer : 0x7400); + s.StatusRegister = current; s.SetActiveStackPointer(outer); + var e = ArchitecturalExpectation.Capture(m); var serial = s.ExceptionSequence; m.Bus.Accesses.Clear(); + e.Write(outer + 44, supplied, 4, m.Model); e.Pc += 8; + e.Sr = (ushort)((current & 0xfff0) | (request == 1 ? 0 : 8)); + Step(m, batch); Check(m, e); + e.Write(outer + 10, (uint)(request == 1 ? 0x45 : 0x65), 2, m.Model); e.Pc += 6; e.Sr = (ushort)(e.Sr & 0xfff0); + Step(m, batch); Check(m, e); + var start = m.Bus.Accesses.Count; + var isp = bank == "ISP" ? inner + 32 : 0x4700u; var msp = bank == "MSP" ? inner + 32 : 0x7400u; + e.A[7] = (resultSr & 0x2000) == 0 ? 0x7800u : (resultSr & 0x1000) == 0 ? isp : msp; + e.InactiveStackPointer = (resultSr & 0x2000) == 0 ? isp : 0x7800u; e.MasterStackPointer = msp; + e.Sr = resultSr; e.Pc = resultPc; e.Write(0x4400, 0xb2c3, 2, m.Model); + Step(m, batch); Check(m, e); + var reads = new (uint Offset, int Width)[] { (0, 2), (2, 4), (6, 2), (30, 2), (8, 2), (10, 2), (16, 4), (20, 2), (22, 2), (24, 4), (30, 2), (12, 2), (14, 2), (28, 2) } + .Skip(request + 1).Concat(Enumerable.Range(0, 10).Select(n => (12u + (uint)n * 2, 2))); + var actual = m.Bus.Accesses.Skip(start).Where(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address >= m.Model.Physical(inner) && a.Address < m.Model.Physical(inner) + 32).Select(a => (a.Address, a.Width)); + if (!actual.SequenceEqual(reads.Select(a => (m.Model.Physical(inner + a.Item1), a.Item2)))) throw new InvalidOperationException("Changed input replayed/skipped a validation or frame read"); + if (!m.Bus.Accesses.Skip(start).Where(a => a.Write).Select(a => (a.Address, a.Width, a.Value)).SequenceEqual(new[] { (0x4400u, 2, 0xb2c3u) })) throw new InvalidOperationException("Changed input repeated or misplaced the pending write"); + for (var n = 0; n < 3; n++) + { + e.D[7 - n] = (uint)new[] { 0x11, 0x22, 0x33 }[n]; e.Sr = (ushort)(e.Sr & 0xfff0); e.Pc += 2; + var before = m.Bus.Accesses.Count; Step(m, batch); Check(m, e); + if (m.Bus.Accesses.Skip(before).Any(a => !a.Write && a.Kind == M68kBusAccessKind.CpuInstructionFetch && a.Address == m.Model.Physical(resultPc + (uint)n * 2))) throw new InvalidOperationException("Changed input discarded a retained instruction word"); + } + if (s.ExceptionSequence != serial || s.UserStackPointer != 0x7800 || s.InterruptStackPointer != isp || s.MasterStackPointer != msp) throw new InvalidOperationException("Changed input altered exception count or stack bank"); + } + private static void Check(SyntheticMachine m, ArchitecturalExpectation e) { if (e.Verify(m) is { } error) throw new InvalidOperationException(error); } + private static void Step(SyntheticMachine m, bool batch) + { + if (!batch) m.Core.ExecuteInstruction(); + else if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, new Boundary()) != 1) throw new InvalidOperationException("Empty changed-input batch"); + } + private sealed class Boundary : IM68kInstructionBoundary { public bool BeforeInstruction() => true; public void AfterInstruction(long a, long b) { } } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 507cb3fb..e52fe8d7 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -12284,3 +12284,83 @@ Evidence is local under `%TEMP%/copper68k-reference-restoration-20261006/`: | discovery | `aaa09e983612da06d214498b73e163efc00774a6eda42e9a7a83a55b9b5e613d` | | parent | `3a2d2b3c8f82f4205224819a3ba82ba2d6066958881b9649d6153f88366cbf64` | | complete source/evidence linkage | `e99fa0e461c07f93451e42cb3f52251d6583654b17938462073e003b600c4853` | + + +## Private pending-write changed validation input — 2026-10-08 + +`SyntheticM68020MoveWriteChangedInputTests` adds **16,384 passing cases / two +executions / 16 reports** on EC020, A1200, 020 and 030. It begins with literal +cold C021/C023 contexts, a canonical word pending store and three retained +instruction words. Actual MOVE.L/ANDI.W/RTE handlers supply changed validation +input and clear DF. Across ISP/MSP, four returned banks, paired low/high frame +locations and all 32 CCR states, the selected changes are: toggled CCR/M state +with nonzero upper input bits; high PC `0x12342400`; format `0xA024` with nonzero +upper bits; and end-word input `0xCAFE9876`. The inner frame retains its original +values, so it cannot supply the expected changed SR or PC by a repeated read. + +Each handler instruction checks PC, registers, defined SR, stack and memory +guards. Return checks the changed SR/PC and bank selection, exact remaining inner +validation/state-load reads, one serialized pending word write and all three +retained following MOVEQ instructions. Word-sized SR/format values use their +low 16 bits. Format A remains format A, its changed low vector bits do not alter +restoration, and this end-word input is semantically ignored by the selected +private contract. No original MOVE or initial validation fault is executed here; +the parent audit retains separately qualified original-value fault handlers. + +All **651,008 parent cases / ten executions / 1,376 reports** retain identical +results. Combined: **667,392 cases / twelve executions / 1,392 reports**. The +**241-input** private graph adds only this fixture and preserves all **39 CPU +inputs**. The maintained audit revalidates its complete pinned parent chain, +source/output hashes, loaded assembly, selection/settings, TRX roster/counters, +every case key/status/weight and retained reports. Strict frozen replay passes. + +Ignoring supplied input causes **16,384 mismatches**. Replaying completed phases +causes **4,096 mismatches and 8,192 unsupported cases**, with **4,096** unaffected +cases passing. Clearing the supplied M bit causes **2,048 mismatches**, with +**14,336** unaffected cases passing. All **30,720** failure diagnostics are +independently checked, including exact SR, PC and unsupported-profile messages. +Unsupported mutant execution fails its test gate; it is not relabeled as an +architectural exception. The first maintained run rejected an incorrect +prediction that all replay failures would be mismatches; its output is retained. +The successful fresh run explicitly distinguishes these categories. Initial +local tuple/import errors are also retained separately from successful evidence. + +Eleven copied evidence controls reject missing reports, empty/foreign/untested +combinations, changed source/roster/settings, missing witnesses, changed/missing +retention and wrong loaded assembly. Validator paths are rebased, hardlinked +targets unlinked before modification and original hashes preserved; controls +execute no CPU. An isolated **225-input / 37-CPU-input** current production build +compiles the fixture with exactly two unavailable tests and no executed cases. +Its explicit `NotExecuted` rows and actual TRX counters are recorded. + +```powershell +python scripts/test-copper68k-move-write-changed-input.py ` + --qualified-parent-directory ` + --output +# --validate-only requires unchanged complete producers, sources and evidence. +``` + +This covers selected changed input under a cold private software contract. Other +frame formats, malformed-protocol repair, byte/long pending stores, zero-to-two +retained words, physical partial transfers, returned-bank pending-store refaults, +trace/interrupt continuation and foreign hardware frames remain unqualified. +Disputed native 030/040 boundaries lack hardware evidence. No private CPU is +imported, package published or regression retired; no new full CPU suite or +consumer replay is claimed. Milestone 6 remains **in progress**, +`roadmapComplete=false`. + +Evidence is local under `%TEMP%/copper68k-reference-restoration-20261006/`: +`audits/MoveWriteChangedInputDiscoveryV2`, `audits/MoveWriteChangedInputV1` +(rejected prediction), `audits/MoveWriteChangedInputV2`, +`move-write-changed-input-controls-v2`, and +`write-changed-input-production-default-v1`. + +| Evidence | SHA-256 | +| --- | --- | +| audit | `4f6e928d90ad460567c23817ee3680e87579d65ed6b725b17904cc7a41c62ceb` | +| controls | `6b290d8dd9dd1c67525dd80474feb386775e5ec24dc024af330ecba9eea1d238` | +| reasons | `1d884bcd85a29bd06c45e4184dd17074cb11158811b293a7c35cfd821c88d5c4` | +| default | `3863c04f28eb8d0f45d8a302af5f019eb5bbc2f4eb502d13d46aa4cb5b385cbe` | +| discovery | `6cec3aa5a90aea1041d012cc45c4e297ba051c9a339cffa067119b2259def539` | +| parent | `540bc6b5f896666eb404cf9ccb6c2da1cbf7ad0e6e62dfef2dde29791c3c6478` | +| complete source/evidence linkage | `57b50a215274295f4e63dd7245d4645fe961fa868266c9699cf76f7aa59bb4cf` | diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index fe1190ad..4e756782 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3874,3 +3874,20 @@ hardware frames remain open. Hardware is unavailable, so disputed native 030/040 boundaries remain unqualified. Exact scope and evidence are in [the qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-validation-exception-entry--2026-10-08). Milestone 6 remains **in progress**, `roadmapComplete=false`. + + +### Milestone 6 private changed validation input — 2026-10-08 + +The cold C021/C023 software contract adds **16,384 passing cases** for changed +SR/PC/same-format/end input via actual handlers, exact remaining read order, +pending-store completion and retained sentinels. All **651,008 parent cases** +retain their reports. Ignored input, replayed phases and lost M state are detected; +all **30,720** mismatch/unsupported diagnostics are independently verified. +Eleven evidence controls reject and strict frozen replay passes. All 39 private +CPU inputs remain unchanged; current production compiles both tests as +unavailable. No production candidate is imported or package published. + +Malformed-protocol repair, wider pending/pipe forms, returned-bank write refaults, +trace/interrupts, physical partial transfers and general hardware frames remain +required. Exact scope and evidence are in [the qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-changed-validation-input--2026-10-08). +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-move-write-changed-input.py b/scripts/test-copper68k-move-write-changed-input.py new file mode 100644 index 00000000..9f78e65d --- /dev/null +++ b/scripts/test-copper68k-move-write-changed-input.py @@ -0,0 +1,124 @@ +"""Audit changed handler input to cold private C021/C023 continuations. + +Requires the complete immutable MoveWriteEntryV1 parent. No private CPU +import, hardware-frame qualification, or package publication is performed. +""" +import argparse, contextlib, hashlib, importlib.util, io, itertools, json, os, re, subprocess +from pathlib import Path +import xml.etree.ElementTree as ET +REPO = Path(__file__).resolve().parents[1] +HELPER = REPO/'scripts/test-copper68k-move-write-entry-fault.py' +spec = importlib.util.spec_from_file_location('write_validation', HELPER) +m = importlib.util.module_from_spec(spec);spec.loader.exec_module(m) +sha,load,save,require,inventory=m.sha,m.load,m.save,m.require,m.inventory +FIXTURE='Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteChangedInputTests.cs' +CPU='Copper68k/M68kAdvancedTimingInterpreter.Rte020.cs' +PARENT_PROOF='540bc6b5f896666eb404cf9ccb6c2da1cbf7ad0e6e62dfef2dde29791c3c6478' +MODELS=['68EC020','68020','68030','A1200'] +VARIANTS=['candidate','ignored-input','replay-phase','lost-m-bit'] +FILTER='FullyQualifiedName~SyntheticM68020MoveWriteChangedInputTests' +ENV={'COPPER68K_RUN_020_MOVE_WRITE_CHANGED_INPUT':'1',**m.ENV} +NS=m.NS + + +def once(text,old,new): + require(text.count(old)==1,'Nonunique changed-input mutation') + return text.replace(old,new) + + +def sources(parent,variant): + prior=load(parent/'candidate/execution.json')['sources'] + require(len(prior)==240 and FIXTURE not in prior,'Incomplete changed-input parent') + data={n:(parent/'candidate/source'/n).read_bytes() for n in prior} + require(all(hashlib.sha256(v).hexdigest()==prior[n] for n,v in data.items()),'Changed changed-input parent sources') + data[FIXTURE]=(REPO/FIXTURE).read_bytes() + text=data[CPU].decode('utf-8').replace('\r\n','\n') + old='ExecuteRte020(context, (ssw & 0x100) == 0 ? input : null);' + if variant=='ignored-input':text=once(text,old,'ExecuteRte020(context, null);') + elif variant=='replay-phase':text=once(text,old,'context.Phase = 0; '+old) + elif variant=='lost-m-bit':text=once(text,'value = repaired; supplied = null;','value = repaired & 0xffffefff; supplied = null;') + if variant!='candidate':data[CPU]=text.encode('utf-8') + return data + + +def command(root,variant): + selection=FILTER+('|'+m.FILTER+'|'+m.m.FILTER+'|'+m.m.m.FILTER+'|'+m.m.m.m.FILTER if variant=='candidate' else '') + return ['dotnet','test',str(root/'source/Copper68k.Tests/Copper68k.Tests.csproj'),'-c','Release','--artifacts-path',str(root/'build'),'--filter',selection,'--logger','trx;LogFileName=audit.trx','--results-directory',str(root)] + + +def expected(model,bank,variant): + rows={} + for location,ccr,returned,request in itertools.product(range(2),range(32),['user','user-M','ISP','MSP'],range(4)): + bad=variant=='ignored-input' or variant=='replay-phase' and request>0 or variant=='lost-m-bit' and request==0 and returned in ['user','ISP'] + status='unsupported' if variant=='replay-phase' and request>=2 else 'mismatching' if bad else 'passing' + key=f'{model}/RTE/write-changed-input/bank={bank}/location={location}/ccr={ccr}/returned={returned}/request={request}' + rows[key]={status:1} + require(len(rows)==1024,'Wrong changed-input inventory') + return rows + + +def verify(root,variant,data,retained,parent_names): + e=load(root/'execution.json');ids={n:hashlib.sha256(v).hexdigest() for n,v in data.items()} + require(e['sources']==ids==inventory(root/'source'),'Changed complete changed-input source inventory') + require(e['command']==command(root,variant) and e['settings']==ENV and e['exit']==(0 if variant=='candidate' else 1),'Wrong changed-input command, settings or exit') + outputs=['execution.log','audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + require(e['outputs']=={n:sha(root/n) for n in outputs},'Changed changed-input outputs') + tree=ET.parse(root/'audit.trx');tests=tree.findall('.//t:UnitTestResult',NS) + names={f'{route}ChangedWriteValidationInput':'Passed' if variant=='candidate' else 'Failed' for route in ['Scalar','Batch']} + if variant=='candidate':names.update(parent_names) + require(len(tests)==len(names) and {x.get('testName').split('.')[-1]:x.get('outcome') for x in tests}==names,'Wrong or empty changed-input selection') + counter=tree.find('.//t:Counters',NS);failed=sum(v=='Failed' for v in names.values()) + require(all(int(counter.get(k,-1))==v for k,v in dict(total=len(names),executed=len(names),passed=len(names)-failed,failed=failed,notExecuted=0).items()),'Wrong changed-input counters') + require(tree.find('.//t:ResultSummary',NS).get('outcome')==('Completed' if variant=='candidate' else 'Failed'),'Wrong changed-input outcome') + definitions={x.get('id'):x for x in tree.findall('.//t:UnitTest',NS)} + require(all(Path(definitions[x.get('testId')].get('storage')).resolve()==(root/'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll').resolve() for x in tests),'Wrong loaded changed-input test assembly') + stdout='\n'.join(x.text or '' for x in tree.findall('.//t:UnitTestResult/t:Output/t:StdOut',NS));reports=[];totals=dict(passing=0,mismatching=0,unsupported=0,untested=0) + def summary(d): + matches=re.findall(re.escape(d['model']+'/'+d['group']+': ')+r'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)',stdout) + require(len(matches)==1 and json.loads(matches[0][0])==d['counts'] and int(matches[0][1])==d['logicalCases'] and int(matches[0][2])==len(d['combinations']),'Changed-input report/TRX disagreement') + for model,bank,route in itertools.product(MODELS,['ISP','MSP'],['scalar','batch']): + group=f'move-write-changed-input-{bank}-{route}';path=root/f'{model}-{group}.json';d=load(path);wanted=expected(model,bank,variant) + counts={s:sum(v.get(s,0) for v in wanted.values()) for s in totals} + require(d['schema']==1 and d['model']==model and d['group']==group and d['logicalCases']==len(wanted) and d['xunitBatches']==1 and d['counts']==counts and d['combinations']==wanted,'Wrong changed-input combinations: '+path.name) + bad={n for n,v in wanted.items() if 'passing' not in v} + require(len(d['failures'])==len(bad) and {x['id'] for x in d['failures']}==bad and all(wanted[x['id']]=={x['status']:1} and x['reason'] for x in d['failures']),'Wrong complete changed-input failure witnesses') + summary(d);reports.append(dict(path=path.name,sha256=sha(path))) + for s in totals:totals[s]+=counts[s] + if variant=='candidate': + require(len(retained)==1376,'Incomplete changed-input retention') + for name,wanted in retained.items(): + d=load(root/name);require(d==wanted,'Changed retained changed-input-parent report: '+name);summary(d);reports.append(dict(path=name,sha256=sha(root/name))) + require({x.name for x in root.glob('*.json')}=={x['path'] for x in reports}|{'execution.json'},'Missing or foreign changed-input reports') + return dict(variant=variant,counts=totals,retentionCases=651008 if variant=='candidate' else 0,executions=len(names),reports=reports,executionSha256=sha(root/'execution.json')) + + +def audit(parent,output,validate): + require(sha(parent/'proof.json')==PARENT_PROOF,'Wrong changed-input parent proof') + with contextlib.redirect_stdout(io.StringIO()):m.audit(parent.parent/'MoveWriteValidationV1',parent,True) + linkage=dict(producers={f.relative_to(REPO).as_posix():sha(f) for f in [Path(__file__),HELPER,REPO/FIXTURE]},parentProofSha256=PARENT_PROOF,variants=VARIANTS,settings=ENV) + if not validate:output.mkdir(parents=True,exist_ok=False);save(output/'inputs.json',linkage) + require(load(output/'inputs.json')==linkage,'Changed changed-input producers or selection') + retained={f.name:load(f) for f in (parent/'candidate').glob('*-move-write-*.json')} + tree=ET.parse(parent/'candidate/audit.trx');parent_names={x.get('testName').split('.')[-1]:x.get('outcome') for x in tree.findall('.//t:UnitTestResult',NS)} + require(len(parent_names)==10 and set(parent_names.values())=={'Passed'},'Incomplete changed-input parent roster') + entries=[] + for variant in VARIANTS: + root=output/variant;data=sources(parent,variant) + if not validate: + (root/'source').mkdir(parents=True) + for n,v in data.items(): + f=root/'source'/n;f.parent.mkdir(parents=True,exist_ok=True);f.write_bytes(v) + env={k:v for k,v in os.environ.items() if not k.startswith('COPPER68K_RUN_')};env.update(ENV);env['COPPER68K_SYNTHETIC_REPORT_DIR']=str(root) + with (root/'execution.log').open('w',encoding='utf-8') as log:done=subprocess.run(command(root,variant),env=env,stdout=log,stderr=subprocess.STDOUT) + outputs=['execution.log','audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + save(root/'execution.json',dict(command=command(root,variant),settings=ENV,exit=done.returncode,sources={n:hashlib.sha256(v).hexdigest() for n,v in data.items()},outputs={n:sha(root/n) for n in outputs})) + entry=verify(root,variant,data,retained,parent_names);entries.append(entry);print(variant,entry['counts'],'retention',entry['retentionCases'],flush=True) + proof=dict(schema=1,inputsSha256=sha(output/'inputs.json'),entries=entries,sourceCount=241,productionImport=False,publication=False,hardwareQualified=False,architecturalPromotion=False,roadmapComplete=False) + if validate:require(load(output/'proof.json')==proof,'Changed complete changed-input proof') + else:save(output/'proof.json',proof) + print('Complete private write-changed-input audit',sha(output/'proof.json'),flush=True) + + +if __name__=='__main__': + parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--qualified-parent-directory',type=Path,required=True);parser.add_argument('--output',type=Path,required=True);parser.add_argument('--validate-only',action='store_true') + args=parser.parse_args();audit(args.qualified_parent_directory.resolve(),args.output.resolve(),args.validate_only) From 965aa7c57595e92c0619d7c23ff220a862e1ee11 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 14:39:30 +0300 Subject: [PATCH 156/211] Qualify private returned-bank pending-write refaults --- Copper68k.Tests/Synthetic/README.md | 21 +++ .../SyntheticM68020MoveWriteRefaultTests.cs | 140 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 86 +++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 18 +++ scripts/test-copper68k-move-write-refault.py | 123 +++++++++++++++ 5 files changed, 388 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteRefaultTests.cs create mode 100644 scripts/test-copper68k-move-write-refault.py diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index bb8e8042..2efe1756 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1560,3 +1560,24 @@ the requested audit. All 39 private CPU inputs remain frozen; current production uses a distinct 37-input CPU graph and both tests are unavailable by default. Malformed protocols, wider forms, store refaults, trace/interrupts and hardware frames remain open. See [the changed-input qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-changed-validation-input--2026-10-08). + + +Cold private pending-store refaults are opt-in with +`COPPER68K_RUN_020_MOVE_WRITE_REFAULT=1`: + +```powershell +python scripts/test-copper68k-move-write-refault.py ` + --qualified-parent-directory ` + --output +``` + +The 458,752 new cases execute two returned-bank store faults and explicit +mapped/software completion, retaining 667,392 parent cases. Saved SR/FC, exact +frame writes, inactive banks and all retained words must survive; the destination +is written once and the source never reread. Four mutations detect flags, FC, +destination and pipe defects. Strict `--validate-only` requires complete unchanged +evidence; missing fixtures, empty selection or mismatch/unsupported legal +execution fail. All 39 private CPU inputs remain frozen. Current production +uses a distinct 37-input CPU graph and both tests are unavailable by default. +Physical partial transfers, entry faults, trace/interrupts, malformed protocols +and hardware frames remain open. See [the returned-bank refault record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#private-returned-bank-pending-write-refaults--2026-10-08). diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteRefaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteRefaultTests.cs new file mode 100644 index 00000000..497535cd --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteRefaultTests.cs @@ -0,0 +1,140 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Cold private C023 frames, two explicit pending-store faults and actual +// handler RTEs. No original MOVE is retried and no silicon frame is asserted. +public sealed class SyntheticM68020MoveWriteRefaultTests(ITestOutputHelper output) +{ + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_REFAULT", "require private returned-bank pending-store refault audit"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarWriteRefaultContinuation() => Audit(false); + [EnvironmentFact("COPPER68K_RUN_020_MOVE_WRITE_REFAULT", "require private returned-bank pending-store refault audit"), Trait("Suite", "ReferenceDiscovery")] + public void BatchWriteRefaultContinuation() => Audit(true); + + private void Audit(bool batch) + { + var failures = new List(); + foreach (var model in ModelSpec.All.Where(x => x.Id is "68EC020" or "68020" or "68030" or "A1200")) + foreach (var width in new[] { 1, 2, 4 }) + foreach (var current in new[] { "ISP", "MSP" }) + foreach (var mode in new[] { "mapped", "software" }) + { + var report = new CoverageBatch(model.Id, $"move-write-refault-{width}-{current}-{mode}-{(batch ? "batch" : "scalar")}"); + var bus = new RefaultBus(); var m = new SyntheticMachine(model, bus); + foreach (var returned in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var location in new[] { 0, 1 }) + foreach (var ccr in Enumerable.Range(0, 32)) + foreach (var fc in new[] { 1, 5 }) + foreach (var count in Enumerable.Range(0, 4)) + for (var lane = 0; lane < width; lane++) + { + var id = $"{model.Id}/MOVE/write-refault/width={width}/current={current}/returned={returned}/location={location}/ccr={ccr}/fc={fc}/count={count}/mode={mode}/byte={lane}"; + try { Run(m, bus, batch, width, current, returned, location, ccr, fc, count, mode, lane); report.Record(id, "passing", null); } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException) { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + try { report.Complete(output); } catch (Exception ex) { failures.Add(ex); } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(x => x.Message))); + } + + private static void Run(SyntheticMachine m, RefaultBus bus, bool batch, int width, string current, string returned, int location, int ccr, int fc, int count, string mode, int lane) + { + bus.Disarm(); m.Reset(); _ = SyntheticExecution.Prepare(m, [0x4e73, 0x4e71]); + var frame = location == 0 ? 0x6000u : 0x12346000u; + var saved = (ushort)(0x700 | ccr | (returned is "ISP" or "MSP" ? 0x2000 : 0) | (returned is "user-M" or "MSP" ? 0x1000 : 0)); + var currentSr = (ushort)((current == "ISP" ? 0x2700 : 0x3700) | ccr); + var value = 0x80a1b2c3u & (width == 1 ? 0xffu : width == 2 ? 0xffffu : uint.MaxValue); + var opcode = (ushort)(width == 1 ? 0x129f : width == 2 ? 0x329f : 0x229f); + var ssw = (ushort)(0x100 | (width == 1 ? 0x10 : width == 2 ? 0x20 : 0) | fc); + var isp = current == "ISP" ? frame + 32 : 0x4700u; + var msp = current == "MSP" ? frame + 32 : 0x7400u; + var faultBank = (saved & 0x1000) == 0 ? "ISP" : "MSP"; + var faultFrame = (faultBank == "ISP" ? isp : msp) - 32; + foreach (var origin in new[] { frame, faultFrame }) + for (uint at = origin - 8; at < origin + 40; at++) m.InitializePhysical(at, 0xa5, 1); + for (uint at = 0x43f8; at < 0x4510; at++) m.InitializePhysical(at, 0xa5, 1); + m.InitializePhysical(8, 0x9020, 4); + m.InitializePhysical(0x9020, 0x4e73, 2); m.InitializePhysical(0x9022, 0x4e71, 2); + for (var n = 0; n < 4; n++) m.InitializePhysical(0x2000u + (uint)n * 2, (uint)(n == 3 ? 0x4e71 : new[] { 0x7e2a, 0x7c2b, 0x7a2c }[n]), 2); + m.InitializePhysical(frame, saved, 2); m.InitializePhysical(frame + 2, 0x2000, 4); + m.InitializePhysical(frame + 6, 0xa008, 2); m.InitializePhysical(frame + 8, 0xc023, 2); + m.InitializePhysical(frame + 10, ssw, 2); m.InitializePhysical(frame + 12, 0x7e11, 2); m.InitializePhysical(frame + 14, 0x7c22, 2); + m.InitializePhysical(frame + 16, 0x4400, 4); m.InitializePhysical(frame + 20, opcode, 2); + m.InitializePhysical(frame + 22, (uint)(1 | count << 8), 2); m.InitializePhysical(frame + 24, value, 4); + m.InitializePhysical(frame + 28, 0x7a33, 2); m.InitializePhysical(frame + 30, 0, 2); + var s = m.Core.State; s.A[0] = 0x4204; s.A[1] = 0x4500; + s.SetUserStackPointer(0x7800); s.SetInterruptStackPointer(current == "ISP" ? frame : 0x4700); s.SetMasterStackPointer(current == "MSP" ? frame : 0x7400); + s.StatusRegister = currentSr; s.SetActiveStackPointer(frame); + var e = ArchitecturalExpectation.Capture(m); var serial = s.ExceptionSequence; + bus.Accesses.Clear(); bus.Arm(m.Model.Physical(0x4400u + (uint)lane), width); + var words = new ushort[16]; words[0] = saved; words[1] = 0; words[2] = 0x2000; words[3] = 0xa008; + words[4] = 0xc023; words[5] = ssw; words[8] = 0; words[9] = 0x4400; words[10] = opcode; + words[11] = (ushort)(1 | count << 8); words[12] = (ushort)(value >> 16); words[13] = (ushort)value; + for (var n = 0; n < count; n++) words[n == 0 ? 6 : n == 1 ? 7 : 14] = (ushort)new[] { 0x7e11, 0x7c22, 0x7a33 }[n]; + for (var n = 0; n < 16; n++) e.Write(faultFrame + (uint)n * 2, words[n], 2, m.Model); + e.A[7] = faultFrame; e.InactiveStackPointer = 0x7800; e.MasterStackPointer = faultBank == "MSP" ? faultFrame : msp; + e.Sr = (ushort)(saved | 0x2000); e.Pc = 0x9020; e.ExceptionVector = 2; + e.ControlChecks["saved SR"] = (x => x.LastExceptionStatusRegister, saved); + e.ControlChecks["saved PC"] = (x => x.LastExceptionStackedProgramCounter, 0x2000); + Step(m, batch); Check(m, e); CheckBanks(m, faultBank == "ISP" ? faultFrame : isp, faultBank == "MSP" ? faultFrame : msp); + CheckEntry(m, 0, faultFrame, words); + if (bus.Rejected.Count != 1 || s.ExceptionSequence != serial + 1) throw new InvalidOperationException("Pending-store fault did not enter exactly once"); + var second = bus.Accesses.Count; + Step(m, batch); Check(m, e); CheckBanks(m, faultBank == "ISP" ? faultFrame : isp, faultBank == "MSP" ? faultFrame : msp); + CheckEntry(m, second, faultFrame, words); + if (bus.Rejected.Count != 2 || s.ExceptionSequence != serial + 2) throw new InvalidOperationException("Explicit pending-store RTE did not refault exactly once"); + + if (mode == "software") + { + var handler = new List { (ushort)(width == 1 ? 0x13fc : width == 2 ? 0x33fc : 0x23fc) }; + if (width == 4) handler.Add((ushort)(value >> 16)); + handler.Add((ushort)value); handler.Add(0); handler.Add(0x4400); + handler.AddRange([0x026f, 0xfeff, 0x000a, 0x4e73]); + for (var n = 0; n < handler.Count; n++) m.InitializePhysical(0x9020u + (uint)n * 2, handler[n], 2); + // Fixture initialization is separate from CPU stores and expectations. + for (var n = 0; n < handler.Count; n++) e.Write(0x9020u + (uint)n * 2, handler[n], 2, m.Model); + e.Write(0x4400, value, width, m.Model); e.Pc += width == 4 ? 10u : 8u; e.Sr = (ushort)((e.Sr & 0xfff0) | 8); + Step(m, batch); Check(m, e); + e.Write(faultFrame + 10, (uint)(ssw & ~0x100), 2, m.Model); e.Pc += 6; e.Sr = (ushort)(e.Sr & 0xfff0); + Step(m, batch); Check(m, e); + } + var final = bus.Accesses.Count; + e.A[7] = returned is "user" or "user-M" ? 0x7800u : returned == "ISP" ? isp : msp; + e.InactiveStackPointer = returned is "user" or "user-M" ? isp : 0x7800u; e.MasterStackPointer = msp; + e.Sr = saved; e.Pc = 0x2000; e.Write(0x4400, value, width, m.Model); + Step(m, batch); Check(m, e); CheckBanks(m, isp, msp); + if (!bus.Accesses.Skip(final).Where(a => a.Write).Select(a => (a.Address, a.Width, a.Value)).SequenceEqual(mode == "software" ? Array.Empty<(uint, int, uint)>() : new[] { (0x4400u, width, value) })) throw new InvalidOperationException("Pending-store completion lost or repeated its write"); + for (var n = 0; n < 3; n++) + { + e.D[7 - n] = (uint)(n < count ? new[] { 0x11, 0x22, 0x33 }[n] : 0x2a + n); e.Sr = (ushort)(e.Sr & 0xfff0); e.Pc += 2; + var before = bus.Accesses.Count; Step(m, batch); Check(m, e); CheckBanks(m, isp, msp); + if (bus.Accesses.Skip(before).Count(a => !a.Write && a.Kind == M68kBusAccessKind.CpuInstructionFetch && a.Address == 0x2000u + (uint)n * 2) != (n < count ? 0 : 1)) throw new InvalidOperationException("Refault changed retained instruction words"); + } + if (s.ExceptionSequence != serial + 2 || !bus.Rejected.SequenceEqual(new[] { (0x4400u, width), (0x4400u, width) })) throw new InvalidOperationException("Refault changed fault provenance"); + if (bus.Accesses.Any(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address != 8 && + !(a.Address >= m.Model.Physical(frame) && a.Address + (uint)a.Width <= m.Model.Physical(frame) + 32) && + !(a.Address >= m.Model.Physical(faultFrame) && a.Address + (uint)a.Width <= m.Model.Physical(faultFrame) + 32))) throw new InvalidOperationException("Refault reread the original operand"); + if (bus.Accesses.Count(a => a.Write && a.Address == 0x4400) != 1) throw new InvalidOperationException("Pending operand was not written exactly once"); + } + private static void CheckEntry(SyntheticMachine m, int start, uint frame, ushort[] words) + { + if (!m.Bus.Accesses.Skip(start).Where(a => a.Write).Select(a => (a.Address, a.Width, a.Value)).SequenceEqual(Enumerable.Range(0, 16).Select(n => (m.Model.Physical(frame + 30 - (uint)n * 2), 2, (uint)words[15 - n])))) throw new InvalidOperationException("Refault lost/reordered completed stack writes"); + } + private static void CheckBanks(SyntheticMachine m, uint isp, uint msp) + { if (m.Core.State.UserStackPointer != 0x7800 || m.Core.State.InterruptStackPointer != isp || m.Core.State.MasterStackPointer != msp) throw new InvalidOperationException("Refault changed an inactive or completed stack bank"); } + private static void Check(SyntheticMachine m, ArchitecturalExpectation e) { if (e.Verify(m) is { } error) throw new InvalidOperationException(error); } + private static void Step(SyntheticMachine m, bool batch) + { if (!batch) m.Core.ExecuteInstruction(); else if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, new Boundary()) != 1) throw new InvalidOperationException("Empty refault batch"); } + private sealed class Boundary : IM68kInstructionBoundary { public bool BeforeInstruction() => true; public void AfterInstruction(long a, long b) { } } + private sealed class RefaultBus : SparseRecordingBus, IM68kPhysicalAddressMap + { + private uint address; private int width, remaining; + internal readonly List<(uint Address, int Width)> Rejected = []; + internal void Disarm() { remaining = 0; Rejected.Clear(); } + internal void Arm(uint at, int size) { Disarm(); address = at; width = size; remaining = 2; } + public bool IsCpuPhysicalAddressMapped(uint at, int size, M68kBusAccessKind kind) + { if (remaining == 0 || kind != M68kBusAccessKind.CpuDataWrite || size != width || unchecked(address - at) >= size) return true; remaining--; Rejected.Add((at, size)); return false; } + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index e52fe8d7..f4fa7df3 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -12364,3 +12364,89 @@ Evidence is local under `%TEMP%/copper68k-reference-restoration-20261006/`: | discovery | `6cec3aa5a90aea1041d012cc45c4e297ba051c9a339cffa067119b2259def539` | | parent | `540bc6b5f896666eb404cf9ccb6c2da1cbf7ad0e6e62dfef2dde29791c3c6478` | | complete source/evidence linkage | `57b50a215274295f4e63dd7245d4645fe961fa868266c9699cf76f7aa59bb4cf` | + + +## Private returned-bank pending-write refaults — 2026-10-08 + +`SyntheticM68020MoveWriteRefaultTests` adds **458,752 passing programs / two +executions / 96 reports** on EC020, A1200, 020 and 030. It starts from literal +cold C023 format-A frames and rejects two successive pending stores during +explicit RTE continuation. The matrix covers B/W/L, every byte of each rejected +request, current ISP/MSP, returned user/user-M/ISP/MSP, saved function codes 1/5 +independent of returned privilege, paired low/high original frame locations, +all 32 CCR states, zero-to-three retained instruction words, scalar/batch +execution and mapped/software completion. The operand is the canonical negative +value `0x80A1B2C3` truncated to size; other value patterns/origins are not newly +enumerated. No original MOVE or original source read is executed. + +Each fault commits a fresh literal 16-word C023 image on the returned M-selected +supervisor bank. The test independently checks every frame word and descending +accepted stack write, saved PC/SR and exception count, all registers, memory +guards, active/inactive stack banks, preserved function code and retained pipe +words. The second actual handler RTE refaults once. The final handler either +allows the pending write or executes MOVE immediate to the saved destination, +clears DF with ANDI.W, and RTEs without another operand write. Subsequent MOVEQ +sentinels distinguish all retained/fetched words. Data reads are confined to +the supplied frames and vector; the unrelated live source/destination registers +remain unchanged. The pending operand is written **exactly once**. This is +explicit serialized continuation, not implicit retry after partial effects. + +All **667,392 parent cases / twelve executions / 1,392 reports** retain identical +results. Combined: **1,126,144 cases / fourteen executions / 1,488 reports**. +The **242-input** private graph adds only this fixture and preserves all **39 CPU +inputs**. Its maintained command revalidates the complete pinned parent chain, +source/output identities, loaded assembly, command/settings, TRX roster/counters, +all case IDs/statuses/weights and retained reports. Strict frozen replay passes. + +Four isolated defects cause exactly predicted mismatches: recomputed flags +**430,080**; lost saved function code **229,376**; live destination reuse +**458,752**; discarded pipe words **344,064**. Unaffected cases pass. A separate +verifier independently checks all **1,462,272** complete diagnostic messages, +including exact SR and model-width physical addresses of corrupted frame bytes. +Eleven copied evidence controls reject missing reports, empty/foreign/untested +combinations, changed source/roster/settings, missing witnesses, changed/missing +retention and wrong loaded assembly. Validator paths are rebased, hardlinked +targets unlinked before modification and original hashes preserved. Controls +execute no CPU. + +The adjacent CPU assemblies in all five test output directories are separately +hash-bound to their corresponding compiled candidate/mutant CPU assemblies. A +copied changed CPU assembly is rejected by that identity verifier, with the +original unchanged. This supplements the maintained loaded-test-assembly gate; +it is an assembly-copy identity check, not new instruction execution. + +The isolated current **226-input / 37-CPU-input** production graph compiles the +fixture with exactly two unavailable tests and zero executed cases. Explicit +`NotExecuted` result rows and actual TRX counters are recorded. This is compile +coverage, not production continuation replay. + +```powershell +python scripts/test-copper68k-move-write-refault.py ` + --qualified-parent-directory ` + --output +# --validate-only requires unchanged complete producers, sources and evidence. +``` + +This qualifies the selected cold private returned-bank refault contract. It +does not qualify actual initial MOVE origins, physical partial transfers, +refault exception-entry failures, trace/interrupt continuation, malformed +protocol repair or foreign hardware frames. Disputed native 030/040 boundaries +remain without hardware evidence. No private CPU is imported, package published +or regression retired; no new full CPU suite, consumer replay or physical timing +qualification is claimed. Milestone 6 remains **in progress**, +`roadmapComplete=false`. + +Evidence is local under `%TEMP%/copper68k-reference-restoration-20261006/`: +`audits/MoveWriteRefaultDiscoveryV1`, `audits/MoveWriteRefaultV1`, +`move-write-refault-controls-v1`, and `write-refault-production-default-v1`. + +| Evidence | SHA-256 | +| --- | --- | +| audit | `96abb10dcbe6c0c51f5922a28f2484018b2bbcdaaac9059eaf0416028eb87bd3` | +| controls | `2f02ce4d895fd3741f6c1b8b487ac7117ec948f756441f482756a7e81e113a0e` | +| reasons | `bf6229fa91391e7ea371b84427312dbe25155878ec046af98853b6a19861f0bf` | +| default | `e54dc9a7cd6e04a2debb8160dffc0e1e178d533a43efe63d1fc92feb77e1d551` | +| discovery | `85b19726b2d483c1912c93d36362482dfb24ee5eafd15cb0582d6ca65cc589a6` | +| parent | `4f6e928d90ad460567c23817ee3680e87579d65ed6b725b17904cc7a41c62ceb` | +| adjacent CPU binding | `ec66795f7044ecd74d8db89f7030e0add303663819de732e05dcf6024780157c` | +| complete source/evidence linkage | `49210ade00f3abf6de0445284556fcf900fe90b2b7ba8de71c2cecd458159ee5` | diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 4e756782..db8acf4b 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3891,3 +3891,21 @@ Malformed-protocol repair, wider pending/pipe forms, returned-bank write refault trace/interrupts, physical partial transfers and general hardware frames remain required. Exact scope and evidence are in [the qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#private-pending-write-changed-validation-input--2026-10-08). Milestone 6 remains **in progress**, `roadmapComplete=false`. + + +### Milestone 6 private returned-bank write refaults — 2026-10-08 + +The cold C023 software contract adds **458,752 passing programs** for two +successive pending-store faults and explicit mapped/software completion, covering +B/W/L, all returned banks, saved FC 1/5, all CCR states, zero-to-three pipe words +and every rejected request byte. All **667,392 parent cases** retain their +reports. Four isolated defects produce exactly predicted mismatches; all +**1,462,272** failure messages are independently checked. Eleven evidence +controls reject and strict frozen replay passes. All 39 private CPU inputs stay +unchanged; production compiles two unavailable tests. No private candidate is +imported, package published or regression retired. + +Actual original MOVE origins, physical partial transfers, refault entry faults, +trace/interrupts, malformed protocol repair and general hardware frames remain +required. Exact scope and evidence are in [the qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#private-returned-bank-pending-write-refaults--2026-10-08). +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-move-write-refault.py b/scripts/test-copper68k-move-write-refault.py new file mode 100644 index 00000000..9c5392f1 --- /dev/null +++ b/scripts/test-copper68k-move-write-refault.py @@ -0,0 +1,123 @@ +"""Audit returned-bank refaults of cold private C023 pending stores. + +Requires the complete immutable MoveWriteChangedInputV2 parent. No private CPU +import, hardware-frame qualification, or package publication is performed. +""" +import argparse, contextlib, hashlib, importlib.util, io, itertools, json, os, re, subprocess +from pathlib import Path +import xml.etree.ElementTree as ET +REPO = Path(__file__).resolve().parents[1] +HELPER = REPO/'scripts/test-copper68k-move-write-changed-input.py' +spec = importlib.util.spec_from_file_location('write_validation', HELPER) +m = importlib.util.module_from_spec(spec);spec.loader.exec_module(m) +sha,load,save,require,inventory=m.sha,m.load,m.save,m.require,m.inventory +FIXTURE='Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteRefaultTests.cs' +CPU='Copper68k/M68kAdvancedTimingInterpreter.FinalWrite020.cs' +PARENT_PROOF='4f6e928d90ad460567c23817ee3680e87579d65ed6b725b17904cc7a41c62ceb' +MODELS=['68EC020','68020','68030','A1200'] +VARIANTS=['candidate','recomputed-flags','lost-fc','wrong-destination','lost-pipe'] +FILTER='FullyQualifiedName~SyntheticM68020MoveWriteRefaultTests' +ENV={'COPPER68K_RUN_020_MOVE_WRITE_REFAULT':'1',**m.ENV} +NS=m.NS + + +def once(text,old,new): + require(text.count(old)==1,'Nonunique refault mutation') + return text.replace(old,new) + + +def sources(parent,variant): + prior=load(parent/'candidate/execution.json')['sources'] + require(len(prior)==241 and FIXTURE not in prior,'Incomplete refault parent') + data={n:(parent/'candidate/source'/n).read_bytes() for n in prior} + require(all(hashlib.sha256(v).hexdigest()==prior[n] for n,v in data.items()),'Changed refault parent sources') + data[FIXTURE]=(REPO/FIXTURE).read_bytes() + text=data[CPU].decode('utf-8').replace('\r\n','\n') + if variant=='recomputed-flags':text=once(text,'if (!_resumingFinalMoveWrite020) SetMoveFlags(value, size);','SetMoveFlags(value, size);') + elif variant=='lost-fc':text=once(text,'_restoredWriteFunctionCode020 = (ushort)(ssw & 7);','_restoredWriteFunctionCode020 = null;') + elif variant=='wrong-destination':text=once(text,'WriteFinalMoveDestination020(address, value, size);','WriteFinalMoveDestination020(State.A[1], value, size);') + elif variant=='lost-pipe':text=once(text,'_instructionPipe = pipe;','_instructionPipe.Reset(pc);') + if variant!='candidate':data[CPU]=text.encode('utf-8') + return data + + +def command(root,variant): + selection=FILTER+('|'+m.FILTER+'|'+m.m.FILTER+'|'+m.m.m.FILTER+'|'+m.m.m.m.FILTER+'|'+m.m.m.m.m.FILTER if variant=='candidate' else '') + return ['dotnet','test',str(root/'source/Copper68k.Tests/Copper68k.Tests.csproj'),'-c','Release','--artifacts-path',str(root/'build'),'--filter',selection,'--logger','trx;LogFileName=audit.trx','--results-directory',str(root)] + + +def expected(model,width,current,mode,variant): + rows={} + for returned,location,ccr,fc,count,lane in itertools.product(['user','user-M','ISP','MSP'],range(2),range(32),[1,5],range(4),range(width)): + bad=variant=='wrong-destination' or variant=='recomputed-flags' and ccr&15!=8 or variant=='lost-fc' and fc!=(5 if returned in ['ISP','MSP'] else 1) or variant=='lost-pipe' and count>0 + key=f'{model}/MOVE/write-refault/width={width}/current={current}/returned={returned}/location={location}/ccr={ccr}/fc={fc}/count={count}/mode={mode}/byte={lane}' + rows[key]={'mismatching' if bad else 'passing':1} + require(len(rows)==2048*width,'Wrong refault inventory') + return rows + + +def verify(root,variant,data,retained,parent_names): + e=load(root/'execution.json');ids={n:hashlib.sha256(v).hexdigest() for n,v in data.items()} + require(e['sources']==ids==inventory(root/'source'),'Changed complete refault source inventory') + require(e['command']==command(root,variant) and e['settings']==ENV and e['exit']==(0 if variant=='candidate' else 1),'Wrong refault command, settings or exit') + outputs=['execution.log','audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + require(e['outputs']=={n:sha(root/n) for n in outputs},'Changed refault outputs') + tree=ET.parse(root/'audit.trx');tests=tree.findall('.//t:UnitTestResult',NS) + names={f'{route}WriteRefaultContinuation':'Passed' if variant=='candidate' else 'Failed' for route in ['Scalar','Batch']} + if variant=='candidate':names.update(parent_names) + require(len(tests)==len(names) and {x.get('testName').split('.')[-1]:x.get('outcome') for x in tests}==names,'Wrong or empty refault selection') + counter=tree.find('.//t:Counters',NS);failed=sum(v=='Failed' for v in names.values()) + require(all(int(counter.get(k,-1))==v for k,v in dict(total=len(names),executed=len(names),passed=len(names)-failed,failed=failed,notExecuted=0).items()),'Wrong refault counters') + require(tree.find('.//t:ResultSummary',NS).get('outcome')==('Completed' if variant=='candidate' else 'Failed'),'Wrong refault outcome') + definitions={x.get('id'):x for x in tree.findall('.//t:UnitTest',NS)} + require(all(Path(definitions[x.get('testId')].get('storage')).resolve()==(root/'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll').resolve() for x in tests),'Wrong loaded refault test assembly') + stdout='\n'.join(x.text or '' for x in tree.findall('.//t:UnitTestResult/t:Output/t:StdOut',NS));reports=[];totals=dict(passing=0,mismatching=0,unsupported=0,untested=0) + def summary(d): + matches=re.findall(re.escape(d['model']+'/'+d['group']+': ')+r'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)',stdout) + require(len(matches)==1 and json.loads(matches[0][0])==d['counts'] and int(matches[0][1])==d['logicalCases'] and int(matches[0][2])==len(d['combinations']),'Changed-input report/TRX disagreement') + for model,width,current,mode,route in itertools.product(MODELS,[1,2,4],['ISP','MSP'],['mapped','software'],['scalar','batch']): + group=f'move-write-refault-{width}-{current}-{mode}-{route}';path=root/f'{model}-{group}.json';d=load(path);wanted=expected(model,width,current,mode,variant) + counts={s:sum(v.get(s,0) for v in wanted.values()) for s in totals} + require(d['schema']==1 and d['model']==model and d['group']==group and d['logicalCases']==len(wanted) and d['xunitBatches']==1 and d['counts']==counts and d['combinations']==wanted,'Wrong refault combinations: '+path.name) + bad={n for n,v in wanted.items() if 'passing' not in v} + require(len(d['failures'])==len(bad) and {x['id'] for x in d['failures']}==bad and all(wanted[x['id']]=={x['status']:1} and x['reason'] for x in d['failures']),'Wrong complete refault failure witnesses') + summary(d);reports.append(dict(path=path.name,sha256=sha(path))) + for s in totals:totals[s]+=counts[s] + if variant=='candidate': + require(len(retained)==1392,'Incomplete refault retention') + for name,wanted in retained.items(): + d=load(root/name);require(d==wanted,'Changed retained refault-parent report: '+name);summary(d);reports.append(dict(path=name,sha256=sha(root/name))) + require({x.name for x in root.glob('*.json')}=={x['path'] for x in reports}|{'execution.json'},'Missing or foreign refault reports') + return dict(variant=variant,counts=totals,retentionCases=667392 if variant=='candidate' else 0,executions=len(names),reports=reports,executionSha256=sha(root/'execution.json')) + + +def audit(parent,output,validate): + require(sha(parent/'proof.json')==PARENT_PROOF,'Wrong refault parent proof') + with contextlib.redirect_stdout(io.StringIO()):m.audit(parent.parent/'MoveWriteEntryV1',parent,True) + linkage=dict(producers={f.relative_to(REPO).as_posix():sha(f) for f in [Path(__file__),HELPER,REPO/FIXTURE]},parentProofSha256=PARENT_PROOF,variants=VARIANTS,settings=ENV) + if not validate:output.mkdir(parents=True,exist_ok=False);save(output/'inputs.json',linkage) + require(load(output/'inputs.json')==linkage,'Changed refault producers or selection') + retained={f.name:load(f) for f in (parent/'candidate').glob('*-move-write-*.json')} + tree=ET.parse(parent/'candidate/audit.trx');parent_names={x.get('testName').split('.')[-1]:x.get('outcome') for x in tree.findall('.//t:UnitTestResult',NS)} + require(len(parent_names)==12 and set(parent_names.values())=={'Passed'},'Incomplete refault parent roster') + entries=[] + for variant in VARIANTS: + root=output/variant;data=sources(parent,variant) + if not validate: + (root/'source').mkdir(parents=True) + for n,v in data.items(): + f=root/'source'/n;f.parent.mkdir(parents=True,exist_ok=True);f.write_bytes(v) + env={k:v for k,v in os.environ.items() if not k.startswith('COPPER68K_RUN_')};env.update(ENV);env['COPPER68K_SYNTHETIC_REPORT_DIR']=str(root) + with (root/'execution.log').open('w',encoding='utf-8') as log:done=subprocess.run(command(root,variant),env=env,stdout=log,stderr=subprocess.STDOUT) + outputs=['execution.log','audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + save(root/'execution.json',dict(command=command(root,variant),settings=ENV,exit=done.returncode,sources={n:hashlib.sha256(v).hexdigest() for n,v in data.items()},outputs={n:sha(root/n) for n in outputs})) + entry=verify(root,variant,data,retained,parent_names);entries.append(entry);print(variant,entry['counts'],'retention',entry['retentionCases'],flush=True) + proof=dict(schema=1,inputsSha256=sha(output/'inputs.json'),entries=entries,sourceCount=242,productionImport=False,publication=False,hardwareQualified=False,architecturalPromotion=False,roadmapComplete=False) + if validate:require(load(output/'proof.json')==proof,'Changed complete refault proof') + else:save(output/'proof.json',proof) + print('Complete private write-refault audit',sha(output/'proof.json'),flush=True) + + +if __name__=='__main__': + parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--qualified-parent-directory',type=Path,required=True);parser.add_argument('--output',type=Path,required=True);parser.add_argument('--validate-only',action='store_true') + args=parser.parse_args();audit(args.qualified_parent_directory.resolve(),args.output.resolve(),args.validate_only) From 9430fc416714c5f68a9757cd44ebe6ee72ee7ed6 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 15:30:49 +0300 Subject: [PATCH 157/211] Qualify actual MOVE secondary entry faults --- Copper68k.Tests/Synthetic/README.md | 19 +++ ...yntheticM68020MoveOriginEntryFaultTests.cs | 156 ++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 79 +++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 16 ++ .../test-copper68k-move-origin-entry-fault.py | 144 ++++++++++++++++ 5 files changed, 414 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020MoveOriginEntryFaultTests.cs create mode 100644 scripts/test-copper68k-move-origin-entry-fault.py diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 2efe1756..19505a4e 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1581,3 +1581,22 @@ execution fail. All 39 private CPU inputs remain frozen. Current production uses a distinct 37-input CPU graph and both tests are unavailable by default. Physical partial transfers, entry faults, trace/interrupts, malformed protocols and hardware frames remain open. See [the returned-bank refault record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#private-returned-bank-pending-write-refaults--2026-10-08). + + +Actual simple MOVE secondary entry faults are opt-in with +`COPPER68K_RUN_020_MOVE_ORIGIN_ENTRY=1`: + +```powershell +python scripts/test-copper68k-move-origin-entry-fault.py ` + --qualified-parent-directory ` + --output +``` + +The 307,200 new cases enumerate 384 supported opcode words, value/CCR boundaries +and rejected operand plus stack/vector entry bytes. All 1,126,144 parent cases +are retained. Four mutations and eleven evidence controls verify the gate; +strict `--validate-only` fails on incomplete or changed evidence. Size/bank +shards preserve every diagnostic without changing logical cases. All 39 private +CPU inputs remain frozen; the distinct production graph compiles two unavailable +tests. Wider operands, partial transfers, refault entry, trace/interrupts and +hardware qualification remain open. See [the actual MOVE entry record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#private-actual-move-secondary-entry-faults--2026-10-08). diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020MoveOriginEntryFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020MoveOriginEntryFaultTests.cs new file mode 100644 index 00000000..bf5d4396 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020MoveOriginEntryFaultTests.cs @@ -0,0 +1,156 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Actual simple MOVE origins followed by a rejected stack/vector entry request. +// Fixed private C023 layout and logical register effects, not silicon timing. +public sealed class SyntheticM68020MoveOriginEntryFaultTests(ITestOutputHelper output) +{ + private sealed record Case(int Width, string Bank, int Location, int Source, int Destination, bool Post, int Value, int Ccr, int OperandByte, int Entry, int EntryByte); + [EnvironmentFact("COPPER68K_RUN_020_MOVE_ORIGIN_ENTRY", "require private actual-MOVE entry-fault audit"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarMoveOriginEntryFaults() => Audit(false); + [EnvironmentFact("COPPER68K_RUN_020_MOVE_ORIGIN_ENTRY", "require private actual-MOVE entry-fault audit"), Trait("Suite", "ReferenceDiscovery")] + public void BatchMoveOriginEntryFaults() => Audit(true); + + private static IEnumerable Cases(string kind, int selectedWidth, string? selectedBank) + { + foreach (var width in new[] { 1, 2, 4 }) + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var location in new[] { 0, 1 }) + { + if (selectedWidth != 0 && (width != selectedWidth || bank != selectedBank)) continue; + if (kind == "opcodes") + { + for (var source = 0; source < 8; source++) + for (var destination = 0; destination < 8; destination++) + foreach (var post in new[] { false, true }) + yield return new(width, bank, location, source, destination, post, 2, 31, 0, 0, 0); + } + else if (kind == "boundaries") + { + for (var value = 0; value < 4; value++) + for (var ccr = 0; ccr < 32; ccr++) + yield return new(width, bank, location, 0, 1, true, value, ccr, 0, 0, 0); + } + else + { + foreach (var source in new[] { 0, 7 }) + foreach (var alias in new[] { false, true }) + foreach (var post in new[] { false, true }) + foreach (var ccr in new[] { 0, 31 }) + for (var operandByte = 0; operandByte < width; operandByte++) + for (var entry = 0; entry < 17; entry++) + for (var entryByte = 0; entryByte < (entry == 16 ? 4 : 2); entryByte++) + yield return new(width, bank, location, source, alias ? source : 1, post, 2, ccr, operandByte, entry, entryByte); + } + } + } + private static IEnumerable<(int Width, string? Bank)> Slices(string kind) + { + if (kind != "entry") { yield return (0, null); yield break; } + foreach (var width in new[] { 1, 2, 4 }) + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) yield return (width, bank); + } + private static ushort Opcode(Case c) => (ushort)((c.Width == 1 ? 0x1090 : c.Width == 2 ? 0x3090 : 0x2090) | c.Source | c.Destination << 9 | (c.Post ? 8 : 0)); + private void Audit(bool batch) + { + var failures = new List(); + foreach (var model in ModelSpec.All.Where(x => x.Id is "68EC020" or "68020" or "68030" or "A1200")) + foreach (var kind in new[] { "opcodes", "entry", "boundaries" }) + foreach (var slice in Slices(kind)) + { + var suffix = kind == "entry" ? $"-{slice.Width}-{slice.Bank}" : ""; + var report = new CoverageBatch(model.Id, $"move-origin-entry-{kind}{suffix}-{(batch ? "batch" : "scalar")}"); + var bus = new EntryBus(); var m = new SyntheticMachine(model, bus); + foreach (var c in Cases(kind, slice.Width, slice.Bank)) + { + var id = $"{model.Id}/MOVE/origin-entry/{kind}/opcode={Opcode(c):X4}/width={c.Width}/bank={c.Bank}/location={c.Location}/source={c.Source}/destination={c.Destination}/post={c.Post}/value={c.Value}/ccr={c.Ccr}/operand-byte={c.OperandByte}/entry={c.Entry}/entry-byte={c.EntryByte}"; + try { Run(m, bus, batch, c); report.Record(id, "passing", null); } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException) { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + try { report.Complete(output); } catch (Exception ex) { failures.Add(ex); } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(x => x.Message))); + } + private static void Run(SyntheticMachine m, EntryBus bus, bool batch, Case c) + { + bus.Disarm(); m.Reset(); var opcode = Opcode(c); _ = SyntheticExecution.Prepare(m, [opcode, 0x7e2a, 0x4e71]); + var origin = c.Location == 0 ? 0u : 0x12340000u; var vbr = c.Location == 0 ? 0u : 0x12345000u; + var usp = origin + 0x6800; var isp = origin + 0x6000; var msp = origin + 0x7400; + var sr = (ushort)(0x700 | c.Ccr | (c.Bank is "ISP" or "MSP" ? 0x2000 : 0) | (c.Bank is "user-M" or "MSP" ? 0x1000 : 0)); + var s = m.Core.State; + for (var n = 0; n < 7; n++) s.A[n] = origin + 0x4200u + (uint)n * 0x100; + s.SetUserStackPointer(usp); s.SetInterruptStackPointer(isp); s.SetMasterStackPointer(msp); s.StatusRegister = sr; s.VectorBaseRegister = vbr; + var source = s.A[c.Source]; var stride = c.Width == 1 && c.Source == 7 ? 2u : (uint)c.Width; + var destination = s.A[c.Destination] + (c.Post && c.Source == c.Destination ? stride : 0); + var mask = c.Width == 4 ? uint.MaxValue : (1u << (c.Width * 8)) - 1; + var value = c.Value switch { 0 => 0u, 1 => mask >> 1, 2 => 1u << (c.Width * 8 - 1), _ => mask }; + foreach (var at in new[] { source, destination, usp, isp, msp }) + for (var offset = -40; offset < 40; offset++) m.InitializePhysical(unchecked(at + (uint)offset), 0xa5, 1); + m.InitializePhysical(source, value, c.Width); m.InitializePhysical(vbr + 8, 0x9020, 4); + var e = ArchitecturalExpectation.Capture(m); var serial = s.ExceptionSequence; + var saved = (ushort)((sr & 0xfff0) | (c.Value == 0 ? 4 : c.Value >= 2 ? 8 : 0)); + if (c.Post) + { + if (c.Source != 7) e.A[c.Source] += stride; + else if (c.Bank == "ISP") isp += stride; + else if (c.Bank == "MSP") msp += stride; + else usp += stride; + } + var master = (sr & 0x1000) != 0; var stack = master ? msp : isp; + var secondary = c.Entry == 16 ? vbr + 8 : stack - (uint)(c.Entry + 1) * 2; + var words = new ushort[16]; + words[0] = saved; words[1] = 0; words[2] = 0x1002; words[3] = 0xa008; words[4] = 0xc023; + words[5] = (ushort)(0x100 | (c.Width == 1 ? 0x10 : c.Width == 2 ? 0x20 : 0) | ((sr & 0x2000) == 0 ? 1 : 5)); + words[8] = (ushort)(destination >> 16); words[9] = (ushort)destination; words[10] = opcode; words[11] = 1; + words[12] = (ushort)(value >> 16); words[13] = (ushort)value; + var completed = c.Entry == 16 ? 16 : c.Entry; + for (var n = 0; n < completed; n++) e.Write(stack - (uint)(n + 1) * 2, words[15 - n], 2, m.Model); + var finalSp = c.Entry == 16 ? stack - 32 : secondary; + if (master) msp = finalSp; else isp = finalSp; + e.A[7] = finalSp; e.InactiveStackPointer = usp; e.MasterStackPointer = msp; + e.Pc = 0x1002; e.Sr = (ushort)(saved | 0x2000); e.Halted = true; e.ExceptionVector = 2; + e.ControlChecks["saved SR"] = (x => x.LastExceptionStatusRegister, saved); + e.ControlChecks["saved PC"] = (x => x.LastExceptionStackedProgramCounter, 0x1002); + e.ControlChecks["exception sequence"] = (x => x.ExceptionSequence, serial + 1); + e.ControlChecks["VBR"] = (x => x.VectorBaseRegister, vbr); + e.ControlChecks["SFC"] = (x => x.SourceFunctionCode, 0); e.ControlChecks["DFC"] = (x => x.DestinationFunctionCode, 0); + bus.Accesses.Clear(); bus.Arm(m.Model.Physical(destination + (uint)c.OperandByte), c.Width, + m.Model.Physical(secondary + (uint)c.EntryByte), c.Entry == 16 ? 4 : 2, + c.Entry == 16 ? M68kBusAccessKind.CpuDataRead : M68kBusAccessKind.CpuDataWrite); + Step(m, batch); Check(m, e); CheckBanks(m, usp, isp, msp); + if (!bus.Rejected.SequenceEqual(new[] { (m.Model.Physical(destination), c.Width, M68kBusAccessKind.CpuDataWrite), + (m.Model.Physical(secondary), c.Entry == 16 ? 4 : 2, c.Entry == 16 ? M68kBusAccessKind.CpuDataRead : M68kBusAccessKind.CpuDataWrite) })) throw new InvalidOperationException("Wrong operand/entry fault requests"); + if (!bus.Accesses.Where(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead).Select(a => (a.Address, a.Width, a.Value)).SequenceEqual(new[] { (m.Model.Physical(source), c.Width, value) })) throw new InvalidOperationException("Origin entry fault reread or misplaced the source"); + if (!bus.Accesses.Where(a => a.Write).Select(a => (a.Address, a.Width, a.Value)).SequenceEqual(Enumerable.Range(0, completed).Select(n => (m.Model.Physical(stack - (uint)(n + 1) * 2), 2, (uint)words[15 - n])))) throw new InvalidOperationException("Origin entry fault repeated/reordered a stack or operand write"); + var accesses = bus.Accesses.Count; + if (batch) { var b = new Boundary(); if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, b) != 0 || b.Before != 0 || b.After != 0) throw new InvalidOperationException("Halted origin executed a batch boundary"); } + else m.Core.ExecuteInstruction(); + Check(m, e); CheckBanks(m, usp, isp, msp); + if (bus.Accesses.Count != accesses || bus.Rejected.Count != 2) throw new InvalidOperationException("Halted origin retried a bus access"); + } + private static void CheckBanks(SyntheticMachine m, uint usp, uint isp, uint msp) + { if (m.Core.State.UserStackPointer != usp || m.Core.State.InterruptStackPointer != isp || m.Core.State.MasterStackPointer != msp) throw new InvalidOperationException("Origin entry fault changed an inactive stack bank"); } + private static void Check(SyntheticMachine m, ArchitecturalExpectation e) { if (e.Verify(m) is { } error) throw new InvalidOperationException(error); } + private static void Step(SyntheticMachine m, bool batch) + { if (!batch) m.Core.ExecuteInstruction(); else { var b = new Boundary(); if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, b) != 1 || b.Before != 1 || b.After != 1) throw new InvalidOperationException("Origin entry batch boundaries differ"); } } + private sealed class Boundary : IM68kInstructionBoundary { internal int Before, After; public bool BeforeInstruction() { Before++; return true; } public void AfterInstruction(long a, long b) => After++; } + private sealed class EntryBus : SparseRecordingBus, IM68kPhysicalAddressMap + { + private uint operand, secondary; private int operandWidth, secondaryWidth, phase; private M68kBusAccessKind secondaryKind; + internal readonly List<(uint Address, int Width, M68kBusAccessKind Kind)> Rejected = []; + internal void Disarm() { phase = 0; Rejected.Clear(); } + internal void Arm(uint first, int firstWidth, uint second, int secondWidth, M68kBusAccessKind kind) + { Disarm(); operand = first; operandWidth = firstWidth; secondary = second; secondaryWidth = secondWidth; secondaryKind = kind; phase = 1; } + public bool IsCpuPhysicalAddressMapped(uint at, int size, M68kBusAccessKind kind) + { + if (phase == 1 && kind == M68kBusAccessKind.CpuDataWrite && size == operandWidth && unchecked(operand - at) < size) + { Rejected.Add((at, size, kind)); phase = 2; return false; } + if (phase == 2 && kind == secondaryKind && size == secondaryWidth && unchecked(secondary - at) < size) + { Rejected.Add((at, size, kind)); phase = 0; return false; } + return true; + } + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index f4fa7df3..e5de7349 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -12450,3 +12450,82 @@ Evidence is local under `%TEMP%/copper68k-reference-restoration-20261006/`: | parent | `4f6e928d90ad460567c23817ee3680e87579d65ed6b725b17904cc7a41c62ceb` | | adjacent CPU binding | `ec66795f7044ecd74d8db89f7030e0add303663819de732e05dcf6024780157c` | | complete source/evidence linkage | `49210ade00f3abf6de0445284556fcf900fe90b2b7ba8de71c2cecd458159ee5` | + + +## Private actual MOVE secondary entry faults — 2026-10-08 + +`SyntheticM68020MoveOriginEntryFaultTests` adds **307,200 passing cases / two +executions / 112 reports** on EC020, A1200, 020 and 030. Actual B/W/L MOVE +instructions read `(An)` or `(An)+` and attempt `(Am)` writes. Separate groups +enumerate all **384 opcode words** in this private supported subset, canonical +values with all 32 CCR states, and every byte of the operand request followed +by every byte of the 16 descending stack-word requests or vector long read. +All groups cover user/user-M/ISP/MSP, paired low/high operand/stack/VBR locations +and scalar/batch execution. Entry cases include A0/A7 sources, aliases and +postincrement; opcode enumeration covers all source/destination registers. +These are rejected physical requests, not physical partial-transfer tests. + +Independent expectations verify the completed source read, source postincrement +and MOVE flags before the operand fault, every accepted literal C023 frame word, +saved PC/SR, exception sequence, all registers, surrounding memory, active and +inactive stack banks, VBR/SFC/DFC and the exact rejected request widths. The +secondary fault halts with no source reread or operand write. A subsequent +scalar/batch call makes no accesses; a halted batch emits no boundary callback. +No instruction is implicitly retried after partial effects. + +All **1,126,144 parent cases / fourteen executions / 1,488 reports** retain +identical results. Combined: **1,433,344 cases / sixteen executions / 1,600 +reports**. The private **243-input** graph adds one fixture and preserves all +**39 CPU inputs**. The maintained command checks the complete pinned parent +chain, sources/outputs, loaded test assembly, command/settings, TRX roster and +counters, complete case IDs/statuses/weights and every failure witness. +Strict frozen replay passes. + +Four isolated defects produce exact mismatch counts: omitted stack-entry halt +**278,528**; extra stack advance **278,528**; wrong saved PC **57,344**; replayed +source read **307,200**. All unaffected cases pass. A separate verifier checks +all **921,600** exact diagnostic reasons. Eleven copied evidence controls reject +missing reports, empty/foreign/untested combinations, changed source/roster/ +settings, missing witnesses, changed/missing retention and wrong loaded assembly. +Rebased control paths and unlinked hardlink targets preserve original evidence; +these controls execute no CPU. + +The first maintained audit was rejected because the reporter's 10,000-failure +cap truncated mutation witnesses. Entry reports were split by size and bank, +with at most **4,608** cases each. An independent old/new inventory comparison +proves all **307,200** logical cases were preserved. The failed output remains +separate; neither the evidence gate nor the cap was weakened. + +The distinct current **227-input / 37-CPU-input** production graph compiles the +fixture, with exactly two unavailable tests and zero executed cases. Explicit +`NotExecuted` rows and actual counters are recorded; this is compile coverage. + +```powershell +python scripts/test-copper68k-move-origin-entry-fault.py ` + --qualified-parent-directory ` + --output +# --validate-only requires unchanged complete producers, sources and evidence. +``` + +This qualifies the selected private software contract. Wider initial operand +forms, physical partial transfers, refault entry failures, trace/interrupt +continuation, malformed protocol repairs and foreign hardware frames remain +open. Disputed native 030/040 trace boundaries remain unqualified without +hardware. No private CPU import, publication, regression retirement, new full +CPU suite, consumer replay or physical timing qualification is claimed. +Milestone 6 remains **in progress**, `roadmapComplete=false`. + +Evidence is local under `%TEMP%/copper68k-reference-restoration-20261006/`: +`audits/MoveOriginEntryV2`, `audits/MoveOriginEntryDiscoveryV2`, +`move-origin-entry-controls-v2` and `origin-entry-production-default-v2`. + +| Evidence | SHA-256 | +| --- | --- | +| audit | `438b991651b717ce10f32c4d5d4f3c09d3eb60c86251f897185a107d94efb2b5` | +| controls | `79b27d79a14b5ac422d54f7e649f59a09ad6df9e3fe13bac088628192e05a403` | +| reasons | `699c97519acba65d7cdf040ccb08c1fe0c06e2e4570a597c1f2dd10ed57367bf` | +| default | `01228abe06fe150d96c0646806d85e4722daa0ec9a7a50f6e753cb22707d9932` | +| discovery | `a422d22d70ad3fc30a0c4a3a74097318ae50db4ff16c632ca12ebbe8587c95d0` | +| parent | `96abb10dcbe6c0c51f5922a28f2484018b2bbcdaaac9059eaf0416028eb87bd3` | +| resharding equivalence | `602b547cf166d1243cff0a3435f7a0917f2d47168b6c679b1f51c493de563940` | +| complete source/evidence linkage | `7682e265b8bd10afe8986375185aad399aed4da06143a058f44d4158f5869a88` | diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index db8acf4b..3e9e537b 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3909,3 +3909,19 @@ Actual original MOVE origins, physical partial transfers, refault entry faults, trace/interrupts, malformed protocol repair and general hardware frames remain required. Exact scope and evidence are in [the qualification record](COPPER68K_REFERENCE_QUALIFICATION.md#private-returned-bank-pending-write-refaults--2026-10-08). Milestone 6 remains **in progress**, `roadmapComplete=false`. + + +### Milestone 6 private actual MOVE entry faults — 2026-10-08 + +Actual simple MOVE origins add **307,200 passing cases**, retaining **1,126,144 +parent cases**. Separate opcode, value/CCR and entry groups cover all 384 +supported words and rejected operand/stack/vector bytes. All four deliberate +defects are detected; **921,600** exact failure reasons, eleven evidence controls +and strict replay are verified. Sharding preserves every logical case and +failure witness. All 39 private CPU inputs remain unchanged; production compiles +two unavailable tests. No private CPU import, publication or regression retirement. + +Wider operand forms, physical partial transfers, refault entry failures, +trace/interrupts, malformed protocols and hardware frames remain open. See +[scope and evidence](COPPER68K_REFERENCE_QUALIFICATION.md#private-actual-move-secondary-entry-faults--2026-10-08). +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-move-origin-entry-fault.py b/scripts/test-copper68k-move-origin-entry-fault.py new file mode 100644 index 00000000..66b0f648 --- /dev/null +++ b/scripts/test-copper68k-move-origin-entry-fault.py @@ -0,0 +1,144 @@ +"""Audit actual MOVE origins and secondary C023 entry faults. + +Requires the complete immutable MoveWriteRefaultV1 parent. No private CPU +import, hardware-frame qualification, or package publication is performed. +""" +import argparse, contextlib, hashlib, importlib.util, io, itertools, json, os, re, subprocess +from pathlib import Path +import xml.etree.ElementTree as ET +REPO = Path(__file__).resolve().parents[1] +HELPER = REPO/'scripts/test-copper68k-move-write-refault.py' +spec = importlib.util.spec_from_file_location('write_validation', HELPER) +m = importlib.util.module_from_spec(spec);spec.loader.exec_module(m) +sha,load,save,require,inventory=m.sha,m.load,m.save,m.require,m.inventory +FIXTURE='Copper68k.Tests/Synthetic/SyntheticM68020MoveOriginEntryFaultTests.cs' +CPU='Copper68k/M68kAdvancedTimingInterpreter.FinalWrite020.cs' +PARENT_PROOF='96abb10dcbe6c0c51f5922a28f2484018b2bbcdaaac9059eaf0416028eb87bd3' +MODELS=['68EC020','68020','68030','A1200'] +VARIANTS=['candidate','no-halt','stack-advance','wrong-saved-pc','source-replay'] +FILTER='FullyQualifiedName~SyntheticM68020MoveOriginEntryFaultTests' +ENV={'COPPER68K_RUN_020_MOVE_ORIGIN_ENTRY':'1',**m.ENV} +NS=m.NS + + +def once(text,old,new): + require(text.count(old)==1,'Nonunique origin-entry mutation') + return text.replace(old,new) + + +def sources(parent,variant): + prior=load(parent/'candidate/execution.json')['sources'] + require(len(prior)==242 and FIXTURE not in prior,'Incomplete origin-entry parent') + data={n:(parent/'candidate/source'/n).read_bytes() for n in prior} + require(all(hashlib.sha256(v).hexdigest()==prior[n] for n,v in data.items()),'Changed origin-entry parent sources') + data[FIXTURE]=(REPO/FIXTURE).read_bytes() + text=data[CPU].decode('utf-8').replace('\r\n','\n') + stack='if (!_physicalAddressMap!.IsCpuPhysicalAddressMapped(State.A[7], 2, M68kBusAccessKind.CpuDataWrite))\n { State.Halted = true; _instructionPipe.Reset(); return; }' + if variant=='no-halt':text=once(text,stack,stack.replace('State.Halted = true','State.Halted = false')) + elif variant=='stack-advance':text=once(text,stack,stack.replace('State.Halted = true','State.SetActiveStackPointer(unchecked(State.A[7] + 2)); State.Halted = true')) + elif variant=='wrong-saved-pc':text=once(text,'words[0] = sr; Long(2, fault.NextPc);','words[0] = sr; Long(2, unchecked(fault.NextPc + 2));') + elif variant=='source-replay':text=once(text,'var sr = State.StatusRegister;','var sr = State.StatusRegister; _ = ReadSized(State.A[fault.Opcode & 7], FinalMoveSize020(fault.Opcode));') + if variant!='candidate':data[CPU]=text.encode('utf-8') + return data + + +def command(root,variant): + prior=m.command(root,'candidate');selection=FILTER+('|'+prior[prior.index('--filter')+1] if variant=='candidate' else '') + return ['dotnet','test',str(root/'source/Copper68k.Tests/Copper68k.Tests.csproj'),'-c','Release','--artifacts-path',str(root/'build'),'--filter',selection,'--logger','trx;LogFileName=audit.trx','--results-directory',str(root)] + + +def cases(kind): + for width,bank,location in itertools.product([1,2,4],['user','user-M','ISP','MSP'],range(2)): + if kind=='opcodes':forms=itertools.product(range(8),range(8),[False,True],[2],[31],[0],[0],[0]) + elif kind=='boundaries':forms=itertools.product([0],[1],[True],range(4),range(32),[0],[0],[0]) + else:forms=((source,source if alias else 1,post,2,ccr,byte,entry,entry_byte) for source,alias,post,ccr,byte,entry in itertools.product([0,7],[False,True],[False,True],[0,31],range(width),range(17)) for entry_byte in range(4 if entry==16 else 2)) + for source,destination,post,value,ccr,byte,entry,entry_byte in forms: + yield width,bank,location,source,destination,post,value,ccr,byte,entry,entry_byte + + +def groups(): + for kind in ['opcodes','entry','boundaries']: + if kind=='entry': + for width,bank in itertools.product([1,2,4],['user','user-M','ISP','MSP']):yield kind,width,bank + else:yield kind,0,None + + +def expected(model,kind,variant,selected_width=0,selected_bank=None): + rows={};opcodes=set() + for width,bank,location,source,destination,post,value,ccr,byte,entry,entry_byte in cases(kind): + if selected_width and (width!=selected_width or bank!=selected_bank):continue + opcode=({1:0x1090,2:0x3090,4:0x2090}[width])|source|(destination<<9)|(8 if post else 0);opcodes.add(opcode) + bad=variant=='source-replay' or variant in ['no-halt','stack-advance'] and entry!=16 or variant=='wrong-saved-pc' and entry>=14 + key=f'{model}/MOVE/origin-entry/{kind}/opcode={opcode:04X}/width={width}/bank={bank}/location={location}/source={source}/destination={destination}/post={post}/value={value}/ccr={ccr}/operand-byte={byte}/entry={entry}/entry-byte={entry_byte}' + rows[key]={'mismatching' if bad else 'passing':1} + require(len(rows)==(1152*selected_width if kind=='entry' else 3072),'Wrong origin-entry inventory') + if kind=='opcodes':require(len(opcodes)==384 and {0x1290,0x3298,0x2e9f}<=opcodes,'Incomplete simple MOVE word enumeration') + return rows + + +def verify(root,variant,data,retained,parent_names): + e=load(root/'execution.json');ids={n:hashlib.sha256(v).hexdigest() for n,v in data.items()} + require(e['sources']==ids==inventory(root/'source'),'Changed complete origin-entry source inventory') + require(e['command']==command(root,variant) and e['settings']==ENV and e['exit']==(0 if variant=='candidate' else 1),'Wrong origin-entry command, settings or exit') + outputs=['execution.log','audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + require(e['outputs']=={n:sha(root/n) for n in outputs},'Changed origin-entry outputs') + tree=ET.parse(root/'audit.trx');tests=tree.findall('.//t:UnitTestResult',NS) + names={f'{route}MoveOriginEntryFaults':'Passed' if variant=='candidate' else 'Failed' for route in ['Scalar','Batch']} + if variant=='candidate':names.update(parent_names) + require(len(tests)==len(names) and {x.get('testName').split('.')[-1]:x.get('outcome') for x in tests}==names,'Wrong or empty origin-entry selection') + counter=tree.find('.//t:Counters',NS);failed=sum(v=='Failed' for v in names.values()) + require(all(int(counter.get(k,-1))==v for k,v in dict(total=len(names),executed=len(names),passed=len(names)-failed,failed=failed,notExecuted=0).items()),'Wrong origin-entry counters') + require(tree.find('.//t:ResultSummary',NS).get('outcome')==('Completed' if variant=='candidate' else 'Failed'),'Wrong origin-entry outcome') + definitions={x.get('id'):x for x in tree.findall('.//t:UnitTest',NS)} + require(all(Path(definitions[x.get('testId')].get('storage')).resolve()==(root/'build/bin/Copper68k.Tests/release/Copper68k.Tests.dll').resolve() for x in tests),'Wrong loaded origin-entry test assembly') + stdout='\n'.join(x.text or '' for x in tree.findall('.//t:UnitTestResult/t:Output/t:StdOut',NS));reports=[];totals=dict(passing=0,mismatching=0,unsupported=0,untested=0) + def summary(d): + matches=re.findall(re.escape(d['model']+'/'+d['group']+': ')+r'(\{[^\r\n]+\}); logical cases=(\d+), xUnit batches=1, combinations=(\d+)',stdout) + require(len(matches)==1 and json.loads(matches[0][0])==d['counts'] and int(matches[0][1])==d['logicalCases'] and int(matches[0][2])==len(d['combinations']),'Changed-input report/TRX disagreement') + for model,(kind,width,bank),route in itertools.product(MODELS,list(groups()),['scalar','batch']): + suffix=f'-{width}-{bank}' if kind=='entry' else '' + group=f'move-origin-entry-{kind}{suffix}-{route}';path=root/f'{model}-{group}.json';d=load(path);wanted=expected(model,kind,variant,width,bank) + counts={s:sum(v.get(s,0) for v in wanted.values()) for s in totals} + require(d['schema']==1 and d['model']==model and d['group']==group and d['logicalCases']==len(wanted) and d['xunitBatches']==1 and d['counts']==counts and d['combinations']==wanted,'Wrong origin-entry combinations: '+path.name) + bad={n for n,v in wanted.items() if 'passing' not in v} + require(len(d['failures'])==len(bad) and {x['id'] for x in d['failures']}==bad and all(wanted[x['id']]=={x['status']:1} and x['reason'] for x in d['failures']),'Wrong complete origin-entry failure witnesses') + summary(d);reports.append(dict(path=path.name,sha256=sha(path))) + for s in totals:totals[s]+=counts[s] + if variant=='candidate': + require(len(retained)==1488,'Incomplete origin-entry retention') + for name,wanted in retained.items(): + d=load(root/name);require(d==wanted,'Changed retained origin-entry-parent report: '+name);summary(d);reports.append(dict(path=name,sha256=sha(root/name))) + require({x.name for x in root.glob('*.json')}=={x['path'] for x in reports}|{'execution.json'},'Missing or foreign origin-entry reports') + return dict(variant=variant,counts=totals,retentionCases=1126144 if variant=='candidate' else 0,executions=len(names),reports=reports,executionSha256=sha(root/'execution.json')) + + +def audit(parent,output,validate): + require(sha(parent/'proof.json')==PARENT_PROOF,'Wrong origin-entry parent proof') + with contextlib.redirect_stdout(io.StringIO()):m.audit(parent.parent/'MoveWriteChangedInputV2',parent,True) + linkage=dict(producers={f.relative_to(REPO).as_posix():sha(f) for f in [Path(__file__),HELPER,REPO/FIXTURE]},parentProofSha256=PARENT_PROOF,variants=VARIANTS,settings=ENV) + if not validate:output.mkdir(parents=True,exist_ok=False);save(output/'inputs.json',linkage) + require(load(output/'inputs.json')==linkage,'Changed origin-entry producers or selection') + retained={f.name:load(f) for f in (parent/'candidate').glob('*-move-write-*.json')} + tree=ET.parse(parent/'candidate/audit.trx');parent_names={x.get('testName').split('.')[-1]:x.get('outcome') for x in tree.findall('.//t:UnitTestResult',NS)} + require(len(parent_names)==14 and set(parent_names.values())=={'Passed'},'Incomplete origin-entry parent roster') + entries=[] + for variant in VARIANTS: + root=output/variant;data=sources(parent,variant) + if not validate: + (root/'source').mkdir(parents=True) + for n,v in data.items(): + f=root/'source'/n;f.parent.mkdir(parents=True,exist_ok=True);f.write_bytes(v) + env={k:v for k,v in os.environ.items() if not k.startswith('COPPER68K_RUN_')};env.update(ENV);env['COPPER68K_SYNTHETIC_REPORT_DIR']=str(root) + with (root/'execution.log').open('w',encoding='utf-8') as log:done=subprocess.run(command(root,variant),env=env,stdout=log,stderr=subprocess.STDOUT) + outputs=['execution.log','audit.trx','build/bin/Copper68k/release/Copper68k.dll','build/bin/Copper68k.Tests/release/Copper68k.Tests.dll'] + save(root/'execution.json',dict(command=command(root,variant),settings=ENV,exit=done.returncode,sources={n:hashlib.sha256(v).hexdigest() for n,v in data.items()},outputs={n:sha(root/n) for n in outputs})) + entry=verify(root,variant,data,retained,parent_names);entries.append(entry);print(variant,entry['counts'],'retention',entry['retentionCases'],flush=True) + proof=dict(schema=1,inputsSha256=sha(output/'inputs.json'),entries=entries,sourceCount=243,productionImport=False,publication=False,hardwareQualified=False,architecturalPromotion=False,roadmapComplete=False) + if validate:require(load(output/'proof.json')==proof,'Changed complete origin-entry proof') + else:save(output/'proof.json',proof) + print('Complete private MOVE-origin-entry audit',sha(output/'proof.json'),flush=True) + + +if __name__=='__main__': + parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--qualified-parent-directory',type=Path,required=True);parser.add_argument('--output',type=Path,required=True);parser.add_argument('--validate-only',action='store_true') + args=parser.parse_args();audit(args.qualified_parent_directory.resolve(),args.output.resolve(),args.validate_only) From c284b948263501183136656307cab2ecce6e0479 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 15:42:41 +0300 Subject: [PATCH 158/211] Consolidate EOR byte postincrement semantic regression --- Copper68k.Tests/M68kLogicalTests.cs | 26 ------- Copper68k.Tests/Synthetic/README.md | 16 +++++ .../SyntheticEorPostincrementTests.cs | 39 +++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 70 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 14 ++++ ...-copper68k-eor-postincrement-retirement.py | 57 +++++++++++++++ scripts/test-copper68k-synthetic.ps1 | 1 + 7 files changed, 197 insertions(+), 26 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticEorPostincrementTests.cs create mode 100644 scripts/test-copper68k-eor-postincrement-retirement.py diff --git a/Copper68k.Tests/M68kLogicalTests.cs b/Copper68k.Tests/M68kLogicalTests.cs index 26e72b4d..a08dd53b 100644 --- a/Copper68k.Tests/M68kLogicalTests.cs +++ b/Copper68k.Tests/M68kLogicalTests.cs @@ -72,32 +72,6 @@ public void ClrLongPredecrementAddressErrorKeepsDecrementedAddressRegister() Assert.Equal(0x1002u, bus.ReadLong(0x4FFC)); } - [Fact] - public void EorBytePostincrementDestinationAdvancesOnce() - { - var bus = new Copper68kTestBus(); - bus.WriteWords(0x1000, 0xBF1B); // EOR.B D7,(A3)+ - bus.Memory[0x2000] = 0xBB; - - var cpu = new M68kInterpreter(bus); - cpu.Reset(0x1000, 0x3000); - cpu.State.A[3] = 0x2000; - cpu.State.D[7] = 0x6A34_196D; - cpu.State.StatusRegister = M68kCpuState.Supervisor | M68kCpuState.Extend | - M68kCpuState.Negative | M68kCpuState.Zero | M68kCpuState.Overflow | - M68kCpuState.Carry; - - cpu.ExecuteInstruction(); - - Assert.Equal(0x2001u, cpu.State.A[3]); - Assert.Equal(0xD6, bus.Memory[0x2000]); - Assert.True(cpu.State.GetFlag(M68kCpuState.Negative)); - Assert.False(cpu.State.GetFlag(M68kCpuState.Zero)); - Assert.False(cpu.State.GetFlag(M68kCpuState.Overflow)); - Assert.False(cpu.State.GetFlag(M68kCpuState.Carry)); - Assert.True(cpu.State.GetFlag(M68kCpuState.Extend)); - } - [Fact] public void EoriToSrClearsUndefinedStatusRegisterBits() { diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 19505a4e..a54a6be2 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1600,3 +1600,19 @@ shards preserve every diagnostic without changing logical cases. All 39 private CPU inputs remain frozen; the distinct production graph compiles two unavailable tests. Wider operands, partial transfers, refault entry, trace/interrupts and hardware qualification remain open. See [the actual MOVE entry record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#private-actual-move-secondary-entry-faults--2026-10-08). + + +Captured EOR byte postincrement consolidation runs in ordinary CI as +`logical-eor-postincrement-captured` (4,096 cases per profile). All register +selections, CCR states and user/supervisor stacks preserve the captured BF1B +operand semantics and A7 byte stride. Reproduce the exact one-fact retirement: + +```powershell +python scripts/test-copper68k-eor-postincrement-retirement.py ` + --output +``` + +`--prepare` verifies the original witness before removal; `--finish-retirement` +checks the exact reduced source; `--validate-only` requires unchanged complete +evidence. Both execution paths are mutated only in isolated builds. The +address-error/trace siblings remain. See [the retirement record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#eor-byte-postincrement-consolidation--2026-10-08). diff --git a/Copper68k.Tests/Synthetic/SyntheticEorPostincrementTests.cs b/Copper68k.Tests/Synthetic/SyntheticEorPostincrementTests.cs new file mode 100644 index 00000000..f94e0ad6 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticEorPostincrementTests.cs @@ -0,0 +1,39 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticEorPostincrementTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void CapturedEorBytePostincrementPreservesSourceAndAdvancesOnce(string modelId) + { + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "logical-eor-postincrement-captured"); + for (var addressReg = 0; addressReg < 8; addressReg++) + for (var dataReg = 0; dataReg < 8; dataReg++) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + // M68000PM EOR: Dn XOR destination at byte width; preserve X and + // the full source register, set N/Z, clear V/C. Captured BB XOR + // 6D = D6 from the historical BF1B regression; A7 byte stride is 2. + machine.Reset(ccr, supervisor); + machine.Core.State.D[dataReg] = 0x6a34196d; + if (addressReg != 7) machine.Core.State.A[addressReg] = 0x2000; + var address = machine.Core.State.A[addressReg]; + var opcode = (ushort)(0xb118 | dataReg << 9 | addressReg); + if (dataReg == 7 && addressReg == 3) Assert.Equal(0xbf1b, opcode); + var operand = new OperandFixture(machine, [opcode], new(3, addressReg), 1, 0xbb, source: false); + Assert.Equal(address, operand.Address); + var expected = operand.Expected; + expected.A[addressReg] = address + (addressReg == 7 ? 2u : 1u); + expected.Sr = (ushort)((expected.Sr & 0xfff0) | 8); + operand.Write(0xd6); + SyntheticExecution.Run(machine, expected, report, + $"{modelId}/EOR/1/D{dataReg}->(A{addressReg})+/captured/super={supervisor}/op={opcode:X4}/ccr={ccr:X2}"); + } + report.Complete(output); + } +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index e5de7349..54aa940f 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -12529,3 +12529,73 @@ Evidence is local under `%TEMP%/copper68k-reference-restoration-20261006/`: | parent | `96abb10dcbe6c0c51f5922a28f2484018b2bbcdaaac9059eaf0416028eb87bd3` | | resharding equivalence | `602b547cf166d1243cff0a3435f7a0917f2d47168b6c679b1f51c493de563940` | | complete source/evidence linkage | `7682e265b8bd10afe8986375185aad399aed4da06143a058f44d4158f5869a88` | + + +## EOR byte postincrement consolidation — 2026-10-08 + +The pure semantic fact `M68kLogicalTests.EorBytePostincrementDestinationAdvancesOnce` +is replaced by `SyntheticEorPostincrementTests` with **32,768 passing cases / +eight batches** across all eight selected profiles. The captured `BF1B` operands +remain literal: D7=`6A34196D`, destination byte=`BB`, result=`D6`; the canonical +supervisor/CCR=31/A3 case retains destination `2000` and final A3=`2001`. +Its fixture SR is `2718`, preserving the fixture's IPL=7; the original fact used +IPL=0 (`2018`) and asserted the same five CCR flags, not the full SR. +The matrix extends this case to every Dn/An selection, both user/supervisor stacks +and all 32 initial CCR states. A7 uses stride 2; other address registers use 1. +The shared operand fixture and common verification check exact PC, all registers, +preserved full source Dn and X, N/Z/V/C, stack selection and guarded memory. +Expectations use literal XOR/flag results and independent addresses; they do not +call production arithmetic, decoding, EA or timing helpers. + +Before retirement, the isolated clean run passed **13 tests**: eight new batches +plus all five original logical regressions. A targeted extra-postincrement defect +in the two actual execution paths fails all **32,768 replacement cases** and the +original fact. Every exact failure ID and `A expected ..., actual ...` message, +including the A7 stride difference, is independently generated and checked. The +original fact specifically fails expected `8193`, actual `8194`. This proves a +targeted defect, not a claim that historical source contained that defect. + +The preparation proof passed nine copied evidence controls before removal. +Only that one fact was then removed from source pinned at +`9430fc416714c5f68a9757cd44ebe6ee72ee7ed6`; its three address-error siblings and +EORI-to-SR trace regression remain unchanged. The current run passes **12 tests**: +all eight replacement batches plus those four retained siblings. Strict +`--validate-only` replay passes with complete source inventories, exact roster, +counts, case keys/weights/statuses/reasons, commands and producer identities. +Controls reject missing fixture, changed producer, empty selection, missing old +witness, wrong weight/reason, changed mutation plus manifest, wrong command and +wrong counters. No controls execute CPU instructions. + +The **228-input / 37-CPU-input** graphs are separately bound: mutation differs +from clean in exactly two CPU files; current differs from clean only in the +single legacy-test file. Production CPU inputs remain unchanged. An additional +linkage verifier binds compiled CPU/test assemblies, adjacent CPU copies, actual +TRX loaded-test paths, logs, full current sources and the ordinary gate script. +The gate now requires **4,096 cases per profile** for the new group, and its +PowerShell syntax check passes. This turn runs the affected tests; it does not +claim a new full CPU suite, consumer replay, external reference audit or physical +timing qualification. + +```powershell +python scripts/test-copper68k-eor-postincrement-retirement.py ` + --output +# --prepare proves the old witness before source retirement. +# --finish-retirement adds current-source verification to prepared evidence. +# --validate-only rejects incomplete or changed frozen evidence. +``` + +Failed preparation directories v1-v3 remain separate. V1 rejected a nonunique +mutation target caused by unnormalized line endings. V2/v3 correctly rejected +wrong predicted cross-model outcomes: the first mutation missed the advanced +general-logical path. V4 targets both executed paths; no failure was excluded or +gate weakened. Milestone 6 remains **in progress**, `roadmapComplete=false`. +Other reference/implementation gaps remain as recorded above. + +Local evidence under `%TEMP%/copper68k-reference-restoration-20261006/`: +`eor-postincrement-consolidation-v4`, `eor-consolidation-complete-link-v1.json`. + +| Evidence | SHA-256 | +| --- | --- | +| pre-retirement preparation | `f5939ba511b6e18a1df987bc89bf38a785b8ec8ef9da7832b3664b222f39caad` | +| complete consolidation | `460acbf420eb434df30ef49b5da0261df2cca64893dd1106c92a2990b85ba838` | +| source/assembly/evidence linkage | `7e8728565e571f020529d5e63f29fa9eb48df45a00d4d702672de1aff43b8c5c` | diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 3e9e537b..2d8b433c 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3925,3 +3925,17 @@ Wider operand forms, physical partial transfers, refault entry failures, trace/interrupts, malformed protocols and hardware frames remain open. See [scope and evidence](COPPER68K_REFERENCE_QUALIFICATION.md#private-actual-move-secondary-entry-faults--2026-10-08). Milestone 6 remains **in progress**, `roadmapComplete=false`. + + +### Milestone 6 EOR postincrement consolidation — 2026-10-08 + +The captured pure EOR byte postincrement fact is replaced by **32,768 passing +cases** across all eight profiles, every Dn/An, user/supervisor and all CCR states. +Before retirement, an extra-increment mutation failed both the original fact +and all replacement cases with exactly checked diagnostics. Nine evidence +controls passed. After exact one-fact removal, eight replacement batches and +all four retained address-error/trace siblings pass; strict replay and source/ +assembly linkage pass. The ordinary gate requires 4,096 cases per profile. +All 37 production CPU inputs remain unchanged; no private CPU import or release. +See [the retirement mapping and evidence](COPPER68K_REFERENCE_QUALIFICATION.md#eor-byte-postincrement-consolidation--2026-10-08). +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-eor-postincrement-retirement.py b/scripts/test-copper68k-eor-postincrement-retirement.py new file mode 100644 index 00000000..a75a160a --- /dev/null +++ b/scripts/test-copper68k-eor-postincrement-retirement.py @@ -0,0 +1,57 @@ +"""Prove captured EOR postincrement replacement before retiring its old fact.""" +from pathlib import Path +import itertools,re,subprocess +from copper68k_consolidation import Audit,check + +REPO=Path(__file__).resolve().parents[1] +GROUP='logical-eor-postincrement-captured' + +def mutate(path,text): + text=text.replace('\r\n','\n') + if path.endswith('M68kAdvancedTimingInterpreter.Logical.cs'): + old='if (mode == 3) WriteGeneralRegister(true, reg, unchecked(address + M68kIntegerSemantics.AddressIncrement(reg, size)));' + check(text.count(old)==1,'Missing unique advanced logical postincrement target') + return text.replace(old,'if (mode == 3) WriteGeneralRegister(true, reg, unchecked(address + M68kIntegerSemantics.AddressIncrement(reg, size) * (top == 11 && size == M68kOperandSize.Byte ? 2u : 1u))); // intentional second EOR increment') + old=''' result = eaValue ^ regValue; + CompletePredecrementLongDestinationBeforeWrite(); + eaOperand.Write(result); + SetLogicFlags(result, operandSize);''' + check(text.count(old)==1,'Missing unique EOR postincrement mutation') + new=old.replace(' SetLogicFlags', ' if (mode == 3 && operandSize == M68kOperandSize.Byte) SetAddressRegister(eaReg, State.A[eaReg] + AddressIncrement(eaReg, operandSize)); // intentional second increment\n SetLogicFlags') + return text.replace(old,new) + +def report(model,mode): + combinations,failures={},[] + for ar,dr,supervisor,ccr in itertools.product(range(8),range(8),[False,True],range(32)): + key=f'{model}/EOR/1/D{dr}->(A{ar})+/captured/super={supervisor}' + status='mismatching' if mode=='Mutation' else 'passing' + counts=combinations.setdefault(key,{});counts[status]=counts.get(status,0)+1 + if mode=='Mutation': + at=(0x4700 if supervisor else 0x7800) if ar==7 else 0x2000 + stride=2 if ar==7 else 1 + failures.append(dict(id=f'{key}/op={0xb118|dr<<9|ar:04X}/ccr={ccr:02X}',status=status,reason=f'A{ar} expected {at+stride:08X}, actual {at+stride*2:08X}')) + return dict(schema=1,model=model,group=GROUP,counts=dict(passing=0 if mode=='Mutation' else 4096,mismatching=4096 if mode=='Mutation' else 0,unsupported=0,untested=0),logicalCases=4096,xunitBatches=1,combinations=combinations,failures=failures) + +class EorAudit(Audit): + def __init__(self,spec): + self.s=spec;self.repo=spec['repo'] + self.original=subprocess.check_output(['git','show',spec['pin']+':'+spec['legacy']],cwd=self.repo).decode('utf-8-sig').replace('\r\n','\n') + marker='\t[Fact]\n\tpublic void '+spec['witness']+'()' + check(self.original.count(marker)==1,'Missing unique historical EOR fact') + start=self.original.index(marker);end=self.original.index('\t[Fact]\n',start+len(marker)) + self.reduced=self.original[:start]+self.original[end:] + +def witness_error(name,message): + check(re.search(r'Expected:\s*8193\b',message) and re.search(r'Actual:\s*8194\b',message),'Wrong historical EOR second increment') + +def make_audit(): + return EorAudit(dict(repo=REPO,producer=Path(__file__).resolve(),pin='9430fc416714c5f68a9757cd44ebe6ee72ee7ed6', + legacy='Copper68k.Tests/M68kLogicalTests.cs',legacy_class='Copper68k.Tests.M68kLogicalTests',legacy_cases=5, + witness='EorBytePostincrementDestinationAdvancesOnce',witness_cases=1, + fixture='Copper68k.Tests/Synthetic/SyntheticEorPostincrementTests.cs',shared_class='Copper68k.Tests.Synthetic.SyntheticEorPostincrementTests', + method='CapturedEorBytePostincrementPreservesSourceAndAdvancesOnce',group=GROUP, + mutation_files=['Copper68k/M68kCore.cs','Copper68k/M68kAdvancedTimingInterpreter.Logical.cs'],mutate=mutate,report=report, + witness_outcome=lambda name:'Failed',witness_error=witness_error, + description=__doc__)) + +if __name__=='__main__':make_audit().main() diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 54f538ce..04196a59 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -96,6 +96,7 @@ try { 'logical-boundaries'=31808; 'logical-and-indirect-captured'=12288; 'logical-not-displacement-captured'=3072; + 'logical-eor-postincrement-captured'=4096; 'logical-invalid-operands'=13248; 'logical-unary-invalid-operands'=$(if ($model -in @('68000','68010')) {12544} elseif ($model -eq '68060') {10304} else {10432}); 'logical-bitfield-invalid-operands'=13312; From 66968c289c7f1f2bbaa889485345b27a67b10014 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 15:51:30 +0300 Subject: [PATCH 159/211] Record milestone six completion requirements and open gates --- Copper68k.Tests/Synthetic/README.md | 7 +- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 88 +++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 4 + ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 4 + 4 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index a54a6be2..d925cd93 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -15,8 +15,11 @@ From the CopperMod root, run: ``` Milestones 1–5 execute MOVE/MOVEA, transfer/address, arithmetic/comparison, -logical/bit/shift/atomic and control/system operations in -**8,761,672 logical cases** across **386 reporting xUnit batches**, across seven models and the A1200 profile. +logical/bit/shift/atomic and control/system operations across seven models and +the A1200 profile. Each run emits its actual logical case and reporting batch +counts; dated qualification records retain earlier totals. The +[milestone 6 completion audit](../../docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md) +records the current completion requirements and unresolved gates. Ordinary `dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release` includes these batches. CI additionally validates every required report and exact count; missing reports, mismatches, unsupported execution and empty groups fail the gate. diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md new file mode 100644 index 00000000..a5ffbbe3 --- /dev/null +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -0,0 +1,88 @@ +# Milestone 6 completion audit + +This is the current completion checklist for **Reference qualification and +consolidation**, derived from the accepted +[synthetic-suite plan](COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md). +Milestone 6 is **in progress**. A passing focused audit, inventory entry or +software-reference correction does not complete the milestone. + +The detailed [qualification record](COPPER68K_REFERENCE_QUALIFICATION.md) +retains historical checkpoints and rejected attempts. This checklist separates +completion requirements from the many bounded experiments recorded there; +it does not replace or discard their remaining gaps. + +## Required evidence + +| Requirement from the accepted plan | Authoritative evidence needed | Current assessment | +| --- | --- | --- | +| Independent audits across 000, 010, EC020, 020, 030, 040, 060 and A1200 | Pinned reference inputs and adapters; executed selections for every profile; explicit exclusions and comparator checks | Scoped SingleStepTests, Musashi and WinUAE audits exist. The broad WinUAE audit still has two unresolved mismatching directories. | +| Complete integer-family inventory; later families cannot disappear | `IntegerInventory.cs`, generated model/family inventory and named execution reports | Inventory and reporting are implemented. Inventory membership alone does not prove execution or internal restoration coverage. | +| Coverage by architectural combination, with passing/mismatching/unsupported/untested distinct | Actual per-model report keys, weights and statuses; exact named TRX selection; no empty or substituted reports | The ordinary gate validates required groups and counts. A fresh current-source full run is underway; its result is not yet qualified. | +| Deterministic coverage in ordinary CI | Current `test-copper68k-synthetic.ps1` and CI invocation; all required reports and positive case counts | Implemented, including 4,096 EOR postincrement cases per profile. Final current-source execution linkage remains pending. | +| Explicit deep-audit command, deterministic seeds and strict missing/empty/mismatch rejection | Deep command/settings, recorded seed and selected inputs; rejected negative controls | Commands and earlier scoped proofs exist. Collect the final linkage to their pinned sources and distinguish historical executions from current ones. | +| Reuse SingleStepTests, Musashi and WinUAE with pinned identities and caveats | Adapter source, reference manifests, source/input hashes, actual execution and documented exclusions | Implemented. SingleStepTests supplies 000 semantic fixtures; Musashi runs independent self-checking programs, not its CPU as an oracle. WinUAE remains a software reference. | +| Replacement detects absolute decoding, extension length, index sign, alias order, A7 stride and flags defects | Six distinct isolated mutation proofs with precise replacement IDs and current source linkage | The maintained mutation command defines all six. Historical proofs need final source/evidence linkage; a mutation definition is not execution evidence. | +| Retire an old regression only after a mapped replacement detects its defect | Exact pinned old method/rows, before-removal proof, mutation witness and after-removal sibling verification | Recent NOT displacement and EOR postincrement retirements have exact proofs. EOR replaces one fact with 32,768 passing cases and retains four siblings. No blanket regression deletion is authorized. | +| Retain cache, prefetch, bus ordering, fault sequencing, JIT and native regressions unless separately proven redundant | Exact test-source diff and full named roster, with every removal accounted for | Specialized tests remain. Refresh the complete current roster and compare it with the previous frozen full run. | +| Production CPU fixes pass full CPU and affected consumers | Same-source full CPU proof; isolated NuGet package; CopperScreen production, host/disk/engine and applicable native results | MOVE16 recovery has full-suite and local .77 consumer evidence. Subsequent consolidation leaves all 37 CPU source inputs unchanged. No new consumer execution is claimed. | +| Preserve successful ordering and timing policy; no automatic retry after partial effects | Relevant sequence/fault tests and change review, separate from timing qualification | Existing checks and scoped explicit-RTE continuation proofs remain. Private candidate results are not production behavior or physical timing qualification. | +| Test-internal framework; public factory; NuGet consumer boundary; immutable published versions | Source/project/package diffs and consumer dependency inspection | Preserved. Private 39-input CPU candidates remain separate from the production 37-input CPU graph. No publication is authorized by this audit. | + +## Gates that still prevent completion + +1. **The broad software-reference gate is failed.** The retained + `QualifiedBasicMove16RecoveryV1` audit reports 1,379 passing directories and + two mismatching directories, with actual test exit 1. The first failures are + 010 RTE invalid-format CCR and 060 ordinary STOP with a new S-clear SR. Later + failures in those directories remain unobserved. Passing qualified presets + do not turn this broad audit green. +2. **Private continuation work is not production qualification.** The 020/030 + read/write recovery candidates and their literal private formats have scoped + software evidence but are not imported into the public CPU. The detailed + record still lists wider restoration, operand, repair/refault, trace/interrupt + and foreign-frame gaps. Preserve those distinctions when reviewing the + promoted integer/system families. +3. **The final evidence set must be coherent.** Link the current complete + source graph, roster, ordinary reports, references, seeded audits and + consolidation witnesses. Do not substitute the latest focused test for a + full suite, relabel an unavailable test as coverage, or combine different + fixture revisions as one executed run. + +For ordinary STOP, [M68000PM 6-85](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) +describes the incoming supervisor check followed by SR transfer and stopping. +[MC68060UM 8.2.5/8.2.6](https://www.nxp.com/docs/en/data-sheet/MC68060UM.pdf) +defines incoming privilege and trace processing. LPSTOP has an explicit new +S-clear rejection rule; these sections do not explicitly establish that rule +for ordinary STOP. The pinned WinUAE implementation labels its STOP rule +undocumented. The disagreement therefore remains unresolved; no guessed CPU +restriction or reference exclusion is justified by this review. + +No hardware is available for the disputed native 030/040 trace boundaries. +Those observations remain unqualified. This does not make other software work +blocked, and software agreement must not be described as hardware evidence. + +## Scope boundaries + +The accepted roadmap excludes FPU arithmetic, enabled-MMU operation, physical +pipeline/cache qualification and OS compatibility. Physical bus/pin/timing +claims remain separate from semantic and timing-policy results. This checklist +does not add those excluded topics as milestone completion requirements. +Conversely, documented architectural exceptions, legal instruction semantics, +saved PC/SR, stack selection and promoted system-operation behavior remain +within the plan; a software-reference caveat cannot hide an implementation gap. + +Diagnostic 010/060 qualification remains separate from desktop readiness. +Publication is a separate release step. PR #22 remains a draft, and +`roadmapComplete=false` until a requirement-by-requirement final audit proves +completion. + +## Current validation work — 2026-10-08 + +Current source checkpoint: `c284b948263501183136656307cab2ecce6e0479`. +The fresh isolated `CurrentReferenceFullV1` run snapshots **228 test/CPU inputs**, +including **37 production CPU inputs** identical to the previous full-run CPU +sources, and enables the same ten qualified WinUAE presets. Its outputs are +local under `%TEMP%/copper68k-reference-restoration-20261006/audits/`. +The run is still in progress; neither success nor final logical counts are +claimed here. It does not include the separately failing broad Basic audit or +private CPU imports. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 54aa940f..6dd13a11 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -4,6 +4,10 @@ Milestone 6 is in progress. This is scoped software-reference evidence, not exhaustive external coverage, physical CPU qualification or desktop readiness. 68010 and 68060 remain diagnostic profiles. +The [milestone 6 completion audit](COPPER68K_MILESTONE6_COMPLETION_AUDIT.md) +maps the accepted requirements to current evidence and outstanding gates. +The dated records below remain scoped historical evidence. + ## Sources and reproducibility The [Motorola programmer reference](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 2d8b433c..f35e1f10 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -117,6 +117,10 @@ pipeline/cache qualification and OS compatibility are outside this roadmap. ## Implementation evidence +The [milestone 6 completion audit](COPPER68K_MILESTONE6_COMPLETION_AUDIT.md) +maps the accepted requirements to evidence and remaining gates. Dated records +below remain historical; focused proofs do not establish whole-roadmap completion. + Record commands, logical case counts, failure discoveries, fixes, reference identities, replacement proofs and remaining required coverage here as work progresses. No milestone is complete merely because an inventory exists or a From 807756a05248c038854c4ba048c7e8edf19b0d83 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 16:05:07 +0300 Subject: [PATCH 160/211] Link current seeded and independent reference evidence --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 25 +++++++++++++++++-- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 8 ++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 8 ++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index a5ffbbe3..799a8373 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -15,11 +15,11 @@ it does not replace or discard their remaining gaps. | Requirement from the accepted plan | Authoritative evidence needed | Current assessment | | --- | --- | --- | -| Independent audits across 000, 010, EC020, 020, 030, 040, 060 and A1200 | Pinned reference inputs and adapters; executed selections for every profile; explicit exclusions and comparator checks | Scoped SingleStepTests, Musashi and WinUAE audits exist. The broad WinUAE audit still has two unresolved mismatching directories. | +| Independent audits across 000, 010, EC020, 020, 030, 040, 060 and A1200 | Pinned reference inputs and adapters; executed selections for every profile; explicit exclusions and comparator checks | Fresh SingleStepTests 000 and Musashi eight-profile audits pass on the frozen current assemblies. The broad WinUAE audit still has two unresolved mismatching directories. | | Complete integer-family inventory; later families cannot disappear | `IntegerInventory.cs`, generated model/family inventory and named execution reports | Inventory and reporting are implemented. Inventory membership alone does not prove execution or internal restoration coverage. | | Coverage by architectural combination, with passing/mismatching/unsupported/untested distinct | Actual per-model report keys, weights and statuses; exact named TRX selection; no empty or substituted reports | The ordinary gate validates required groups and counts. A fresh current-source full run is underway; its result is not yet qualified. | | Deterministic coverage in ordinary CI | Current `test-copper68k-synthetic.ps1` and CI invocation; all required reports and positive case counts | Implemented, including 4,096 EOR postincrement cases per profile. Final current-source execution linkage remains pending. | -| Explicit deep-audit command, deterministic seeds and strict missing/empty/mismatch rejection | Deep command/settings, recorded seed and selected inputs; rejected negative controls | Commands and earlier scoped proofs exist. Collect the final linkage to their pinned sources and distinguish historical executions from current ones. | +| Explicit deep-audit command, deterministic seeds and strict missing/empty/mismatch rejection | Deep command/settings, recorded seed and selected inputs; rejected negative controls | Fresh seed 68020 runs 10,000 samples per family/profile: 320,000 passing cases. Six actual invalid/missing/empty requests fail. Mismatch rejection has separate mutation evidence; retain final source/evidence linkage. | | Reuse SingleStepTests, Musashi and WinUAE with pinned identities and caveats | Adapter source, reference manifests, source/input hashes, actual execution and documented exclusions | Implemented. SingleStepTests supplies 000 semantic fixtures; Musashi runs independent self-checking programs, not its CPU as an oracle. WinUAE remains a software reference. | | Replacement detects absolute decoding, extension length, index sign, alias order, A7 stride and flags defects | Six distinct isolated mutation proofs with precise replacement IDs and current source linkage | The maintained mutation command defines all six. Historical proofs need final source/evidence linkage; a mutation definition is not execution evidence. | | Retire an old regression only after a mapped replacement detects its defect | Exact pinned old method/rows, before-removal proof, mutation witness and after-removal sibling verification | Recent NOT displacement and EOR postincrement retirements have exact proofs. EOR replaces one fact with 32,768 passing cases and retains four siblings. No blanket regression deletion is authorized. | @@ -86,3 +86,24 @@ local under `%TEMP%/copper68k-reference-restoration-20261006/audits/`. The run is still in progress; neither success nor final logical counts are claimed here. It does not include the separately failing broad Basic audit or private CPU imports. + +`CurrentDeepReferencesV1` has completed against those **same frozen assemblies**: +three named tests pass, with 320,000 seeded cases in 32 reports, 312,500 +SingleStepTests cases in 125 files and 536 passing Musashi programs across all +eight profiles. Musashi retains 88 explicit exclusions. Complete pinned input +sets (127 SingleStepTests files including exclusions and 78 Musashi binaries), +source and binary hashes, command/settings, loaded test paths, exact TRX roster, +report keys/weights/statuses and per-file/per-program outcomes were independently +verified. This does not qualify physical timing or every instruction combination. + +`CurrentDeepRejectionControlsV1` executes six actual failing adapter requests: +zero seed, zero samples, empty model selection, missing SingleStepTests corpus, +empty SingleStepTests filter and missing Musashi root. Each executes exactly one +named failing test with its expected diagnostic before emulated CPU execution; +original input and assembly hashes remain unchanged. These are adapter guards, +not a claim to have tested every CLI guard or every possible malformed input. + +| Current deep evidence | SHA-256 | +| --- | --- | +| Complete seeded/reference proof | `6bf1ae0d33a92445ea1387580c4d09bff3f944627e51569fd94f49a0cca55f6b` | +| Six actual rejection controls | `4e80db7425c2b65ca3dcb9dba14ce45786d1a1a30d3527f784c8c3ab245a0c8a` | diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 6dd13a11..f7ae59c3 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8,6 +8,14 @@ The [milestone 6 completion audit](COPPER68K_MILESTONE6_COMPLETION_AUDIT.md) maps the accepted requirements to current evidence and outstanding gates. The dated records below remain scoped historical evidence. +Current source checkpoint `c284b94` has fresh seeded and SingleStepTests/Musashi +evidence bound to one frozen assembly pair: 320,000 seeded cases, 312,500 +SingleStepTests cases and 536 Musashi programs pass, with 88 explicit Musashi +exclusions. Six actual invalid/missing/empty adapter requests reject. The full +CPU run with ten qualified WinUAE presets is still pending; the broad 010 RTE / +060 STOP audit remains failed. See the completion audit for exact identities +and scope. No current full-suite success or roadmap completion is claimed. + ## Sources and reproducibility The [Motorola programmer reference](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index f35e1f10..c0efa3a6 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -121,6 +121,14 @@ The [milestone 6 completion audit](COPPER68K_MILESTONE6_COMPLETION_AUDIT.md) maps the accepted requirements to evidence and remaining gates. Dated records below remain historical; focused proofs do not establish whole-roadmap completion. +Fresh frozen `c284b94` seeded/reference validation (2026-10-08) passes 320,000 +seeded cases, 312,500 SingleStepTests cases and 536 Musashi programs across the +selected profiles, with 88 explicit program exclusions. Six actual invalid/ +missing/empty adapter requests fail. Sources, input sets, loaded assemblies, +commands, exact roster and every report outcome are independently linked. +The current full CPU run is still active; its result is not yet claimed. +Milestone 6 remains in progress, `roadmapComplete=false`. + Record commands, logical case counts, failure discoveries, fixes, reference identities, replacement proofs and remaining required coverage here as work progresses. No milestone is complete merely because an inventory exists or a From 51444ea77c3a51c0c0c75766ee99f4d81aa96300 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 16:20:46 +0300 Subject: [PATCH 161/211] Reject duplicate synthetic audit model selections --- .../Synthetic/SyntheticDeepAuditTests.cs | 1 + docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 30 +++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 30 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 13 ++++++++ scripts/test-copper68k-synthetic.ps1 | 1 + 5 files changed, 75 insertions(+) diff --git a/Copper68k.Tests/Synthetic/SyntheticDeepAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticDeepAuditTests.cs index 50ffe90a..0136b428 100644 --- a/Copper68k.Tests/Synthetic/SyntheticDeepAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticDeepAuditTests.cs @@ -21,6 +21,7 @@ public void RecordedSeedMoveSamples() Assert.True(int.TryParse(Environment.GetEnvironmentVariable("COPPER68K_SYNTHETIC_SAMPLES"), out var samples) && samples > 0, "An audit must select positive samples"); var selected = (Environment.GetEnvironmentVariable("COPPER68K_SYNTHETIC_MODELS") ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries); Assert.NotEmpty(selected); + Assert.True(selected.Distinct(StringComparer.Ordinal).Count() == selected.Length, "Duplicate audit model IDs are not allowed"); Assert.All(selected, id => Assert.Contains(ModelSpec.All, m => m.Id == id)); var opcodes = MoveSpecification.Opcodes().ToArray(); foreach (var id in selected) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 799a8373..b1de8480 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -107,3 +107,33 @@ not a claim to have tested every CLI guard or every possible malformed input. | --- | --- | | Complete seeded/reference proof | `6bf1ae0d33a92445ea1387580c4d09bff3f944627e51569fd94f49a0cca55f6b` | | Six actual rejection controls | `4e80db7425c2b65ca3dcb9dba14ce45786d1a1a30d3527f784c8c3ab245a0c8a` | + +## Duplicate model-selection guards — 2026-10-08 + +The seeded MOVE adapter and command preflight now reject duplicate model IDs. +The frozen baseline request `68020,68020` incorrectly passed: it executed eight +groups but overwrote repeated report paths, leaving four reports. A request must +not silently substitute repeated execution for distinct profile coverage. + +The isolated guarded build passes **3,200 cases in 32 reports**, using all eight +profiles, seed 68020 and 100 samples per family/profile. Adjacent and separated +duplicates each fail exactly one named adapter test before CPU execution, with +no reports. Both `-Deep` and `-ValidateReportsOnly` reject duplicates before +creating output directories. Exact source, loaded assembly, command, TRX roster, +report keys/counts and rejection diagnostics are independently verified. Only +one test source and the command preflight change; all **37 production CPU inputs** +remain unchanged. No package or consumer rerun is needed for this test-only fix. + +The running full audit and the earlier 320,000-case deep/reference audit retain +their frozen `c284b94` fixture and gate. They are not relabeled as execution of +the guarded fixture. The focused guarded build supplies the separate evidence +for this sole test-source difference; the full audit remains pending. + +| Guard evidence | SHA-256 | +| --- | --- | +| Reproduced unguarded baseline | `d3d3ba54eee5ae08a1259bd4ae5fd41e48ed703335f8f801dffd91ca24dd9c66` | +| Guarded valid and rejection executions | `f01b927d108dc7191ef16212f3d6ad0f2e606a271e3be05b92ec72c92a573751` | +| Independent complete source/evidence linkage | `788a9af242c2c0c07233d752bdb4d001b769deef8ee9b59b325d281f66936eaa` | + +This closes the observed duplicate-selection gap, not the remaining milestone-6 +reference disagreements or broader qualification requirements. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index f7ae59c3..35f0a7f4 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -12611,3 +12611,33 @@ Local evidence under `%TEMP%/copper68k-reference-restoration-20261006/`: | pre-retirement preparation | `f5939ba511b6e18a1df987bc89bf38a785b8ec8ef9da7832b3664b222f39caad` | | complete consolidation | `460acbf420eb434df30ef49b5da0261df2cca64893dd1106c92a2990b85ba838` | | source/assembly/evidence linkage | `7e8728565e571f020529d5e63f29fa9eb48df45a00d4d702672de1aff43b8c5c` | + +## Duplicate model-selection guards — 2026-10-08 + +The seeded MOVE adapter and command preflight now reject duplicate model IDs. +The frozen baseline request `68020,68020` incorrectly passed: it executed eight +groups but overwrote repeated report paths, leaving four reports. A request must +not silently substitute repeated execution for distinct profile coverage. + +The isolated guarded build passes **3,200 cases in 32 reports**, using all eight +profiles, seed 68020 and 100 samples per family/profile. Adjacent and separated +duplicates each fail exactly one named adapter test before CPU execution, with +no reports. Both `-Deep` and `-ValidateReportsOnly` reject duplicates before +creating output directories. Exact source, loaded assembly, command, TRX roster, +report keys/counts and rejection diagnostics are independently verified. Only +one test source and the command preflight change; all **37 production CPU inputs** +remain unchanged. No package or consumer rerun is needed for this test-only fix. + +The running full audit and the earlier 320,000-case deep/reference audit retain +their frozen `c284b94` fixture and gate. They are not relabeled as execution of +the guarded fixture. The focused guarded build supplies the separate evidence +for this sole test-source difference; the full audit remains pending. + +| Guard evidence | SHA-256 | +| --- | --- | +| Reproduced unguarded baseline | `d3d3ba54eee5ae08a1259bd4ae5fd41e48ed703335f8f801dffd91ca24dd9c66` | +| Guarded valid and rejection executions | `f01b927d108dc7191ef16212f3d6ad0f2e606a271e3be05b92ec72c92a573751` | +| Independent complete source/evidence linkage | `788a9af242c2c0c07233d752bdb4d001b769deef8ee9b59b325d281f66936eaa` | + +This closes the observed duplicate-selection gap, not the remaining milestone-6 +reference disagreements or broader qualification requirements. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index c0efa3a6..7b492243 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3951,3 +3951,16 @@ assembly linkage pass. The ordinary gate requires 4,096 cases per profile. All 37 production CPU inputs remain unchanged; no private CPU import or release. See [the retirement mapping and evidence](COPPER68K_REFERENCE_QUALIFICATION.md#eor-byte-postincrement-consolidation--2026-10-08). Milestone 6 remains **in progress**, `roadmapComplete=false`. + + +### Milestone 6 checkpoint — duplicate model-selection rejection, 2026-10-08 + +The seeded adapter and command now reject duplicate profile IDs. The old +`68020,68020` request passed while overwriting half its report paths. The guarded +fixture passes 3,200 valid cases across all eight profiles; two actual adapter +requests and both command modes reject duplicates before emulated execution or +output creation. Independent linkage verifies all 228 source inputs and the +unchanged 37 production CPU inputs. Full/deep reference runs retain their +separately identified pre-guard snapshot; no current full-suite success is +claimed. See the completion audit for exact evidence. Milestone 6 remains +**in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 04196a59..94d8f842 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -17,6 +17,7 @@ $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $knownModels = @('68000', '68010', '68EC020', '68020', '68030', '68040', '68060', 'A1200') if ($Models.Count -eq 0 -or @($Models | Where-Object { $_ -cnotin $knownModels }).Count -gt 0) { throw 'Empty or unknown CPU model selection' } +if (@($Models | Select-Object -Unique).Count -ne $Models.Count) { throw 'Duplicate CPU model selection' } if ($Seed -eq 0) { throw 'Seed must be nonzero' } if ([bool]$WinUaePath -ne [bool]$WinUaeLibrary) { throw 'WinUAE audit requires both generated fixtures and native library' } if ($WinUaePath -and $WinUaeSourceCommit -notmatch '^[0-9a-fA-F]{40}$') { throw 'WinUAE audit requires the pinned generator source commit' } From 434ec10823911d799665f5f1a1cd30ee2372f8b3 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 16:34:26 +0300 Subject: [PATCH 162/211] Refresh six required MOVE mutation witnesses --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 34 ++++++++++++++++++- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 32 +++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 12 +++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index b1de8480..f6346ff0 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -21,7 +21,7 @@ it does not replace or discard their remaining gaps. | Deterministic coverage in ordinary CI | Current `test-copper68k-synthetic.ps1` and CI invocation; all required reports and positive case counts | Implemented, including 4,096 EOR postincrement cases per profile. Final current-source execution linkage remains pending. | | Explicit deep-audit command, deterministic seeds and strict missing/empty/mismatch rejection | Deep command/settings, recorded seed and selected inputs; rejected negative controls | Fresh seed 68020 runs 10,000 samples per family/profile: 320,000 passing cases. Six actual invalid/missing/empty requests fail. Mismatch rejection has separate mutation evidence; retain final source/evidence linkage. | | Reuse SingleStepTests, Musashi and WinUAE with pinned identities and caveats | Adapter source, reference manifests, source/input hashes, actual execution and documented exclusions | Implemented. SingleStepTests supplies 000 semantic fixtures; Musashi runs independent self-checking programs, not its CPU as an oracle. WinUAE remains a software reference. | -| Replacement detects absolute decoding, extension length, index sign, alias order, A7 stride and flags defects | Six distinct isolated mutation proofs with precise replacement IDs and current source linkage | The maintained mutation command defines all six. Historical proofs need final source/evidence linkage; a mutation definition is not execution evidence. | +| Replacement detects absolute decoding, extension length, index sign, alias order, A7 stride and flags defects | Six distinct isolated mutation proofs with precise replacement IDs and current source linkage | The maintained mutation command defines all six. Fresh isolated 51444ea executions detect all six against a 75,214-case clean baseline. Exact source/roster/report linkage and every mismatch ID are verified. | | Retire an old regression only after a mapped replacement detects its defect | Exact pinned old method/rows, before-removal proof, mutation witness and after-removal sibling verification | Recent NOT displacement and EOR postincrement retirements have exact proofs. EOR replaces one fact with 32,768 passing cases and retains four siblings. No blanket regression deletion is authorized. | | Retain cache, prefetch, bus ordering, fault sequencing, JIT and native regressions unless separately proven redundant | Exact test-source diff and full named roster, with every removal accounted for | Specialized tests remain. Refresh the complete current roster and compare it with the previous frozen full run. | | Production CPU fixes pass full CPU and affected consumers | Same-source full CPU proof; isolated NuGet package; CopperScreen production, host/disk/engine and applicable native results | MOVE16 recovery has full-suite and local .77 consumer evidence. Subsequent consolidation leaves all 37 CPU source inputs unchanged. No new consumer execution is claimed. | @@ -137,3 +137,35 @@ for this sole test-source difference; the full audit remains pending. This closes the observed duplicate-selection gap, not the remaining milestone-6 reference disagreements or broader qualification requirements. + +## Six current MOVE mutation witnesses — 2026-10-08 + +`CurrentMoveMutationsV1` refreshes the six required MOVE mutation checks using +isolated `51444ea` sources. Three clean 68020 batches pass **75,214 cases**. +Each deliberate defect changes exactly one CPU source in a separate copy; +the pushed production sources remain unchanged. Every intended named test runs +and fails with a concrete replacement witness. Independent verification checks +all 228 source inputs, exact mutation text, loaded assemblies, commands, rosters, +report statuses and complete combination weights. A separate literal-witness +check validates all six intended cases and maps **all 8,712 mismatch IDs** to +their report combinations, preserving the clean baseline's keys and weights. + +| Mutation | Executed cases | Mismatches | Diagnostic at the pinned witness | +| --- | ---: | ---: | --- | +| absolute-decode | 9,726 | 8 | `PC expected 00001004, actual 00001006` | +| extension-length | 9,726 | 160 | `PC expected 00001004, actual 00001006` | +| index-sign | 7,120 | 288 | `SR expected 2708, actual 2704, mask=FFFF` | +| alias-order | 9,726 | 24 | `Memory 00004001: expected EE, actual 5A` | +| a7-stride | 9,726 | 8 | `A7 expected 00004702, actual 00004701` | +| move-flags | 58,368 | 8,224 | `SR expected 2704, actual 2700, mask=FFFF` | + +| Evidence | SHA-256 | +| --- | --- | +| Complete source/execution/report linkage | `129af1d38941789f70c1cb845ea64d8e7a2de95364cf9dd5cc584400414a5995` | +| Six literal witnesses and complete diagnostic mapping | `6730dac567a7cae1cfb606337ccca0ec0805150416627af1c77a6a2665e1ebe9` | + +This refresh proves detection of the six named defects on 68020. It does not +claim a new all-model mutation audit or physical qualification. The full CPU +run remains active on its separate frozen `c284b94` fixture. No production CPU +source, consumer package or retired regression changes in this checkpoint; +milestone 6 stays **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 35f0a7f4..33099b6b 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -12641,3 +12641,35 @@ for this sole test-source difference; the full audit remains pending. This closes the observed duplicate-selection gap, not the remaining milestone-6 reference disagreements or broader qualification requirements. + +## Six current MOVE mutation witnesses — 2026-10-08 + +`CurrentMoveMutationsV1` refreshes the six required MOVE mutation checks using +isolated `51444ea` sources. Three clean 68020 batches pass **75,214 cases**. +Each deliberate defect changes exactly one CPU source in a separate copy; +the pushed production sources remain unchanged. Every intended named test runs +and fails with a concrete replacement witness. Independent verification checks +all 228 source inputs, exact mutation text, loaded assemblies, commands, rosters, +report statuses and complete combination weights. A separate literal-witness +check validates all six intended cases and maps **all 8,712 mismatch IDs** to +their report combinations, preserving the clean baseline's keys and weights. + +| Mutation | Executed cases | Mismatches | Diagnostic at the pinned witness | +| --- | ---: | ---: | --- | +| absolute-decode | 9,726 | 8 | `PC expected 00001004, actual 00001006` | +| extension-length | 9,726 | 160 | `PC expected 00001004, actual 00001006` | +| index-sign | 7,120 | 288 | `SR expected 2708, actual 2704, mask=FFFF` | +| alias-order | 9,726 | 24 | `Memory 00004001: expected EE, actual 5A` | +| a7-stride | 9,726 | 8 | `A7 expected 00004702, actual 00004701` | +| move-flags | 58,368 | 8,224 | `SR expected 2704, actual 2700, mask=FFFF` | + +| Evidence | SHA-256 | +| --- | --- | +| Complete source/execution/report linkage | `129af1d38941789f70c1cb845ea64d8e7a2de95364cf9dd5cc584400414a5995` | +| Six literal witnesses and complete diagnostic mapping | `6730dac567a7cae1cfb606337ccca0ec0805150416627af1c77a6a2665e1ebe9` | + +This refresh proves detection of the six named defects on 68020. It does not +claim a new all-model mutation audit or physical qualification. The full CPU +run remains active on its separate frozen `c284b94` fixture. No production CPU +source, consumer package or retired regression changes in this checkpoint; +milestone 6 stays **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 7b492243..8b2ede0d 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3964,3 +3964,15 @@ unchanged 37 production CPU inputs. Full/deep reference runs retain their separately identified pre-guard snapshot; no current full-suite success is claimed. See the completion audit for exact evidence. Milestone 6 remains **in progress**, `roadmapComplete=false`. + + +### Milestone 6 checkpoint — fresh six-defect MOVE proof, 2026-10-08 + +Fresh isolated 51444ea executions detect absolute decoding, extension length, +index sign, alias order, A7 stride and flags defects against a clean 75,214-case +68020 baseline. Each mutant changes one isolated CPU input; pushed CPU sources +stay unchanged. Independent source, loaded-assembly, exact-roster and report +verification is supplemented by six literal witnesses and all 8,712 diagnostic +ID-to-combination mappings. The current full run retains its pre-guard frozen +fixture and remains pending. See the completion audit for identities and scope; +milestone 6 stays **in progress**, `roadmapComplete=false`. From ea18c2c0d2d5e2db69421091b7fc83d7b1fc7596 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 16:58:47 +0300 Subject: [PATCH 163/211] Link full qualification and later failed-reference observations --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 121 ++++++++++++++++-- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 112 +++++++++++++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 30 +++++ 3 files changed, 249 insertions(+), 14 deletions(-) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index f6346ff0..a1343de1 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -17,13 +17,13 @@ it does not replace or discard their remaining gaps. | --- | --- | --- | | Independent audits across 000, 010, EC020, 020, 030, 040, 060 and A1200 | Pinned reference inputs and adapters; executed selections for every profile; explicit exclusions and comparator checks | Fresh SingleStepTests 000 and Musashi eight-profile audits pass on the frozen current assemblies. The broad WinUAE audit still has two unresolved mismatching directories. | | Complete integer-family inventory; later families cannot disappear | `IntegerInventory.cs`, generated model/family inventory and named execution reports | Inventory and reporting are implemented. Inventory membership alone does not prove execution or internal restoration coverage. | -| Coverage by architectural combination, with passing/mismatching/unsupported/untested distinct | Actual per-model report keys, weights and statuses; exact named TRX selection; no empty or substituted reports | The ordinary gate validates required groups and counts. A fresh current-source full run is underway; its result is not yet qualified. | -| Deterministic coverage in ordinary CI | Current `test-copper68k-synthetic.ps1` and CI invocation; all required reports and positive case counts | Implemented, including 4,096 EOR postincrement cases per profile. Final current-source execution linkage remains pending. | +| Coverage by architectural combination, with passing/mismatching/unsupported/untested distinct | Actual per-model report keys, weights and statuses; exact named TRX selection; no empty or substituted reports | The ordinary gate validates required groups and counts. The frozen c284b94 full run is qualified: 86,806,858 passing cases / 967 reports. The sole subsequent fixture guard has separate focused execution evidence. | +| Deterministic coverage in ordinary CI | Current `test-copper68k-synthetic.ps1` and CI invocation; all required reports and positive case counts | Implemented, including 4,096 EOR postincrement cases per profile. Frozen full execution and the separately qualified duplicate-selection guard are explicitly linked. | | Explicit deep-audit command, deterministic seeds and strict missing/empty/mismatch rejection | Deep command/settings, recorded seed and selected inputs; rejected negative controls | Fresh seed 68020 runs 10,000 samples per family/profile: 320,000 passing cases. Six actual invalid/missing/empty requests fail. Mismatch rejection has separate mutation evidence; retain final source/evidence linkage. | | Reuse SingleStepTests, Musashi and WinUAE with pinned identities and caveats | Adapter source, reference manifests, source/input hashes, actual execution and documented exclusions | Implemented. SingleStepTests supplies 000 semantic fixtures; Musashi runs independent self-checking programs, not its CPU as an oracle. WinUAE remains a software reference. | | Replacement detects absolute decoding, extension length, index sign, alias order, A7 stride and flags defects | Six distinct isolated mutation proofs with precise replacement IDs and current source linkage | The maintained mutation command defines all six. Fresh isolated 51444ea executions detect all six against a 75,214-case clean baseline. Exact source/roster/report linkage and every mismatch ID are verified. | | Retire an old regression only after a mapped replacement detects its defect | Exact pinned old method/rows, before-removal proof, mutation witness and after-removal sibling verification | Recent NOT displacement and EOR postincrement retirements have exact proofs. EOR replaces one fact with 32,768 passing cases and retains four siblings. No blanket regression deletion is authorized. | -| Retain cache, prefetch, bus ordering, fault sequencing, JIT and native regressions unless separately proven redundant | Exact test-source diff and full named roster, with every removal accounted for | Specialized tests remain. Refresh the complete current roster and compare it with the previous frozen full run. | +| Retain cache, prefetch, bus ordering, fault sequencing, JIT and native regressions unless separately proven redundant | Exact test-source diff and full named roster, with every removal accounted for | Specialized tests remain. The exact 5,400-test frozen roster is compared with its predecessor, including all retirements, replacements and unavailable additions. | | Production CPU fixes pass full CPU and affected consumers | Same-source full CPU proof; isolated NuGet package; CopperScreen production, host/disk/engine and applicable native results | MOVE16 recovery has full-suite and local .77 consumer evidence. Subsequent consolidation leaves all 37 CPU source inputs unchanged. No new consumer execution is claimed. | | Preserve successful ordering and timing policy; no automatic retry after partial effects | Relevant sequence/fault tests and change review, separate from timing qualification | Existing checks and scoped explicit-RTE continuation proofs remain. Private candidate results are not production behavior or physical timing qualification. | | Test-internal framework; public factory; NuGet consumer boundary; immutable published versions | Source/project/package diffs and consumer dependency inspection | Preserved. Private 39-input CPU candidates remain separate from the production 37-input CPU graph. No publication is authorized by this audit. | @@ -33,8 +33,10 @@ it does not replace or discard their remaining gaps. 1. **The broad software-reference gate is failed.** The retained `QualifiedBasicMove16RecoveryV1` audit reports 1,379 passing directories and two mismatching directories, with actual test exit 1. The first failures are - 010 RTE invalid-format CCR and 060 ordinary STOP with a new S-clear SR. Later - failures in those directories remain unobserved. Passing qualified presets + 010 RTE invalid-format CCR and 060 ordinary STOP with a new S-clear SR. The original replay stops at those first + failures. A separate isolated discovery now visits 6,248 RTE and 196,608 STOP + callbacks, retaining 2,233 / 65,536 mismatching cases; aggregate comparison + sites are observed, while individual architectural witnesses remain limited. Passing qualified presets do not turn this broad audit green. 2. **Private continuation work is not production qualification.** The 020/030 read/write recovery candidates and their literal private formats have scoped @@ -79,12 +81,12 @@ completion. ## Current validation work — 2026-10-08 Current source checkpoint: `c284b948263501183136656307cab2ecce6e0479`. -The fresh isolated `CurrentReferenceFullV1` run snapshots **228 test/CPU inputs**, +The completed isolated `CurrentReferenceFullV1` run snapshots **228 test/CPU inputs**, including **37 production CPU inputs** identical to the previous full-run CPU sources, and enables the same ten qualified WinUAE presets. Its outputs are local under `%TEMP%/copper68k-reference-restoration-20261006/audits/`. -The run is still in progress; neither success nor final logical counts are -claimed here. It does not include the separately failing broad Basic audit or +It finishes with 5,337 passing / 63 unavailable / zero failed tests and +86,806,858 passing logical cases; exact evidence is recorded below. It does not include the separately failing broad Basic audit or private CPU imports. `CurrentDeepReferencesV1` has completed against those **same frozen assemblies**: @@ -127,7 +129,8 @@ remain unchanged. No package or consumer rerun is needed for this test-only fix. The running full audit and the earlier 320,000-case deep/reference audit retain their frozen `c284b94` fixture and gate. They are not relabeled as execution of the guarded fixture. The focused guarded build supplies the separate evidence -for this sole test-source difference; the full audit remains pending. +for this sole test-source difference. At that guard checkpoint, the full audit +was pending; its completed result is recorded below. | Guard evidence | SHA-256 | | --- | --- | @@ -169,3 +172,103 @@ claim a new all-model mutation audit or physical qualification. The full CPU run remains active on its separate frozen `c284b94` fixture. No production CPU source, consumer package or retired regression changes in this checkpoint; milestone 6 stays **in progress**, `roadmapComplete=false`. + +## Frozen full-suite and refreshed MOVE mutation qualification — 2026-10-08 + +The complete `CurrentReferenceFullV1` run has finished: **5,337 passing tests, +63 unavailable tests, zero failures**, 5,400 total. Its 967 profile reports contain +**86,806,858 passing logical cases**, with zero mismatching, unsupported or +untested cases in those executed reports. All ten requested qualified WinUAE +presets pass. The exact roster accounts for the five retired NOT/EOR rows, +16 replacement batches and ten unavailable private-candidate tests added since +the preceding full snapshot. All 951 retained profile reports match that +snapshot; the 16 added reports match their focused consolidation evidence. + +Independent verification binds every frozen source, command/settings, loaded +assembly, TRX outcome, report key/weight/status, native input and result to this +execution. The frozen ordinary report gate also passes. **Unavailable tests do +not supply coverage**, and the separately failed broad 010 RTE / 060 STOP audit +is not part of this passing selection. + +The full and 320,000-case deep/reference runs use frozen `c284b94` sources. +The only subsequent test-source change is the duplicate-selection guard, +qualified by its separate 3,200-case build and four actual rejection requests. +The full audit's original gate is preserved and executed explicitly; it is not +relabeled as a run of the guarded fixture. All 37 production CPU source inputs +are identical across these snapshots and the retained local .77 consumer proof. + +`CurrentMoveMutationsV1` independently refreshes the six required MOVE witnesses +on isolated `51444ea` sources. Its three clean 68020 batches pass **75,214 cases**. +Each deliberately changed production source is confined to one isolated copy; +the exact intended test runs and detects that defect. Source hashes, mutation +text, loaded assemblies, commands, named rosters, complete report weights and +recorded diagnostic witnesses are verified. A separate literal-witness check +also binds every one of the 8,712 mismatch IDs to its report combination and +proves unchanged baseline combination keys and weights. This demonstrates causal detection +for the six defects, not hardware qualification or a new all-model mutation run. + +| Mutation | Logical cases | Mismatches | Replacement witness | +| --- | ---: | ---: | --- | +| absolute-decode | 9,726 | 8 | `68020/MOVE/1/(A0)->abs.w/canonical/op=11D0/v=89ABCDEE/ccr=00` | +| extension-length | 9,726 | 160 | `68020/MOVE/1/abs.w->(A0)/canonical/op=10B8/v=89ABCDEE/ccr=00` | +| index-sign | 7,120 | 288 | `68020/MOVE/1/index(A0)->D1/brief/D0/W/scale=1/d=-32/ignored-format=False/op=1230/v=89ABCDEE/ccr=00` | +| alias-order | 9,726 | 24 | `68020/MOVE/1/(A0)+->(A0)/canonical/op=1098/v=89ABCDEE/ccr=00` | +| a7-stride | 9,726 | 8 | `68020/MOVE/1/(A7)+->D0/canonical/op=101F/v=89ABCDEE/ccr=00` | +| move-flags | 58,368 | 8,224 | `68020/MOVE/1/D0->D1/boundary-ccr/op=1200/v=00000000/ccr=00` | + +| Complete evidence | SHA-256 | +| --- | --- | +| Frozen full-suite, references and ordinary gate | `03b60c8348a83e75f7581cf663a4c6629ec5687a02e15c746c134472960885a8` | +| Fresh six-mutation source/execution/witness linkage | `129af1d38941789f70c1cb845ea64d8e7a2de95364cf9dd5cc584400414a5995` | +| Six literal witnesses and all 8,712 diagnostic mappings | `6730dac567a7cae1cfb606337ccca0ec0805150416627af1c77a6a2665e1ebe9` | + +No CPU source changes, public package publication or additional consumer replays +are included. The unresolved broad-reference disagreements, private production +qualification and remaining consolidation requirements keep milestone 6 +**in progress**, `roadmapComplete=false`. + +## Later-case observation of the failed Basic groups — 2026-10-08 + +The original broad audit still fails at its first 010 RTE and 060 STOP +disagreements. Its native `continue_on_error` option only continues across +instruction directories; a separate compile-time switch stops each directory +at its first failure. Changing that option alone would not observe later cases. + +The isolated `BasicContinuationDiscoveryV3` bridge visits the remaining cases +without altering input fixtures, CPU execution or reference comparisons. It +removes the two early exits, preserves aggregate failure across input files, +counts comparison events at 21 existing error sites and resets only diagnostic +buffer bookkeeping before each comparison. Observer calls retain the original +error statement's scope. An independent reconstruction verifies the complete +native/header diff; all 3,546 pinned input files, original DLL and 37 production +CPU source inputs remain unchanged. Its test-only graph has 229 source inputs, +including one local discovery fixture that is not imported into production. + +| Profile / selection | Callbacks and validations | Failed cases | Comparison error events | +| --- | ---: | ---: | ---: | +| 010 RTE | 6,248 | 2,233 | 4,466 | +| 060 STOP | 196,608 | 65,536 | 131,072 | +| Each profile's clean NOP control | 2 | 0 | 0 | +| Each profile's corrupted NOP control | 2 | 2 | 2 | + +The RTE aggregate records 2,233 SR and 2,233 saved-frame byte comparisons. +The STOP aggregate records 65,536 exception-vector and 65,536 SR comparisons. No other comparator sites fire in these two selections, and +there is no emulator-level unsupported execution. These are aggregate site +distributions, not retained independent architectural expectations or a complete +per-case register/frame witness archive. The controls establish that clean +execution remains successful and deliberately wrong results remain failures. + +The actual discovery request exits 1 and its exact named xUnit test fails after +writing the report. It does not turn either disputed family or the original +broad gate green. The first compiler-environment failure and a subsequent native +crash with no usable result remain rejected attempts; only the repaired V3 +execution is qualified. The crash motivated resetting diagnostic output per +comparison, and observer statements were also made explicitly scoped. + +Complete source/native/input/assembly/TRX/report linkage is recorded in the +local `BasicContinuationDiscoveryV3/proof.json`, SHA-256 +`64b51aec11c3568ad0c941d50d667ec4ceb287813e640146f5abba1bd360acaa`. +Software-reference disagreement still does not settle the undocumented flags +or STOP behavior. No CPU fix, reference exclusion, production bridge change, +consumer replay or package publication is selected from these counts. +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 33099b6b..d5c72284 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -11,10 +11,11 @@ The dated records below remain scoped historical evidence. Current source checkpoint `c284b94` has fresh seeded and SingleStepTests/Musashi evidence bound to one frozen assembly pair: 320,000 seeded cases, 312,500 SingleStepTests cases and 536 Musashi programs pass, with 88 explicit Musashi -exclusions. Six actual invalid/missing/empty adapter requests reject. The full -CPU run with ten qualified WinUAE presets is still pending; the broad 010 RTE / -060 STOP audit remains failed. See the completion audit for exact identities -and scope. No current full-suite success or roadmap completion is claimed. +exclusions. Six actual invalid/missing/empty adapter requests reject. The frozen full +CPU run passes 5,337 tests and 86,806,858 logical cases, with 63 unavailable tests +and ten qualified WinUAE presets. The broad 010 RTE / 060 STOP audit remains failed. See the completion audit for exact identities +and scope. The subsequent duplicate-selection guard and six MOVE mutations have separate +current-source evidence; no roadmap completion is claimed. ## Sources and reproducibility @@ -12631,7 +12632,8 @@ remain unchanged. No package or consumer rerun is needed for this test-only fix. The running full audit and the earlier 320,000-case deep/reference audit retain their frozen `c284b94` fixture and gate. They are not relabeled as execution of the guarded fixture. The focused guarded build supplies the separate evidence -for this sole test-source difference; the full audit remains pending. +for this sole test-source difference. At that guard checkpoint, the full audit +was pending; its completed result is recorded below. | Guard evidence | SHA-256 | | --- | --- | @@ -12673,3 +12675,103 @@ claim a new all-model mutation audit or physical qualification. The full CPU run remains active on its separate frozen `c284b94` fixture. No production CPU source, consumer package or retired regression changes in this checkpoint; milestone 6 stays **in progress**, `roadmapComplete=false`. + +## Frozen full-suite and refreshed MOVE mutation qualification — 2026-10-08 + +The complete `CurrentReferenceFullV1` run has finished: **5,337 passing tests, +63 unavailable tests, zero failures**, 5,400 total. Its 967 profile reports contain +**86,806,858 passing logical cases**, with zero mismatching, unsupported or +untested cases in those executed reports. All ten requested qualified WinUAE +presets pass. The exact roster accounts for the five retired NOT/EOR rows, +16 replacement batches and ten unavailable private-candidate tests added since +the preceding full snapshot. All 951 retained profile reports match that +snapshot; the 16 added reports match their focused consolidation evidence. + +Independent verification binds every frozen source, command/settings, loaded +assembly, TRX outcome, report key/weight/status, native input and result to this +execution. The frozen ordinary report gate also passes. **Unavailable tests do +not supply coverage**, and the separately failed broad 010 RTE / 060 STOP audit +is not part of this passing selection. + +The full and 320,000-case deep/reference runs use frozen `c284b94` sources. +The only subsequent test-source change is the duplicate-selection guard, +qualified by its separate 3,200-case build and four actual rejection requests. +The full audit's original gate is preserved and executed explicitly; it is not +relabeled as a run of the guarded fixture. All 37 production CPU source inputs +are identical across these snapshots and the retained local .77 consumer proof. + +`CurrentMoveMutationsV1` independently refreshes the six required MOVE witnesses +on isolated `51444ea` sources. Its three clean 68020 batches pass **75,214 cases**. +Each deliberately changed production source is confined to one isolated copy; +the exact intended test runs and detects that defect. Source hashes, mutation +text, loaded assemblies, commands, named rosters, complete report weights and +recorded diagnostic witnesses are verified. A separate literal-witness check +also binds every one of the 8,712 mismatch IDs to its report combination and +proves unchanged baseline combination keys and weights. This demonstrates causal detection +for the six defects, not hardware qualification or a new all-model mutation run. + +| Mutation | Logical cases | Mismatches | Replacement witness | +| --- | ---: | ---: | --- | +| absolute-decode | 9,726 | 8 | `68020/MOVE/1/(A0)->abs.w/canonical/op=11D0/v=89ABCDEE/ccr=00` | +| extension-length | 9,726 | 160 | `68020/MOVE/1/abs.w->(A0)/canonical/op=10B8/v=89ABCDEE/ccr=00` | +| index-sign | 7,120 | 288 | `68020/MOVE/1/index(A0)->D1/brief/D0/W/scale=1/d=-32/ignored-format=False/op=1230/v=89ABCDEE/ccr=00` | +| alias-order | 9,726 | 24 | `68020/MOVE/1/(A0)+->(A0)/canonical/op=1098/v=89ABCDEE/ccr=00` | +| a7-stride | 9,726 | 8 | `68020/MOVE/1/(A7)+->D0/canonical/op=101F/v=89ABCDEE/ccr=00` | +| move-flags | 58,368 | 8,224 | `68020/MOVE/1/D0->D1/boundary-ccr/op=1200/v=00000000/ccr=00` | + +| Complete evidence | SHA-256 | +| --- | --- | +| Frozen full-suite, references and ordinary gate | `03b60c8348a83e75f7581cf663a4c6629ec5687a02e15c746c134472960885a8` | +| Fresh six-mutation source/execution/witness linkage | `129af1d38941789f70c1cb845ea64d8e7a2de95364cf9dd5cc584400414a5995` | +| Six literal witnesses and all 8,712 diagnostic mappings | `6730dac567a7cae1cfb606337ccca0ec0805150416627af1c77a6a2665e1ebe9` | + +No CPU source changes, public package publication or additional consumer replays +are included. The unresolved broad-reference disagreements, private production +qualification and remaining consolidation requirements keep milestone 6 +**in progress**, `roadmapComplete=false`. + +## Later-case observation of the failed Basic groups — 2026-10-08 + +The original broad audit still fails at its first 010 RTE and 060 STOP +disagreements. Its native `continue_on_error` option only continues across +instruction directories; a separate compile-time switch stops each directory +at its first failure. Changing that option alone would not observe later cases. + +The isolated `BasicContinuationDiscoveryV3` bridge visits the remaining cases +without altering input fixtures, CPU execution or reference comparisons. It +removes the two early exits, preserves aggregate failure across input files, +counts comparison events at 21 existing error sites and resets only diagnostic +buffer bookkeeping before each comparison. Observer calls retain the original +error statement's scope. An independent reconstruction verifies the complete +native/header diff; all 3,546 pinned input files, original DLL and 37 production +CPU source inputs remain unchanged. Its test-only graph has 229 source inputs, +including one local discovery fixture that is not imported into production. + +| Profile / selection | Callbacks and validations | Failed cases | Comparison error events | +| --- | ---: | ---: | ---: | +| 010 RTE | 6,248 | 2,233 | 4,466 | +| 060 STOP | 196,608 | 65,536 | 131,072 | +| Each profile's clean NOP control | 2 | 0 | 0 | +| Each profile's corrupted NOP control | 2 | 2 | 2 | + +The RTE aggregate records 2,233 SR and 2,233 saved-frame byte comparisons. +The STOP aggregate records 65,536 exception-vector and 65,536 SR comparisons. No other comparator sites fire in these two selections, and +there is no emulator-level unsupported execution. These are aggregate site +distributions, not retained independent architectural expectations or a complete +per-case register/frame witness archive. The controls establish that clean +execution remains successful and deliberately wrong results remain failures. + +The actual discovery request exits 1 and its exact named xUnit test fails after +writing the report. It does not turn either disputed family or the original +broad gate green. The first compiler-environment failure and a subsequent native +crash with no usable result remain rejected attempts; only the repaired V3 +execution is qualified. The crash motivated resetting diagnostic output per +comparison, and observer statements were also made explicitly scoped. + +Complete source/native/input/assembly/TRX/report linkage is recorded in the +local `BasicContinuationDiscoveryV3/proof.json`, SHA-256 +`64b51aec11c3568ad0c941d50d667ec4ceb287813e640146f5abba1bd360acaa`. +Software-reference disagreement still does not settle the undocumented flags +or STOP behavior. No CPU fix, reference exclusion, production bridge change, +consumer replay or package publication is selected from these counts. +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 8b2ede0d..e2ea69bd 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -3976,3 +3976,33 @@ verification is supplemented by six literal witnesses and all 8,712 diagnostic ID-to-combination mappings. The current full run retains its pre-guard frozen fixture and remains pending. See the completion audit for identities and scope; milestone 6 stays **in progress**, `roadmapComplete=false`. + + +### Milestone 6 checkpoint — full run and six current MOVE mutants, 2026-10-08 + +The frozen c284b94 full run passes 5,337 tests, with 63 unavailable and zero +failures; 967 profile reports contain 86,806,858 passing logical cases. All ten +requested qualified WinUAE presets and the frozen ordinary report gate pass. +The exact roster and retained/new reports are independently linked. The newer +duplicate-selection guard retains separate focused evidence; no snapshot is +relabeled. Fresh isolated 51444ea mutation runs detect all six required MOVE +defects against a clean 75,214-case baseline, with exact source, assembly, +roster and report/witness linkage. All 37 CPU inputs remain unchanged; no new +consumer execution or package publication is claimed. See the completion audit +for identities and remaining gates. Milestone 6 stays **in progress**, +`roadmapComplete=false`. + + +### Milestone 6 checkpoint — later failed-reference cases, 2026-10-08 + +An isolated continuation bridge visits 6,248 010 RTE and 196,608 060 STOP cases +without changing the original fixture inputs or 37 CPU inputs. It retains +2,233 / 65,536 failing cases, with saved-frame/SR and exception/SR comparison +events respectively. Four clean/corrupted NOP controls validate the observer; +the discovery request still exits 1 and the original broad gate remains failed. +The complete native diff, pinned inputs, source graph, loaded assemblies, +named test and aggregate report are independently verified. V1/V2 failures are +rejected evidence; only V3 is qualified. Counts do not settle undocumented +hardware behavior, and no CPU change or reference exclusion follows. See the +completion audit for exact scope; milestone 6 stays **in progress**, +`roadmapComplete=false`. From 3a5314fda44b2e5fb1bfb4df3aa198681daecc97 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 17:22:36 +0300 Subject: [PATCH 164/211] Consolidate captured ANDI displacement regression --- Copper68k.Tests/M68020HdfBootTests.cs | 30 -------- Copper68k.Tests/Synthetic/README.md | 21 +++++ .../SyntheticAndiDisplacementTests.cs | 42 ++++++++++ docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 66 +++++++++++++++- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 60 ++++++++++++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 ++++ ...-copper68k-andi-displacement-retirement.py | 76 +++++++++++++++++++ scripts/test-copper68k-synthetic.ps1 | 1 + 8 files changed, 276 insertions(+), 35 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticAndiDisplacementTests.cs create mode 100644 scripts/test-copper68k-andi-displacement-retirement.py diff --git a/Copper68k.Tests/M68020HdfBootTests.cs b/Copper68k.Tests/M68020HdfBootTests.cs index b3054d20..effa5d34 100644 --- a/Copper68k.Tests/M68020HdfBootTests.cs +++ b/Copper68k.Tests/M68020HdfBootTests.cs @@ -196,36 +196,6 @@ public void MoveaWordDisplacementSignExtendsBeforeOverwritingAliasedBase(M68kCpu } } - [Theory] - [InlineData(M68kCpuModel.M68020)] - [InlineData(M68kCpuModel.M68EC020)] - [InlineData(M68kCpuModel.M68030)] - [InlineData(M68kCpuModel.M68040)] - public void AndiLongDisplacementConsumesLongImmediateAndPreservesExtend(M68kCpuModel model) - { - foreach (var mask in new uint[] { 0, 0x80123456, 0x7FFFFFFF }) - foreach (var displacement in new[] { -8, 8 }) - { - var bus = new Copper68kTestBus(0x10000); - bus.WriteWords(0x1000, 0x02A9, (ushort)(mask >> 16), (ushort)mask, (ushort)displacement); - var address = (uint)(0x3000 + displacement); - bus.WriteWord(address - 2, 0xAAAA); - bus.WriteLong(address, 0xFFFFFFFF); - bus.WriteWord(address + 4, 0xBBBB); - using var cpu = M68kCoreFactory.Default.Create(model, bus); - cpu.Reset(0x1000, 0x7000); - cpu.State.A[1] = 0x3000; - cpu.State.StatusRegister = 0x271F; - cpu.ExecuteInstruction(); - Assert.Equal(mask, bus.ReadLong(address)); - Assert.Equal(0xAAAA, bus.ReadWord(address - 2)); - Assert.Equal(0xBBBB, bus.ReadWord(address + 4)); - Assert.Equal(mask == 0 ? 0x2714 : mask == 0x80123456 ? 0x2718 : 0x2710, cpu.State.StatusRegister); - Assert.Equal(0x1008u, cpu.State.ProgramCounter); - Assert.Equal(0x3000u, cpu.State.A[1]); - } - } - [Theory] [InlineData(M68kCpuModel.M68020)] [InlineData(M68kCpuModel.M68EC020)] diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index d925cd93..de3a5aef 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1619,3 +1619,24 @@ python scripts/test-copper68k-eor-postincrement-retirement.py ` checks the exact reduced source; `--validate-only` requires unchanged complete evidence. Both execution paths are mutated only in isolated builds. The address-error/trace siblings remain. See [the retirement record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#eor-byte-postincrement-consolidation--2026-10-08). + + +## Captured ANDI displacement retirement + +`SyntheticAndiDisplacementTests` reuses operand setup and architectural/memory +verification for 24,576 cases across all eight profiles. It includes the original +three masks and signed displacements, every address register, both privilege +states and all CCRs, with literal encoding and exact extension consumption. +The maintained helper proves a lost-X defect fails both the four pinned old +rows and 12,288 replacement cases before removing only those rows: + +```powershell +python scripts/test-copper68k-andi-displacement-retirement.py --output artifacts/andi-displacement-retirement +python scripts/test-copper68k-andi-displacement-retirement.py --validate-only --output artifacts/andi-displacement-retirement +``` + +The ordinary gate requires 3,072 cases per profile. The helper pins the witness +and class inventory; reproduce against this checkpoint if later retirements +change that class. Exact identities, retained sibling verification, negative +controls and the separate full/report-only scopes are in the +[qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#andi-long-displacement-consolidation--2026-10-08). diff --git a/Copper68k.Tests/Synthetic/SyntheticAndiDisplacementTests.cs b/Copper68k.Tests/Synthetic/SyntheticAndiDisplacementTests.cs new file mode 100644 index 00000000..1985464e --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticAndiDisplacementTests.cs @@ -0,0 +1,42 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticAndiDisplacementTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void CapturedLongAndiDisplacementConsumesImmediateAndPreservesExtend(string modelId) + { + // M68000PM ANDI: preserve X, set N/Z from the selected-width result, + // and clear V/C. Literal captured masks and displacements are extended + // across every address register, both stack banks and all CCR images. + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "logical-andi-displacement-captured"); + foreach (var mask in new uint[] { 0, 0x80123456, 0x7fffffff }) + foreach (short displacement in new short[] { -8, 8 }) + for (var register = 0; register < 8; register++) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + machine.Reset(ccr, supervisor); + if (register == 7) machine.Core.State.SetActiveStackPointer(0x3000); + else machine.Core.State.A[register] = 0x3000; + var opcode = (ushort)(0x02a8 | register); + if (register == 1) Assert.Equal(0x02a9, opcode); + var operand = new OperandFixture(machine, [opcode, (ushort)(mask >> 16), (ushort)mask], + new(5, register), 4, uint.MaxValue, source: false, + options: new(DestinationDisplacement: displacement)); + Assert.Equal(unchecked((uint)(0x3000 + displacement)), operand.Address); + Assert.Equal(0x1008u, operand.NextPc); + var expected = operand.Expected; + operand.Write(mask); // FFFFFFFF AND the captured mask. + expected.Sr = (ushort)((expected.Sr & 0xfff0) | + (mask == 0 ? 4 : 0) | ((mask & 0x80000000) != 0 ? 8 : 0)); + SyntheticExecution.Run(machine, expected, report, + $"{modelId}/ANDI/4/d16(A{register})/captured/super={supervisor}/op={opcode:X4}/mask={mask:X8}/disp={displacement}/ccr={ccr:X2}"); + } + report.Complete(output); + } +} diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index a1343de1..e1567fa9 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -17,12 +17,12 @@ it does not replace or discard their remaining gaps. | --- | --- | --- | | Independent audits across 000, 010, EC020, 020, 030, 040, 060 and A1200 | Pinned reference inputs and adapters; executed selections for every profile; explicit exclusions and comparator checks | Fresh SingleStepTests 000 and Musashi eight-profile audits pass on the frozen current assemblies. The broad WinUAE audit still has two unresolved mismatching directories. | | Complete integer-family inventory; later families cannot disappear | `IntegerInventory.cs`, generated model/family inventory and named execution reports | Inventory and reporting are implemented. Inventory membership alone does not prove execution or internal restoration coverage. | -| Coverage by architectural combination, with passing/mismatching/unsupported/untested distinct | Actual per-model report keys, weights and statuses; exact named TRX selection; no empty or substituted reports | The ordinary gate validates required groups and counts. The frozen c284b94 full run is qualified: 86,806,858 passing cases / 967 reports. The sole subsequent fixture guard has separate focused execution evidence. | -| Deterministic coverage in ordinary CI | Current `test-copper68k-synthetic.ps1` and CI invocation; all required reports and positive case counts | Implemented, including 4,096 EOR postincrement cases per profile. Frozen full execution and the separately qualified duplicate-selection guard are explicitly linked. | +| Coverage by architectural combination, with passing/mismatching/unsupported/untested distinct | Actual per-model report keys, weights and statuses; exact named TRX selection; no empty or substituted reports | The ordinary gate validates required groups and counts. The frozen c284b94 full run is qualified: 86,806,858 passing cases / 967 reports. The guard and later ANDI consolidation retain separate focused execution evidence. | +| Deterministic coverage in ordinary CI | Current `test-copper68k-synthetic.ps1` and CI invocation; all required reports and positive case counts | Implemented, including 4,096 EOR postincrement and 3,072 ANDI displacement cases per profile. Frozen full execution and the separately qualified duplicate-selection guard are explicitly linked. | | Explicit deep-audit command, deterministic seeds and strict missing/empty/mismatch rejection | Deep command/settings, recorded seed and selected inputs; rejected negative controls | Fresh seed 68020 runs 10,000 samples per family/profile: 320,000 passing cases. Six actual invalid/missing/empty requests fail. Mismatch rejection has separate mutation evidence; retain final source/evidence linkage. | | Reuse SingleStepTests, Musashi and WinUAE with pinned identities and caveats | Adapter source, reference manifests, source/input hashes, actual execution and documented exclusions | Implemented. SingleStepTests supplies 000 semantic fixtures; Musashi runs independent self-checking programs, not its CPU as an oracle. WinUAE remains a software reference. | | Replacement detects absolute decoding, extension length, index sign, alias order, A7 stride and flags defects | Six distinct isolated mutation proofs with precise replacement IDs and current source linkage | The maintained mutation command defines all six. Fresh isolated 51444ea executions detect all six against a 75,214-case clean baseline. Exact source/roster/report linkage and every mismatch ID are verified. | -| Retire an old regression only after a mapped replacement detects its defect | Exact pinned old method/rows, before-removal proof, mutation witness and after-removal sibling verification | Recent NOT displacement and EOR postincrement retirements have exact proofs. EOR replaces one fact with 32,768 passing cases and retains four siblings. No blanket regression deletion is authorized. | +| Retire an old regression only after a mapped replacement detects its defect | Exact pinned old method/rows, before-removal proof, mutation witness and after-removal sibling verification | NOT displacement, EOR postincrement and ANDI displacement retirements have exact proofs. EOR replaces one fact with 32,768 passing cases and retains four siblings. No blanket regression deletion is authorized. | | Retain cache, prefetch, bus ordering, fault sequencing, JIT and native regressions unless separately proven redundant | Exact test-source diff and full named roster, with every removal accounted for | Specialized tests remain. The exact 5,400-test frozen roster is compared with its predecessor, including all retirements, replacements and unavailable additions. | | Production CPU fixes pass full CPU and affected consumers | Same-source full CPU proof; isolated NuGet package; CopperScreen production, host/disk/engine and applicable native results | MOVE16 recovery has full-suite and local .77 consumer evidence. Subsequent consolidation leaves all 37 CPU source inputs unchanged. No new consumer execution is claimed. | | Preserve successful ordering and timing policy; no automatic retry after partial effects | Relevant sequence/fault tests and change review, separate from timing qualification | Existing checks and scoped explicit-RTE continuation proofs remain. Private candidate results are not production behavior or physical timing qualification. | @@ -191,7 +191,7 @@ not supply coverage**, and the separately failed broad 010 RTE / 060 STOP audit is not part of this passing selection. The full and 320,000-case deep/reference runs use frozen `c284b94` sources. -The only subsequent test-source change is the duplicate-selection guard, +At that full-run checkpoint, the sole subsequent test-source change was the duplicate-selection guard, qualified by its separate 3,200-case build and four actual rejection requests. The full audit's original gate is preserved and executed explicitly; it is not relabeled as a run of the guarded fixture. All 37 production CPU source inputs @@ -272,3 +272,61 @@ Software-reference disagreement still does not settle the undocumented flags or STOP behavior. No CPU fix, reference exclusion, production bridge change, consumer replay or package publication is selected from these counts. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## ANDI long-displacement consolidation — 2026-10-08 + +The four rows of `M68020HdfBootTests.AndiLongDisplacementConsumesLongImmediateAndPreservesExtend` +at source pin `ea18c2c0d2d5e2db69421091b7fc83d7b1fc7596` are replaced by +`SyntheticAndiDisplacementTests.CapturedLongAndiDisplacementConsumesImmediateAndPreservesExtend`. +The replacement executes **24,576 cases / eight profiles**, 3,072 per profile: +three original masks, both signed displacements, every address register, +user/supervisor stacks and all 32 initial CCR images. It uses the shared operand +fixture, public CPU factory and full architectural/memory verification. Literal +`02A9` encoding, independent operand address and exact next PC `1008` check the +long immediate followed by displacement. Expectations preserve X, derive N/Z +from the result and clear V/C without production arithmetic or decoding helpers. +The original A1 / supervisor / CCR-31 operand combinations are included; +surrounding-memory canaries use the shared fixture rather than the old AAAA/BBBB +literal bytes. This retires semantic rows, not native HDF boot coverage. + +Before removal, all **71 original HDF rows plus eight new batches** pass. +A deliberate lost-X defect in the base and advanced ANDI execution paths fails +all four original rows and exactly **12,288 replacement cases**. Every mismatch +ID and diagnostic, combination key/weight and full named TRX selection is +checked against independently enumerated expectations. After exact removal, +all **67 retained HDF rows plus eight replacement batches** pass. Nine copied +evidence corruptions reject, and strict replay reproduces the complete proof. + +The ordinary command requires all eight new reports, each with 3,072 passing +cases. Its composed **report-only** gate passes **86,638,954 semantic cases / +775 batches / 975 profile reports**, using the previously qualified full run +plus the focused new reports and unchanged integer inventory. Two actual +requests with a missing 000 report or wrong 000 case count reject. This is not +a newly executed whole CPU suite. All **37 production CPU inputs** remain +unchanged; the current test/CPU graph has **229 inputs**. Earlier full-suite, +deep/reference and local .77 consumer results retain their original identities. +No package or new consumer execution is claimed. + +The first mutation attempt rejected CRLF-sensitive anchors before mutant +execution. A first composed gate also rejected an omitted inventory. These +attempts remain separate; the accepted V2 audit and complete V2 gate correct +the tooling inputs without changing expectations or weakening acceptance. + +| Evidence | SHA-256 | +| --- | --- | +| Before-removal preparation | `86aafb2afc40d73d75b9fe95925cd15ca2d94885fb3ec04aeaee5df14aa0b4ec` | +| Complete Clean / Mutation / Current proof | `f16ce319c088dd400da0bbebbc3a986a28994857544bd2382702e7da818f122a` | +| Complete report-only ordinary gate | `a75ad1fec362627f69bf0040681f151b5e8163a3766392071707c0e970a29cb5` | +| Two actual required-report controls | `1c7b586342ecbed43f302ce4e3cd782920bb43ac1efb3307ea2c5e558e7fb9f3` | +| Independent source / retirement / binary / report linkage | `a412f3851eb369d0738b5f1e6fd191f0b19837984e21d30d6f60a4cdb91c1a08` | + +```powershell +python scripts/test-copper68k-andi-displacement-retirement.py --output artifacts/andi-displacement-retirement +python scripts/test-copper68k-andi-displacement-retirement.py --validate-only --output artifacts/andi-displacement-retirement +``` + +The helper pins the original witness/class inventory and checks exact source +scope; use this checkpoint for reproduction if later retirements change that +class. The 010 RTE / 060 STOP broad gate and private-production restoration +requirements remain open. Milestone 6 stays **in progress**, +`roadmapComplete=false`. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index d5c72284..fb4dabdf 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -12694,7 +12694,7 @@ not supply coverage**, and the separately failed broad 010 RTE / 060 STOP audit is not part of this passing selection. The full and 320,000-case deep/reference runs use frozen `c284b94` sources. -The only subsequent test-source change is the duplicate-selection guard, +At that full-run checkpoint, the sole subsequent test-source change was the duplicate-selection guard, qualified by its separate 3,200-case build and four actual rejection requests. The full audit's original gate is preserved and executed explicitly; it is not relabeled as a run of the guarded fixture. All 37 production CPU source inputs @@ -12775,3 +12775,61 @@ Software-reference disagreement still does not settle the undocumented flags or STOP behavior. No CPU fix, reference exclusion, production bridge change, consumer replay or package publication is selected from these counts. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +## ANDI long-displacement consolidation — 2026-10-08 + +The four rows of `M68020HdfBootTests.AndiLongDisplacementConsumesLongImmediateAndPreservesExtend` +at source pin `ea18c2c0d2d5e2db69421091b7fc83d7b1fc7596` are replaced by +`SyntheticAndiDisplacementTests.CapturedLongAndiDisplacementConsumesImmediateAndPreservesExtend`. +The replacement executes **24,576 cases / eight profiles**, 3,072 per profile: +three original masks, both signed displacements, every address register, +user/supervisor stacks and all 32 initial CCR images. It uses the shared operand +fixture, public CPU factory and full architectural/memory verification. Literal +`02A9` encoding, independent operand address and exact next PC `1008` check the +long immediate followed by displacement. Expectations preserve X, derive N/Z +from the result and clear V/C without production arithmetic or decoding helpers. +The original A1 / supervisor / CCR-31 operand combinations are included; +surrounding-memory canaries use the shared fixture rather than the old AAAA/BBBB +literal bytes. This retires semantic rows, not native HDF boot coverage. + +Before removal, all **71 original HDF rows plus eight new batches** pass. +A deliberate lost-X defect in the base and advanced ANDI execution paths fails +all four original rows and exactly **12,288 replacement cases**. Every mismatch +ID and diagnostic, combination key/weight and full named TRX selection is +checked against independently enumerated expectations. After exact removal, +all **67 retained HDF rows plus eight replacement batches** pass. Nine copied +evidence corruptions reject, and strict replay reproduces the complete proof. + +The ordinary command requires all eight new reports, each with 3,072 passing +cases. Its composed **report-only** gate passes **86,638,954 semantic cases / +775 batches / 975 profile reports**, using the previously qualified full run +plus the focused new reports and unchanged integer inventory. Two actual +requests with a missing 000 report or wrong 000 case count reject. This is not +a newly executed whole CPU suite. All **37 production CPU inputs** remain +unchanged; the current test/CPU graph has **229 inputs**. Earlier full-suite, +deep/reference and local .77 consumer results retain their original identities. +No package or new consumer execution is claimed. + +The first mutation attempt rejected CRLF-sensitive anchors before mutant +execution. A first composed gate also rejected an omitted inventory. These +attempts remain separate; the accepted V2 audit and complete V2 gate correct +the tooling inputs without changing expectations or weakening acceptance. + +| Evidence | SHA-256 | +| --- | --- | +| Before-removal preparation | `86aafb2afc40d73d75b9fe95925cd15ca2d94885fb3ec04aeaee5df14aa0b4ec` | +| Complete Clean / Mutation / Current proof | `f16ce319c088dd400da0bbebbc3a986a28994857544bd2382702e7da818f122a` | +| Complete report-only ordinary gate | `a75ad1fec362627f69bf0040681f151b5e8163a3766392071707c0e970a29cb5` | +| Two actual required-report controls | `1c7b586342ecbed43f302ce4e3cd782920bb43ac1efb3307ea2c5e558e7fb9f3` | +| Independent source / retirement / binary / report linkage | `a412f3851eb369d0738b5f1e6fd191f0b19837984e21d30d6f60a4cdb91c1a08` | + +```powershell +python scripts/test-copper68k-andi-displacement-retirement.py --output artifacts/andi-displacement-retirement +python scripts/test-copper68k-andi-displacement-retirement.py --validate-only --output artifacts/andi-displacement-retirement +``` + +The helper pins the original witness/class inventory and checks exact source +scope; use this checkpoint for reproduction if later retirements change that +class. The 010 RTE / 060 STOP broad gate and private-production restoration +requirements remain open. Milestone 6 stays **in progress**, +`roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index e2ea69bd..64f0da6a 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4006,3 +4006,18 @@ rejected evidence; only V3 is qualified. Counts do not settle undocumented hardware behavior, and no CPU change or reference exclusion follows. See the completion audit for exact scope; milestone 6 stays **in progress**, `roadmapComplete=false`. + + +### Milestone 6 checkpoint — ANDI displacement retirement, 2026-10-08 + +The captured long-immediate/displacement ANDI witness is replaced by 24,576 +shared-fixture cases across eight profiles. A lost-X mutation fails all four +pinned original rows and exactly 12,288 replacement cases; every diagnostic, +key/weight and named selection is verified. Exact removal leaves all 67 HDF +siblings and eight replacement batches passing. Nine evidence controls and +strict replay pass. Ordinary CI requires 3,072 cases per profile, with actual +missing/wrong-weight controls rejecting. Its composed report-only gate passes +86,638,954 cases / 775 batches / 975 reports; this is not a new full-suite run. +The current graph has 229 source inputs and unchanged 37 CPU inputs. Original +full/deep/consumer identities and broader failed/private gates remain retained; +no publication follows. Milestone 6 stays **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-andi-displacement-retirement.py b/scripts/test-copper68k-andi-displacement-retirement.py new file mode 100644 index 00000000..25c112a2 --- /dev/null +++ b/scripts/test-copper68k-andi-displacement-retirement.py @@ -0,0 +1,76 @@ +"""Pinned ANDI displacement replacement: fresh isolated execution or strict replay.""" +from pathlib import Path +import itertools +import re +from copper68k_consolidation import Audit, check + +REPO = Path(__file__).resolve().parent.parent +GROUP = "logical-andi-displacement-captured" + +def mutate(path, text): + if path.endswith("M68kAdvancedTimingInterpreter.cs"): + start = text.index(" private void ExecuteAndiLongImmediateToAddressDisplacement(") + end = text.index("\n private ", start + 20) + block = text[start:end] + old = "SetMoveFlags(result, M68kOperandSize.Long);" + check(block.count(old) == 1, "Missing advanced ANDI target") + return text[:start] + block.replace(old, old + "\n State.StatusRegister &= 0xffef; // intentional lost X") + text[end:] + pattern = r"(case 0x0200:\r?\n\s+destination &= immediate;\r?\n\s+[^\r\n]+\r?\n\s+SetLogicFlags\(destination, size\);)" + check(len(re.findall(pattern, text)) == 2, "Missing base ANDI targets") + return re.sub(pattern, lambda m: m[0] + "\n State.StatusRegister &= 0xffef; // intentional lost X", text) + +def report(model, mode): + combinations, failures = {}, [] + for mask, displacement, register, supervisor, ccr in itertools.product( + [0, 0x80123456, 0x7fffffff], [-8, 8], range(8), [False, True], range(32)): + key = f"{model}/ANDI/4/d16(A{register})/captured/super={supervisor}" + failed = mode == "Mutation" and bool(ccr & 16) + status = "mismatching" if failed else "passing" + counts = combinations.setdefault(key, {}) + counts[status] = counts.get(status, 0) + 1 + if failed: + expected = (0x2700 if supervisor else 0x700) | (ccr & 16) | (4 if mask == 0 else 0) | (8 if mask & 0x80000000 else 0) + failures.append(dict(id=f"{key}/op={0x02a8 | register:04X}/mask={mask:08X}/disp={displacement}/ccr={ccr:02X}", status=status, + reason=f"SR expected {expected:04X}, actual {expected & ~16:04X}, mask=FFFF")) + return dict(schema=1, model=model, group=GROUP, + counts=dict(passing=3072-len(failures), mismatching=len(failures), unsupported=0, untested=0), + logicalCases=3072, xunitBatches=1, combinations=combinations, failures=failures) + +class AndiAudit(Audit): + def legacy_names(self): + names = {} + for match in re.finditer(r"public void (\w+)\(\s*([^)]*)\)", self.original): + method, signature = match.groups() + start = self.original.rfind("[Theory]", 0, match.start()) + parameters = [v.strip().split()[-1] for v in signature.split(",")] + if method == "AddLongPcDisplacementReadsMemoryFromExtensionPcAndSetsArithmeticFlags": + rows = itertools.product(["M68020", "M68EC020", "M68030", "M68040"], [-128, 128], + [(0, 1, 1, 0), (0xffffffff, 1, 0, 21), (0x7fffffff, 1, 0x80000000, 10), (0x80000000, 0x80000000, 0, 23)]) + values = [[m, str(d), *map(str, arithmetic)] for m, d, arithmetic in rows] + else: + rows = re.findall(r"\[InlineData\(([^)]*)\)\]", self.original[start:match.start()]) + values = [[v.strip().split('.')[-1] if v.strip().startswith('M68kCpuModel.') else str(int(v.strip(), 0)) for v in row.split(',')] for row in rows] + check(bool(values), "Missing pinned legacy rows") + for value in values: + check(len(value) == len(parameters), "Wrong pinned parameter inventory") + name = self.s['legacy_class'] + '.' + method + '(' + ', '.join(f'{k}: {v}' for k, v in zip(parameters, value)) + ')' + check(name not in names, "Duplicate pinned row") + names[name] = 'Passed' + check(len(names) == 71, "Incomplete pinned HDF class") + return names + + +def witness_error(name, message): + check(re.search(r"Expected:\s*10004\b", message) and re.search(r"Actual:\s*9988\b", message), "Wrong historical ANDI lost X") + +def make_audit(): + return AndiAudit(dict(repo=REPO, producer=Path(__file__).resolve(), pin="ea18c2c0d2d5e2db69421091b7fc83d7b1fc7596", + legacy="Copper68k.Tests/M68020HdfBootTests.cs", legacy_class="Copper68k.Tests.M68020HdfBootTests", legacy_cases=71, + witness="AndiLongDisplacementConsumesLongImmediateAndPreservesExtend", witness_cases=4, + fixture="Copper68k.Tests/Synthetic/SyntheticAndiDisplacementTests.cs", shared_class="Copper68k.Tests.Synthetic.SyntheticAndiDisplacementTests", + method="CapturedLongAndiDisplacementConsumesImmediateAndPreservesExtend", group=GROUP, + mutation_files=["Copper68k/M68kCore.cs", "Copper68k/M68kAdvancedTimingInterpreter.cs"], mutate=mutate, report=report, + witness_outcome=lambda name: "Failed", witness_error=witness_error, + description="Prove ANDI displacement shared coverage before exact four-row retirement.")) + +if __name__ == "__main__": make_audit().main() diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 94d8f842..532c67a3 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -97,6 +97,7 @@ try { 'logical-boundaries'=31808; 'logical-and-indirect-captured'=12288; 'logical-not-displacement-captured'=3072; + 'logical-andi-displacement-captured'=3072; 'logical-eor-postincrement-captured'=4096; 'logical-invalid-operands'=13248; 'logical-unary-invalid-operands'=$(if ($model -in @('68000','68010')) {12544} elseif ($model -eq '68060') {10304} else {10432}); From 56c0a69202d0b77a76ae7436a77f5a546a14adc2 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 17:42:19 +0300 Subject: [PATCH 165/211] Record per-case failed-reference witnesses --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 7 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 67 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 14 ++++ 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index e1567fa9..5966593d 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -35,8 +35,11 @@ it does not replace or discard their remaining gaps. two mismatching directories, with actual test exit 1. The first failures are 010 RTE invalid-format CCR and 060 ordinary STOP with a new S-clear SR. The original replay stops at those first failures. A separate isolated discovery now visits 6,248 RTE and 196,608 STOP - callbacks, retaining 2,233 / 65,536 mismatching cases; aggregate comparison - sites are observed, while individual architectural witnesses remain limited. Passing qualified presets + callbacks, retaining 2,233 / 65,536 mismatching cases. A later isolated + observer retains every passing/failing adapter image and proves per-case + SR/frame disagreement for RTE and vector/SR disagreement for STOP, with + unchanged comparison code and counts. Hardware expectations remain + unresolved; these images are diagnostic evidence. Passing qualified presets do not turn this broad audit green. 2. **Private continuation work is not production qualification.** The 020/030 read/write recovery candidates and their literal private formats have scoped diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index fb4dabdf..7cf789c0 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -12833,3 +12833,70 @@ scope; use this checkpoint for reproduction if later retirements change that class. The 010 RTE / 060 STOP broad gate and private-production restoration requirements remain open. Milestone 6 stays **in progress**, `roadmapComplete=false`. + + +## Per-case failed-reference witnesses — 2026-10-08 + +`BasicContinuationDiscoveryV4` extends the accepted isolated V3 observer with +one retained NDJSON witness per validation, including passing cases. Its frozen +mainline source is `3a5314f`; **229 mainline inputs plus one isolated test input** +leave all **37 production CPU inputs unchanged**. Original input files, native +bridge and V3 evidence remain immutable. No observer is imported into the CPU. + +Every witness records its model/group-relative ordinal, the exact callback's +initial integer registers/PC/SR and supervisor/master stack pointers, initial +instruction and supervisor-stack bytes, decoded reference register/PC/SR image, +the returned CPU image, expected/actual vectors, defined SR mask, comparison-site +deltas and supported expected/observed frame bytes. These are **adapter images**, +not a new hardware oracle. `expectedSrRaw` retains the reference tag's high-half +ignore bits; comparisons use the recorded mask. A no-exception sentinel is +reported as vector zero by this adapter. Snapshot reads are bounded and use zero +for unmapped bytes. Observed frame bytes are meaningful as exception-frame +evidence only when the actual vector/frame is valid; notably, the STOP vector +mismatch's raw memory snapshot is not an actual privilege-exception frame. + +All **202,856 disputed cases** and **eight control cases** have unique contiguous +ordinals and precisely match the V3 callback, failure, site and frame counts. +Per-case evidence now establishes co-occurrence, rather than inferring it from +aggregate site totals: + +| Selection | Passing cases | Mismatching cases | Sites in every mismatch | +| --- | ---: | ---: | --- | +| 68010 RTE | 4,015 | 2,233 | SR result and saved-frame byte | +| 68060 STOP | 131,072 | 65,536 | Exception vector and SR result | + +Every failing RTE witness expects and receives vector **14**; its input format +is neither legal 010 format 0 nor 8. The supported eight-byte saved frame differs +in SR, with the remaining bytes matching. The first failure is ordinal **12**: +initial SR `201F`, expected `2011`, actual `201F`; expected/actual frame images +are `20110087FFA00038` / `201F0087FFA00038`. +Every failing STOP witness starts in supervisor mode and supplies a new S-clear +SR. It expects vector **8** but receives **0**. The first is ordinal **3**, opcode +`4E72 0000`: expected SR `2000`, actual `0000`. + +Clean NOP controls preserve registers, PC and defined SR with no error sites. +Corrupted NOP controls change only returned D0 and each produce exactly the +unchanged-register comparison failure. All six native verdicts retain the V3 +contract. The single named discovery test still **fails with exit 1** after +recording all witnesses; this is deliberately failed reference evidence. + +Independent verification reverses every observer insertion and requires exact +equality with the accepted V3 comparison code/header. It binds all 3,546 pinned +input files, complete source graph, producer/native hashes, loaded assemblies, +exact named TRX result, six outputs, every ordinal and every site's counts. +The verifier's initial assumptions about counting only C# CPU files and an +ILLEGAL sentinel vector were rejected; the accepted verifier includes the CPU +project input and checks the existing adapter's vector-zero contract. Neither +execution nor expectations were changed to satisfy those checks. + +| Retained evidence | SHA-256 | +| --- | --- | +| V3 parent discovery proof | `64b51aec11c3568ad0c941d50d667ec4ceb287813e640146f5abba1bd360acaa` | +| V4 complete inputs | `0b829b0df2b6d99f979eba21ab53bac673c88894023b0a363e8efd2699e64d7d` | +| V4 execution and witness output hashes | `07eff4b7c6e15ad9c2290bd8953ccb9262e516240e1fb4ae9e1aef630dbd3bb9` | +| V4 independent per-case proof | `d808738036ce30bf89a929a729c3a754ff722d211e7db8c22d6b905e5d981386` | + +These witnesses confirm the location and extent of software disagreements; +they do not settle the undocumented behavior. No CPU fix, reference exclusion, +public release or new whole-suite/consumer run follows. The broad gate remains +failed and milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 64f0da6a..9e5aaf8e 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4021,3 +4021,17 @@ missing/wrong-weight controls rejecting. Its composed report-only gate passes The current graph has 229 source inputs and unchanged 37 CPU inputs. Original full/deep/consumer identities and broader failed/private gates remain retained; no publication follows. Milestone 6 stays **in progress**, `roadmapComplete=false`. + + +### Milestone 6 checkpoint — per-case reference witnesses, 2026-10-08 + +An isolated observer now retains all 202,856 disputed RTE/STOP cases and eight +clean/corrupted NOP control cases, with contiguous ordinals and initial, +reference and returned adapter images. Every 010 RTE mismatch has SR and saved +frame disagreement; every 060 STOP mismatch has vector and SR disagreement. +The 2,233 / 65,536 failure counts are unchanged. Reversing the observer additions +proves exact V3 comparison-code equality; complete input/source/native/assembly, +named failing test and per-case linkage are verified. The request still exits 1. +Source 3a5314f has 229 mainline inputs plus one isolated test, with all 37 CPU +inputs unchanged. No hardware qualification, guessed fix, exclusion, import or +publication follows. Milestone 6 stays **in progress**, `roadmapComplete=false`. From 55fe8b76f1c83fdb8cf9cac7ce5e5e6f5aa80dff Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 18:05:06 +0300 Subject: [PATCH 166/211] Consolidate aliased MOVEA word displacement regression --- Copper68k.Tests/M68020HdfBootTests.cs | 26 ------- Copper68k.Tests/Synthetic/README.md | 22 ++++++ .../SyntheticMoveaDisplacementTests.cs | 37 ++++++++++ docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 4 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 53 +++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 ++++ ...copper68k-movea-displacement-retirement.py | 74 +++++++++++++++++++ scripts/test-copper68k-synthetic.ps1 | 2 +- 8 files changed, 204 insertions(+), 29 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticMoveaDisplacementTests.cs create mode 100644 scripts/test-copper68k-movea-displacement-retirement.py diff --git a/Copper68k.Tests/M68020HdfBootTests.cs b/Copper68k.Tests/M68020HdfBootTests.cs index effa5d34..c4d1b1f9 100644 --- a/Copper68k.Tests/M68020HdfBootTests.cs +++ b/Copper68k.Tests/M68020HdfBootTests.cs @@ -170,32 +170,6 @@ public void MoveByteDisplacementToPredecrementReadsAliasedSourceBeforeUpdatingAd } } - [Theory] - [InlineData(M68kCpuModel.M68020)] - [InlineData(M68kCpuModel.M68EC020)] - [InlineData(M68kCpuModel.M68030)] - [InlineData(M68kCpuModel.M68040)] - public void MoveaWordDisplacementSignExtendsBeforeOverwritingAliasedBase(M68kCpuModel model) - { - foreach (var value in new ushort[] { 0x8001, 0x7FFF, 0 }) - foreach (var displacement in new[] { -8, 8 }) - { - var bus = new Copper68kTestBus(0x10000); - bus.WriteWords(0x1000, 0x3269, (ushort)displacement); // MOVEA.W (d16,A1),A1 - var address = (uint)(0x3000 + displacement); - bus.WriteWord(address, value); - using var cpu = M68kCoreFactory.Default.Create(model, bus); - cpu.Reset(0x1000, 0x7000); - cpu.State.A[1] = 0x3000; - cpu.State.StatusRegister = 0x271F; - cpu.ExecuteInstruction(); - Assert.Equal(value == 0x8001 ? 0xFFFF8001u : (uint)value, cpu.State.A[1]); - Assert.Equal(value, bus.ReadWord(address)); - Assert.Equal(0x271F, cpu.State.StatusRegister); - Assert.Equal(0x1004u, cpu.State.ProgramCounter); - } - } - [Theory] [InlineData(M68kCpuModel.M68020)] [InlineData(M68kCpuModel.M68EC020)] diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index de3a5aef..de2c5d34 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1640,3 +1640,25 @@ and class inventory; reproduce against this checkpoint if later retirements change that class. Exact identities, retained sibling verification, negative controls and the separate full/report-only scopes are in the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#andi-long-displacement-consolidation--2026-10-08). + +## Captured aliased MOVEA displacement retirement + +`SyntheticMoveaDisplacementTests` reuses `MoveFixture` for the captured word +values and signed displacements, every aliased address register, both privilege +states and all CCRs: 3,072 cases per profile. Full register/SR/memory verification, +exact PC and the following NOP check sign extension and source-before-destination +ordering. The original A1 / supervisor / CCR-31 cases are included. + +```powershell +python scripts/test-copper68k-movea-displacement-retirement.py --output artifacts/movea-displacement-retirement +python scripts/test-copper68k-movea-displacement-retirement.py --validate-only --output artifacts/movea-displacement-retirement +``` + +The helper requires four pinned originals and all 8,192 replacement mismatches +under deliberate zero extension before retiring the rows. It checks every +failure identifier and verifies all retained HDF siblings. `--prepare` and +`--finish-retirement` support the before-removal workflow; strict replay and nine +corruption controls reject incomplete or changed evidence. The ordinary gate +requires the replacement group. Reproduce at this checkpoint if later HDF +retirements change its pinned class inventory. See the +[qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#movea-word-aliased-displacement-consolidation--2026-10-08). diff --git a/Copper68k.Tests/Synthetic/SyntheticMoveaDisplacementTests.cs b/Copper68k.Tests/Synthetic/SyntheticMoveaDisplacementTests.cs new file mode 100644 index 00000000..b655f889 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticMoveaDisplacementTests.cs @@ -0,0 +1,37 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +public sealed class SyntheticMoveaDisplacementTests(ITestOutputHelper output) +{ + public static IEnumerable Models => SyntheticMoveTests.Models; + + [Theory, MemberData(nameof(Models)), Trait("Suite", "Synthetic")] + public void CapturedWordMoveaReadsAliasedDisplacementBeforeSignExtending(string modelId) + { + // M68000PM MOVEA: sign extend W to the complete address register, + // preserve CCR, and evaluate the source before replacing its base. + var machine = new SyntheticMachine(ModelSpec.All.Single(m => m.Id == modelId)); + var report = new CoverageBatch(modelId, "movea-displacement-captured"); + foreach (var value in new uint[] { 0x8001, 0x7fff, 0 }) + foreach (short displacement in new short[] { -8, 8 }) + for (var register = 0; register < 8; register++) + foreach (var supervisor in new[] { false, true }) + for (var ccr = 0; ccr < 32; ccr++) + { + var opcode = (ushort)(0x3068 | register << 9 | register); + if (register == 1) Assert.Equal(0x3269, opcode); + var fixture = new MoveFixture(machine, opcode, value, ccr, supervisor, + scenario: $"captured-displacement/super={supervisor}/disp={displacement}", + options: new(SourceDisplacement: displacement), + customize: m => { + if (register == 7) m.Core.State.SetActiveStackPointer(0x3000); + else m.Core.State.A[register] = 0x3000; + }); + SyntheticMoveTests.Run(machine, report, fixture); + Assert.Equal(0x1004u, fixture.NextPc); + Assert.Equal(value, fixture.OperandValue); + } + report.Complete(output); + } +} diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 5966593d..34278f48 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -18,11 +18,11 @@ it does not replace or discard their remaining gaps. | Independent audits across 000, 010, EC020, 020, 030, 040, 060 and A1200 | Pinned reference inputs and adapters; executed selections for every profile; explicit exclusions and comparator checks | Fresh SingleStepTests 000 and Musashi eight-profile audits pass on the frozen current assemblies. The broad WinUAE audit still has two unresolved mismatching directories. | | Complete integer-family inventory; later families cannot disappear | `IntegerInventory.cs`, generated model/family inventory and named execution reports | Inventory and reporting are implemented. Inventory membership alone does not prove execution or internal restoration coverage. | | Coverage by architectural combination, with passing/mismatching/unsupported/untested distinct | Actual per-model report keys, weights and statuses; exact named TRX selection; no empty or substituted reports | The ordinary gate validates required groups and counts. The frozen c284b94 full run is qualified: 86,806,858 passing cases / 967 reports. The guard and later ANDI consolidation retain separate focused execution evidence. | -| Deterministic coverage in ordinary CI | Current `test-copper68k-synthetic.ps1` and CI invocation; all required reports and positive case counts | Implemented, including 4,096 EOR postincrement and 3,072 ANDI displacement cases per profile. Frozen full execution and the separately qualified duplicate-selection guard are explicitly linked. | +| Deterministic coverage in ordinary CI | Current `test-copper68k-synthetic.ps1` and CI invocation; all required reports and positive case counts | Implemented, including 4,096 EOR postincrement and 3,072 ANDI / MOVEA displacement cases per profile. Frozen full execution and the separately qualified duplicate-selection guard are explicitly linked. | | Explicit deep-audit command, deterministic seeds and strict missing/empty/mismatch rejection | Deep command/settings, recorded seed and selected inputs; rejected negative controls | Fresh seed 68020 runs 10,000 samples per family/profile: 320,000 passing cases. Six actual invalid/missing/empty requests fail. Mismatch rejection has separate mutation evidence; retain final source/evidence linkage. | | Reuse SingleStepTests, Musashi and WinUAE with pinned identities and caveats | Adapter source, reference manifests, source/input hashes, actual execution and documented exclusions | Implemented. SingleStepTests supplies 000 semantic fixtures; Musashi runs independent self-checking programs, not its CPU as an oracle. WinUAE remains a software reference. | | Replacement detects absolute decoding, extension length, index sign, alias order, A7 stride and flags defects | Six distinct isolated mutation proofs with precise replacement IDs and current source linkage | The maintained mutation command defines all six. Fresh isolated 51444ea executions detect all six against a 75,214-case clean baseline. Exact source/roster/report linkage and every mismatch ID are verified. | -| Retire an old regression only after a mapped replacement detects its defect | Exact pinned old method/rows, before-removal proof, mutation witness and after-removal sibling verification | NOT displacement, EOR postincrement and ANDI displacement retirements have exact proofs. EOR replaces one fact with 32,768 passing cases and retains four siblings. No blanket regression deletion is authorized. | +| Retire an old regression only after a mapped replacement detects its defect | Exact pinned old method/rows, before-removal proof, mutation witness and after-removal sibling verification | NOT displacement, EOR postincrement, ANDI displacement and aliased MOVEA word retirements have exact proofs. EOR replaces one fact with 32,768 passing cases and retains four siblings. No blanket regression deletion is authorized. | | Retain cache, prefetch, bus ordering, fault sequencing, JIT and native regressions unless separately proven redundant | Exact test-source diff and full named roster, with every removal accounted for | Specialized tests remain. The exact 5,400-test frozen roster is compared with its predecessor, including all retirements, replacements and unavailable additions. | | Production CPU fixes pass full CPU and affected consumers | Same-source full CPU proof; isolated NuGet package; CopperScreen production, host/disk/engine and applicable native results | MOVE16 recovery has full-suite and local .77 consumer evidence. Subsequent consolidation leaves all 37 CPU source inputs unchanged. No new consumer execution is claimed. | | Preserve successful ordering and timing policy; no automatic retry after partial effects | Relevant sequence/fault tests and change review, separate from timing qualification | Existing checks and scoped explicit-RTE continuation proofs remain. Private candidate results are not production behavior or physical timing qualification. | diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 7cf789c0..7c38a476 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -12900,3 +12900,56 @@ These witnesses confirm the location and extent of software disagreements; they do not settle the undocumented behavior. No CPU fix, reference exclusion, public release or new whole-suite/consumer run follows. The broad gate remains failed and milestone 6 remains **in progress**, `roadmapComplete=false`. + + +## MOVEA word aliased-displacement consolidation — 2026-10-08 + +`SyntheticMoveaDisplacementTests` replaces the four model rows of +`M68020HdfBootTests.MoveaWordDisplacementSignExtendsBeforeOverwritingAliasedBase`, +pinned at `56c0a69202d0b77a76ae7436a77f5a546a14adc2`. It reuses `MoveFixture`, +independent addressing setup and full architectural/memory checks through the +public CPU factory. The exact captured `8001`, `7FFF`, zero and signed -8/+8 +displacements are extended to every aliased address register, both privilege +states and all 32 CCRs: **3,072 cases per profile / 24,576 across eight profiles**. +Literal `3269` is checked, exact next PC is `1004`, and the following NOP exposes +overconsumed extensions. Word sign extension replaces the complete address +register, source memory remains unchanged and MOVEA preserves full SR/CCR. +The original A1/supervisor/CCR-31 combinations are included; surrounding-memory +guards use the shared fixture. This retires semantic rows, not native HDF boot. + +Before removal, **67 original HDF rows plus eight replacement batches** pass. +Deliberate zero extension in the isolated base/advanced execution paths fails +all four old rows and exactly **8,192 replacement cases**. Every mismatch ID +and register diagnostic is checked against an independently enumerated bit +pattern, as are all combination keys/weights, source graphs, loaded assemblies +and named TRX selections. After exact removal, **63 retained HDF rows plus eight +replacement batches** pass. Nine copied evidence corruptions reject; strict +replay reproduces the proof. All **37 CPU inputs remain unchanged** in the +current **230-input** test/CPU graph. + +The ordinary gate requires the new group for every profile. Its composed +**report-only** request passes **86,663,530 semantic cases / 783 batches / 983 +profile reports**, retaining all 975 previous reports and the integer inventory +plus eight freshly executed replacement reports. Actual missing-report and +wrong-case-count requests reject for the 000 replacement report. This is not a +new full CPU run; earlier full, deep/reference and local .77 consumer evidence +retain their original identities. No new package or consumer run is claimed. + +| Evidence | SHA-256 | +| --- | --- | +| Before-removal preparation | `1423e58b8914b02aa9b96c68371b46bd990e8130bd4cdd1a207f1801a954236c` | +| Complete Clean / Mutation / Current proof | `d316e73ed45bf8b7aa1ca07a6f7933718ae7a582b36168f58d6b4ded0d57eda2` | +| Independent source / binary / per-case linkage | `60c0f2dc46011aca711e3082983358b724318aa1f151a89ed2ee5ff1cc2be1a1` | +| Composed ordinary report-only gate | `39ab3117b0d8efed9afff595980a035ca7111542b2d6e7d51cf54ad69206ac39` | +| Two actual required-report controls | `18c17fcc46f00d1bc57bbdd4054ac293762cb26f8f06100d09639366bc12fb49` | + +```powershell +python scripts/test-copper68k-movea-displacement-retirement.py --output artifacts/movea-displacement-retirement +python scripts/test-copper68k-movea-displacement-retirement.py --validate-only --output artifacts/movea-displacement-retirement +``` + +The helper pins the original class inventory; use this checkpoint for future +reproduction if later HDF retirements change that class. The unresolved broad +RTE/STOP disagreements and private production/restoration requirements remain +open. No publication follows. Milestone 6 stays **in progress**, +`roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 9e5aaf8e..fb2629a4 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4035,3 +4035,18 @@ named failing test and per-case linkage are verified. The request still exits 1. Source 3a5314f has 229 mainline inputs plus one isolated test, with all 37 CPU inputs unchanged. No hardware qualification, guessed fix, exclusion, import or publication follows. Milestone 6 stays **in progress**, `roadmapComplete=false`. + + +### Milestone 6 checkpoint — captured MOVEA displacement retirement, 2026-10-08 + +Shared MOVE fixtures now cover all captured aliased word MOVEA values with +every address register, user/supervisor state and CCR: 24,576 passing cases. +Zero extension fails all four pinned old rows and exactly 8,192 replacements; +every diagnostic, key/weight and named selection is verified. Exact removal +leaves all 63 HDF siblings and eight replacement batches passing. Nine evidence +controls and strict replay pass. Ordinary CI requires 3,072 cases per profile, +with actual missing/wrong-count rejection. The composed report-only gate passes +86,663,530 cases / 783 batches / 983 reports, without claiming a new full run. +All 37 CPU inputs remain unchanged in the 230-input graph; earlier full, +reference and consumer identities remain retained. Broader failed/private gates +remain open. Milestone 6 stays **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-movea-displacement-retirement.py b/scripts/test-copper68k-movea-displacement-retirement.py new file mode 100644 index 00000000..8352d199 --- /dev/null +++ b/scripts/test-copper68k-movea-displacement-retirement.py @@ -0,0 +1,74 @@ +"""Pinned aliased MOVEA displacement replacement; isolated execution and replay.""" +from pathlib import Path +import itertools +import re +from copper68k_consolidation import Audit, check +REPO = Path(__file__).resolve().parent.parent +GROUP = "movea-displacement-captured" + +def mutate(path, text): + if path.endswith("M68kAdvancedTimingInterpreter.cs"): + start = text.index(" private void ExecuteMoveWordAddressDisplacementToAddress(") + end = text.index("\n private ", start + 20) + block = text[start:end] + old = "unchecked((uint)(int)(short)ReadWord(" + check(block.count(old) == 1, "Missing advanced MOVEA target") + return text[:start] + block.replace(old, "unchecked((uint)ReadWord(") + text[end:] + pattern = r"((?:operand\.Size|(?A{register}/captured-displacement/super={supervisor}/disp={displacement}" + failed = mode == "Mutation" and value == 0x8001 + status = "mismatching" if failed else "passing" + counts = combinations.setdefault(key, {}) + counts[status] = counts.get(status, 0) + 1 + if failed: + failures.append(dict(id=f"{key}/op={0x3068 | register << 9 | register:04X}/v={value:08X}/ccr={ccr:02X}", status=status, + reason=f"A{register} expected FFFF8001, actual 00008001")) + return dict(schema=1, model=model, group=GROUP, + counts=dict(passing=3072-len(failures), mismatching=len(failures), unsupported=0, untested=0), + logicalCases=3072, xunitBatches=1, combinations=combinations, failures=failures) + +class MoveaAudit(Audit): + def legacy_names(self): + names = {} + for match in re.finditer(r"public void (\w+)\(\s*([^)]*)\)", self.original): + method, signature = match.groups() + start = self.original.rfind("[Theory]", 0, match.start()) + parameters = [v.strip().split()[-1] for v in signature.split(",")] + if method == "AddLongPcDisplacementReadsMemoryFromExtensionPcAndSetsArithmeticFlags": + rows = itertools.product(["M68020", "M68EC020", "M68030", "M68040"], [-128, 128], + [(0, 1, 1, 0), (0xffffffff, 1, 0, 21), (0x7fffffff, 1, 0x80000000, 10), (0x80000000, 0x80000000, 0, 23)]) + values = [[m, str(d), *map(str, arithmetic)] for m, d, arithmetic in rows] + else: + rows = re.findall(r"\[InlineData\(([^)]*)\)\]", self.original[start:match.start()]) + values = [[v.strip().split('.')[-1] if v.strip().startswith('M68kCpuModel.') else str(int(v.strip(), 0)) for v in row.split(',')] for row in rows] + check(bool(values), "Missing pinned legacy rows") + for value in values: + check(len(value) == len(parameters), "Wrong pinned parameter inventory") + name = self.s['legacy_class'] + '.' + method + '(' + ', '.join(f'{k}: {v}' for k, v in zip(parameters, value)) + ')' + check(name not in names, "Duplicate pinned row") + names[name] = 'Passed' + check(len(names) == 67, "Incomplete pinned HDF class") + return names + + +def witness_error(name, message): + check(re.search(r"Expected:\s*4294934529\b", message) and re.search(r"Actual:\s*32769\b", message), "Wrong historical MOVEA sign extension") + +def make_audit(): + return MoveaAudit(dict(repo=REPO, producer=Path(__file__).resolve(), pin="56c0a69202d0b77a76ae7436a77f5a546a14adc2", + legacy="Copper68k.Tests/M68020HdfBootTests.cs", legacy_class="Copper68k.Tests.M68020HdfBootTests", legacy_cases=67, + witness="MoveaWordDisplacementSignExtendsBeforeOverwritingAliasedBase", witness_cases=4, + fixture="Copper68k.Tests/Synthetic/SyntheticMoveaDisplacementTests.cs", shared_class="Copper68k.Tests.Synthetic.SyntheticMoveaDisplacementTests", + method="CapturedWordMoveaReadsAliasedDisplacementBeforeSignExtending", group=GROUP, + mutation_files=["Copper68k/M68kCore.cs", "Copper68k/M68kAdvancedTimingInterpreter.cs"], mutate=mutate, report=report, + witness_outcome=lambda name: "Failed", witness_error=witness_error, + description="Prove aliased word MOVEA displacement shared coverage before exact four-row retirement.")) + +if __name__ == "__main__": make_audit().main() diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index 532c67a3..c38576f7 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -61,7 +61,7 @@ try { $logicalCases = 0 $logicalBatches = 0 foreach ($model in $knownModels) { - $expected = @{'move-opcodes'=9726; 'move-values-ccr'=58368; 'move-invalid-operands'=2562; 'move-alignment'=12; + $expected = @{'move-opcodes'=9726; 'movea-displacement-captured'=3072; 'move-values-ccr'=58368; 'move-invalid-operands'=2562; 'move-alignment'=12; 'move-extensions-aliases'=$(if ($model -in @('68000','68010')) {1756} else {7120}); 'move-register-overlap'=$(if ($model -in @('68000','68010')) {1801} else {2593}); 'move-address-boundary'=$(if ($model -in @('68000','68010')) {44} else {60}); From 86469f7eee4abd75916af7af761b9532f381dbd1 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 18:22:23 +0300 Subject: [PATCH 167/211] Bind retirement evidence to executed assemblies and methods --- Copper68k.Tests/Synthetic/README.md | 17 ++++ docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 2 +- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 59 ++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 +++ scripts/copper68k_consolidation.py | 92 ++++++++++++++++++- 5 files changed, 179 insertions(+), 6 deletions(-) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index de2c5d34..edd6b5f4 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1662,3 +1662,20 @@ corruption controls reject incomplete or changed evidence. The ordinary gate requires the replacement group. Reproduce at this checkpoint if later HDF retirements change its pinned class inventory. See the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#movea-word-aliased-displacement-consolidation--2026-10-08). + +## Retirement execution identity + +The shared retirement runner writes schema-2 evidence with hashes for both CPU +DLL copies and the test DLL. Replay requires those binaries to remain present +and unchanged, the CPU copies to agree, and each named result to match its test +definition and method. Both recorded assembly paths must identify the isolated +test DLL. Missing or inconsistent artifacts fail before a result is accepted. + +Each audit executes twenty corruption controls, including missing/changed +binaries and inconsistent test paths, IDs or methods. Relocated control copies +receive explicitly rebased paths before applying their individual corruption. +Historical schema-1 evidence is not upgraded in place; use its original source +checkpoint and independent verification record. Fresh MOVEA and EOR audits and +strict replay qualify the new runner, without changing their semantic coverage +or retiring additional tests. See the +[execution-identity record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#retirement-execution-identity--2026-10-08). diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 34278f48..4e9ad5f9 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -22,7 +22,7 @@ it does not replace or discard their remaining gaps. | Explicit deep-audit command, deterministic seeds and strict missing/empty/mismatch rejection | Deep command/settings, recorded seed and selected inputs; rejected negative controls | Fresh seed 68020 runs 10,000 samples per family/profile: 320,000 passing cases. Six actual invalid/missing/empty requests fail. Mismatch rejection has separate mutation evidence; retain final source/evidence linkage. | | Reuse SingleStepTests, Musashi and WinUAE with pinned identities and caveats | Adapter source, reference manifests, source/input hashes, actual execution and documented exclusions | Implemented. SingleStepTests supplies 000 semantic fixtures; Musashi runs independent self-checking programs, not its CPU as an oracle. WinUAE remains a software reference. | | Replacement detects absolute decoding, extension length, index sign, alias order, A7 stride and flags defects | Six distinct isolated mutation proofs with precise replacement IDs and current source linkage | The maintained mutation command defines all six. Fresh isolated 51444ea executions detect all six against a 75,214-case clean baseline. Exact source/roster/report linkage and every mismatch ID are verified. | -| Retire an old regression only after a mapped replacement detects its defect | Exact pinned old method/rows, before-removal proof, mutation witness and after-removal sibling verification | NOT displacement, EOR postincrement, ANDI displacement and aliased MOVEA word retirements have exact proofs. EOR replaces one fact with 32,768 passing cases and retains four siblings. No blanket regression deletion is authorized. | +| Retire an old regression only after a mapped replacement detects its defect | Exact pinned old method/rows, before-removal proof, mutation witness and after-removal sibling verification | NOT displacement, EOR postincrement, ANDI displacement and aliased MOVEA word retirements have exact proofs. EOR replaces one fact with 32,768 passing cases and retains four siblings. Schema-2 retirement replay now requires execution DLL hashes and exact loaded definition/method linkage; fresh MOVEA/EOR executions, 20 corruption controls each and strict replay are qualified. No blanket regression deletion is authorized. | | Retain cache, prefetch, bus ordering, fault sequencing, JIT and native regressions unless separately proven redundant | Exact test-source diff and full named roster, with every removal accounted for | Specialized tests remain. The exact 5,400-test frozen roster is compared with its predecessor, including all retirements, replacements and unavailable additions. | | Production CPU fixes pass full CPU and affected consumers | Same-source full CPU proof; isolated NuGet package; CopperScreen production, host/disk/engine and applicable native results | MOVE16 recovery has full-suite and local .77 consumer evidence. Subsequent consolidation leaves all 37 CPU source inputs unchanged. No new consumer execution is claimed. | | Preserve successful ordering and timing policy; no automatic retry after partial effects | Relevant sequence/fault tests and change review, separate from timing qualification | Existing checks and scoped explicit-RTE continuation proofs remain. Private candidate results are not production behavior or physical timing qualification. | diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 7c38a476..0d0c4d0d 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -12953,3 +12953,62 @@ reproduction if later HDF retirements change that class. The unresolved broad RTE/STOP disagreements and private production/restoration requirements remain open. No publication follows. Milestone 6 stays **in progress**, `roadmapComplete=false`. + + +## Retirement execution identity — 2026-10-08 + +Review of the maintained `copper68k_consolidation.py` runner found that replay +checked source, reports and result names but did not require execution binaries +or bind loaded assembly paths. An actual copied-MOVEA verification at `55fe8b7` +accepted **no build binaries** and definitions pointing to another directory. +This is a runner gap, not evidence that the independently linked delivered CPU +runs used incorrect assemblies. Their original proofs and outputs are retained. + +Schema-2 execution evidence now records the test DLL and both CPU DLL copies, +requires exact presence/hashes and matching CPU copies, and binds every named +result to its unique definition ID/name, assembly storage path and test-method +assembly/class/name. Both assembly paths must resolve to the isolated test DLL. +Aggregate proof entries retain those hashes as well as manifest/TRX identities. +The runner does not manufacture missing binaries or replay instructions. + +Fresh isolated **Clean / Mutation / Current** executions qualify two adapters: + +| Selection | Clean tests | Mutation tests | Current tests | Cases per eight-profile batch | Mutation mismatches | +| --- | ---: | ---: | ---: | ---: | ---: | +| MOVEA displacement / HDF witnesses | 75 passing | 12 failing as expected | 71 passing | 24,576 | 8,192 | +| EOR postincrement / logical witnesses | 13 passing | 9 failing as expected | 12 passing | 32,768 | 32,768 | + +Every case report is byte-identical to its preceding scoped identity audit, and +the complete named outcome rosters match. The 230-input graph and all 37 CPU +inputs are unchanged. Independent verification binds all sources, three +assemblies per mode, manifests/logs/TRX definitions/methods and all report files. +No new semantic coverage, retirement, whole-suite, consumer or hardware result +is claimed. Earlier full/reference and .77 consumer identities remain intact. + +Each adapter passes **20 actual copied-evidence rejection controls**, including +the retained nine source/report/roster controls and eleven new manifest, +binary, definition and method guards. Relocated controls copy binaries and +explicitly rebase both assembly paths before applying their corruption. +Independent inspection identifies every new corruption, and strict replay +reproduces both complete schema-2 proofs. An actual request to replay old +schema-1 MOVEA evidence rejects with `Wrong producer or scope`; the original +proof, manifests and TRX hashes remain unchanged. Use the historical source +checkpoint for old-format reproduction; no evidence is migrated or relabeled. + +The first storage-only candidate passed its narrower checks but accepted a +contradictory method assembly/name. That acceptance was reproduced and retained; +V1 is not the final complete identity gate. Fresh V2 execution adds method +binding and its discriminating controls rather than weakening expectations. + +| Evidence | SHA-256 | +| --- | --- | +| Original missing-binary / foreign-path acceptance | `77e67879ef2db665bf31480ad39ff2d405da8c8eea9d467716935894481b652a` | +| Storage-only candidate's method-identity gap | `2837c9e18540d0bdea40a708b2b27a1a80dc8f1f4238fff7c8cfcc701ba9b7fd` | +| Complete V2 MOVEA execution / 20 controls / replay | `6f920b7dfc7741e32baf2def44b3d67b3a6ac62e3a767aac03f4c8464bff0071` | +| Complete V2 EOR execution / 20 controls / replay | `0aee8a754493825789936f6702da1034c4175de628434bf64b34e77fbe9198a1` | +| Actual old-schema rejection / immutable original evidence | `c8742bdf5fec6d5901c763f5bfb8ac61e7d8615f19c5c0d1d6e20409c10dc115` | +| Independent complete source / binary / method / control linkage | `95415494e995a0338aa42a8663d95bce6b6741c93cbbc377c947eb713383a544` | + +The broad 010 RTE / 060 STOP gate remains failed, and private production and +restoration gaps remain open. No package is published. Milestone 6 stays +**in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index fb2629a4..58566bd5 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4050,3 +4050,18 @@ with actual missing/wrong-count rejection. The composed report-only gate passes All 37 CPU inputs remain unchanged in the 230-input graph; earlier full, reference and consumer identities remain retained. Broader failed/private gates remain open. Milestone 6 stays **in progress**, `roadmapComplete=false`. + + +### Milestone 6 checkpoint — retirement execution identity, 2026-10-08 + +The maintained retirement runner now requires schema-2 manifests with both CPU +DLLs and the test DLL, matching hashes and exact definition/method/path linkage. +Missing binaries and foreign paths were reproduced as accepted by the old +runner; a storage-only candidate's method gap was also reproduced. Fresh V2 +MOVEA and EOR Clean/Mutation/Current executions preserve every prior case +report and named outcome, with twenty actual corruption controls per adapter +and strict replay passing. Old-schema replay rejects without altering retained +evidence. All 230 source inputs and 37 CPU inputs remain unchanged; no new +retirement, semantic scope, full-suite or consumer run is claimed. Broader +failed/private gates remain open. Milestone 6 stays **in progress**, +`roadmapComplete=false`. diff --git a/scripts/copper68k_consolidation.py b/scripts/copper68k_consolidation.py index 9f36e13e..4077496b 100644 --- a/scripts/copper68k_consolidation.py +++ b/scripts/copper68k_consolidation.py @@ -11,6 +11,11 @@ MODELS = ["68000", "68010", "68EC020", "68020", "68030", "68040", "68060", "A1200"] NS = {"t": "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"} +EXECUTED_ASSEMBLIES = ( + "build/bin/Copper68k/release/Copper68k.dll", + "build/bin/Copper68k.Tests/release/Copper68k.Tests.dll", + "build/bin/Copper68k.Tests/release/Copper68k.dll", +) def check(ok, reason): @@ -74,18 +79,26 @@ def command(self, root, mode): "--artifacts-path", str(root / "build"), "--filter", f"FullyQualifiedName~{self.s['method']}|FullyQualifiedName~{selection}", "--logger", "trx;LogFileName=audit.trx", "--results-directory", str(root)] + @staticmethod + def assemblies(root): + check(all((root / path).is_file() for path in EXECUTED_ASSEMBLIES), "Missing executed assembly") + hashes = {path: sha(root / path) for path in EXECUTED_ASSEMBLIES} + check(hashes[EXECUTED_ASSEMBLIES[0]] == hashes[EXECUTED_ASSEMBLIES[2]], "Executed CPU copies disagree") + return hashes + def execute(self, root, mode): for p, contents in self.sources(mode).items(): target = root / "source" / p target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(contents) args = self.command(root, mode) - data = dict(schema=1, mode=mode, pin=self.s["pin"], producers=self.producers(), sources=self.inventory(root / "source"), protected=self.protected(), command=args) + data = dict(schema=2, mode=mode, pin=self.s["pin"], producers=self.producers(), sources=self.inventory(root / "source"), protected=self.protected(), command=args) env = dict(os.environ, COPPER68K_SYNTHETIC_MODELS=",".join(MODELS), COPPER68K_SYNTHETIC_REPORT_DIR=str(root)) print(mode + ": executing shared coverage and pinned legacy selection", flush=True) with (root / "execution.log").open("w", encoding="utf-8") as log: data["testExit"] = subprocess.run(args, cwd=self.repo, env=env, stdout=log, stderr=subprocess.STDOUT).returncode data["logSha256"] = sha(root / "execution.log") + data["executedAssemblies"] = self.assemblies(root) save(root / "inputs.json", data) def legacy_names(self): @@ -111,13 +124,15 @@ def legacy_names(self): def verify(self, root, mode): data = load(root / "inputs.json") - check(data["schema"] == 1 and data["mode"] == mode and data["pin"] == self.s["pin"] and data["producers"] == self.producers(), "Wrong producer or scope") + check(data["schema"] == 2 and data["mode"] == mode and data["pin"] == self.s["pin"] and data["producers"] == self.producers(), "Wrong producer or scope") ids = {p: hashlib.sha256(v).hexdigest() for p, v in self.sources(mode).items()} check(data["sources"] == ids, "Wrong baseline or mutation identity") check(self.inventory(root / "source") == ids, "Incomplete or changed source inventory") check(data["protected"] == self.protected(), "Wrong protected identities") check(data["command"] == self.command(root, mode), "Wrong execution command") check(data["testExit"] == (1 if mode == "Mutation" else 0) and data["logSha256"] == sha(root / "execution.log"), "Wrong execution or log") + check(set(data.get("executedAssemblies", {})) == set(EXECUTED_ASSEMBLIES), "Wrong executed assembly manifest") + check(data["executedAssemblies"] == self.assemblies(root), "Changed executed assembly") names = self.legacy_names() witnesses = {n for n in names if self.s["witness"] in n} check(len(witnesses) == self.s["witness_cases"], "Wrong witness roster") @@ -129,6 +144,20 @@ def verify(self, root, mode): tree = ET.parse(root / "audit.trx") tests = tree.findall(".//t:UnitTestResult", NS) check(len(tests) == len(names) and {t.get("testName") for t in tests} == set(names), "Wrong or empty execution selection") + definitions = tree.findall(".//t:UnitTest", NS) + expected_storage = (root / EXECUTED_ASSEMBLIES[1]).resolve() + check(len(definitions) == len(names) and + {d.get("id"): d.get("name") for d in definitions} == + {t.get("testId"): t.get("testName") for t in tests} and + len({d.get("id") for d in definitions}) == len(names) and + all(d.get("storage") and Path(d.get("storage")).resolve() == expected_storage for d in definitions), + "Wrong loaded test assembly or definitions") + for definition in definitions: + method = definition.find("t:TestMethod", NS) + check(method is not None and method.get("codeBase") and + Path(method.get("codeBase")).resolve() == expected_storage and + method.get("className", "") + "." + method.get("name", "") == definition.get("name").split("(", 1)[0], + "Wrong loaded test method identity") check(all(t.get("outcome") == names[t.get("testName")] for t in tests), "Wrong execution outcomes") for t in tests: name = t.get("testName") @@ -150,14 +179,25 @@ def verify(self, root, mode): passed += expected["counts"]["passing"] missed += expected["counts"]["mismatching"] check({p.name for p in root.glob(f"*-{self.s['group']}.json")} == {r["path"] for r in reports}, "Foreign report selection") - return dict(mode=mode, passing=passed, mismatching=missed, executions=len(names), reports=reports, inputsSha256=sha(root / "inputs.json"), trxSha256=sha(root / "audit.trx")) + return dict(mode=mode, passing=passed, mismatching=missed, executions=len(names), reports=reports, executedAssemblies=data["executedAssemblies"], inputsSha256=sha(root / "inputs.json"), trxSha256=sha(root / "audit.trx")) def integrity(self, output, validate_only): definitions = [("MissingFixture", "Incomplete or changed source inventory"), ("WrongProducer", "Wrong producer or scope"), ("EmptySelection", "Wrong or empty execution selection"), ("MissingWitness", "Wrong or empty execution selection"), ("WrongWeight", "Wrong report keys weights or failures"), ("WrongFailure", "Wrong report keys weights or failures"), ("ChangedMutationAndManifest", "Wrong baseline or mutation identity"), ("WrongCommand", "Wrong execution command"), - ("WrongCounters", "Wrong TRX counters")] + ("WrongCounters", "Wrong TRX counters"), + ("MissingAssemblyManifest", "Wrong executed assembly manifest"), + ("MissingTestAssembly", "Missing executed assembly"), + ("MissingCpuAssembly", "Missing executed assembly"), + ("ChangedTestAssembly", "Changed executed assembly"), + ("ChangedCpuAssembly", "Executed CPU copies disagree"), + ("WrongLoadedAssembly", "Wrong loaded test assembly or definitions"), + ("MissingTestDefinition", "Wrong loaded test assembly or definitions"), + ("WrongTestDefinitionId", "Wrong loaded test assembly or definitions"), + ("MissingTestMethod", "Wrong loaded test method identity"), + ("WrongMethodAssembly", "Wrong loaded test method identity"), + ("WrongMethodName", "Wrong loaded test method identity")] records = [] for name, reason in definitions: target = output / "integrity" / name @@ -166,6 +206,17 @@ def integrity(self, output, validate_only): shutil.copytree(parent / "source", target / "source", ignore=shutil.ignore_patterns("bin", "obj")) for path in [parent / "inputs.json", parent / "execution.log", parent / "audit.trx", *parent.glob(f"*-{self.s['group']}.json")]: shutil.copyfile(path, target / path.name) + for relative in EXECUTED_ASSEMBLIES: + path = target / relative + path.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(parent / relative, path) + # A relocated control has its own copied assembly. Rebase only + # the recorded path before applying its single corruption. + tree = ET.parse(target / "audit.trx") + for definition in tree.findall(".//t:UnitTest", NS): + definition.set("storage", str((target / EXECUTED_ASSEMBLIES[1]).resolve())) + definition.find("t:TestMethod", NS).set("codeBase", str((target / EXECUTED_ASSEMBLIES[1]).resolve())) + tree.write(target / "audit.trx") data = load(target / "inputs.json") data["command"] = self.command(target, "Mutation") if name == "MissingFixture": @@ -181,6 +232,37 @@ def integrity(self, output, validate_only): data["sources"][key] = sha(path) elif name == "WrongCommand": data["command"][data["command"].index("--filter") + 1] = "FullyQualifiedName~Nothing" + elif name == "MissingAssemblyManifest": + data.pop("executedAssemblies") + elif name in ["MissingTestAssembly", "MissingCpuAssembly"]: + path = target / EXECUTED_ASSEMBLIES[1 if name == "MissingTestAssembly" else 0] + check(path.resolve().is_relative_to(target.resolve()), "Unsafe integrity path") + path.unlink() + elif name in ["ChangedTestAssembly", "ChangedCpuAssembly"]: + path = target / EXECUTED_ASSEMBLIES[1 if name == "ChangedTestAssembly" else 2] + with path.open("ab") as stream: + stream.write(b"intentional integrity corruption") + elif name in ["WrongLoadedAssembly", "MissingTestDefinition", "WrongTestDefinitionId"]: + tree = ET.parse(target / "audit.trx") + definitions = tree.find("t:TestDefinitions", NS) + if name == "WrongLoadedAssembly": + definitions[0].set("storage", str((parent / EXECUTED_ASSEMBLIES[1]).resolve())) + elif name == "MissingTestDefinition": + definitions.remove(definitions[0]) + else: + definitions[0].set("id", "00000000-0000-0000-0000-000000000000") + tree.write(target / "audit.trx") + elif name in ["MissingTestMethod", "WrongMethodAssembly", "WrongMethodName"]: + tree = ET.parse(target / "audit.trx") + definition = tree.find("t:TestDefinitions/t:UnitTest", NS) + method = definition.find("t:TestMethod", NS) + if name == "MissingTestMethod": + definition.remove(method) + elif name == "WrongMethodAssembly": + method.set("codeBase", str((parent / EXECUTED_ASSEMBLIES[1]).resolve())) + else: + method.set("name", "CompletelyDifferentInstruction") + tree.write(target / "audit.trx") elif name in ["EmptySelection", "MissingWitness", "WrongCounters"]: tree = ET.parse(target / "audit.trx") if name == "WrongCounters": @@ -231,7 +313,7 @@ def main(self): self.execute(root, mode) entries.append(self.verify(root, mode)) print(f"{mode}: {entries[-1]['passing']} passing / {entries[-1]['mismatching']} precise mismatches", flush=True) - proof = dict(schema=1, producers=self.producers(), pin=self.s["pin"], entries=entries, + proof = dict(schema=2, producers=self.producers(), pin=self.s["pin"], entries=entries, integrity=self.integrity(output, args.validate_only or args.finish_retirement), retiredRows=self.s["witness_cases"] if not args.prepare else 0, productionCpuChanged=False, publication=False, roadmapComplete=False) path = output / ("preparation-proof.json" if args.prepare else "proof.json") From 90f0cb2205b9194a13ba36eb7f3222fa9de685b9 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 18:44:38 +0300 Subject: [PATCH 168/211] Record current-build seeded and software-reference qualification --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 13 ++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 26 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 13 ++++++++++ 3 files changed, 52 insertions(+) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 4e9ad5f9..31074270 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -333,3 +333,16 @@ scope; use this checkpoint for reproduction if later retirements change that class. The 010 RTE / 060 STOP broad gate and private-production restoration requirements remain open. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +### Current-build reference evidence, 2026-10-08 + +The frozen `86469f7` build now has independently verified fresh deep/reference +evidence: 320,000 seeded cases, 312,500 SingleStepTests cases and 536 Musashi +programs pass. Its 88 Musashi exclusions remain explicit. Proof SHA-256: +`293c7675e4f9e5f70b6b5fa602eab9ea0bc4b26724b553e6a477f5063883b64e`. +All 230 source/project inputs and 37 CPU inputs are bound to the executed +assemblies and named test methods. The same-build full-suite execution is +still pending; this checkpoint does not close the coherent final-evidence +requirement. No hardware is available for disputed 030/040 boundaries. The +failed broad gate and private-production restoration work remain required; +milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 0d0c4d0d..cbdc5565 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13012,3 +13012,29 @@ binding and its discriminating controls rather than weakening expectations. The broad 010 RTE / 060 STOP gate remains failed, and private production and restoration gaps remain open. No package is published. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +### Current-build seeded and software-reference checkpoint, 2026-10-08 + +`CurrentDeepReferencesV2` executes the three named seeded, SingleStepTests and +Musashi selections against the already built, frozen `86469f7` assemblies: +230 source/project inputs and 37 unchanged CPU inputs. All three pass. Independent +verification binds the source graph, all three assembly hashes, exact TRX +definitions and method assembly paths, reference inputs and every report. + +Seed 68020 with 10,000 samples per model produces **320,000 passing cases / +32 reports** across all eight profiles. The pinned SingleStepTests corpus +produces **312,500 passing cases / 125 files**; its 127 recorded inputs retain +the documented TAS/TRAPV exclusions. The pinned Musashi corpus produces +**536 passing programs / 88 documented exclusions** from 78 inputs across +eight profiles. Reference pins remain respectively +`64b253116a3de04aaac4346c43680960dc9b67e5` and +`72c1d74800f3087b45a0c1a7342601bbed898881`. + +The independently verified proof SHA-256 is +`293c7675e4f9e5f70b6b5fa602eab9ea0bc4b26724b553e6a477f5063883b64e`. +The separate `CurrentReferenceFullV2` execution uses the same assemblies and +is still running at this checkpoint; no full-suite pass is claimed. Earlier +full-suite, rejection-control, mutation and consumer evidence retains its own +identity. No hardware is available to settle disputed 030/040 boundaries; +software agreement cannot close them. The broad failed and private-restoration +gates remain open. Milestone 6 stays **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 58566bd5..ccd2a94a 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4065,3 +4065,16 @@ evidence. All 230 source inputs and 37 CPU inputs remain unchanged; no new retirement, semantic scope, full-suite or consumer run is claimed. Broader failed/private gates remain open. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +### Milestone 6 checkpoint — current-build deep references, 2026-10-08 + +Fresh seeded and pinned software-reference execution against frozen `86469f7` +assemblies passes: 320,000 seeded cases / 32 reports, 312,500 SingleStepTests +cases / 125 files and 536 Musashi programs with 88 documented exclusions. +Independent verification binds all 230 source/project inputs, 37 unchanged CPU +inputs, loaded assemblies, exact named methods, corpus inputs and reports. +Proof: `293c7675e4f9e5f70b6b5fa602eab9ea0bc4b26724b553e6a477f5063883b64e`. +The separate full CPU run is still pending. With no hardware available, disputed +030/040 boundaries remain unqualified; broad failed and private-restoration +requirements also remain open. Milestone 6 stays **in progress**, +`roadmapComplete=false`. See the reference qualification checkpoint for scope. From 4b3923d51490abfbf3dc3f84053ea63327cf5847 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 18:49:43 +0300 Subject: [PATCH 169/211] Qualify deep-audit rejection guards against current assemblies --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 8 ++++++-- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 11 +++++++++++ docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 5 +++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 31074270..2bfd7e14 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -19,7 +19,7 @@ it does not replace or discard their remaining gaps. | Complete integer-family inventory; later families cannot disappear | `IntegerInventory.cs`, generated model/family inventory and named execution reports | Inventory and reporting are implemented. Inventory membership alone does not prove execution or internal restoration coverage. | | Coverage by architectural combination, with passing/mismatching/unsupported/untested distinct | Actual per-model report keys, weights and statuses; exact named TRX selection; no empty or substituted reports | The ordinary gate validates required groups and counts. The frozen c284b94 full run is qualified: 86,806,858 passing cases / 967 reports. The guard and later ANDI consolidation retain separate focused execution evidence. | | Deterministic coverage in ordinary CI | Current `test-copper68k-synthetic.ps1` and CI invocation; all required reports and positive case counts | Implemented, including 4,096 EOR postincrement and 3,072 ANDI / MOVEA displacement cases per profile. Frozen full execution and the separately qualified duplicate-selection guard are explicitly linked. | -| Explicit deep-audit command, deterministic seeds and strict missing/empty/mismatch rejection | Deep command/settings, recorded seed and selected inputs; rejected negative controls | Fresh seed 68020 runs 10,000 samples per family/profile: 320,000 passing cases. Six actual invalid/missing/empty requests fail. Mismatch rejection has separate mutation evidence; retain final source/evidence linkage. | +| Explicit deep-audit command, deterministic seeds and strict missing/empty/mismatch rejection | Deep command/settings, recorded seed and selected inputs; rejected negative controls | Frozen 86469f7 seed 68020 runs 10,000 samples per family/profile: 320,000 passing cases. Eight same-build invalid/missing/empty/duplicate requests fail. Mismatch rejection retains separate mutation evidence. The full same-build suite remains pending. | | Reuse SingleStepTests, Musashi and WinUAE with pinned identities and caveats | Adapter source, reference manifests, source/input hashes, actual execution and documented exclusions | Implemented. SingleStepTests supplies 000 semantic fixtures; Musashi runs independent self-checking programs, not its CPU as an oracle. WinUAE remains a software reference. | | Replacement detects absolute decoding, extension length, index sign, alias order, A7 stride and flags defects | Six distinct isolated mutation proofs with precise replacement IDs and current source linkage | The maintained mutation command defines all six. Fresh isolated 51444ea executions detect all six against a 75,214-case clean baseline. Exact source/roster/report linkage and every mismatch ID are verified. | | Retire an old regression only after a mapped replacement detects its defect | Exact pinned old method/rows, before-removal proof, mutation witness and after-removal sibling verification | NOT displacement, EOR postincrement, ANDI displacement and aliased MOVEA word retirements have exact proofs. EOR replaces one fact with 32,768 passing cases and retains four siblings. Schema-2 retirement replay now requires execution DLL hashes and exact loaded definition/method linkage; fresh MOVEA/EOR executions, 20 corruption controls each and strict replay are qualified. No blanket regression deletion is authorized. | @@ -341,7 +341,11 @@ evidence: 320,000 seeded cases, 312,500 SingleStepTests cases and 536 Musashi programs pass. Its 88 Musashi exclusions remain explicit. Proof SHA-256: `293c7675e4f9e5f70b6b5fa602eab9ea0bc4b26724b553e6a477f5063883b64e`. All 230 source/project inputs and 37 CPU inputs are bound to the executed -assemblies and named test methods. The same-build full-suite execution is +assemblies and named test methods. Eight fresh same-build invalid requests +reject with exact named failures and guard diagnostics, including duplicate +model IDs. Independent command/source/assembly/method/corpus linkage proof: +`f344233c7317e1957dc436d0be12a4bca093bb167130ff8fc70741a2510e0140`. +These controls generate no coverage reports. The same-build full-suite execution is still pending; this checkpoint does not close the coherent final-evidence requirement. No hardware is available for disputed 030/040 boundaries. The failed broad gate and private-production restoration work remain required; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index cbdc5565..faf4041c 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13032,6 +13032,17 @@ eight profiles. Reference pins remain respectively The independently verified proof SHA-256 is `293c7675e4f9e5f70b6b5fa602eab9ea0bc4b26724b553e6a477f5063883b64e`. +`CurrentDeepRejectionControlsV2` then executes eight actual requests against +those same assemblies: zero seed, zero samples, empty models, missing +SingleStepTests, empty SingleStepTests selection, missing Musashi, and adjacent +and separated duplicate model IDs. Each exits 1 with exactly one named failing +test and its expected guard diagnostic, without a generated coverage report. +Independent verification checks command/settings, all source and assembly +hashes, exact TRX result/definition/method paths and unchanged reference inputs. +Execution proof: `ad35d75a3552113678d18ad1d3a83ed4fca06efcf93d1f863231a0761829565b`; +independent proof: `f344233c7317e1957dc436d0be12a4bca093bb167130ff8fc70741a2510e0140`. +These are adapter rejection controls, not extra instruction coverage. + The separate `CurrentReferenceFullV2` execution uses the same assemblies and is still running at this checkpoint; no full-suite pass is claimed. Earlier full-suite, rejection-control, mutation and consumer evidence retains its own diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index ccd2a94a..16d84973 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4074,6 +4074,11 @@ cases / 125 files and 536 Musashi programs with 88 documented exclusions. Independent verification binds all 230 source/project inputs, 37 unchanged CPU inputs, loaded assemblies, exact named methods, corpus inputs and reports. Proof: `293c7675e4f9e5f70b6b5fa602eab9ea0bc4b26724b553e6a477f5063883b64e`. +Eight fresh invalid requests against the same assemblies each fail exactly one +named test with the intended diagnostic: six missing/empty/invalid input checks +plus adjacent and separated duplicate models. Exact command, source, binary, +method, report absence and unchanged corpus linkage is independently verified: +`f344233c7317e1957dc436d0be12a4bca093bb167130ff8fc70741a2510e0140`. The separate full CPU run is still pending. With no hardware available, disputed 030/040 boundaries remain unqualified; broad failed and private-restoration requirements also remain open. Milestone 6 stays **in progress**, From 29922b7aec41e2ae04c2fd9c58169f069cb23337 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 19:45:11 +0300 Subject: [PATCH 170/211] Qualify complete current CPU and reference evidence --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 23 ++++++----- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 40 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 18 +++++++++ 3 files changed, 71 insertions(+), 10 deletions(-) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 2bfd7e14..c489c37f 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -17,18 +17,18 @@ it does not replace or discard their remaining gaps. | --- | --- | --- | | Independent audits across 000, 010, EC020, 020, 030, 040, 060 and A1200 | Pinned reference inputs and adapters; executed selections for every profile; explicit exclusions and comparator checks | Fresh SingleStepTests 000 and Musashi eight-profile audits pass on the frozen current assemblies. The broad WinUAE audit still has two unresolved mismatching directories. | | Complete integer-family inventory; later families cannot disappear | `IntegerInventory.cs`, generated model/family inventory and named execution reports | Inventory and reporting are implemented. Inventory membership alone does not prove execution or internal restoration coverage. | -| Coverage by architectural combination, with passing/mismatching/unsupported/untested distinct | Actual per-model report keys, weights and statuses; exact named TRX selection; no empty or substituted reports | The ordinary gate validates required groups and counts. The frozen c284b94 full run is qualified: 86,806,858 passing cases / 967 reports. The guard and later ANDI consolidation retain separate focused execution evidence. | +| Coverage by architectural combination, with passing/mismatching/unsupported/untested distinct | Actual per-model report keys, weights and statuses; exact named TRX selection; no empty or substituted reports | Frozen 86469f7 full execution is qualified: 86,856,010 passing cases / 983 reports. All combination keys, weights and statuses match retained or focused reference reports. The ordinary gate passes 86,663,530 cases / 783 batches. | | Deterministic coverage in ordinary CI | Current `test-copper68k-synthetic.ps1` and CI invocation; all required reports and positive case counts | Implemented, including 4,096 EOR postincrement and 3,072 ANDI / MOVEA displacement cases per profile. Frozen full execution and the separately qualified duplicate-selection guard are explicitly linked. | -| Explicit deep-audit command, deterministic seeds and strict missing/empty/mismatch rejection | Deep command/settings, recorded seed and selected inputs; rejected negative controls | Frozen 86469f7 seed 68020 runs 10,000 samples per family/profile: 320,000 passing cases. Eight same-build invalid/missing/empty/duplicate requests fail. Mismatch rejection retains separate mutation evidence. The full same-build suite remains pending. | +| Explicit deep-audit command, deterministic seeds and strict missing/empty/mismatch rejection | Deep command/settings, recorded seed and selected inputs; rejected negative controls | Frozen 86469f7 seed 68020 runs 10,000 samples per family/profile: 320,000 passing cases. Eight same-build invalid/missing/empty/duplicate requests fail. Full and deep execution now share verified assembly identities. Mismatch rejection retains separate mutation evidence. | | Reuse SingleStepTests, Musashi and WinUAE with pinned identities and caveats | Adapter source, reference manifests, source/input hashes, actual execution and documented exclusions | Implemented. SingleStepTests supplies 000 semantic fixtures; Musashi runs independent self-checking programs, not its CPU as an oracle. WinUAE remains a software reference. | | Replacement detects absolute decoding, extension length, index sign, alias order, A7 stride and flags defects | Six distinct isolated mutation proofs with precise replacement IDs and current source linkage | The maintained mutation command defines all six. Fresh isolated 51444ea executions detect all six against a 75,214-case clean baseline. Exact source/roster/report linkage and every mismatch ID are verified. | | Retire an old regression only after a mapped replacement detects its defect | Exact pinned old method/rows, before-removal proof, mutation witness and after-removal sibling verification | NOT displacement, EOR postincrement, ANDI displacement and aliased MOVEA word retirements have exact proofs. EOR replaces one fact with 32,768 passing cases and retains four siblings. Schema-2 retirement replay now requires execution DLL hashes and exact loaded definition/method linkage; fresh MOVEA/EOR executions, 20 corruption controls each and strict replay are qualified. No blanket regression deletion is authorized. | -| Retain cache, prefetch, bus ordering, fault sequencing, JIT and native regressions unless separately proven redundant | Exact test-source diff and full named roster, with every removal accounted for | Specialized tests remain. The exact 5,400-test frozen roster is compared with its predecessor, including all retirements, replacements and unavailable additions. | +| Retain cache, prefetch, bus ordering, fault sequencing, JIT and native regressions unless separately proven redundant | Exact test-source diff and full named roster, with every removal accounted for | Specialized tests remain. The exact 5,408-test frozen roster is compared with its predecessor: eight retired ANDI/MOVEA rows and sixteen replacement batches are accounted for. All 63 unavailable rows remain explicit. | | Production CPU fixes pass full CPU and affected consumers | Same-source full CPU proof; isolated NuGet package; CopperScreen production, host/disk/engine and applicable native results | MOVE16 recovery has full-suite and local .77 consumer evidence. Subsequent consolidation leaves all 37 CPU source inputs unchanged. No new consumer execution is claimed. | | Preserve successful ordering and timing policy; no automatic retry after partial effects | Relevant sequence/fault tests and change review, separate from timing qualification | Existing checks and scoped explicit-RTE continuation proofs remain. Private candidate results are not production behavior or physical timing qualification. | | Test-internal framework; public factory; NuGet consumer boundary; immutable published versions | Source/project/package diffs and consumer dependency inspection | Preserved. Private 39-input CPU candidates remain separate from the production 37-input CPU graph. No publication is authorized by this audit. | -## Gates that still prevent completion +## Completion gates and evidence status 1. **The broad software-reference gate is failed.** The retained `QualifiedBasicMove16RecoveryV1` audit reports 1,379 passing directories and @@ -47,11 +47,14 @@ it does not replace or discard their remaining gaps. record still lists wider restoration, operand, repair/refault, trace/interrupt and foreign-frame gaps. Preserve those distinctions when reviewing the promoted integer/system families. -3. **The final evidence set must be coherent.** Link the current complete - source graph, roster, ordinary reports, references, seeded audits and - consolidation witnesses. Do not substitute the latest focused test for a - full suite, relabel an unavailable test as coverage, or combine different - fixture revisions as one executed run. +3. **Current snapshot coherence is verified.** The frozen `86469f7` graph has + complete full-suite, ordinary-gate and same-assembly deep/reference/guard + evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; + their reports match the full run. The six MOVE mutation baseline reports + match full execution, and all 37 CPU inputs match the retained .77 consumer + package. Proof: `fea989f4167448e1c35576e4269416a67d54b8b0a8f9f440a9f2b00d5f0368f4`. + Future source changes require renewed linkage. This closes snapshot + coherence, while gates 1 and 2 still prevent milestone completion. For ordinary STOP, [M68000PM 6-85](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) describes the incoming supervisor check followed by SR transfer and stopping. @@ -81,7 +84,7 @@ Publication is a separate release step. PR #22 remains a draft, and `roadmapComplete=false` until a requirement-by-requirement final audit proves completion. -## Current validation work — 2026-10-08 +## Earlier frozen validation — 2026-10-08 (c284b94) Current source checkpoint: `c284b948263501183136656307cab2ecce6e0479`. The completed isolated `CurrentReferenceFullV1` run snapshots **228 test/CPU inputs**, diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index faf4041c..fa722bda 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13049,3 +13049,43 @@ full-suite, rejection-control, mutation and consumer evidence retains its own identity. No hardware is available to settle disputed 030/040 boundaries; software agreement cannot close them. The broad failed and private-restoration gates remain open. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +### Complete current-build qualification and evidence linkage, 2026-10-08 + +`CurrentReferenceFullV2` has finished with **5,345 passing tests, 63 unavailable, +zero failures / 5,408 total**. Independent V4 verification qualifies +**86,856,010 passing logical cases / 983 profile reports** and all ten retained +qualified WinUAE presets. It checks every named result, exact loaded assembly +and method identity, all report keys/weights/statuses, immutable reference input +sets, source graph and execution outputs. The ordinary gate passes +**86,663,530 semantic cases / 783 batches**. The 33 opcode-matrix rows that +legitimately share one definition are checked individually; none is dropped. +There are 5,376 definitions for the complete 5,408-result roster. + +The full proof SHA-256 is +`ee6dcf069e3747ea536bd3650ce66f0e727e51bb184b662d4df25cb4ac006980`. +The source snapshot is `86469f7eee4abd75916af7af761b9532f381dbd1`, with +230 source/project inputs and 37 unchanged CPU inputs. Later documentation +commits do not change that executed graph. The same assembly hashes bind the +320,000-case seeded run, 312,500-case SingleStepTests run, 536 Musashi programs +with 88 exclusions, and eight actual invalid deep-audit requests above. + +Independent cross-evidence linkage verifies that schema-2 EOR and MOVEA +retirement Current inputs equal the complete 230-input graph and their reports +equal full execution. The three retained six-defect MOVE baseline reports also +equal the full reports: 75,214 clean cases and 8,712 precisely mapped mutant +mismatches retain their earlier execution identity. All 37 CPU inputs match +the immutable local .77 package sources; the retained consumer package, +loaded CPU copies, NuGet boundary and test outputs remain intact. These are +171 host passes / six unavailable, 74 disk passes, 1,080 engine passes and two +native floppy replays. No new consumer execution is claimed. + +Complete cross-evidence linkage SHA-256: +`fea989f4167448e1c35576e4269416a67d54b8b0a8f9f440a9f2b00d5f0368f4`. +Current CPU/consumer linkage SHA-256: +`286e15c68243304a2a4df1465e8a2e2899ff25dd841fe3352b036757eddf2c3e`. +Earlier proof files retain their original identities. The independently +qualified snapshot closes the current evidence-coherence requirement; the +failed broad 010 RTE / 060 STOP gate and private-production restoration gaps +remain open. Hardware is unavailable for disputed boundaries. Milestone 6 +remains **in progress**, `roadmapComplete=false`; PR #22 remains draft. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 16d84973..434e9c8a 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4083,3 +4083,21 @@ The separate full CPU run is still pending. With no hardware available, disputed 030/040 boundaries remain unqualified; broad failed and private-restoration requirements also remain open. Milestone 6 stays **in progress**, `roadmapComplete=false`. See the reference qualification checkpoint for scope. + +### Milestone 6 checkpoint — complete current evidence, 2026-10-08 + +The frozen `86469f7` full suite and independent verification finish successfully: +5,345 passing / 63 unavailable / zero failed tests, 86,856,010 logical cases / +983 reports and ten qualified WinUAE presets. The ordinary gate passes +86,663,530 semantic cases / 783 batches. Exact source, loaded methods, all named +results, report keys/weights/statuses and reference input sets are verified. +Full proof: `ee6dcf069e3747ea536bd3650ce66f0e727e51bb184b662d4df25cb4ac006980`. + +Same-build deep/reference and eight rejection controls, unchanged-CPU .77 +consumer results, six-defect MOVE baseline reports and schema-2 EOR/MOVEA +retirement graphs are linked without relabeling earlier execution identities. +All 230 source/project inputs and 37 CPU inputs remain unchanged. Linkage proof: +`fea989f4167448e1c35576e4269416a67d54b8b0a8f9f440a9f2b00d5f0368f4`. +This closes current snapshot coherence. The failed broad reference gate and +private-production restoration requirements remain open; milestone 6 stays +**in progress**, `roadmapComplete=false`, and PR #22 remains draft. From ac4c18f9ee6cb7d67a254c694f8c4bce7fde36a8 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 20:09:29 +0300 Subject: [PATCH 171/211] Qualify private C021 return metadata guards --- .../SyntheticM68020RteMetadataTests.cs | 104 ++++++++++++++++++ docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 16 ++- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 51 +++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 17 +++ 4 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020RteMetadataTests.cs diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020RteMetadataTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020RteMetadataTests.cs new file mode 100644 index 00000000..0fff15de --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020RteMetadataTests.cs @@ -0,0 +1,104 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Interpreter-private C021 rejection controls, not foreign silicon frames. +public sealed class SyntheticM68020RteMetadataTests(ITestOutputHelper output) +{ + [EnvironmentFact("COPPER68K_RUN_020_RTE_METADATA", "qualify private C021 metadata rejection"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarPrivateMetadataControls() => Audit(false); + [EnvironmentFact("COPPER68K_RUN_020_RTE_METADATA", "qualify private C021 metadata rejection"), Trait("Suite", "ReferenceDiscovery")] + public void BatchPrivateMetadataControls() => Audit(true); + + private static readonly string[] Forms = ["valid", "phase-range", "phase-version", "phase-end", "fault-address", "ssw-size", "ssw-fc", "ssw-extra", "returned-frame", "returned-bank"]; + private void Audit(bool batch) + { + var failures = new List(); + foreach (var model in ModelSpec.All.Where(m => m.Id is "68EC020" or "68020" or "68030" or "A1200")) + { + var report = new CoverageBatch(model.Id, $"rte-private-metadata-{(batch ? "batch" : "scalar")}"); + var machine = new SyntheticMachine(model); + foreach (var master in new[] { false, true }) + foreach (var high in new[] { false, true }) + foreach (var form in Forms) + for (var ccr = 0; ccr < 32; ccr++) + { + var id = $"{model.Id}/RTE/private-C021/{form}/master={master}/high={high}/op=4E73/ccr={ccr:X2}"; + try { Run(machine, batch, master, high, ccr, form); report.Record(id, "passing", null); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + try { report.Complete(output); } catch (Exception ex) { failures.Add(ex); } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(e => e.Message))); + } + + private static void Run(SyntheticMachine m, bool batch, bool master, bool high, int ccr, string form) + { + m.Reset(); _ = SyntheticExecution.Prepare(m, [0x4e73, 0x7e2a, 0x4e71]); + var inner = high ? 0x12346000u : 0x6000u; var outer = inner - 92; + var sr = (ushort)((master ? 0x3700 : 0x2700) | ccr); + for (var at = outer - 8; at < inner + 0x110; at++) m.InitializePhysical(at, 0xa5, 1); + for (uint offset = 0; offset < 92; offset += 2) m.InitializePhysical(outer + offset, 0, 2); + m.InitializePhysical(outer, sr, 2); m.InitializePhysical(outer + 2, 0x9000, 4); + m.InitializePhysical(outer + 6, 0xb008, 2); m.InitializePhysical(outer + 8, 0xc021, 2); + m.InitializePhysical(outer + 10, 0x65, 2); // supplied word / supervisor data + m.InitializePhysical(outer + 16, inner, 4); m.InitializePhysical(outer + 20, inner, 4); + m.InitializePhysical(outer + 28, (uint)sr << 16, 4); m.InitializePhysical(outer + 32, 0x3000, 4); + m.InitializePhysical(outer + 44, sr, 4); // independent supplied initial SR + m.InitializePhysical(inner, sr, 2); m.InitializePhysical(inner + 2, 0x3000, 4); + m.InitializePhysical(inner + 6, 0, 2); m.InitializePhysical(0x3000, 0x7c2b, 2); + m.InitializePhysical(0x3002, 0x4e71, 2); + switch (form) + { + case "phase-range": m.InitializePhysical(outer + 40, 0xffff, 2); break; + case "phase-version": m.InitializePhysical(outer + 40, 3, 2); break; + case "phase-end": m.InitializePhysical(outer + 40, 4, 2); break; + case "fault-address": m.InitializePhysical(outer + 16, inner + 1, 4); break; + case "ssw-size": m.InitializePhysical(outer + 10, 0x45, 2); break; + case "ssw-fc": m.InitializePhysical(outer + 10, 0x61, 2); break; + case "ssw-extra": m.InitializePhysical(outer + 10, 0x265, 2); break; + case "returned-frame": + m.InitializePhysical(outer + 16, inner + 0x100, 4); m.InitializePhysical(outer + 20, inner + 0x100, 4); break; + case "returned-bank": m.InitializePhysical(outer, (uint)(sr ^ 0x1000), 2); break; + } + var s = m.Core.State; + s.SetUserStackPointer(0x7800); s.SetInterruptStackPointer(master ? 0x4700u : outer); + s.SetMasterStackPointer(master ? outer : 0x7400u); s.StatusRegister = sr; s.SetActiveStackPointer(outer); + var e = ArchitecturalExpectation.Capture(m); var serial = s.ExceptionSequence; + var isp = s.InterruptStackPointer; var msp = s.MasterStackPointer; + m.Bus.Accesses.Clear(); + var boundary = new Boundary(); + void Step() + { + if (batch) Assert.Equal(1, ((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, boundary)); + else m.Core.ExecuteInstruction(); + } + if (form == "valid") + { + Step(); e.Pc = 0x3000; e.A[7] = inner + 8; + if (master) { e.MasterStackPointer = inner + 8; msp = inner + 8; } else isp = inner + 8; + if (batch) { Assert.Equal(1, boundary.Before); Assert.Equal(1, boundary.After); } + Assert.Null(e.Verify(m)); + // A following instruction proves the restored PC is actually used. + e.D[6] = 43; e.Pc = 0x3002; e.Sr = (ushort)(sr & 0xfff0); Step(); + if (batch) { Assert.Equal(2, boundary.Before); Assert.Equal(2, boundary.After); } + } + else + { + Assert.Throws(Step); + e.Pc = SyntheticMachine.Code + 2; + if (batch) { Assert.Equal(1, boundary.Before); Assert.Equal(0, boundary.After); } + Assert.DoesNotContain(m.Bus.Accesses, a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address >= m.Model.Physical(inner) && a.Address < m.Model.Physical(inner) + 0x110); + } + Assert.Null(e.Verify(m)); Assert.Equal(serial, s.ExceptionSequence); + Assert.Equal(0x7800u, s.UserStackPointer); Assert.Equal(isp, s.InterruptStackPointer); Assert.Equal(msp, s.MasterStackPointer); + Assert.DoesNotContain(m.Bus.Accesses, a => a.Write); + } + private sealed class Boundary : IM68kInstructionBoundary + { + internal int Before, After; + public bool BeforeInstruction() { Before++; return true; } + public void AfterInstruction(long before, long after) => After++; + } +} diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index c489c37f..fc031688 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -47,14 +47,15 @@ it does not replace or discard their remaining gaps. record still lists wider restoration, operand, repair/refault, trace/interrupt and foreign-frame gaps. Preserve those distinctions when reviewing the promoted integer/system families. -3. **Current snapshot coherence is verified.** The frozen `86469f7` graph has +3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; their reports match the full run. The six MOVE mutation baseline reports match full execution, and all 37 CPU inputs match the retained .77 consumer package. Proof: `fea989f4167448e1c35576e4269416a67d54b8b0a8f9f440a9f2b00d5f0368f4`. - Future source changes require renewed linkage. This closes snapshot - coherence, while gates 1 and 2 still prevent milestone completion. + A later test-only C021 metadata fixture has separate focused evidence below; + the full run still qualifies the original 230-input snapshot. Future source + changes require renewed linkage. Gates 1 and 2 still prevent milestone completion. For ordinary STOP, [M68000PM 6-85](https://www.nxp.com/docs/en/reference-manual/M68000PM.pdf) describes the incoming supervisor check followed by SR transfer and stopping. @@ -84,6 +85,15 @@ Publication is a separate release step. PR #22 remains a draft, and `roadmapComplete=false` until a requirement-by-requirement final audit proves completion. +## Later focused validation — 2026-10-08 (private metadata) + +The later private C021 metadata checkpoint adds one opt-in fixture with 10,240 +passing controls and 6,144 exact isolated mutant failure witnesses. It closes +the covered phase/SSW/returned-stack software guard gap; it does not qualify +foreign frames, private C023 production recovery or disputed hardware trace +boundaries. See the latest qualification record for scope and proof identities. +No production CPU input changes. Milestone 6 remains in progress. + ## Earlier frozen validation — 2026-10-08 (c284b94) Current source checkpoint: `c284b948263501183136656307cab2ecce6e0479`. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index fa722bda..cd5cab25 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13089,3 +13089,54 @@ qualified snapshot closes the current evidence-coherence requirement; the failed broad 010 RTE / 060 STOP gate and private-production restoration gaps remain open. Hardware is unavailable for disputed boundaries. Milestone 6 remains **in progress**, `roadmapComplete=false`; PR #22 remains draft. + +### Private C021 metadata guard qualification — 2026-10-08 + +`SyntheticM68020RteMetadataTests` checks the existing interpreter-private C021 +return-frame contract on EC020, 020, 030 and A1200. Scalar and batch routes cover +ISP/MSP, low/high addresses and all 32 initial CCR values. The literal fixtures +include valid return and following-instruction controls, malformed phase, +phase/format combinations, fault addresses, SSW size/function-code/reserved bits, +returned frame and stack-bank selection. Rejection must preserve registers, +stack banks and memory, perform no writes, and avoid reading the inner frame. +These are software-contract checks, not foreign hardware-frame expectations. + +The isolated `C021MetadataDiscoveryV2` baseline passes **10,240 cases** in two +named tests and eight reports: **1,024 valid controls / 9,216 rejection controls**. +Its 231 inputs are exactly the qualified frozen 230-input graph plus this +fixture; all 37 production CPU inputs are unchanged. Independent verification +checks source and loaded assembly identities, exact methods, report keys and +weights. Baseline proof: +`503bce056816d59dd2a9f29caa1b861e433a17f49a8b904f4d278626fa84a4c8`. + +`C021MetadataMutationsV2` removes phase-range, SSW and returned-stack guards in +three separate isolated copies. Every case ID and diagnostic is independently +checked: **1,024 / 3,072 / 2,048** mismatches respectively, **6,144** total. +Unaffected combinations and valid controls remain passing. Returned-frame +mutants are detected by forbidden inner-frame reads even when a later error +would otherwise satisfy the expected rejection. Mutation proof: +`57100cb8564b3845086f9015f576a33aa35a941c4ac9472ccd77c07a2a6f4045`. + +The original V1 evidence is retained. Its mutation verifier rejected malformed +CCR case labels (`{ccr:02X}` in C#); V2 corrects the fixture to `{ccr:X2}` and +reruns baseline and mutants in fresh directories. V1 is not a qualified mutation +proof and has not been relabeled. Local outputs remain beneath +`%TEMP%/copper68k-reference-restoration-20261006/audits/`. + +Run the focused fixture explicitly (an ordinary skipped opt-in is unavailable +coverage): + +```powershell +$env:COPPER68K_RUN_020_RTE_METADATA = '1' +$env:COPPER68K_SYNTHETIC_REPORT_DIR = 'artifacts/c021-metadata' +dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path artifacts/c021-metadata-build --filter 'FullyQualifiedName~SyntheticM68020RteMetadataTests' --logger 'trx;LogFileName=metadata.trx' --results-directory artifacts/c021-metadata +Remove-Item Env:COPPER68K_RUN_020_RTE_METADATA +Remove-Item Env:COPPER68K_SYNTHETIC_REPORT_DIR +``` + +This test-only addition has focused validation; the earlier full/deep and +consumer executions retain their original identities. No production CPU fix, +private C023 import, regression retirement or package publication is made. +Hardware is unavailable for disputed 030/040 trace boundaries. The broad +reference failures and other continuation gaps remain open; milestone 6 stays +**in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 434e9c8a..b5c5cc78 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4101,3 +4101,20 @@ All 230 source/project inputs and 37 CPU inputs remain unchanged. Linkage proof: This closes current snapshot coherence. The failed broad reference gate and private-production restoration requirements remain open; milestone 6 stays **in progress**, `roadmapComplete=false`, and PR #22 remains draft. + +### Milestone 6 checkpoint — private metadata guards, 2026-10-08 + +A test-only C021 fixture adds 10,240 passing private return-frame software +controls across EC020, 020, 030 and A1200, with scalar/batch execution, both +supervisor stack banks, low/high addresses and all CCR values. Valid returns +execute a following-instruction sentinel. Three isolated guard removals produce +6,144 independently checked case IDs and diagnostics. Baseline and mutation +proofs are recorded in the reference qualification log, including the rejected +V1 label-format attempt and fresh corrected V2 execution. + +The new 231-input focused graph is the earlier fully qualified 230-input graph +plus one test fixture; all 37 production CPU inputs remain unchanged. Earlier +full/deep/consumer results are retained under their original identities. This +does not qualify foreign hardware frames or close the broad reference and +restoration gaps. With no 030/040 hardware available, disputed trace boundaries +remain unqualified. Milestone 6 stays **in progress**, `roadmapComplete=false`. From 31b3363218794948833db4e15635042f0bda74ac Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 20:21:52 +0300 Subject: [PATCH 172/211] Maintain strict private RTE metadata qualification and replay --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 8 + docs/COPPER68K_REFERENCE_QUALIFICATION.md | 41 ++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 12 ++ .../prove-copper68k-rte-metadata-integrity.py | 157 +++++++++++++++ scripts/test-copper68k-rte-metadata.py | 183 ++++++++++++++++++ 5 files changed, 401 insertions(+) create mode 100644 scripts/prove-copper68k-rte-metadata-integrity.py create mode 100644 scripts/test-copper68k-rte-metadata.py diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index fc031688..7c39fc2a 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -94,6 +94,14 @@ foreign frames, private C023 production recovery or disputed hardware trace boundaries. See the latest qualification record for scope and proof identities. No production CPU input changes. Milestone 6 remains in progress. +The maintained `test-copper68k-rte-metadata.py` command reproduces the baseline +and all three isolated mutants, then strictly verifies sources, loaded methods, +assemblies, result counters and exact reports. Replay passes. The companion +integrity command rejects 17 corrupted copied validator inputs at their intended +guards and preserves original evidence. Commands and proof identities are in +the latest qualification record. This closes reproducibility for this focused +private software audit, without qualifying the broader unresolved behavior. + ## Earlier frozen validation — 2026-10-08 (c284b94) Current source checkpoint: `c284b948263501183136656307cab2ecce6e0479`. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index cd5cab25..c1a12376 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13140,3 +13140,44 @@ private C023 import, regression retirement or package publication is made. Hardware is unavailable for disputed 030/040 trace boundaries. The broad reference failures and other continuation gaps remain open; milestone 6 stays **in progress**, `roadmapComplete=false`. + +### Maintained private C021 audit command — 2026-10-08 + +The repository now includes an isolated baseline/mutation command and strict +replay. It snapshots tracked CPU/test sources, clears inherited audit settings, +builds into fresh outputs and executes exactly the two named metadata tests. +Each of three isolated mutants changes only its specific guard. Verification +checks the current source graph and producer identity, exact command/settings, +execution outputs, both CPU assembly copies, all result counters, loaded test +methods/assembly paths, every combination weight and ordered failure diagnostic. +Missing fixtures, missing/empty selections, unexecuted rows and mismatches cannot +be accepted as baseline success. Existing output directories are rejected. + +```powershell +python scripts/test-copper68k-rte-metadata.py --output artifacts/c021-audit +python scripts/test-copper68k-rte-metadata.py --output artifacts/c021-audit --replay +python scripts/prove-copper68k-rte-metadata-integrity.py --audit artifacts/c021-audit --output artifacts/c021-integrity +``` + +The completed `C021MaintainedCommandV2` execution passes **10,240 baseline cases** +and detects **6,144** exact mutant witnesses. Strict replay passes. Independent +linkage verifies all 231 inputs and every report against the earlier separately +qualified baseline/mutants, plus the named methods and binary identities. +Command proof: `9efd2808a8eb5759b3b8738c7d2635fa0c216043e212519ee08421ba9b76d7a3`. +Independent linkage: `9d4701f7f15d9a4caaa53a6a00a51f6c76acd6efcdf0fb237aaa5803b56ca1ab`. +The earlier maintained-driver V1 run is retained under its original producer +identity; V2 adds explicit TRX counter and missing-method checks in a fresh run. + +`C021MaintainedIntegrityV1` checks 17 single-corruption controls. Each relocated +validator fixture passes a positive structural check before its deliberate +corruption; these copied inputs are not claimed as new CPU executions. All +controls reject at their exact intended guard, including coherent source/output +manifest tampering, empty selection/coverage, missing fixtures/assemblies, +loaded-method identity, counters/outcomes, failure IDs and diagnostics. The +original execution files remain unchanged. Integrity proof: +`e954c840409307783e3258b9a3d487a98b88cdcd13f0c6b21c1508e1c92e3979`. + +This adds maintained audit tooling, without changing the 231-input focused +CPU/test graph or the 37 production CPU inputs. Full-suite, deep-reference, +consumer and hardware qualification keep their earlier scope and identities. +Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index b5c5cc78..5e00bb1c 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4118,3 +4118,15 @@ full/deep/consumer results are retained under their original identities. This does not qualify foreign hardware frames or close the broad reference and restoration gaps. With no 030/040 hardware available, disputed trace boundaries remain unqualified. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +### Milestone 6 checkpoint — maintained private audit, 2026-10-08 + +The maintained private C021 command snapshots and builds isolated sources, +reproduces 10,240 passing baseline cases and 6,144 exact mutation witnesses, +and supports strict replay. Seventeen corruption controls reject missing/empty +or coherently altered validator inputs at their intended guards, with positive +relocation controls and unchanged original evidence. Independent comparison +matches all focused source graphs and reports to the earlier qualified runs. +No CPU/test input changes accompany the tooling. The qualification record gives +commands, scope and proof identities. Milestone 6 remains **in progress**; +failed broad references, private recovery and hardware questions remain open. diff --git a/scripts/prove-copper68k-rte-metadata-integrity.py b/scripts/prove-copper68k-rte-metadata-integrity.py new file mode 100644 index 00000000..7aa6ee2f --- /dev/null +++ b/scripts/prove-copper68k-rte-metadata-integrity.py @@ -0,0 +1,157 @@ +"""Corrupt copied validator inputs; original execution artifacts stay immutable.""" +import argparse +import importlib.util +from pathlib import Path +import shutil +import xml.etree.ElementTree as ET + +SPEC = importlib.util.spec_from_file_location("metadata_audit", Path(__file__).with_name("test-copper68k-rte-metadata.py")) +A = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(A) +CONTROLS = { + "MissingFixture": "Missing source fixture", + "ChangedSourceAndManifest": "Wrong source, command or producer identity", + "WrongProducer": "Wrong source, command or producer identity", + "WrongFilter": "Wrong source, command or producer identity", + "MissingAssembly": "Missing execution outputs", + "ChangedCpuAndManifest": "Executed CPU copies disagree", + "EmptySelection": "Missing, empty or substituted test selection", + "WrongCounters": "Wrong execution counters", + "MissingDefinition": "Missing, empty or substituted test selection", + "MissingMethod": "Missing loaded test method", + "WrongMethod": "Wrong loaded method", + "WrongMethodAssembly": "Wrong loaded test assembly", + "WrongOutcome": "Wrong named outcome", + "MissingReport": "Missing coverage report", + "EmptyCoverage": "Missing, empty or incorrect coverage combinations", + "WrongFailureId": "Wrong ordered failure identifiers", + "WrongDiagnostic": "Wrong rejection witness", +} + + +def reseal(run): + execution = A.load(run / "execution.json") + execution["inputsSha256"] = A.sha(run / "inputs.json") + execution["outputs"] = {n: A.sha(run / n) for n in A.OUTPUTS if (run / n).is_file()} + A.save(run / "execution.json", execution) + + +def copied_inputs(parent, target): + # These are relocated validator fixtures, never claimed as new CPU runs. + # The positive check establishes that the later single corruption reaches + # its intended guard instead of failing on a relocated path/hash first. + for variant in A.VARIANTS: + src, dst = parent / variant, target / variant + inputs = A.load(src / "inputs.json") + for name in inputs["sources"]: + path = dst / "source" / name + path.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(src / "source" / name, path) + for name in [*A.OUTPUTS, "execution.json", *[f.name for f in src.glob("*-rte-private-metadata-*.json")]]: + path = dst / name + path.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(src / name, path) + inputs["command"], inputs["settings"] = A.command(dst), A.settings(dst) + A.save(dst / "inputs.json", inputs) + tree = ET.parse(dst / "metadata.trx") + for definition in tree.findall(".//t:UnitTest", A.NS): + definition.set("storage", str((dst / A.OUTPUTS[3]).resolve())) + definition.find("t:TestMethod", A.NS).set("codeBase", str((dst / A.OUTPUTS[3]).resolve())) + tree.write(dst / "metadata.trx", encoding="utf-8", xml_declaration=True) + reseal(dst) + + +def corrupt(target, name): + run = target / ("PhaseRange" if name in ["WrongFailureId", "WrongDiagnostic"] else "Baseline") + inputs = A.load(run / "inputs.json") + if name == "MissingFixture": + path = run / "source" / A.FIXTURE + A.check(path.resolve().is_relative_to(target.resolve()), "Unsafe control path") + path.unlink() + elif name == "ChangedSourceAndManifest": + path = run / "source" / A.TARGET + path.write_bytes(path.read_bytes() + b"\n// unrelated source change\n") + inputs["sources"][A.TARGET] = A.sha(path) + elif name == "WrongProducer": + inputs["producerSha256"] = "0" * 64 + elif name == "WrongFilter": + inputs["command"][inputs["command"].index("--filter") + 1] = "FullyQualifiedName~Nothing" + elif name == "MissingAssembly": + path = run / A.OUTPUTS[3] + A.check(path.resolve().is_relative_to(target.resolve()), "Unsafe control path") + path.unlink() + elif name == "ChangedCpuAndManifest": + path = run / A.OUTPUTS[2] + path.write_bytes(path.read_bytes() + b"intentional integrity corruption") + elif name in ["MissingReport", "EmptyCoverage", "WrongFailureId", "WrongDiagnostic"]: + path = run / "68020-rte-private-metadata-scalar.json" + if name == "MissingReport": + A.check(path.resolve().is_relative_to(target.resolve()), "Unsafe control path") + path.unlink() + else: + report = A.load(path) + if name == "EmptyCoverage": + report["logicalCases"] = 0 + report["combinations"] = {} + elif name == "WrongFailureId": + report["failures"][0]["id"] += "/unrelated-case" + else: + report["failures"][0]["reason"] = "unrelated failure" + A.save(path, report) + else: + tree = ET.parse(run / "metadata.trx") + definitions = tree.find("t:TestDefinitions", A.NS) + results = tree.find("t:Results", A.NS) + method = definitions[0].find("t:TestMethod", A.NS) + if name == "EmptySelection": + for row in list(results): + results.remove(row) + elif name == "WrongCounters": + tree.find("t:ResultSummary/t:Counters", A.NS).set("executed", "0") + elif name == "MissingDefinition": + definitions.remove(definitions[0]) + elif name == "MissingMethod": + definitions[0].remove(method) + elif name == "WrongMethod": + method.set("name", "CompletelyDifferentInstruction") + elif name == "WrongMethodAssembly": + method.set("codeBase", str(target / "unrelated.dll")) + elif name == "WrongOutcome": + results[0].set("outcome", "NotExecuted") + else: + raise ValueError("Unknown corruption control") + tree.write(run / "metadata.trx", encoding="utf-8", xml_declaration=True) + A.save(run / "inputs.json", inputs) + reseal(run) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--audit", required=True, type=Path) + parser.add_argument("--output", required=True, type=Path) + args = parser.parse_args() + parent, output = args.audit.resolve(), args.output.resolve() + A.check(not output.exists(), "Controls output already exists; choose a fresh directory") + original = A.source_bytes() + A.check(A.load(parent / "proof.json") == A.verify(parent, original), "Original audit failed strict replay") + protected = {str(f.relative_to(parent)): A.sha(f) for f in parent.rglob("*") if f.is_file() and not {"obj"}.intersection(f.relative_to(parent).parts)} + records = [] + for name, reason in CONTROLS.items(): + target = output / name + copied_inputs(parent, target) + positive = A.verify(target, original) + corrupt(target, name) + try: + A.verify(target, original) + except ValueError as error: + A.check(str(error) == reason, "Wrong corruption rejection: " + name + ": " + str(error)) + else: + raise ValueError("Corrupted validator inputs accepted: " + name) + records.append(dict(name=name, expectedReason=reason, positiveRelocationPassed=True, positiveRecord=positive)) + print(name + ": rejected at intended guard", flush=True) + A.check(all(A.sha(parent / n) == h for n, h in protected.items()), "Original evidence changed") + A.save(output / "proof.json", dict(schema=1, controls=records, originalProofSha256=A.sha(parent / "proof.json"), originalFiles=protected, originalUnchanged=True, validatorSha256=A.sha(Path(A.__file__)), producerSha256=A.sha(Path(__file__)), scope="copied validator-input corruption controls; no new CPU or hardware execution", roadmapComplete=False)) + + +if __name__ == "__main__": + main() diff --git a/scripts/test-copper68k-rte-metadata.py b/scripts/test-copper68k-rte-metadata.py new file mode 100644 index 00000000..f9517dbe --- /dev/null +++ b/scripts/test-copper68k-rte-metadata.py @@ -0,0 +1,183 @@ +"""Isolated private C021 baseline/mutation audit; no hardware qualification.""" +import argparse +import hashlib +import itertools +import json +import os +from pathlib import Path +import re +import subprocess +import xml.etree.ElementTree as ET + +REPO = Path(__file__).resolve().parent.parent +FIXTURE = "Copper68k.Tests/Synthetic/SyntheticM68020RteMetadataTests.cs" +TARGET = "Copper68k/M68kAdvancedTimingInterpreter.Rte020.cs" +MODELS = ["68EC020", "68020", "68030", "A1200"] +FORMS = ["valid", "phase-range", "phase-version", "phase-end", "fault-address", "ssw-size", "ssw-fc", "ssw-extra", "returned-frame", "returned-bank"] +MUTATIONS = { + "PhaseRange": ("context.Phase is < 0 or > 4 || ", ["phase-range"], 1024), + "SswGuard": (" || (ssw & ~0x100) != expectedSsw", ["ssw-size", "ssw-fc", "ssw-extra"], 3072), + "ReturnedStack": (" if (returnedStack != context.Frame)\n throw new UnsupportedM68kTimingException(0x4e73, State.LastInstructionProgramCounter, _profile);", ["returned-frame", "returned-bank"], 2048), +} +VARIANTS = ["Baseline", *MUTATIONS] +OUTPUTS = ["execution.log", "metadata.trx", "build/bin/Copper68k/release/Copper68k.dll", "build/bin/Copper68k.Tests/release/Copper68k.Tests.dll", "build/bin/Copper68k.Tests/release/Copper68k.dll"] +NS = {"t": "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"} + + +def check(ok, reason): + if not ok: + raise ValueError(reason) + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def load(path): + return json.loads(path.read_text(encoding="utf-8-sig")) + + +def save(path, value): + path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8", newline="\n") + + +def source_bytes(): + names = subprocess.check_output(["git", "ls-files", "Copper68k", "Copper68k.Tests"], cwd=REPO, text=True).splitlines() + selected = {n: (REPO / n).read_bytes() for n in sorted(set(names + [FIXTURE])) if Path(n).suffix in [".cs", ".csproj"]} + check(bool(selected) and FIXTURE in selected and TARGET in selected, "Missing required source fixtures") + check(b"{ccr:X2}" in selected[FIXTURE], "Missing canonical CCR case identifiers") + return selected + + +def variant_sources(original, variant): + result = dict(original) + if variant != "Baseline": + text = result[TARGET].decode("utf-8-sig").replace("\r\n", "\n") + anchor = MUTATIONS[variant][0] + check(text.count(anchor) == 1, "Missing or ambiguous " + variant + " mutation target") + result[TARGET] = text.replace(anchor, "").encode() + return result + + +def command(run): + return ["dotnet", "test", str(run / "source/Copper68k.Tests/Copper68k.Tests.csproj"), "-c", "Release", "--artifacts-path", str(run / "build"), "--filter", "FullyQualifiedName~SyntheticM68020RteMetadataTests", "--logger", "trx;LogFileName=metadata.trx", "--results-directory", str(run)] + + +def settings(run): + return {"COPPER68K_RUN_020_RTE_METADATA": "1", "COPPER68K_SYNTHETIC_REPORT_DIR": str(run)} + + +def execute(root, original): + check(not root.exists(), "Audit output already exists; choose a fresh directory") + for variant in VARIANTS: + run = root / variant + sources = variant_sources(original, variant) + for name, data in sources.items(): + target = run / "source" / name + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) + inputs = dict(schema=1, variant=variant, sources={n: hashlib.sha256(b).hexdigest() for n, b in sources.items()}, producerSha256=sha(Path(__file__)), command=command(run), settings=settings(run)) + save(run / "inputs.json", inputs) + environment = {k: v for k, v in os.environ.items() if not k.startswith("COPPER68K_")} + environment.update(settings(run)) + print(variant + ": executing isolated private metadata controls", flush=True) + with (run / "execution.log").open("w", encoding="utf-8") as log: + done = subprocess.run(command(run), cwd=REPO, env=environment, stdout=log, stderr=subprocess.STDOUT) + check(all((run / name).is_file() for name in OUTPUTS), "Missing execution outputs") + save(run / "execution.json", dict(exit=done.returncode, inputsSha256=sha(run / "inputs.json"), outputs={n: sha(run / n) for n in OUTPUTS})) + check(done.returncode == (0 if variant == "Baseline" else 1), "Unexpected execution status: " + variant) + + +def verify_reason(failure, model): + reason = failure["reason"].replace("\r\n", "\n") + if "/returned-frame/" not in failure["id"]: + check(reason == "Assert.Throws() Failure: No exception was thrown\nExpected: typeof(Copper68k.UnsupportedM68kTimingException)", "Wrong rejection witness") + return + inner = 0x12346000 if "/high=True/" in failure["id"] else 0x6000 + def physical(address): + return address & 0xffffff if model in ["68EC020", "A1200"] else address + accesses = re.findall(r"BusAccess \{ Address = (\d+), Width = (\d+), Write = (False|True), Value = (\d+), Kind = (\w+) \}", reason) + actual = [(int(a), int(w), write, int(v), k) for a, w, write, v, k in accesses] + expected = [(physical(at), width, "False", value, "CpuDataRead") for at, width, value in [(inner-4, 2, 0), (inner-2, 2, 0), (inner+0x102, 4, 0xa5a5a5a5), (inner+0x106, 2, 0xa5a5), (inner+0x11e, 2, 0)]] + check(reason.startswith("Assert.DoesNotContain() Failure: Filter matched in collection\n") and actual == expected, "Wrong forbidden-inner-read witness") + + +def verify(root, original): + records = [] + for variant in VARIANTS: + run = root / variant + sources = {n: hashlib.sha256(b).hexdigest() for n, b in variant_sources(original, variant).items()} + inputs = load(run / "inputs.json") + check(inputs == dict(schema=1, variant=variant, sources=sources, producerSha256=sha(Path(__file__)), command=command(run), settings=settings(run)), "Wrong source, command or producer identity") + check(all((run / "source" / n).is_file() for n in sources), "Missing source fixture") + check(all(sha(run / "source" / n) == h for n, h in sources.items()), "Changed source snapshot") + execution = load(run / "execution.json") + check(all((run / name).is_file() for name in OUTPUTS), "Missing execution outputs") + check(execution == dict(exit=0 if variant == "Baseline" else 1, inputsSha256=sha(run / "inputs.json"), outputs={n: sha(run / n) for n in OUTPUTS}), "Wrong execution or output identity") + check(execution["outputs"][OUTPUTS[2]] == execution["outputs"][OUTPUTS[4]], "Executed CPU copies disagree") + tree = ET.parse(run / "metadata.trx") + counters = tree.find(".//t:ResultSummary/t:Counters", NS) + expected_counters = {n: "0" for n in ["total", "executed", "passed", "failed", "error", "timeout", "aborted", "inconclusive", "passedButRunAborted", "notRunnable", "notExecuted", "disconnected", "warning", "completed", "inProgress", "pending"]} + expected_counters.update(total="2", executed="2", passed="2" if variant == "Baseline" else "0", failed="0" if variant == "Baseline" else "2") + check(counters is not None and counters.attrib == expected_counters, "Wrong execution counters") + summary = tree.find(".//t:ResultSummary", NS) + check(summary.get("outcome") == ("Completed" if variant == "Baseline" else "Failed"), "Wrong summary outcome") + rows, definitions = tree.findall(".//t:UnitTestResult", NS), tree.findall(".//t:UnitTest", NS) + names = {f"Copper68k.Tests.Synthetic.SyntheticM68020RteMetadataTests.{route}PrivateMetadataControls" for route in ["Scalar", "Batch"]} + check(len(rows) == len(definitions) == 2 and {r.get("testName") for r in rows} == names, "Missing, empty or substituted test selection") + lookup = {d.get("id"): d for d in definitions} + check(len(lookup) == 2, "Duplicate test definitions") + for row in rows: + check(row.get("testId") in lookup, "Missing matching test definition") + definition = lookup[row.get("testId")] + method = definition.find("t:TestMethod", NS) + check(method is not None, "Missing loaded test method") + check(row.get("outcome") == ("Passed" if variant == "Baseline" else "Failed"), "Wrong named outcome") + check(definition.get("name") == row.get("testName") == method.get("className") + "." + method.get("name"), "Wrong loaded method") + check(Path(definition.get("storage")).resolve() == Path(method.get("codeBase")).resolve() == (run / OUTPUTS[3]).resolve(), "Wrong loaded test assembly") + bad = [] if variant == "Baseline" else MUTATIONS[variant][1] + reports, mismatches = {}, 0 + for model, route in itertools.product(MODELS, ["scalar", "batch"]): + name = f"{model}-rte-private-metadata-{route}.json" + check((run / name).is_file(), "Missing coverage report") + report = load(run / name) + combinations, identifiers = {}, [] + for master, high, form in itertools.product([False, True], [False, True], FORMS): + key = f"{model}/RTE/private-C021/{form}/master={master}/high={high}" + combinations[key] = {"mismatching" if form in bad else "passing": 32} + if form in bad: + identifiers += [f"{key}/op=4E73/ccr={ccr:02X}" for ccr in range(32)] + count = len(identifiers) + expected = dict(schema=1, model=model, group=f"rte-private-metadata-{route}", counts=dict(passing=1280-count, mismatching=count, unsupported=0, untested=0), logicalCases=1280, xunitBatches=1, combinations=combinations) + check(set(report) == set(expected) | {"failures"} and all(report[k] == v for k, v in expected.items()), "Missing, empty or incorrect coverage combinations") + check([f["id"] for f in report["failures"]] == identifiers, "Wrong ordered failure identifiers") + for failure in report["failures"]: + check(set(failure) == {"id", "status", "reason"} and failure["status"] == "mismatching", "Wrong failure schema") + verify_reason(failure, model) + reports[name] = sha(run / name) + mismatches += count + check({f.name for f in run.glob("*-rte-private-metadata-*.json")} == set(reports), "Unexpected coverage reports") + check(mismatches == (0 if variant == "Baseline" else MUTATIONS[variant][2]), "Wrong mutation witness count") + records.append(dict(variant=variant, inputsSha256=sha(run / "inputs.json"), executionSha256=sha(run / "execution.json"), reports=reports, mismatches=mismatches)) + return dict(schema=1, records=records, baselineCases=10240, exactMutationWitnesses=6144, scope="private C021 software contract; no foreign-frame hardware qualification", productionCpuChanged=False, publication=False, roadmapComplete=False) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument("--replay", action="store_true", help="Strictly verify retained execution without running tests") + args = parser.parse_args() + root = args.output.resolve() + original = source_bytes() + if not args.replay: + execute(root, original) + proof = verify(root, original) + if args.replay: + check(load(root / "proof.json") == proof, "Changed proof record") + else: + save(root / "proof.json", proof) + print("Verified 10,240 private controls and 6,144 exact mutant witnesses", flush=True) + + +if __name__ == "__main__": + main() From 45e732accafd8f19ac41a4662151b7257f6c8db8 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 20:40:27 +0300 Subject: [PATCH 173/211] Record combined private candidate qualification and full replay start --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 5 ++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 51 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 11 ++++ 3 files changed, 67 insertions(+) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 7c39fc2a..b7a1e13a 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -47,6 +47,11 @@ it does not replace or discard their remaining gaps. record still lists wider restoration, operand, repair/refault, trace/interrupt and foreign-frame gaps. Preserve those distinctions when reviewing the promoted integer/system families. + The latest isolated 258-input candidate preserves the current production + MOVE16 correction. Its 38-test / 1,641,472-case preflight is independently + qualified, and exact discovery proves 8,124 complete-suite names. That full + execution has started; completed results and full verification remain pending. + This is candidate evidence, not production import or hardware qualification. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index c1a12376..b9fdf29b 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13181,3 +13181,54 @@ This adds maintained audit tooling, without changing the 231-input focused CPU/test graph or the 37 production CPU inputs. Full-suite, deep-reference, consumer and hardware qualification keep their earlier scope and identities. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +### Current tests and MOVE16 correction on the private candidate — 2026-10-08 + +Source inspection finds that the latest qualified private 39-input CPU graph +predates two current production MOVE16 files. A wholesale copy would lose the +production correction. The new isolated `CurrentPrivateRebaseV3` graph contains +all **194 current test/project inputs**, the **25 retained private-only fixtures** +and **39 CPU inputs**, **258 inputs** total. The two current production files, +`M68040Support.cs` and `M68kAdvancedTimingInterpreter.System.cs`, are preserved +byte-for-byte. Other private CPU inputs retain their original candidate identity. +No private code is imported into the public source tree. + +Independent source verification reconstructs both normalized three-way merges +and proves their results equal the current production files. The first raw-byte +merge attempt produced line-ending conflicts; V2 normalized the merge inputs, +and V3 retains the exact current bytes after confirming semantic equivalence. +Those preliminary graphs are retained separately. Source-only proof: +`2c5326714600deece6bcda04be958a403237627b07677a26b15c05b20f5feded`. + +The corrected `preflight-v2` runs **38 named tests**, all passing, with +**1,641,472 logical cases / 1,628 reports**, zero mismatching, unsupported or +untested cases in this selection. It includes actual private MOVE entry/refault +checks, current MOVE16 faults/reserved encodings, MOVES faults/completion and +C021 metadata guards. Every previously qualified selected report is identical; +new literal MOVES fault fixtures also pass. Independent verification checks the +sources, loaded test methods and assembly paths, exact named outcomes, memory +coverage report keys/weights and prior-report equality. Preflight proof: +`c017d73a13bd91ee62b2e5fda1bdc65de7b03f1bb31e081a2c9a7ec24d6215af`. + +The first preflight's expected-roster recorder omitted eight MOVES names selected +by its prefix filter; it is preliminary evidence. The fresh corrected run +explicitly enables both optional MOVES discovery tests. A verifier attempt also +incorrectly equated a parameterized display name with the unparameterized method +name; corrected verification retains exact display names and separately checks +actual method identity. Neither acceptance criteria nor reports were weakened. + +Independent compiled discovery finds exactly **8,124 named test rows**. The +33 deferred opcode-matrix rows are obtained from the compiled fixture without +executing instructions; expansion matches the retained 2,714 private-only cases +plus the 5,410 current cases. Discovery proof: +`c6ffecec912aa6d3907c14040054f4a6fb4ebc02c1bc2e84a6758d8a1da02b12`. + +The complete suite has **started** on these same frozen assemblies, with all +8,124 expected names, ten qualified WinUAE presets and scoped private settings. +Expected outcomes are 8,087 passes and 37 explicitly unavailable rows, derived +before execution from retained rosters and the qualified preflight. These are +expectations, not completed results. The producer and test process were confirmed +live; completion and independent full verification remain pending. No candidate +full-suite, consumer, hardware or production-import qualification is claimed by +this start. Public CPU sources and package dependencies are unchanged. Milestone +6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 5e00bb1c..3f3721f0 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4130,3 +4130,14 @@ matches all focused source graphs and reports to the earlier qualified runs. No CPU/test input changes accompany the tooling. The qualification record gives commands, scope and proof identities. Milestone 6 remains **in progress**; failed broad references, private recovery and hardware questions remain open. + +### Milestone 6 checkpoint — combined candidate validation started, 2026-10-08 + +The isolated private candidate now preserves the current production MOVE16 +correction and all current test inputs. Independent source reconstruction and a +38-test preflight pass: 1,641,472 cases / 1,628 reports. Compiled discovery proves +8,124 named cases, including deferred theory rows. The complete suite has started +on the same frozen assemblies with ten qualified reference presets. Completion, +independent full verification and affected-consumer qualification remain pending; +no private CPU code is imported. See the qualification record for identities, +preliminary attempts and scope. Milestone 6 stays **in progress**. From 4ca734b26f1f7a2c824afd9daf7fa43f4f02edb5 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 21:09:24 +0300 Subject: [PATCH 174/211] Qualify combined candidate consumers and same-build references --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 5 ++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 59 ++++++++++++++++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 +++++ 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index b7a1e13a..78cbe426 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -52,6 +52,11 @@ it does not replace or discard their remaining gaps. qualified, and exact discovery proves 8,124 complete-suite names. That full execution has started; completed results and full verification remain pending. This is candidate evidence, not production import or hardware qualification. + The same candidate's local .78 consumer checks and same-build seeded, + SingleStepTests/Musashi and eight rejection controls are independently + verified. Source/package linkage is complete for these scopes. The full + expected catalog has 13,737 reports / 110,928,842 cases; actual complete-suite + execution and verification remain pending. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index b9fdf29b..bf8349c6 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13206,8 +13206,8 @@ untested cases in this selection. It includes actual private MOVE entry/refault checks, current MOVE16 faults/reserved encodings, MOVES faults/completion and C021 metadata guards. Every previously qualified selected report is identical; new literal MOVES fault fixtures also pass. Independent verification checks the -sources, loaded test methods and assembly paths, exact named outcomes, memory -coverage report keys/weights and prior-report equality. Preflight proof: +sources, loaded test methods and assembly paths, exact named outcomes, coverage +report keys/weights and prior-report equality. Preflight proof: `c017d73a13bd91ee62b2e5fda1bdc65de7b03f1bb31e081a2c9a7ec24d6215af`. The first preflight's expected-roster recorder omitted eight MOVES names selected @@ -13232,3 +13232,58 @@ live; completion and independent full verification remain pending. No candidate full-suite, consumer, hardware or production-import qualification is claimed by this start. Public CPU sources and package dependencies are unchanged. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +### Combined candidate consumers and deep references — 2026-10-08 + +The isolated 258-input / 39-CPU-input candidate now has affected-consumer and +same-build deep/reference evidence. This checkpoint does not finish the complete +CPU replay, which is still running, or import private CPU code into production. + +A fresh local-only `1.5.2-synthetic-dev.78` package snapshots the 39 CPU/project +inputs plus README/icon assets, 41 inputs total. CopperScreen uses the same clean +`aa1dad5` archive as the retained .77 checks, with a new private package cache and +an explicit NuGet version override. The production build passes with zero +warnings/errors; **171 host passes / six unavailable**, **74 disk passes**, +**1,080 engine passes** and **two Workbench 3.1 floppy replays** pass. Independent +verification checks the exact retained test rosters, archive/source identities, +NuGet-only dependency boundary, private cache, package and all three consumer CPU +copies, commands/results and native input hashes. The live checkout is untouched. +Consumer proof: `b4cf981621487404e583d20b79cdab6532b3307455ffa7f021c8284ae20dcb10`. +This is local package validation, without publication, HD boot or throughput claims. + +The same frozen test/CPU assemblies pass three named deep/reference executions: +**320,000 seeded cases / 32 reports**, **312,500 SingleStepTests cases / 125 files** +and **536 Musashi programs / 88 explicit exclusions** across all eight profiles. +Seed 68020 and 10,000 samples per family/profile, pinned complete input sets, +loaded methods/assembly paths, exact commands/settings, report weights/statuses, +per-file/program outcomes and source/binary identities are independently checked. +Deep proof: `faf0f671a72e153dd3f2fce9a1bca8fe5addb7e41a4fce2a2acb64fd057a4699`. +SingleStepTests qualifies the 000 corpus; Musashi programs remain self-checking +software fixtures. Neither result is physical timing or complete hardware coverage. + +Eight actual invalid audit requests reject on these same assemblies: zero seed, +zero samples, empty model selection, missing/empty SingleStepTests input, missing +Musashi root and adjacent/separated duplicate model IDs. Each executes exactly +one named failing test with the intended diagnostic, no coverage report and no +CPU execution. Original inputs and binaries remain unchanged. Independent proof: +`1d3c23788c0cbfae1f7ec4224216c660ed007d0343365e600a46a1d99220560a`. + +Cross-evidence linkage binds the complete 258-input graph and three loaded +assemblies for preflight, discovery, deep references and controls to the exact +39 CPU inputs in the local consumer package. Linkage proof: +`b3bb40f371313364fc60747d97756f00f40647ba6959d548017c5e3e8d7d4d39`. +The earlier V1 linkage remains separate; V2 explicitly binds the controls' +producer input record rather than using an optional-key fallback. + +Before full completion, the expected coverage catalog is independently assembled +from retained qualified runs: **13,737 reports / 110,928,842 logical cases**. +Overlapping reference reports are identical; no conflicting report is silently +replaced. Catalog proof: +`c795fab6ee2591db10d04c69f245e48ed320be9159f91ff7b1fe3f09aaa640ca`. +These are expected full results, not completed execution. The full verifier is +prepared to check all 8,124 named outcomes, loaded definitions/methods, every +report/key/weight, ten reference presets and immutable sources/inputs/binaries. + +The broad Basic mismatches, private recovery limits and disputed hardware +boundaries remain open. Full replay/verification is pending; milestone 6 stays +**in progress**, `roadmapComplete=false`, and PR #22 stays draft. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 3f3721f0..e9c45a15 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4141,3 +4141,18 @@ on the same frozen assemblies with ten qualified reference presets. Completion, independent full verification and affected-consumer qualification remain pending; no private CPU code is imported. See the qualification record for identities, preliminary attempts and scope. Milestone 6 stays **in progress**. + +### Milestone 6 checkpoint — combined candidate consumers and references, 2026-10-08 + +The local-only .78 package preserves the NuGet consumer boundary and passes the +clean CopperScreen build, 171 host tests / six unavailable, 74 disk tests, +1,080 engine tests and two native floppy replays. Same-build deep audits pass +320,000 seeded cases, 312,500 SingleStepTests cases and 536 Musashi programs / +88 exclusions. Eight actual invalid requests reject before CPU execution. +Independent linkage binds the 258-input graph, loaded assemblies and 39 packaged +CPU inputs. Proofs and reference caveats are in the qualification record. + +The full expected report catalog is frozen at 13,737 reports / 110,928,842 cases; +full execution remains running and its independent verification is pending. +No publication or private production import occurs. Milestone 6 remains +**in progress**, with broad reference, restoration and hardware gaps explicit. From df2a9eca2d12b361726ea0e86e3d8d7cb8105881 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 21:23:27 +0300 Subject: [PATCH 175/211] Record combined candidate API comparison --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 32 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 10 ++++++ 2 files changed, 42 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index bf8349c6..d6a4b246 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13287,3 +13287,35 @@ report/key/weight, ten reference presets and immutable sources/inputs/binaries. The broad Basic mismatches, private recovery limits and disputed hardware boundaries remain open. Full replay/verification is pending; milestone 6 stays **in progress**, `roadmapComplete=false`, and PR #22 stays draft. + +### Combined candidate API and source review — 2026-10-08 + +Separate reflection processes compare the compiled production assembly from the +completed 230-input full run with the frozen combined candidate assembly used by +the pending full run. Both expose **20 types / 211 public or protected records**, +with **zero added or removed records**. The comparison includes type bases and +interfaces, generic constraints, member signatures and accessibilities, constant +values, optional parameter defaults and accessor visibility. No constructors or +CPU instructions execute. Independent verification binds both DLL identities, +their dependency directories, helper sources/output, and the candidate's full-run +input manifest. This qualifies managed signatures; it does not establish physical +layout, serialization compatibility, runtime behavior or hardware correctness. + +A normalized source patch removes line-ending noise and identifies six semantic +CPU files: `Access020`, `FinalWrite020`, `Move`, `Operand020`, `Rte020` and the +main advanced interpreter. The new files and private C022/C023 dispatch, selected +operand/final-write continuation and existing mapped execution paths are visible +for review. The current MOVE16 correction remains unchanged. The patch is local +review evidence, not an import into production or a package release. + +Evidence under the retained audit root: + +| Record | SHA-256 | +| --- | --- | +| `api-review-v1/proof.json` | `cd1acd76f9e0f49447def8214fd5cf6d72bb52e72e6e5797d3a0d24e474ae7fc` | +| `api-review-v1/independent-proof.json` | `7b199ab22f626e240a0bad63028ec3222fa8950e06e2649ae4b145e3d3ca57f5` | +| `api-review-v1/semantic-source.patch` | `f596f8b6d10d046b035e315cde444012af94189c9859980c70ec3969d4540c2b` | + +The full suite is still running. Hardware is unavailable for the disputed 030/040 +boundaries, which remain explicitly unqualified. No broad reference failure is +reclassified. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index e9c45a15..67d30cd7 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4156,3 +4156,13 @@ The full expected report catalog is frozen at 13,737 reports / 110,928,842 cases full execution remains running and its independent verification is pending. No publication or private production import occurs. Milestone 6 remains **in progress**, with broad reference, restoration and hardware gaps explicit. + +### Milestone 6 checkpoint — combined candidate API review, 2026-10-08 + +Separate-process compiled API comparison and independent evidence verification +find zero managed public/protected signature changes: 20 types / 211 records in +both production and the combined candidate. A normalized six-file CPU source +patch is prepared for review without production import. The qualification record +states the exact scope and proof identities. Full-suite execution remains running; +unavailable hardware leaves disputed 030/040 boundaries unqualified. Milestone 6 +stays **in progress**, `roadmapComplete=false`. From f638541752c6814c24db3eab037bf817ffd35049 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 21:42:20 +0300 Subject: [PATCH 176/211] Qualify private predecrement MOVE write recovery --- .gitattributes | 2 + .../SyntheticM68020PredecrementWriteTests.cs | 141 ++++++++++++++++++ docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 4 + docs/COPPER68K_REFERENCE_QUALIFICATION.md | 66 ++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 13 ++ .../m68020-predecrement-write-candidate.patch | 52 +++++++ 6 files changed, 278 insertions(+) create mode 100644 .gitattributes create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020PredecrementWriteTests.cs create mode 100644 scripts/reference/m68020-predecrement-write-candidate.patch diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..faa3becf --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Unified patch context needs its single-space blank lines; keep evidence bytes stable. +scripts/reference/m68020-predecrement-write-candidate.patch text eol=lf whitespace=-blank-at-eol diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020PredecrementWriteTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020PredecrementWriteTests.cs new file mode 100644 index 00000000..c6931040 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020PredecrementWriteTests.cs @@ -0,0 +1,141 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Selected predecrement-source final-write software contract. A denied logical +// destination must retain the accepted decrement/source value across explicit RTE. +// C023 is a private emulator format, not a silicon internal-state claim. +public sealed class SyntheticM68020PredecrementWriteTests(ITestOutputHelper output) +{ + private static readonly ModelSpec[] Models = ModelSpec.All.Where(x => x.Id is "68EC020" or "68020" or "68030" or "A1200").ToArray(); + [Theory, Trait("Suite", "ReferenceDiscovery")] + [InlineData(false)] [InlineData(true)] + public void DirectFinalWriteControls(bool batch) => Audit(batch, false); + [EnvironmentFact("COPPER68K_RUN_020_PREDECREMENT_WRITE", "require actual final-write fault recovery"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarFinalWriteFaults() => Audit(false, true); + [EnvironmentFact("COPPER68K_RUN_020_PREDECREMENT_WRITE", "require actual final-write fault recovery"), Trait("Suite", "ReferenceDiscovery")] + public void BatchFinalWriteFaults() => Audit(true, true); + + private void Audit(bool batch, bool fault) + { + var failures = new List(); + foreach (var model in Models) + { + var bus = new FaultBus(); var m = new SyntheticMachine(model, bus); + var report = new CoverageBatch(model.Id, $"move-predecrement-write-{(fault ? "fault" : "controls")}-{(batch ? "batch" : "scalar")}"); + foreach (var width in new[] { 1, 2, 4 }) + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var source in Enumerable.Range(0, 8)) + foreach (var alias in new[] { false, true }) + foreach (var vi in Enumerable.Range(0, 4)) + foreach (var ccr in Enumerable.Range(0, 32)) + { + var id = $"{model.Id}/MOVE/write/width={width}/bank={bank}/source={source}/predecrement=true/alias={alias}/value={vi}/ccr={ccr}"; + try + { + var mask = width == 1 ? 0xffu : width == 2 ? 0xffffu : uint.MaxValue; + var value = vi switch { 0 => 0u, 1 => 0x80818283u & mask, 2 => mask >> 1, _ => mask }; + var stride = width == 1 && source == 7 ? 2u : (uint)width; + var destination = alias ? 0x4200u : 0x4400u; + var destinationRegister = alias ? source : source == 1 ? 2 : 1; + var opcode = (ushort)((width == 1 ? 0x10a0 : width == 2 ? 0x30a0 : 0x20a0) | source | (destinationRegister << 9)); + bus.Disarm(); m.Reset(ccr); _ = SyntheticExecution.Prepare(m, [opcode, 0x7e2a, 0x4e71]); + for (uint address = 0x41f8; address < 0x4210; address++) m.InitializePhysical(address, 0xa5, 1); + for (uint address = 0x43f8; address < 0x4410; address++) m.InitializePhysical(address, 0xa5, 1); + m.InitializePhysical(0x4200, value, width); m.InitializePhysical(0x9020, 0x4e73, 2); m.InitializePhysical(0x9022, 0x4e71, 2); + for (var register = 0; register < 7; register++) m.Core.State.A[register] = 0x4000u + (uint)register * 0x100; + if (source != 7) m.Core.State.A[source] = 0x4200u + stride; + if (!alias) m.Core.State.A[destinationRegister] = destination; + m.Core.State.SetUserStackPointer(source == 7 && bank is "user" or "user-M" ? 0x4200u : 0x7800u); + m.Core.State.SetInterruptStackPointer(source == 7 && bank == "ISP" ? 0x4200u : 0x4700u); + m.Core.State.SetMasterStackPointer(source == 7 && bank == "MSP" ? 0x4200u + stride : 0x7400u); + var sr = (ushort)(0x700 | ccr | (bank is "ISP" or "MSP" ? 0x2000 : 0) | (bank is "user-M" or "MSP" ? 0x1000 : 0)); + m.Core.State.StatusRegister = sr; m.Core.State.SetActiveStackPointer(source == 7 ? 0x4200u + stride : bank == "MSP" ? 0x7400u : bank == "ISP" ? 0x4700u : 0x7800u); + var e = ArchitecturalExpectation.Capture(m); var serial = m.Core.State.ExceptionSequence; + e.Sr = (ushort)((sr & 0xfff0) | (value == 0 ? 4 : (value & (1u << (width * 8 - 1))) != 0 ? 8 : 0)); + e.A[source] -= stride; if (source == 7 && bank == "MSP") e.MasterStackPointer = e.A[7]; + bus.Accesses.Clear(); bus.Observe(destination, width); if (fault) bus.Arm(destination, width); Step(m, batch); + if (fault) + { + if (bus.Rejected != 1 || m.Core.State.LastExceptionVector != 2 || m.Core.State.ExceptionSequence != serial + 1 || + m.Core.State.ProgramCounter != 0x9020 || m.Core.State.Halted || m.Core.State.Stopped) + throw new InvalidOperationException("Final-write request did not enter vector 2 exactly once"); + var frame = m.Core.State.A[7]; + var expectedFrame = (bank is "ISP" or "MSP" ? e.A[7] : bank == "user-M" ? 0x7400u : 0x4700u) - 32; + if (frame != expectedFrame || bus.SuccessfulSourceReads != 1 || bus.SuccessfulDestinationWrites != 0 || + m.Core.State.D[7] != e.D[7] || m.PeekPhysical(frame + 6, 2) != 0xa008 || m.PeekPhysical(frame, 2) != e.Sr || m.PeekPhysical(frame + 2, 4) != 0x1002 || + (m.PeekPhysical(frame + 10, 2) & 0x1f7) != (0x100u | (width == 1 ? 0x10u : width == 2 ? 0x20u : 0) | ((sr & 0x2000) != 0 ? 5u : 1u)) || + m.PeekPhysical(frame + 16, 4) != destination || (m.PeekPhysical(frame + 24, 4) & mask) != value) + throw new InvalidOperationException("Final-write format-A PC/SR/SSW/address/output differs"); + if (bus.Accesses.Any(a => a.Write && (a.Address < frame || a.Address >= frame + 32))) + throw new InvalidOperationException("Rejected final write reached ordinary memory"); + for (uint n = 0; n < 32; n++) e.Memory[model.Physical(frame + n)] = bus.Peek(model.Physical(frame + n)); + bus.ExcludeFrameReads(frame); e.ExceptionVector = 2; Step(m, batch); + } + e.Write(destination, value, width, model); e.Pc = 0x1002; Check(m, e); + // Existing approximate execution policy, not physical cycles. + var plan = ((M68kAdvancedTimingInterpreter)m.Core).Timing.LastInstructionTiming.Plan; + if (plan.Key != M68kInstructionTimingKey.GeneralMove || plan.NativeCycles != 9 || + plan.Barriers != M68kTimingBarrier.None || plan.UsesHeadTail != (model.Id == "68030") || + plan.HeadCycles != (model.Id == "68030" ? 2 : 0) || plan.TailCycles != 0) + throw new InvalidOperationException($"Predecrement timing policy differs: {plan.Key}/{plan.NativeCycles}/{plan.HeadCycles}/{plan.TailCycles}"); + // Frame traffic can overlap A7 source guards; distinguish the + // intended operand widths/value from frame word transfers. + if (bus.SuccessfulSourceReads != 1 || bus.SuccessfulDestinationWrites != 1 || bus.Rejected != (fault ? 1 : 0)) + throw new InvalidOperationException("Final-write recovery repeated a completed source/update or write"); + e.D[7] = 42; e.Pc = 0x1004; e.Sr = (ushort)(e.Sr & 0xfff0); Step(m, batch); Check(m, e); + report.Record(id, "passing", null); + } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException) { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + try { report.Complete(output); } catch (Exception ex) { failures.Add(ex); } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(x => x.Message))); + } + private static void Check(SyntheticMachine m, ArchitecturalExpectation e) + { var error = e.Verify(m); if (error != null) throw new InvalidOperationException(error); } + private static void Step(SyntheticMachine m, bool batch) + { + if (!batch) { m.Core.ExecuteInstruction(); return; } + var boundary = new Boundary(); if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, boundary) != 1 || boundary.Before != 1 || boundary.After != 1) + throw new InvalidOperationException("Batch boundary differs"); + } + private sealed class Boundary : IM68kInstructionBoundary + { public int Before, After; public bool BeforeInstruction() { Before++; return true; } public void AfterInstruction(long a, long b) => After++; } + private sealed class FaultBus : SparseRecordingBus, IM68kPhysicalAddressMap, IM68kBus + { + private uint? denied; private int width; + internal int Rejected, SuccessfulSourceReads, SuccessfulDestinationWrites; + private uint destination; private int operandWidth; private uint? frameReadStart; + internal void Disarm() { denied = frameReadStart = null; Rejected = SuccessfulSourceReads = SuccessfulDestinationWrites = 0; } + internal void Arm(uint address, int size) { denied = address; width = size; } + public bool IsCpuPhysicalAddressMapped(uint address, int size, M68kBusAccessKind kind) + { + if (denied is not { } at || kind != M68kBusAccessKind.CpuDataWrite || size != width || address != at) return true; + denied = null; Rejected++; return false; + } + // Record actual transport, independently of the mapping query. A denied + // destination must never be dispatched to the ordinary bus. + private void BeforeWrite(uint address, int size, M68kBusAccessKind kind) + { + if (kind != M68kBusAccessKind.CpuDataWrite) return; + if (denied == address && size == width) throw new InvalidOperationException("Denied destination write bypassed physical map"); + if (address == destination && size == operandWidth) SuccessfulDestinationWrites++; + } + internal void Observe(uint address, int size) { destination = address; operandWidth = size; } + internal void ExcludeFrameReads(uint frame) => frameReadStart = frame; + private void BeforeRead(uint address, int size, M68kBusAccessKind kind) + { + if (address is >= 0x41f8 and < 0x4210 && size == operandWidth && kind == M68kBusAccessKind.CpuDataRead && + !(frameReadStart is { } frame && address >= frame && address + (uint)size <= frame + 32)) SuccessfulSourceReads++; + } + byte IM68kBus.ReadByte(uint a, ref long c, M68kBusAccessKind k) { BeforeRead(a, 1, k); return base.ReadByte(a, ref c, k); } + ushort IM68kBus.ReadWord(uint a, ref long c, M68kBusAccessKind k) { BeforeRead(a, 2, k); return base.ReadWord(a, ref c, k); } + uint IM68kBus.ReadLong(uint a, ref long c, M68kBusAccessKind k) { BeforeRead(a, 4, k); return base.ReadLong(a, ref c, k); } + void IM68kBus.WriteByte(uint a, byte v, ref long c, M68kBusAccessKind k) { BeforeWrite(a, 1, k); base.WriteByte(a, v, ref c, k); } + void IM68kBus.WriteWord(uint a, ushort v, ref long c, M68kBusAccessKind k) { BeforeWrite(a, 2, k); base.WriteWord(a, v, ref c, k); } + void IM68kBus.WriteLong(uint a, uint v, ref long c, M68kBusAccessKind k) { BeforeWrite(a, 4, k); base.WriteLong(a, v, ref c, k); } + } +} diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 78cbe426..9c976349 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -57,6 +57,10 @@ it does not replace or discard their remaining gaps. verified. Source/package linkage is complete for these scopes. The full expected catalog has 13,737 reports / 110,928,842 cases; actual complete-suite execution and verification remain pending. + A newer 259-input private predecrement final-write correction has selected + 1,204,224-case evidence and a reviewable two-file patch. Its broader full/deep/API + and consumer qualification is pending; the running 258-input full replay cannot + qualify the newer CPU changes. No private production import occurs. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index d6a4b246..9810064b 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13319,3 +13319,69 @@ Evidence under the retained audit root: The full suite is still running. Hardware is unavailable for the disputed 030/040 boundaries, which remain explicitly unqualified. No broad reference failure is reclassified. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +### Predecrement-source final-write discovery and private correction — 2026-10-08 + +The new `SyntheticM68020PredecrementWriteTests` exercises byte/word/long +`MOVE -(An),(Am)` with all eight source registers, aliased and distinct +destinations, four stack modes, four boundary values, all 32 CCR values and +scalar/batch execution on EC020, 020, 030 and A1200. Registers, flags, next PC, +memory canaries, explicit fault/RTE progression and source/write counts are +checked through the existing public factory and recording bus. The canonical +operand is independently placed at 4200; the initial source register is one +stride beyond it. A7 byte stride is two. The following MOVEQ sentinel and the +existing nine-cycle general-EA policy discriminate replay, extra register effects +and timing changes. This selected matrix does not claim every destination +register or a physical partial-transfer protocol. + +An unchanged copy of the qualified 258-input private candidate plus this fixture +has **196,608 passing mapped controls and 196,608 mismatching denied-write cases**: +four named tests, two passed and two failed, actual exit 1. Every fault combination +fails, with the first 10,000 retained diagnostics per report stating +`Denied destination write bypassed physical map`. The reporting cap is explicit; +the remaining failing combination statuses are retained, without a claim to +have stored their individual diagnostics. The general MOVE route dispatched the +destination directly instead of creating the pending-write frame. + +The isolated correction changes only private `Move.cs` and `FinalWrite020.cs`. +It checks this selected destination before dispatch, admits source mode 4 into +the C023 software format and restores the same general-EA timing plan after an +explicit RTE. The accepted source decrement/read and captured value are retained; +the opcode/source is not retried. Normal mapped controls are byte-for-byte equal +as JSON reports to the unchanged baseline. Twelve selected tests pass: +**1,204,224 cases / 264 reports**, consisting of **393,216 new cases** and +**811,008 retained cases**. All retained reports match the earlier qualified +catalog exactly; zero selected mismatching, unsupported or untested cases remain. +Independent verification checks 259 source/project inputs, three executed DLLs, +exact TRX outcomes/definitions/methods/paths, counters, combination keys/weights, +baseline diagnostics and immutable outputs. An initial verifier rejected Windows +path separators; V2 normalizes them without changing evidence or criteria. + +The reviewable patch is +`scripts/reference/m68020-predecrement-write-candidate.patch`. Apply it only to a +fresh isolated copy of the private candidate source, with the new fixture copied +into its test project; the production tree is unchanged. A two-file patch replay +proves normalized source equality to the executed correction. Patch application +preserves some original CRLF bytes, so this is normalized source equality, not a +claim that replayed raw source or rebuilt DLL hashes equal the executed build. +The selected audit enables `COPPER68K_RUN_020_PREDECREMENT_WRITE=1`; its precise +filter is `FullyQualifiedName~SyntheticM68020PredecrementWriteTests`. The retained +checks additionally select `SyntheticM68020MoveWriteFaultTests`, +`SyntheticM68020MoveOriginEntryFaultTests` and `SyntheticM68020MoveWriteHandlerTests` +with their corresponding three opt-in variables enabled. Output reports and a +TRX must be requested; missing/empty selections cannot establish this result. + +| Evidence | SHA-256 | +| --- | --- | +| `audits/PredecrementWriteProofV2.json` | `4775c37e7b3e166524f2abd632b76bec835ee3eba6a99c0dec470c08fa1faa64` | +| Candidate patch | `65a874ecb01896059ce1ad00c8b3ff2ccf8267a2379d2ebe11d3b655a354e128` | +| New fixture | `129c13cf4549091eacc6a6334534f0be3497003500a33373b9463c0b72b6b770` | +| `audits/PredecrementPatchReplayV1/proof.json` | `26fe0756bf3017a09fcea9a232e99c6a595907c98024c052afdf89bf45b48844` | + +This closes the selected private whole-request destination-check gap, not +initial predecrement read faults, other destinations, partial transfers, trace, +enabled MMU/cache or foreign frames. The newer 259-input / 39-CPU correction +requires full/deep/API and affected-consumer qualification. The earlier 258-input +full run continues unchanged and cannot qualify these new CPU changes. Production +has 232 inputs / 37 unchanged CPU inputs after adding the fixture. No private +CPU is imported and no package is published. Milestone 6 remains **in progress**. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 67d30cd7..fa45110c 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4166,3 +4166,16 @@ patch is prepared for review without production import. The qualification record states the exact scope and proof identities. Full-suite execution remains running; unavailable hardware leaves disputed 030/040 boundaries unqualified. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +### Milestone 6 checkpoint — predecrement final-write correction, 2026-10-08 + +A new selected `MOVE -(An),(Am)` fixture passes 196,608 normal controls and +exposes 196,608 denied writes bypassing the physical map in the isolated private +candidate. A two-file private correction preserves the accepted source decrement, +captured value and existing general-EA timing policy across explicit RTE. Exact +independent verification qualifies 1,204,224 passing cases / 264 reports, including +811,008 unchanged retained cases. The test and reviewable candidate patch are +saved; production CPU code is unchanged. See the qualification record for scope, +evidence identities and patch replay caveats. Newer full/deep/API and consumer +qualification is pending; the earlier full run remains on its frozen inputs. +Milestone 6 stays **in progress**, `roadmapComplete=false`. diff --git a/scripts/reference/m68020-predecrement-write-candidate.patch b/scripts/reference/m68020-predecrement-write-candidate.patch new file mode 100644 index 00000000..4c5f9da0 --- /dev/null +++ b/scripts/reference/m68020-predecrement-write-candidate.patch @@ -0,0 +1,52 @@ +--- a/Copper68k/M68kAdvancedTimingInterpreter.FinalWrite020.cs ++++ b/Copper68k/M68kAdvancedTimingInterpreter.FinalWrite020.cs +@@ -18,7 +18,7 @@ + } + + private static bool IsSimpleFinalMove020(ushort opcode) => opcode >> 12 is >= 1 and <= 3 && +- ((opcode >> 6) & 7) == 2 && ((opcode >> 3) & 7) is 2 or 3; ++ ((opcode >> 6) & 7) == 2 && ((opcode >> 3) & 7) is 2 or 3 or 4; + + private static M68kOperandSize FinalMoveSize020(ushort opcode) => (opcode >> 12) switch + { 1 => M68kOperandSize.Byte, 3 => M68kOperandSize.Word, _ => M68kOperandSize.Long }; +@@ -33,6 +33,18 @@ + (true, 3) => M68kInstructionTimingKey.MoveWordPostIncrementToAddressIndirect, + _ => M68kInstructionTimingKey.MoveLongPostIncrementToAddressIndirect + }; ++ ++ private void CompleteFinalMoveWriteTiming020(ushort opcode) ++ { ++ if (((opcode >> 3) & 7) != 4) { CompleteTiming(FinalMoveTiming020(opcode)); return; } ++ // This shape already uses the general-EA route on mapped execution. ++ // Only its pending destination write remains; preserve that same plan. ++ var cycles = _profile.FixedInstructionNativeCycles ?? 9; ++ var plan = _profile.Model == M68kAcceleratorModel.M68030 ++ ? M68kInstructionPlan.CreateHeadTail(M68kInstructionTimingKey.GeneralMove, "MOVE/MOVEA general EA", cycles, 2, 0) ++ : M68kInstructionPlan.CreateFlat(M68kInstructionTimingKey.GeneralMove, "MOVE/MOVEA general EA", cycles); ++ CompleteTimingPlan(plan); ++ } + + private void WriteFinalMoveDestination020(uint address, uint value, M68kOperandSize size) + { +@@ -128,7 +140,7 @@ + if ((ssw & 0x100) != 0) WriteFinalMoveDestination020(address, value, size); + } + finally { _resumingFinalMoveWrite020 = previous; _restoredWriteFunctionCode020 = previousFc; } +- CompleteTiming(FinalMoveTiming020(opcode)); ++ CompleteFinalMoveWriteTiming020(opcode); + return true; + } + } +--- a/Copper68k/M68kAdvancedTimingInterpreter.Move.cs ++++ b/Copper68k/M68kAdvancedTimingInterpreter.Move.cs +@@ -55,7 +55,9 @@ + else + { + var destination = ResolveMoveAddress(destinationMode, destinationRegister, size, opcode); +- WriteSized(destination, value, size); ++ if (destinationMode == 2 && sourceMode == 4) ++ WriteFinalMoveDestination020(destination, value, size); ++ else WriteSized(destination, value, size); + if (destinationMode == 3) WriteGeneralRegister(true, destinationRegister, + unchecked(destination + M68kIntegerSemantics.AddressIncrement(destinationRegister, size))); + } From de252408b5ed3e41d3fb72ed29ddabc1ade3f810 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 22:04:47 +0300 Subject: [PATCH 177/211] Qualify newer private candidate consumers and references --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 9 ++- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 58 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 13 +++++ 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 9c976349..8658757b 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -58,9 +58,12 @@ it does not replace or discard their remaining gaps. expected catalog has 13,737 reports / 110,928,842 cases; actual complete-suite execution and verification remain pending. A newer 259-input private predecrement final-write correction has selected - 1,204,224-case evidence and a reviewable two-file patch. Its broader full/deep/API - and consumer qualification is pending; the running 258-input full replay cannot - qualify the newer CPU changes. No private production import occurs. + 1,204,224-case evidence and a reviewable two-file patch. Its same-build deep/API, + eight invalid-request guards and local .79 consumers are independently verified. + Exact source/assembly/package linkage is recorded in the qualification log. + Its newer full replay has started; both full results and verification remain + pending. The running 258-input replay cannot qualify the newer CPU changes. + No private production import occurs. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 9810064b..0468201f 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13385,3 +13385,61 @@ requires full/deep/API and affected-consumer qualification. The earlier 258-inpu full run continues unchanged and cannot qualify these new CPU changes. Production has 232 inputs / 37 unchanged CPU inputs after adding the fixture. No private CPU is imported and no package is published. Milestone 6 remains **in progress**. + +### Predecrement candidate integration checkpoint — 2026-10-08 + +The newer 259-input / 39-CPU graph is independently frozen at source proof +`cb425ab482a49f783ab722ca0e8cf504d17424f33ebf050ae22d58bd7ca4f4fa`. +Compiled discovery gives exactly **8,128 named rows**. Before execution, its full +catalog binds **13,753 reports / 111,322,058 expected cases** to retained qualified +reports plus the sixteen new predecrement reports. The full suite starts from +the already qualified selected-run assemblies, without rebuilding, and enables +the same ten qualified native reference presets plus the predecrement fixture. +Expected outcomes are 8,091 passed / 37 unavailable; these are expectations, +not completed full execution. The earlier 258-input full replay continues on +its original sources and assemblies. Neither running audit is restarted. + +The newer assemblies independently pass **320,000 seeded cases / 32 reports**, +**312,500 SingleStepTests cases / 125 files** and **536 Musashi programs / 88 +exclusions**. Pinned inputs, exclusions, exact loaded methods/paths, commands, +settings, case weights/statuses and binary/source identities are verified. +The same assemblies execute eight actual invalid audit requests, each with one +intended failed test and no CPU execution/report: zero seed/samples, empty model +selection, missing/empty SingleStepTests input, missing Musashi input and +adjacent/separated duplicate models. Guard verifier V1 stops while constructing +its output because the newer source record has different lineage metadata; +V2 records lineage from the deep input and preserves all eight checks and their +original execution evidence. No rejected proof is relabeled as successful. + +Separate reflection processes compare this newer compiled CPU with the retained +production CPU: **20 exported types / 211 public or protected records**, zero +added or removed managed signatures. Independent verification binds the helper, +two distinct assembly inputs and candidate full-run manifest. This retains the +managed-signature scope and earlier physical-layout/runtime caveats. + +A fresh local-only `1.5.2-synthetic-dev.79` package contains these exact 39 CPU +inputs plus README/icon assets. The clean `aa1dad5` CopperScreen archive, separate +package cache and explicit version override preserve the NuGet-only boundary. +Independent consumer verification proves a clean production build, **171 host +passes / six unavailable**, **74 disk passes**, **1,080 engine passes** and **two +native Workbench 3.1 floppy replays**, with exact retained rosters, archive/source +identities, all three loaded CPU copies and unchanged ROM/media hashes. Package +version/build metadata gives the package its own binary identity; source equality +is explicit rather than silently equating the package DLL with the full-run DLL. +No HD boot, throughput or hardware qualification is added by these checks. + +| Newer candidate evidence | SHA-256 | +| --- | --- | +| `discovery/proof.json` | `382381db2d8ee78241e8c2ce81e442db13972e85c17c18659f49d422a92d49a2` | +| `expected-reports.json` | `ed62506e84c964961c6eb4b2c238078924c4c461a76a74f7b2046767ab0e6c93` | +| `audits/PredecrementDeepV1/proof.json` | `dfab8a22affec7c935b7a4daacc750947de871a419be8b50b9e6d1c878eede3e` | +| `audits/PredecrementDeepControlsV1/independent-proof.json` | `4c5e4b14d1990e141aa7f0975380a7363598b43348efeea271ac8df6623cb28d` | +| `api-review-v2/independent-proof.json` | `88e05e1302689d664253342d5df1df893134e1bd75961ec8464acf745589879c` | +| `consumer-79-complete-proof-v1.json` | `d19650946c07337af5a9a406a2a3549fb8ca1a75794450bb693c55ea39ec9ce8` | +| `predecrement-evidence-link-v1.json` | `b744c7979487b28aa7b134011b22a90e2d755f4808e95a9d23e36a93afb06a0a` | + +The final linkage checks all 259 source inputs, exact shared full/deep/guard/API +assemblies and all 39 local-package CPU inputs. Both full runs remain pending. +Broad reference disagreements, wider restoration and hardware boundaries remain +open. Production CPU code is unchanged; no package is published. Milestone 6 +remains **in progress**, `roadmapComplete=false`, and PR #22 remains draft. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index fa45110c..c814a3bb 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4179,3 +4179,16 @@ saved; production CPU code is unchanged. See the qualification record for scope, evidence identities and patch replay caveats. Newer full/deep/API and consumer qualification is pending; the earlier full run remains on its frozen inputs. Milestone 6 stays **in progress**, `roadmapComplete=false`. + +### Milestone 6 checkpoint — newer candidate integration, 2026-10-08 + +The 259-input predecrement candidate has exact 8,128-row compiled discovery and +a frozen 13,753-report / 111,322,058-case expected catalog. Its full run has +started on the selected-run assemblies; the earlier 258-input run continues +separately. Same-build deep references and eight actual invalid-request guards +are independently verified, as are zero managed public/protected API changes. +The fresh local-only .79 package passes clean CopperScreen build, 171 host / +six unavailable, 74 disk, 1,080 engine and two native floppy checks. Explicit +source/assembly/package linkage and caveats are in the qualification record. +Full execution and its independent verification remain pending; no private CPU +import or publication occurs. Milestone 6 stays **in progress**. From b92f6f55f10fac476591758845e848180cb7ffbb Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 22:42:29 +0300 Subject: [PATCH 178/211] Record complete predecessor CPU qualification --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 14 ++++++---- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 28 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 12 ++++++++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 8658757b..45e8dd48 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -47,22 +47,24 @@ it does not replace or discard their remaining gaps. record still lists wider restoration, operand, repair/refault, trace/interrupt and foreign-frame gaps. Preserve those distinctions when reviewing the promoted integer/system families. - The latest isolated 258-input candidate preserves the current production + The isolated 258-input predecessor preserves the current production MOVE16 correction. Its 38-test / 1,641,472-case preflight is independently qualified, and exact discovery proves 8,124 complete-suite names. That full - execution has started; completed results and full verification remain pending. + execution completes with 8,087 passed / 37 unavailable / zero failed. + Independent full verification binds 13,737 reports / 110,928,842 cases, + ten qualified native presets and the ordinary 86,663,530-case gate. This is candidate evidence, not production import or hardware qualification. The same candidate's local .78 consumer checks and same-build seeded, SingleStepTests/Musashi and eight rejection controls are independently verified. Source/package linkage is complete for these scopes. The full - expected catalog has 13,737 reports / 110,928,842 cases; actual complete-suite - execution and verification remain pending. + completed evidence is linked by + `d9c5948d380cf3acb9ebe277b349a2c1a56b74d8546dbb9fcee27356a1a1b898`. A newer 259-input private predecrement final-write correction has selected 1,204,224-case evidence and a reviewable two-file patch. Its same-build deep/API, eight invalid-request guards and local .79 consumers are independently verified. Exact source/assembly/package linkage is recorded in the qualification log. - Its newer full replay has started; both full results and verification remain - pending. The running 258-input replay cannot qualify the newer CPU changes. + Its newer full replay has started; its result and independent verification + remain pending. The completed predecessor cannot qualify the newer CPU changes. No private production import occurs. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 0468201f..6fc61db1 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13443,3 +13443,31 @@ assemblies and all 39 local-package CPU inputs. Both full runs remain pending. Broad reference disagreements, wider restoration and hardware boundaries remain open. Production CPU code is unchanged; no package is published. Milestone 6 remains **in progress**, `roadmapComplete=false`, and PR #22 remains draft. + +### Completed predecessor full qualification — 2026-10-08 + +The frozen 258-input / 39-CPU `CurrentPrivateRebaseV3` suite completes with +**8,087 passed / 37 unavailable / zero failed**, across 8,124 named rows and +8,092 definitions. Independent verification checks the exact discovery roster, +loaded assemblies and methods, source inputs, **13,737 reports / 110,928,842 +logical cases**, and ten qualified native reference presets. The ordinary +report-only gate passes **86,663,530 cases / 783 batches** on these outputs. +Unavailable rows remain unavailable coverage; the broad Basic disagreements +and opt-in restoration discovery gates are not reclassified by this result. + +Full proof SHA-256: +`bfbe80fbdda42673441afe46158db41d56e068f394a5f87ce50db8dda282c2d4`. +The fresh `current-private-complete-link-v1.json` binds this completed proof to +the previously verified same-build deep/guard evidence, same-source local .78 +consumer evidence and compiled API review. Its SHA-256 is +`d9c5948d380cf3acb9ebe277b349a2c1a56b74d8546dbb9fcee27356a1a1b898`. +The first linkage request rejected an incorrectly selected V1 prior record; +the documented qualified V2 record is used, with all original pins preserved. +Historical records that said full execution was pending remain historical. + +This qualifies the predecessor only. The newer 259-input predecrement correction +still has a separate running full suite; no result is transferred between the +two CPU versions. No hardware is available for the disputed 030/040 boundaries, +which remain unqualified. Production import, wider restoration and the failed +broad reference gate remain open. No package is published. Milestone 6 remains +**in progress**, `roadmapComplete=false`, and PR #22 remains draft. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index c814a3bb..03c003b6 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4192,3 +4192,15 @@ six unavailable, 74 disk, 1,080 engine and two native floppy checks. Explicit source/assembly/package linkage and caveats are in the qualification record. Full execution and its independent verification remain pending; no private CPU import or publication occurs. Milestone 6 stays **in progress**. + +### Milestone 6 checkpoint — predecessor full qualification, 2026-10-08 + +The 258-input combined predecessor now completes independent full verification: +8,087 tests pass / 37 remain unavailable, with zero failures, 13,737 reports and +110,928,842 logical cases. The ordinary gate and ten qualified native presets +pass. Fresh linkage binds this result to its deep/guard, API and local .78 +consumer evidence. The qualification record supplies proof identities and scope. +The newer 259-input predecrement full run is still pending and cannot inherit +the predecessor's result. Hardware is unavailable; disputed trace boundaries +remain unqualified. Broad reference and wider restoration gaps keep milestone 6 +**in progress**, `roadmapComplete=false`; no private CPU import or publication. From ebad4de8b1480ee82bd2ecd48fe24d4d7ad9717f Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 22:45:59 +0300 Subject: [PATCH 179/211] Update 040 fault inventory coverage descriptions --- .../Synthetic/SyntheticM68040AccessFrameAuditTests.cs | 4 ++-- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index f99538a8..15391c73 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -35,11 +35,11 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() form == "frame-validation-fault" ? "Validation, preserved-trace repair/software service, successful mixed-epoch chains and mixed-epoch validation fault capture have separate software coverage; mixed-epoch repair/retry, original trace suspension/resumption and internal restoration remain required. Physical/hardware behavior is separately unqualified" : form == "real-access-fault-entry" - ? "Cache-disabled accurate instruction-fetch restart and actual legal MOVE memory-destination faults through WB1 handler/RTE are covered separately; other integer/read/MOVEM/MOVE16 faults, enabled-cache deferral and compiled fault pipelines remain required" + ? "Cache-disabled accurate instruction-fetch restart, actual legal MOVE memory-destination faults, scoped normal-space MOVES read/store faults and five legal MOVE16 operand-fault forms have separate software coverage. MOVE16 write recovery uses an explicit saved-line handler/RTE; its read TT and WB1 validity remain disputed. Other integer/read/MOVEM faults, wider operand/repair/refault combinations, enabled-cache deferral and compiled fault pipelines remain required" : form == "odd-PC-chained-SR-provenance" ? "Chained format0/2/3 and format7 normal/CM SR handoff, stack consumption and header reads have separate generated WinUAE software coverage; access-frame validation/no-replay checks are synthetic. Separate composed-entry and executed-MMU-entry CM lifetime discovery profiles disagree after odd-PC fault/repair; the executed profile also exposes stacked-SR disagreement. Neither qualifies the full run loop or hardware. CT/CU/CP pending/foreign-context chains, full reference exception qualification and hardware behavior remain unqualified" : form == "writeback-handler" - ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks; actual legal MOVE destination faults exercise WB1 through handler/RTE. Nested WB2/3, cache-push/MOVE16 lines and other data-fault construction remain required. Physical function-code spaces and hardware timing are unqualified" + ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks. Supplied same-width WB1/2/3 outer slots have actual nested MOVES-store fault/handler/RTE coverage at CCR=31; original construction of all slots, mixed widths, all CCRs, deeper repeated faults and trace/interrupt interruption remain required. Five legal MOVE16 forms have explicit saved-line completion coverage; cache-push lines and other data-fault construction remain required. Physical function-code spaces, MOVE16 WB1 validity and hardware timing are unqualified" : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 45e8dd48..24c36826 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -66,6 +66,12 @@ it does not replace or discard their remaining gaps. Its newer full replay has started; its result and independent verification remain pending. The completed predecessor cannot qualify the newer CPU changes. No private production import occurs. + The 040 access-frame discovery inventory retains all 480 required untested + combinations (three banks, 32 CCR images, five protocol labels). Its diagnostic + notes now acknowledge the separately qualified MOVES, nested supplied-slot + writeback and MOVE16 fixtures without treating their bounded coverage as + completion of those broader protocols. Only descriptions change; no case, + status, enable flag or failure condition is removed. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; From b50aee247bdd127ebc9bd604b0b30d35d21032c5 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 23:00:33 +0300 Subject: [PATCH 180/211] Qualify private predecrement MOVE read recovery --- .gitattributes | 1 + .../SyntheticM68020PredecrementReadTests.cs | 169 ++++++++++++++++++ docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 8 + docs/COPPER68K_REFERENCE_QUALIFICATION.md | 58 ++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 ++ .../m68020-predecrement-read-candidate.patch | 29 +++ 6 files changed, 280 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68020PredecrementReadTests.cs create mode 100644 scripts/reference/m68020-predecrement-read-candidate.patch diff --git a/.gitattributes b/.gitattributes index faa3becf..b84a49dc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ # Unified patch context needs its single-space blank lines; keep evidence bytes stable. scripts/reference/m68020-predecrement-write-candidate.patch text eol=lf whitespace=-blank-at-eol +scripts/reference/m68020-predecrement-read-candidate.patch text eol=lf whitespace=-blank-at-eol diff --git a/Copper68k.Tests/Synthetic/SyntheticM68020PredecrementReadTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68020PredecrementReadTests.cs new file mode 100644 index 00000000..8779cbed --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68020PredecrementReadTests.cs @@ -0,0 +1,169 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Private whole-request predecrement read continuation. The accepted source +// decrement must survive explicit RTE without recalculation or duplicate update. +// No foreign frame, partial physical transfer or trace qualification is claimed. +public sealed class SyntheticM68020PredecrementReadTests(ITestOutputHelper output) +{ + private static readonly ModelSpec[] Models = ModelSpec.All.Where(x => x.Id is "68EC020" or "68020" or "68030" or "A1200").ToArray(); + [Theory, Trait("Suite", "ReferenceDiscovery")] + [InlineData(false)] [InlineData(true)] + public void DirectWidthControls(bool batch) => Audit(batch, false); + [EnvironmentFact("COPPER68K_RUN_020_PREDECREMENT_READ", "require private all-width read/write continuations"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarReadWriteContinuations() => Audit(false, true); + [EnvironmentFact("COPPER68K_RUN_020_PREDECREMENT_READ", "require private all-width read/write continuations"), Trait("Suite", "ReferenceDiscovery")] + public void BatchReadWriteContinuations() => Audit(true, true); + + private void Audit(bool batch, bool fault) + { + var failures = new List(); + foreach (var model in Models) + { + var bus = new FaultBus(); var m = new SyntheticMachine(model, bus); + foreach (var width in new[] { 1, 2, 4 }) + foreach (var lane in fault ? new[] { "read", "read-write", "read-write-refault" } : new[] { "control" }) + foreach (var offset in fault ? Enumerable.Range(0, width) : new[] { 0 }) + { + var report = new CoverageBatch(model.Id, $"predecrement-read-write-{width}-{lane}-{offset}-{(batch ? "batch" : "scalar")}"); + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var source in Enumerable.Range(0, 8)) + foreach (var alias in new[] { false, true }) + foreach (var vi in Enumerable.Range(0, 4)) + foreach (var ccr in Enumerable.Range(0, 32)) + { + var id = $"{model.Id}/MOVE/predecrement-read-write/width={width}/lane={lane}/offset={offset}/bank={bank}/source={source}/predecrement=true/alias={alias}/value={vi}/ccr={ccr}"; + try + { + var mask = width == 1 ? 0xffu : width == 2 ? 0xffffu : uint.MaxValue; + var value = vi switch { 0 => 0u, 1 => 0x80818283u & mask, 2 => mask >> 1, _ => mask }; + var stride = width == 1 && source == 7 ? 2u : (uint)width; + var destination = alias ? 0x4200u : 0x4400u; + var destinationRegister = alias ? source : source == 1 ? 2 : 1; + var opcode = (ushort)((width == 1 ? 0x10a0 : width == 2 ? 0x30a0 : 0x20a0) | source | (destinationRegister << 9)); + bus.ResetObservation(); m.Reset(ccr); _ = SyntheticExecution.Prepare(m, [opcode, 0x7e2a, 0x4e71]); + for (uint a = 0x41f8; a < 0x4210; a++) m.InitializePhysical(a, 0xa5, 1); + for (uint a = 0x43f8; a < 0x4410; a++) m.InitializePhysical(a, 0xa5, 1); + m.InitializePhysical(0x4200, value, width); m.InitializePhysical(0x9020, 0x4e73, 2); m.InitializePhysical(0x9022, 0x4e71, 2); + for (var register = 0; register < 7; register++) m.Core.State.A[register] = 0x4000u + (uint)register * 0x100; + if (source != 7) m.Core.State.A[source] = 0x4200u + stride; + if (!alias) m.Core.State.A[destinationRegister] = destination; + m.Core.State.SetUserStackPointer(source == 7 && bank is "user" or "user-M" ? 0x4200u : 0x7800u); + m.Core.State.SetInterruptStackPointer(source == 7 && bank == "ISP" ? 0x4200u : 0x4700u); + m.Core.State.SetMasterStackPointer(source == 7 && bank == "MSP" ? 0x4200u + stride : 0x7400u); + var sr = (ushort)(0x700 | ccr | (bank is "ISP" or "MSP" ? 0x2000 : 0) | (bank is "user-M" or "MSP" ? 0x1000 : 0)); + m.Core.State.StatusRegister = sr; m.Core.State.SetActiveStackPointer(source == 7 ? 0x4200u + stride : bank == "MSP" ? 0x7400u : bank == "ISP" ? 0x4700u : 0x7800u); + var e = ArchitecturalExpectation.Capture(m); var serial = m.Core.State.ExceptionSequence; + e.A[source] -= stride; if (source == 7 && bank == "MSP") e.MasterStackPointer = e.A[7]; + var readFrame = (bank is "ISP" or "MSP" ? e.A[7] : bank == "user-M" ? 0x7400u : 0x4700u) - 92; + bus.Accesses.Clear(); bus.Observe(destination, width); + if (fault) bus.ArmRead(0x4200u + (uint)offset); + if (lane != "read" && fault) bus.ArmWrite(destination); + Step(m, batch); + if (fault) + { + if (m.Core.State.A[7] != readFrame || m.Core.State.ProgramCounter != 0x9020 || m.Core.State.ExceptionSequence != serial + 1 || + m.Core.State.LastExceptionVector != 2 || m.Core.State.Halted || m.Core.State.Stopped || + m.PeekPhysical(readFrame, 2) != sr || m.PeekPhysical(readFrame + 2, 4) != 0x1000 || m.PeekPhysical(readFrame + 6, 2) != 0xb008 || + (m.PeekPhysical(readFrame + 10, 2) & 0x1f7) != (0x140u | (width == 1 ? 0x10u : width == 2 ? 0x20u : 0) | ((sr & 0x2000) != 0 ? 5u : 1u)) || + m.PeekPhysical(readFrame + 16, 4) != 0x4200 || bus.Reads != 0 || bus.Writes != 0 || bus.ReadDenied != 1 || bus.WriteDenied != 0) + throw new InvalidOperationException("All-width read fault changed prefix/frame/PC/SR/SSW"); + for (var n = 0; n < 8; n++) if (m.Core.State.D[n] != e.D[n] || n != 7 && m.Core.State.A[n] != e.A[n]) + throw new InvalidOperationException("Read denial performed an early register effect"); + CaptureFrame(m, e, bus, readFrame, 92); e.ExceptionVector = 2; + Step(m, batch); + } + e.Sr = (ushort)((sr & 0xfff0) | (value == 0 ? 4 : (value & (1u << (width * 8 - 1))) != 0 ? 8 : 0)); + if (fault && lane != "read") + { + var frame = (bank is "ISP" or "MSP" ? e.A[7] : bank == "user-M" ? 0x7400u : 0x4700u) - 32; + var count = 2; + CheckWriteBoundary(m, e, bus, frame, destination, value, mask, width, serial + count); + CaptureFrame(m, e, bus, frame, 32); + if (lane == "read-write-refault") + { + bus.ArmWrite(destination); Step(m, batch); count++; + CheckWriteBoundary(m, e, bus, frame, destination, value, mask, width, serial + count); + CaptureFrame(m, e, bus, frame, 32); + } + Step(m, batch); + } + e.Write(destination, value, width, model); e.Pc = 0x1002; Check(m, e); + var plan = ((M68kAdvancedTimingInterpreter)m.Core).Timing.LastInstructionTiming.Plan; + if (plan.Key != M68kInstructionTimingKey.GeneralMove || plan.NativeCycles != 9 || + plan.Barriers != M68kTimingBarrier.None || plan.UsesHeadTail != (model.Id == "68030") || + plan.HeadCycles != (model.Id == "68030" ? 2 : 0) || plan.TailCycles != 0) + throw new InvalidOperationException("Predecrement read continuation changed timing policy"); + var exceptions = fault ? lane == "read" ? 1 : lane == "read-write" ? 2 : 3 : 0; + if (bus.Reads != 1 || bus.Writes != 1 || bus.ReadDenied != (fault ? 1 : 0) || bus.WriteDenied != (fault ? exceptions - 1 : 0) || + m.Core.State.ExceptionSequence != serial + exceptions || + bus.Accesses.Count(a => !a.Write && a.Kind == M68kBusAccessKind.CpuInstructionFetch && a.Address == 0x1000) > 1) + throw new InvalidOperationException("All-width continuation repeated a source/update/write or opcode fetch"); + e.D[7] = 42; e.Pc = 0x1004; e.Sr = (ushort)(e.Sr & 0xfff0); Step(m, batch); Check(m, e); + report.Record(id, "passing", null); + } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException) { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + try { report.Complete(output); } catch (Exception ex) { failures.Add(ex); } + } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(x => x.Message))); + } + private static void CaptureFrame(SyntheticMachine m, ArchitecturalExpectation e, FaultBus bus, uint frame, int length) + { for (uint n = 0; n < length; n++) e.Memory[m.Model.Physical(frame + n)] = bus.Peek(m.Model.Physical(frame + n)); bus.Frames.Add((frame, length)); } + private static void CheckWriteBoundary(SyntheticMachine m, ArchitecturalExpectation e, FaultBus bus, uint frame, uint address, uint value, uint mask, int width, long serial) + { + var sr = e.Sr; + if (m.Core.State.ProgramCounter != 0x9020 || m.Core.State.A[7] != frame || m.Core.State.ExceptionSequence != serial || m.Core.State.LastExceptionVector != 2 || m.Core.State.Halted || m.Core.State.Stopped || + m.PeekPhysical(frame, 2) != sr || m.PeekPhysical(frame + 2, 4) != 0x1002 || m.PeekPhysical(frame + 6, 2) != 0xa008 || + (m.PeekPhysical(frame + 10, 2) & 0x1f7) != (0x100u | (width == 1 ? 0x10u : width == 2 ? 0x20u : 0) | ((sr & 0x2000) != 0 ? 5u : 1u)) || + m.PeekPhysical(frame + 16, 4) != address || (m.PeekPhysical(frame + 24, 4) & mask) != value || bus.Reads != 1 || bus.Writes != 0) + throw new InvalidOperationException("Read-to-write fault lost completed source/flags or pending write/frame"); + for (var n = 0; n < 8; n++) if (m.Core.State.D[n] != e.D[n] || n != 7 && m.Core.State.A[n] != e.A[n]) + throw new InvalidOperationException("Read-to-write fault repeated a register update"); + } + private static void Check(SyntheticMachine m, ArchitecturalExpectation e) { var error = e.Verify(m); if (error != null) throw new InvalidOperationException(error); } + private static void Step(SyntheticMachine m, bool batch) + { + if (!batch) { m.Core.ExecuteInstruction(); return; } + var b = new Boundary(); if (((IM68kBatchCore)m.Core).ExecuteInstructions(1, null, b) != 1 || b.Before != 1 || b.After != 1) throw new InvalidOperationException("Batch boundaries differ"); + } + private sealed class Boundary : IM68kInstructionBoundary { public int Before, After; public bool BeforeInstruction() { Before++; return true; } public void AfterInstruction(long a, long b) => After++; } + private sealed class FaultBus : SparseRecordingBus, IM68kPhysicalAddressMap, IM68kBus + { + private uint? deniedRead, deniedWrite; private uint destination; private int width; + internal int Reads, Writes, ReadDenied, WriteDenied; + internal readonly List<(uint Start, int Length)> Frames = []; + internal void ResetObservation() { deniedRead = deniedWrite = null; Reads = Writes = ReadDenied = WriteDenied = 0; Frames.Clear(); } + internal void Observe(uint target, int size) { destination = target; width = size; } + internal void ArmRead(uint at) => deniedRead = at; + internal void ArmWrite(uint at) => deniedWrite = at; + public bool IsCpuPhysicalAddressMapped(uint a, int size, M68kBusAccessKind kind) + { + if (kind == M68kBusAccessKind.CpuDataRead && size == width && deniedRead is { } r && unchecked(r - a) < size) { deniedRead = null; ReadDenied++; return false; } + if (kind == M68kBusAccessKind.CpuDataWrite && size == width && deniedWrite == a) { deniedWrite = null; WriteDenied++; return false; } + return true; + } + private void Read(uint a, int size, M68kBusAccessKind kind) + { + if (kind != M68kBusAccessKind.CpuDataRead) return; + if (deniedRead is { } r && size == width && unchecked(r - a) < size) throw new InvalidOperationException("Source read bypassed physical map"); + if (size == width && a is >= 0x41f8 and < 0x4210 && !Frames.Any(f => a >= f.Start && a + (uint)size <= f.Start + f.Length)) Reads++; + } + private void Write(uint a, int size, M68kBusAccessKind kind) + { + if (kind != M68kBusAccessKind.CpuDataWrite) return; + if (deniedWrite == a && size == width) throw new InvalidOperationException("Destination write bypassed physical map"); + if (a == destination && size == width) Writes++; + } + byte IM68kBus.ReadByte(uint a, ref long c, M68kBusAccessKind k) { Read(a, 1, k); return base.ReadByte(a, ref c, k); } + ushort IM68kBus.ReadWord(uint a, ref long c, M68kBusAccessKind k) { Read(a, 2, k); return base.ReadWord(a, ref c, k); } + uint IM68kBus.ReadLong(uint a, ref long c, M68kBusAccessKind k) { Read(a, 4, k); return base.ReadLong(a, ref c, k); } + void IM68kBus.WriteByte(uint a, byte v, ref long c, M68kBusAccessKind k) { Write(a, 1, k); base.WriteByte(a, v, ref c, k); } + void IM68kBus.WriteWord(uint a, ushort v, ref long c, M68kBusAccessKind k) { Write(a, 2, k); base.WriteWord(a, v, ref c, k); } + void IM68kBus.WriteLong(uint a, uint v, ref long c, M68kBusAccessKind k) { Write(a, 4, k); base.WriteLong(a, v, ref c, k); } + } +} diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 24c36826..cf184465 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -72,6 +72,14 @@ it does not replace or discard their remaining gaps. writeback and MOVE16 fixtures without treating their bounded coverage as completion of those broader protocols. Only descriptions change; no case, status, enable flag or failure condition is removed. + A subsequent 260-input private candidate also closes selected whole-request + predecrement source-read recovery in focused execution: 3,244,032 passing + cases / 520 reports, including 1,671,168 byte-identical retained cases. + The new fixture and one-file review patch preserve the accepted decrement + across explicit RTE, including read/write/refault lanes. It requires separate + full/deep/API and consumer qualification; the running 259-input full audit + cannot qualify that newer change. Production import and wider protocols remain + open. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 6fc61db1..e8e6baea 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13471,3 +13471,61 @@ two CPU versions. No hardware is available for the disputed 030/040 boundaries, which remain unqualified. Production import, wider restoration and the failed broad reference gate remain open. No package is published. Milestone 6 remains **in progress**, `roadmapComplete=false`, and PR #22 remains draft. + +### Predecrement source-read continuation — 2026-10-08 + +`SyntheticM68020PredecrementReadTests` independently chooses source address +`4200`, encodes B/W/L `MOVE -(An),(Am)` and starts An one stride above it. +It covers every source address register, distinct/aliased destinations, four +stack modes, four boundary values, all 32 CCR images and scalar/batch execution +on EC020, 020, 030 and A1200. A7 byte stride is two. Whole-request read denial +selects every operand byte; separate lanes exercise read recovery alone, +read-to-write faults and one repeated final-write fault. Register/memory guards, +frame provenance, exact following PC and a following MOVEQ sentinel are checked. +The accepted source decrement remains complete before the read fault; only the +pending suffix may execute after explicit RTE. + +The frozen 260-input predecessor passes **196,608 normal controls** and reports +**1,376,256 unsupported continuations**, failing both selected fault tests. +All unsupported diagnostics identify RTE at `9020`. Its read classifier excludes +source mode 4 for memory destinations, so it records an unknown continuation; +restoration also excludes that form. The isolated one-file correction in +`Operand020.cs` admits this already-decremented source and uses the existing +general-EA completion policy for it. It does not rerun address calculation or +the opcode. The mapped policy remains nine native policy cycles, with the +existing 030 head/tail split; this is not physical timing qualification. + +All **14 selected tests / 3,244,032 cases / 520 reports pass**, with zero +mismatching, unsupported or untested selected cases. This includes **1,572,864 +new cases** and **1,671,168 retained read/write, predecrement final-write and +handler cases**. Normal controls and all retained reports are byte-identical +to their pinned predecessors. Independent verification checks every case ID, +status/weight and baseline diagnostic, exact source changes, named TRX roster, +loaded definitions/methods/assemblies and immutable evidence. + +| Evidence | SHA-256 | +| --- | --- | +| `audits/PredecrementReadProofV1.json` | `28cd5d79924c841bb10ad47134a955412b6f552195b4c940c95300f729af4de2` | +| New fixture | `5427d03ab43d753a2307ea2a41e83ed5f3325f20f9c83d27d3e0574768843bed` | +| `scripts/reference/m68020-predecrement-read-candidate.patch` | `576ea9749c637d5873ea56f6c22c081551e52239eda966a0ff45ce5985c2d494` | +| `audits/PredecrementReadPatchReplayV1/proof.json` | `1b5489d5ccbee6f4e73b6cb184d38204ac68538f4c2d02be0b3e4722a93f5c82` | + +Apply the review patch only to a fresh isolated copy of the earlier private +predecrement-write candidate, and copy the new fixture into that test project. +Enable `COPPER68K_RUN_020_PREDECREMENT_READ=1`, select +`FullyQualifiedName~SyntheticM68020PredecrementReadTests`, and request reports +through `COPPER68K_SYNTHETIC_REPORT_DIR` plus a TRX. The retained selection adds +`SyntheticM68020MemoryReadWriteTests`, `SyntheticM68020PredecrementWriteTests` +and `SyntheticM68020MoveWriteHandlerTests`, with their corresponding opt-ins. +Fresh patch replay proves normalized source equality; preserved CRLF bytes mean +raw replayed source and rebuilt binary identity are not claimed equal. + +The 260-input read correction requires its own full/deep/API and isolated +consumer qualification. The running 259-input full audit remains unchanged and +cannot qualify this newer CPU change. The later mainline 040 inventory edit +changes only two diagnostic descriptions, preserving all 480 untested cases; +it is not relabeled as part of these frozen executions. This closes selected +private whole-request source-read recovery, not other destination forms, foreign +frames, partial transfers, trace/interrupt or enabled MMU/cache behavior. +Production CPU inputs remain unchanged; no private CPU is imported and no +package is published. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 03c003b6..4f94c185 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4204,3 +4204,18 @@ The newer 259-input predecrement full run is still pending and cannot inherit the predecessor's result. Hardware is unavailable; disputed trace boundaries remain unqualified. Broad reference and wider restoration gaps keep milestone 6 **in progress**, `roadmapComplete=false`; no private CPU import or publication. + +### Milestone 6 checkpoint — predecrement read recovery, 2026-10-08 + +The next isolated private gap is reproduced and corrected: selected +`MOVE -(An),(Am)` read faults previously recorded an unknown continuation and +failed at RTE. The one-file candidate admits the accepted decrement and completes +only the pending suffix, preserving the mapped general-EA timing policy. +Independent verification qualifies 3,244,032 passing cases / 520 reports, +including 1,671,168 byte-identical retained cases. The new fixture covers all +source address registers, aliases, A7 stride, stack modes, CCRs, scalar/batch +routes and read/write/refault lanes. Its patch and replay are reviewable; +production CPU code is unchanged. Full/deep/API and consumer qualification of +this newer 260-input graph remain required. The earlier full audit continues on +its own frozen inputs. Broad reference, wider restoration and hardware gaps keep +milestone 6 **in progress**, `roadmapComplete=false`. diff --git a/scripts/reference/m68020-predecrement-read-candidate.patch b/scripts/reference/m68020-predecrement-read-candidate.patch new file mode 100644 index 00000000..8330ad58 --- /dev/null +++ b/scripts/reference/m68020-predecrement-read-candidate.patch @@ -0,0 +1,29 @@ +--- a/Copper68k/M68kAdvancedTimingInterpreter.Operand020.cs ++++ b/Copper68k/M68kAdvancedTimingInterpreter.Operand020.cs +@@ -64,7 +64,7 @@ + _physicalAddressMap!.IsCpuPhysicalAddressMapped(address, width, kind)) return; + var opcode = State.LastOpcode; var top = opcode >> 12; + var mode = (opcode >> 3) & 7; var register = opcode & 7; var destination = (opcode >> 6) & 7; +- var memoryDestination = top is >= 1 and <= 3 && destination == 2 && mode is 2 or 3; ++ var memoryDestination = top is >= 1 and <= 3 && destination == 2 && mode is 2 or 3 or 4; + var moveSource = top is >= 1 and <= 3 && (destination <= 1 || memoryDestination) && !(top == 1 && destination == 1) && + (mode is >= 2 and <= 5 || mode == 7 && register <= 2) && width == (top == 1 ? 1 : top == 3 ? 2 : 4) && + State.ExceptionSequence == ExecutionBoundaryExceptionSequence && +@@ -202,7 +202,7 @@ + count > M68kAdvancedInstructionPipe.Capacity || indexedExtraCycles > int.MaxValue || (ssw & ~0x100) != expectedSsw || + kind == PendingReadKind020.TraceVector && width != 4 || + kind == PendingReadKind020.MoveSource && (top is < 1 or > 3 || +- destinationMode > 1 && !(top is >= 1 and <= 3 && destinationMode == 2 && mode is 2 or 3) || top == 1 && destinationMode == 1 || ++ destinationMode > 1 && !(top is >= 1 and <= 3 && destinationMode == 2 && mode is 2 or 3 or 4) || top == 1 && destinationMode == 1 || + !(mode is >= 2 and <= 5 || mode == 7 && register <= 2) || width != expectedWidth || + nextPc != unchecked(pc + 2 + extensions) || indexedExtraCycles != 0)) + throw new UnsupportedM68kTimingException(0x4e73, State.LastInstructionProgramCounter, _profile); +@@ -265,7 +265,7 @@ + else if (destinationMode == 2) { WriteFinalMoveDestination020(State.A[destination], value, size); SetMoveFlags(value, size); } + else WriteGeneralRegister(true, destination, expectedWidth == 2 ? unchecked((uint)(int)(short)value) : value); + if (destinationMode == 2) +- CompleteTiming(FinalMoveTiming020(opcode)); ++ CompleteFinalMoveWriteTiming020(opcode); + else if (indexed && indexedTimingKey != M68kInstructionTimingKey.GeneralMove) + CompleteIndexedMoveTiming(indexedTimingKey, indexedExtension); + else From ec3c285ded9c6c6fc256bebb3a58407f00d2ac44 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 23:25:42 +0300 Subject: [PATCH 181/211] Qualify private read candidate references and consumers --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 14 +++-- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 56 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 14 +++++ 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index cf184465..8931ef91 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -76,10 +76,16 @@ it does not replace or discard their remaining gaps. predecrement source-read recovery in focused execution: 3,244,032 passing cases / 520 reports, including 1,671,168 byte-identical retained cases. The new fixture and one-file review patch preserve the accepted decrement - across explicit RTE, including read/write/refault lanes. It requires separate - full/deep/API and consumer qualification; the running 259-input full audit - cannot qualify that newer change. Production import and wider protocols remain - open. + across explicit RTE, including read/write/refault lanes. Same-build deep, + eight actual rejection guards, compiled API and clean local .80 consumers are + independently qualified. Its own full suite has started with 8,132 names and + a frozen 13,945-report / 112,894,922-case expected catalog. Full execution and + independent verification remain pending; the running 259-input audit cannot + qualify this newer change. Explicit linkage: + `2e092d99a6116d1ebd0b95605b71c61d71c095d1f76c7b4ec511c95eb1db0915`. + Two later 040 diagnostic strings are explicitly separated from the frozen + inputs, with the same 480-case failing inventory retained. Production import + and wider protocols remain open. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index e8e6baea..8d4aa23c 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -11970,6 +11970,7 @@ boundary disagreements. No private C023 production candidate is imported and no package is published. Milestone 6 remains **in progress**, `roadmapComplete=false`. + ## 040 MOVE16 physical line-write recovery — 2026-10-08 Actual normal-space MOVE16 destination faults previously escaped through the @@ -13529,3 +13530,58 @@ private whole-request source-read recovery, not other destination forms, foreign frames, partial transfers, trace/interrupt or enabled MMU/cache behavior. Production CPU inputs remain unchanged; no private CPU is imported and no package is published. Milestone 6 remains **in progress**, `roadmapComplete=false`. + +### Predecrement read candidate integration checkpoint — 2026-10-08 + +The 260-input / 39-CPU read candidate has frozen source and assembly identities, +exact compiled discovery of **8,132 named rows**, and an expected catalog of +**13,945 reports / 112,894,922 cases**. Its complete suite has started on the +focused-run assemblies with ten qualified native presets. Expected outcomes +are 8,095 passed / 37 unavailable; these are not completed full execution. +The earlier 259-input full run continues separately. Neither is restarted. + +The source proof compares all **196 current test/project inputs** with the +frozen candidate: only the two 040 inventory diagnostic strings differ. +Case generation, statuses, flags and failure conditions are unchanged. The +frozen executions retain their own exact source identities rather than being +relabeled as execution of the later wording. Catalog preparation first fails +with a memory-allocation error before writing output; retrying the unchanged +command succeeds. A producer-adaptation guard also rejects a verifier-only +anchor absent from the producer; the corrected adaptation preserves all +execution/verifier checks and explicitly accounts for those distinct schemas. + +Independent same-build deep verification qualifies **320,000 seeded cases**, +**312,500 SingleStepTests cases / 125 files** and **536 Musashi programs / 88 +exclusions**. The input pins and reference caveats remain unchanged. Eight +actual invalid requests each fail at the intended guard before CPU execution +or coverage output. Separate-process compiled API comparison and independent +verification find zero managed public/protected signature changes: +**20 types / 211 records**. Physical layout and runtime compatibility are not +claimed from that comparison. + +The fresh local-only `1.5.2-synthetic-dev.80` package preserves all 39 candidate +CPU inputs. A clean `aa1dad5` CopperScreen archive passes the production build +with zero warnings/errors, **171 host tests / six unavailable**, **74 disk**, +**1,080 engine** and **two native Workbench 3.1 floppy replays**. Independent +verification binds the archive, NuGet-only dependency/cache boundary, exact +rosters, all three loaded CPU copies and unchanged ROM/media hashes. Package +version/build metadata gives its DLL a separate identity; CPU source equality +is explicit. No HD boot, throughput, live dirty-tree or hardware claim is added. + +| Newer read candidate evidence | SHA-256 | +| --- | --- | +| `source-proof.json` | `34a52560f78161238f20bfd567766d2dc236aa42e8fa72ac3fa11f2da1a3d958` | +| `discovery/proof.json` | `3d7f2f68db776e79838cdf04452795b3492b8e4f76caa24badce4e3162166d35` | +| `expected-reports.json` | `ade892d48f0ff53e7a91e1fd79c9bc952f1a23125b2af9466b19edb06f7ccf24` | +| `audits/PredecrementReadDeepV1/proof.json` | `6b990d51c3ce9b23e149280621641d50b2fd009aa54f8279b45a7e4c9f505186` | +| `audits/PredecrementReadDeepControlsV1/independent-proof.json` | `26cbb4c5fb24f4331d56f8448ecd6e71249c1b0574b62972ed49cd00c50a51eb` | +| `api-review-v3/independent-proof.json` | `f50d5c54a381de88143f15948aa25bcd5ca591c42284d2cf97456a7b9ac11744` | +| `consumer-80-complete-proof-v1.json` | `ca939d946871daac39b3bd6c0bf1791f0ef78081f9f6636be9d1821d6d6ffd95` | +| `predecrement-read-evidence-link-v1.json` | `2e092d99a6116d1ebd0b95605b71c61d71c095d1f76c7b4ec511c95eb1db0915` | + +Explicit linkage checks all 260 sources, shared full/deep/guard/API assemblies, +qualified reports and all 39 packaged CPU sources and consumer copies. Full +execution and its independent verification remain pending. Failed broad +references, wider restoration and unavailable hardware remain open. No private +CPU is imported and no package is published. Milestone 6 stays **in progress**, +`roadmapComplete=false`; PR #22 remains draft. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 4f94c185..09ac658f 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4219,3 +4219,17 @@ production CPU code is unchanged. Full/deep/API and consumer qualification of this newer 260-input graph remain required. The earlier full audit continues on its own frozen inputs. Broad reference, wider restoration and hardware gaps keep milestone 6 **in progress**, `roadmapComplete=false`. + +### Milestone 6 checkpoint — read candidate integration, 2026-10-08 + +The 260-input read candidate has exact 8,132-row discovery and a frozen +13,945-report / 112,894,922-case expected catalog. Full execution has started on +the focused-run assemblies. Independent deep/reference and eight rejection +controls pass, with zero compiled managed API changes. The local-only .80 +package passes the clean CopperScreen build, 171 host / six unavailable, +74 disk, 1,080 engine and two native floppy checks. Source/assembly/package +linkage and the two later diagnostic-string differences are explicit in the +qualification record. Both running full candidates retain separate identities +and still require independent full verification. Broad reference, restoration +and hardware gaps keep milestone 6 **in progress**, `roadmapComplete=false`; +no private CPU import or package publication occurs. From df9de9bb727f473b8ee31b376efa1c8e83c803ec Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Thu, 8 Oct 2026 23:56:39 +0300 Subject: [PATCH 182/211] Make private predecrement recovery audit reproducible --- .gitattributes | 2 + docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 14 + docs/COPPER68K_REFERENCE_QUALIFICATION.md | 54 + ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 14 + ...opper68k-private-predecrement-integrity.py | 148 + .../m68020-private-predecrement-manifest.json | 2382 +++++++++++++++++ ...m68020-read-write-recovery-candidate.patch | 749 ++++++ .../test-copper68k-private-predecrement.py | 182 ++ 8 files changed, 3545 insertions(+) create mode 100644 scripts/prove-copper68k-private-predecrement-integrity.py create mode 100644 scripts/reference/m68020-private-predecrement-manifest.json create mode 100644 scripts/reference/m68020-read-write-recovery-candidate.patch create mode 100644 scripts/test-copper68k-private-predecrement.py diff --git a/.gitattributes b/.gitattributes index b84a49dc..470426df 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,5 @@ # Unified patch context needs its single-space blank lines; keep evidence bytes stable. scripts/reference/m68020-predecrement-write-candidate.patch text eol=lf whitespace=-blank-at-eol scripts/reference/m68020-predecrement-read-candidate.patch text eol=lf whitespace=-blank-at-eol +scripts/reference/m68020-read-write-recovery-candidate.patch text eol=lf whitespace=-blank-at-eol,-blank-at-eof +scripts/reference/m68020-private-predecrement-manifest.json text eol=lf diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 8931ef91..a74385a9 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -86,6 +86,20 @@ it does not replace or discard their remaining gaps. Two later 040 diagnostic strings are explicitly separated from the frozen inputs, with the same 480-case failing inventory retained. Production import and wider protocols remain open. + A portable selected audit now reconstructs that private candidate from the + current 233-input checkout in an isolated 235-input snapshot. The combined + six-file review patch reproduces all 39 qualified CPU inputs; all 196 test + inputs match the current checkout. Its fresh execution passes all 14 named + tests and 3,244,032 cases in 520 byte-identical pinned reports, and strict + replay passes. Independent linkage proof: + `836e17c0f43e3565076aa8381320b704a5e21c106e6687544dbf1624d3dff263`. + This supplies maintained reproduction without the temporary predecessor. + It does not qualify the whole current suite or import the private CPU. + A relocated valid evidence copy passes and all eight copied corruption + controls reject the intended report/selection/method/assembly/source defects. + The original evidence remains unchanged; no new CPU execution is claimed + from those validator controls. Their proof is + `8d9cfea6562c99b9b7662b443fa0a57f004318291d8f525837fb842800da92ae`. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 8d4aa23c..7691d75b 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13585,3 +13585,57 @@ execution and its independent verification remain pending. Failed broad references, wider restoration and unavailable hardware remain open. No private CPU is imported and no package is published. Milestone 6 stays **in progress**, `roadmapComplete=false`; PR #22 remains draft. + +### Portable selected private recovery — 2026-10-08 + +The maintained command below reconstructs the complete private candidate from +the current checkout rather than requiring the temporary 259-input predecessor. +The pinned combined six-file patch is review material: it is applied only in +the command's isolated output directory. No production CPU implementation is +imported. The manifest pins the current 233 source/project inputs, all 39 +normalized candidate CPU inputs, the exact 14 named tests, and all 520 report +hashes and logical counts. Its LF checkout rule preserves the pinned manifest +bytes on Windows. Later source changes require an explicit new qualification; +the helper rejects a changed baseline rather than silently accepting it. + +```powershell +python scripts/test-copper68k-private-predecrement.py --output artifacts/private-predecrement +python scripts/test-copper68k-private-predecrement.py --validate-only --output artifacts/private-predecrement +python scripts/prove-copper68k-private-predecrement-integrity.py --audit artifacts/private-predecrement --output artifacts/private-predecrement-controls +``` + +Fresh `PortablePredecrementV1` execution passes **14 named tests**, with zero +skips/failures, and **3,244,032 cases / 520 reports**. The isolated snapshot has +235 source/project inputs: 39 private CPU and 196 current test inputs. All +normalized CPU inputs match the separately qualified 260-input candidate; +the temporary candidate's additional test files are absent from this snapshot. +All current test inputs match, including the later 040 diagnostic descriptions. +Every selected report is byte-identical to its pinned predecessor. Strict +replay passes and independent source/report/assembly/execution checks bind +this new snapshot. Its DLLs have their own identities; source equality does +not relabel it as the previous build or a full current-suite execution. The +isolated build logs retain SourceLink's missing-repository warnings. + +The integrity command uses copied validator inputs, without executing a CPU. +A relocated valid copy passes. Eight independently altered copies each exit 1 +for the required reason: missing report, zero-count changed report, empty test +selection, wrong loaded method, wrong assembly path, changed DLL, changed source +and extra report. TRX mutations update the outer output hash so method/selection +checks must reject the internal defect. Hard-linked immutable files are +detached by atomic replacement before alteration; every original evidence +file hash remains unchanged and its final strict replay passes. + +| Portable selected evidence | SHA-256 | +| --- | --- | +| Combined six-file candidate patch | `624a9134a9f88dacda50d15508f5418106a77da766c7051a53322fa079e13490` | +| Pinned manifest | `d7ade9a6020cd711f05a8e7b0ef6fd427b223a48c50239ac6949e3d83f85b06c` | +| `PortablePredecrementV1/proof.json` | `98cb43ae342c42e7e757df8c3ec302ac56f74a3a88a1a750017dcbd2d4aff381` | +| `PortablePredecrementIndependentV1.json` | `836e17c0f43e3565076aa8381320b704a5e21c106e6687544dbf1624d3dff263` | +| `PortablePredecrementControlsV1/proof.json` | `8d9cfea6562c99b9b7662b443fa0a57f004318291d8f525837fb842800da92ae` | + +Both broader 259/260-input full executions remain live on their original +assemblies and require terminal results and independent verification. This +portable selected result does not satisfy those full gates. Broad 010 RTE / +060 STOP disagreements, wider restoration protocols and unavailable hardware +remain explicit. Milestone 6 stays **in progress**, `roadmapComplete=false`; +PR #22 stays draft. No package publication or private production import occurs. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 09ac658f..570be787 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4233,3 +4233,17 @@ qualification record. Both running full candidates retain separate identities and still require independent full verification. Broad reference, restoration and hardware gaps keep milestone 6 **in progress**, `roadmapComplete=false`; no private CPU import or package publication occurs. + +### Milestone 6 checkpoint — portable private recovery audit, 2026-10-08 + +The selected private recovery audit can now be reproduced from the current +233-input checkout using a pinned combined patch, without a temporary +predecessor checkout. It runs an isolated 235-input snapshot with 39 private +CPU inputs and all 196 current test inputs. Fresh execution passes 14 named +tests / 3,244,032 cases / 520 reports; strict replay passes and independent +checks establish source equality and byte-identical pinned reports. Production +CPU inputs remain unchanged. The qualification record contains the maintained +commands and source/evidence identities. Broader full runs retain their own +frozen inputs and pending verification; the selected result cannot close +milestone 6, the broad failed reference gate or hardware gaps. Milestone 6 stays +**in progress**, `roadmapComplete=false`; no private import or publication. diff --git a/scripts/prove-copper68k-private-predecrement-integrity.py b/scripts/prove-copper68k-private-predecrement-integrity.py new file mode 100644 index 00000000..fbfd4219 --- /dev/null +++ b/scripts/prove-copper68k-private-predecrement-integrity.py @@ -0,0 +1,148 @@ +"""Exercise copied-evidence rejection controls; this command does not execute a CPU.""" +import argparse +import importlib.util +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import xml.etree.ElementTree as ET + + +HELPER = Path(__file__).with_name("test-copper68k-private-predecrement.py") +spec = importlib.util.spec_from_file_location("private_audit", HELPER) +audit = importlib.util.module_from_spec(spec) +spec.loader.exec_module(audit) + + +def replace(root, path, data): + # Atomic replacement detaches a hard link without changing the audited file. + path.resolve().relative_to(root.resolve()) + temporary = path.with_name(path.name + ".control-copy") + audit.check(not temporary.exists(), "Existing control temporary file") + temporary.write_bytes(data) + os.replace(temporary, path) + + +def copy_evidence(source, target, value): + audit.check(not target.exists(), "Control output must be fresh") + inputs = audit.load(source / "inputs.json") + execution = audit.load(source / "execution.json") + names = [*("source/" + n for n in inputs["sources"]), + *("source/" + n for n in inputs["assets"]), + *execution["assemblies"], *value["reports"], "execution.log"] + for name in names: + destination = target / name + destination.parent.mkdir(parents=True, exist_ok=True) + os.link(source / name, destination) + inputs["command"] = audit.command(target) + inputs["settings"]["COPPER68K_SYNTHETIC_REPORT_DIR"] = str(target) + audit.save(target / "inputs.json", inputs) + tree = ET.parse(source / "audit.trx") + loaded = str(target / "build/bin/Copper68k.Tests/release/Copper68k.Tests.dll") + for definition in tree.findall(".//t:UnitTest", audit.NS): + definition.set("storage", loaded) + definition.find("t:TestMethod", audit.NS).set("codeBase", loaded) + tree.write(target / "audit.trx", encoding="utf-8", xml_declaration=True) + execution["inputsSha256"] = audit.sha(target / "inputs.json") + execution["outputs"]["audit.trx"] = audit.sha(target / "audit.trx") + audit.save(target / "execution.json", execution) + audit.save(target / "proof.json", audit.validate(target, value)) + + +def modify_trx(root, action): + tree = ET.parse(root / "audit.trx") + action(tree) + tree.write(root / "audit.trx", encoding="utf-8", xml_declaration=True) + execution = audit.load(root / "execution.json") + execution["outputs"]["audit.trx"] = audit.sha(root / "audit.trx") + audit.save(root / "execution.json", execution) + + +def run(root): + result = subprocess.run([sys.executable, str(HELPER), "--validate-only", "--output", str(root)], + capture_output=True, text=True) + (root / "control.stdout.txt").write_text(result.stdout, encoding="utf-8") + (root / "control.stderr.txt").write_text(result.stderr, encoding="utf-8") + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--audit", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + source, output = args.audit.resolve(), args.output.resolve() + audit.check(not output.exists(), "Control output must be fresh") + value = audit.manifest() + audit.check(audit.load(source / "proof.json") == audit.validate(source, value), "Original evidence failed replay") + inputs, execution = audit.load(source / "inputs.json"), audit.load(source / "execution.json") + immutable = [*("source/" + n for n in inputs["sources"]), + *("source/" + n for n in inputs["assets"]), *execution["assemblies"], + *value["reports"], "execution.log", "audit.trx", "inputs.json", "execution.json", "proof.json"] + before = {n: audit.sha(source / n) for n in immutable} + positive = output / "RelocatedPositive" + copy_evidence(source, positive, value) + accepted = run(positive) + audit.check(accepted.returncode == 0, "Positive copied-evidence replay failed") + report = sorted(value["reports"])[0] + cpu = "build/bin/Copper68k.Tests/release/Copper68k.dll" + source_file = sorted(inputs["sources"])[0] + controls = [ + ("MissingReport", "Missing or extra required reports"), + ("ChangedReport", "Changed architectural cases or results"), + ("EmptySelection", "Missing, empty, extra or failed selection"), + ("WrongMethod", "Wrong loaded method"), + ("WrongAssembly", "Wrong loaded test assembly"), + ("ChangedDLL", "Missing or changed execution DLLs"), + ("ChangedSource", "Changed candidate source inventory"), + ("ExtraReport", "Missing or extra required reports"), + ] + results = {} + for name, diagnostic in controls: + root = output / name + copy_evidence(source, root, value) + if name == "MissingReport": + target = root / report + target.resolve().relative_to(root) + target.rename(root / (report + ".missing")) + elif name == "ChangedReport": + changed = audit.load(root / report) + changed["logicalCases"] = 0 + replace(root, root / report, (json.dumps(changed) + "\n").encode()) + elif name == "EmptySelection": + def remove_rows(tree): + rows = tree.find("t:Results", audit.NS) + for row in list(rows): + rows.remove(row) + modify_trx(root, remove_rows) + elif name == "WrongMethod": + modify_trx(root, lambda tree: tree.find(".//t:TestMethod", audit.NS).set("name", "WrongMethod")) + elif name == "WrongAssembly": + modify_trx(root, lambda tree: tree.find(".//t:TestMethod", audit.NS).set("codeBase", str(root / "Wrong.dll"))) + elif name == "ChangedDLL": + replace(root, root / cpu, (root / cpu).read_bytes() + b"changed") + elif name == "ChangedSource": + target = root / "source" / source_file + replace(root, target, target.read_bytes() + b"\n// changed control\n") + else: + shutil.copyfile(root / report, root / "68020-extra-control.json") + rejected = run(root) + audit.check(rejected.returncode == 1 and diagnostic in rejected.stderr, + "Control did not reject for its required reason: " + name) + results[name] = dict(exit=rejected.returncode, diagnostic=diagnostic, + stdoutSha256=audit.sha(root / "control.stdout.txt"), + stderrSha256=audit.sha(root / "control.stderr.txt")) + print("Rejected copied-evidence control: " + name, flush=True) + audit.check(all(audit.sha(source / n) == h for n, h in before.items()), "Original evidence changed") + audit.check(audit.load(source / "proof.json") == audit.validate(source, value), "Original replay changed") + audit.save(output / "proof.json", dict(schema=1, originalProofSha256=before["proof.json"], + helperSha256=audit.sha(HELPER), producerSha256=audit.sha(Path(__file__)), + positiveReplayExit=accepted.returncode, controls=results, originalUnchanged=True, + copiedValidatorInputs=True, newCpuExecution=False, hardwareQualification=False)) + print("Qualified eight rejection controls and positive relocation; no new CPU execution", flush=True) + + +if __name__ == "__main__": + main() diff --git a/scripts/reference/m68020-private-predecrement-manifest.json b/scripts/reference/m68020-private-predecrement-manifest.json new file mode 100644 index 00000000..0bab8019 --- /dev/null +++ b/scripts/reference/m68020-private-predecrement-manifest.json @@ -0,0 +1,2382 @@ +{ + "schema": 1, + "scope": "Private predecrement read/write and retained handlers; no production import or full-suite/hardware qualification.", + "baseSourceNormalizedSha256": { + "Copper68k.Tests/Copper68k.Tests.csproj": "312981fb35dc8af78a7b8e32ed5332d3dcadf184374c240448af5fbb1fa268e0", + "Copper68k.Tests/Copper68kTestBus.cs": "a097e924ddd25ac38cd38920bce3bef830b7c6022e33762add89f8ac4d457f3b", + "Copper68k.Tests/EnvironmentFactAttribute.cs": "419f7a9e25cdacbf6588b899c111288999c7c936a8486ebab6bfeac11529eafc", + "Copper68k.Tests/M68000CachedDbraInterruptFenceTests.cs": "8d5abf2a13af9a36561e7560d8f9e1ddb94afd32ddfe823c348b2c5f047781c2", + "Copper68k.Tests/M68000JitDirectZeroWaitTests.cs": "9637222279f77fd46640936d75ebd25a31da5d24800173476e317ed81b2ee6d8", + "Copper68k.Tests/M68000TraceExceptionTests.cs": "4599a73d61e68efc62c982f9abf2313bbdb2069690f2cb788d615244f22b070c", + "Copper68k.Tests/M68010InterpreterTests.cs": "6fbac278c5729fb31d6919f24c0bd3c406a5f8608f57ea07b9ad97cf7e950a49", + "Copper68k.Tests/M68020AdditionalAgaOperandTests.cs": "022269e754b7f2ee9d5aa865e4d6f24e913582e6af5e4e52c06d320c20bc29b4", + "Copper68k.Tests/M68020AddressSourceTests.cs": "5f90d7aaff3238acbe2b8cb5e38b82a571f59b5d2ca96f45efb52dbd9db86ab1", + "Copper68k.Tests/M68020BrianOperandTests.cs": "8e8af93ca91a94095e1c7bd21666c48197cf43748719d6b3e661f6125cd1d90e", + "Copper68k.Tests/M68020ChipInstructionCacheTests.cs": "759028d902e71583c49162cb678b2e5f09b9c3cad44facfa2f21577a3069557c", + "Copper68k.Tests/M68020CmpmTests.cs": "66ae2c196d8dcb363715d97f15b876232d2393ea4a5be0d06cfb577c2bc0b590", + "Copper68k.Tests/M68020ContinuationTests.cs": "632d09785081458abd880867d60526853f6598b6d567b257acc77866f89002a9", + "Copper68k.Tests/M68020ExpansionTests.cs": "7fc005e59ee5425073c9305ea66f86d0eb51e71a1da8fc3431be973cfccfe843", + "Copper68k.Tests/M68020FastRamBootTests.cs": "293aa6a05fc52e5cac3698f641970e0d46ebb3b05384e6cc880b726da2995152", + "Copper68k.Tests/M68020FullIndexedMoveTests.cs": "12812dc140954206bee3d5d233cb4a61fb3dd655e78bda39fe61f8c0f15364af", + "Copper68k.Tests/M68020GameContinuationTests.cs": "78887fb291011a4d1499e53d76a2806ed746586084d7db8e13b49d6aa8b45e78", + "Copper68k.Tests/M68020HdfBootTests.cs": "45cb463d20e580dd1688d69098dd4dcf3455c3a1139aebd8bf7ed37db99d5930", + "Copper68k.Tests/M68020IndexedJsrTests.cs": "e2962592f981f76bb4222777dfd568e9c15faa0941a280dd121f5a6da05ef117", + "Copper68k.Tests/M68020InterpreterTests.cs": "4579066275f830f3959065e2404edec22790d7c338994a1fb595a5b1d4964688", + "Copper68k.Tests/M68020NativeGameOperandTests.cs": "0b1880b05941913b16062c81a2110b69c7374abee598b2d60db9282044b7962c", + "Copper68k.Tests/M68040CacheControlTests.cs": "3dd0c7219624c5184a066452265042d6443f1adaf17de6ff7be1b04e2eb0cae4", + "Copper68k.Tests/M68040FpuContinuationStateTests.cs": "db9c79aea818cf7adb5ef52012a9ddb7ef53b95336e80b2e329ddaa1cf8ab495", + "Copper68k.Tests/M68040FpuTests.cs": "54fdc51597d6fd8a146d2ba9563f11395c44130b5c193d51ffac19edcd599b1b", + "Copper68k.Tests/M68040InterpreterTests.cs": "8dfd53cf9af488256834a5aaefd0e926c9cc1d519b1373aed398e96151f36708", + "Copper68k.Tests/M68040MmuTests.cs": "1f1f4d12072cb80236bc10cc9f1df335d7f3ffc20d373ff6972a788e5dfd1bcb", + "Copper68k.Tests/M68040MovemContinuationStateTests.cs": "46ab523a9152f1017ef6ec613637807e57da5d2dfd58e6ef7aca32c0c081bcb4", + "Copper68k.Tests/M68040OddReturnStateTests.cs": "c43044017ab1f664a82d0e94ffe7906f5a99d1766c8880d9ea07d46657b40e8e", + "Copper68k.Tests/M68040UncachedFetchTests.cs": "698e4fe59ee62c862a9abd83d5f03435110dd3dc2e734130d709df6232caac18", + "Copper68k.Tests/M68060InterpreterTests.cs": "969ae07caf3cfa001a4933ac8a4fb1fcb4d3ef3b6a3dc43bc2a1802c61fa2ae8", + "Copper68k.Tests/M68EC020InterpreterTests.cs": "0a5ba9a009f0766255a52c8b5eb2c04f238fc70d6295905cbea7fd6d1f23a0a7", + "Copper68k.Tests/M68kAddTests.cs": "5e3b5021c86c7fec4cc9faf207823835523cd6e07a46fc755e26955642d21662", + "Copper68k.Tests/M68kAddressBusWidthTests.cs": "3fd32d1e180fa09980f7c2e14c43345aeca52ac69d51bc9585c9050bb10495fb", + "Copper68k.Tests/M68kAddressErrorTests.cs": "d845279ac810f6ae6da5505c8c193509754c27a900ba34b1f7e3c15bc50d1e8f", + "Copper68k.Tests/M68kAdvancedFastPathTests.cs": "34da2dad29e285051b9db5dad7b1db5b90b1fb8e929d483c4619ef8fc48fea76", + "Copper68k.Tests/M68kBcdTests.cs": "f9a9ec702a34201baa4fa657d7222e07b286a4dad991102bc7c40bfed397ed2f", + "Copper68k.Tests/M68kBitTests.cs": "e4718e2e229b682b688d8a091a3c4c106f5382df41e593c95e24741d22250829", + "Copper68k.Tests/M68kBranchTests.cs": "8565ba85c89eab8b4967fd649cfc7cacb2722d9cfe07535023987d42a990a1e6", + "Copper68k.Tests/M68kChkTests.cs": "0a1f6e684375c83a7c818631385c198a6f05c1897d19b2110e7063c47166f4bf", + "Copper68k.Tests/M68kCmpTests.cs": "05baa5db6ddb2ed27f420944b7f13f7d00c6a737bf7d1d4d2d33dca0a1940f61", + "Copper68k.Tests/M68kCodeSnapshotTests.cs": "18ef7fa8414ed1d35f96b8846a36e5f41b2decc86d50ee2e25a9c9a959ad8497", + "Copper68k.Tests/M68kDecoderTests.cs": "ece24606f04b2fffded6867dfc3f8bc898f422c442f046130eff89e001bb834b", + "Copper68k.Tests/M68kDispatchTierTests.cs": "a277d5c94679bfe5dbd9bbe604ad8b0f02348cb887d91c38eb14c3d4a9c57235", + "Copper68k.Tests/M68kDivideTests.cs": "ec9bfaf0561512acaadc646f09981d06d28db463528fc5d9bea18f9842392800", + "Copper68k.Tests/M68kInstructionFrequencyTests.cs": "fc23174f22587068f574ba77b29273ad52fa10c11023dbf4e19e388c831fcbf9", + "Copper68k.Tests/M68kInterpreterCoreBehaviorTests.cs": "0d5b94828064e1237c0e2c99b3174a1119cf197820f13d48d11d69be1909a9a0", + "Copper68k.Tests/M68kInterpreterHierarchyTests.cs": "ee2a78c5c35b8fc94c578cb3fd848b041ac7cc8bc4ca2c9275d4825cf1a6ebc2", + "Copper68k.Tests/M68kInterpreterTestHelpers.cs": "9ab827d132947331429fa2febd23d513ae9dbc92381507b9c21c35989684e0ca", + "Copper68k.Tests/M68kJumpTests.cs": "38e5f539c3b05b8a06aff0465175adf7dc1d1e6e3bc5d186d7aa377960c243ce", + "Copper68k.Tests/M68kLogicalTests.cs": "344102f71fa3e65c25c738b5986534b78764e0337bee068568d751022deabd2b", + "Copper68k.Tests/M68kMoveTests.cs": "09bf9857b506c9897909fe91637644a101d96259560117d85dec9e59778ed64a", + "Copper68k.Tests/M68kMovemModelIsolationTests.cs": "36c35951eaf2a3e2af0c2f47e43a242c3dc9b03073d736786b312e296f5cb528", + "Copper68k.Tests/M68kMovemTests.cs": "ad33ede82e2dfbb83609e2965a812a8b5198a2a53a189c15f38a2aa175d30431", + "Copper68k.Tests/M68kMultiplyTests.cs": "81ded2574ce3b74f4b7c9b278a546fec00beee6cae1bcedff9eeac554ddc9f19", + "Copper68k.Tests/M68kMusashiConformanceTests.cs": "c35416f212c8e9de50592b73a7c942b125d3678d4371efa8a39db85cc09a1c35", + "Copper68k.Tests/M68kMusashiModelAuditTests.cs": "2f8842d9de580b9ab24287356dd0bd081f8f9565aaa18c1805c3be7056d9b818", + "Copper68k.Tests/M68kOpcodeMatrixTests.cs": "512cc6dd8d13c908e3143b9f901e7d7954d301aebbe170070cbca848b4be2b50", + "Copper68k.Tests/M68kPlanTierDifferentialTests.cs": "021e529f2eb249809ed4250c83a17d21561e4bd698ba7609b6045da85b0a8632", + "Copper68k.Tests/M68kShiftTests.cs": "275d017006f919a8b3ceffa5586540731f6a26e94826451496a67da2a7f44b15", + "Copper68k.Tests/M68kSingleStepConformanceTests.cs": "fc93b034b7ad2745b0ce228f3d02d99e55d0fdc51efb671908d957853597fa94", + "Copper68k.Tests/M68kStoppedFastForwardTests.cs": "d2f7ed39b80e27320292b1ba61f86e5f2870ed8e951cefa7c0d2283c8379b96c", + "Copper68k.Tests/M68kSystemTests.cs": "1df6f61c5729635e5b1952dab0e0db5c17a116dcca0dfd3808d4f5feffa0742a", + "Copper68k.Tests/M68kTimingFormulaTests.cs": "f22eb91b684b39aacf15d57ff6d43119aecc502861412390d08f26067e693caa", + "Copper68k.Tests/M68kWinUaeCacheEncodingAuditTests.cs": "03f162f668addd29c4e62795d9256505b4ba651ae98ce21d0af516e4027e78b3", + "Copper68k.Tests/M68kWinUaeCas2AuditTests.cs": "d8b1df312e26bfb6cc05c3d91203d2549ab92405ee81ba55b35c3d82dd4c72ca", + "Copper68k.Tests/M68kWinUaeCasAuditTests.cs": "d4c5d7cbf81fce9164b010f57caed441d13dd7403399358ef63ba825c4199cd3", + "Copper68k.Tests/M68kWinUaeCpuTesterConformanceTests.cs": "efe49abb9610adf6b7d8d344b1b91d53d821b9349c3a7736685efba8b152688f", + "Copper68k.Tests/M68kWinUaeInputValidationTests.cs": "cbaabb136e9d7db37f386d336a2008956c1878a3f85a6a4ead0087c2c8644c57", + "Copper68k.Tests/M68kWinUaeLongArithmeticAuditTests.cs": "927162ff30aebfb5d253e6987c2c686fbb97d1ecd82185fbf53a5cdefad0ab7e", + "Copper68k.Tests/M68kWinUaeLowPowerStopAuditTests.cs": "0f6d06344b6b8c40ee0a41a49df7774e6878ad4195a64ec9ca87dc80e55fb91e", + "Copper68k.Tests/M68kWinUaeModelAuditTests.cs": "ab18f5640cad70d9bb1cd355a3a538b199f7dd652f677bf109202d6ecf1a04b1", + "Copper68k.Tests/M68kWinUaeMove16AuditTests.cs": "a36b17f83fd67b3ff06de5f131739f96c7aef23657a1af21b12020094191a3d4", + "Copper68k.Tests/M68kWinUaeMovesAuditTests.cs": "9754a27f8d461fdf776e57184ddfcfb9dca94c4af4a42a7f0743030c8860a6e1", + "Copper68k.Tests/M68kWinUaeTrapBoundsAuditTests.cs": "47a3ab6f85bd6b9c89087edd7ad1367310aa3df67ee936432e26cb3b15e94671", + "Copper68k.Tests/M68kWinUaeTrapTraceAuditTests.cs": "16f2402a7c08940e4145ea210212cd941d0742434c94ad9d8c15e5162379540f", + "Copper68k.Tests/M68kWinUaeWordDivisionAuditTests.cs": "02640e5bc67dd136f1fefa8f6fae58df332cf91a8d52cef0e97eeaa0c1004195", + "Copper68k.Tests/M68kWriteOrderingTests.cs": "647d696811143182659a9c1333894504aaf49761595a278c3315e2a8b54df614", + "Copper68k.Tests/MoveByteImmediatePostIncrementTests.cs": "75d6e18e081ac2a2617bec2e66275b0ba9d3574d52805ca5835990f0189e3d71", + "Copper68k.Tests/MovemAbsoluteLongExecutionTests.cs": "7db15ba7e91382c7c635abcfd792fa9e93157e5d245052f2b0b0c3697691c679", + "Copper68k.Tests/Synthetic/AddressingFixtures.cs": "bdd223021245fc1e0df644e0790d728d131daf4b240e84f93fe526e3d6374ff7", + "Copper68k.Tests/Synthetic/ArchitecturalExpectation.cs": "0b882f6bdd81e0997c9cb0c10dbf136b64e4682296e5ac76a5f29eee2d2baba9", + "Copper68k.Tests/Synthetic/ArithmeticSpecification.cs": "a898f60fa259b0c6124da0fe6353cd2508908d17d2bd24b79f2523e93b503bb3", + "Copper68k.Tests/Synthetic/ControlSpecification.cs": "c7eecce18ec7ea405b1e1c27ea339dad8219cb24e457d5d48385632705253e51", + "Copper68k.Tests/Synthetic/IntegerInventory.cs": "9e488d6bdac9afa748e81bad281e4227bd4ba3135836bc80fcf99bf3eee84937", + "Copper68k.Tests/Synthetic/InvalidOperandScenario.cs": "c93f0b7df5183294fd892dceefb49c31812149cb19c9c527042b453175529176", + "Copper68k.Tests/Synthetic/LogicalSpecification.cs": "e78b87680e28e30ef9dc59cbdbd3dd333c1c4ab0648962efca6cdcce9ca96f7c", + "Copper68k.Tests/Synthetic/M68010RteFormatDiscoveryTests.cs": "00895386634fff0bbc583d57bee96db86cc405979825be33f17407664eabf408", + "Copper68k.Tests/Synthetic/M68040CmLifetimeDiscoveryTests.cs": "de5569b32b1998505b01605d3952d2cac82119c33fe9684f93f491d7dab556a0", + "Copper68k.Tests/Synthetic/M68040MixedEpochRetryDiscoveryTests.cs": "8b059fb83c71dd2e758565aa1c47b4ae92e697feef925e937f0c90d2621176f2", + "Copper68k.Tests/Synthetic/M68060StopDiscoveryTests.cs": "727010c363a4fa9481ff2bc32d23b5ca0386c3aa8ecf73c3e07515af94dd18ee", + "Copper68k.Tests/Synthetic/MoveSpecification.cs": "9af8fe887088bd23fb5a56e784ae705b4170ed0f241ad052f23e752bc45d74c3", + "Copper68k.Tests/Synthetic/SyntheticAndIndirectTests.cs": "83637399d652f79db89c00a4c3c1422684bed32602f5a8ef54d3648a549e889e", + "Copper68k.Tests/Synthetic/SyntheticAndiDisplacementTests.cs": "a7a91b0182b70ed5a4e3c470acd76e8476b4dcad7da535a7cd6c704e4966889e", + "Copper68k.Tests/Synthetic/SyntheticArithmeticTests.cs": "03615ee297bb765eca939d8e9e4a6aaaa6f0e07692852647afc2602063a706cc", + "Copper68k.Tests/Synthetic/SyntheticAtomicTests.cs": "f6a6f7fc2466fbcb9554bb2df45aed8a45c1a372b308a60b6ab4173178106b34", + "Copper68k.Tests/Synthetic/SyntheticBitFieldTests.cs": "58e01a76ebfc9586214d251f98540382538c9ba2e0efb5a2e6e1d799689c7823", + "Copper68k.Tests/Synthetic/SyntheticBitShiftTests.cs": "f077c1ccc01522271318fdcd4a43b11220ab3debc8720bdd627309e304c33a83", + "Copper68k.Tests/Synthetic/SyntheticBoundsTests.cs": "ffb1679a76fc344aa53cdeca082345bc3e11819ff8750b82362bae1aeb53c1cd", + "Copper68k.Tests/Synthetic/SyntheticBusControlTests.cs": "e1f5a13110b22b8eafdf8d423ea37b61c8202b6a2dd9d705a18c5a2510b04d49", + "Copper68k.Tests/Synthetic/SyntheticCacheEncodingTests.cs": "a3c96aa137ee85e159fcf99ed55cf877c80292a39cc4abee7309c021acf1df8c", + "Copper68k.Tests/Synthetic/SyntheticCmpaDisplacementTests.cs": "a5c3538d3b3126bb12ca9256830b1f9613ccbac6b6822c4d5d5ac0e67dade7d6", + "Copper68k.Tests/Synthetic/SyntheticControlTests.cs": "55037302bedf9657ce1c730abe2b226b25e21bd7a4c01b9bae5111d34cb35b8d", + "Copper68k.Tests/Synthetic/SyntheticDebugInstructionTests.cs": "a67ff5029d531e0847b87807e361e8d697466d76428c40234f8b35ab884716a9", + "Copper68k.Tests/Synthetic/SyntheticDeepAuditTests.cs": "46cfdc181702386f9d5d56a1ec3c396ec82810f90d5a135511cd4d133a8fdb45", + "Copper68k.Tests/Synthetic/SyntheticEorPostincrementTests.cs": "e9df29be9467e97efb5a3621ee00f71459c5b6220b3cbf9f84d31a95fe20be3d", + "Copper68k.Tests/Synthetic/SyntheticExceptionControlTests.cs": "9e9248c26a858c49710ea51da303e49764979192dd2adae17b343dfa0699acdb", + "Copper68k.Tests/Synthetic/SyntheticExecution.cs": "60f4cdb0e8e3044c0594a4378e4af9779b83c4dab8ec00d8e5e78e6d23ff1bef", + "Copper68k.Tests/Synthetic/SyntheticExgRegisterTests.cs": "baa2dfd8f0ad030dcb69ff4b6d60992a7574415cd4804edc6df1e85cab202979", + "Copper68k.Tests/Synthetic/SyntheticExtendDecimalTests.cs": "9c26bec55e717d1fc1dc02ac6b94dfd999b99a1bdb725b45f290dc9600f9758d", + "Copper68k.Tests/Synthetic/SyntheticInvalidAddressOperationTests.cs": "2d95941ed986e8bc0ab79158396582c2e30e33e95aa80480145852bbcb73e29d", + "Copper68k.Tests/Synthetic/SyntheticInvalidArithmeticAtomicTests.cs": "db9b484676f8278aa2ca9b1a57c93871ccabd13bc1ff6c5ae86937d28b3c7827", + "Copper68k.Tests/Synthetic/SyntheticInvalidBinaryOperandTests.cs": "b65b0f1cf6f40334d3fc88a0e1a22222a7a3f60eaa6bfc13b656d06d5ab9e944", + "Copper68k.Tests/Synthetic/SyntheticInvalidBoundsBitfieldTests.cs": "3f1cc9e1379f3a797b1a109b2bf737d93a58da10e8237394017bd7b9a777a446", + "Copper68k.Tests/Synthetic/SyntheticInvalidLogicalTests.cs": "e60d00feac370e31820c89ba77a747e68f6629a09c98cefa93d18c2425e6bade", + "Copper68k.Tests/Synthetic/SyntheticInvalidMemoryShiftTests.cs": "5162c069b8a2cb49fbdda5c05bd7abaa934ec5163e410708548480b642b9efc2", + "Copper68k.Tests/Synthetic/SyntheticInvalidQuickConditionTests.cs": "8edb9df20ece5dcc2f11d6e05b6a279b77951a99dad8a6eec5ef3ae2a6619e74", + "Copper68k.Tests/Synthetic/SyntheticInvalidSystemTests.cs": "43c2f84f04c2715915ccad8280f4f046de563298b6985a6a681b1cbe5edb5a15", + "Copper68k.Tests/Synthetic/SyntheticInvalidUnaryMultiplyDivideTests.cs": "bb5e6add68535fbf459442e0bae51114000248d73ddb148a3132b23beec379d6", + "Copper68k.Tests/Synthetic/SyntheticLineFEncodingTests.cs": "afad4a7ab59d8565b5d0dd673d80bdb63afaf13dc0bd238b72730e71223d67a8", + "Copper68k.Tests/Synthetic/SyntheticLogicalTests.cs": "1c27fec573c732576d042a1a0037c97081c0ddbf21f2fb3bf2f585f96465cb5e", + "Copper68k.Tests/Synthetic/SyntheticLowPowerStopTests.cs": "61a87aa642ad1e53216ddc7189d93c05e8533b04e7466cb1c7e165049928fd77", + "Copper68k.Tests/Synthetic/SyntheticM68000DoubleFaultTests.cs": "9624c41ac9f9e646c7c6b84a7a61a03a3406b720dc9863676912e71dd462a007", + "Copper68k.Tests/Synthetic/SyntheticM68010Format8Tests.cs": "b9bf810644c5b0f2c908a2ee3c300bd8de8906e1d261f22b3b5aeafff47733be", + "Copper68k.Tests/Synthetic/SyntheticM68010LongMoveRestartTests.cs": "cca94e3314d691c963f4aca355943687deade26dd0c2f1d1383815d2c4ae05b3", + "Copper68k.Tests/Synthetic/SyntheticM68010MoveRestartTests.cs": "485c6f193e858bb27f5af4e692af5b58e46e41ebb94b06840563eed55406151b", + "Copper68k.Tests/Synthetic/SyntheticM68010MovecTests.cs": "e41dd92e251f5c9b84a71e6ce8953ce0ab6b2654a05ea325ad9936702bcd22d7", + "Copper68k.Tests/Synthetic/SyntheticM68020AddressFrameDiscoveryTests.cs": "e60b54560d6aabc43f3f5c02c35366e8f7490c0f26498a1b9177c57cc089611f", + "Copper68k.Tests/Synthetic/SyntheticM68020MemoryDestinationRecoveryTests.cs": "bf15b553122fc37c846f74230ff6eb069b4a3d07020966fa112b21257808a491", + "Copper68k.Tests/Synthetic/SyntheticM68020MemoryReadWriteTests.cs": "423ef41897c53c59d849cff7df45606802cfa526133a6ef8ca37f2d0276dd212", + "Copper68k.Tests/Synthetic/SyntheticM68020MoveOriginEntryFaultTests.cs": "554ab3cec9b71eef1470996a524a0865efd1f2154677c79d7a4162b1cbce9c96", + "Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteChangedInputTests.cs": "1c58a6384f334714c2825f24cd0f46948c05fcf23ea1954a07bb92b54b50c87f", + "Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteEntryFaultTests.cs": "332d29f2edfcc5aa216b05563e33ef91bbd9c7efbfc67a99e1fba101ccb3eb8c", + "Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFaultTests.cs": "f3b9c74338137347907b09a2cef42ede2825b2010658a0243b5e2d348bfea474", + "Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFrameFaultTests.cs": "9d764420d2f76bf2402adfd2d58a0588d18a25827963da647e8e9c15f912cd94", + "Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteFrameTests.cs": "96298919fd26e35754c44b7e5949a2522685de24272141974c3754b218c90740", + "Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteHandlerTests.cs": "a7090579d8f738be07a03eb5bd01ef1e0dfc35c05204c8fda5598e965716c4bf", + "Copper68k.Tests/Synthetic/SyntheticM68020MoveWritePipeTests.cs": "160fdaa629acb3ca6ff12b03d88c586beba08bea9e0f7f55fee1631554569b50", + "Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteRefaultTests.cs": "d3b136290411f027f9f89960e64b63fd9e0b6c82bb844d4a5734a0d396aabaa5", + "Copper68k.Tests/Synthetic/SyntheticM68020MoveWriteValidationTests.cs": "fe282ee21603d26aa42d85094ab053895ecbf390eb39a57c01ce30d482597462", + "Copper68k.Tests/Synthetic/SyntheticM68020PredecrementReadTests.cs": "5427d03ab43d753a2307ea2a41e83ed5f3325f20f9c83d27d3e0574768843bed", + "Copper68k.Tests/Synthetic/SyntheticM68020PredecrementWriteTests.cs": "129c13cf4549091eacc6a6334534f0be3497003500a33373b9463c0b72b6b770", + "Copper68k.Tests/Synthetic/SyntheticM68020PrefetchRecoveryTests.cs": "10953cc31cfaeea328e154f94381b43966a9c31d82beadfe001206b2669f6a77", + "Copper68k.Tests/Synthetic/SyntheticM68020RteAddressTransportTests.cs": "a79b1c10097a44454dd26aa677c288cbd944d359328fb090593b440f23090cc3", + "Copper68k.Tests/Synthetic/SyntheticM68020RteFaultTests.cs": "52f52a95ac6346964988c0df8d59341302e1c2066205ef7e308d6e7124ff83e6", + "Copper68k.Tests/Synthetic/SyntheticM68020RteMetadataTests.cs": "dc6945345b3bef2840ee2d2360e047bde9a5ec6eaccd935cb04e744680f51b9d", + "Copper68k.Tests/Synthetic/SyntheticM68030PmmuPrivilegeTests.cs": "b2208dd5e3c130f02e4c8ce96d732bfb83d18cca2c91649a501b870d4d5841f5", + "Copper68k.Tests/Synthetic/SyntheticM68040AccessDoubleFaultTests.cs": "4d2da25611284027c5841aecf71493abfbf10a8dbb3735d7b11e436961bdec68", + "Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs": "9f3f7ca9fdd96d56c87a3d00938e1a9a05e0b427d147cdbceef46735373336b7", + "Copper68k.Tests/Synthetic/SyntheticM68040BatchFaultTests.cs": "9b74e0208635b1bdb694afbb06d01a09ee0d0f31715817989dddfacdd0b579e9", + "Copper68k.Tests/Synthetic/SyntheticM68040ChainedOddReturnTests.cs": "3fbee0ebaf25ce06641ef03d6ee07ebf293f13fe5f19fd7cbe670745e3f9829a", + "Copper68k.Tests/Synthetic/SyntheticM68040FpuContinuationTests.cs": "ab3aba621e9c590ee47ec27b486398bd5ff3d2d3fc6d0f03598c78442f2d0ef2", + "Copper68k.Tests/Synthetic/SyntheticM68040HandlerPrefetchTests.cs": "5826e8304602df3dac94048eb73f80608fa2c1ec0f080dd052532c067d5dfbf9", + "Copper68k.Tests/Synthetic/SyntheticM68040InstructionFaultTests.cs": "80914da4fa41dffaaff04fd2070a2730937d300a8e9497fc1f54413bda20cb6a", + "Copper68k.Tests/Synthetic/SyntheticM68040MmuInstructionTests.cs": "247e44c5ba0116ed393624a0b1921b724f35ae11d830375ea449786fe6f6f1e6", + "Copper68k.Tests/Synthetic/SyntheticM68040Move16FaultTests.cs": "da4d61987b162ddf6f3bdd2fc13d15f2a2b87260042edaed0710f25c20795625", + "Copper68k.Tests/Synthetic/SyntheticM68040MovemContinuationTests.cs": "8bf800c5c10d69f3b88ad9944dd75b598abd36a1e8c02418bcfa34f1490d2e4b", + "Copper68k.Tests/Synthetic/SyntheticM68040MovemMaskFaultTests.cs": "51f6acedd3beec8d008559cda54489a108d4bd8ff4bf7cd3205848e55c8a2237", + "Copper68k.Tests/Synthetic/SyntheticM68040MovemReadRecoveryTests.cs": "b01b57d372f95f2ff825f0ca992b4c4571f757b876e44d09a3e6075562e4fc6e", + "Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteFaultDiscoveryTests.cs": "6c7ccea7523a87a612e693c9132da170e8abf4a237226be0bdfe9c8fd987101e", + "Copper68k.Tests/Synthetic/SyntheticM68040MovemWriteRecoveryTests.cs": "3e25ad2091d5a6b90818b9aa14935e62f3ebef75ed5f15e880d9e40fb80e34b8", + "Copper68k.Tests/Synthetic/SyntheticM68040MovesFaultDiscoveryTests.cs": "9f9fcc646b2a093477e4825c584138ad878f8ba52559a314f714dc3c9d8ea9e8", + "Copper68k.Tests/Synthetic/SyntheticM68040MovesReadFaultTests.cs": "18cd5b985be7fb566995c3799562228b9fe56b3d0710134119d8ca4ae48fca3e", + "Copper68k.Tests/Synthetic/SyntheticM68040MovesWriteFaultTests.cs": "c0b786b5c5ff98af96ee0e96991d76173d7d04e3ac591ab900075eb043231b13", + "Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs": "ed932469f093927943f726b0250eb27bf1bad5634734d0d974cc659ddc02713d", + "Copper68k.Tests/Synthetic/SyntheticM68040OddReturnTests.cs": "e09eba013db34fb576c9cae7637389eb040bee33f7723741a916b0634cf9749c", + "Copper68k.Tests/Synthetic/SyntheticM68040OperandReadFaultDiscoveryTests.cs": "a2b74358cd457607d2e1a2a8c6945342a80cd9543c5385b072bd8078d42019a4", + "Copper68k.Tests/Synthetic/SyntheticM68040OperandWriteFaultTests.cs": "1ccefa627a5333f44262441f11088e9acb074ebfe7bd242455990920db38b74f", + "Copper68k.Tests/Synthetic/SyntheticM68040RteRepairTests.cs": "f8df7d6ee9e29ead38696b70193f766ebe40a25f393cf5914a6668a68fbe4ac5", + "Copper68k.Tests/Synthetic/SyntheticM68040RteValidationFaultTests.cs": "a20a53696e3d59aae3112ccda4af1101817c33b513f49b5c8dbf313ced12d933", + "Copper68k.Tests/Synthetic/SyntheticM68040SoftwareTraceProgram.cs": "631b91374fd570b8d848d2e479e9c3db9faa33dd16bd41388f3e0dc45988250e", + "Copper68k.Tests/Synthetic/SyntheticM68040ThrowawayTests.cs": "fe301f287b45a95dd00e30fddba19f313c1a34d42fe37235f8c12a29382f8cf8", + "Copper68k.Tests/Synthetic/SyntheticM68040UserRteFaultTests.cs": "30df0643c9d8ed668805552a86a273aef96682885bf33eea7365fb364e201aee", + "Copper68k.Tests/Synthetic/SyntheticM68040WritebackProgram.cs": "5b44b67e305aeca279c552c570eacc1d65cbecafcdc8c105415e802fbc6d0563", + "Copper68k.Tests/Synthetic/SyntheticM68040WritebackTests.cs": "7381fdff0a0f8655f04edb18a2cf5069922fdaaae9adf64c1d7665efe08642dd", + "Copper68k.Tests/Synthetic/SyntheticMachine.cs": "03342a838cb5862e622285350b5615d94b08bdfd0ef7074882b4390026261b59", + "Copper68k.Tests/Synthetic/SyntheticModelSystemTests.cs": "61c2227cd4165505b6cdc115ffbd9874fbb0c62101dbc0ff984109b74a045727", + "Copper68k.Tests/Synthetic/SyntheticModuleTests.cs": "de1ff9a2fc78ba198e47b07fb71d06368116428c71ad8fd34c68fa995ee68bb1", + "Copper68k.Tests/Synthetic/SyntheticMoveExceptionTests.cs": "99a156eb3c1b4c45989db4ef9eb757606439dba4d1e48730a331036034d15382", + "Copper68k.Tests/Synthetic/SyntheticMoveTests.cs": "669d5c70c9cf8f1128f2274d27ad6297c22694f0ca54ecba720636a63f926f85", + "Copper68k.Tests/Synthetic/SyntheticMoveaDisplacementTests.cs": "3c7a704c0035442ee6bb3d14f6a609dfb95fdfd0c377d89d930541512be15237", + "Copper68k.Tests/Synthetic/SyntheticMovecControlEncodingTests.cs": "8d243c796b604cd7c6d47a11bd4b2fc02182922e6eb01fbd737ac299c1adca33", + "Copper68k.Tests/Synthetic/SyntheticMovecRegisterFixture.cs": "e395c928f2a0f5c62350d3f86072baef6340b389f0e6eb92e433fc94684bdc5e", + "Copper68k.Tests/Synthetic/SyntheticMultiplyDivideTests.cs": "f6f5edd725d2eab5a612dceb974f10a4ec52e1669e8c3a91bc9724076ae43de9", + "Copper68k.Tests/Synthetic/SyntheticNotDisplacementTests.cs": "33fa6019413337ff67f3884e8c32c867ffb1a201377e56a804d3ead5a3836038", + "Copper68k.Tests/Synthetic/SyntheticPcrTests.cs": "ce06df810c1c4ece8aaf5132e429c60ac98ef3731d36c86e909073c41ab7fdb3", + "Copper68k.Tests/Synthetic/SyntheticReservedMove16Tests.cs": "b22226f77f16cf1ec838ae3fec53963a1dbd2659c85af2ef2b42e4cf2d4d958c", + "Copper68k.Tests/Synthetic/SyntheticSubaIndirectTests.cs": "9cc0e770e97b6d5b6d3254aad268c321f37b20819999fde54f75623af8672ca2", + "Copper68k.Tests/Synthetic/SyntheticSystemTests.cs": "c95bebeaaa9692d1ee56e8e7dd1b97571070deef839fa89a44dd850a01340b16", + "Copper68k.Tests/Synthetic/SyntheticTransferTests.cs": "3454c5c21143a9eaf70257bcba1b992af6574ee2a44de29739f9ab798851d4f5", + "Copper68k.Tests/Synthetic/SyntheticTranslationControlTests.cs": "c93eee77197dbfcd6e9d6f18b103628d55b67ae08d42685b8ba46e41d4553b5d", + "Copper68k.Tests/Synthetic/SyntheticTransparentControlTests.cs": "c983f09745425abc7f8c0c4998ab12f60f2c564101b46fd1ed220a37f65a8729", + "Copper68k.Tests/Synthetic/SyntheticTrapTraceTests.cs": "7576393e17dcf1afc3172b9d187e6e39d66a515e70420ac79d181c99fb26397a", + "Copper68k.Tests/Synthetic/SyntheticUnassignedFourLineTests.cs": "c6311d02ea4866dd20a1e95bb8ac458c9425b9425ee0122ea610bdba5f216f41", + "Copper68k.Tests/Synthetic/SyntheticUnassignedMoveQuickTests.cs": "6533833d88cbc244df36dbcde1753a6a4aed7836f9cf4563924c82c2626db05d", + "Copper68k.Tests/WinUaeArchitecturalFlags.cs": "ea0c66b81d231066c0fd3930e7b1ae44744ddc0b8da3183254870902cc31afe2", + "Copper68k.Tests/WinUaeArchitecturalFlagsTests.cs": "cc892d9f0830238a7b8215555000e921b442eaf2da0856670742033b8c6d303d", + "Copper68k/AssemblyInfo.cs": "ffeccb35cd2e966f4ba10a70f45d05619a45244e011ddc67a3cfd10268d98439", + "Copper68k/Copper68k.csproj": "a1e043e2e4f69cb4d5354bf68f0e2613ebef26de9607a069f7c613a4e921d7ad", + "Copper68k/M68010Interpreter.cs": "ee664c4a6bd807cdf1512b7a48d69d87144b17af426f5f649300119fe761cd4c", + "Copper68k/M68010LongMoveResumeFrame.cs": "c7e4a0bddbcfd376ccc27eb52bdd780c01332f473e3e8046c0ec93b35654316c", + "Copper68k/M68010WordMoveResumeFrame.cs": "8390df13f239402c99d4d3433daf943f02e7b1b2b52e3e3ad958176e6c85dbad", + "Copper68k/M68020CpuProfile.cs": "e2f512ea84aa21b2309f177f12b9c72ff033b00cffac3b3cba5d88eed9773d55", + "Copper68k/M68020Interpreter.cs": "bb329f18d45d28492dbfc83a82cf4c414cf61e4fd9a8b11f708ccb4bed5738d3", + "Copper68k/M68030Interpreter.cs": "7ed874780bb15ec995c21fd2d32c8d38b34e6c12a2ae6b961e6685b67e409963", + "Copper68k/M68040PendingFpuExceptions.cs": "f1698c97090fc14aa5d28ea89117ef838d1e12abffebfcbb41dac2b6469d3f54", + "Copper68k/M68040Support.cs": "5e047b8f5a03215ae9d471ad3a51b97f5bfbe6c3547d7c6e6882aaa3d3c7648e", + "Copper68k/M68060Interpreter.cs": "9e7a6e3251f34882305001f3776ef7fdd9e2173d1de42afc9291c2cca6a570f2", + "Copper68k/M68EC020Interpreter.cs": "ee4c389d0a5af22627e9436184c2fde770a4f727d32804ceb0c279ca6dfe421c", + "Copper68k/M68kAdvancedTimingInterpreter.Access020.cs": "6dcb0143af04d9a27270fafd0b108482b02ca7bea6fdee91961fee75abc7104c", + "Copper68k/M68kAdvancedTimingInterpreter.Arithmetic.cs": "d20c7f9bdbd981769f60f6508971803d98bb09d97bec9beedda850d4c8bff8af", + "Copper68k/M68kAdvancedTimingInterpreter.Logical.cs": "244eac71f349421c917e945912382d51a0d50b381e2eed7a3058c4769601a6a0", + "Copper68k/M68kAdvancedTimingInterpreter.Module.cs": "58ef45198c2c8b97553584382ae63f37e335c9bcc761613555b82c235fc25cbb", + "Copper68k/M68kAdvancedTimingInterpreter.Move.cs": "373b22e7e0a994a73555f087fed9bbce2fa73b71d449aa1292d4000d56288ef4", + "Copper68k/M68kAdvancedTimingInterpreter.Movem.cs": "63509842aade92b33c57473e8c70452d4898a3b468f8e9d1f12fc2aca704d500", + "Copper68k/M68kAdvancedTimingInterpreter.Rte.cs": "5a3fe6b849689c81682a051cde92aa0a7d319bc633d0f8d24a2a84a3dac0a82e", + "Copper68k/M68kAdvancedTimingInterpreter.Rte020.cs": "4438b55f9e522eb8c4aa082cd734b0be4145b3b8c87c874c8a42b9922af319ef", + "Copper68k/M68kAdvancedTimingInterpreter.System.cs": "12e5050ea0e1c67fb41046161480b1b5bc3626c9aebdbcd11ef772c034e607dc", + "Copper68k/M68kAdvancedTimingInterpreter.cs": "dd7a1f20ec2c6685d6bb3c3d890f0ad2f6d721c4fbf5e69a1af2a4bf087ab821", + "Copper68k/M68kBusAccessKind.cs": "e92bedcb7ce4d1f7ee7f184f122681dbf010d2c61727b5e445cd853fd805ce93", + "Copper68k/M68kCodeSnapshot.cs": "76a38f3ffd4dff95b3dde8ddffa86ad94e68bdfbb06f0d087f1100edc33ef9c7", + "Copper68k/M68kCore.cs": "45f0eb7b27f844fea8fc5f685b25f2fc0595c0f8563c647fe395af5e889e7d20", + "Copper68k/M68kDecodedInstruction.cs": "50471481b4075fe0c38f935315b98dfab060ba0eab201387c11a5e7be7e1e868", + "Copper68k/M68kEmulationException.cs": "2256c5e1abee5139489acab7bf2f6f5c54bb2a99c6be8d0e7f3b1c4813781b74", + "Copper68k/M68kInstructionFrequency.cs": "97ea9d9339515ff5555c887a70eef2ec936b932e65672eeca0803b04901d0a84", + "Copper68k/M68kIntegerSemantics.cs": "57da61c7f13d0c9832feabae99ad8fcc72e5eac86c87049270d2e18e3d417f9b", + "Copper68k/M68kJitAlignmentEmitter.cs": "196a82783670a934c48fb4e32270434317996839e55ee5bdc5c7b4f3d8e777b6", + "Copper68k/M68kJitBus.cs": "82222fb54e1b4a9daa90850b08cd2f092488deb29ec97d245c8368ae904a746c", + "Copper68k/M68kJitCore.cs": "c7e20808260bfb3da46801fbbefcac56e2049b4f53322f23e6a31ab0b5a2e4cd", + "Copper68k/M68kJitCpuModel.cs": "3acf8e1ff354dd96229a4a5f6b6e0e554fb71a54516c608226f5117c8ff1af06", + "Copper68k/M68kJitEmitters.cs": "6745e3ca4751da8469bce17ae9ef769be917c56c50e1a9c7233d79512e43c256", + "Copper68k/M68kOpcodePlan.cs": "bfa2628d4e05aac56f4cec21e901e62140225a7264d739aaeb9fdb1ceda32609", + "Copper68k/M68kTimingEngine.cs": "d04f7f93c69a1735864f234305c9acb0a8a012cc4a0533c152d1aceee72bfda1", + "Copper68k/M68kTimingFormula.cs": "6724346efc1e7b6c8a89dd91b4387eaac9e12a5e2916343f4346a6111f4a8277" + }, + "candidateCpuNormalizedSha256": { + "Copper68k/AssemblyInfo.cs": "ffeccb35cd2e966f4ba10a70f45d05619a45244e011ddc67a3cfd10268d98439", + "Copper68k/Copper68k.csproj": "a1e043e2e4f69cb4d5354bf68f0e2613ebef26de9607a069f7c613a4e921d7ad", + "Copper68k/M68010Interpreter.cs": "ee664c4a6bd807cdf1512b7a48d69d87144b17af426f5f649300119fe761cd4c", + "Copper68k/M68010LongMoveResumeFrame.cs": "c7e4a0bddbcfd376ccc27eb52bdd780c01332f473e3e8046c0ec93b35654316c", + "Copper68k/M68010WordMoveResumeFrame.cs": "8390df13f239402c99d4d3433daf943f02e7b1b2b52e3e3ad958176e6c85dbad", + "Copper68k/M68020CpuProfile.cs": "e2f512ea84aa21b2309f177f12b9c72ff033b00cffac3b3cba5d88eed9773d55", + "Copper68k/M68020Interpreter.cs": "bb329f18d45d28492dbfc83a82cf4c414cf61e4fd9a8b11f708ccb4bed5738d3", + "Copper68k/M68030Interpreter.cs": "7ed874780bb15ec995c21fd2d32c8d38b34e6c12a2ae6b961e6685b67e409963", + "Copper68k/M68040PendingFpuExceptions.cs": "f1698c97090fc14aa5d28ea89117ef838d1e12abffebfcbb41dac2b6469d3f54", + "Copper68k/M68040Support.cs": "5e047b8f5a03215ae9d471ad3a51b97f5bfbe6c3547d7c6e6882aaa3d3c7648e", + "Copper68k/M68060Interpreter.cs": "9e7a6e3251f34882305001f3776ef7fdd9e2173d1de42afc9291c2cca6a570f2", + "Copper68k/M68EC020Interpreter.cs": "ee4c389d0a5af22627e9436184c2fde770a4f727d32804ceb0c279ca6dfe421c", + "Copper68k/M68kAdvancedTimingInterpreter.Access020.cs": "ac67a04180f3f01bd4c9b637133a009d9febb477853a5f1c17bb041ed4b12ef2", + "Copper68k/M68kAdvancedTimingInterpreter.Arithmetic.cs": "d20c7f9bdbd981769f60f6508971803d98bb09d97bec9beedda850d4c8bff8af", + "Copper68k/M68kAdvancedTimingInterpreter.FinalWrite020.cs": "db1e6b0b8a88a817431254f70a1614df2326f02f721ec6d2056df30cc2f44ee1", + "Copper68k/M68kAdvancedTimingInterpreter.Logical.cs": "244eac71f349421c917e945912382d51a0d50b381e2eed7a3058c4769601a6a0", + "Copper68k/M68kAdvancedTimingInterpreter.Module.cs": "58ef45198c2c8b97553584382ae63f37e335c9bcc761613555b82c235fc25cbb", + "Copper68k/M68kAdvancedTimingInterpreter.Move.cs": "827d8537122e23fa0153d3876bde1d948bfce0685326f538098764fe26437963", + "Copper68k/M68kAdvancedTimingInterpreter.Movem.cs": "63509842aade92b33c57473e8c70452d4898a3b468f8e9d1f12fc2aca704d500", + "Copper68k/M68kAdvancedTimingInterpreter.Operand020.cs": "b0e6725c9e95ea848ada71e68700aeb86986aa422902b2046ad1886a74b2a256", + "Copper68k/M68kAdvancedTimingInterpreter.Rte.cs": "5a3fe6b849689c81682a051cde92aa0a7d319bc633d0f8d24a2a84a3dac0a82e", + "Copper68k/M68kAdvancedTimingInterpreter.Rte020.cs": "0565aa8b3c04a2e904241481ab078c477e2048f7f1a4c40d98e3520673e91904", + "Copper68k/M68kAdvancedTimingInterpreter.System.cs": "12e5050ea0e1c67fb41046161480b1b5bc3626c9aebdbcd11ef772c034e607dc", + "Copper68k/M68kAdvancedTimingInterpreter.cs": "438092ff0fe2f8b8efec0bfa66ca3fcf877e805f714cad84ba0fb02957e8b2a1", + "Copper68k/M68kBusAccessKind.cs": "e92bedcb7ce4d1f7ee7f184f122681dbf010d2c61727b5e445cd853fd805ce93", + "Copper68k/M68kCodeSnapshot.cs": "76a38f3ffd4dff95b3dde8ddffa86ad94e68bdfbb06f0d087f1100edc33ef9c7", + "Copper68k/M68kCore.cs": "45f0eb7b27f844fea8fc5f685b25f2fc0595c0f8563c647fe395af5e889e7d20", + "Copper68k/M68kDecodedInstruction.cs": "50471481b4075fe0c38f935315b98dfab060ba0eab201387c11a5e7be7e1e868", + "Copper68k/M68kEmulationException.cs": "2256c5e1abee5139489acab7bf2f6f5c54bb2a99c6be8d0e7f3b1c4813781b74", + "Copper68k/M68kInstructionFrequency.cs": "97ea9d9339515ff5555c887a70eef2ec936b932e65672eeca0803b04901d0a84", + "Copper68k/M68kIntegerSemantics.cs": "57da61c7f13d0c9832feabae99ad8fcc72e5eac86c87049270d2e18e3d417f9b", + "Copper68k/M68kJitAlignmentEmitter.cs": "196a82783670a934c48fb4e32270434317996839e55ee5bdc5c7b4f3d8e777b6", + "Copper68k/M68kJitBus.cs": "82222fb54e1b4a9daa90850b08cd2f092488deb29ec97d245c8368ae904a746c", + "Copper68k/M68kJitCore.cs": "c7e20808260bfb3da46801fbbefcac56e2049b4f53322f23e6a31ab0b5a2e4cd", + "Copper68k/M68kJitCpuModel.cs": "3acf8e1ff354dd96229a4a5f6b6e0e554fb71a54516c608226f5117c8ff1af06", + "Copper68k/M68kJitEmitters.cs": "6745e3ca4751da8469bce17ae9ef769be917c56c50e1a9c7233d79512e43c256", + "Copper68k/M68kOpcodePlan.cs": "bfa2628d4e05aac56f4cec21e901e62140225a7264d739aaeb9fdb1ceda32609", + "Copper68k/M68kTimingEngine.cs": "d04f7f93c69a1735864f234305c9acb0a8a012cc4a0533c152d1aceee72bfda1", + "Copper68k/M68kTimingFormula.cs": "6724346efc1e7b6c8a89dd91b4387eaac9e12a5e2916343f4346a6111f4a8277" + }, + "patchSha256": "624a9134a9f88dacda50d15508f5418106a77da766c7051a53322fa079e13490", + "expectedTests": { + "Copper68k.Tests.Synthetic.SyntheticM68020PredecrementReadTests.DirectWidthControls(batch: True)": "Passed", + "Copper68k.Tests.Synthetic.SyntheticM68020MemoryReadWriteTests.DirectWidthControls(batch: True)": "Passed", + "Copper68k.Tests.Synthetic.SyntheticM68020PredecrementReadTests.DirectWidthControls(batch: False)": "Passed", + "Copper68k.Tests.Synthetic.SyntheticM68020MemoryReadWriteTests.DirectWidthControls(batch: False)": "Passed", + "Copper68k.Tests.Synthetic.SyntheticM68020MemoryReadWriteTests.BatchReadWriteContinuations": "Passed", + "Copper68k.Tests.Synthetic.SyntheticM68020PredecrementWriteTests.BatchFinalWriteFaults": "Passed", + "Copper68k.Tests.Synthetic.SyntheticM68020MemoryReadWriteTests.ScalarReadWriteContinuations": "Passed", + "Copper68k.Tests.Synthetic.SyntheticM68020MoveWriteHandlerTests.ScalarPendingWriteHandlers": "Passed", + "Copper68k.Tests.Synthetic.SyntheticM68020PredecrementWriteTests.DirectFinalWriteControls(batch: False)": "Passed", + "Copper68k.Tests.Synthetic.SyntheticM68020PredecrementWriteTests.ScalarFinalWriteFaults": "Passed", + "Copper68k.Tests.Synthetic.SyntheticM68020PredecrementWriteTests.DirectFinalWriteControls(batch: True)": "Passed", + "Copper68k.Tests.Synthetic.SyntheticM68020MoveWriteHandlerTests.BatchPendingWriteHandlers": "Passed", + "Copper68k.Tests.Synthetic.SyntheticM68020PredecrementReadTests.BatchReadWriteContinuations": "Passed", + "Copper68k.Tests.Synthetic.SyntheticM68020PredecrementReadTests.ScalarReadWriteContinuations": "Passed" + }, + "reports": { + "68020-memory-read-write-1-control-0-batch.json": { + "sha256": "a8e01d35e88fafec027ca815b0178e246ca036aa7a612890268d1827fe0eefa0", + "logicalCases": 4096 + }, + "68020-memory-read-write-1-control-0-scalar.json": { + "sha256": "f5cbae86648a29fff2e44115bb24a12832dd82c687d3fb720395ddb23575ac93", + "logicalCases": 4096 + }, + "68020-memory-read-write-1-read-0-batch.json": { + "sha256": "d601a70290a82a24326f7e5e106d137b21c0559ae18cda1f30e91857420d85b9", + "logicalCases": 4096 + }, + "68020-memory-read-write-1-read-0-scalar.json": { + "sha256": "b47f4b536206517af79b0f1dfa086d085c744961729881d2222706b0db25350a", + "logicalCases": 4096 + }, + "68020-memory-read-write-1-read-write-0-batch.json": { + "sha256": "81e537a1201a8d2b2336f087f9a0be1db21a752163d341569ef0f843a0e7eb2a", + "logicalCases": 4096 + }, + "68020-memory-read-write-1-read-write-0-scalar.json": { + "sha256": "357b47003cb007e08dbdfa39ed49768d259933515833147abf3c5abf62c9798a", + "logicalCases": 4096 + }, + "68020-memory-read-write-1-read-write-refault-0-batch.json": { + "sha256": "9e62523bc8113d21ec219d85dbe95af9220536ef0231a2ce5ed5e21bc78c59db", + "logicalCases": 4096 + }, + "68020-memory-read-write-1-read-write-refault-0-scalar.json": { + "sha256": "c43d04fde9db4e8a63729273c801cf21bdcaa9f0f7a679a47e6f98393c3df887", + "logicalCases": 4096 + }, + "68020-memory-read-write-2-control-0-batch.json": { + "sha256": "5779a39987805d05985a275d1367fe720f51c2141308f0ddd97e58dbe8282c01", + "logicalCases": 4096 + }, + "68020-memory-read-write-2-control-0-scalar.json": { + "sha256": "8e58bce4dfe6e78ed3d14be393c3fb298ecee5f39612f9c46626dd8fcb70b9cb", + "logicalCases": 4096 + }, + "68020-memory-read-write-2-read-0-batch.json": { + "sha256": "2eaa5f8484e40b9bcbbd908c181288c2eb3b7f89c2cc8423a5a7bcc32b9c0952", + "logicalCases": 4096 + }, + "68020-memory-read-write-2-read-0-scalar.json": { + "sha256": "69d60abdd505908e63a550126cd6f7f17a9518da7c1cf51227c889330a11bddb", + "logicalCases": 4096 + }, + "68020-memory-read-write-2-read-1-batch.json": { + "sha256": "748652e0db41fa208f8529612ff2dfcb6c3497829bdb9d7f065e81f8157fe626", + "logicalCases": 4096 + }, + "68020-memory-read-write-2-read-1-scalar.json": { + "sha256": "4dacdfddf2d3a1a95cc349f79c79a7fef11cc591a0d3d9ea73c6485d03ac0d02", + "logicalCases": 4096 + }, + "68020-memory-read-write-2-read-write-0-batch.json": { + "sha256": "ba2773a94b407f706f7fd6ca647e0244b162bd59b8ebdea8c731008894c78a91", + "logicalCases": 4096 + }, + "68020-memory-read-write-2-read-write-0-scalar.json": { + "sha256": "b4fe6c93956674fabd7523e26b51fe09b3d682df110e86d261c81faf11dfe7a6", + "logicalCases": 4096 + }, + "68020-memory-read-write-2-read-write-1-batch.json": { + "sha256": "ea7dde79f261d7a590afd84f9828da993ac8514694b04799a0fd6cd3ff8ddd47", + "logicalCases": 4096 + }, + "68020-memory-read-write-2-read-write-1-scalar.json": { + "sha256": "61f2ab6f6bbf5069538a24450536c625f6fc22094d77ae91dfe76ebcd0158710", + "logicalCases": 4096 + }, + "68020-memory-read-write-2-read-write-refault-0-batch.json": { + "sha256": "c35efdbbdbae35dbed7ccabd10d8570b8309e0a6b8c2406c850566b132323784", + "logicalCases": 4096 + }, + "68020-memory-read-write-2-read-write-refault-0-scalar.json": { + "sha256": "94031d2ecd00095c32f6b4f0ee5d058f7cec0dda8f5de65ac50fdc1783df1d0f", + "logicalCases": 4096 + }, + "68020-memory-read-write-2-read-write-refault-1-batch.json": { + "sha256": "fda41ac47698baf51e8c15ea7879dff9f3bc6be18845d292737a8757fc955bb6", + "logicalCases": 4096 + }, + "68020-memory-read-write-2-read-write-refault-1-scalar.json": { + "sha256": "15b1118512576ac59acb70e9256b781f557859509c2fcfa1c7b4834b8d91c513", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-control-0-batch.json": { + "sha256": "770564e769e780b5c212585fab76e4c7320a12903017465158df75eaf0b91eac", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-control-0-scalar.json": { + "sha256": "9dc9fb4ffae20d161c13884f5a2b54e2642a33a50c6420da82d3e3db8e7a8f0f", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-0-batch.json": { + "sha256": "4ed34652abb7043f7a59de99cdb55f292dd9d54ed8937064183d22c5c512c2b1", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-0-scalar.json": { + "sha256": "f0090e5b6fac0ec6c910793187a8be4322c0f577a5ba873c57bf20c1f02be2c0", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-1-batch.json": { + "sha256": "11c7b288b8c9533e00ba39cfae81fc432afda946f348a10c23c795688212b1db", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-1-scalar.json": { + "sha256": "6e3101b30fc72a633116916877e40b74d3d1f5b7fad29791cb8cd866610904ce", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-2-batch.json": { + "sha256": "748c45e5806fdb0efc73b23ca4b9aad2f3a4b4c736442919b4ab7f76f97bead7", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-2-scalar.json": { + "sha256": "6d2ec62e65649abaad556f2495d130a8d108c9be64825b74933420c56f16713b", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-3-batch.json": { + "sha256": "653d1f09a80912be15789f3155b36a2ae5d252752633cb2acf64314e7d43b393", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-3-scalar.json": { + "sha256": "a910f252f2fafc328e6d5fad9e5dfd83cd1a679b7693a827cdf06131a9a3df87", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-write-0-batch.json": { + "sha256": "4a03bb38879a43e5b3d94781584f186f1a01c79ae34b986271ab0b9bf27ff442", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-write-0-scalar.json": { + "sha256": "b0f08fc6747ed09d4ded085ac2574a385a4ea4ca0f1b0be4219466c560c1105e", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-write-1-batch.json": { + "sha256": "b59e359390aa1bb003926fbc5ce45e6bea5d3ff9c488a41ea3885bb528d9ee04", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-write-1-scalar.json": { + "sha256": "4ed6f2e76f038b1294ee5700ed0bf8dd196f89d0622104947977bc0a3d8c8c6c", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-write-2-batch.json": { + "sha256": "c300c4e3ad413ad2366e0e5781b90dbfff92331a46bb2d7955bfe05decf3b6ed", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-write-2-scalar.json": { + "sha256": "39366c78c6af530afeccab2b6fe36480abb1fdc222f96db683b0f216f7cf8df9", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-write-3-batch.json": { + "sha256": "e683aa4205b2f0b13718403367781fe106433f7e7405905fe2adf82cb33350b9", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-write-3-scalar.json": { + "sha256": "a5847c484fbe83bc266c8bfd49b6375349afcbc757c75938a9b5fa3d0cc2edd8", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-write-refault-0-batch.json": { + "sha256": "62a6f3904930b4e43f484ff69be145624a43d9d85f6969e8862b0511824decd1", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-write-refault-0-scalar.json": { + "sha256": "f35d36901ece7174c987a300789f42c972400e3455b84db1e14428fe7c03d7e7", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-write-refault-1-batch.json": { + "sha256": "a391109febd4990ce3edf1690ce678ac350909decd7dbc27b4e7979b762ae3f0", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-write-refault-1-scalar.json": { + "sha256": "cbb867223c80b8bee67fa92cd4c244c06340bf773469bb569f0345bc53a19a9b", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-write-refault-2-batch.json": { + "sha256": "7fdc77f5f7fbf84f94b6b889c039168e3b852ae5647e8b75bb5967418d6f84fb", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-write-refault-2-scalar.json": { + "sha256": "1299d3571eab66b729d1736f4c82da139747709795ef87310746053701abc634", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-write-refault-3-batch.json": { + "sha256": "0ce6505468d63439c9a3763106488a79c022f608dd5616db0ecc60f142c03b89", + "logicalCases": 4096 + }, + "68020-memory-read-write-4-read-write-refault-3-scalar.json": { + "sha256": "8af89ad868a4b5653cbe040cf499846cf36fc1b34daa6ac07ef255abc3552b4e", + "logicalCases": 4096 + }, + "68020-move-predecrement-write-controls-batch.json": { + "sha256": "bebe91efcbbdbc906239ac952d8e44c41f9ae1c7e71e18c7ac0dd0d06766a0a4", + "logicalCases": 24576 + }, + "68020-move-predecrement-write-controls-scalar.json": { + "sha256": "f48352f74a7b335de6779733f8eab7bcfb58c18badc7a8f64b1d8872171adee1", + "logicalCases": 24576 + }, + "68020-move-predecrement-write-fault-batch.json": { + "sha256": "bdae975cb7658dd49b776d3656dde229df70db80ebafccf5855bbd66418e8ac9", + "logicalCases": 24576 + }, + "68020-move-predecrement-write-fault-scalar.json": { + "sha256": "704e3524b374272cda4e9b1c40dfc03895381ff1e2cc0a2cac01c0f5e0b0b21c", + "logicalCases": 24576 + }, + "68020-move-write-handlers-1-edited-refault-batch.json": { + "sha256": "ecb54d6bc7f4a7667f8dfb260270bf14fefac5dcc1866eed62c8dff4f8183213", + "logicalCases": 4096 + }, + "68020-move-write-handlers-1-edited-refault-scalar.json": { + "sha256": "f86e09e7f9b79c741db1cc38dd032ee16cd1ea51949b339077858838c6234409", + "logicalCases": 4096 + }, + "68020-move-write-handlers-1-original-batch.json": { + "sha256": "fb7badb503c6284cef9bde0bda620ac827e392069143d407dc408e1143543144", + "logicalCases": 4096 + }, + "68020-move-write-handlers-1-original-scalar.json": { + "sha256": "58a83506a1698c92adc43be3b584fbb69cf06214e99d132668970aeddd91a1db", + "logicalCases": 4096 + }, + "68020-move-write-handlers-1-output-batch.json": { + "sha256": "c87c91a68cea9c42c23efbf488f7b38bec1973ca8255f45c66b4bf7ab1c2b9eb", + "logicalCases": 4096 + }, + "68020-move-write-handlers-1-output-scalar.json": { + "sha256": "5c218c5112795a115a291ef103e809a3ad988984fb046217ee8628782a20c63e", + "logicalCases": 4096 + }, + "68020-move-write-handlers-1-refault-batch.json": { + "sha256": "caf567545cc6860061d6dfaf9a7bcf6afbb553a330f30d13a5453f2d3d5b131e", + "logicalCases": 4096 + }, + "68020-move-write-handlers-1-refault-scalar.json": { + "sha256": "514c997477c61cb236c08ce66b1a5019183f3fd27818b2b60430ad7b6cba1eed", + "logicalCases": 4096 + }, + "68020-move-write-handlers-1-software-batch.json": { + "sha256": "0e4588c6c7d22e736935ab6aa1a145867bfd78ed0ced9a04ac046880787d9fe1", + "logicalCases": 4096 + }, + "68020-move-write-handlers-1-software-scalar.json": { + "sha256": "74e877fe74008d02b70e301c1386f4ffc76388be2c3ce5456f2c89f8a4728f56", + "logicalCases": 4096 + }, + "68020-move-write-handlers-2-edited-refault-batch.json": { + "sha256": "3e02cf23d5a4c69600716fa298601c52780c4974df580a20e3a70b58c9f7de3c", + "logicalCases": 4096 + }, + "68020-move-write-handlers-2-edited-refault-scalar.json": { + "sha256": "ba4d74e65bec574d7ebf8585f250f78851b6db478fdf1bdba281e14351d57818", + "logicalCases": 4096 + }, + "68020-move-write-handlers-2-original-batch.json": { + "sha256": "f4fd14515049c02ea9921f9ff090465f58c6ba8831629ff2931319d2181b41f6", + "logicalCases": 4096 + }, + "68020-move-write-handlers-2-original-scalar.json": { + "sha256": "4b42933467f786d8f686cac308c4a10fad3b30eb5384ff08609ca9697f0626bd", + "logicalCases": 4096 + }, + "68020-move-write-handlers-2-output-batch.json": { + "sha256": "0fda62af8ae14a04eb7d8662821e914c1d8d640bd16b361496672ea453b95d27", + "logicalCases": 4096 + }, + "68020-move-write-handlers-2-output-scalar.json": { + "sha256": "599175b6b53e30d8dabef21557c923260a647fd130497fc5e466bb2eed95eef9", + "logicalCases": 4096 + }, + "68020-move-write-handlers-2-refault-batch.json": { + "sha256": "9b425b761ddae7640422a0c9862c014c14a1a4eec7ddda7f1d7ddceb112fff2e", + "logicalCases": 4096 + }, + "68020-move-write-handlers-2-refault-scalar.json": { + "sha256": "ef4d26145bb3b82c184fb899f754579175f82af9c1323f41e17d598dd4760704", + "logicalCases": 4096 + }, + "68020-move-write-handlers-2-software-batch.json": { + "sha256": "a14d6b6c24a68771e0bab2e419e2bb35f2c8b81bb518d29fc2d53df0a353a2e5", + "logicalCases": 4096 + }, + "68020-move-write-handlers-2-software-scalar.json": { + "sha256": "9b4cfe2e6ced896aa20c36282d07149478f229d0d3c21a5ee57e139331e65168", + "logicalCases": 4096 + }, + "68020-move-write-handlers-4-edited-refault-batch.json": { + "sha256": "406225f4f9c2900c8b7b8e7f0353eb6d55a4f4b9fc3a60b90797ee07d6dce77d", + "logicalCases": 4096 + }, + "68020-move-write-handlers-4-edited-refault-scalar.json": { + "sha256": "7fbfc8e02dad922ce5481bc8d6b342c30adfb42a3ba3557b77108d3607e97281", + "logicalCases": 4096 + }, + "68020-move-write-handlers-4-original-batch.json": { + "sha256": "2c1b6edb8bf60ec6cf0bc52bc4109669d6875eb51abbf38c3b76b5f1afb6ecac", + "logicalCases": 4096 + }, + "68020-move-write-handlers-4-original-scalar.json": { + "sha256": "22b07c4b9486dea59580860c6a3a521e42c34c88cce925c36682b5b0bd55fe1d", + "logicalCases": 4096 + }, + "68020-move-write-handlers-4-output-batch.json": { + "sha256": "debb4c8b6b6f38206fcbb20642c66940e723025183dbc7fe20138a55d871db68", + "logicalCases": 4096 + }, + "68020-move-write-handlers-4-output-scalar.json": { + "sha256": "b31c954bb3c76abef26fe7559f115fdfbe8fa937b1021205ff3688c0d60f1196", + "logicalCases": 4096 + }, + "68020-move-write-handlers-4-refault-batch.json": { + "sha256": "05767991b2964bb63aa14c54b422fb3946e662eb9b97b38a06568f35643cbffc", + "logicalCases": 4096 + }, + "68020-move-write-handlers-4-refault-scalar.json": { + "sha256": "47f009c04f6d5d9df0804ac77521b39565e9151c0087e30b9e7786eb5cd29a9c", + "logicalCases": 4096 + }, + "68020-move-write-handlers-4-software-batch.json": { + "sha256": "42fabb9870eb741c94c4672a3ddf1d63fc032c55133df06cc7aa902cb92191ff", + "logicalCases": 4096 + }, + "68020-move-write-handlers-4-software-scalar.json": { + "sha256": "cc76bf86d4dc3afd9d5fbea1383d6c72e4dc032654641bbc330f98f070befffb", + "logicalCases": 4096 + }, + "68020-predecrement-read-write-1-control-0-batch.json": { + "sha256": "02ca955acf205c45760b24d8cac51b7df6ab8b912ebda119fc308aa2172ea117", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-1-control-0-scalar.json": { + "sha256": "48f55a27c3d3f65a3d52e64d36e7d5a6bb96f8f67920bc37ced5d4a02082e4d5", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-1-read-0-batch.json": { + "sha256": "210ea9592bf03b1b90af6ecc2f20d79902231e5ac488ee34c61e91f066502eb7", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-1-read-0-scalar.json": { + "sha256": "06d78daf1507f25eb9d64de7ccbeae856d0664d7094027b0c61319013bb4241e", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-1-read-write-0-batch.json": { + "sha256": "fe608fcd43ca372d418f92a18e89ffb5580b507bb7c25fc35ba9a37f9a641c68", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-1-read-write-0-scalar.json": { + "sha256": "0720d68bcc7a0b8206f51faf8ac1a5090a235a1f5aeb90781ebb397db74bdda4", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-1-read-write-refault-0-batch.json": { + "sha256": "426393fdc33a3633217c3e9d3f0a28b304244ebb75f77aed232d3f60bbe0f1db", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-1-read-write-refault-0-scalar.json": { + "sha256": "ffff14c1fd19339b64e55160be63619acbf5e8cab10c358de5e84dfdc81bedc4", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-2-control-0-batch.json": { + "sha256": "72392c2ca77c9e781f96eecf0c98ebadac8bc707fa06cf16488c749d29baad04", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-2-control-0-scalar.json": { + "sha256": "79748c2ec3f69b92a0fd9d0b0d3898c9d80557c7a77e16f10295ea63dabee18e", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-2-read-0-batch.json": { + "sha256": "294f7e4ab1f3440ecb39a59e4e70770a68860124c32b1bd5e80bc04155a1250b", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-2-read-0-scalar.json": { + "sha256": "be5daab3ad6f4c71bccf8709768a032931f686d71254ef71d3d50e8d6cb51fed", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-2-read-1-batch.json": { + "sha256": "d754237f30b879eeb32b0e1c8c3acef0a6ed12a6ff23339880a396216d731b16", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-2-read-1-scalar.json": { + "sha256": "173a0708e4340b74b7217ffbcacc8ac624985401c24c625729721982e862d425", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-2-read-write-0-batch.json": { + "sha256": "2cb8fa2022fbba847c81c2f0d5dca174bf200e80b2486e4794b7aca14661e3f6", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-2-read-write-0-scalar.json": { + "sha256": "c6351264ebd31127061a6fe57c788ddb570d727b3e13fb200fa40a3bff88dbff", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-2-read-write-1-batch.json": { + "sha256": "d369f5fde510350d9bb79d507977bae88a6d0c94b52fbdf6338ff627a35d59dc", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-2-read-write-1-scalar.json": { + "sha256": "39564eb7a780c9258a5578e5ec450630d81dd9e7bbf2fe47239b1f4214878230", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-2-read-write-refault-0-batch.json": { + "sha256": "52467feadb6b14f0dfc456ab873d2f6f0e9f95d699a351258213cfb304d77da8", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-2-read-write-refault-0-scalar.json": { + "sha256": "bcf4f912d54e1f2d1add0eed4f15fa9a5973ba0650b4db8dd132fd541b2170c5", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-2-read-write-refault-1-batch.json": { + "sha256": "673cc59f7ede1c45dfdca52d3c74dd40532cc23e8e15dd22432452c4e29e090b", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-2-read-write-refault-1-scalar.json": { + "sha256": "5611bfa67026acf670b94efb7498d7d3be29ae0876b071c50696b94868e25618", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-control-0-batch.json": { + "sha256": "f02bfdf038ed9c4a61ee72326689af179b2f34609a592999f89bbfe73792cba6", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-control-0-scalar.json": { + "sha256": "8282f977db828abc6f0c494bb544e179a473ddfbad18c5b868772d5fa2a693db", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-0-batch.json": { + "sha256": "ff0c62d60d89c9fa8423ae901afad205b95cc4dbde9545d20e5b25059d8bc7f1", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-0-scalar.json": { + "sha256": "fb6e98409e5ebd1cf8788b89017f261929f797df80738800b569a529fd279bf6", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-1-batch.json": { + "sha256": "c9d637186e02823fd780ee825cc343f356bd2b04857e6087c14a944e266ffd59", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-1-scalar.json": { + "sha256": "1562d382dd7c521723ecbfe500a121cf3f25f8dfe9b67b5edf77d9daaf999df0", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-2-batch.json": { + "sha256": "f21a3227d630a1efb4c4ef57e132cb81c2f8252be052c4b83f09ce9c5b69cc31", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-2-scalar.json": { + "sha256": "91556569025b721206e3a2c38c00dc74354f539d905f519c6dac7810e4115750", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-3-batch.json": { + "sha256": "e24d53b03a06c5129cbcd184bf2fb390a349257efb713cf0603f8df7d24ddb3f", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-3-scalar.json": { + "sha256": "6774d1c5ec46a691fb3bbaaaf14b6729d51180ac4e4f0128bf078a98ab56fc55", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-write-0-batch.json": { + "sha256": "cb1e9026f6ea0f20ca0be5a1dc6356220ae67fbcc4b8bde402a6dae7c933682a", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-write-0-scalar.json": { + "sha256": "b430b89a30925583a064781bd8659244afbc0ce2d014eb4dacb22ee1dc626231", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-write-1-batch.json": { + "sha256": "94f778fa16fe93f3b35c7d21afa4209b2d90655cee4fc4e450c22cc7deec352f", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-write-1-scalar.json": { + "sha256": "8b6899a80a16fc4d053b0e0911c43028e319fc9fcace01d347c1b33b15bcf232", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-write-2-batch.json": { + "sha256": "92912154bb8819d9d59e56a261da11a6227405327f763106fdf1217a461698ab", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-write-2-scalar.json": { + "sha256": "c80565e1283a83dc2df2e8e30ffc6ca1da16f7ef2331b2461b1bacd81c44b2df", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-write-3-batch.json": { + "sha256": "88ec73cc9ca6dc71951a4aabf84dcc52f97527f034e39e168c389de36e1ea2e7", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-write-3-scalar.json": { + "sha256": "df2a2c3bc81105f55a97954ff721d09344124f2b71f28c647f9eba51f5b0dfa9", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-write-refault-0-batch.json": { + "sha256": "2152d9139fd90ec93d6938e7d6ccd8cf9d4fa93c13fb7a5a20c8cfa64a04aa99", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-write-refault-0-scalar.json": { + "sha256": "2dca8fbf9f4ade772d9654609794dbc4f72c120bbd1c2c3dccbfbd70e2b72de2", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-write-refault-1-batch.json": { + "sha256": "96f522717967ba3f3a0a10250cc8b31a08f199469a13a9c51cd4bce5820e6719", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-write-refault-1-scalar.json": { + "sha256": "a041713c07b1cc48fae178a4c1453f673b97270d5b01878954cdf454833f2b9a", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-write-refault-2-batch.json": { + "sha256": "bd892a306637d0eff5879a669831655924d7beceda876cdbc798db57c32122fb", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-write-refault-2-scalar.json": { + "sha256": "1fba3c6aa4c32debd8779b43d46aa52ad24bbed496498a9f67bf1b53e816d861", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-write-refault-3-batch.json": { + "sha256": "d98bb21c4a449cc4e771f499bbf6e6ccf5b9b9432317e0fdea86863b79d7cddb", + "logicalCases": 8192 + }, + "68020-predecrement-read-write-4-read-write-refault-3-scalar.json": { + "sha256": "44d2c95961581923937e54602ca07c30aa7f2689e20f7deba8392eb6ed8ae6fb", + "logicalCases": 8192 + }, + "68030-memory-read-write-1-control-0-batch.json": { + "sha256": "15e3f0981528d8b236c86751b1dd96691a7d7c59fc3e3df8c954e4c2f2eafa4a", + "logicalCases": 4096 + }, + "68030-memory-read-write-1-control-0-scalar.json": { + "sha256": "6feaf4de7fc243cfc7d49cb7c351594cf09280783c078d55a4d588032a8fca33", + "logicalCases": 4096 + }, + "68030-memory-read-write-1-read-0-batch.json": { + "sha256": "be0406dea775706449cc30daf7602a55b9b12a17a21fc1890e6e3943645698d1", + "logicalCases": 4096 + }, + "68030-memory-read-write-1-read-0-scalar.json": { + "sha256": "b8b43b1176339ef26e8525cb7b1ecdea4318afb9276375367cc7302ca2fadd19", + "logicalCases": 4096 + }, + "68030-memory-read-write-1-read-write-0-batch.json": { + "sha256": "0c20d017f94b12e302ff8e1ae5ce002b26baece711549000961c74eb71e18475", + "logicalCases": 4096 + }, + "68030-memory-read-write-1-read-write-0-scalar.json": { + "sha256": "68760948f6168ea1a32ee40d9713c84fa74da021c9152921174c9751eb49770f", + "logicalCases": 4096 + }, + "68030-memory-read-write-1-read-write-refault-0-batch.json": { + "sha256": "75dd52830e48aacddcc1d09469b8206f6d51df785eaccbb247fdfc5568466b49", + "logicalCases": 4096 + }, + "68030-memory-read-write-1-read-write-refault-0-scalar.json": { + "sha256": "1ab2b0e918ba747a18c2904f3735410d712d863c4c6976316b31366342884e45", + "logicalCases": 4096 + }, + "68030-memory-read-write-2-control-0-batch.json": { + "sha256": "b63872a2294526205e6dbf5c7c2ac1b24810b574e4249ba832ba9a95bd92cc5f", + "logicalCases": 4096 + }, + "68030-memory-read-write-2-control-0-scalar.json": { + "sha256": "4303e0ec085d320dfb0cd0c807937e8fad74560c9bb4d88a2bb610cfb3d6e747", + "logicalCases": 4096 + }, + "68030-memory-read-write-2-read-0-batch.json": { + "sha256": "ea7b73c1253d57053f315bcbe411a01441df4600bccd5ee2172aaba6c793986d", + "logicalCases": 4096 + }, + "68030-memory-read-write-2-read-0-scalar.json": { + "sha256": "f57d72b503b34bf34d1b0ec2838aa4be36999208977866d5e8be0e6f3fe0eccc", + "logicalCases": 4096 + }, + "68030-memory-read-write-2-read-1-batch.json": { + "sha256": "6ef7a194d9f062b335743a29c6579c9517e5e5da480dc0c6228d2679fd2f32e3", + "logicalCases": 4096 + }, + "68030-memory-read-write-2-read-1-scalar.json": { + "sha256": "7a65009eced672b1050c1c7bfeb51607d9ddc285b174bd44d3902855977939e5", + "logicalCases": 4096 + }, + "68030-memory-read-write-2-read-write-0-batch.json": { + "sha256": "aade661c042444f908ea27bcd842b9eeebb71f9972c95ade2f71571ad28c8065", + "logicalCases": 4096 + }, + "68030-memory-read-write-2-read-write-0-scalar.json": { + "sha256": "71567da4379510bf283eb62beef9cd0a56b83cdd887c41124e50277735d0b921", + "logicalCases": 4096 + }, + "68030-memory-read-write-2-read-write-1-batch.json": { + "sha256": "0f7fb5e2986e4986960459f4f22127852ebc5b1c0fe68b4c5cd52150710ac66a", + "logicalCases": 4096 + }, + "68030-memory-read-write-2-read-write-1-scalar.json": { + "sha256": "6998a0f62185b02cee73e6ec2b7289b0bb5d20bdb618ace7435c51a50a65fc8d", + "logicalCases": 4096 + }, + "68030-memory-read-write-2-read-write-refault-0-batch.json": { + "sha256": "31e147a3e4cbf859a4815ed62e83d20be3c6e78b6952f6e6223b95f3e85aa845", + "logicalCases": 4096 + }, + "68030-memory-read-write-2-read-write-refault-0-scalar.json": { + "sha256": "53be7d0278264122aa27607e7f9f7ae853f86be3c8346df19d5f310dc2b32c43", + "logicalCases": 4096 + }, + "68030-memory-read-write-2-read-write-refault-1-batch.json": { + "sha256": "9ff9e39bc22f27d65ca96f695d1a8b48ca41366984c0d2164dd14555614548ba", + "logicalCases": 4096 + }, + "68030-memory-read-write-2-read-write-refault-1-scalar.json": { + "sha256": "04f540eced6a560f01a85a849bfacbe2b6024200c2d86544b84a2e095ccceddf", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-control-0-batch.json": { + "sha256": "bad3ebdb092372c1d0131bb9ab9969658f6adbfa291a1be867b54b3488b860f9", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-control-0-scalar.json": { + "sha256": "d8f5fbad11000b4198a75d657ba19ac7f3f526f42d7d57b03bedc2c545970336", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-0-batch.json": { + "sha256": "1491566b8f577053da7079309c174f4b51e964c3e0419028c53e314e8693c3a2", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-0-scalar.json": { + "sha256": "f9f8a1e40d22f3091d0e413a38417e9e51513922764f63577b96a59fbb323b50", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-1-batch.json": { + "sha256": "bcdc13d67b6451f568899a697b8cca675a8e031d93da997c2b08dd906aa2643f", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-1-scalar.json": { + "sha256": "a65cbb031ccc65351f1631a83a999e851730ac621a629f862a5780e89778845b", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-2-batch.json": { + "sha256": "1d744bd25555437f13bc0c2d3bc9be98dba4231482425a9b5d4ddabe33773802", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-2-scalar.json": { + "sha256": "acb2b43e71681978a3df3d9248d12c45dec6ec18f9496fdaf78f3aaad473f7e8", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-3-batch.json": { + "sha256": "cb0d844ced692c4e4d6db5bac5d9c0f0846bcf52a9acee030517a82c2a1cc449", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-3-scalar.json": { + "sha256": "80c217d0b86d05a9c00658654325ce348a26ce8c24c17bcc332bfff71c180c1d", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-write-0-batch.json": { + "sha256": "258d52d69ef46cf679f7f593a23c53c6a37d210953a89ddf32b4808a69684dae", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-write-0-scalar.json": { + "sha256": "d26162bf7496a29644956533a06d8ff9a673966e6693322ca349e1f1f33660e3", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-write-1-batch.json": { + "sha256": "3c8255aa43a0c9dcbedb0e8b8f3168cf9a52960d2635500a5e5f5064b0a05c9a", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-write-1-scalar.json": { + "sha256": "405ead2b127cd39f78bdf1457437a209ffa08b2999b94f468a77ec2bbc1e64b2", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-write-2-batch.json": { + "sha256": "1f1476b18970f3b2722309bb8c8d6ba1358aa479db8918e69f69cda7c3647d53", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-write-2-scalar.json": { + "sha256": "c927cc81d106a5783f6cb55f550a12d339b0ca52681bf9b87b156d9dc164d71c", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-write-3-batch.json": { + "sha256": "915e87fb7ebf304009c332f1c26dcc5fadb008bed77ad4d87a385b3438e8e345", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-write-3-scalar.json": { + "sha256": "c5f35357137978a0971e833831a469ce2847c4a6774f06180d7a4bee8a3f6e41", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-write-refault-0-batch.json": { + "sha256": "49abd1bdf8b63ee9ca207a13b6d154a213ce9e08990d42edd0b91289dab71140", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-write-refault-0-scalar.json": { + "sha256": "dd8d6df7db911cc2b334f1a4fbfbd80b182f76224a76a23946820ee6b35c9cf2", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-write-refault-1-batch.json": { + "sha256": "aba737a591c0f895931c47eb7f9ff80debb2171fa635ee36eaf03215c4f15e57", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-write-refault-1-scalar.json": { + "sha256": "b4a489c546e336f84f0ddaa232ec9f327d0e6f6e6be8649814d350ac18df189a", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-write-refault-2-batch.json": { + "sha256": "7fb0570d7fa442649050e3445816712b9c39d5a70ad3ed4cc27c6976d88c3922", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-write-refault-2-scalar.json": { + "sha256": "1adb0984cd8b7a72d2069c21a4a89302e823c9884813fbe9c96b032f23a6104f", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-write-refault-3-batch.json": { + "sha256": "bf412372934246bd91696668acf656d61e08c815bf620dd126afa850c1fbaf89", + "logicalCases": 4096 + }, + "68030-memory-read-write-4-read-write-refault-3-scalar.json": { + "sha256": "fac0ff14769f0b49c39d34bfb47f1bb37d9c9f32ebab1968d482ed37f257b5b2", + "logicalCases": 4096 + }, + "68030-move-predecrement-write-controls-batch.json": { + "sha256": "2034d25b2d181701deed0de71f36361c52fe1f0f4b3d6072a77a948bc102b0e5", + "logicalCases": 24576 + }, + "68030-move-predecrement-write-controls-scalar.json": { + "sha256": "fe4975a97c6a0248a16e1375a1c6c53af91f19a48e558941646c6774f89d566c", + "logicalCases": 24576 + }, + "68030-move-predecrement-write-fault-batch.json": { + "sha256": "3eb0ce32318cfeb06b308dbef9fd837a07ed958d3ab4b2a2f49f6c580f0e6122", + "logicalCases": 24576 + }, + "68030-move-predecrement-write-fault-scalar.json": { + "sha256": "16a771af470323604d0e9e51c920d868b7136c0803bd15934b46d316cd586545", + "logicalCases": 24576 + }, + "68030-move-write-handlers-1-edited-refault-batch.json": { + "sha256": "5133b32bd9f4ca56a23fd1f63675d175f1d983f815e31c22db04eb802f82af89", + "logicalCases": 4096 + }, + "68030-move-write-handlers-1-edited-refault-scalar.json": { + "sha256": "f1176d4088f0bd561079ac47f1134e7f0693da826f8ebe19aaeb7ce1edac92f9", + "logicalCases": 4096 + }, + "68030-move-write-handlers-1-original-batch.json": { + "sha256": "9887912025b8c9fb4c70c4ecc0d2029e3832389a856e19b2d4b780940b090b2c", + "logicalCases": 4096 + }, + "68030-move-write-handlers-1-original-scalar.json": { + "sha256": "fbfc02e54ea946a152a4cd613f5b07b305277348d6a8d1af5404a600c1b1337d", + "logicalCases": 4096 + }, + "68030-move-write-handlers-1-output-batch.json": { + "sha256": "7ba52147406bf58981bb6456c33a05df3857c652530e8d5c7d1748511213a9a5", + "logicalCases": 4096 + }, + "68030-move-write-handlers-1-output-scalar.json": { + "sha256": "667f9c29d5dd16229426db3a2a3ccf7bfa6d012b3c3a8b1d48e6f9d4dcb42693", + "logicalCases": 4096 + }, + "68030-move-write-handlers-1-refault-batch.json": { + "sha256": "4e5e8e92fd77c5f0d48116404e72dc923091319bd6cae2bf34c72e67b4ccb41b", + "logicalCases": 4096 + }, + "68030-move-write-handlers-1-refault-scalar.json": { + "sha256": "810cf73ad10bf804518ba79e0295b5b8ee0e4f04f5bfc5450138c4e9460bd262", + "logicalCases": 4096 + }, + "68030-move-write-handlers-1-software-batch.json": { + "sha256": "67a77f3f62afd38e85701b9a0eec2ca501ded03f88bc793280a2286d021fa6a2", + "logicalCases": 4096 + }, + "68030-move-write-handlers-1-software-scalar.json": { + "sha256": "95c813c6fdee96aa5bb1cba666c36f65226bd456c569813858da3bc152ebed8d", + "logicalCases": 4096 + }, + "68030-move-write-handlers-2-edited-refault-batch.json": { + "sha256": "1099fd8345510f05513162782bb76eda6ee1998c9c92bd204910b6e78742c5c1", + "logicalCases": 4096 + }, + "68030-move-write-handlers-2-edited-refault-scalar.json": { + "sha256": "af99b353dbe1f4a6b57119e86055d5484227e175a58249c295b4a37001a18878", + "logicalCases": 4096 + }, + "68030-move-write-handlers-2-original-batch.json": { + "sha256": "94f7f408516c3719c7d67f8d229455c3e46b2b7aac660995a48a098e86ade2f9", + "logicalCases": 4096 + }, + "68030-move-write-handlers-2-original-scalar.json": { + "sha256": "b04d150080be5e4969a28b9475d45350c8308bcc750ec55942fea751a992f1b1", + "logicalCases": 4096 + }, + "68030-move-write-handlers-2-output-batch.json": { + "sha256": "2bd9048f9c1cda57be51d4cb825f7bd83a172adc99c3c3f3b17f94068c5b1c63", + "logicalCases": 4096 + }, + "68030-move-write-handlers-2-output-scalar.json": { + "sha256": "9fdc9c1c3cef93abc3623962ada40c23ab347b84507a3ab371f7a924764a5561", + "logicalCases": 4096 + }, + "68030-move-write-handlers-2-refault-batch.json": { + "sha256": "107c9765f2f570ce3594c0a30ae8b2c8f0b677179d39d7964e23188d9d7c6ae1", + "logicalCases": 4096 + }, + "68030-move-write-handlers-2-refault-scalar.json": { + "sha256": "933da98947483c5c4b05be1f0da868d827ff40b300779b99c202da77eeb8efd3", + "logicalCases": 4096 + }, + "68030-move-write-handlers-2-software-batch.json": { + "sha256": "650e22b21ebfec62ab41ebc7ace422a7553aabd873d4321ea8f77ffa63887233", + "logicalCases": 4096 + }, + "68030-move-write-handlers-2-software-scalar.json": { + "sha256": "579df631c6b17ecceaca5051a247efbd7eba23967185063490676e43aad8d4aa", + "logicalCases": 4096 + }, + "68030-move-write-handlers-4-edited-refault-batch.json": { + "sha256": "4502c70af1f75dd13fcfeef2a2fea9e11877fac824d15507a70f9131bcf8ce7a", + "logicalCases": 4096 + }, + "68030-move-write-handlers-4-edited-refault-scalar.json": { + "sha256": "b250dd958287a68689149ba13871685b5f774032ada45b8dcd0bac6788986ac0", + "logicalCases": 4096 + }, + "68030-move-write-handlers-4-original-batch.json": { + "sha256": "577c33a67eba7a4ef7087dea88da047a3ec0a0550f3652dc56202e66fef160a0", + "logicalCases": 4096 + }, + "68030-move-write-handlers-4-original-scalar.json": { + "sha256": "057a2b0d2f31f76edc8add740ae2ade365c7a5946d410227e66edab64f87d995", + "logicalCases": 4096 + }, + "68030-move-write-handlers-4-output-batch.json": { + "sha256": "52ce9679321d2ac50e34d06d78f3b16983e63bd2bbb59447538ef87cc67d074e", + "logicalCases": 4096 + }, + "68030-move-write-handlers-4-output-scalar.json": { + "sha256": "6a2d82ff9e7d1ce46cf99a90b99803892a8b89633963712b65008d11f6cb5cca", + "logicalCases": 4096 + }, + "68030-move-write-handlers-4-refault-batch.json": { + "sha256": "43436e50e9227d9908839e815fbde22c1d74e6b67a5cf291c0eed5b24758e005", + "logicalCases": 4096 + }, + "68030-move-write-handlers-4-refault-scalar.json": { + "sha256": "9f825c2e4afe14d257ef13b59422a53ef16e5022a50fd4f16ec86c8c1dcb638c", + "logicalCases": 4096 + }, + "68030-move-write-handlers-4-software-batch.json": { + "sha256": "b03ad013a6856b7b727e2f0317580815c6cf2bae9c547af391ede73a4bbd4f54", + "logicalCases": 4096 + }, + "68030-move-write-handlers-4-software-scalar.json": { + "sha256": "953a0ad3a5467f8aa3f413f1701c907fcbb30bc3e79efbaf8504d954d808c205", + "logicalCases": 4096 + }, + "68030-predecrement-read-write-1-control-0-batch.json": { + "sha256": "8ad5e2219e2b54a3253e1cee7da59e3ca8685cc9beeac8958939499044e24dcf", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-1-control-0-scalar.json": { + "sha256": "6f4d9a2ab3a3691c9ffcd55dee734a264edcf8db974543ef85f35513ce6e7750", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-1-read-0-batch.json": { + "sha256": "88be8ca38cce9206c6a96e9f53223cc1cac7d1f1ae9c762b6279d4ad44151f13", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-1-read-0-scalar.json": { + "sha256": "9b5e651bea6c21e8999458d029fa3d1d28e7e32c074629d8024efe41b1abe8fd", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-1-read-write-0-batch.json": { + "sha256": "03405ed4262c95e79f97f6d460bd873dc394e95db6555d259eaea805e9dfd1b7", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-1-read-write-0-scalar.json": { + "sha256": "9895792b50c3b0f334e0ef0b853787106efcf256a2dd02241501619d37dc37c6", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-1-read-write-refault-0-batch.json": { + "sha256": "3afa22d002f92c29957f3677e7f407e2962d7f47975035dbe35fe7ca422c8aed", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-1-read-write-refault-0-scalar.json": { + "sha256": "cc19f9de48f024500a4c6204f6fb3c30f9e2d7e09dae5aafb4939b90a8a80e53", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-2-control-0-batch.json": { + "sha256": "5fa7c669c6566bb9ceaafd7a8b16beea9f720c4916e4c48ad43661876adcdbe8", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-2-control-0-scalar.json": { + "sha256": "4470c2b500cacdf89db69913ccee6946ee6d7cb0787e393706607276c75720e1", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-2-read-0-batch.json": { + "sha256": "447be5ac1f22a2398eb0c35777fcd9a71a2b8a4f36897253b2fc35b3478c79b2", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-2-read-0-scalar.json": { + "sha256": "bd034c9536dcfd2f2bebec7ebaa73658fa5b95d86a2f2431d8cbc646e05cfba3", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-2-read-1-batch.json": { + "sha256": "aefcd45d5d023e032bc4e28a0b41d35c43079167eeb517576cb7789eef6551e7", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-2-read-1-scalar.json": { + "sha256": "f5c7d13a651220c7101cee4354cc292761edf010d693e7938d69e9f8ac02b328", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-2-read-write-0-batch.json": { + "sha256": "0e239c3a6342364dbc6328bf857e5cd305a0b968fb1e6ba0a47c2541c9024686", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-2-read-write-0-scalar.json": { + "sha256": "fe351b3132feb4ab95e85a0ac18c4f49e453b7031fcf413cecabbc7ab3fdf2f5", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-2-read-write-1-batch.json": { + "sha256": "c180e40ef90b38461c1506e7144f85461e705a7941c816e869c59dadf36e3c41", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-2-read-write-1-scalar.json": { + "sha256": "293f2e64090478696cb01b1ab419fa6a405894c5317b0cbf821d386d5a75c131", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-2-read-write-refault-0-batch.json": { + "sha256": "1f1f2749a0537c08dccf8fe8dd35bf488dbb0b13ea6d6a872aeb4a5d2543078e", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-2-read-write-refault-0-scalar.json": { + "sha256": "b061f867f009ed6c297e344fd6b35fc935a35c4a9bd199243c22a70c8d01222f", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-2-read-write-refault-1-batch.json": { + "sha256": "980fe2b23d0e3318c26de2713310a6faf13749acf495da18fc24b9ad66b49d44", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-2-read-write-refault-1-scalar.json": { + "sha256": "708b0372d573f70db3c31b4555a8f08631fe0d8cac5f6e70d84665eae7158745", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-control-0-batch.json": { + "sha256": "129829e459e98558ac7ba4c58ad41f0119e6e067cc1aa5280c40a558c5250736", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-control-0-scalar.json": { + "sha256": "c0540304569987514d9441114fa62920840f117727dce3fd032a373a40cc0143", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-0-batch.json": { + "sha256": "faa0c4ad16c653a0d6967fedb4c83de7d8b119210d62cbdec5c3099234b01951", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-0-scalar.json": { + "sha256": "f2fe163a00f4ff09b9705c675faddcd0a31a82ec0ea16805333ac6bcadbbf232", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-1-batch.json": { + "sha256": "9875e8628ae50647add8c2f01d99b3b16b968fe0eefadad860822a961d36d465", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-1-scalar.json": { + "sha256": "1b20aa4b6672b5e79e530b0adc4cae1cf83c4cb334bcb8f87bec0cc1d9f444b2", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-2-batch.json": { + "sha256": "428e449dcb47a0621028f2d147baa99bed62b4f642b6c76e58d38030ab5e726e", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-2-scalar.json": { + "sha256": "9623532bfe4ae154e8ae2a3bded51386a736b96b9528b31d7241a9a0f96b9fff", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-3-batch.json": { + "sha256": "7825cb95e58e35ccbf99949bf2e78cb67f5e4a50d21a1330c00097ef32fd470a", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-3-scalar.json": { + "sha256": "69962586bb5778193c1a068bd7ee7489e7b0c03f601e6eb9064794a8ccabe16a", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-write-0-batch.json": { + "sha256": "6b86de31044fabdc439552b997acc605430762c5ced3c2ac1d41879260eb711c", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-write-0-scalar.json": { + "sha256": "d4314a84b5f79665d199ac14b92dd548796e2789625ae80746b7b58dd3094f22", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-write-1-batch.json": { + "sha256": "ec4d49bc398b74e4b0a619bc32e9f57f2ccddafca71a27fe933444cab1f6f93b", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-write-1-scalar.json": { + "sha256": "7b82ae12d617c98f05781b020a62c06d5ae10629f7f8d0d1d0910fe433dd5162", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-write-2-batch.json": { + "sha256": "aabf32deda14c5bdd6561969afad5a6f5ec4de581cb1e2ce453b8a61205c988b", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-write-2-scalar.json": { + "sha256": "6fd2836629e2b29a4ec274036c9fde6df637dc87306fd101da05f0cb68117ec1", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-write-3-batch.json": { + "sha256": "0829ffaa6ee0aacf0f6d7cc536663d38dbad05189e3c43650e704010388c7cdd", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-write-3-scalar.json": { + "sha256": "358e4b15f8cd6f88e255b22c1ad83d790d53d01b2cf7f0f4ac6cc740fe4dd14d", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-write-refault-0-batch.json": { + "sha256": "ee2960c22e558efc45cf49dcaba3d7ee5175bfef3fa92aa444c39a8070dc2b46", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-write-refault-0-scalar.json": { + "sha256": "5192824c1cb69f115e4450cfc1ab2c00f497881defae8588723b6deec39ac4a3", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-write-refault-1-batch.json": { + "sha256": "782188730a26d67d649ff2c35c16109faa5a30f9f5350d1b1db3239c625888fb", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-write-refault-1-scalar.json": { + "sha256": "db8adfb1e4c560c696fd6363e1af0165e9d0d84426ee248088ff0d579b44d100", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-write-refault-2-batch.json": { + "sha256": "b69bb018be155d3588c56f333f2b0ca5e0477b610e74417941a94082ba0865a5", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-write-refault-2-scalar.json": { + "sha256": "b52aee3b26f75ba49d8062f52b09bba35ee245426a0e8f58215e2e8004a2928e", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-write-refault-3-batch.json": { + "sha256": "f38ac9f5be51d46e4d5539320d20d8495cf2e3d50f6132ac096e9829d86f10d5", + "logicalCases": 8192 + }, + "68030-predecrement-read-write-4-read-write-refault-3-scalar.json": { + "sha256": "6d7eaafe5d66aa008d3393cbbf420d961a13d9bdf142aa4bc7ff1f14d3d87e63", + "logicalCases": 8192 + }, + "68EC020-memory-read-write-1-control-0-batch.json": { + "sha256": "3f3177551d448fe111207d2527f5f8aace610f8f0fbd1c75b80814ff19d1bd3a", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-1-control-0-scalar.json": { + "sha256": "0aea3e19af9ca72998875b28b5b9fb5ceabe6bedfd1aecd8a0acde53d6155171", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-1-read-0-batch.json": { + "sha256": "a5e96bbaaa135d97e06c1d675afbb2a9366591d9a6bef9064091d1780c7e5bf2", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-1-read-0-scalar.json": { + "sha256": "f7b03d79aafd81a717771ba00d52f88b6dce178b9f61b8db7184a706bd993aa6", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-1-read-write-0-batch.json": { + "sha256": "c5427f9ef5a45f5e944fe7aa8e128b365cda4643d792ca35b245cba7db3b857d", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-1-read-write-0-scalar.json": { + "sha256": "e81397fa416a0da11fcc5b006caa22d22ff6c95e098cb0d8ce87a793b91a1646", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-1-read-write-refault-0-batch.json": { + "sha256": "e39c7a2ecde6ea99e4f2036f3874050575c897b59821a793c4cd805820cf724a", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-1-read-write-refault-0-scalar.json": { + "sha256": "6b8a7fbf91126d4755c620cd50cca81496f9b021cd507353d3779e91110d508f", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-2-control-0-batch.json": { + "sha256": "08a7305f57370a5aaf1eaff7e5f97b0a1fb8876f9b132fd3143ba333ebf21916", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-2-control-0-scalar.json": { + "sha256": "a761b4a7e8359d33bd62cc9ac5a73abe5ce801166abe987f0e5b7b6462df4312", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-2-read-0-batch.json": { + "sha256": "d304624baa2e2866066c089581de869715a4b846a09b441bdd3bbed5cf3acfcc", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-2-read-0-scalar.json": { + "sha256": "1ce298daf5fa68667c00bfaf9a80d03a33e21c8f42dfc070bad3f9b1327475ca", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-2-read-1-batch.json": { + "sha256": "3e4753936eb7f274c8d6a8383107c69217a3ff611055bdc6c060c03ce703c8b0", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-2-read-1-scalar.json": { + "sha256": "1f4da85515dd236709079b94054cbcc158676cc630b50191980fbd3fdbafdcf1", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-2-read-write-0-batch.json": { + "sha256": "4d09bea2b9bd2c55eaf43eb748dae2d53e0d85a73f7b1a0e7d6adad69a06e1e8", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-2-read-write-0-scalar.json": { + "sha256": "2b3dec7eb666f071a44d0ff452cc13c671b670c02f527b7854dce582fe505107", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-2-read-write-1-batch.json": { + "sha256": "5876b1152862c4b77e4d6dcd94c31057b655c911d87f930f8e7e0c0bb39b5d2e", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-2-read-write-1-scalar.json": { + "sha256": "e03a0d6095d20ebb39ebe3d5c1ebea38cfaebad194428b6b5b097edd3bbca677", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-2-read-write-refault-0-batch.json": { + "sha256": "fc3f3dd3fb920d65577f831488e6c6d25046d043768090828ff48feb39274f92", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-2-read-write-refault-0-scalar.json": { + "sha256": "c8f81fa41c26c18a9b0444c92894b552d42e754cedcdc406458fd1c305d36bf1", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-2-read-write-refault-1-batch.json": { + "sha256": "28875737e4e5dd4d2ccb5b400adcce072e1b1c9767903d392fdfc3d10304d482", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-2-read-write-refault-1-scalar.json": { + "sha256": "74f3b289f72f89b25a194162860a73a49b7eec3b67a9bcf50786c233ec341ee5", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-control-0-batch.json": { + "sha256": "3fadf7169f29ddc891a7a017a0f136512b01b3919427242e722768b007ac05bb", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-control-0-scalar.json": { + "sha256": "9fce5db11349a64e7754ed23708ae21ab7d053d4aa4d9f9dd169a4a6d06e9974", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-0-batch.json": { + "sha256": "26d8d7e85becbd98c8d65431c6a49726de88eb7159dd08bcc732c37ce8134a8c", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-0-scalar.json": { + "sha256": "564afb89ad87512b4671e1b76a5e188a4b860c40227ba0ebdb93fc130f01f54d", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-1-batch.json": { + "sha256": "31b07f09350d777c189a41c92e341adb89780e86aacfacd0d5bf1fac750bb593", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-1-scalar.json": { + "sha256": "b28e1536420b95249c1471b61e297684ee1ba4e7475db1dc1a33a6dd037474bd", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-2-batch.json": { + "sha256": "996c2a6e3838f6b43a045e5e5e2e7a9d5ff2832adbc5621c7e11b738aac6ac2d", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-2-scalar.json": { + "sha256": "84d6c1f89988d3217723b6ae3d4ef3eaf40d2ee7417d5ff3071aeedf7f345b0a", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-3-batch.json": { + "sha256": "ff6da683dc5393027ce77baf97b379fd0e658b63b61db72ec437dc5244540b4a", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-3-scalar.json": { + "sha256": "7251896d89ca61242cfe7ce1c6197c4cbf408a627cf1d73b780d7c527d84688b", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-write-0-batch.json": { + "sha256": "f18810455201d2b3f80eeb66972afd654cfee8876a823e753bdaf36c52cf2c05", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-write-0-scalar.json": { + "sha256": "cf20047845d76303372cb41ee63722a5260effd46c5faa9563acfa45278a18f4", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-write-1-batch.json": { + "sha256": "f29b60dea3841f67fde94dea5dc5c6d49fe39318789dd74edebf273174193f67", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-write-1-scalar.json": { + "sha256": "40d3ae2cbf00c3f5869085dc51865d646bf2bbc3f2379e9d646ffda6f1f54a77", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-write-2-batch.json": { + "sha256": "dfd57922b4928b0ebd9bc8f3eff3c567e07d338fa3e3a216a336bfdfa65ed036", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-write-2-scalar.json": { + "sha256": "1c2e813376216a3ec4a6a7c919f63a0cd3399b04e3da5d8200ee83437630fe79", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-write-3-batch.json": { + "sha256": "454a7438da7cdf2a345faeb45e477b9ec007cfafc913d544e0c3bd90603c6d29", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-write-3-scalar.json": { + "sha256": "9ab2051b615ad92558f402b78fc9ace6e25cd39dc783a1551ec5853e08b0012d", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-write-refault-0-batch.json": { + "sha256": "7b20bfa9f41529059f7e86fee0e07c2c7d262b98d9785454a499b54a88215465", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-write-refault-0-scalar.json": { + "sha256": "0f162cb56692efdb3a731ed56088481a5c9277586593bb83f30d2918363451f8", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-write-refault-1-batch.json": { + "sha256": "69840836363d5c4949ec3a42923eeb21510221583fc79413db5883dd4fdffbd4", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-write-refault-1-scalar.json": { + "sha256": "40ce0f8af427ea227adc1bba89fac2262d1d0ca84a789cd162d1e5fd7f870aa6", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-write-refault-2-batch.json": { + "sha256": "896e46dc979c999697491e861fe6377ebf2bdf72140649899d52251b2b7795cf", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-write-refault-2-scalar.json": { + "sha256": "3ac1618e2dafe31f5764f1fc65300ac601d75536b68031dae5ab0c9379a257f6", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-write-refault-3-batch.json": { + "sha256": "a3502185a60e0754811ca4567effbef4b50791bc1a75ea56edf81a69ae7e296e", + "logicalCases": 4096 + }, + "68EC020-memory-read-write-4-read-write-refault-3-scalar.json": { + "sha256": "8db3392cf7a363ee23b597c266024af969af45f72d0fdb499f636cd413cbe7c0", + "logicalCases": 4096 + }, + "68EC020-move-predecrement-write-controls-batch.json": { + "sha256": "81588c9aa34e465704509155b76294d1277648719f2fdc29ad00cfbdbf1e5b92", + "logicalCases": 24576 + }, + "68EC020-move-predecrement-write-controls-scalar.json": { + "sha256": "a95eb7fc9107a4f3f1ab1d3601fdd699b215ebe5a019e88b70724aef9998b1e3", + "logicalCases": 24576 + }, + "68EC020-move-predecrement-write-fault-batch.json": { + "sha256": "46817d979921a90dd8788fd19abb4f478d11ed52e4387cffa277c313dc78698f", + "logicalCases": 24576 + }, + "68EC020-move-predecrement-write-fault-scalar.json": { + "sha256": "22798b8cb4cbef2c7a7ca154402de31b469eb983d88e4a5dbf4104b4c2ee3ea4", + "logicalCases": 24576 + }, + "68EC020-move-write-handlers-1-edited-refault-batch.json": { + "sha256": "af3c9c19543ae43eae95dc6b973edf30fa755a19b69b9ae7ec54bd70931bef44", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-1-edited-refault-scalar.json": { + "sha256": "bfae1d207b0d0a8518c902340fbc1dbe53f8e4c35841faa6c4e62b9b45323960", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-1-original-batch.json": { + "sha256": "c7272dafb0ae7849b78be2e3af16c2e1d784a7356822c6f7fcbf447ef60aea7a", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-1-original-scalar.json": { + "sha256": "7680784db06cc59cda3658fabadae1b73b907e12c1f588effc8243b3b7366a50", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-1-output-batch.json": { + "sha256": "f5c96ffab8c622f4b7515e50f2fd0587916bfc7da7c94e7141b404559060840c", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-1-output-scalar.json": { + "sha256": "d6236c80198218dfd7155c6a92d2c1ac2370898aca9eb6de198850a4f6c3861c", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-1-refault-batch.json": { + "sha256": "b33c205379e9a7f91be22d66b909c8d8d558bec39d065c7d61377e69a3946034", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-1-refault-scalar.json": { + "sha256": "66c1ed6aaac03568d4971db6391c935b9f09727d5ca1eaa074315ea948eb13e8", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-1-software-batch.json": { + "sha256": "82709bf0cf34bab7c56dcd7bddf7457a7696832e1e26982e1475c70a28ea5ff5", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-1-software-scalar.json": { + "sha256": "ac5a4259898be7ff41819c806c5f1e537ca00b6608f4ad67b9b784521cc86349", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-2-edited-refault-batch.json": { + "sha256": "aa03378cb7809622fc16d92308d22aa57883c39a40499ae43da5ecc86153112b", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-2-edited-refault-scalar.json": { + "sha256": "703c9792948db1caa7ff15984e014033cca5d5813a54a9038e931f6c38c548c6", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-2-original-batch.json": { + "sha256": "7c571bfb67ab68334b826c0fb37e6ce60dca203f03d1a88ba713fc5d9eb2b8c6", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-2-original-scalar.json": { + "sha256": "a379c537d03370f0696ff49c80b80ea40c6761ea16667088265ec65591f46a18", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-2-output-batch.json": { + "sha256": "99cbf7d7f1385f623c8277340b4993b6612474044ce29d80ac20857aafa1edb5", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-2-output-scalar.json": { + "sha256": "f6c4b59200e48bee492b75f7dca5e1d9a9fa4600829637ba7c8b152e6779dcb4", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-2-refault-batch.json": { + "sha256": "a55a87fe8c87603834029b886e8c5ca15b3489ea39891d6ecbec560ad195611d", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-2-refault-scalar.json": { + "sha256": "a4615929ac1ee3707f222c79f41673660fd39643c31227e78b97b2901c8f4a01", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-2-software-batch.json": { + "sha256": "8e2696a25c091d89b23c9fa67c78b8faecddfa67ea376f8ac9c63e4c87dec50b", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-2-software-scalar.json": { + "sha256": "2b6cda252421e99fcd6fa695d999174abeb30aa7592c91967a1fd00f8657cb2c", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-4-edited-refault-batch.json": { + "sha256": "802a4afccdbc99512d2d8b4aab480ba3faccd5f89ed44b0ea985a813c5d04f94", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-4-edited-refault-scalar.json": { + "sha256": "1414675503d92252df69a240491ed164928edaebee94c91d01d7720ec438aa2e", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-4-original-batch.json": { + "sha256": "c0bde4eebae103a3ea0c7efbf8e7f0a57a3964722f82fcda00ad4f137d0bfdda", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-4-original-scalar.json": { + "sha256": "90347f312fa223b2cc828f7c28e9191d91c47497afdde2f92d1b1dae44210c26", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-4-output-batch.json": { + "sha256": "0665fc677f15c111156927f8413a241e8eeb5776a0fbbdf35aeadebbabbe66e5", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-4-output-scalar.json": { + "sha256": "bff66a702d87630fd6138e569ec9c24a4472ce1161b0eb18bc00e5d1700c36b8", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-4-refault-batch.json": { + "sha256": "cb9cb534104a6fe40c6a62da51628dab7356ad8602b7a13d85270c72150198c4", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-4-refault-scalar.json": { + "sha256": "8c86e9461ca2dcbf651fb1fe16396ebe1b823d5d7c33be2037c1c5182ff81bbb", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-4-software-batch.json": { + "sha256": "856f99ddc4a25e96ea95647d0107890789148126ca97e4ba5c3b1f0d0c2f88e3", + "logicalCases": 4096 + }, + "68EC020-move-write-handlers-4-software-scalar.json": { + "sha256": "19d13c96361e839324153c206242bf849b444e6fd461a79e34ead82d05542bbd", + "logicalCases": 4096 + }, + "68EC020-predecrement-read-write-1-control-0-batch.json": { + "sha256": "7ab46f2954ee6f3110405f97bbfb082301fc605230a7a512ecaa6b5c9fb3a0ee", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-1-control-0-scalar.json": { + "sha256": "2f8991943e30172629425dc55d901c5978a0c33f3123953fbee3750717c86265", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-1-read-0-batch.json": { + "sha256": "b2c46af2121533770caeff0f51b114e00281c8f540a939488f6444428be101b1", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-1-read-0-scalar.json": { + "sha256": "4b867c6b42c0f9e98c922743d89ad2a99d6a37c0ea042ea3fec2895fa849c5b6", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-1-read-write-0-batch.json": { + "sha256": "a9dc46c4c50fb5dc8b03cfb2dcffc66f1b4f08d48580aff4cd444ac74820c4fd", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-1-read-write-0-scalar.json": { + "sha256": "87428d04fb787624f09ca9f33908f0048ef10f45153759afbeb224640659ff00", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-1-read-write-refault-0-batch.json": { + "sha256": "c5407913b84ad1369b64864d978895c8b80b0b24bb28d638cbd11b9d0910551d", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-1-read-write-refault-0-scalar.json": { + "sha256": "784711c2248af017cd39f6317e4514bfbdf77258485c946b869e28a3d5ea2039", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-2-control-0-batch.json": { + "sha256": "a12f4ba9a692bd19853d217159742a19422473f12237a708cc1e115268f0ec82", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-2-control-0-scalar.json": { + "sha256": "52a8e775b5ca3f498ead4ca9633522c8afdfd23c25da5a498ed9bc6b5da7288d", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-2-read-0-batch.json": { + "sha256": "441319012b84f0e750168dda9b73bb556145d20a4437b5bd526724d24d2cd2ab", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-2-read-0-scalar.json": { + "sha256": "e87207d9de4c866c17105973b39fd3ad7e4cb760b10c415a8cf3af2a5ab22109", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-2-read-1-batch.json": { + "sha256": "47a86f1c04c5055329573bc2dc55843f5f88ee47824f5823f64733bd21e236d3", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-2-read-1-scalar.json": { + "sha256": "ebf7746d1094134991f59d032d89e67460287e6ab230c5388de990a7d7c7e2d2", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-2-read-write-0-batch.json": { + "sha256": "1d0ed43d5bca842bde55aac8b853752d5608160b8c573cede402ebab2d48c07b", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-2-read-write-0-scalar.json": { + "sha256": "73963c7c75cd0bcf72a1f1d6838f744931e0142fbb5c68ec7a07e43840fee9e0", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-2-read-write-1-batch.json": { + "sha256": "cc3ac357b1e6a8fb083967997bf1699b69ae92595a797795b1ef9eadd0ccebd4", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-2-read-write-1-scalar.json": { + "sha256": "5e9dcb9f854446799694746751142308dfe90d1e12d276c58284d640af2e0e69", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-2-read-write-refault-0-batch.json": { + "sha256": "2c060b1635a6984fa23ed3db18e2f8d93610bd679a6c4e93c650f311debd2920", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-2-read-write-refault-0-scalar.json": { + "sha256": "7b55aeb3cf65277e37c542903d931ef4d7db6899771763e1214674187d1cb2a9", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-2-read-write-refault-1-batch.json": { + "sha256": "2f296d14aebce3c18208546c47d7ec9c8dbafee6e512e78c24beacbf7d445575", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-2-read-write-refault-1-scalar.json": { + "sha256": "227197c540fa856ef610e6f878a39c364b915bf73d7c23a12ce93fc36fb47ad5", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-control-0-batch.json": { + "sha256": "f76ae70768a8596dfb4659e7fe56abfe61eabb53a3b51b494752d9fa40afb456", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-control-0-scalar.json": { + "sha256": "d08cffbf4b3e9a548291e9b06bcaaf1f7979888e172b8c90b6dc401cd60025fb", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-0-batch.json": { + "sha256": "57ae6a75f0f62e8107f588f11313a0e798acaf61813fc0398e0ea72e5d934e5d", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-0-scalar.json": { + "sha256": "3b1867039e19920a0a8be162147aee5722cf975a2ea2860f9dce2d84cd9aae3e", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-1-batch.json": { + "sha256": "9ea7582734a387df233439e1036d5c09c7ad398278601ff2bd1bf273716287e8", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-1-scalar.json": { + "sha256": "6ec6087be7bbc4bff6a497fd9e4834b1ffdb6f01236edc0480e4f32f93aa697f", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-2-batch.json": { + "sha256": "2997c8db34011ebd07e6a28093157e59eaeca5f224d6f8cb1ceef4a40ed39dfc", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-2-scalar.json": { + "sha256": "814a9a094f100b05175781c07ab910fe1808a9d08a6350d41f4bb1f9f536fa12", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-3-batch.json": { + "sha256": "5c72c1c88f286cf789289b1c046bfb5fff2aa036ab8bdb57d59430e94b073d59", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-3-scalar.json": { + "sha256": "a120b6c326de69e76ca91d9e10f26eb719542634dd75e9ee7447d6e10ba32c17", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-write-0-batch.json": { + "sha256": "bf37525d74f8257ff56b57d22246771cc8225d6ce44cb117409d1cd4d5493f09", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-write-0-scalar.json": { + "sha256": "4d043e73c5eea8a3804bbd2b43261e0972bf5ebc204389d3b5a64ea4e0c7362a", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-write-1-batch.json": { + "sha256": "a61e533be4603cddd6745fe3659017d7dc7d85159bed6ce44ebc405f9eaf441c", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-write-1-scalar.json": { + "sha256": "a13a250972dcd0cd7d4fd9f4b370463ea7738c7c81e2255c94be6144adfd6ee3", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-write-2-batch.json": { + "sha256": "ac47a7c52775768fffb9168a86e46673b02b8f65bd5bb2ffc431bf42a93e1028", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-write-2-scalar.json": { + "sha256": "b8d9894f8574d84209229e47e0078c2dad8548a8bc028959d7b934b60b7ba55f", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-write-3-batch.json": { + "sha256": "6fefbff2f6165355a485f5bfe0626c790e52b1aa6e1c3fe4b53ed09a4e89e5f2", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-write-3-scalar.json": { + "sha256": "cf0326185cdc8d11cdef2172b09b475e454658b8c2d3f8682e50f0e0427d8d71", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-write-refault-0-batch.json": { + "sha256": "b4a9c31785736cc57bacd227af7bd42b7cb81d7ee5136c050510298348c3c8b3", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-write-refault-0-scalar.json": { + "sha256": "8097c98def5dd2c7eec9771e52071a6e533cbdefdcb650ee9f716071580ab51d", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-write-refault-1-batch.json": { + "sha256": "5dd828df8e2f600c412261e4057a9f01810321d7eace42cddb8d88dfdba0afc2", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-write-refault-1-scalar.json": { + "sha256": "0c3caf798eb514d664c711e1e4e0b65dedd28c04492e524d89bb729821286c1a", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-write-refault-2-batch.json": { + "sha256": "26e016321aa75e55f6a4dae750cf500ded3aafee932d0d2b96ec9a1c7fed8045", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-write-refault-2-scalar.json": { + "sha256": "95229953b4eb5abad065511cb4a6113a4cf07f56d475496025c9d77326a4ef43", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-write-refault-3-batch.json": { + "sha256": "848d3372a0f7dc5dc8432ed51a534731669a03dc9e546f5539a8e38b14db28d0", + "logicalCases": 8192 + }, + "68EC020-predecrement-read-write-4-read-write-refault-3-scalar.json": { + "sha256": "bc3f22627b436a7e5cd35c42eda52e4c7bca9856b4c1d60b0bad027600be8e70", + "logicalCases": 8192 + }, + "A1200-memory-read-write-1-control-0-batch.json": { + "sha256": "44064b5c4fdbf2d8e7b66d17ba9937e5a333d936515ae4f283ee82d09e5d7fb4", + "logicalCases": 4096 + }, + "A1200-memory-read-write-1-control-0-scalar.json": { + "sha256": "885b4d2a9f47844ae93db61223763688e2a4e1c35b42c287006dc37f74ff334d", + "logicalCases": 4096 + }, + "A1200-memory-read-write-1-read-0-batch.json": { + "sha256": "8491f6a4d4a4537219b5e364c26455a78f1718fbba59859099180b4773c9cc34", + "logicalCases": 4096 + }, + "A1200-memory-read-write-1-read-0-scalar.json": { + "sha256": "2f14498077d69ee557047944f8f71e8519e41e769bf19928e21d29484174b64a", + "logicalCases": 4096 + }, + "A1200-memory-read-write-1-read-write-0-batch.json": { + "sha256": "72711338ea86a58d9dd821535c814d60ecf327f3cba9124379ceebfcdc094d77", + "logicalCases": 4096 + }, + "A1200-memory-read-write-1-read-write-0-scalar.json": { + "sha256": "a30b59de9136e1d13e67341b1d894f0244d88396cc237931e5a4d6735a566236", + "logicalCases": 4096 + }, + "A1200-memory-read-write-1-read-write-refault-0-batch.json": { + "sha256": "9ba5db1beb1da5a6e50b35bc0a27d0fd47eb181be692dde3916e3809ac13982d", + "logicalCases": 4096 + }, + "A1200-memory-read-write-1-read-write-refault-0-scalar.json": { + "sha256": "119b8d89b6dce53c34e5027ca3e73fe6039493a9b44d574929853bf40fdcecee", + "logicalCases": 4096 + }, + "A1200-memory-read-write-2-control-0-batch.json": { + "sha256": "ad1d71c45911021093fed5d7b7b742465153459ffb84973925a8cf4e25468fdb", + "logicalCases": 4096 + }, + "A1200-memory-read-write-2-control-0-scalar.json": { + "sha256": "3c617cc4a6960e7ef50eb37f29e9e838c19d34dcb09bd0c1950b58adbdc5ce14", + "logicalCases": 4096 + }, + "A1200-memory-read-write-2-read-0-batch.json": { + "sha256": "37b2bbb7d5b820a569e3c6b36a3ce08fd4dc17f38f6c731d640477cebffa4ae2", + "logicalCases": 4096 + }, + "A1200-memory-read-write-2-read-0-scalar.json": { + "sha256": "65c4c9cea7db4409ecdfe32ba0da9955d871afaab8cace65c4cc9eba89ade87c", + "logicalCases": 4096 + }, + "A1200-memory-read-write-2-read-1-batch.json": { + "sha256": "b1ab7cffbcd8c98020fa9f9b75c2a8129db0f1fc18a268295ff539728120d3f0", + "logicalCases": 4096 + }, + "A1200-memory-read-write-2-read-1-scalar.json": { + "sha256": "e215a6f1949050883ed8f49cc1105b93776c87d3b4687e2ce1d97a8ffbc0f047", + "logicalCases": 4096 + }, + "A1200-memory-read-write-2-read-write-0-batch.json": { + "sha256": "4829b858840e516cda0cc2b0fe8af06e31ca20c592eb472a7a911b0741c19baa", + "logicalCases": 4096 + }, + "A1200-memory-read-write-2-read-write-0-scalar.json": { + "sha256": "2fcac28583cd42bb62cae14c93ca52fea1369b2fd9706681a9418b5b0119ec1a", + "logicalCases": 4096 + }, + "A1200-memory-read-write-2-read-write-1-batch.json": { + "sha256": "b7b9cd5b5e1198a1bf9c15260fc66aedc42d005b277ade8a3603740951318e4e", + "logicalCases": 4096 + }, + "A1200-memory-read-write-2-read-write-1-scalar.json": { + "sha256": "0317ba313da5f277f42bd71f5a28927770fcdff60797edd6d9325c0b05230274", + "logicalCases": 4096 + }, + "A1200-memory-read-write-2-read-write-refault-0-batch.json": { + "sha256": "c96a5f7d4cc9a0471f2466259aa9841fc117b3dade525108331004494686e8ca", + "logicalCases": 4096 + }, + "A1200-memory-read-write-2-read-write-refault-0-scalar.json": { + "sha256": "30fc73ead9dff6ed2340af8a50841154509e70776bba5a4e6edef3188d864d8c", + "logicalCases": 4096 + }, + "A1200-memory-read-write-2-read-write-refault-1-batch.json": { + "sha256": "e45111a692f19b5756602dd0036c3e0a28f6f49537905d76ee8597888865a5b3", + "logicalCases": 4096 + }, + "A1200-memory-read-write-2-read-write-refault-1-scalar.json": { + "sha256": "11e5531669888c6b59e7aca067122fdd770e321e522b3c25abffbaa8254e9408", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-control-0-batch.json": { + "sha256": "01f9c5cb93ec80bf664208dcf40daba62cfdf6786c530143089ab90ff8326534", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-control-0-scalar.json": { + "sha256": "741585c01a8af45e402e470be726f61e2e2194cd21cb4d0f244e962c52d0a9e1", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-0-batch.json": { + "sha256": "250059a56817d65ffa78604219d6a7d77e31d08a8d1bf59a891ca0be7337f54e", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-0-scalar.json": { + "sha256": "a881e4bfdd7ef9d0eebdd9b6443c48324c9f0828a07820a60476e3b6ee888549", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-1-batch.json": { + "sha256": "983fc72bc56e59a32aad8830d1001f6f41c0fd6703e38a326aff94ce5c6458e8", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-1-scalar.json": { + "sha256": "0f1b23d28fbbea5c8e5e1332fdfc907728cc5df090b53ead3032a54c6a3a73c0", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-2-batch.json": { + "sha256": "5fa0224ae11a9df9b5652bb6d7f1516afe337c641ae108393a72517b8784ef99", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-2-scalar.json": { + "sha256": "9ddf6d7a61ab76524484c73c82cd43d0c9d7c17c30df208b6ee6148b19491fc4", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-3-batch.json": { + "sha256": "5725d0eeca2c520eab47a603e3699c515790d65e8760b31f7d7e75cdfb1c4475", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-3-scalar.json": { + "sha256": "2555cf9878aa3219ca9de7efe258d9e6d65a9c97d96f9c4ff9adfa4d5d7233b7", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-write-0-batch.json": { + "sha256": "cc39c674e5ff62216a2b9fded28f1f1298f6f9a3ada9186ff1ae523e5462e33a", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-write-0-scalar.json": { + "sha256": "314d1730af31521ccfbfd8cdc0ddcfb542324a977fc3e147d322c0207f7d230b", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-write-1-batch.json": { + "sha256": "0d70fd3d0bffb620aba6afa41c20bcabb8d1eefba32dfa394b4f4c683b28914e", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-write-1-scalar.json": { + "sha256": "6928f5ea3f822ce1e3fa12044c6bf8fb50231d795fe6cebb65a77eb79169c768", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-write-2-batch.json": { + "sha256": "c0960606f5fd737a0fa1cd3401f4bb4f11a67591a73b834d85ea99d10a38b8bb", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-write-2-scalar.json": { + "sha256": "34382bfc3c1fb888e5b75e696b9c490904254cb0a0ec3b2cd9747523b03ea90d", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-write-3-batch.json": { + "sha256": "b002f89d1d959b17308ef7bd53f8da9d7673a8628a1290fad468f61f63ecf75a", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-write-3-scalar.json": { + "sha256": "0653ef9cc7045eb4275a96df238409b33d5c3b85ed89cd1cfd38bdcaf99d156d", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-write-refault-0-batch.json": { + "sha256": "0837d7a5575c8fa7529c4b04d5d2989c6fdbca065c921d63e6325022bdad79b5", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-write-refault-0-scalar.json": { + "sha256": "2565305afb8f84103d9b704fd6796f2d44aceea9c6f953be50c008025625437c", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-write-refault-1-batch.json": { + "sha256": "0ed8557f2cbdac3dec428bef6088051d6287f2aa16f5a268dffa6d3009011fab", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-write-refault-1-scalar.json": { + "sha256": "baeafdf0da1d8799b349d676d1c295bd0dc8cbe6e223fc914e20c8870b165788", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-write-refault-2-batch.json": { + "sha256": "7f02f4fce885f96b1ca51aedcef91f17d5c9959f02f46663754fb995bd2f8a53", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-write-refault-2-scalar.json": { + "sha256": "21498a2f1a72999bdd06da3d0bd514e4838d1b3bcdcba4142c2e896391774281", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-write-refault-3-batch.json": { + "sha256": "7e9f4a7322698052d66c93f0df7745091fe974c12f92034c1d1570b7a8aaefff", + "logicalCases": 4096 + }, + "A1200-memory-read-write-4-read-write-refault-3-scalar.json": { + "sha256": "6e7df33367d855d8d72095802c8490a52f30737b0a939df6935d9e15be67d5ce", + "logicalCases": 4096 + }, + "A1200-move-predecrement-write-controls-batch.json": { + "sha256": "c8ba73a367c95a1ba3388f85fcdf0b95db0c02c5390da62ee975f5fcd3e9367e", + "logicalCases": 24576 + }, + "A1200-move-predecrement-write-controls-scalar.json": { + "sha256": "e8a035b31a1481381e275ad1a3b2b071755e07c049b46135f468e823c57a76c8", + "logicalCases": 24576 + }, + "A1200-move-predecrement-write-fault-batch.json": { + "sha256": "6bf77fa4d295017feca8afb1b896479889fd2bbbbfeeed3e720a4fed83ed4404", + "logicalCases": 24576 + }, + "A1200-move-predecrement-write-fault-scalar.json": { + "sha256": "1fac843c1105f886450107bf0c429d13ef18e75cf520f8900f122eb8473539d3", + "logicalCases": 24576 + }, + "A1200-move-write-handlers-1-edited-refault-batch.json": { + "sha256": "feff2b8754e0a051fb7ba2f00e3006c0b3f3fb6739a875aeee19471933599b4f", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-1-edited-refault-scalar.json": { + "sha256": "90bc38d81965663e98eab8fbb53d7970ea0d033142080e62a66e5b8f0935523a", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-1-original-batch.json": { + "sha256": "5adda8549ded6c7e5c9c693958e7b85ae9131799d434d327d2db5d1e5b4443de", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-1-original-scalar.json": { + "sha256": "4f804ff9cae2811beb28d30330d701aed74872003f0648fcd6fa94f290d2cf16", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-1-output-batch.json": { + "sha256": "459f61cb43fdb90017e59037a77a0d4f54b725028a22df00bb76cb717bf3da6c", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-1-output-scalar.json": { + "sha256": "006183b5125bef8f654f3952ee4f34d03f714f402f6cb369937f248ff83f4726", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-1-refault-batch.json": { + "sha256": "02380ac59ba9fdaa0d7a4c07cab53c2438618f98290f570c134ce456b4616310", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-1-refault-scalar.json": { + "sha256": "fcfc96ba0cca5d89e32a8acd193c11c1df88ce933047c0ce84d26e938152192e", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-1-software-batch.json": { + "sha256": "3680b2972e61cedc7275e247139eab04709c6c91a5c06a5a931980525bf8bccc", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-1-software-scalar.json": { + "sha256": "bf8487e1379678c4f2ba9a250565cf0ec75afb9a9d37400f391c5a470ece53bc", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-2-edited-refault-batch.json": { + "sha256": "7517ad86f0074e98b2b685cc72f063d4b2313bdcc911a9b878303a2ba45b1f6e", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-2-edited-refault-scalar.json": { + "sha256": "f47cb126d020e29187fcee168f308df06daae644a2b5802f88629bbc89bfb309", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-2-original-batch.json": { + "sha256": "b297eaaacc4d1bf21e3ac7e75b5b1ed33793337e5aaeb8b5ba624aa562d2a3c7", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-2-original-scalar.json": { + "sha256": "187eec36fefe3f62549964a1b4b224a68112dc6698634a86bd893110958cf1c4", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-2-output-batch.json": { + "sha256": "e16ac7c379c38413d65bb665922e020dd752edc1239f1fdfc262131630c404d1", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-2-output-scalar.json": { + "sha256": "4b125407cc949a8e7336c608f77cb2d2e5b4796098e75d8a1c890c90fb5b70ad", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-2-refault-batch.json": { + "sha256": "ef19e3e1059887d84932b82ef5e3abe06bbfec9b45400b44e9b6298b2592ea2e", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-2-refault-scalar.json": { + "sha256": "1655068e37ea7139eeb64fc2c94311d0787512116f32c32c37ab37f6bba7a9c2", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-2-software-batch.json": { + "sha256": "88b052dccb70c18aa943a3c1b5b20b545301d70af2092c88171a2ecc2cf53f91", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-2-software-scalar.json": { + "sha256": "ffb67be49d1a21f7f4362e845411a488e6bd39f4044ce8fbfb123a285b96fc75", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-4-edited-refault-batch.json": { + "sha256": "7a9802e8fb0f19b626312a3a265ee407c6a85079bb1d3132e6524d2fc485befa", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-4-edited-refault-scalar.json": { + "sha256": "527218a7995303a599448a22332180ae8fe9e156c5d0c2d0061bcd49f596d4fd", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-4-original-batch.json": { + "sha256": "3a1b7542155e08352e76cf7537bd78f28891ebc0b58e561c6142aedf7f722456", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-4-original-scalar.json": { + "sha256": "3ea18c4b129a14df436e85f5febc1d5a90249dba3625b7423927bf26cb94b023", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-4-output-batch.json": { + "sha256": "751d6603b811632a9b26f8817673233f3dfc6e72d349b21b79cc78689d4931ae", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-4-output-scalar.json": { + "sha256": "a131f544ceab72bac003f1cc7fbc60be2f2b9540604e101b7ddc59d15225d28a", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-4-refault-batch.json": { + "sha256": "3019db04069ecaba6faa330230bb8bc6b1a45bc3f4c358a588433d91a5be65e3", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-4-refault-scalar.json": { + "sha256": "14cead8a8da3d8a4b73d812d13ad7e93c17f58a25a8020dd8b78caa4340f2bb4", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-4-software-batch.json": { + "sha256": "1b52012dab5dc4d001dce1c4e813627ad8ce826f5e9ec3148ac97587111948f8", + "logicalCases": 4096 + }, + "A1200-move-write-handlers-4-software-scalar.json": { + "sha256": "0136317d9e79fc06f5db5033797a0e3ed828e1930ba47038c66ec54592b0f4cd", + "logicalCases": 4096 + }, + "A1200-predecrement-read-write-1-control-0-batch.json": { + "sha256": "f3f2eeb891c98445dc7f5f272df59135901e1b1bdac28705bbd81ad5f620a78a", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-1-control-0-scalar.json": { + "sha256": "56d200fb889ca394613f583324343e88165a08142c2a6d4de2f49508a325bff9", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-1-read-0-batch.json": { + "sha256": "5d0a0137b9377f71f65a8314c9ca1d5252191237e426a47dd17c958f1b845f21", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-1-read-0-scalar.json": { + "sha256": "dfbfb4cdae50a50e73d80d683e2e6546df50ace9d27a89ecd5dcc682cf1b231c", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-1-read-write-0-batch.json": { + "sha256": "32c800f61200c87a3c6b0924ee94d7a94af10fb95d86ce6e13b10fe3736ea427", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-1-read-write-0-scalar.json": { + "sha256": "b080376bb8f9d0a4084aaa234a4bc7f65951f7e6ea7a831be9730360584d2e60", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-1-read-write-refault-0-batch.json": { + "sha256": "0504a7bf8186ca385a85ad736fb9f96d163942ef2ba48bd22938b43627135d0b", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-1-read-write-refault-0-scalar.json": { + "sha256": "08861fb4d572b6e13aa21784782c8f81db696023e39b8ecfcf6487be847c65f2", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-2-control-0-batch.json": { + "sha256": "baa86f66363f6fbe65cea471b7c8bef0b33f5316620f5d79f1650c5943fff815", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-2-control-0-scalar.json": { + "sha256": "1f2991e396c2159b9846f8c32217534e7cd7b3ceee95118491ef56cc18e6012a", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-2-read-0-batch.json": { + "sha256": "5f24f70cdeefdb9ae01f771e7aa1e243d2de28aa01fb1362b38caeb5d3d0f43a", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-2-read-0-scalar.json": { + "sha256": "d82bf3f22d262d142c39e9244679043609283ddab33d10cdeff32940c6148db3", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-2-read-1-batch.json": { + "sha256": "b6329358c42020f0c9ad2223e823b39a7ca310fdc4413278342268fc9b6e9763", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-2-read-1-scalar.json": { + "sha256": "8f9db392a9722906f45aea76f7a62e654cb4ff5667cd3e4b9f870962f0eb6a3c", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-2-read-write-0-batch.json": { + "sha256": "82554670f3fc400e36c7e044b2830200bd5e5880b6c5821193fb244eca9b15db", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-2-read-write-0-scalar.json": { + "sha256": "e424f22c9639e75d92afc40f181e65bacea4f2e690710e82af9271e19e46b7e3", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-2-read-write-1-batch.json": { + "sha256": "12d4dc8fd9873219e0d3bf751f108f656420a5a81e941d18f35c01ae1e6ff72e", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-2-read-write-1-scalar.json": { + "sha256": "4aaa6563c5bb854be577cb85198723b56c2c8c54993b040edd931717aac6159c", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-2-read-write-refault-0-batch.json": { + "sha256": "31846203a08b526d10362b921ca0cdbe980462eecc1d5e5bbd8ded91852849bb", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-2-read-write-refault-0-scalar.json": { + "sha256": "e9624e84e23f2bbf5a3cced10d8486cd387c381d9cff1604e217c6ad1fc55e9b", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-2-read-write-refault-1-batch.json": { + "sha256": "22ceeae34be81d9d1f3980ca3061b675db8d4df643f63e001237b76d461bb486", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-2-read-write-refault-1-scalar.json": { + "sha256": "e5b920fdb7b59c4976a7751980d904ee5b2afe4146b41ad03ffb9fa85296c607", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-control-0-batch.json": { + "sha256": "ccebbd7aef4322edb8050ead1e88aca3e598cfe84bebbf4ef063e6456ba454ae", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-control-0-scalar.json": { + "sha256": "db83eba1075f6c88e6fd88ca41c838609665b50dcd05f6ed7e0c55cdea9a3d2f", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-0-batch.json": { + "sha256": "b35d4555da2a5cae745c473b4314d4f6dacfbe63119ccfb5d8e3f49e6ab64893", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-0-scalar.json": { + "sha256": "1933a4d6945b0da7da89d7d1f4d910469a5e8045466195d80db7573b5c7a7111", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-1-batch.json": { + "sha256": "0e176196a931b342d906aa9a66fcc26d5207fa81e76c709d3086529571eabbf9", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-1-scalar.json": { + "sha256": "8d2562acf5e22770ba5fe64ea058382fd81f789786e8e9c2caf781431707030c", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-2-batch.json": { + "sha256": "25f9f27d787bfacbd18a8a6c586580a7abb6b08e0c5797e0b8f56806950ea221", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-2-scalar.json": { + "sha256": "effd251f5696601e4dfa7ff4caff9987378f1e580058950ed73b17135d4a7c55", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-3-batch.json": { + "sha256": "ed7d089f5ba77f4d41492201b2beb9b7da685fab0aa1a1db0cbd7928b2d30909", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-3-scalar.json": { + "sha256": "63f587eaabe025e8b00fd70d8e1892a2a0b128706393f69c30d868204060e292", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-write-0-batch.json": { + "sha256": "a8f87f0ec95222b4b13f20e31a5623919ab0255a824409aeb98f1416899e2f3c", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-write-0-scalar.json": { + "sha256": "1b7281ac30822c157d5c95085b690d7c794a484aca7e28c140d189c2d53a549d", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-write-1-batch.json": { + "sha256": "16d94af2f2b2e30c1f8bbab510e7a669e80d7e296d4c88c6622efe5097041a05", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-write-1-scalar.json": { + "sha256": "177f3cec9df09f9a1db7230a039daf63d102b45eac7ffe3833f782c20dced08d", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-write-2-batch.json": { + "sha256": "7cc5ce6cc7dcee86f007f564a76370bbe7140b6563d8a26b248bf531d1394f66", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-write-2-scalar.json": { + "sha256": "451ad9cc66c91fc6758b998bceefddbb09705e58d29cc143d0db5e12f58c7b22", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-write-3-batch.json": { + "sha256": "f25b1d624d0b49169a271b218544e7becd43d37cbc3de94857f62e0c437537c0", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-write-3-scalar.json": { + "sha256": "c86abe7cff0f75223a042291fbc6fc1278fb3f82d2558fcdf313fceb4b0108ca", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-write-refault-0-batch.json": { + "sha256": "4c47312d394f3c0f9f4112d27224043a42285cddb07b91615788d72f6299e504", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-write-refault-0-scalar.json": { + "sha256": "8a5df6cd8352655209e6d08f77dc7d1aaaf4909b39fe8a2b3cd45030969034db", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-write-refault-1-batch.json": { + "sha256": "5b2f95c8bdaf9c889a9fba032252da737308c6d081ac40b01aef3ce6ebbdf0c0", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-write-refault-1-scalar.json": { + "sha256": "e9ef46e8fbcc8d7495bd0cddcc13400ddb0b6063ed7dd14fe02486449e9fef02", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-write-refault-2-batch.json": { + "sha256": "f07e2beda988b6c0ca7c56d33eac8f60d583aa4844d5b0be44644f34d99e17ec", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-write-refault-2-scalar.json": { + "sha256": "1a7050574ccb6be498fa49712aebc51c42c8bc385c51353805a62668e8bc2013", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-write-refault-3-batch.json": { + "sha256": "4c79b82a81e99f4738ac662839ea41cd70c3dc3e5c25834e71a504f9bb7aae93", + "logicalCases": 8192 + }, + "A1200-predecrement-read-write-4-read-write-refault-3-scalar.json": { + "sha256": "dd3a087fdf19d7d5cb784045393fab4e668dff3672b4bcb42d6592351dfcf4aa", + "logicalCases": 8192 + } + }, + "logicalCases": 3244032, + "referenceProofSha256": "28cd5d79924c841bb10ad47134a955412b6f552195b4c940c95300f729af4de2", + "artifactDevelopmentCommit": "ec3c285ded9c6c6fc256bebb3a58407f00d2ac44" +} diff --git a/scripts/reference/m68020-read-write-recovery-candidate.patch b/scripts/reference/m68020-read-write-recovery-candidate.patch new file mode 100644 index 00000000..114d3f69 --- /dev/null +++ b/scripts/reference/m68020-read-write-recovery-candidate.patch @@ -0,0 +1,749 @@ +--- production/Copper68k/M68kAdvancedTimingInterpreter.Access020.cs ++++ candidate/Copper68k/M68kAdvancedTimingInterpreter.Access020.cs +@@ -52,6 +52,10 @@ + var ssw = ReadRteFrameWord(frame + 10); + if (format == 11 && context == RteValidationContext020) + return RestoreRteValidation020(frame, sr, pc, ssw); ++ if (format == 11 && context == OperandReadContext020) ++ return RestoreOperandRead020(frame, sr, pc, ssw); ++ if (format == 10 && context == FinalMoveWriteContext020) ++ return RestoreFinalMoveWrite020(frame, sr, pc, ssw); + // Data faults and foreign opaque images remain implementation gaps. + // Never silently consume them as ordinary frame-size pops. + if (format != 11 || context != InstructionPrefetchContext020 || (ssw & 0xcf00) != 0) +--- production/Copper68k/M68kAdvancedTimingInterpreter.FinalWrite020.cs ++++ candidate/Copper68k/M68kAdvancedTimingInterpreter.FinalWrite020.cs +@@ -0,0 +1,146 @@ ++/* Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT */ ++// Isolated candidate only; copied into an audit snapshot by the test command. ++namespace Copper68k; ++ ++internal partial class M68kAdvancedTimingInterpreter ++{ ++ private const ushort FinalMoveWriteContext020 = 0xc023; ++ private bool _enteringFinalMoveWrite020, _resumingFinalMoveWrite020; ++ private ushort? _restoredWriteFunctionCode020; ++ ++ private sealed class FinalMoveWriteFault020(uint address, uint value, ushort opcode, ++ uint nextPc, ushort functionCode, M68kAdvancedInstructionPipe pipe) : ++ M68kEmulationException($"020/030 final MOVE write fault at {address:X8}.") ++ { ++ internal readonly uint Address = address, Value = value, NextPc = nextPc; ++ internal readonly ushort Opcode = opcode, FunctionCode = functionCode; ++ internal readonly M68kAdvancedInstructionPipe Pipe = pipe; ++ } ++ ++ private static bool IsSimpleFinalMove020(ushort opcode) => opcode >> 12 is >= 1 and <= 3 && ++ ((opcode >> 6) & 7) == 2 && ((opcode >> 3) & 7) is 2 or 3 or 4; ++ ++ private static M68kOperandSize FinalMoveSize020(ushort opcode) => (opcode >> 12) switch ++ { 1 => M68kOperandSize.Byte, 3 => M68kOperandSize.Word, _ => M68kOperandSize.Long }; ++ ++ private static M68kInstructionTimingKey FinalMoveTiming020(ushort opcode) => ++ (((opcode >> 3) & 7) == 3, opcode >> 12) switch ++ { ++ (false, 1) => M68kInstructionTimingKey.MoveByteAddressIndirectToAddressIndirect, ++ (false, 3) => M68kInstructionTimingKey.MoveWordAddressIndirectToAddressIndirect, ++ (false, _) => M68kInstructionTimingKey.MoveLongAddressIndirectToAddressIndirect, ++ (true, 1) => M68kInstructionTimingKey.MoveBytePostIncrementToAddressIndirect, ++ (true, 3) => M68kInstructionTimingKey.MoveWordPostIncrementToAddressIndirect, ++ _ => M68kInstructionTimingKey.MoveLongPostIncrementToAddressIndirect ++ }; ++ ++ private void CompleteFinalMoveWriteTiming020(ushort opcode) ++ { ++ if (((opcode >> 3) & 7) != 4) { CompleteTiming(FinalMoveTiming020(opcode)); return; } ++ // This shape already uses the general-EA route on mapped execution. ++ // Only its pending destination write remains; preserve that same plan. ++ var cycles = _profile.FixedInstructionNativeCycles ?? 9; ++ var plan = _profile.Model == M68kAcceleratorModel.M68030 ++ ? M68kInstructionPlan.CreateHeadTail(M68kInstructionTimingKey.GeneralMove, "MOVE/MOVEA general EA", cycles, 2, 0) ++ : M68kInstructionPlan.CreateFlat(M68kInstructionTimingKey.GeneralMove, "MOVE/MOVEA general EA", cycles); ++ CompleteTimingPlan(plan); ++ } ++ ++ private void WriteFinalMoveDestination020(uint address, uint value, M68kOperandSize size) ++ { ++ var width = size == M68kOperandSize.Byte ? 1 : size == M68kOperandSize.Word ? 2 : 4; ++ if (_has020PhysicalMap && !_physicalAddressMap!.IsCpuPhysicalAddressMapped(address, width, M68kBusAccessKind.CpuDataWrite)) ++ { ++ if (!IsSimpleFinalMove020(State.LastOpcode) || size != FinalMoveSize020(State.LastOpcode) || ++ State.ProgramCounter != unchecked(State.LastInstructionProgramCounter + 2)) ++ throw new UnsupportedM68kTimingException(State.LastOpcode, State.LastInstructionProgramCounter, _profile); ++ // Normal mapped execution keeps its existing ordering. On this ++ // final-write fault the completed MOVE flags belong in saved SR. ++ // A repeated pending write must retain the returned SR verbatim. ++ if (!_resumingFinalMoveWrite020) SetMoveFlags(value, size); ++ var fc = _restoredWriteFunctionCode020 ?? (ushort)((State.StatusRegister & 0x2000) != 0 ? 5 : 1); ++ throw new FinalMoveWriteFault020(address, value, State.LastOpcode, State.ProgramCounter, fc, _instructionPipe); ++ } ++ WriteSized(address, value, size); ++ } ++ ++ private void RaiseFinalMoveWriteFault020(FinalMoveWriteFault020 fault) ++ { ++ if (_enteringFinalMoveWrite020) { State.Halted = true; _instructionPipe.Reset(); return; } ++ var sr = State.StatusRegister; ++ var size = FinalMoveSize020(fault.Opcode); ++ var words = new ushort[16]; ++ void Long(int offset, uint value) { words[offset / 2] = (ushort)(value >> 16); words[offset / 2 + 1] = (ushort)value; } ++ words[0] = sr; Long(2, fault.NextPc); words[3] = 0xa008; ++ words[4] = FinalMoveWriteContext020; ++ words[5] = (ushort)(0x100 | (size == M68kOperandSize.Byte ? 0x10 : size == M68kOperandSize.Word ? 0x20 : 0) | fault.FunctionCode); ++ Long(16, fault.Address); words[10] = fault.Opcode; ++ words[11] = (ushort)(1 | (fault.Pipe.Count << 8)); Long(24, fault.Value); ++ var pipe = fault.Pipe; ++ for (var n = 0; n < fault.Pipe.Count; n++) ++ { ++ var address = pipe.HeadAddress; ++ _ = pipe.TryConsumeKnownHead(out var word, out _); ++ if (address != unchecked(fault.NextPc + (uint)n * 2)) ++ throw new UnsupportedM68kTimingException(fault.Opcode, unchecked(fault.NextPc - 2), _profile); ++ words[n == 0 ? 6 : n == 1 ? 7 : 14] = word; ++ } ++ if (fault.Pipe.Count != 0 && fault.Pipe.NextAddress != unchecked(fault.NextPc + (uint)fault.Pipe.Count * 2)) ++ throw new UnsupportedM68kTimingException(fault.Opcode, unchecked(fault.NextPc - 2), _profile); ++ State.RecordException(2, fault.NextPc, sr); ++ State.StatusRegister = (ushort)((sr | M68kCpuState.Supervisor) & ~0xc000); ++ _enteringFinalMoveWrite020 = true; ++ try ++ { ++ for (var n = words.Length - 1; n >= 0; n--) ++ { ++ State.SetActiveStackPointer(unchecked(State.A[7] - 2)); ++ if (!_physicalAddressMap!.IsCpuPhysicalAddressMapped(State.A[7], 2, M68kBusAccessKind.CpuDataWrite)) ++ { State.Halted = true; _instructionPipe.Reset(); return; } ++ WriteWord(State.A[7], words[n]); ++ } ++ _instructionPipe.Reset(); ++ State.ProgramCounter = ReadLong(unchecked(State.VectorBaseRegister + 8)); ++ CompleteTiming(M68kInstructionTimingKey.IllegalInstruction); ++ } ++ catch (OperandReadFault020) { State.Halted = true; _instructionPipe.Reset(); } ++ finally { _enteringFinalMoveWrite020 = false; } ++ } ++ ++ private bool RestoreFinalMoveWrite020(uint frame, ushort sr, uint pc, ushort ssw) ++ { ++ var address = ReadRteFrameLong(unchecked(frame + 16)); ++ var opcode = ReadRteFrameWord(unchecked(frame + 20)); ++ var metadata = ReadRteFrameWord(unchecked(frame + 22)); ++ var value = ReadRteFrameLong(unchecked(frame + 24)); ++ var reserved = ReadRteFrameWord(unchecked(frame + 30)); ++ var count = metadata >> 8; ++ var size = FinalMoveSize020(opcode); ++ var expectedSsw = (size == M68kOperandSize.Byte ? 0x10 : size == M68kOperandSize.Word ? 0x20 : 0) | (ssw & 4) | 1; ++ if (!IsSimpleFinalMove020(opcode) || (metadata & 0xff) != 1 || count > M68kAdvancedInstructionPipe.Capacity || reserved != 0 || ++ (ssw & ~0x100) != expectedSsw || (sr & 0xc000) != 0 || (pc & 1) != 0) ++ throw new UnsupportedM68kTimingException(0x4e73, State.LastInstructionProgramCounter, _profile); ++ M68kAdvancedInstructionPipe pipe = default; pipe.Reset(pc); ++ for (var n = 0; n < count; n++) ++ { ++ var offset = n == 0 ? 12u : n == 1 ? 14u : 28u; ++ pipe.Append(unchecked(pc + (uint)n * 2), ReadRteFrameWord(unchecked(frame + offset)), default); ++ } ++ // Read the full private state before committing stack, SR or the write. ++ for (var offset = 12u; offset < 32; offset += 2) _ = ReadRteFrameWord(unchecked(frame + offset)); ++ State.SetActiveStackPointer(unchecked(frame + 32)); State.StatusRegister = sr; State.ProgramCounter = pc; ++ CompleteTiming(M68kInstructionTimingKey.Rte); _instructionPipe = pipe; ++ State.LastOpcode = opcode; State.LastInstructionProgramCounter = unchecked(pc - 2); ++ var previous = _resumingFinalMoveWrite020; var previousFc = _restoredWriteFunctionCode020; ++ _resumingFinalMoveWrite020 = true; _restoredWriteFunctionCode020 = (ushort)(ssw & 7); ++ try ++ { ++ // DF clear means software completed the cycle. No opcode fetch, ++ // source read, EA calculation, source update or flag recomputation. ++ if ((ssw & 0x100) != 0) WriteFinalMoveDestination020(address, value, size); ++ } ++ finally { _resumingFinalMoveWrite020 = previous; _restoredWriteFunctionCode020 = previousFc; } ++ CompleteFinalMoveWriteTiming020(opcode); ++ return true; ++ } ++} +--- production/Copper68k/M68kAdvancedTimingInterpreter.Move.cs ++++ candidate/Copper68k/M68kAdvancedTimingInterpreter.Move.cs +@@ -36,10 +36,18 @@ + value = size == M68kOperandSize.Long ? FetchLong() : FetchWord(); + else + { +- var source = ResolveMoveAddress(sourceMode, sourceRegister, size, opcode); +- value = ReadSized(source, size); +- if (sourceMode == 3) WriteGeneralRegister(true, sourceRegister, +- unchecked(source + M68kIntegerSemantics.AddressIncrement(sourceRegister, size))); ++ if (destinationMode <= 1 && (sourceMode == 6 || sourceMode == 7 && sourceRegister == 3)) ++ { ++ var baseAddress = sourceMode == 6 ? State.A[sourceRegister] : State.ProgramCounter; ++ value = ReadIndexedMoveSource020(baseAddress, FetchWord(), opcode, M68kInstructionTimingKey.GeneralMove); ++ } ++ else ++ { ++ var source = ResolveMoveAddress(sourceMode, sourceRegister, size, opcode); ++ value = ReadSized(source, size); ++ if (sourceMode == 3) WriteGeneralRegister(true, sourceRegister, ++ unchecked(source + M68kIntegerSemantics.AddressIncrement(sourceRegister, size))); ++ } + } + if (destinationMode == 0) WriteDataRegisterSized(destinationRegister, value, size); + else if (destinationMode == 1) +@@ -47,7 +55,9 @@ + else + { + var destination = ResolveMoveAddress(destinationMode, destinationRegister, size, opcode); +- WriteSized(destination, value, size); ++ if (destinationMode == 2 && sourceMode == 4) ++ WriteFinalMoveDestination020(destination, value, size); ++ else WriteSized(destination, value, size); + if (destinationMode == 3) WriteGeneralRegister(true, destinationRegister, + unchecked(destination + M68kIntegerSemantics.AddressIncrement(destinationRegister, size))); + } +--- production/Copper68k/M68kAdvancedTimingInterpreter.Operand020.cs ++++ candidate/Copper68k/M68kAdvancedTimingInterpreter.Operand020.cs +@@ -0,0 +1,283 @@ ++/* Copyright (C) 2026 Ilkka Lehtoranta. SPDX-License-Identifier: MIT */ ++namespace Copper68k; ++ ++internal partial class M68kAdvancedTimingInterpreter ++{ ++ // Isolated development. The selected simple MOVE suffix can resume; ++ // other pending operations remain explicit unsupported continuations. ++ private const ushort OperandReadContext020 = 0xc022; ++ private readonly bool _has020PhysicalMap; ++ private bool _entering020ReadFault; ++ private bool _resumingMoveRead020, _readingTraceVector020; ++ private ushort? _restoredOperandFunctionCode020; ++ private enum PendingReadKind020 : ushort { Unknown, MoveSource, TraceVector, MoveIndexedSource, MoveIndexedPointer } ++ private readonly record struct IndexedReadContext020(ushort Extension, M68kInstructionTimingKey TimingKey, ++ uint PointerOffset, PendingReadKind020 Kind); ++ private IndexedReadContext020 _indexedReadContext020; ++ ++ private uint ReadIndexedMoveSource020(uint baseAddress, ushort extension, ushort opcode, M68kInstructionTimingKey timingKey) ++ { ++ var previous = _indexedReadContext020; ++ _indexedReadContext020 = new(extension, timingKey, 0, PendingReadKind020.MoveIndexedSource); ++ try ++ { ++ var address = CalculateIndexedOperandAddress(baseAddress, extension, opcode); ++ var top = opcode >> 12; ++ return ReadSized(address, top == 1 ? M68kOperandSize.Byte : top == 3 ? M68kOperandSize.Word : M68kOperandSize.Long); ++ } ++ finally { _indexedReadContext020 = previous; } ++ } ++ ++ private uint ReadIndexedPointer020(uint address, uint pointerOffset) ++ { ++ if (_indexedReadContext020.Kind != PendingReadKind020.MoveIndexedSource) return ReadLong(address); ++ var previous = _indexedReadContext020; ++ _indexedReadContext020 = previous with { Kind = PendingReadKind020.MoveIndexedPointer, PointerOffset = pointerOffset }; ++ try { return ReadLong(address); } ++ finally { _indexedReadContext020 = previous; } ++ } ++ ++ private uint ReadPendingIndexed020(uint address, M68kOperandSize size, IndexedReadContext020 context) ++ { ++ var previous = _indexedReadContext020; _indexedReadContext020 = context; ++ try { return ReadPendingMoveSource020(address, size); } ++ finally { _indexedReadContext020 = previous; } ++ } ++ ++ private sealed class OperandReadFault020(uint address, int width, uint instructionPc, ++ uint nextPc, ushort opcode, uint guestStack, M68kAdvancedInstructionPipe pipe, PendingReadKind020 kind, int indexedExtraCycles, ushort functionCode, IndexedReadContext020 indexedContext) : ++ M68kEmulationException($"020/030 operand read fault at {address:X8}.") ++ { ++ internal readonly uint Address = address, InstructionPc = instructionPc, NextPc = nextPc, GuestStack = guestStack; ++ internal readonly int Width = width; ++ internal readonly ushort Opcode = opcode; ++ internal readonly M68kAdvancedInstructionPipe Pipe = pipe; ++ internal readonly PendingReadKind020 Kind = kind; ++ internal readonly int IndexedExtraCycles = indexedExtraCycles; ++ internal readonly ushort FunctionCode = functionCode; ++ internal readonly IndexedReadContext020 IndexedContext = indexedContext; ++ } ++ ++ private void CheckOperandRead020(uint address, int width, M68kBusAccessKind kind) ++ { ++ if (!_has020PhysicalMap || kind != M68kBusAccessKind.CpuDataRead || ++ _physicalAddressMap!.IsCpuPhysicalAddressMapped(address, width, kind)) return; ++ var opcode = State.LastOpcode; var top = opcode >> 12; ++ var mode = (opcode >> 3) & 7; var register = opcode & 7; var destination = (opcode >> 6) & 7; ++ var memoryDestination = top is >= 1 and <= 3 && destination == 2 && mode is 2 or 3 or 4; ++ var moveSource = top is >= 1 and <= 3 && (destination <= 1 || memoryDestination) && !(top == 1 && destination == 1) && ++ (mode is >= 2 and <= 5 || mode == 7 && register <= 2) && width == (top == 1 ? 1 : top == 3 ? 2 : 4) && ++ State.ExceptionSequence == ExecutionBoundaryExceptionSequence && ++ (_resumingMoveRead020 || State.LastInstructionProgramCounter == ExecutionBoundaryProgramCounter); ++ var indexed = _indexedReadContext020.Kind is PendingReadKind020.MoveIndexedSource or PendingReadKind020.MoveIndexedPointer; ++ var phase = _readingTraceVector020 ? PendingReadKind020.TraceVector : indexed ? _indexedReadContext020.Kind : ++ moveSource ? PendingReadKind020.MoveSource : PendingReadKind020.Unknown; ++ var functionCode = _readingTraceVector020 ? (ushort)5 : _restoredOperandFunctionCode020 ?? ++ (ushort)(((State.StatusRegister & 0x2000) != 0 ? 4 : 0) | ++ (mode == 7 && (phase == PendingReadKind020.MoveSource && register == 2 || indexed && register == 3) ? 2 : 1)); ++ throw new OperandReadFault020(address, width, State.LastInstructionProgramCounter, ++ State.ProgramCounter, opcode, State.A[7], _instructionPipe, phase, _indexedOperandExtraCycles, functionCode, indexed ? _indexedReadContext020 : default); ++ } ++ ++ private uint ReadPendingTraceVector020(uint address) ++ { ++ var previous = _readingTraceVector020; _readingTraceVector020 = true; ++ try { return ReadLong(address); } ++ finally { _readingTraceVector020 = previous; } ++ } ++ ++ private uint ReadPendingMoveSource020(uint address, M68kOperandSize size) ++ { ++ var previous = _resumingMoveRead020; _resumingMoveRead020 = true; ++ try { return ReadSized(address, size); } ++ finally { _resumingMoveRead020 = previous; } ++ } ++ ++ private void RaiseOperandReadFault020(OperandReadFault020 fault) ++ { ++ if (_entering020ReadFault) { State.Halted = true; _instructionPipe.Reset(); return; } ++ var sr = State.StatusRegister; ++ State.RecordException(2, fault.InstructionPc, sr); ++ State.StatusRegister = (ushort)((sr | M68kCpuState.Supervisor) & ~0xc000); ++ var words = new ushort[46]; ++ void Long(int offset, uint value) { words[offset / 2] = (ushort)(value >> 16); words[offset / 2 + 1] = (ushort)value; } ++ words[0] = sr; Long(2, fault.InstructionPc); words[3] = 0xb008; ++ words[4] = OperandReadContext020; ++ words[5] = (ushort)(0x140 | (fault.Width == 1 ? 0x10 : fault.Width == 2 ? 0x20 : 0) | ++ fault.FunctionCode); ++ Long(0x10, fault.Address); Long(0x14, fault.NextPc); Long(0x1c, fault.GuestStack); ++ words[0x28 / 2] = fault.Opcode; words[0x2a / 2] = (ushort)fault.Width; ++ words[0x30 / 2] = (ushort)fault.Pipe.Count; Long(0x32, fault.Pipe.NextAddress); ++ words[0x50 / 2] = (ushort)fault.Kind; ++ Long(0x52, (uint)fault.IndexedExtraCycles); ++ if (fault.Kind is PendingReadKind020.MoveIndexedSource or PendingReadKind020.MoveIndexedPointer) ++ { ++ words[0x18 / 2] = fault.IndexedContext.Extension; ++ words[0x1a / 2] = (ushort)fault.IndexedContext.TimingKey; ++ Long(0x20, fault.IndexedContext.PointerOffset); ++ words[0x56 / 2] = 1; // private indexed suffix context version, not a silicon frame claim ++ } ++ var pipe = fault.Pipe; ++ for (var n = 0; n < fault.Pipe.Count; n++) ++ { ++ var address = pipe.HeadAddress; ++ _ = pipe.TryConsumeKnownHead(out var word, out _); ++ Long(0x38 + n * 6, address); words[(0x3c + n * 6) / 2] = word; ++ if (n < 2) words[6 + n] = word; ++ if (n == 1) Long(0x24, address); ++ } ++ _entering020ReadFault = true; ++ try ++ { ++ for (var n = words.Length - 1; n >= 0; n--) ++ { ++ State.SetActiveStackPointer(unchecked(State.A[7] - 2)); ++ if (!_physicalAddressMap!.IsCpuPhysicalAddressMapped(State.A[7], 2, M68kBusAccessKind.CpuDataWrite)) ++ { State.Halted = true; _instructionPipe.Reset(); return; } ++ WriteWord(State.A[7], words[n]); ++ } ++ _instructionPipe.Reset(); ++ State.ProgramCounter = ReadLong(unchecked(State.VectorBaseRegister + 8)); ++ CompleteTiming(M68kInstructionTimingKey.IllegalInstruction); ++ } ++ catch (OperandReadFault020) { State.Halted = true; _instructionPipe.Reset(); } ++ finally { _entering020ReadFault = false; } ++ } ++ ++ private bool RestoreOperandRead020(uint frame, ushort sr, uint pc, ushort ssw) ++ { ++ var address = ReadRteFrameLong(unchecked(frame + 0x10)); ++ var nextPc = ReadRteFrameLong(unchecked(frame + 0x14)); ++ _ = ReadRteFrameLong(unchecked(frame + 0x1c)); // retain provenance read and fault sequencing ++ var opcode = ReadRteFrameWord(unchecked(frame + 0x28)); ++ var width = ReadRteFrameWord(unchecked(frame + 0x2a)); ++ var input = ReadRteFrameLong(unchecked(frame + 0x2c)); ++ var count = ReadRteFrameWord(unchecked(frame + 0x30)); ++ var pipeNext = ReadRteFrameLong(unchecked(frame + 0x32)); ++ var kind = (PendingReadKind020)ReadRteFrameWord(unchecked(frame + 0x50)); ++ var indexedExtraCycles = ReadRteFrameLong(unchecked(frame + 0x52)); ++ var indexedExtension = ReadRteFrameWord(unchecked(frame + 0x18)); ++ var indexedTimingKey = (M68kInstructionTimingKey)ReadRteFrameWord(unchecked(frame + 0x1a)); ++ var pointerOffset = ReadRteFrameLong(unchecked(frame + 0x20)); ++ var indexedVersion = ReadRteFrameWord(unchecked(frame + 0x56)); ++ var top = opcode >> 12; var mode = (opcode >> 3) & 7; var register = opcode & 7; ++ var destinationMode = (opcode >> 6) & 7; ++ var expectedWidth = top == 1 ? 1 : top == 3 ? 2 : 4; ++ var extensions = mode == 5 || mode == 7 && register is 0 or 2 ? 2u : mode == 7 && register == 1 ? 4u : 0u; ++ var indexed = kind is PendingReadKind020.MoveIndexedSource or PendingReadKind020.MoveIndexedPointer; ++ var programReference = mode == 7 && (kind == PendingReadKind020.MoveSource && register == 2 || indexed && register == 3); ++ if (indexed) ++ { ++ var full = (indexedExtension & 0x100) != 0; ++ var baseSize = (indexedExtension >> 4) & 3; ++ var indirect = indexedExtension & 7; ++ var suppressIndex = (indexedExtension & 0x40) != 0; ++ var indexedLength = 2u + (full ? (baseSize == 2 ? 2u : baseSize == 3 ? 4u : 0) + ++ ((indirect & 3) == 2 ? 2u : (indirect & 3) == 3 ? 4u : 0) : 0); ++ var extra = full ? 4 + (baseSize == 2 ? 2 : baseSize == 3 ? 6 : 0) + ++ (indirect == 0 ? 0 : 5 + ((indirect & 3) >= 2 ? 2 : 0)) : 0; ++ var briefKey = destinationMode == 0 ? expectedWidth switch ++ { ++ 1 => M68kInstructionTimingKey.MoveByteBriefIndexedToData, ++ 2 => M68kInstructionTimingKey.MoveWordBriefIndexedToData, ++ _ => M68kInstructionTimingKey.MoveLongBriefIndexedToData ++ } : expectedWidth == 4 ? M68kInstructionTimingKey.MoveLongBriefIndexedToAddress : ++ mode == 7 ? M68kInstructionTimingKey.MoveWordPcBriefIndexedToAddress : M68kInstructionTimingKey.MoveWordBriefIndexedToAddress; ++ if (top is < 1 or > 3 || destinationMode > 1 || top == 1 && destinationMode == 1 || ++ !(mode == 6 || mode == 7 && register == 3) || indexedVersion != 1 || ++ width != (kind == PendingReadKind020.MoveIndexedPointer ? 4 : expectedWidth) || ++ nextPc != unchecked(pc + 2 + indexedLength) || indexedExtraCycles != extra || ++ indexedTimingKey != briefKey && indexedTimingKey != M68kInstructionTimingKey.GeneralMove || ++ full && (baseSize == 0 || (indexedExtension & 8) != 0 || indirect == 4 || suppressIndex && indirect >= 4) || ++ kind == PendingReadKind020.MoveIndexedPointer && (!full || indirect == 0) || ++ kind == PendingReadKind020.MoveIndexedSource && pointerOffset != 0) ++ throw new UnsupportedM68kTimingException(0x4e73, State.LastInstructionProgramCounter, _profile); ++ } ++ // The saved data-cycle address space belongs to the faulted access. ++ // A handler may edit the returned SR without changing that cycle's FC. ++ // Keep size/direction/reference-kind checks independent of returned S. ++ var expectedSsw = 0x40 | (width == 1 ? 0x10 : width == 2 ? 0x20 : 0) | ++ (kind == PendingReadKind020.TraceVector ? 5 : (ssw & 4) | (programReference ? 2 : 1)); ++ if (kind is not (PendingReadKind020.MoveSource or PendingReadKind020.TraceVector or PendingReadKind020.MoveIndexedSource or PendingReadKind020.MoveIndexedPointer) || ++ count > M68kAdvancedInstructionPipe.Capacity || indexedExtraCycles > int.MaxValue || (ssw & ~0x100) != expectedSsw || ++ kind == PendingReadKind020.TraceVector && width != 4 || ++ kind == PendingReadKind020.MoveSource && (top is < 1 or > 3 || ++ destinationMode > 1 && !(top is >= 1 and <= 3 && destinationMode == 2 && mode is 2 or 3 or 4) || top == 1 && destinationMode == 1 || ++ !(mode is >= 2 and <= 5 || mode == 7 && register <= 2) || width != expectedWidth || ++ nextPc != unchecked(pc + 2 + extensions) || indexedExtraCycles != 0)) ++ throw new UnsupportedM68kTimingException(0x4e73, State.LastInstructionProgramCounter, _profile); ++ // RTE consumes the selected active frame. Its fault-time A7 provenance ++ // does not require the frame to remain at its original stack address. ++ // The completed effective address is already serialized separately. ++ M68kAdvancedInstructionPipe pipe = default; ++ pipe.Reset(pipeNext); ++ for (var n = 0; n < count; n++) ++ { ++ var wordAddress = ReadRteFrameLong(unchecked(frame + 0x38u + (uint)n * 6)); ++ var word = ReadRteFrameWord(unchecked(frame + 0x3cu + (uint)n * 6)); ++ if (wordAddress != unchecked(nextPc + (uint)n * 2)) ++ throw new UnsupportedM68kTimingException(0x4e73, State.LastInstructionProgramCounter, _profile); ++ pipe.Append(wordAddress, word, default); ++ } ++ if (count != 0 && pipe.NextAddress != pipeNext) ++ throw new UnsupportedM68kTimingException(0x4e73, State.LastInstructionProgramCounter, _profile); ++ for (var offset = 0x10u; offset < 92; offset += 2) _ = ReadRteFrameWord(unchecked(frame + offset)); ++ State.SetActiveStackPointer(unchecked(frame + 92)); State.StatusRegister = sr; State.ProgramCounter = nextPc; ++ CompleteTiming(M68kInstructionTimingKey.Rte); ++ _instructionPipe = pipe; ++ State.LastInstructionProgramCounter = pc; State.LastOpcode = opcode; _indexedOperandExtraCycles = (int)indexedExtraCycles; ++ if (kind == PendingReadKind020.TraceVector) ++ { ++ // Trace's frame and the traced instruction's effects already exist. ++ // Complete only the saved vector read, with its original address. ++ State.ProgramCounter = (ssw & 0x100) == 0 ? input : ReadPendingTraceVector020(address); ++ CompleteTiming(M68kInstructionTimingKey.IllegalInstruction); ++ return true; ++ } ++ var size = expectedWidth == 1 ? M68kOperandSize.Byte : expectedWidth == 2 ? M68kOperandSize.Word : M68kOperandSize.Long; ++ // No BeginInstruction, FetchWord, extension consumption or EA calculation: ++ // only the suspended read and its not-yet-completed register/flag suffix. ++ uint value; ++ var previousFunctionCode = _restoredOperandFunctionCode020; ++ _restoredOperandFunctionCode020 = (ushort)(ssw & 7); ++ try ++ { ++ if (indexed) ++ { ++ var context = new IndexedReadContext020(indexedExtension, indexedTimingKey, pointerOffset, kind); ++ if (kind == PendingReadKind020.MoveIndexedPointer) ++ { ++ // Only the suspended pointer read remains. The index/base/outer ++ // components and extensions have already been evaluated. ++ var pointer = (ssw & 0x100) == 0 ? input : ReadPendingIndexed020(address, M68kOperandSize.Long, context); ++ var sourceAddress = unchecked(pointer + pointerOffset); ++ context = context with { Kind = PendingReadKind020.MoveIndexedSource, PointerOffset = 0 }; ++ value = ReadPendingIndexed020(sourceAddress, size, context); ++ } ++ else value = (ssw & 0x100) == 0 ? input : ReadPendingIndexed020(address, size, context); ++ } ++ else value = (ssw & 0x100) == 0 ? input : ReadPendingMoveSource020(address, size); ++ } ++ finally { _restoredOperandFunctionCode020 = previousFunctionCode; } ++ if (mode == 3) WriteGeneralRegister(true, register, unchecked(address + M68kIntegerSemantics.AddressIncrement(register, size))); ++ var destination = (opcode >> 9) & 7; ++ if (destinationMode == 0) { WriteDataRegisterSized(destination, value, size); SetMoveFlags(value, size); } ++ else if (destinationMode == 2) { WriteFinalMoveDestination020(State.A[destination], value, size); SetMoveFlags(value, size); } ++ else WriteGeneralRegister(true, destination, expectedWidth == 2 ? unchecked((uint)(int)(short)value) : value); ++ if (destinationMode == 2) ++ CompleteFinalMoveWriteTiming020(opcode); ++ else if (indexed && indexedTimingKey != M68kInstructionTimingKey.GeneralMove) ++ CompleteIndexedMoveTiming(indexedTimingKey, indexedExtension); ++ else ++ { ++ var cycles = _profile.FixedInstructionNativeCycles ?? 4 + MoveEaPolicyCycles(mode, register) + (indexed ? (int)indexedExtraCycles : 0); ++ var plan = _profile.Model == M68kAcceleratorModel.M68030 ++ ? M68kInstructionPlan.CreateHeadTail(M68kInstructionTimingKey.GeneralMove, "MOVE resumed read suffix", cycles, 2, 0) ++ : M68kInstructionPlan.CreateFlat(M68kInstructionTimingKey.GeneralMove, "MOVE resumed read suffix", cycles); ++ CompleteTimingPlan(plan); ++ } ++ if ((sr & 0x8000) != 0) RaiseFormat0Exception(9, State.ProgramCounter, M68kInstructionTimingKey.IllegalInstruction); ++ return true; ++ } ++} ++ +--- production/Copper68k/M68kAdvancedTimingInterpreter.Rte020.cs ++++ candidate/Copper68k/M68kAdvancedTimingInterpreter.Rte020.cs +@@ -34,7 +34,8 @@ + { + if (!IsRte020Mapped(address, width, M68kBusAccessKind.CpuDataRead)) + throw new RteReadFault020(null, address, width); +- return width == 2 ? ReadWord(address) : ReadLong(address); ++ return width == 2 ? _timedBus.ReadWord(address, M68kBusAccessKind.CpuDataRead) : ++ _timedBus.ReadLong(address, M68kBusAccessKind.CpuDataRead); + } + + private uint ReadRteValidation020(RteValidation020 context, ref uint? supplied) +@@ -45,7 +46,8 @@ + { + if (!IsRte020Mapped(context.Address, context.Width, M68kBusAccessKind.CpuDataRead)) + throw new RteReadFault020(context, context.Address, context.Width); +- value = context.Width == 2 ? ReadWord(context.Address) : ReadLong(context.Address); ++ value = context.Width == 2 ? _timedBus.ReadWord(context.Address, M68kBusAccessKind.CpuDataRead) : ++ _timedBus.ReadLong(context.Address, M68kBusAccessKind.CpuDataRead); + } + return context.Width == 2 ? value & 0xffff : value; + } +--- production/Copper68k/M68kAdvancedTimingInterpreter.cs ++++ candidate/Copper68k/M68kAdvancedTimingInterpreter.cs +@@ -4126,6 +4126,8 @@ + + internal uint NextAddress { get; private set; } + ++ internal uint HeadAddress => _address0; ++ + internal bool IsEmpty => Count == 0; + + internal void Reset(uint nextAddress = 0) +@@ -4382,6 +4384,8 @@ + ? null : bus as IM68kCodeReader; + _jitBus = bus as IM68kJitBus; + _physicalAddressMap = bus as IM68kPhysicalAddressMap; ++ _has020PhysicalMap = Has020AccessFrames && _physicalAddressMap is not null && ++ _physicalAddressMap is not M68EC020AddressMaskedBus { HasPhysicalAddressMap: false }; + _hasModelSpecificInstructions = hasModelSpecificInstructions; + _enableAdvancedFastPath = + enableAdvancedFastPath && +@@ -4514,6 +4518,14 @@ + try + { + ExecuteInstructionWithTrace(); ++ } ++ catch (OperandReadFault020 fault) ++ { ++ RaiseOperandReadFault020(fault); ++ } ++ catch (FinalMoveWriteFault020 fault) ++ { ++ RaiseFinalMoveWriteFault020(fault); + } + catch (M68040MmuFaultException ex) + { +@@ -4724,6 +4736,16 @@ + exitBlock = true; + } + else ExecuteHotInstruction(hotInstruction.Kind, opcode); ++ } ++ catch (OperandReadFault020 fault) ++ { ++ RaiseOperandReadFault020(fault); ++ exitBlock = true; ++ } ++ catch (FinalMoveWriteFault020 fault) ++ { ++ RaiseFinalMoveWriteFault020(fault); ++ exitBlock = true; + } + catch (M68040MmuFaultException ex) + { +@@ -4825,6 +4847,16 @@ + throw new InvalidOperationException("The MC68040 FPU hot instruction was not handled."); + } + else ExecuteHotInstruction(hotInstruction.Kind, opcode); ++ } ++ catch (OperandReadFault020 fault) ++ { ++ RaiseOperandReadFault020(fault); ++ exitBlock = true; ++ } ++ catch (FinalMoveWriteFault020 fault) ++ { ++ RaiseFinalMoveWriteFault020(fault); ++ exitBlock = true; + } + catch (M68040MmuFaultException ex) + { +@@ -5512,6 +5544,7 @@ + var destination = (opcode >> 9) & 7; + var source = opcode & 7; + var address = State.A[source]; ++ CheckOperandRead020(address, 4, M68kBusAccessKind.CpuDataRead); + var value = _timedBus.ReadDataLongHot(address); + State.D[destination] = value; + WriteGeneralRegister(true, source, address + 4); +@@ -11160,7 +11193,7 @@ + var destination = (opcode >> 9) & 7; + var baseRegister = opcode & 7; + var extension = FetchWord(); +- var value = ReadLong(CalculateIndexedOperandAddress(State.A[baseRegister], extension, opcode)); ++ var value = ReadIndexedMoveSource020(State.A[baseRegister], extension, opcode, M68kInstructionTimingKey.MoveLongBriefIndexedToData); + State.D[destination] = value; + SetMoveFlags(value, M68kOperandSize.Long); + CompleteIndexedMoveTiming(M68kInstructionTimingKey.MoveLongBriefIndexedToData, extension); +@@ -11173,7 +11206,7 @@ + var destination = (opcode >> 9) & 7; + var baseRegister = opcode & 7; + var extension = FetchWord(); +- var value = ReadLong(CalculateIndexedOperandAddress(State.A[baseRegister], extension, opcode)); ++ var value = ReadIndexedMoveSource020(State.A[baseRegister], extension, opcode, M68kInstructionTimingKey.MoveLongBriefIndexedToAddress); + WriteGeneralRegister(true, destination, value); + CompleteIndexedMoveTiming(M68kInstructionTimingKey.MoveLongBriefIndexedToAddress, extension); + } +@@ -11185,7 +11218,7 @@ + var destination = (opcode >> 9) & 7; + var extensionAddress = State.ProgramCounter; + var extension = FetchWord(); +- var value = ReadLong(CalculateIndexedOperandAddress(extensionAddress, extension, opcode)); ++ var value = ReadIndexedMoveSource020(extensionAddress, extension, opcode, M68kInstructionTimingKey.MoveLongBriefIndexedToAddress); + WriteGeneralRegister(true, destination, value); + CompleteIndexedMoveTiming(M68kInstructionTimingKey.MoveLongBriefIndexedToAddress, extension); + } +@@ -11255,7 +11288,7 @@ + var destination = (opcode >> 9) & 7; + var source = opcode & 7; + var value = ReadLong(State.A[source]); +- WriteLong(State.A[destination], value); ++ WriteFinalMoveDestination020(State.A[destination], value, M68kOperandSize.Long); + SetMoveFlags(value, M68kOperandSize.Long); + CompleteTiming(M68kInstructionTimingKey.MoveLongAddressIndirectToAddressIndirect); + } +@@ -11448,7 +11481,7 @@ + var destination = (opcode >> 9) & 7; + var source = opcode & 7; + var value = ReadByte(State.A[source]); +- WriteByte(State.A[destination], value); ++ WriteFinalMoveDestination020(State.A[destination], value, M68kOperandSize.Byte); + SetMoveFlags(value, M68kOperandSize.Byte); + CompleteTiming(M68kInstructionTimingKey.MoveByteAddressIndirectToAddressIndirect); + } +@@ -11486,7 +11519,7 @@ + var destination = (opcode >> 9) & 7; + var baseRegister = opcode & 7; + var extension = FetchWord(); +- var value = ReadByte(CalculateIndexedOperandAddress(State.A[baseRegister], extension, opcode)); ++ var value = (byte)ReadIndexedMoveSource020(State.A[baseRegister], extension, opcode, M68kInstructionTimingKey.MoveByteBriefIndexedToData); + WriteDataRegisterByte(destination, value); + SetMoveFlags(value, M68kOperandSize.Byte); + CompleteIndexedMoveTiming(M68kInstructionTimingKey.MoveByteBriefIndexedToData, extension); +@@ -11499,7 +11532,7 @@ + var destination = (opcode >> 9) & 7; + var extensionAddress = State.ProgramCounter; + var extension = FetchWord(); +- var value = ReadByte(CalculateIndexedOperandAddress(extensionAddress, extension, opcode)); ++ var value = (byte)ReadIndexedMoveSource020(extensionAddress, extension, opcode, M68kInstructionTimingKey.MoveByteBriefIndexedToData); + WriteDataRegisterByte(destination, value); + SetMoveFlags(value, M68kOperandSize.Byte); + CompleteIndexedMoveTiming(M68kInstructionTimingKey.MoveByteBriefIndexedToData, extension); +@@ -11546,7 +11579,7 @@ + _ = FetchWord(); + var extensionAddress = State.ProgramCounter; + var extension = FetchWord(); +- var value = unchecked((uint)(int)(short)ReadWord(CalculateIndexedOperandAddress(extensionAddress, extension, opcode))); ++ var value = unchecked((uint)(int)(short)ReadIndexedMoveSource020(extensionAddress, extension, opcode, M68kInstructionTimingKey.MoveWordPcBriefIndexedToAddress)); + WriteGeneralRegister(true, (opcode >> 9) & 7, value); + CompleteIndexedMoveTiming(M68kInstructionTimingKey.MoveWordPcBriefIndexedToAddress, extension); + } +@@ -11616,7 +11649,7 @@ + BeginInstruction(opcode); + _ = FetchWord(); + var value = ReadWord(State.A[opcode & 7]); +- WriteWord(State.A[(opcode >> 9) & 7], value); ++ WriteFinalMoveDestination020(State.A[(opcode >> 9) & 7], value, M68kOperandSize.Word); + SetMoveFlags(value, M68kOperandSize.Word); + CompleteTiming(M68kInstructionTimingKey.MoveWordAddressIndirectToAddressIndirect); + } +@@ -11692,7 +11725,7 @@ + var value = ReadSized(address, size); + WriteGeneralRegister(true, sourceRegister, unchecked(address + M68kIntegerSemantics.AddressIncrement(sourceRegister, size))); + // Resolve the destination after source EA side effects, including An aliasing. +- WriteSized(State.A[(opcode >> 9) & 7], value, size); ++ WriteFinalMoveDestination020(State.A[(opcode >> 9) & 7], value, size); + SetMoveFlags(value, size); + CompleteTiming(size switch + { +@@ -11723,7 +11756,7 @@ + var destination = (opcode >> 9) & 7; + var source = opcode & 7; + var extension = FetchWord(); +- var value = ReadWord(CalculateIndexedOperandAddress(State.A[source], extension, opcode)); ++ var value = (ushort)ReadIndexedMoveSource020(State.A[source], extension, opcode, M68kInstructionTimingKey.MoveWordBriefIndexedToData); + WriteDataRegisterWord(destination, value); + SetMoveFlags(value, M68kOperandSize.Word); + CompleteIndexedMoveTiming(M68kInstructionTimingKey.MoveWordBriefIndexedToData, extension); +@@ -11750,7 +11783,7 @@ + var destination = (opcode >> 9) & 7; + var extensionAddress = State.ProgramCounter; + var extension = FetchWord(); +- var value = ReadLong(CalculateIndexedOperandAddress(extensionAddress, extension, opcode)); ++ var value = ReadIndexedMoveSource020(extensionAddress, extension, opcode, M68kInstructionTimingKey.MoveLongBriefIndexedToData); + State.D[destination] = value; + SetMoveFlags(value, M68kOperandSize.Long); + CompleteIndexedMoveTiming(M68kInstructionTimingKey.MoveLongBriefIndexedToData, extension); +@@ -11763,7 +11796,7 @@ + var destination = (opcode >> 9) & 7; + var extensionAddress = State.ProgramCounter; + var extension = FetchWord(); +- var value = ReadWord(CalculateIndexedOperandAddress(extensionAddress, extension, opcode)); ++ var value = (ushort)ReadIndexedMoveSource020(extensionAddress, extension, opcode, M68kInstructionTimingKey.MoveWordBriefIndexedToData); + WriteDataRegisterWord(destination, value); + SetMoveFlags(value, M68kOperandSize.Word); + CompleteIndexedMoveTiming(M68kInstructionTimingKey.MoveWordBriefIndexedToData, extension); +@@ -18597,7 +18630,8 @@ + PushWord((ushort)((vector is 5 or 6 or 7 or 9 ? 0x2000 : Format0ExceptionFrame) | ((vector * 4) & 0x0FFF))); + PushLong(stackedProgramCounter); + PushWord(savedStatusRegister); +- State.ProgramCounter = ReadLong(State.VectorBaseRegister + ((uint)vector * 4)); ++ var vectorAddress = unchecked(State.VectorBaseRegister + ((uint)vector * 4)); ++ State.ProgramCounter = Has020AccessFrames && vector == 9 ? ReadPendingTraceVector020(vectorAddress) : ReadLong(vectorAddress); + CompleteTiming(timingKey); + } + +@@ -18875,16 +18909,19 @@ + + protected byte ReadByte(uint address, M68kBusAccessKind accessKind = M68kBusAccessKind.CpuDataRead) + { ++ CheckOperandRead020(address, 1, accessKind); + return _timedBus.ReadByte(address, accessKind); + } + + protected ushort ReadWord(uint address, M68kBusAccessKind accessKind = M68kBusAccessKind.CpuDataRead) + { ++ CheckOperandRead020(address, 2, accessKind); + return _timedBus.ReadWord(address, accessKind); + } + + protected uint ReadLong(uint address) + { ++ CheckOperandRead020(address, 4, M68kBusAccessKind.CpuDataRead); + return _timedBus.ReadLong(address, M68kBusAccessKind.CpuDataRead); + } + +@@ -19269,8 +19306,7 @@ + BeginInstruction(opcode); + _ = FetchWord(); + var extension = FetchWord(); +- var address = CalculateIndexedOperandAddress(State.A[opcode & 7], extension, opcode); +- var value = unchecked((uint)(int)(short)ReadWord(address)); ++ var value = unchecked((uint)(int)(short)ReadIndexedMoveSource020(State.A[opcode & 7], extension, opcode, M68kInstructionTimingKey.MoveWordBriefIndexedToAddress)); + WriteGeneralRegister(true, (opcode >> 9) & 7, value); + CompleteIndexedMoveTiming(M68kInstructionTimingKey.MoveWordBriefIndexedToAddress, extension); + } +@@ -19298,7 +19334,7 @@ + var address = unchecked(((extension & 0x80) != 0 ? 0u : baseAddress) + baseDisplacement); + if (indirect == 0) return unchecked(address + index); + // Both levels use ordinary timed data reads on the host's single bus clock. +- var pointer = ReadLong(unchecked(address + (indirect < 4 ? index : 0u))); ++ var pointer = ReadIndexedPointer020(unchecked(address + (indirect < 4 ? index : 0u)), unchecked(outerDisplacement + (indirect >= 5 ? index : 0u))); + return unchecked(pointer + outerDisplacement + (indirect >= 5 ? index : 0u)); + } + diff --git a/scripts/test-copper68k-private-predecrement.py b/scripts/test-copper68k-private-predecrement.py new file mode 100644 index 00000000..7eab23f0 --- /dev/null +++ b/scripts/test-copper68k-private-predecrement.py @@ -0,0 +1,182 @@ +"""Reproduce the private candidate in an isolated snapshot; never import it.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import subprocess +import xml.etree.ElementTree as ET + +REPO = Path(__file__).resolve().parent.parent +MANIFEST = REPO / "scripts/reference/m68020-private-predecrement-manifest.json" +PATCH = REPO / "scripts/reference/m68020-read-write-recovery-candidate.patch" +NS = {"t": "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"} +CLASSES = ["SyntheticM68020PredecrementReadTests", "SyntheticM68020MemoryReadWriteTests", + "SyntheticM68020PredecrementWriteTests", "SyntheticM68020MoveWriteHandlerTests"] +SETTINGS = {"COPPER68K_RUN_020_PREDECREMENT_READ": "1", "COPPER68K_RUN_020_MEMORY_READ_WRITE": "1", + "COPPER68K_RUN_020_PREDECREMENT_WRITE": "1", "COPPER68K_RUN_020_MOVE_WRITE_HANDLERS": "1"} + + +def check(value, message): + if not value: + raise ValueError(message) + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def normalized(path): + return hashlib.sha256(path.read_text(encoding="utf-8-sig").encode()).hexdigest() + + +def load(path): + return json.loads(path.read_text(encoding="utf-8-sig")) + + +def save(path, value): + path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8", newline="\n") + + +def manifest(): + check(sha(MANIFEST) == "d7ade9a6020cd711f05a8e7b0ef6fd427b223a48c50239ac6949e3d83f85b06c", "Changed pinned reference manifest") + value = load(MANIFEST) + check(value["schema"] == 1 and len(value["reports"]) == 520 and len(value["expectedTests"]) == 14, + "Missing or incomplete pinned audit inventory") + check(value["logicalCases"] == 3244032 and sum(r["logicalCases"] for r in value["reports"].values()) == 3244032, + "Invalid logical case inventory") + check(len(value["baseSourceNormalizedSha256"]) == 233 and len(value["candidateCpuNormalizedSha256"]) == 39, + "Invalid source inventory") + check(set(value["expectedTests"].values()) == {"Passed"} and sha(PATCH) == value["patchSha256"], + "Changed candidate patch or expected outcomes") + for name in value["baseSourceNormalizedSha256"]: + check(not Path(name).is_absolute() and ".." not in Path(name).parts and + name.startswith(("Copper68k/", "Copper68k.Tests/")) and Path(name).suffix in [".cs", ".csproj"], + "Invalid source path: " + name) + return value + + +def command(root): + return ["dotnet", "test", str(root / "source/Copper68k.Tests/Copper68k.Tests.csproj"), "-c", "Release", + "--artifacts-path", str(root / "build"), "--filter", + "|".join("FullyQualifiedName~" + name for name in CLASSES), "--logger", + "trx;LogFileName=audit.trx", "--results-directory", str(root)] + + +def expected_sources(value): + result = {n: h for n, h in value["baseSourceNormalizedSha256"].items() if n.startswith("Copper68k.Tests/")} + result.update(value["candidateCpuNormalizedSha256"]) + check(len(result) == 235, "Incomplete isolated candidate inventory") + return result + + +def execute(root, value): + check(not root.exists(), "Choose a fresh output directory; existing evidence is never overwritten") + names = subprocess.check_output(["git", "ls-files", "Copper68k", "Copper68k.Tests"], cwd=REPO, text=True).splitlines() + names = {n for n in names if Path(n).suffix in [".cs", ".csproj"]} + check(names == set(value["baseSourceNormalizedSha256"]), "Source inventory changed; use the audited checkpoint or requalify") + check(all(normalized(REPO / n) == h for n, h in value["baseSourceNormalizedSha256"].items()), + "Baseline sources changed; use the audited checkpoint or requalify") + before = {n: sha(REPO / n) for n in sorted(names)} + assets = {"Copper68k/" + n: sha(REPO / "Copper68k" / n) for n in ["README.md", "copper68k-icon.png"]} + for name in [*before, *assets]: + target = root / "source" / name + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes((REPO / name).read_bytes()) + # Retain the qualified source's intentional whitespace; never repair it. + for options in [["--check"], []]: + subprocess.run(["git", "apply", *options, "--whitespace=nowarn", "-p1", str(PATCH)], + cwd=root / "source", check=True) + required = expected_sources(value) + check(all(normalized(root / "source" / n) == h for n, h in required.items()), "Candidate replay differs from qualified source") + sources = {n: sha(root / "source" / n) for n in sorted(required)} + settings = dict(SETTINGS, COPPER68K_SYNTHETIC_REPORT_DIR=str(root)) + inputs = dict(schema=1, sources=sources, assets=assets, baselineSources=before, settings=settings, + command=command(root), manifestSha256=sha(MANIFEST), patchSha256=sha(PATCH), producerSha256=sha(Path(__file__))) + save(root / "inputs.json", inputs) + environment = {k: v for k, v in os.environ.items() if not k.startswith("COPPER68K_")} + environment.update(settings) + print("Executing isolated current-checkout candidate: 14 named tests / 3,244,032 required cases", flush=True) + with (root / "execution.log").open("w", encoding="utf-8") as log: + done = subprocess.run(command(root), env=environment, stdout=log, stderr=subprocess.STDOUT) + assemblies = {str(f.relative_to(root)).replace("\\", "/"): sha(f) for f in (root / "build/bin").rglob("Copper68k*.dll")} + result = dict(exit=done.returncode, inputsSha256=sha(root / "inputs.json"), + sourcesUnchanged=all(sha(root / "source" / n) == h for n, h in sources.items()), + productionUnchanged=all(sha(REPO / n) == h for n, h in before.items()), + outputs={n: sha(root / n) for n in ["execution.log", "audit.trx"] if (root / n).exists()}, assemblies=assemblies) + save(root / "execution.json", result) + + +def validate(root, value): + inputs, execution = load(root / "inputs.json"), load(root / "execution.json") + check(inputs["schema"] == 1 and inputs["manifestSha256"] == sha(MANIFEST) and + inputs["patchSha256"] == sha(PATCH) and inputs["producerSha256"] == sha(Path(__file__)), "Changed audit inputs or command") + check(execution["exit"] == 0 and execution["sourcesUnchanged"] and execution["productionUnchanged"], "Failed execution or changed sources") + check(execution["inputsSha256"] == sha(root / "inputs.json") and inputs["command"] == command(root), "Execution/command identity differs") + check(inputs["settings"] == dict(SETTINGS, COPPER68K_SYNTHETIC_REPORT_DIR=str(root)), "Changed audit settings") + required = expected_sources(value) + check(set(inputs["sources"]) == set(required) and all(normalized(root / "source" / n) == h for n, h in required.items()), "Changed candidate source inventory") + check(set(inputs["baselineSources"]) == set(value["baseSourceNormalizedSha256"]), "Changed baseline source inventory") + check(set(inputs["assets"]) == {"Copper68k/README.md", "Copper68k/copper68k-icon.png"} and + all(sha(root / "source" / n) == h for n, h in inputs["assets"].items()), "Missing or changed build assets") + actual = {f.relative_to(root / "source").as_posix() for pattern in ["*.cs", "*.csproj"] for f in (root / "source").rglob(pattern)} + check(actual == set(required) and all(sha(root / "source" / n) == h for n, h in inputs["sources"].items()), "Missing, extra or changed source files") + check(set(execution["outputs"]) == {"execution.log", "audit.trx"} and + all(sha(root / n) == h for n, h in execution["outputs"].items()), "Changed execution output") + assemblies = execution["assemblies"] + paths = {"build/bin/Copper68k/release/Copper68k.dll", "build/bin/Copper68k.Tests/release/Copper68k.dll", + "build/bin/Copper68k.Tests/release/Copper68k.Tests.dll"} + check(set(assemblies) == paths and all(sha(root / n) == h for n, h in assemblies.items()), "Missing or changed execution DLLs") + check(assemblies["build/bin/Copper68k/release/Copper68k.dll"] == assemblies["build/bin/Copper68k.Tests/release/Copper68k.dll"], "CPU copies differ") + tree = ET.parse(root / "audit.trx") + rows = tree.findall(".//t:UnitTestResult", NS) + check(len(rows) == 14 and {r.get("testName"): r.get("outcome") for r in rows} == value["expectedTests"], "Missing, empty, extra or failed selection") + counters = tree.find(".//t:Counters", NS) + check(all(int(counters.get(k, -1)) == v for k, v in dict(total=14, passed=14, executed=14, failed=0, notExecuted=0).items()), "Invalid execution counters") + check(tree.find(".//t:ResultSummary", NS).get("outcome") == "Completed", "Incomplete test run") + definitions = {d.get("id"): d for d in tree.findall(".//t:UnitTest", NS)} + check(len(definitions) == 14 and set(definitions) == {r.get("testId") for r in rows}, "Missing or duplicated test definitions") + for row in rows: + definition = definitions[row.get("testId")] + method = definition.find("t:TestMethod", NS) + cls, name = row.get("testName").split("(")[0].rsplit(".", 1) + check(definition.get("name") == row.get("testName"), "Wrong loaded test definition") + check(method.get("className").split(",")[0] == cls and method.get("name") == name, "Wrong loaded method") + check(Path(definition.get("storage")).resolve() == Path(method.get("codeBase")).resolve() == + (root / "build/bin/Copper68k.Tests/release/Copper68k.Tests.dll").resolve(), "Wrong loaded test assembly") + reports = {f.name for f in root.glob("*.json") if re.match(r"^(68000|68010|68EC020|68020|68030|68040|68060|A1200)-", f.name)} + check(reports == set(value["reports"]), "Missing or extra required reports") + for name, record in value["reports"].items(): + check(sha(root / name) == record["sha256"], "Changed architectural cases or results: " + name) + report = load(root / name) + count = record["logicalCases"] + check(count > 0 and report["logicalCases"] == count and report["counts"] == + dict(passing=count, mismatching=0, unsupported=0, untested=0) and not report["failures"], "Nonpassing or empty report: " + name) + check(sum(v.get("passing", 0) for v in report["combinations"].values()) == count and + all(set(v) == {"passing"} and v["passing"] > 0 for v in report["combinations"].values()), "Invalid combination coverage: " + name) + return dict(schema=1, manifestSha256=sha(MANIFEST), inputsSha256=sha(root / "inputs.json"), executionSha256=sha(root / "execution.json"), + sourceInputs=235, cpuInputs=39, namedTests=14, reports=520, logicalCases=3244032, + fullSuiteQualification=False, hardwareQualification=False, productionImport=False, publication=False) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, default=REPO / "artifacts/private-predecrement") + parser.add_argument("--validate-only", action="store_true") + args = parser.parse_args() + root = args.output.resolve() + value = manifest() + if not args.validate_only: + execute(root, value) + result = validate(root, value) + if args.validate_only: + check(load(root / "proof.json") == result, "Stored proof differs from strict replay") + else: + check(not (root / "proof.json").exists(), "Evidence already exists") + save(root / "proof.json", result) + print("Qualified selected private recovery: 3,244,032 cases / 520 reports; full and hardware qualification remain separate", flush=True) + + +if __name__ == "__main__": + main() From 5ce0f504f14e43404ee3b308f619318bf7f34848 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 00:19:05 +0300 Subject: [PATCH 183/211] Identify pinned private audit reproduction checkout --- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 7691d75b..b79877c2 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13597,6 +13597,9 @@ normalized candidate CPU inputs, the exact 14 named tests, and all 520 report hashes and logical counts. Its LF checkout rule preserves the pinned manifest bytes on Windows. Later source changes require an explicit new qualification; the helper rejects a changed baseline rather than silently accepting it. +Use checkout `df9de9bb727f473b8ee31b376efa1c8e83c803ec` for this pinned +reproduction. Later test-only extensions also change that baseline; their +evidence does not authorize rewriting the manifest or relabeling this run. ```powershell python scripts/test-copper68k-private-predecrement.py --output artifacts/private-predecrement From de4bcc24d42b50bf1937fb4e93e4cd5a3c965c52 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 00:24:56 +0300 Subject: [PATCH 184/211] Qualify mixed-width nested 040 writeback recovery --- .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- ...yntheticM68040NestedWritebackFaultTests.cs | 54 ++++++++++++---- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 11 ++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 63 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 16 +++++ 5 files changed, 133 insertions(+), 13 deletions(-) diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 15391c73..ee157064 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -39,7 +39,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() : form == "odd-PC-chained-SR-provenance" ? "Chained format0/2/3 and format7 normal/CM SR handoff, stack consumption and header reads have separate generated WinUAE software coverage; access-frame validation/no-replay checks are synthetic. Separate composed-entry and executed-MMU-entry CM lifetime discovery profiles disagree after odd-PC fault/repair; the executed profile also exposes stacked-SR disagreement. Neither qualifies the full run loop or hardware. CT/CU/CP pending/foreign-context chains, full reference exception qualification and hardware behavior remain unqualified" : form == "writeback-handler" - ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks. Supplied same-width WB1/2/3 outer slots have actual nested MOVES-store fault/handler/RTE coverage at CCR=31; original construction of all slots, mixed widths, all CCRs, deeper repeated faults and trace/interrupt interruption remain required. Five legal MOVE16 forms have explicit saved-line completion coverage; cache-push lines and other data-fault construction remain required. Physical function-code spaces, MOVE16 WB1 validity and hardware timing are unqualified" + ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks. Actual nested MOVES-store fault/handler/RTE coverage includes same-width WB1/2/3 at CCR=31 and all 24 mixed-width triples at all 32 CCRs, common FC1/5 and ISP/MSP outer frames. Original construction of all slots, other same-width CCRs, heterogeneous FCs, user outer returns, deeper repeated faults and trace/interrupt interruption remain required. Five legal MOVE16 forms have explicit saved-line completion coverage; cache-push lines and other data-fault construction remain required. Physical function-code spaces, MOVE16 WB1 validity and hardware timing are unqualified" : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs index 09d10ed0..13e9d0fe 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs @@ -24,39 +24,69 @@ public void ActualHandlerStoresFaultCompleteAndResume(bool batch) for (var faultByte = 0; faultByte < width; faultByte++) { var id = $"68040/writeback/nested-physical-fault/size={width}/lane={lane}/FC={fc}/bank={bank}/WB={slot}/byte={faultByte}"; - try { Run(batch, width, lane, fc, bank, slot, faultByte); report.Record(id, "passing", null); } + try { Run(batch, [width, width, width], 31, lane, fc, bank, slot, faultByte); report.Record(id, "passing", null); } catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); } } report.Complete(output); } - private static void Run(bool batch, int width, uint lane, int fc, string bank, int faultSlot, int faultByte) + [EnvironmentFact("COPPER68K_RUN_040_MIXED_NESTED_WRITEBACK", "require mixed-width nested writebacks across all CCR values"), Trait("Suite", "ReferenceDiscovery")] + public void MixedWidthsAndAllCcrScalar() => MixedWidthsAndAllCcr(false); + + [EnvironmentFact("COPPER68K_RUN_040_MIXED_NESTED_WRITEBACK", "require mixed-width nested writebacks across all CCR values"), Trait("Suite", "ReferenceDiscovery")] + public void MixedWidthsAndAllCcrBatch() => MixedWidthsAndAllCcr(true); + + private void MixedWidthsAndAllCcr(bool batch) + { + var report = new CoverageBatch("68040", "mixed-nested-writeback-fault-" + (batch ? "batch" : "scalar")); + foreach (var w1 in new[] { 1, 2, 4 }) + foreach (var w2 in new[] { 1, 2, 4 }) + foreach (var w3 in new[] { 1, 2, 4 }) + { + if (w1 == w2 && w2 == w3) continue; // Existing same-width group stays separate. + int[] widths = [w1, w2, w3]; + for (var ccr = 0; ccr < 32; ccr++) + for (uint lane = 0; lane < 4; lane++) + foreach (var fc in new[] { 1, 5 }) + foreach (var bank in new[] { "ISP", "MSP" }) + for (var slot = 1; slot <= 3; slot++) + for (var faultByte = 0; faultByte < widths[slot - 1]; faultByte++) + { + var id = $"68040/writeback/mixed-nested-physical-fault/sizes={w1}-{w2}-{w3}/ccr={ccr:X2}/lane={lane}/FC={fc}/bank={bank}/WB={slot}/byte={faultByte}"; + try { Run(batch, widths, ccr, lane, fc, bank, slot, faultByte); report.Record(id, "passing", null); } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); } + } + } + report.Complete(output); + } + + private static void Run(bool batch, int[] widths, int ccr, uint lane, int fc, string bank, int faultSlot, int faultByte) { var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); - m.Reset(31); - var sr = (ushort)(bank == "MSP" ? 0x371f : 0x271f); + m.Reset(ccr); + var sr = (ushort)((bank == "MSP" ? 0x3700 : 0x2700) | ccr); var stacks = new Dictionary { ["user"] = 0x7800, ["ISP"] = 0x4700, ["MSP"] = 0x7400 }; var frame = stacks[bank] - 60; stacks[bank] = frame; + var width = widths[faultSlot - 1]; var size = width == 1 ? 0x20 : width == 2 ? 0x40 : 0; var wbStatus = (ushort)(0x80 | size | fc); - ushort[] statuses = [wbStatus, wbStatus, wbStatus]; + var statuses = widths.Select(w => (ushort)(0x80 | (w == 1 ? 0x20 : w == 2 ? 0x40 : 0) | fc)).ToArray(); uint[] addresses = [0x4200 + lane, 0x4300 + lane, 0x4400 + lane]; uint[] values = [0x89abcdef, 0x10203040, 0xfedcba98]; - var mask = width == 1 ? 0xffu : width == 2 ? 0xffffu : uint.MaxValue; - values = values.Select(x => x & mask).ToArray(); + values = values.Select((x, n) => x & (widths[n] == 1 ? 0xffu : widths[n] == 2 ? 0xffffu : uint.MaxValue)).ToArray(); for (uint at = frame; at < frame + 60; at++) m.InitializePhysical(at, 0, 1); m.InitializePhysical(frame, sr, 2); m.InitializePhysical(frame + 2, Next, 4); m.InitializePhysical(frame + 6, 0x7008, 2); - m.InitializePhysical(frame + 12, (uint)(size | fc), 2); + m.InitializePhysical(frame + 12, (uint)((statuses[0] & 0x60) | fc), 2); m.InitializePhysical(frame + 20, addresses[0], 4); for (var n = 0; n < 3; n++) { - m.InitializePhysical(frame + (uint)SyntheticM68040WritebackProgram.StatusOffset(n + 1), wbStatus, 2); + m.InitializePhysical(frame + (uint)SyntheticM68040WritebackProgram.StatusOffset(n + 1), statuses[n], 2); m.InitializePhysical(frame + (uint)SyntheticM68040WritebackProgram.AddressOffset(n + 1), addresses[n], 4); if (n == 0) - for (var b = 0; b < width; b++) - m.InitializePhysical(frame + 44 + ((addresses[0] + (uint)b) & 3), values[0] >> (8 * (width - 1 - b)), 1); + for (var b = 0; b < widths[0]; b++) + m.InitializePhysical(frame + 44 + ((addresses[0] + (uint)b) & 3), values[0] >> (8 * (widths[0] - 1 - b)), 1); else m.InitializePhysical(frame + (uint)SyntheticM68040WritebackProgram.DataOffset(n + 1), values[n], 4); for (uint b = 0; b < 12; b++) m.InitializePhysical(addresses[n] - 4 + b, 0x5a, 1); } @@ -135,7 +165,7 @@ void Define(uint at, uint value, int count) Execute("outer " + i.Operation); } } - if (!faulted || !stores.SequenceEqual(addresses.Select((a, n) => (a, width, values[n])))) + if (!faulted || !stores.SequenceEqual(addresses.Select((a, n) => (a, widths[n], values[n])))) throw new InvalidOperationException("WB1/WB2/WB3 completion order differs"); var operandWrites = bus.Accesses.Where(a => a.Write && addresses.Contains(a.Address)) .Select(a => (a.Address, a.Width, a.Value)); diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index a74385a9..c4f302a0 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -100,6 +100,17 @@ it does not replace or discard their remaining gaps. The original evidence remains unchanged; no new CPU execution is claimed from those validator controls. Their proof is `8d9cfea6562c99b9b7662b443fa0a57f004318291d8f525837fb842800da92ae`. + A later production-CPU fixture extension qualifies supplied mixed-width + nested writebacks: all 24 B/W/L width triples with differing slot widths, + all 32 CCR values, four lanes, common FC1/5, ISP/MSP outer frames, every + rejected byte/slot, and scalar/batch execution. Four named tests pass with + 172,032 new cases and 672 byte-identical retained cases. Independent proof: + `d41d476f7ef765c49c94fd7f6c180774ef84bbd32bd489cad6f3e9ffde56ac30`. + All 37 production CPU inputs are unchanged. The later inventory description + update changes no case generation or failing gate; all 480 required untested + protocol IDs remain. Original slot construction, other same-width CCRs, + heterogeneous FCs, user outer returns, deeper refaults and trace/interrupt + interruption remain open. This focused result is not a whole-suite result. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index b79877c2..62b7cefa 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13642,3 +13642,66 @@ portable selected result does not satisfy those full gates. Broad 010 RTE / 060 STOP disagreements, wider restoration protocols and unavailable hardware remain explicit. Milestone 6 stays **in progress**, `roadmapComplete=false`; PR #22 stays draft. No package publication or private production import occurs. + +### Supplied mixed-width 040 nested writebacks — 2026-10-09 + +`SyntheticM68040NestedWritebackFaultTests` now reuses one execution fixture for +independent WB1/WB2/WB3 widths and the initial CCR. The original same-width +theory still selects CCR=31 and retains its exact IDs. Two opt-in scalar/batch +facts exercise all **24 mixed B/W/L triples**, all **32 CCR values**, four +address lanes, common FC1/5, ISP/MSP outer frames, every pending slot and every +rejected byte of that slot's transfer. Each route has **86,016 cases**. +The outer supplied frame uses each slot's own size, masked value and status; +WB1 data stays memory aligned and WB2/WB3 data stays register aligned. + +Fresh isolated `MixedNestedWritebackV1` execution passes **all four named +tests**, with zero skipped/failed, **172,032 new cases** and **672 retained +cases** in four reports. Independent verification enumerates every new case +identifier and its unit weight, and binds exact source, loaded DLL, +definition/method, command, TRX counters and report identities. Both retained +336-case reports are byte-identical to `CurrentReferenceFullV2`. All **37 +production CPU inputs** match that earlier qualified CPU graph; no CPU fix, +package or consumer rerun is introduced. SourceLink's isolated-build warnings +remain recorded. + +The fixture executes a real normal-space handler-store fault, captures the +defined nested format-7 fields and completes that pending store through its +integer handler and explicit RTE. It checks saved PC/SR, register restoration, +DFC/SFC, stack consumption, WB1/WB2/WB3 order, accepted store widths/values, +canaries, no repeated/omitted store and a following MOVEQ sentinel. The outer +slots are supplied test inputs; this does not qualify their original hardware +construction, physical spaces, enabled MMU/cache/pipeline behavior or timing. + +```powershell +$env:COPPER68K_RUN_040_MIXED_NESTED_WRITEBACK = '1' +$env:COPPER68K_SYNTHETIC_REPORT_DIR = [IO.Path]::GetFullPath('artifacts/mixed-nested-writeback-reports') +dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path artifacts/mixed-nested-writeback-build --filter FullyQualifiedName~SyntheticM68040NestedWritebackFaultTests --logger 'trx;LogFileName=mixed.trx' --results-directory artifacts/mixed-nested-writeback-results +``` + +Use a fresh report/results directory and require all four named rows; skipped +opt-in rows are unavailable coverage. These selected tests do not replace the +ordinary suite or the explicit broader protocol gate. + +| Mixed nested evidence | SHA-256 | +| --- | --- | +| `MixedNestedWritebackV1/inputs.json` | `e97c6b22474f06da3d6f14808ef9304f0eedecb7cb5907d75be96e2d8906dc5b` | +| `MixedNestedWritebackV1/execution.json` | `f521dc5b1701f76d31519470288af7562742167d3a059903028cdbe652293184` | +| `MixedNestedWritebackIndependentV1.json` | `d41d476f7ef765c49c94fd7f6c180774ef84bbd32bd489cad6f3e9ffde56ac30` | +| New scalar report | `088f94d8e28700561089a2b9133f7c884609f2385c3a1d717fe92d98534f645c` | +| New batch report | `139739cbee60ec888ad6db9ba378a41e69ef9166001cd66371a306edc1bcf105` | +| Later inventory-description check | `ea63100f66ea260cfc4f9b73fe517d360d7935458cf3b8d72af7bc3dc6c8c594` | + +The executed snapshot has **233 source/project inputs**. A subsequent single +diagnostic literal in `SyntheticM68040AccessFrameAuditTests` acknowledges this +coverage. Static inspection proves only that literal changed: all **480 +required untested protocol IDs**, statuses, enable flag and failure gate remain. +That later note is separate from the frozen execution inputs; no full current +suite is claimed. The earlier private 259/260-input full executions retain +their original snapshots and pending verification. + +Original construction of all outer slots, other CCR values for same-width +slots, heterogeneous FCs, user outer returns, deeper repeated faults and +trace/interrupt interruption remain required. The failed broad 010 RTE / 060 +STOP reference gate and unavailable 030/040 hardware remain open. Milestone 6 +stays **in progress**, `roadmapComplete=false`; PR #22 stays draft. No package +publication or private production import occurs. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 570be787..a64bb17c 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4247,3 +4247,19 @@ commands and source/evidence identities. Broader full runs retain their own frozen inputs and pending verification; the selected result cannot close milestone 6, the broad failed reference gate or hardware gaps. Milestone 6 stays **in progress**, `roadmapComplete=false`; no private import or publication. + +### Milestone 6 checkpoint — mixed nested 040 writebacks, 2026-10-09 + +The shared supplied-frame handler fixture now varies each WB1/WB2/WB3 width +independently and initial CCR. All 24 mixed B/W/L triples pass across all 32 +CCR values, four lanes, common FC1/5, ISP/MSP outer frames, each rejected byte +and pending slot, and scalar/batch routes: 172,032 new cases. Its two existing +same-width rows retain 672 cases and byte-identical reports. Four named tests +pass; independent source/assembly/method/report checks are recorded in the +qualification log. Production CPU code is unchanged. The 480-case broader +040 protocol gate remains untested and failing when requested; only its +coverage description is updated. Original construction, other CCRs for +same-width slots, heterogeneous FCs, user outer returns, deeper refaults and +trace/interrupt interruption remain required. This focused result does not +qualify the running private full suites or close milestone 6. Status remains +**in progress**, `roadmapComplete=false`; no import or publication. From bfceb08a589663691919ddc882bd3d19600fc5fc Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 00:38:33 +0300 Subject: [PATCH 185/211] Record complete private write-candidate qualification --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 12 ++++-- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 37 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 13 +++++++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index c4f302a0..1bafba00 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -63,9 +63,15 @@ it does not replace or discard their remaining gaps. 1,204,224-case evidence and a reviewable two-file patch. Its same-build deep/API, eight invalid-request guards and local .79 consumers are independently verified. Exact source/assembly/package linkage is recorded in the qualification log. - Its newer full replay has started; its result and independent verification - remain pending. The completed predecessor cannot qualify the newer CPU changes. - No private production import occurs. + Its full replay now completes with 8,091 passed / 37 unavailable / zero + failed. Independent verification binds all 8,128 rows, 13,753 reports / + 111,322,058 cases, ten qualified native presets and the ordinary gate. + Complete linkage to its same-build deep/API/guards and same-source .79 + consumers is + `3097ce893cfc9581dcb597b3f4c15232f7f42edc85e7c3f6d4a8c9584806924b`. + This qualifies the frozen 259-input graph; it cannot qualify the later + 260-input read change or the current test extensions. No private production + import occurs. The 040 access-frame discovery inventory retains all 480 required untested combinations (three banks, 32 CCR images, five protocol labels). Its diagnostic notes now acknowledge the separately qualified MOVES, nested supplied-slot diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 62b7cefa..35ef26cd 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13705,3 +13705,40 @@ trace/interrupt interruption remain required. The failed broad 010 RTE / 060 STOP reference gate and unavailable 030/040 hardware remain open. Milestone 6 stays **in progress**, `roadmapComplete=false`; PR #22 stays draft. No package publication or private production import occurs. + +### Complete private predecrement write-candidate evidence — 2026-10-09 + +The frozen `PredecrementWriteFixV1` **259-input / 39-CPU** snapshot finishes +with **8,091 passed / 37 unavailable / zero failed**. Independent full +verification checks the exact **8,128-row roster**, **8,096 definitions**, +all loaded methods/assemblies, commands, sources, pinned native inputs and +every **13,753 report / 111,322,058 logical cases** against its frozen catalog. +All ten qualified WinUAE preset reports match their retained architectural +results with the new CPU/adapter identities. The report-only ordinary gate +passes **86,663,530 cases / 783 batches**. All unavailable rows remain explicit; +the broad Basic audit is still failed and is not substituted by these presets. + +The complete link independently verifies the same source/assembly identities +across full, selected, seeded, SingleStepTests/Musashi and eight request-guard +evidence. It checks the compiled API's zero changes (20 types / 211 records) +and the already qualified same-source local-only `.79` consumer evidence. +Package metadata retains its separate binary identity; source equality is +explicit. No new reference, API, package or consumer execution is claimed. + +| Complete write-candidate evidence | SHA-256 | +| --- | --- | +| `PredecrementWriteFixV1/full/proof.json` | `850ee974c38565c99ae90cd943fda3f0a68196581c89729a7459aec00fc9e154` | +| Earlier bounded source/deep/API/consumer link | `b744c7979487b28aa7b134011b22a90e2d755f4808e95a9d23e36a93afb06a0a` | +| `predecrement-complete-link-v1.json` | `3097ce893cfc9581dcb597b3f4c15232f7f42edc85e7c3f6d4a8c9584806924b` | + +This full proof qualifies only its original snapshot. The **260-input read +candidate's full execution remains live** and requires its own independent +verification. The subsequent 040 mixed-width fixture has its own production +CPU selected evidence; newer same-width-CCR/user-return work is separate and +not yet qualified. Earlier expected catalogs and completed predecessors are +not relabeled as runs of those later sources. + +The source remains a private review candidate. The broad 010 RTE / 060 STOP +disagreements, broader restoration protocols and unavailable 030/040 hardware +remain open. Milestone 6 stays **in progress**, `roadmapComplete=false`; +PR #22 remains draft. No CPU import or package publication occurs. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index a64bb17c..4dd40f33 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4263,3 +4263,16 @@ same-width slots, heterogeneous FCs, user outer returns, deeper refaults and trace/interrupt interruption remain required. This focused result does not qualify the running private full suites or close milestone 6. Status remains **in progress**, `roadmapComplete=false`; no import or publication. + +### Milestone 6 checkpoint — complete private write-candidate evidence, 2026-10-09 + +The frozen 259-input / 39-CPU write-recovery candidate completes its full run: +8,091 passed / 37 unavailable / zero failed. Independent verification binds +all 8,128 rows, 13,753 reports / 111,322,058 logical cases, ten qualified native +presets and the ordinary 86,663,530-case / 783-batch semantic gate. Complete +linkage connects that full proof to the candidate's already qualified deep +references, eight request guards, compiled API and same-source local .79 +consumers. These results qualify that frozen graph, not the newer read candidate +or later production-test extensions. Broad reference and restoration gaps keep +milestone 6 **in progress**, `roadmapComplete=false`; no private import or +package publication occurs. From 2c3b1d29fa73e48778399ee1abd3905af8a2c05b Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 00:54:24 +0300 Subject: [PATCH 186/211] Qualify supplied 040 user returns and same-width CCRs --- .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- ...yntheticM68040NestedWritebackFaultTests.cs | 53 ++++++++++++++-- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 12 ++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 61 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 +++++ 5 files changed, 136 insertions(+), 7 deletions(-) diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index ee157064..f32ff3d8 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -39,7 +39,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() : form == "odd-PC-chained-SR-provenance" ? "Chained format0/2/3 and format7 normal/CM SR handoff, stack consumption and header reads have separate generated WinUAE software coverage; access-frame validation/no-replay checks are synthetic. Separate composed-entry and executed-MMU-entry CM lifetime discovery profiles disagree after odd-PC fault/repair; the executed profile also exposes stacked-SR disagreement. Neither qualifies the full run loop or hardware. CT/CU/CP pending/foreign-context chains, full reference exception qualification and hardware behavior remain unqualified" : form == "writeback-handler" - ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks. Actual nested MOVES-store fault/handler/RTE coverage includes same-width WB1/2/3 at CCR=31 and all 24 mixed-width triples at all 32 CCRs, common FC1/5 and ISP/MSP outer frames. Original construction of all slots, other same-width CCRs, heterogeneous FCs, user outer returns, deeper repeated faults and trace/interrupt interruption remain required. Five legal MOVE16 forms have explicit saved-line completion coverage; cache-push lines and other data-fault construction remain required. Physical function-code spaces, MOVE16 WB1 validity and hardware timing are unqualified" + ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks. Actual nested MOVES-store fault/handler/RTE coverage includes all same-width and 24 mixed-width triples at all 32 CCRs, common FC1/5 and ISP/MSP outer frames; supplied user returns cover three same-width and six distinct-width permutations at all CCRs. Original construction of all slots, other user-width combinations and user-M returns, heterogeneous FCs, deeper repeated faults and trace/interrupt interruption remain required. Five legal MOVE16 forms have explicit saved-line completion coverage; cache-push lines and other data-fault construction remain required. Physical function-code spaces, MOVE16 WB1 validity and hardware timing are unqualified" : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs index 13e9d0fe..583eb266 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs @@ -60,14 +60,55 @@ private void MixedWidthsAndAllCcr(bool batch) report.Complete(output); } + [EnvironmentFact("COPPER68K_RUN_040_NESTED_RETURN_BANKS", "require other same-width CCRs and supplied user-frame returns"), Trait("Suite", "ReferenceDiscovery")] + public void OtherCcrAndUserReturnsScalar() => OtherCcrAndUserReturns(false); + + [EnvironmentFact("COPPER68K_RUN_040_NESTED_RETURN_BANKS", "require other same-width CCRs and supplied user-frame returns"), Trait("Suite", "ReferenceDiscovery")] + public void OtherCcrAndUserReturnsBatch() => OtherCcrAndUserReturns(true); + + private void OtherCcrAndUserReturns(bool batch) + { + var report = new CoverageBatch("68040", "nested-writeback-return-banks-" + (batch ? "batch" : "scalar")); + foreach (var bank in new[] { "user", "ISP", "MSP" }) + foreach (var w1 in new[] { 1, 2, 4 }) + foreach (var w2 in new[] { 1, 2, 4 }) + foreach (var w3 in new[] { 1, 2, 4 }) + { + var same = w1 == w2 && w2 == w3; + var allDifferent = w1 != w2 && w2 != w3 && w1 != w3; + // Separate dimensions: complete the same-width CCR matrix and use + // fixed width permutations for user returns, not a larger product. + if (!same && (bank != "user" || !allDifferent)) continue; + int[] widths = [w1, w2, w3]; + for (var ccr = 0; ccr < 32; ccr++) + { + if (bank != "user" && ccr == 31) continue; // Retained original group. + for (uint lane = 0; lane < 4; lane++) + foreach (var fc in new[] { 1, 5 }) + for (var slot = 1; slot <= 3; slot++) + for (var faultByte = 0; faultByte < widths[slot - 1]; faultByte++) + { + var id = $"68040/writeback/nested-return-bank/sizes={w1}-{w2}-{w3}/ccr={ccr:X2}/lane={lane}/FC={fc}/bank={bank}/WB={slot}/byte={faultByte}"; + try { Run(batch, widths, ccr, lane, fc, bank, slot, faultByte); report.Record(id, "passing", null); } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); } + } + } + } + report.Complete(output); + } + private static void Run(bool batch, int[] widths, int ccr, uint lane, int fc, string bank, int faultSlot, int faultByte) { var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); m.Reset(ccr); - var sr = (ushort)((bank == "MSP" ? 0x3700 : 0x2700) | ccr); + var sr = (ushort)((bank == "user" ? 0x0700 : bank == "MSP" ? 0x3700 : 0x2700) | ccr); + // User-frame writebacks are serviced by supervisor code on ISP. The + // supplied outer SR changes mode only when the outer RTE completes. + var handlerBank = bank == "user" ? "ISP" : bank; + var handlerSr = (ushort)((handlerBank == "MSP" ? 0x3700 : 0x2700) | ccr); var stacks = new Dictionary { ["user"] = 0x7800, ["ISP"] = 0x4700, ["MSP"] = 0x7400 }; - var frame = stacks[bank] - 60; stacks[bank] = frame; + var frame = stacks[handlerBank] - 60; stacks[handlerBank] = frame; var width = widths[faultSlot - 1]; var size = width == 1 ? 0x20 : width == 2 ? 0x40 : 0; var wbStatus = (ushort)(0x80 | size | fc); @@ -96,7 +137,7 @@ private static void Run(bool batch, int[] widths, int ccr, uint lane, int fc, st m.InitializePhysical(Next + 2, 0x4e71, 2); m.Start(); m.Core.State.SetUserStackPointer(stacks["user"]); m.Core.State.SetInterruptStackPointer(stacks["ISP"]); m.Core.State.SetMasterStackPointer(stacks["MSP"]); - m.Core.State.StatusRegister = sr; m.Core.State.ProgramCounter = Handler; + m.Core.State.StatusRegister = handlerSr; m.Core.State.ProgramCounter = Handler; m.Core.State.DestinationFunctionCode = 7; m.Core.State.SourceFunctionCode = 6; var e = ArchitecturalExpectation.Capture(m); var sequence = m.Core.State.ExceptionSequence; e.ControlChecks["DFC"] = (s => s.DestinationFunctionCode, 7); @@ -113,7 +154,7 @@ private static void Run(bool batch, int[] widths, int ccr, uint lane, int fc, st { faulted = true; var savedSr = e.Sr; var resume = i.Pc + 4; - var nested = stacks[bank] - 60; stacks[bank] = nested; + var nested = stacks[handlerBank] - 60; stacks[handlerBank] = nested; M68040StackFixture.SetStacks(e, stacks, (ushort)(savedSr & 0x3fff)); e.Pc = Handler; e.ExceptionVector = 2; e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence + 1); @@ -147,7 +188,7 @@ private static void Run(bool batch, int[] widths, int ccr, uint lane, int fc, st var ni = program.Instructions.Single(x => x.Pc == e.Pc); program.Expect(ni, m, e, nested, ns, na, nd, nestedD, nestedA, (uint)fc, stacks, nestedStores); if (ni.Operation == "return") - { stacks[bank] = nested + 60; M68040StackFixture.SetStacks(e, stacks, savedSr); e.Pc = resume; returned = true; } + { stacks[handlerBank] = nested + 60; M68040StackFixture.SetStacks(e, stacks, savedSr); e.Pc = resume; returned = true; } Execute("nested " + ni.Operation); } if (!nestedStores.SequenceEqual(new[] { (addresses[faultSlot - 1], width, values[faultSlot - 1]) })) @@ -161,7 +202,7 @@ void Define(uint at, uint value, int count) { program.Expect(i, m, e, frame, statuses, addresses, data, originalD, originalA, 7, stacks, stores); if (i.Operation == "return") - { stacks[bank] = frame + 60; M68040StackFixture.SetStacks(e, stacks, sr); e.Pc = Next; } + { stacks[handlerBank] = frame + 60; M68040StackFixture.SetStacks(e, stacks, sr); e.Pc = Next; } Execute("outer " + i.Operation); } } diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 1bafba00..b3044a48 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -117,6 +117,18 @@ it does not replace or discard their remaining gaps. protocol IDs remain. Original slot construction, other same-width CCRs, heterogeneous FCs, user outer returns, deeper refaults and trace/interrupt interruption remain open. This focused result is not a whole-suite result. + A further selected slice adds 20,832 same-width CCR=0..30 cases and 32,256 + supplied user-return cases (three same-width triples and six distinct-width + permutations, all CCRs). Four named tests pass with those 53,088 new cases + and 672 unchanged controls. Independent proof: + `7decf4a860ff66b4f3911b2126b0760d7f042018090cbda8d1afac64242d0136`. + It separates the supplied user SR from the supervisor handler SR, checks + nested ISP service and the final USP switch. The earlier mixed-width matrix + retains its own frozen evidence; it is not reexecuted by this slice. All + 37 production CPU inputs remain unchanged. A later diagnostic-only update + retains all 480 cases and the failing gate. Other user-width combinations, + user-M returns, heterogeneous FCs, deeper refaults, trace/interrupts and + original slot construction remain open. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 35ef26cd..b9b955e8 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13742,3 +13742,64 @@ The source remains a private review candidate. The broad 010 RTE / 060 STOP disagreements, broader restoration protocols and unavailable 030/040 hardware remain open. Milestone 6 stays **in progress**, `roadmapComplete=false`; PR #22 remains draft. No CPU import or package publication occurs. + +### Supplied 040 user returns and remaining same-width CCRs — 2026-10-09 + +The shared supplied-frame fixture separates the outer saved SR from the running +supervisor handler SR. For a supplied user return the outer frame and nested +fault service use ISP; the final outer RTE selects USP. ISP/MSP returns retain +their existing supervisor handler behavior. No production CPU input changes. + +A fresh selected run passes four named tests, zero failed or skipped, with +53,088 new cases and 672 unchanged controls. Each new scalar/batch route has +26,544 cases: 16,128 user, 5,208 ISP and 5,208 MSP. User coverage includes three +same-width B/W/L triples and six distinct-width permutations, all 32 CCRs. +Supervisor coverage adds CCR=0..30 for same-width triples; the retained controls +already cover CCR=31. Both routes vary four lanes, common FC1/5, each pending +slot and every rejected byte. This does not cover the other eighteen mixed-width +user combinations or user-M returns. + +The fixture checks saved SR/PC, nested stack allocation, pending data, handler +completion, RTE, final USP/ISP/MSP, DFC/SFC, memory canaries, store width/value +and order, and the following MOVEQ sentinel. Independent verification checks +every literal case identifier and weight, all 233 snapshot inputs, all 37 CPU +inputs against the retained production baseline, three assembly identities, +loaded method identities, the completed four-row TRX and four reports. The +earlier 172,032 mixed-width cases retain separate frozen evidence and are not +rerun or relabeled here. This selected run is not a whole-class/full-CPU run; +no new API, package, consumer or hardware qualification is claimed. + +Reproduce in fresh output directories; four completed passing named rows are +required and skipped rows are unavailable coverage: + +```powershell +$env:COPPER68K_RUN_040_NESTED_RETURN_BANKS = '1' +$env:COPPER68K_SYNTHETIC_REPORT_DIR = [IO.Path]::GetFullPath('artifacts/nested-return-bank-reports') +dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path artifacts/nested-return-bank-build --filter 'FullyQualifiedName~SyntheticM68040NestedWritebackFaultTests.OtherCcrAndUserReturns|FullyQualifiedName~SyntheticM68040NestedWritebackFaultTests.ActualHandlerStoresFaultCompleteAndResume' --logger 'trx;LogFileName=returns.trx' --results-directory artifacts/nested-return-bank-results +``` + +| Selected evidence | SHA-256 | +| --- | --- | +| `NestedReturnBanksV1/inputs.json` | `fa5784d7c0b4d5ccc51c4da90a9f896f407961cfbbb21337fd5658a3e8769b8e` | +| `NestedReturnBanksV1/execution.json` | `16047dbf0bcacf05852ea7c6490f70c856c3b849bd4a7672ae52cb5ff1a6ec1c` | +| `NestedReturnBanksIndependentV1.json` | `7decf4a860ff66b4f3911b2126b0760d7f042018090cbda8d1afac64242d0136` | +| New scalar report | `2f13ab3151e013f7ab460d9e98f7fbc8b629ef6b5b17e4b6d7c7532ab0cea1b7` | +| New batch report | `8644263f6470b55a7d8570a6fbb4aafb2d9e76f6e5e8e152b8ea60258677b24f` | +| Tested/recovered fixture | `ec2e32af86eca7b8887303ff828269ca4cc560762909c22dbc7d31d175f1531a` | + +An external cleanup removed the checkout after verification. The pushed base +was restored and the fixture recovered byte-for-byte from its executed source +snapshot. The diagnostic-only coverage description was reconstructed separately: +its bytes differ from the lost description, so its earlier hash is not reused. +A fresh static check verifies that only one diagnostic literal changes, with +all 480 required untested cases, enabling condition and failing gate unchanged. +Its recovered source identity is +`b5b0310ea6e8621e9d2a982d157368b9f4841b1942b322404766f923548f8afd`. +The dirty primary checkout and original evidence remain untouched. + +Original slot construction, other user-width combinations, user-M returns, +heterogeneous FCs, deeper refaults and trace/interrupt interruption remain open. +Broad failed reference gates and unavailable hardware remain unqualified. The +read-candidate full run requires its own terminal result and independent proof. +Milestone 6 remains **in progress**, `roadmapComplete=false`; no CPU import or +package publication occurs. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 4dd40f33..4b8413e3 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4276,3 +4276,18 @@ consumers. These results qualify that frozen graph, not the newer read candidate or later production-test extensions. Broad reference and restoration gaps keep milestone 6 **in progress**, `roadmapComplete=false`; no private import or package publication occurs. + +### Milestone 6 checkpoint — supplied user returns and same-width CCRs, 2026-10-09 + +The shared nested-writeback fixture now distinguishes the supplied outer SR +from the running supervisor handler SR. A selected four-test slice passes +53,088 new cases: 20,832 remaining same-width CCR cases and 32,256 canonical +supplied user returns. Both scalar/batch routes cover all CCRs, lanes, common +FC1/5, each rejected byte/slot, nested supervisor service and final USP return. +Its 672 original controls remain byte-identical. Independent verification binds +all identifiers, exact named methods and source/assembly/report identities. +All 37 production CPU inputs are unchanged; earlier mixed-width evidence is +not relabeled as this later source execution. The qualification log records +scope, commands, identities and wider user-width/user-M/FC/refault/trace gaps. +All 480 broader protocol cases remain untested and fail their requested gate. +Milestone 6 remains **in progress**, `roadmapComplete=false`; no import or release. From b8578b689e3d80919129d92eb767dd146cc10797 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 01:03:12 +0300 Subject: [PATCH 187/211] Qualify remaining supplied 040 user-frame widths --- .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- ...yntheticM68040NestedWritebackFaultTests.cs | 31 +++++++++++ docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 11 ++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 55 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 14 +++++ 5 files changed, 112 insertions(+), 1 deletion(-) diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index f32ff3d8..89cc683a 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -39,7 +39,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() : form == "odd-PC-chained-SR-provenance" ? "Chained format0/2/3 and format7 normal/CM SR handoff, stack consumption and header reads have separate generated WinUAE software coverage; access-frame validation/no-replay checks are synthetic. Separate composed-entry and executed-MMU-entry CM lifetime discovery profiles disagree after odd-PC fault/repair; the executed profile also exposes stacked-SR disagreement. Neither qualifies the full run loop or hardware. CT/CU/CP pending/foreign-context chains, full reference exception qualification and hardware behavior remain unqualified" : form == "writeback-handler" - ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks. Actual nested MOVES-store fault/handler/RTE coverage includes all same-width and 24 mixed-width triples at all 32 CCRs, common FC1/5 and ISP/MSP outer frames; supplied user returns cover three same-width and six distinct-width permutations at all CCRs. Original construction of all slots, other user-width combinations and user-M returns, heterogeneous FCs, deeper repeated faults and trace/interrupt interruption remain required. Five legal MOVE16 forms have explicit saved-line completion coverage; cache-push lines and other data-fault construction remain required. Physical function-code spaces, MOVE16 WB1 validity and hardware timing are unqualified" + ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks. Actual nested MOVES-store fault/handler/RTE coverage includes all 27 B/W/L width triples at all 32 CCRs, common FC1/5 and supplied user/ISP/MSP outer frames, with separate frozen execution evidence for each promoted slice. Original construction of all slots, user-M returns, heterogeneous FCs, deeper repeated faults and trace/interrupt interruption remain required. Five legal MOVE16 forms have explicit saved-line completion coverage; cache-push lines and other data-fault construction remain required. Physical function-code spaces, MOVE16 WB1 validity and hardware timing are unqualified" : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs index 583eb266..e2207349 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs @@ -97,6 +97,37 @@ private void OtherCcrAndUserReturns(bool batch) report.Complete(output); } + [EnvironmentFact("COPPER68K_RUN_040_USER_MIXED_NESTED_WRITEBACK", "require remaining mixed-width supplied user-frame returns"), Trait("Suite", "ReferenceDiscovery")] + public void RemainingUserMixedWidthsScalar() => RemainingUserMixedWidths(false); + + [EnvironmentFact("COPPER68K_RUN_040_USER_MIXED_NESTED_WRITEBACK", "require remaining mixed-width supplied user-frame returns"), Trait("Suite", "ReferenceDiscovery")] + public void RemainingUserMixedWidthsBatch() => RemainingUserMixedWidths(true); + + private void RemainingUserMixedWidths(bool batch) + { + var report = new CoverageBatch("68040", "user-mixed-nested-writeback-fault-" + (batch ? "batch" : "scalar")); + foreach (var w1 in new[] { 1, 2, 4 }) + foreach (var w2 in new[] { 1, 2, 4 }) + foreach (var w3 in new[] { 1, 2, 4 }) + { + // Exactly two distinct widths: the preceding user-return slice + // separately qualifies same-width and three-distinct-width triples. + if ((w1 == w2 && w2 == w3) || (w1 != w2 && w2 != w3 && w1 != w3)) continue; + int[] widths = [w1, w2, w3]; + for (var ccr = 0; ccr < 32; ccr++) + for (uint lane = 0; lane < 4; lane++) + foreach (var fc in new[] { 1, 5 }) + for (var slot = 1; slot <= 3; slot++) + for (var faultByte = 0; faultByte < widths[slot - 1]; faultByte++) + { + var id = $"68040/writeback/user-mixed-nested-physical-fault/sizes={w1}-{w2}-{w3}/ccr={ccr:X2}/lane={lane}/FC={fc}/bank=user/WB={slot}/byte={faultByte}"; + try { Run(batch, widths, ccr, lane, fc, "user", slot, faultByte); report.Record(id, "passing", null); } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); } + } + } + report.Complete(output); + } + private static void Run(bool batch, int[] widths, int ccr, uint lane, int fc, string bank, int faultSlot, int faultByte) { var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index b3044a48..a37989b3 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -129,6 +129,17 @@ it does not replace or discard their remaining gaps. retains all 480 cases and the failing gate. Other user-width combinations, user-M returns, heterogeneous FCs, deeper refaults, trace/interrupts and original slot construction remain open. + The eighteen remaining mixed-width user triples subsequently pass 64,512 + new cases plus 672 unchanged controls in four named tests. Independent proof + `7aefba59b42a4371e59dec1f656a9b1e1aef2016bfecd6a01eadd0722ffd440f` + verifies their exact identifiers, sources, assemblies and reports. Together + with the separate earlier slices, supplied user/ISP/MSP returns now cover + all 27 B/W/L triples, all CCRs, common FC1/5, lanes and rejected bytes/slots. + Two restored CPU files differ from the retained baseline only in line endings; + exact CRLF-to-LF comparison is recorded, not raw-source equality. No shared + execution-fixture or CPU semantic change occurs. User-M returns, + heterogeneous FCs, deeper refaults, trace/interrupts and original construction + remain open; the 480-case broader gate is retained. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index b9b955e8..0e33870d 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13803,3 +13803,58 @@ Broad failed reference gates and unavailable hardware remain unqualified. The read-candidate full run requires its own terminal result and independent proof. Milestone 6 remains **in progress**, `roadmapComplete=false`; no CPU import or package publication occurs. + +### Remaining supplied user-frame width triples — 2026-10-09 + +The eighteen B/W/L triples with exactly two distinct widths complete the +remaining supplied user-return width dimension. The shared `Run` fixture is +unchanged byte-for-byte in text compared with `2c3b1d2`; only two environment +facts and their deterministic scenario generator are added. They run scalar +and batch paths across all 32 CCRs, four lanes, common FC1/5, every pending +slot and every rejected byte. Each route has 32,256 new cases. The previous +same-width and three-distinct-width user triples retain their separate evidence. + +Fresh execution completes with four named passes, zero failed or skipped: +64,512 new cases plus 672 retained controls. Independent verification binds all +233 raw snapshot inputs and assets, all three executed assembly identities, +the exact command/settings, loaded test definitions/methods, completed TRX +counters and every report key/status/weight. Both retained reports are identical +to the earlier production full-run controls. Original handler execution, +saved-SR/PC, stack selection, pending stores, canaries, DFC/SFC, store ordering +and following-instruction checks are reused without changing their expectations. + +The restored checkout differs from the pinned production CPU baseline only by +line endings in `M68040Support.cs` and `M68kAdvancedTimingInterpreter.System.cs`. +The verifier checks the baseline raw hashes and compares all 37 CPU files after +CRLF-to-LF conversion only. It does not claim raw equality for those two inputs, +reuse a previous assembly identity or qualify current mainline/consumer binaries. + +Reproduce with fresh output directories; require four completed named passes: + +```powershell +$env:COPPER68K_RUN_040_USER_MIXED_NESTED_WRITEBACK = '1' +$env:COPPER68K_SYNTHETIC_REPORT_DIR = [IO.Path]::GetFullPath('artifacts/user-mixed-nested-reports') +dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path artifacts/user-mixed-nested-build --filter 'FullyQualifiedName~SyntheticM68040NestedWritebackFaultTests.RemainingUserMixedWidths|FullyQualifiedName~SyntheticM68040NestedWritebackFaultTests.ActualHandlerStoresFaultCompleteAndResume' --logger 'trx;LogFileName=returns.trx' --results-directory artifacts/user-mixed-nested-results +``` + +| Selected evidence | SHA-256 | +| --- | --- | +| `UserMixedNestedV1/inputs.json` | `522b906f3a39a66bd596a46f67943a7cbc7d6a2e48746bb5abc921c7e4304022` | +| `UserMixedNestedV1/execution.json` | `9a3a252eb364d84a56fc6cb0e44537c2ad63c718790c661e6e9aedf013d29bb0` | +| `UserMixedNestedIndependentV1.json` | `7aefba59b42a4371e59dec1f656a9b1e1aef2016bfecd6a01eadd0722ffd440f` | +| New scalar report | `481dd6480ac02ef9205781081284331b46d30fbe521827133fd00b6917c409fb` | +| New batch report | `0cab2d95c97c3139025d94d7aa0bb4cdfcd4770ca32b25cf85946927cdc99693` | +| Later diagnostic-only static proof | `efee44dfa6d966e6084c9a57edc917446babff745c535b79e7c162956a85c29a` | + +The subsequent inventory description acknowledges the combined supplied-frame +width coverage; its separate static check retains all 480 untested protocol +cases, enabling condition and failing gate. It is not part of the executed +snapshot. No earlier mixed-width/user-return evidence is relabeled as execution +of this later test source. No full-current-suite, API, package, consumer or +hardware result is claimed. The live private read-candidate full run retains +its own source/assembly identities and still requires independent verification. + +Original frame construction, user-M returns, heterogeneous FCs, deeper repeated +faults and trace/interrupt interruption remain required. The broad reference +disagreements remain failed/unqualified. Milestone 6 remains **in progress**, +`roadmapComplete=false`; PR #22 stays draft. No CPU import or release occurs. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 4b8413e3..c47ccba4 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4291,3 +4291,17 @@ not relabeled as this later source execution. The qualification log records scope, commands, identities and wider user-width/user-M/FC/refault/trace gaps. All 480 broader protocol cases remain untested and fail their requested gate. Milestone 6 remains **in progress**, `roadmapComplete=false`; no import or release. + +### Milestone 6 checkpoint — remaining supplied user widths, 2026-10-09 + +Four selected tests pass 64,512 new cases for the eighteen remaining mixed-width +user triples and 672 retained controls with identical reports. Independent +verification checks exact case identifiers, source/assembly/method identities +and the unchanged shared execution fixture. All 37 CPU inputs match the retained +baseline after CRLF-to-LF conversion only; two raw identities changed during +checkout restoration. Earlier user and supervisor slices retain separate frozen +evidence. Their combined supplied-frame coverage includes all 27 B/W/L triples, +all CCRs, lanes, common FC1/5 and rejected bytes/slots on scalar/batch routes. +Original frame construction, user-M, heterogeneous FCs, deeper refaults and +trace/interrupt qualification remain open. The 480-case broader gate is retained. +Milestone 6 remains **in progress**, `roadmapComplete=false`; no import or release. From a2d4b3b3f85ea15cc3cb9a8e311df32984519c31 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 01:10:46 +0300 Subject: [PATCH 188/211] Qualify differing nested writeback function codes --- .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- ...yntheticM68040NestedWritebackFaultTests.cs | 47 ++++++++++++++++- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 8 +++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 52 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 14 +++++ 5 files changed, 120 insertions(+), 3 deletions(-) diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 89cc683a..6b5c46e8 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -39,7 +39,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() : form == "odd-PC-chained-SR-provenance" ? "Chained format0/2/3 and format7 normal/CM SR handoff, stack consumption and header reads have separate generated WinUAE software coverage; access-frame validation/no-replay checks are synthetic. Separate composed-entry and executed-MMU-entry CM lifetime discovery profiles disagree after odd-PC fault/repair; the executed profile also exposes stacked-SR disagreement. Neither qualifies the full run loop or hardware. CT/CU/CP pending/foreign-context chains, full reference exception qualification and hardware behavior remain unqualified" : form == "writeback-handler" - ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks. Actual nested MOVES-store fault/handler/RTE coverage includes all 27 B/W/L width triples at all 32 CCRs, common FC1/5 and supplied user/ISP/MSP outer frames, with separate frozen execution evidence for each promoted slice. Original construction of all slots, user-M returns, heterogeneous FCs, deeper repeated faults and trace/interrupt interruption remain required. Five legal MOVE16 forms have explicit saved-line completion coverage; cache-push lines and other data-fault construction remain required. Physical function-code spaces, MOVE16 WB1 validity and hardware timing are unqualified" + ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks. Actual nested MOVES-store fault/handler/RTE coverage includes all 27 B/W/L width triples at all 32 CCRs, common FC1/5 and supplied user/ISP/MSP outer frames, with separate frozen execution evidence for each promoted slice. Differing per-slot FC1/5 has separate structural-width and canonical-CCR coverage. Original construction of all slots, user-M returns, wider FC combinations, deeper repeated faults and trace/interrupt interruption remain required. Five legal MOVE16 forms have explicit saved-line completion coverage; cache-push lines and other data-fault construction remain required. Physical function-code spaces, MOVE16 WB1 validity and hardware timing are unqualified" : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs index e2207349..2175e967 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs @@ -129,6 +129,48 @@ private void RemainingUserMixedWidths(bool batch) } private static void Run(bool batch, int[] widths, int ccr, uint lane, int fc, string bank, int faultSlot, int faultByte) + => RunFunctionCodes(batch, widths, ccr, lane, [fc, fc, fc], bank, faultSlot, faultByte); + + [EnvironmentFact("COPPER68K_RUN_040_HETEROGENEOUS_NESTED_WRITEBACK", "require differing function codes in nested writeback slots"), Trait("Suite", "ReferenceDiscovery")] + public void HeterogeneousFunctionCodesScalar() => HeterogeneousFunctionCodes(false); + + [EnvironmentFact("COPPER68K_RUN_040_HETEROGENEOUS_NESTED_WRITEBACK", "require differing function codes in nested writeback slots"), Trait("Suite", "ReferenceDiscovery")] + public void HeterogeneousFunctionCodesBatch() => HeterogeneousFunctionCodes(true); + + private void HeterogeneousFunctionCodes(bool batch) + { + var report = new CoverageBatch("68040", "heterogeneous-nested-writeback-fault-" + (batch ? "batch" : "scalar")); + foreach (var f1 in new[] { 1, 5 }) + foreach (var f2 in new[] { 1, 5 }) + foreach (var f3 in new[] { 1, 5 }) + { + if (f1 == f2 && f2 == f3) continue; // Common FC groups retain separate coverage. + foreach (var bank in new[] { "user", "ISP", "MSP" }) + foreach (var w1 in new[] { 1, 2, 4 }) + foreach (var w2 in new[] { 1, 2, 4 }) + foreach (var w3 in new[] { 1, 2, 4 }) + { + int[] widths = [w1, w2, w3]; + // Structural widths use CCR=31; one canonical width triple + // independently exercises the other CCRs without duplicates. + for (var ccr = 0; ccr < 32; ccr++) + { + if (ccr != 31 && (w1 != 1 || w2 != 2 || w3 != 4)) continue; + for (uint lane = 0; lane < 4; lane++) + for (var slot = 1; slot <= 3; slot++) + for (var faultByte = 0; faultByte < widths[slot - 1]; faultByte++) + { + var id = $"68040/writeback/heterogeneous-nested-physical-fault/sizes={w1}-{w2}-{w3}/FCs={f1}-{f2}-{f3}/ccr={ccr:X2}/lane={lane}/bank={bank}/WB={slot}/byte={faultByte}"; + try { RunFunctionCodes(batch, widths, ccr, lane, [f1, f2, f3], bank, slot, faultByte); report.Record(id, "passing", null); } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); } + } + } + } + } + report.Complete(output); + } + + private static void RunFunctionCodes(bool batch, int[] widths, int ccr, uint lane, int[] functionCodes, string bank, int faultSlot, int faultByte) { var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); @@ -141,16 +183,17 @@ private static void Run(bool batch, int[] widths, int ccr, uint lane, int fc, st var stacks = new Dictionary { ["user"] = 0x7800, ["ISP"] = 0x4700, ["MSP"] = 0x7400 }; var frame = stacks[handlerBank] - 60; stacks[handlerBank] = frame; var width = widths[faultSlot - 1]; + var fc = functionCodes[faultSlot - 1]; var size = width == 1 ? 0x20 : width == 2 ? 0x40 : 0; var wbStatus = (ushort)(0x80 | size | fc); - var statuses = widths.Select(w => (ushort)(0x80 | (w == 1 ? 0x20 : w == 2 ? 0x40 : 0) | fc)).ToArray(); + var statuses = widths.Select((w, n) => (ushort)(0x80 | (w == 1 ? 0x20 : w == 2 ? 0x40 : 0) | functionCodes[n])).ToArray(); uint[] addresses = [0x4200 + lane, 0x4300 + lane, 0x4400 + lane]; uint[] values = [0x89abcdef, 0x10203040, 0xfedcba98]; values = values.Select((x, n) => x & (widths[n] == 1 ? 0xffu : widths[n] == 2 ? 0xffffu : uint.MaxValue)).ToArray(); for (uint at = frame; at < frame + 60; at++) m.InitializePhysical(at, 0, 1); m.InitializePhysical(frame, sr, 2); m.InitializePhysical(frame + 2, Next, 4); m.InitializePhysical(frame + 6, 0x7008, 2); - m.InitializePhysical(frame + 12, (uint)((statuses[0] & 0x60) | fc), 2); + m.InitializePhysical(frame + 12, (uint)((statuses[0] & 0x60) | functionCodes[0]), 2); m.InitializePhysical(frame + 20, addresses[0], 4); for (var n = 0; n < 3; n++) { diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index a37989b3..ca08dce8 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -140,6 +140,14 @@ it does not replace or discard their remaining gaps. execution-fixture or CPU semantic change occurs. User-M returns, heterogeneous FCs, deeper refaults, trace/interrupts and original construction remain open; the 480-case broader gate is retained. + Differing FC1/5 slot patterns now have separate selected evidence: 58,464 + new cases and 672 identical controls, four named passes, proof + `36232f1eb49a65f505579a35d7094fd9ee2143d3c0e2c24dacc588b735526476`. + All six differing patterns cover all 27 widths, user/ISP/MSP, lanes and + rejected bytes/slots at CCR=31; canonical widths 1/2/4 cover all CCRs. + This is separated structural/CCR coverage, not their full cross-product or + physical function-code-space qualification. Wider FCs, user-M, repeated + faults, original construction and trace/interrupts remain open. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 0e33870d..f1429a27 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13858,3 +13858,55 @@ Original frame construction, user-M returns, heterogeneous FCs, deeper repeated faults and trace/interrupt interruption remain required. The broad reference disagreements remain failed/unqualified. Milestone 6 remains **in progress**, `roadmapComplete=false`; PR #22 stays draft. No CPU import or release occurs. + +### Differing function codes in nested supplied writebacks — 2026-10-09 + +The shared fixture now accepts an independent function code for each WB slot. +Its existing common-code entry supplies the same FC three times. Slot status +words use their respective codes, the supplied outer SSW describes WB1, and +nested fault expectations and DFC restoration use the faulted slot's code. +The production handler program, CPU implementation and execution timing policy +are unchanged. This extends software-visible DFC checks; the recording bus does +not provide distinct physical function-code address spaces. + +Fresh execution completes with four named passes, zero failed or skipped: +58,464 new cases and 672 retained controls whose reports remain byte-identical. +There are 29,232 new cases per route, 9,744 per user/ISP/MSP bank. All six +differing FC1/5 patterns cover all 27 B/W/L triples, four lanes, every rejected +byte and slot at CCR=31: 13,608 cases per route. Canonical widths 1/2/4 cover +CCR=0..30 across those same patterns/banks/lanes/faults: 15,624 per route. +This deliberately separates structural and CCR dimensions; it does not claim +their full Cartesian product, user-M or other function-code values. + +Independent verification binds the exact command/settings, all 233 raw source +inputs and assets, three executed assemblies, named loaded definitions/methods, +completed TRX counters and every generated combination key/status/weight. +All 37 CPU inputs equal the pinned production baseline after CRLF-to-LF only; +the same two restored raw newline differences are retained explicitly. Prior +broader common-FC user/width matrices remain separate frozen evidence and are +not rerun or relabeled as results from this modified shared fixture. + +Reproduce with fresh directories; require four completed named passes: + +```powershell +$env:COPPER68K_RUN_040_HETEROGENEOUS_NESTED_WRITEBACK = '1' +$env:COPPER68K_SYNTHETIC_REPORT_DIR = [IO.Path]::GetFullPath('artifacts/heterogeneous-nested-reports') +dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path artifacts/heterogeneous-nested-build --filter 'FullyQualifiedName~SyntheticM68040NestedWritebackFaultTests.HeterogeneousFunctionCodes|FullyQualifiedName~SyntheticM68040NestedWritebackFaultTests.ActualHandlerStoresFaultCompleteAndResume' --logger 'trx;LogFileName=returns.trx' --results-directory artifacts/heterogeneous-nested-results +``` + +| Selected evidence | SHA-256 | +| --- | --- | +| `HeterogeneousNestedV1/inputs.json` | `0e7f79ad0d7489e5386b924fabb29604c68351d5e49be5c478fd69d67f7f5cf7` | +| `HeterogeneousNestedV1/execution.json` | `1e3472926eebbe900c53adae46a2d49b2bbc561f50ada7ad8a374c39e9d6db9b` | +| `HeterogeneousNestedIndependentV1.json` | `36232f1eb49a65f505579a35d7094fd9ee2143d3c0e2c24dacc588b735526476` | +| New scalar report | `52b50e8c7b71c6bb966d83966d5e805ac278c20087f55f098bf43cf1d2f1f2d1` | +| New batch report | `8137655d3f8831c39fe8d20c0572e9508b7ae9edb04a5c674882b2550bc56afe` | +| Later diagnostic-only static proof | `c4f619437afcc6ad6622ff48ef51ecb0ebc7b7a1080e83008180d2df705d5b02` | + +The subsequent inventory literal changes only its description and preserves all +480 untested cases, enabling condition and failing gate. It is separate from +the executed snapshot. No whole-current-suite, API, package, consumer or hardware +qualification is claimed. Original frame construction, wider FC combinations, +user-M, deeper repeated faults and trace/interrupt interruption remain open, +alongside the broad failed reference gates. Milestone 6 remains **in progress**, +`roadmapComplete=false`; no private CPU import or publication occurs. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index c47ccba4..78a46242 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4292,6 +4292,20 @@ scope, commands, identities and wider user-width/user-M/FC/refault/trace gaps. All 480 broader protocol cases remain untested and fail their requested gate. Milestone 6 remains **in progress**, `roadmapComplete=false`; no import or release. +### Milestone 6 checkpoint — differing nested writeback function codes, 2026-10-09 + +The shared supplied-frame fixture accepts an independent FC1/5 for each slot; +existing common-code callers preserve their inputs. Four selected tests pass +58,464 new cases and 672 byte-identical controls. All six differing FC patterns +cover all 27 width triples and user/ISP/MSP at CCR=31; canonical widths 1/2/4 +cover the remaining CCRs. Both routes check lanes, rejected bytes/slots, nested +service, DFC restoration and final return. Independent verification binds all +case keys and source/assembly/method/report identities. No CPU semantics change; +the two restored raw newline differences remain explicitly recorded. The +480-case broader gate remains untested/failing. Wider FCs, user-M, repeated +faults, original construction and trace/interrupts remain open; physical FC +spaces are not qualified. Milestone 6 stays **in progress**, `roadmapComplete=false`. + ### Milestone 6 checkpoint — remaining supplied user widths, 2026-10-09 Four selected tests pass 64,512 new cases for the eighteen remaining mixed-width From 8826c5eff5303d3c14d29b0786bfc013e77c072d Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 01:18:37 +0300 Subject: [PATCH 189/211] Record complete private read-candidate qualification --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 15 +++++-- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 42 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 13 ++++++ 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index ca08dce8..14368fe1 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -84,10 +84,17 @@ it does not replace or discard their remaining gaps. The new fixture and one-file review patch preserve the accepted decrement across explicit RTE, including read/write/refault lanes. Same-build deep, eight actual rejection guards, compiled API and clean local .80 consumers are - independently qualified. Its own full suite has started with 8,132 names and - a frozen 13,945-report / 112,894,922-case expected catalog. Full execution and - independent verification remain pending; the running 259-input audit cannot - qualify this newer change. Explicit linkage: + independently qualified. Its own full suite completes with 8,095 passed, + 37 unavailable and zero failed, all 8,132 rows and 8,100 loaded definitions. + Independent verification binds 13,945 reports / 112,894,922 cases, ten native + presets and the ordinary 86,663,530-case / 783-batch gate. Full proof: + `dd26113d245a986970cb2c59dfe63d8031e0546f58136be77057009dda7a6ef5`. + Complete linkage to its same-build deep/API/guards and same-source local .80 + consumers is + `b23e23001bd04601b926c0c13044c979566731f64b112c395d7dbb0713980c9d`. + This qualifies only the frozen 260-input / 39-CPU candidate; later production + test extensions and current mainline retain their own identities. Earlier + bounded linkage: `2e092d99a6116d1ebd0b95605b71c61d71c095d1f76c7b4ec511c95eb1db0915`. Two later 040 diagnostic strings are explicitly separated from the frozen inputs, with the same 480-case failing inventory retained. Production import diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index f1429a27..c1367f50 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13910,3 +13910,45 @@ qualification is claimed. Original frame construction, wider FC combinations, user-M, deeper repeated faults and trace/interrupt interruption remain open, alongside the broad failed reference gates. Milestone 6 remains **in progress**, `roadmapComplete=false`; no private CPU import or publication occurs. + +### Complete frozen private read-candidate qualification — 2026-10-09 + +The 260-input / 39-CPU read-recovery candidate's full execution terminates with +actual exit 0. Independent verification then completes with exit 0: 8,095 +passing rows, 37 explicitly unavailable, zero failed, all 8,132 exact named rows +and 8,100 loaded test definitions. It verifies all snapshot source hashes, +three executed assembly identities, input/reference manifests, completed TRX +counters, every report's cases/statuses/weights and its pinned reference report, +and the exact complete report set. The actual result is 13,945 reports and +112,894,922 logical cases; it is no longer only an expected catalog. + +Ten qualified native presets retain exact pinned fixture/native/adapter +identities and matching outcomes. The ordinary report gate passes 86,663,530 +semantic cases / 783 batches on this same full evidence. The 37 unavailable +rows retain their names and outcomes; broad Basic disagreements and unavailable +hardware are not turned into passing coverage. + +The complete evidence linker rechecks the prior bounded proof identities, +source/assembly equality, full input hash, selected/discovery/catalog links, +compiled API result (20 types / 211 records, zero signature changes), and +ordinary summary identity. It connects the full result to the already verified +same-build deep/reference/request guards and same-source local .80 consumers. +Package metadata retains a separate binary identity; source equality is +explicit. No reference, API, package or consumer rerun is claimed here. + +| Complete read-candidate evidence | SHA-256 | +| --- | --- | +| `PredecrementReadFixV1/full/proof.json` | `dd26113d245a986970cb2c59dfe63d8031e0546f58136be77057009dda7a6ef5` | +| Earlier bounded source/deep/API/consumer link | `2e092d99a6116d1ebd0b95605b71c61d71c095d1f76c7b4ec511c95eb1db0915` | +| `predecrement-read-complete-link-v1.json` | `b23e23001bd04601b926c0c13044c979566731f64b112c395d7dbb0713980c9d` | + +This qualifies only its frozen 260-input / 39-CPU graph. The newer supplied +mixed-width/user-return/function-code fixtures retain their own selected +production-CPU evidence. The user-M extension is still running and cannot be +qualified by this earlier full result. Current mainline, the dirty primary +checkout, physical timing/FC spaces and HD boot are not qualified here. + +The broad 010 RTE / 060 STOP reference disagreements, wider repair/refault, +original construction and trace/interrupt requirements remain open. Milestone 6 +remains **in progress**, `roadmapComplete=false`; PR #22 stays draft. No private +CPU implementation is imported and no package publication occurs. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 78a46242..edd1063f 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2079,6 +2079,19 @@ user-tail software trace-service programs, internal restoration and all other retained model/reference/consolidation requirements remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`. +### Milestone 6 checkpoint — complete private read-candidate evidence, 2026-10-09 + +The frozen 260-input / 39-CPU read-recovery candidate completes its full run +with 8,095 passed, 37 unavailable and zero failed. Independent verification +binds all 8,132 rows, 8,100 loaded definitions, 13,945 reports / 112,894,922 +logical cases, ten qualified native presets and the ordinary 86,663,530-case +/ 783-batch gate. Complete linkage connects that proof with the candidate's +same-build deep references, request guards, compiled API and same-source local +.80 consumers. Later fixture extensions and current mainline are not qualified +by this frozen run. Broad reference disagreements and restoration gaps remain +open; milestone 6 stays **in progress**, `roadmapComplete=false`. No private CPU +import or publication occurs. + ### Milestone 6 user-tail software trace service — 2026-10-06 Four new `UserTailSoftwareTraceService` scalar/batch groups compose the existing From 33e712f46f254d9e7769d961e434b5cded1dfeed Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 01:21:52 +0300 Subject: [PATCH 190/211] Qualify supplied user-M nested writeback returns --- .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- ...yntheticM68040NestedWritebackFaultTests.cs | 33 ++++++++++- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 9 +++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 56 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 13 +++++ 5 files changed, 110 insertions(+), 3 deletions(-) diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 6b5c46e8..2da63027 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -39,7 +39,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() : form == "odd-PC-chained-SR-provenance" ? "Chained format0/2/3 and format7 normal/CM SR handoff, stack consumption and header reads have separate generated WinUAE software coverage; access-frame validation/no-replay checks are synthetic. Separate composed-entry and executed-MMU-entry CM lifetime discovery profiles disagree after odd-PC fault/repair; the executed profile also exposes stacked-SR disagreement. Neither qualifies the full run loop or hardware. CT/CU/CP pending/foreign-context chains, full reference exception qualification and hardware behavior remain unqualified" : form == "writeback-handler" - ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks. Actual nested MOVES-store fault/handler/RTE coverage includes all 27 B/W/L width triples at all 32 CCRs, common FC1/5 and supplied user/ISP/MSP outer frames, with separate frozen execution evidence for each promoted slice. Differing per-slot FC1/5 has separate structural-width and canonical-CCR coverage. Original construction of all slots, user-M returns, wider FC combinations, deeper repeated faults and trace/interrupt interruption remain required. Five legal MOVE16 forms have explicit saved-line completion coverage; cache-push lines and other data-fault construction remain required. Physical function-code spaces, MOVE16 WB1 validity and hardware timing are unqualified" + ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks. Actual nested MOVES-store fault/handler/RTE coverage includes all 27 B/W/L width triples at all 32 CCRs, common FC1/5 and supplied user/user-M/ISP/MSP outer frames, with separate frozen execution evidence for each promoted slice. Differing per-slot FC1/5 has separate structural-width and canonical-CCR coverage for user/ISP/MSP. Original construction of all slots, wider FC combinations, deeper repeated faults and trace/interrupt interruption remain required. Five legal MOVE16 forms have explicit saved-line completion coverage; cache-push lines and other data-fault construction remain required. Physical function-code spaces, MOVE16 WB1 validity and hardware timing are unqualified" : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs index 2175e967..17f7d593 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs @@ -131,6 +131,34 @@ private void RemainingUserMixedWidths(bool batch) private static void Run(bool batch, int[] widths, int ccr, uint lane, int fc, string bank, int faultSlot, int faultByte) => RunFunctionCodes(batch, widths, ccr, lane, [fc, fc, fc], bank, faultSlot, faultByte); + [EnvironmentFact("COPPER68K_RUN_040_USER_M_NESTED_WRITEBACK", "require supplied user-M nested writeback returns"), Trait("Suite", "ReferenceDiscovery")] + public void UserMasterBitReturnsScalar() => UserMasterBitReturns(false); + + [EnvironmentFact("COPPER68K_RUN_040_USER_M_NESTED_WRITEBACK", "require supplied user-M nested writeback returns"), Trait("Suite", "ReferenceDiscovery")] + public void UserMasterBitReturnsBatch() => UserMasterBitReturns(true); + + private void UserMasterBitReturns(bool batch) + { + var report = new CoverageBatch("68040", "user-M-nested-writeback-fault-" + (batch ? "batch" : "scalar")); + foreach (var w1 in new[] { 1, 2, 4 }) + foreach (var w2 in new[] { 1, 2, 4 }) + foreach (var w3 in new[] { 1, 2, 4 }) + { + int[] widths = [w1, w2, w3]; + for (var ccr = 0; ccr < 32; ccr++) + for (uint lane = 0; lane < 4; lane++) + foreach (var fc in new[] { 1, 5 }) + for (var slot = 1; slot <= 3; slot++) + for (var faultByte = 0; faultByte < widths[slot - 1]; faultByte++) + { + var id = $"68040/writeback/user-M-nested-physical-fault/sizes={w1}-{w2}-{w3}/ccr={ccr:X2}/lane={lane}/FC={fc}/bank=user-M/WB={slot}/byte={faultByte}"; + try { Run(batch, widths, ccr, lane, fc, "user-M", slot, faultByte); report.Record(id, "passing", null); } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); } + } + } + report.Complete(output); + } + [EnvironmentFact("COPPER68K_RUN_040_HETEROGENEOUS_NESTED_WRITEBACK", "require differing function codes in nested writeback slots"), Trait("Suite", "ReferenceDiscovery")] public void HeterogeneousFunctionCodesScalar() => HeterogeneousFunctionCodes(false); @@ -175,10 +203,11 @@ private static void RunFunctionCodes(bool batch, int[] widths, int ccr, uint lan var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); m.Reset(ccr); - var sr = (ushort)((bank == "user" ? 0x0700 : bank == "MSP" ? 0x3700 : 0x2700) | ccr); + var userReturn = bank is "user" or "user-M"; + var sr = (ushort)((bank == "user-M" ? 0x1700 : bank == "user" ? 0x0700 : bank == "MSP" ? 0x3700 : 0x2700) | ccr); // User-frame writebacks are serviced by supervisor code on ISP. The // supplied outer SR changes mode only when the outer RTE completes. - var handlerBank = bank == "user" ? "ISP" : bank; + var handlerBank = userReturn ? "ISP" : bank; var handlerSr = (ushort)((handlerBank == "MSP" ? 0x3700 : 0x2700) | ccr); var stacks = new Dictionary { ["user"] = 0x7800, ["ISP"] = 0x4700, ["MSP"] = 0x7400 }; var frame = stacks[handlerBank] - 60; stacks[handlerBank] = frame; diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 14368fe1..0b766a68 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -155,6 +155,15 @@ it does not replace or discard their remaining gaps. This is separated structural/CCR coverage, not their full cross-product or physical function-code-space qualification. Wider FCs, user-M, repeated faults, original construction and trace/interrupts remain open. + A supplied user-M extension subsequently passes all 27 width triples, all + CCRs, common FC1/5, lanes and rejected bytes/slots: 96,768 new cases plus + 672 identical controls, four named passes. Independent proof: + `ac3d97610b813b7e31ed2364ef675f3df6988c0d2818c958e23a3c52409aaf4d`. + Nested service uses ISP and the final return selects USP while preserving + the supplied M bit. Earlier slices remain separate frozen evidence, not + execution of this updated fixture. Differing-FC user-M, wider FCs, repeated + faults, original construction and trace/interrupts remain open. The broader + 480-case gate remains untested/failing. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index c1367f50..982f4435 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -13952,3 +13952,59 @@ The broad 010 RTE / 060 STOP reference disagreements, wider repair/refault, original construction and trace/interrupt requirements remain open. Milestone 6 remains **in progress**, `roadmapComplete=false`; PR #22 stays draft. No private CPU implementation is imported and no package publication occurs. + +### Supplied user-M nested-writeback returns — 2026-10-09 + +The nested fixture now distinguishes supplied user-M (`S=0,M=1`) from user +(`S=0,M=0`) when forming the outer SR. Both run the completion handler and +nested fault service on ISP; the final outer RTE selects USP. The user-M +expectation preserves the supplied M bit in the restored SR. Existing fixed +SR and stack-selection examples in `SyntheticM68040ThrowawayTests` and the +normal supplied writeback matrix already cover that state; this slice adds +actual nested MOVES-store faults. Common-code callers and the handler program +are unchanged. No production CPU or timing-policy change is made. + +Fresh execution completes with four named passes, zero failed or skipped: +96,768 new cases, 48,384 per scalar/batch route, plus 672 unchanged controls +with byte-identical reports. All 27 B/W/L slot-width triples, all 32 CCRs, four +lanes, common FC1/5 and every pending slot/rejected byte are exercised. The +shared expectations check defined nested saved SR/PC, stack positions, pending +data, completion order, DFC/SFC restoration, final USP/ISP/MSP and following +instruction, with canaries and untouched architectural registers. + +Independent verification binds exact command/settings, all 233 raw snapshot +inputs/assets, all three executed assemblies, four loaded definitions/methods, +completed TRX counters and every case key/status/weight in all four reports. +All 37 production CPU files match the retained baseline after CRLF-to-LF only; +the two restored raw newline differences remain explicit. Previous user, +supervisor and differing-FC slices retain their own frozen source identities +and are not rerun or relabeled as results of this updated fixture. + +Reproduce with fresh directories and require four completed named passes: + +```powershell +$env:COPPER68K_RUN_040_USER_M_NESTED_WRITEBACK = '1' +$env:COPPER68K_SYNTHETIC_REPORT_DIR = [IO.Path]::GetFullPath('artifacts/user-M-nested-reports') +dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path artifacts/user-M-nested-build --filter 'FullyQualifiedName~SyntheticM68040NestedWritebackFaultTests.UserMasterBitReturns|FullyQualifiedName~SyntheticM68040NestedWritebackFaultTests.ActualHandlerStoresFaultCompleteAndResume' --logger 'trx;LogFileName=returns.trx' --results-directory artifacts/user-M-nested-results +``` + +| Selected evidence | SHA-256 | +| --- | --- | +| `UserMNestedV1/inputs.json` | `eb822b51cc0b8deaeb6d5fbf071e6fbae50dd843f50b4c828ac121f8f3d7cad5` | +| `UserMNestedV1/execution.json` | `2c15dda3ddedf00503037b1b99f12244a7a849ba8b017b1e480f559fd48aaeb5` | +| `UserMNestedIndependentV1.json` | `ac3d97610b813b7e31ed2364ef675f3df6988c0d2818c958e23a3c52409aaf4d` | +| New scalar report | `fb550fe2702f8c25717de75982134386a840300310e40d211225c9d4b69f92c6` | +| New batch report | `83a580f9e2f668ecf3ccd20539ef2cdb1c4a20ba2e68ad668f7cd8d48f5e3548` | +| Later diagnostic-only static proof | `594db36251b094efb43e15773378d458e33a5c3b3b0ee48bc0bc3bcb64ec245d` | + +The later single diagnostic literal acknowledges supplied user-M coverage but +retains all 480 required untested protocol cases, enabling condition and failing +gate. It is separate from the executed snapshot. Differing-FC user-M, wider FC +values, deeper repeated faults, original frame construction and trace/interrupt +interruption remain required. Physical FC spaces are unqualified. No whole +current-suite, hardware, API, package or consumer qualification is claimed. + +All launched audit workers are now terminal; the complete private read candidate +retains the separate full/deep/API/consumer linkage recorded above. Broad 010 +RTE / 060 STOP disagreements remain failed/unqualified. Milestone 6 remains +**in progress**, `roadmapComplete=false`; no private CPU import or publication. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index edd1063f..741c4ad5 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2092,6 +2092,19 @@ by this frozen run. Broad reference disagreements and restoration gaps remain open; milestone 6 stays **in progress**, `roadmapComplete=false`. No private CPU import or publication occurs. +### Milestone 6 checkpoint — supplied user-M nested returns, 2026-10-09 + +Four selected tests pass 96,768 new user-M cases plus 672 identical controls. +All 27 B/W/L triples, all CCRs, common FC1/5, lanes, rejected bytes/slots and +scalar/batch routes are covered. The supplied SR has S clear/M set; nested +service runs on ISP and final RTE selects USP while preserving M. Independent +verification binds every case, source, assembly, loaded method and report. +No CPU semantics change; restored raw newline differences remain explicit. +Earlier slices retain their separate source identities. The broader 480-case +gate is retained; differing-FC user-M, wider FCs, repeated faults, original +construction and trace/interrupts remain required. Milestone 6 stays +**in progress**, `roadmapComplete=false`; no private import or publication. + ### Milestone 6 user-tail software trace service — 2026-10-06 Four new `UserTailSoftwareTraceService` scalar/batch groups compose the existing From dd010679dbb33a22cc26851844b905ee76117020 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 01:25:18 +0300 Subject: [PATCH 191/211] Qualify differing FCs for supplied user-M returns --- .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- ...yntheticM68040NestedWritebackFaultTests.cs | 13 +++-- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 7 +++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 53 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 12 +++++ 5 files changed, 83 insertions(+), 4 deletions(-) diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 2da63027..f1af7daf 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -39,7 +39,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() : form == "odd-PC-chained-SR-provenance" ? "Chained format0/2/3 and format7 normal/CM SR handoff, stack consumption and header reads have separate generated WinUAE software coverage; access-frame validation/no-replay checks are synthetic. Separate composed-entry and executed-MMU-entry CM lifetime discovery profiles disagree after odd-PC fault/repair; the executed profile also exposes stacked-SR disagreement. Neither qualifies the full run loop or hardware. CT/CU/CP pending/foreign-context chains, full reference exception qualification and hardware behavior remain unqualified" : form == "writeback-handler" - ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks. Actual nested MOVES-store fault/handler/RTE coverage includes all 27 B/W/L width triples at all 32 CCRs, common FC1/5 and supplied user/user-M/ISP/MSP outer frames, with separate frozen execution evidence for each promoted slice. Differing per-slot FC1/5 has separate structural-width and canonical-CCR coverage for user/ISP/MSP. Original construction of all slots, wider FC combinations, deeper repeated faults and trace/interrupt interruption remain required. Five legal MOVE16 forms have explicit saved-line completion coverage; cache-push lines and other data-fault construction remain required. Physical function-code spaces, MOVE16 WB1 validity and hardware timing are unqualified" + ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks. Actual nested MOVES-store fault/handler/RTE coverage includes all 27 B/W/L width triples at all 32 CCRs, common FC1/5 and supplied user/user-M/ISP/MSP outer frames, with separate frozen execution evidence for each promoted slice. Differing per-slot FC1/5 has separate structural-width and canonical-CCR coverage for user/user-M/ISP/MSP. Original construction of all slots, wider FC combinations, deeper repeated faults and trace/interrupt interruption remain required. Five legal MOVE16 forms have explicit saved-line completion coverage; cache-push lines and other data-fault construction remain required. Physical function-code spaces, MOVE16 WB1 validity and hardware timing are unqualified" : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs index 17f7d593..338be9aa 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs @@ -165,15 +165,22 @@ private void UserMasterBitReturns(bool batch) [EnvironmentFact("COPPER68K_RUN_040_HETEROGENEOUS_NESTED_WRITEBACK", "require differing function codes in nested writeback slots"), Trait("Suite", "ReferenceDiscovery")] public void HeterogeneousFunctionCodesBatch() => HeterogeneousFunctionCodes(true); - private void HeterogeneousFunctionCodes(bool batch) + [EnvironmentFact("COPPER68K_RUN_040_USER_M_HETEROGENEOUS_WRITEBACK", "require differing function codes for supplied user-M nested returns"), Trait("Suite", "ReferenceDiscovery")] + public void HeterogeneousUserMasterBitScalar() => HeterogeneousFunctionCodes(false, true); + + [EnvironmentFact("COPPER68K_RUN_040_USER_M_HETEROGENEOUS_WRITEBACK", "require differing function codes for supplied user-M nested returns"), Trait("Suite", "ReferenceDiscovery")] + public void HeterogeneousUserMasterBitBatch() => HeterogeneousFunctionCodes(true, true); + + private void HeterogeneousFunctionCodes(bool batch, bool userMasterBit = false) { - var report = new CoverageBatch("68040", "heterogeneous-nested-writeback-fault-" + (batch ? "batch" : "scalar")); + var group = userMasterBit ? "user-M-heterogeneous-nested-writeback-fault-" : "heterogeneous-nested-writeback-fault-"; + var report = new CoverageBatch("68040", group + (batch ? "batch" : "scalar")); foreach (var f1 in new[] { 1, 5 }) foreach (var f2 in new[] { 1, 5 }) foreach (var f3 in new[] { 1, 5 }) { if (f1 == f2 && f2 == f3) continue; // Common FC groups retain separate coverage. - foreach (var bank in new[] { "user", "ISP", "MSP" }) + foreach (var bank in userMasterBit ? new[] { "user-M" } : new[] { "user", "ISP", "MSP" }) foreach (var w1 in new[] { 1, 2, 4 }) foreach (var w2 in new[] { 1, 2, 4 }) foreach (var w3 in new[] { 1, 2, 4 }) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 0b766a68..c02fb1d8 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -164,6 +164,13 @@ it does not replace or discard their remaining gaps. execution of this updated fixture. Differing-FC user-M, wider FCs, repeated faults, original construction and trace/interrupts remain open. The broader 480-case gate remains untested/failing. + Differing-FC user-M now has the same separated structural/CCR selection: + 19,488 new cases, 672 identical controls, four named passes, independent proof + `2f48b1f58ee95fad7d86c582bf3f09171023420d865edcfdbb8bfc50fcbd0b79`. + All six differing FC1/5 patterns cover all 27 widths at CCR=31, with widths + 1/2/4 covering all CCRs. Earlier user/ISP/MSP evidence remains separate. + Wider FCs, repeated faults, original construction and trace/interrupts remain + open; physical FC spaces and the full dimension cross-product are not claimed. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 982f4435..6875ebe4 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -14008,3 +14008,56 @@ All launched audit workers are now terminal; the complete private read candidate retains the separate full/deep/API/consumer linkage recorded above. Broad 010 RTE / 060 STOP disagreements remain failed/unqualified. Milestone 6 remains **in progress**, `roadmapComplete=false`; no private CPU import or publication. + +### Differing FC1/5 for supplied user-M nested returns — 2026-10-09 + +The shared differing-function-code generator now selects user-M independently +through two new environment facts. Its execution fixture is unchanged. All +six nonuniform FC1/5 triples cover all 27 B/W/L width triples at CCR=31; +canonical widths 1/2/4 additionally cover CCR=0..30. Both scalar/batch routes +vary four lanes, every pending slot and every rejected byte. This separates +structural and CCR dimensions; no full Cartesian product is claimed. + +Fresh execution completes with four named passes, zero failed or skipped: +19,488 new cases plus 672 retained controls whose reports are byte-identical. +Each route has 9,744 new cases: 4,536 structural and 5,208 other-CCR cases. +Nested service, faulted-slot DFC, saved SR/PC, stack selection, final USP/M-bit +restoration, pending-store order, canaries and following-instruction checks use +the previously qualified shared expectations. Earlier user/ISP/MSP differing-FC +evidence retains its own frozen inputs and is not rerun or relabeled here. + +Independent verification checks exact command/settings, all 233 raw snapshot +inputs/assets, three executed assemblies, loaded definitions/methods, completed +four-row TRX counters and every case key/status/weight. All 37 CPU files match +the retained baseline after CRLF-to-LF only; the same two restored raw newline +differences remain explicit. No CPU semantics or timing policy changes occur. + +Reproduce in fresh directories and require four completed named passes: + +```powershell +$env:COPPER68K_RUN_040_USER_M_HETEROGENEOUS_WRITEBACK = '1' +$env:COPPER68K_SYNTHETIC_REPORT_DIR = [IO.Path]::GetFullPath('artifacts/user-M-heterogeneous-reports') +dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path artifacts/user-M-heterogeneous-build --filter 'FullyQualifiedName~SyntheticM68040NestedWritebackFaultTests.HeterogeneousUserMasterBit|FullyQualifiedName~SyntheticM68040NestedWritebackFaultTests.ActualHandlerStoresFaultCompleteAndResume' --logger 'trx;LogFileName=returns.trx' --results-directory artifacts/user-M-heterogeneous-results +``` + +| Selected evidence | SHA-256 | +| --- | --- | +| `UserMHeterogeneousV1/inputs.json` | `9edbfd6d54d0ab8770147851a530c67656d82cb8814ea1cae31c754ef7c71da4` | +| `UserMHeterogeneousV1/execution.json` | `2382936e2a37e4efe934724be0cdae41e7a9b95f387102f189e6298ad173e423` | +| `UserMHeterogeneousIndependentV1.json` | `2f48b1f58ee95fad7d86c582bf3f09171023420d865edcfdbb8bfc50fcbd0b79` | +| New scalar report | `183fa6bdd19a7a00bf1093008836ebcfac80fdaa8ea3ee7c9539402e21fa21cd` | +| New batch report | `bcee5a841bd6f484257933b87cfe73b641dd949b22c9aedce9a265d82d407ec3` | +| Later diagnostic-only static proof | `910e7516df594f40362c21b861276c416075c78710338a867ac75cba6b589e7a` | + +The subsequent diagnostic literal retains all 480 required untested cases, +enabling condition and failing gate. Wider FC values, deeper repeated faults, +original frame construction and trace/interrupt interruption remain open. +Source review confirms that the present nested fixture injects exactly one +handler-store rejection; it cannot qualify another fault during its nested +handler's store. Such coverage must verify each distinct nested frame and +explicit RTE unwind while preserving one accepted final operand write. + +Physical FC spaces remain unqualified; no full-current-suite, hardware, API, +package or consumer result is claimed. The broad 010 RTE / 060 STOP disagreements +remain failed/unqualified. Milestone 6 stays **in progress**, `roadmapComplete=false`; +no private CPU import or publication occurs. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 741c4ad5..cbc5ff94 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2105,6 +2105,18 @@ gate is retained; differing-FC user-M, wider FCs, repeated faults, original construction and trace/interrupts remain required. Milestone 6 stays **in progress**, `roadmapComplete=false`; no private import or publication. +### Milestone 6 checkpoint — differing FCs for supplied user-M, 2026-10-09 + +The shared differing-FC generator now selects supplied user-M independently. +Four named tests pass 19,488 new cases plus 672 byte-identical controls. Six +differing FC1/5 patterns cover all 27 widths, lanes and rejected bytes/slots at +CCR=31; canonical widths 1/2/4 cover all CCRs on scalar/batch routes. Independent +verification binds all case and source/assembly/method/report identities. The +execution fixture and production CPU remain unchanged. Earlier bank selections +retain their own frozen evidence. Wider FCs, repeated faults, original construction +and trace/interrupts remain open; the 480-case broader gate is retained. Milestone 6 +stays **in progress**, `roadmapComplete=false`; no private import or publication. + ### Milestone 6 user-tail software trace service — 2026-10-06 Four new `UserTailSoftwareTraceService` scalar/batch groups compose the existing From 0f9138fe67cee639f4f1bcb2c0d25a43e9db281f Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 01:35:58 +0300 Subject: [PATCH 192/211] Qualify repeated nested writeback fault unwinds --- .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- ...yntheticM68040NestedWritebackFaultTests.cs | 154 +++++++++++++----- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 10 ++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 65 ++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 ++ 5 files changed, 200 insertions(+), 46 deletions(-) diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index f1af7daf..0f3a6649 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -39,7 +39,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() : form == "odd-PC-chained-SR-provenance" ? "Chained format0/2/3 and format7 normal/CM SR handoff, stack consumption and header reads have separate generated WinUAE software coverage; access-frame validation/no-replay checks are synthetic. Separate composed-entry and executed-MMU-entry CM lifetime discovery profiles disagree after odd-PC fault/repair; the executed profile also exposes stacked-SR disagreement. Neither qualifies the full run loop or hardware. CT/CU/CP pending/foreign-context chains, full reference exception qualification and hardware behavior remain unqualified" : form == "writeback-handler" - ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks. Actual nested MOVES-store fault/handler/RTE coverage includes all 27 B/W/L width triples at all 32 CCRs, common FC1/5 and supplied user/user-M/ISP/MSP outer frames, with separate frozen execution evidence for each promoted slice. Differing per-slot FC1/5 has separate structural-width and canonical-CCR coverage for user/user-M/ISP/MSP. Original construction of all slots, wider FC combinations, deeper repeated faults and trace/interrupt interruption remain required. Five legal MOVE16 forms have explicit saved-line completion coverage; cache-push lines and other data-fault construction remain required. Physical function-code spaces, MOVE16 WB1 validity and hardware timing are unqualified" + ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks. Actual nested MOVES-store fault/handler/RTE coverage includes all 27 B/W/L width triples at all 32 CCRs, common FC1/5 and supplied user/user-M/ISP/MSP outer frames, with separate frozen execution evidence for each promoted slice. Differing per-slot FC1/5 has separate structural-width and canonical-CCR coverage for all four banks. Repeated faults of the same pending store at depths two/three have separate structural-width and canonical-CCR coverage with common FC1/5 and explicit frame unwinds. Original construction of all slots, wider FC/refault combinations and trace/interrupt interruption remain required. Five legal MOVE16 forms have explicit saved-line completion coverage; cache-push lines and other data-fault construction remain required. Physical function-code spaces, MOVE16 WB1 validity and hardware timing are unqualified" : "Required fault/context protocol has no independently qualified execution fixture yet"); report.Complete(output); } diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs index 338be9aa..7be54da0 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs @@ -205,7 +205,58 @@ private void HeterogeneousFunctionCodes(bool batch, bool userMasterBit = false) report.Complete(output); } - private static void RunFunctionCodes(bool batch, int[] widths, int ccr, uint lane, int[] functionCodes, string bank, int faultSlot, int faultByte) + [EnvironmentFact("COPPER68K_RUN_040_REPEATED_NESTED_WRITEBACK", "require repeated nested handler-store faults and explicit returns"), Trait("Suite", "ReferenceDiscovery")] + public void RepeatedNestedFaultsScalar() => RepeatedNestedFaults(false); + + [EnvironmentFact("COPPER68K_RUN_040_REPEATED_NESTED_WRITEBACK", "require repeated nested handler-store faults and explicit returns"), Trait("Suite", "ReferenceDiscovery")] + public void RepeatedNestedFaultsBatch() => RepeatedNestedFaults(true); + + [Theory, Trait("Suite", "Synthetic")] + [InlineData(false)] + [InlineData(true)] + public void RepeatedNestedReferenceExamples(bool batch) + { + var report = new CoverageBatch("68040", "repeated-nested-writeback-reference-" + (batch ? "batch" : "scalar")); + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var depth in new[] { 2, 3 }) + for (var slot = 1; slot <= 3; slot++) + { + int[] widths = [1, 2, 4]; + var id = $"68040/writeback/repeated-nested-reference/depth={depth}/sizes=1-2-4/ccr=05/lane=3/FC=5/bank={bank}/WB={slot}/byte={widths[slot - 1] - 1}"; + try { RunFunctionCodes(batch, widths, 5, 3, [5, 5, 5], bank, slot, widths[slot - 1] - 1, depth); report.Record(id, "passing", null); } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); } + } + report.Complete(output); + } + + private void RepeatedNestedFaults(bool batch) + { + var report = new CoverageBatch("68040", "repeated-nested-writeback-fault-" + (batch ? "batch" : "scalar")); + foreach (var depth in new[] { 2, 3 }) + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var w1 in new[] { 1, 2, 4 }) + foreach (var w2 in new[] { 1, 2, 4 }) + foreach (var w3 in new[] { 1, 2, 4 }) + { + int[] widths = [w1, w2, w3]; + for (var ccr = 0; ccr < 32; ccr++) + { + if (ccr != 31 && (w1 != 1 || w2 != 2 || w3 != 4)) continue; + for (uint lane = 0; lane < 4; lane++) + foreach (var fc in new[] { 1, 5 }) + for (var slot = 1; slot <= 3; slot++) + for (var faultByte = 0; faultByte < widths[slot - 1]; faultByte++) + { + var id = $"68040/writeback/repeated-nested-physical-fault/depth={depth}/sizes={w1}-{w2}-{w3}/ccr={ccr:X2}/lane={lane}/FC={fc}/bank={bank}/WB={slot}/byte={faultByte}"; + try { RunFunctionCodes(batch, widths, ccr, lane, [fc, fc, fc], bank, slot, faultByte, depth); report.Record(id, "passing", null); } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); } + } + } + } + report.Complete(output); + } + + private static void RunFunctionCodes(bool batch, int[] widths, int ccr, uint lane, int[] functionCodes, string bank, int faultSlot, int faultByte, int faultDepth = 1) { var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); @@ -254,7 +305,7 @@ private static void RunFunctionCodes(bool batch, int[] widths, int ccr, uint lan e.ControlChecks["SFC"] = (s => s.SourceFunctionCode, 6); e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence); var originalD = (uint[])e.D.Clone(); var originalA = (uint[])e.A.Clone(); - var stores = new List<(uint Address, int Width, uint Value)>(); var faulted = false; var steps = 0; + var stores = new List<(uint Address, int Width, uint Value)>(); var faulted = false; var steps = 0; var faults = 0; bus.Accesses.Clear(); while (e.Pc != Next) { @@ -263,50 +314,63 @@ private static void RunFunctionCodes(bool batch, int[] widths, int ccr, uint lan if (!faulted && i.Slot == faultSlot && i.Operation.StartsWith("store-")) { faulted = true; - var savedSr = e.Sr; var resume = i.Pc + 4; - var nested = stacks[handlerBank] - 60; stacks[handlerBank] = nested; - M68040StackFixture.SetStacks(e, stacks, (ushort)(savedSr & 0x3fff)); - e.Pc = Handler; e.ExceptionVector = 2; - e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence + 1); - e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, resume); - e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, savedSr); - for (uint b = 8; b < 60; b++) e.MemoryMasks[nested + b] = 0; - Define(0, savedSr, 2); Define(2, resume, 4); Define(6, 0x7008, 2); - Define(12, (uint)(size | fc), 2); e.MemoryMasks[nested + 13] = 0x7f; - foreach (uint at in new uint[] { 14, 16 }) { Define(at, 0, 2); e.MemoryMasks[nested + at + 1] = 0x80; } - Define(18, wbStatus, 2); Define(20, addresses[faultSlot - 1], 4); Define(40, addresses[faultSlot - 1], 4); - for (var b = 0; b < width; b++) - { - var at = nested + 44 + ((addresses[faultSlot - 1] + (uint)b) & 3); - e.Memory[at] = (byte)(values[faultSlot - 1] >> (8 * (width - 1 - b))); e.MemoryMasks[at] = 255; - } - bus.Arm(addresses[faultSlot - 1] + (uint)faultByte, M68kBusAccessKind.CpuDataWrite); - Execute("nested fault entry"); - if (bus.Rejected.Count != 1) throw new InvalidOperationException("Missing unique handler-store rejection"); - for (uint b = 8; b < 60; b++) e.Memory[nested + b] = bus.Peek(nested + b); - e.MemoryMasks.Clear(); - ushort[] ns = [wbStatus, 0, 0]; uint[] na = [addresses[faultSlot - 1], 0, 0]; - uint[] nd = [m.PeekPhysical(nested + 44, 4), 0, 0]; - var nestedD = (uint[])e.D.Clone(); var nestedA = (uint[])e.A.Clone(); - var nestedStores = new List<(uint Address, int Width, uint Value)>(); var nsteps = 0; - // The reentrant handler shares code addresses with its caller. - // Seeing the resume PC inside the nested handler is not a return. - var returned = false; - while (!returned) + CompleteFaultedStore(i, 1, stores); + + void CompleteFaultedStore(SyntheticM68040WritebackProgram.Instruction faulting, int depth, + List<(uint Address, int Width, uint Value)> callerStores) { - if (++nsteps > 120) throw new InvalidOperationException("Nested handler did not terminate"); - var ni = program.Instructions.Single(x => x.Pc == e.Pc); - program.Expect(ni, m, e, nested, ns, na, nd, nestedD, nestedA, (uint)fc, stacks, nestedStores); - if (ni.Operation == "return") - { stacks[handlerBank] = nested + 60; M68040StackFixture.SetStacks(e, stacks, savedSr); e.Pc = resume; returned = true; } - Execute("nested " + ni.Operation); - } - if (!nestedStores.SequenceEqual(new[] { (addresses[faultSlot - 1], width, values[faultSlot - 1]) })) - throw new InvalidOperationException("Nested handler completed wrong pending store"); - stores.Add((addresses[faultSlot - 1], width, values[faultSlot - 1])); + faults++; + var savedSr = e.Sr; var resume = faulting.Pc + 4; + var nested = stacks[handlerBank] - 60; stacks[handlerBank] = nested; + M68040StackFixture.SetStacks(e, stacks, (ushort)(savedSr & 0x3fff)); + e.Pc = Handler; e.ExceptionVector = 2; + e.ControlChecks["exception sequence"] = (s => s.ExceptionSequence, sequence + (uint)faults); + e.ControlChecks["saved PC"] = (s => s.LastExceptionStackedProgramCounter, resume); + e.ControlChecks["saved SR"] = (s => s.LastExceptionStatusRegister, savedSr); + for (uint b = 8; b < 60; b++) e.MemoryMasks[nested + b] = 0; + Define(0, savedSr, 2); Define(2, resume, 4); Define(6, 0x7008, 2); + Define(12, (uint)(size | fc), 2); e.MemoryMasks[nested + 13] = 0x7f; + foreach (uint at in new uint[] { 14, 16 }) { Define(at, 0, 2); e.MemoryMasks[nested + at + 1] = 0x80; } + Define(18, wbStatus, 2); Define(20, addresses[faultSlot - 1], 4); Define(40, addresses[faultSlot - 1], 4); + for (var b = 0; b < width; b++) + { + var at = nested + 44 + ((addresses[faultSlot - 1] + (uint)b) & 3); + e.Memory[at] = (byte)(values[faultSlot - 1] >> (8 * (width - 1 - b))); e.MemoryMasks[at] = 255; + } + bus.Arm(addresses[faultSlot - 1] + (uint)faultByte, M68kBusAccessKind.CpuDataWrite); + Execute("nested fault entry"); + if (bus.Rejected.Count != faults || bus.Rejected[^1].Address != addresses[faultSlot - 1] || + bus.Rejected[^1].Width != width) throw new InvalidOperationException("Wrong handler-store rejection count/address/width"); + for (uint b = 8; b < 60; b++) e.Memory[nested + b] = bus.Peek(nested + b); + e.MemoryMasks.Clear(); + ushort[] ns = [wbStatus, 0, 0]; uint[] na = [addresses[faultSlot - 1], 0, 0]; + uint[] nd = [m.PeekPhysical(nested + 44, 4), 0, 0]; + var nestedD = (uint[])e.D.Clone(); var nestedA = (uint[])e.A.Clone(); + var nestedStores = new List<(uint Address, int Width, uint Value)>(); var nsteps = 0; + // The reentrant handler shares code addresses with its caller. + // Seeing the resume PC inside the nested handler is not a return. + var returned = false; + while (!returned) + { + if (++nsteps > 120) throw new InvalidOperationException("Nested handler did not terminate"); + var ni = program.Instructions.Single(x => x.Pc == e.Pc); + if (depth < faultDepth && ni.Slot == 1 && ni.Operation.StartsWith("store-")) + { + CompleteFaultedStore(ni, depth + 1, nestedStores); + continue; // Explicit nested service returns after the faulted MOVES. + } + program.Expect(ni, m, e, nested, ns, na, nd, nestedD, nestedA, (uint)fc, stacks, nestedStores); + if (ni.Operation == "return") + { stacks[handlerBank] = nested + 60; M68040StackFixture.SetStacks(e, stacks, savedSr); e.Pc = resume; returned = true; } + Execute("nested " + ni.Operation); + } + if (!nestedStores.SequenceEqual(new[] { (addresses[faultSlot - 1], width, values[faultSlot - 1]) })) + throw new InvalidOperationException("Nested handler completed wrong pending store"); + callerStores.Add((addresses[faultSlot - 1], width, values[faultSlot - 1])); - void Define(uint at, uint value, int count) - { e.Write(nested + at, value, count, m.Model); for (uint b = 0; b < count; b++) e.MemoryMasks[nested + at + b] = 255; } + void Define(uint at, uint value, int count) + { e.Write(nested + at, value, count, m.Model); for (uint b = 0; b < count; b++) e.MemoryMasks[nested + at + b] = 255; } + } } else { @@ -316,7 +380,7 @@ void Define(uint at, uint value, int count) Execute("outer " + i.Operation); } } - if (!faulted || !stores.SequenceEqual(addresses.Select((a, n) => (a, widths[n], values[n])))) + if (!faulted || faults != faultDepth || !stores.SequenceEqual(addresses.Select((a, n) => (a, widths[n], values[n])))) throw new InvalidOperationException("WB1/WB2/WB3 completion order differs"); var operandWrites = bus.Accesses.Where(a => a.Write && addresses.Contains(a.Address)) .Select(a => (a.Address, a.Width, a.Value)); diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index c02fb1d8..b95d900d 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -171,6 +171,16 @@ it does not replace or discard their remaining gaps. 1/2/4 covering all CCRs. Earlier user/ISP/MSP evidence remains separate. Wider FCs, repeated faults, original construction and trace/interrupts remain open; physical FC spaces and the full dimension cross-product are not claimed. + Repeated same-store faults at depths two and three now pass 51,968 generated + cases, 48 bounded examples and 672 identical single-fault controls in six + named tests. Independent proof: + `23a093183fe481a644ada6ee0bad839316ca0ca40edd1132c6d74cbe436ff18f`. + All four banks, common FC1/5, lanes and rejected bytes/slots cover all 27 + widths at CCR=31 and canonical widths 1/2/4 at all CCRs. Each saved frame and + explicit RTE unwind is checked; the final operand trace has one accepted + write per slot. Earlier larger single-fault matrices retain separate source + identities. Wider FC/refault combinations, original construction and + trace/interrupt interruption remain open; the 480-case broader gate remains. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 6875ebe4..4f5357b8 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -14061,3 +14061,68 @@ Physical FC spaces remain unqualified; no full-current-suite, hardware, API, package or consumer result is claimed. The broad 010 RTE / 060 STOP disagreements remain failed/unqualified. Milestone 6 stays **in progress**, `roadmapComplete=false`; no private CPU import or publication occurs. + +### Repeated nested pending-store faults and explicit unwind — 2026-10-09 + +The supplied-frame fixture factors its existing nested handler service into a +recursive test helper. At each level it injects an actual physical-map rejection +of the same pending MOVES store, checks the defined new format-7 frame, executes +the integer completion handler, and explicitly returns after the faulted MOVES. +Depths two and three exercise another rejection while servicing a prior fault. +The helper never retries the original opcode. It checks exception sequence, +saved SR/PC, frame header/SSW/WB1 pending data, rejection address/width/count, +stack selection and consumption, restored registers/DFC/SFC and following +instruction. Each frame's return restores its caller; the final operand trace +must contain exactly one accepted write per WB slot in order. + +Physical rejection is whole-request rejection when the chosen byte falls in +the requested access. It does not qualify partially accepted bus transfers, +enabled MMU/cache behavior, physical FC spaces or hardware timing. The outer +frame is supplied; its original construction remains unqualified. New ordinary +bounded cases use fixed widths 1/2/4, CCR=5, lane=3, FC5, the last byte of each +slot and all four banks at depths two/three: 24 per scalar/batch route. + +An initial smoke build rejected a test-code uint/int sequence-count expression; +the corrected build passes both original control rows, and the repeated bounded +rows pass separately. These smoke results do not qualify the generated matrix. +After formatting, a fresh isolated 233-input snapshot runs six named tests with +zero failed or skipped: 51,968 generated cases, 48 bounded cases and 672 retained +single-fault controls. Their two reports are byte-identical to the retained +production baseline. All 37 CPU files match after CRLF-to-LF only, with the +same two restored raw newline differences recorded. No CPU or timing change occurs. + +Per route, 25,984 generated cases cover all four banks (6,496 each), common +FC1/5, both depths, four lanes and all slots/rejected bytes. All 27 widths at +CCR=31 supply 12,096 cases; canonical widths 1/2/4 at CCR=0..30 supply 13,888. +Independent verification binds every key/status/weight, exact command/settings, +all raw snapshot inputs/assets, three executed DLLs, six loaded definitions/ +methods, completed TRX counters and all six coverage reports. Earlier larger +single-fault matrices retain their own sources and are not relabeled as runs +of the newly factored fixture. No full-current-suite claim is made. + +Reproduce in fresh directories and require six completed named passes: + +```powershell +$env:COPPER68K_RUN_040_REPEATED_NESTED_WRITEBACK = '1' +$env:COPPER68K_SYNTHETIC_REPORT_DIR = [IO.Path]::GetFullPath('artifacts/repeated-nested-reports') +dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path artifacts/repeated-nested-build --filter 'FullyQualifiedName~SyntheticM68040NestedWritebackFaultTests.RepeatedNested|FullyQualifiedName~SyntheticM68040NestedWritebackFaultTests.ActualHandlerStoresFaultCompleteAndResume' --logger 'trx;LogFileName=returns.trx' --results-directory artifacts/repeated-nested-results +``` + +| Selected evidence | SHA-256 | +| --- | --- | +| `RepeatedNestedV1/inputs.json` | `206536c0b91ac5e2ab0bfa90e3103ab72beab4c74aaa18acbe4c1143f35ca30e` | +| `RepeatedNestedV1/execution.json` | `264807dea8180cf7e04336d0da9a3e4ef6666ec76a47ee24cb26a69495718225` | +| `RepeatedNestedIndependentV1.json` | `23a093183fe481a644ada6ee0bad839316ca0ca40edd1132c6d74cbe436ff18f` | +| Generated scalar report | `2f1cd974f33d6556281e11adaacc9146923562ea4d866e8e1724cb51b70f6b7f` | +| Generated batch report | `fd61dcfddc89ffe429ba55e01613457ffc14599d6da63157288b2740b9077272` | +| Bounded scalar report | `094a26babdef2219c5800753a467458ff83fae2a0d4c72bbbaa082ceee204716` | +| Bounded batch report | `364634717511501b08567dd52e36e4b1a94dbd6db5a44b1942f3114480596c33` | +| Later diagnostic-only static proof | `d5d9ef50a3a65753d61adcbf74d4d6c3a8287f0c4f19528b92e676e70ad9f4ed` | + +The subsequent inventory literal acknowledges this scoped repeated-fault +coverage without changing any of the 480 untested cases, enabling condition or +failing gate. It is separate from the executed snapshot. Wider FC/refault +combinations, other repair/operand/frame fault paths, original construction and +trace/interrupt interruption remain open. Broad 010 RTE / 060 STOP disagreements +remain failed/unqualified. Milestone 6 stays **in progress**, `roadmapComplete=false`; +no private CPU import, package or consumer rerun, or publication occurs. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index cbc5ff94..b7e60e8f 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2117,6 +2117,21 @@ retain their own frozen evidence. Wider FCs, repeated faults, original construct and trace/interrupts remain open; the 480-case broader gate is retained. Milestone 6 stays **in progress**, `roadmapComplete=false`; no private import or publication. +### Milestone 6 checkpoint — repeated nested writeback faults, 2026-10-09 + +The shared supplied-frame fixture explicitly services repeated pending-store +faults at depths two and three. Six selected tests pass 51,968 generated cases, +48 bounded ordinary-CI examples and 672 byte-identical single-fault controls. +Each saved SR/PC, frame, stack consumption, pending datum, DFC and explicit +return is checked; the accepted operand-write trace remains one write per slot. +All four banks, common FC1/5 and lanes/rejected bytes/slots cover all 27 widths +at CCR=31, with canonical widths 1/2/4 at all CCRs. Independent verification +binds exact case/source/assembly/method/report identities. Earlier larger +single-fault selections retain separate frozen evidence. No CPU semantics or +timing-policy change occurs. Wider FC/refault combinations, original construction +and trace/interrupts remain open; the 480-case broader failing gate is retained. +Milestone 6 stays **in progress**, `roadmapComplete=false`; no import or release. + ### Milestone 6 user-tail software trace service — 2026-10-06 Four new `UserTailSoftwareTraceService` scalar/batch groups compose the existing From 02e6c585fb1effdb70e2c57a4e35aee0288e1bd1 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 01:44:39 +0300 Subject: [PATCH 193/211] Maintain strict nested writeback audit and replay --- Copper68k.Tests/Synthetic/README.md | 36 +++ docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 10 + docs/COPPER68K_REFERENCE_QUALIFICATION.md | 57 +++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 12 + ...opper68k-040-nested-writeback-integrity.py | 120 +++++++++ .../test-copper68k-040-nested-writebacks.py | 231 ++++++++++++++++++ 6 files changed, 466 insertions(+) create mode 100644 scripts/prove-copper68k-040-nested-writeback-integrity.py create mode 100644 scripts/test-copper68k-040-nested-writebacks.py diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index edd6b5f4..7b3d73ce 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1679,3 +1679,39 @@ checkpoint and independent verification record. Fresh MOVEA and EOR audits and strict replay qualify the new runner, without changing their semantic coverage or retiring additional tests. See the [execution-identity record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#retirement-execution-identity--2026-10-08). + +### Maintained supplied-frame nested-writeback audit + +From the CopperMod root, run the complete 040 nested-writeback selection in a +fresh directory. Python uses only its standard library; the command invokes +the existing xUnit project and public CPU factory in an isolated source copy. + +```powershell +python scripts/test-copper68k-040-nested-writebacks.py --output artifacts/040-nested-complete +python scripts/test-copper68k-040-nested-writebacks.py --validate-only --output artifacts/040-nested-complete +``` + +The complete gate requires all 18 named tests and 517,040 cases in 18 reports. +It independently enumerates all case keys/weights, requires completed passing +rows with exact loaded methods and DLL paths, and binds command/settings, +snapshot sources/assets and execution binaries. Missing/extra reports, empty +or substituted selections, mismatches, skipped tests and changed artifacts fail. +Replay requires the same producer and current source contents (CRLF-to-LF only +for source comparison). Relocated evidence retains its original executed paths +and binary identities; validation never executes its recorded command. + +For a small execution/validator check use `--selection controls`, which requires +four named tests, 720 cases and four reports. It does not qualify the complete +selection. Copied-evidence integrity checks execute no CPU: + +```powershell +python scripts/test-copper68k-040-nested-writebacks.py --selection controls --output artifacts/040-nested-controls +python scripts/prove-copper68k-040-nested-writeback-integrity.py --source artifacts/040-nested-controls --output artifacts/040-nested-integrity +``` + +The integrity command preserves original evidence, requires a relocated valid +copy to pass, and requires eight copied corruptions to reject. The fixture +covers supplied outer frames and whole-request physical-map rejections; original +construction, broader repair/refault/trace/interrupt paths, partially accepted +transfers, physical FC spaces and hardware timing remain separate. It does not +import private CPU code, publish a package or qualify the whole CPU suite. diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index b95d900d..72a167e2 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -181,6 +181,16 @@ it does not replace or discard their remaining gaps. write per slot. Earlier larger single-fault matrices retain separate source identities. Wider FC/refault combinations, original construction and trace/interrupt interruption remain open; the 480-case broader gate remains. + A maintained nested-writeback command now binds exact named selections, + independent case inventories, source/assembly/method identities and strict + replay. Its fresh four-test / 720-case control selection passes, proof + `40ccd9ec76894a82d9355bab19632ed578414be30f63e8247b8496351c40b401`. + Relocated evidence passes and eight copied corruptions reject, with original + evidence unchanged, proof + `e892970c10fbe3b654c99db8848f72e9c309406ca3f8c8fcc5b04d867e7e009f`. + The complete selection is running and still requires its own result: 18 + named tests / 517,040 expected cases / 18 reports. Expected inventory is not + execution. This supplies maintained reproduction without closing other gates. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 4f5357b8..925677a8 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -14126,3 +14126,60 @@ combinations, other repair/operand/frame fault paths, original construction and trace/interrupt interruption remain open. Broad 010 RTE / 060 STOP disagreements remain failed/unqualified. Milestone 6 stays **in progress**, `roadmapComplete=false`; no private CPU import, package or consumer rerun, or publication occurs. + +### Maintained complete nested-writeback audit and integrity controls — 2026-10-09 + +`scripts/test-copper68k-040-nested-writebacks.py` supplies a maintained isolated +execution and strict replay command for the complete current fixture. The +default selection requires all 18 exact named rows and 517,040 cases across +18 reports. Its Python expectations enumerate literal widths, CCRs, lanes, +FCs, banks, depths, slots and rejection bytes independently of production +decoder/EA/timing helpers. The smaller explicit `controls` selection requires +four exact named rows / 720 cases / four reports; it cannot qualify the default. + +The command records raw snapshot source/asset identities, source hashes after +CRLF-to-LF only for checkout comparison, exact command/settings and three +executed DLLs. Validation requires completed passing counters, exact loaded +test definitions/methods and recorded DLL paths, every combination key/weight, +positive case counts and the complete report set. Missing/extra JSON evidence, +extra/missing CPU/test DLLs, changed source/binaries/output, empty/substituted +selections, mismatches and skipped rows fail. Replay never executes the recorded +command; copied evidence can relocate while preserving its original executed +root and identities. Producer changes or source changes require new execution +or use of the corresponding source checkpoint. + +A first control run passes before the final extra-evidence/DLL checks are added; +it retains its separate producer identity and is not current-tool qualification. +Fresh V2 execution with the final command and strict replay both pass four +named rows / 720 cases / four reports. The maintained integrity command creates +separate copies, validates a relocated positive copy and rejects eight corruptions: +missing report, wrong weight, extra report, empty named selection, wrong loaded +method, wrong loaded assembly, changed DLL and changed source. Method/selection +and source controls rehash their altered metadata to reach structural/current- +source checks. Original evidence is verified unchanged. No CPU is executed by +those integrity controls. Two actual unknown-selection/missing-evidence requests +also reject before CPU execution and create no output directory. + +| Maintained bounded evidence | SHA-256 | +| --- | --- | +| Fresh `MaintainedNestedControlsV2/proof.json` | `40ccd9ec76894a82d9355bab19632ed578414be30f63e8247b8496351c40b401` | +| Eight corruptions / relocated valid / original unchanged | `e892970c10fbe3b654c99db8848f72e9c309406ca3f8c8fcc5b04d867e7e009f` | +| Two actual request rejections and expected complete inventory | `3f01fa541632edefccb594bf40841c12aabe77250c6c6726dd530a63ad0624da` | + +```powershell +python scripts/test-copper68k-040-nested-writebacks.py --selection controls --output artifacts/040-nested-controls +python scripts/test-copper68k-040-nested-writebacks.py --selection controls --validate-only --output artifacts/040-nested-controls +python scripts/prove-copper68k-040-nested-writeback-integrity.py --source artifacts/040-nested-controls --output artifacts/040-nested-integrity +python scripts/test-copper68k-040-nested-writebacks.py --output artifacts/040-nested-complete +python scripts/test-copper68k-040-nested-writebacks.py --validate-only --output artifacts/040-nested-complete +``` + +The complete selection is now executing on its own 233-input / 37-CPU source +snapshot. Its 18-row / 18-report / 517,040-case inventory is independently checked +but remains expected coverage until execution and validation complete. Earlier +bounded results are not relabeled as execution of this complete selection. +This is a fixture-class audit, not the whole CPU suite, hardware, API, package +or consumer qualification. Whole-request rejection, supplied outer frames and +other fixture boundaries remain explicit. The broader 480-case failing gate, +broad reference disagreements and remaining restoration requirements stay open. +Milestone 6 remains **in progress**, `roadmapComplete=false`; no import or release. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index b7e60e8f..0c97a485 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2132,6 +2132,18 @@ timing-policy change occurs. Wider FC/refault combinations, original constructio and trace/interrupts remain open; the 480-case broader failing gate is retained. Milestone 6 stays **in progress**, `roadmapComplete=false`; no import or release. +### Milestone 6 checkpoint — maintained nested-writeback audit, 2026-10-09 + +A maintained command executes isolated sources and independently validates +exact selections, loaded methods, sources/DLLs and architectural case keys. +Fresh execution and strict replay pass its four-test / 720-case control selection. +A relocated valid copy passes, eight copied corruptions reject, and two actual +unknown/missing-evidence requests fail before CPU execution. The complete +18-test / 517,040-case selection has started; its inventory is expected coverage, +not completed execution. Commands, caveats and evidence identities are recorded +in the qualification log and test README. No source API/CPU change or import +occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + ### Milestone 6 user-tail software trace service — 2026-10-06 Four new `UserTailSoftwareTraceService` scalar/batch groups compose the existing diff --git a/scripts/prove-copper68k-040-nested-writeback-integrity.py b/scripts/prove-copper68k-040-nested-writeback-integrity.py new file mode 100644 index 00000000..dba0b33c --- /dev/null +++ b/scripts/prove-copper68k-040-nested-writeback-integrity.py @@ -0,0 +1,120 @@ +"""Validate copied-evidence rejection controls; never execute a CPU.""" +import argparse +import importlib.util +import json +from pathlib import Path +import shutil +import subprocess +import sys +import xml.etree.ElementTree as ET + +HELPER = Path(__file__).with_name("test-copper68k-040-nested-writebacks.py") +spec = importlib.util.spec_from_file_location("nested_audit", HELPER) +audit = importlib.util.module_from_spec(spec) +spec.loader.exec_module(audit) + + +def copy_evidence(source, target): + audit.check(not target.exists(), "Use a fresh copied-evidence directory") + inputs, execution = audit.load(source / "inputs.json"), audit.load(source / "execution.json") + files = [*("source/" + n for n in inputs["sources"]), *("source/" + n for n in inputs["assets"]), + *execution["assemblies"], *execution["outputs"], "inputs.json", "execution.json", "proof.json", + *(p.name for p in source.glob("68040-*.json"))] + for name in files: + target_file = target / name + target_file.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source / name, target_file) + + +def refresh_trx(root): + execution = audit.load(root / "execution.json") + execution["outputs"]["audit.trx"] = audit.sha(root / "audit.trx") + audit.save(root / "execution.json", execution) + proof = audit.load(root / "proof.json") + proof["executionSha256"] = audit.sha(root / "execution.json") + audit.save(root / "proof.json", proof) + + +def corrupt(root, kind, report): + if kind == "missing-report": + (root / report).unlink() + elif kind == "wrong-weight": + value = audit.load(root / report) + value["combinations"][next(iter(value["combinations"]))]["passing"] = 2 + audit.save(root / report, value) + elif kind == "extra-report": + shutil.copy2(root / report, root / "68000-unexpected.json") + elif kind in ["empty-selection", "wrong-method", "wrong-assembly"]: + tree = ET.parse(root / "audit.trx") + if kind == "empty-selection": + for parent in tree.iter(): + for child in list(parent): + if child.tag == "{" + audit.NS["t"] + "}UnitTestResult": + parent.remove(child) + else: + definition = tree.find(".//t:UnitTest", audit.NS) + method = definition.find("t:TestMethod", audit.NS) + if kind == "wrong-method": + method.set("name", "UnrelatedPassingMethod") + else: + wrong = str(Path(audit.load(root / "inputs.json")["executedRoot"]) / "build/bin/Copper68k/release/Copper68k.dll") + method.set("codeBase", wrong) + definition.set("storage", wrong) + tree.write(root / "audit.trx", encoding="utf-8", xml_declaration=True) + refresh_trx(root) + elif kind == "changed-dll": + path = root / "build/bin/Copper68k.Tests/release/Copper68k.Tests.dll" + path.write_bytes(path.read_bytes() + b"copied corruption control") + elif kind == "changed-source": + name = "Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs" + path = root / "source" / name + path.write_bytes(path.read_bytes() + b"\n// copied source change\n") + inputs = audit.load(root / "inputs.json") + inputs["sources"][name], inputs["normalizedSources"][name] = audit.sha(path), audit.normalized(path) + audit.save(root / "inputs.json", inputs) + execution = audit.load(root / "execution.json") + execution["inputsSha256"] = audit.sha(root / "inputs.json") + audit.save(root / "execution.json", execution) + proof = audit.load(root / "proof.json") + proof["inputsSha256"], proof["executionSha256"] = audit.sha(root / "inputs.json"), audit.sha(root / "execution.json") + audit.save(root / "proof.json", proof) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + source, output = args.source.resolve(), args.output.resolve() + audit.check(not output.exists(), "Use a fresh integrity directory") + selection = audit.load(source / "inputs.json")["selection"] + audit.validate(source, selection) + originals = {p.relative_to(source).as_posix(): audit.sha(p) for p in source.rglob("*") if p.is_file()} + output.mkdir(parents=True) + controls = {"missing-report": "FileNotFoundError", "wrong-weight": "Wrong case keys/weights", + "extra-report": "Unexpected audit JSON evidence", "empty-selection": "Missing/extra/duplicate/failed/skipped named tests", + "wrong-method": "Wrong loaded method/assembly", "wrong-assembly": "Wrong loaded method/assembly", + "changed-dll": "Wrong/changed DLL identity", "changed-source": "Changed source:"} + records = {} + report = next(iter(audit.load(source / "proof.json")["reports"])) + for kind in ["relocated-valid", *controls]: + target = output / kind + copy_evidence(source, target) + if kind != "relocated-valid": + corrupt(target, kind, report) + command = [sys.executable, str(HELPER), "--validate-only", "--selection", selection, "--output", str(target)] + result = subprocess.run(command, text=True, capture_output=True) + text = result.stdout + result.stderr + (output / (kind + ".log")).write_text(text, encoding="utf-8") + audit.check(result.returncode == 0 if kind == "relocated-valid" else result.returncode != 0 and controls[kind] in text, + "Wrong integrity result: " + kind) + records[kind] = dict(exit=result.returncode, command=command, diagnostic=controls.get(kind), logSha256=audit.sha(output / (kind + ".log"))) + audit.check(originals == {p.relative_to(source).as_posix(): audit.sha(p) for p in source.rglob("*") if p.is_file()}, "Original evidence changed") + audit.save(output / "proof.json", dict(schema=1, sourceProofSha256=audit.sha(source / "proof.json"), + producerSha256=audit.sha(Path(__file__)), verifierSha256=audit.sha(HELPER), controls=records, + relocatedValidPasses=True, rejectedCorruptions=8, originalEvidenceUnchanged=True, cpuExecuted=False)) + print("Relocated evidence passes; eight copied corruptions reject; original evidence unchanged") + + +if __name__ == "__main__": + main() diff --git a/scripts/test-copper68k-040-nested-writebacks.py b/scripts/test-copper68k-040-nested-writebacks.py new file mode 100644 index 00000000..12bc5bdf --- /dev/null +++ b/scripts/test-copper68k-040-nested-writebacks.py @@ -0,0 +1,231 @@ +"""Execute/replay exact supplied-frame nested-writeback selections; no CPU import.""" +import argparse +import hashlib +import itertools +import json +import os +from pathlib import Path +import subprocess +import xml.etree.ElementTree as ET + +REPO = Path(__file__).resolve().parent.parent +NS = {"t": "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"} +CLASS = "Copper68k.Tests.Synthetic.SyntheticM68040NestedWritebackFaultTests." +FLAGS = ["COPPER68K_RUN_040_" + suffix for suffix in [ + "MIXED_NESTED_WRITEBACK", "NESTED_RETURN_BANKS", "USER_MIXED_NESTED_WRITEBACK", + "USER_M_NESTED_WRITEBACK", "HETEROGENEOUS_NESTED_WRITEBACK", + "USER_M_HETEROGENEOUS_WRITEBACK", "REPEATED_NESTED_WRITEBACK"]] +GROUPS = [ + ("nested-writeback-fault", "ActualHandlerStoresFaultCompleteAndResume", 336, True), + ("mixed-nested-writeback-fault", "MixedWidthsAndAllCcr", 86016, False), + ("nested-writeback-return-banks", "OtherCcrAndUserReturns", 26544, False), + ("user-mixed-nested-writeback-fault", "RemainingUserMixedWidths", 32256, False), + ("user-M-nested-writeback-fault", "UserMasterBitReturns", 48384, False), + ("heterogeneous-nested-writeback-fault", "HeterogeneousFunctionCodes", 29232, False), + ("user-M-heterogeneous-nested-writeback-fault", "HeterogeneousUserMasterBit", 9744, False), + ("repeated-nested-writeback-fault", "RepeatedNestedFaults", 25984, False), + ("repeated-nested-writeback-reference", "RepeatedNestedReferenceExamples", 24, True)] + + +def check(value, message): + if not value: + raise ValueError(message) + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def normalized(path): + return hashlib.sha256(path.read_bytes().replace(b"\r\n", b"\n")).hexdigest() + + +def load(path): + return json.loads(path.read_text(encoding="utf-8-sig")) + + +def save(path, value): + path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8", newline="\n") + + +def source_names(): + names = subprocess.check_output(["git", "ls-files", "Copper68k", "Copper68k.Tests"], cwd=REPO, text=True).splitlines() + names = sorted(n for n in names if Path(n).suffix in [".cs", ".csproj"]) + check(len(names) == len(set(names)) > 0 and any(n.startswith("Copper68k/") for n in names), "Empty/duplicate source selection") + return names + + +def groups(selection): + check(selection in ["controls", "complete"], "Unknown audit selection") + return [g for g in GROUPS if selection == "complete" or g[3]] + + +def command(root, selection): + chosen = groups(selection) + filter_value = "FullyQualifiedName~" + CLASS[:-1] if selection == "complete" else "|".join( + "FullyQualifiedName~" + CLASS + g[1] for g in chosen) + return ["dotnet", "test", str(root / "source/Copper68k.Tests/Copper68k.Tests.csproj"), "-c", "Release", + "--artifacts-path", str(root / "build"), "--filter", filter_value, + "--logger", "trx;LogFileName=audit.trx", "--results-directory", str(root)] + + +def expected_keys(group): + """Literal architectural fixture inputs; no production decoder/EA helpers.""" + for widths in itertools.product((1, 2, 4), repeat=3): + distinct = len(set(widths)) + sizes = "-".join(map(str, widths)) + if group == "nested-writeback-fault": + if distinct != 1: + continue + for lane, fc, bank, slot in itertools.product(range(4), (1, 5), ("ISP", "MSP"), (1, 2, 3)): + for byte in range(widths[slot - 1]): + yield f"68040/writeback/nested-physical-fault/size={widths[0]}/lane={lane}/FC={fc}/bank={bank}/WB={slot}/byte={byte}" + elif group in ["heterogeneous-nested-writeback-fault", "user-M-heterogeneous-nested-writeback-fault", "repeated-nested-writeback-fault"]: + repeated = group == "repeated-nested-writeback-fault" + banks = ("user", "user-M", "ISP", "MSP") if repeated else ("user-M",) if group.startswith("user-M-") else ("user", "ISP", "MSP") + codes = [(1, 1, 1), (5, 5, 5)] if repeated else [f for f in itertools.product((1, 5), repeat=3) if len(set(f)) > 1] + for bank, fcs, ccr, lane, slot in itertools.product(banks, codes, range(32), range(4), (1, 2, 3)): + if ccr != 31 and widths != (1, 2, 4): + continue + for byte in range(widths[slot - 1]): + if repeated: + for depth in (2, 3): + yield f"68040/writeback/repeated-nested-physical-fault/depth={depth}/sizes={sizes}/ccr={ccr:02X}/lane={lane}/FC={fcs[0]}/bank={bank}/WB={slot}/byte={byte}" + else: + fc_text = "-".join(map(str, fcs)) + yield f"68040/writeback/heterogeneous-nested-physical-fault/sizes={sizes}/FCs={fc_text}/ccr={ccr:02X}/lane={lane}/bank={bank}/WB={slot}/byte={byte}" + elif group == "repeated-nested-writeback-reference": + if widths != (1, 2, 4): + continue + for bank, depth, slot in itertools.product(("user", "user-M", "ISP", "MSP"), (2, 3), (1, 2, 3)): + yield f"68040/writeback/repeated-nested-reference/depth={depth}/sizes=1-2-4/ccr=05/lane=3/FC=5/bank={bank}/WB={slot}/byte={widths[slot-1]-1}" + else: + banks = ("ISP", "MSP") if group == "mixed-nested-writeback-fault" else ("user", "ISP", "MSP") if group == "nested-writeback-return-banks" else ("user-M",) if group.startswith("user-M-") else ("user",) + form = {"mixed-nested-writeback-fault": "mixed-nested-physical-fault", "nested-writeback-return-banks": "nested-return-bank", + "user-mixed-nested-writeback-fault": "user-mixed-nested-physical-fault", "user-M-nested-writeback-fault": "user-M-nested-physical-fault"}[group] + for bank, ccr, lane, fc, slot in itertools.product(banks, range(32), range(4), (1, 5), (1, 2, 3)): + if group == "mixed-nested-writeback-fault" and distinct == 1: + continue + if group == "user-mixed-nested-writeback-fault" and distinct != 2: + continue + if group == "nested-writeback-return-banks" and not (distinct == 1 or (bank == "user" and distinct == 3)): + continue + if group == "nested-writeback-return-banks" and bank != "user" and ccr == 31: + continue + for byte in range(widths[slot - 1]): + yield f"68040/writeback/{form}/sizes={sizes}/ccr={ccr:02X}/lane={lane}/FC={fc}/bank={bank}/WB={slot}/byte={byte}" + + +def execute(root, selection): + check(not root.exists(), "Use a fresh output directory; evidence is never overwritten") + names = source_names() + sources = {n: sha(REPO / n) for n in names} + assets = {"Copper68k/" + n: sha(REPO / "Copper68k" / n) for n in ["README.md", "copper68k-icon.png"]} + for name in [*sources, *assets]: + target = root / "source" / name + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes((REPO / name).read_bytes()) + settings = dict(COPPER68K_SYNTHETIC_REPORT_DIR=str(root)) + if selection == "complete": + settings.update({n: "1" for n in FLAGS}) + inputs = dict(schema=1, profile="040-nested-writebacks", selection=selection, executedRoot=str(root), + sources=sources, assets=assets, normalizedSources={n: normalized(REPO / n) for n in names}, + settings=settings, command=command(root, selection), producerSha256=sha(Path(__file__))) + save(root / "inputs.json", inputs) + environment = {k: v for k, v in os.environ.items() if not k.startswith("COPPER68K_")} + environment.update(settings) + with (root / "execution.log").open("w", encoding="utf-8") as log: + result = subprocess.run(inputs["command"], env=environment, stdout=log, stderr=subprocess.STDOUT) + execution = dict(exit=result.returncode, inputsSha256=sha(root / "inputs.json"), + outputs={n: sha(root / n) for n in ["execution.log", "audit.trx"] if (root / n).exists()}, + assemblies={f.relative_to(root).as_posix(): sha(f) for f in (root / "build/bin").rglob("Copper68k*.dll")}, + sourcesUnchanged=all(sha(root / "source" / n) == sha(REPO / n) == h for n, h in sources.items())) + save(root / "execution.json", execution) + check(result.returncode == 0, "Execution failed; inspect execution.log") + + +def validate(root, selection): + inputs, execution = load(root / "inputs.json"), load(root / "execution.json") + check(inputs["schema"] == 1 and inputs["profile"] == "040-nested-writebacks" and inputs["selection"] == selection, "Wrong audit profile/selection") + origin = Path(inputs["executedRoot"]) + check(origin.is_absolute() and inputs["command"] == command(origin, selection), "Wrong/empty command selection") + settings = dict(COPPER68K_SYNTHETIC_REPORT_DIR=str(origin)) + if selection == "complete": + settings.update({n: "1" for n in FLAGS}) + check(inputs["settings"] == settings and inputs["producerSha256"] == sha(Path(__file__)), "Wrong audit settings/producer") + check(execution["exit"] == 0 and execution["sourcesUnchanged"] and execution["inputsSha256"] == sha(root / "inputs.json"), "Incomplete/changed execution") + check(set(inputs["sources"]) == set(inputs["normalizedSources"]) == set(source_names()), "Wrong source inventory") + for name, digest in inputs["sources"].items(): + check(sha(root / "source" / name) == digest and normalized(root / "source" / name) == inputs["normalizedSources"][name] == normalized(REPO / name), "Changed source: " + name) + check(set(inputs["assets"]) == {"Copper68k/README.md", "Copper68k/copper68k-icon.png"}, "Wrong assets") + check(all(sha(root / "source" / n) == sha(REPO / n) == h for n, h in inputs["assets"].items()), "Changed assets") + required_dlls = {"build/bin/Copper68k/release/Copper68k.dll", "build/bin/Copper68k.Tests/release/Copper68k.dll", "build/bin/Copper68k.Tests/release/Copper68k.Tests.dll"} + check(set(execution["assemblies"]) == required_dlls and all(sha(root / n) == h for n, h in execution["assemblies"].items()), "Wrong/changed DLL identity") + check({p.relative_to(root).as_posix() for p in (root / "build/bin").rglob("Copper68k*.dll")} == required_dlls, "Extra/missing CPU or test DLL") + check(execution["assemblies"]["build/bin/Copper68k/release/Copper68k.dll"] == execution["assemblies"]["build/bin/Copper68k.Tests/release/Copper68k.dll"], "CPU copies differ") + check(set(execution["outputs"]) == {"execution.log", "audit.trx"} and all(sha(root / n) == h for n, h in execution["outputs"].items()), "Changed execution output") + chosen, names = groups(selection), set() + for _, method, _, theory in chosen: + names.update(CLASS + method + (f"(batch: {mode})" if theory else mode) for mode in (["False", "True"] if theory else ["Scalar", "Batch"])) + tree = ET.parse(root / "audit.trx") + rows = tree.findall(".//t:UnitTestResult", NS) + check(len(rows) == len(names) and {r.get("testName") for r in rows} == names and all(r.get("outcome") == "Passed" for r in rows), "Missing/extra/duplicate/failed/skipped named tests") + check(tree.find(".//t:ResultSummary", NS).get("outcome") == "Completed", "Incomplete test run") + counters = tree.find(".//t:Counters", NS) + check(all(int(counters.get(k, -1)) == v for k, v in dict(total=len(names), executed=len(names), passed=len(names), failed=0, notExecuted=0).items()), "Wrong test counters") + definitions = tree.findall(".//t:UnitTest", NS) + lookup = {d.get("id"): d for d in definitions} + check(len(lookup) == len(definitions) == len(names) and set(lookup) == {r.get("testId") for r in rows}, "Wrong loaded definitions") + dll = (origin / "build/bin/Copper68k.Tests/release/Copper68k.Tests.dll").resolve() + for row in rows: + definition = lookup[row.get("testId")] + method = definition.find("t:TestMethod", NS) + check(definition.get("name") == row.get("testName") and method.get("className").split(",")[0] == CLASS[:-1] and + method.get("name") == row.get("testName").split("(")[0].rsplit(".", 1)[1] and + Path(definition.get("storage")).resolve() == Path(method.get("codeBase")).resolve() == dll, "Wrong loaded method/assembly") + reports, total = {}, 0 + for group, _, count, _ in chosen: + keys = list(expected_keys(group)) + check(len(keys) == len(set(keys)) == count > 0, "Invalid independent case inventory: " + group) + for mode in ["scalar", "batch"]: + name = f"68040-{group}-{mode}.json" + report = load(root / name) + check(report["model"] == "68040" and report["group"] == group + "-" + mode and report["logicalCases"] == count and report["xunitBatches"] == 1 and + report["counts"] == dict(passing=count, mismatching=0, unsupported=0, untested=0) and not report["failures"], "Wrong/failed report: " + name) + check(set(report["combinations"]) == set(keys) and all(v == {"passing": 1} for v in report["combinations"].values()), "Wrong case keys/weights: " + name) + reports[name] = sha(root / name) + total += count + check({p.name for p in root.glob("68040-*.json")} == set(reports), "Missing/extra report selection") + metadata = {"inputs.json", "execution.json"} | ({"proof.json"} if (root / "proof.json").exists() else set()) + check({p.name for p in root.glob("*.json")} == set(reports) | metadata, "Unexpected audit JSON evidence") + check(total == (720 if selection == "controls" else 517040), "Wrong logical total") + return dict(schema=1, selection=selection, namedPasses=len(names), logicalCases=total, reports=reports, + inputsSha256=sha(root / "inputs.json"), executionSha256=sha(root / "execution.json"), + producerSha256=sha(Path(__file__)), executedRoot=str(origin), evidenceRoot=str(root), + sourceInputs=len(inputs["sources"]), productionCpuInputs=sum(n.startswith("Copper68k/") for n in inputs["sources"]), + sourceComparison="snapshot raw hashes; current checkout CRLF-to-LF only", fullCpuSuite=False, + hardwareQualified=False, productionImport=False, publication=False, roadmapComplete=False) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--selection", choices=["controls", "complete"], default="complete") + parser.add_argument("--validate-only", action="store_true") + args = parser.parse_args() + root = args.output.resolve() + if not args.validate_only: + execute(root, args.selection) + proof = validate(root, args.selection) + if args.validate_only: + existing = load(root / "proof.json") + existing["evidenceRoot"] = str(root) + check(existing == proof, "Proof differs from actual evidence") + else: + check(not (root / "proof.json").exists(), "Existing proof") + save(root / "proof.json", proof) + print(f"Qualified {args.selection}: {proof['namedPasses']} tests / {proof['logicalCases']} cases / {len(proof['reports'])} reports") + + +if __name__ == "__main__": + main() From 5a6eda38f7ea2e7648c84e19fbdd627dc631aae6 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 02:00:24 +0300 Subject: [PATCH 194/211] Prove repeated nested faults detect wrong saved PC --- Copper68k.Tests/Synthetic/README.md | 14 ++ docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 6 + docs/COPPER68K_REFERENCE_QUALIFICATION.md | 34 +++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 13 ++ ...per68k-040-nested-writeback-pc-mutation.py | 128 ++++++++++++++++++ 5 files changed, 195 insertions(+) create mode 100644 scripts/test-copper68k-040-nested-writeback-pc-mutation.py diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 7b3d73ce..6d0efc39 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1715,3 +1715,17 @@ covers supplied outer frames and whole-request physical-map rejections; original construction, broader repair/refault/trace/interrupt paths, partially accepted transfers, physical FC spaces and hardware timing remain separate. It does not import private CPU code, publish a package or qualify the whole CPU suite. + +To prove the repeated-fault fixture detects a wrong saved PC, use a qualified +control audit as the baseline and a fresh output directory: + +```powershell +python scripts/test-copper68k-040-nested-writeback-pc-mutation.py --baseline artifacts/040-nested-controls --output artifacts/040-nested-pc-mutation +``` + +The command changes one completed-MOVES saved-PC assignment in its isolated CPU +copy. It requires exactly 48 repeated-fault cases to fail on saved PC and all +672 initial-fault controls to retain byte-identical passing reports. Actual +failing xUnit execution, precise diagnostics, case inventories, loaded methods, +source and binary identities are checked before writing `mutation-proof.json`. +It rejects unrelated source changes and verifies production source is unchanged. diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 72a167e2..c53f1487 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -191,6 +191,12 @@ it does not replace or discard their remaining gaps. The complete selection is running and still requires its own result: 18 named tests / 517,040 expected cases / 18 reports. Expected inventory is not execution. This supplies maintained reproduction without closing other gates. + A separate maintained saved-PC mutation audit detects exactly 48 repeated- + fault mismatches while all 672 initial-fault controls remain byte-identical. + It binds actual failing xUnit execution, exact diagnostics, case inventories, + loaded methods and isolated source/binary identities. Proof: + `a5f7da133c9a875b6b56804318e2af22700e1da98a4965e8467d307daacf2fd4`. + Production CPU source is unchanged; this is fixture sensitivity evidence. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 925677a8..3e633b22 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -14183,3 +14183,37 @@ or consumer qualification. Whole-request rejection, supplied outer frames and other fixture boundaries remain explicit. The broader 480-case failing gate, broad reference disagreements and remaining restoration requirements stay open. Milestone 6 remains **in progress**, `roadmapComplete=false`; no import or release. + +## Nested completed-MOVES saved-PC mutation — 2026-10-09 + +The maintained `test-copper68k-040-nested-writeback-pc-mutation.py` command uses +the qualified `MaintainedNestedControlsV2` baseline and creates a fresh isolated +233-input source snapshot. It changes only the completed-MOVES saved-PC +assignment in `Copper68k/M68040Support.cs`: a prior recorded exception vector +selects the instruction-start PC instead of the correct following PC for nested +faults. The initial fault still uses the correct following PC. No production +source, fixture expectation or timing policy is edited. + +Actual xUnit execution fails exactly the two repeated-reference rows; both +initial-fault rows pass. The independent verifier requires all 48 repeated-fault +identifiers to mismatch on `nested fault entry: saved PC expected`, every case +weight and diagnostic, and 672 byte-identical initial-fault passing cases. +Source, asset, execution-output, three DLL and loaded-method identities are +bound; all current production source inputs must match the clean baseline. +The first mutation attempt used an incorrect zero-exception-counter assumption: +reset itself records vector -1 and advances that counter. Its four failed rows +are retained under `NestedSavedPcMutationV1`; the failure-roster verifier rejects +that attempt and no proof is written. The accepted V2 mutation changes its +scope without weakening expected outcomes. + +Accepted `NestedSavedPcMutationV2/mutation-proof.json` SHA-256: +`a5f7da133c9a875b6b56804318e2af22700e1da98a4965e8467d307daacf2fd4`. + +```powershell +python scripts/test-copper68k-040-nested-writeback-pc-mutation.py --baseline artifacts/040-nested-controls --output artifacts/040-nested-pc-mutation +``` + +The complete nested selection is still running with its unchanged producer and +233 source identities. This mutation proves fixture sensitivity, not hardware +qualification or completion of wider restoration/reference gates. Milestone 6 +stays **in progress**, `roadmapComplete=false`; no import or publication. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 0c97a485..21c7cf22 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4357,6 +4357,19 @@ scope, commands, identities and wider user-width/user-M/FC/refault/trace gaps. All 480 broader protocol cases remain untested and fail their requested gate. Milestone 6 remains **in progress**, `roadmapComplete=false`; no import or release. +### Milestone 6 checkpoint — nested saved-PC mutation, 2026-10-09 + +A maintained isolated mutation command proves the repeated-fault fixture detects +an incorrect completed-MOVES saved PC: exactly 48 repeated-fault examples fail +on saved PC, while 672 initial-fault controls retain byte-identical passing +reports. All 233 production test/project/CPU inputs are unchanged. The first +attempt incorrectly assumed reset left the exception counter at zero and is +retained as rejected evidence; the accepted mutation uses the prior exception +vector to affect only nested faults. No expectations are weakened. The complete +nested audit remains a separate running selection. Broader reference and +restoration requirements remain open; milestone 6 stays **in progress**, +`roadmapComplete=false`; no private CPU import or release. + ### Milestone 6 checkpoint — differing nested writeback function codes, 2026-10-09 The shared supplied-frame fixture accepts an independent FC1/5 for each slot; diff --git a/scripts/test-copper68k-040-nested-writeback-pc-mutation.py b/scripts/test-copper68k-040-nested-writeback-pc-mutation.py new file mode 100644 index 00000000..27d83ee0 --- /dev/null +++ b/scripts/test-copper68k-040-nested-writeback-pc-mutation.py @@ -0,0 +1,128 @@ +"""Prove repeated-fault fixtures reject a nested MOVES saved-PC mutation.""" +import argparse +import importlib.util +from pathlib import Path +import subprocess +import xml.etree.ElementTree as ET + +HELPER = Path(__file__).with_name("test-copper68k-040-nested-writebacks.py") +spec = importlib.util.spec_from_file_location("nested_audit", HELPER) +audit = importlib.util.module_from_spec(spec) +spec.loader.exec_module(audit) +CPU_FILE = "Copper68k/M68040Support.cs" + + +def mutate(root): + path = root / "source" / CPU_FILE + before = path.read_bytes() + start = before.index(b"else if (!State.M68040Mmu.Enabled && fault.CompletedMovesWrite &&") + end = before.index(b"else if (!State.M68040Mmu.Enabled && fault.Write && fault.MovemEffectiveAddress.HasValue", start) + region = before[start:end] + old = b"fault = fault with { StackedProgramCounter = State.ProgramCounter };" + # Reset records vector -1 and increments ExceptionSequence. Use the prior + # recorded vector to preserve first-fault behavior, not a zero-counter guess. + new = b"fault = fault with { StackedProgramCounter = State.LastExceptionVector < 0 ? State.ProgramCounter : ExecutionBoundaryProgramCounter };" + audit.check(region.count(old) == 1, "Changed/ambiguous MOVES mutation anchor") + after = before[:start] + region.replace(old, new) + before[end:] + path.write_bytes(after) + inputs = audit.load(root / "inputs.json") + inputs["sources"][CPU_FILE] = audit.sha(path) + inputs["normalizedSources"][CPU_FILE] = audit.normalized(path) + audit.save(root / "inputs.json", inputs) + return dict(source=CPU_FILE, beforeSha256=audit.sha(audit.REPO / CPU_FILE), afterSha256=audit.sha(path), + oldLiteral=old.decode(), newLiteral=new.decode(), firstFaultUnchanged=True) + + +def verify(root, baseline, mutation): + inputs, execution = audit.load(root / "inputs.json"), audit.load(root / "execution.json") + clean_inputs = audit.load(baseline / "inputs.json") + clean_proof = audit.validate(baseline, "controls") + audit.check(execution["exit"] == 1 and not execution["sourcesUnchanged"], "Expected actual failing mutant execution") + audit.check(inputs["selection"] == "controls" and inputs["command"] == audit.command(root, "controls") and + inputs["settings"] == {"COPPER68K_SYNTHETIC_REPORT_DIR": str(root)} and + inputs["producerSha256"] == audit.sha(HELPER), "Wrong mutant command/settings/producer") + audit.check(execution["inputsSha256"] == audit.sha(root / "inputs.json"), "Wrong mutant input identity") + audit.check(set(inputs["sources"]) == set(clean_inputs["sources"]) == set(audit.source_names()), "Wrong mutant source roster") + changed = [n for n in inputs["sources"] if inputs["sources"][n] != clean_inputs["sources"][n]] + audit.check(changed == [CPU_FILE], "Mutation changed unrelated source inputs") + audit.check(all(audit.sha(root / "source" / n) == h for n, h in inputs["sources"].items()), "Mutant source changed during execution") + audit.check(all(audit.normalized(root / "source" / n) == h for n, h in inputs["normalizedSources"].items()), "Mutant normalized identities differ") + audit.check(all(audit.sha(audit.REPO / n) == clean_inputs["sources"][n] for n in clean_inputs["sources"]), "Production source changed") + audit.check(inputs["assets"] == clean_inputs["assets"] and all(audit.sha(root / "source" / n) == h for n, h in inputs["assets"].items()), "Changed mutant assets") + audit.check(len(execution["assemblies"]) == 3 and all(audit.sha(root / n) == h for n, h in execution["assemblies"].items()), "Changed mutant DLLs") + audit.check(execution["assemblies"]["build/bin/Copper68k/release/Copper68k.dll"] == execution["assemblies"]["build/bin/Copper68k.Tests/release/Copper68k.dll"], "Mutant CPU copies differ") + audit.check(set(execution["outputs"]) == {"execution.log", "audit.trx"} and all(audit.sha(root / n) == h for n, h in execution["outputs"].items()), "Changed mutant execution output") + tree = ET.parse(root / "audit.trx") + rows = tree.findall(".//t:UnitTestResult", audit.NS) + expected = {audit.CLASS + method + f"(batch: {mode})": outcome for method, outcome in [ + ("ActualHandlerStoresFaultCompleteAndResume", "Passed"), ("RepeatedNestedReferenceExamples", "Failed")] + for mode in ["False", "True"]} + audit.check(len(rows) == 4 and {r.get("testName"): r.get("outcome") for r in rows} == expected, "Wrong mutation failure roster") + audit.check(tree.find(".//t:ResultSummary", audit.NS).get("outcome") == "Failed", "Wrong mutant summary") + counters = tree.find(".//t:Counters", audit.NS) + audit.check(all(int(counters.get(k, -1)) == v for k, v in dict(total=4, executed=4, passed=2, failed=2, notExecuted=0).items()), "Wrong mutant counters") + definitions = tree.findall(".//t:UnitTest", audit.NS) + lookup = {d.get("id"): d for d in definitions} + audit.check(len(lookup) == len(definitions) == 4 and set(lookup) == {r.get("testId") for r in rows}, "Wrong mutant loaded definitions") + for row in rows: + d = lookup[row.get("testId")] + m = d.find("t:TestMethod", audit.NS) + audit.check(d.get("name") == row.get("testName") and m.get("className").split(",")[0] == audit.CLASS[:-1] and + m.get("name") == row.get("testName").split("(")[0].rsplit(".", 1)[1] and + Path(d.get("storage")).resolve() == Path(m.get("codeBase")).resolve() == + (root / "build/bin/Copper68k.Tests/release/Copper68k.Tests.dll").resolve(), "Wrong mutant loaded method/assembly") + reports = {} + for mode in ["scalar", "batch"]: + control = f"68040-nested-writeback-fault-{mode}.json" + audit.check(audit.sha(root / control) == clean_proof["reports"][control], "Single-fault controls no longer byte-identical") + name = f"68040-repeated-nested-writeback-reference-{mode}.json" + report = audit.load(root / name) + keys = set(audit.expected_keys("repeated-nested-writeback-reference")) + audit.check(report["model"] == "68040" and report["group"] == "repeated-nested-writeback-reference-" + mode and report["xunitBatches"] == 1 and + report["logicalCases"] == 24 and report["counts"] == dict(passing=0, mismatching=24, unsupported=0, untested=0), "Wrong mutant case outcomes") + audit.check(set(report["combinations"]) == keys and all(v == {"mismatching": 1} for v in report["combinations"].values()), "Wrong mutation case keys") + failures = report["failures"] + audit.check(len(failures) == 24 and {f["id"] for f in failures} == keys and all(f["status"] == "mismatching" and + "nested fault entry: saved PC expected" in f["reason"] for f in failures), "Wrong mutation witness diagnostics") + reports[control], reports[name] = audit.sha(root / control), audit.sha(root / name) + audit.check({p.name for p in root.glob("68040-*.json")} == set(reports), "Unexpected mutant reports") + return dict(schema=1, cleanProofSha256=audit.sha(baseline / "proof.json"), helperSha256=audit.sha(HELPER), + producerSha256=audit.sha(Path(__file__)), mutation=mutation, inputsSha256=audit.sha(root / "inputs.json"), + executionSha256=audit.sha(root / "execution.json"), reports=reports, mismatchWitnesses=48, + unchangedSingleFaultCases=672, firstFaultControlsByteIdentical=True, productionSourceUnchanged=True, + hardwareQualified=False, productionImport=False, publication=False, roadmapComplete=False) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--baseline", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + baseline, root = args.baseline.resolve(), args.output.resolve() + audit.validate(baseline, "controls") + mutation = None + original_run = subprocess.run + + def intercepted(command, *positional, **keywords): + nonlocal mutation + if isinstance(command, list) and command and command[0] == "dotnet": + audit.check(mutation is None, "Unexpected repeated compiler/test invocation") + mutation = mutate(root) + return original_run(command, *positional, **keywords) + + subprocess.run = intercepted + try: + try: + audit.execute(root, "controls") + except ValueError as error: + audit.check(str(error) == "Execution failed; inspect execution.log", "Unexpected audit rejection") + finally: + subprocess.run = original_run + audit.check(mutation is not None, "Missing mutation execution") + proof = verify(root, baseline, mutation) + audit.save(root / "mutation-proof.json", proof) + print("Nested saved-PC mutation: 48 precise mismatches; 672 single-fault controls byte-identical") + + +if __name__ == "__main__": + main() From a96d16ac491e62989278cece55bd117973ee0f02 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 02:16:51 +0300 Subject: [PATCH 195/211] Record complete nested writeback qualification --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 11 +++-- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 40 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 16 ++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index c53f1487..a847d0ac 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -188,9 +188,14 @@ it does not replace or discard their remaining gaps. Relocated evidence passes and eight copied corruptions reject, with original evidence unchanged, proof `e892970c10fbe3b654c99db8848f72e9c309406ca3f8c8fcc5b04d867e7e009f`. - The complete selection is running and still requires its own result: 18 - named tests / 517,040 expected cases / 18 reports. Expected inventory is not - execution. This supplies maintained reproduction without closing other gates. + The complete selection now finishes with 18 passing named tests / 517,040 + passing cases / 18 reports, followed by successful strict replay. Maintained + proof: `7367fe2e3452934531cc35d3eef26e842a95db8095e2592ec54340933e064f36`. + Independent source/assembly/method/case/report linkage and comparison with + all pinned historical reports pass: + `e97c134ce8a2a59c3b70cfefed2bac672c7f97c1546925bf1f619a8bea0f5bfd`. + This qualifies the complete fixture class on its frozen 233-input / 37-CPU + snapshot, not the whole CPU suite or other restoration/reference gates. A separate maintained saved-PC mutation audit detects exactly 48 repeated- fault mismatches while all 672 initial-fault controls remain byte-identical. It binds actual failing xUnit execution, exact diagnostics, case inventories, diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 3e633b22..86b6c256 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -14217,3 +14217,43 @@ The complete nested selection is still running with its unchanged producer and 233 source identities. This mutation proves fixture sensitivity, not hardware qualification or completion of wider restoration/reference gates. Milestone 6 stays **in progress**, `roadmapComplete=false`; no import or publication. + +## Complete maintained nested-writeback audit — 2026-10-09 + +The unchanged maintained producer completes its `complete` selection in +`MaintainedNestedCompleteV1`: **18 passing named tests / 517,040 passing cases / +18 reports**, zero mismatches, unsupported cases, untested cases or skipped +rows in this selected fixture class. Actual execution exits 0; strict +`--validate-only` replay independently recomputes the same maintained proof. + +The separate full-selection verifier checks the exact 18 loaded methods and +definitions, completed counters, command/settings, all 233 current raw source/ +project inputs and assets, executed CPU/test assembly identities, every case +count/status/weight and the exact report roster. All 18 reports are byte-identical +to their pinned previously qualified slices, including retained initial-fault +controls and repeated-fault examples. All 37 production CPU inputs match the +retained frozen production baseline after CRLF-to-LF conversion only. The raw +newline differences in `M68040Support.cs` and +`M68kAdvancedTimingInterpreter.System.cs` remain recorded; no semantic CPU +change is inferred from the restored checkout. The complete fixture uses the +unchanged producer SHA-256 +`cc2a3f3ff2ba7d4938abf8d0c11f7355f9d058fda79006f920c8e0b255aacd96`. + +| Complete fixture evidence | SHA-256 | +| --- | --- | +| Maintained execution and strict replay proof | `7367fe2e3452934531cc35d3eef26e842a95db8095e2592ec54340933e064f36` | +| Independent full selection and pinned-report linkage | `e97c134ce8a2a59c3b70cfefed2bac672c7f97c1546925bf1f619a8bea0f5bfd` | + +```powershell +python scripts/test-copper68k-040-nested-writebacks.py --output artifacts/040-nested-complete +python scripts/test-copper68k-040-nested-writebacks.py --validate-only --output artifacts/040-nested-complete +``` + +This qualifies the complete supplied-frame nested fixture on its frozen current +test/production-CPU graph. It does not relabel the older whole-CPU result as an +execution of the newer test graph, qualify physical FC spaces/partially accepted +transfers or hardware timing, or close original-frame construction and broader +repair/refault/operand/trace/interrupt gaps. The broader 480-case failing gate +and failed broad software-reference gate remain. All launched audit workers +are terminal. Milestone 6 remains **in progress**, `roadmapComplete=false`; no +private CPU import, package publication or new consumer execution occurs. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 21c7cf22..c934536c 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4397,3 +4397,19 @@ all CCRs, lanes, common FC1/5 and rejected bytes/slots on scalar/batch routes. Original frame construction, user-M, heterogeneous FCs, deeper refaults and trace/interrupt qualification remain open. The 480-case broader gate is retained. Milestone 6 remains **in progress**, `roadmapComplete=false`; no import or release. + +### Milestone 6 checkpoint — complete maintained nested audit, 2026-10-09 + +The complete supplied-frame 040 nested-writeback selection finishes with all +18 named tests / 517,040 cases / 18 reports passing. Strict replay passes. +Independent verification binds all 233 source/project inputs, 37 production CPU +inputs, three executed DLLs, exact loaded methods and every report, and confirms +all reports are byte-identical to pinned earlier qualified slices. The frozen +production CPU baseline matches after CRLF-to-LF conversion only; the two raw +restoration differences remain explicit. This completes maintained execution +qualification for the fixture class, including all its single/repeated-fault +groups; it is not a newly executed whole CPU suite. The qualification record +contains exact commands and proof identities. Broader reference disagreements, +the 480-case restoration gate and original-frame/repair/trace/interrupt coverage +remain open. Milestone 6 stays **in progress**, `roadmapComplete=false`; no +private CPU import, new package or consumer execution is claimed. From d993b9c176d228a45f5be27434c9e1bc491b55d9 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 02:38:32 +0300 Subject: [PATCH 196/211] Qualify differing function codes through repeated faults --- Copper68k.Tests/Synthetic/README.md | 27 ++++++++ ...yntheticM68040HeterogeneousRefaultTests.cs | 68 +++++++++++++++++++ ...yntheticM68040NestedWritebackFaultTests.cs | 2 +- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 11 +++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 52 ++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 17 +++++ 6 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040HeterogeneousRefaultTests.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 6d0efc39..45b303c5 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1729,3 +1729,30 @@ copy. It requires exactly 48 repeated-fault cases to fail on saved PC and all failing xUnit execution, precise diagnostics, case inventories, loaded methods, source and binary identities are checked before writing `mutation-proof.json`. It rejects unrelated source changes and verifies production source is unchanged. + +### Differing function codes through repeated nested faults + +`SyntheticM68040HeterogeneousRefaultTests` reuses the existing handler/frame +fixture. Its ordinary examples execute 288 cases across both routes, six +nonuniform FC1/5 triples, four supplied stack banks and fault depths two/three. +The optional generated matrix covers all 27 B/W/L triples at CCR=31 and +canonical widths 1/2/4 at all CCRs, with all lanes and rejected bytes/slots. +This is separated structural and CCR coverage, not the full Cartesian product. + +For focused xUnit execution, set a fresh report directory and explicitly enable +the generated cases. The selection includes the 720 retained controls: + +```powershell +$env:COPPER68K_RUN_040_HETEROGENEOUS_REFAULT = '1' +$env:COPPER68K_SYNTHETIC_REPORT_DIR = 'artifacts/040-heterogeneous-refault/reports' +dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path artifacts/040-heterogeneous-refault/build --filter 'FullyQualifiedName~SyntheticM68040HeterogeneousRefaultTests|FullyQualifiedName~SyntheticM68040NestedWritebackFaultTests.ActualHandlerStoresFaultCompleteAndResume|FullyQualifiedName~SyntheticM68040NestedWritebackFaultTests.RepeatedNestedReferenceExamples' --logger 'trx;LogFileName=refault.trx' --results-directory artifacts/040-heterogeneous-refault/results +Remove-Item Env:COPPER68K_RUN_040_HETEROGENEOUS_REFAULT +Remove-Item Env:COPPER68K_SYNTHETIC_REPORT_DIR +``` + +The required result is eight passing named rows, eight reports and 156,912 +passing cases: 156,192 new and 720 retained. This xUnit invocation generates +results; independent source/binary/method/report qualification is recorded +separately in the [qualification log](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#differing-function-codes-through-repeated-nested-faults--2026-10-09). +Earlier maintained nested-audit proofs retain their frozen source checkpoints; +adding this test file does not relabel those proofs as current-tree execution. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040HeterogeneousRefaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040HeterogeneousRefaultTests.cs new file mode 100644 index 00000000..494bb1af --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040HeterogeneousRefaultTests.cs @@ -0,0 +1,68 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Reuse the independently specified handler/frame fixture. Combining differing +// slot FCs with repeated faults must preserve the faulted slot's code through +// every nested service and restore the caller's DFC on each explicit RTE. +public sealed class SyntheticM68040HeterogeneousRefaultTests(ITestOutputHelper output) +{ + private const string Enable = "COPPER68K_RUN_040_HETEROGENEOUS_REFAULT"; + private static readonly int[][] Codes = [[1, 1, 5], [1, 5, 1], [1, 5, 5], [5, 1, 1], [5, 1, 5], [5, 5, 1]]; + private static readonly string[] Banks = ["user", "user-M", "ISP", "MSP"]; + + [Theory, Trait("Suite", "Synthetic")] + [InlineData(false)] + [InlineData(true)] + public void DifferingCodesSurviveRepeatedNestedReferenceExamples(bool batch) + { + var report = new CoverageBatch("68040", "heterogeneous-refault-reference-" + (batch ? "batch" : "scalar")); + foreach (var codes in Codes) + foreach (var bank in Banks) + foreach (var depth in new[] { 2, 3 }) + for (var slot = 1; slot <= 3; slot++) + Case(report, batch, [1, 2, 4], 5, 3, codes, bank, slot, (slot == 1 ? 1 : slot == 2 ? 2 : 4) - 1, depth, true); + report.Complete(output); + } + + [EnvironmentFact(Enable, "require differing per-slot FCs through repeated nested service"), Trait("Suite", "ReferenceDiscovery")] + public void DifferingCodesThroughRepeatedNestedFaultsScalar() => Generated(false); + + [EnvironmentFact(Enable, "require differing per-slot FCs through repeated nested service"), Trait("Suite", "ReferenceDiscovery")] + public void DifferingCodesThroughRepeatedNestedFaultsBatch() => Generated(true); + + private void Generated(bool batch) + { + var report = new CoverageBatch("68040", "heterogeneous-refault-" + (batch ? "batch" : "scalar")); + foreach (var codes in Codes) + foreach (var bank in Banks) + foreach (var depth in new[] { 2, 3 }) + foreach (var w1 in new[] { 1, 2, 4 }) + foreach (var w2 in new[] { 1, 2, 4 }) + foreach (var w3 in new[] { 1, 2, 4 }) + { + int[] widths = [w1, w2, w3]; + for (var ccr = 0; ccr < 32; ccr++) + { + if (ccr != 31 && (w1 != 1 || w2 != 2 || w3 != 4)) continue; + for (uint lane = 0; lane < 4; lane++) + for (var slot = 1; slot <= 3; slot++) + for (var faultByte = 0; faultByte < widths[slot - 1]; faultByte++) + Case(report, batch, widths, ccr, lane, codes, bank, slot, faultByte, depth, false); + } + } + report.Complete(output); + } + + private static void Case(CoverageBatch report, bool batch, int[] widths, int ccr, uint lane, + int[] codes, string bank, int slot, int faultByte, int depth, bool reference) + { + var id = $"68040/writeback/heterogeneous-refault{(reference ? "-reference" : "")}/depth={depth}/sizes={string.Join('-', widths)}/FCs={string.Join('-', codes)}/ccr={ccr:X2}/lane={lane}/bank={bank}/WB={slot}/byte={faultByte}"; + try + { + SyntheticM68040NestedWritebackFaultTests.RunFunctionCodes(batch, widths, ccr, lane, codes, bank, slot, faultByte, depth); + report.Record(id, "passing", null); + } + catch (Exception ex) { report.Record(id, "mismatching", ex.ToString()); } + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs index 7be54da0..48733779 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040NestedWritebackFaultTests.cs @@ -256,7 +256,7 @@ private void RepeatedNestedFaults(bool batch) report.Complete(output); } - private static void RunFunctionCodes(bool batch, int[] widths, int ccr, uint lane, int[] functionCodes, string bank, int faultSlot, int faultByte, int faultDepth = 1) + internal static void RunFunctionCodes(bool batch, int[] widths, int ccr, uint lane, int[] functionCodes, string bank, int faultSlot, int faultByte, int faultDepth = 1) { var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index a847d0ac..472e57e5 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -202,6 +202,17 @@ it does not replace or discard their remaining gaps. loaded methods and isolated source/binary identities. Proof: `a5f7da133c9a875b6b56804318e2af22700e1da98a4965e8467d307daacf2fd4`. Production CPU source is unchanged; this is fixture sensitivity evidence. + A later 234-input snapshot qualifies differing FC1/5 triples through repeated + faults: 156,192 new cases plus 720 byte-identical controls / eight named tests. + Independent proof: + `7a3aed859b09de33ab33c039b11870243ff756471481bf180270262295e4c200`. + A wrong nested FC mutation detects 144 exact FC1 witnesses while 144 FC5 + examples and 720 controls retain their expected outcomes. Exact identifier, + diagnostic and source/assembly/method linkage proof: + `bd70e343b17541f3162a4ce3f581b56e1095a12aa9d8b283ca6407eefa2a5e73`. + The execution helper changes only from private to test-internal visibility; + all 37 production CPU inputs remain raw-identical to the preceding snapshot. + Broader original-frame/repair/trace/interrupt requirements remain open. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 86b6c256..cfa59ef3 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -14257,3 +14257,55 @@ repair/refault/operand/trace/interrupt gaps. The broader 480-case failing gate and failed broad software-reference gate remain. All launched audit workers are terminal. Milestone 6 remains **in progress**, `roadmapComplete=false`; no private CPU import, package publication or new consumer execution occurs. + +## Differing function codes through repeated nested faults — 2026-10-09 + +`SyntheticM68040HeterogeneousRefaultTests` composes the previously separate +differing-FC and repeated-fault dimensions through the shared handler/frame +fixture. Only `RunFunctionCodes` visibility changes from private to test-internal; +its execution body and expectations remain byte-identical after CRLF-to-LF +conversion. One test file is added, producing a 234-input / 37-CPU snapshot. +All production CPU inputs remain raw-identical to `MaintainedNestedCompleteV1`. + +Fresh isolated `HeterogeneousRefaultV1` execution exits 0 and passes **eight named +tests / 156,912 cases / eight reports**: **156,192 new cases** and **720 retained +controls** with byte-identical reports. The new cases comprise 155,904 generated +cases and 288 bounded ordinary examples. Six nonuniform FC1/5 triples cover all +four supplied outer banks and fault depths two/three. All 27 B/W/L triples run +at CCR=31; canonical widths 1/2/4 run at all CCRs. Both routes cover all lanes +and rejected bytes/slots. This is separated structural and CCR coverage. +Each recursive service checks the faulted slot's FC, pending datum, saved frame, +stack/DFC unwind and single accepted write, using explicit handler execution +and RTE. No automatic instruction retry is introduced. + +Independent verification enumerates every literal case identifier and weight, +checks the exact eight loaded methods/definitions, completed counters, all +source/asset/binary/output identities and precise source scope. The four +retained reports match the preceding qualified complete audit byte-for-byte. + +An isolated mutation in `HeterogeneousRefaultFcMutationV1` changes one completed- +MOVES fault assignment: after a prior exception, its recorded function code is +forced to 5. First-fault behavior and saved PC remain unchanged. Actual xUnit +execution exits 1: the two new bounded reference rows fail; all four retained +rows pass. Exactly **144 nested FC1 cases** mismatch and **144 FC5 cases** pass, +alongside **720 byte-identical passing controls**. Independent verification +derives each failure's frame address and expected/actual SSW byte from its bank, +slot and size and requires that exact `nested fault entry: Memory` diagnostic. +All six loaded methods, counts, identifiers, mutation literal, source scope, +assets, three DLLs and outputs are bound to the clean qualification. The +production checkout remains unchanged by mutation execution. + +| Evidence | SHA-256 | +| --- | --- | +| Independent clean execution, source scope and retained report linkage | `7a3aed859b09de33ab33c039b11870243ff756471481bf180270262295e4c200` | +| Independent 144 precise wrong-FC witnesses and unchanged outcomes | `bd70e343b17541f3162a4ce3f581b56e1095a12aa9d8b283ca6407eefa2a5e73` | + +The [test README](../Copper68k.Tests/Synthetic/README.md#differing-function-codes-through-repeated-nested-faults) +records focused execution. These results qualify the frozen 234-input selected +graph; the preceding maintained 233-input complete-fixture and 230-input whole- +CPU proofs retain their original checkpoints. Whole-request rejection and +supplied outer frames do not qualify physical FC spaces, partially accepted +transfers, original construction or wider repair/operand/trace/interrupt paths. +The 480-case broader failing inventory is unchanged. Both launched workers are +terminal; milestone 6 stays **in progress**, `roadmapComplete=false`; no private +CPU import, publication or new package/API/consumer execution occurs. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index c934536c..51ab448a 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -4413,3 +4413,20 @@ contains exact commands and proof identities. Broader reference disagreements, the 480-case restoration gate and original-frame/repair/trace/interrupt coverage remain open. Milestone 6 stays **in progress**, `roadmapComplete=false`; no private CPU import, new package or consumer execution is claimed. + +### Milestone 6 checkpoint — differing-FC repeated faults, 2026-10-09 + +The repeated-fault fixture now combines nonuniform per-slot FC1/5 triples with +depths two/three across user/user-M/ISP/MSP supplied outer frames. A selected +eight-test execution passes 156,192 new cases and 720 byte-identical retained +controls. All 27 widths run at CCR=31; canonical widths 1/2/4 run at all CCRs, +with all lanes and rejected bytes/slots on scalar/batch routes. Independent +verification binds every key/weight and source/assembly/method/report identity. +A wrong nested FC mutation detects 144 exact FC1 cases, preserves 144 FC5 cases +and all 720 controls, and verifies the precise frame-byte diagnostic for every +witness. The new test class reuses the unchanged execution helper, changing +only its visibility from private to test-internal. The 234-input test graph +keeps all 37 CPU inputs raw-identical. Earlier whole-class and whole-CPU proofs +retain their frozen inputs. The broader 480-case gate, original construction, +repair/operand/trace/interrupt and reference disagreements remain open. +Milestone 6 stays **in progress**, `roadmapComplete=false`; no import or release. From 65dfea87e543f79163235166d0be7ac8c2e2b569 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 02:52:31 +0300 Subject: [PATCH 197/211] Qualify completed first-read fault recovery --- Copper68k.Tests/Synthetic/README.md | 32 ++++++++++ ...ticM68040OperandReadFaultDiscoveryTests.cs | 64 +++++++++++++++++-- ...SyntheticM68040OperandReadRecoveryTests.cs | 36 +++++++++++ docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 9 +++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 48 ++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 17 +++++ 6 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040OperandReadRecoveryTests.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 45b303c5..927bb320 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1756,3 +1756,35 @@ results; independent source/binary/method/report qualification is recorded separately in the [qualification log](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#differing-function-codes-through-repeated-nested-faults--2026-10-09). Earlier maintained nested-audit proofs retain their frozen source checkpoints; adding this test file does not relabel those proofs as current-tree execution. + +### First indirect operand-read fault recovery + +`SyntheticM68040OperandReadRecoveryTests` extends the shared read-entry fixture +through executed handler RTE, completed instruction and following MOVEQ. It +covers MOVE, MOVEA, ADD, CMP, TST and one-register MOVEM on four supplied stack +banks. Ordinary examples cover 128 programs / 512 phase checks; the opt-in +matrix adds 40,960 programs / 163,840 phase checks across all CCRs, lanes and +rejected bytes. Expected arithmetic and flags use test-only mathematical +specifications; literal encoding controls remain in the existing fixture. + +Focused execution, including unchanged fault-entry and indexed-MOVEM controls: + +```powershell +$env:COPPER68K_RUN_040_OPERAND_READ_RECOVERY = '1' +$env:COPPER68K_RUN_040_OPERAND_READ_DISCOVERY = '1' +$env:COPPER68K_SYNTHETIC_REPORT_DIR = 'artifacts/040-read-recovery/reports' +dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path artifacts/040-read-recovery/build --filter 'FullyQualifiedName~SyntheticM68040OperandReadRecoveryTests|FullyQualifiedName~SyntheticM68040OperandReadFaultDiscoveryTests' --logger 'trx;LogFileName=read.trx' --results-directory artifacts/040-read-recovery/results +Remove-Item Env:COPPER68K_RUN_040_OPERAND_READ_RECOVERY +Remove-Item Env:COPPER68K_RUN_040_OPERAND_READ_DISCOVERY +Remove-Item Env:COPPER68K_SYNTHETIC_REPORT_DIR +``` + +Require nine passing named rows / six reports: 164,352 new phase checks and +122,880 retained entry cases. CCR and phase precede the report's opcode marker, +so every new phase has its own combination key. The physical map rejects one +whole first read, then allows it; no accepted operand effects precede the fault +and the completed instruction has exactly one accepted operand read. Other EAs, +later MOVEM transfers, incoming trace, actual mapping repair, enabled MMU and +physical cache/pipeline behavior remain separate. This execution command emits +results; [independent qualification](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#first-indirect-operand-read-recovery--2026-10-09) +binds source, binary, methods, inventories and retained evidence separately. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadFaultDiscoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadFaultDiscoveryTests.cs index b9dbed8e..e2210d5c 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadFaultDiscoveryTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadFaultDiscoveryTests.cs @@ -9,7 +9,7 @@ public sealed class SyntheticM68040OperandReadFaultDiscoveryTests(ITestOutputHel { internal const string Enable = "COPPER68K_RUN_040_OPERAND_READ_DISCOVERY"; private const uint Operand = 0x4200, Handler = 0xa000; - private static readonly (string Family, int Width, ushort Opcode, bool Movem)[] Instructions = + internal static readonly (string Family, int Width, ushort Opcode, bool Movem)[] Instructions = [ ("MOVE", 1, 0x1010, false), ("MOVE", 2, 0x3010, false), ("MOVE", 4, 0x2010, false), ("MOVEA", 2, 0x3050, false), ("MOVEA", 4, 0x2050, false), @@ -115,11 +115,17 @@ private void Audit(bool batch) report.Complete(output); } - private static void Case(CoverageBatch report, bool batch, + internal static void Case(CoverageBatch report, bool batch, (string Family, int Width, ushort Opcode, bool Movem) instruction, - string bank, ushort trace, int ccr, int lane, int faultByte) + string bank, ushort trace, int ccr, int lane, int faultByte, bool recover = false, string cohort = "") { var id = $"68040/{instruction.Family}/operand-read-fault/size={instruction.Width}/bank={bank}/T={trace:X4}/lane={lane}/byte={faultByte}/op={instruction.Opcode:X4}/ccr={ccr:X2}"; + if (recover) + { + if (trace != 0) throw new ArgumentException("Recovery trace epochs require separate qualification"); + id = id.Replace("operand-read-fault", "operand-read-recovery/" + cohort); + id = id.Replace($"/op={instruction.Opcode:X4}/ccr={ccr:X2}", $"/ccr={ccr:X2}/op={instruction.Opcode:X4}"); + } try { var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); @@ -131,6 +137,12 @@ private static void Case(CoverageBatch report, bool batch, m.InitializePhysical(SyntheticMachine.Code, instruction.Opcode, 2); m.InitializePhysical(SyntheticMachine.Code + 2, instruction.Movem ? 0x0001u : 0x4e71u, 2); m.InitializePhysical(SyntheticMachine.Code + 4, 0x4e71, 2); + var next = SyntheticMachine.Code + (instruction.Movem ? 4u : 2u); + if (recover) + { + m.InitializePhysical(next, 0x7e55, 2); + m.InitializePhysical(next + 2, 0x4e71, 2); + } m.InitializePhysical(8, Handler, 4); m.InitializePhysical(Handler, 0x4e73, 2); m.InitializePhysical(Handler + 2, 0x4e71, 2); @@ -179,7 +191,51 @@ private static void Case(CoverageBatch report, bool batch, for (uint n = 20; n < 24; n++) e.MemoryMasks[frame + n] = 255; bus.Arm(Operand + (uint)(lane + faultByte), M68kBusAccessKind.CpuDataRead); // Exactly one faulting attempt. Do not retry partially executed instructions. - M68040StackFixture.Step(m, e, report, id, batch); + if (!recover) { M68040StackFixture.Step(m, e, report, id, batch); return; } + string[] phases = ["fault-entry", "handler-RTE", "completed-read", "following-MOVEQ"]; + string PhaseId(string name) => id.Replace("/op=", "/phase=" + name + "/op="); + var phase = 0; + bool Step() + { + if (M68040StackFixture.Step(m, e, report, PhaseId(phases[phase++]), batch)) return true; + while (phase < phases.Length) report.Record(PhaseId(phases[phase++]), "untested", "Recovery prerequisite failed"); + return false; + } + e.ControlChecks["accepted operand reads"] = (_ => (uint)bus.Accesses.Count(a => + !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address == Operand + (uint)lane), 0); + if (!Step()) return; + // The handler executes RTE. Only the first read was rejected, with + // no committed operand effects; no automatic opcode retry is used. + pointers[exceptionBank] += 60; + M68040StackFixture.SetStacks(e, pointers, sr); + e.Pc = SyntheticMachine.Code; e.ExceptionVector = null; + if (!Step()) return; + uint value = 0; + for (var b = 0; b < instruction.Width; b++) value = (value << 8) | (uint)(0x80 + lane + b); + var mask = MoveSpecification.Mask(instruction.Width); + if (instruction.Family is "ADD" or "CMP") + { + var result = ArithmeticSpecification.Binary(e.D[0], value, instruction.Width, sr, + subtract: instruction.Family == "CMP", compare: instruction.Family == "CMP"); + if (instruction.Family == "ADD") e.D[0] = (e.D[0] & ~mask) | result.Value; + e.Sr = result.Sr; + } + else if (instruction.Family is "MOVE" or "TST") + { + if (instruction.Family == "MOVE") e.D[0] = (e.D[0] & ~mask) | value; + e.Sr = (ushort)((sr & 0xfff0) | (value == 0 ? 4 : 0) | ((value & (1u << (instruction.Width * 8 - 1))) != 0 ? 8 : 0)); + } + else if (instruction.Family == "MOVEA") + e.A[0] = unchecked((uint)ArithmeticSpecification.Signed(value, instruction.Width)); + else if (instruction.Movem) + e.D[0] = unchecked((uint)ArithmeticSpecification.Signed(value, instruction.Width)); + else throw new ArgumentException("Missing independent read semantics"); + e.Pc = next; + e.ControlChecks["accepted operand reads"] = (_ => (uint)bus.Accesses.Count(a => + !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address == Operand + (uint)lane), 1); + if (!Step()) return; + e.D[7] = 0x55; e.Pc = next + 2; e.Sr &= 0xfff0; + Step(); } catch (NotSupportedException ex) { report.Record(id, "unsupported", ex.Message); } catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadRecoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadRecoveryTests.cs new file mode 100644 index 00000000..59c3d8f9 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadRecoveryTests.cs @@ -0,0 +1,36 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// MC68040UM 8.4.6.2/7: restart a rejected first indirect operand read after +// explicit handler RTE. No earlier operand effects, trace epoch, enabled MMU, +// cache/pipeline behavior or partially accepted physical transfer is qualified. +public sealed class SyntheticM68040OperandReadRecoveryTests(ITestOutputHelper output) +{ + private const string Enable = "COPPER68K_RUN_040_OPERAND_READ_RECOVERY"; + + [Theory, Trait("Suite", "Synthetic")] + [InlineData(false)] + [InlineData(true)] + public void FirstReadFaultCompletesAfterExplicitHandlerReturn(bool batch) => Audit(batch, false); + + [EnvironmentFact(Enable, "require completed first-read fault recovery across CCRs, lanes and fault bytes"), Trait("Suite", "ReferenceDiscovery")] + public void EveryCcrLaneAndFaultByteScalar() => Audit(false, true); + + [EnvironmentFact(Enable, "require completed first-read fault recovery across CCRs, lanes and fault bytes"), Trait("Suite", "ReferenceDiscovery")] + public void EveryCcrLaneAndFaultByteBatch() => Audit(true, true); + + private void Audit(bool batch, bool generated) + { + var cohort = generated ? "matrix" : "reference"; + var report = new CoverageBatch("68040", $"operand-read-recovery-{cohort}-{(batch ? "batch" : "scalar")}"); + foreach (var instruction in SyntheticM68040OperandReadFaultDiscoveryTests.Instructions) + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var ccr in generated ? Enumerable.Range(0, 32) : [31]) + foreach (var lane in generated ? Enumerable.Range(0, 4) : [0]) + foreach (var faultByte in generated ? Enumerable.Range(0, instruction.Width) : [instruction.Width - 1]) + SyntheticM68040OperandReadFaultDiscoveryTests.Case(report, batch, instruction, bank, 0, ccr, lane, faultByte, + recover: true, cohort: cohort); + report.Complete(output); + } +} diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 472e57e5..c69cd411 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -213,6 +213,15 @@ it does not replace or discard their remaining gaps. The execution helper changes only from private to test-internal visibility; all 37 production CPU inputs remain raw-identical to the preceding snapshot. Broader original-frame/repair/trace/interrupt requirements remain open. + Normal first indirect reads now have separate completed-recovery evidence + for MOVE/MOVEA/ADD/CMP/TST and one-register MOVEM. Nine selected rows pass + 41,088 recovery programs / 164,352 phase checks, plus 122,880 byte-identical + retained entry cases and existing encoding/indexed-MOVEM controls. Independent + proof: `ae72aea3c23a2290c24cfdf34b74f4c4933bbbedfb01f81a110072b9777e9778`. + The 235-input snapshot preserves all 37 production CPU inputs. The physical + rejection is one-shot and whole-request, with no earlier operand effects; + other EAs/later transfers, incoming trace and actual mapping repair remain + open. This does not qualify the wider 480-case restoration inventory. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index cfa59ef3..31068ca7 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -14309,3 +14309,51 @@ transfers, original construction or wider repair/operand/trace/interrupt paths. The 480-case broader failing inventory is unchanged. Both launched workers are terminal; milestone 6 stays **in progress**, `roadmapComplete=false`; no private CPU import, publication or new package/API/consumer execution occurs. + +## First indirect operand-read recovery — 2026-10-09 + +`SyntheticM68040OperandReadRecoveryTests` extends the shared read-entry fixture +for the sixteen literal MOVE/MOVEA/ADD/CMP/TST/single-register-MOVEM forms. Its +expected results use independent mathematical `ArithmeticSpecification` and +test-only masks/sign extension; production decoder, EA, arithmetic and timing +helpers are not consulted. Four supplied stack banks, all 32 CCRs, four lanes +and every rejected byte cover a first indirect read. The bus denies one whole +request and then allows it. No operand transfer or register effect commits +before that rejection. Software executes the handler's RTE; completed execution +has exactly one accepted operand read and the next MOVEQ sentinel is checked. +Each program verifies fault entry, handler RTE, completed read and following +MOVEQ with the shared full architectural/memory checks. + +The bounded smoke passes three rows, including both ordinary routes and the +existing sixteen literal encoding witnesses. A preliminary full V1 run passes +nine rows, but the generic report splits at `/op=` and aggregated its trailing +CCR and phase fields. That run remains separate evidence. The corrected V2 +puts CCR and phase before the opcode marker and runs a fresh isolated snapshot. +No production CPU or expected semantic result is changed by this correction. + +`OperandReadRecoveryV2` exits 0: **nine passing named rows / six reports**, with +**41,088 recovery programs / 164,352 new phase checks** and **122,880 retained +entry cases**. The ordinary examples account for 128 programs / 512 phase checks; +the generated matrix accounts for 40,960 programs / 163,840 checks. The existing +encoding fact and both indexed MOVEM recovery controls also pass. Both retained +entry reports are byte-identical to pinned `OperandReadAcceptance` evidence. + +Independent verification enumerates all 164,352 new phase keys with weight one +and all 1,920 retained keys per route with weight 32. It checks exact nine loaded +methods/definitions, complete counters, command/settings, all source/asset/output +and three DLL identities, precise source scope and historical retained hashes. +One test file is added and only the shared read fixture changes; all 37 CPU +inputs remain raw-identical to the preceding qualified 234-input graph. The new +snapshot has 235 source/project inputs. Independent proof SHA-256: +`ae72aea3c23a2290c24cfdf34b74f4c4933bbbedfb01f81a110072b9777e9778`. + +The [test README](../Copper68k.Tests/Synthetic/README.md#first-indirect-operand-read-fault-recovery) +records focused execution. The handler performs RTE after a one-shot rejection, +not actual software mapping repair. This does not qualify other EAs, reads after +partial effects/later MOVEM transfers, incoming trace epochs, enabled MMU, +partially accepted physical requests or physical cache/pipeline behavior. +Earlier whole-class and whole-CPU graphs retain their original identities. The +480-case broader failing gate, trace and broad reference disagreements remain. +All launched workers are terminal. Milestone 6 stays **in progress**, +`roadmapComplete=false`; no CPU semantics/timing change, private import, +publication or new package/API/consumer execution occurs. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 51ab448a..e2230afd 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2132,6 +2132,23 @@ timing-policy change occurs. Wider FC/refault combinations, original constructio and trace/interrupts remain open; the 480-case broader failing gate is retained. Milestone 6 stays **in progress**, `roadmapComplete=false`; no import or release. +### Milestone 6 checkpoint — first indirect read recovery, 2026-10-09 + +The existing read-entry fixture now has an opt-in recovery path checking actual +handler RTE, completed instruction and following sentinel. Nine selected rows +pass 41,088 new recovery programs / 164,352 phase checks and 122,880 byte-identical +retained entry cases. MOVE/MOVEA/ADD/CMP/TST and one-register MOVEM cover four +banks, all CCRs, lanes and rejected bytes, with zero incoming trace and no earlier +accepted operand effects. Independent expectations reuse mathematical test-only +specifications; fixed encoding and indexed-MOVEM controls pass. CCR and phase +are explicit report keys; a preliminary passing run that aggregated those fields +retains separate evidence, followed by fresh corrected execution. All 235 inputs, +37 unchanged CPU inputs, loaded methods and report identifiers/weights are bound. +The whole-request rejection is one-shot; other EAs/later transfers, actual mapping +repair and trace/interrupt protocols remain open. The broader 480-case gate and +reference disagreements remain. Milestone 6 stays **in progress**, +`roadmapComplete=false`; no production CPU change, private import or release. + ### Milestone 6 checkpoint — maintained nested-writeback audit, 2026-10-09 A maintained command executes isolated sources and independently validates From 09b37367d3507297d0cc5e359c0f6b8f2046244d Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 03:02:18 +0300 Subject: [PATCH 198/211] Qualify executed persistent read mapping repair --- Copper68k.Tests/Synthetic/README.md | 20 +++++++ ...ticM68040OperandReadFaultDiscoveryTests.cs | 49 +++++++++++++++-- ...eticM68040OperandReadMappingRepairTests.cs | 54 +++++++++++++++++++ docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 9 ++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 49 +++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 ++++++ 6 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040OperandReadMappingRepairTests.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 927bb320..6cc6bf6e 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1788,3 +1788,23 @@ later MOVEM transfers, incoming trace, actual mapping repair, enabled MMU and physical cache/pipeline behavior remain separate. This execution command emits results; [independent qualification](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#first-indirect-operand-read-recovery--2026-10-09) binds source, binary, methods, inventories and retained evidence separately. + +### Executed persistent test-bus mapping repair + +`SyntheticM68040OperandReadMappingRepairTests` keeps the selected read denied +until the handler executes `MOVE.W #1,$4500.L`, then RTE. `$4500` is a fixture +controller register, not an Amiga device or MMU. Fixed examples require two +successive denials before enabling the map and validate the literal handler. +Every recovery program checks the single controller store, neighboring memory, +handler flags, saved-frame preservation, restored CCR and one accepted read. + +Enable `COPPER68K_RUN_040_OPERAND_READ_MAPPING_REPAIR=1` for its generated +matrix and select `SyntheticM68040OperandReadMappingRepairTests`. With the +preceding two classes and their flags also enabled, the focused selection +requires 14 passing rows / ten reports: 205,440 new phase checks for 41,088 +repair programs, plus 287,232 retained phase/entry checks. All CCRs, lanes, +rejected bytes and four banks use the same first-read scope. The mapping class +alone has five rows / four reports when its generated matrix is enabled. +The [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#persistent-test-bus-mapping-repair--2026-10-09) +binds exact execution and retained identities. Enabled MMU, hardware devices, +other EAs/later transfers and incoming trace remain separate requirements. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadFaultDiscoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadFaultDiscoveryTests.cs index e2210d5c..b1f16d18 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadFaultDiscoveryTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadFaultDiscoveryTests.cs @@ -117,7 +117,7 @@ private void Audit(bool batch) internal static void Case(CoverageBatch report, bool batch, (string Family, int Width, ushort Opcode, bool Movem) instruction, - string bank, ushort trace, int ccr, int lane, int faultByte, bool recover = false, string cohort = "") + string bank, ushort trace, int ccr, int lane, int faultByte, bool recover = false, string cohort = "", bool repairMapping = false) { var id = $"68040/{instruction.Family}/operand-read-fault/size={instruction.Width}/bank={bank}/T={trace:X4}/lane={lane}/byte={faultByte}/op={instruction.Opcode:X4}/ccr={ccr:X2}"; if (recover) @@ -128,7 +128,10 @@ internal static void Case(CoverageBatch report, bool batch, } try { - var bus = new SyntheticM68040AccessDoubleFaultTests.FaultBus(); + if (repairMapping && !recover) throw new ArgumentException("Mapping repair requires executed recovery"); + SyntheticM68040AccessDoubleFaultTests.FaultBus bus = repairMapping + ? new MappingRepairBus(Operand + (uint)(lane + faultByte)) + : new SyntheticM68040AccessDoubleFaultTests.FaultBus(); var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040"), bus); m.Reset(ccr); m.Core.State.A[0] = Operand + (uint)lane; @@ -146,6 +149,12 @@ internal static void Case(CoverageBatch report, bool batch, m.InitializePhysical(8, Handler, 4); m.InitializePhysical(Handler, 0x4e73, 2); m.InitializePhysical(Handler + 2, 0x4e71, 2); + if (repairMapping) + { + for (var n = -4; n < 8; n++) m.InitializePhysical(unchecked(MappingRepairBus.Control + (uint)n), 0x5a, 1); + m.InitializePhysical(MappingRepairBus.Control, 0, 2); + InitializeMappingRepair(m, Handler); + } foreach (var top in new uint[] { 0x4700, 0x7400 }) for (var n = -64; n < 4; n++) m.InitializePhysical(unchecked(top + (uint)n), (uint)(n ^ 0x5a), 1); m.Start(); @@ -192,7 +201,9 @@ internal static void Case(CoverageBatch report, bool batch, bus.Arm(Operand + (uint)(lane + faultByte), M68kBusAccessKind.CpuDataRead); // Exactly one faulting attempt. Do not retry partially executed instructions. if (!recover) { M68040StackFixture.Step(m, e, report, id, batch); return; } - string[] phases = ["fault-entry", "handler-RTE", "completed-read", "following-MOVEQ"]; + string[] phases = repairMapping + ? ["fault-entry", "mapping-repair", "handler-RTE", "completed-read", "following-MOVEQ"] + : ["fault-entry", "handler-RTE", "completed-read", "following-MOVEQ"]; string PhaseId(string name) => id.Replace("/op=", "/phase=" + name + "/op="); var phase = 0; bool Step() @@ -203,7 +214,17 @@ bool Step() } e.ControlChecks["accepted operand reads"] = (_ => (uint)bus.Accesses.Count(a => !a.Write && a.Kind == M68kBusAccessKind.CpuDataRead && a.Address == Operand + (uint)lane), 0); + if (repairMapping) e.ControlChecks["mapping repair stores"] = (_ => (uint)bus.Accesses.Count(a => + a.Write && a.Address == MappingRepairBus.Control), 0); if (!Step()) return; + if (repairMapping) + { + e.Write(MappingRepairBus.Control, 1, 2, m.Model); + e.Pc = Handler + 8; e.Sr &= 0xfff0; // MOVE.W #1,abs.L preserves X, clears N/Z/V/C. + e.ControlChecks["mapping repair stores"] = (_ => (uint)bus.Accesses.Count(a => + a.Write && a.Address == MappingRepairBus.Control && a.Width == 2 && a.Value == 1), 1); + if (!Step()) return; + } // The handler executes RTE. Only the first read was rejected, with // no committed operand effects; no automatic opcode retry is used. pointers[exceptionBank] += 60; @@ -240,4 +261,26 @@ bool Step() catch (NotSupportedException ex) { report.Record(id, "unsupported", ex.Message); } catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } } + + internal static void InitializeMappingRepair(SyntheticMachine m, uint handler) + { + // MOVE.W #1,$00004500.L; RTE; NOP. This register belongs to the test bus. + ushort[] words = [0x33fc, 1, 0, 0x4500, 0x4e73, 0x4e71]; + for (var n = 0; n < words.Length; n++) m.InitializePhysical(handler + (uint)n * 2, words[n], 2); + } + + internal sealed class MappingRepairBus(uint faultByte) : SyntheticM68040AccessDoubleFaultTests.FaultBus, IM68kPhysicalAddressMap + { + internal const uint Control = 0x4500; + // Reimplement the interface so the persistent mapping is used by the + // public CPU factory, even when the fixture holds the base bus type. + public new bool IsCpuPhysicalAddressMapped(uint address, int byteCount, M68kBusAccessKind kind) + { + if (kind != M68kBusAccessKind.CpuDataRead || unchecked(faultByte - address) >= byteCount) + return base.IsCpuPhysicalAddressMapped(address, byteCount, kind); + if (Peek(Control, 2) == 1) return true; + Rejected.Add((address, byteCount, kind, Accesses.Count)); + return false; + } + } } diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadMappingRepairTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadMappingRepairTests.cs new file mode 100644 index 00000000..9b2bf2ce --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040OperandReadMappingRepairTests.cs @@ -0,0 +1,54 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Persistent physical-map fixture; software must execute the controller store +// before RTE can restart the first indirect read. This is not an enabled MMU. +public sealed class SyntheticM68040OperandReadMappingRepairTests(ITestOutputHelper output) +{ + private const string Enable = "COPPER68K_RUN_040_OPERAND_READ_MAPPING_REPAIR"; + + [Theory, Trait("Suite", "Synthetic")] + [InlineData(false)] + [InlineData(true)] + public void HandlerReallyRepairsPersistentMappingBeforeReturning(bool batch) => Audit(batch, false); + + [EnvironmentFact(Enable, "require executed persistent mapping repair for all first-read CCR/lane/byte cases"), Trait("Suite", "ReferenceDiscovery")] + public void EveryCcrLaneAndFaultByteScalar() => Audit(false, true); + + [EnvironmentFact(Enable, "require executed persistent mapping repair for all first-read CCR/lane/byte cases"), Trait("Suite", "ReferenceDiscovery")] + public void EveryCcrLaneAndFaultByteBatch() => Audit(true, true); + + [Fact, Trait("Suite", "Synthetic")] + public void PersistentMappingAndLiteralRepairProgramHaveFixedReferenceExamples() + { + var bus = new SyntheticM68040OperandReadFaultDiscoveryTests.MappingRepairBus(0x4201); + IM68kPhysicalAddressMap mapping = bus; + Assert.False(mapping.IsCpuPhysicalAddressMapped(0x4200, 2, M68kBusAccessKind.CpuDataRead)); + Assert.False(mapping.IsCpuPhysicalAddressMapped(0x4200, 2, M68kBusAccessKind.CpuDataRead)); + Assert.Equal(2, bus.Rejected.Count); // A second request must remain denied. + Assert.True(mapping.IsCpuPhysicalAddressMapped(0x41fe, 2, M68kBusAccessKind.CpuDataRead)); + Assert.True(mapping.IsCpuPhysicalAddressMapped(0x4200, 2, M68kBusAccessKind.CpuInstructionFetch)); + bus.Initialize(0x4500, 1, 2); + Assert.True(mapping.IsCpuPhysicalAddressMapped(0x4200, 2, M68kBusAccessKind.CpuDataRead)); + var m = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); m.Reset(); + SyntheticM68040OperandReadFaultDiscoveryTests.InitializeMappingRepair(m, 0xa000); + ushort[] expected = [0x33fc, 1, 0, 0x4500, 0x4e73, 0x4e71]; + for (var n = 0; n < expected.Length; n++) Assert.Equal((uint)expected[n], m.PeekPhysical(0xa000 + (uint)n * 2, 2)); + } + + private void Audit(bool batch, bool generated) + { + var cohort = generated ? "mapping-matrix" : "mapping-reference"; + var report = new CoverageBatch("68040", $"operand-read-{cohort}-{(batch ? "batch" : "scalar")}"); + foreach (var instruction in SyntheticM68040OperandReadFaultDiscoveryTests.Instructions) + foreach (var bank in new[] { "user", "user-M", "ISP", "MSP" }) + foreach (var ccr in generated ? Enumerable.Range(0, 32) : [31]) + foreach (var lane in generated ? Enumerable.Range(0, 4) : [0]) + foreach (var faultByte in generated ? Enumerable.Range(0, instruction.Width) : [instruction.Width - 1]) + SyntheticM68040OperandReadFaultDiscoveryTests.Case(report, batch, instruction, bank, 0, ccr, lane, faultByte, + recover: true, cohort: cohort, repairMapping: true); + report.Complete(output); + } +} diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index c69cd411..49aee91b 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -222,6 +222,15 @@ it does not replace or discard their remaining gaps. rejection is one-shot and whole-request, with no earlier operand effects; other EAs/later transfers, incoming trace and actual mapping repair remain open. This does not qualify the wider 480-case restoration inventory. + A subsequent persistent test-bus map qualifies executed software repair: + 14 rows / ten reports, 41,088 repair programs / 205,440 new phase checks, + plus 287,232 byte-identical retained checks. The handler's literal word store + enables the fixture map before RTE and the completed operand read; each store + and saved/restored state is checked. Independent proof: + `3e3762420c06d0939afb23df4138a42e47ada188e6daa2ab45de21aa9091d607`. + The 236-input snapshot keeps all 37 CPU inputs unchanged. This is controlled + bus coverage, not enabled MMU or physical device qualification; wider + EA/partial-effect/trace and the 480-case protocols remain open. 3. **Frozen snapshot coherence is verified.** The frozen `86469f7` graph has complete full-suite, ordinary-gate and same-assembly deep/reference/guard evidence. Schema-2 EOR/MOVEA retirement Current graphs match all 230 inputs; diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 31068ca7..c316318b 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -14357,3 +14357,52 @@ Earlier whole-class and whole-CPU graphs retain their original identities. The All launched workers are terminal. Milestone 6 stays **in progress**, `roadmapComplete=false`; no CPU semantics/timing change, private import, publication or new package/API/consumer execution occurs. + +## Persistent test-bus mapping repair — 2026-10-09 + +`SyntheticM68040OperandReadMappingRepairTests` extends the shared first-read +recovery fixture with a persistent physical map. The selected read stays denied +until a word value 1 is stored at fixture controller `$4500`. This register is +test-bus state; no Amiga device or enabled MMU behavior is implied. The handler +executes literal `MOVE.W #1,$00004500.L; RTE`, and each program verifies exactly +one accepted repair store, register canaries, changed handler flags, preserved +saved frame, restored CCR/stacks, one accepted operand read and following MOVEQ. +Its mapping-interface reimplementation is checked through the same interface +the public CPU factory uses. Fixed examples require two successive denied +requests with no repair, permit unrelated accesses, then enable the mapping and +validate the handler's literal words. Those fixture examples are separate from +the CPU's executed repair qualification. + +The bounded smoke passes both scalar/batch rows and the fixed-example fact. +Fresh isolated `OperandReadMappingRepairV1` execution then exits 0 with **14 +passing named rows / ten reports**. It qualifies **41,088 repair programs / +205,440 new phase checks**: fault entry, controller store, handler RTE, completed +read and following MOVEQ. Ordinary examples cover 128 programs / 640 checks; +the optional matrix covers 40,960 programs / 204,800 checks. All CCRs, four lanes, +rejected bytes and user/user-M/ISP/MSP cover the sixteen earlier indirect forms. +The six retained reports remain byte-identical: 164,352 one-shot recovery phase +checks and 122,880 fault-entry cases. Existing literal encoding and indexed MOVEM +controls also pass. The persistent map does not expire after its first rejection. + +Independent verification enumerates every new and retained combination key and +weight, checks exact 14 loaded methods/definitions and completed counters, +command/settings, all raw source/asset/output identities, three DLLs and exact +source scope. One test file is added and only the shared read fixture changes; +all 37 CPU inputs remain raw-identical to the preceding 235-input snapshot. The +new selected graph has 236 inputs. Proof SHA-256: +`3e3762420c06d0939afb23df4138a42e47ada188e6daa2ab45de21aa9091d607`. + +For focused execution enable `COPPER68K_RUN_040_OPERAND_READ_MAPPING_REPAIR=1` +and select `SyntheticM68040OperandReadMappingRepairTests`. To reproduce the +14-row selection, also enable the preceding recovery/discovery flags and include +their two classes. Use fresh reports/build/results directories as shown in the +[test README](../Copper68k.Tests/Synthetic/README.md#executed-persistent-test-bus-mapping-repair). +Execution generates results; independent artifact qualification is distinct. + +This is software bus-map repair at a rejected first indirect read. It does not +qualify a real controller, enabled MMU, partially accepted transfers, other EAs, +later MOVEM transfers, incoming trace epochs or physical pipeline/cache timing. +Earlier graphs retain their frozen evidence. All launched workers are terminal; +the broader 480-case gate and reference disagreements remain. Milestone 6 stays +**in progress**, `roadmapComplete=false`; no production CPU semantics/timing +change, private import, publication or new package/API/consumer execution occurs. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index e2230afd..b04df281 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -2149,6 +2149,21 @@ repair and trace/interrupt protocols remain open. The broader 480-case gate and reference disagreements remain. Milestone 6 stays **in progress**, `roadmapComplete=false`; no production CPU change, private import or release. +### Milestone 6 checkpoint — executed test-bus mapping repair, 2026-10-09 + +A persistent physical-map fixture now requires the handler to execute a +controller word store before RTE and successful first-read completion. Fixed +examples prove repeated denial without repair and validate the literal program. +Fresh execution passes 14 named rows / ten reports: 41,088 new repair programs / +205,440 phase checks, plus 287,232 byte-identical retained phase/entry checks. +All CCRs, lanes, rejected bytes and four banks cover the same sixteen indirect +read forms. Independent source/assembly/method/inventory/report linkage passes; +the 236-input snapshot preserves all 37 production CPU inputs. The control +register is test-bus state, not a hardware device or enabled MMU. Other EAs, +later/partial-effect transfers, trace/interrupts and the broad reference gate +remain open; all 480 broader protocol IDs stay untested in their required gate. +Milestone 6 remains **in progress**, `roadmapComplete=false`; no import or release. + ### Milestone 6 checkpoint — maintained nested-writeback audit, 2026-10-09 A maintained command executes isolated sources and independently validates From bb3a085591c5ffafd2763cdbbe2663a8889bc03f Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 03:09:29 +0300 Subject: [PATCH 199/211] Maintain strict first-read recovery qualification --- Copper68k.Tests/Synthetic/README.md | 21 ++ docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 10 + docs/COPPER68K_REFERENCE_QUALIFICATION.md | 37 +++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 12 + ...68k-040-operand-read-recovery-integrity.py | 120 ++++++++++ ...est-copper68k-040-operand-read-recovery.py | 223 ++++++++++++++++++ 6 files changed, 423 insertions(+) create mode 100644 scripts/prove-copper68k-040-operand-read-recovery-integrity.py create mode 100644 scripts/test-copper68k-040-operand-read-recovery.py diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 6cc6bf6e..bc08b365 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1808,3 +1808,24 @@ alone has five rows / four reports when its generated matrix is enabled. The [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#persistent-test-bus-mapping-repair--2026-10-09) binds exact execution and retained identities. Enabled MMU, hardware devices, other EAs/later transfers and incoming trace remain separate requirements. + +### Maintained first-read recovery audit + +The maintained command isolates source and build outputs, enumerates the required +phase/CCR keys independently, and checks source, DLL, loaded-method and report +identities. `controls` requires eight passing tests / 1,152 checks / four reports; +`complete` requires fourteen / 492,672 / ten. Neither selection is a whole CPU +suite or hardware qualification. Existing environment flags are cleared before +the selected audit flags are supplied. + +```powershell +python scripts/test-copper68k-040-operand-read-recovery.py --selection complete --output artifacts/040-read-recovery +python scripts/test-copper68k-040-operand-read-recovery.py --selection complete --validate-only --output artifacts/040-read-recovery +python scripts/prove-copper68k-040-operand-read-recovery-integrity.py --source artifacts/040-read-recovery --output artifacts/040-read-recovery-integrity +``` + +Use a fresh directory for execution and integrity controls. Replay reads recorded +evidence without executing its recorded command; a valid relocated copy can pass. +Missing/extra reports, wrong weights, empty/skipped/failed selections, substituted +methods or assemblies, and changed sources/DLLs reject. Replay requires this +source checkpoint, with only CRLF-to-LF checkout normalization permitted. diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 49aee91b..4b910009 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -30,6 +30,16 @@ it does not replace or discard their remaining gaps. ## Completion gates and evidence status +The first-read entry/recovery/persistent test-bus repair selection now has a +maintained isolated execution/replay command. Its fresh complete selection passes +14 named tests / 492,672 checks / ten reports; controls pass eight / 1,152 / four. +All reports and raw source inputs match the earlier qualified 236-input snapshot. +Relocated evidence passes, eight copied corruptions reject, and two actual +invalid requests reject before execution. Independent linkage proof: +`ba48688f47a6d9d81114640240259f6dac37784e7c1f9ac11fdaebfe0a29d437`. +This satisfies maintained reproduction for that bounded slice. It does not clear +the broader 480-case restoration gate or reference disagreements below. + 1. **The broad software-reference gate is failed.** The retained `QualifiedBasicMove16RecoveryV1` audit reports 1,379 passing directories and two mismatching directories, with actual test exit 1. The first failures are diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index c316318b..c595839e 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -14406,3 +14406,40 @@ Earlier graphs retain their frozen evidence. All launched workers are terminal; the broader 480-case gate and reference disagreements remain. Milestone 6 stays **in progress**, `roadmapComplete=false`; no production CPU semantics/timing change, private import, publication or new package/API/consumer execution occurs. + +## Maintained first-read recovery audit — 2026-10-09 + +`scripts/test-copper68k-040-operand-read-recovery.py` now maintains isolated +execution and strict replay for the three entry/recovery/mapping classes. The +independent inventory preserves every recovery phase and CCR key; entry keys +retain their documented weight 32. Exact named tests, completed counters, loaded +methods, three DLL identities, command/settings, source/asset snapshot and report +roster/counts/statuses are required. Empty or substituted selections cannot pass. +Replay never executes a recorded command and accepts a relocated valid copy. + +Fresh `MaintainedReadRecoveryControlsV1` passes **eight named tests / 1,152 +checks / four reports**. `MaintainedReadRecoveryCompleteV1` passes **fourteen / +492,672 / ten**. Both strict replays pass. Independent verification confirms all +236 source/project inputs and 37 production CPU inputs remain raw-identical to +the earlier mapping-repair snapshot; every report is byte-identical to that +qualified evidence. This is new focused execution, not a whole-CPU run. + +The maintained integrity command accepts relocated evidence and rejects eight +copied corruptions: missing/extra report, wrong weight, empty selection, wrong +method/assembly, changed DLL and changed source. Original evidence is unchanged. +Two actual unknown-selection/missing-output requests reject before creating +output or executing a CPU. Exact commands are in the +[test README](../Copper68k.Tests/Synthetic/README.md#maintained-first-read-recovery-audit). + +| Evidence | SHA-256 | +| --- | --- | +| Maintained controls | `d4eb079ba41fb8681f3bcf31ab686d98f152cbd0b0269b0bb8fa87a8061c9d59` | +| Maintained complete selection | `9a655e6e32e40f637c2f73f99c66130a0e9fb8f476c9921b738515f267f379c5` | +| Relocation and eight corruption controls | `1c341129c56b746163f98ab1241fba8fb5a6ae6fd9fcfc5f12ab4d5f294b3c86` | +| Independent source/execution/report linkage and request guards | `ba48688f47a6d9d81114640240259f6dac37784e7c1f9ac11fdaebfe0a29d437` | + +Earlier full-suite and candidate evidence retains its frozen identities. First +indirect reads, zero incoming trace and fictional test-bus repair are the scope; +other operands, partial effects, trace/interrupts and hardware mapping remain +open. The broader 480-case gate is unchanged. Milestone 6 remains **in progress**, +`roadmapComplete=false`; no production CPU change, private import or publication. diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index b04df281..3292fefa 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1,5 +1,17 @@ # Copper68k synthetic instruction suite +### Milestone 6 checkpoint — maintained first-read audit, 2026-10-09 + +The entry/recovery/persistent test-bus mapping slice now has a maintained isolated +command with strict evidence replay and independent phase/CCR inventories. +Fresh complete execution passes fourteen named tests / 492,672 checks / ten +reports; controls pass eight / 1,152 / four. All reports and raw source inputs +match the earlier qualified 236-input snapshot. Relocated evidence passes and +eight copied corruptions reject; two invalid requests fail before execution. +Exact commands and proof identities are in the qualification log and test README. +All 37 CPU inputs remain unchanged. Milestone 6 stays **in progress**, +`roadmapComplete=false`; broader restoration/reference gaps are retained. + ## Scope and status Accepted implementation plan, 2026-10-03. Source baseline: PR #19, commit diff --git a/scripts/prove-copper68k-040-operand-read-recovery-integrity.py b/scripts/prove-copper68k-040-operand-read-recovery-integrity.py new file mode 100644 index 00000000..ad166826 --- /dev/null +++ b/scripts/prove-copper68k-040-operand-read-recovery-integrity.py @@ -0,0 +1,120 @@ +"""Validate copied-evidence rejection controls; never execute a CPU.""" +import argparse +import importlib.util +import json +from pathlib import Path +import shutil +import subprocess +import sys +import xml.etree.ElementTree as ET + +HELPER = Path(__file__).with_name("test-copper68k-040-operand-read-recovery.py") +spec = importlib.util.spec_from_file_location("read_recovery_audit", HELPER) +audit = importlib.util.module_from_spec(spec) +spec.loader.exec_module(audit) + + +def copy_evidence(source, target): + audit.check(not target.exists(), "Use a fresh copied-evidence directory") + inputs, execution = audit.load(source / "inputs.json"), audit.load(source / "execution.json") + files = [*("source/" + n for n in inputs["sources"]), *("source/" + n for n in inputs["assets"]), + *execution["assemblies"], *execution["outputs"], "inputs.json", "execution.json", "proof.json", + *(p.name for p in source.glob("68040-*.json"))] + for name in files: + target_file = target / name + target_file.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source / name, target_file) + + +def refresh_trx(root): + execution = audit.load(root / "execution.json") + execution["outputs"]["audit.trx"] = audit.sha(root / "audit.trx") + audit.save(root / "execution.json", execution) + proof = audit.load(root / "proof.json") + proof["executionSha256"] = audit.sha(root / "execution.json") + audit.save(root / "proof.json", proof) + + +def corrupt(root, kind, report): + if kind == "missing-report": + (root / report).unlink() + elif kind == "wrong-weight": + value = audit.load(root / report) + value["combinations"][next(iter(value["combinations"]))]["passing"] = 2 + audit.save(root / report, value) + elif kind == "extra-report": + shutil.copy2(root / report, root / "68000-unexpected.json") + elif kind in ["empty-selection", "wrong-method", "wrong-assembly"]: + tree = ET.parse(root / "audit.trx") + if kind == "empty-selection": + for parent in tree.iter(): + for child in list(parent): + if child.tag == "{" + audit.NS["t"] + "}UnitTestResult": + parent.remove(child) + else: + definition = tree.find(".//t:UnitTest", audit.NS) + method = definition.find("t:TestMethod", audit.NS) + if kind == "wrong-method": + method.set("name", "UnrelatedPassingMethod") + else: + wrong = str(Path(audit.load(root / "inputs.json")["executedRoot"]) / "build/bin/Copper68k/release/Copper68k.dll") + method.set("codeBase", wrong) + definition.set("storage", wrong) + tree.write(root / "audit.trx", encoding="utf-8", xml_declaration=True) + refresh_trx(root) + elif kind == "changed-dll": + path = root / "build/bin/Copper68k.Tests/release/Copper68k.Tests.dll" + path.write_bytes(path.read_bytes() + b"copied corruption control") + elif kind == "changed-source": + name = "Copper68k.Tests/Synthetic/SyntheticM68040OperandReadMappingRepairTests.cs" + path = root / "source" / name + path.write_bytes(path.read_bytes() + b"\n// copied source change\n") + inputs = audit.load(root / "inputs.json") + inputs["sources"][name], inputs["normalizedSources"][name] = audit.sha(path), audit.normalized(path) + audit.save(root / "inputs.json", inputs) + execution = audit.load(root / "execution.json") + execution["inputsSha256"] = audit.sha(root / "inputs.json") + audit.save(root / "execution.json", execution) + proof = audit.load(root / "proof.json") + proof["inputsSha256"], proof["executionSha256"] = audit.sha(root / "inputs.json"), audit.sha(root / "execution.json") + audit.save(root / "proof.json", proof) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + source, output = args.source.resolve(), args.output.resolve() + audit.check(not output.exists(), "Use a fresh integrity directory") + selection = audit.load(source / "inputs.json")["selection"] + audit.validate(source, selection) + originals = {p.relative_to(source).as_posix(): audit.sha(p) for p in source.rglob("*") if p.is_file()} + output.mkdir(parents=True) + controls = {"missing-report": "FileNotFoundError", "wrong-weight": "Wrong case keys/weights", + "extra-report": "Unexpected audit JSON evidence", "empty-selection": "Missing/extra/duplicate/failed/skipped named tests", + "wrong-method": "Wrong loaded method/assembly", "wrong-assembly": "Wrong loaded method/assembly", + "changed-dll": "Wrong/changed DLL identity", "changed-source": "Changed source:"} + records = {} + report = next(iter(audit.load(source / "proof.json")["reports"])) + for kind in ["relocated-valid", *controls]: + target = output / kind + copy_evidence(source, target) + if kind != "relocated-valid": + corrupt(target, kind, report) + command = [sys.executable, str(HELPER), "--validate-only", "--selection", selection, "--output", str(target)] + result = subprocess.run(command, text=True, capture_output=True) + text = result.stdout + result.stderr + (output / (kind + ".log")).write_text(text, encoding="utf-8") + audit.check(result.returncode == 0 if kind == "relocated-valid" else result.returncode != 0 and controls[kind] in text, + "Wrong integrity result: " + kind) + records[kind] = dict(exit=result.returncode, command=command, diagnostic=controls.get(kind), logSha256=audit.sha(output / (kind + ".log"))) + audit.check(originals == {p.relative_to(source).as_posix(): audit.sha(p) for p in source.rglob("*") if p.is_file()}, "Original evidence changed") + audit.save(output / "proof.json", dict(schema=1, sourceProofSha256=audit.sha(source / "proof.json"), + producerSha256=audit.sha(Path(__file__)), verifierSha256=audit.sha(HELPER), controls=records, + relocatedValidPasses=True, rejectedCorruptions=8, originalEvidenceUnchanged=True, cpuExecuted=False)) + print("Relocated evidence passes; eight copied corruptions reject; original evidence unchanged") + + +if __name__ == "__main__": + main() diff --git a/scripts/test-copper68k-040-operand-read-recovery.py b/scripts/test-copper68k-040-operand-read-recovery.py new file mode 100644 index 00000000..bb344944 --- /dev/null +++ b/scripts/test-copper68k-040-operand-read-recovery.py @@ -0,0 +1,223 @@ +"""Execute/replay first-read fault recovery and persistent test-bus repair.""" +import argparse +import hashlib +import itertools +import json +import os +from pathlib import Path +import subprocess +import xml.etree.ElementTree as ET + +REPO = Path(__file__).resolve().parent.parent +NS = {"t": "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"} +PREFIX = "Copper68k.Tests.Synthetic." +CLASSES = ["SyntheticM68040OperandReadFaultDiscoveryTests", "SyntheticM68040OperandReadRecoveryTests", + "SyntheticM68040OperandReadMappingRepairTests"] +FLAGS = ["COPPER68K_RUN_040_OPERAND_READ_" + suffix for suffix in ["DISCOVERY", "RECOVERY", "MAPPING_REPAIR"]] +GROUPS = [ + ("operand-read-recovery-reference", 256, True), + ("operand-read-mapping-reference", 320, True), + ("operand-read-recovery-matrix", 81920, False), + ("operand-read-mapping-matrix", 102400, False), + ("operand-read-fault-discovery", 61440, False)] +FORMS = [("MOVE", 1), ("MOVE", 2), ("MOVE", 4), ("MOVEA", 2), ("MOVEA", 4), + ("ADD", 1), ("ADD", 2), ("ADD", 4), ("CMP", 1), ("CMP", 2), ("CMP", 4), + ("TST", 1), ("TST", 2), ("TST", 4), ("MOVEM", 2), ("MOVEM", 4)] +BANKS = ("user", "user-M", "ISP", "MSP") + + +def named_tests(selection): + groups(selection) # Reject unknown selections before creating output. + old, recovery, mapping = [PREFIX + c + "." for c in CLASSES] + names = {old + "FixedManualReadInstructionWitnessesValidateEncodings", + mapping + "PersistentMappingAndLiteralRepairProgramHaveFixedReferenceExamples"} + names.update(old + f"ActualMovemFaultRetainsCalculatedEaAcrossLoadedBaseAndIndex(full: {v})" + for v in ("False", "True")) + for cl, method in [(recovery, "FirstReadFaultCompletesAfterExplicitHandlerReturn"), + (mapping, "HandlerReallyRepairsPersistentMappingBeforeReturning")]: + names.update(cl + method + f"(batch: {v})" for v in ("False", "True")) + if selection == "complete": + names.update(old + m for m in ("ScalarReadFaultsRequireFormat7", "BatchReadFaultsRequireFormat7")) + names.update(cl + "EveryCcrLaneAndFaultByte" + route + for cl in (recovery, mapping) for route in ("Scalar", "Batch")) + return names + + +def check(value, message): + if not value: + raise ValueError(message) + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def normalized(path): + return hashlib.sha256(path.read_bytes().replace(b"\r\n", b"\n")).hexdigest() + + +def load(path): + return json.loads(path.read_text(encoding="utf-8-sig")) + + +def save(path, value): + path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8", newline="\n") + + +def source_names(): + names = subprocess.check_output(["git", "ls-files", "Copper68k", "Copper68k.Tests"], cwd=REPO, text=True).splitlines() + names = sorted(n for n in names if Path(n).suffix in [".cs", ".csproj"]) + check(len(names) == len(set(names)) > 0 and any(n.startswith("Copper68k/") for n in names), "Empty/duplicate source selection") + return names + + +def groups(selection): + check(selection in ["controls", "complete"], "Unknown audit selection") + return [g for g in GROUPS if selection == "complete" or g[2]] + + +def command(root, selection): + groups(selection) + # Select only required methods in controls; optional EnvironmentFacts must + # not appear as skipped rows or silently inflate the selected inventory. + filter_value = "|".join("FullyQualifiedName~" + n.split("(")[0] + for n in sorted({n for n in named_tests(selection)})) + return ["dotnet", "test", str(root / "source/Copper68k.Tests/Copper68k.Tests.csproj"), "-c", "Release", + "--artifacts-path", str(root / "build"), "--filter", filter_value, + "--logger", "trx;LogFileName=audit.trx", "--results-directory", str(root)] + + +def expected_keys(group): + """Independent literal fixture inventory, with CCR/phase preserved as keys.""" + entry = group == "operand-read-fault-discovery" + generated = entry or group.endswith("-matrix") + cohort = group.removeprefix("operand-read-") + if cohort.startswith("recovery-"): + cohort = cohort.removeprefix("recovery-") + phases = ("fault-entry", "mapping-repair", "handler-RTE", "completed-read", "following-MOVEQ") if group.startswith("operand-read-mapping-") else ( + "fault-entry", "handler-RTE", "completed-read", "following-MOVEQ") + for family, width in FORMS: + for bank, trace, ccr, lane in itertools.product( + BANKS, (0, 0x8000, 0x4000) if entry else (0,), + (31,) if entry or not generated else range(32), + range(4) if generated else (0,)): + for byte in range(width) if generated else (width - 1,): + base = f"68040/{family}/operand-read-" + if entry: + yield base + f"fault/size={width}/bank={bank}/T={trace:04X}/lane={lane}/byte={byte}" + else: + for phase in phases: + yield base + f"recovery/{cohort}/size={width}/bank={bank}/T={trace:04X}/lane={lane}/byte={byte}/ccr={ccr:02X}/phase={phase}" + + +def execute(root, selection): + check(not root.exists(), "Use a fresh output directory; evidence is never overwritten") + names = source_names() + sources = {n: sha(REPO / n) for n in names} + assets = {"Copper68k/" + n: sha(REPO / "Copper68k" / n) for n in ["README.md", "copper68k-icon.png"]} + for name in [*sources, *assets]: + target = root / "source" / name + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes((REPO / name).read_bytes()) + settings = dict(COPPER68K_SYNTHETIC_REPORT_DIR=str(root)) + if selection == "complete": + settings.update({n: "1" for n in FLAGS}) + inputs = dict(schema=1, profile="040-operand-read-recovery", selection=selection, executedRoot=str(root), + sources=sources, assets=assets, normalizedSources={n: normalized(REPO / n) for n in names}, + settings=settings, command=command(root, selection), producerSha256=sha(Path(__file__))) + save(root / "inputs.json", inputs) + environment = {k: v for k, v in os.environ.items() if not k.startswith("COPPER68K_")} + environment.update(settings) + with (root / "execution.log").open("w", encoding="utf-8") as log: + result = subprocess.run(inputs["command"], env=environment, stdout=log, stderr=subprocess.STDOUT) + execution = dict(exit=result.returncode, inputsSha256=sha(root / "inputs.json"), + outputs={n: sha(root / n) for n in ["execution.log", "audit.trx"] if (root / n).exists()}, + assemblies={f.relative_to(root).as_posix(): sha(f) for f in (root / "build/bin").rglob("Copper68k*.dll")}, + sourcesUnchanged=all(sha(root / "source" / n) == sha(REPO / n) == h for n, h in sources.items())) + save(root / "execution.json", execution) + check(result.returncode == 0, "Execution failed; inspect execution.log") + + +def validate(root, selection): + inputs, execution = load(root / "inputs.json"), load(root / "execution.json") + check(inputs["schema"] == 1 and inputs["profile"] == "040-operand-read-recovery" and inputs["selection"] == selection, "Wrong audit profile/selection") + origin = Path(inputs["executedRoot"]) + check(origin.is_absolute() and inputs["command"] == command(origin, selection), "Wrong/empty command selection") + settings = dict(COPPER68K_SYNTHETIC_REPORT_DIR=str(origin)) + if selection == "complete": + settings.update({n: "1" for n in FLAGS}) + check(inputs["settings"] == settings and inputs["producerSha256"] == sha(Path(__file__)), "Wrong audit settings/producer") + check(execution["exit"] == 0 and execution["sourcesUnchanged"] and execution["inputsSha256"] == sha(root / "inputs.json"), "Incomplete/changed execution") + check(set(inputs["sources"]) == set(inputs["normalizedSources"]) == set(source_names()), "Wrong source inventory") + for name, digest in inputs["sources"].items(): + check(sha(root / "source" / name) == digest and normalized(root / "source" / name) == inputs["normalizedSources"][name] == normalized(REPO / name), "Changed source: " + name) + check(set(inputs["assets"]) == {"Copper68k/README.md", "Copper68k/copper68k-icon.png"}, "Wrong assets") + check(all(sha(root / "source" / n) == sha(REPO / n) == h for n, h in inputs["assets"].items()), "Changed assets") + required_dlls = {"build/bin/Copper68k/release/Copper68k.dll", "build/bin/Copper68k.Tests/release/Copper68k.dll", "build/bin/Copper68k.Tests/release/Copper68k.Tests.dll"} + check(set(execution["assemblies"]) == required_dlls and all(sha(root / n) == h for n, h in execution["assemblies"].items()), "Wrong/changed DLL identity") + check({p.relative_to(root).as_posix() for p in (root / "build/bin").rglob("Copper68k*.dll")} == required_dlls, "Extra/missing CPU or test DLL") + check(execution["assemblies"]["build/bin/Copper68k/release/Copper68k.dll"] == execution["assemblies"]["build/bin/Copper68k.Tests/release/Copper68k.dll"], "CPU copies differ") + check(set(execution["outputs"]) == {"execution.log", "audit.trx"} and all(sha(root / n) == h for n, h in execution["outputs"].items()), "Changed execution output") + chosen, names = groups(selection), named_tests(selection) + tree = ET.parse(root / "audit.trx") + rows = tree.findall(".//t:UnitTestResult", NS) + check(len(rows) == len(names) and {r.get("testName") for r in rows} == names and all(r.get("outcome") == "Passed" for r in rows), "Missing/extra/duplicate/failed/skipped named tests") + check(tree.find(".//t:ResultSummary", NS).get("outcome") == "Completed", "Incomplete test run") + counters = tree.find(".//t:Counters", NS) + check(all(int(counters.get(k, -1)) == v for k, v in dict(total=len(names), executed=len(names), passed=len(names), failed=0, notExecuted=0).items()), "Wrong test counters") + definitions = tree.findall(".//t:UnitTest", NS) + lookup = {d.get("id"): d for d in definitions} + check(len(lookup) == len(definitions) == len(names) and set(lookup) == {r.get("testId") for r in rows}, "Wrong loaded definitions") + dll = (origin / "build/bin/Copper68k.Tests/release/Copper68k.Tests.dll").resolve() + for row in rows: + definition = lookup[row.get("testId")] + method = definition.find("t:TestMethod", NS) + check(definition.get("name") == row.get("testName") and method.get("className").split(",")[0] == row.get("testName").split("(")[0].rsplit(".", 1)[0] and + method.get("name") == row.get("testName").split("(")[0].rsplit(".", 1)[1] and + Path(definition.get("storage")).resolve() == Path(method.get("codeBase")).resolve() == dll, "Wrong loaded method/assembly") + reports, total = {}, 0 + for group, count, _ in chosen: + keys = list(expected_keys(group)) + check(len(keys) == len(set(keys)) == (count // 32 if group == "operand-read-fault-discovery" else count) > 0, "Invalid independent case inventory: " + group) + for mode in ["scalar", "batch"]: + name = f"68040-{group}-{mode}.json" + report = load(root / name) + check(report["model"] == "68040" and report["group"] == group + "-" + mode and report["logicalCases"] == count and report["xunitBatches"] == 1 and + report["counts"] == dict(passing=count, mismatching=0, unsupported=0, untested=0) and not report["failures"], "Wrong/failed report: " + name) + check(set(report["combinations"]) == set(keys) and all(v == {"passing": 32 if group == "operand-read-fault-discovery" else 1} for v in report["combinations"].values()), "Wrong case keys/weights: " + name) + reports[name] = sha(root / name) + total += count + check({p.name for p in root.glob("68040-*.json")} == set(reports), "Missing/extra report selection") + metadata = {"inputs.json", "execution.json"} | ({"proof.json"} if (root / "proof.json").exists() else set()) + check({p.name for p in root.glob("*.json")} == set(reports) | metadata, "Unexpected audit JSON evidence") + check(total == (1152 if selection == "controls" else 492672), "Wrong logical total") + return dict(schema=1, selection=selection, namedPasses=len(names), logicalCases=total, reports=reports, + inputsSha256=sha(root / "inputs.json"), executionSha256=sha(root / "execution.json"), + producerSha256=sha(Path(__file__)), executedRoot=str(origin), evidenceRoot=str(root), + sourceInputs=len(inputs["sources"]), productionCpuInputs=sum(n.startswith("Copper68k/") for n in inputs["sources"]), + sourceComparison="snapshot raw hashes; current checkout CRLF-to-LF only", fullCpuSuite=False, + hardwareQualified=False, productionImport=False, publication=False, roadmapComplete=False) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--selection", choices=["controls", "complete"], default="complete") + parser.add_argument("--validate-only", action="store_true") + args = parser.parse_args() + root = args.output.resolve() + if not args.validate_only: + execute(root, args.selection) + proof = validate(root, args.selection) + if args.validate_only: + existing = load(root / "proof.json") + existing["evidenceRoot"] = str(root) + check(existing == proof, "Proof differs from actual evidence") + else: + check(not (root / "proof.json").exists(), "Existing proof") + save(root / "proof.json", proof) + print(f"Qualified {args.selection}: {proof['namedPasses']} tests / {proof['logicalCases']} cases / {len(proof['reports'])} reports") + + +if __name__ == "__main__": + main() From 567246de8b77c4fb51a1620cae6222167a38093a Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 03:15:25 +0300 Subject: [PATCH 200/211] Expose lost and unrelated CP vectors after task transfer --- Copper68k.Tests/Synthetic/README.md | 25 +++++ ...eticM68040ContextTransferDiscoveryTests.cs | 95 +++++++++++++++++++ docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 10 ++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 47 +++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 12 +++ 5 files changed, 189 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040ContextTransferDiscoveryTests.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index bc08b365..f719064c 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1829,3 +1829,28 @@ evidence without executing its recorded command; a valid relocated copy can pass Missing/extra reports, wrong weights, empty/skipped/failed selections, substituted methods or assemblies, and changed sources/DLLs reject. Replay requires this source checkpoint, with only CRLF-to-LF checkout normalization permitted. + +### CP context-transfer discovery + +`SyntheticM68040ContextTransferDiscoveryTests` diagnoses the retained vector +ownership gap using supplied suspended delivery, public task-context copy and +switch APIs, and the existing guarded format-7 fixture. Local scalar/batch +controls cover vectors 49–55 and user/ISP/MSP: 42 passing cases total. The +optional transferred requirement covers fresh and unrelated-pending destinations +at CCR=31, trace zero. It currently **fails**: 42 unsupported fresh cases and 42 +wrong-vector cases across both routes. These are implementation gaps, not +expected processor exceptions. Ordinary unavailable discovery rows are not +successful context-transfer coverage. + +```powershell +$env:COPPER68K_RUN_040_CONTEXT_TRANSFER_DISCOVERY = '1' +$env:COPPER68K_SYNTHETIC_REPORT_DIR = 'artifacts/040-context-transfer/reports' +dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path artifacts/040-context-transfer/build --filter 'FullyQualifiedName~SyntheticM68040ContextTransferDiscoveryTests' --logger 'trx;LogFileName=audit.trx' --results-directory artifacts/040-context-transfer/results +Remove-Item Env:COPPER68K_RUN_040_CONTEXT_TRANSFER_DISCOVERY +Remove-Item Env:COPPER68K_SYNTHETIC_REPORT_DIR +``` + +The requested selection must retain all four named rows and four reports, with +two passing local rows and two failing discovery rows until the gap is resolved. +The source pending vector is an explicit fixture input; no FPU arithmetic, +hardware frame migration/FSAVE ABI, original access entry or trace is qualified. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040ContextTransferDiscoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040ContextTransferDiscoveryTests.cs new file mode 100644 index 00000000..f3db9de7 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040ContextTransferDiscoveryTests.cs @@ -0,0 +1,95 @@ +using Copper68k; +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Diagnose the retained CP context-transferred-vector requirement. Pending +// delivery is a supplied input, as in SyntheticM68040FpuContinuationTests. +// CopyTaskContextFrom/SwitchTaskContext are emulator APIs, not guest opcodes. +// This does not qualify silicon/FSAVE images, FPU arithmetic or a migration ABI. +public sealed class SyntheticM68040ContextTransferDiscoveryTests(ITestOutputHelper output) +{ + private const string Enable = "COPPER68K_RUN_040_CONTEXT_TRANSFER_DISCOVERY"; + private const uint Target = 0x6000, Vbr = 0x10000, Ea = 0x12345678; + + [Theory, Trait("Suite", "Synthetic")] + [InlineData(false)] + [InlineData(true)] + public void LocalPendingDeliveryControls(bool batch) => Audit(batch, false); + + [EnvironmentFact(Enable, "diagnose lost or unrelated CP vectors after public task-context transfer"), Trait("Suite", "ReferenceDiscovery")] + public void TransferredVectorRequirementScalar() => Audit(false, true); + + [EnvironmentFact(Enable, "diagnose lost or unrelated CP vectors after public task-context transfer"), Trait("Suite", "ReferenceDiscovery")] + public void TransferredVectorRequirementBatch() => Audit(true, true); + + private void Audit(bool batch, bool transfer) + { + var route = batch ? "batch" : "scalar"; + var report = new CoverageBatch("68040", $"cp-context-{(transfer ? "transfer-discovery" : "local-controls")}-{route}"); + foreach (var vector in Enumerable.Range(49, 7)) + foreach (var bank in new[] { "user", "ISP", "MSP" }) + foreach (var destination in transfer ? new[] { "fresh", "unrelated-pending" } : ["local"]) + { + var source = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); + var (_, frame, sr) = SyntheticM68040AccessFrameAuditTests.Prepare(source, 7, bank, 31, 0, Vbr, Ea, 0x8005); + // This vector was selected before delivery was suspended. It must + // not be inferred from mutable FPCR/FPSR or an unrelated event. + source.Core.State.M68040PendingFpuExceptions.Begin(3, vector, Target); + foreach (var n in Enumerable.Range(49, 7)) + source.InitializePhysical(Vbr + (uint)n * 4, 0xb000u + (uint)n * 32, 4); + var machine = source; + if (transfer) + { + var saved = new M68kCpuState(); + saved.CopyTaskContextFrom(source.Core.State); + machine = new SyntheticMachine(source.Model, source.Bus); + machine.Core.Reset(SyntheticMachine.Code, 0x4700); + if (destination == "unrelated-pending") + machine.Core.State.M68040PendingFpuExceptions.Begin(3, vector == 55 ? 49 : 55, Target); + machine.Core.SwitchTaskContext(saved); + } + var e = ArchitecturalExpectation.Capture(machine); + e.ControlChecks["selected vector"] = (s => unchecked((uint)s.LastExceptionVector), (uint)vector); + e.ControlChecks["one pending delivery"] = (s => s.ExceptionSequence, machine.Core.State.ExceptionSequence + 1); + e.ControlChecks["original saved PC"] = (s => s.LastExceptionStackedProgramCounter, Target); + e.ControlChecks["original saved SR"] = (s => s.LastExceptionStatusRegister, sr); + e.Sr = (ushort)((sr | 0x2000) & ~0xc000); + e.A[7] = frame + 48; e.InactiveStackPointer = 0x7800; + if (bank == "MSP") e.MasterStackPointer = frame + 48; + e.Write(frame + 48, sr, 2, machine.Model); + e.Write(frame + 50, Target, 4, machine.Model); + e.Write(frame + 54, (uint)(0x3000 | vector * 4), 2, machine.Model); + e.Write(frame + 56, Ea, 4, machine.Model); + e.Pc = 0xb000u + (uint)vector * 32; e.ExceptionVector = vector; + var id = $"68040/RTE/CP-context/vector={vector}/bank={bank}/destination={destination}/ccr=1F/phase=converted-delivery/op=4E73"; + try + { + if (batch) + { + var boundary = new Boundary(); + var count = ((IM68kBatchCore)machine.Core).ExecuteInstructions(1, machine.Core.State.Cycles + 1000, boundary); + if (count != 1 || boundary.Before != 1 || boundary.After != 1) + throw new InvalidOperationException($"Batch count/callbacks differ: {count}/{boundary.Before}/{boundary.After}"); + } + else machine.Core.ExecuteInstruction(); + var mismatch = e.Verify(machine); + report.Record(id, mismatch == null ? "passing" : "mismatching", mismatch == null ? null + : $"Selected vector expected {vector}, observed {machine.Core.State.LastExceptionVector}; {mismatch}"); + } + catch (Exception ex) when (ex is UnsupportedM68kTimingException or UnsupportedM68kOpcodeException or UnsupportedM68040InstructionException) + { report.Record(id, "unsupported", ex.Message); } + catch (Exception ex) { report.Record(id, "mismatching", ex.Message); } + } + // Every requested transfer failure remains a failed gate, not an + // expected-exception pass or a reference exclusion. + report.Complete(output); + } + + private sealed class Boundary : IM68kInstructionBoundary + { + public int Before, After; + public bool BeforeInstruction() { Before++; return true; } + public void AfterInstruction(long previousCycle, long currentCycle) => After++; + } +} diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 4b910009..b3ba311d 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -30,6 +30,16 @@ it does not replace or discard their remaining gaps. ## Completion gates and evidence status +CP context transfer now has actual failed discovery evidence rather than only +an untested label. With supplied pending vectors 49–55 and user/ISP/MSP frames, +local scalar/batch controls pass 42 cases. Public task-context copy/switch loses +the source vector on fresh destinations (42 unsupported cases) and reuses an +unrelated destination event (42 exact wrong-vector cases). The optional request +exits 1 with two named passes / two failures. Independent proof: +`ccd22c898d1a092ecf8c31d73e45f5560b573823eae1a88a3048d3201688d4db`. +This diagnoses the emulator transfer path; it does not establish a hardware +frame-migration ABI or clear the broader 480-case failing gate. + The first-read entry/recovery/persistent test-bus repair selection now has a maintained isolated execution/replay command. Its fresh complete selection passes 14 named tests / 492,672 checks / ten reports; controls pass eight / 1,152 / four. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index c595839e..841f0fd2 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -14407,6 +14407,53 @@ the broader 480-case gate and reference disagreements remain. Milestone 6 stays **in progress**, `roadmapComplete=false`; no production CPU semantics/timing change, private import, publication or new package/API/consumer execution occurs. +## CP context-transferred vector discovery — 2026-10-09 + +`SyntheticM68040ContextTransferDiscoveryTests` gives the retained CP vector +ownership requirement a bounded executable witness. The original selected vector +is supplied pending integer-delivery state, following the established local CP +fixture. Vectors 49–55 and user/ISP/MSP frames run at CCR=31, trace zero. Literal +vector-table addresses distinguish every handler. Common architectural checks +verify local conversion, saved SR/PC, format/vector/EA and guarded frame memory. + +For transferred cases, a new `M68kCpuState` is saved through public +`CopyTaskContextFrom`, then installed on a second factory-created core through +`SwitchTaskContext`. Both cores share fixture memory; only one executes each +case. A fresh destination has no pending vector. The other destination has a +deliberately unrelated pending event: 55 when the source is 49–54, or 49 when +the source is 55. Neither is an architectural expectation calculated from CPU +helpers. This is an emulator API discovery, not a guest scheduler/FSAVE ABI or +hardware frame migration qualification. + +Fresh isolated `ContextTransferDiscoveryV3` **exits 1**, with **two passing +local rows / two failing discovery rows / four reports**. Both routes have 21 +local passing cases, 21 unsupported fresh transfers and 21 wrong-vector transfers: +**42 passing / 42 unsupported / 42 mismatching** overall. Wrong-vector diagnostics +retain the expected/observed vector and exact distinct handler PC for every case. +Fresh scalar execution exposes `UnsupportedM68040InstructionException`; batch +exposes `UnsupportedM68kTimingException`. Both are reported as unsupported, +without turning an implementation gap into an expected processor exception. + +Independent verification binds the exact command/settings, 237 raw source/project +inputs, three DLLs, loaded method definitions, named outcomes, report keys/weights +and every precise failure diagnostic. The only source addition is this class; +all 236 predecessor inputs, including all 37 production CPU inputs, remain +raw-identical. Proof SHA-256: +`ccd22c898d1a092ecf8c31d73e45f5560b573823eae1a88a3048d3201688d4db`. +The first workspace discovery classified the batch unsupported exception as a +mismatch through a shared helper. Its replacement uses scoped explicit exception +classification and preserves actual failures. A later independent-verifier +attempt rejected its assumption that scalar/batch wrapper messages were equal; +V3 verification requires each route's exact observed exception message. + +Reproduction is documented in the +[test README](../Copper68k.Tests/Synthetic/README.md#cp-context-transfer-discovery). +Ordinary unavailable discovery rows supply no transfer coverage. This diagnoses +two concrete gaps; it does not qualify all CCRs/trace states, original access +entry, FPU arithmetic or hardware state migration. No CPU change or private +implementation import occurs. The broader 480-case inventory remains unchanged +and failing. Milestone 6 stays **in progress**, `roadmapComplete=false`. + ## Maintained first-read recovery audit — 2026-10-09 `scripts/test-copper68k-040-operand-read-recovery.py` now maintains isolated diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 3292fefa..04dce250 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1,5 +1,17 @@ # Copper68k synthetic instruction suite +### Milestone 6 checkpoint — CP context-transfer failure, 2026-10-09 + +The previously untested context-transferred-vector requirement now has a bounded +failed discovery fixture using supplied pending delivery and public task APIs. +Local controls pass 42 cases; transferred fresh destinations produce 42 +unsupported cases and unrelated-pending destinations produce 42 wrong-vector +cases across scalar/batch routes. Actual xUnit exit is 1, with two passing and +two failing named rows. Independent source/assembly/method/report verification +retains exact diagnostics. No production CPU fix or migration ABI is claimed. +The 237-input graph preserves all 37 CPU inputs; broader 480-case requirements +remain. Milestone 6 stays **in progress**, `roadmapComplete=false`. + ### Milestone 6 checkpoint — maintained first-read audit, 2026-10-09 The entry/recovery/persistent test-bus mapping slice now has a maintained isolated From b6ed3ffff1947961a04076bad88a2707ae75a351 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 03:26:13 +0300 Subject: [PATCH 201/211] Qualify private suspended-delivery context snapshot candidate --- .gitattributes | 1 + Copper68k.Tests/Synthetic/README.md | 22 ++++++++ ...eticM68040ContextSnapshotDiscoveryTests.cs | 51 +++++++++++++++++ docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 12 ++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 55 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 14 +++++ .../m68040-context-pending-candidate.patch | 41 ++++++++++++++ 7 files changed, 196 insertions(+) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040ContextSnapshotDiscoveryTests.cs create mode 100644 scripts/reference/m68040-context-pending-candidate.patch diff --git a/.gitattributes b/.gitattributes index 470426df..37a943c6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,4 @@ scripts/reference/m68020-predecrement-write-candidate.patch text eol=lf whitespa scripts/reference/m68020-predecrement-read-candidate.patch text eol=lf whitespace=-blank-at-eol scripts/reference/m68020-read-write-recovery-candidate.patch text eol=lf whitespace=-blank-at-eol,-blank-at-eof scripts/reference/m68020-private-predecrement-manifest.json text eol=lf +scripts/reference/m68040-context-pending-candidate.patch text eol=lf whitespace=-blank-at-eol diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index f719064c..4f3f5e57 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1854,3 +1854,25 @@ The requested selection must retain all four named rows and four reports, with two passing local rows and two failing discovery rows until the gap is resolved. The source pending vector is an explicit fixture input; no FPU arithmetic, hardware frame migration/FSAVE ABI, original access entry or trace is qualified. + +### Private pending-delivery snapshot candidate + +`SyntheticM68040ContextSnapshotDiscoveryTests` checks nested LIFO order, +distinct identity for equal-valued deliveries, replacement rather than append, +self-copy, independent completion/reset, empty-context clearing, and the existing +machine-time policy of `CopyFrom` versus `CopyTaskContextFrom`. Enable +`COPPER68K_RUN_040_CONTEXT_SNAPSHOT_DISCOVERY=1` when explicitly requesting it. +Production currently fails this test; unavailable execution supplies no coverage. + +The reviewed `scripts/reference/m68040-context-pending-candidate.patch` changes +only an isolated CPU copy. It clones delivery entries when saving/restoring CPU +state and clears unrelated destination entries. It adds no public API. Apply it +only to a separate source copy matching this checkpoint, after normalizing its +two affected files from CRLF to LF; require `git apply --check` before applying. +Select the snapshot and context-transfer discovery classes plus +`M68040FpuContinuationStateTests`, with both discovery flags enabled. The clean +production baseline has 18 named passes / three failures. The private candidate +passes all 21 named rows and 126 reported cases, preserving local reports and +the sixteen retained delivery tests. It still rejects a foreign frame without +saved delivery state. Full CPU, API and isolated NuGet consumer qualification +remain separate gates; the candidate is not imported or published. diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040ContextSnapshotDiscoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040ContextSnapshotDiscoveryTests.cs new file mode 100644 index 00000000..87f00e7e --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040ContextSnapshotDiscoveryTests.cs @@ -0,0 +1,51 @@ +using Copper68k; + +namespace Copper68k.Tests.Synthetic; + +// Emulator snapshot ownership contract; not a silicon context/FSAVE ABI. +public sealed class SyntheticM68040ContextSnapshotDiscoveryTests +{ + [EnvironmentFact("COPPER68K_RUN_040_CONTEXT_SNAPSHOT_DISCOVERY", "qualify independent suspended-delivery snapshots"), Trait("Suite", "ReferenceDiscovery")] + public void SnapshotsOwnIndependentNestedPendingDeliveries() + { + foreach (var taskOnly in new[] { false, true }) + { + var source = new M68kCpuState { Cycles = 100, NativeCycles = 200, ProgramCounter = 0x1000 }; + source.M68040PendingFpuExceptions.Begin(3, 49, 0x6000); + source.M68040PendingFpuExceptions.Begin(3, 55, 0x6000); + var originalTop = source.M68040PendingFpuExceptions.Begin(3, 55, 0x6000); + var saved = new M68kCpuState { Cycles = 500, NativeCycles = 700 }; + saved.M68040PendingFpuExceptions.Begin(3, 53, 0x6000); + Copy(saved, source); Copy(saved, source); // Replace, never append. + Assert.Equal(taskOnly ? 500 : 100, saved.Cycles); + Assert.Equal(taskOnly ? 700 : 200, saved.NativeCycles); + Assert.Equal(0x1000u, saved.ProgramCounter); + var top = saved.M68040PendingFpuExceptions.Find(3); + Assert.NotNull(top); Assert.Equal(55, top.Vector); Assert.NotSame(originalTop, top); + Copy(saved, saved); // Self-copy must retain the same delivery ownership. + Assert.Same(top, saved.M68040PendingFpuExceptions.Find(3)); + saved.M68040PendingFpuExceptions.Complete(top); + var equal = saved.M68040PendingFpuExceptions.Find(3); + Assert.NotNull(equal); Assert.Equal(55, equal.Vector); Assert.NotSame(top, equal); + saved.M68040PendingFpuExceptions.Complete(top); // Already consumed identity. + Assert.Same(equal, saved.M68040PendingFpuExceptions.Find(3)); + saved.M68040PendingFpuExceptions.Complete(equal); + Assert.Equal(49, saved.M68040PendingFpuExceptions.Find(3)!.Vector); + Assert.Same(originalTop, source.M68040PendingFpuExceptions.Find(3)); + saved.M68040PendingFpuExceptions.Reset(); + Assert.Null(saved.M68040PendingFpuExceptions.Find(3)); + Assert.Same(originalTop, source.M68040PendingFpuExceptions.Find(3)); + Copy(saved, source); + source.M68040PendingFpuExceptions.Reset(); + Assert.Equal(55, saved.M68040PendingFpuExceptions.Find(3)!.Vector); + Copy(saved, new M68kCpuState()); // Empty incoming context clears stale delivery. + Assert.Null(saved.M68040PendingFpuExceptions.Find(3)); + + void Copy(M68kCpuState destination, M68kCpuState from) + { + if (taskOnly) destination.CopyTaskContextFrom(from); + else destination.CopyFrom(from); + } + } + } +} diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index b3ba311d..be0f3c52 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -30,6 +30,18 @@ it does not replace or discard their remaining gaps. ## Completion gates and evidence status +A private two-file CP context snapshot candidate now passes the selected +21-row gate, including all 126 reported cases, nested snapshot ownership and +sixteen retained delivery tests. The baseline still has 18 passes / three +failures. Independent proof: +`802db3def3cbc940e545fb0cc2efcc80120009f72d1bb59ee335e6c0b26d45e7`. +The candidate copies suspended delivery identities as task-local state without +changing the public API. It is not imported; production and the broader 480-case +gate remain unchanged. Full CPU qualification is running on its frozen +238-input graph with ten pinned native presets. Discovery display entries are +not the executed-row count; final roster/report/API/consumer qualification is +still required before production readiness can be assessed. + CP context transfer now has actual failed discovery evidence rather than only an untested label. With supplied pending vectors 49–55 and user/ISP/MSP frames, local scalar/batch controls pass 42 cases. Public task-context copy/switch loses diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 841f0fd2..1e4c6007 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -14407,6 +14407,61 @@ the broader 480-case gate and reference disagreements remain. Milestone 6 stays **in progress**, `roadmapComplete=false`; no production CPU semantics/timing change, private import, publication or new package/API/consumer execution occurs. +## Private pending-delivery context snapshot candidate — 2026-10-09 + +The existing public task-switch contract replaces task-local state while keeping +machine time. A supplied suspended CP event is already integer-delivery state; +its selected vector cannot be reconstructed from handler-modified FPU registers. +The private candidate therefore copies this state with CPU snapshots. The patch +adds an internal deep-copy method to `M68040PendingFpuExceptions` and calls it from +`M68kCpuState.CopyFrom` and `CopyTaskContextFrom`. It replaces destination entries, +preserves nested order, assigns independent identities to equal-valued entries, +and leaves self-copy unchanged. No public signature or instruction timing policy +change is proposed. Production CPU source remains untouched. + +`SyntheticM68040ContextSnapshotDiscoveryTests` requests both copy APIs and checks +independent completion/reset, LIFO order, equal-valued nested identities, repeated +replacement, self-copy, clearing from an empty incoming context, PC and machine +time. Together with context-transfer discovery and the sixteen retained +`M68040FpuContinuationStateTests` rows, a fresh isolated baseline has **18 passes / +three failures / actual exit 1**. Its four transfer reports are byte-identical to +the earlier failed V3 evidence, and its snapshot fails with expected vector 55 +versus stale destination vector 53. The isolated candidate has **21 passes / zero +failures / actual exit 0**, including **126 passing reported cases**. Local +reports remain byte-identical. The retained tests cover accurate and warmed +JIT V1/V2 delivery, nested/reset/redirect behavior, and completed-store non-replay. +A foreign CP frame without supplied delivery state still rejects explicitly. + +Independent checks bind all 238 source/project inputs, both isolated source +graphs, exact two-file normalized patch reconstruction, three DLLs per graph, +commands/settings, 21 named outcomes and loaded methods, every report key/weight +and the exact before-failure diagnostic. All 237 preceding source inputs remain +raw-identical in the production baseline; only the snapshot test is added. + +| Review/evidence | SHA-256 | +| --- | --- | +| Two-file candidate patch | `e18ec5c9ebb583ac3da579e70e3cc43b39a2a356be174973e461f3d533bdb5de` | +| Independent selected baseline/candidate proof | `802db3def3cbc940e545fb0cc2efcc80120009f72d1bb59ee335e6c0b26d45e7` | + +Full qualification is running from the same candidate source and compiled DLLs +under `ContextPendingCandidateV1/full-v4`, with the ten pinned native reference +presets from `CurrentReferenceFullV2`. All native input hashes are checked before +execution. Preflight records 5,420 discovery display entries, **not executed +rows**. One non-serializable MemberData method represents 33 old execution rows; +twelve argument displays differ at encoding/truncation boundaries. Their exact +method groups and unchanged fixture sources are bound separately, and the +original 5,408 execution names are retained for final roster checks. Three earlier +preflights rejected incorrect display assumptions or a source filename before +full CPU execution; only the corrected V4 full request starts. No final full +roster/report, API or consumer qualification is claimed while it runs. + +The [test README](../Copper68k.Tests/Synthetic/README.md#private-pending-delivery-snapshot-candidate) +records reproduction constraints and selected expectations. This is an emulator +context snapshot candidate, not a silicon/FSAVE migration ABI or qualification +of original access-frame construction. Broader restoration/trace and reference +gaps remain. Milestone 6 stays **in progress**, `roadmapComplete=false`; no private +production import, package publication or consumer qualification occurs here. + ## CP context-transferred vector discovery — 2026-10-09 `SyntheticM68040ContextTransferDiscoveryTests` gives the retained CP vector diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 04dce250..e7aa78cb 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1,5 +1,19 @@ # Copper68k synthetic instruction suite +### Milestone 6 checkpoint — private context snapshot candidate, 2026-10-09 + +A reviewed two-file private patch copies suspended integer-delivery state with +saved CPU contexts, preserving independent nested identities and replacing stale +destination events. A new opt-in snapshot test covers self-copy, completion/reset +independence, empty-context clearing and existing machine-time policy. The +production baseline fails three of 21 selected rows; the private candidate passes +all 21 and 126 reported cases, including sixteen retained delivery tests. Exact +source/patch/assembly/method/report linkage is independently verified. Full CPU +qualification has started on the frozen 238-input graph with ten pinned native +presets; final full/API/consumer evidence is pending. No production CPU import +or public API change occurs. Milestone 6 stays **in progress**, +`roadmapComplete=false`; broader restoration/reference gaps remain. + ### Milestone 6 checkpoint — CP context-transfer failure, 2026-10-09 The previously untested context-transferred-vector requirement now has a bounded diff --git a/scripts/reference/m68040-context-pending-candidate.patch b/scripts/reference/m68040-context-pending-candidate.patch new file mode 100644 index 00000000..03a2ed5a --- /dev/null +++ b/scripts/reference/m68040-context-pending-candidate.patch @@ -0,0 +1,41 @@ +--- a/Copper68k/M68040PendingFpuExceptions.cs ++++ b/Copper68k/M68040PendingFpuExceptions.cs +@@ -33,5 +33,20 @@ + if (ReferenceEquals(_entries[n], entry)) { _entries.RemoveAt(n); return; } + } + ++ internal void CopyFrom(M68040PendingFpuExceptions source) ++ { ++ if (ReferenceEquals(this, source)) return; ++ // A saved task owns its delivery identities. Completion/reset in one ++ // context must not consume an equal-valued event in another context. ++ List? copied = null; ++ if (source._entries != null) ++ { ++ copied = new List(source._entries.Count); ++ foreach (var entry in source._entries) ++ copied.Add(new Entry(entry.Format, entry.Vector, entry.ProgramCounter)); ++ } ++ _entries = copied; ++ } ++ + internal void Reset() => _entries?.Clear(); + } +--- a/Copper68k/M68kCore.cs ++++ b/Copper68k/M68kCore.cs +@@ -1404,6 +1404,7 @@ + Stopped = source.Stopped; + LastOpcode = source.LastOpcode; + LastInstructionProgramCounter = source.LastInstructionProgramCounter; ++ M68040PendingFpuExceptions.CopyFrom(source.M68040PendingFpuExceptions); + } + + /// +@@ -1430,6 +1431,7 @@ + CacheAddressRegister = source.CacheAddressRegister; + LastOpcode = source.LastOpcode; + LastInstructionProgramCounter = source.LastInstructionProgramCounter; ++ M68040PendingFpuExceptions.CopyFrom(source.M68040PendingFpuExceptions); + } + + /// From 760c836f0f460ebd912c9da09dc44a576c1c2838 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 03:45:29 +0300 Subject: [PATCH 202/211] Record context candidate reference and standard consumer qualification --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 17 +++++++- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 40 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 +++++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index be0f3c52..0af26ea2 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -30,6 +30,19 @@ it does not replace or discard their remaining gaps. ## Completion gates and evidence status +The same frozen private context candidate now has independently verified +same-build deep/reference evidence: 320,000 seeded cases, 312,500 SingleStepTests +000 cases and 536 Musashi programs pass, with 88 explicit exclusions. All eight +invalid-request controls fail. Compiled public/protected API comparison finds +zero changes across 20 types / 211 records, and normalized CPU source review +confirms exactly two intended semantic changes. A local-only `.81` package +passes a clean committed CopperScreen build, 172 host tests with six unavailable, +74 disk tests and 1,080 engine tests. Native boot could not start because the +original ROM directory is missing; the failed flow is retained separately from +the independently verified standard tests. Full CPU execution is still active, +so full/API/consumer integration is not yet complete. Exact identities appear +in the qualification record. No candidate import or publication occurs. + A private two-file CP context snapshot candidate now passes the selected 21-row gate, including all 126 reported cases, nested snapshot ownership and sixteen retained delivery tests. The baseline still has 18 passes / three @@ -39,8 +52,8 @@ The candidate copies suspended delivery identities as task-local state without changing the public API. It is not imported; production and the broader 480-case gate remain unchanged. Full CPU qualification is running on its frozen 238-input graph with ten pinned native presets. Discovery display entries are -not the executed-row count; final roster/report/API/consumer qualification is -still required before production readiness can be assessed. +not the executed-row count; final full roster/report and native consumer +qualification are still required before production readiness can be assessed. CP context transfer now has actual failed discovery evidence rather than only an untested label. With supplied pending vectors 49–55 and user/ISP/MSP frames, diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 1e4c6007..bc74d499 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8,6 +8,46 @@ The [milestone 6 completion audit](COPPER68K_MILESTONE6_COMPLETION_AUDIT.md) maps the accepted requirements to current evidence and outstanding gates. The dated records below remain scoped historical evidence. +### Private context candidate: same-build references and standard consumers, 2026-10-09 + +This checkpoint extends the selected private candidate proof +`802db3def3cbc940e545fb0cc2efcc80120009f72d1bb59ee335e6c0b26d45e7`. +It does not import the two-file CPU patch or change the failed broader gates. +All executions use the frozen candidate's 238 source inputs / 37 CPU inputs; +deep and guard executions load the exact DLLs used by its selected gate and +active full run. + +| Check | Independently verified result | Proof SHA-256 | +| --- | --- | --- | +| Compiled API and normalized source review | 20 exported types / 211 public/protected records per assembly; zero signature additions/removals. Exactly two CPU source files have semantic changes after newline normalization. This does not qualify serialization/layout or physical behavior. | `55a13349df7c0b50482f3e0b849e61e2db6267d29f66838bb14a5c44a0b96fad` | +| Seeded and reference audits | Three named passes, no skips: seed 68020, 10,000 samples/family/profile, 320,000 cases / 32 reports; 312,500 SingleStepTests 000 cases / 125 files; 536 Musashi programs / 88 exclusions / 624 rows across all eight profiles. | `74959ff987e9b6f42765b900935ad230c7dd30e64ac1a781238310b7cca0b722` | +| Invalid-request controls | Eight actual requests each exit 1 with one exact named failure and no case reports: zero seed, zero samples, empty models, missing/empty SingleStepTests, missing Musashi, adjacent/separated duplicate models. | `a1738f233166e8c8f2c395c2cad1386b807dc1563a90e8634060d813c6a78ba4` | +| Standard CopperScreen consumers | Clean archived commit `a4e80b68a7b58e6c12b940b9d786b2ac73757965`; build succeeds with zero warnings/errors; 172 host passes / six unavailable, 74 disk passes and 1,080 engine passes. Exact named rosters retain prior rows and add only the committed null-list regression. All three loaded CPU DLLs match the local package; projects retain the NuGet boundary. | `d418329545540e794067f7519cb2ba079d2ddb7473cf4e94c5940098f4b2e99d` | + +SingleStepTests remains pinned to +`64b253116a3de04aaac4346c43680960dc9b67e5`, with TAS/TRAPV explicitly excluded. +Musashi remains pinned to `72c1d74800f3087b45a0c1a7342601bbed898881`; +its self-checking programs do not use the Musashi CPU as an oracle. +The isolated package is `1.5.2-synthetic-dev.81`, never published. Its 39 pack +inputs include the 37 executed CPU source/project inputs plus README/icon. +Package verification SHA-256: +`15b96db03d9bdcdea9e60e7f4eb3172c0b813d5300bc711fa930141ab98c3528`. +The consumer scope excludes unrelated dirty working changes. + +The original consumer command exits 1 after the three standard test suites: +`C:/Users/ilkle/Koodit/TestData/ROM/kickstart-3.1-a500.rom` is missing, so +native input validation fails before either Workbench replay starts. This is +unavailable native coverage, not a passing replay. The original failed producer, +flow log and execution record are preserved and hashed by the standard proof. +The retained Workbench ADF is still available; the ROM must be relocated or +restored with its original pinned hash before a separate native-only resume. +Completed build and test results need not be rerun for an input-path repair. + +Full CPU execution remains active on the same frozen assemblies with ten pinned +native reference presets. Its discovery display entries do not establish final +execution rows or report coverage. Milestone 6 remains in progress, with no +full-integration, hardware, production-import or release claim. + Current source checkpoint `c284b94` has fresh seeded and SingleStepTests/Musashi evidence bound to one frozen assembly pair: 320,000 seeded cases, 312,500 SingleStepTests cases and 536 Musashi programs pass, with 88 explicit Musashi diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index e7aa78cb..c7c55376 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1,5 +1,20 @@ # Copper68k synthetic instruction suite +### Milestone 6 checkpoint — context candidate reference and consumer checks, 2026-10-09 + +The frozen 238-input private context candidate passes same-build deep audits: +320,000 seeded cases, 312,500 pinned SingleStepTests 000 cases and 536 Musashi +programs, with 88 explicit exclusions. All eight invalid-request controls fail +as required. Independent source, assembly, loaded-method and report checks bind +these results. Compiled public/protected API comparison finds no changes across +20 exported types / 211 records; normalized review confirms only the two intended +CPU source changes. A local-only `.81` package builds the clean CopperScreen +commit `a4e80b6`: 172 host tests pass with six unavailable, 74 disk tests and +1,080 engine tests pass. Native boot remains unavailable because the original +ROM directory disappeared before replay; the failed consumer flow is retained. +Full CPU execution remains active and unqualified. No production import or +publication occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. + ### Milestone 6 checkpoint — private context snapshot candidate, 2026-10-09 A reviewed two-file private patch copies suspended integer-delivery state with From 0e10bb9090a0a9675df92b2611c5e657301e1252 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 04:06:05 +0300 Subject: [PATCH 203/211] Qualify full private context candidate against retained coverage --- Copper68k.Tests/Synthetic/README.md | 14 ++++ docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 25 +++++-- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 66 ++++++++++++++++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 +++++ 4 files changed, 113 insertions(+), 7 deletions(-) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 4f3f5e57..38753931 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1876,3 +1876,17 @@ passes all 21 named rows and 126 reported cases, preserving local reports and the sixteen retained delivery tests. It still rejects a foreign frame without saved delivery state. Full CPU, API and isolated NuGet consumer qualification remain separate gates; the candidate is not imported or published. + +The frozen 238-input / 37-CPU candidate now has qualified full execution: +5,363 passes / 89 unavailable / zero failures across 5,452 exact named rows; +87,250,840 passing cases / 1,027 reports and ten pinned native reference presets. +All prior rows and 983 prior reports remain; the 44 new reports match their +qualified focused fixtures. The report-only ordinary gate passes 86,663,530 +cases / 783 batches. Same-source full/deep/guard/API/standard-consumer linkage +is verified. The local-only `.81` package passes the clean committed CopperScreen +build and 172 host / 74 disk / 1,080 engine tests, with six optional host rows +unavailable. Native consumer boot has not run because its original ROM directory +is missing. These are private snapshot qualifications; broader restoration and +reference disagreements remain, and no CPU import or publication occurs. +See the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#full-private-context-candidate-qualification-2026-10-09) +for exact identities and separation of execution rows from discovery display. diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 0af26ea2..53196fb1 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -23,13 +23,26 @@ it does not replace or discard their remaining gaps. | Reuse SingleStepTests, Musashi and WinUAE with pinned identities and caveats | Adapter source, reference manifests, source/input hashes, actual execution and documented exclusions | Implemented. SingleStepTests supplies 000 semantic fixtures; Musashi runs independent self-checking programs, not its CPU as an oracle. WinUAE remains a software reference. | | Replacement detects absolute decoding, extension length, index sign, alias order, A7 stride and flags defects | Six distinct isolated mutation proofs with precise replacement IDs and current source linkage | The maintained mutation command defines all six. Fresh isolated 51444ea executions detect all six against a 75,214-case clean baseline. Exact source/roster/report linkage and every mismatch ID are verified. | | Retire an old regression only after a mapped replacement detects its defect | Exact pinned old method/rows, before-removal proof, mutation witness and after-removal sibling verification | NOT displacement, EOR postincrement, ANDI displacement and aliased MOVEA word retirements have exact proofs. EOR replaces one fact with 32,768 passing cases and retains four siblings. Schema-2 retirement replay now requires execution DLL hashes and exact loaded definition/method linkage; fresh MOVEA/EOR executions, 20 corruption controls each and strict replay are qualified. No blanket regression deletion is authorized. | -| Retain cache, prefetch, bus ordering, fault sequencing, JIT and native regressions unless separately proven redundant | Exact test-source diff and full named roster, with every removal accounted for | Specialized tests remain. The exact 5,408-test frozen roster is compared with its predecessor: eight retired ANDI/MOVEA rows and sixteen replacement batches are accounted for. All 63 unavailable rows remain explicit. | +| Retain cache, prefetch, bus ordering, fault sequencing, JIT and native regressions unless separately proven redundant | Exact test-source diff and full named roster, with every removal accounted for | Specialized tests remain. The private context candidate retains all 5,408 prior rows and adds 44: 18 pass / 26 unavailable. Its exact 5,452-row roster has 89 explicitly unavailable rows. Prior retirement mappings and evidence remain separate. | | Production CPU fixes pass full CPU and affected consumers | Same-source full CPU proof; isolated NuGet package; CopperScreen production, host/disk/engine and applicable native results | MOVE16 recovery has full-suite and local .77 consumer evidence. Subsequent consolidation leaves all 37 CPU source inputs unchanged. No new consumer execution is claimed. | | Preserve successful ordering and timing policy; no automatic retry after partial effects | Relevant sequence/fault tests and change review, separate from timing qualification | Existing checks and scoped explicit-RTE continuation proofs remain. Private candidate results are not production behavior or physical timing qualification. | | Test-internal framework; public factory; NuGet consumer boundary; immutable published versions | Source/project/package diffs and consumer dependency inspection | Preserved. Private 39-input CPU candidates remain separate from the production 37-input CPU graph. No publication is authorized by this audit. | ## Completion gates and evidence status +Full private context-candidate qualification is complete on its frozen +238-input / 37-CPU graph: 5,363 passes, 89 unavailable rows, zero failures, +87,250,840 passing cases / 1,027 reports and ten pinned native reference presets. +The independent verifier checks exact named rows, loaded definitions, every +report key/status/weight and reference-input identities. All 983 prior reports +remain unchanged; 44 additions match qualified focused evidence. The ordinary +gate passes 86,663,530 cases / 783 batches. Full proof: +`9c6c91a0abe28aafffbd8cad848800a9393c10117b9358062025d0b61d8b5fea`. +Same-source full/deep/guards/API/standard-consumer linkage proof: +`d5a0e81a6a472d82847b4b313437bea5198fb7d01a95592489b2a49c5d425eb6`. +Native consumer boot remains unavailable; full integration and production +readiness are not claimed. The candidate is not imported or published. + The same frozen private context candidate now has independently verified same-build deep/reference evidence: 320,000 seeded cases, 312,500 SingleStepTests 000 cases and 536 Musashi programs pass, with 88 explicit exclusions. All eight @@ -39,8 +52,8 @@ confirms exactly two intended semantic changes. A local-only `.81` package passes a clean committed CopperScreen build, 172 host tests with six unavailable, 74 disk tests and 1,080 engine tests. Native boot could not start because the original ROM directory is missing; the failed flow is retained separately from -the independently verified standard tests. Full CPU execution is still active, -so full/API/consumer integration is not yet complete. Exact identities appear +the independently verified standard tests. Full CPU execution is qualified, +but native consumer integration is not yet complete. Exact identities appear in the qualification record. No candidate import or publication occurs. A private two-file CP context snapshot candidate now passes the selected @@ -50,10 +63,10 @@ failures. Independent proof: `802db3def3cbc940e545fb0cc2efcc80120009f72d1bb59ee335e6c0b26d45e7`. The candidate copies suspended delivery identities as task-local state without changing the public API. It is not imported; production and the broader 480-case -gate remain unchanged. Full CPU qualification is running on its frozen +gate remain unchanged. Full CPU qualification is complete on its frozen 238-input graph with ten pinned native presets. Discovery display entries are -not the executed-row count; final full roster/report and native consumer -qualification are still required before production readiness can be assessed. +separate from the verified execution roster. Native consumer qualification is +still required before production readiness can be assessed. CP context transfer now has actual failed discovery evidence rather than only an untested label. With supplied pending vectors 49–55 and user/ISP/MSP frames, diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index bc74d499..372bcfd8 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8,6 +8,70 @@ The [milestone 6 completion audit](COPPER68K_MILESTONE6_COMPLETION_AUDIT.md) maps the accepted requirements to current evidence and outstanding gates. The dated records below remain scoped historical evidence. +### Full private context candidate qualification, 2026-10-09 + +The same frozen private candidate now completes full CPU execution with actual +exit 0: **5,363 passing rows / 89 unavailable / zero failures**, total **5,452**. +Independent proof verifies every loaded definition/method, exact row outcomes, +all **1,027 reports / 87,250,840 passing logical cases**, ten pinned native +WinUAE reference presets and the unchanged integer-family inventory. +It retains all 5,408 rows of the qualified production checkpoint. The 44 added +rows are independently enumerated from their fixture declarations: 18 passes +and 26 optional rows unavailable under this selection. Optional unavailability +does not become successful coverage. Discovery's 5,420 display entries include +one nonserializable method for 33 executed matrix rows; twelve other argument +display differences are bound to unchanged fixture sources. The final TRX, +not discovery count, establishes the exact execution roster. + +All 983 prior synthetic coverage reports have identical contents and combination +keys/statuses/weights. The 44 new reports add 394,830 cases and match independently +qualified predecrement controls, context transfer, repeated/heterogeneous nested +reference examples and first-read recovery/mapping reference fixtures. Incoming +CPU sources remain unchanged after execution, and all full/deep/guard runs load +the same three pinned candidate DLLs. The native adapter reports preserve every +prior field/count except their explicitly checked new CPU/adapter DLL hashes; +manifest entries, sizes, hashes and exact `.dat` inventories remain pinned. +These native software-reference presets are separate from Amiga ROM/media boot. + +The current maintained report-only ordinary gate, checked against its frozen +predecessor after newline normalization, exits 0 on this result directory: +**86,663,530 deterministic cases / 783 xUnit batches**. This gate does not clear +the optional broader restoration inventory or disputed software-reference cases. + +| Evidence | SHA-256 | +| --- | --- | +| Full exact roster/report/reference/ordinary-gate proof | `9c6c91a0abe28aafffbd8cad848800a9393c10117b9358062025d0b61d8b5fea` | +| Full input snapshot | `2304bca366f27a166cbf706778f844ee577e753793ede91d33cea63ab0fdd69c` | +| Full TRX | `13baf0b767d6be7aafdd35948822ab3a64a4cc8cdfc9116d2ad699f7034ce737` | +| Same-source full/deep/guards/API/standard-consumer linkage | `d5a0e81a6a472d82847b4b313437bea5198fb7d01a95592489b2a49c5d425eb6` | + +The full producer uses the selected candidate's already-built assemblies with +`--no-build --no-restore`, Release and isolated outputs, the ten declared pinned +reference settings, and both context-transfer/snapshot discovery flags enabled. +All other undeclared `COPPER68K_*` variables are removed before execution. The +separate independent verifier checks the producer's exact command/settings, +source/assembly identities, prior/focused report proofs, reference manifests, +and then invokes `test-copper68k-synthetic.ps1 -ValidateReportsOnly` against the +completed directory. Requested broad Basic and 480-case failing discovery gates +are not selected or relabeled as passing. + +The linkage includes the previously qualified seed 68020 deep/reference audit, +eight actual rejection controls, zero managed API changes across 20 types / 211 +records, and the clean CopperScreen `a4e80b6` standard consumer checks through +local-only `1.5.2-synthetic-dev.81`. It confirms that all 37 packaged CPU source +inputs equal the executed candidate source inputs; package-version metadata +produces a separately checked packaged DLL identity. The original consumer flow +still exits 1 because native input validation cannot find the old ROM directory. +No native consumer replay or full consumer integration is claimed. A native-only +resume is prepared for the original pinned ROM after relocation, preserving the +completed standard suites and failed initial flow. + +The candidate remains an unimported two-file prototype. Production behavior, +the failed broad 010/060 audit, disputed 030/040 trace boundaries and wider +restoration gates remain unchanged. This checkpoint is neither physical timing +qualification nor a release. Milestone 6 remains **in progress**, +`roadmapComplete=false`. + ### Private context candidate: same-build references and standard consumers, 2026-10-09 This checkpoint extends the selected private candidate proof @@ -48,7 +112,7 @@ native reference presets. Its discovery display entries do not establish final execution rows or report coverage. Milestone 6 remains in progress, with no full-integration, hardware, production-import or release claim. -Current source checkpoint `c284b94` has fresh seeded and SingleStepTests/Musashi +Earlier source checkpoint `c284b94` has fresh seeded and SingleStepTests/Musashi evidence bound to one frozen assembly pair: 320,000 seeded cases, 312,500 SingleStepTests cases and 536 Musashi programs pass, with 88 explicit Musashi exclusions. Six actual invalid/missing/empty adapter requests reject. The frozen full diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index c7c55376..b974291e 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1,5 +1,20 @@ # Copper68k synthetic instruction suite +### Milestone 6 checkpoint — full private context candidate, 2026-10-09 + +The frozen 238-input / 37-CPU private context candidate completes full execution +with 5,363 passes, 89 explicitly unavailable rows and zero failures. Independent +qualification verifies the exact 5,452-row roster, every loaded method, all +1,027 reports / 87,250,840 passing cases and ten pinned native reference presets. +All 5,408 prior rows remain; the 44 additions have 18 passes / 26 unavailable. +All 983 prior coverage reports are unchanged; 44 new reports add 394,830 cases +matching their qualified focused fixtures. The ordinary gate passes 86,663,530 +cases / 783 batches. Full, deep, rejection controls, API and local `.81` standard +consumers are linked to the same executed CPU source graph. Native consumer boot +remains unavailable because the original ROM directory is missing. No candidate +is imported or published; the broad reference/restoration gates remain open. +Milestone 6 stays **in progress**, `roadmapComplete=false`. + ### Milestone 6 checkpoint — context candidate reference and consumer checks, 2026-10-09 The frozen 238-input private context candidate passes same-build deep audits: From d1dbf941d2991455a33579e3872f36719aebbe5a Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 04:18:50 +0300 Subject: [PATCH 204/211] Qualify suspended context delivery across all CCR images --- Copper68k.Tests/Synthetic/README.md | 29 ++++++++++ .../SyntheticM68040ContextTransferCcrTests.cs | 29 ++++++++++ ...eticM68040ContextTransferDiscoveryTests.cs | 9 ++-- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 13 +++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 53 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 18 +++++++ 6 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 Copper68k.Tests/Synthetic/SyntheticM68040ContextTransferCcrTests.cs diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 38753931..b43b9a91 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1890,3 +1890,32 @@ is missing. These are private snapshot qualifications; broader restoration and reference disagreements remain, and no CPU import or publication occurs. See the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#full-private-context-candidate-qualification-2026-10-09) for exact identities and separation of execution rows from discovery display. + +### All-CCR pending-delivery context transfer + +Enable `COPPER68K_RUN_040_CONTEXT_TRANSFER_CCR=1` to request +`SyntheticM68040ContextTransferCcrTests`. It reuses the context-transfer fixture +with all 32 initial CCR images and their complementary saved CCRs. This +distinguishes restored flags from stale live flags. Both local and transfer +groups run on fresh per-case machines and aggregate their failures; continuing +the independent group does not retry a partially executed instruction. + +```powershell +$env:COPPER68K_RUN_040_CONTEXT_TRANSFER_CCR = '1' +dotnet test Copper68k.Tests/Copper68k.Tests.csproj -c Release --artifacts-path artifacts/context-ccr-tests --filter 'FullyQualifiedName~SyntheticM68040ContextTransferCcrTests' +``` + +Requested production execution fails at the retained context-copy defect; +disabled optional execution is unavailable coverage. The private two-file +snapshot candidate passes 4,032 new cases / four new reports and retains all +126 canonical cases unchanged. Vectors 49–55, user/ISP/MSP frames and local, +fresh or unrelated-pending destinations run through scalar/batch routes. +The supplied saved SR and converted frame/live SR are checked independently. +An isolated mutation that clears live Z produces 2,016 exact new mismatches, +while all 126 original cases pass. V2 executes every requested group; the V1 +mutation stopped after a local-group failure and is not complete evidence. +The selected 239-input graph shares all 37 CPU inputs with the corresponding +qualified baseline/private candidate, but it is not a new full-suite result. +Incoming trace, foreign hardware frames, FPU arithmetic, physical timing and +the broader 480-case requirement remain separate. No CPU import occurs. +See the [qualification record](../../docs/COPPER68K_REFERENCE_QUALIFICATION.md#all-ccr-context-transfer-and-live-z-mutation-2026-10-09). diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040ContextTransferCcrTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040ContextTransferCcrTests.cs new file mode 100644 index 00000000..f5d1efc5 --- /dev/null +++ b/Copper68k.Tests/Synthetic/SyntheticM68040ContextTransferCcrTests.cs @@ -0,0 +1,29 @@ +using Xunit.Abstractions; + +namespace Copper68k.Tests.Synthetic; + +// Supplied suspended integer-delivery context and emulator task APIs. +// Saved CCR is the complement of the incoming CCR, so a stale current SR +// cannot satisfy restoration checks. No incoming trace or silicon ABI claim. +public sealed class SyntheticM68040ContextTransferCcrTests(ITestOutputHelper output) +{ + private const string Enable = "COPPER68K_RUN_040_CONTEXT_TRANSFER_CCR"; + + [EnvironmentFact(Enable, "require all CCR images through local and transferred suspended delivery"), Trait("Suite", "ReferenceDiscovery")] + public void ScalarAllCcrControlsAndTransfer() => Audit(false); + + [EnvironmentFact(Enable, "require all CCR images through local and transferred suspended delivery"), Trait("Suite", "ReferenceDiscovery")] + public void BatchAllCcrControlsAndTransfer() => Audit(true); + + private void Audit(bool batch) + { + var fixture = new SyntheticM68040ContextTransferDiscoveryTests(output); + var failures = new List(); + foreach (var transfer in new[] { false, true }) + { + try { fixture.Audit(batch, transfer, allCcr: true); } + catch (Exception ex) { failures.Add(ex); } + } + Assert.True(failures.Count == 0, string.Join("\n", failures.Select(ex => ex.Message))); + } +} diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040ContextTransferDiscoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040ContextTransferDiscoveryTests.cs index f3db9de7..683bd981 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040ContextTransferDiscoveryTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040ContextTransferDiscoveryTests.cs @@ -23,16 +23,17 @@ public sealed class SyntheticM68040ContextTransferDiscoveryTests(ITestOutputHelp [EnvironmentFact(Enable, "diagnose lost or unrelated CP vectors after public task-context transfer"), Trait("Suite", "ReferenceDiscovery")] public void TransferredVectorRequirementBatch() => Audit(true, true); - private void Audit(bool batch, bool transfer) + internal void Audit(bool batch, bool transfer, bool allCcr = false) { var route = batch ? "batch" : "scalar"; - var report = new CoverageBatch("68040", $"cp-context-{(transfer ? "transfer-discovery" : "local-controls")}-{route}"); + var report = new CoverageBatch("68040", $"cp-context-{(transfer ? "transfer-discovery" : "local-controls")}{(allCcr ? "-all-ccr" : "")}-{route}"); foreach (var vector in Enumerable.Range(49, 7)) foreach (var bank in new[] { "user", "ISP", "MSP" }) foreach (var destination in transfer ? new[] { "fresh", "unrelated-pending" } : ["local"]) + foreach (var ccr in allCcr ? Enumerable.Range(0, 32) : [31]) { var source = new SyntheticMachine(ModelSpec.All.Single(x => x.Id == "68040")); - var (_, frame, sr) = SyntheticM68040AccessFrameAuditTests.Prepare(source, 7, bank, 31, 0, Vbr, Ea, 0x8005); + var (_, frame, sr) = SyntheticM68040AccessFrameAuditTests.Prepare(source, 7, bank, ccr, 0, Vbr, Ea, 0x8005); // This vector was selected before delivery was suspended. It must // not be inferred from mutable FPCR/FPSR or an unrelated event. source.Core.State.M68040PendingFpuExceptions.Begin(3, vector, Target); @@ -62,7 +63,7 @@ private void Audit(bool batch, bool transfer) e.Write(frame + 54, (uint)(0x3000 | vector * 4), 2, machine.Model); e.Write(frame + 56, Ea, 4, machine.Model); e.Pc = 0xb000u + (uint)vector * 32; e.ExceptionVector = vector; - var id = $"68040/RTE/CP-context/vector={vector}/bank={bank}/destination={destination}/ccr=1F/phase=converted-delivery/op=4E73"; + var id = $"68040/RTE/CP-context/vector={vector}/bank={bank}/destination={destination}/ccr={ccr:X2}/phase=converted-delivery/op=4E73"; try { if (batch) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 53196fb1..24f33e68 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -30,6 +30,19 @@ it does not replace or discard their remaining gaps. ## Completion gates and evidence status +A later selected 239-input graph extends context transfer across all 32 initial +CCRs with complementary saved CCRs. The candidate passes all 23 named rows / +4,158 cases; baseline has 18 passes / five failures. All 4,032 new cases cover +vectors 49–55, three supplied banks, three destination states and both routes. +The retained 126 canonical cases remain byte-identical. A single isolated +live-Z-clearing mutation preserves those canonical passes but produces exactly +2,016 new SR mismatches. All independent groups execute and aggregate failures; +no transfer report disappears after a local failure. Independent proof: +`6c530977128b59dca1569edeb3b6551a5982aa479f2b3ed32fc182fb0d1d2397`. +This is selected zero-trace host-context coverage. The earlier full proof remains +bound to its frozen 238-input graph, and the broader 480-case gate is unchanged. +No production CPU source changes, candidate import or publication occurs. + Full private context-candidate qualification is complete on its frozen 238-input / 37-CPU graph: 5,363 passes, 89 unavailable rows, zero failures, 87,250,840 passing cases / 1,027 reports and ten pinned native reference presets. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 372bcfd8..a59d2123 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8,6 +8,59 @@ The [milestone 6 completion audit](COPPER68K_MILESTONE6_COMPLETION_AUDIT.md) maps the accepted requirements to current evidence and outstanding gates. The dated records below remain scoped historical evidence. +### All-CCR context transfer and live-Z mutation, 2026-10-09 + +The later frozen **239-input / 37-CPU** graph extends the shared context-transfer +fixture with all 32 initial CCR patterns. Supplied saved CCR is `initial ^ 31`, +so retaining the incoming CCR cannot satisfy restoration. Seven supplied pending +vectors 49–55, three outer banks (user/ISP/MSP), local controls and fresh/unrelated +destination contexts run through scalar/batch execution. The new four reports +contain **4,032 cases**; the existing four canonical reports retain **126 cases** +byte-identically for each corresponding baseline/candidate. Snapshot ownership +and all sixteen retained delivery tests also run in the clean selection. + +| Variant | Actual exit / named outcomes | Reported case outcomes | +| --- | --- | --- | +| Production baseline V2 | Exit 1; 18 passes / five failures | 1,386 passing local cases, 1,386 unsupported fresh transfers and 1,386 exact wrong-vector unrelated-pending transfers. | +| Private context snapshot candidate V2 | Exit 0; all 23 rows pass | All 4,158 cases pass in eight reports, zero mismatching/unsupported/untested. | +| Isolated live-Z-clearing mutation V2 | Exit 1; four canonical rows pass / two new rows fail | 2,142 passes / exactly 2,016 SR mismatches in eight reports; all 126 canonical cases still pass. | + +The mutation changes exactly one expression in an isolated +`M68kAdvancedTimingInterpreter.Rte.cs`: the converted live-SR mask changes from +`~0xc000` to `~0xc004`. Saved frame SR, selected vector and every other CPU source +remain unchanged. The independent expected mismatch set consists exactly of +the new cases with initial Z clear, whose complementary saved Z must be set. +Every retained diagnostic identifies the same expected/observed vector and +expected live SR versus that SR with bit 2 cleared. All other new combinations +pass. This demonstrates added flag-discriminating coverage that the canonical +CCR-31 / saved-CCR-0 cases alone lacked. + +V1 clean execution passed all 4,032 new cases, but its mutation failed the local +group before the transfer group ran. That incomplete mutation attempt remains +separate. The V2 harness aggregates failures from independent groups, executing +both on fresh per-case machines. It never retries a partially executed opcode. +All eight requested V2 reports exist even for negative variants; unknown or +missing reports fail independent verification. No acceptance mask is weakened. + +Independent proof SHA-256: +`6c530977128b59dca1569edeb3b6551a5982aa479f2b3ed32fc182fb0d1d2397`. +The verifier binds exact commands/settings, source graphs, loaded DLLs and +definitions, every named outcome/counter, precise independent vector/bank/ +destination/CCR report keys and unit weights, all expected status totals, the +single mutation source delta, exact SR diagnostics and canonical report hashes. +The clean variants preserve the corresponding production/private candidate's +37 CPU source/project hashes. Only one shared test fixture changes and one +test class is added. The preceding full proof remains the frozen 238-input +qualification, not execution of this later graph. No full-current-suite or new +consumer qualification is claimed from this selected slice. + +These tests exercise supplied suspended integer-delivery state and public +emulator context APIs with zero incoming trace. They do not establish a silicon +FSAVE/context-migration ABI, FPU arithmetic, foreign-frame restoration or +physical timing. The 480-case broader protocol gate and disputed references +remain unchanged. No production CPU fix/import or package publication occurs. +Milestone 6 remains **in progress**, `roadmapComplete=false`. + ### Full private context candidate qualification, 2026-10-09 The same frozen private candidate now completes full CPU execution with actual diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index b974291e..fe027875 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1,5 +1,23 @@ # Copper68k synthetic instruction suite +### Milestone 6 checkpoint — all-CCR context transfer, 2026-10-09 + +The selected context-transfer fixture now covers all 32 initial CCR images, +complementary saved CCRs, vectors 49–55, user/ISP/MSP frames and local/fresh/ +unrelated-pending destinations through scalar and batch execution. The frozen +239-input graph preserves the prior production and private candidate's 37 CPU +inputs. Its private candidate passes 23 named rows / 4,158 cases, including +4,032 new cases and 126 byte-identical retained canonical cases. Production +baseline execution retains 18 passes / five failures. An isolated live-Z-clearing +mutation leaves all 126 canonical cases passing but produces exactly 2,016 new +flag mismatches. Independent checks verify every requested report key, weight, +status and saved/live SR diagnostic. Independent groups aggregate failures so +neither disappears after the other fails. The earlier V1 mutation stopped after +the local group and remains incomplete evidence; V2 executes both groups. +This is selected host-context/zero-trace evidence, not qualification of the +whole 239-input suite or the wider 480-case gate. No CPU import or publication +occurs; milestone 6 stays **in progress**, `roadmapComplete=false`. + ### Milestone 6 checkpoint — full private context candidate, 2026-10-09 The frozen 238-input / 37-CPU private context candidate completes full execution From 39ddb61d9ee491ed8f66f73af61dd014ab1e69c9 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 04:38:13 +0300 Subject: [PATCH 205/211] Link all-CCR candidate to same-build reference audits --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 11 ++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 28 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 15 ++++++++++ 3 files changed, 54 insertions(+) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 24f33e68..f11475c6 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -30,6 +30,17 @@ it does not replace or discard their remaining gaps. ## Completion gates and evidence status +The 239-input all-CCR graph now has independently verified same-build references, +eight invalid-request controls and compiled API review. Deep execution passes +320,000 seeded cases, 312,500 SingleStepTests cases and 536 Musashi programs, +with 88 explicit exclusions. Full/deep/guards share exact DLL identities; API +comparison finds no changes across 20 types / 211 records. All 37 CPU inputs +match the retained `.81` standard-consumer package. Linkage proof: +`c0e002d8c8c24b159e81dfef72087042c66243aa7a11bc67c9bb54d722db97ec`. +Full execution of this graph is active with all-CCR discovery enabled; the +238-input full proof is not relabeled. No new consumer execution is claimed. +Missing native ROM input and unresolved broader gates still prevent completion. + A later selected 239-input graph extends context transfer across all 32 initial CCRs with complementary saved CCRs. The candidate passes all 23 named rows / 4,158 cases; baseline has 18 passes / five failures. All 4,032 new cases cover diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index a59d2123..c90a3786 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8,6 +8,34 @@ The [milestone 6 completion audit](COPPER68K_MILESTONE6_COMPLETION_AUDIT.md) maps the accepted requirements to current evidence and outstanding gates. The dated records below remain scoped historical evidence. +### Current all-CCR candidate reference linkage, 2026-10-09 + +The frozen 239-input / 37-CPU all-CCR candidate has independently verified +reference evidence on the exact DLLs used by its active full execution. + +| Evidence | Verified result | SHA-256 | +| --- | --- | --- | +| Same-build deep and pinned references | Three named passes, no skips; seed 68020 / 10,000 samples yields 320,000 cases / 32 reports. SingleStepTests 000 yields 312,500 cases / 125 files; Musashi yields 536 programs / 88 explicit exclusions across eight profiles. | `2de4bc5bb95aba6fd9ec6bb72fbfa5d52626a959c5893e959226bf5b5ef09d40` | +| Eight actual invalid requests | Each exits 1 with one exact named failure and no case reports: zero seed/samples, empty models, missing/empty SingleStepTests, missing Musashi and adjacent/separated duplicate models. | `9260f6bbef7a5f5c68d6a0e05ea66b63872f55a9841e4b3107bb510d1ce92d0b` | +| Compiled API and normalized CPU source review | Separate reflection processes find zero public/protected signature changes across 20 types / 211 records; exactly two intended CPU source changes. No serialization/layout/hardware claim. | `685f2689f5d0bac8de47406f3c2d292088dfd5a64ce7bb935f0a10268be141d5` | +| Same-source/assembly and retained package linkage | Full/deep/guards share DLL identities; all 37 CPU source/project inputs match the previously verified local `.81` package. Its consumer scope remains clean committed CopperScreen `a4e80b6`, not unrelated dirty code. | `c0e002d8c8c24b159e81dfef72087042c66243aa7a11bc67c9bb54d722db97ec` | + +Corpus pins, exclusions, case IDs/weights, named methods, source and binary +identities are checked as in the preceding qualified checkpoint. SingleStepTests +remains pinned to `64b253116a3de04aaac4346c43680960dc9b67e5` with TAS/TRAPV +explicitly excluded; Musashi remains pinned to +`72c1d74800f3087b45a0c1a7342601bbed898881` and supplies self-checking programs, +not its CPU as an oracle. The retained consumer package is not repacked or +published. Source linkage does not become a new consumer execution or native +replay; its original ROM directory remains unavailable. + +Full execution remains active on the same candidate DLLs with the current CCR +request enabled and ten pinned native reference presets. It has 5,422 discovery +display entries, which are not the final executed-row count. Its exact full +roster/report verifier is prepared but has not yet qualified terminal results. +The preceding 238-input full proof remains immutable and separately scoped. +No production CPU changes/import, new release or roadmap completion is claimed. + ### All-CCR context transfer and live-Z mutation, 2026-10-09 The later frozen **239-input / 37-CPU** graph extends the shared context-transfer diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index fe027875..3106f2b6 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1,5 +1,20 @@ # Copper68k synthetic instruction suite +### Milestone 6 checkpoint — current all-CCR reference linkage, 2026-10-09 + +The frozen 239-input candidate now has same-build independently verified deep +evidence: 320,000 seeded cases, 312,500 SingleStepTests 000 cases and 536 Musashi +programs, with 88 exclusions retained. Eight actual invalid requests fail with +exact diagnostics. Compiled API comparison finds zero changes across 20 types / +211 records and only the two intended normalized CPU source changes. Full, +deep and guard runs share exact candidate DLL identities; all 37 CPU source +inputs match the retained local `.81` standard-consumer package. This source +linkage is not a new consumer replay. Full execution of the 239-input graph +is active with the CCR request enabled and ten pinned reference presets; the +earlier 238-input full proof remains separate. Native boot is unavailable, +and broader reference/restoration gates remain open. No import/publication +occurs; milestone 6 stays **in progress**, `roadmapComplete=false`. + ### Milestone 6 checkpoint — all-CCR context transfer, 2026-10-09 The selected context-transfer fixture now covers all 32 initial CCR images, From 225d826146abb95bb00fe8e27df332b9ddba8194 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 05:07:05 +0300 Subject: [PATCH 206/211] Record full all-CCR reference qualification --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 15 +++++++-- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 31 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 17 ++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index f11475c6..f2f6131c 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -23,7 +23,7 @@ it does not replace or discard their remaining gaps. | Reuse SingleStepTests, Musashi and WinUAE with pinned identities and caveats | Adapter source, reference manifests, source/input hashes, actual execution and documented exclusions | Implemented. SingleStepTests supplies 000 semantic fixtures; Musashi runs independent self-checking programs, not its CPU as an oracle. WinUAE remains a software reference. | | Replacement detects absolute decoding, extension length, index sign, alias order, A7 stride and flags defects | Six distinct isolated mutation proofs with precise replacement IDs and current source linkage | The maintained mutation command defines all six. Fresh isolated 51444ea executions detect all six against a 75,214-case clean baseline. Exact source/roster/report linkage and every mismatch ID are verified. | | Retire an old regression only after a mapped replacement detects its defect | Exact pinned old method/rows, before-removal proof, mutation witness and after-removal sibling verification | NOT displacement, EOR postincrement, ANDI displacement and aliased MOVEA word retirements have exact proofs. EOR replaces one fact with 32,768 passing cases and retains four siblings. Schema-2 retirement replay now requires execution DLL hashes and exact loaded definition/method linkage; fresh MOVEA/EOR executions, 20 corruption controls each and strict replay are qualified. No blanket regression deletion is authorized. | -| Retain cache, prefetch, bus ordering, fault sequencing, JIT and native regressions unless separately proven redundant | Exact test-source diff and full named roster, with every removal accounted for | Specialized tests remain. The private context candidate retains all 5,408 prior rows and adds 44: 18 pass / 26 unavailable. Its exact 5,452-row roster has 89 explicitly unavailable rows. Prior retirement mappings and evidence remain separate. | +| Retain cache, prefetch, bus ordering, fault sequencing, JIT and native regressions unless separately proven redundant | Exact test-source diff and full named roster, with every removal accounted for | Specialized tests remain. The current all-CCR private context candidate retains all 5,408 prior rows and adds 46: 20 pass / 26 unavailable. Its exact 5,454-row roster has 89 explicitly unavailable rows. Prior retirement mappings and evidence remain separate. | | Production CPU fixes pass full CPU and affected consumers | Same-source full CPU proof; isolated NuGet package; CopperScreen production, host/disk/engine and applicable native results | MOVE16 recovery has full-suite and local .77 consumer evidence. Subsequent consolidation leaves all 37 CPU source inputs unchanged. No new consumer execution is claimed. | | Preserve successful ordering and timing policy; no automatic retry after partial effects | Relevant sequence/fault tests and change review, separate from timing qualification | Existing checks and scoped explicit-RTE continuation proofs remain. Private candidate results are not production behavior or physical timing qualification. | | Test-internal framework; public factory; NuGet consumer boundary; immutable published versions | Source/project/package diffs and consumer dependency inspection | Preserved. Private 39-input CPU candidates remain separate from the production 37-input CPU graph. No publication is authorized by this audit. | @@ -37,8 +37,17 @@ with 88 explicit exclusions. Full/deep/guards share exact DLL identities; API comparison finds no changes across 20 types / 211 records. All 37 CPU inputs match the retained `.81` standard-consumer package. Linkage proof: `c0e002d8c8c24b159e81dfef72087042c66243aa7a11bc67c9bb54d722db97ec`. -Full execution of this graph is active with all-CCR discovery enabled; the -238-input full proof is not relabeled. No new consumer execution is claimed. +Full execution of this graph is independently qualified with all-CCR discovery +enabled: 5,365 passes, 89 unavailable rows, zero failures, 87,254,872 passing +cases / 1,031 reports and ten pinned native reference presets. Every named row, +loaded definition and report key/status/weight is checked. The 983 prior reports +remain unchanged; 48 additions match qualified fixtures. The ordinary gate passes +86,663,530 cases / 783 batches. Full proof: +`7ec3339429e005d4e5d21946c66444723e77928bfa8b9bbaa064fc4601ae72b9`. +Final same-build full/reference/API and retained standard-consumer CPU-source +linkage proof: +`ba5125866548de59170b367d3e23ce94f4a0fd6fc16b9dcf92f302c320d1ec43`. +The 238-input full proof is not relabeled. No new consumer execution is claimed. Missing native ROM input and unresolved broader gates still prevent completion. A later selected 239-input graph extends context transfer across all 32 initial diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index c90a3786..8d9dcce1 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8,6 +8,37 @@ The [milestone 6 completion audit](COPPER68K_MILESTONE6_COMPLETION_AUDIT.md) maps the accepted requirements to current evidence and outstanding gates. The dated records below remain scoped historical evidence. +### Full all-CCR candidate qualification, 2026-10-09 + +The frozen **239-input / 37-CPU** graph finishes full execution with actual exit +0. Independent verification checks **5,454 named rows: 5,365 passing, 89 explicitly +unavailable, zero failing**, including exact loaded test definitions. All 5,408 +prior rows remain; 46 additions have 20 passes / 26 unavailable. Discovery display +entries are not substituted for this executed roster. + +All **1,031 reports / 87,254,872 passing logical cases** are verified by exact +combination keys, statuses and weights. The 983 prior coverage reports remain +unchanged; 48 additions contribute 398,862 cases matching independent focused +fixture inventories. Ten pinned native reference presets retain their exact +expected results apart from checked execution DLL identities. Native manifests, +input files and the integer inventory are verified. The report-only ordinary gate +passes **86,663,530 cases / 783 batches**; it is not another CPU execution. + +| Evidence | SHA-256 | +| --- | --- | +| Full execution inputs | `35bbe6ddbdf53a5468a2b3f4a0bc4c6f3e2b978dc4c6208e4a41d02bd88db137` | +| Independent full roster/report qualification | `7ec3339429e005d4e5d21946c66444723e77928bfa8b9bbaa064fc4601ae72b9` | +| Same-build full/deep/guards/API and retained standard-consumer CPU-source linkage | `ba5125866548de59170b367d3e23ce94f4a0fd6fc16b9dcf92f302c320d1ec43` | + +The final link checks exact current full/deep/guard DLL identities and the API +review's full-input identity. All 37 CPU source inputs match the retained local +`.81` package and its separately verified clean-commit standard consumer tests. +This is source linkage, not a new consumer execution. Native boot remains +unavailable because the original ROM folder is missing. Full consumer integration, +production readiness, candidate import and publication are not claimed. The +earlier 238-input evidence remains immutable. Broader reference/restoration gates +remain open; milestone 6 stays **in progress**, `roadmapComplete=false`. + ### Current all-CCR candidate reference linkage, 2026-10-09 The frozen 239-input / 37-CPU all-CCR candidate has independently verified diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 3106f2b6..c675141a 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1,5 +1,22 @@ # Copper68k synthetic instruction suite +### Milestone 6 checkpoint — full all-CCR candidate, 2026-10-09 + +The frozen 239-input / 37-CPU candidate completes full execution with 5,365 +passes, 89 explicitly unavailable rows and zero failures. Independent verification +checks all 5,454 named rows and loaded definitions, 1,031 reports / 87,254,872 +passing cases, and ten pinned native reference presets. All 5,408 prior rows +remain; 46 additions have 20 passes / 26 unavailable. The 983 prior coverage +reports remain unchanged; 48 additions match independently qualified fixtures. +The ordinary gate passes 86,663,530 cases / 783 batches. Full proof: +`7ec3339429e005d4e5d21946c66444723e77928bfa8b9bbaa064fc4601ae72b9`. +Same-build full/deep/guards/API and retained `.81` standard-consumer CPU-source +linkage proof: +`ba5125866548de59170b367d3e23ce94f4a0fd6fc16b9dcf92f302c320d1ec43`. +No new consumer execution is claimed. Native boot remains unavailable; broader +reference/restoration gates remain open. No candidate import or publication +occurs. Milestone 6 stays **in progress**, `roadmapComplete=false`. + ### Milestone 6 checkpoint — current all-CCR reference linkage, 2026-10-09 The frozen 239-input candidate now has same-build independently verified deep From b1fcdecc33b418e92fcfaf62c26fbf1c8c615394 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 05:11:20 +0300 Subject: [PATCH 207/211] Clarify remaining context qualification without weakening its gate --- Copper68k.Tests/Synthetic/README.md | 8 +++++-- .../SyntheticM68040AccessFrameAuditTests.cs | 2 +- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 10 ++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 23 +++++++++++++++++++ scripts/test-copper68k-040-access-frames.ps1 | 2 +- 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index b43b9a91..60f61b77 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -265,8 +265,12 @@ produces a format error for type-1 modules; ordinary RAM is never used as one. Detailed RTE bus-fault restart/internal-state restoration on 010/020/030, 020/030 coprocessor midinstruction restoration and detailed 040 access-fault -entry/validation, writeback handlers and CP context transfer remain **untested**. They are -listed separately in report qualification boundaries rather than reported as +entry/validation, writeback handlers and CP context transfer retain broader +**untested** requirements. Later bounded fixtures have separately recorded +execution, including failed production CP transfer discovery and a passing private +zero-trace/all-CCR snapshot candidate. Those results do not complete the broader +480-case fault/context inventory or import the candidate into production. These +requirements are listed separately in report qualification boundaries rather than reported as invalid frame formats or passing coverage. External BKPT instruction replacement, physical MOVES function-code buses and LPSTOP CPU-space broadcast are also unavailable qualification. Cache/prefetch/fault/JIT/native suites remain retained; diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 0f3a6649..8ca5ea39 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -40,7 +40,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() ? "Chained format0/2/3 and format7 normal/CM SR handoff, stack consumption and header reads have separate generated WinUAE software coverage; access-frame validation/no-replay checks are synthetic. Separate composed-entry and executed-MMU-entry CM lifetime discovery profiles disagree after odd-PC fault/repair; the executed profile also exposes stacked-SR disagreement. Neither qualifies the full run loop or hardware. CT/CU/CP pending/foreign-context chains, full reference exception qualification and hardware behavior remain unqualified" : form == "writeback-handler" ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks. Actual nested MOVES-store fault/handler/RTE coverage includes all 27 B/W/L width triples at all 32 CCRs, common FC1/5 and supplied user/user-M/ISP/MSP outer frames, with separate frozen execution evidence for each promoted slice. Differing per-slot FC1/5 has separate structural-width and canonical-CCR coverage for all four banks. Repeated faults of the same pending store at depths two/three have separate structural-width and canonical-CCR coverage with common FC1/5 and explicit frame unwinds. Original construction of all slots, wider FC/refault combinations and trace/interrupt interruption remain required. Five legal MOVE16 forms have explicit saved-line completion coverage; cache-push lines and other data-fault construction remain required. Physical function-code spaces, MOVE16 WB1 validity and hardware timing are unqualified" - : "Required fault/context protocol has no independently qualified execution fixture yet"); + : "Public task-context copy/switch has separately qualified failed production discovery: fresh destinations lose the suspended vector and unrelated destinations reuse a stale event. A private two-file snapshot candidate passes selected vectors49-55 across all32 initial CCRs, user/ISP/MSP, local/fresh/unrelated destinations and scalar/batch routes with zero incoming trace, including full-suite/reference/API and retained standard-consumer source linkage. The candidate is not imported; native consumer boot, foreign frames, wider trace/fault/context protocols and independent hardware expectations remain unqualified. These bounded fixtures do not complete this broader requirement"); report.Complete(output); } diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index f2f6131c..db9a38c1 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -30,6 +30,16 @@ it does not replace or discard their remaining gaps. ## Completion gates and evidence status +The broader access-frame diagnostic now acknowledges failed production CP +transfer discovery and private zero-trace/all-CCR candidate qualification, without +clearing the wider requirement. Fresh isolated execution verifies all 480 exact +IDs remain untested and the named gate fails, actual xUnit exit 1. Three control +rows pass with 29,952 cases and byte-identical reports. Only one C# diagnostic +string changes; all 37 CPU inputs and remaining test sources stay unchanged. +Independent proof: +`5166dfc8ba84b853f8747c6a174d1c935b2d007930d48e6607431f57c7b33022`. +This focused production-source check does not relabel the frozen full candidate. + The 239-input all-CCR graph now has independently verified same-build references, eight invalid-request controls and compiled API review. Deep execution passes 320,000 seeded cases, 312,500 SingleStepTests cases and 536 Musashi programs, diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 8d9dcce1..763b346d 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8,6 +8,29 @@ The [milestone 6 completion audit](COPPER68K_MILESTONE6_COMPLETION_AUDIT.md) maps the accepted requirements to current evidence and outstanding gates. The dated records below remain scoped historical evidence. +### Broader context-gate diagnostic correction, 2026-10-09 + +The broad access-frame gate no longer describes CP context transfer as lacking +any qualified fixture. Its diagnostic distinguishes failed production discovery +from the privately qualified zero-trace/all-CCR snapshot candidate and preserves +the remaining foreign-frame, trace/fault/context and native-consumer gaps. The +command's limitation and README use the same distinction. No status, identifier, +case count, enable flag or acceptance condition changes. + +A fresh isolated production-source selection has three passing control tests and +one expected failing broad-gate test, actual xUnit exit 1. Independent verification +checks all **480 exact IDs / 15 combinations**, each still **untested**, plus +**29,952 passing control cases** whose reports remain byte-identical to the frozen +full checkpoint. The gate's only C# change is one diagnostic string; all 37 CPU +inputs and every other test source remain unchanged. This focused selection is +not a new whole-suite execution or candidate import. + +Proof SHA-256: +`5166dfc8ba84b853f8747c6a174d1c935b2d007930d48e6607431f57c7b33022`. +The frozen 239-input full candidate retains its prior source identity. Broader +gates remain failed/incomplete; milestone 6 stays **in progress**, +`roadmapComplete=false`. + ### Full all-CCR candidate qualification, 2026-10-09 The frozen **239-input / 37-CPU** graph finishes full execution with actual exit diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index f50db332..7088cbea 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -132,7 +132,7 @@ if (-not $ValidateReportsOnly) { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } limitations=@('Synthetic frames and supervisor-stack physical map faults, not hardware captures or enabled-MMU access faults', - 'CP context transfer and detailed fault protocol inventory remain untested and fail the gate', + 'The broader 480-case fault/context protocol inventory remains untested and fails the gate; separately qualified failed production CP transfer discovery and private zero-trace/all-CCR snapshot evidence do not complete it', 'Multiple continuation bits are architecturally undefined and excluded', 'Selected active accurate-batch paths verify counts/callbacks and scalar/batch bus/cycle policy; generic short operand frames do not qualify architectural format-7 data restart', 'Instruction-fault fixtures use cache-disabled accurate execution and physical-map rejection; speculative deferral, enabled caches/MMU and compiled fetch PC provenance remain unqualified', From ea54036c198aa8e95ef78691ce09e8a1668aa18b Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 16:22:51 +0300 Subject: [PATCH 208/211] Qualify recovered native Workbench consumer replays --- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 17 +++++++++- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 31 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 16 ++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index db9a38c1..6665ce28 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -30,6 +30,20 @@ it does not replace or discard their remaining gaps. ## Completion gates and evidence status +The native consumer input is recovered with its exact pinned hash. Two Workbench +floppy boot rows pass on the retained local `.81` package and clean consumer +commit `a4e80b6`, with 0 and 2 MiB Fast RAM, no skips and exact cycle/PC/mapping/ +framebuffer assertions after 10,000 fields each. Independent verification binds +media, command, loaded methods, archived sources, package and all 37 CPU inputs +to the frozen all-CCR full/reference/API evidence. The standard build and host/ +disk/engine results remain unchanged; the original failed flow is preserved. +Proof: +`41629e60cdc0da0ef43f35893110d29422907f1c01cd951536034aaf263977bf`. +Consumer integration for this frozen private candidate scope is complete. +Production readiness and broader reference/restoration gates remain unresolved; +no candidate import or publication occurs. Earlier unavailable-native records +retain their historical scope. + The broader access-frame diagnostic now acknowledges failed production CP transfer discovery and private zero-trace/all-CCR candidate qualification, without clearing the wider requirement. Fresh isolated execution verifies all 480 exact @@ -58,7 +72,8 @@ Final same-build full/reference/API and retained standard-consumer CPU-source linkage proof: `ba5125866548de59170b367d3e23ce94f4a0fd6fc16b9dcf92f302c320d1ec43`. The 238-input full proof is not relabeled. No new consumer execution is claimed. -Missing native ROM input and unresolved broader gates still prevent completion. +Native boot is now independently qualified as recorded above; unresolved broader +gates still prevent completion. A later selected 239-input graph extends context transfer across all 32 initial CCRs with complementary saved CCRs. The candidate passes all 23 named rows / diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 763b346d..96f94d6d 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8,6 +8,37 @@ The [milestone 6 completion audit](COPPER68K_MILESTONE6_COMPLETION_AUDIT.md) maps the accepted requirements to current evidence and outstanding gates. The dated records below remain scoped historical evidence. +### Recovered native consumer boot qualification, 2026-10-09 + +The ROM relocated under `C:\Data\ROM` has the exact required SHA-256 +`8c8a0cf04f91b88eaf0c4f1126041987067e2286a8ee590bdbae447a8000c5ee`. +The retained Workbench ADF also retains its pin. A separate native-only resume +uses the already built local `.81` consumer; it preserves the original failed +flow and standard evidence rather than rewriting them as successful. + +Both `SuppliedNativeWorkbench31BootsThroughTheDesktopSession` rows pass, +**two executed / zero skipped / zero failed**, with 0 and 2 MiB Fast RAM. +Each renders 10,000 fields and checks no fault, ROM overlay removal, exact cycles +1,420,928,790 / 1,420,928,894, PC `F81476`, Fast RAM mapping and framebuffer +SHA-256 `aae907938c788bd6f3f29521d2f3fffccc5ee4db0acc0634b19bc024080dd84f`. +These are functional native boot regressions, not throughput measurements. + +Independent verification binds the exact command/settings, media pins, named +TRX rows and loaded definitions, immutable local package, three loaded CPU DLLs, +39 package inputs / 37 CPU inputs and archived clean consumer source commit +`a4e80b68a7b58e6c12b940b9d786b2ac73757965`. The prior build and 172 passing host +tests with six unavailable, 74 disk tests and 1,080 engine tests remain unchanged. +All CPU inputs match the separately qualified frozen all-CCR candidate's full, +reference and API evidence. Native consumer integration for this scope is now +complete. Independent proof: +`41629e60cdc0da0ef43f35893110d29422907f1c01cd951536034aaf263977bf`. + +This does not qualify unrelated primary changes, hard disk boot, the current +diagnostic-only source edit as a whole-suite execution, or broader architectural +restoration protocols. Production readiness, candidate import and publication +remain false; reference disagreements remain unresolved. Milestone 6 stays +**in progress**, `roadmapComplete=false`. + ### Broader context-gate diagnostic correction, 2026-10-09 The broad access-frame gate no longer describes CP context transfer as lacking diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index c675141a..719e4b59 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1,5 +1,21 @@ # Copper68k synthetic instruction suite +### Milestone 6 checkpoint — recovered native consumer input, 2026-10-09 + +The relocated Kickstart 3.1 A500 ROM matches the original pinned hash. Two +native Workbench floppy replays now pass on the retained local-only `.81` +package and clean CopperScreen commit `a4e80b6`, with 0 and 2 MiB Fast RAM. +Each executes 10,000 fields and verifies no host fault, ROM overlay removal, +exact cycle count, CPU PC, Fast RAM mapping and the reviewed desktop framebuffer +hash. Independent verification checks the exact named rows/loaded methods, +media, package/CPU identities, archived consumer sources and unchanged standard +results. Proof: +`41629e60cdc0da0ef43f35893110d29422907f1c01cd951536034aaf263977bf`. +This completes consumer integration for that frozen private candidate scope; +the original failed flow remains retained. No primary dirty code, new CPU build, +candidate import or publication is included. Broader architectural/reference +gates remain open; milestone 6 stays **in progress**, `roadmapComplete=false`. + ### Milestone 6 checkpoint — full all-CCR candidate, 2026-10-09 The frozen 239-input / 37-CPU candidate completes full execution with 5,365 From 69497ded62852c50ef3068d583cead3db7d45359 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 16:30:11 +0300 Subject: [PATCH 209/211] Preserve independent pending deliveries in copied CPU contexts --- Copper68k.Tests/Synthetic/README.md | 24 ++++++++----- ...eticM68040ContextSnapshotDiscoveryTests.cs | 2 +- Copper68k/M68040PendingFpuExceptions.cs | 15 ++++++++ Copper68k/M68kCore.cs | 2 ++ docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 14 +++++++- docs/COPPER68K_REFERENCE_QUALIFICATION.md | 35 +++++++++++++++++++ ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 16 +++++++++ 7 files changed, 98 insertions(+), 10 deletions(-) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 60f61b77..7937ac65 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -1854,21 +1854,29 @@ Remove-Item Env:COPPER68K_RUN_040_CONTEXT_TRANSFER_DISCOVERY Remove-Item Env:COPPER68K_SYNTHETIC_REPORT_DIR ``` -The requested selection must retain all four named rows and four reports, with -two passing local rows and two failing discovery rows until the gap is resolved. +The historical production baseline retained all four named rows and reports, +with two passing local rows and two failing discovery rows. The approved +context-copy fix now makes all four pass; the original failed evidence remains. The source pending vector is an explicit fixture input; no FPU arithmetic, hardware frame migration/FSAVE ABI, original access entry or trace is qualified. -### Private pending-delivery snapshot candidate +### Pending-delivery snapshot ownership `SyntheticM68040ContextSnapshotDiscoveryTests` checks nested LIFO order, distinct identity for equal-valued deliveries, replacement rather than append, self-copy, independent completion/reset, empty-context clearing, and the existing -machine-time policy of `CopyFrom` versus `CopyTaskContextFrom`. Enable -`COPPER68K_RUN_040_CONTEXT_SNAPSHOT_DISCOVERY=1` when explicitly requesting it. -Production currently fails this test; unavailable execution supplies no coverage. - -The reviewed `scripts/reference/m68040-context-pending-candidate.patch` changes +machine-time policy of `CopyFrom` versus `CopyTaskContextFrom`. The approved +two-file context-copy fix is now in production, and this regression runs as an +ordinary Synthetic Fact without an opt-in flag. Fresh selected production +execution passes 23 named rows / 4,158 cases with eight byte-identical candidate +reports. All 37 CPU inputs match the qualified full/reference/API and `.81` +standard/native consumer sources. Other private candidates remain unimported. +Independent promotion proof: +`c1e006019ea3c3db0a331aa58f66e2a8b412d0f9943d7cb4ccc864bc66ebae33`. + +The following records describe the earlier private checkpoint; do not reapply +the patch to current production. The reviewed +`scripts/reference/m68040-context-pending-candidate.patch` changes only an isolated CPU copy. It clones delivery entries when saving/restoring CPU state and clears unrelated destination entries. It adds no public API. Apply it only to a separate source copy matching this checkpoint, after normalizing its diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040ContextSnapshotDiscoveryTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040ContextSnapshotDiscoveryTests.cs index 87f00e7e..83e30619 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040ContextSnapshotDiscoveryTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040ContextSnapshotDiscoveryTests.cs @@ -5,7 +5,7 @@ namespace Copper68k.Tests.Synthetic; // Emulator snapshot ownership contract; not a silicon context/FSAVE ABI. public sealed class SyntheticM68040ContextSnapshotDiscoveryTests { - [EnvironmentFact("COPPER68K_RUN_040_CONTEXT_SNAPSHOT_DISCOVERY", "qualify independent suspended-delivery snapshots"), Trait("Suite", "ReferenceDiscovery")] + [Fact, Trait("Suite", "Synthetic")] public void SnapshotsOwnIndependentNestedPendingDeliveries() { foreach (var taskOnly in new[] { false, true }) diff --git a/Copper68k/M68040PendingFpuExceptions.cs b/Copper68k/M68040PendingFpuExceptions.cs index 2ca8bc93..dff754a0 100644 --- a/Copper68k/M68040PendingFpuExceptions.cs +++ b/Copper68k/M68040PendingFpuExceptions.cs @@ -33,5 +33,20 @@ internal void Complete(Entry? entry) if (ReferenceEquals(_entries[n], entry)) { _entries.RemoveAt(n); return; } } + internal void CopyFrom(M68040PendingFpuExceptions source) + { + if (ReferenceEquals(this, source)) return; + // A saved task owns its delivery identities. Completion/reset in one + // context must not consume an equal-valued event in another context. + List? copied = null; + if (source._entries != null) + { + copied = new List(source._entries.Count); + foreach (var entry in source._entries) + copied.Add(new Entry(entry.Format, entry.Vector, entry.ProgramCounter)); + } + _entries = copied; + } + internal void Reset() => _entries?.Clear(); } diff --git a/Copper68k/M68kCore.cs b/Copper68k/M68kCore.cs index 59e6f09e..f3a623de 100644 --- a/Copper68k/M68kCore.cs +++ b/Copper68k/M68kCore.cs @@ -1404,6 +1404,7 @@ public void CopyFrom(M68kCpuState source) Stopped = source.Stopped; LastOpcode = source.LastOpcode; LastInstructionProgramCounter = source.LastInstructionProgramCounter; + M68040PendingFpuExceptions.CopyFrom(source.M68040PendingFpuExceptions); } /// @@ -1430,6 +1431,7 @@ public void CopyTaskContextFrom(M68kCpuState source) CacheAddressRegister = source.CacheAddressRegister; LastOpcode = source.LastOpcode; LastInstructionProgramCounter = source.LastInstructionProgramCounter; + M68040PendingFpuExceptions.CopyFrom(source.M68040PendingFpuExceptions); } /// diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 6665ce28..3ec60194 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -24,12 +24,24 @@ it does not replace or discard their remaining gaps. | Replacement detects absolute decoding, extension length, index sign, alias order, A7 stride and flags defects | Six distinct isolated mutation proofs with precise replacement IDs and current source linkage | The maintained mutation command defines all six. Fresh isolated 51444ea executions detect all six against a 75,214-case clean baseline. Exact source/roster/report linkage and every mismatch ID are verified. | | Retire an old regression only after a mapped replacement detects its defect | Exact pinned old method/rows, before-removal proof, mutation witness and after-removal sibling verification | NOT displacement, EOR postincrement, ANDI displacement and aliased MOVEA word retirements have exact proofs. EOR replaces one fact with 32,768 passing cases and retains four siblings. Schema-2 retirement replay now requires execution DLL hashes and exact loaded definition/method linkage; fresh MOVEA/EOR executions, 20 corruption controls each and strict replay are qualified. No blanket regression deletion is authorized. | | Retain cache, prefetch, bus ordering, fault sequencing, JIT and native regressions unless separately proven redundant | Exact test-source diff and full named roster, with every removal accounted for | Specialized tests remain. The current all-CCR private context candidate retains all 5,408 prior rows and adds 46: 20 pass / 26 unavailable. Its exact 5,454-row roster has 89 explicitly unavailable rows. Prior retirement mappings and evidence remain separate. | -| Production CPU fixes pass full CPU and affected consumers | Same-source full CPU proof; isolated NuGet package; CopperScreen production, host/disk/engine and applicable native results | MOVE16 recovery has full-suite and local .77 consumer evidence. Subsequent consolidation leaves all 37 CPU source inputs unchanged. No new consumer execution is claimed. | +| Production CPU fixes pass full CPU and affected consumers | Same-source full CPU proof; isolated NuGet package; CopperScreen production, host/disk/engine and applicable native results | MOVE16 recovery retains its .77 evidence. The approved context-copy fix has exact 37-input CPU identity with the qualified full/reference/API candidate and its .81 standard/native consumers; fresh production-source focused tests pass. Other private candidates remain unimported. | | Preserve successful ordering and timing policy; no automatic retry after partial effects | Relevant sequence/fault tests and change review, separate from timing qualification | Existing checks and scoped explicit-RTE continuation proofs remain. Private candidate results are not production behavior or physical timing qualification. | | Test-internal framework; public factory; NuGet consumer boundary; immutable published versions | Source/project/package diffs and consumer dependency inspection | Preserved. Private 39-input CPU candidates remain separate from the production 37-input CPU graph. No publication is authorized by this audit. | ## Completion gates and evidence status +The user approved production promotion of only the context-copy patch. Both +state-copy APIs now preserve independently owned pending delivery snapshots. +The ownership regression runs as an ordinary Synthetic Fact. Fresh production +execution passes 23 selected rows / 4,158 cases, with all eight reports identical +to the qualified candidate. All 37 CPU inputs match its same-source full CPU, +reference/API and .81 standard/native consumer evidence exactly. Independent +promotion linkage proof: +`c1e006019ea3c3db0a331aa58f66e2a8b412d0f9943d7cb4ccc864bc66ebae33`. +No whole-suite claim is added for the changed diagnostic/test-attribute graph. +Other private candidates, disputed architectural rules and broader gates remain +unchanged. No publication occurs; earlier no-import statements are historical. + The native consumer input is recovered with its exact pinned hash. Two Workbench floppy boot rows pass on the retained local `.81` package and clean consumer commit `a4e80b6`, with 0 and 2 MiB Fast RAM, no skips and exact cycle/PC/mapping/ diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 96f94d6d..7ed2b87d 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -8,6 +8,41 @@ The [milestone 6 completion audit](COPPER68K_MILESTONE6_COMPLETION_AUDIT.md) maps the accepted requirements to current evidence and outstanding gates. The dated records below remain scoped historical evidence. +### Approved context-copy production promotion, 2026-10-09 + +The user explicitly authorized **only** the two-file context-copy fix after +the full/reference/API and recovered native consumer checks. The CPU state +copy APIs now copy pending delivery entries as independently owned nested +snapshots, replacing stale entries and preserving self-copy. No public API, +instruction ordering, timing policy or retry mechanism changes. The other +private continuation candidates remain separate. + +`SnapshotsOwnIndependentNestedPendingDeliveries` is now an ordinary Synthetic +Fact. It checks both copy APIs, replacement rather than append, equal-valued +nested identities, self-copy, completion/reset independence, empty-context +clearing and existing task/full-copy cycle rules. Its execution no longer needs +an environment flag. The all-CCR transfer discovery remains separately enabled. + +Fresh isolated production-source execution passes **23 named tests / 4,158 +cases / eight reports**, with zero failures or unavailable rows. Independent +verification checks exact loaded methods and that every report is byte-identical +to the previously qualified candidate. All **37 CPU source/project inputs** are +byte-identical to that candidate and its retained `.81` consumer package; its +full CPU, deep/reference, API, standard consumer and two native boot results +therefore retain same-CPU-source linkage. This is not a new whole-suite execution +of the changed test graph. The only test-source differences from the frozen +candidate are the independently checked broad-gate diagnostic and this Fact +attribute; no expectation or execution fixture changes. + +The initial source preflight rejected mixed line endings before execution. +An exact newline-only comparison permitted restoring the already qualified +source bytes; the repaired execution then passed without weakened checks. +Independent production promotion proof: +`c1e006019ea3c3db0a331aa58f66e2a8b412d0f9943d7cb4ccc864bc66ebae33`. +Earlier no-import records remain historical. Broad architectural/restoration +gates are unchanged; no package publication occurs. Milestone 6 remains +**in progress**, `roadmapComplete=false`. + ### Recovered native consumer boot qualification, 2026-10-09 The ROM relocated under `C:\Data\ROM` has the exact required SHA-256 diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index 719e4b59..d94fb407 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1,5 +1,21 @@ # Copper68k synthetic instruction suite +### Milestone 6 checkpoint — approved context-copy production fix, 2026-10-09 + +The user approved promotion of only the reviewed two-file context-copy fix. +`CopyFrom` and `CopyTaskContextFrom` now copy pending delivery state with +independent nested identities, replace stale destination state and preserve +self-copy behavior. The snapshot-ownership regression runs in ordinary CI. +Fresh production-source execution passes 23 selected tests / 4,158 cases; +all eight reports match the qualified candidate byte-for-byte. All 37 CPU +source inputs exactly match the candidate with retained full-suite, reference, +API and standard/native consumer qualification. Independent linkage proof: +`c1e006019ea3c3db0a331aa58f66e2a8b412d0f9943d7cb4ccc864bc66ebae33`. +The current diagnostic/attribute edits have focused verification; the older full +run retains its frozen test-source identity. Other private candidates are not +imported, disputed architectural behavior is unchanged, and no package is +published. Milestone 6 remains **in progress**, `roadmapComplete=false`. + ### Milestone 6 checkpoint — recovered native consumer input, 2026-10-09 The relocated Kickstart 3.1 A500 ROM matches the original pinned hash. Two From 9a097834f71f6ea9e46c828a82aa61b91369b333 Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 16:32:55 +0300 Subject: [PATCH 210/211] Align remaining qualification descriptions with approved context fix --- Copper68k.Tests/Synthetic/README.md | 6 +++--- .../Synthetic/SyntheticM68040AccessFrameAuditTests.cs | 2 +- docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 4 ++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 6 ++++++ scripts/test-copper68k-040-access-frames.ps1 | 2 +- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 7937ac65..40ede70e 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -267,9 +267,9 @@ Detailed RTE bus-fault restart/internal-state restoration on 010/020/030, 020/030 coprocessor midinstruction restoration and detailed 040 access-fault entry/validation, writeback handlers and CP context transfer retain broader **untested** requirements. Later bounded fixtures have separately recorded -execution, including failed production CP transfer discovery and a passing private -zero-trace/all-CCR snapshot candidate. Those results do not complete the broader -480-case fault/context inventory or import the candidate into production. These +execution, including historical failed CP transfer discovery and the subsequently +approved zero-trace/all-CCR context-copy production fix. Those results do not +complete the broader 480-case fault/context inventory. These requirements are listed separately in report qualification boundaries rather than reported as invalid frame formats or passing coverage. External BKPT instruction replacement, physical MOVES function-code buses and LPSTOP CPU-space broadcast are also diff --git a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs index 8ca5ea39..78c6a044 100644 --- a/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs +++ b/Copper68k.Tests/Synthetic/SyntheticM68040AccessFrameAuditTests.cs @@ -40,7 +40,7 @@ public void RemainingFaultProtocolsCannotDisappearFromTheGate() ? "Chained format0/2/3 and format7 normal/CM SR handoff, stack consumption and header reads have separate generated WinUAE software coverage; access-frame validation/no-replay checks are synthetic. Separate composed-entry and executed-MMU-entry CM lifetime discovery profiles disagree after odd-PC fault/repair; the executed profile also exposes stacked-SR disagreement. Neither qualifies the full run loop or hardware. CT/CU/CP pending/foreign-context chains, full reference exception qualification and hardware behavior remain unqualified" : form == "writeback-handler" ? "Executed supplied normal B/W/L handler covers validity, WB1 lane alignment, WB1/2/3 order, overlap, DFC and restored trace/stacks. Actual nested MOVES-store fault/handler/RTE coverage includes all 27 B/W/L width triples at all 32 CCRs, common FC1/5 and supplied user/user-M/ISP/MSP outer frames, with separate frozen execution evidence for each promoted slice. Differing per-slot FC1/5 has separate structural-width and canonical-CCR coverage for all four banks. Repeated faults of the same pending store at depths two/three have separate structural-width and canonical-CCR coverage with common FC1/5 and explicit frame unwinds. Original construction of all slots, wider FC/refault combinations and trace/interrupt interruption remain required. Five legal MOVE16 forms have explicit saved-line completion coverage; cache-push lines and other data-fault construction remain required. Physical function-code spaces, MOVE16 WB1 validity and hardware timing are unqualified" - : "Public task-context copy/switch has separately qualified failed production discovery: fresh destinations lose the suspended vector and unrelated destinations reuse a stale event. A private two-file snapshot candidate passes selected vectors49-55 across all32 initial CCRs, user/ISP/MSP, local/fresh/unrelated destinations and scalar/batch routes with zero incoming trace, including full-suite/reference/API and retained standard-consumer source linkage. The candidate is not imported; native consumer boot, foreign frames, wider trace/fault/context protocols and independent hardware expectations remain unqualified. These bounded fixtures do not complete this broader requirement"); + : "Historical production discovery lost suspended vectors on fresh destinations and reused stale unrelated events. The approved two-file context-copy fix now preserves independent snapshots and passes selected vectors49-55 across all32 initial CCRs, user/ISP/MSP, local/fresh/unrelated destinations and scalar/batch routes with zero incoming trace, with same-CPU full/reference/API and standard/native consumer qualification. Foreign frames, wider trace/fault/context protocols and independent hardware expectations remain unqualified. These bounded fixtures do not complete this broader requirement"); report.Complete(output); } diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 3ec60194..669471e6 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -39,6 +39,10 @@ reference/API and .81 standard/native consumer evidence exactly. Independent promotion linkage proof: `c1e006019ea3c3db0a331aa58f66e2a8b412d0f9943d7cb4ccc864bc66ebae33`. No whole-suite claim is added for the changed diagnostic/test-attribute graph. +The subsequent broad-gate diagnostic/command wording describes the approved +production fix; exact replacement verification preserves every case/status and +CPU source, with no new execution claim. Scope proof: +`2e4831b30e02d57158be76b3073dceab8c63c3f071101891389a807c4b6197e2`. Other private candidates, disputed architectural rules and broader gates remain unchanged. No publication occurs; earlier no-import statements are historical. diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 7ed2b87d..1b9f9027 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -39,6 +39,12 @@ An exact newline-only comparison permitted restoring the already qualified source bytes; the repaired execution then passed without weakened checks. Independent production promotion proof: `c1e006019ea3c3db0a331aa58f66e2a8b412d0f9943d7cb4ccc864bc66ebae33`. +After promotion, one broad-gate diagnostic and one command limitation are updated +to describe the fix as approved production behavior. Static exact-replacement +verification proves that case generation, statuses, acceptance conditions and +all CPU sources are unchanged. It adds no execution claim or gate promotion. +Scope proof: +`2e4831b30e02d57158be76b3073dceab8c63c3f071101891389a807c4b6197e2`. Earlier no-import records remain historical. Broad architectural/restoration gates are unchanged; no package publication occurs. Milestone 6 remains **in progress**, `roadmapComplete=false`. diff --git a/scripts/test-copper68k-040-access-frames.ps1 b/scripts/test-copper68k-040-access-frames.ps1 index 7088cbea..3ef319f7 100644 --- a/scripts/test-copper68k-040-access-frames.ps1 +++ b/scripts/test-copper68k-040-access-frames.ps1 @@ -132,7 +132,7 @@ if (-not $ValidateReportsOnly) { @{file=$_; sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()} } limitations=@('Synthetic frames and supervisor-stack physical map faults, not hardware captures or enabled-MMU access faults', - 'The broader 480-case fault/context protocol inventory remains untested and fails the gate; separately qualified failed production CP transfer discovery and private zero-trace/all-CCR snapshot evidence do not complete it', + 'The broader 480-case fault/context protocol inventory remains untested and fails the gate; historical failed CP transfer discovery and the approved zero-trace/all-CCR context-copy fix do not complete it', 'Multiple continuation bits are architecturally undefined and excluded', 'Selected active accurate-batch paths verify counts/callbacks and scalar/batch bus/cycle policy; generic short operand frames do not qualify architectural format-7 data restart', 'Instruction-fault fixtures use cache-disabled accurate execution and physical-map rejection; speculative deferral, enabled caches/MMU and compiled fetch PC provenance remain unqualified', From e15a3329831823ea82f144e56158e2d0ff9c818b Mon Sep 17 00:00:00 2001 From: Ilkka Lehtoranta Date: Fri, 9 Oct 2026 16:59:57 +0300 Subject: [PATCH 211/211] Accept qualified coverage and explicitly defer unresolved behavior --- .github/workflows/ci.yml | 2 +- Copper68k.Tests/Synthetic/README.md | 8 +++ docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md | 22 ++++++- docs/COPPER68K_QUALIFICATION_SCOPE.json | 58 +++++++++++++++++++ docs/COPPER68K_REFERENCE_QUALIFICATION.md | 32 +++++++++- ...PER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md | 22 ++++++- scripts/copper68k-qualification-scope.ps1 | 28 +++++++++ scripts/test-copper68k-reference-audits.ps1 | 4 +- scripts/test-copper68k-synthetic.ps1 | 3 + 9 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 docs/COPPER68K_QUALIFICATION_SCOPE.json create mode 100644 scripts/copper68k-qualification-scope.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3aec5e7c..188a342e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: COPPER68K_SYNTHETIC_REPORT_DIR: ${{ github.workspace }}/artifacts/synthetic-suite run: dotnet test Copper68k.Tests\Copper68k.Tests.csproj --configuration Release - - name: Require complete synthetic milestone coverage + - name: Require accepted synthetic coverage and report deferred qualification shell: pwsh run: ./scripts/test-copper68k-synthetic.ps1 -ValidateReportsOnly -OutputDirectory artifacts/synthetic-suite diff --git a/Copper68k.Tests/Synthetic/README.md b/Copper68k.Tests/Synthetic/README.md index 40ede70e..17a952cf 100644 --- a/Copper68k.Tests/Synthetic/README.md +++ b/Copper68k.Tests/Synthetic/README.md @@ -280,6 +280,14 @@ address base, reserved module fields and simultaneous T1/T0 are excluded. ## Milestone 6 reference work +The known suite is accepted under the user's 2026-10-09 approval. Unclear +qualification is deferred and opt-in, with IDs/reasons/routes retained in +`docs/COPPER68K_QUALIFICATION_SCOPE.json` and embedded in command summaries. +This does not weaken any required milestone 1-5 report or count. Explicitly +requested discovery/reference audits still fail on mismatch, unsupported +execution, missing fixtures or empty selections; deferred entries are not passes. +Historical evidence below retains its original source and acceptance scope. + The pinned Musashi command now audits all selected profiles across both integer program directories. It records passing/excluded programs and input identities; missing/incomplete fixtures and mismatches fail. See diff --git a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md index 669471e6..dc72c620 100644 --- a/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md +++ b/docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md @@ -3,8 +3,26 @@ This is the current completion checklist for **Reference qualification and consolidation**, derived from the accepted [synthetic-suite plan](COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md). -Milestone 6 is **in progress**. A passing focused audit, inventory entry or -software-reference correction does not complete the milestone. +The user approved acceptance of **known coverage** on 2026-10-09 and opted +unclear qualification out for now. That approved scope is **accepted**; +exhaustive qualification remains incomplete. The +[scope registry](COPPER68K_QUALIFICATION_SCOPE.json) retains deferred areas, +reasons and explicit opt-in routes. They do not block known-coverage acceptance, +are not counted as passing, and still fail their audits when explicitly requested. + +Required milestone 1-5 matrices, documented exception checks, mutation witnesses, +mapped retirements and qualified consumers are unchanged. No blanket regression +deletion, other private candidate import or publication is authorized. The broad +480-case inventory and historical failed reference evidence remain retained. +The older completion gates below describe the original exhaustive scope; under +this approval their unclear portions are deferred rather than release blockers +for the accepted suite. `roadmapComplete=false` retains that distinction. + +The revised report-only gate passes the unchanged 86,663,530 cases / 783 batches. +Its summary adds only scope metadata. Missing required coverage and dropped, +passing-labeled or duplicate deferrals all reject in actual negative requests. +Policy/report verification proof: +`8740d0938cb8b1ad5ba04de44f8e472bb144f57623a1287da05156c2ca4e7a45`. The detailed [qualification record](COPPER68K_REFERENCE_QUALIFICATION.md) retains historical checkpoints and rejected attempts. This checklist separates diff --git a/docs/COPPER68K_QUALIFICATION_SCOPE.json b/docs/COPPER68K_QUALIFICATION_SCOPE.json new file mode 100644 index 00000000..05f058f6 --- /dev/null +++ b/docs/COPPER68K_QUALIFICATION_SCOPE.json @@ -0,0 +1,58 @@ +{ + "schema": 1, + "approvalDate": "2026-10-09", + "acceptance": "known-coverage-only", + "approvedModels": ["68000", "68010", "68EC020", "68020", "68030", "68040", "68060", "A1200"], + "approvedCoverage": [ + "Required milestone 1-5 integer semantic matrices and defined architectural exceptions", + "Pinned passing reference selections with their recorded exclusions and limitations", + "Verified mutation witnesses and mapped instruction-test consolidation", + "Promoted context-copy ownership fix and its ordinary regression", + "Qualified same-CPU-source standard and native consumer checks" + ], + "deferred": [ + { + "id": "010-invalid-format-rte-ccr", + "status": "deferred", + "reason": "Software references disagree on CCR after rejected RTE formats; an independent expectation remains unresolved.", + "optIn": "COPPER68K_RUN_010_RTE_FORMAT_DISCOVERY=1", + "record": "docs/COPPER68K_REFERENCE_QUALIFICATION.md" + }, + { + "id": "060-ordinary-stop-new-user-sr", + "status": "deferred", + "reason": "Ordinary STOP with a new S-clear SR disagrees with an undocumented software-reference rule.", + "optIn": "COPPER68K_RUN_060_STOP_DISCOVERY=1", + "record": "docs/COPPER68K_REFERENCE_QUALIFICATION.md" + }, + { + "id": "030-040-disputed-trace-boundaries", + "status": "deferred", + "reason": "Original trace provenance across fault, repair and retry has unresolved independent expectations; software agreement is not hardware evidence.", + "optIn": "scripts/test-copper68k-030-return-trace-reference.ps1; COPPER68K_RUN_040_MIXED_RETRY_DISCOVERY=1", + "record": "docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md" + }, + { + "id": "broader-architectural-restoration", + "status": "deferred", + "reason": "The 480-case broader inventory, original-frame construction, partial transfers, repair/refault, foreign frames and wider trace/interrupt protocols remain unqualified. Known bounded fixtures do not complete them.", + "optIn": "COPPER68K_RUN_040_ACCESS_FRAME_AUDIT=1; scripts/test-copper68k-040-access-frames.ps1", + "record": "docs/COPPER68K_MILESTONE6_COMPLETION_AUDIT.md" + }, + { + "id": "other-private-continuation-candidates", + "status": "deferred", + "reason": "Only the context-copy fix was approved for production. Other private continuation formats and candidates are not production qualification.", + "optIn": "scripts/test-copper68k-030-memory-destination-reference.py", + "record": "docs/COPPER68K_REFERENCE_QUALIFICATION.md" + } + ], + "rules": [ + "Deferred coverage is visible and nonblocking for acceptance of known coverage; it is never counted as passing.", + "No legal case is removed from the required milestone 1-5 matrices.", + "An explicitly requested discovery or reference audit still fails on mismatches, unsupported execution, missing fixtures or empty selections.", + "Historical failing evidence and deferred discovery fixtures remain retained.", + "Deferral does not authorize importing another private candidate or publishing a package." + ], + "outsideRoadmap": ["FPU arithmetic", "enabled MMU operation", "physical pipeline/cache qualification", "OS compatibility"] +} diff --git a/docs/COPPER68K_REFERENCE_QUALIFICATION.md b/docs/COPPER68K_REFERENCE_QUALIFICATION.md index 1b9f9027..e4b9f1b3 100644 --- a/docs/COPPER68K_REFERENCE_QUALIFICATION.md +++ b/docs/COPPER68K_REFERENCE_QUALIFICATION.md @@ -1,6 +1,7 @@ # Synthetic suite reference qualification -Milestone 6 is in progress. This is scoped software-reference evidence, not +Milestone 6's known-coverage scope is accepted; unclear qualification is explicitly +deferred by the user on 2026-10-09. This is scoped software-reference evidence, not exhaustive external coverage, physical CPU qualification or desktop readiness. 68010 and 68060 remain diagnostic profiles. @@ -8,6 +9,35 @@ The [milestone 6 completion audit](COPPER68K_MILESTONE6_COMPLETION_AUDIT.md) maps the accepted requirements to current evidence and outstanding gates. The dated records below remain scoped historical evidence. +### Accepted known coverage and deferred discovery, 2026-10-09 + +The user approved the already known suite and opted unclear parts out for now. +`docs/COPPER68K_QUALIFICATION_SCOPE.json` records the approved profiles/coverage +and five deferred areas with reasons and explicit opt-in routes. Ordinary and +reference summaries embed that registry and its hash. The required semantic +reports and counts are unchanged; no mismatching/unsupported/untested case is +reclassified as passing. Earlier scoped full, deep, mutation, retirement and +consumer proofs remain authoritative for their frozen source identities. + +The 010 invalid-format RTE CCR and 060 ordinary STOP disagreements, disputed +030/040 trace boundaries, broader restoration inventory and other private +continuation candidates are nonblocking for this approved acceptance. They remain +unqualified and visible. Their discovery tests, failing gates and original +reference data are retained; an explicit opt-in request still requires honest +execution and fails on mismatch or missing/empty input. No new candidate import +or publication is authorized. `roadmapComplete=false` remains deliberate. + +Fresh report-only verification of the revised command passes the unchanged +**86,663,530 cases / 783 batches** gate. The summary differs from the prior +qualified summary only by the accepted-scope fields; retained source reports are +unchanged. Four actual negative requests reject: a missing required MOVE opcode +report, a dropped deferred area, a deferred entry labeled passing and a duplicate +deferred ID. This is policy/report verification, not a new CPU execution. +Scope registry SHA-256: +`31b86efa03f367eb7871c7e172b956fe9cd80f0420bd5b24b08094491db4b5ae`. +Verification proof: +`8740d0938cb8b1ad5ba04de44f8e472bb144f57623a1287da05156c2ca4e7a45`. + ### Approved context-copy production promotion, 2026-10-09 The user explicitly authorized **only** the two-file context-copy fix after diff --git a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md index d94fb407..3f3058f7 100644 --- a/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md +++ b/docs/COPPER68K_SYNTHETIC_INSTRUCTION_SUITE_PLAN.md @@ -1,5 +1,25 @@ # Copper68k synthetic instruction suite +### Approved acceptance scope — known coverage, 2026-10-09 + +The user approved the test suite for already known behavior and explicitly opted +unclear behavior out for now. Milestone 6's **known-coverage acceptance is +complete**; unresolved qualification is **deferred**, not passing. The machine- +readable [scope registry](COPPER68K_QUALIFICATION_SCOPE.json) retains five deferred +areas with reasons and explicit opt-in routes: 010 rejected-RTE CCR, 060 ordinary +STOP privilege, disputed 030/040 trace boundaries, broader architectural +restoration, and unpromoted private continuation candidates. + +The required milestone 1-5 semantic matrices, defined exceptions, mutation +witnesses, reference identities and consumer checks retain their existing gates. +Explicitly requested discovery/reference audits still fail on mismatches, +unsupported execution, missing fixtures or empty selections. Their historical +failures and the 480-case untested inventory are retained. The approved scope +does not authorize another production import or package publication. Ordinary +and reference summaries include the scope and its hash; `roadmapComplete=false` +continues to distinguish accepted known coverage from exhaustive qualification. +Earlier in-progress/blocked checkpoints remain historical. + ### Milestone 6 checkpoint — approved context-copy production fix, 2026-10-09 The user approved promotion of only the reviewed two-file context-copy fix. @@ -172,7 +192,7 @@ model. A gap report alone does not complete it.** | 3. Arithmetic and comparison | Complete | ADD/SUB variants, quick/immediate/address/extend forms, comparisons, multiply/divide, decimal/packing operations; overflow, borrow, carry, sticky zero, exceptional operands. | | 4. Logical, bit and shift operations | Complete: semantic gate, 2026-10-04 | Logical/immediate/unary operations, bit manipulation, shifts/rotates, bitfields, atomic integer operations; preservation and memory effects. | | 5. Control and system operations | Complete: scoped semantic gate, 2026-10-04 | Branches, conditions, calls/returns, stack frames, traps, privilege-sensitive transfers, STOP/RESET, model-specific integer/system instructions; exception frames, saved PC/SR, stack selection, interrupt/trace. | -| 6. Reference qualification and consolidation | In progress | Independent reference audits across selected models, gap review, retire proven redundant tests; publish architectural combination coverage, not just xUnit counts. | +| 6. Reference qualification and consolidation | Complete for approved known coverage; unclear qualification deferred, 2026-10-09 | Independent reference audits, exact mutation/retirement evidence and architectural coverage retained. The explicit scope registry keeps deferred areas visible and opt-in; no exhaustive qualification claim. | For every promoted family, mismatches and emulator-level unsupported execution fail its gate. Documented processor exceptions (for example unimplemented integer diff --git a/scripts/copper68k-qualification-scope.ps1 b/scripts/copper68k-qualification-scope.ps1 new file mode 100644 index 00000000..e54ddaab --- /dev/null +++ b/scripts/copper68k-qualification-scope.ps1 @@ -0,0 +1,28 @@ +function Get-Copper68kQualificationScope([string]$Repository) { + $path = Join-Path $Repository 'docs/COPPER68K_QUALIFICATION_SCOPE.json' + $scope = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json + if ($scope.schema -ne 1 -or $scope.acceptance -cne 'known-coverage-only' -or + ($scope.approvedModels -join ',') -cne '68000,68010,68EC020,68020,68030,68040,68060,A1200' -or + $scope.approvedCoverage.Count -eq 0 -or $scope.deferred.Count -eq 0) { + throw 'Missing or incompatible Copper68k qualification scope' + } + $ids = @($scope.deferred | ForEach-Object { $_.id }) + if (@($ids | Select-Object -Unique).Count -ne $ids.Count) { throw 'Duplicate deferred qualification ID' } + $required = @('010-invalid-format-rte-ccr', '060-ordinary-stop-new-user-sr', + '030-040-disputed-trace-boundaries', 'broader-architectural-restoration', + 'other-private-continuation-candidates') + if (($ids | Sort-Object) -join ',' -cne (($required | Sort-Object) -join ',')) { + throw 'Missing or unexpected deferred qualification area' + } + foreach ($item in $scope.deferred) { + if ([string]::IsNullOrWhiteSpace($item.id) -or $item.status -cne 'deferred' -or + [string]::IsNullOrWhiteSpace($item.reason) -or [string]::IsNullOrWhiteSpace($item.optIn) -or + -not (Test-Path -LiteralPath (Join-Path $Repository $item.record) -PathType Leaf)) { + throw "Invalid deferred qualification entry: $($item.id)" + } + } + [ordered]@{ + policySha256 = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() + policy = $scope + } +} diff --git a/scripts/test-copper68k-reference-audits.ps1 b/scripts/test-copper68k-reference-audits.ps1 index 8356ebe9..ac3ae407 100644 --- a/scripts/test-copper68k-reference-audits.ps1 +++ b/scripts/test-copper68k-reference-audits.ps1 @@ -11,6 +11,8 @@ param( ) $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot +. (Join-Path $PSScriptRoot 'copper68k-qualification-scope.ps1') +$qualificationScope = Get-Copper68kQualificationScope $repo if (-not $SingleStepPath -and -not $MusashiPath -and -not $WinUaePath) { throw 'Select at least one reference audit' } if (-not $WinUaePath -and ($WinUaeGeneratorSource -or $WinUaeRunnerSource)) { throw 'WinUAE source arguments require a Basic input directory' } $output = [IO.Path]::GetFullPath($OutputDirectory, $repo) @@ -133,5 +135,5 @@ foreach ($job in $jobs) { $records += [ordered]@{ reference=$job.name; reportSha256=(Hash $reportPath); trxSha256=(Hash (Join-Path $reportDir 'audit.trx')); cpuSha256=(Hash (Join-Path $build 'bin/Copper68k/release/Copper68k.dll')); testSha256=(Hash (Join-Path $build 'bin/Copper68k.Tests/release/Copper68k.Tests.dll')) } } finally { foreach ($key in $saved.Keys) { [Environment]::SetEnvironmentVariable($key,$saved[$key]) } } } -[ordered]@{ schema=1; inputsSha256=(Hash (Join-Path $output 'reference-inputs.json')); audits=$records; roadmapComplete=$false } | +[ordered]@{ schema=1; inputsSha256=(Hash (Join-Path $output 'reference-inputs.json')); audits=$records; qualificationScope=$qualificationScope; roadmapComplete=$false } | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'reference-verification.json') -Encoding utf8 diff --git a/scripts/test-copper68k-synthetic.ps1 b/scripts/test-copper68k-synthetic.ps1 index c38576f7..2047ec0a 100644 --- a/scripts/test-copper68k-synthetic.ps1 +++ b/scripts/test-copper68k-synthetic.ps1 @@ -15,6 +15,8 @@ param( ) $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot +. (Join-Path $PSScriptRoot 'copper68k-qualification-scope.ps1') +$qualificationScope = Get-Copper68kQualificationScope $repo $knownModels = @('68000', '68010', '68EC020', '68020', '68030', '68040', '68060', 'A1200') if ($Models.Count -eq 0 -or @($Models | Where-Object { $_ -cnotin $knownModels }).Count -gt 0) { throw 'Empty or unknown CPU model selection' } if (@($Models | Select-Object -Unique).Count -ne $Models.Count) { throw 'Duplicate CPU model selection' } @@ -369,6 +371,7 @@ try { $inventory | Add-Member -NotePropertyName qualificationBoundaries -NotePropertyValue @('RTE fault restart/internal formats remain untested: 010 non-MOVE/foreign frame8 restart, 020/030 frames9/A/B, 040 frame7 CP context transfer and detailed access-fault restoration', 'BKPT external replacement responder is unavailable', 'MOVES physical function-code spaces and LPSTOP CPU-space broadcast are unqualified', 'CALLM/RTM type1 protocol uses an internal synthetic responder; normal public buses have none', '060 HALT debug-port restart, PULSE PST pins and debug pipeline commands are unavailable') $inventory | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $output 'qualified-inventory.json') @{schema=1; deterministicLogicalCases=$logicalCases; deterministicXunitBatches=$logicalBatches; moveGate='passing'; transferGate='passing'; arithmeticGate='passing'; logicalGate='passing'; controlGate='passing'; m68040DisabledMmuInstructionGate='passing'; m68000AddressErrorDoubleFaultGate='passing'; m68010Format8StructureGate='passing'; m68010WordMoveRestartGate='passing'; roadmapComplete=$false; + approvedCoverageGate='passing'; qualificationScope=$qualificationScope; seeded=$(if ($Deep) {@{seed=$Seed; samplesPerModel=$Samples; models=$Models}} else {$null}); externalReferences=$references; unavailableReferenceCoverage=$(if ($references.Count -eq 0) {'External audits not requested/executed'} else {'Software-reference coverage is scoped by the per-program exclusions; hardware and exhaustive external instruction-combination qualification remain unavailable'}); m68040MmuInstructionScope=@{translationEnabled=$false; dfc=@(1,2,5,6); globalRegisterField='canonical zero'; undefined='Disabled PTEST MMUSR; DFC 0/3/4/7'; enabledMmu='Unqualified flat-table approximation; PFLUSH conservatively flushes all ATC entries'};