summaryrefslogtreecommitdiff
path: root/src/test
diff options
context:
space:
mode:
Diffstat (limited to 'src/test')
-rw-r--r--src/test/c/audio_test.c48
-rw-r--r--src/test/java/com/seaofnodes/simple/ArithNodeTest.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/ArrayInitTest.java211
-rw-r--r--src/test/java/com/seaofnodes/simple/AssemblyDumpTest.java61
-rw-r--r--src/test/java/com/seaofnodes/simple/AudioTest.java94
-rw-r--r--src/test/java/com/seaofnodes/simple/BrainFuckTest.java8
-rw-r--r--src/test/java/com/seaofnodes/simple/BreakoutTest.java216
-rw-r--r--src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java25
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter01Test.java9
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter17bTest.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter21Test.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter22Test.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter23Test.java5
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter25Test.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/ConstantArrayTest.java123
-rw-r--r--src/test/java/com/seaofnodes/simple/ConvertNodeTest.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/DrawTest.java229
-rw-r--r--src/test/java/com/seaofnodes/simple/EscapeFlowTest.java31
-rw-r--r--src/test/java/com/seaofnodes/simple/Eval2.java25
-rw-r--r--src/test/java/com/seaofnodes/simple/FloatComparisonNativeTest.java65
-rw-r--r--src/test/java/com/seaofnodes/simple/FloatNegationNativeTest.java21
-rw-r--r--src/test/java/com/seaofnodes/simple/GraphicsTest.java194
-rw-r--r--src/test/java/com/seaofnodes/simple/IntegerLiteralTest.java46
-rw-r--r--src/test/java/com/seaofnodes/simple/IntrinsicTest.java188
-rw-r--r--src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java25
-rw-r--r--src/test/java/com/seaofnodes/simple/MethodAccessTest.java118
-rw-r--r--src/test/java/com/seaofnodes/simple/MethodFieldsTest.java131
-rw-r--r--src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java82
-rw-r--r--src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java16
-rw-r--r--src/test/java/com/seaofnodes/simple/StreamIOTest.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/StringFormatTest.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/TestC.java5
-rw-r--r--src/test/java/com/seaofnodes/simple/TimeTest.java183
-rw-r--r--src/test/java/com/seaofnodes/simple/TypeTest.java20
-rw-r--r--src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java26
35 files changed, 2204 insertions, 29 deletions
diff --git a/src/test/c/audio_test.c b/src/test/c/audio_test.c
new file mode 100644
index 00000000..f82504c3
--- /dev/null
+++ b/src/test/c/audio_test.c
@@ -0,0 +1,48 @@
+// Explicit optional integration checks: real miniaudio thread, silent device.
+#define SIMPLE_AUDIO_TEST_NULL
+#include "../../main/c/simple_miniaudio.c"
+#include <assert.h>
+
+int main(void) {
+ assert(api.play(1)==-1 && api.playing()==0);
+ assert(api.stop()==0);
+ api.close();
+ for (int pass=0;pass<2;++pass) {
+ assert(api.open()==0 && api.open()==0);
+ assert(api.load(NULL,1)==-1);
+ int16_t pcm[RATE]={0};
+ assert(api.load(pcm,0)==-1 && api.load(pcm,RATE*10+1)==-1);
+ int sound=api.load(pcm,RATE);
+ assert(sound==4);
+ pcm[0]=12345;
+ assert(sounds[sound].pcm[0]==0);
+ assert(api.play(-1)==-1 && api.play(0)==-1 && api.play(SLOTS)==-1);
+ assert(api.play(sound)==0 && api.playing());
+ // A one-second sound returns while playback is still in progress.
+ assert(api.stop()==0 && !api.playing());
+ for (int cue=1;cue<=3;++cue) {
+ assert(api.play(cue)==0);
+ int elapsed=0;
+ while (api.playing() && elapsed<2000) { ma_sleep(10); elapsed+=10; }
+ assert(!api.playing());
+ }
+ for (int i=5;i<SLOTS;++i) assert(api.load(pcm,1)==i);
+ assert(api.load(pcm,1)==-1);
+ assert(api.play(sound)==0);
+ api.close(); // Join during playback, then reopen next pass.
+ api.close();
+ assert(api.play(sound)==-1 && !api.playing());
+ }
+ // The loss cue reverses exactly the same three softly enveloped notes.
+ assert(tone(1,0)==0 && tone(2,0)==0 && tone(3,1)==0);
+ uint32_t note=RATE*16/100;
+ for (int n=0;n<3;++n)
+ assert(memcmp(sounds[2].pcm+n*note,sounds[3].pcm+(2-n)*note,note*sizeof(int16_t))==0);
+ for (int i=1;i<=3;++i) {
+ assert(sounds[i].pcm[0]==0);
+ for (uint32_t j=0;j<sounds[i].count;++j) assert(abs(sounds[i].pcm[j])<=3200);
+ free(sounds[i].pcm); sounds[i].pcm=NULL;
+ }
+ puts("audio native checks passed");
+ return 0;
+}
diff --git a/src/test/java/com/seaofnodes/simple/ArithNodeTest.java b/src/test/java/com/seaofnodes/simple/ArithNodeTest.java
index 1e6c6fc0..b29c9706 100644
--- a/src/test/java/com/seaofnodes/simple/ArithNodeTest.java
+++ b/src/test/java/com/seaofnodes/simple/ArithNodeTest.java
@@ -65,7 +65,7 @@ public class ArithNodeTest {
TypePtr.gather(inputs);
TypeMemPtr.gather(inputs);
TypeFunPtr.gather(inputs);
- inputs.add(TypeConAryB.make(new byte[]{1,2,3}));
+ inputs.add(TypeConAry.make(new byte[]{1,2,3}));
for( long value : new long[]{Long.MIN_VALUE,Long.MIN_VALUE+1,-1,1,Long.MAX_VALUE} )
inputs.add(TypeInteger.constant(value));
for( double value : new double[]{-0.0,0.0,-1.0,1.0,16777216.0,16777217.0,Double.NaN,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY} )
diff --git a/src/test/java/com/seaofnodes/simple/ArrayInitTest.java b/src/test/java/com/seaofnodes/simple/ArrayInitTest.java
new file mode 100644
index 00000000..d298828e
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/ArrayInitTest.java
@@ -0,0 +1,211 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.node.ArrayInitNode;
+import com.seaofnodes.simple.node.ConstantNode;
+import com.seaofnodes.simple.node.Node;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
+import java.util.ArrayList;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class ArrayInitTest {
+ @Test public void constantListComputeIsMonotone() {
+ CodeGen code = new CodeGen("return 0;").parse();
+ TypeStruct layout = TypeStruct.makeAry("[]i32",TypeInteger.U32,2,TypeInteger.I32,3,true);
+ Type array = TypeMemPtr.make(layout);
+ Node ctrl = ConstantNode.seed(Type.CONTROL);
+ Node ptr = ConstantNode.seed(Type.TOP);
+ Node len = ConstantNode.seed(Type.TOP);
+ Node x = ConstantNode.seed(Type.TOP), y = ConstantNode.seed(Type.TOP);
+ ArrayInitNode init = new ArrayInitNode(ctrl,code.ZERO,ptr,len,2,null,new Ary<>(new Node[]{x,y}));
+ Type[][] inputs = {
+ {Type.TOP,TypeMemPtr.TOP,array},
+ {Type.TOP,TypeInteger.TOP,TypeInteger.TWO,TypeInteger.U32},
+ {Type.TOP,TypeInteger.TOP,TypeInteger.constant(5),TypeInteger.BOT},
+ {Type.TOP,TypeInteger.TOP,TypeInteger.constant(6),TypeInteger.BOT}
+ };
+ var states = new ArrayList<Type[]>();
+ for( Type p : inputs[0] )
+ for( Type l : inputs[1] )
+ for( Type a : inputs[2] )
+ for( Type b : inputs[3] ) {
+ ptr._type = p; len._type = l; x._type = a; y._type = b;
+ states.add(new Type[]{p,l,a,b,init.compute()});
+ }
+ for( Type[] before : states )
+ for( Type[] after : states )
+ if( before[0].isa(after[0]) && before[1].isa(after[1]) &&
+ before[2].isa(after[2]) && before[3].isa(after[3]) )
+ assertTrue("Array initializer rose from "+before[4]+" to "+after[4],before[4].isa(after[4]));
+ }
+
+ private static void value(String src, long expected) {
+ CodeGen code=new CodeGen(src).driver(CodeGen.Phase.TypeCheck);
+ assertEquals(Long.toString(expected),Eval2.eval(code,0,100000));
+ }
+
+ private static void bad(String src, String message) {
+ try {
+ new CodeGen(src).driver(CodeGen.Phase.TypeCheck);
+ fail("Expected: "+message);
+ } catch( com.seaofnodes.simple.parser.Parser.ParseException e ) {
+ assertTrue(e.getMessage(),e.getMessage().contains(message));
+ }
+ }
+
+ @Test public void lists() {
+ value("val a=new u16[3]{025755,1,65538}; return a#*100000+a[0]+a[1]+a[2];",311248);
+ value("val a=new int[]{4,7,9,}; a[1]=2; return a#+a[0]+a[1]+a[2];",18);
+ value("val a=new int[]{}; return a#;",0);
+ bad("val a=new int[2]{1}; return a[0];","initializer has 1 elements");
+ }
+
+ @Test public void generators() {
+ value("int n=6; val a=new int[n]{int i -> i*i;}; return a[5]+a#;",31);
+ value("int base=7; val a=new int[3]{int i -> return base+i;}; return a[2];",9);
+ value("val a=new int[3]{int i -> if(i==1) return 9; i+1;}; return a[0]+a[1]+a[2];",13);
+ value("val a=new int[0]{int i -> 1/i;}; return a#;",0);
+ bad("val a=new int[]{int i -> i;}; return a#;","requires a length");
+ bad("val f={-> int x=1; val a=new int[2]{int i -> x++;}; return a[0];}; return f();","captured local");
+ }
+
+ @Test public void referencesAndReadonly() {
+ value("struct S { int x; new S={i64 v -> x=v;}; }; val a=new S[~3]{int i -> new S(i+4);}; return a[2].x;",6);
+ value("val a=new u8[~]{1,2,3}; return a[1];",2);
+ value("val names=new u8[~][~2]{\"Alice\",\"Bob\"}; return names[1][0];",66);
+ value("val a=new int[ ~ ]{4,5}; return a[1];",5);
+ value("val a=new {i64 -> i64}[]{ {i64 x -> x+1;}, {i64 x -> x*2;} }; return a[1](7);",14);
+ value("struct S { int x=1; }; val a=new !S[1]{new S}; a[0].x=8; return a[0].x;",8);
+ bad("struct S { int x=1; }; val a=new !S[~1]{new S}; a[0].x=8; return a[0].x;","final field");
+ value("struct S { int x; }; val a=new S?[~2]{u32 i -> null;}; return a[1]==null;",1);
+ bad("val a=new int[~2]{1,2}; a[0]=3; return a[0];","final field");
+ bad("struct S { int x; }; val a=new S[1]{null}; return a#;","declared type");
+ }
+
+ @Test public void nativeInitializers() throws Exception {
+ TestC.runSF("""
+ val main = { ->
+ int n=8;
+ val a=new u16[~n]{int i -> i*i+1;};
+ val b=new u8[]{257,2,3};
+ return a[7]==50 && b[0]==1 && a#==8 ? 0 : 1;
+ };
+ ""","arrayInitializers","",-1);
+ }
+
+ @Test public void nestedBodies() {
+ value("""
+ val f={ ->
+ int offset=5;
+ val a=new int[4]{int i ->
+ int sum=offset;
+ for(int j=0;j<i;j++) { if(j==1) continue; sum+=j; }
+ if(i==2) return 20;
+ return sum;
+ };
+ return a[0]+a[1]+a[2]+a[3];
+ };
+ return f();
+ """,37);
+ value("""
+ val a=new int[][3]{int i ->
+ new int[2]{int j -> i*10+j;};
+ };
+ return a[2][1]+a[0][0];
+ """,21);
+ value("""
+ val a=new int[2]{int i ->
+ val twice={i64 x -> return x*2;};
+ twice(i);
+ };
+ return a[1];
+ """,2);
+ }
+
+ @Test public void orderAndIdentity() throws Exception {
+ TestC.runSF("""
+ struct Counter {
+ int calls=0;
+ val next={->calls++;};
+ };
+ struct Item {
+ int x;
+ new Item={i64 v -> x=v;};
+ };
+ val build_noInline={u32 n, !Counter c ->
+ new Item[~n]{u32 i -> new Item(c.next());};
+ };
+ val main={->
+ val c=new Counter;
+ val empty=build_noInline(0,c);
+ if(empty#!=0 || c.calls!=0) return 1;
+ val a=build_noInline(4,c);
+ if(c.calls!=4 || a[0].x!=0 || a[3].x!=3 || a[0]==a[1]) return 2;
+ val b=new int[]{c.next(),c.next(),c.next()};
+ if(b[0]!=4 || b[2]!=6) return 3;
+ val d=new int[c.next()]{i64 i -> c.next();};
+ if(d#!=7 || d[0]!=8 || d[6]!=14 || c.calls!=15) return 4;
+ return 0;
+ };
+ ""","arrayInitOrder","",-1);
+ }
+
+ @Test public void diagnostics() {
+ bad("val f={u32 n -> new int[n]{1,2};}; return f(arg)#;","length must equal that constant");
+ bad("val a=new int[2]{u8 i -> i;}; return a[0];","generator index");
+ bad("val a=new int[2]{int return -> 0;}; return a[0];","index name");
+ bad("val a=new int[2]{int i -> i++;}; return a[0];","final 'i'");
+ bad("val f={->int x=0; return new int[2]{int i -> x=i;};}; return f()#;","captured local");
+ bad("val a=new int[2]{int i -> break;}; return a[0];","No active loop");
+ bad("val a=new int[-1]{}; return a#;","outside the u32 range");
+ bad("val a=new int[1.5]{int i -> i;}; return a#;","integer");
+ bad("val a=new int[~]{1,2}; val b=a; b[0]++; return b[0];","final field");
+ for( String init : new String[]{"self","int i -> self;"} )
+ bad("struct C { int x; new C={-> val a=new C[1]{"+init+"}; x=a#;}; }; return new C().x;",
+ "fully initialized elements");
+ }
+
+ @Test public void forwardReferencesAndLoopScopes() {
+ value("""
+ val f={ ->
+ int sum=0;
+ for(int k=0;k<3;k++) {
+ val a=new int[2]{int i ->
+ if(i) return later(i);
+ later(3);
+ };
+ if(k==1) continue;
+ sum+=a[0]+a[1];
+ }
+ return sum;
+ };
+ val later={i64 i -> i*2;};
+ return f();
+ """,16);
+ }
+
+ @Test public void optimizationOrder() {
+ String src="""
+ struct Box { int x; new Box={i64 v -> x=v;}; };
+ val f={u32 n ->
+ int base=7;
+ val a=new Box[~n]{u32 i ->
+ int x=base;
+ for(int j=0;j<i;j++) x+=j;
+ if(i==2) return new Box(20);
+ new Box(x);
+ };
+ return n==0 ? 0 : a[n-1].x;
+ };
+ return f(arg&7);
+ """;
+ for( long seed : new long[]{1,123,987654321} ) {
+ CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(code,0));
+ assertEquals("20",Eval2.eval(code,3));
+ assertEquals("13",Eval2.eval(code,5));
+ }
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/AssemblyDumpTest.java b/src/test/java/com/seaofnodes/simple/AssemblyDumpTest.java
new file mode 100644
index 00000000..0e4c230a
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/AssemblyDumpTest.java
@@ -0,0 +1,61 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.BAOS;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HexFormat;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class AssemblyDumpTest {
+ @Test public void libraryUnitsAndGlyphs() throws Exception {
+ Path root=Path.of("build/objs/assembly-dump");
+ Files.createDirectories(root);
+ Path build=Files.createTempDirectory(root,"sys-");
+ CodeGen code=new CodeGen("src/main/smp",build.toString(),null,"sys",null,123,true,TypeInteger.BOT)
+ .driver(CodeGen.Phase.Serialize,TestC.CPU_PORT,TestC.CALL_CONVENTION)
+ .driver(CodeGen.Phase.Encoding,TestC.CPU_PORT,TestC.CALL_CONVENTION);
+ var entry=code._encoding;
+ String before=code.asm();
+ assertSame("Dumping another unit must not change the active encoding",entry,code._encoding);
+ for( var unit : code._compunits.values() )
+ if( unit._src!=null ) {
+ String heading="=== Compilation unit: "+unit._cname+" ===";
+ assertEquals("Exactly one heading for "+unit._cname,2,before.split(heading,-1).length);
+ }
+ int start=before.indexOf("=== Compilation unit: sys.draw.Canvas ===");
+ assertTrue(start>=0);
+ int end=before.indexOf("=== Compilation unit:",start+1);
+ String canvas=before.substring(start,end<0 ? before.length() : end);
+ assertTrue(canvas.contains("Constant Pool"));
+ assertTrue(canvas.contains("sys.draw.Canvas._glyphs: []u16"));
+ TypeMemPtr ptr=(TypeMemPtr)code._compunits.get("sys/draw/Canvas")._clz.field("_glyphs")._t;
+ TypeConAry glyphs=(TypeConAry)ptr._obj.field("[]")._t;
+ assertEquals(96,glyphs.len());
+ BAOS bytes=new BAOS();
+ glyphs.write(bytes);
+ assertTrue("The complete font follows its u32 length header",
+ canvas.contains("60000000"+HexFormat.of().formatHex(bytes.toByteArray())));
+ code.exportELF(false,false);
+ com.seaofnodes.simple.codegen.PrintRegTestSupport.checkDataAliases(
+ build.resolve("sys/draw/Canvas.o"),196,"sys.draw.Canvas._glyphs");
+ assertSame(entry,code._encoding);
+ assertEquals("Export must not truncate the dump to its entry unit",before,code.asm());
+ Files.writeString(build.resolve("sys.asm"),before);
+ }
+
+ @Test public void combinedEncodingAndEarlierPhase() {
+ for( String cpu : new String[]{"x86_64_v2","arm","riscv"} ) {
+ CodeGen code=new CodeGen("val a=new u16[~]{5,6,7}; return a[arg&1];")
+ .driver(CodeGen.Phase.RegAlloc,cpu,"SystemV");
+ assertFalse(code.asm().contains("Constant Pool"));
+ code.driver(cpu,"SystemV",true,false);
+ String dump=code.asm();
+ assertFalse(dump.contains("=== Compilation unit:"));
+ assertTrue(dump.contains("Constant Pool"));
+ assertTrue(dump.contains("03000000050006000700"));
+ }
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/AudioTest.java b/src/test/java/com/seaofnodes/simple/AudioTest.java
new file mode 100644
index 00000000..db8542d0
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/AudioTest.java
@@ -0,0 +1,94 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.util.Ary;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+// Loader and Simple/C ABI checks: no miniaudio download or audio device.
+public class AudioTest {
+ private static String missing, playback;
+
+ @BeforeClass public static void compilePrograms() throws Exception {
+ var paths=new Ary<>(new String[]{TestC.RELEASE_SYS_DIR});
+ missing=TestC.compile("""
+ val main = { ->
+ if( sys.audio.open()!=sys.audio.ERROR ) return 1;
+ if( sys.audio.boop()!=sys.audio.ERROR || sys.audio.playing() ) return 2;
+ sys.audio.stop(); sys.audio.close(); sys.audio.close();
+ sys.io.p("silent fallback\\n");
+ return 0;
+ };
+ ""","audio_missing",paths,TestC.CALL_CONVENTION,null,null,-1);
+ playback=TestC.compile("""
+ val main = { ->
+ if( sys.audio.open()!=sys.audio.OK ) return 1;
+ val pcm = new i16[3];
+ pcm[0]=-32768; pcm[1]=123; pcm[2]=32767;
+ int sound = sys.audio.load(pcm);
+ if( sound!=4 || sys.audio.play(sound)!=0 || !sys.audio.playing() ) return 2;
+ if( sys.audio.boop()!=0 || sys.audio.win()!=0 || sys.audio.lose()!=0 ) return 3;
+ sys.audio.stop();
+ if( sys.audio.playing() ) return 4;
+ sys.audio.close();
+ return 0;
+ };
+ ""","audio_playback",paths,TestC.CALL_CONVENTION,null,null,-1);
+ }
+
+ private static String run(String exe, String backend) throws Exception {
+ ProcessBuilder pb=new ProcessBuilder(exe).redirectErrorStream(true);
+ if( backend==null ) pb.environment().remove("SIMPLE_MINIAUDIO");
+ else pb.environment().put("SIMPLE_MINIAUDIO",backend);
+ Process child=pb.start();
+ try {
+ assertTrue("audio program timed out",child.waitFor(10,TimeUnit.SECONDS));
+ String out=new String(child.getInputStream().readAllBytes(),StandardCharsets.UTF_8);
+ assertEquals(out,0,child.exitValue());
+ return out;
+ } finally { if( child.isAlive() ) child.destroyForcibly(); }
+ }
+
+ @Test public void silentFallback() throws Exception {
+ assertEquals("silent fallback\n",run(missing,null));
+ assertEquals("silent fallback\n",run(missing,"off"));
+ assertTrue(run(missing,"no-such-miniaudio-backend").contains("cannot load backend"));
+ }
+
+ @Test public void nativeBoundary() throws Exception {
+ Path source=Path.of("build/objs/audio_stub.c");
+ Path library=Path.of("build/objs/audio_stub"+(TestC.OS.startsWith("Windows") ? ".dll" : ".so"));
+ Files.writeString(source,"""
+ #include "simple_audio.h"
+ static int active;
+ static int32_t open_audio(void) { return 0; }
+ static int32_t load(const int16_t *p, uint32_t n) {
+ return n==3 && p[0]==-32768 && p[1]==123 && p[2]==32767 ? 4 : -1;
+ }
+ static int32_t play(int32_t cue) { active=1; return cue>=1 && cue<=4 ? 0 : -1; }
+ static int32_t stop(void) { active=0; return 0; }
+ static int32_t playing(void) { return active; }
+ static void close_audio(void) { active=0; }
+ static const struct simple_audio_api api={1,open_audio,load,play,stop,playing,close_audio};
+ const struct simple_audio_api *simple_miniaudio_api(void) { return &api; }
+ """);
+ TestC.exec(30,"gcc","-shared","-fPIC","-Isrc/main/c",source.toString(),"-o",library.toString());
+ assertEquals("",run(playback,library.toAbsolutePath().toString()));
+ }
+
+ @Test public void incompatibleBackend() throws Exception {
+ Path source=Path.of("build/objs/audio_bad.c");
+ Path library=Path.of("build/objs/audio_bad"+(TestC.OS.startsWith("Windows") ? ".dll" : ".so"));
+ Files.writeString(source,"""
+ #include "simple_audio.h"
+ static const struct simple_audio_api api={999};
+ const struct simple_audio_api *simple_miniaudio_api(void) { return &api; }
+ """);
+ TestC.exec(30,"gcc","-shared","-fPIC","-Isrc/main/c",source.toString(),"-o",library.toString());
+ assertTrue(run(missing,library.toAbsolutePath().toString()).contains("incompatible backend"));
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/BrainFuckTest.java b/src/test/java/com/seaofnodes/simple/BrainFuckTest.java
index 63858df0..07e702c7 100644
--- a/src/test/java/com/seaofnodes/simple/BrainFuckTest.java
+++ b/src/test/java/com/seaofnodes/simple/BrainFuckTest.java
@@ -1,19 +1,21 @@
package com.seaofnodes.simple;
import com.seaofnodes.isa.eval.EvalRisc5;
-
import com.seaofnodes.isa.eval.EvalArm64;
-
import java.io.IOException;
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
import java.nio.file.Files;
import java.nio.file.Path;
-
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class BrainFuckTest {
@Test public void testBrainFuckAryu8() throws IOException {
+
+
String brain_fuck = "Hello World!\n";
String src =
"""
diff --git a/src/test/java/com/seaofnodes/simple/BreakoutTest.java b/src/test/java/com/seaofnodes/simple/BreakoutTest.java
new file mode 100644
index 00000000..7f3e56d4
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/BreakoutTest.java
@@ -0,0 +1,216 @@
+package com.seaofnodes.simple;
+
+import java.awt.image.BufferedImage;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import javax.imageio.ImageIO;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+// Exercise the actual Simple game and rasterizer in native code, without a window.
+public class BreakoutTest {
+ private static String game() throws Exception {
+ String source = Files.readString(Path.of("docs/examples/Breakout.smp"));
+ // Physics/rendering tests supply their own entry point. Input and
+ // sound are exercised by GraphicsTest with the complete program.
+ int end = source.indexOf("// Short, fading triangle waves");
+ assertTrue("Breakout physics/rendering section",end >= 0);
+ String physics = source.substring(0,end);
+ // The shared square-root helper now follows the input code.
+ int sqrt = source.indexOf("val _sqrt =");
+ if( sqrt >= end ) {
+ int close = source.indexOf("\n};",sqrt);
+ assertTrue("Square-root helper end",close >= 0);
+ physics += source.substring(sqrt,close+4);
+ }
+ return physics;
+ }
+
+ @Test public void firstServeMovesBall() throws Exception {
+ TestC.runSYS(game()+"""
+ val main = { ->
+ val g = new Game; g._reset();
+ if( g.speed != 320.0 || g.paddle != 504.0 ) return 1;
+ g._serve();
+ f64 x = g.x, y = g.y;
+ g._tick(0);
+ if( g.phase != PLAY || g.x <= x || g.y >= y ) return 2;
+ g._paddle(2000.0);
+ if( g.paddle != RIGHT-PADDLE_HALF ) return 3;
+ return 0;
+ };
+ ""","breakoutServe","",-1);
+ }
+
+ @Test public void paddleLivesAndPause() throws Exception {
+ TestC.runSYS(game()+"""
+ val main={ ->
+ val g=new Game; g._reset();
+ if( g.remaining!=192 || g.score!=0 || g.lives!=3 || g.phase!=READY ) return 1;
+ g._tick(1);
+ if( g.paddle<510.39 || g.paddle>510.41 || g.x!=g.paddle ) return 2;
+ g._paddle(-100.0); if( g.paddle != 80.0 ) return 3;
+ g._paddle(2000.0); if( g.paddle != 928.0 ) return 4;
+ g._reset();
+ g.x=g.paddle-50.0; g._rebound();
+ if( g.dx > -0.78 || g.dy>=0.0 ) return 5;
+ g.x=g.paddle+50.0; g._rebound();
+ if( g.dx < 0.78 || g.dy>=0.0 ) return 6;
+ g.x=g.paddle; g._rebound();
+ if( g.dy > -0.99 || g.dx*g.dx+g.dy*g.dy<0.999999 ) return 7;
+ g._serve(); g.paused=true;
+ f64 x=g.x, y=g.y, paddle=g.paddle;
+ g._tick(1);
+ if( g.x!=x || g.y!=y || g.paddle!=paddle ) return 8;
+ g.paused=false;
+ for( int i=0; i<3; i++ ) {
+ g._serve(); g.x=40.0; g.y=723.0; g.dx=0.0; g.dy=1.0;
+ g._tick(0);
+ if( g.lives!=2-i || g.backWall || g.sound!=5 ) return 9;
+ }
+ if( g.phase!=LOST ) return 10;
+ g._serve(); if( g.phase!=LOST ) return 11;
+ g._reset();
+ if( g.phase!=READY || g.lives!=3 || g.boosted || g.paused || g.speed!=320.0 ) return 12;
+ return 0;
+ };
+ ""","breakoutControls","",-1);
+ }
+
+ @Test public void collisionsAndBoost() throws Exception {
+ TestC.runSYS(game()+"""
+ val main={ ->
+ val g=new Game; g._reset();
+ g._serve();
+ g.x=29.1; g.y=400.0; g.dx=-1.0; g.dy=0.0;
+ g._tick(0);
+ if( g.dx<=0.0 || g.x<29.0 || g.sound!=1 ) return 1;
+ g.x=978.9; g.dx=1.0; g._tick(0);
+ if( g.dx>=0.0 || g.x>979.0 ) return 2;
+ // Front of the bottom row: one point, no acceleration or boost.
+ g.x=44.0; g.y=330.0; g.dx=0.0; g.dy=-1.0;
+ g._tick(0);
+ if( g.bricks[168]!=0 || g.score!=1 || g.dy<=0.0 || g.boosted || g.soundDepth!=1 ) return 3;
+ // Top of a brick alone does not arm the back-wall bonus.
+ g.x=44.0; g.y=146.0; g.dy=1.0;
+ g._tick(0);
+ if( g.bricks[0]!=0 || g.score!=9 || g.speed!=565.0 || g.boosted || g.soundDepth!=8 ) return 4;
+ // Back wall arms it; hitting a side still must not trigger it.
+ g.x=84.0; g.y=89.1; g.dy=-1.0;
+ g._tick(0);
+ if( !g.backWall || g.dy<=0.0 || g.boosted ) return 5;
+ g.x=60.0; g.y=160.0; g.dx=1.0; g.dy=0.0;
+ g._tick(0);
+ if( g.bricks[1]!=0 || g.dx>=0.0 || g.boosted ) return 6;
+ g.x=124.0; g.y=146.0; g.dx=0.0; g.dy=1.0;
+ g._tick(0);
+ if( !g.boosted || g.speed<932.24 || g.speed>932.26 || g.dy>=0.0 || g.sound!=3 || g.soundDepth!=8 ) return 7;
+ // Another back hit cannot multiply the speed a second time.
+ g.x=164.0; g.y=146.0; g.dy=1.0;
+ g._tick(0);
+ if( g.speed>932.26 || g.bricks[3]!=0 ) return 8;
+ // A boosted front hit cannot tunnel through the brick either.
+ g.x=84.0; g.y=331.0; g.dy=-1.0;
+ g._tick(0);
+ if( g.bricks[169]!=0 || g.dy<=0.0 || g.speed<932.24 ) return 9;
+ // Actual paddle contact disarms the trip and chooses its angle.
+ g.x=g.paddle+50.0; g.y=668.0; g.dy=1.0; g.dx=0.0;
+ g._tick(0);
+ if( g.dy>=0.0 || g.dx<0.78 || g.backWall || !g.boosted ) return 10;
+ // A lost ball clears both kinds of acceleration, preserving the wall/score.
+ int score=g.score, remaining=g.remaining;
+ g.x=40.0; g.y=723.0; g.dx=0.0; g.dy=1.0;
+ g._tick(0);
+ if( g.phase!=READY || g.lives!=2 || g.speed!=320.0 || g.depth!=1 || g.boosted ) return 11;
+ if( g.score!=score || g.remaining!=remaining ) return 12;
+ g._serve();
+ if( g.speed!=320.0 ) return 13;
+ // The new ball can earn its own boost.
+ g.sound=0; g.backWall=true;
+ g.x=204.0; g.y=146.0; g.dx=0.0; g.dy=1.0;
+ g._tick(0);
+ if( !g.boosted || g.speed<932.24 || g.sound!=3 || g.soundDepth!=8 ) return 14;
+ return 0;
+ };
+ ""","breakoutCollisions","",-1);
+ }
+
+ @Test public void gridEdgesAndGaps() throws Exception {
+ TestC.runSYS(game()+"""
+ val main = { ->
+ val g = new Game; g._reset();
+ val c = new sys.draw.Canvas(WIDTH,HEIGHT);
+ g._draw(c);
+ g._serve();
+ // The center lies below the last grid row and between columns.
+ // Both neighboring bricks overlap the ball; the left one wins.
+ g.x = 64.0; g.y = 330.0; g.dx = 0.0; g.dy = -1.0;
+ g._tick(0);
+ if( g.bricks[168] != 0 || g.bricks[169] != 1 || g.score != 1 || g.dy <= 0.0 ) return 1;
+ // A cleared neighbor must not hide the other candidate.
+ g.x = 64.0; g.y = 330.0; g.dx = 0.0; g.dy = -1.0;
+ g._tick(0);
+ if( g.bricks[169] != 0 || g.score != 2 || g.dy <= 0.0 ) return 2;
+ // Rightmost column, then a position entirely below the wall.
+ g.x = 978.0; g.y = 330.0; g.dx = 0.0; g.dy = -1.0;
+ g._tick(0);
+ if( g.bricks[191] != 0 || g.score != 3 || g.dy <= 0.0 ) return 3;
+ g.x = 44.0; g.y = 400.0; g.dx = 0.0; g.dy = -1.0;
+ g._tick(0);
+ if( g.score != 3 || g.dy >= 0.0 ) return 4;
+ // Overlapping rows: the first box contains the center but no
+ // face was crossed; continue to the next row's top face.
+ g.x = 44.0; g.y = 168.0; g.dx = 0.0; g.dy = 1.0;
+ g._tick(0);
+ if( g.bricks[0] != 1 || g.bricks[24] != 0 || g.score != 10 || g.dy >= 0.0 ) return 5;
+ // Contact just above the first row still belongs to that row.
+ g.x = 44.0; g.y = 146.0; g.dx = 0.0; g.dy = 1.0;
+ g._tick(0);
+ if( g.bricks[0] != 0 || g.score != 18 || g.dy >= 0.0 ) return 6;
+ return 0;
+ };
+ ""","breakoutGrid","",-1);
+ }
+
+ @Test public void clearEveryBrickAndRender() throws Exception {
+ TestC.runSYS(game()+"""
+ val main={ ->
+ val g=new Game; g._reset();
+ val c=new sys.draw.Canvas(WIDTH,HEIGHT);
+ g._draw(c);
+ for( int row=0; row<ROWS; row++ )
+ if( c.pixels[(BRICK_TOP+row*BRICK_DY+4)*WIDTH+28]!=COLORS[row] ) return 1;
+ if( c.pixels[675*WIDTH+504]!=CYAN || c.pixels[663*WIDTH+504]!=WHITE ) return 2;
+ // Save a native-rendered preview for manual visual review.
+ int fd=sys.io.create("build/objs/breakout-preview.bgra");
+ i64 pixels=c.pixels;
+ if( fd<0 || sys.libc.write(fd,pixels,c.pixels#*4)!=c.pixels#*4 ) return 3;
+ sys.io.close(fd);
+ g._serve();
+ // Drive through every brick's front, bottom row first.
+ for( int i=COLS*ROWS-1; i>=0; i-- ) {
+ g.x=LEFT+20.0+(i-i/COLS*COLS)*BRICK_DX;
+ g.y=BRICK_TOP+1.0+(i/COLS)*BRICK_DY+BRICK_H+RADIUS;
+ g.dx=0.0; g.dy=-1.0;
+ g._tick(0);
+ if( g.bricks[i]!=0 ) return 4;
+ }
+ if( g.phase!=WON || g.remaining!=0 || g.score!=864 || g.sound!=6 ) return 5;
+ f64 y=g.y; g._tick(0);
+ if( g.y!=y ) return 6;
+ g._reset();
+ for( int i=0; i<g.bricks#; i++ ) if( g.bricks[i]!=1 ) return 7;
+ if( g.score!=0 || g.remaining!=192 ) return 8;
+ return 0;
+ };
+ ""","breakoutClear","",-1);
+ byte[] pixels=Files.readAllBytes(Path.of("build/objs/breakout-preview.bgra"));
+ assertEquals(1008*760*4,pixels.length);
+ var image=new BufferedImage(1008,760,BufferedImage.TYPE_INT_RGB);
+ var bytes=ByteBuffer.wrap(pixels).order(ByteOrder.LITTLE_ENDIAN);
+ for(int y=0;y<760;y++) for(int x=0;x<1008;x++) image.setRGB(x,y,bytes.getInt());
+ ImageIO.write(image,"png",Path.of("build/objs/breakout-preview.png").toFile());
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java b/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java
index 31c0302b..fd816c68 100644
--- a/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java
+++ b/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java
@@ -4,10 +4,35 @@ import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.TypeInteger;
import com.seaofnodes.simple.type.TypeMem;
+import com.seaofnodes.simple.type.Type;
+import com.seaofnodes.simple.type.XInt;
import org.junit.Test;
import static org.junit.Assert.*;
public class BulkMemPhiTest {
+ @Test public void testUnescapedSliceKeepsItsOwnEscapes() {
+ CodeGen code = new CodeGen("");
+ code._phase = CodeGen.Phase.Lower;
+ Node defaults = code.con(TypeMem.START);
+ Node method = code.con(TypeMem.make(55,Type.BOTTOM,false,false,false,XInt.make(4),XInt.EMPTY));
+ MemMergeNode fields = new MemMergeNode(null,defaults);
+ fields.alias(55,method);
+ fields.init();
+ assertSame("The bulk excludes this unescaped field",TypeMem.START,fields._type);
+ Node ctrl = new CProjNode(code._start,0,"Ctrl").init();
+ Node iff = new IfNode(ctrl,code.con(TypeInteger.BOOL)).init();
+ RegionNode region = new RegionNode(null,null,
+ new CProjNode(iff,0,"True").init(),
+ new CProjNode(iff,1,"False").init()).init();
+ BulkMemPhiNode bulk = new BulkMemPhiNode("$mem",region,fields,defaults).init();
+ MemMergeNode split = (MemMergeNode)bulk.idealize();
+ Node precise = split.alias(55);
+ assertTrue("A new slice must not inherit an escape bound that excludes its method",
+ precise.compute().isa(precise._type));
+ precise.peephole();
+ assertTrue(XInt.bit(((TypeMem)precise._type)._escFs,4));
+ }
+
// Reduced from Capitalize's Lower-phase failure. A precise MemMerge slot
// may still be supplied by bulk memory which covers the requested alias.
@Test public void testBulkInPreciseSlot() {
diff --git a/src/test/java/com/seaofnodes/simple/Chapter01Test.java b/src/test/java/com/seaofnodes/simple/Chapter01Test.java
index fa7fb68a..6db79c16 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter01Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter01Test.java
@@ -41,13 +41,8 @@ public class Chapter01Test {
}
@Test
- public void testBad2() {
- try {
- new CodeGen("return 0123;").parse();
- fail();
- } catch( RuntimeException e ) {
- assertEquals("Syntax error: integer values cannot start with '0'",e.getMessage());
- }
+ public void testOctal() {
+ assertEquals("return 83;",new CodeGen("return 0123;").parse().print());
}
@Test
diff --git a/src/test/java/com/seaofnodes/simple/Chapter17bTest.java b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java
index 12e54193..8a66dcc5 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter17bTest.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java
@@ -568,8 +568,8 @@ struct _A {
return new _A(new !B?[0]).array_of_b;
""");
code.driver(CodeGen.Phase.TypeCheck);
- assertEquals("return []*B?;", code.print());
- assertEquals("*B {}?[]", Eval2.eval(code,0));
+ assertEquals("return []*Test.B?;", code.print());
+ assertEquals("*Test.B {}?[]", Eval2.eval(code,0));
}
// ---------------------------------------------------------------
diff --git a/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/src/test/java/com/seaofnodes/simple/Chapter21Test.java
index c6d558eb..14a091ac 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter21Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter21Test.java
@@ -461,6 +461,7 @@ return i;
String src =
"""
val test_sqrt = { flt x ->
+ if( x==0.0 ) return 0.0; // Avoid 0/0: unordered comparisons must stay false.
flt epsilon = 1e-15;
flt guess = x;
while( 1 ) {
@@ -531,7 +532,8 @@ val sieve = { int N ->
String sprimes = sb.p("]").toString();
// Compile, link against native C; expect the above string of primes to be printed out by C
- TestC.runC(src, "sieve", sprimes,344);
+ // Load/Phi hoisting now requires the pointer before the loop entry.
+ TestC.runC(src, "sieve", sprimes,216);
// Evaluate on RISC5 emulator; expect return of an array of primes in
// the simulated heap.
diff --git a/src/test/java/com/seaofnodes/simple/Chapter22Test.java b/src/test/java/com/seaofnodes/simple/Chapter22Test.java
index 9bebee23..c9791de4 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter22Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter22Test.java
@@ -332,9 +332,7 @@ return 0;
public void testFinalArray() {
String src = """
int N=4;
-i32[] !is = new i32[N];
-for( int i=0; i<N; i++ )
- is[i] = i*i;
+i32[~] !is = new i32[~N]{ int i -> i*i; };
val _sum = { i32[~] is -> // final array
int sum=0;
for( int i=0; i<is#; i++ )
diff --git a/src/test/java/com/seaofnodes/simple/Chapter23Test.java b/src/test/java/com/seaofnodes/simple/Chapter23Test.java
index 89e41efa..8667c0bd 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter23Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter23Test.java
@@ -279,7 +279,7 @@ return _s.peek('q');
CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck);
assertEquals("return Phi(Region,0,1);", code.print());
assertEquals("1", Eval2.eval(code, 0));
- testCPU(src,"x86_64_v2", "win64",31,null);
+ testCPU(src,"x86_64_v2", "win64",23,null);
};
@Test
@@ -303,8 +303,7 @@ return s.skip().x;
try { new CodeGen(src).driver(CodeGen.Phase.TypeCheck); fail(); }
catch( Exception e ) {
- // Bad error message, but basically requires a mutable 'x' field.
- org.junit.Assert.assertTrue(e.getMessage().startsWith("Argument #0 isa"));
+ assertEquals("Receiver for method 'skip' is read-only Test._Scan; expected writable Test._Scan",e.getMessage());
}
};
diff --git a/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/src/test/java/com/seaofnodes/simple/Chapter25Test.java
index 67e7f982..85f273a1 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter25Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter25Test.java
@@ -475,9 +475,9 @@ public class Chapter25Test {
// Elf files are sane
- // Sys depends on io, libc, text, char, collections, and array utilities.
+ // Software drawing and the optional native graphics interface.
assertArrayEquals(new String[]{"sys/adt", "sys/ary",
- "sys/aryi64", "sys/aryu8", "sys/char", "sys/io", "sys/libc", "sys/rt", "sys/scan", "sys/text"},sys_elf._deps);
+ "sys/aryi64", "sys/aryu8", "sys/audio", "sys/char", "sys/draw", "sys/gfx", "sys/integer", "sys/io", "sys/libc", "sys/math", "sys/rt", "sys/scan", "sys/text", "sys/time"},sys_elf._deps);
assertArrayEquals(new String[]{"sys"},sys_elf._units);
assertSame("class:sys" ,sys_elf._clz._name);
}
diff --git a/src/test/java/com/seaofnodes/simple/ConstantArrayTest.java b/src/test/java/com/seaofnodes/simple/ConstantArrayTest.java
new file mode 100644
index 00000000..c641487b
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/ConstantArrayTest.java
@@ -0,0 +1,123 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.codegen.StaticData;
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.parser.Parser;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class ConstantArrayTest {
+ private static StaticData.ObjectData array(CodeGen code, TypeInteger storage, int len) {
+ var arrays=code._encoding._data.entries().stream().filter(o ->
+ o.type instanceof TypeStruct ts && ts.isConAry() &&
+ ts.field("[]")._t instanceof TypeConAry c && c.storageType()==storage && c.len()==len).toList();
+ assertEquals(1,arrays.size());
+ assertTrue(arrays.getFirst().readOnly);
+ return arrays.getFirst();
+ }
+
+ @Test public void typedDataAndSharing() throws Exception {
+ String source="""
+ val a=new u16[~]{025755,65535,65538};
+ val b=new u16[~]{025755,65535,65538};
+ val fresh={ -> new u16[~]{025755,65535,65538}; };
+ val main={ ->
+ if(a!=b || a!=fresh() || a#!=3 || a[0]!=025755 || a[1]!=65535 || a[2]!=2) return 1;
+ val bytes=new u8[~]{65,66,67};
+ if(bytes!="ABC") return 2;
+ val signed=new i16[~]{-32768,-1,32767};
+ if(signed[0]!=-32768 || signed[1]!=-1 || signed[2]!=32767) return 3;
+ val ints=new i32[~]{-2147483648,2147483647};
+ if(ints[0]!=-2147483648 || ints[1]!=2147483647) return 4;
+ val uints=new u32[~]{0,4294967295};
+ if(uints[1]!=4294967295) return 5;
+ val longs=new i64[~]{1<<63,0x7fffffffffffffff};
+ if(longs[0]!=(1<<63) || longs[1]!=0x7fffffffffffffff) return 6;
+ val empty=new u16[~]{};
+ if(empty#!=0) return 7;
+ return 0;
+ };
+ """;
+ TestC.runSF(source,"constantArrayWidths","",-1);
+ CodeGen code=new CodeGen(source).driver(CodeGen.Phase.Encoding,TestC.CPU_PORT,TestC.CALL_CONVENTION);
+ StaticData.ObjectData obj=array(code,TypeInteger.U16,3);
+ assertEquals("Test.a, Test.b",obj.label());
+ assertEquals(12,obj.size); // Four-byte length, six data bytes, alignment padding.
+ byte[] bytes=code._encoding._cpool.buf();
+ assertEquals(3,bytes[obj.offset]);
+ assertEquals(025755&255,bytes[obj.offset+4]&255);
+ assertEquals(025755>>>8,bytes[obj.offset+5]&255);
+ assertEquals(255,bytes[obj.offset+6]&255);
+ assertEquals(255,bytes[obj.offset+7]&255);
+ assertNull(code._stop.walk(n -> n instanceof NewNode ? n : null));
+ com.seaofnodes.simple.codegen.PrintRegTestSupport.checkDataAliases(
+ Path.of("build/objs/constantArrayWidths.o"),12,"constantArrayWidths.a","constantArrayWidths.b");
+ }
+
+ @Test public void arbitraryProducerUsesStaticData() {
+ for(String cpu:new String[]{"x86_64_v2","arm","riscv"}) {
+ CodeGen code=new CodeGen("u16[~]? unused=null; return 0;").parse();
+ TypeStruct layout=(TypeStruct)Parser.TYPES.get("[]u16");
+ TypeConAry contents=TypeConAry.make(TypeInteger.U16,new long[]{1,65535});
+ TypeStruct array=TypeStruct.makeAry(layout._name,layout.field("#")._alias,layout.field("[]")._alias,contents);
+ Type ptr=TypeMemPtr.make((byte)2,array,true).makeRO();
+ // No initializer or allocation node: only compute() supplies the type.
+ Node producer=new ReadOnlyNode(code.ZERO) {
+ @Override public String label() { return "constant-array producer"; }
+ @Override public Type compute() { return ptr; }
+ };
+ ReturnNode ret=code._stop.walk(n -> n instanceof ReturnNode r && r.fun().isClz() ? r : null);
+ assertNotNull(ret);
+ ret.setDef(2,producer.peephole());
+ code.add(ret);
+ code.driver(CodeGen.Phase.Encoding,cpu,"SystemV");
+ StaticData.ObjectData obj=array(code,TypeInteger.U16,2);
+ assertEquals(8,obj.size);
+ }
+ }
+
+ @Test public void sideEffectsAndMutableArrays() throws Exception {
+ CodeGen eval=new CodeGen("""
+ val same_noInline={u16[~] a -> a==new u16[~]{1,2};};
+ return same_noInline(new u16[~]{1,2});
+ """).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("1",Eval2.eval(eval,0));
+ TestC.runSF("""
+ val next={int[] counter -> counter[0]++; return 7;};
+ val main={ ->
+ val count=new int[1];
+ val a=new int[~]{next(count),next(count)};
+ if(count[0]!=2 || a[0]!=7 || a[1]!=7) return 1;
+ val b=new int[]{7,7}, c=new int[]{7,7};
+ if(b==c) return 2;
+ int[~] view=b;
+ b[0]=9;
+ if(view[0]!=9 || c[0]!=7) return 3;
+ return 0;
+ };
+ ""","constantArrayEffects","",-1);
+ }
+
+ @Test public void importedTypedConstants() throws Exception {
+ Path root=Path.of("build/objs/separate");
+ Files.createDirectories(root);
+ Path lib=Files.createTempDirectory(root,"constant-arrays-");
+ new CodeGen(null,lib.toString(),null,"Table","""
+ val small=new u16[~]{1,65535};
+ val wide=new i64[~]{1<<63,0x7fffffffffffffff};
+ val uints=new u32[~]{4294967295};
+ val signed=new i32[~]{-2147483648};
+ """,126,true,TypeInteger.BOT).driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
+ var paths=new Ary<>(new String[]{lib.toString()});
+ CodeGen code=new CodeGen(null,"build/objs",paths,"TableClient","""
+ return Table.small[1]==65535 && Table.wide[0]==(1<<63) &&
+ Table.wide[1]==0x7fffffffffffffff && Table.uints[0]==4294967295 && Table.signed[0]==-2147483648;
+ """,126,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("1",Eval2.eval(code,0));
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/ConvertNodeTest.java b/src/test/java/com/seaofnodes/simple/ConvertNodeTest.java
index 96d73f26..bdda3171 100644
--- a/src/test/java/com/seaofnodes/simple/ConvertNodeTest.java
+++ b/src/test/java/com/seaofnodes/simple/ConvertNodeTest.java
@@ -28,7 +28,7 @@ public class ConvertNodeTest {
for( boolean arrayRO : new boolean[]{false,true} )
inputs.add(TypeMemPtr.make(TypeStruct.makeAry("[]test",len,-1,
TypeMemPtr.TEST.withAccess(elementRO),-2,false)).withAccess(arrayRO));
- inputs.add(TypeConAryB.make(new byte[]{1,2,3}));
+ inputs.add(TypeConAry.make(new byte[]{1,2,3}));
for( long value : new long[]{Long.MIN_VALUE,-129,-128,-1,0,1,127,128,255,256,Long.MAX_VALUE} )
inputs.add(TypeInteger.constant(value));
for( double value : new double[]{-0.0,0.0,1.0,16777217.0,Double.NaN,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY} )
diff --git a/src/test/java/com/seaofnodes/simple/DrawTest.java b/src/test/java/com/seaofnodes/simple/DrawTest.java
new file mode 100644
index 00000000..1ca161cf
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/DrawTest.java
@@ -0,0 +1,229 @@
+package com.seaofnodes.simple;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Test;
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.util.Ary;
+import static org.junit.Assert.*;
+
+// Native raster and game checks; no backend or display is needed.
+public class DrawTest {
+ @Test public void nativeClockCoordinates() throws Exception {
+ String clock=Files.readString(Path.of("docs/examples/Clock.smp"))
+ .replace("val main =","val _windowMain =");
+ StringBuilder source=new StringBuilder(clock+"""
+ val _check_noInline={ f64 turn, int expectedX, int expectedY ->
+ int x=360+_sin(turn)*128, y=232-_sin(turn+0.25)*128;
+ if( x<expectedX-1 || x>expectedX+1 || y<expectedY-1 || y>expectedY+1 ) {
+ sys.io.p("turn %f(turn): %(x),%(y) expected %(expectedX),%(expectedY)\\n");
+ return 1;
+ }
+ return 0;
+ };
+ val main={ ->
+ """);
+ for(int i=0;i<=60;i++) {
+ double turn=i/60.0;
+ int x=(int)(360+clockSine(turn)*128), y=(int)(232-clockSine(turn+0.25)*128);
+ source.append("if(_check_noInline(").append(turn).append(',').append(x).append(',').append(y)
+ .append(")!=0) return 1;");
+ }
+ source.append("return 0; };");
+ TestC.runSYS(source.toString(),"clockCoordinates","",-1);
+ }
+
+ private static double clockSine(double turn) {
+ turn-=(long)turn;
+ boolean negative=turn>0.5;
+ double x=(negative ? turn-0.5 : turn)*6.283185307179586;
+ double product=x*(3.141592653589793-x);
+ return (negative ? -16.0 : 16.0)*product/(49.34802200544679-4*product);
+ }
+
+ @Test public void nativeAnalogSweep() throws Exception {
+ String clock=Files.readString(Path.of("docs/examples/Clock.smp"))
+ .replace("val main =","val _windowMain =");
+ StringBuilder source=new StringBuilder(clock+"""
+ val _renderCheck_noInline={ int second, int millisecond, int expectedX, int expectedY ->
+ val c=new sys.draw.Canvas(720,540);
+ _analog(c,3,20,second,millisecond);
+ var found=false;
+ for( int y=expectedY-2; y<=expectedY+2; y++ )
+ for( int x=expectedX-2; x<=expectedX+2; x++ )
+ if( c.pixels[y*720+x]==0xc3ffcb ) found=true;
+ if( !found ) {
+ sys.io.p("missing second hand %(second).%(millisecond) expected %(expectedX),%(expectedY)\\n");
+ return 1;
+ }
+ // Every dial mark/hand must stay within the dial's bounds.
+ for( int y=0; y<540; y++ ) for( int x=0; x<720; x++ )
+ if( (x<204 || x>516 || y<76 || y>388) && c.pixels[y*720+x]!=0 ) {
+ sys.io.p("stray pixel %(x),%(y) color %x(c.pixels[y*720+x]) at %(second).%(millisecond)\\n");
+ return 2;
+ }
+ return 0;
+ };
+ val main={ ->
+ """);
+ for(int millis:new int[]{0,500,7500,15000,30000,45000,59500}) {
+ double turn=millis/60000.0;
+ int x=(int)(360+clockSine(turn)*128), y=(int)(232-clockSine(turn+0.25)*128);
+ source.append("if(_renderCheck_noInline(").append(millis/1000).append(',').append(millis%1000)
+ .append(',').append(x).append(',').append(y).append(")!=0) return 1;");
+ }
+ source.append("return 0; };");
+ TestC.runSYS(source.toString(),"clockAnalogSweep","",-1);
+ }
+
+ @Test public void buttonInteractionAndRendering() throws Exception {
+ TestC.runSYS("""
+ val main={ ->
+ val b=new sys.draw.Button(10,10,60,30,"GO");
+ if( !b.contains(10,10) || b.contains(70,20) || b.contains(20,40) ) return 1;
+ if( b.update(20,20,true) || !b.pressed || b.update(20,20,true) ) return 2;
+ if( !b.update(20,20,false) || b.pressed || b.update(20,20,false) ) return 3;
+ if( b.update(0,0,true) || b.update(20,20,true) || b.update(20,20,false) ) return 4;
+ b.update(20,20,true);
+ if( b.update(0,0,false) ) return 5;
+ b.update(20,20,true); b.enabled=false;
+ if( b.update(20,20,true) || b.pressed || b.update(20,20,false) ) return 6;
+ b.enabled=true;
+ val c=new sys.draw.Canvas(90,60);
+ b.update(20,20,false); b.draw(c,2,9,3,5);
+ if( c.pixels[10*90+10]!=9 || c.pixels[13*90+13]!=5 || c.pixels[0]!=0 ) return 7;
+ b.update(0,0,false); b.draw(c,2,9,3,5);
+ if( c.pixels[13*90+13]!=3 ) return 8;
+ return 0;
+ };
+ ""","drawButton","",-1);
+ }
+
+ @Test public void clockFacesWithoutWindow() throws Exception {
+ String clock=Files.readString(Path.of("docs/examples/Clock.smp"))
+ .replace("val main =","val _windowMain =");
+ CodeGen code=new CodeGen(null,null,new Ary<>(new String[]{TestC.RELEASE_SYS_DIR}),"ClockRaster",clock+"""
+ val main={ ->
+ val c=new sys.draw.Canvas(720,540);
+ _digit(c,10,10,8);
+ if( c.pixels[10*720+40]!=GREEN || c.pixels[82*720+40]!=GREEN || c.pixels[154*720+40]!=GREEN ) return 1;
+ _digit(c,10,10,1);
+ if( c.pixels[10*720+40]!=DIM || c.pixels[40*720+78]!=GREEN || c.pixels[40*720+14]!=DIM ) return 2;
+ c.clear(BACKGROUND); _digital(c,23,59,58);
+ if( c.pixels[214*720+254]!=GREEN || c.pixels[262*720+470]!=GREEN ) return 3;
+ c.clear(BACKGROUND); _analog(c,3,0,0,0);
+ if( c.pixels[232*720+430]!=GREEN || c.pixels[160*720+360]!=0xc3ffcb || c.pixels[232*720+360]!=GREEN ) return 4;
+ if( _sin(0.25)<0.999 || _sin(0.75)>-0.999 || _sin(1.0)!=0.0 ) return 5;
+ return 0;
+ };
+ return main();
+ """,126L,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(code,0,3000000));
+ }
+
+ @Test public void rectanglesAndColors() throws Exception {
+ TestC.runSYS("""
+ val main = { ->
+ val c = new sys.draw.Canvas(12,10);
+ u32 color = sys.draw.rgb(300,-1,128);
+ if( color!=16711808 ) return 1;
+ c.clear(7).fillRect(-2,-1,5,4,color);
+ for( int y=0; y<10; y++ )
+ for( int x=0; x<12; x++ )
+ if( c.pixels[y*12+x]!=(x<3 && y<3 ? color : 7) ) return 2;
+ c.clear(0).rect(2,2,7,6,1,9);
+ for( int y=0; y<10; y++ )
+ for( int x=0; x<12; x++ ) {
+ var inside = 2<=x<9 && 2<=y<8;
+ var border = inside && (x==2 || x==8 || y==2 || y==7);
+ if( c.pixels[y*12+x]!=(border ? 9 : 0) ) return 3;
+ }
+ c.clear(5).fillRect(20,0,8,8,1).rect(0,0,12,10,0,1);
+ c.fillRect(0,0,-1,5,1).pixel(-1,0,1).pixel(12,9,1);
+ for( int i=0; i<c.pixels#; i++ ) if( c.pixels[i]!=5 ) return 4;
+ return 0;
+ };
+ ""","drawRectangles","",-1);
+ }
+
+ @Test public void circlesLinesAndText() throws Exception {
+ TestC.runSYS("""
+ val main = { ->
+ val c = new sys.draw.Canvas(16,16);
+ c.fillCircle(8,8,3,1);
+ for( int y=0; y<16; y++ )
+ for( int x=0; x<16; x++ )
+ if( c.pixels[y*16+x]!=((x-8)*(x-8)+(y-8)*(y-8)<=9 ? 1 : 0) ) {
+ sys.io.p("circle mismatch %(x) %(y): %(c.pixels[y*16+x])\\n");
+ return 1;
+ }
+ c.clear(0).circle(8,8,3,1,2);
+ if( c.pixels[8*16+8]!=0 || c.pixels[8*16+11]!=2 || c.pixels[8*16+10]!=0 ) return 2;
+ c.clear(0).circle(8,8,3,3,2);
+ if( c.pixels[8*16+8]!=2 ) return 8;
+ c.clear(0).line(-5,8,20,8,3,3);
+ for( int y=0; y<16; y++ )
+ for( int x=0; x<16; x++ )
+ if( c.pixels[y*16+x]!=(7<=y<=9 ? 3 : 0) ) return 3;
+ c.clear(0).line(8,8,8,8,2,4);
+ if( c.pixels[8*16+8]!=4 || c.pixels[8*16+9]!=4 || c.pixels[9*16+9]!=0 ) return 4;
+ c.clear(0).line(2,2,13,13,1,5);
+ if( c.pixels[7*16+7]!=5 || c.pixels[7*16+8]!=0 ) return 5;
+ c.clear(0).text(0,0,"x",1,6).text(8,0,"X",1,6);
+ for( int y=0; y<5; y++ )
+ for( int x=0; x<3; x++ )
+ if( c.pixels[y*16+x]!=c.pixels[y*16+x+8] ) return 6;
+ c.clear(0).text(0,0,":/",2,6);
+ for( int y=0; y<10; y++ )
+ for( int x=0; x<16; x++ ) {
+ int row=y/2, col=x/2;
+ var colon=col==1 && (row==1 || row==3);
+ var slash=(row<2 && col==6) || (row==2 && col==5) || (row>2 && col==4);
+ if( c.pixels[y*16+x]!=((colon || slash) ? 6 : 0) ) return 9;
+ }
+ c.clear(0).fillCircle(-1,-1,2,7);
+ if( c.pixels[0]!=7 || c.pixels[2]!=0 ) return 7;
+ return 0;
+ };
+ ""","drawCurves","",-1);
+ }
+
+ @Test public void graphicalGameWithoutWindow() throws Exception {
+ String game=Files.readString(Path.of("docs/examples/TicTacToeGraphical.smp"))
+ .replace("val main =","val _windowMain =");
+ TestC.runSYS(game+"""
+
+ val main = { ->
+ val board = new u8[9];
+ _reset(board);
+ if( _square(48,168)!=0 || _square(551,671)!=8 || _square(552,400)!=-1 || _square(100,167)!=-1 ) return 1;
+ if( !_move(board,4) || board[4]!='X' || board[0]!='O' ) return 2;
+ if( _sound(board)!=sys.audio.BOOP ) return 10;
+ if( _move(board,4) || _move(board,0) || _move(board,-1) || _move(board,9) ) return 3;
+ val moves = "1879";
+ _reset(board);
+ for( int i=0; i<moves#; i++ )
+ if( !_move(board,moves[i]-'1') ) return 4;
+ if( _winner(board)!='X' || _move(board,1) ) return 5;
+ if( _sound(board)!=sys.audio.WIN ) return 11;
+ val screen = new sys.draw.Canvas(600,780);
+ _draw(screen,board,-1);
+ if( screen.pixels[252*600+132]==screen.pixels[0] ) return 6;
+ _reset(board);
+ val draw = "12768";
+ for( int i=0; i<draw#; i++ ) _move(board,draw[i]-'1');
+ if( !_finished(board) || _winner(board)!=0 ) return 7;
+ _reset(board);
+ _move(board,0); _move(board,1); _move(board,3);
+ if( _winner(board)!='O' ) return 8;
+ if( _sound(board)!=sys.audio.LOSE ) return 12;
+ _draw(screen,board,-1);
+ _reset(board);
+ if( _finished(board) ) return 9;
+ _draw(screen,board,4);
+ return 0;
+ };
+ ""","drawGame","",-1);
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java b/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java
index 5a20388b..12634f24 100644
--- a/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java
+++ b/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java
@@ -7,6 +7,37 @@ import org.junit.Test;
import static org.junit.Assert.*;
public class EscapeFlowTest {
+ @Test public void testArrayGeneratorDoesNotPublishUnrelatedFunctions() {
+ for( long seed : new long[]{1,123,987654321} )
+ for( String access : new String[]{"","~"} ) {
+ CodeGen code=new CodeGen("""
+ val _hidden_noInline={i64 x -> x+1;};
+ val data=new i32[%s(arg&7)]{int i -> i*i;};
+ return data#;
+ """.formatted(access),seed,true).parse();
+ int hidden=named(code,"_hidden_noInline").sig().fidx();
+ code.driver(CodeGen.Phase.TypeCheck);
+ TypeMem outside=((TypeTuple)code._start._type).mem();
+ assertFalse(XInt.bit(outside._escFs,hidden));
+ assertEquals("0",Eval2.eval(code,0));
+ assertEquals("4",Eval2.eval(code,4));
+ }
+ }
+
+ @Test public void testArrayGeneratorPublishesItsCallbacks() {
+ CodeGen code=new CodeGen("""
+ val _callback_noInline={i64 x -> x+1;};
+ val callbacks=new {i64 -> i64}[~3]{int i -> _callback_noInline;};
+ return callbacks;
+ """).parse();
+ FunNode callback=named(code,"_callback_noInline");
+ int fidx=callback.sig().fidx();
+ code.driver(CodeGen.Phase.TypeCheck);
+ assertTrue(XInt.bit(((TypeTuple)code._start._type).mem()._escFs,fidx));
+ assertFalse(callback.isDead());
+ assertTrue(callback.unknownCallers());
+ }
+
// A cofinite outside set must not revive private allocation helpers.
@Test public void testOpaqueRoundTrip() {
for( long seed : new long[]{1,123,987654321} ) {
diff --git a/src/test/java/com/seaofnodes/simple/Eval2.java b/src/test/java/com/seaofnodes/simple/Eval2.java
index 8d0e3d5b..eef05f93 100644
--- a/src/test/java/com/seaofnodes/simple/Eval2.java
+++ b/src/test/java/com/seaofnodes/simple/Eval2.java
@@ -110,6 +110,7 @@ public abstract class Eval2 {
// Class pointers and initializer self parameters share one object per run.
private static final HashMap<String,Object[]> CLASSES = new HashMap<>();
+ private static final IdentityHashMap<TypeStruct,Object[]> ARRAYS = new IdentityHashMap<>();
public static String eval( CodeGen code, long arg ) { return eval(code,arg,1000); }
public static String eval( CodeGen code, long arg, int timeout ) {
@@ -122,6 +123,7 @@ public abstract class Eval2 {
// selection. Objects loaded later are scheduled independently.
code.driver(CodeGen.Phase.LocalSched);
CLASSES.clear();
+ ARRAYS.clear();
// Timer for when we run too long. Ticks once per backedge or function call
int loopCnt = 0;
@@ -334,6 +336,8 @@ public abstract class Eval2 {
case RoundF32Node rnd -> (double)(float)d(rnd.in(1));
case ToIntegerNode cvt -> (long)d(cvt.in(1));
case ToFloatNode toflt-> (double)x(toflt.in(1));
+ case Intrinsic.F64BitsNode cast -> Double.doubleToRawLongBits(d(cast.in(1)));
+ case Intrinsic.F64FromBitsNode cast -> Double.longBitsToDouble(x(cast.in(1)));
case XorNode xor -> x(xor.in(1)) ^ x(xor.in(2));
default -> throw Utils.TODO();
};
@@ -361,9 +365,12 @@ public abstract class Eval2 {
if( tmp._obj.isAry() ) { // Constant array ptr
Type elem = tmp._obj.field("[]")._t;
if( elem instanceof TypeConAry con ) {
+ Object[] old=ARRAYS.get(tmp._obj);
+ if( old!=null ) yield old;
Object[] xs = new Object[con.len()];
for( int i=0; i<con.len(); i++ )
xs[i] = con.at8(i);
+ ARRAYS.put(tmp._obj,xs);
yield xs;
} else {
// Generic constant array, used as a default input to a
@@ -388,7 +395,7 @@ public abstract class Eval2 {
TypeMem mem = (TypeMem)con._type;
Object val = con(mem._t);
TypeMemPtr tmp = (TypeMemPtr)esc.self()._type;
- int idx = tmp._obj.findAlias(esc.fld()._alias);
+ int idx = layout(tmp._obj).findAlias(esc.fld()._alias);
Object[] fs = (Object[])val(esc.self());
if( tmp._obj.isAry() ) {
assert idx==0; // length field
@@ -443,10 +450,20 @@ public abstract class Eval2 {
private static Object[] classObject(TypeStruct type) {
Object[] ptr = CLASSES.get(type._name);
- if( ptr==null ) CLASSES.put(type._name,ptr=alloc(type));
+ if( ptr==null ) CLASSES.put(type._name,ptr=alloc(layout(type)));
return ptr;
}
+ // Imported pointer views may retain parsing-only forward-reference fields.
+ // Like native field offsets, evaluator slots use the module's final layout.
+ private static TypeStruct layout(TypeStruct type) {
+ if( TypeStruct.startsClzPrefix(type._name) ) {
+ CompUnit cu=CODE._compunits.get(type._name.substring(6).replace('.','/'));
+ if( cu!=null ) return cu._clz;
+ }
+ return type;
+ }
+
private static Object not( NotNode not ) {
Object n = val(not.in(1));
return ( n==null || (n instanceof Double D && D==0.0) || n instanceof Long L && L==0 )
@@ -465,7 +482,7 @@ public abstract class Eval2 {
TypeMemPtr tmp = (TypeMemPtr)ld.ptr()._type;
int idx = tmp._obj.isAry()
? offToIdx(x(ld.off()),tmp._obj)
- : tmp._obj.find(ld._name);
+ : layout(tmp._obj).find(ld._name);
return fs[idx];
}
@@ -476,7 +493,7 @@ public abstract class Eval2 {
Object[] fs = (Object[])val(st.ptr());
int idx = tmp._obj.isAry()
? offToIdx(x(st.off()),tmp._obj)
- : tmp._obj.find(st._name);
+ : layout(tmp._obj).find(st._name);
Object val = val(st.val());
// Idealization can remove conversions already performed by the store.
Type type = st.declaredType();
diff --git a/src/test/java/com/seaofnodes/simple/FloatComparisonNativeTest.java b/src/test/java/com/seaofnodes/simple/FloatComparisonNativeTest.java
new file mode 100644
index 00000000..c0d8be3a
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/FloatComparisonNativeTest.java
@@ -0,0 +1,65 @@
+package com.seaofnodes.simple;
+
+import org.junit.Test;
+
+public class FloatComparisonNativeTest extends TestC {
+ @Test public void longFloatingBranches() throws Exception {
+ StringBuilder source=new StringBuilder("val far_noInline={f64 a,f64 b,i64[] out -> if(a<b){");
+ for(int i=0;i<32;i++) source.append("out[").append(i).append("]=").append(i+1).append(';');
+ source.append("""
+ return 1; } return 0; };
+ val main={ ->
+ val out=new i64[32];
+ if( far_noInline(1.0,2.0,out)!=1 || out[31]!=32 ) return 1;
+ out[31]=0;
+ if( far_noInline(2.0,1.0,out)!=0 || out[31]!=0 ) return 2;
+ f64 nan=1e300*1e300-1e300*1e300;
+ if( far_noInline(nan,1.0,out)!=0 || out[31]!=0 ) return 3;
+ if( far_noInline(1.0,nan,out)!=0 || out[31]!=0 ) return 4;
+ return 0;
+ };
+ """);
+ runSYS(source.toString(),"floatLongBranches","",-1);
+ }
+
+ @Test public void branchesAndValues() throws Exception {
+ String[] literals={"-1.0","0.0","2.0","1e300*1e300","-1e300*1e300",
+ "1e300*1e300-1e300*1e300"};
+ double[] values={-1,0,2,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY,Double.NaN};
+ StringBuilder source=new StringBuilder("""
+ val branch_noInline={ f64 a, f64 b ->
+ int bits=0;
+ if( a<b ) bits+=1;
+ if( a<=b ) bits+=2;
+ if( a==b ) bits+=4;
+ if( a!=b ) bits+=8;
+ if( a>=b ) bits+=16;
+ if( a>b ) bits+=32;
+ return bits;
+ };
+ val value_noInline={ f64 a, f64 b ->
+ val lt=a<b, le=a<=b, eq=a==b, ne=a!=b, ge=a>=b, gt=a>b;
+ return lt+le*2+eq*4+ne*8+ge*16+gt*32;
+ };
+ val check_noInline={ f64 a, f64 b, int expected, int tag ->
+ val branches=branch_noInline(a,b), values=value_noInline(a,b);
+ if( branches!=expected || values!=expected ) {
+ sys.io.p("case %(tag): branches %(branches), values %(values), expected %(expected)\\n");
+ return 1;
+ }
+ return 0;
+ };
+ val main={ ->
+ """);
+ for(int i=0;i<values.length;i++) for(int j=0;j<values.length;j++) {
+ double a=values[i],b=values[j];
+ int expected=(a<b ? 1 : 0)|(a<=b ? 2 : 0)|(a==b ? 4 : 0)|
+ (a!=b ? 8 : 0)|(a>=b ? 16 : 0)|(a>b ? 32 : 0);
+ source.append("if(check_noInline(").append(literals[i]).append(',').append(literals[j])
+ .append(',').append(expected).append(',').append(i*values.length+j)
+ .append(")!=0) return 1;");
+ }
+ source.append("return 0; };");
+ runSYS(source.toString(),"floatComparisons","",-1);
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/FloatNegationNativeTest.java b/src/test/java/com/seaofnodes/simple/FloatNegationNativeTest.java
new file mode 100644
index 00000000..dbbe353a
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/FloatNegationNativeTest.java
@@ -0,0 +1,21 @@
+package com.seaofnodes.simple;
+
+import org.junit.Test;
+
+public class FloatNegationNativeTest extends TestC {
+ @Test public void negateRuntimeValues() throws Exception {
+ // Keep negation out of constant folding, including both signs of zero.
+ runSYS("""
+ val neg_noInline={f64 x -> -x;};
+ val main={ ->
+ if( neg_noInline(2.5)!=-2.5 || neg_noInline(-3.75)!=3.75 ) return 1;
+ if( 1.0/neg_noInline(0.0)!=-1e300*1e300 ) return 2;
+ if( 1.0/neg_noInline(-0.0)!=1e300*1e300 ) return 3;
+ if( neg_noInline(1e300*1e300)!=-1e300*1e300 ) return 4;
+ f64 nan=neg_noInline(1e300*1e300-1e300*1e300);
+ if( nan==nan ) return 5;
+ return 0;
+ };
+ ""","floatNegation","",-1);
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/GraphicsTest.java b/src/test/java/com/seaofnodes/simple/GraphicsTest.java
new file mode 100644
index 00000000..110a6a38
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/GraphicsTest.java
@@ -0,0 +1,194 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.util.Ary;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+// These tests never open a window and do not need MiniFB installed.
+public class GraphicsTest {
+ private static String graphics, text, game, clock, breakout;
+
+ @BeforeClass public static void compilePrograms() throws Exception {
+ var paths = new Ary<>(new String[]{TestC.RELEASE_SYS_DIR});
+ graphics = TestC.compile(Files.readString(Path.of("docs/examples/Graphics.smp")),
+ "graphics_missing",paths,TestC.CALL_CONVENTION,null,null,-1);
+ text = TestC.compile("val main={ -> sys.io.p(\"text still works\\n\"); return 0; };",
+ "graphics_text",paths,TestC.CALL_CONVENTION,null,null,-1);
+ game = TestC.compile(Files.readString(Path.of("docs/examples/TicTacToeGraphical.smp")),
+ "graphics_game",paths,TestC.CALL_CONVENTION,null,null,-1);
+ clock = TestC.compile(Files.readString(Path.of("docs/examples/Clock.smp")),
+ "graphics_clock",paths,TestC.CALL_CONVENTION,null,null,-1);
+ breakout = TestC.compile(Files.readString(Path.of("docs/examples/Breakout.smp")),
+ "graphics_breakout",paths,TestC.CALL_CONVENTION,null,null,-1);
+ }
+
+ private static String run(String exe, String backend, int exit) throws Exception {
+ return run(exe,backend,exit,null);
+ }
+
+ private static String run(String exe, String backend, int exit, Path cwd) throws Exception {
+ ProcessBuilder pb = new ProcessBuilder(Path.of(exe).toAbsolutePath().toString()).redirectErrorStream(true);
+ if (cwd!=null) pb.directory(cwd.toFile());
+ if( backend == null ) pb.environment().remove("SIMPLE_MINIFB");
+ else pb.environment().put("SIMPLE_MINIFB",backend);
+ Process child = pb.start();
+ try {
+ assertTrue("native program timed out",child.waitFor(10,TimeUnit.SECONDS));
+ String output = new String(child.getInputStream().readAllBytes(),java.nio.charset.StandardCharsets.UTF_8);
+ assertEquals(output,exit,child.exitValue());
+ return output;
+ } finally { if( child.isAlive() ) child.destroyForcibly(); }
+ }
+
+ @Test public void missingConfiguration() throws Exception {
+ Path dir=Files.createTempDirectory(Path.of("build/objs"),"graphics-missing-");
+ Path bin=Files.createDirectory(dir.resolve("bin"));
+ Path exe=Files.copy(Path.of(graphics),bin.resolve(Path.of(graphics).getFileName()));
+ String output = run(exe.toString(),null,1,dir);
+ assertTrue(output,output.contains("MiniFB backend not found"));
+ assertTrue(output,output.contains("make graphics"));
+ }
+
+ @Test public void discoverBackendWithoutConfiguration() throws Exception {
+ Path root=Files.createTempDirectory(Path.of("build/objs"),"graphics-discovery-");
+ String abi=TestC.OS.startsWith("Windows") ? "x86_64_v2_win64" : "x86_64_v2_SystemV";
+ String name=TestC.OS.startsWith("Windows") ? "simple_minifb.dll" : "libsimple_minifb.so";
+ Path source=root.resolve("backend.c");
+ Files.writeString(source,"""
+ #include "simple_graphics.h"
+ #include <stdio.h>
+ static int32_t open_window(const char *s,uint32_t w,uint32_t h) {
+ puts("discovered mock MiniFB"); return -1;
+ }
+ static int32_t present(const uint32_t *p,uint32_t n) { return 0; }
+ static int32_t poll_events(int32_t n) { return 0; }
+ static int32_t position(void) { return 0; }
+ static int32_t wait_events(int64_t n) { return 0; }
+ static void close_window(void) {}
+ static const struct simple_graphics_api api={SIMPLE_GRAPHICS_VERSION,
+ open_window,present,poll_events,position,position,poll_events,poll_events,close_window,wait_events};
+ const struct simple_graphics_api *simple_minifb_api(void) { return &api; }
+ """);
+ Path mock=root.resolve(name);
+ TestC.exec(30,"gcc","-shared","-fPIC","-Isrc/main/c",source.toString(),"-o",mock.toString());
+ Path cwd=Files.createDirectory(root.resolve("empty"));
+ Path bin=Files.createDirectory(root.resolve("bin"));
+ Path exe=Files.copy(Path.of(graphics),bin.resolve(Path.of(graphics).getFileName()));
+ Path beside=Files.copy(mock,bin.resolve(name));
+ assertTrue(run(exe.toString(),null,1,cwd).contains("discovered mock MiniFB"));
+ // Explicit bad overrides must not silently fall back to the mock.
+ assertTrue(run(exe.toString(),"no-such-backend",1,cwd).contains("cannot load MiniFB"));
+ Files.delete(beside);
+ Path release=Files.createDirectories(root.resolve("build/release/"+abi));
+ Files.copy(mock,release.resolve(name));
+ Path examples=Files.createDirectories(root.resolve("build/examples/"+abi));
+ Path example=Files.copy(exe,examples.resolve(exe.getFileName()));
+ assertTrue(run(example.toString(),null,1,cwd).contains("discovered mock MiniFB"));
+ assertTrue(run(exe.toString(),null,1,root).contains("discovered mock MiniFB"));
+ }
+
+ @Test public void disabledGraphics() throws Exception {
+ assertTrue(run(graphics,"off",1).contains("disabled"));
+ }
+
+ @Test public void gameWithoutBackend() throws Exception {
+ assertTrue(run(game,"off",1).contains("disabled"));
+ }
+
+ @Test public void clockWithoutBackend() throws Exception {
+ assertTrue(run(clock,"off",1).contains("disabled"));
+ }
+
+ @Test public void breakoutWithoutBackend() throws Exception {
+ assertTrue(run(breakout,"off",1).contains("disabled"));
+ }
+
+ @Test public void breakoutInputAndFrames() throws Exception {
+ Path source=Path.of("build/objs/breakout_input.c");
+ Path library=Path.of("build/objs/breakout_input"+(TestC.OS.startsWith("Windows") ? ".dll" : ".so"));
+ Files.writeString(source,"""
+ #include "simple_graphics.h"
+ #include <stdio.h>
+ #include <unistd.h>
+ static int frames, previous_ball, previous_paddle;
+ static int32_t open_window(const char *s,uint32_t w,uint32_t h) {
+ return w==1008 && h==760 ? 0 : -1;
+ }
+ static int32_t present(const uint32_t *p,uint32_t n) {
+ if(n!=1008*760) return -1;
+ int paddle=-1,ball=-1;
+ for(int x=24;x<984;x++) if(p[675*1008+x]==0x4ce0ef) { paddle=x; break; }
+ for(int y=560;y<674 && ball<0;y++)
+ for(int x=24;x<984;x++) if(p[y*1008+x]==0xeef7ff) { ball=y; break; }
+ int ok=paddle>=0 && ball>=0;
+ if(frames==0) ok=ok && paddle==448 && ball==658;
+ if(frames==1 || frames==6 || frames==9) ok=ok && ball<previous_ball;
+ if(frames==2 || frames==3) ok=ok && paddle>previous_paddle;
+ if(frames==4 || frames==5) ok=ok && paddle==previous_paddle && ball==previous_ball;
+ if(frames==7) ok=ok && paddle==584;
+ if(frames==8) ok=ok && paddle==448 && ball==658;
+ if(!ok) {
+ printf("bad frame %d: paddle %d (was %d), ball %d (was %d)\\n",
+ frames,paddle,previous_paddle,ball,previous_ball);
+ return -1;
+ }
+ previous_ball=ball; previous_paddle=paddle;
+ if(++frames==10) puts("breakout input smoke passed");
+ return 0;
+ }
+ static int32_t poll_events(int32_t n) { return 0; }
+ static int32_t mouse_x(void) { return frames>=7 ? 640 : 504; }
+ static int32_t mouse_y(void) { return 680; }
+ static int32_t key(int32_t k) {
+ return (k==' ' && (frames==1 || frames==9)) ||
+ (k==262 && (frames==2 || frames==3)) ||
+ (k=='P' && (frames==4 || frames==6)) ||
+ (k=='R' && (frames==8 || frames==9));
+ }
+ static int32_t wait_events(int64_t ms) { usleep(ms*1000); return 0; }
+ static void close_window(void) {}
+ static const struct simple_graphics_api api={SIMPLE_GRAPHICS_VERSION,
+ open_window,present,poll_events,mouse_x,mouse_y,poll_events,key,close_window,wait_events};
+ const struct simple_graphics_api *simple_minifb_api(void) { return &api; }
+ """);
+ TestC.exec(30,"gcc","-shared","-fPIC","-Isrc/main/c",source.toString(),"-o",library.toString());
+ var pb=new ProcessBuilder(Path.of(breakout).toAbsolutePath().toString(),"smoke").redirectErrorStream(true);
+ pb.environment().put("SIMPLE_MINIFB",library.toAbsolutePath().toString());
+ pb.environment().put("SIMPLE_MINIAUDIO","off");
+ Process child=pb.start();
+ try {
+ assertTrue("Breakout timed out",child.waitFor(10,TimeUnit.SECONDS));
+ String output=new String(child.getInputStream().readAllBytes(),java.nio.charset.StandardCharsets.UTF_8);
+ assertEquals(output,0,child.exitValue());
+ assertEquals("breakout input smoke passed\n",output);
+ } finally { if(child.isAlive()) child.destroyForcibly(); }
+ }
+
+ @Test public void missingLibrary() throws Exception {
+ String missing = Path.of("build","no-such-minifb-backend").toAbsolutePath().toString();
+ String output = run(graphics,missing,1);
+ assertTrue(output,output.contains("cannot load MiniFB backend"));
+ }
+
+ @Test public void textIgnoresGraphicsConfiguration() throws Exception {
+ assertEquals("text still works\n",run(text,"off",0));
+ assertEquals("text still works\n",run(text,"no-such-backend",0));
+ }
+
+ @Test public void incompatibleLibrary() throws Exception {
+ Path source = Path.of("build/objs/graphics_incompatible.c");
+ Path library = Path.of("build/objs/graphics_incompatible" +
+ (TestC.OS.startsWith("Windows") ? ".dll" : ".so"));
+ Files.writeString(source,"""
+ #include "simple_graphics.h"
+ static const struct simple_graphics_api api = {999};
+ const struct simple_graphics_api *simple_minifb_api(void) { return &api; }
+ """);
+ TestC.exec(30,"gcc","-shared","-fPIC","-Isrc/main/c",source.toString(),"-o",library.toString());
+ assertTrue(run(graphics,library.toAbsolutePath().toString(),1).contains("incompatible MiniFB backend"));
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/IntegerLiteralTest.java b/src/test/java/com/seaofnodes/simple/IntegerLiteralTest.java
new file mode 100644
index 00000000..975042c2
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/IntegerLiteralTest.java
@@ -0,0 +1,46 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.type.TypeFloat;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class IntegerLiteralTest {
+ private static void value(String expression, long expected) {
+ var code = new CodeGen("return "+expression+";").parse();
+ assertEquals(expression,TypeInteger.constant(expected),code.expr()._type);
+ }
+
+ @Test public void valuesAndBitPatterns() {
+ value("0x00e47b57",14973783);
+ value("0XfF",255);
+ value("025755",11245); // A: rows 010, 101, 111, 101, 101.
+ value("000000",0);
+ value("0x7fffffffffffffff",Long.MAX_VALUE);
+ value("0x8000000000000000",Long.MIN_VALUE);
+ value("0xffffffffffffffff",-1);
+ value("0777777777777777777777",Long.MAX_VALUE);
+ value("01000000000000000000000",Long.MIN_VALUE);
+ value("01777777777777777777777",-1);
+ value("0x00000000000000000001",1);
+ value("-0x2a + 052",0);
+ value("0x1e+2",32); // e and f are hex digits, not float markers.
+ value("0xff & 017",15);
+ value("017<<0x4",240);
+ value("0x0f",15);
+ value("0123",83);
+ value("000000000000000000000000000000000001",1);
+ assertEquals(TypeFloat.constant(12.5),new CodeGen("return 012.5;").parse().expr()._type);
+ assertEquals(TypeFloat.constant(12.0),new CodeGen("return 012f;").parse().expr()._type);
+ }
+
+ @Test public void nativeLiterals() throws Exception {
+ TestC.runSF("""
+ val check_noInline = { i64 mask, u32 color, int glyph ->
+ return mask==-1 && color==14973783 && glyph==11245 ? 0 : 1;
+ };
+ val main = { -> return check_noInline(0xffffffffffffffff,0x00e47b57,025755); };
+ ""","basedLiterals","",-1);
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/IntrinsicTest.java b/src/test/java/com/seaofnodes/simple/IntrinsicTest.java
new file mode 100644
index 00000000..e3b30efb
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/IntrinsicTest.java
@@ -0,0 +1,188 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
+import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class IntrinsicTest {
+ private static final String DECL="""
+ { f64 -> i64 } simple_f64_bits="C";
+ { i64 -> f64 } simple_f64_from_bits="C";
+ """;
+
+ @Test public void constantsAndRawNaNs() {
+ long[] patterns={0,Long.MIN_VALUE,1,0x0010000000000000L,0x3ff0000000000000L,
+ 0x7fefffffffffffffL,0x7ff0000000000000L,0xfff0000000000000L,
+ 0x7ff8000000000001L,0x7ff8000000000002L,0xfff800000000002aL,0x7ff0000000000001L};
+ for(long bits:patterns) {
+ String literal=bits==Long.MIN_VALUE ? "(-9223372036854775807-1)" : Long.toString(bits);
+ CodeGen code=new CodeGen(DECL+"return simple_f64_bits(simple_f64_from_bits("+literal+"));")
+ .driver(CodeGen.Phase.TypeCheck);
+ assertNull(code._stop.walk(n -> n instanceof CallNode ? n : null));
+ assertEquals(Long.toString(bits),Eval2.eval(code,0));
+ }
+ assertNotSame(TypeFloat.constant(Double.longBitsToDouble(patterns[8])),
+ TypeFloat.constant(Double.longBitsToDouble(patterns[9])));
+ }
+
+ @Test public void runtimeNodesAndMemory() {
+ CodeGen code=new CodeGen(DECL+"""
+ val bits=simple_f64_bits;
+ val a=new f64[1];
+ a[0]=simple_f64_from_bits(arg);
+ if( arg==0 ) a[0]=-0.0;
+ return bits(a[0]);
+ """).driver(CodeGen.Phase.TypeCheck);
+ assertNotNull(code._stop.walk(n -> n instanceof Intrinsic.BitCastNode ? n : null));
+ assertNull(code._stop.walk(n -> n instanceof CallNode ? n : null));
+ assertEquals(Long.toString(Long.MIN_VALUE),Eval2.eval(code,0));
+ assertEquals("9221120237041090602",Eval2.eval(code,0x7ff800000000002aL));
+ }
+
+ @Test public void ordinaryFunctionWithSameName() {
+ CodeGen code=new CodeGen("""
+ val simple_f64_bits={ f64 x -> 42; };
+ return simple_f64_bits(1.0);
+ """);
+ assertEquals("42",Eval2.eval(code,0));
+ }
+
+ @Test public void wrongSignature() {
+ try {
+ new CodeGen("{ i64 -> i64 } simple_f64_bits=\"C\"; return simple_f64_bits(arg);")
+ .driver(CodeGen.Phase.TypeCheck);
+ fail("The reserved C symbol has fixed semantics and a fixed signature");
+ } catch(RuntimeException e) {
+ assertTrue(e.getMessage(),e.getMessage().contains("Intrinsic 'simple_f64_bits' requires"));
+ }
+ }
+
+ @Test public void nativeRepresentations() throws Exception {
+ TestC.runSYS("""
+ val bits_noInline={ f64 x -> sys.math.f64Bits(x); };
+ val float_noInline={ i64 x -> sys.math.f64FromBits(x); };
+ val main={ ->
+ val patterns=new i64[~]{0,sys.integer.MIN_I64,1,0x0010000000000000,
+ 0x3ff0000000000000,0x7fefffffffffffff,0x7ff0000000000000,
+ 0x7ff8000000000001,0x7ff8000000000002,-2251799813685206,0x7ff0000000000001};
+ for( int i=0; i<patterns#; i++ )
+ if( bits_noInline(float_noInline(patterns[i]))!=patterns[i] ) return 1;
+ if( bits_noInline(-0.0)!=sys.integer.MIN_I64 ) return 2;
+ if( float_noInline(0x4008000000000000)!=3.0 ) return 3;
+ int numeric=3.75;
+ if( numeric!=3 ) return 4;
+ return 0;
+ };
+ ""","intrinsicNative","",-1);
+ }
+
+ @Test public void indirectCallsUseCFallback() throws Exception {
+ String source=DECL+"""
+ { f64 -> i64 } llround="C";
+ val apply_noInline={ { f64 -> i64 } f, f64 x -> f(x); };
+ val main={ ->
+ if( apply_noInline(simple_f64_bits,-0.0)!=(-9223372036854775807-1) ) return 1;
+ if( apply_noInline(llround,3.75)!=4 ) return 2;
+ if( apply_noInline(simple_f64_bits,1.0)!=0x3ff0000000000000 ) return 3;
+ if( apply_noInline(simple_f64_bits,simple_f64_from_bits(0x7ff800000000002a))!=0x7ff800000000002a ) return 4;
+ return 0;
+ };
+ """;
+ CodeGen code=new CodeGen(source).driver(CodeGen.Phase.TypeCheck);
+ assertNotNull("The indirect C call must survive",code._stop.walk(n ->
+ n instanceof CallNode c && c.external() && !c.tfp().isConstant() ? n : null));
+ TestC.runSYS(source,"intrinsicFallback","",-1);
+ }
+
+ @Test public void importedNodesAndNaNConstants() throws Exception {
+ Path dir=Files.createTempDirectory(Path.of("build/objs"),"intrinsic-import-");
+ String library=DECL+"""
+ val bits_noInline={ f64 x -> simple_f64_bits(x); };
+ val float_noInline={ i64 x -> simple_f64_from_bits(x); };
+ val nan=simple_f64_from_bits(0x7ff8000000000001);
+ val otherNaN=simple_f64_from_bits(0x7ff8000000000002);
+ """;
+ new CodeGen(null,dir.toString(),null,"Bits",library,126,true,TypeInteger.BOT)
+ .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
+ var paths=new Ary<>(new String[]{dir.toString()});
+ String[] clients={
+ "return Bits.bits_noInline(Bits.float_noInline(arg));",
+ "return Bits.bits_noInline(Bits.nan);",
+ "return Bits.bits_noInline(Bits.otherNaN);"
+ };
+ long[] expected={0x7ff800000000002aL,0x7ff8000000000001L,0x7ff8000000000002L};
+ for(int i=0;i<clients.length;i++) {
+ CodeGen client=new CodeGen(null,dir.toString(),paths,"Client",clients[i],126,true,TypeInteger.BOT);
+ assertEquals(Long.toString(expected[i]),Eval2.eval(client,expected[0]));
+ }
+ }
+
+ @Test public void allBackendsSelectRepresentationMoves() {
+ // The array store/load and branch keep both directions alive, rather
+ // than allowing the inverse-cast peephole to erase the whole test.
+ String source=DECL+"""
+ val a=new f64[1];
+ a[0]=simple_f64_from_bits(arg);
+ if( arg==0 ) a[0]=-0.0;
+ return simple_f64_bits(a[0]);
+ """;
+ for(String cpu:new String[]{"x86_64_v2","arm","riscv"}) {
+ CodeGen code=new CheckedCodeGen(source).driver(CodeGen.Phase.Encoding,cpu,"SystemV");
+ assertNotNull(code._stop.walk(n -> n instanceof MachConcreteNode m && m.op().equals("f64bits") ? n : null));
+ assertNotNull(code._stop.walk(n -> n instanceof MachConcreteNode m && m.op().equals("f64frombits") ? n : null));
+ }
+ }
+
+ @Test public void invalidArgumentIsNotInlinedAway() {
+ try {
+ new CodeGen(DECL+"return simple_f64_bits(1);").driver(CodeGen.Phase.TypeCheck);
+ fail("Intrinsic inlining must not hide the call's type error");
+ } catch(RuntimeException e) {
+ assertTrue(e.getMessage(),e.getMessage().contains("Argument #1 isa"));
+ }
+ }
+
+ @Test public void discardedValueKeepsArgumentEffects() throws Exception {
+ TestC.runSYS(DECL+"""
+ val next={ i64[] a -> a[0]++; return 1.0; };
+ val main={ ->
+ val a=new i64[1];
+ simple_f64_bits(next(a));
+ return a[0]!=1;
+ };
+ ""","intrinsicEffects","",-1);
+ }
+
+ @Test public void emulatedRepresentationMoves() throws Exception {
+ long[] patterns={0,Long.MIN_VALUE,1,0x3ff0000000000000L,0x7ff0000000000000L,
+ 0xfff0000000000000L,0x7ff800000000002aL,0x7ff0000000000001L};
+ for(boolean toBits:new boolean[]{true,false}) {
+ String source=toBits
+ ? "val cast={ f64 x -> { f64 -> i64 } simple_f64_bits=\"C\"; return simple_f64_bits(x); };"
+ : "val cast={ i64 x -> { i64 -> f64 } simple_f64_from_bits=\"C\"; return simple_f64_from_bits(x); };";
+ var arm=TestArm64.build("cast",source,0,-1,false);
+ var risc=TestRisc5.build(source,"cast",0,-1,false);
+ int armStart=arm._pc, riscStart=risc._pc;
+ for(long bits:patterns) {
+ arm._pc=armStart; risc._pc=riscStart;
+ if(toBits) {
+ arm.fregs[0]=Double.longBitsToDouble(bits);
+ risc.fregs[10]=Double.longBitsToDouble(bits);
+ } else {
+ arm.regs[0]=bits;
+ risc.regs[10]=bits;
+ }
+ assertEquals(0,arm.step(1000));
+ assertEquals(0,risc.step(1000));
+ assertEquals(bits,toBits ? arm.regs[0] : Double.doubleToRawLongBits(arm.fregs[0]));
+ assertEquals(bits,toBits ? risc.regs[10] : Double.doubleToRawLongBits(risc.fregs[10]));
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java b/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java
index adeba136..6d81af85 100644
--- a/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java
+++ b/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java
@@ -8,6 +8,31 @@ import static org.junit.Assert.*;
// Execution regressions are separate from the fixed spill-measurement cohorts.
public class MemoryEncodingTest {
+ @Test public void testIntegerMemoryOperandsAcrossCalls() throws java.io.IOException {
+ TestC.runSF("""
+ val touch_noInline={i64[] a, i64 i -> a[i]+=1;};
+ val scan_noInline={i64[] a, i64 limit, i64 delta ->
+ int below=0, above=0;
+ for(int i=0;i<a#;i++) {
+ touch_noInline(a,i);
+ if(a[i]<limit) below++;
+ if(limit<a[i]) above++;
+ a[i]+=delta;
+ }
+ return below*10+above;
+ };
+ val main={->
+ val a=new i64[]{0x7ffffffffffffffe,-2,0};
+ if(scan_noInline(a,0x7fffffffffffffff,0)!=20) return 1;
+ if(a[0]!=0x7fffffffffffffff || a[1]!=-1 || a[2]!=1) return 2;
+ val b=new i64[]{-3,-2,0};
+ if(scan_noInline(b,-1,0x100000000)!=11) return 3;
+ if(b[0]!=0xfffffffe || b[1]!=0xffffffff || b[2]!=0x100000001) return 4;
+ return 0;
+ };
+ ""","integerMemoryAcrossCalls","",-1);
+ }
+
@Test public void testLoopLoadSearch() {
String[] sources = {
"""
diff --git a/src/test/java/com/seaofnodes/simple/MethodAccessTest.java b/src/test/java/com/seaofnodes/simple/MethodAccessTest.java
new file mode 100644
index 00000000..7275b151
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/MethodAccessTest.java
@@ -0,0 +1,118 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.parser.Parser;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.util.Ary;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class MethodAccessTest {
+ @Test public void readersAcceptReadOnlyReceivers() throws Exception {
+ String source = """
+ struct Child { int n = 7; };
+ struct Box {
+ !Child child = new Child;
+ int[] values = new int[]{3,5};
+ int count = 2;
+ { i64 } read = { -> count+child.n+values[1]; };
+ val delegate = { -> read(); };
+ val recurse = { i64 n -> n == 0 ? read() : recurse(n-1); };
+ val draw = { !Child canvas -> canvas.n = count; };
+ val outer = { -> later(count); };
+ };
+ val later = { int n -> n+1; };
+ val check = { Box box ->
+ val canvas = new Child;
+ box.draw(canvas);
+ return box.read()+box.delegate()+box.recurse(3)+canvas.n+box.outer();
+ };
+ return check(new Box);
+ """;
+ assertEquals("47",Eval2.eval(new CodeGen(source).driver(CodeGen.Phase.LocalSched),0));
+ TestC.runSF("val main = { -> "+source.replace("return check(new Box);","return check(new Box) != 47;")+" };",
+ "readonlyMethods","",-1);
+ }
+
+ @Test public void writesThroughReceiverRequireAccess() {
+ String[] methods = {
+ "val act = { -> x = 2; };",
+ "val act = { -> self.x++; };",
+ "val act = { -> val nested = { -> x++; }; nested(); };",
+ "val act = { -> val alias = self; alias.x += 1; };",
+ "val act = { -> child.n = 2; };",
+ "val act = { -> values[0]++; };",
+ "val act = { -> write(); }; val write = { -> x++; };",
+ "val act = { -> val f = x == 0 ? left : right; f(); }; val left = { -> x++; }; val right = { -> x--; };",
+ "val act = { -> child.write(); };",
+ "val act = { -> write(self); };",
+ "{ int } act = \"C\";"
+ };
+ for( String method : methods ) {
+ String source = """
+ struct Child { int n; val write = { -> n++; }; };
+ struct Box {
+ int x;
+ !Child child = new Child;
+ int[] values = new int[1];
+ """+method+"""
+ };
+ val write = { !Box box -> box.x++; };
+ val check = { Box box -> box.act(); return 0; };
+ return check(new Box);
+ """;
+ try {
+ new CodeGen(source).driver(CodeGen.Phase.TypeCheck);
+ fail("Accepted writer: "+method);
+ } catch( Parser.ParseException e ) {
+ assertTrue(method+": "+e.getMessage(),e.getMessage().contains("read-only"));
+ }
+ new CodeGen(source.replace("val check = { Box box", "val check = { !Box box"))
+ .driver(CodeGen.Phase.TypeCheck);
+ }
+ }
+
+ @Test public void returningWritableAliasesRequiresAccess() {
+ String source = """
+ struct Box { int x; val alias = { -> self; }; };
+ val check = { Box box -> box.alias(); };
+ return check(new Box);
+ """;
+ try {
+ new CodeGen(source).driver(CodeGen.Phase.TypeCheck);
+ fail("Read-only receiver exposed as writable");
+ } catch( Parser.ParseException e ) {
+ assertTrue(e.getMessage(),e.getMessage().contains("read-only"));
+ }
+ source = source.replace("val alias = { -> self; }", "val alias = { -> Box view = self; view; }");
+ new CodeGen(source).driver(CodeGen.Phase.TypeCheck);
+ }
+
+ @Test public void importedReceiverAccess() throws Exception {
+ var root = Files.createDirectories(Path.of("build/objs/separate"));
+ var lib = Files.createTempDirectory(root,"method-access-");
+ new CodeGen(null,lib.toString(),null,"Counters","""
+ struct Counter {
+ int n = 7;
+ val read = { -> n; };
+ val write = { -> n++; };
+ };
+ val make = { -> new Counter; };
+ """,126,true,TypeInteger.BOT)
+ .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
+ var paths = new Ary<>(new String[]{lib.toString()});
+ String source = "val box = Counters.make(); val check = { Counters.Counter c -> c.read(); }; return check(box);";
+ var code = new CodeGen(null,"build/objs",paths,"CounterReader",source,126,true,
+ TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("7",Eval2.eval(code,0));
+ try {
+ new CodeGen(null,"build/objs",paths,"CounterWriter",source.replace("c.read()","c.write()"),126,true,
+ TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ fail("Imported writer accepted a read-only receiver");
+ } catch( Parser.ParseException e ) {
+ assertTrue(e.getMessage(),e.getMessage().contains("read-only"));
+ }
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/MethodFieldsTest.java b/src/test/java/com/seaofnodes/simple/MethodFieldsTest.java
new file mode 100644
index 00000000..3fe5c28b
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/MethodFieldsTest.java
@@ -0,0 +1,131 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.parser.Parser;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class MethodFieldsTest {
+ @Test public void nestedFunctionStillRejectsLocalCapture() {
+ String source = """
+ struct Game {
+ int count;
+ val input = { int amount ->
+ val action = { -> count += amount; };
+ action();
+ };
+ };
+ return 0;
+ """;
+ try {
+ new CodeGen(source).driver(CodeGen.Phase.Parse);
+ fail("Captured a nonconstant local parameter");
+ } catch( Parser.ParseException e ) {
+ assertEquals("Variable 'amount' is out of function scope and must be a final constant",e.getMessage());
+ }
+ }
+
+ @Test public void nestedFunctionReceiver() throws Exception {
+ String source = """
+ struct Game {
+ bool held, paused;
+ int count;
+ val debounce = { bool pressed, bool wasHeld, { bool } action ->
+ if( pressed && !wasHeld ) action();
+ return pressed;
+ };
+ val bump = { -> count++; return false; };
+ val input = { bool pressed ->
+ if( true ) {
+ held = debounce(pressed,held,{ ->
+ paused = !paused;
+ bump();
+ val again = { -> count += 2; return false; };
+ again();
+ return false;
+ });
+ }
+ };
+ val named = { -> debounce(true,false,bump); };
+ };
+ val a = new Game, b = new Game;
+ a.input(true); a.input(true);
+ if( a.count != 3 || !a.paused || b.count != 0 || b.paused ) return 1;
+ a.input(false); a.input(true);
+ if( a.count != 6 || a.paused ) return 2;
+ b.input(true); b.named();
+ if( b.count != 4 || !b.paused || a.count != 6 ) return 3;
+ return 0;
+ """;
+ assertEquals("0",Eval2.eval(new CodeGen(source).driver(CodeGen.Phase.LocalSched),0));
+ TestC.runSF("val main = { -> "+source+" };","nestedFunctionReceiver","",-1);
+ }
+
+ @Test public void laterSiblingParameterType() throws Exception {
+ String source = """
+ struct Game {
+ val input = { !Controls keys -> keys.presses++; return keys.presses; };
+ val read = { Controls keys -> keys.presses; };
+ };
+ struct Controls { int presses = 4; };
+ val game = new Game, keys = new Controls;
+ if( game.input(keys) != 5 || game.read(keys) != 5 ) return 1;
+ return 0;
+ """;
+ assertEquals("0",Eval2.eval(new CodeGen(source).driver(CodeGen.Phase.LocalSched),0));
+ TestC.runSF("val main = { -> "+source+" };","forwardMethodType","",-1);
+ }
+
+ private static final String FIELDS = """
+ int count = 100;
+ struct Counter {
+ val step = { int n ->
+ count = count+n;
+ ++count;
+ count += 2;
+ speed *= 2.0;
+ return read();
+ };
+ val local = { int count -> count+1; };
+ val read = { -> count; };
+ int count = 3;
+ f64 speed = 1.5;
+ };
+ val a = new Counter, b = new Counter;
+ if( a.step(4) != 10 || b.read() != 3 || a.speed != 3.0 ) return 1;
+ if( a.local(20) != 21 || a.read() != 10 || count != 100 ) return 2;
+ if( b.step(1) != 7 || a.read() != 10 ) return 3;
+ return 0;
+ """;
+
+ @Test public void laterFieldsAndMethods() throws Exception {
+ assertEquals("0",Eval2.eval(new CodeGen(FIELDS).driver(CodeGen.Phase.LocalSched),0));
+ TestC.runSF("val main = { -> "+FIELDS+" };","methodFields","",-1);
+ }
+
+ @Test public void laterGlobalAndRecursiveMethod() {
+ String source = """
+ struct Counter {
+ val step = { i64 n ->
+ if( n == 0 ) return value;
+ value += increment;
+ return step(n-1);
+ };
+ int value = 1;
+ };
+ val increment = 2;
+ return new Counter.step(3);
+ """;
+ assertEquals("7",Eval2.eval(new CodeGen(source).driver(CodeGen.Phase.LocalSched),0));
+ }
+
+ @Test public void laterFinalFieldStillRejectsWrites() {
+ String source = "struct C { val change = { -> x = 2; }; int ~x = 1; }; return new C.change();";
+ try {
+ new CodeGen(source).driver(CodeGen.Phase.TypeCheck);
+ fail("Method wrote a final field");
+ } catch( Parser.ParseException e ) {
+ assertEquals("Cannot reassign final 'x'",e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java b/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java
index 2b7eea78..d342892c 100644
--- a/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java
+++ b/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java
@@ -10,6 +10,57 @@ import org.junit.Test;
import static org.junit.Assert.*;
public class ParserDiagnosticTest {
+ @Test public void testMissingForwardTypeDiagnostic() {
+ String source = """
+ val read = { Missing value ->
+ return value.field;
+ };
+ return 0;
+ """;
+ var error = rejected(source);
+ assertEquals("Unknown type 'Test.Missing'",error.getMessage());
+ assertTrue(error.diagnostic(),error.diagnostic().startsWith("Test.smp:2:"));
+ assertTrue(error.diagnostic(),error.diagnostic().contains("return value.field;"));
+ }
+
+ @Test public void testReadOnlyReceiverDiagnostic() {
+ String source = """
+ struct Box {
+ int x = 1;
+ val bump = { -> x++; x; };
+ };
+ val draw = { Box b ->
+ return b.bump();
+ };
+ return draw(new Box);
+ """;
+ try {
+ new CodeGen(source).driver(CodeGen.Phase.TypeCheck);
+ fail("Read-only receiver accepted by a writable method");
+ } catch( Parser.ParseException e ) {
+ assertEquals("Receiver for method 'bump' is read-only Test.Box; expected writable Test.Box",e.getMessage());
+ assertTrue(e.diagnostic(),e.diagnostic().startsWith("Test.smp:6:18: error: "));
+ assertFalse(e.getMessage().contains("{i32"));
+ }
+ }
+
+ @Test public void testReadOnlyArgumentDiagnostic() {
+ String source = """
+ struct Box { int x = 1; };
+ val write = { !Box b -> b.x = 2; };
+ val draw = { Box b -> write(b); };
+ return draw(new Box);
+ """;
+ try {
+ new CodeGen(source).driver(CodeGen.Phase.TypeCheck);
+ fail("Read-only argument accepted by a writable parameter");
+ } catch( Parser.ParseException e ) {
+ assertEquals("Argument #1 is read-only Test.Box; expected writable Test.Box",e.getMessage());
+ }
+ new CodeGen(source.replace("val draw = { Box b", "val draw = { !Box b"))
+ .driver(CodeGen.Phase.TypeCheck);
+ }
+
private static Parser.ParseException rejected(String source) {
try {
new CodeGen(source).driver(CodeGen.Phase.Parse);
@@ -30,7 +81,18 @@ public class ParserDiagnosticTest {
diagnostic("return 9223372036854775808;",1,8,"Integer literal is outside the signed 64-bit range");
diagnostic("return 1e;",1,8,"Invalid floating-point literal '1e'");
diagnostic("return 1.2.3f;",1,8,"Invalid floating-point literal '1.2.3f'");
- diagnostic("return 012;",1,8,"Syntax error: integer values cannot start with '0'");
+ diagnostic("return 018;",1,8,"Invalid octal integer literal '018'");
+ }
+
+ @Test public void testBasedIntegerLiteralErrors() {
+ diagnostic("return 0x;",1,8,"Hexadecimal integer literal requires at least one digit");
+ diagnostic("return 0x12g;",1,8,"Invalid hexadecimal integer literal '0x12g'");
+ diagnostic("return 0128;",1,8,"Invalid octal integer literal '0128'");
+ diagnostic("return 0x_ff;",1,8,"Invalid hexadecimal integer literal '0x_ff'");
+ diagnostic("return 09;",1,8,"Invalid octal integer literal '09'");
+ diagnostic("return 0x10000000000000000;",1,8,"Hexadecimal integer literal exceeds 64 bits");
+ diagnostic("return 02000000000000000000000;",1,8,"Octal integer literal exceeds 64 bits");
+ diagnostic("int 0xabc;",1,5,"Expected an identifier, found '0xabc'");
}
@Test public void testUnclosedCommentsAndStrings() {
@@ -123,6 +185,24 @@ public class ParserDiagnosticTest {
+"\n return x;\n ^",error.diagnostic());
}
+ @Test public void testJUnitReportsSourceDiagnostic() {
+ var error=rejected("val dist=new i64[3]{int i -> i;}\ndist[0]=0;");
+ String expected="Test.smp:2:1: error: Syntax error, expected `;` but found `dist`"
+ +"\n dist[0]=0;\n ^"+System.lineSeparator();
+ var test=org.junit.runner.Description.createTestDescription(getClass(),"missingSemicolon");
+ assertEquals(expected,new org.junit.runner.notification.Failure(test,error).getTrace());
+
+ var bytes=new java.io.ByteArrayOutputStream();
+ error.printStackTrace(new java.io.PrintStream(bytes,true,StandardCharsets.UTF_8));
+ assertEquals(expected,bytes.toString(StandardCharsets.UTF_8));
+ assertEquals("Syntax error, expected `;` but found `dist`",error.getMessage());
+
+ // Compiler implementation failures must still include Java frames.
+ String trace=new org.junit.runner.notification.Failure(test,new IllegalStateException("compiler bug")).getTrace();
+ assertTrue(trace,trace.contains("java.lang.IllegalStateException: compiler bug"));
+ assertTrue(trace,trace.contains("ParserDiagnosticTest.testJUnitReportsSourceDiagnostic"));
+ }
+
@Test public void testEOF() {
var error=rejected("return 1");
assertEquals("Test.smp:1:9: error: Syntax error, expected `;` but found `EOF`"
diff --git a/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
index 1853aa48..1c278791 100644
--- a/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
+++ b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
@@ -10,6 +10,22 @@ import org.junit.Test;
import static org.junit.Assert.*;
public class SeparateCompilationTest {
+ @Test public void testImportedReadOnlyPrimitiveArray() throws Exception {
+ Path build=Path.of("build/objs/separate");
+ Files.createDirectories(build);
+ Path lib=Files.createTempDirectory(build,"pixels-");
+ new CodeGen(null,lib.toString(),null,"Pixels",
+ "val first={ u32[~] pixels -> pixels[0]; };",123,true,TypeInteger.BOT)
+ .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
+ var paths=new Ary<>(new String[]{lib.toString()});
+ CodeGen code=new CodeGen(null,"build/objs",paths,"PixelClient","""
+ val pixels=new u32[2];
+ pixels[0]=4294967295;
+ return Pixels.first(pixels);
+ """,123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("4294967295",Eval2.eval(code,0));
+ }
+
@Test public void testImportedNativeExposure() throws Exception {
Path build=Path.of("build/objs/separate");
Files.createDirectories(build);
diff --git a/src/test/java/com/seaofnodes/simple/StreamIOTest.java b/src/test/java/com/seaofnodes/simple/StreamIOTest.java
index f3491314..7b459e75 100644
--- a/src/test/java/com/seaofnodes/simple/StreamIOTest.java
+++ b/src/test/java/com/seaofnodes/simple/StreamIOTest.java
@@ -131,6 +131,10 @@ public class StreamIOTest extends TestC {
gcc.addAll(linkObjs(code));
gcc.add(runtimeObject("simple_crt"));
gcc.add(runtimeObject("simple_args"));
+ gcc.add(runtimeObject("simple_graphics"));
+ gcc.add(runtimeObject("simple_audio"));
+ gcc.add(runtimeObject("simple_time"));
+ if( !OS.startsWith("Windows") ) gcc.add("-ldl");
gcc.addAll(new String[]{"-lm","-o",exe});
exec(TEST_TIMEOUT_SECONDS,gcc.asAry());
assertEquals("ok\n",exec(TEST_TIMEOUT_SECONDS,exe));
diff --git a/src/test/java/com/seaofnodes/simple/StringFormatTest.java b/src/test/java/com/seaofnodes/simple/StringFormatTest.java
index 53a10092..05e801dc 100644
--- a/src/test/java/com/seaofnodes/simple/StringFormatTest.java
+++ b/src/test/java/com/seaofnodes/simple/StringFormatTest.java
@@ -128,6 +128,10 @@ public class StringFormatTest extends TestC {
@Test public void testWrongTypesAndReservedNames() {
rejects("return \"%d(1.5)\";", "must be a");
+ rejects("return \"%t(1.5)\";", "must be a");
+ rejects("return \"%tQ(0)\";", "Expected a time format");
+ rejects("return \"%.2t(0)\";", "Precision is not supported");
+ rejects("return \"%+t(0)\";", "Invalid flags");
rejects("return \"%s(17)\";", "must be a");
rejects("return \"%(null)\";", "Cannot format");
rejects("val f={int int -> int;}; return 0;", "Expected an identifier");
diff --git a/src/test/java/com/seaofnodes/simple/TestC.java b/src/test/java/com/seaofnodes/simple/TestC.java
index dd7999c9..63bb98da 100644
--- a/src/test/java/com/seaofnodes/simple/TestC.java
+++ b/src/test/java/com/seaofnodes/simple/TestC.java
@@ -190,6 +190,11 @@ public abstract class TestC {
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);
+ // The loader has no MiniFB dependency and opens nothing until gfx.open.
+ params.add(runtimeObject("simple_graphics"));
+ params.add(runtimeObject("simple_audio"));
+ params.add(runtimeObject("simple_time"));
+ if( !OS.startsWith("Windows") ) params.add("-ldl");
// Standalone Simple programs export `simple_main`; the small C
// runtime owns native main and calls the Simple argument converter
// before making the ABI-qualified entry call.
diff --git a/src/test/java/com/seaofnodes/simple/TimeTest.java b/src/test/java/com/seaofnodes/simple/TimeTest.java
new file mode 100644
index 00000000..dfffc1b9
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/TimeTest.java
@@ -0,0 +1,183 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.util.Ary;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class TimeTest extends TestC {
+ private static void checks(String body, String name) throws Exception {
+ runSYS("val main={ -> "+body+" return 0; };",name,"",-1);
+ }
+
+ @Test public void builtInFormats() throws Exception {
+ runSYS("""
+ val main={ ->
+ i64 t=sys.time.parse("2026-11-12T00:34:56.789Z");
+ sys.io.p("%t(t)|%tF(t)|%tr(t)|%tR(t)|%tM(83456)|%tD(t)|%tT(t)\\n");
+ sys.io.p("[%30t(0)][%-10tr(sys.time.hours(12))]\\n");
+ sys.io.p("%tr(0)|%tr(sys.time.hours(13))|%tM(-1)|%tM(-9223372036854775807-1)");
+ return 0;
+ };
+ ""","timeFormats",
+ "2026-11-12T00:34:56.789Z|2026-11-12T00:34:56.789Z|12:34am|00:34|01:23.456|12/11/26|00:34:56\n"+
+ "[ 1970-01-01T00:00:00.000Z][12:00pm ]\n"+
+ "12:00am|1:00pm|-00:00.001|-153722867280912:55.808",-1);
+ }
+
+ @Test public void calendarAgainstJava() throws Exception {
+ long[] values={-62135596800000L,-2203977600000L,-2203891200000L,-86400001,-86400000,-1,0,1,
+ 951782400000L,1709164800123L,1794443696789L,253402300799999L};
+ DateTimeFormatter iso=DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneOffset.UTC);
+ StringBuilder src=new StringBuilder("val show_noInline={i64 t -> sys.io.p(\"%t(t)|\"); val d=sys.time.utc(t); sys.io.p(\"%(d.weekday)\\n\"); }; val main={ -> ");
+ StringBuilder expected=new StringBuilder();
+ for (long value:values) {
+ src.append("show_noInline(").append(value).append(");");
+ var dt=Instant.ofEpochMilli(value).atOffset(ZoneOffset.UTC);
+ expected.append(iso.format(dt)).append('|').append(dt.getDayOfWeek().getValue()%7).append('\n');
+ }
+ src.append("return 0; };");
+ runSYS(src.toString(),"timeCalendar",expected.toString(),-1);
+ }
+
+ @Test public void parseAndConstruction() throws Exception {
+ checks("""
+ if( sys.time.parse("1970-01-01T00:00:00Z")!=0 ) return 1;
+ if( sys.time.parse("1970-01-01T01:00:00.123+01:00")!=123 ) return 2;
+ if( sys.time.parse("1969-12-31T16:00:00-08:00")!=0 ) return 3;
+ if( sys.time.parse("1969-12-31T23:59:59.999Z")!=-1 ) return 4;
+ if( sys.time.parse("1969-12-31T23:59:59.999+01:00")!=-3600001 ) return 5;
+ if( sys.time.parse("1970-01-01T00:00:30+00:00:30")!=0 ) return 6;
+ if( sys.time.parse("2000-02-29T00:00:00Z")!=951782400000 ) return 7;
+ if( sys.time.makeUTC(2024,2,29,0,0,0,123)!=1709164800123 ) return 8;
+ if( sys.time.makeUTC(1900,2,29,0,0,0,0)!=-1 || sys.time.makeUTC(2024,2,29,24,0,0,0)!=-1 ) return 9;
+ if( sys.time.utc(sys.time.MIN-1).year!=0 || sys.time.utc(sys.time.MAX+1).year!=0 ) return 10;
+ ""","timeParse");
+ String[] bad={"","now","2026-11-12","2026-11-12T00:00:00","0000-01-01T00:00:00Z",
+ "1900-02-29T00:00:00Z","2026-02-29T00:00:00Z","2026-13-01T00:00:00Z",
+ "2026-11-00T00:00:00Z","2026-11-12T24:00:00Z","2026-11-12T00:60:00Z",
+ "2026-11-12T00:00:60Z","2026-11-12T00:00:00.1Z","2026-11-12T00:00:00.1234Z",
+ "2026-11-12T00:00:00+24:00","2026-11-12T00:00:00+01:60","2026-11-12T00:00:00Zx",
+ "2026-11-12T00:00:00.00xZ","0001-01-01T00:00:00+01:00"};
+ StringBuilder src=new StringBuilder("val main={ -> ");
+ for (int i=0;i<bad.length;++i)
+ src.append("if(sys.time.parse(\"").append(bad[i]).append("\")!=-1) return ").append(i+1).append(';');
+ src.append("return 0; };");
+ runSYS(src.toString(),"timeBadParse","",-1);
+ }
+
+ @Test public void durationArithmetic() throws Exception {
+ checks("""
+ if( sys.time.seconds(2)!=2000 || sys.time.minutes(2)!=120000 || sys.time.hours(2)!=7200000 || sys.time.days(2)!=172800000 ) return 1;
+ if( sys.time.toSeconds(-1999)!=-1 || sys.time.toMinutes(119999)!=1 || sys.time.toHours(7200000)!=2 || sys.time.toDays(172800000)!=2 ) return 2;
+ if( sys.time.seconds(9223372036854775)!=9223372036854775000 || sys.time.seconds(-9223372036854775)!=-9223372036854775000 ) return 3;
+ if( sys.time.seconds(9223372036854776)!=-1 || sys.time.seconds(-9223372036854776)!=-1 ) return 4;
+ if( sys.time.remaining(5,10)!=0 || sys.time.remaining(10,5)!=5 || sys.time.remaining(9223372036854775807,-1)!=9223372036854775807 ) return 5;
+ if( sys.time.fromNanos(-1999999)!=-1 || sys.time.toNanos(1234)!=1234000000 || sys.time.toNanos(9223372036855)!=-1 ) return 6;
+ ""","timeArithmetic");
+ }
+
+ @Test public void timerOrderingAndMissedTicks() throws Exception {
+ checks("""
+ val q=new sys.time.Timers();
+ if( q.poll(0)!=0 || q.nextDeadline()!=-1 ) return 1;
+ u32 a=q.afterAt(100,20), b=q.afterAt(100,10), c=q.afterAt(100,10);
+ if( a==0 || b==0 || c==0 ) return 10;
+ if( q.nextDeadline()!=110 || q.poll(109)!=0 || q.poll(110)!=b || q.poll(110)!=c || q.poll(110)!=0 ) return 2;
+ if( !q.cancel(a) || q.cancel(a) || q.cancel(0) || q.nextDeadline()!=-1 ) return 3;
+ u32 repeat=q.everyAt(100,10);
+ if( q.poll(135)!=repeat || q.nextDeadline()!=140 || q.poll(135)!=0 ) return 4;
+ if( q.poll(140)!=repeat || q.nextDeadline()!=150 || !q.cancel(repeat) ) return 5;
+ for( int i=0; i<20; i++ ) if( q.afterAt(200,i)==0 ) return 11;
+ u32 prev=0;
+ for( int i=0; i<20; i++ ) { u32 id=q.poll(300); if( id<=prev ) return 6; prev=id; }
+ if( q.nextDeadline()!=-1 || q.poll(1000)!=0 ) return 7;
+ if( q.afterAt(-1,1)!=0 || q.afterAt(0,-1)!=0 || q.everyAt(0,0)!=0 || q.afterAt(9223372036854775807,1)!=0 ) return 8;
+ u32 last=q.everyAt(9223372036854775806,1);
+ if( last==0 || q.poll(9223372036854775807)!=last || q.nextDeadline()!=-1 ) return 9;
+ // Exhaust IDs without wrapping or letting an old ID cancel a new timer.
+ q._next=0xfffffffe;
+ u32 high=q.afterAt(0,10), max=q.afterAt(0,10);
+ if( high!=0xfffffffe || max!=0xffffffff || q.afterAt(0,0)!=0 ) return 12;
+ if( q.cancel(a) || q.poll(10)!=high || q.poll(10)!=max || q.poll(10)!=0 ) return 13;
+ if( q.afterAt(0,0)!=0 || q.nextDeadline()!=-1 ) return 14;
+ ""","timeTimers");
+ }
+
+ private static String run(String source, String name, String zone) throws Exception {
+ String exe=compile(source,name,new Ary<>(new String[]{RELEASE_SYS_DIR}),CALL_CONVENTION,null,null,-1);
+ ProcessBuilder pb=new ProcessBuilder(exe).redirectErrorStream(true);
+ if (zone!=null) pb.environment().put("TZ",zone);
+ Process child=pb.start();
+ try {
+ assertTrue("time program timed out",child.waitFor(10,TimeUnit.SECONDS));
+ String out=new String(child.getInputStream().readAllBytes(),StandardCharsets.UTF_8);
+ assertEquals(out,0,child.exitValue());
+ return out;
+ } finally { if (child.isAlive()) child.destroyForcibly(); }
+ }
+
+ @Test public void localZoneAndDaylightSaving() throws Exception {
+ String out=run("""
+ val show_noInline={u8[~] s ->
+ i64 t=sys.time.parse(s); val d=sys.time.local(t);
+ sys.io.p("%(sys.time.formatLocal(t,sys.time.ISO))|%(d.offsetSeconds)|%b(d.daylight)|%(sys.time.zoneName(t))\\n");
+ };
+ val main={ -> show_noInline("2026-01-01T00:00:00Z"); show_noInline("2026-07-01T00:00:00Z"); return 0; };
+ ""","timeZone","EST5EDT,M3.2.0/2,M11.1.0/2");
+ assertEquals("2025-12-31T19:00:00.000-05:00|-18000|false|EST\n2026-06-30T20:00:00.000-04:00|-14400|true|EDT\n",out);
+ }
+
+ @Test public void liveClocksAndWaits() throws Exception {
+ String out=run("""
+ val main={ ->
+ i64 start=sys.time.monotonic();
+ i64 ns=sys.time.nanoTime();
+ if( start<0 || ns/1000000<start || ns/1000000>sys.time.monotonic() ) return 1;
+ if( sys.time.sleep(10)!=0 || sys.time.monotonic()-start<10 ) return 2;
+ i64 deadline=sys.time.monotonic()+10;
+ if( sys.time.sleepUntil(deadline)!=0 || sys.time.monotonic()<deadline ) return 3;
+ if( sys.time.sleepUntil(0)!=0 || sys.time.sleep(-1)!=-1 || sys.time.sleepUntil(-1)!=-1 ) return 4;
+ i64 wall=sys.time.now();
+ if( wall<1700000000000 || wall>4102444800000 ) return 5;
+ return 0;
+ };
+ ""","timeClocks",null);
+ assertEquals("",out);
+ }
+
+ @Test public void pureCalendarInEvaluator() {
+ CodeGen code=new CodeGen("src/main/smp",null,null,"TimeEval","""
+ val s="%t(-1)|%tM(1234)";
+ val expected="1969-12-31T23:59:59.999Z|00:01.234";
+ if( s#!=expected# ) return 1;
+ for( int i=0; i<s#; i++ ) if( s[i]!=expected[i] ) return 2;
+ return 0;
+ """,123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(code,0,100000));
+ }
+
+ @Test public void wideLibrarySerialization() throws Exception {
+ // A growing stdlib crossed the historical byte-sized alias/count limit.
+ // Exercise a separately compiled library whose live memory spans it.
+ StringBuilder source=new StringBuilder("struct Wide { ");
+ for (int i=0;i<300;++i) source.append("int !f").append(i).append("=0;");
+ source.append("}; val make_noInline={int x -> val v=new Wide();");
+ for (int i : new int[]{0,255,299}) source.append("v.f").append(i).append("=x+").append(i).append(';');
+ source.append("return v; };");
+ Path library=Files.createTempDirectory(Path.of("build/objs"),"wide-memory-");
+ new CodeGen(null,library.toString(),null,"WideMemory",source.toString(),123,true,TypeInteger.BOT)
+ .driver(CPU_PORT,CALL_CONVENTION,false,false);
+ run("val main={ -> val v=WideMemory.make_noInline(7); return v.f0!=7 || v.f255!=262 || v.f299!=306; };",
+ "timeWideMemory",new Ary<>(new String[]{library.toString(),RELEASE_SYS_DIR}),
+ CALL_CONVENTION,null,null,"",-1);
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/TypeTest.java b/src/test/java/com/seaofnodes/simple/TypeTest.java
index 16b75fdc..52e857e9 100644
--- a/src/test/java/com/seaofnodes/simple/TypeTest.java
+++ b/src/test/java/com/seaofnodes/simple/TypeTest.java
@@ -7,6 +7,26 @@ import static org.junit.Assert.assertSame;
public class TypeTest {
+ @Test public void testConstantArrayElements() {
+ TypeConAry range=TypeConAry.make(TypeInteger.U16,new long[]{5,6,7});
+ Assert.assertEquals("[5-7]",range.toString());
+ Assert.assertEquals("[5-7]",TypeConAry.make(TypeInteger.U8,new long[]{5,6,7}).toString());
+ Assert.assertEquals("[\"abc\"]",TypeConAry.make("abc").toString());
+ for( TypeInteger storage : new TypeInteger[]{TypeInteger.BOOL,TypeInteger.I8,TypeInteger.U8,
+ TypeInteger.I16,TypeInteger.U16,TypeInteger.I32,TypeInteger.U32,TypeInteger.BOT} ) {
+ TypeConAry empty=TypeConAry.make(storage,new long[0]);
+ assertSame(storage.dual(),empty.elem());
+ assertSame(storage,((TypeConAry)empty.dual()).elem());
+ for( long[] values : new long[][]{{0},{1},{0,1},{storage._min,storage._max}} ) {
+ TypeConAry array=TypeConAry.make(storage,values);
+ Assert.assertTrue(empty.isa(array));
+ assertSame(array,empty.meet(array));
+ assertSame(empty,array.join(empty));
+ check_symmetric(empty,array);
+ }
+ }
+ }
+
@Test public void testOpaqueExternalEscapes() {
TypeMem defaults = TypeMem.START.makeFrom(XInt.make(2),XInt.make(3));
TypeMem prior = TypeMem.START.makeFrom(XInt.make(5),XInt.make(7));
diff --git a/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java b/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java
index b255bcf7..190cf17c 100644
--- a/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java
+++ b/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java
@@ -3,6 +3,30 @@ import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.TypeInteger;
import static org.junit.Assert.*;
public class PrintRegTestSupport {
+ public static void checkDataAliases(java.nio.file.Path file, int size, String... names) {
+ ElfReader elf=ElfReader.load(file.toFile(),null);
+ assertNotNull(elf);
+ ElfReader.Section symbols=java.util.Arrays.stream(elf._sections)
+ .filter(s -> ".symtab".equals(s._name)).findFirst().orElseThrow();
+ ElfReader.Section strings=elf._sections[symbols._link];
+ var bytes=java.nio.ByteBuffer.wrap(elf._buf).order(java.nio.ByteOrder.LITTLE_ENDIAN);
+ var wanted=new java.util.HashSet<>(java.util.Arrays.asList(names));
+ Long address=null;
+ for( int i=0; i<symbols._size/symbols._esize; i++ ) {
+ int off=(int)symbols._offset+i*symbols._esize;
+ String name=strings.str(bytes.getInt(off));
+ if( !wanted.remove(name) ) continue;
+ assertTrue("Debug aliases precede global symbols",i<symbols._info);
+ assertEquals("Local object symbol",1,bytes.get(off+4)&255);
+ assertEquals(".rodata",elf._sections[bytes.getShort(off+6)&65535]._name);
+ assertEquals(size,bytes.getLong(off+16));
+ long value=bytes.getLong(off+8);
+ if( address!=null ) assertEquals("Aliases share storage",address.longValue(),value);
+ address=value;
+ }
+ assertTrue("Missing debug symbols: "+wanted,wanted.isEmpty());
+ }
+
public static void check() throws Exception {
var code = new CodeGen("return 0;").parse();
var alloc = new RegAlloc(code);
@@ -61,7 +85,7 @@ public class PrintRegTestSupport {
visit.put(marker,t);
var method = com.seaofnodes.simple.print.ASMPrinter.class.getDeclaredMethod(
"printConstantPool",int.class,com.seaofnodes.simple.util.SB.class,
- com.seaofnodes.simple.util.BAOS.class,java.util.HashMap.class,boolean.class,String.class);
+ com.seaofnodes.simple.util.BAOS.class,java.util.Map.class,boolean.class,String.class);
method.setAccessible(true);
try {
var sb = new com.seaofnodes.simple.util.SB();