summaryrefslogtreecommitdiff
path: root/chapter29/src/test/java/com/seaofnodes/simple/TestC.java
diff options
context:
space:
mode:
Diffstat (limited to 'chapter29/src/test/java/com/seaofnodes/simple/TestC.java')
-rw-r--r--chapter29/src/test/java/com/seaofnodes/simple/TestC.java282
1 files changed, 282 insertions, 0 deletions
diff --git a/chapter29/src/test/java/com/seaofnodes/simple/TestC.java b/chapter29/src/test/java/com/seaofnodes/simple/TestC.java
new file mode 100644
index 00000000..54adf3af
--- /dev/null
+++ b/chapter29/src/test/java/com/seaofnodes/simple/TestC.java
@@ -0,0 +1,282 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen;
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.codegen.CompUnit;
+import com.seaofnodes.simple.codegen.ElfReader;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.util.Ary;
+import com.seaofnodes.simple.util.Utils;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.concurrent.TimeUnit;
+import static org.junit.Assert.*;
+
+// Call a C driver program, which calls a Simple-generated .o, and self-checks.
+// Expects GCC from the default CLI, compiles C, compiles Simple, links and runs.
+public abstract class TestC {
+ public static final int TEST_TIMEOUT_SECONDS = 60;
+
+ public static final String OS = System.getProperty("os.name");
+ public static final String CPU = System.getProperty("os.arch");
+
+ public static final String CALL_CONVENTION = OS.startsWith("Windows") ? "win64" : "SystemV";
+ public static final String CPU_PORT = switch( CPU ) {
+ case "amd64" -> "x86_64_v2";
+ default -> throw Utils.TODO("Map Yer CPU Port Here");
+ };
+ public static final String CPU_ABI = CPU_PORT+"_"+CALL_CONVENTION;
+ public static final String RELEASE_SYS_DIR = "build/release/"+CPU_ABI;
+
+ public static String runtimeObject() {
+ String home = System.getenv("SIMPLE_HOME");
+ String root = home==null || home.isEmpty() ? "build/release" : home;
+ return Path.of(root,CPU_ABI,"simple_crt.o").toString();
+ }
+
+ public static final String C_DRIVERS_DIR = "src/test/java/com/seaofnodes/simple/progs/";
+
+ /**
+ * Compile, link and run
+ * - WITH a C driver
+ * - using the default OS/CPU calling convention.
+ * - no lib sys
+ */
+ public static void runC( String src, String base, String expected, int spills) throws IOException {
+ String cfile = C_DRIVERS_DIR+base+".c";
+ run(src, base, null, CALL_CONVENTION, "", cfile, expected, spills);
+ }
+
+ /**
+ * Compile, link and run
+ * - withOUT a C driver
+ * - using the default OS/CPU calling convention.
+ * - no lib sys
+ */
+ public static void runSF( String src, String base, String expected, int spills ) throws IOException {
+ run(src, base, null, CALL_CONVENTION, null,null, expected,spills);
+ }
+
+ /**
+ * Compile, link and run
+ * - withOUT a C driver
+ * - using the default OS/CPU calling convention.
+ * - WITH a test lib sys
+ */
+ public static void runSYS( String src, String base, String expected, int spills ) throws IOException {
+ Ary<String> externPaths = new Ary<>(new String[]{RELEASE_SYS_DIR});
+ run(src, base, externPaths, CALL_CONVENTION, null,null, expected,spills);
+ }
+
+
+ /**
+ * Compile, link and run - general case
+ *
+ * @param src Program source to compile
+ * @param base Compiled binary final base name
+ * @param externPaths External symbol search path
+ * @param simple_conv Argument convention when calling Simple code
+ * @param c_conv Argument convention when calling C code, or null if not
+ * linking against a C driver program
+ * @param cfile Associated C program which will drive the Simple program,
+ * or null if not linking against a C driver program
+ * @param expected Expected stdout string
+ * @param spills Expected limit on spill code, used to validate the
+ * register allocation is reasonable
+ */
+ public static void run( String src, String base, Ary<String> externPaths, String simple_conv, String c_conv, String cfile, String expected, int spills ) throws IOException {
+ run(src, base, externPaths, simple_conv, c_conv, cfile, null, expected, spills);
+ }
+
+ public static void runArgs( String src, String base, Ary<String> externPaths, String simple_conv,
+ String expected, int spills, String... programArgs ) throws IOException {
+ run0(src,base,externPaths,simple_conv,null,null,null,expected,spills,programArgs);
+ }
+
+ public static void run( String src, String base, Ary<String> externPaths, String simple_conv, String c_conv, String cfile, String stdin, String expected, int spills ) throws IOException {
+ run0(src,base,externPaths,simple_conv,c_conv,cfile,stdin,expected,spills,new String[0]);
+ }
+
+ private static void run0( String src, String base, Ary<String> externPaths, String simple_conv, String c_conv,
+ String cfile, String stdin, String expected, int spills, String[] programArgs ) throws IOException {
+ String exe = compile(src,base,externPaths,simple_conv,c_conv,cfile,spills);
+
+ String[] execArgs = new String[programArgs.length+1];
+ execArgs[0] = exe;
+ System.arraycopy(programArgs,0,execArgs,1,programArgs.length);
+ String result = execStdin(stdin, 0, execArgs );
+ assertEquals(expected,result);
+ }
+
+ public static String compile( String src, String base, Ary<String> externPaths, String simple_conv, String c_conv,
+ String cfile, int spills ) throws IOException {
+ // Simple file base-name example:
+ // foo.smp ->
+ // build/objs/foo.o - object file
+ // build/objs/foo.exe - linked executable Windows
+ // build/objs/foo - linked executable Linux
+ String pathBase = "build/objs/"+base;
+ String exe = pathBase+(OS.startsWith("Windows") ? ".exe" : "");
+ // Compile simple, emit ELF
+ CodeGen code = new CheckedCodeGen(null,"build/objs",externPaths,base,src,126L,true,TypeInteger.BOT);
+ code.driver( CPU_PORT, simple_conv, false, cfile==null );
+
+ linkExe(linkObjs(code), c_conv, cfile, exe);
+
+ // Allocation quality not degraded
+ SpillStats.checkSpills(spills,code._regAlloc._spillScaled);
+ return exe;
+ }
+
+ // Link with gcc, and execute the resulting binary, returning stdout as a
+ // String. Any errors *assert* instead of returning some error code; this
+ // utility is meant to execute inside a JUnit test which will catch the
+ // assert and flag the test as failed.
+ 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" : "");
+ linkExe(new Ary<>(objs),null,null,exe);
+ // Module tests intentionally use the program exit code as their result.
+ ExecResult result = execute(null,0,exe);
+ return result.exit==0 ? result.out : "exec exit code: "+result.exit;
+ }
+
+ // Link with gcc, and execute the resulting binary, returning stdout as a
+ // String. Any errors *assert* instead of returning some error code; this
+ // utility is meant to execute inside a JUnit test which will catch the
+ // assert and flag the test as failed.
+ public static String gcc( String obj, String c_conv, String cfile, boolean stdin, String... args ) throws IOException {
+ return gcc(obj,c_conv,cfile,stdin ? "" : null,null,args);
+ }
+
+ public static String gcc( String obj, String c_conv, String cfile, String stdin, String... args ) throws IOException {
+ return gcc(obj,c_conv,cfile,stdin,null,args);
+ }
+
+ public static String gcc( String obj, String c_conv, String cfile, String stdin, Ary<String> linkObjs, String... args ) throws IOException {
+ linkExe(obj,c_conv,cfile,linkObjs,args[0]);
+ return execStdin(stdin,0,args);
+ }
+
+ // Immediate compile/link: the compiler already resolved every Simple object.
+ public static String gcc(CodeGen code, String c_conv, String cfile, String stdin, String... args) throws IOException {
+ linkExe(linkObjs(code),c_conv,cfile,args[0]);
+ return execStdin(stdin,0,args);
+ }
+
+ // Object-only tests supply every link input explicitly.
+ public static void linkExe( String entry, String c_conv, String cfile, Ary<String> libraries, String exe ) throws IOException {
+ Ary<String> objects = new Ary<>(String.class);
+ objects.add(entry);
+ if( libraries!=null ) objects.addAll(libraries);
+ linkExe(objects,c_conv,cfile,exe);
+ }
+
+ private static void linkExe(Ary<String> objects, String c_conv, String cfile, String exe) throws IOException {
+ // Compile the C program. Compiling code and constants in the low
+ // 2Gig. Pointers are 64b BUT since always in the low 2G all the
+ // high bits are zero - and Simple code can be emitted treating
+ // pointers as 4bytes.
+ var params = new Ary<>(String.class);
+ params.add("gcc");
+ params.add(OS.startsWith("Windows") ? "-Wl,--image-base,0x400000" : "-no-pie");
+ if( cfile!=null ) params.add(cfile); // Associated C driver, usually has a `main`
+ params.addAll(objects);
+ // Standalone Simple programs export `simple_main`; the small C
+ // runtime owns the native main symbol and converts argc/argv into a
+ // normal Simple array before making the ABI-qualified call.
+ if( cfile==null ) params.add(runtimeObject());
+ params.addAll(new String[] {
+ "-lm", // Picks up 'sqrt' for newtonFloat tests to compare
+ "-g", // debug symbols
+ "-o", // name the output file
+ exe, // name the output file
+ });
+ // Calling convention for C calls, if any
+ if( cfile!=null ) {
+ params.add("-D");
+ params.add("CALL_CONV="+c_conv);
+ }
+
+ // Run GCC to link (optionally compile C driver code)
+ exec(TEST_TIMEOUT_SECONDS,params.asAry());
+ }
+
+ public static String exec( String... args ) throws IOException {
+ return execStdin(null,0,args);
+ }
+
+ // Tests can bound execution without imposing a deadline on the CLI driver.
+ public static String exec( int seconds, String... args ) throws IOException {
+ return execStdin(null,seconds,args);
+ }
+
+ private static String execStdin( String stdin, int seconds, String... args ) throws IOException {
+ ExecResult result = execute(stdin,seconds,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, int seconds, String... args ) throws IOException {
+ // Execute results
+ ProcessBuilder smp = new ProcessBuilder(args);
+ 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();
+ if( seconds>0 && !p.waitFor(seconds,TimeUnit.SECONDS) ) {
+ String process = "pid="+p.pid()+", cpu="+
+ p.info().totalCpuDuration().map(Object::toString).orElse("unknown");
+ p.destroyForcibly();
+ boolean stopped = p.waitFor(5,TimeUnit.SECONDS);
+ throw new IOException(String.join(" ",args)+"\ntimed out after "+seconds+" seconds ("+process+")"+
+ (stopped ? "" : "; process did not terminate")+
+ "\nstdout:\n"+Files.readString(stdoutFile)+
+ "\nstderr:\n"+Files.readString(stderrFile));
+ }
+ // Windows statuses have 32 bits; narrowing can turn a failure into 0.
+ int exit = seconds>0 ? p.exitValue() : 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);
+ }
+ }
+
+ // Gather all CompUnit files and all externally named symbols
+ static Ary<String> linkObjs(CodeGen code) throws IOException {
+ Ary<String> objNames = new Ary<>(String.class);
+ for( CompUnit cu : code._compunits.values() ) {
+ File obj = cu._obj;
+ if( obj==null && cu._src!=null ) // Source supplied as a string.
+ obj = new File(code._buildDir+"/"+(code._srcName==null ? "Test" : code._srcName)+".o");
+ if( obj!=null ) objNames.add(obj.getCanonicalPath());
+ }
+ return objNames;
+ }
+}