summaryrefslogtreecommitdiff
path: root/src/main/smp
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-08-10 11:11:18 -0700
committerCliff Click <cliffc@acm.org>2026-11-13 13:35:58 -0800
commit870c822ce26bd866761f6abd71fd9e109b9d9d98 (patch)
tree65b1d43dcc5e1085e15d301c5ff628d9dcfd483e /src/main/smp
parent2cb35c65bde63f90d7afc3d7a8a985b0c6ea15d7 (diff)
downloadSimple-870c822ce26bd866761f6abd71fd9e109b9d9d98.tar.gz
Simple-870c822ce26bd866761f6abd71fd9e109b9d9d98.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/main/smp')
-rw-r--r--src/main/smp/sys.smp5
-rw-r--r--src/main/smp/sys/ary.smp4
-rw-r--r--src/main/smp/sys/aryu8.smp2
-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.smp203
-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/rt/simple_args.smp6
-rw-r--r--src/main/smp/sys/text.smp5
-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
15 files changed, 671 insertions, 6 deletions
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..88574522 100644
--- a/src/main/smp/sys/aryu8.smp
+++ b/src/main/smp/sys/aryu8.smp
@@ -69,7 +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);
+ if( str# == sys.integer.MAX_U32 ) libc._exit(1);
u8[] !strz = new u8[str#+1];
u32 len=str#;
ary.copy(strz,0,str,0,len);
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..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<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..c736e4e2
--- /dev/null
+++ b/src/main/smp/sys/draw/Canvas.smp
@@ -0,0 +1,203 @@
+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<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
+val _glyph = { u8 c ->
+ // Only space for upper-case letters in a 3x5 font
+ if( 'a' <= c <= 'z' )
+ c -= 'a'-'A';
+ // Five octal digits: one 3-bit row per digit, top to bottom.
+ if( c==' ' ) return 000000;
+ if( c=='0' ) return 075557;
+ if( c=='1' ) return 026227;
+ if( c=='2' ) return 061247;
+ if( c=='3' ) return 061216;
+ if( c=='4' ) return 055711;
+ if( c=='5' ) return 074616;
+ if( c=='6' ) return 034757;
+ if( c=='7' ) return 071222;
+ if( c=='8' ) return 075757;
+ if( c=='9' ) return 075716;
+ if( c=='A' ) return 025755;
+ if( c=='B' ) return 065656;
+ if( c=='C' ) return 034443;
+ if( c=='D' ) return 065556;
+ if( c=='E' ) return 074647;
+ if( c=='F' ) return 074644;
+ if( c=='G' ) return 034553;
+ if( c=='H' ) return 055755;
+ if( c=='I' ) return 072227;
+ if( c=='J' ) return 011152;
+ if( c=='K' ) return 055655;
+ if( c=='L' ) return 044447;
+ if( c=='M' ) return 057755;
+ if( c=='N' ) return 057775;
+ if( c=='O' ) return 025552;
+ if( c=='P' ) return 065644;
+ if( c=='Q' ) return 025573;
+ if( c=='R' ) return 065655;
+ if( c=='S' ) return 034216;
+ if( c=='T' ) return 072222;
+ if( c=='U' ) return 055557;
+ if( c=='V' ) return 055552;
+ if( c=='W' ) return 055775;
+ if( c=='X' ) return 055255;
+ if( c=='Y' ) return 055222;
+ if( c=='Z' ) return 071247;
+ if( c=='-' ) return 000700;
+ if( c=='.' ) return 000002;
+ if( c==':' ) return 002020;
+ if( c==',' ) return 000024;
+ if( c==';' ) return 002024;
+ if( c=='\'' ) return 022000;
+ if( c=='"' ) return 055000;
+ if( c=='`' ) return 042000;
+ if( c=='/' ) return 011244;
+ if( c=='\\' ) return 044211;
+ if( c=='(' ) return 012221;
+ if( c==')' ) return 042224;
+ if( c=='[' ) return 032223;
+ if( c==']' ) return 062226;
+ if( c=='{' ) return 032423;
+ if( c=='}' ) return 062126;
+ if( c=='+' ) return 002720;
+ if( c=='=' ) return 007070;
+ if( c=='_' ) return 000007;
+ if( c=='<' ) return 012421;
+ if( c=='>' ) return 042124;
+ if( c=='|' ) return 022222;
+ if( c=='^' ) return 025000;
+ if( c=='~' ) return 003600;
+ if( c=='*' ) return 005250;
+ if( c=='#' ) return 057575;
+ if( c=='%' ) return 051245;
+ if( c=='!' ) return 022202;
+ if( c=='?' ) return 061202;
+ return 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<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..a28edf6d 100644
--- a/src/main/smp/sys/text.smp
+++ b/src/main/smp/sys/text.smp
@@ -96,3 +96,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<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..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;
+ };
+};