From 236a9b50ffe5c13f33027dbf5fef38f9fc58a8f9 Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Mon, 10 Aug 2026 11:11:18 -0700 Subject: Chapter 31: Software Drawing, Time, and Graphical Games Co-authored-by: Hsiang-Ying Fu <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: fuhsnn <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: Symious <14933944+symious@users.noreply.github.com> Co-authored-by: Jun Kawasaki Co-authored-by: Claude Opus 5 --- src/main/c/simple_audio.c | 40 ++++ src/main/c/simple_audio.h | 16 ++ src/main/c/simple_graphics.c | 102 ++++++++++ src/main/c/simple_graphics.h | 20 ++ src/main/c/simple_miniaudio.c | 123 ++++++++++++ src/main/c/simple_minifb.c | 117 +++++++++++ src/main/c/simple_time.c | 61 ++++++ src/main/java/com/seaofnodes/simple/README.md | 2 +- .../com/seaofnodes/simple/codegen/Encoding.java | 10 +- .../com/seaofnodes/simple/codegen/Serialize.java | 12 +- .../com/seaofnodes/simple/codegen/TypeCheck.java | 6 +- .../com/seaofnodes/simple/node/ArrayInitNode.java | 44 +++++ .../java/com/seaofnodes/simple/node/LoadNode.java | 16 +- .../com/seaofnodes/simple/node/MemMergeNode.java | 16 +- .../simple/node/cpus/x86_64_v2/JmpX86.java | 27 ++- .../simple/node/cpus/x86_64_v2/MemOpX86.java | 6 +- .../simple/node/cpus/x86_64_v2/NegX86.java | 2 +- .../simple/node/cpus/x86_64_v2/NotX86.java | 2 +- .../simple/node/cpus/x86_64_v2/SetX86.java | 18 +- .../simple/node/cpus/x86_64_v2/StoreX86.java | 3 + .../simple/node/cpus/x86_64_v2/x86_64_v2.java | 17 +- .../com/seaofnodes/simple/parser/Allocations.java | 15 +- .../simple/parser/ArrayInitializers.java | 185 +++++++++++++++++ .../com/seaofnodes/simple/parser/Expressions.java | 2 +- .../com/seaofnodes/simple/parser/Functions.java | 3 + .../java/com/seaofnodes/simple/parser/Lexer.java | 35 +++- .../java/com/seaofnodes/simple/parser/Parser.java | 6 + .../java/com/seaofnodes/simple/parser/README.md | 2 +- .../com/seaofnodes/simple/parser/References.java | 2 + .../com/seaofnodes/simple/parser/Statements.java | 1 + .../java/com/seaofnodes/simple/parser/Strings.java | 2 +- .../com/seaofnodes/simple/parser/package-info.java | 2 +- .../com/seaofnodes/simple/print/ASMPrinter.java | 4 +- .../java/com/seaofnodes/simple/type/TypeMem.java | 9 +- src/main/smp/sys.smp | 5 + src/main/smp/sys/ary.smp | 4 +- src/main/smp/sys/aryu8.smp | 11 +- src/main/smp/sys/audio.smp | 29 +++ src/main/smp/sys/draw.smp | 18 ++ src/main/smp/sys/draw/Button.smp | 41 ++++ src/main/smp/sys/draw/Canvas.smp | 149 ++++++++++++++ src/main/smp/sys/gfx.smp | 44 +++++ src/main/smp/sys/integer.smp | 6 + src/main/smp/sys/io.smp | 2 +- src/main/smp/sys/rt/simple_args.smp | 6 +- src/main/smp/sys/text.smp | 10 +- src/main/smp/sys/time.smp | 219 +++++++++++++++++++++ src/main/smp/sys/time/DateTime.smp | 13 ++ src/main/smp/sys/time/Timers.smp | 80 ++++++++ 49 files changed, 1494 insertions(+), 71 deletions(-) create mode 100644 src/main/c/simple_audio.c create mode 100644 src/main/c/simple_audio.h create mode 100644 src/main/c/simple_graphics.c create mode 100644 src/main/c/simple_graphics.h create mode 100644 src/main/c/simple_miniaudio.c create mode 100644 src/main/c/simple_minifb.c create mode 100644 src/main/c/simple_time.c create mode 100644 src/main/java/com/seaofnodes/simple/node/ArrayInitNode.java create mode 100644 src/main/java/com/seaofnodes/simple/parser/ArrayInitializers.java create mode 100644 src/main/smp/sys/audio.smp create mode 100644 src/main/smp/sys/draw.smp create mode 100644 src/main/smp/sys/draw/Button.smp create mode 100644 src/main/smp/sys/draw/Canvas.smp create mode 100644 src/main/smp/sys/gfx.smp create mode 100644 src/main/smp/sys/integer.smp create mode 100644 src/main/smp/sys/time.smp create mode 100644 src/main/smp/sys/time/DateTime.smp create mode 100644 src/main/smp/sys/time/Timers.smp (limited to 'src/main') 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 +#include +#include +#include + +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 + +// 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_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 +#include +#include +#include +#include + +#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;iversion != 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 + +// 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 +#include +#include +#include +#include + +#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[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 +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#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 +#include +#include + +// 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_secINT64_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_year0; + 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/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 _bigCons = new HashMap<>(); + // Relocations belong to instruction occurrences, including equal constant + // loads cloned by scheduling/register allocation. Structural keys lose sites. + public final IdentityHashMap _bigCons = new IdentityHashMap<>(); public final StaticData _data; public final HashMap _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 _internals = new HashMap<>(); + private final IdentityHashMap _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 _externals = new HashMap<>(); + public final IdentityHashMap _externals = new IdentityHashMap<>(); public Encoding external( Node n, String extern ) { _externals.put(n,extern); return this; } - public final HashMap _externData = new HashMap<>(); + public final IdentityHashMap _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/Serialize.java b/src/main/java/com/seaofnodes/simple/codegen/Serialize.java index ae4ecb74..79bebe1f 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 errs = new Ary<>(Node.class); Ary constructs = new Ary<>(ConstructNode.class); Ary views = new Ary<>(InitViewNode.class); + Ary 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..30f86ac6 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/ArrayInitNode.java @@ -0,0 +1,44 @@ +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 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 _print1(ExprPrinter p) { return p.p("arrayInit(").n(in(2)).p(")"); } + @Override public Type compute() { return in(0)._type==Type.CONTROL ? in(2)._type : Type.TOP; } + @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; ir.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 strs, HashMap types, IdentityHashMap 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 strs, HashMap types, IdentityHashMap 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/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/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..aed8ee4c 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[]{ @@ -370,8 +382,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..5c6b5765 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)); 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..70eeae1f --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/parser/ArrayInitializers.java @@ -0,0 +1,185 @@ +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 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_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/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 * *
-     *     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]?
      * 
*/ diff --git a/src/main/java/com/seaofnodes/simple/parser/Functions.java b/src/main/java/com/seaofnodes/simple/parser/Functions.java index 5175acc6..8047fabc 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Functions.java +++ b/src/main/java/com/seaofnodes/simple/parser/Functions.java @@ -198,6 +198,8 @@ 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); @@ -275,6 +277,7 @@ final class Functions { P.statements._breakScope = breakScope; P._scope.pop(); P.construction._frame = outerConstruction; + P.arrays._frame = outerArray; return ret; } 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=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/Parser.java b/src/main/java/com/seaofnodes/simple/parser/Parser.java index 0cf3c0e7..3e7bb6b8 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Parser.java +++ b/src/main/java/com/seaofnodes/simple/parser/Parser.java @@ -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); @@ -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 ) { diff --git a/src/main/java/com/seaofnodes/simple/parser/README.md b/src/main/java/com/seaofnodes/simple/parser/README.md index cee1d64a..d0220761 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 | diff --git a/src/main/java/com/seaofnodes/simple/parser/References.java b/src/main/java/com/seaofnodes/simple/parser/References.java index 6335bad2..f170cae2 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 && 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/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. * - *

See {@code chapter30/docs/30-grammar.md} for the production-to-method map. + *

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..bdd887e6 100644 --- a/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java +++ b/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java @@ -10,7 +10,7 @@ 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 { @@ -93,7 +93,7 @@ public final class ASMPrinter extends AssemblyAdapter { 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 relos, + static int printConstantPool(int address, SB sb, BAOS bits, Map 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/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/smp/sys.smp b/src/main/smp/sys.smp index 1201315b..f7889c05 100644 --- a/src/main/smp/sys.smp +++ b/src/main/smp/sys.smp @@ -2,12 +2,17 @@ // 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 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/ary.smp b/src/main/smp/sys/ary.smp index c139cd33..11ddb7a0 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; diff --git a/src/main/smp/sys/aryu8.smp b/src/main/smp/sys/aryu8.smp index 18543f13..76d47ec5 100644 --- a/src/main/smp/sys/aryu8.smp +++ b/src/main/smp/sys/aryu8.smp @@ -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 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..61b250d3 --- /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..edf01119 --- /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=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..5bd7d68e --- /dev/null +++ b/src/main/smp/sys/draw/Canvas.smp @@ -0,0 +1,149 @@ +struct Canvas { + u32 width; + u32 height; + u32[] pixels; + + new Canvas = { int w, int 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 + if( 0<=x + 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 + 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 = x0x1 ? 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> (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/rt/simple_args.smp b/src/main/smp/sys/rt/simple_args.smp index 3f630e54..f2890c1c 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 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..2c4b44ff 100644 --- a/src/main/smp/sys/text.smp +++ b/src/main/smp/sys/text.smp @@ -5,10 +5,7 @@ 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 -> @@ -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..1706990e --- /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 _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( instantMAX ) 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 + 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 ""; + 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'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 instantMAX ? -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..eca32d15 --- /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; + }; +}; -- cgit v1.2.3