From 207febef9924c8fa5391185e75d42a7ea480bb38 Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Mon, 10 Aug 2026 11:11:18 -0700 Subject: Chapter 31: Software Drawing, Time, and Graphical Games Co-authored-by: Hsiang-Ying Fu <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: fuhsnn <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: Symious <14933944+symious@users.noreply.github.com> Co-authored-by: Jun Kawasaki Co-authored-by: Claude Opus 5 --- src/main/smp/sys.smp | 6 + src/main/smp/sys/adt/bitset.smp | 4 +- src/main/smp/sys/ary.smp | 8 +- src/main/smp/sys/aryi64.smp | 6 +- src/main/smp/sys/aryu8.smp | 13 +-- 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/libc.smp | 8 +- src/main/smp/sys/math.smp | 15 +++ src/main/smp/sys/rt/simple_args.smp | 8 +- src/main/smp/sys/text.smp | 84 +++++++------- src/main/smp/sys/time.smp | 219 ++++++++++++++++++++++++++++++++++++ src/main/smp/sys/time/DateTime.smp | 13 +++ src/main/smp/sys/time/Timers.smp | 80 +++++++++++++ 19 files changed, 687 insertions(+), 66 deletions(-) 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/math.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/smp') diff --git a/src/main/smp/sys.smp b/src/main/smp/sys.smp index 1201315b..3fb8d94b 100644 --- a/src/main/smp/sys.smp +++ b/src/main/smp/sys.smp @@ -2,12 +2,18 @@ // top-level default standard library import val adt = adt; // Abstract Data Types +val audio = audio; // Optional background sound (miniaudio) val ary = ary; // Array utilities val aryi64 = aryi64; // Variable sized array of i64 val aryu8 = aryu8; // Variable sized array of u8 val char = char; // Character utilities +val integer = integer; // Integer bounds +val math = math; // Floating-point representation primitives val io = io; // IO library +val gfx = gfx; // Optional pixel window (MiniFB) +val draw = draw; // Software raster drawing in Simple val text = text; // Typed text conversions +val time = time; // Clocks, calendar text, and cooperative timers val libc = libc; // libc FFI bindings val rt = rt; // Run Time support val scan = scan; // A simple string parser diff --git a/src/main/smp/sys/adt/bitset.smp b/src/main/smp/sys/adt/bitset.smp index d02cd2fe..a51a63e1 100644 --- a/src/main/smp/sys/adt/bitset.smp +++ b/src/main/smp/sys/adt/bitset.smp @@ -20,7 +20,7 @@ struct bitset { if( x < len ) return self; while( x >= len ) len *= 2; var bits = new i64[len]; - for( int i=0; i<_bits#; i++ ) + for( int i = 0; i < _bits#; i++ ) bits[i] = _bits[i]; _bits = bits; return self; @@ -51,7 +51,7 @@ struct bitset { }; val clear = { -> - for( int i=0; i<_bits#; i++ ) _bits[i] = 0; + for( int i = 0; i < _bits#; i++ ) _bits[i] = 0; return self; }; }; diff --git a/src/main/smp/sys/ary.smp b/src/main/smp/sys/ary.smp index c139cd33..b94ffa8f 100644 --- a/src/main/smp/sys/ary.smp +++ b/src/main/smp/sys/ary.smp @@ -3,12 +3,12 @@ // Geometric growth without wrapping a u32 length. Zero means too large. val capacity = { u32 current, i64 needed -> - if( needed < 0 || needed > 4294967295 ) return 0; + if( needed < 0 || needed > sys.integer.MAX_U32 ) return 0; if( needed <= current ) return current; i64 size = current ? current : 1; while( size < needed ) { size *= 2; - if( size > 4294967295 ) size = 4294967295; + if( size > sys.integer.MAX_U32 ) size = sys.integer.MAX_U32; } u32 result = size; return result; @@ -19,7 +19,7 @@ val copy = { u8[] dst, u32 doff, u8[~] src, u32 soff, u32 len -> if( doff > dst# || soff > src# ) return dst; if( len > dst# - doff ) len = dst# - doff; if( len > src# - soff ) len = src# - soff; - for( u32 i=0; i u32 len1 = len > left1 ? left1 : len; u32 len2 = len > left2 ? left2 : len; u32 max = len1 > len2 ? len2 : len1; - for( u32 i=0; i // TODO: clunky default constructor syntax var str = new aryu8().add('['); - for( u32 i=0; i 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 - 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 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 - i64 len=s#; - if( precision>=0 && precision= 0 && precision < len ) len = precision; + i64 pad = width-len; if( !(spec&256) ) _pad(b,pad,' '); - for( i64 i=0; i - 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 ""; + 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; + }; +}; -- cgit v1.2.3