summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-11-14 16:23:39 -0800
committerCliff Click <cliffc@acm.org>2026-11-14 16:23:39 -0800
commit7e66b7ae6b00a2e1652db32f01f0bf480508ef05 (patch)
treef09689493fd820544b4cd94d4b1e941d84a24b4b
parent3d0f3f38d9d8345b6508b3289dc94b0500c53d9e (diff)
downloadSimple-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.
-rw-r--r--chapter31/README.md2
-rw-r--r--chapter31/docs/examples/Breakout.smp375
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java5
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/node/CallNode.java9
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/node/LoadNode.java10
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/node/StoreNode.java4
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/parser/Declarations.java4
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/parser/Functions.java61
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/parser/MethodAccess.java130
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/parser/Parser.java6
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/parser/README.md13
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/parser/References.java11
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/parser/Types.java11
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/parser/Var.java5
-rw-r--r--chapter31/src/main/smp/sys/draw/Canvas.smp2
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/BreakoutTest.java123
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/Chapter17bTest.java4
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/Chapter23Test.java5
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/MethodAccessTest.java118
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/MethodFieldsTest.java131
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java51
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);