summaryrefslogtreecommitdiff
path: root/src/main/smp/sys
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/smp/sys')
-rw-r--r--src/main/smp/sys/adt/bitset.smp4
-rw-r--r--src/main/smp/sys/ary.smp8
-rw-r--r--src/main/smp/sys/aryi64.smp6
-rw-r--r--src/main/smp/sys/aryu8.smp13
-rw-r--r--src/main/smp/sys/audio.smp29
-rw-r--r--src/main/smp/sys/draw.smp18
-rw-r--r--src/main/smp/sys/draw/Button.smp41
-rw-r--r--src/main/smp/sys/draw/Canvas.smp149
-rw-r--r--src/main/smp/sys/gfx.smp44
-rw-r--r--src/main/smp/sys/integer.smp6
-rw-r--r--src/main/smp/sys/io.smp2
-rw-r--r--src/main/smp/sys/libc.smp8
-rw-r--r--src/main/smp/sys/math.smp15
-rw-r--r--src/main/smp/sys/rt/simple_args.smp8
-rw-r--r--src/main/smp/sys/text.smp84
-rw-r--r--src/main/smp/sys/time.smp219
-rw-r--r--src/main/smp/sys/time/DateTime.smp13
-rw-r--r--src/main/smp/sys/time/Timers.smp80
18 files changed, 681 insertions, 66 deletions
diff --git a/src/main/smp/sys/adt/bitset.smp b/src/main/smp/sys/adt/bitset.smp
index d02cd2fe..a51a63e1 100644
--- a/src/main/smp/sys/adt/bitset.smp
+++ b/src/main/smp/sys/adt/bitset.smp
@@ -20,7 +20,7 @@ struct bitset {
if( x < len ) return self;
while( x >= len ) len *= 2;
var bits = new i64[len];
- for( int i=0; i<_bits#; i++ )
+ for( int i = 0; i < _bits#; i++ )
bits[i] = _bits[i];
_bits = bits;
return self;
@@ -51,7 +51,7 @@ struct bitset {
};
val clear = { ->
- for( int i=0; i<_bits#; i++ ) _bits[i] = 0;
+ for( int i = 0; i < _bits#; i++ ) _bits[i] = 0;
return self;
};
};
diff --git a/src/main/smp/sys/ary.smp b/src/main/smp/sys/ary.smp
index c139cd33..b94ffa8f 100644
--- a/src/main/smp/sys/ary.smp
+++ b/src/main/smp/sys/ary.smp
@@ -3,12 +3,12 @@
// Geometric growth without wrapping a u32 length. Zero means too large.
val capacity = { u32 current, i64 needed ->
- if( needed < 0 || needed > 4294967295 ) return 0;
+ if( needed < 0 || needed > sys.integer.MAX_U32 ) return 0;
if( needed <= current ) return current;
i64 size = current ? current : 1;
while( size < needed ) {
size *= 2;
- if( size > 4294967295 ) size = 4294967295;
+ if( size > sys.integer.MAX_U32 ) size = sys.integer.MAX_U32;
}
u32 result = size;
return result;
@@ -19,7 +19,7 @@ val copy = { u8[] dst, u32 doff, u8[~] src, u32 soff, u32 len ->
if( doff > dst# || soff > src# ) return dst;
if( len > dst# - doff ) len = dst# - doff;
if( len > src# - soff ) len = src# - soff;
- for( u32 i=0; i<len; i++ )
+ for( u32 i = 0; i < len; i++ )
dst[i+doff] = src[i+soff];
return dst;
};
@@ -33,7 +33,7 @@ val cmp = { u8[~] src1, u32 off1, u8[~] src2, u32 off2, u32 len ->
u32 len1 = len > left1 ? left1 : len;
u32 len2 = len > left2 ? left2 : len;
u32 max = len1 > len2 ? len2 : len1;
- for( u32 i=0; i<max; i++ )
+ for( u32 i = 0; i < max; i++ )
if( src1[off1+i] != src2[off2+i] )
return src1[off1+i] - src2[off2+i]; // Bytes determine compare
return len1 - len2; // Length determines compare
diff --git a/src/main/smp/sys/aryi64.smp b/src/main/smp/sys/aryi64.smp
index b9f1fbed..aba1b459 100644
--- a/src/main/smp/sys/aryi64.smp
+++ b/src/main/smp/sys/aryi64.smp
@@ -14,7 +14,7 @@ struct aryi64 {
u32 newlen = ary.capacity(buf#,len+len2);
if( !newlen ) libc._exit(1); // Length exceeds the array representation.
i64[] !buf2 = new i64[newlen];
- for( u32 i=0; i<len; i++ )
+ for( u32 i = 0; i < len; i++ )
buf2[i] = buf[i];
buf = buf2;
self;
@@ -32,7 +32,7 @@ struct aryi64 {
val str = { ->
// TODO: clunky default constructor syntax
var str = new aryu8().add('[');
- for( u32 i=0; i<len; i++ )
+ for( u32 i = 0; i < len; i++ )
str.num(buf[i]).add(',').add(' ');
if( len ) str.undo(2);
return str.add(']');
@@ -79,7 +79,7 @@ val copy = { i64[] dst, u32 doff, i64[~] src, u32 soff, u32 len ->
if( doff > dst# || soff > src# ) return dst;
if( len > dst# - doff ) len = dst# - doff;
if( len > src# - soff ) len = src# - soff;
- for( u32 i=0; i<len; i++ )
+ for( u32 i = 0; i < len; i++ )
dst[i+doff] = src[i+soff];
return dst;
};
diff --git a/src/main/smp/sys/aryu8.smp b/src/main/smp/sys/aryu8.smp
index 18543f13..3f462898 100644
--- a/src/main/smp/sys/aryu8.smp
+++ b/src/main/smp/sys/aryu8.smp
@@ -27,7 +27,7 @@ struct aryu8 {
// Add all of a plain array to the aryu8
val addAry = { u8[~] xs ->
- u32 n=xs#;
+ u32 n = xs#;
_grow(n);
ary.copy(buf,len,xs,0,n);
len += n;
@@ -60,7 +60,7 @@ val atoi = { u8[~] str ->
while( x < str# && str[x] <= ' ' ) x++;
if( x == str# ) return 0;
bool neg = false;
- if( str[x]=='-' ) { neg=true; x++; }
+ if( str[x] == '-' ) { neg = true; x++; }
i64 n = 0;
while( x < str# && '0' <= str[x] <= '9' )
n = n*10 + str[x++]-'0';
@@ -69,10 +69,7 @@ val atoi = { u8[~] str ->
// Zero-terminate a string for libc, reserving one array element for the zero.
{ u8[~] -> u8[] } str0 = { u8[~] str ->
- if( str# == 4294967295 ) libc._exit(1);
- u8[] !strz = new u8[str#+1];
- u32 len=str#;
- ary.copy(strz,0,str,0,len);
- strz[str#] = '\0';
- strz;
+ if( str# == sys.integer.MAX_U32 ) libc._exit(1);
+ // Copy, terminate and return
+ return new u8[str#+1]{i64 i -> i < str# ? str[i] : '\0'; };
};
diff --git a/src/main/smp/sys/audio.smp b/src/main/smp/sys/audio.smp
new file mode 100644
index 00000000..89e6dfa9
--- /dev/null
+++ b/src/main/smp/sys/audio.smp
@@ -0,0 +1,29 @@
+// Optional background audio. All API calls belong on the game/main thread.
+int OK = 0;
+int ERROR = -1;
+int BOOP = 1;
+int WIN = 2;
+int LOSE = 3;
+int RATE = 48000;
+{ i32 } simple_audio_open = "C";
+{ i64 u32 -> i32 } simple_audio_load = "C";
+{ i32 -> i32 } simple_audio_play = "C";
+{ i32 } simple_audio_stop = "C";
+{ i32 } simple_audio_playing = "C";
+{ i32 } simple_audio_close = "C";
+// Missing/disabled backend returns ERROR quietly; the game may remain silent.
+val open = { -> simple_audio_open(); };
+// Mono signed 16-bit PCM at RATE, up to ten seconds. Copies into native memory.
+// Load at startup: up to 28 custom handles, valid until close, then reload.
+val load = { i16[~] samples ->
+ i64 ptr = samples;
+ simple_audio_load(ptr,samples#);
+};
+// Starts/restarts immediately, replacing the previous sound; never waits for it.
+val play = { int sound -> simple_audio_play(sound); };
+val boop = { -> simple_audio_play(BOOP); };
+val win = { -> simple_audio_play(WIN); };
+val lose = { -> simple_audio_play(LOSE); };
+val stop = { -> simple_audio_stop(); };
+val playing = { -> simple_audio_playing(); };
+val close = { -> simple_audio_close(); };
diff --git a/src/main/smp/sys/draw.smp b/src/main/smp/sys/draw.smp
new file mode 100644
index 00000000..2a8b6117
--- /dev/null
+++ b/src/main/smp/sys/draw.smp
@@ -0,0 +1,18 @@
+// Software raster drawing. No window or native calls; pixels are packed RGB (0x00RRGGBB).
+u32 BLACK = 0x000000;
+u32 WHITE = 0xffffff;
+u32 RED = 0xff0000;
+u32 GREEN = 0x00ff00;
+u32 BLUE = 0x0000ff;
+
+// Components are clamped to bytes rather than wrapping.
+val rgb = { int r, int g, int b ->
+ r = r < 0 ? 0 : r > 255 ? 255 : r;
+ g = g < 0 ? 0 : g > 255 ? 255 : g;
+ b = b < 0 ? 0 : b > 255 ? 255 : b;
+ u32 color = (r<<16) | (g<<8) | b;
+ return color;
+};
+
+val Canvas = Canvas; // Pixel canvas and drawing methods
+val Button = Button; // Pointer state and rendering for a rectangular button
diff --git a/src/main/smp/sys/draw/Button.smp b/src/main/smp/sys/draw/Button.smp
new file mode 100644
index 00000000..c90ed2d3
--- /dev/null
+++ b/src/main/smp/sys/draw/Button.smp
@@ -0,0 +1,41 @@
+// Feed pointer state once per event-loop iteration. Activate on release inside,
+// only after a press that began inside. No native/window dependency.
+struct Button {
+ int x, y, width, height;
+ u8[~] !label;
+ bool !enabled = true, !hovered = false, !pressed = false;
+ bool !_held = false, !_armed = false;
+ new Button = { int px, int py, int w, int h, u8[~] text ->
+ x = px; y = py; width = w; height = h; label = text;
+ };
+ val contains = { i64 px, i64 py ->
+ if( width <= 0 || height <= 0 ) return false;
+ i64 right = x, bottom = y; right += width; bottom += height;
+ if( px < x || py < y || px >= right || py >= bottom ) return false;
+ return true;
+ };
+ val update = { i64 px, i64 py, bool down ->
+ hovered = false;
+ if( enabled ) hovered = contains(px,py);
+ if( !enabled ) _armed = false;
+ if( down && !_held ) _armed = hovered;
+ var clicked = false;
+ if( !down ) if( _held ) if( _armed ) if( hovered ) clicked = true;
+ if( !down ) _armed = false;
+ _held = down;
+ pressed = false;
+ if( down ) if( _armed ) if( hovered ) pressed = true;
+ return clicked;
+ };
+ val draw = { !sys.draw.Canvas screen, int scale, u32 foreground, u32 background, u32 active ->
+ screen.fillRect(x,y,width,height,enabled && (hovered || pressed) ? active : background);
+ screen.rect(x,y,width,height,2,enabled ? foreground : active);
+ if( scale > 0 ) {
+ i64 textWidth = label#; textWidth = (textWidth*4-1)*scale;
+ if( textWidth <= width-8 && 5*scale <= height-8 )
+ screen.text(x+(width-textWidth)/2,y+(height-5*scale)/2+(pressed ? 1 : 0),
+ label,scale,enabled ? foreground : active);
+ }
+ return self;
+ };
+};
diff --git a/src/main/smp/sys/draw/Canvas.smp b/src/main/smp/sys/draw/Canvas.smp
new file mode 100644
index 00000000..9ba3514a
--- /dev/null
+++ b/src/main/smp/sys/draw/Canvas.smp
@@ -0,0 +1,149 @@
+struct Canvas {
+ u32 width;
+ u32 height;
+ u32[] pixels;
+
+ new Canvas = { u32 w, u32 h ->
+ i64 count = w;
+ count *= h;
+ if( w <= 0 || h <= 0 || count > sys.integer.MAX_I32 ) libc._exit(1);
+ width = w;
+ height = h;
+ pixels = new u32[count];
+ };
+
+ val clear = { u32 color ->
+ for( int i = 0; i < pixels#; i++ ) pixels[i] = color;
+ return self;
+ };
+
+ val pixel = { i64 x, i64 y, u32 color ->
+ if( 0 <= x < width && 0 <= y < height ) pixels[y*width+x] = color;
+ return self;
+ };
+
+ // Integer coordinates, top-left origin. Rectangle extents are half-open.
+ val fillRect = { i64 x, i64 y, i64 w, i64 h, u32 color ->
+ if( w <= 0 || h <= 0 ) return self;
+ i64 right = x; right += w;
+ i64 bottom = y; bottom += h;
+ if( right <= 0 || bottom <= 0 || x >= width || y >= height ) return self;
+ int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y;
+ int x1 = right > width ? width : right, y1 = bottom > height ? height : bottom;
+ for( int py = y0; py < y1; py++ )
+ for( int px = x0; px < x1; px++ ) pixels[py*width+px] = color;
+ return self;
+ };
+
+ // Outline stays inside the rectangle. Wide borders become a filled rect.
+ val rect = { i64 x, i64 y, i64 w, i64 h, i64 thickness, u32 color ->
+ if( w <= 0 || h <= 0 || thickness <= 0 ) return self;
+ if( thickness >= w || thickness >= h || thickness >= w-thickness || thickness >= h-thickness )
+ return fillRect(x,y,w,h,color);
+ fillRect(x,y,w,thickness,color);
+ fillRect(x,y+h-thickness,w,thickness,color);
+ fillRect(x,y+thickness,thickness,h-2*thickness,color);
+ return fillRect(x+w-thickness,y+thickness,thickness,h-2*thickness,color);
+ };
+
+ // Integer centers are pixel centers. Circle outlines extend inward.
+ val fillCircle = { i64 cx, i64 cy, int radius, u32 color ->
+ return _circle(cx,cy,radius,0,true,color);
+ };
+ val circle = { i64 cx, i64 cy, int radius, int thickness, u32 color ->
+ return _circle(cx,cy,radius,thickness,false,color);
+ };
+ val _circle = { i64 cx, i64 cy, int radius, int thickness, bool filled, u32 color ->
+ if( radius < 0 || (!filled && thickness <= 0) ) return self;
+ i64 left = cx; left -= radius;
+ i64 top = cy; top -= radius;
+ i64 right = cx; right += radius;
+ i64 bottom = cy; bottom += radius;
+ if( right < 0 || bottom < 0 || left >= width || top >= height ) return self;
+ int x0 = left < 0 ? 0 : left, y0 = top < 0 ? 0 : top;
+ int x1 = right >= width ? width-1 : right, y1 = bottom >= height ? height-1 : bottom;
+ i64 outer2 = radius;
+ outer2 *= radius;
+ i64 inner = filled ? -1 : radius-thickness;
+ for( int py = y0; py <= y1; py++ )
+ for( int px = x0; px <= x1; px++ ) {
+ i64 dx = px; dx -= cx;
+ i64 dy = py; dy -= cy;
+ i64 d2 = dx*dx+dy*dy;
+ if( d2 <= outer2 && (inner <= 0 || d2 > inner*inner) ) pixels[py*width+px] = color;
+ }
+ return self;
+ };
+
+ // Round caps; a zero-length segment is a dot. Only the clipped bounding
+ // box is scanned. Floating distances avoid overflowing coordinate squares.
+ val line = { i64 x0, i64 y0, i64 x1, i64 y1, i64 thickness, u32 color ->
+ if( thickness <= 0 ) return self;
+ i64 pad = thickness/2+1;
+ i64 left = x0 < x1 ? x0 : x1; left -= pad;
+ i64 top = y0 < y1 ? y0 : y1; top -= pad;
+ i64 right = x0 > x1 ? x0 : x1; right += pad;
+ i64 bottom = y0 > y1 ? y0 : y1; bottom += pad;
+ if( right < 0 || bottom < 0 || left >= width || top >= height ) return self;
+ int bx0 = left < 0 ? 0 : left, by0 = top < 0 ? 0 : top;
+ int bx1 = right >= width ? width-1 : right, by1 = bottom >= height ? height-1 : bottom;
+ for( int py = by0; py <= by1; py++ )
+ for( int px = bx0; px <= bx1; px++ )
+ if( _onLine(px,py,x0,y0,x1,y1,thickness) ) pixels[py*width+px] = color;
+ return self;
+ };
+
+ // Small 3x5 bitmap font for game labels. Lowercase maps to uppercase.
+ // Newline returns to x; unsupported characters draw a question mark.
+ val text = { i64 x, i64 y, u8[~] str, int scale, u32 color ->
+ if( scale <= 0 ) return self;
+ i64 px = x, py = y;
+ for( int i = 0; i < str#; i++ ) {
+ u8 c = str[i];
+ if( c == '\n' ) { px = x; py += 6*scale; continue; }
+ int bits = _glyph(c);
+ for( int row = 0; row < 5; row++ )
+ for( int col = 0; col < 3; col++ )
+ if( (bits >> (14-row*3-col)) & 1 )
+ fillRect(px+col*scale,py+row*scale,scale,scale,color);
+ px += 4*scale;
+ }
+ return self;
+ };
+};
+
+// An old-school 3x5 font, ASCII 32..127. Five octal row digits need u16.
+// Lowercase uses uppercase shapes; unsupported characters use '?'.
+val _glyphs = new u16[~96]{
+ 000000,022202,055000,057575,061202,051245,061202,022000, // space..'
+ 012221,042224,005250,002720,000024,000700,000002,011244, // (../
+ 075557,026227,061247,061216,055711,074616,034757,071222, // 0..7
+ 075757,075716,002020,002024,012421,007070,042124,061202, // 8..?
+ 061202,025755,065656,034443,065556,074647,074644,034553, // @..G
+ 055755,072227,011152,055655,044447,057755,057775,025552, // H..O
+ 065644,025573,065655,034216,072222,055557,055552,055775, // P..W
+ 055255,055222,071247,032223,044211,062226,025000,000007, // X.._
+ 042000,025755,065656,034443,065556,074647,074644,034553, // `..g
+ 055755,072227,011152,055655,044447,057755,057775,025552, // h..o
+ 065644,025573,065655,034216,072222,055557,055552,055775, // p..w
+ 055255,055222,071247,032423,022222,062126,003600,061202, // x..DEL
+};
+
+val _glyph = { u8 c ->
+ return 32 <= c < 128 ? _glyphs[c-32] : 061202;
+};
+
+// Pixel coverage for a round-ended segment, separate from traversal.
+val _onLine = { i64 px, i64 py, i64 x0, i64 y0, i64 x1, i64 y1, i64 thickness ->
+ f64 dx = x1; dx -= x0;
+ f64 dy = y1; dy -= y0;
+ f64 vx = px; vx -= x0;
+ f64 vy = py; vy -= y0;
+ f64 length2 = dx*dx+dy*dy;
+ bool point = x0 == x1 && y0 == y1;
+ f64 t = point ? 0.0 : (vx*dx+vy*dy)/length2;
+ t = t < 0 ? 0.0 : t > 1 ? 1.0 : t;
+ f64 ex = vx-t*dx, ey = vy-t*dy;
+ f64 half = thickness/2.0;
+ return ex*ex+ey*ey <= half*half;
+};
diff --git a/src/main/smp/sys/gfx.smp b/src/main/smp/sys/gfx.smp
new file mode 100644
index 00000000..ae203bd1
--- /dev/null
+++ b/src/main/smp/sys/gfx.smp
@@ -0,0 +1,44 @@
+// Minimal pixel-window interface; drawing belongs in Simple.
+// One fixed-size window, main thread only. Pixel format is 0x00RRGGBB.
+// open/present/poll return OK, CLOSED (normal user close/Esc), or ERROR.
+int OK = 0;
+int CLOSED = 1;
+int ERROR = -1;
+int LEFT = 1;
+int ESCAPE = 256;
+
+{ i64 u32 u32 -> i32 } simple_gfx_open = "C";
+{ i64 u32 -> i32 } simple_gfx_present = "C";
+{ i32 -> i32 } simple_gfx_poll = "C";
+{ i32 } simple_gfx_mouse_x = "C";
+{ i32 } simple_gfx_mouse_y = "C";
+{ i32 -> i32 } simple_gfx_button = "C";
+{ i32 -> i32 } simple_gfx_key = "C";
+{ i32 } simple_gfx_close = "C";
+{ i64 -> i32 } simple_gfx_wait = "C";
+
+// Failure prints a diagnostic to stderr and returns ERROR; callers decide
+// whether to exit or fall back to text. No native library is loaded until here.
+val open = { u8[~] title, u32 width, u32 height ->
+ i64 ptr = aryu8.str0(title);
+ simple_gfx_open(ptr,width,height);
+};
+
+// Array length must be width*height. The native layer copies for OS repaints;
+// it never keeps a pointer into the Simple heap after this call returns.
+val present = { u32[~] pixels ->
+ i64 ptr = pixels;
+ simple_gfx_present(ptr,pixels#);
+};
+
+// wait=true waits up to 17ms, waking early for input to avoid a busy loop.
+val poll = { bool wait -> simple_gfx_poll(wait); };
+val mouseX = { -> simple_gfx_mouse_x(); };
+val mouseY = { -> simple_gfx_mouse_y(); };
+val button = { int button -> simple_gfx_button(button); };
+// Printable keys use uppercase ASCII; special keys use MiniFB's key codes.
+val key = { int key -> simple_gfx_key(key); };
+val close = { -> simple_gfx_close(); };
+// Wait for input or at most timeout milliseconds; 0 polls, -1 waits indefinitely.
+// This blocks the game thread, dispatches window events, and may wake early.
+val wait = { i64 timeout_ms -> simple_gfx_wait(timeout_ms); };
diff --git a/src/main/smp/sys/integer.smp b/src/main/smp/sys/integer.smp
new file mode 100644
index 00000000..a839e597
--- /dev/null
+++ b/src/main/smp/sys/integer.smp
@@ -0,0 +1,6 @@
+// Integer bounds.
+
+i64 MIN_I64 = 1<<63;
+i64 MAX_I64 = -(MIN_I64+1);
+i32 MAX_I32 = (1<<31)-1;
+u32 MAX_U32 = (1<<32)-1;
diff --git a/src/main/smp/sys/io.smp b/src/main/smp/sys/io.smp
index 14993429..6269cde5 100644
--- a/src/main/smp/sys/io.smp
+++ b/src/main/smp/sys/io.smp
@@ -21,7 +21,7 @@ val read = { i32 fd ->
return null;
}
if( rez == 0 ) break;
- if( buf.len+rez > 4294967295 ) {
+ if( buf.len+rez > sys.integer.MAX_U32 ) {
libc.set_errno(libc.EOVERFLOW);
return null;
}
diff --git a/src/main/smp/sys/libc.smp b/src/main/smp/sys/libc.smp
index 7122bbd8..8e417461 100644
--- a/src/main/smp/sys/libc.smp
+++ b/src/main/smp/sys/libc.smp
@@ -53,9 +53,9 @@ i32 EOVERFLOW = "C";
// addr len prot flags fd off -> void*
{ i64 i64 i32 i32 i32 i32 -> i64 } mmap = "C";
// mmap flags
-i32 PROT_EXEC = 4;
-i32 PROT_READ = 1;
-i32 PROT_WRITE= 2;
-i32 PROT_NONE = 0;
+i32 PROT_EXEC = 4;
+i32 PROT_READ = 1;
+i32 PROT_WRITE = 2;
+i32 PROT_NONE = 0;
i32 MAP_PRIVATE = 2;
i32 MAP_ANON = 32;
diff --git a/src/main/smp/sys/math.smp b/src/main/smp/sys/math.smp
new file mode 100644
index 00000000..a5e1887c
--- /dev/null
+++ b/src/main/smp/sys/math.smp
@@ -0,0 +1,15 @@
+// Mathematical constants, rounded to the nearest f64 value.
+val PI = 3.141592653589793;
+val E = 2.718281828459045;
+
+// Fixed-semantics C bindings recognized by the compiler's intrinsic inliner.
+// These preserve all 64 bits, including signed zero and NaN payloads.
+// They are NOT numeric conversions (ordinary typed assignments still are).
+{ f64 -> i64 } simple_f64_bits = "C";
+{ i64 -> f64 } simple_f64_from_bits = "C";
+val f64Bits = simple_f64_bits;
+val f64FromBits = simple_f64_from_bits;
+
+
+val max = { f64 x, f64 y -> return x > y ? x : y; };
+val min = { f64 x, f64 y -> return x < y ? x : y; };
diff --git a/src/main/smp/sys/rt/simple_args.smp b/src/main/smp/sys/rt/simple_args.smp
index 3f630e54..8bfcc1a7 100644
--- a/src/main/smp/sys/rt/simple_args.smp
+++ b/src/main/smp/sys/rt/simple_args.smp
@@ -1,18 +1,20 @@
// Native startup support, compiled independently and linked with the CRT.
// Raw native addresses stay i64; only the arrays allocated here are Simple objects.
+// This startup object is built without sys imports.
+u32 _MAX_U32 = (1<<32)-1;
{ i64 i64 -> i64 } simple_argv = "C";
{ i64 -> i64 } strlen = "C";
{ i64 i64 i64 -> i64 } memcpy = "C";
val simple_args = { i64 argc, i64 argv ->
- if( argc < 0 || argc > 4294967295 ) return null;
+ if( argc < 0 || argc > _MAX_U32 ) return null;
// Zero-filled reference arrays require nullable elements. Every slot is
// filled before returning; the native entry bridge publishes u8[~][].
val args = new u8[~]?[argc];
- for( i64 i=0; i<argc; i++ ) {
+ for( i64 i = 0; i < argc; i++ ) {
i64 src = simple_argv(argv,i);
i64 len = strlen(src);
- if( len < 0 || len > 4294967295 ) return null;
+ if( len < 0 || len > _MAX_U32 ) return null;
val bytes = new u8[len];
i64 dst = bytes;
memcpy(dst,src,len);
diff --git a/src/main/smp/sys/text.smp b/src/main/smp/sys/text.smp
index a414ff45..43698f22 100644
--- a/src/main/smp/sys/text.smp
+++ b/src/main/smp/sys/text.smp
@@ -5,78 +5,75 @@
val begin = { -> new aryu8(); };
val end = { aryu8 b ->
- val bytes = new u8[b.len];
- ary.copy(bytes,0,b.buf,0,b.len);
- u8[~] result = bytes;
- result;
+ new u8[~b.len]{ i64 i -> b.buf[i]; };
};
val _pad = { !aryu8 b, i64 n, u8 c ->
- for( i64 i=0; i<n; i++ ) b.add(c);
+ for( i64 i = 0; i < n; i++ ) b.add(c);
b;
};
val string = { !aryu8 b, u8[~] s, i64 spec, i64 width, i64 precision ->
- i64 len=s#;
- if( precision>=0 && precision<len ) len=precision;
- i64 pad=width-len;
+ i64 len = s#;
+ if( precision >= 0 && precision < len ) len = precision;
+ i64 pad = width-len;
if( !(spec&256) ) _pad(b,pad,' ');
- for( i64 i=0; i<len; i++ ) b.add(s[i]);
+ for( i64 i = 0; i < len; i++ ) b.add(s[i]);
if( spec&256 ) _pad(b,pad,' ');
b;
};
val integer = { !aryu8 b, i64 value, i64 spec, i64 width, i64 precision ->
- i64 kind=spec&255;
- if( kind=='b' ) return string(b,value ? "true" : "false",spec,width,-1);
- if( kind=='c' ) {
+ i64 kind = spec&255;
+ if( kind == 'b' ) return string(b,value ? "true" : "false",spec,width,-1);
+ if( kind == 'c' ) {
if( !(spec&256) ) _pad(b,width-1,' ');
- u8 c=value;
+ u8 c = value;
b.add(c);
if( spec&256 ) _pad(b,width-1,' ');
return b;
}
- bool signed=kind==0 || kind=='d' || kind=='i';
- i64 base=kind=='o' ? 8 : (kind=='x' || kind=='X' ? 16 : 10);
- u8[~] alphabet=kind=='X' ? "0123456789ABCDEF" : "0123456789abcdef";
- val digits=new u8[22]; // Maximum: 22 octal digits for a 64-bit integer.
- i64 count=0, n=value;
+ bool signed = kind == 0 || kind == 'd' || kind == 'i';
+ i64 base = kind == 'o' ? 8 : (kind == 'x' || kind == 'X' ? 16 : 10);
+ u8[~] alphabet = kind == 'X' ? "0123456789ABCDEF" : "0123456789abcdef";
+ val digits = new u8[22]; // Maximum: 22 octal digits for a 64-bit integer.
+ i64 count = 0, n = value;
// Signed decimal stays nonpositive, including the minimum i64.
- if( signed && n>0 ) n=-n;
- if( n==0 && precision!=0 ) digits[count++]='0';
- while( n!=0 ) {
- i64 q=0, r=0;
- if( signed ) { q=n/base; r=q*base-n; }
+ if( signed && n > 0 ) n = -n;
+ if( n == 0 && precision != 0 ) digits[count++] = '0';
+ while( n != 0 ) {
+ i64 q = 0, r = 0;
+ if( signed ) { q = n/base; r = q*base-n; }
else {
// Unsigned division without a u64 type. Halving first makes
// the dividend nonnegative; the remainder needs at most one fix.
- q=((n>>>1)/base)*2;
- r=n-q*base;
- if( r>=base ) { q++; r-=base; }
+ q = ((n>>>1)/base)*2;
+ r = n-q*base;
+ if( r >= base ) { q++; r -= base; }
}
- digits[count++]=alphabet[r];
- n=q;
+ digits[count++] = alphabet[r];
+ n = q;
}
- u8[~] !prefix="";
+ u8[~] !prefix = "";
if( signed ) {
- if( value<0 ) prefix="-";
- else if( spec&512 ) prefix="+";
- else if( spec&1024 ) prefix=" ";
- } else if( (spec&2048) && value!=0 ) {
- if( kind=='x' ) prefix="0x";
- if( kind=='X' ) prefix="0X";
+ if( value < 0 ) prefix = "-";
+ else if( spec&512 ) prefix = "+";
+ else if( spec&1024 ) prefix = " ";
+ } else if( (spec&2048) && value != 0 ) {
+ if( kind == 'x' ) prefix = "0x";
+ if( kind == 'X' ) prefix = "0X";
}
- i64 zeros=precision>count ? precision-count : 0;
+ i64 zeros = precision > count ? precision-count : 0;
// Alternate octal always begins with a zero, including precision-zero 0.
- if( kind=='o' && (spec&2048) && zeros==0 && (count==0 || digits[count-1]!='0') ) zeros=1;
- i64 pad=width-prefix#-zeros-count;
- bool left=(spec&256)!=0;
- bool zero=(spec&4096)!=0 && precision<0 && !left;
+ if( kind == 'o' && (spec&2048) && zeros == 0 && (count == 0 || digits[count-1] != '0') ) zeros = 1;
+ i64 pad = width-prefix#-zeros-count;
+ bool left = (spec&256) != 0;
+ bool zero = (spec&4096) != 0 && precision < 0 && !left;
if( !left && !zero ) _pad(b,pad,' ');
b.addAry(prefix);
if( zero ) _pad(b,pad,'0');
_pad(b,zeros,'0');
- while( count>0 ) b.add(digits[--count]);
+ while( count > 0 ) b.add(digits[--count]);
if( left ) _pad(b,pad,' ');
b;
};
@@ -96,3 +93,8 @@ val f64 = { f64 x -> end(floating(begin(),x, 0 ,0,-1)); };
val char = { i64 x -> end(integer (begin(),x,'c',0,-1)); };
val bool = { bool x -> end(integer (begin(),x,'b',0,-1)); };
val hex = { i64 x -> end(integer (begin(),x,'x',0,-1)); };
+
+// Time selectors occupy spec bits 16..23; padding shares string formatting.
+val time = { !aryu8 b, i64 value, i64 spec, i64 width, i64 precision ->
+ string(b,sys.time.format(value,(spec>>>16)&255),spec,width,-1);
+};
diff --git a/src/main/smp/sys/time.smp b/src/main/smp/sys/time.smp
new file mode 100644
index 00000000..a3e9b3a2
--- /dev/null
+++ b/src/main/smp/sys/time.smp
@@ -0,0 +1,219 @@
+// Time utilities; get the current time (both nanos and millis), sleep, time zones.
+
+// Instants: bare i64 UTC Unix-epoch milliseconds. Durations: i64 milliseconds.
+// Monotonic readings are a separate clock domain, for deadlines/elapsed time.
+
+val DateTime = DateTime;
+val Timers = Timers;
+i64 SECOND = 1000;
+i64 MINUTE = 60*SECOND;
+i64 HOUR = 60*MINUTE;
+i64 DAY = 24*HOUR;
+i64 NANOS_PER_MS = 1000*1000;
+
+// Gregorian leap days: every fourth year, except centuries not divisible by 400.
+i64 _DAYS_PER_4_YEARS = 4*365+1;
+i64 _DAYS_PER_CENTURY = 100*365+100/4-1;
+i64 _DAYS_PER_ERA = 4*_DAYS_PER_CENTURY+1;
+// Complete years before the Unix epoch and after the last supported date.
+i64 _DAYS_BEFORE_EPOCH = 1969*365+1969/4-1969/100+1969/400;
+i64 _DAYS_BEFORE_YEAR_10000 = 9999*365+9999/4-9999/100+9999/400;
+// The March-based calendar algorithm starts at 0000-03-01, 306 days before 0001-01-01.
+i64 _MARCH_EPOCH_DAYS = _DAYS_BEFORE_EPOCH+306;
+i64 MIN = -_DAYS_BEFORE_EPOCH*DAY; // 0001-01-01T00:00:00.000Z
+i64 MAX = (_DAYS_BEFORE_YEAR_10000-_DAYS_BEFORE_EPOCH)*DAY-1; // 9999-12-31T23:59:59.999Z
+
+// Access to the underlying LIBC functions
+int ISO = 0, CLOCK12 = 1, CLOCK24 = 2, ELAPSED = 3, DATE = 4, CLOCK24_SECONDS = 5;
+{ i64 } simple_time_now = "C";
+{ i64 } simple_time_mono = "C";
+{ i64 } simple_time_nano = "C";
+{ i64 i32 -> i32 } simple_time_sleep = "C";
+{ i64 i64 -> i32 } simple_time_local = "C";
+{ i64 i64 u32 -> i32 } simple_time_zone = "C";
+val now = { -> simple_time_now (); };
+val monotonic = { -> simple_time_mono(); };
+val nanoTime = { -> simple_time_nano(); };
+val sleep = { i64 duration_ms -> simple_time_sleep(duration_ms,0); };
+val sleepUntil = { i64 deadline_ms -> simple_time_sleep(deadline_ms,1); };
+
+// Signed duration conversions. Overflow returns -1; reverse conversions truncate
+// toward zero. An exact duration day is 24 hours, independent of DST.
+val _scale = { i64 n, i64 unit ->
+ if( n > sys.integer.MAX_I64/unit || n < sys.integer.MIN_I64/unit ) return -1;
+ return n*unit;
+};
+val seconds = { i64 n -> _scale(n,SECOND); };
+val minutes = { i64 n -> _scale(n,MINUTE); };
+val hours = { i64 n -> _scale(n,HOUR); };
+val days = { i64 n -> _scale(n,DAY); };
+val toSeconds = { i64 ms -> ms/SECOND; };
+val toMinutes = { i64 ms -> ms/MINUTE; };
+val toHours = { i64 ms -> ms/HOUR; };
+val toDays = { i64 ms -> ms/DAY; };
+val fromNanos = { i64 ns -> ns/NANOS_PER_MS; };
+val toNanos = { i64 ms -> _scale(ms,NANOS_PER_MS); };
+
+// Clamp to zero once due, and saturate for widely separated i64 readings.
+val remaining = { i64 deadline_ms, i64 current_ms ->
+ if( deadline_ms <= current_ms ) return 0;
+ if( current_ms < 0 && deadline_ms > sys.integer.MAX_I64+current_ms ) return sys.integer.MAX_I64;
+ return deadline_ms-current_ms;
+};
+
+val _mod = { i64 n, i64 unit -> n-n/unit*unit; };
+val leap = { int year -> _mod(year,4) == 0 && (_mod(year,100) != 0 || _mod(year,400) == 0); };
+val monthDays = { int year, int month ->
+ if( month < 1 || month > 12 ) return 0;
+ return month == 2 ? (leap(year) ? 29 : 28) :
+ month == 4 || month == 6 || month == 9 || month == 11 ? 30 : 31;
+};
+
+// Gregorian civil dates, expressed using 400-year eras; no per-year loops.
+val _days = { i64 year, i64 month, i64 day ->
+ year -= month <= 2 ? 1 : 0;
+ i64 era = year/400, y = year-era*400;
+ i64 mp = month+(month > 2 ? -3 : 9);
+ return era*_DAYS_PER_ERA+y*365+y/4-y/100+(153*mp+2)/5+day-1-_MARCH_EPOCH_DAYS;
+};
+
+val valid = { int year, int month, int day, int hour, int minute, int second, int millisecond ->
+ return year >= 1 && year <= 9999 && month >= 1 && month <= 12 && day >= 1 && day <= monthDays(year,month) &&
+ hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59 && second >= 0 && second <= 59 &&
+ millisecond >= 0 && millisecond <= 999;
+};
+
+val makeUTC = { int year, int month, int day, int hour, int minute, int second, int millisecond ->
+ if( !valid(year,month,day,hour,minute,second,millisecond) ) return -1;
+ return _days(year,month,day)*DAY+hour*HOUR+minute*MINUTE+second*SECOND+millisecond;
+};
+
+val utc = { i64 instant ->
+ val d = DateTime.empty();
+ if( instant < MIN || instant > MAX ) return d;
+ i64 day = instant/DAY, rest = _mod(instant,DAY);
+ if( rest < 0 ) { rest += DAY; day--; }
+ i64 z = day+_MARCH_EPOCH_DAYS, era = z/_DAYS_PER_ERA, doe = z-era*_DAYS_PER_ERA;
+ i64 y = (doe-doe/(_DAYS_PER_4_YEARS-1)+doe/_DAYS_PER_CENTURY-doe/(_DAYS_PER_ERA-1))/365;
+ i64 doy = doe-(365*y+y/4-y/100), mp = (5*doy+2)/153;
+ d.day = doy-(153*mp+2)/5+1;
+ d.month = mp+(mp < 10 ? 3 : -9);
+ d.year = y+era*400+(d.month <= 2 ? 1 : 0);
+ d.hour = rest/HOUR; d.minute = _mod(rest/MINUTE,60);
+ d.second = _mod(rest/SECOND,60); d.millisecond = _mod(rest,SECOND);
+ d.weekday = _mod(day+4,7);
+ if( d.weekday < 0 ) d.weekday += 7;
+ return d;
+};
+
+val local = { i64 instant ->
+ val d = DateTime.empty();
+ val f = new i64[10];
+ i64 ptr = f;
+ if( simple_time_local(instant,ptr) != 0 ) return d;
+ d.year = f[0]; d.month = f[1]; d.day = f[2];
+ d.hour = f[3]; d.minute = f[4]; d.second = f[5]; d.millisecond = f[6];
+ d.weekday = f[7]; d.offsetSeconds = f[8]; d.daylight = f[9] != 0;
+ return d;
+};
+
+val zoneName = { i64 instant ->
+ val bytes = new u8[128];
+ i64 ptr = bytes;
+ int n = simple_time_zone(instant,ptr,bytes#);
+ if( n < 0 ) return "";
+ val b = new aryu8();
+ for( int i = 0; i < n; i++ ) b.add(bytes[i]);
+ return text.end(b);
+};
+
+val _digits = { !aryu8 b, i64 n, int width ->
+ text.integer(b,n,'d'|4096,width,-1);
+};
+
+val _format = { i64 instant, int style, bool localZone ->
+ val d = localZone ? local(instant) : utc(instant);
+ if( d.year == 0 || style < 0 || style > 5 || style == 3 ) return "<invalid time>";
+ val b = new aryu8();
+ if( style == 4 ) {
+ _digits(b,d.day,2).add('/'); _digits(b,d.month,2).add('/'); _digits(b,_mod(d.year,100),2);
+ } else {
+ if( style == 0 ) {
+ _digits(b,d.year,4).add('-'); _digits(b,d.month,2).add('-'); _digits(b,d.day,2).add('T');
+ }
+ int hour = d.hour;
+ if( style == 1 ) { hour = _mod(hour,12); if( hour == 0 ) hour = 12; }
+ _digits(b,hour,style == 1 ? 1 : 2).add(':'); _digits(b,d.minute,2);
+ if( style == 0 || style == 5 ) { b.add(':'); _digits(b,d.second,2); }
+ if( style == 1 ) b.addAry(d.hour < 12 ? "am" : "pm");
+ if( style == 0 ) {
+ b.add('.'); _digits(b,d.millisecond,3);
+ if( d.offsetSeconds == 0 ) b.add('Z');
+ else {
+ i64 offset = d.offsetSeconds;
+ b.add(offset < 0 ? '-' : '+');
+ if( offset < 0 ) offset = -offset;
+ _digits(b,offset/3600,2).add(':'); _digits(b,_mod(offset/60,60),2);
+ if( _mod(offset,60) != 0 ) { b.add(':'); _digits(b,_mod(offset,60),2); }
+ }
+ }
+ }
+ return text.end(b);
+};
+
+val elapsed = { i64 ms ->
+ val b = new aryu8();
+ // Nonpositive arithmetic handles the minimum i64 without negating it.
+ if( ms < 0 ) b.add('-'); else ms = -ms;
+ _digits(b,-(ms/MINUTE),2).add(':');
+ _digits(b,-_mod(ms/SECOND,60),2).add('.'); _digits(b,-_mod(ms,SECOND),3);
+ return text.end(b);
+};
+val format = { i64 instant, int style ->
+ if( style == ELAPSED ) return elapsed(instant);
+ return _format(instant,style,false);
+};
+val formatLocal = { i64 instant, int style ->
+ if( style == ELAPSED ) return elapsed(instant);
+ return _format(instant,style,true);
+};
+
+val _number = { u8[~] s, i64 at, int count ->
+ if( at < 0 || count < 1 || at+count > s# ) return -1;
+ int n = 0;
+ for( int i = 0; i < count; i++ ) {
+ int c = s[at+i];
+ if( c < '0' || c > '9' ) return -1;
+ n = n*10+c-'0';
+ }
+ return n;
+};
+
+// Keep suffix validation separate from calendar construction.
+val _parseZone = { u8[~] s, int at, i64 instant ->
+ if( at < 0 || at >= s# ) return -1;
+ i64 offset = 0;
+ if( s[at] == 'Z' ) { if( at+1 != s# ) return -1; }
+ else {
+ int sign = s[at] == '+' ? 1 : s[at] == '-' ? -1 : 0;
+ if( !sign || (s# != at+6 && s# != at+9) || s[at+3] != ':' ) return -1;
+ int oh = _number(s,at+1,2), om = _number(s,at+4,2), os = 0;
+ if( s# == at+9 ) { if( s[at+6] != ':' ) return -1; os = _number(s,at+7,2); }
+ if( oh < 0 || oh > 23 || om < 0 || om > 59 || os < 0 || os > 59 ) return -1;
+ offset = sign*(oh*HOUR+om*MINUTE+os*SECOND);
+ }
+ instant -= offset;
+ return instant < MIN || instant > MAX ? -1 : instant;
+};
+
+// Strict ISO absolute instants only. -1 is reserved for failure (and therefore
+// cannot distinguish the valid instant one millisecond before Unix epoch).
+val parse = { u8[~] s ->
+ if( s# < 20 || s[4] != '-' || s[7] != '-' || s[10] != 'T' || s[13] != ':' || s[16] != ':' ) return -1;
+ int y = _number(s, 0,4), m = _number(s, 5,2), d = _number(s, 8,2);
+ int h = _number(s,11,2), min = _number(s,14,2), sec = _number(s,17,2);
+ bool fraction = s[19] == '.';
+ int ms = fraction ? _number(s,20,3) : 0;
+ if( !valid(y,m,d,h,min,sec,ms) ) return -1;
+ return _parseZone(s,fraction ? 23 : 19,makeUTC(y,m,d,h,min,sec,ms));
+};
diff --git a/src/main/smp/sys/time/DateTime.smp b/src/main/smp/sys/time/DateTime.smp
new file mode 100644
index 00000000..53b6688e
--- /dev/null
+++ b/src/main/smp/sys/time/DateTime.smp
@@ -0,0 +1,13 @@
+// Calendar view of an instant. Month/day start at 1; weekday is Sunday=0.
+// year==0 means out of the supported calendar range (years 1 through 9999).
+
+// All values default zero/false
+struct DateTime {
+ int year, month, day;
+ int hour, minute, second, millisecond;
+ int weekday, offsetSeconds;
+ bool daylight;
+};
+
+// Factory in the defining unit, so callers need no forward instance type.
+val empty = { -> new DateTime(); };
diff --git a/src/main/smp/sys/time/Timers.smp b/src/main/smp/sys/time/Timers.smp
new file mode 100644
index 00000000..7cca2e9c
--- /dev/null
+++ b/src/main/smp/sys/time/Timers.smp
@@ -0,0 +1,80 @@
+// Cooperative timers. poll returns IDs; callbacks never run on another thread.
+// IDs are u32; 0 means failure or no timer due. Millisecond values use _ms.
+
+// The
+
+
+struct Timers {
+ // Collection of timers (as struct-of-arrays)
+ u32[] !_ids = new u32[8]; // Which timer id for this slot
+ i64[] !_due_ms = new i64[8]; // Monotonic deadline in milliseconds
+ i64[] !_period_ms = new i64[8]; // When does it repeat
+ // Next available timer ID; 0 means all IDs have been used.
+ u32 !_next = 1;
+
+ // Add a new timer
+ { i64 i64 i64 -> u32 } _add = { i64 current_ms, i64 delay_ms, i64 period_ms ->
+ if( current_ms < 0 || delay_ms < 0 || current_ms > sys.integer.MAX_I64-delay_ms || _next == 0 ) return 0;
+ // Ugly linear scan for an available slot
+ int slot = 0;
+ while( slot < _ids# && _ids[slot] != 0 ) slot++;
+ // Double size for lots of timers
+ if( slot == _ids# ) {
+ if( _ids# > sys.integer.MAX_I32/2 ) return 0;
+ val ids = new u32[_ids#*2];
+ val due_ms = new i64[_ids#*2], periods_ms = new i64[_ids#*2];
+ for( int i = 0; i < _ids#; i++ ) { ids[i] = _ids[i]; due_ms[i] = _due_ms[i]; periods_ms[i] = _period_ms[i]; }
+ _ids = ids; _due_ms = due_ms; _period_ms = periods_ms;
+ // Retry against the newly published arrays after growing.
+ return _add(current_ms,delay_ms,period_ms);
+ }
+ // Next available timer id
+ u32 id = _next;
+ _next = id == sys.integer.MAX_U32 ? 0 : id+1;
+ _ids[slot] = id; _due_ms[slot] = current_ms+delay_ms; _period_ms[slot] = period_ms;
+ return id;
+ };
+
+ // Explicit-clock versions make game logic and tests deterministic.
+ { i64 i64 -> u32 } afterAt = { i64 current_ms, i64 delay_ms -> _add(current_ms,delay_ms,0); };
+ { i64 i64 -> u32 } everyAt = { i64 current_ms, i64 period_ms -> period_ms <= 0 ? 0 : _add(current_ms,period_ms,period_ms); };
+ { i64 -> u32 } after = { i64 delay_ms -> afterAt(time.monotonic(),delay_ms ); };
+ { i64 -> u32 } every = { i64 period_ms -> everyAt(time.monotonic(),period_ms); };
+
+ // Remove a timer
+ val cancel = { u32 id ->
+ if( id == 0 ) return false;
+ for( int i = 0; i < _ids#; i++ ) if( _ids[i] == id ) { _ids[i] = 0; return true; }
+ return false;
+ };
+
+ // Earliest absolute monotonic deadline in milliseconds, or -1 when empty.
+ val nextDeadline = { ->
+ i64 next_ms = -1;
+ // Ugly linear scan again, but short
+ for( int i = 0; i < _ids#; i++ )
+ if( _ids[i] != 0 && (next_ms < 0 || _due_ms[i] < next_ms) ) next_ms = _due_ms[i];
+ return next_ms;
+ };
+
+ // Next firing timer.
+
+ // Earliest deadline first; equal deadlines use registration order.
+ // Repeats skip missed ticks and retain their phase, avoiding bursts/drift.
+ { i64 -> u32 } poll = { i64 current_ms ->
+ int slot = -1;
+ for( int i = 0; i < _ids#; i++ )
+ if( _ids[i] != 0 && _due_ms[i] <= current_ms && (slot < 0 || _due_ms[i] < _due_ms[slot] ||
+ (_due_ms[i] == _due_ms[slot] && _ids[i] < _ids[slot])) ) slot = i;
+ if( slot < 0 ) return 0;
+ u32 id = _ids[slot];
+ i64 period_ms = _period_ms[slot];
+ if( period_ms == 0 ) _ids[slot] = 0;
+ else {
+ i64 steps = (current_ms-_due_ms[slot])/period_ms;
+ if( steps >= (sys.integer.MAX_I64-_due_ms[slot])/period_ms ) _ids[slot] = 0;
+ else _due_ms[slot] += (steps+1)*period_ms;
+ }
+ return id;
+ };
+};