summaryrefslogtreecommitdiff
path: root/chapter25
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-09-19 14:41:56 -0700
committerCliff Click <cliffc@acm.org>2026-09-19 14:41:56 -0700
commit628d94039c38f9d13df661da66df69943ccb2061 (patch)
tree883bcbb1e0af08479b74fd58d6e97b3379bc5c43 /chapter25
parenta78b1bd06f79652e268b1a530d579b0faf665031 (diff)
downloadSimple-628d94039c38f9d13df661da66df69943ccb2061.tar.gz
Simple-628d94039c38f9d13df661da66df69943ccb2061.zip
Handle r/w <clinit> sections
Diffstat (limited to 'chapter25')
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java3
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java9
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java3
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java3
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java27
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/NativeExecutionTest.java69
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/TestC.java62
7 files changed, 144 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) {