summaryrefslogtreecommitdiff
path: root/chapter25/src
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-08-12 11:20:14 -0700
committerCliff Click <cliffc@acm.org>2026-08-12 11:20:14 -0700
commit1cb8279a64f4f79615ac8358927b11237df29bf2 (patch)
treed057b4ee723a9d29e16f2c71959c0ae2773cdb63 /chapter25/src
parent628452bb9f6137d433395fae5a9f5d780fb0f1e1 (diff)
downloadSimple-1cb8279a64f4f79615ac8358927b11237df29bf2.tar.gz
Simple-1cb8279a64f4f79615ac8358927b11237df29bf2.zip
Real argc/argv args
Beef up BubbleSort error handling
Diffstat (limited to 'chapter25/src')
-rw-r--r--chapter25/src/main/c/simple_crt.c57
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/Parser.java37
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java19
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java30
-rw-r--r--chapter25/src/main/smp/sys/Scan.smp8
-rw-r--r--chapter25/src/main/smp/sys/aryi64.smp13
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java31
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/TestC.java26
8 files changed, 186 insertions, 35 deletions
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[] {