From 299addd03094389bb622e0fe1d7ad585b51e146d Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Fri, 13 Nov 2026 13:10:15 -0800 Subject: Cleanp the new time & graphics APIs --- chapter31/docs/drawing.md | 3 + chapter31/docs/examples/Clock.smp | 76 +++++++++++-------- chapter31/docs/examples/TicTacToeGraphical.smp | 1 + chapter31/docs/time.md | 28 ++++--- chapter31/src/main/smp/sys.smp | 1 + chapter31/src/main/smp/sys/ary.smp | 4 +- chapter31/src/main/smp/sys/aryu8.smp | 2 +- chapter31/src/main/smp/sys/draw.smp | 6 +- chapter31/src/main/smp/sys/draw/Canvas.smp | 31 +++++++- chapter31/src/main/smp/sys/gfx.smp | 2 +- chapter31/src/main/smp/sys/integer.smp | 6 ++ chapter31/src/main/smp/sys/io.smp | 2 +- chapter31/src/main/smp/sys/rt/simple_args.smp | 6 +- chapter31/src/main/smp/sys/time.smp | 84 ++++++++++++++------- chapter31/src/main/smp/sys/time/DateTime.smp | 10 ++- chapter31/src/main/smp/sys/time/Timers.smp | 85 ++++++++++++++-------- .../test/java/com/seaofnodes/simple/DrawTest.java | 22 ++++-- .../test/java/com/seaofnodes/simple/TimeTest.java | 33 +++++---- 18 files changed, 267 insertions(+), 135 deletions(-) create mode 100644 chapter31/src/main/smp/sys/integer.smp diff --git a/chapter31/docs/drawing.md b/chapter31/docs/drawing.md index af0a7f06..b99c1bf8 100644 --- a/chapter31/docs/drawing.md +++ b/chapter31/docs/drawing.md @@ -50,6 +50,9 @@ Packed colors use hexadecimal literals (`0xRRGGBB`); `rgb(r,g,b)` remains handy for computed components. The font masks use leading-zero octal, with five digits after the prefix: each digit is one three-pixel row, top to bottom. For example, `A` is `025755`: rows `010`, `101`, `111`, `101`, `101`. +The font includes all printable ASCII punctuation except `@`, `&`, and `$`, +which use the question-mark fallback. Colons and slashes support clock and date +labels; quotes, brackets, arithmetic signs, and other small symbols fit in 3×5. ## Button widget diff --git a/chapter31/docs/examples/Clock.smp b/chapter31/docs/examples/Clock.smp index 900126e1..2dea18ec 100644 --- a/chapter31/docs/examples/Clock.smp +++ b/chapter31/docs/examples/Clock.smp @@ -1,50 +1,61 @@ // Local clocks; click the button or press Space to switch. Q/Escape exits. // Any argument runs ten frames, exercising both faces for smoke tests. u32 GREEN=0x62ff70, DIM=0x123d20, BACKGROUND=0x020b05; + val main = { u8[~][] args -> - val screen=new sys.draw.Canvas(720,540); + // The main Canvas + val screen = new sys.draw.Canvas(720,540); if( sys.gfx.open("Simple digital and analog clocks",screen.width,screen.height)!=sys.gfx.OK ) return 1; - val button=new sys.draw.Button(220,454,280,52,"ANALOG CLOCK"); - val timers=new sys.time.Timers(); - i64 frame=timers.every(33); - if( frame<0 ) { sys.gfx.close(); return 1; } + + // Button to flip clock style + val button = new sys.draw.Button(220,454,280,52,"ANALOG CLOCK"); + val timers = new sys.time.Timers(); + u32 frame_id = timers.every(33); + if( frame_id==0 ) { sys.gfx.close(); return 1; } + bool analog=false, held=false; int frames=0, status=sys.gfx.OK; + + // Main event loop while( status==sys.gfx.OK ) { - bool space=sys.gfx.key(' '); - bool down=sys.gfx.button(sys.gfx.LEFT); - bool click=button.update(sys.gfx.mouseX(),sys.gfx.mouseY(),down); + bool space = sys.gfx.key(' '); + bool down = sys.gfx.button(sys.gfx.LEFT); + bool click = button.update(sys.gfx.mouseX(),sys.gfx.mouseY(),down); if( click || (space && !held) ) analog=!analog; - held=space; - button.label=analog ? "DIGITAL CLOCK" : "ANALOG CLOCK"; + held = space; + button.label = analog ? "DIGITAL CLOCK" : "ANALOG CLOCK"; if( sys.gfx.key('Q') ) break; - if( timers.poll(sys.time.monotonic())==frame ) { + if( timers.poll(sys.time.monotonic())==frame_id ) { _draw(screen,sys.time.now(),analog); button.draw(screen,3,GREEN,BACKGROUND,DIM); - status=sys.gfx.present(screen.pixels); + status = sys.gfx.present(screen.pixels); if( args#>1 ) { frames++; if( frames==5 ) analog=true; if( frames==10 ) break; } } - if( status==sys.gfx.OK ) status=sys.gfx.wait(sys.time.remaining(timers.nextDeadline(),sys.time.monotonic())); + if( status!=sys.gfx.OK ) break; + // Stall until next timer event + status = sys.gfx.wait(sys.time.remaining(timers.nextDeadline(),sys.time.monotonic())); } sys.gfx.close(); return status==sys.gfx.ERROR ? 1 : 0; }; -val _draw = { !sys.draw.Canvas screen, i64 wall, i64 analog -> - val d=sys.time.local(wall); + +val _draw = { !sys.draw.Canvas screen, i64 wall_ms, i64 analog -> + val d = sys.time.local(wall_ms); screen.clear(BACKGROUND); screen.text(266,24,"SIMPLE CLOCK",4,GREEN); - if( analog ) _analog_noInline(screen,d.hour,d.minute,d.second,d.millisecond); - else _digital_noInline(screen,d.hour,d.minute,d.second); - screen.text(298,412,"%(sys.time.formatLocal(wall,sys.time.DATE))",4,GREEN); + if( analog ) _analog(screen,d.hour,d.minute,d.second,d.millisecond); + else _digital(screen,d.hour,d.minute,d.second); + screen.text(298,412,"%(sys.time.formatLocal(wall_ms,sys.time.DATE))",4,GREEN); screen.text(249,520,"SPACE SWITCH Q OR ESC QUIT",2,GREEN); }; + // a..g bits: top, upper-right, lower-right, bottom, lower-left, upper-left, // middle. Faint unlit bars retain the old LED figure-eight silhouette. -val _digit_noInline = { !sys.draw.Canvas c, int x, int y, i64 digit -> +val _digit = { !sys.draw.Canvas c, int x, int y, i64 digit -> int bits=digit==0 ? 0x3f : digit==1 ? 0x06 : digit==2 ? 0x5b : digit==3 ? 0x4f : digit==4 ? 0x66 : digit==5 ? 0x6d : digit==6 ? 0x7d : digit==7 ? 0x07 : digit==8 ? 0x7f : digit==9 ? 0x6f : 0; @@ -60,10 +71,11 @@ val _digit_noInline = { !sys.draw.Canvas c, int x, int y, i64 digit -> } } }; -val _digital_noInline = { !sys.draw.Canvas c, int hour, int minute, int second -> - _digit_noInline(c,68,166,hour/10); _digit_noInline(c,154,166,hour-hour/10*10); - _digit_noInline(c,284,166,minute/10); _digit_noInline(c,370,166,minute-minute/10*10); - _digit_noInline(c,500,166,second/10); _digit_noInline(c,586,166,second-second/10*10); + +val _digital = { !sys.draw.Canvas c, int hour, int minute, int second -> + _digit(c,68,166,hour/10); _digit(c,154,166,hour-hour/10*10); + _digit(c,284,166,minute/10); _digit(c,370,166,minute-minute/10*10); + _digit(c,500,166,second/10); _digit(c,586,166,second-second/10*10); for( int i=0; i<2; i++ ) { c.fillCircle(254+i*216,214,6,GREEN); c.fillCircle(254+i*216,262,6,GREEN); @@ -72,7 +84,7 @@ val _digital_noInline = { !sys.draw.Canvas c, int hour, int minute, int second - }; // Rational sine approximation, accurate enough for pixel clock geometry. // Arguments are nonnegative turns; no additional native math dependency. -val _sin_noInline = { f64 turn -> +val _sin = { f64 turn -> i64 whole=turn; turn-=whole; bool negative=turn>0.5; f64 x=(negative ? turn-0.5 : turn)*6.283185307179586; @@ -80,26 +92,26 @@ val _sin_noInline = { f64 turn -> f64 numerator=negative ? -16.0 : 16.0; return numerator*product/(49.34802200544679-4*product); }; -val _hand_noInline = { !sys.draw.Canvas c, f64 turn, int length, int thickness, u32 color -> - int x=360+_sin_noInline(turn)*length, y=232-_sin_noInline(turn+0.25)*length; +val _hand = { !sys.draw.Canvas c, f64 turn, int length, int thickness, u32 color -> + int x=360+_sin(turn)*length, y=232-_sin(turn+0.25)*length; c.line(360,232,x,y,thickness,color); }; -val _analog_noInline = { !sys.draw.Canvas c, int hour, int minute, int second, int millisecond -> +val _analog = { !sys.draw.Canvas c, int hour, int minute, int second, int millisecond -> c.circle(360,232,154,3,GREEN); for( int i=0; i<60; i++ ) { f64 turn=i/60.0; bool major=i-i/5*5==0; int inner=major ? 133 : 143; - int x0=360+_sin_noInline(turn)*inner, y0=232-_sin_noInline(turn+0.25)*inner; - int x1=360+_sin_noInline(turn)*149, y1=232-_sin_noInline(turn+0.25)*149; + int x0=360+_sin(turn)*inner, y0=232-_sin(turn+0.25)*inner; + int x1=360+_sin(turn)*149, y1=232-_sin(turn+0.25)*149; c.line(x0,y0,x1,y1,major ? 3 : 1,GREEN); } c.text(349,100,"12",2,GREEN); c.text(484,227,"3",2,GREEN); c.text(357,354,"6",2,GREEN); c.text(230,227,"9",2,GREEN); f64 seconds=second+millisecond/1000.0; f64 minutes=minute+seconds/60.0; - _hand_noInline(c,(hour-hour/12*12+minutes/60.0)/12.0,78,8,GREEN); - _hand_noInline(c,minutes/60.0,112,5,GREEN); - _hand_noInline(c,seconds/60.0,128,2,0xc3ffcb); + _hand(c,(hour-hour/12*12+minutes/60.0)/12.0,78,8,GREEN); + _hand(c,minutes/60.0,112,5,GREEN); + _hand(c,seconds/60.0,128,2,0xc3ffcb); c.fillCircle(360,232,6,GREEN); }; diff --git a/chapter31/docs/examples/TicTacToeGraphical.smp b/chapter31/docs/examples/TicTacToeGraphical.smp index fddcf254..da77e8f6 100644 --- a/chapter31/docs/examples/TicTacToeGraphical.smp +++ b/chapter31/docs/examples/TicTacToeGraphical.smp @@ -12,6 +12,7 @@ val main = { u8[~][] args -> int result = sys.gfx.present(screen.pixels); bool mouseHeld = false, keyHeld = false, replayHeld = false; int hover = -1, frames = 0; + while( result==sys.gfx.OK ) { result = sys.gfx.wait(17); if( result!=sys.gfx.OK ) break; diff --git a/chapter31/docs/time.md b/chapter31/docs/time.md index 04865626..10733006 100644 --- a/chapter31/docs/time.md +++ b/chapter31/docs/time.md @@ -12,9 +12,9 @@ monotonic deadline. | `time.now()` | Unix-epoch milliseconds; may jump when the system clock changes. | | `time.monotonic()` | Monotonic milliseconds for elapsed time and deadlines. | | `time.nanoTime()` | Monotonic nanoseconds, with the clock's available resolution. | -| `time.sleep(ms)` | Sleep for a nonnegative duration. | -| `time.sleepUntil(deadline)` | Wait for a monotonic millisecond deadline; returns immediately if past. | -| `time.remaining(deadline, current)` | Nonnegative difference, saturating on overflow. | +| `time.sleep(duration_ms)` | Sleep for a nonnegative duration. | +| `time.sleepUntil(deadline_ms)` | Wait for a monotonic millisecond deadline; returns immediately if past. | +| `time.remaining(deadline_ms, current_ms)` | Nonnegative difference, saturating on overflow. | Clock errors return `-1`; sleeps return zero on success or `-1` on error, with `sys.libc.errno()` available. Interrupted sleeps resume rather than @@ -27,7 +27,11 @@ to milliseconds. `toSeconds(ms)`, `toMinutes(ms)`, `toHours(ms)`, and `toDays(ms)` truncate toward zero. A duration day is exactly 24 hours, independent of daylight saving. `toNanos(ms)` and `fromNanos(ns)` convert between millisecond and nanosecond units. Multiplication overflow returns -`-1`. Constants `SECOND`, `MINUTE`, `HOUR`, and `DAY` are also available. +`-1`. Constants `SECOND`, `MINUTE`, `HOUR`, and `DAY` are also available, +defined as `1000`, `60*SECOND`, `60*MINUTE`, and `24*HOUR` respectively. +`NANOS_PER_MS` is `1000*1000`. Overflow checks use the shared +`sys.integer.MIN_I64` and `MAX_I64` bounds; `MAX_I32` and `MAX_U32` in that +module also describe the library's array and timer-ID limits. ## String interpolation @@ -83,22 +87,28 @@ formatting and calendar conversion still support that instant. Create `val timers = new sys.time.Timers();` on the game thread: +Timer IDs are `u32`, with `0` reserved for registration failure or no timer due. +Deadlines, delays, and periods remain `i64` milliseconds; their parameter, +field, and local names end in `_ms` to distinguish them from IDs. + | Operation | Meaning | | --- | --- | -| `after(delay)` | Register a one-shot timer; return its positive ID, or `-1`. | -| `every(period)` | Register a repeating timer; period must be positive. | -| `afterAt(current, delay)`, `everyAt(current, period)` | Supply a monotonic reading explicitly, useful for deterministic tests. | +| `after(delay_ms)` | Register a one-shot timer; return its positive `u32` ID, or `0`. | +| `every(period_ms)` | Register a repeating timer; period must be positive. Returns a `u32` ID, or `0`. | +| `afterAt(current_ms, delay_ms)`, `everyAt(current_ms, period_ms)` | Supply a monotonic reading explicitly, useful for deterministic tests. | | `cancel(id)` | Remove a timer; return whether it was present. | | `nextDeadline()` | Earliest monotonic deadline, or `-1` if empty. | -| `poll(current)` | Return the next due ID, or `-1` if none. | +| `poll(current_ms)` | Return the next due `u32` ID, or `0` if none. | Poll repeatedly to drain due timers. Deadlines order delivery, then registration ID breaks ties. Repeating timers advance from their previous deadline, skipping missed ticks to the first future deadline rather than drifting or replaying a backlog. Deadline overflow rejects registration or cancels a repeating timer. +IDs are never reused within a collection; registration fails once all nonzero +`u32` IDs have been issued. There are no callbacks or timer threads: the caller handles each returned ID. -`gfx.wait(timeoutMs)` processes events after waiting for input or a timeout: +`gfx.wait(timeout_ms)` processes events after waiting for input or a timeout: zero polls, `-1` waits indefinitely, values below `-1` fail. It returns the same `OK`, `CLOSED`, or `ERROR` statuses as `gfx.poll`, and may wake early. Re-read the monotonic clock after each wake. This allows one thread to handle timers, diff --git a/chapter31/src/main/smp/sys.smp b/chapter31/src/main/smp/sys.smp index 296635ae..f7889c05 100644 --- a/chapter31/src/main/smp/sys.smp +++ b/chapter31/src/main/smp/sys.smp @@ -7,6 +7,7 @@ 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 diff --git a/chapter31/src/main/smp/sys/ary.smp b/chapter31/src/main/smp/sys/ary.smp index c139cd33..11ddb7a0 100644 --- a/chapter31/src/main/smp/sys/ary.smp +++ b/chapter31/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/chapter31/src/main/smp/sys/aryu8.smp b/chapter31/src/main/smp/sys/aryu8.smp index 18543f13..88574522 100644 --- a/chapter31/src/main/smp/sys/aryu8.smp +++ b/chapter31/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/chapter31/src/main/smp/sys/draw.smp b/chapter31/src/main/smp/sys/draw.smp index 0e4a27d4..61b250d3 100644 --- a/chapter31/src/main/smp/sys/draw.smp +++ b/chapter31/src/main/smp/sys/draw.smp @@ -1,9 +1,9 @@ // Software raster drawing. No window or native calls; pixels are packed RGB (0x00RRGGBB). u32 BLACK = 0x000000; u32 WHITE = 0xffffff; -u32 RED = 0xe05252; -u32 GREEN = 0x48b88c; -u32 BLUE = 0x558ee6; +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 -> diff --git a/chapter31/src/main/smp/sys/draw/Canvas.smp b/chapter31/src/main/smp/sys/draw/Canvas.smp index 8c01b4b3..c736e4e2 100644 --- a/chapter31/src/main/smp/sys/draw/Canvas.smp +++ b/chapter31/src/main/smp/sys/draw/Canvas.smp @@ -6,7 +6,7 @@ struct Canvas { new Canvas = { int w, int h -> i64 count = w; count *= h; - if( w<=0 || h<=0 || count>2147483647 ) libc._exit(1); + if( w<=0 || h<=0 || count>sys.integer.MAX_I32 ) libc._exit(1); width = w; height = h; pixels = new u32[count]; @@ -112,9 +112,12 @@ struct Canvas { }; }; +// 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( 'a'<=c<='z' ) c -= 'a'-'A'; if( c==' ' ) return 000000; if( c=='0' ) return 075557; if( c=='1' ) return 026227; @@ -155,6 +158,30 @@ val _glyph = { u8 c -> 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; diff --git a/chapter31/src/main/smp/sys/gfx.smp b/chapter31/src/main/smp/sys/gfx.smp index cde2cc0d..6fa35117 100644 --- a/chapter31/src/main/smp/sys/gfx.smp +++ b/chapter31/src/main/smp/sys/gfx.smp @@ -39,4 +39,4 @@ 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 -> simple_gfx_wait(timeout); }; +val wait = { i64 timeout_ms -> simple_gfx_wait(timeout_ms); }; diff --git a/chapter31/src/main/smp/sys/integer.smp b/chapter31/src/main/smp/sys/integer.smp new file mode 100644 index 00000000..a839e597 --- /dev/null +++ b/chapter31/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/chapter31/src/main/smp/sys/io.smp b/chapter31/src/main/smp/sys/io.smp index 14993429..6269cde5 100644 --- a/chapter31/src/main/smp/sys/io.smp +++ b/chapter31/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/chapter31/src/main/smp/sys/rt/simple_args.smp b/chapter31/src/main/smp/sys/rt/simple_args.smp index 3f630e54..f2890c1c 100644 --- a/chapter31/src/main/smp/sys/rt/simple_args.smp +++ b/chapter31/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/chapter31/src/main/smp/sys/time.smp b/chapter31/src/main/smp/sys/time.smp index cdcb5055..1706990e 100644 --- a/chapter31/src/main/smp/sys/time.smp +++ b/chapter31/src/main/smp/sys/time.smp @@ -1,10 +1,29 @@ +// 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, MINUTE=60000, HOUR=3600000, DAY=86400000; -i64 MIN=-62135596800000, MAX=253402300799999; // Calendar years 1..9999. -i64 LIMIT=9223372036854775807; +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"; @@ -12,34 +31,36 @@ int ISO=0, CLOCK12=1, CLOCK24=2, ELAPSED=3, DATE=4, CLOCK24_SECONDS=5; { 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 now = { -> simple_time_now (); }; val monotonic = { -> simple_time_mono(); }; -val nanoTime = { -> simple_time_nano(); }; -val sleep = { i64 ms -> simple_time_sleep(ms,0); }; -val sleepUntil = { i64 deadline -> simple_time_sleep(deadline,1); }; +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. +// 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>LIMIT/unit || n<(-LIMIT-1)/unit ) return -1; + if( n>sys.integer.MAX_I64/unit || n _scale(n,SECOND); }; val minutes = { i64 n -> _scale(n,MINUTE); }; -val hours = { i64 n -> _scale(n,HOUR); }; -val days = { i64 n -> _scale(n,DAY); }; +val 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/1000000; }; -val toNanos = { i64 ms -> _scale(ms,1000000); }; +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, i64 current -> - if( deadline<=current ) return 0; - if( current<0 && deadline>LIMIT+current ) return LIMIT; - return deadline-current; +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 -> @@ -53,24 +74,27 @@ 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*146097+y*365+y/4-y/100+(153*mp+2)/5+day-1-719468; + return era*_DAYS_PER_ERA+y*365+y/4-y/100+(153*mp+2)/5+day-1-_MARCH_EPOCH_DAYS; }; + val valid = { int year, int month, int day, int hour, int minute, int second, int millisecond -> return year>=1 && year<=9999 && month>=1 && month<=12 && day>=1 && day<=monthDays(year,month) && hour>=0 && hour<=23 && minute>=0 && minute<=59 && second>=0 && second<=59 && millisecond>=0 && millisecond<=999; }; + val makeUTC = { int year, int month, int day, int hour, int minute, int second, int millisecond -> if( !valid(year,month,day,hour,minute,second,millisecond) ) return -1; return _days(year,month,day)*DAY+hour*HOUR+minute*MINUTE+second*SECOND+millisecond; }; + val utc = { i64 instant -> val d=DateTime.empty(); if( instantMAX ) return d; i64 day=instant/DAY, rest=_mod(instant,DAY); if( rest<0 ) { rest+=DAY; day--; } - i64 z=day+719468, era=z/146097, doe=z-era*146097; - i64 y=(doe-doe/1460+doe/36524-doe/146096)/365; + 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); @@ -81,6 +105,7 @@ val utc = { i64 instant -> if( d.weekday<0 ) d.weekday+=7; return d; }; + val local = { i64 instant -> val d=DateTime.empty(); val f=new i64[10]; @@ -91,6 +116,7 @@ val local = { i64 instant -> 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; @@ -104,10 +130,11 @@ val zoneName = { i64 instant -> 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); + val d = localZone ? local(instant) : utc(instant); if( d.year==0 || style<0 || style>5 || style==3 ) return ""; - val b=new aryu8(); + 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 { @@ -161,8 +188,9 @@ val _number = { u8[~] s, i64 at, int count -> } return n; }; + // Keep suffix validation separate from calendar construction. -val _parseZone_noInline = { u8[~] s, int at, i64 instant -> +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; } @@ -182,10 +210,10 @@ val _parseZone_noInline = { u8[~] s, int at, i64 instant -> // 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 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_noInline(s,fraction ? 23 : 19,makeUTC(y,m,d,h,min,sec,ms)); + return _parseZone(s,fraction ? 23 : 19,makeUTC(y,m,d,h,min,sec,ms)); }; diff --git a/chapter31/src/main/smp/sys/time/DateTime.smp b/chapter31/src/main/smp/sys/time/DateTime.smp index 5d117467..53b6688e 100644 --- a/chapter31/src/main/smp/sys/time/DateTime.smp +++ b/chapter31/src/main/smp/sys/time/DateTime.smp @@ -1,10 +1,12 @@ // 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=0, !month=0, !day=0; - int !hour=0, !minute=0, !second=0, !millisecond=0; - int !weekday=0, !offsetSeconds=0; - bool !daylight=false; + 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. diff --git a/chapter31/src/main/smp/sys/time/Timers.smp b/chapter31/src/main/smp/sys/time/Timers.smp index b98c96ef..eca32d15 100644 --- a/chapter31/src/main/smp/sys/time/Timers.smp +++ b/chapter31/src/main/smp/sys/time/Timers.smp @@ -1,54 +1,79 @@ // 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 { - i64[] !_ids=new i64[8], !_due=new i64[8], !_period=new i64[8]; - i64 !_next=1; + // 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; - val _add = { i64 current, i64 delay, i64 period -> - if( current<0 || delay<0 || current>time.LIMIT-delay || _next==time.LIMIT ) return -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#>1073741823 ) return -1; - val ids=new i64[_ids#*2], due=new i64[_ids#*2], periods=new i64[_ids#*2]; - for( int i=0; i<_ids#; i++ ) { ids[i]=_ids[i]; due[i]=_due[i]; periods[i]=_period[i]; } - _ids=ids; _due=due; _period=periods; + 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,delay,period); + return _add(current_ms,delay_ms,period_ms); } - i64 id=_next++; - _ids[slot]=id; _due[slot]=current+delay; _period[slot]=period; + // 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. - val afterAt = { i64 current, i64 delay -> _add(current,delay,0); }; - val everyAt = { i64 current, i64 period -> period<=0 ? -1 : _add(current,period,period); }; - val after = { i64 delay -> afterAt(time.monotonic(),delay); }; - val every = { i64 period -> everyAt(time.monotonic(),period); }; - val cancel = { i64 id -> - if( id<=0 ) return false; + { 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=-1; + i64 next_ms = -1; + // Ugly linear scan again, but short for( int i=0; i<_ids#; i++ ) - if( _ids[i]!=0 && (next<0 || _due[i] - int slot=-1; + { i64 -> u32 } poll = { i64 current_ms -> + int slot = -1; for( int i=0; i<_ids#; i++ ) - if( _ids[i]!=0 && _due[i]<=current && (slot<0 || _due[i]<_due[slot] || - (_due[i]==_due[slot] && _ids[i]<_ids[slot])) ) slot=i; - if( slot<0 ) return -1; - i64 id=_ids[slot], period=_period[slot]; - if( period==0 ) _ids[slot]=0; + 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-_due[slot])/period; - if( steps>=(time.LIMIT-_due[slot])/period ) _ids[slot]=0; - else _due[slot]+=(steps+1)*period; + i64 steps = (current_ms-_due_ms[slot])/period_ms; + if( steps>=(sys.integer.MAX_I64-_due_ms[slot])/period_ms ) _ids[slot]=0; + else _due_ms[slot] += (steps+1)*period_ms; } return id; }; diff --git a/chapter31/src/test/java/com/seaofnodes/simple/DrawTest.java b/chapter31/src/test/java/com/seaofnodes/simple/DrawTest.java index 72e2cd9d..1ca161cf 100644 --- a/chapter31/src/test/java/com/seaofnodes/simple/DrawTest.java +++ b/chapter31/src/test/java/com/seaofnodes/simple/DrawTest.java @@ -15,7 +15,7 @@ public class DrawTest { .replace("val main =","val _windowMain ="); StringBuilder source=new StringBuilder(clock+""" val _check_noInline={ f64 turn, int expectedX, int expectedY -> - int x=360+_sin_noInline(turn)*128, y=232-_sin_noInline(turn+0.25)*128; + int x=360+_sin(turn)*128, y=232-_sin(turn+0.25)*128; if( xexpectedX+1 || yexpectedY+1 ) { sys.io.p("turn %f(turn): %(x),%(y) expected %(expectedX),%(expectedY)\\n"); return 1; @@ -48,7 +48,7 @@ public class DrawTest { StringBuilder source=new StringBuilder(clock+""" val _renderCheck_noInline={ int second, int millisecond, int expectedX, int expectedY -> val c=new sys.draw.Canvas(720,540); - _analog_noInline(c,3,20,second,millisecond); + _analog(c,3,20,second,millisecond); var found=false; for( int y=expectedY-2; y<=expectedY+2; y++ ) for( int x=expectedX-2; x<=expectedX+2; x++ ) @@ -106,15 +106,15 @@ public class DrawTest { CodeGen code=new CodeGen(null,null,new Ary<>(new String[]{TestC.RELEASE_SYS_DIR}),"ClockRaster",clock+""" val main={ -> val c=new sys.draw.Canvas(720,540); - _digit_noInline(c,10,10,8); + _digit(c,10,10,8); if( c.pixels[10*720+40]!=GREEN || c.pixels[82*720+40]!=GREEN || c.pixels[154*720+40]!=GREEN ) return 1; - _digit_noInline(c,10,10,1); + _digit(c,10,10,1); if( c.pixels[10*720+40]!=DIM || c.pixels[40*720+78]!=GREEN || c.pixels[40*720+14]!=DIM ) return 2; - c.clear(BACKGROUND); _digital_noInline(c,23,59,58); + c.clear(BACKGROUND); _digital(c,23,59,58); if( c.pixels[214*720+254]!=GREEN || c.pixels[262*720+470]!=GREEN ) return 3; - c.clear(BACKGROUND); _analog_noInline(c,3,0,0,0); + c.clear(BACKGROUND); _analog(c,3,0,0,0); if( c.pixels[232*720+430]!=GREEN || c.pixels[160*720+360]!=0xc3ffcb || c.pixels[232*720+360]!=GREEN ) return 4; - if( _sin_noInline(0.25)<0.999 || _sin_noInline(0.75)>-0.999 || _sin_noInline(1.0)!=0.0 ) return 5; + if( _sin(0.25)<0.999 || _sin(0.75)>-0.999 || _sin(1.0)!=0.0 ) return 5; return 0; }; return main(); @@ -174,6 +174,14 @@ public class DrawTest { for( int y=0; y<5; y++ ) for( int x=0; x<3; x++ ) if( c.pixels[y*16+x]!=c.pixels[y*16+x+8] ) return 6; + c.clear(0).text(0,0,":/",2,6); + for( int y=0; y<10; y++ ) + for( int x=0; x<16; x++ ) { + int row=y/2, col=x/2; + var colon=col==1 && (row==1 || row==3); + var slash=(row<2 && col==6) || (row==2 && col==5) || (row>2 && col==4); + if( c.pixels[y*16+x]!=((colon || slash) ? 6 : 0) ) return 9; + } c.clear(0).fillCircle(-1,-1,2,7); if( c.pixels[0]!=7 || c.pixels[2]!=0 ) return 7; return 0; diff --git a/chapter31/src/test/java/com/seaofnodes/simple/TimeTest.java b/chapter31/src/test/java/com/seaofnodes/simple/TimeTest.java index c3cdc6cf..dfffc1b9 100644 --- a/chapter31/src/test/java/com/seaofnodes/simple/TimeTest.java +++ b/chapter31/src/test/java/com/seaofnodes/simple/TimeTest.java @@ -88,20 +88,27 @@ public class TimeTest extends TestC { @Test public void timerOrderingAndMissedTicks() throws Exception { checks(""" val q=new sys.time.Timers(); - if( q.poll(0)!=-1 || q.nextDeadline()!=-1 ) return 1; - i64 a=q.afterAt(100,20), b=q.afterAt(100,10), c=q.afterAt(100,10); - if( q.nextDeadline()!=110 || q.poll(109)!=-1 || q.poll(110)!=b || q.poll(110)!=c || q.poll(110)!=-1 ) return 2; - if( !q.cancel(a) || q.cancel(a) || q.cancel(-1) || q.nextDeadline()!=-1 ) return 3; - i64 repeat=q.everyAt(100,10); - if( q.poll(135)!=repeat || q.nextDeadline()!=140 || q.poll(135)!=-1 ) return 4; + if( q.poll(0)!=0 || q.nextDeadline()!=-1 ) return 1; + u32 a=q.afterAt(100,20), b=q.afterAt(100,10), c=q.afterAt(100,10); + if( a==0 || b==0 || c==0 ) return 10; + if( q.nextDeadline()!=110 || q.poll(109)!=0 || q.poll(110)!=b || q.poll(110)!=c || q.poll(110)!=0 ) return 2; + if( !q.cancel(a) || q.cancel(a) || q.cancel(0) || q.nextDeadline()!=-1 ) return 3; + u32 repeat=q.everyAt(100,10); + if( q.poll(135)!=repeat || q.nextDeadline()!=140 || q.poll(135)!=0 ) return 4; if( q.poll(140)!=repeat || q.nextDeadline()!=150 || !q.cancel(repeat) ) return 5; - for( int i=0; i<20; i++ ) if( q.afterAt(200,i)<0 ) return 11; - i64 prev=-1; - for( int i=0; i<20; i++ ) { i64 id=q.poll(300); if( id<=prev ) return 6; prev=id; } - if( q.nextDeadline()!=-1 || q.poll(1000)!=-1 ) return 7; - if( q.afterAt(-1,1)!=-1 || q.afterAt(0,-1)!=-1 || q.everyAt(0,0)!=-1 || q.afterAt(9223372036854775807,1)!=-1 ) return 8; - i64 last=q.everyAt(9223372036854775806,1); - if( last<0 || q.poll(9223372036854775807)!=last || q.nextDeadline()!=-1 ) return 9; + for( int i=0; i<20; i++ ) if( q.afterAt(200,i)==0 ) return 11; + u32 prev=0; + for( int i=0; i<20; i++ ) { u32 id=q.poll(300); if( id<=prev ) return 6; prev=id; } + if( q.nextDeadline()!=-1 || q.poll(1000)!=0 ) return 7; + if( q.afterAt(-1,1)!=0 || q.afterAt(0,-1)!=0 || q.everyAt(0,0)!=0 || q.afterAt(9223372036854775807,1)!=0 ) return 8; + u32 last=q.everyAt(9223372036854775806,1); + if( last==0 || q.poll(9223372036854775807)!=last || q.nextDeadline()!=-1 ) return 9; + // Exhaust IDs without wrapping or letting an old ID cancel a new timer. + q._next=0xfffffffe; + u32 high=q.afterAt(0,10), max=q.afterAt(0,10); + if( high!=0xfffffffe || max!=0xffffffff || q.afterAt(0,0)!=0 ) return 12; + if( q.cancel(a) || q.poll(10)!=high || q.poll(10)!=max || q.poll(10)!=0 ) return 13; + if( q.afterAt(0,0)!=0 || q.nextDeadline()!=-1 ) return 14; ""","timeTimers"); } -- cgit v1.2.3