summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-08-10 11:11:18 -0700
committerCliff Click <cliffc@acm.org>2026-11-14 16:23:39 -0800
commit207febef9924c8fa5391185e75d42a7ea480bb38 (patch)
tree68ec6225a89fa1178c32fdf659b9b4f2c3327288 /src
parent259f60b105eb770bc6f7e3bd36d91f2b217e9315 (diff)
downloadSimple-207febef9924c8fa5391185e75d42a7ea480bb38.tar.gz
Simple-207febef9924c8fa5391185e75d42a7ea480bb38.zip
Chapter 31: Software Drawing, Time, and Graphical Gameslinear
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 <jun784@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/main/c/simple_audio.c40
-rw-r--r--src/main/c/simple_audio.h16
-rw-r--r--src/main/c/simple_crt.c13
-rw-r--r--src/main/c/simple_graphics.c102
-rw-r--r--src/main/c/simple_graphics.h20
-rw-r--r--src/main/c/simple_miniaudio.c123
-rw-r--r--src/main/c/simple_minifb.c117
-rw-r--r--src/main/c/simple_time.c61
-rw-r--r--src/main/java/com/seaofnodes/simple/README.md2
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/CodeGen.java6
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java15
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/Encoding.java10
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/IterPeeps.java4
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/ParseAll.java5
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/Serialize.java16
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/StaticData.java10
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java6
-rw-r--r--src/main/java/com/seaofnodes/simple/node/ArrayInitNode.java70
-rw-r--r--src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java24
-rw-r--r--src/main/java/com/seaofnodes/simple/node/CallEndNode.java4
-rw-r--r--src/main/java/com/seaofnodes/simple/node/CallNode.java12
-rw-r--r--src/main/java/com/seaofnodes/simple/node/EscapeNode.java6
-rw-r--r--src/main/java/com/seaofnodes/simple/node/Intrinsic.java119
-rw-r--r--src/main/java/com/seaofnodes/simple/node/LoadNode.java26
-rw-r--r--src/main/java/com/seaofnodes/simple/node/MemMergeNode.java16
-rw-r--r--src/main/java/com/seaofnodes/simple/node/MemOpNode.java3
-rw-r--r--src/main/java/com/seaofnodes/simple/node/Node.java4
-rw-r--r--src/main/java/com/seaofnodes/simple/node/StoreNode.java4
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/arm/BitCastARM.java23
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java1
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/riscv/BitCastRISC.java23
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java1
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/BitCastX86.java23
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java27
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java6
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegFX86.java25
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java18
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java3
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java23
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Allocations.java17
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/ArrayInitializers.java187
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Conversions.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Declarations.java4
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Expressions.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Functions.java64
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Lexer.java35
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/MethodAccess.java130
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Parser.java12
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/README.md15
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/References.java13
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Statements.java1
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Strings.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Types.java11
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Var.java5
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/package-info.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/print/ASMPrinter.java15
-rw-r--r--src/main/java/com/seaofnodes/simple/type/Type.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/type/TypeConAry.java147
-rw-r--r--src/main/java/com/seaofnodes/simple/type/TypeConAryB.java20
-rw-r--r--src/main/java/com/seaofnodes/simple/type/TypeConAryI.java16
-rw-r--r--src/main/java/com/seaofnodes/simple/type/TypeFloat.java8
-rw-r--r--src/main/java/com/seaofnodes/simple/type/TypeMem.java9
-rw-r--r--src/main/java/com/seaofnodes/simple/type/TypeStruct.java11
-rw-r--r--src/main/smp/sys.smp6
-rw-r--r--src/main/smp/sys/adt/bitset.smp4
-rw-r--r--src/main/smp/sys/ary.smp8
-rw-r--r--src/main/smp/sys/aryi64.smp6
-rw-r--r--src/main/smp/sys/aryu8.smp13
-rw-r--r--src/main/smp/sys/audio.smp29
-rw-r--r--src/main/smp/sys/draw.smp18
-rw-r--r--src/main/smp/sys/draw/Button.smp41
-rw-r--r--src/main/smp/sys/draw/Canvas.smp149
-rw-r--r--src/main/smp/sys/gfx.smp44
-rw-r--r--src/main/smp/sys/integer.smp6
-rw-r--r--src/main/smp/sys/io.smp2
-rw-r--r--src/main/smp/sys/libc.smp8
-rw-r--r--src/main/smp/sys/math.smp15
-rw-r--r--src/main/smp/sys/rt/simple_args.smp8
-rw-r--r--src/main/smp/sys/text.smp84
-rw-r--r--src/main/smp/sys/time.smp219
-rw-r--r--src/main/smp/sys/time/DateTime.smp13
-rw-r--r--src/main/smp/sys/time/Timers.smp80
-rw-r--r--src/test/c/audio_test.c48
-rw-r--r--src/test/java/com/seaofnodes/simple/ArithNodeTest.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/ArrayInitTest.java211
-rw-r--r--src/test/java/com/seaofnodes/simple/AssemblyDumpTest.java61
-rw-r--r--src/test/java/com/seaofnodes/simple/AudioTest.java94
-rw-r--r--src/test/java/com/seaofnodes/simple/BrainFuckTest.java8
-rw-r--r--src/test/java/com/seaofnodes/simple/BreakoutTest.java216
-rw-r--r--src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java25
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter01Test.java9
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter17bTest.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter21Test.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter22Test.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter23Test.java5
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter25Test.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/ConstantArrayTest.java123
-rw-r--r--src/test/java/com/seaofnodes/simple/ConvertNodeTest.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/DrawTest.java229
-rw-r--r--src/test/java/com/seaofnodes/simple/EscapeFlowTest.java31
-rw-r--r--src/test/java/com/seaofnodes/simple/Eval2.java25
-rw-r--r--src/test/java/com/seaofnodes/simple/FloatComparisonNativeTest.java65
-rw-r--r--src/test/java/com/seaofnodes/simple/FloatNegationNativeTest.java21
-rw-r--r--src/test/java/com/seaofnodes/simple/GraphicsTest.java194
-rw-r--r--src/test/java/com/seaofnodes/simple/IntegerLiteralTest.java46
-rw-r--r--src/test/java/com/seaofnodes/simple/IntrinsicTest.java188
-rw-r--r--src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java25
-rw-r--r--src/test/java/com/seaofnodes/simple/MethodAccessTest.java118
-rw-r--r--src/test/java/com/seaofnodes/simple/MethodFieldsTest.java131
-rw-r--r--src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java82
-rw-r--r--src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java16
-rw-r--r--src/test/java/com/seaofnodes/simple/StreamIOTest.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/StringFormatTest.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/TestC.java5
-rw-r--r--src/test/java/com/seaofnodes/simple/TimeTest.java183
-rw-r--r--src/test/java/com/seaofnodes/simple/TypeTest.java20
-rw-r--r--src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java26
121 files changed, 4480 insertions, 271 deletions
diff --git a/src/main/c/simple_audio.c b/src/main/c/simple_audio.c
new file mode 100644
index 00000000..b8ed62de
--- /dev/null
+++ b/src/main/c/simple_audio.c
@@ -0,0 +1,40 @@
+#include "simple_audio.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <dlfcn.h>
+
+static const struct simple_audio_api *api;
+static void *library;
+
+int32_t simple_audio_open(void) {
+ const char *path = getenv("SIMPLE_MINIAUDIO");
+ if (path && strcmp(path,"off")==0) return -1;
+ if (!api) {
+ if (!path || !*path) return -1; // Optional: games may run silently.
+ library = dlopen(path,RTLD_NOW | RTLD_LOCAL);
+ if (!library) {
+ fprintf(stderr,"Simple audio: cannot load backend '%s': %s\n",path,dlerror());
+ return -1;
+ }
+ const struct simple_audio_api *(*get_api)(void) =
+ (const struct simple_audio_api *(*)(void))dlsym(library,"simple_miniaudio_api");
+ const struct simple_audio_api *candidate = get_api ? get_api() : NULL;
+ if (!candidate || candidate->version!=SIMPLE_AUDIO_VERSION || !candidate->open ||
+ !candidate->load || !candidate->play || !candidate->stop ||
+ !candidate->playing || !candidate->close) {
+ fprintf(stderr,"Simple audio: incompatible backend; rebuild with make audio.\n");
+ dlclose(library);
+ library=NULL;
+ return -1;
+ }
+ api=candidate;
+ atexit(api->close);
+ }
+ return api->open();
+}
+int32_t simple_audio_load(const int16_t *samples, uint32_t count) { return api ? api->load(samples,count) : -1; }
+int32_t simple_audio_play(int32_t sound) { return api ? api->play(sound) : -1; }
+int32_t simple_audio_stop(void) { return api ? api->stop() : 0; }
+int32_t simple_audio_playing(void) { return api ? api->playing() : 0; }
+int32_t simple_audio_close(void) { if (api) api->close(); return 0; }
diff --git a/src/main/c/simple_audio.h b/src/main/c/simple_audio.h
new file mode 100644
index 00000000..77cce530
--- /dev/null
+++ b/src/main/c/simple_audio.h
@@ -0,0 +1,16 @@
+#ifndef SIMPLE_AUDIO_H
+#define SIMPLE_AUDIO_H
+#include <stdint.h>
+
+// Main-thread API. Samples are copied; no Simple heap pointer is retained.
+struct simple_audio_api {
+ int32_t version;
+ int32_t (*open)(void);
+ int32_t (*load)(const int16_t *, uint32_t);
+ int32_t (*play)(int32_t);
+ int32_t (*stop)(void);
+ int32_t (*playing)(void);
+ void (*close)(void);
+};
+#define SIMPLE_AUDIO_VERSION 1
+#endif
diff --git a/src/main/c/simple_crt.c b/src/main/c/simple_crt.c
index 438ae5f9..39708629 100644
--- a/src/main/c/simple_crt.c
+++ b/src/main/c/simple_crt.c
@@ -6,6 +6,19 @@
#include <termios.h>
#include <unistd.h>
+// Fixed-semantics compiler intrinsics. Keep these definitions for indirect
+// calls; direct calls may inline to representation-preserving register moves.
+int64_t simple_f64_bits(double value) {
+ int64_t bits;
+ memcpy(&bits, &value, sizeof(bits));
+ return bits;
+}
+double simple_f64_from_bits(int64_t bits) {
+ double value;
+ memcpy(&value, &bits, sizeof(value));
+ return value;
+}
+
// errno may be thread-local or a macro calling a platform accessor.
int32_t simple_errno(void) { return errno; }
int32_t simple_set_errno(int32_t error) { errno = error; return -1; }
diff --git a/src/main/c/simple_graphics.c b/src/main/c/simple_graphics.c
new file mode 100644
index 00000000..40d66ba9
--- /dev/null
+++ b/src/main/c/simple_graphics.c
@@ -0,0 +1,102 @@
+#define _POSIX_C_SOURCE 200809L
+#include "simple_graphics.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <dlfcn.h>
+#include <unistd.h>
+
+#ifdef __CYGWIN__
+#define BACKEND_NAME "simple_minifb.dll"
+#define BACKEND_ABI "x86_64_v2_win64"
+#else
+#define BACKEND_NAME "libsimple_minifb.so"
+#define BACKEND_ABI "x86_64_v2_SystemV"
+#endif
+
+// Executable-relative paths work even when launched outside the checkout.
+// Working-directory paths also cover test executables and chapter/root shells.
+static void *find_backend(void) {
+ char executable[4096], path[8192];
+ ssize_t n=readlink("/proc/self/exe",executable,sizeof(executable)-1);
+ if (n>0 && n<(ssize_t)sizeof(executable)-1) {
+ executable[n]=0;
+ char *slash=strrchr(executable,'/');
+ if (slash) {
+ *slash=0;
+ const char *relative[]={"/" BACKEND_NAME,
+ "/../../release/" BACKEND_ABI "/" BACKEND_NAME,
+ "/../release/" BACKEND_ABI "/" BACKEND_NAME};
+ for (unsigned i=0;i<sizeof(relative)/sizeof(*relative);++i) {
+ snprintf(path,sizeof(path),"%s%s",executable,relative[i]);
+ void *handle=dlopen(path,RTLD_NOW | RTLD_LOCAL);
+ if (handle) return handle;
+ }
+ }
+ }
+ const char *relative[]={"./" BACKEND_NAME,
+ "./build/release/" BACKEND_ABI "/" BACKEND_NAME,
+ "./chapter31/build/release/" BACKEND_ABI "/" BACKEND_NAME};
+ for (unsigned i=0;i<sizeof(relative)/sizeof(*relative);++i) {
+ void *handle=dlopen(relative[i],RTLD_NOW | RTLD_LOCAL);
+ if (handle) return handle;
+ }
+ return NULL;
+}
+
+// No MiniFB headers or link dependency in the default runtime. Cygwin's
+// dlopen also loads native Windows DLLs; both use the x64 Windows C ABI.
+static const struct simple_graphics_api *api;
+static void *library;
+
+static int load_graphics(void) {
+ // Explicitly disabling graphics also makes headless tests deterministic.
+ const char *path = getenv("SIMPLE_MINIFB");
+ if (path && strcmp(path, "off") == 0) {
+ fprintf(stderr, "Simple graphics: disabled (SIMPLE_MINIFB=off).\n");
+ return -1;
+ }
+ if (api) return 0;
+ int explicit_path=path && *path;
+ library = explicit_path ? dlopen(path, RTLD_NOW | RTLD_LOCAL) : find_backend();
+ if (!library) {
+ if (explicit_path)
+ fprintf(stderr, "Simple graphics: cannot load MiniFB backend '%s': %s\n", path, dlerror());
+ else
+ fprintf(stderr, "Simple graphics: MiniFB backend not found beside the executable or in build/release/"
+ BACKEND_ABI ". Run make graphics, or set SIMPLE_MINIFB to its full path.\n");
+ return -1;
+ }
+ if (!explicit_path) path="automatically discovered backend";
+ const struct simple_graphics_api *(*get_api)(void) =
+ (const struct simple_graphics_api *(*)(void))dlsym(library, "simple_minifb_api");
+ const struct simple_graphics_api *candidate = get_api ? get_api() : NULL;
+ if (!candidate || candidate->version != SIMPLE_GRAPHICS_VERSION ||
+ !candidate->open || !candidate->present || !candidate->poll ||
+ !candidate->mouse_x || !candidate->mouse_y || !candidate->button ||
+ !candidate->key || !candidate->close || !candidate->wait) {
+ fprintf(stderr, "Simple graphics: incompatible MiniFB backend '%s'. Rebuild with make graphics.\n", path);
+ dlclose(library);
+ library = NULL;
+ return -1;
+ }
+ api = candidate;
+ // Keep the library loaded until process exit; its window callbacks may
+ // still be registered. Closing a window releases its native resources.
+ atexit(api->close);
+ return 0;
+}
+
+int32_t simple_gfx_open(const char *title, uint32_t width, uint32_t height) {
+ return load_graphics() ? -1 : api->open(title, width, height);
+}
+int32_t simple_gfx_present(const uint32_t *pixels, uint32_t count) {
+ return api ? api->present(pixels, count) : -1;
+}
+int32_t simple_gfx_poll(int32_t wait) { return api ? api->poll(wait) : -1; }
+int32_t simple_gfx_mouse_x(void) { return api ? api->mouse_x() : 0; }
+int32_t simple_gfx_mouse_y(void) { return api ? api->mouse_y() : 0; }
+int32_t simple_gfx_button(int32_t button) { return api ? api->button(button) : 0; }
+int32_t simple_gfx_key(int32_t key) { return api ? api->key(key) : 0; }
+int32_t simple_gfx_close(void) { if (api) api->close(); return 0; }
+int32_t simple_gfx_wait(int64_t timeout) { return api ? api->wait(timeout) : -1; }
diff --git a/src/main/c/simple_graphics.h b/src/main/c/simple_graphics.h
new file mode 100644
index 00000000..370256be
--- /dev/null
+++ b/src/main/c/simple_graphics.h
@@ -0,0 +1,20 @@
+#ifndef SIMPLE_GRAPHICS_H
+#define SIMPLE_GRAPHICS_H
+#include <stdint.h>
+
+// Private, versioned boundary between the tiny runtime loader and MiniFB.
+// All calls run on the main thread. Pixel memory is borrowed for present only.
+struct simple_graphics_api {
+ int32_t version;
+ int32_t (*open)(const char *, uint32_t, uint32_t);
+ int32_t (*present)(const uint32_t *, uint32_t);
+ int32_t (*poll)(int32_t);
+ int32_t (*mouse_x)(void);
+ int32_t (*mouse_y)(void);
+ int32_t (*button)(int32_t);
+ int32_t (*key)(int32_t);
+ void (*close)(void);
+ int32_t (*wait)(int64_t);
+};
+#define SIMPLE_GRAPHICS_VERSION 2
+#endif
diff --git a/src/main/c/simple_miniaudio.c b/src/main/c/simple_miniaudio.c
new file mode 100644
index 00000000..03dbadd7
--- /dev/null
+++ b/src/main/c/simple_miniaudio.c
@@ -0,0 +1,123 @@
+#include "simple_audio.h"
+#define MA_NO_DECODING
+#define MA_NO_ENCODING
+#define MA_NO_RESOURCE_MANAGER
+#define MA_NO_NODE_GRAPH
+#define MA_NO_ENGINE
+#define MINIAUDIO_IMPLEMENTATION
+#include <miniaudio.h>
+#include <math.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+#define RATE 48000
+#define SLOTS 32
+static ma_device device;
+static ma_mutex mutex;
+static int opened, active;
+static uint32_t cursor;
+static struct { int16_t *pcm; uint32_t count; } sounds[SLOTS];
+
+// Only native-owned immutable PCM is read on the audio thread. The lock covers
+// a bounded copy, never allocation, synthesis, device calls, or Simple code.
+static void output(ma_device *unused, void *out, const void *in, ma_uint32 frames) {
+ (void)unused; (void)in;
+ memset(out,0,frames*sizeof(int16_t));
+ ma_mutex_lock(&mutex);
+ if (active) {
+ uint32_t count=sounds[active].count-cursor;
+ if (count>frames) count=frames;
+ memcpy(out,sounds[active].pcm+cursor,count*sizeof(int16_t));
+ cursor+=count;
+ if (cursor==sounds[active].count) active=0;
+ }
+ ma_mutex_unlock(&mutex);
+}
+static int32_t load(const int16_t *pcm, uint32_t count) {
+ if (!opened || !pcm || !count || count>RATE*10) return -1;
+ int slot;
+ for (slot=4; slot<SLOTS && sounds[slot].pcm; ++slot) {}
+ if (slot==SLOTS) return -1;
+ int16_t *copy=malloc(count*sizeof(*copy));
+ if (!copy) return -1;
+ memcpy(copy,pcm,count*sizeof(*copy));
+ ma_mutex_lock(&mutex);
+ sounds[slot].pcm=copy; sounds[slot].count=count;
+ ma_mutex_unlock(&mutex);
+ return slot;
+}
+static int32_t play(int32_t sound) {
+ if (!opened || sound<=0 || sound>=SLOTS || !sounds[sound].pcm) return -1;
+ ma_mutex_lock(&mutex);
+ active=sound; cursor=0;
+ ma_mutex_unlock(&mutex);
+ return 0;
+}
+static int32_t stop(void) {
+ if (opened) { ma_mutex_lock(&mutex); active=0; ma_mutex_unlock(&mutex); }
+ return 0;
+}
+static int32_t playing(void) {
+ if (!opened) return 0;
+ ma_mutex_lock(&mutex);
+ int result=active!=0;
+ ma_mutex_unlock(&mutex);
+ return result;
+}
+static void close_audio(void) {
+ if (!opened) return;
+ ma_device_uninit(&device); // Joins callback before freeing buffers/mutex.
+ ma_mutex_uninit(&mutex);
+ for (int i=1;i<SLOTS;++i) {
+ free(sounds[i].pcm); sounds[i].pcm=NULL; sounds[i].count=0;
+ }
+ opened=0; active=0; cursor=0;
+}
+static int tone(int slot, int descending) {
+ uint32_t note=slot==1 ? RATE*7/100 : RATE*16/100;
+ int notes=slot==1 ? 1 : 3;
+ sounds[slot].count=note*notes;
+ sounds[slot].pcm=calloc(note*notes,sizeof(int16_t));
+ if (!sounds[slot].pcm) return -1;
+ const double pitches[3]={523.251,659.255,783.991};
+ for (int n=0;n<notes;++n) {
+ double hz=slot==1 ? 440.0 : pitches[descending ? 2-n : n];
+ for (uint32_t i=0;i<note;++i) {
+ double t=(double)i/note;
+ double envelope=sin(3.141592653589793*t);
+ sounds[slot].pcm[n*note+i]=(int16_t)(3200*envelope*envelope*sin(6.283185307179586*hz*i/RATE));
+ }
+ }
+ return 0;
+}
+static int32_t open_audio(void) {
+ if (opened) return 0;
+ if (ma_mutex_init(&mutex)!=MA_SUCCESS) return -1;
+ ma_device_config config=ma_device_config_init(ma_device_type_playback);
+ config.playback.format=ma_format_s16;
+ config.playback.channels=1;
+ config.sampleRate=RATE;
+ config.dataCallback=output;
+#ifdef SIMPLE_AUDIO_TEST_NULL
+ ma_backend backend=ma_backend_null;
+ ma_result result=ma_device_init_ex(&backend,1,NULL,&config,&device);
+#else
+ ma_result result=ma_device_init(NULL,&config,&device);
+#endif
+ if (result!=MA_SUCCESS) {
+ ma_mutex_uninit(&mutex);
+ fprintf(stderr,"Simple audio: no playback device (%d); continuing silently.\n",result);
+ return -1;
+ }
+ opened=1;
+ if (tone(1,0) || tone(2,0) || tone(3,1) || ma_device_start(&device)!=MA_SUCCESS) {
+ close_audio(); return -1;
+ }
+ return 0;
+}
+static const struct simple_audio_api api={SIMPLE_AUDIO_VERSION,open_audio,load,play,stop,playing,close_audio};
+#ifdef _WIN32
+__declspec(dllexport)
+#endif
+const struct simple_audio_api *simple_miniaudio_api(void) { return &api; }
diff --git a/src/main/c/simple_minifb.c b/src/main/c/simple_minifb.c
new file mode 100644
index 00000000..2117f0d2
--- /dev/null
+++ b/src/main/c/simple_minifb.c
@@ -0,0 +1,117 @@
+#include "simple_graphics.h"
+#include <MiniFB.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+#ifdef _WIN32
+#include <windows.h>
+#else
+#include <errno.h>
+#include <time.h>
+#include <poll.h>
+#include "WindowData.h"
+#include "x11/WindowData_X11.h"
+#endif
+
+// One fixed-size window for now. No Simple pointer survives a native call:
+// MiniFB retains its last draw buffer for OS repaint events, so copy it here.
+static struct mfb_window *window;
+static uint32_t *frame;
+static uint32_t width, height;
+
+// The C-only build never installs the upstream C++ callback wrappers.
+void release_cpp_stub(struct mfb_window *unused) { (void)unused; }
+
+static void close_window(void) {
+ if (window) {
+ mfb_close(window); // Requests close.
+ mfb_update_events(window); // Actually destroys the window.
+ window = NULL;
+ }
+ free(frame);
+ frame = NULL;
+}
+
+static int32_t open_window(const char *title, uint32_t w, uint32_t h) {
+ if (window || !w || !h || w > UINT32_MAX / 4 ||
+ (uint64_t)w * h > UINT32_MAX || (uint64_t)w * h > SIZE_MAX / 4) {
+ fprintf(stderr, "Simple graphics: invalid dimensions or a window is already open.\n");
+ return -1;
+ }
+ frame = calloc((size_t)w * h, sizeof(*frame));
+ if (!frame) {
+ fprintf(stderr, "Simple graphics: cannot allocate the display buffer.\n");
+ return -1;
+ }
+ window = mfb_open(title, w, h);
+ if (!window) {
+ fprintf(stderr, "Simple graphics: MiniFB could not open a window (is a graphical display available?).\n");
+ close_window();
+ return -1;
+ }
+ width = w;
+ height = h;
+ return 0;
+}
+
+static int32_t status(mfb_update_state state) {
+ if (state == MFB_STATE_OK) return 0;
+ if (state == MFB_STATE_EXIT) {
+ window = NULL; // MiniFB has already destroyed it.
+ close_window();
+ return 1;
+ }
+ fprintf(stderr, "Simple graphics: MiniFB update failed (%d).\n", state);
+ close_window();
+ return -1;
+}
+
+static int32_t present(const uint32_t *pixels, uint32_t count) {
+ if (!window) return 1;
+ if (!pixels || count != (uint64_t)width * height) {
+ fprintf(stderr, "Simple graphics: pixel array length must equal width * height.\n");
+ return -1;
+ }
+ memcpy(frame, pixels, (size_t)count * sizeof(*frame));
+ return status(mfb_update_ex(window, frame, width, height));
+}
+
+static int32_t wait_events(int64_t timeout) {
+ if (!window) return 1;
+ if (timeout < -1) return -1;
+ // May return early; the game recomputes its deadlines after every wakeup.
+ int ms=timeout>INT_MAX ? INT_MAX : (int)timeout;
+#ifdef _WIN32
+ if (MsgWaitForMultipleObjectsEx(0,NULL,ms<0 ? INFINITE : (DWORD)ms,
+ QS_ALLINPUT,MWMO_INPUTAVAILABLE)==WAIT_FAILED) return -1;
+#else
+ // The pinned MiniFB X11 backend owns this connection. Check already queued
+ // Xlib events before waiting on its socket; no new event thread is needed.
+ SWindowData *data=(SWindowData *)window;
+ SWindowData_X11 *native=(SWindowData_X11 *)data->specific;
+ if (!XPending(native->display)) {
+ struct pollfd fd={ConnectionNumber(native->display),POLLIN,0};
+ if (poll(&fd,1,ms)<0 && errno!=EINTR) return -1;
+ }
+#endif
+ return status(mfb_update_events(window));
+}
+static int32_t poll_events(int32_t wait) { return wait_events(wait ? 17 : 0); }
+static int32_t mouse_x(void) { return window ? mfb_get_mouse_x(window) : 0; }
+static int32_t mouse_y(void) { return window ? mfb_get_mouse_y(window) : 0; }
+static int32_t button(int32_t b) {
+ return window && b >= 0 && b < 8 ? mfb_get_mouse_button_buffer(window)[b] != 0 : 0;
+}
+static int32_t key(int32_t k) {
+ return window && k >= 0 && k <= MFB_KB_KEY_LAST ? mfb_get_key_buffer(window)[k] != 0 : 0;
+}
+
+static const struct simple_graphics_api api = {
+ SIMPLE_GRAPHICS_VERSION, open_window, present, poll_events,
+ mouse_x, mouse_y, button, key, close_window, wait_events
+};
+#ifdef _WIN32
+__declspec(dllexport)
+#endif
+const struct simple_graphics_api *simple_minifb_api(void) { return &api; }
diff --git a/src/main/c/simple_time.c b/src/main/c/simple_time.c
new file mode 100644
index 00000000..2e579922
--- /dev/null
+++ b/src/main/c/simple_time.c
@@ -0,0 +1,61 @@
+#define _POSIX_C_SOURCE 200809L
+#include <time.h>
+#include <stdint.h>
+#include <errno.h>
+
+// Fixed-width bridge; no native struct layout or retained Simple pointers.
+static int64_t clock_value(clockid_t clock, int64_t scale) {
+ struct timespec t;
+ if (clock_gettime(clock,&t)) return -1;
+ if (t.tv_sec>INT64_MAX/scale || t.tv_sec<INT64_MIN/scale) {
+ errno=EOVERFLOW; return -1;
+ }
+ int64_t base=(int64_t)t.tv_sec*scale;
+ int64_t fraction=t.tv_nsec/(1000000000/scale);
+ if (base>INT64_MAX-fraction) { errno=EOVERFLOW; return -1; }
+ return base+fraction;
+}
+int64_t simple_time_now(void) { return clock_value(CLOCK_REALTIME,1000); }
+int64_t simple_time_mono(void) { return clock_value(CLOCK_MONOTONIC,1000); }
+int64_t simple_time_nano(void) { return clock_value(CLOCK_MONOTONIC,1000000000); }
+int32_t simple_time_sleep(int64_t ms, int32_t absolute) {
+ if (ms<0) { errno=EINVAL; return -1; }
+ struct timespec t={ms/1000,(ms%1000)*1000000};
+ int result;
+ if (!absolute) {
+ // Retry the remaining duration after signals; no busy waiting.
+ do { result=nanosleep(&t,&t); } while (result && errno==EINTR);
+ return result;
+ }
+ do { result=clock_nanosleep(CLOCK_MONOTONIC,TIMER_ABSTIME,&t,NULL); } while (result==EINTR);
+ if (result) { errno=result; return -1; }
+ return 0;
+}
+static int leap(int y) { return y%4==0 && (y%100!=0 || y%400==0); }
+static int local_fields(int64_t ms, struct tm *local, struct tm *utc) {
+ tzset();
+ int64_t sec=ms/1000;
+ if (ms%1000<0) --sec;
+ time_t t=(time_t)sec;
+ if ((int64_t)t!=sec || !localtime_r(&t,local) || !gmtime_r(&t,utc)) return -1;
+ if (local->tm_year < -1899 || local->tm_year > 8099) { errno=EOVERFLOW; return -1; }
+ return 0;
+}
+int32_t simple_time_local(int64_t ms, int64_t *out) {
+ struct tm t,u;
+ if (local_fields(ms,&t,&u)) return -1;
+ int days=t.tm_yday-u.tm_yday;
+ if (t.tm_year>u.tm_year) days+=365+leap(u.tm_year+1900);
+ if (t.tm_year<u.tm_year) days-=365+leap(t.tm_year+1900);
+ int offset=days*86400+(t.tm_hour-u.tm_hour)*3600+(t.tm_min-u.tm_min)*60+t.tm_sec-u.tm_sec;
+ out[0]=t.tm_year+1900; out[1]=t.tm_mon+1; out[2]=t.tm_mday;
+ out[3]=t.tm_hour; out[4]=t.tm_min; out[5]=t.tm_sec;
+ out[6]=(ms%1000+1000)%1000; out[7]=t.tm_wday;
+ out[8]=offset; out[9]=t.tm_isdst>0;
+ return 0;
+}
+int32_t simple_time_zone(int64_t ms, char *out, uint32_t capacity) {
+ struct tm t,u;
+ if (!capacity || local_fields(ms,&t,&u)) return -1;
+ return (int32_t)strftime(out,capacity,"%Z",&t);
+}
diff --git a/src/main/java/com/seaofnodes/simple/README.md b/src/main/java/com/seaofnodes/simple/README.md
index af86f21e..a8916aa8 100644
--- a/src/main/java/com/seaofnodes/simple/README.md
+++ b/src/main/java/com/seaofnodes/simple/README.md
@@ -2,7 +2,7 @@
| Directory | Contents |
| --- | --- |
-| `parser/` | Recursive-descent grammar routines, lexing, name resolution, and construction of the initial IR. `Var` describes a lexical binding. See the [grammar map](../../../../../../docs/chapter30/30-grammar.md). |
+| `parser/` | Recursive-descent grammar routines, lexing, name resolution, and construction of the initial IR. `Var` describes a lexical binding. See the [grammar map](../../../../../../docs/chapter31/31-grammar.md). |
| `node/` | Sea-of-Nodes IR nodes: graph edges, type computation, and local peephole rewrites. |
| `node/cpus/` | Machine-specific nodes and instruction selection support for `arm`, `riscv`, and `x86_64_v2`. |
| `codegen/` | Compilation driver and passes: module loading, optimization, scheduling, register allocation, serialization, and machine-code emission. `IterPeeps` runs the peephole worklist. |
diff --git a/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java b/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java
index b4c93e1e..8618b47b 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java
@@ -786,7 +786,11 @@ public class CodeGen {
public boolean _asmLittle=true;
public String asm() { return asm(new SB()).toString(); }
SB asm(SB sb) {
- ASMPrinter.print(sb,this);
+ if( _encodings.isEmpty() ) ASMPrinter.print(sb,this);
+ else for( var entry : _encodings.entrySet() ) {
+ sb.p("\n=== Compilation unit: ").p(entry.getKey()._cname).p(" ===\n");
+ ASMPrinter.print(sb,this,entry.getValue());
+ }
return sb;
}
diff --git a/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java b/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java
index fcf5c287..c5b25be0 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java
@@ -318,11 +318,15 @@ public class ElfWriter {
_strtab = new StringSection();
// place all the symbols
+ Encoding enc = _code._encoding;
int nlocals = 1/*null*/+9/*number of sections*/+1/*Simple section*/;
+ int debugLocals=0;
+ for( StaticData.ObjectData obj : enc._data.objects.values() )
+ if( !obj.external ) debugLocals+=obj.debugNames.size();
+ nlocals+=debugLocals;
SymbolSection symbols = new SymbolSection(nlocals);
// we've already constructed these entire sections in the encoding phase
- Encoding enc = _code._encoding;
// Append ELF-only thunks to a copy. Export must not change instruction
// offsets or the image used by the in-memory linker and emulators.
BAOS textBits = new BAOS(enc._bits.toByteArray(),enc._bits.size());
@@ -402,10 +406,17 @@ public class ElfWriter {
SimpleSection simple = new SimpleSection(_code._serial);
// Write the local section header symbols
- assert nlocals == 1+_sections._len;
+ assert nlocals == 1+_sections._len+debugLocals;
symbols.symbol("",0,0,0); // Null symbol at index 0
for( Section s : _sections )
symbols.symbol(s._name, s._index, SYM_BIND_LOCAL, SYM_TYPE_SECTION, 0, s.size());
+ // Local aliases describe the pooled object without changing linkage.
+ // Several source constants may name the same bytes.
+ for( StaticData.ObjectData obj : enc._data.objects.values() )
+ if( !obj.external )
+ for( String name : obj.debugNames )
+ symbols.symbol(name,obj.readOnly ? rodata._index : rwdata._index,
+ SYM_BIND_LOCAL,SYM_TYPE_OBJECT,obj.offset,obj.size);
int size = 64; // ELF header size
// size of all section data
diff --git a/src/main/java/com/seaofnodes/simple/codegen/Encoding.java b/src/main/java/com/seaofnodes/simple/codegen/Encoding.java
index b07a8fec..eb62c36a 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/Encoding.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/Encoding.java
@@ -39,7 +39,9 @@ public class Encoding implements com.seaofnodes.isa.CodeSink {
public final BAOS _bits = new BAOS(); // Instructions / encodings
public final BAOS _cpool = new BAOS(); // Constant r/o pool
public final BAOS _sdata = new BAOS(); // Static r/w pool
- public final HashMap<Node,Relo> _bigCons = new HashMap<>();
+ // Relocations belong to instruction occurrences, including equal constant
+ // loads cloned by scheduling/register allocation. Structural keys lose sites.
+ public final IdentityHashMap<Node,Relo> _bigCons = new IdentityHashMap<>();
public final StaticData _data;
public final HashMap<String,Integer> _externalAddresses = new HashMap<>();
@@ -157,7 +159,7 @@ public class Encoding implements com.seaofnodes.isa.CodeSink {
// Nodes need "relocation" patching; things done after code is placed.
// Record src and dst Nodes.
- private final HashMap<Node,CFGNode> _internals = new HashMap<>();
+ private final IdentityHashMap<Node,CFGNode> _internals = new IdentityHashMap<>();
// Source is a Call, destination in the Fun.
public Encoding relo( CallNode call ) {
FunNode fun = _code.link(call.tfp());
@@ -187,13 +189,13 @@ public class Encoding implements com.seaofnodes.isa.CodeSink {
}
// External references only located by Strings a link-time
- public final HashMap<Node,String> _externals = new HashMap<>();
+ public final IdentityHashMap<Node,String> _externals = new IdentityHashMap<>();
public Encoding external( Node n, String extern ) {
_externals.put(n,extern);
return this;
}
- public final HashMap<Node,String> _externData = new HashMap<>();
+ public final IdentityHashMap<Node,String> _externData = new IdentityHashMap<>();
public void externalData(Node n, String name) { _externData.put(n,name); }
// Store t as a 32/64 bit constant in the code space; generate RIP-relative
diff --git a/src/main/java/com/seaofnodes/simple/codegen/IterPeeps.java b/src/main/java/com/seaofnodes/simple/codegen/IterPeeps.java
index 985cd4d2..f4acb729 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/IterPeeps.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/IterPeeps.java
@@ -65,8 +65,10 @@ public class IterPeeps {
* Iterate peepholes and inlining to a fixed point
*/
public void iterate( CodeGen code ) {
+ assert !CodeGen.expensiveAssert(0) || (progressOnList(code, _work) && schedulableUses(code));
boolean didInline = false;
Ary<Node> defer = new Ary<>(Node.class);
+
while( true ) {
// Clean up everything that does not grow the code
iteratePeeps(code);
@@ -97,8 +99,6 @@ public class IterPeeps {
// Run all the code-reduction and type-lifting peeps as possible
private void iteratePeeps( CodeGen code ) {
- assert !CodeGen.expensiveAssert(0) || (progressOnList(code, _work) && schedulableUses(code));
-
Node n;
while( (n = _work.pop()) != null ) {
if( n.isDead() ) continue;
diff --git a/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java b/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java
index 3306ff8f..882a0f8a 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java
@@ -150,7 +150,8 @@ public abstract class ParseAll {
}
private static void addRequiredTypes(CodeGen code, CompUnit cunit) {
- for( String typeName : code.P._requiredTypes ) {
+ for( var required : code.P._requiredTypes.entrySet() ) {
+ String typeName = required.getKey();
// A forward use can have been resolved later in this source file.
if( Parser.TYPES.get(typeName) instanceof TypeStruct ts && !ts._fref ) continue;
try {
@@ -158,7 +159,7 @@ public abstract class ParseAll {
if( xcunit != null )
cunit.addDep(xcunit);
else
- assert false : "expected to find required type";
+ throw Parser.error("Unknown type '"+typeName+"'",required.getValue());
} catch( IOException ioe ) {
throw new RuntimeException(ioe);
}
diff --git a/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java b/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java
index 3991cd48..1b335c50 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java
@@ -215,7 +215,7 @@ public class RegAlloc {
_deferredLoopSelf.clear();
for( byte round=0; !graphColor(round); round++ ) {
split(round);
- if( round >= 7 ) {
+ if( round >= 10 ) {
FunNode fun = _cfg.at(0).fun();
throw new IllegalStateException("Register allocation failed after eight rounds in "+
(fun==null ? "<root>" : fun.label()));
diff --git a/src/main/java/com/seaofnodes/simple/codegen/Serialize.java b/src/main/java/com/seaofnodes/simple/codegen/Serialize.java
index ae4ecb74..b168c4af 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/Serialize.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/Serialize.java
@@ -417,7 +417,7 @@ public class Serialize {
assert elf._clz._name==strs[1/*skip null ptr*/];
for( Type type : types )
if( type instanceof TypeStruct ts )
- Parser.TYPES.put(ts._name,ts);
+ installType(ts);
for( Type type : types ) {
if( !(type instanceof TypeStruct ts) || !TypeStruct.startsClzPrefix(ts._name) )
continue;
@@ -429,7 +429,7 @@ public class Serialize {
elf._pubTypes = new TypeStruct[npubTypes];
for( int i=0; i<npubTypes; i++ ) {
TypeStruct ts = elf._pubTypes[i] = (TypeStruct)types[pubTypeIds[i]];
- Parser.TYPES.put(ts._name,ts);
+ installType(ts);
}
elf._functions = new FunctionSummary[bais.packed4()];
for( int i = 0; i<elf._functions.length; i++ ) {
@@ -444,6 +444,18 @@ public class Serialize {
return elf._types = new TypeRead(types,strs,fileAliases,fileFidxs,fileRpcs);
}
+ private static void installType(TypeStruct ts) {
+ // TYPES names the mutable array declaration. Read-only arrays in
+ // imported signatures keep their own types, but must not replace that
+ // declaration: closeOver looks up its mutable variant by name.
+ if( ts.isConAry() ) {
+ TypeConAry ary=(TypeConAry)ts.field("[]")._t;
+ ts=TypeStruct.makeAry(ts._name,TypeInteger.U32,ts.field("#")._alias,
+ ary.storageType(),ts.field("[]")._alias,false);
+ } else if( ts.isAry() ) ts = ts.replace(ts.field("[]").makeFrom(false));
+ Parser.TYPES.put(ts._name,ts);
+ }
+
static void readAll( CodeGen code, ElfReader elf, GlobalBits aliases, GlobalBits fidxs, GlobalBits rpcs ) {
BAOS bais = elf._bais;
TypeRead tread = readTypes(code, elf, aliases, fidxs, rpcs);
diff --git a/src/main/java/com/seaofnodes/simple/codegen/StaticData.java b/src/main/java/com/seaofnodes/simple/codegen/StaticData.java
index 5d1a519b..3d3a40dd 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/StaticData.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/StaticData.java
@@ -14,7 +14,10 @@ public final class StaticData {
public final String symbol;
public final boolean readOnly, external;
public final int alignment, size;
+ // Source names are aliases, not part of the object's pooling identity.
+ final TreeSet<String> debugNames = new TreeSet<>();
public int offset;
+ public String label() { return debugNames.isEmpty() ? symbol : String.join(", ",debugNames); }
ObjectData(Type t, String name, boolean ro, boolean ext) {
type=t; symbol=name; readOnly=ro; external=ext;
alignment=t.alignment();
@@ -73,7 +76,12 @@ public final class StaticData {
// Register before following fields: class objects may refer to each other.
if( !obj.external && t instanceof TypeStruct ts )
for( Field f : ts._fields )
- if( !f._extern && f._t.isConstant() ) target(f._t);
+ if( !f._extern && f._t.isConstant() ) {
+ target(f._t);
+ if( clz && f._final && f._t instanceof TypeMemPtr ptr &&
+ pointerConstant(ptr) && !isClass(ptr._obj) )
+ object(ptr._obj).debugNames.add(ts._name.substring(TypeStruct.CLZ.length())+"."+f._fname);
+ }
return obj;
}
diff --git a/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java b/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java
index a86cd18a..418edaf6 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java
@@ -21,12 +21,14 @@ abstract class TypeCheck {
Ary<Node> errs = new Ary<>(Node.class);
Ary<ConstructNode> constructs = new Ary<>(ConstructNode.class);
Ary<InitViewNode> views = new Ary<>(InitViewNode.class);
+ Ary<ArrayInitNode> arrays = new Ary<>(ArrayInitNode.class);
code._stop.walk( n -> {
if( n.err() != null )
errs.add(n);
if( n instanceof CLInitNode ) code.add(n);
if( n instanceof ConstructNode con ) constructs.add(con);
if( n instanceof InitViewNode view ) views.add(view);
+ if( n instanceof ArrayInitNode array ) arrays.add(array);
return null;
});
@@ -57,13 +59,15 @@ abstract class TypeCheck {
if( !con.isDead() ) con.subsume(con.in(1));
for( InitViewNode view : views )
if( !view.isDead() ) view.subsume(view.in(1));
+ for( ArrayInitNode array : arrays )
+ if( !array.isDead() ) array.subsume(array.in(2));
}
private static int errorRank(Node n) {
// Bad memory access originates the error. Missing construction fields
// in turn explain generic-scalar arithmetic further downstream.
if( n instanceof MemOpNode && noBottomInputs(n) ) return 0;
- if( n instanceof ConstructNode ) return 1;
+ if( n instanceof ConstructNode || n instanceof ArrayInitNode ) return 1;
return noBottomInputs(n) ? 2 : 3;
}
diff --git a/src/main/java/com/seaofnodes/simple/node/ArrayInitNode.java b/src/main/java/com/seaofnodes/simple/node/ArrayInitNode.java
new file mode 100644
index 00000000..c6ae90ea
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/node/ArrayInitNode.java
@@ -0,0 +1,70 @@
+package com.seaofnodes.simple.node;
+
+import com.seaofnodes.print.ExprPrinter;
+import com.seaofnodes.simple.parser.Lexer;
+import com.seaofnodes.simple.parser.Parser;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
+
+/** Source checks for array construction and its yielded elements. Erased by
+ * TypeCheck before serialization and scheduling. */
+public final class ArrayInitNode extends Node {
+ private final int _count; // List size, -1 for a generator, -2 for a yielded element.
+ private final Lexer _loc;
+ public ArrayInitNode(Node ctrl, Node mem, Node ptr, Node len, int count, Lexer loc, Ary<Node> values) {
+ super(ctrl,mem,ptr,len);
+ _count=count; _loc=loc;
+ for( Node value : values ) addDef(value);
+ }
+ public ArrayInitNode(Node ctrl, Node value, Lexer loc) {
+ super(ctrl,null,value);
+ _count=-2; _loc=loc;
+ }
+ @Override public String label() { return "ArrayInit"; }
+ @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return p.p("arrayInit(").n(in(2)).p(")"); }
+ @Override public Type compute() {
+ if( in(0)._type != Type.CONTROL ) return Type.TOP;
+ Type ptr = in(2)._type;
+ if( _count < 0 ) return ptr;
+ // A list is strict in all its inputs. Wait for every input before
+ // choosing a constant payload or falling to the ordinary array type.
+ // Otherwise one SCCP worklist order can return PtrTop (or an array)
+ // before another unresolved input sends the result back up to Top.
+ if( ptr.isHigh() || in(3)._type.isHigh() ) return Type.TOP;
+ for( int i = 4; i < nIns(); i++ )
+ if( in(i)._type.isHigh() ) return Type.TOP;
+ if( !(ptr instanceof TypeMemPtr tmp) ) return ptr;
+ Field body = tmp._obj.field("[]");
+ if( body == null || !body._final || !(body._t instanceof TypeInteger storage) ) return ptr;
+ if( !(in(3)._type instanceof TypeInteger length) || !length.isConstant() || length.value() != _count ) return ptr;
+ long[] values = new long[_count];
+ for( int i = 0; i < _count; i++ ) {
+ Type value = in(i+4)._type;
+ if( !(value instanceof TypeInteger integer) || !integer.isConstant() ) return ptr;
+ values[i] = integer.value();
+ }
+ TypeConAry contents = TypeConAry.make(storage,values);
+ TypeStruct array = TypeStruct.makeAry(tmp._obj._name,tmp._obj.field("#")._alias,body._alias,contents);
+ // The surrounding ReadOnlyNode supplies the public view. Keep this
+ // node's access flags consistent when its contents cease to be constant.
+ return TypeMemPtr.make((byte)2,array,true).withAccess(tmp._ro);
+ }
+ @Override public Node idealize() { return null; }
+ @Override public Parser.ParseException err() {
+ if( in(0)._type==Type.XCONTROL ) return null;
+ if( _count==-2 )
+ return TypeMemPtr.mayBePartial(in(2)._type)
+ ? Parser.error("Array initializer requires fully initialized elements",_loc) : null;
+ if( !(in(3)._type instanceof TypeInteger len) )
+ return Parser.error("Array length must be an integer",_loc);
+ if( len.isConstant() && (len.value()<0 || len.value()>TypeInteger.U32._max) )
+ return Parser.error("Array length is outside the u32 range",_loc);
+ if( _count>=0 && (!len.isConstant() || len.value()!=_count) )
+ return Parser.error("Array initializer has "+_count+" elements; length must equal that constant",_loc);
+ for( int i=4; i<nIns(); i++ )
+ if( TypeMemPtr.mayBePartial(in(i)._type) )
+ return Parser.error("Array initializer requires fully initialized elements",_loc);
+ return null;
+ }
+ @Override public boolean eq(Node n) { return this==n; }
+}
diff --git a/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java b/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java
index 130b631c..b49163e7 100644
--- a/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java
@@ -261,15 +261,35 @@ public class BulkMemPhiNode extends PhiNode {
mphi = new MemPhiNode(ScopeNode.memName(alias),alias);
mphi.addDef(region());
// Due to cycles, must set before calling peephole
- // Splitting preserves the bulk Phi's established escape bound. Starting
- // at BOT would temporarily invent escapes when the new slice is merged.
+ // An escaped slice must preserve the bulk Phi's established bound.
+ // An unescaped slice was excluded from that summary: include the
+ // facts hidden in predecessor slices before wiring any cyclic Phis.
TypeMem old = (TypeMem)_type;
+ if( !XInt.bit(old._escAs,alias) )
+ old = (TypeMem)old.meet(sliceBound(this,alias,new BitSet()));
mphi.setType(TypeMem.make(alias,old._t,old._one,old._clz,false,old._escFs,old._escAs));
for( int i=1; i<nIns(); i++ )
mphi.addDef(CodeGen.CODE.add(preciseInput(in(i),alias)));
return CodeGen.CODE.add(mphi);
}
+ // A bulk summary ignores unescaped slots in MemMerge inputs. Follow the
+ // same slots that splitting will expose, including across loop backedges.
+ // A visited cycle contributes TOP; its incoming definitions supply the bound.
+ private static TypeMem sliceBound(Node mem, int alias, BitSet visit) {
+ if( visit.get(mem._nid) ) return TypeMem.TOP;
+ visit.set(mem._nid);
+ if( mem instanceof MemMergeNode merge )
+ return sliceBound(merge.alias(alias),alias,visit);
+ if( mem instanceof BulkMemPhiNode bulk ) {
+ TypeMem bound = TypeMem.TOP;
+ for( int i = 1; i < bulk.nIns(); i++ )
+ bound = (TypeMem)bound.meet(sliceBound(bulk.in(i),alias,visit));
+ return bound;
+ }
+ return mem._type instanceof TypeMem bound ? bound : TypeMem.BOT;
+ }
+
MemPhiNode _findPhi(int alias) {
for( Node use : region().outs() )
if( use instanceof MemPhiNode mphi && mphi._alias==alias )
diff --git a/src/main/java/com/seaofnodes/simple/node/CallEndNode.java b/src/main/java/com/seaofnodes/simple/node/CallEndNode.java
index b5156c2d..108dd024 100644
--- a/src/main/java/com/seaofnodes/simple/node/CallEndNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/CallEndNode.java
@@ -85,6 +85,8 @@ public class CallEndNode extends CFGNode implements MultiNode {
return ftype.isHigh() ? TypeTuple.STATE.dual() : TypeTuple.STATE;
// Mid-fold, just take the inlined callee's return.
if( _folding ) return in(1)._type;
+ TypeTuple intrinsic=Intrinsic.compute(call,this);
+ if( intrinsic!=null ) return intrinsic;
// The outside continuation reads Start directly: Call can remain
// CONTROL while its possible returned identities change.
@@ -135,6 +137,8 @@ public class CallEndNode extends CFGNode implements MultiNode {
}
public byte maybeInline() {
+ byte intrinsic=Intrinsic.tryInline(this);
+ if( intrinsic>0 ) return intrinsic;
byte inline = inlineCandidate(false);
if( CodeGen.CODE._midAssert )
return inline;
diff --git a/src/main/java/com/seaofnodes/simple/node/CallNode.java b/src/main/java/com/seaofnodes/simple/node/CallNode.java
index ac5e4eee..9e9c5224 100644
--- a/src/main/java/com/seaofnodes/simple/node/CallNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/CallNode.java
@@ -97,6 +97,7 @@ public class CallNode extends CFGNode {
if( _type!=Type.CONTROL ) return TypeMem.BOT;
if( !(fptr()._type instanceof TypeFunPtr) )
return fptr()._type.isHigh() ? TypeMem.START : TypeMem.BOT;
+ if( Intrinsic.pure(this) ) return TypeMem.START;
if( !external() ) return TypeMem.START;
Type input = dep.addDepForwards(mem())._type;
TypeMem escaped = input instanceof TypeMem m ? m : input.isHigh() ? TypeMem.TOP : TypeMem.BOT;
@@ -241,6 +242,8 @@ public class CallNode extends CFGNode {
return Parser.error("Expected a function, but found " + fptr()._type, _loc);
if( !tfp.notNull() )
return Parser.error( "Might be null calling "+tfp, _loc);
+ Parser.ParseException intrinsic=Intrinsic.err(this);
+ if( intrinsic!=null ) return intrinsic;
if( nargs() != tfp.nargs() )
return Parser.error( "Expecting "+(tfp.nargs()-1)+" arguments, but found "+(nargs()-1), _loc);
@@ -253,6 +256,15 @@ public class CallNode extends CFGNode {
// Constructors return private memory and carry hidden self and
// selfMem arguments ahead of the source-level arguments.
int argno = i - (tfp.ret() instanceof TypeMem ? 2 : 0);
+ // Access qualifiers are not shown by the structural type dump.
+ // Report the differing permission instead of two huge shapes.
+ if( arg(i+2)._type instanceof TypeMemPtr actual && actual._ro &&
+ tfp.arg(i) instanceof TypeMemPtr formal && !formal._ro ) {
+ String who = i == 0 ? name() == null ? "Receiver" : "Receiver for method '"+name()+"'"
+ : "Argument #"+argno;
+ return Parser.error(who+" is read-only "+actual._obj.str()+
+ "; expected writable "+formal._obj.str(),_loc);
+ }
return Parser.error( "Argument #" + argno + " isa " + arg(i+2)._type + ", but must be a " + tfp.arg(i), _loc );
}
diff --git a/src/main/java/com/seaofnodes/simple/node/EscapeNode.java b/src/main/java/com/seaofnodes/simple/node/EscapeNode.java
index 57340609..a8a6151c 100644
--- a/src/main/java/com/seaofnodes/simple/node/EscapeNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/EscapeNode.java
@@ -64,6 +64,10 @@ public class EscapeNode extends TypeNode {
@Override
public Type compute() {
+ // A static constant array has no private heap writes to publish.
+ // This depends on its type, regardless of which node produced it.
+ if( self()!=null && self()._type instanceof TypeMemPtr ptr &&
+ ptr.isConstant() && ptr._obj.isConAry() ) return pub()._type;
if( priv()._type.isHigh() ) return TypeMem.TOP;
if( pub ()._type.isHigh() ) return TypeMem.TOP;
TypeMem mpriv = (TypeMem)priv()._type;
@@ -100,6 +104,8 @@ public class EscapeNode extends TypeNode {
}
@Override public Node idealize() {
+ if( self()._type instanceof TypeMemPtr ptr && ptr.isConstant() && ptr._obj.isConAry() )
+ return pub();
// Sharpen private memory input
if( priv() instanceof MemMergeNode mmm ) {
setDef(2,mmm.alias(fld()._alias));
diff --git a/src/main/java/com/seaofnodes/simple/node/Intrinsic.java b/src/main/java/com/seaofnodes/simple/node/Intrinsic.java
new file mode 100644
index 00000000..82626507
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/node/Intrinsic.java
@@ -0,0 +1,119 @@
+package com.seaofnodes.simple.node;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.parser.Parser;
+import com.seaofnodes.simple.type.*;
+
+// Fixed-semantics C functions. Recognition, effects and expansion live here;
+// Call/CallEnd only delegate. These symbols also have ordinary C definitions
+// for calls whose target is not known at compile time.
+public final class Intrinsic {
+ private Intrinsic() {}
+
+ private enum Op {
+ F64_BITS("simple_f64_bits",TypeFloat.F64,TypeInteger.BOT,true),
+ F64_FROM_BITS("simple_f64_from_bits",TypeInteger.BOT,TypeFloat.F64,true);
+
+ final String symbol;
+ final Type arg, ret;
+ final boolean pure;
+ Op(String symbol, Type arg, Type ret, boolean pure) {
+ this.symbol=symbol; this.arg=arg; this.ret=ret; this.pure=pure;
+ }
+ boolean signature(TypeFunPtr fp) {
+ // Slot zero is the implicit receiver, omitted by the C ABI.
+ return fp.nargs()==2 && fp.arg(1)==arg && fp.ret()==ret;
+ }
+ Expansion expand(CallNode call) {
+ Node value=switch(this) {
+ case F64_BITS -> new F64BitsNode(call.arg(3)).peephole();
+ case F64_FROM_BITS -> new F64FromBitsNode(call.arg(3)).peephole();
+ };
+ return new Expansion(call.ctrl(),call.mem(),value);
+ }
+ }
+
+ private static Op lookup(CallNode call) {
+ if( !(call.fptr()._type instanceof TypeFunPtr fp) || !fp.notNull() || !fp.isConstant() ) return null;
+ String symbol=CodeGen.CODE.externFunc(fp.fidx());
+ if( symbol==null ) return null; // A Simple function with this name is ordinary code.
+ for( Op op : Op.values() ) if( op.symbol.equals(symbol) ) return op;
+ return null;
+ }
+
+ public static Parser.ParseException err(CallNode call) {
+ Op op=lookup(call);
+ return op==null || op.signature(call.tfp()) ? null : Parser.error(
+ "Intrinsic '"+op.symbol+"' requires { "+op.arg.str()+" -> "+op.ret.str()+" }",call._loc);
+ }
+
+ // Both initial operations are pure and expose no references. Future atomic
+ // operations need their own memory/escape semantics, not this passthrough.
+ public static boolean pure(CallNode call) {
+ Op op=lookup(call);
+ return op!=null && op.pure && op.signature(call.tfp());
+ }
+
+ public static TypeTuple compute(CallNode call, Node dep) {
+ if( !pure(call) ) return null;
+ return TypeTuple.make(Type.CONTROL,dep.addDep(call.mem())._type,call.tfp().ret());
+ }
+
+ // An expansion replaces the complete call state, even for a pure value op.
+ private record Expansion(Node control, Node memory, Node value) {
+ void replace(CallEndNode end) {
+ Node[] state={control,memory,value};
+ for( Node n : state ) n.keep();
+ end.keep();
+ for( Node use : end._outputs.asAry() ) {
+ if( use==null ) continue; // Our temporary keep.
+ assert use instanceof Proj;
+ use.subsume(state[((Proj)use).idx()]);
+ }
+ end.unkill();
+ for( Node n : state ) n.unkill();
+ CodeGen.CODE.invalidateIDepthCaches();
+ }
+ }
+
+ // Same cheap-inlining worklist as ordinary functions, but no body/Return,
+ // cloning, size heuristic, or import loading is needed.
+ public static byte tryInline(CallEndNode end) {
+ if( end.folding() || !(end.in(0) instanceof CallNode call) ) return 0;
+ Op op=lookup(call);
+ if( op==null || !op.signature(call.tfp()) || call.nargs()!=call.tfp().nargs() ||
+ !end.goodArgs(call,call.tfp()) ) return 0;
+ if( !CodeGen.CODE._midAssert ) op.expand(call).replace(end);
+ return 1;
+ }
+
+ public abstract static class BitCastNode extends Node {
+ BitCastNode(Node value) { super(null,value); }
+ public abstract boolean toBits();
+ @Override public Type compute() {
+ Type input=in(1)._type;
+ Type result=toBits() ? TypeInteger.BOT : TypeFloat.F64;
+ if( input.isHigh() ) return result.dual();
+ if( toBits() && input instanceof TypeFloat f && f.isConstant() )
+ return TypeInteger.constant(Double.doubleToRawLongBits(f.value()));
+ if( !toBits() && input instanceof TypeInteger i && i.isConstant() )
+ return TypeFloat.constant(Double.longBitsToDouble(i.value()));
+ return result;
+ }
+ @Override public Node idealize() {
+ return in(1) instanceof BitCastNode cast && cast.toBits()!=toBits() ? cast.in(1) : null;
+ }
+ }
+ public static final class F64BitsNode extends BitCastNode {
+ public F64BitsNode(Node value) { super(value); }
+ @Override public boolean toBits() { return true; }
+ @Override public Tag serialTag() { return Tag.F64Bits; }
+ @Override protected String format() { return "f64Bits(%1)"; }
+ }
+ public static final class F64FromBitsNode extends BitCastNode {
+ public F64FromBitsNode(Node value) { super(value); }
+ @Override public boolean toBits() { return false; }
+ @Override public Tag serialTag() { return Tag.F64FromBits; }
+ @Override protected String format() { return "f64FromBits(%1)"; }
+ }
+}
diff --git a/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/src/main/java/com/seaofnodes/simple/node/LoadNode.java
index eb78ed1f..14efa315 100644
--- a/src/main/java/com/seaofnodes/simple/node/LoadNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/LoadNode.java
@@ -98,7 +98,15 @@ public class LoadNode extends MemOpNode {
t = mem._alias==_alias ? mem._t : actual==null ? TypeScalar.BOT : actual._t;
} else if( mem._alias==_alias )
t = t.join(mem._t);
- if( !mem._one ) t = mem.outside(t);
+ // A final method's identity comes from its declaration, not from an
+ // unknown outside writer. It remains callable on a local object even
+ // when that function has never escaped to outside code.
+ if( !mem._one ) {
+ Type declared = Parser.TYPES.get(tmp._obj._name);
+ Field field = declared instanceof TypeStruct obj ? obj.field(_name) : null;
+ if( !(field != null && field._final && field._t instanceof TypeFunPtr fun && fun.isConstant()) )
+ t = mem.outside(t);
+ }
// Store forwarding is also a value fact during SCCP. Waiting for the
// later rewrite can turn a known non-null stored pointer into a
// nullable declaration, then BOTTOM at the next dereference.
@@ -364,15 +372,13 @@ public class LoadNode extends MemOpNode {
if( ptr instanceof PhiNode pphi && pphi.region() == r )
return true;
- // No, so can we lift this ptr?
- CFGNode cptr = ptr.cfg0();
- if( cptr != null )
- // Pointer is controlled high
- // TODO: Really needs to be the LCA of all inputs is high
- return cptr.idepth() <= r.idepth();
-
- // Dunno without a longer walk
- return false;
+ // Each copied Load needs its pointer before the Region, including
+ // a loop's entry arm. input 0 alone misses floating Loads whose own
+ // pointer was produced inside the loop.
+ CFGNode pinned = ptr.cfg0();
+ if( pinned==null || pinned.idepth()>r.idepth() ) return false;
+ CFGNode cptr = ptr instanceof PhiNode phi ? phi.region() : CFGNode.earlyCFG(ptr);
+ return cptr==null || cptr.dominates(r.idom(memphi),memphi);
}
@Override public Parser.ParseException err() {
diff --git a/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java b/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java
index aa3f6d57..19420839 100644
--- a/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java
@@ -20,8 +20,20 @@ public class MemMergeNode extends Node {
public MemMergeNode( Node ...nodes) { super(nodes); _type = TypeMem.BOT; }
public MemMergeNode(MemMergeNode mem) { super(mem); }
@Override public Tag serialTag() { return Tag.MemMerge; }
- public void packed( BAOS baos, HashMap<String,Integer> strs, HashMap<Type,Integer> types, IdentityHashMap<Node, Integer> anodes ) { baos.packed1(nIns()); }
- static Node make( BAOS bais ) { Node mem = new MemMergeNode(); mem.setDefX(bais.packed1()-1,null); return mem; }
+ public void packed( BAOS baos, HashMap<String,Integer> strs, HashMap<Type,Integer> types, IdentityHashMap<Node, Integer> anodes ) {
+ // A memory merge always has inputs. Zero escapes a wide alias table,
+ // preserving every existing nonzero single-byte count.
+ assert nIns()>0;
+ baos.packed1(nIns()<=255 ? nIns() : 0);
+ if( nIns()>255 ) baos.packed4(nIns());
+ }
+ static Node make( BAOS bais ) {
+ int count=bais.packed1();
+ if( count==0 ) count=bais.packed4();
+ Node mem=new MemMergeNode();
+ mem.setDefX(count-1,null);
+ return mem;
+ }
@Override public String label() { return "ALLMEM"; }
@Override public boolean isMem() { return true; }
diff --git a/src/main/java/com/seaofnodes/simple/node/MemOpNode.java b/src/main/java/com/seaofnodes/simple/node/MemOpNode.java
index 35183380..bce11b50 100644
--- a/src/main/java/com/seaofnodes/simple/node/MemOpNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/MemOpNode.java
@@ -94,7 +94,8 @@ public abstract class MemOpNode extends TypeNode {
if( !(ptr()._type instanceof TypeMemPtr tmp) )
return Type.BOTTOM;
Field fld = tmp._obj.field(_name);
- return fld == null || fld._t == null ? Type.BOTTOM : fld._t;
+ return fld == null || fld._t == null ? Type.BOTTOM :
+ fld._t instanceof TypeConAry ary ? ary.storageType() : fld._t;
}
@Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return _printMach(p); }
diff --git a/src/main/java/com/seaofnodes/simple/node/Node.java b/src/main/java/com/seaofnodes/simple/node/Node.java
index 2815dfba..d4c0794c 100644
--- a/src/main/java/com/seaofnodes/simple/node/Node.java
+++ b/src/main/java/com/seaofnodes/simple/node/Node.java
@@ -77,7 +77,7 @@ public abstract class Node extends BaseNode<Node> implements Cloneable {
New,Never,Not,Or,Parm,Phi,Proj,PtrToInt,
ReadOnly,Return,Region,RoundF32,
Sar,Shl,Shr,Start,StartCU,StopCU,Stop,Store,Sub,
- ToInteger,ToFloat,XCtrl,Xor;
+ ToInteger,ToFloat,XCtrl,Xor,F64Bits,F64FromBits;
public static final Tag[] VALS = values();
public Node make( BAOS bais, String[] strs, Type[] types, GlobalBits fileAliases, GlobalBits aliases ) {
return switch(this) {
@@ -103,6 +103,8 @@ public abstract class Node extends BaseNode<Node> implements Cloneable {
case Sub -> new SubNode(null,null,(byte)bais.packed1());
case ToFloat-> new ToFloatNode(null);
case ToInteger-> new ToIntegerNode(null);
+ case F64Bits-> new Intrinsic.F64BitsNode(null);
+ case F64FromBits-> new Intrinsic.F64FromBitsNode(null);
case XCtrl -> new XCtrlNode();
case Xor -> { int mode=bais.packed1(); assert mode==1; yield new XorNode(null,null,null); }
diff --git a/src/main/java/com/seaofnodes/simple/node/StoreNode.java b/src/main/java/com/seaofnodes/simple/node/StoreNode.java
index 010d5f61..204e0c4b 100644
--- a/src/main/java/com/seaofnodes/simple/node/StoreNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/StoreNode.java
@@ -243,6 +243,10 @@ public class StoreNode extends MemOpNode {
private byte storageType() {
if( !(ptr()._type instanceof TypeMemPtr tmp) )
return 0;
+ // Completing a forward declaration changes TYPES without changing
+ // this Store's inputs. Wait for the pointer's type upgrade, which
+ // queues its users, before consulting the completed declaration.
+ if( tmp._obj._fref ) return 0;
TypeStruct obj = (TypeStruct)Parser.TYPES.get(tmp._obj._name);
Field fld = obj.field(_name);
if( fld == null || fld._t == null )
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/BitCastARM.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/BitCastARM.java
new file mode 100644
index 00000000..ec57b3f8
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/BitCastARM.java
@@ -0,0 +1,23 @@
+package com.seaofnodes.simple.node.cpus.arm;
+
+import com.seaofnodes.isa.Arm64;
+import com.seaofnodes.simple.codegen.*;
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.util.SB;
+
+// A representation move, with no arithmetic, rounding, or flag changes.
+public class BitCastARM extends MachConcreteNode implements MachNode {
+ private final boolean _toBits;
+ BitCastARM(Intrinsic.BitCastNode cast) { super(cast); _toBits=cast.toBits(); }
+ @Override public String op() { return _toBits ? "f64bits" : "f64frombits"; }
+ @Override public RegMask regmap(int i) { assert i==1; return _toBits ? arm.DMASK : arm.RMASK; }
+ @Override public RegMask outregmap() { return _toBits ? arm.WMASK : arm.DMASK; }
+ @Override public void encoding(Encoding enc) {
+ enc.add4(Arm64.f_mov_general(Arm64.OP_FMOV,0b01,0,_toBits ? 0b110 : 0b111,
+ enc.reg(in(1))-(_toBits ? arm.D_OFFSET : 0),
+ enc.reg(this)-(_toBits ? 0 : arm.D_OFFSET)));
+ }
+ @Override public void asm(CodeGen code, SB sb) {
+ sb.p(code.reg(this)).p(" = bits ").p(code.reg(in(1)));
+ }
+}
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java
index ee7b0fc3..2d47edf5 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java
@@ -350,6 +350,7 @@ public class arm extends Machine {
case StoreNode st -> st(st);
case SubNode sub -> sub.mode()==2 ? new SubFARM(sub) : sub(sub);
case ToFloatNode tfn -> new I2F8ARM(tfn);
+ case Intrinsic.BitCastNode cast -> new BitCastARM(cast);
case ToIntegerNode cvt -> new F8ToIARM(cvt);
case XorNode xor -> xor(xor);
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/BitCastRISC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/BitCastRISC.java
new file mode 100644
index 00000000..141ff7d5
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/BitCastRISC.java
@@ -0,0 +1,23 @@
+package com.seaofnodes.simple.node.cpus.riscv;
+
+import com.seaofnodes.isa.RiscV;
+import com.seaofnodes.simple.codegen.*;
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.util.SB;
+
+// A representation move, with no arithmetic, rounding, or flag changes.
+public class BitCastRISC extends MachConcreteNode implements MachNode {
+ private final boolean _toBits;
+ BitCastRISC(Intrinsic.BitCastNode cast) { super(cast); _toBits=cast.toBits(); }
+ @Override public String op() { return _toBits ? "f64bits" : "f64frombits"; }
+ @Override public RegMask regmap(int i) { assert i==1; return _toBits ? riscv.FMASK : riscv.RMASK; }
+ @Override public RegMask outregmap() { return _toBits ? riscv.WMASK : riscv.FMASK; }
+ @Override public void encoding(Encoding enc) {
+ enc.add4(RiscV.r_type(RiscV.OP_FP,
+ enc.reg(this)-(_toBits ? 0 : riscv.F_OFFSET),0,
+ enc.reg(in(1))-(_toBits ? riscv.F_OFFSET : 0),0,_toBits ? 0b1110001 : 0b1111001));
+ }
+ @Override public void asm(CodeGen code, SB sb) {
+ sb.p(code.reg(this)).p(" = bits ").p(code.reg(in(1)));
+ }
+}
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java
index d455337b..de754393 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java
@@ -47,7 +47,7 @@ public class SplitRISC extends SplitNode {
// FPR->FPR
src -= riscv.F_OFFSET;
dst -= riscv.F_OFFSET;
- enc.add4(RiscV.r_type(RiscV.OP_FP, dst, 0b000, src, src, 0b0010101)); // dst = FMIN(src,src)
+ enc.add4(RiscV.r_type(RiscV.OP_FP, dst, 0b000, src, src, 0b0010001)); // FSGNJ.D preserves NaN bits.
} else if(!srcX && dstX) {
//GPR->FPR
// fmv.d.x
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java
index 378955a5..976d1a52 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java
@@ -254,6 +254,7 @@ public class riscv extends Machine {
case StoreNode st -> st(st);
case SubNode sub -> sub.mode()==2 ? new SubFRISC(sub) : sub(sub);
case ToFloatNode tfn -> i2f8(tfn);
+ case Intrinsic.BitCastNode cast -> new BitCastRISC(cast);
case ToIntegerNode cvt -> new F8ToIRISC(cvt);
case XorNode xor -> xor(xor);
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/BitCastX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/BitCastX86.java
new file mode 100644
index 00000000..bf550032
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/BitCastX86.java
@@ -0,0 +1,23 @@
+package com.seaofnodes.simple.node.cpus.x86_64_v2;
+
+import com.seaofnodes.isa.X86;
+import com.seaofnodes.simple.codegen.*;
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.util.SB;
+
+// A representation move, with no arithmetic, rounding, or flag changes.
+public class BitCastX86 extends MachConcreteNode implements MachNode {
+ private final boolean _toBits;
+ BitCastX86(Intrinsic.BitCastNode cast) { super(cast); _toBits=cast.toBits(); }
+ @Override public String op() { return _toBits ? "f64bits" : "f64frombits"; }
+ @Override public RegMask regmap(int i) { assert i==1; return _toBits ? x86_64_v2.XMASK : x86_64_v2.RMASK; }
+ @Override public RegMask outregmap() { return _toBits ? x86_64_v2.WMASK : x86_64_v2.XMASK; }
+ @Override public void encoding(Encoding enc) {
+ X86.move(enc,!_toBits,_toBits,
+ enc.reg(this)-(_toBits ? 0 : x86_64_v2.XMM_OFFSET),
+ enc.reg(in(1))-(_toBits ? x86_64_v2.XMM_OFFSET : 0));
+ }
+ @Override public void asm(CodeGen code, SB sb) {
+ sb.p(code.reg(this)).p(" = bits ").p(code.reg(in(1)));
+ }
+}
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java
index 10cee059..ae725659 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java
@@ -9,6 +9,7 @@ import com.seaofnodes.simple.util.Utils;
// Jump on flags, uses flags
public class JmpX86 extends IfNode implements MachNode, RIPRelSize {
String _bop;
+ boolean _unsigned, _floating, _unorderedTaken;
JmpX86( IfNode iff, String bop ) {
super(iff);
_bop = bop;
@@ -23,29 +24,47 @@ public class JmpX86 extends IfNode implements MachNode, RIPRelSize {
if( set instanceof SetX86 setx && (cmp instanceof CmpX86 || cmp instanceof CmpIX86 || cmp instanceof CmpMemX86 || cmp instanceof CmpFX86) ) {
_inputs.set( 1, cmp );
_bop = setx._bop;
+ _unsigned = setx._unsigned;
+ _floating = cmp instanceof CmpFX86;
+ _unorderedTaken = _bop.equals("!=");
} else
throw Utils.TODO();
}
@Override public RegMask regmap(int i) { assert i==1; return x86_64_v2.FLAGS_MASK; }
@Override public RegMask outregmap() { return null; }
- @Override public void negate() { _bop = negate(_bop); }
+ @Override public void negate() {
+ _bop = negate(_bop);
+ if( _floating ) _unorderedTaken = !_unorderedTaken;
+ }
+
+ private int condition() { return x86_64_v2.jumpop(_bop,_unsigned)&15; }
@Override public void encoding( Encoding enc ) {
if( in(1)==null && _bop=="!=" ) return;
- X86.branch(enc,in(1)==null ? -1 : x86_64_v2.jumpop(_bop)&15);
+ // UCOMISD sets CF/ZF/PF, not signed-integer SF/OF. Handle unordered
+ // separately, including when layout has negated the original test.
+ if( _floating ) {
+ if( _unorderedTaken ) enc.add1(0x0F).add1(0x8A).add4(0); // JP target
+ else enc.add1(0x7A).add1(0); // JP past the ordered jump
+ }
+ X86.branch(enc,in(1)==null ? -1 : condition());
enc.jump(this,cproj(0));
}
// Delta is from opcode start, but X86 measures from the end of the 2-byte encoding
@Override public byte encSize(int delta) {
if( in(1)==null && _bop=="!=" ) return 0;
- return X86.branchSize(delta,in(1)!=null);
+ int prefix = !_floating ? 0 : _unorderedTaken ? 6 : 2;
+ return (byte)(prefix + X86.branchSize(delta-prefix,in(1)!=null));
}
// Delta is from opcode start
@Override public void patch( Encoding enc, int opStart, int opLen, int delta ) {
assert !(in(1)==null && _bop=="!=");
- X86.patchBranch(enc.bits(),opStart,opLen,delta,in(1)==null ? -1 : x86_64_v2.jumpop(_bop)&15);
+ int prefix = !_floating ? 0 : _unorderedTaken ? 6 : 2;
+ if( _floating )
+ X86.patchBranch(enc.bits(),opStart,prefix,_unorderedTaken ? delta : opLen,10);
+ X86.patchBranch(enc.bits(),opStart+prefix,opLen-prefix,delta-prefix,in(1)==null ? -1 : condition());
}
@Override public void asm(CodeGen code, SB sb) {
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java
index fcca7f8f..b0ecd685 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java
@@ -67,9 +67,9 @@ public abstract class MemOpX86 extends MemOpNode implements MachNode {
if( i==1 ) return null; // Memory
if( i==2 ) return x86_64_v2.RMASK; // base in GPR
if( i==3 ) return x86_64_v2.RMASK; // index in GPR
- if( i==4 ) return _sz >= '4'
- ? x86_64_v2.MEM_MASK // value in GPR or XMM
- : x86_64_v2.RMASK; // Bytes and shorts in GPR only
+ // Integer arithmetic and comparisons require a GPR value operand.
+ // Only StoreX86 has alternate encodings that accept XMM values.
+ if( i==4 ) return x86_64_v2.RMASK;
return null; // Anti-dependence
}
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegFX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegFX86.java
new file mode 100644
index 00000000..61662fd2
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegFX86.java
@@ -0,0 +1,25 @@
+package com.seaofnodes.simple.node.cpus.x86_64_v2;
+
+import com.seaofnodes.isa.X86;
+import com.seaofnodes.simple.codegen.*;
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.type.TypeFloat;
+import com.seaofnodes.simple.util.SB;
+
+// Flip the sign bit, preserving signed zero, infinities, and NaN payloads.
+public class NegFX86 extends MachConcreteNode implements MachNode {
+ NegFX86(MinusNode neg) {
+ super(neg);
+ addDef(new FltX86(new ConstantNode(TypeFloat.constant(-0.0)),null));
+ }
+ @Override public String op() { return "negf"; }
+ @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.XMASK; }
+ @Override public RegMask outregmap() { return x86_64_v2.XMASK; }
+ @Override public int twoAddress() { return 1; }
+ @Override public void encoding(Encoding enc) {
+ X86.sse(enc,0x66,0x0F57,false,enc.reg(this)-x86_64_v2.XMM_OFFSET,enc.reg(in(2))-x86_64_v2.XMM_OFFSET);
+ }
+ @Override public void asm(CodeGen code, SB sb) {
+ sb.p(code.reg(this)).p(" ^= ").p(code.reg(in(2))).p(" // sign bit");
+ }
+}
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java
index 86610f6d..4494985d 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java
@@ -9,7 +9,7 @@ public class NegX86 extends MachConcreteNode implements MachNode {
NegX86(MinusNode not) { super(not); }
@Override public String op() { return "neg"; }
@Override public RegMask regmap(int i) { return x86_64_v2.RMASK; }
- @Override public RegMask outregmap() { return x86_64_v2.RMASK; }
+ @Override public RegMask outregmap() { return x86_64_v2.WMASK; }
@Override public RegMask killmap() { return x86_64_v2.FLAGS_MASK; }
@Override public int twoAddress() { return 1; }
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java
index 1e4e7c26..1291cf78 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java
@@ -9,7 +9,7 @@ public class NotX86 extends MachConcreteNode implements MachNode {
NotX86(NotNode not) { super(not); }
@Override public String op() { return "not"; }
@Override public RegMask regmap(int i) { return x86_64_v2.RMASK; }
- @Override public RegMask outregmap() { return x86_64_v2.RMASK; }
+ @Override public RegMask outregmap() { return x86_64_v2.WMASK; }
@Override public RegMask killmap() { return x86_64_v2.FLAGS_MASK; }
@Override public void encoding( Encoding enc ) {
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java
index 864c1b55..bcc4880b 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java
@@ -4,7 +4,6 @@ import com.seaofnodes.isa.X86;
import com.seaofnodes.simple.codegen.*;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.util.SB;
-import com.seaofnodes.simple.util.Utils;
// Corresponds to the x86 instruction "sete && setne".
// Use result of comparison without jump.
@@ -25,15 +24,14 @@ public class SetX86 extends MachConcreteNode implements MachNode {
@Override public RegMask outregmap() { return x86_64_v2.WMASK; }
@Override public void encoding( Encoding enc ) {
- int op=switch (_bop) {
- case "==" -> 0x94; // SETE
- case "<" -> _unsigned ? 0x92 : 0x9C; // SETB /SETL
- case "<=" -> _unsigned ? 0x96 : 0x9E; // SETBE/SETLE
- case ">=" -> _unsigned ? 0x93 : 0x9D; // SETAE/SETGE
- case ">" -> _unsigned ? 0x97 : 0x9F; // SETA /SETG
- default -> throw Utils.TODO();
- };
- X86.set(enc,op&15,enc.reg(this));
+ int dst=enc.reg(this);
+ if( in(1) instanceof CmpFX86 ) {
+ // MOV preserves comparison flags; unordered is true only for !=.
+ X86.unary(enc,0xC7,0,dst);
+ enc.add4(_bop.equals("!=") ? 1 : 0);
+ enc.add1(0x7A).add1(dst>=4 ? 8 : 6); // JP past SETcc/MOVZX
+ }
+ X86.set(enc,x86_64_v2.jumpop(_bop,_unsigned)&15,dst);
}
@Override public void asm(CodeGen code, SB sb) {
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java
index 9ffa25fc..8dfd31e7 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java
@@ -25,6 +25,9 @@ public class StoreX86 extends MemOpX86 {
}
// Register mask allowed as a result. 0 for no register.
@Override public RegMask outregmap() { return null; }
+ @Override public RegMask regmap(int i) {
+ return i==4 && _sz>='4' ? x86_64_v2.MEM_MASK : super.regmap(i);
+ }
@Override public void encoding( Encoding enc ) {
short ptr=enc.reg(ptr()), idx=enc.reg(idx()), src=enc.reg(val());
if( src==-1 ) X86.storeImm(enc,Integer.numberOfTrailingZeros(_sz-'0'),_imm,ptr,idx,_off,_scale);
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java
index 3d71b4ea..dc15b185 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java
@@ -87,6 +87,18 @@ public class x86_64_v2 extends Machine {
default -> throw new IllegalArgumentException("Too many arguments");
};
}
+ static int jumpop(String op, boolean unsigned) {
+ if( !unsigned ) return jumpop(op);
+ return switch( op ) {
+ case "==" -> 0x84;
+ case "!=" -> 0x85;
+ case "<" -> 0x82;
+ case "<=" -> 0x86;
+ case ">" -> 0x87;
+ case ">=" -> 0x83;
+ default -> throw new IllegalArgumentException(op);
+ };
+ }
// Limit of float args passed in registers
static RegMask[] XMMS8 = new RegMask[]{
@@ -282,6 +294,7 @@ public class x86_64_v2 extends Machine {
public Node instSelect(Node n) {
return switch (n) {
case AddNode add -> add(add);
+ case Intrinsic.BitCastNode cast -> new BitCastX86(cast);
case AndNode and -> and(and);
case BoolNode bool -> cmp(bool);
case CLInitNode init -> new CLInitNode(init);
@@ -298,10 +311,7 @@ public class x86_64_v2 extends Machine {
case IfNode iff -> jmp(iff);
case LoadNode ld -> ld(ld);
case MemMergeNode mem -> new MemMergeNode(mem);
- case MinusNode neg -> {
- if( neg.mode()==2 ) throw Utils.TODO();
- yield new NegX86(neg);
- }
+ case MinusNode neg -> neg.mode()==2 ? new NegFX86(neg) : new NegX86(neg);
case MulNode mul -> mul.mode()==2 ? new MulFX86(mul) : mul(mul);
case NewNode nnn -> new NewX86(nnn);
case NotNode not -> new NotX86(not);
@@ -370,8 +380,9 @@ public class x86_64_v2 extends Machine {
private Node _lea(Node add, Node base, Node idx, int off) {
int scale = 0;
- if( base instanceof ShlNode && !(idx instanceof ShlNode) )
- throw Utils.TODO(); // Bug in canonicalization, should on RHS
+ if( base instanceof ShlNode && !(idx instanceof ShlNode) ) {
+ Node tmp = base; base = idx; idx = tmp;
+ }
if( idx instanceof ShlNode shift && shift.in(2) instanceof ConstantNode shfcon &&
shfcon._con instanceof TypeInteger tscale && 0 <= tscale.value() && tscale.value() <= 3 ) {
idx = shift.in(1);
diff --git a/src/main/java/com/seaofnodes/simple/parser/Allocations.java b/src/main/java/com/seaofnodes/simple/parser/Allocations.java
index 861f95b6..64234045 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Allocations.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Allocations.java
@@ -25,11 +25,20 @@ final class Allocations {
}
// Parse ary[ length_expr ]
if( P.match("[") ) {
- if( !t.makeZero().isa(t) )
- throw Parser.error("Cannot allocate a non-nullable, since arrays are always zero/null filled",site);
+ boolean readonly = P.match("~");
+ if( P.match("]") ) {
+ P.require("{");
+ return P.arrays.parse(t,readonly,null,site);
+ }
Node len = P.expressions.parseAssignment().keep();
P.require("]");
+ if( P.match("{") ) return P.arrays.parse(t,readonly,len,site);
+ if( !t.makeZero().isa(t) )
+ throw Parser.error("Cannot allocate a non-nullable, since arrays are always zero/null filled",site);
+ if( readonly ) return new ReadOnlyNode(allocArray(P.types.typeAry(t,true),len)).peephole();
return allocArray(P.types.typeAry(t,false),len);
+ } else if( t instanceof TypeMemPtr ary && ary._obj.isAry() && P.match("{") ) {
+ return P.arrays.parse(ary._obj.field("[]")._t,ary._ro,null,site);
} else {
if( !(t instanceof TypeMemPtr tmp) )
throw Parser.error("Cannot allocate a "+t.str(),site);
@@ -101,8 +110,6 @@ final class Allocations {
// TODO: Needs runtime check.
Node st = P.peep(new StoreNode(null, "#", 1, TypeInteger.U32, null, smem, self, ConFldOffNode.make(ts,"#"), len.unkeep(), true));
- // TODO: Allow add-on "constructor" to init the array elements
-
Node prior = P.mem();
Node esc = P.peep(new EscapeNode(lenFld,self,st,prior));
P.mem(P.mergeAlias(prior,lenFld._alias,esc));
@@ -117,7 +124,7 @@ final class Allocations {
TypeStruct ts = P.types.typeAry(TypeInteger.U8,true);
int lenAlias = ts.field("#" )._alias;
int elemAlias = ts.field("[]")._alias;
- TypeConAryB body = TypeConAryB.make(s);
+ TypeConAry body = TypeConAry.make(s);
TypeInteger slen = TypeInteger.constant(s.length);
// Make a TMP, not-null (byte)2, singleton (true) (requires text
// strings are hash-interned), with a TypeStruct having a constant
diff --git a/src/main/java/com/seaofnodes/simple/parser/ArrayInitializers.java b/src/main/java/com/seaofnodes/simple/parser/ArrayInitializers.java
new file mode 100644
index 00000000..079853cd
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/parser/ArrayInitializers.java
@@ -0,0 +1,187 @@
+package com.seaofnodes.simple.parser;
+
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.node.ScopeNode.Kind;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
+
+/** Array construction keeps the destination private until all elements exist.
+ * A generator is a scoped, immediately executed loop body, not an escaping closure. */
+final class ArrayInitializers {
+ private final Parser P;
+ Frame _frame;
+
+ static final class Frame {
+ final Kind.Block _kind;
+ final int _depth;
+ final Var _value;
+ ScopeNode _returns;
+ Frame(Kind.Block kind, int depth, Var value) {
+ _kind=kind; _depth=depth; _value=value;
+ }
+ }
+
+ ArrayInitializers(Parser P) { this.P=P; }
+
+ // Opening brace consumed; an absent length means a list with inferred size.
+ Node parse(Type elem, boolean readonly, Node len, Lexer site) {
+ int pos=P.pos();
+ Type indexType=P.types.parseType();
+ String index=indexType==null ? null : P._lexer.matchId();
+ boolean generator=index!=null && P.match("->");
+ if( !generator ) P.pos(pos);
+ if( generator && Parser.KEYWORDS.contains(index) )
+ throw Parser.error("Expected an array index name",site);
+ if( generator && (indexType!=TypeInteger.BOT && indexType!=TypeInteger.I32 && indexType!=TypeInteger.U32) )
+ throw Parser.error("Array generator index must be int, i64, or u32",site);
+ if( generator && len==null )
+ throw Parser.error("Array generator requires a length",site);
+
+ Ary<Node> values=new Ary<>(Node.class);
+ if( !generator ) {
+ if( !P.peek('}') ) do {
+ values.add(P.conversions.convertExpr(P.expressions.parseAssignment(),elem).keep());
+ } while( P.match(",") && !P.peek('}') );
+ P.require("}");
+ if( len==null ) len=P.con(values.size()).keep();
+ }
+
+ TypeStruct ts=P.types.typeAry(elem,readonly);
+ Field body=ts.field("[]"), length=ts.field("#");
+ Node size=new AddNode(ConFldOffNode.make(ts,"[]"),
+ new ShlNode(null,len,P.con(ts.aryScale())).peephole()).peephole();
+ NewNode alloc=new NewNode(ts,P.ctrl(),size).init();
+ Node self=new ProjNode(alloc,0,ts.str()).init().keep();
+ Node mem=new ProjNode(alloc,1,"#arrayMem").init().keep();
+ Node slen=new StoreNode(site,"#",length._alias,TypeInteger.U32,null,mem,self,
+ ConFldOffNode.make(ts,"#"),len,true).peephole().keep();
+
+ if( generator ) {
+ mem=generator(ts,self,mem,len,index,indexType,site);
+ } else {
+ for( int i=0; i<values.size(); i++ ) {
+ Node st=store(ts,self,mem,P.con(i),values.at(i),site).keep();
+ mem.unkeep();
+ mem=st;
+ }
+ }
+
+ Node result=new ArrayInitNode(P.ctrl(),mem,self,len,
+ generator ? -1 : values.size(),site,values).peephole().keep();
+ Node prior=P.mem();
+ P.mem(P.mergeAlias(prior,length._alias,new EscapeNode(length,result,slen.unkeep(),prior).peephole()));
+ prior=P.mem();
+ P.mem(P.mergeAlias(prior,body._alias,new EscapeNode(body,result,mem,prior).peephole()));
+ mem.unkeep(); self.unkeep(); len.unkeep();
+ for( Node value : values ) value.unkeep();
+ result.unkeep();
+ return readonly ? new ReadOnlyNode(result).peephole() : result;
+ }
+
+ private Node store(TypeStruct ts, Node self, Node mem, Node idx, Node value, Lexer site) {
+ Field fld=ts.field("[]");
+ Node off=new AddNode(ConFldOffNode.make(ts,"[]"),
+ new ShlNode(null,idx,P.con(ts.aryScale())).peephole()).peephole();
+ return new StoreNode(site,"[]",fld._alias,fld._t,P.ctrl(),mem,self,off,value,true).peephole();
+ }
+
+ private Var define(String name, Type type, boolean fin, Node value, Lexer site) {
+ if( !P._scope.define(name,type,fin,value,site) ) throw Parser.error("Duplicate name '"+name+"'",site);
+ return P._scope.lookup(name);
+ }
+
+ private Node read(Var var) { return P._scope.in(P._scope.update(var,null)); }
+
+ private Node generator(TypeStruct ts, Node self, Node mem, Node len, String name,
+ Type indexType, Lexer site) {
+ Frame outer=_frame;
+ ScopeNode oldBreak=P.statements._breakScope, oldContinue=P.statements._continueScope;
+ P.statements._breakScope=P.statements._continueScope=null;
+ Kind.Block kind=new Kind.Block();
+ P._scope.push(kind);
+ // The counter stays wide even when the source index is a narrow int.
+ // The private memory and result slots cannot be named in source.
+ Var counter=define("$arrayIndex",TypeInteger.BOT,false,P.con(0),site);
+ Var memory=define("$arrayMem",TypeMem.makePrivate(Type.BOTTOM),false,mem.unkeep(),site);
+ Var result=define("$arrayValue",ts.field("[]")._t,false,P.con(Type.TOP),site);
+ _frame=new Frame(kind,P._scope.depth(),result);
+
+ P.ctrl(new LoopNode(site,null,P.ctrl()).peephole());
+ ScopeNode head=P._scope.keep();
+ P._xScopes.push(P._scope=P._scope.dup(true));
+ Node idx=read(counter).keep();
+ Node pred=new BoolNode.LT(idx,len).peephole().keep();
+ Node iff=new IfNode(P.ctrl(),pred).peephole().keep();
+ CFGNode yes=new CProjNode(iff,0,"True").peephole().keep();
+ CFGNode no=(CFGNode)new CProjNode(iff.unkeep(),1,"False").peephole();
+ P.ctrl(no);
+ ScopeNode exit=P._scope.dup();
+ P._xScopes.push(exit);
+ exit.addGuards(no,pred,true);
+ P.ctrl(yes.unkeep());
+ P._scope.addGuards(yes,pred.unkeep(),false);
+
+ P._scope.push(new Kind.Block());
+ define(name,indexType,true,new ConvertNode(indexType,idx).peephole(),site);
+ Node last=P._code.ZERO;
+ while( !P.peek('}') && !P._lexer.isEOF() ) {
+ P._code.add(last);
+ last=P.statements.parseStatement();
+ }
+ P.require("}");
+ if( P.ctrl()._type!=Type.XCONTROL ) addReturn(last);
+ P._scope.pop();
+ if( _frame._returns!=null ) {
+ P._scope.kill();
+ P._scope=_frame._returns;
+ }
+ Node value=read(result);
+ // The loop starts with the whole private array memory. Keep that shape
+ // on the backedge too: mixing it with an element slice yields BOTTOM,
+ // which would falsely publish every function when the array escapes.
+ Node prior=read(memory).keep();
+ Node st=store(ts,self,prior,idx,value,site);
+ P._scope.update(memory,P.mergeAlias(prior.unkeep(),ts.field("[]")._alias,st));
+ P._scope.update(counter,new AddNode(idx.unkeep(),P.con(1)).peephole());
+ P._scope.removeGuards(yes);
+ head.balanceLoopFRefs(P._scope);
+ exit.balanceLoopFRefs(P._scope);
+ head.endLoop(P._scope,exit);
+ head.unkeep().kill();
+ P._xScopes.pop();
+ P._xScopes.pop();
+ if( !P._xScopes.isEmpty() ) { P._xScopes.pop(); P._xScopes.push(exit); }
+ P._scope=exit;
+ Node done=read(memory).keep();
+ P._scope.pop();
+ _frame=outer;
+ P.statements._breakScope=oldBreak;
+ P.statements._continueScope=oldContinue;
+ return done;
+ }
+
+ Node addReturn(Node expr) {
+ Node value=P.conversions.convertExpr(expr,_frame._value.type());
+ value=new ArrayInitNode(P.ctrl(),value,P.loc()).peephole().keep();
+ P.mem(); // Materialize lazy memory before cloning a return path.
+ ScopeNode cur=P._scope.dup();
+ // Saved paths share Var records with the active scope. Drop locals
+ // without promoting them, then copy forward references explicitly.
+ while( cur.depth()>_frame._depth ) cur._pop();
+ cur.balanceLoopFRefs(P._scope);
+ cur.update(_frame._value,value.unkeep());
+ if( _frame._returns==null ) _frame._returns=cur;
+ else {
+ _frame._returns.balanceLoopFRefs(cur);
+ cur.balanceLoopFRefs(_frame._returns);
+ _frame._returns.ctrl(_frame._returns.mergeScopes(cur,P.loc()).peephole());
+ }
+ P.ctrl(P._code.XCTRL);
+ return P._code.ZERO;
+ }
+
+ void checkAssignment(Var var, Lexer site) {
+ if( _frame!=null && var._idx<_frame._kind._lexSize )
+ throw Parser.error("Cannot assign captured local '"+var._name+"' in an array generator",site);
+ }
+}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Conversions.java b/src/main/java/com/seaofnodes/simple/parser/Conversions.java
index fa11311d..72247ca6 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Conversions.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Conversions.java
@@ -37,7 +37,7 @@ final class Conversions {
case TypeFloat tf -> TypeFloat.F64;
case TypeMemPtr tmp -> tmp.makeVar();
case TypeFunPtr tfp -> tfp;
- case TypeConAry<?> a -> a;
+ case TypeConAry a -> a;
case TypePtr ptr -> TypePtr.PTR;
case TypeScalar ts -> TypeScalar.BOT;
default -> t;
diff --git a/src/main/java/com/seaofnodes/simple/parser/Declarations.java b/src/main/java/com/seaofnodes/simple/parser/Declarations.java
index 13fa59b8..473a7c24 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Declarations.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Declarations.java
@@ -91,6 +91,10 @@ final class Declarations {
// expr is a constant function
if( t instanceof TypeFunPtr decl && expr._type instanceof TypeFunPtr tfp && tfp.isConstant() ) {
t = decl.makeFrom(tfp.fidx());
+ // The explicit signature specifies source parameters/return;
+ // the receiver of a method body is still inferred.
+ if( expr instanceof FunPtrNode && P.structs.inStructDeclaration() )
+ t = ((TypeFunPtr)t).makeFrom(tfp.arg(0),0);
FunNode fun = P._code.link(tfp);
if( fun != null )
fun.setName(name); // Assign debug name to Simple function
diff --git a/src/main/java/com/seaofnodes/simple/parser/Expressions.java b/src/main/java/com/seaofnodes/simple/parser/Expressions.java
index 247d974e..6486aba8 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Expressions.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Expressions.java
@@ -400,7 +400,7 @@ final class Expressions {
* Parse integer literal
*
* <pre>
- * integerLiteral: [1-9][0-9]* | [0]
+ * integerLiteral: [1-9][0-9]* | 0[0-7]* | 0[xX][0-9a-fA-F]+
* floatLiteral: [digits].[digits]?[e [digits]]?[f]?
* </pre>
*/
diff --git a/src/main/java/com/seaofnodes/simple/parser/Functions.java b/src/main/java/com/seaofnodes/simple/parser/Functions.java
index 5175acc6..84bd509e 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Functions.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Functions.java
@@ -14,6 +14,11 @@ final class Functions {
// Merge all function exits here.
ScopeNode _returnScope;
+ // Instance method headers are available immediately; their bodies need
+ // the complete field scope, just like explicitly declared constructors.
+ private record Method(FunPtrNode ptr, Lexer body, Lexer loc, BitSet finals, String[] ids) {}
+ private Ary<Method> _methods;
+
Functions(Parser P) { this.P = P; }
/**
@@ -29,6 +34,7 @@ final class Functions {
Ary<String> ids = new Ary<>(String.class);
BitSet finals = new BitSet();
Type self = P.types.implicitSelfType();
+ if( P.structs.inStructDeclaration() ) self = self.makeRO();
ts.push(self);
ids.push(self == TypePtr.PTR ? "#self" : "self");
@@ -38,10 +44,49 @@ final class Functions {
parameters(ts,ids,finals,false);
// Make a concrete function type, with a fidx
TypeFunPtr tfp = TypeFunPtr.make1((byte)2,true,ts.asAry(),Type.BOTTOM,P._code.fidx(P._ref._cname));
+ if( P.structs.inStructDeclaration() ) {
+ FunPtrNode ptr = (FunPtrNode)new FunPtrNode(tfp,P._code._start,null).peephole().keep();
+ _methods.push(new Method(ptr,P.loc(),loc,finals,ids.asAry()));
+ P._lexer.skipTo('{','}',"method body");
+ return ptr;
+ }
ReturnNode ret = parseFunctionBody(tfp,loc,finals,ids.asAry());
return new FunPtrNode(tfp,P._code._start,ret).peephole();
}
+ private void finishMethods() {
+ if( _methods == null ) return;
+ Ary<Method> methods = _methods;
+ Lexer after = P._lexer;
+ for( Method method : methods ) {
+ P._lexer = new Lexer(method.body());
+ TypeFunPtr sig = (TypeFunPtr)method.ptr()._con;
+ ReturnNode ret = parseFunctionBody(sig,method.loc(),method.finals(),method.ids());
+ method.ptr().setDef(1,ret);
+ P.require("}");
+ }
+ Ary<FunNode> functions = new Ary<>(FunNode.class);
+ for( Method method : methods ) functions.add(method.ptr().fun());
+ MethodAccess.infer(functions);
+ for( Method method : methods ) {
+ ReturnNode ret = method.ptr().ret();
+ TypeFunPtr sig = ret.fun().sig();
+ method.ptr().peephole();
+ // Declarations saw the header before its FunNode existed.
+ for( Var var : P._scope._vars )
+ if( var.type() instanceof TypeFunPtr tfp && tfp.isConstant() && tfp.fidx() == sig.fidx() ) {
+ ret.fun().setName(var._name);
+ var.refineType(var.type().join(method.ptr()._type));
+ TypeStruct self = P._scope.constructorSelf();
+ TypeStruct shape = (TypeStruct)Parser.TYPES.get(self._name);
+ Parser.TYPES.put(shape._name,shape.replace(shape.field(var._name).makeFrom(var.type())));
+ break;
+ }
+ method.ptr().unkeep();
+ }
+ P._lexer = after;
+ }
+
Node parseConstructor(String name, boolean explicit) {
P.require("{");
Ary<Type> ts = new Ary<>(Type.class);
@@ -198,9 +243,13 @@ final class Functions {
}
ReturnNode parseBody( String funName, TypeFunPtr sig, Lexer loc, BitSet finals, String[] ids) {
+ ArrayInitializers.Frame outerArray = P.arrays._frame;
+ P.arrays._frame = null;
Structs.Constructor outerCtor = P.structs._pendingCtor;
P.structs._pendingCtor = null;
boolean isInit = FunNode.isInit(funName);
+ Ary<Method> outerMethods = _methods;
+ _methods = FunNode.isInstance(funName) ? new Ary<>(Method.class) : null;
boolean isCtor = isInit || P.structs.isExplicitConstructorName(funName);
String typeName = isCtor ? ((TypeMemPtr)sig._sig[0])._obj._name : null;
@@ -233,7 +282,11 @@ final class Functions {
nvar = P.structs.updateSelfAsFieldsDiscovered( typeName, nvar );
}
- if( isInit ) P.structs.finishConstructor();
+ if( isInit ) {
+ finishMethods();
+ P.structs.finishConstructor();
+ }
+ _methods = outerMethods;
P.structs._pendingCtor = outerCtor;
ParmNode self = fun.parm(2);
@@ -275,6 +328,7 @@ final class Functions {
P.statements._breakScope = breakScope;
P._scope.pop();
P.construction._frame = outerConstruction;
+ P.arrays._frame = outerArray;
return ret;
}
@@ -300,8 +354,14 @@ final class Functions {
// All args, "as-if" called externally
for( int i = 0; i<ids.length; i++ ) {
Type t = sig.arg(i);
+ // Parse an implicit receiver with write access, then infer the
+ // public requirement from its uses once every method is present.
+ if( i == 0 && ids[0].equals("self") && !FunNode.isInit(funName) &&
+ !(sig.ret() instanceof TypeMem) && t instanceof TypeMemPtr ptr )
+ t = ptr.withAccess(false);
// Args take a default input if called from Start/external-world
- Node parm = new ParmNode(ids[i],i+2,t,fun,ConstantNode.seed(fun.defaultArg(i)).peephole()).peephole();
+ Type seed = i == 0 && t instanceof TypeMemPtr ? t : fun.defaultArg(i);
+ Node parm = new ParmNode(ids[i],i+2,t,fun,ConstantNode.seed(seed).peephole()).peephole();
if( t instanceof TypeMemPtr ptr && ptr._partial ) parm = new ConstructNode(parm).peephole();
P._scope.define(ids[i], t, (i==0 && ids[0]=="self") || (finals!=null && finals.get(i)), parm, loc);
}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Lexer.java b/src/main/java/com/seaofnodes/simple/parser/Lexer.java
index 5e4b7311..62dd054e 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Lexer.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Lexer.java
@@ -239,6 +239,7 @@ public class Lexer {
// Return a constant Type, either TypeInteger or TypeFloat
Type parseNumber() {
Lexer start=new Lexer(this);
+ if( isHex() ) return parseInteger(parseNumberString(),16,2,start);
int old = _position;
int len = isLongOrDouble();
boolean f32 = peek()=='f';
@@ -250,7 +251,7 @@ public class Lexer {
return TypeFloat.constant(Float.parseFloat(literal));
if( len > 0 ) {
if( len > 1 && _input[old]=='0' )
- throw Parser.error("Syntax error: integer values cannot start with '0'",start);
+ return parseInteger(literal,8,0,start);
long i = Long.parseLong(literal);
return TypeInteger.constant(i);
}
@@ -262,6 +263,11 @@ public class Lexer {
}
private String parseNumberString() {
int old = _position;
+ if( isHex() ) {
+ _position += 2;
+ while( isIdLetter(peek()) ) inc();
+ return new String(_input,old,_position-old);
+ }
int len = Math.abs(isLongOrDouble());
if( peek()=='f' ) {
nextChar();
@@ -270,6 +276,24 @@ public class Lexer {
return new String(_input,old,len);
}
+ private boolean isHex() {
+ return peek()=='0' && _position+1<_input.length && (peek(1)=='x' || peek(1)=='X');
+ }
+
+ private TypeInteger parseInteger(String literal, int radix, int prefix, Lexer start) {
+ String kind = radix==16 ? "Hexadecimal" : "Octal";
+ if( literal.length()==prefix )
+ throw Parser.error(kind+" integer literal requires at least one digit",start);
+ for( int i=prefix; i<literal.length(); i++ )
+ if( Character.digit(literal.charAt(i),radix)<0 )
+ throw Parser.error("Invalid "+kind.toLowerCase()+" integer literal '"+literal+"'",start);
+ try {
+ return TypeInteger.constant(Long.parseUnsignedLong(literal.substring(prefix),radix));
+ } catch( NumberFormatException ex ) {
+ throw Parser.error(kind+" integer literal exceeds 64 bits",start);
+ }
+ }
+
// Return +len that ends a long
// Return -len that ends a double
private int isLongOrDouble() {
@@ -381,14 +405,19 @@ public class Lexer {
precision = formatNumber(site);
}
char kind=nextChar();
- if( "diuoxXfFeEgGaAcsb".indexOf(kind)<0 )
+ if( "diuoxXfFeEgGaAcsbt".indexOf(kind)<0 )
throw Parser.error("Expected a string format: %(expression), %d(expression), or %%",site);
boolean number="diuoxXfFeEgGaA".indexOf(kind)>=0;
if( !number && (flags&~1)!=0 || "uoxX".indexOf(kind)>=0 && (flags&6)!=0 ||
"diu".indexOf(kind)>=0 && (flags&8)!=0 )
throw Parser.error("Invalid flags for %"+kind,site);
- if( precision>=0 && (kind=='c' || kind=='b') )
+ if( precision>=0 && (kind=='c' || kind=='b' || kind=='t') )
throw Parser.error("Precision is not supported for %"+kind,site);
+ if( kind=='t' && peek()!='(' ) {
+ int style="FrRMDT".indexOf(nextChar());
+ if( style<0 ) throw Parser.error("Expected a time format: %t, %tF, %tr, %tR, %tM, %tD, or %tT",site);
+ flags |= style<<8;
+ }
if( peek()!='(' ) throw Parser.error("Expected '(' after string format %"+kind,site);
inc();
return new Format(kind,flags,width,precision,site);
diff --git a/src/main/java/com/seaofnodes/simple/parser/MethodAccess.java b/src/main/java/com/seaofnodes/simple/parser/MethodAccess.java
new file mode 100644
index 00000000..099ba036
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/parser/MethodAccess.java
@@ -0,0 +1,130 @@
+package com.seaofnodes.simple.parser;
+
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
+import java.util.ArrayDeque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Set;
+
+/** Infer the access required through an instance method's receiver. */
+final class MethodAccess {
+ private final FunNode _fun;
+ private final Set<Node> _aliases = Collections.newSetFromMap(new IdentityHashMap<>());
+ private final Set<MethodAccess> _calls = new HashSet<>();
+ private boolean _writes;
+
+ private MethodAccess(FunNode fun) { _fun = fun; }
+
+ static void infer(Ary<FunNode> functions) {
+ HashMap<Integer,MethodAccess> methods = new HashMap<>();
+ for( FunNode fun : functions ) methods.put(fun.sig().fidx(),new MethodAccess(fun));
+ for( MethodAccess method : methods.values() ) method.scan(methods);
+ boolean changed;
+ do {
+ changed = false;
+ for( MethodAccess method : methods.values() )
+ if( !method._writes )
+ for( MethodAccess callee : method._calls )
+ if( callee._writes ) { method._writes = true; changed = true; break; }
+ } while( changed );
+ for( MethodAccess method : methods.values() ) method.finish();
+ }
+
+ private void scan(HashMap<Integer,MethodAccess> methods) {
+ if( _fun.parm(2) == null ) return; // Unused receiver.
+ ArrayDeque<Node> work = new ArrayDeque<>();
+ work.add(_fun.parm(2));
+ while( !work.isEmpty() ) {
+ Node value = work.remove();
+ if( !_aliases.add(value) ) continue;
+ for( Node use : value._outputs ) {
+ if( use == null || use instanceof ReadOnlyNode ) continue;
+ if( use instanceof StoreNode store ) {
+ if( store.ptr() == value || store.val() == value ) _writes = true;
+ } else if( use instanceof CallNode call ) {
+ for( int i = 2; i < call.nIns()-1; i++ )
+ if( call.in(i) == value ) call(call,i-2,methods);
+ } else if( use instanceof ReturnNode ret ) {
+ // Returning a writable alias must not grant write access
+ // to an object reached through a read-only receiver.
+ if( ret.expr() == value ) _writes = true;
+ } else if( use instanceof LoadNode load && load.ptr() == value ) {
+ Type type = type(load);
+ if( type instanceof TypeMemPtr ptr ) {
+ if( !ptr._ro ) work.add(load);
+ } else if( type == Type.BOTTOM || type == TypeScalar.BOT ) _writes = true;
+ } else if( use instanceof PhiNode || use instanceof GuardNode ) {
+ work.add(use);
+ } else if( use instanceof ConvertNode convert ) {
+ if( !(convert.dst() instanceof TypeMemPtr ptr && ptr._ro) ) _writes = true;
+ } else if( use instanceof BoolNode || use instanceof NotNode ||
+ use instanceof FldOffNode || use instanceof ScopeNode || use instanceof CLInitNode ) {
+ // Comparisons and field offsets do not expose write access.
+ } else {
+ // Includes raw-address casts and unrecognized escapes.
+ _writes = true;
+ }
+ }
+ }
+ }
+
+ private void call(CallNode call, int arg, HashMap<Integer,MethodAccess> methods) {
+ // Every member of this struct is now declared. An unresolved bare
+ // function name belongs to an outer scope, whose hidden slot is not self.
+ if( arg == 0 && call.fptr() instanceof FRefNode ) return;
+ Type type = type(call.fptr());
+ if( !(type instanceof TypeFunPtr sig) || arg >= sig.nargs() ) { _writes = true; return; }
+ if( arg == 0 && sig.arg(0) == TypePtr.PTR ) return;
+ if( arg == 0 ) {
+ if( XInt.isHigh(sig.fidxs()) ) { _writes = true; return; }
+ for( int fidx = XInt.next(sig.fidxs(),-1); fidx >= 0; fidx = XInt.next(sig.fidxs(),fidx) ) {
+ MethodAccess local = methods.get(fidx);
+ if( local != null ) _calls.add(local);
+ else {
+ FunNode target = CodeGen.CODE.link(fidx);
+ Type receiver = target == null ? sig.arg(0) : target.sig().arg(0);
+ if( !(receiver instanceof TypeMemPtr ptr && ptr._ro) ) _writes = true;
+ }
+ }
+ return;
+ }
+ if( !(sig.arg(arg) instanceof TypeMemPtr ptr && ptr._ro) ) _writes = true;
+ }
+
+ private static Type type(Node value) {
+ // Flow types can still be BOTTOM while the parser discovers fields.
+ if( value instanceof LoadNode load ) {
+ TypeStruct obj = load.off() instanceof ConFldOffNode off ? off._ts :
+ type(load.ptr()) instanceof TypeMemPtr ptr ? ptr._obj : null;
+ if( obj != null ) {
+ if( !obj.isAry() && Parser.TYPES.get(obj._name) instanceof TypeStruct latest ) obj = latest;
+ Field field = obj.field(load._name);
+ if( field != null ) return field._t;
+ }
+ }
+ return value._type;
+ }
+
+ private void finish() {
+ ParmNode self = _fun.parm(2);
+ if( self == null ) return;
+ TypeMemPtr receiver = ((TypeMemPtr)self._declaredType).withAccess(!_writes);
+ _fun.setSig(_fun.sig().makeFrom(receiver,0));
+ if( _writes ) return;
+ self._declaredType = receiver;
+ self.setDef(1,ConstantNode.seed(receiver).peephole());
+ // These parser-time assumptions are settled before Iter/SCCP. No
+ // writable alias escapes a method classified as read-only.
+ for( Node alias : _aliases )
+ if( alias._type instanceof TypeMemPtr ptr ) {
+ alias._type = ptr.makeRO();
+ CodeGen.CODE.add(alias);
+ CodeGen.CODE.addAll(alias._outputs);
+ }
+ }
+}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Parser.java b/src/main/java/com/seaofnodes/simple/parser/Parser.java
index 0cf3c0e7..9c18385d 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Parser.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Parser.java
@@ -7,7 +7,7 @@ import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.Ary;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.LinkedHashSet;
+import java.util.LinkedHashMap;
import java.util.Stack;
/** Source-unit entry point and shared cursor, scope, and IR utilities.
@@ -21,6 +21,7 @@ public class Parser {
final Functions functions = new Functions(this);
final Structs structs = new Structs(this);
final Allocations allocations = new Allocations(this);
+ final ArrayInitializers arrays = new ArrayInitializers(this);
final References references = new References(this);
final Conversions conversions = new Conversions(this);
final Construction construction = new Construction(this);
@@ -91,7 +92,7 @@ public class Parser {
public final Stack<ScopeNode> _xScopes = new Stack<>();
// Layouts needed by field accesses in this source file. Resolve after
// parsing, since a later local struct declaration can satisfy the use.
- public final LinkedHashSet<String> _requiredTypes = new LinkedHashSet<>();
+ public final LinkedHashMap<String,Lexer> _requiredTypes = new LinkedHashMap<>();
// Mapping from a type name to a Type. The string name matches
// `type.str()` call.
@@ -120,6 +121,11 @@ public class Parser {
public final Lexer _loc;
ParseException( String msg, Lexer loc ) { super(msg); _loc = loc==null ? null : new Lexer(loc); }
public String diagnostic() { return _loc==null ? getMessage() : _loc.diagnostic(getMessage()); }
+ // JUnit and uncaught-exception reporters use these entry points rather
+ // than the CLI's diagnostic(). Source errors should retain their source
+ // excerpt and caret there too. getMessage() stays stable for callers.
+ @Override public void printStackTrace(java.io.PrintStream out) { out.println(diagnostic()); }
+ @Override public void printStackTrace(java.io.PrintWriter out) { out.println(diagnostic()); }
}
public Parser(CodeGen code ) {
@@ -265,7 +271,7 @@ public class Parser {
// Field offset for an unknown type
public Node fldoff( Node n, String name ) {
if( n._type instanceof TypeMemPtr ptr && ptr._obj._fref )
- _requiredTypes.add(ptr._obj._name);
+ _requiredTypes.putIfAbsent(ptr._obj._name,loc());
return new FldOffNode(n,name,loc()).peephole();
}
public Node peep( Node n ) {
diff --git a/src/main/java/com/seaofnodes/simple/parser/README.md b/src/main/java/com/seaofnodes/simple/parser/README.md
index cee1d64a..48cf13c8 100644
--- a/src/main/java/com/seaofnodes/simple/parser/README.md
+++ b/src/main/java/com/seaofnodes/simple/parser/README.md
@@ -2,7 +2,7 @@
The parser builds Sea-of-Nodes IR directly. Each component shares the cursor,
active scope, and compilation state through its `Parser P` reference; there is no intermediate
-AST. The [grammar map](../../../../../../../docs/chapter30/30-grammar.md) connects language
+AST. The [grammar map](../../../../../../../docs/chapter31/31-grammar.md) connects language
productions to their entry points.
| File | Responsibility |
@@ -14,6 +14,7 @@ productions to their entry points.
| `Declarations.java` | Bindings, initializers, mutability modifiers, and native declarations. |
| `Types.java` | Type syntax, qualified-name lookup, arrays, function types, and forward types. |
| `Functions.java` | Function expressions, parameters, arguments, calls, and function entry/return graphs. |
+| `MethodAccess.java` | Infers whether an instance method needs writable access through its receiver. |
| `Structs.java` | Struct and constructor declarations, field discovery, and initialization Stores. |
| `Construction.java` | Selects private self memory during parsing; its comment explains BOT fields, the TypeMemPtr qualifier, and checks deferred to TypeCheck. |
| `References.java` | Bare-name and field resolution, implicit receivers, assignments, and forward references. |
@@ -23,8 +24,16 @@ productions to their entry points.
| `Var.java` | Lexical binding metadata: name, type, scope slot, and mutability. |
Start with `Statements.parseStatement` or `Expressions.parseAssignment` to follow
-the grammar. Loop updates are parsed after their bodies, and declared constructor
-bodies after field discovery; `Lexer.skipTo` scans these deferred fragments.
+the grammar. Loop updates are parsed after their bodies. Instance method headers
+are parsed immediately, but their bodies and declared constructor bodies wait
+until all fields are known. Methods can therefore use fields declared later in
+the struct. `Lexer.skipTo` scans these deferred fragments.
+
+Method receivers accept read-only access unless the body needs writable access.
+The check follows aliases, child references, array elements, and calls between
+methods. Writing an independent argument does not require a writable receiver.
+Returning or storing a writable receiver alias also requires writable access;
+an explicit read-only view can be exposed without that requirement.
`Parser` keeps the short cursor, scope/memory, IR-building, and diagnostic helpers
used by these components. Independent helpers live with their data: class-object
diff --git a/src/main/java/com/seaofnodes/simple/parser/References.java b/src/main/java/com/seaofnodes/simple/parser/References.java
index 6335bad2..7724103e 100644
--- a/src/main/java/com/seaofnodes/simple/parser/References.java
+++ b/src/main/java/com/seaofnodes/simple/parser/References.java
@@ -120,6 +120,7 @@ final class References {
return P.expressions.parsePostfixMethod(P.construction.view(rvalue),P._scope.in(P._scope.update(P._scope.var(selfx),null)));
}
// Assign-update direct into Scope
+ P.arrays.checkAssignment(var,site);
if( var._final && !P._scope.canInit(var) )
throw Parser.error("Cannot reassign final '"+var._name+"'",site);
checkSharedFunction(var);
@@ -130,6 +131,7 @@ final class References {
}
void checkAssignment(Var def, Lexer site) {
+ if( fieldSelf(def)==null ) P.arrays.checkAssignment(def,site);
String name = def._name;
// A final field may receive its first value in its constructor.
if( P._scope.in(def._idx)._type!=Type.TOP && def._final &&
@@ -278,7 +280,7 @@ final class References {
// Check for assign-update, "ptr.fld += expr" or "ary[idx]++"
char ch = pre==0 ? P._lexer.matchOperAssign() : (char)pre;
if( ch!=0 ) {
- if( fld!=null && decl==Type.BOTTOM )
+ if( fld!=null && decl==Type.BOTTOM && !ts._fref )
throw Parser.error( "'" + ts._name + "' is not fully initialized, field '" + fld._fname + "' needs to be set in a constructor",site );
Node op = P.expressions.opAssign(ch,load, decl ).keep();
mem.keep();
@@ -306,10 +308,15 @@ final class References {
// Fields are lvalues in memory, including inside their initializer and
// explicit constructor. Scope slots retain declaration metadata only.
private Node fieldSelf(Var var) {
- int kx = P._scope.kindx(var), fx = P._scope.enclosingFuncOrDecl();
+ int kx = P._scope.kindx(var);
ScopeNode.Kind kind = P._scope._kinds.at(kx);
if( !(kind instanceof Kind.Func inst) || !FunNode.isInstance(inst._name) || var._idx<inst._lexSize+2 ) return null;
- if( kx==fx || kx+1==fx ) return defaultSelf();
+ // Nested functions have their own receiver parameter. Resolve fields
+ // through it even across intervening function and block scopes.
+ Node self = defaultSelf();
+ Type owner = P._scope.var(inst._lexSize).type();
+ if( self._type instanceof TypeMemPtr ptr && owner instanceof TypeMemPtr declared &&
+ ptr._obj._name.equals(declared._obj._name) ) return self;
return null;
}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Statements.java b/src/main/java/com/seaofnodes/simple/parser/Statements.java
index 707202ab..0c2a25f7 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Statements.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Statements.java
@@ -78,6 +78,7 @@ final class Statements {
*/
private Node parseReturn() {
var expr = P.require(P.expressions.parseAssignment(), ";");
+ if( P.arrays._frame!=null ) return P.arrays.addReturn(expr);
return P.functions.addReturn(expr);
}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Strings.java b/src/main/java/com/seaofnodes/simple/parser/Strings.java
index 5bf76585..be913371 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Strings.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Strings.java
@@ -32,7 +32,7 @@ final class Strings {
Node strings=P.references.member(text,"string",fmt.site()).keep();
fptr=new FormatNode(value,ints.unkeep(),floats.unkeep(),strings.unkeep(),fmt.site()).peephole();
} else {
- String family="diuoxXcb".indexOf(fmt.kind())>=0 ? "integer" : fmt.kind()=='s' ? "string" : "floating";
+ String family="diuoxXcb".indexOf(fmt.kind())>=0 ? "integer" : fmt.kind()=='s' ? "string" : fmt.kind()=='t' ? "time" : "floating";
fptr=P.references.member(text,family,fmt.site());
}
call(text,fptr,fmt.site(),builder,value.unkeep(),P.con(fmt.kind() | fmt.flags()<<8),
diff --git a/src/main/java/com/seaofnodes/simple/parser/Types.java b/src/main/java/com/seaofnodes/simple/parser/Types.java
index 827518fd..501924f0 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Types.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Types.java
@@ -37,6 +37,12 @@ final class Types {
if( t0==Type.BOTTOM || t0==Type.TOP ) return t0; // var/val
tname = typeName(tname).intern();
t0 = Parser.TYPES.get(tname);
+ // Bare forward types use the same file-relative name as `new`.
+ // A later sibling declaration must complete this very type.
+ if( t0 == null && tname.indexOf('.') == -1 ) {
+ tname = fullTypeName(tname);
+ t0 = Parser.TYPES.get(tname);
+ }
}
Type t1 = t0==null ? TypeStruct.forward(tname) : t0;
@@ -135,9 +141,8 @@ final class Types {
Type implicitSelfType() {
TypeStruct self = P._scope.constructorSelf();
- // Ordinary instance methods accept the declared object shape. The
- // constructor-only receiver starts at the low/default field values,
- // and is built explicitly by parseConstructor().
+ // Function types without a body must allow for writes. Function
+ // expressions can infer a weaker requirement in MethodAccess.
return self == null ? TypePtr.PTR : TypeMemPtr.make(self);
}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Var.java b/src/main/java/com/seaofnodes/simple/parser/Var.java
index ddbb91d8..5361148f 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Var.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Var.java
@@ -33,6 +33,11 @@ public class Var {
public Type type() { return _type; }
public Type _type() { return _type; } // ScopeNode printer needs to have no side effects
+ void refineType(Type type) {
+ assert type.isa(_type) : _name+": "+_type+" -> "+type;
+ _type = type;
+ }
+
// Forward reference variables (not types) must be BOTTOM and
// distinct from inferred variables
public boolean isFRef() { return _fref; }
diff --git a/src/main/java/com/seaofnodes/simple/parser/package-info.java b/src/main/java/com/seaofnodes/simple/parser/package-info.java
index c886f72a..73ebec2e 100644
--- a/src/main/java/com/seaofnodes/simple/parser/package-info.java
+++ b/src/main/java/com/seaofnodes/simple/parser/package-info.java
@@ -7,7 +7,7 @@
* use-side name/field resolution; Allocations, ControlFlow, and Conversions
* contain their shared IR construction. Lexer also supplies location snapshots.
*
- * <p>See {@code chapter30/docs/30-grammar.md} for the production-to-method map.
+ * <p>See {@code chapter31/docs/31-grammar.md} for the production-to-method map.
* Evaluation order and scope lifetimes are explicit: loop updates and declared
* constructor bodies are the two deferred source fragments. There is no AST.
*/
diff --git a/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java b/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java
index e8218afd..e4d1659d 100644
--- a/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java
+++ b/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java
@@ -10,19 +10,22 @@ import com.seaofnodes.simple.type.*;
import java.util.ArrayList;
import com.seaofnodes.simple.codegen.Encoding;
import com.seaofnodes.simple.util.BAOS;
-import java.util.HashMap;
+import java.util.Map;
/** Machine-specific facts; traversal, columns and byte display live in print/. */
public final class ASMPrinter extends AssemblyAdapter<Node> {
private final CodeGen _code;
private final Encoding _enc;
- private ASMPrinter(CodeGen code) {
+ private ASMPrinter(CodeGen code, Encoding enc) {
super(new IRPrinter());
_code=code;
- _enc=code._encoding;
+ _enc=enc;
}
public static SB print(SB sb, CodeGen code) {
- return sb.p(new com.seaofnodes.print.ASMPrinter<>(new ASMPrinter(code)).print());
+ return print(sb,code,code._encoding);
+ }
+ public static SB print(SB sb, CodeGen code, Encoding enc) {
+ return sb.p(new com.seaofnodes.print.ASMPrinter<>(new ASMPrinter(code,enc)).print());
}
@Override public ArrayList<Node> blocks() {
var blocks=new ArrayList<Node>();
@@ -89,11 +92,11 @@ public final class ASMPrinter extends AssemblyAdapter<Node> {
var data=new ArrayList<Data>();
for( var obj : _enc._data.entries() )
if( !obj.external && obj.readOnly==ro )
- data.add(new Data(obj,obj.symbol+": "+obj.type.str(),obj.alignment,obj.size));
+ data.add(new Data(obj,obj.label()+": "+obj.type.str(),obj.alignment,obj.size));
return data;
}
// Retain this diagnostic entry point for checking that pool printing never lays out types.
- static int printConstantPool(int address, SB sb, BAOS bits, HashMap<Node,Encoding.Relo> relos,
+ static int printConstantPool(int address, SB sb, BAOS bits, Map<Node,Encoding.Relo> relos,
boolean ro, String name) {
sb.p(com.seaofnodes.print.ASMPrinter.pool(new Pool(name,bits.buf(),0,address,entries(relos.values(),ro))));
return address+bits.size();
diff --git a/src/main/java/com/seaofnodes/simple/type/Type.java b/src/main/java/com/seaofnodes/simple/type/Type.java
index 0f655f49..2e228494 100644
--- a/src/main/java/com/seaofnodes/simple/type/Type.java
+++ b/src/main/java/com/seaofnodes/simple/type/Type.java
@@ -531,7 +531,7 @@ public class Type /*implements Cloneable*/ {
// tag-space except for the first tag-space which uses 1 tag for
// multiple Types instead of 1 Type having multiple tags. Has to
// be ordered to match _type enum.
- for( Type t : new Type[]{Type.XNIL,TypeScalar.BOT,TypePtr.PTR,TypeInteger.BOT,TypeFloat.F64,TypeConAryB.ABC,TypeRPC.BOT,TypeMemPtr.BOT,TypeFunPtr.BOT,TypeMem.BOT,Field.BOT,TypeStruct.BOT,TypeTuple.BOT} )
+ for( Type t : new Type[]{Type.XNIL,TypeScalar.BOT,TypePtr.PTR,TypeInteger.BOT,TypeFloat.F64,TypeConAry.ABC,TypeRPC.BOT,TypeMemPtr.BOT,TypeFunPtr.BOT,TypeMem.BOT,Field.BOT,TypeStruct.BOT,TypeTuple.BOT} )
TAGOFFS[t._type+1] = TAGOFFS[t._type] + t.TAGOFF();
assert TAGOFFS[TMAX] <= 255;
return TAGOFFS;
diff --git a/src/main/java/com/seaofnodes/simple/type/TypeConAry.java b/src/main/java/com/seaofnodes/simple/type/TypeConAry.java
index 9cb23e09..c10f09aa 100644
--- a/src/main/java/com/seaofnodes/simple/type/TypeConAry.java
+++ b/src/main/java/com/seaofnodes/simple/type/TypeConAry.java
@@ -1,33 +1,48 @@
package com.seaofnodes.simple.type;
import com.seaofnodes.simple.util.BAOS;
-import com.seaofnodes.simple.util.Utils;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
/**
- * Represents a constant array of primitives
+ * Constant integer contents with an explicit storage width and signedness.
*/
-public abstract class TypeConAry<A> extends TypeScalar {
+public final class TypeConAry extends TypeScalar {
boolean _any;
byte _widen;
- // One of byte or int array
- public final A _ary;
+ private final TypeInteger _storage;
+ private final long[] _ary;
- TypeConAry( boolean any, byte widen, A ary ) { super(TCONARY); _any = any; _widen = widen; _ary = ary; }
+ private TypeConAry(boolean any, byte widen, TypeInteger storage, long[] ary) {
+ super(TCONARY);
+ _any=any; _widen=widen; _storage=storage; _ary=ary;
+ }
+ static final TypeConAry ABC=make("abc");
+ static final TypeConAry ABCD=make("abcd");
public static void gather(ArrayList<Type> ts) {
- ts.add(TypeConAryB.make(""));
- ts.add(TypeConAryB.ABC);
- ts.add(TypeConAryB.ABCD);
- ts.add(TypeConAryI.I123);
+ ts.add(make(""));
+ ts.add(ABC);
+ ts.add(ABCD);
+ ts.add(make(TypeInteger.I32,new long[]{1,2,3}));
+ ts.add(make(TypeInteger.U16,new long[]{0,65535}));
+ ts.add(make(TypeInteger.BOT,new long[]{Long.MIN_VALUE,Long.MAX_VALUE}));
+ for( TypeInteger storage : STORAGE ) {
+ ts.add(make(storage,new long[0]));
+ ts.add(make(storage,new long[]{0,1}));
+ }
}
// Fresh, uninterned type sharing this array; xdual must not intern.
- abstract TypeConAry<A> _make(boolean any, byte widen);
+ private TypeConAry _make(boolean any, byte widen) {
+ return new TypeConAry(any,widen,_storage,_ary);
+ }
- @Override TypeConAry<A> xdual() { return _make( !_any, (byte)(3-_widen) ); }
+ @Override TypeConAry xdual() { return _make( !_any, (byte)(3-_widen) ); }
@Override Type xmeet(Type t) {
TypeConAry ary = (TypeConAry)t; // Invariant
+ if( len()==0 ) return emptyMeet(ary);
+ if( ary.len()==0 ) return ary.emptyMeet(this);
// Same base array but different?
if( _equals(ary) )
// Return array with larger widen
@@ -37,15 +52,7 @@ public abstract class TypeConAry<A> extends TypeScalar {
}
Type ymeet( TypeInteger ti ) {
- // Empty arrays have no observed elements: use the storage bounds.
- // Check both endpoints just as the loop checks a nonempty body;
- // retaining high integer inputs here preserves lattice duality.
- if( len()==0 ) {
- TypeInteger bound=emptyElem();
- if( !ti.isa(TypeInteger.make(bound._min,bound._min,ti._widen)) ||
- !ti.isa(TypeInteger.make(bound._max,bound._max,ti._widen)) )
- return ti.meet(bound);
- }
+ if( len()==0 ) return emptyMeet(ti);
// if i can isa *each* element, then maybe can keep.
// no good to i.isa(elem()) because fails the dual
for( int j=0; j<len(); j++ )
@@ -56,12 +63,21 @@ public abstract class TypeConAry<A> extends TypeScalar {
}
private boolean _equals(TypeConAry ary) {
- int len = len();
- if( len != ary.len() ) return false;
- for( int i=0; i<len; i++ )
- if( at8(i) != ary.at8(i) )
- return false;
- return true;
+ return _storage==ary._storage && (len()!=0 || _any==ary._any) && Arrays.equals(_ary,ary._ary);
+ }
+ @Override boolean eq(Type t) { return t instanceof TypeConAry ary && _widen==ary._widen && _equals(ary); }
+ @Override int hash() { return Arrays.hashCode(_ary)^_storage.hashCode()^(len()==0 && _any ? 1231 : 0); }
+ @Override public String str() {
+ boolean text=_storage==TypeInteger.U8;
+ for( long value : _ary )
+ if( value<32 || value>126 ) { text=false; break; }
+ if( !text ) {
+ String range=elem().str();
+ return range.startsWith("[") ? range : "["+range+"]";
+ }
+ byte[] bytes = new byte[len()];
+ for( int i=0; i<bytes.length; i++ ) bytes[i]=(byte)_ary[i];
+ return "[\""+new String(bytes)+"\"]";
}
@Override public boolean isHigh() { return this==TOP; }
@@ -70,8 +86,6 @@ public abstract class TypeConAry<A> extends TypeScalar {
// Meet-over-elements type
public Type elem() {
- if( _ary==null )
- return _any ? Type.TOP : BOTTOM;
int len = len();
long min = Long.MAX_VALUE;
if( len==0 ) return emptyElem();
@@ -83,23 +97,80 @@ public abstract class TypeConAry<A> extends TypeScalar {
return TypeInteger.make(min,max,_widen);
}
private TypeInteger emptyElem() {
- return log_size()==0 ? TypeInteger.make(0,255,_widen)
- : TypeInteger.make(Integer.MIN_VALUE,Integer.MAX_VALUE,_widen);
+ TypeInteger storage=storageType();
+ return _any ? TypeInteger.make(storage._min,storage._max,_widen)
+ : TypeInteger.make(storage._max,storage._min,_widen);
+ }
+ // Keep the empty payload distinct from its element bound. It sits just
+ // above the storage dual; its own dual sits just below the storage bound.
+ // This makes it a subtype of every constant array within those bounds,
+ // while preserving meet/join symmetry with ordinary integer ranges.
+ private Type emptyMeet(Type t) {
+ Type bound=emptyElem();
+ if( t.isa(bound) && (_any || t!=bound) ) return this;
+ return bound.meet(t);
}
- public abstract long at8(int idx);
- public abstract int len();
- @Override public abstract int log_size();
- public void write( BAOS baos ) { throw Utils.TODO("Should not reach here: abstract constant array cannot be written"); }
+ public TypeInteger storageType() { return _storage; }
+ public long at8(int idx) { return _ary[idx]; }
+ public int len() { return _ary.length; }
+ @Override public int log_size() { return _storage.log_size(); }
+ public void write( BAOS baos ) {
+ for( int i=0; i<len(); i++ ) {
+ long value=at8(i);
+ for( int b=0; b<(1<<log_size()); b++ ) baos.write((byte)(value >>> (8*b)));
+ }
+ }
+
+ /** Canonical contents, independent of the node which discovered them. */
+ public static TypeConAry make(TypeInteger storage, long[] values) {
+ long[] copy=values.clone();
+ int shift=64-(8<<storage.log_size());
+ for( int i=0; i<copy.length; i++ )
+ copy[i]=storage._min<0 ? (copy[i]<<shift)>>shift : copy[i]&storage._max;
+ return new TypeConAry(false,(byte)0,storage,copy).intern();
+ }
+ public static TypeConAry make(byte[] bytes) {
+ long[] values=new long[bytes.length];
+ for( int i=0; i<bytes.length; i++ ) values[i]=bytes[i]&255;
+ return new TypeConAry(false,(byte)0,TypeInteger.U8,values).intern();
+ }
+ public static TypeConAry make(String s) { return make(s.getBytes()); }
+
+ // Keep the existing byte-array tag and encoding. An impossible Java array
+ // length escapes to a typed payload, without renumbering later type tags.
+ private static final int TYPED = -1;
+ private static final TypeInteger[] STORAGE={TypeInteger.BOOL,TypeInteger.I8,TypeInteger.U8,
+ TypeInteger.I16,TypeInteger.U16,TypeInteger.I32,TypeInteger.U32,TypeInteger.BOT};
// Reserve tags for u8 array
@Override int TAGOFF() { return 1; }
@Override public void packed( BAOS baos, HashMap<String,Integer> strs ) {
- assert log_size()==0 && !_any;
baos.write(TAGOFFS[TCONARY]+0);
+ if( storageType()!=TypeInteger.U8 || _any || _widen!=0 ) {
+ baos.packed4(TYPED);
+ int kind=0;
+ while( kind<STORAGE.length && STORAGE[kind]!=storageType() ) kind++;
+ assert kind<STORAGE.length;
+ baos.packed1(kind);
+ baos.packed1((_any ? 4 : 0)|_widen);
+ }
baos.packed4(len());
- baos.write((byte[])_ary);
+ write(baos);
}
static TypeConAry packed( int tag, BAOS bais ) {
- return TypeConAryB.make(bais.read(new byte[bais.packed4()]));
+ int len=bais.packed4();
+ if( len!=TYPED ) return make(bais.read(new byte[len]));
+ TypeInteger storage=STORAGE[bais.packed1()];
+ int flags=bais.packed1();
+ long[] values=new long[bais.packed4()];
+ int size=1<<storage.log_size();
+ for( int i=0; i<values.length; i++ ) {
+ long value=0;
+ for( int b=0; b<size; b++ ) value|=(long)bais.read() << (8*b);
+ if( storage._min<0 && size<8 ) value=(value << (64-size*8)) >> (64-size*8);
+ values[i]=value;
+ }
+ TypeConAry ary=make(storage,values);
+ return flags==0 ? ary : ary._make((flags&4)!=0,(byte)(flags&3)).intern();
}
}
diff --git a/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java b/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java
deleted file mode 100644
index 84be5fb2..00000000
--- a/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.seaofnodes.simple.type;
-
-import com.seaofnodes.simple.util.BAOS;
-import java.util.Arrays;
-
-public class TypeConAryB extends TypeConAry<byte[]> {
- private TypeConAryB(boolean any, byte widen, byte[] bs) { super(any,widen,bs); }
- @Override TypeConAryB _make( boolean any, byte widen ) { return new TypeConAryB(any,widen,_ary); }
- public static TypeConAryB make( byte[] bs ) { return new TypeConAryB(false,(byte)0,bs).intern(); }
- public static TypeConAryB make( String s ) { return make(s.getBytes()); }
- static final TypeConAryB ABC = make("abc");
- static final TypeConAryB ABCD = make("abcd");
- @Override public long at8(int idx) { return _ary[idx]&255; }
- @Override public int len() { return _ary.length; }
- @Override public int log_size() { return 0; }
- @Override public String str() { return "[\""+new String(_ary)+"\"]"; }
- @Override boolean eq(Type t) { return t instanceof TypeConAryB ary && _widen==ary._widen && Arrays.equals(_ary,ary._ary); }
- @Override int hash() { return Arrays.hashCode(_ary); }
- @Override public void write( BAOS baos ) { baos.write(_ary); }
-}
diff --git a/src/main/java/com/seaofnodes/simple/type/TypeConAryI.java b/src/main/java/com/seaofnodes/simple/type/TypeConAryI.java
deleted file mode 100644
index b1b061de..00000000
--- a/src/main/java/com/seaofnodes/simple/type/TypeConAryI.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.seaofnodes.simple.type;
-
-import java.util.Arrays;
-
-public class TypeConAryI extends TypeConAry<int[]> {
- private TypeConAryI(boolean any, byte widen, int[] bs) { super(any,widen,bs); }
- public static TypeConAryI make( int[] bs ) { return new TypeConAryI(false,(byte)0,bs).intern(); }
- @Override TypeConAryI _make( boolean any, byte widen ) { return new TypeConAryI(any,widen,_ary); }
- static final TypeConAryI I123 = make(new int[]{1,2,3});
- @Override public long at8(int idx) { return _ary[idx]; }
- @Override public int len() { return _ary.length; }
- @Override public int log_size() { return 2; }
- @Override public String str() { return "[i32]"; }
- @Override boolean eq(Type t) { return t instanceof TypeConAryI ary && _widen==ary._widen && Arrays.equals(_ary,ary._ary); }
- @Override int hash() { return Arrays.hashCode(_ary); }
-}
diff --git a/src/main/java/com/seaofnodes/simple/type/TypeFloat.java b/src/main/java/com/seaofnodes/simple/type/TypeFloat.java
index d410e9cf..8d04fcbf 100644
--- a/src/main/java/com/seaofnodes/simple/type/TypeFloat.java
+++ b/src/main/java/com/seaofnodes/simple/type/TypeFloat.java
@@ -107,7 +107,7 @@ public class TypeFloat extends TypeScalar {
else {
assert isConstant();
baos.write(TAGOFFS[_type] + 2);
- baos.packed8(Double.doubleToLongBits(_con));
+ baos.packed8(Double.doubleToRawLongBits(_con));
}
}
@@ -121,12 +121,12 @@ public class TypeFloat extends TypeScalar {
}
@Override
- int hash() { return (int)(Double.hashCode(_con) ^ _sz ^ (1<<17)); }
+ int hash() { return Long.hashCode(Double.doubleToRawLongBits(_con)) ^ _sz ^ (1<<17); }
@Override
public boolean eq( Type t ) {
TypeFloat i = (TypeFloat)t; // Contract
- // Allow NaN to check for equality
- return _sz==i._sz && Double.doubleToLongBits(_con)==Double.doubleToLongBits(i._con);
+ // Representation conversions observe NaN signs/payloads and signed zero.
+ return _sz==i._sz && Double.doubleToRawLongBits(_con)==Double.doubleToRawLongBits(i._con);
}
}
diff --git a/src/main/java/com/seaofnodes/simple/type/TypeMem.java b/src/main/java/com/seaofnodes/simple/type/TypeMem.java
index 5faa7107..1c83ddd7 100644
--- a/src/main/java/com/seaofnodes/simple/type/TypeMem.java
+++ b/src/main/java/com/seaofnodes/simple/type/TypeMem.java
@@ -211,14 +211,19 @@ public class TypeMem extends Type {
+ (!_one ? 0 : (_clz ? 4 : 8)));
if( _alias!=1 ) {
// generic alias
- assert _alias <= 255;
- baos.packed1( _alias );
+ // Zero is reserved for alias-set flags, never a memory slice.
+ // Use it as an escape for large libraries, retaining the existing
+ // single-byte encoding (including 255) for older objects.
+ assert _alias>1;
+ baos.packed1(_alias<=255 ? _alias : 0);
+ if( _alias>255 ) baos.packed4(_alias);
}
XInt.packed(baos,_escFs);
XInt.packed(baos,_escAs);
}
static TypeMem packed( int tag, BAOS bais ) {
int alias = (tag&1)==0 ? 1 : bais.packed1();
+ if( alias==0 ) alias=bais.packed4();
boolean xfinal= (tag&2)==2;
boolean one = (tag&12)!=0;
boolean clz = (tag&4)==4;
diff --git a/src/main/java/com/seaofnodes/simple/type/TypeStruct.java b/src/main/java/com/seaofnodes/simple/type/TypeStruct.java
index 7c9758bd..11ba3cc4 100644
--- a/src/main/java/com/seaofnodes/simple/type/TypeStruct.java
+++ b/src/main/java/com/seaofnodes/simple/type/TypeStruct.java
@@ -146,8 +146,7 @@ public class TypeStruct extends Type {
Type len = TypeInteger.constant(con.len());
return make(name,false,
Field.make("#" ,len , lenAlias,true),
- Field.make("[]" ,con.elem(),bodyAlias,true),
- Field.make("[con]",con ,bodyAlias,true));
+ Field.make("[]" ,con ,bodyAlias,true));
}
public TypeStruct makeInit() {
@@ -190,6 +189,9 @@ public class TypeStruct extends Type {
}
@Override TypeStruct _close( String name, HashMap<String, Type> TYPES ) {
assert (name==null) != (TYPES==null); // One or the other
+ // Primitive constant contents are already closed. Looking up their
+ // shared array name would discard the length and payload on import.
+ if( isConAry() ) return this;
String namePlusFinal = isAry() ? (_name+(_fields[1]._final ? "~" : "!")).intern() : _name;
TypeStruct ts = (TypeStruct)VISIT.get(namePlusFinal);
if( ts!=null ) return ts;
@@ -251,7 +253,7 @@ public class TypeStruct extends Type {
public static final TypeStruct TEST= make("test",false,Field.TEST);
private static final TypeStruct ARY = makeAry("[]i64",TypeInteger.U32,-1,TypeInteger.BOT,-2,false);
private static final TypeStruct STR = makeAry("[]u8" ,TypeInteger.U32,-1,TypeInteger.U8 ,-4,false);
- private static final TypeStruct ABC = makeAry("[]u8",-1,-4,TypeConAryB.ABC);
+ private static final TypeStruct ABC = makeAry("[]u8",-1,-4,TypeConAry.ABC);
// Representative unresolved forms for the exhaustive lattice tests:
// bare forward ref, forward ref constrained by a field use, and definition.
private static final TypeStruct FREF0 = forward("%FREF");
@@ -462,6 +464,7 @@ public class TypeStruct extends Type {
@Override public int log_size() { throw Utils.TODO("Should not reach here: structs have no scalar log size"); }
@Override public int size() { return offset(_fields.length); }
@Override public int alignment() {
+ if( isAry() ) return Math.max(2,aryScale()); // The length is always u32.
int align = 0;
for( Field f : _fields )
if( !f._extern ) align = Math.max(align, f._t.alignment());
@@ -607,7 +610,7 @@ public class TypeStruct extends Type {
int align = _fields[1]._t.alignment();
_offs[1] = (4 + ((1<<align)-1)) & -(1<<align);
if( _fields[1]._t instanceof TypeConAry ary ) // Array length is well-defined for constants, not defined for runtime arrays
- _offs[2] = _offs[1]+ary.len();
+ _offs[2] = _offs[1]+(ary.len()<<ary.log_size());
} else {
int[] cnts = new int[5]; // Count of fields at log field size
diff --git a/src/main/smp/sys.smp b/src/main/smp/sys.smp
index 1201315b..3fb8d94b 100644
--- a/src/main/smp/sys.smp
+++ b/src/main/smp/sys.smp
@@ -2,12 +2,18 @@
// top-level default standard library import
val adt = adt; // Abstract Data Types
+val audio = audio; // Optional background sound (miniaudio)
val ary = ary; // Array utilities
val aryi64 = aryi64; // Variable sized array of i64
val aryu8 = aryu8; // Variable sized array of u8
val char = char; // Character utilities
+val integer = integer; // Integer bounds
+val math = math; // Floating-point representation primitives
val io = io; // IO library
+val gfx = gfx; // Optional pixel window (MiniFB)
+val draw = draw; // Software raster drawing in Simple
val text = text; // Typed text conversions
+val time = time; // Clocks, calendar text, and cooperative timers
val libc = libc; // libc FFI bindings
val rt = rt; // Run Time support
val scan = scan; // A simple string parser
diff --git a/src/main/smp/sys/adt/bitset.smp b/src/main/smp/sys/adt/bitset.smp
index d02cd2fe..a51a63e1 100644
--- a/src/main/smp/sys/adt/bitset.smp
+++ b/src/main/smp/sys/adt/bitset.smp
@@ -20,7 +20,7 @@ struct bitset {
if( x < len ) return self;
while( x >= len ) len *= 2;
var bits = new i64[len];
- for( int i=0; i<_bits#; i++ )
+ for( int i = 0; i < _bits#; i++ )
bits[i] = _bits[i];
_bits = bits;
return self;
@@ -51,7 +51,7 @@ struct bitset {
};
val clear = { ->
- for( int i=0; i<_bits#; i++ ) _bits[i] = 0;
+ for( int i = 0; i < _bits#; i++ ) _bits[i] = 0;
return self;
};
};
diff --git a/src/main/smp/sys/ary.smp b/src/main/smp/sys/ary.smp
index c139cd33..b94ffa8f 100644
--- a/src/main/smp/sys/ary.smp
+++ b/src/main/smp/sys/ary.smp
@@ -3,12 +3,12 @@
// Geometric growth without wrapping a u32 length. Zero means too large.
val capacity = { u32 current, i64 needed ->
- if( needed < 0 || needed > 4294967295 ) return 0;
+ if( needed < 0 || needed > sys.integer.MAX_U32 ) return 0;
if( needed <= current ) return current;
i64 size = current ? current : 1;
while( size < needed ) {
size *= 2;
- if( size > 4294967295 ) size = 4294967295;
+ if( size > sys.integer.MAX_U32 ) size = sys.integer.MAX_U32;
}
u32 result = size;
return result;
@@ -19,7 +19,7 @@ val copy = { u8[] dst, u32 doff, u8[~] src, u32 soff, u32 len ->
if( doff > dst# || soff > src# ) return dst;
if( len > dst# - doff ) len = dst# - doff;
if( len > src# - soff ) len = src# - soff;
- for( u32 i=0; i<len; i++ )
+ for( u32 i = 0; i < len; i++ )
dst[i+doff] = src[i+soff];
return dst;
};
@@ -33,7 +33,7 @@ val cmp = { u8[~] src1, u32 off1, u8[~] src2, u32 off2, u32 len ->
u32 len1 = len > left1 ? left1 : len;
u32 len2 = len > left2 ? left2 : len;
u32 max = len1 > len2 ? len2 : len1;
- for( u32 i=0; i<max; i++ )
+ for( u32 i = 0; i < max; i++ )
if( src1[off1+i] != src2[off2+i] )
return src1[off1+i] - src2[off2+i]; // Bytes determine compare
return len1 - len2; // Length determines compare
diff --git a/src/main/smp/sys/aryi64.smp b/src/main/smp/sys/aryi64.smp
index b9f1fbed..aba1b459 100644
--- a/src/main/smp/sys/aryi64.smp
+++ b/src/main/smp/sys/aryi64.smp
@@ -14,7 +14,7 @@ struct aryi64 {
u32 newlen = ary.capacity(buf#,len+len2);
if( !newlen ) libc._exit(1); // Length exceeds the array representation.
i64[] !buf2 = new i64[newlen];
- for( u32 i=0; i<len; i++ )
+ for( u32 i = 0; i < len; i++ )
buf2[i] = buf[i];
buf = buf2;
self;
@@ -32,7 +32,7 @@ struct aryi64 {
val str = { ->
// TODO: clunky default constructor syntax
var str = new aryu8().add('[');
- for( u32 i=0; i<len; i++ )
+ for( u32 i = 0; i < len; i++ )
str.num(buf[i]).add(',').add(' ');
if( len ) str.undo(2);
return str.add(']');
@@ -79,7 +79,7 @@ val copy = { i64[] dst, u32 doff, i64[~] src, u32 soff, u32 len ->
if( doff > dst# || soff > src# ) return dst;
if( len > dst# - doff ) len = dst# - doff;
if( len > src# - soff ) len = src# - soff;
- for( u32 i=0; i<len; i++ )
+ for( u32 i = 0; i < len; i++ )
dst[i+doff] = src[i+soff];
return dst;
};
diff --git a/src/main/smp/sys/aryu8.smp b/src/main/smp/sys/aryu8.smp
index 18543f13..3f462898 100644
--- a/src/main/smp/sys/aryu8.smp
+++ b/src/main/smp/sys/aryu8.smp
@@ -27,7 +27,7 @@ struct aryu8 {
// Add all of a plain array to the aryu8
val addAry = { u8[~] xs ->
- u32 n=xs#;
+ u32 n = xs#;
_grow(n);
ary.copy(buf,len,xs,0,n);
len += n;
@@ -60,7 +60,7 @@ val atoi = { u8[~] str ->
while( x < str# && str[x] <= ' ' ) x++;
if( x == str# ) return 0;
bool neg = false;
- if( str[x]=='-' ) { neg=true; x++; }
+ if( str[x] == '-' ) { neg = true; x++; }
i64 n = 0;
while( x < str# && '0' <= str[x] <= '9' )
n = n*10 + str[x++]-'0';
@@ -69,10 +69,7 @@ val atoi = { u8[~] str ->
// Zero-terminate a string for libc, reserving one array element for the zero.
{ u8[~] -> u8[] } str0 = { u8[~] str ->
- if( str# == 4294967295 ) libc._exit(1);
- u8[] !strz = new u8[str#+1];
- u32 len=str#;
- ary.copy(strz,0,str,0,len);
- strz[str#] = '\0';
- strz;
+ if( str# == sys.integer.MAX_U32 ) libc._exit(1);
+ // Copy, terminate and return
+ return new u8[str#+1]{i64 i -> i < str# ? str[i] : '\0'; };
};
diff --git a/src/main/smp/sys/audio.smp b/src/main/smp/sys/audio.smp
new file mode 100644
index 00000000..89e6dfa9
--- /dev/null
+++ b/src/main/smp/sys/audio.smp
@@ -0,0 +1,29 @@
+// Optional background audio. All API calls belong on the game/main thread.
+int OK = 0;
+int ERROR = -1;
+int BOOP = 1;
+int WIN = 2;
+int LOSE = 3;
+int RATE = 48000;
+{ i32 } simple_audio_open = "C";
+{ i64 u32 -> i32 } simple_audio_load = "C";
+{ i32 -> i32 } simple_audio_play = "C";
+{ i32 } simple_audio_stop = "C";
+{ i32 } simple_audio_playing = "C";
+{ i32 } simple_audio_close = "C";
+// Missing/disabled backend returns ERROR quietly; the game may remain silent.
+val open = { -> simple_audio_open(); };
+// Mono signed 16-bit PCM at RATE, up to ten seconds. Copies into native memory.
+// Load at startup: up to 28 custom handles, valid until close, then reload.
+val load = { i16[~] samples ->
+ i64 ptr = samples;
+ simple_audio_load(ptr,samples#);
+};
+// Starts/restarts immediately, replacing the previous sound; never waits for it.
+val play = { int sound -> simple_audio_play(sound); };
+val boop = { -> simple_audio_play(BOOP); };
+val win = { -> simple_audio_play(WIN); };
+val lose = { -> simple_audio_play(LOSE); };
+val stop = { -> simple_audio_stop(); };
+val playing = { -> simple_audio_playing(); };
+val close = { -> simple_audio_close(); };
diff --git a/src/main/smp/sys/draw.smp b/src/main/smp/sys/draw.smp
new file mode 100644
index 00000000..2a8b6117
--- /dev/null
+++ b/src/main/smp/sys/draw.smp
@@ -0,0 +1,18 @@
+// Software raster drawing. No window or native calls; pixels are packed RGB (0x00RRGGBB).
+u32 BLACK = 0x000000;
+u32 WHITE = 0xffffff;
+u32 RED = 0xff0000;
+u32 GREEN = 0x00ff00;
+u32 BLUE = 0x0000ff;
+
+// Components are clamped to bytes rather than wrapping.
+val rgb = { int r, int g, int b ->
+ r = r < 0 ? 0 : r > 255 ? 255 : r;
+ g = g < 0 ? 0 : g > 255 ? 255 : g;
+ b = b < 0 ? 0 : b > 255 ? 255 : b;
+ u32 color = (r<<16) | (g<<8) | b;
+ return color;
+};
+
+val Canvas = Canvas; // Pixel canvas and drawing methods
+val Button = Button; // Pointer state and rendering for a rectangular button
diff --git a/src/main/smp/sys/draw/Button.smp b/src/main/smp/sys/draw/Button.smp
new file mode 100644
index 00000000..c90ed2d3
--- /dev/null
+++ b/src/main/smp/sys/draw/Button.smp
@@ -0,0 +1,41 @@
+// Feed pointer state once per event-loop iteration. Activate on release inside,
+// only after a press that began inside. No native/window dependency.
+struct Button {
+ int x, y, width, height;
+ u8[~] !label;
+ bool !enabled = true, !hovered = false, !pressed = false;
+ bool !_held = false, !_armed = false;
+ new Button = { int px, int py, int w, int h, u8[~] text ->
+ x = px; y = py; width = w; height = h; label = text;
+ };
+ val contains = { i64 px, i64 py ->
+ if( width <= 0 || height <= 0 ) return false;
+ i64 right = x, bottom = y; right += width; bottom += height;
+ if( px < x || py < y || px >= right || py >= bottom ) return false;
+ return true;
+ };
+ val update = { i64 px, i64 py, bool down ->
+ hovered = false;
+ if( enabled ) hovered = contains(px,py);
+ if( !enabled ) _armed = false;
+ if( down && !_held ) _armed = hovered;
+ var clicked = false;
+ if( !down ) if( _held ) if( _armed ) if( hovered ) clicked = true;
+ if( !down ) _armed = false;
+ _held = down;
+ pressed = false;
+ if( down ) if( _armed ) if( hovered ) pressed = true;
+ return clicked;
+ };
+ val draw = { !sys.draw.Canvas screen, int scale, u32 foreground, u32 background, u32 active ->
+ screen.fillRect(x,y,width,height,enabled && (hovered || pressed) ? active : background);
+ screen.rect(x,y,width,height,2,enabled ? foreground : active);
+ if( scale > 0 ) {
+ i64 textWidth = label#; textWidth = (textWidth*4-1)*scale;
+ if( textWidth <= width-8 && 5*scale <= height-8 )
+ screen.text(x+(width-textWidth)/2,y+(height-5*scale)/2+(pressed ? 1 : 0),
+ label,scale,enabled ? foreground : active);
+ }
+ return self;
+ };
+};
diff --git a/src/main/smp/sys/draw/Canvas.smp b/src/main/smp/sys/draw/Canvas.smp
new file mode 100644
index 00000000..9ba3514a
--- /dev/null
+++ b/src/main/smp/sys/draw/Canvas.smp
@@ -0,0 +1,149 @@
+struct Canvas {
+ u32 width;
+ u32 height;
+ u32[] pixels;
+
+ new Canvas = { u32 w, u32 h ->
+ i64 count = w;
+ count *= h;
+ if( w <= 0 || h <= 0 || count > sys.integer.MAX_I32 ) libc._exit(1);
+ width = w;
+ height = h;
+ pixels = new u32[count];
+ };
+
+ val clear = { u32 color ->
+ for( int i = 0; i < pixels#; i++ ) pixels[i] = color;
+ return self;
+ };
+
+ val pixel = { i64 x, i64 y, u32 color ->
+ if( 0 <= x < width && 0 <= y < height ) pixels[y*width+x] = color;
+ return self;
+ };
+
+ // Integer coordinates, top-left origin. Rectangle extents are half-open.
+ val fillRect = { i64 x, i64 y, i64 w, i64 h, u32 color ->
+ if( w <= 0 || h <= 0 ) return self;
+ i64 right = x; right += w;
+ i64 bottom = y; bottom += h;
+ if( right <= 0 || bottom <= 0 || x >= width || y >= height ) return self;
+ int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y;
+ int x1 = right > width ? width : right, y1 = bottom > height ? height : bottom;
+ for( int py = y0; py < y1; py++ )
+ for( int px = x0; px < x1; px++ ) pixels[py*width+px] = color;
+ return self;
+ };
+
+ // Outline stays inside the rectangle. Wide borders become a filled rect.
+ val rect = { i64 x, i64 y, i64 w, i64 h, i64 thickness, u32 color ->
+ if( w <= 0 || h <= 0 || thickness <= 0 ) return self;
+ if( thickness >= w || thickness >= h || thickness >= w-thickness || thickness >= h-thickness )
+ return fillRect(x,y,w,h,color);
+ fillRect(x,y,w,thickness,color);
+ fillRect(x,y+h-thickness,w,thickness,color);
+ fillRect(x,y+thickness,thickness,h-2*thickness,color);
+ return fillRect(x+w-thickness,y+thickness,thickness,h-2*thickness,color);
+ };
+
+ // Integer centers are pixel centers. Circle outlines extend inward.
+ val fillCircle = { i64 cx, i64 cy, int radius, u32 color ->
+ return _circle(cx,cy,radius,0,true,color);
+ };
+ val circle = { i64 cx, i64 cy, int radius, int thickness, u32 color ->
+ return _circle(cx,cy,radius,thickness,false,color);
+ };
+ val _circle = { i64 cx, i64 cy, int radius, int thickness, bool filled, u32 color ->
+ if( radius < 0 || (!filled && thickness <= 0) ) return self;
+ i64 left = cx; left -= radius;
+ i64 top = cy; top -= radius;
+ i64 right = cx; right += radius;
+ i64 bottom = cy; bottom += radius;
+ if( right < 0 || bottom < 0 || left >= width || top >= height ) return self;
+ int x0 = left < 0 ? 0 : left, y0 = top < 0 ? 0 : top;
+ int x1 = right >= width ? width-1 : right, y1 = bottom >= height ? height-1 : bottom;
+ i64 outer2 = radius;
+ outer2 *= radius;
+ i64 inner = filled ? -1 : radius-thickness;
+ for( int py = y0; py <= y1; py++ )
+ for( int px = x0; px <= x1; px++ ) {
+ i64 dx = px; dx -= cx;
+ i64 dy = py; dy -= cy;
+ i64 d2 = dx*dx+dy*dy;
+ if( d2 <= outer2 && (inner <= 0 || d2 > inner*inner) ) pixels[py*width+px] = color;
+ }
+ return self;
+ };
+
+ // Round caps; a zero-length segment is a dot. Only the clipped bounding
+ // box is scanned. Floating distances avoid overflowing coordinate squares.
+ val line = { i64 x0, i64 y0, i64 x1, i64 y1, i64 thickness, u32 color ->
+ if( thickness <= 0 ) return self;
+ i64 pad = thickness/2+1;
+ i64 left = x0 < x1 ? x0 : x1; left -= pad;
+ i64 top = y0 < y1 ? y0 : y1; top -= pad;
+ i64 right = x0 > x1 ? x0 : x1; right += pad;
+ i64 bottom = y0 > y1 ? y0 : y1; bottom += pad;
+ if( right < 0 || bottom < 0 || left >= width || top >= height ) return self;
+ int bx0 = left < 0 ? 0 : left, by0 = top < 0 ? 0 : top;
+ int bx1 = right >= width ? width-1 : right, by1 = bottom >= height ? height-1 : bottom;
+ for( int py = by0; py <= by1; py++ )
+ for( int px = bx0; px <= bx1; px++ )
+ if( _onLine(px,py,x0,y0,x1,y1,thickness) ) pixels[py*width+px] = color;
+ return self;
+ };
+
+ // Small 3x5 bitmap font for game labels. Lowercase maps to uppercase.
+ // Newline returns to x; unsupported characters draw a question mark.
+ val text = { i64 x, i64 y, u8[~] str, int scale, u32 color ->
+ if( scale <= 0 ) return self;
+ i64 px = x, py = y;
+ for( int i = 0; i < str#; i++ ) {
+ u8 c = str[i];
+ if( c == '\n' ) { px = x; py += 6*scale; continue; }
+ int bits = _glyph(c);
+ for( int row = 0; row < 5; row++ )
+ for( int col = 0; col < 3; col++ )
+ if( (bits >> (14-row*3-col)) & 1 )
+ fillRect(px+col*scale,py+row*scale,scale,scale,color);
+ px += 4*scale;
+ }
+ return self;
+ };
+};
+
+// An old-school 3x5 font, ASCII 32..127. Five octal row digits need u16.
+// Lowercase uses uppercase shapes; unsupported characters use '?'.
+val _glyphs = new u16[~96]{
+ 000000,022202,055000,057575,061202,051245,061202,022000, // space..'
+ 012221,042224,005250,002720,000024,000700,000002,011244, // (../
+ 075557,026227,061247,061216,055711,074616,034757,071222, // 0..7
+ 075757,075716,002020,002024,012421,007070,042124,061202, // 8..?
+ 061202,025755,065656,034443,065556,074647,074644,034553, // @..G
+ 055755,072227,011152,055655,044447,057755,057775,025552, // H..O
+ 065644,025573,065655,034216,072222,055557,055552,055775, // P..W
+ 055255,055222,071247,032223,044211,062226,025000,000007, // X.._
+ 042000,025755,065656,034443,065556,074647,074644,034553, // `..g
+ 055755,072227,011152,055655,044447,057755,057775,025552, // h..o
+ 065644,025573,065655,034216,072222,055557,055552,055775, // p..w
+ 055255,055222,071247,032423,022222,062126,003600,061202, // x..DEL
+};
+
+val _glyph = { u8 c ->
+ return 32 <= c < 128 ? _glyphs[c-32] : 061202;
+};
+
+// Pixel coverage for a round-ended segment, separate from traversal.
+val _onLine = { i64 px, i64 py, i64 x0, i64 y0, i64 x1, i64 y1, i64 thickness ->
+ f64 dx = x1; dx -= x0;
+ f64 dy = y1; dy -= y0;
+ f64 vx = px; vx -= x0;
+ f64 vy = py; vy -= y0;
+ f64 length2 = dx*dx+dy*dy;
+ bool point = x0 == x1 && y0 == y1;
+ f64 t = point ? 0.0 : (vx*dx+vy*dy)/length2;
+ t = t < 0 ? 0.0 : t > 1 ? 1.0 : t;
+ f64 ex = vx-t*dx, ey = vy-t*dy;
+ f64 half = thickness/2.0;
+ return ex*ex+ey*ey <= half*half;
+};
diff --git a/src/main/smp/sys/gfx.smp b/src/main/smp/sys/gfx.smp
new file mode 100644
index 00000000..ae203bd1
--- /dev/null
+++ b/src/main/smp/sys/gfx.smp
@@ -0,0 +1,44 @@
+// Minimal pixel-window interface; drawing belongs in Simple.
+// One fixed-size window, main thread only. Pixel format is 0x00RRGGBB.
+// open/present/poll return OK, CLOSED (normal user close/Esc), or ERROR.
+int OK = 0;
+int CLOSED = 1;
+int ERROR = -1;
+int LEFT = 1;
+int ESCAPE = 256;
+
+{ i64 u32 u32 -> i32 } simple_gfx_open = "C";
+{ i64 u32 -> i32 } simple_gfx_present = "C";
+{ i32 -> i32 } simple_gfx_poll = "C";
+{ i32 } simple_gfx_mouse_x = "C";
+{ i32 } simple_gfx_mouse_y = "C";
+{ i32 -> i32 } simple_gfx_button = "C";
+{ i32 -> i32 } simple_gfx_key = "C";
+{ i32 } simple_gfx_close = "C";
+{ i64 -> i32 } simple_gfx_wait = "C";
+
+// Failure prints a diagnostic to stderr and returns ERROR; callers decide
+// whether to exit or fall back to text. No native library is loaded until here.
+val open = { u8[~] title, u32 width, u32 height ->
+ i64 ptr = aryu8.str0(title);
+ simple_gfx_open(ptr,width,height);
+};
+
+// Array length must be width*height. The native layer copies for OS repaints;
+// it never keeps a pointer into the Simple heap after this call returns.
+val present = { u32[~] pixels ->
+ i64 ptr = pixels;
+ simple_gfx_present(ptr,pixels#);
+};
+
+// wait=true waits up to 17ms, waking early for input to avoid a busy loop.
+val poll = { bool wait -> simple_gfx_poll(wait); };
+val mouseX = { -> simple_gfx_mouse_x(); };
+val mouseY = { -> simple_gfx_mouse_y(); };
+val button = { int button -> simple_gfx_button(button); };
+// Printable keys use uppercase ASCII; special keys use MiniFB's key codes.
+val key = { int key -> simple_gfx_key(key); };
+val close = { -> simple_gfx_close(); };
+// Wait for input or at most timeout milliseconds; 0 polls, -1 waits indefinitely.
+// This blocks the game thread, dispatches window events, and may wake early.
+val wait = { i64 timeout_ms -> simple_gfx_wait(timeout_ms); };
diff --git a/src/main/smp/sys/integer.smp b/src/main/smp/sys/integer.smp
new file mode 100644
index 00000000..a839e597
--- /dev/null
+++ b/src/main/smp/sys/integer.smp
@@ -0,0 +1,6 @@
+// Integer bounds.
+
+i64 MIN_I64 = 1<<63;
+i64 MAX_I64 = -(MIN_I64+1);
+i32 MAX_I32 = (1<<31)-1;
+u32 MAX_U32 = (1<<32)-1;
diff --git a/src/main/smp/sys/io.smp b/src/main/smp/sys/io.smp
index 14993429..6269cde5 100644
--- a/src/main/smp/sys/io.smp
+++ b/src/main/smp/sys/io.smp
@@ -21,7 +21,7 @@ val read = { i32 fd ->
return null;
}
if( rez == 0 ) break;
- if( buf.len+rez > 4294967295 ) {
+ if( buf.len+rez > sys.integer.MAX_U32 ) {
libc.set_errno(libc.EOVERFLOW);
return null;
}
diff --git a/src/main/smp/sys/libc.smp b/src/main/smp/sys/libc.smp
index 7122bbd8..8e417461 100644
--- a/src/main/smp/sys/libc.smp
+++ b/src/main/smp/sys/libc.smp
@@ -53,9 +53,9 @@ i32 EOVERFLOW = "C";
// addr len prot flags fd off -> void*
{ i64 i64 i32 i32 i32 i32 -> i64 } mmap = "C";
// mmap flags
-i32 PROT_EXEC = 4;
-i32 PROT_READ = 1;
-i32 PROT_WRITE= 2;
-i32 PROT_NONE = 0;
+i32 PROT_EXEC = 4;
+i32 PROT_READ = 1;
+i32 PROT_WRITE = 2;
+i32 PROT_NONE = 0;
i32 MAP_PRIVATE = 2;
i32 MAP_ANON = 32;
diff --git a/src/main/smp/sys/math.smp b/src/main/smp/sys/math.smp
new file mode 100644
index 00000000..a5e1887c
--- /dev/null
+++ b/src/main/smp/sys/math.smp
@@ -0,0 +1,15 @@
+// Mathematical constants, rounded to the nearest f64 value.
+val PI = 3.141592653589793;
+val E = 2.718281828459045;
+
+// Fixed-semantics C bindings recognized by the compiler's intrinsic inliner.
+// These preserve all 64 bits, including signed zero and NaN payloads.
+// They are NOT numeric conversions (ordinary typed assignments still are).
+{ f64 -> i64 } simple_f64_bits = "C";
+{ i64 -> f64 } simple_f64_from_bits = "C";
+val f64Bits = simple_f64_bits;
+val f64FromBits = simple_f64_from_bits;
+
+
+val max = { f64 x, f64 y -> return x > y ? x : y; };
+val min = { f64 x, f64 y -> return x < y ? x : y; };
diff --git a/src/main/smp/sys/rt/simple_args.smp b/src/main/smp/sys/rt/simple_args.smp
index 3f630e54..8bfcc1a7 100644
--- a/src/main/smp/sys/rt/simple_args.smp
+++ b/src/main/smp/sys/rt/simple_args.smp
@@ -1,18 +1,20 @@
// Native startup support, compiled independently and linked with the CRT.
// Raw native addresses stay i64; only the arrays allocated here are Simple objects.
+// This startup object is built without sys imports.
+u32 _MAX_U32 = (1<<32)-1;
{ i64 i64 -> i64 } simple_argv = "C";
{ i64 -> i64 } strlen = "C";
{ i64 i64 i64 -> i64 } memcpy = "C";
val simple_args = { i64 argc, i64 argv ->
- if( argc < 0 || argc > 4294967295 ) return null;
+ if( argc < 0 || argc > _MAX_U32 ) return null;
// Zero-filled reference arrays require nullable elements. Every slot is
// filled before returning; the native entry bridge publishes u8[~][].
val args = new u8[~]?[argc];
- for( i64 i=0; i<argc; i++ ) {
+ for( i64 i = 0; i < argc; i++ ) {
i64 src = simple_argv(argv,i);
i64 len = strlen(src);
- if( len < 0 || len > 4294967295 ) return null;
+ if( len < 0 || len > _MAX_U32 ) return null;
val bytes = new u8[len];
i64 dst = bytes;
memcpy(dst,src,len);
diff --git a/src/main/smp/sys/text.smp b/src/main/smp/sys/text.smp
index a414ff45..43698f22 100644
--- a/src/main/smp/sys/text.smp
+++ b/src/main/smp/sys/text.smp
@@ -5,78 +5,75 @@
val begin = { -> new aryu8(); };
val end = { aryu8 b ->
- val bytes = new u8[b.len];
- ary.copy(bytes,0,b.buf,0,b.len);
- u8[~] result = bytes;
- result;
+ new u8[~b.len]{ i64 i -> b.buf[i]; };
};
val _pad = { !aryu8 b, i64 n, u8 c ->
- for( i64 i=0; i<n; i++ ) b.add(c);
+ for( i64 i = 0; i < n; i++ ) b.add(c);
b;
};
val string = { !aryu8 b, u8[~] s, i64 spec, i64 width, i64 precision ->
- i64 len=s#;
- if( precision>=0 && precision<len ) len=precision;
- i64 pad=width-len;
+ i64 len = s#;
+ if( precision >= 0 && precision < len ) len = precision;
+ i64 pad = width-len;
if( !(spec&256) ) _pad(b,pad,' ');
- for( i64 i=0; i<len; i++ ) b.add(s[i]);
+ for( i64 i = 0; i < len; i++ ) b.add(s[i]);
if( spec&256 ) _pad(b,pad,' ');
b;
};
val integer = { !aryu8 b, i64 value, i64 spec, i64 width, i64 precision ->
- i64 kind=spec&255;
- if( kind=='b' ) return string(b,value ? "true" : "false",spec,width,-1);
- if( kind=='c' ) {
+ i64 kind = spec&255;
+ if( kind == 'b' ) return string(b,value ? "true" : "false",spec,width,-1);
+ if( kind == 'c' ) {
if( !(spec&256) ) _pad(b,width-1,' ');
- u8 c=value;
+ u8 c = value;
b.add(c);
if( spec&256 ) _pad(b,width-1,' ');
return b;
}
- bool signed=kind==0 || kind=='d' || kind=='i';
- i64 base=kind=='o' ? 8 : (kind=='x' || kind=='X' ? 16 : 10);
- u8[~] alphabet=kind=='X' ? "0123456789ABCDEF" : "0123456789abcdef";
- val digits=new u8[22]; // Maximum: 22 octal digits for a 64-bit integer.
- i64 count=0, n=value;
+ bool signed = kind == 0 || kind == 'd' || kind == 'i';
+ i64 base = kind == 'o' ? 8 : (kind == 'x' || kind == 'X' ? 16 : 10);
+ u8[~] alphabet = kind == 'X' ? "0123456789ABCDEF" : "0123456789abcdef";
+ val digits = new u8[22]; // Maximum: 22 octal digits for a 64-bit integer.
+ i64 count = 0, n = value;
// Signed decimal stays nonpositive, including the minimum i64.
- if( signed && n>0 ) n=-n;
- if( n==0 && precision!=0 ) digits[count++]='0';
- while( n!=0 ) {
- i64 q=0, r=0;
- if( signed ) { q=n/base; r=q*base-n; }
+ if( signed && n > 0 ) n = -n;
+ if( n == 0 && precision != 0 ) digits[count++] = '0';
+ while( n != 0 ) {
+ i64 q = 0, r = 0;
+ if( signed ) { q = n/base; r = q*base-n; }
else {
// Unsigned division without a u64 type. Halving first makes
// the dividend nonnegative; the remainder needs at most one fix.
- q=((n>>>1)/base)*2;
- r=n-q*base;
- if( r>=base ) { q++; r-=base; }
+ q = ((n>>>1)/base)*2;
+ r = n-q*base;
+ if( r >= base ) { q++; r -= base; }
}
- digits[count++]=alphabet[r];
- n=q;
+ digits[count++] = alphabet[r];
+ n = q;
}
- u8[~] !prefix="";
+ u8[~] !prefix = "";
if( signed ) {
- if( value<0 ) prefix="-";
- else if( spec&512 ) prefix="+";
- else if( spec&1024 ) prefix=" ";
- } else if( (spec&2048) && value!=0 ) {
- if( kind=='x' ) prefix="0x";
- if( kind=='X' ) prefix="0X";
+ if( value < 0 ) prefix = "-";
+ else if( spec&512 ) prefix = "+";
+ else if( spec&1024 ) prefix = " ";
+ } else if( (spec&2048) && value != 0 ) {
+ if( kind == 'x' ) prefix = "0x";
+ if( kind == 'X' ) prefix = "0X";
}
- i64 zeros=precision>count ? precision-count : 0;
+ i64 zeros = precision > count ? precision-count : 0;
// Alternate octal always begins with a zero, including precision-zero 0.
- if( kind=='o' && (spec&2048) && zeros==0 && (count==0 || digits[count-1]!='0') ) zeros=1;
- i64 pad=width-prefix#-zeros-count;
- bool left=(spec&256)!=0;
- bool zero=(spec&4096)!=0 && precision<0 && !left;
+ if( kind == 'o' && (spec&2048) && zeros == 0 && (count == 0 || digits[count-1] != '0') ) zeros = 1;
+ i64 pad = width-prefix#-zeros-count;
+ bool left = (spec&256) != 0;
+ bool zero = (spec&4096) != 0 && precision < 0 && !left;
if( !left && !zero ) _pad(b,pad,' ');
b.addAry(prefix);
if( zero ) _pad(b,pad,'0');
_pad(b,zeros,'0');
- while( count>0 ) b.add(digits[--count]);
+ while( count > 0 ) b.add(digits[--count]);
if( left ) _pad(b,pad,' ');
b;
};
@@ -96,3 +93,8 @@ val f64 = { f64 x -> end(floating(begin(),x, 0 ,0,-1)); };
val char = { i64 x -> end(integer (begin(),x,'c',0,-1)); };
val bool = { bool x -> end(integer (begin(),x,'b',0,-1)); };
val hex = { i64 x -> end(integer (begin(),x,'x',0,-1)); };
+
+// Time selectors occupy spec bits 16..23; padding shares string formatting.
+val time = { !aryu8 b, i64 value, i64 spec, i64 width, i64 precision ->
+ string(b,sys.time.format(value,(spec>>>16)&255),spec,width,-1);
+};
diff --git a/src/main/smp/sys/time.smp b/src/main/smp/sys/time.smp
new file mode 100644
index 00000000..a3e9b3a2
--- /dev/null
+++ b/src/main/smp/sys/time.smp
@@ -0,0 +1,219 @@
+// Time utilities; get the current time (both nanos and millis), sleep, time zones.
+
+// Instants: bare i64 UTC Unix-epoch milliseconds. Durations: i64 milliseconds.
+// Monotonic readings are a separate clock domain, for deadlines/elapsed time.
+
+val DateTime = DateTime;
+val Timers = Timers;
+i64 SECOND = 1000;
+i64 MINUTE = 60*SECOND;
+i64 HOUR = 60*MINUTE;
+i64 DAY = 24*HOUR;
+i64 NANOS_PER_MS = 1000*1000;
+
+// Gregorian leap days: every fourth year, except centuries not divisible by 400.
+i64 _DAYS_PER_4_YEARS = 4*365+1;
+i64 _DAYS_PER_CENTURY = 100*365+100/4-1;
+i64 _DAYS_PER_ERA = 4*_DAYS_PER_CENTURY+1;
+// Complete years before the Unix epoch and after the last supported date.
+i64 _DAYS_BEFORE_EPOCH = 1969*365+1969/4-1969/100+1969/400;
+i64 _DAYS_BEFORE_YEAR_10000 = 9999*365+9999/4-9999/100+9999/400;
+// The March-based calendar algorithm starts at 0000-03-01, 306 days before 0001-01-01.
+i64 _MARCH_EPOCH_DAYS = _DAYS_BEFORE_EPOCH+306;
+i64 MIN = -_DAYS_BEFORE_EPOCH*DAY; // 0001-01-01T00:00:00.000Z
+i64 MAX = (_DAYS_BEFORE_YEAR_10000-_DAYS_BEFORE_EPOCH)*DAY-1; // 9999-12-31T23:59:59.999Z
+
+// Access to the underlying LIBC functions
+int ISO = 0, CLOCK12 = 1, CLOCK24 = 2, ELAPSED = 3, DATE = 4, CLOCK24_SECONDS = 5;
+{ i64 } simple_time_now = "C";
+{ i64 } simple_time_mono = "C";
+{ i64 } simple_time_nano = "C";
+{ i64 i32 -> i32 } simple_time_sleep = "C";
+{ i64 i64 -> i32 } simple_time_local = "C";
+{ i64 i64 u32 -> i32 } simple_time_zone = "C";
+val now = { -> simple_time_now (); };
+val monotonic = { -> simple_time_mono(); };
+val nanoTime = { -> simple_time_nano(); };
+val sleep = { i64 duration_ms -> simple_time_sleep(duration_ms,0); };
+val sleepUntil = { i64 deadline_ms -> simple_time_sleep(deadline_ms,1); };
+
+// Signed duration conversions. Overflow returns -1; reverse conversions truncate
+// toward zero. An exact duration day is 24 hours, independent of DST.
+val _scale = { i64 n, i64 unit ->
+ if( n > sys.integer.MAX_I64/unit || n < sys.integer.MIN_I64/unit ) return -1;
+ return n*unit;
+};
+val seconds = { i64 n -> _scale(n,SECOND); };
+val minutes = { i64 n -> _scale(n,MINUTE); };
+val hours = { i64 n -> _scale(n,HOUR); };
+val days = { i64 n -> _scale(n,DAY); };
+val toSeconds = { i64 ms -> ms/SECOND; };
+val toMinutes = { i64 ms -> ms/MINUTE; };
+val toHours = { i64 ms -> ms/HOUR; };
+val toDays = { i64 ms -> ms/DAY; };
+val fromNanos = { i64 ns -> ns/NANOS_PER_MS; };
+val toNanos = { i64 ms -> _scale(ms,NANOS_PER_MS); };
+
+// Clamp to zero once due, and saturate for widely separated i64 readings.
+val remaining = { i64 deadline_ms, i64 current_ms ->
+ if( deadline_ms <= current_ms ) return 0;
+ if( current_ms < 0 && deadline_ms > sys.integer.MAX_I64+current_ms ) return sys.integer.MAX_I64;
+ return deadline_ms-current_ms;
+};
+
+val _mod = { i64 n, i64 unit -> n-n/unit*unit; };
+val leap = { int year -> _mod(year,4) == 0 && (_mod(year,100) != 0 || _mod(year,400) == 0); };
+val monthDays = { int year, int month ->
+ if( month < 1 || month > 12 ) return 0;
+ return month == 2 ? (leap(year) ? 29 : 28) :
+ month == 4 || month == 6 || month == 9 || month == 11 ? 30 : 31;
+};
+
+// Gregorian civil dates, expressed using 400-year eras; no per-year loops.
+val _days = { i64 year, i64 month, i64 day ->
+ year -= month <= 2 ? 1 : 0;
+ i64 era = year/400, y = year-era*400;
+ i64 mp = month+(month > 2 ? -3 : 9);
+ return era*_DAYS_PER_ERA+y*365+y/4-y/100+(153*mp+2)/5+day-1-_MARCH_EPOCH_DAYS;
+};
+
+val valid = { int year, int month, int day, int hour, int minute, int second, int millisecond ->
+ return year >= 1 && year <= 9999 && month >= 1 && month <= 12 && day >= 1 && day <= monthDays(year,month) &&
+ hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59 && second >= 0 && second <= 59 &&
+ millisecond >= 0 && millisecond <= 999;
+};
+
+val makeUTC = { int year, int month, int day, int hour, int minute, int second, int millisecond ->
+ if( !valid(year,month,day,hour,minute,second,millisecond) ) return -1;
+ return _days(year,month,day)*DAY+hour*HOUR+minute*MINUTE+second*SECOND+millisecond;
+};
+
+val utc = { i64 instant ->
+ val d = DateTime.empty();
+ if( instant < MIN || instant > MAX ) return d;
+ i64 day = instant/DAY, rest = _mod(instant,DAY);
+ if( rest < 0 ) { rest += DAY; day--; }
+ i64 z = day+_MARCH_EPOCH_DAYS, era = z/_DAYS_PER_ERA, doe = z-era*_DAYS_PER_ERA;
+ i64 y = (doe-doe/(_DAYS_PER_4_YEARS-1)+doe/_DAYS_PER_CENTURY-doe/(_DAYS_PER_ERA-1))/365;
+ i64 doy = doe-(365*y+y/4-y/100), mp = (5*doy+2)/153;
+ d.day = doy-(153*mp+2)/5+1;
+ d.month = mp+(mp < 10 ? 3 : -9);
+ d.year = y+era*400+(d.month <= 2 ? 1 : 0);
+ d.hour = rest/HOUR; d.minute = _mod(rest/MINUTE,60);
+ d.second = _mod(rest/SECOND,60); d.millisecond = _mod(rest,SECOND);
+ d.weekday = _mod(day+4,7);
+ if( d.weekday < 0 ) d.weekday += 7;
+ return d;
+};
+
+val local = { i64 instant ->
+ val d = DateTime.empty();
+ val f = new i64[10];
+ i64 ptr = f;
+ if( simple_time_local(instant,ptr) != 0 ) return d;
+ d.year = f[0]; d.month = f[1]; d.day = f[2];
+ d.hour = f[3]; d.minute = f[4]; d.second = f[5]; d.millisecond = f[6];
+ d.weekday = f[7]; d.offsetSeconds = f[8]; d.daylight = f[9] != 0;
+ return d;
+};
+
+val zoneName = { i64 instant ->
+ val bytes = new u8[128];
+ i64 ptr = bytes;
+ int n = simple_time_zone(instant,ptr,bytes#);
+ if( n < 0 ) return "";
+ val b = new aryu8();
+ for( int i = 0; i < n; i++ ) b.add(bytes[i]);
+ return text.end(b);
+};
+
+val _digits = { !aryu8 b, i64 n, int width ->
+ text.integer(b,n,'d'|4096,width,-1);
+};
+
+val _format = { i64 instant, int style, bool localZone ->
+ val d = localZone ? local(instant) : utc(instant);
+ if( d.year == 0 || style < 0 || style > 5 || style == 3 ) return "<invalid time>";
+ val b = new aryu8();
+ if( style == 4 ) {
+ _digits(b,d.day,2).add('/'); _digits(b,d.month,2).add('/'); _digits(b,_mod(d.year,100),2);
+ } else {
+ if( style == 0 ) {
+ _digits(b,d.year,4).add('-'); _digits(b,d.month,2).add('-'); _digits(b,d.day,2).add('T');
+ }
+ int hour = d.hour;
+ if( style == 1 ) { hour = _mod(hour,12); if( hour == 0 ) hour = 12; }
+ _digits(b,hour,style == 1 ? 1 : 2).add(':'); _digits(b,d.minute,2);
+ if( style == 0 || style == 5 ) { b.add(':'); _digits(b,d.second,2); }
+ if( style == 1 ) b.addAry(d.hour < 12 ? "am" : "pm");
+ if( style == 0 ) {
+ b.add('.'); _digits(b,d.millisecond,3);
+ if( d.offsetSeconds == 0 ) b.add('Z');
+ else {
+ i64 offset = d.offsetSeconds;
+ b.add(offset < 0 ? '-' : '+');
+ if( offset < 0 ) offset = -offset;
+ _digits(b,offset/3600,2).add(':'); _digits(b,_mod(offset/60,60),2);
+ if( _mod(offset,60) != 0 ) { b.add(':'); _digits(b,_mod(offset,60),2); }
+ }
+ }
+ }
+ return text.end(b);
+};
+
+val elapsed = { i64 ms ->
+ val b = new aryu8();
+ // Nonpositive arithmetic handles the minimum i64 without negating it.
+ if( ms < 0 ) b.add('-'); else ms = -ms;
+ _digits(b,-(ms/MINUTE),2).add(':');
+ _digits(b,-_mod(ms/SECOND,60),2).add('.'); _digits(b,-_mod(ms,SECOND),3);
+ return text.end(b);
+};
+val format = { i64 instant, int style ->
+ if( style == ELAPSED ) return elapsed(instant);
+ return _format(instant,style,false);
+};
+val formatLocal = { i64 instant, int style ->
+ if( style == ELAPSED ) return elapsed(instant);
+ return _format(instant,style,true);
+};
+
+val _number = { u8[~] s, i64 at, int count ->
+ if( at < 0 || count < 1 || at+count > s# ) return -1;
+ int n = 0;
+ for( int i = 0; i < count; i++ ) {
+ int c = s[at+i];
+ if( c < '0' || c > '9' ) return -1;
+ n = n*10+c-'0';
+ }
+ return n;
+};
+
+// Keep suffix validation separate from calendar construction.
+val _parseZone = { u8[~] s, int at, i64 instant ->
+ if( at < 0 || at >= s# ) return -1;
+ i64 offset = 0;
+ if( s[at] == 'Z' ) { if( at+1 != s# ) return -1; }
+ else {
+ int sign = s[at] == '+' ? 1 : s[at] == '-' ? -1 : 0;
+ if( !sign || (s# != at+6 && s# != at+9) || s[at+3] != ':' ) return -1;
+ int oh = _number(s,at+1,2), om = _number(s,at+4,2), os = 0;
+ if( s# == at+9 ) { if( s[at+6] != ':' ) return -1; os = _number(s,at+7,2); }
+ if( oh < 0 || oh > 23 || om < 0 || om > 59 || os < 0 || os > 59 ) return -1;
+ offset = sign*(oh*HOUR+om*MINUTE+os*SECOND);
+ }
+ instant -= offset;
+ return instant < MIN || instant > MAX ? -1 : instant;
+};
+
+// Strict ISO absolute instants only. -1 is reserved for failure (and therefore
+// cannot distinguish the valid instant one millisecond before Unix epoch).
+val parse = { u8[~] s ->
+ if( s# < 20 || s[4] != '-' || s[7] != '-' || s[10] != 'T' || s[13] != ':' || s[16] != ':' ) return -1;
+ int y = _number(s, 0,4), m = _number(s, 5,2), d = _number(s, 8,2);
+ int h = _number(s,11,2), min = _number(s,14,2), sec = _number(s,17,2);
+ bool fraction = s[19] == '.';
+ int ms = fraction ? _number(s,20,3) : 0;
+ if( !valid(y,m,d,h,min,sec,ms) ) return -1;
+ return _parseZone(s,fraction ? 23 : 19,makeUTC(y,m,d,h,min,sec,ms));
+};
diff --git a/src/main/smp/sys/time/DateTime.smp b/src/main/smp/sys/time/DateTime.smp
new file mode 100644
index 00000000..53b6688e
--- /dev/null
+++ b/src/main/smp/sys/time/DateTime.smp
@@ -0,0 +1,13 @@
+// Calendar view of an instant. Month/day start at 1; weekday is Sunday=0.
+// year==0 means out of the supported calendar range (years 1 through 9999).
+
+// All values default zero/false
+struct DateTime {
+ int year, month, day;
+ int hour, minute, second, millisecond;
+ int weekday, offsetSeconds;
+ bool daylight;
+};
+
+// Factory in the defining unit, so callers need no forward instance type.
+val empty = { -> new DateTime(); };
diff --git a/src/main/smp/sys/time/Timers.smp b/src/main/smp/sys/time/Timers.smp
new file mode 100644
index 00000000..7cca2e9c
--- /dev/null
+++ b/src/main/smp/sys/time/Timers.smp
@@ -0,0 +1,80 @@
+// Cooperative timers. poll returns IDs; callbacks never run on another thread.
+// IDs are u32; 0 means failure or no timer due. Millisecond values use _ms.
+
+// The
+
+
+struct Timers {
+ // Collection of timers (as struct-of-arrays)
+ u32[] !_ids = new u32[8]; // Which timer id for this slot
+ i64[] !_due_ms = new i64[8]; // Monotonic deadline in milliseconds
+ i64[] !_period_ms = new i64[8]; // When does it repeat
+ // Next available timer ID; 0 means all IDs have been used.
+ u32 !_next = 1;
+
+ // Add a new timer
+ { i64 i64 i64 -> u32 } _add = { i64 current_ms, i64 delay_ms, i64 period_ms ->
+ if( current_ms < 0 || delay_ms < 0 || current_ms > sys.integer.MAX_I64-delay_ms || _next == 0 ) return 0;
+ // Ugly linear scan for an available slot
+ int slot = 0;
+ while( slot < _ids# && _ids[slot] != 0 ) slot++;
+ // Double size for lots of timers
+ if( slot == _ids# ) {
+ if( _ids# > sys.integer.MAX_I32/2 ) return 0;
+ val ids = new u32[_ids#*2];
+ val due_ms = new i64[_ids#*2], periods_ms = new i64[_ids#*2];
+ for( int i = 0; i < _ids#; i++ ) { ids[i] = _ids[i]; due_ms[i] = _due_ms[i]; periods_ms[i] = _period_ms[i]; }
+ _ids = ids; _due_ms = due_ms; _period_ms = periods_ms;
+ // Retry against the newly published arrays after growing.
+ return _add(current_ms,delay_ms,period_ms);
+ }
+ // Next available timer id
+ u32 id = _next;
+ _next = id == sys.integer.MAX_U32 ? 0 : id+1;
+ _ids[slot] = id; _due_ms[slot] = current_ms+delay_ms; _period_ms[slot] = period_ms;
+ return id;
+ };
+
+ // Explicit-clock versions make game logic and tests deterministic.
+ { i64 i64 -> u32 } afterAt = { i64 current_ms, i64 delay_ms -> _add(current_ms,delay_ms,0); };
+ { i64 i64 -> u32 } everyAt = { i64 current_ms, i64 period_ms -> period_ms <= 0 ? 0 : _add(current_ms,period_ms,period_ms); };
+ { i64 -> u32 } after = { i64 delay_ms -> afterAt(time.monotonic(),delay_ms ); };
+ { i64 -> u32 } every = { i64 period_ms -> everyAt(time.monotonic(),period_ms); };
+
+ // Remove a timer
+ val cancel = { u32 id ->
+ if( id == 0 ) return false;
+ for( int i = 0; i < _ids#; i++ ) if( _ids[i] == id ) { _ids[i] = 0; return true; }
+ return false;
+ };
+
+ // Earliest absolute monotonic deadline in milliseconds, or -1 when empty.
+ val nextDeadline = { ->
+ i64 next_ms = -1;
+ // Ugly linear scan again, but short
+ for( int i = 0; i < _ids#; i++ )
+ if( _ids[i] != 0 && (next_ms < 0 || _due_ms[i] < next_ms) ) next_ms = _due_ms[i];
+ return next_ms;
+ };
+
+ // Next firing timer.
+
+ // Earliest deadline first; equal deadlines use registration order.
+ // Repeats skip missed ticks and retain their phase, avoiding bursts/drift.
+ { i64 -> u32 } poll = { i64 current_ms ->
+ int slot = -1;
+ for( int i = 0; i < _ids#; i++ )
+ if( _ids[i] != 0 && _due_ms[i] <= current_ms && (slot < 0 || _due_ms[i] < _due_ms[slot] ||
+ (_due_ms[i] == _due_ms[slot] && _ids[i] < _ids[slot])) ) slot = i;
+ if( slot < 0 ) return 0;
+ u32 id = _ids[slot];
+ i64 period_ms = _period_ms[slot];
+ if( period_ms == 0 ) _ids[slot] = 0;
+ else {
+ i64 steps = (current_ms-_due_ms[slot])/period_ms;
+ if( steps >= (sys.integer.MAX_I64-_due_ms[slot])/period_ms ) _ids[slot] = 0;
+ else _due_ms[slot] += (steps+1)*period_ms;
+ }
+ return id;
+ };
+};
diff --git a/src/test/c/audio_test.c b/src/test/c/audio_test.c
new file mode 100644
index 00000000..f82504c3
--- /dev/null
+++ b/src/test/c/audio_test.c
@@ -0,0 +1,48 @@
+// Explicit optional integration checks: real miniaudio thread, silent device.
+#define SIMPLE_AUDIO_TEST_NULL
+#include "../../main/c/simple_miniaudio.c"
+#include <assert.h>
+
+int main(void) {
+ assert(api.play(1)==-1 && api.playing()==0);
+ assert(api.stop()==0);
+ api.close();
+ for (int pass=0;pass<2;++pass) {
+ assert(api.open()==0 && api.open()==0);
+ assert(api.load(NULL,1)==-1);
+ int16_t pcm[RATE]={0};
+ assert(api.load(pcm,0)==-1 && api.load(pcm,RATE*10+1)==-1);
+ int sound=api.load(pcm,RATE);
+ assert(sound==4);
+ pcm[0]=12345;
+ assert(sounds[sound].pcm[0]==0);
+ assert(api.play(-1)==-1 && api.play(0)==-1 && api.play(SLOTS)==-1);
+ assert(api.play(sound)==0 && api.playing());
+ // A one-second sound returns while playback is still in progress.
+ assert(api.stop()==0 && !api.playing());
+ for (int cue=1;cue<=3;++cue) {
+ assert(api.play(cue)==0);
+ int elapsed=0;
+ while (api.playing() && elapsed<2000) { ma_sleep(10); elapsed+=10; }
+ assert(!api.playing());
+ }
+ for (int i=5;i<SLOTS;++i) assert(api.load(pcm,1)==i);
+ assert(api.load(pcm,1)==-1);
+ assert(api.play(sound)==0);
+ api.close(); // Join during playback, then reopen next pass.
+ api.close();
+ assert(api.play(sound)==-1 && !api.playing());
+ }
+ // The loss cue reverses exactly the same three softly enveloped notes.
+ assert(tone(1,0)==0 && tone(2,0)==0 && tone(3,1)==0);
+ uint32_t note=RATE*16/100;
+ for (int n=0;n<3;++n)
+ assert(memcmp(sounds[2].pcm+n*note,sounds[3].pcm+(2-n)*note,note*sizeof(int16_t))==0);
+ for (int i=1;i<=3;++i) {
+ assert(sounds[i].pcm[0]==0);
+ for (uint32_t j=0;j<sounds[i].count;++j) assert(abs(sounds[i].pcm[j])<=3200);
+ free(sounds[i].pcm); sounds[i].pcm=NULL;
+ }
+ puts("audio native checks passed");
+ return 0;
+}
diff --git a/src/test/java/com/seaofnodes/simple/ArithNodeTest.java b/src/test/java/com/seaofnodes/simple/ArithNodeTest.java
index 1e6c6fc0..b29c9706 100644
--- a/src/test/java/com/seaofnodes/simple/ArithNodeTest.java
+++ b/src/test/java/com/seaofnodes/simple/ArithNodeTest.java
@@ -65,7 +65,7 @@ public class ArithNodeTest {
TypePtr.gather(inputs);
TypeMemPtr.gather(inputs);
TypeFunPtr.gather(inputs);
- inputs.add(TypeConAryB.make(new byte[]{1,2,3}));
+ inputs.add(TypeConAry.make(new byte[]{1,2,3}));
for( long value : new long[]{Long.MIN_VALUE,Long.MIN_VALUE+1,-1,1,Long.MAX_VALUE} )
inputs.add(TypeInteger.constant(value));
for( double value : new double[]{-0.0,0.0,-1.0,1.0,16777216.0,16777217.0,Double.NaN,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY} )
diff --git a/src/test/java/com/seaofnodes/simple/ArrayInitTest.java b/src/test/java/com/seaofnodes/simple/ArrayInitTest.java
new file mode 100644
index 00000000..d298828e
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/ArrayInitTest.java
@@ -0,0 +1,211 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.node.ArrayInitNode;
+import com.seaofnodes.simple.node.ConstantNode;
+import com.seaofnodes.simple.node.Node;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
+import java.util.ArrayList;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class ArrayInitTest {
+ @Test public void constantListComputeIsMonotone() {
+ CodeGen code = new CodeGen("return 0;").parse();
+ TypeStruct layout = TypeStruct.makeAry("[]i32",TypeInteger.U32,2,TypeInteger.I32,3,true);
+ Type array = TypeMemPtr.make(layout);
+ Node ctrl = ConstantNode.seed(Type.CONTROL);
+ Node ptr = ConstantNode.seed(Type.TOP);
+ Node len = ConstantNode.seed(Type.TOP);
+ Node x = ConstantNode.seed(Type.TOP), y = ConstantNode.seed(Type.TOP);
+ ArrayInitNode init = new ArrayInitNode(ctrl,code.ZERO,ptr,len,2,null,new Ary<>(new Node[]{x,y}));
+ Type[][] inputs = {
+ {Type.TOP,TypeMemPtr.TOP,array},
+ {Type.TOP,TypeInteger.TOP,TypeInteger.TWO,TypeInteger.U32},
+ {Type.TOP,TypeInteger.TOP,TypeInteger.constant(5),TypeInteger.BOT},
+ {Type.TOP,TypeInteger.TOP,TypeInteger.constant(6),TypeInteger.BOT}
+ };
+ var states = new ArrayList<Type[]>();
+ for( Type p : inputs[0] )
+ for( Type l : inputs[1] )
+ for( Type a : inputs[2] )
+ for( Type b : inputs[3] ) {
+ ptr._type = p; len._type = l; x._type = a; y._type = b;
+ states.add(new Type[]{p,l,a,b,init.compute()});
+ }
+ for( Type[] before : states )
+ for( Type[] after : states )
+ if( before[0].isa(after[0]) && before[1].isa(after[1]) &&
+ before[2].isa(after[2]) && before[3].isa(after[3]) )
+ assertTrue("Array initializer rose from "+before[4]+" to "+after[4],before[4].isa(after[4]));
+ }
+
+ private static void value(String src, long expected) {
+ CodeGen code=new CodeGen(src).driver(CodeGen.Phase.TypeCheck);
+ assertEquals(Long.toString(expected),Eval2.eval(code,0,100000));
+ }
+
+ private static void bad(String src, String message) {
+ try {
+ new CodeGen(src).driver(CodeGen.Phase.TypeCheck);
+ fail("Expected: "+message);
+ } catch( com.seaofnodes.simple.parser.Parser.ParseException e ) {
+ assertTrue(e.getMessage(),e.getMessage().contains(message));
+ }
+ }
+
+ @Test public void lists() {
+ value("val a=new u16[3]{025755,1,65538}; return a#*100000+a[0]+a[1]+a[2];",311248);
+ value("val a=new int[]{4,7,9,}; a[1]=2; return a#+a[0]+a[1]+a[2];",18);
+ value("val a=new int[]{}; return a#;",0);
+ bad("val a=new int[2]{1}; return a[0];","initializer has 1 elements");
+ }
+
+ @Test public void generators() {
+ value("int n=6; val a=new int[n]{int i -> i*i;}; return a[5]+a#;",31);
+ value("int base=7; val a=new int[3]{int i -> return base+i;}; return a[2];",9);
+ value("val a=new int[3]{int i -> if(i==1) return 9; i+1;}; return a[0]+a[1]+a[2];",13);
+ value("val a=new int[0]{int i -> 1/i;}; return a#;",0);
+ bad("val a=new int[]{int i -> i;}; return a#;","requires a length");
+ bad("val f={-> int x=1; val a=new int[2]{int i -> x++;}; return a[0];}; return f();","captured local");
+ }
+
+ @Test public void referencesAndReadonly() {
+ value("struct S { int x; new S={i64 v -> x=v;}; }; val a=new S[~3]{int i -> new S(i+4);}; return a[2].x;",6);
+ value("val a=new u8[~]{1,2,3}; return a[1];",2);
+ value("val names=new u8[~][~2]{\"Alice\",\"Bob\"}; return names[1][0];",66);
+ value("val a=new int[ ~ ]{4,5}; return a[1];",5);
+ value("val a=new {i64 -> i64}[]{ {i64 x -> x+1;}, {i64 x -> x*2;} }; return a[1](7);",14);
+ value("struct S { int x=1; }; val a=new !S[1]{new S}; a[0].x=8; return a[0].x;",8);
+ bad("struct S { int x=1; }; val a=new !S[~1]{new S}; a[0].x=8; return a[0].x;","final field");
+ value("struct S { int x; }; val a=new S?[~2]{u32 i -> null;}; return a[1]==null;",1);
+ bad("val a=new int[~2]{1,2}; a[0]=3; return a[0];","final field");
+ bad("struct S { int x; }; val a=new S[1]{null}; return a#;","declared type");
+ }
+
+ @Test public void nativeInitializers() throws Exception {
+ TestC.runSF("""
+ val main = { ->
+ int n=8;
+ val a=new u16[~n]{int i -> i*i+1;};
+ val b=new u8[]{257,2,3};
+ return a[7]==50 && b[0]==1 && a#==8 ? 0 : 1;
+ };
+ ""","arrayInitializers","",-1);
+ }
+
+ @Test public void nestedBodies() {
+ value("""
+ val f={ ->
+ int offset=5;
+ val a=new int[4]{int i ->
+ int sum=offset;
+ for(int j=0;j<i;j++) { if(j==1) continue; sum+=j; }
+ if(i==2) return 20;
+ return sum;
+ };
+ return a[0]+a[1]+a[2]+a[3];
+ };
+ return f();
+ """,37);
+ value("""
+ val a=new int[][3]{int i ->
+ new int[2]{int j -> i*10+j;};
+ };
+ return a[2][1]+a[0][0];
+ """,21);
+ value("""
+ val a=new int[2]{int i ->
+ val twice={i64 x -> return x*2;};
+ twice(i);
+ };
+ return a[1];
+ """,2);
+ }
+
+ @Test public void orderAndIdentity() throws Exception {
+ TestC.runSF("""
+ struct Counter {
+ int calls=0;
+ val next={->calls++;};
+ };
+ struct Item {
+ int x;
+ new Item={i64 v -> x=v;};
+ };
+ val build_noInline={u32 n, !Counter c ->
+ new Item[~n]{u32 i -> new Item(c.next());};
+ };
+ val main={->
+ val c=new Counter;
+ val empty=build_noInline(0,c);
+ if(empty#!=0 || c.calls!=0) return 1;
+ val a=build_noInline(4,c);
+ if(c.calls!=4 || a[0].x!=0 || a[3].x!=3 || a[0]==a[1]) return 2;
+ val b=new int[]{c.next(),c.next(),c.next()};
+ if(b[0]!=4 || b[2]!=6) return 3;
+ val d=new int[c.next()]{i64 i -> c.next();};
+ if(d#!=7 || d[0]!=8 || d[6]!=14 || c.calls!=15) return 4;
+ return 0;
+ };
+ ""","arrayInitOrder","",-1);
+ }
+
+ @Test public void diagnostics() {
+ bad("val f={u32 n -> new int[n]{1,2};}; return f(arg)#;","length must equal that constant");
+ bad("val a=new int[2]{u8 i -> i;}; return a[0];","generator index");
+ bad("val a=new int[2]{int return -> 0;}; return a[0];","index name");
+ bad("val a=new int[2]{int i -> i++;}; return a[0];","final 'i'");
+ bad("val f={->int x=0; return new int[2]{int i -> x=i;};}; return f()#;","captured local");
+ bad("val a=new int[2]{int i -> break;}; return a[0];","No active loop");
+ bad("val a=new int[-1]{}; return a#;","outside the u32 range");
+ bad("val a=new int[1.5]{int i -> i;}; return a#;","integer");
+ bad("val a=new int[~]{1,2}; val b=a; b[0]++; return b[0];","final field");
+ for( String init : new String[]{"self","int i -> self;"} )
+ bad("struct C { int x; new C={-> val a=new C[1]{"+init+"}; x=a#;}; }; return new C().x;",
+ "fully initialized elements");
+ }
+
+ @Test public void forwardReferencesAndLoopScopes() {
+ value("""
+ val f={ ->
+ int sum=0;
+ for(int k=0;k<3;k++) {
+ val a=new int[2]{int i ->
+ if(i) return later(i);
+ later(3);
+ };
+ if(k==1) continue;
+ sum+=a[0]+a[1];
+ }
+ return sum;
+ };
+ val later={i64 i -> i*2;};
+ return f();
+ """,16);
+ }
+
+ @Test public void optimizationOrder() {
+ String src="""
+ struct Box { int x; new Box={i64 v -> x=v;}; };
+ val f={u32 n ->
+ int base=7;
+ val a=new Box[~n]{u32 i ->
+ int x=base;
+ for(int j=0;j<i;j++) x+=j;
+ if(i==2) return new Box(20);
+ new Box(x);
+ };
+ return n==0 ? 0 : a[n-1].x;
+ };
+ return f(arg&7);
+ """;
+ for( long seed : new long[]{1,123,987654321} ) {
+ CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(code,0));
+ assertEquals("20",Eval2.eval(code,3));
+ assertEquals("13",Eval2.eval(code,5));
+ }
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/AssemblyDumpTest.java b/src/test/java/com/seaofnodes/simple/AssemblyDumpTest.java
new file mode 100644
index 00000000..0e4c230a
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/AssemblyDumpTest.java
@@ -0,0 +1,61 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.BAOS;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HexFormat;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class AssemblyDumpTest {
+ @Test public void libraryUnitsAndGlyphs() throws Exception {
+ Path root=Path.of("build/objs/assembly-dump");
+ Files.createDirectories(root);
+ Path build=Files.createTempDirectory(root,"sys-");
+ CodeGen code=new CodeGen("src/main/smp",build.toString(),null,"sys",null,123,true,TypeInteger.BOT)
+ .driver(CodeGen.Phase.Serialize,TestC.CPU_PORT,TestC.CALL_CONVENTION)
+ .driver(CodeGen.Phase.Encoding,TestC.CPU_PORT,TestC.CALL_CONVENTION);
+ var entry=code._encoding;
+ String before=code.asm();
+ assertSame("Dumping another unit must not change the active encoding",entry,code._encoding);
+ for( var unit : code._compunits.values() )
+ if( unit._src!=null ) {
+ String heading="=== Compilation unit: "+unit._cname+" ===";
+ assertEquals("Exactly one heading for "+unit._cname,2,before.split(heading,-1).length);
+ }
+ int start=before.indexOf("=== Compilation unit: sys.draw.Canvas ===");
+ assertTrue(start>=0);
+ int end=before.indexOf("=== Compilation unit:",start+1);
+ String canvas=before.substring(start,end<0 ? before.length() : end);
+ assertTrue(canvas.contains("Constant Pool"));
+ assertTrue(canvas.contains("sys.draw.Canvas._glyphs: []u16"));
+ TypeMemPtr ptr=(TypeMemPtr)code._compunits.get("sys/draw/Canvas")._clz.field("_glyphs")._t;
+ TypeConAry glyphs=(TypeConAry)ptr._obj.field("[]")._t;
+ assertEquals(96,glyphs.len());
+ BAOS bytes=new BAOS();
+ glyphs.write(bytes);
+ assertTrue("The complete font follows its u32 length header",
+ canvas.contains("60000000"+HexFormat.of().formatHex(bytes.toByteArray())));
+ code.exportELF(false,false);
+ com.seaofnodes.simple.codegen.PrintRegTestSupport.checkDataAliases(
+ build.resolve("sys/draw/Canvas.o"),196,"sys.draw.Canvas._glyphs");
+ assertSame(entry,code._encoding);
+ assertEquals("Export must not truncate the dump to its entry unit",before,code.asm());
+ Files.writeString(build.resolve("sys.asm"),before);
+ }
+
+ @Test public void combinedEncodingAndEarlierPhase() {
+ for( String cpu : new String[]{"x86_64_v2","arm","riscv"} ) {
+ CodeGen code=new CodeGen("val a=new u16[~]{5,6,7}; return a[arg&1];")
+ .driver(CodeGen.Phase.RegAlloc,cpu,"SystemV");
+ assertFalse(code.asm().contains("Constant Pool"));
+ code.driver(cpu,"SystemV",true,false);
+ String dump=code.asm();
+ assertFalse(dump.contains("=== Compilation unit:"));
+ assertTrue(dump.contains("Constant Pool"));
+ assertTrue(dump.contains("03000000050006000700"));
+ }
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/AudioTest.java b/src/test/java/com/seaofnodes/simple/AudioTest.java
new file mode 100644
index 00000000..db8542d0
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/AudioTest.java
@@ -0,0 +1,94 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.util.Ary;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+// Loader and Simple/C ABI checks: no miniaudio download or audio device.
+public class AudioTest {
+ private static String missing, playback;
+
+ @BeforeClass public static void compilePrograms() throws Exception {
+ var paths=new Ary<>(new String[]{TestC.RELEASE_SYS_DIR});
+ missing=TestC.compile("""
+ val main = { ->
+ if( sys.audio.open()!=sys.audio.ERROR ) return 1;
+ if( sys.audio.boop()!=sys.audio.ERROR || sys.audio.playing() ) return 2;
+ sys.audio.stop(); sys.audio.close(); sys.audio.close();
+ sys.io.p("silent fallback\\n");
+ return 0;
+ };
+ ""","audio_missing",paths,TestC.CALL_CONVENTION,null,null,-1);
+ playback=TestC.compile("""
+ val main = { ->
+ if( sys.audio.open()!=sys.audio.OK ) return 1;
+ val pcm = new i16[3];
+ pcm[0]=-32768; pcm[1]=123; pcm[2]=32767;
+ int sound = sys.audio.load(pcm);
+ if( sound!=4 || sys.audio.play(sound)!=0 || !sys.audio.playing() ) return 2;
+ if( sys.audio.boop()!=0 || sys.audio.win()!=0 || sys.audio.lose()!=0 ) return 3;
+ sys.audio.stop();
+ if( sys.audio.playing() ) return 4;
+ sys.audio.close();
+ return 0;
+ };
+ ""","audio_playback",paths,TestC.CALL_CONVENTION,null,null,-1);
+ }
+
+ private static String run(String exe, String backend) throws Exception {
+ ProcessBuilder pb=new ProcessBuilder(exe).redirectErrorStream(true);
+ if( backend==null ) pb.environment().remove("SIMPLE_MINIAUDIO");
+ else pb.environment().put("SIMPLE_MINIAUDIO",backend);
+ Process child=pb.start();
+ try {
+ assertTrue("audio program timed out",child.waitFor(10,TimeUnit.SECONDS));
+ String out=new String(child.getInputStream().readAllBytes(),StandardCharsets.UTF_8);
+ assertEquals(out,0,child.exitValue());
+ return out;
+ } finally { if( child.isAlive() ) child.destroyForcibly(); }
+ }
+
+ @Test public void silentFallback() throws Exception {
+ assertEquals("silent fallback\n",run(missing,null));
+ assertEquals("silent fallback\n",run(missing,"off"));
+ assertTrue(run(missing,"no-such-miniaudio-backend").contains("cannot load backend"));
+ }
+
+ @Test public void nativeBoundary() throws Exception {
+ Path source=Path.of("build/objs/audio_stub.c");
+ Path library=Path.of("build/objs/audio_stub"+(TestC.OS.startsWith("Windows") ? ".dll" : ".so"));
+ Files.writeString(source,"""
+ #include "simple_audio.h"
+ static int active;
+ static int32_t open_audio(void) { return 0; }
+ static int32_t load(const int16_t *p, uint32_t n) {
+ return n==3 && p[0]==-32768 && p[1]==123 && p[2]==32767 ? 4 : -1;
+ }
+ static int32_t play(int32_t cue) { active=1; return cue>=1 && cue<=4 ? 0 : -1; }
+ static int32_t stop(void) { active=0; return 0; }
+ static int32_t playing(void) { return active; }
+ static void close_audio(void) { active=0; }
+ static const struct simple_audio_api api={1,open_audio,load,play,stop,playing,close_audio};
+ const struct simple_audio_api *simple_miniaudio_api(void) { return &api; }
+ """);
+ TestC.exec(30,"gcc","-shared","-fPIC","-Isrc/main/c",source.toString(),"-o",library.toString());
+ assertEquals("",run(playback,library.toAbsolutePath().toString()));
+ }
+
+ @Test public void incompatibleBackend() throws Exception {
+ Path source=Path.of("build/objs/audio_bad.c");
+ Path library=Path.of("build/objs/audio_bad"+(TestC.OS.startsWith("Windows") ? ".dll" : ".so"));
+ Files.writeString(source,"""
+ #include "simple_audio.h"
+ static const struct simple_audio_api api={999};
+ const struct simple_audio_api *simple_miniaudio_api(void) { return &api; }
+ """);
+ TestC.exec(30,"gcc","-shared","-fPIC","-Isrc/main/c",source.toString(),"-o",library.toString());
+ assertTrue(run(missing,library.toAbsolutePath().toString()).contains("incompatible backend"));
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/BrainFuckTest.java b/src/test/java/com/seaofnodes/simple/BrainFuckTest.java
index 63858df0..07e702c7 100644
--- a/src/test/java/com/seaofnodes/simple/BrainFuckTest.java
+++ b/src/test/java/com/seaofnodes/simple/BrainFuckTest.java
@@ -1,19 +1,21 @@
package com.seaofnodes.simple;
import com.seaofnodes.isa.eval.EvalRisc5;
-
import com.seaofnodes.isa.eval.EvalArm64;
-
import java.io.IOException;
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
import java.nio.file.Files;
import java.nio.file.Path;
-
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class BrainFuckTest {
@Test public void testBrainFuckAryu8() throws IOException {
+
+
String brain_fuck = "Hello World!\n";
String src =
"""
diff --git a/src/test/java/com/seaofnodes/simple/BreakoutTest.java b/src/test/java/com/seaofnodes/simple/BreakoutTest.java
new file mode 100644
index 00000000..7f3e56d4
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/BreakoutTest.java
@@ -0,0 +1,216 @@
+package com.seaofnodes.simple;
+
+import java.awt.image.BufferedImage;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import javax.imageio.ImageIO;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+// Exercise the actual Simple game and rasterizer in native code, without a window.
+public class BreakoutTest {
+ private static String game() throws Exception {
+ String source = Files.readString(Path.of("docs/examples/Breakout.smp"));
+ // Physics/rendering tests supply their own entry point. Input and
+ // sound are exercised by GraphicsTest with the complete program.
+ int end = source.indexOf("// Short, fading triangle waves");
+ assertTrue("Breakout physics/rendering section",end >= 0);
+ String physics = source.substring(0,end);
+ // The shared square-root helper now follows the input code.
+ int sqrt = source.indexOf("val _sqrt =");
+ if( sqrt >= end ) {
+ int close = source.indexOf("\n};",sqrt);
+ assertTrue("Square-root helper end",close >= 0);
+ physics += source.substring(sqrt,close+4);
+ }
+ return physics;
+ }
+
+ @Test public void firstServeMovesBall() throws Exception {
+ TestC.runSYS(game()+"""
+ val main = { ->
+ val g = new Game; g._reset();
+ if( g.speed != 320.0 || g.paddle != 504.0 ) return 1;
+ g._serve();
+ f64 x = g.x, y = g.y;
+ g._tick(0);
+ if( g.phase != PLAY || g.x <= x || g.y >= y ) return 2;
+ g._paddle(2000.0);
+ if( g.paddle != RIGHT-PADDLE_HALF ) return 3;
+ return 0;
+ };
+ ""","breakoutServe","",-1);
+ }
+
+ @Test public void paddleLivesAndPause() throws Exception {
+ TestC.runSYS(game()+"""
+ val main={ ->
+ val g=new Game; g._reset();
+ if( g.remaining!=192 || g.score!=0 || g.lives!=3 || g.phase!=READY ) return 1;
+ g._tick(1);
+ if( g.paddle<510.39 || g.paddle>510.41 || g.x!=g.paddle ) return 2;
+ g._paddle(-100.0); if( g.paddle != 80.0 ) return 3;
+ g._paddle(2000.0); if( g.paddle != 928.0 ) return 4;
+ g._reset();
+ g.x=g.paddle-50.0; g._rebound();
+ if( g.dx > -0.78 || g.dy>=0.0 ) return 5;
+ g.x=g.paddle+50.0; g._rebound();
+ if( g.dx < 0.78 || g.dy>=0.0 ) return 6;
+ g.x=g.paddle; g._rebound();
+ if( g.dy > -0.99 || g.dx*g.dx+g.dy*g.dy<0.999999 ) return 7;
+ g._serve(); g.paused=true;
+ f64 x=g.x, y=g.y, paddle=g.paddle;
+ g._tick(1);
+ if( g.x!=x || g.y!=y || g.paddle!=paddle ) return 8;
+ g.paused=false;
+ for( int i=0; i<3; i++ ) {
+ g._serve(); g.x=40.0; g.y=723.0; g.dx=0.0; g.dy=1.0;
+ g._tick(0);
+ if( g.lives!=2-i || g.backWall || g.sound!=5 ) return 9;
+ }
+ if( g.phase!=LOST ) return 10;
+ g._serve(); if( g.phase!=LOST ) return 11;
+ g._reset();
+ if( g.phase!=READY || g.lives!=3 || g.boosted || g.paused || g.speed!=320.0 ) return 12;
+ return 0;
+ };
+ ""","breakoutControls","",-1);
+ }
+
+ @Test public void collisionsAndBoost() throws Exception {
+ TestC.runSYS(game()+"""
+ val main={ ->
+ val g=new Game; g._reset();
+ g._serve();
+ g.x=29.1; g.y=400.0; g.dx=-1.0; g.dy=0.0;
+ g._tick(0);
+ if( g.dx<=0.0 || g.x<29.0 || g.sound!=1 ) return 1;
+ g.x=978.9; g.dx=1.0; g._tick(0);
+ if( g.dx>=0.0 || g.x>979.0 ) return 2;
+ // Front of the bottom row: one point, no acceleration or boost.
+ g.x=44.0; g.y=330.0; g.dx=0.0; g.dy=-1.0;
+ g._tick(0);
+ if( g.bricks[168]!=0 || g.score!=1 || g.dy<=0.0 || g.boosted || g.soundDepth!=1 ) return 3;
+ // Top of a brick alone does not arm the back-wall bonus.
+ g.x=44.0; g.y=146.0; g.dy=1.0;
+ g._tick(0);
+ if( g.bricks[0]!=0 || g.score!=9 || g.speed!=565.0 || g.boosted || g.soundDepth!=8 ) return 4;
+ // Back wall arms it; hitting a side still must not trigger it.
+ g.x=84.0; g.y=89.1; g.dy=-1.0;
+ g._tick(0);
+ if( !g.backWall || g.dy<=0.0 || g.boosted ) return 5;
+ g.x=60.0; g.y=160.0; g.dx=1.0; g.dy=0.0;
+ g._tick(0);
+ if( g.bricks[1]!=0 || g.dx>=0.0 || g.boosted ) return 6;
+ g.x=124.0; g.y=146.0; g.dx=0.0; g.dy=1.0;
+ g._tick(0);
+ if( !g.boosted || g.speed<932.24 || g.speed>932.26 || g.dy>=0.0 || g.sound!=3 || g.soundDepth!=8 ) return 7;
+ // Another back hit cannot multiply the speed a second time.
+ g.x=164.0; g.y=146.0; g.dy=1.0;
+ g._tick(0);
+ if( g.speed>932.26 || g.bricks[3]!=0 ) return 8;
+ // A boosted front hit cannot tunnel through the brick either.
+ g.x=84.0; g.y=331.0; g.dy=-1.0;
+ g._tick(0);
+ if( g.bricks[169]!=0 || g.dy<=0.0 || g.speed<932.24 ) return 9;
+ // Actual paddle contact disarms the trip and chooses its angle.
+ g.x=g.paddle+50.0; g.y=668.0; g.dy=1.0; g.dx=0.0;
+ g._tick(0);
+ if( g.dy>=0.0 || g.dx<0.78 || g.backWall || !g.boosted ) return 10;
+ // A lost ball clears both kinds of acceleration, preserving the wall/score.
+ int score=g.score, remaining=g.remaining;
+ g.x=40.0; g.y=723.0; g.dx=0.0; g.dy=1.0;
+ g._tick(0);
+ if( g.phase!=READY || g.lives!=2 || g.speed!=320.0 || g.depth!=1 || g.boosted ) return 11;
+ if( g.score!=score || g.remaining!=remaining ) return 12;
+ g._serve();
+ if( g.speed!=320.0 ) return 13;
+ // The new ball can earn its own boost.
+ g.sound=0; g.backWall=true;
+ g.x=204.0; g.y=146.0; g.dx=0.0; g.dy=1.0;
+ g._tick(0);
+ if( !g.boosted || g.speed<932.24 || g.sound!=3 || g.soundDepth!=8 ) return 14;
+ return 0;
+ };
+ ""","breakoutCollisions","",-1);
+ }
+
+ @Test public void gridEdgesAndGaps() throws Exception {
+ TestC.runSYS(game()+"""
+ val main = { ->
+ val g = new Game; g._reset();
+ val c = new sys.draw.Canvas(WIDTH,HEIGHT);
+ g._draw(c);
+ g._serve();
+ // The center lies below the last grid row and between columns.
+ // Both neighboring bricks overlap the ball; the left one wins.
+ g.x = 64.0; g.y = 330.0; g.dx = 0.0; g.dy = -1.0;
+ g._tick(0);
+ if( g.bricks[168] != 0 || g.bricks[169] != 1 || g.score != 1 || g.dy <= 0.0 ) return 1;
+ // A cleared neighbor must not hide the other candidate.
+ g.x = 64.0; g.y = 330.0; g.dx = 0.0; g.dy = -1.0;
+ g._tick(0);
+ if( g.bricks[169] != 0 || g.score != 2 || g.dy <= 0.0 ) return 2;
+ // Rightmost column, then a position entirely below the wall.
+ g.x = 978.0; g.y = 330.0; g.dx = 0.0; g.dy = -1.0;
+ g._tick(0);
+ if( g.bricks[191] != 0 || g.score != 3 || g.dy <= 0.0 ) return 3;
+ g.x = 44.0; g.y = 400.0; g.dx = 0.0; g.dy = -1.0;
+ g._tick(0);
+ if( g.score != 3 || g.dy >= 0.0 ) return 4;
+ // Overlapping rows: the first box contains the center but no
+ // face was crossed; continue to the next row's top face.
+ g.x = 44.0; g.y = 168.0; g.dx = 0.0; g.dy = 1.0;
+ g._tick(0);
+ if( g.bricks[0] != 1 || g.bricks[24] != 0 || g.score != 10 || g.dy >= 0.0 ) return 5;
+ // Contact just above the first row still belongs to that row.
+ g.x = 44.0; g.y = 146.0; g.dx = 0.0; g.dy = 1.0;
+ g._tick(0);
+ if( g.bricks[0] != 0 || g.score != 18 || g.dy >= 0.0 ) return 6;
+ return 0;
+ };
+ ""","breakoutGrid","",-1);
+ }
+
+ @Test public void clearEveryBrickAndRender() throws Exception {
+ TestC.runSYS(game()+"""
+ val main={ ->
+ val g=new Game; g._reset();
+ val c=new sys.draw.Canvas(WIDTH,HEIGHT);
+ g._draw(c);
+ for( int row=0; row<ROWS; row++ )
+ if( c.pixels[(BRICK_TOP+row*BRICK_DY+4)*WIDTH+28]!=COLORS[row] ) return 1;
+ if( c.pixels[675*WIDTH+504]!=CYAN || c.pixels[663*WIDTH+504]!=WHITE ) return 2;
+ // Save a native-rendered preview for manual visual review.
+ int fd=sys.io.create("build/objs/breakout-preview.bgra");
+ i64 pixels=c.pixels;
+ if( fd<0 || sys.libc.write(fd,pixels,c.pixels#*4)!=c.pixels#*4 ) return 3;
+ sys.io.close(fd);
+ g._serve();
+ // Drive through every brick's front, bottom row first.
+ for( int i=COLS*ROWS-1; i>=0; i-- ) {
+ g.x=LEFT+20.0+(i-i/COLS*COLS)*BRICK_DX;
+ g.y=BRICK_TOP+1.0+(i/COLS)*BRICK_DY+BRICK_H+RADIUS;
+ g.dx=0.0; g.dy=-1.0;
+ g._tick(0);
+ if( g.bricks[i]!=0 ) return 4;
+ }
+ if( g.phase!=WON || g.remaining!=0 || g.score!=864 || g.sound!=6 ) return 5;
+ f64 y=g.y; g._tick(0);
+ if( g.y!=y ) return 6;
+ g._reset();
+ for( int i=0; i<g.bricks#; i++ ) if( g.bricks[i]!=1 ) return 7;
+ if( g.score!=0 || g.remaining!=192 ) return 8;
+ return 0;
+ };
+ ""","breakoutClear","",-1);
+ byte[] pixels=Files.readAllBytes(Path.of("build/objs/breakout-preview.bgra"));
+ assertEquals(1008*760*4,pixels.length);
+ var image=new BufferedImage(1008,760,BufferedImage.TYPE_INT_RGB);
+ var bytes=ByteBuffer.wrap(pixels).order(ByteOrder.LITTLE_ENDIAN);
+ for(int y=0;y<760;y++) for(int x=0;x<1008;x++) image.setRGB(x,y,bytes.getInt());
+ ImageIO.write(image,"png",Path.of("build/objs/breakout-preview.png").toFile());
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java b/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java
index 31c0302b..fd816c68 100644
--- a/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java
+++ b/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java
@@ -4,10 +4,35 @@ import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.TypeInteger;
import com.seaofnodes.simple.type.TypeMem;
+import com.seaofnodes.simple.type.Type;
+import com.seaofnodes.simple.type.XInt;
import org.junit.Test;
import static org.junit.Assert.*;
public class BulkMemPhiTest {
+ @Test public void testUnescapedSliceKeepsItsOwnEscapes() {
+ CodeGen code = new CodeGen("");
+ code._phase = CodeGen.Phase.Lower;
+ Node defaults = code.con(TypeMem.START);
+ Node method = code.con(TypeMem.make(55,Type.BOTTOM,false,false,false,XInt.make(4),XInt.EMPTY));
+ MemMergeNode fields = new MemMergeNode(null,defaults);
+ fields.alias(55,method);
+ fields.init();
+ assertSame("The bulk excludes this unescaped field",TypeMem.START,fields._type);
+ Node ctrl = new CProjNode(code._start,0,"Ctrl").init();
+ Node iff = new IfNode(ctrl,code.con(TypeInteger.BOOL)).init();
+ RegionNode region = new RegionNode(null,null,
+ new CProjNode(iff,0,"True").init(),
+ new CProjNode(iff,1,"False").init()).init();
+ BulkMemPhiNode bulk = new BulkMemPhiNode("$mem",region,fields,defaults).init();
+ MemMergeNode split = (MemMergeNode)bulk.idealize();
+ Node precise = split.alias(55);
+ assertTrue("A new slice must not inherit an escape bound that excludes its method",
+ precise.compute().isa(precise._type));
+ precise.peephole();
+ assertTrue(XInt.bit(((TypeMem)precise._type)._escFs,4));
+ }
+
// Reduced from Capitalize's Lower-phase failure. A precise MemMerge slot
// may still be supplied by bulk memory which covers the requested alias.
@Test public void testBulkInPreciseSlot() {
diff --git a/src/test/java/com/seaofnodes/simple/Chapter01Test.java b/src/test/java/com/seaofnodes/simple/Chapter01Test.java
index fa7fb68a..6db79c16 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter01Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter01Test.java
@@ -41,13 +41,8 @@ public class Chapter01Test {
}
@Test
- public void testBad2() {
- try {
- new CodeGen("return 0123;").parse();
- fail();
- } catch( RuntimeException e ) {
- assertEquals("Syntax error: integer values cannot start with '0'",e.getMessage());
- }
+ public void testOctal() {
+ assertEquals("return 83;",new CodeGen("return 0123;").parse().print());
}
@Test
diff --git a/src/test/java/com/seaofnodes/simple/Chapter17bTest.java b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java
index 12e54193..8a66dcc5 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter17bTest.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java
@@ -568,8 +568,8 @@ struct _A {
return new _A(new !B?[0]).array_of_b;
""");
code.driver(CodeGen.Phase.TypeCheck);
- assertEquals("return []*B?;", code.print());
- assertEquals("*B {}?[]", Eval2.eval(code,0));
+ assertEquals("return []*Test.B?;", code.print());
+ assertEquals("*Test.B {}?[]", Eval2.eval(code,0));
}
// ---------------------------------------------------------------
diff --git a/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/src/test/java/com/seaofnodes/simple/Chapter21Test.java
index c6d558eb..14a091ac 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter21Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter21Test.java
@@ -461,6 +461,7 @@ return i;
String src =
"""
val test_sqrt = { flt x ->
+ if( x==0.0 ) return 0.0; // Avoid 0/0: unordered comparisons must stay false.
flt epsilon = 1e-15;
flt guess = x;
while( 1 ) {
@@ -531,7 +532,8 @@ val sieve = { int N ->
String sprimes = sb.p("]").toString();
// Compile, link against native C; expect the above string of primes to be printed out by C
- TestC.runC(src, "sieve", sprimes,344);
+ // Load/Phi hoisting now requires the pointer before the loop entry.
+ TestC.runC(src, "sieve", sprimes,216);
// Evaluate on RISC5 emulator; expect return of an array of primes in
// the simulated heap.
diff --git a/src/test/java/com/seaofnodes/simple/Chapter22Test.java b/src/test/java/com/seaofnodes/simple/Chapter22Test.java
index 9bebee23..c9791de4 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter22Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter22Test.java
@@ -332,9 +332,7 @@ return 0;
public void testFinalArray() {
String src = """
int N=4;
-i32[] !is = new i32[N];
-for( int i=0; i<N; i++ )
- is[i] = i*i;
+i32[~] !is = new i32[~N]{ int i -> i*i; };
val _sum = { i32[~] is -> // final array
int sum=0;
for( int i=0; i<is#; i++ )
diff --git a/src/test/java/com/seaofnodes/simple/Chapter23Test.java b/src/test/java/com/seaofnodes/simple/Chapter23Test.java
index 89e41efa..8667c0bd 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter23Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter23Test.java
@@ -279,7 +279,7 @@ return _s.peek('q');
CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck);
assertEquals("return Phi(Region,0,1);", code.print());
assertEquals("1", Eval2.eval(code, 0));
- testCPU(src,"x86_64_v2", "win64",31,null);
+ testCPU(src,"x86_64_v2", "win64",23,null);
};
@Test
@@ -303,8 +303,7 @@ return s.skip().x;
try { new CodeGen(src).driver(CodeGen.Phase.TypeCheck); fail(); }
catch( Exception e ) {
- // Bad error message, but basically requires a mutable 'x' field.
- org.junit.Assert.assertTrue(e.getMessage().startsWith("Argument #0 isa"));
+ assertEquals("Receiver for method 'skip' is read-only Test._Scan; expected writable Test._Scan",e.getMessage());
}
};
diff --git a/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/src/test/java/com/seaofnodes/simple/Chapter25Test.java
index 67e7f982..85f273a1 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter25Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter25Test.java
@@ -475,9 +475,9 @@ public class Chapter25Test {
// Elf files are sane
- // Sys depends on io, libc, text, char, collections, and array utilities.
+ // Software drawing and the optional native graphics interface.
assertArrayEquals(new String[]{"sys/adt", "sys/ary",
- "sys/aryi64", "sys/aryu8", "sys/char", "sys/io", "sys/libc", "sys/rt", "sys/scan", "sys/text"},sys_elf._deps);
+ "sys/aryi64", "sys/aryu8", "sys/audio", "sys/char", "sys/draw", "sys/gfx", "sys/integer", "sys/io", "sys/libc", "sys/math", "sys/rt", "sys/scan", "sys/text", "sys/time"},sys_elf._deps);
assertArrayEquals(new String[]{"sys"},sys_elf._units);
assertSame("class:sys" ,sys_elf._clz._name);
}
diff --git a/src/test/java/com/seaofnodes/simple/ConstantArrayTest.java b/src/test/java/com/seaofnodes/simple/ConstantArrayTest.java
new file mode 100644
index 00000000..c641487b
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/ConstantArrayTest.java
@@ -0,0 +1,123 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.codegen.StaticData;
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.parser.Parser;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class ConstantArrayTest {
+ private static StaticData.ObjectData array(CodeGen code, TypeInteger storage, int len) {
+ var arrays=code._encoding._data.entries().stream().filter(o ->
+ o.type instanceof TypeStruct ts && ts.isConAry() &&
+ ts.field("[]")._t instanceof TypeConAry c && c.storageType()==storage && c.len()==len).toList();
+ assertEquals(1,arrays.size());
+ assertTrue(arrays.getFirst().readOnly);
+ return arrays.getFirst();
+ }
+
+ @Test public void typedDataAndSharing() throws Exception {
+ String source="""
+ val a=new u16[~]{025755,65535,65538};
+ val b=new u16[~]{025755,65535,65538};
+ val fresh={ -> new u16[~]{025755,65535,65538}; };
+ val main={ ->
+ if(a!=b || a!=fresh() || a#!=3 || a[0]!=025755 || a[1]!=65535 || a[2]!=2) return 1;
+ val bytes=new u8[~]{65,66,67};
+ if(bytes!="ABC") return 2;
+ val signed=new i16[~]{-32768,-1,32767};
+ if(signed[0]!=-32768 || signed[1]!=-1 || signed[2]!=32767) return 3;
+ val ints=new i32[~]{-2147483648,2147483647};
+ if(ints[0]!=-2147483648 || ints[1]!=2147483647) return 4;
+ val uints=new u32[~]{0,4294967295};
+ if(uints[1]!=4294967295) return 5;
+ val longs=new i64[~]{1<<63,0x7fffffffffffffff};
+ if(longs[0]!=(1<<63) || longs[1]!=0x7fffffffffffffff) return 6;
+ val empty=new u16[~]{};
+ if(empty#!=0) return 7;
+ return 0;
+ };
+ """;
+ TestC.runSF(source,"constantArrayWidths","",-1);
+ CodeGen code=new CodeGen(source).driver(CodeGen.Phase.Encoding,TestC.CPU_PORT,TestC.CALL_CONVENTION);
+ StaticData.ObjectData obj=array(code,TypeInteger.U16,3);
+ assertEquals("Test.a, Test.b",obj.label());
+ assertEquals(12,obj.size); // Four-byte length, six data bytes, alignment padding.
+ byte[] bytes=code._encoding._cpool.buf();
+ assertEquals(3,bytes[obj.offset]);
+ assertEquals(025755&255,bytes[obj.offset+4]&255);
+ assertEquals(025755>>>8,bytes[obj.offset+5]&255);
+ assertEquals(255,bytes[obj.offset+6]&255);
+ assertEquals(255,bytes[obj.offset+7]&255);
+ assertNull(code._stop.walk(n -> n instanceof NewNode ? n : null));
+ com.seaofnodes.simple.codegen.PrintRegTestSupport.checkDataAliases(
+ Path.of("build/objs/constantArrayWidths.o"),12,"constantArrayWidths.a","constantArrayWidths.b");
+ }
+
+ @Test public void arbitraryProducerUsesStaticData() {
+ for(String cpu:new String[]{"x86_64_v2","arm","riscv"}) {
+ CodeGen code=new CodeGen("u16[~]? unused=null; return 0;").parse();
+ TypeStruct layout=(TypeStruct)Parser.TYPES.get("[]u16");
+ TypeConAry contents=TypeConAry.make(TypeInteger.U16,new long[]{1,65535});
+ TypeStruct array=TypeStruct.makeAry(layout._name,layout.field("#")._alias,layout.field("[]")._alias,contents);
+ Type ptr=TypeMemPtr.make((byte)2,array,true).makeRO();
+ // No initializer or allocation node: only compute() supplies the type.
+ Node producer=new ReadOnlyNode(code.ZERO) {
+ @Override public String label() { return "constant-array producer"; }
+ @Override public Type compute() { return ptr; }
+ };
+ ReturnNode ret=code._stop.walk(n -> n instanceof ReturnNode r && r.fun().isClz() ? r : null);
+ assertNotNull(ret);
+ ret.setDef(2,producer.peephole());
+ code.add(ret);
+ code.driver(CodeGen.Phase.Encoding,cpu,"SystemV");
+ StaticData.ObjectData obj=array(code,TypeInteger.U16,2);
+ assertEquals(8,obj.size);
+ }
+ }
+
+ @Test public void sideEffectsAndMutableArrays() throws Exception {
+ CodeGen eval=new CodeGen("""
+ val same_noInline={u16[~] a -> a==new u16[~]{1,2};};
+ return same_noInline(new u16[~]{1,2});
+ """).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("1",Eval2.eval(eval,0));
+ TestC.runSF("""
+ val next={int[] counter -> counter[0]++; return 7;};
+ val main={ ->
+ val count=new int[1];
+ val a=new int[~]{next(count),next(count)};
+ if(count[0]!=2 || a[0]!=7 || a[1]!=7) return 1;
+ val b=new int[]{7,7}, c=new int[]{7,7};
+ if(b==c) return 2;
+ int[~] view=b;
+ b[0]=9;
+ if(view[0]!=9 || c[0]!=7) return 3;
+ return 0;
+ };
+ ""","constantArrayEffects","",-1);
+ }
+
+ @Test public void importedTypedConstants() throws Exception {
+ Path root=Path.of("build/objs/separate");
+ Files.createDirectories(root);
+ Path lib=Files.createTempDirectory(root,"constant-arrays-");
+ new CodeGen(null,lib.toString(),null,"Table","""
+ val small=new u16[~]{1,65535};
+ val wide=new i64[~]{1<<63,0x7fffffffffffffff};
+ val uints=new u32[~]{4294967295};
+ val signed=new i32[~]{-2147483648};
+ """,126,true,TypeInteger.BOT).driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
+ var paths=new Ary<>(new String[]{lib.toString()});
+ CodeGen code=new CodeGen(null,"build/objs",paths,"TableClient","""
+ return Table.small[1]==65535 && Table.wide[0]==(1<<63) &&
+ Table.wide[1]==0x7fffffffffffffff && Table.uints[0]==4294967295 && Table.signed[0]==-2147483648;
+ """,126,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("1",Eval2.eval(code,0));
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/ConvertNodeTest.java b/src/test/java/com/seaofnodes/simple/ConvertNodeTest.java
index 96d73f26..bdda3171 100644
--- a/src/test/java/com/seaofnodes/simple/ConvertNodeTest.java
+++ b/src/test/java/com/seaofnodes/simple/ConvertNodeTest.java
@@ -28,7 +28,7 @@ public class ConvertNodeTest {
for( boolean arrayRO : new boolean[]{false,true} )
inputs.add(TypeMemPtr.make(TypeStruct.makeAry("[]test",len,-1,
TypeMemPtr.TEST.withAccess(elementRO),-2,false)).withAccess(arrayRO));
- inputs.add(TypeConAryB.make(new byte[]{1,2,3}));
+ inputs.add(TypeConAry.make(new byte[]{1,2,3}));
for( long value : new long[]{Long.MIN_VALUE,-129,-128,-1,0,1,127,128,255,256,Long.MAX_VALUE} )
inputs.add(TypeInteger.constant(value));
for( double value : new double[]{-0.0,0.0,1.0,16777217.0,Double.NaN,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY} )
diff --git a/src/test/java/com/seaofnodes/simple/DrawTest.java b/src/test/java/com/seaofnodes/simple/DrawTest.java
new file mode 100644
index 00000000..1ca161cf
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/DrawTest.java
@@ -0,0 +1,229 @@
+package com.seaofnodes.simple;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Test;
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.util.Ary;
+import static org.junit.Assert.*;
+
+// Native raster and game checks; no backend or display is needed.
+public class DrawTest {
+ @Test public void nativeClockCoordinates() throws Exception {
+ String clock=Files.readString(Path.of("docs/examples/Clock.smp"))
+ .replace("val main =","val _windowMain =");
+ StringBuilder source=new StringBuilder(clock+"""
+ val _check_noInline={ f64 turn, int expectedX, int expectedY ->
+ int x=360+_sin(turn)*128, y=232-_sin(turn+0.25)*128;
+ if( x<expectedX-1 || x>expectedX+1 || y<expectedY-1 || y>expectedY+1 ) {
+ sys.io.p("turn %f(turn): %(x),%(y) expected %(expectedX),%(expectedY)\\n");
+ return 1;
+ }
+ return 0;
+ };
+ val main={ ->
+ """);
+ for(int i=0;i<=60;i++) {
+ double turn=i/60.0;
+ int x=(int)(360+clockSine(turn)*128), y=(int)(232-clockSine(turn+0.25)*128);
+ source.append("if(_check_noInline(").append(turn).append(',').append(x).append(',').append(y)
+ .append(")!=0) return 1;");
+ }
+ source.append("return 0; };");
+ TestC.runSYS(source.toString(),"clockCoordinates","",-1);
+ }
+
+ private static double clockSine(double turn) {
+ turn-=(long)turn;
+ boolean negative=turn>0.5;
+ double x=(negative ? turn-0.5 : turn)*6.283185307179586;
+ double product=x*(3.141592653589793-x);
+ return (negative ? -16.0 : 16.0)*product/(49.34802200544679-4*product);
+ }
+
+ @Test public void nativeAnalogSweep() throws Exception {
+ String clock=Files.readString(Path.of("docs/examples/Clock.smp"))
+ .replace("val main =","val _windowMain =");
+ StringBuilder source=new StringBuilder(clock+"""
+ val _renderCheck_noInline={ int second, int millisecond, int expectedX, int expectedY ->
+ val c=new sys.draw.Canvas(720,540);
+ _analog(c,3,20,second,millisecond);
+ var found=false;
+ for( int y=expectedY-2; y<=expectedY+2; y++ )
+ for( int x=expectedX-2; x<=expectedX+2; x++ )
+ if( c.pixels[y*720+x]==0xc3ffcb ) found=true;
+ if( !found ) {
+ sys.io.p("missing second hand %(second).%(millisecond) expected %(expectedX),%(expectedY)\\n");
+ return 1;
+ }
+ // Every dial mark/hand must stay within the dial's bounds.
+ for( int y=0; y<540; y++ ) for( int x=0; x<720; x++ )
+ if( (x<204 || x>516 || y<76 || y>388) && c.pixels[y*720+x]!=0 ) {
+ sys.io.p("stray pixel %(x),%(y) color %x(c.pixels[y*720+x]) at %(second).%(millisecond)\\n");
+ return 2;
+ }
+ return 0;
+ };
+ val main={ ->
+ """);
+ for(int millis:new int[]{0,500,7500,15000,30000,45000,59500}) {
+ double turn=millis/60000.0;
+ int x=(int)(360+clockSine(turn)*128), y=(int)(232-clockSine(turn+0.25)*128);
+ source.append("if(_renderCheck_noInline(").append(millis/1000).append(',').append(millis%1000)
+ .append(',').append(x).append(',').append(y).append(")!=0) return 1;");
+ }
+ source.append("return 0; };");
+ TestC.runSYS(source.toString(),"clockAnalogSweep","",-1);
+ }
+
+ @Test public void buttonInteractionAndRendering() throws Exception {
+ TestC.runSYS("""
+ val main={ ->
+ val b=new sys.draw.Button(10,10,60,30,"GO");
+ if( !b.contains(10,10) || b.contains(70,20) || b.contains(20,40) ) return 1;
+ if( b.update(20,20,true) || !b.pressed || b.update(20,20,true) ) return 2;
+ if( !b.update(20,20,false) || b.pressed || b.update(20,20,false) ) return 3;
+ if( b.update(0,0,true) || b.update(20,20,true) || b.update(20,20,false) ) return 4;
+ b.update(20,20,true);
+ if( b.update(0,0,false) ) return 5;
+ b.update(20,20,true); b.enabled=false;
+ if( b.update(20,20,true) || b.pressed || b.update(20,20,false) ) return 6;
+ b.enabled=true;
+ val c=new sys.draw.Canvas(90,60);
+ b.update(20,20,false); b.draw(c,2,9,3,5);
+ if( c.pixels[10*90+10]!=9 || c.pixels[13*90+13]!=5 || c.pixels[0]!=0 ) return 7;
+ b.update(0,0,false); b.draw(c,2,9,3,5);
+ if( c.pixels[13*90+13]!=3 ) return 8;
+ return 0;
+ };
+ ""","drawButton","",-1);
+ }
+
+ @Test public void clockFacesWithoutWindow() throws Exception {
+ String clock=Files.readString(Path.of("docs/examples/Clock.smp"))
+ .replace("val main =","val _windowMain =");
+ CodeGen code=new CodeGen(null,null,new Ary<>(new String[]{TestC.RELEASE_SYS_DIR}),"ClockRaster",clock+"""
+ val main={ ->
+ val c=new sys.draw.Canvas(720,540);
+ _digit(c,10,10,8);
+ if( c.pixels[10*720+40]!=GREEN || c.pixels[82*720+40]!=GREEN || c.pixels[154*720+40]!=GREEN ) return 1;
+ _digit(c,10,10,1);
+ if( c.pixels[10*720+40]!=DIM || c.pixels[40*720+78]!=GREEN || c.pixels[40*720+14]!=DIM ) return 2;
+ c.clear(BACKGROUND); _digital(c,23,59,58);
+ if( c.pixels[214*720+254]!=GREEN || c.pixels[262*720+470]!=GREEN ) return 3;
+ c.clear(BACKGROUND); _analog(c,3,0,0,0);
+ if( c.pixels[232*720+430]!=GREEN || c.pixels[160*720+360]!=0xc3ffcb || c.pixels[232*720+360]!=GREEN ) return 4;
+ if( _sin(0.25)<0.999 || _sin(0.75)>-0.999 || _sin(1.0)!=0.0 ) return 5;
+ return 0;
+ };
+ return main();
+ """,126L,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(code,0,3000000));
+ }
+
+ @Test public void rectanglesAndColors() throws Exception {
+ TestC.runSYS("""
+ val main = { ->
+ val c = new sys.draw.Canvas(12,10);
+ u32 color = sys.draw.rgb(300,-1,128);
+ if( color!=16711808 ) return 1;
+ c.clear(7).fillRect(-2,-1,5,4,color);
+ for( int y=0; y<10; y++ )
+ for( int x=0; x<12; x++ )
+ if( c.pixels[y*12+x]!=(x<3 && y<3 ? color : 7) ) return 2;
+ c.clear(0).rect(2,2,7,6,1,9);
+ for( int y=0; y<10; y++ )
+ for( int x=0; x<12; x++ ) {
+ var inside = 2<=x<9 && 2<=y<8;
+ var border = inside && (x==2 || x==8 || y==2 || y==7);
+ if( c.pixels[y*12+x]!=(border ? 9 : 0) ) return 3;
+ }
+ c.clear(5).fillRect(20,0,8,8,1).rect(0,0,12,10,0,1);
+ c.fillRect(0,0,-1,5,1).pixel(-1,0,1).pixel(12,9,1);
+ for( int i=0; i<c.pixels#; i++ ) if( c.pixels[i]!=5 ) return 4;
+ return 0;
+ };
+ ""","drawRectangles","",-1);
+ }
+
+ @Test public void circlesLinesAndText() throws Exception {
+ TestC.runSYS("""
+ val main = { ->
+ val c = new sys.draw.Canvas(16,16);
+ c.fillCircle(8,8,3,1);
+ for( int y=0; y<16; y++ )
+ for( int x=0; x<16; x++ )
+ if( c.pixels[y*16+x]!=((x-8)*(x-8)+(y-8)*(y-8)<=9 ? 1 : 0) ) {
+ sys.io.p("circle mismatch %(x) %(y): %(c.pixels[y*16+x])\\n");
+ return 1;
+ }
+ c.clear(0).circle(8,8,3,1,2);
+ if( c.pixels[8*16+8]!=0 || c.pixels[8*16+11]!=2 || c.pixels[8*16+10]!=0 ) return 2;
+ c.clear(0).circle(8,8,3,3,2);
+ if( c.pixels[8*16+8]!=2 ) return 8;
+ c.clear(0).line(-5,8,20,8,3,3);
+ for( int y=0; y<16; y++ )
+ for( int x=0; x<16; x++ )
+ if( c.pixels[y*16+x]!=(7<=y<=9 ? 3 : 0) ) return 3;
+ c.clear(0).line(8,8,8,8,2,4);
+ if( c.pixels[8*16+8]!=4 || c.pixels[8*16+9]!=4 || c.pixels[9*16+9]!=0 ) return 4;
+ c.clear(0).line(2,2,13,13,1,5);
+ if( c.pixels[7*16+7]!=5 || c.pixels[7*16+8]!=0 ) return 5;
+ c.clear(0).text(0,0,"x",1,6).text(8,0,"X",1,6);
+ for( int y=0; y<5; y++ )
+ for( int x=0; x<3; x++ )
+ if( c.pixels[y*16+x]!=c.pixels[y*16+x+8] ) return 6;
+ c.clear(0).text(0,0,":/",2,6);
+ for( int y=0; y<10; y++ )
+ for( int x=0; x<16; x++ ) {
+ int row=y/2, col=x/2;
+ var colon=col==1 && (row==1 || row==3);
+ var slash=(row<2 && col==6) || (row==2 && col==5) || (row>2 && col==4);
+ if( c.pixels[y*16+x]!=((colon || slash) ? 6 : 0) ) return 9;
+ }
+ c.clear(0).fillCircle(-1,-1,2,7);
+ if( c.pixels[0]!=7 || c.pixels[2]!=0 ) return 7;
+ return 0;
+ };
+ ""","drawCurves","",-1);
+ }
+
+ @Test public void graphicalGameWithoutWindow() throws Exception {
+ String game=Files.readString(Path.of("docs/examples/TicTacToeGraphical.smp"))
+ .replace("val main =","val _windowMain =");
+ TestC.runSYS(game+"""
+
+ val main = { ->
+ val board = new u8[9];
+ _reset(board);
+ if( _square(48,168)!=0 || _square(551,671)!=8 || _square(552,400)!=-1 || _square(100,167)!=-1 ) return 1;
+ if( !_move(board,4) || board[4]!='X' || board[0]!='O' ) return 2;
+ if( _sound(board)!=sys.audio.BOOP ) return 10;
+ if( _move(board,4) || _move(board,0) || _move(board,-1) || _move(board,9) ) return 3;
+ val moves = "1879";
+ _reset(board);
+ for( int i=0; i<moves#; i++ )
+ if( !_move(board,moves[i]-'1') ) return 4;
+ if( _winner(board)!='X' || _move(board,1) ) return 5;
+ if( _sound(board)!=sys.audio.WIN ) return 11;
+ val screen = new sys.draw.Canvas(600,780);
+ _draw(screen,board,-1);
+ if( screen.pixels[252*600+132]==screen.pixels[0] ) return 6;
+ _reset(board);
+ val draw = "12768";
+ for( int i=0; i<draw#; i++ ) _move(board,draw[i]-'1');
+ if( !_finished(board) || _winner(board)!=0 ) return 7;
+ _reset(board);
+ _move(board,0); _move(board,1); _move(board,3);
+ if( _winner(board)!='O' ) return 8;
+ if( _sound(board)!=sys.audio.LOSE ) return 12;
+ _draw(screen,board,-1);
+ _reset(board);
+ if( _finished(board) ) return 9;
+ _draw(screen,board,4);
+ return 0;
+ };
+ ""","drawGame","",-1);
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java b/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java
index 5a20388b..12634f24 100644
--- a/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java
+++ b/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java
@@ -7,6 +7,37 @@ import org.junit.Test;
import static org.junit.Assert.*;
public class EscapeFlowTest {
+ @Test public void testArrayGeneratorDoesNotPublishUnrelatedFunctions() {
+ for( long seed : new long[]{1,123,987654321} )
+ for( String access : new String[]{"","~"} ) {
+ CodeGen code=new CodeGen("""
+ val _hidden_noInline={i64 x -> x+1;};
+ val data=new i32[%s(arg&7)]{int i -> i*i;};
+ return data#;
+ """.formatted(access),seed,true).parse();
+ int hidden=named(code,"_hidden_noInline").sig().fidx();
+ code.driver(CodeGen.Phase.TypeCheck);
+ TypeMem outside=((TypeTuple)code._start._type).mem();
+ assertFalse(XInt.bit(outside._escFs,hidden));
+ assertEquals("0",Eval2.eval(code,0));
+ assertEquals("4",Eval2.eval(code,4));
+ }
+ }
+
+ @Test public void testArrayGeneratorPublishesItsCallbacks() {
+ CodeGen code=new CodeGen("""
+ val _callback_noInline={i64 x -> x+1;};
+ val callbacks=new {i64 -> i64}[~3]{int i -> _callback_noInline;};
+ return callbacks;
+ """).parse();
+ FunNode callback=named(code,"_callback_noInline");
+ int fidx=callback.sig().fidx();
+ code.driver(CodeGen.Phase.TypeCheck);
+ assertTrue(XInt.bit(((TypeTuple)code._start._type).mem()._escFs,fidx));
+ assertFalse(callback.isDead());
+ assertTrue(callback.unknownCallers());
+ }
+
// A cofinite outside set must not revive private allocation helpers.
@Test public void testOpaqueRoundTrip() {
for( long seed : new long[]{1,123,987654321} ) {
diff --git a/src/test/java/com/seaofnodes/simple/Eval2.java b/src/test/java/com/seaofnodes/simple/Eval2.java
index 8d0e3d5b..eef05f93 100644
--- a/src/test/java/com/seaofnodes/simple/Eval2.java
+++ b/src/test/java/com/seaofnodes/simple/Eval2.java
@@ -110,6 +110,7 @@ public abstract class Eval2 {
// Class pointers and initializer self parameters share one object per run.
private static final HashMap<String,Object[]> CLASSES = new HashMap<>();
+ private static final IdentityHashMap<TypeStruct,Object[]> ARRAYS = new IdentityHashMap<>();
public static String eval( CodeGen code, long arg ) { return eval(code,arg,1000); }
public static String eval( CodeGen code, long arg, int timeout ) {
@@ -122,6 +123,7 @@ public abstract class Eval2 {
// selection. Objects loaded later are scheduled independently.
code.driver(CodeGen.Phase.LocalSched);
CLASSES.clear();
+ ARRAYS.clear();
// Timer for when we run too long. Ticks once per backedge or function call
int loopCnt = 0;
@@ -334,6 +336,8 @@ public abstract class Eval2 {
case RoundF32Node rnd -> (double)(float)d(rnd.in(1));
case ToIntegerNode cvt -> (long)d(cvt.in(1));
case ToFloatNode toflt-> (double)x(toflt.in(1));
+ case Intrinsic.F64BitsNode cast -> Double.doubleToRawLongBits(d(cast.in(1)));
+ case Intrinsic.F64FromBitsNode cast -> Double.longBitsToDouble(x(cast.in(1)));
case XorNode xor -> x(xor.in(1)) ^ x(xor.in(2));
default -> throw Utils.TODO();
};
@@ -361,9 +365,12 @@ public abstract class Eval2 {
if( tmp._obj.isAry() ) { // Constant array ptr
Type elem = tmp._obj.field("[]")._t;
if( elem instanceof TypeConAry con ) {
+ Object[] old=ARRAYS.get(tmp._obj);
+ if( old!=null ) yield old;
Object[] xs = new Object[con.len()];
for( int i=0; i<con.len(); i++ )
xs[i] = con.at8(i);
+ ARRAYS.put(tmp._obj,xs);
yield xs;
} else {
// Generic constant array, used as a default input to a
@@ -388,7 +395,7 @@ public abstract class Eval2 {
TypeMem mem = (TypeMem)con._type;
Object val = con(mem._t);
TypeMemPtr tmp = (TypeMemPtr)esc.self()._type;
- int idx = tmp._obj.findAlias(esc.fld()._alias);
+ int idx = layout(tmp._obj).findAlias(esc.fld()._alias);
Object[] fs = (Object[])val(esc.self());
if( tmp._obj.isAry() ) {
assert idx==0; // length field
@@ -443,10 +450,20 @@ public abstract class Eval2 {
private static Object[] classObject(TypeStruct type) {
Object[] ptr = CLASSES.get(type._name);
- if( ptr==null ) CLASSES.put(type._name,ptr=alloc(type));
+ if( ptr==null ) CLASSES.put(type._name,ptr=alloc(layout(type)));
return ptr;
}
+ // Imported pointer views may retain parsing-only forward-reference fields.
+ // Like native field offsets, evaluator slots use the module's final layout.
+ private static TypeStruct layout(TypeStruct type) {
+ if( TypeStruct.startsClzPrefix(type._name) ) {
+ CompUnit cu=CODE._compunits.get(type._name.substring(6).replace('.','/'));
+ if( cu!=null ) return cu._clz;
+ }
+ return type;
+ }
+
private static Object not( NotNode not ) {
Object n = val(not.in(1));
return ( n==null || (n instanceof Double D && D==0.0) || n instanceof Long L && L==0 )
@@ -465,7 +482,7 @@ public abstract class Eval2 {
TypeMemPtr tmp = (TypeMemPtr)ld.ptr()._type;
int idx = tmp._obj.isAry()
? offToIdx(x(ld.off()),tmp._obj)
- : tmp._obj.find(ld._name);
+ : layout(tmp._obj).find(ld._name);
return fs[idx];
}
@@ -476,7 +493,7 @@ public abstract class Eval2 {
Object[] fs = (Object[])val(st.ptr());
int idx = tmp._obj.isAry()
? offToIdx(x(st.off()),tmp._obj)
- : tmp._obj.find(st._name);
+ : layout(tmp._obj).find(st._name);
Object val = val(st.val());
// Idealization can remove conversions already performed by the store.
Type type = st.declaredType();
diff --git a/src/test/java/com/seaofnodes/simple/FloatComparisonNativeTest.java b/src/test/java/com/seaofnodes/simple/FloatComparisonNativeTest.java
new file mode 100644
index 00000000..c0d8be3a
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/FloatComparisonNativeTest.java
@@ -0,0 +1,65 @@
+package com.seaofnodes.simple;
+
+import org.junit.Test;
+
+public class FloatComparisonNativeTest extends TestC {
+ @Test public void longFloatingBranches() throws Exception {
+ StringBuilder source=new StringBuilder("val far_noInline={f64 a,f64 b,i64[] out -> if(a<b){");
+ for(int i=0;i<32;i++) source.append("out[").append(i).append("]=").append(i+1).append(';');
+ source.append("""
+ return 1; } return 0; };
+ val main={ ->
+ val out=new i64[32];
+ if( far_noInline(1.0,2.0,out)!=1 || out[31]!=32 ) return 1;
+ out[31]=0;
+ if( far_noInline(2.0,1.0,out)!=0 || out[31]!=0 ) return 2;
+ f64 nan=1e300*1e300-1e300*1e300;
+ if( far_noInline(nan,1.0,out)!=0 || out[31]!=0 ) return 3;
+ if( far_noInline(1.0,nan,out)!=0 || out[31]!=0 ) return 4;
+ return 0;
+ };
+ """);
+ runSYS(source.toString(),"floatLongBranches","",-1);
+ }
+
+ @Test public void branchesAndValues() throws Exception {
+ String[] literals={"-1.0","0.0","2.0","1e300*1e300","-1e300*1e300",
+ "1e300*1e300-1e300*1e300"};
+ double[] values={-1,0,2,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY,Double.NaN};
+ StringBuilder source=new StringBuilder("""
+ val branch_noInline={ f64 a, f64 b ->
+ int bits=0;
+ if( a<b ) bits+=1;
+ if( a<=b ) bits+=2;
+ if( a==b ) bits+=4;
+ if( a!=b ) bits+=8;
+ if( a>=b ) bits+=16;
+ if( a>b ) bits+=32;
+ return bits;
+ };
+ val value_noInline={ f64 a, f64 b ->
+ val lt=a<b, le=a<=b, eq=a==b, ne=a!=b, ge=a>=b, gt=a>b;
+ return lt+le*2+eq*4+ne*8+ge*16+gt*32;
+ };
+ val check_noInline={ f64 a, f64 b, int expected, int tag ->
+ val branches=branch_noInline(a,b), values=value_noInline(a,b);
+ if( branches!=expected || values!=expected ) {
+ sys.io.p("case %(tag): branches %(branches), values %(values), expected %(expected)\\n");
+ return 1;
+ }
+ return 0;
+ };
+ val main={ ->
+ """);
+ for(int i=0;i<values.length;i++) for(int j=0;j<values.length;j++) {
+ double a=values[i],b=values[j];
+ int expected=(a<b ? 1 : 0)|(a<=b ? 2 : 0)|(a==b ? 4 : 0)|
+ (a!=b ? 8 : 0)|(a>=b ? 16 : 0)|(a>b ? 32 : 0);
+ source.append("if(check_noInline(").append(literals[i]).append(',').append(literals[j])
+ .append(',').append(expected).append(',').append(i*values.length+j)
+ .append(")!=0) return 1;");
+ }
+ source.append("return 0; };");
+ runSYS(source.toString(),"floatComparisons","",-1);
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/FloatNegationNativeTest.java b/src/test/java/com/seaofnodes/simple/FloatNegationNativeTest.java
new file mode 100644
index 00000000..dbbe353a
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/FloatNegationNativeTest.java
@@ -0,0 +1,21 @@
+package com.seaofnodes.simple;
+
+import org.junit.Test;
+
+public class FloatNegationNativeTest extends TestC {
+ @Test public void negateRuntimeValues() throws Exception {
+ // Keep negation out of constant folding, including both signs of zero.
+ runSYS("""
+ val neg_noInline={f64 x -> -x;};
+ val main={ ->
+ if( neg_noInline(2.5)!=-2.5 || neg_noInline(-3.75)!=3.75 ) return 1;
+ if( 1.0/neg_noInline(0.0)!=-1e300*1e300 ) return 2;
+ if( 1.0/neg_noInline(-0.0)!=1e300*1e300 ) return 3;
+ if( neg_noInline(1e300*1e300)!=-1e300*1e300 ) return 4;
+ f64 nan=neg_noInline(1e300*1e300-1e300*1e300);
+ if( nan==nan ) return 5;
+ return 0;
+ };
+ ""","floatNegation","",-1);
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/GraphicsTest.java b/src/test/java/com/seaofnodes/simple/GraphicsTest.java
new file mode 100644
index 00000000..110a6a38
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/GraphicsTest.java
@@ -0,0 +1,194 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.util.Ary;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+// These tests never open a window and do not need MiniFB installed.
+public class GraphicsTest {
+ private static String graphics, text, game, clock, breakout;
+
+ @BeforeClass public static void compilePrograms() throws Exception {
+ var paths = new Ary<>(new String[]{TestC.RELEASE_SYS_DIR});
+ graphics = TestC.compile(Files.readString(Path.of("docs/examples/Graphics.smp")),
+ "graphics_missing",paths,TestC.CALL_CONVENTION,null,null,-1);
+ text = TestC.compile("val main={ -> sys.io.p(\"text still works\\n\"); return 0; };",
+ "graphics_text",paths,TestC.CALL_CONVENTION,null,null,-1);
+ game = TestC.compile(Files.readString(Path.of("docs/examples/TicTacToeGraphical.smp")),
+ "graphics_game",paths,TestC.CALL_CONVENTION,null,null,-1);
+ clock = TestC.compile(Files.readString(Path.of("docs/examples/Clock.smp")),
+ "graphics_clock",paths,TestC.CALL_CONVENTION,null,null,-1);
+ breakout = TestC.compile(Files.readString(Path.of("docs/examples/Breakout.smp")),
+ "graphics_breakout",paths,TestC.CALL_CONVENTION,null,null,-1);
+ }
+
+ private static String run(String exe, String backend, int exit) throws Exception {
+ return run(exe,backend,exit,null);
+ }
+
+ private static String run(String exe, String backend, int exit, Path cwd) throws Exception {
+ ProcessBuilder pb = new ProcessBuilder(Path.of(exe).toAbsolutePath().toString()).redirectErrorStream(true);
+ if (cwd!=null) pb.directory(cwd.toFile());
+ if( backend == null ) pb.environment().remove("SIMPLE_MINIFB");
+ else pb.environment().put("SIMPLE_MINIFB",backend);
+ Process child = pb.start();
+ try {
+ assertTrue("native program timed out",child.waitFor(10,TimeUnit.SECONDS));
+ String output = new String(child.getInputStream().readAllBytes(),java.nio.charset.StandardCharsets.UTF_8);
+ assertEquals(output,exit,child.exitValue());
+ return output;
+ } finally { if( child.isAlive() ) child.destroyForcibly(); }
+ }
+
+ @Test public void missingConfiguration() throws Exception {
+ Path dir=Files.createTempDirectory(Path.of("build/objs"),"graphics-missing-");
+ Path bin=Files.createDirectory(dir.resolve("bin"));
+ Path exe=Files.copy(Path.of(graphics),bin.resolve(Path.of(graphics).getFileName()));
+ String output = run(exe.toString(),null,1,dir);
+ assertTrue(output,output.contains("MiniFB backend not found"));
+ assertTrue(output,output.contains("make graphics"));
+ }
+
+ @Test public void discoverBackendWithoutConfiguration() throws Exception {
+ Path root=Files.createTempDirectory(Path.of("build/objs"),"graphics-discovery-");
+ String abi=TestC.OS.startsWith("Windows") ? "x86_64_v2_win64" : "x86_64_v2_SystemV";
+ String name=TestC.OS.startsWith("Windows") ? "simple_minifb.dll" : "libsimple_minifb.so";
+ Path source=root.resolve("backend.c");
+ Files.writeString(source,"""
+ #include "simple_graphics.h"
+ #include <stdio.h>
+ static int32_t open_window(const char *s,uint32_t w,uint32_t h) {
+ puts("discovered mock MiniFB"); return -1;
+ }
+ static int32_t present(const uint32_t *p,uint32_t n) { return 0; }
+ static int32_t poll_events(int32_t n) { return 0; }
+ static int32_t position(void) { return 0; }
+ static int32_t wait_events(int64_t n) { return 0; }
+ static void close_window(void) {}
+ static const struct simple_graphics_api api={SIMPLE_GRAPHICS_VERSION,
+ open_window,present,poll_events,position,position,poll_events,poll_events,close_window,wait_events};
+ const struct simple_graphics_api *simple_minifb_api(void) { return &api; }
+ """);
+ Path mock=root.resolve(name);
+ TestC.exec(30,"gcc","-shared","-fPIC","-Isrc/main/c",source.toString(),"-o",mock.toString());
+ Path cwd=Files.createDirectory(root.resolve("empty"));
+ Path bin=Files.createDirectory(root.resolve("bin"));
+ Path exe=Files.copy(Path.of(graphics),bin.resolve(Path.of(graphics).getFileName()));
+ Path beside=Files.copy(mock,bin.resolve(name));
+ assertTrue(run(exe.toString(),null,1,cwd).contains("discovered mock MiniFB"));
+ // Explicit bad overrides must not silently fall back to the mock.
+ assertTrue(run(exe.toString(),"no-such-backend",1,cwd).contains("cannot load MiniFB"));
+ Files.delete(beside);
+ Path release=Files.createDirectories(root.resolve("build/release/"+abi));
+ Files.copy(mock,release.resolve(name));
+ Path examples=Files.createDirectories(root.resolve("build/examples/"+abi));
+ Path example=Files.copy(exe,examples.resolve(exe.getFileName()));
+ assertTrue(run(example.toString(),null,1,cwd).contains("discovered mock MiniFB"));
+ assertTrue(run(exe.toString(),null,1,root).contains("discovered mock MiniFB"));
+ }
+
+ @Test public void disabledGraphics() throws Exception {
+ assertTrue(run(graphics,"off",1).contains("disabled"));
+ }
+
+ @Test public void gameWithoutBackend() throws Exception {
+ assertTrue(run(game,"off",1).contains("disabled"));
+ }
+
+ @Test public void clockWithoutBackend() throws Exception {
+ assertTrue(run(clock,"off",1).contains("disabled"));
+ }
+
+ @Test public void breakoutWithoutBackend() throws Exception {
+ assertTrue(run(breakout,"off",1).contains("disabled"));
+ }
+
+ @Test public void breakoutInputAndFrames() throws Exception {
+ Path source=Path.of("build/objs/breakout_input.c");
+ Path library=Path.of("build/objs/breakout_input"+(TestC.OS.startsWith("Windows") ? ".dll" : ".so"));
+ Files.writeString(source,"""
+ #include "simple_graphics.h"
+ #include <stdio.h>
+ #include <unistd.h>
+ static int frames, previous_ball, previous_paddle;
+ static int32_t open_window(const char *s,uint32_t w,uint32_t h) {
+ return w==1008 && h==760 ? 0 : -1;
+ }
+ static int32_t present(const uint32_t *p,uint32_t n) {
+ if(n!=1008*760) return -1;
+ int paddle=-1,ball=-1;
+ for(int x=24;x<984;x++) if(p[675*1008+x]==0x4ce0ef) { paddle=x; break; }
+ for(int y=560;y<674 && ball<0;y++)
+ for(int x=24;x<984;x++) if(p[y*1008+x]==0xeef7ff) { ball=y; break; }
+ int ok=paddle>=0 && ball>=0;
+ if(frames==0) ok=ok && paddle==448 && ball==658;
+ if(frames==1 || frames==6 || frames==9) ok=ok && ball<previous_ball;
+ if(frames==2 || frames==3) ok=ok && paddle>previous_paddle;
+ if(frames==4 || frames==5) ok=ok && paddle==previous_paddle && ball==previous_ball;
+ if(frames==7) ok=ok && paddle==584;
+ if(frames==8) ok=ok && paddle==448 && ball==658;
+ if(!ok) {
+ printf("bad frame %d: paddle %d (was %d), ball %d (was %d)\\n",
+ frames,paddle,previous_paddle,ball,previous_ball);
+ return -1;
+ }
+ previous_ball=ball; previous_paddle=paddle;
+ if(++frames==10) puts("breakout input smoke passed");
+ return 0;
+ }
+ static int32_t poll_events(int32_t n) { return 0; }
+ static int32_t mouse_x(void) { return frames>=7 ? 640 : 504; }
+ static int32_t mouse_y(void) { return 680; }
+ static int32_t key(int32_t k) {
+ return (k==' ' && (frames==1 || frames==9)) ||
+ (k==262 && (frames==2 || frames==3)) ||
+ (k=='P' && (frames==4 || frames==6)) ||
+ (k=='R' && (frames==8 || frames==9));
+ }
+ static int32_t wait_events(int64_t ms) { usleep(ms*1000); return 0; }
+ static void close_window(void) {}
+ static const struct simple_graphics_api api={SIMPLE_GRAPHICS_VERSION,
+ open_window,present,poll_events,mouse_x,mouse_y,poll_events,key,close_window,wait_events};
+ const struct simple_graphics_api *simple_minifb_api(void) { return &api; }
+ """);
+ TestC.exec(30,"gcc","-shared","-fPIC","-Isrc/main/c",source.toString(),"-o",library.toString());
+ var pb=new ProcessBuilder(Path.of(breakout).toAbsolutePath().toString(),"smoke").redirectErrorStream(true);
+ pb.environment().put("SIMPLE_MINIFB",library.toAbsolutePath().toString());
+ pb.environment().put("SIMPLE_MINIAUDIO","off");
+ Process child=pb.start();
+ try {
+ assertTrue("Breakout timed out",child.waitFor(10,TimeUnit.SECONDS));
+ String output=new String(child.getInputStream().readAllBytes(),java.nio.charset.StandardCharsets.UTF_8);
+ assertEquals(output,0,child.exitValue());
+ assertEquals("breakout input smoke passed\n",output);
+ } finally { if(child.isAlive()) child.destroyForcibly(); }
+ }
+
+ @Test public void missingLibrary() throws Exception {
+ String missing = Path.of("build","no-such-minifb-backend").toAbsolutePath().toString();
+ String output = run(graphics,missing,1);
+ assertTrue(output,output.contains("cannot load MiniFB backend"));
+ }
+
+ @Test public void textIgnoresGraphicsConfiguration() throws Exception {
+ assertEquals("text still works\n",run(text,"off",0));
+ assertEquals("text still works\n",run(text,"no-such-backend",0));
+ }
+
+ @Test public void incompatibleLibrary() throws Exception {
+ Path source = Path.of("build/objs/graphics_incompatible.c");
+ Path library = Path.of("build/objs/graphics_incompatible" +
+ (TestC.OS.startsWith("Windows") ? ".dll" : ".so"));
+ Files.writeString(source,"""
+ #include "simple_graphics.h"
+ static const struct simple_graphics_api api = {999};
+ const struct simple_graphics_api *simple_minifb_api(void) { return &api; }
+ """);
+ TestC.exec(30,"gcc","-shared","-fPIC","-Isrc/main/c",source.toString(),"-o",library.toString());
+ assertTrue(run(graphics,library.toAbsolutePath().toString(),1).contains("incompatible MiniFB backend"));
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/IntegerLiteralTest.java b/src/test/java/com/seaofnodes/simple/IntegerLiteralTest.java
new file mode 100644
index 00000000..975042c2
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/IntegerLiteralTest.java
@@ -0,0 +1,46 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.type.TypeFloat;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class IntegerLiteralTest {
+ private static void value(String expression, long expected) {
+ var code = new CodeGen("return "+expression+";").parse();
+ assertEquals(expression,TypeInteger.constant(expected),code.expr()._type);
+ }
+
+ @Test public void valuesAndBitPatterns() {
+ value("0x00e47b57",14973783);
+ value("0XfF",255);
+ value("025755",11245); // A: rows 010, 101, 111, 101, 101.
+ value("000000",0);
+ value("0x7fffffffffffffff",Long.MAX_VALUE);
+ value("0x8000000000000000",Long.MIN_VALUE);
+ value("0xffffffffffffffff",-1);
+ value("0777777777777777777777",Long.MAX_VALUE);
+ value("01000000000000000000000",Long.MIN_VALUE);
+ value("01777777777777777777777",-1);
+ value("0x00000000000000000001",1);
+ value("-0x2a + 052",0);
+ value("0x1e+2",32); // e and f are hex digits, not float markers.
+ value("0xff & 017",15);
+ value("017<<0x4",240);
+ value("0x0f",15);
+ value("0123",83);
+ value("000000000000000000000000000000000001",1);
+ assertEquals(TypeFloat.constant(12.5),new CodeGen("return 012.5;").parse().expr()._type);
+ assertEquals(TypeFloat.constant(12.0),new CodeGen("return 012f;").parse().expr()._type);
+ }
+
+ @Test public void nativeLiterals() throws Exception {
+ TestC.runSF("""
+ val check_noInline = { i64 mask, u32 color, int glyph ->
+ return mask==-1 && color==14973783 && glyph==11245 ? 0 : 1;
+ };
+ val main = { -> return check_noInline(0xffffffffffffffff,0x00e47b57,025755); };
+ ""","basedLiterals","",-1);
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/IntrinsicTest.java b/src/test/java/com/seaofnodes/simple/IntrinsicTest.java
new file mode 100644
index 00000000..e3b30efb
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/IntrinsicTest.java
@@ -0,0 +1,188 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
+import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class IntrinsicTest {
+ private static final String DECL="""
+ { f64 -> i64 } simple_f64_bits="C";
+ { i64 -> f64 } simple_f64_from_bits="C";
+ """;
+
+ @Test public void constantsAndRawNaNs() {
+ long[] patterns={0,Long.MIN_VALUE,1,0x0010000000000000L,0x3ff0000000000000L,
+ 0x7fefffffffffffffL,0x7ff0000000000000L,0xfff0000000000000L,
+ 0x7ff8000000000001L,0x7ff8000000000002L,0xfff800000000002aL,0x7ff0000000000001L};
+ for(long bits:patterns) {
+ String literal=bits==Long.MIN_VALUE ? "(-9223372036854775807-1)" : Long.toString(bits);
+ CodeGen code=new CodeGen(DECL+"return simple_f64_bits(simple_f64_from_bits("+literal+"));")
+ .driver(CodeGen.Phase.TypeCheck);
+ assertNull(code._stop.walk(n -> n instanceof CallNode ? n : null));
+ assertEquals(Long.toString(bits),Eval2.eval(code,0));
+ }
+ assertNotSame(TypeFloat.constant(Double.longBitsToDouble(patterns[8])),
+ TypeFloat.constant(Double.longBitsToDouble(patterns[9])));
+ }
+
+ @Test public void runtimeNodesAndMemory() {
+ CodeGen code=new CodeGen(DECL+"""
+ val bits=simple_f64_bits;
+ val a=new f64[1];
+ a[0]=simple_f64_from_bits(arg);
+ if( arg==0 ) a[0]=-0.0;
+ return bits(a[0]);
+ """).driver(CodeGen.Phase.TypeCheck);
+ assertNotNull(code._stop.walk(n -> n instanceof Intrinsic.BitCastNode ? n : null));
+ assertNull(code._stop.walk(n -> n instanceof CallNode ? n : null));
+ assertEquals(Long.toString(Long.MIN_VALUE),Eval2.eval(code,0));
+ assertEquals("9221120237041090602",Eval2.eval(code,0x7ff800000000002aL));
+ }
+
+ @Test public void ordinaryFunctionWithSameName() {
+ CodeGen code=new CodeGen("""
+ val simple_f64_bits={ f64 x -> 42; };
+ return simple_f64_bits(1.0);
+ """);
+ assertEquals("42",Eval2.eval(code,0));
+ }
+
+ @Test public void wrongSignature() {
+ try {
+ new CodeGen("{ i64 -> i64 } simple_f64_bits=\"C\"; return simple_f64_bits(arg);")
+ .driver(CodeGen.Phase.TypeCheck);
+ fail("The reserved C symbol has fixed semantics and a fixed signature");
+ } catch(RuntimeException e) {
+ assertTrue(e.getMessage(),e.getMessage().contains("Intrinsic 'simple_f64_bits' requires"));
+ }
+ }
+
+ @Test public void nativeRepresentations() throws Exception {
+ TestC.runSYS("""
+ val bits_noInline={ f64 x -> sys.math.f64Bits(x); };
+ val float_noInline={ i64 x -> sys.math.f64FromBits(x); };
+ val main={ ->
+ val patterns=new i64[~]{0,sys.integer.MIN_I64,1,0x0010000000000000,
+ 0x3ff0000000000000,0x7fefffffffffffff,0x7ff0000000000000,
+ 0x7ff8000000000001,0x7ff8000000000002,-2251799813685206,0x7ff0000000000001};
+ for( int i=0; i<patterns#; i++ )
+ if( bits_noInline(float_noInline(patterns[i]))!=patterns[i] ) return 1;
+ if( bits_noInline(-0.0)!=sys.integer.MIN_I64 ) return 2;
+ if( float_noInline(0x4008000000000000)!=3.0 ) return 3;
+ int numeric=3.75;
+ if( numeric!=3 ) return 4;
+ return 0;
+ };
+ ""","intrinsicNative","",-1);
+ }
+
+ @Test public void indirectCallsUseCFallback() throws Exception {
+ String source=DECL+"""
+ { f64 -> i64 } llround="C";
+ val apply_noInline={ { f64 -> i64 } f, f64 x -> f(x); };
+ val main={ ->
+ if( apply_noInline(simple_f64_bits,-0.0)!=(-9223372036854775807-1) ) return 1;
+ if( apply_noInline(llround,3.75)!=4 ) return 2;
+ if( apply_noInline(simple_f64_bits,1.0)!=0x3ff0000000000000 ) return 3;
+ if( apply_noInline(simple_f64_bits,simple_f64_from_bits(0x7ff800000000002a))!=0x7ff800000000002a ) return 4;
+ return 0;
+ };
+ """;
+ CodeGen code=new CodeGen(source).driver(CodeGen.Phase.TypeCheck);
+ assertNotNull("The indirect C call must survive",code._stop.walk(n ->
+ n instanceof CallNode c && c.external() && !c.tfp().isConstant() ? n : null));
+ TestC.runSYS(source,"intrinsicFallback","",-1);
+ }
+
+ @Test public void importedNodesAndNaNConstants() throws Exception {
+ Path dir=Files.createTempDirectory(Path.of("build/objs"),"intrinsic-import-");
+ String library=DECL+"""
+ val bits_noInline={ f64 x -> simple_f64_bits(x); };
+ val float_noInline={ i64 x -> simple_f64_from_bits(x); };
+ val nan=simple_f64_from_bits(0x7ff8000000000001);
+ val otherNaN=simple_f64_from_bits(0x7ff8000000000002);
+ """;
+ new CodeGen(null,dir.toString(),null,"Bits",library,126,true,TypeInteger.BOT)
+ .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
+ var paths=new Ary<>(new String[]{dir.toString()});
+ String[] clients={
+ "return Bits.bits_noInline(Bits.float_noInline(arg));",
+ "return Bits.bits_noInline(Bits.nan);",
+ "return Bits.bits_noInline(Bits.otherNaN);"
+ };
+ long[] expected={0x7ff800000000002aL,0x7ff8000000000001L,0x7ff8000000000002L};
+ for(int i=0;i<clients.length;i++) {
+ CodeGen client=new CodeGen(null,dir.toString(),paths,"Client",clients[i],126,true,TypeInteger.BOT);
+ assertEquals(Long.toString(expected[i]),Eval2.eval(client,expected[0]));
+ }
+ }
+
+ @Test public void allBackendsSelectRepresentationMoves() {
+ // The array store/load and branch keep both directions alive, rather
+ // than allowing the inverse-cast peephole to erase the whole test.
+ String source=DECL+"""
+ val a=new f64[1];
+ a[0]=simple_f64_from_bits(arg);
+ if( arg==0 ) a[0]=-0.0;
+ return simple_f64_bits(a[0]);
+ """;
+ for(String cpu:new String[]{"x86_64_v2","arm","riscv"}) {
+ CodeGen code=new CheckedCodeGen(source).driver(CodeGen.Phase.Encoding,cpu,"SystemV");
+ assertNotNull(code._stop.walk(n -> n instanceof MachConcreteNode m && m.op().equals("f64bits") ? n : null));
+ assertNotNull(code._stop.walk(n -> n instanceof MachConcreteNode m && m.op().equals("f64frombits") ? n : null));
+ }
+ }
+
+ @Test public void invalidArgumentIsNotInlinedAway() {
+ try {
+ new CodeGen(DECL+"return simple_f64_bits(1);").driver(CodeGen.Phase.TypeCheck);
+ fail("Intrinsic inlining must not hide the call's type error");
+ } catch(RuntimeException e) {
+ assertTrue(e.getMessage(),e.getMessage().contains("Argument #1 isa"));
+ }
+ }
+
+ @Test public void discardedValueKeepsArgumentEffects() throws Exception {
+ TestC.runSYS(DECL+"""
+ val next={ i64[] a -> a[0]++; return 1.0; };
+ val main={ ->
+ val a=new i64[1];
+ simple_f64_bits(next(a));
+ return a[0]!=1;
+ };
+ ""","intrinsicEffects","",-1);
+ }
+
+ @Test public void emulatedRepresentationMoves() throws Exception {
+ long[] patterns={0,Long.MIN_VALUE,1,0x3ff0000000000000L,0x7ff0000000000000L,
+ 0xfff0000000000000L,0x7ff800000000002aL,0x7ff0000000000001L};
+ for(boolean toBits:new boolean[]{true,false}) {
+ String source=toBits
+ ? "val cast={ f64 x -> { f64 -> i64 } simple_f64_bits=\"C\"; return simple_f64_bits(x); };"
+ : "val cast={ i64 x -> { i64 -> f64 } simple_f64_from_bits=\"C\"; return simple_f64_from_bits(x); };";
+ var arm=TestArm64.build("cast",source,0,-1,false);
+ var risc=TestRisc5.build(source,"cast",0,-1,false);
+ int armStart=arm._pc, riscStart=risc._pc;
+ for(long bits:patterns) {
+ arm._pc=armStart; risc._pc=riscStart;
+ if(toBits) {
+ arm.fregs[0]=Double.longBitsToDouble(bits);
+ risc.fregs[10]=Double.longBitsToDouble(bits);
+ } else {
+ arm.regs[0]=bits;
+ risc.regs[10]=bits;
+ }
+ assertEquals(0,arm.step(1000));
+ assertEquals(0,risc.step(1000));
+ assertEquals(bits,toBits ? arm.regs[0] : Double.doubleToRawLongBits(arm.fregs[0]));
+ assertEquals(bits,toBits ? risc.regs[10] : Double.doubleToRawLongBits(risc.fregs[10]));
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java b/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java
index adeba136..6d81af85 100644
--- a/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java
+++ b/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java
@@ -8,6 +8,31 @@ import static org.junit.Assert.*;
// Execution regressions are separate from the fixed spill-measurement cohorts.
public class MemoryEncodingTest {
+ @Test public void testIntegerMemoryOperandsAcrossCalls() throws java.io.IOException {
+ TestC.runSF("""
+ val touch_noInline={i64[] a, i64 i -> a[i]+=1;};
+ val scan_noInline={i64[] a, i64 limit, i64 delta ->
+ int below=0, above=0;
+ for(int i=0;i<a#;i++) {
+ touch_noInline(a,i);
+ if(a[i]<limit) below++;
+ if(limit<a[i]) above++;
+ a[i]+=delta;
+ }
+ return below*10+above;
+ };
+ val main={->
+ val a=new i64[]{0x7ffffffffffffffe,-2,0};
+ if(scan_noInline(a,0x7fffffffffffffff,0)!=20) return 1;
+ if(a[0]!=0x7fffffffffffffff || a[1]!=-1 || a[2]!=1) return 2;
+ val b=new i64[]{-3,-2,0};
+ if(scan_noInline(b,-1,0x100000000)!=11) return 3;
+ if(b[0]!=0xfffffffe || b[1]!=0xffffffff || b[2]!=0x100000001) return 4;
+ return 0;
+ };
+ ""","integerMemoryAcrossCalls","",-1);
+ }
+
@Test public void testLoopLoadSearch() {
String[] sources = {
"""
diff --git a/src/test/java/com/seaofnodes/simple/MethodAccessTest.java b/src/test/java/com/seaofnodes/simple/MethodAccessTest.java
new file mode 100644
index 00000000..7275b151
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/MethodAccessTest.java
@@ -0,0 +1,118 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.parser.Parser;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.util.Ary;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class MethodAccessTest {
+ @Test public void readersAcceptReadOnlyReceivers() throws Exception {
+ String source = """
+ struct Child { int n = 7; };
+ struct Box {
+ !Child child = new Child;
+ int[] values = new int[]{3,5};
+ int count = 2;
+ { i64 } read = { -> count+child.n+values[1]; };
+ val delegate = { -> read(); };
+ val recurse = { i64 n -> n == 0 ? read() : recurse(n-1); };
+ val draw = { !Child canvas -> canvas.n = count; };
+ val outer = { -> later(count); };
+ };
+ val later = { int n -> n+1; };
+ val check = { Box box ->
+ val canvas = new Child;
+ box.draw(canvas);
+ return box.read()+box.delegate()+box.recurse(3)+canvas.n+box.outer();
+ };
+ return check(new Box);
+ """;
+ assertEquals("47",Eval2.eval(new CodeGen(source).driver(CodeGen.Phase.LocalSched),0));
+ TestC.runSF("val main = { -> "+source.replace("return check(new Box);","return check(new Box) != 47;")+" };",
+ "readonlyMethods","",-1);
+ }
+
+ @Test public void writesThroughReceiverRequireAccess() {
+ String[] methods = {
+ "val act = { -> x = 2; };",
+ "val act = { -> self.x++; };",
+ "val act = { -> val nested = { -> x++; }; nested(); };",
+ "val act = { -> val alias = self; alias.x += 1; };",
+ "val act = { -> child.n = 2; };",
+ "val act = { -> values[0]++; };",
+ "val act = { -> write(); }; val write = { -> x++; };",
+ "val act = { -> val f = x == 0 ? left : right; f(); }; val left = { -> x++; }; val right = { -> x--; };",
+ "val act = { -> child.write(); };",
+ "val act = { -> write(self); };",
+ "{ int } act = \"C\";"
+ };
+ for( String method : methods ) {
+ String source = """
+ struct Child { int n; val write = { -> n++; }; };
+ struct Box {
+ int x;
+ !Child child = new Child;
+ int[] values = new int[1];
+ """+method+"""
+ };
+ val write = { !Box box -> box.x++; };
+ val check = { Box box -> box.act(); return 0; };
+ return check(new Box);
+ """;
+ try {
+ new CodeGen(source).driver(CodeGen.Phase.TypeCheck);
+ fail("Accepted writer: "+method);
+ } catch( Parser.ParseException e ) {
+ assertTrue(method+": "+e.getMessage(),e.getMessage().contains("read-only"));
+ }
+ new CodeGen(source.replace("val check = { Box box", "val check = { !Box box"))
+ .driver(CodeGen.Phase.TypeCheck);
+ }
+ }
+
+ @Test public void returningWritableAliasesRequiresAccess() {
+ String source = """
+ struct Box { int x; val alias = { -> self; }; };
+ val check = { Box box -> box.alias(); };
+ return check(new Box);
+ """;
+ try {
+ new CodeGen(source).driver(CodeGen.Phase.TypeCheck);
+ fail("Read-only receiver exposed as writable");
+ } catch( Parser.ParseException e ) {
+ assertTrue(e.getMessage(),e.getMessage().contains("read-only"));
+ }
+ source = source.replace("val alias = { -> self; }", "val alias = { -> Box view = self; view; }");
+ new CodeGen(source).driver(CodeGen.Phase.TypeCheck);
+ }
+
+ @Test public void importedReceiverAccess() throws Exception {
+ var root = Files.createDirectories(Path.of("build/objs/separate"));
+ var lib = Files.createTempDirectory(root,"method-access-");
+ new CodeGen(null,lib.toString(),null,"Counters","""
+ struct Counter {
+ int n = 7;
+ val read = { -> n; };
+ val write = { -> n++; };
+ };
+ val make = { -> new Counter; };
+ """,126,true,TypeInteger.BOT)
+ .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
+ var paths = new Ary<>(new String[]{lib.toString()});
+ String source = "val box = Counters.make(); val check = { Counters.Counter c -> c.read(); }; return check(box);";
+ var code = new CodeGen(null,"build/objs",paths,"CounterReader",source,126,true,
+ TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("7",Eval2.eval(code,0));
+ try {
+ new CodeGen(null,"build/objs",paths,"CounterWriter",source.replace("c.read()","c.write()"),126,true,
+ TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ fail("Imported writer accepted a read-only receiver");
+ } catch( Parser.ParseException e ) {
+ assertTrue(e.getMessage(),e.getMessage().contains("read-only"));
+ }
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/MethodFieldsTest.java b/src/test/java/com/seaofnodes/simple/MethodFieldsTest.java
new file mode 100644
index 00000000..3fe5c28b
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/MethodFieldsTest.java
@@ -0,0 +1,131 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.parser.Parser;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class MethodFieldsTest {
+ @Test public void nestedFunctionStillRejectsLocalCapture() {
+ String source = """
+ struct Game {
+ int count;
+ val input = { int amount ->
+ val action = { -> count += amount; };
+ action();
+ };
+ };
+ return 0;
+ """;
+ try {
+ new CodeGen(source).driver(CodeGen.Phase.Parse);
+ fail("Captured a nonconstant local parameter");
+ } catch( Parser.ParseException e ) {
+ assertEquals("Variable 'amount' is out of function scope and must be a final constant",e.getMessage());
+ }
+ }
+
+ @Test public void nestedFunctionReceiver() throws Exception {
+ String source = """
+ struct Game {
+ bool held, paused;
+ int count;
+ val debounce = { bool pressed, bool wasHeld, { bool } action ->
+ if( pressed && !wasHeld ) action();
+ return pressed;
+ };
+ val bump = { -> count++; return false; };
+ val input = { bool pressed ->
+ if( true ) {
+ held = debounce(pressed,held,{ ->
+ paused = !paused;
+ bump();
+ val again = { -> count += 2; return false; };
+ again();
+ return false;
+ });
+ }
+ };
+ val named = { -> debounce(true,false,bump); };
+ };
+ val a = new Game, b = new Game;
+ a.input(true); a.input(true);
+ if( a.count != 3 || !a.paused || b.count != 0 || b.paused ) return 1;
+ a.input(false); a.input(true);
+ if( a.count != 6 || a.paused ) return 2;
+ b.input(true); b.named();
+ if( b.count != 4 || !b.paused || a.count != 6 ) return 3;
+ return 0;
+ """;
+ assertEquals("0",Eval2.eval(new CodeGen(source).driver(CodeGen.Phase.LocalSched),0));
+ TestC.runSF("val main = { -> "+source+" };","nestedFunctionReceiver","",-1);
+ }
+
+ @Test public void laterSiblingParameterType() throws Exception {
+ String source = """
+ struct Game {
+ val input = { !Controls keys -> keys.presses++; return keys.presses; };
+ val read = { Controls keys -> keys.presses; };
+ };
+ struct Controls { int presses = 4; };
+ val game = new Game, keys = new Controls;
+ if( game.input(keys) != 5 || game.read(keys) != 5 ) return 1;
+ return 0;
+ """;
+ assertEquals("0",Eval2.eval(new CodeGen(source).driver(CodeGen.Phase.LocalSched),0));
+ TestC.runSF("val main = { -> "+source+" };","forwardMethodType","",-1);
+ }
+
+ private static final String FIELDS = """
+ int count = 100;
+ struct Counter {
+ val step = { int n ->
+ count = count+n;
+ ++count;
+ count += 2;
+ speed *= 2.0;
+ return read();
+ };
+ val local = { int count -> count+1; };
+ val read = { -> count; };
+ int count = 3;
+ f64 speed = 1.5;
+ };
+ val a = new Counter, b = new Counter;
+ if( a.step(4) != 10 || b.read() != 3 || a.speed != 3.0 ) return 1;
+ if( a.local(20) != 21 || a.read() != 10 || count != 100 ) return 2;
+ if( b.step(1) != 7 || a.read() != 10 ) return 3;
+ return 0;
+ """;
+
+ @Test public void laterFieldsAndMethods() throws Exception {
+ assertEquals("0",Eval2.eval(new CodeGen(FIELDS).driver(CodeGen.Phase.LocalSched),0));
+ TestC.runSF("val main = { -> "+FIELDS+" };","methodFields","",-1);
+ }
+
+ @Test public void laterGlobalAndRecursiveMethod() {
+ String source = """
+ struct Counter {
+ val step = { i64 n ->
+ if( n == 0 ) return value;
+ value += increment;
+ return step(n-1);
+ };
+ int value = 1;
+ };
+ val increment = 2;
+ return new Counter.step(3);
+ """;
+ assertEquals("7",Eval2.eval(new CodeGen(source).driver(CodeGen.Phase.LocalSched),0));
+ }
+
+ @Test public void laterFinalFieldStillRejectsWrites() {
+ String source = "struct C { val change = { -> x = 2; }; int ~x = 1; }; return new C.change();";
+ try {
+ new CodeGen(source).driver(CodeGen.Phase.TypeCheck);
+ fail("Method wrote a final field");
+ } catch( Parser.ParseException e ) {
+ assertEquals("Cannot reassign final 'x'",e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java b/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java
index 2b7eea78..d342892c 100644
--- a/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java
+++ b/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java
@@ -10,6 +10,57 @@ import org.junit.Test;
import static org.junit.Assert.*;
public class ParserDiagnosticTest {
+ @Test public void testMissingForwardTypeDiagnostic() {
+ String source = """
+ val read = { Missing value ->
+ return value.field;
+ };
+ return 0;
+ """;
+ var error = rejected(source);
+ assertEquals("Unknown type 'Test.Missing'",error.getMessage());
+ assertTrue(error.diagnostic(),error.diagnostic().startsWith("Test.smp:2:"));
+ assertTrue(error.diagnostic(),error.diagnostic().contains("return value.field;"));
+ }
+
+ @Test public void testReadOnlyReceiverDiagnostic() {
+ String source = """
+ struct Box {
+ int x = 1;
+ val bump = { -> x++; x; };
+ };
+ val draw = { Box b ->
+ return b.bump();
+ };
+ return draw(new Box);
+ """;
+ try {
+ new CodeGen(source).driver(CodeGen.Phase.TypeCheck);
+ fail("Read-only receiver accepted by a writable method");
+ } catch( Parser.ParseException e ) {
+ assertEquals("Receiver for method 'bump' is read-only Test.Box; expected writable Test.Box",e.getMessage());
+ assertTrue(e.diagnostic(),e.diagnostic().startsWith("Test.smp:6:18: error: "));
+ assertFalse(e.getMessage().contains("{i32"));
+ }
+ }
+
+ @Test public void testReadOnlyArgumentDiagnostic() {
+ String source = """
+ struct Box { int x = 1; };
+ val write = { !Box b -> b.x = 2; };
+ val draw = { Box b -> write(b); };
+ return draw(new Box);
+ """;
+ try {
+ new CodeGen(source).driver(CodeGen.Phase.TypeCheck);
+ fail("Read-only argument accepted by a writable parameter");
+ } catch( Parser.ParseException e ) {
+ assertEquals("Argument #1 is read-only Test.Box; expected writable Test.Box",e.getMessage());
+ }
+ new CodeGen(source.replace("val draw = { Box b", "val draw = { !Box b"))
+ .driver(CodeGen.Phase.TypeCheck);
+ }
+
private static Parser.ParseException rejected(String source) {
try {
new CodeGen(source).driver(CodeGen.Phase.Parse);
@@ -30,7 +81,18 @@ public class ParserDiagnosticTest {
diagnostic("return 9223372036854775808;",1,8,"Integer literal is outside the signed 64-bit range");
diagnostic("return 1e;",1,8,"Invalid floating-point literal '1e'");
diagnostic("return 1.2.3f;",1,8,"Invalid floating-point literal '1.2.3f'");
- diagnostic("return 012;",1,8,"Syntax error: integer values cannot start with '0'");
+ diagnostic("return 018;",1,8,"Invalid octal integer literal '018'");
+ }
+
+ @Test public void testBasedIntegerLiteralErrors() {
+ diagnostic("return 0x;",1,8,"Hexadecimal integer literal requires at least one digit");
+ diagnostic("return 0x12g;",1,8,"Invalid hexadecimal integer literal '0x12g'");
+ diagnostic("return 0128;",1,8,"Invalid octal integer literal '0128'");
+ diagnostic("return 0x_ff;",1,8,"Invalid hexadecimal integer literal '0x_ff'");
+ diagnostic("return 09;",1,8,"Invalid octal integer literal '09'");
+ diagnostic("return 0x10000000000000000;",1,8,"Hexadecimal integer literal exceeds 64 bits");
+ diagnostic("return 02000000000000000000000;",1,8,"Octal integer literal exceeds 64 bits");
+ diagnostic("int 0xabc;",1,5,"Expected an identifier, found '0xabc'");
}
@Test public void testUnclosedCommentsAndStrings() {
@@ -123,6 +185,24 @@ public class ParserDiagnosticTest {
+"\n return x;\n ^",error.diagnostic());
}
+ @Test public void testJUnitReportsSourceDiagnostic() {
+ var error=rejected("val dist=new i64[3]{int i -> i;}\ndist[0]=0;");
+ String expected="Test.smp:2:1: error: Syntax error, expected `;` but found `dist`"
+ +"\n dist[0]=0;\n ^"+System.lineSeparator();
+ var test=org.junit.runner.Description.createTestDescription(getClass(),"missingSemicolon");
+ assertEquals(expected,new org.junit.runner.notification.Failure(test,error).getTrace());
+
+ var bytes=new java.io.ByteArrayOutputStream();
+ error.printStackTrace(new java.io.PrintStream(bytes,true,StandardCharsets.UTF_8));
+ assertEquals(expected,bytes.toString(StandardCharsets.UTF_8));
+ assertEquals("Syntax error, expected `;` but found `dist`",error.getMessage());
+
+ // Compiler implementation failures must still include Java frames.
+ String trace=new org.junit.runner.notification.Failure(test,new IllegalStateException("compiler bug")).getTrace();
+ assertTrue(trace,trace.contains("java.lang.IllegalStateException: compiler bug"));
+ assertTrue(trace,trace.contains("ParserDiagnosticTest.testJUnitReportsSourceDiagnostic"));
+ }
+
@Test public void testEOF() {
var error=rejected("return 1");
assertEquals("Test.smp:1:9: error: Syntax error, expected `;` but found `EOF`"
diff --git a/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
index 1853aa48..1c278791 100644
--- a/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
+++ b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
@@ -10,6 +10,22 @@ import org.junit.Test;
import static org.junit.Assert.*;
public class SeparateCompilationTest {
+ @Test public void testImportedReadOnlyPrimitiveArray() throws Exception {
+ Path build=Path.of("build/objs/separate");
+ Files.createDirectories(build);
+ Path lib=Files.createTempDirectory(build,"pixels-");
+ new CodeGen(null,lib.toString(),null,"Pixels",
+ "val first={ u32[~] pixels -> pixels[0]; };",123,true,TypeInteger.BOT)
+ .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
+ var paths=new Ary<>(new String[]{lib.toString()});
+ CodeGen code=new CodeGen(null,"build/objs",paths,"PixelClient","""
+ val pixels=new u32[2];
+ pixels[0]=4294967295;
+ return Pixels.first(pixels);
+ """,123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("4294967295",Eval2.eval(code,0));
+ }
+
@Test public void testImportedNativeExposure() throws Exception {
Path build=Path.of("build/objs/separate");
Files.createDirectories(build);
diff --git a/src/test/java/com/seaofnodes/simple/StreamIOTest.java b/src/test/java/com/seaofnodes/simple/StreamIOTest.java
index f3491314..7b459e75 100644
--- a/src/test/java/com/seaofnodes/simple/StreamIOTest.java
+++ b/src/test/java/com/seaofnodes/simple/StreamIOTest.java
@@ -131,6 +131,10 @@ public class StreamIOTest extends TestC {
gcc.addAll(linkObjs(code));
gcc.add(runtimeObject("simple_crt"));
gcc.add(runtimeObject("simple_args"));
+ gcc.add(runtimeObject("simple_graphics"));
+ gcc.add(runtimeObject("simple_audio"));
+ gcc.add(runtimeObject("simple_time"));
+ if( !OS.startsWith("Windows") ) gcc.add("-ldl");
gcc.addAll(new String[]{"-lm","-o",exe});
exec(TEST_TIMEOUT_SECONDS,gcc.asAry());
assertEquals("ok\n",exec(TEST_TIMEOUT_SECONDS,exe));
diff --git a/src/test/java/com/seaofnodes/simple/StringFormatTest.java b/src/test/java/com/seaofnodes/simple/StringFormatTest.java
index 53a10092..05e801dc 100644
--- a/src/test/java/com/seaofnodes/simple/StringFormatTest.java
+++ b/src/test/java/com/seaofnodes/simple/StringFormatTest.java
@@ -128,6 +128,10 @@ public class StringFormatTest extends TestC {
@Test public void testWrongTypesAndReservedNames() {
rejects("return \"%d(1.5)\";", "must be a");
+ rejects("return \"%t(1.5)\";", "must be a");
+ rejects("return \"%tQ(0)\";", "Expected a time format");
+ rejects("return \"%.2t(0)\";", "Precision is not supported");
+ rejects("return \"%+t(0)\";", "Invalid flags");
rejects("return \"%s(17)\";", "must be a");
rejects("return \"%(null)\";", "Cannot format");
rejects("val f={int int -> int;}; return 0;", "Expected an identifier");
diff --git a/src/test/java/com/seaofnodes/simple/TestC.java b/src/test/java/com/seaofnodes/simple/TestC.java
index dd7999c9..63bb98da 100644
--- a/src/test/java/com/seaofnodes/simple/TestC.java
+++ b/src/test/java/com/seaofnodes/simple/TestC.java
@@ -190,6 +190,11 @@ public abstract class TestC {
params.add(OS.startsWith("Windows") ? "-Wl,--image-base,0x400000" : "-no-pie");
if( cfile!=null ) params.add(cfile); // Associated C driver, usually has a `main`
params.addAll(objects);
+ // The loader has no MiniFB dependency and opens nothing until gfx.open.
+ params.add(runtimeObject("simple_graphics"));
+ params.add(runtimeObject("simple_audio"));
+ params.add(runtimeObject("simple_time"));
+ if( !OS.startsWith("Windows") ) params.add("-ldl");
// Standalone Simple programs export `simple_main`; the small C
// runtime owns native main and calls the Simple argument converter
// before making the ABI-qualified entry call.
diff --git a/src/test/java/com/seaofnodes/simple/TimeTest.java b/src/test/java/com/seaofnodes/simple/TimeTest.java
new file mode 100644
index 00000000..dfffc1b9
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/TimeTest.java
@@ -0,0 +1,183 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.util.Ary;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class TimeTest extends TestC {
+ private static void checks(String body, String name) throws Exception {
+ runSYS("val main={ -> "+body+" return 0; };",name,"",-1);
+ }
+
+ @Test public void builtInFormats() throws Exception {
+ runSYS("""
+ val main={ ->
+ i64 t=sys.time.parse("2026-11-12T00:34:56.789Z");
+ sys.io.p("%t(t)|%tF(t)|%tr(t)|%tR(t)|%tM(83456)|%tD(t)|%tT(t)\\n");
+ sys.io.p("[%30t(0)][%-10tr(sys.time.hours(12))]\\n");
+ sys.io.p("%tr(0)|%tr(sys.time.hours(13))|%tM(-1)|%tM(-9223372036854775807-1)");
+ return 0;
+ };
+ ""","timeFormats",
+ "2026-11-12T00:34:56.789Z|2026-11-12T00:34:56.789Z|12:34am|00:34|01:23.456|12/11/26|00:34:56\n"+
+ "[ 1970-01-01T00:00:00.000Z][12:00pm ]\n"+
+ "12:00am|1:00pm|-00:00.001|-153722867280912:55.808",-1);
+ }
+
+ @Test public void calendarAgainstJava() throws Exception {
+ long[] values={-62135596800000L,-2203977600000L,-2203891200000L,-86400001,-86400000,-1,0,1,
+ 951782400000L,1709164800123L,1794443696789L,253402300799999L};
+ DateTimeFormatter iso=DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneOffset.UTC);
+ StringBuilder src=new StringBuilder("val show_noInline={i64 t -> sys.io.p(\"%t(t)|\"); val d=sys.time.utc(t); sys.io.p(\"%(d.weekday)\\n\"); }; val main={ -> ");
+ StringBuilder expected=new StringBuilder();
+ for (long value:values) {
+ src.append("show_noInline(").append(value).append(");");
+ var dt=Instant.ofEpochMilli(value).atOffset(ZoneOffset.UTC);
+ expected.append(iso.format(dt)).append('|').append(dt.getDayOfWeek().getValue()%7).append('\n');
+ }
+ src.append("return 0; };");
+ runSYS(src.toString(),"timeCalendar",expected.toString(),-1);
+ }
+
+ @Test public void parseAndConstruction() throws Exception {
+ checks("""
+ if( sys.time.parse("1970-01-01T00:00:00Z")!=0 ) return 1;
+ if( sys.time.parse("1970-01-01T01:00:00.123+01:00")!=123 ) return 2;
+ if( sys.time.parse("1969-12-31T16:00:00-08:00")!=0 ) return 3;
+ if( sys.time.parse("1969-12-31T23:59:59.999Z")!=-1 ) return 4;
+ if( sys.time.parse("1969-12-31T23:59:59.999+01:00")!=-3600001 ) return 5;
+ if( sys.time.parse("1970-01-01T00:00:30+00:00:30")!=0 ) return 6;
+ if( sys.time.parse("2000-02-29T00:00:00Z")!=951782400000 ) return 7;
+ if( sys.time.makeUTC(2024,2,29,0,0,0,123)!=1709164800123 ) return 8;
+ if( sys.time.makeUTC(1900,2,29,0,0,0,0)!=-1 || sys.time.makeUTC(2024,2,29,24,0,0,0)!=-1 ) return 9;
+ if( sys.time.utc(sys.time.MIN-1).year!=0 || sys.time.utc(sys.time.MAX+1).year!=0 ) return 10;
+ ""","timeParse");
+ String[] bad={"","now","2026-11-12","2026-11-12T00:00:00","0000-01-01T00:00:00Z",
+ "1900-02-29T00:00:00Z","2026-02-29T00:00:00Z","2026-13-01T00:00:00Z",
+ "2026-11-00T00:00:00Z","2026-11-12T24:00:00Z","2026-11-12T00:60:00Z",
+ "2026-11-12T00:00:60Z","2026-11-12T00:00:00.1Z","2026-11-12T00:00:00.1234Z",
+ "2026-11-12T00:00:00+24:00","2026-11-12T00:00:00+01:60","2026-11-12T00:00:00Zx",
+ "2026-11-12T00:00:00.00xZ","0001-01-01T00:00:00+01:00"};
+ StringBuilder src=new StringBuilder("val main={ -> ");
+ for (int i=0;i<bad.length;++i)
+ src.append("if(sys.time.parse(\"").append(bad[i]).append("\")!=-1) return ").append(i+1).append(';');
+ src.append("return 0; };");
+ runSYS(src.toString(),"timeBadParse","",-1);
+ }
+
+ @Test public void durationArithmetic() throws Exception {
+ checks("""
+ if( sys.time.seconds(2)!=2000 || sys.time.minutes(2)!=120000 || sys.time.hours(2)!=7200000 || sys.time.days(2)!=172800000 ) return 1;
+ if( sys.time.toSeconds(-1999)!=-1 || sys.time.toMinutes(119999)!=1 || sys.time.toHours(7200000)!=2 || sys.time.toDays(172800000)!=2 ) return 2;
+ if( sys.time.seconds(9223372036854775)!=9223372036854775000 || sys.time.seconds(-9223372036854775)!=-9223372036854775000 ) return 3;
+ if( sys.time.seconds(9223372036854776)!=-1 || sys.time.seconds(-9223372036854776)!=-1 ) return 4;
+ if( sys.time.remaining(5,10)!=0 || sys.time.remaining(10,5)!=5 || sys.time.remaining(9223372036854775807,-1)!=9223372036854775807 ) return 5;
+ if( sys.time.fromNanos(-1999999)!=-1 || sys.time.toNanos(1234)!=1234000000 || sys.time.toNanos(9223372036855)!=-1 ) return 6;
+ ""","timeArithmetic");
+ }
+
+ @Test public void timerOrderingAndMissedTicks() throws Exception {
+ checks("""
+ val q=new sys.time.Timers();
+ if( q.poll(0)!=0 || q.nextDeadline()!=-1 ) return 1;
+ u32 a=q.afterAt(100,20), b=q.afterAt(100,10), c=q.afterAt(100,10);
+ if( a==0 || b==0 || c==0 ) return 10;
+ if( q.nextDeadline()!=110 || q.poll(109)!=0 || q.poll(110)!=b || q.poll(110)!=c || q.poll(110)!=0 ) return 2;
+ if( !q.cancel(a) || q.cancel(a) || q.cancel(0) || q.nextDeadline()!=-1 ) return 3;
+ u32 repeat=q.everyAt(100,10);
+ if( q.poll(135)!=repeat || q.nextDeadline()!=140 || q.poll(135)!=0 ) return 4;
+ if( q.poll(140)!=repeat || q.nextDeadline()!=150 || !q.cancel(repeat) ) return 5;
+ for( int i=0; i<20; i++ ) if( q.afterAt(200,i)==0 ) return 11;
+ u32 prev=0;
+ for( int i=0; i<20; i++ ) { u32 id=q.poll(300); if( id<=prev ) return 6; prev=id; }
+ if( q.nextDeadline()!=-1 || q.poll(1000)!=0 ) return 7;
+ if( q.afterAt(-1,1)!=0 || q.afterAt(0,-1)!=0 || q.everyAt(0,0)!=0 || q.afterAt(9223372036854775807,1)!=0 ) return 8;
+ u32 last=q.everyAt(9223372036854775806,1);
+ if( last==0 || q.poll(9223372036854775807)!=last || q.nextDeadline()!=-1 ) return 9;
+ // Exhaust IDs without wrapping or letting an old ID cancel a new timer.
+ q._next=0xfffffffe;
+ u32 high=q.afterAt(0,10), max=q.afterAt(0,10);
+ if( high!=0xfffffffe || max!=0xffffffff || q.afterAt(0,0)!=0 ) return 12;
+ if( q.cancel(a) || q.poll(10)!=high || q.poll(10)!=max || q.poll(10)!=0 ) return 13;
+ if( q.afterAt(0,0)!=0 || q.nextDeadline()!=-1 ) return 14;
+ ""","timeTimers");
+ }
+
+ private static String run(String source, String name, String zone) throws Exception {
+ String exe=compile(source,name,new Ary<>(new String[]{RELEASE_SYS_DIR}),CALL_CONVENTION,null,null,-1);
+ ProcessBuilder pb=new ProcessBuilder(exe).redirectErrorStream(true);
+ if (zone!=null) pb.environment().put("TZ",zone);
+ Process child=pb.start();
+ try {
+ assertTrue("time program timed out",child.waitFor(10,TimeUnit.SECONDS));
+ String out=new String(child.getInputStream().readAllBytes(),StandardCharsets.UTF_8);
+ assertEquals(out,0,child.exitValue());
+ return out;
+ } finally { if (child.isAlive()) child.destroyForcibly(); }
+ }
+
+ @Test public void localZoneAndDaylightSaving() throws Exception {
+ String out=run("""
+ val show_noInline={u8[~] s ->
+ i64 t=sys.time.parse(s); val d=sys.time.local(t);
+ sys.io.p("%(sys.time.formatLocal(t,sys.time.ISO))|%(d.offsetSeconds)|%b(d.daylight)|%(sys.time.zoneName(t))\\n");
+ };
+ val main={ -> show_noInline("2026-01-01T00:00:00Z"); show_noInline("2026-07-01T00:00:00Z"); return 0; };
+ ""","timeZone","EST5EDT,M3.2.0/2,M11.1.0/2");
+ assertEquals("2025-12-31T19:00:00.000-05:00|-18000|false|EST\n2026-06-30T20:00:00.000-04:00|-14400|true|EDT\n",out);
+ }
+
+ @Test public void liveClocksAndWaits() throws Exception {
+ String out=run("""
+ val main={ ->
+ i64 start=sys.time.monotonic();
+ i64 ns=sys.time.nanoTime();
+ if( start<0 || ns/1000000<start || ns/1000000>sys.time.monotonic() ) return 1;
+ if( sys.time.sleep(10)!=0 || sys.time.monotonic()-start<10 ) return 2;
+ i64 deadline=sys.time.monotonic()+10;
+ if( sys.time.sleepUntil(deadline)!=0 || sys.time.monotonic()<deadline ) return 3;
+ if( sys.time.sleepUntil(0)!=0 || sys.time.sleep(-1)!=-1 || sys.time.sleepUntil(-1)!=-1 ) return 4;
+ i64 wall=sys.time.now();
+ if( wall<1700000000000 || wall>4102444800000 ) return 5;
+ return 0;
+ };
+ ""","timeClocks",null);
+ assertEquals("",out);
+ }
+
+ @Test public void pureCalendarInEvaluator() {
+ CodeGen code=new CodeGen("src/main/smp",null,null,"TimeEval","""
+ val s="%t(-1)|%tM(1234)";
+ val expected="1969-12-31T23:59:59.999Z|00:01.234";
+ if( s#!=expected# ) return 1;
+ for( int i=0; i<s#; i++ ) if( s[i]!=expected[i] ) return 2;
+ return 0;
+ """,123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(code,0,100000));
+ }
+
+ @Test public void wideLibrarySerialization() throws Exception {
+ // A growing stdlib crossed the historical byte-sized alias/count limit.
+ // Exercise a separately compiled library whose live memory spans it.
+ StringBuilder source=new StringBuilder("struct Wide { ");
+ for (int i=0;i<300;++i) source.append("int !f").append(i).append("=0;");
+ source.append("}; val make_noInline={int x -> val v=new Wide();");
+ for (int i : new int[]{0,255,299}) source.append("v.f").append(i).append("=x+").append(i).append(';');
+ source.append("return v; };");
+ Path library=Files.createTempDirectory(Path.of("build/objs"),"wide-memory-");
+ new CodeGen(null,library.toString(),null,"WideMemory",source.toString(),123,true,TypeInteger.BOT)
+ .driver(CPU_PORT,CALL_CONVENTION,false,false);
+ run("val main={ -> val v=WideMemory.make_noInline(7); return v.f0!=7 || v.f255!=262 || v.f299!=306; };",
+ "timeWideMemory",new Ary<>(new String[]{library.toString(),RELEASE_SYS_DIR}),
+ CALL_CONVENTION,null,null,"",-1);
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/TypeTest.java b/src/test/java/com/seaofnodes/simple/TypeTest.java
index 16b75fdc..52e857e9 100644
--- a/src/test/java/com/seaofnodes/simple/TypeTest.java
+++ b/src/test/java/com/seaofnodes/simple/TypeTest.java
@@ -7,6 +7,26 @@ import static org.junit.Assert.assertSame;
public class TypeTest {
+ @Test public void testConstantArrayElements() {
+ TypeConAry range=TypeConAry.make(TypeInteger.U16,new long[]{5,6,7});
+ Assert.assertEquals("[5-7]",range.toString());
+ Assert.assertEquals("[5-7]",TypeConAry.make(TypeInteger.U8,new long[]{5,6,7}).toString());
+ Assert.assertEquals("[\"abc\"]",TypeConAry.make("abc").toString());
+ for( TypeInteger storage : new TypeInteger[]{TypeInteger.BOOL,TypeInteger.I8,TypeInteger.U8,
+ TypeInteger.I16,TypeInteger.U16,TypeInteger.I32,TypeInteger.U32,TypeInteger.BOT} ) {
+ TypeConAry empty=TypeConAry.make(storage,new long[0]);
+ assertSame(storage.dual(),empty.elem());
+ assertSame(storage,((TypeConAry)empty.dual()).elem());
+ for( long[] values : new long[][]{{0},{1},{0,1},{storage._min,storage._max}} ) {
+ TypeConAry array=TypeConAry.make(storage,values);
+ Assert.assertTrue(empty.isa(array));
+ assertSame(array,empty.meet(array));
+ assertSame(empty,array.join(empty));
+ check_symmetric(empty,array);
+ }
+ }
+ }
+
@Test public void testOpaqueExternalEscapes() {
TypeMem defaults = TypeMem.START.makeFrom(XInt.make(2),XInt.make(3));
TypeMem prior = TypeMem.START.makeFrom(XInt.make(5),XInt.make(7));
diff --git a/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java b/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java
index b255bcf7..190cf17c 100644
--- a/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java
+++ b/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java
@@ -3,6 +3,30 @@ import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.TypeInteger;
import static org.junit.Assert.*;
public class PrintRegTestSupport {
+ public static void checkDataAliases(java.nio.file.Path file, int size, String... names) {
+ ElfReader elf=ElfReader.load(file.toFile(),null);
+ assertNotNull(elf);
+ ElfReader.Section symbols=java.util.Arrays.stream(elf._sections)
+ .filter(s -> ".symtab".equals(s._name)).findFirst().orElseThrow();
+ ElfReader.Section strings=elf._sections[symbols._link];
+ var bytes=java.nio.ByteBuffer.wrap(elf._buf).order(java.nio.ByteOrder.LITTLE_ENDIAN);
+ var wanted=new java.util.HashSet<>(java.util.Arrays.asList(names));
+ Long address=null;
+ for( int i=0; i<symbols._size/symbols._esize; i++ ) {
+ int off=(int)symbols._offset+i*symbols._esize;
+ String name=strings.str(bytes.getInt(off));
+ if( !wanted.remove(name) ) continue;
+ assertTrue("Debug aliases precede global symbols",i<symbols._info);
+ assertEquals("Local object symbol",1,bytes.get(off+4)&255);
+ assertEquals(".rodata",elf._sections[bytes.getShort(off+6)&65535]._name);
+ assertEquals(size,bytes.getLong(off+16));
+ long value=bytes.getLong(off+8);
+ if( address!=null ) assertEquals("Aliases share storage",address.longValue(),value);
+ address=value;
+ }
+ assertTrue("Missing debug symbols: "+wanted,wanted.isEmpty());
+ }
+
public static void check() throws Exception {
var code = new CodeGen("return 0;").parse();
var alloc = new RegAlloc(code);
@@ -61,7 +85,7 @@ public class PrintRegTestSupport {
visit.put(marker,t);
var method = com.seaofnodes.simple.print.ASMPrinter.class.getDeclaredMethod(
"printConstantPool",int.class,com.seaofnodes.simple.util.SB.class,
- com.seaofnodes.simple.util.BAOS.class,java.util.HashMap.class,boolean.class,String.class);
+ com.seaofnodes.simple.util.BAOS.class,java.util.Map.class,boolean.class,String.class);
method.setAccessible(true);
try {
var sb = new com.seaofnodes.simple.util.SB();