diff options
80 files changed, 4554 insertions, 372 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 @@ -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)。 @@ -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/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/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(); |
