diff options
| author | Cliff Click <cliffc@acm.org> | 2026-09-19 14:41:56 -0700 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-09-19 14:41:56 -0700 |
| commit | 628d94039c38f9d13df661da66df69943ccb2061 (patch) | |
| tree | 883bcbb1e0af08479b74fd58d6e97b3379bc5c43 | |
| parent | a78b1bd06f79652e268b1a530d579b0faf665031 (diff) | |
| download | Simple-628d94039c38f9d13df661da66df69943ccb2061.tar.gz Simple-628d94039c38f9d13df661da66df69943ccb2061.zip | |
Handle r/w <clinit> sections
8 files changed, 159 insertions, 32 deletions
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java b/chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java index 7af7e5f1..a74303e6 100644 --- a/chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java +++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java @@ -319,8 +319,7 @@ public class ElfWriter { // Write relocations for the constant pool String cpoolPrefix = _code.entryClinitName().replaceAll("[^A-Za-z0-9_$]", "_") + "$CPOOL$"; for( Encoding.Relo relo : enc._bigCons.values() ) { - boolean ro = !(relo._t instanceof TypeStruct ts) || ts.isConstant(); - DataSection data = ro ? rodata : rwdata; + DataSection data = relo.readOnly() ? rodata : rwdata; int symidx = symbols.symbol(cpoolPrefix+symbols.gidx(), data._index, SYM_BIND_GLOBAL, SYM_TYPE_FUNC, relo._target, relo._t.size()); relocations.writeReloOffPlus(relo._opStart+relo._off, symidx, relo._elf ); } diff --git a/chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java b/chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java index 4c7475fd..48ca4335 100644 --- a/chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java +++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java @@ -53,6 +53,12 @@ public class Encoding { Relo( Node op, Type t, byte off, byte elf ) { _op=op; _t=t; _off=off; _elf=elf; } + public boolean readOnly() { + // Class initializers still write their fields, even when every + // final value is constant. Their storage must remain writable. + return !(_t instanceof TypeStruct ts) || + (!Parser.startsClzPrefix(ts._name) && ts.isConstant()); + } } Encoding( CodeGen code ) { @@ -510,8 +516,7 @@ public class Encoding { Ary<Relo>[] raligns = new Ary[5]; for( Node op : _bigCons.keySet() ) { Relo relo = _bigCons.get(op); - // non-constant structs in the r/w data, everything else in r/o data - if( (relo._t instanceof TypeStruct ts && !ts.isConstant()) == ro ) + if( relo.readOnly() != ro ) continue; int align = relo._t.alignment(); Ary<Relo> relos = raligns[align]==null ? (raligns[align]=new Ary<>(Relo.class)) : raligns[align]; diff --git a/chapter25/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java b/chapter25/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java index e0b59395..d249b80c 100644 --- a/chapter25/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java +++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java @@ -23,8 +23,7 @@ public class LinkMem { // Patch local ops, e.g. loading float constants from the constant pool for( Node op : enc._bigCons.keySet() ) { Encoding.Relo relo = enc._bigCons.get(op); - boolean ro = !(relo._t instanceof TypeStruct ts) || ts.isConstant(); - int target = relo._target+(ro ? cpool : sdata); + int target = relo._target+(relo.readOnly() ? cpool : sdata); ((RIPRelSize)relo._op).patch(enc, relo._opStart, enc.opLen(relo._op), target - relo._opStart); } diff --git a/chapter25/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java b/chapter25/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java index e15d7dbe..7f9f3ad9 100644 --- a/chapter25/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java +++ b/chapter25/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java @@ -45,8 +45,7 @@ public abstract class ASMPrinter { Ary<Relo>[] raligns = new Ary[5]; for( Node op : bigCons.keySet() ) { Relo relo = bigCons.get(op); - // non-constant structs in the r/w data, everything else in r/o data - if( (relo._t instanceof TypeStruct ts && !ts.isConstant()) == ro ) + if( relo.readOnly() != ro ) continue; int align = relo._t.alignment(); Ary<Relo> relos = raligns[align]==null ? (raligns[align]=new Ary<>( Relo.class)) : raligns[align]; diff --git a/chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java index 2966ec04..d2357ef1 100644 --- a/chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java +++ b/chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java @@ -8,6 +8,9 @@ import com.seaofnodes.simple.util.Ary; import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.concurrent.TimeUnit; @@ -235,21 +238,39 @@ int v0=0^0; } @Test + public void testConstantClassInitialization() throws IOException { + TestC.runSF("val x = 42; return 0;","constantClassInt","",0); + TestC.runSF("val f = { -> 7; }; return 0;","constantClassFunction","",0); + } + + @Test public void testHelloWorld() throws IOException { String expected = "Hello, World!\n"; String prog = "return sys.io.p(\""+expected+"\") - "+expected.length()+";"; - TestC.run(prog,"helloWorld",new Ary<>(new String[]{SYS_BLDDIR}), + // tests_raw1 concurrently builds Chapter22Test's helloWorld executable. + TestC.run(prog,"helloWorldSys",new Ary<>(new String[]{SYS_BLDDIR}), TestC.CALL_CONVENTION, null, null, expected,0); } @Test public void testHelloWorldDriver() throws Exception { - Simple.main(new String[]{"-L",SYS_BLDDIR,"--norun","docs/examples/A_helloWorld.smp"}); + String out = "build/objs/helloWorldDriver/"; + Simple.main(new String[]{"-L",SYS_BLDDIR,"-o",out,"--norun","docs/examples/A_helloWorld.smp"}); + assertTrue(Files.isRegularFile(Path.of(out,"A_helloWorld.o"))); } @Test public void testHelloWorldDriverLibFile() throws Exception { - Simple.main(new String[]{"-L",SYS_FILE.toString(),"docs/examples/A_helloWorld.smp"}); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + PrintStream saved = System.out; + try( PrintStream capture = new PrintStream(output,true,StandardCharsets.UTF_8) ) { + System.setOut(capture); + Simple.main(new String[]{"-L",SYS_FILE.toString(),"-o","build/objs/helloWorldDriverLibFile/", + "docs/examples/A_helloWorld.smp"}); + } finally { + System.setOut(saved); + } + assertEquals("Hello, World!\n",output.toString(StandardCharsets.UTF_8)); } @Test diff --git a/chapter25/src/test/java/com/seaofnodes/simple/NativeExecutionTest.java b/chapter25/src/test/java/com/seaofnodes/simple/NativeExecutionTest.java new file mode 100644 index 00000000..f3f65183 --- /dev/null +++ b/chapter25/src/test/java/com/seaofnodes/simple/NativeExecutionTest.java @@ -0,0 +1,69 @@ +package com.seaofnodes.simple; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeTrue; + +public class NativeExecutionTest { + // A subprocess with deterministic stdout, stderr, and process status. + public static class Child { + public static void main(String[] args) { + System.out.println("before exit"); + System.err.println("child diagnostic"); + System.exit(Integer.parseInt(args[0])); + } + } + + private static String child(int exit) throws IOException { + String java = Path.of(System.getProperty("java.home"),"bin","java").toString(); + return TestC.exec(java,"-cp",System.getProperty("java.class.path"), + Child.class.getName(),Integer.toString(exit)); + } + + private static void checkFailure(int exit) throws IOException { + try { + child(exit); + fail("Accepted process exit "+exit); + } catch( IOException e ) { + assertTrue(e.getMessage(),e.getMessage().contains("exit code: "+exit)); + assertTrue(e.getMessage(),e.getMessage().contains("before exit")); + assertTrue(e.getMessage(),e.getMessage().contains("child diagnostic")); + } + } + + @Test public void testNonzeroExit() throws IOException { checkFailure(7); } + + @Test public void testFullWindowsExitCode() throws IOException { + assumeTrue(TestC.OS.startsWith("Windows")); + checkFailure(256); // Narrowing to a byte incorrectly reports success. + checkFailure(2816); // Observed Cygwin status for the Hello World crash. + } + + @Test public void testSuccessfulOutput() throws IOException { + assertEquals("before exit"+System.lineSeparator(),child(0)); + } + + @Test public void testHelloWorldArtifacts() throws IOException { + new Chapter22Test().testHelloWorld(); + new Chapter25Test().testHelloWorld(); + // Model a parallel run paused between Chapter 22's link and execution. + // Chapter 25 must not replace the executable Chapter 22 is about to run. + String exe = "build/objs/helloWorld"+(TestC.OS.startsWith("Windows") ? ".exe" : ""); + assertEquals("Hello, World!",TestC.exec(exe)); + } + + @Test public void testDriverNonzeroExit() throws Exception { + Path dir = Files.createTempDirectory(Path.of("build/objs"),"driver-exit-"); + Path src = dir.resolve("fail.smp"); + Files.writeString(src,"return 7;"); + try { + Simple.main(new String[]{src.toString()}); + fail("Driver accepted a failed executable"); + } catch( IOException e ) { + assertTrue(e.getMessage(),e.getMessage().contains("exit code: 7")); + } + } +} diff --git a/chapter25/src/test/java/com/seaofnodes/simple/TestC.java b/chapter25/src/test/java/com/seaofnodes/simple/TestC.java index d9595282..55c62c37 100644 --- a/chapter25/src/test/java/com/seaofnodes/simple/TestC.java +++ b/chapter25/src/test/java/com/seaofnodes/simple/TestC.java @@ -138,7 +138,10 @@ public abstract class TestC { public static String gcc( String main, double ignore, String... objs ) throws IOException { String BLDDIR = "build/objs/test0/"; String exe = BLDDIR+main+(OS.startsWith("Windows") ? ".exe" : ""); - return gcc(objs[0],null,null,false,exe); + linkExe(objs[0],null,null,null,exe); + // Module tests intentionally use the program exit code as their result. + ExecResult result = execute(null,exe); + return result.exit==0 ? result.out : "exec exit code: "+result.exit; } // Link with gcc, and execute the resulting binary, returning stdout as a @@ -209,29 +212,46 @@ public abstract class TestC { } private static String execStdin( String stdin, String... args ) throws IOException { + ExecResult result = execute(stdin,args); + if( result.exit!=0 ) + throw new IOException(String.join(" ",args)+"\nexec exit code: "+result.exit+ + "\nstdout:\n"+result.out+"\nstderr:\n"+result.err); + return result.out; + } + + private record ExecResult(int exit, String out, String err) {} + + private static ExecResult execute( String stdin, String... args ) throws IOException { // Execute results ProcessBuilder smp = new ProcessBuilder(args); - File stdinFile = null; - File stdoutFile = File.createTempFile("simple-stdout",".txt"); - smp.redirectOutput(stdoutFile); - if( stdin!=null && stdin.isEmpty() ) smp.redirectInput(ProcessBuilder.Redirect.INHERIT); - else if( stdin!=null ) { - stdinFile = File.createTempFile("simple-stdin",".txt"); - Files.writeString(stdinFile.toPath(),stdin); - smp.redirectInput(stdinFile); + Path stdinFile = null; + Path stdoutFile = Files.createTempFile("simple-stdout",".txt"); + Path stderrFile = Files.createTempFile("simple-stderr",".txt"); + Process p = null; + try { + smp.redirectOutput(stdoutFile.toFile()); + smp.redirectError(stderrFile.toFile()); + if( stdin!=null && stdin.isEmpty() ) smp.redirectInput(ProcessBuilder.Redirect.INHERIT); + else if( stdin!=null ) { + stdinFile = Files.createTempFile("simple-stdin",".txt"); + Files.writeString(stdinFile,stdin); + smp.redirectInput(stdinFile.toFile()); + } + p = smp.start(); + if( stdin==null ) + p.getOutputStream().close(); + // Windows statuses have 32 bits; narrowing can turn a failure into 0. + int exit = p.waitFor(); + return new ExecResult(exit,Files.readString(stdoutFile),Files.readString(stderrFile)); + } catch( InterruptedException e ) { + if( p!=null ) p.destroyForcibly(); + Thread.currentThread().interrupt(); + throw new IOException("interrupted",e); + } finally { + if( stdinFile!=null ) Files.deleteIfExists(stdinFile); + Files.deleteIfExists(stdoutFile); + Files.deleteIfExists(stderrFile); } - Process p = smp.start(); - if( stdin==null ) - p.getOutputStream().close(); - int exit; - try { exit = (byte)p.waitFor(); } catch( InterruptedException e ) { throw new IOException("interrupted"); } - String result = Files.readString(stdoutFile.toPath()); - if( stdinFile != null ) - stdinFile.delete(); - stdoutFile.delete(); - if( exit!=0 ) - return "exec exit code: "+exit; - return result; } static Ary<String> linkObjs(Ary<String> externPaths) { diff --git a/skills/chapter25-codex-notes.md b/skills/chapter25-codex-notes.md index 8fa5eefb..bb75ac56 100644 --- a/skills/chapter25-codex-notes.md +++ b/skills/chapter25-codex-notes.md @@ -198,6 +198,11 @@ Consequences: ## Serialization and code generation +- Class storage must be writable during `<clinit>`, even when all final field + values are constant. `Encoding.Relo.readOnly()` owns this distinction for + pool emission, ELF symbols, in-memory linking, and assembly printing. Ordinary + constant objects remain read-only. `val x = 42; return 0;` exposed a native + read-only write that emulator memory did not reject. - Object files carry canonical types plus ideal Simple IR. Every new semantic type field, node field, tag, alias map, or mode needs balanced serialization, deserialization, type upgrading, equality, and bijection testing. @@ -220,6 +225,16 @@ Consequences: allocation, and byte encoding. Compare IR, `CodeGen.asm()`, and `objdump`. 6. Rebuild `sys.o` after compiler changes that affect serialized IR or native code; stale system objects can make results appear inconsistent. +7. Preserve the full process exit status and capture stderr. Chapter 22's Hello + World printed its expected output before crashing; Cygwin returned 2816, + which `(byte)waitFor()` turned into zero. Expected stdout alone cannot prove + successful execution. Normal TestC/driver execution now throws on failure; + the legacy module-test helper explicitly treats exit codes as results. +8. Chapter 25's `make -j 4 tests` runs test groups in separate JVMs. Native + artifact names must be distinct across groups: Chapter22Test uses + `helloWorld`, Chapter25Test uses `helloWorldSys`, and driver variants have + separate output directories. `NativeExecutionTest` covers overwriting an + executable between link and execution, full exit statuses, and driver errors. Useful test ladder: |
