summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.argsT.txt1
-rw-r--r--.out.txt304
-rw-r--r--Makefile26
-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.md266
-rw-r--r--docs/chapter31/README.ja.md20
-rw-r--r--docs/chapter31/README.md62
-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.md100
-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/Clock.smp105
-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.md84
-rw-r--r--docs/chapter31/examples/TicTacToe.smp133
-rw-r--r--docs/chapter31/examples/TicTacToeGraphical.smp167
-rw-r--r--docs/chapter31/graphics.md132
-rw-r--r--docs/chapter31/time.md119
-rw-r--r--docs/roadmap.md86
-rw-r--r--graphics.mk70
-rw-r--r--skills/chapter25-codex-notes.md24
-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.c102
-rw-r--r--src/main/c/simple_graphics.h20
-rw-r--r--src/main/c/simple_miniaudio.c123
-rw-r--r--src/main/c/simple_minifb.c117
-rw-r--r--src/main/c/simple_time.c61
-rw-r--r--src/main/java/com/seaofnodes/simple/README.md2
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/Encoding.java10
-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/MemMergeNode.java16
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java27
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java18
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java17
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Expressions.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Lexer.java35
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/README.md2
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Strings.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/package-info.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/print/ASMPrinter.java4
-rw-r--r--src/main/java/com/seaofnodes/simple/type/TypeMem.java9
-rw-r--r--src/main/smp/sys.smp4
-rw-r--r--src/main/smp/sys/audio.smp29
-rw-r--r--src/main/smp/sys/draw.smp18
-rw-r--r--src/main/smp/sys/draw/Button.smp41
-rw-r--r--src/main/smp/sys/draw/Canvas.smp176
-rw-r--r--src/main/smp/sys/gfx.smp42
-rw-r--r--src/main/smp/sys/text.smp5
-rw-r--r--src/main/smp/sys/time.smp191
-rw-r--r--src/main/smp/sys/time/DateTime.smp11
-rw-r--r--src/main/smp/sys/time/Timers.smp55
-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.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter25Test.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/DrawTest.java221
-rw-r--r--src/test/java/com/seaofnodes/simple/FloatComparisonNativeTest.java65
-rw-r--r--src/test/java/com/seaofnodes/simple/GraphicsTest.java127
-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.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/StringFormatTest.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/TestC.java5
-rw-r--r--src/test/java/com/seaofnodes/simple/TimeTest.java176
-rw-r--r--src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java2
82 files changed, 4580 insertions, 456 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..c9a398e8 100644
--- a/Makefile
+++ b/Makefile
@@ -37,6 +37,9 @@ 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_TIME:= $(RELEASE)/$(CPU_ABI)/simple_time.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 +68,32 @@ $(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) $(RELEASE_TIME) 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 $@ $<
+
+$(RELEASE_TIME): src/main/c/simple_time.c 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..9e199d03
--- /dev/null
+++ b/docs/chapter31/31-grammar.md
@@ -0,0 +1,266 @@
+# 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 TIME_STYLE? ;
+// 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 t.
+// TIME_STYLE: F r R M D T, allowed only after t; default F (ISO UTC).
+// Time styles: ISO, 12-hour, 24-hour, elapsed MM:SS.mmm, DD/MM/YY, HH:MM:SS.
+// Duplicate/inapplicable flags and precision on c/b/t are rejected.
+// Time accepts width and left alignment only; see time.md.
+// 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..ef6b0cdc
--- /dev/null
+++ b/docs/chapter31/README.md
@@ -0,0 +1,62 @@
+# Chapter 31: Software Drawing, Time, 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.
+
+The [time library](time.md) adds millisecond clocks, calendar conversion,
+absolute ISO parsing, built-in time formatting, sleeps, and cooperative timers.
+The [digital and analog clock](examples/Clock.smp) demonstrates single-threaded
+animation and the reusable `sys.draw.Button` widget:
+`make -C chapter31 run-clock CTAGS=`. Click its button or press Space to switch
+between green seven-segment digits and an analog dial.
+
+```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..af0a7f06
--- /dev/null
+++ b/docs/chapter31/drawing.md
@@ -0,0 +1,100 @@
+# 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`.
+
+## Button widget
+
+`new sys.draw.Button(x,y,width,height,label)` creates a rectangular software
+button. It owns no window or native resources. Feed pointer coordinates and
+the held mouse-button state to `update(x,y,down)` once per event-loop iteration.
+It returns true once on release inside, provided the press began inside.
+Dragging in from outside does not activate it; releasing outside cancels.
+
+`hovered` and `pressed` expose visual state. Set `enabled=false` to cancel a
+pending press and disable activation; `label` can be changed at any time.
+`contains(x,y)` uses half-open bounds. `draw(canvas,scale,foreground,background,active)`
+paints the button and centers its 3x5 label when it fits. Colors belong to the
+caller. Keyboard shortcuts belong to the application; this small widget has
+no focus system. The [clock example](examples/Clock.smp) shows mouse activation
+and a Space shortcut in the same single-threaded loop.
+
+## 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.
+Backend discovery and explicit overrides follow [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/Clock.smp b/docs/chapter31/examples/Clock.smp
new file mode 100644
index 00000000..900126e1
--- /dev/null
+++ b/docs/chapter31/examples/Clock.smp
@@ -0,0 +1,105 @@
+// Local clocks; click the button or press Space to switch. Q/Escape exits.
+// Any argument runs ten frames, exercising both faces for smoke tests.
+u32 GREEN=0x62ff70, DIM=0x123d20, BACKGROUND=0x020b05;
+val main = { u8[~][] args ->
+ val screen=new sys.draw.Canvas(720,540);
+ if( sys.gfx.open("Simple digital and analog clocks",screen.width,screen.height)!=sys.gfx.OK ) return 1;
+ val button=new sys.draw.Button(220,454,280,52,"ANALOG CLOCK");
+ val timers=new sys.time.Timers();
+ i64 frame=timers.every(33);
+ if( frame<0 ) { sys.gfx.close(); return 1; }
+ bool analog=false, held=false;
+ int frames=0, status=sys.gfx.OK;
+ while( status==sys.gfx.OK ) {
+ bool space=sys.gfx.key(' ');
+ bool down=sys.gfx.button(sys.gfx.LEFT);
+ bool click=button.update(sys.gfx.mouseX(),sys.gfx.mouseY(),down);
+ if( click || (space && !held) ) analog=!analog;
+ held=space;
+ button.label=analog ? "DIGITAL CLOCK" : "ANALOG CLOCK";
+ if( sys.gfx.key('Q') ) break;
+ if( timers.poll(sys.time.monotonic())==frame ) {
+ _draw(screen,sys.time.now(),analog);
+ button.draw(screen,3,GREEN,BACKGROUND,DIM);
+ status=sys.gfx.present(screen.pixels);
+ if( args#>1 ) {
+ frames++;
+ if( frames==5 ) analog=true;
+ if( frames==10 ) break;
+ }
+ }
+ if( status==sys.gfx.OK ) status=sys.gfx.wait(sys.time.remaining(timers.nextDeadline(),sys.time.monotonic()));
+ }
+ sys.gfx.close();
+ return status==sys.gfx.ERROR ? 1 : 0;
+};
+val _draw = { !sys.draw.Canvas screen, i64 wall, i64 analog ->
+ val d=sys.time.local(wall);
+ screen.clear(BACKGROUND);
+ screen.text(266,24,"SIMPLE CLOCK",4,GREEN);
+ if( analog ) _analog_noInline(screen,d.hour,d.minute,d.second,d.millisecond);
+ else _digital_noInline(screen,d.hour,d.minute,d.second);
+ screen.text(298,412,"%(sys.time.formatLocal(wall,sys.time.DATE))",4,GREEN);
+ screen.text(249,520,"SPACE SWITCH Q OR ESC QUIT",2,GREEN);
+};
+// a..g bits: top, upper-right, lower-right, bottom, lower-left, upper-left,
+// middle. Faint unlit bars retain the old LED figure-eight silhouette.
+val _digit_noInline = { !sys.draw.Canvas c, int x, int y, i64 digit ->
+ int bits=digit==0 ? 0x3f : digit==1 ? 0x06 : digit==2 ? 0x5b :
+ digit==3 ? 0x4f : digit==4 ? 0x66 : digit==5 ? 0x6d :
+ digit==6 ? 0x7d : digit==7 ? 0x07 : digit==8 ? 0x7f : digit==9 ? 0x6f : 0;
+ for( int i=0; i<7; i++ ) {
+ u32 color=(bits&(1<<i))!=0 ? GREEN : DIM;
+ if( i==0 || i==3 || i==6 ) {
+ int dy=i==0 ? 0 : i==3 ? 144 : 72;
+ c.line(x+14,y+dy,x+58,y+dy,10,color);
+ } else {
+ bool right=i==1 || i==2;
+ int dx=right ? 68 : 4, dy=i==1 || i==5 ? 12 : 84;
+ c.line(x+dx,y+dy,x+dx,y+dy+48,10,color);
+ }
+ }
+};
+val _digital_noInline = { !sys.draw.Canvas c, int hour, int minute, int second ->
+ _digit_noInline(c,68,166,hour/10); _digit_noInline(c,154,166,hour-hour/10*10);
+ _digit_noInline(c,284,166,minute/10); _digit_noInline(c,370,166,minute-minute/10*10);
+ _digit_noInline(c,500,166,second/10); _digit_noInline(c,586,166,second-second/10*10);
+ for( int i=0; i<2; i++ ) {
+ c.fillCircle(254+i*216,214,6,GREEN);
+ c.fillCircle(254+i*216,262,6,GREEN);
+ }
+ c.text(272,352,"LOCAL TIME 24H",3,GREEN);
+};
+// Rational sine approximation, accurate enough for pixel clock geometry.
+// Arguments are nonnegative turns; no additional native math dependency.
+val _sin_noInline = { f64 turn ->
+ i64 whole=turn; turn-=whole;
+ bool negative=turn>0.5;
+ f64 x=(negative ? turn-0.5 : turn)*6.283185307179586;
+ f64 product=x*(3.141592653589793-x);
+ f64 numerator=negative ? -16.0 : 16.0;
+ return numerator*product/(49.34802200544679-4*product);
+};
+val _hand_noInline = { !sys.draw.Canvas c, f64 turn, int length, int thickness, u32 color ->
+ int x=360+_sin_noInline(turn)*length, y=232-_sin_noInline(turn+0.25)*length;
+ c.line(360,232,x,y,thickness,color);
+};
+val _analog_noInline = { !sys.draw.Canvas c, int hour, int minute, int second, int millisecond ->
+ c.circle(360,232,154,3,GREEN);
+ for( int i=0; i<60; i++ ) {
+ f64 turn=i/60.0;
+ bool major=i-i/5*5==0;
+ int inner=major ? 133 : 143;
+ int x0=360+_sin_noInline(turn)*inner, y0=232-_sin_noInline(turn+0.25)*inner;
+ int x1=360+_sin_noInline(turn)*149, y1=232-_sin_noInline(turn+0.25)*149;
+ c.line(x0,y0,x1,y1,major ? 3 : 1,GREEN);
+ }
+ c.text(349,100,"12",2,GREEN); c.text(484,227,"3",2,GREEN);
+ c.text(357,354,"6",2,GREEN); c.text(230,227,"9",2,GREEN);
+ f64 seconds=second+millisecond/1000.0;
+ f64 minutes=minute+seconds/60.0;
+ _hand_noInline(c,(hour-hour/12*12+minutes/60.0)/12.0,78,8,GREEN);
+ _hand_noInline(c,minutes/60.0,112,5,GREEN);
+ _hand_noInline(c,seconds/60.0,128,2,0xc3ffcb);
+ c.fillCircle(360,232,6,GREEN);
+};
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..46965ac0
--- /dev/null
+++ b/docs/chapter31/examples/README.md
@@ -0,0 +1,84 @@
+# 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.
+
+The [digital and analog clock](Clock.smp) demonstrates the [time library](../time.md),
+green seven-segment digits, smooth clock hands, and a reusable software button.
+Run `make run-clock CTAGS=`; click the button or press Space to switch faces,
+and Q/Escape to exit. Both faces show system-local time.
+`make clock-example CTAGS=` builds it without starting a window.
+
+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..fddcf254
--- /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.wait(17);
+ 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..4977b490
--- /dev/null
+++ b/docs/chapter31/graphics.md
@@ -0,0 +1,132 @@
+# 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 an available 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
+```
+
+With no `SIMPLE_MINIFB` setting, the runtime searches beside the executable,
+then its `../../release/<ABI>` and `../release/<ABI>` directories. These cover
+the examples and test build layouts regardless of the current directory.
+It also checks the current directory, `build/release/<ABI>`, and
+`chapter31/build/release/<ABI>` relative to the current directory. Thus built
+examples such as `Clock.exe` can be launched directly without configuration.
+
+`SIMPLE_MINIFB` overrides discovery; an invalid explicit path reports an error
+without falling back. `SIMPLE_MINIFB=off` disables graphics for headless tests.
+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 up to 17ms, waking for input. |
+| `wait(timeoutMs)` | Wait for input or timeout, then process events; 0 polls, -1 waits indefinitely. |
+| `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`, `poll`, and `wait` 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.
+
+Waits may return early; values below -1 are errors. Recheck the monotonic
+clock after waking. The [time library and animation example](time.md) combine
+this wait with cooperative timer deadlines. Backend API version 2 adds the
+wait operation; rebuild older MiniFB adapters from the cached sources.
+
+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/docs/chapter31/time.md b/docs/chapter31/time.md
new file mode 100644
index 00000000..04865626
--- /dev/null
+++ b/docs/chapter31/time.md
@@ -0,0 +1,119 @@
+# Time and single-threaded animation
+
+`sys.time` uses bare `i64` values: absolute instants are milliseconds since
+the Unix epoch in UTC; durations are milliseconds. Monotonic readings belong
+to a separate clock domain. Never subtract a wall-clock reading from a
+monotonic deadline.
+
+## Clocks and waits
+
+| Call | Result |
+| --- | --- |
+| `time.now()` | Unix-epoch milliseconds; may jump when the system clock changes. |
+| `time.monotonic()` | Monotonic milliseconds for elapsed time and deadlines. |
+| `time.nanoTime()` | Monotonic nanoseconds, with the clock's available resolution. |
+| `time.sleep(ms)` | Sleep for a nonnegative duration. |
+| `time.sleepUntil(deadline)` | Wait for a monotonic millisecond deadline; returns immediately if past. |
+| `time.remaining(deadline, current)` | Nonnegative difference, saturating on overflow. |
+
+Clock errors return `-1`; sleeps return zero on success or `-1` on error,
+with `sys.libc.errno()` available. Interrupted sleeps resume rather than
+returning early. Wakeup times are approximate, and these calls do not wake a
+suspended laptop. Sleeping blocks the one Simple execution thread; use
+`gfx.wait` when window input must remain responsive.
+
+`seconds(n)`, `minutes(n)`, `hours(n)`, and `days(n)` convert signed counts
+to milliseconds. `toSeconds(ms)`, `toMinutes(ms)`, `toHours(ms)`, and
+`toDays(ms)` truncate toward zero. A duration day is exactly 24 hours,
+independent of daylight saving. `toNanos(ms)` and `fromNanos(ns)` convert
+between millisecond and nanosecond units. Multiplication overflow returns
+`-1`. Constants `SECOND`, `MINUTE`, `HOUR`, and `DAY` are also available.
+
+## String interpolation
+
+Time formatting is built into the existing string interpolation syntax:
+
+| Format | Example | Named style |
+| --- | --- | --- |
+| `%t(instant)` or `%tF(instant)` | `2026-11-12T00:34:05.123Z` | `time.ISO` |
+| `%tr(instant)` | `12:34am` | `time.CLOCK12` |
+| `%tR(instant)` | `00:34` | `time.CLOCK24` |
+| `%tT(instant)` | `00:34:05` | `time.CLOCK24_SECONDS` |
+| `%tD(instant)` | `12/11/26` | `time.DATE` |
+| `%tM(duration)` | `02:03.456` | `time.ELAPSED` |
+
+For example, `"UTC %tR(sys.time.now())"`. Width and left alignment work as
+for strings: `%30t(instant)` and `%-30t(instant)`. Precision and numeric flags
+are rejected. `%tM` formats a duration, with total minutes rather than wrapping
+at an hour; negative durations keep their sign.
+
+Interpolation uses UTC, making its output stable across machines.
+`time.format(instant, style)` is the equivalent library call;
+`time.formatLocal(instant, style)` uses the system's local zone. Local ISO
+output includes a numeric offset instead of `Z` when nonzero, retaining
+offset seconds for historical zones. Invalid calendar values/styles produce
+`<invalid time>`. The ISO form retains all millisecond bits within the supported
+calendar range; short formats intentionally omit information.
+
+## Calendar values and absolute parsing
+
+`time.utc(instant)` and `time.local(instant)` return a `DateTime` with fields
+`year`, `month`, `day`, `hour`, `minute`, `second`, `millisecond`, `weekday`
+(Sunday = 0), `offsetSeconds` (east of UTC), and `daylight`. A zero year means
+conversion failed. `time.zoneName(instant)` returns the local abbreviation or
+an empty string on error. Local conversion uses the operating system's zone
+rules, including DST at that instant; no time-zone database is downloaded.
+
+`time.makeUTC(year, month, day, hour, minute, second, millisecond)` validates
+and constructs an instant, returning `-1` on invalid fields. `time.valid(...)`,
+`time.leap(year)`, and `time.monthDays(year, month)` support calendar checks.
+Calendar conversion supports Gregorian years 1 through 9999, bounded by
+`time.MIN` and `time.MAX`, and rejects leap seconds.
+
+`time.parse(text)` accepts strict absolute ISO text:
+`YYYY-MM-DDTHH:mm:ss[.SSS]Z` or the same with an explicit `+HH:MM`/`-HH:MM`
+offset (optionally with `:SS`). Fractions have exactly three digits. No relative
+time parsing, implicit local zone, locale-dependent parsing, or named-zone
+conversion is provided. Offsets must have hours 00–23 and minutes/seconds 00–59.
+Invalid text or an out-of-range result returns `-1`. By design, this sentinel
+is indistinguishable from the valid instant `1969-12-31T23:59:59.999Z`;
+formatting and calendar conversion still support that instant.
+
+## Cooperative timers
+
+Create `val timers = new sys.time.Timers();` on the game thread:
+
+| Operation | Meaning |
+| --- | --- |
+| `after(delay)` | Register a one-shot timer; return its positive ID, or `-1`. |
+| `every(period)` | Register a repeating timer; period must be positive. |
+| `afterAt(current, delay)`, `everyAt(current, period)` | Supply a monotonic reading explicitly, useful for deterministic tests. |
+| `cancel(id)` | Remove a timer; return whether it was present. |
+| `nextDeadline()` | Earliest monotonic deadline, or `-1` if empty. |
+| `poll(current)` | Return the next due ID, or `-1` if none. |
+
+Poll repeatedly to drain due timers. Deadlines order delivery, then registration
+ID breaks ties. Repeating timers advance from their previous deadline, skipping
+missed ticks to the first future deadline rather than drifting or replaying a
+backlog. Deadline overflow rejects registration or cancels a repeating timer.
+There are no callbacks or timer threads: the caller handles each returned ID.
+
+`gfx.wait(timeoutMs)` processes events after waiting for input or a timeout:
+zero polls, `-1` waits indefinitely, values below `-1` fail. It returns the same
+`OK`, `CLOSED`, or `ERROR` statuses as `gfx.poll`, and may wake early. Re-read the
+monotonic clock after each wake. This allows one thread to handle timers,
+input, simulation, and drawing without busy waiting.
+
+The [digital and analog clock example](examples/Clock.smp) combines these pieces. From
+the repository root, with graphics dependencies already cached:
+
+```sh
+make -C chapter31 clock-example CTAGS= # Build without opening a window.
+make -C chapter31 run-clock CTAGS= # Build the backend and run.
+```
+
+Click the button or press Space to switch between green seven-segment digits
+and an analog dial with smoothly moving hands. Both display system-local time;
+Q/Escape exits. Passing any argument directly to the executable closes it after
+ten frames, switching faces halfway through. Hands are computed from the current
+time, so delayed frames do not create a simulation catch-up backlog.
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 0a6e065d..d490ecfa 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -32,7 +32,6 @@ design decisions and chapter boundaries.
- [Partial-initialization contracts](#inferred-partial-initialization-contracts--deferred) — inferred requirements, nested guarantees, and imported contracts.
- [Automatic memory management](#automatic-memory-management) — ownership, sharing, cycles, and native retention.
- [A graphical game](#a-small-graphical-game) — exercise graphics, keyboard, and audio APIs.
- - [Time and animation](#time-timers-and-animation) — scoped follow-on chapter with clocks, calendar values, cooperative timers, and responsive waits.
- [Trailing arrays](#structs-with-trailing-arrays) — inline elements after named fields.
- [Array initialization](#array-initialization) — generate each element during allocation.
- [String libraries](#string-libraries) — ownership, Unicode, and additional representations beyond byte buffers.
@@ -379,89 +378,8 @@ Optional [background audio](chapter31/docs/audio.md) uses miniaudio for
soft move sounds and ascending/descending win/loss cues. Further games remain
future work; MiniFB supplies only the window and input boundary.
-### Time timers and animation
-
-Scoped on 2026-11-12 as a proposed Chapter 32 following Chapter 31's drawing,
-graphics, and audio work. Save this scope while closing Chapter 31; the time
-interface and animation work are not implemented yet. This is primarily a
-standard-library/runtime chapter, with no anticipated compiler changes or new
-external dependencies. Keep the game loop and timer dispatch single-threaded.
-
-Default instants are bare `i64` milliseconds since Unix epoch, UTC. Durations
-are also bare `i64` milliseconds. Distinguish wall-clock readings from monotonic
-readings by operation names and documentation: animation and timer deadlines
-must not depend on system-clock corrections.
-
-Proposed `sys.time` core:
-
-| Operation | Meaning |
-| --- | --- |
-| `now()` | UTC Unix-epoch milliseconds. |
-| `monotonic()` | Milliseconds from an unspecified monotonic origin, for deadlines and elapsed time. |
-| `nanoTime()` | Monotonic nanoseconds; units do not promise nanosecond clock resolution. |
-| `sleep(ms)` | Suspend the calling thread for a duration. |
-| `sleepUntil(deadline)` | Wait until a monotonic millisecond deadline. |
-| `seconds(n)`, `minutes(n)`, `hours(n)`, `days(n)` | Convert durations to milliseconds; a day here is exactly 24 hours. |
-| `toSeconds(ms)`, `toMinutes(ms)`, etc. | Convert back with explicitly documented rounding. |
-
-Cygwin's existing runtime headers expose `clock_gettime`, `clock_nanosleep`,
-and `localtime_r`; investigate sharing the native clock/calendar bridge with
-Linux. Native Windows interval measurement can use QueryPerformanceCounter.
-Epoch nanoseconds are optional: signed `i64` would cover only about 292 years
-on either side of 1970, so retain milliseconds as the default representation.
-Sleep deadlines are approximate; handle interruptions and avoid busy waiting.
-
-Initial calendar/text support:
-
-- Format UTC as `2026-11-12T15:04:05.123Z`; parse that form and explicit numeric
- UTC offsets such as `-08:00`.
-- Break instants into UTC or system-local calendar fields. An auxiliary
- `DateTime` structure can contain year, month, day, hour, minute, second,
- millisecond, weekday, UTC offset, daylight-saving status, and zone abbreviation.
- The underlying instant remains a bare `i64`.
-- Construct instants from UTC fields or calendar fields with explicit offsets.
-- Query the local UTC offset, DST status, and zone abbreviation at a supplied
- instant, using the operating system's existing time-zone machinery.
-- Defer arbitrary named-zone conversion, bundled time-zone databases,
- natural-language parsing, and locale-specific formatting. Parsing local
- calendar times without an offset needs a policy for ambiguous/nonexistent
- times around daylight-saving transitions.
-
-Keep arithmetic, parsing, formatting, and timer scheduling in Simple where
-practical. Limit native code to clock readings, OS waits, and local-zone
-conversion. Validate overflow, negative epoch division/remainders, leap days,
-invalid dates, and supported calendar ranges. A parse error cannot use `-1`
-as a sentinel because that is a valid instant. Fixed-duration days and calendar
-day arithmetic are separate operations.
-
-Animation needs an input-aware wait in addition to ordinary sleep. Replace
-the graphics adapter's fixed 17ms delay with a proposed `gfx.wait(timeoutMs)`
-that processes events and wakes for either input or timeout. The game loop
-waits until the earlier of its next frame and next timer, then handles input,
-due timers, simulation, and drawing on the same thread. Windows has a
-message-aware timed wait; X11 needs corresponding backend integration.
-
-A small cooperative timer queue in Simple can expose `after(delay)` and
-`every(period)` returning timer IDs, plus `cancel(id)`, `nextDeadline()`, and
-`poll(now)` returning due IDs. Returning IDs avoids requiring closures or
-background callbacks. Advance repeating deadlines from their previous deadline
-to avoid drift. Define a missed-tick policy and bound animation catch-up;
-reset or clamp frame deltas after laptop sleep rather than replaying hours of
-frames. Explicitly exclude waking the computer from system suspend.
-
-Use an animated game example to drive the chapter. Planning estimate:
-roughly 1,000-2,000 implementation lines, plus tests and documentation. Clock
-readings, sleep, and unit conversion are small; calendar/text/zone support,
-cooperative scheduling, responsive graphics waiting, and an animated example
-make the complete scope medium-sized. Test timer ordering, cancellation,
-repetition, and clock jumps with injected/fake readings; keep actual sleep tests
-broadly bounded and hardware/display checks optional.
-
-Platform references: [clock_gettime](https://man7.org/linux/man-pages/man2/clock_gettime.2.html),
-[clock_nanosleep](https://man7.org/linux/man-pages/man2/clock_nanosleep.2.html),
-[calendar conversion](https://man7.org/linux/man-pages/man3/localtime.3.html),
-[Windows clock guidance](https://learn.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps),
-and [message-aware waiting](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-msgwaitformultipleobjectsex).
+Chapter 31 also implements [time and single-threaded animation](chapter31/docs/time.md),
+including cooperative timers and responsive input waits.
### Structs with trailing arrays
diff --git a/graphics.mk b/graphics.mk
new file mode 100644
index 00000000..5b0bce09
--- /dev/null
+++ b/graphics.mk
@@ -0,0 +1,70 @@
+# 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,)
+CLOCK_EXAMPLE := $(GAME_DIR)/Clock$(if $(filter Windows_NT,$(OS)),.exe,)
+
+.PHONY: clock-example run-clock
+clock-example: $(CLOCK_EXAMPLE)
+$(CLOCK_EXAMPLE): docs/examples/Clock.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)/Clock.o $<
+run-clock: graphics clock-example
+ SIMPLE_MINIFB="$(abspath $(GRAPHICS_LIB))" $(CLOCK_EXAMPLE)
+
+.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/skills/chapter25-codex-notes.md b/skills/chapter25-codex-notes.md
index e0fa3dc5..45120c4c 100644
--- a/skills/chapter25-codex-notes.md
+++ b/skills/chapter25-codex-notes.md
@@ -66,6 +66,30 @@ Chapter 30 retains the preceding compiler and library. Keep inherited Java test
use LF for new text files, and do not add Python code or test drivers.
Runnable examples belong in `chapter31/docs/examples`.
+MiniFB discovery defaults to executable-relative and obvious chapter build
+locations; examples should run directly without environment setup. Explicit
+`SIMPLE_MINIFB` paths override discovery and `off` keeps headless tests stable.
+
+The Clock example now shows digital/analog local-time clocks, switched by the
+software `sys.draw.Button` widget or Space. Button input is caller-fed, fires
+on release inside after a press inside, and has no native/window dependency.
+
+Clock rendering needs native regression coverage as well as Eval2: evaluator
+checks cannot catch instruction encoding or relocation errors. X86 floating
+comparisons use UCOMISD's CF/ZF/PF, preserve unordered behavior when layout
+negates a branch, and must not use signed SF/OF conditions. Encoding metadata
+for relocation sites uses instruction identity: scheduling/RA can clone equal
+constant loads, each of which still requires its own relocation.
+
+Time and animation now belong in Chapter 31, not a follow-on chapter proposal.
+See `chapter31/docs/time.md`: bare i64 epoch milliseconds, distinct monotonic
+deadlines, optional monotonic nanoseconds, OS local zones, Simple calendar/text
+and cooperative timer IDs, and input-aware `gfx.wait`. Cliff explicitly accepts
+`-1` for absolute parse errors despite its collision with the instant just before
+the epoch; relative time parsing is deferred. Built-in `%t` selectors cover ISO,
+12/24-hour clocks, elapsed minutes, calendar dates, and clock seconds. UTC is
+the interpolation default; local-zone formatting is an explicit library call.
+
## Chapter 30 starting point (2026-11-07; superseded by Chapter 31)
Cliff committed the Chapter 29 work as `bb9b30b2c` and requested merging
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..40d66ba9
--- /dev/null
+++ b/src/main/c/simple_graphics.c
@@ -0,0 +1,102 @@
+#define _POSIX_C_SOURCE 200809L
+#include "simple_graphics.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <dlfcn.h>
+#include <unistd.h>
+
+#ifdef __CYGWIN__
+#define BACKEND_NAME "simple_minifb.dll"
+#define BACKEND_ABI "x86_64_v2_win64"
+#else
+#define BACKEND_NAME "libsimple_minifb.so"
+#define BACKEND_ABI "x86_64_v2_SystemV"
+#endif
+
+// Executable-relative paths work even when launched outside the checkout.
+// Working-directory paths also cover test executables and chapter/root shells.
+static void *find_backend(void) {
+ char executable[4096], path[8192];
+ ssize_t n=readlink("/proc/self/exe",executable,sizeof(executable)-1);
+ if (n>0 && n<(ssize_t)sizeof(executable)-1) {
+ executable[n]=0;
+ char *slash=strrchr(executable,'/');
+ if (slash) {
+ *slash=0;
+ const char *relative[]={"/" BACKEND_NAME,
+ "/../../release/" BACKEND_ABI "/" BACKEND_NAME,
+ "/../release/" BACKEND_ABI "/" BACKEND_NAME};
+ for (unsigned i=0;i<sizeof(relative)/sizeof(*relative);++i) {
+ snprintf(path,sizeof(path),"%s%s",executable,relative[i]);
+ void *handle=dlopen(path,RTLD_NOW | RTLD_LOCAL);
+ if (handle) return handle;
+ }
+ }
+ }
+ const char *relative[]={"./" BACKEND_NAME,
+ "./build/release/" BACKEND_ABI "/" BACKEND_NAME,
+ "./chapter31/build/release/" BACKEND_ABI "/" BACKEND_NAME};
+ for (unsigned i=0;i<sizeof(relative)/sizeof(*relative);++i) {
+ void *handle=dlopen(relative[i],RTLD_NOW | RTLD_LOCAL);
+ if (handle) return handle;
+ }
+ return NULL;
+}
+
+// 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;
+ int explicit_path=path && *path;
+ library = explicit_path ? dlopen(path, RTLD_NOW | RTLD_LOCAL) : find_backend();
+ if (!library) {
+ if (explicit_path)
+ fprintf(stderr, "Simple graphics: cannot load MiniFB backend '%s': %s\n", path, dlerror());
+ else
+ fprintf(stderr, "Simple graphics: MiniFB backend not found beside the executable or in build/release/"
+ BACKEND_ABI ". Run make graphics, or set SIMPLE_MINIFB to its full path.\n");
+ return -1;
+ }
+ if (!explicit_path) path="automatically discovered backend";
+ 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 || !candidate->wait) {
+ 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; }
+int32_t simple_gfx_wait(int64_t timeout) { return api ? api->wait(timeout) : -1; }
diff --git a/src/main/c/simple_graphics.h b/src/main/c/simple_graphics.h
new file mode 100644
index 00000000..370256be
--- /dev/null
+++ b/src/main/c/simple_graphics.h
@@ -0,0 +1,20 @@
+#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);
+ int32_t (*wait)(int64_t);
+};
+#define SIMPLE_GRAPHICS_VERSION 2
+#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..2117f0d2
--- /dev/null
+++ b/src/main/c/simple_minifb.c
@@ -0,0 +1,117 @@
+#include "simple_graphics.h"
+#include <MiniFB.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+#ifdef _WIN32
+#include <windows.h>
+#else
+#include <errno.h>
+#include <time.h>
+#include <poll.h>
+#include "WindowData.h"
+#include "x11/WindowData_X11.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 wait_events(int64_t timeout) {
+ if (!window) return 1;
+ if (timeout < -1) return -1;
+ // May return early; the game recomputes its deadlines after every wakeup.
+ int ms=timeout>INT_MAX ? INT_MAX : (int)timeout;
+#ifdef _WIN32
+ if (MsgWaitForMultipleObjectsEx(0,NULL,ms<0 ? INFINITE : (DWORD)ms,
+ QS_ALLINPUT,MWMO_INPUTAVAILABLE)==WAIT_FAILED) return -1;
+#else
+ // The pinned MiniFB X11 backend owns this connection. Check already queued
+ // Xlib events before waiting on its socket; no new event thread is needed.
+ SWindowData *data=(SWindowData *)window;
+ SWindowData_X11 *native=(SWindowData_X11 *)data->specific;
+ if (!XPending(native->display)) {
+ struct pollfd fd={ConnectionNumber(native->display),POLLIN,0};
+ if (poll(&fd,1,ms)<0 && errno!=EINTR) return -1;
+ }
+#endif
+ return status(mfb_update_events(window));
+}
+static int32_t poll_events(int32_t wait) { return wait_events(wait ? 17 : 0); }
+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, wait_events
+};
+#ifdef _WIN32
+__declspec(dllexport)
+#endif
+const struct simple_graphics_api *simple_minifb_api(void) { return &api; }
diff --git a/src/main/c/simple_time.c b/src/main/c/simple_time.c
new file mode 100644
index 00000000..2e579922
--- /dev/null
+++ b/src/main/c/simple_time.c
@@ -0,0 +1,61 @@
+#define _POSIX_C_SOURCE 200809L
+#include <time.h>
+#include <stdint.h>
+#include <errno.h>
+
+// Fixed-width bridge; no native struct layout or retained Simple pointers.
+static int64_t clock_value(clockid_t clock, int64_t scale) {
+ struct timespec t;
+ if (clock_gettime(clock,&t)) return -1;
+ if (t.tv_sec>INT64_MAX/scale || t.tv_sec<INT64_MIN/scale) {
+ errno=EOVERFLOW; return -1;
+ }
+ int64_t base=(int64_t)t.tv_sec*scale;
+ int64_t fraction=t.tv_nsec/(1000000000/scale);
+ if (base>INT64_MAX-fraction) { errno=EOVERFLOW; return -1; }
+ return base+fraction;
+}
+int64_t simple_time_now(void) { return clock_value(CLOCK_REALTIME,1000); }
+int64_t simple_time_mono(void) { return clock_value(CLOCK_MONOTONIC,1000); }
+int64_t simple_time_nano(void) { return clock_value(CLOCK_MONOTONIC,1000000000); }
+int32_t simple_time_sleep(int64_t ms, int32_t absolute) {
+ if (ms<0) { errno=EINVAL; return -1; }
+ struct timespec t={ms/1000,(ms%1000)*1000000};
+ int result;
+ if (!absolute) {
+ // Retry the remaining duration after signals; no busy waiting.
+ do { result=nanosleep(&t,&t); } while (result && errno==EINTR);
+ return result;
+ }
+ do { result=clock_nanosleep(CLOCK_MONOTONIC,TIMER_ABSTIME,&t,NULL); } while (result==EINTR);
+ if (result) { errno=result; return -1; }
+ return 0;
+}
+static int leap(int y) { return y%4==0 && (y%100!=0 || y%400==0); }
+static int local_fields(int64_t ms, struct tm *local, struct tm *utc) {
+ tzset();
+ int64_t sec=ms/1000;
+ if (ms%1000<0) --sec;
+ time_t t=(time_t)sec;
+ if ((int64_t)t!=sec || !localtime_r(&t,local) || !gmtime_r(&t,utc)) return -1;
+ if (local->tm_year < -1899 || local->tm_year > 8099) { errno=EOVERFLOW; return -1; }
+ return 0;
+}
+int32_t simple_time_local(int64_t ms, int64_t *out) {
+ struct tm t,u;
+ if (local_fields(ms,&t,&u)) return -1;
+ int days=t.tm_yday-u.tm_yday;
+ if (t.tm_year>u.tm_year) days+=365+leap(u.tm_year+1900);
+ if (t.tm_year<u.tm_year) days-=365+leap(t.tm_year+1900);
+ int offset=days*86400+(t.tm_hour-u.tm_hour)*3600+(t.tm_min-u.tm_min)*60+t.tm_sec-u.tm_sec;
+ out[0]=t.tm_year+1900; out[1]=t.tm_mon+1; out[2]=t.tm_mday;
+ out[3]=t.tm_hour; out[4]=t.tm_min; out[5]=t.tm_sec;
+ out[6]=(ms%1000+1000)%1000; out[7]=t.tm_wday;
+ out[8]=offset; out[9]=t.tm_isdst>0;
+ return 0;
+}
+int32_t simple_time_zone(int64_t ms, char *out, uint32_t capacity) {
+ struct tm t,u;
+ if (!capacity || local_fields(ms,&t,&u)) return -1;
+ return (int32_t)strftime(out,capacity,"%Z",&t);
+}
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/Encoding.java b/src/main/java/com/seaofnodes/simple/codegen/Encoding.java
index b07a8fec..eb62c36a 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/Encoding.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/Encoding.java
@@ -39,7 +39,9 @@ public class Encoding implements com.seaofnodes.isa.CodeSink {
public final BAOS _bits = new BAOS(); // Instructions / encodings
public final BAOS _cpool = new BAOS(); // Constant r/o pool
public final BAOS _sdata = new BAOS(); // Static r/w pool
- public final HashMap<Node,Relo> _bigCons = new HashMap<>();
+ // Relocations belong to instruction occurrences, including equal constant
+ // loads cloned by scheduling/register allocation. Structural keys lose sites.
+ public final IdentityHashMap<Node,Relo> _bigCons = new IdentityHashMap<>();
public final StaticData _data;
public final HashMap<String,Integer> _externalAddresses = new HashMap<>();
@@ -157,7 +159,7 @@ public class Encoding implements com.seaofnodes.isa.CodeSink {
// Nodes need "relocation" patching; things done after code is placed.
// Record src and dst Nodes.
- private final HashMap<Node,CFGNode> _internals = new HashMap<>();
+ private final IdentityHashMap<Node,CFGNode> _internals = new IdentityHashMap<>();
// Source is a Call, destination in the Fun.
public Encoding relo( CallNode call ) {
FunNode fun = _code.link(call.tfp());
@@ -187,13 +189,13 @@ public class Encoding implements com.seaofnodes.isa.CodeSink {
}
// External references only located by Strings a link-time
- public final HashMap<Node,String> _externals = new HashMap<>();
+ public final IdentityHashMap<Node,String> _externals = new IdentityHashMap<>();
public Encoding external( Node n, String extern ) {
_externals.put(n,extern);
return this;
}
- public final HashMap<Node,String> _externData = new HashMap<>();
+ public final IdentityHashMap<Node,String> _externData = new IdentityHashMap<>();
public void externalData(Node n, String name) { _externData.put(n,name); }
// Store t as a 32/64 bit constant in the code space; generate RIP-relative
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/MemMergeNode.java b/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java
index aa3f6d57..19420839 100644
--- a/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java
@@ -20,8 +20,20 @@ public class MemMergeNode extends Node {
public MemMergeNode( Node ...nodes) { super(nodes); _type = TypeMem.BOT; }
public MemMergeNode(MemMergeNode mem) { super(mem); }
@Override public Tag serialTag() { return Tag.MemMerge; }
- public void packed( BAOS baos, HashMap<String,Integer> strs, HashMap<Type,Integer> types, IdentityHashMap<Node, Integer> anodes ) { baos.packed1(nIns()); }
- static Node make( BAOS bais ) { Node mem = new MemMergeNode(); mem.setDefX(bais.packed1()-1,null); return mem; }
+ public void packed( BAOS baos, HashMap<String,Integer> strs, HashMap<Type,Integer> types, IdentityHashMap<Node, Integer> anodes ) {
+ // A memory merge always has inputs. Zero escapes a wide alias table,
+ // preserving every existing nonzero single-byte count.
+ assert nIns()>0;
+ baos.packed1(nIns()<=255 ? nIns() : 0);
+ if( nIns()>255 ) baos.packed4(nIns());
+ }
+ static Node make( BAOS bais ) {
+ int count=bais.packed1();
+ if( count==0 ) count=bais.packed4();
+ Node mem=new MemMergeNode();
+ mem.setDefX(count-1,null);
+ return mem;
+ }
@Override public String label() { return "ALLMEM"; }
@Override public boolean isMem() { return true; }
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java
index 10cee059..ae725659 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java
@@ -9,6 +9,7 @@ import com.seaofnodes.simple.util.Utils;
// Jump on flags, uses flags
public class JmpX86 extends IfNode implements MachNode, RIPRelSize {
String _bop;
+ boolean _unsigned, _floating, _unorderedTaken;
JmpX86( IfNode iff, String bop ) {
super(iff);
_bop = bop;
@@ -23,29 +24,47 @@ public class JmpX86 extends IfNode implements MachNode, RIPRelSize {
if( set instanceof SetX86 setx && (cmp instanceof CmpX86 || cmp instanceof CmpIX86 || cmp instanceof CmpMemX86 || cmp instanceof CmpFX86) ) {
_inputs.set( 1, cmp );
_bop = setx._bop;
+ _unsigned = setx._unsigned;
+ _floating = cmp instanceof CmpFX86;
+ _unorderedTaken = _bop.equals("!=");
} else
throw Utils.TODO();
}
@Override public RegMask regmap(int i) { assert i==1; return x86_64_v2.FLAGS_MASK; }
@Override public RegMask outregmap() { return null; }
- @Override public void negate() { _bop = negate(_bop); }
+ @Override public void negate() {
+ _bop = negate(_bop);
+ if( _floating ) _unorderedTaken = !_unorderedTaken;
+ }
+
+ private int condition() { return x86_64_v2.jumpop(_bop,_unsigned)&15; }
@Override public void encoding( Encoding enc ) {
if( in(1)==null && _bop=="!=" ) return;
- X86.branch(enc,in(1)==null ? -1 : x86_64_v2.jumpop(_bop)&15);
+ // UCOMISD sets CF/ZF/PF, not signed-integer SF/OF. Handle unordered
+ // separately, including when layout has negated the original test.
+ if( _floating ) {
+ if( _unorderedTaken ) enc.add1(0x0F).add1(0x8A).add4(0); // JP target
+ else enc.add1(0x7A).add1(0); // JP past the ordered jump
+ }
+ X86.branch(enc,in(1)==null ? -1 : condition());
enc.jump(this,cproj(0));
}
// Delta is from opcode start, but X86 measures from the end of the 2-byte encoding
@Override public byte encSize(int delta) {
if( in(1)==null && _bop=="!=" ) return 0;
- return X86.branchSize(delta,in(1)!=null);
+ int prefix = !_floating ? 0 : _unorderedTaken ? 6 : 2;
+ return (byte)(prefix + X86.branchSize(delta-prefix,in(1)!=null));
}
// Delta is from opcode start
@Override public void patch( Encoding enc, int opStart, int opLen, int delta ) {
assert !(in(1)==null && _bop=="!=");
- X86.patchBranch(enc.bits(),opStart,opLen,delta,in(1)==null ? -1 : x86_64_v2.jumpop(_bop)&15);
+ int prefix = !_floating ? 0 : _unorderedTaken ? 6 : 2;
+ if( _floating )
+ X86.patchBranch(enc.bits(),opStart,prefix,_unorderedTaken ? delta : opLen,10);
+ X86.patchBranch(enc.bits(),opStart+prefix,opLen-prefix,delta-prefix,in(1)==null ? -1 : condition());
}
@Override public void asm(CodeGen code, SB sb) {
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java
index 86610f6d..4494985d 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java
@@ -9,7 +9,7 @@ public class NegX86 extends MachConcreteNode implements MachNode {
NegX86(MinusNode not) { super(not); }
@Override public String op() { return "neg"; }
@Override public RegMask regmap(int i) { return x86_64_v2.RMASK; }
- @Override public RegMask outregmap() { return x86_64_v2.RMASK; }
+ @Override public RegMask outregmap() { return x86_64_v2.WMASK; }
@Override public RegMask killmap() { return x86_64_v2.FLAGS_MASK; }
@Override public int twoAddress() { return 1; }
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java
index 1e4e7c26..1291cf78 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java
@@ -9,7 +9,7 @@ public class NotX86 extends MachConcreteNode implements MachNode {
NotX86(NotNode not) { super(not); }
@Override public String op() { return "not"; }
@Override public RegMask regmap(int i) { return x86_64_v2.RMASK; }
- @Override public RegMask outregmap() { return x86_64_v2.RMASK; }
+ @Override public RegMask outregmap() { return x86_64_v2.WMASK; }
@Override public RegMask killmap() { return x86_64_v2.FLAGS_MASK; }
@Override public void encoding( Encoding enc ) {
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java
index 864c1b55..bcc4880b 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java
@@ -4,7 +4,6 @@ import com.seaofnodes.isa.X86;
import com.seaofnodes.simple.codegen.*;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.util.SB;
-import com.seaofnodes.simple.util.Utils;
// Corresponds to the x86 instruction "sete && setne".
// Use result of comparison without jump.
@@ -25,15 +24,14 @@ public class SetX86 extends MachConcreteNode implements MachNode {
@Override public RegMask outregmap() { return x86_64_v2.WMASK; }
@Override public void encoding( Encoding enc ) {
- int op=switch (_bop) {
- case "==" -> 0x94; // SETE
- case "<" -> _unsigned ? 0x92 : 0x9C; // SETB /SETL
- case "<=" -> _unsigned ? 0x96 : 0x9E; // SETBE/SETLE
- case ">=" -> _unsigned ? 0x93 : 0x9D; // SETAE/SETGE
- case ">" -> _unsigned ? 0x97 : 0x9F; // SETA /SETG
- default -> throw Utils.TODO();
- };
- X86.set(enc,op&15,enc.reg(this));
+ int dst=enc.reg(this);
+ if( in(1) instanceof CmpFX86 ) {
+ // MOV preserves comparison flags; unordered is true only for !=.
+ X86.unary(enc,0xC7,0,dst);
+ enc.add4(_bop.equals("!=") ? 1 : 0);
+ enc.add1(0x7A).add1(dst>=4 ? 8 : 6); // JP past SETcc/MOVZX
+ }
+ X86.set(enc,x86_64_v2.jumpop(_bop,_unsigned)&15,dst);
}
@Override public void asm(CodeGen code, SB sb) {
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..aed8ee4c 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
@@ -87,6 +87,18 @@ public class x86_64_v2 extends Machine {
default -> throw new IllegalArgumentException("Too many arguments");
};
}
+ static int jumpop(String op, boolean unsigned) {
+ if( !unsigned ) return jumpop(op);
+ return switch( op ) {
+ case "==" -> 0x84;
+ case "!=" -> 0x85;
+ case "<" -> 0x82;
+ case "<=" -> 0x86;
+ case ">" -> 0x87;
+ case ">=" -> 0x83;
+ default -> throw new IllegalArgumentException(op);
+ };
+ }
// Limit of float args passed in registers
static RegMask[] XMMS8 = new RegMask[]{
@@ -370,8 +382,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..62dd054e 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() {
@@ -381,14 +405,19 @@ public class Lexer {
precision = formatNumber(site);
}
char kind=nextChar();
- if( "diuoxXfFeEgGaAcsb".indexOf(kind)<0 )
+ if( "diuoxXfFeEgGaAcsbt".indexOf(kind)<0 )
throw Parser.error("Expected a string format: %(expression), %d(expression), or %%",site);
boolean number="diuoxXfFeEgGaA".indexOf(kind)>=0;
if( !number && (flags&~1)!=0 || "uoxX".indexOf(kind)>=0 && (flags&6)!=0 ||
"diu".indexOf(kind)>=0 && (flags&8)!=0 )
throw Parser.error("Invalid flags for %"+kind,site);
- if( precision>=0 && (kind=='c' || kind=='b') )
+ if( precision>=0 && (kind=='c' || kind=='b' || kind=='t') )
throw Parser.error("Precision is not supported for %"+kind,site);
+ if( kind=='t' && peek()!='(' ) {
+ int style="FrRMDT".indexOf(nextChar());
+ if( style<0 ) throw Parser.error("Expected a time format: %t, %tF, %tr, %tR, %tM, %tD, or %tT",site);
+ flags |= style<<8;
+ }
if( peek()!='(' ) throw Parser.error("Expected '(' after string format %"+kind,site);
inc();
return new Format(kind,flags,width,precision,site);
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/Strings.java b/src/main/java/com/seaofnodes/simple/parser/Strings.java
index 5bf76585..be913371 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Strings.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Strings.java
@@ -32,7 +32,7 @@ final class Strings {
Node strings=P.references.member(text,"string",fmt.site()).keep();
fptr=new FormatNode(value,ints.unkeep(),floats.unkeep(),strings.unkeep(),fmt.site()).peephole();
} else {
- String family="diuoxXcb".indexOf(fmt.kind())>=0 ? "integer" : fmt.kind()=='s' ? "string" : "floating";
+ String family="diuoxXcb".indexOf(fmt.kind())>=0 ? "integer" : fmt.kind()=='s' ? "string" : fmt.kind()=='t' ? "time" : "floating";
fptr=P.references.member(text,family,fmt.site());
}
call(text,fptr,fmt.site(),builder,value.unkeep(),P.con(fmt.kind() | fmt.flags()<<8),
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/java/com/seaofnodes/simple/print/ASMPrinter.java b/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java
index e8218afd..bdd887e6 100644
--- a/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java
+++ b/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java
@@ -10,7 +10,7 @@ import com.seaofnodes.simple.type.*;
import java.util.ArrayList;
import com.seaofnodes.simple.codegen.Encoding;
import com.seaofnodes.simple.util.BAOS;
-import java.util.HashMap;
+import java.util.Map;
/** Machine-specific facts; traversal, columns and byte display live in print/. */
public final class ASMPrinter extends AssemblyAdapter<Node> {
@@ -93,7 +93,7 @@ public final class ASMPrinter extends AssemblyAdapter<Node> {
return data;
}
// Retain this diagnostic entry point for checking that pool printing never lays out types.
- static int printConstantPool(int address, SB sb, BAOS bits, HashMap<Node,Encoding.Relo> relos,
+ static int printConstantPool(int address, SB sb, BAOS bits, Map<Node,Encoding.Relo> relos,
boolean ro, String name) {
sb.p(com.seaofnodes.print.ASMPrinter.pool(new Pool(name,bits.buf(),0,address,entries(relos.values(),ro))));
return address+bits.size();
diff --git a/src/main/java/com/seaofnodes/simple/type/TypeMem.java b/src/main/java/com/seaofnodes/simple/type/TypeMem.java
index 5faa7107..1c83ddd7 100644
--- a/src/main/java/com/seaofnodes/simple/type/TypeMem.java
+++ b/src/main/java/com/seaofnodes/simple/type/TypeMem.java
@@ -211,14 +211,19 @@ public class TypeMem extends Type {
+ (!_one ? 0 : (_clz ? 4 : 8)));
if( _alias!=1 ) {
// generic alias
- assert _alias <= 255;
- baos.packed1( _alias );
+ // Zero is reserved for alias-set flags, never a memory slice.
+ // Use it as an escape for large libraries, retaining the existing
+ // single-byte encoding (including 255) for older objects.
+ assert _alias>1;
+ baos.packed1(_alias<=255 ? _alias : 0);
+ if( _alias>255 ) baos.packed4(_alias);
}
XInt.packed(baos,_escFs);
XInt.packed(baos,_escAs);
}
static TypeMem packed( int tag, BAOS bais ) {
int alias = (tag&1)==0 ? 1 : bais.packed1();
+ if( alias==0 ) alias=bais.packed4();
boolean xfinal= (tag&2)==2;
boolean one = (tag&12)!=0;
boolean clz = (tag&4)==4;
diff --git a/src/main/smp/sys.smp b/src/main/smp/sys.smp
index 1201315b..296635ae 100644
--- a/src/main/smp/sys.smp
+++ b/src/main/smp/sys.smp
@@ -2,12 +2,16 @@
// 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 time = time; // Clocks, calendar text, and cooperative timers
val libc = libc; // libc FFI bindings
val rt = rt; // Run Time support
val scan = scan; // A simple string parser
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..0e4a27d4
--- /dev/null
+++ b/src/main/smp/sys/draw.smp
@@ -0,0 +1,18 @@
+// 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
+val Button = Button; // Pointer state and rendering for a rectangular button
diff --git a/src/main/smp/sys/draw/Button.smp b/src/main/smp/sys/draw/Button.smp
new file mode 100644
index 00000000..edf01119
--- /dev/null
+++ b/src/main/smp/sys/draw/Button.smp
@@ -0,0 +1,41 @@
+// Feed pointer state once per event-loop iteration. Activate on release inside,
+// only after a press that began inside. No native/window dependency.
+struct Button {
+ int x, y, width, height;
+ u8[~] !label;
+ bool !enabled=true, !hovered=false, !pressed=false;
+ bool !_held=false, !_armed=false;
+ new Button = { int px, int py, int w, int h, u8[~] text ->
+ x=px; y=py; width=w; height=h; label=text;
+ };
+ val contains = { i64 px, i64 py ->
+ if( width<=0 || height<=0 ) return false;
+ i64 right=x, bottom=y; right+=width; bottom+=height;
+ if( px<x || py<y || px>=right || py>=bottom ) return false;
+ return true;
+ };
+ val update = { i64 px, i64 py, bool down ->
+ hovered=false;
+ if( enabled ) hovered=contains(px,py);
+ if( !enabled ) _armed=false;
+ if( down && !_held ) _armed=hovered;
+ var clicked=false;
+ if( !down ) if( _held ) if( _armed ) if( hovered ) clicked=true;
+ if( !down ) _armed=false;
+ _held=down;
+ pressed=false;
+ if( down ) if( _armed ) if( hovered ) pressed=true;
+ return clicked;
+ };
+ val draw = { !sys.draw.Canvas screen, int scale, u32 foreground, u32 background, u32 active ->
+ screen.fillRect(x,y,width,height,enabled && (hovered || pressed) ? active : background);
+ screen.rect(x,y,width,height,2,enabled ? foreground : active);
+ if( scale>0 ) {
+ i64 textWidth=label#; textWidth=(textWidth*4-1)*scale;
+ if( textWidth<=width-8 && 5*scale<=height-8 )
+ screen.text(x+(width-textWidth)/2,y+(height-5*scale)/2+(pressed ? 1 : 0),
+ label,scale,enabled ? foreground : active);
+ }
+ return self;
+ };
+};
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..cde2cc0d
--- /dev/null
+++ b/src/main/smp/sys/gfx.smp
@@ -0,0 +1,42 @@
+// 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";
+{ i64 -> i32 } simple_gfx_wait = "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 up to 17ms, waking early for input to avoid 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(); };
+// Wait for input or at most timeout milliseconds; 0 polls, -1 waits indefinitely.
+// This blocks the game thread, dispatches window events, and may wake early.
+val wait = { i64 timeout -> simple_gfx_wait(timeout); };
diff --git a/src/main/smp/sys/text.smp b/src/main/smp/sys/text.smp
index a414ff45..a28edf6d 100644
--- a/src/main/smp/sys/text.smp
+++ b/src/main/smp/sys/text.smp
@@ -96,3 +96,8 @@ val f64 = { f64 x -> end(floating(begin(),x, 0 ,0,-1)); };
val char = { i64 x -> end(integer (begin(),x,'c',0,-1)); };
val bool = { bool x -> end(integer (begin(),x,'b',0,-1)); };
val hex = { i64 x -> end(integer (begin(),x,'x',0,-1)); };
+
+// Time selectors occupy spec bits 16..23; padding shares string formatting.
+val time = { !aryu8 b, i64 value, i64 spec, i64 width, i64 precision ->
+ string(b,sys.time.format(value,(spec>>>16)&255),spec,width,-1);
+};
diff --git a/src/main/smp/sys/time.smp b/src/main/smp/sys/time.smp
new file mode 100644
index 00000000..cdcb5055
--- /dev/null
+++ b/src/main/smp/sys/time.smp
@@ -0,0 +1,191 @@
+// Instants: bare i64 UTC Unix-epoch milliseconds. Durations: i64 milliseconds.
+// Monotonic readings are a separate clock domain, for deadlines/elapsed time.
+val DateTime = DateTime;
+val Timers = Timers;
+i64 SECOND=1000, MINUTE=60000, HOUR=3600000, DAY=86400000;
+i64 MIN=-62135596800000, MAX=253402300799999; // Calendar years 1..9999.
+i64 LIMIT=9223372036854775807;
+int ISO=0, CLOCK12=1, CLOCK24=2, ELAPSED=3, DATE=4, CLOCK24_SECONDS=5;
+{ i64 } simple_time_now = "C";
+{ i64 } simple_time_mono = "C";
+{ i64 } simple_time_nano = "C";
+{ i64 i32 -> i32 } simple_time_sleep = "C";
+{ i64 i64 -> i32 } simple_time_local = "C";
+{ i64 i64 u32 -> i32 } simple_time_zone = "C";
+val now = { -> simple_time_now(); };
+val monotonic = { -> simple_time_mono(); };
+val nanoTime = { -> simple_time_nano(); };
+val sleep = { i64 ms -> simple_time_sleep(ms,0); };
+val sleepUntil = { i64 deadline -> simple_time_sleep(deadline,1); };
+
+// Signed duration conversions. Overflow returns -1; reverse conversions truncate
+// toward zero. An exact duration day is 24 hours, independent of DST.
+val _scale = { i64 n, i64 unit ->
+ if( n>LIMIT/unit || n<(-LIMIT-1)/unit ) return -1;
+ return n*unit;
+};
+val seconds = { i64 n -> _scale(n,SECOND); };
+val minutes = { i64 n -> _scale(n,MINUTE); };
+val hours = { i64 n -> _scale(n,HOUR); };
+val days = { i64 n -> _scale(n,DAY); };
+val toSeconds = { i64 ms -> ms/SECOND; };
+val toMinutes = { i64 ms -> ms/MINUTE; };
+val toHours = { i64 ms -> ms/HOUR; };
+val toDays = { i64 ms -> ms/DAY; };
+val fromNanos = { i64 ns -> ns/1000000; };
+val toNanos = { i64 ms -> _scale(ms,1000000); };
+// Clamp to zero once due, and saturate for widely separated i64 readings.
+val remaining = { i64 deadline, i64 current ->
+ if( deadline<=current ) return 0;
+ if( current<0 && deadline>LIMIT+current ) return LIMIT;
+ return deadline-current;
+};
+val _mod = { i64 n, i64 unit -> n-n/unit*unit; };
+val leap = { int year -> _mod(year,4)==0 && (_mod(year,100)!=0 || _mod(year,400)==0); };
+val monthDays = { int year, int month ->
+ if( month<1 || month>12 ) return 0;
+ return month==2 ? (leap(year) ? 29 : 28) :
+ month==4 || month==6 || month==9 || month==11 ? 30 : 31;
+};
+
+// Gregorian civil dates, expressed using 400-year eras; no per-year loops.
+val _days = { i64 year, i64 month, i64 day ->
+ year -= month<=2 ? 1 : 0;
+ i64 era=year/400, y=year-era*400;
+ i64 mp=month+(month>2 ? -3 : 9);
+ return era*146097+y*365+y/4-y/100+(153*mp+2)/5+day-1-719468;
+};
+val valid = { int year, int month, int day, int hour, int minute, int second, int millisecond ->
+ return year>=1 && year<=9999 && month>=1 && month<=12 && day>=1 && day<=monthDays(year,month) &&
+ hour>=0 && hour<=23 && minute>=0 && minute<=59 && second>=0 && second<=59 &&
+ millisecond>=0 && millisecond<=999;
+};
+val makeUTC = { int year, int month, int day, int hour, int minute, int second, int millisecond ->
+ if( !valid(year,month,day,hour,minute,second,millisecond) ) return -1;
+ return _days(year,month,day)*DAY+hour*HOUR+minute*MINUTE+second*SECOND+millisecond;
+};
+val utc = { i64 instant ->
+ val d=DateTime.empty();
+ if( instant<MIN || instant>MAX ) return d;
+ i64 day=instant/DAY, rest=_mod(instant,DAY);
+ if( rest<0 ) { rest+=DAY; day--; }
+ i64 z=day+719468, era=z/146097, doe=z-era*146097;
+ i64 y=(doe-doe/1460+doe/36524-doe/146096)/365;
+ i64 doy=doe-(365*y+y/4-y/100), mp=(5*doy+2)/153;
+ d.day=doy-(153*mp+2)/5+1;
+ d.month=mp+(mp<10 ? 3 : -9);
+ d.year=y+era*400+(d.month<=2 ? 1 : 0);
+ d.hour=rest/HOUR; d.minute=_mod(rest/MINUTE,60);
+ d.second=_mod(rest/SECOND,60); d.millisecond=_mod(rest,SECOND);
+ d.weekday=_mod(day+4,7);
+ if( d.weekday<0 ) d.weekday+=7;
+ return d;
+};
+val local = { i64 instant ->
+ val d=DateTime.empty();
+ val f=new i64[10];
+ i64 ptr=f;
+ if( simple_time_local(instant,ptr)!=0 ) return d;
+ d.year=f[0]; d.month=f[1]; d.day=f[2];
+ d.hour=f[3]; d.minute=f[4]; d.second=f[5]; d.millisecond=f[6];
+ d.weekday=f[7]; d.offsetSeconds=f[8]; d.daylight=f[9]!=0;
+ return d;
+};
+val zoneName = { i64 instant ->
+ val bytes=new u8[128];
+ i64 ptr=bytes;
+ int n=simple_time_zone(instant,ptr,bytes#);
+ if( n<0 ) return "";
+ val b=new aryu8();
+ for( int i=0; i<n; i++ ) b.add(bytes[i]);
+ return text.end(b);
+};
+
+val _digits = { !aryu8 b, i64 n, int width ->
+ text.integer(b,n,'d'|4096,width,-1);
+};
+val _format = { i64 instant, int style, bool localZone ->
+ val d=localZone ? local(instant) : utc(instant);
+ if( d.year==0 || style<0 || style>5 || style==3 ) return "<invalid time>";
+ val b=new aryu8();
+ if( style==4 ) {
+ _digits(b,d.day,2).add('/'); _digits(b,d.month,2).add('/'); _digits(b,_mod(d.year,100),2);
+ } else {
+ if( style==0 ) {
+ _digits(b,d.year,4).add('-'); _digits(b,d.month,2).add('-'); _digits(b,d.day,2).add('T');
+ }
+ int hour=d.hour;
+ if( style==1 ) { hour=_mod(hour,12); if( hour==0 ) hour=12; }
+ _digits(b,hour,style==1 ? 1 : 2).add(':'); _digits(b,d.minute,2);
+ if( style==0 || style==5 ) { b.add(':'); _digits(b,d.second,2); }
+ if( style==1 ) b.addAry(d.hour<12 ? "am" : "pm");
+ if( style==0 ) {
+ b.add('.'); _digits(b,d.millisecond,3);
+ if( d.offsetSeconds==0 ) b.add('Z');
+ else {
+ i64 offset=d.offsetSeconds;
+ b.add(offset<0 ? '-' : '+');
+ if( offset<0 ) offset=-offset;
+ _digits(b,offset/3600,2).add(':'); _digits(b,_mod(offset/60,60),2);
+ if( _mod(offset,60)!=0 ) { b.add(':'); _digits(b,_mod(offset,60),2); }
+ }
+ }
+ }
+ return text.end(b);
+};
+
+val elapsed = { i64 ms ->
+ val b=new aryu8();
+ // Nonpositive arithmetic handles the minimum i64 without negating it.
+ if( ms<0 ) b.add('-'); else ms=-ms;
+ _digits(b,-(ms/MINUTE),2).add(':');
+ _digits(b,-_mod(ms/SECOND,60),2).add('.'); _digits(b,-_mod(ms,SECOND),3);
+ return text.end(b);
+};
+val format = { i64 instant, int style ->
+ if( style==ELAPSED ) return elapsed(instant);
+ return _format(instant,style,false);
+};
+val formatLocal = { i64 instant, int style ->
+ if( style==ELAPSED ) return elapsed(instant);
+ return _format(instant,style,true);
+};
+
+val _number = { u8[~] s, i64 at, int count ->
+ if( at<0 || count<1 || at+count>s# ) return -1;
+ int n=0;
+ for( int i=0; i<count; i++ ) {
+ int c=s[at+i];
+ if( c<'0' || c>'9' ) return -1;
+ n=n*10+c-'0';
+ }
+ return n;
+};
+// Keep suffix validation separate from calendar construction.
+val _parseZone_noInline = { u8[~] s, int at, i64 instant ->
+ if( at<0 || at>=s# ) return -1;
+ i64 offset=0;
+ if( s[at]=='Z' ) { if( at+1!=s# ) return -1; }
+ else {
+ int sign=s[at]=='+' ? 1 : s[at]=='-' ? -1 : 0;
+ if( !sign || (s#!=at+6 && s#!=at+9) || s[at+3]!=':' ) return -1;
+ int oh=_number(s,at+1,2), om=_number(s,at+4,2), os=0;
+ if( s#==at+9 ) { if( s[at+6]!=':' ) return -1; os=_number(s,at+7,2); }
+ if( oh<0 || oh>23 || om<0 || om>59 || os<0 || os>59 ) return -1;
+ offset=sign*(oh*HOUR+om*MINUTE+os*SECOND);
+ }
+ instant-=offset;
+ return instant<MIN || instant>MAX ? -1 : instant;
+};
+
+// Strict ISO absolute instants only. -1 is reserved for failure (and therefore
+// cannot distinguish the valid instant one millisecond before Unix epoch).
+val parse = { u8[~] s ->
+ if( s#<20 || s[4]!='-' || s[7]!='-' || s[10]!='T' || s[13]!=':' || s[16]!=':' ) return -1;
+ int y=_number(s,0,4), m=_number(s,5,2), d=_number(s,8,2);
+ int h=_number(s,11,2), min=_number(s,14,2), sec=_number(s,17,2);
+ bool fraction=s[19]=='.';
+ int ms=fraction ? _number(s,20,3) : 0;
+ if( !valid(y,m,d,h,min,sec,ms) ) return -1;
+ return _parseZone_noInline(s,fraction ? 23 : 19,makeUTC(y,m,d,h,min,sec,ms));
+};
diff --git a/src/main/smp/sys/time/DateTime.smp b/src/main/smp/sys/time/DateTime.smp
new file mode 100644
index 00000000..5d117467
--- /dev/null
+++ b/src/main/smp/sys/time/DateTime.smp
@@ -0,0 +1,11 @@
+// Calendar view of an instant. Month/day start at 1; weekday is Sunday=0.
+// year==0 means out of the supported calendar range (years 1 through 9999).
+struct DateTime {
+ int !year=0, !month=0, !day=0;
+ int !hour=0, !minute=0, !second=0, !millisecond=0;
+ int !weekday=0, !offsetSeconds=0;
+ bool !daylight=false;
+};
+
+// Factory in the defining unit, so callers need no forward instance type.
+val empty = { -> new DateTime(); };
diff --git a/src/main/smp/sys/time/Timers.smp b/src/main/smp/sys/time/Timers.smp
new file mode 100644
index 00000000..b98c96ef
--- /dev/null
+++ b/src/main/smp/sys/time/Timers.smp
@@ -0,0 +1,55 @@
+// Cooperative timers. poll returns IDs; callbacks never run on another thread.
+struct Timers {
+ i64[] !_ids=new i64[8], !_due=new i64[8], !_period=new i64[8];
+ i64 !_next=1;
+
+ val _add = { i64 current, i64 delay, i64 period ->
+ if( current<0 || delay<0 || current>time.LIMIT-delay || _next==time.LIMIT ) return -1;
+ int slot=0;
+ while( slot<_ids# && _ids[slot]!=0 ) slot++;
+ if( slot==_ids# ) {
+ if( _ids#>1073741823 ) return -1;
+ val ids=new i64[_ids#*2], due=new i64[_ids#*2], periods=new i64[_ids#*2];
+ for( int i=0; i<_ids#; i++ ) { ids[i]=_ids[i]; due[i]=_due[i]; periods[i]=_period[i]; }
+ _ids=ids; _due=due; _period=periods;
+ // Retry against the newly published arrays after growing.
+ return _add(current,delay,period);
+ }
+ i64 id=_next++;
+ _ids[slot]=id; _due[slot]=current+delay; _period[slot]=period;
+ return id;
+ };
+ // Explicit-clock versions make game logic and tests deterministic.
+ val afterAt = { i64 current, i64 delay -> _add(current,delay,0); };
+ val everyAt = { i64 current, i64 period -> period<=0 ? -1 : _add(current,period,period); };
+ val after = { i64 delay -> afterAt(time.monotonic(),delay); };
+ val every = { i64 period -> everyAt(time.monotonic(),period); };
+ val cancel = { i64 id ->
+ if( id<=0 ) return false;
+ for( int i=0; i<_ids#; i++ ) if( _ids[i]==id ) { _ids[i]=0; return true; }
+ return false;
+ };
+ val nextDeadline = { ->
+ i64 next=-1;
+ for( int i=0; i<_ids#; i++ )
+ if( _ids[i]!=0 && (next<0 || _due[i]<next) ) next=_due[i];
+ return next;
+ };
+ // Earliest deadline first; equal deadlines use registration order.
+ // Repeats skip missed ticks and retain their phase, avoiding bursts/drift.
+ val poll = { i64 current ->
+ int slot=-1;
+ for( int i=0; i<_ids#; i++ )
+ if( _ids[i]!=0 && _due[i]<=current && (slot<0 || _due[i]<_due[slot] ||
+ (_due[i]==_due[slot] && _ids[i]<_ids[slot])) ) slot=i;
+ if( slot<0 ) return -1;
+ i64 id=_ids[slot], period=_period[slot];
+ if( period==0 ) _ids[slot]=0;
+ else {
+ i64 steps=(current-_due[slot])/period;
+ if( steps>=(time.LIMIT-_due[slot])/period ) _ids[slot]=0;
+ else _due[slot]+=(steps+1)*period;
+ }
+ return id;
+ };
+};
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..14a091ac 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter21Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter21Test.java
@@ -461,6 +461,7 @@ return i;
String src =
"""
val test_sqrt = { flt x ->
+ if( x==0.0 ) return 0.0; // Avoid 0/0: unordered comparisons must stay false.
flt epsilon = 1e-15;
flt guess = x;
while( 1 ) {
@@ -531,7 +532,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..6b892c65 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/time"},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..72e2cd9d
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/DrawTest.java
@@ -0,0 +1,221 @@
+package com.seaofnodes.simple;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Test;
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.util.Ary;
+import static org.junit.Assert.*;
+
+// Native raster and game checks; no backend or display is needed.
+public class DrawTest {
+ @Test public void nativeClockCoordinates() throws Exception {
+ String clock=Files.readString(Path.of("docs/examples/Clock.smp"))
+ .replace("val main =","val _windowMain =");
+ StringBuilder source=new StringBuilder(clock+"""
+ val _check_noInline={ f64 turn, int expectedX, int expectedY ->
+ int x=360+_sin_noInline(turn)*128, y=232-_sin_noInline(turn+0.25)*128;
+ if( x<expectedX-1 || x>expectedX+1 || y<expectedY-1 || y>expectedY+1 ) {
+ sys.io.p("turn %f(turn): %(x),%(y) expected %(expectedX),%(expectedY)\\n");
+ return 1;
+ }
+ return 0;
+ };
+ val main={ ->
+ """);
+ for(int i=0;i<=60;i++) {
+ double turn=i/60.0;
+ int x=(int)(360+clockSine(turn)*128), y=(int)(232-clockSine(turn+0.25)*128);
+ source.append("if(_check_noInline(").append(turn).append(',').append(x).append(',').append(y)
+ .append(")!=0) return 1;");
+ }
+ source.append("return 0; };");
+ TestC.runSYS(source.toString(),"clockCoordinates","",-1);
+ }
+
+ private static double clockSine(double turn) {
+ turn-=(long)turn;
+ boolean negative=turn>0.5;
+ double x=(negative ? turn-0.5 : turn)*6.283185307179586;
+ double product=x*(3.141592653589793-x);
+ return (negative ? -16.0 : 16.0)*product/(49.34802200544679-4*product);
+ }
+
+ @Test public void nativeAnalogSweep() throws Exception {
+ String clock=Files.readString(Path.of("docs/examples/Clock.smp"))
+ .replace("val main =","val _windowMain =");
+ StringBuilder source=new StringBuilder(clock+"""
+ val _renderCheck_noInline={ int second, int millisecond, int expectedX, int expectedY ->
+ val c=new sys.draw.Canvas(720,540);
+ _analog_noInline(c,3,20,second,millisecond);
+ var found=false;
+ for( int y=expectedY-2; y<=expectedY+2; y++ )
+ for( int x=expectedX-2; x<=expectedX+2; x++ )
+ if( c.pixels[y*720+x]==0xc3ffcb ) found=true;
+ if( !found ) {
+ sys.io.p("missing second hand %(second).%(millisecond) expected %(expectedX),%(expectedY)\\n");
+ return 1;
+ }
+ // Every dial mark/hand must stay within the dial's bounds.
+ for( int y=0; y<540; y++ ) for( int x=0; x<720; x++ )
+ if( (x<204 || x>516 || y<76 || y>388) && c.pixels[y*720+x]!=0 ) {
+ sys.io.p("stray pixel %(x),%(y) color %x(c.pixels[y*720+x]) at %(second).%(millisecond)\\n");
+ return 2;
+ }
+ return 0;
+ };
+ val main={ ->
+ """);
+ for(int millis:new int[]{0,500,7500,15000,30000,45000,59500}) {
+ double turn=millis/60000.0;
+ int x=(int)(360+clockSine(turn)*128), y=(int)(232-clockSine(turn+0.25)*128);
+ source.append("if(_renderCheck_noInline(").append(millis/1000).append(',').append(millis%1000)
+ .append(',').append(x).append(',').append(y).append(")!=0) return 1;");
+ }
+ source.append("return 0; };");
+ TestC.runSYS(source.toString(),"clockAnalogSweep","",-1);
+ }
+
+ @Test public void buttonInteractionAndRendering() throws Exception {
+ TestC.runSYS("""
+ val main={ ->
+ val b=new sys.draw.Button(10,10,60,30,"GO");
+ if( !b.contains(10,10) || b.contains(70,20) || b.contains(20,40) ) return 1;
+ if( b.update(20,20,true) || !b.pressed || b.update(20,20,true) ) return 2;
+ if( !b.update(20,20,false) || b.pressed || b.update(20,20,false) ) return 3;
+ if( b.update(0,0,true) || b.update(20,20,true) || b.update(20,20,false) ) return 4;
+ b.update(20,20,true);
+ if( b.update(0,0,false) ) return 5;
+ b.update(20,20,true); b.enabled=false;
+ if( b.update(20,20,true) || b.pressed || b.update(20,20,false) ) return 6;
+ b.enabled=true;
+ val c=new sys.draw.Canvas(90,60);
+ b.update(20,20,false); b.draw(c,2,9,3,5);
+ if( c.pixels[10*90+10]!=9 || c.pixels[13*90+13]!=5 || c.pixels[0]!=0 ) return 7;
+ b.update(0,0,false); b.draw(c,2,9,3,5);
+ if( c.pixels[13*90+13]!=3 ) return 8;
+ return 0;
+ };
+ ""","drawButton","",-1);
+ }
+
+ @Test public void clockFacesWithoutWindow() throws Exception {
+ String clock=Files.readString(Path.of("docs/examples/Clock.smp"))
+ .replace("val main =","val _windowMain =");
+ CodeGen code=new CodeGen(null,null,new Ary<>(new String[]{TestC.RELEASE_SYS_DIR}),"ClockRaster",clock+"""
+ val main={ ->
+ val c=new sys.draw.Canvas(720,540);
+ _digit_noInline(c,10,10,8);
+ if( c.pixels[10*720+40]!=GREEN || c.pixels[82*720+40]!=GREEN || c.pixels[154*720+40]!=GREEN ) return 1;
+ _digit_noInline(c,10,10,1);
+ if( c.pixels[10*720+40]!=DIM || c.pixels[40*720+78]!=GREEN || c.pixels[40*720+14]!=DIM ) return 2;
+ c.clear(BACKGROUND); _digital_noInline(c,23,59,58);
+ if( c.pixels[214*720+254]!=GREEN || c.pixels[262*720+470]!=GREEN ) return 3;
+ c.clear(BACKGROUND); _analog_noInline(c,3,0,0,0);
+ if( c.pixels[232*720+430]!=GREEN || c.pixels[160*720+360]!=0xc3ffcb || c.pixels[232*720+360]!=GREEN ) return 4;
+ if( _sin_noInline(0.25)<0.999 || _sin_noInline(0.75)>-0.999 || _sin_noInline(1.0)!=0.0 ) return 5;
+ return 0;
+ };
+ return main();
+ """,126L,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(code,0,3000000));
+ }
+
+ @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/FloatComparisonNativeTest.java b/src/test/java/com/seaofnodes/simple/FloatComparisonNativeTest.java
new file mode 100644
index 00000000..c0d8be3a
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/FloatComparisonNativeTest.java
@@ -0,0 +1,65 @@
+package com.seaofnodes.simple;
+
+import org.junit.Test;
+
+public class FloatComparisonNativeTest extends TestC {
+ @Test public void longFloatingBranches() throws Exception {
+ StringBuilder source=new StringBuilder("val far_noInline={f64 a,f64 b,i64[] out -> if(a<b){");
+ for(int i=0;i<32;i++) source.append("out[").append(i).append("]=").append(i+1).append(';');
+ source.append("""
+ return 1; } return 0; };
+ val main={ ->
+ val out=new i64[32];
+ if( far_noInline(1.0,2.0,out)!=1 || out[31]!=32 ) return 1;
+ out[31]=0;
+ if( far_noInline(2.0,1.0,out)!=0 || out[31]!=0 ) return 2;
+ f64 nan=1e300*1e300-1e300*1e300;
+ if( far_noInline(nan,1.0,out)!=0 || out[31]!=0 ) return 3;
+ if( far_noInline(1.0,nan,out)!=0 || out[31]!=0 ) return 4;
+ return 0;
+ };
+ """);
+ runSYS(source.toString(),"floatLongBranches","",-1);
+ }
+
+ @Test public void branchesAndValues() throws Exception {
+ String[] literals={"-1.0","0.0","2.0","1e300*1e300","-1e300*1e300",
+ "1e300*1e300-1e300*1e300"};
+ double[] values={-1,0,2,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY,Double.NaN};
+ StringBuilder source=new StringBuilder("""
+ val branch_noInline={ f64 a, f64 b ->
+ int bits=0;
+ if( a<b ) bits+=1;
+ if( a<=b ) bits+=2;
+ if( a==b ) bits+=4;
+ if( a!=b ) bits+=8;
+ if( a>=b ) bits+=16;
+ if( a>b ) bits+=32;
+ return bits;
+ };
+ val value_noInline={ f64 a, f64 b ->
+ val lt=a<b, le=a<=b, eq=a==b, ne=a!=b, ge=a>=b, gt=a>b;
+ return lt+le*2+eq*4+ne*8+ge*16+gt*32;
+ };
+ val check_noInline={ f64 a, f64 b, int expected, int tag ->
+ val branches=branch_noInline(a,b), values=value_noInline(a,b);
+ if( branches!=expected || values!=expected ) {
+ sys.io.p("case %(tag): branches %(branches), values %(values), expected %(expected)\\n");
+ return 1;
+ }
+ return 0;
+ };
+ val main={ ->
+ """);
+ for(int i=0;i<values.length;i++) for(int j=0;j<values.length;j++) {
+ double a=values[i],b=values[j];
+ int expected=(a<b ? 1 : 0)|(a<=b ? 2 : 0)|(a==b ? 4 : 0)|
+ (a!=b ? 8 : 0)|(a>=b ? 16 : 0)|(a>b ? 32 : 0);
+ source.append("if(check_noInline(").append(literals[i]).append(',').append(literals[j])
+ .append(',').append(expected).append(',').append(i*values.length+j)
+ .append(")!=0) return 1;");
+ }
+ source.append("return 0; };");
+ runSYS(source.toString(),"floatComparisons","",-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..66ced278
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/GraphicsTest.java
@@ -0,0 +1,127 @@
+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, clock;
+
+ @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);
+ clock = TestC.compile(Files.readString(Path.of("docs/examples/Clock.smp")),
+ "graphics_clock",paths,TestC.CALL_CONVENTION,null,null,-1);
+ }
+
+ private static String run(String exe, String backend, int exit) throws Exception {
+ return run(exe,backend,exit,null);
+ }
+
+ private static String run(String exe, String backend, int exit, Path cwd) throws Exception {
+ ProcessBuilder pb = new ProcessBuilder(Path.of(exe).toAbsolutePath().toString()).redirectErrorStream(true);
+ if (cwd!=null) pb.directory(cwd.toFile());
+ 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 {
+ Path dir=Files.createTempDirectory(Path.of("build/objs"),"graphics-missing-");
+ Path bin=Files.createDirectory(dir.resolve("bin"));
+ Path exe=Files.copy(Path.of(graphics),bin.resolve(Path.of(graphics).getFileName()));
+ String output = run(exe.toString(),null,1,dir);
+ assertTrue(output,output.contains("MiniFB backend not found"));
+ assertTrue(output,output.contains("make graphics"));
+ }
+
+ @Test public void discoverBackendWithoutConfiguration() throws Exception {
+ Path root=Files.createTempDirectory(Path.of("build/objs"),"graphics-discovery-");
+ String abi=TestC.OS.startsWith("Windows") ? "x86_64_v2_win64" : "x86_64_v2_SystemV";
+ String name=TestC.OS.startsWith("Windows") ? "simple_minifb.dll" : "libsimple_minifb.so";
+ Path source=root.resolve("backend.c");
+ Files.writeString(source,"""
+ #include "simple_graphics.h"
+ #include <stdio.h>
+ static int32_t open_window(const char *s,uint32_t w,uint32_t h) {
+ puts("discovered mock MiniFB"); return -1;
+ }
+ static int32_t present(const uint32_t *p,uint32_t n) { return 0; }
+ static int32_t poll_events(int32_t n) { return 0; }
+ static int32_t position(void) { return 0; }
+ static int32_t wait_events(int64_t n) { return 0; }
+ static void close_window(void) {}
+ static const struct simple_graphics_api api={SIMPLE_GRAPHICS_VERSION,
+ open_window,present,poll_events,position,position,poll_events,poll_events,close_window,wait_events};
+ const struct simple_graphics_api *simple_minifb_api(void) { return &api; }
+ """);
+ Path mock=root.resolve(name);
+ TestC.exec(30,"gcc","-shared","-fPIC","-Isrc/main/c",source.toString(),"-o",mock.toString());
+ Path cwd=Files.createDirectory(root.resolve("empty"));
+ Path bin=Files.createDirectory(root.resolve("bin"));
+ Path exe=Files.copy(Path.of(graphics),bin.resolve(Path.of(graphics).getFileName()));
+ Path beside=Files.copy(mock,bin.resolve(name));
+ assertTrue(run(exe.toString(),null,1,cwd).contains("discovered mock MiniFB"));
+ // Explicit bad overrides must not silently fall back to the mock.
+ assertTrue(run(exe.toString(),"no-such-backend",1,cwd).contains("cannot load MiniFB"));
+ Files.delete(beside);
+ Path release=Files.createDirectories(root.resolve("build/release/"+abi));
+ Files.copy(mock,release.resolve(name));
+ Path examples=Files.createDirectories(root.resolve("build/examples/"+abi));
+ Path example=Files.copy(exe,examples.resolve(exe.getFileName()));
+ assertTrue(run(example.toString(),null,1,cwd).contains("discovered mock MiniFB"));
+ assertTrue(run(exe.toString(),null,1,root).contains("discovered mock MiniFB"));
+ }
+
+ @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 clockWithoutBackend() throws Exception {
+ assertTrue(run(clock,"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..7b459e75 100644
--- a/src/test/java/com/seaofnodes/simple/StreamIOTest.java
+++ b/src/test/java/com/seaofnodes/simple/StreamIOTest.java
@@ -131,6 +131,10 @@ 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"));
+ gcc.add(runtimeObject("simple_time"));
+ 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/StringFormatTest.java b/src/test/java/com/seaofnodes/simple/StringFormatTest.java
index 53a10092..05e801dc 100644
--- a/src/test/java/com/seaofnodes/simple/StringFormatTest.java
+++ b/src/test/java/com/seaofnodes/simple/StringFormatTest.java
@@ -128,6 +128,10 @@ public class StringFormatTest extends TestC {
@Test public void testWrongTypesAndReservedNames() {
rejects("return \"%d(1.5)\";", "must be a");
+ rejects("return \"%t(1.5)\";", "must be a");
+ rejects("return \"%tQ(0)\";", "Expected a time format");
+ rejects("return \"%.2t(0)\";", "Precision is not supported");
+ rejects("return \"%+t(0)\";", "Invalid flags");
rejects("return \"%s(17)\";", "must be a");
rejects("return \"%(null)\";", "Cannot format");
rejects("val f={int int -> int;}; return 0;", "Expected an identifier");
diff --git a/src/test/java/com/seaofnodes/simple/TestC.java b/src/test/java/com/seaofnodes/simple/TestC.java
index dd7999c9..63bb98da 100644
--- a/src/test/java/com/seaofnodes/simple/TestC.java
+++ b/src/test/java/com/seaofnodes/simple/TestC.java
@@ -190,6 +190,11 @@ 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"));
+ params.add(runtimeObject("simple_time"));
+ 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.
diff --git a/src/test/java/com/seaofnodes/simple/TimeTest.java b/src/test/java/com/seaofnodes/simple/TimeTest.java
new file mode 100644
index 00000000..c3cdc6cf
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/TimeTest.java
@@ -0,0 +1,176 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.util.Ary;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class TimeTest extends TestC {
+ private static void checks(String body, String name) throws Exception {
+ runSYS("val main={ -> "+body+" return 0; };",name,"",-1);
+ }
+
+ @Test public void builtInFormats() throws Exception {
+ runSYS("""
+ val main={ ->
+ i64 t=sys.time.parse("2026-11-12T00:34:56.789Z");
+ sys.io.p("%t(t)|%tF(t)|%tr(t)|%tR(t)|%tM(83456)|%tD(t)|%tT(t)\\n");
+ sys.io.p("[%30t(0)][%-10tr(sys.time.hours(12))]\\n");
+ sys.io.p("%tr(0)|%tr(sys.time.hours(13))|%tM(-1)|%tM(-9223372036854775807-1)");
+ return 0;
+ };
+ ""","timeFormats",
+ "2026-11-12T00:34:56.789Z|2026-11-12T00:34:56.789Z|12:34am|00:34|01:23.456|12/11/26|00:34:56\n"+
+ "[ 1970-01-01T00:00:00.000Z][12:00pm ]\n"+
+ "12:00am|1:00pm|-00:00.001|-153722867280912:55.808",-1);
+ }
+
+ @Test public void calendarAgainstJava() throws Exception {
+ long[] values={-62135596800000L,-2203977600000L,-2203891200000L,-86400001,-86400000,-1,0,1,
+ 951782400000L,1709164800123L,1794443696789L,253402300799999L};
+ DateTimeFormatter iso=DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneOffset.UTC);
+ StringBuilder src=new StringBuilder("val show_noInline={i64 t -> sys.io.p(\"%t(t)|\"); val d=sys.time.utc(t); sys.io.p(\"%(d.weekday)\\n\"); }; val main={ -> ");
+ StringBuilder expected=new StringBuilder();
+ for (long value:values) {
+ src.append("show_noInline(").append(value).append(");");
+ var dt=Instant.ofEpochMilli(value).atOffset(ZoneOffset.UTC);
+ expected.append(iso.format(dt)).append('|').append(dt.getDayOfWeek().getValue()%7).append('\n');
+ }
+ src.append("return 0; };");
+ runSYS(src.toString(),"timeCalendar",expected.toString(),-1);
+ }
+
+ @Test public void parseAndConstruction() throws Exception {
+ checks("""
+ if( sys.time.parse("1970-01-01T00:00:00Z")!=0 ) return 1;
+ if( sys.time.parse("1970-01-01T01:00:00.123+01:00")!=123 ) return 2;
+ if( sys.time.parse("1969-12-31T16:00:00-08:00")!=0 ) return 3;
+ if( sys.time.parse("1969-12-31T23:59:59.999Z")!=-1 ) return 4;
+ if( sys.time.parse("1969-12-31T23:59:59.999+01:00")!=-3600001 ) return 5;
+ if( sys.time.parse("1970-01-01T00:00:30+00:00:30")!=0 ) return 6;
+ if( sys.time.parse("2000-02-29T00:00:00Z")!=951782400000 ) return 7;
+ if( sys.time.makeUTC(2024,2,29,0,0,0,123)!=1709164800123 ) return 8;
+ if( sys.time.makeUTC(1900,2,29,0,0,0,0)!=-1 || sys.time.makeUTC(2024,2,29,24,0,0,0)!=-1 ) return 9;
+ if( sys.time.utc(sys.time.MIN-1).year!=0 || sys.time.utc(sys.time.MAX+1).year!=0 ) return 10;
+ ""","timeParse");
+ String[] bad={"","now","2026-11-12","2026-11-12T00:00:00","0000-01-01T00:00:00Z",
+ "1900-02-29T00:00:00Z","2026-02-29T00:00:00Z","2026-13-01T00:00:00Z",
+ "2026-11-00T00:00:00Z","2026-11-12T24:00:00Z","2026-11-12T00:60:00Z",
+ "2026-11-12T00:00:60Z","2026-11-12T00:00:00.1Z","2026-11-12T00:00:00.1234Z",
+ "2026-11-12T00:00:00+24:00","2026-11-12T00:00:00+01:60","2026-11-12T00:00:00Zx",
+ "2026-11-12T00:00:00.00xZ","0001-01-01T00:00:00+01:00"};
+ StringBuilder src=new StringBuilder("val main={ -> ");
+ for (int i=0;i<bad.length;++i)
+ src.append("if(sys.time.parse(\"").append(bad[i]).append("\")!=-1) return ").append(i+1).append(';');
+ src.append("return 0; };");
+ runSYS(src.toString(),"timeBadParse","",-1);
+ }
+
+ @Test public void durationArithmetic() throws Exception {
+ checks("""
+ if( sys.time.seconds(2)!=2000 || sys.time.minutes(2)!=120000 || sys.time.hours(2)!=7200000 || sys.time.days(2)!=172800000 ) return 1;
+ if( sys.time.toSeconds(-1999)!=-1 || sys.time.toMinutes(119999)!=1 || sys.time.toHours(7200000)!=2 || sys.time.toDays(172800000)!=2 ) return 2;
+ if( sys.time.seconds(9223372036854775)!=9223372036854775000 || sys.time.seconds(-9223372036854775)!=-9223372036854775000 ) return 3;
+ if( sys.time.seconds(9223372036854776)!=-1 || sys.time.seconds(-9223372036854776)!=-1 ) return 4;
+ if( sys.time.remaining(5,10)!=0 || sys.time.remaining(10,5)!=5 || sys.time.remaining(9223372036854775807,-1)!=9223372036854775807 ) return 5;
+ if( sys.time.fromNanos(-1999999)!=-1 || sys.time.toNanos(1234)!=1234000000 || sys.time.toNanos(9223372036855)!=-1 ) return 6;
+ ""","timeArithmetic");
+ }
+
+ @Test public void timerOrderingAndMissedTicks() throws Exception {
+ checks("""
+ val q=new sys.time.Timers();
+ if( q.poll(0)!=-1 || q.nextDeadline()!=-1 ) return 1;
+ i64 a=q.afterAt(100,20), b=q.afterAt(100,10), c=q.afterAt(100,10);
+ if( q.nextDeadline()!=110 || q.poll(109)!=-1 || q.poll(110)!=b || q.poll(110)!=c || q.poll(110)!=-1 ) return 2;
+ if( !q.cancel(a) || q.cancel(a) || q.cancel(-1) || q.nextDeadline()!=-1 ) return 3;
+ i64 repeat=q.everyAt(100,10);
+ if( q.poll(135)!=repeat || q.nextDeadline()!=140 || q.poll(135)!=-1 ) return 4;
+ if( q.poll(140)!=repeat || q.nextDeadline()!=150 || !q.cancel(repeat) ) return 5;
+ for( int i=0; i<20; i++ ) if( q.afterAt(200,i)<0 ) return 11;
+ i64 prev=-1;
+ for( int i=0; i<20; i++ ) { i64 id=q.poll(300); if( id<=prev ) return 6; prev=id; }
+ if( q.nextDeadline()!=-1 || q.poll(1000)!=-1 ) return 7;
+ if( q.afterAt(-1,1)!=-1 || q.afterAt(0,-1)!=-1 || q.everyAt(0,0)!=-1 || q.afterAt(9223372036854775807,1)!=-1 ) return 8;
+ i64 last=q.everyAt(9223372036854775806,1);
+ if( last<0 || q.poll(9223372036854775807)!=last || q.nextDeadline()!=-1 ) return 9;
+ ""","timeTimers");
+ }
+
+ private static String run(String source, String name, String zone) throws Exception {
+ String exe=compile(source,name,new Ary<>(new String[]{RELEASE_SYS_DIR}),CALL_CONVENTION,null,null,-1);
+ ProcessBuilder pb=new ProcessBuilder(exe).redirectErrorStream(true);
+ if (zone!=null) pb.environment().put("TZ",zone);
+ Process child=pb.start();
+ try {
+ assertTrue("time 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 localZoneAndDaylightSaving() throws Exception {
+ String out=run("""
+ val show_noInline={u8[~] s ->
+ i64 t=sys.time.parse(s); val d=sys.time.local(t);
+ sys.io.p("%(sys.time.formatLocal(t,sys.time.ISO))|%(d.offsetSeconds)|%b(d.daylight)|%(sys.time.zoneName(t))\\n");
+ };
+ val main={ -> show_noInline("2026-01-01T00:00:00Z"); show_noInline("2026-07-01T00:00:00Z"); return 0; };
+ ""","timeZone","EST5EDT,M3.2.0/2,M11.1.0/2");
+ assertEquals("2025-12-31T19:00:00.000-05:00|-18000|false|EST\n2026-06-30T20:00:00.000-04:00|-14400|true|EDT\n",out);
+ }
+
+ @Test public void liveClocksAndWaits() throws Exception {
+ String out=run("""
+ val main={ ->
+ i64 start=sys.time.monotonic();
+ i64 ns=sys.time.nanoTime();
+ if( start<0 || ns/1000000<start || ns/1000000>sys.time.monotonic() ) return 1;
+ if( sys.time.sleep(10)!=0 || sys.time.monotonic()-start<10 ) return 2;
+ i64 deadline=sys.time.monotonic()+10;
+ if( sys.time.sleepUntil(deadline)!=0 || sys.time.monotonic()<deadline ) return 3;
+ if( sys.time.sleepUntil(0)!=0 || sys.time.sleep(-1)!=-1 || sys.time.sleepUntil(-1)!=-1 ) return 4;
+ i64 wall=sys.time.now();
+ if( wall<1700000000000 || wall>4102444800000 ) return 5;
+ return 0;
+ };
+ ""","timeClocks",null);
+ assertEquals("",out);
+ }
+
+ @Test public void pureCalendarInEvaluator() {
+ CodeGen code=new CodeGen("src/main/smp",null,null,"TimeEval","""
+ val s="%t(-1)|%tM(1234)";
+ val expected="1969-12-31T23:59:59.999Z|00:01.234";
+ if( s#!=expected# ) return 1;
+ for( int i=0; i<s#; i++ ) if( s[i]!=expected[i] ) return 2;
+ return 0;
+ """,123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(code,0,100000));
+ }
+
+ @Test public void wideLibrarySerialization() throws Exception {
+ // A growing stdlib crossed the historical byte-sized alias/count limit.
+ // Exercise a separately compiled library whose live memory spans it.
+ StringBuilder source=new StringBuilder("struct Wide { ");
+ for (int i=0;i<300;++i) source.append("int !f").append(i).append("=0;");
+ source.append("}; val make_noInline={int x -> val v=new Wide();");
+ for (int i : new int[]{0,255,299}) source.append("v.f").append(i).append("=x+").append(i).append(';');
+ source.append("return v; };");
+ Path library=Files.createTempDirectory(Path.of("build/objs"),"wide-memory-");
+ new CodeGen(null,library.toString(),null,"WideMemory",source.toString(),123,true,TypeInteger.BOT)
+ .driver(CPU_PORT,CALL_CONVENTION,false,false);
+ run("val main={ -> val v=WideMemory.make_noInline(7); return v.f0!=7 || v.f255!=262 || v.f299!=306; };",
+ "timeWideMemory",new Ary<>(new String[]{library.toString(),RELEASE_SYS_DIR}),
+ CALL_CONVENTION,null,null,"",-1);
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java b/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java
index b255bcf7..b0f0f225 100644
--- a/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java
+++ b/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java
@@ -61,7 +61,7 @@ public class PrintRegTestSupport {
visit.put(marker,t);
var method = com.seaofnodes.simple.print.ASMPrinter.class.getDeclaredMethod(
"printConstantPool",int.class,com.seaofnodes.simple.util.SB.class,
- com.seaofnodes.simple.util.BAOS.class,java.util.HashMap.class,boolean.class,String.class);
+ com.seaofnodes.simple.util.BAOS.class,java.util.Map.class,boolean.class,String.class);
method.setAccessible(true);
try {
var sb = new com.seaofnodes.simple.util.SB();