summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.argsT.txt1
-rw-r--r--.out.txt304
-rw-r--r--Makefile21
-rw-r--r--README.ja.md2
-rw-r--r--README.md2
-rw-r--r--audio.mk31
-rw-r--r--chapter31.iml (renamed from chapter30.iml)4
-rw-r--r--docs/chapter31/31-grammar.ja.md182
-rw-r--r--docs/chapter31/31-grammar.md263
-rw-r--r--docs/chapter31/README.ja.md20
-rw-r--r--docs/chapter31/README.md55
-rw-r--r--docs/chapter31/audio.md62
-rw-r--r--docs/chapter31/construction-groups.md241
-rw-r--r--docs/chapter31/deferred-construction.md385
-rw-r--r--docs/chapter31/drawing.md84
-rw-r--r--docs/chapter31/escape-flow.md147
-rw-r--r--docs/chapter31/examples/A_helloWorld.smp2
-rw-r--r--docs/chapter31/examples/BubbleSort.smp38
-rw-r--r--docs/chapter31/examples/Capitalize.smp23
-rw-r--r--docs/chapter31/examples/Dijkstra.smp74
-rw-r--r--docs/chapter31/examples/FileIO.smp52
-rw-r--r--docs/chapter31/examples/Graphics.smp17
-rw-r--r--docs/chapter31/examples/README.md78
-rw-r--r--docs/chapter31/examples/TicTacToe.smp133
-rw-r--r--docs/chapter31/examples/TicTacToeGraphical.smp167
-rw-r--r--docs/chapter31/graphics.md119
-rw-r--r--graphics.mk62
-rw-r--r--src/main/c/simple_audio.c40
-rw-r--r--src/main/c/simple_audio.h16
-rw-r--r--src/main/c/simple_graphics.c60
-rw-r--r--src/main/c/simple_graphics.h19
-rw-r--r--src/main/c/simple_miniaudio.c123
-rw-r--r--src/main/c/simple_minifb.c113
-rw-r--r--src/main/java/com/seaofnodes/simple/README.md2
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/Serialize.java12
-rw-r--r--src/main/java/com/seaofnodes/simple/node/LoadNode.java16
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java5
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Expressions.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Lexer.java26
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/README.md2
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/package-info.java2
-rw-r--r--src/main/smp/sys.smp3
-rw-r--r--src/main/smp/sys/audio.smp29
-rw-r--r--src/main/smp/sys/draw.smp17
-rw-r--r--src/main/smp/sys/draw/Canvas.smp176
-rw-r--r--src/main/smp/sys/gfx.smp38
-rw-r--r--src/test/c/audio_test.c48
-rw-r--r--src/test/java/com/seaofnodes/simple/AudioTest.java94
-rw-r--r--src/test/java/com/seaofnodes/simple/BrainFuckTest.java8
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter01Test.java9
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter21Test.java3
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter25Test.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/DrawTest.java105
-rw-r--r--src/test/java/com/seaofnodes/simple/GraphicsTest.java75
-rw-r--r--src/test/java/com/seaofnodes/simple/IntegerLiteralTest.java46
-rw-r--r--src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java13
-rw-r--r--src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java16
-rw-r--r--src/test/java/com/seaofnodes/simple/StreamIOTest.java3
-rw-r--r--src/test/java/com/seaofnodes/simple/TestC.java4
59 files changed, 3356 insertions, 342 deletions
diff --git a/.argsT.txt b/.argsT.txt
deleted file mode 100644
index 827ddba7..00000000
--- a/.argsT.txt
+++ /dev/null
@@ -1 +0,0 @@
- src/test/java/com/seaofnodes/simple/Chapter09Test.java src/test/java/com/seaofnodes/simple/ParserTest.java src/test/java/com/seaofnodes/simple/Chapter16Test.java src/test/java/com/seaofnodes/simple/TicTacToeTest.java src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java src/test/java/com/seaofnodes/simple/Chapter23Test.java src/test/java/com/seaofnodes/simple/NativeExecutionTest.java src/test/java/com/seaofnodes/simple/Chapter14Test.java src/test/java/com/seaofnodes/simple/ArithNodeTest.java src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java src/test/java/com/seaofnodes/simple/Chapter10Test.java src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java src/test/java/com/seaofnodes/simple/ConstructionRequirementsTest.java src/test/java/com/seaofnodes/simple/Chapter13Test.java src/test/java/com/seaofnodes/simple/Chapter06Test.java src/test/java/com/seaofnodes/simple/TestRunner.java src/test/java/com/seaofnodes/simple/Chapter03Test.java src/test/java/com/seaofnodes/simple/FuzzerWrap.java src/test/java/com/seaofnodes/simple/EscapeFlowTest.java src/test/java/com/seaofnodes/simple/Int32Test.java src/test/java/com/seaofnodes/simple/Chapter24Test.java src/test/java/com/seaofnodes/simple/TerminalInputTest.java src/test/java/com/seaofnodes/simple/ConstructorCompletionTest.java src/test/java/com/seaofnodes/simple/RuntimeArgsTest.java src/test/java/com/seaofnodes/simple/Chapter04Test.java src/test/java/com/seaofnodes/simple/ChainedComparisonTest.java src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java src/test/java/com/seaofnodes/simple/MutabilityTest.java src/test/java/com/seaofnodes/simple/FinalConstructionTest.java src/test/java/com/seaofnodes/simple/Chapter28bTest.java src/test/java/com/seaofnodes/simple/Chapter21Test.java src/test/java/com/seaofnodes/simple/Chapter07Test.java src/test/java/com/seaofnodes/simple/BrainFuckTest.java src/test/java/com/seaofnodes/simple/Chapter05Test.java src/test/java/com/seaofnodes/simple/PhaseTest.java src/test/java/com/seaofnodes/simple/Eval2.java src/test/java/com/seaofnodes/simple/TestArm64.java src/test/java/com/seaofnodes/simple/Chapter12Test.java src/test/java/com/seaofnodes/simple/StringFormatTest.java src/test/java/com/seaofnodes/simple/TestC.java src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java src/test/java/com/seaofnodes/simple/Simple.java src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java src/test/java/com/seaofnodes/simple/Chapter02Test.java src/test/java/com/seaofnodes/simple/Chapter15Test.java src/test/java/com/seaofnodes/simple/BoundaryTest.java src/test/java/com/seaofnodes/simple/Chapter25Test.java src/test/java/com/seaofnodes/simple/Chapter22Test.java src/test/java/com/seaofnodes/simple/PartialReadTest.java src/test/java/com/seaofnodes/simple/MergeSortTest.java src/test/java/com/seaofnodes/simple/SpillStats.java src/test/java/com/seaofnodes/simple/Chapter17bTest.java src/test/java/com/seaofnodes/simple/CallEndTest.java src/test/java/com/seaofnodes/simple/Chapter01Test.java src/test/java/com/seaofnodes/simple/MinusNodeTest.java src/test/java/com/seaofnodes/simple/Chapter18Test.java src/test/java/com/seaofnodes/simple/TypeTest.java src/test/java/com/seaofnodes/simple/StaticDataTest.java src/test/java/com/seaofnodes/simple/TestRisc5.java src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java src/test/java/com/seaofnodes/simple/ConvertNodeTest.java src/test/java/com/seaofnodes/simple/Chapter08Test.java src/test/java/com/seaofnodes/simple/Chapter17aTest.java src/test/java/com/seaofnodes/simple/Chapter19Test.java src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java src/test/java/com/seaofnodes/simple/PrefixUpdateTest.java src/test/java/com/seaofnodes/simple/Chapter20Test.java src/test/java/com/seaofnodes/simple/Main.java src/test/java/com/seaofnodes/simple/StreamIOTest.java src/test/java/com/seaofnodes/simple/codegen/ParseAllTestSupport.java src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java src/test/java/com/seaofnodes/simple/codegen/FlagSpillTestSupport.java src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java ../isa/src/test-support/java/com/seaofnodes/isa/eval/EvalArm64.java ../isa/src/test-support/java/com/seaofnodes/isa/eval/EvalRisc5.java
diff --git a/.out.txt b/.out.txt
deleted file mode 100644
index be785578..00000000
--- a/.out.txt
+++ /dev/null
@@ -1,304 +0,0 @@
-src\test\java\com\seaofnodes\simple\Chapter09Test.java:6: error: package org.junit does not exist
-import org.junit.Assert;
- ^
-src\test\java\com\seaofnodes\simple\Chapter09Test.java:7: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\Chapter09Test.java:8: error: package org.junit does not exist
-import org.junit.Ignore;
- ^
-src\test\java\com\seaofnodes\simple\Chapter09Test.java:10: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\ParserTest.java:5: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\ParserTest.java:6: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\Chapter16Test.java:4: error: package org.junit does not exist
-import org.junit.Ignore;
- ^
-src\test\java\com\seaofnodes\simple\Chapter16Test.java:5: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\Chapter16Test.java:6: error: package org.junit does not exist
-import static org.junit.Assert.assertEquals;
- ^
-src\test\java\com\seaofnodes\simple\Chapter16Test.java:6: error: static import only from classes and interfaces
-import static org.junit.Assert.assertEquals;
-^
-src\test\java\com\seaofnodes\simple\Chapter16Test.java:7: error: package org.junit does not exist
-import static org.junit.Assert.fail;
- ^
-src\test\java\com\seaofnodes\simple\Chapter16Test.java:7: error: static import only from classes and interfaces
-import static org.junit.Assert.fail;
-^
-src\test\java\com\seaofnodes\simple\TicTacToeTest.java:8: error: package org.junit does not exist
-import org.junit.BeforeClass;
- ^
-src\test\java\com\seaofnodes\simple\TicTacToeTest.java:9: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\TicTacToeTest.java:10: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\ConstructionGroupTest.java:5: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\ConstructionGroupTest.java:6: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\codegen\RegAllocTestSupport.java:7: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\Chapter23Test.java:17: error: package org.junit does not exist
-import org.junit.Ignore;
- ^
-src\test\java\com\seaofnodes\simple\Chapter23Test.java:18: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\Chapter23Test.java:19: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\NativeExecutionTest.java:6: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\NativeExecutionTest.java:7: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\NativeExecutionTest.java:8: error: package org.junit does not exist
-import static org.junit.Assume.assumeTrue;
- ^
-src\test\java\com\seaofnodes\simple\NativeExecutionTest.java:8: error: static import only from classes and interfaces
-import static org.junit.Assume.assumeTrue;
-^
-src\test\java\com\seaofnodes\simple\Chapter14Test.java:5: error: package org.junit does not exist
-import org.junit.Ignore;
- ^
-src\test\java\com\seaofnodes\simple\Chapter14Test.java:6: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\Chapter14Test.java:7: error: package org.junit does not exist
-import static org.junit.Assert.assertEquals;
- ^
-src\test\java\com\seaofnodes\simple\Chapter14Test.java:7: error: static import only from classes and interfaces
-import static org.junit.Assert.assertEquals;
-^
-src\test\java\com\seaofnodes\simple\Chapter14Test.java:8: error: package org.junit does not exist
-import static org.junit.Assert.fail;
- ^
-src\test\java\com\seaofnodes\simple\Chapter14Test.java:8: error: static import only from classes and interfaces
-import static org.junit.Assert.fail;
-^
-src\test\java\com\seaofnodes\simple\ArithNodeTest.java:8: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\ArithNodeTest.java:9: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\CompoundAssignmentTest.java:5: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\CompoundAssignmentTest.java:6: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\Chapter14FloatTest.java:4: error: package org.junit does not exist
-import org.junit.Ignore;
- ^
-src\test\java\com\seaofnodes\simple\Chapter14FloatTest.java:5: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\Chapter14FloatTest.java:6: error: package org.junit does not exist
-import static org.junit.Assert.assertEquals;
- ^
-src\test\java\com\seaofnodes\simple\Chapter14FloatTest.java:6: error: static import only from classes and interfaces
-import static org.junit.Assert.assertEquals;
-^
-src\test\java\com\seaofnodes\simple\Chapter10Test.java:5: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\Chapter10Test.java:6: error: package org.junit does not exist
-import static org.junit.Assert.assertEquals;
- ^
-src\test\java\com\seaofnodes\simple\Chapter10Test.java:6: error: static import only from classes and interfaces
-import static org.junit.Assert.assertEquals;
-^
-src\test\java\com\seaofnodes\simple\Chapter10Test.java:7: error: package org.junit does not exist
-import static org.junit.Assert.fail;
- ^
-src\test\java\com\seaofnodes\simple\Chapter10Test.java:7: error: static import only from classes and interfaces
-import static org.junit.Assert.fail;
-^
-src\test\java\com\seaofnodes\simple\BulkMemPhiTest.java:7: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\BulkMemPhiTest.java:8: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\MemoryEncodingTest.java:6: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\MemoryEncodingTest.java:7: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\ConstructionRequirementsTest.java:5: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\ConstructionRequirementsTest.java:6: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\ConstructionRequirementsTest.java:8: error: package org.junit does not exist
-@org.junit.Ignore("Deferred: inferred field requirements and incomplete-object traversal")
- ^
-src\test\java\com\seaofnodes\simple\Chapter13Test.java:5: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\Chapter13Test.java:6: error: package org.junit does not exist
-import static org.junit.Assert.assertEquals;
- ^
-src\test\java\com\seaofnodes\simple\Chapter13Test.java:6: error: static import only from classes and interfaces
-import static org.junit.Assert.assertEquals;
-^
-src\test\java\com\seaofnodes\simple\Chapter06Test.java:6: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\Chapter06Test.java:8: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\TestRunner.java:3: error: package org.junit.internal does not exist
-import org.junit.internal.TextListener;
- ^
-src\test\java\com\seaofnodes\simple\TestRunner.java:4: error: package org.junit.runner does not exist
-import org.junit.runner.JUnitCore;
- ^
-src\test\java\com\seaofnodes\simple\TestRunner.java:5: error: package org.junit.runner does not exist
-import org.junit.runner.Request;
- ^
-src\test\java\com\seaofnodes\simple\Chapter03Test.java:4: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\Chapter03Test.java:5: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\FuzzerWrap.java:5: error: package org.junit does not exist
-import org.junit.Ignore;
- ^
-src\test\java\com\seaofnodes\simple\FuzzerWrap.java:6: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\FuzzerWrap.java:7: error: package org.junit does not exist
-import static org.junit.Assert.assertTrue;
- ^
-src\test\java\com\seaofnodes\simple\FuzzerWrap.java:7: error: static import only from classes and interfaces
-import static org.junit.Assert.assertTrue;
-^
-src\test\java\com\seaofnodes\simple\EscapeFlowTest.java:6: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\EscapeFlowTest.java:7: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\Int32Test.java:10: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\Int32Test.java:11: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\Chapter24Test.java:15: error: package org.junit does not exist
-import org.junit.Ignore;
- ^
-src\test\java\com\seaofnodes\simple\Chapter24Test.java:16: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\Chapter24Test.java:17: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\TerminalInputTest.java:12: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\TerminalInputTest.java:13: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\ConstructorCompletionTest.java:5: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\ConstructorCompletionTest.java:6: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\RuntimeArgsTest.java:4: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\RuntimeArgsTest.java:5: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\TestC.java:21: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\Chapter04Test.java:6: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\Chapter04Test.java:7: error: package org.junit does not exist
-import org.junit.Ignore;
- ^
-src\test\java\com\seaofnodes\simple\Chapter04Test.java:9: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\ChainedComparisonTest.java:5: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\ChainedComparisonTest.java:6: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\SeparateCompilationTest.java:9: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\SeparateCompilationTest.java:10: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\MutabilityTest.java:3: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\MutabilityTest.java:4: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\FinalConstructionTest.java:5: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\FinalConstructionTest.java:6: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\Chapter28bTest.java:4: error: package org.junit does not exist
-import org.junit.Ignore;
- ^
-src\test\java\com\seaofnodes\simple\Chapter28bTest.java:5: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\Chapter28bTest.java:7: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\Chapter21Test.java:13: error: package org.junit does not exist
-import org.junit.Ignore;
- ^
-src\test\java\com\seaofnodes\simple\Chapter21Test.java:14: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\Chapter21Test.java:15: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\Chapter07Test.java:5: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\Chapter07Test.java:7: error: package org.junit does not exist
-import static org.junit.Assert.*;
- ^
-src\test\java\com\seaofnodes\simple\BrainFuckTest.java:9: error: package org.junit does not exist
-import org.junit.Test;
- ^
-src\test\java\com\seaofnodes\simple\BrainFuckTest.java:10: error: package org.junit does not exist
-import static org.junit.Assert.assertEquals;
- ^
-Note: Some input files use unchecked or unsafe operations.
-Note: Recompile with -Xlint:unchecked for details.
-100 errors
-only showing the first 100 errors, of 2590 total; use -Xmaxerrs if you would like to see more
diff --git a/Makefile b/Makefile
index 4503c993..e685b205 100644
--- a/Makefile
+++ b/Makefile
@@ -37,6 +37,8 @@ RELEASE:= build/release
TEST_SYS:= $(OBJS)/lib_$(CPU_ABI)/sys.o
RELEASE_SYS:= $(RELEASE)/$(CPU_ABI)/sys.o
RELEASE_CRT:= $(RELEASE)/$(CPU_ABI)/simple_crt.o
+RELEASE_GRAPHICS:= $(RELEASE)/$(CPU_ABI)/simple_graphics.o
+RELEASE_AUDIO:= $(RELEASE)/$(CPU_ABI)/simple_audio.o
RELEASE_ARGS:= $(RELEASE)/$(CPU_ABI)/simple_args.o
C_CALL_CONV:= $(if $(filter win64,$(CALL_CONV)),ms_abi,sysv_abi)
GAME_DIR:= build/examples/$(CPU_ABI)
@@ -65,11 +67,28 @@ $(RELEASE_ARGS): src/main/smp/sys/rt/simple_args.smp $(CLZDIR)/main/.mtag $(simp
@echo "simping " $@
@$(JVM) com.seaofnodes.simple.Simple --cpu $(CPU_PORT) --abi $(CALL_CONV) --norun -o $@ $<
-$(RELEASE_CRT): src/main/c/simple_crt.c $(RELEASE_ARGS) Makefile
+$(RELEASE_CRT): src/main/c/simple_crt.c $(RELEASE_ARGS) $(RELEASE_GRAPHICS) $(RELEASE_AUDIO) Makefile
@echo "compiling " $@
@[ -d $(dir $@) ] || mkdir -p $(dir $@)
@gcc -c -O2 -DCALL_CONV=$(C_CALL_CONV) -o $@ $<
+$(RELEASE_GRAPHICS): src/main/c/simple_graphics.c src/main/c/simple_graphics.h Makefile
+ @echo "compiling " $@
+ @mkdir -p $(dir $@)
+ @gcc -c -O2 -Wall -Wextra -o $@ $<
+
+# Optional native graphics backend. Nothing in the default build depends on it.
+include graphics.mk
+include audio.mk
+
+$(RELEASE_AUDIO): src/main/c/simple_audio.c src/main/c/simple_audio.h Makefile
+ @mkdir -p $(dir $@)
+ @gcc -c -O2 -Wall -Wextra -o $@ $<
+
+.PHONY: tests_nographics
+tests_nographics:
+ @SIMPLE_MINIFB=off SIMPLE_MINIAUDIO=off $(MAKE) tests
+
#######################################################
# Base launch line for JVM tests
diff --git a/README.ja.md b/README.ja.md
index 77f2d0b2..c331e47f 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -70,7 +70,7 @@ Simple 言語のスタイルは、C または Java のサブセットに倣っã�
* [第28b章](docs/chapter28b/README.ja.md):分離コンパイル
* [第29章](docs/chapter29/README.ja.md):技術的負債の整理
* [第30章](docs/chapter30/README.ja.md):テキスト整形と診断
-* 第31章:ソフトウェア描画とグラフィカルゲーム(作業中)
+* [第31章](docs/chapter31/README.ja.md):ソフトウェア描画とグラフィカルゲーム(作業中)
付録:[ネイティブライブラリ](docs/appendices/native-libraries.md)、
[最適化とバックエンドの調整](docs/appendices/optimizer-tuning.ja.md)。
diff --git a/README.md b/README.md
index 93a8cbd8..c30d8dc3 100644
--- a/README.md
+++ b/README.md
@@ -84,7 +84,7 @@ The Simple language is styled after a subset of C or Java.
* [Chapter 28b](docs/chapter28b/README.md): Separate Compilation
* [Chapter 29](docs/chapter29/README.md): Tech Debt Cleanup, Partial Objects
* [Chapter 30](docs/chapter30/README.md): Text Formatting and Diagnostics
-* Chapter 31: Software Drawing and Graphical Games (work in progress)
+* [Chapter 31](docs/chapter31/README.md): Software Drawing and Graphical Games (work in progress)
Appendices: [Native libraries](docs/appendices/native-libraries.md),
[optimizer and backend tuning](docs/appendices/optimizer-tuning.md).
diff --git a/audio.mk b/audio.mk
new file mode 100644
index 00000000..4aeeec20
--- /dev/null
+++ b/audio.mk
@@ -0,0 +1,31 @@
+# Optional miniaudio, independent of the default build and MiniFB.
+MINIAUDIO_REV := 0.11.23
+MINIAUDIO_SRC := lib/miniaudio-$(MINIAUDIO_REV)
+MINIAUDIO_HEADER := $(MINIAUDIO_SRC)/miniaudio.h
+ifeq ($(OS),Windows_NT)
+ AUDIO_CC := x86_64-w64-mingw32-gcc
+ AUDIO_LIB := $(RELEASE)/$(CPU_ABI)/simple_miniaudio.dll
+ AUDIO_FLAGS := -D_WIN32_WINNT=0x0601
+ AUDIO_LIBS := -lm
+else
+ AUDIO_CC := gcc
+ AUDIO_LIB := $(RELEASE)/$(CPU_ABI)/libsimple_miniaudio.so
+ AUDIO_FLAGS := -fPIC
+ AUDIO_LIBS := -lpthread -lm -ldl
+endif
+.PHONY: audio
+audio: $(AUDIO_LIB)
+.PHONY: test-audio
+test-audio: $(MINIAUDIO_HEADER)
+ @mkdir -p build/objs
+ @$(AUDIO_CC) -O2 -Wall -Wextra $(AUDIO_FLAGS) -I$(MINIAUDIO_SRC) src/test/c/audio_test.c $(AUDIO_LIBS) -o build/objs/audio_test$(if $(filter Windows_NT,$(OS)),.exe,)
+ @build/objs/audio_test$(if $(filter Windows_NT,$(OS)),.exe,)
+lib libs init: $(MINIAUDIO_HEADER)
+
+$(MINIAUDIO_HEADER):
+ @$(call download-library,https://raw.githubusercontent.com/mackron/miniaudio/$(MINIAUDIO_REV)/miniaudio.h)
+$(AUDIO_LIB): $(MINIAUDIO_HEADER) src/main/c/simple_miniaudio.c src/main/c/simple_audio.h audio.mk
+ @echo "building " $@
+ @mkdir -p $(dir $@)
+ @$(AUDIO_CC) -shared -O2 -Wall -Wextra $(AUDIO_FLAGS) -I$(MINIAUDIO_SRC) src/main/c/simple_miniaudio.c $(AUDIO_LIBS) -o $@
+ @sed -n '/^This software is available as a choice of the following licenses/,$$p' $(MINIAUDIO_HEADER) > $(dir $@)miniaudio-LICENSE
diff --git a/chapter30.iml b/chapter31.iml
index 793144ee..f4007a98 100644
--- a/chapter30.iml
+++ b/chapter31.iml
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="false">
- <output url="file://$MODULE_DIR$/../build/idea/production/chapter30" />
- <output-test url="file://$MODULE_DIR$/../build/idea/test/chapter30" />
+ <output url="file://$MODULE_DIR$/../build/idea/production/chapter31" />
+ <output-test url="file://$MODULE_DIR$/../build/idea/test/chapter31" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
diff --git a/docs/chapter31/31-grammar.ja.md b/docs/chapter31/31-grammar.ja.md
new file mode 100644
index 00000000..f4635f21
--- /dev/null
+++ b/docs/chapter31/31-grammar.ja.md
@@ -0,0 +1,182 @@
+# 第31章の文法
+
+[English](31-grammar.md) | 日本語
+
+```antlrv4
+grammar SimpleLanguage;
+
+// Name lookup, assignable operands, type compatibility, and access permissions
+// are checked by the compiler in addition to these syntax rules.
+// A file is a compilation unit; its path supplies its namespace.
+// Qualified names resolve nested types and other units without an import keyword.
+program : statement* EOF ;
+
+block : '{' statement* '}' ;
+
+statement
+ : block
+ | returnStatement
+ | ifStatement
+ | whileStatement
+ | forStatement
+ | breakStatement
+ | continueStatement
+ | structDeclaration
+ | declaration ';'
+ | assignment ';'
+ | ';'
+ ;
+
+returnStatement : 'return' assignment ';' ;
+ifStatement : 'if' '(' assignment ')' statement ('else' statement)? ;
+whileStatement : 'while' '(' assignment ')' statement ;
+breakStatement : 'break' ';' ;
+continueStatement : 'continue' ';' ;
+
+forStatement
+ : 'for' '(' (declaration | assignment)? ';' assignment? ';' assignment? ')' statement
+ ;
+
+structDeclaration : 'struct' IDENTIFIER '{' structMember* '}' ';' ;
+structMember : constructorDeclaration | methodDeclaration | statement ;
+
+// The constructor name must match the enclosing struct's name.
+constructorDeclaration : 'new' IDENTIFIER '=' constructorExpression ';' ;
+constructorExpression : '{' constructorParameterList? '->' statement* '}' ;
+constructorParameterList : (constructorParameter ','?)+ ;
+constructorParameter : type '^'? bindingModifier? IDENTIFIER ;
+// ^ accepts an unfinished struct reference; ? independently permits null.
+// Only self may be read while incomplete; ^ arguments may be stored/forwarded.
+// A new result is incomplete iff an actual argument is. See construction-groups.md.
+
+// A method is an ordinary fixed function binding in a struct definition.
+// The compiler supplies self; it is not written in parameterList.
+// Function types and expressions both receive their implicit receiver here.
+methodDeclaration : 'val' bindingModifier? IDENTIFIER '=' functionExpression ';' ;
+
+declaration : declarationType binding (',' binding)* ;
+declarationType : type | 'var' | 'val' ;
+binding : bindingModifier? IDENTIFIER ('=' initializer)? ;
+bindingModifier : '!' | '~' ;
+// A declaration initializer consisting of the string "C" declares an extern
+// symbol: a function, or explicit numeric native storage. Ordinary strings
+// elsewhere are array-valued expressions.
+initializer : assignment ;
+
+// The prefix qualifies the named struct reference, not an enclosing array.
+// Each [] / [~] suffix qualifies its own array layer. ? requires a reference.
+type
+ : functionType
+ | primitiveType typeSuffix*
+ | accessModifier? typeName typeSuffix*
+ ;
+accessModifier : '!' | '~' ;
+typeSuffix : '?' | '[]' | '[~]' ;
+typeName : IDENTIFIER ('.' IDENTIFIER)* ;
+
+primitiveType
+ : 'int' | 'i8' | 'i16' | 'i32' | 'i64'
+ | 'u1' | 'u8' | 'u16' | 'u32' | 'byte' | 'bool'
+ | 'flt' | 'f32' | 'f64'
+ ;
+
+assignment : expression (assignmentOperator assignment)? ;
+assignmentOperator
+ : '=' | '+=' | '-=' | '*=' | '/=' | '&=' | '|=' | '^='
+ | '<<=' | '>>=' | '>>>='
+ ;
+
+expression : logicalExpression ('?' assignment (':' assignment)?)? ;
+
+// Both logical operators short-circuit and associate to the right here.
+logicalExpression : bitwiseExpression (('&&' | '||') logicalExpression)? ;
+
+bitwiseExpression
+ : equalityExpression (('&' | '|' | '^') equalityExpression)*
+ ;
+
+equalityExpression : comparisonExpression (('==' | '!=') comparisonExpression)* ;
+
+// Chained comparisons test adjacent operands and short-circuit. A chain must
+// keep its direction; equality has lower precedence and does not join a chain.
+comparisonExpression
+ : shiftExpression
+ ( (('<' | '<=') shiftExpression)+ | (('>' | '>=') shiftExpression)+ )?
+ ;
+
+shiftExpression
+ : additiveExpression (('<<' | '>>' | '>>>') additiveExpression)*
+ ;
+
+additiveExpression
+ : multiplicativeExpression (('+' | '-') multiplicativeExpression)*
+ ;
+
+multiplicativeExpression
+ : unaryExpression (('*' | '/') unaryExpression)*
+ ;
+
+unaryExpression
+ : ('-' | '!') unaryExpression
+ | updateOperator IDENTIFIER
+ | postfixExpression
+ ;
+
+// Prefix updates require a local name; postfix updates also allow fields
+// and array elements. All updates require an assignable operand.
+updateOperator : '++' | '--' ;
+postfixExpression : primaryExpression postfixSuffix* updateOperator? ;
+postfixSuffix : '.' IDENTIFIER | '[' assignment ']' | '#' | callSuffix | methodCallSuffix ;
+
+// For a fixed function-valued member, object.name(args) passes object as self.
+// This overlaps ordinary field selection followed by a call syntactically;
+// name/type resolution determines which call form applies.
+methodCallSuffix : '.' IDENTIFIER callSuffix ;
+
+primaryExpression
+ : INTEGER_LITERAL
+ | FLOAT_LITERAL
+ | STRING_LITERAL
+ | CHARACTER_LITERAL
+ | 'true'
+ | 'false'
+ | 'null'
+ | IDENTIFIER
+ | '(' assignment ')'
+ | newExpression
+ | functionExpression
+ ;
+
+// A function is an expression bound with an ordinary declaration.
+// Its last statement supplies the implicit result; return can exit earlier.
+functionExpression : '{' parameterList? '->' statement* '}' ;
+parameterList : (parameter ','?)+ ;
+parameter : type bindingModifier? IDENTIFIER ;
+
+// Function types have unnamed, space-separated argument types.
+// {int} is the zero-argument type; its function expression still uses ->.
+functionType
+ : '{' type+ '->' type '}' '?'?
+ | '{' type '}' '?'?
+ ;
+
+callSuffix : '(' argumentList? ')' ;
+argumentList : assignment (',' assignment)* ','? ;
+
+// Allocation invokes the declared constructor; inline initializer blocks from
+// earlier chapters are replaced by constructor arguments.
+newExpression : 'new' type ('[' assignment ']' | callSuffix)? ;
+
+INTEGER_LITERAL : [1-9] DIGIT* | '0' [0-7]* | '0' [xX] HEX_DIGIT+ ;
+FLOAT_LITERAL : DIGIT+ ('.' DIGIT* ('e' DIGIT+)? | 'e' '-'? DIGIT+) ;
+STRING_LITERAL : '"' (ESCAPE | ~["\\])* '"' ;
+CHARACTER_LITERAL : '\'' (ESCAPE | ~['\\]) '\'' ;
+fragment ESCAPE : '\\' [0ntr\\'"] ;
+IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ;
+fragment NON_DIGIT : [a-zA-Z_] ;
+fragment DIGIT : [0-9] ;
+fragment HEX_DIGIT : [0-9a-fA-F] ;
+WHITESPACE : [ \t\r\n]+ -> skip ;
+LINE_COMMENT : '//' ~[\r\n]* -> skip ;
+BLOCK_COMMENT : '/*' .*? '*/' -> skip ;
+```
diff --git a/docs/chapter31/31-grammar.md b/docs/chapter31/31-grammar.md
new file mode 100644
index 00000000..a774e059
--- /dev/null
+++ b/docs/chapter31/31-grammar.md
@@ -0,0 +1,263 @@
+# Grammar for Chapter 31
+
+English | [日本語](31-grammar.ja.md)
+
+The handwritten recursive-descent implementation lives in
+[`com.seaofnodes.simple.parser`](../../src/main/java/com/seaofnodes/simple/parser/).
+The production-to-code map below the grammar describes where syntax ends and
+name resolution or graph construction begins. This grammar documents the
+language; it is not input to a parser generator.
+
+`int` is an alias for `i32`; `i64` explicitly selects 64-bit integer storage.
+This alias does not change literal inference or intermediate arithmetic.
+
+Integer literals use decimal (`42`), leading-zero octal (`052`), or hexadecimal
+(`0x2a`, `0X2A`). Octal digits are 0–7; `08` and `09` are errors. Hexadecimal
+and octal literals can spell any 64-bit bit pattern: the high bit gives the
+usual signed value, so `0xffffffffffffffff` is -1. Decimal literals retain
+their signed 64-bit limit. Floating literals remain decimal, including forms
+such as `012.5` and `012f`. A radix prefix does not change integer inference
+or storage width.
+
+```antlrv4
+grammar SimpleLanguage;
+
+// Name lookup, assignable operands, type compatibility, and access permissions
+// are checked by the compiler in addition to these syntax rules.
+// A file is a compilation unit; its path supplies its namespace.
+// Qualified names resolve nested types and other units without an import keyword.
+program : statement* EOF ;
+
+block : '{' statement* '}' ;
+
+statement
+ : block
+ | returnStatement
+ | ifStatement
+ | whileStatement
+ | forStatement
+ | breakStatement
+ | continueStatement
+ | structDeclaration
+ | declaration ';'
+ | assignment ';'
+ | ';'
+ ;
+
+returnStatement : 'return' assignment ';' ;
+ifStatement : 'if' '(' assignment ')' statement ('else' statement)? ;
+whileStatement : 'while' '(' assignment ')' statement ;
+breakStatement : 'break' ';' ;
+continueStatement : 'continue' ';' ;
+
+forStatement
+ : 'for' '(' (declaration | assignment)? ';' assignment? ';' assignment? ')' statement
+ ;
+
+structDeclaration : 'struct' IDENTIFIER '{' structMember* '}' ';' ;
+structMember : constructorDeclaration | methodDeclaration | statement ;
+
+// The constructor name must match the enclosing struct's name.
+constructorDeclaration : 'new' IDENTIFIER '=' constructorExpression ';' ;
+constructorExpression : '{' constructorParameterList? '->' statement* '}' ;
+constructorParameterList : (constructorParameter ','?)+ ;
+constructorParameter : type '^'? bindingModifier? IDENTIFIER ;
+// ^ accepts an unfinished struct reference; ? independently permits null.
+// Only self may be read while incomplete; ^ arguments may be stored/forwarded.
+// A new result is incomplete iff an actual argument is. See construction-groups.md.
+
+// A method is an ordinary fixed function binding in a struct definition.
+// The compiler supplies self; it is not written in parameterList.
+// Function types and expressions both receive their implicit receiver here.
+methodDeclaration : 'val' bindingModifier? memberName '=' functionExpression ';' ;
+memberName : IDENTIFIER | primitiveType ;
+
+declaration : declarationType binding (',' binding)* ;
+declarationType : type | 'var' | 'val' ;
+binding : bindingModifier? IDENTIFIER ('=' initializer)? ;
+// At class/struct member scope, memberName also permits primitive type names.
+// Ordinary locals and parameters still require a non-keyword IDENTIFIER.
+bindingModifier : '!' | '~' ;
+// A declaration initializer consisting of the string "C" declares an extern
+// symbol: a function, or explicit numeric native storage. Ordinary strings
+// elsewhere are array-valued expressions.
+initializer : assignment ;
+
+// The prefix qualifies the named struct reference, not an enclosing array.
+// Each [] / [~] suffix qualifies its own array layer. ? requires a reference.
+type
+ : functionType
+ | primitiveType typeSuffix*
+ | accessModifier? typeName typeSuffix*
+ ;
+accessModifier : '!' | '~' ;
+typeSuffix : '?' | '[]' | '[~]' ;
+typeName : IDENTIFIER ('.' IDENTIFIER)* ;
+
+primitiveType
+ : 'int' | 'i8' | 'i16' | 'i32' | 'i64'
+ | 'u1' | 'u8' | 'u16' | 'u32' | 'byte' | 'bool'
+ | 'flt' | 'f32' | 'f64'
+ ;
+
+assignment : expression (assignmentOperator assignment)? ;
+assignmentOperator
+ : '=' | '+=' | '-=' | '*=' | '/=' | '&=' | '|=' | '^='
+ | '<<=' | '>>=' | '>>>='
+ ;
+
+expression : logicalExpression ('?' assignment (':' assignment)?)? ;
+
+// Both logical operators short-circuit and associate to the right here.
+logicalExpression : bitwiseExpression (('&&' | '||') logicalExpression)? ;
+
+bitwiseExpression
+ : equalityExpression (('&' | '|' | '^') equalityExpression)*
+ ;
+
+equalityExpression : comparisonExpression (('==' | '!=') comparisonExpression)* ;
+
+// Chained comparisons test adjacent operands and short-circuit. A chain must
+// keep its direction; equality has lower precedence and does not join a chain.
+comparisonExpression
+ : shiftExpression
+ ( (('<' | '<=') shiftExpression)+ | (('>' | '>=') shiftExpression)+ )?
+ ;
+
+shiftExpression
+ : additiveExpression (('<<' | '>>' | '>>>') additiveExpression)*
+ ;
+
+additiveExpression
+ : multiplicativeExpression (('+' | '-') multiplicativeExpression)*
+ ;
+
+multiplicativeExpression
+ : unaryExpression (('*' | '/') unaryExpression)*
+ ;
+
+unaryExpression
+ : ('-' | '!') unaryExpression
+ | updateOperator IDENTIFIER
+ | postfixExpression
+ ;
+
+// Prefix updates require a local name; postfix updates also allow fields
+// and array elements. All updates require an assignable operand.
+updateOperator : '++' | '--' ;
+postfixExpression : primaryExpression postfixSuffix* updateOperator? ;
+postfixSuffix : '.' memberName | '[' assignment ']' | '#' | callSuffix | methodCallSuffix ;
+
+// For a fixed function-valued member, object.name(args) passes object as self.
+// This overlaps ordinary field selection followed by a call syntactically;
+// name/type resolution determines which call form applies.
+methodCallSuffix : '.' memberName callSuffix ;
+
+primaryExpression
+ : INTEGER_LITERAL
+ | FLOAT_LITERAL
+ | stringExpression
+ | CHARACTER_LITERAL
+ | 'true'
+ | 'false'
+ | 'null'
+ | IDENTIFIER
+ | '(' assignment ')'
+ | newExpression
+ | functionExpression
+ ;
+
+// A function is an expression bound with an ordinary declaration.
+// Its last statement supplies the implicit result; return can exit earlier.
+functionExpression : '{' parameterList? '->' statement* '}' ;
+parameterList : (parameter ','?)+ ;
+parameter : type bindingModifier? IDENTIFIER ;
+
+// Function types have unnamed, space-separated argument types.
+// {int} is the zero-argument type; its function expression still uses ->.
+functionType
+ : '{' type+ '->' type '}' '?'?
+ | '{' type '}' '?'?
+ ;
+
+callSuffix : '(' argumentList? ')' ;
+argumentList : assignment (',' assignment)* ','? ;
+
+// Allocation invokes the declared constructor; inline initializer blocks from
+// earlier chapters are replaced by constructor arguments.
+newExpression : 'new' type ('[' assignment ']' | callSuffix)? ;
+
+INTEGER_LITERAL : [1-9] DIGIT* | '0' [0-7]* | '0' [xX] HEX_DIGIT+ ;
+// The f suffix also accepts an integer-shaped token, e.g. 12f.
+FLOAT_LITERAL : DIGIT+ ('.' DIGIT* ('e' DIGIT+)? | 'e' '-'? DIGIT+) 'f'?
+ | DIGIT+ 'f' ;
+// String text uses a lexical mode: whitespace/comments are literal here.
+// Interpolation re-enters the ordinary expression grammar, recursively.
+stringExpression : '"' (STRING_TEXT | ESCAPE | '%%' | interpolation)* '"' ;
+interpolation : '%' format? '(' assignment ')' ;
+format : FORMAT_FLAG* FORMAT_WIDTH? ('.' FORMAT_WIDTH?)? FORMAT_KIND ;
+// Flags: - + space # 0. Width/precision: decimal digits, no expressions.
+// Kinds: d i u o x X f F e E g G a A c s b.
+// Duplicate/inapplicable flags and precision on c/b are rejected.
+// STRING_TEXT excludes double quote, backslash, and percent.
+CHARACTER_LITERAL : '\'' (ESCAPE | ~['\\]) '\'' ;
+fragment ESCAPE : '\\' [0ntr\\'"%] ;
+IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ;
+fragment NON_DIGIT : [a-zA-Z_] ;
+fragment DIGIT : [0-9] ;
+fragment HEX_DIGIT : [0-9a-fA-F] ;
+WHITESPACE : [ \t\r\n]+ -> skip ;
+LINE_COMMENT : '//' ~[\r\n]* -> skip ;
+BLOCK_COMMENT : '/*' .*? '*/' -> skip ;
+```
+
+## Reading the parser alongside the grammar
+
+All paths below are within `src/main/java/com/seaofnodes/simple/parser/`.
+The components share one `Parser` instance containing the cursor, active scope,
+compilation unit, and graph utilities. They are package-private collaborators;
+there is no inheritance chain, AST, or additional compiler pass.
+
+| Productions | Implementation |
+| --- | --- |
+| `program` | [`Parser.parse`](../../src/main/java/com/seaofnodes/simple/parser/Parser.java): establish the source-unit scope, parse its initializer, resolve remaining file names, publish its class |
+| `statement`, `block`, loops, jumps, `returnStatement` | [`Statements`](../../src/main/java/com/seaofnodes/simple/parser/Statements.java), starting at `parseStatement` |
+| `declaration`, `binding`, `bindingModifier` | [`Declarations`](../../src/main/java/com/seaofnodes/simple/parser/Declarations.java): declaration/expression lookahead, then each binding and initializer |
+| `structDeclaration`, `constructorDeclaration` | [`Structs`](../../src/main/java/com/seaofnodes/simple/parser/Structs.java), starting at `parseStruct` and `parseConstructorDeclaration`; field discovery and initialization checks follow these entry points |
+| `type`, `typeName`, `functionType` | [`Types`](../../src/main/java/com/seaofnodes/simple/parser/Types.java): `parseType` and `parseFunctionType`; qualified-name lookup lives in `typeName`, `dottedName`, and `rootedName` |
+| `assignment` through `primaryExpression` | [`Expressions`](../../src/main/java/com/seaofnodes/simple/parser/Expressions.java): `parseAssignment`, `parseExpression`, `parseLogical`, `parseBitwise`, `parseEquality`, `parseComparison`, `parseShift`, `parseAddition`, `parseMultiplication`, `parseUnary`, `parsePrimary` |
+| `postfixExpression`, member selection and updates | `Expressions.parsePostfix` / `parsePostfixMethod` dispatch to [`References.parseField`](../../src/main/java/com/seaofnodes/simple/parser/References.java); `References.parseName` handles bare names, implicit receivers, and forward references |
+| `functionExpression`, `parameterList`, calls, `argumentList` | [`Functions`](../../src/main/java/com/seaofnodes/simple/parser/Functions.java): `parseFunction`, `parseConstructor`, shared `parameters` and `arguments`, then `parseBody` |
+| `newExpression` | [`Allocations.parseNew`](../../src/main/java/com/seaofnodes/simple/parser/Allocations.java); the allocation-helper graph builder follows the expression routines |
+| `stringExpression`, `interpolation` | [`Strings.parse`](../../src/main/java/com/seaofnodes/simple/parser/Strings.java), using `Lexer.stringPart` / `stringFormat`; normal expression parsing resumes inside each interpolation |
+| Literals, identifiers, comments, token matching | [`Lexer`](../../src/main/java/com/seaofnodes/simple/parser/Lexer.java); it also supplies source-location snapshots used by diagnostics and serialized IR |
+
+`ControlFlow` shares guarded scope merging between `if`, `?:`, `&&`, and `||`.
+`Conversions` shares binding, field, and result conversions. These
+operations are compiler semantics rather than additional grammar productions.
+`Parser` retains the type registry and the small control/memory/peephole helpers
+used by several productions and by the rest of the compiler.
+
+There are a few deliberate differences between a syntax-only parser and this
+one-pass IR builder:
+
+- A statement beginning with `{` might be a block or a function-type binding.
+ `Declarations` and `Types` use speculative parsing and cursor restore;
+ qualified type lookup also stops before a suffix that belongs to an expression.
+- Bare assignment, field assignment, and compound updates share the grammar's
+ precedence but use different IR paths. `References` decides between an SSA
+ local, a constructor field's local value, and an actual memory access. A
+ function-valued field may also supply an implicit receiver to a call.
+- `for` updates are scanned first and parsed after the body, where their effects
+ belong. Explicit constructor bodies are scanned first and parsed after field
+ discovery, so later fields have local slots. Both use `Lexer.skipTo`, which
+ ignores delimiters inside comments and quoted text.
+- `Structs.parseInitializer` parses a file or struct body as a function while
+ discovering fields. `Functions.parseBody` still owns function scope/return
+ construction; moving it does not change constructor or memory semantics.
+
+The syntax chart does not describe all name/type restrictions. In particular,
+prefix updates require an already-known non-pointer binding, and a private-class
+privacy policy is separate from recognizing a qualified type name. The existing
+same-named function binding inside a struct is also recognized as a constructor;
+`new Name = { ... };` is the explicit constructor spelling above.
diff --git a/docs/chapter31/README.ja.md b/docs/chapter31/README.ja.md
new file mode 100644
index 00000000..eaa11cd1
--- /dev/null
+++ b/docs/chapter31/README.ja.md
@@ -0,0 +1,20 @@
+# 第31章:ソフトウェア描画とグラフィカルゲーム
+
+[English](README.md) | 日本語
+
+[前の章:第30章 — テキスト整形と診断](../chapter30/README.ja.md)
+
+第30章で完成したコンパイラと標準ライブラリを出発点とする章です。
+Simple で描画する `sys.draw.Canvas` と、MiniFB を使うオプションの
+ウィンドウ・入力 API `sys.gfx` を使って、グラフィカルな三目並べを実装します。
+矩形、円、太い線、色、ビットマップ文字を扱えます。
+詳細は[英語版の描画ライブラリとゲームの説明](drawing.md)を参照してください。
+通常のビルドとテストに MiniFB や画面表示は必要ありません。
+
+```sh
+make -C chapter31 run-graphical-tictactoe CTAGS=
+```
+
+このスナップショットの[文法](31-grammar.ja.md)と
+[実行可能な例](examples/)を用意しています。
+ビルド手順は[英語版](README.md)を参照してください。
diff --git a/docs/chapter31/README.md b/docs/chapter31/README.md
new file mode 100644
index 00000000..0e133fce
--- /dev/null
+++ b/docs/chapter31/README.md
@@ -0,0 +1,55 @@
+# Chapter 31: Software Drawing and Graphical Games
+
+English | [日本語](README.ja.md)
+
+[Previous: Chapter 30 — Text Formatting and Diagnostics](../chapter30/README.md)
+
+Small graphical games drive this chapter's language and library work, starting
+with a pixel window for a Simple-written 2-D graphics library. MiniFB supplies
+the optional native window/input backend; drawing stays in Simple.
+
+The [drawing library](drawing.md) provides a pixel canvas, colors,
+rectangles, circles, thick lines, and bitmap labels. The
+[graphical tic-tac-toe game](examples/TicTacToeGraphical.smp) uses it for
+mouse/keyboard play against the terminal game's CPU strategy.
+The optional [audio library](audio.md) adds soft move sounds and rising
+or falling win/loss cues, played in the background by miniaudio.
+Hexadecimal colors (`0xe47b57`) and leading-zero octal font masks (`025755`)
+are supported by this chapter's integer literal syntax.
+
+```sh
+make -C chapter31 libs CTAGS= # Once while online (also: init or lib).
+make -C chapter31 run-graphical-tictactoe CTAGS=
+```
+
+This explicitly builds the optional graphics/audio backends and opens a window. Use
+`graphical-tictactoe` instead to build the executable without starting it or
+requiring MiniFB.
+Dependencies stay cached under `chapter31/lib`, including across `make clean`.
+Only explicit `libs`/`init`/`lib` setup targets access the internet; other
+targets use local files and fail promptly if something needs downloading.
+
+The default build and test suite need no graphics dependencies. See
+[optional graphics](graphics.md) for building MiniFB, running the pixel
+example, and handling a missing backend. `make tests_nographics CTAGS=` runs
+all ordinary tests with graphics explicitly disabled.
+
+The inherited language and library are described in [Chapter 30](../chapter30/README.md).
+The [grammar and code map](31-grammar.md),
+[source layout](../../src/main/java/com/seaofnodes/simple/README.md), and
+[runnable examples](examples/) describe this snapshot.
+
+From the repository root:
+
+```sh
+make lib tests release CHAPTERS=chapter31 CTAGS=
+make -C chapter31 tictactoe CTAGS=
+```
+
+The game binary is `chapter31/build/examples/x86_64_v2_win64/TicTacToe.exe`
+on Windows and `chapter31/build/examples/x86_64_v2_SystemV/TicTacToe` on Linux.
+For a WSL build in the same checkout, use `CLZDIR=build/classes-linux` to keep
+Linux Java outputs separate from Windows outputs.
+
+IntelliJ has a `chapter31` module and a **chapter31 tests** run configuration.
+Inherited Java test names retain their historical chapter numbers.
diff --git a/docs/chapter31/audio.md b/docs/chapter31/audio.md
new file mode 100644
index 00000000..30817012
--- /dev/null
+++ b/docs/chapter31/audio.md
@@ -0,0 +1,62 @@
+# Optional background audio
+
+`sys.audio` uses [miniaudio](https://github.com/mackron/miniaudio) through an
+optional native C backend. Playback runs on its own thread; the game keeps
+processing input and drawing. No Python, CMake, codec packages, or sound files
+are needed. Default builds/tests never download miniaudio or open a device.
+
+From `chapter31`:
+
+```sh
+make libs CTAGS= # Once while online, or use make init.
+make audio CTAGS=
+make run-graphical-tictactoe CTAGS=
+```
+
+The last command builds both optional backends and runs the game. Accepted
+moves get a quiet 70ms sine boop. A player win gets a 480ms rising C-E-G
+celebration; a CPU win reverses the notes. Invalid input and occupied squares
+are silent. Replay stops any previous cue. Win/loss cues replace the ordinary
+move boop; draws use the ordinary boop.
+
+For direct execution in PowerShell, alongside `SIMPLE_MINIFB`:
+
+```powershell
+$env:SIMPLE_MINIAUDIO = (Resolve-Path build/release/x86_64_v2_win64/simple_miniaudio.dll).Path
+.\build\examples\x86_64_v2_win64\TicTacToeGraphical.exe
+```
+
+Linux uses `libsimple_miniaudio.so` in the corresponding release directory.
+`SIMPLE_MINIAUDIO` must name the full backend path. Unset it or set it to `off`
+for silent play. An unavailable audio device returns `ERROR` and the game
+continues. Audio and graphics can be configured independently.
+
+| Operation | Meaning |
+| --- | --- |
+| `open()` | Open output and generate built-in sounds; `OK` (0) or `ERROR` (-1). Repeated open is safe. |
+| `boop()`, `win()`, `lose()` | Play a built-in cue; same as `play(BOOP/WIN/LOSE)`. |
+| `load(samples)` | Copy `i16[~]` mono PCM at `RATE` (48000Hz); return handle or `ERROR`. |
+| `play(handle)` | Start/restart, replacing the current sound. Return `OK` or `ERROR`. |
+| `stop()` | Stop playback; safe before open/after close. |
+| `playing()` | Whether samples remain to submit; final device buffering may remain. |
+| `close()` | Join the audio thread and free device/buffers; safe to repeat. |
+
+All calls belong on the main/game thread. Load custom sounds at startup:
+there are 28 custom slots, each up to ten seconds, retained until close.
+Custom handles must be reloaded after close/reopen. Signed samples range
+from -32768 to 32767; quiet effects should use much smaller amplitudes.
+Native code copies the array and never retains a Simple heap pointer or calls
+Simple from the audio thread. Playback itself neither allocates nor waits for
+the sound duration. This first interface plays one sound at a time.
+
+`audio.mk` pins miniaudio 0.11.23, cached under `lib/miniaudio-0.11.23/` by
+`make libs` (or `init`; `lib` remains a setup alias). Only these explicit setup
+targets can download dependencies. All builds, runs, and tests use local
+copies, surviving `make clean`; missing cache entries fail promptly with setup
+instructions. Windows uses the existing MinGW-w64 compiler;
+Linux uses GCC and system pthread/math/dl libraries. No audio development
+headers need installing. The upstream header includes its public-domain and
+MIT-0 license options, copied beside the backend as `miniaudio-LICENSE`.
+`make test-audio CTAGS=` builds the backend checks from the local cache
+and runs a real audio thread with miniaudio's silent null
+device; normal tests exercise the loader and game without hardware.
diff --git a/docs/chapter31/construction-groups.md b/docs/chapter31/construction-groups.md
new file mode 100644
index 00000000..197a6082
--- /dev/null
+++ b/docs/chapter31/construction-groups.md
@@ -0,0 +1,241 @@
+# Construction: field types and incomplete references
+
+Instance constructors use ordinary field Loads and Stores. There are two checks:
+whether a field has a value, and whether an object reference is usable outside
+construction. Neither requires a collection of constructor-group identities.
+
+## Field initialization
+
+New produces private `TypeMem` containing the object's `TypeStruct`, with
+`TypeScalar.BOT` for fresh fields. Defaults emit ordinary Stores: zero for an
+ordinary integer, null for a nullable pointer, no Store for a required field.
+An assignment strongly updates its field in private memory. Phi meets the
+incoming memories; a missing live arm survives because `BOT.meet(7) == BOT`.
+An initialized unknown integer has `TypeInteger.BOT`, not `TypeScalar.BOT`.
+Arrays retain their language-level zero filling.
+
+One `#selfMem` binding carries this memory through scopes and returns. A Load
+checks its incoming field type. Constructor completion checks the merged Return
+memory; the explicit constructor receives the declaration initializer's merged
+result, including early exits. There is no backwards initialization query,
+per-field parser state, or store-all instance exit.
+
+The existing Iter/Opto passes propagate these types; TypeCheck reports errors.
+There is no extra construction type solve or retained source-use list. An
+initialized field carried unchanged around a loop needs the optimistic SCCP
+solution. Numeric fields in loop memory widen as scalar loop values do.
+Arithmetic, conversions, and numeric Stores preserve Scalar.BOT, so a live bad
+read cannot disappear merely because it is multiplied by zero. Genuinely unused
+reads may be removed by DCE.
+
+The allocator's ConstructNode retains the constructor's final private memory
+for its completion check. TypeCheck gathers these nodes during its existing
+error walk and removes checked boundaries before lowering. The pass removal
+exposes known failures; see [the late-check notes](deferred-construction.md).
+
+Internal construction memory parameters use actual allocation/initializer call
+edges; the hypothetical external caller does not supply their memory. They
+retain their memory family when there are no live callers. Function-valued
+fields retain their declared signatures in private summaries, avoiding repeated
+unfolding of a constructor whose returned memory contains its own function field.
+Class singleton initialization keeps its older store-at-exit implementation.
+
+## Partial references and readable fields
+
+`self` and incoming `C ^arg` parameters are incomplete references. `C? ^arg`
+also accepts null. Ordinary functions and constructors accept the qualifier on
+struct parameters; arrays cannot be qualified this way.
+
+- An allocation result is incomplete if any actual argument is incomplete.
+- With only completed arguments, including null, the result is complete.
+- An incomplete reference may be stored into self or passed to a `^` parameter.
+- Ordinary calls and stores into other objects require completed references.
+- A borrowed partial can read fields its incoming type proves initialized.
+ It cannot write fields, even non-finals. Exact local aliases of self retain
+ the original constructor's access to its current private memory.
+- An object-reference field read through a partial is conservatively
+ incomplete, with no initialized-field guarantees for its referent. There is
+ no inference of initialized-field requirements on `^` arguments.
+ Arrays are already initialized and cannot receive
+ incomplete elements, so an initialized array field can be used normally.
+
+For example:
+
+```simple
+struct Child {
+ Test.Parent? parent;
+ new Child = { Test.Parent? ^par -> parent = par; };
+};
+struct Parent {
+ Child child;
+ int~ payload;
+ new Parent = { int n ->
+ child = new Child(self); // Incomplete result: self is incomplete.
+ // Retention does not authorize reading parent.payload through child.
+ payload = n;
+ };
+};
+val parent = new Parent(arg);
+val local = new Child(parent); // Complete argument, complete result.
+return local.parent==parent; // Lookup is allowed.
+```
+
+Here `Test` names the source unit. After the root Parent constructor completes,
+the entire cycle is usable: all members have initialized their own fields, and
+none could expose an incomplete reference to ordinary code during construction.
+The Child constructor still treats its `^par` as incomplete when the actual
+argument is complete. This public constructor also has unknown callers, whose
+partial arguments promise no initialized fields, so `par.payload` is unreadable.
+
+## Forward field views
+
+At a use of self, `InitViewNode` combines the pointer with that program point's
+private memory. Its TypeMemPtr contains declared types for initialized fields
+and Scalar.BOT for missing fields. The node retains the declaration separately
+from the incoming flow shape, so an exact self alias captured before a Store
+can receive a fresh view afterward. Earlier calls retain their earlier views.
+
+For example, a private helper can read `x` while `y` is still unset:
+
+```simple
+struct _C {
+ int~ x;
+ int~ y;
+ new _C = { int n -> x=n; y=_read(self); };
+};
+val _read = { _C ^p -> return p.x; };
+return new _C(arg).y;
+```
+
+Parm meets the incoming shapes using the normal type flow. If any live caller
+lacks `x`, Scalar.BOT survives and the Load reports the missing initialization.
+Unknown callers supply an empty field promise. Parsing must also start borrowed
+parameters at that bound: otherwise early folding of `p.x*0` could hide a bad
+read before SCCP. The hidden constructor receiver retains its declaration;
+its private memory supplies its read checks and precise field updates.
+
+Values still come from normal memory. Before a call borrowing self, EscapeNode
+borrow slices carry the current constructor Stores onto the ordinary call
+memory edge. These slices are aliasable and do not publish the private identity
+or its fields into the global escape sets. No hidden partial-memory arguments,
+field bitsets, additional passes, or recursive flow queries are introduced.
+
+InitView folds before TypeCheck when its input already has the same type and
+retains a structural partial view. Nested partial-only Construct wrappers also
+collapse. A Parm's current partial type alone is insufficient: inlining may
+later substitute a complete actual argument, which must not grant the borrowed
+parameter permission to write or publish that object. The completion form of
+Construct already folds once its memory proves initialization and its result
+type matches its input.
+
+TypeCheck checks the consumers and erases remaining InitViews along with the
+checked Construct boundaries in its existing walk. A surviving field view is
+not necessarily erroneous: a reader may need only the initialized subset.
+Construct remains necessary for
+final-field completion and for the partial qualifier of each source `new`.
+Alias Phis and nested reference loads are conservative; they do not acquire
+the original constructor's write permission. This prototype adds no general
+ban on overlapping allocations of the same class: only the owning receiver
+uses private memory, while borrowed reads use aliasable memory.
+
+## Aliases and loops
+
+Incomplete is a property of a value, not just the current contents of a parser
+Var. Consider `while (n--) { use(p); p = self; }`: the backedge introduces an
+incomplete value after `use(p)` has been parsed. `TypeMemPtr._partial` carries
+that restriction through ordinary forward typing. It is an independent
+two-point lattice: meet ORs the restriction and dual complements it. Copies,
+conversions, Stores, and Phis preserve it; a merge is incomplete if any live
+input is incomplete. A `^` formal stays incomplete even when its known callers
+are complete. The receiver's private memory identifies the own-self exception;
+another incomplete pointer of the same class has no such permission.
+
+A small `ConstructNode` wraps each source `new`. Its runtime value is the
+allocator result; its type adds the OR of that call's actual argument qualifiers.
+This distinguishes `new Child(self)` from `new Child(null)` even when both call
+the same constructor. Looking only at the shared callee Return would lose this
+distinction. The node disappears after TypeCheck, before lowering.
+
+A completed receiver gives a completed view of its pointer fields. Construction
+may retain incomplete references but cannot publish them, so reaching the root
+`new` boundary with complete arguments makes the entire reachable cycle usable.
+There is no group identity or traversal to mark members complete. Conversely,
+object-reference fields read through incomplete self remain conservative, even
+if the particular stored reference was complete.
+
+The existing optimistic SCCP solve also handles qualifier cycles. For
+`p = null; while (n--) p = new Link(p);`, no input introduces incompleteness,
+so `p` stays complete and a subsequent field read is allowed. A backedge carrying
+self instead makes the Phi and its uses incomplete.
+
+`Construction` selects private self memory during parsing; it retains no
+source-use lists or flow facts and performs no recursive Load/Phi/Call query. The old
+root and allocation bitsets are gone. There are no field-requirement sets,
+hidden `^` memory arguments, or runtime group objects.
+
+`PartialReadTest` exercises field reads, missing fields, forwarding, merged
+callers, loops, exact aliases, constructor retention, and borrowed write and
+publication restrictions. `ConstructionGroupTest` exercises cycles, complete/null arguments, prohibited
+lookups/publication, aliases, branch merges, complete and incomplete loop
+backedges, and declared restrictions with only complete callers, including an
+uninlined constructor through machine encoding. `ConstructorCompletionTest`,
+`FinalConstructionTest`, and `BoundaryTest` exercise field-memory semantics.
+The former inferred-requirement litmus tests are preserved, explicitly deferred,
+in `ConstructionRequirementsTest`. Separate compilation of `^` functions is
+still deliberately unsupported. The pointer qualifier has an object-file
+representation, but importing and checking the full construction contract is
+outside this experiment. Rebuild chapter 29 object files after the type-format
+change.
+
+## Deferred field-aware contracts (2026-11-07)
+
+Decision: do not implement inferred contracts now. Cliff considers the required
+infrastructure too large for the feature alone; revisit when a general
+backwards/liveness flow is justified, potentially for memory management.
+The later forward-field-view prototype above does not implement this contract
+discovery proposal. Separate compilation of `^` interfaces remains rejected.
+
+The intended extension would let functions and constructors receive partially
+initialized objects, read initialized fields, retain partial references with
+their restrictions, and write non-final fields. Only the original constructor
+would retain permission to initialize/reassign its own final fields; passing a
+pointer must not pass that permission. Final-field reads during construction
+would not establish value stability against later writes by that constructor.
+
+The proposed contract representation uses normal types, not a separate field
+bitset. For a function reading only `P.pi`, its unknown-caller default would be
+schematically `Partial{pi:declared-pi-type, name:TypeScalar.BOT}`. Scalar.BOT
+means no initialization guarantee, not a requirement that the caller leave the
+field unset. Required fields receive their declared types, covering every legal
+external caller. MEET with actual arguments preserves those guarantees only
+when the callers satisfy them; extra initialized fields supplied by locals do
+not strengthen the body beyond the external contract. TypeStruct/Field already
+meet field contents, and nested/cyclic types can represent nested contracts.
+TOP is not the missing-initialization marker.
+
+Cliff proposed discovering conservative, context-insensitive requirements after
+Iter and before SCCP. Freeze the resulting default argument types on functions;
+SCCP consumes them without refining the contracts. Reads on branches SCCP later
+eliminates may still impose requirements, as may reads following assignments.
+That rejection of some valid programs is acceptable for an opt-in `^` feature.
+Combine compatible requirement shapes with JOIN; combine possible caller states
+with MEET. Keep the contract distinct from the merged Parm type, and check each
+actual argument against it for a call-site diagnostic such as
+"foo requires Partial.pi to be initialized". Serialize contracts with declaration
+metadata so checking imported calls does not require loading bodies.
+
+The complication is discovering and propagating those contracts. Local SSA
+aliases share values; Phi demands reach every input. A demand on a loaded field
+becomes a nested receiver requirement. Calls substitute actual arguments for
+formals, and recursive forwarding needs a convergent propagation rule. Cyclic
+types represent recursive requirements but do not by themselves prevent a
+discovery algorithm from unfolding progressively deeper paths. Indirect calls
+need contracts available through their callable interfaces; returns need usable
+guarantees without retroactively strengthening an exported contract for a client.
+Stored references and helper writes also require a coherent account of private
+construction memory and object identity; that discussion was deliberately left
+unfinished. None of this requires H-M or bidirectional type inference, but it
+does require a substantial backwards demand analysis to handle well.
+
+The ignored ConstructionRequirementsTest retains useful litmus cases, not an
+approved implementation plan. Do not restart this work without a new request.
diff --git a/docs/chapter31/deferred-construction.md b/docs/chapter31/deferred-construction.md
new file mode 100644
index 00000000..104ab9e9
--- /dev/null
+++ b/docs/chapter31/deferred-construction.md
@@ -0,0 +1,385 @@
+# Deferring construction checks: experiment
+
+The [escape-flow continuation](escape-flow.md) supersedes the frozen-public
+fallback and remaining recursive-test failures described later in these notes.
+All four recursive examples now pass their original printed-IR expectations.
+The older audit below records the evidence that led to this change.
+
+The extra construction pass is removed from the working implementation.
+`Construction.check()`, its retained source-use lists, `Iter.types()`, and the
+separate constructor-Return walk are gone. Errors now belong to the existing
+TypeCheck walk; ConstructNode retains final private memory for completion checks.
+No additional whole-graph pass was added. Constructor rejection tests remain active.
+
+## Constant folding
+
+`TypeScalar.BOT` is not an initialized integer. Arithmetic, comparisons,
+negation, numeric conversions, and typed Stores must preserve it rather than
+manufacturing an integer or floating-point result. In particular, `x*0` and
+`x==x` cannot become constants while `x` has this type. An initialized unknown
+integer still permits those folds.
+
+The scan found and changed these related cases:
+
+- Multiplication by zero now folds in `compute`, after checking numeric inputs.
+ Integer strength reduction also waits for integer operands; an unfinished
+ loop Phi could eventually become a float.
+- Same-input comparisons fold in `compute`. Floating comparisons keep NaN
+ semantics; `x==x` is not necessarily true. The integer range implementation
+ of `!=` also had the equality answers reversed.
+- Arithmetic pushed through Phis uses each copied operation's `compute`,
+ including floating-point constants, instead of an unchecked integer cast.
+- Known array length offsets and element shifts fold in
+ `ConFldOffNode.compute`, rather than its `idealize`.
+- `Not`, unary minus, conversions, and Stores preserve unknown initialization.
+ `RoundF32` already passed that type through.
+
+This does not ban creating constant *operands* in rewrites such as `x*8` to
+`x<<3`. Memory forwarding still has cases that return a stored constant or an
+array's zero-filled element; these were inspected but not rewritten into a new
+memory analysis. `Load.idealize` retains a `Scalar.BOT` load.
+
+The changes exposed a rejected-Phi-fold worklist cycle. Factoring
+`Phi(bool & 7, integer & bool)` widens the result from `bool` to `[0-7]` and
+must fail. Building and then killing the candidate woke the same dependencies
+forever. The trial now computes its types before registering use edges.
+
+## Preserving the constructor body through optimization
+
+This regression must reject a live incomplete lookup:
+
+```simple
+struct Child {
+ Test.Parent? parent;
+ new Child = { Test.Parent? ^par -> parent = par; };
+};
+struct Parent {
+ Child child;
+ int~ payload;
+ new Parent = { ->
+ child = new Child(self);
+ payload = child.parent == null;
+ };
+};
+return new Parent().payload;
+```
+
+`child` is incomplete because its constructor received incomplete `self`.
+Reading `child.parent` must therefore fail. The read supplies the final result;
+this is not a discarded local or dead-code issue.
+
+The decisive failure was private-memory Parm typing. During Iter, inlining
+removed a real caller while the function still had its unknown-caller edge.
+The special case returned MemTop anyway. That erased the memory merge, Store,
+and the bad Load feeding it, before TypeCheck could report the error.
+
+Parms now use their normal merge. Hidden initializer/constructor helpers have
+only actual allocator callers; the source-visible constructor binding denotes
+the allocator. Public allocators still receive unknown callers, so unused public
+constructor bodies remain checked. This also avoids poisoning a helper's private
+memory with a fictitious external default.
+
+Inlining presents a second issue: a complete actual pointer must not grant
+permission to read a restricted formal. A partial-only ConstructNode gives
+self and ^ parameters their body view independently of Parm typing. It remains
+until TypeCheck. Load's alias search recognizes that this view preserves the
+address, while compute/err continue to see the qualified pointer.
+
+A smaller regression reads an unwritten field through a self cycle:
+
+```simple
+struct C {
+ C next = self;
+ int~ x;
+ new C = { -> x = next.x; };
+};
+return new C().x;
+```
+
+Both examples now reject at TypeCheck. A positive regression initializes x to
+7, increments it in the constructor, and checks that the result folds to 8.
+
+The broader run exposed a function-lifetime bug during SCCP: unlinking calls
+could delete a body subsequently rediscovered from a type-level function
+reference. Returns are kept during the existing reset walk and released once
+call edges have been rebuilt. No additional graph walk is needed.
+
+Class-init dependency discovery also skips a CLInit whose receiver has not
+resolved to a class. A Scalar.BOT receiver is left for its Load's TypeCheck
+error, rather than dereferencing a null class name.
+
+The trial successfully uses the existing passes to prove a chain built from
+`null` complete. It also rejects direct `par.payload`, `par.payload*0`, and
+`int x = par.payload; value = x*0` after the arithmetic/conversion repairs.
+
+Genuinely unused reads are allowed to disappear under late-check semantics.
+Rejection tests must keep the read live. `testUnusedIncompleteReadCanDie` checks
+that an unused `par.payload` read is eliminated and the program returns 5.
+
+## Optimization differences and register allocation
+
+`Chapter18Test.testFcn9` exposed an independent escape bug. StopCU treated the
+class initializer's synthetic self as an object already held by an external
+caller. Its broad function-valued field type exposed the local x function even
+though the initializer never returned and x had no callers after inlining.
+Skipping that synthetic argument restores the empty escape set for x and the
+expected `return Top;`. Actual public-call arguments still escape normally.
+
+That correction exposed `Chapter18Test.testFcn8`: a Store computed from TOP
+memory interpreted its optimistic singleton bit as established private memory.
+It widened a function value to the declared signature, then narrowed its FIDX
+set when memory resolved. Store now returns TOP while its memory is TOP, so
+SCCP remains monotone.
+
+The String differences have a different cause. In Chapter19, the private
+_hashCodeString body dies; the surviving functions are the initializer,
+allocator, equals, and public hashCode. Its golden string now omits the two dead
+private-helper MemTop Returns and matches the current addition order.
+`Add.spine_cmp` breaks this tie by node ID. Strength reduction creates the Sub
+in IterPostOpto, after the byte Load (Sub#594 versus Load#504 in one traced copy),
+so the Sub sorts to the left. This is node ordering, not a lost value type.
+
+In the Chapter20/21 fixtures, _hashCodeString really escapes through the implicit
+return, and hashCode contains an inlined copy. Before the allocator repair, both loops needed an extra x86
+register copy with the new addition operand order. A disposable probe
+swapped only `((hash<<5)-hash) + byte` to `byte + ((hash<<5)-hash)` after Unlink:
+no types or live functions changed, but the weighted split score fell from 37
+to 21. Each hash body fell from 17 to 9; the allocator stayed at 3.
+
+Reconstructing the earlier source by reversing the saved construction-pass
+removal patch reproduces the expected 29. Its standalone _hashCodeString costs
+17, inlined hashCode costs 9, and allocator costs 3. Before the allocator repair,
+these costs were 17, 17, and 3. Both versions contain two hash loops; no inlining decision changed.
+Current code emits two fewer functions, the dead initializer/constructor helpers,
+which contributed zero. Thus the entire increase is one loop-weighted register
+copy in the inlined hashCode body, not another retained function or stack spill.
+
+With the old operand order, the byte Load can write the loop's hash register
+and Add accumulates the shifted/subtracted value into it. With the new order,
+Add's destructive input is the Sub result. Its two-address tie plus the Phi
+backedge joins the shift/Sub/Add chain to the old hash live range, which overlaps
+the shifted value at the subtraction. `RegAlloc.splitSelfConflict` inserts a
+`use/self/two` copy before Add. The existing commute heuristic in `splitByLoop`
+is not reached: self conflicts take an earlier splitting path. Commuting Add
+before splitting is the concrete optimization opportunity.
+
+The allocator now does this in `splitSelfConflict`, using the existing failed-
+range traversal and scratch list. It commutes two-address operators when the
+RHS has one use and belongs to a different live range, then omits the copy for
+the tie it broke. An allocator-generated Split is excluded: its single use can
+hide a longer-lived value, and commuting onto it can undo an earlier repair.
+The existing coloring retry rebuilds live ranges. There is no new whole-IR pass
+or analysis structure. The machine's `commutes()` hook covers Add, Mul, Xor,
+And, and Or (and commutative floating instructions).
+
+The String score is now 21: each hash body costs 9 and the allocator costs 3.
+The three x86 expectations are updated to 21. A single regression exercises
+all five integer operators in both machine operand orders, checks register
+constraints, and encodes the results. Existing testFlags2 guards against the
+Split-operand regression.
+
+## Incremental Iter setup
+
+Iter no longer resets and requeues the whole graph after parsing. `Node.init()`
+queues initialized nodes, and Calls queue themselves so Iter can link targets
+loaded after the call was parsed. Iter seeds its new global escape summary,
+then drains the worklist. Import loading still resets the newly loaded nodes
+and repairs affected neighbors.
+
+Removing the sweep also exposed an unused receiver left behind when field
+lookup switches from an instance to its class. That path now releases the keep
+and kills the receiver if unused.
+
+FldOffNode's base is data in slot 1. Slot 0 is reserved for control; putting a
+data parameter there made inliner body discovery exclude an unresolved offset.
+The regression `CallEndTest.testUnresolvedOffsetInInlineBody` checks that the
+offset belongs to the function body even before its type resolves.
+
+## Type upgrades in Opto setup
+
+ParseAll still closes the named types and freezes the public interface after
+loading all units. It no longer walks the IR. Opto installs those closed types
+and resolves forward references during its existing reset walk. Setup runs no
+peepholes against the partly reset graph: resolved FRefs propagate their input
+type in SCCP and disappear during subsequent peepholes. Construction identities
+also wait until this setup has installed complete field declarations.
+
+That walk retains Returns while call edges are rebuilt, gathers unresolved
+numeric modes, and records dependencies for cleanup after SCCP. Type upgrades
+also register cleanup dependencies, even if the eventual value type is unchanged.
+Old Iter types are snapshots for change detection, not upper bounds after
+recursive declarations have been closed. SCCP still asserts monotone falling
+types and a complete worklist within its own solve.
+
+The move exposed assumptions formerly hidden by early upgrades. Resolving an
+already known class name must retain its known fields instead of replacing them
+with an empty placeholder. A Load must not forward a stored pointer whose open
+shape is weaker than the Load's computed type. Escape must retain escapes from
+the actual private value before narrowing it to its declared field type, so
+selecting a Store out of a MemMerge does not introduce new escapes.
+
+An unknown return or memory effect carries FULL escape sets. StopCU and
+GlobalEscape preserve that bound: narrowing it to public names would omit
+private objects and callbacks that a subsequent load-forwarding rewrite can
+reveal. Both summaries recompute from their inputs without retaining previous
+escape sets. Refining a value removes possibilities; it never adds escapes
+outside the old bound. IterPeeps has no special summary handling, and its
+worklist assertion requires every node's type to lift, including queued nodes.
+`testUnknownEscapesRefineToPrivateValues` checks both escape sinks;
+`testPrivateRecursiveReturn` covers cyclic private objects and field loads.
+
+The ordinary dependency mechanism also covers signature refinement and lazy
+imports. FunNode depends on its Return when refining its signature. Calls
+depend on their target FunNode; while a target is absent, they depend on Start
+and retry when a linker mapping is installed. Loading bodies and adding call
+edges still use `Iter.repair` for graph changes, not for value refinement.
+
+SCCP's Start linking honors FULL for private targets too. Incoming arguments
+are not themselves outgoing escapes; Stores, Returns, and external-call
+arguments describe those effects. Constant singleton field slices carry the
+stored value's escapes, not unrelated bulk-memory escapes frozen at the time
+of folding. This preserves precision for local recursive callbacks.
+
+As bounds sharpen after SCCP, FunNode removes unknown callers even if concrete
+callers remain. Called initializers retain their Return through StopCU because
+CLInit has no Return edge. Removing an uncalled function queues the affected
+Stops, and compilation-unit wrappers retain distinct identities even when
+empty. These are worklist notifications and ownership rules, not extra passes.
+
+### Remaining precision issue with FULL escapes
+
+The monotone bounds expose a feedback problem in the existing grand cycle.
+In `Chapter12Test.testPrivateRecursiveReturn`, SCCP sees BOTTOM for the final
+load chain and therefore FULL escapes. These feed through Start's memory into
+the function memory results. Post-SCCP forwarding resolves the return to `_B`,
+but that memory cycle remains at MemBot and keeps private constructors callable
+by unknown callers. This is conservative, but loses the previous dead-function
+elimination. It does not justify narrowing BOTTOM to public names or adding
+the IterPeeps widening repair back.
+
+For this example, the root Return goes from `[Ctrl, MemBot, Bot]` after Opto to
+`[Ctrl, MemBot, *_B]` after TypeCheck; Stop's memory remains MemBot. The remaining
+design question is how to avoid this feedback while retaining conservative
+bounds and the existing pass budget. The current experiment leaves the printed
+IR expectations unchanged so they continue to expose this loss of precision.
+
+The source-validity audit separates these cases from Chapter10 `testIfOrNull`.
+That test originally returned the nullable pointer itself. A historical edit
+changed it to `return bar.a;` without adding TypeCheck, leaving a real null
+error hidden behind a printed-IR assertion. It now again returns `bar`, runs
+through TypeCheck, and passes with `return Phi(Region,null,Test._Bar);`.
+
+All four remaining programs pass TypeCheck. The Chapter12 recursive-object
+tests have one pending bad Load after Opto; forwarding eliminates every Load
+and resolves the result to `17`, `_B`, or `_R`, while escape memory stays MemBot.
+
+The two Chapter17b forward-type tests now pass their original IR expectations
+and explicitly run TypeCheck. Their undeclared `B` is an opaque external type,
+not an arbitrary unknown local value. EscapeNode uses the frozen public
+interface as its fallback for that type, preserving escapes already present
+in its private and public memory inputs. This applies to both the actual private
+value and the declared field bound: in `testForward1`, the private array element
+type is already `B?`. Bounding only FIDXs is insufficient; FULL aliases expose
+private class fields, which then expose their constructor FIDXs.
+
+CodeGen caches the fallback when ParseAll freezes the public interface. It
+returns FULL until Opto's existing reset walk installs the closed types into
+the IR. Only then does an open `_fref` reliably denote an external object.
+Ordinary open local structs, `TypeScalar.BOT`, and abstract pointers still
+require FULL. No new pass or IterPeeps hook is involved.
+
+### Escape-bound audit
+
+All Chapter29 `escapesFrom`, `XInt.FULL`, and `TypeMem.BOT` uses were reviewed,
+including the equivalent `escapes` helper and indirect `makePrivate` uses.
+The following remaining sites warrant work; they have not been narrowed here:
+
+* **StoreNode's three `escapesFrom(val)` paths.** The singleton constant,
+ private instance, and ordinary aliasing paths still treat an opaque external
+ value as FULL. A concrete example is:
+
+ ```simple
+ struct _A { B? !b; new _A = { B? p -> b=p; }; };
+ val wrap = { B? b -> return new _A(b); };
+ return wrap;
+ ```
+
+ This passes TypeCheck, but the surviving Stores of `b` have FULL FIDX and
+ alias sets and keep an extra `_A` allocator. Do not mechanically substitute
+ the public default: singleton slices deliberately discard unrelated incoming
+ memory escapes. A narrower opaque bound must retain private values previously
+ given to external code, without freezing unrelated transient escapes into
+ singleton constants. Ordinary aliasing Stores already retain incoming sets.
+
+* **StopCU return values, CallNode external arguments, and SummaryNode arguments.**
+ These use `escapes`, which currently duplicates the unbounded `escapesFrom`.
+ `val identity = { B? b -> return b; }; return identity;` passes TypeCheck but
+ gives StopCU MemBot. Likewise,
+ `val send = { B? b -> { B? -> int } retain = "C"; return retain(b); }; return send;`
+ gives the native CallEnd MemBot. These are concrete instances of the same
+ opaque-type precision loss. A future change can share the bounded helper,
+ while retaining incoming/returned memory, explicit private arguments, and
+ imported-summary effects. StopCU's public function seed and `escapesAliases`
+ class seed describe known declarations; GlobalEscape's `escapes` calls merely
+ union already computed TypeMem sets.
+
+* **CLInitNode's possible-initialization result.** Even a known initializer
+ yields TypeMem.BOT until its ready flag is true; only the initialized flag's
+ slice is precise. This loses both memory-content and escape precision.
+ CLInit has an analysis call edge but no Return edge supplying callee effects.
+ Improving it needs a dependency on the initializer's effects, including
+ recursive dependencies. The public interface alone is not a safe cap: that
+ initializer can publish private callbacks. Its unresolved-flag BOT fallback
+ is also conservative, but normally disappears when the receiver resolves.
+
+* **CallEndNode's unknown or unlinked target result.** Both return TypeMem.BOT.
+ The unlinked case covers invalid calls and transient missing edges; SCCP links
+ valid finite targets before computing CallEnd. Truly unknown targets can be
+ local private functions as well as external functions, so a public-only cap
+ would be wrong. Native calls already use CallNode's argument/memory effects.
+ Any refinement must distinguish target provenance and retain explicit private
+ arguments; it is not the same small fix as an opaque external object.
+
+The other broad defaults appear justified:
+
+* TypeMem's generic BOTTOM/scalar/pointer cases and TypeStruct's unknown scalar
+ fields must include private callbacks that later value refinement can reveal.
+ Private-memory declarations/Phis use `makePrivate(BOTTOM)` for the same reason.
+ MemMerge's `makePrivate(struct)` describes actual fields, not an external
+ opaque object.
+* Parser/function memory declarations, initial node types, TypeTuple.STATE,
+ and low-family error fallbacks in Store, MemMerge, BulkMemPhi, Start, and
+ TypeTuple need worst-case memory. CallNode and SummaryNode likewise cannot
+ promise bounded effects when their inputs are still arbitrary low values.
+* Iter's import reset gives newly loaded nonconstant memory FULL before repairing
+ affected neighbors. Saved types belong to another caller context; constants
+ retain their own computed types. This is a safe pessimistic seed, though it
+ can contribute to the existing memory-cycle precision problem.
+* TypeFunPtr's FULL signatures and TypeRPC.BOT describe unknown function/return
+ targets. Serialization/remapping preserves the FULL sentinel; type gathering
+ exercises it; LoopNode's `isa(TypeMem.BOT)` is a type-family check. These do
+ not independently manufacture an escape effect.
+
+Ignoring assertions, the remaining whole-node walks through TypeCheck are
+Opto's setup/reset and TypeCheck's error scan. Class-init checking separately
+scans function control flow and runs SCC over the resulting dependency graph.
+
+## Validation
+
+Chapter29's latest run tested 594 active cases with assertions: 590 passed and
+four printed-IR comparisons failed. `make -k tests` ran raw0 (400/404), raw1
+(38), standalone (13), system (29), and fuzzer (1); the aggregate group (109)
+was run separately because Make skips it after a prerequisite failure.
+The remaining failures retain extra private constructors: Chapter12 `testCoRecur`,
+`testCoRecur2`, `testCoRecur3`, and `testPrivateRecursiveReturn`. Both Chapter17b
+forward tests pass unchanged goldens. TypeTest.testOpaqueExternalEscapes checks
+both fallback sets, preservation of prior private escapes, and FULL for ordinary
+unknowns. System, spill, import, callback, and constructor tests pass. The only
+golden changed during the earlier source-validity audit belongs to the restored
+`return bar;` source; none were changed to accept extra functions.
+
+The offset regression fails with the old slot-0 layout. Constructor and
+boundary tests pass, including both missed rejections and the prior NPE.
+Printed-IR expectations omit the dead private-helper MemTop Returns; no
+rejection expectations were relaxed. The three String spill checks now expect
+21 on x86, with the same live functions and inlining decisions.
diff --git a/docs/chapter31/drawing.md b/docs/chapter31/drawing.md
new file mode 100644
index 00000000..3fec1f12
--- /dev/null
+++ b/docs/chapter31/drawing.md
@@ -0,0 +1,84 @@
+# Drawing in Simple
+
+`sys.draw` draws into an ordinary `u32[]`. It has no native dependencies and
+does not open a window. `sys.gfx` presents the resulting pixels using the
+optional [MiniFB backend](graphics.md).
+
+```simple
+val canvas = new sys.draw.Canvas(600,780);
+u32 orange = 0xe47b57;
+canvas.clear(sys.draw.WHITE);
+canvas.fillRect(20,20,180,80,orange);
+canvas.rect(20,20,180,80,3,sys.draw.BLACK);
+canvas.circle(300,200,48,8,sys.draw.BLUE);
+canvas.fillCircle(420,200,20,sys.draw.GREEN);
+canvas.line(40,320,250,440,12,orange);
+canvas.text(40,500,"YOUR TURN",4,sys.draw.BLACK);
+// After sys.gfx.open succeeds:
+sys.gfx.present(canvas.pixels);
+```
+
+## Small raster API
+
+Coordinates start at the upper left; x increases rightward and y downward.
+Drawing methods return the canvas for chaining. Everything is opaque RGB;
+there is no blending, antialiasing, transform stack, or native drawing state.
+Coordinates accept `i64` so ordinary widened arithmetic can be passed directly.
+Circle radii and circle stroke widths are `int`. Use screen-sized coordinates,
+extents, and stroke widths.
+
+| Operation | Behavior |
+| --- | --- |
+| `rgb(r,g,b)` | Clamp components to 0–255 and pack RGB. `BLACK`, `WHITE`, `RED`, `GREEN`, and `BLUE` are available too. |
+| `new Canvas(width,height)` | Allocate a zero-filled pixel array. Dimensions must be positive and their product must fit a signed 32-bit index; invalid sizes exit with status 1. Allocation can still fail for lack of memory. |
+| `clear(color)` | Fill the entire canvas. |
+| `pixel(x,y,color)` | Set one pixel; ignore points outside the canvas. |
+| `fillRect(x,y,w,h,color)` | Fill a half-open rectangle: x through x+w-1, y through y+h-1. |
+| `rect(x,y,w,h,thickness,color)` | Draw an inward outline. A sufficiently wide outline fills the rectangle. |
+| `fillCircle(cx,cy,radius,color)` | Fill a circle; radius zero draws its center pixel. |
+| `circle(cx,cy,radius,thickness,color)` | Draw an inward circular outline. |
+| `line(x0,y0,x1,y1,thickness,color)` | Draw a round-ended segment. A zero-length segment draws a round dot. Thickness is its geometric diameter, sampled at integer pixel centers. |
+| `text(x,y,string,scale,color)` | Draw a 3×5 bitmap font, with one column between characters. Upper/lowercase ASCII letters, digits, spaces, and basic punctuation are supported. Newline advances six scaled pixels. Unknown characters use `?`. |
+
+Shapes clip to the canvas. Nonpositive rectangle extents, stroke widths, or
+text scale do nothing; negative circle radii do nothing. Line and circle
+coverage scans only the clipped bounding box, which is sufficient for small
+turn-based games. This is a starting raster library rather than a fast animated
+renderer. The pixel array remains available for direct drawing and inspection.
+
+Packed colors use hexadecimal literals (`0xRRGGBB`); `rgb(r,g,b)` remains handy
+for computed components. The font masks use leading-zero octal, with five
+digits after the prefix: each digit is one three-pixel row, top to bottom.
+For example, `A` is `025755`: rows `010`, `101`, `111`, `101`, `101`.
+
+## Graphical tic-tac-toe
+
+From `chapter31`:
+
+```sh
+make run-graphical-tictactoe CTAGS=
+```
+
+Click an empty square or press **1–9**, numbered left-to-right, top-to-bottom.
+You are X; the CPU is O. It wins when possible, blocks your immediate win,
+then prefers the center, corners, and edges. The result stays visible with
+the winning line highlighted. Press **R** or **Space** to start a new game,
+or **Q**/**Escape** to quit. Restart is also available during a game.
+
+Held buttons and keys do not take repeated turns. Moving the pointer highlights
+empty cells. The canvas is redrawn only after a move, restart, or hover change;
+polling still processes window events and waits between polls.
+
+`make graphical-tictactoe CTAGS=` builds without opening a window or requiring
+MiniFB. Its output is `build/examples/x86_64_v2_win64/TicTacToeGraphical.exe`
+on Windows or `build/examples/x86_64_v2_SystemV/TicTacToeGraphical` on Linux.
+The explicit backend path follows [the pixel-window instructions](graphics.md).
+Passing any argument performs a five-poll, self-closing smoke test.
+
+The original `make tictactoe` still builds the terminal game.
+The graphical game also uses optional [background audio](audio.md): a soft
+boop for accepted moves, rising tones for a player win, and falling tones for
+a CPU win. Bad input is silent. The run target builds/configures both backends.
+`make test CLASS=DrawTest CTAGS=` checks raster pixels and complete game turns
+without a display. `make tests_nographics CTAGS=` runs the ordinary suite with
+the backend disabled.
diff --git a/docs/chapter31/escape-flow.md b/docs/chapter31/escape-flow.md
new file mode 100644
index 00000000..8ad49f15
--- /dev/null
+++ b/docs/chapter31/escape-flow.md
@@ -0,0 +1,147 @@
+# Outside-world escape flow
+
+The `escape-flow` branch developed this design in Chapter29 and backported it
+to Chapters 28a/28b. Chapter28a uses `LocalIds` and its existing initializer
+root/lowering protocol; Chapter28b adds saved summaries and cofinite identity
+remapping. Neither port imports Chapter29's deferred-construction model.
+
+## The cycle
+
+The outside world's access to local objects and functions is a value in the
+Start/Stop cycle. Public declarations are not that value. The frozen public
+interface inventory has been removed; public-method and public-alias tests
+check the converged outside escape sets directly.
+
+At Opto setup, the outer Start receives a constant foreign-identity seed:
+
+```
+foreign identities = FULL minus identities owned by this compilation
+outside identities = foreign identities union discovered local escapes
+```
+
+Known native function and storage identities are foreign too. The infinite
+tail permits future foreign functions even when no local callback has escaped.
+The finite exclusions include registered imported identities, including bodies
+represented by summaries. This scans identity tables, not the IR. Opto's
+existing reset/worklist solve discovers the local escapes.
+
+StartCU includes the shared outside state. Infinite FIDX sets must retain their
+exclusions: `Opto.linkStart` checks each local function's membership rather than
+treating every cofinite set as FULL. Serialization remaps a cofinite set by
+remapping its finite exclusions and complementing again.
+
+Before Opto, outside queries remain pessimistic. During SCCP, a consumer can
+read the foreign seed before Start has run: otherwise a temporary finite set
+can turn a generic foreign callback into an unrelated local function with the
+same arity. No previous escape result is accumulated to repair value flow.
+
+## Consumers and dependencies
+
+* Memory and callback defaults for unknown callers use Start's memory
+ projection. Parm applies its declared callback signature to the escaping
+ FIDX set. Defaults whose values do not depend on escapes remain constants.
+* Unknown/native CallEnd results use the outside state and the current call's
+ exposed arguments. Callback results are bounded by their declared signature
+ and the outside FIDX set. Explicit linked Return effects are retained.
+* CallEnd registers a direct dependency on Start. Depending only on Call is
+ insufficient: Call can stay CONTROL while the outside set changes. This
+ first implementation uses `addDepForwards`; it does not change CallEnd's
+ input layout or introduce a synthetic Fun/Return pair.
+* Infinite calls use the outside continuation. Escaped local functions remain
+ callable through Start, and their effects return through Stop. Attaching
+ every same-arity body to an infinite call is incorrect: argument/result
+ families may differ despite the equal arity.
+* Outgoing Call argument exposure is independent of CallEnd, including a call
+ that never returns. The shared fixed point also permits immediate callback
+ round trips and reentrant calls.
+* Opaque external values in Escape, Store, Return, and imported-summary paths
+ use the current outside/input-memory bound. Explicit private values and
+ already accumulated escapes remain included. Arbitrary local BOTTOM values
+ still require FULL.
+* Function-valued Loads use their memory input's FIDX bound, joined with the
+ field declaration. They do not independently manufacture an escape set.
+
+CLInit was another unconditional producer of FULL. During Opto it now depends
+on the known initializer's Return and merges those effects with incoming
+memory. It waits for live memory before publishing effects. Lowering computes
+the replacement CallEnd's linked type before exposing its projections, so a
+temporary unlinked MemBot cannot widen the already analyzed caller.
+
+## Why the recursive tests needed local precision too
+
+All four former Chapter12 printed-IR failures pass their original expectations.
+The problem was not solely an outside-call default:
+
+1. Convert widened a non-null pointer to a nullable declaration until its later
+ identity rewrite. It now preserves non-nullness in compute. Preserving the
+ entire source shape conditionally failed the existing monotonicity test;
+ shape and access still follow the declaration.
+2. A Load could wait until post-SCCP Store forwarding to recover that non-null
+ pointer. In the meantime the next dereference produced BOTTOM, which fed
+ FULL around the memory cycle and revived private constructors.
+3. Forwarded addresses, resolved field aliases, and already-bypassed CLInit
+ projections were also waiting for graph rewrites.
+
+Load's existing forwarding search now supplies pointer value facts during
+compute, without constructing or rewriting nodes. It can recognize a stored
+address through a valid pointer conversion or a proven forwarded Load, use a
+resolved named field before layout, and follow an existing CLInit bypass.
+The same dependency mechanism reruns the search when its evidence changes.
+Nullable/incomplete access checks still precede this sharpening; arbitrary
+memory Phis and unknown stores remain barriers. This adds local queries, not a
+new whole-program pass, analysis map, or second IR.
+
+## Hidden class receivers
+
+A class function's hidden `#self` defaults to its owning class singleton.
+Seeding that input with TypePtr.PTR used to give FULL escapes when forwarded
+to an unknown callback. For example:
+
+```simple
+val _hidden_noInline = { int n -> return n+2; };
+val apply_noInline = { {int->int} f,int n -> return f(n); };
+return apply_noInline;
+```
+
+`FunNode.defaultArg` now supplies the Test class singleton for this receiver,
+both during parsing and when Opto reconnects unknown callers. The callback
+parameter still reads Start dynamically. The printed-IR regression verifies
+that only `<clinit>` and `apply_noInline` escape; `_hidden_noInline` stays private.
+
+The function signature retains TypePtr.PTR in this slot. The backends use it
+to identify the hidden ABI argument, and callback annotations must not bind
+the receiver to the class where the annotation appears. Explicit calls still
+supply their actual receiver; instance methods keep their declared object
+type. This sharpens the default input, not every abstract pointer: a local
+merge of a private object and private callback can also have an abstract
+pointer type, so capping arbitrary TypePtr values at outside escapes is unsound.
+
+## Remaining design question
+
+Late imported identities also deserve further precision work: identities not
+registered when Opto starts lie in the foreign tail. Existing import tests
+exercise correctness, but this can remain conservative when bodies arrive
+later. This limitation is not repaired by resetting the whole IR.
+
+## Validation
+
+The combined `make -k tests CHAPTERS="chapter28a chapter28b chapter29" CTAGS=`
+run passes with assertions: 538 active tests in Chapter28a, 578 in Chapter28b,
+and 601 in Chapter29. Shared printer and ISA checks pass too. The log is saved
+under ignored `build/escape-backport-final.log`.
+
+`EscapeFlowTest` covers opaque wrappers with three worklist seeds, foreign
+callback parameters, private callbacks passed to and returned from native code,
+and a CallEnd dependency that responds when only Start's escape set changes.
+It also checks that public allocators escape while their private-memory helpers
+do not. Class-init tests cover unknown callback cycles; saved-library tests in
+28b/29 verify private-function exclusions survive identity remapping.
+The existing suites cover nonreturning native calls, class-init cycles,
+constructor rejection, serialization, imported callbacks, and three backends.
+The four recursive-class examples also pass a separate 16-case worklist-seed
+check. Backport IR expectations now retain public allocator Returns instead of
+private-memory helper Returns. The String allocation/encoding/export fixtures
+in 28a/28b/29 now declare `_hashCodeString` before the public `hashCode` wrapper,
+so the implicit return exposes the wrapper. Their measured spill expectations
+are 12/8/6 (x86/RISC-V/ARM); native x86 export expects 12. The reordered fixture
+also measures 8 on RISC-V before the backport, versus the old order's 6.
diff --git a/docs/chapter31/examples/A_helloWorld.smp b/docs/chapter31/examples/A_helloWorld.smp
new file mode 100644
index 00000000..4ed6abdf
--- /dev/null
+++ b/docs/chapter31/examples/A_helloWorld.smp
@@ -0,0 +1,2 @@
+sys.io.p("Hello, World!\n");
+return 0;
diff --git a/docs/chapter31/examples/BubbleSort.smp b/docs/chapter31/examples/BubbleSort.smp
new file mode 100644
index 00000000..6bdb13a9
--- /dev/null
+++ b/docs/chapter31/examples/BubbleSort.smp
@@ -0,0 +1,38 @@
+
+// An implementation of
+// https://sampleprograms.io/projects/bubble-sort/
+//
+// Bubble sort, with reading (parsing) and writing (formatting) the data.
+
+val sort = { !sys.aryi64 nums ->
+ sys.aryi64?! more = nums;
+ while( more ) {
+ more = null;
+ for( u32 i = 0; i < nums.len-1; i++ )
+ if( nums.buf[i] > nums.buf[i+1] ) {
+ u32 next = i+1;
+ more = nums.swap(i,next);
+ }
+ }
+};
+
+// Read one process argument of the form "[ 4, 3, 2, 1]" and print it sorted.
+// args[0] is the executable name, as with native C argc/argv.
+val main = { u8[~][] args ->
+ val usage = { ->
+ sys.io.p("Usage: please provide a list of at least two integers to sort in the format \"[1, 2, 3, 4, 5]\"\n");
+ return 0;
+ };
+ if( args# != 2 ) return usage();
+
+ var nums = sys.aryi64.parseAryI64(args[1]);
+ if( !nums ) return usage();
+ if( nums.len < 2 ) return usage();
+
+ // Bubble sort array of integers
+ sort(nums);
+
+ // Print the same bracketed array-list format accepted as input.
+ nums.str().add('\n').write(1);
+ return 0;
+};
diff --git a/docs/chapter31/examples/Capitalize.smp b/docs/chapter31/examples/Capitalize.smp
new file mode 100644
index 00000000..00fdd481
--- /dev/null
+++ b/docs/chapter31/examples/Capitalize.smp
@@ -0,0 +1,23 @@
+
+// An implementation of
+// https://sampleprograms.io/projects/capitalize/
+
+val main = { u8[~][] args ->
+ if( args# > 2 ) {
+ sys.io.p("Use quotes around multiple strings.\n");
+ return 0;
+ }
+ if( args# != 2 || args[1]# == 0 ) {
+ sys.io.p("Usage: please provide a string\n");
+ return 0;
+ }
+
+ u8[~] input = args[1];
+ var output = new u8[input# + 1];
+ u32 len = input#;
+ sys.ary.copy(output,0,input,0,len);
+ output[0] = sys.char.toupper(input[0]);
+ output[input#] = '\n';
+ sys.io.p(output);
+ return 0;
+};
diff --git a/docs/chapter31/examples/Dijkstra.smp b/docs/chapter31/examples/Dijkstra.smp
new file mode 100644
index 00000000..768bdd08
--- /dev/null
+++ b/docs/chapter31/examples/Dijkstra.smp
@@ -0,0 +1,74 @@
+
+// An implementation of
+// https://sampleprograms.io/projects/dijkstra/
+
+val shortestPath = { !sys.aryi64 matrix, int nodes, int src, int dst ->
+ i64 infinity = (1 << 63)-1;
+ var dist = new i64[nodes];
+ var visited = new sys.adt.bitset();
+ // All nodes are infinitely far from each other
+ for( int i=0; i<nodes; i++ ) dist[i] = infinity;
+ // Starting point is zero far from starting point
+ dist[src] = 0;
+
+ i64 result = -1;
+ int count = 0;
+ while( count < nodes && result < 0 ) {
+ int nearest = -1;
+ i64 best = infinity;
+ for( int node=0; node<nodes; node++ )
+ if( !visited.get(node) && dist[node] < best ) {
+ nearest = node;
+ best = dist[node];
+ }
+ if( nearest < 0 ) count = nodes;
+ else {
+ if( nearest == dst ) return best;
+ visited.set(nearest);
+ for( int node=0; node<nodes; node++ ) {
+ u32 index=nearest*nodes + node;
+ i64 weight = matrix.get(index);
+ if( weight > 0 && !visited.get(node) && best+weight < dist[node] )
+ dist[node] = best+weight;
+ }
+ }
+ count++;
+ }
+ return result;
+};
+
+val usage = { ->
+ sys.io.p("Usage: please provide three inputs: a serialized matrix, a source node and a destination node\n");
+ return 0;
+};
+
+val main = { u8[~][] args ->
+ // Exactly 3 args
+ if( args# != 4 ) return usage();
+ // First is a serialized square matrix of positive numbers
+ var matrix = sys.aryi64.parseAryI64(args[1]);
+ if( !matrix ) return usage();
+ if( !matrix.len ) return usage();
+ // All numbers are positive
+ for( u32 i=0; i<matrix.len; i++ )
+ if( matrix.get(i) < 0 ) return usage();
+ // Matrix is square
+ int nodes = 0;
+ while( nodes*nodes < matrix.len ) nodes++;
+ if( nodes*nodes != matrix.len )
+ return usage(); // Not a square matrix
+
+ // src,dst are values within the matrix
+ i64 src = sys.aryu8.atoi(args[2]);
+ i64 dst = sys.aryu8.atoi(args[3]);
+ if( src < 0 || src >= nodes || dst < 0 || dst >= nodes )
+ return usage(); // Source and destination land within the graph
+
+ int source=src, destination=dst;
+ i64 cost = shortestPath(matrix,nodes,source,destination);
+ if( cost >= 0 ) {
+ new sys.aryu8().num(cost).add('\n').write(1);
+ return 0;
+ }
+ return usage();
+};
diff --git a/docs/chapter31/examples/FileIO.smp b/docs/chapter31/examples/FileIO.smp
new file mode 100644
index 00000000..59b3159e
--- /dev/null
+++ b/docs/chapter31/examples/FileIO.smp
@@ -0,0 +1,52 @@
+
+// An implementation of
+// https://sampleprograms.io/projects/file-input-output/
+
+val FILE = "output.txt";
+
+val main = { u8[~][] args ->
+ var str = new sys.aryu8();
+ str.addAry("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n");
+ str.addAry("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n");
+ str.addAry("Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n");
+ str.addAry("Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n");
+
+ // Create
+ i32 fd = sys.io.create(FILE);
+ if( fd == -1 ) return _error();
+ // Write
+ u32 len = str.len;
+ int wrote = sys.io.writeOff(fd,str.buf,0,len);
+ if( wrote == -1 ) return _error();
+ if( wrote != str.len ) return _error2(-2);
+ // Close
+ if( sys.io.close(fd) != 0 ) return _error();
+
+ // Open
+ i32 fd2 = sys.io.open(FILE);
+ if( fd2 == -1 ) return _error();
+ // Read
+ var got = sys.io.read(fd2);
+ if( !got ) return _error();
+ if( got# != str.len ) return _error2(-3);
+ // Close
+ if( sys.io.close(fd2) != 0 ) return _error();
+
+ // Check contents are equal
+ if( sys.ary.cmp(got,0,str.buf,0,len) )
+ return _error2(-4);
+
+ // Remove
+ if( sys.io.remove(FILE) != 0 ) return _error();
+
+ sys.io.p("File I/O succeeded\n");
+ return 0;
+};
+
+val _error2 = { i32 errno ->
+ sys.io.remove(FILE);
+ new sys.aryu8().addAry("Failed with error code ").num(errno).add('\n').write(sys.libc.STDERR);
+ sys.libc._exit(errno);
+};
+
+val _error = { -> _error2(sys.libc.errno()); };
diff --git a/docs/chapter31/examples/Graphics.smp b/docs/chapter31/examples/Graphics.smp
new file mode 100644
index 00000000..7ba5c6e7
--- /dev/null
+++ b/docs/chapter31/examples/Graphics.smp
@@ -0,0 +1,17 @@
+// Optional graphics smoke test: pixels are drawn entirely in Simple.
+// Pass any argument for a short, self-closing test; otherwise close or press Esc.
+val main = { u8[~][] args ->
+ if( sys.gfx.open("Simple pixels",320,240) != sys.gfx.OK ) return 1;
+ val pixels = new u32[320*240];
+ for( int y=0; y<240; y++ )
+ for( int x=0; x<320; x++ )
+ pixels[y*320+x] = (x*255/319 << 16) | (y*255/239 << 8) | 64;
+ int result = sys.gfx.present(pixels);
+ int frames = 0;
+ while( result == sys.gfx.OK ) {
+ result = sys.gfx.poll(true);
+ if( args# > 1 && ++frames == 5 ) break;
+ }
+ sys.gfx.close();
+ return result == sys.gfx.ERROR ? 1 : 0;
+};
diff --git a/docs/chapter31/examples/README.md b/docs/chapter31/examples/README.md
new file mode 100644
index 00000000..a1d81be2
--- /dev/null
+++ b/docs/chapter31/examples/README.md
@@ -0,0 +1,78 @@
+# Runnable examples
+
+These programs live beside the documentation so readers can find and run them.
+Their non-graphical native regression tests run as part of `make tests`.
+The optional [pixel-window example](Graphics.smp) uses MiniFB; see
+[graphics setup](../graphics.md). `make run-graphics` opens it explicitly.
+The [graphical tic-tac-toe](TicTacToeGraphical.smp) uses the Simple
+[drawing library](../drawing.md). Run it with
+`make run-graphical-tictactoe CTAGS=`; click a cell or press 1–9, R/Space to
+replay, and Q/Escape to quit. The terminal game below remains available.
+Optional [background audio](../audio.md) supplies soft move and win/loss cues.
+
+From `chapter31`, build the tic-tac-toe executable (without starting it):
+
+```sh
+make lib tictactoe CTAGS=
+```
+
+Run the Windows build from PowerShell:
+
+```powershell
+.\build\examples\x86_64_v2_win64\TicTacToe.exe
+```
+
+Or from Cygwin:
+
+```sh
+./build/examples/x86_64_v2_win64/TicTacToe.exe
+```
+
+The Windows executable uses Cygwin's runtime; its `bin` directory must be on
+`PATH` (as it is when building with Cygwin GCC).
+
+For a native Linux build, run these commands from this same `chapter31`
+directory in WSL. Separate Java and native output directories let the Windows
+and Linux builds coexist:
+
+```sh
+make lib tictactoe CLZDIR=build/classes-linux CTAGS=
+./build/examples/x86_64_v2_SystemV/TicTacToe
+```
+
+From PowerShell, prefix either command with `wsl -d Ubuntu-24.04 --`.
+On a standalone Linux checkout, `CLZDIR` can be omitted.
+The compiler's `--link` option produces an executable without running it;
+`--norun` continues to produce object files only.
+
+`TicTacToe.smp` currently prints the welcome, square numbering, and empty board,
+then waits for a move. Press **1–9** without Enter to place X, or
+**q**, Escape, Ctrl-C, or Ctrl-D to quit. Enter is silently ignored; other
+invalid keys and occupied squares prompt again without taking a turn. The board
+is printed after each player and CPU move; only accepted move digits are echoed.
+Play continues until someone wins or the board is
+full. The CPU takes an immediate win, blocks an immediate loss, then prefers
+the center, corners, and edges.
+
+Other examples are Hello World (`A_helloWorld.smp`), `BubbleSort.smp`,
+`Capitalize.smp`, `Dijkstra.smp`, and `FileIO.smp`. The latter creates, checks,
+and removes `output.txt` in the working directory.
+
+## Single-keystroke input
+
+`sys.io.getch()` blocks for one byte without terminal editing or echo. It returns
+0–255 for a byte, or -1 for EOF or an error. On -1, `sys.libc.errno()` is
+`sys.libc.EOF` (-1) for end-of-input, otherwise the OS error code.
+Terminal Ctrl-C and Ctrl-D are bytes 3
+and 4, so the application decides how to handle them. Unicode and function keys
+may produce multiple bytes; this is an ASCII-oriented byte API, not a key-event
+decoder.
+
+The small C runtime adapter uses [POSIX terminal modes](https://man7.org/linux/man-pages/man3/termios.3.html),
+including Cygwin on Windows. It restores the saved mode before returning,
+including after an interrupted read. Files and pipes use an ordinary one-byte
+read. `make test CLASS=TerminalInputTest` runs the Java regression tests for
+redirected input, errors, prompt output, Enter, and Escape. Check terminal
+editing, echo, and mode restoration by running the executable in a terminal.
+`make test CLASS=TicTacToeTest` checks complete games, all eight winning lines,
+draws, occupied squares, and CPU blocking.
diff --git a/docs/chapter31/examples/TicTacToe.smp b/docs/chapter31/examples/TicTacToe.smp
new file mode 100644
index 00000000..a710e440
--- /dev/null
+++ b/docs/chapter31/examples/TicTacToe.smp
@@ -0,0 +1,133 @@
+// Player is X, CPU is O. Squares are numbered left-to-right, top-to-bottom.
+val main = { u8[~][] args ->
+ sys.io.p("Tic-tac-toe!\nYou are X; the CPU is O. You go first.\n");
+ sys.io.p("Press a square's number (no Enter needed), or q/Esc to quit.\n");
+ sys.io.p("Squares:\n 1 | 2 | 3 \n---+---+---\n 4 | 5 | 6 \n---+---+---\n 7 | 8 | 9 \n\n");
+ val board = new u8[9];
+ for( int i=0; i<board#; i++ ) board[i]=' ';
+ // Draw empty board
+ _draw(board);
+
+ while( true ) {
+ sys.io.p("Your move (1-9): ");
+ // Valid user move 1-9, or 0 for quitting
+ int square = _validMove(board) - 1;
+ if( square < 0 ) return 0;
+ // Set user choice; print board
+ board[square] = 'X';
+ sys.io.p("\n");
+ _draw(board);
+ // A winner?
+ if( _finished(board) ) return 0;
+ // Compute CPU move
+ int reply = _cpu(board);
+ // Set CPU choice; print board
+ board[reply]='O';
+ sys.io.p("CPU plays %(reply+1).\n");
+ _draw(board);
+ // A winner?
+ if( _finished(board) ) return 0;
+ }
+};
+
+// Return with a valid user move 1-9, or 0 to exit
+val _validMove = { u8[~] board ->
+ while( true ) {
+ int key = sys.io.getch();
+ if( key == -1 && sys.libc.errno() != sys.libc.EOF ) {
+ sys.io.err("\nUnable to read the keyboard.\n");
+ return 0;
+ }
+ // Various quitting conditions
+ if( key == -1 || key == 'q' || key == 'Q' || key == 27 || key == 3 || key == 4 ) {
+ sys.io.p("\nGoodbye!\n");
+ return 0;
+ }
+ // Ignore CR/LF
+ if( key == '\r' || key == '\n' ) continue;
+
+ // Valid choice
+ if( key < '1' || key > '9' ) {
+ sys.io.p("\nChoose a square from 1 to 9: ");
+ continue;
+ }
+ if( board[key-'1'] != ' ' ) {
+ sys.io.p("\nThat square is occupied. Your move (1-9): ");
+ continue;
+ }
+ sys.io.p("%c(key)");
+ return key-'0';
+ }
+};
+
+// Return the winning mark, or zero.
+val _winner = { u8[~] board ->
+ for( int i=0; i<3; i++ ) {
+ int row = i*3;
+ if( board[row]!=' ' && board[row]==board[row+1] && board[row]==board[row+2] )
+ return board[row];
+ if( board[i]!=' ' && board[i]==board[i+3] && board[i]==board[i+6] )
+ return board[i];
+ }
+ if( board[4]!=' ' ) {
+ if( board[0]==board[4] && board[4]==board[8] ) return board[4];
+ if( board[2]==board[4] && board[4]==board[6] ) return board[4];
+ }
+ return 0;
+};
+
+val _finished = { u8[~] board ->
+ int winner = _winner(board);
+ if( winner=='X' ) { sys.io.p("You win!\n"); return true; }
+ if( winner=='O' ) { sys.io.p("CPU wins!\n"); return true; }
+ for( int i=0; i<board#; i++ )
+ if( board[i]==' ' ) return false;
+ sys.io.p("It's a draw!\n");
+ return true;
+};
+
+// Try each empty square, restoring it before returning.
+val _threat = { u8[] board, u8 mark ->
+ for( int i=0; i<board#; i++ ) {
+ if( board[i]==' ' ) {
+ board[i]=mark;
+ bool wins = _winner(board)==mark;
+ board[i]=' '; // Restore board
+ if( wins ) return i;
+ }
+ }
+ return -1;
+};
+
+// Win if possible, otherwise block, then prefer center, corners, and edges.
+// Called only after _finished has ruled out a full board.
+val _cpu = { u8[] board ->
+ // If O can win, do so
+ int win = _threat(board,'O');
+ if( win>=0 ) return win;
+ // If X will win next move, block
+ int block = _threat(board,'X');
+ if( block>=0 ) return block;
+ // If center is open, take it
+ if( board[4]==' ' ) return 4;
+ // If a corner is open, take it
+ for( int i=0; i<board#; i+=2 )
+ if( board[i]==' ' ) return i;
+ // Take whatever is available
+ for( int i=0; i<board#; i++ )
+ if( board[i]==' ' ) return i;
+ return -1;
+};
+
+val _draw = { u8[~] board ->
+ val out = new sys.aryu8();
+ for( int row=0; row<3; row++ ) {
+ for( int col=0; col<3; col++ ) {
+ out.add(' ').add(board[row*3+col]).add(' ');
+ if( col<2 ) out.add('|');
+ }
+ out.add('\n');
+ if( row<2 ) out.addAry("---+---+---\n");
+ }
+ out.write(sys.libc.STDOUT);
+};
diff --git a/docs/chapter31/examples/TicTacToeGraphical.smp b/docs/chapter31/examples/TicTacToeGraphical.smp
new file mode 100644
index 00000000..b4f8bcf4
--- /dev/null
+++ b/docs/chapter31/examples/TicTacToeGraphical.smp
@@ -0,0 +1,167 @@
+// Player is X, CPU is O. Squares are numbered left-to-right, top-to-bottom.
+// Graphical tic-tac-toe; the same strategy as TicTacToe.smp.
+// Click an empty square or press 1-9. R/Space replays; Q/Escape quits.
+// Pass any argument for a five-poll window smoke test.
+val main = { u8[~][] args ->
+ val screen = new sys.draw.Canvas(600,780);
+ val board = new u8[9];
+ _reset(board);
+ if( sys.gfx.open("Simple tic-tac-toe",screen.width,screen.height) != sys.gfx.OK ) return 1;
+ sys.audio.open(); // Audio is optional; failure leaves the game silent.
+ _draw(screen,board,-1);
+ int result = sys.gfx.present(screen.pixels);
+ bool mouseHeld = false, keyHeld = false, replayHeld = false;
+ int hover = -1, frames = 0;
+ while( result==sys.gfx.OK ) {
+ result = sys.gfx.poll(true);
+ if( result!=sys.gfx.OK ) break;
+ if( sys.gfx.key('Q') ) break;
+ bool down = sys.gfx.button(sys.gfx.LEFT);
+ bool replay = sys.gfx.key('R') || sys.gfx.key(' ');
+ int square = _square(sys.gfx.mouseX(),sys.gfx.mouseY());
+ bool dirty = square!=hover;
+ hover = square;
+ if( replay && !replayHeld ) { sys.audio.stop(); _reset(board); dirty=true; }
+ bool digit = false;
+ int choice = down && !mouseHeld ? square : -1;
+ for( int i=0; i<9; i++ ) {
+ int key = '1'+i;
+ if( sys.gfx.key(key) ) {
+ digit = true;
+ if( !keyHeld ) choice=i;
+ }
+ }
+ if( !(replay && !replayHeld) && _move(board,choice) ) {
+ sys.audio.play(_sound(board));
+ dirty=true;
+ }
+ mouseHeld = down;
+ keyHeld = digit;
+ replayHeld = replay;
+ if( dirty ) {
+ _draw(screen,board,hover);
+ result = sys.gfx.present(screen.pixels);
+ }
+ if( args#>1 && ++frames==5 ) break;
+ }
+ sys.audio.close();
+ sys.gfx.close();
+ return result==sys.gfx.ERROR ? 1 : 0;
+};
+
+// Called only for an accepted move; invalid input never requests a sound.
+val _sound = { u8[~] board ->
+ int winner = _winner(board);
+ return winner=='X' ? sys.audio.WIN : winner=='O' ? sys.audio.LOSE : sys.audio.BOOP;
+};
+
+val _reset = { u8[] board ->
+ for( int i=0; i<9; i++ ) board[i]=' ';
+};
+val _square = { int x, int y ->
+ if( x<48 || x>=552 || y<168 || y>=672 ) return -1;
+ return (y-168)/168*3+(x-48)/168;
+};
+
+// One player turn followed by one CPU turn. Invalid/finished moves do nothing.
+val _move = { u8[] board, int square ->
+ if( square<0 || square>=9 || board[square]!=' ' || _finished(board) ) return false;
+ board[square] = 'X';
+ if( !_finished(board) ) {
+ int reply = _cpu(board);
+ if( reply>=0 ) board[reply]='O';
+ }
+ return true;
+};
+
+val _finished = { u8[~] board ->
+ if( _winner(board) ) return true;
+ for( int i=0; i<9; i++ ) if( board[i]==' ' ) return false;
+ return true;
+};
+
+val _winningLine = { u8[~] board ->
+ for( int i=0; i<3; i++ ) {
+ int row = i*3;
+ if( board[row]!=' ' && board[row]==board[row+1] && board[row]==board[row+2] ) return i;
+ if( board[i]!=' ' && board[i]==board[i+3] && board[i]==board[i+6] ) return 3+i;
+ }
+ if( board[4]!=' ' ) {
+ if( board[0]==board[4] && board[4]==board[8] ) return 6;
+ if( board[2]==board[4] && board[4]==board[6] ) return 7;
+ }
+ return -1;
+};
+// Return the winning mark, or zero.
+val _winner = { u8[~] board ->
+ int line = _winningLine(board);
+ return line<0 ? 0 : line<3 ? board[line*3] : line<6 ? board[line-3] : board[4];
+};
+
+// Try each empty square, restoring it before returning.
+val _threat = { u8[] board, u8 mark ->
+ for( int i=0; i<9; i++ )
+ if( board[i]==' ' ) {
+ board[i] = mark;
+ bool wins = _winner(board)==mark;
+ board[i] = ' '; // Restore board
+ if( wins ) return i;
+ }
+ return -1;
+};
+// Win if possible, otherwise block, then prefer center, corners, and edges.
+// Called only after _finished has ruled out a full board.
+val _cpu = { u8[] board ->
+ // If O can win, do so
+ int win = _threat(board,'O');
+ if( win>=0 ) return win;
+ // If X will win next move, block
+ int block = _threat(board,'X');
+ if( block>=0 ) return block;
+ // If center is open, take it
+ if( board[4]==' ' ) return 4;
+ // If a corner is open, take it
+ for( int i=0; i<9; i+=2 ) if( board[i]==' ' ) return i;
+ // Take whatever is available
+ for( int i=0; i<9; i++ ) if( board[i]==' ' ) return i;
+ return -1;
+};
+
+val _draw = { !sys.draw.Canvas screen, u8[~] board, int hover ->
+ u32 background = 0x142e36, paper = 0xf2eddf, ink = 0x35535a;
+ u32 cross = 0xe47b57, ring = 0x248b89, accent = 0xf0c778;
+ int winner = _winner(board);
+ bool finished = _finished(board);
+ screen.clear(background);
+ screen.text(48,34,"TIC TAC TOE",6,paper);
+ screen.fillRect(48,90,504,3,ink);
+ screen.text(48,116,finished ? (winner=='X' ? "YOU WIN!" : winner=='O' ? "CPU WINS" : "DRAW") : "YOUR TURN",4,accent);
+ screen.line(428,112,450,134,5,cross).line(450,112,428,134,5,cross);
+ screen.circle(496,123,13,5,0x70c9bf);
+ screen.text(428,145,"YOU",2,0xa2b9b7);
+ screen.text(484,145,"CPU",2,0xa2b9b7);
+ screen.fillRect(48,168,504,504,paper);
+ if( !finished && hover>=0 && hover<9 && board[hover]==' ' )
+ screen.fillRect(48+(hover-hover/3*3)*168,168+hover/3*168,168,168,0xd9e8de);
+ for( int i=1; i<3; i++ ) {
+ screen.fillRect(48+i*168-3,168,6,504,ink);
+ screen.fillRect(48,168+i*168-3,504,6,ink);
+ }
+ for( int i=0; i<9; i++ ) {
+ int cx = 132+(i-i/3*3)*168, cy = 252+i/3*168;
+ if( board[i]=='X' )
+ screen.line(cx-38,cy-38,cx+38,cy+38,12,cross).line(cx+38,cy-38,cx-38,cy+38,12,cross);
+ else if( board[i]=='O' ) screen.circle(cx,cy,48,11,ring);
+ else screen.text(cx-6,cy-10,i==0 ? "1" : i==1 ? "2" : i==2 ? "3" :
+ i==3 ? "4" : i==4 ? "5" : i==5 ? "6" : i==6 ? "7" : i==7 ? "8" : "9",4,0x95a59e);
+ }
+ int line = _winningLine(board);
+ if( line>=0 ) {
+ int first = line<3 ? line*3 : line<6 ? line-3 : line==6 ? 0 : 2;
+ int last = line<3 ? first+2 : line<6 ? first+6 : line==6 ? 8 : 6;
+ screen.line(132+(first-first/3*3)*168,252+first/3*168,132+(last-last/3*3)*168,252+last/3*168,5,0xeab64e);
+ }
+ screen.text(48,704,"CLICK A SQUARE OR PRESS 1-9",3,paper);
+ screen.text(48,738,"R OR SPACE REPLAY Q OR ESC QUIT",3,0xa2b9b7);
+ screen.rect(48,168,504,504,3,ink);
+};
diff --git a/docs/chapter31/graphics.md b/docs/chapter31/graphics.md
new file mode 100644
index 00000000..2394a401
--- /dev/null
+++ b/docs/chapter31/graphics.md
@@ -0,0 +1,119 @@
+# Optional graphics
+
+Simple owns the pixel array and drawing code. [MiniFB](https://github.com/emoon/minifb)
+provides a window, input, and pixel presentation. The first interface is
+`sys.gfx`: one fixed-size window, operated on the main thread. The
+[Graphics example](examples/Graphics.smp) draws a color gradient in Simple.
+The [drawing library and graphical tic-tac-toe](drawing.md) build on this pixel
+interface. `make run-graphical-tictactoe CTAGS=` builds the optional backend and
+opens the game; `make graphical-tictactoe CTAGS=` only builds its executable.
+The game run target also builds optional [background audio](audio.md).
+
+## Default: no graphics dependency
+
+From `chapter31`, the usual `make release`, `make tests`, and `make tictactoe`
+need no MiniFB, graphics headers, OpenGL, CMake, or running display. Nothing
+downloads dependencies unless you explicitly request `make libs` or `make init`
+(`make lib` remains a setup alias). Build and run targets never use the network.
+
+`make tests_nographics CTAGS=` runs the entire ordinary suite with
+`SIMPLE_MINIFB=off`. This includes the missing-backend tests, which never open
+windows. It excludes the interactive `run-graphics` target.
+
+The runtime contains a small loader using the OS dynamic-library facilities.
+It does nothing until `sys.gfx.open`. Without a configured backend, `open`
+prints a diagnostic to stderr and returns `sys.gfx.ERROR` (-1). Programs can
+return a nonzero status or fall back to text; the library does not terminate
+the process. Non-graphics programs ignore even a broken graphics configuration.
+
+## Build the optional backend
+
+From `chapter31`:
+
+```sh
+make libs CTAGS= # Once while online; fills the persistent lib/ cache.
+make graphics graphics-example CTAGS=
+make run-graphics CTAGS=
+```
+
+`libs` downloads the revision pinned in [graphics.mk](../graphics.mk) into
+ignored `lib/minifb-<revision>.tar.gz` and extracts it under `lib/`.
+`graphics` builds only its C framebuffer backend from these local sources. Windows
+uses the MinGW-w64 GCC compiler (`x86_64-w64-mingw32-gcc`) and GDI; the rest of
+Simple still uses the existing Cygwin GCC. Linux uses GCC and X11 (`libx11-dev`
+on Ubuntu), including XWayland on a Wayland desktop. OpenGL, the C++ callback
+wrappers, and upstream example programs are omitted. Native Wayland and macOS
+builds are not provided here yet.
+
+Outputs:
+
+| Platform | Backend | Example |
+| --- | --- | --- |
+| Windows | `build/release/x86_64_v2_win64/simple_minifb.dll` | `build/examples/x86_64_v2_win64/Graphics.exe` |
+| Linux | `build/release/x86_64_v2_SystemV/libsimple_minifb.so` | `build/examples/x86_64_v2_SystemV/Graphics` |
+
+`MiniFB-LICENSE` is copied alongside the backend; include it when distributing
+the library. MiniFB sources remain outside the tracked Simple sources.
+The cache survives `make clean`. Editing a makefile or rebuilding a backend
+does not fetch or check GitHub. Missing dependencies fail promptly with
+instructions to run `make libs` while online.
+
+`graphics-example` alone builds the executable without requiring MiniFB. It
+also demonstrates the diagnostic and exit status 1 when graphics is absent.
+`run-graphics` builds both, sets the backend path, and opens a window. Close it
+or release Escape to exit. Passing any argument to the executable makes it
+close after five polling iterations, for a quick smoke test.
+
+To run directly from PowerShell:
+
+```powershell
+$env:SIMPLE_MINIFB = (Resolve-Path build/release/x86_64_v2_win64/simple_minifb.dll).Path
+.\build\examples\x86_64_v2_win64\Graphics.exe
+```
+
+From Cygwin bash:
+
+```sh
+export SIMPLE_MINIFB="$PWD/build/release/x86_64_v2_win64/simple_minifb.dll"
+./build/examples/x86_64_v2_win64/Graphics.exe
+```
+
+For WSL, from the repository root:
+
+```sh
+wsl -d Ubuntu-24.04 -- bash -lc 'cd chapter31 && make graphics graphics-example CLZDIR=build/classes-linux CTAGS='
+```
+
+Inside WSL, set `SIMPLE_MINIFB` to the full path of the Linux `.so`, then run
+the Linux example with a working X11 display. For an automated virtual display:
+
+```sh
+SIMPLE_MINIFB="$PWD/build/release/x86_64_v2_SystemV/libsimple_minifb.so" \
+ xvfb-run -a build/examples/x86_64_v2_SystemV/Graphics smoke
+```
+
+The backend path is explicit: a DLL installed elsewhere or a different working
+directory cannot silently select the wrong version. `SIMPLE_MINIFB=off` disables
+graphics; an absent, unloadable, or incompatible backend produces a diagnostic.
+A backend that loads but cannot create a window also reports an error cleanly.
+
+## Initial interface
+
+| Operation | Meaning |
+| --- | --- |
+| `open(title,width,height)` | Open one window; returns `OK` or `ERROR`. |
+| `present(pixels)` | Present a `u32[~]` of exactly `width*height` pixels, packed `0x00RRGGBB`. |
+| `poll(wait)` | Process input; `true` waits about 17ms first to avoid a busy loop. |
+| `mouseX()`, `mouseY()` | Current mouse position, origin at upper left. |
+| `button(LEFT)`, `key(code)` | Current pressed state; printable keys use uppercase ASCII. |
+| `close()` | Release native window and pixel storage; safe to repeat. |
+
+`present` and `poll` return `OK` (0), `CLOSED` (1, normal close/Escape), or
+`ERROR` (-1). Stop polling after either terminal result and call `close`.
+The initial input API samples held state; it is not a queue of every transition.
+
+MiniFB retains its last framebuffer for repaint events. The adapter copies the
+Simple array into native-owned storage before presenting; it never retains a
+Simple heap pointer. Framebuffer allocation, the copy, and window/input calls
+are the only native work here. Drawing primitives and the games can be built
+in Simple on top of this boundary.
diff --git a/graphics.mk b/graphics.mk
new file mode 100644
index 00000000..9ede649d
--- /dev/null
+++ b/graphics.mk
@@ -0,0 +1,62 @@
+# Optional MiniFB build, pinned independently of Simple's default build.
+# A C-only GDI/X11 subset: no OpenGL, C++ wrapper, examples, or CMake required.
+MINIFB_REV := f92be8e966f194004853c10196e704f4510f58d4
+MINIFB_SRC := lib/minifb-$(MINIFB_REV)
+MINIFB_ARCHIVE := lib/minifb-$(MINIFB_REV).tar.gz
+MINIFB_STAMP := $(MINIFB_SRC)/.simple-$(MINIFB_REV)
+MINIFB_COMMON := MiniFB_common MiniFB_cpu_x86 MiniFB_internal MiniFB_stretch \
+ MiniFB_stretch_sse2 MiniFB_stretch_avx2 MiniFB_stretch_neon MiniFB_timer MiniFB_utf8
+ifeq ($(OS),Windows_NT)
+ GRAPHICS_CC := x86_64-w64-mingw32-gcc
+ GRAPHICS_LIB := $(RELEASE)/$(CPU_ABI)/simple_minifb.dll
+ MINIFB_PLATFORM := windows/WinMiniFB
+ MINIFB_LIBS := -lgdi32 -luser32 -lwinmm
+ MINIFB_FLAGS := -D_WIN32_WINNT=0x0601
+else
+ GRAPHICS_CC := gcc
+ GRAPHICS_LIB := $(RELEASE)/$(CPU_ABI)/libsimple_minifb.so
+ MINIFB_PLATFORM := x11/X11MiniFB x11/X11MiniFB_scale_funcs x11/X11MiniFB_text \
+ MiniFB_linux MiniFB_evdev_keys
+ MINIFB_LIBS := -lX11 -lm
+ MINIFB_FLAGS := -fPIC -D_POSIX_C_SOURCE=200809L
+endif
+MINIFB_SOURCES = $(addprefix $(MINIFB_SRC)/src/,$(addsuffix .c,$(MINIFB_COMMON) $(MINIFB_PLATFORM)))
+GRAPHICS_EXAMPLE := $(GAME_DIR)/Graphics$(if $(filter Windows_NT,$(OS)),.exe,)
+GRAPHICAL_TICTACTOE := $(GAME_DIR)/TicTacToeGraphical$(if $(filter Windows_NT,$(OS)),.exe,)
+
+.PHONY: graphics graphics-example run-graphics
+.PHONY: graphical-tictactoe run-graphical-tictactoe
+graphical-tictactoe: $(GRAPHICAL_TICTACTOE)
+$(GRAPHICAL_TICTACTOE): docs/examples/TicTacToeGraphical.smp $(CLZDIR)/test/.ttag $(RELEASE_SYS) $(RELEASE_CRT) graphics.mk
+ @$(JVM) com.seaofnodes.simple.Simple --link --cpu $(CPU_PORT) --abi $(CALL_CONV) -L $(dir $(RELEASE_SYS)) -o $(GAME_DIR)/TicTacToeGraphical.o $<
+
+run-graphical-tictactoe: graphics audio graphical-tictactoe
+ SIMPLE_MINIFB="$(abspath $(GRAPHICS_LIB))" SIMPLE_MINIAUDIO="$(abspath $(AUDIO_LIB))" $(GRAPHICAL_TICTACTOE)
+graphics: $(GRAPHICS_LIB)
+
+lib libs init: $(MINIFB_STAMP)
+
+$(MINIFB_ARCHIVE):
+ @$(call download-library,https://codeload.github.com/emoon/minifb/tar.gz/$(MINIFB_REV))
+
+$(MINIFB_STAMP): $(MINIFB_ARCHIVE)
+ @mkdir -p $(MINIFB_SRC)
+ @tar -xzf $< --strip-components=1 -C $(MINIFB_SRC)
+ @touch $@
+
+$(GRAPHICS_LIB): $(MINIFB_STAMP) src/main/c/simple_minifb.c src/main/c/simple_graphics.h graphics.mk
+ @echo "building " $@
+ @mkdir -p $(dir $@)
+ @$(GRAPHICS_CC) -shared -O2 -Wall -Wextra -Wno-unused-parameter -Wno-implicit-fallthrough $(MINIFB_FLAGS) \
+ -I$(MINIFB_SRC)/include -I$(MINIFB_SRC)/src \
+ src/main/c/simple_minifb.c $(MINIFB_SOURCES) $(MINIFB_LIBS) -o $@
+ @cp $(MINIFB_SRC)/LICENSE $(dir $@)MiniFB-LICENSE
+
+# This executable also builds without MiniFB, to exercise the missing-backend path.
+graphics-example: $(GRAPHICS_EXAMPLE)
+$(GRAPHICS_EXAMPLE): docs/examples/Graphics.smp $(CLZDIR)/test/.ttag $(RELEASE_SYS) $(RELEASE_CRT) graphics.mk
+ @$(JVM) com.seaofnodes.simple.Simple --link --cpu $(CPU_PORT) --abi $(CALL_CONV) -L $(dir $(RELEASE_SYS)) -o $(GAME_DIR)/Graphics.o $<
+
+# An interactive target, never part of default/tests.
+run-graphics: graphics graphics-example
+ SIMPLE_MINIFB="$(abspath $(GRAPHICS_LIB))" $(GRAPHICS_EXAMPLE)
diff --git a/src/main/c/simple_audio.c b/src/main/c/simple_audio.c
new file mode 100644
index 00000000..b8ed62de
--- /dev/null
+++ b/src/main/c/simple_audio.c
@@ -0,0 +1,40 @@
+#include "simple_audio.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <dlfcn.h>
+
+static const struct simple_audio_api *api;
+static void *library;
+
+int32_t simple_audio_open(void) {
+ const char *path = getenv("SIMPLE_MINIAUDIO");
+ if (path && strcmp(path,"off")==0) return -1;
+ if (!api) {
+ if (!path || !*path) return -1; // Optional: games may run silently.
+ library = dlopen(path,RTLD_NOW | RTLD_LOCAL);
+ if (!library) {
+ fprintf(stderr,"Simple audio: cannot load backend '%s': %s\n",path,dlerror());
+ return -1;
+ }
+ const struct simple_audio_api *(*get_api)(void) =
+ (const struct simple_audio_api *(*)(void))dlsym(library,"simple_miniaudio_api");
+ const struct simple_audio_api *candidate = get_api ? get_api() : NULL;
+ if (!candidate || candidate->version!=SIMPLE_AUDIO_VERSION || !candidate->open ||
+ !candidate->load || !candidate->play || !candidate->stop ||
+ !candidate->playing || !candidate->close) {
+ fprintf(stderr,"Simple audio: incompatible backend; rebuild with make audio.\n");
+ dlclose(library);
+ library=NULL;
+ return -1;
+ }
+ api=candidate;
+ atexit(api->close);
+ }
+ return api->open();
+}
+int32_t simple_audio_load(const int16_t *samples, uint32_t count) { return api ? api->load(samples,count) : -1; }
+int32_t simple_audio_play(int32_t sound) { return api ? api->play(sound) : -1; }
+int32_t simple_audio_stop(void) { return api ? api->stop() : 0; }
+int32_t simple_audio_playing(void) { return api ? api->playing() : 0; }
+int32_t simple_audio_close(void) { if (api) api->close(); return 0; }
diff --git a/src/main/c/simple_audio.h b/src/main/c/simple_audio.h
new file mode 100644
index 00000000..77cce530
--- /dev/null
+++ b/src/main/c/simple_audio.h
@@ -0,0 +1,16 @@
+#ifndef SIMPLE_AUDIO_H
+#define SIMPLE_AUDIO_H
+#include <stdint.h>
+
+// Main-thread API. Samples are copied; no Simple heap pointer is retained.
+struct simple_audio_api {
+ int32_t version;
+ int32_t (*open)(void);
+ int32_t (*load)(const int16_t *, uint32_t);
+ int32_t (*play)(int32_t);
+ int32_t (*stop)(void);
+ int32_t (*playing)(void);
+ void (*close)(void);
+};
+#define SIMPLE_AUDIO_VERSION 1
+#endif
diff --git a/src/main/c/simple_graphics.c b/src/main/c/simple_graphics.c
new file mode 100644
index 00000000..41073470
--- /dev/null
+++ b/src/main/c/simple_graphics.c
@@ -0,0 +1,60 @@
+#include "simple_graphics.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <dlfcn.h>
+
+// No MiniFB headers or link dependency in the default runtime. Cygwin's
+// dlopen also loads native Windows DLLs; both use the x64 Windows C ABI.
+static const struct simple_graphics_api *api;
+static void *library;
+
+static int load_graphics(void) {
+ // Explicitly disabling graphics also makes headless tests deterministic.
+ const char *path = getenv("SIMPLE_MINIFB");
+ if (path && strcmp(path, "off") == 0) {
+ fprintf(stderr, "Simple graphics: disabled (SIMPLE_MINIFB=off).\n");
+ return -1;
+ }
+ if (api) return 0;
+ if (!path || !*path) {
+ fprintf(stderr, "Simple graphics: MiniFB backend not configured. "
+ "Run make graphics, then set SIMPLE_MINIFB to the backend library's full path.\n");
+ return -1;
+ }
+ library = dlopen(path, RTLD_NOW | RTLD_LOCAL);
+ if (!library) {
+ fprintf(stderr, "Simple graphics: cannot load MiniFB backend '%s': %s\n", path, dlerror());
+ return -1;
+ }
+ const struct simple_graphics_api *(*get_api)(void) =
+ (const struct simple_graphics_api *(*)(void))dlsym(library, "simple_minifb_api");
+ const struct simple_graphics_api *candidate = get_api ? get_api() : NULL;
+ if (!candidate || candidate->version != SIMPLE_GRAPHICS_VERSION ||
+ !candidate->open || !candidate->present || !candidate->poll ||
+ !candidate->mouse_x || !candidate->mouse_y || !candidate->button ||
+ !candidate->key || !candidate->close) {
+ fprintf(stderr, "Simple graphics: incompatible MiniFB backend '%s'. Rebuild with make graphics.\n", path);
+ dlclose(library);
+ library = NULL;
+ return -1;
+ }
+ api = candidate;
+ // Keep the library loaded until process exit; its window callbacks may
+ // still be registered. Closing a window releases its native resources.
+ atexit(api->close);
+ return 0;
+}
+
+int32_t simple_gfx_open(const char *title, uint32_t width, uint32_t height) {
+ return load_graphics() ? -1 : api->open(title, width, height);
+}
+int32_t simple_gfx_present(const uint32_t *pixels, uint32_t count) {
+ return api ? api->present(pixels, count) : -1;
+}
+int32_t simple_gfx_poll(int32_t wait) { return api ? api->poll(wait) : -1; }
+int32_t simple_gfx_mouse_x(void) { return api ? api->mouse_x() : 0; }
+int32_t simple_gfx_mouse_y(void) { return api ? api->mouse_y() : 0; }
+int32_t simple_gfx_button(int32_t button) { return api ? api->button(button) : 0; }
+int32_t simple_gfx_key(int32_t key) { return api ? api->key(key) : 0; }
+int32_t simple_gfx_close(void) { if (api) api->close(); return 0; }
diff --git a/src/main/c/simple_graphics.h b/src/main/c/simple_graphics.h
new file mode 100644
index 00000000..35a0aa08
--- /dev/null
+++ b/src/main/c/simple_graphics.h
@@ -0,0 +1,19 @@
+#ifndef SIMPLE_GRAPHICS_H
+#define SIMPLE_GRAPHICS_H
+#include <stdint.h>
+
+// Private, versioned boundary between the tiny runtime loader and MiniFB.
+// All calls run on the main thread. Pixel memory is borrowed for present only.
+struct simple_graphics_api {
+ int32_t version;
+ int32_t (*open)(const char *, uint32_t, uint32_t);
+ int32_t (*present)(const uint32_t *, uint32_t);
+ int32_t (*poll)(int32_t);
+ int32_t (*mouse_x)(void);
+ int32_t (*mouse_y)(void);
+ int32_t (*button)(int32_t);
+ int32_t (*key)(int32_t);
+ void (*close)(void);
+};
+#define SIMPLE_GRAPHICS_VERSION 1
+#endif
diff --git a/src/main/c/simple_miniaudio.c b/src/main/c/simple_miniaudio.c
new file mode 100644
index 00000000..03dbadd7
--- /dev/null
+++ b/src/main/c/simple_miniaudio.c
@@ -0,0 +1,123 @@
+#include "simple_audio.h"
+#define MA_NO_DECODING
+#define MA_NO_ENCODING
+#define MA_NO_RESOURCE_MANAGER
+#define MA_NO_NODE_GRAPH
+#define MA_NO_ENGINE
+#define MINIAUDIO_IMPLEMENTATION
+#include <miniaudio.h>
+#include <math.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+#define RATE 48000
+#define SLOTS 32
+static ma_device device;
+static ma_mutex mutex;
+static int opened, active;
+static uint32_t cursor;
+static struct { int16_t *pcm; uint32_t count; } sounds[SLOTS];
+
+// Only native-owned immutable PCM is read on the audio thread. The lock covers
+// a bounded copy, never allocation, synthesis, device calls, or Simple code.
+static void output(ma_device *unused, void *out, const void *in, ma_uint32 frames) {
+ (void)unused; (void)in;
+ memset(out,0,frames*sizeof(int16_t));
+ ma_mutex_lock(&mutex);
+ if (active) {
+ uint32_t count=sounds[active].count-cursor;
+ if (count>frames) count=frames;
+ memcpy(out,sounds[active].pcm+cursor,count*sizeof(int16_t));
+ cursor+=count;
+ if (cursor==sounds[active].count) active=0;
+ }
+ ma_mutex_unlock(&mutex);
+}
+static int32_t load(const int16_t *pcm, uint32_t count) {
+ if (!opened || !pcm || !count || count>RATE*10) return -1;
+ int slot;
+ for (slot=4; slot<SLOTS && sounds[slot].pcm; ++slot) {}
+ if (slot==SLOTS) return -1;
+ int16_t *copy=malloc(count*sizeof(*copy));
+ if (!copy) return -1;
+ memcpy(copy,pcm,count*sizeof(*copy));
+ ma_mutex_lock(&mutex);
+ sounds[slot].pcm=copy; sounds[slot].count=count;
+ ma_mutex_unlock(&mutex);
+ return slot;
+}
+static int32_t play(int32_t sound) {
+ if (!opened || sound<=0 || sound>=SLOTS || !sounds[sound].pcm) return -1;
+ ma_mutex_lock(&mutex);
+ active=sound; cursor=0;
+ ma_mutex_unlock(&mutex);
+ return 0;
+}
+static int32_t stop(void) {
+ if (opened) { ma_mutex_lock(&mutex); active=0; ma_mutex_unlock(&mutex); }
+ return 0;
+}
+static int32_t playing(void) {
+ if (!opened) return 0;
+ ma_mutex_lock(&mutex);
+ int result=active!=0;
+ ma_mutex_unlock(&mutex);
+ return result;
+}
+static void close_audio(void) {
+ if (!opened) return;
+ ma_device_uninit(&device); // Joins callback before freeing buffers/mutex.
+ ma_mutex_uninit(&mutex);
+ for (int i=1;i<SLOTS;++i) {
+ free(sounds[i].pcm); sounds[i].pcm=NULL; sounds[i].count=0;
+ }
+ opened=0; active=0; cursor=0;
+}
+static int tone(int slot, int descending) {
+ uint32_t note=slot==1 ? RATE*7/100 : RATE*16/100;
+ int notes=slot==1 ? 1 : 3;
+ sounds[slot].count=note*notes;
+ sounds[slot].pcm=calloc(note*notes,sizeof(int16_t));
+ if (!sounds[slot].pcm) return -1;
+ const double pitches[3]={523.251,659.255,783.991};
+ for (int n=0;n<notes;++n) {
+ double hz=slot==1 ? 440.0 : pitches[descending ? 2-n : n];
+ for (uint32_t i=0;i<note;++i) {
+ double t=(double)i/note;
+ double envelope=sin(3.141592653589793*t);
+ sounds[slot].pcm[n*note+i]=(int16_t)(3200*envelope*envelope*sin(6.283185307179586*hz*i/RATE));
+ }
+ }
+ return 0;
+}
+static int32_t open_audio(void) {
+ if (opened) return 0;
+ if (ma_mutex_init(&mutex)!=MA_SUCCESS) return -1;
+ ma_device_config config=ma_device_config_init(ma_device_type_playback);
+ config.playback.format=ma_format_s16;
+ config.playback.channels=1;
+ config.sampleRate=RATE;
+ config.dataCallback=output;
+#ifdef SIMPLE_AUDIO_TEST_NULL
+ ma_backend backend=ma_backend_null;
+ ma_result result=ma_device_init_ex(&backend,1,NULL,&config,&device);
+#else
+ ma_result result=ma_device_init(NULL,&config,&device);
+#endif
+ if (result!=MA_SUCCESS) {
+ ma_mutex_uninit(&mutex);
+ fprintf(stderr,"Simple audio: no playback device (%d); continuing silently.\n",result);
+ return -1;
+ }
+ opened=1;
+ if (tone(1,0) || tone(2,0) || tone(3,1) || ma_device_start(&device)!=MA_SUCCESS) {
+ close_audio(); return -1;
+ }
+ return 0;
+}
+static const struct simple_audio_api api={SIMPLE_AUDIO_VERSION,open_audio,load,play,stop,playing,close_audio};
+#ifdef _WIN32
+__declspec(dllexport)
+#endif
+const struct simple_audio_api *simple_miniaudio_api(void) { return &api; }
diff --git a/src/main/c/simple_minifb.c b/src/main/c/simple_minifb.c
new file mode 100644
index 00000000..34663e2e
--- /dev/null
+++ b/src/main/c/simple_minifb.c
@@ -0,0 +1,113 @@
+#include "simple_graphics.h"
+#include <MiniFB.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#ifdef _WIN32
+#include <windows.h>
+#else
+#include <errno.h>
+#include <time.h>
+#endif
+
+// One fixed-size window for now. No Simple pointer survives a native call:
+// MiniFB retains its last draw buffer for OS repaint events, so copy it here.
+static struct mfb_window *window;
+static uint32_t *frame;
+static uint32_t width, height;
+
+// The C-only build never installs the upstream C++ callback wrappers.
+void release_cpp_stub(struct mfb_window *unused) { (void)unused; }
+
+static void close_window(void) {
+ if (window) {
+ mfb_close(window); // Requests close.
+ mfb_update_events(window); // Actually destroys the window.
+ window = NULL;
+ }
+ free(frame);
+ frame = NULL;
+}
+
+static int32_t open_window(const char *title, uint32_t w, uint32_t h) {
+ if (window || !w || !h || w > UINT32_MAX / 4 ||
+ (uint64_t)w * h > UINT32_MAX || (uint64_t)w * h > SIZE_MAX / 4) {
+ fprintf(stderr, "Simple graphics: invalid dimensions or a window is already open.\n");
+ return -1;
+ }
+ frame = calloc((size_t)w * h, sizeof(*frame));
+ if (!frame) {
+ fprintf(stderr, "Simple graphics: cannot allocate the display buffer.\n");
+ return -1;
+ }
+ window = mfb_open(title, w, h);
+ if (!window) {
+ fprintf(stderr, "Simple graphics: MiniFB could not open a window (is a graphical display available?).\n");
+ close_window();
+ return -1;
+ }
+ width = w;
+ height = h;
+ return 0;
+}
+
+static int32_t status(mfb_update_state state) {
+ if (state == MFB_STATE_OK) return 0;
+ if (state == MFB_STATE_EXIT) {
+ window = NULL; // MiniFB has already destroyed it.
+ close_window();
+ return 1;
+ }
+ fprintf(stderr, "Simple graphics: MiniFB update failed (%d).\n", state);
+ close_window();
+ return -1;
+}
+
+static int32_t present(const uint32_t *pixels, uint32_t count) {
+ if (!window) return 1;
+ if (!pixels || count != (uint64_t)width * height) {
+ fprintf(stderr, "Simple graphics: pixel array length must equal width * height.\n");
+ return -1;
+ }
+ memcpy(frame, pixels, (size_t)count * sizeof(*frame));
+ return status(mfb_update_ex(window, frame, width, height));
+}
+
+static int32_t poll_events(int32_t wait) {
+ if (!window) return 1;
+ if (wait) {
+ // mfb_wait_sync returns false for both destruction and errors that
+ // leave the window alive. Use a short OS wait, then the status-bearing
+ // event API, so we know whether the window still needs destruction.
+#ifdef _WIN32
+ Sleep(17);
+#else
+ struct timespec delay = {0, 16666667};
+ while (nanosleep(&delay, &delay) < 0) {
+ if (errno != EINTR) {
+ perror("Simple graphics: event wait");
+ close_window();
+ return -1;
+ }
+ }
+#endif
+ }
+ return status(mfb_update_events(window));
+}
+static int32_t mouse_x(void) { return window ? mfb_get_mouse_x(window) : 0; }
+static int32_t mouse_y(void) { return window ? mfb_get_mouse_y(window) : 0; }
+static int32_t button(int32_t b) {
+ return window && b >= 0 && b < 8 ? mfb_get_mouse_button_buffer(window)[b] != 0 : 0;
+}
+static int32_t key(int32_t k) {
+ return window && k >= 0 && k <= MFB_KB_KEY_LAST ? mfb_get_key_buffer(window)[k] != 0 : 0;
+}
+
+static const struct simple_graphics_api api = {
+ SIMPLE_GRAPHICS_VERSION, open_window, present, poll_events,
+ mouse_x, mouse_y, button, key, close_window
+};
+#ifdef _WIN32
+__declspec(dllexport)
+#endif
+const struct simple_graphics_api *simple_minifb_api(void) { return &api; }
diff --git a/src/main/java/com/seaofnodes/simple/README.md b/src/main/java/com/seaofnodes/simple/README.md
index af86f21e..a8916aa8 100644
--- a/src/main/java/com/seaofnodes/simple/README.md
+++ b/src/main/java/com/seaofnodes/simple/README.md
@@ -2,7 +2,7 @@
| Directory | Contents |
| --- | --- |
-| `parser/` | Recursive-descent grammar routines, lexing, name resolution, and construction of the initial IR. `Var` describes a lexical binding. See the [grammar map](../../../../../../docs/chapter30/30-grammar.md). |
+| `parser/` | Recursive-descent grammar routines, lexing, name resolution, and construction of the initial IR. `Var` describes a lexical binding. See the [grammar map](../../../../../../docs/chapter31/31-grammar.md). |
| `node/` | Sea-of-Nodes IR nodes: graph edges, type computation, and local peephole rewrites. |
| `node/cpus/` | Machine-specific nodes and instruction selection support for `arm`, `riscv`, and `x86_64_v2`. |
| `codegen/` | Compilation driver and passes: module loading, optimization, scheduling, register allocation, serialization, and machine-code emission. `IterPeeps` runs the peephole worklist. |
diff --git a/src/main/java/com/seaofnodes/simple/codegen/Serialize.java b/src/main/java/com/seaofnodes/simple/codegen/Serialize.java
index ae4ecb74..79bebe1f 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/Serialize.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/Serialize.java
@@ -417,7 +417,7 @@ public class Serialize {
assert elf._clz._name==strs[1/*skip null ptr*/];
for( Type type : types )
if( type instanceof TypeStruct ts )
- Parser.TYPES.put(ts._name,ts);
+ installType(ts);
for( Type type : types ) {
if( !(type instanceof TypeStruct ts) || !TypeStruct.startsClzPrefix(ts._name) )
continue;
@@ -429,7 +429,7 @@ public class Serialize {
elf._pubTypes = new TypeStruct[npubTypes];
for( int i=0; i<npubTypes; i++ ) {
TypeStruct ts = elf._pubTypes[i] = (TypeStruct)types[pubTypeIds[i]];
- Parser.TYPES.put(ts._name,ts);
+ installType(ts);
}
elf._functions = new FunctionSummary[bais.packed4()];
for( int i = 0; i<elf._functions.length; i++ ) {
@@ -444,6 +444,14 @@ public class Serialize {
return elf._types = new TypeRead(types,strs,fileAliases,fileFidxs,fileRpcs);
}
+ private static void installType(TypeStruct ts) {
+ // TYPES names the mutable array declaration. Read-only arrays in
+ // imported signatures keep their own types, but must not replace that
+ // declaration: closeOver looks up its mutable variant by name.
+ if( ts.isAry() ) ts = ts.replace(ts.field("[]").makeFrom(false));
+ Parser.TYPES.put(ts._name,ts);
+ }
+
static void readAll( CodeGen code, ElfReader elf, GlobalBits aliases, GlobalBits fidxs, GlobalBits rpcs ) {
BAOS bais = elf._bais;
TypeRead tread = readTypes(code, elf, aliases, fidxs, rpcs);
diff --git a/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/src/main/java/com/seaofnodes/simple/node/LoadNode.java
index eb78ed1f..35db4c2f 100644
--- a/src/main/java/com/seaofnodes/simple/node/LoadNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/LoadNode.java
@@ -364,15 +364,13 @@ public class LoadNode extends MemOpNode {
if( ptr instanceof PhiNode pphi && pphi.region() == r )
return true;
- // No, so can we lift this ptr?
- CFGNode cptr = ptr.cfg0();
- if( cptr != null )
- // Pointer is controlled high
- // TODO: Really needs to be the LCA of all inputs is high
- return cptr.idepth() <= r.idepth();
-
- // Dunno without a longer walk
- return false;
+ // Each copied Load needs its pointer before the Region, including
+ // a loop's entry arm. input 0 alone misses floating Loads whose own
+ // pointer was produced inside the loop.
+ CFGNode pinned = ptr.cfg0();
+ if( pinned==null || pinned.idepth()>r.idepth() ) return false;
+ CFGNode cptr = ptr instanceof PhiNode phi ? phi.region() : CFGNode.earlyCFG(ptr);
+ return cptr==null || cptr.dominates(r.idom(memphi),memphi);
}
@Override public Parser.ParseException err() {
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java
index 3d71b4ea..673339a9 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java
@@ -370,8 +370,9 @@ public class x86_64_v2 extends Machine {
private Node _lea(Node add, Node base, Node idx, int off) {
int scale = 0;
- if( base instanceof ShlNode && !(idx instanceof ShlNode) )
- throw Utils.TODO(); // Bug in canonicalization, should on RHS
+ if( base instanceof ShlNode && !(idx instanceof ShlNode) ) {
+ Node tmp = base; base = idx; idx = tmp;
+ }
if( idx instanceof ShlNode shift && shift.in(2) instanceof ConstantNode shfcon &&
shfcon._con instanceof TypeInteger tscale && 0 <= tscale.value() && tscale.value() <= 3 ) {
idx = shift.in(1);
diff --git a/src/main/java/com/seaofnodes/simple/parser/Expressions.java b/src/main/java/com/seaofnodes/simple/parser/Expressions.java
index 247d974e..6486aba8 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Expressions.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Expressions.java
@@ -400,7 +400,7 @@ final class Expressions {
* Parse integer literal
*
* <pre>
- * integerLiteral: [1-9][0-9]* | [0]
+ * integerLiteral: [1-9][0-9]* | 0[0-7]* | 0[xX][0-9a-fA-F]+
* floatLiteral: [digits].[digits]?[e [digits]]?[f]?
* </pre>
*/
diff --git a/src/main/java/com/seaofnodes/simple/parser/Lexer.java b/src/main/java/com/seaofnodes/simple/parser/Lexer.java
index 5e4b7311..9ab678ca 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Lexer.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Lexer.java
@@ -239,6 +239,7 @@ public class Lexer {
// Return a constant Type, either TypeInteger or TypeFloat
Type parseNumber() {
Lexer start=new Lexer(this);
+ if( isHex() ) return parseInteger(parseNumberString(),16,2,start);
int old = _position;
int len = isLongOrDouble();
boolean f32 = peek()=='f';
@@ -250,7 +251,7 @@ public class Lexer {
return TypeFloat.constant(Float.parseFloat(literal));
if( len > 0 ) {
if( len > 1 && _input[old]=='0' )
- throw Parser.error("Syntax error: integer values cannot start with '0'",start);
+ return parseInteger(literal,8,0,start);
long i = Long.parseLong(literal);
return TypeInteger.constant(i);
}
@@ -262,6 +263,11 @@ public class Lexer {
}
private String parseNumberString() {
int old = _position;
+ if( isHex() ) {
+ _position += 2;
+ while( isIdLetter(peek()) ) inc();
+ return new String(_input,old,_position-old);
+ }
int len = Math.abs(isLongOrDouble());
if( peek()=='f' ) {
nextChar();
@@ -270,6 +276,24 @@ public class Lexer {
return new String(_input,old,len);
}
+ private boolean isHex() {
+ return peek()=='0' && _position+1<_input.length && (peek(1)=='x' || peek(1)=='X');
+ }
+
+ private TypeInteger parseInteger(String literal, int radix, int prefix, Lexer start) {
+ String kind = radix==16 ? "Hexadecimal" : "Octal";
+ if( literal.length()==prefix )
+ throw Parser.error(kind+" integer literal requires at least one digit",start);
+ for( int i=prefix; i<literal.length(); i++ )
+ if( Character.digit(literal.charAt(i),radix)<0 )
+ throw Parser.error("Invalid "+kind.toLowerCase()+" integer literal '"+literal+"'",start);
+ try {
+ return TypeInteger.constant(Long.parseUnsignedLong(literal.substring(prefix),radix));
+ } catch( NumberFormatException ex ) {
+ throw Parser.error(kind+" integer literal exceeds 64 bits",start);
+ }
+ }
+
// Return +len that ends a long
// Return -len that ends a double
private int isLongOrDouble() {
diff --git a/src/main/java/com/seaofnodes/simple/parser/README.md b/src/main/java/com/seaofnodes/simple/parser/README.md
index cee1d64a..d0220761 100644
--- a/src/main/java/com/seaofnodes/simple/parser/README.md
+++ b/src/main/java/com/seaofnodes/simple/parser/README.md
@@ -2,7 +2,7 @@
The parser builds Sea-of-Nodes IR directly. Each component shares the cursor,
active scope, and compilation state through its `Parser P` reference; there is no intermediate
-AST. The [grammar map](../../../../../../../docs/chapter30/30-grammar.md) connects language
+AST. The [grammar map](../../../../../../../docs/chapter31/31-grammar.md) connects language
productions to their entry points.
| File | Responsibility |
diff --git a/src/main/java/com/seaofnodes/simple/parser/package-info.java b/src/main/java/com/seaofnodes/simple/parser/package-info.java
index c886f72a..73ebec2e 100644
--- a/src/main/java/com/seaofnodes/simple/parser/package-info.java
+++ b/src/main/java/com/seaofnodes/simple/parser/package-info.java
@@ -7,7 +7,7 @@
* use-side name/field resolution; Allocations, ControlFlow, and Conversions
* contain their shared IR construction. Lexer also supplies location snapshots.
*
- * <p>See {@code chapter30/docs/30-grammar.md} for the production-to-method map.
+ * <p>See {@code chapter31/docs/31-grammar.md} for the production-to-method map.
* Evaluation order and scope lifetimes are explicit: loop updates and declared
* constructor bodies are the two deferred source fragments. There is no AST.
*/
diff --git a/src/main/smp/sys.smp b/src/main/smp/sys.smp
index 1201315b..40a4b955 100644
--- a/src/main/smp/sys.smp
+++ b/src/main/smp/sys.smp
@@ -2,11 +2,14 @@
// top-level default standard library import
val adt = adt; // Abstract Data Types
+val audio = audio; // Optional background sound (miniaudio)
val ary = ary; // Array utilities
val aryi64 = aryi64; // Variable sized array of i64
val aryu8 = aryu8; // Variable sized array of u8
val char = char; // Character utilities
val io = io; // IO library
+val gfx = gfx; // Optional pixel window (MiniFB)
+val draw = draw; // Software raster drawing in Simple
val text = text; // Typed text conversions
val libc = libc; // libc FFI bindings
val rt = rt; // Run Time support
diff --git a/src/main/smp/sys/audio.smp b/src/main/smp/sys/audio.smp
new file mode 100644
index 00000000..89e6dfa9
--- /dev/null
+++ b/src/main/smp/sys/audio.smp
@@ -0,0 +1,29 @@
+// Optional background audio. All API calls belong on the game/main thread.
+int OK = 0;
+int ERROR = -1;
+int BOOP = 1;
+int WIN = 2;
+int LOSE = 3;
+int RATE = 48000;
+{ i32 } simple_audio_open = "C";
+{ i64 u32 -> i32 } simple_audio_load = "C";
+{ i32 -> i32 } simple_audio_play = "C";
+{ i32 } simple_audio_stop = "C";
+{ i32 } simple_audio_playing = "C";
+{ i32 } simple_audio_close = "C";
+// Missing/disabled backend returns ERROR quietly; the game may remain silent.
+val open = { -> simple_audio_open(); };
+// Mono signed 16-bit PCM at RATE, up to ten seconds. Copies into native memory.
+// Load at startup: up to 28 custom handles, valid until close, then reload.
+val load = { i16[~] samples ->
+ i64 ptr = samples;
+ simple_audio_load(ptr,samples#);
+};
+// Starts/restarts immediately, replacing the previous sound; never waits for it.
+val play = { int sound -> simple_audio_play(sound); };
+val boop = { -> simple_audio_play(BOOP); };
+val win = { -> simple_audio_play(WIN); };
+val lose = { -> simple_audio_play(LOSE); };
+val stop = { -> simple_audio_stop(); };
+val playing = { -> simple_audio_playing(); };
+val close = { -> simple_audio_close(); };
diff --git a/src/main/smp/sys/draw.smp b/src/main/smp/sys/draw.smp
new file mode 100644
index 00000000..0721659a
--- /dev/null
+++ b/src/main/smp/sys/draw.smp
@@ -0,0 +1,17 @@
+// Software raster drawing. No window or native calls; pixels are packed RGB (0x00RRGGBB).
+u32 BLACK = 0x000000;
+u32 WHITE = 0xffffff;
+u32 RED = 0xe05252;
+u32 GREEN = 0x48b88c;
+u32 BLUE = 0x558ee6;
+
+// Components are clamped to bytes rather than wrapping.
+val rgb = { int r, int g, int b ->
+ r = r<0 ? 0 : r>255 ? 255 : r;
+ g = g<0 ? 0 : g>255 ? 255 : g;
+ b = b<0 ? 0 : b>255 ? 255 : b;
+ u32 color = (r<<16) | (g<<8) | b;
+ return color;
+};
+
+val Canvas = Canvas; // Pixel canvas and drawing methods
diff --git a/src/main/smp/sys/draw/Canvas.smp b/src/main/smp/sys/draw/Canvas.smp
new file mode 100644
index 00000000..8c01b4b3
--- /dev/null
+++ b/src/main/smp/sys/draw/Canvas.smp
@@ -0,0 +1,176 @@
+struct Canvas {
+ u32 width;
+ u32 height;
+ u32[] pixels;
+
+ new Canvas = { int w, int h ->
+ i64 count = w;
+ count *= h;
+ if( w<=0 || h<=0 || count>2147483647 ) libc._exit(1);
+ width = w;
+ height = h;
+ pixels = new u32[count];
+ };
+
+ val clear = { u32 color ->
+ for( int i=0; i<pixels#; i++ ) pixels[i] = color;
+ return self;
+ };
+
+ val pixel = { i64 x, i64 y, u32 color ->
+ if( 0<=x<width && 0<=y<height ) pixels[y*width+x] = color;
+ return self;
+ };
+
+ // Integer coordinates, top-left origin. Rectangle extents are half-open.
+ val fillRect = { i64 x, i64 y, i64 w, i64 h, u32 color ->
+ if( w<=0 || h<=0 ) return self;
+ i64 right = x; right += w;
+ i64 bottom = y; bottom += h;
+ if( right<=0 || bottom<=0 || x>=width || y>=height ) return self;
+ int x0 = x<0 ? 0 : x, y0 = y<0 ? 0 : y;
+ int x1 = right>width ? width : right, y1 = bottom>height ? height : bottom;
+ for( int py=y0; py<y1; py++ )
+ for( int px=x0; px<x1; px++ ) pixels[py*width+px] = color;
+ return self;
+ };
+
+ // Outline stays inside the rectangle. Wide borders become a filled rect.
+ val rect = { i64 x, i64 y, i64 w, i64 h, i64 thickness, u32 color ->
+ if( w<=0 || h<=0 || thickness<=0 ) return self;
+ if( thickness>=w || thickness>=h || thickness>=w-thickness || thickness>=h-thickness )
+ return fillRect(x,y,w,h,color);
+ fillRect(x,y,w,thickness,color);
+ fillRect(x,y+h-thickness,w,thickness,color);
+ fillRect(x,y+thickness,thickness,h-2*thickness,color);
+ return fillRect(x+w-thickness,y+thickness,thickness,h-2*thickness,color);
+ };
+
+ // Integer centers are pixel centers. Circle outlines extend inward.
+ val fillCircle = { i64 cx, i64 cy, int radius, u32 color ->
+ return _circle(cx,cy,radius,0,true,color);
+ };
+ val circle = { i64 cx, i64 cy, int radius, int thickness, u32 color ->
+ return _circle(cx,cy,radius,thickness,false,color);
+ };
+ val _circle = { i64 cx, i64 cy, int radius, int thickness, bool filled, u32 color ->
+ if( radius<0 || (!filled && thickness<=0) ) return self;
+ i64 left = cx; left -= radius;
+ i64 top = cy; top -= radius;
+ i64 right = cx; right += radius;
+ i64 bottom = cy; bottom += radius;
+ if( right<0 || bottom<0 || left>=width || top>=height ) return self;
+ int x0 = left<0 ? 0 : left, y0 = top<0 ? 0 : top;
+ int x1 = right>=width ? width-1 : right, y1 = bottom>=height ? height-1 : bottom;
+ i64 outer2 = radius;
+ outer2 *= radius;
+ i64 inner = filled ? -1 : radius-thickness;
+ for( int py=y0; py<=y1; py++ )
+ for( int px=x0; px<=x1; px++ ) {
+ i64 dx = px; dx -= cx;
+ i64 dy = py; dy -= cy;
+ i64 d2 = dx*dx+dy*dy;
+ if( d2<=outer2 && (inner<=0 || d2>inner*inner) ) pixels[py*width+px] = color;
+ }
+ return self;
+ };
+
+ // Round caps; a zero-length segment is a dot. Only the clipped bounding
+ // box is scanned. Floating distances avoid overflowing coordinate squares.
+ val line = { i64 x0, i64 y0, i64 x1, i64 y1, i64 thickness, u32 color ->
+ if( thickness<=0 ) return self;
+ i64 pad = thickness/2+1;
+ i64 left = x0<x1 ? x0 : x1; left -= pad;
+ i64 top = y0<y1 ? y0 : y1; top -= pad;
+ i64 right = x0>x1 ? x0 : x1; right += pad;
+ i64 bottom = y0>y1 ? y0 : y1; bottom += pad;
+ if( right<0 || bottom<0 || left>=width || top>=height ) return self;
+ int bx0 = left<0 ? 0 : left, by0 = top<0 ? 0 : top;
+ int bx1 = right>=width ? width-1 : right, by1 = bottom>=height ? height-1 : bottom;
+ for( int py=by0; py<=by1; py++ )
+ for( int px=bx0; px<=bx1; px++ )
+ if( _onLine(px,py,x0,y0,x1,y1,thickness) ) pixels[py*width+px] = color;
+ return self;
+ };
+
+ // Small 3x5 bitmap font for game labels. Lowercase maps to uppercase.
+ // Newline returns to x; unsupported characters draw a question mark.
+ val text = { i64 x, i64 y, u8[~] str, int scale, u32 color ->
+ if( scale<=0 ) return self;
+ i64 px = x, py = y;
+ for( int i=0; i<str#; i++ ) {
+ u8 c = str[i];
+ if( c=='\n' ) { px=x; py+=6*scale; continue; }
+ int bits = _glyph(c);
+ for( int row=0; row<5; row++ )
+ for( int col=0; col<3; col++ )
+ if( (bits >> (14-row*3-col)) & 1 )
+ fillRect(px+col*scale,py+row*scale,scale,scale,color);
+ px += 4*scale;
+ }
+ return self;
+ };
+};
+
+val _glyph = { u8 c ->
+ // Five octal digits: one 3-bit row per digit, top to bottom.
+ if( 'a'<=c<='z' ) c -= 'a'-'A';
+ if( c==' ' ) return 000000;
+ if( c=='0' ) return 075557;
+ if( c=='1' ) return 026227;
+ if( c=='2' ) return 061247;
+ if( c=='3' ) return 061216;
+ if( c=='4' ) return 055711;
+ if( c=='5' ) return 074616;
+ if( c=='6' ) return 034757;
+ if( c=='7' ) return 071222;
+ if( c=='8' ) return 075757;
+ if( c=='9' ) return 075716;
+ if( c=='A' ) return 025755;
+ if( c=='B' ) return 065656;
+ if( c=='C' ) return 034443;
+ if( c=='D' ) return 065556;
+ if( c=='E' ) return 074647;
+ if( c=='F' ) return 074644;
+ if( c=='G' ) return 034553;
+ if( c=='H' ) return 055755;
+ if( c=='I' ) return 072227;
+ if( c=='J' ) return 011152;
+ if( c=='K' ) return 055655;
+ if( c=='L' ) return 044447;
+ if( c=='M' ) return 057755;
+ if( c=='N' ) return 057775;
+ if( c=='O' ) return 025552;
+ if( c=='P' ) return 065644;
+ if( c=='Q' ) return 025573;
+ if( c=='R' ) return 065655;
+ if( c=='S' ) return 034216;
+ if( c=='T' ) return 072222;
+ if( c=='U' ) return 055557;
+ if( c=='V' ) return 055552;
+ if( c=='W' ) return 055775;
+ if( c=='X' ) return 055255;
+ if( c=='Y' ) return 055222;
+ if( c=='Z' ) return 071247;
+ if( c=='-' ) return 000700;
+ if( c=='.' ) return 000002;
+ if( c==':' ) return 002020;
+ if( c=='!' ) return 022202;
+ if( c=='?' ) return 061202;
+ return 061202;
+};
+
+// Pixel coverage for a round-ended segment, separate from traversal.
+val _onLine = { i64 px, i64 py, i64 x0, i64 y0, i64 x1, i64 y1, i64 thickness ->
+ f64 dx = x1; dx -= x0;
+ f64 dy = y1; dy -= y0;
+ f64 vx = px; vx -= x0;
+ f64 vy = py; vy -= y0;
+ f64 length2 = dx*dx+dy*dy;
+ bool point = x0==x1 && y0==y1;
+ f64 t = point ? 0.0 : (vx*dx+vy*dy)/length2;
+ t = t<0 ? 0.0 : t>1 ? 1.0 : t;
+ f64 ex = vx-t*dx, ey = vy-t*dy;
+ f64 half = thickness/2.0;
+ return ex*ex+ey*ey<=half*half;
+};
diff --git a/src/main/smp/sys/gfx.smp b/src/main/smp/sys/gfx.smp
new file mode 100644
index 00000000..1f0e3ca9
--- /dev/null
+++ b/src/main/smp/sys/gfx.smp
@@ -0,0 +1,38 @@
+// Minimal pixel-window interface; drawing belongs in Simple.
+// One fixed-size window, main thread only. Pixel format is 0x00RRGGBB.
+// open/present/poll return OK, CLOSED (normal user close/Esc), or ERROR.
+int OK = 0;
+int CLOSED = 1;
+int ERROR = -1;
+int LEFT = 1;
+int ESCAPE = 256;
+
+{ i64 u32 u32 -> i32 } simple_gfx_open = "C";
+{ i64 u32 -> i32 } simple_gfx_present = "C";
+{ i32 -> i32 } simple_gfx_poll = "C";
+{ i32 } simple_gfx_mouse_x = "C";
+{ i32 } simple_gfx_mouse_y = "C";
+{ i32 -> i32 } simple_gfx_button = "C";
+{ i32 -> i32 } simple_gfx_key = "C";
+{ i32 } simple_gfx_close = "C";
+
+// Failure prints a diagnostic to stderr and returns ERROR; callers decide
+// whether to exit or fall back to text. No native library is loaded until here.
+val open = { u8[~] title, u32 width, u32 height ->
+ i64 ptr = aryu8.str0(title);
+ simple_gfx_open(ptr,width,height);
+};
+// Array length must be width*height. The native layer copies for OS repaints;
+// it never keeps a pointer into the Simple heap after this call returns.
+val present = { u32[~] pixels ->
+ i64 ptr = pixels;
+ simple_gfx_present(ptr,pixels#);
+};
+// wait=true waits about 17ms before polling, avoiding a busy loop.
+val poll = { bool wait -> simple_gfx_poll(wait); };
+val mouseX = { -> simple_gfx_mouse_x(); };
+val mouseY = { -> simple_gfx_mouse_y(); };
+val button = { int button -> simple_gfx_button(button); };
+// Printable keys use uppercase ASCII; special keys use MiniFB's key codes.
+val key = { int key -> simple_gfx_key(key); };
+val close = { -> simple_gfx_close(); };
diff --git a/src/test/c/audio_test.c b/src/test/c/audio_test.c
new file mode 100644
index 00000000..f82504c3
--- /dev/null
+++ b/src/test/c/audio_test.c
@@ -0,0 +1,48 @@
+// Explicit optional integration checks: real miniaudio thread, silent device.
+#define SIMPLE_AUDIO_TEST_NULL
+#include "../../main/c/simple_miniaudio.c"
+#include <assert.h>
+
+int main(void) {
+ assert(api.play(1)==-1 && api.playing()==0);
+ assert(api.stop()==0);
+ api.close();
+ for (int pass=0;pass<2;++pass) {
+ assert(api.open()==0 && api.open()==0);
+ assert(api.load(NULL,1)==-1);
+ int16_t pcm[RATE]={0};
+ assert(api.load(pcm,0)==-1 && api.load(pcm,RATE*10+1)==-1);
+ int sound=api.load(pcm,RATE);
+ assert(sound==4);
+ pcm[0]=12345;
+ assert(sounds[sound].pcm[0]==0);
+ assert(api.play(-1)==-1 && api.play(0)==-1 && api.play(SLOTS)==-1);
+ assert(api.play(sound)==0 && api.playing());
+ // A one-second sound returns while playback is still in progress.
+ assert(api.stop()==0 && !api.playing());
+ for (int cue=1;cue<=3;++cue) {
+ assert(api.play(cue)==0);
+ int elapsed=0;
+ while (api.playing() && elapsed<2000) { ma_sleep(10); elapsed+=10; }
+ assert(!api.playing());
+ }
+ for (int i=5;i<SLOTS;++i) assert(api.load(pcm,1)==i);
+ assert(api.load(pcm,1)==-1);
+ assert(api.play(sound)==0);
+ api.close(); // Join during playback, then reopen next pass.
+ api.close();
+ assert(api.play(sound)==-1 && !api.playing());
+ }
+ // The loss cue reverses exactly the same three softly enveloped notes.
+ assert(tone(1,0)==0 && tone(2,0)==0 && tone(3,1)==0);
+ uint32_t note=RATE*16/100;
+ for (int n=0;n<3;++n)
+ assert(memcmp(sounds[2].pcm+n*note,sounds[3].pcm+(2-n)*note,note*sizeof(int16_t))==0);
+ for (int i=1;i<=3;++i) {
+ assert(sounds[i].pcm[0]==0);
+ for (uint32_t j=0;j<sounds[i].count;++j) assert(abs(sounds[i].pcm[j])<=3200);
+ free(sounds[i].pcm); sounds[i].pcm=NULL;
+ }
+ puts("audio native checks passed");
+ return 0;
+}
diff --git a/src/test/java/com/seaofnodes/simple/AudioTest.java b/src/test/java/com/seaofnodes/simple/AudioTest.java
new file mode 100644
index 00000000..db8542d0
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/AudioTest.java
@@ -0,0 +1,94 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.util.Ary;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+// Loader and Simple/C ABI checks: no miniaudio download or audio device.
+public class AudioTest {
+ private static String missing, playback;
+
+ @BeforeClass public static void compilePrograms() throws Exception {
+ var paths=new Ary<>(new String[]{TestC.RELEASE_SYS_DIR});
+ missing=TestC.compile("""
+ val main = { ->
+ if( sys.audio.open()!=sys.audio.ERROR ) return 1;
+ if( sys.audio.boop()!=sys.audio.ERROR || sys.audio.playing() ) return 2;
+ sys.audio.stop(); sys.audio.close(); sys.audio.close();
+ sys.io.p("silent fallback\\n");
+ return 0;
+ };
+ ""","audio_missing",paths,TestC.CALL_CONVENTION,null,null,-1);
+ playback=TestC.compile("""
+ val main = { ->
+ if( sys.audio.open()!=sys.audio.OK ) return 1;
+ val pcm = new i16[3];
+ pcm[0]=-32768; pcm[1]=123; pcm[2]=32767;
+ int sound = sys.audio.load(pcm);
+ if( sound!=4 || sys.audio.play(sound)!=0 || !sys.audio.playing() ) return 2;
+ if( sys.audio.boop()!=0 || sys.audio.win()!=0 || sys.audio.lose()!=0 ) return 3;
+ sys.audio.stop();
+ if( sys.audio.playing() ) return 4;
+ sys.audio.close();
+ return 0;
+ };
+ ""","audio_playback",paths,TestC.CALL_CONVENTION,null,null,-1);
+ }
+
+ private static String run(String exe, String backend) throws Exception {
+ ProcessBuilder pb=new ProcessBuilder(exe).redirectErrorStream(true);
+ if( backend==null ) pb.environment().remove("SIMPLE_MINIAUDIO");
+ else pb.environment().put("SIMPLE_MINIAUDIO",backend);
+ Process child=pb.start();
+ try {
+ assertTrue("audio program timed out",child.waitFor(10,TimeUnit.SECONDS));
+ String out=new String(child.getInputStream().readAllBytes(),StandardCharsets.UTF_8);
+ assertEquals(out,0,child.exitValue());
+ return out;
+ } finally { if( child.isAlive() ) child.destroyForcibly(); }
+ }
+
+ @Test public void silentFallback() throws Exception {
+ assertEquals("silent fallback\n",run(missing,null));
+ assertEquals("silent fallback\n",run(missing,"off"));
+ assertTrue(run(missing,"no-such-miniaudio-backend").contains("cannot load backend"));
+ }
+
+ @Test public void nativeBoundary() throws Exception {
+ Path source=Path.of("build/objs/audio_stub.c");
+ Path library=Path.of("build/objs/audio_stub"+(TestC.OS.startsWith("Windows") ? ".dll" : ".so"));
+ Files.writeString(source,"""
+ #include "simple_audio.h"
+ static int active;
+ static int32_t open_audio(void) { return 0; }
+ static int32_t load(const int16_t *p, uint32_t n) {
+ return n==3 && p[0]==-32768 && p[1]==123 && p[2]==32767 ? 4 : -1;
+ }
+ static int32_t play(int32_t cue) { active=1; return cue>=1 && cue<=4 ? 0 : -1; }
+ static int32_t stop(void) { active=0; return 0; }
+ static int32_t playing(void) { return active; }
+ static void close_audio(void) { active=0; }
+ static const struct simple_audio_api api={1,open_audio,load,play,stop,playing,close_audio};
+ const struct simple_audio_api *simple_miniaudio_api(void) { return &api; }
+ """);
+ TestC.exec(30,"gcc","-shared","-fPIC","-Isrc/main/c",source.toString(),"-o",library.toString());
+ assertEquals("",run(playback,library.toAbsolutePath().toString()));
+ }
+
+ @Test public void incompatibleBackend() throws Exception {
+ Path source=Path.of("build/objs/audio_bad.c");
+ Path library=Path.of("build/objs/audio_bad"+(TestC.OS.startsWith("Windows") ? ".dll" : ".so"));
+ Files.writeString(source,"""
+ #include "simple_audio.h"
+ static const struct simple_audio_api api={999};
+ const struct simple_audio_api *simple_miniaudio_api(void) { return &api; }
+ """);
+ TestC.exec(30,"gcc","-shared","-fPIC","-Isrc/main/c",source.toString(),"-o",library.toString());
+ assertTrue(run(missing,library.toAbsolutePath().toString()).contains("incompatible backend"));
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/BrainFuckTest.java b/src/test/java/com/seaofnodes/simple/BrainFuckTest.java
index 63858df0..07e702c7 100644
--- a/src/test/java/com/seaofnodes/simple/BrainFuckTest.java
+++ b/src/test/java/com/seaofnodes/simple/BrainFuckTest.java
@@ -1,19 +1,21 @@
package com.seaofnodes.simple;
import com.seaofnodes.isa.eval.EvalRisc5;
-
import com.seaofnodes.isa.eval.EvalArm64;
-
import java.io.IOException;
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
import java.nio.file.Files;
import java.nio.file.Path;
-
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class BrainFuckTest {
@Test public void testBrainFuckAryu8() throws IOException {
+
+
String brain_fuck = "Hello World!\n";
String src =
"""
diff --git a/src/test/java/com/seaofnodes/simple/Chapter01Test.java b/src/test/java/com/seaofnodes/simple/Chapter01Test.java
index fa7fb68a..6db79c16 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter01Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter01Test.java
@@ -41,13 +41,8 @@ public class Chapter01Test {
}
@Test
- public void testBad2() {
- try {
- new CodeGen("return 0123;").parse();
- fail();
- } catch( RuntimeException e ) {
- assertEquals("Syntax error: integer values cannot start with '0'",e.getMessage());
- }
+ public void testOctal() {
+ assertEquals("return 83;",new CodeGen("return 0123;").parse().print());
}
@Test
diff --git a/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/src/test/java/com/seaofnodes/simple/Chapter21Test.java
index c6d558eb..1a57d4da 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter21Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter21Test.java
@@ -531,7 +531,8 @@ val sieve = { int N ->
String sprimes = sb.p("]").toString();
// Compile, link against native C; expect the above string of primes to be printed out by C
- TestC.runC(src, "sieve", sprimes,344);
+ // Load/Phi hoisting now requires the pointer before the loop entry.
+ TestC.runC(src, "sieve", sprimes,216);
// Evaluate on RISC5 emulator; expect return of an array of primes in
// the simulated heap.
diff --git a/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/src/test/java/com/seaofnodes/simple/Chapter25Test.java
index 67e7f982..7f2b4a77 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter25Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter25Test.java
@@ -475,9 +475,9 @@ public class Chapter25Test {
// Elf files are sane
- // Sys depends on io, libc, text, char, collections, and array utilities.
+ // Software drawing and the optional native graphics interface.
assertArrayEquals(new String[]{"sys/adt", "sys/ary",
- "sys/aryi64", "sys/aryu8", "sys/char", "sys/io", "sys/libc", "sys/rt", "sys/scan", "sys/text"},sys_elf._deps);
+ "sys/aryi64", "sys/aryu8", "sys/audio", "sys/char", "sys/draw", "sys/gfx", "sys/io", "sys/libc", "sys/rt", "sys/scan", "sys/text"},sys_elf._deps);
assertArrayEquals(new String[]{"sys"},sys_elf._units);
assertSame("class:sys" ,sys_elf._clz._name);
}
diff --git a/src/test/java/com/seaofnodes/simple/DrawTest.java b/src/test/java/com/seaofnodes/simple/DrawTest.java
new file mode 100644
index 00000000..6eb261f1
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/DrawTest.java
@@ -0,0 +1,105 @@
+package com.seaofnodes.simple;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Test;
+
+// Native raster and game checks; no backend or display is needed.
+public class DrawTest {
+ @Test public void rectanglesAndColors() throws Exception {
+ TestC.runSYS("""
+ val main = { ->
+ val c = new sys.draw.Canvas(12,10);
+ u32 color = sys.draw.rgb(300,-1,128);
+ if( color!=16711808 ) return 1;
+ c.clear(7).fillRect(-2,-1,5,4,color);
+ for( int y=0; y<10; y++ )
+ for( int x=0; x<12; x++ )
+ if( c.pixels[y*12+x]!=(x<3 && y<3 ? color : 7) ) return 2;
+ c.clear(0).rect(2,2,7,6,1,9);
+ for( int y=0; y<10; y++ )
+ for( int x=0; x<12; x++ ) {
+ var inside = 2<=x<9 && 2<=y<8;
+ var border = inside && (x==2 || x==8 || y==2 || y==7);
+ if( c.pixels[y*12+x]!=(border ? 9 : 0) ) return 3;
+ }
+ c.clear(5).fillRect(20,0,8,8,1).rect(0,0,12,10,0,1);
+ c.fillRect(0,0,-1,5,1).pixel(-1,0,1).pixel(12,9,1);
+ for( int i=0; i<c.pixels#; i++ ) if( c.pixels[i]!=5 ) return 4;
+ return 0;
+ };
+ ""","drawRectangles","",-1);
+ }
+
+ @Test public void circlesLinesAndText() throws Exception {
+ TestC.runSYS("""
+ val main = { ->
+ val c = new sys.draw.Canvas(16,16);
+ c.fillCircle(8,8,3,1);
+ for( int y=0; y<16; y++ )
+ for( int x=0; x<16; x++ )
+ if( c.pixels[y*16+x]!=((x-8)*(x-8)+(y-8)*(y-8)<=9 ? 1 : 0) ) {
+ sys.io.p("circle mismatch %(x) %(y): %(c.pixels[y*16+x])\\n");
+ return 1;
+ }
+ c.clear(0).circle(8,8,3,1,2);
+ if( c.pixels[8*16+8]!=0 || c.pixels[8*16+11]!=2 || c.pixels[8*16+10]!=0 ) return 2;
+ c.clear(0).circle(8,8,3,3,2);
+ if( c.pixels[8*16+8]!=2 ) return 8;
+ c.clear(0).line(-5,8,20,8,3,3);
+ for( int y=0; y<16; y++ )
+ for( int x=0; x<16; x++ )
+ if( c.pixels[y*16+x]!=(7<=y<=9 ? 3 : 0) ) return 3;
+ c.clear(0).line(8,8,8,8,2,4);
+ if( c.pixels[8*16+8]!=4 || c.pixels[8*16+9]!=4 || c.pixels[9*16+9]!=0 ) return 4;
+ c.clear(0).line(2,2,13,13,1,5);
+ if( c.pixels[7*16+7]!=5 || c.pixels[7*16+8]!=0 ) return 5;
+ c.clear(0).text(0,0,"x",1,6).text(8,0,"X",1,6);
+ for( int y=0; y<5; y++ )
+ for( int x=0; x<3; x++ )
+ if( c.pixels[y*16+x]!=c.pixels[y*16+x+8] ) return 6;
+ c.clear(0).fillCircle(-1,-1,2,7);
+ if( c.pixels[0]!=7 || c.pixels[2]!=0 ) return 7;
+ return 0;
+ };
+ ""","drawCurves","",-1);
+ }
+
+ @Test public void graphicalGameWithoutWindow() throws Exception {
+ String game=Files.readString(Path.of("docs/examples/TicTacToeGraphical.smp"))
+ .replace("val main =","val _windowMain =");
+ TestC.runSYS(game+"""
+
+ val main = { ->
+ val board = new u8[9];
+ _reset(board);
+ if( _square(48,168)!=0 || _square(551,671)!=8 || _square(552,400)!=-1 || _square(100,167)!=-1 ) return 1;
+ if( !_move(board,4) || board[4]!='X' || board[0]!='O' ) return 2;
+ if( _sound(board)!=sys.audio.BOOP ) return 10;
+ if( _move(board,4) || _move(board,0) || _move(board,-1) || _move(board,9) ) return 3;
+ val moves = "1879";
+ _reset(board);
+ for( int i=0; i<moves#; i++ )
+ if( !_move(board,moves[i]-'1') ) return 4;
+ if( _winner(board)!='X' || _move(board,1) ) return 5;
+ if( _sound(board)!=sys.audio.WIN ) return 11;
+ val screen = new sys.draw.Canvas(600,780);
+ _draw(screen,board,-1);
+ if( screen.pixels[252*600+132]==screen.pixels[0] ) return 6;
+ _reset(board);
+ val draw = "12768";
+ for( int i=0; i<draw#; i++ ) _move(board,draw[i]-'1');
+ if( !_finished(board) || _winner(board)!=0 ) return 7;
+ _reset(board);
+ _move(board,0); _move(board,1); _move(board,3);
+ if( _winner(board)!='O' ) return 8;
+ if( _sound(board)!=sys.audio.LOSE ) return 12;
+ _draw(screen,board,-1);
+ _reset(board);
+ if( _finished(board) ) return 9;
+ _draw(screen,board,4);
+ return 0;
+ };
+ ""","drawGame","",-1);
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/GraphicsTest.java b/src/test/java/com/seaofnodes/simple/GraphicsTest.java
new file mode 100644
index 00000000..9c432ce4
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/GraphicsTest.java
@@ -0,0 +1,75 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.util.Ary;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+// These tests never open a window and do not need MiniFB installed.
+public class GraphicsTest {
+ private static String graphics, text, game;
+
+ @BeforeClass public static void compilePrograms() throws Exception {
+ var paths = new Ary<>(new String[]{TestC.RELEASE_SYS_DIR});
+ graphics = TestC.compile(Files.readString(Path.of("docs/examples/Graphics.smp")),
+ "graphics_missing",paths,TestC.CALL_CONVENTION,null,null,-1);
+ text = TestC.compile("val main={ -> sys.io.p(\"text still works\\n\"); return 0; };",
+ "graphics_text",paths,TestC.CALL_CONVENTION,null,null,-1);
+ game = TestC.compile(Files.readString(Path.of("docs/examples/TicTacToeGraphical.smp")),
+ "graphics_game",paths,TestC.CALL_CONVENTION,null,null,-1);
+ }
+
+ private static String run(String exe, String backend, int exit) throws Exception {
+ ProcessBuilder pb = new ProcessBuilder(exe).redirectErrorStream(true);
+ if( backend == null ) pb.environment().remove("SIMPLE_MINIFB");
+ else pb.environment().put("SIMPLE_MINIFB",backend);
+ Process child = pb.start();
+ try {
+ assertTrue("native program timed out",child.waitFor(10,TimeUnit.SECONDS));
+ String output = new String(child.getInputStream().readAllBytes(),java.nio.charset.StandardCharsets.UTF_8);
+ assertEquals(output,exit,child.exitValue());
+ return output;
+ } finally { if( child.isAlive() ) child.destroyForcibly(); }
+ }
+
+ @Test public void missingConfiguration() throws Exception {
+ String output = run(graphics,null,1);
+ assertTrue(output,output.contains("MiniFB backend not configured"));
+ assertTrue(output,output.contains("make graphics"));
+ }
+
+ @Test public void disabledGraphics() throws Exception {
+ assertTrue(run(graphics,"off",1).contains("disabled"));
+ }
+
+ @Test public void gameWithoutBackend() throws Exception {
+ assertTrue(run(game,"off",1).contains("disabled"));
+ }
+
+ @Test public void missingLibrary() throws Exception {
+ String missing = Path.of("build","no-such-minifb-backend").toAbsolutePath().toString();
+ String output = run(graphics,missing,1);
+ assertTrue(output,output.contains("cannot load MiniFB backend"));
+ }
+
+ @Test public void textIgnoresGraphicsConfiguration() throws Exception {
+ assertEquals("text still works\n",run(text,"off",0));
+ assertEquals("text still works\n",run(text,"no-such-backend",0));
+ }
+
+ @Test public void incompatibleLibrary() throws Exception {
+ Path source = Path.of("build/objs/graphics_incompatible.c");
+ Path library = Path.of("build/objs/graphics_incompatible" +
+ (TestC.OS.startsWith("Windows") ? ".dll" : ".so"));
+ Files.writeString(source,"""
+ #include "simple_graphics.h"
+ static const struct simple_graphics_api api = {999};
+ const struct simple_graphics_api *simple_minifb_api(void) { return &api; }
+ """);
+ TestC.exec(30,"gcc","-shared","-fPIC","-Isrc/main/c",source.toString(),"-o",library.toString());
+ assertTrue(run(graphics,library.toAbsolutePath().toString(),1).contains("incompatible MiniFB backend"));
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/IntegerLiteralTest.java b/src/test/java/com/seaofnodes/simple/IntegerLiteralTest.java
new file mode 100644
index 00000000..975042c2
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/IntegerLiteralTest.java
@@ -0,0 +1,46 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.type.TypeFloat;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class IntegerLiteralTest {
+ private static void value(String expression, long expected) {
+ var code = new CodeGen("return "+expression+";").parse();
+ assertEquals(expression,TypeInteger.constant(expected),code.expr()._type);
+ }
+
+ @Test public void valuesAndBitPatterns() {
+ value("0x00e47b57",14973783);
+ value("0XfF",255);
+ value("025755",11245); // A: rows 010, 101, 111, 101, 101.
+ value("000000",0);
+ value("0x7fffffffffffffff",Long.MAX_VALUE);
+ value("0x8000000000000000",Long.MIN_VALUE);
+ value("0xffffffffffffffff",-1);
+ value("0777777777777777777777",Long.MAX_VALUE);
+ value("01000000000000000000000",Long.MIN_VALUE);
+ value("01777777777777777777777",-1);
+ value("0x00000000000000000001",1);
+ value("-0x2a + 052",0);
+ value("0x1e+2",32); // e and f are hex digits, not float markers.
+ value("0xff & 017",15);
+ value("017<<0x4",240);
+ value("0x0f",15);
+ value("0123",83);
+ value("000000000000000000000000000000000001",1);
+ assertEquals(TypeFloat.constant(12.5),new CodeGen("return 012.5;").parse().expr()._type);
+ assertEquals(TypeFloat.constant(12.0),new CodeGen("return 012f;").parse().expr()._type);
+ }
+
+ @Test public void nativeLiterals() throws Exception {
+ TestC.runSF("""
+ val check_noInline = { i64 mask, u32 color, int glyph ->
+ return mask==-1 && color==14973783 && glyph==11245 ? 0 : 1;
+ };
+ val main = { -> return check_noInline(0xffffffffffffffff,0x00e47b57,025755); };
+ ""","basedLiterals","",-1);
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java b/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java
index 2b7eea78..ef26a943 100644
--- a/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java
+++ b/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java
@@ -30,7 +30,18 @@ public class ParserDiagnosticTest {
diagnostic("return 9223372036854775808;",1,8,"Integer literal is outside the signed 64-bit range");
diagnostic("return 1e;",1,8,"Invalid floating-point literal '1e'");
diagnostic("return 1.2.3f;",1,8,"Invalid floating-point literal '1.2.3f'");
- diagnostic("return 012;",1,8,"Syntax error: integer values cannot start with '0'");
+ diagnostic("return 018;",1,8,"Invalid octal integer literal '018'");
+ }
+
+ @Test public void testBasedIntegerLiteralErrors() {
+ diagnostic("return 0x;",1,8,"Hexadecimal integer literal requires at least one digit");
+ diagnostic("return 0x12g;",1,8,"Invalid hexadecimal integer literal '0x12g'");
+ diagnostic("return 0128;",1,8,"Invalid octal integer literal '0128'");
+ diagnostic("return 0x_ff;",1,8,"Invalid hexadecimal integer literal '0x_ff'");
+ diagnostic("return 09;",1,8,"Invalid octal integer literal '09'");
+ diagnostic("return 0x10000000000000000;",1,8,"Hexadecimal integer literal exceeds 64 bits");
+ diagnostic("return 02000000000000000000000;",1,8,"Octal integer literal exceeds 64 bits");
+ diagnostic("int 0xabc;",1,5,"Expected an identifier, found '0xabc'");
}
@Test public void testUnclosedCommentsAndStrings() {
diff --git a/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
index 1853aa48..1c278791 100644
--- a/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
+++ b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
@@ -10,6 +10,22 @@ import org.junit.Test;
import static org.junit.Assert.*;
public class SeparateCompilationTest {
+ @Test public void testImportedReadOnlyPrimitiveArray() throws Exception {
+ Path build=Path.of("build/objs/separate");
+ Files.createDirectories(build);
+ Path lib=Files.createTempDirectory(build,"pixels-");
+ new CodeGen(null,lib.toString(),null,"Pixels",
+ "val first={ u32[~] pixels -> pixels[0]; };",123,true,TypeInteger.BOT)
+ .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
+ var paths=new Ary<>(new String[]{lib.toString()});
+ CodeGen code=new CodeGen(null,"build/objs",paths,"PixelClient","""
+ val pixels=new u32[2];
+ pixels[0]=4294967295;
+ return Pixels.first(pixels);
+ """,123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("4294967295",Eval2.eval(code,0));
+ }
+
@Test public void testImportedNativeExposure() throws Exception {
Path build=Path.of("build/objs/separate");
Files.createDirectories(build);
diff --git a/src/test/java/com/seaofnodes/simple/StreamIOTest.java b/src/test/java/com/seaofnodes/simple/StreamIOTest.java
index f3491314..e47cd929 100644
--- a/src/test/java/com/seaofnodes/simple/StreamIOTest.java
+++ b/src/test/java/com/seaofnodes/simple/StreamIOTest.java
@@ -131,6 +131,9 @@ public class StreamIOTest extends TestC {
gcc.addAll(linkObjs(code));
gcc.add(runtimeObject("simple_crt"));
gcc.add(runtimeObject("simple_args"));
+ gcc.add(runtimeObject("simple_graphics"));
+ gcc.add(runtimeObject("simple_audio"));
+ if( !OS.startsWith("Windows") ) gcc.add("-ldl");
gcc.addAll(new String[]{"-lm","-o",exe});
exec(TEST_TIMEOUT_SECONDS,gcc.asAry());
assertEquals("ok\n",exec(TEST_TIMEOUT_SECONDS,exe));
diff --git a/src/test/java/com/seaofnodes/simple/TestC.java b/src/test/java/com/seaofnodes/simple/TestC.java
index dd7999c9..741ee13b 100644
--- a/src/test/java/com/seaofnodes/simple/TestC.java
+++ b/src/test/java/com/seaofnodes/simple/TestC.java
@@ -190,6 +190,10 @@ public abstract class TestC {
params.add(OS.startsWith("Windows") ? "-Wl,--image-base,0x400000" : "-no-pie");
if( cfile!=null ) params.add(cfile); // Associated C driver, usually has a `main`
params.addAll(objects);
+ // The loader has no MiniFB dependency and opens nothing until gfx.open.
+ params.add(runtimeObject("simple_graphics"));
+ params.add(runtimeObject("simple_audio"));
+ if( !OS.startsWith("Windows") ) params.add("-ldl");
// Standalone Simple programs export `simple_main`; the small C
// runtime owns native main and calls the Simple argument converter
// before making the ABI-qualified entry call.