diff options
| author | Cliff Click <cliffc@acm.org> | 2026-11-14 16:23:39 -0800 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-11-14 16:23:39 -0800 |
| commit | 7e66b7ae6b00a2e1652db32f01f0bf480508ef05 (patch) | |
| tree | f09689493fd820544b4cd94d4b1e941d84a24b4b /chapter31 | |
| parent | 3d0f3f38d9d8345b6508b3289dc94b0500c53d9e (diff) | |
| download | Simple-7e66b7ae6b00a2e1652db32f01f0bf480508ef05.tar.gz Simple-7e66b7ae6b00a2e1652db32f01f0bf480508ef05.zip | |
CLeanup Breakoutmain
Fix a half-dozen compiler bugs; various kinds of forward refs and readable types.
Allow read-only self-types; and read-only methods.
Anonymous inner functions are treated as methods.
Better error messages in many places.
Diffstat (limited to 'chapter31')
21 files changed, 870 insertions, 210 deletions
diff --git a/chapter31/README.md b/chapter31/README.md index f6141950..ff40999a 100644 --- a/chapter31/README.md +++ b/chapter31/README.md @@ -73,7 +73,7 @@ units directly: `SECOND=1000`, `MINUTE=60*SECOND`, and `HOUR=60*MINUTE`. for due timer IDs; no timer thread or callback is needed. This might be a mindset change for you game writers, it depends on what era you started writing them. Here it is all single-threaded and event-loop driven - but when there is nothing -left to do, you need to wait for an event. +left to do, you need to wait for an event. Time also works in string interpolation: diff --git a/chapter31/docs/examples/Breakout.smp b/chapter31/docs/examples/Breakout.smp index e5757b45..b1d0b443 100644 --- a/chapter31/docs/examples/Breakout.smp +++ b/chapter31/docs/examples/Breakout.smp @@ -1,6 +1,6 @@ // Clear 24 x 8 bricks. Arrows or mouse move; Space/click serves; P pauses; // R restarts; Q/Escape exits. Any argument runs ten frames as a smoke test. -int WIDTH = 1008, HEIGHT = 760, COLS = 24, ROWS = 8; +u32 WIDTH = 1008, HEIGHT = 760, COLS = 24, ROWS = 8; int LEFT = 24, RIGHT = 984, TOP = 84, BOTTOM = 718; int BRICK_TOP = 152, BRICK_DX = 40, BRICK_DY = 22, BRICK_W = 36, BRICK_H = 18; int RADIUS = 5, PADDLE_HALF = 56, PADDLE_Y = 674; @@ -17,18 +17,76 @@ struct Game { int score = 0, remaining = COLS*ROWS, lives = 3, depth = 1, phase = READY; bool boosted = false, backWall = false, paused = false; f64 x = 504.0, y = 663.0, dx = 0.4, dy = -0.916515138991168; - int soundDepth = 1; - // Serve the ball; give it a nominal direction and speed - val _serve = { -> - if( phase == READY && !paused ) { - phase = PLAY; - dx = 0.4; - dy = -_sqrt(1.0-dx*dx); + // Fixed 8 ms simulation, split further to travel at most two pixels per step. + // Even at maximum boosted speed a ball cannot tunnel through an 18 px brick. + val _tick = { int direction -> + if( paused || phase == WON || phase == LOST ) return 0; + _paddle(paddle+direction*6.4); + if( phase == READY ) { x = paddle; y = PADDLE_Y-RADIUS-6; return 0; } + f64 seconds = 0.008; // 8ms + while( seconds > 0.000000001 && phase == PLAY ) { + f64 dt = 2.0/speed; + if( dt > seconds ) dt = seconds; + seconds -= dt; + f64 oldX = x, oldY = y; + x += dx*speed*dt; + y += dy*speed*dt; + if( x < LEFT+RADIUS && dx < 0.0 ) { + x = 2.0*(LEFT+RADIUS)-x; dx = -dx; _cue(1); + } + if( x > RIGHT-RADIUS && dx > 0.0 ) { + x = 2.0*(RIGHT-RADIUS)-x; dx = -dx; _cue(1); + } + if( y < TOP+RADIUS && dy < 0.0 ) { + y = 2.0*(TOP+RADIUS)-y; dy = -dy; + backWall = true; _cue(1); + } + if( dy > 0.0 && oldY <= PADDLE_Y-RADIUS && y >= PADDLE_Y-RADIUS && + paddle-PADDLE_HALF-RADIUS <= x <= paddle+PADDLE_HALF+RADIUS ) _rebound(); + if( y > BOTTOM+RADIUS ) { + _lostBall(); + break; + } + // Map the center into the grid of radius-expanded brick boxes. + // Their overlap is smaller than one cell: at most 2 x 2 candidates. + if( x <= LEFT+2 -RADIUS || x >= LEFT+2 +(COLS-1)*BRICK_DX+BRICK_W+RADIUS || + y <= BRICK_TOP-RADIUS || y >= BRICK_TOP+(ROWS-1)*BRICK_DY+BRICK_H+RADIUS ) + continue; + int col = (x-(LEFT+2 -RADIUS))/BRICK_DX; + int row = (y-(BRICK_TOP-RADIUS))/BRICK_DY; + bool hit = false; + for( int r = row > 0 ? row-1 : 0; r <= row && r < ROWS && !hit; r++ ) + for( int c = col > 0 ? col-1 : 0; c <= col && c < COLS; c++ ) { + int i = r*COLS+c; + if( bricks[i] == 0 ) continue; + f64 left = LEFT+2 +c*BRICK_DX-RADIUS; + f64 top = BRICK_TOP+r*BRICK_DY-RADIUS; + f64 right = left+BRICK_W+2*RADIUS; + f64 bottom= top +BRICK_H+2*RADIUS; + if( left < x < right && top < y < bottom ) { + bool back = false; + // Resolve the face crossed this step, using the previous center. + // Vertical faces win a simultaneous corner contact. + if( oldY <= top && dy > 0.0 ) { + y = top; dy = -dy; back = true; + } else if( oldY >= bottom && dy < 0.0 ) { + y = bottom; dy = -dy; + } else if( oldX <= left && dx > 0.0 ) { + x = left; dx = -dx; + } else if( oldX >= right && dx < 0.0 ) { + x = right; dx = -dx; + } else continue; // Missed all bricks + _brick(i,back); + hit = true; + break; + } + } } return 0; }; + // Several sound things might happen in this event, pick one int sound = 0; // Increasing priority: wall, paddle, brick, boost, miss, win. val _cue = { int snd -> if( snd > sound ) sound = snd; }; @@ -41,11 +99,10 @@ struct Game { f64 paddle; val _paddle = { f64 x -> paddle = x < LEFT +PADDLE_HALF ? LEFT +PADDLE_HALF : - x > RIGHT-PADDLE_HALF ? RIGHT+PADDLE_HALF : + x > RIGHT-PADDLE_HALF ? RIGHT-PADDLE_HALF : x; }; - // Hit the paddle val _rebound = { -> // Where along the paddle did you hit? @@ -62,6 +119,7 @@ struct Game { }; // Hit a brick? + int soundDepth; // Which brick depth for the boop val _brick = { int index, bool back -> if( bricks[index] == 0 ) return 0; // No brick to hit! bricks[index] = 0; // Clear brick @@ -70,13 +128,25 @@ struct Game { remaining--; // Fewer bricks to iwn if( row > depth ) depth = row; // Max depth seen // Brick-hit sound - if( sound <= 3 ) { soundDepth = row; _cue(3); } - if( back && backWall && !boosted ) { boosted = true; _cue(4); } - if( remaining == 0 ) { phase = WON; _cue(6); } + if( sound <= 3 ) { _cue(3); soundDepth = row; } + if( back && backWall && !boosted ) { _cue(3); boosted = true; } + if( remaining == 0 ) { _cue(6); phase = WON; } _speed(); return 0; }; + // Missed ball with paddle + val _lostBall = { -> + lives--; + phase = lives == 0 ? LOST : READY; + backWall = false; + depth = 1; + boosted = false; + _speed(); + x = paddle; y = PADDLE_Y-RADIUS-6; + _cue(5); + }; + // Reset for another round val _reset = { -> for( int i = 0; i < bricks#; i++ ) bricks[i] = 1; @@ -89,74 +159,133 @@ struct Game { backWall = false; paused = false; sound = 0; + soundDepth = 1; paddle = 504.0; x = paddle; y = PADDLE_Y-RADIUS-6; _speed(); }; -}; -// Fixed 8 ms simulation, split further to travel at most two pixels per step. -// Even at maximum boosted speed a ball cannot tunnel through an 18 px brick. -val _tick = { !Game g, int direction -> - if( g.paused || g.phase == WON || g.phase == LOST ) return 0; - g._paddle(g.paddle+direction*6.4); - if( g.phase == READY ) { g.x = g.paddle; g.y = PADDLE_Y-RADIUS-6; return 0; } - f64 seconds = 0.008; - while( seconds > 0.000000001 && g.phase == PLAY ) { - f64 dt = 2.0/g.speed; - if( dt > seconds ) dt = seconds; - seconds -= dt; - f64 oldX = g.x, oldY = g.y; - g.x += g.dx*g.speed*dt; - g.y += g.dy*g.speed*dt; - if( g.x < LEFT+RADIUS && g.dx < 0.0 ) { - g.x = 2.0*(LEFT+RADIUS)-g.x; g.dx = -g.dx; g._cue(1); - } - if( g.x > RIGHT-RADIUS && g.dx > 0.0 ) { - g.x = 2.0*(RIGHT-RADIUS)-g.x; g.dx = -g.dx; g._cue(1); - } - if( g.y < TOP+RADIUS && g.dy < 0.0 ) { - g.y = 2.0*(TOP+RADIUS)-g.y; g.dy = -g.dy; - g.backWall = true; g._cue(1); + // -------------------------------- + // Drawing code + val _hud = { !sys.draw.Canvas c -> + c.text(24,22,"BREAKOUT",5,WHITE); + c.text(264,20,"SCORE",2,MUTED); + _number(c,264,40,score,3,4,WHITE); + c.text(424,20,"BRICKS",2,MUTED); + _number(c,424,40,remaining,3,4,WHITE); + c.text(584,20,"BALLS",2,MUTED); + for( int i = 0; i < lives; i++ ) c.fillCircle(590+23*i,50,5,CYAN); + c.text(714,20,"SPEED",2,MUTED); + int ispeed = speed; + _number(c,714,40,ispeed,3,4,boosted ? COLORS[0] : WHITE); + c.text(838,40,boosted ? "BOOST!" : "NORMAL",3,boosted ? COLORS[0] : MUTED); + }; + + + val _bricks = { !sys.draw.Canvas c -> + c.fillRect(LEFT-4,TOP-4,RIGHT-LEFT+8,4,MUTED); + c.fillRect(LEFT-4,TOP,4,BOTTOM-TOP,0x29435a); + c.fillRect(RIGHT,TOP,4,BOTTOM-TOP,0x29435a); + c.text(376,108,boosted ? "BACK WALL BOOST ACTIVE" : "BREAK THROUGH FOR A BOOST",2,boosted ? COLORS[0] : MUTED); + for( int i = 0; i < bricks#; i++ ) { + if( bricks[i] == 0 ) continue; + int row = i/COLS; + i64 x = LEFT+2+(i-row*COLS)*BRICK_DX, y = BRICK_TOP+row*BRICK_DY; + c.fillRect(x,y,BRICK_W,BRICK_H,COLORS[row]); + c.fillRect(x,y,BRICK_W,2,WHITE); } - if( g.dy > 0.0 && oldY <= PADDLE_Y-RADIUS && g.y >= PADDLE_Y-RADIUS && - g.paddle-PADDLE_HALF-RADIUS <= g.x <= g.paddle+PADDLE_HALF+RADIUS ) g._rebound(); - if( g.y > BOTTOM+RADIUS ) { - g.lives--; - g.phase = g.lives == 0 ? LOST : READY; - g.backWall = false; - g.depth = 1; - g.boosted = false; - g._speed(); - g.x = g.paddle; g.y = PADDLE_Y-RADIUS-6; - g._cue(5); - break; + }; + + val _overlay = { !sys.draw.Canvas c -> + if( phase != PLAY || paused ) { + c.fillRect(246,422,516,108,0x17263a).rect(246,422,516,108,2,0x29435a); + u8[~] title = paused ? "PAUSED" : phase == WON ? "WALL CLEARED!" : phase == LOST ? "GAME OVER" : "READY?"; + c.text(504-(title#*4-1)*4/2,442,title,4,WHITE); + u8[~] help = paused ? "P TO CONTINUE" : phase == WON || phase == LOST ? "R TO PLAY AGAIN" : "SPACE OR CLICK TO SERVE"; + c.text(504-(help#*4-1)*2/2,490,help,2,CYAN); } - for( int i = 0; i < g.bricks#; i++ ) { - if( g.bricks[i] == 0 ) continue; - f64 left = LEFT+2+(i-i/COLS*COLS)*BRICK_DX-RADIUS; - f64 top = BRICK_TOP+(i/COLS)*BRICK_DY-RADIUS; - f64 right = left+BRICK_W+2*RADIUS, bottom = top+BRICK_H+2*RADIUS; - if( left < g.x < right && top < g.y < bottom ) { - bool back = false; - // Resolve the face crossed this step, using the previous center. - // Vertical faces win a simultaneous corner contact. - if( oldY <= top && g.dy > 0.0 ) { - g.y = top; g.dy = -g.dy; back = true; - } else if( oldY >= bottom && g.dy < 0.0 ) { - g.y = bottom; g.dy = -g.dy; - } else if( oldX <= left && g.dx > 0.0 ) { - g.x = left; g.dx = -g.dx; - } else if( oldX >= right && g.dx < 0.0 ) { - g.x = right; g.dx = -g.dx; - } else continue; - g._brick(i,back); - break; + return 0; + }; + + // With this Canvas, we draw into the blank canvas, then "present". The + // underlying graphics lib copies the bits, and we are then free to clear + // and draw again while the old bits continue to be presented. + val _draw = { !sys.draw.Canvas c -> + c.clear(BACKGROUND); + _hud(c); + _bricks(c); + i64 paddle = paddle, ballX = x, ballY = y; + int glowRadius = RADIUS+3; + c.fillRect(paddle-PADDLE_HALF,PADDLE_Y,2*PADDLE_HALF,12,CYAN); + c.fillRect(paddle-PADDLE_HALF+4,PADDLE_Y+2,2*PADDLE_HALF-8,2,WHITE); + c.fillCircle(ballX,ballY,glowRadius,boosted ? 0x672a42 : 0x224b61); + c.fillCircle(ballX,ballY,RADIUS,boosted ? COLORS[0] : WHITE); + _overlay(c); + c.text(24,738,"ARROWS / MOUSE: MOVE SPACE / CLICK: SERVE P: PAUSE R: RESTART Q / ESC: QUIT",2,MUTED); + }; + + // Served and still holding serve key + bool serveHeld; + // Paused and still holding pause key + bool pauseHeld; + // Restart and still holding restart key + bool restartHeld; + // Last mouse position; track moving mouse + int lastMouse; + + // Read and act on the controls + val _input = { -> + val gfx = sys.gfx; + bool serve = gfx.key(' ') || gfx.button(gfx.LEFT); + bool pause = gfx.key('P'); + bool restart= gfx.key('R'); + + // Keyboard direction and mouse position + int direction = (gfx.key(KEY_RIGHT) ? 1 : 0)-(gfx.key(KEY_LEFT) ? 1 : 0); + int mouse = gfx.mouseX(); + + // Restart requested + restartHeld = debounce( restart, restartHeld, { -> _reset(); sys.audio.stop(); } ); + + // Playing or ready to play game? + if( phase == READY || phase == PLAY ) { + // Flip pause state + pauseHeld = debounce( pause, pauseHeld, { -> paused = !paused; } ); + + // If no key and mouse moved, set paddle to mouse + if( mouse != lastMouse && direction == 0 && !paused ) { + _paddle(mouse*1.0); // Set paddle to mouse + // If READY and not PLAY, ball tracks paddle directly + if( phase == READY ) x = paddle; } + lastMouse = mouse; + } - } - return 0; + + // Serve requested + serveHeld = debounce( serve, serveHeld, _serve ); + + // Return key direction, so the tick tracking can slide paddle smoothly + return direction; + }; + + val debounce = { bool b, bool bHeld, { int } action -> + if( b && !bHeld ) // Action requested and repeating + action(); // Do action + b; // Dont action again, until 'b' is released + }; + + + // Serve the ball; give it a nominal direction and speed + val _serve = { -> + if( phase == READY && !paused ) { + phase = PLAY; + dx = 0.4; + dy = -_sqrt(1.0-dx*dx); + } + return 0; + }; }; // Fixed-width decimal labels avoid allocating new strings on every frame. @@ -166,55 +295,6 @@ val _number = { !sys.draw.Canvas c, i64 x, i64 y, int value, int digits, int sca value /= 10; } }; -val _hud = { !sys.draw.Canvas c, Game g -> - c.text(24,22,"BREAKOUT",5,WHITE); - c.text(264,20,"SCORE",2,MUTED); - _number(c,264,40,g.score,3,4,WHITE); - c.text(424,20,"BRICKS",2,MUTED); - _number(c,424,40,g.remaining,3,4,WHITE); - c.text(584,20,"BALLS",2,MUTED); - for( int i = 0; i < g.lives; i++ ) c.fillCircle(590+23*i,50,5,CYAN); - c.text(714,20,"SPEED",2,MUTED); - int speed = g.speed; - _number(c,714,40,speed,3,4,g.boosted ? COLORS[0] : WHITE); - c.text(838,40,g.boosted ? "BOOST!" : "NORMAL",3,g.boosted ? COLORS[0] : MUTED); -}; -val _bricks = { !sys.draw.Canvas c, Game g -> - c.fillRect(LEFT-4,TOP-4,RIGHT-LEFT+8,4,MUTED); - c.fillRect(LEFT-4,TOP,4,BOTTOM-TOP,0x29435a); - c.fillRect(RIGHT,TOP,4,BOTTOM-TOP,0x29435a); - c.text(376,108,g.boosted ? "BACK WALL BOOST ACTIVE" : "BREAK THROUGH FOR A BOOST",2,g.boosted ? COLORS[0] : MUTED); - for( int i = 0; i < g.bricks#; i++ ) { - if( g.bricks[i] == 0 ) continue; - int row = i/COLS; - i64 x = LEFT+2+(i-row*COLS)*BRICK_DX, y = BRICK_TOP+row*BRICK_DY; - c.fillRect(x,y,BRICK_W,BRICK_H,COLORS[row]); - c.fillRect(x,y,BRICK_W,2,WHITE); - } -}; -val _overlay = { !sys.draw.Canvas c, Game g -> - if( g.phase != PLAY || g.paused ) { - c.fillRect(246,422,516,108,0x17263a).rect(246,422,516,108,2,0x29435a); - u8[~] title = g.paused ? "PAUSED" : g.phase == WON ? "WALL CLEARED!" : g.phase == LOST ? "GAME OVER" : "READY?"; - c.text(504-(title#*4-1)*4/2,442,title,4,WHITE); - u8[~] help = g.paused ? "P TO CONTINUE" : g.phase == WON || g.phase == LOST ? "R TO PLAY AGAIN" : "SPACE OR CLICK TO SERVE"; - c.text(504-(help#*4-1)*2/2,490,help,2,CYAN); - } - return 0; -}; -val _draw = { !sys.draw.Canvas c, Game g -> - c.clear(BACKGROUND); - _hud(c,g); - _bricks(c,g); - i64 paddle = g.paddle, ballX = g.x, ballY = g.y; - int glowRadius = RADIUS+3; - c.fillRect(paddle-PADDLE_HALF,PADDLE_Y,2*PADDLE_HALF,12,CYAN); - c.fillRect(paddle-PADDLE_HALF+4,PADDLE_Y+2,2*PADDLE_HALF-8,2,WHITE); - c.fillCircle(ballX,ballY,glowRadius,g.boosted ? 0x672a42 : 0x224b61); - c.fillCircle(ballX,ballY,RADIUS,g.boosted ? COLORS[0] : WHITE); - _overlay(c,g); - c.text(24,738,"ARROWS / MOUSE: MOVE SPACE / CLICK: SERVE P: PAUSE R: RESTART Q / ESC: QUIT",2,MUTED); -}; // Short, fading triangle waves; the array initializer builds PCM once at startup. val _tone = { int period -> @@ -226,27 +306,7 @@ val _tone = { int period -> int handle = sys.audio.load(samples); return handle < 0 ? sys.audio.BOOP : handle; }; -struct Controls { - bool serveHeld = false, pauseHeld = false, restartHeld = false; - int lastMouse = 504; -}; -val _input = { !Game g, !Controls keys -> - val gfx = sys.gfx; - bool serve = gfx.key(' ') || gfx.button(gfx.LEFT); - bool pause = gfx.key('P'), restart = gfx.key('R'); - if( restart && !keys.restartHeld ) { g._reset(); sys.audio.stop(); } - if( pause && !keys.pauseHeld && (g.phase == READY || g.phase == PLAY) ) g.paused = !g.paused; - int direction = (gfx.key(KEY_RIGHT) ? 1 : 0)-(gfx.key(KEY_LEFT) ? 1 : 0); - int mouse = gfx.mouseX(); - if( mouse != keys.lastMouse && direction == 0 && !g.paused && (g.phase == READY || g.phase == PLAY) ) { - g._paddle(mouse*1.0); - if( g.phase == READY ) g.x = g.paddle; - } - keys.lastMouse = mouse; - if( serve && !keys.serveHeld ) g._serve(); - keys.serveHeld = serve; keys.pauseHeld = pause; keys.restartHeld = restart; - return direction; -}; + // Fast low accuracy sqrt val _sqrt = { f64 n -> @@ -256,32 +316,43 @@ val _sqrt = { f64 n -> }; val main = { u8[~][] args -> - val gfx = sys.gfx, audio = sys.audio, time = sys.time; - val c = new sys.draw.Canvas(WIDTH,HEIGHT); - val g = new Game; - if( gfx.open("Simple Breakout",c.width,c.height) != gfx.OK ) return 1; + // Shortcuts + val gfx = sys.gfx, audio = sys.audio; + + // Open a window + if( gfx.open("Simple Breakout",WIDTH,HEIGHT) != gfx.OK ) return 1; + + // Audio setup; pre-specify a series of sounds audio.open(); // Optional: a missing audio backend leaves play silent. val sounds = new int[~]{0,_tone(120),_tone(80),audio.BOOP,audio.WIN,audio.LOSE,audio.WIN}; // Bottom to top: roughly 333..800 Hz, below the old 1000 Hz brick tone. val brickSounds = new int[~ROWS]{int i -> int period = 144-i*12; _tone(period);}; - val keys = new Controls; - keys.lastMouse = gfx.mouseX(); + + // Setup drawing board + val c = new sys.draw.Canvas(WIDTH,HEIGHT); + + // Setup game contents + val g = new Game; + g._reset(); + + // int frames = 0, status = gfx.OK; - i64 last_ms = time.monotonic(), pending_ms = 0, frame_ms = last_ms; + i64 last_ms = sys.time.monotonic(), pending_ms = 0, frame_ms = last_ms; + while( status == gfx.OK ) { if( gfx.key('Q') || gfx.key(gfx.ESCAPE) ) break; - int direction = _input(g,keys); - i64 now_ms = time.monotonic(), elapsed_ms = now_ms-last_ms; + int direction = g._input(); + i64 now_ms = sys.time.monotonic(), elapsed_ms = now_ms-last_ms; last_ms = now_ms; // Drop long stalls instead of teleporting the ball after a window drag. pending_ms += elapsed_ms < 0 ? 0 : elapsed_ms > 64 ? 64 : elapsed_ms; - while( pending_ms >= STEP_ms ) { _tick(g,direction); pending_ms -= STEP_ms; } + while( pending_ms >= STEP_ms ) { g._tick(direction); pending_ms -= STEP_ms; } if( g.sound != 0 ) { audio.play(g.sound == 3 ? brickSounds[g.soundDepth-1] : sounds[g.sound]); g.sound = 0; } if( now_ms >= frame_ms ) { - _draw(c,g); + g._draw(c); status = gfx.present(c.pixels); frame_ms = now_ms+FRAME_ms; if( args# > 1 && ++frames == 10 ) break; diff --git a/chapter31/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java b/chapter31/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java index 3306ff8f..882a0f8a 100644 --- a/chapter31/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java +++ b/chapter31/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java @@ -150,7 +150,8 @@ public abstract class ParseAll { } private static void addRequiredTypes(CodeGen code, CompUnit cunit) { - for( String typeName : code.P._requiredTypes ) { + for( var required : code.P._requiredTypes.entrySet() ) { + String typeName = required.getKey(); // A forward use can have been resolved later in this source file. if( Parser.TYPES.get(typeName) instanceof TypeStruct ts && !ts._fref ) continue; try { @@ -158,7 +159,7 @@ public abstract class ParseAll { if( xcunit != null ) cunit.addDep(xcunit); else - assert false : "expected to find required type"; + throw Parser.error("Unknown type '"+typeName+"'",required.getValue()); } catch( IOException ioe ) { throw new RuntimeException(ioe); } diff --git a/chapter31/src/main/java/com/seaofnodes/simple/node/CallNode.java b/chapter31/src/main/java/com/seaofnodes/simple/node/CallNode.java index 690acfb9..9e9c5224 100644 --- a/chapter31/src/main/java/com/seaofnodes/simple/node/CallNode.java +++ b/chapter31/src/main/java/com/seaofnodes/simple/node/CallNode.java @@ -256,6 +256,15 @@ public class CallNode extends CFGNode { // Constructors return private memory and carry hidden self and // selfMem arguments ahead of the source-level arguments. int argno = i - (tfp.ret() instanceof TypeMem ? 2 : 0); + // Access qualifiers are not shown by the structural type dump. + // Report the differing permission instead of two huge shapes. + if( arg(i+2)._type instanceof TypeMemPtr actual && actual._ro && + tfp.arg(i) instanceof TypeMemPtr formal && !formal._ro ) { + String who = i == 0 ? name() == null ? "Receiver" : "Receiver for method '"+name()+"'" + : "Argument #"+argno; + return Parser.error(who+" is read-only "+actual._obj.str()+ + "; expected writable "+formal._obj.str(),_loc); + } return Parser.error( "Argument #" + argno + " isa " + arg(i+2)._type + ", but must be a " + tfp.arg(i), _loc ); } diff --git a/chapter31/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/chapter31/src/main/java/com/seaofnodes/simple/node/LoadNode.java index 35db4c2f..14efa315 100644 --- a/chapter31/src/main/java/com/seaofnodes/simple/node/LoadNode.java +++ b/chapter31/src/main/java/com/seaofnodes/simple/node/LoadNode.java @@ -98,7 +98,15 @@ public class LoadNode extends MemOpNode { t = mem._alias==_alias ? mem._t : actual==null ? TypeScalar.BOT : actual._t; } else if( mem._alias==_alias ) t = t.join(mem._t); - if( !mem._one ) t = mem.outside(t); + // A final method's identity comes from its declaration, not from an + // unknown outside writer. It remains callable on a local object even + // when that function has never escaped to outside code. + if( !mem._one ) { + Type declared = Parser.TYPES.get(tmp._obj._name); + Field field = declared instanceof TypeStruct obj ? obj.field(_name) : null; + if( !(field != null && field._final && field._t instanceof TypeFunPtr fun && fun.isConstant()) ) + t = mem.outside(t); + } // Store forwarding is also a value fact during SCCP. Waiting for the // later rewrite can turn a known non-null stored pointer into a // nullable declaration, then BOTTOM at the next dereference. diff --git a/chapter31/src/main/java/com/seaofnodes/simple/node/StoreNode.java b/chapter31/src/main/java/com/seaofnodes/simple/node/StoreNode.java index 010d5f61..204e0c4b 100644 --- a/chapter31/src/main/java/com/seaofnodes/simple/node/StoreNode.java +++ b/chapter31/src/main/java/com/seaofnodes/simple/node/StoreNode.java @@ -243,6 +243,10 @@ public class StoreNode extends MemOpNode { private byte storageType() { if( !(ptr()._type instanceof TypeMemPtr tmp) ) return 0; + // Completing a forward declaration changes TYPES without changing + // this Store's inputs. Wait for the pointer's type upgrade, which + // queues its users, before consulting the completed declaration. + if( tmp._obj._fref ) return 0; TypeStruct obj = (TypeStruct)Parser.TYPES.get(tmp._obj._name); Field fld = obj.field(_name); if( fld == null || fld._t == null ) diff --git a/chapter31/src/main/java/com/seaofnodes/simple/parser/Declarations.java b/chapter31/src/main/java/com/seaofnodes/simple/parser/Declarations.java index 13fa59b8..473a7c24 100644 --- a/chapter31/src/main/java/com/seaofnodes/simple/parser/Declarations.java +++ b/chapter31/src/main/java/com/seaofnodes/simple/parser/Declarations.java @@ -91,6 +91,10 @@ final class Declarations { // expr is a constant function if( t instanceof TypeFunPtr decl && expr._type instanceof TypeFunPtr tfp && tfp.isConstant() ) { t = decl.makeFrom(tfp.fidx()); + // The explicit signature specifies source parameters/return; + // the receiver of a method body is still inferred. + if( expr instanceof FunPtrNode && P.structs.inStructDeclaration() ) + t = ((TypeFunPtr)t).makeFrom(tfp.arg(0),0); FunNode fun = P._code.link(tfp); if( fun != null ) fun.setName(name); // Assign debug name to Simple function diff --git a/chapter31/src/main/java/com/seaofnodes/simple/parser/Functions.java b/chapter31/src/main/java/com/seaofnodes/simple/parser/Functions.java index 8047fabc..84bd509e 100644 --- a/chapter31/src/main/java/com/seaofnodes/simple/parser/Functions.java +++ b/chapter31/src/main/java/com/seaofnodes/simple/parser/Functions.java @@ -14,6 +14,11 @@ final class Functions { // Merge all function exits here. ScopeNode _returnScope; + // Instance method headers are available immediately; their bodies need + // the complete field scope, just like explicitly declared constructors. + private record Method(FunPtrNode ptr, Lexer body, Lexer loc, BitSet finals, String[] ids) {} + private Ary<Method> _methods; + Functions(Parser P) { this.P = P; } /** @@ -29,6 +34,7 @@ final class Functions { Ary<String> ids = new Ary<>(String.class); BitSet finals = new BitSet(); Type self = P.types.implicitSelfType(); + if( P.structs.inStructDeclaration() ) self = self.makeRO(); ts.push(self); ids.push(self == TypePtr.PTR ? "#self" : "self"); @@ -38,10 +44,49 @@ final class Functions { parameters(ts,ids,finals,false); // Make a concrete function type, with a fidx TypeFunPtr tfp = TypeFunPtr.make1((byte)2,true,ts.asAry(),Type.BOTTOM,P._code.fidx(P._ref._cname)); + if( P.structs.inStructDeclaration() ) { + FunPtrNode ptr = (FunPtrNode)new FunPtrNode(tfp,P._code._start,null).peephole().keep(); + _methods.push(new Method(ptr,P.loc(),loc,finals,ids.asAry())); + P._lexer.skipTo('{','}',"method body"); + return ptr; + } ReturnNode ret = parseFunctionBody(tfp,loc,finals,ids.asAry()); return new FunPtrNode(tfp,P._code._start,ret).peephole(); } + private void finishMethods() { + if( _methods == null ) return; + Ary<Method> methods = _methods; + Lexer after = P._lexer; + for( Method method : methods ) { + P._lexer = new Lexer(method.body()); + TypeFunPtr sig = (TypeFunPtr)method.ptr()._con; + ReturnNode ret = parseFunctionBody(sig,method.loc(),method.finals(),method.ids()); + method.ptr().setDef(1,ret); + P.require("}"); + } + Ary<FunNode> functions = new Ary<>(FunNode.class); + for( Method method : methods ) functions.add(method.ptr().fun()); + MethodAccess.infer(functions); + for( Method method : methods ) { + ReturnNode ret = method.ptr().ret(); + TypeFunPtr sig = ret.fun().sig(); + method.ptr().peephole(); + // Declarations saw the header before its FunNode existed. + for( Var var : P._scope._vars ) + if( var.type() instanceof TypeFunPtr tfp && tfp.isConstant() && tfp.fidx() == sig.fidx() ) { + ret.fun().setName(var._name); + var.refineType(var.type().join(method.ptr()._type)); + TypeStruct self = P._scope.constructorSelf(); + TypeStruct shape = (TypeStruct)Parser.TYPES.get(self._name); + Parser.TYPES.put(shape._name,shape.replace(shape.field(var._name).makeFrom(var.type()))); + break; + } + method.ptr().unkeep(); + } + P._lexer = after; + } + Node parseConstructor(String name, boolean explicit) { P.require("{"); Ary<Type> ts = new Ary<>(Type.class); @@ -203,6 +248,8 @@ final class Functions { Structs.Constructor outerCtor = P.structs._pendingCtor; P.structs._pendingCtor = null; boolean isInit = FunNode.isInit(funName); + Ary<Method> outerMethods = _methods; + _methods = FunNode.isInstance(funName) ? new Ary<>(Method.class) : null; boolean isCtor = isInit || P.structs.isExplicitConstructorName(funName); String typeName = isCtor ? ((TypeMemPtr)sig._sig[0])._obj._name : null; @@ -235,7 +282,11 @@ final class Functions { nvar = P.structs.updateSelfAsFieldsDiscovered( typeName, nvar ); } - if( isInit ) P.structs.finishConstructor(); + if( isInit ) { + finishMethods(); + P.structs.finishConstructor(); + } + _methods = outerMethods; P.structs._pendingCtor = outerCtor; ParmNode self = fun.parm(2); @@ -303,8 +354,14 @@ final class Functions { // All args, "as-if" called externally for( int i = 0; i<ids.length; i++ ) { Type t = sig.arg(i); + // Parse an implicit receiver with write access, then infer the + // public requirement from its uses once every method is present. + if( i == 0 && ids[0].equals("self") && !FunNode.isInit(funName) && + !(sig.ret() instanceof TypeMem) && t instanceof TypeMemPtr ptr ) + t = ptr.withAccess(false); // Args take a default input if called from Start/external-world - Node parm = new ParmNode(ids[i],i+2,t,fun,ConstantNode.seed(fun.defaultArg(i)).peephole()).peephole(); + Type seed = i == 0 && t instanceof TypeMemPtr ? t : fun.defaultArg(i); + Node parm = new ParmNode(ids[i],i+2,t,fun,ConstantNode.seed(seed).peephole()).peephole(); if( t instanceof TypeMemPtr ptr && ptr._partial ) parm = new ConstructNode(parm).peephole(); P._scope.define(ids[i], t, (i==0 && ids[0]=="self") || (finals!=null && finals.get(i)), parm, loc); } diff --git a/chapter31/src/main/java/com/seaofnodes/simple/parser/MethodAccess.java b/chapter31/src/main/java/com/seaofnodes/simple/parser/MethodAccess.java new file mode 100644 index 00000000..099ba036 --- /dev/null +++ b/chapter31/src/main/java/com/seaofnodes/simple/parser/MethodAccess.java @@ -0,0 +1,130 @@ +package com.seaofnodes.simple.parser; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Set; + +/** Infer the access required through an instance method's receiver. */ +final class MethodAccess { + private final FunNode _fun; + private final Set<Node> _aliases = Collections.newSetFromMap(new IdentityHashMap<>()); + private final Set<MethodAccess> _calls = new HashSet<>(); + private boolean _writes; + + private MethodAccess(FunNode fun) { _fun = fun; } + + static void infer(Ary<FunNode> functions) { + HashMap<Integer,MethodAccess> methods = new HashMap<>(); + for( FunNode fun : functions ) methods.put(fun.sig().fidx(),new MethodAccess(fun)); + for( MethodAccess method : methods.values() ) method.scan(methods); + boolean changed; + do { + changed = false; + for( MethodAccess method : methods.values() ) + if( !method._writes ) + for( MethodAccess callee : method._calls ) + if( callee._writes ) { method._writes = true; changed = true; break; } + } while( changed ); + for( MethodAccess method : methods.values() ) method.finish(); + } + + private void scan(HashMap<Integer,MethodAccess> methods) { + if( _fun.parm(2) == null ) return; // Unused receiver. + ArrayDeque<Node> work = new ArrayDeque<>(); + work.add(_fun.parm(2)); + while( !work.isEmpty() ) { + Node value = work.remove(); + if( !_aliases.add(value) ) continue; + for( Node use : value._outputs ) { + if( use == null || use instanceof ReadOnlyNode ) continue; + if( use instanceof StoreNode store ) { + if( store.ptr() == value || store.val() == value ) _writes = true; + } else if( use instanceof CallNode call ) { + for( int i = 2; i < call.nIns()-1; i++ ) + if( call.in(i) == value ) call(call,i-2,methods); + } else if( use instanceof ReturnNode ret ) { + // Returning a writable alias must not grant write access + // to an object reached through a read-only receiver. + if( ret.expr() == value ) _writes = true; + } else if( use instanceof LoadNode load && load.ptr() == value ) { + Type type = type(load); + if( type instanceof TypeMemPtr ptr ) { + if( !ptr._ro ) work.add(load); + } else if( type == Type.BOTTOM || type == TypeScalar.BOT ) _writes = true; + } else if( use instanceof PhiNode || use instanceof GuardNode ) { + work.add(use); + } else if( use instanceof ConvertNode convert ) { + if( !(convert.dst() instanceof TypeMemPtr ptr && ptr._ro) ) _writes = true; + } else if( use instanceof BoolNode || use instanceof NotNode || + use instanceof FldOffNode || use instanceof ScopeNode || use instanceof CLInitNode ) { + // Comparisons and field offsets do not expose write access. + } else { + // Includes raw-address casts and unrecognized escapes. + _writes = true; + } + } + } + } + + private void call(CallNode call, int arg, HashMap<Integer,MethodAccess> methods) { + // Every member of this struct is now declared. An unresolved bare + // function name belongs to an outer scope, whose hidden slot is not self. + if( arg == 0 && call.fptr() instanceof FRefNode ) return; + Type type = type(call.fptr()); + if( !(type instanceof TypeFunPtr sig) || arg >= sig.nargs() ) { _writes = true; return; } + if( arg == 0 && sig.arg(0) == TypePtr.PTR ) return; + if( arg == 0 ) { + if( XInt.isHigh(sig.fidxs()) ) { _writes = true; return; } + for( int fidx = XInt.next(sig.fidxs(),-1); fidx >= 0; fidx = XInt.next(sig.fidxs(),fidx) ) { + MethodAccess local = methods.get(fidx); + if( local != null ) _calls.add(local); + else { + FunNode target = CodeGen.CODE.link(fidx); + Type receiver = target == null ? sig.arg(0) : target.sig().arg(0); + if( !(receiver instanceof TypeMemPtr ptr && ptr._ro) ) _writes = true; + } + } + return; + } + if( !(sig.arg(arg) instanceof TypeMemPtr ptr && ptr._ro) ) _writes = true; + } + + private static Type type(Node value) { + // Flow types can still be BOTTOM while the parser discovers fields. + if( value instanceof LoadNode load ) { + TypeStruct obj = load.off() instanceof ConFldOffNode off ? off._ts : + type(load.ptr()) instanceof TypeMemPtr ptr ? ptr._obj : null; + if( obj != null ) { + if( !obj.isAry() && Parser.TYPES.get(obj._name) instanceof TypeStruct latest ) obj = latest; + Field field = obj.field(load._name); + if( field != null ) return field._t; + } + } + return value._type; + } + + private void finish() { + ParmNode self = _fun.parm(2); + if( self == null ) return; + TypeMemPtr receiver = ((TypeMemPtr)self._declaredType).withAccess(!_writes); + _fun.setSig(_fun.sig().makeFrom(receiver,0)); + if( _writes ) return; + self._declaredType = receiver; + self.setDef(1,ConstantNode.seed(receiver).peephole()); + // These parser-time assumptions are settled before Iter/SCCP. No + // writable alias escapes a method classified as read-only. + for( Node alias : _aliases ) + if( alias._type instanceof TypeMemPtr ptr ) { + alias._type = ptr.makeRO(); + CodeGen.CODE.add(alias); + CodeGen.CODE.addAll(alias._outputs); + } + } +} diff --git a/chapter31/src/main/java/com/seaofnodes/simple/parser/Parser.java b/chapter31/src/main/java/com/seaofnodes/simple/parser/Parser.java index 3e7bb6b8..9c18385d 100644 --- a/chapter31/src/main/java/com/seaofnodes/simple/parser/Parser.java +++ b/chapter31/src/main/java/com/seaofnodes/simple/parser/Parser.java @@ -7,7 +7,7 @@ import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.util.Ary; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashSet; +import java.util.LinkedHashMap; import java.util.Stack; /** Source-unit entry point and shared cursor, scope, and IR utilities. @@ -92,7 +92,7 @@ public class Parser { public final Stack<ScopeNode> _xScopes = new Stack<>(); // Layouts needed by field accesses in this source file. Resolve after // parsing, since a later local struct declaration can satisfy the use. - public final LinkedHashSet<String> _requiredTypes = new LinkedHashSet<>(); + public final LinkedHashMap<String,Lexer> _requiredTypes = new LinkedHashMap<>(); // Mapping from a type name to a Type. The string name matches // `type.str()` call. @@ -271,7 +271,7 @@ public class Parser { // Field offset for an unknown type public Node fldoff( Node n, String name ) { if( n._type instanceof TypeMemPtr ptr && ptr._obj._fref ) - _requiredTypes.add(ptr._obj._name); + _requiredTypes.putIfAbsent(ptr._obj._name,loc()); return new FldOffNode(n,name,loc()).peephole(); } public Node peep( Node n ) { diff --git a/chapter31/src/main/java/com/seaofnodes/simple/parser/README.md b/chapter31/src/main/java/com/seaofnodes/simple/parser/README.md index c70dcaba..e6281e50 100644 --- a/chapter31/src/main/java/com/seaofnodes/simple/parser/README.md +++ b/chapter31/src/main/java/com/seaofnodes/simple/parser/README.md @@ -14,6 +14,7 @@ productions to their entry points. | `Declarations.java` | Bindings, initializers, mutability modifiers, and native declarations. | | `Types.java` | Type syntax, qualified-name lookup, arrays, function types, and forward types. | | `Functions.java` | Function expressions, parameters, arguments, calls, and function entry/return graphs. | +| `MethodAccess.java` | Infers whether an instance method needs writable access through its receiver. | | `Structs.java` | Struct and constructor declarations, field discovery, and initialization Stores. | | `Construction.java` | Selects private self memory during parsing; its comment explains BOT fields, the TypeMemPtr qualifier, and checks deferred to TypeCheck. | | `References.java` | Bare-name and field resolution, implicit receivers, assignments, and forward references. | @@ -23,8 +24,16 @@ productions to their entry points. | `Var.java` | Lexical binding metadata: name, type, scope slot, and mutability. | Start with `Statements.parseStatement` or `Expressions.parseAssignment` to follow -the grammar. Loop updates are parsed after their bodies, and declared constructor -bodies after field discovery; `Lexer.skipTo` scans these deferred fragments. +the grammar. Loop updates are parsed after their bodies. Instance method headers +are parsed immediately, but their bodies and declared constructor bodies wait +until all fields are known. Methods can therefore use fields declared later in +the struct. `Lexer.skipTo` scans these deferred fragments. + +Method receivers accept read-only access unless the body needs writable access. +The check follows aliases, child references, array elements, and calls between +methods. Writing an independent argument does not require a writable receiver. +Returning or storing a writable receiver alias also requires writable access; +an explicit read-only view can be exposed without that requirement. `Parser` keeps the short cursor, scope/memory, IR-building, and diagnostic helpers used by these components. Independent helpers live with their data: class-object diff --git a/chapter31/src/main/java/com/seaofnodes/simple/parser/References.java b/chapter31/src/main/java/com/seaofnodes/simple/parser/References.java index f170cae2..7724103e 100644 --- a/chapter31/src/main/java/com/seaofnodes/simple/parser/References.java +++ b/chapter31/src/main/java/com/seaofnodes/simple/parser/References.java @@ -280,7 +280,7 @@ final class References { // Check for assign-update, "ptr.fld += expr" or "ary[idx]++" char ch = pre==0 ? P._lexer.matchOperAssign() : (char)pre; if( ch!=0 ) { - if( fld!=null && decl==Type.BOTTOM ) + if( fld!=null && decl==Type.BOTTOM && !ts._fref ) throw Parser.error( "'" + ts._name + "' is not fully initialized, field '" + fld._fname + "' needs to be set in a constructor",site ); Node op = P.expressions.opAssign(ch,load, decl ).keep(); mem.keep(); @@ -308,10 +308,15 @@ final class References { // Fields are lvalues in memory, including inside their initializer and // explicit constructor. Scope slots retain declaration metadata only. private Node fieldSelf(Var var) { - int kx = P._scope.kindx(var), fx = P._scope.enclosingFuncOrDecl(); + int kx = P._scope.kindx(var); ScopeNode.Kind kind = P._scope._kinds.at(kx); if( !(kind instanceof Kind.Func inst) || !FunNode.isInstance(inst._name) || var._idx<inst._lexSize+2 ) return null; - if( kx==fx || kx+1==fx ) return defaultSelf(); + // Nested functions have their own receiver parameter. Resolve fields + // through it even across intervening function and block scopes. + Node self = defaultSelf(); + Type owner = P._scope.var(inst._lexSize).type(); + if( self._type instanceof TypeMemPtr ptr && owner instanceof TypeMemPtr declared && + ptr._obj._name.equals(declared._obj._name) ) return self; return null; } diff --git a/chapter31/src/main/java/com/seaofnodes/simple/parser/Types.java b/chapter31/src/main/java/com/seaofnodes/simple/parser/Types.java index 827518fd..501924f0 100644 --- a/chapter31/src/main/java/com/seaofnodes/simple/parser/Types.java +++ b/chapter31/src/main/java/com/seaofnodes/simple/parser/Types.java @@ -37,6 +37,12 @@ final class Types { if( t0==Type.BOTTOM || t0==Type.TOP ) return t0; // var/val tname = typeName(tname).intern(); t0 = Parser.TYPES.get(tname); + // Bare forward types use the same file-relative name as `new`. + // A later sibling declaration must complete this very type. + if( t0 == null && tname.indexOf('.') == -1 ) { + tname = fullTypeName(tname); + t0 = Parser.TYPES.get(tname); + } } Type t1 = t0==null ? TypeStruct.forward(tname) : t0; @@ -135,9 +141,8 @@ final class Types { Type implicitSelfType() { TypeStruct self = P._scope.constructorSelf(); - // Ordinary instance methods accept the declared object shape. The - // constructor-only receiver starts at the low/default field values, - // and is built explicitly by parseConstructor(). + // Function types without a body must allow for writes. Function + // expressions can infer a weaker requirement in MethodAccess. return self == null ? TypePtr.PTR : TypeMemPtr.make(self); } diff --git a/chapter31/src/main/java/com/seaofnodes/simple/parser/Var.java b/chapter31/src/main/java/com/seaofnodes/simple/parser/Var.java index ddbb91d8..5361148f 100644 --- a/chapter31/src/main/java/com/seaofnodes/simple/parser/Var.java +++ b/chapter31/src/main/java/com/seaofnodes/simple/parser/Var.java @@ -33,6 +33,11 @@ public class Var { public Type type() { return _type; } public Type _type() { return _type; } // ScopeNode printer needs to have no side effects + void refineType(Type type) { + assert type.isa(_type) : _name+": "+_type+" -> "+type; + _type = type; + } + // Forward reference variables (not types) must be BOTTOM and // distinct from inferred variables public boolean isFRef() { return _fref; } diff --git a/chapter31/src/main/smp/sys/draw/Canvas.smp b/chapter31/src/main/smp/sys/draw/Canvas.smp index 2fbcf6ea..9ba3514a 100644 --- a/chapter31/src/main/smp/sys/draw/Canvas.smp +++ b/chapter31/src/main/smp/sys/draw/Canvas.smp @@ -3,7 +3,7 @@ struct Canvas { u32 height; u32[] pixels; - new Canvas = { int w, int h -> + new Canvas = { u32 w, u32 h -> i64 count = w; count *= h; if( w <= 0 || h <= 0 || count > sys.integer.MAX_I32 ) libc._exit(1); diff --git a/chapter31/src/test/java/com/seaofnodes/simple/BreakoutTest.java b/chapter31/src/test/java/com/seaofnodes/simple/BreakoutTest.java index efdb1dd7..7f3e56d4 100644 --- a/chapter31/src/test/java/com/seaofnodes/simple/BreakoutTest.java +++ b/chapter31/src/test/java/com/seaofnodes/simple/BreakoutTest.java @@ -28,35 +28,51 @@ public class BreakoutTest { return physics; } + @Test public void firstServeMovesBall() throws Exception { + TestC.runSYS(game()+""" + val main = { -> + val g = new Game; g._reset(); + if( g.speed != 320.0 || g.paddle != 504.0 ) return 1; + g._serve(); + f64 x = g.x, y = g.y; + g._tick(0); + if( g.phase != PLAY || g.x <= x || g.y >= y ) return 2; + g._paddle(2000.0); + if( g.paddle != RIGHT-PADDLE_HALF ) return 3; + return 0; + }; + ""","breakoutServe","",-1); + } + @Test public void paddleLivesAndPause() throws Exception { TestC.runSYS(game()+""" val main={ -> - val g=new Game; + val g=new Game; g._reset(); if( g.remaining!=192 || g.score!=0 || g.lives!=3 || g.phase!=READY ) return 1; - _tick(g,1); + g._tick(1); if( g.paddle<510.39 || g.paddle>510.41 || g.x!=g.paddle ) return 2; g._paddle(-100.0); if( g.paddle != 80.0 ) return 3; g._paddle(2000.0); if( g.paddle != 928.0 ) return 4; - _reset(g); - g.x=g.paddle-50.0; _rebound(g); + g._reset(); + g.x=g.paddle-50.0; g._rebound(); if( g.dx > -0.78 || g.dy>=0.0 ) return 5; - g.x=g.paddle+50.0; _rebound(g); + g.x=g.paddle+50.0; g._rebound(); if( g.dx < 0.78 || g.dy>=0.0 ) return 6; - g.x=g.paddle; _rebound(g); + g.x=g.paddle; g._rebound(); if( g.dy > -0.99 || g.dx*g.dx+g.dy*g.dy<0.999999 ) return 7; - _serve(g); g.paused=true; + g._serve(); g.paused=true; f64 x=g.x, y=g.y, paddle=g.paddle; - _tick(g,1); + g._tick(1); if( g.x!=x || g.y!=y || g.paddle!=paddle ) return 8; g.paused=false; for( int i=0; i<3; i++ ) { - _serve(g); g.x=40.0; g.y=723.0; g.dx=0.0; g.dy=1.0; - _tick(g,0); + g._serve(); g.x=40.0; g.y=723.0; g.dx=0.0; g.dy=1.0; + g._tick(0); if( g.lives!=2-i || g.backWall || g.sound!=5 ) return 9; } if( g.phase!=LOST ) return 10; - _serve(g); if( g.phase!=LOST ) return 11; - _reset(g); + g._serve(); if( g.phase!=LOST ) return 11; + g._reset(); if( g.phase!=READY || g.lives!=3 || g.boosted || g.paused || g.speed!=320.0 ) return 12; return 0; }; @@ -66,67 +82,104 @@ public class BreakoutTest { @Test public void collisionsAndBoost() throws Exception { TestC.runSYS(game()+""" val main={ -> - val g=new Game; - _serve(g); + val g=new Game; g._reset(); + g._serve(); g.x=29.1; g.y=400.0; g.dx=-1.0; g.dy=0.0; - _tick(g,0); + g._tick(0); if( g.dx<=0.0 || g.x<29.0 || g.sound!=1 ) return 1; - g.x=978.9; g.dx=1.0; _tick(g,0); + g.x=978.9; g.dx=1.0; g._tick(0); if( g.dx>=0.0 || g.x>979.0 ) return 2; // Front of the bottom row: one point, no acceleration or boost. g.x=44.0; g.y=330.0; g.dx=0.0; g.dy=-1.0; - _tick(g,0); + g._tick(0); if( g.bricks[168]!=0 || g.score!=1 || g.dy<=0.0 || g.boosted || g.soundDepth!=1 ) return 3; // Top of a brick alone does not arm the back-wall bonus. g.x=44.0; g.y=146.0; g.dy=1.0; - _tick(g,0); + g._tick(0); if( g.bricks[0]!=0 || g.score!=9 || g.speed!=565.0 || g.boosted || g.soundDepth!=8 ) return 4; // Back wall arms it; hitting a side still must not trigger it. g.x=84.0; g.y=89.1; g.dy=-1.0; - _tick(g,0); + g._tick(0); if( !g.backWall || g.dy<=0.0 || g.boosted ) return 5; g.x=60.0; g.y=160.0; g.dx=1.0; g.dy=0.0; - _tick(g,0); + g._tick(0); if( g.bricks[1]!=0 || g.dx>=0.0 || g.boosted ) return 6; g.x=124.0; g.y=146.0; g.dx=0.0; g.dy=1.0; - _tick(g,0); - if( !g.boosted || g.speed<932.24 || g.speed>932.26 || g.dy>=0.0 || g.sound!=4 ) return 7; + g._tick(0); + if( !g.boosted || g.speed<932.24 || g.speed>932.26 || g.dy>=0.0 || g.sound!=3 || g.soundDepth!=8 ) return 7; // Another back hit cannot multiply the speed a second time. g.x=164.0; g.y=146.0; g.dy=1.0; - _tick(g,0); + g._tick(0); if( g.speed>932.26 || g.bricks[3]!=0 ) return 8; // A boosted front hit cannot tunnel through the brick either. g.x=84.0; g.y=331.0; g.dy=-1.0; - _tick(g,0); + g._tick(0); if( g.bricks[169]!=0 || g.dy<=0.0 || g.speed<932.24 ) return 9; // Actual paddle contact disarms the trip and chooses its angle. g.x=g.paddle+50.0; g.y=668.0; g.dy=1.0; g.dx=0.0; - _tick(g,0); + g._tick(0); if( g.dy>=0.0 || g.dx<0.78 || g.backWall || !g.boosted ) return 10; // A lost ball clears both kinds of acceleration, preserving the wall/score. int score=g.score, remaining=g.remaining; g.x=40.0; g.y=723.0; g.dx=0.0; g.dy=1.0; - _tick(g,0); + g._tick(0); if( g.phase!=READY || g.lives!=2 || g.speed!=320.0 || g.depth!=1 || g.boosted ) return 11; if( g.score!=score || g.remaining!=remaining ) return 12; - _serve(g); + g._serve(); if( g.speed!=320.0 ) return 13; // The new ball can earn its own boost. g.sound=0; g.backWall=true; g.x=204.0; g.y=146.0; g.dx=0.0; g.dy=1.0; - _tick(g,0); - if( !g.boosted || g.speed<932.24 || g.sound!=4 ) return 14; + g._tick(0); + if( !g.boosted || g.speed<932.24 || g.sound!=3 || g.soundDepth!=8 ) return 14; return 0; }; ""","breakoutCollisions","",-1); } + @Test public void gridEdgesAndGaps() throws Exception { + TestC.runSYS(game()+""" + val main = { -> + val g = new Game; g._reset(); + val c = new sys.draw.Canvas(WIDTH,HEIGHT); + g._draw(c); + g._serve(); + // The center lies below the last grid row and between columns. + // Both neighboring bricks overlap the ball; the left one wins. + g.x = 64.0; g.y = 330.0; g.dx = 0.0; g.dy = -1.0; + g._tick(0); + if( g.bricks[168] != 0 || g.bricks[169] != 1 || g.score != 1 || g.dy <= 0.0 ) return 1; + // A cleared neighbor must not hide the other candidate. + g.x = 64.0; g.y = 330.0; g.dx = 0.0; g.dy = -1.0; + g._tick(0); + if( g.bricks[169] != 0 || g.score != 2 || g.dy <= 0.0 ) return 2; + // Rightmost column, then a position entirely below the wall. + g.x = 978.0; g.y = 330.0; g.dx = 0.0; g.dy = -1.0; + g._tick(0); + if( g.bricks[191] != 0 || g.score != 3 || g.dy <= 0.0 ) return 3; + g.x = 44.0; g.y = 400.0; g.dx = 0.0; g.dy = -1.0; + g._tick(0); + if( g.score != 3 || g.dy >= 0.0 ) return 4; + // Overlapping rows: the first box contains the center but no + // face was crossed; continue to the next row's top face. + g.x = 44.0; g.y = 168.0; g.dx = 0.0; g.dy = 1.0; + g._tick(0); + if( g.bricks[0] != 1 || g.bricks[24] != 0 || g.score != 10 || g.dy >= 0.0 ) return 5; + // Contact just above the first row still belongs to that row. + g.x = 44.0; g.y = 146.0; g.dx = 0.0; g.dy = 1.0; + g._tick(0); + if( g.bricks[0] != 0 || g.score != 18 || g.dy >= 0.0 ) return 6; + return 0; + }; + ""","breakoutGrid","",-1); + } + @Test public void clearEveryBrickAndRender() throws Exception { TestC.runSYS(game()+""" val main={ -> - val g=new Game; + val g=new Game; g._reset(); val c=new sys.draw.Canvas(WIDTH,HEIGHT); - _draw(c,g); + g._draw(c); for( int row=0; row<ROWS; row++ ) if( c.pixels[(BRICK_TOP+row*BRICK_DY+4)*WIDTH+28]!=COLORS[row] ) return 1; if( c.pixels[675*WIDTH+504]!=CYAN || c.pixels[663*WIDTH+504]!=WHITE ) return 2; @@ -135,19 +188,19 @@ public class BreakoutTest { i64 pixels=c.pixels; if( fd<0 || sys.libc.write(fd,pixels,c.pixels#*4)!=c.pixels#*4 ) return 3; sys.io.close(fd); - _serve(g); + g._serve(); // Drive through every brick's front, bottom row first. for( int i=COLS*ROWS-1; i>=0; i-- ) { g.x=LEFT+20.0+(i-i/COLS*COLS)*BRICK_DX; g.y=BRICK_TOP+1.0+(i/COLS)*BRICK_DY+BRICK_H+RADIUS; g.dx=0.0; g.dy=-1.0; - _tick(g,0); + g._tick(0); if( g.bricks[i]!=0 ) return 4; } if( g.phase!=WON || g.remaining!=0 || g.score!=864 || g.sound!=6 ) return 5; - f64 y=g.y; _tick(g,0); + f64 y=g.y; g._tick(0); if( g.y!=y ) return 6; - _reset(g); + g._reset(); for( int i=0; i<g.bricks#; i++ ) if( g.bricks[i]!=1 ) return 7; if( g.score!=0 || g.remaining!=192 ) return 8; return 0; diff --git a/chapter31/src/test/java/com/seaofnodes/simple/Chapter17bTest.java b/chapter31/src/test/java/com/seaofnodes/simple/Chapter17bTest.java index 12e54193..8a66dcc5 100644 --- a/chapter31/src/test/java/com/seaofnodes/simple/Chapter17bTest.java +++ b/chapter31/src/test/java/com/seaofnodes/simple/Chapter17bTest.java @@ -568,8 +568,8 @@ struct _A { return new _A(new !B?[0]).array_of_b; """); code.driver(CodeGen.Phase.TypeCheck); - assertEquals("return []*B?;", code.print()); - assertEquals("*B {}?[]", Eval2.eval(code,0)); + assertEquals("return []*Test.B?;", code.print()); + assertEquals("*Test.B {}?[]", Eval2.eval(code,0)); } // --------------------------------------------------------------- diff --git a/chapter31/src/test/java/com/seaofnodes/simple/Chapter23Test.java b/chapter31/src/test/java/com/seaofnodes/simple/Chapter23Test.java index 89e41efa..8667c0bd 100644 --- a/chapter31/src/test/java/com/seaofnodes/simple/Chapter23Test.java +++ b/chapter31/src/test/java/com/seaofnodes/simple/Chapter23Test.java @@ -279,7 +279,7 @@ return _s.peek('q'); CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); assertEquals("return Phi(Region,0,1);", code.print()); assertEquals("1", Eval2.eval(code, 0)); - testCPU(src,"x86_64_v2", "win64",31,null); + testCPU(src,"x86_64_v2", "win64",23,null); }; @Test @@ -303,8 +303,7 @@ return s.skip().x; try { new CodeGen(src).driver(CodeGen.Phase.TypeCheck); fail(); } catch( Exception e ) { - // Bad error message, but basically requires a mutable 'x' field. - org.junit.Assert.assertTrue(e.getMessage().startsWith("Argument #0 isa")); + assertEquals("Receiver for method 'skip' is read-only Test._Scan; expected writable Test._Scan",e.getMessage()); } }; diff --git a/chapter31/src/test/java/com/seaofnodes/simple/MethodAccessTest.java b/chapter31/src/test/java/com/seaofnodes/simple/MethodAccessTest.java new file mode 100644 index 00000000..7275b151 --- /dev/null +++ b/chapter31/src/test/java/com/seaofnodes/simple/MethodAccessTest.java @@ -0,0 +1,118 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.parser.Parser; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.Ary; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Test; +import static org.junit.Assert.*; + +public class MethodAccessTest { + @Test public void readersAcceptReadOnlyReceivers() throws Exception { + String source = """ + struct Child { int n = 7; }; + struct Box { + !Child child = new Child; + int[] values = new int[]{3,5}; + int count = 2; + { i64 } read = { -> count+child.n+values[1]; }; + val delegate = { -> read(); }; + val recurse = { i64 n -> n == 0 ? read() : recurse(n-1); }; + val draw = { !Child canvas -> canvas.n = count; }; + val outer = { -> later(count); }; + }; + val later = { int n -> n+1; }; + val check = { Box box -> + val canvas = new Child; + box.draw(canvas); + return box.read()+box.delegate()+box.recurse(3)+canvas.n+box.outer(); + }; + return check(new Box); + """; + assertEquals("47",Eval2.eval(new CodeGen(source).driver(CodeGen.Phase.LocalSched),0)); + TestC.runSF("val main = { -> "+source.replace("return check(new Box);","return check(new Box) != 47;")+" };", + "readonlyMethods","",-1); + } + + @Test public void writesThroughReceiverRequireAccess() { + String[] methods = { + "val act = { -> x = 2; };", + "val act = { -> self.x++; };", + "val act = { -> val nested = { -> x++; }; nested(); };", + "val act = { -> val alias = self; alias.x += 1; };", + "val act = { -> child.n = 2; };", + "val act = { -> values[0]++; };", + "val act = { -> write(); }; val write = { -> x++; };", + "val act = { -> val f = x == 0 ? left : right; f(); }; val left = { -> x++; }; val right = { -> x--; };", + "val act = { -> child.write(); };", + "val act = { -> write(self); };", + "{ int } act = \"C\";" + }; + for( String method : methods ) { + String source = """ + struct Child { int n; val write = { -> n++; }; }; + struct Box { + int x; + !Child child = new Child; + int[] values = new int[1]; + """+method+""" + }; + val write = { !Box box -> box.x++; }; + val check = { Box box -> box.act(); return 0; }; + return check(new Box); + """; + try { + new CodeGen(source).driver(CodeGen.Phase.TypeCheck); + fail("Accepted writer: "+method); + } catch( Parser.ParseException e ) { + assertTrue(method+": "+e.getMessage(),e.getMessage().contains("read-only")); + } + new CodeGen(source.replace("val check = { Box box", "val check = { !Box box")) + .driver(CodeGen.Phase.TypeCheck); + } + } + + @Test public void returningWritableAliasesRequiresAccess() { + String source = """ + struct Box { int x; val alias = { -> self; }; }; + val check = { Box box -> box.alias(); }; + return check(new Box); + """; + try { + new CodeGen(source).driver(CodeGen.Phase.TypeCheck); + fail("Read-only receiver exposed as writable"); + } catch( Parser.ParseException e ) { + assertTrue(e.getMessage(),e.getMessage().contains("read-only")); + } + source = source.replace("val alias = { -> self; }", "val alias = { -> Box view = self; view; }"); + new CodeGen(source).driver(CodeGen.Phase.TypeCheck); + } + + @Test public void importedReceiverAccess() throws Exception { + var root = Files.createDirectories(Path.of("build/objs/separate")); + var lib = Files.createTempDirectory(root,"method-access-"); + new CodeGen(null,lib.toString(),null,"Counters",""" + struct Counter { + int n = 7; + val read = { -> n; }; + val write = { -> n++; }; + }; + val make = { -> new Counter; }; + """,126,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + var paths = new Ary<>(new String[]{lib.toString()}); + String source = "val box = Counters.make(); val check = { Counters.Counter c -> c.read(); }; return check(box);"; + var code = new CodeGen(null,"build/objs",paths,"CounterReader",source,126,true, + TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertEquals("7",Eval2.eval(code,0)); + try { + new CodeGen(null,"build/objs",paths,"CounterWriter",source.replace("c.read()","c.write()"),126,true, + TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + fail("Imported writer accepted a read-only receiver"); + } catch( Parser.ParseException e ) { + assertTrue(e.getMessage(),e.getMessage().contains("read-only")); + } + } +} diff --git a/chapter31/src/test/java/com/seaofnodes/simple/MethodFieldsTest.java b/chapter31/src/test/java/com/seaofnodes/simple/MethodFieldsTest.java new file mode 100644 index 00000000..3fe5c28b --- /dev/null +++ b/chapter31/src/test/java/com/seaofnodes/simple/MethodFieldsTest.java @@ -0,0 +1,131 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.parser.Parser; +import org.junit.Test; +import static org.junit.Assert.*; + +public class MethodFieldsTest { + @Test public void nestedFunctionStillRejectsLocalCapture() { + String source = """ + struct Game { + int count; + val input = { int amount -> + val action = { -> count += amount; }; + action(); + }; + }; + return 0; + """; + try { + new CodeGen(source).driver(CodeGen.Phase.Parse); + fail("Captured a nonconstant local parameter"); + } catch( Parser.ParseException e ) { + assertEquals("Variable 'amount' is out of function scope and must be a final constant",e.getMessage()); + } + } + + @Test public void nestedFunctionReceiver() throws Exception { + String source = """ + struct Game { + bool held, paused; + int count; + val debounce = { bool pressed, bool wasHeld, { bool } action -> + if( pressed && !wasHeld ) action(); + return pressed; + }; + val bump = { -> count++; return false; }; + val input = { bool pressed -> + if( true ) { + held = debounce(pressed,held,{ -> + paused = !paused; + bump(); + val again = { -> count += 2; return false; }; + again(); + return false; + }); + } + }; + val named = { -> debounce(true,false,bump); }; + }; + val a = new Game, b = new Game; + a.input(true); a.input(true); + if( a.count != 3 || !a.paused || b.count != 0 || b.paused ) return 1; + a.input(false); a.input(true); + if( a.count != 6 || a.paused ) return 2; + b.input(true); b.named(); + if( b.count != 4 || !b.paused || a.count != 6 ) return 3; + return 0; + """; + assertEquals("0",Eval2.eval(new CodeGen(source).driver(CodeGen.Phase.LocalSched),0)); + TestC.runSF("val main = { -> "+source+" };","nestedFunctionReceiver","",-1); + } + + @Test public void laterSiblingParameterType() throws Exception { + String source = """ + struct Game { + val input = { !Controls keys -> keys.presses++; return keys.presses; }; + val read = { Controls keys -> keys.presses; }; + }; + struct Controls { int presses = 4; }; + val game = new Game, keys = new Controls; + if( game.input(keys) != 5 || game.read(keys) != 5 ) return 1; + return 0; + """; + assertEquals("0",Eval2.eval(new CodeGen(source).driver(CodeGen.Phase.LocalSched),0)); + TestC.runSF("val main = { -> "+source+" };","forwardMethodType","",-1); + } + + private static final String FIELDS = """ + int count = 100; + struct Counter { + val step = { int n -> + count = count+n; + ++count; + count += 2; + speed *= 2.0; + return read(); + }; + val local = { int count -> count+1; }; + val read = { -> count; }; + int count = 3; + f64 speed = 1.5; + }; + val a = new Counter, b = new Counter; + if( a.step(4) != 10 || b.read() != 3 || a.speed != 3.0 ) return 1; + if( a.local(20) != 21 || a.read() != 10 || count != 100 ) return 2; + if( b.step(1) != 7 || a.read() != 10 ) return 3; + return 0; + """; + + @Test public void laterFieldsAndMethods() throws Exception { + assertEquals("0",Eval2.eval(new CodeGen(FIELDS).driver(CodeGen.Phase.LocalSched),0)); + TestC.runSF("val main = { -> "+FIELDS+" };","methodFields","",-1); + } + + @Test public void laterGlobalAndRecursiveMethod() { + String source = """ + struct Counter { + val step = { i64 n -> + if( n == 0 ) return value; + value += increment; + return step(n-1); + }; + int value = 1; + }; + val increment = 2; + return new Counter.step(3); + """; + assertEquals("7",Eval2.eval(new CodeGen(source).driver(CodeGen.Phase.LocalSched),0)); + } + + @Test public void laterFinalFieldStillRejectsWrites() { + String source = "struct C { val change = { -> x = 2; }; int ~x = 1; }; return new C.change();"; + try { + new CodeGen(source).driver(CodeGen.Phase.TypeCheck); + fail("Method wrote a final field"); + } catch( Parser.ParseException e ) { + assertEquals("Cannot reassign final 'x'",e.getMessage()); + } + } +} diff --git a/chapter31/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java b/chapter31/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java index a1253d05..d342892c 100644 --- a/chapter31/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java +++ b/chapter31/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java @@ -10,6 +10,57 @@ import org.junit.Test; import static org.junit.Assert.*; public class ParserDiagnosticTest { + @Test public void testMissingForwardTypeDiagnostic() { + String source = """ + val read = { Missing value -> + return value.field; + }; + return 0; + """; + var error = rejected(source); + assertEquals("Unknown type 'Test.Missing'",error.getMessage()); + assertTrue(error.diagnostic(),error.diagnostic().startsWith("Test.smp:2:")); + assertTrue(error.diagnostic(),error.diagnostic().contains("return value.field;")); + } + + @Test public void testReadOnlyReceiverDiagnostic() { + String source = """ + struct Box { + int x = 1; + val bump = { -> x++; x; }; + }; + val draw = { Box b -> + return b.bump(); + }; + return draw(new Box); + """; + try { + new CodeGen(source).driver(CodeGen.Phase.TypeCheck); + fail("Read-only receiver accepted by a writable method"); + } catch( Parser.ParseException e ) { + assertEquals("Receiver for method 'bump' is read-only Test.Box; expected writable Test.Box",e.getMessage()); + assertTrue(e.diagnostic(),e.diagnostic().startsWith("Test.smp:6:18: error: ")); + assertFalse(e.getMessage().contains("{i32")); + } + } + + @Test public void testReadOnlyArgumentDiagnostic() { + String source = """ + struct Box { int x = 1; }; + val write = { !Box b -> b.x = 2; }; + val draw = { Box b -> write(b); }; + return draw(new Box); + """; + try { + new CodeGen(source).driver(CodeGen.Phase.TypeCheck); + fail("Read-only argument accepted by a writable parameter"); + } catch( Parser.ParseException e ) { + assertEquals("Argument #1 is read-only Test.Box; expected writable Test.Box",e.getMessage()); + } + new CodeGen(source.replace("val draw = { Box b", "val draw = { !Box b")) + .driver(CodeGen.Phase.TypeCheck); + } + private static Parser.ParseException rejected(String source) { try { new CodeGen(source).driver(CodeGen.Phase.Parse); |
