summaryrefslogtreecommitdiff
path: root/chapter31
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-11-12 23:28:58 -0800
committerCliff Click <cliffc@acm.org>2026-11-12 23:28:58 -0800
commitb7440c1e4c182ad977996d91bfb8125091ffae92 (patch)
tree8f530f554d1b1e1c1eba02f361286e3b6b575849 /chapter31
parent40d2d3f458846494c6e4bbf284fe5a61bb4bb417 (diff)
downloadSimple-b7440c1e4c182ad977996d91bfb8125091ffae92.tar.gz
Simple-b7440c1e4c182ad977996d91bfb8125091ffae92.zip
ch31 early push needs review
Diffstat (limited to 'chapter31')
-rw-r--r--chapter31/Makefile7
-rw-r--r--chapter31/README.md9
-rw-r--r--chapter31/docs/31-grammar.md9
-rw-r--r--chapter31/docs/drawing.md18
-rw-r--r--chapter31/docs/examples/Clock.smp105
-rw-r--r--chapter31/docs/examples/README.md6
-rw-r--r--chapter31/docs/examples/TicTacToeGraphical.smp2
-rw-r--r--chapter31/docs/graphics.md25
-rw-r--r--chapter31/docs/time.md119
-rw-r--r--chapter31/graphics.mk8
-rw-r--r--chapter31/src/main/c/simple_graphics.c58
-rw-r--r--chapter31/src/main/c/simple_graphics.h3
-rw-r--r--chapter31/src/main/c/simple_minifb.c36
-rw-r--r--chapter31/src/main/c/simple_time.c61
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/codegen/Encoding.java10
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java16
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java27
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java2
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java2
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java18
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java12
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/parser/Lexer.java9
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/parser/Strings.java2
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java4
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/type/TypeMem.java9
-rw-r--r--chapter31/src/main/smp/sys.smp1
-rw-r--r--chapter31/src/main/smp/sys/draw.smp1
-rw-r--r--chapter31/src/main/smp/sys/draw/Button.smp41
-rw-r--r--chapter31/src/main/smp/sys/gfx.smp6
-rw-r--r--chapter31/src/main/smp/sys/text.smp5
-rw-r--r--chapter31/src/main/smp/sys/time.smp191
-rw-r--r--chapter31/src/main/smp/sys/time/DateTime.smp11
-rw-r--r--chapter31/src/main/smp/sys/time/Timers.smp55
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/Chapter21Test.java1
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java2
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/DrawTest.java116
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/FloatComparisonNativeTest.java65
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/GraphicsTest.java60
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/StreamIOTest.java1
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/StringFormatTest.java4
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/TestC.java1
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/TimeTest.java176
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java2
43 files changed, 1242 insertions, 74 deletions
diff --git a/chapter31/Makefile b/chapter31/Makefile
index e685b205..c9a398e8 100644
--- a/chapter31/Makefile
+++ b/chapter31/Makefile
@@ -39,6 +39,7 @@ RELEASE_SYS:= $(RELEASE)/$(CPU_ABI)/sys.o
RELEASE_CRT:= $(RELEASE)/$(CPU_ABI)/simple_crt.o
RELEASE_GRAPHICS:= $(RELEASE)/$(CPU_ABI)/simple_graphics.o
RELEASE_AUDIO:= $(RELEASE)/$(CPU_ABI)/simple_audio.o
+RELEASE_TIME:= $(RELEASE)/$(CPU_ABI)/simple_time.o
RELEASE_ARGS:= $(RELEASE)/$(CPU_ABI)/simple_args.o
C_CALL_CONV:= $(if $(filter win64,$(CALL_CONV)),ms_abi,sysv_abi)
GAME_DIR:= build/examples/$(CPU_ABI)
@@ -67,7 +68,7 @@ $(RELEASE_ARGS): src/main/smp/sys/rt/simple_args.smp $(CLZDIR)/main/.mtag $(simp
@echo "simping " $@
@$(JVM) com.seaofnodes.simple.Simple --cpu $(CPU_PORT) --abi $(CALL_CONV) --norun -o $@ $<
-$(RELEASE_CRT): src/main/c/simple_crt.c $(RELEASE_ARGS) $(RELEASE_GRAPHICS) $(RELEASE_AUDIO) Makefile
+$(RELEASE_CRT): src/main/c/simple_crt.c $(RELEASE_ARGS) $(RELEASE_GRAPHICS) $(RELEASE_AUDIO) $(RELEASE_TIME) Makefile
@echo "compiling " $@
@[ -d $(dir $@) ] || mkdir -p $(dir $@)
@gcc -c -O2 -DCALL_CONV=$(C_CALL_CONV) -o $@ $<
@@ -85,6 +86,10 @@ $(RELEASE_AUDIO): src/main/c/simple_audio.c src/main/c/simple_audio.h Makefile
@mkdir -p $(dir $@)
@gcc -c -O2 -Wall -Wextra -o $@ $<
+$(RELEASE_TIME): src/main/c/simple_time.c Makefile
+ @mkdir -p $(dir $@)
+ @gcc -c -O2 -Wall -Wextra -o $@ $<
+
.PHONY: tests_nographics
tests_nographics:
@SIMPLE_MINIFB=off SIMPLE_MINIAUDIO=off $(MAKE) tests
diff --git a/chapter31/README.md b/chapter31/README.md
index 65c8d787..b82d9612 100644
--- a/chapter31/README.md
+++ b/chapter31/README.md
@@ -1,4 +1,4 @@
-# Chapter 31: Software Drawing and Graphical Games
+# Chapter 31: Software Drawing, Time, and Graphical Games
English | [日本語](README.ja.md)
@@ -17,6 +17,13 @@ or falling win/loss cues, played in the background by miniaudio.
Hexadecimal colors (`0xe47b57`) and leading-zero octal font masks (`025755`)
are supported by this chapter's integer literal syntax.
+The [time library](docs/time.md) adds millisecond clocks, calendar conversion,
+absolute ISO parsing, built-in time formatting, sleeps, and cooperative timers.
+The [digital and analog clock](docs/examples/Clock.smp) demonstrates single-threaded
+animation and the reusable `sys.draw.Button` widget:
+`make -C chapter31 run-clock CTAGS=`. Click its button or press Space to switch
+between green seven-segment digits and an analog dial.
+
```sh
make -C chapter31 libs CTAGS= # Once while online (also: init or lib).
make -C chapter31 run-graphical-tictactoe CTAGS=
diff --git a/chapter31/docs/31-grammar.md b/chapter31/docs/31-grammar.md
index 19f834af..3083fcc2 100644
--- a/chapter31/docs/31-grammar.md
+++ b/chapter31/docs/31-grammar.md
@@ -195,10 +195,13 @@ FLOAT_LITERAL : DIGIT+ ('.' DIGIT* ('e' DIGIT+)? | 'e' '-'? DIGIT+) 'f'?
// Interpolation re-enters the ordinary expression grammar, recursively.
stringExpression : '"' (STRING_TEXT | ESCAPE | '%%' | interpolation)* '"' ;
interpolation : '%' format? '(' assignment ')' ;
-format : FORMAT_FLAG* FORMAT_WIDTH? ('.' FORMAT_WIDTH?)? FORMAT_KIND ;
+format : FORMAT_FLAG* FORMAT_WIDTH? ('.' FORMAT_WIDTH?)? FORMAT_KIND TIME_STYLE? ;
// Flags: - + space # 0. Width/precision: decimal digits, no expressions.
-// Kinds: d i u o x X f F e E g G a A c s b.
-// Duplicate/inapplicable flags and precision on c/b are rejected.
+// Kinds: d i u o x X f F e E g G a A c s b t.
+// TIME_STYLE: F r R M D T, allowed only after t; default F (ISO UTC).
+// Time styles: ISO, 12-hour, 24-hour, elapsed MM:SS.mmm, DD/MM/YY, HH:MM:SS.
+// Duplicate/inapplicable flags and precision on c/b/t are rejected.
+// Time accepts width and left alignment only; see time.md.
// STRING_TEXT excludes double quote, backslash, and percent.
CHARACTER_LITERAL : '\'' (ESCAPE | ~['\\]) '\'' ;
fragment ESCAPE : '\\' [0ntr\\'"%] ;
diff --git a/chapter31/docs/drawing.md b/chapter31/docs/drawing.md
index 3fec1f12..af0a7f06 100644
--- a/chapter31/docs/drawing.md
+++ b/chapter31/docs/drawing.md
@@ -51,6 +51,22 @@ 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`.
+## Button widget
+
+`new sys.draw.Button(x,y,width,height,label)` creates a rectangular software
+button. It owns no window or native resources. Feed pointer coordinates and
+the held mouse-button state to `update(x,y,down)` once per event-loop iteration.
+It returns true once on release inside, provided the press began inside.
+Dragging in from outside does not activate it; releasing outside cancels.
+
+`hovered` and `pressed` expose visual state. Set `enabled=false` to cancel a
+pending press and disable activation; `label` can be changed at any time.
+`contains(x,y)` uses half-open bounds. `draw(canvas,scale,foreground,background,active)`
+paints the button and centers its 3x5 label when it fits. Colors belong to the
+caller. Keyboard shortcuts belong to the application; this small widget has
+no focus system. The [clock example](examples/Clock.smp) shows mouse activation
+and a Space shortcut in the same single-threaded loop.
+
## Graphical tic-tac-toe
From `chapter31`:
@@ -72,7 +88,7 @@ polling still processes window events and waits between polls.
`make graphical-tictactoe CTAGS=` builds without opening a window or requiring
MiniFB. Its output is `build/examples/x86_64_v2_win64/TicTacToeGraphical.exe`
on Windows or `build/examples/x86_64_v2_SystemV/TicTacToeGraphical` on Linux.
-The explicit backend path follows [the pixel-window instructions](graphics.md).
+Backend discovery and explicit overrides follow [the pixel-window instructions](graphics.md).
Passing any argument performs a five-poll, self-closing smoke test.
The original `make tictactoe` still builds the terminal game.
diff --git a/chapter31/docs/examples/Clock.smp b/chapter31/docs/examples/Clock.smp
new file mode 100644
index 00000000..900126e1
--- /dev/null
+++ b/chapter31/docs/examples/Clock.smp
@@ -0,0 +1,105 @@
+// 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);
+ 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; }
+ bool analog=false, held=false;
+ int frames=0, status=sys.gfx.OK;
+ 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);
+ if( click || (space && !held) ) analog=!analog;
+ held=space;
+ button.label=analog ? "DIGITAL CLOCK" : "ANALOG CLOCK";
+ if( sys.gfx.key('Q') ) break;
+ if( timers.poll(sys.time.monotonic())==frame ) {
+ _draw(screen,sys.time.now(),analog);
+ button.draw(screen,3,GREEN,BACKGROUND,DIM);
+ 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()));
+ }
+ 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);
+ 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);
+ 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 ->
+ 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;
+ for( int i=0; i<7; i++ ) {
+ u32 color=(bits&(1<<i))!=0 ? GREEN : DIM;
+ if( i==0 || i==3 || i==6 ) {
+ int dy=i==0 ? 0 : i==3 ? 144 : 72;
+ c.line(x+14,y+dy,x+58,y+dy,10,color);
+ } else {
+ bool right=i==1 || i==2;
+ int dx=right ? 68 : 4, dy=i==1 || i==5 ? 12 : 84;
+ c.line(x+dx,y+dy,x+dx,y+dy+48,10,color);
+ }
+ }
+};
+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);
+ for( int i=0; i<2; i++ ) {
+ c.fillCircle(254+i*216,214,6,GREEN);
+ c.fillCircle(254+i*216,262,6,GREEN);
+ }
+ c.text(272,352,"LOCAL TIME 24H",3,GREEN);
+};
+// Rational sine approximation, accurate enough for pixel clock geometry.
+// Arguments are nonnegative turns; no additional native math dependency.
+val _sin_noInline = { f64 turn ->
+ i64 whole=turn; turn-=whole;
+ bool negative=turn>0.5;
+ f64 x=(negative ? turn-0.5 : turn)*6.283185307179586;
+ f64 product=x*(3.141592653589793-x);
+ 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;
+ c.line(360,232,x,y,thickness,color);
+};
+val _analog_noInline = { !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;
+ 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);
+ c.fillCircle(360,232,6,GREEN);
+};
diff --git a/chapter31/docs/examples/README.md b/chapter31/docs/examples/README.md
index a1d81be2..46965ac0 100644
--- a/chapter31/docs/examples/README.md
+++ b/chapter31/docs/examples/README.md
@@ -10,6 +10,12 @@ The [graphical tic-tac-toe](TicTacToeGraphical.smp) uses the Simple
replay, and Q/Escape to quit. The terminal game below remains available.
Optional [background audio](../audio.md) supplies soft move and win/loss cues.
+The [digital and analog clock](Clock.smp) demonstrates the [time library](../time.md),
+green seven-segment digits, smooth clock hands, and a reusable software button.
+Run `make run-clock CTAGS=`; click the button or press Space to switch faces,
+and Q/Escape to exit. Both faces show system-local time.
+`make clock-example CTAGS=` builds it without starting a window.
+
From `chapter31`, build the tic-tac-toe executable (without starting it):
```sh
diff --git a/chapter31/docs/examples/TicTacToeGraphical.smp b/chapter31/docs/examples/TicTacToeGraphical.smp
index b4f8bcf4..fddcf254 100644
--- a/chapter31/docs/examples/TicTacToeGraphical.smp
+++ b/chapter31/docs/examples/TicTacToeGraphical.smp
@@ -13,7 +13,7 @@ val main = { u8[~][] args ->
bool mouseHeld = false, keyHeld = false, replayHeld = false;
int hover = -1, frames = 0;
while( result==sys.gfx.OK ) {
- result = sys.gfx.poll(true);
+ result = sys.gfx.wait(17);
if( result!=sys.gfx.OK ) break;
if( sys.gfx.key('Q') ) break;
bool down = sys.gfx.button(sys.gfx.LEFT);
diff --git a/chapter31/docs/graphics.md b/chapter31/docs/graphics.md
index 2394a401..4977b490 100644
--- a/chapter31/docs/graphics.md
+++ b/chapter31/docs/graphics.md
@@ -21,7 +21,7 @@ downloads dependencies unless you explicitly request `make libs` or `make init`
windows. It excludes the interactive `run-graphics` target.
The runtime contains a small loader using the OS dynamic-library facilities.
-It does nothing until `sys.gfx.open`. Without a configured backend, `open`
+It does nothing until `sys.gfx.open`. Without an available backend, `open`
prints a diagnostic to stderr and returns `sys.gfx.ERROR` (-1). Programs can
return a nonzero status or fall back to text; the library does not terminate
the process. Non-graphics programs ignore even a broken graphics configuration.
@@ -92,9 +92,16 @@ SIMPLE_MINIFB="$PWD/build/release/x86_64_v2_SystemV/libsimple_minifb.so" \
xvfb-run -a build/examples/x86_64_v2_SystemV/Graphics smoke
```
-The backend path is explicit: a DLL installed elsewhere or a different working
-directory cannot silently select the wrong version. `SIMPLE_MINIFB=off` disables
-graphics; an absent, unloadable, or incompatible backend produces a diagnostic.
+With no `SIMPLE_MINIFB` setting, the runtime searches beside the executable,
+then its `../../release/<ABI>` and `../release/<ABI>` directories. These cover
+the examples and test build layouts regardless of the current directory.
+It also checks the current directory, `build/release/<ABI>`, and
+`chapter31/build/release/<ABI>` relative to the current directory. Thus built
+examples such as `Clock.exe` can be launched directly without configuration.
+
+`SIMPLE_MINIFB` overrides discovery; an invalid explicit path reports an error
+without falling back. `SIMPLE_MINIFB=off` disables graphics for headless tests.
+An absent, unloadable, or incompatible backend produces a diagnostic.
A backend that loads but cannot create a window also reports an error cleanly.
## Initial interface
@@ -103,15 +110,21 @@ A backend that loads but cannot create a window also reports an error cleanly.
| --- | --- |
| `open(title,width,height)` | Open one window; returns `OK` or `ERROR`. |
| `present(pixels)` | Present a `u32[~]` of exactly `width*height` pixels, packed `0x00RRGGBB`. |
-| `poll(wait)` | Process input; `true` waits about 17ms first to avoid a busy loop. |
+| `poll(wait)` | Process input; `true` waits up to 17ms, waking for input. |
+| `wait(timeoutMs)` | Wait for input or timeout, then process events; 0 polls, -1 waits indefinitely. |
| `mouseX()`, `mouseY()` | Current mouse position, origin at upper left. |
| `button(LEFT)`, `key(code)` | Current pressed state; printable keys use uppercase ASCII. |
| `close()` | Release native window and pixel storage; safe to repeat. |
-`present` and `poll` return `OK` (0), `CLOSED` (1, normal close/Escape), or
+`present`, `poll`, and `wait` return `OK` (0), `CLOSED` (1, normal close/Escape), or
`ERROR` (-1). Stop polling after either terminal result and call `close`.
The initial input API samples held state; it is not a queue of every transition.
+Waits may return early; values below -1 are errors. Recheck the monotonic
+clock after waking. The [time library and animation example](time.md) combine
+this wait with cooperative timer deadlines. Backend API version 2 adds the
+wait operation; rebuild older MiniFB adapters from the cached sources.
+
MiniFB retains its last framebuffer for repaint events. The adapter copies the
Simple array into native-owned storage before presenting; it never retains a
Simple heap pointer. Framebuffer allocation, the copy, and window/input calls
diff --git a/chapter31/docs/time.md b/chapter31/docs/time.md
new file mode 100644
index 00000000..04865626
--- /dev/null
+++ b/chapter31/docs/time.md
@@ -0,0 +1,119 @@
+# Time and single-threaded animation
+
+`sys.time` uses bare `i64` values: absolute instants are milliseconds since
+the Unix epoch in UTC; durations are milliseconds. Monotonic readings belong
+to a separate clock domain. Never subtract a wall-clock reading from a
+monotonic deadline.
+
+## Clocks and waits
+
+| Call | Result |
+| --- | --- |
+| `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. |
+
+Clock errors return `-1`; sleeps return zero on success or `-1` on error,
+with `sys.libc.errno()` available. Interrupted sleeps resume rather than
+returning early. Wakeup times are approximate, and these calls do not wake a
+suspended laptop. Sleeping blocks the one Simple execution thread; use
+`gfx.wait` when window input must remain responsive.
+
+`seconds(n)`, `minutes(n)`, `hours(n)`, and `days(n)` convert signed counts
+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.
+
+## String interpolation
+
+Time formatting is built into the existing string interpolation syntax:
+
+| Format | Example | Named style |
+| --- | --- | --- |
+| `%t(instant)` or `%tF(instant)` | `2026-11-12T00:34:05.123Z` | `time.ISO` |
+| `%tr(instant)` | `12:34am` | `time.CLOCK12` |
+| `%tR(instant)` | `00:34` | `time.CLOCK24` |
+| `%tT(instant)` | `00:34:05` | `time.CLOCK24_SECONDS` |
+| `%tD(instant)` | `12/11/26` | `time.DATE` |
+| `%tM(duration)` | `02:03.456` | `time.ELAPSED` |
+
+For example, `"UTC %tR(sys.time.now())"`. Width and left alignment work as
+for strings: `%30t(instant)` and `%-30t(instant)`. Precision and numeric flags
+are rejected. `%tM` formats a duration, with total minutes rather than wrapping
+at an hour; negative durations keep their sign.
+
+Interpolation uses UTC, making its output stable across machines.
+`time.format(instant, style)` is the equivalent library call;
+`time.formatLocal(instant, style)` uses the system's local zone. Local ISO
+output includes a numeric offset instead of `Z` when nonzero, retaining
+offset seconds for historical zones. Invalid calendar values/styles produce
+`<invalid time>`. The ISO form retains all millisecond bits within the supported
+calendar range; short formats intentionally omit information.
+
+## Calendar values and absolute parsing
+
+`time.utc(instant)` and `time.local(instant)` return a `DateTime` with fields
+`year`, `month`, `day`, `hour`, `minute`, `second`, `millisecond`, `weekday`
+(Sunday = 0), `offsetSeconds` (east of UTC), and `daylight`. A zero year means
+conversion failed. `time.zoneName(instant)` returns the local abbreviation or
+an empty string on error. Local conversion uses the operating system's zone
+rules, including DST at that instant; no time-zone database is downloaded.
+
+`time.makeUTC(year, month, day, hour, minute, second, millisecond)` validates
+and constructs an instant, returning `-1` on invalid fields. `time.valid(...)`,
+`time.leap(year)`, and `time.monthDays(year, month)` support calendar checks.
+Calendar conversion supports Gregorian years 1 through 9999, bounded by
+`time.MIN` and `time.MAX`, and rejects leap seconds.
+
+`time.parse(text)` accepts strict absolute ISO text:
+`YYYY-MM-DDTHH:mm:ss[.SSS]Z` or the same with an explicit `+HH:MM`/`-HH:MM`
+offset (optionally with `:SS`). Fractions have exactly three digits. No relative
+time parsing, implicit local zone, locale-dependent parsing, or named-zone
+conversion is provided. Offsets must have hours 00–23 and minutes/seconds 00–59.
+Invalid text or an out-of-range result returns `-1`. By design, this sentinel
+is indistinguishable from the valid instant `1969-12-31T23:59:59.999Z`;
+formatting and calendar conversion still support that instant.
+
+## Cooperative timers
+
+Create `val timers = new sys.time.Timers();` on the game thread:
+
+| 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. |
+| `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 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.
+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:
+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,
+input, simulation, and drawing without busy waiting.
+
+The [digital and analog clock example](examples/Clock.smp) combines these pieces. From
+the repository root, with graphics dependencies already cached:
+
+```sh
+make -C chapter31 clock-example CTAGS= # Build without opening a window.
+make -C chapter31 run-clock CTAGS= # Build the backend and run.
+```
+
+Click the button or press Space to switch between green seven-segment digits
+and an analog dial with smoothly moving hands. Both display system-local time;
+Q/Escape exits. Passing any argument directly to the executable closes it after
+ten frames, switching faces halfway through. Hands are computed from the current
+time, so delayed frames do not create a simulation catch-up backlog.
diff --git a/chapter31/graphics.mk b/chapter31/graphics.mk
index 9ede649d..5b0bce09 100644
--- a/chapter31/graphics.mk
+++ b/chapter31/graphics.mk
@@ -23,6 +23,14 @@ endif
MINIFB_SOURCES = $(addprefix $(MINIFB_SRC)/src/,$(addsuffix .c,$(MINIFB_COMMON) $(MINIFB_PLATFORM)))
GRAPHICS_EXAMPLE := $(GAME_DIR)/Graphics$(if $(filter Windows_NT,$(OS)),.exe,)
GRAPHICAL_TICTACTOE := $(GAME_DIR)/TicTacToeGraphical$(if $(filter Windows_NT,$(OS)),.exe,)
+CLOCK_EXAMPLE := $(GAME_DIR)/Clock$(if $(filter Windows_NT,$(OS)),.exe,)
+
+.PHONY: clock-example run-clock
+clock-example: $(CLOCK_EXAMPLE)
+$(CLOCK_EXAMPLE): docs/examples/Clock.smp $(CLZDIR)/test/.ttag $(RELEASE_SYS) $(RELEASE_CRT) graphics.mk
+ @$(JVM) com.seaofnodes.simple.Simple --link --cpu $(CPU_PORT) --abi $(CALL_CONV) -L $(dir $(RELEASE_SYS)) -o $(GAME_DIR)/Clock.o $<
+run-clock: graphics clock-example
+ SIMPLE_MINIFB="$(abspath $(GRAPHICS_LIB))" $(CLOCK_EXAMPLE)
.PHONY: graphics graphics-example run-graphics
.PHONY: graphical-tictactoe run-graphical-tictactoe
diff --git a/chapter31/src/main/c/simple_graphics.c b/chapter31/src/main/c/simple_graphics.c
index 41073470..40d66ba9 100644
--- a/chapter31/src/main/c/simple_graphics.c
+++ b/chapter31/src/main/c/simple_graphics.c
@@ -1,8 +1,48 @@
+#define _POSIX_C_SOURCE 200809L
#include "simple_graphics.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
+#include <unistd.h>
+
+#ifdef __CYGWIN__
+#define BACKEND_NAME "simple_minifb.dll"
+#define BACKEND_ABI "x86_64_v2_win64"
+#else
+#define BACKEND_NAME "libsimple_minifb.so"
+#define BACKEND_ABI "x86_64_v2_SystemV"
+#endif
+
+// Executable-relative paths work even when launched outside the checkout.
+// Working-directory paths also cover test executables and chapter/root shells.
+static void *find_backend(void) {
+ char executable[4096], path[8192];
+ ssize_t n=readlink("/proc/self/exe",executable,sizeof(executable)-1);
+ if (n>0 && n<(ssize_t)sizeof(executable)-1) {
+ executable[n]=0;
+ char *slash=strrchr(executable,'/');
+ if (slash) {
+ *slash=0;
+ const char *relative[]={"/" BACKEND_NAME,
+ "/../../release/" BACKEND_ABI "/" BACKEND_NAME,
+ "/../release/" BACKEND_ABI "/" BACKEND_NAME};
+ for (unsigned i=0;i<sizeof(relative)/sizeof(*relative);++i) {
+ snprintf(path,sizeof(path),"%s%s",executable,relative[i]);
+ void *handle=dlopen(path,RTLD_NOW | RTLD_LOCAL);
+ if (handle) return handle;
+ }
+ }
+ }
+ const char *relative[]={"./" BACKEND_NAME,
+ "./build/release/" BACKEND_ABI "/" BACKEND_NAME,
+ "./chapter31/build/release/" BACKEND_ABI "/" BACKEND_NAME};
+ for (unsigned i=0;i<sizeof(relative)/sizeof(*relative);++i) {
+ void *handle=dlopen(relative[i],RTLD_NOW | RTLD_LOCAL);
+ if (handle) return handle;
+ }
+ return NULL;
+}
// No MiniFB headers or link dependency in the default runtime. Cygwin's
// dlopen also loads native Windows DLLs; both use the x64 Windows C ABI.
@@ -17,23 +57,24 @@ static int load_graphics(void) {
return -1;
}
if (api) return 0;
- if (!path || !*path) {
- fprintf(stderr, "Simple graphics: MiniFB backend not configured. "
- "Run make graphics, then set SIMPLE_MINIFB to the backend library's full path.\n");
- return -1;
- }
- library = dlopen(path, RTLD_NOW | RTLD_LOCAL);
+ int explicit_path=path && *path;
+ library = explicit_path ? dlopen(path, RTLD_NOW | RTLD_LOCAL) : find_backend();
if (!library) {
- fprintf(stderr, "Simple graphics: cannot load MiniFB backend '%s': %s\n", path, dlerror());
+ if (explicit_path)
+ fprintf(stderr, "Simple graphics: cannot load MiniFB backend '%s': %s\n", path, dlerror());
+ else
+ fprintf(stderr, "Simple graphics: MiniFB backend not found beside the executable or in build/release/"
+ BACKEND_ABI ". Run make graphics, or set SIMPLE_MINIFB to its full path.\n");
return -1;
}
+ if (!explicit_path) path="automatically discovered backend";
const struct simple_graphics_api *(*get_api)(void) =
(const struct simple_graphics_api *(*)(void))dlsym(library, "simple_minifb_api");
const struct simple_graphics_api *candidate = get_api ? get_api() : NULL;
if (!candidate || candidate->version != SIMPLE_GRAPHICS_VERSION ||
!candidate->open || !candidate->present || !candidate->poll ||
!candidate->mouse_x || !candidate->mouse_y || !candidate->button ||
- !candidate->key || !candidate->close) {
+ !candidate->key || !candidate->close || !candidate->wait) {
fprintf(stderr, "Simple graphics: incompatible MiniFB backend '%s'. Rebuild with make graphics.\n", path);
dlclose(library);
library = NULL;
@@ -58,3 +99,4 @@ int32_t simple_gfx_mouse_y(void) { return api ? api->mouse_y() : 0; }
int32_t simple_gfx_button(int32_t button) { return api ? api->button(button) : 0; }
int32_t simple_gfx_key(int32_t key) { return api ? api->key(key) : 0; }
int32_t simple_gfx_close(void) { if (api) api->close(); return 0; }
+int32_t simple_gfx_wait(int64_t timeout) { return api ? api->wait(timeout) : -1; }
diff --git a/chapter31/src/main/c/simple_graphics.h b/chapter31/src/main/c/simple_graphics.h
index 35a0aa08..370256be 100644
--- a/chapter31/src/main/c/simple_graphics.h
+++ b/chapter31/src/main/c/simple_graphics.h
@@ -14,6 +14,7 @@ struct simple_graphics_api {
int32_t (*button)(int32_t);
int32_t (*key)(int32_t);
void (*close)(void);
+ int32_t (*wait)(int64_t);
};
-#define SIMPLE_GRAPHICS_VERSION 1
+#define SIMPLE_GRAPHICS_VERSION 2
#endif
diff --git a/chapter31/src/main/c/simple_minifb.c b/chapter31/src/main/c/simple_minifb.c
index 34663e2e..2117f0d2 100644
--- a/chapter31/src/main/c/simple_minifb.c
+++ b/chapter31/src/main/c/simple_minifb.c
@@ -3,11 +3,15 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <limits.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <errno.h>
#include <time.h>
+#include <poll.h>
+#include "WindowData.h"
+#include "x11/WindowData_X11.h"
#endif
// One fixed-size window for now. No Simple pointer survives a native call:
@@ -73,27 +77,27 @@ static int32_t present(const uint32_t *pixels, uint32_t count) {
return status(mfb_update_ex(window, frame, width, height));
}
-static int32_t poll_events(int32_t wait) {
+static int32_t wait_events(int64_t timeout) {
if (!window) return 1;
- if (wait) {
- // mfb_wait_sync returns false for both destruction and errors that
- // leave the window alive. Use a short OS wait, then the status-bearing
- // event API, so we know whether the window still needs destruction.
+ if (timeout < -1) return -1;
+ // May return early; the game recomputes its deadlines after every wakeup.
+ int ms=timeout>INT_MAX ? INT_MAX : (int)timeout;
#ifdef _WIN32
- Sleep(17);
+ if (MsgWaitForMultipleObjectsEx(0,NULL,ms<0 ? INFINITE : (DWORD)ms,
+ QS_ALLINPUT,MWMO_INPUTAVAILABLE)==WAIT_FAILED) return -1;
#else
- struct timespec delay = {0, 16666667};
- while (nanosleep(&delay, &delay) < 0) {
- if (errno != EINTR) {
- perror("Simple graphics: event wait");
- close_window();
- return -1;
- }
- }
-#endif
+ // The pinned MiniFB X11 backend owns this connection. Check already queued
+ // Xlib events before waiting on its socket; no new event thread is needed.
+ SWindowData *data=(SWindowData *)window;
+ SWindowData_X11 *native=(SWindowData_X11 *)data->specific;
+ if (!XPending(native->display)) {
+ struct pollfd fd={ConnectionNumber(native->display),POLLIN,0};
+ if (poll(&fd,1,ms)<0 && errno!=EINTR) return -1;
}
+#endif
return status(mfb_update_events(window));
}
+static int32_t poll_events(int32_t wait) { return wait_events(wait ? 17 : 0); }
static int32_t mouse_x(void) { return window ? mfb_get_mouse_x(window) : 0; }
static int32_t mouse_y(void) { return window ? mfb_get_mouse_y(window) : 0; }
static int32_t button(int32_t b) {
@@ -105,7 +109,7 @@ static int32_t key(int32_t k) {
static const struct simple_graphics_api api = {
SIMPLE_GRAPHICS_VERSION, open_window, present, poll_events,
- mouse_x, mouse_y, button, key, close_window
+ mouse_x, mouse_y, button, key, close_window, wait_events
};
#ifdef _WIN32
__declspec(dllexport)
diff --git a/chapter31/src/main/c/simple_time.c b/chapter31/src/main/c/simple_time.c
new file mode 100644
index 00000000..2e579922
--- /dev/null
+++ b/chapter31/src/main/c/simple_time.c
@@ -0,0 +1,61 @@
+#define _POSIX_C_SOURCE 200809L
+#include <time.h>
+#include <stdint.h>
+#include <errno.h>
+
+// Fixed-width bridge; no native struct layout or retained Simple pointers.
+static int64_t clock_value(clockid_t clock, int64_t scale) {
+ struct timespec t;
+ if (clock_gettime(clock,&t)) return -1;
+ if (t.tv_sec>INT64_MAX/scale || t.tv_sec<INT64_MIN/scale) {
+ errno=EOVERFLOW; return -1;
+ }
+ int64_t base=(int64_t)t.tv_sec*scale;
+ int64_t fraction=t.tv_nsec/(1000000000/scale);
+ if (base>INT64_MAX-fraction) { errno=EOVERFLOW; return -1; }
+ return base+fraction;
+}
+int64_t simple_time_now(void) { return clock_value(CLOCK_REALTIME,1000); }
+int64_t simple_time_mono(void) { return clock_value(CLOCK_MONOTONIC,1000); }
+int64_t simple_time_nano(void) { return clock_value(CLOCK_MONOTONIC,1000000000); }
+int32_t simple_time_sleep(int64_t ms, int32_t absolute) {
+ if (ms<0) { errno=EINVAL; return -1; }
+ struct timespec t={ms/1000,(ms%1000)*1000000};
+ int result;
+ if (!absolute) {
+ // Retry the remaining duration after signals; no busy waiting.
+ do { result=nanosleep(&t,&t); } while (result && errno==EINTR);
+ return result;
+ }
+ do { result=clock_nanosleep(CLOCK_MONOTONIC,TIMER_ABSTIME,&t,NULL); } while (result==EINTR);
+ if (result) { errno=result; return -1; }
+ return 0;
+}
+static int leap(int y) { return y%4==0 && (y%100!=0 || y%400==0); }
+static int local_fields(int64_t ms, struct tm *local, struct tm *utc) {
+ tzset();
+ int64_t sec=ms/1000;
+ if (ms%1000<0) --sec;
+ time_t t=(time_t)sec;
+ if ((int64_t)t!=sec || !localtime_r(&t,local) || !gmtime_r(&t,utc)) return -1;
+ if (local->tm_year < -1899 || local->tm_year > 8099) { errno=EOVERFLOW; return -1; }
+ return 0;
+}
+int32_t simple_time_local(int64_t ms, int64_t *out) {
+ struct tm t,u;
+ if (local_fields(ms,&t,&u)) return -1;
+ int days=t.tm_yday-u.tm_yday;
+ if (t.tm_year>u.tm_year) days+=365+leap(u.tm_year+1900);
+ if (t.tm_year<u.tm_year) days-=365+leap(t.tm_year+1900);
+ int offset=days*86400+(t.tm_hour-u.tm_hour)*3600+(t.tm_min-u.tm_min)*60+t.tm_sec-u.tm_sec;
+ out[0]=t.tm_year+1900; out[1]=t.tm_mon+1; out[2]=t.tm_mday;
+ out[3]=t.tm_hour; out[4]=t.tm_min; out[5]=t.tm_sec;
+ out[6]=(ms%1000+1000)%1000; out[7]=t.tm_wday;
+ out[8]=offset; out[9]=t.tm_isdst>0;
+ return 0;
+}
+int32_t simple_time_zone(int64_t ms, char *out, uint32_t capacity) {
+ struct tm t,u;
+ if (!capacity || local_fields(ms,&t,&u)) return -1;
+ return (int32_t)strftime(out,capacity,"%Z",&t);
+}
diff --git a/chapter31/src/main/java/com/seaofnodes/simple/codegen/Encoding.java b/chapter31/src/main/java/com/seaofnodes/simple/codegen/Encoding.java
index b07a8fec..eb62c36a 100644
--- a/chapter31/src/main/java/com/seaofnodes/simple/codegen/Encoding.java
+++ b/chapter31/src/main/java/com/seaofnodes/simple/codegen/Encoding.java
@@ -39,7 +39,9 @@ public class Encoding implements com.seaofnodes.isa.CodeSink {
public final BAOS _bits = new BAOS(); // Instructions / encodings
public final BAOS _cpool = new BAOS(); // Constant r/o pool
public final BAOS _sdata = new BAOS(); // Static r/w pool
- public final HashMap<Node,Relo> _bigCons = new HashMap<>();
+ // Relocations belong to instruction occurrences, including equal constant
+ // loads cloned by scheduling/register allocation. Structural keys lose sites.
+ public final IdentityHashMap<Node,Relo> _bigCons = new IdentityHashMap<>();
public final StaticData _data;
public final HashMap<String,Integer> _externalAddresses = new HashMap<>();
@@ -157,7 +159,7 @@ public class Encoding implements com.seaofnodes.isa.CodeSink {
// Nodes need "relocation" patching; things done after code is placed.
// Record src and dst Nodes.
- private final HashMap<Node,CFGNode> _internals = new HashMap<>();
+ private final IdentityHashMap<Node,CFGNode> _internals = new IdentityHashMap<>();
// Source is a Call, destination in the Fun.
public Encoding relo( CallNode call ) {
FunNode fun = _code.link(call.tfp());
@@ -187,13 +189,13 @@ public class Encoding implements com.seaofnodes.isa.CodeSink {
}
// External references only located by Strings a link-time
- public final HashMap<Node,String> _externals = new HashMap<>();
+ public final IdentityHashMap<Node,String> _externals = new IdentityHashMap<>();
public Encoding external( Node n, String extern ) {
_externals.put(n,extern);
return this;
}
- public final HashMap<Node,String> _externData = new HashMap<>();
+ public final IdentityHashMap<Node,String> _externData = new IdentityHashMap<>();
public void externalData(Node n, String name) { _externData.put(n,name); }
// Store t as a 32/64 bit constant in the code space; generate RIP-relative
diff --git a/chapter31/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java b/chapter31/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java
index aa3f6d57..19420839 100644
--- a/chapter31/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java
+++ b/chapter31/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java
@@ -20,8 +20,20 @@ public class MemMergeNode extends Node {
public MemMergeNode( Node ...nodes) { super(nodes); _type = TypeMem.BOT; }
public MemMergeNode(MemMergeNode mem) { super(mem); }
@Override public Tag serialTag() { return Tag.MemMerge; }
- public void packed( BAOS baos, HashMap<String,Integer> strs, HashMap<Type,Integer> types, IdentityHashMap<Node, Integer> anodes ) { baos.packed1(nIns()); }
- static Node make( BAOS bais ) { Node mem = new MemMergeNode(); mem.setDefX(bais.packed1()-1,null); return mem; }
+ public void packed( BAOS baos, HashMap<String,Integer> strs, HashMap<Type,Integer> types, IdentityHashMap<Node, Integer> anodes ) {
+ // A memory merge always has inputs. Zero escapes a wide alias table,
+ // preserving every existing nonzero single-byte count.
+ assert nIns()>0;
+ baos.packed1(nIns()<=255 ? nIns() : 0);
+ if( nIns()>255 ) baos.packed4(nIns());
+ }
+ static Node make( BAOS bais ) {
+ int count=bais.packed1();
+ if( count==0 ) count=bais.packed4();
+ Node mem=new MemMergeNode();
+ mem.setDefX(count-1,null);
+ return mem;
+ }
@Override public String label() { return "ALLMEM"; }
@Override public boolean isMem() { return true; }
diff --git a/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java b/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java
index 10cee059..ae725659 100644
--- a/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java
+++ b/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java
@@ -9,6 +9,7 @@ import com.seaofnodes.simple.util.Utils;
// Jump on flags, uses flags
public class JmpX86 extends IfNode implements MachNode, RIPRelSize {
String _bop;
+ boolean _unsigned, _floating, _unorderedTaken;
JmpX86( IfNode iff, String bop ) {
super(iff);
_bop = bop;
@@ -23,29 +24,47 @@ public class JmpX86 extends IfNode implements MachNode, RIPRelSize {
if( set instanceof SetX86 setx && (cmp instanceof CmpX86 || cmp instanceof CmpIX86 || cmp instanceof CmpMemX86 || cmp instanceof CmpFX86) ) {
_inputs.set( 1, cmp );
_bop = setx._bop;
+ _unsigned = setx._unsigned;
+ _floating = cmp instanceof CmpFX86;
+ _unorderedTaken = _bop.equals("!=");
} else
throw Utils.TODO();
}
@Override public RegMask regmap(int i) { assert i==1; return x86_64_v2.FLAGS_MASK; }
@Override public RegMask outregmap() { return null; }
- @Override public void negate() { _bop = negate(_bop); }
+ @Override public void negate() {
+ _bop = negate(_bop);
+ if( _floating ) _unorderedTaken = !_unorderedTaken;
+ }
+
+ private int condition() { return x86_64_v2.jumpop(_bop,_unsigned)&15; }
@Override public void encoding( Encoding enc ) {
if( in(1)==null && _bop=="!=" ) return;
- X86.branch(enc,in(1)==null ? -1 : x86_64_v2.jumpop(_bop)&15);
+ // UCOMISD sets CF/ZF/PF, not signed-integer SF/OF. Handle unordered
+ // separately, including when layout has negated the original test.
+ if( _floating ) {
+ if( _unorderedTaken ) enc.add1(0x0F).add1(0x8A).add4(0); // JP target
+ else enc.add1(0x7A).add1(0); // JP past the ordered jump
+ }
+ X86.branch(enc,in(1)==null ? -1 : condition());
enc.jump(this,cproj(0));
}
// Delta is from opcode start, but X86 measures from the end of the 2-byte encoding
@Override public byte encSize(int delta) {
if( in(1)==null && _bop=="!=" ) return 0;
- return X86.branchSize(delta,in(1)!=null);
+ int prefix = !_floating ? 0 : _unorderedTaken ? 6 : 2;
+ return (byte)(prefix + X86.branchSize(delta-prefix,in(1)!=null));
}
// Delta is from opcode start
@Override public void patch( Encoding enc, int opStart, int opLen, int delta ) {
assert !(in(1)==null && _bop=="!=");
- X86.patchBranch(enc.bits(),opStart,opLen,delta,in(1)==null ? -1 : x86_64_v2.jumpop(_bop)&15);
+ int prefix = !_floating ? 0 : _unorderedTaken ? 6 : 2;
+ if( _floating )
+ X86.patchBranch(enc.bits(),opStart,prefix,_unorderedTaken ? delta : opLen,10);
+ X86.patchBranch(enc.bits(),opStart+prefix,opLen-prefix,delta-prefix,in(1)==null ? -1 : condition());
}
@Override public void asm(CodeGen code, SB sb) {
diff --git a/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java b/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java
index 86610f6d..4494985d 100644
--- a/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java
+++ b/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java
@@ -9,7 +9,7 @@ public class NegX86 extends MachConcreteNode implements MachNode {
NegX86(MinusNode not) { super(not); }
@Override public String op() { return "neg"; }
@Override public RegMask regmap(int i) { return x86_64_v2.RMASK; }
- @Override public RegMask outregmap() { return x86_64_v2.RMASK; }
+ @Override public RegMask outregmap() { return x86_64_v2.WMASK; }
@Override public RegMask killmap() { return x86_64_v2.FLAGS_MASK; }
@Override public int twoAddress() { return 1; }
diff --git a/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java b/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java
index 1e4e7c26..1291cf78 100644
--- a/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java
+++ b/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java
@@ -9,7 +9,7 @@ public class NotX86 extends MachConcreteNode implements MachNode {
NotX86(NotNode not) { super(not); }
@Override public String op() { return "not"; }
@Override public RegMask regmap(int i) { return x86_64_v2.RMASK; }
- @Override public RegMask outregmap() { return x86_64_v2.RMASK; }
+ @Override public RegMask outregmap() { return x86_64_v2.WMASK; }
@Override public RegMask killmap() { return x86_64_v2.FLAGS_MASK; }
@Override public void encoding( Encoding enc ) {
diff --git a/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java b/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java
index 864c1b55..bcc4880b 100644
--- a/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java
+++ b/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java
@@ -4,7 +4,6 @@ import com.seaofnodes.isa.X86;
import com.seaofnodes.simple.codegen.*;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.util.SB;
-import com.seaofnodes.simple.util.Utils;
// Corresponds to the x86 instruction "sete && setne".
// Use result of comparison without jump.
@@ -25,15 +24,14 @@ public class SetX86 extends MachConcreteNode implements MachNode {
@Override public RegMask outregmap() { return x86_64_v2.WMASK; }
@Override public void encoding( Encoding enc ) {
- int op=switch (_bop) {
- case "==" -> 0x94; // SETE
- case "<" -> _unsigned ? 0x92 : 0x9C; // SETB /SETL
- case "<=" -> _unsigned ? 0x96 : 0x9E; // SETBE/SETLE
- case ">=" -> _unsigned ? 0x93 : 0x9D; // SETAE/SETGE
- case ">" -> _unsigned ? 0x97 : 0x9F; // SETA /SETG
- default -> throw Utils.TODO();
- };
- X86.set(enc,op&15,enc.reg(this));
+ int dst=enc.reg(this);
+ if( in(1) instanceof CmpFX86 ) {
+ // MOV preserves comparison flags; unordered is true only for !=.
+ X86.unary(enc,0xC7,0,dst);
+ enc.add4(_bop.equals("!=") ? 1 : 0);
+ enc.add1(0x7A).add1(dst>=4 ? 8 : 6); // JP past SETcc/MOVZX
+ }
+ X86.set(enc,x86_64_v2.jumpop(_bop,_unsigned)&15,dst);
}
@Override public void asm(CodeGen code, SB sb) {
diff --git a/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java b/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java
index 673339a9..aed8ee4c 100644
--- a/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java
+++ b/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java
@@ -87,6 +87,18 @@ public class x86_64_v2 extends Machine {
default -> throw new IllegalArgumentException("Too many arguments");
};
}
+ static int jumpop(String op, boolean unsigned) {
+ if( !unsigned ) return jumpop(op);
+ return switch( op ) {
+ case "==" -> 0x84;
+ case "!=" -> 0x85;
+ case "<" -> 0x82;
+ case "<=" -> 0x86;
+ case ">" -> 0x87;
+ case ">=" -> 0x83;
+ default -> throw new IllegalArgumentException(op);
+ };
+ }
// Limit of float args passed in registers
static RegMask[] XMMS8 = new RegMask[]{
diff --git a/chapter31/src/main/java/com/seaofnodes/simple/parser/Lexer.java b/chapter31/src/main/java/com/seaofnodes/simple/parser/Lexer.java
index 9ab678ca..62dd054e 100644
--- a/chapter31/src/main/java/com/seaofnodes/simple/parser/Lexer.java
+++ b/chapter31/src/main/java/com/seaofnodes/simple/parser/Lexer.java
@@ -405,14 +405,19 @@ public class Lexer {
precision = formatNumber(site);
}
char kind=nextChar();
- if( "diuoxXfFeEgGaAcsb".indexOf(kind)<0 )
+ if( "diuoxXfFeEgGaAcsbt".indexOf(kind)<0 )
throw Parser.error("Expected a string format: %(expression), %d(expression), or %%",site);
boolean number="diuoxXfFeEgGaA".indexOf(kind)>=0;
if( !number && (flags&~1)!=0 || "uoxX".indexOf(kind)>=0 && (flags&6)!=0 ||
"diu".indexOf(kind)>=0 && (flags&8)!=0 )
throw Parser.error("Invalid flags for %"+kind,site);
- if( precision>=0 && (kind=='c' || kind=='b') )
+ if( precision>=0 && (kind=='c' || kind=='b' || kind=='t') )
throw Parser.error("Precision is not supported for %"+kind,site);
+ if( kind=='t' && peek()!='(' ) {
+ int style="FrRMDT".indexOf(nextChar());
+ if( style<0 ) throw Parser.error("Expected a time format: %t, %tF, %tr, %tR, %tM, %tD, or %tT",site);
+ flags |= style<<8;
+ }
if( peek()!='(' ) throw Parser.error("Expected '(' after string format %"+kind,site);
inc();
return new Format(kind,flags,width,precision,site);
diff --git a/chapter31/src/main/java/com/seaofnodes/simple/parser/Strings.java b/chapter31/src/main/java/com/seaofnodes/simple/parser/Strings.java
index 5bf76585..be913371 100644
--- a/chapter31/src/main/java/com/seaofnodes/simple/parser/Strings.java
+++ b/chapter31/src/main/java/com/seaofnodes/simple/parser/Strings.java
@@ -32,7 +32,7 @@ final class Strings {
Node strings=P.references.member(text,"string",fmt.site()).keep();
fptr=new FormatNode(value,ints.unkeep(),floats.unkeep(),strings.unkeep(),fmt.site()).peephole();
} else {
- String family="diuoxXcb".indexOf(fmt.kind())>=0 ? "integer" : fmt.kind()=='s' ? "string" : "floating";
+ String family="diuoxXcb".indexOf(fmt.kind())>=0 ? "integer" : fmt.kind()=='s' ? "string" : fmt.kind()=='t' ? "time" : "floating";
fptr=P.references.member(text,family,fmt.site());
}
call(text,fptr,fmt.site(),builder,value.unkeep(),P.con(fmt.kind() | fmt.flags()<<8),
diff --git a/chapter31/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java b/chapter31/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java
index e8218afd..bdd887e6 100644
--- a/chapter31/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java
+++ b/chapter31/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java
@@ -10,7 +10,7 @@ import com.seaofnodes.simple.type.*;
import java.util.ArrayList;
import com.seaofnodes.simple.codegen.Encoding;
import com.seaofnodes.simple.util.BAOS;
-import java.util.HashMap;
+import java.util.Map;
/** Machine-specific facts; traversal, columns and byte display live in print/. */
public final class ASMPrinter extends AssemblyAdapter<Node> {
@@ -93,7 +93,7 @@ public final class ASMPrinter extends AssemblyAdapter<Node> {
return data;
}
// Retain this diagnostic entry point for checking that pool printing never lays out types.
- static int printConstantPool(int address, SB sb, BAOS bits, HashMap<Node,Encoding.Relo> relos,
+ static int printConstantPool(int address, SB sb, BAOS bits, Map<Node,Encoding.Relo> relos,
boolean ro, String name) {
sb.p(com.seaofnodes.print.ASMPrinter.pool(new Pool(name,bits.buf(),0,address,entries(relos.values(),ro))));
return address+bits.size();
diff --git a/chapter31/src/main/java/com/seaofnodes/simple/type/TypeMem.java b/chapter31/src/main/java/com/seaofnodes/simple/type/TypeMem.java
index 5faa7107..1c83ddd7 100644
--- a/chapter31/src/main/java/com/seaofnodes/simple/type/TypeMem.java
+++ b/chapter31/src/main/java/com/seaofnodes/simple/type/TypeMem.java
@@ -211,14 +211,19 @@ public class TypeMem extends Type {
+ (!_one ? 0 : (_clz ? 4 : 8)));
if( _alias!=1 ) {
// generic alias
- assert _alias <= 255;
- baos.packed1( _alias );
+ // Zero is reserved for alias-set flags, never a memory slice.
+ // Use it as an escape for large libraries, retaining the existing
+ // single-byte encoding (including 255) for older objects.
+ assert _alias>1;
+ baos.packed1(_alias<=255 ? _alias : 0);
+ if( _alias>255 ) baos.packed4(_alias);
}
XInt.packed(baos,_escFs);
XInt.packed(baos,_escAs);
}
static TypeMem packed( int tag, BAOS bais ) {
int alias = (tag&1)==0 ? 1 : bais.packed1();
+ if( alias==0 ) alias=bais.packed4();
boolean xfinal= (tag&2)==2;
boolean one = (tag&12)!=0;
boolean clz = (tag&4)==4;
diff --git a/chapter31/src/main/smp/sys.smp b/chapter31/src/main/smp/sys.smp
index 40a4b955..296635ae 100644
--- a/chapter31/src/main/smp/sys.smp
+++ b/chapter31/src/main/smp/sys.smp
@@ -11,6 +11,7 @@ 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/chapter31/src/main/smp/sys/draw.smp b/chapter31/src/main/smp/sys/draw.smp
index 0721659a..0e4a27d4 100644
--- a/chapter31/src/main/smp/sys/draw.smp
+++ b/chapter31/src/main/smp/sys/draw.smp
@@ -15,3 +15,4 @@ val rgb = { int r, int g, int b ->
};
val Canvas = Canvas; // Pixel canvas and drawing methods
+val Button = Button; // Pointer state and rendering for a rectangular button
diff --git a/chapter31/src/main/smp/sys/draw/Button.smp b/chapter31/src/main/smp/sys/draw/Button.smp
new file mode 100644
index 00000000..edf01119
--- /dev/null
+++ b/chapter31/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/chapter31/src/main/smp/sys/gfx.smp b/chapter31/src/main/smp/sys/gfx.smp
index 1f0e3ca9..cde2cc0d 100644
--- a/chapter31/src/main/smp/sys/gfx.smp
+++ b/chapter31/src/main/smp/sys/gfx.smp
@@ -15,6 +15,7 @@ int ESCAPE = 256;
{ 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.
@@ -28,7 +29,7 @@ val present = { u32[~] pixels ->
i64 ptr = pixels;
simple_gfx_present(ptr,pixels#);
};
-// wait=true waits about 17ms before polling, avoiding a busy loop.
+// 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(); };
@@ -36,3 +37,6 @@ 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 -> simple_gfx_wait(timeout); };
diff --git a/chapter31/src/main/smp/sys/text.smp b/chapter31/src/main/smp/sys/text.smp
index a414ff45..a28edf6d 100644
--- a/chapter31/src/main/smp/sys/text.smp
+++ b/chapter31/src/main/smp/sys/text.smp
@@ -96,3 +96,8 @@ val f64 = { f64 x -> end(floating(begin(),x, 0 ,0,-1)); };
val char = { i64 x -> end(integer (begin(),x,'c',0,-1)); };
val bool = { bool x -> end(integer (begin(),x,'b',0,-1)); };
val hex = { i64 x -> end(integer (begin(),x,'x',0,-1)); };
+
+// Time selectors occupy spec bits 16..23; padding shares string formatting.
+val time = { !aryu8 b, i64 value, i64 spec, i64 width, i64 precision ->
+ string(b,sys.time.format(value,(spec>>>16)&255),spec,width,-1);
+};
diff --git a/chapter31/src/main/smp/sys/time.smp b/chapter31/src/main/smp/sys/time.smp
new file mode 100644
index 00000000..cdcb5055
--- /dev/null
+++ b/chapter31/src/main/smp/sys/time.smp
@@ -0,0 +1,191 @@
+// 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;
+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 ms -> simple_time_sleep(ms,0); };
+val sleepUntil = { i64 deadline -> simple_time_sleep(deadline,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>LIMIT/unit || n<(-LIMIT-1)/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/1000000; };
+val toNanos = { i64 ms -> _scale(ms,1000000); };
+// 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 _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*146097+y*365+y/4-y/100+(153*mp+2)/5+day-1-719468;
+};
+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+719468, era=z/146097, doe=z-era*146097;
+ i64 y=(doe-doe/1460+doe/36524-doe/146096)/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_noInline = { 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_noInline(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
new file mode 100644
index 00000000..5d117467
--- /dev/null
+++ b/chapter31/src/main/smp/sys/time/DateTime.smp
@@ -0,0 +1,11 @@
+// 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).
+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;
+};
+
+// Factory in the defining unit, so callers need no forward instance type.
+val empty = { -> new DateTime(); };
diff --git a/chapter31/src/main/smp/sys/time/Timers.smp b/chapter31/src/main/smp/sys/time/Timers.smp
new file mode 100644
index 00000000..b98c96ef
--- /dev/null
+++ b/chapter31/src/main/smp/sys/time/Timers.smp
@@ -0,0 +1,55 @@
+// Cooperative timers. poll returns IDs; callbacks never run on another thread.
+struct Timers {
+ i64[] !_ids=new i64[8], !_due=new i64[8], !_period=new i64[8];
+ i64 !_next=1;
+
+ val _add = { i64 current, i64 delay, i64 period ->
+ if( current<0 || delay<0 || current>time.LIMIT-delay || _next==time.LIMIT ) return -1;
+ int slot=0;
+ while( slot<_ids# && _ids[slot]!=0 ) slot++;
+ 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;
+ // Retry against the newly published arrays after growing.
+ return _add(current,delay,period);
+ }
+ i64 id=_next++;
+ _ids[slot]=id; _due[slot]=current+delay; _period[slot]=period;
+ 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;
+ for( int i=0; i<_ids#; i++ ) if( _ids[i]==id ) { _ids[i]=0; return true; }
+ return false;
+ };
+ val nextDeadline = { ->
+ i64 next=-1;
+ for( int i=0; i<_ids#; i++ )
+ if( _ids[i]!=0 && (next<0 || _due[i]<next) ) next=_due[i];
+ return next;
+ };
+ // Earliest deadline first; equal deadlines use registration order.
+ // Repeats skip missed ticks and retain their phase, avoiding bursts/drift.
+ val poll = { i64 current ->
+ 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;
+ else {
+ i64 steps=(current-_due[slot])/period;
+ if( steps>=(time.LIMIT-_due[slot])/period ) _ids[slot]=0;
+ else _due[slot]+=(steps+1)*period;
+ }
+ return id;
+ };
+};
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/chapter31/src/test/java/com/seaofnodes/simple/Chapter21Test.java
index 1a57d4da..14a091ac 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/Chapter21Test.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/Chapter21Test.java
@@ -461,6 +461,7 @@ return i;
String src =
"""
val test_sqrt = { flt x ->
+ if( x==0.0 ) return 0.0; // Avoid 0/0: unordered comparisons must stay false.
flt epsilon = 1e-15;
flt guess = x;
while( 1 ) {
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java
index 7f2b4a77..6b892c65 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java
@@ -477,7 +477,7 @@ public class Chapter25Test {
// Software drawing and the optional native graphics interface.
assertArrayEquals(new String[]{"sys/adt", "sys/ary",
- "sys/aryi64", "sys/aryu8", "sys/audio", "sys/char", "sys/draw", "sys/gfx", "sys/io", "sys/libc", "sys/rt", "sys/scan", "sys/text"},sys_elf._deps);
+ "sys/aryi64", "sys/aryu8", "sys/audio", "sys/char", "sys/draw", "sys/gfx", "sys/io", "sys/libc", "sys/rt", "sys/scan", "sys/text", "sys/time"},sys_elf._deps);
assertArrayEquals(new String[]{"sys"},sys_elf._units);
assertSame("class:sys" ,sys_elf._clz._name);
}
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/DrawTest.java b/chapter31/src/test/java/com/seaofnodes/simple/DrawTest.java
index 6eb261f1..72e2cd9d 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/DrawTest.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/DrawTest.java
@@ -3,9 +3,125 @@ package com.seaofnodes.simple;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.Test;
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.util.Ary;
+import static org.junit.Assert.*;
// Native raster and game checks; no backend or display is needed.
public class DrawTest {
+ @Test public void nativeClockCoordinates() throws Exception {
+ String clock=Files.readString(Path.of("docs/examples/Clock.smp"))
+ .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;
+ if( x<expectedX-1 || x>expectedX+1 || y<expectedY-1 || y>expectedY+1 ) {
+ sys.io.p("turn %f(turn): %(x),%(y) expected %(expectedX),%(expectedY)\\n");
+ return 1;
+ }
+ return 0;
+ };
+ val main={ ->
+ """);
+ for(int i=0;i<=60;i++) {
+ double turn=i/60.0;
+ int x=(int)(360+clockSine(turn)*128), y=(int)(232-clockSine(turn+0.25)*128);
+ source.append("if(_check_noInline(").append(turn).append(',').append(x).append(',').append(y)
+ .append(")!=0) return 1;");
+ }
+ source.append("return 0; };");
+ TestC.runSYS(source.toString(),"clockCoordinates","",-1);
+ }
+
+ private static double clockSine(double turn) {
+ turn-=(long)turn;
+ boolean negative=turn>0.5;
+ double x=(negative ? turn-0.5 : turn)*6.283185307179586;
+ double product=x*(3.141592653589793-x);
+ return (negative ? -16.0 : 16.0)*product/(49.34802200544679-4*product);
+ }
+
+ @Test public void nativeAnalogSweep() throws Exception {
+ String clock=Files.readString(Path.of("docs/examples/Clock.smp"))
+ .replace("val main =","val _windowMain =");
+ 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);
+ var found=false;
+ for( int y=expectedY-2; y<=expectedY+2; y++ )
+ for( int x=expectedX-2; x<=expectedX+2; x++ )
+ if( c.pixels[y*720+x]==0xc3ffcb ) found=true;
+ if( !found ) {
+ sys.io.p("missing second hand %(second).%(millisecond) expected %(expectedX),%(expectedY)\\n");
+ return 1;
+ }
+ // Every dial mark/hand must stay within the dial's bounds.
+ for( int y=0; y<540; y++ ) for( int x=0; x<720; x++ )
+ if( (x<204 || x>516 || y<76 || y>388) && c.pixels[y*720+x]!=0 ) {
+ sys.io.p("stray pixel %(x),%(y) color %x(c.pixels[y*720+x]) at %(second).%(millisecond)\\n");
+ return 2;
+ }
+ return 0;
+ };
+ val main={ ->
+ """);
+ for(int millis:new int[]{0,500,7500,15000,30000,45000,59500}) {
+ double turn=millis/60000.0;
+ int x=(int)(360+clockSine(turn)*128), y=(int)(232-clockSine(turn+0.25)*128);
+ source.append("if(_renderCheck_noInline(").append(millis/1000).append(',').append(millis%1000)
+ .append(',').append(x).append(',').append(y).append(")!=0) return 1;");
+ }
+ source.append("return 0; };");
+ TestC.runSYS(source.toString(),"clockAnalogSweep","",-1);
+ }
+
+ @Test public void buttonInteractionAndRendering() throws Exception {
+ TestC.runSYS("""
+ val main={ ->
+ val b=new sys.draw.Button(10,10,60,30,"GO");
+ if( !b.contains(10,10) || b.contains(70,20) || b.contains(20,40) ) return 1;
+ if( b.update(20,20,true) || !b.pressed || b.update(20,20,true) ) return 2;
+ if( !b.update(20,20,false) || b.pressed || b.update(20,20,false) ) return 3;
+ if( b.update(0,0,true) || b.update(20,20,true) || b.update(20,20,false) ) return 4;
+ b.update(20,20,true);
+ if( b.update(0,0,false) ) return 5;
+ b.update(20,20,true); b.enabled=false;
+ if( b.update(20,20,true) || b.pressed || b.update(20,20,false) ) return 6;
+ b.enabled=true;
+ val c=new sys.draw.Canvas(90,60);
+ b.update(20,20,false); b.draw(c,2,9,3,5);
+ if( c.pixels[10*90+10]!=9 || c.pixels[13*90+13]!=5 || c.pixels[0]!=0 ) return 7;
+ b.update(0,0,false); b.draw(c,2,9,3,5);
+ if( c.pixels[13*90+13]!=3 ) return 8;
+ return 0;
+ };
+ ""","drawButton","",-1);
+ }
+
+ @Test public void clockFacesWithoutWindow() throws Exception {
+ String clock=Files.readString(Path.of("docs/examples/Clock.smp"))
+ .replace("val main =","val _windowMain =");
+ 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);
+ 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);
+ 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);
+ 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);
+ 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;
+ return 0;
+ };
+ return main();
+ """,126L,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(code,0,3000000));
+ }
+
@Test public void rectanglesAndColors() throws Exception {
TestC.runSYS("""
val main = { ->
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/FloatComparisonNativeTest.java b/chapter31/src/test/java/com/seaofnodes/simple/FloatComparisonNativeTest.java
new file mode 100644
index 00000000..c0d8be3a
--- /dev/null
+++ b/chapter31/src/test/java/com/seaofnodes/simple/FloatComparisonNativeTest.java
@@ -0,0 +1,65 @@
+package com.seaofnodes.simple;
+
+import org.junit.Test;
+
+public class FloatComparisonNativeTest extends TestC {
+ @Test public void longFloatingBranches() throws Exception {
+ StringBuilder source=new StringBuilder("val far_noInline={f64 a,f64 b,i64[] out -> if(a<b){");
+ for(int i=0;i<32;i++) source.append("out[").append(i).append("]=").append(i+1).append(';');
+ source.append("""
+ return 1; } return 0; };
+ val main={ ->
+ val out=new i64[32];
+ if( far_noInline(1.0,2.0,out)!=1 || out[31]!=32 ) return 1;
+ out[31]=0;
+ if( far_noInline(2.0,1.0,out)!=0 || out[31]!=0 ) return 2;
+ f64 nan=1e300*1e300-1e300*1e300;
+ if( far_noInline(nan,1.0,out)!=0 || out[31]!=0 ) return 3;
+ if( far_noInline(1.0,nan,out)!=0 || out[31]!=0 ) return 4;
+ return 0;
+ };
+ """);
+ runSYS(source.toString(),"floatLongBranches","",-1);
+ }
+
+ @Test public void branchesAndValues() throws Exception {
+ String[] literals={"-1.0","0.0","2.0","1e300*1e300","-1e300*1e300",
+ "1e300*1e300-1e300*1e300"};
+ double[] values={-1,0,2,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY,Double.NaN};
+ StringBuilder source=new StringBuilder("""
+ val branch_noInline={ f64 a, f64 b ->
+ int bits=0;
+ if( a<b ) bits+=1;
+ if( a<=b ) bits+=2;
+ if( a==b ) bits+=4;
+ if( a!=b ) bits+=8;
+ if( a>=b ) bits+=16;
+ if( a>b ) bits+=32;
+ return bits;
+ };
+ val value_noInline={ f64 a, f64 b ->
+ val lt=a<b, le=a<=b, eq=a==b, ne=a!=b, ge=a>=b, gt=a>b;
+ return lt+le*2+eq*4+ne*8+ge*16+gt*32;
+ };
+ val check_noInline={ f64 a, f64 b, int expected, int tag ->
+ val branches=branch_noInline(a,b), values=value_noInline(a,b);
+ if( branches!=expected || values!=expected ) {
+ sys.io.p("case %(tag): branches %(branches), values %(values), expected %(expected)\\n");
+ return 1;
+ }
+ return 0;
+ };
+ val main={ ->
+ """);
+ for(int i=0;i<values.length;i++) for(int j=0;j<values.length;j++) {
+ double a=values[i],b=values[j];
+ int expected=(a<b ? 1 : 0)|(a<=b ? 2 : 0)|(a==b ? 4 : 0)|
+ (a!=b ? 8 : 0)|(a>=b ? 16 : 0)|(a>b ? 32 : 0);
+ source.append("if(check_noInline(").append(literals[i]).append(',').append(literals[j])
+ .append(',').append(expected).append(',').append(i*values.length+j)
+ .append(")!=0) return 1;");
+ }
+ source.append("return 0; };");
+ runSYS(source.toString(),"floatComparisons","",-1);
+ }
+}
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/GraphicsTest.java b/chapter31/src/test/java/com/seaofnodes/simple/GraphicsTest.java
index 9c432ce4..66ced278 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/GraphicsTest.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/GraphicsTest.java
@@ -10,7 +10,7 @@ import static org.junit.Assert.*;
// These tests never open a window and do not need MiniFB installed.
public class GraphicsTest {
- private static String graphics, text, game;
+ private static String graphics, text, game, clock;
@BeforeClass public static void compilePrograms() throws Exception {
var paths = new Ary<>(new String[]{TestC.RELEASE_SYS_DIR});
@@ -20,10 +20,17 @@ public class GraphicsTest {
"graphics_text",paths,TestC.CALL_CONVENTION,null,null,-1);
game = TestC.compile(Files.readString(Path.of("docs/examples/TicTacToeGraphical.smp")),
"graphics_game",paths,TestC.CALL_CONVENTION,null,null,-1);
+ clock = TestC.compile(Files.readString(Path.of("docs/examples/Clock.smp")),
+ "graphics_clock",paths,TestC.CALL_CONVENTION,null,null,-1);
}
private static String run(String exe, String backend, int exit) throws Exception {
- ProcessBuilder pb = new ProcessBuilder(exe).redirectErrorStream(true);
+ return run(exe,backend,exit,null);
+ }
+
+ private static String run(String exe, String backend, int exit, Path cwd) throws Exception {
+ ProcessBuilder pb = new ProcessBuilder(Path.of(exe).toAbsolutePath().toString()).redirectErrorStream(true);
+ if (cwd!=null) pb.directory(cwd.toFile());
if( backend == null ) pb.environment().remove("SIMPLE_MINIFB");
else pb.environment().put("SIMPLE_MINIFB",backend);
Process child = pb.start();
@@ -36,11 +43,52 @@ public class GraphicsTest {
}
@Test public void missingConfiguration() throws Exception {
- String output = run(graphics,null,1);
- assertTrue(output,output.contains("MiniFB backend not configured"));
+ Path dir=Files.createTempDirectory(Path.of("build/objs"),"graphics-missing-");
+ Path bin=Files.createDirectory(dir.resolve("bin"));
+ Path exe=Files.copy(Path.of(graphics),bin.resolve(Path.of(graphics).getFileName()));
+ String output = run(exe.toString(),null,1,dir);
+ assertTrue(output,output.contains("MiniFB backend not found"));
assertTrue(output,output.contains("make graphics"));
}
+ @Test public void discoverBackendWithoutConfiguration() throws Exception {
+ Path root=Files.createTempDirectory(Path.of("build/objs"),"graphics-discovery-");
+ String abi=TestC.OS.startsWith("Windows") ? "x86_64_v2_win64" : "x86_64_v2_SystemV";
+ String name=TestC.OS.startsWith("Windows") ? "simple_minifb.dll" : "libsimple_minifb.so";
+ Path source=root.resolve("backend.c");
+ Files.writeString(source,"""
+ #include "simple_graphics.h"
+ #include <stdio.h>
+ static int32_t open_window(const char *s,uint32_t w,uint32_t h) {
+ puts("discovered mock MiniFB"); return -1;
+ }
+ static int32_t present(const uint32_t *p,uint32_t n) { return 0; }
+ static int32_t poll_events(int32_t n) { return 0; }
+ static int32_t position(void) { return 0; }
+ static int32_t wait_events(int64_t n) { return 0; }
+ static void close_window(void) {}
+ static const struct simple_graphics_api api={SIMPLE_GRAPHICS_VERSION,
+ open_window,present,poll_events,position,position,poll_events,poll_events,close_window,wait_events};
+ const struct simple_graphics_api *simple_minifb_api(void) { return &api; }
+ """);
+ Path mock=root.resolve(name);
+ TestC.exec(30,"gcc","-shared","-fPIC","-Isrc/main/c",source.toString(),"-o",mock.toString());
+ Path cwd=Files.createDirectory(root.resolve("empty"));
+ Path bin=Files.createDirectory(root.resolve("bin"));
+ Path exe=Files.copy(Path.of(graphics),bin.resolve(Path.of(graphics).getFileName()));
+ Path beside=Files.copy(mock,bin.resolve(name));
+ assertTrue(run(exe.toString(),null,1,cwd).contains("discovered mock MiniFB"));
+ // Explicit bad overrides must not silently fall back to the mock.
+ assertTrue(run(exe.toString(),"no-such-backend",1,cwd).contains("cannot load MiniFB"));
+ Files.delete(beside);
+ Path release=Files.createDirectories(root.resolve("build/release/"+abi));
+ Files.copy(mock,release.resolve(name));
+ Path examples=Files.createDirectories(root.resolve("build/examples/"+abi));
+ Path example=Files.copy(exe,examples.resolve(exe.getFileName()));
+ assertTrue(run(example.toString(),null,1,cwd).contains("discovered mock MiniFB"));
+ assertTrue(run(exe.toString(),null,1,root).contains("discovered mock MiniFB"));
+ }
+
@Test public void disabledGraphics() throws Exception {
assertTrue(run(graphics,"off",1).contains("disabled"));
}
@@ -49,6 +97,10 @@ public class GraphicsTest {
assertTrue(run(game,"off",1).contains("disabled"));
}
+ @Test public void clockWithoutBackend() throws Exception {
+ assertTrue(run(clock,"off",1).contains("disabled"));
+ }
+
@Test public void missingLibrary() throws Exception {
String missing = Path.of("build","no-such-minifb-backend").toAbsolutePath().toString();
String output = run(graphics,missing,1);
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/StreamIOTest.java b/chapter31/src/test/java/com/seaofnodes/simple/StreamIOTest.java
index e47cd929..7b459e75 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/StreamIOTest.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/StreamIOTest.java
@@ -133,6 +133,7 @@ public class StreamIOTest extends TestC {
gcc.add(runtimeObject("simple_args"));
gcc.add(runtimeObject("simple_graphics"));
gcc.add(runtimeObject("simple_audio"));
+ gcc.add(runtimeObject("simple_time"));
if( !OS.startsWith("Windows") ) gcc.add("-ldl");
gcc.addAll(new String[]{"-lm","-o",exe});
exec(TEST_TIMEOUT_SECONDS,gcc.asAry());
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/StringFormatTest.java b/chapter31/src/test/java/com/seaofnodes/simple/StringFormatTest.java
index 53a10092..05e801dc 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/StringFormatTest.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/StringFormatTest.java
@@ -128,6 +128,10 @@ public class StringFormatTest extends TestC {
@Test public void testWrongTypesAndReservedNames() {
rejects("return \"%d(1.5)\";", "must be a");
+ rejects("return \"%t(1.5)\";", "must be a");
+ rejects("return \"%tQ(0)\";", "Expected a time format");
+ rejects("return \"%.2t(0)\";", "Precision is not supported");
+ rejects("return \"%+t(0)\";", "Invalid flags");
rejects("return \"%s(17)\";", "must be a");
rejects("return \"%(null)\";", "Cannot format");
rejects("val f={int int -> int;}; return 0;", "Expected an identifier");
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/TestC.java b/chapter31/src/test/java/com/seaofnodes/simple/TestC.java
index 741ee13b..63bb98da 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/TestC.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/TestC.java
@@ -193,6 +193,7 @@ public abstract class TestC {
// The loader has no MiniFB dependency and opens nothing until gfx.open.
params.add(runtimeObject("simple_graphics"));
params.add(runtimeObject("simple_audio"));
+ params.add(runtimeObject("simple_time"));
if( !OS.startsWith("Windows") ) params.add("-ldl");
// Standalone Simple programs export `simple_main`; the small C
// runtime owns native main and calls the Simple argument converter
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/TimeTest.java b/chapter31/src/test/java/com/seaofnodes/simple/TimeTest.java
new file mode 100644
index 00000000..c3cdc6cf
--- /dev/null
+++ b/chapter31/src/test/java/com/seaofnodes/simple/TimeTest.java
@@ -0,0 +1,176 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.util.Ary;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class TimeTest extends TestC {
+ private static void checks(String body, String name) throws Exception {
+ runSYS("val main={ -> "+body+" return 0; };",name,"",-1);
+ }
+
+ @Test public void builtInFormats() throws Exception {
+ runSYS("""
+ val main={ ->
+ i64 t=sys.time.parse("2026-11-12T00:34:56.789Z");
+ sys.io.p("%t(t)|%tF(t)|%tr(t)|%tR(t)|%tM(83456)|%tD(t)|%tT(t)\\n");
+ sys.io.p("[%30t(0)][%-10tr(sys.time.hours(12))]\\n");
+ sys.io.p("%tr(0)|%tr(sys.time.hours(13))|%tM(-1)|%tM(-9223372036854775807-1)");
+ return 0;
+ };
+ ""","timeFormats",
+ "2026-11-12T00:34:56.789Z|2026-11-12T00:34:56.789Z|12:34am|00:34|01:23.456|12/11/26|00:34:56\n"+
+ "[ 1970-01-01T00:00:00.000Z][12:00pm ]\n"+
+ "12:00am|1:00pm|-00:00.001|-153722867280912:55.808",-1);
+ }
+
+ @Test public void calendarAgainstJava() throws Exception {
+ long[] values={-62135596800000L,-2203977600000L,-2203891200000L,-86400001,-86400000,-1,0,1,
+ 951782400000L,1709164800123L,1794443696789L,253402300799999L};
+ DateTimeFormatter iso=DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneOffset.UTC);
+ StringBuilder src=new StringBuilder("val show_noInline={i64 t -> sys.io.p(\"%t(t)|\"); val d=sys.time.utc(t); sys.io.p(\"%(d.weekday)\\n\"); }; val main={ -> ");
+ StringBuilder expected=new StringBuilder();
+ for (long value:values) {
+ src.append("show_noInline(").append(value).append(");");
+ var dt=Instant.ofEpochMilli(value).atOffset(ZoneOffset.UTC);
+ expected.append(iso.format(dt)).append('|').append(dt.getDayOfWeek().getValue()%7).append('\n');
+ }
+ src.append("return 0; };");
+ runSYS(src.toString(),"timeCalendar",expected.toString(),-1);
+ }
+
+ @Test public void parseAndConstruction() throws Exception {
+ checks("""
+ if( sys.time.parse("1970-01-01T00:00:00Z")!=0 ) return 1;
+ if( sys.time.parse("1970-01-01T01:00:00.123+01:00")!=123 ) return 2;
+ if( sys.time.parse("1969-12-31T16:00:00-08:00")!=0 ) return 3;
+ if( sys.time.parse("1969-12-31T23:59:59.999Z")!=-1 ) return 4;
+ if( sys.time.parse("1969-12-31T23:59:59.999+01:00")!=-3600001 ) return 5;
+ if( sys.time.parse("1970-01-01T00:00:30+00:00:30")!=0 ) return 6;
+ if( sys.time.parse("2000-02-29T00:00:00Z")!=951782400000 ) return 7;
+ if( sys.time.makeUTC(2024,2,29,0,0,0,123)!=1709164800123 ) return 8;
+ if( sys.time.makeUTC(1900,2,29,0,0,0,0)!=-1 || sys.time.makeUTC(2024,2,29,24,0,0,0)!=-1 ) return 9;
+ if( sys.time.utc(sys.time.MIN-1).year!=0 || sys.time.utc(sys.time.MAX+1).year!=0 ) return 10;
+ ""","timeParse");
+ String[] bad={"","now","2026-11-12","2026-11-12T00:00:00","0000-01-01T00:00:00Z",
+ "1900-02-29T00:00:00Z","2026-02-29T00:00:00Z","2026-13-01T00:00:00Z",
+ "2026-11-00T00:00:00Z","2026-11-12T24:00:00Z","2026-11-12T00:60:00Z",
+ "2026-11-12T00:00:60Z","2026-11-12T00:00:00.1Z","2026-11-12T00:00:00.1234Z",
+ "2026-11-12T00:00:00+24:00","2026-11-12T00:00:00+01:60","2026-11-12T00:00:00Zx",
+ "2026-11-12T00:00:00.00xZ","0001-01-01T00:00:00+01:00"};
+ StringBuilder src=new StringBuilder("val main={ -> ");
+ for (int i=0;i<bad.length;++i)
+ src.append("if(sys.time.parse(\"").append(bad[i]).append("\")!=-1) return ").append(i+1).append(';');
+ src.append("return 0; };");
+ runSYS(src.toString(),"timeBadParse","",-1);
+ }
+
+ @Test public void durationArithmetic() throws Exception {
+ checks("""
+ if( sys.time.seconds(2)!=2000 || sys.time.minutes(2)!=120000 || sys.time.hours(2)!=7200000 || sys.time.days(2)!=172800000 ) return 1;
+ if( sys.time.toSeconds(-1999)!=-1 || sys.time.toMinutes(119999)!=1 || sys.time.toHours(7200000)!=2 || sys.time.toDays(172800000)!=2 ) return 2;
+ if( sys.time.seconds(9223372036854775)!=9223372036854775000 || sys.time.seconds(-9223372036854775)!=-9223372036854775000 ) return 3;
+ if( sys.time.seconds(9223372036854776)!=-1 || sys.time.seconds(-9223372036854776)!=-1 ) return 4;
+ if( sys.time.remaining(5,10)!=0 || sys.time.remaining(10,5)!=5 || sys.time.remaining(9223372036854775807,-1)!=9223372036854775807 ) return 5;
+ if( sys.time.fromNanos(-1999999)!=-1 || sys.time.toNanos(1234)!=1234000000 || sys.time.toNanos(9223372036855)!=-1 ) return 6;
+ ""","timeArithmetic");
+ }
+
+ @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(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;
+ ""","timeTimers");
+ }
+
+ private static String run(String source, String name, String zone) throws Exception {
+ String exe=compile(source,name,new Ary<>(new String[]{RELEASE_SYS_DIR}),CALL_CONVENTION,null,null,-1);
+ ProcessBuilder pb=new ProcessBuilder(exe).redirectErrorStream(true);
+ if (zone!=null) pb.environment().put("TZ",zone);
+ Process child=pb.start();
+ try {
+ assertTrue("time program timed out",child.waitFor(10,TimeUnit.SECONDS));
+ String out=new String(child.getInputStream().readAllBytes(),StandardCharsets.UTF_8);
+ assertEquals(out,0,child.exitValue());
+ return out;
+ } finally { if (child.isAlive()) child.destroyForcibly(); }
+ }
+
+ @Test public void localZoneAndDaylightSaving() throws Exception {
+ String out=run("""
+ val show_noInline={u8[~] s ->
+ i64 t=sys.time.parse(s); val d=sys.time.local(t);
+ sys.io.p("%(sys.time.formatLocal(t,sys.time.ISO))|%(d.offsetSeconds)|%b(d.daylight)|%(sys.time.zoneName(t))\\n");
+ };
+ val main={ -> show_noInline("2026-01-01T00:00:00Z"); show_noInline("2026-07-01T00:00:00Z"); return 0; };
+ ""","timeZone","EST5EDT,M3.2.0/2,M11.1.0/2");
+ assertEquals("2025-12-31T19:00:00.000-05:00|-18000|false|EST\n2026-06-30T20:00:00.000-04:00|-14400|true|EDT\n",out);
+ }
+
+ @Test public void liveClocksAndWaits() throws Exception {
+ String out=run("""
+ val main={ ->
+ i64 start=sys.time.monotonic();
+ i64 ns=sys.time.nanoTime();
+ if( start<0 || ns/1000000<start || ns/1000000>sys.time.monotonic() ) return 1;
+ if( sys.time.sleep(10)!=0 || sys.time.monotonic()-start<10 ) return 2;
+ i64 deadline=sys.time.monotonic()+10;
+ if( sys.time.sleepUntil(deadline)!=0 || sys.time.monotonic()<deadline ) return 3;
+ if( sys.time.sleepUntil(0)!=0 || sys.time.sleep(-1)!=-1 || sys.time.sleepUntil(-1)!=-1 ) return 4;
+ i64 wall=sys.time.now();
+ if( wall<1700000000000 || wall>4102444800000 ) return 5;
+ return 0;
+ };
+ ""","timeClocks",null);
+ assertEquals("",out);
+ }
+
+ @Test public void pureCalendarInEvaluator() {
+ CodeGen code=new CodeGen("src/main/smp",null,null,"TimeEval","""
+ val s="%t(-1)|%tM(1234)";
+ val expected="1969-12-31T23:59:59.999Z|00:01.234";
+ if( s#!=expected# ) return 1;
+ for( int i=0; i<s#; i++ ) if( s[i]!=expected[i] ) return 2;
+ return 0;
+ """,123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(code,0,100000));
+ }
+
+ @Test public void wideLibrarySerialization() throws Exception {
+ // A growing stdlib crossed the historical byte-sized alias/count limit.
+ // Exercise a separately compiled library whose live memory spans it.
+ StringBuilder source=new StringBuilder("struct Wide { ");
+ for (int i=0;i<300;++i) source.append("int !f").append(i).append("=0;");
+ source.append("}; val make_noInline={int x -> val v=new Wide();");
+ for (int i : new int[]{0,255,299}) source.append("v.f").append(i).append("=x+").append(i).append(';');
+ source.append("return v; };");
+ Path library=Files.createTempDirectory(Path.of("build/objs"),"wide-memory-");
+ new CodeGen(null,library.toString(),null,"WideMemory",source.toString(),123,true,TypeInteger.BOT)
+ .driver(CPU_PORT,CALL_CONVENTION,false,false);
+ run("val main={ -> val v=WideMemory.make_noInline(7); return v.f0!=7 || v.f255!=262 || v.f299!=306; };",
+ "timeWideMemory",new Ary<>(new String[]{library.toString(),RELEASE_SYS_DIR}),
+ CALL_CONVENTION,null,null,"",-1);
+ }
+}
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java b/chapter31/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java
index b255bcf7..b0f0f225 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java
@@ -61,7 +61,7 @@ public class PrintRegTestSupport {
visit.put(marker,t);
var method = com.seaofnodes.simple.print.ASMPrinter.class.getDeclaredMethod(
"printConstantPool",int.class,com.seaofnodes.simple.util.SB.class,
- com.seaofnodes.simple.util.BAOS.class,java.util.HashMap.class,boolean.class,String.class);
+ com.seaofnodes.simple.util.BAOS.class,java.util.Map.class,boolean.class,String.class);
method.setAccessible(true);
try {
var sb = new com.seaofnodes.simple.util.SB();