diff options
| author | Cliff Click <cliffc@acm.org> | 2026-09-19 19:11:14 -0700 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-09-19 19:11:14 -0700 |
| commit | a8c96e04c59d37fe561319e86474f55c497705bb (patch) | |
| tree | f5136e51bbf713aff85490ff3c3de7d46902368b | |
| parent | 2f779dd115acf1fc99c7b3b416273fdb66ad48cb (diff) | |
| download | Simple-a8c96e04c59d37fe561319e86474f55c497705bb.tar.gz Simple-a8c96e04c59d37fe561319e86474f55c497705bb.zip | |
Fixed st8 on arm/risc evals
16 files changed, 182 insertions, 11 deletions
diff --git a/chapter21/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/chapter21/src/test/java/com/seaofnodes/simple/Chapter21Test.java index d8a5cebc..a5d86fa5 100644 --- a/chapter21/src/test/java/com/seaofnodes/simple/Chapter21Test.java +++ b/chapter21/src/test/java/com/seaofnodes/simple/Chapter21Test.java @@ -11,6 +11,34 @@ import org.junit.Test; import static org.junit.Assert.*; public class Chapter21Test { + @Test public void testRisc64BitStore() { + byte[] mem = new byte[24]; + EvalRisc5 cpu = new EvalRisc5(mem,mem.length); + check64BitStore(mem,cpu::st8,cpu::ld8); + } + + @Test public void testArm64BitStore() { + byte[] mem = new byte[24]; + EvalArm64 cpu = new EvalArm64(mem,mem.length); + check64BitStore(mem,cpu::st8,cpu::ld8); + } + + // Check bytes independently of ld8, including overwrite and adjacent memory. + private static void check64BitStore(byte[] mem, + java.util.function.BiConsumer<Integer,Long> store, + java.util.function.IntToLongFunction load) { + java.util.Arrays.fill(mem,(byte)0xA5); + for( long value : new long[]{0x0123456789ABCDEFL,0xFEDCBA9876543210L, + -1L,0L,Long.MIN_VALUE,Long.MAX_VALUE} ) { + store.accept(8,value); + for( int i=0; i<mem.length; i++ ) { + int expected = i>=8 && i<16 ? (int)(value >>> (8*(i-8)))&0xFF : 0xA5; + assertEquals("byte "+i,expected,mem[i]&0xFF); + } + assertEquals(value,load.applyAsLong(8)); + } + } + @Test @Ignore public void testJig() throws IOException { diff --git a/chapter21/src/test/java/com/seaofnodes/simple/EvalArm64.java b/chapter21/src/test/java/com/seaofnodes/simple/EvalArm64.java index 3ea9bea0..c10eccde 100644 --- a/chapter21/src/test/java/com/seaofnodes/simple/EvalArm64.java +++ b/chapter21/src/test/java/com/seaofnodes/simple/EvalArm64.java @@ -50,7 +50,7 @@ public class EvalArm64 { public void st1(int x, int val) { _buf[x] = (byte)val; } public void st2(int x, int val) { st1(x,val); st1(x+1,val>> 8); } public void st4(int x, int val) { st2(x,val); st2(x+2,val>>16); } - public void st8(int x, long val) { st4(x,(int)val); st2(x+4,(int)(val>>32)); } + public void st8(int x, long val) { st4(x,(int)val); st4(x+4,(int)(val>>32)); } public int step(int maxops) { int trap = 0; diff --git a/chapter21/src/test/java/com/seaofnodes/simple/EvalRisc5.java b/chapter21/src/test/java/com/seaofnodes/simple/EvalRisc5.java index fd953342..aa9702f1 100644 --- a/chapter21/src/test/java/com/seaofnodes/simple/EvalRisc5.java +++ b/chapter21/src/test/java/com/seaofnodes/simple/EvalRisc5.java @@ -55,7 +55,7 @@ import java.util.Arrays; public void st1(int x, int val) { _buf[x] = (byte)val; } public void st2(int x, int val) { st1(x,val); st1(x+1,val>> 8); } public void st4(int x, int val) {st2(x,val); st2(x+2,val>>16); } - public void st8(int x, long val) { st4(x,(int)val); st2(x+4,(int)(val>>32)); } + public void st8(int x, long val) { st4(x,(int)val); st4(x+4,(int)(val>>32)); } // Note: only a few bits are used. (Machine = 3, User = 0) // Bits 0..1 = privilege. diff --git a/chapter22/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/chapter22/src/test/java/com/seaofnodes/simple/Chapter21Test.java index 77d1e337..5382a2bc 100644 --- a/chapter22/src/test/java/com/seaofnodes/simple/Chapter21Test.java +++ b/chapter22/src/test/java/com/seaofnodes/simple/Chapter21Test.java @@ -11,6 +11,34 @@ import org.junit.Test; import static org.junit.Assert.*; public class Chapter21Test { + @Test public void testRisc64BitStore() { + byte[] mem = new byte[24]; + EvalRisc5 cpu = new EvalRisc5(mem,mem.length); + check64BitStore(mem,cpu::st8,cpu::ld8); + } + + @Test public void testArm64BitStore() { + byte[] mem = new byte[24]; + EvalArm64 cpu = new EvalArm64(mem,mem.length); + check64BitStore(mem,cpu::st8,cpu::ld8); + } + + // Check bytes independently of ld8, including overwrite and adjacent memory. + private static void check64BitStore(byte[] mem, + java.util.function.BiConsumer<Integer,Long> store, + java.util.function.IntToLongFunction load) { + java.util.Arrays.fill(mem,(byte)0xA5); + for( long value : new long[]{0x0123456789ABCDEFL,0xFEDCBA9876543210L, + -1L,0L,Long.MIN_VALUE,Long.MAX_VALUE} ) { + store.accept(8,value); + for( int i=0; i<mem.length; i++ ) { + int expected = i>=8 && i<16 ? (int)(value >>> (8*(i-8)))&0xFF : 0xA5; + assertEquals("byte "+i,expected,mem[i]&0xFF); + } + assertEquals(value,load.applyAsLong(8)); + } + } + @Test @Ignore public void testJig() throws IOException { diff --git a/chapter22/src/test/java/com/seaofnodes/simple/EvalArm64.java b/chapter22/src/test/java/com/seaofnodes/simple/EvalArm64.java index 981799bb..6f596799 100644 --- a/chapter22/src/test/java/com/seaofnodes/simple/EvalArm64.java +++ b/chapter22/src/test/java/com/seaofnodes/simple/EvalArm64.java @@ -54,7 +54,7 @@ public class EvalArm64 { public void st1(int x, int val) { _buf[x] = (byte)val; } public void st2(int x, int val) { st1(x,val); st1(x+1,val>> 8); } public void st4(int x, int val) { st2(x,val); st2(x+2,val>>16); } - public void st8(int x, long val) { st4(x,(int)val); st2(x+4,(int)(val>>32)); } + public void st8(int x, long val) { st4(x,(int)val); st4(x+4,(int)(val>>32)); } public int step(int maxops) { int trap = 0; diff --git a/chapter22/src/test/java/com/seaofnodes/simple/EvalRisc5.java b/chapter22/src/test/java/com/seaofnodes/simple/EvalRisc5.java index 0a1c034f..7b918615 100644 --- a/chapter22/src/test/java/com/seaofnodes/simple/EvalRisc5.java +++ b/chapter22/src/test/java/com/seaofnodes/simple/EvalRisc5.java @@ -58,7 +58,7 @@ public class EvalRisc5 { public void st1(int x, int val) { _buf[x] = (byte)val; } public void st2(int x, int val) { st1(x,val); st1(x+1,val>> 8); } public void st4(int x, int val) {st2(x,val); st2(x+2,val>>16); } - public void st8(int x, long val) { st4(x,(int)val); st2(x+4,(int)(val>>32)); } + public void st8(int x, long val) { st4(x,(int)val); st4(x+4,(int)(val>>32)); } // Note: only a few bits are used. (Machine = 3, User = 0) // Bits 0..1 = privilege. diff --git a/chapter23/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/chapter23/src/test/java/com/seaofnodes/simple/Chapter21Test.java index 7f4898e0..d4913698 100644 --- a/chapter23/src/test/java/com/seaofnodes/simple/Chapter21Test.java +++ b/chapter23/src/test/java/com/seaofnodes/simple/Chapter21Test.java @@ -12,6 +12,34 @@ import org.junit.Test; import static org.junit.Assert.*; public class Chapter21Test { + @Test public void testRisc64BitStore() { + byte[] mem = new byte[24]; + EvalRisc5 cpu = new EvalRisc5(mem,mem.length); + check64BitStore(mem,cpu::st8,cpu::ld8); + } + + @Test public void testArm64BitStore() { + byte[] mem = new byte[24]; + EvalArm64 cpu = new EvalArm64(mem,mem.length); + check64BitStore(mem,cpu::st8,cpu::ld8); + } + + // Check bytes independently of ld8, including overwrite and adjacent memory. + private static void check64BitStore(byte[] mem, + java.util.function.BiConsumer<Integer,Long> store, + java.util.function.IntToLongFunction load) { + java.util.Arrays.fill(mem,(byte)0xA5); + for( long value : new long[]{0x0123456789ABCDEFL,0xFEDCBA9876543210L, + -1L,0L,Long.MIN_VALUE,Long.MAX_VALUE} ) { + store.accept(8,value); + for( int i=0; i<mem.length; i++ ) { + int expected = i>=8 && i<16 ? (int)(value >>> (8*(i-8)))&0xFF : 0xA5; + assertEquals("byte "+i,expected,mem[i]&0xFF); + } + assertEquals(value,load.applyAsLong(8)); + } + } + @Test public void testJig() throws IOException { diff --git a/chapter23/src/test/java/com/seaofnodes/simple/EvalArm64.java b/chapter23/src/test/java/com/seaofnodes/simple/EvalArm64.java index c719b8e8..e7e4ada3 100644 --- a/chapter23/src/test/java/com/seaofnodes/simple/EvalArm64.java +++ b/chapter23/src/test/java/com/seaofnodes/simple/EvalArm64.java @@ -55,7 +55,7 @@ public class EvalArm64 { public void st1(int x, int val) { _buf[x] = (byte)val; } public void st2(int x, int val) { st1(x,val); st1(x+1,val>> 8); } public void st4(int x, int val) { st2(x,val); st2(x+2,val>>16); } - public void st8(int x, long val) { st4(x,(int)val); st2(x+4,(int)(val>>32)); } + public void st8(int x, long val) { st4(x,(int)val); st4(x+4,(int)(val>>32)); } public int step(int maxops) { int trap = 0; diff --git a/chapter23/src/test/java/com/seaofnodes/simple/EvalRisc5.java b/chapter23/src/test/java/com/seaofnodes/simple/EvalRisc5.java index 8c0a8792..a4cc7044 100644 --- a/chapter23/src/test/java/com/seaofnodes/simple/EvalRisc5.java +++ b/chapter23/src/test/java/com/seaofnodes/simple/EvalRisc5.java @@ -59,7 +59,7 @@ public class EvalRisc5 { public void st1(int x, int val) { _buf[x] = (byte)val; } public void st2(int x, int val) { st1(x,val); st1(x+1,val>> 8); } public void st4(int x, int val) {st2(x,val); st2(x+2,val>>16); } - public void st8(int x, long val) { st4(x,(int)val); st2(x+4,(int)(val>>32)); } + public void st8(int x, long val) { st4(x,(int)val); st4(x+4,(int)(val>>32)); } // Note: only a few bits are used. (Machine = 3, User = 0) // Bits 0..1 = privilege. diff --git a/chapter24/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/chapter24/src/test/java/com/seaofnodes/simple/Chapter21Test.java index ec55735e..7277a4d2 100644 --- a/chapter24/src/test/java/com/seaofnodes/simple/Chapter21Test.java +++ b/chapter24/src/test/java/com/seaofnodes/simple/Chapter21Test.java @@ -12,6 +12,34 @@ import org.junit.Test; import static org.junit.Assert.*; public class Chapter21Test { + @Test public void testRisc64BitStore() { + byte[] mem = new byte[24]; + EvalRisc5 cpu = new EvalRisc5(mem,mem.length); + check64BitStore(mem,cpu::st8,cpu::ld8); + } + + @Test public void testArm64BitStore() { + byte[] mem = new byte[24]; + EvalArm64 cpu = new EvalArm64(mem,mem.length); + check64BitStore(mem,cpu::st8,cpu::ld8); + } + + // Check bytes independently of ld8, including overwrite and adjacent memory. + private static void check64BitStore(byte[] mem, + java.util.function.BiConsumer<Integer,Long> store, + java.util.function.IntToLongFunction load) { + java.util.Arrays.fill(mem,(byte)0xA5); + for( long value : new long[]{0x0123456789ABCDEFL,0xFEDCBA9876543210L, + -1L,0L,Long.MIN_VALUE,Long.MAX_VALUE} ) { + store.accept(8,value); + for( int i=0; i<mem.length; i++ ) { + int expected = i>=8 && i<16 ? (int)(value >>> (8*(i-8)))&0xFF : 0xA5; + assertEquals("byte "+i,expected,mem[i]&0xFF); + } + assertEquals(value,load.applyAsLong(8)); + } + } + @Test public void testJig() throws IOException { diff --git a/chapter24/src/test/java/com/seaofnodes/simple/EvalArm64.java b/chapter24/src/test/java/com/seaofnodes/simple/EvalArm64.java index 9575d9fe..04fee337 100644 --- a/chapter24/src/test/java/com/seaofnodes/simple/EvalArm64.java +++ b/chapter24/src/test/java/com/seaofnodes/simple/EvalArm64.java @@ -55,7 +55,7 @@ public class EvalArm64 { public void st1(int x, int val) { _buf[x] = (byte)val; } public void st2(int x, int val) { st1(x,val); st1(x+1,val>> 8); } public void st4(int x, int val) { st2(x,val); st2(x+2,val>>16); } - public void st8(int x, long val) { st4(x,(int)val); st2(x+4,(int)(val>>32)); } + public void st8(int x, long val) { st4(x,(int)val); st4(x+4,(int)(val>>32)); } public int step(int maxops) { int trap = 0; diff --git a/chapter24/src/test/java/com/seaofnodes/simple/EvalRisc5.java b/chapter24/src/test/java/com/seaofnodes/simple/EvalRisc5.java index 8c0a8792..a4cc7044 100644 --- a/chapter24/src/test/java/com/seaofnodes/simple/EvalRisc5.java +++ b/chapter24/src/test/java/com/seaofnodes/simple/EvalRisc5.java @@ -59,7 +59,7 @@ public class EvalRisc5 { public void st1(int x, int val) { _buf[x] = (byte)val; } public void st2(int x, int val) { st1(x,val); st1(x+1,val>> 8); } public void st4(int x, int val) {st2(x,val); st2(x+2,val>>16); } - public void st8(int x, long val) { st4(x,(int)val); st2(x+4,(int)(val>>32)); } + public void st8(int x, long val) { st4(x,(int)val); st4(x+4,(int)(val>>32)); } // Note: only a few bits are used. (Machine = 3, User = 0) // Bits 0..1 = privilege. diff --git a/chapter25/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/chapter25/src/test/java/com/seaofnodes/simple/Chapter21Test.java index 6eb9a698..6d64f2d2 100644 --- a/chapter25/src/test/java/com/seaofnodes/simple/Chapter21Test.java +++ b/chapter25/src/test/java/com/seaofnodes/simple/Chapter21Test.java @@ -9,6 +9,34 @@ import org.junit.Test; import static org.junit.Assert.*; public class Chapter21Test { + @Test public void testRisc64BitStore() { + byte[] mem = new byte[24]; + EvalRisc5 cpu = new EvalRisc5(mem,mem.length); + check64BitStore(mem,cpu::st8,cpu::ld8); + } + + @Test public void testArm64BitStore() { + byte[] mem = new byte[24]; + EvalArm64 cpu = new EvalArm64(mem,mem.length); + check64BitStore(mem,cpu::st8,cpu::ld8); + } + + // Check bytes independently of ld8, including overwrite and adjacent memory. + private static void check64BitStore(byte[] mem, + java.util.function.BiConsumer<Integer,Long> store, + java.util.function.IntToLongFunction load) { + java.util.Arrays.fill(mem,(byte)0xA5); + for( long value : new long[]{0x0123456789ABCDEFL,0xFEDCBA9876543210L, + -1L,0L,Long.MIN_VALUE,Long.MAX_VALUE} ) { + store.accept(8,value); + for( int i=0; i<mem.length; i++ ) { + int expected = i>=8 && i<16 ? (int)(value >>> (8*(i-8)))&0xFF : 0xA5; + assertEquals("byte "+i,expected,mem[i]&0xFF); + } + assertEquals(value,load.applyAsLong(8)); + } + } + @Test public void testJig() throws IOException { diff --git a/chapter25/src/test/java/com/seaofnodes/simple/EvalArm64.java b/chapter25/src/test/java/com/seaofnodes/simple/EvalArm64.java index 32a5b152..d511f18f 100644 --- a/chapter25/src/test/java/com/seaofnodes/simple/EvalArm64.java +++ b/chapter25/src/test/java/com/seaofnodes/simple/EvalArm64.java @@ -55,7 +55,7 @@ public class EvalArm64 { public void st1(int x, int val) { _buf[x] = (byte)val; } public void st2(int x, int val) { st1(x,val); st1(x+1,val>> 8); } public void st4(int x, int val) { st2(x,val); st2(x+2,val>>16); } - public void st8(int x, long val) { st4(x,(int)val); st2(x+4,(int)(val>>32)); } + public void st8(int x, long val) { st4(x,(int)val); st4(x+4,(int)(val>>32)); } public int step(int maxops) { int trap = 0; diff --git a/chapter25/src/test/java/com/seaofnodes/simple/EvalRisc5.java b/chapter25/src/test/java/com/seaofnodes/simple/EvalRisc5.java index 8c0a8792..a4cc7044 100644 --- a/chapter25/src/test/java/com/seaofnodes/simple/EvalRisc5.java +++ b/chapter25/src/test/java/com/seaofnodes/simple/EvalRisc5.java @@ -59,7 +59,7 @@ public class EvalRisc5 { public void st1(int x, int val) { _buf[x] = (byte)val; } public void st2(int x, int val) { st1(x,val); st1(x+1,val>> 8); } public void st4(int x, int val) {st2(x,val); st2(x+2,val>>16); } - public void st8(int x, long val) { st4(x,(int)val); st2(x+4,(int)(val>>32)); } + public void st8(int x, long val) { st4(x,(int)val); st4(x+4,(int)(val>>32)); } // Note: only a few bits are used. (Machine = 3, User = 0) // Bits 0..1 = privilege. diff --git a/docs/chapter-backports.md b/docs/chapter-backports.md index 7dc4cbc3..7151dc4d 100644 --- a/docs/chapter-backports.md +++ b/docs/chapter-backports.md @@ -19,7 +19,6 @@ the correction into the chapter's representation, not the entire modern file. | ID | Change | Proposed destination | Scope and acceptance evidence | |---|---|---|---| -| B10 | Preserve widening state in integer nonzero refinement | **Audit** first chapter with both widening and `nonZero`; no later than 24 | Chapter 25 `TypeInteger.nonZero` preserves `_widen`. Check lattice laws and loop refinement. Exclude TypeScalar, storage-type, and serialization changes. | | B11 | Diagnose return types using the optimized return expression | 18; 19 already has the correction | `ReturnNode.err()` uses `expr()._type` instead of the parse-time `mt` aggregate. Chapter 18 rejects `struct S { u8 x; }; return new S; return 0;` with a mixed integer/reference error; Chapter 19 accepts it. Also test genuinely reachable incompatible returns. This is a candidate, not yet an applied or isolated-patch-verified fix. | | B12 | Encode the actual destination register for two-address immediate multiply | 21 | `MulIX86` inherits `ImmX86.encoding`, whose ModRM.reg is fixed to zero and whose REX.R is clear. With source/destination both `rcx`, multiplying by 11 emits `48 6b c1 0b` (destination `rax`) instead of `48 6b c9 0b`. With both `r9`, it emits `49 6b c1 0b` instead of `4d 6b c9 0b`. Give multiply its proper register fields while preserving Chapter 21's two-address allocation contract; do not change the opcode-extension fields used by other `ImmX86` subclasses. Later chapters use a separate multiply encoder. | @@ -132,6 +131,38 @@ Historical results below are dated evidence, not a substitute for a fresh baseline. Logs live in ignored build directories and may no longer exist. Reusable implementation lessons are in `skills/chapter25-codex-notes.md`. +### Issue #247: emulator 64-bit stores corrected locally, 2026-09-19 + +[Issue #247](https://github.com/SeaOfNodes/Simple/issues/247) identifies `st8` +writing the upper half with `st2`. The identical bug exists in EvalRisc5 and +EvalArm64 in every snapshot 21-25, starting with their introduction. Both now +use `st4` for the upper half, writing all eight bytes rather than leaving the +upper two unchanged. + +`Chapter21Test.testRisc64BitStore` and `testArm64BitStore` are carried through +21-25. They check individual little-endian bytes independently of the load +helper, round trips, positive/negative/extreme values, overwriting with zero, +and untouched neighboring memory. All ten cases failed before the correction +at the first unwritten byte (0xA5 instead of 0x23). + +Full `make tests` passed with assertions enabled in each snapshot 21-25, +including 445 tests in Chapter 25. Log: +`chapter25/build/issue247-review/fixed.log`. + +### B10 audit: still pending in Chapter 24, 2026-09-19 + +The fix is present in Chapter 25 (introduced with commit `51f1f8f4`), but has +not reached Chapter 24. Integer `nonZero` appears in 15; `_widen` first appears +in 24, so earlier snapshots do not need this backport. + +An assertions-enabled probe of the current compiled snapshots tested `[0,255]` +and `[-255,0]` at widening levels 0-3. Chapter 24 resets all six nonzero widening +levels to zero; Chapter 25 preserves all of them. Both pass refinement (`isa`), +idempotence, double-dual, singleton normalization, and zero-input checks. This +establishes the missing type-level backport; it is not a loop-failure reproducer. +Probe: `chapter25/build/b10-review/B10Probe.java`. No compiler correction was +made during this audit; B10 remains in the pending queue above. + ### B09: complete locally, 2026-09-19 Audited `Node.p(depth)`, recursive printing, labels, scope/graph viewers, and |
