From 236a9b50ffe5c13f33027dbf5fef38f9fc58a8f9 Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Mon, 10 Aug 2026 11:11:18 -0700 Subject: Chapter 31: Software Drawing, Time, and Graphical Games Co-authored-by: Hsiang-Ying Fu <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: fuhsnn <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: Symious <14933944+symious@users.noreply.github.com> Co-authored-by: Jun Kawasaki Co-authored-by: Claude Opus 5 --- .../java/com/seaofnodes/simple/ArrayInitTest.java | 175 ++++++++++++++++ src/test/java/com/seaofnodes/simple/AudioTest.java | 94 +++++++++ .../java/com/seaofnodes/simple/BrainFuckTest.java | 8 +- .../java/com/seaofnodes/simple/Chapter01Test.java | 9 +- .../java/com/seaofnodes/simple/Chapter21Test.java | 4 +- .../java/com/seaofnodes/simple/Chapter22Test.java | 4 +- .../java/com/seaofnodes/simple/Chapter25Test.java | 4 +- src/test/java/com/seaofnodes/simple/DrawTest.java | 229 +++++++++++++++++++++ .../java/com/seaofnodes/simple/EscapeFlowTest.java | 31 +++ src/test/java/com/seaofnodes/simple/Eval2.java | 18 +- .../simple/FloatComparisonNativeTest.java | 65 ++++++ .../java/com/seaofnodes/simple/GraphicsTest.java | 127 ++++++++++++ .../com/seaofnodes/simple/IntegerLiteralTest.java | 46 +++++ .../com/seaofnodes/simple/MemoryEncodingTest.java | 25 +++ .../seaofnodes/simple/ParserDiagnosticTest.java | 31 ++- .../seaofnodes/simple/SeparateCompilationTest.java | 16 ++ .../java/com/seaofnodes/simple/StreamIOTest.java | 4 + .../com/seaofnodes/simple/StringFormatTest.java | 4 + src/test/java/com/seaofnodes/simple/TestC.java | 5 + src/test/java/com/seaofnodes/simple/TimeTest.java | 183 ++++++++++++++++ .../simple/codegen/PrintRegTestSupport.java | 2 +- 21 files changed, 1062 insertions(+), 22 deletions(-) create mode 100644 src/test/java/com/seaofnodes/simple/ArrayInitTest.java create mode 100644 src/test/java/com/seaofnodes/simple/AudioTest.java create mode 100644 src/test/java/com/seaofnodes/simple/DrawTest.java create mode 100644 src/test/java/com/seaofnodes/simple/FloatComparisonNativeTest.java create mode 100644 src/test/java/com/seaofnodes/simple/GraphicsTest.java create mode 100644 src/test/java/com/seaofnodes/simple/IntegerLiteralTest.java create mode 100644 src/test/java/com/seaofnodes/simple/TimeTest.java (limited to 'src/test/java') 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..6cba5ee3 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/ArrayInitTest.java @@ -0,0 +1,175 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import org.junit.Test; +import static org.junit.Assert.*; + +public class ArrayInitTest { + 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 + 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(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/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/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 i*i; }; val _sum = { i32[~] is -> // final array int sum=0; for( int i=0; i + int x=360+_sin(turn)*128, y=232-_sin(turn+0.25)*128; + if( xexpectedX+1 || yexpectedY+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 + 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 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..b6228666 100644 --- a/src/test/java/com/seaofnodes/simple/Eval2.java +++ b/src/test/java/com/seaofnodes/simple/Eval2.java @@ -388,7 +388,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 +443,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 +475,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 +486,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 + 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+=16; + if( a>b ) bits+=32; + return bits; + }; + val value_noInline={ f64 a, f64 b -> + val lt=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=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/GraphicsTest.java b/src/test/java/com/seaofnodes/simple/GraphicsTest.java new file mode 100644 index 00000000..66ced278 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/GraphicsTest.java @@ -0,0 +1,127 @@ +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; + + @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); + } + + 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 + 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 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/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 + 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/ParserDiagnosticTest.java b/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java index 2b7eea78..a1253d05 100644 --- a/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java +++ b/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java @@ -30,7 +30,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 +134,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(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/1000000sys.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()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 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/codegen/PrintRegTestSupport.java b/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java index b255bcf7..b0f0f225 100644 --- a/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java +++ b/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java @@ -61,7 +61,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(); -- cgit v1.2.3