summaryrefslogtreecommitdiff
path: root/chapter31
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-11-11 15:40:22 -0800
committerCliff Click <cliffc@acm.org>2026-11-12 13:48:30 -0800
commit0720fd8ed0aa92248622a9f3c3ebd522b002fd91 (patch)
tree3ba939eb4e786373cedc54e1afe0bbdbfd16dc01 /chapter31
parent18c04fd4aab0ad0bf3b3eece9551cc7aa7b9f796 (diff)
downloadSimple-0720fd8ed0aa92248622a9f3c3ebd522b002fd91.tar.gz
Simple-0720fd8ed0aa92248622a9f3c3ebd522b002fd91.zip
graphic tictactoe
Hex & octal digits
Diffstat (limited to 'chapter31')
-rw-r--r--chapter31/Makefile2
-rw-r--r--chapter31/README.ja.md12
-rw-r--r--chapter31/README.md17
-rw-r--r--chapter31/docs/31-grammar.ja.md3
-rw-r--r--chapter31/docs/31-grammar.md11
-rw-r--r--chapter31/docs/drawing.md81
-rw-r--r--chapter31/docs/examples/README.md4
-rw-r--r--chapter31/docs/examples/TicTacToeGraphical.smp156
-rw-r--r--chapter31/docs/graphics.md3
-rw-r--r--chapter31/graphics.mk8
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/parser/Expressions.java2
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/parser/Lexer.java26
-rw-r--r--chapter31/src/main/smp/sys.smp1
-rw-r--r--chapter31/src/main/smp/sys/draw.smp17
-rw-r--r--chapter31/src/main/smp/sys/draw/Canvas.smp176
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/Chapter01Test.java9
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/Chapter21Test.java3
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java4
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/DrawTest.java102
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/GraphicsTest.java8
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/IntegerLiteralTest.java46
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java13
22 files changed, 684 insertions, 20 deletions
diff --git a/chapter31/Makefile b/chapter31/Makefile
index 9e4f5b89..10717b55 100644
--- a/chapter31/Makefile
+++ b/chapter31/Makefile
@@ -232,7 +232,7 @@ tests_sys: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag $(TEST_SYS) $(RELEASE_CRT)
CLASS ?= Chapter25Test
METHOD ?=
.PHONY: test
-test: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag $(TEST_SYS) $(RELEASE_CRT)
+test: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag $(TEST_SYS) $(RELEASE_SYS) $(RELEASE_CRT)
@$(JVM) com.seaofnodes.simple.TestRunner "$(CLASS)" "$(METHOD)"
fuzzer: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag
diff --git a/chapter31/README.ja.md b/chapter31/README.ja.md
index ccd5777e..9f264ae2 100644
--- a/chapter31/README.ja.md
+++ b/chapter31/README.ja.md
@@ -1,11 +1,19 @@
-# 第31章:作業中
+# 第31章:ソフトウェア描画とグラフィカルゲーム
[English](README.md) | 日本語
[前の章:第30章 — テキスト整形と診断](../chapter30/README.ja.md)
第30章で完成したコンパイラと標準ライブラリを出発点とする章です。
-次のテーマは未定です。[ロードマップ](../docs/roadmap.ja.md)を参照してください。
+Simple で描画する `sys.draw.Canvas` と、MiniFB を使うオプションの
+ウィンドウ・入力 API `sys.gfx` を使って、グラフィカルな三目並べを実装します。
+矩形、円、太い線、色、ビットマップ文字を扱えます。
+詳細は[英語版の描画ライブラリとゲームの説明](docs/drawing.md)を参照してください。
+通常のビルドとテストに MiniFB や画面表示は必要ありません。
+
+```sh
+make -C chapter31 run-graphical-tictactoe CTAGS=
+```
このスナップショットの[文法](docs/31-grammar.ja.md)と
[実行可能な例](docs/examples/)を用意しています。
diff --git a/chapter31/README.md b/chapter31/README.md
index 011cb7ed..6b7d9eb7 100644
--- a/chapter31/README.md
+++ b/chapter31/README.md
@@ -1,4 +1,4 @@
-# Chapter 31: Work in Progress
+# Chapter 31: Software Drawing and Graphical Games
English | [日本語](README.ja.md)
@@ -8,6 +8,21 @@ Small graphical games drive this chapter's language and library work, starting
with a pixel window for a Simple-written 2-D graphics library. MiniFB supplies
the optional native window/input backend; drawing stays in Simple.
+The [drawing library](docs/drawing.md) provides a pixel canvas, colors,
+rectangles, circles, thick lines, and bitmap labels. The
+[graphical tic-tac-toe game](docs/examples/TicTacToeGraphical.smp) uses it for
+mouse/keyboard play against the terminal game's CPU strategy.
+Hexadecimal colors (`0xe47b57`) and leading-zero octal font masks (`025755`)
+are supported by this chapter's integer literal syntax.
+
+```sh
+make -C chapter31 run-graphical-tictactoe CTAGS=
+```
+
+This explicitly builds the optional backend and opens a window. Use
+`graphical-tictactoe` instead to build the executable without starting it or
+requiring MiniFB.
+
The default build and test suite need no graphics dependencies. See
[optional graphics](docs/graphics.md) for building MiniFB, running the pixel
example, and handling a missing backend. `make tests_nographics CTAGS=` runs
diff --git a/chapter31/docs/31-grammar.ja.md b/chapter31/docs/31-grammar.ja.md
index 465bdcf7..f4635f21 100644
--- a/chapter31/docs/31-grammar.ja.md
+++ b/chapter31/docs/31-grammar.ja.md
@@ -167,7 +167,7 @@ argumentList : assignment (',' assignment)* ','? ;
// earlier chapters are replaced by constructor arguments.
newExpression : 'new' type ('[' assignment ']' | callSuffix)? ;
-INTEGER_LITERAL : '0' | [1-9] DIGIT* ;
+INTEGER_LITERAL : [1-9] DIGIT* | '0' [0-7]* | '0' [xX] HEX_DIGIT+ ;
FLOAT_LITERAL : DIGIT+ ('.' DIGIT* ('e' DIGIT+)? | 'e' '-'? DIGIT+) ;
STRING_LITERAL : '"' (ESCAPE | ~["\\])* '"' ;
CHARACTER_LITERAL : '\'' (ESCAPE | ~['\\]) '\'' ;
@@ -175,6 +175,7 @@ fragment ESCAPE : '\\' [0ntr\\'"] ;
IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ;
fragment NON_DIGIT : [a-zA-Z_] ;
fragment DIGIT : [0-9] ;
+fragment HEX_DIGIT : [0-9a-fA-F] ;
WHITESPACE : [ \t\r\n]+ -> skip ;
LINE_COMMENT : '//' ~[\r\n]* -> skip ;
BLOCK_COMMENT : '/*' .*? '*/' -> skip ;
diff --git a/chapter31/docs/31-grammar.md b/chapter31/docs/31-grammar.md
index 1aa31d42..19f834af 100644
--- a/chapter31/docs/31-grammar.md
+++ b/chapter31/docs/31-grammar.md
@@ -11,6 +11,14 @@ language; it is not input to a parser generator.
`int` is an alias for `i32`; `i64` explicitly selects 64-bit integer storage.
This alias does not change literal inference or intermediate arithmetic.
+Integer literals use decimal (`42`), leading-zero octal (`052`), or hexadecimal
+(`0x2a`, `0X2A`). Octal digits are 0–7; `08` and `09` are errors. Hexadecimal
+and octal literals can spell any 64-bit bit pattern: the high bit gives the
+usual signed value, so `0xffffffffffffffff` is -1. Decimal literals retain
+their signed 64-bit limit. Floating literals remain decimal, including forms
+such as `012.5` and `012f`. A radix prefix does not change integer inference
+or storage width.
+
```antlrv4
grammar SimpleLanguage;
@@ -179,7 +187,7 @@ argumentList : assignment (',' assignment)* ','? ;
// earlier chapters are replaced by constructor arguments.
newExpression : 'new' type ('[' assignment ']' | callSuffix)? ;
-INTEGER_LITERAL : '0' | [1-9] DIGIT* ;
+INTEGER_LITERAL : [1-9] DIGIT* | '0' [0-7]* | '0' [xX] HEX_DIGIT+ ;
// The f suffix also accepts an integer-shaped token, e.g. 12f.
FLOAT_LITERAL : DIGIT+ ('.' DIGIT* ('e' DIGIT+)? | 'e' '-'? DIGIT+) 'f'?
| DIGIT+ 'f' ;
@@ -197,6 +205,7 @@ fragment ESCAPE : '\\' [0ntr\\'"%] ;
IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ;
fragment NON_DIGIT : [a-zA-Z_] ;
fragment DIGIT : [0-9] ;
+fragment HEX_DIGIT : [0-9a-fA-F] ;
WHITESPACE : [ \t\r\n]+ -> skip ;
LINE_COMMENT : '//' ~[\r\n]* -> skip ;
BLOCK_COMMENT : '/*' .*? '*/' -> skip ;
diff --git a/chapter31/docs/drawing.md b/chapter31/docs/drawing.md
new file mode 100644
index 00000000..d9d7e624
--- /dev/null
+++ b/chapter31/docs/drawing.md
@@ -0,0 +1,81 @@
+# Drawing in Simple
+
+`sys.draw` draws into an ordinary `u32[]`. It has no native dependencies and
+does not open a window. `sys.gfx` presents the resulting pixels using the
+optional [MiniFB backend](graphics.md).
+
+```simple
+val canvas = new sys.draw.Canvas(600,780);
+u32 orange = 0xe47b57;
+canvas.clear(sys.draw.WHITE);
+canvas.fillRect(20,20,180,80,orange);
+canvas.rect(20,20,180,80,3,sys.draw.BLACK);
+canvas.circle(300,200,48,8,sys.draw.BLUE);
+canvas.fillCircle(420,200,20,sys.draw.GREEN);
+canvas.line(40,320,250,440,12,orange);
+canvas.text(40,500,"YOUR TURN",4,sys.draw.BLACK);
+// After sys.gfx.open succeeds:
+sys.gfx.present(canvas.pixels);
+```
+
+## Small raster API
+
+Coordinates start at the upper left; x increases rightward and y downward.
+Drawing methods return the canvas for chaining. Everything is opaque RGB;
+there is no blending, antialiasing, transform stack, or native drawing state.
+Coordinates accept `i64` so ordinary widened arithmetic can be passed directly.
+Circle radii and circle stroke widths are `int`. Use screen-sized coordinates,
+extents, and stroke widths.
+
+| Operation | Behavior |
+| --- | --- |
+| `rgb(r,g,b)` | Clamp components to 0–255 and pack RGB. `BLACK`, `WHITE`, `RED`, `GREEN`, and `BLUE` are available too. |
+| `new Canvas(width,height)` | Allocate a zero-filled pixel array. Dimensions must be positive and their product must fit a signed 32-bit index; invalid sizes exit with status 1. Allocation can still fail for lack of memory. |
+| `clear(color)` | Fill the entire canvas. |
+| `pixel(x,y,color)` | Set one pixel; ignore points outside the canvas. |
+| `fillRect(x,y,w,h,color)` | Fill a half-open rectangle: x through x+w-1, y through y+h-1. |
+| `rect(x,y,w,h,thickness,color)` | Draw an inward outline. A sufficiently wide outline fills the rectangle. |
+| `fillCircle(cx,cy,radius,color)` | Fill a circle; radius zero draws its center pixel. |
+| `circle(cx,cy,radius,thickness,color)` | Draw an inward circular outline. |
+| `line(x0,y0,x1,y1,thickness,color)` | Draw a round-ended segment. A zero-length segment draws a round dot. Thickness is its geometric diameter, sampled at integer pixel centers. |
+| `text(x,y,string,scale,color)` | Draw a 3×5 bitmap font, with one column between characters. Upper/lowercase ASCII letters, digits, spaces, and basic punctuation are supported. Newline advances six scaled pixels. Unknown characters use `?`. |
+
+Shapes clip to the canvas. Nonpositive rectangle extents, stroke widths, or
+text scale do nothing; negative circle radii do nothing. Line and circle
+coverage scans only the clipped bounding box, which is sufficient for small
+turn-based games. This is a starting raster library rather than a fast animated
+renderer. The pixel array remains available for direct drawing and inspection.
+
+Packed colors use hexadecimal literals (`0xRRGGBB`); `rgb(r,g,b)` remains handy
+for computed components. The font masks use leading-zero octal, with five
+digits after the prefix: each digit is one three-pixel row, top to bottom.
+For example, `A` is `025755`: rows `010`, `101`, `111`, `101`, `101`.
+
+## Graphical tic-tac-toe
+
+From `chapter31`:
+
+```sh
+make run-graphical-tictactoe CTAGS=
+```
+
+Click an empty square or press **1–9**, numbered left-to-right, top-to-bottom.
+You are X; the CPU is O. It wins when possible, blocks your immediate win,
+then prefers the center, corners, and edges. The result stays visible with
+the winning line highlighted. Press **R** or **Space** to start a new game,
+or **Q**/**Escape** to quit. Restart is also available during a game.
+
+Held buttons and keys do not take repeated turns. Moving the pointer highlights
+empty cells. The canvas is redrawn only after a move, restart, or hover change;
+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).
+Passing any argument performs a five-poll, self-closing smoke test.
+
+The original `make tictactoe` still builds the terminal game.
+`make test CLASS=DrawTest CTAGS=` checks raster pixels and complete game turns
+without a display. `make tests_nographics CTAGS=` runs the ordinary suite with
+the backend disabled.
diff --git a/chapter31/docs/examples/README.md b/chapter31/docs/examples/README.md
index 2f19f09c..db95487e 100644
--- a/chapter31/docs/examples/README.md
+++ b/chapter31/docs/examples/README.md
@@ -4,6 +4,10 @@ These programs live beside the documentation so readers can find and run them.
Their non-graphical native regression tests run as part of `make tests`.
The optional [pixel-window example](Graphics.smp) uses MiniFB; see
[graphics setup](../graphics.md). `make run-graphics` opens it explicitly.
+The [graphical tic-tac-toe](TicTacToeGraphical.smp) uses the Simple
+[drawing library](../drawing.md). Run it with
+`make run-graphical-tictactoe CTAGS=`; click a cell or press 1–9, R/Space to
+replay, and Q/Escape to quit. The terminal game below remains available.
From `chapter31`, build the tic-tac-toe executable (without starting it):
diff --git a/chapter31/docs/examples/TicTacToeGraphical.smp b/chapter31/docs/examples/TicTacToeGraphical.smp
new file mode 100644
index 00000000..7daf6710
--- /dev/null
+++ b/chapter31/docs/examples/TicTacToeGraphical.smp
@@ -0,0 +1,156 @@
+// Player is X, CPU is O. Squares are numbered left-to-right, top-to-bottom.
+// Graphical tic-tac-toe; the same strategy as TicTacToe.smp.
+// Click an empty square or press 1-9. R/Space replays; Q/Escape quits.
+// Pass any argument for a five-poll window smoke test.
+val main = { u8[~][] args ->
+ val screen = new sys.draw.Canvas(600,780);
+ val board = new u8[9];
+ _reset(board);
+ if( sys.gfx.open("Simple tic-tac-toe",screen.width,screen.height) != sys.gfx.OK ) return 1;
+ _draw(screen,board,-1);
+ int result = sys.gfx.present(screen.pixels);
+ bool mouseHeld = false, keyHeld = false, replayHeld = false;
+ int hover = -1, frames = 0;
+ while( result==sys.gfx.OK ) {
+ result = sys.gfx.poll(true);
+ if( result!=sys.gfx.OK ) break;
+ if( sys.gfx.key('Q') ) break;
+ bool down = sys.gfx.button(sys.gfx.LEFT);
+ bool replay = sys.gfx.key('R') || sys.gfx.key(' ');
+ int square = _square(sys.gfx.mouseX(),sys.gfx.mouseY());
+ bool dirty = square!=hover;
+ hover = square;
+ if( replay && !replayHeld ) { _reset(board); dirty=true; }
+ bool digit = false;
+ int choice = down && !mouseHeld ? square : -1;
+ for( int i=0; i<9; i++ ) {
+ int key = '1'+i;
+ if( sys.gfx.key(key) ) {
+ digit = true;
+ if( !keyHeld ) choice=i;
+ }
+ }
+ if( !(replay && !replayHeld) && _move(board,choice) ) dirty=true;
+ mouseHeld = down;
+ keyHeld = digit;
+ replayHeld = replay;
+ if( dirty ) {
+ _draw(screen,board,hover);
+ result = sys.gfx.present(screen.pixels);
+ }
+ if( args#>1 && ++frames==5 ) break;
+ }
+ sys.gfx.close();
+ return result==sys.gfx.ERROR ? 1 : 0;
+};
+
+val _reset = { u8[] board ->
+ for( int i=0; i<9; i++ ) board[i]=' ';
+};
+val _square = { int x, int y ->
+ if( x<48 || x>=552 || y<168 || y>=672 ) return -1;
+ return (y-168)/168*3+(x-48)/168;
+};
+
+// One player turn followed by one CPU turn. Invalid/finished moves do nothing.
+val _move = { u8[] board, int square ->
+ if( square<0 || square>=9 || board[square]!=' ' || _finished(board) ) return false;
+ board[square] = 'X';
+ if( !_finished(board) ) {
+ int reply = _cpu(board);
+ if( reply>=0 ) board[reply]='O';
+ }
+ return true;
+};
+
+val _finished = { u8[~] board ->
+ if( _winner(board) ) return true;
+ for( int i=0; i<9; i++ ) if( board[i]==' ' ) return false;
+ return true;
+};
+
+val _winningLine = { u8[~] board ->
+ for( int i=0; i<3; i++ ) {
+ int row = i*3;
+ if( board[row]!=' ' && board[row]==board[row+1] && board[row]==board[row+2] ) return i;
+ if( board[i]!=' ' && board[i]==board[i+3] && board[i]==board[i+6] ) return 3+i;
+ }
+ if( board[4]!=' ' ) {
+ if( board[0]==board[4] && board[4]==board[8] ) return 6;
+ if( board[2]==board[4] && board[4]==board[6] ) return 7;
+ }
+ return -1;
+};
+// Return the winning mark, or zero.
+val _winner = { u8[~] board ->
+ int line = _winningLine(board);
+ return line<0 ? 0 : line<3 ? board[line*3] : line<6 ? board[line-3] : board[4];
+};
+
+// Try each empty square, restoring it before returning.
+val _threat = { u8[] board, u8 mark ->
+ for( int i=0; i<9; i++ )
+ if( board[i]==' ' ) {
+ board[i] = mark;
+ bool wins = _winner(board)==mark;
+ board[i] = ' '; // Restore board
+ if( wins ) return i;
+ }
+ return -1;
+};
+// Win if possible, otherwise block, then prefer center, corners, and edges.
+// Called only after _finished has ruled out a full board.
+val _cpu = { u8[] board ->
+ // If O can win, do so
+ int win = _threat(board,'O');
+ if( win>=0 ) return win;
+ // If X will win next move, block
+ int block = _threat(board,'X');
+ if( block>=0 ) return block;
+ // If center is open, take it
+ if( board[4]==' ' ) return 4;
+ // If a corner is open, take it
+ for( int i=0; i<9; i+=2 ) if( board[i]==' ' ) return i;
+ // Take whatever is available
+ for( int i=0; i<9; i++ ) if( board[i]==' ' ) return i;
+ return -1;
+};
+
+val _draw = { !sys.draw.Canvas screen, u8[~] board, int hover ->
+ u32 background = 0x142e36, paper = 0xf2eddf, ink = 0x35535a;
+ u32 cross = 0xe47b57, ring = 0x248b89, accent = 0xf0c778;
+ int winner = _winner(board);
+ bool finished = _finished(board);
+ screen.clear(background);
+ screen.text(48,34,"TIC TAC TOE",6,paper);
+ screen.fillRect(48,90,504,3,ink);
+ screen.text(48,116,finished ? (winner=='X' ? "YOU WIN!" : winner=='O' ? "CPU WINS" : "DRAW") : "YOUR TURN",4,accent);
+ screen.line(428,112,450,134,5,cross).line(450,112,428,134,5,cross);
+ screen.circle(496,123,13,5,0x70c9bf);
+ screen.text(428,145,"YOU",2,0xa2b9b7);
+ screen.text(484,145,"CPU",2,0xa2b9b7);
+ screen.fillRect(48,168,504,504,paper);
+ if( !finished && hover>=0 && hover<9 && board[hover]==' ' )
+ screen.fillRect(48+(hover-hover/3*3)*168,168+hover/3*168,168,168,0xd9e8de);
+ for( int i=1; i<3; i++ ) {
+ screen.fillRect(48+i*168-3,168,6,504,ink);
+ screen.fillRect(48,168+i*168-3,504,6,ink);
+ }
+ for( int i=0; i<9; i++ ) {
+ int cx = 132+(i-i/3*3)*168, cy = 252+i/3*168;
+ if( board[i]=='X' )
+ screen.line(cx-38,cy-38,cx+38,cy+38,12,cross).line(cx+38,cy-38,cx-38,cy+38,12,cross);
+ else if( board[i]=='O' ) screen.circle(cx,cy,48,11,ring);
+ else screen.text(cx-6,cy-10,i==0 ? "1" : i==1 ? "2" : i==2 ? "3" :
+ i==3 ? "4" : i==4 ? "5" : i==5 ? "6" : i==6 ? "7" : i==7 ? "8" : "9",4,0x95a59e);
+ }
+ int line = _winningLine(board);
+ if( line>=0 ) {
+ int first = line<3 ? line*3 : line<6 ? line-3 : line==6 ? 0 : 2;
+ int last = line<3 ? first+2 : line<6 ? first+6 : line==6 ? 8 : 6;
+ screen.line(132+(first-first/3*3)*168,252+first/3*168,132+(last-last/3*3)*168,252+last/3*168,5,0xeab64e);
+ }
+ screen.text(48,704,"CLICK A SQUARE OR PRESS 1-9",3,paper);
+ screen.text(48,738,"R OR SPACE REPLAY Q OR ESC QUIT",3,0xa2b9b7);
+ screen.rect(48,168,504,504,3,ink);
+};
diff --git a/chapter31/docs/graphics.md b/chapter31/docs/graphics.md
index 7e1636f9..fb3972ee 100644
--- a/chapter31/docs/graphics.md
+++ b/chapter31/docs/graphics.md
@@ -4,6 +4,9 @@ Simple owns the pixel array and drawing code. [MiniFB](https://github.com/emoon/
provides a window, input, and pixel presentation. The first interface is
`sys.gfx`: one fixed-size window, operated on the main thread. The
[Graphics example](examples/Graphics.smp) draws a color gradient in Simple.
+The [drawing library and graphical tic-tac-toe](drawing.md) build on this pixel
+interface. `make run-graphical-tictactoe CTAGS=` builds the optional backend and
+opens the game; `make graphical-tictactoe CTAGS=` only builds its executable.
## Default: no graphics dependency
diff --git a/chapter31/graphics.mk b/chapter31/graphics.mk
index a42bc99e..719c5fcd 100644
--- a/chapter31/graphics.mk
+++ b/chapter31/graphics.mk
@@ -21,8 +21,16 @@ else
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,)
.PHONY: graphics graphics-example run-graphics
+.PHONY: graphical-tictactoe run-graphical-tictactoe
+graphical-tictactoe: $(GRAPHICAL_TICTACTOE)
+$(GRAPHICAL_TICTACTOE): docs/examples/TicTacToeGraphical.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)/TicTacToeGraphical.o $<
+
+run-graphical-tictactoe: graphics graphical-tictactoe
+ SIMPLE_MINIFB="$(abspath $(GRAPHICS_LIB))" $(GRAPHICAL_TICTACTOE)
graphics: $(GRAPHICS_LIB)
$(MINIFB_STAMP): graphics.mk
diff --git a/chapter31/src/main/java/com/seaofnodes/simple/parser/Expressions.java b/chapter31/src/main/java/com/seaofnodes/simple/parser/Expressions.java
index 247d974e..6486aba8 100644
--- a/chapter31/src/main/java/com/seaofnodes/simple/parser/Expressions.java
+++ b/chapter31/src/main/java/com/seaofnodes/simple/parser/Expressions.java
@@ -400,7 +400,7 @@ final class Expressions {
* Parse integer literal
*
* <pre>
- * integerLiteral: [1-9][0-9]* | [0]
+ * integerLiteral: [1-9][0-9]* | 0[0-7]* | 0[xX][0-9a-fA-F]+
* floatLiteral: [digits].[digits]?[e [digits]]?[f]?
* </pre>
*/
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 5e4b7311..9ab678ca 100644
--- a/chapter31/src/main/java/com/seaofnodes/simple/parser/Lexer.java
+++ b/chapter31/src/main/java/com/seaofnodes/simple/parser/Lexer.java
@@ -239,6 +239,7 @@ public class Lexer {
// Return a constant Type, either TypeInteger or TypeFloat
Type parseNumber() {
Lexer start=new Lexer(this);
+ if( isHex() ) return parseInteger(parseNumberString(),16,2,start);
int old = _position;
int len = isLongOrDouble();
boolean f32 = peek()=='f';
@@ -250,7 +251,7 @@ public class Lexer {
return TypeFloat.constant(Float.parseFloat(literal));
if( len > 0 ) {
if( len > 1 && _input[old]=='0' )
- throw Parser.error("Syntax error: integer values cannot start with '0'",start);
+ return parseInteger(literal,8,0,start);
long i = Long.parseLong(literal);
return TypeInteger.constant(i);
}
@@ -262,6 +263,11 @@ public class Lexer {
}
private String parseNumberString() {
int old = _position;
+ if( isHex() ) {
+ _position += 2;
+ while( isIdLetter(peek()) ) inc();
+ return new String(_input,old,_position-old);
+ }
int len = Math.abs(isLongOrDouble());
if( peek()=='f' ) {
nextChar();
@@ -270,6 +276,24 @@ public class Lexer {
return new String(_input,old,len);
}
+ private boolean isHex() {
+ return peek()=='0' && _position+1<_input.length && (peek(1)=='x' || peek(1)=='X');
+ }
+
+ private TypeInteger parseInteger(String literal, int radix, int prefix, Lexer start) {
+ String kind = radix==16 ? "Hexadecimal" : "Octal";
+ if( literal.length()==prefix )
+ throw Parser.error(kind+" integer literal requires at least one digit",start);
+ for( int i=prefix; i<literal.length(); i++ )
+ if( Character.digit(literal.charAt(i),radix)<0 )
+ throw Parser.error("Invalid "+kind.toLowerCase()+" integer literal '"+literal+"'",start);
+ try {
+ return TypeInteger.constant(Long.parseUnsignedLong(literal.substring(prefix),radix));
+ } catch( NumberFormatException ex ) {
+ throw Parser.error(kind+" integer literal exceeds 64 bits",start);
+ }
+ }
+
// Return +len that ends a long
// Return -len that ends a double
private int isLongOrDouble() {
diff --git a/chapter31/src/main/smp/sys.smp b/chapter31/src/main/smp/sys.smp
index deaafa0b..3ef19099 100644
--- a/chapter31/src/main/smp/sys.smp
+++ b/chapter31/src/main/smp/sys.smp
@@ -8,6 +8,7 @@ val aryu8 = aryu8; // Variable sized array of u8
val char = char; // Character utilities
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 libc = libc; // libc FFI bindings
val rt = rt; // Run Time support
diff --git a/chapter31/src/main/smp/sys/draw.smp b/chapter31/src/main/smp/sys/draw.smp
new file mode 100644
index 00000000..0721659a
--- /dev/null
+++ b/chapter31/src/main/smp/sys/draw.smp
@@ -0,0 +1,17 @@
+// Software raster drawing. No window or native calls; pixels are packed RGB (0x00RRGGBB).
+u32 BLACK = 0x000000;
+u32 WHITE = 0xffffff;
+u32 RED = 0xe05252;
+u32 GREEN = 0x48b88c;
+u32 BLUE = 0x558ee6;
+
+// Components are clamped to bytes rather than wrapping.
+val rgb = { int r, int g, int b ->
+ r = r<0 ? 0 : r>255 ? 255 : r;
+ g = g<0 ? 0 : g>255 ? 255 : g;
+ b = b<0 ? 0 : b>255 ? 255 : b;
+ u32 color = (r<<16) | (g<<8) | b;
+ return color;
+};
+
+val Canvas = Canvas; // Pixel canvas and drawing methods
diff --git a/chapter31/src/main/smp/sys/draw/Canvas.smp b/chapter31/src/main/smp/sys/draw/Canvas.smp
new file mode 100644
index 00000000..8c01b4b3
--- /dev/null
+++ b/chapter31/src/main/smp/sys/draw/Canvas.smp
@@ -0,0 +1,176 @@
+struct Canvas {
+ u32 width;
+ u32 height;
+ u32[] pixels;
+
+ new Canvas = { int w, int h ->
+ i64 count = w;
+ count *= h;
+ if( w<=0 || h<=0 || count>2147483647 ) libc._exit(1);
+ width = w;
+ height = h;
+ pixels = new u32[count];
+ };
+
+ val clear = { u32 color ->
+ for( int i=0; i<pixels#; i++ ) pixels[i] = color;
+ return self;
+ };
+
+ val pixel = { i64 x, i64 y, u32 color ->
+ if( 0<=x<width && 0<=y<height ) pixels[y*width+x] = color;
+ return self;
+ };
+
+ // Integer coordinates, top-left origin. Rectangle extents are half-open.
+ val fillRect = { i64 x, i64 y, i64 w, i64 h, u32 color ->
+ if( w<=0 || h<=0 ) return self;
+ i64 right = x; right += w;
+ i64 bottom = y; bottom += h;
+ if( right<=0 || bottom<=0 || x>=width || y>=height ) return self;
+ int x0 = x<0 ? 0 : x, y0 = y<0 ? 0 : y;
+ int x1 = right>width ? width : right, y1 = bottom>height ? height : bottom;
+ for( int py=y0; py<y1; py++ )
+ for( int px=x0; px<x1; px++ ) pixels[py*width+px] = color;
+ return self;
+ };
+
+ // Outline stays inside the rectangle. Wide borders become a filled rect.
+ val rect = { i64 x, i64 y, i64 w, i64 h, i64 thickness, u32 color ->
+ if( w<=0 || h<=0 || thickness<=0 ) return self;
+ if( thickness>=w || thickness>=h || thickness>=w-thickness || thickness>=h-thickness )
+ return fillRect(x,y,w,h,color);
+ fillRect(x,y,w,thickness,color);
+ fillRect(x,y+h-thickness,w,thickness,color);
+ fillRect(x,y+thickness,thickness,h-2*thickness,color);
+ return fillRect(x+w-thickness,y+thickness,thickness,h-2*thickness,color);
+ };
+
+ // Integer centers are pixel centers. Circle outlines extend inward.
+ val fillCircle = { i64 cx, i64 cy, int radius, u32 color ->
+ return _circle(cx,cy,radius,0,true,color);
+ };
+ val circle = { i64 cx, i64 cy, int radius, int thickness, u32 color ->
+ return _circle(cx,cy,radius,thickness,false,color);
+ };
+ val _circle = { i64 cx, i64 cy, int radius, int thickness, bool filled, u32 color ->
+ if( radius<0 || (!filled && thickness<=0) ) return self;
+ i64 left = cx; left -= radius;
+ i64 top = cy; top -= radius;
+ i64 right = cx; right += radius;
+ i64 bottom = cy; bottom += radius;
+ if( right<0 || bottom<0 || left>=width || top>=height ) return self;
+ int x0 = left<0 ? 0 : left, y0 = top<0 ? 0 : top;
+ int x1 = right>=width ? width-1 : right, y1 = bottom>=height ? height-1 : bottom;
+ i64 outer2 = radius;
+ outer2 *= radius;
+ i64 inner = filled ? -1 : radius-thickness;
+ for( int py=y0; py<=y1; py++ )
+ for( int px=x0; px<=x1; px++ ) {
+ i64 dx = px; dx -= cx;
+ i64 dy = py; dy -= cy;
+ i64 d2 = dx*dx+dy*dy;
+ if( d2<=outer2 && (inner<=0 || d2>inner*inner) ) pixels[py*width+px] = color;
+ }
+ return self;
+ };
+
+ // Round caps; a zero-length segment is a dot. Only the clipped bounding
+ // box is scanned. Floating distances avoid overflowing coordinate squares.
+ val line = { i64 x0, i64 y0, i64 x1, i64 y1, i64 thickness, u32 color ->
+ if( thickness<=0 ) return self;
+ i64 pad = thickness/2+1;
+ i64 left = x0<x1 ? x0 : x1; left -= pad;
+ i64 top = y0<y1 ? y0 : y1; top -= pad;
+ i64 right = x0>x1 ? x0 : x1; right += pad;
+ i64 bottom = y0>y1 ? y0 : y1; bottom += pad;
+ if( right<0 || bottom<0 || left>=width || top>=height ) return self;
+ int bx0 = left<0 ? 0 : left, by0 = top<0 ? 0 : top;
+ int bx1 = right>=width ? width-1 : right, by1 = bottom>=height ? height-1 : bottom;
+ for( int py=by0; py<=by1; py++ )
+ for( int px=bx0; px<=bx1; px++ )
+ if( _onLine(px,py,x0,y0,x1,y1,thickness) ) pixels[py*width+px] = color;
+ return self;
+ };
+
+ // Small 3x5 bitmap font for game labels. Lowercase maps to uppercase.
+ // Newline returns to x; unsupported characters draw a question mark.
+ val text = { i64 x, i64 y, u8[~] str, int scale, u32 color ->
+ if( scale<=0 ) return self;
+ i64 px = x, py = y;
+ for( int i=0; i<str#; i++ ) {
+ u8 c = str[i];
+ if( c=='\n' ) { px=x; py+=6*scale; continue; }
+ int bits = _glyph(c);
+ for( int row=0; row<5; row++ )
+ for( int col=0; col<3; col++ )
+ if( (bits >> (14-row*3-col)) & 1 )
+ fillRect(px+col*scale,py+row*scale,scale,scale,color);
+ px += 4*scale;
+ }
+ return self;
+ };
+};
+
+val _glyph = { u8 c ->
+ // Five octal digits: one 3-bit row per digit, top to bottom.
+ if( 'a'<=c<='z' ) c -= 'a'-'A';
+ if( c==' ' ) return 000000;
+ if( c=='0' ) return 075557;
+ if( c=='1' ) return 026227;
+ if( c=='2' ) return 061247;
+ if( c=='3' ) return 061216;
+ if( c=='4' ) return 055711;
+ if( c=='5' ) return 074616;
+ if( c=='6' ) return 034757;
+ if( c=='7' ) return 071222;
+ if( c=='8' ) return 075757;
+ if( c=='9' ) return 075716;
+ if( c=='A' ) return 025755;
+ if( c=='B' ) return 065656;
+ if( c=='C' ) return 034443;
+ if( c=='D' ) return 065556;
+ if( c=='E' ) return 074647;
+ if( c=='F' ) return 074644;
+ if( c=='G' ) return 034553;
+ if( c=='H' ) return 055755;
+ if( c=='I' ) return 072227;
+ if( c=='J' ) return 011152;
+ if( c=='K' ) return 055655;
+ if( c=='L' ) return 044447;
+ if( c=='M' ) return 057755;
+ if( c=='N' ) return 057775;
+ if( c=='O' ) return 025552;
+ if( c=='P' ) return 065644;
+ if( c=='Q' ) return 025573;
+ if( c=='R' ) return 065655;
+ if( c=='S' ) return 034216;
+ if( c=='T' ) return 072222;
+ if( c=='U' ) return 055557;
+ if( c=='V' ) return 055552;
+ if( c=='W' ) return 055775;
+ if( c=='X' ) return 055255;
+ if( c=='Y' ) return 055222;
+ if( c=='Z' ) return 071247;
+ if( c=='-' ) return 000700;
+ if( c=='.' ) return 000002;
+ if( c==':' ) return 002020;
+ if( c=='!' ) return 022202;
+ if( c=='?' ) return 061202;
+ return 061202;
+};
+
+// Pixel coverage for a round-ended segment, separate from traversal.
+val _onLine = { i64 px, i64 py, i64 x0, i64 y0, i64 x1, i64 y1, i64 thickness ->
+ f64 dx = x1; dx -= x0;
+ f64 dy = y1; dy -= y0;
+ f64 vx = px; vx -= x0;
+ f64 vy = py; vy -= y0;
+ f64 length2 = dx*dx+dy*dy;
+ bool point = x0==x1 && y0==y1;
+ f64 t = point ? 0.0 : (vx*dx+vy*dy)/length2;
+ t = t<0 ? 0.0 : t>1 ? 1.0 : t;
+ f64 ex = vx-t*dx, ey = vy-t*dy;
+ f64 half = thickness/2.0;
+ return ex*ex+ey*ey<=half*half;
+};
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/Chapter01Test.java b/chapter31/src/test/java/com/seaofnodes/simple/Chapter01Test.java
index fa7fb68a..6db79c16 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/Chapter01Test.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/Chapter01Test.java
@@ -41,13 +41,8 @@ public class Chapter01Test {
}
@Test
- public void testBad2() {
- try {
- new CodeGen("return 0123;").parse();
- fail();
- } catch( RuntimeException e ) {
- assertEquals("Syntax error: integer values cannot start with '0'",e.getMessage());
- }
+ public void testOctal() {
+ assertEquals("return 83;",new CodeGen("return 0123;").parse().print());
}
@Test
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/chapter31/src/test/java/com/seaofnodes/simple/Chapter21Test.java
index c6d558eb..1a57d4da 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/Chapter21Test.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/Chapter21Test.java
@@ -531,7 +531,8 @@ val sieve = { int N ->
String sprimes = sb.p("]").toString();
// Compile, link against native C; expect the above string of primes to be printed out by C
- TestC.runC(src, "sieve", sprimes,344);
+ // Load/Phi hoisting now requires the pointer before the loop entry.
+ TestC.runC(src, "sieve", sprimes,216);
// Evaluate on RISC5 emulator; expect return of an array of primes in
// the simulated heap.
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java
index 44cdbada..3b71bd2a 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java
@@ -475,9 +475,9 @@ public class Chapter25Test {
// Elf files are sane
- // Sys includes the graphics interface; its native backend is optional.
+ // Software drawing and the optional native graphics interface.
assertArrayEquals(new String[]{"sys/adt", "sys/ary",
- "sys/aryi64", "sys/aryu8", "sys/char", "sys/gfx", "sys/io", "sys/libc", "sys/rt", "sys/scan", "sys/text"},sys_elf._deps);
+ "sys/aryi64", "sys/aryu8", "sys/char", "sys/draw", "sys/gfx", "sys/io", "sys/libc", "sys/rt", "sys/scan", "sys/text"},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
new file mode 100644
index 00000000..b3e8edd0
--- /dev/null
+++ b/chapter31/src/test/java/com/seaofnodes/simple/DrawTest.java
@@ -0,0 +1,102 @@
+package com.seaofnodes.simple;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Test;
+
+// Native raster and game checks; no backend or display is needed.
+public class DrawTest {
+ @Test public void rectanglesAndColors() throws Exception {
+ TestC.runSYS("""
+ val main = { ->
+ val c = new sys.draw.Canvas(12,10);
+ u32 color = sys.draw.rgb(300,-1,128);
+ if( color!=16711808 ) return 1;
+ c.clear(7).fillRect(-2,-1,5,4,color);
+ for( int y=0; y<10; y++ )
+ for( int x=0; x<12; x++ )
+ if( c.pixels[y*12+x]!=(x<3 && y<3 ? color : 7) ) return 2;
+ c.clear(0).rect(2,2,7,6,1,9);
+ for( int y=0; y<10; y++ )
+ for( int x=0; x<12; x++ ) {
+ var inside = 2<=x<9 && 2<=y<8;
+ var border = inside && (x==2 || x==8 || y==2 || y==7);
+ if( c.pixels[y*12+x]!=(border ? 9 : 0) ) return 3;
+ }
+ c.clear(5).fillRect(20,0,8,8,1).rect(0,0,12,10,0,1);
+ c.fillRect(0,0,-1,5,1).pixel(-1,0,1).pixel(12,9,1);
+ for( int i=0; i<c.pixels#; i++ ) if( c.pixels[i]!=5 ) return 4;
+ return 0;
+ };
+ ""","drawRectangles","",-1);
+ }
+
+ @Test public void circlesLinesAndText() throws Exception {
+ TestC.runSYS("""
+ val main = { ->
+ val c = new sys.draw.Canvas(16,16);
+ c.fillCircle(8,8,3,1);
+ for( int y=0; y<16; y++ )
+ for( int x=0; x<16; x++ )
+ if( c.pixels[y*16+x]!=((x-8)*(x-8)+(y-8)*(y-8)<=9 ? 1 : 0) ) {
+ sys.io.p("circle mismatch %(x) %(y): %(c.pixels[y*16+x])\\n");
+ return 1;
+ }
+ c.clear(0).circle(8,8,3,1,2);
+ if( c.pixels[8*16+8]!=0 || c.pixels[8*16+11]!=2 || c.pixels[8*16+10]!=0 ) return 2;
+ c.clear(0).circle(8,8,3,3,2);
+ if( c.pixels[8*16+8]!=2 ) return 8;
+ c.clear(0).line(-5,8,20,8,3,3);
+ for( int y=0; y<16; y++ )
+ for( int x=0; x<16; x++ )
+ if( c.pixels[y*16+x]!=(7<=y<=9 ? 3 : 0) ) return 3;
+ c.clear(0).line(8,8,8,8,2,4);
+ if( c.pixels[8*16+8]!=4 || c.pixels[8*16+9]!=4 || c.pixels[9*16+9]!=0 ) return 4;
+ c.clear(0).line(2,2,13,13,1,5);
+ if( c.pixels[7*16+7]!=5 || c.pixels[7*16+8]!=0 ) return 5;
+ c.clear(0).text(0,0,"x",1,6).text(8,0,"X",1,6);
+ for( int y=0; y<5; y++ )
+ for( int x=0; x<3; x++ )
+ if( c.pixels[y*16+x]!=c.pixels[y*16+x+8] ) return 6;
+ c.clear(0).fillCircle(-1,-1,2,7);
+ if( c.pixels[0]!=7 || c.pixels[2]!=0 ) return 7;
+ return 0;
+ };
+ ""","drawCurves","",-1);
+ }
+
+ @Test public void graphicalGameWithoutWindow() throws Exception {
+ String game=Files.readString(Path.of("docs/examples/TicTacToeGraphical.smp"))
+ .replace("val main =","val _windowMain =");
+ TestC.runSYS(game+"""
+
+ val main = { ->
+ val board = new u8[9];
+ _reset(board);
+ if( _square(48,168)!=0 || _square(551,671)!=8 || _square(552,400)!=-1 || _square(100,167)!=-1 ) return 1;
+ if( !_move(board,4) || board[4]!='X' || board[0]!='O' ) return 2;
+ if( _move(board,4) || _move(board,0) || _move(board,-1) || _move(board,9) ) return 3;
+ val moves = "1879";
+ _reset(board);
+ for( int i=0; i<moves#; i++ )
+ if( !_move(board,moves[i]-'1') ) return 4;
+ if( _winner(board)!='X' || _move(board,1) ) return 5;
+ val screen = new sys.draw.Canvas(600,780);
+ _draw(screen,board,-1);
+ if( screen.pixels[252*600+132]==screen.pixels[0] ) return 6;
+ _reset(board);
+ val draw = "12768";
+ for( int i=0; i<draw#; i++ ) _move(board,draw[i]-'1');
+ if( !_finished(board) || _winner(board)!=0 ) return 7;
+ _reset(board);
+ _move(board,0); _move(board,1); _move(board,3);
+ if( _winner(board)!='O' ) return 8;
+ _draw(screen,board,-1);
+ _reset(board);
+ if( _finished(board) ) return 9;
+ _draw(screen,board,4);
+ return 0;
+ };
+ ""","drawGame","",-1);
+ }
+}
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/GraphicsTest.java b/chapter31/src/test/java/com/seaofnodes/simple/GraphicsTest.java
index 2e9c8dbc..9c432ce4 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;
+ private static String graphics, text, game;
@BeforeClass public static void compilePrograms() throws Exception {
var paths = new Ary<>(new String[]{TestC.RELEASE_SYS_DIR});
@@ -18,6 +18,8 @@ public class GraphicsTest {
"graphics_missing",paths,TestC.CALL_CONVENTION,null,null,-1);
text = TestC.compile("val main={ -> sys.io.p(\"text still works\\n\"); return 0; };",
"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);
}
private static String run(String exe, String backend, int exit) throws Exception {
@@ -43,6 +45,10 @@ public class GraphicsTest {
assertTrue(run(graphics,"off",1).contains("disabled"));
}
+ @Test public void gameWithoutBackend() throws Exception {
+ assertTrue(run(game,"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/IntegerLiteralTest.java b/chapter31/src/test/java/com/seaofnodes/simple/IntegerLiteralTest.java
new file mode 100644
index 00000000..975042c2
--- /dev/null
+++ b/chapter31/src/test/java/com/seaofnodes/simple/IntegerLiteralTest.java
@@ -0,0 +1,46 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.type.TypeFloat;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class IntegerLiteralTest {
+ private static void value(String expression, long expected) {
+ var code = new CodeGen("return "+expression+";").parse();
+ assertEquals(expression,TypeInteger.constant(expected),code.expr()._type);
+ }
+
+ @Test public void valuesAndBitPatterns() {
+ value("0x00e47b57",14973783);
+ value("0XfF",255);
+ value("025755",11245); // A: rows 010, 101, 111, 101, 101.
+ value("000000",0);
+ value("0x7fffffffffffffff",Long.MAX_VALUE);
+ value("0x8000000000000000",Long.MIN_VALUE);
+ value("0xffffffffffffffff",-1);
+ value("0777777777777777777777",Long.MAX_VALUE);
+ value("01000000000000000000000",Long.MIN_VALUE);
+ value("01777777777777777777777",-1);
+ value("0x00000000000000000001",1);
+ value("-0x2a + 052",0);
+ value("0x1e+2",32); // e and f are hex digits, not float markers.
+ value("0xff & 017",15);
+ value("017<<0x4",240);
+ value("0x0f",15);
+ value("0123",83);
+ value("000000000000000000000000000000000001",1);
+ assertEquals(TypeFloat.constant(12.5),new CodeGen("return 012.5;").parse().expr()._type);
+ assertEquals(TypeFloat.constant(12.0),new CodeGen("return 012f;").parse().expr()._type);
+ }
+
+ @Test public void nativeLiterals() throws Exception {
+ TestC.runSF("""
+ val check_noInline = { i64 mask, u32 color, int glyph ->
+ return mask==-1 && color==14973783 && glyph==11245 ? 0 : 1;
+ };
+ val main = { -> return check_noInline(0xffffffffffffffff,0x00e47b57,025755); };
+ ""","basedLiterals","",-1);
+ }
+}
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java b/chapter31/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java
index 2b7eea78..ef26a943 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java
@@ -30,7 +30,18 @@ public class ParserDiagnosticTest {
diagnostic("return 9223372036854775808;",1,8,"Integer literal is outside the signed 64-bit range");
diagnostic("return 1e;",1,8,"Invalid floating-point literal '1e'");
diagnostic("return 1.2.3f;",1,8,"Invalid floating-point literal '1.2.3f'");
- diagnostic("return 012;",1,8,"Syntax error: integer values cannot start with '0'");
+ diagnostic("return 018;",1,8,"Invalid octal integer literal '018'");
+ }
+
+ @Test public void testBasedIntegerLiteralErrors() {
+ diagnostic("return 0x;",1,8,"Hexadecimal integer literal requires at least one digit");
+ diagnostic("return 0x12g;",1,8,"Invalid hexadecimal integer literal '0x12g'");
+ diagnostic("return 0128;",1,8,"Invalid octal integer literal '0128'");
+ diagnostic("return 0x_ff;",1,8,"Invalid hexadecimal integer literal '0x_ff'");
+ diagnostic("return 09;",1,8,"Invalid octal integer literal '09'");
+ diagnostic("return 0x10000000000000000;",1,8,"Hexadecimal integer literal exceeds 64 bits");
+ diagnostic("return 02000000000000000000000;",1,8,"Octal integer literal exceeds 64 bits");
+ diagnostic("int 0xabc;",1,5,"Expected an identifier, found '0xabc'");
}
@Test public void testUnclosedCommentsAndStrings() {