diff options
| author | Cliff Click <cliffc@acm.org> | 2026-11-12 23:28:58 -0800 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-11-12 23:28:58 -0800 |
| commit | b7440c1e4c182ad977996d91bfb8125091ffae92 (patch) | |
| tree | 8f530f554d1b1e1c1eba02f361286e3b6b575849 | |
| parent | 40d2d3f458846494c6e4bbf284fe5a61bb4bb417 (diff) | |
| download | Simple-b7440c1e4c182ad977996d91bfb8125091ffae92.tar.gz Simple-b7440c1e4c182ad977996d91bfb8125091ffae92.zip | |
ch31 early push needs review
45 files changed, 1268 insertions, 158 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(); diff --git a/docs/roadmap.md b/docs/roadmap.md index cc4dae71..98f319db 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -32,7 +32,6 @@ design decisions and chapter boundaries. - [Partial-initialization contracts](#inferred-partial-initialization-contracts--deferred) — inferred requirements, nested guarantees, and imported contracts. - [Automatic memory management](#automatic-memory-management) — ownership, sharing, cycles, and native retention. - [A graphical game](#a-small-graphical-game) — exercise graphics, keyboard, and audio APIs. - - [Time and animation](#time-timers-and-animation) — scoped follow-on chapter with clocks, calendar values, cooperative timers, and responsive waits. - [Trailing arrays](#structs-with-trailing-arrays) — inline elements after named fields. - [Array initialization](#array-initialization) — generate each element during allocation. - [String libraries](#string-libraries) — ownership, Unicode, and additional representations beyond byte buffers. @@ -379,89 +378,8 @@ Optional [background audio](../chapter31/docs/audio.md) uses miniaudio for soft move sounds and ascending/descending win/loss cues. Further games remain future work; MiniFB supplies only the window and input boundary. -### Time timers and animation - -Scoped on 2026-11-12 as a proposed Chapter 32 following Chapter 31's drawing, -graphics, and audio work. Save this scope while closing Chapter 31; the time -interface and animation work are not implemented yet. This is primarily a -standard-library/runtime chapter, with no anticipated compiler changes or new -external dependencies. Keep the game loop and timer dispatch single-threaded. - -Default instants are bare `i64` milliseconds since Unix epoch, UTC. Durations -are also bare `i64` milliseconds. Distinguish wall-clock readings from monotonic -readings by operation names and documentation: animation and timer deadlines -must not depend on system-clock corrections. - -Proposed `sys.time` core: - -| Operation | Meaning | -| --- | --- | -| `now()` | UTC Unix-epoch milliseconds. | -| `monotonic()` | Milliseconds from an unspecified monotonic origin, for deadlines and elapsed time. | -| `nanoTime()` | Monotonic nanoseconds; units do not promise nanosecond clock resolution. | -| `sleep(ms)` | Suspend the calling thread for a duration. | -| `sleepUntil(deadline)` | Wait until a monotonic millisecond deadline. | -| `seconds(n)`, `minutes(n)`, `hours(n)`, `days(n)` | Convert durations to milliseconds; a day here is exactly 24 hours. | -| `toSeconds(ms)`, `toMinutes(ms)`, etc. | Convert back with explicitly documented rounding. | - -Cygwin's existing runtime headers expose `clock_gettime`, `clock_nanosleep`, -and `localtime_r`; investigate sharing the native clock/calendar bridge with -Linux. Native Windows interval measurement can use QueryPerformanceCounter. -Epoch nanoseconds are optional: signed `i64` would cover only about 292 years -on either side of 1970, so retain milliseconds as the default representation. -Sleep deadlines are approximate; handle interruptions and avoid busy waiting. - -Initial calendar/text support: - -- Format UTC as `2026-11-12T15:04:05.123Z`; parse that form and explicit numeric - UTC offsets such as `-08:00`. -- Break instants into UTC or system-local calendar fields. An auxiliary - `DateTime` structure can contain year, month, day, hour, minute, second, - millisecond, weekday, UTC offset, daylight-saving status, and zone abbreviation. - The underlying instant remains a bare `i64`. -- Construct instants from UTC fields or calendar fields with explicit offsets. -- Query the local UTC offset, DST status, and zone abbreviation at a supplied - instant, using the operating system's existing time-zone machinery. -- Defer arbitrary named-zone conversion, bundled time-zone databases, - natural-language parsing, and locale-specific formatting. Parsing local - calendar times without an offset needs a policy for ambiguous/nonexistent - times around daylight-saving transitions. - -Keep arithmetic, parsing, formatting, and timer scheduling in Simple where -practical. Limit native code to clock readings, OS waits, and local-zone -conversion. Validate overflow, negative epoch division/remainders, leap days, -invalid dates, and supported calendar ranges. A parse error cannot use `-1` -as a sentinel because that is a valid instant. Fixed-duration days and calendar -day arithmetic are separate operations. - -Animation needs an input-aware wait in addition to ordinary sleep. Replace -the graphics adapter's fixed 17ms delay with a proposed `gfx.wait(timeoutMs)` -that processes events and wakes for either input or timeout. The game loop -waits until the earlier of its next frame and next timer, then handles input, -due timers, simulation, and drawing on the same thread. Windows has a -message-aware timed wait; X11 needs corresponding backend integration. - -A small cooperative timer queue in Simple can expose `after(delay)` and -`every(period)` returning timer IDs, plus `cancel(id)`, `nextDeadline()`, and -`poll(now)` returning due IDs. Returning IDs avoids requiring closures or -background callbacks. Advance repeating deadlines from their previous deadline -to avoid drift. Define a missed-tick policy and bound animation catch-up; -reset or clamp frame deltas after laptop sleep rather than replaying hours of -frames. Explicitly exclude waking the computer from system suspend. - -Use an animated game example to drive the chapter. Planning estimate: -roughly 1,000-2,000 implementation lines, plus tests and documentation. Clock -readings, sleep, and unit conversion are small; calendar/text/zone support, -cooperative scheduling, responsive graphics waiting, and an animated example -make the complete scope medium-sized. Test timer ordering, cancellation, -repetition, and clock jumps with injected/fake readings; keep actual sleep tests -broadly bounded and hardware/display checks optional. - -Platform references: [clock_gettime](https://man7.org/linux/man-pages/man2/clock_gettime.2.html), -[clock_nanosleep](https://man7.org/linux/man-pages/man2/clock_nanosleep.2.html), -[calendar conversion](https://man7.org/linux/man-pages/man3/localtime.3.html), -[Windows clock guidance](https://learn.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps), -and [message-aware waiting](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-msgwaitformultipleobjectsex). +Chapter 31 also implements [time and single-threaded animation](../chapter31/docs/time.md), +including cooperative timers and responsive input waits. ### Structs with trailing arrays diff --git a/skills/chapter25-codex-notes.md b/skills/chapter25-codex-notes.md index c155804c..abc28f1b 100644 --- a/skills/chapter25-codex-notes.md +++ b/skills/chapter25-codex-notes.md @@ -66,6 +66,30 @@ Chapter 30 retains the preceding compiler and library. Keep inherited Java test use LF for new text files, and do not add Python code or test drivers. Runnable examples belong in `chapter31/docs/examples`. +MiniFB discovery defaults to executable-relative and obvious chapter build +locations; examples should run directly without environment setup. Explicit +`SIMPLE_MINIFB` paths override discovery and `off` keeps headless tests stable. + +The Clock example now shows digital/analog local-time clocks, switched by the +software `sys.draw.Button` widget or Space. Button input is caller-fed, fires +on release inside after a press inside, and has no native/window dependency. + +Clock rendering needs native regression coverage as well as Eval2: evaluator +checks cannot catch instruction encoding or relocation errors. X86 floating +comparisons use UCOMISD's CF/ZF/PF, preserve unordered behavior when layout +negates a branch, and must not use signed SF/OF conditions. Encoding metadata +for relocation sites uses instruction identity: scheduling/RA can clone equal +constant loads, each of which still requires its own relocation. + +Time and animation now belong in Chapter 31, not a follow-on chapter proposal. +See `chapter31/docs/time.md`: bare i64 epoch milliseconds, distinct monotonic +deadlines, optional monotonic nanoseconds, OS local zones, Simple calendar/text +and cooperative timer IDs, and input-aware `gfx.wait`. Cliff explicitly accepts +`-1` for absolute parse errors despite its collision with the instant just before +the epoch; relative time parsing is deferred. Built-in `%t` selectors cover ISO, +12/24-hour clocks, elapsed minutes, calendar dates, and clock seconds. UTC is +the interpolation default; local-zone formatting is an explicit library call. + ## Chapter 30 starting point (2026-11-07; superseded by Chapter 31) Cliff committed the Chapter 29 work as `bb9b30b2c` and requested merging |
