diff options
Diffstat (limited to 'chapter25')
| -rw-r--r-- | chapter25/Makefile | 17 | ||||
| -rw-r--r-- | chapter25/docs/examples/BubbleSort.smp | 22 | ||||
| -rw-r--r-- | chapter25/src/main/c/simple_crt.c | 57 | ||||
| -rw-r--r-- | chapter25/src/main/java/com/seaofnodes/simple/Parser.java | 37 | ||||
| -rw-r--r-- | chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java | 19 | ||||
| -rw-r--r-- | chapter25/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java | 30 | ||||
| -rw-r--r-- | chapter25/src/main/smp/sys/Scan.smp | 8 | ||||
| -rw-r--r-- | chapter25/src/main/smp/sys/aryi64.smp | 13 | ||||
| -rw-r--r-- | chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java | 31 | ||||
| -rw-r--r-- | chapter25/src/test/java/com/seaofnodes/simple/TestC.java | 26 |
10 files changed, 212 insertions, 48 deletions
diff --git a/chapter25/Makefile b/chapter25/Makefile index 1ab66a60..98b6e368 100644 --- a/chapter25/Makefile +++ b/chapter25/Makefile @@ -145,6 +145,8 @@ CALL_CONV:= win64 CPU_ABI:= $(CPU_PORT)_$(CALL_CONV) RELEASE:= build/release RELEASE_SYS:= $(RELEASE)/$(CPU_ABI)/sys.o +RELEASE_CRT:= $(RELEASE)/$(CPU_ABI)/simple_crt.o +C_CALL_CONV:= $(if $(filter win64,$(CALL_CONV)),ms_abi,sysv_abi) $(OBJS)/lib_$(CPU_ABI)/sys.o: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag $(SYS_SMPS) @echo "simping " $@ @@ -155,29 +157,34 @@ $(RELEASE_SYS): $(OBJS)/lib_$(CPU_ABI)/sys.o @[ -d $(dir $@) ] || mkdir -p $(dir $@) @cp $< $@ +$(RELEASE_CRT): src/main/c/simple_crt.c + @echo "compiling " $@ + @[ -d $(dir $@) ] || mkdir -p $(dir $@) + @gcc -c -O2 -DCALL_CONV=$(C_CALL_CONV) -o $@ $< + ####################################################### # Base launch line for JVM tests JVM=nice java -ea -cp "$(CLZDIR)/main${SEP}${jars}${SEP}$(CLZDIR)/test" # Tests that do NOT need sys.smp -tests_raw0: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag +tests_raw0: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag $(RELEASE_CRT) @echo "testing " $(test_raw0) @[ -d $(OBJS) ] || mkdir -p $(OBJS) @$(JVM) org.junit.runner.JUnitCore $(test_raw0) -tests_raw1: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag +tests_raw1: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag $(RELEASE_CRT) @echo "testing " $(test_raw1) @[ -d $(OBJS) ] || mkdir -p $(OBJS) @$(JVM) org.junit.runner.JUnitCore $(test_raw1) # Larger standalone tests, still JUnits, still no sys.smp -tests_alone: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag +tests_alone: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag $(RELEASE_CRT) @echo "testing " $(test_alone) @[ -d $(OBJS) ] || mkdir -p $(OBJS) @$(JVM) org.junit.runner.JUnitCore $(test_alone) # Tests that DO need sys.smp -tests_sys: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag $(RELEASE_SYS) +tests_sys: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag $(RELEASE_SYS) $(RELEASE_CRT) @echo "testing " $(test_sys) @[ -d $(OBJS) ] || mkdir -p $(OBJS) @$(JVM) org.junit.runner.JUnitCore $(test_sys) @@ -193,7 +200,7 @@ tests: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag $(RELEASE_SYS) tests_raw0 tests # Build a Simple jar and release stdlib -release: build/release/Simple.jar $(RELEASE_SYS) +release: build/release/Simple.jar $(RELEASE_SYS) $(RELEASE_CRT) # Build a Simple jar build/release/Simple.jar: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag diff --git a/chapter25/docs/examples/BubbleSort.smp b/chapter25/docs/examples/BubbleSort.smp index 864e26da..1a4359ae 100644 --- a/chapter25/docs/examples/BubbleSort.smp +++ b/chapter25/docs/examples/BubbleSort.smp @@ -17,17 +17,23 @@ val sort = { sys.aryi64 nums -> } }; -// Read from stdin a line of the form "[ 4, 3, 2, 1]" and print it sorted. -val main = { -> - // Parse array of numbers from stdin - var nums = sys.aryi64.parseAryI64( sys.io.stdin() ); - if( !nums ) - return sys.io.p("stdin must look like [4, 3, 2, 1]"); +// Read one process argument of the form "[ 4, 3, 2, 1]" and print it sorted. +// args[0] is the executable name, as with native C argc/argv. +val main = { u8[~][] args -> + val usage = { -> + sys.io.p("Usage: please provide a list of at least two integers to sort in the format \"[1, 2, 3, 4, 5]\"\n"); + return 0; + }; + if( args# != 2 ) return usage(); + + var nums = sys.aryi64.parseAryI64(args[1]); + if( !nums ) return usage(); + if( nums.len < 2 ) return usage(); // Bubble sort array of integers sort(nums); - // Print a list of numbers to stdout - nums.str().write(1); + // Print the comma-separated result without the input brackets. + nums.csv().write(1); return 0; }; diff --git a/chapter25/src/main/c/simple_crt.c b/chapter25/src/main/c/simple_crt.c new file mode 100644 index 00000000..b79e6f88 --- /dev/null +++ b/chapter25/src/main/c/simple_crt.c @@ -0,0 +1,57 @@ +#include <stdint.h> +#include <stdlib.h> +#include <string.h> + +// Native process arguments converted to ordinary Simple array layouts. +// The outer array is mutable; every contained byte array is published as +// immutable and non-null to Simple code. +typedef struct SimpleBytes { + uint32_t len; + unsigned char bytes[]; +} SimpleBytes; + +typedef struct SimpleArgs { + uint32_t len; + uint32_t pad; + SimpleBytes *args[]; +} SimpleArgs; + +__attribute__((CALL_CONV)) +extern int64_t simple_main(SimpleArgs *args); + +static SimpleArgs *simple_args(int argc, char **argv) { + if (argc < 0 || (uint64_t)argc > UINT32_MAX) + return NULL; + + SimpleArgs *args = calloc(1, sizeof(*args) + (size_t)argc * sizeof(args->args[0])); + if (args == NULL) + return NULL; + args->len = (uint32_t)argc; + + for (int i = 0; i < argc; i++) { + size_t len = strlen(argv[i]); + if (len > UINT32_MAX) { + free(args); + return NULL; + } + SimpleBytes *str = malloc(sizeof(*str) + len); + if (str == NULL) { + for (int j = 0; j < i; j++) free(args->args[j]); + free(args); + return NULL; + } + str->len = (uint32_t)len; + memcpy(str->bytes, argv[i], len); + args->args[i] = str; + } + return args; +} + +int main(int argc, char **argv) { + SimpleArgs *args = simple_args(argc, argv); + if (args == NULL) + return 1; + // The language-level hidden receiver is not part of the emitted ABI for + // free functions; source argument zero occupies native argument zero. + return (int)simple_main(args); +} diff --git a/chapter25/src/main/java/com/seaofnodes/simple/Parser.java b/chapter25/src/main/java/com/seaofnodes/simple/Parser.java index 4c2dfc84..e964fa5b 100644 --- a/chapter25/src/main/java/com/seaofnodes/simple/Parser.java +++ b/chapter25/src/main/java/com/seaofnodes/simple/Parser.java @@ -1450,8 +1450,10 @@ public class Parser { // Make an array type of t. Always record a mutable version, // but return the requested version. private TypeStruct typeAry( Type t, boolean efinal ) { - if( t instanceof TypeMemPtr tmp && tmp.notNull() ) - throw error("Arrays of reference types must always be nullable"); + // Non-null reference arrays are valid types, even though an ordinary + // zero-filled `new T[len]` cannot construct one. Allocation retains + // that check in alloc(); trusted runtimes (notably argv construction) + // may fill the private array completely before publishing it. String tname = ("[]"+t.str()).intern(); TypeStruct ta = (TypeStruct)TYPES.get(tname); if( ta==null ) { @@ -2406,22 +2408,33 @@ public class Parser { } if( _lexer.isEOF() ) throw error("Unclosed string"); - char esc = _lexer.nextChar(); - sb.append(switch( esc ) { - case 'n' -> '\n'; - case 't' -> '\t'; - case 'r' -> '\r'; - case '\\' -> '\\'; - case '"' -> '"'; - default -> throw error("Unknown string escape \\\\"+esc); - }); + sb.append(escapedChar("string")); } throw error("Unclosed string"); } + private char escapedChar(String kind) { + char esc = _lexer.nextChar(); + return switch( esc ) { + case 'n' -> '\n'; + case 't' -> '\t'; + case 'r' -> '\r'; + case '\\' -> '\\'; + case '"' -> '"'; + case '\'' -> '\''; + default -> throw error("Unknown "+kind+" escape \\\\"+esc); + }; + } + // Already parsed "'" private Node parseChar() { - return require(con(TypeInteger.constant(_lexer.nextChar())),"'"); + if( _lexer.isEOF() ) throw error("Unclosed character"); + char c = _lexer.nextChar(); + if( c=='\\' ) { + if( _lexer.isEOF() ) throw error("Unclosed character"); + c = escapedChar("character"); + } + return require(con(TypeInteger.constant(c)),"'"); } private boolean noLowerCase(String s) { 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 508cdcca..274be838 100644 --- a/chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java +++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java @@ -182,7 +182,7 @@ public class ElfWriter { // creates function and stores where it starts. // Filters to just this class.obj file. - void encodeFunctions(StopNode stop, Encoding enc, int text_idx) { + void encodeFunctions(StopNode stop, Encoding enc, int text_idx, boolean emitEntrySymbol) { for( Node stopcu : stop._inputs ) { for( Node ret : stopcu._inputs ) { FunNode fun = ((ReturnNode)ret).fun(); @@ -191,8 +191,15 @@ public class ElfWriter { int end = enc.opStart(fun.ret()) + enc.opLen(fun.ret()); long value = enc.opStart(fun); long size = end - value; - if( fun._name != null ) // Anonymous functions have no name - symbol(fun._name, text_idx, SYM_BIND_GLOBAL, SYM_TYPE_FUNC, value, size); + if( fun._name != null ) { // Anonymous functions have no name + // The C runtime owns `main` and adapts argc/argv into + // a normal Simple array. Keep the Simple function on + // its native ABI under a private entry symbol. + String name = emitEntrySymbol && "main".equals(fun._name) + ? "simple_main" + : fun._name; + symbol(name, text_idx, SYM_BIND_GLOBAL, SYM_TYPE_FUNC, value, size); + } } } } @@ -205,14 +212,14 @@ public class ElfWriter { for( FunNode fun : _code._linker ) if( fun != null && _code.owns(fun) ) { if( "main".equals(fun._name) ) - return; // Top-level declared a main, take the existing one + return; // encodeFunctions exported this as simple_main if( entryName.equals(fun._name) ) clinit = fun; // Keep top-level entry, if we don't find an explicit main } // No top-level main, so the top-level clinit becomes a default main if( clinit != null ) { assert enc.opStart(clinit) == 0; - symbol("main",text_idx, SYM_BIND_GLOBAL, SYM_TYPE_FUNC, enc.opStart(clinit), 0); + symbol("simple_main",text_idx, SYM_BIND_GLOBAL, SYM_TYPE_FUNC, enc.opStart(clinit), 0); return; } throw new IllegalStateException("Missing entry <clinit> "+entryName); @@ -293,7 +300,7 @@ public class ElfWriter { // populate function symbols - symbols.encodeFunctions(_code._stop, _code._encoding, text._index); + symbols.encodeFunctions(_code._stop, _code._encoding, text._index, emitEntrySymbol); // Export the source compilation unit <clinit> as the C runtime "main". // Encoding keeps this function at text offset zero. if( emitEntrySymbol ) diff --git a/chapter25/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java b/chapter25/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java index 5d8ed03c..fa187000 100644 --- a/chapter25/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java +++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java @@ -63,6 +63,13 @@ public abstract class GlobalCodeMotion { private static void breakUpGlobalConstantSingle( Node con ) { + // If an immediate global-value user still spans functions, it cannot + // yet be assigned to one clone. Keeping the constant at Start is a + // valid schedule; a downstream value with concrete ownership can be + // split independently. + for( Node use : con.outs() ) + if( use != null && useFun(use)==null ) + return; // While constant has users in different functions while( true ) { // Find a function user, and another function @@ -106,26 +113,29 @@ public abstract class GlobalCodeMotion { } private static FunNode useFun(Node use) { + return useFun(use,new BitSet()); + } + + private static FunNode useFun(Node use, BitSet visit) { + if( visit.get(use._nid) ) return null; + visit.set(use._nid); if( use instanceof ReturnNode ret ) return ret.fun(); if( use instanceof ParmNode parm ) return parm.fun(); CFGNode cfg = use.cfg0(); - if( cfg==null ) return null; - FunNode fun = cfg.fun(); + FunNode fun = cfg==null ? null : cfg.fun(); // Use itself is not in a function... which makes it a 2-part // constant such as happens on some chips where large constants have // to be built up in parts. PtrToInt followed by the array-body Add is // another zero-code/global-value chain, so walk through any number of // single-use pieces until reaching the function-owned use. - if( fun==null ) { - do { - assert use.nOuts()==1; - use = use.out(0); - cfg = use.cfg0(); - fun = cfg==null ? null : cfg.fun(); - } while( fun==null ); - } + if( fun==null ) + for( Node out : use.outs() ) { + FunNode f = useFun(out,(BitSet)visit.clone()); + if( f != null && fun != null && f != fun ) return null; + if( f != null ) fun = f; + } return fun; } diff --git a/chapter25/src/main/smp/sys/Scan.smp b/chapter25/src/main/smp/sys/Scan.smp index acf366f7..acf9fbb8 100644 --- a/chapter25/src/main/smp/sys/Scan.smp +++ b/chapter25/src/main/smp/sys/Scan.smp @@ -3,6 +3,7 @@ // Scanner; a simple string parser. struct Scan { int !x; // Cursor + bool !bad; // True after a malformed token u8[~] buf; // Immutable byte array, must be set in 2nd constructor new Scan = { u8[~] b -> @@ -19,6 +20,7 @@ struct Scan { // True if find required character, and skip it; else False. val match = { u8 c -> skip(); + if( x >= buf# ) return false; if( buf[x] != c ) return false; x++; return true; @@ -28,8 +30,12 @@ struct Scan { val number = { -> skip(); bool neg = match('-'); + if( x >= buf# || buf[x] < '0' || '9' < buf[x] ) { + bad = true; + return 0; + } i64 n = 0; - while( '0' <= buf[x] & buf[x] <= '9' ) + while( x < buf# && '0' <= buf[x] & buf[x] <= '9' ) n = n*10 + (buf[x++] - '0'); return neg ? -n : n; }; diff --git a/chapter25/src/main/smp/sys/aryi64.smp b/chapter25/src/main/smp/sys/aryi64.smp index 8a154f34..c2bc394f 100644 --- a/chapter25/src/main/smp/sys/aryi64.smp +++ b/chapter25/src/main/smp/sys/aryi64.smp @@ -36,6 +36,14 @@ struct aryi64 { str.undo(2).add(']'); }; + // Comma-separated form used by command-line examples. + val csv = { -> + var str = new aryu8(new u8[1]); + for( int i=0; i<len; i++ ) + str.num(buf[i]).add(',').add(' '); + str.undo(2).add('\n'); + }; + val swap = { int i, int j -> i64 tmp = buf[i]; buf[i] = buf[j]; @@ -62,9 +70,12 @@ val parseAryI64 = { u8[~] str -> // Parse number commas "123, " until something else while( s.x < s.buf# ) { nums.add(s.number()); + if( s.bad ) return null; if( !s.match(',') ) break; }; // Close the list - return s.match(']') ? nums : null; + if( !s.match(']') ) return null; + s.skip(); + return s.x == s.buf# ? nums : null; }; diff --git a/chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java index 313ae1cf..8f1c7285 100644 --- a/chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java +++ b/chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java @@ -47,6 +47,13 @@ public class Chapter25Test { assertEquals("1",Eval2.eval(code,0)); } + @Test + public void testCharacterEscapes() { + CodeGen code = new CodeGen("return '\\n'==10 && '\\t'==9 && '\\r'==13;") + .driver(CodeGen.Phase.TypeCheck); + assertEquals("1",Eval2.eval(code,0)); + } + @Test @Ignore public void testModule0() throws IOException { @@ -243,7 +250,7 @@ public class Chapter25Test { String syms = run(new String[]{"nm",obj}); assertTrue(syms, syms.contains(" U sys.io.p_noInline")); - String out = run(new String[]{"gcc",obj,sys_file.toString(),"-lm","-g","-o",exe}); + String out = run(new String[]{"gcc",obj,sys_file.toString(),TestC.runtimeObject(),"-lm","-g","-o",exe}); assertEquals("",out); String rez = run(new String[]{exe}); assertEquals(expected,rez); @@ -278,9 +285,25 @@ return rez < buf# ? 0 : sys.libc._exit(-2); File sys_file = buildTestSys(false); assertTrue("Missing "+sys_file+"; testBubbles depends on testSys building it", sys_file.exists()); String src = Files.readString( Path.of("docs/examples/BubbleSort.smp")); - TestC.run(src,"BubbleSort",new Ary<>(new String[]{SYS_BLDDIR}), - TestC.CALL_CONVENTION, null, null, - "[3, 2,-17, 999 ] ", "[-17, 2, 3, 999]", -1); + TestC.runArgs(src,"BubbleSort",new Ary<>(new String[]{SYS_BLDDIR}), + TestC.CALL_CONVENTION,"-17, 2, 3, 999\n",-1, + "[3, 2,-17, 999 ]"); + + String obj = "build/objs/BubbleSort.o"; + String exe = "build/objs/BubbleSort"+(TestC.OS.startsWith("Windows") ? ".exe" : ""); + Ary<String> libs = new Ary<>(new String[]{sys_file.toString()}); + assertEquals("1, 2, 3, 4, 4, 5\n", + TestC.gcc(obj,null,null,null,libs,exe,"[4, 5, 3, 1, 4, 2]")); + assertEquals("1, 2, 3, 4, 5\n", + TestC.gcc(obj,null,null,null,libs,exe,"[1, 2, 3, 4, 5]")); + assertEquals("1, 2, 3, 4, 5, 6, 7, 8, 9\n", + TestC.gcc(obj,null,null,null,libs,exe,"[9, 8, 7, 6, 5, 4, 3, 2, 1]")); + + String usage = "Usage: please provide a list of at least two integers to sort in the format \"[1, 2, 3, 4, 5]\"\n"; + assertEquals(usage,TestC.gcc(obj,null,null,null,libs,exe)); + assertEquals(usage,TestC.gcc(obj,null,null,null,libs,exe,"")); + assertEquals(usage,TestC.gcc(obj,null,null,null,libs,exe,"[1]")); + assertEquals(usage,TestC.gcc(obj,null,null,null,libs,exe,"[4 5 3]")); } } diff --git a/chapter25/src/test/java/com/seaofnodes/simple/TestC.java b/chapter25/src/test/java/com/seaofnodes/simple/TestC.java index 7177f765..2f737579 100644 --- a/chapter25/src/test/java/com/seaofnodes/simple/TestC.java +++ b/chapter25/src/test/java/com/seaofnodes/simple/TestC.java @@ -8,6 +8,7 @@ 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.concurrent.TimeUnit; import static org.junit.Assert.*; @@ -26,6 +27,12 @@ public abstract class TestC { 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/"; /** @@ -80,7 +87,17 @@ public abstract class TestC { 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 { // Simple file base-name example: // foo.smp -> // build/objs/foo.o - object file @@ -93,7 +110,10 @@ public abstract class TestC { CodeGen code = new CodeGen(null,"build/objs",externPaths,base,src,126L,TypeInteger.BOT); code.driver( CPU_PORT, simple_conv, false, cfile==null ); - String result = gcc(obj, c_conv, cfile, stdin, linkObjs(externPaths), exe ); + String[] execArgs = new String[programArgs.length+1]; + execArgs[0] = exe; + System.arraycopy(programArgs,0,execArgs,1,programArgs.length); + String result = gcc(obj, c_conv, cfile, stdin, linkObjs(externPaths), execArgs ); assertEquals(expected,result); // Allocation quality not degraded @@ -135,6 +155,10 @@ public abstract class TestC { params.add("gcc"); if( cfile!=null ) params.add(cfile); // Associated C driver, usually has a `main` params.add(obj); + // 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()); if( linkObjs != null ) params.addAll(linkObjs.asAry()); params.addAll(new String[] { |
