From 207febef9924c8fa5391185e75d42a7ea480bb38 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/ArithNodeTest.java | 2 +- .../java/com/seaofnodes/simple/ArrayInitTest.java | 211 +++++++++++++++++++ .../com/seaofnodes/simple/AssemblyDumpTest.java | 61 ++++++ src/test/java/com/seaofnodes/simple/AudioTest.java | 94 +++++++++ .../java/com/seaofnodes/simple/BrainFuckTest.java | 8 +- .../java/com/seaofnodes/simple/BreakoutTest.java | 216 +++++++++++++++++++ .../java/com/seaofnodes/simple/BulkMemPhiTest.java | 25 +++ .../java/com/seaofnodes/simple/Chapter01Test.java | 9 +- .../java/com/seaofnodes/simple/Chapter17bTest.java | 4 +- .../java/com/seaofnodes/simple/Chapter21Test.java | 4 +- .../java/com/seaofnodes/simple/Chapter22Test.java | 4 +- .../java/com/seaofnodes/simple/Chapter23Test.java | 5 +- .../java/com/seaofnodes/simple/Chapter25Test.java | 4 +- .../com/seaofnodes/simple/ConstantArrayTest.java | 123 +++++++++++ .../com/seaofnodes/simple/ConvertNodeTest.java | 2 +- src/test/java/com/seaofnodes/simple/DrawTest.java | 229 +++++++++++++++++++++ .../java/com/seaofnodes/simple/EscapeFlowTest.java | 31 +++ src/test/java/com/seaofnodes/simple/Eval2.java | 25 ++- .../simple/FloatComparisonNativeTest.java | 65 ++++++ .../seaofnodes/simple/FloatNegationNativeTest.java | 21 ++ .../java/com/seaofnodes/simple/GraphicsTest.java | 194 +++++++++++++++++ .../com/seaofnodes/simple/IntegerLiteralTest.java | 46 +++++ .../java/com/seaofnodes/simple/IntrinsicTest.java | 188 +++++++++++++++++ .../com/seaofnodes/simple/MemoryEncodingTest.java | 25 +++ .../com/seaofnodes/simple/MethodAccessTest.java | 118 +++++++++++ .../com/seaofnodes/simple/MethodFieldsTest.java | 131 ++++++++++++ .../seaofnodes/simple/ParserDiagnosticTest.java | 82 +++++++- .../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 ++++++++++++++++ src/test/java/com/seaofnodes/simple/TypeTest.java | 20 ++ .../simple/codegen/PrintRegTestSupport.java | 26 ++- 34 files changed, 2156 insertions(+), 29 deletions(-) create mode 100644 src/test/java/com/seaofnodes/simple/ArrayInitTest.java create mode 100644 src/test/java/com/seaofnodes/simple/AssemblyDumpTest.java create mode 100644 src/test/java/com/seaofnodes/simple/AudioTest.java create mode 100644 src/test/java/com/seaofnodes/simple/BreakoutTest.java create mode 100644 src/test/java/com/seaofnodes/simple/ConstantArrayTest.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/FloatNegationNativeTest.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/IntrinsicTest.java create mode 100644 src/test/java/com/seaofnodes/simple/MethodAccessTest.java create mode 100644 src/test/java/com/seaofnodes/simple/MethodFieldsTest.java create mode 100644 src/test/java/com/seaofnodes/simple/TimeTest.java (limited to 'src/test/java/com/seaofnodes') 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(); + 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 + 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=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=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 + 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 + 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( 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..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 CLASSES = new HashMap<>(); + private static final IdentityHashMap 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 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/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 + 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 + #include + 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 && ballprevious_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 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 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 + 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(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/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