From f4bd723d929e53475f7990374dca7b7dfcbbdc42 Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Sun, 1 Nov 2026 10:33:55 -0800 Subject: Add Chapter 29 for tech debt cleanup with the parser split Keep Chapter 28b intact and carry the grammar-oriented parser package, Var move, IterPeeps move, and parser regression tests into Chapter 29. Defer privacy and exclude its experimental tests and fixture changes. Register the chapter in the indexes, IntelliJ modules and run configurations, Maven reactor, roadmap, and linear-history publisher. Windows and Linux each pass 573 tests, release builds, 199 allocation replays, and 29 native checks. --- .../src/test/java/com/seaofnodes/simple/TestC.java | 282 +++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 chapter29/src/test/java/com/seaofnodes/simple/TestC.java (limited to 'chapter29/src/test/java/com/seaofnodes/simple/TestC.java') 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 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 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 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 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 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 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 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 libraries, String exe ) throws IOException { + Ary 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 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 linkObjs(CodeGen code) throws IOException { + Ary 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; + } +} -- cgit v1.2.3