diff options
| author | Cliff Click <cliffc@acm.org> | 2026-11-11 15:40:22 -0800 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-11-12 13:48:30 -0800 |
| commit | 0720fd8ed0aa92248622a9f3c3ebd522b002fd91 (patch) | |
| tree | 3ba939eb4e786373cedc54e1afe0bbdbfd16dc01 /chapter31 | |
| parent | 18c04fd4aab0ad0bf3b3eece9551cc7aa7b9f796 (diff) | |
| download | Simple-0720fd8ed0aa92248622a9f3c3ebd522b002fd91.tar.gz Simple-0720fd8ed0aa92248622a9f3c3ebd522b002fd91.zip | |
graphic tictactoe
Hex & octal digits
Diffstat (limited to 'chapter31')
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() { |
