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